写点什么

Go 语言原理分析 - 数组

作者:王博
  • 2021 年 12 月 18 日
  • 本文字数:1790 字

    阅读完需:约 6 分钟

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)


发布于: 40 分钟前阅读数: 6
用户头像

王博

关注

我是一名后端,写代码的憨憨 2018.12.29 加入

还未添加个人简介

评论

发布
暂无评论
Go语言原理分析-数组