参数传递

在Go中函数参数传递都是值传递,都会进行拷贝,比如对于函数func(a *A, b B)(aa, bb),此时a是aa的副本,b是bb的副本。

Go中没有引用传递

传递指针还是传递值(在什么时候传递指针,什么时候传递值)

在如下情况需要传递指针:

  1. 需要在函数(或方法)中修改变量的值(如对象的属性)

  2. 需要传递的对象是一个大的结构体(struct)或者是一个大的数组,原因是给该对象的指针创建副本比直接给该对象创建副本的代价要小。

如果是在函数作用域内定义变量则建议将对象定义为值类型,因为Go编译器尽量将对象分配到栈上,如果定义为指针类型则可能会被分配到堆上(会对垃圾回收产生影响)

引用传递(传引用,C++支持,Go不支持)

go并不支持引用传递。引用传递是类似于func(&a A)(aa),对于a、aa的内存地址是一样,那么该函数是引用传递。但实际上在go中传递的指针之间内存地址并不一样,由于拷贝它们有着不同的地址

值传递

go的参数传递过程中都会对原始数据进行拷贝(创建副本,在函数中参与运算的是其副本),我们称这种传递方式为值传递(只是说这些副本有些是某个变量的副本,有些是某个变量的指针的副本)

副本的创建

给值类型创建副本

我们在函数中修改值对象的副本并不会对原始对象产生影响(原始对象的值不变),副本对象的内存地址和原始对象的内存地址不同。

type CodePorter struct {
    Name string
    Age  uint32
}

func main() {
    caseValue()
    // OUT:
    //原始的coder:             {Name:JX Age:24},              内存地址:0xc00000c160
    //传入修改后的coder:        {Name:JX Age:25},              内存地址:0xc00000c1a0
    //调用后原始的coder:        {Name:JX Age:24},              内存地址:0xc00000c160
}

func caseValue() {
    var coder = CodePorter{Name: "JX", Age: 24}
    fmt.Printf("原始的Bird:\t\t %+v, \t\t内存地址:%p\n", coder, &coder)
    passV(coder)
    fmt.Printf("调用后原始的Bird:\t %+v, \t\t内存地址:%p\n", coder, &coder)
}

func passV(coder CodePorter) {
    coder.Age++
    fmt.Printf("传入修改后的Bird:\t %+v, \t\t内存地址:%p\n", coder, &coder)
}

给指针类型创建副本

我们在函数中通过值对象的指针修改属性的值,原始对象的该属性值也会变化,但是指针副本对象的内存地址和原始对象的内存地址不同。通过指针修改的属性值能生效的原因是,原始指针对象、副本指针对象指向同一块内存地址

type CodePorter struct {
    Name string
    Age  uint32
}

func main() {
    casePorter()
    // OUT:
    //原始的coder:             {Name:JX Age:24},      内存地址:0xc00000c200, 指针的内存地址: 0xc00000e030
    //传入修改后的coder:        {Name:JX Age:25},      内存地址:0xc00000c200, 指针的内存地址: 0xc00000e038
    //调用后原始的coder:        {Name:JX Age:25},      内存地址:0xc00000c200, 指针的内存地址: 0xc00000e030
}

func casePorter() {
    var coder = &CodePorter{Name: "JX", Age: 24}
    fmt.Printf("原始的Bird:\t\t %+v, \t内存地址:%p, 指针的内存地址: %p\n", *coder, coder, &coder)
    passP(coder)
    fmt.Printf("调用后原始的Bird:\t %+v, \t内存地址:%p, 指针的内存地址: %p\n", *coder, coder, &coder)
}

func passP(coder *CodePorter) {
    coder.Age++
    fmt.Printf("传入修改后的Bird:\t %+v, \t内存地址:%p, 指针的内存地址: %p\n", *coder, coder, &coder)
}

什么时候创建副本

赋值的时候就会创建副本,各副本内存地址都不同, 如下所示:

