相关文章推荐

6. Go编译过程-类型检查

在前边的一篇文章中分享了抽象语法树的构建,下边的一个阶段就是类型检查,它会遍历每一个抽象语法树的结点,会按照如下步骤对不同类型的结点进行类型检查(静态类型检查):

  • 常量、类型和函数名及类型验证
  • 变量的赋值和初始化
  • 计算编译时的常量、将声明与标识符绑定
  • 会对一些内置函数进行改写(下边介绍源码时会提到)
  • 哈希键值对的类型
  • 做特别的语法或语义检查(引用的结构体字段是否是大写可导出的?数组字面量的访问是否超过了其长度?数组的索引是不是正整数?)
  • 通过类型检查,它可以保证每一个抽象语法树的结点不会出现类型错误(注意,编译阶段是静态类型检查),源代码中的静态类型错误,会在类型检查的过程中被发现。并且,如果某个类型是否实现了某个接口,也会在该阶段被检查出来

    通过本文你可以了解到Go的类型检查阶段都做了哪些事情,以及在检查一些特殊类型的结点时,对结点做了哪些特殊的改写(比如:map、make)

    类型检查整体概览

    类型检查阶段会遍历抽象语法树的每一个节点,确定节点的类型。例如下边这两种形式

    var test int
    test := 1
    

    第一种是直接指定了类型,第二种是需要编译器通过类型推断来得到变量的类型

    在前边的几篇文章中提到了,Go编译的入口文件在:

    Go的编译入口文件:src/cmd/compile/main.go -> gc.Main(archInit)
    

    进入到gc.Main(archInit)方法,你会看到,执行完词法分析、语法分析、抽象语法树的构建之后,有下边这么一段代码:

    func Main(archInit func(*Arch)) {
    	......
    	lines := parseFiles(flag.Args())//词法分析、语法分析、抽象语法树构建都在这里
    	......
    	//开始遍历抽象语法树,对每个结点进行类型检查
    	for i := 0; i < len(xtop); i++ {
    		n := xtop[i]
    		if op := n.Op; op != ODCL && op != OAS && op != OAS2 && (op != ODCLTYPE || !n.Left.Name.Param.Alias()) {
    			xtop[i] = typecheck(n, ctxStmt)
    	for i := 0; i < len(xtop); i++ {
    		n := xtop[i]
    		if op := n.Op; op == ODCL || op == OAS || op == OAS2 || op == ODCLTYPE && n.Left.Name.Param.Alias() {
    			xtop[i] = typecheck(n, ctxStmt)
    	......
    	checkMapKeys()//对哈希中键的类型进行检查
    	......
    

    这里的xtop是一个数组,它里边是每一棵抽象语法树的根节点(在抽象语法树构建这篇文章中提到,它会将每一种声明语句都构建成一棵抽象语法树,比如var、const、type、func等)。因此它会从每一棵树的根节点开始遍历,逐一进行类型检查

    从上边的代码中我们可以看到,类型检查主要是调用了:/usr/local/go/src/cmd/compile/internal/gc/typecheck.go→typecheck方法,该方法会对常量、类型、函数声明、赋值语句等进行类型检查。同时后边调用了checkMapKeys()方法对哈希的键进行类型检查(下边会对这两个方法的实现进行详细的介绍)

    其实在typecheck方法中,核心逻辑在它调用的typecheck1方法中。该方法中由一个很大的switch构成,根据每个节点的Op,来选择不同的处理逻辑。这里边分支非常多,我这里仅选择几个比较特别的进行深入的了解

    func typecheck1(n *Node, top int) (res *Node) {
    		......
    		switch n.Op {
    		// until typecheck is complete, do nothing.
    		default:
    			Dump("typecheck", n)
    			Fatalf("typecheck %v", n.Op)
    		// names
    		case OLITERAL:
    			ok |= ctxExpr
    			if n.Type == nil && n.Val().Ctype() == CTSTR {
    				n.Type = types.UntypedString
    		case ONONAME:
    			ok |= ctxExpr
    		case ONAME:
    			......
    		case OTARRAY:
    		......
    		case OTMAP:
    		......
    		......
    

    深入了解类型检查

    OAS:赋值语句

    // Left = Right or (if Colas=true) Left := Right
    // If Colas, then Ninit includes a DCL node for Left.
    

    赋值语句的核心是调用了:/usr/local/go/src/cmd/compile/internal/gc/typecheck.go→typecheckas

    case OAS:
    		ok |= ctxStmt
    		typecheckas(n)
    		// Code that creates temps does not bother to set defn, so do it here.
    		if n.Left.Op == ONAME && n.Left.IsAutoTmp() {
    			n.Left.Name.Defn = n
    

    typecheckas方法中,可以看到如下这段代码,它是将右边常量的类型,赋值给左边变量的类型

    func typecheckas(n *Node) {
    		......
    		if n.Left.Name != nil && n.Left.Name.Defn == n && n.Left.Name.Param.Ntype == nil {
    				n.Right = defaultlit(n.Right, nil)
    				n.Left.Type = n.Right.Type
    		......
    

    比如:var a = 666

    OTARRAY:切片

    OTARRAY // []int, [8]int, [N]int or [...]int
    

    对于节点类型是OTARRAY的情况,它会先检查切片值的类型(该节点的右节点)

    case OTARRAY:
    		ok |= ctxType
    		r := typecheck(n.Right, ctxType)
    		if r.Type == nil {
    			n.Type = nil
    			return n
    

    然后根据左边节点的不同,分三种情况,即[]int、[...]int、[6]int

  • []int:直接调用t = types.NewSlice(r.Type),返回了一个 TSLICE 类型的结构体,元素的类型信息也会存储在结构体中
  • [...]int:交由typecheckcomplit方法处理,
  • func 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
    				length := typecheckarraylit(elemType, -1, n.List.Slice(), "array literal")
    				n.Op = OARRAYLIT
    				n.Type = types.NewArray(elemType, length)
    				n.Right = nil
    				return n
    		......
    

    该方法会获取到数组中元素的数量,然后调用[types.NewArray](https://draveness.me/golang/tree/cmd/compile/internal/types.NewArray) 初始化一个存储着数组中元素类型和数组大小的结构体

  • [6]int:如果在声明切片时,带了数组的大小,则直接调用[types.NewArray](https://draveness.me/golang/tree/cmd/compile/internal/types.NewArray) 初始化一个存储着数组中元素类型和数组大小的结构体
  • 可以发现数组的长度是类型检查期间确定的

    在最后,它会更新该节点的Type等信息

    setTypeNode(n, t)
    n.Left = nil
    n.Right = nil
    checkwidth(t)
    

    OTMAP:map(哈希)

    OTMAP    // map[string]int
    

    对于OTMAP类型的结点,它会分别对左右两边的部分进行类型检查,然后创建一个*TMAP结构体,将MAP的键值类型存到该结构体中*

    case OTMAP:
    		ok |= ctxType
    		n.Left = typecheck(n.Left, ctxType)
    		n.Right = typecheck(n.Right, ctxType)
    		l := n.Left
    		r := n.Right
    		if l.Type == nil || r.Type == nil {
    			n.Type = nil
    			return n
    		if l.Type.NotInHeap() {
    			yyerror("incomplete (or unallocatable) map key not allowed")
    		if r.Type.NotInHeap() {
    			yyerror("incomplete (or unallocatable) map value not allowed")
    		setTypeNode(n, types.NewMap(l.Type, r.Type))
    		mapqueue = append(mapqueue, n) // check map keys when all types are settled
    		n.Left = nil
    		n.Right = nil
    ......
    func NewMap(k, v *Type) *Type {
    	t := New(TMAP)
    	mt := t.MapType()
    	mt.Key = k
    	mt.Elem = v
    	return t
    

    我们从代码中可以发现,它不仅对结点进行了修改,而且将结点放入了一个mapqueue队列。在前边的概览部分提到了checkMapKeys()会对哈希键的类型进行再次的检查

    func checkMapKeys() {
    	for _, n := range mapqueue {
    		k := n.Type.MapType().Key
    		if !k.Broke() && !IsComparable(k) {
    			yyerrorl(n.Pos, "invalid map key type %v", k)
    	mapqueue = nil
    

    其实就是遍历队列,验证这些类型是否可以作为map的key

    OMAKE:make

    OMAKE          // make(List) (before type checking converts to one of the following)
    OMAKECHAN      // make(Type, Left) (type is chan)
    OMAKEMAP       // make(Type, Left) (type is map)
    OMAKESLICE     // make(Type, Left, Right) (type is slice)
    

    在编写go代码时,我们经常会用到make关键字来创建slice、map、channel等,在Go编译的类型检查阶段,它会细分OMAKE的结点,比如:

    make slice:OMAKESLICE
    make map:OMAKEMAP
    make chan:OMAKECHAN
    

    具体实现就是,它会先获取到make的第一个参数,也就是类型。根据这个类型,进行不同的处理

    case OMAKE:
    		ok |= ctxExpr
    		args := n.List.Slice()
    		......
    		l := args[0]
    		l = typecheck(l, ctxType)
    		t := l.Type
    		......
    		i := 1
    		switch t.Etype {
    		default:
    			yyerror("cannot make type %v", t)
    			n.Type = nil
    			return n
    		case TSLICE:
    			......
    			n.Left = l
    			n.Right = r
    			n.Op = OMAKESLICE
    		case TMAP:
    			......
    			n.Op = OMAKEMAP
    		case TCHAN:
    			......
    			n.Op = OMAKECHAN
    		n.Type = t
    
  • 如果第一个参数是切片类型:获取切片的长度(len)和容量(cap),然后对len和cap进行合法性校验。并且改写了节点的类型
  • 如果第一个参数是map类型:获取make的第二个参数,如果没有,则默认设置为0(map的大小)。并且改写节点的类型
  • 如果第一个参数是chan类型:获取make的第二个参数,如果没有,则默认设置为0(chan的缓冲区大小)。并且改写节点的类型
  • 我这里没粘代码了,大家可以自行去看

    本文主要是分享了类型检查中的几个特殊的节点类型。还有很多其它类型的节点的类型检查,大家可以自行的去看源码

    在前边几篇文章中没有分享Go的源码调试,所以下篇文章计划分享Go的源码调试方式。并且以抽象语法树的构建为例,对它进行调试

  • go-ast-book
  • 《Go语言底层原理剖析》
  • 面向信仰编程-类型检查
  • 腾讯云开发者
     
    推荐文章