鍍金池/ 教程/ GO/ 11.12 接口與動(dòng)態(tài)類型
4.7 strings 和 strconv 包
13.6 啟動(dòng)外部命令和程序
?# 11.4 類型判斷:type-switch
12.1 讀取用戶的輸入
10.6 方法
12.2 文件讀寫
13 錯(cuò)誤處理與測試
9.3 鎖和 sync 包
12.3 文件拷貝
?# 11.7 第一個(gè)例子:使用 Sorter 接口排序
?# 11.5 測試一個(gè)值是否實(shí)現(xiàn)了某個(gè)接口
6.4 defer 和追蹤
12.10 XML 數(shù)據(jù)格式
13.10 性能調(diào)試:分析并優(yōu)化 Go 程序
?# 11.1 接口是什么
2.2 Go 環(huán)境變量
2.6 安裝目錄清單
2.5 在 Windows 上安裝 Go
11.11 Printf 和反射
1.2 語言的主要特性與發(fā)展的環(huán)境和影響因素
9.0 包(package)
7.4 切片重組(reslice)
13.2 運(yùn)行時(shí)異常和 panic
10.2 使用工廠方法創(chuàng)建結(jié)構(gòu)體實(shí)例
12.8 使用接口的實(shí)際例子:fmt.Fprintf
2.4 在 Mac OS X 上安裝 Go
3.8 Go 性能說明
7.2 切片
8.0 Map
3.1 Go 開發(fā)環(huán)境的基本要求
5.6 標(biāo)簽與 goto
6.10 使用閉包調(diào)試
9.5 自定義包和可見性
4.3 常量
?# 11.2 接口嵌套接口
6.5 內(nèi)置函數(shù)
前言
10.8 垃圾回收和 SetFinalizer
2.8 Go 解釋器
13.7 Go 中的單元測試和基準(zhǔn)測試
6.8 閉包
4.9 指針
13.1 錯(cuò)誤處理
10.1 結(jié)構(gòu)體定義
5.1 if-else 結(jié)構(gòu)
6.6 遞歸函數(shù)
9.9 通過 Git 打包和安裝
2.7 Go 運(yùn)行時(shí)(runtime)
10.7 類型的 String() 方法和格式化描述符
3.7 其它工具
9.6 為自定義包使用 godoc
11.12 接口與動(dòng)態(tài)類型
13.3 從 panic 中恢復(fù)(Recover)
10.3 使用自定義包中的結(jié)構(gòu)體
11.14 結(jié)構(gòu)體、集合和高階函數(shù)
3.6 生成代碼文檔
9.2 regexp 包
4.1 文件名、關(guān)鍵字與標(biāo)識(shí)符
?# 11.6 使用方法集與接口
7.0 數(shù)組與切片
7.1 聲明和初始化
12.11 用 Gob 傳輸數(shù)據(jù)
5.5 Break 與 continue
1.1 起源與發(fā)展
?# 11 接口(Interfaces)與反射(reflection)
6.9 應(yīng)用閉包:將函數(shù)作為返回值
4.2 Go 程序的基本結(jié)構(gòu)和要素
8.6 將 map 的鍵值對(duì)調(diào)
6.11 計(jì)算函數(shù)執(zhí)行時(shí)間
5.0 控制結(jié)構(gòu)
10.5 匿名字段和內(nèi)嵌結(jié)構(gòu)體
4.6 字符串
3.0 編輯器、集成開發(fā)環(huán)境與其它工具
13.8 測試的具體例子
7.6 字符串、數(shù)組和切片的應(yīng)用
8.4 map 類型的切片
3.9 與其它語言進(jìn)行交互
7.3 For-range 結(jié)構(gòu)
9.7 使用 go install 安裝自定義包
6.0 函數(shù)
9.8 自定義包的目錄結(jié)構(gòu)、go install 和 go test
6.3 傳遞變長參數(shù)
13.9 用(測試數(shù)據(jù))表驅(qū)動(dòng)測試
11.9 空接口
8.1 聲明、初始化和 make
6.2 函數(shù)參數(shù)與返回值
9.11 在 Go 程序中使用外部庫
3.3 調(diào)試器
4.5 基本類型和運(yùn)算符
?# 11.8 第二個(gè)例子:讀和寫
12.5 用 buffer 讀取文件
總結(jié):Go 中的面向?qū)ο?/span>
11.10 反射包
12.7 用 defer 關(guān)閉文件
9.4 精密計(jì)算和 big 包
4.4 變量
6.1 介紹
13.4 自定義包中的錯(cuò)誤處理和 panicking
12.4 從命令行讀取參數(shù)
9.10 Go 的外部包和項(xiàng)目
8.3 for-range 的配套用法
3.5 格式化代碼
10.4 帶標(biāo)簽的結(jié)構(gòu)體
7.5 切片的復(fù)制與追加
?# 11.3 類型斷言:如何檢測和轉(zhuǎn)換接口變量的類型
5.4 for 結(jié)構(gòu)
4.8 時(shí)間和日期
2.3 在 Linux 上安裝 Go
12 讀寫數(shù)據(jù)
6.12 通過內(nèi)存緩存來提升性能
9.1 標(biāo)準(zhǔn)庫概述
12.6 用切片讀寫文件
10 結(jié)構(gòu)(struct)與方法(method)
8.5 map 的排序
12.9 JSON 數(shù)據(jù)格式
13.5 一種用閉包處理錯(cuò)誤的模式
3.2 編輯器和集成開發(fā)環(huán)境
12.12 Go 中的密碼學(xué)
5.2 測試多返回值函數(shù)的錯(cuò)誤
6.7 將函數(shù)作為參數(shù)
8.2 測試鍵值對(duì)是否存在及刪除元素
3.4 構(gòu)建并運(yùn)行 Go 程序
2.1 平臺(tái)與架構(gòu)
5.3 switch 結(jié)構(gòu)

