Go 只有值传递,为什么切片和指针又像“引用传递”?

南乔
南乔
发布时间:2026-08-25| 130人浏览过
原创

前言

很多刚接触 Go 的开发者,都会在“值传递”和“引用传递”上卡一会儿:为什么传入 int 改不动原值,传入 slice 却像是能改到外面?问题不在于 Go 存在两套传参机制,而在于不同类型被复制后,携带的数据含义并不一样。把这件事拆开看清之后,你就能判断什么时候直接传值,什么时候该用指针,什么时候又要特别留意切片的 append 行为。

很多刚接触 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 明明能在函数里改到外面的数据,这不就是引用传递吗?

用纸面卡片对比 Go 中值传递与切片看似引用传递的原因
值被复制了,数据为什么还会变把“参数一定复制”和“复制后仍指向同一底层数据”放在一张图里。

严格说,不是。这里仍然是值传递,只不过被复制的那个“值”,本身就带有指向底层数据结构的信息。

先看切片:

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 扩容后为何外层长度不变
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,要分清你改的是底层数据,还是变量本身的结构信息。

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。