天天看點

golang快速入門[9.2]-深入數組用法、陷阱與編譯時

前文

  • golang快速入門[1]-go語言導論
  • golang快速入門[2.1]-go語言開發環境配置-windows
  • golang快速入門[2.2]-go語言開發環境配置-macOS
  • golang快速入門[2.3]-go語言開發環境配置-linux
  • golang快速入門[3]-go語言helloworld
  • golang快速入門[4]-go語言如何編譯為機器碼
  • golang快速入門[5.1]-go語言是如何運作的-連結器
  • golang快速入門[5.2]-go語言是如何運作的-記憶體概述
  • golang快速入門[5.3]-go語言是如何運作的-記憶體配置設定
  • golang快速入門[6.1]-內建開發環境-goland詳解
  • golang快速入門[6.2]-內建開發環境-emacs詳解
  • golang快速入門[7.1]-項目與依賴管理-gopath
  • golang快速入門[7.2]-北冥神功—go module絕技
  • golang快速入門[8.1]-變量類型、聲明指派、作用域聲明周期與變量記憶體配置設定
  • golang快速入門[8.2]-自動類型推斷的秘密
  • golang快速入門[8.3]-深入了解浮點數
  • golang快速入門[8.4]-常量與隐式類型轉換
  • golang快速入門[9.1]-深入字元串的存儲、編譯與運作

前言

  • 在本節我們将介紹go語言中重要的資料類型——數組
  • 數組是一個重要的資料類型,通常會與go語言另一個重要的結構:切片作對比。
  • go語言中數組與其他語言有在顯著的不同,包括其不能夠進行添加,以及值拷貝的特性。在這一小節中,将會詳細介紹。

數組的聲明與定義

//聲明三種方式
var arr [3]int
var arr2  = [4]int{1,2,3,4}
arr4 :=[...]int{2,3,4}      

簡單擷取數組類型

fmt.Printf("類型arr3: %T,類型arr4: %T\n",arr3,arr4)      

擷取數組長度與通過下标擷取

len(arr3)
arr3[2]      

編譯時

  • 數組在編譯時的資料類型為

    TARRAY

    ,通過

    NewArray

    函數進行建立,AST節點的Op操作:

    OARRAYLIT

// NewArray returns a new fixed-length array Type.
func NewArray(elem *Type, bound int64) *Type {
    if bound < 0 {
        Fatalf("NewArray: invalid bound %v", bound)
    }
    t := New(TARRAY)
    t.Extra = &Array{Elem: elem, Bound: bound}
    t.SetNotInHeap(elem.NotInHeap())
    return t
}      
  • 内部的Array結構存儲了數組中的類型以及數組的大小
// Array contains Type fields specific to array types.
type Array struct {
    Elem  *Type // element type
    Bound int64 // number of elements; <0 if unknown yet
}      
  • 數組的聲明中,存在一個文法糖。

    […]int{2,3,4}

    。 其實質與一般的數組聲明類似的。
  • 對于字面量的初始化方式,在編譯時,通過

    typecheckcomplit

     函數循環字面量分别進行指派。
func typecheckcomplit(n *Node) (res *Node) {
    nl := n.List.Slice()
        for i2, l := range nl {
            i++
            if i > length {
                length = i
                if checkBounds && length > t.NumElem() {
                    setlineno(l)
                    yyerror("array index %d out of bounds [0:%d]", length-1, t.NumElem())
                    checkBounds = false
                }
            }
        }

        if t.IsDDDArray() {
            t.SetNumElem(length)
        }
    }
}      
  • 抽象的表達就是:
a:=[3]int{2,3,4}
變為
var arr [3]int
a[0] = 2
a[1] = 3
a[2] = 4      
  • 如果

    t.IsDDDArray

    判斷到是文法糖的形式進行的數組初始化,那麼會将其長度設定到數組中

    t.SetNumElem(length)

    .
  • 在編譯期的優化階段,還會進行重要的優化。在函數

    anylit

    中,當數組的長度小于4時,在運作時會在棧中進行初始化

    initKindDynamic

    。當數組的長度大于4,會在靜态區初始化數組

    initKindStatic

