產(chǎn)品環(huán)境中 Go 語(yǔ)言的最佳實(shí)踐
在SoundCloud,我們?yōu)榭蛻?hù)構(gòu)建了產(chǎn)品的API?;蛘哒f(shuō),我們主要的網(wǎng)站、手機(jī)客戶(hù)端和手機(jī)應(yīng)用是該API的***批客戶(hù)。該API背后是一個(gè)領(lǐng)域性的服務(wù):SoundCloud基本上以面向服務(wù)體系結(jié)構(gòu)的形式運(yùn)作。
我們也是通曉多種語(yǔ)言的組織,因?yàn)槲覀兪褂昧撕芏嗾Z(yǔ)言。并且這些服務(wù)(和基礎(chǔ)設(shè)施支持)的許多部分是使用Golang開(kāi)發(fā)的。事實(shí)上,我們都是早期Golang的使用者:目前,我們已在產(chǎn)品中使用Golang有兩年半的時(shí)間。相關(guān)項(xiàng)目包括:
-
Bazooka,我們內(nèi)部服務(wù)平臺(tái);產(chǎn)品思想非常類(lèi)似于Keroku或Flynn。
-
我們外圍的傳輸層使用通用的nginx, HAProxy等等,但是它們要和Golang服務(wù)協(xié)作。
-
我們的音頻存儲(chǔ)在AWS S3上,但是上傳、轉(zhuǎn)碼和生成鏈接等需要Golang服務(wù)協(xié)調(diào)處理。
-
搜索采用了Elasticsearch, 探測(cè)使用復(fù)雜的機(jī)器學(xué)習(xí)模型,但是它們都與由Golang開(kāi)發(fā)的基礎(chǔ)設(shè)施相集成。
-
Prometheus,一個(gè)早期階段的遙測(cè)系統(tǒng)純粹是有Golang開(kāi)發(fā)。
-
當(dāng)前,流處理采用Cassandra,但是我們正打算(幾乎)完全使用Golang代替。
-
我們也正在試驗(yàn)用Golnag開(kāi)發(fā)的HTTP流媒體直播服務(wù)。
-
許多其他面向產(chǎn)品的小服務(wù)。
這些項(xiàng)目大概有六個(gè)團(tuán)隊(duì)開(kāi)發(fā),包括十多人的SoundCloud勤雜工,他們中的大部分會(huì)全職使用Golang。畢竟在這個(gè)時(shí)候,這些項(xiàng)目和這樣混雜的工程師中,我們已經(jīng)逐漸形成了在產(chǎn)品中使用Golang的***實(shí)踐方法。我們的這些教訓(xùn)將對(duì)其他開(kāi)始大舉投資Golang的組織提供幫助。
開(kāi)發(fā)環(huán)境
在我們的筆記本上,我們已經(jīng)設(shè)定了單一、全局的GOPATH。就個(gè)人而言,我喜歡使用$HOME,但是許多其他人使用$HOME下的一個(gè)子目錄。我們克隆倉(cāng)庫(kù)進(jìn)入GOPATH的相對(duì)路徑,然后就可直接工作。即,
- $ mkdir -p $GOPATH/src/github.com/soundcloud
- $ cd $GOPATH/src/github.com/soundcloud
- $ git clone git@github.com:soundcloud/roshi
我們中的許多人在早期一直和約定俗成的事情做斗爭(zhēng),以保持我們自己特有的代碼組織方法。事實(shí)上,它根本不值得如此麻煩。
對(duì)于編輯器,許多用戶(hù)使用Vim以及各種插件。(我使用的vim-go就不錯(cuò)。)還有許多人,包括我自己也是,結(jié)合GoSublime使用Sublime Text。也有少數(shù)人使用Emacs,但沒(méi)有人用IDE。我不確定這是不是個(gè)***的實(shí)踐,但標(biāo)出來(lái)挺有趣的。
庫(kù)結(jié)構(gòu)
我們的***實(shí)踐是確保任何事情簡(jiǎn)單。許多服務(wù)源碼半打包在main包中。
- github.com/soundcloud/simple/
- README.md
- Makefile
- main.go
- main_test.go
- support.go
- support_test.go
比如我們的搜索調(diào)度器,兩年后仍然是這樣。在確定需要前不要?jiǎng)?chuàng)建新結(jié)構(gòu)。
也許在某些時(shí)候你需要?jiǎng)?chuàng)建一個(gè)新的支持包。在你的main庫(kù)中使用子目錄,并使用完整的限定名導(dǎo)入。如果該包只有一個(gè)文件或一個(gè)結(jié)構(gòu),那么它肯定不需要分拆出來(lái)。
有時(shí)一個(gè)倉(cāng)庫(kù)中需要包含多個(gè)二進(jìn)制文件;比如這個(gè)任務(wù)需要一個(gè)服務(wù),一個(gè)工作進(jìn)程,或一個(gè)監(jiān)控。在這種情況下,將每個(gè)二進(jìn)制文件放在特定main包的單獨(dú)的子目錄中,并使用其他的子目錄(或包)來(lái)實(shí)現(xiàn)共享的功能。
- github.com/soundcloud/complex/
- README.md
- Makefile
- complex-server/
- main.go
- main_test.go
- handlers.go
- handlers_test.go
- complex-worker/
- main.go
- main_test.go
- process.go
- process_test.go
- shared/
- foo.go
- foo_test.go
- bar.go
- bar_test.go
請(qǐng)注意,不要引入asrc目錄。由于vendor子目錄異常(下面介紹更多內(nèi)容)不要在倉(cāng)庫(kù)中包含src目錄,或?qū)⑵涮砑拥紾OPATH。
格式及樣式
通常來(lái)說(shuō),首先配置你的編輯器保存代碼交給go fmt(或goimports),使用默認(rèn)參數(shù)。這意味使用tab縮進(jìn),用空格對(duì)齊。格式不正確的代碼將不能提交。
過(guò)去的風(fēng)格指南非常廣泛,但谷歌最近發(fā)布了他們的 代碼審查意見(jiàn) 文檔,這幾乎就是我們應(yīng)遵守的公約。因此,我們使用它。
實(shí)際上我們把它推進(jìn)了一點(diǎn):
-
避免命名返回參數(shù),除非他們能明確和顯著地提高透明度。
-
避免用 make 和 new,除非他們是必要的(new(int),或 make(Chan int)),或者我們能提前知道要分配的東西的尺寸( make(map[int]string,n),或 make([]int,0,256))。
-
使用 struct{} 作為標(biāo)記值,而不是布爾或接口{}。例如,集合是 map[string]struct{};信道是 chan struct{}。它明確標(biāo)明了信息的明確缺乏。
打斷長(zhǎng)行的參數(shù)也很好。那更象是Java的風(fēng)格:
- // 不要這樣。
- func process(dst io.Writer, readTimeout,
- writeTimeout time.Duration, allowInvalid bool,
- max int, src <-chan util.Job) {
- // ...
- }
這樣會(huì)更好:
- func process(
- dst io.Writer,
- readTimeout, writeTimeout time.Duration,
- allowInvalid bool,
- max int,
- src <-chan util.Job,
- ) {
- // ...
- }
當(dāng)構(gòu)造對(duì)象時(shí)也同樣分為多行:
- f := foo.New(foo.Config{
- Site: "zombo.com",
- Out: os.Stdout,
- Dest: conference.KeyPair{
- Key: "gophercon",
- Value: 2014,
- },
- })
另外,當(dāng)分配新的對(duì)象時(shí),在初始化部分傳遞成員值(如上面)比下面這樣過(guò)后設(shè)置要好。
- // 不要這樣。
- f := &Foo{} // or, even worse: new(Foo)
- f.Site = "zombo.com"
- f.Out = os.Stdout
- f.Dest.Key = "gophercon"
- f.Dest.Value = 2014
配置
我們嘗試了通過(guò)多種方式向Go程序傳遞配置:解析配置文件,用 os.Getenv 直接從環(huán)境中提取配置,各種增值flag解析包。***,最合乎經(jīng)濟(jì)原則的就是普通的package flag,它的嚴(yán)格類(lèi)型和簡(jiǎn)單語(yǔ)義對(duì)我們所需的一切都絕對(duì)夠用而且夠好。
我們主要部署12-Factor 的應(yīng)用,12-Factor 應(yīng)用程序通過(guò)環(huán)境傳遞配置。但即使這樣,我們也使用一個(gè)啟動(dòng)腳本來(lái)把環(huán)境變量轉(zhuǎn)換為flags。Flags作為程序及其運(yùn)行環(huán)境之間的一個(gè)明確和全文檔化的表面區(qū)域。他們對(duì)于了解和操作程序來(lái)說(shuō)是非常寶貴的。
一個(gè)關(guān)于flags的不錯(cuò)的習(xí)慣是把他們定義到你的main函數(shù)中。這樣就能防止你在代碼中隨意的將他們作為全局變量使用,這使你嚴(yán)格的遵守依賴(lài)注入從而方便測(cè)試。
- func main() {
- var (
- payload = flag.String("payload", "abc", "payload data")
- delay = flag.Duration("delay", 1*time.Second, "write delay")
- )
- flag.Parse()
- // ...
- }
日志和遙測(cè)
我們嘗試過(guò)幾個(gè)日志框架,他們提供像日志級(jí)別,調(diào)試,路由輸出,自定義格式化等等功能。最終我們選定package log。因?yàn)槲覀冎挥涗浛刹僮餍畔ⅰ?這意味著需要人工處理的 serious, panic級(jí)別的錯(cuò)誤,或者結(jié)構(gòu)化數(shù)據(jù)會(huì)被其他機(jī)器消耗。 舉個(gè)例子,搜索轉(zhuǎn)發(fā)器發(fā)送每一個(gè)它使用上下文信息處理的請(qǐng)求,因此我們的分析工作流可以看到新西蘭的人們經(jīng)常搜索 Lorde, 或者隨便什么。
我們考慮到遙測(cè),在一個(gè)運(yùn)行過(guò)程中釋放出的任何其他量:請(qǐng)求響應(yīng)時(shí)間,QPS,運(yùn)行錯(cuò)誤,隊(duì)列深度等等。并且遙測(cè)基本上包括兩種模式:push和pull。
-
push意味著釋放指標(biāo)到一個(gè)已知的系統(tǒng)。例如Graphite, Statsd, and AirBrake
-
pull意味著在一些已知的位置暴露指標(biāo),并允許已知的系統(tǒng)去擦除它們。例如,expvar和Prometheus(或許還有其他的)
當(dāng)然兩種方式都有自己的存在性。當(dāng)你開(kāi)始使用時(shí),push是直觀和簡(jiǎn)單的。但是推送指標(biāo)的增長(zhǎng)卻有悖常理:你得到的越大,成本越高。我們過(guò)去發(fā)現(xiàn)在特定規(guī)模大小的基礎(chǔ)設(shè)施上,pull是該尺度下的唯一模型。那也有許多值能反映一個(gè)運(yùn)行的系統(tǒng)。所以,***的實(shí)踐是:expvar或者類(lèi)似風(fēng)格的。
測(cè)試和驗(yàn)證
在一年的過(guò)程中我們嘗試了許多的測(cè)試庫(kù)和框架,但是很快放棄了他們中的大部分,今天我們所有的測(cè)試通過(guò)數(shù)據(jù)驅(qū)動(dòng)(表驅(qū)動(dòng))測(cè)試,用普通的包測(cè)試。我們沒(méi)有強(qiáng)烈或者明確的抱怨測(cè)試/檢查包,除此之外,他們根本沒(méi)有提供巨大的價(jià)值。有一件事情是有幫助的:reflect.DeepEqual讓你更簡(jiǎn)單的對(duì)任意值進(jìn)行比較(例如expected對(duì)got)。
包測(cè)試是面向單元測(cè)試的,對(duì)于集成測(cè)試,就會(huì)有點(diǎn)麻煩。運(yùn)行的外部服務(wù)依賴(lài)于你的集成環(huán)境,但是我們找到了一個(gè)好的方式集成他們。寫(xiě)一個(gè)integration_test.go,給它一個(gè)integration的構(gòu)建標(biāo)簽。定義(全局)標(biāo)志,比如服務(wù)地址和連接字符串,用他們?cè)谀愕臏y(cè)試中。
- // +build integration
- var fooAddr = flag.String(...)
- func TestToo(t *testing.T) {
- f, err := foo.Connect(*fooAddr)
- // ...
- }
go test 和 go build 一樣建立標(biāo)簽,所以你可以調(diào)用 go test -tags=integration 。它也綜合了 flag.Parse 包的 main,所以任何被聲明和可見(jiàn)的 flags 將被處理和提供給你的測(cè)試。
通過(guò)驗(yàn)證,我的意思是靜態(tài)代碼驗(yàn)證。幸運(yùn)的是,Go 有一些很好的工具。我發(fā)現(xiàn)當(dāng)考慮使用哪種工具時(shí)考慮編寫(xiě)代碼的階段很有用。
當(dāng)做這種事時(shí) | 使用這個(gè) |
---|---|
保存 | go fmt(或 goimports) |
構(gòu)建 | go vet,golint, 或者 go test |
部署 | go test -tags=integration |
插曲
到目前為止,還沒(méi)東西過(guò)于瘋狂。當(dāng)做調(diào)查編撰這個(gè)列表的時(shí)候,讓我注意的只是如何。。。。。。結(jié)論如何的無(wú)趣。讓人沉悶。我想強(qiáng)調(diào)這些非常輕量,純標(biāo)準(zhǔn)庫(kù)的約定能真正推廣到大群體的開(kāi)發(fā)人員和多元化的項(xiàng)目生態(tài)系統(tǒng)。你絕對(duì)不會(huì)僅僅因?yàn)槟愕拇a庫(kù)已經(jīng)超過(guò)一定的規(guī)模,或者只是因?yàn)樗?em>可能 增長(zhǎng)超過(guò)一定行數(shù), 而需要你自己的查錯(cuò)框架,或者測(cè)試庫(kù)。你真的是不會(huì)需要它的。標(biāo)準(zhǔn)的語(yǔ)法和用法在代碼大規(guī)模時(shí)仍然功能優(yōu)雅。
依賴(lài)管理
依賴(lài)管理! 呃! ᕕ( ᐛ )ᕗ
依賴(lài)管理的狀態(tài)在 Go 生態(tài)系統(tǒng)中是一個(gè)熱門(mén)的爭(zhēng)論點(diǎn),我們還沒(méi)有想到***的解決方案。但是,我們選用了一個(gè)似乎不錯(cuò)的妥協(xié)方案。
你的項(xiàng)目有多么重要? | 你的依賴(lài)管理方案是… |
---|---|
嗯… | go get -d,然后祈禱! |
很好. | VENDORING |
(值得提出的是,我們有令人震驚數(shù)量的長(zhǎng)期產(chǎn)品服務(wù),依然依賴(lài)于***個(gè)選項(xiàng).然而,因?yàn)槲覀円话銢](méi)有使用太多第三方代碼,以及主要問(wèn)題通常在編譯階段就被檢測(cè)到,我們僥幸規(guī)避了這個(gè)問(wèn)題.)
Vendoring意味著拷貝依賴(lài)到項(xiàng)目代碼庫(kù),然后在編譯的時(shí)候使用它們.依賴(lài)于你下載的內(nèi)容,這里有兩個(gè)vendoring的***實(shí)踐.
下載 | Vendor目錄名 | 過(guò)程 |
---|---|---|
二進(jìn)制 | _vendor | 加GOPATH前綴編譯 |
庫(kù) | vendor | 重寫(xiě)import語(yǔ)句 |
如果下載二進(jìn)制,就在代碼庫(kù)的根目錄創(chuàng)建一個(gè)_vendor子目錄.(帶上下劃線,這樣,go工具就會(huì)在處理時(shí)忽略它,例如go test ./...)對(duì)待它就像對(duì)待GOPATH一樣; 例如,拷貝這個(gè)依賴(lài)github.com/user/dep 到 _vendor/src/github.com/user/dep. 然后,編寫(xiě)一個(gè)所謂的神圣的編譯過(guò)程,它將_vendor加入到可能存在的GOPATH之中. (記住: GOPATH 實(shí)際是一個(gè)路徑的列表,當(dāng)go工具處理import時(shí),會(huì)按順序搜索這個(gè)列表.)例如,你可能擁有一個(gè)頂層的Makefile文件,如下所示:
- GO ?= go
- GOPATH := $(CURDIR)/_vendor:$(GOPATH)
- all: build
- build:
- $(GO) build
如果你正在下載某個(gè)類(lèi)庫(kù)在你的根存儲(chǔ)庫(kù)上創(chuàng)建一個(gè)vendor子目錄。處理這件事就像在包目錄上加一個(gè)前綴。舉例來(lái)說(shuō),拷貝來(lái)自于github.com/user/dep的項(xiàng)目放到vendor/user/dep。在這之后,重寫(xiě)你所有的引入(import),及其相互關(guān)系。此時(shí)是很痛苦的,當(dāng)剩下的內(nèi)容需要go get兼容的時(shí)候,看起來(lái)最有效的方式是確保事實(shí)上可重新構(gòu)建(actually-reproducible build)。值得注意的是,我們?cè)趯?shí)踐中很少去下載類(lèi)庫(kù),因此這個(gè)辦法雖然麻煩卻很有效。
如何在實(shí)際中拷貝一個(gè)依賴(lài)關(guān)系到你自己的存儲(chǔ)庫(kù)是另外一個(gè)熱門(mén)的話題。最簡(jiǎn)單的方法是從一個(gè)克隆(clone)中手動(dòng)復(fù)制文件,如果你不關(guān)心上游部門(mén)的推送,這可能是***的答案。有些人使用git子模塊,但我們發(fā)現(xiàn)它們非常違反直覺(jué)并難以管理(對(duì)許多 人來(lái)說(shuō)也是這樣,這是有記錄的)。我們對(duì)于git子目錄(的管理)已經(jīng)很成功,他工作起來(lái)就像是子模塊。還有大量的工具是用來(lái)自動(dòng)處理這項(xiàng)工作的?,F(xiàn)在,它看起來(lái)就像godep發(fā)展非常積極,而且還很值得研究。
構(gòu)建與部署
構(gòu)建與部署有其技巧性,因此它與你的操作環(huán)境耦合緊密。我要描述下我們的場(chǎng)景,因?yàn)槲艺J(rèn)為它是個(gè)好模型,但它可能無(wú)法直接應(yīng)用到你的組織機(jī)構(gòu)中。
就構(gòu)建而言,我們通常直接使用 go build 來(lái)開(kāi)發(fā),以及一個(gè) Makefile 用于剪裁官方構(gòu)建。這主要是因?yàn)槲覀兪煜ざ喾N語(yǔ)言,并且我們的工具使用需要做到最小功能合集(最小公倍數(shù))。并且,我們的構(gòu)建系統(tǒng)始于一個(gè)空環(huán)境,也需要自備編譯器( Makefile 文件很難看?。?。
對(duì)部署而言,對(duì)我們***的吸引是無(wú)狀態(tài)之于有狀態(tài)。
模式 | 樣例 | 模型 | 部署名稱(chēng) | 部署形式 |
---|---|---|---|---|
無(wú)狀態(tài) | Request router | 12-Factor | Scaling | Containers |
有狀態(tài) | Redis | None, really | Provisioning | Containers? |
我們主要部署無(wú)狀態(tài)的服務(wù),方式類(lèi)似于 Heroku。
- $ git push bazooka master
- $ bazooka scale -r <new> -n 4 ...
- $ # validate
- $ bazooka scale -r <old> -n 0 ...