intro
go作为一个新生的语言,跟C++相比提供了更多的易用性,但是对(习惯了C++的)新手来说这种便利也封装了更多的细节。一个基本的问题是:C++工程通常基于Makefile/CMake/bazel等外部工具进行构建,但是go的构建通常只需要使用go build或者go install这样的单个命令完成即可。
这也也会带来一些疑惑:当一个工程会生成多个命令(例如k8s输出的kubectl、kube apiserver等)的时候,go build如何来构建?如何区分构建输出的是一个package还是一个可执行文件?命令行中可以同时提供package(文件夹)和go源文件吗?可以同时构建多个package吗?命令行中的文件夹会自动递归处理吗?
由于文件夹和package有某种天然的相似性,那么同一个文件夹中可以包含多个package吗?反过来,一个package可以分散在多个文件夹吗?
同时包含源文件和文件
当命令行同时提供了源文件(p1.go)和文件夹(.)时,build会提示错误:named files must be .go files: .
(gdb) info proc
process 309746
cmdline = '/home/tsecer/source/go/bin/go build p1/p1.go .'
cwd = '/home/tsecer/playground/go/multiple_pack'
exe = '/home/tsecer/source/go/bin/go'
(gdb) bt
#0 cmd/go/internal/load.GoFilesPackage (ctx=..., opts=..., gofiles=..., ~r0=<optimized out>) at /home/tsecer/source/go/src/cmd/go/internal/load/pkg.go:3120
#1 0x00000000008c5d5c in cmd/go/internal/load.PackagesAndErrors (ctx=..., opts=..., patterns=..., ~r0=..., ~r0=...) at /home/tsecer/source/go/src/cmd/go/internal/load/pkg.go:2792
#2 0x0000000000928ce8 in cmd/go/internal/work.runBuild (ctx=..., cmd=<optimized out>, args=...) at /home/tsecer/source/go/src/cmd/go/internal/work/build.go:470
#3 0x00000000009c282e in main.invoke (cmd=0xf92be0, args=...) at /home/tsecer/source/go/src/cmd/go/main.go:295
#4 0x00000000009c1c79 in main.main () at /home/tsecer/source/go/src/cmd/go/main.go:209
(gdb)
通过对应的代码和调用链可以看到:如果在命令行中检测到一个".go"结尾的文件,则会进入到GoFilesPackage函数。
///@file: src\cmd\go\internal\load\pkg.go
func PackagesAndErrors(ctx context.Context, opts PackageOpts, patterns []string) []*Package {
ctx, span := trace.StartSpan(ctx, "load.PackagesAndErrors")
defer span.Done()
for _, p := range patterns {
// Listing is only supported with all patterns referring to either:
// - Files that are part of the same directory.
// - Explicit package paths or patterns.
if strings.HasSuffix(p, ".go") {
// We need to test whether the path is an actual Go file and not a
// package path or pattern ending in '.go' (see golang.org/issue/34653).
if fi, err := fsys.Stat(p); err == nil && !fi.IsDir() {
pkgs := []*Package{GoFilesPackage(ctx, opts, patterns)}
setPGOProfilePath(pkgs)
return pkgs
}
}
}
///...
}
GoFilesPackage函数中会检测所有输入都为".go"结尾(的源文件)。
///@file: src\cmd\go\internal\load\pkg.go
func GoFilesPackage(ctx context.Context, opts PackageOpts, gofiles []string) *Package {
modload.Init()
for _, f := range gofiles {
if !strings.HasSuffix(f, ".go") {
pkg := new(Package)
pkg.Internal.Local = true
pkg.Internal.CmdlineFiles = true
pkg.Name = f
pkg.Error = &PackageError{
Err: fmt.Errorf("named files must be .go files: %s", pkg.Name),
}
pkg.Incomplete = true
return pkg
}
}
///..
}
结论:build的输入如果包含一个go源文件,所有输入都必须是源文件。
文件夹
开始的分支就是判断是否开启了module功能,这个功能直观上说就是go.mod实现的功能。如果没有开启该功能(cfg.ModulesEnabled),会执行到search.ImportPaths函数,从这个名字看,应该主要是文件系统的操作。
///@file: src\cmd\go\internal\load\pkg.go
func PackagesAndErrors(ctx context.Context, opts PackageOpts, patterns []string) []*Package {
ctx, span := trace.StartSpan(ctx, "load.PackagesAndErrors")
defer span.Done()
///...
var matches []*search.Match
if modload.Init(); cfg.ModulesEnabled {
modOpts := modload.PackageOpts{
ResolveMissingImports: true,
LoadTests: opts.ModResolveTests,
SilencePackageErrors: true,
}
matches, _ = modload.LoadPackages(ctx, modOpts, patterns...)
} else {
noModRoots := []string{}
matches = search.ImportPaths(patterns, noModRoots)
}
///...
}
search.ImportPaths返回的是一个Match结构,从结构中看,函数已经将文件系统中内容按照package提取了出来(也就是从文件系统文件转换成了对应的package名字)。
///@file: src\cmd\go\internal\search\search.go
// A Match represents the result of matching a single package pattern.
type Match struct {
pattern string // the pattern itself
Dirs []string // if the pattern is local, directories that potentially contain matching packages
Pkgs []string // matching packages (import paths)
Errs []error // errors matching the patterns to packages, NOT errors loading those packages
// Errs may be non-empty even if len(Pkgs) > 0, indicating that some matching
// packages could be located but results may be incomplete.
// If len(Pkgs) == 0 && len(Errs) == 0, the pattern is well-formed but did not
// match any packages.
}
文件夹搜索完成之后,对于package和main的处理主要还是在runBuild函数,这个函数比较有意思的地方是递归调用了构建进程(go build),这也意味着它能够处理多个package的情况。
这里的流程是判断如果命令行通过-o选项指定了输出,如果输出是一个文件夹,那么循环中会为每个main生成一个AutoAction动作(进行构建);如果没有指定-o输出,则会对所有搜索到的pkg调用"go build"进程。
///@file: src\cmd\go\internal\work\build.go
func runBuild(ctx context.Context, cmd *base.Command, args []string) {
///...
if cfg.BuildO != "" {
// If the -o name exists and is a directory or
// ends with a slash or backslash, then
// write all main packages to that directory.
// Otherwise require only a single package be built.
if fi, err := os.Stat(cfg.BuildO); (err == nil && fi.IsDir()) ||
strings.HasSuffix(cfg.BuildO, "/") ||
strings.HasSuffix(cfg.BuildO, string(os.PathSeparator)) {
if !explicitO {
base.Fatalf("go: build output %q already exists and is a directory", cfg.BuildO)
}
a := &Action{Mode: "go build"}
for _, p := range pkgs {
if p.Name != "main" {
continue
}
p.Target = filepath.Join(cfg.BuildO, p.DefaultExecName())
p.Target += cfg.ExeSuffix
p.Stale = true
p.StaleReason = "build -o flag in use"
a.Deps = append(a.Deps, b.AutoAction(ModeInstall, depMode, p))
}
if len(a.Deps) == 0 {
base.Fatalf("go: no main packages to build")
}
b.Do(ctx, a)
return
}
if len(pkgs) > 1 {
base.Fatalf("go: cannot write multiple packages to non-directory %s", cfg.BuildO)
} else if len(pkgs) == 0 {
base.Fatalf("no packages to build")
}
p := pkgs[0]
p.Target = cfg.BuildO
p.Stale = true // must build - not up to date
p.StaleReason = "build -o flag in use"
a := b.AutoAction(ModeInstall, depMode, p)
b.Do(ctx, a)
return
}
a := &Action{Mode: "go build"}
for _, p := range pkgs {
a.Deps = append(a.Deps, b.AutoAction(ModeBuild, depMode, p))
}
if cfg.BuildBuildmode == "shared" {
a = b.buildmodeShared(ModeBuild, depMode, args, pkgs, a)
}
b.Do(ctx, a)
}
compile or link
在前面的AutoAction函数中,会直接根据package的名字决定是执行编译还是链接。
///@file: src\cmd\go\internal\work\action.go
// AutoAction returns the "right" action for go build or go install of p.
func (b *Builder) AutoAction(mode, depMode BuildMode, p *load.Package) *Action {
if p.Name == "main" {
return b.LinkAction(mode, depMode, p)
}
return b.CompileAction(mode, depMode, p)
}
none
没有指定文件夹时使用当前目录。
///@file: src\cmd\go\internal\search\search.go
// CleanPatterns returns the patterns to use for the given command line. It
// canonicalizes the patterns but does not evaluate any matches. For patterns
// that are not local or absolute paths, it preserves text after '@' to avoid
// modifying version queries.
func CleanPatterns(patterns []string) []string {
if len(patterns) == 0 {
return []string{"."}
}
一些测试
可以看见,package在go内部是编译成了.a类型文件。
tsecer@harry-LC0:~/playground/go/multiple_pack$ strace -f -s99999 -e trace=fork,execve go build ./p1 ./p2
execve("/usr/local/go/bin/go", ["go", "build", "./p1", "./p2"], 0x7ffda83e86d8 /* 49 vars */) = 0
strace: Process 310589 attached
strace: Process 310591 attached
strace: Process 310590 attached
...
strace: Process 310614 attached
[pid 310615] execve("/usr/local/go/pkg/tool/linux_amd64/compile", ["/usr/local/go/pkg/tool/linux_amd64/compile", "-o", "/tmp/go-build1404442041/b001/_pkg_.a", "-trimpath", "/tmp/go-build1404442041/b001=>", "-p", "_/home/tsecer/playground/go/multiple_pack/p1", "-lang=go1.23", "-complete", "-buildid", "6tEaMmstUtv9GYfQK7V5/6tEaMmstUtv9GYfQK7V5", "-goversion", "go1.23.4", "-c=2", "-D", "_/home/tsecer/playground/go/multiple_pack/p1", "-importcfg", "/tmp/go-build1404442041/b001/importcfg", "-pack", "/home/tsecer/playground/go/multiple_pack/p1/p1.go"], 0xc000138008 /* 76 vars */) = 0
strace: Process 310616 attached
strace: Process 310618 attached
strace: Process 310619 attached
strace: Process 310617 attached
[pid 310614] execve("/usr/local/go/pkg/tool/linux_amd64/compile", ["/usr/local/go/pkg/tool/linux_amd64/compile", "-o", "/tmp/go-build1404442041/b055/_pkg_.a", "-trimpath", "/tmp/go-build1404442041/b055=>", "-p", "_/home/tsecer/playground/go/multiple_pack/p2", "-lang=go1.23", "-complete", "-buildid", "e22utOjUG4SFW6GL6LOZ/e22utOjUG4SFW6GL6LOZ", "-goversion", "go1.23.4", "-c=2", "-D", "_/home/tsecer/playground/go/multiple_pack/p2", "-importcfg", "/tmp/go-build1404442041/b055/importcfg", "-pack", "/home/tsecer/playground/go/multiple_pack/p2/p2.go"], 0xc000097688 /* 76 vars */ <unfinished ...>
同时构建两个可执行文件。但是也可以看到,因为package的名字是main,所以生成可执行文件的名字只能根据(第一个)文件名来决定。
tsecer@harry: date
Tue 07 Jan 2025 09:55:01 AM PST
tsecer@harry:
tsecer@harry:
tsecer@harry:
tsecer@harry: date
Tue 07 Jan 2025 09:55:10 AM PST
tsecer@harry: cat p1/main.go
package main
func main() {
}
tsecer@harry: cat p2/main.go
package main
func main() {
}
tsecer@harry: go build -o output/ ./p1 ./p2
tsecer@harry: ll output/
total 2992
drwxrwxr-x 2 tsecer tsecer 4096 Jan 7 09:54 ./
drwxrwxr-x 5 tsecer tsecer 4096 Jan 7 09:54 ../
-rwxrwxr-x 1 tsecer tsecer 1524390 Jan 7 09:55 p1*
-rwxrwxr-x 1 tsecer tsecer 1524390 Jan 7 09:55 p2*
tsecer@harry:
outro
go build的确相当于内置了C++中Makefile需要完成的功能,简化了构建过程。
标签:package,实现,cmd,tsecer,pkg,go,build From: https://www.cnblogs.com/tsecer/p/18658089