歡迎光臨
每天分享高質量文章

Go2設計草案介紹

前言

Go,毫無疑問已經成為主流服務端開發語言之一,但它的型別特性卻少的可憐,僅支援 structural subtyping。在 TIOBE 排名前二十的語言中,不管是上古語言 Java, 還是 2010 年之後出現的新語言 Rust/Julia 等,都支援至少三種型別特性,對此社群抱怨很多,另外還有它的錯誤處理方式,以及在 Go1.11 版本才解決的依賴管理等問題。在最近的 GopherCon2018 上,官方放出瞭解決這些問題的草案 (draft),這些內容還沒有成為正式的提案 (proposal), 只是先發出來供大家討論,最終會形成正式提案並被逐步引入到後續的版本中。此次放出的草案,集中討論了三個問題,泛型 / 錯誤處理 / 錯誤值。

泛型

泛型是復用邏輯的一個有效手段,在 2016 和 2017 年的 Go 語言調查中,泛型都列在最迫切的需求之首,在 Go1.0 release 之後 Go team 就已經開始探索如何引入泛型,但同時要保持 Go 的簡潔性 (開發者喜愛 Go 的主要原因之一),之前的幾種實現方式都存在嚴重的問題,被廢棄掉了,所以進展並不算快,甚至導致部分人誤解為 Go team 並不打算引入泛型。現在,最新的草案經過半年的討論和最佳化,已經確認可行 (could work),我們期待已久的泛型幾乎是板上釘釘的事情了,那麼 Go 的泛型大概長什麼樣?

在沒有泛型的情況下,透過 interface{}是可以解決部分問題的,比如 ring的實現,但這種方法只適合用在資料容器裡, 且需要做型別轉換。當我們需要實現一個通用的函式時,就做不到了,例如實現一個函式,其傳回傳入的 map 的 key:

  1. package main

  2. import "fmt"

  3. func Keys(m map[interface{}]interface{}) []interface{} {

  4.    keys := make([]interface{}, 0)

  5.    for k, _ := range m {

  6.        keys = append(keys, k)

  7.    }

  8.    return keys

  9. }

  10. func main() {

  11.    m := make(map[string]string, 1)

  12.    m["demo"] = "data"

  13.    fmt.Println(Keys(m))

  14. }

這樣寫連編譯都透過不了,因為型別不匹配。那麼參考其他支援泛型的語言的語法,可以這樣寫:

  1. package main

  2. import "fmt"

  3. func Keys<K, V>(m map[K]V) []K {

  4.    keys := make([]K, 0)

  5.    for k, _ := range m {

  6.        keys = append(keys, k)

  7.    }

  8.    return keys

  9. }

  10. func main() {

  11.    m := make(map[string]string, 1)

  12.    m["demo"] = "data"

  13.    fmt.Println(Keys(m))

  14. }

但是這種寫法是有缺陷的,假設 append 函式並不支援 string 型別,就可能會出現編譯錯誤。我們可以看下其他語言的做法:

  1. // rust

  2. fn print_g<T: Graph>(g : T) {

  3.    println!("graph area {}", g.area());

  4. }

Rust 在宣告 T 的時候,限定了入參的型別,即入參 g 必須是 Graph 的子類。和 Rust 的 nominal subtyping 不同,Go 屬於 structural subtyping,沒有顯式的型別關係宣告,因此不能使用此種方式。Go 在草案中引入了 contract來解決這個問題,語法類似於函式, 寫法更複雜,但表達能力比 Rust 要更強:

  1. // comparable contract

  2. contract Equal(t T) {

  3.    t == t

  4. }

  5. // addable contract

  6. contract Addable(t T) {

  7.    t + t

  8. }

上述程式碼分別約束了 T 必須是可比較的 (comparable),必須是能做加法運算(addable) 的。使用方式很簡單, 定義函式的時候加上約束即可:

  1. func Sum(type T Addable(T))(x []T) T {

  2.    var total T

  3.    for _, v := range x {

  4.        total += v

  5.    }

  6.    return total

  7. }

  8. var x []int

  9. total := Sum(int)(x)

得益於型別推斷,在呼叫 Sum 時可以簡寫成:

  1. total := Sum(x)

contract 在使用時,如果引數是一一對應的 (可推斷), 也可以省略引數:

  1. func Sum(type T Addable)(x []T) T {

  2.    var total T

  3.    for _, v := range x {

  4.        total += v

  5.    }

  6.    return total

  7. }

不可推斷時就需要指明該 contract 是用來約束誰的:

  1. func Keys(type K, V Equal(K))(m map[K]V) []K {

  2.    ...

  3. }

當然,下麵的寫法也可以推斷,最終如何就看 Go team 的抉擇了:

  1. func Keys(type K Equal, V)(m map[K]V) []K {

  2.    ...

  3. }

關於實現方面的內容,這裡不再討論,留給高手吧。官方開通了反饋渠道,可以去提意見,對於我來說,唯一不滿意的地方是顯式的 type關鍵字, 可能是為了方便和後邊的函式引數相區分吧。

錯誤處理

健壯的程式需要大量的錯誤處理邏輯,在極端情況下,錯誤處理邏輯甚至比業務邏輯還要多,那麼更簡潔有效的錯誤處理語法是我們所追求的。

