瞭解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開發相關內容,望大家感興趣的支持一下,在此特別感謝。

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