11.12 接口與動(dòng)態(tài)類型

11.12.1 Go 的動(dòng)態(tài)類型

在經(jīng)典的面向?qū)ο笳Z言(像 C++,Java 和 C#)中數(shù)據(jù)和方法被封裝為 的概念:類包含它們兩者,并且不能剝離。

Go 沒有類:數(shù)據(jù)(結(jié)構(gòu)體或更一般的類型)和方法是一種松耦合的正交關(guān)系。

Go 中的接口跟 Java/C# 類似:都是必須提供一個(gè)指定方法集的實(shí)現(xiàn)。但是更加靈活通用:任何提供了接口方法實(shí)現(xiàn)代碼的類型都隱式地實(shí)現(xiàn)了該接口,而不用顯式地聲明。

和其它語言相比,Go 是唯一結(jié)合了接口值,靜態(tài)類型檢查(是否該類型實(shí)現(xiàn)了某個(gè)接口),運(yùn)行時(shí)動(dòng)態(tài)轉(zhuǎn)換的語言,并且不需要顯式地聲明類型是否滿足某個(gè)接口。該特性允許我們?cè)诓桓淖円延械拇a的情況下定義和使用新接口。

接收一個(gè)(或多個(gè))接口類型作為參數(shù)的函數(shù),其實(shí)參可以是任何實(shí)現(xiàn)了該接口的類型。 實(shí)現(xiàn)了某個(gè)接口的類型可以被傳給任何以此接口為參數(shù)的函數(shù) 。

類似于 Python 和 Ruby 這類動(dòng)態(tài)語言中的 動(dòng)態(tài)類型(duck typing);這意味著對(duì)象可以根據(jù)提供的方法被處理(例如,作為參數(shù)傳遞給函數(shù)),而忽略它們的實(shí)際類型:它們能做什么比它們是什么更重要。

這在程序 duck_dance.go 中得以闡明,函數(shù) DuckDance 接受一個(gè) IDuck 接口類型變量。僅當(dāng) DuckDance 被實(shí)現(xiàn)了 IDuck 接口的類型調(diào)用時(shí)程序才能編譯通過。

