Go进阶之切片实现原理并高效使用

0 阅读8分钟

Go中map的内部实现.png slice中文多译为切片.是Go语言在数组之上提供的一个重要的抽象数据类型.在Go语

言中.对于绝大多数需要使用数组的组合.切片实现了完美替代.并且和数组相比.切片提

供了更灵活 更高效的数据序列访问接口.

1.切片是什么:

Go语言数组是一个固定长度的 容纳同构类型元素的连续序列.因此Go数组类型具有

两个属性.元素类型和数组长度.这两个类型都相同的数组类型是等价的.比如以下变量

a b c对应的数组类型是三个不同的数组类型.

var a [8]int

var b [8]byte 

var c [9]int

变量a b对应的数组类型长度属性相同.但元素类型不同(一个是int 另一个是byte).变

量a c对应的数组类型的元素相同.都是int但是类型长度不同(一个是8.一个是9).

Go数组是值语义的.这意味这一个数组变量表示的是整个数组.在Go语言中传递数组

是纯粹的值拷贝.对于元素类型长度较大或元素个数较多的数组.如果直接以数组类型

参数传递到函数中会有不小的性能损耗.这时会有很多人使用数组指针类型来定义函

数.然后将数组地址传进函数.这样的确可以避免性能损耗.在Go语言中.更地道的方式

是使用切片.

切片之于数组就像是文件描述符之于文件.在Go语言中.数组更多是"退居幕后".承担

的是底层存储空间的角色.而切片则走向"前台".为底层的存储(数组)打开了一个访问

的"窗口".

因此可以称切片是数组的描述符.切片之所以能在函数参数传递时避免较大性能损耗.

因为它是"描述符"的特性.切片这个描述符是固定大小.无论底层的数组元素类型有多

大.切片打开的窗口有多长.

运行时表示:

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

array:指向下层数组某元素的指针.该元素也是切片的起始元素.

len:切片的长度.即切片中当前元素的个数.

cap:切片的最大容量.cap>=len.

在运行时.每个切片变量都是一个runtime.slice结构体类型的实例.

s := make([]byte,5)

切片s运行层面表示:

通过上述语句创建的切片.编译器会自动为切片建立一个底层数组.如果没有make中指定cap参数.那么cap=len.即编译器建立的数组长度len.

可以通过语法u[low:high]创建对已存在数组进行操作的切片.这被称为数组的切片化(slicing):

u := [10]byte{11,12,13,14,15,16,17,18,19,20}

s := u[3:7]

可以看到切片打开了一个操作数组u的窗口.通过s看到的第一个元素是u[3].通过s能看到并操作的数组元素个数为4个(high-low).切片的容量值(cap)取决于底层数组的长度.从切片s的第一个元素s[0].即u[3]到数组末尾一共有7个存储元素的槽位,因此切片s的cap为7.也可以为一个已存在数组建立多个操作数组的切片.

三个切片s1 s2 s3都是数组u的描述符.无论哪个切片对数组进行的修改操作都会反映到其他切片中.比如将s3[0]置为24.那么s1[2]也会变成24.因为s3[0]直接操作的是底层数组u的第四个元素u[3].

还可以通过语法s[low:high]基于已有切片创建新的切片.这被称为切片的reslicing.新创建的切片与原切片同样是共享底层数组的.并通过新切片对数组的修改也会反映到原切片中.

当切片作为函数参数传递给函数时.实际传递的是切片的内部表示.也就是上面的runtime.slice结构实例.因此无论切片描述的底层数组有多大.切片作为参数传递带来的性能损耗都是很小且恒定的.甚至可以小到忽略不计.这就是函数在参数中多使用切片而不用数组指针的原因之一.另一个原因是就是切片可以提供比指针更为强大的功能.比如下标访问 边界溢出校验 动态扩容等.

2.切片的高级特性:动态扩容

Go切片还支持一个重要的高级特性:动态扩容.切片类型是部分满足零值可用理念的.即零值切片也可以通过append预定义函数进行元素赋值操作.

var s []byte  //s被赋予零值nil.

s = append(s,1)

由于初值为零值.s这个描述符并没有绑定对应的底层数组.而经过append操作后.s显然已绑定了属于它的底层数组.

func main() {
    //s被赋予零值nil.
    var s []int
    s = append(s, 11)
    fmt.Println(len(s), cap(s))
    s = append(s, 12)
    fmt.Println(len(s), cap(s))
    s = append(s, 13)
    fmt.Println(len(s), cap(s))
    s = append(s, 14)
    fmt.Println(len(s), cap(s))
    s = append(s, 15)
    fmt.Println(len(s), cap(s))
}

