Golang介面型別-上篇

1、概述
介面是計算機系統中多個元件共享的邊界,不同的元件能夠在邊界上交換資訊。介面的本質是引入一個新的中間層,呼叫方可以通過介面與具體實現分離,解除上下游的耦合,上層的模組不再需要依賴下層的具體模組,只需要依賴一個約定好的介面
簡單來說, Go
語言中的介面就是一組方法的簽名。介面是 Go
語言整個型別系統的基石,其他語言的介面是不同元件之間的契約的存在,對契約的實現是強制性的,必須顯式宣告實現了該介面,這類介面稱之為“侵入式介面”。而 Go
語言的介面是隱式存在,只要實現了該介面的所有函式則代表已經實現了該介面,並不需要顯式的介面宣告
介面的比喻
一個常見的例子,電腦上只有一個 USB
介面。這個 USB
介面可以接 MP3
、數碼相機、攝像頭、滑鼠、鍵盤等。所有的上述硬體都可以公用這個介面,有很好的擴充套件性,該 USB
介面定義了一種規範,只要實現了該規範,就可以將不同的裝置接入電腦,而裝置的改變並不會對電腦本身有什麼影響(低耦合)
介面表示呼叫者和設計者的一種約定,在多人合作開發同一個專案時,事先定義好相互呼叫的介面可以大大提高開發的效率。介面是用類來實現的,實現介面的類必須嚴格按照介面的宣告來實現介面提供的所有功能。有了介面,就可以在不影響現有介面宣告的情況下,修改介面的內部實現,從而使相容性問題最小化
2、介面的隱式實現
在 Java
中實現介面需要顯式地宣告介面並實現所有方法,而在 Go
中實現介面的所有方法就隱式地實現了介面
定義介面需要使用 interface
關鍵字,在介面中只能定義方法簽名,不能包含成員變數,例如
type error interface { Error() string }
如果一個型別需要實現 error
介面,那麼它只需要實現 Error() string
方法,下面的 RPCError
結構體就是 error
介面的一個實現
type RPCError struct { Code int64 Message string } func (e *RPCError) Error() string { return fmt.Sprintf("%s, code=%d", e.Message, e.Code) }
會發現上述程式碼根本就沒有 error
介面的影子,這正是因為 Go
語言中介面的實現都是隱式的
3、介面定義和宣告
介面是自定義型別,是對其他型別行為的抽象(定義一個介面型別,把其他型別的值賦值給自定義的介面)
介面定義使用 interface
標識,聲明瞭一系列的函式簽名(函式名、函式引數、函式返回值)在定義介面時可以指定介面名稱,在後續宣告介面變數時使用
宣告介面變數只需要定義變數型別為介面名,此時變數被初始化為 nil
package main import "fmt" type Sender interface { Send(to string, msg string) error SendAll(tos []string, msg string) error } func main() { var sender Sender fmt.Printf("%T %v\n", sender, sender) // <nil> <nil> }
4、介面型別賦值
為介面型別方法賦值,一般是定義一個結構體,需要保證結構體方法(方法名、引數)均與介面中定義相同
package main import "fmt" type Sender interface { Send(to string, msg string) error SendAll(tos []string, msg string) error } type EmailSender struct { } func (s EmailSender) Send(to, msg string) error { fmt.Println("傳送郵件給:", to, ",訊息內容是:", msg) return nil } func (s EmailSender) SendAll(tos []string, msg string) error { for _, to := range tos { s.Send(to, msg) } return nil } func main() { var sender Sender = EmailSender{} fmt.Printf("%T %v\n", sender, sender) // <nil> <nil> sender.Send("geek", "早上好") sender.SendAll([]string{"aa","bb"}, "中午好") }
使用介面的好處,概念上可能不好理解,來一個實際例子
package main import "fmt" type Sender interface { Send(to string, msg string) error SendAll(tos []string, msg string) error } type EmailSender struct { } func (s EmailSender) Send(to, msg string) error { fmt.Println("傳送郵件給:", to, ",訊息內容是:", msg) return nil } func (s EmailSender) SendAll(tos []string, msg string) error { for _, to := range tos { s.Send(to, msg) } return nil } type SmsSender struct { } func (s SmsSender) Send(to, msg string) error { fmt.Println("傳送簡訊給:", to, ", 訊息內容是:", msg) return nil } func (s SmsSender) SendAll(tos []string, msg string) error { for _, to := range tos { s.Send(to, msg) } return nil } //func do(sender EmailSender) { func do(sender Sender) { sender.Send("領導", "工作日誌") } func main() { var sender Sender = EmailSender{} fmt.Printf("%T %v\n", sender, sender) // <nil> <nil> sender.Send("geek", "早上好") sender.SendAll([]string{"aa","bb"}, "中午好") do(sender) sender = SmsSender{} do(sender) }
按照上面的示例,最後定義變數 sender
為介面型別 Sender
,呼叫介面方法時,只需要指定介面型別對應的結構體是什麼,因為在定義介面時,已經聲明瞭此介面實現了 Send
、 SendAll
兩個方法
var sender Sender = EmailSender{} // 或 var sender Sender = SmsSender{} // 單獨定義go函式呼叫 func do(sender Sender) { sender.Send("領導", "工作日誌") }
如果沒有介面,那麼最終呼叫時,還需要對應上其具體的結構體型別,寫法為
var sender EmailSender = EmailSender{} // 或 var sender SmsSender = SmsSender{} // 單獨定義go函式呼叫 func do(sender EmailSender) { // func do(sender SmsSender) { sender.Send("領導", "工作日誌") }
很明顯,前者使用介面定義變數,在傳參時也使用介面型別定義,在使用上更為簡單,僅僅只需要調整初始化的結構體型別即可
5、介面型別物件
當自定義型別實現了介面型別中宣告的所有函式時,則該型別的物件可以賦值給介面變數,並使用介面變數呼叫實現的介面
-
方法接收者全為值型別
如上面的例子
-
方法接收者全為指標型別
package main import "fmt" type Sender interface { Send(to string, msg string) error SendAll(tos []string, msg string) error } type SmsSender struct { } func (s *SmsSender) Send(to, msg string) error { fmt.Println("傳送簡訊給:", to, ", 訊息內容是:", msg) return nil } func (s *SmsSender) SendAll(tos []string, msg string) error { for _, to := range tos { s.Send(to, msg) } return nil } func do(sender Sender) { sender.Send("領導", "工作日誌") } func main() { var sender Sender = &SmsSender{} // 指標型別 do(sender) }
- 方法接收者既有值型別又有指標型別
WechatSender
的 send
和 sendAll
, send
有指標和值, sendAll
只有指標,因此初始化的時候只能用指標,不能用值
package main import "fmt" type Sender interface { Send(to string, msg string) error SendAll(tos []string, msg string) error } type WechatSender struct { } // Send 接收者為值物件 func (s WechatSender) Send(to, msg string) error { fmt.Println("傳送微信給:", to, ", 訊息內容是:", msg) return nil } // SendAll 接收者為指標物件 func (s *WechatSender) SendAll(tos []string, msg string) error { for _, to := range tos { s.Send(to, msg) } return nil } //func do(sender EmailSender) { func do(sender Sender) { sender.Send("領導", "工作日誌") } func main() { var sender Sender = &WechatSender{} do(sender) }
當介面(A)包含另外一個介面(B)中宣告的所有函式時(A介面函式是B介面函式的父集,B是A的子集),介面(A)的物件也可以賦值給其子集的介面(B)變數
package main import "fmt" type SignalSender interface { Send(to, msg string) error } type Sender interface { Send(to string, msg string) error SendAll(tos []string, msg string) error } ... func main() { var ssender SignalSender = sender // 以介面的變數初始化另外一個介面 ssender.Send("aa", "你好") }
若兩個介面宣告同樣的函式簽名,則這兩個介面完全等價
當型別和父集介面賦值給介面變數時,只能呼叫介面變數定義介面中宣告的函式(方法)
6、介面應用舉例
實際的生產例子,可以加深對介面的理解。例如多個數據源推送和查詢資料
package main import ( "fmt" "log" ) /* 1、多個數據源 2、query方法查詢資料 3、pushdata方法寫入資料 */ type DataSource interface { PushData(data string) QueryData(name string) string } type redis struct { Name string Addr string } func (r *redis) PushData (data string) { log.Printf("pushdata,name:%s,data:%s\n", r.Name,data) } func (r *redis) QueryData (name string) string { log.Printf("querydata,name:%s,data:%s\n", r.Name,name) return name + "redis" } type kafka struct { Name string Addr string } func (k *kafka) PushData (data string) { log.Printf("pushdata,name:%s,data:%s\n", k.Name,data) } func (k *kafka) QueryData (name string) string { log.Printf("querydata,name:%s,data:%s\n", k.Name,name) return name + "kafka" } var Dm = make(map[string]DataSource) func main() { r:=redis{ Name: "redis", Addr: "127.0.0.1", } k:=kafka{ Name:"kafka", Addr:"192.169.0.1", } // 註冊資料來源到承載的容器中 Dm["redis"] = &r Dm["kafka"] = &k // 推送資料 for i:=0;i<5;i++{ key:=fmt.Sprintf("key_%d", i) for _,ds:=range Dm{ ds.PushData(key) } } // 查詢資料 for i:=0;i<5;i++{ key:=fmt.Sprintf("key_%d", i) //r:=Dm["redis"] //r.QueryData(key) for _,ds:=range Dm{ res:=ds.QueryData(key) log.Printf("query_from_ds,res:%s", res) } } }
See you ~
參考:https://draveness.me/golang/docs/part2-foundation/ch04-basic/golang-interface/
- Gradle打包工具入門
- 服務網格和Istio初識-續
- 服務網格和Istio初識
- Golang與非對稱加密
- ack叢集Terway網路場景下的vSwitch擴容
- Golang與對稱加密
- 基於ack k8s叢集排程的方案設計
- 基於Dockerfile構建容器映象的最佳實踐
- Golang反射-下篇
- Golang反射-上篇
- Azure DevOps的使用入門
- Golang介面型別-下篇
- Golang介面型別-上篇
- 基於Python實現原生的登入驗證碼
- Golang開發命令列工具之flag包的使用
- Golang檔案操作-下篇
- k8s環境下處理容器時間問題的多種姿勢
- Golang基準測試
- 淺談Prometheus的資料儲存
- Golang單元測試