示例 11.16 duck_dance.go

package main

import "fmt"

type IDuck interface {
    Quack()
    Walk()
}

func DuckDance(duck IDuck) {
    for i := 1; i <= 3; i++ {
        duck.Quack()
        duck.Walk()
    }
}

type Bird struct {
    // ...
}

func (b *Bird) Quack() {
    fmt.Println("I am quacking!")
}

func (b *Bird) Walk()  {
    fmt.Println("I am walking!")
}

func main() {
    b := new(Bird)
    DuckDance(b)
}

輸出:

I am quacking!
I am walking!
I am quacking!
I am walking!
I am quacking!
I am walking!

如果 Bird 沒有實(shí)現(xiàn) Walk()(把它注釋掉),會(huì)得到一個(gè)編譯錯(cuò)誤:

cannot use b (type *Bird) as type IDuck in function argument:
*Bird does not implement IDuck (missing Walk method)

如果對(duì) cat 調(diào)用函數(shù) DuckDance(),Go 會(huì)提示編譯錯(cuò)誤,但是 Python 和 Ruby 會(huì)以運(yùn)行時(shí)錯(cuò)誤結(jié)束。

11.12.2 動(dòng)態(tài)方法調(diào)用

像 Python,Ruby 這類語言,動(dòng)態(tài)類型是延遲綁定的(在運(yùn)行時(shí)進(jìn)行):方法只是用參數(shù)和變量簡單地調(diào)用,然后在運(yùn)行時(shí)才解析(它們很可能有像 responds_to 這樣的方法來檢查對(duì)象是否可以響應(yīng)某個(gè)方法,但是這也意味著更大的編碼量和更多的測試工作)

Go 的實(shí)現(xiàn)與此相反,通常需要編譯器靜態(tài)檢查的支持:當(dāng)變量被賦值給一個(gè)接口類型的變量時(shí),編譯器會(huì)檢查其是否實(shí)現(xiàn)了該接口的所有函數(shù)。如果方法調(diào)用作用于像 interface{} 這樣的“泛型”上,你可以通過類型斷言(參見 11.3 節(jié))來檢查變量是否實(shí)現(xiàn)了相應(yīng)接口。

例如,你用不同的類型表示 XML 輸出流中的不同實(shí)體。然后我們?yōu)?XML 定義一個(gè)如下的“寫”接口(甚至可以把它定義為私有接口):

type xmlWriter interface {
    WriteXML(w io.Writer) error
}

現(xiàn)在我們可以實(shí)現(xiàn)適用于該流類型的任何變量的 StreamXML 函數(shù),并用類型斷言檢查傳入的變量是否實(shí)現(xiàn)了該接口;如果沒有,我們就調(diào)用內(nèi)建的 encodeToXML 來完成相應(yīng)工作:

// Exported XML streaming function.
func StreamXML(v interface{}, w io.Writer) error {
    if xw, ok := v.(xmlWriter); ok {
        // It’s an  xmlWriter, use method of asserted type.
        return xw.WriteXML(w)
    }
    // No implementation, so we have to use our own function (with perhaps reflection):
    return encodeToXML(v, w)
}

// Internal XML encoding function.
func encodeToXML(v interface{}, w io.Writer) error {
    // ...
}

Go 在這里用了和 gob 相同的機(jī)制:定義了兩個(gè)接口 GobEncoderGobDecoder。這樣就允許類型自己實(shí)現(xiàn)從流編解碼的具體方式;如果沒有實(shí)現(xiàn)就使用標(biāo)準(zhǔn)的反射方式。

因此 Go 提供了動(dòng)態(tài)語言的優(yōu)點(diǎn),卻沒有其他動(dòng)態(tài)語言在運(yùn)行時(shí)可能發(fā)生錯(cuò)誤的缺點(diǎn)。

對(duì)于動(dòng)態(tài)語言非常重要的單元測試來說,這樣即可以減少單元測試的部分需求,又可以發(fā)揮相當(dāng)大的作用。

