gopl-zh.github.com/ch7/ch7-13.md

5.6 KiB
Raw Blame History

7.13. 類型開關

接口被以兩種不同的方式使用。在第一個方式中以io.Readerio.Writerfmt.Stringersort.Interfacehttp.Handler和error爲典型一個接口的方法表達了實現這個接口的具體類型間的相思性但是隱藏了代表的細節和這些具體類型本身的操作。重點在於方法上而不是具體的類型上。

第二個方式利用一個接口值可以持有各種具體類型值的能力併且將這個接口認爲是這些類型的union聯合。類型斷言用來動態地區别這些類型併且對每一種情況都不一樣。在這個方式中重點在於具體的類型滿足這個接口而不是在於接口的方法如果它確實有一些的話併且沒有任何的信息隱藏。我們將以這種方式使用的接口描述爲discriminated unions可辨識聯合

如果你熟悉面向對象編程你可能會將這兩種方式當作是subtype polymorphism子類型多態和 ad hoc polymorphism非參數多態但是你不需要去記住這些術語。對於本章剩下的部分我們將會呈現一些第二種方式的例子。

和其它那些語言一樣Go語言査詢一個SQL數據庫的API會榦淨地將査詢中固定的部分和變化的部分分開。一個調用的例子可能看起來像這樣

import "database/sql"

func listTracks(db sql.DB, artist string, minYear, maxYear int) {
    result, err := db.Exec(
        "SELECT * FROM tracks WHERE artist = ? AND ? <= year AND year <= ?",
        artist, minYear, maxYear)
    // ...
}

Exec方法使用SQL字面量替換在査詢字符串中的每個'?'SQL字面量表示相應參數的值它有可能是一個布爾值一個數字一個字符串或者nil空值。用這種方式構造査詢可以幫助避免SQL註入攻擊這種攻擊就是對手可以通過利用輸入內容中不正確的引文來控製査詢語句。在Exec函數內部我們可能會找到像下面這樣的一個函數它會將每一個參數值轉換成它的SQL字面量符號。

func sqlQuote(x interface{}) string {
    if x == nil {
        return "NULL"
    } else if _, ok := x.(int); ok {
        return fmt.Sprintf("%d", x)
    } else if _, ok := x.(uint); ok {
        return fmt.Sprintf("%d", x)
    } else if b, ok := x.(bool); ok {
        if b {
            return "TRUE"
        }
        return "FALSE"
    } else if s, ok := x.(string); ok {
        return sqlQuoteString(s) // (not shown)
    } else {
        panic(fmt.Sprintf("unexpected type %T: %v", x, x))
    }
}

switch語句可以簡化if-else鏈如果這個if-else鏈對一連串值做相等測試。一個相似的type switch類型開關可以簡化類型斷言的if-else鏈。

在它最簡單的形式中一個類型開關像普通的switch語句一樣它的運算對象是x.(type)它使用了關鍵詞字面量type併且每個case有一到多個類型。一個類型開關基於這個接口值的動態類型使一個多路分支有效。這個nil的case和if x == nil匹配併且這個default的case和如果其它case都不匹配的情況匹配。一個對sqlQuote的類型開關可能會有這些case

switch x.(type) {
    case nil:       // ...
    case int, uint: // ...
    case bool:      // ...
    case string:    // ...
    default:        // ...
}

和(§1.8)中的普通switch語句一樣每一個case會被順序的進行考慮併且當一個匹配找到時這個case中的內容會被執行。當一個或多個case類型是接口時case的順序就會變得很重要因爲可能會有兩個case同時匹配的情況。default case相對其它case的位置是無所謂的。它不會允許落空發生。

註意到在原來的函數中對於bool和string情況的邏輯需要通過類型斷言訪問提取的值。因爲這個做法很典型類型開關語句有一個擴展的形式它可以將提取的值綁定到一個在每個case范圍內的新變量。

switch x := x.(type) { /* ... */ }

這里我們已經將新的變量也命名爲x和類型斷言一樣重用變量名是很常見的。和一個switch語句相似地一個類型開關隱式的創建了一個語言塊因此新變量x的定義不會和外面塊中的x變量衝突。每一個case也會隱式的創建一個單獨的語言塊。

使用類型開關的擴展形式來重寫sqlQuote函數會讓這個函數更加的清晰

func sqlQuote(x interface{}) string {
    switch x := x.(type) {
    case nil:
        return "NULL"
    case int, uint:
        return fmt.Sprintf("%d", x) // x has type interface{} here.
    case bool:
        if x {
            return "TRUE"
        }
        return "FALSE"
    case string:
        return sqlQuoteString(x) // (not shown)
    default:
        panic(fmt.Sprintf("unexpected type %T: %v", x, x))
    }
}

在這個版本的函數中在每個單一類型的case內部變量x和這個case的類型相同。例如變量x在bool的case中是bool類型和string的case中是string類型。在所有其它的情況中變量x是switch運算對象的類型接口在這個例子中運算對象是一個interface{}。當多個case需要相同的操作時比如int和uint的情況類型開關可以很容易的合併這些情況。

盡管sqlQuote接受一個任意類型的參數但是這個函數隻會在它的參數匹配類型開關中的一個case時運行到結束其它情況的它會panic出“unexpected type”消息。雖然x的類型是interface{}但是我們把它認爲是一個intuintboolstring和nil值的discriminated union可識别聯合