爲何選擇Go語言 GO語言都能作什麼產品

Go語言,又稱Golang,是Google開發的一款靜態強類型、編譯型、併發型,並具備垃圾回收機制的編程語言,它的運行速度很是之快,同時還有以下特性:具備一流的標準庫、無繼承關係、支持多核;同時它還有着傳說級的設計者與極其優秀的社區支持,更別提還有對於咱們這些web應用的編寫者異常方便、能夠避免事件循環與回調地獄的goroutine-per-request設置了(每次請求處理都須要啓動一個獨立的goroutine)。目前,Go語言已經成爲構建系統、服務器,特別是微服務的熱門選擇。 

正如使用其它新興語言或技術同樣,咱們在早期的實驗階段經歷了好一陣子的摸索期。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語言中引用類型的默認值)。 
Java代碼 
  1. func something() (thing string, err error) {    
  2.     s := db.GetSomething()  
  3.     if s == "" {  
  4.         return s, errors.New("Nothing Found")  
  5.     }  
  6.     return s, nil  
  7. }  

因爲咱們想要建立一個error,並在調用棧的更高層級中進行處理,所以最終使用了panic。 
Java代碼 
  1. s, err := something()    
  2.     if err != nil {  
  3.     panic(err)  
  4. }  

結果咱們徹底驚呆了:一個error?天啊,運行它! 

但在Go中,你會發現error其實也是返回值,在函數調用和響應處理中十分常見,而panic則會拖慢應用的性能,並致使崩潰——相似運行異常時的崩潰。爲何要僅僅由於須要函數返回error就這樣作呢?這是咱們的教訓。在1.6 版本發佈前,轉儲panic的堆棧也負責轉儲全部運行的Go程序,致使在查找問題起源時很是困難,咱們在一大堆不相關的內容上查找了好久,白費力氣。 

就算有一個真正不可恢復的error,或是遇到了運行時的panic,極可能你也並不但願整個web服務器崩潰,由於它也是不少其餘服務的中間件(你的數據庫也使用事務機制對吧?) 所以咱們學到了處理這些panic的方式:在Revel中添加filter可以讓這些panic恢復,還能獲取日誌文件中的堆棧追蹤記錄併發送到 Sentry,而後經過電郵以及Teamwork Chat實時聊天工具給咱們發送警告,API向前端返回「500內部服務器錯誤」。 
Java代碼 
  1. // PanicFilter wraps the action invocation in a protective defer blanket that  
  2. // recovers panics, logs everything, and returns 500.  
  3. func PanicFilter(rc *revel.Controller, fc []revel.Filter) {    
  4.     defer func() {  
  5.         if err := recover(); err != nil {  
  6.             handleInvocationPanic(rc, err) // stack trace, logging. alerting              
  7.         }  
  8.     }()  
  9.     fc[0](rc, fc[1:])  
  10. }  

3. 小心不止一次從Request.Body的讀取 
從http.Request.Body讀取內容以後,其Body就被抽空了,隨後再次讀取會返回空body[]byte{} 。這是由於在讀取一個http.Request.Body的數據時,讀取器會停在數據的末尾,想要再次讀取必須先進行重置。然而,http.Request.Body是一個io.ReadWriter,並未提供Peek或Seek之類能解決這個問題的方法。有一個解決辦法是先將Body複製到內存中,讀取以後再將本來的內容填回去。若是有大量request的話,這種方式的開銷很大,只能算權宜之計。 

下面是一段短小而完整的代碼: 
Java代碼 
  1. package main  
  2.   
  3. import (    
  4.     "bytes"  
  5.     "fmt"  
  6.     "io/ioutil"  
  7.     "net/http"  
  8. )  
  9.   
  10. func main() {    
  11.     r := http.Request{}  
  12.     // Body is an io.ReadWriter so we wrap it up in a NopCloser to satisfy that interface  
  13.     r.Body = ioutil.NopCloser(bytes.NewBuffer([]byte("test")))  
  14.   
  15.     s, _ := ioutil.ReadAll(r.Body)  
  16.     fmt.Println(string(s)) // prints "test"  
  17.   
  18.     s, _ = ioutil.ReadAll(r.Body)  
  19.     fmt.Println(string(s)) // prints empty string!   
  20. }  

