golang初探與命令源碼分析

  • 2019 年 10 月 18 日
  • 筆記

前段時間有群友在群里問一個go語言的問題:

就是有一個main.go的main函數里調用了另一個demo.go里的hello()函數。其中main.go和hello.go同屬於main包。但是在main.go的目錄下執行go run main.go卻報hello函數沒有定義的錯:

image

程式碼結構如下:

**gopath ---- src**              **----gohello**                    **----hello.go**                        ** ----main.go**

main.go如下:

package main    import "fmt"    func main() {     fmt.Println("my name is main")     hello()  }  

hello.go如下:

package main    import "fmt"    func hello() {   fmt.Println("my name is hello")  }

當時我看了以為是他GOPATH配置的有問題,然後自己也按照這樣試了一下,報同樣的錯,在網上查了,也有兩篇文章是關於這個錯的,也提供了解決方法,即用go run main.go hello.go,試了確實是可以的。

image

雖然是個很簡單的問題,但是也涉及到了go語言底層對於命令行參數的解析。那就來分析一下語言底層的實現吧,看一下底層做了什麼,為什麼報這個錯?

分析:

以下使用到的Go SDK版本為1.8.3

該版本中go支援的基本命令有以下16個:

build       compile packages and dependencies  clean       remove object files  doc         show documentation for package or symbol  env         print Go environment information  bug         start a bug report  fix         run go tool fix on packages  fmt         run gofmt on package sources  generate    generate Go files by processing source  get         download and install packages and dependencies  install     compile and install packages and dependencies  list        list packages  run         compile and run Go program  test        test packages  tool        run specified go tool  version     print Go version  vet         run go tool vet on packages

在Go SDK的src/cmd/go包下有main.go文件中,Command類型的commands數組對該16個命令提供了支援:

image

我們首先知道go語言的初始化流程如下:

image

在執行main.go中的主函數main之前,對import進來的包按順序初始化,最後初始化main.go中的類型和變數,當初始化到commands數組時,由於cmdRun定義在於main.go同包下的run.go中,那麼就先去初始化run.go中的變數和init方法,如下程式碼,先把cmdRun初始化為Command類型,然後執行init()函數。

var cmdRun = &Command{   UsageLine: "run [build flags] [-exec xprog] gofiles... [arguments...]",   Short:     "compile and run Go program",   Long: `  Run compiles and runs the main package comprising the named Go source files.  A Go source file is defined to be a file ending in a literal ".go" suffix.    By default, 'go run' runs the compiled binary directly: 'a.out arguments...'.  If the -exec flag is given, 'go run' invokes the binary using xprog:   'xprog a.out arguments...'.  If the -exec flag is not given, GOOS or GOARCH is different from the system  default, and a program named go_$GOOS_$GOARCH_exec can be found  on the current search path, 'go run' invokes the binary using that program,  for example 'go_nacl_386_exec a.out arguments...'. This allows execution of  cross-compiled programs when a simulator or other execution method is  available.    For more about build flags, see 'go help build'.    See also: go build.   `,  }    func init() {   cmdRun.Run = runRun // break init loop     addBuildFlags(cmdRun)   cmdRun.Flag.Var((*stringsFlag)(&execCmd), "exec", "")  }

init()中,將runRun(其實類型是一個方法,用於處理run後的參數)賦值給cmdRu.run,addBuildFlags(cmdRun)主要是給run後面增加命令行參數(如:-x是列印其執行過程中用到的所有命令,同時執行它們)。其他15個命令和cmdRun類似,各有各的run實現。

下來主要看main.go中main的這塊程式碼:

for _, cmd := range commands {     if cmd.Name() == args[0] && cmd.Runnable() {       cmd.Flag.Usage = func() { cmd.Usage() }       if cmd.CustomFlags {         args = args[1:]       } else {         cmd.Flag.Parse(args[1:])         args = cmd.Flag.Args()       }       cmd.Run(cmd, args)       exit()       return     }   }

這塊程式碼遍歷commands數組,當遍歷到cmdRun時,cmd.Name()其實就是拿到cmdRun.UsageLine的第一個單詞run

image

就會進入if分支,由於cmd.CustomFlags沒有初始化故為false,走else分支,然後開始解析args命令行參數,args[1:]即取run後的所有參數。然後去執行cmd.Run(cmd, args),對於cmdRun來說,這裡執行的就是run.go中init()的第一行賦值cmdRun.run(上面說了,這是一個函數,不同命令實現方式不同),即去執行run.go中的runRun函數,該函數主要是將命令行參數當文件去處理,如果是_test為後綴的,即測試文件,直接報錯。如果是目錄也直接報錯(而且go run後面只能包含一個含main函數的go文件)。注意到有這麼一行:

p := goFilesPackage(files)

goFilesPackage(files)除了校驗文件類型和後綴,還會入棧,載入,出棧等操作,由於啟動的時候沒有傳遞hello.go,所以系統載入main.go時找不到hello函數,導致報錯。



本公眾號免費提供csdn下載服務,海量IT學習資源,如果你準備入IT坑,勵志成為優秀的程式猿,那麼這些資源很適合你,包括但不限於java、go、python、springcloud、elk、嵌入式 、大數據、面試資料、前端 等資源。同時我們組建了一個技術交流群,裡面有很多大佬,會不定時分享技術文章,如果你想來一起學習提高,可以公眾號後台回復【2】,免費邀請加技術交流群互相學習提高,會不定期分享編程IT相關資源。


掃碼關注,精彩內容第一時間推給你

image