原创分享 Go 语言传值和深浅复制问题

astaxie · 2020年02月12日 · 56 次阅读

关于传值还是传引用的问题,在go 的调用规范中有提到,在函数调用中是传值的(是否有其他的例外情况,我们后面再来考察),但是在下面的例子中,我们将看到大部分情况下,这句话是好理解的,但是还是会有意外。那么这句话应该怎么理解呢?

先来看看这个例子:

1. 一个例子

type Color int

const (
  black = iota
  green
  blue
)

type Cat struct {
  name string
  age  int
  legs Legs
  eye  map[string]Eye
  tail Tail
}

type Eye struct {
  color Color
}

type Tail struct {
  length float64
}

type Legs struct {
  count  int
  length []int
}

这只猫融合了好几种类型的数据,包括基本类型,内嵌自定义对象,map,slice。将 Legs 而不是 Leg 定义为一个对象是有意而为之,是为了测试一个内嵌对象中包含的基本类型和 slice 类型时的复制行为。

func ModifyCat(cat Cat) {
  fmt.Printf("enter value cat:%v\n", cat)
  fmt.Printf("in modify: cat:%p, cat.name:%p, cat.tail:%p\n", &cat, &cat.name, &cat.tail)
  fmt.Printf("in modify: cat.legs.length:%p, cat.legs.count:%p\n", &cat.legs.length, &cat.legs.count)
  cat.name = "Ben"
  cat.eye["left"] = Eye{blue}
  cat.tail = Tail{234.56}
  cat.legs.count = 3
  cat.legs.length[0] = 0
  fmt.Printf("exit value cat:%v\n\n", cat)
}

func main() {
  catA := Cat{
    name: "tom",
    age:  1,
    legs: Legs{count: 4, length: []int{10, 10, 10, 10}},
    eye:  map[string]Eye{"left":{black}, "right":{green}},
    tail: Tail{123.45},
  }

  fmt.Printf("value catA:%v, catB:%v\n", catA, catB)
  fmt.Printf("address catA:%p, catB:%p\n", &catA, &catB)
  fmt.Printf("address catA.eye:%p, catB.eye:%p,\n", &catA.eye, &catB.eye)
  fmt.Printf("address catA.name:%p, catB.name:%p\n", &catA.name, &catB.name)
  fmt.Printf("address catA.tail:%p, catB.tail:%p\n", &catA.tail, &catB.tail)
  fmt.Printf("address catA.legs.count:%p, catB.legs.count:%p\n", &catA.legs.count, &catB.legs.count)
  fmt.Printf("address catA.legs.length:%p, catB.legs.length:%p\n", &catA.legs.length, &catB.legs.length)
  fmt.Println()

  ModifyCat(catB)
  fmt.Printf("value catA:%v, catB:%v\n", catA, catB)
  fmt.Printf("address catA:%p, catB:%p\n", &catA, &catB)
  fmt.Printf("address catA.eye:%p, catB.eye:%p,\n", &catA.eye, &catB.eye)
  fmt.Printf("address catA.name:%p, catB.name:%p\n", &catA.name, &catB.name)
  fmt.Printf("address catA.tail:%p, catB.tail:%p\n", &catA.tail, &catB.tail)
  fmt.Printf("address catA.legs.count:%p, catB.legs.count:%p\n", &catA.legs.count, &catB.legs.count)
  fmt.Printf("address catA.legs.length:%p, catB.legs.length:%p\n\n", &catA.legs.length, &catB.legs.length)

  catB.name = "Ben"
  fmt.Printf("in modify: cat: %p\n", &catB)
  catB.eye["right"] = Eye{black}
  catB.tail = Tail{234.56}
  catB.legs.count = 3
  catB.legs.length[1] = 0
  fmt.Printf("value catA:%v, catB:%v\n", catA, catB)
  fmt.Printf("address catA:%p, catB:%p\n", &catA, &catB)
  fmt.Printf("address catA.eye:%p, catB.eye:%p,\n", &catA.eye, &catB.eye)
  fmt.Printf("address catA.name:%p, catB.name:%p\n", &catA.name, &catB.name)
  fmt.Printf("address catA.tail:%p, catB.tail:%p\n", &catA.tail, &catB.tail)
  fmt.Printf("address catA.legs.count:%p, catB.legs.count:%p\n", &catA.legs.count, &catB.legs.count)
  fmt.Printf("address catA.legs.length:%p, catB.legs.length:%p\n", &catA.legs.length, &catB.legs.length)
}

