Go語言全棧開發:接口

文章目錄

接口(interface)定義了一個對象的行爲規範,只定義規範不實現,由具體的對象來實現規範的細節。

接口

接口類型

在Go語言中接口(interface)是一種類型,一種抽象的類型。

interface是一組method的集合,是duck-type programming的一種體現。接口做的事情就像是定義一個協議(規則),只要一臺機器有洗衣服和甩乾的功能,我就稱它爲洗衣機。不關心屬性(數據),只關心行爲(方法)。

爲了保護你的Go語言職業生涯,請牢記接口(interface)是一種類型。

爲什麼要使用接口
type Cat struct{}

func (c Cat) Say() string { return "喵喵喵" }

type Dog struct{}

func (d Dog) Say() string { return "汪汪汪" }

func main() {
	c := Cat{}
	fmt.Println("貓:", c.Say())
	d := Dog{}
	fmt.Println("狗:", d.Say())
}

上面的代碼中定義了貓和狗,然後它們都會叫,你會發現main函數中明顯有重複的代碼,如果我們後續再加上豬、青蛙等動物的話,我們的代碼還會一直重複下去。那我們能不能把它們當成“能叫的動物”來處理呢?

像類似的例子在我們編程過程中會經常遇到:

比如一個網上商城可能使用支付寶、微信、銀聯等方式去在線支付,我們能不能把它們當成“支付方式”來處理呢?

比如三角形,四邊形,圓形都能計算周長和麪積,我們能不能把它們當成“圖形”來處理呢?

比如銷售、行政、程序員都能計算月薪,我們能不能把他們當成“員工”來處理呢?

Go語言中爲了解決類似上面的問題,就設計了接口這個概念。接口區別於我們之前所有的具體類型,接口是一種抽象的類型。當你看到一個接口類型的值時,你不知道它是什麼,唯一知道的是通過它的方法能做什麼。

接口的定義

Go語言提倡面向接口編程。

每個接口由數個方法組成,接口的定義格式如下:

type 接口類型名 interface{
    方法名1( 參數列表1 ) 返回值列表1
    方法名2( 參數列表2 ) 返回值列表2}

其中:

  • 接口名:使用type將接口定義爲自定義的類型名。Go語言的接口在命名時,一般會在單詞後面添加er,如有寫操作的接口叫Writer,有字符串功能的接口叫Stringer等。接口名最好要能突出該接口的類型含義。
  • 方法名:當方法名首字母是大寫且這個接口類型名首字母也是大寫時,這個方法可以被接口所在的包(package)之外的代碼訪問。
  • 參數列表、返回值列表:參數列表和返回值列表中的參數變量名可以省略。

舉個例子:

type writer interface{
    Write([]byte) error
}

當你看到這個接口類型的值時,你不知道它是什麼,唯一知道的就是可以通過它的Write方法來做一些事情。

實現接口的條件

一個對象只要全部實現了接口中的方法,那麼就實現了這個接口。換句話說,接口就是一個 需要實現的方法列表

我們來定義一個Sayer接口:

// Sayer 接口
type Sayer interface {
	say()
}

定義dogcat兩個結構體:

type dog struct {}

type cat struct {}

因爲Sayer接口裏只有一個say方法,所以我們只需要給dogcat 分別實現say方法就可以實現Sayer接口了。

// dog實現了Sayer接口
func (d dog) say() {
	fmt.Println("汪汪汪")
}

// cat實現了Sayer接口
func (c cat) say() {
	fmt.Println("喵喵喵")
}

接口的實現就是這麼簡單,只要實現了接口中的所有方法,就實現了這個接口。

接口類型變量

那實現了接口有什麼用呢?

接口類型變量能夠存儲所有實現了該接口的實例。 例如上面的示例中,Sayer類型的變量能夠存儲dogcat類型的變量。

func main() {
	var x Sayer // 聲明一個Sayer類型的變量x
	a := cat{}  // 實例化一個cat
	b := dog{}  // 實例化一個dog
	x = a       // 可以把cat實例直接賦值給x
	x.say()     // 喵喵喵
	x = b       // 可以把dog實例直接賦值給x
	x.say()     // 汪汪汪
}

