你想在 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