Go 語言 通過類型斷言詢問行為

2023-03-14 16:55 更新

原文鏈接:https://gopl-zh.github.io/ch7/ch7-12.html


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

下面這段邏輯和net/http包中web服務(wù)器負(fù)責(zé)寫入HTTP頭字段(例如:"Content-type:text/html")的部分相似。io.Writer接口類型的變量w代表HTTP響應(yīng);寫入它的字節(jié)最終被發(fā)送到某個人的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(...)進(jìn)行轉(zhuǎn)換。這個轉(zhuǎn)換分配內(nèi)存并且做一個拷貝,但是這個拷貝在轉(zhuǎn)換后幾乎立馬就被丟棄掉。讓我們假裝這是一個web服務(wù)器的核心部分并且我們的性能分析表示這個內(nèi)存分配使服務(wù)器的速度變慢。這里我們可以避免掉內(nèi)存分配么?

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

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

// 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
    }
    // ...
}

為了避免重復(fù)定義,我們將這個檢查移入到一個實用工具函數(shù)writeString中,但是它太有用了以致于標(biāo)準(zhǔn)庫將它作為io.WriteString函數(shù)提供。這是向一個io.Writer接口寫入字符串的推薦方法。

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

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

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

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

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

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滿足這兩個接口類型中的一個,具體滿足的接口決定對值的格式化方式。如果都不滿足,默認(rèn)的case或多或少會統(tǒng)一地使用反射來處理所有的其它類型;我們可以在第12章知道具體是怎么實現(xiàn)的。

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



以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號