一、数组

数组赋值给数组

Go 数组是值类型,因此赋值操作和函数传参数会复制整个数组的数据,例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func main() {
  a := [3]int{1, 2, 3}
  b := a
  fmt.Printf("a addr: %p, a[0] addr: %p\n", &a, &(a[0]))
  fmt.Printf("b addr: %p, b[0] addr: %p\n", &b, &(b[0]))
  test(a)
}

func test(arr [3]int) {
  fmt.Printf("arr addr: %p, arr[0] addr: %p\n", &arr, &(arr[0]))
}

结果:

1
2
a addr: 0xc04204a0e0, a[0] addr: 0xc04204a0e0
b addr: 0xc04204a100, b[0] addr: 0xc04204a100

可以看到,b 的地址和 a 的地址不同,同时可以看到,数组的地址即为数组第一个元素的地址。

数组赋值给数组指针

上面已经看到数组直接赋值是值传递,可以考虑用指针来实现传地址,例:

1
2
3
4
5
6
func main() {
  a := [3]int{1, 2, 3}
  b := &a
  fmt.Printf("a addr: %p, a[0] addr: %p, value: %v\n", &a, &(a[0]), a)
  fmt.Printf("b addr: %p, value: %p, b[0] addr: %p, value: %v\n", &b, b, &(b[0]), b)
}

结果:

1
2
a addr: 0xc04204a0e0, a[0] addr: 0xc04204a0e0, value: [1 2 3]
b addr: 0xc04206a018, value: 0xc04204a0e0, b[0] addr: 0xc04204a0e0, value: &[1 2 3]

可以看到,指针的地址和数组的地址不一样,指针的值是数组的地址,即这里指针 b 是一个指向数组 a 地址的变量。这样无论是直接赋值给指针,还是在函数中用指针来传递,都可以达到不复制数组,并且修改原数组值的目的。

二、切片

切片运行时实际结构为 SliceHeader ,其结构定义为:

1
2
3
4
5
    type SliceHeader struct {
      Data uintptr
      Len  int
      Cap  int
    }

三个成员即切片指向的数据源数组地址,切片长度和切片容量。当切片之间传递时,实际上是 SliceHeader 之间的值传递,由于传递之后,Data 指向的数组地址是同一个,所以修改操作会同步。下面从几个实际例子来探究不同情况下的运行结果。

从数组得到切片

1、切片得到数组时,如果切片没有进行扩容,则指向的数据源还是此数组,任何对数组或切片的值修改操作,另一个的值也随之改变

切片未扩容

Go 切片是可以从数组得到的,以下代码,从切片得到数组:

1
2
3
4
5
6
func main() {
  a := [3]int{1, 2, 3}
  b := a[:]
  fmt.Printf("a addr: %p, a[0] addr: %p\n", &a, &(a[0]))
  fmt.Printf("b addr: %p, b[0] addr: %p\n", &b, &(b[0]))
}

结果:

1
2
a addr: 0xc04200c4a0, a[0] addr: 0xc04200c4a0
b addr: 0xc0420023e0, b[0] addr: 0xc04200c4a0

