我在使用 go build -x -a -w main.go > build.sh
时,本来想把 build 的整个过程导出,然后可以分析 build 实际执行的情况,但是发现 build.sh 一直是空文件,这就引起了我的好奇,为什么将日志重定向到文件没有生效。
我去翻了一下源码,发现
$GOROOT/src/cmd/go/internal/cfg/cfg.go
下
// These are general "build flags" used by build and other commands.
var (
...
BuildV bool // -v flag
BuildWork bool // -work flag
BuildX bool // -x flag
....
)
有如下全局变量定义,如果 build 后面添加 flag ,则如数记录在这里。
$GOROOT/src/cmd/go/internal/work/gc.go:389
if cfg.BuildN || cfg.BuildX {
cmdline := str.StringList(base.Tool("pack"), "r", absAfile, absOfiles)
b.Showcmd(p.Dir, "%s # internal", joinUnambiguously(cmdline))
}
后面有很多判断,就是根据这个 BuildX 判断是否需要打印执行日志。
这里继续追一下 Showcmd 函数,就是打印的函数,
$GOROOT/src/cmd/go/internal/work/exec.go:1784
func (b *Builder) Showcmd(dir string, format string, args ...interface{}) {
b.output.Lock()
defer b.output.Unlock()
b.Print(b.fmtcmd(dir, format, args...) + "\n")
}
这里反正就是一些拼接。
$GOROOT/src/cmd/go/internal/work/action.go:37
type Builder struct {
...
Print func(args ...interface{}) (int, error)
}
b.Print 定义是 func(args ...interface{}) (int, error)
,再去追一下 fmtcmd 函数。
$GOROOT/src/cmd/go/internal/work/exec.go:1763
func (b *Builder) fmtcmd(dir string, format string, args ...interface{}) string {
cmd := fmt.Sprintf(format, args...)
if dir != "" && dir != "/" {
dot := " ."
if dir[len(dir)-1] == filepath.Separator {
dot += string(filepath.Separator)
}
cmd = strings.ReplaceAll(" "+cmd, " "+dir, dot)[1:]
if b.scriptDir != dir {
b.scriptDir = dir
cmd = "cd " + dir + "\n" + cmd
}
}
if b.WorkDir != "" {
cmd = strings.ReplaceAll(cmd, b.WorkDir, "$WORK")
}
return cmd
}
做了一些判断,当前路径和执行脚本路径,则 cd 到脚本路径下。
逻辑就是这个函数返回了一个字符串,然后 b.Print 打印出来了。
所以要看一下 这个 Print 是如何赋值的。
所以要看一下入口文件 go build
的入口文件,如何定义的了。
$GOROOT/src/cmd/go/main.go 为 go 这个 cmd 的入口文件,
文件的 init 函数当中有一行定义
func init() {
base.Go.Commands = []*base.Command{
bug.CmdBug,
work.CmdBuild, # 这里是重点
clean.CmdClean,
doc.CmdDoc,
envcmd.CmdEnv,
fix.CmdFix,
fmtcmd.CmdFmt,
generate.CmdGenerate,
get.CmdGet,
work.CmdInstall,
list.CmdList,
modcmd.CmdMod,
run.CmdRun,
test.CmdTest,
tool.CmdTool,
version.CmdVersion,
vet.CmdVet,
help.HelpBuildmode,
help.HelpC,
help.HelpCache,
help.HelpEnvironment,
help.HelpFileType,
modload.HelpGoMod,
help.HelpGopath,
get.HelpGopathGet,
modfetch.HelpGoproxy,
help.HelpImportPath,
modload.HelpModules,
modget.HelpModuleGet,
help.HelpPackages,
test.HelpTestflag,
test.HelpTestfunc,
}
work.CmdBuild 是整个 go build 子命令的所有定义模块
$GOROOT/src/cmd/go/internal/work/build.go:150
func init() {
// break init cycle
CmdBuild.Run = runBuild # 重点
CmdInstall.Run = runInstall
CmdBuild.Flag.BoolVar(&cfg.BuildI, "i", false, "")
CmdBuild.Flag.StringVar(&cfg.BuildO, "o", "", "output file")
CmdInstall.Flag.BoolVar(&cfg.BuildI, "i", false, "")
AddBuildFlags(CmdBuild)
AddBuildFlags(CmdInstall)
}
init 为当前 package 的初始化方法。
$GOROOT/src/cmd/go/internal/work/build.go:279
func runBuild(cmd *base.Command, args []string) {
BuildInit()
var b Builder
b.Init() # 重点
...
}
runBuild 里面发现 Print 的定义
func (b *Builder) Init() {
# 就是现在
b.Print = func(a ...interface{}) (int, error) {
return fmt.Fprint(os.Stderr, a...)
}
b.actionCache = make(map[cacheKey]*Action)
b.mkdirCache = make(map[string]bool)
b.toolIDCache = make(map[string]string)
b.buildIDCache = make(map[string]string)
if cfg.BuildN {
b.WorkDir = "$WORK"
} else {
tmp, err := ioutil.TempDir(os.Getenv("GOTMPDIR"), "go-build")
if err != nil {
base.Fatalf("go: creating work dir: %v", err)
}
if !filepath.IsAbs(tmp) {
abs, err := filepath.Abs(tmp)
if err != nil {
os.RemoveAll(tmp)
base.Fatalf("go: creating work dir: %v", err)
}
tmp = abs
}
b.WorkDir = tmp
if cfg.BuildX || cfg.BuildWork {
fmt.Fprintf(os.Stderr, "WORK=%s\n", b.WorkDir)
}
if !cfg.BuildWork {
workdir := b.WorkDir
base.AtExit(func() { os.RemoveAll(workdir) })
}
}
if _, ok := cfg.OSArchSupportsCgo[cfg.Goos+"/"+cfg.Goarch]; !ok && cfg.BuildContext.Compiler == "gc" {
fmt.Fprintf(os.Stderr, "cmd/go: unsupported GOOS/GOARCH pair %s/%s\n", cfg.Goos, cfg.Goarch)
os.Exit(2)
}
for _, tag := range cfg.BuildContext.BuildTags {
if strings.Contains(tag, ",") {
fmt.Fprintf(os.Stderr, "cmd/go: -tags space-separated list contains comma\n")
os.Exit(2)
}
}
}
经过层层封装,这里的第一个赋值就是 b.Print ,清楚写着最终是调用的系统的打印方法,将输出打印到标准输出 Stderr。
经过上面源码的探索,发现 go build -x 最终是将日志写到标准错误流当中,由于 Linux 系统定义的三个标准流的如下,0 表示输入流,1表示标准输出流,2表示标准错误流。
又因为默认重定向的符号 >
,是省略了一个 1,实际是 1>
,
所以将以上的命令修改如下 go build -x -a -w main.go 2> build.sh
就可以将日志输出到文件中。
顺便说一句,如果是将正确日志,错误日志都输出到日志文件则可以如下:
go build -x -a -w main.go > build.sh &2>1
&2 表示错误流重定向到 1 标准输出流。
网友评论