可以看到切片s的len值是线性增长的.但cap值呈现出不规则的变化.

1).最初s初值为零值(nil).此时s没有绑定底层数组.

2).通过append操作向切片s添加一个元素11.此时append会首先分配底层数组u1(数组长度1).然后将s内部表示中的array指向u1.并设置len=1.cap=1.

3).通过append操作向切片s添加一个元素12.此时len(s)=1.cap(s)=1.append判断底层数组剩余空间不满足添加新元素的要求.于是创建了一个底层数组u2.长度为2(u1数组长度的2倍).并将u1中的元素复制到u2中.最后将s内部表示的array指向u2.并设置len=2,cap=2.

4).通过append操作向切片s再添加一个元素13.此时len(s)=2.cap(s)=2.append判断底层数组剩余空间不满足新元素的要求.于是创建了一个新的底层数组u3.长度为4(u2数组长度的2倍).并将u2中的元素赋值到u3中.最后将内部的array指向u3.设置len=3.cap=4.

5).通过append操作向切片s再添加一个元素14.此时len(s)=3.cap(s)=4.append判断底层数组剩余空间满足添加新元素的要求.于是将14放在下一个元素的位置(数组u3末尾).并将s内部表示中的len加1变为4.

6).通过append操作向切片添加最后一个元素15.此时len(s)=4.cap(s)=4.append判断底层数组剩余空间不满足添加新元素的要求,于是创建了一个新的底层数组u4.长度为8(u3数组长度的2倍).并将u3中的元素赋值到u4中.最后将array指向u4.并设置len(s)=5.cap为u4的数组长度.

append会根据切片需要.在当前底层数组容量无法满足的情况下.动态分配新的数组.新数组长度会按一定算法扩展.新数组建立后.append会把旧数组中的数据复制到新数组中.之后新数组便成为切片的底层数组.旧数组后续会被垃圾回收掉.

func main() {
    u := []int{11, 12, 13, 14, 15}
    fmt.Println("array:", u)
    s := u[1:3]
    fmt.Printf("slice(len=%d,cap=%d):%v\n", len(s), cap(s), s)
    s = append(s, 24)
    fmt.Println("after append 24,array:", u)
    fmt.Printf("after append 24,slice(len=%d,cap=%d):%v\n", len(s), cap(s), s)
    s = append(s, 25)
    fmt.Println("after append 25,array:", u)
    fmt.Printf("after append 25,slice(len=%d,cap=%d):%v\n", len(s), cap(s), s)
    s = append(s, 26)
    fmt.Println("after append 26,array:", u)
    fmt.Printf("after append 26,slice(len=%d,cap=%d):%v\n", len(s), cap(s), s)

    s[0] = 22
    fmt.Println("after reassign 1st elem of slice,array:", u)
    fmt.Printf("after reassign 1st elem of slice,slice(len=%d,cap=%d):%v\n", len(s), cap(s), s)
}

从结果可以看出.当达到底层数组u的边界后.此后在添加元素26.append发现底层数组不满足要求了.于是创建了一个新的底层数组.并将原切片元素复制到新数组,

3.尽量使用cap参数创建切片:

append操作是一件利器.它让切片类型部分满足了"零值可用"的理念.从append的原理中能看到重新分配底层数组并复制元素的操作代价还是蛮大的.尤其是当元素较多的情况下.可以通过切片的利用场景对切片容量规模进行预估.并在创建新切片的时候以cap的形式传递给内置函数.

func BenchmarkSliceInitWithoutCap(b *testing.B) {
    for n := 0; n < b.N; n++ {
       s1 := make([]int, 0)
       for i := 0; i < 10000; i++ {
          s1 = append(s1, i)
       }
    }
}

func BenchmarkSliceInitWithCap(b *testing.B) {
    for n := 0; n < b.N; n++ {
       s1 := make([]int, 0, 10000)
       for i := 0; i < 10000; i++ {
          s1 = append(s1, i)
       }
    }
}

而今才道当时错.心绪凄迷.红泪偷垂.满眼春风百事非.

情知此后无来计.强说欢期.一别如斯.落尽梨花月又西. 纳兰.

语雀地址www.yuque.com/itbosunmian…?

《Go.》 密码:xbkk 欢迎大家访问.提意见.

如果大家喜欢我的分享的话.可以关注我的微信公众号

念何架构之路