很多刚接触 Go 的开发者,都会在“值传递”和“引用传递”上卡一会儿:为什么传入 int 改不动原值,传入 slice 却像是能改到外面?问题不在于 Go 存在两套传参机制,而在于不同类型被复制后,携带的数据含义并不一样。把这件事拆开看清之后,你就能判断什么时候直接传值,什么时候该用指针,什么时候又要特别留意切片的 append 行为。
Go 里其实只有一种传参方式
先说结论:Go 语言中,所有函数参数传递都是值传递。
这里的“值传递”,可以理解成调用函数时,实参会被复制一份,再交给函数内部使用。函数里改的是这份副本,默认不会直接改到外部变量本身。
这个规则先放到基本类型上看,最容易理解。像 int、float、bool、string,以及数组、结构体,传入函数时复制的就是它们自己的值:
func modifyValue(num int) {
num = 100
}
func main() {
value := 5
modifyValue(value)
fmt.Println(value) // 输出:5,原始值未改变
}
在这个例子里,modifyValue 收到的是 value 的副本。函数里把 num 改成 100,只影响副本,不影响外层的 value。
结构体同样如此:
type Person struct {
Name string
Age int
}
func modifyStruct(p Person) {
p.Name = "Alice"
}
func main() {
person := Person{Name: "Bob", Age: 25}
modifyStruct(person)
fmt.Println(person.Name) // 输出:Bob,原始值未改变
}
虽然结构体里字段很多,看起来更“复杂”,但传参规则没有变化。只要参数不是指针,函数拿到的就是一份拷贝。
为什么切片、map、channel 看起来像“引用传递”
争议通常出在这里。很多人会说,Go 虽然名义上是值传递,但 slice、map、channel 明明能在函数里改到外面的数据,这不就是引用传递吗?

严格说,不是。这里仍然是值传递,只不过被复制的那个“值”,本身就带有指向底层数据结构的信息。
先看切片:
func modifySlice(s []int) {
s[0] = 100
}
func main() {
slice := []int{1, 2, 3}
modifySlice(slice)
fmt.Println(slice) // 输出:[100, 2, 3]
}
结果看起来像是函数直接改了外部变量,但真正发生的过程是:
- 传参时,
slice这个切片值被复制了一份; - 复制出来的新切片值,仍然指向同一个底层数组;
- 因此函数内通过
s[0]改数据时,改到的是那块共享的底层数组。
也就是说,被复制的是切片描述符,不是整块底层数组。所以它表现得像“引用”,本质上仍然符合值传递规则。
map 和 channel 也可以用类似思路理解:函数参数被复制,但复制出来的值依然关联着同一份底层结构,因此在函数中操作它们,外层往往能观察到变化。
什么时候该用指针:拿到可修改原值的效果
如果你的目标很明确,就是希望函数内修改外部变量本身,那么应该传指针。
对基本类型来说,这是最典型的写法:
func modifyByPointer(num *int) {
*num = 100
}
func main() {
value := 5
modifyByPointer(&value)
fmt.Println(value) // 输出:100,原始值被修改
}
这次被复制进函数的,仍然是一个值,只不过这个值是指针。它保存的是外部变量的地址,所以通过 *num 解引用后,改到的就是原始数据。
结构体也是同一个道理:
func modifyStructByPointer(p *Person) {
p.Name = "Alice"
}
func main() {
person := Person{Name: "Bob", Age: 25}
modifyStructByPointer(&person)
fmt.Println(person.Name) // 输出:Alice,原始值被修改
}
很多 Go 代码里的“能改到外部对象”,本质都来自这里:传入的是指针值,函数据此访问了原对象,而不是 Go 额外提供了“按引用传参”。
切片最容易踩坑的地方:append 不一定改到外层
切片有一个特别值得单独拿出来说的情况:append。

前面说切片像“引用”,容易让人顺手得出一个错误判断:既然函数里改切片内容会影响外部,那在函数里 append 之后,外部切片也一定会变长。实际并不一定。
func appendSlice(s []int) {
s = append(s, 4)
// 这里s可能已经指向了新的底层数组
}
func main() {
s := []int{1, 2, 3}
appendSlice(s)
fmt.Println(s) // 输出:[1, 2, 3],长度未变
}
原因在于,append 可能触发扩容。一旦容量不够,运行时会分配新的底层数组,并让函数内部那个新的切片值指向新地址。这样一来,函数内部后续操作的是“新切片”,外层变量仍然保持原样。
如果你希望函数不仅能改切片中的元素,还能把切片长度变化同步回外层,那么就该传切片指针:
func appendSlice(s *[]int) {
*s = append(*s, 4)
}
func main() {
s := []int{1, 2, 3}
appendSlice(&s)
fmt.Println(s) // 输出:[1, 2, 3, 4]
}
这段代码里,函数修改的不再是切片副本本身,而是外层切片变量保存的那个值,所以长度变化也会被保留下来。
实战里怎么选:值传递、指针传递与性能判断
理解原理之后,选择就可以落到很具体的判断上。
适合直接传值的情况
- 基本数据类型,如
int、float、bool、string; - 体积不大的结构体或数组;
- 你不希望函数修改原始数据时。
适合传指针的情况
- 需要在函数内部修改原始数据;
- 结构体较大,想减少复制开销;
- 某些接口方法设计上就应绑定指针接收者时。
对切片、map、channel 的实际处理
- 大多数场景下直接传就够用;
- 如果只是改切片中的元素,通常不需要再包一层指针;
- 如果需要让切片长度变化回写到外层,才考虑传
*[]T。
性能上也可以顺着这个思路理解:小对象直接复制,往往简单且高效;大结构体如果频繁复制,成本会更高,这时传指针更划算。但性能不是唯一标准,代码语义是否清晰同样重要。一个函数到底是“只读参数”,还是“会改调用方数据”,最好从参数类型上就能看出来。
最后可以记住一条足够实用的判断规则:Go 只有值传递;如果你要改外部变量本身,就传指针;如果传的是切片、map、channel,要分清你改的是底层数据,还是变量本身的结构信息。









