Go语言的传参和传引用[转]

本文详细解释了Go语言中的传参机制,澄清了许多关于传值与传引用的常见误区,并通过示例代码展示了slice、map及channel等类型的处理方式。

传参和传引用的问题

很多非官方的文档和教材(包括一些已经出版的图书), 对Go语言的传参和引用的讲解 
都有很多问题. 导致众多Go语言新手对Go的函数参数传参有很多误解.

而传参和传引用是编程语言的根本问题, 如果这个问题理解错误可能会导致很多问题.

传slice不是传引用!

首先, Go语言的函数调用参数全部是传值的, 包括 slice/map/chan 在内所有类型, 没有传引用的说法.

具体请看Go语言的规范:

After they are evaluated, the parameters of the call are passed by value to the function and the called function begins execution.

from: http://golang.org/ref/spec#Calls

什么叫传引用?

比如有以下代码:

var a Object
doSomething(a) // 修改a的值 print(a) 

如果函数doSomething修改a的值, 然后print打印出来的也是修改后的值, 
那么就可以认为doSomething是通过引用的方式使用了参数a.

为什么传slice不是传引用?

我们构造以下的代码:

func main() {
    a := []int{1,2,3} fmt.Println(a) modifySlice(a) fmt.Println(a) } func modifySlice(data []int) { data = nil } 

其中modifySlice修改了切片a, 输出结果如下:

[1 2 3]
[1 2 3] 

说明a在调用modifySlice前后并没有任何变化, 因此a必然是传值的!

为什么很多人误以为slice是传引用呢?

可能是FAQ说slice是引用类型, 但并不是传引用!

下面这个代码可能是错误的根源:

func main() {
    a := []int{1,2,3}
    fmt.Println(a) modifySliceData(a) fmt.Println(a) } func modifySliceData(data []int) { data[0] = 0 } 

输出为:

[1 2 3]
[0 2 3] 

函数modifySliceData确实通过参数修改了切片的内容.

但是请注意: 修改通过函数修改参数内容的机制有很多, 其中传参数的地址就可以修改参数的值(其实是修改参数中指针指向的数据), 并不是只有引用一种方式!

传指针和传引用是等价的吗?

比如有以下代码:

func main() {
    a := new(int) fmt.Println(a) modify(a) fmt.Println(a) } func modify(a *int) { a = nil } 

输出为:

0xc010000000
0xc010000000

可以看出指针a本身并没有变化. 传指针或传地址也只能修改指针指向的内存的值, 
并不能改变指针本身在值.

因此, 函数参数传传指针也是传值的, 并不是传引用!

所有类型的函数参数都是传值的!

包括slice/map/chan等基础类型和自定义的类型都是传值的.

但是因为slicemap/chan底层结构的差异, 又导致了它们传值的影响并不完全等同.

重点归纳如下:

  • GoSpec: the parameters of the call are passed by value!

  • map/slice/chan 都是传值, 不是传引用

  • map/chan 对应指针, 和引用类似

  • slice 是结构体和指针的混合体

  • slice 含 values/count/capacity 等信息, 是按值传递

  • slice 中的 values 是指针, 按值传递

  • 按值传递的 slice 只能修改values指向的数据, 其他都不能修改

  • 以指针或结构体的角度看, 都是值传递!

那Go语言有传引用的说法吗?

Go语言其实也是有传引用的地方的, 但是不是函数的参数, 而是闭包对外部环境是通过引用访问的.

查看以下的代码:

func main() {
    a := new(int) fmt.Println(a) func() { a = nil }() fmt.Println(a) } 

输出为:

0xc010000000
<nil>

因为闭包是通过引用的方式使用外部环境的a变量, 因此可以直接修改a的值.

比如下面2段代码的输出是截然不同的, 原因就是第二个代码是通过闭包引用的方式输出i变量:

