這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
標準庫— 操作源碼之收集go包資訊:go/build
在golang標準庫中,有那麼一類包,它們用於處理go項目目錄結構、源碼、文法、基本操作等。一般程式中可能用不到這些包,但在go工具鏈源碼中用到了,之所以學習這些標準庫,是為了更好的看go工具鏈的源碼。首先我們來看收集go包資訊的庫:go/build
一、build包概述
該包文檔中首先介紹了Go Path。如果對該部分還不清楚,可以看下文檔的說明;或者官方其他文檔;或者看 Go項目的目錄結構。
如果你看過go源碼,應該見到過類似這樣的包注釋:+build ignore。這是編譯約束條件(Build Constraints),可以理解為條件編譯。關於這部分的更多內容,稍後詳細介紹。
二、類型和函數
1、ToolDir變數
var ToolDir = filepath.Join(runtime.GOROOT(), "pkg/tool/"+runtime.GOOS+"_"+runtime.GOARCH)
該變數的值是go工具鏈的路徑。6g/6l之類的工具,就在這個路徑下
2、ArchChar函數
獲得架構的字元表示。在之前的文章中介紹過。比如:x86 32bit用8表示;amd64用6表示等。該函數通過傳入goarch,獲得對應的架構字元。如:build.ArchChar(runtime.GOARCH)
3、IsLocalImport函數
判斷是否為“本地匯入“,類似”.”, “..”, “./foo”或者”../foo”。正式項目,一般不建議用本地匯入,使用本地匯入很多人會說找不到包。
4、Context類型
該類型為構建(build)指定上下文環境。比如:當前作業系統、架構、Go根目錄、GOPATH等
該類型除了提供變數成員,還提供了函數成員,如果函數成員是nil,則會使用其他庫提供的函數。
5、Package類型
描述Go包
三、主要類型的方法(包括執行個體化)
1、Context的執行個體化和方法
var Default Context = defaultContext()
這是預設實現,go build工具使用的就是這個預設實現。Default會使用GOARCH、GOOS、GOROOT和GOPATH環境變數,如果沒設定,則使用安裝runtime中的值。
1)Import方法
func (ctxt *Context) Import(path string, srcDir string, mode ImportMode) (*Package, error)
匯入一個包,返回Package類型指標。path參數跟在代碼中import path的path一樣。srcDir是源碼所在路徑;而ImportMode類型,build包中提供了兩種:FindOnly和AllowBinary。AllowBinary可以在包源碼不存在的時候,編譯好的包對象檔案直接被引用。
之前,在論壇(關於go的程式碼群組織)中討論過這樣一個問題:go build 的時候,如果依賴的包源碼不存在,編譯不成功,有一個解決辦法是通過go tool 6g這種方式編譯。現在,在你知道了AllowBinary參數之後,應該可以通過修改go工具源碼來解決這個問題。在src/cmd/go目錄中的pkg.go中225有這樣的代碼:
?
www.usr.cc
| 123 |
// TODO:After Go 1, decide when topass build.AllowBinary here.// See issue3268 for mistakes to avoid.bp, err := buildContext.Import(path,srcDir, 0) |
通過查看Import的源碼,可以知道包安裝的細節,比如安裝到哪裡。
當目錄不包含源碼,如果出錯,則返回NoGoError錯誤。
另外,build包提供了Import方法的一個簡便方法,即:Import函數,預設調用Default的Import方法
2)ImportDir方法
內部實現:return ctxt.Import(“.”, dir, mode)
3)SrcDirs方法
列出GOROOT和GOPATH中的源碼目錄。比如,我沒有設定GOPATH,執行結果如下:
fmt.Println(build.Default.SrcDirs()) // [ c:\Go\src\pkg]
2、Package的執行個體化和方法
Context的Import和ImportDir都會返回Package執行個體(*Package),當然,也可以直接執行個體化。
Package提供了一個方法,判斷一個Package是否是命令,也就是是否是main包
func (p *Package) IsCommand() bool
3、關於Context和Package的欄位
由於這兩種類型欄位很多,包文檔中每個欄位都有注釋,在此不一一解釋。
四、構建約束(build constraints)
或者叫條件編譯(編譯條件)
1、使用說明
在go源碼中(src/pkg或src/cmd)搜尋+build,發現有不少檔案的開頭有這樣的注釋
+build xxx
構建約束是一行以+build開始的注釋。在+build之後列出了一些條件,在這個條件成立的時,該檔案應該包含在包中(也就是應該被編譯進包檔案),約束可以出現在任何源檔案中,也就是不限於go源檔案。不過,這些條件必須在檔案最頂部(正是代碼的前面,也就是說,+build之前可以有其他注釋),在+build注釋之後,應該有一個空行(這是為了和package doc區分開)。
文法規則:
1)只允許是字母數字或_
2)多個條件之間,空格表示OR;逗號表示AND;歎號(!)表示NOT
3)一個檔案可以有多個+build,它們之間的關係是AND。如:
// +build linux darwin
// +build 386
等價於
// +build (linux OR darwin) AND 386
4)預定義了一些條件:
runtime.GOOS、runtime.GOARCH、compiler(gc或gccgo)、cgo、context.BuildTags中的其他單詞
5)如果一個檔案名稱(不含尾碼),以 *_GOOS, *_GOARCH, 或 *_GOOS_GOARCH結尾,它們隱式包含了 構建約束
6)當不想編譯某個檔案時,可以加上// +build ignore。這裡的ignore可以是其他單詞,只是ignore更能讓人知道什麼意思
更多詳細資料,可以查看go/build/build.go檔案中shouldBuild和match方法。
2、應用執行個體
除了*_GOOS這種預定義的應用,我們看一個實際的應用。
比如,項目中需要在測試環境輸出Debug資訊,一般通過一個變數(或常量)來控制是測試環境還是生產環境,比如:if DEBUG {},這樣在生產環境每次也會進行這樣的判斷。在golang-nuts郵件清單中有人問過這樣的問題,貌似沒有討論出更好的方法(想要跟C中條件編譯一樣的效果)。下面我們採用Build constraints來實現。
1)檔案清單:main.go logger_debug.go logger_product.go
2)在main.go中簡單的調用Debug()方法。
3)在logger_product.go中的Debug()是空實現,但是在檔案開始加上// + build !debug
4)在logger_debug.go中的Debug()是需要輸出的調試資訊,同時在檔案開始加上// + build debug
這樣,在測試環境編譯的時傳遞-tags參數:go build/install -tags “debug” logger。生產環境:go build/install logger就行了。
對於生產環境,不傳遞-tags時,為什麼會編譯logger_product.go呢?因為在go/build/build.go中的match方法中有這麼一句:
1 if strings.HasPrefix(name, "!") { // negation
2 return len(name) > 1 && !ctxt.match(name[1:])
3 }
也就是說,只要有!(不能只是!),tag不在BuildTags中時,總是會編譯。
完整源碼在github上