前言
类似于python,golang中同样提供了 append函数,并且pyhon中的list和golang中的切片都属于引用类型,即函数中对参数的操作会影响到原变量。但实际上,两者的用法并不相同,因此容易造成误用。
测试
python中的append
def func(a):
a.append(2)
if __name__ == '__main__':
a = [1]
func(a)
print(a)
输出
[1, 2]
函数会在变量a的尾部添加一个2,因为是对变量a指向的数组直接操作,因此对主函数中的变量产生了影响。
go中的append
golang中的append常见写法如下
func main() {
s := []int{1}
slice_test(s)
fmt.Println(s)
}
func slice_test(s []int) {
s = append(s, 2)
}
输出
[1]
与python不同的是 append并不是切片对象本身的方法,而是额外的函数,以返回值的方法返回添加之后的结果。因此s = append(s, 2)
的操作修改的不是s指向的数组而是s本身,相对于使得s切片指向了一片新的地址,因而并不会对主函数造成影响。
下面代码显示了操作前后s的地址
func main() {
s := []int{1}
fmt.Printf("主函数中s的地址:%p\n", s)
slice_test(s[:])
fmt.Println(s)
}
func slice_test(s []int) {
fmt.Printf("append前s的地址:%p\n", s)
s = append(s, 2)
fmt.Printf("append后s的地址:%p\n", s)
}
输出
主函数中s的地址:0xc00000e098
append前s的地址:0xc00000e098
append后s的地址:0xc00000e0c0
关于python中列表的加法
在python中,如下写法类似于go中的这种情况。
def func2(a):
a=a+[2]
if __name__ == '__main__':
a = [1]
func2(a)
print(a)
输出
[1]
但注意,在python的list中 a+=b和a=a+b的实现是不同的
def func3(a):
a+=[2]
if __name__ == '__main__':
a = [1]
func3(a)
print(a)
输出
[1, 2]
因为这两种写法分别对应了__add__
和__iadd__
两个内置方法(魔法函数)。而__add__
是将相加结果作为返回值返回并不会影响变量本身,而__iadd__
会对变量本身就行修改并且返回这个对象本身。
def func4(a):
print('id of a=', id(a))
c=a.__iadd__([2])
print('id of c=', id(c))
print('c=', c)
if __name__ == '__main__':
a = [1]
func4(a)
print('a=',a)
输出结果
id of a= 1725500185920
id of c= 1725500185920
c= [1, 2]
a= [1, 2]
可以看到a和c的地址是相同的。
问题解决
但是有些场景下,我们希望在函数中的append操作得到保留,这一问题可以借助指针解决。
func main() {
s := []int{1}
fmt.Printf("主函数中&s的地址:%p\n", &s)
slice_test2(&s)
fmt.Println(s)
}
func slice_test2(p *[]int) {
fmt.Printf("append前p的地址:%p\n", p)
*p = append(*p, 2)
fmt.Printf("append后p的地址:%p\n", p)
}
输出
主函数中&s的地址:0xc000092060
append前p的地址:0xc000092060
append后p的地址:0xc000092060
[1 2]
这里我们将s的指针而非s的本身传入,因此后序修改的是指针p指向的地址空间的内容,所以在函数中的操作得到了保留。
拓展
Go语言:append函数源码学习及切片深度拷贝问题_go append源码_pengpengzhou的博客-CSDN博客
调用append函数时,当原有长度加上新追加的长度如果超过容量则会新建一个数组,新旧切片会指向不同的数组;如果没有超过容量则在原有数组上追加元素,新旧切片会指向相同的数组,这时对其中一个切片的修改会同时影响到另一个切片。
因此建议始终使用切片本身接收append的返回值,避免引用混乱。
测试
当s容量为1时
func main() {
s := make([]int, 1, 1) // 这里s的容量也是1,append会开辟新的数组。
s[0] = 1
slice_test3(s)
fmt.Println(s)
}
func slice_test3(s []int) {
fmt.Printf("append前s的地址:%p\n", s)
s = append(s, 2)
fmt.Printf("append后s的地址:%p\n", s)
s[0] = 0
}
输出
PS D:\code\GO\test> go run .\main.go // 可以看到地址不同,对s[0]的修改也无效
append前s的地址:0xc00009e058
append后s的地址:0xc00009e080
[1]
执行完s := append(s, 2)
语句后,s指向了一片新的地址,因此即使对s[0]进行修改,也不会对原函数造成影响。
而当s容量为2时,append不会开辟新的数组
func main() {
s := make([]int, 1, 2) // 这里s的容量是2,append不会开辟新的数组。
s[0] = 1
slice_test3(s)
fmt.Println(s)
}
func slice_test3(s []int) {
fmt.Printf("append前s的地址:%p\n", s)
s = append(s, 2)
fmt.Printf("append后s的地址:%p\n", s)
s[0] = 0
}
输出
PS D:\code\GO\test> go run .\main.go // 可以看到地址相同,对s[0]的修改也影响到了主函数
append前s的地址:0xc00000e0b0
append后s的地址:0xc00000e0b0
[0]
此时对s[0]的修改就会影响到主函数中的s。
切片的底层数据结构
后端 - go源码分析——切片 - 个人文章 - SegmentFault 思否
golang中的slice切片底层模型reflect.SliceHeader_golang sliceheader_raoxiaoya的博客-CSDN博客
切片本质上是一个结构体slice,他的属性由下面三部分组成:
- array:元素存哪里
- len:已经存了多少
- cap:容量是多少
type slice struct {
array unsafe.Pointer //数组的指针
len int //切片长度
cap int //切片容量
}
而slice 的底层模型为reflect.SliceHeader
,我们通过这个反射机制,是可以查看这个结构体的。
func main() {
s := make([]int, 1, 2)
s[0] = 1
slice_test3(s)
sh := (*reflect.SliceHeader)(unsafe.Pointer(&s))
fmt.Println(s)
fmt.Printf("主函数中, s的底层数据结构为%v\n", sh)
}
func slice_test3(s []int) {
fmt.Printf("append前s的地址:%p\n", s)
s = append(s, 2)
fmt.Printf("append后s的地址:%p\n", s)
s[0] = 0
sh := (*reflect.SliceHeader)(unsafe.Pointer(&s))
fmt.Printf("测试函数中,s的底层数据结构为%v\n", sh)
}
输出
PS D:\code\GO\test> go run .\main.go
append前s的地址:0xc00009e070
append后s的地址:0xc00009e070
测试函数中,s的底层数据结构为&{824634368112 2 2}
[0]
主函数中, s的底层数据结构为&{824634368112 1 2}
可以看到内部结构体的指针指向的相同的位置。
进一步地,我们可以借助sh, 将成员变量len修改为2:
func main() {
s := make([]int, 1, 2)
s[0] = 1
slice_test3(s)
sh := (*reflect.SliceHeader)(unsafe.Pointer(&s))
sh.Len = 2
fmt.Println(s)
fmt.Printf("主函数中, s的底层数据结构为%v\n", sh)
}
func slice_test3(s []int) {
fmt.Printf("append前s的地址:%p\n", s)
s = append(s, 2)
fmt.Printf("append后s的地址:%p\n", s)
s[0] = 0
sh := (*reflect.SliceHeader)(unsafe.Pointer(&s))
fmt.Printf("测试函数中,s的底层数据结构为%v\n", sh)
不出所料,输出结果为:
PS D:\code\GO\test> go run .\main.go
append前s的地址:0xc00000e0b0
append后s的地址:0xc00000e0b0
测试函数中,s的底层数据结构为&{824633778352 2 2}
[0 2]
主函数中, s的底层数据结构为&{824633778352 2 2}
另外,看一下当创建切片时,指定cap=1的情况
func main() {
s := make([]int, 1, 1)
s[0] = 1
slice_test3(s)
sh := (*reflect.SliceHeader)(unsafe.Pointer(&s))
fmt.Println(s)
fmt.Printf("主函数中, s的底层数据结构为%v\n", sh)
}
func slice_test3(s []int) {
fmt.Printf("append前s的地址:%p\n", s)
s = append(s, 2)
fmt.Printf("append后s的地址:%p\n", s)
s[0] = 0
sh := (*reflect.SliceHeader)(unsafe.Pointer(&s))
fmt.Printf("测试函数中,s的底层数据结构为%v\n", sh)
}
输出
PS D:\code\GO\test> go run .\main.go
append前s的地址:0xc00000e098
append后s的地址:0xc00000e0c0
测试函数中,s的底层数据结构为&{824633778368 2 2}
[1]
主函数中, s的底层数据结构为&{824633778328 1 1}
可以看到,两个变量指向了不同的结果