Categories
程式開發

我使用Golang兩年後的總結:優點和令人討厭的怪癖


過去兩年來,我就職於Assembled,一直使用Go工作。自公司成立以來,Go就一直是我們的主要後端語言。在我之前的工作中,我混合著使用Ruby和Scala,所以肯定有過一段適應期。總體而言,我發現Go的表現基本上與廣告宣傳的一樣:儘管有些怪異,但它非常適合專業工作。

背景介紹

Assembled是客戶支持團隊用來管理人員配備和分析的Web應用程序。從架構的角度來看,它是一個標準的Web應用程序:React前端通過內部HTTP API訪問Go後端,後者反過來訪問PostgreSQL數據庫。

也就是說,有幾個特定的應用程序,我們也必須對其進行優化:

  • 我們維護一個外部API
  • 我們從基於Python的機器學習服務中生成時間序列預測
  • 我們執行相當繁重的批處理,因為我們通過SFTP同步請求中心系統的調度。
  • 我們運行相當複雜的優化算法;我的兄弟John在HN評論中對此進行了詳細的分享。
  • 我們處理令人痛苦的時區和重複事件相關的邏輯

生產中的Go

我們構建Assembled,不使用Python、Java、Haskell(或……),特意決定使用Go。決定因素可歸結為:靜態類型、簡單性、標準庫和速度。在實踐中,我所見過的一切都與Go在文檔中所呈現的方式相吻合:

Go富有表現力、簡潔、乾淨且高效……Go可以快速編譯成機器碼,同時還具有垃圾收集的便利性及運行時反射的能力。這是一種快速的、靜態類型的編譯語言,但它感覺起來就像是一種動態類型的解釋語言。

對新手工程師來說,它很乏味,但很簡單

Go的簡單性使得新手工程師可以快速使用我們的代碼庫,他們中的許多人以前從未使用過該語言(包括我自己)。我懷疑Go的設計迫使我們編寫比以前更簡單、更明確的代碼。也就是說,簡單的確會導致重複,這一點已經得到了充分的證明。截至撰寫本文時,代碼片段 if err != nil在我們的代碼庫中出現了2919次。

標準庫可以滿足需要

Go的標準庫是一座閃亮的“燈塔”。像 bytes、encoding/json、database/sql、net/http和time這樣的包是全面且經過精心設計的。例如,在Python中,它會告訴我們第三方庫(顯然,三方庫是好的!)採用的是HTTP默認缺省值。

Go的設計顯然考慮了生產代碼。最引人注目的是,Assembled有一段時間存在嚴重的性能問題(敲打敲打),我們可以使用net/pprof和runtime/pprof進行調試。這些功能非常強大,並且易於通過HTTP處理程序啟用,如下所示。我唯一的遺憾是,我找到的解釋該輸出的最佳指南被埋沒在一篇博客文章裡了。

  superAdminMux.HandleFunc("/debug/pprof/heap", pprof.Handler("heap").ServeHTTP)
  superAdminMux.HandleFunc("/debug/pprof/profile", pprof.Profile)

快速、簡單的構建影響一切

Go最好的地方在於,我們可以輕鬆運行go build,並且幾乎無需等待,就可以可靠地期望一個可以運行的可執行文件。在沒有IntelliJ或Eclipse的情況下, Java仍然會使編譯過程痛苦不堪,更不要說從Ruby或Python開始了。

快速、簡單的構建體驗使許多下游任務變得更容易:

  • 我們的部署命令本質上是git pull,然後是go install
  • 持續集成(CI),好吧……我們可以說Go也沒有問題

主要的困難在於,採用文件監視和循環重建的方式進行本地開發。我們有多個構建目標(例如,應用程序後端和API),並使用https://github.com/cespare/reflex,但這需要一些配合工作才能很好地在Mac OS X運行。

標準格式和文檔

