golang与python中append的对比及底层分析

前言

类似于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}

可以看到,两个变量指向了不同的结果

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值