Go中所有参数均为值传递,传递的是slice/map/channel/interface/指针的头信息或地址副本,而非数据本体或C++式引用;修改元素生效,但重新赋值或append不影响原变量。

Go 里所有参数都是值传递,包括 slice、map、channel、interface 和指针
这是最容易误解的点:看到 slice 被函数修改后原变量也变了,就以为是“传引用”。其实不是。Go 没有引用传递,只有值传递——但传的是「头信息结构体」的副本。比如 slice 底层是 struct { ptr *T; len, cap int },这个结构体被复制了,所以修改 slice[i] 会影响原底层数组,但对 slice 本身做 append 后重新赋值,就影响不到调用方。
常见错误现象:
- 修改 slice 元素生效,但 append 后长度没变
- 向函数传 map,增删 key 生效,但给形参重新赋值 m = make(map[string]int) 不影响外层
- 传指针,改 *p 生效,但让 p = &x 不影响外层指针变量
- 本质:传的是 header(头)或地址的副本,不是数据本体的副本,也不是 C++ 那种引用类型
- 性能影响:传
slice/map很轻量(通常 24 字节以内),不拷贝底层数组或哈希表 - 如果真想让函数能替换整个容器(比如重分配
slice),必须传*[]T或返回新值
什么时候必须传指针才能修改原始值
只有当你需要在函数内改变「变量所指向的内存地址的内容」,且该内容不属于 header 结构时,才需要指针。典型场景是修改 struct 字段、数组元素(非 slice)、基础类型变量。
使用场景:
- 更新一个 struct 的多个字段,避免返回大结构体
- 修改调用方的 int、string(注意:string 本身是只读 header,要改内容只能换整个 string)
- 实现类似 json.Unmarshal 这种需要写入目标内存的函数
-
func incr(n *int) { *n++ }可以改变调用方的int;传int就不行 -
func fill(s *[1000]byte) {}传数组会拷贝 1000 字节;传*[1000]byte只拷贝 8 字节指针 - 不要为了“省拷贝”盲目传指针——小 struct(如
type Point struct{ x,y float64 })按值传反而更快
slice、map、channel 的“伪引用行为”怎么安全利用
它们的“可修改性”来自底层共享,但边界很明确:header 复制,数据不复制。只要不越界、不触发扩容或重建,就能安全复用。
容易踩的坑:
- append 可能导致底层数组换新,原 slice 失去连接
- 多个 slice 共享同一底层数组,一个改内容,其他都看到——这常被忽略,引发并发或逻辑 bug
- map 是运行时动态分配的,传 map 值本身不会 panic,但 nil map 写入会 panic
- 检查是否共享底层数组:
unsafe.SliceHeader对比ptr字段(仅调试用) - 要隔离修改,用
copy(dst, src)构造新底层数组 - 并发读写
map必须加锁或改用sync.Map;slice同理,尤其涉及append
interface{} 传参时的隐藏开销和陷阱
传 interface{} 本质是传两个字:类型信息 + 数据指针(或小值直接塞进去)。对大 struct 来说,可能触发一次内存分配;对小值(如 int)则无额外开销。
立即学习“go语言免费学习笔记(深入)”;
典型问题:
- 函数接收 interface{},内部做类型断言失败,panic 或返回 false
- 把指针传进 interface{},再取地址,得到的是接口内部的副本地址,不是原变量地址
- fmt.Printf("%p", &v) 打印 interface{} 变量的地址,和它装的值的地址不是一回事
- 避免无谓装箱:如果函数只处理
string,别定义成接收interface{} - 反射操作
interface{}中的指针值,要用reflect.Value.Elem()解一层 - 性能敏感路径慎用
interface{},尤其是高频调用函数
真正难的不是记住“Go 全是值传递”,而是判断某个具体类型在某次调用中,值传递的“值”到底包含什么——是整个数组?还是三个机器字的 header?这得看类型底层实现,而不是语法表象。