Go 的接口提高了代碼的分離度,改善了代碼的復(fù)用性,使得代碼開發(fā)過程中的設(shè)計(jì)模式更容易實(shí)現(xiàn)。用 Go 接口還能實(shí)現(xiàn) 依賴注入模式。

11.12.3 接口的提取

提取接口 是非常有用的設(shè)計(jì)模式,可以減少需要的類型和方法數(shù)量,而且不需要像傳統(tǒng)的基于類的面向?qū)ο笳Z言那樣維護(hù)整個(gè)的類層次結(jié)構(gòu)。

Go 接口可以讓開發(fā)者找出自己寫的程序中的類型。假設(shè)有一些擁有共同行為的對(duì)象,并且開發(fā)者想要抽象出這些行為,這時(shí)就可以創(chuàng)建一個(gè)接口來使用。 我們來擴(kuò)展 11.1 節(jié)的示例 11.2 interfaces_poly.go,假設(shè)我們需要一個(gè)新的接口 TopologicalGenus,用來給 shape 排序(這里簡單地實(shí)現(xiàn)為返回 int)。我們需要做的是給想要滿足接口的類型實(shí)現(xiàn) Rank() 方法:

示例 11.17 multi_interfaces_poly.go

//multi_interfaces_poly.go
package main

import "fmt"

type Shaper interface {
    Area() float32
}

type TopologicalGenus interface {
    Rank() int
}

type Square struct {
    side float32
}

func (sq *Square) Area() float32 {
    return sq.side * sq.side
}

func (sq *Square) Rank() int {
    return 1
}

type Rectangle struct {
    length, width float32
}

func (r Rectangle) Area() float32 {
    return r.length * r.width
}

func (r Rectangle) Rank() int {
    return 2
}

func main() {
    r := Rectangle{5, 3} // Area() of Rectangle needs a value
    q := &Square{5}      // Area() of Square needs a pointer
    shapes := []Shaper{r, q}
    fmt.Println("Looping through shapes for area ...")
    for n, _ := range shapes {
        fmt.Println("Shape details: ", shapes[n])
        fmt.Println("Area of this shape is: ", shapes[n].Area())
    }
    topgen := []TopologicalGenus{r, q}
    fmt.Println("Looping through topgen for rank ...")
    for n, _ := range topgen {
        fmt.Println("Shape details: ", topgen[n])
        fmt.Println("Topological Genus of this shape is: ", topgen[n].Rank())
    }
}

輸出:

Looping through shapes for area ...
Shape details:  {5 3}
Area of this shape is:  15
Shape details:  &{5}
Area of this shape is:  25
Looping through topgen for rank ...
Shape details:  {5 3}
Topological Genus of this shape is:  2
Shape details:  &{5}
Topological Genus of this shape is:  1

所以你不用提前設(shè)計(jì)出所有的接口;整個(gè)設(shè)計(jì)可以持續(xù)演進(jìn),而不用廢棄之前的決定。類型要實(shí)現(xiàn)某個(gè)接口,它本身不用改變,你只需要在這個(gè)類型上實(shí)現(xiàn)新的方法。

11.12.4 顯式地指明類型實(shí)現(xiàn)了某個(gè)接口

如果你希望滿足某個(gè)接口的類型顯式地聲明它們實(shí)現(xiàn)了這個(gè)接口,你可以向接口的方法集中添加一個(gè)具有描述性名字的方法。例如:

type Fooer interface {
    Foo()
    ImplementsFooer()
}

類型 Bar 必須實(shí)現(xiàn) ImplementsFooer 方法來滿足 Fooer 接口,以清楚地記錄這個(gè)事實(shí)。

type Bar struct{}
func (b Bar) ImplementsFooer() {}
func (b Bar) Foo() {}

大部分代碼并不使用這樣的約束,因?yàn)樗拗屏私涌诘膶?shí)用性。

但是有些時(shí)候,這樣的約束在大量相似的接口中被用來解決歧義。

11.12.5 空接口和函數(shù)重載

