Go 源碼閱讀之 flag 包

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 包踩過的坑:

  1. 沒法針對單個文件進行測試,須要針對包。

這裏重點說一下 export_test.go 文件,它是flag包的一部分package flag,可是它確實專門爲測試而存在的,說白了也就一個ResetForTesting方法,用來清除全部命令參數狀態而且直接設置Usage函數。該方法會在測試用例中被頻繁使用。因此單獨運行如下命令會報錯"flag_test.go:30:2: undefined: ResetForTesting"

# 測試當前目錄(報錯)
go test -v .
# 測試包
go test -v flag
  1. 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 接口裏的 StringSet 方法,那麼該類型 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

newmake 是 Go 語言中兩種內存分配原語。兩者所作的事情和針對的類型都不同。 new 和其餘編程語言中的關鍵字功能相似,都是向系統申請一段內存空間來存儲對應類型的數據,但又有些區別,區別在於它會將該片空間置零。也就是說 new(T) 會根據類型 T 在堆上 申請一片置零的內存空間,並返回指針 *Tmake 只針對切片,映射和信道三種數據類型 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 接口( StringSet 方法),此時該類型就能夠賦值給 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 測試原理
  • [ ] 接口轉換原理
  • [ ] 反射

參考文獻

  1. Go 夜讀之 flag 包視頻
  2. 實效 Go 編程以內存分配
  3. Go 語言設計與實現之 make 和 new
  4. 菜鳥教程之 Go 語言變量做用域
  5. Go 語言聖經中關於做用域
  6. Go 語言中值 receiver 和指針 receiver 的對比
  7. Go CodeReviewComments
  8. Golang 變量做用域
  9. Go 語言聖經中關於做用域
  10. Go 語言設計與實現之接口

若是該文章對您產生了幫助,或者您對技術文章感興趣,能夠關注微信公衆號: 技術茶話會, 可以第一時間收到相關的技術文章,謝謝! 技術茶話會


本篇文章由一文多發平臺ArtiPub自動發佈
相關文章
相關標籤/搜索