Go中的 slice 和 array

本文是听过Go进阶训练营《再探Golang的slice、map、channel》之后一些思考。

一、array

数组的定义:

var buffer [256]byte

array的存在是必要的,它可以很好的表示矩阵信息,但是在Go中数组的存在,更重要的意义在于作为slice的源数据存储介质。

二、slice

切片的定义:

var slice []byte = buffer[100:150]

那么slice这个变量在这里究竟是什么呢?我们采用下面的表述形式来理解slice:

type sliceHeader struct {
    Length        int
    ZerothElement *byte
}

slice := sliceHeader{
    Length:        50,
    ZerothElement: &buffer[100],
}

其中主要包含了长度和一个指向数组某个元素的指针。Go程序员们经常谈论“slice header”,因为“slice header”是slice变量实际存储的内容。

三、slice作函数入参

slice是个对象,包含了一个指针和一个长度值。它并不是一个指向对象的指针。
这一点非常重要。
举例如下:

func AddOneToEachElement(slice []byte) {
    for i := range slice {
        slice[i]++
    }
}

slice作为参数传到函数中时,实际是传递的slice header的复制。尽管复制了一份slice header,但是不论是原始slice header,还是复制的slice header,它们指向的同一个数组。所以上述函数,会把slice范围内的数组的元素都进行加一操作。
我们可以通过传递slice header改变数组中的值,但是由于传递的是slice header的复制,无法改变slice header的值。如果想要改变slice header的值,需要把新的slice header作为值返回。

四、 slice的Capacity

Capacity记录了数组元素的个数。当试图让slice的右边界超过Capacity时,会触发数组越界的panic。
此时sliceHeader的定义多了一个Capacity的字段:

type sliceHeader struct {
    Length        int
    Capacity      int
    ZerothElement *byte
}

这也符合了go源码中对于slice的定义:

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

五、make

我们无法让扩展slice超过它的Capacity,但是可以通过make指令新申请一个数组,把数据拷贝过去,让slice在新的数组上进行表述。例如:

    slice := make([]int, 10, 15)
    newSlice := make([]int, len(slice), 2*cap(slice))
    copy(newSlice, slice)
    slice = newSlice

六、append

append,有两点需要注意:

  1. slice扩容,当cap < 1024时,每次2;当cap >= 1024 时,每次1.25
  2. 由于slice header经常变动,所以append必须返回slice header的值,编译器也不允许调用append时,不对新的slice header进行保存
  3. 预先分配内存可以提升性能
  4. 直接使用index赋值而非append可以提升性能

理解了上述逻辑,slice作为入参传入函数时,如果slice未扩容,那么变更的数组还是在原来的数组上。如果slice扩容了,数组是新创建的,变更就不再原数组上了

七、nil

举例:

func main() {
    var s[]int
    b, _ := json.Marshal(s)
    fmt.Println(string(b))
}

上述程序会打印null,说明底层数组还没有创建。

func main {
    s := []int{}
    b, _ := json.Marshal(s)
    fmt.Println(string(b))
}

上述程序会打印[],说明这种方式底层的数组已经创建了。

这是一个坑点:

  • 使用[]Type{}或者make([]Type)初始化后,slice不为nil
  • 使用var x[]Type后,slice为nil

八、string

string是元素类型为byte的只读的slice。
因此可以进行如下操作:

slash := "/usr/ken"[0] // yields the byte value '/'.

或者

usr := "/usr/ken"[0:4] // yields the string "/usr"

可以对string和slice进行强制类型转换

str := string(slice)
slice := []byte(usr)

创建string的切片就开销很小,只需要创建string header就可以了,由于string是只读的slice,所以不同的string header可以安全地共享同一个数组。

九、一个技巧

下面的两个函数,哪个效率更高?

func normal(s []int) {
    i := 0
    i += s[0]
    i += s[1]
    i += s[2]
    i += s[3]
    println(i)
}

func bce(s []int) {
    _ = s[3]
    
    i := 0
    i += s[0]
    i += s[1]
    i += s[2]
    i += s[3]
    println(i)
}

bce的汇编会被编译器优化,减少slice越界的检查次数,所以bce函数的效率更高

这里的经验是,如果能确定访问的slice长度,可以先执行一次让编译器去做优化。

十、总结

明确slice header的概念,对理解slice的各种行为非常有帮助,希望对读者有所帮助。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值