接口是一種協議,好比一個汽車的協議,就應該有 「行駛」,「按喇叭」,「開遠光」 等功能(方法),這就是實現汽車的協議規範,完成了汽車的協議規範,就實現了汽車的接口,而後使用接口python
接口的定義:自己是調用方和實現方均須要遵照的一種協議,你們按照統一的方法命名參數類型和數量來協調邏輯處理的過程編程
Go 語言中的接口是雙方約定的一種合做協議;接口實現者不須要關心接口會被如何使用,調用者也不須要關心接口的實現細節。接口是一種類型,也是一種抽象結構,不會暴露所含數據的格式、類型及結構sass
Go 語言中接口的設計是非侵入式的,接口編寫者無須知道接口被哪些類型實現,而接口實現者只需知道實現的是什麼樣子的接口,無須指明是哪個接口。編譯器知道最終編譯時使用哪一個類型實現哪一個接口,或者接口應該由誰來實現socket
非侵入式設計是 Go 語言設計師通過多年的大項目經驗總結出來的設計之道。只有讓接口和實現者真正解耦,編譯速度才能真正提升,項目之間的耦合度也會下降ide
接口聲明的格式函數
接口類型名:在命名時,通常會在單詞的後面添加 er,例如寫操做的接口叫 Writer,關閉功能的接口叫 Closerthis
方法名:當方法名首字母是大寫,且這個接口類型名首字母也是大寫時,這個方法能夠被接口所在的包以外的代碼訪問spa
參數列表、返回值列表:.net
type 接口類型名 interface{ 方法名1(參數列表) 返回值列表 方法名2(參數列表) 返回值列表 ... }
實現接口設計
接口定義後,須要實現接口,調用方纔能正確編譯經過並使用接口
接口的實現須要遵循兩條規則才能使接口可用
1)接口的方法與實現接口的類型方法格式一致
在類型中添加與接口簽名一致的方法就能夠實現該方法。簽名包括方法中的名稱、參數列表、返回參數列表。也就是說,只要實現接口類型中的方法的名稱、參數列表、返回參數列表中的任意一項與接口要實現的方法不一致,那麼接口的這個方法就不會被實現
模擬數據寫入的 Demo:
// 實現一個寫入器接口 type Writer interface{ WriteData(data interface{}) error } // 文件類型結構體 type file struct {} // 實現 Writer 接口的 WriteData 方法 func (f *file) WriteData(data interface{}) error { // 模擬寫入數據 fmt.Println(data) return nil } func main(){ // 實例化 file var f *file = new(file) // 聲明 Writer 接口 var W Writer // 將文件類型結構體賦值給接口 W = f // 使用接口調用數據寫入 W.WriteData("hello, world~") } 運行結果: hello, world~
2)接口中全部方法均被實現
當一個接口中有多個方法時,只有這些方法都被實現了,接口才能正確編譯並使用
類型與接口的關係
類型與接口之間有一對多和多對一的關係
1)一個類型實現多個接口
把 Socket 可以寫入數據和須要關閉的特性使用接口來描述,Demo:
// 實現一個寫入接口 type Writer interface{ Write(b []byte)(n int, err error) } // 實現一個關閉接口 type Closer interface{ Close() (err error) } // 套接字結構體 type Socket struct {} // 實現 Writer 接口的 Write 方法 func (s *Socket) Write(b []byte) (n int, err error) { fmt.Println("write data") return 0, nil } // 實現 Closer 接口的 Close 方法 func (s *Socket) Close() (err error) { fmt.Println("closer socket") return nil } func main(){ // 實例化 file var s *Socket = new(Socket) // 聲明 Writer 和 Closer 接口 var W Writer var C Closer // 將文件類型結構體賦值給接口 W = s C = s // 使用接口調用數據寫入 W.Write(make([]byte, 0)) C.Close() } 運行結果: write data closer socket
2)多個類型對應一個接口(這裏多個類型本質上指的仍是一個類型)
Service 接口定義了兩個方法:一個開啓服務的方法 Start(),一個是輸出日誌的方法 Log(),使用 GameService 結構體來實現 Service 接口;GameService 本身的結構只能實現 Start(),而 Service 接口中的 Log() 已經被一個輸出日誌的 Logging 實現了, 無須再進行 GameService 再從新實現一遍,因此,選擇將 Logging 嵌入到 GameService 能最大程度的避免冗餘代碼,詳細實現過程以下:
// 實現一個服務接口 type Service interface{ Start(args string)(err error) Log(args string)(err error) } // 日誌器結構體 type Logging struct{} // 日誌記錄方法 func (l *Logging) Log(info string) (err error){ fmt.Println(info) return nil } // 遊戲服務結構體,內嵌日誌器 type GameService struct{ Logging } // 遊戲服務開啓方法 func (gs *GameService) Start(args string) (err error){ fmt.Println("game start", args) return nil } func main(){ // 實例化 遊戲服務結構體,並將實例賦值給 Service var s Service = new(GameService) // 使用接口調用服務啓動,日誌記錄 s.Start("come on") s.Log("this is a log info") } 運行結果: game start come on this is a log info
錯誤示例:
若是遊戲服務結構體單獨實現 Start() 方法,日誌器單獨實現 Log() 方法,這樣並無實現接口的全部的方法
// 實現一個服務接口 type Service interface{ Start(args string)(err error) Log(args string)(err error) } // 日誌器結構體 type Logging struct{} // 日誌記錄方法 func (l *Logging) Log(info string) (err error){ fmt.Println(info) return nil } // 遊戲服務結構體,內嵌日誌器 type GameService struct{} // 遊戲服務開啓方法 func (gs *GameService) Start(args string) (err error){ fmt.Println("game start", args) return nil } func main(){ // 實例化 遊戲服務結構體,並將實例賦值給 Service var s Service = new(GameService) // 使用接口調用服務啓動,日誌記錄 s.Start("come on") s.Log("this is a log info") } 運行結果: ./main_04.go:31:9: cannot use new(GameService) (type *GameService) as type Service in assignment: *GameService does not implement Service (missing Log method)
使用 sort.Interface 接口實現排序
在排序時,使用 sort.Interface 提供數據的一些特性和操做方法,這個接口定義代碼以下:
type Interface interface { // 獲取元素數量 Len() int // 小於比較 Less(i, j int) bool // 交換元素 Swap(i, j int) }
這個接口須要實現者實現三個方法:Len(),Less(),Swap()
對一系列字符串進行排序時,把字符串放入切片,使用 type 關鍵字,定義爲自定義的類型,爲了讓 sort 包可以識別自定義類型,就必須讓自定義類型實現 sort.Interface 接口
package main import ( "fmt" "sort" ) // 將[]string定義爲MyStringList類型 type MyStringList []string func (m MyStringList) Len() int { return len(m) } func (m MyStringList) Less(i, j int) bool { return m[i] < m[j] } func (m MyStringList) Swap(i, j int) { m[i], m[j] = m[j], m[i] } func main() { // 準備一個內容被打亂順序的字符串切片 var names MyStringList = MyStringList{ "3 Triple Kill", "5 Penta Kill", "2 Double Kill", "4 Quadra Kill", "1 First Blood", } // 使用sort包進行排序(Sort 接收一個Interface類型,MyStringList會被賦值給 Interface 類型) sort.Sort(names) // 遍歷打印結果 for _, v := range names { fmt.Printf("%s\n", v) } } 運行結果: 1 First Blood 2 Double Kill 3 Triple Kill 4 Quadra Kill 5 Penta Kill
常見類型的便捷排序
經過 sort.Interface 接口的排序過程具備很強的可定製性
1)字符串切片的便捷排序(與在上面用自定義類型實現的邏輯同樣)
sort 包中有一個 StringSlice 類型,定義以下:
type StringSlice []string func (p StringSlice) Len() int { return len(p) } func (p StringSlice) Less(i, j int) bool { return p[i] < p[j] } func (p StringSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } // Sort is a convenience method. func (p StringSlice) Sort() { Sort(p) }
使用 sort 包下的 StringSlice 類型就能夠對字符串切片進行排序,簡化上面的步驟:
names := sort.StringSlice{ "3 Triple Kill", "5 Penta Kill", "2 Double Kill", "4 Quadra Kill", "1 First Blood", } sort.Sort(names)
字符串排序簡化版:sort.Strings(names)
2)sort 包下其它的內建排序接口
類 型 | 實現 sort.lnterface 的類型 | 直接排序方法 | 說 明 |
---|---|---|---|
字符串(String) | StringSlice | sort.Strings(a [] string) | 字符 ASCII 值升序 |
整型(int) | IntSlice | sort.Ints(a []int) | 數值升序 |
雙精度浮點(float64) | Float64Slice | sort.Float64s(a []float64) | 數值升序 |
編程中常常用到的 int3二、int6四、float3二、bool 類型沒有由 sort 實現,須要開發者本身編寫
對結構體數據進行排序
除了基本類型,也能夠對結構體的字段進行排序,結構體的多個字段在排序中可能存在多種排序規則,如先按分類排序,而後按名稱排序
demo:定義英雄結構體,有 Name 和 Kind 字段,排序時要求先按照分類排序,相同分類則按名稱排序,實現以下(排序的對象是英雄結構體):
// 定義int常量, 相似於枚舉 const ( None int = iota // 0 Tank // 1 Assassin // 2 Mage // 3 ) // Hero 結構體 type Hero struct{ Name string Kind int } // 自定義 Hero 的切片的類型 type Heros []Hero // 實現 sort.Interface 接口方法 func (hs Heros) Len() int { return len(hs) } func (hs Heros) Less(i, j int) bool { // 優先對分類進行排序 if hs[i].Kind != hs[j].Kind { return hs[i].Kind < hs[j].Kind } else { // secondary: Name return hs[i].Name < hs[j].Name } } func (hs Heros) Swap (i,j int) { hs[i], hs[j] = hs[j], hs[i] } func main(){ var heros Heros = Heros{ Hero{"呂布", Tank}, Hero{"李白", Assassin}, Hero{"妲己", Mage}, Hero{"貂蟬", Assassin}, Hero{"關羽", Tank}, Hero{"諸葛亮", Mage}, } fmt.Println("before:") for _, v := range(heros){ fmt.Println(v) } sort.Sort(heros) fmt.Println("\nafter:") for _, v := range(heros){ fmt.Println(v) } } 運行結果: before: {呂布 1} {李白 2} {妲己 3} {貂蟬 2} {關羽 1} {諸葛亮 3} after: {關羽 1} {呂布 1} {李白 2} {貂蟬 2} {妲己 3} {諸葛亮 3}
Go 語言中,不只結構體之間能夠嵌套,接口之間也能夠經過嵌套組合造成新的接口
1)io 包中的接口嵌套組合
io 包中定義了寫入接口(Writer)、關閉接口(Closer)、寫入關閉組合接口(WriterCloser),代碼以下:
type Writer interface { Write(p []byte) (n int, err error) } type Closer interface { Close() error } type WriteCloser interface { Writer Closer }
2)在代碼中使用接口嵌套組合
咱們實現一下上面 io 包中的三個接口(一個類型對應多個接口)
type Device struct { Name string } func (d Device) Write(p []byte) (n int, err error) { fmt.Printf("%v call Write methodi\n", d) return 0, nil } func (d Device) Close() error { fmt.Printf("%v call Close method\n", d) return nil } func main(){ var device1 io.Writer = Device{"device1"} device1.Write(make([]byte, 0)) var device2 io.Closer = Device{"device2"} device2.Close() var device3 io.WriteCloser = Device{"device3"} device3.Write(make([]byte, 0)) device3.Close() } 運行結果: {device1} call Write methodi {device2} call Close method {device3} call Write methodi {device3} call Close method
使用接口斷言 type assertions 將接口轉換成另一個接口,也能夠將接口準換爲另外的類型,接口的轉換在開發中很是常見,使用也很是頻繁
空接口
interface{},空接口沒有任何方法,全部類型都實現了空接口(因此類型均可以賦值給空接口),下面會詳細說
類型判斷的格式
t := i.(T) 其中,i 是接口變量,T 是要轉換的目標類型,t 是準換後的變量
若是 i 沒有實現 T 接口的全部方法,即斷言失敗,會觸發 panic,因此有一種友好的寫法
斷言失敗時,將會把 ok 置爲 false, t 置爲 T 類型的 0 值,斷言成功時,ok 置爲 true,t 置爲斷言後的結果
t, ok := i.(T)
將接口轉換成其它接口
一種類型實現了多個接口,就能夠在多個接口之間進行切換
demo:鳥 和 豬具備不一樣的特性,鳥能夠飛,能夠行走,豬隻能行走,如今有飛行動物接口 Flyer 和 行走動物接口 Walker,若是用結構體分別實現鳥和豬,鳥實現 Fly() 和 Walk(),豬實現 Walk(),那麼鳥類型實現了飛行動物接口和行走動物接口,豬實現了行走動物接口
下面的demo中,豬類型實現了從空接口轉換到行走接口,鳥類型實現了從空接口轉換到行走接口,而後轉換到飛行接口
//飛行接口 type Flyer interface { Fly() } //行走接口 type Walker interface { Walk() } //豬結構體,實現行走接口 type Pig struct{} func (p Pig) Walk() { fmt.Println("pig walk") } //鳥結構體,實現行走和飛行接口 type Bird struct{} func (b Bird) Walk() { fmt.Println("bird walk") } func (b Bird) Fly() { fmt.Println("bird fly") } func main() { //空接口接收豬類型 var pig interface{} = new(Pig) var bird interface{} = new(Bird) //判斷對象類型是否實現行走接口(轉換到行走接口) pigWalk, pigWalker := pig.(Walker) birdWalk, birdWalker := bird.(Walker) birdFly, isFlyer := bird.(Flyer) //若是實現行走接口,則調用行走接口方法 if pigWalker { pigWalk.Walk() } if birdWalker { birdWalk.Walk() } if isFlyer { birdFly.Fly() } } 運行結果: pig walk bird walk bird fly
將接口轉換爲類型
在上面的代碼中,能夠將普通的指針類型 new(Pig),轉換成接口 Walker,那麼將 Walker 接口轉換成 *Pig 類型也是能夠的
var walk Walker = new(Pig) //接口轉換爲類型 p, ok := walk.(*Pig) if ok {fmt.Printf("%T", p)}
可是若是把普通指針類型 new(*Pig),轉換成接口,而後將接口轉換成 *Bird,這樣會觸發 panic: interface conversion: main.Walker is *main.Pig, not *main.Bird
var walk Walker = new(Pig) p := walk.(*Bird) fmt.Printf("%T", p) 運行結果: panic: interface conversion: main.Walker is *main.Pig, not *main.Bird
報錯意思是:接口轉換類型時,main.Walker 接口的內部保存的是 *main.pig,而不是 *main.bird
所以,接口在轉換爲類型時,接口內保存的類型指針,必須是要轉換的類型指針
空接口類型
空接口是接口類型的特殊形式,空接口沒有任何方法,從實現的角度看,任何類型都實現了空接口,所以空接口類型能夠保存任何值,也能夠從空接口中取出原值
空接口的內部實現保存了對象的類型與指針,使用空接口保存一個數據的過程會比直接用變量保存稍慢,所以在開發中,應在須要的地方使用空接口,而不是全部地方都使用空接口
1)將值保存到空接口(從類型轉換成接口)
func main(){ var any interface{} any = 666 any = "hello, world~" fmt.Println(any) } 運行結果: hello, world~
2)從空接口中獲取值(從接口轉換成類型)
func main(){ var any interface{} any = "hello, world~" var value string = any.(string) fmt.Println(value) } 運行結果: hello, world~
使用空接口實現能夠保存任意值的字典(實現 python 中的字典)
空接口能夠保存任何類型,這個特性能夠方便的用於容器的設計,下面的例子中使用 map 和 insterface{} 實現了 python 中的字典,包含有 設置值、取值、清空的方法
package main import "fmt" //定義key,value 可爲任意值的字典結構體 type Dict struct { data map[interface{}]interface{} } //設置值 func (d Dict) Set(key, value interface{}) { d.data[key] = value } //根據鍵獲取值 func (d Dict) Get(key interface{}) interface{} { return d.data[key] } //清空Dict func (d *Dict) Clear(){ d.data = make(map[interface{}]interface{}) } func main(){ //字典結構包含有 map,須要在建立 Dictionary 實例時初始化 map var dict Dict = Dict{} dict.data = make(map[interface{}]interface{}) //var dict Dict = Dict{map[interface{}]interface{}{}} //能夠寫成這種 dict.Set("name", "johny") dict.Set("age", 12) dict.Set(666, 666) // 根據鍵獲取值(這裏拿到的是 interface{},須要根據空接口中的值類型進行斷言取值,很差用) fmt.Println(dict.Get("name").(string)) fmt.Println(dict.Get("age").(int)) fmt.Println(dict.Get(666).(int)) // 清空字典 dict.Clear() fmt.Println(dict) } 運行結果: johny 12 666 {map[]}
問題:在空接口轉換成類型的時候,須要進行類型的斷言,若是你不知道空接口中的類型,則須要作判斷,有點麻煩
接口類型斷言
在從接口轉換成類型的時候,每每會不清楚要轉換的目標類型是什麼,因此須要判斷空接口中的類型,if 的語句代碼太繁雜,這裏使用 switch 實現
1)類型斷言 switch 格式(斷言基本類型)
package main import "fmt" func assertions(element interface{}) { switch element.(type){ case int: fmt.Println(element.(int)) case string: fmt.Println(element.(string)) case float64: fmt.Println(element.(float64)) default: fmt.Println("unsupported types") } } func main(){ assertions("666") assertions("hello, world") assertions(true) } 運行結果: 666 hello, world unsupported types
2)斷言接口類型
多個接口進行斷言時,也可使用 switch 分支簡化判斷過程
demo:如今移動支付逐漸成爲人們廣泛使用的支付方式,移動支付可使用 faceID,而現金支付容易被偷(Stolen),使用 switch 接口斷言能夠方便判斷是哪一種支付接口,進行方法調用
現有兩個支付接口 CantainCanUseFaceID 和 ContainStolen,分別實現了 UseFaceID() 和 Stolen() 方法,在支付函數 Payment() 中進行接口斷言,而後調用相應的方法
package main import "fmt" // 移動支付接口 type CantainCanUseFaceID interface { UseFaceID() } //現金支付接口 type ContainStolen interface { Stolen() } //alipay 結構體,實現移動支付接口 type Alipay struct {} func (a *Alipay) UseFaceID() { fmt.Println("alipay payment") } //現金支付結構體,實現現金支付接口 type Cash struct {} func (c *Cash) Stolen() { fmt.Println("cash payment") } func Payment(patternPayment interface{}) { switch patternPayment.(type) { // 可使用移動支付 case CantainCanUseFaceID: faceIDPayment := patternPayment.(CantainCanUseFaceID) faceIDPayment.UseFaceID() // 可使用現金支付 case ContainStolen: cashPayment := patternPayment.(ContainStolen) cashPayment.Stolen() } } func main() { //使用 alipay 支付 Payment(new(Alipay)) //使用現金支付 Payment(new(Cash)) } 運行結果: alipay payment cash payment
end ~