接口類型的合理運用

接口定義

在 Go 語言的語境中,當咱們在談論「接口」的時候,必定指的是接口類型。由於接口類型與其餘數據類型不一樣,它是無法被值化的,或者說是無法被實例化的。數據結構

type 接口名 interface {
	方法列表
}

接口類型聲明中的這些方法所表明的就是該接口的方法集合。一個接口的方法集合就是它的所有特徵設計

實現接口

對於任何數據類型,只要它的方法集合中徹底包含了一個接口的所有特徵(即所有的方法),那麼它就必定是這個接口的實現類型。指針

怎樣斷定一個數據類型的某一個方法實現的就是某個接口類型中的某個方法呢?

兩個充分必要條件,一個是「兩個方法的簽名須要徹底一致」,另外一個是「兩個方法的名稱要如出一轍」。code

duck typing

是一種無侵入式的接口實現方式。對象

這是程序設計中的一種類型推斷風格,這種風格適用於動態語言(好比PHP、Python、Ruby、Typescript、Perl、Objective-C、Lua、Julia、JavaScript、Java、Groovy、C#等)和某些靜態語言(好比Golang,通常來講,靜態類型語言在編譯時便已肯定了變量的類型,可是Golang的實現是:在編譯時推斷變量的類型),支持"鴨子類型"的語言的解釋器/編譯器將會在解析(Parse)或編譯時,推斷對象的類型。繼承

" In other words, don't check whether it IS-a duck: check whether it QUACKS-like-a duck, WALKS-like-a duck, etc, etc, depending on exactly what subset of duck-like behaviour you need to play your language-games with" [2]接口

"鴨子類型"像多態同樣工做,可是沒有繼承。ip

「鴨子類型」的語言是這麼推斷的:一隻鳥走起來像鴨子、遊起泳來像鴨子、叫起來也像鴨子,那它就能夠被當作鴨子。也就是說,它不關注對象的類型,而是關注對象具備的行爲(方法)。編譯器

動態類型&動態值&靜態類型

package main

import "fmt"

type Pet interface {
	SetName(name string)
	Name() string
	Category() string
}

type Dog struct {
	name string // 名字。
}

func (dog *Dog) SetName(name string) {
	dog.name = name
}

func (dog Dog) Name() string {
	return dog.name
}

func (dog Dog) Category() string {
	return "dog"
}

func main() {
	// 示例1。
	dog := Dog{"little pig"}
	_, ok := interface{}(dog).(Pet)
	fmt.Printf("Dog implements interface Pet: %v\n", ok)
	_, ok = interface{}(&dog).(Pet)
	fmt.Printf("*Dog implements interface Pet: %v\n", ok)
	fmt.Println()

	// 示例2。
	var pet Pet = &dog
	fmt.Printf("This pet is a %s, the name is %q.\n",
		pet.Category(), pet.Name())
}

對於一個接口類型的變量來講,咱們賦給它的值能夠被叫作它的實際值(也稱動態值),而該值的類型能夠被叫作這個變量的實際類型(也稱動態類型)string

對於變量pet來說,它的動態類型是*Dog,動態值就是&dog ,它的靜態類型就是Pet,而且永遠是Pet,可是它的動態類型卻會隨着咱們賦給它的動態值而變化。

爲接口變量賦值

當咱們爲一個接口變量賦值時會發生什麼?

首選記住一個規則:若是咱們使用一個變量給另一個變量賦值,那麼真正賦給後者的,並非前者持有的那個值,而是該值的一個副本。

接口類型值的存儲方式和結構

接口類型自己是沒法被值化的。在咱們賦予它實際的值以前,它的值必定會是nil,這也是它的零值。

當咱們給一個接口變量賦值的時候,該變量的動態類型會與它的動態值一塊兒被存儲在一個專用的數據結構中。咱們就把這個專用的數據結構叫作iface吧,在 Go 語言的runtime包中它其實就叫這個名字。iface的實例會包含兩個指針,一個是指向類型信息的指針,另外一個是指向動態值的指針。這裏 的類型信息是由另外一個專用數據結構的實例承載的,其中包含了動態值的類型,以及使它實現了接口的方法和調用它們的途徑,等等。總之,接口變量被賦予動態值的時候,存儲的是包含了這個動態值的副本的一個結構更加複雜的值。

接口變量賦值爲真正的nil

在 Go 語言中,咱們把由字面量nil表示的值叫作無類型的nil。這是真正的nil,由於它的類型也是nil的。

只要咱們把一個有類型的nil賦給接口變量,那麼這個變量的值就必定不會是那個真正的nil。

怎樣才能讓一個接口變量的值真正爲nil呢?要麼只聲明它但不作初始化,要麼直接把字面量nil賦給它。

接口的組合

接口類型間的嵌入也被稱爲接口的組合。接口類型間的嵌入比結構體字段的嵌入要更簡單一些,由於它不會涉及方法間的「屏蔽」。只要組合的接口之間有同名的方法就會產生衝突,從而沒法經過編譯,即便同名方法的簽名彼此不一樣也會是如此。所以,接口的組合根本不可能致使「屏蔽」現象的出現。

Go 語言團隊鼓勵咱們聲明體量較小的接口,並建議咱們經過這種接口間的組合來擴展程序、增長程序的靈活性

這是由於相比於包含不少方法的大接口而言,小接口能夠更加專一地表達某一種能力或某一類特徵,同時也更容易被組合在一塊兒。

Go 語言標準庫代碼包io中的ReadWriteCloser接口和ReadWriter接口就是這樣的例子,它們都是由若干個小接口組合而成的。以io.ReadWriteCloser接口爲例,它是由io.Reader、io.Writer和io.Closer這三個接口組成的。

這三個接口都只包含了一個方法,是典型的小接口。它們中的每個都只表明了一種能力,分別是讀出、寫入和關閉。咱們編寫這幾個小接口的實現類型一般都會很容易。而且,一旦咱們同時實現了它們,就等於實現了它們的組合接口io.ReadWriteCloser。即便咱們只實現了io.Reader和io.Writer,那麼也等同於實現了io.ReadWriter接口,由於後者就是前兩個接口組成的。能夠看到,這幾個io包中的接口共同組成了一個接口矩陣。它們既相互關聯又獨立存在

相關文章
相關標籤/搜索