了解Go编译处理(一)—— go tool
前言
博主在查找一些Go內置的關鍵字(如make、append等)的具體實現源碼時,發現網上的部分說明只直接提到了源碼位于哪個package等,并未提及緣由。對應package內的源碼中的func又是怎么被調用?這些都是讓人疑惑的地方。
在初步研究后,博主準備通過一系列的文章,大致說明下這些內置關鍵字的處理過程及調用的查找快速查找方式,希望能對大家查找源碼實現有所幫助。
Go是編譯型語言,Go程序需要經過編譯生成可執行文件才能運行,編譯的命令是go build。go是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 reportbuild compile packages and dependenciesclean remove object files and cached filesdoc show documentation for package or symbolenv print Go environment informationfix update packages to use new APIsfmt gofmt (reformat) package sourcesgenerate generate Go files by processing sourceget add dependencies to current module and install theminstall compile and install packages and dependencieslist list packages or modulesmod module maintenancerun compile and run Go programtest test packagestool run specified go toolversion print Go versionvet report likely mistakes in packagesUse "go help <command>" for more information about a command.Additional help topics:buildmode build modesc calling between Go and Ccache build and test cachingenvironment environment variablesfiletype file typesgo.mod the go.mod filegopath GOPATH environment variablegopath-get legacy GOPATH go getgoproxy module proxy protocolimportpath import path syntaxmodules modules, module versions, and moremodule-get module-aware go getmodule-auth module authentication using go.summodule-private module configuration for non-public modulespackages package lists and patternstestflag testing flagstestfunc testing functionsUse "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,//bugwork.CmdBuild,//buildclean.CmdClean,//cleandoc.CmdDoc,//docenvcmd.CmdEnv,//envfix.CmdFix,//fixfmtcmd.CmdFmt,//fmtgenerate.CmdGenerate,//generatemodget.CmdGet,//getwork.CmdInstall,//installlist.CmdList,//listmodcmd.CmdMod,//modrun.CmdRun,//runtest.CmdTest,//testtool.CmdTool,//toolversion.CmdVersion,//versionvet.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() {_ = go11tagflag.Usage = base.Usageflag.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 messagesif 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 = cmdargs = 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的處理邏輯大致如下:
- 如果沒有命令,直接打印mainUsage
- 后續僅一個命令且為documentation,打印所有命令的documentation。
- 后續有多個命令時,確認后續命名是否是前一個子命令。若不是,則打印出錯處;若一直是,則打印對應的說明。
- 目標命令存在子命令
- 若傳入命令不足,則打印說明并退出;
- 若后續命令為help,則打印對應的說明
- 正常,則依次拼湊命令
- 若命令不可執行,則跳過
- 參數解析(若是需要自行解析,由對應的命令進行解析,否則統一解析參數),執行命令Run,執行結束后退出
總體來說,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開發相關內容,望大家感興趣的支持一下,在此特別感謝。
總結
以上是生活随笔為你收集整理的了解Go编译处理(一)—— go tool的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 手机上将png转pdf_如何在Windo
- 下一篇: 人工智能——微粒群优化算法