for i := 0; i < 5; i++ {
    defer fmt.Printf("%d ", i) // Output: 4 3 2 1 0 } fmt.Printf("\n") for i := 0; i < 5; i++ { defer func(){ fmt.Printf("%d ", i) } () // Output: 5 5 5 5 5 } 

像第二个代码就是于闭包引用导致的副作用, 回避这个副作用的办法是通过参数传值或每次闭包构造不同的临时变量:

// 方法1: 每次循环构造一个临时变量 i
for i := 0; i < 5; i++ {
    i := i
    defer func(){ fmt.Printf("%d ", i) } () // Output: 4 3 2 1 0 } // 方法2: 通过函数参数传参 for i := 0; i < 5; i++ { defer func(i int){ fmt.Printf("%d ", i) } (i) // Output: 4 3 2 1 0 } 

什么是引用类型, 和指针有何区别/联系 ?

在Go语言的官方FAQ中描述, maps/slices/channels 是引用类型, 数组是值类型:

Why are maps, slices, and channels references while arrays are values?

There's a lot of history on that topic. Early on, maps and channels were syntactically pointers and it was impossible to declare or use a non-pointer instance. Also, we struggled with how arrays should work. Eventually we decided that the strict separation of pointers and values made the language harder to use. Changing these types to act as references to the associated, shared data structures resolved these issues. This change added some regrettable complexity to the language but had a large effect on usability: Go became a more productive, comfortable language when it was introduced.

from: http://golang.org/doc/faq#references

我个人理解, 引用类型和指针在底层实现上是一样的. 
但是引用类型在语法上隐藏了显示的指针操作. 
引用类型和函数参数的传引用/传值并不是一个概念.

我们知道 maps/slices/channels 在底层虽然隐含了指针, 
但是使用中并没有需要使用指针的语法. 
但是引用内存毕竟是基于指针实现, 因此就必须依赖 make/new 之类的函数才能构造出来. 
当然它们都支持字面值语法构造, 但是本质上还是需要一个构造的过程的.

要用好Go语言的引用类型, 必须要了解一些底层的结构(特别是slice的混合结构).

我们可以自己给Go语言模拟一个引用类型. 
我们可以将值类型特定的数组类型定义为一个引用类型(同时提供一个构造函数):

type RefIntArray2 *[2]int

func NewRefIntArray2() RefIntArray2 {
    return RefIntArray2(new([2]int)) } 

这样我们就可以将 RefIntArray2 当作引用类型来使用.

func main() {
    refArr2 := NewRefIntArray2()
    fmt.Println(refArr2)
    modifyRefArr2(refArr2)
    fmt.Println(refArr2)
}

func modifyRefArr2(arr RefIntArray2) {
    arr[0] = 1 } 

输出为:

&[0 0]
&[1 0]

之所以选择数组作为例子, 是因为Go语言的数组指针可以直接用[]访问的语法糖. 
所以, 引用类型一般都是底层指针实现, 只是在上层加上的语法糖而已.

注: 本节根据 @hooluupog 和 @LoongWong 的评论做的补充.

总结

  • 函数参数传值, 闭包传引用!
  • slice 含 values/count/capacity 等信息, 是按值传递
  • 按值传递的 slice 只能修改values指向的数据, 其他都不能修改
  • slice 是结构体和指针的混合体
  • 引用类型和传引用是两个概念
 
from:http://my.oschina.net/chai2010/blog/161384
<think>我们正在讨论Go语言中的指针引用机制。用户的问题是想理解Go语言中的指针及其引用机制。根据之前的对话历史,用户之前询问了关于布尔指针字段的判断,现在向更一般的指针概念。 参考引用: [^1]: 介绍了指针是Go语言中重要的概念,允许直接操作内存地址,实现高效的数据共享、修改递。 [^2]: 指针是存储另一个变量内存地址的变量,通过指针可以间接操作其他变量的Go的指针不支持指针算术,但提供了简洁安全的操作方式。 [^3]: 指针是特殊类型,实例保存的是被引用对象的内存地址,而非对象自身的。未初始化的指针默认为nil。 [^4]: 展示了方法接收器为指针类型类型的区别,指针接收器可以修改结构体字段,而接收器不能。 我们需要讲解以下内容: 1. 指针的基本概念(声明、初始化、零) 2. 指针的操作(取地址&、解引用*) 3. 指针的使用场景(函数传参、方法接收器、避免大对象拷贝) 4. 指针的注意事项(空指针、指针不能进行算术运算) 5. 指针与引用的区别(Go中没有引用类型,但有引用语义,如切片、映射、通道等,它们内部有指针) 注意:在Go语言中,没有引用类型(如C++的引用),但是有引用语义的类型(引用类型指的是切片、映射、通道,它们实际上是对底层数据结构的引用,但使用上类似于引用)。 结构: 1. 指针基础 2. 指针操作 3. 指针使用场景 4. 常见错误及避免方法 5. 代码示例 同时,根据用户上一次的问题(布尔指针的判断)引用[4]中的方法接收器示例,可以强调指针在修改原上的作用。 最后生成相关问题。</think>### Go语言指针与引用机制详解 Go语言中的指针是**存储变量内存地址的特殊变量**,通过指针可以间接访问修改内存中的数据。Go的指针设计既保留了直接内存操作的能力,又通过限制指针运算避免了常见的内存安全问题[^1][^2]。 --- ### 一、核心概念解析 #### 1. 指针基础 | 操作 | 语法 | 说明 | 示例 | |--------------|-----------|-------------------------------|---------------------------| | **声明** | `var p *T`| 创建指向T类型变量的指针 | `var p *int` | | **取地址** | `&var` | 获取变量的内存地址 | `x := 10; p := &x` | | **解引用** | `*p` | 访问指针指向的 | `fmt.Println(*p) // 10` | | **零** | `nil` | 未初始化指针的默认[^3] | `var p *int; p == nil` | #### 2. 引用机制特性 - **递 vs 指针递** ```go func modifyValue(v int) { v = 100 } // 递(副本) func modifyPointer(p *int) { *p = 100 } // 指针递(原) func main() { x := 10 modifyValue(x) // x 仍为 10 modifyPointer(&x) // x 变为 100 } ``` - **结构体方法接收器** - 接收器:操作结构体副本 - 指针接收器:操作原结构体[^4] ```go type User struct{ Name string } func (u User) Rename1(s string) { u.Name = s } // 无效修改 func (u *User) Rename2(s string) { u.Name = s } // 有效修改 ``` --- ### 二、指针使用场景 1. **高效递大对象** 避免结构体复制开销: ```go type BigData struct { /* 大量字段 */ } func Process(data *BigData) { ... } // 仅递8字节指针 ``` 2. **修改函数外部变量** 突破函数作用域限制: ```go func ResetCounter(c *int) { *c = 0 } ``` 3. **实现可选字段** 通过`nil`指针表示字段未设置: ```go type Config struct { Timeout *int // nil表示未设置 } ``` --- ### 三、关键注意事项 1. **空指针解引用** 必须检查指针非空: ```go // 危险操作(可能panic) *p = 10 // 安全操作 if p != nil { *p = 10 } ``` 2. **指针不可运算** Go禁止指针算术(区别于C/C++): ```go arr := [3]int{1,2,3} p := &arr[0] p++ // 编译错误:指针不能运算 ``` 3. **/指针接收器混用** 编译器自动换: ```go u := User{} u.Rename2("A") // 自动换为(&u).Rename2 p := &User{} p.Rename1("B") // 自动换为(*p).Rename1 ``` --- ### 四、引用语义类型 Go虽无引用变量,但内置类型具有引用语义: | 类型 | 说明 | 内存模型 | |---------|-----------------------|------------------| | Slice | 包含底层数组指针 | `struct{ ptr, len, cap }` | | Map | 指向哈希表的指针 | 运行时实现 | | Channel | 指向通信管道的指针 | 运行时实现 | ```go s := []int{1,2,3} s2 := s // 共享底层数组 s2[0] = 99 // 修改影响s fmt.Println(s) // [99 2 3] ``` --- ### 五、最佳实践 1. **优先使用类型** 小对象更安全高效: ```go // 推荐(小于3字段结构体) func PrintPoint(p Point) { ... } ``` 2. **nil作为有效状态** 明确处理空指针: ```go func (s *Service) Start() error { if s == nil { return errors.New("nil service") } // ... } ``` 3. **使用指针辅助函数** ```go // 安全指针解引用 func DerefInt(p *int, defaultVal int) int { if p != nil { return *p } return defaultVal } ``` > Go的指针机制在内存安全与性能间取得平衡,理解其「**递本质+指针显式操作**」的特性是掌握Go内存模型的关键[^1][^2]。
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值