前言
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:
package main
import "fmt"
func Keys(m map[interface{}]interface{}) []interface{} {
keys := make([]interface{}, 0)
for k, _ := range m {
keys = append(keys, k)
}
return keys
}
func main() {
m := make(map[string]string, 1)
m["demo"] = "data"
fmt.Println(Keys(m))
}
這樣寫連編譯都透過不了,因為型別不匹配。那麼參考其他支援泛型的語言的語法,可以這樣寫:
package main
import "fmt"
func Keys<K, V>(m map[K]V) []K {
keys := make([]K, 0)
for k, _ := range m {
keys = append(keys, k)
}
return keys
}
func main() {
m := make(map[string]string, 1)
m["demo"] = "data"
fmt.Println(Keys(m))
}
但是這種寫法是有缺陷的,假設 append 函式並不支援 string 型別,就可能會出現編譯錯誤。我們可以看下其他語言的做法:
// rust
fn print_g<T: Graph>(g : T) {
println!("graph area {}", g.area());
}
Rust 在宣告 T 的時候,限定了入參的型別,即入參 g 必須是 Graph 的子類。和 Rust 的 nominal subtyping 不同,Go 屬於 structural subtyping,沒有顯式的型別關係宣告,因此不能使用此種方式。Go 在草案中引入了 contract
來解決這個問題,語法類似於函式, 寫法更複雜,但表達能力比 Rust 要更強:
// comparable contract
contract Equal(t T) {
t == t
}
// addable contract
contract Addable(t T) {
t + t
}
上述程式碼分別約束了 T 必須是可比較的 (comparable),必須是能做加法運算(addable) 的。使用方式很簡單, 定義函式的時候加上約束即可:
func Sum(type T Addable(T))(x []T) T {
var total T
for _, v := range x {
total += v
}
return total
}
var x []int
total := Sum(int)(x)
得益於型別推斷,在呼叫 Sum 時可以簡寫成:
total := Sum(x)
contract 在使用時,如果引數是一一對應的 (可推斷), 也可以省略引數:
func Sum(type T Addable)(x []T) T {
var total T
for _, v := range x {
total += v
}
return total
}
不可推斷時就需要指明該 contract 是用來約束誰的:
func Keys(type K, V Equal(K))(m map[K]V) []K {
...
}
當然,下麵的寫法也可以推斷,最終如何就看 Go team 的抉擇了:
func Keys(type K Equal, V)(m map[K]V) []K {
...
}
關於實現方面的內容,這裡不再討論,留給高手吧。官方開通了反饋渠道,可以去提意見,對於我來說,唯一不滿意的地方是顯式的 type
關鍵字, 可能是為了方便和後邊的函式引數相區分吧。
錯誤處理
健壯的程式需要大量的錯誤處理邏輯,在極端情況下,錯誤處理邏輯甚至比業務邏輯還要多,那麼更簡潔有效的錯誤處理語法是我們所追求的。
先看下目前 Go 的錯誤處理方式,一個複製檔案的例子:
func CopyFile(src, dst string) error {
r, err := os.Open(src)
if err != nil {
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
defer r.Close()
w, err := os.Create(dst)
if err != nil {
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
if _, err := io.Copy(w, r); err != nil {
w.Close()
os.Remove(dst)
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
if err := w.Close(); err != nil {
os.Remove(dst)
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
}
上述程式碼中,錯誤處理的程式碼佔了總程式碼量的接近 50%!
Go 的 assignment-and-if-statement
錯誤處理陳述句是罪魁禍首,草案引入了 check
運算式來代替:
r := check os.Open(src)
但這隻代替了賦值運算式和 if 陳述句,從之前的例子中我們可以看到,有四行完全相同的程式碼:
return fmt.Errorf("copy %s %s: %v", src, dst, err)
它是可以被統一處理的, 於是 Go 在引入 check
的同時引入了 handle
陳述句:
handle err {
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
修改後的程式碼為:
func CopyFile(src, dst string) error {
handle err {
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
r := check os.Open(src)
defer r.Close()
w := check os.Create(dst)
handle err {
w.Close()
os.Remove(dst) // (only if a check fails)
}
check io.Copy(w, r)
check w.Close()
return nil
}
check 失敗後,先被執行最裡層的 (inner most) 的 handler,接著被上一個(按照語法順序)handler 處理,直到 handler 執行了 return
陳述句。
Go team 對該草案的期望是能夠減少錯誤處理的程式碼量, 且相容之前的錯誤處理方式, 要求不算高,這個設計也算能接受吧。
反饋渠道
錯誤值
Go 的錯誤值目前存在兩個問題。一,錯誤鏈 (棧) 沒有被很好地表達;二,缺少更豐富的錯誤輸出方式。在該草案之前,已經有不少第三方的 package 實現了這些功能,現在要進行標準化。目前,對於多呼叫層級的錯誤,我們使用 fmt.Errorf 或者自定義的 Error 來包裹它:
package main
import (
"fmt"
"io"
)
type RpcError struct {
Line uint
}
func (s *RpcError) Error() string {
return fmt.Sprintf("(%d): no route to the remote address", s.Line)
}
func fn3() error {
return io.EOF
}
func fn2() error {
if err := fn3(); err != nil {
return &RpcError{Line: 12}
}
return nil
}
func fn1() error {
if err := fn2(); err != nil {
return fmt.Errorf("call fn2 failed, %s", err)
}
return nil
}
func main() {
if err := fn1(); err != nil {
fmt.Println(err)
}
}
此程式的輸出為:
call fn2 failed, (12): no route to the remote address
很明顯的問題是,我們在 main 函式裡對 error 進行處理的時候不能進行型別判斷, 比如使用 if 陳述句判斷:
if err == io.EOF { ... }
或者進行型別斷言:
if pe, ok := err.(*os.PathError); ok { ... pe.Path ... }
它是一個 RpcError 還是 io.EOF? 無從知曉。一大串的錯誤資訊,人類可以很好地理解,但對於程式程式碼來說就很困難。
error inspection
草案引入了一個 error wrapper 來包裹錯誤鏈, 它相當於一個指標,將錯誤棧連結起來:
package errors
// A Wrapper is an error implementation
// wrapping context around another error.
type Wrapper interface {
// Unwrap returns the next error in the error chain.
// If there is no next error, Unwrap returns nil.
Unwrap() error
}
每個層級的 error 都實現這個 wrapper,這樣在 main 函式裡,我們可以透過 err.Unwrap() 來獲取下一個層級的 error。另外,草案引入了兩個函式來簡化這個過程:
// Is reports whether err or any of the errors in its chain is equal to target.
func Is(err, target error) bool
// As checks whether err or any of the errors in its chain is a value of type E.
// If so, it returns the discovered value of type E, with ok set to true.
// If not, it returns the zero value of type E, with ok set to false.
func As(type E)(err error) (e E, ok bool)
error formatting
有時候我們需要將錯誤資訊分類,因為某些情況下你需要所有的資訊,某些情況下只需要部分資訊,因此草案引入了一個 interface:
package errors
type Formatter interface {
Format(p Printer) (next error)
}
error 型別可以實現 Format 函式來列印更詳細的資訊:
func (e *WriteError) Format(p errors.Printer) (next error) {
p.Printf("write %s database", e.Database)
if p.Detail() {
p.Printf("more detail here")
}
return e.Err
}
func (e *WriteError) Error() string { return fmt.Sprint(e) }
在你使用 fmt.Println("%+v", err)
列印錯誤資訊時,它會呼叫 Format 函式。
反饋渠道