你想在 Go 2 中看到什麼?
本文由小土翻譯自 What I'd like to see in Go 2.0[1],翻譯不當之處,煩請指出。
閱讀完文章,如果你也有想在 Go2 中看到的特性,歡迎留言討論。
目錄
-
關於作者
-
現代模板引擎
-
改進
range,以免 Copy 值 -
確定性的
select -
結構化日誌記錄接口
-
多錯誤處理
-
針對 JSON Marshal 的 error
-
標準庫中不再有公共變量
-
對緩衝渲染器的本地支持
-
結束語
Go 是我最喜歡的編程語言之一,但它仍然遠非完美。在過去的 10 年裏,我用 Go 來構建小型的輔助項目和大型的應用程序。雖然這門語言與 2009 年最初發布時相比已經有了很大的發展,但這篇文章強調了我認爲 Go 仍有改進空間的一些領域。
在我們開始之前,我想明確一點:我不是在批評個人或他們的貢獻。我唯一的意圖是努力使 Go 成爲最好的編程語言。
關於作者
Seth Vargo 是谷歌的工程師。此前他曾在 HashiCorp、Chef Software、CustomInk 和一些匹茲堡的創業公司工作。他是《Learning Chef[2]》一書的作者,熱衷於減少技術上的不平等。當他沒有寫作、從事開源工作、教學或在會議上發言時,Seth 喜歡與他的朋友共度時光併爲非營利組織提供建議。
現代模板引擎
Go 標準庫有兩個模板包:text/template[3] 和 html/template[4]. 它們使用大致相同的語法,但html/template處理實體轉義和其他一些特定於 Web 的結構。不幸的是,這兩個軟件包都不適合或不夠強大,在沒有大量開發人員投資的情況下,可以滿足足夠高級的使用情況。
-
**編譯時錯誤。**與 Go 本身不同,Go 模板包會很樂意讓您將整數作爲字符串傳遞,但會在運行時呈現錯誤。這意味着開發人員需要嚴格測試其模板中所有可能的輸入,而不是能夠依賴類型系統。Go 的模板包應該支持編譯時類型檢查。
-
與 Go 匹配
range子句。 我仍然把 Go 模板中的範圍子句的順序弄得一團糟,因爲它有時與 Go 本身的順序是相反的。對於兩個參數,模板引擎與標準庫相匹配。{{ range $a, $b := .Items }} // [$a = 0, $b = "foo"]for a, b := range items { // [a = 0, b = "foo"]但是,只有一個參數,模板引擎產生的是值,而 Go 渲染產生的是索引:
{{ range $a := .Items }} // [$a = "foo"]for a := range items { // [a = 0]Go 的模板包應該符合標準庫的工作方式。
-
**開箱即用 (Batteries included),反射是可選的。**作爲一般規則,我認爲大多數開發人員不應該需要與反射進行交互。但是,如果你想做任何基本的加法和減法之外的事情,Go 的模板包將迫使你使用反射。內置函數非常小,而且只能滿足一小部分用例。
在我寫完 Consul Template[5] 之後,很明顯,標準的 Go 模板功能不足以滿足用戶的需求。超過一半的問題是關於嘗試使用 Go 的模板語言。今天,Consul Template 有超過 [超過 50 個 "輔助" 函數](https://github.com/hashicorp/consul-template/blob/master/docs/templating-language.md "超過 50 個" 輔助 "函數"),其中絕大部分確實應該在標準模板語言中使用。
Consul Template 在這裏並不孤單。Hugo[6] 還有一個相當廣泛的輔助函數列表 [7],同樣,其中絕大多數應該真正使用標準模板語言。即使在我最近的項目中,Exposure Notification[8] 我們也無法逃脫反射 [9].
Go 的模板語言確實需要具有更廣泛的函數表面積。
-
短路評估。
編輯: 正如許多人所指出的那樣,這個特性將出現在 Go 1.18[10].
Go 的模板語言總是在子句中評估整個條件,這會產生一些非常有趣的 bug(直到運行時纔會再次出現)。請考慮以下情況,其中
$foo可能爲零:{{ if (and $foo $foo.Bar) }}看起來這似乎很好,但是將評估這兩個條件 - 表達式中沒有短路邏輯。這意味着
$foo如果爲 nil,這將引發運行時異常。要解決此問題,你必須分離條件子句:
{{ if $foo }} {{ if $foo.Bar }} {{ end }}Go 的模板語言應該像標準庫一樣工作,在第一個真值時停止執行條件。
-
**對特定 web 工具的投資。**我當
Ruby on Rails開發人員已經很多年了,我真的很喜歡建立漂亮的網絡應用程序是如此簡單。使用 Go 的模板語言,即使是最簡單的任務 - 例如將一個項目列表打印成一個句子 - 對於初學者來說也是難以企及的,特別是與 Rails 的Enumerable#to_sentence的相比。
改進range,以免 Copy 值
雖然它有很好的文檔記錄,但總是意外地複製範圍子句中的值。例如,請思考以下代碼:
type Foo struct {
bar string
}
func main() {
list := []Foo{{"A"}, {"B"}, {"C"}}
cp := make([]*Foo, len(list))
for i, value := range list {
cp[i] = &value
}
fmt.Printf("list: %q\n", list)
fmt.Printf("cp: %q\n", cp)
}
cp的價值是什麼?如果你說[A B C],可悲的是你是錯誤的。而實際上是:
[C C C]
這是因爲 Go 在子句中使用了值的副本,而不是值本身。**在 Go 2.0 中,Range子句應通過引用傳遞值。**在這個領域已經有一些關於 Go 2.0 的建議,包括改善 for-loop 人體工程學設計 [11] 和在每次迭代中重新定義範圍循環變量 [12],所以我對此抱有謹慎的希望。
確定性的select
在 select 語句的多個條件爲真的情況下,會通過統一的僞隨機來選擇 case 的 [13]. 這是一個非常微妙的錯誤來源,並且它被看起來外觀相似的switch語句而加劇,該語句確實按其編寫的順序進行評估。
考慮一下下面的代碼,我們希望它的行爲是 "如果系統停止了,什麼都不做。否則等待新的工作,最多 5 秒,然後超時"。:
for {
select {
case <-doneCh: // or <-ctx.Done():
return
case thing := <-thingCh:
// ... long-running operation
case <-time.After(5*time.Second):
return fmt.Errorf("timeout")
}
}
如果在輸入語句時滿足多個條件(例如 doneCh已關閉且已超過 5 秒),那麼哪個路徑將被執行是不確定的行爲。這使得編寫正確的取消代碼變得惱人的冗長:
for {
// Check here in case we've been CPU throttled for an extended time, we need to
// check graceful stop or risk returning a timeout error.
// 優雅地停止關閉
select {
case <-doneCh:
return
default:
}
select {
case <-doneCh:
return
case thing := <-thingCh:
// Even though this case won, we still might ALSO be stopped.
// 儘管選中這個case,我們也會被停止
select {
case <-doneCh:
return
default:
}
// ...
default <-time.After(5*time.Second):
// Even though this case won, we still might ALSO be stopped.
// 儘管超時,我們也會被停止
select {
case <-doneCh:
return
default:
}
return fmt.Errorf("timeout")
}
}
如果 select 被更新爲確定性的,原來的代碼(在我看來,它更簡單,更容易達到)將按原定計劃工作。然而,由於 select 的非確定性,我們必須不斷檢查主導條件。
與此相關的是,我很想看到一種 "如果這個通道包含任何消息,就從這個通道讀取,否則繼續" 的速記語法。目前的語法是冗長的。
select {
case <-doneCh:
return
default:
}
我很想看到這個檢查的更簡潔的版本,也許是這樣的語法:
select <-?doneCh: // not valid Go 不過在Go是無效的
結構化日誌記錄接口
Go 的標準庫包括 log[14] 包,這對於基本用途來說是不錯的。但是,大多數生產系統都希望進行結構化日誌記錄,Go 中不乏結構化日誌庫 [15]:
-
apex/log[16]
-
go-kit/log[17]
-
golang/glog[18]
-
hashicorp/go-hclog[19]
-
inconshreveable/log15[20]
-
rs/zerolog[21]
-
sirupsen/logrus[22]
-
uber/zap[23]
Go 在這一領域缺乏主見,導致了這些包的泛濫,其中大部分都有不兼容的函數和簽名。因此,一個庫的作者不可能發出結構化的日誌。例如,我希望能夠在 go-retry[24]、 go-envconfig[25]、或 go-githubactions[26] 中發射結構化日誌,但這樣做需要與這些庫中的一個緊密耦合。理想情況下,我希望我的庫的用戶可以選擇他們的結構化日誌解決方案,但由於缺乏一個通用的結構化日誌接口,這一點非常困難。
Go 標準庫需要定義一個結構化的日誌接口, 所有這些現有的上游包都可以選擇實現該接口。然後,作爲庫作者,我可以選擇接受一個接口log.StructuredLogger,實現者可以做出自己的選擇:
func WithLogger(l log.StructuredLogger) Option {
return func(f *Foo) *Foo {
f.logger = l
return f
}
}
我把這樣一個接口的草圖快速勾勒了出來。
// StructuredLogger is an interface for structured logging.
type StructuredLogger interface {
// Log logs a message.
Log(message string, fields ...LogField)
// LogAt logs a message at the provided level. Perhaps we could also have
// Debugf, Infof, etc, but I think that might be too limiting for the standard
// library.
LogAt(level LogLevel, message string, fields ...LogField)
// LogEntry logs a complete log entry. See LogEntry for the default values if
// any fields are missing.
LogEntry(entry *LogEntry)
}
// LogLevel is the underlying log level.
type LogLevel uint8
// LogEntry represents a single log entry.
type LogEntry struct {
// Level is the log level. If no level is provided, the default level of
// LevelError is used.
Level LogLevel
// Message is the actual log message.
Message string
// Fields is the list of structured logging fields. If two fields have the same
// Name, the later one takes precedence.
Fields []*LogField
}
// LogField is a tuple of the named field (a string) and its underlying value.
type LogField struct {
Name string
Value interface{}
}
圍繞着實際的接口會是什麼樣子,如何最小化分配,以及如何最大化兼容性,有很多討論,但目標是定義一個其他日誌庫可以輕鬆實現的接口。
在我的 Ruby 時代,有大量的 Ruby 版本管理器,每個都有自己的 dotfile 名稱和語法。Fletcher Nichol 設法說服了這些 Ruby 版本管理器的所有維護者,使其標準化爲. ruby-version,只需寫一個 gist[27]。我希望我們能在 Go 社區中使用結構化日誌記錄做類似的事情。
多錯誤處理
有很多情況,特別是對於後臺工作或週期性任務,系統可能會並行處理一些事情或在出現錯誤時繼續處理。在這些情況下,返回一個多重錯誤是有幫助的。在標準庫中沒有對處理錯誤集合的內置支持。
圍繞多錯誤處理有清晰簡潔的標準庫定義,可以統一社區,減少錯誤處理不當的風險,正如我們看到的錯誤包裝 (wrap) 和解包(unwrap)。
針對 JSON Marshal 的 error
說到 error,你知道嗎,將 error 類型嵌入到一個結構字段中,然後將該結構作爲 JSON 進行marshal,將 "error" 字段marshal爲 {}?
// https://play.golang.org/p/gl7BPJOgmjr
package main
import (
"encoding/json"
"fmt"
)
type Response1 struct {
Err error `json:"error"`
}
func main() {
v1 := &Response1{Err: fmt.Errorf("oops")}
b1, err := json.Marshal(v1)
if err != nil {
panic(err)
}
// got: {"error":{}}
// want: {"error": "oops"}
fmt.Println(string(b1))
}
至少對於內置的errorString類型,Go 應該爲. Error() 的結果進行marshal。另外,對於 Go 2.0 來說,當試圖marshal一個沒有實現自定義marshal邏輯的 error 類型時,JSON marshal會返回一個錯誤。
標準庫中不再有公共變量
僅舉一個例子,http.DefaultClient和http.DefaultTransport都是具有共享狀態的全局變量。http.DefaultClient沒有配置超時,這使得 DOS 你自己的服務和創造瓶頸變得很容易。許多軟件包都會突變,http.DefaultClient和http.DefaultTransport,這可能會浪費開發者數天的資源來追蹤錯誤。
Go 2.0 應該把這些東西變成私有的,並通過一個函數調用來公開它們,返回有關變量的唯一分配。另外,Go 2.0 還可以實現 "凍結" 的全局變量,這樣它們就不能被其他包所改變。
從軟件供應鏈的角度來看,我也擔心這類問題。如果我可以開發一個有用的包,祕密地修改http.DefaultTransport,使用一個自定義的RoundTripper,將你的所有流量通過我的服務器輸送出去,那將是一個非常糟糕的瞬間。
對緩衝渲染器的本地支持
這更像是一個 "不爲人所知或有據可查的事情"。大多數示例(包括 Go 文檔中的示例)都鼓勵執行以下操作,以便通過 Web 請求對 JSON 進行 marshal 或渲染 HTML:
func toJSON(w http.ResponseWriter, i interface{}) {
if err := json.NewEncoder(w).Encode(i); err != nil {
http.Error(w, "oops", http.StatusInternalServerError)
}
}
func toHTML(w http.ResponseWriter, tmpl string, i interface{}) {
if err := templates.ExecuteTemplate(w, tmpl, i); err != nil {
http.Error(w, "oops", http.StatusInternalServerError)
}
}
然而,對於這兩種情況,如果i足夠大,有可能在發送第一個字節(和 200 狀態碼)後,編碼 / 執行失敗。在這一點上,請求是無法恢復的,因爲你無法改變響應代碼。
大體上被接受的緩解方案是先渲染,然後複製到 w。這仍然會有很小的出錯空間(由於連接問題導致向 w 寫入失敗),但它確保在發送第一個字節之前,編碼 / 執行是成功的。然而,在每個請求中分配一個字節片是很昂貴的,所以你通常會使用緩衝池 [28]。
這種方法非常冗長,並將許多不必要的複雜性推給實現者。相反,如果 Go 能自動處理此緩衝池管理,可能會使用EncodePooled等函數,那就更好了。
結束語
Go 仍然是我最喜歡的編程語言之一,這就是爲什麼我覺得可以強調這些批評的原因。與任何編程語言一樣,Go 也在不斷髮展。你認爲這些是好主意嗎?還是說它們是糟糕的建議?請在 Twitter[29] 上告訴我。
參考資料
[1]
What I'd like to see in Go 2.0: https://www.sethvargo.com/what-id-like-to-see-in-go-2
[2]
Learning Chef: https://www.amazon.com/Learning-Chef-Configuration-Management-Automation/dp/1491944935
[3]
text/template: https://pkg.go.dev/text/template
[4]
html/template: https://pkg.go.dev/html/template
[5]
Consul Template: https://github.com/hashicorp/consul-template
[6]
Hugo: https://gohugo.io/
[7]
相當廣泛的輔助函數列表: https://gohugo.io/functions/
[8]
Exposure Notification: https://g.co/ens
[9]
無法逃脫反射: https://github.com/google/exposure-notifications-verification-server/blob/0ec489ba95137d5be10e1617d1dcdc2d1ee6e5e9/pkg/render/renderer.go#L232-L280
[10]
Go 1.18: https://tip.golang.org/doc/go1.18#text/template
[11]
改善 for-loop 人體工程學設計: https://github.com/golang/go/issues/24282
[12]
在每次迭代中重新定義範圍循環變量: https://github.com/golang/go/issues/20733
[13]
會通過統一的僞隨機來選擇 case 的: https://golang.org/ref/spec#Select_statements
[14]
log: https://pkg.go.dev/log
[15]
Go 中不乏結構化日誌庫: https://www.client9.com/logging-packages-in-golang/
[16]
apex/log: https://github.com/apex/log
[17]
go-kit/log: https://github.com/go-kit/kit/tree/master/log
[18]
golang/glog: https://github.com/golang/glog
[19]
hashicorp/go-hclog: https://github.com/hashicorp/go-hclog
[20]
inconshreveable/log15: https://github.com/inconshreveable/log15
[21]
rs/zerolog: https://github.com/rs/zerolog
[22]
sirupsen/logrus: https://github.com/sirupsen/logrus
[23]
uber/zap: https://github.com/uber-go/zap
[24]
go-retry: https://github.com/sethvargo/go-retry
[25]
go-envconfig: https://github.com/sethvargo/go-envconfig
[26]
go-githubactions: https://github.com/sethvargo/go-githubactions
[27]
gist: https://gist.github.com/fnichol/1912050
[28]
使用緩衝池: https://github.com/google/exposure-notifications-verification-server/blob/08797939a56463fe85f0d1b7325374821ee31448/pkg/render/html.go#L65-L91
[29]
Twitter: https://twitter.com/sethvargo
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/_I6ffLOsYsX4Q-EE0UR1cA