[toc]
> Go 語言不是一種 “傳統” 的面向對象編程語言:它里面沒有類和繼承的概念。但是 Go 語言里有非常靈活的`接口`概念,通過它可以實現很多面向對象的特性
# 1. 什么是interface(接口)
`簡單地說,interface是一組method的組合,但是這些method不包含(實現)代碼,我們通過interface來定義對象的一組行為。`
> 接口里不能包含變量
# 2. 接口聲明
```
type Namer interface {
Method1(參數列表1) 返回值列表1
Method2(參數列表2) 返回值列表
...
}
```
示例
```
// 變量名未忽略
type writer interface{
Write(p []byte) (n int, err error)
}
// 變量名被忽略
type writer interface{
Write([]byte) (int, error)
}
```
# 3. 接口規范
- 接口類型名:方法的接口名,<font color=red>由方法名加 [e]r 后綴組成</font>,例如 Printer、Reader、Writer、Logger、Converter 等等。還有一些不常用的方式(當后綴 er 不合適時),比如 Recoverable,此時接口名以 able 結尾,或者以 I 開頭。
- 方法名:當方法名首字母是大寫時,且這個接口類型名首字母也是大寫時,這個方法可以被接口所在的包(package)之外的代碼訪問。
- 參數列表、返回值列表:參數列表和返回值列表中的參數變量名可以被忽略,
- Go 語言中的接口都很簡短,通常它們會包含 0 個、最多 3 個方法
- 接口里的所有方法都沒有方法體,即接口的方法都是沒有實現的方法。接口體現了程序設計的多態和高內聚低偶合的思想
# 4. 接口嵌套(接口繼承)
> 在Go語言中,不僅結構體與結構體之間可以嵌套,接口與接口間也可以通過嵌套創造出新的接口。
接口與接口嵌套組合而成了新接口,只要接口的所有方法被實現,則這個接口中的所有嵌套接口的方法均可以被調用
`比如接口 File 包含了 ReadWrite 和 Lock 的所有方法,它還額外有一個 Close() 方法`
```
type ReadWrite interface {
Read(b Buffer) bool
Write(b Buffer) bool
}
type Lock interface {
Lock()
Unlock()
}
type File interface {
ReadWrite
Lock
Close()
}
```
> <font color=red>一個接口(比如 A 接口)可以繼承多個別的接口(比如 B,C 接口),這時如果要實現 A 接口,也必須將 B,C 接口的方法也全部實現
# 5. 接口實現
如果一個類型實現了一個接口里的所有方法,那么這個類型就實現了這個接口
如下面示例,類型fileHandle 實現了DataWrite接口:
```
type DataWrite interface {
Write(data interface{}) error
}
type fileHandle struct {
....
}
func (f fileHandle)Write(data interface{}) error {
fmt.Println("文件寫入中....")
return nil
}
```
# 6.類型與接口的關系
類型和接口之間有一對多和多對一的關系。
## 6.1 一對多
一個類型可以同時實現多個接口,而接口間彼此獨立,不知道對方的實現。
```
// 定義Writer接口
type Writer interface{
Write([]byte) int
}
// 定義Reader接口
type Reader interface{
Read()
}
// 定義一個File類型
type File struct {
}
// File類型 實現Writer接口
func (f File)Write(p []byte) int {
return 0
}
// File類型 定義Reader接口
func (f File)Read() {
}
```
## 6.2 多對一
一個接口的方法,不一定需要由一個類型完全實現,接口的方法可以通過在類型中嵌入其他類型或者結構體來實現。也就是說,使用者并不關心某個接口的方法是通過一個類型完全實現的,還是通過多個結構嵌入到一個結構體中拼湊起來共同實現的。
- 示例分析
Service接口定義了兩個方法:一個是開啟服務的方法:Start(),一個是輸出日志的方法:Log()。使用GameService結構體來實現Service,GameService結構只能實現Start()方法,Service接口中的Log()方法被日志器(Logger)實現了,將Logger嵌入到GameService中,則達成了GameService結構體實現Service接口,從而能最大程度地避免代碼冗余,簡化代碼結構。
詳細實現過程如下:
```
01 // 一個服務需要滿足能夠開啟和寫日志的功能
02 type Service interface {
03 Start() // 開啟服務
04 Log(string) // 日志輸出
05 }
06
07 // 日志器
08 type Logger struct {
09 }
10
11 // 實現Service的Log()方法
12 func (g *Logger) Log(l string) {
13
14 }
15
16 // 游戲服務
17 type GameService struct {
18 Logger // 嵌入日志器
19 }
20
21 // 實現Service的Start()方法
22 func (g *GameService) Start() {
23 }
```
代碼說明如下:
- 第2行,定義服務接口,一個服務需要實現Start()方法和日志方法。
- 第8行,定義能輸出日志的日志器結構。
- 第12行,為Logger添加Log()方法,同時實現Service的Log()方法。
- 第17行,定義GameService結構
- 第18行,在Game Service中嵌入Logger日志器,以實現日志功能。
- 第22行,Game Service的Start()方法實現了Service的Start()方法。
此時,實例化GameService,并將實例賦給Service,代碼如下:
```
var s Service = new(Game Service)
s.Start()
s.Log("hello")
```
s就可以使用Start()方法和Log()方法,其中,Start()由GameService實現,Log()方法由Logger實現。
# 7. 接口和類型間轉換
Go語言中使用接口斷言(type assertions)將接口轉換成另外一個接口,也可以將接口轉換為另外的類型。接口的轉換在開發中非常常見,使用也非常頻繁。
## 7.1 類型斷言的格式
如果發生接口未實現時,將會把ok置為false,t置為T類型的0值。正常實現時,ok為true。
這里ok可以被認為是:`i接口是否實現T類型的結果`。
類型斷言的基本格式如下:
```
t,ok := i.(T)
```
- i 代表接口變量。
- T 代表轉換的目標類型。
- t 代表轉換后的變量。
## 7.2 將接口轉為其他接口
`實現某個接口的類型同時實現了另外一個接口,此時可以在兩個接口間轉換。`
- 代碼示例
鳥和豬具有不同的特性,鳥可以飛,豬不能飛,但兩種動物都可以行走。如果使用結構體實現鳥和豬,讓它們具備自己特性的Fly()和Walk()方法就讓鳥和豬各自實現了飛行動物接口(Flyer)和行走動物接口(Walker)。
將鳥和豬的實例創建后,被保存到interface{}類型的map中。interface{}類型表示空接口,意思就是這種接口可以保存為任意類型。對保存有鳥或豬的實例的interface{}變量進行斷言操作,如果斷言對象是斷言指定的類型,則返回轉換為斷言對象類型的接口;如果不是指定的斷言類型時,斷言的第二個參數將返回false。
實現代碼如下:
```
01 package main
02
03 import "fmt"
04
05 // 定義飛行動物接口
06 type Flyer interface {
07 Fly()
08 }
09
10 // 定義行走動物接口
11 type Walker interface {
12 Walk()
13 }
14
15 // 定義鳥類
16 type bird struct {
17 }
18
19 // 實現飛行動物接口
20 func (b *bird) Fly() {
21 fmt.Println("bird: fly")
22 }
23
24 // 為鳥添加Walk()方法,實現行走動物接口
25 func (b *bird) Walk() {
26 fmt.Println("bird: walk")
27 }
28
29 // 定義豬
30 type pig struct {
31 }
32
33 // 為豬添加Walk()方法,實現行走動物接口
34 func (p *pig) Walk() {
35 fmt.Println("pig: walk")
36 }
37
38 func main() {
39
40 // 創建動物的名字到實例的映射
41 animals := map[string]interface{}{
42 "bird": new(bird),
43 "pig": new(pig),
44 }
45
46 // 遍歷映射
47 for name, obj := range animals {
48
49 // 判斷對象是否為飛行動物
50 f, isFlyer := obj.(Flyer)
51 // 判斷對象是否為行走動物
52 w, isWalker := obj.(Walker)
53
54 fmt.Printf("name: %s is Flyer: %v is Walker: %v\n", name, is Flyer, is Walker)
55
56 // 如果是飛行動物則調用飛行動物接口
57 if isFlyer {
58 f.Fly()
59 }
60
61 // 如果是行走動物則調用行走動物接口
62 if isWalker {
63 w.Walk()
64 }
65 }
66 }
```
代碼說明如下:
- 第6行定義了飛行動物的接口。
- 第11行定義了行走動物的接口。
- 第16和30行分別定義了鳥和豬兩個對象,并分別實現了飛行動物和行走動物接口。
- 第41行是一個map,映射對象名字和對象實例,實例是鳥和豬。
- 第47行開始遍歷map,obj為interface{}接口類型。
- 第50行中,使用類型斷言獲得f,類型為Flyer及isFlyer的斷言成功的判定。
- 第52行中,使用類型斷言獲得w,類型為Walker及isWalker的斷言成功的判定。
- 第57和62行,根據飛行動物和行走動物兩者
代碼輸出如下:
```
name: pig is Flyer: false is Walker: true
pig: walk
name: bird is Flyer: true is Walker: true
bird: fly
bird: walk
```
# 8.空接口 — 能保存所有值的類型
空接口是接口類型的特殊形式,`空接口沒有任何方法`,因此任何類型都無須實現空接口。從實現的角度看,任何值都滿足這個接口的需求。因此空接口類型可以保存任何值,也可以從空接口中取出原值。
## 8.1 將值保存到空接口
```
01 var any interface{}
02
03 any = 1
04 fmt.Println(any)
05
06 any = "hello"
07 fmt.Println(any)
08
09 any = false
10 fmt.Println(any)
```
代碼輸出如下:
```
1
hello
false
```
- 第1行,聲明any為interface{}類型的變量。
- 第3行,為any賦值一個整型1。
- 第4行,打印any的值,提供給fmt.Println的類型依然是interface{}。
- 第6行,為any賦值一個字符串hello。此時any內部保存了一個字符串。但類型依然是interface{}。
- 第9行,賦值布爾值。
## 8.2 從空接口獲取值
保存到空接口的值,如果直接取出指定類型的值時,會發生編譯錯誤
代碼如下:
```
01 // 聲明a變量,類型int,初始值為1
02 var a int = 1
03
04 // 聲明i變量,類型為interface{},初始值為a,此時i的值變為1
05 var i interface{} = a
06
07 // 聲明b變量,嘗試賦值i
08 var b int = i
```
第8行代碼編譯報錯:
```
cannot use i (type interface {}) as type int in assignment: need type assertion
```
> 編譯器告訴我們,不能將i變量視為int類型賦值給b。在代碼第5行中,將a的值賦值給i時,雖然i在賦值完成后的內部值為int,但i還是一個interface{}類型的變量。
為了讓第8行的操作能夠完成,`編譯器提示我們得使用type assertion,意思就是類型斷言`。
使用類型斷言修改第8行代碼如下:
```
var b int = i.(int)
```
修改后,代碼可以編譯通過,并且b可以獲得i變量保存的a變量的值:1。
## 8.3 空接口的值比較
空接口在保存不同的值后,可以和其他變量值一樣使用“==”進行比較操作。空接口的比較有以下幾種特性。
### 8.3.1 類型不同的空接口間的比較結果不相同
保存有類型不同的值的空接口進行比較時,Go語言會優先比較值的類型。因此類型不同,比較結果也是不相同的,代碼如下:
```
01 // a保存整型
02 var a interface{} = 100
03
04 // b保存字符串
05 var b interface{} = "hi"
06
07 // 兩個空接口不相等
08 fmt.Println(a == b) // 輸出: false
```
### 8.3.2 不能比較空接口中的動態值
當接口中保存有動態類型的值時,運行時將觸發錯誤,
代碼如下:
```
01 // c保存包含10的整型切片
02 var c interface{} = []int{10}
03
04 // d保存包含20的整型切片
05 var d interface{} = []int{20}
06
07 // 這里會發生崩潰
08 fmt.Println(c == d)
```
代碼運行到第8行時發生崩潰:
```
panic: runtime error: comparing uncomparable type []int
```
這是一個運行時錯誤,提示[]int是不可比較的類型。
zhuoge
- 下面列出了幾種類型及比較情況
類型 | 說明
---|---
map | 宕機錯誤,不可比較
切片([]T)| 宕機錯誤,不可比較
通道(channel)| 可比較,必須由一個make生成,也就是說同一個通道才會true,否則false
數組([容量]T) | 可比較,編譯期知道兩個數組是否一致
結構體|可比較,可以逐個比較結構體的值
# 9. 接口使用中的注意事項
- 接口本身不能創建實例,但是可以指向一個實現了該接口的自定義類型的變量(實例)
```
package main
import "fmt"
type PeopleI interface {
GetName() string
}
type Girl struct {
Name string
}
func (g Girl) GetName() string {
return g.Name
}
func main() {
var p PeopleI
var g Girl
g.Name = "小花"
p = g //指向一個實現了該接口的自定義類型的變量
fmt.Println(p.GetName())
}
```
- 接口中所有的方法都沒有方法體,即都是沒有實現的方法。
- 在 Go中,一個自定義類型需要將某個接口的所有方法都實現,我們說這個自定義類型實現 了該接口。
- 一個自定義類型只有實現了某個接口,才能將該自定義類型的實例(變量)賦給接口類型
- 只要是自定義數據類型,就可以實現接口,不僅僅是結構體類型
- 一個自定義類型可以實現多個接口
- Go接口中不能有任何變量
- interface類型默認是一個指針(引用類型),如果沒有對interface初始化就使用,那么會輸出nil
- 空接口 interface{} 沒有任何方法,所以所有類型都實現了空接口, 即我們可以把任何一個變量 賦給空接口