前言
博主在查找一些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 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的處理邏輯大致如下:
- 沒有參數時,直接打印mainUsage,退出
- 對於get、help命令,如果沒開啓mod,則mod get替換爲get
- 第一個參數爲help時,根據後續命令處理
- 如果沒有命令,直接打印mainUsage
- 後續僅一個命令且爲documentation,打印所有命令的documentation。
- 後續有多個命令時,確認後續命名是否是前一個子命令。若不是,則打印出錯處;若一直是,則打印對應的說明。
- 檢查GOPATH,若GOPATH與GOROOT設置在同一文件夾,則警告
- 檢查GOROOT
- 獲取環境變量,對於錯誤設置的環境變量值,會主動修正至正確值,減少錯誤帶來的影響
- 循環查找對應的命令
- 目標命令存在子命令
- 若傳入命令不足,則打印說明並退出;
- 若後續命令爲help,則打印對應的說明
- 正常,則依次拼湊命令
- 若命令不可執行,則跳過
- 參數解析(若是需要自行解析,由對應的命令進行解析,否則統一解析參數),執行命令Run,執行結束後退出
- 目標命令存在子命令
- 若命令不存在,打印
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開發相關內容,望大家感興趣的支持一下,在此特別感謝。