這裏包括複製及回填的代碼: 
Java代碼 
  1. content, _ := ioutil.ReadAll(r.Body)    
  2. // Replace the body with a new io.ReadCloser that yields the same bytes  
  3. r.Body = ioutil.NopCloser(bytes.NewBuffer(content))    
  4. again, _ = ioutil.ReadAll(r.Body)    

能夠建立一些util函數: 
Java代碼 
  1. func ReadNotDrain(r *http.Request) (content []byte, err error) {    
  2.     content, err = ioutil.ReadAll(r.Body)  
  3.     r.Body = ioutil.NopCloser(bytes.NewBuffer(content))   
  4.     return  
  5. }  

以替代調用相似ioutil.ReadAll的方式: 
Java代碼 
  1. 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代碼,那麼這種方法值得一試。 
Java代碼 
  1. var customer Customer    
  2.    query = db.  
  3.    Joins("inner join tickets on tickets.customersId = customers.id").  
  4.    Where("tickets.id = ?", e.Id).  
  5.    Where("tickets.state = ?", "active").  
  6.    Where("customers.state = ?", "Cork").  
  7.    Where("customers.isPaid = ?", false).   
  8.    First(&customer)  

5. 無指向的指針是沒有意義的 
實際上這裏特指切片(slice)。你在向函數傳值時使用到了切片?在Go語言中,數組(array)也是數值,若是有大量的數組的話,你也不但願每次傳值或者分配時都要複製一下吧?沒錯,讓內存傳遞數組的開銷是很大的,但在Go語言中,99%的時間裏咱們處理的都是切片而不是數組。通常來說,切片能夠當成數組部分片斷的描述(常常是所有的片斷),包含指向數組開始元素的指針、切片的長度與容量。 

切片的每一個部分只須要8個字節, 所以不管底層是什麼,數組有多大都不會超過24個字節。 

 

咱們常常向函數切片發送指針,覺得能節省空間。 
Java代碼 
  1. t := getTickets() // e.g. returns []Tickets, a slice    
  2. ft := filterTickets(&t)  
  3.   
  4. func filterTickets(t *[]Tickets) []Tickets {}    

顯而易見,若是沒找到ticket,則返回0, 0, error;若是找到了ticket,則返回120, 80, nil之類的格式,具體數值取決於ticket的count。關鍵在於:若是在函數簽名中命名了返回值,就可使用return(naked return),在調用返回時,也會返回每一個命名返回值所在的狀態。 

然而,咱們有一些大型函數,大到有些笨重的那種。在函數中的,任何長度須要翻頁的naked returns都會極大地影響可讀性,並容易形成細微不易察覺的bug。特別若是有多個返回點的話,千萬不要使用naked returns或者大型函數。 

下面是一個例子: 
Java代碼 
  1. func findTickets() (tickets []Ticket, countActive int64, err error) {    
  2.     tickets, countActive := db.GetTickets()  
  3.     if tickets == 0 {  
  4.         err = errors.New("no tickets found!")  
  5.     } else {  
  6.         tickets += addClosed()  
  7.         // return, hmmm...okay, I might know what this is  
  8.         return   
  9.     }  
  10.     .  
  11.     .  
  12.     .  
  13.     // lots more code  
  14.     .  
  15.     .  
  16.     .  
  17.     if countActive > 0 {  
  18.         countActive - closedToday()  
  19.         // have to scroll back up now just to be sure...  
  20.         return  
  21.     }  
  22.     .  
  23.     .  
  24.     .  
  25.     // Okay, by now I definitely can't remember what I was returning or what values they might have  
  26.     return  
  27. }  

7. 小心做用域與縮略聲明 
在Go語言中,若是在不一樣的塊區內使用相同的縮略名:=來聲明變量時,因爲做用域(scope)的存在,會出現一些細微不易察覺的bug,咱們稱之爲shadowing。 
Java代碼 
  1. func findTickets() (tickets []Ticket, countActive int64) {    
  2.     tickets, countActive := db.GetTickets() // 10 tickets returned, 3 active  
  3.     if countActive > 0 {  
  4.         // oops, tickets redeclared and used just in this block  
  5.         tickets, err := removeClosed() // 6 tickets left after removing closed  
  6.         if err != nil {  
  7.             // Argh! We used the variables here for logging!, if we didn't we would  
  8.             // have received a compile-time error at least for unused variables.  
  9.             log.Printf("could not remove closed %s, ticket count %d", err.Error(), len(tickets))  
  10.         }  
  11.     }  
  12.     return // this will return 10 tickets o_O  
  13. }  