Tips: 觀察下面的代碼,體味此處_的妙用

// 摘自gin框架routergroup.go
type IRouter interface{ ... }

type RouterGroup struct { ... }

var _ IRouter = &RouterGroup{}  // 確保RouterGroup實現了接口IRouter
值接收者和指針接收者實現接口的區別

使用值接收者實現接口和使用指針接收者實現接口有什麼區別呢?接下來我們通過一個例子看一下其中的區別。

我們有一個Mover接口和一個dog結構體。

type Mover interface {
	move()
}

type dog struct {}

| 值接收者實現接口

func (d dog) move() {
	fmt.Println("狗會動")
}

此時實現接口的是dog類型:

func main() {
	var x Mover
	var wangcai = dog{} // 旺財是dog類型
	x = wangcai         // x可以接收dog類型
	var fugui = &dog{}  // 富貴是*dog類型
	x = fugui           // x可以接收*dog類型
	x.move()
}

從上面的代碼中我們可以發現,使用值接收者實現接口之後,不管是dog結構體還是結構體指針*dog類型的變量都可以賦值給該接口變量。因爲Go語言中有對指針類型變量求值的語法糖,dog指針fugui內部會自動求值*fugui

| 指針接收者實現接口

同樣的代碼我們再來測試一下使用指針接收者有什麼區別:

func (d *dog) move() {
	fmt.Println("狗會動")
}
func main() {
	var x Mover
	var wangcai = dog{} // 旺財是dog類型
	x = wangcai         // x不可以接收dog類型
	var fugui = &dog{}  // 富貴是*dog類型
	x = fugui           // x可以接收*dog類型
}

此時實現Mover接口的是*dog類型,所以不能給x傳入dog類型的wangcai,此時x只能存儲*dog類型的值。

| 面試題

注意: 這是一道你需要回答“能”或者“不能”的題!

首先請觀察下面的這段代碼,然後請回答這段代碼能不能通過編譯?

type People interface {
	Speak(string) string
}

type Student struct{}

func (stu *Student) Speak(think string) (talk string) {
	if think == "sb" {
		talk = "你是個大帥比"
	} else {
		talk = "您好"
	}
	return
}

func main() {
	var peo People = Student{}
	think := "bitch"
	fmt.Println(peo.Speak(think))
}
類型與接口的關係

| 一個類型實現多個接口

一個類型可以同時實現多個接口,而接口間彼此獨立,不知道對方的實現。 例如,狗可以叫,也可以動。我們就分別定義Sayer接口和Mover接口,如下: Mover接口。

// Sayer 接口
type Sayer interface {
	say()
}

// Mover 接口
type Mover interface {
	move()
}

dog既可以實現Sayer接口,也可以實現Mover接口。

type dog struct {
	name string
}

// 實現Sayer接口
func (d dog) say() {
	fmt.Printf("%s會叫汪汪汪\n", d.name)
}

// 實現Mover接口
func (d dog) move() {
	fmt.Printf("%s會動\n", d.name)
}

func main() {
	var x Sayer
	var y Mover

	var a = dog{name: "旺財"}
	x = a
	y = a
	x.say()
	y.move()
}

| 多個類型實現同一接口

Go語言中不同的類型還可以實現同一接口 首先我們定義一個Mover接口,它要求必須由一個move方法。

// Mover 接口
type Mover interface {
	move()
}

例如狗可以動,汽車也可以動,可以使用如下代碼實現這個關係:

type dog struct {
	name string
}

type car struct {
	brand string
}

// dog類型實現Mover接口
func (d dog) move() {
	fmt.Printf("%s會跑\n", d.name)
}

// car類型實現Mover接口
func (c car) move() {
	fmt.Printf("%s速度70邁\n", c.brand)
}

這個時候我們在代碼中就可以把狗和汽車當成一個會動的物體來處理了,不再需要關注它們具體是什麼,只需要調用它們的move方法就可以了。

