4.5.3-1.2 类型检查准备工作

类型检查的准备工作涉及到两个方法:check.initFiles()check.collectObjects().

编译器一次编译的文件只能属于同一个包,而方法checkFiles 的输入是一个 AST 列表,check.initFiles()会检查所有 AST 是否属于同一个包,并初始化字段 check.files 属性。

在分析方法check.collectObjects()之前,我们再来回顾一下类型检查的总体概念以及与之相关的数据结构。程序中涉及到类型的地方有如下几个方面:

  1. 类型的申明,即创建新的类型

  2. 类型的使用,例如申明变量时,或者申明函数的参数与返回值时需要指明类型信息

  3. 类型的推导,即推导出表达式返回值的类型,例如表达式1 + 1.0的类型是浮点数

  4. 类型的兼容性检查,检查在代码中实际类型与期望类型是否一致,例如判断一个类型的值能够赋给另一个类型,或者两个类型是否等价、是否可以相互转换

程序的类型信息通过类型数据结构来表示,类型检查的目的就是为所有需要类型信息的地方创建对应的类型结构,并判断相互之间是否兼容。Go 语言要求每个标识符都必须先申明再使用,我们可以将标识符的申明看着在程序中创建了一个新的对象,并给该对象取了一个名字,即一个命名对象(Named Entity),一个命名对象用一个Object对象来表示。类型检查说到底需要对源代码进行分析,即语法分析的输出:AST ,但仔细观察Object 接口会发现该对象并没有封装 AST 中的节点,类型检查器将 Object 对象对应的 AST 节点信息封装在declInfo中,而 Object 对象与 declInfo 的对应关系保存在checker.objMap中。

现在回到check.collectObjects()上来,在Object的作用中我们提到类型检查是围绕着 Object 对象展开的,类型检查器需要为每个 (Object, declInfo) 组合构造出对应的类型数据结构。而方法check.collectObjects()的任务就是通过 AST 创建出 Object 与 declInfo;除此之外,该方法还会检查包内的命名冲突,以及将所有的方法与其 Receiver 进行绑定。该方法定义在文件GCROOT/compile/internal/types2/resolver.go中,其代码的总体框架如下:

