Go語言程式起步
在進行Go語言具體內容學習之前,讓我們以實際的程式一起來從整體上解下Go語言程式,包括: Go語言程式結構,Go語言包規則,Go程式編譯、執行、測試以及程式文件使用。
1. Go語言程式結構
$GOPATH/ ├── bin │├── dlv │├── gocode │├── main ├── pkg │└── darwin_amd64 │├── github.com ││└── peterh │└── zhongxiao.yzx │└── add.a └── src ├── github.com │├── peterh ││└── liner └── zhongxiao.yzx ├── add |├── example_test.go │└── add.go └── test |├── benchmark_test.go |└── ut_test.go └── main.go
如上所示的Go程式結構以$GOPATH為根目錄,對應的工作區目錄有三個子目錄。
- pkg目錄
主要用於儲存編譯後的目標檔案, 例如第三方外掛github.com/peterh和定義zhongxiao.yzx/add.a - bin目錄
用於儲存編譯後的可執行程式,例如第三方外掛dlv(vscode debugger外掛),gocode以及自定義可執行程式main - src目錄
用於儲存原始碼。每個包被儲存在以$GOPATH/src為根目錄,包匯入路徑為相對路徑的子目錄中,例如第三方外掛包github.com/peterh相對應的路徑目錄;自定義開發目錄zhongxiao.yzx下有2個子包add, test分別放置自定義包和測試程式。
main.go
// main.go package main import ( "fmt" "zhongxiao.yzx/add" ) func main() { a, b := 3, 4 result := add.BigAdd(3, 4) fmt.Printf("%d + %d = %d", a, b, result) }
add.go
package add func BigAdd(a int, b int) int { return a + b }
example_test.go
package add import ( "fmt" ) func ExampleBigAdd() { fmt.Println(BigAdd(3, 4)) fmt.Println(BigAdd(4, 4)) // Output: // 7 // 8 }
ut_test.go
package test import ( "testing" "zhongxiao.yzx/add" ) func Test_BigAdd(t *testing.T) { a, b := 3, 3 result := add.BigAdd(a, b) if 6 != result { t.Errorf("%d + %d = %d %d expected", a, b, result, 6) } }
benchmark_test.go
package test import ( "fmt" "testing" "zhongxiao.yzx/add" ) func Benchmark_BigAdd(b *testing.B) { fmt.Println("Benchmark_Test") for i := 0; i < b.N; i++ { add.BigAdd(i, i) } }
一個Go語言編寫的程式對應一個或多個以.go為檔案字尾名的原始檔中(如上示例程式碼)。
- 每個原始檔以包的宣告語句開始,說明該原始檔是屬於哪個包。
- 包宣告語句之後是import語句匯入依賴的其它包。
- 然後是包一級的型別、變數、常量、函式的宣告語句(包一級的各種型別的宣告語句的順序無關緊要,但是函式內部的名字則必須先宣告之後才能使用)。
- 在包一級宣告語句宣告的名字可在整個包對應的每個原始檔中訪問,而不是僅僅在其宣告語句所在的原始檔中訪問。
- 區域性宣告的名字就只能在函式內部很小的範圍被訪問
2. Go工程編譯,執行, 測試
構造以上的Go工程,進入自定義包目錄zhongxiao.yzx,通過如下的命令和過程我們可以編譯,執行及測試Go程式。
- go build
構建指定的包和它依賴的包,然後丟棄除了最後的可執行檔案之外所有的中間編譯結果。如果包的名字是main, go build 將呼叫聯結器在當前目錄建立一個可執行程式;如果包是一個庫, 則忽略輸出結果,以匯入路徑的最後一段作為目標產物名字。Go語言的工具鏈將原始碼及其依賴編譯成目標檔案(預設為靜態連結庫和bin,可以編譯時通過 -buildmode編譯選項指定) - go install
與go build命令很相似,但它會儲存每個包的編譯成果,而不是將它們都丟棄。被編譯的包會被儲存到 $ GOPATH/pkg目錄下,目錄路徑和 src目錄路徑對應; 可執行程式被儲存到 $ GOPATH/bin目錄。go install 命令和 go build 命令都不會重新編譯沒有發生變化的包,這可以使後續構建更快捷。
為了方便編譯依賴的包, go build -i 命令將安裝每個目標所依賴的包。 - go run
在zhongxiao.yzx目錄下執行 go run main.go 即可執行測試程式。 -
go test
go test命令會遍歷所有的*_test.go檔案中符合命名規則的函式,然後生成一個臨時的main包用於呼叫相應的測試函式,然後構建並執行、報告測試結果,最後清理測試中生成的臨時檔案。
採用如上所示工程作為go test示例,包目錄內所有以*_test.go為字尾名的原始檔並不是go build構建包的一部分,它們是go test測試的一部分。
在*_test.go檔案中,有三種類型的函式:
-
測試函式
一個測試函式是以 Test 為函式名字首的函式,用於測試程式的一些邏輯行為是否正確;go test命令會呼叫這些測試函式並報告測試結果是PASS或FAIL。
ut_test.go
package test import ( "testing" "zhongxiao.yzx/add" ) func Test_BigAdd(t *testing.T) { a, b := 3, 3 result := add.BigAdd(a, b) if 6 != result { t.Errorf("%d + %d = %d %d expected", a, b, result, 6) } }
在zhongxiao.yzx/test目錄下執行如下的命令可以執行測試函式
go test -coverprofile=c.out //執行test並生成覆蓋率profile go tool cover -html=c.out // c.out profile生成html```
-
基準測試函式
基準測試函式是以 Benchmark 為函式名字首的函式,它們用於衡量一些函式的效能;go test命令會多次執行基準函式以計算一個平均的執行時間。
benchmark_test.go
package test import ( "fmt" "testing" "zhongxiao.yzx/add" ) func Benchmark_BigAdd(b *testing.B) { fmt.Println("Benchmark_Test") for i := 0; i < b.N; i++ { add.BigAdd(i, i) } }
在zhongxiao.yzx/test目錄下執行
go test -bench=. // 執行基準測試
預設情況下不執行任何基準測試。需要通過 -bench 命令列標誌引數手工指定要執行的基準測試函式。該引數是一個正則表示式,用於匹配要執行的基準測試函式的名字,預設值是空的。其中“.”模式將可以匹配所有基準測試函式。
-
示例函式
示例函式是以 Example 為函式名字首的函式,提供一個由編譯器保證正確性的示例文件。func ExampleBigAdd() { fmt.Println(BigAdd(3, 4)) fmt.Println(BigAdd(4, 4)) // Output: // 7 // 8 }
-
根據示例函式的字尾名部分,godoc的web文件會將一個示例函式關聯到某個具體函式或包本身,因此ExampleBigAdd示例函式將是BigAdd函式文件的一部分,Example示例函式將是包文件的一部分。
-
示例文件的第二個用處是在 go test 執行測試的時候也執行示例函式測試。如果示例函式內含有類似上面例子中的 // Output: 格式的註釋,那麼測試工具會執行這個示例函式,然後檢測這個示例函式的標準輸出和註釋是否匹配。
-
-
3. Go語言包組織及使用
從上面的示例程式可見,包是組織Go語言工程的基本單元。Go語言中的包和其他語言的庫或模組的概念類似,目的都是為了支援模組化、封裝、單獨編譯和程式碼重用。一個包的原始碼儲存在一個或多個以.go為檔案字尾名的原始檔中,通常一個包所在目錄路徑的字尾是包的匯入路徑;例如包gopl.io/ch1/helloworld對應的目錄路徑是 $ GOPATH/src/ gopl.io/ch1/helloworld 。
- 每個包都對應一個獨立的名字空間
- 包可以通過控制哪些名字是外部可見的來隱藏內部實現資訊。在Go語言中,一個簡單的規則是:如果一個名字是大寫字母開頭的,那麼該名字是匯出的
- 在每個原始檔的包宣告前僅跟著的註釋是包註釋。通常,包註釋的第一句應該先是包的功能概要說明。一個包通常只有一個原始檔有包註釋(譯註:如果有多個包註釋,目前的文件工具會根據原始檔名的先後順序將它們連結為一個包註釋)。如果包註釋很大,通常會放到一個獨立的doc.go檔案中。
- 在Go語言程式中,每個包都是有一個全域性唯一的匯入路徑,一個匯入路徑代表一個目錄中的一個或多個Go原始檔,按照慣例,一個包的名字和包的匯入路徑的最後一個欄位相同,例如gopl.io/ch2/tempconv包的名字一般是tempconv。即使兩個包的匯入路徑不同,它們依然可能有一個相同的包名。例如,math/rand包和crypto/rand包的包名都是rand。
- 包可以用一個特殊的init初始化函式來執行包的初始化工作。每個檔案都可以包含多個init初始化函式。
func init() { /* ... */ }
這樣的init初始化函式除了不能被呼叫或引用外,其他行為和普通函式類似。在每個檔案中的init初始化函式,在程式開始執行時按照它們宣告的順序被自動呼叫。每個包在解決依賴的前提下,以匯入宣告的順序初始化,每個包只會被初始化一次。
- 匯入宣告
import ( "crypto/rand" mrand "math/rand"// alternative name mrand avoids conflict import _ "image/png" // 匿名匯入 register PNG decoder )
如果我們想同時匯入兩個有著相同名字的包,例如math/rand包和crypto/rand包,那麼匯入宣告必須至少為一個同名包指定一個新的包名以避免衝突。匯入包的重新命名隻影響當前的原始檔。其它的原始檔如果匯入了相同的包,可以用匯入包原本預設的名字或重新命名為另一個完全不同的名字。
如果只是匯入一個包而並不使用匯入的包將會導致一個編譯錯誤。有時候我們只是想利
用匯入包計算包級變數的初始化表示式和執行匯入包的init初始化函式, 我們可以用下劃線 _ 來重新命名匯入的包。這個被稱為包的匿名匯入。它通常是用來實現一個編譯時機制,然後通過在main主程式入口選擇性地匯入附加的包。
import ( "database/sql" _ "github.com/lib/pq" // enable support for Postgres _ "github.com/go-sql-driver/mysql" // enable support for MySQL ) db, err = sql.Open("postgres", dbname) // OK db, err = sql.Open("mysql", dbname) // OK db, err = sql.Open("sqlite3", dbname) // returns error: unknown driver "sqlite3"
-
特殊包及其匯入路徑
- 包對應一個可執行程式,也就是main包本身的匯入路徑是無關緊要的。名字為main的包是給go build構建命令一個資訊,這個包編譯完之後必須呼叫聯結器生成一個可執行程式。
- 包所在的目錄中可能有一些檔名是以 test.go 為字尾的Go原始檔(前面必須有其它的字元,因為以
test
字首的原始檔是被忽略的),並且這些原始檔宣告的包名也是以_test為字尾名的。所有以_test為字尾包名的測試外部擴充套件包都由go test命令獨立編譯。 - 一些依賴版本號的管理工具會在匯入路徑後追加版本號資訊,例如”gopkg.in/yaml.v2”。這種情況下包的名字並不包含版本號字尾,而是yaml。
- 內部包
Go語言的構建工具對包含 internal 名字的路徑段的包匯入路徑做了特殊處理。這種包叫internal包,一個internal包只能被和internal目錄有同一個父目錄的包所匯入。例如,net/http/internal/chunked內部包只能被net/http/httputil或net/http包匯入,但是不能被net/url包匯入。
4. godoc文件的生成
在Go語言中,Go為我們提供了快速生成文件以及檢視文件的工具,讓我們可以很容易的編寫檢視文件。
Go提供了兩種檢視文件的方式,如下工程為例
$Workspace/dev/golang/src/ └── zhongxiao.yzx ├── add |├── example_test.go │└── add.go └── test ├── benchmark_test.go └── ut_test.go
-
一種是使用go doc命令在終端檢視。
zhongxiao.yxz@MacBook-Pro:~/Workspace/dev/golang/src/zhongxiao.yzx$go doc zhongxiao.yzx/add
package add // import "zhongxiao.yzx/add" func BigAdd(a int, b int) int
或者通過godoc命令檢視
zhongxiao.yzx@MacBook-Pro:~/Workspace/dev/golang/src/zhongxiao.yzx$godoc cmd/zhongxiao.yzx/add
PACKAGE DOCUMENTATION package add import "." FUNCTIONS func BigAdd(a int, b int) int
-
第二種方式,是使用瀏覽器檢視的方式,通過godoc命令可以在本機啟動一個web服務,
zhongxiao.yzx@MacBook-Pro:~/Workspace/dev/golang/src/zhongxiao.yzx$godoc -http=:8080
godoc -http引數指定Web服務監聽的IP和Port,執行後,我們就可以開啟瀏覽器,輸入 http://localhost:8080/pkg/ 進行訪問了。
如下圖所以,你會發現開啟的頁面,和GoLang的官方網站一樣,但是文件中增加了本地的包,這些包是基於本地GOROOT和GOPATH這兩個路徑下的所有包生成的文件。

示例程式中zhongxiao.yzx/add下有如下2個檔案
add.go
package add func BigAdd(a int, b int) int { return a + b }
example_test.go
func ExampleBigAdd() { fmt.Println(BigAdd(3, 4)) fmt.Println(BigAdd(4, 4)) // Output: // 7 // 8 }
godoc工具根據example_test.go示例程式,將一個示例函式關聯到某個具體函式或包本身,因此ExampleBigAdd示例函式將是BigAdd函式文件的一部分,Example示例函式將是包文件的一部分。(example_test.go必須以_test.go作為字尾,否則go工具會將該檔案作為普通的檔案對待,詳見go test測試部分內容)

[1]: The Go Programming Language : https://golang.org/