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,調用接口方法時,只需要指定接口類型對應的結構體是什麼,因爲在定義接口時,已經聲明瞭此接口實現了SendSendAll兩個方法

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)
}

WechatSendersendsendAllsend有指針和值,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 ~

參考資料

[1]

https://draveness.me/golang/docs/part2-foundation/ch04-basic/golang-interface/

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/CP6CIUsia55tSxj5wu-7Kg