type CodePorter struct {
    Name string
    Age  uint32
}

type Worker struct {
    Name string
    Age  uint32
}

func main() {
    var coder1 = CodePorter{Name: "JX", Age: 24}
    var coder2 = coder1
    fmt.Printf("coder1:\t\t %+v, \t\t内存地址:%p\n", coder1, &coder1)
    fmt.Printf("coder2:\t\t %+v, \t\t内存地址:%p\n", coder2, &coder2)
    coder3 := coder1
    fmt.Printf("coder2:\t\t %+v, \t\t内存地址:%p\n", coder3, &coder3)
    // 内存布局一样,可以进行强制转换
    coder4 := Worker(coder1)
    fmt.Printf("coder4:\t\t %+v, \t\t内存地址:%p\n", coder4, &coder4)

    // OUT:
    //coder1:          {Name:JX Age:24},              内存地址:0xc0000a6020
    //coder2:          {Name:JX Age:24},              内存地址:0xc0000a6040
    //coder2:          {Name:JX Age:24},              内存地址:0xc0000a60a0
    //coder4:          {Name:JX Age:24},              内存地址:0xc0000a60e0
}

对于array、slice以及map在初始化以及按索引设置值时会创建副本

type CodePorter struct {
    Name string
    Age  uint32
}

func main() {
    caseArraySliceMap()
    // OUT:
    //coder:           {Name:JX Age:24},              内存地址:0xc00000c280
    //coder:           {Name:JX Age:24},              内存地址:0xc000066180
    //coder:           {Name:JX Age:24},              内存地址:0xc000066198
    //coder:           {Name:JX Age:24},              内存地址:0xc0000661b0
    //coder:           {Name:JX Age:24},              内存地址:0xc0000661c8
    //coder:           {Name:JX Age:24}
    //coder:           {Name:JX Age:24},              内存地址:0xc00000c380
    //coder:           {Name:JX Age:24},              内存地址:0xc00000c280
}

func caseArraySliceMap() {
    var coder = CodePorter{Name: "JX", Age: 24}
    fmt.Printf("coder:\t\t %+v, \t\t内存地址:%p\n", coder, &coder)

    var array = [2]CodePorter{coder}
    array[1] = coder
    coder.Age++
    fmt.Printf("coder:\t\t %+v, \t\t内存地址:%p\n", array[0], &array[0])
    fmt.Printf("coder:\t\t %+v, \t\t内存地址:%p\n", array[1], &array[1])
    coder.Age--

    var slice = []CodePorter{coder}
    slice = append(slice, coder)
    coder.Age++
    fmt.Printf("coder:\t\t %+v, \t\t内存地址:%p\n", slice[0], &slice[0])
    fmt.Printf("coder:\t\t %+v, \t\t内存地址:%p\n", slice[1], &slice[1])
    coder.Age--

    var mp = map[int]CodePorter{0: coder}
    coder.Age++
    fmt.Printf("coder:\t\t %+v \n", mp[0])
    var xc = mp[0]
    fmt.Printf("coder:\t\t %+v, \t\t内存地址:%p\n", xc, &xc)
    coder.Age--

    fmt.Printf("coder:\t\t %+v, \t\t内存地址:%p\n", coder, &coder)
}

for-range循环也是将元素的副本赋值给循环变量,所以变量得到的是集合元素的副本

type CodePorter struct {
    Name string
    Age  uint32
}

func main() {
    caseForRange()
    // OUT
    //array coder0:            {Name:JX Age:24},              内存地址:0xc00000c3e0
    //array coder1:            {Name:JX Age:24},              内存地址:0xc00000c3e0
    //array coder2:            {Name:JX Age:24},              内存地址:0xc00000c3e0
    //slice coder0:            {Name:JX Age:24},              内存地址:0xc00000c460
    //slice coder1:            {Name:JX Age:24},              内存地址:0xc00000c460
    //slice coder2:            {Name:JX Age:24},              内存地址:0xc00000c460
    //map coder2:              {Name:JX Age:24},              内存地址:0xc00000c4e0
    //map coder0:              {Name:JX Age:24},              内存地址:0xc00000c4e0
    //map coder1:              {Name:JX Age:24},              内存地址:0xc00000c4e0
}