可以看到,切片 b 指向了新地址,但是第一个元素的地址和数组 a 的一致。那这里修改数组某个元素值切片的值会改变么,或者修改切片的某个元素值数组的值会改变么?下面继续测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func main() {
  a := [3]int{1, 2, 3}
  b := a[:]
  fmt.Printf("a addr: %p, a[0] addr: %p, value: %v\n", &a, &(a[0]), a)
  fmt.Printf("b addr: %p, b[0] addr: %p, value: %v\n", &b, &(b[0]), b
  a[0] = 10
  b[1] = 20
  fmt.Printf("a addr: %p, a[0] addr: %p, value: %v\n", &a, &(a[0]), a)
  fmt.Printf("b addr: %p, b[0] addr: %p, value: %v\n", &b, &(b[0]), b)
}

结果:

1
2
3
4
a addr: 0xc04204a0e0, a[0] addr: 0xc04204a0e0, value: [1 2 3]
b addr: 0xc0420463a0, b[0] addr: 0xc04204a0e0, value: [1 2 3]E
a addr: 0xc04204a0e0, a[0] addr: 0xc04204a0e0, value: [10 20 3]
b addr: 0xc0420463a0, b[0] addr: 0xc04204a0e0, value: [10 20 3]

可以看到,不管是修改数组的值,还是切片的值,另一个对应的值也改变了,这验证了切片指向的数据源是数组 a。

切片扩容情况

下面是从数组得到切片后,执行 append 操作:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func main() {
  a := [3]int{1, 2, 3}
  b := a[:]
  fmt.Printf("a addr: %p, a[0] addr: %p, value: %v\n", &a, &(a[0]), a)
  fmt.Printf("b addr: %p, b[0] addr: %p, value: %v\n", &b, &(b[0]), b)
  b = append(b, 4)
  fmt.Printf("a addr: %p, a[0] addr: %p, value: %v\n", &a, &(a[0]), a)
  fmt.Printf("b addr: %p, b[0] addr: %p, value: %v\n", &b, &(b[0]), b)

  a[0] = 10
  b[1] = 20
  fmt.Printf("a addr: %p, a[0] addr: %p, value: %v\n", &a, &(a[0]), a)
  fmt.Printf("b addr: %p, b[0] addr: %p, value: %v\n", &b, &(b[0]), b)
}

结果:

1
2
3
4
5
6
a addr: 0xc04204a0e0, a[0] addr: 0xc04204a0e0, value: [1 2 3]
b addr: 0xc0420463a0, b[0] addr: 0xc04204a0e0, value: [1 2 3]
a addr: 0xc04204a0e0, a[0] addr: 0xc04204a0e0, value: [1 2 3]
b addr: 0xc0420463a0, b[0] addr: 0xc042068030, value: [1 2 3 4]
a addr: 0xc04204a0e0, a[0] addr: 0xc04204a0e0, value: [10 2 3]
b addr: 0xc0420463a0, b[0] addr: 0xc042068030, value: [1 20 3 4]

可以看到,扩容后切片 b 第一个元素的地址发生了变化,因此后续对数组值得修改和对切片值的修改,都不会影响到另一个的值。

切片之间赋值

Golang 中切片是引用类型,直接赋值后,修改任意一个的某个元素值,另一个也会随之改变。但是如果在赋值之后,切片进行了扩容操作,则会指向新的数据源,因此修改值操作不会影响原来的切片。如果不想影响另一个的结果,可以用 copy 函数来实现。例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
func main() {
  a := []int{1, 2, 3}
  b := a
  fmt.Printf("a addr: %p, a[0] addr: %p, value: %v\n", &a, &(a[0]), a)
  fmt.Printf("b addr: %p, b[0] addr: %p, value: %v\n", &b, &(b[0]), b)  //b[0] 地址和 a[0] 地址一样
  fmt.Println("info a:", len(a), cap(a))
  fmt.Println("info b:", len(b), cap(b))

  b = append(b, 4)
  fmt.Printf("a addr: %p, a[0] addr: %p, value: %v\n", &a, &(a[0]), a)
  fmt.Printf("b addr: %p, b[0] addr: %p, value: %v\n", &b, &(b[0]), b)  //b[0] 地址变化了
  fmt.Println("info a:", len(a), cap(a))
  fmt.Println("info b:", len(b), cap(b))

  a[0] = 10
  b[1] = 20
  fmt.Printf("a addr: %p, a[0] addr: %p, value: %v\n", &a, &(a[0]), a)
  fmt.Printf("b addr: %p, b[0] addr: %p, value: %v\n", &b, &(b[0]), b)
}

结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
a addr: 0xc0420463a0, a[0] addr: 0xc04204a0e0, value: [1 2 3]
b addr: 0xc0420463c0, b[0] addr: 0xc04204a0e0, value: [1 2 3]
info a: 3 3
info b: 3 3
a addr: 0xc0420463a0, a[0] addr: 0xc04204a0e0, value: [1 2 3]
b addr: 0xc0420463c0, b[0] addr: 0xc042068030, value: [1 2 3 4]
info a: 3 3
info b: 4 6
a addr: 0xc0420463a0, a[0] addr: 0xc04204a0e0, value: [10 2 3]
b addr: 0xc0420463c0, b[0] addr: 0xc042068030, value: [1 20 3 4]

切片 append 分析

当容量足够时,直接在当前长度的下一个位置保存数据,即使还有其他切片也指向这一块地址。例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func main() {
  data := [5]int{1, 2, 3, 4, 5}
  a := data[0:1]
  b := data[0:4]
  fmt.Println("info a:", len(a), cap(a))
  fmt.Println("info b:", len(b), cap(b))  //切片 a 和 b 数据源都指向 data,但是长度不同
  fmt.Printf("a addr: %p, a[0] addr: %p, value: %v\n", &a, &(a[0]), a)
  fmt.Printf("b addr: %p, b[0] addr: %p, value: %v\n", &b, &(b[0]), b)
  a = append(a, 10)  //向 a 添加数据,直接在数据第二个位置保存数据,这样 data 结果也改变了
  fmt.Println("info a:", len(a), cap(a))
  fmt.Println("info b:", len(b), cap(b))
  fmt.Printf("a addr: %p, a[0] addr: %p, value: %v\n", &a, &(a[0]), a)
  fmt.Printf("b addr: %p, b[0] addr: %p, value: %v\n", &b, &(b[0]), b)
}

结果:

1
2
3
4
5
6
7
8
info a: 1 5
info b: 4 5
a addr: 0xc0420463a0, a[0] addr: 0xc042068030, value: [1]
b addr: 0xc0420463c0, b[0] addr: 0xc042068030, value: [1 2 3 4]
info a: 2 5
info b: 4 5
a addr: 0xc0420463a0, a[0] addr: 0xc042068030, value: [1 10]
b addr: 0xc0420463c0, b[0] addr: 0xc042068030, value: [1 10 3 4]

可以看到,由于多个切片指向同一块数据源,任何修改操作都会使得其他变量结果改变。

总结

对于切片,只要清楚它指向的数据源是否有改变,即关注 append 前后时,容量是否有变化,就能判断其运行结果。