先看下目前 Go 的錯誤處理方式,一個複製檔案的例子:

  1. func CopyFile(src, dst string) error {

  2.    r, err := os.Open(src)

  3.    if err != nil {

  4.        return fmt.Errorf("copy %s %s: %v", src, dst, err)

  5.    }

  6.    defer r.Close()

  7.    w, err := os.Create(dst)

  8.    if err != nil {

  9.        return fmt.Errorf("copy %s %s: %v", src, dst, err)

  10.    }

  11.    if _, err := io.Copy(w, r); err != nil {

  12.        w.Close()

  13.        os.Remove(dst)

  14.        return fmt.Errorf("copy %s %s: %v", src, dst, err)

  15.    }

  16.    if err := w.Close(); err != nil {

  17.        os.Remove(dst)

  18.        return fmt.Errorf("copy %s %s: %v", src, dst, err)

  19.    }

  20. }

上述程式碼中,錯誤處理的程式碼佔了總程式碼量的接近 50%!

Go 的 assignment-and-if-statement錯誤處理陳述句是罪魁禍首,草案引入了 check運算式來代替:

  1. r := check os.Open(src)

但這隻代替了賦值運算式和 if 陳述句,從之前的例子中我們可以看到,有四行完全相同的程式碼:

  1. return fmt.Errorf("copy %s %s: %v", src, dst, err)

它是可以被統一處理的, 於是 Go 在引入 check的同時引入了 handle陳述句:

  1. handle err {

  2.        return fmt.Errorf("copy %s %s: %v", src, dst, err)

  3. }

修改後的程式碼為:

  1. func CopyFile(src, dst string) error {

  2.    handle err {

  3.        return fmt.Errorf("copy %s %s: %v", src, dst, err)

  4.    }

  5.    r := check os.Open(src)

  6.    defer r.Close()

  7.    w := check os.Create(dst)

  8.    handle err {

  9.        w.Close()

  10.        os.Remove(dst) // (only if a check fails)

  11.    }

  12.    check io.Copy(w, r)

  13.    check w.Close()

  14.    return nil

  15. }

check 失敗後,先被執行最裡層的 (inner most) 的 handler,接著被上一個(按照語法順序)handler 處理,直到 handler 執行了 return陳述句。

Go team 對該草案的期望是能夠減少錯誤處理的程式碼量, 且相容之前的錯誤處理方式, 要求不算高,這個設計也算能接受吧。

反饋渠道

錯誤值

Go 的錯誤值目前存在兩個問題。一,錯誤鏈 (棧) 沒有被很好地表達;二,缺少更豐富的錯誤輸出方式。在該草案之前,已經有不少第三方的 package 實現了這些功能,現在要進行標準化。目前,對於多呼叫層級的錯誤,我們使用 fmt.Errorf 或者自定義的 Error 來包裹它:

  1. package main

  2. import (

  3.    "fmt"

  4.    "io"

  5. )

  6. type RpcError struct {

  7.    Line uint

  8. }

  9. func (s *RpcError) Error() string {

  10.    return fmt.Sprintf("(%d): no route to the remote address", s.Line)

  11. }

  12. func fn3() error {

  13.    return io.EOF

  14. }

  15. func fn2() error {

  16.    if err := fn3(); err != nil {

  17.        return &RpcError{Line: 12}

  18.    }

  19.    return nil

  20. }

  21. func fn1() error {

  22.    if err := fn2(); err != nil {

  23.        return fmt.Errorf("call fn2 failed, %s", err)

  24.    }

  25.    return nil

  26. }

  27. func main() {

  28.    if err := fn1(); err != nil {

  29.        fmt.Println(err)

  30.    }

  31. }

此程式的輸出為:

  1. call fn2 failed, (12): no route to the remote address

很明顯的問題是,我們在 main 函式裡對 error 進行處理的時候不能進行型別判斷, 比如使用 if 陳述句判斷:

  1. if err == io.EOF { ... }

或者進行型別斷言:

  1. if pe, ok := err.(*os.PathError); ok { ... pe.Path ... }

它是一個 RpcError 還是 io.EOF? 無從知曉。一大串的錯誤資訊,人類可以很好地理解,但對於程式程式碼來說就很困難。

error inspection

草案引入了一個 error wrapper 來包裹錯誤鏈, 它相當於一個指標,將錯誤棧連結起來:

  1. package errors

  2. // A Wrapper is an error implementation

  3. // wrapping context around another error.

  4. type Wrapper interface {

  5.    // Unwrap returns the next error in the error chain.

  6.    // If there is no next error, Unwrap returns nil.

  7.    Unwrap() error

  8. }

每個層級的 error 都實現這個 wrapper,這樣在 main 函式裡,我們可以透過 err.Unwrap() 來獲取下一個層級的 error。另外,草案引入了兩個函式來簡化這個過程:

  1. // Is reports whether err or any of the errors in its chain is equal to target.

  2. func Is(err, target error) bool

  3. // As checks whether err or any of the errors in its chain is a value of type E.

  4. // If so, it returns the discovered value of type E, with ok set to true.

  5. // If not, it returns the zero value of type E, with ok set to false.

  6. func As(type E)(err error) (e E, ok bool)

error formatting

有時候我們需要將錯誤資訊分類,因為某些情況下你需要所有的資訊,某些情況下只需要部分資訊,因此草案引入了一個 interface:

  1. package errors

  2. type Formatter interface {

  3.    Format(p Printer) (next error)

  4. }

error 型別可以實現 Format 函式來列印更詳細的資訊:

  1. func (e *WriteError) Format(p errors.Printer) (next error) {

  2.    p.Printf("write %s database", e.Database)

  3.    if p.Detail() {

  4.        p.Printf("more detail here")

  5.    }

  6.    return e.Err

  7. }

  8. func (e *WriteError) Error() string { return fmt.Sprint(e) }

在你使用 fmt.Println("%+v", err)列印錯誤資訊時,它會呼叫 Format 函式。

反饋渠道

贊(0)

分享創造快樂

© 2024 知識星球   網站地圖