Go语言的传参和传引用

Go语言的传参和传引用[OSC源创会主题补充1]

66人收藏此文章, 我要收藏发表于2天前(2013-09-14 22:10) , 已有 1496次阅读 ,共 12个评论

目录:[ - ]

  • 传参和传引用的问题
  • slice不是引用!
  • 什么叫引用?
  • 为什么slice不是引用?
  • 为什么很多人误以为slice是引用呢?
  • 传指针和传引用是等价的吗?
  • 所有类型的函数参数都是传值的!
  • 那Go语言有传引用的说法吗?
  • 总结
  • OSC源创会主题补充系列:

    1. Go语言的传参和传引用
    2. Go语言的类型转换和类型断言

    Go语言规范虽然很简单, 但是深入掌握Go语言却需要很多底层知识.

    本来第20期的武汉OSC源创会有Go语言的专题讲座, 谁知道说取消就取消了.

    我最近也整理了一些Go语言资料, 有Go语言的历史/现状/未来发展的八卦和Go语言常见的问题和陷阱两个部分, 本来打算OSC源创会能和武汉的Gopher分享 下的, 谁知道(由于不是赞助商也不是微软的大牛)主办方根本不给任何的机会.

    100+人数的交流会基本都是扯淡, 还是小规模的讨论沙龙比较靠谱, 以后再也不会去OSC源创会当听众了.

    现在计划将各个小问题暂时作为博客发表.

    传参和传引用的问题

    很多非官方的文档和教材(包括一些已经出版的图书), 对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是引用呢?

    可能是 因为很多新接触Go语言的新手, 看到Go语言的文档说Go的切片和C语言的数组类型, 而C语言的数组是传地址的(注意: 不是传引用!).

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

    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 } 

    总结

    • 函数参数传值, 闭包传引用!
    • slice 含 values/count/capacity 等信息, 是按值传递
    • 按值传递的 slice 只能修改values指向的数据, 其他都不能修改
    • slice 是结构体和指针的混合体

    你可能感兴趣的:(Go语言的传参和传引用)