func caseForRange() {
    var coder = CodePorter{Name: "JX", Age: 24}
    var array = [3]CodePorter{coder, coder, coder}
    for i, p := range array {
        fmt.Printf("array coder%d:\t\t %+v, \t\t内存地址:%p\n", i, p, &p)
    }

    var slice = []CodePorter{coder, coder, coder}
    for i, p := range slice {
        fmt.Printf("slice coder%d:\t\t %+v, \t\t内存地址:%p\n", i, p, &p)
    }

    var mp = map[int]CodePorter{0: coder, 1: coder, 2: coder}
    for i, p := range mp {
        fmt.Printf("map coder%d:\t\t %+v, \t\t内存地址:%p\n", i, p, &p)
    }
}

也许你已经注意到了,循环变量的地址是一样的,因为在go的for range中循环变量是重用的

往channel中send元素也会创建对象副本

type CodePorter struct {
    Name string
    Age  uint32
}

func main() {
    caseChannel()
    // OUT
    //coder0:          {Name:JX Age:24},              内存地址:0xc0000a6520
    //coder1:          {Name:JX Age:24},              内存地址:0xc0000a6560
    //coder2:          {Name:JX Age:25},              内存地址:0xc0000a6560
    //coder3:          {Name:JX Age:26},              内存地址:0xc0000a6560
}

func caseChannel() {
    var coder = CodePorter{Name: "JX", Age: 24}
    fmt.Printf("coder0:\t\t %+v, \t\t内存地址:%p\n", coder, &coder)
    var ch = make(chan CodePorter, 3)
    ch <- coder
    coder.Age++
    ch <- coder
    coder.Age++
    ch <- coder
    coder.Age++

    var p CodePorter
    p = <-ch
    fmt.Printf("coder1:\t\t %+v, \t\t内存地址:%p\n", p, &p)
    p = <-ch
    fmt.Printf("coder2:\t\t %+v, \t\t内存地址:%p\n", p, &p)
    p = <-ch
    fmt.Printf("coder3:\t\t %+v, \t\t内存地址:%p\n", p, &p)
    close(ch)
}

总结

  1. 在Go中函数参数传递方式是值传递

  2. 值传递时会进行拷贝(创建副本,副本有自己的内存地址且各不相同)

  3. 将变量作为参数传递给函数和方法会发生副本的创建。

  4. 对于返回值,将返回值赋值给其它变量或者传递给其它的函数和方法,就会创建副本

  5. 因为方法(method)最终会产生一个receiver作为第一个参数的函数(参看规范,另外我们在编写单元测试时,使用gomonkey去mock某个对象的方法时,第一个参数需要指定其receiver) 当receiver为T类型时,会发生创建副本,调用副本上的方法。 当receiver为*T类型时,只是会创建对象的指针,不创建对象的副本,方法内对receiver的改动会影响原始值。

  6. 指针类型创建副本的开销很小

  7. bool、数值类型一般不必考虑指针类型,原因在于这些对象很小,创建副本的开销可以忽略

  8. array(数组)是值类型,在参数传递或赋值的时候会创建副本,对于大的数组的参数传递或赋值的时候如果对值进行拷贝开销会很大,因此可以设计为[...]*T类型(某类型指针数组)

  9. map、slice和channel是引用类型,他们本身就是引用了其他类型来存储数据。一般来说不需要定义为指针类型

  10. 字符串在空省时默认值时"",如果你需要默认值为nil则需要定位为*string类型。在序列化对象时""表示字段存在且值为空字符串,nil表示字段不存在。string与[]byte直接进行强制转换时会发生数据的复制,用下面方式进行转换性能更好:

