這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
最近看了一篇關於go產品開發最佳實務的文章,go-in-procution。作者總結了他們在用go開發過程中的很多實際經驗,我們很多其實也用到了,鑒於此,這裡就簡單的寫寫讀後感,後續我也爭取能將這篇文章翻譯出來。後續我用soundcloud來指代原作者。
開發環境
在soundcloud,每個人使用一個獨立的GOPATH,並且在GOPATH直接按照go規定的代碼路徑方式clone代碼。
$ mkdir -p $GOPATH/src/github.com/soundcloud$ cd $GOPATH/src/github.com/soundcloud$ git clone git@github.com:soundcloud/roshi
對於go來說,通常的工程管理應該是如下的目錄結構:
proj/ src/ modulea/ a.go moudleb/ b.go app/ main.go pkg/ bin/
然後我們在GOPATH裡面將proj的路徑設定上去,這樣就可以進行編譯運行了。這本來沒啥,但是如果我們要將其代碼提交到github,並允許另外的開發人員使用,我們就不能將整個proj的東西提交上面,如果提交了,就很蛋疼了。外面的開發人員可能這麼引用:
import "github.com/yourname/proj/src/modulea"
但是我們自己在代碼裡面就可以直接:
import "github.com/yourname/proj/modulea"
如果外面的開發人員需要按照去掉src的引用方式,只能把GOPATH設定到proj目錄,如果import的多了,會讓人崩潰的。
我曾今也被這事情給折騰了好久,終於再看了vitess的代碼之後,發現了上面這種方式,覺得非常不錯。
工程目錄結構
如果一個項目中檔案數量不是很多,直接放在main包裡面就行了,不需要在拆分成多個包了,譬如:
github.com/soundcloud/simple/ README.md Makefile main.go main_test.go support.go support_test.go
如果真的有公用的類庫,在拆分成單獨的包處理。
有時候,一個工程可能會包括多個二進位應用。譬如,一個job可能需要一個server,一個worker或者一個janitor,在這種情況下,建立多個子目錄作為不同的main包,分別放置不同的二進位應用。同時使用另外的子目錄實現公用的函數。
github.com/soundcloud/complex/README.mdMakefilecomplex-server/ main.go main_test.go handlers.go handlers_test.gocomplex-worker/ main.go main_test.go process.go process_test.goshared/ foo.go foo_test.go bar.go bar_test.go
這點我的做法稍微有一點不一樣,主要是參考vitess,我喜歡建立一個總的cmd目錄,然後再在裡面設定不同的子目錄,這樣外面就不需要猜測這個目錄是庫還是應用。
代碼風格
代碼風格這沒啥好說的,直接使用gofmt解決,通常我們也約定gofmt的時候不帶任何其他參數。
最好將你的編輯器配置成儲存代碼的時候自動進行gofmt處理。
Google最近發布了go的代碼規範,soundcloud做了一些改進:
如果一個函數有多個參數,並且單行長度很長,需要拆分,最好不用java的方式:
// Don't do this.func process(dst io.Writer, readTimeout, writeTimeout time.Duration, allowInvalid bool, max int, src <-chan util.Job) { // ...}
而是使用:
func process( dst io.Writer, readTimeout, writeTimeout time.Duration, allowInvalid bool, max int, src <-chan util.Job,) { // ...}
類似的,當構造一個對象的時候,最好在初始化的時候就傳入相關參數,而不是在後面設定:
f := foo.New(foo.Config{ Site: "zombo.com", Out: os.Stdout, Dest: conference.KeyPair{ Key: "gophercon", Value: 2014, },})// Don't do this.f := &Foo{} // or, even worse: new(Foo)f.Site = "zombo.com"f.Out = os.Stdoutf.Dest.Key = "gophercon"f.Dest.Value = 2014
如果一些變數是後續通過其他動作才能擷取的,我覺得就可以在後續設定了。
配置
soundcloud使用go的flag包來進行配置參數的傳遞,而不是通過設定檔或者環境變數。
flag的配置是在main函數裡面定義的,而不是在全域範圍內。
func main() { var ( payload = flag.String("payload", "abc", "payload data") delay = flag.Duration("delay", 1*time.Second, "write delay") ) flag.Parse() // ...}
關於使用flag作為配置參數的傳遞,我持保留意見。如果一個應用需要特別多的配置參數,使用flag比較讓人蛋疼了。這時候,使用設定檔反而比較好,我個人傾向於使用json作為配置,原因在這裡。
日誌
soundcloud使用的是go的log日誌,他們也說明了他們的log並不需要太多的其他功能,譬如log分級等。對於log,我參考python的log寫了一個,在這裡。該log支援log層級,支援自訂loghandler。
soundcloud還提到了一個telemetry的概念,我真沒好的辦法進行翻譯,據我的瞭解可能就是程式的資訊收集,包括回應時間,QPS,記憶體運行錯誤等。
通常telemetry有兩種方式,推和拉。
推模式就是主動的將資訊發送給特定的外部系統,而拉模式則是將其寫入到某一個地方,允許外部系統來擷取該資料。
這兩種方式都有不同的定位,如果需要及時,直觀的看到資料,推模式是一個很好的選擇,但是該模式可能會佔用過多的資源,尤其是在資料量大的情況下面,會很消耗CPU和頻寬。
soundcloud貌似採用的是拉模型。
關於這點我是深表贊同,我們有一個服務,需要將其資訊發送到一個統計平台共後續的資訊,開始的時候,我們使用推模式,每產生一條記錄,我們直接通過http推給後面的統計平台,終於,隨著壓力的增大,整個統計平台被我們發掛了,拒絕服務。最終,我們採用了將資料寫到本地,然後通過另一個程式拉取再發送的方式解決。
測試
soundcloud使用go的testing包進行測試,然後也使用flag的方式來進行整合測試,如下:
// +build integrationvar fooAddr = flag.String(...)func TestToo(t *testing.T) { f, err := foo.Connect(*fooAddr) // ...}
因為go test也支援類似go build那種flag傳遞,它會預設合成一個main package,然後在裡面進行flag parse處理。
這種方式我現在沒有採用,我都是在測試案例裡面直接寫死了一個全域的配置,主要是為了方便的在根目錄進行 go test ./...處理。不過使用flag的方式我覺得靈活性很大,後面如果有可能會考慮。
go的testing包提供的功能並不強,譬如沒有提供assert_equal這類東西,但是我們可以通過reflect.DeepEqual來解決。
依賴管理
這塊其實也是我非常想解決的。現在我們的代碼就是很暴力的用go get來解決依賴問題,這個其實很有風險的,如果某一個依賴包更改了介面,那麼我們go get的時候可能會出問題了。
soundcloud使用了一種vendor的方式進行依賴管理。其實很簡單,就是把依賴的東西全部拷貝到自己的工程下面,當做自己的代碼來使用。不過這個就需要週期性維護依賴包的更新了。
如果引入的是一個可執行包,在自己的工程目錄下面建立一個_vendor檔案夾(這樣go的相關tool例如go test就會忽略該檔案夾的東西)。把_vendor作為單獨的GOPATH,例如,拷貝github.com/user/dep到_vendor/src/github.com/user/dep下面。然後將_vendor加入自己的GOPATH中,如下:
GO ?= goGOPATH := $(CURDIR)/_vendor:$(GOPATH)all: buildbuild: $(GO) build
如果引入的是一個庫,那麼將其放入vendor目錄中,將vendor作為package的首碼,例如拷貝github.com/user/dep到vendor/user/dep,並更改所有的相關import語句。
因為我們並不需要頻繁的對這些引入的工程進行go get更新處理,所以大多數時候這樣做都很值。
我開始的時候也採用的是類似的做法,只不過我不叫vendor,而叫做3rd,後來為了方便還是決定改成直接go get,雖然知道這樣風險比較大。沒準後續使用godep可能是一個不錯的解決辦法。
構建和部署
soundcloud在開發過程中直接使用go build來構建系統,然後使用一個Makefile來處理正式的構建。
因為soundcloud主要部署很多無狀態的服務,類似Heroku提供了很簡單的一種方式:
$ git push bazooka master$ bazooka scale -r <new> -n 4 ...$ # validate$ bazooka scale -r <old> -n 0 ...
這方面,我們直接使用一個簡單的Makefile來構建系統,如下:
all: build build: go install ${SRC}clean: go clean -i ${SRC}test: go test ${SRC}
應用程式的發布採用最原始的scp到目標機器在重啟的方式,不過現在正在測試使用salt來發布應用。而對於應用程式的啟動,停止這些,我們則使用supervisor來進行管理。
總結
總的來說,這篇文章很詳細的講解了用go進行產品開發過程中的很多經驗,希望對大家有協助。