go函数参数全是值传递,包括slice、map、chan等所谓“引用类型”,本质是拷贝含指针的结构体;修改底层数据可见,修改结构体字段(如append扩容、map重赋值)不影响外部。

Go函数参数全是值传递,别被“引用类型”骗了
Go 没有引用传递,所有函数参数都是值传递——包括 []int、map[string]int、chan int。所谓“能改内容”,只是因为你传进去的那个值,本身是个含指针的结构体(比如 slice 是三个字段:ptr、len、cap),拷贝后,ptr 字段仍指向原底层数组。
常见错误现象:append(s, x) 后外部 slice 没变;m["k"] = v 却生效了。原因不是“slice 是引用类型”,而是:append 可能修改 ptr 字段(扩容时换底层数组),而你没接收返回值;m["k"] = v 只改哈希表数据,不改 map 结构体本身的字段。
-
func f(s []int) { s = append(s, 1) }:形参s是副本,扩容后s.ptr指向新地址,但调用方变量不变 -
func f(s []int) { s[0] = 99 }:改的是s.ptr指向的同一块内存,外部可见 -
func f(m map[string]int) { m["x"] = 1 }:map 头部结构体被拷贝,但其中指针仍指向原哈希表,写入生效 -
func f(m map[string]int) { m = make(map[string]int) }:只改了副本的头部指针,不影响外部
struct 传值 or 传指针?看大小和是否要改原值
struct 是纯值类型,传参即拷贝全部字段。小 struct(如 type Point struct{ X, Y int })按值传更安全、GC 压力小;大 struct(含 []byte、嵌套 map 或字段总大小 > 64 字节)传 *T 避免意外拷贝开销。
真正决定要不要用指针的,不是“它是不是 struct”,而是两个信号:
- 需要在函数内修改字段并让调用方看到 → 必须传
*T - struct 实例太大(编译器提示逃逸或实测性能下降)→ 优先考虑
*T - 哪怕 struct 很小,只要它字段里含 slice/map/chan,这些字段内部指针会被拷贝,但 struct 本身仍是副本 —— 所以
s.Fields = nil不影响外部,但s.Data[0] = 1可能影响
string 是只读值类型,不是引用类型
string 在 Go 中由两个机器字组成:ptr(指向底层 []byte)和 len。赋值时拷贝这两个字段,但底层字节数组不可变。所以它既高效(避免重复拷贝数据),又安全(不会意外共享可变状态)。
常见误判:
-
s1 := "hello"; s2 := s1:s2拷贝了ptr和len,但底层字节数组未复制(因为只读) -
s1 := "hello"; s2 := s1[:3]:生成新 string,ptr指向原数组同一位置,len=3—— 共享底层内存,但无法通过任一 string 修改它 - 误以为
string是引用类型,试图用指针去“避免拷贝”:没必要,且增加 GC 开销(指针本身要追踪)
逃逸分析决定变量在栈还是堆,不是类型决定的
值类型不一定在栈上,引用类型的“头”也不一定在堆上。分配位置由逃逸分析决定:如果变量生命周期超出当前函数(比如返回局部变量地址、被闭包捕获、大小不确定),就逃逸到堆;否则默认在栈上。
验证方式:go build -gcflags "-m" 查看逃逸信息。例如:
-
func f() *int { x := 1; return &x }→x逃逸,分配在堆 -
func f() []int { return make([]int, 10) }→ 底层数组逃逸(因需跨函数存在),但 slice 头部结构体可能仍在栈上 -
var x int在全局 → 直接在 data 段,不涉及栈/堆
真正容易被忽略的点:即使你传的是 *T,如果 T 很大且没逃逸,*T 指向的仍是栈内存 —— 此时返回该指针是危险的;而一个很小的 struct,若被闭包捕获,也会逃逸到堆。别只盯着类型,要看实际使用方式。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











