一、数组
1.1 数组赋值给数组
Go数组是值类型,因此赋值操作和函数传参数会复制整个数组的数据,例:
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]))
}
结果:
a addr: 0xc04204a0e0, a[0] addr: 0xc04204a0e0
b addr: 0xc04204a100, b[0] addr: 0xc04204a100
可以看到,b的地址和a的地址不同,同时可以看到,数组的地址即为数组第一个元素的地址。
1.2 数组赋值给数组指针
上面已经看到数组直接赋值是值传递,可以考虑用指针来实现传地址,例:
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)
}
结果:
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 ,其结构定义为:
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
三个成员即切片指向的数据源数组地址,切片长度和切片容量。当切片之间传递时,实际上是SliceHeader 之间的值传递,由于传递之后,Data指向的数组地址是同一个,所以修改操作会同步。下面从几个实际例子来探究不同情况下的运行结果。
1.2 从数组得到切片
1、切片得到数组时,如果切片没有进行扩容,则指向的数据源还是此数组,任何对数组或切片的值修改操作,另一个的值也随之改变
切片未扩容
Go 切片是可以从数组得到的,以下代码,从切片得到数组:
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]))
}
结果:
a addr: 0xc04200c4a0, a[0] addr: 0xc04200c4a0
b addr: 0xc0420023e0, b[0] addr: 0xc04200c4a0
可以看到,切片b指向了新地址,但是第一个元素的地址和数组a的一致。那这里修改数组某个元素值切片的值会改变么,或者修改切片的某个元素值数组的值会改变么?下面继续测试:
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)
}
结果:
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操作:
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)
}
结果:
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函数来实现。例:
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)
}
结果:
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分析
当容量足够时,直接在当前长度的下一个位置保存数据,即使还有其他切片也指向这一块地址。例:
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)
}
结果:
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前后时,容量是否有变化,就能判断其运行结果。