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中約定的行爲,這個行爲會返迴一個適合打印的字符串。