Go 100 mistakes 之如何正確設定列舉值中的零值

yudotyang發表於2021-07-28

本文是對《100 Go Mistackes:How to Avoid Them》一書的翻譯。因翻譯水平有限,難免存在翻譯準確性問題,敬請諒解。 本文首發於微信公眾號 “Go 學堂”,想獲取更多資訊,請關注該公眾號。

列舉型別是由一組值組成的資料型別。在 Go 語言中,沒有 enum 這樣的關鍵字。然而,處理一組值最好的方法是用型別別名和常量。但是,我們無法達到其他語言所能達到的安全水平。這就是為什麼我們在處理列舉值時必須要小心的原因。讓我們來看一些相關的實踐以及如何避免一些常見的錯誤。

下面列出了一週中周幾的列表:

type Weekday int 

const (
    Monday Weekday = 0 
    Tuesday Weekday = 1
    Wednesday Weekday = 2
    Thursday  Weekday = 3
    Friday    Weekday = 4
    Saturday  Weekday = 5
    Sunday    Weekday = 6
)

① 定義一個自定義的 Weekday 型別

② 建立一個 Weekday 型別的 Modany 常量

建立一個 Weekday 型別的好處是可以強制讓編譯時做型別檢查以及提高可讀性。如果我們沒有建立一個 Weekday 型別,那麼下面的函式簽名對於呼叫者來說可能會有一點模糊:

func GetCurrentWeekday() int {
    // ...
}

一個 int 型別可以包含任何值,同時閱讀者如果沒有相關的閱讀文件或者程式碼的話也不能猜出該函式返回的是什麼值。相反,如果定義一個 Weekday 型別,那麼就會使該函式的簽名更清晰:

func getCurrentWeekday() Weekday {
    // ...
}

在這個例子中,我們強制指定了返回具體的型別。

我們建立 Weekday 型別的列舉值的方法是比較合適的。然而,在 Go 中,還有有一種慣用的方法來宣告列舉中的常量,那就是使用常量生成器 iota

注意:在本例中,我們還可以將 Weekday 宣告為 uint32,以強制正值並確保每個 Weekday 變數分配 32 位。

iota

iota 用於建立一系列相關值,而無需明確設定這些值。 它指示編譯器複製每個常量表示式,直到塊結束或找到賦值。

下面是用 iota 的 Weekday 版本:

type Weekday int
const (
    Monday Weekday = iota 
    Tuesday
    Wednesday
    Thursday
    Friday
    Saturday
    Sunday
)

① 使用 iota 定義列舉值

itoa 的值從 0 開始並每行增加 1。此版本等同於第一個版本:

  • Monday = 0
  • Tuesday = 1
  • Wednesday = 3
  • etc

使用 iota 允許我們避免手動定義常量值。例如,在大的列舉中手動設定常量值是會容易出錯的。進一步說,我們不用對每一個變數都重複指定 Weekday 型別:我們定義的所有變數都是一個 Weekday 型別。

注意:我們可以在更復雜的表示式中使用 iota。下面是從 Effective Go 中出現的一個關於處理 ByteSize 列舉值的例子:

type ByteSize float64
const (
  _ = iota 
  KB ByteSize = 1 << (10 * iota) 
  MB 
  GB
  TB
  PB
  EB
  ZB
  YB
)

① 通過給 _ 賦值忽略第一行的值 ② 在該行 iota 等於 1,因此 KB 被設定成 1 << (10 * 1) ③ 在這一行,iota 等於 2,本行將會重複上一行的表示式,因此 MB 被設定成了 1 << (10 * 2)

讓我們看看在 Go 的列舉中如何處理未知值(unknown values)

Unknow 值

既然我們已經理解了在 Go 中處理列舉值的原理,讓我們考慮下下面的例子。我們將實現一個 HTTP 處理以便將 JSON 格式的請求解碼成 Request 結構體型別。該結構體將會包含一個 Weekday 型別的 Unknown 值。下面是第一版本的實現:

type Weekday int 

const (
    Monday Weekday = iota
    Tuesday
    Wednesday
    Thursday
    Friday
    Saturday
    Sunday
    Unknown 
)

type Request struct { 
    ID int `json:"id"`
    Weekday Weekday `json:"weekday"`
}

func httpHandler(w http.REsponseWriter, r *http.Request) { 
    bytes, err != readBody(r) 
    if err != nil {
        w.WriteHeader(http.StatusBadRequest)
        return
    }

    var request Request
    err = json.Unmarshal(bytes, &request) 
    if err != nil {
        w.WriteHeader(http.StatusBadRequest)
        return
    }

    // Use Request
}

① 重用我們定義的 Weekday 列舉值

② 定義 Unknown 常量

③ 定義一個包含 Weekday 欄位的 Request 結構體

④ 實現一個 HTTP 處理器

⑤ 讀取請求體並返回一個 [] byte

⑥ 解碼 JSON 請求體

在這個例子中,我們建立了一個 Request 結構體,該結構體從一個 JSON 請求體中解碼而來。這段程式碼非常完整有效。在例子中,我們可以接收一個 JSON 內容並正確解碼:

{
    "id": 1234,
    "weekday": 0
}

這裡,Weekday 欄位的值會等於 0:Monday。

現在,如果在 JSON 內容中不包含 weekday 欄位會怎麼樣呢?

{
    "Id": 1235
}

解析該內容的時候將不會引起任何錯誤。然而,在 Request 結構體中的 weekday 欄位值將會被設定成一個 int 型別:0 值。因此,就像是在上次請求中的 Monday。

那我們應該如何區分請求中是傳遞的 Monday 還是沒有就沒有傳遞 weekday 欄位呢?這個問題和我們定義 Weekday 列舉的方式有關。實際上,Unknown 是列舉值的最後一個值。因此,它的值應該等於 7.

為了解決該問題,處理一個 unknown 的列舉值的最好的實踐方法是將它設定成 0(int 型別的零值)。因此,我們應該按如下方式生命 Weekday 列舉值:

type Weekday int

const (
    Unknown Weekday = iota 
    Monday
    Tuesday
    Wednesday
    Thursday
    Friday
    Saturday
    Sunday
)

① Unknow 現在等於 0 了

如果 JSON 請求體中的 weekday 的值是空,那將會被解析成 Unknown;這就是我們所需要的。

根據經驗,列舉的未知值應該設定為列舉型別的零值。這樣,我們就可以區分出顯示值和缺失值了。

更多原創文章乾貨分享,請關注公眾號
  • Go 100 mistakes 之如何正確設定列舉值中的零值
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章