func string2Bytes(s string) []byte {
   var x = (*[2]uintptr)(unsafe.Pointer(&s))
   var h = [3]uintptr{x[0], x[1], x[1]}
   return *(*[]byte)(unsafe.Pointer(&h))
}

func bytes2String(b []byte) string {
   return *(*string)(unsafe.Pointer(&b))
}
  1. 函数也是一个指针类型,对函数对象的赋值只是又创建了一个对次函数对象的指针。

解惑

  1. 传递指针时我们可以通过它修改原来的值,怎么会是一个拷贝呢?

    在给指针类型创建副本的实验中,我们通过输出的内存地址可以确认参数传递过来的指针是原始指针的一个副本。至于为什么可以通过指针副本修改原来的值,原因是指针副本和原始指针指向的是同一个内存地址。

    指针传递解释

  2. 对于map、slice、channel在函数中修改为什么原始对象的值也会改变?

    首先map、slice、channel都是引用类型,我们传递给函数的副本和原始对象所数据指向的内存地址(指针)是同一个

引用类型:map、slice、channel

源码见:src/runtime

map

// makemap implements Go map creation for 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 h.buckets != nil, bucket pointed to can be used as the first bucket.
func makemap(t *maptype, hint int, h *hmap) *hmap {
  // ...
}

对于map来说,当我们使用make对其初始化的时候实际上返回的是*hmap, 就是说map === *hmap

chan

func makechan(t *chantype, size int) *hchan {
  // ...
}

类似map,当我们使用make对chan进行初始化的时候实际上返回的是*hchan, 就是说chan === *hchan

slice

我们先看一下slice的定义,他其实是一个结构体,数据存储在array中(用一个字段存储其内存地址)

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

和之前一样,我们看看使用make初始化时得到的是什么,emm...看起来不像之前的map、chan那样明显,难道有什么不可描述的“交易”

func makeslice(et *_type, len, cap int) unsafe.Pointer {
  // ... 
  return mallocgc(mem, et, true)
}

其实在src/reflect/value.go里面有一个这样的结构用于表示slice

// SliceHeader is the runtime representation of a slice.
// It cannot be used safely or portably and its representation may
// change in a later release.
// Moreover, the Data field is not sufficient to guarantee the data
// it references will not be garbage collected, so programs must keep
// a separate, correctly typed pointer to the underlying data.
type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

// 可以通过SliceHeader解析出makeslice产生的虚拟结构体,取到数据
func (v Value) Pointer() uintptr {
    // ...
    case Slice:
        return (*SliceHeader)(v.ptr).Data
    }
}

所以在函数中我们可以通过下标修改slice的副本时,原始对象该下标的值也会改变。

注意:指针赋值操作

func main() {
  caseEditPorter()
    // OUT
    //初始化:                       &{Name:2020年的JX Age:24},             内存地址:0xc0000ae030
    //editPorterErr:           &{Name:2020年的JX Age:24},             内存地址:0xc0000ae030
    //editPorter:              &{Name:18岁的JX Age:18},               内存地址:0xc0000ae030
}

func caseEditPorter() {
    var a = &CodePorter{Age: 24, Name: "2020年的JX"}
    fmt.Printf("初始化:\t\t %+v, \t\t内存地址:%p\n", a, &a)
    editPorterErr(a)
    fmt.Printf("editPorterErr:\t\t %+v, \t\t内存地址:%p\n", a, &a)
    editPorter(a)
    fmt.Printf("editPorter:\t\t %+v, \t\t内存地址:%p\n", a, &a)
}

func editPorterErr(c *CodePorter) {
    var x = &CodePorter{Age: 18, Name: "18岁的JX"}
    c = x
}

func editPorter(c *CodePorter) {
    var x = &CodePorter{Age: 18, Name: "18岁的JX"}
    *c = *x
}

参考

[]T 还是 []*T, 这是一个问题

Go语言参数传递是传值还是传引用

深入解析Go中Slice底层实现

最后更新于

这有帮助吗?