這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
第1期的“寫Go代碼時遇到的那些問題”一經發布後得到了很多Gopher的支援和讚賞,這也是我繼續寫下去的動力!不過這裡依然要強調的是這一系列文章反映的是筆者在實踐中對代碼編寫的認知以及代碼的演化過程。這裡的代碼也許只是“中間階段”,並不是什麼最優的結果,我記錄的只是對問題、對代碼的一個思考曆程。不過,十分歡迎交流與批評指正。
一、dep的日常操作
雖然dep在國內使用依然有init失敗率較高(因為一些qiang外的第三方package)的坎兒,但我和主流Gopher社區和項目一樣,義無反顧地選擇在程式碼程式庫中使用dep。本周dep剛剛發布了0.4.1版本,與之前版本最大的不同在於dep發布了其官網以及相對完整的文檔(以替代原先在github項目首頁上的簡陋的、格式較low的FAQ),這也是dep繼續走向成熟的一個標誌。不過關於dep何時能merge到go tools鏈當中,目前還是未知數。不過dep會在相當長的一段時期繼續以獨立工具的形式存在,直到merge到Go tools中並被廣泛接受。
包依賴管理工具在日常開發中並不需要太多的存在感,我們需要的這類工具特徵是功能強大但介面“小”,對開發人員體驗好,不太需要太關心其運行原理,dep基本符合。dep日常操作最主要的三個命令:dep init、dep ensure和dep status。在《初窺dep》一文中,我曾重點說過dep init原理,這裡就不重點說了,我們用一個例子來說說使用dep的日常workflow。
1、dep init empty project
我們可以對一個empty project或一個初具架構雛形的project進行init,這裡init一個empty project,作為後續的樣本基礎:
➜ $GOPATH/src/depdemo $dep init -vGetting direct dependencies...Checked 1 directories for packages.Found 0 direct dependencies.Root project is "depdemo" 0 transitively valid internal packages 0 external packages imported from 0 projects(0) ✓ select (root) ✓ found solution with 0 packages from 0 projectsSolver wall times by segment: select-root: 68.406µs other: 9.806µs TOTAL: 78.212µs➜ $GOPATH/src/depdemo $lsGopkg.lock Gopkg.toml vendor/➜ $GOPATH/src/depdemo $dep statusPROJECT CONSTRAINT VERSION REVISION LATEST PKGS USED
dep init有三個輸出:Gopkg.lock、Gopkg.toml和vendor目錄,其中Gopkg.toml(包含example,但注釋掉了)和vendor都是空的,Gopkg.lock中僅包含了一些給gps使用的metadata:
➜ $GOPATH/src/depdemo git:(a337d5b) $cat Gopkg.lock# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.[solve-meta] analyzer-name = "dep" analyzer-version = 1 inputs-digest = "ab4fef131ee828e96ba67d31a7d690bd5f2f42040c6766b1b12fe856f87e0ff7" solver-name = "gps-cdcl" solver-version = 1
2、常規操作迴圈:for { 填代碼 -> dep ensure }
接下來的常規操作就是我們要為project添加代碼了。我們先來為工程添加一個main.go檔案,源碼如下:
// main.gopackage mainimport "fmt"func main() { fmt.Println("depdemo")}
這份代碼的依賴只是std庫的fmt,並沒有使用第三方的依賴,因此當我們通過dep status查看目前狀態、使用ensure去做同步時,發現dep並沒有什麼要做的:
➜ $GOPATH/src/depdemo $dep statusPROJECT CONSTRAINT VERSION REVISION LATEST PKGS USED➜ $GOPATH/src/depdemo $dep ensure -vGopkg.lock was already in sync with imports and Gopkg.toml
好吧。我們再來為main.go添點“有用”的內容:一段讀取toml設定檔的代碼。
//data.tomlid = "12345678abcdefgh"name = "tonybai"city = "shenyang"// main.gopackage mainimport ( "fmt" "log" "github.com/BurntSushi/toml")type Person struct { ID string Name string City string}func main() { p := Person{} if _, err := toml.DecodeFile("./data.toml", &p); err != nil { log.Fatal(err) } fmt.Println(p)}
之後,再來執行dep status:
➜ $GOPATH/src/depdemo $dep statusLock inputs-digest mismatch due to the following packages missing from the lock:PROJECT MISSING PACKAGESgithub.com/BurntSushi/toml [github.com/BurntSushi/toml]This happens when a new import is added. Run `dep ensure` to install the missing packages.input-digest mismatch
我們看到dep status檢測到項目出現”不同步”的情況(代碼中引用的toml包在Gopkg.lock中沒有),並建議使用dep ensure命令去做一次sync。
我們來ensure一下(ensure的輸入輸出見):
$GOPATH/src/depdemo git:(master) $dep ensure -vRoot project is "depdemo" 1 transitively valid internal packages 1 external packages imported from 1 projects(0) ✓ select (root)(1) ? attempt github.com/BurntSushi/toml with 1 pkgs; 7 versions to try(1) try github.com/BurntSushi/toml@v0.3.0(1) ✓ select github.com/BurntSushi/toml@v0.3.0 w/1 pkgs ✓ found solution with 1 packages from 1 projectsSolver wall times by segment: b-source-exists: 15.821158205s... ... b-deduce-proj-root: 5.453µs TOTAL: 16.176846089s(1/1) Wrote github.com/BurntSushi/toml@v0.3.0
我們來看看項目中的檔案都發生了哪些變化:
$git statusOn branch masterChanges not staged for commit: (use "git add <file>..." to update what will be committed) (use "git checkout -- <file>..." to discard changes in working directory) modified: Gopkg.lockUntracked files: (use "git add <file>..." to include in what will be committed) vendor/
可以看到Gopkg.lock檔案和vendor目錄下發生了變化:
$git diffdiff --git a/Gopkg.lock b/Gopkg.lockindex bef2d00..c5ae854 100644--- a/Gopkg.lock+++ b/Gopkg.lock@@ -1,9 +1,15 @@ # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.+[[projects]]+ name = "github.com/BurntSushi/toml"+ packages = ["."]+ revision = "b26d9c308763d68093482582cea63d69be07a0f0"+ version = "v0.3.0"+ [solve-meta] analyzer-name = "dep" analyzer-version = 1- inputs-digest = "ab4fef131ee828e96ba67d31a7d690bd5f2f42040c6766b1b12fe856f87e0ff7"+ inputs-digest = "25c744eb70aefb94032db749509fd34b2fb6e7c6041e8b8c405f7e97d10bdb8d" solver-name = "gps-cdcl" solver-version = 1$tree -L 2 vendorvendor└── github.com └── BurntSushi
可以看到Gopkg.lock中增加了toml包的依賴條目(版本v0.3.0),input-digest這個中繼資料欄位的值也發生了變更;並且vendor目錄下多了toml包的源碼,至此項目又到達了“同步”狀態。
3、添加約束
大多數情況下,我們到這裡就算完成了dep work flow的一次cycle,但如果你需要為第三方包的版本加上一些約束條件,那麼dep ensure -add就會派上用場,比如說:我們要使用toml包的v0.2.x版本,而不是v0.3.0版本,我們需要為github.com/BurntSushi/toml添加一條約束:
$dep ensure -v -add github.com/BurntSushi/toml@v0.2.0Fetching sources...(1/1) github.com/BurntSushi/toml@v0.2.0Root project is "depdemo" 1 transitively valid internal packages 1 external packages imported from 1 projects(0) ✓ select (root)(1) ? attempt github.com/BurntSushi/toml with 1 pkgs; at least 1 versions to try(1) try github.com/BurntSushi/toml@v0.3.0(2) ✗ github.com/BurntSushi/toml@v0.3.0 not allowed by constraint ^0.2.0:(2) ^0.2.0 from (root)(1) try github.com/BurntSushi/toml@v0.2.0(1) ✓ select github.com/BurntSushi/toml@v0.2.0 w/1 pkgs ✓ found solution with 1 packages from 1 projectsSolver wall times by segment:... ... TOTAL: 599.252392ms(1/1) Wrote github.com/BurntSushi/toml@v0.2.0
add約束後,Gopkg.toml中增加了一條記錄:
// Gopkg.toml[[constraint]] name = "github.com/BurntSushi/toml" version = "0.2.0"
Gopkg.lock中的toml條目的版本回退為v0.2.0:
diff --git a/Gopkg.lock b/Gopkg.lockindex c5ae854..a557251 100644--- a/Gopkg.lock+++ b/Gopkg.lock@@ -4,12 +4,12 @@ [[projects]] name = "github.com/BurntSushi/toml" packages = ["."]- revision = "b26d9c308763d68093482582cea63d69be07a0f0"- version = "v0.3.0"+ revision = "bbd5bb678321a0d6e58f1099321dfa73391c1b6f"+ version = "v0.2.0" [solve-meta] analyzer-name = "dep" analyzer-version = 1- inputs-digest = "25c744eb70aefb94032db749509fd34b2fb6e7c6041e8b8c405f7e97d10bdb8d"+ inputs-digest = "9fd144de0cc448be93418c927b5ce2a70e03ec7f260fa7e0867f970ff121c7d7" solver-name = "gps-cdcl" solver-version = 1$dep statusPROJECT CONSTRAINT VERSION REVISION LATEST PKGS USEDgithub.com/BurntSushi/toml ^0.2.0 v0.2.0 bbd5bb6 v0.2.0 1
vendor目錄下的toml包源碼也回退到v0.2.0的源碼。關於約束規則的構成文法,可以參考dep文檔。
4、revendor/update vendor
使用vendor機制後,由於第三方依賴包修正bug或引入你需要的功能,revendor第三方依賴包版本或者叫update vendor會成為一個周期性的工作。比如:toml包做了一些bugfix,並發布了v0.2.1版本。在我的depdemo中,為了一併fix掉這些bug,我需要重新vendor toml包。之前我們加的constraint是滿足升級到v0.2.1版本的,因此我們不需要重新設定constraints,我們只需要單獨revendor toml即可,可以使用dep ensure -update 命令:
$dep ensure -v -update github.com/BurntSushi/tomlRoot project is "depdemo" 1 transitively valid internal packages 1 external packages imported from 1 projects(0) ✓ select (root)(1) ? attempt github.com/BurntSushi/toml with 1 pkgs; 7 versions to try(1) try github.com/BurntSushi/toml@v0.3.0(2) ✗ github.com/BurntSushi/toml@v0.3.0 not allowed by constraint ^0.2.0:(2) ^0.2.0 from (root)(1) try github.com/BurntSushi/toml@v0.2.0(1) ✓ select github.com/BurntSushi/toml@v0.2.0 w/1 pkgs ✓ found solution with 1 packages from 1 projectsSolver wall times by segment: b-list-versions: 1m18.267880815s .... ... TOTAL: 1m57.118656393s
由於真實的toml並沒有v0.2.1版本且沒有v0.2.x版本,因此我們的dep ensure -update並沒有真正擷取到資料。vendor和Gopkg.lock都沒有變化。
5、dep日常操作小結
下面這幅圖包含了上述三個dep日常操作,可以直觀地看出不同操作後,對項目帶來的改變:
“工欲善其事,必先利其器”,熟練的掌握dep的日常操作流程對提升開發效率大有裨益。
二、“逾時等待退出”架構的一種實現
很多時候,我們在程式中都要啟動多個goroutine協作完成應用的商務邏輯,比如:
func main() { go producer.Start() go consumer.Start() go watcher.Start() ... ...}
啟動容易停止難!當程式要退出時,最粗暴的方法就是不管三七二十一,main goroutine直接退出;優雅些的方式,也是*nix系統通常的作法是:通知一下各個Goroutine要退出了,然後等待一段時間後再真正退出。粗暴地直接退出的方式可能會導致業務資料的損壞、不完整或丟失。等待逾時的方式雖然不能完全避免“損失”,但是它給了各個goroutine一個“挽救資料”的機會,可以儘可能地減少損失的程度。
但這些goroutine形態很可能不同,有些是server,有些可能是client worker或其manager,因此似乎很難用一種統一的架構全面管理他們的啟動、運行和退出,於是我們縮窄“互動面”,我們只做“逾時等待退出”。我們定義一個interface:
type GracefullyShutdowner interface { Shutdown(waitTimeout time.Duration) error}
這樣,凡是實現了該interface的類型均可在程式退出時得到退出的通知,並有機會做退出前的最後清理工作。這裡還提供了一個類似http.HandlerFunc的類型ShutdownerFunc ,用於將普通function轉化為實現了GracefullyShutdowner interface的類型執行個體:
type ShutdownerFunc func(time.Duration) errorfunc (f ShutdownerFunc) Shutdown(waitTimeout time.Duration) error { return f(waitTimeout)}
1、並發退出
退出也至少有兩種類型,一種是並發退出,這種退出方式下各個goroutine的退出先後次序對資料處理無影響;另外一種則是順序退出,即各個goroutine之間的退出是必須按照一定次序進行的。我們先來說並發退出。上代碼!
// shutdown.gofunc ConcurrencyShutdown(waitTimeout time.Duration, shutdowners ...GracefullyShutdowner) error { c := make(chan struct{}) go func() { var wg sync.WaitGroup for _, g := range shutdowners { wg.Add(1) go func(shutdowner GracefullyShutdowner) { shutdowner.Shutdown(waitTimeout) wg.Done() }(g) } wg.Wait() c <- struct{}{} }() select { case <-c: return nil case <-time.After(waitTimeout): return errors.New("wait timeout") }}
我們將各個GracefullyShutdowner介面的實現以一個變長參數的形式傳入ConcurrencyShutdown函數。ConcurrencyShutdown函數實現也很簡單,通過:
- 為每個shutdowner啟動一個goroutine實現並發退出,並將timeout參數傳入shutdowner的Shutdown方法中;
- sync.WaitGroup在外層等待每個goroutine的退出;
- 通過select一個退出指示channel和time.After返回的timer channel來決定到底是正常退出還是逾時退出。
該函數的具體使用方法可以參考:shutdown_test.go。
//shutdown_test.gofunc shutdownMaker(processTm int) func(time.Duration) error { return func(time.Duration) error { time.Sleep(time.Second * time.Duration(processTm)) return nil }}func TestConcurrencyShutdown(t *testing.T) { f1 := shutdownMaker(2) f2 := shutdownMaker(6) err := ConcurrencyShutdown(time.Duration(10)*time.Second, ShutdownerFunc(f1), ShutdownerFunc(f2)) if err != nil { t.Errorf("want nil, actual: %s", err) return } err = ConcurrencyShutdown(time.Duration(4)*time.Second, ShutdownerFunc(f1), ShutdownerFunc(f2)) if err == nil { t.Error("want timeout, actual nil") return }}
2、串列退出
有了並發退出作為基礎,串列退出也很簡單了!
//shutdown.gofunc SequentialShutdown(waitTimeout time.Duration, shutdowners ...GracefullyShutdowner) error { start := time.Now() var left time.Duration for _, g := range shutdowners { elapsed := time.Since(start) left = waitTimeout - elapsed c := make(chan struct{}) go func(shutdowner GracefullyShutdowner) { shutdowner.Shutdown(left) c <- struct{}{} }(g) select { case <-c: //continue case <-time.After(left): return errors.New("wait timeout") } } return nil}
串列退出的一個問題是waitTimeout的確定,因為這個逾時時間是所有goroutine的退出時間之和。在上述代碼裡,我把每次的lefttime傳入下一個要執行的goroutine的Shutdown方法中,外部select也同樣使用這個left作為timeout的值。對照ConcurrencyShutdown,SequentialShutdown更簡單,這裡就不詳細說了。
3、小結
這是一個可用的、拋磚引玉式的實現,但還有很多改進空間,比如:可以考慮一下擷取每個shutdowner.Shutdown後的傳回值(error),留給大家自行考量吧。
三、Testcase的setUp和tearDown
Go語言內建testing架構,事實證明這是Go語言的一個巨大優勢之一,Gopher們也非常喜歡這個testing包。但Testing這個事情比較複雜,有些情境還需要我們自己動腦筋在標準testing架構下實現需要的功能,比如:當測試代碼需要訪問外部資料庫、Redis或串連遠端server時。遇到這種情況,很多人想到了Mock,沒錯。Mock技術在一定程度上可以解決這些問題,但如果使用mock技術,業務代碼就得為了test而去做一層抽象,提升了代碼理解的難度,在有些時候這還真不如直接存取真實的外部環境。
這裡先不討論這兩種方式的好壞優劣,這裡僅討論如果在testing中訪問真實環境我們該如何測試。在經典單元測試架構中,我們經常能看到setUp和tearDown兩個方法,它們分別用於在testcase執行之前初始化testcase的執行環境以及在testcase執行後清理執行環境,以保證每兩個testcase之間都是獨立的、互不干擾的。在真實環境下進行測試,我們也可以利用setUp和tearDown來為每個testcase初始化和清理case依賴的真實環境。
setUp和tearDown也是有層級的,有全域級、testsuite級以及testcase級。在Go中,在標準testing架構下,我們接觸到的是全域級和testcase層級。Go中對全域級的setUp和tearDown的支援還要追溯到Go 1.4,Go 1.4引入了TestMain方法,支援在諸多testcase執行之前為測試代碼添加自訂setUp,以及在testing執行之後進行tearDown操作,例如:
func TestMain(m *testing.M) { err := setup() if err != nil { fmt.Println(err) os.Exit(-1) } r := m.Run() teardown() os.Exit(r)}
但在testcase層級,Go testing包並沒有提供方法上的支援。在2017年的GopherCon大會上,Hashicorp的創始人Mitchell Hashimoto做了題為:“Advanced Testing in Go”的主題演講,這份資料裡提出了一種較為優雅的為testcase進行setUp和teawDown的方法:
//setup-teardown-demo/foo_test.gopackage foo_testimport ( "fmt" "testing")func setUp(t *testing.T, args ...interface{}) func() { fmt.Println("testcase setUp") // use t and args return func() { // use t // use args fmt.Println("testcase tearDown") }}func TestXXX(t *testing.T) { defer setUp(t)() fmt.Println("invoke testXXX")}
這個方案充分利用了函數這個first-class type以及閉包的作用,每個Testcase可以定製自己的setUp和tearDown,也可以使用通用的setUp和tearDown,執行的效果如下:
$go test -v .=== RUN TestXXXtestcase setUpinvoke testXXXtestcase tearDown--- PASS: TestXXX (0.00s)PASSok github.com/bigwhite/experiments/writing-go-code-issues/2nd-issue/setup-teardown-demo 0.010s
四、錯誤處理
本來想碼一些關於Go錯誤處理的文字,但發現自己在2015年就寫過一篇舊文《Go語言錯誤處理》,對Go錯誤處理的方方面面總結的很全面了。即便到今天也不過時,這當然也得益於Go1相容規範的存在。因此有興趣於此的朋友們,請移步到《Go語言錯誤處理》這篇文章吧。
註:本文所涉及的範例程式碼,請到這裡下載。
微博:@tonybai_cn
公眾號:iamtonybai
github.com: https://github.com/bigwhite
讚賞:
2018, bigwhite. 著作權.