7.12. 通過類型斷言詢問行為

下面這段邏輯和net/http包中web服務器負責寫入HTTP頭字段(例如:"Content-type:text/html")的部分相似。io.Writer接口類型的變量w代表HTTP響應;寫入它的字節最終被髮送到某個人的web瀏覽器上。

func writeHeader(w io.Writer, contentType string) error {
    if _, err := w.Write([]byte("Content-Type: ")); err != nil {
        return err
    }
    if _, err := w.Write([]byte(contentType)); err != nil {
        return err
    }
    // ...
}

因為Write方法需要傳入一個byte切片而我們希望寫入的值是一個字符串,所以我們需要使用[]byte(...)進行轉換。這個轉換分配內存並且做一個拷貝,但是這個拷貝在轉換後幾乎立馬就被丟棄掉。讓我們假裝這是一個web服務器的核心部分並且我們的性能分析表示這個內存分配使服務器的速度變慢。這裡我們可以避免掉內存分配麼?

這個io.Writer接口告訴我們關於w持有的具體類型的唯一東西:就是可以向它寫入字節切片。如果我們回顧net/http包中的內幕,我們知道在這個程序中的w變量持有的動態類型也有一個允許字符串高效寫入的WriteString方法;這個方法會避免去分配一個臨時的拷貝。(這可能像在黑夜中射擊一樣,但是許多滿足io.Writer接口的重要類型同時也有WriteString方法,包括*bytes.Buffer*os.File*bufio.Writer。)

我們不能對任意io.Writer類型的變量w,假設它也擁有WriteString方法。但是我們可以定義一個只有這個方法的新接口並且使用類型斷言來檢測是否w的動態類型滿足這個新接口。

// writeString writes s to w.
// If w has a WriteString method, it is invoked instead of w.Write.
func writeString(w io.Writer, s string) (n int, err error) {
    type stringWriter interface {
        WriteString(string) (n int, err error)
    }
    if sw, ok := w.(stringWriter); ok {
        return sw.WriteString(s) // avoid a copy
    }
    return w.Write([]byte(s)) // allocate temporary copy
}

func writeHeader(w io.Writer, contentType string) error {
    if _, err := writeString(w, "Content-Type: "); err != nil {
        return err
    }
    if _, err := writeString(w, contentType); err != nil {
        return err
    }
    // ...
}

為了避免重複定義,我們將這個檢查移入到一個實用工具函數writeString中,但是它太有用了以致於標準庫將它作為io.WriteString函數提供。這是向一個io.Writer接口寫入字符串的推薦方法。

這個例子的神奇之處在於,沒有定義了WriteString方法的標準接口,也沒有指定它是一個所需行為的標準接口。一個具體類型只會通過它的方法決定它是否滿足stringWriter接口,而不是任何它和這個接口類型所表達的關係。它的意思就是上面的技術依賴於一個假設,這個假設就是:如果一個類型滿足下面的這個接口,然後WriteString(s)方法就必須和Write([]byte(s))有相同的效果。

interface {
    io.Writer
    WriteString(s string) (n int, err error)
}

儘管io.WriteString實施了這個假設,但是調用它的函數極少可能會去實施類似的假設。定義一個特定類型的方法隱式地獲取了對特定行為的協約。對於Go語言的新手,特別是那些來自有強類型語言使用背景的新手,可能會發現它缺乏顯式的意圖令人感到混亂,但是在實戰的過程中這幾乎不是一個問題。除了空接口interface{},接口類型很少意外巧合地被實現。

上面的writeString函數使用一個類型斷言來獲知一個普遍接口類型的值是否滿足一個更加具體的接口類型;並且如果滿足,它會使用這個更具體接口的行為。這個技術可以被很好的使用,不論這個被詢問的接口是一個標準如io.ReadWriter,或者用戶定義的如stringWriter接口。

這也是fmt.Fprintf函數怎麼從其它所有值中區分滿足error或者fmt.Stringer接口的值。在fmt.Fprintf內部,有一個將單個操作對象轉換成一個字符串的步驟,像下面這樣:

package fmt

func formatOneValue(x interface{}) string {
    if err, ok := x.(error); ok {
        return err.Error()
    }
    if str, ok := x.(Stringer); ok {
        return str.String()
    }
    // ...all other types...
}

如果x滿足這兩個接口類型中的一個,具體滿足的接口決定對值的格式化方式。如果都不滿足,默認的case或多或少會統一地使用反射來處理所有的其它類型;我們可以在第12章知道具體是怎麼實現的。

再一次的,它假設任何有String方法的類型都滿足fmt.Stringer中約定的行為,這個行為會返回一個適合打印的字符串。