【go】golang源码各个包功能分析

快速链接: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定位到模块根目录,根据环境变量GO111MODULEon, 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.gocfg.BuildO = init,
go build main.go init.gocfg.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包,还会被linkWork目录下,但不会进行安装。所以,多包构建的过程中还是会链接生成可执行文件, 但可执行文件只会被生成到WORK目录下。如果没有指定-work标志,则随着程序结束,这些生成的可执行文件也会被一并删除。

link是依赖build的,build生成归档文件,会缓存到Build-Cache中,但是link不会缓存。linkbuild生成的ActionID不相同。

对于build action,还有一点需要注意,如果存在一个link action依赖了这个build action,如果目标文件已经存在且没有发生变化,则即使build actionBuild-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.TargetBuildID是否以当前ActionActionID + "/"开头即可。

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, doubleerror,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,ifelse的语句以panicreturn结束,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)

外部依赖分析

GOSSAFUNCGOSSADIR

环境变量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

https://github.com/golang/go/blob/release-branch.go1.15/src/cmd/compile/internal/syntax/syntax.go#L84

ParseFile将文件编译成AST。在go tool compile调用时,会启动多个协程去编译文件(并发度=NCPU+10)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值