Golang 學(xué)習(xí)筆記七 接口

一、概念

《快學(xué) Go 語言》第 9 課 —— 接口

1.接口定義
Go 語言的接口類型非常特別,它的作用和 Java 語言的接口一樣,但是在形式上有很大的差別。Java 語言需要在類的定義上顯式實(shí)現(xiàn)了某些接口,才可以說這個(gè)類具備了接口定義的能力。但是 Go 語言的接口是隱式的,只要結(jié)構(gòu)體上定義的方法在形式上(名稱、參數(shù)和返回值)和接口定義的一樣,那么這個(gè)結(jié)構(gòu)體就自動實(shí)現(xiàn)了這個(gè)接口,我們就可以使用這個(gè)接口變量來指向這個(gè)結(jié)構(gòu)體對象。下面我們看個(gè)例子

package main

import "fmt"

// 可以聞
type Smellable interface {
  smell()
}

// 可以吃
type Eatable interface {
  eat()
}

// 蘋果既可能聞又能吃
type Apple struct {}

func (a Apple) smell() {
  fmt.Println("apple can smell")
}

func (a Apple) eat() {
  fmt.Println("apple can eat")
}

// 花只可以聞
type Flower struct {}

func (f Flower) smell() {
  fmt.Println("flower can smell")
}

func main() {
  var s1 Smellable
  var s2 Eatable
  var apple = Apple{}
  var flower = Flower{}
  s1 = apple
  s1.smell()
  s1 = flower
  s1.smell()
  s2 = apple
  s2.eat()
}

--------------------
apple can smell
flower can smell
apple can eat

上面的代碼定義了兩種接口,Apple 結(jié)構(gòu)體同時(shí)實(shí)現(xiàn)了這兩個(gè)接口,而 Flower 結(jié)構(gòu)體只實(shí)現(xiàn)了 Smellable 接口。我們并沒有使用類似于 Java 語言的 implements 關(guān)鍵字,結(jié)構(gòu)體和接口就自動產(chǎn)生了關(guān)聯(lián)。

2.空接口
如果一個(gè)接口里面沒有定義任何方法,那么它就是空接口,任意結(jié)構(gòu)體都隱式地實(shí)現(xiàn)了空接口。

Go 語言為了避免用戶重復(fù)定義很多空接口,它自己內(nèi)置了一個(gè),這個(gè)空接口的名字特別奇怪,叫 interface{} ,初學(xué)者會非常不習(xí)慣。之所以這個(gè)類型名帶上了大括號,那是在告訴用戶括號里什么也沒有。我始終認(rèn)為這種名字很古怪,它讓代碼看起來有點(diǎn)丑陋。

空接口里面沒有方法,所以它也不具有任何能力,其作用相當(dāng)于 Java 的 Object 類型,可以容納任意對象,它是一個(gè)萬能容器。比如一個(gè)字典的 key 是字符串,但是希望 value 可以容納任意類型的對象,類似于 Java 語言的 Map 類型,這時(shí)候就可以使用空接口類型 interface{}。

package main

import "fmt"

func main() {
    // 連續(xù)兩個(gè)大括號,是不是看起來很別扭
    var user = map[string]interface{}{
        "age": 30,
        "address": "Beijing Tongzhou",
        "married": true,
    }
    fmt.Println(user)
    // 類型轉(zhuǎn)換語法來了
    var age = user["age"].(int)
    var address = user["address"].(string)
    var married = user["married"].(bool)
    fmt.Println(age, address, married)
}

-------------
map[age:30 address:Beijing Tongzhou married:true]
30 Beijing Tongzhou true

代碼中 user 字典變量的類型是 map[string]interface{},從這個(gè)字典中直接讀取得到的 value 類型是 interface{},需要通過類型轉(zhuǎn)換才能得到期望的變量。

3.用接口來模擬多態(tài)

package main

import "fmt"

type Fruitable interface {
    eat()
}

type Fruit struct {
    Name string  // 屬性變量
    Fruitable  // 匿名內(nèi)嵌接口變量
}

func (f Fruit) want() {
    fmt.Printf("I like ")
    f.eat() // 外結(jié)構(gòu)體會自動繼承匿名內(nèi)嵌變量的方法
}

type Apple struct {}

func (a Apple) eat() {
    fmt.Println("eating apple")
}

type Banana struct {}

func (b Banana) eat() {
    fmt.Println("eating banana")
}

func main() {
    var f1 = Fruit{"Apple", Apple{}}
    var f2 = Fruit{"Banana", Banana{}}
    f1.want()
    f2.want()
}

---------
I like eating apple
I like eating banana

使用這種方式模擬多態(tài)本質(zhì)上是通過組合屬性變量(Name)和接口變量(Fruitable)來做到的,屬性變量是對象的數(shù)據(jù),而接口變量是對象的功能,將它們組合到一塊就形成了一個(gè)完整的多態(tài)性的結(jié)構(gòu)體。
《GoInAction》第118頁也提供了一個(gè)例子:

