深入了解Go Slice(二)—— 切片的详细处理过程

前言

Slice由3部分组成:指针、长度和容量,指针指向的底层数组,长度是当前容纳的数据长度,容量是能容量数据的最大长度。

其结构如下:

// runtime/slice.go
type slice struct {
    array unsafe.Pointer// 指向数组的指针
    len   int
    cap   int
}

创建slice的方式有以下几种方式:

  1. 直接通过make创建,可以指定len、cap

    slice := make([]int,5,10)

  2. 对数组/slice进行切片生成

    var data [10]int
    slice := data[2:8]
    slice = slice[1:3]

  3. 对slice进行append生成

    slice := make([]int,5,10)
    slice = append(slice,6)

第一种方式,深入了解Go Slice(一)—— make的详细处理过程一文中已做详细探讨,不再复述。本文主要对第二种方式进行研究。

本系列文章涉及到的builtin/builtin.go的func或Go的关键字都是在编译期间进行处理,这些调用过程在cmd/compile/internal/gc包下,其处理细节大多复杂。为简化细节,文章会使用部分reflect包中的代码来说明处理逻辑。reflect需要在runtime时重新构建出相关的Type、Value,其与直接调用的处理过程可能并不完全一致,但处理逻辑是一致的,因为两者操作的结果是一致的。

reflect内的func仅用来说明处理逻辑,实际使用builtin内的type/func或者Go关键字,都需要在编译时进行处理。

切片

同样以一个问题先热身,以下代码中的sl的len与cap分别是多少?

list := [10]int{}
sl := list[5:8]//sl=? len=? cap=?

我们可以很确认len=3 (8-5),但cap呢?我们从源码去找答案。

切片处理

你对切片实现有过好奇吗?通过左右节点如同对原数据进行裁切一般,即可获取新的数据。结合slice的结构,你能想象怎么去实现切片的功能吗?

不妨大胆猜测下。

slice的len/cap显然无法直接描述当前array的具体位置的,slice若想实现记录起始/结束位置的话只能通过array本身。

array持有连续的内存,每个索引对应的位置都存有数据。当我们对数组进行切片array[start:end]操作时,只需要将array的指针指向array[start],即可记录起始位置。但此时只记录了一个起始位置,若想存储两个地址,则需要然后通过len记录结束位置(end=start+len),从而记录了切片的完整位置。

这是基于使用机制的猜测,我们结合下reflect/value.go的Slice function来看下是否如所猜测的一样。

// Slice returns v[i:j].
// It panics if v's Kind is not Array, Slice or String, or if v is an unaddressable array,
// or if the indexes are out of bounds.
func (v Value) Slice(i, j int) Value {
    var (
        cap  int
        typ  *sliceType
        base unsafe.Pointer
    )
    switch kind := v.kind(); kind {
    default:
        panic(&ValueError{"reflect.Value.Slice", v.kind()})

    case Array:
        if v.flag&flagAddr == 0 {
            panic("reflect.Value.Slice: slice of unaddressable array")
        }
        tt := (*arrayType)(unsafe.Pointer(v.typ))
        cap = int(tt.len)
        typ = (*sliceType)(unsafe.Pointer(tt.slice))
        base = v.ptr

    case Slice:
        typ = (*sliceType)(unsafe.Pointer(v.typ))
        s := (*sliceHeader)(v.ptr)
        base = s.Data
        cap = s.Cap

    case String:
        s := (*stringHeader)(v.ptr)
        if i < 0 || j < i || j > s.Len {
            panic("reflect.Value.Slice: string slice index out of bounds")
        }
        var t stringHeader
        if i < s.Len {
            t = stringHeader{arrayAt(s.Data, i, 1, "i < s.Len"), j - i}
        }
        return Value{v.typ, unsafe.Pointer(&t), v.flag}
    }

    if i < 0 || j < i || j > cap {
        panic("reflect.Value.Slice: slice index out of bounds")
    }

    // Declare slice so that gc can see the base pointer in it.
    var x []unsafe.Pointer

    // Reinterpret as *sliceHeader to edit.
    s := (*sliceHeader)(unsafe.Pointer(&x))
    s.Len = j - i
    s.Cap = cap - i
    if cap-i > 0 {
        s.Data = arrayAt(base, i, typ.elem.Size(), "i < cap")
    } else {
        // do not advance pointer, to avoid pointing beyond end of slice
        s.Data = base
    }

    fl := v.flag.ro() | flagIndir | flag(Slice)
    return Value{typ.common(), unsafe.Pointer(&x), fl}
}

