快速链接:https://github.com/golang/go/tree/release-branch.go1.15//src/cmd/go/internal
命令说明文档:https://github.com/golang/go/blob/release-branch.go1.15/src/cmd/go/alldocs.go
cmd/go/internal/load
包:根据包的ImportPath确定引用名称
代码:https://github.com/golang/go/blob/release-branch.go1.15/src/cmd/go/internal/load/pkg.go#L1544
如果是顶级路径:log, fmt等,则直接使用其本身作为包名
非顶级路径:如果没有使用go模块,则直接使用路径分割的最后一个,如果;如果使用go模块(> go1.11), 则如果最后一个是v2,v3,… v10 (v+有效数字, 除了v0,v1,v05), 则判定是版本,使用倒数第二个路径元素作为引入名称。
cmd/go/internal/modload
加载模块
loader.resolveMissingImports
buildList
对象
一个全局对象,包含所有需要构建的模块
queryImport
queryImport
函数:通过import包路径找到模块的路径,包括模块名,版本,和文件系统位置
func queryImport(ctx context.Context, path string) (module.Version, error) {
}
Init
如果是go mod init
,则创建一个go.mod文件,
否则,通过findModuleRoot
定位到模块根目录,根据环境变量GO111MODULE
是on
, off
,auto
来加载模块。
当值是auto
时,如果查找不到模块根目录,会保持使用GOPATH模式.
除此之外,还会检查如果存在$GOPATH/go.mod
,则报错!
最后,设置cfg.ModulesEnabled = true
findModuleRoot
查找当前目录所属的go.mod
https://github.com/golang/go/blob/release-branch.go1.15/src/cmd/go/internal/modload/init.go#L583
LoadPackages
加载包路径
LoadPackages(ctx context.Context, opts PackageOpts, patterns ...string)
Lookup
查找包的源码目录
检查模块冲突
一次编译过程会列出所有模块,存放在buildList
对象中。确定所有的模块之后,会检查是否存在同一个模块被当做两个不同的包路径引用的情况(通常是使用了replace)。如果模块a被替换成b,则所有a的包的引用实际上都解析到模块b,但是同时,所有b的包的引用也解析到模块b,从语义上来说,它们在内存的数据应该只有1份。但是目前go的实现并非如此,而且也不可能做到这样,只有在两个模块结构完全相同的情况下,这种替换才是100%安全的。
所以,go禁止同一个模块以两种不同的包名引用。
https://github.com/golang/go/blob/release-branch.go1.15/src/cmd/go/internal/modload/buildlist.go#L123
cmd/go/internal/cfg
构建时的参数,如-a
,-o
等标志参数
cmd/go/internal/work
work包包含go build
的入口:https://github.com/golang/go/blob/release-branch.go1.15/src/cmd/go/internal/work/build.go#L346
go build
支持的标志
-work
保留工作目录,程序结束后不自动删除生成的对象文件夹
-debug-actiongraph=graph.json
将编译动作图dump到graph.json
go build
的过程
work.runBuild()
-> BuildInit()
-> modload.Init()
load.PackagesForBuild(args)
-> load.PackagesAndErros(args)
-> 将命令行的patterns转换成初始的packages
-> 预加载通过Context.Import和Context.ImportDir来获取所有的Import依赖(递归进行,go协程,最大并发数=CPU数,见下面的介绍)
work.CompileAction(mainPackage) // 加载main包,构建依赖树,从依赖为0的包开始编译,直到最终编译完主包(main)
经过load.PackagesForBuild(args)
加载出来的包含了最基本的信息:目录,模块,import列表等,但是此时还不包含源文件的AST等具体信息。
并发加载pkg信息的过程
首先准备一个大小为N的并发控制管道,每次获得一个管道占用的机会,就启动一个协程加载包信息,加载完成后释放管道。然后对引用的所有包,递归执行这个过程。
由于所有子包的加载都在协程中进行,所以假如所有的初始包都能获得管道占用的机会,那么这个过程就不再阻塞,但是此时依赖包、依赖包的依赖包可能都还没有加载完成,因为协程还在递归产生更多协程。
加载的包将会保存在缓存中。
https://github.com/golang/go/blob/release-branch.go1.15/src/cmd/go/internal/imports/tags.go#L913
如何检测import环?
在加载每个包路径的过程中,维护一个栈记录import路径,然后,遍历所有的Imports,将Import的路径通过递归调用LoadImport
。在加载每个Import之前,都会先将自身的路径push到栈中,所有Import加载完之后再pop出来,然后设置Package.Internal.Imports
属性。
何时检测环呢? 当一个Package已经被加载到缓存中,但是此时Package.Internal.Imports
属性仍然为空,这说明当前的包处于一个环中。因为包加载了就会放入到缓存中,但是包的这个属性需要依赖所有import的包加载完设置。如果Package.Internal.Imports
不为空,则可以安全地重用这个缓存对象。
CompileAction
是如何构建依赖并驱动构建过程的?
假定有包的依赖关系:
main
|-->a
| |-->b
|
|-->c
|-->a
Action结构如下:
type Action struct {
Mode string // description of action operation
Package *load.Package // the package this action works on
Deps []*Action // actions that must happen before this one
Func func(*Builder, context.Context, *Action) error // the action itself (nil = no-op)
Args []string // additional args for runProgram
triggers []*Action // inverse of deps
// Generated files, directories.
Objdir string // directory for intermediate objects
Target string // goal of the action: the created package or executable
built string // the actual created package or executable
actionID cache.ActionID // cache ID of action input
buildID string // build ID of action output
// Execution state.
pending int // number of deps yet to complete
priority int // relative execution priority
Failed bool // whether the action failed
//...
}
其中Deps
保存依赖关系,triggers
保存被依赖关系。
通过work.Builder.Do()
来触发Action
执行。
调试:可通过-debug-actiongraph=graph.json
进行编译活动图的调试。
注:如果是unsafe
, builtin
包,则无需构建。
构建过程:首先根据Imports从main节点构造出Action的依赖树,然后深度遍历转换成列表,使用一个同样大小的channel来记录待完成的任务,根据Deps构造triggers逆反关系,依赖数量使用pending记录。
其次,作为Bootstrap驱动,那些pending=0的Action
将会加入到ready队列中,然后,ready队列中一旦有新的就绪数据产生,就会遍历对应的triggers
将它们的pending
数量减1, 如果pending==0
,则对应地也将其加入到ready队列中。重复整个过程,直到main.pending==0
.
核心编译过程
上面讲述了如何从命令行参数转换为包和路径,然后构建活动依赖图并执行的过程。这里将介绍单个包是如何被编译的。
编译的主要代码在cmd/go/internal/work
包的Builder.build
方法中。其核心任务,是将高层次的包的概念,转换到低层次的文件和库的概念,然后传递给较为底层的go tool compile
的调用
每个包的Action都会使用所有的编译参数来生成一个唯一的Hash, 包括本次编译涉及的所有源文件内容和标志等。
如果没有命中编译缓存,就会通过调用go tool compile
的方式,以命令行去编译一个包,并预先指定一个编译输出目录objdir
, 生成的文件就是objdir/_pkg_.a
。除了常规的go
文件, 还提供了cc
,asm
,也就是对c
语言和汇编语言的编译,最终结果都是生成归档文件和对象文件(.o
)
然后调用go tool pack
将这些对象文件全部都打包到objdir/_pkg_.a
归档文件中。pack
的实现其实就是在.a
文件尾部追加.o
文件,只不过在追加文件内容之前,先要写一些头部字段。(实际上无法调用go tool pack
这个命令,可以理解为很简单的ar
程序的实现)
然后,将归档好的文件复制到Build-Cache
中即可。
通过-work
参数保留工作目录,打印出归档文件的位置,和最终Build-Cache
的中文件对比,发现没有差异,这说明Build-Cache
的文件就是从最终打包好的objdir/_pkg_.a
文件复制过去的。
编译缓存
每个Action.Hash
都可以唯一映射到go-build
的缓存文件,比如:~/Library/Caches/go-build/4b/4bb6ff70820dec2d60de038c0e2a6f264995f0f233d945672f7f2dd1778646bb-d
每个包都唯一对应Build-Cache
中的唯一的一个归档文件(ar
),main
包可以通过link
生成可执行文件。
不仅会缓存go compile
的结果,而且会缓存它的标准输出,所以你可能在第二次编译时,及时已经命中了缓存,但是命令行依旧输出了编译的信息。
go build
的参数
-work
打印go的工作目录(一个临时创建的目录),并且不删除。
go build somefile.go
参数是文件
go build *.go
实际上到了go的运行时获取到的并不是 *.go
, 而是具体的文件列表,因为bash将pattern扩展了, 如果你运行 go build "*.go"
, 你会收到路径不存在的错误提示.
如果go build
的参数中至少有一个go文件,就会使用第一个出现的文件作为生成文件名, 比如
go build init.go main.go
则 cfg.BuildO = init
,
go build main.go init.go
则 cfg.BuildO = main
.
go build
无参数
等价于go build .
go build some
有参数
some的形式如果是以./
, ../
开头,则是相对import, 否则就是绝对import.
如果some中包含all
这个特殊的包,则它表示把所有的test引入的包也包括在内,默认情况下,在go1.16之前,all
也包含所有除了main模块之外的所有其他模块的测试依赖。all
代表GOPATH
中发现的所有包,
如果包名中含有 ...
,则将其视为通配符, 会递归搜索父目录下的所有子包,跳过下列目录:
- 名称为testdata
- 含有go.mod(也就是说不会递归导入模块)
- 不含有go文件的空目录(因为不是一个go包,go包至少含有一个go文件)
单个包和多个包构建的区别
当go build some
只有1个包时(go build
也只有1个包)且这个包是main时, go内部会强制生成单个可执行文件,也就是说,这种情况下如果-o
没有指定名称,cfg.BuildO
会生成一个值,从而除了编译成.a
归档文件之外,还有一个额外的link
步骤生成可执行文件。
如果是多个包时,此时就不会再生成可执行文件了。这种情况下,不会使用Install。每个包都被,如果是main
包,还会被link
到Work
目录下,但不会进行安装。所以,多包构建的过程中还是会链接生成可执行文件, 但可执行文件只会被生成到WORK
目录下。如果没有指定-work
标志,则随着程序结束,这些生成的可执行文件也会被一并删除。
link
是依赖build
的,build
生成归档文件,会缓存到Build-Cache
中,但是link
不会缓存。link
和build
生成的ActionID不相同。
对于build action
,还有一点需要注意,如果存在一个link action
依赖了这个build action
,如果目标文件已经存在且没有发生变化,则即使build action
的Build-Cache
被清除了,build action
也不会被执行。所以测试的时候需要注意这一点,如果希望触发编译,需要同时删除缓存的库和链接生成的文件。
go build -o dir/
指定目录
如果-o
参数指定的名称是一个目录,或者以/
结尾,则go会将所有发现的main
包都编译,生成到这个目录下。
cmd/go/internal/imports
构建标签(buildtags)
loadTags(), 首先会把 GOOS,GOARCH, Compiler(值:“gc”, “gccgo”) 作为tag传入,根据环境变量注入"cgo"标签,然后是命令行指定的标签
https://github.com/golang/go/blob/release-branch.go1.15/src/cmd/go/internal/imports/tags.go#L21
cmd/internal/buildid
用于读取已编译文件的BuildID
,所有的BuildID都会保存到生成文件的某个区域(归档文件头部,可执行文件某个Section)。
从当前的实现方式来说,可以把BuildID
看成ActionID + "/" + ActionID
.因此,要判断一个可执行文件是否需要编译,只需要看当前Action.Target
的BuildID
是否以当前Action
的ActionID + "/"
开头即可。
https://github.com/golang/go/blob/release-branch.go1.15/src/cmd/cmd/internal/buildid/buildid.go#L31
go/build
Context
BuildContext 定义了默认的Context
Context结构,定义了构建的上下文,build tags, 并提供了几个可以定制的函数,如果没有赋值使用默认实现。
// A Context specifies the supporting context for a build.
type Context struct {
GOARCH string // target architecture
GOOS string // target operating system
GOROOT string // Go root
GOPATH string // Go path
// Dir is the caller's working directory, or the empty string to use
// the current directory of the running process. In module mode, this is used
// to locate the main module.
//
// If Dir is non-empty, directories passed to Import and ImportDir must
// be absolute.
Dir string
CgoEnabled bool // whether cgo files are included
UseAllFiles bool // use files regardless of +build lines, file names
Compiler string // compiler to assume when computing target paths
// The build and release tags specify build constraints
// that should be considered satisfied when processing +build lines.
// Clients creating a new context may customize BuildTags, which
// defaults to empty, but it is usually an error to customize ReleaseTags,
// which defaults to the list of Go releases the current release is compatible with.
// BuildTags is not set for the Default build Context.
// In addition to the BuildTags and ReleaseTags, build constraints
// consider the values of GOARCH and GOOS as satisfied tags.
// The last element in ReleaseTags is assumed to be the current release.
BuildTags []string
ReleaseTags []string
// The install suffix specifies a suffix to use in the name of the installation
// directory. By default it is empty, but custom builds that need to keep
// their outputs separate can set InstallSuffix to do so. For example, when
// using the race detector, the go command uses InstallSuffix = "race", so
// that on a Linux/386 system, packages are written to a directory named
// "linux_386_race" instead of the usual "linux_386".
InstallSuffix string
// By default, Import uses the operating system's file system calls
// to read directories and files. To read from other sources,
// callers can set the following functions. They all have default
// behaviors that use the local file system, so clients need only set
// the functions whose behaviors they wish to change.
// JoinPath joins the sequence of path fragments into a single path.
// If JoinPath is nil, Import uses filepath.Join.
JoinPath func(elem ...string) string
// SplitPathList splits the path list into a slice of individual paths.
// If SplitPathList is nil, Import uses filepath.SplitList.
SplitPathList func(list string) []string
// IsAbsPath reports whether path is an absolute path.
// If IsAbsPath is nil, Import uses filepath.IsAbs.
IsAbsPath func(path string) bool
// IsDir reports whether the path names a directory.
// If IsDir is nil, Import calls os.Stat and uses the result's IsDir method.
IsDir func(path string) bool
// HasSubdir reports whether dir is lexically a subdirectory of
// root, perhaps multiple levels below. It does not try to check
// whether dir exists.
// If so, HasSubdir sets rel to a slash-separated path that
// can be joined to root to produce a path equivalent to dir.
// If HasSubdir is nil, Import uses an implementation built on
// filepath.EvalSymlinks.
HasSubdir func(root, dir string) (rel string, ok bool)
// ReadDir returns a slice of fs.FileInfo, sorted by Name,
// describing the content of the named directory.
// If ReadDir is nil, Import uses ioutil.ReadDir.
ReadDir func(dir string) ([]fs.FileInfo, error)
// OpenFile opens a file (not a directory) for reading.
// If OpenFile is nil, Import uses os.Open.
OpenFile func(path string) (io.ReadCloser, error)
}
Context.Import
Context.Import
通过包的路径来确定包的目录,这个过程中可通过调用go list -e -- path
读取输出来确定,注意-e
选项,在找不到的情况下返回path本身.
https://github.com/golang/go/blob/release-branch.go1.15/src/go/build/build.go#L492
go tool compile
经过上面的介绍,我们知道真正编译包的工作是通过调用go tool compile
完成的,我们现在来看一下其内部实现。
https://github.com/golang/go/blob/release-branch.go1.15/src/cmd/compile/main.go#L41
支持的命令行标志
-+
编译runtime
-std
编译标准库
-d str
打印调试信息, go tool compile -d help
显示debug帮助信息
-pack
输出到file.a
而不是file.o
-trimpath
从记录文件中去掉前缀
-p pkg
指定import path
下面这个例子,取自go build
的调试输出,例子:
go tool compile -o $WORK/b001/_pkg_.a -trimpath $WORK/b001=> -p main -lang=go1.13 -complete -
buildid 9_0557aJwaAvYDCxLHSA/9_0557aJwaAvYDCxLHSA -D -importcfg $WORK/b001/importcfg -pack -c=4 ./init.go ./main.go $WORK/b001/_gomod_.go
初始化工作
下面这段代码是gc.Main
的初始部分,我们可以看到内置包:go.builtin
,unsafe
,runtime
等
localpkg = types.NewPkg("", "")
localpkg.Prefix = "\"\""
// We won't know localpkg's height until after import
// processing. In the mean time, set to MaxPkgHeight to ensure
// height comparisons at least work until then.
localpkg.Height = types.MaxPkgHeight
// pseudo-package, for scoping
builtinpkg = types.NewPkg("go.builtin", "") // TODO(gri) name this package go.builtin?
builtinpkg.Prefix = "go.builtin" // not go%2ebuiltin
// pseudo-package, accessed by import "unsafe"
unsafepkg = types.NewPkg("unsafe", "unsafe")
// Pseudo-package that contains the compiler's builtin
// declarations for package runtime. These are declared in a
// separate package to avoid conflicts with package runtime's
// actual declarations, which may differ intentionally but
// insignificantly.
Runtimepkg = types.NewPkg("go.runtime", "runtime")
Runtimepkg.Prefix = "runtime"
// pseudo-packages used in symbol tables
itabpkg = types.NewPkg("go.itab", "go.itab")
itabpkg.Prefix = "go.itab" // not go%2eitab
itablinkpkg = types.NewPkg("go.itablink", "go.itablink")
itablinkpkg.Prefix = "go.itablink" // not go%2eitablink
trackpkg = types.NewPkg("go.track", "go.track")
trackpkg.Prefix = "go.track" // not go%2etrack
// pseudo-package used for map zero values
mappkg = types.NewPkg("go.map", "go.map")
mappkg.Prefix = "go.map"
// pseudo-package used for methods with anonymous receivers
gopkg = types.NewPkg("go", "")
读取-importcfg
参数
https://github.com/golang/go/blob/release-branch.go1.15/src/cmd/compile/internal/gc/main.go#L882
读取包名到已编译的文件对象的映射,后续的编译只需要使用这个map来确定编译好的文件。
该文件内容格式:
packagefile name=path
...
importmap alias=name
....
initUniverse
初始化一些基本的类型,如原始类型int
, double
,error
,byte
,rune
,unsafe.Pointer
等类型
原始类型初始化在builtin
包中,Pointer
初始化在unsafe
包中.
因此实际上这些包是不会专门去编译生成的。
parseFiles(files)
将文件编译成*syntax.File
类型,每个语法节点都是*Node
类型,所有的文件的节点顶级节点(decls
)都记录在全局数组xtop
中。
顶级节点的声明和转换包括:
https://github.com/golang/go/blob/release-branch.go1.15/src/cmd/compile/internal/gc/noder.go#L286
import语句
转换成已编译的.a
或.o
文件
resolve
https://github.com/golang/go/blob/release-branch.go1.15/src/cmd/compile/internal/gc/typecheck.go#L80
将未命名的实体解析为具体的值或类型。 语法解析完成后,类似于int
, iota
这样的语法节点(*Node
实体)是还没有具体的值和类型的。resolve
针对所有没有解析的实体(Op==ONONAME
)进行解析。比如:
const (
C0 int = iota // int和iota是未解析的
C1
)
var a NONE // NONE是未解析的
typecheck
调用类型检查
经过resolve
解析之后,如果还存在Op==ONONAME
的节点,则报错:
<file>:<lineno>:<col> undefined: <name>
deadcode
无用代码消除
无用代码是什么意思呢?举个例子:
a && b 如果a=true, 则简化为b
如果a=false, 则简化为a
a || b 如果a=true,则简化为a
如果a=false,则简化为b
if a {
left
}else{
right
}
如果a=true则简化为left,a=false则简化为right
if a {
...
panic or return
}
...
如果a=true,且if或else的语句以panic或return结束, 则if后面的所有语句都可以消除
fninit(nodes)
构造init
函数, 收集所有的包级别的init函数和初始化语句:赋值语句,调用语句等。
遍历语法树,构建出变量初始化依赖图。如果存在环状依赖,则报错
var g = b
var b = d
var d = g
cycleinit.go:5:5: typechecking loop involving g = b
cycleinit.go:5:5 g
cycleinit.go:8:5 d = g
cycleinit.go:8:5 d
cycleinit.go:6:5 b = d
cycleinit.go:6:5 b
cycleinit.go:5:5 g = b
capturevars(fn)
变量捕获分析(值捕获或引用捕获)
typecheckinl
内联
escapes(xtop)
逃逸分析
transformclosure
闭包转换
initssaconfig
,compileFunctions
ssa编译和pass优化
typecheck(external)
外部依赖分析
GOSSAFUNC
和GOSSADIR
环境变量GOSSAFUNC
的格式: NAME, NAME+, NAME:CFG, NAME:CFG+
其中,+表示输出到标准输出, CFG表示打印Control Flow Graph.
GOSSADIR
指定输出目录,如果指定了,则输出文件为: $GOSSADIR/$importPath/$funcName.html
调用compile
go tool compile -o tmp/moduleworld.a -p main -lang=go1.13 -complete *.go
cmd/compile/internal/syntax
syntax.ParseFile
ParseFile
将文件编译成AST。在go tool compile
调用时,会启动多个协程去编译文件(并发度=NCPU+10)。