初探Golang数据结构之Slice的使用
作者:freesia13279
在阅读Go语言圣经时,一直对数组和切片的使用场景好奇,不明白为什么推荐使用切片来代替数组。希望能通过一些梳理,能更好的理解切片和数组,找到他们合适的使用场景。
切片与数组
关于切片和数组怎么选择,我们来讨论下这个问题。
在Go中,数组是值类型,赋值和函数传参都会复制整个数组数据。
func main() { a := [2]int{100, 200} // 赋值 var b = a fmt.Printf("a : %p , %v\n", &a, a) fmt.Printf("b : %p , %v\n", &b, b) // 函数传参 f(a) f(b) } func f(array [2]int) { fmt.Printf("array : %p , %v\n", &array, array) }
输出结果:
a : 0xc0000180a0 , [100 200]
b : 0xc0000180b0 , [100 200]
array : 0xc0000180f0 , [100 200]
array : 0xc000018110 , [100 200]
可以看到,四个内存地址都不相同,印证了前面的说法。当数组数据量达到百万级别时,复制数组会给内存带来巨大的压力,那能否通过传递指针来解决呢?
func main() { a := [1]int{100} f1(&a) fmt.Printf("array : %p , %v\n", &a, a) } func f1(p *[1]int) { fmt.Printf("f1 array : %p , %v\n", p, *p) (*p)[0] += 100 }
输出结果:
f1 array : 0xc0000b0008 , [100]
array : 0xc0000b0008 , [200]
可以看到,数组指针可以实现我们想要的效果,解决了复制数组带来的内存问题,不过函数接收的指针来自值拷贝,相对来说没有切片那么灵活。
func main() { a := [1]int{100} f1(&a) // 切片 b := a[:] f2(&b) fmt.Printf("array : %p , %v\n", &a, a) } func f1(p *[1]int) { fmt.Printf("f1 array : %p , %v\n", p, *p) (*p)[0] += 100 } func f2(p *[]int) { fmt.Printf("f2 array : %p , %v\n", p, *p) (*p)[0] += 100 } //输出结果 f1 array : 0xc000018098 , [100] f2 array : 0xc00000c030 , [200] array : 0xc000018098 , [300]
可以看到,切片的指针和原来数组的指针是不同的。
总结
通常来说,使用数组进行参数传递会消耗较多内存,采用切片可以避免此问题。切片是引用传递,不会占用较多内存,效率更高一些。
切片的数据结构
切片在编译期是 cmd/compile/internal/types/type.go 包下的Slice类型,而它的运行时的数据结构位于 reflect.SliceHeader
type SliceHeader struct { Data uintptr // 指向数组的指针 Len int // 当前切片的长度 Cap int // 当前切片的容量,cap 总是 >= len } // 占用24个字节 fmt.Println(unsafe.Sizeof(reflect.SliceHeader{}))
切片是对数组一个连续片段的引用,这个片段可以是整个数组,也可以是数组的一部分。切片的长度可以在运行时修改,最小为0,最大为关联数组的长度,切片是一个长度可变的动态窗口。
创建切片
使用make
slice := make([]int, 4, 6)
内存空间申请了6个int类型的内存大小。由于len=4,所以后面2个空间暂时无法访问到,但是容量是存在的。此时数组里每个变量都=0。
字面量
slice := []int{0, 1, 2}
nil切片和空切片
// nil 切片 var s []int // 空切片 s2 := make([]int, 0) s3 := []int{}
空切片和 nil 切片的区别在于,空切片指向的地址不是nil,指向的是一个内存地址,但是它没有分配任何内存空间,即底层元素包含0个元素。
func main() { var s []int s2 := []int{} s3 := make([]int, 0) fmt.Println(s == nil) fmt.Println(s2 == nil) fmt.Println(s3 == nil) } // 输出结果 true false false
简单说,nil切片指针值为nil;而空切片的指针值不为nil,
需要说明的一点是,不管是使用 nil 切片还是空切片,对其调用内置函数 append,len 和 cap 的效果都是一样的。
但使用append时要注意:
- 如果append追加的数据长度小于等于cap-len,只做一次数据拷贝。
- 如果append追加的数据长度大于cap-len,则会分配一块更大的内存,然后把原数据拷贝过来,再进行追加。
特别当我们需要构建一个切片,是从原有切片复制而来时,要注意值覆盖问题。
func main() { s1 := []int{0, 1, 2, 3} // 先定义一个现有切片 s2 := s1[0:1] // 复制现有的切片 s2 = append(s2, 100) fmt.Print(s1) }
输出结果
[0 100 2 3]
原因还是切片本质上是数组的一个动态窗口,当cap够用时,不会新开辟内存空间进行复制,此时对数组的任何修改,都会对其他代理此数组的切片产生连带影响。
到此这篇关于初探Golang数据结构之Slice的使用的文章就介绍到这了,更多相关Go Slice内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!