正如使用其它新興語言或技術同樣,咱們在早期的實驗階段經歷了好一陣子的摸索期。Go語言確實有本身的風格與使用習慣,尤爲是對於從面嚮對象語言(好比Java)或腳本語言(好比Python)轉過來的開發者而言更是如此。因此咱們非常犯了些錯誤,在本文中咱們但願能與你們分享所得。若是在生產環境中使用Go語言,下面這些問題都有可能碰到,但願本文能爲Go語言的初學者提供一些幫助。
1. Revel不是好的選擇
對於初學Go語言、須要構建web服務器的用戶來講,他們也許會認爲此時須要一個合適的框架。使用MVC框架確有優點,主要是因爲慣例優先原則設置了一系列的項目架構與慣例,從而賦予了項目一致性,並下降了跨項目開發的門檻。但咱們發現:自行配置比遵循慣例更爲強大,尤爲是Go語言已經將編寫web應用的難度降到了最低,而咱們的不少web應用都是小型服務。最重要的是:咱們的應用不符合慣例。
Revel的設計初衷在於:嘗試將Play或Rails之類的框架引入Go語言,而不是運用Go與stdlib的力量,並以其爲基礎進行構建。根據Go語言編寫者的說法:
引用
最初這只是一個有趣的項目,我想嘗試可否在不那麼神奇的Go語言中複製神奇的Play框架體驗。
公平來說,那時候在一種新語言中採用MVC框架對咱們來講頗有意義——無需爭論架構,同時新團隊也能連貫地構建內容。在使用Go語言以前,我所編寫的每一個web應用都有着藉助MVC框架的痕跡。在C#中使用了ASP.NET MVC,在Java中使用了SpringMVC,在PHP中使用了Symfony,在Python中使用了CherryPy,在Ruby中使用了RoR,但最後咱們終於發現,在Go語言中不須要框架。標準庫HTTP包已經包含所需的內容了,通常只要加入多路複用器(好比 mux)來選擇路由,再加入lib來處理中間件(好比
negroni)的任務(包括身份驗證與登陸等)就足夠了。
Go的標準庫HTTP包設計讓這項工做十分簡單,使用者會漸漸發現:Go的強大有一部分緣由就在於其工具鏈與相關的工具——其中包含各類可運行在代碼中的強大命令。但在Revel中,因爲項目架構的設定,再加上缺少package main與func main() {}入口(這些都是慣用和必要的Go命令),咱們沒法使用這些工具。事實上Revel附帶本身的命令包,鏡像一些相似run與build之類的命令。
使用Revel後,咱們:
- 沒法運行go build;
- 沒法運行go install;
- 沒法使用 race detector (–race);
- 沒法使用go-fuzz或者其它須要可構建Go資源的強大工具;
- 沒法使用其它中間件或者路由;
- 熱重載雖然簡潔,但很緩慢,Revel在源上使用了反射機制(reflection),且從1.4版原本看,編譯時間也增長了大約30%。因爲並未使用go install,程序包沒有緩存;
- 因爲在Go 1.5及以上版本中編譯速度更慢,所以沒法遷移到高版本,爲了將內核升級到1.6版,咱們去掉了Revel;
- Revel將測試放置在/test dir下面,違反了Go語言中將_test.go文件與測試文件打包在一塊兒的習慣;
- 要想運行Revel測試,須要啓動服務器並執行集成測試。
咱們發現Revel的不少方式與Go語言的構建習慣相去甚遠,同時也失去了一些強大go工具集的協助。
2. 明智地使用Panics
若是你是從Java或C#轉到Go語言的開發者,可能會有些不太習慣Go語言中的錯誤處理方式(error handling)。在Go語言中,函數可返回多個值,所以在返回其餘值時一併返回error是很典型的狀況,若是一切運行正常的話,resturnsError返回的值爲nil(nil是Go語言中引用類型的默認值)。
- func something() (thing string, err error) {
- s := db.GetSomething()
- if s == "" {
- return s, errors.New("Nothing Found")
- }
- return s, nil
- }
因爲咱們想要建立一個error,並在調用棧的更高層級中進行處理,所以最終使用了panic。
- s, err := something()
- if err != nil {
- panic(err)
- }
結果咱們徹底驚呆了:一個error?天啊,運行它!
但在Go中,你會發現error其實也是返回值,在函數調用和響應處理中十分常見,而panic則會拖慢應用的性能,並致使崩潰——相似運行異常時的崩潰。爲何要僅僅由於須要函數返回error就這樣作呢?這是咱們的教訓。在1.6 版本發佈前,轉儲panic的堆棧也負責轉儲全部運行的Go程序,致使在查找問題起源時很是困難,咱們在一大堆不相關的內容上查找了好久,白費力氣。
就算有一個真正不可恢復的error,或是遇到了運行時的panic,極可能你也並不但願整個web服務器崩潰,由於它也是不少其餘服務的中間件(你的數據庫也使用事務機制對吧?) 所以咱們學到了處理這些panic的方式:在Revel中添加filter可以讓這些panic恢復,還能獲取日誌文件中的堆棧追蹤記錄併發送到
Sentry,而後經過電郵以及Teamwork Chat實時聊天工具給咱們發送警告,API向前端返回「500內部服務器錯誤」。
- func PanicFilter(rc *revel.Controller, fc []revel.Filter) {
- defer func() {
- if err := recover(); err != nil {
- handleInvocationPanic(rc, err)
- }
- }()
- fc[0](rc, fc[1:])
- }
3. 小心不止一次從Request.Body的讀取
從http.Request.Body讀取內容以後,其Body就被抽空了,隨後再次讀取會返回空body[]byte{} 。這是由於在讀取一個http.Request.Body的數據時,讀取器會停在數據的末尾,想要再次讀取必須先進行重置。然而,http.Request.Body是一個io.ReadWriter,並未提供Peek或Seek之類能解決這個問題的方法。有一個解決辦法是先將Body複製到內存中,讀取以後再將本來的內容填回去。若是有大量request的話,這種方式的開銷很大,只能算權宜之計。
下面是一段短小而完整的代碼:
- package main
-
- import (
- "bytes"
- "fmt"
- "io/ioutil"
- "net/http"
- )
-
- func main() {
- r := http.Request{}
-
- r.Body = ioutil.NopCloser(bytes.NewBuffer([]byte("test")))
-
- s, _ := ioutil.ReadAll(r.Body)
- fmt.Println(string(s))
-
- s, _ = ioutil.ReadAll(r.Body)
- fmt.Println(string(s))
- }
這裏包括複製及回填的代碼:
- content, _ := ioutil.ReadAll(r.Body)
- r.Body = ioutil.NopCloser(bytes.NewBuffer(content))
- again, _ = ioutil.ReadAll(r.Body)
能夠建立一些util函數:
- func ReadNotDrain(r *http.Request) (content []byte, err error) {
- content, err = ioutil.ReadAll(r.Body)
- r.Body = ioutil.NopCloser(bytes.NewBuffer(content))
- return
- }
以替代調用相似ioutil.ReadAll的方式:
- content, err := ReadNotDrain(&r)
固然,如今你已經用no-op替換了r.Body.Close(),在request.Body中調用Close時將不會執行任何操做,這也是httputil.DumpRequest的工做方式。
4. 一些持續優化的庫有助於SQL的編寫
在Teamwork Desk,向用戶提供web應用服務的核心功能常要涉及MySQL,而咱們沒有使用存儲程序,所以在Go之中的數據層包含一些很複雜的MySQL……並且某些代碼所構建的查詢複雜程度,足以媲美奧林匹克體操比賽的冠軍。一開始,咱們用
Gorm及其可鏈API來構建SQL,在Gorm中仍可以使用原始的SQL,並讓它根據你的結構來生成結果(但在實踐中,近來咱們發現這類操做愈來愈頻繁,這表明着咱們須要從新調整使用Gorm的方式,以確保找到最佳方式,或者須要多看些替代方案——但也沒什麼好怕的!)
對於一些人來講,對象關係映射(ORM)很是糟糕,它會讓人失去控制力與理解力,以及優化查詢的可能性,這種想法沒錯,但咱們只是用Gorm做爲構建查詢(能理解其輸出的那部分)的封裝方式,而不是看成ORM來徹底使用。在這種狀況下,咱們能夠像下面這樣使用其可鏈API來構建查詢,並根據具體結構來調整結果。它的不少功能方便在代碼中手寫SQL,還支持Preloading、Limits、Grouping、Associations、Raw SQL、Transactions等操做,若是你要在Go語言中手寫SQL代碼,那麼這種方法值得一試。
- var customer Customer
- query = db.
- Joins("inner join tickets on tickets.customersId = customers.id").
- Where("tickets.id = ?", e.Id).
- Where("tickets.state = ?", "active").
- Where("customers.state = ?", "Cork").
- Where("customers.isPaid = ?", false).
- First(&customer)
5. 無指向的指針是沒有意義的
實際上這裏特指切片(slice)。你在向函數傳值時使用到了切片?在Go語言中,數組(array)也是數值,若是有大量的數組的話,你也不但願每次傳值或者分配時都要複製一下吧?沒錯,讓內存傳遞數組的開銷是很大的,但在Go語言中,99%的時間裏咱們處理的都是切片而不是數組。通常來說,切片能夠當成數組部分片斷的描述(常常是所有的片斷),包含指向數組開始元素的指針、切片的長度與容量。
切片的每一個部分只須要8個字節, 所以不管底層是什麼,數組有多大都不會超過24個字節。
咱們常常向函數切片發送指針,覺得能節省空間。
- t := getTickets()
- ft := filterTickets(&t)
-
- func filterTickets(t *[]Tickets) []Tickets {}
顯而易見,若是沒找到ticket,則返回0, 0, error;若是找到了ticket,則返回120, 80, nil之類的格式,具體數值取決於ticket的count。關鍵在於:若是在函數簽名中命名了返回值,就可使用return(naked return),在調用返回時,也會返回每一個命名返回值所在的狀態。
然而,咱們有一些大型函數,大到有些笨重的那種。在函數中的,任何長度須要翻頁的naked returns都會極大地影響可讀性,並容易形成細微不易察覺的bug。特別若是有多個返回點的話,千萬不要使用naked returns或者大型函數。
下面是一個例子:
- func findTickets() (tickets []Ticket, countActive int64, err error) {
- tickets, countActive := db.GetTickets()
- if tickets == 0 {
- err = errors.New("no tickets found!")
- } else {
- tickets += addClosed()
-
- return
- }
- .
- .
- .
-
- .
- .
- .
- if countActive > 0 {
- countActive - closedToday()
-
- return
- }
- .
- .
- .
-
- return
- }
7. 小心做用域與縮略聲明
在Go語言中,若是在不一樣的塊區內使用相同的縮略名:=來聲明變量時,因爲做用域(scope)的存在,會出現一些細微不易察覺的bug,咱們稱之爲shadowing。
- func findTickets() (tickets []Ticket, countActive int64) {
- tickets, countActive := db.GetTickets()
- if countActive > 0 {
-
- tickets, err := removeClosed()
- if err != nil {
-
-
- log.Printf("could not remove closed %s, ticket count %d", err.Error(), len(tickets))
- }
- }
- return
- }
具體在於:=縮略變量的聲明與分配問題,通常來講若是在左邊使用新變量時,纔會編譯:=,但若是左邊出現其餘新變量的話,也是有效的。在上例中,err是新變量,由於在函數返回的參數中已經聲明過,你覺得ticket會被自動覆蓋。但事實並不是如此,因爲塊區做用域的存在,在聲明和分配新的ticket變量後,一旦塊區閉合,其做用域就會丟失。爲了解決這個問題,咱們只需聲明變量err位於塊區以外,再用=來代替:=,優秀的編輯器(好比加入Go插件的Emacs或Sublime就能解決這個shadowing的問題)。
- func findTickets() (tickets []Ticket, countActive int64) {
- var err error
- tickets, countActive := db.GetTickets()
- if countActive > 0 {
- tickets, err = removeClosed()
- if err != nil {
- log.Printf("could not remove closed %s, ticket count %d", err.Error(), len(tickets))
- }
- }
- return
- }
8. 映射與隨機崩潰
在併發訪問時,映射並不安全。咱們曾出現過這個狀況:將映射做爲應用整個生命週期的應用級變量,在咱們的應用中,這個映射是用來收集每一個控制器統計數據的,固然在Go語言中每一個http request都是本身的goroutine。
你能夠猜到下面會發生什麼,實際上不一樣的goroutine會嘗試同時訪問映射,也多是讀取,也多是寫入,可能會形成panic而致使應用崩潰(咱們在Ubuntu中使用了
upstart腳本,在進程中止時重啓應用,至少保證應用算是「在線」)。有趣的是:這種狀況隨機出現,在1.6版本以前,想要找出像這樣出現panic的緣由都有些費勁,由於堆棧轉儲包含全部運行狀態下的goroutine,從而致使咱們須要過濾大量的日誌。
在併發訪問時,Go團隊的確考慮過映射的安全性問題,但最終放棄了,由於在大多數狀況下這種方式會形成非必要開銷,在
golang.org的FAQ中有這樣的解釋:
在通過長期討論後,咱們決定在使用映射時,通常不需從多個goroutine執行安全訪問。在確實須要安全訪問時,映射極可能屬於已經同步過的較大數據架構或者計算。所以,若是要求全部映射操做須要互斥鎖的話,會拖慢大多數程序,但效果寥寥無幾。因爲不經控制的映射訪問會讓程序崩潰,做出這個決定並不容易。
咱們的代碼看起來就象這樣:
- package stats
-
- var Requests map[*revel.Controller]*RequestLog
- var RequestLogs map[string]*PathLog
咱們對其進行了修改,使用stdlib的同步數據包:在封裝映射的結構中嵌入讀取/寫入互斥鎖。咱們爲這個結構添加了一些helper:Add與Get方法:
- var Requests ConcurrentRequestLogMap
-
- func init() {
- Requests = ConcurrentRequestLogMap{items: make(map[interface{}]*RequestLog)}
- }
-
- type ConcurrentRequestLogMap struct {
- sync.RWMutex
- items map[interface{}]*RequestLog
- }
-
- func (m *ConcurrentRequestLogMap) Add(k interface{}, v *RequestLog) {
- m.Lock()
- m.items[k] = v
- m.Unlock()
- }
-
- func (m *ConcurrentRequestLogMap) Get(k interface{}) (*RequestLog, bool) {
- m.RLock()
- v, ok := m.items[k]
- m.RUnlock()
-
- return v, ok
- }
如今不再會崩潰了。
9. Vendor的使用
好吧,雖然難以啓齒,但咱們恰好犯了這個錯誤,罪責重大——在將代碼部署到生產環境時,咱們竟然沒有使用vendor。
簡單解釋一下,在Go語言中,咱們經過從項目根目錄下運行go get ./...來得到依賴, 每一個依賴都須要從主服務器的HEAD上拉取,很顯然這種狀況很是糟糕,除非在$GOPATH的服務器上保存依賴的準確版本,而且一直不作更新(也不從新構建或運行新的服務器),若是更改無可迴避,你會對生產環境中運行的代碼失去控制。在Go 1.4版本中,咱們使用了Godeps及其GOPATH來執行vendor;在1.5版本中,咱們使用了GO15VENDOREXPERIMENT環境變量;到了1.6版本,終於不須要工具了——項目根目錄下的/vendor能夠自動識別爲依賴的存放位置。你能夠在不一樣的vendor工具中選擇一個來追蹤版本號,讓依賴的添加與更新更爲簡單(移除.git,更新清單等)。
收穫良多,但學無止境 上面僅僅列出了咱們初期所犯錯誤與所獲心得的一小部分。咱們只是由5名開發者組成的小團隊,建立了Teamwork Desk,儘管去年咱們在Go語言方面所獲良多,但還有大批的優秀功能蜂擁而至。今年咱們會出席各類關於Go語言的大會,包括在丹佛舉行的GopherCon大會;另外我還在Cork的當地開發者聚會上就Go的使用進行了討論。 咱們會繼續發佈Go語言相關的開源工具,並致力於回饋現有的庫。目前咱們已經適當提供了一些小型項目(參見列表),所發的Pull Request也被Stripe、Revel以及一些其餘的開源Go項目所採納。