了解Go编译处理(一)—— go tool

前言

博主在查找一些Go内置的关键字(如make、append等)的具体实现源码时,发现网上的部分说明只直接提到了源码位于哪个package等,并未提及缘由。对应package内的源码中的func又是怎么被调用?这些都是让人疑惑的地方。
在初步研究后,博主准备通过一系列的文章,大致说明下这些内置关键字的处理过程及调用的查找快速查找方式,希望能对大家查找源码实现有所帮助。

Go是编译型语言,Go程序需要经过编译生成可执行文件才能运行,编译的命令是go buildgo是Go语言自带的强大工具,包含多个command,如go get命令拉取或更新代码,go run运行代码。在了解编译命令go build之前,先了解下go

使用方式

可以直接在终端中运行go,即可看到如下的使用提示。

Go is a tool for managing Go source code.

Usage:

    go <command> [arguments]

The commands are:

    bug         start a bug report
    build       compile packages and dependencies
    clean       remove object files and cached files
    doc         show documentation for package or symbol
    env         print Go environment information
    fix         update packages to use new APIs
    fmt         gofmt (reformat) package sources
    generate    generate Go files by processing source
    get         add dependencies to current module and install them
    install     compile and install packages and dependencies
    list        list packages or modules
    mod         module maintenance
    run         compile and run Go program
    test        test packages
    tool        run specified go tool
    version     print Go version
    vet         report likely mistakes in packages

Use "go help <command>" for more information about a command.

Additional help topics:

    buildmode   build modes
    c           calling between Go and C
    cache       build and test caching
    environment environment variables
    filetype    file types
    go.mod      the go.mod file
    gopath      GOPATH environment variable
    gopath-get  legacy GOPATH go get
    goproxy     module proxy protocol
    importpath  import path syntax
    modules     modules, module versions, and more
    module-get  module-aware go get
    module-auth module authentication using go.sum
    module-private module configuration for non-public modules
    packages    package lists and patterns
    testflag    testing flags
    testfunc    testing functions

Use "go help <topic>" for more information about that topic.

从提示中可以看出,go工具的使用方式如下:

go <command> [arguments]

对于具体command的说明可以运行go help <command>获取。

go tool溯源

go tool本身是由go语言实现的,源码位于/cmd/go package。

这里说下查找源码的简单小方法:当无法直接通过调用间的跳转找到源码时,可以直接通过全局搜索(范围选择所有位置)的方式来找相关的源码。如:我们要找go命令的源码,我们知道go命令的参数解析都是经过flag实现的。直接运行go命令,可以看到相关的help,搜索任一命令对应的解释,如:build的compile packages and dependencies,经过简单的排查即可找到对应的源码。

go tool main

go tool对应的源码入口在/cmd/go/main.go文件中,命令的入口为main func。main.go中还包含2个init的func,在了解main func前,先看下init func。

init func

func init() {
    base.Go.Commands = []*base.Command{
        //以下为具体的命令
        bug.CmdBug,//bug
        work.CmdBuild,//build
        clean.CmdClean,//clean
        doc.CmdDoc,//doc
        envcmd.CmdEnv,//env
        fix.CmdFix,//fix
        fmtcmd.CmdFmt,//fmt
        generate.CmdGenerate,//generate
        modget.CmdGet,//get
        work.CmdInstall,//install
        list.CmdList,//list
        modcmd.CmdMod,//mod
        run.CmdRun,//run
        test.CmdTest,//test
        tool.CmdTool,//tool
        version.CmdVersion,//version
        vet.CmdVet,//vet
        //以下为命令的具体的参数
        help.HelpBuildmode,
        help.HelpC,
        help.HelpCache,
        help.HelpEnvironment,
        help.HelpFileType,
        modload.HelpGoMod,
        help.HelpGopath,
        get.HelpGopathGet,
        modfetch.HelpGoproxy,
        help.HelpImportPath,
        modload.HelpModules,
        modget.HelpModuleGet,
        modfetch.HelpModuleAuth,
        modfetch.HelpModulePrivate,
        help.HelpPackages,
        test.HelpTestflag,
        test.HelpTestfunc,
    }
}

type Command struct {
    // Run runs the command.
    // The args are the arguments after the command name.
    Run func(cmd *Command, args []string)

    // UsageLine is the one-line usage message.
    // The words between "go" and the first flag or argument in the line are taken to be the command name.
    UsageLine string

    // Short is the short description shown in the 'go help' output.
    Short string

    // Long is the long message shown in the 'go help <this-command>' output.
    Long string

    // Flag is a set of flags specific to this command.
    Flag flag.FlagSet

    // CustomFlags indicates that the command will do its own
    // flag parsing.
    CustomFlags bool

    // Commands lists the available commands and help topics.
    // The order here is the order in which they are printed by 'go help'.
    // Note that subcommands are in general best avoided.
    Commands []*Command
}

