你想在 Go 2 中看到什麼?

本文由小土翻譯自 What I'd like to see in Go 2.0[1],翻譯不當之處,煩請指出。

閱讀完文章,如果你也有想在 Go2 中看到的特性,歡迎留言討論。

目錄

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 的結構。不幸的是,這兩個軟件包都不適合或不夠強大,在沒有大量開發人員投資的情況下,可以滿足足夠高級的使用情況。

改進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]:

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.DefaultClienthttp.DefaultTransport都是具有共享狀態的全局變量。http.DefaultClient沒有配置超時,這使得 DOS 你自己的服務和創造瓶頸變得很容易。許多軟件包都會突變,http.DefaultClienthttp.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