前言
Slice由3部分組成:指針、長度和容量,指針指向的底層數組,長度是當前容納的數據長度,容量是能容量數據的最大長度。
其結構如下:
// runtime/slice.go
type slice struct {
array unsafe.Pointer// 指向數組的指針
len int
cap int
}
創建slice的方式有以下幾種方式:
- 直接通過make創建,可以指定len、cap
slice := make([]int,5,10)
- 對數組/slice進行切片生成
var data [10]int
slice := data[2:8]
slice = slice[1: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的地址。平時使用切片爲什麼只有這幾個特定類型的原因在此。
看下具體的處理過程:
-
聲明cap、typ、base用以存儲當前v的容量、類型、基準位置
-
判斷v的類型,不符合的類型直接panic。
-
字符串類型
- 將指針數據轉爲*stringHeader,判斷i,j是否越界,是則panic
- 構造新的stringHeader t
- 如果i小於字符串的總長度,stringHeader的Data指向原數據第i元素的置,長度爲j-i,賦值到給t
- 返回新的t,結束
- 數組類型
- 如果不可尋址,panic。
- 將指針數據轉爲arrayType,cap爲數組的長度,typ爲arrayType的slice轉的*sliceType,base爲指針。
- Slice類型
- 將指針數據轉爲sliceHeader,cap爲slice的cap,base爲slice的data,typ爲sliceType
- 數組與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
根據以上源碼分析,我們可以知道:
-
切片後指針向後偏移i個元素的位置
-
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複製到新變量中,再進行切片操作。
總結
本文從源碼詳細講述了切片的實現,以及切片後數據的變化。重點內容如下:
-
切片會改變指向底層數組指針的位置,切片的開始位置意味者底層數組指針偏移元素的位置。slice通過記錄偏移的開始位置,再結合len,實現記錄切片在底層數組數據的起始位置。
-
切片後的slice,len等於切片的結束end與起始位置start的差值,cap則是原cap與起始位置start的差值。
len=end-start
cap=cap-start
-
切片持有的是數組數據的指針,無論是切片數據變化還是底層數組數據變化,最終都會體現在底層數據以及上層的slice中,使用時需要注意。
公衆號
鄙人剛剛開通了公衆號,專注於分享Go開發相關內容,望大家感興趣的支持一下,在此特別感謝。