Go 標準庫中的一個設計敗筆:哨兵錯誤

大家好,我是煎魚。

在 Go 的歷史發展中,總是有或多或少的坑。最近遇到一個跟錯誤類型定義和聲明使用有關的小坑。

翻了一圈 Go 社區裏的爭論,發現又是一個暫時無法解決的未解之坑。

今天分享給大家,平時開發時也可以給自己避避坑,以免有人亂用。

快速背景

在 Go 裏有一種錯誤類型的定義,官方叫做哨兵錯誤(Sentinel errors):

哨兵錯誤,常用於在程序中與全局變量的值對比。

可以參考最常見的 os 標準庫,Go 官方自己在標準庫內就是如此定義。

如下代碼:

package os
...

var (
 ErrInvalid = fs.ErrInvalid // "invalid argument"
 ErrPermission = fs.ErrPermission // "permission denied"
 ErrExist      = fs.ErrExist      // "file already exists"
 ErrNotExist   = fs.ErrNotExist   // "file does not exist"
 ErrClosed     = fs.ErrClosed     // "file already closed"

這種寫法有個非常大的問題,這些全局變量可以被被用戶直接進行重新賦值和分配,破壞掉原有的值。

如下 “暴力” 代碼:

package main

import (
 "fmt"
 "os"
)

func main() {
 var nilFile *os.File

 // 原本的運行結果:prints 0, error("invalid argument")
 fmt.Println(nilFile.Read(nil))

 // 破壞這個哨兵錯誤的值,以便於後面引發問題
 os.ErrInvalid = nil

 // 由於重新設置值後,繞過了內部錯誤檢查而導致程序出問題
 fmt.Println(nilFile.Read(nil))
}

這要是誰在程序裏一個不小心變更或者埋個坑,那就真的是很莫名其妙了。排查的時候也很難受。真的是直呼無語了。

社區建議

用常量定義錯誤

於是社區裏有個小夥伴 @myaaaaaaaaa 就提出了一個新的方案,將錯誤類型用常量重新定義,改造一下,希望能夠解決這個問題。

用常量類型改造,如下代碼:

package os

type osError string

func (err osError) Error() string {
 return string(err)
}

const (
 ErrInvalid    = osError("invalid argument")
 ErrPermission = osError("permission denied")
 ErrExist      = osError("file already exists")
 ErrNotExist   = osError("file does not exist")
 ErrClosed     = osError("file already closed")
)

常量不能重新賦值,可以解決前面提到的 var 定義的全局變量被用戶亂改,進而影響標準庫內程序運行的情況。

用結構體類型定義錯誤

社區內進而也有 @Jorropo 提出了用結構體類型來解決這個問題會更好一些。

如下代碼:

type errInvalid struct{}

func (errInvalid) Error() string {
 return "invalid argument"
}

type errPermission struct{}

func (errPermission) Error() string {
 return "permission denied"
}

type errExist struct{}

func (errExist) Error() string {
 return "file already exists"
}

type errNotExist struct{}

func (errNotExist) Error() string {
 return "file does not exist"
}

type errClosed struct{}

func (errClosed) Error() string {
 return "file already closed"
}

var (
 ErrInvalid    errInvalid
 ErrPermission errPermission
 ErrExist      errExist
 ErrNotExist   errNotExist
 ErrClosed     errClosed
)

官方答覆

Go 核心團隊整體上是認同在標準庫的設計中哨兵錯誤是會帶來明確的問題的。

但很可惜 Go 核心團隊成員 @Ian Lance Taylor 依舊錶示:“謝謝,這是個好主意。但我們現在不能做這樣的改動,因爲這會破壞 Go1 的兼容性保證”

煎魚注:千言萬語,標準的好人卡。

總結

這在 Go 裏算是一個或大或小的 “難言之隱”,因爲 Go 核心團隊是經常在一些設計領域上是標榜要較爲嚴格和顯式的,但是在哨兵錯誤定義這塊早期就翻了個車。

(從後續表述來看,也不是故意爲了允許修改全局變量而這麼寫的)

同時確實是變更寫法的話,會明顯違反 Go1 兼容性保障,這相當於是手心手背都是肉了。兼容性,有好有壞。

這問題最早在幾年前撕泛型設計時我就見過了。期望未來也基於 rsc 延伸的向前向後的兼容性擴展來靈活應對這個問題了。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/SG58FiEgs-9Q1jHxpDSx9Q