Go 语言的编译器入口在 src/cmd/compile/internal/gc/main.go 的 Main 函数。
编译器类型检查主要逻辑在 src/cmd/compile/internal/gc/typecheck.go 的 typecheck 函数中。
我们都知道,Go 语言数组在初始化之后大小就无法改变了,元素类型相同但大小不同的数组类型在 Go 看来是不同类型。
在 Go 里面,数组的初始化有两种方式,这两种声明方式在运行期间得到的结果相同,后面一种在编译期间会转换成前一种。
a := [3]{1,2,3}a := [...]{1,2,3}
复制代码
第一种的编译期处理方式如下:
// src/cmd/compile/internal/types/type.go.NewArray// 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}
复制代码
第二种的处理会稍微复杂一些,它对应的类型是OCOMPLIT,而第一种的类型是OTARRAY。处理方式如下:
//src/cmd/compile/internal/gc/typecheck.go.typecheckcomplitfunc typecheckcomplit(n *Node) (res *Node) { ... // Need to handle [...]T arrays specially. if n.Right.Op == OTARRAY && n.Right.Left != nil && n.Right.Left.Op == ODDD { n.Right.Right = typecheck(n.Right.Right, ctxType) if n.Right.Right.Type == nil { n.Type = nil return n } elemType := n.Right.Right.Type //通过调用typecheckarraylit来获取数组中元素的数量,就可以转化成第一种声明方式了 length := typecheckarraylit(elemType, -1, n.List.Slice(), "array literal")
n.Op = OARRAYLIT n.Type = types.NewArray(elemType, length) n.Right = nil return n }
...
switch t.Etype { case TARRAY: typecheckarraylit(t.Elem(), t.NumElem(), n.List.Slice(), "array literal") n.Op = OARRAYLIT n.Right = nil ...}
复制代码
除了声明方式、类型检查,数组越界也是数组一个很重要的特性,有一些错误我们是可以在编译期发现
//src/cmd/compile/internal/gc/typecheck.go.typecheck1case OINDEX: ok |= ctxExpr n.Left = typecheck(n.Left, ctxExpr) n.Left = defaultlit(n.Left, nil) n.Left = implicitstar(n.Left) l := n.Left n.Right = typecheck(n.Right, ctxExpr) r := n.Right t := l.Type if t == nil || r.Type == nil { n.Type = nil return n } switch t.Etype { default: yyerror("invalid operation: %v (type %v does not support indexing)", n, t) n.Type = nil return n
case TSTRING, TARRAY, TSLICE: n.Right = indexlit(n.Right) if t.IsString() { n.Type = types.Bytetype } else { n.Type = t.Elem() } why := "string" if t.IsArray() { why = "array" } else if t.IsSlice() { why = "slice" }
if n.Right.Type != nil && !n.Right.Type.IsInteger() { yyerror("non-integer %s index %v", why, n.Right) break }
if !n.Bounded() && Isconst(n.Right, CTINT) { x := n.Right.Int64Val() if x < 0 { yyerror("invalid %s index %v (index must be non-negative)", why, n.Right) } else if t.IsArray() && x >= t.NumElem() { yyerror("invalid array index %v (out of bounds for %d-element array)", n.Right, t.NumElem()) } else if Isconst(n.Left, CTSTR) && x >= int64(len(n.Left.StringVal())) { yyerror("invalid string index %v (out of bounds for %d-byte string)", n.Right, len(n.Left.StringVal())) } else if n.Right.Val().U.(*Mpint).Cmp(maxintval[TINT]) > 0 { yyerror("invalid %s index %v (index too large)", why, n.Right) } }
复制代码
可以看到核心逻辑主要为非整数检查、非负数检查、数组越界检查。
如果在编辑期间无法判断是否可能会越界(比如下标是由参数传入的),则会在生成的 SSA 指令(生成的中间代码里)带上判断是否越界的操作(也就是说会把检查函数插入到运行期间执行),并且从 SSA 代码里可以看出,无论是数组寻址还是赋值,都是在编译期执行的,没有运行时的参与。
SSA 生成指令(GOSSA=func_name go build main.go)
评论