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

前文

前言

  • 在本節我們將介紹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~

參考資料

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