func main() {
	var x Mover
	var a = dog{name: "旺財"}
	var b = car{brand: "保時捷"}
	x = a
	x.move()
	x = b
	x.move()
}

上面的代碼執行結果如下:

旺財會跑
保時捷速度70

並且一個接口的方法,不一定需要由一個類型完全實現,接口的方法可以通過在類型中嵌入其他類型或者結構體來實現。

// WashingMachine 洗衣機
type WashingMachine interface {
	wash()
	dry()
}

// 甩幹器
type dryer struct{}

// 實現WashingMachine接口的dry()方法
func (d dryer) dry() {
	fmt.Println("甩一甩")
}

// 海爾洗衣機
type haier struct {
	dryer //嵌入甩幹器
}

// 實現WashingMachine接口的wash()方法
func (h haier) wash() {
	fmt.Println("洗刷刷")
}
接口嵌套

接口與接口間可以通過嵌套創造出新的接口。

// Sayer 接口
type Sayer interface {
	say()
}

// Mover 接口
type Mover interface {
	move()
}

// 接口嵌套
type animal interface {
	Sayer
	Mover
}

嵌套得到的接口的使用與普通接口一樣,這裏我們讓cat實現animal接口:

type cat struct {
	name string
}

func (c cat) say() {
	fmt.Println("喵喵喵")
}

func (c cat) move() {
	fmt.Println("貓會動")
}

func main() {
	var x animal
	x = cat{name: "花花"}
	x.move()
	x.say()
}
空接口

| 空接口的定義

func main() {
	// 定義一個空接口x
	var x interface{}
	s := "Hello 沙河"
	x = s
	fmt.Printf("type:%T value:%v\n", x, x)
	i := 100
	x = i
	fmt.Printf("type:%T value:%v\n", x, x)
	b := true
	x = b
	fmt.Printf("type:%T value:%v\n", x, x)
}

| 空接口的應用

空接口作爲函數的參數

使用空接口實現可以接收任意類型的函數參數。

// 空接口作爲函數參數
func show(a interface{}) {
	fmt.Printf("type:%T value:%v\n", a, a)
}

空接口作爲map的值

使用空接口實現可以保存任意值的字典。

// 空接口作爲map值
	var studentInfo = make(map[string]interface{})
	studentInfo["name"] = "沙河娜扎"
	studentInfo["age"] = 18
	studentInfo["married"] = false
	fmt.Println(studentInfo)
類型斷言

空接口可以存儲任意類型的值,那我們如何獲取其存儲的具體數據呢?

| 接口值

一個接口的值(簡稱接口值)是由一個具體類型具體類型的值兩部分組成的。這兩部分分別稱爲接口的動態類型動態值

我們來看一個具體的例子:

var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil

請看下圖分解:

在這裏插入圖片描述

想要判斷空接口中的值這個時候就可以使用類型斷言,其語法格式:

x.(T)

其中:

  • x:表示類型爲interface{}的變量
  • T:表示斷言x可能是的類型。

該語法返回兩個參數,第一個參數是x轉化爲T類型後的變量,第二個值是一個布爾值,若爲true則表示斷言成功,爲false則表示斷言失敗。

舉個例子:

func main() {
	var x interface{}
	x = "Hello 武漢"
	v, ok := x.(string)
	if ok {
		fmt.Println(v)
	} else {
		fmt.Println("類型斷言失敗")
	}
}

上面的示例中如果要斷言多次就需要寫多個if判斷,這個時候我們可以使用switch語句來實現:

func justifyType(x interface{}) {
	switch v := x.(type) {
	case string:
		fmt.Printf("x is a string,value is %v\n", v)
	case int:
		fmt.Printf("x is a int is %v\n", v)
	case bool:
		fmt.Printf("x is a bool is %v\n", v)
	default:
		fmt.Println("unsupport type!")
	}
}

因爲空接口可以存儲任意類型值的特點,所以空接口在Go語言中的使用十分廣泛。

關於接口需要注意的是,只有當有兩個或兩個以上的具體類型必須以相同的方式進行處理時才需要定義接口。不要爲了接口而寫接口,那樣只會增加不必要的抽象,導致不必要的運行時損耗。

