前文
- 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~