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

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

See you ~

關注公眾號加群,更多原創干貨與你分享 ~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM