Go中没有引用类型,只有值传递和指针;slice、map等“引用语义”源于其底层结构含指针字段,但本身是值类型,不可取地址。

Go 里没有“引用类型”,只有指针和值传递
Go 语言根本不存在 C++ 或 C# 意义上的“引用类型”——int、string、struct、map、slice 全部按值传递。所谓“引用语义”,只是因为某些内置类型(如 slice、map、chan、func、interface{})的底层结构体里包含指针字段,导致它们在赋值或传参时“看起来像引用”。但它们本身不是指针,也不能取地址(&m 对 map 是非法的)。
常见错误现象:
• 以为 map 是引用类型,就试图对它做 &m 传参,结果编译报错:cannot take the address of m
• 修改函数内 slice 的元素能影响外部,就误以为 slice 是“引用”,但扩容后原变量不变——因为底层数组指针被复制了,扩容会分配新数组
- 真正能取地址、能显式解引用的,只有
*前缀定义的指针类型,比如*int、*MyStruct -
new(T)和&T{}效果等价,都返回*T;但make([]int, 0)返回的是值([]int),不是指针 - 性能上:传大 struct 时,
*T比T更轻量;但小 struct(如两个int)直接传值反而更快,避免间接寻址开销
什么时候必须用 * 指针?
不是“想改就加星号”,而是看是否需要修改**调用方变量所指向的内存内容**。如果只是读或局部修改,值传递足够;如果要让函数内修改反映到调用方的原始变量上,才需要指针。
使用场景举例:
• 实现类似 swap(&a, &b) 这种交换两个变量的值
• 方法接收者需要修改结构体字段(比如 user.SetName("x"))
• 避免大对象拷贝(如含千字节字段的 struct)
立即学习“go语言免费学习笔记(深入)”;
- 方法接收者用
func (u *User) Save()而不是func (u User) Save(),否则u.Name = "x"只改副本 - 传
*[]byte是罕见且危险的——通常应传[]byte并靠返回值更新(b = append(b, x)),因为 slice 本身已含指针 - 初始化后立即解引用空指针(
var p *int; *p = 1)会 panic:invalid memory address or nil pointer dereference
slice 和 map 的“假引用”行为怎么理解?
它们是值类型,但内部结构含指针:一个 slice 是三字长结构体(ptr、len、cap),map 是一个 *hmap 指针包装。所以赋值时复制的是这个结构体,其中的指针字段仍指向同一片底层数据。
容易踩的坑:
• s2 := s1; s2[0] = 99 → s1[0] 也变,因为共用底层数组
• s2 = append(s2, 1) 后再改 s2[0],s1 不受影响——可能已触发扩容,s2.ptr 指向新数组
• m2 := m1; m2["k"] = "v" 总是影响 m1,因为 m2 和 m1 的底层 *hmap 指针相同
-
len和cap是slice值的一部分,修改s2的len(如切片操作s2 = s2[1:])不影响s1.len -
map不能比较(==报错),也不能作为map的 key,因为其底层指针无法安全哈希 - 想真正“深拷贝”
map或slice,得手动遍历赋值,或用json.Marshal/Unmarshal(注意性能和类型限制)
GC 和逃逸分析怎么影响指针使用?
Go 编译器会根据变量生命周期决定分配在栈还是堆:如果函数返回了某个局部变量的指针,该变量必然逃逸到堆;反之,没被取地址的局部变量大概率留在栈上。
实操建议:
• 用 go build -gcflags="-m" 看逃逸分析结果,例如 ./main.go:12:2: &x escapes to heap
• 不要为了“省拷贝”盲目加 *:如果本可栈分配的变量因此逃逸,反而增加 GC 压力
• 接口值(io.Reader 等)存储具体类型时,若该类型是指针,则接口内保存指针;若是值,则保存副本——这也影响逃逸
- 返回局部变量地址一定会逃逸,但返回局部指针变量的值(如
return p,其中p本身是*int)不等于逃逸——要看p指向哪 - 闭包捕获变量时,若捕获的是指针,那它指向的内存只要闭包还活着就不会被回收
- 大量小对象频繁分配+指针持有,容易导致堆碎片和 GC 频繁,比单纯栈拷贝更重
事情说清了就结束。真正难的不是记住“指针怎么写”,而是判断某个变量是否该被共享、是否需跨作用域修改、以及那个“共享”到底是靠指针、靠内置类型的隐式指针字段,还是靠 channel 通信——这三者语义完全不同。