var Go = &Command{
    UsageLine: "go",
    Long:      `Go is a tool for managing Go source code.`,
    // Commands initialized in package main
}

我们知道,同一个文件中出现多个init func时,会按照出现的顺序依次执行。

先看第一个init。base.Go是Command的具体实例,内里包含的UsageLine与Long正对应我们运行go命令获取的前2行。整个func就是是对base.Go初始化过程,封装了各个命令的对应处理至Commands参数中。

注意:每个命令的处理也有相关的init处理,根据依赖关系,这些init func运行在main的init前。如build对应的work.CmdBuild,其init中就指定了CmdBuild的Run func(此处仅粗略提及整个处理过程,具体过程在后续的文章中会详细探讨)。

var CmdBuild = &base.Command{
    UsageLine: "go build [-o output] [-i] [build flags] [packages]",
    Short:     "compile packages and dependencies",
    Long: `
Build compiles the packages named by the import paths,
along with their dependencies, but it does not install the results.
...
    `,
}

func init() {
    ...
    CmdBuild.Run = runBuild
    ...
}

第二个init封装了默认的Usage,mainUsage中是对base.Go的格式化说明。

func init() {
    base.Usage = mainUsage
}

func mainUsage() {
    help.PrintUsage(os.Stderr, base.Go)
    os.Exit(2)
}

func PrintUsage(w io.Writer, cmd *base.Command) {
    bw := bufio.NewWriter(w)
    tmpl(bw, usageTemplate, cmd)
    bw.Flush()
}

var usageTemplate = `{{.Long | trim}}

Usage:

    {{.UsageLine}} <command> [arguments]

The commands are:
{{range .Commands}}{{if or (.Runnable) .Commands}}
    {{.Name | printf "%-11s"}} {{.Short}}{{end}}{{end}}

Use "go help{{with .LongName}} {{.}}{{end}} <command>" for more information about a command.
{{if eq (.UsageLine) "go"}}
Additional help topics:
{{range .Commands}}{{if and (not .Runnable) (not .Commands)}}
    {{.Name | printf "%-11s"}} {{.Short}}{{end}}{{end}}

Use "go help{{with .LongName}} {{.}}{{end}} <topic>" for more information about that topic.
{{end}}
`

main func

main func是程序运行的入口,看下其处理的逻辑。