我们首先创建了一直猫 catA,然后创建一个 catB,并直接将 catA 赋值给 catB,然后将 catB 传给 ModifyCat(Cat) 函数修改这只新猫的模样。并打印出前后的过程。

我们看到的具体输出如下:

value catA:{tom 1 {4 [10 10 10 10]} map[left:{0} right:{1}] {123.45}}, catB:{tom 1 {4 [10 10 10 10]} map[left:{0} right:{1}] {123.45}}
address catA:0xc820016190, catB:0xc8200161e0
address catA.eye:0xc8200161c8, catB.eye:0xc820016218,
address catA.name:0xc820016190, catB.name:0xc8200161e0
address catA.tail:0xc8200161d0, catB.tail:0xc820016220
address catA.legs.count:0xc8200161a8, catB.legs.count:0xc8200161f8
address catA.legs.length:0xc8200161b0, catB.legs.length:0xc820016200

enter value cat:{tom 1 {4 [10 10 10 10]} map[right:{1} left:{0}] {123.45}}
in modify: cat:0xc8200162d0, cat.name:0xc8200162d0, cat.tail:0xc820016310
in modify: cat.legs.length:0xc8200162f0, cat.legs.count:0xc8200162e8
exit value cat:{Ben 1 {3 [0 10 10 10]} map[left:{2} right:{1}] {234.56}}

value catA:{tom 1 {4 [0 10 10 10]} map[left:{2} right:{1}] {123.45}}, catB:{tom 1 {4 [0 10 10 10]} map[left:{2} right:{1}] {123.45}}
address catA:0xc820016190, catB:0xc8200161e0
address catA.eye:0xc8200161c8, catB.eye:0xc820016218,
address catA.name:0xc820016190, catB.name:0xc8200161e0
address catA.tail:0xc8200161d0, catB.tail:0xc820016220
address catA.legs.count:0xc8200161a8, catB.legs.count:0xc8200161f8
address catA.legs.length:0xc8200161b0, catB.legs.length:0xc820016200

value catA:{tom 1 {4 [0 0 10 10]} map[left:{2} right:{0}] {123.45}}, catB:{Ben 1 {3 [0 0 10 10]} map[right:{0} left:{2}] {234.56}}  
address catA:0xc820016190, catB:0xc8200161e0
address catA.eye:0xc8200161c8, catB.eye:0xc820016218,
address catA.name:0xc820016190, catB.name:0xc8200161e0
address catA.tail:0xc8200161d0, catB.tail:0xc820016220
address catA.legs.count:0xc8200161a8, catB.legs.count:0xc8200161f8
address catA.legs.length:0xc8200161b0, catB.legs.length:0xc820016200

根据输出可以看到几点:

  1. catB 进行赋值之后,不管是 cat 本身,还是 cat 内部的各个属性的地址都已经改变了。
  2. 在函数内打印 cat 的几个属性的地址,可以看到和传入之前的 catB 是不同的。
  3. 在函数内对 cat 的名字修改后,并没有影响 catB,cat 是传值的。
  4. 通过赋值得到的 catB,直接修改 catB 的名字,也没有影响 catA 的名字。
  5. 这些看似符合 go 规范的说法。

几点意外:

  1. 我们在函数内对 cat 的左眼的修改,影响到了外部的 catB,甚至影响到了 catA!
  2. 在外部对 catB 的右眼的修改,影响到了 catA
  3. 在函数内外对 cat 的腿部的长度 (slice 类型) 的修改,都和眼睛有一样的效果。但是对腿部的数量 (int 类型) 的修改则没有。
  4. 这些看起来可不像传值该有的表现!!!

