Go 官方的定义

本部分引用 Go 官方 FAQ 的 “When are function parameters passed by value?”,内容如下。

如同 C 系列的所有语言一样,Go 语言中的所有东西都是以值传递的。也就是说,一个函数总是得到一个被传递的东西的副本,就像有一个赋值语句将值赋给参数一样。

传值和传引用

什么是传值(值传递)

传值的意思是:函数传递的总是原来这个东西的一个副本,一副拷贝。其指的是在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。 比如我们传递一个int类型的参数,传递的其实是这个参数的一个副本;传递一个指针类型的参数,其实传递的是这个该指针的一份拷贝,而不是这个指针指向的值。

对于int这类基础类型我们可以很好的理解,它们就是一个拷贝,但是指针呢?我们觉得可以通过它修改原来的值,怎么会是一个拷贝呢?下面我们看个例子。

test_demo.go

package main

import (
	"fmt"
	"testing"
)

func modify(ip *int) {
	fmt.Printf("函数里接收到的指针的内存地址是:%p\n", &ip)
	*ip = 1
}

func TestDemo(t *testing.T) {
	i := 10
	ip := &i
	fmt.Printf("原始指针的内存地址是:%p\n", &ip)
	modify(ip)
	fmt.Println("int值被修改了,新值为:", i)

}

输出结果:

原始指针的内存地址是:0xc00000e038
函数里接收到的指针的内存地址是:0xc00000e040
int值被修改了,新值为: 1

什么是传引用(引用传递)

传引用,也叫做引用传递, 指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

在 Go 语言中,官方已经明确了没有传引用,也就是没有引用传递这一情况。

争议最大的 map 和 slice
这时候又有小伙伴疑惑了,你看 Go 语言中的 map 和 slice 类型,能直接修改,难道不是同个内存地址,不是引用了?

其实在 FAQ 中有一句提醒很重要:“map 和 slice 的行为类似于指针,它们是包含指向底层 map 或 slice 数据的指针的描述符”。

迷惑map

package main

import (
	"fmt"
	"testing"
)

func modify(p map[string]int) {
	fmt.Printf("函数里接收到map的内存地址是:%p\n", &p)
	p["张三"] = 20
}

func TestDemo(t *testing.T) {
	persons := make(map[string]int)
	persons["张三"] = 19

	mp := &persons

	fmt.Printf("原始map的内存地址是:%p\n", mp)
	modify(persons)
	fmt.Println("map值被修改了,新值为:", persons)
}

输出结果:

原始map的内存地址是:0xc000114028
函数里接收到map的内存地址是:0xc000114030

确实是值传递,那修改后的 map 的结果应该是什么。既然是值传递,那肯定就是 “这次一定!",对吗?

输出结果:

map值被修改了,新值为: map[张三:20]

原因:

makemap
// makemap implements a Go map creation make(map[k]v, hint)
// If the compiler has determined that the map or the first bucket
// can be created on the stack, h and/or bucket may be non-nil.
// If h != nil, the map can be created directly in h.
// If bucket != nil, bucket can be used as the first bucket.
func makemap(t *maptype, hint int64, h *hmap, bucket unsafe.Pointer) *hmap {
    //省略无关代码
}
src/runtime/hashmap.go*hmapmap
func modify(p map)func modify(p *hmap)func modify(ip *int)

这类情况我们称其为 “引用类型” ,但 “引用类型” 不等同于就是传引用,又或是引用传递了,还是有比较明确的区别的。

chan类型

chan类型本质上和map类型是一样的,这里不做过多的介绍,参考下源代码:

func makechan(t *chantype, size int64) *hchan {
    //省略无关代码
}
chanmapmake*hchan

和map、chan都不一样的slice

slicemapchan
package main

import (
	"fmt"
	"testing"
)

func modify(ages []int) {
	fmt.Printf("函数里接收到slice的内存地址是%p\n", ages)
	ages[0] = 1
}

func TestDemo(t *testing.T) {
	ages := []int{6, 6, 6}
	fmt.Printf("原始slice的内存地址是%p\n", ages)
	modify(ages)
	fmt.Println(ages)
}
ages
& %p
fmt
func (p *pp) fmtPointer(value reflect.Value, verb rune) {
    var u uintptr
    switch value.Kind() {
    case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.Slice, reflect.UnsafePointer:
        u = value.Pointer()
    default:
        p.badVerb(verb)
        return
    }
    //省略部分代码
}
chanmapslicevalue.Pointer()
// If v's Kind is Slice, the returned pointer is to the first
// element of the slice. If the slice is nil the returned value
// is 0.  If the slice is empty but non-nil the return value is non-zero.
func (v Value) Pointer() uintptr {
    // TODO: deprecate
    k := v.kind()
    switch k {
    //省略无关代码
    case Slice:
        return (*SliceHeader)(v.ptr).Data
    }
}
slicesliceData
type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}
sliceslicearray(Data)slice

总结

最终可以确认的是Go语言中所有的传参都是值传递(传值),都是一个副本,一个拷贝。

slicemapchan

再记住,Go里只有传值(值传递)。

参考