初探Golang数据结构之Slice

在阅读Go语言圣经时,一直对数组和切片的使用场景好奇,不明白为什么推荐使用切片来代替数组。希望能通过一些梳理,能更好的理解切片和数组,找到他们合适的使用场景。

切片与数组

关于切片和数组怎么选择,我们来讨论下这个问题。

在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)
}

输出结果:

Go 复制代码
a : 0xc0000180a0 , [100 200]
b : 0xc0000180b0 , [100 200]
array : 0xc0000180f0 , [100 200]
array : 0xc000018110 , [100 200]

可以看到,四个内存地址都不相同,印证了前面的说法。当数组数据量达到百万级别时,复制数组会给内存带来巨大的压力,那能否通过传递指针来解决呢?

Go 复制代码
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
}

输出结果:

Go 复制代码
f1 array : 0xc0000b0008 , [100]
array : 0xc0000b0008 , [200]

可以看到,数组指针可以实现我们想要的效果,解决了复制数组带来的内存问题,不过函数接收的指针来自值拷贝,相对来说没有切片那么灵活。

Go 复制代码
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

Go 复制代码
type SliceHeader struct {
        Data uintptr // 指向数组的指针
        Len  int    // 当前切片的长度
        Cap  int    // 当前切片的容量,cap 总是 >= len
}
// 占用24个字节
fmt.Println(unsafe.Sizeof(reflect.SliceHeader{}))

切片是对数组一个连续片段的引用,这个片段可以是整个数组,也可以是数组的一部分。切片的长度可以在运行时修改,最小为0,最大为关联数组的长度,切片是一个长度可变的动态窗口

创建切片

使用make

Go 复制代码
slice := make([]int, 4, 6)

内存空间申请了6个int类型的内存大小。由于len=4,所以后面2个空间暂时无法访问到,但是容量是存在的。此时数组里每个变量都=0。

字面量

Go 复制代码
slice := []int{0, 1, 2}

nil切片和空切片

Go 复制代码
// nil 切片
var s []int
// 空切片 
s2 := make([]int, 0)
s3 := []int{}

空切片和 nil 切片的区别在于,空切片指向的地址不是nil,指向的是一个内存地址,但是它没有分配任何内存空间,即底层元素包含0个元素。

Go 复制代码
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,原因详见bytetech.info/articles/71...

需要说明的一点是,不管是使用 nil 切片还是空切片,对其调用内置函数 append,len 和 cap 的效果都是一样的。

但使用append时要注意:

  • 如果append追加的数据长度小于等于cap-len,只做一次数据拷贝。

  • 如果append追加的数据长度大于cap-len,则会分配一块更大的内存,然后把原数据拷贝过来,再进行追加。

特别当我们需要构建一个切片,是从原有切片复制而来时,要注意值覆盖问题。

Go 复制代码
func main() {
    s1 := []int{0, 1, 2, 3} // 先定义一个现有切片
    s2 := s1[0:1]           // 复制现有的切片
    s2 = append(s2, 100)
    fmt.Print(s1)
}

输出结果

Go 复制代码
[0 100 2 3]

原因还是切片本质上是数组的一个动态窗口,当cap够用时,不会新开辟内存空间进行复制,此时对数组的任何修改,都会对其他代理此数组的切片产生连带影响。

相关推荐
HyggeBest11 小时前
Golang 并发原语 Sync Once
后端·go
zhuyasen1 天前
当Go框架拥有“大脑”,Sponge框架集成AI开发项目,从“手写”到一键“生成”业务逻辑代码
后端·go·ai编程
写代码的比利1 天前
Kratos 对接口进行加密转发处理的两个方法
go
chenqianghqu1 天前
goland编译过程加载dll路径时出现失败
go
马里嗷1 天前
Go 1.25 标准库更新
后端·go·github
郭京京2 天前
go语言redis中使用lua脚本
redis·go·lua
心月狐的流火号2 天前
分布式锁技术详解与Go语言实现
分布式·微服务·go
一个热爱生活的普通人2 天前
使用 Makefile 和 Docker 简化你的 Go 服务部署流程
后端·go