要理解这个问题,先来了解 slice 的底层结构。

2. 理解 Slice

slice 包括三个部分,一个指针 ptr,指向 slice 的第一个元素;一个长度 len 表示 slice 的长度,一个容量 cap 表示 slice 的容量。如下图:

go-slices-usage-and-internals_slice-struct

而 slice 的指针所指向 “第一个元素” 实际上是一个底层数组的第一个元素。可能会有多个 slice 共享着同一个底层数组。这种方式导致对 slice 的截取,拼接等操作都异常高效,可以在常数时间内完成。

考虑下面的数组,这样的数组可以通过make([]byte, 5)来创建:

go-slices-usage-and-internals_slice-1

然后对该数组执行s = s[2:4]操作:

go-slices-usage-and-internals_slice-2

对于底层数组而言一次都没变,仅仅是 slice 的头部三个元素变发生了变化。

容量和长度的关系是 len<=cap。一个 slice 的 cap 就是底层数组的长度。当 len>cap(比如做了一次很长的 append) 的时候,就需要构造一个容量更长的底层数组。

所以 slice 并不是简单的传值关系,就像指针和 chan 这些引用类型一样,map 和 slice 对底层元素的修改都是引用类型的,map 和 slice 的头部地址可以发生改变,但是他们引用到的底层数组可能公共的。

3. 闭包 (closure)

需要注意的是,go 的闭包也是引用类型。考虑下面的代码:

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
}

第二个函数是外部变量在闭包内通过被改变了的情况,defer 函数内的表达式会在它出现的地方就已经被求值,然后在退出函数之前,按照 defer 出现的顺序逆向执行。也就是说当第一条 defer 求值的时候,i=1,第五条 defer 求值的时候,i=5,对于两个函数都是如此。区别在于第一个函数的 i 在每次 defer 是传值进 printf 函数的,所以在 defer 中,i 等于有 5 份拷贝,而第二个函数使用闭包的方式引用了外部变量 i 其实只有一份!

要在闭包中避免上面的问题,可以有两种方式。

// 方法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
}

4. 深度拷贝

关于深度拷贝,这里有个使用 gob 序列化反序列化的例子:

func deepCopy(dst, src interface{}) error {
  var buf bytes.Buffer
  if err := gob.NewEncoder(&buf).Encode(src); err != nil {
    return err
  }
  return gob.NewDecoder(bytes.NewBuffer(buf.Bytes())).Decode(dst)
}

另外也有一些利用反射进行实现的方式:

5. 最佳实践

slice 和 map 都不支持==操作符,判断两个 slice 相等需要自己写循环判断,这种循环判断的方式效率并不会很低; map 不支持对元素取地址,如果这样做,编辑器会拒绝编译,原因是随着 map 容量的扩张,底层数据结构可能改变,导致所取得的地址无效。 对 slice 的元素取地址编译器是不禁止的,但是我们仍应该避免这样做,因为 slice 扩张也会导致在新的内存空间重新构造底层数组,而如果操作之前保存的地址值可能会导致无法预料的结果。

for…range…操作中,如果取了值,而不是通过去下标,像这样:for i,v := range s,其中 v 并不是 s 内元素的一个引用,改变 v 的值,并不能改变 s 中对应位置的元素。如果要这样做必须通过下标 s[i] 进行操作。

  1. 参考
  2. Go Slices: usage and internals
  3. effective_go.html#maps
  4. go spec#Calls

原文地址:http://kchu.me/2016/03/27/Go%E8%AF%AD%E8%A8%80%E4%BC%A0%E5%80%BC%E5%92%8C%E6%B7%B1%E6%B5%85%E5%A4%8D%E5%88%B6%E9%97%AE%E9%A2%98/

更多原创文章干货分享,请关注公众号
  • 加微信实战群请加微信(注明:实战群):gocnio
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册