4.5.3-1.3e 处理delayed队列

这是类型检查的第二阶段,即执行 check.delayed 里面的函数。在进行第一阶段的类型检查时,很多事情无法在对表达式进行递归检查时完成,这些任务会被推入 check.delayed 中。典型的几类放入延迟处理队列的任务如下:

函数体的类型检查

函数体的类型检查必须在全局申明的类型检查之后进行,因为函数体与全局类型申明可能会相互引用,例如如下例子:

type I interface {
  m([unsafe.Sizeof(func() { I.m(nil) })]byte)
}

可以这样申明方法是因为 unsafe 包内的方法在编译阶段执行,而不是运行阶段

可以发现方法 I.m 参数中所涉及到的函数体又引用了 I.m. 如果在对 I.m 进行类型检查的过程中就对函数体(I.m(nil))进行类型检查的话,由于此时I.m还没有完成检查,方法 m 还没有被收录到接口I中,所以在函数体中将无法成功地完成对符号I.m的解析,编译器会报错:I.m undefined (type I has no field or method m).

对函数体进行类型检查的函数是check.funcBody(), 查看其调用者(Caller)就可以看到被推入 delayed 队列中的任务。

循环依赖检查

编译器要做的一件重要事情便是计算一个类型所需要的空间大小,基于这一点,只要类型中出现了循环引用,那么就是不合法的,例如:

type T T // 类型会无穷展开
type A struct {
    self A // 递归引用,类型同样会无穷展开
}

这种递归引用会导致编译器在计算空间时陷入死循环,所以编译器需要识别出这种情况并报错。在完成全局声明的类型检查之后,就需要对每个类型进行循环依赖检查,该逻辑通过$GCROOT/compile/internal/types2/decl.go中的函数func (check *Checker) validType(typ Type, path []Object) typeInfo {}来完成。类型依赖的拓扑结构本质上是一个有向图(Directed Graph),该函数通过深度优先遍历(Deep-First Search)的方式检测是否存在环(Circle)。其主体逻辑如下:

func (check *Checker) validType(typ Type, path []Object) typeInfo {
    const (
        unknown typeInfo = iota
        marked
        valid
        invalid
    )

    switch t := typ.(type) {
    case *Array:
        return check.validType(t.elem, path)

    case *Struct:
        for _, f := range t.fields {
            if check.validType(f.typ, path) == invalid {
                return invalid
            }
        }

    case *Interface:
        for _, etyp := range t.embeddeds {
            if check.validType(etyp, path) == invalid {
                return invalid
            }
        }

    case *Named:
        switch t.info {
        case unknown:
            t.info = marked
            t.info = check.validType(t.orig, append(path, t.obj))
        case marked:
            for i, tn := range path {
                if tn == t.obj {
                    // 检测到循环依赖
                    check.cycleError(path[i:])
                    t.info = invalid
                    return t.info
                }
            }
            panic("internal error: cycle start not found")
        }
        return t.info

    case *instance:
        return check.validType(t.expand(), path)
    }

    return valid
}

其中参数typ是待检查类型,而path是当前已经遍历过的对象路径,只有Named这个 case (Defined Type) 可能产生循环依赖,如果当前对象已被标记并且在path中,则我们就发现了循环依赖。而对于其它复合类型,继续递归进行检查即可。

通过上述代码可以发现对于指针类型,总是不会有循环依赖(指针类型不在任何 case 中,方法总是返回 valid),例如如下类型申明就是合法的:

type T *T
type A struct {
    self *A
}

这是因为指针所需要的内存空间是固定的,所以编译器可以顺利地计算出上面类型所需要的地址空间。

type T *T 这种申明符合语法,但却并没有实际的应用场景。Go 语言允许这种申明存在是为了在设计上及可能保证语言的简洁性与正交性,因此并不会为了避免无用结构而刻意添加特殊的语言规则。

整理接口信息

对于接口类型,在完成类型检查后还需要做一些信息整理工作,例如将所有内嵌接口的方法整合到一起,如果声明了泛型的 constraints 的话,还需要推算出该接口实际的 constraints 类型(在前文介绍topbottom类型时已有介绍),这些逻辑都在方法check.completeInterface()中完成。

其他情况

例如检查 map 类型的 key 的类型是否是 Comparable 的。还有各种其它情况需要放入延迟队列,这里不再一一列出。

最后更新于