深入瞭解Go Slice(一)—— make的詳細處理過程

前言

數組(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的方式有以下幾種方式:

  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)

本文主要介紹第一種方式,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開發相關內容,望大家感興趣的支持一下,在此特別感謝。

參考

[1]Arrays, slices (and strings): The mechanics of ‘append’

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