前言
數組(Array)是一個由固定長度的特定類型元素組成的序列,一個數組可以由零個或多個元素組成。因其長度的不可變動,數組在Go中很少直接使用。作爲替代是Slice(切片),它是可以增長和收縮的動態序列,Slice功能也更靈活,在Go中有着廣泛使用。
關於Slice的使用有很多文章已經介紹了,本系列文章無意再重複介紹使用過程,主要專注於瞭解Slice的結構及底層的處理邏輯,從源碼的角度加深對使用的瞭解,解決一些常見的使用錯誤點。
本系列文章涉及到的builtin/builtin.go的func或Go的關鍵字都是在編譯期間進行處理,這些調用過程在cmd/compile/internal/gc包下,其處理細節大多複雜。爲簡化細節,文章會使用部分reflect包中的代碼來說明處理邏輯。reflect需要在runtime時重新構建出相關的Type、Value,其與直接調用的處理過程可能並不完全一致,但處理邏輯是一致的,因爲兩者操作的結果是一致的。
reflect內的func僅用來說明處理邏輯,實際使用builtin內的type/func或者Go關鍵字,都需要在編譯時進行處理。
概念
先從一個問題開始熱身:
以下哪個變量的類型是Array?哪個是Slice?
var a [10]int
b := make([]int,10)
c := a[5:9]
定義
A slice is a data structure describing a contiguous section of an array stored separately from the slice variable itself. A slice is not an array. A slice describes a piece of an array.1
以上定義來自Go官方博客關於slices相關機制的說明,大致意思如下:
Slice是一種數據結構,描述與Slice變量本身分開存儲的Array的連續部分。 Slice不是Array。Slice描述了Array的一部分。
類型與聲明
Array的類型:
[len]Type
Slice的類型:
[]Type
聲明變量時如下:
var a [10]int// Array
b := make([]int,10)// 直接聲明Slice
結構
Slice由3部分組成:指針、長度和容量,指針指向的底層數組,長度是當前容納的數據長度,容量是能容量數據的最大長度。
Slice底層實際是一個struct,結構如下:
// 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)
本文主要介紹第一種方式,slice make的過程。
make過程
slice描述了Array的一部分,從slice的結構可以看到大致的關係了,現在從底層的創建過程來確認兩者的關係。
makeslice
slice的array確實是數組嗎?先看下slice與array的創建過程:
//以下代碼來源於:runtime/malloc.go
func makeslice(et *_type, len, cap int) unsafe.Pointer {
mem, overflow := math.MulUintptr(et.size, uintptr(cap))
if overflow || mem > maxAlloc || len < 0 || len > cap {
// NOTE: Produce a 'len out of range' error instead of a
// 'cap out of range' error when someone does make([]T, bignumber).
// 'cap out of range' is true too, but since the cap is only being
// supplied implicitly, saying len is clearer.
// See golang.org/issue/4085.
mem, overflow := math.MulUintptr(et.size, uintptr(len))
if overflow || mem > maxAlloc || len < 0 {
panicmakeslicelen()
}
panicmakeslicecap()
}
return mallocgc(mem, et, true)
}
makeslice分配的內存大小爲類型et的size * cap
,創建時會判斷是否超過允許的分配的最大內存。
newarray
// newarray allocates an array of n elements of type typ.
func newarray(typ *_type, n int) unsafe.Pointer {
if n == 1 {
return mallocgc(typ.size, typ, true)
}
mem, overflow := math.MulUintptr(typ.size, uintptr(n))
if overflow || mem > maxAlloc || n < 0 {
panic(plainError("runtime: allocation size out of range"))
}
return mallocgc(mem, typ, true)
}
newarray分配的內存大小爲size * len
,與makeslice比,主要少了cap相關的檢查。兩者結合看,slice
在make時會先創建cap
大小的array
,這是Slice與Array的最直接的聯繫。
slice與array的關係
從makeslice的源碼結合newarray來看,makeslice除校驗cap外,實際就是在創建一個大小爲cap的數組。
如果覺得兩者的關係還不夠直觀,我們從reflect/value.go中的MakeSlice的源碼看下:
// SliceHeader is the runtime representation of a slice.
// It cannot be used safely or portably and its representation may
// change in a later release.
// Moreover, the Data field is not sufficient to guarantee the data
// it references will not be garbage collected, so programs must keep
// a separate, correctly typed pointer to the underlying data.
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
// sliceHeader is a safe version of SliceHeader used within this package.
type sliceHeader struct {
Data unsafe.Pointer
Len int
Cap int
}
// MakeSlice creates a new zero-initialized slice value
// for the specified slice type, length, and capacity.
func MakeSlice(typ Type, len, cap int) Value {
if typ.Kind() != Slice {
panic("reflect.MakeSlice of non-slice type")
}
if len < 0 {
panic("reflect.MakeSlice: negative len")
}
if cap < 0 {
panic("reflect.MakeSlice: negative cap")
}
if len > cap {
panic("reflect.MakeSlice: len > cap")
}
s := sliceHeader{unsafe_NewArray(typ.Elem().(*rtype), cap), len, cap}
return Value{typ.(*rtype), unsafe.Pointer(&s), flagIndir | flag(Slice)}
}
sliceHeader是slice在運行時的表示,sliceHeader在構造時,先通過unsafe_NewArray創建Data。而unsafe_NewArray就是調用的newarray,因此MakeSlice就是創建一個持有cap大小的數組的sliceHeader。
//go:linkname reflect_unsafe_NewArray reflect.unsafe_NewArray
func reflect_unsafe_NewArray(typ *_type, n int) unsafe.Pointer {
return newarray(typ, n)
}
總結
本文主要從源碼的角度去探究Slice與Array的關係。slice底層是一個strcut類型的數據結構,slice的在make時會先創建一個cap大小的數組,然後持有數組的指針,從而去描述一個數組。這是基礎的數據結構,後續的使用均是建立在此基礎上的。
思考
我們知道對數組、Slice進行切片操作可以返回新的slice類型的數據,string也可以進行切片操作,但返回的數據類型並不是slice,而是string,這是爲什麼呢?後續我們一起接着探索切片的過程吧!
公衆號
鄙人剛剛開通了公衆號,專注於分享Go開發相關內容,望大家感興趣的支持一下,在此特別感謝。