func main() {
    _ = go11tag
    flag.Usage = base.Usage
    flag.Parse()
    log.SetFlags(0)

    args := flag.Args()
    if len(args) < 1 {
        base.Usage()
    }

    if args[0] == "get" || args[0] == "help" {
        if !modload.WillBeEnabled() {
            // Replace module-aware get with GOPATH get if appropriate.
            *modget.CmdGet = *get.CmdGet
        }
    }

    cfg.CmdName = args[0] // for error messages
    if args[0] == "help" {
        help.Help(os.Stdout, args[1:])
        return
    }

    // Diagnose common mistake: GOPATH==GOROOT.
    // This setting is equivalent to not setting GOPATH at all,
    // which is not what most people want when they do it.
    if gopath := cfg.BuildContext.GOPATH; filepath.Clean(gopath) == filepath.Clean(runtime.GOROOT()) {
        fmt.Fprintf(os.Stderr, "warning: GOPATH set to GOROOT (%s) has no effect\n", gopath)
    } else {
        for _, p := range filepath.SplitList(gopath) {
            // Some GOPATHs have empty directory elements - ignore them.
            // See issue 21928 for details.
            if p == "" {
                continue
            }
            // Note: using HasPrefix instead of Contains because a ~ can appear
            // in the middle of directory elements, such as /tmp/git-1.8.2~rc3
            // or C:\PROGRA~1. Only ~ as a path prefix has meaning to the shell.
            if strings.HasPrefix(p, "~") {
                fmt.Fprintf(os.Stderr, "go: GOPATH entry cannot start with shell metacharacter '~': %q\n", p)
                os.Exit(2)
            }
            if !filepath.IsAbs(p) {
                if cfg.Getenv("GOPATH") == "" {
                    // We inferred $GOPATH from $HOME and did a bad job at it.
                    // Instead of dying, uninfer it.
                    cfg.BuildContext.GOPATH = ""
                } else {
                    fmt.Fprintf(os.Stderr, "go: GOPATH entry is relative; must be absolute path: %q.\nFor more details see: 'go help gopath'\n", p)
                    os.Exit(2)
                }
            }
        }
    }

    if fi, err := os.Stat(cfg.GOROOT); err != nil || !fi.IsDir() {
        fmt.Fprintf(os.Stderr, "go: cannot find GOROOT directory: %v\n", cfg.GOROOT)
        os.Exit(2)
    }

    // Set environment (GOOS, GOARCH, etc) explicitly.
    // In theory all the commands we invoke should have
    // the same default computation of these as we do,
    // but in practice there might be skew
    // This makes sure we all agree.
    cfg.OrigEnv = os.Environ()
    cfg.CmdEnv = envcmd.MkEnv()
    for _, env := range cfg.CmdEnv {
        if os.Getenv(env.Name) != env.Value {
            os.Setenv(env.Name, env.Value)
        }
    }

BigCmdLoop:
    for bigCmd := base.Go; ; {
        for _, cmd := range bigCmd.Commands {
            if cmd.Name() != args[0] {
                continue
            }
            if len(cmd.Commands) > 0 {
                bigCmd = cmd
                args = args[1:]
                if len(args) == 0 {
                    help.PrintUsage(os.Stderr, bigCmd)
                    base.SetExitStatus(2)
                    base.Exit()
                }
                if args[0] == "help" {
                    // Accept 'go mod help' and 'go mod help foo' for 'go help mod' and 'go help mod foo'.
                    help.Help(os.Stdout, append(strings.Split(cfg.CmdName, " "), args[1:]...))
                    return
                }
                cfg.CmdName += " " + args[0]
                continue BigCmdLoop
            }
            if !cmd.Runnable() {
                continue
            }
            cmd.Flag.Usage = func() { cmd.Usage() }
            if cmd.CustomFlags {
                args = args[1:]
            } else {
                base.SetFromGOFLAGS(cmd.Flag)
                cmd.Flag.Parse(args[1:])
                args = cmd.Flag.Args()
            }
            cmd.Run(cmd, args)
            base.Exit()
            return
        }
        helpArg := ""
        if i := strings.LastIndex(cfg.CmdName, " "); i >= 0 {
            helpArg = " " + cfg.CmdName[:i]
        }
        fmt.Fprintf(os.Stderr, "go %s: unknown command\nRun 'go help%s' for usage.\n", cfg.CmdName, helpArg)
        base.SetExitStatus(2)
        base.Exit()
    }
}

main的处理逻辑大致如下:

  1. 没有参数时,直接打印mainUsage,退出
  2. 对于get、help命令,如果没开启mod,则mod get替换为get
  3. 第一个参数为help时,根据后续命令处理
    • 如果没有命令,直接打印mainUsage
    • 后续仅一个命令且为documentation,打印所有命令的documentation。
    • 后续有多个命令时,确认后续命名是否是前一个子命令。若不是,则打印出错处;若一直是,则打印对应的说明。
  4. 检查GOPATH,若GOPATH与GOROOT设置在同一文件夹,则警告
  5. 检查GOROOT
  6. 获取环境变量,对于错误设置的环境变量值,会主动修正至正确值,减少错误带来的影响
  7. 循环查找对应的命令
    • 目标命令存在子命令
      • 若传入命令不足,则打印说明并退出;
      • 若后续命令为help,则打印对应的说明
      • 正常,则依次拼凑命令
    • 若命令不可执行,则跳过
    • 参数解析(若是需要自行解析,由对应的命令进行解析,否则统一解析参数),执行命令Run,执行结束后退出
  8. 若命令不存在,打印unknown command错误并结束运行

总体来说,go会先检查GOROOT、GOPATH等环境变量是否符合要求,然后获取调用参数。查询参数中是否出现init中封装在Commands中处理的具体Command,如果有的话,则进行subCommand的匹配,一直到完全匹配为止,中间有任何匹配不上处,均会报错退出。注意:go针对help及非help命令做了处理,两者最大的不同处是,匹配后help返回的使用提示,其他命令则执行命令的操作。

总结

本文主要是介绍go工具入口的处理逻辑,go工具的源码位于/cmd/go package,其本身只负责部分flag的解析,command的匹配,具体的执行由其internal package下对应command的Run func执行。稍后的文章中会说明go build的处理过程。

公众号

鄙人刚刚开通了公众号,专注于分享Go开发相关内容,望大家感兴趣的支持一下,在此特别感谢。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章