func (check *Checker) collectObjects() {
	type methodInfo struct {
		obj  *Func        // method
		ptr  bool         // true if pointer receiver
		recv *syntax.Name // receiver type name
	}
	var methods []methodInfo // collected methods with valid receivers and non-blank _ names
	var fileScopes []*Scope

	// 1. 处理 AST 中的顶级申明,创建 Object 与 declInfo 对象
	for fileNo, file := range check.files {
		// 创建文件作用域
		fileScope := NewScope(check.pkg.scope, startPos(file), endPos(file), check.filename(fileNo))
		fileScopes = append(fileScopes, fileScope)
		check.recordScope(file, fileScope)

		for index, decl := range file.DeclList {
			switch s := decl.(type) {
			case *syntax.ImportDecl:
				// 通过包加载器将引用的包加载进来
				imp := check.importPackage(s.Path.Pos(), path, fileDir)

				pkgName := NewPkgName(s.Pos(), pkg, name, imp)
				if s.LocalPkgName != nil {
					// 如果是 import db "runtime/debug", 则将 LocalPkgName, 即这里的包别名 db, 注册到 info.Defs 中
					check.recordDef(s.LocalPkgName, pkgName)
				} else {
					check.recordImplicit(s, pkgName)
				}
				check.imports = append(check.imports, pkgName)
				if name == "." {
					// 如果是  import . "xxxxx", 则将包 xxxxx 中的 public 符号导入当前文件作用域
					if check.dotImportMap == nil {
						check.dotImportMap = make(map[dotImportKey]*PkgName)
					}
					for _, obj := range imp.scope.elems {
						if obj.Exported() {
							fileScope.Insert(obj)
							check.dotImportMap[dotImportKey{fileScope, obj}] = pkgName
						}
					}
				} else {
					// 将包对象插入当前的文件作用域
					check.declare(fileScope, nil, pkgName, nopos)
				}
			case *syntax.ConstDecl:
				// 得到常量的初始化表达式列表
				values := unpackExpr(last.Values)
				for i, name := range s.NameList {
					// 创建常量的 Object 对象
					obj := NewConst(name.Pos(), pkg, name.Value, nil, iota) // iota 表示当前 ConstDecl 在当前常量组内的索引。常量组表示用括号括起来的一组常量申明
					var init syntax.Expr
					if i < len(values) {
						init = values[i]
					}
					// 创建常量对应的 declInfo 对象
					d := &declInfo{file: fileScope, vtyp: last.Type, init: init, inherited: inherited}
					// 将 obj 放入包作用域,并保存 obj -> d 的映射关系到 check.objMap 中
					check.declarePkgObj(name, obj, d)
				}

				// arity 方法来用判断常量名称(s.NameList)与初始化表达式(values)的个数是否相等
				check.arity(s.Pos(), s.NameList, values, true, inherited)

			case *syntax.VarDecl:
				// lhs - left hand side, 用来对应变量申明中左边的变量名列表,这里会为每个变量创建一个 Object 对象。
				// 与变量申明不同的是,变量的初始化表达式的个数有两种情况:
				// 1. 数目与 lhs 变量名个数一样。此时每个变量的 Object 对象都对应自己的初始化表达式的 declInfo 对象
				// 2. 一个表达式,返回值的个数与 lhs 变量名个数一样。此时所有的 Object 对象都复用同一个 declInfo 对象
				lhs := make([]*Var, len(s.NameList))

				// 所以如果初始化表达式 s.Values 不是一个列表的话,则创建一个所有变量 Object 复用的 declInfo 对象
				var d1 *declInfo
				if _, ok := s.Values.(*syntax.ListExpr); !ok {
					d1 = &declInfo{file: fileScope, lhs: lhs, vtyp: s.Type, init: s.Values}
				}

				values := unpackExpr(s.Values) // 尝试着将初始化表达式转换为列表
				for i, name := range s.NameList {
					// 为每个变量创建 Object 对象
					obj := NewVar(name.Pos(), pkg, name.Value, nil)
					lhs[i] = obj

					d := d1
					if d == nil {
						// 如果没有复用的 declInfo 对象,则从初始化列表中取出对应的表达式,并创建 declInfo 对象
						var init syntax.Expr
						if i < len(values) {
							init = values[i]
						}
						d = &declInfo{file: fileScope, vtyp: s.Type, init: init}
					}

					// 将 obj 放入包作用域,并保存 obj -> d 的映射关系到 check.objMap 中
					check.declarePkgObj(name, obj, d)
				}

				// 如果变量类型为空的话,则必定包含初始化表达式,此时需要校验变量名与初始化表达式的个数是否合理
				if s.Type == nil || values != nil {
					check.arity(s.Pos(), s.NameList, values, false, false)
				}

			case *syntax.TypeDecl:
				// 对于类型申明,直接创建 TypeName 对象与 declInfo 对象
				obj := NewTypeName(s.Name.Pos(), pkg, s.Name.Value, nil)
				check.declarePkgObj(s.Name, obj, &declInfo{file: fileScope, tdecl: s})

			case *syntax.FuncDecl:
				d := s
				name := d.Name.Value
				obj := NewFunc(d.Name.Pos(), pkg, name, nil) // 创建函数的 Object 对象,函数与方法的申明都对应的是 FuncDecl, 接下来对二者分开处理
				if d.Recv == nil {
					if name == "init" {
						obj.parent = pkg.scope
						check.recordDef(d.Name, obj) // 如果是 init 方法,则只存入 info.Defs 中而不插入当前的包作用域内,因为 init 方法不能被其他方法调用
					} else {
						check.declare(pkg.scope, d.Name, obj, nopos) // 如果是普通方法,则需要插入当前的包作用域内,用于符号解析
					}
				} else {
					ptr, recv, _ := check.unpackRecv(d.Recv.Type, false)
					if recv != nil && name != "_" {
						methods = append(methods, methodInfo{obj, ptr, recv}) // 在本方法的最后需要将所有的方法与其 Receiver 绑定,所以此处记录一下方法对象
					}
					check.recordDef(d.Name, obj) // 方法的解析通过 Receiver 来完成,所以只将该对象放入 info.Defs 即可,不能插入包作用域内
				}
				info := &declInfo{file: fileScope, fdecl: d} // 创建函数对应的 declInfo
				check.objMap[obj] = info                     // 保存 obj -> declInfo 的映射关系
				obj.setOrder(uint32(len(check.objMap)))      // 用于对方法进行排序
			default:
				check.errorf(s, invalidAST+"unknown syntax.Decl node %T", s)
			}
		}
	}

	// 2. 检查包内的命名冲突
	for _, scope := range fileScopes {
		for _, obj := range scope.elems {
			if alt := pkg.scope.Lookup(obj.Name()); alt != nil {
				var err error_
				if pkg, ok := obj.(*PkgName); ok {
					err.errorf(alt, "%s already declared through import of %s", alt.Name(), pkg.Imported())
					err.recordAltDecl(pkg)
				} else {
					err.errorf(alt, "%s already declared through dot-import of %s", alt.Name(), obj.Pkg())
					// TODO(gri) dot-imported objects don't have a position; recordAltDecl won't print anything
					err.recordAltDecl(obj)
				}
				check.report(&err)
			}
		}
	}

	// 3. 将所有的方法与其 receiver 绑定起来
	if methods != nil {
		check.methods = make(map[*TypeName][]*Func)
		for i := range methods {
			m := &methods[i]
			ptr, base := check.resolveBaseTypeName(m.ptr, m.recv) // 解析 receiver 的基础类型
			if base != nil {
				m.obj.hasPtrRecv = ptr
				check.methods[base] = append(check.methods[base], m.obj)
			}
		}
	}
}

