深入瞭解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開發相關內容,望大家感興趣的支持一下,在此特別感謝。

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