
本文深入剖析 go 语言中函数参数传递的本质差异,阐明为何传入指针可修改原始变量,而传入值类型(即使在函数内取其地址并修改)却无法影响调用方变量,并通过对比代码与内存行为揭示 go “始终按值传递”的核心原则。
本文深入剖析 go 语言中函数参数传递的本质差异,阐明为何传入指针可修改原始变量,而传入值类型(即使在函数内取其地址并修改)却无法影响调用方变量,并通过对比代码与内存行为揭示 go “始终按值传递”的核心原则。
Go 语言有一个常被误解但极其关键的原则:所有函数参数都按值传递(pass by value)。这意味着:无论你传的是 int、string 还是 *int,函数接收到的永远是一个副本——只是这个“副本”的内容决定了它能否间接影响原始数据。
✅ 正确方式:传入指针,修改所指向的内存
在第一段示例中,函数签名是 func add1(a *int) int,你显式传入的是变量 x 的地址(&x):
func add1(a *int) int {
*a = *a + 1 // 解引用后直接修改 a 所指向的内存位置(即 x 的存储单元)
return *a
}
func main() {
x := 3
x1 := add1(&x) // 传入 x 的地址
fmt.Println("x =", x) // 输出: x = 4 ← 原始变量被修改
}这里,a 是一个指针类型的值副本(例如,a 的值可能是 0xc0000140b0),但它指向的仍是原始变量 x 在内存中的同一位置。因此 *a = ... 实质上是在原地写入,效果等同于直接操作 x。
❌ 错误理解:传入值后再取地址,无法回溯原始变量
第二段代码看似“也用了指针”,实则逻辑完全不同:
func add1(a int) int {
p := &a // p 指向的是形参 a(x 的值拷贝)的栈地址,不是 x 本身!
*p = *p + 1 // 修改的是局部变量 a 的值(在函数栈帧内)
return *p
}
func main() {
x := 3
x1 := add1(x) // 传入的是 x 的值 3,a 是独立的 int 变量
fmt.Println("x =", x) // 输出: x = 3 ← x 完全未被触及
}关键点在于:
- a 是 x 的独立副本,位于 add1 函数自己的栈帧中;
- &a 获取的是这个局部副本的地址,而非 x 的地址;
- 因此 *p = ... 只修改了栈帧内的 a,函数返回后该栈帧被销毁,x 毫无影响。
? 类比理解:第一种像把快递柜钥匙(指针)交给维修工,他能直接打开你的柜子(原始内存)修理;第二种像你复印了一份钥匙(值拷贝),维修工用复印件配了一把新钥匙去开他自己家的柜子——和你的柜子毫无关系。
⚠️ 注意事项与最佳实践
- 不要混淆“指针变量的值”和“指针所指向的对象”:Go 中 *int 类型变量本身也是按值传递的,但它的“值”恰好是一个内存地址。
- 性能考量:对于小类型(如 int, bool, struct{}),传值开销极小;对于大结构体或切片/映射/通道等引用类型,传指针更高效且语义清晰(明确表示需修改)。
- 语义优先:是否使用指针,首要依据是设计意图——若函数需修改调用方状态,应接收指针;若仅读取或计算,优先传值以保证不可变性与线程安全。
✅ 总结一句话
Go 中没有“传引用”,只有“传值”;但当你传入一个指针值时,你传的是一个能定位到原始数据的地址——这才是修改生效的根本原因。