我是否提到過,Go顯然是為專業人士打造的?

  • gofmt處理縮進和對齊;我使用vim-go插件,該插件會在我們保存.go文件時自動應用
  • 這裡有一個關於Go文檔是如何不同的雄辯解釋;我最喜歡的是標準外觀,位於https://godoc.org/公共存儲庫中,它能在本地運行並能快速提取項目特定代碼的事實。

Gopher ❤️

如此可愛的化身。這裡有篇關於Go gopher起源的有趣讀物:https://blog.golang.org/gopher

我使用Golang兩年後的總結:優點和令人討厭的怪癖 1

痛點

Go也有令人討厭的怪癖。其中的很多已經在其他地方被很好地記錄下來了,但出於完整性的考慮,我將它們包括進來。

沒有官方的包管理者故事(直到最近都沒有)

從Go 1.14(2020年2月)開始,Go模塊已經準備好投入生產使用了。在此之前,這裡是“荒蕪的西部”:我們登陸了dep,但沒有機會遷移到模塊中。 dep是(或曾經是)一項令人欽佩的工作,但它也非常緩慢。通常的建議是將依賴項簽入到存儲庫中(例如,在/vendor文件夾中),這在生產環境設置中可能並不令人欣狂。

GOPATH讓人困惑

GOPATH目錄應該魔法地包含所有代碼。我認為(基於Wiki的推測)這與簡化從遠程存儲庫中的獲取有關,例如,go get github.com/my/repo。從理論上講這很優雅,但在實踐中卻是令人困惑的,因為如果我們沒有將代碼放在正確的位置上,就會什麼都行不通。因此,Go給我留下了非常負面的第一印象。

現在,我工作計算機上的.profile文件只有如下內容:

  export GOPATH=$HOME/go
  export PATH="$GOPATH/bin:$PATH"
  cd $GOPATH/src/github.com/assembledhq/assembled

錯誤很難自省

大多數人都抱怨Go的錯誤處理太過冗長,也很難使用。在Go 1.13(2019年10月)中,添加了用於包裝、拆包和比較的優秀方法,但不幸的是,採用率方面我們處於落後狀態。

使用1.13版本之前編寫的代碼,不會對錯誤進行包裝,這也特別痛苦。例如,在Google自己的API bindings中,請求的底層的HTTP錯誤不會被包裝,因此無法作為googleapi.RetrieveError、公共錯誤接口、甚至是低級的url.Error進行檢查。唯一的選項是字符串匹配,我們這樣做,通常是為了捕獲類似invalid_grant這樣的OAuth錯誤。

例如,比較一下Scala中模式匹配是如何進行錯誤處理的。

Nil versus zero values Nil與零值

對空值或缺省後故意將其值設置為零的值進行建模,是很乏味的。例如,在Stripe的Go bindings中可以看到這種遷移。在我們的代碼中,我們通常會返回一個指針和錯誤,例如(*string,error)。通過引入非關聯nil指針的可能性來打破類型安全。我們可以檢查res == nil以及err != nil,但編譯器不能把我們從健忘或懶惰中拯救出來。

缺乏面向對象的表現力

Go建議使用接口和類型嵌入來複製有用的面向對象行為,這種行為在其他語言中是自然存在的。事實證明,這些工具具有很大的限制性,在不同的情況下,我們可以偶然繞過類型系統。這導致會誘惑。

Go社區對此進行了詳細介紹。這裡有一個非常不錯的總結:https://blog.golang.org/why-generics

結論

最初使用Go,我花了一些時間來熱身。正如我在GOPATH中所描述的那樣,入門有些困惑。從諸如Ruby和Scala之類的語言轉換過來,也意味著需要轉變思維方式(或兩種)。然而,在使用該語言的兩年裡,我開始真正享受它的簡單和明確性了。

在Assembled公司,該語言確實非常適合我們的用例,這是一個主要的標準Web應用程序。我對這個生態系統寄予厚望,感覺就像我們正在使用經過精心設計和良好維護的工具一樣。因此,在快速變更代碼庫的同時,提供穩定服務所需的基線工作量就更少了。

原文鏈接:

https://wgyn.github.io/2020/04/12/reflections-on-2-years-of-golang.html