Go 源碼閱讀系列是個人源碼閱讀筆記。由於本人的電腦上 Go 的版本是1.13.4,因此就選擇了該版本做爲學習的版本。爲此我在 Github 上 Fork 了 Go 的源碼,並建立了 study1.13.4 分支,來記錄對於源碼的我的理解或者說中文註釋也行。每當閱讀完一個包後都會進行一下小結,就像這篇是對flag包的總結整理。固然在整理的過程當中發現 Go夜讀系列視頻,也讓我受益頗多。git
簡介
flag 包是 Go 裏用於解析命令行參數的包。爲何選擇它做爲第一個閱讀的包,由於它的代碼量少。其核心代碼只有一個 1000 不到的 flag.go 文件。github
文件結構
flag 包的文件結構很簡單,就一層。一個文件夾裏放了 5 個文件,其文件及其做用以下:編程
-
flag.go數組
flag 的核心包,實現了命令行參數解析的全部功能bash
-
export_test.go微信
測試的實用工具,定義了全部測試須要的基礎變量和函數app
-
flag_test.go編程語言
flag 的測試文件,包含了 17 個測試單元函數
-
example_test.go工具
flag 的樣例文件,介紹了 flag 包的三種經常使用的用法樣例
-
example_value_test.go
flag 的樣例文件,介紹了一個更復雜的樣例
運行測試
我先介紹一下 Go 的運行環境。
# 經過 brew install go 安裝,源碼位置爲 $GOROOT/src GOROOT=/usr/local/opt/go/libexec # 閱讀的源碼經過 go get -v -d github.com/haojunyu/go 下載,源碼位置爲 $GOPATH/src/github.com GOPATH=$HOME/go
單獨測試 flag 包踩過的坑:
- 沒法針對單個文件進行測試,須要針對包。
這裏重點說一下 export_test.go 文件,它是flag包的一部分package flag
,可是它確實專門爲測試而存在的,說白了也就一個ResetForTesting
方法,用來清除全部命令參數狀態而且直接設置Usage函數。該方法會在測試用例中被頻繁使用。因此單獨運行如下命令會報錯"flag_test.go:30:2: undefined: ResetForTesting"
# 測試當前目錄(報錯) go test -v . # 測試包 go test -v flag
go test -v flag
測試的源碼是$GOROOT/src
下的(以我當前的測試環境)
指定 flag 包後,實際運行的源碼是 $GOROOT
下的,這個應該和個人安裝方式有關係。
總結
接口轉換能實現相似 C++ 中模板的功能
flag 包中定義了一個結構體類型叫 Flag
,它用來存放一個命令參數,其定義以下。
// A Flag represents the state of a flag. // 結構體Flag表示一個參數的全部信息,包括名稱,幫助信息,實際值和默認值 type Flag struct { Name string // name as it appears on command line名稱 Usage string // help message幫助信息 Value Value // value as set實現了取值/賦值方法的接口 DefValue string // default value (as text); for usage message默認值 }
其中命令參數的值是一個 Value
接口類型,其定義以下:
// Set is called once, in command line order, for each flag present. // The flag package may call the String method with a zero-valued receiver, // such as a nil pointer. // 接口Value是個接口,在結構體Flag中用來存儲每一個參數的動態值(參數類型格式各樣) type Value interface { String() string // 取值方法 Set(string) error // 賦值方法 }
爲何這麼作?由於這樣作可以實現相似模板的功能。任何一個類型 T
只要實現了 Value
接口裏的 String
和 Set
方法,那麼該類型 T
的變量 v
就能夠轉換成 Value
接口類型,並使用 String
來取值,使用 Set
來賦值。這樣就能完美的解決不一樣類型使用相同的代碼操做目的,和 C++ 中的模板有相同的功效。
函數 vs 方法
函數和方法都是一組一塊兒執行一個任務的語句,兩者的區別在於調用者不一樣,函數的調用者是包 package,而方法的調用者是接受者 receiver。在 flag 的源碼中,有太多的函數裏面只有一行,就是用包裏的變量 CommandLine
調用同名方法。
// Parsed reports whether f.Parse has been called. // Parsed方法: 命令行參數是否已經解析 func (f *FlagSet) Parsed() bool { return f.parsed } // Parsed reports whether the command-line flags have been parsed. func Parsed() bool { return CommandLine.Parsed() }
new
vs make
new
和 make
是 Go 語言中兩種內存分配原語。兩者所作的事情和針對的類型都不同。 new
和其餘編程語言中的關鍵字功能相似,都是向系統申請一段內存空間來存儲對應類型的數據,但又有些區別,區別在於它會將該片空間置零。也就是說 new(T)
會根據類型 T
在堆上 申請一片置零的內存空間,並返回指針 *T
。 make
只針對切片,映射和信道三種數據類型 T
的構建,並返回類型爲 T
的一個已經初始化(而非零)的值。緣由是這三種數據類型都是引用數據類型,在使用前必須初始化。就像切片是一個具備三項內容的描述符,包含一個指向數組的指針,長度和容量。經過 make
建立對應類型的變量過程是先分配一段空間,接着根據對應的描述符來建立對應的類型變量。關於 make
的細節能夠看 draveness 寫的 Go語言設計與實現。
// Bool defines a bool flag with specified name, default value, and usage string. // The return value is the address of a bool variable that stores the value of the flag. func (f *FlagSet) Bool(name string, value bool, usage string) *bool { p := new(bool) f.BoolVar(p, name, value, usage) return p } // sortFlags returns the flags as a slice in lexicographical sorted order. // sortFlags函數:按字典順序排序命令參數,並返回Flag的切片 func sortFlags(flags map[string]*Flag) []*Flag { result := make([]*Flag, len(flags)) i := 0 for _, f := range flags { result[i] = f i++ } sort.Slice(result, func(i, j int) bool { return result[i].Name < result[j].Name }) return result }
指針賦值給接口變量
Go 中的接口有兩層含義,第一層是一組方法(不是函數)的簽名,它須要接受者(具體類型 T
或具體類型指針 *T
)來實現細節;另外一層是一個類型,而該類型能接受全部現實該接受的接受者。深刻理解接口的概念能夠細讀 Go語言設計與實現之接口。在 flag 包中的 StringVar
方法中newStringValue(value, p)
返回的是 *stringValue
類型,而該類型(接受者)實現了 Value
接口( String
和 Set
方法),此時該類型就能夠賦值給 Value
接口變量。
// StringVar defines a string flag with specified name, default value, and usage string. // The argument p points to a string variable in which to store the value of the flag. // StringVar方法:將命令行參數的默認值value賦值給變量*p,並生成結構Flag並置於接受者中f.formal func (f *FlagSet) StringVar(p *string, name string, value string, usage string) { f.Var(newStringValue(value, p), name, usage) // newStringValue返回值是*stringValue類型,之因此能賦值給Value接口是由於newStringValue實現Value接口時定義的接受者爲*stringValue }
flag文件夾中有flag_test
包
flag 文件夾下有 flag_test
包,是由於該文件夾下包含了核心代碼 flag.go 和測試代碼 *_test.go 。這兩部分代碼並無經過文件夾來區分。因此該 flag_test
包存在的意義是將測試代碼與核心代碼區分出來。而該包被引用時只會使用到核心代碼。
// example_test.go package flag_test
做用域
關於做用域 Golang變量做用域 和 GO語言聖經中關於做用域 都有了詳細的介紹,前者更通俗易懂些,後者更專業些。在 flag 包的 TestUsage
測試樣例中,由於 func(){called=true}
是在函數 TestUsage
中定義函數,而且直接做爲形參傳遞給 ResetForTesting
函數,因此該函數是和局部變量 called
是同級的,固然在該函數中給該變量賦值也是合理的。
// called變量的做用域 func TestUsage(t *testing.T) { called := false // 變量called的做用域 ResetForTesting(func() { called = true }) if CommandLine.Parse([]string{"-x"}) == nil { t.Error("parse did not fail for unknown flag") } else { t.Error("hahahh") } if !called { t.Error("did not call Usage for unknown flag") } }
後續深刻TODO
- [ ] go test 測試原理
- [ ] 接口轉換原理
- [ ] 反射
參考文獻
- Go 夜讀之 flag 包視頻
- 實效 Go 編程以內存分配
- Go 語言設計與實現之 make 和 new
- 菜鳥教程之 Go 語言變量做用域
- Go 語言聖經中關於做用域
- Go 語言中值 receiver 和指針 receiver 的對比
- Go CodeReviewComments
- Golang 變量做用域
- Go 語言聖經中關於做用域
- Go 語言設計與實現之接口
若是該文章對您產生了幫助,或者您對技術文章感興趣,能夠關注微信公衆號: 技術茶話會, 可以第一時間收到相關的技術文章,謝謝!
本篇文章由一文多發平臺ArtiPub自動發佈