天天看點

Go語言基礎之接口Go語言基礎之接口

Go語言基礎之接口

  • Go語言基礎之接口
    • 接口
    • 接口類型
    • 為什麼要使用接口
    • 接口的定義
    • 實作接口的條件
    • 接口類型變量
    • 值接收者和指針接收者實作接口的差別
      • 值接收者實作接口
      • 指針接收者實作接口
      • 面試題
    • 類型與接口的關系
      • 一個類型實作多個接口
      • 多個類型實作同一接口
    • 接口嵌套
    • 空接口
      • 空接口的定義
      • 空接口的應用
    • 類型斷言
      • 接口值

Go語言基礎之接口

筆記來源:

李文周老師的部落格—https://www.liwenzhou.com/posts/Go/12_interface/#autoid-1-10-1

接口(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()
}
           

定義

dog

cat

兩個結構體:

type dog struct {}

type cat struct {}
           

因為

Sayer

接口裡隻有一個

say

方法,是以我們隻需要給

dog

cat

分别實作

say

方法就可以實作

Sayer

接口了。

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

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

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

接口類型變量

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

接口類型變量能夠存儲所有實作了該接口的執行個體。 例如上面的示例中,

Sayer

類型的變量能夠存儲

dog

cat

類型的變量。

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語言中的使用十分廣泛。

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

繼續閱讀