这里的代码只是为了展现核心的处理思路,为了节约篇幅,很多细节以及错误检查的部分都删除了。在三个步骤中,最重要的是第一部分:Object 对象与 declInfo 的创建与注册。其中涉及到的几个注册方法的概要逻辑如下:

// id 不能为 nil, 将对象放入 info.Defs 中
func (check *Checker) recordDef(id *syntax.Name, obj Object) {
	if m := check.Defs; m != nil {
		m[id] = obj
	}
}

// 将 obj 插入 scope 中,如果 id 不为 nil, 则同时放入 info.Defs 中
func (check *Checker) declare(scope *Scope, id *syntax.Name, obj Object, pos syntax.Pos) {
	if obj.Name() != "_" {
		if alt := scope.Insert(obj); alt != nil {
			// 忽略错误处理代码
			return
		}
		obj.setScopePos(pos)
	}
	if id != nil {
		check.recordDef(id, obj)
	}
}

// 将 obj 注册到包作用域内,并建立 obj -> d 的映射关系
func (check *Checker) declarePkgObj(ident *syntax.Name, obj Object, d *declInfo) {
	// 忽略对象名称校验逻辑:对象名称不能为 init, main
	check.declare(check.pkg.scope, ident, obj, nopos)
	check.objMap[obj] = d
	obj.setOrder(uint32(len(check.objMap)))
}

总而言之,该方法会向下列字段添加内容:

  • info.Defs

  • info.Implicits

  • checker.imports

  • checker.objMap

  • checker.methods

细心的读者可能已经发现:这里只是创建了顶级申明的类型检查对象,但是对于源代码中的每个表达式,或者涉及到类型使用的语句(例如赋值语句 a = foo()),类型检查都是需要的,那么这些地方的类型检查是如何完成的呢?答案是类型检查是通过递归下降的方式进行的,当类型检查器对顶级申明的对象进行类型检查时,其会递归地对当前 AST 节点的所有子节点进行类型检查,这里的逻辑相当于只是类型检查的初始推动力,完成了这里的工作之后,真正的类型检查就开始了。

最后更新于