## 7.12. 通過類型斷言詢問行為
下面這段邏輯和net/http包中web服務器負責寫入HTTP頭字段(例如:"Content-type:text/html)的部分相似。io.Writer接口類型的變量w代表HTTP響應;寫入它的字節最終被發送到某個人的web瀏覽器上。
```go
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的動態類型滿足這個新接口。
```go
// 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))有相同的效果。
```go
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內部,有一個將單個操作對象轉換成一個字符串的步驟,像下面這樣:
```go
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中約定的行為,這個行為會返回一個適合打印的字符串。
- 前言
- Go語言起源
- Go語言項目
- 本書的組織
- 更多的信息
- 致謝
- 入門
- Hello, World
- 命令行參數
- 查找重復的行
- GIF動畫
- 獲取URL
- 并發獲取多個URL
- Web服務
- 本章要點
- 程序結構
- 命名
- 聲明
- 變量
- 賦值
- 類型
- 包和文件
- 作用域
- 基礎數據類型
- 整型
- 浮點數
- 復數
- 布爾型
- 字符串
- 常量
- 復合數據類型
- 數組
- Slice
- Map
- 結構體
- JSON
- 文本和HTML模板
- 函數
- 函數聲明
- 遞歸
- 多返回值
- 錯誤
- 函數值
- 匿名函數
- 可變參數
- Deferred函數
- Panic異常
- Recover捕獲異常
- 方法
- 方法聲明
- 基于指針對象的方法
- 通過嵌入結構體來擴展類型
- 方法值和方法表達式
- 示例: Bit數組
- 封裝
- 接口
- 接口是合約
- 接口類型
- 實現接口的條件
- flag.Value接口
- 接口值
- sort.Interface接口
- http.Handler接口
- error接口
- 示例: 表達式求值
- 類型斷言
- 基于類型斷言識別錯誤類型
- 通過類型斷言查詢接口
- 類型分支
- 示例: 基于標記的XML解碼
- 補充幾點
- Goroutines和Channels
- Goroutines
- 示例: 并發的Clock服務
- 示例: 并發的Echo服務
- Channels
- 并發的循環
- 示例: 并發的Web爬蟲
- 基于select的多路復用
- 并發的退出
- 示例: 聊天服務
- 基于共享變量的并發
- 競爭條件
- sync.Mutex互斥鎖
- sync.RWMutex讀寫鎖
- 內存同步
- 競爭條件檢測
- 示例: 并發的非阻塞緩存
- Goroutines和線程
- 包和工具
- 包簡介
- 導入路徑
- 包聲明
- 導入聲明
- 包的匿名導入
- 包和命名
- 工具
- 測試
- go test
- 測試函數
- 測試覆蓋率
- 基準測試
- 剖析
- 示例函數
- 反射
- 為何需要反射?
- reflect.Type和reflect.Value
- Display遞歸打印
- 示例: 編碼S表達式
- 通過reflect.Value修改值
- 示例: 解碼S表達式
- 顯示一個類型的方法集
- 幾點忠告
- 底層編程
- unsafe.Sizeof, Alignof 和 Offsetof
- unsafe.Pointer
- 示例: 深度相等判斷
- 通過cgo調用C代碼
- 幾點忠告
- 附錄
- 附錄A:原文勘誤
- 附錄B:作者譯者
- 附錄C:譯文授權
- 附錄D:其它語言