func anylit(n *Node, var_ *Node, init *Nodes) {
    t := n.Type
    switch n.Op {
    case OSTRUCTLIT, OARRAYLIT:
        if !t.IsStruct() && !t.IsArray() {
            Fatalf("anylit: not struct/array")
        }

        if var_.isSimpleName() && n.List.Len() > 4 {
            ...
            fixedlit(ctxt, initKindStatic, n, vstat, init)

            // copy static to var
            a := nod(OAS, var_, vstat)

            a = typecheck(a, ctxStmt)
            a = walkexpr(a, init)
            init.Append(a)

            // add expressions to automatic
            fixedlit(inInitFunction, initKindDynamic, n, var_, init)
            break
        }
}      
  • 他們都是通過

    fixedlit

    函數實作的。
func fixedlit(ctxt initContext, kind initKind, n *Node, var_ *Node, init *Nodes) {
    for _, r := range n.List.Slice() {
    // build list of assignments: var[index] = expr
    setlineno(a)
    a = nod(OAS, a, value)
    a = typecheck(a, ctxStmt)
    switch n.Op {
        ...
        switch kind {
        case initKindStatic:
            genAsStatic(a)
        case initKindDynamic, initKindLocalCode:
            a = orderStmtInPlace(a, map[string][]*Node{})
            a = walkstmt(a)
            init.Append(a)
        default:
            Fatalf("fixedlit: bad kind %d", kind)
        }

    }
}      

數組索引

 var a [3]int
 b := a[1]      
  • 數組通路越界是非常嚴重的錯誤,Go 語言中對越界的判斷是可以在編譯期間由靜态類型檢查完成的,

    typecheck1

     函數會對通路數組的索引進行驗證:
func typecheck1(n *Node, top int) (res *Node) {
    switch n.Op {
    case OINDEX:
        ok |= ctxExpr
        l := n.Left  // array
        r := n.Right // index
        switch n.Left.Type.Etype {
        case TSTRING, TARRAY, TSLICE:
            ...
            if n.Right.Type != nil && !n.Right.Type.IsInteger() {
                yyerror("non-integer array index %v", n.Right)
                break
            }
            if !n.Bounded() && Isconst(n.Right, CTINT) {
                x := n.Right.Int64()
                if x < 0 {
                    yyerror("invalid array index %v (index must be non-negative)", n.Right)
                } else if n.Left.Type.IsArray() && x >= n.Left.Type.NumElem() {
                    yyerror("invalid array index %v (out of bounds for %d-element array)", n.Right, n.Left.Type.NumElem())
                }
            }
        }
    ...
    }
}      
  • 通路數組的索引是非整數時會直接報錯 —— non-integer array index %v;
  • 通路數組的索引是負數時會直接報錯 —— "invalid array index %v (index must be non-negative)";
  • 通路數組的索引越界時會直接報錯 —— "invalid array index %v (out of bounds for %d-element array)";
  • 數組和字元串的一些簡單越界錯誤都會在編譯期間發現,比如我們直接使用整數或者常量通路數組,但是如果使用變量去通路數組或者字元串時,編譯器就無法發現對應的錯誤了,這時就需要在運作時去判斷錯誤。
i:= 3
m:= a[i]      
  • Go 語言運作時在發現數組、切片和字元串的越界操作會由運作時的 panicIndex 和 runtime.goPanicIndex 函數觸發程式的運作時錯誤并導緻崩潰退出:
TEXT runtime·panicIndex(SB),NOSPLIT,$0-8
    MOVL    AX, x+0(FP)
    MOVL    CX, y+4(FP)
    JMP runtime·goPanicIndex(SB)

func goPanicIndex(x int, y int) {
    panicCheck1(getcallerpc(), "index out of range")
    panic(boundsError{x: int64(x), signed: true, y: y, code: boundsIndex})
}      
  • 最後要提到的是,即便數組的索引是變量。在某些時候仍然能夠在編譯時通過優化檢測出越界并在運作時報錯。
  • 例如對于一個簡單的代碼
a := [3]int{1,2,3}
b := 8
_ = a[b]      
  • 我們可以通過如下指令生成ssa.html。顯示整個編譯時的執行過程。
GOSSAFUNC=main GOOS=linux GOARCH=amd64 go tool compile close.go      
  • start階段為最初生成ssa的階段,
start
b1:-
v1 (?) = InitMem <mem>
v2 (?) = SP <uintptr>
v3 (?) = SB <uintptr>
v4 (15) = VarDef <mem> {arr} v1
v5 (15) = LocalAddr <*[3]int> {arr} v2 v4
v6 (15) = Zero <mem> {[3]int} [24] v5 v4
v7 (?) = Const64 <int> [1]
v8 (15) = LocalAddr <*[3]int> {arr} v2 v6
v9 (?) = Const64 <int> [0]
v10 (?) = Const64 <int> [3]
v11 (15) = PtrIndex <*int> v8 v9
v12 (15) = Store <mem> {int} v11 v7 v6
v13 (?) = Const64 <int> [2]
v14 (15) = LocalAddr <*[3]int> {arr} v2 v12
v15 (15) = PtrIndex <*int> v14 v7
v16 (15) = Store <mem> {int} v15 v13 v12
v17 (15) = LocalAddr <*[3]int> {arr} v2 v16
v18 (15) = PtrIndex <*int> v17 v13
v19 (15) = Store <mem> {int} v18 v10 v16
v20 (?) = Const64 <int> [4] (i[int])
v21 (17) = LocalAddr <*[3]int> {arr} v2 v19
v22 (17) = IsInBounds <bool> v20 v10
If v22 → b2 b3 (likely) (17)
b2: ← b1-
v25 (17) = PtrIndex <*int> v21 v20
v26 (17) = Copy <mem> v19
v27 (17) = Load <int> v25 v26 (elem[int])
Ret v26 (19)
b3: ← b1-
v23 (17) = Copy <mem> v19
v24 (17) = PanicBounds <mem> [0] v20 v10 v23
Exit v24 (17)      
  • 通過函數IsInBounds判斷數組長度與索引大小進行對比。

    v22 (17) = IsInBounds v20 v10

    ,如果失敗即執行

    v24 (17) = PanicBounds [0] v20 v10 v23

  • genssa

    生成彙編代碼的階段,我們能夠看到直接被優化為了

    00008 (17) CALL runtime.panicIndex(SB)

     即在運作時直接會觸發Panic
genssa
# main.go
00000 (14) TEXT "".main(SB), ABIInternal
00001 (14) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
00002 (14) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
00003 (14) FUNCDATA $2, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
v3
00004 (+17) PCDATA $0, $0
v3
00005 (+17) PCDATA $1, $0
v3
00006 (+17) MOVL $4, AX
v19
00007 (17) MOVL $3, CX
v24
00008 (17) CALL runtime.panicIndex(SB)
00009 (17) XCHGL AX, AX
00010 (?) END      

數組的值拷貝問題

  • 無論是指派的

    b

    還是函數調用中的形參

    c

    ,都是值拷貝的
a:= [3]int{1,2,3}
b = a

func Change(c [3]int){
    ...
}      

我們可以通過簡單的列印位址來驗證:

package main

import "fmt"

func main() {
    a := [5]int{1,2,3,4,5}
    fmt.Printf("a:%p\n",&a)
    b:=a
    CopyArray(a)
    fmt.Printf("b:%p\n",&b)
}
//
func CopyArray( c [5]int){
    fmt.Printf("c:%p\n",&c)
}      

輸出為:

a:0xc00001a150
c:0xc00001a1b0
b:0xc00001a180      
  • 說明每一個數組在記憶體的位置都是不相同的,驗證其是值拷貝

總結

  • 數組是go語言中的特殊類型,其與其他語言不太一樣。他不可以添加,但是可以擷取值,擷取長度。
  • 同時,數組的拷貝都是值拷貝,是以不要盡量不要進行大數組的拷貝。
  • 常量的下标以及某一些變量的下标的通路越界問題可以在編譯時檢測到,但是變量的下标的數組越界問題隻會在運作時報錯。
  • […]int{2,3,4}

    ,但是本質本沒有什麼差别
  • 在編譯期的優化階段,還會進行重要的優化。當數組的長度小于4時,在運作時會在棧中進行初始化。當數組的長度大于4,會在靜态區初始化數組
  • 其實我們在go語言中對于數組用得較少,而是更多的使用切片。這是下一節的内容。see you~

參考資料

  • 項目連結
  • 作者知乎