練習題

使用接口的方式實現一個既可以往終端寫日誌也可以往文件寫日誌的簡易日誌庫。

【對比python】

#面向對象回顧
# 封裝
# 繼承
# 多態(python是隱式語言(不同於java,屬於顯式語言),本身就是多態,沒有提供非多態的實現方式)


#-----------------------------------------------------------------------------------------------------------------------
# 實現接口的支付示例
from abc import ABCMeta,abstractmethod

class Payment(metaclass=ABCMeta):
    @abstractmethod
    def pay(self,money):
        pass


class AliPay(Payment):
    def pay(self,money):
        print('支付寶支付%s元'%money)


class WeChatPay(Payment):
    def pay(self,money,arg): # 沒有按照接口中要求的參數實現,給出了警告
        print('微信支付%s元'%money)


class UnionPay(Payment):
    def pay(self,money):
        print('銀聯支付%s元'%money)


class ApplePay(Payment):
    def pay(self,money):
        print('蘋果支付%s元'%money)

# 函數簽名:函數名、參數個數和類型、返回值類型
# 接口:一種特殊的類,聲明瞭若干方法,要求實現該接口的類必須實現這些方法
# 作用:限制繼承接口的類的方法的名稱及調用方式;隱藏了類的內部實現(python不行,C++中可以只提供二進制的接口文件)
# 總結:接口就是一種抽象的基類(父類),限制繼承它的類必須實現接口中定義的某些方法

# python中對實現接口不嚴謹的示例
a = WeChatPay()
a.pay(200,12) # 並沒有報錯,對方法中的參數個數及類型沒有如java,C++中那樣嚴格限制

#-----------------------------------------------------------------------------------------------------------------------
# 設計模式中的六大原則

# 開放閉合原則
# 一個軟件實體如類、模塊和函數應該對擴展開放,對修改關閉。即軟件實體應儘量在不修改原有代碼的情況下進行擴展。
# 總結:修改代碼不行(關鍵代碼不能改),添加代碼可以

# 里氏(Liskov)替換原則
# 所有引用基類(父類)的地方必須能透明地使用其子類的對象。

# 依賴倒置原則
# 高層模塊不應該依賴低層模塊,二者都應該依賴其抽象;抽象不應該依賴細節;細節應該依賴抽象。換言之,要針對接口編程,而不是針對實現編程。

# 接口隔離原則
# 使用多個專門的接口,而不使用單一的總接口,即客戶端(高層的模塊或代碼)不應該依賴那些它不需要的接口。

"""
class Animal(metaclass=ABCMeta):
    @abstractmethod
    def walk(self):
        pass

    @abstractmethod
    def fly(self):
        pass

    @abstractmethod
    def swim(self):
        pass

class Tiger(Animal):
    pass

class Swan(Animal):
    pass

class Whale(Animal):
    pass
"""

# 上面的老虎類,天鵝類,鯨魚類都必須實現動物接口的所有方法,不滿足接口隔開原則

# 改進如下,把單一總接口,改爲多個專用接口:
class WalkAnimal(metaclass=ABCMeta):
    @abstractmethod
    def walk(self):
        pass

class FlyAnimal(metaclass=ABCMeta):
    @abstractmethod
    def fly(self):
        pass

class SwimAnimal(metaclass=ABCMeta):

    @abstractmethod
    def swim(self):
        pass

class Tiger(WalkAnimal):
    def walk(self):
        pass

class Swan(WalkAnimal,FlyAnimal):
    def walk(self):
        pass

    def fly(self):
        pass

class Whale(SwimAnimal):
    def swim(self):
        pass

# python中多繼承,常用在多實現接口時使用,而java是單繼承(只能繼承一個父類)多實現接口(但可以實現多個接口)

# 迪米特法則
# 一個軟件實體應當儘可能少地與其他實體發生相互作用。解耦,依賴越少越好

# 單一職責原則
# 不要存在多於一個導致類變更的原因。通俗的說,即一個類只負責一項職責。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章