在 6.1 節(jié)中, 我們看到函數(shù)重載是不被允許的。在 Go 語言中函數(shù)重載可以用可變參數(shù) ...T 作為函數(shù)最后一個(gè)參數(shù)來實(shí)現(xiàn)(參見 6.3 節(jié))。如果我們把 T 換為空接口,那么可以知道任何類型的變量都是滿足 T (空接口)類型的,這樣就允許我們傳遞任何數(shù)量任何類型的參數(shù)給函數(shù),即重載的實(shí)際含義。

函數(shù) fmt.Printf 就是這樣做的:

fmt.Printf(format string, a ...interface{}) (n int, errno error)

這個(gè)函數(shù)通過枚舉 slice 類型的實(shí)參動(dòng)態(tài)確定所有參數(shù)的類型。并查看每個(gè)類型是否實(shí)現(xiàn)了 String() 方法,如果是就用于產(chǎn)生輸出信息。我們可以回到 11.10 節(jié)查看這些細(xì)節(jié)。

11.12.6 接口的繼承

當(dāng)一個(gè)類型包含(內(nèi)嵌)另一個(gè)類型(實(shí)現(xiàn)了一個(gè)或多個(gè)接口)的指針時(shí),這個(gè)類型就可以使用(另一個(gè)類型)所有的接口方法。

例如:

type Task struct {
    Command string
    *log.Logger
}

這個(gè)類型的工廠方法像這樣:

func NewTask(command string, logger *log.Logger) *Task {
    return &Task{command, logger}
}

當(dāng) log.Logger 實(shí)現(xiàn)了 Log() 方法后,Task 的實(shí)例 task 就可以調(diào)用該方法:

task.Log()

類型可以通過繼承多個(gè)接口來提供像 多重繼承 一樣的特性:

type ReaderWriter struct {
    *io.Reader
    *io.Writer
}

上面概述的原理被應(yīng)用于整個(gè) Go 包,多態(tài)用得越多,代碼就相對(duì)越少(參見 12.8 節(jié))。這被認(rèn)為是 Go 編程中的重要的最佳實(shí)踐。

有用的接口可以在開發(fā)的過程中被歸納出來。添加新接口非常容易,因?yàn)橐延械念愋筒挥米儎?dòng)(僅僅需要實(shí)現(xiàn)新接口的方法)。已有的函數(shù)可以擴(kuò)展為使用接口類型的約束性參數(shù):通常只有函數(shù)簽名需要改變。對(duì)比基于類的 OO 類型的語言在這種情況下則需要適應(yīng)整個(gè)類層次結(jié)構(gòu)的變化。

練習(xí) 11.11map_function_interface.go

在練習(xí) 7.13 中我們定義了一個(gè) map 函數(shù)來使用 int 切片 (map_function.go)。

通過空接口和類型斷言,現(xiàn)在我們可以寫一個(gè)可以應(yīng)用于許多類型的 泛型 的 map 函數(shù),為 int 和 string 構(gòu)建一個(gè)把 int 值加倍和連接字符串值的 map 函數(shù) mapFunc。

提示:為了可讀性可以定義一個(gè) interface{} 的別名,比如:type obj interface{}

練習(xí) 11.12map_function_interface_var.go

稍微改變練習(xí) 11.9,允許 mapFunc 接收不定數(shù)量的 items。

練習(xí) 11.13main_stack.gostack/stack_general.go

在練習(xí) 10.10 和 10.11 中我們開發(fā)了一些棧結(jié)構(gòu)類型。但是它們被限制為某種固定的內(nèi)建類型。現(xiàn)在用一個(gè)元素類型是 interface{}(空接口)的切片開發(fā)一個(gè)通用的棧類型。

實(shí)現(xiàn)下面的棧方法:

Len() int
IsEmpty() bool
Push(x interface{})
Pop() (x interface{}, error)

Pop() 改變棧并返回最頂部的元素;Top() 只返回最頂部元素。

在主程序中構(gòu)建一個(gè)充滿不同類型元素的棧,然后彈出并打印所有元素的值。

鏈接