// Sample program to show how polymorphic behavior with interfaces.
package main

import (
    "fmt"
)

// notifier is an interface that defines notification
// type behavior.
type notifier interface {
    notify()
}

// user defines a user in the program.
type user struct {
    name  string
    email string
}

// notify implements the notifier interface with a pointer receiver.
func (u *user) notify() {
    fmt.Printf("Sending user email to %s<%s>\n",
        u.name,
        u.email)
}

// admin defines a admin in the program.
type admin struct {
    name  string
    email string
}

// notify implements the notifier interface with a pointer receiver.
func (a *admin) notify() {
    fmt.Printf("Sending admin email to %s<%s>\n",
        a.name,
        a.email)
}

// main is the entry point for the application.
func main() {
    // Create a user value and pass it to sendNotification.
    bill := user{"Bill", "bill@email.com"}
    sendNotification(&bill)

    // Create an admin value and pass it to sendNotification.
    lisa := admin{"Lisa", "lisa@email.com"}
    sendNotification(&lisa)
}

// sendNotification accepts values that implement the notifier
// interface and sends notifications.
func sendNotification(n notifier) {
    n.notify()
}

在第 53 行中,我們再次聲明了多態(tài)函數(shù) sendNotification,這個(gè)函數(shù)接受一個(gè)實(shí)現(xiàn)了notifier 接口的值作為參數(shù)。既然任意一個(gè)實(shí)體類型都能實(shí)現(xiàn)該接口,那么這個(gè)函數(shù)可以針對任意實(shí)體類型的值來執(zhí)行 notifier 方法。因此,這個(gè)函數(shù)就能提供多態(tài)的行為。

4.接口的組合繼承
接口的定義也支持組合繼承,比如我們可以將兩個(gè)接口定義合并為一個(gè)接口如下

type Smellable interface {
  smell()
}

type Eatable interface {
  eat()
}

type Fruitable interface {
  Smellable
  Eatable
}

這時(shí) Fruitable 接口就自動包含了 smell() 和 eat() 兩個(gè)方法,它和下面的定義是等價(jià)的。

type Fruitable interface {
  smell()
  eat()
}

5.接口變量的賦值
變量賦值本質(zhì)上是一次內(nèi)存淺拷貝,切片的賦值是拷貝了切片頭,字符串的賦值是拷貝了字符串的頭部,而數(shù)組的賦值呢是直接拷貝整個(gè)數(shù)組。接口變量的賦值會不會不一樣呢?接下來我們做一個(gè)實(shí)驗(yàn)

package main

import "fmt"

type Rect struct {
    Width int
    Height int
}

func main() {
    var a interface {}
    var r = Rect{50, 50}
    a = r

    var rx = a.(Rect)
    r.Width = 100
    r.Height = 100
    fmt.Println(rx)
}

------
{50 50}

6.嵌入類型
《GoInAction》也提供了例子

// Sample program to show how to embed a type into another type and
// the relationship between the inner and outer type.
package main

import (
    "fmt"
)

// user defines a user in the program.
type user struct {
    name  string
    email string
}

// notify implements a method that can be called via
// a value of type user.
func (u *user) notify() {
    fmt.Printf("Sending user email to %s<%s>\n",
        u.name,
        u.email)
}

// admin represents an admin user with privileges.
type admin struct {
    user  // Embedded Type
    level string
}

// main is the entry point for the application.
func main() {
    // Create an admin user.
    ad := admin{
        user: user{
            name:  "john smith",
            email: "john@yahoo.com",
        },
        level: "super",
    }

    // We can access the inner type's method directly.
    ad.user.notify()

    // The inner type's method is promoted.
    ad.notify()
}

這展示了內(nèi)部類型是如何存在于外部類型內(nèi),并且總是可訪問的。不過,借助內(nèi)部類型提升,notify 方法也可以直接通過 ad 變量來訪問

再改造一下:

// notifier is an interface that defined notification
// type behavior.
type notifier interface {
    notify()
}
// sendNotification accepts values that implement the notifier
// interface and sends notifications.
func sendNotification(n notifier) {
    n.notify()
}
// main is the entry point for the application.
func main() {
    // Create an admin user.
    ad := admin{
        user: user{
            name:  "john smith",
            email: "john@yahoo.com",
        },
        level: "super",
    }

    // Send the admin user a notification.
    // The embedded inner type's implementation of the
    // interface is "promoted" to the outer type.
    sendNotification(&ad)
}

在代碼清單 5-58 的第 37 行,我們創(chuàng)建了一個(gè)名為 ad 的變量,其類型是外部類型 admin。這個(gè)類型內(nèi)部嵌入了 user 類型。之后第 48 行,我們將這個(gè)外部類型變量的地址傳給 sendNotification 函數(shù)。編譯器認(rèn)為這個(gè)指針實(shí)現(xiàn)了 notifier 接口,并接受了這個(gè)值的傳遞。不過如果看一下整個(gè)示例程序,就會發(fā)現(xiàn) admin 類型并沒有實(shí)現(xiàn)這個(gè)接口。由于內(nèi)部類型的提升,內(nèi)部類型實(shí)現(xiàn)的接口會自動提升到外部類型。這意味著由于內(nèi)部類型的實(shí)現(xiàn),外部類型也同樣實(shí)現(xiàn)了這個(gè)接口。