具體在於:=縮略變量的聲明與分配問題,通常來講若是在左邊使用新變量時,纔會編譯:=,但若是左邊出現其餘新變量的話,也是有效的。在上例中,err是新變量,由於在函數返回的參數中已經聲明過,你覺得ticket會被自動覆蓋。但事實並不是如此,因爲塊區做用域的存在,在聲明和分配新的ticket變量後,一旦塊區閉合,其做用域就會丟失。爲了解決這個問題,咱們只需聲明變量err位於塊區以外,再用=來代替:=,優秀的編輯器(好比加入Go插件的Emacs或Sublime就能解決這個shadowing的問題)。 
Java代碼 
  1. func findTickets() (tickets []Ticket, countActive int64) {    
  2.     var err error  
  3.     tickets, countActive := db.GetTickets() // 10 tickets returned, 3 active  
  4.     if countActive > 0 {  
  5.         tickets, err = removeClosed() // 6 tickets left after removing closed  
  6.         if err != nil {  
  7.             log.Printf("could not remove closed %s, ticket count %d", err.Error(), len(tickets))  
  8.         }  
  9.     }  
  10.     return // this will return 6 tickets  
  11. }  

8. 映射與隨機崩潰 
在併發訪問時,映射並不安全。咱們曾出現過這個狀況:將映射做爲應用整個生命週期的應用級變量,在咱們的應用中,這個映射是用來收集每一個控制器統計數據的,固然在Go語言中每一個http request都是本身的goroutine。 

你能夠猜到下面會發生什麼,實際上不一樣的goroutine會嘗試同時訪問映射,也多是讀取,也多是寫入,可能會形成panic而致使應用崩潰(咱們在Ubuntu中使用了 upstart腳本,在進程中止時重啓應用,至少保證應用算是「在線」)。有趣的是:這種狀況隨機出現,在1.6版本以前,想要找出像這樣出現panic的緣由都有些費勁,由於堆棧轉儲包含全部運行狀態下的goroutine,從而致使咱們須要過濾大量的日誌。 

在併發訪問時,Go團隊的確考慮過映射的安全性問題,但最終放棄了,由於在大多數狀況下這種方式會形成非必要開銷,在 golang.org的FAQ中有這樣的解釋: 

在通過長期討論後,咱們決定在使用映射時,通常不需從多個goroutine執行安全訪問。在確實須要安全訪問時,映射極可能屬於已經同步過的較大數據架構或者計算。所以,若是要求全部映射操做須要互斥鎖的話,會拖慢大多數程序,但效果寥寥無幾。因爲不經控制的映射訪問會讓程序崩潰,做出這個決定並不容易。 
咱們的代碼看起來就象這樣: 
Java代碼 
  1. package stats  
  2.   
  3. var Requests map[*revel.Controller]*RequestLog    
  4. var RequestLogs map[string]*PathLog    

咱們對其進行了修改,使用stdlib的同步數據包:在封裝映射的結構中嵌入讀取/寫入互斥鎖。咱們爲這個結構添加了一些helper:Add與Get方法: 
Java代碼 
  1. var Requests ConcurrentRequestLogMap  
  2.   
  3. // init is run for each package when the app first runs  
  4. func init() {    
  5.     Requests = ConcurrentRequestLogMap{items: make(map[interface{}]*RequestLog)}  
  6. }  
  7.   
  8. type ConcurrentRequestLogMap struct {    
  9.     sync.RWMutex // We embed the sync primitive, a reader/writer Mutex  
  10.     items map[interface{}]*RequestLog  
  11. }  
  12.   
  13. func (m *ConcurrentRequestLogMap) Add(k interface{}, v *RequestLog) {    
  14.     m.Lock() // Here we can take a write lock  
  15.     m.items[k] = v  
  16.     m.Unlock()  
  17. }  
  18.   
  19. func (m *ConcurrentRequestLogMap) Get(k interface{}) (*RequestLog, bool) {    
  20.     m.RLock() // And here we can take a read lock  
  21.     v, ok := m.items[k]  
  22.     m.RUnlock()  
  23.   
  24.     return v, ok  
  25. }  

如今不再會崩潰了。 

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項目所採納。 
相關文章
相關標籤/搜索