golang数组和切片深入分析

一、数组

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前后时,容量是否有变化,就能判断其运行结果。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值