Slice func的注释中明确说明了返回的是v[i:j],且要求v是Array, Slice 或者String类型。func还要求如果是Array还必须是可寻址的Array,不然slice无法获取并存储Array的地址。平时使用切片为什么只有这几个特定类型的原因在此。

看下具体的处理过程:

  1. 声明cap、typ、base用以存储当前v的容量、类型、基准位置

  2. 判断v的类型,不符合的类型直接panic。

  3. 字符串类型

  • 将指针数据转为*stringHeader,判断i,j是否越界,是则panic
  • 构造新的stringHeader t
  • 如果i小于字符串的总长度,stringHeader的Data指向原数据第i元素的置,长度为j-i,赋值到给t
  • 返回新的t,结束
  1. 数组类型
  • 如果不可寻址,panic。
  • 将指针数据转为arrayType,cap为数组的长度,typ为arrayType的slice转的*sliceType,base为指针。
  1. Slice类型
  • 将指针数据转为sliceHeader,cap为slice的cap,base为slice的data,typ为sliceType
  1. 数组与Slice统一处理部分
  • 判断i,j是否越界,是则panic
  • 声明slice(原因是:以便gc可以看到其中的base指针,避免base被回收,转为*sliceHeader,修改sliceHeader
  • 设置slice的s.Len = j - i,s.Cap = cap - i
  • i<cap则,在基准的位置,指向数据的第i个元素的位置(即向后偏移i个元的位置);否则,赋值原数据的位置(len与cap均为0)
  • 返回slice

根据以上源码分析,我们可以知道:

  1. 切片后指针向后偏移i个元素的位置

  2. Array/Slice切片后的len、cap具体为

len=end-start
cap=cap-start

另外值得注意的是:

从string类型切片后的类型仍为string(stringHeader为string的底层结构),Array/Slice切片后的类型为Slice。

以上内容是基于reflect中的源码对slice操作作的解释性说明,如有谬误,欢迎指出。

现在我们可以回答上面问题中的cap = 5 =(10-5)。

Slice的数据变动问题

结合以上切片的具体处理过程,请回答以下代码运行后,data和list内的数据是什么?

data := [10]int{}
slice := data[5:9]
data[0] = 5

答案如下:

slice=[5 0 0 0]
data=[0 0 0 0 0 5 0 0 0 0]

如果在以上代码最后再添加以下代码?结果又是如何呢?

data[6] = 6

答案如下:

slice=[5 6 0 0]
data=[0 0 0 0 0 5 6 0 0 0]

为什么slice的改动会影响data的数据?为什么data的改动会影响slice的数据?

因为Array/Slice切片持有的是原Array/Slice的指针,意味着对Slice进行修改(不涉及扩容,扩容是另外一种情况,后续会单独讨论),必然会影响持有的原Array/Slice的数据。同样,原数据的变更,也会影响Slice。因此,Slice在使用时,务必确认在切片前后是否需要对原数据进行操作,如需保留原数据,请使用copy复制到新变量中,再进行切片操作。

总结

本文从源码详细讲述了切片的实现,以及切片后数据的变化。重点内容如下:

  1. 切片会改变指向底层数组指针的位置,切片的开始位置意味者底层数组指针偏移元素的位置。slice通过记录偏移的开始位置,再结合len,实现记录切片在底层数组数据的起始位置。

  2. 切片后的slice,len等于切片的结束end与起始位置start的差值,cap则是原cap与起始位置start的差值。

    len=end-start
    cap=cap-start

  3. 切片持有的是数组数据的指针,无论是切片数据变化还是底层数组数据变化,最终都会体现在底层数据以及上层的slice中,使用时需要注意。

公众号

鄙人刚刚开通了公众号,专注于分享Go开发相关内容,望大家感兴趣的支持一下,在此特别感谢。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章