Golang介面型別-上篇

語言: CN / TW / HK

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 ~

參考:http://draveness.me/golang/docs/part2-foundation/ch04-basic/golang-interface/