Go 保留了指针的核心能力(间接寻址、传地址),但砍掉了指针算术和引用类型 &T,换来内存安全和 GC 兼容。
C++ 对照速查表
| 概念 | C++ | Go | 差异说明 |
|---|---|---|---|
| 声明指针 | int* p / int *p | var p *int | Go 把 * 放在类型前 |
| 取地址 | &x | &x | 完全相同 |
| 解引用 | *p | *p | 完全相同 |
| 指针算术 | p++, p + n | 不支持 | Go 禁止,用切片代替 |
| 空指针 | NULL / nullptr | nil | Go 没有 NULL 关键字 |
| 引用类型 | int& ref = x | 不存在 | Go 没有引用,统一用指针 |
| new | new int (调用构造) | new(int) → *int | Go 的 new 只分配零值内存,不调用构造函数 |
| struct 访问 | ptr->field | ptr.field | Go 编译器自动解引用,不需要 -> |
| make | 无对应物 | make([]int, 5) | 仅用于 slice/map/channel,返回初始化后的值(不是指针) |
1. 指针基础:&、*、*T
package main
import "fmt"
func main() {
x := 42
var p *int = &x // p 是指向 x 的指针,*int 表示"指向 int 的指针"
fmt.Println("x 的值:", *p) // 解引用:通过指针读取值 → 42
fmt.Println("x 的地址:", p) // 打印地址 → 0xc0000b4008(类似)
*p = 100 // 通过指针修改原变量
fmt.Println("修改后 x:", x) // → 100
// 零值:指针的零值是 nil
var q *int
fmt.Println("q == nil:", q == nil) // → true
}
要点:*int 的 * 是类型修饰符(“int 的指针”),*p 的 * 是操作符(“解引用”)。
2. 指针零值 nil
var p *int // nil,不指向任何内存
// 常见用法:用 nil 表示"无值"
type Node struct {
Val int
Next *Node // 链表场景,默认 nil 表示没有下一个节点
}
// 判断是否为空
if p == nil {
fmt.Println("p 是空指针")
}
C++ 对照:
nullptr/NULL。Go 中 nil 可以和指针、slice、map、channel、interface 比较。
3. 指针不能做算术运算
arr := [3]int{10, 20, 30}
p := &arr[0]
// ❌ 以下全部编译报错:
// p++
// p + 1
// p - 1
// ✅ 正确做法:用切片遍历
for i, v := range arr {
fmt.Printf("arr[%d] = %d\n", i, v)
}
// 如果确实需要偏移访问,用索引
fmt.Println(arr[1]) // → 20
这是 Go 最重要的安全设计之一:彻底杜绝了 C/C++ 中指针越界的可能。
4. new 函数
// new(T) 分配零值内存,返回 *T(指向该内存的指针)
p := new(int) // *int,指向 0
q := new(string) // *string,指向 ""
r := new([]int) // *[]int,指向 nil slice
fmt.Println(*p, *q, *r) // → 0 "" []
// 等价写法(更常用):
p2 := &int{} // 字面量取地址,效果相同
与 C++ new 的区别:
- C++
new int调用构造函数,可能得到未定义值(POD 类型) - Go
new(int)一定给你零值(0、false、""、nil),GC 保障生命周期
5. make 与 new 的区别
// new(T) → 返回 *T(指针),零值
p := new([]int) // *[]int,底层 slice 是 nil
// make → 返回 T(值),初始化后的值
s := make([]int, 5) // []int{0, 0, 0, 0, 0},长度 5
m := make(map[string]int) // 空但已初始化的 map,可以安全使用
ch := make(chan int) // 已初始化的 channel
// 典型错误:对 map 用 new
m2 := new(map[string]int)
// (*m2)["key"] = 1 // ❌ panic! map 是 nil,没有初始化
// 正确做法
m3 := make(map[string]int)
m3["key"] = 1 // ✅ 安全
new(T) | make(T, ...) | |
|---|---|---|
| 返回类型 | *T(指针) | T(值本身) |
| 内存状态 | 零值 | 初始化(分配底层数据结构) |
| 适用类型 | 任意类型 | 仅 slice、map、channel |
| 类比 C++ | new T() | 类似构造函数内部的初始化 |
6. 指针作为函数参数(引用传递效果)
// C++: void increment(int& x) 或 void increment(int* x)
// Go: 没有引用,统一用指针
func increment(p *int) {
*p++ // 通过指针修改原值
}
func main() {
x := 10
increment(&x)
fmt.Println(x) // → 11
}
// 实际应用:交换函数
func swap(a, b *int) {
*a, *b = *b, *a
}
// 多返回值 + 指针的惯用写法
func divmod(a, b int) (result int, ok bool) {
if b == 0 {
return 0, false // ok = false 表示失败
}
return a / b, true
}
Go 没有"按引用传递"的概念,所有参数都是值传递。指针只是传递了"地址的副本",效果类似引用。
7. 指针与结构体:自动解引用
type Point struct {
X, Y int
}
func main() {
p := Point{X: 1, Y: 2}
pp := &p // pp 是 *Point
// C++ 中需要 pp->X,Go 中直接 pp.X(编译器自动解引用)
fmt.Println(pp.X) // → 1
pp.X = 10 // 直接修改,不需要 (*pp).X
// 调用方法也自动解引用
p.Print() // ✅ 值接收者,编译器自动取地址
pp.Print() // ✅ 指针接收者,直接调用
}
func (p Point) Print() {
fmt.Printf("(%d, %d)\n", p.X, p.Y)
}
这是 Go 的语法糖:编译器会在需要时自动插入
&或*,让*T和T在方法调用上行为一致。
8. unsafe.Pointer 简介
import "unsafe"
func main() {
x := 42
p := unsafe.Pointer(&x)
// 可以转换为任意指针类型(绕过类型系统)
p2 := (*float64)(p)
fmt.Println(*p2) // → 42(按 float64 解释同一块内存,结果无意义)
// 可以做偏移运算(唯一的指针算术入口)
arr := [3]int{10, 20, 30}
base := unsafe.Pointer(&arr[0])
offset := unsafe.Pointer(uintptr(base) + unsafe.Sizeof(arr[0]))
p3 := (*int)(offset)
fmt.Println(*p3) // → 20
}
使用建议:
- 绝大多数场景不需要
unsafe.Pointer - 它绕过了类型安全和 GC 的保护,可能导致不可预期的行为
- 典型使用场景:
reflect包内部、与 C 交互(CGo)、极致性能优化 - 如果你觉得自己需要用它,先考虑是否有更 Go 的写法
常见坑
1. nil 指针解引用 → panic
var p *int
// fmt.Println(*p) // ❌ panic: runtime error: invalid memory address
C++ 中解引用空指针是未定义行为(段错误/数据损坏)。Go 中是确定的 panic,不会造成内存损坏。
2. new 返回零值,不调用"构造函数"
type User struct {
Name string
Age int
}
u := new(User)
fmt.Println(u.Name) // → ""(零值),不是 "default"
// 如果需要初始化,用结构体字面量
u2 := &User{Name: "张三", Age: 25}
3. 指针比较是比较地址
a := 10
b := 10
pa := &a
pb := &b
fmt.Println(pa == pb) // → false(两个不同变量,地址不同)
// 同一个变量
pc := &a
fmt.Println(pa == pc) // → true(同一个地址)
4. 返回局部变量的指针是安全的
func newUser() *User {
u := User{Name: "李四"} // 局部变量
return &u // ✅ Go 允许!编译器自动逃逸到堆
}
// C++ 中这是悬垂指针(dangling pointer),Go 由 GC 管理,完全安全
5. *T 和 T 的选择
| 场景 | 推荐 | 原因 |
|---|---|---|
| 基本类型(int, bool, float64) | T | 值很小,指针反而多一次间接寻址 |
| 小结构体(< 3 个字段,总共 < 32 字节) | T | 复制成本低,避免 GC 压力 |
| 大结构体 | *T | 避免大值拷贝 |
| 需要修改原值 | *T | 值传递的副本改不了原值 |
| struct 方法需要修改接收者 | *T(指针接收者) | 否则修改只作用于副本 |
| slice / map / channel | T | 本身就是引用语义(内部含指针) |
经验法则:如果拿不定主意,先用值类型
T,需要时再改为*T。Go 编译器和go vet会提醒你。
练习任务
- 声明一个
*int变量,用&取地址赋值,用*解引用读取 - 写一个
swap(a, b *int)函数,用指针交换两个整数的值 - 用
new分配一个结构体指针,验证字段是零值 - 对比
new([]int)和make([]int, 3)的区别(打印各自的类型和值) - 写一个
Person结构体,分别用值接收者和指针接收者实现String() string方法,观察调用差异 - 写一个函数接收
*Person,在函数内修改字段,验证原值是否被修改 - 用
unsafe.Pointer将*int转为*float64,观察输出(理解类型系统外的操作) - 思考:为什么 slice、map、channel 不需要传指针就能在函数内修改?(提示:内部结构)
豫公网安备41108202000982号