如果外部類型并不需要使用內(nèi)部類型的實(shí)現(xiàn),而想使用自己的一套實(shí)現(xiàn),該怎么辦?

// notify implements a method that can be called via
// a value of type Admin.
func (a *admin) notify() {
    fmt.Printf("Sending admin email to %s<%s>\n",
        a.name,
        a.email)
}

// main is the entry point for the application.
func main() {
    // Create an admin user.
    ad := admin{
        user: user{
            name:  "john smith",
            email: "john@yahoo.com",
        },
        level: "super",
    }

    // Send the admin user a notification.
    // The embedded inner type's implementation of the
    // interface is NOT "promoted" to the outer type.
    sendNotification(&ad)

    // We can access the inner type's method directly.
    ad.user.notify()

    // The inner type's method is NOT promoted.
    ad.notify()
}

---------------------------------------------
Sending admin email to john smith<john@yahoo.com>
Sending user email to john smith<john@yahoo.com>
Sending admin email to john smith<john@yahoo.com>

這表明,如果外部類型實(shí)現(xiàn)了 notify 方法,內(nèi)部類型的實(shí)現(xiàn)就不會被提升。不過內(nèi)部類型的值一直存在,因此還可以通過直接訪問內(nèi)部類型的值,來調(diào)用沒有被提升的內(nèi)部類型實(shí)現(xiàn)的方法。

關(guān)于嵌套結(jié)構(gòu)體的應(yīng)用,可以在Golang sort自定義排序中看到

二、值接收和引用接收

Golang 學(xué)習(xí)筆記六 函數(shù)function和方法method的區(qū)別講方法時(shí),有個(gè)例子,當(dāng)通過值或指針調(diào)用方法時(shí),go編譯器會自動幫我們處理方法接收者的不一致。

type user struct{
    name string
    email string
}

func (u user) printName(){
    fmt.Printf("name: %s\n", u.name)
}

func (u *user) printEmail(){
    fmt.Printf("email: %s\n", u.email)
}

func main() {
    bill := user{"bill","bill@gmail.com"}
    lisa := &user{"lisa","lisa@gmail.com"};

    bill.printName()
    lisa.printName()

    bill.printEmail()
    lisa.printEmail()
}

正常打?。?/p>

name: bill
name: lisa
email: bill@gmail.com
email: lisa@gmail.com

但是,如果通過接口類型的值調(diào)用方法,規(guī)則有很大不同:
在上面代碼中加上兩個(gè)接口

type printNamer interface{
    printName()
}

type printEmailer interface{
    printEmail()
}

func sendPrintName(n printNamer) {
    n.printName()
}

func sendPrintEmail(n printEmailer){
    n.printEmail()
}

func main() {
        ...
    sendPrintName(bill)
    sendPrintName(lisa)

    sendPrintEmail(bill)
    sendPrintEmail(lisa)

這里sendPrintEmail(bill)編譯不通過,提示:cannot use bill (type user) as type printEmailer in argument to sendPrintEmail:user does not implement printEmailer (printEmail method has pointer receiver)

觀察一下區(qū)別,bill是一個(gè)值,printEmail接收者是個(gè)指針,失敗了。但是另外一個(gè)不一致的卻能通過,那就是lisa是個(gè)指針,但是printName的接收者要求是值,為啥就能通過呢。

在《Go in Action》第118頁描述了方法集的規(guī)則:
使用指針作為接收者聲明的方法,只能在接口類型的值是一個(gè)指針的時(shí)候被調(diào)用。使用值作為接收者聲明的方法,在接口類型的值為值或者指針時(shí),都可以被調(diào)用。

為什么會有這種限制?事實(shí)上,編譯器并不是總能自動獲得一個(gè)值的地址,如代碼清單 5-46 所示。

代碼清單 5-46 listing46.go
01 // 這個(gè)示例程序展示不是總能
02 // 獲取值的地址
03 package main
04
05 import "fmt"
06
07 // duration 是一個(gè)基于 int 類型的類型
08 type duration int
09
10 // 使用更可讀的方式格式化 duration 值
11 func (d *duration) pretty() string {
12  return fmt.Sprintf("Duration: %d", *d)
13 }
14
15 // main 是應(yīng)用程序的入口
16 func main() {
17  duration(42).pretty()
18
19  // ./listing46.go:17: 不能通過指針調(diào)用 duration(42)的方法
20  // ./listing46.go:17: 不能獲取 duration(42)的地址
21 }

這里編譯通過,運(yùn)行也會報(bào)錯。我們改成變量調(diào)用就可以了:

    dd := duration(42)
    dd.pretty()
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容