讀生產環境下go語言最佳實務有感

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

最近看了一篇關於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做了一些改進:

  • 避免命名函數傳回值,除非能明確的表明含義。

  • 盡量少用make和new,除非真有必要,或者預Crowdsourced Security Testing道需要分配的大小。

  • 使用struct{}作為標記值,而不是bool或者interface{}。譬如set我們就用map[string]struct{}來實現,而不是map[string]bool。

如果一個函數有多個參數,並且單行長度很長,需要拆分,最好不用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進行產品開發過程中的很多經驗,希望對大家有協助。


相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.