天天看點

Go 接口( Interface )

接口(interface)定義了一個對象的行為規範,隻定義規範不實作,由具體的對象來實作規範的細節。 原文傳送

1.1. 接口

1.1.1. 接口類型

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

interface是一組method的集合,是duck-type programming的一種展現。接口做的事情就像是定義一個協定(規則),隻要一台機器有洗衣服和甩幹的功能,我就稱它為洗衣機。不關心屬性(資料),隻關心行為(方法)。

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

1.1.2. 為什麼要使用接口

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語言中為了解決類似上面的問題,就設計了接口這個概念。接口差別于我們之前所有的具體類型,接口是一種抽象的類型。當你看到一個接口類型的值時,你不知道它是什麼,唯一知道的是通過它的方法能做什麼。

1.1.3. 接口的定義

Go語言提倡面向接口程式設計。

  • 接口是一個或多個方法簽名的集合。
  • 任何類型的方法集中隻要擁有該接口'對應的全部方法'簽名。
  • 就表示它 "實作" 了該接口,無須在該類型上顯式聲明實作了哪個接口。
  • 這稱為Structural Typing。
  • 所謂對應方法,是指有相同名稱、參數清單 (不包括參數名) 以及傳回值。
  • 當然,該類型還可以有其他方法。
  • 接口隻有方法聲明,沒有實作,沒有資料字段。
  • 接口可以匿名嵌入其他接口,或嵌入到結構中。
  • 對象指派給接口時,會發生拷貝,而接口内部存儲的是指向這個複制品的指針,既無法修改複制品的狀态,也無法擷取指針。
  • 隻有當接口存儲的類型和對象都為nil時,接口才等于nil。
  • 接口調用不會做receiver的自動轉換。
  • 接口同樣支援匿名字段方法。
  • 接口也可實作類似OOP中的多态。
  • 空接口可以作為任何類型資料的容器。
  • 一個類型可實作多個接口。
  • 接口命名習慣以 er 結尾。

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

type 接口類型名 interface{
    方法名1( 參數清單1 ) 傳回值清單1
    方法名2( 參數清單2 ) 傳回值清單2
    …
}
           

其中:

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

舉個例子:

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

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

1.1.4. 實作接口的條件

一個對象隻要全部實作了接口中的方法,那麼就實作了這個接口。換句話說,接口就是一個需要實作的方法清單。

我們來定義一個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("喵喵喵")
}
           

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

1.1.5. 接口類型變量

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

接口類型變量能夠存儲所有實作了該接口的執行個體。 例如上面的示例中,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()     // 汪汪汪
}
           

1.1.6. 值接收者和指針接收者實作接口的差別

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

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

type Mover interface {
    move()
}

type dog struct {}
1.1.7. 值接收者實作接口
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

1.1.8. 指針接收者實作接口

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

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

類型的值。

1.1.9. 下面的代碼是一個比較好的面試題

請問下面的代碼是否能通過編譯?

type People interface {
    Speak(string) string
}

type Student struct{}

func (stu *Stduent) 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))
}
           

1.2. 類型與接口的關系

1.2.1. 一個類型實作多個接口

一個類型可以同時實作多個接口,而接口間彼此獨立,不知道對方的實作。 例如,狗可以叫,也可以動。我們就分别定義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()
}
           

1.2.2. 多個類型實作同一接口

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("洗刷刷")
}
           

1.2.3. 接口嵌套

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

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

1.3. 空接口

1.3.1. 空接口的定義

空接口是指沒有定義任何方法的接口。是以任何類型都實作了空接口。

空接口類型的變量可以存儲任意類型的變量。

func main() {
    // 定義一個空接口x
    var x interface{}
    s := "pprof.cn"
    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)
}
           

1.3.2. 空接口的應用

空接口作為函數的參數

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

// 空接口作為函數參數
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)
           

1.3.3. 類型斷言

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

接口值

一個接口的值(簡稱接口值)是由一個具體類型和具體類型的值兩部分組成的。這兩部分分别稱為接口的動态類型和動态值。

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

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 = "pprof.cn"
    v, ok := x.(string)
    if ok {
	fmt.Println(v)
    } else {
	fmt.Println("類型斷言失敗")
    }
}
           
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!")
    }
}