14 Go語言——接口interface詳解

Go語言——接口interface詳解

一、Duck Typing 概念

go語言中的duck typing並非真正的duck typing,可是他是相似的概念,go語言接口的實現就能夠看作爲duck typing。舉例:什麼是鴨子? 數據結構

其餘面向對象的語言可能認爲,活生生的鴨子纔是鴨子,要定義它的屬性和方法。可是go語言中,鴨子的定義並非真實的對象,它是由使用者來決定到底什麼是鴨子。好比,真的鴨子和玩具的鴨子。像鴨子走路,像鴨子叫(長得像鴨子),那麼就是鴨子。因此,go語言中的接口就是這樣,它沒必要顯示的去聲明它,它只關注是否實現了相應的方法。它描述的事物的外部行爲,而非內部結構併發

面向對象的繼承、抽象接口等等目的都是代碼的複用。既然是複用,那就要從使用者的角度去想,我認爲是什麼樣子它就是什麼樣子。我只關心這段代碼結構能作哪些事情,我複用它,我才無論它符不符合常識。函數

go語言就是一個結構化類型系統,相似 duck typing。只要實現了接口的全部方法,就表示該類型實現了該接口。ui

二、GO 語言interface特色

  • 接口是一個或多個方法簽名的集合
  • 只要某個類型擁有該接口的全部方法簽名,即算實現該接口,無需顯示聲明實現了哪一個接口,這稱爲 Structural Typing
  • 接口只有方法聲明,沒有實現,沒有數據字段
  • 接口能夠匿名嵌入其它接口,或嵌入到結構中
  • 將對象賦值給接口時,會發生拷貝,而接口內部存儲的是指向這個複製品的指針,既沒法修改複製品的狀態,也沒法獲取指針
  • 只有當接口存儲的類型和對象都爲nil時,接口才等於nil
  • 接口調用不會作receiver的自動轉換
  • 接口一樣支持匿名字段方法
  • 接口也可實現相似OOP中的多態
  • 空接口能夠做爲任何類型數據的容器

三、接口定義

3.1 接口類型

go語言中的接口也是一種類型,他具體描述了一系列方法的集合,一個實現了這些方法的具體類型是這個接口類型的實例。 定義接口很簡單,使用type關鍵字,其實就是定義一個結構體,可是內部只有方法的聲明,沒有實現。spa

type Stringer interface {//接口的定義就是如此的簡單。
    String() string
}

3.2 接口的實現方式

go語言接口的獨特之處就是,不須要顯示的去實現接口。一個類型若是擁有一個接口須要的全部方法,那麼這個類型就實現了這個接口。 這種隱式實現接口的方式,同時也提升了靈活性。指針

type Stringer interface {
    String() string
}
type Printer interface {
    Stringer // 接口嵌⼊。
    Print()
}
type User struct {
    id int
    name string
}
func (self *User) String() string {
    return fmt.Sprintf("user %d, %s", self.id, self.name)
}
func (self *User) Print() {
    fmt.Println(self.String())
}
func main() {
    var t Printer = &User{1, "Tom"} // *User ⽅法集包含 String、 Print。
    t.Print()
}

上面的代碼能夠看到,一個類型只要實現了接口定義的全部方法(是指有相同名稱、參數列表 (不包括參數名) 以及返回值 ),那麼這個類型就實現了這個接口,能夠說這個類型如今是這個接口類型,能夠直接進行賦值(其實也是隱式轉換),好比var t Printer = &User{1, "Tom"}。 那麼既然如此,一個類型就能夠實現多個接口,只要它擁有了這些接口類型的全部方法,那麼這個類型就是實現了多個接口。同時這個類型也就是多種形式的存在,反過來講一個接口能夠被不一樣類型實現,這就是go語言中的多態了。code

3.3 interface{}空接口的實現

空接⼝ interface{} 沒有任何⽅法簽名,也就意味着任何類型都實現了空接⼝。其做⽤相似⾯向對象語⾔中的根對象 object。對象

3.4 類型斷言

一個類型斷言檢查接口類型實例是否爲某一類型 。語法爲x.(T) ,x爲類型實例,T爲目標接口的類型。好比排序

value, ok := x.(T) x :表明要判斷的變量,T :表明被判斷的類型,value:表明返回的值,ok:表明是否爲該類型。即:ok partern方式。注意:x 必須爲inteface類型,否則會報錯。繼承

不過咱們通常用switch進行判斷,叫作 type switch。注意:不支持fallthrough.

func main() {
    var o interface{} = &User{1, "Tom"}
    switch v := o.(type) {
        case nil:             // o == nil
            fmt.Println("nil")
        case fmt.Stringer:     // interface
            fmt.Println(v)
        case func() string: // func
            fmt.Println(v())
        case *User:         // *struct
            fmt.Printf("%d, %s\n", v.id, v.name)
        default:
            fmt.Println("unknown")
    }
}

3.5 接口轉換

能夠將擁有超集的接口轉換爲子集的接口,反之出錯

type User struct {
    id int
    name string
}
func (self *User) String() string {
    return fmt.Sprintf("%d, %s", self.id, self.name)
}
func main() {
    var o interface{} = &User{1, "Tom"}
    if i, ok := o.(fmt.Stringer); ok { // ok-idiom
        fmt.Println(i)
    }
    u := o.(*User)
    // u := o.(User) // panic: interface is *main.User, not main.User
    fmt.Println(u)
}

經過類型判斷,若是不一樣類型轉換會發生panic.

3.6 匿名接口

匿名接口可用做變量類型,或者是結構成員。

type Tester struct {
    s interface {
        String() string
    }
}
type User struct {
    id int
    name string
}
func (self *User) String() string {
    return fmt.Sprintf("user %d, %s", self.id, self.name)
}
func main() {
    t := Tester{&User{1, "Tom"}}
    fmt.Println(t.s.String())
}
//輸出:
user 1, Tom

四、接口的內部實現

4.1 接口值

接口值可使用 == 和 !=來進行比較。兩個接口值相等僅當它們都是nil值或者它們的動態類型相同,而且動態值也根據這個動態類型的==操做相等。由於接口值是可比較的,因此它們能夠用在map的鍵或者做爲switch語句的操做數。
然而,若是兩個接口值的動態類型相同,可是這個動態類型是不可比較的(好比切片) ,將它們進行比較就會失敗而且panic。

那麼接口值內部究竟是什麼結構呢?

4.2 接口內部結構

// 沒有方法的interface
type eface struct {
    _type *_type   //類型信息
    data  unsafe.Pointer  //數據指針
}

// 記錄着Go語言中某個數據類型的基本特徵,_type是go全部類型的公共描述
//能夠簡單的認爲,接口能夠經過一個  _type *_type 直接或間接表述go全部的類型就能夠了
type _type struct {
    size       uintptr    //類型的大小
    ptrdata    uintptr    //存儲全部指針的內存前綴的大小
    hash       uint32    //類型的hash
    tflag      tflag    //類型的tags
    align      uint8    //結構體內對齊
    fieldalign uint8    //結構體做爲field時的對齊
    kind       uint8    //類型編號 定義於 runtime/typekind.go
    alg        *typeAlg    // 類型元方法 存儲hash 和equal兩個操做。
    gcdata    *byte        //GC 相關信息
    str       nameOff    //類型名字的偏移
    ptrToThis typeOff
}

// 有方法的interface
type iface struct {
    tab  *itab
    data unsafe.Pointer
}

type itab struct {
    inter  *interfacetype    //接口定義的類型信息
    _type  *_type            //接口實際指向值的類型信息
    link   *itab
    hash   uint32
    bad    bool
    inhash bool
    unused [2]byte
    fun    [1]uintptr        //接口方法實現列表,即函數地址列表,按字典序排序
}

// interface數據類型對應的type
type interfacetype struct {
    typ     _type
    pkgpath name
    mhdr    []imethod
}

存在兩種interface,一種是帶有方法的interface,一種是不帶方法的interface。

對於不帶方法的接口類型,Go語言中的全部變量均可以賦值給interface{}變量,interface能夠表述go全部的類型,_type存儲類型信息,data存儲類型的值的指針,指向實際值或者實際值的拷貝。

對於帶方法的接口類型,tab *itab 存儲指向了iTable的指針,ITable存儲了類型相關的信息以及相關方法集,而data 一樣存儲了實例值的指針,指向實際值或者是實際值的一個拷貝。

實現了interface中定義方法的變量能夠賦值給帶方法的interface變量,而且能夠經過interface直接調用對應的方法,實現了其它面嚮對象語言的多態的概念。

go語言interface的源碼錶示,接口實際上是一個兩個字段長度的數據結構。因此任何一個interface變量都是佔用16個byte的內存空間。從大的方面來講,如圖:

在這裏插入圖片描述

var n notifier n=user("Bill") 將一個實現了notifier接口實例user賦給變量n。那咱們先來看有方法的接口的內部是怎麼樣的。接口n 內部兩個字段 tab *itab 和 data unsafe.Pointer, 第一個字段存儲的是指向ITable(接口表)的指針,這個內部表包括已經存儲值的類型和與這個值相關聯的一組方法。第二個字段存儲的是,指向所存儲值的指針。注意:這裏是將一個值賦值給接口,並不是指針,那麼就會先將值拷貝一份,開闢內存空間存儲,而後將此內存地址賦給接口的data字段。也就是說,值傳遞時,接口存儲的值的指針實際上是指向一個副本。

在這裏插入圖片描述

若是是將指針賦值給接口類型,那麼第二個字段data存儲的就是指針的拷貝,指向的是原來的內存。

再進一步瞭解,內部是如何存儲的。

沒有方法的interface 內部變量第一個字段爲*_type 類型,這個_type記錄這某種數據類型的基本信息,好比佔用內存大小(size),數據類型名稱等等。而後,第二個字段存儲的仍是指向值的指針。

每種數據類型都存在一個與之對應的_type結構體(Go語言原生的各類數據類型,用戶自定義的結構體,用戶自定義的interface等等)。

在這裏引用的https://www.jianshu.com/p/700...

在這裏插入圖片描述

小結:總的來講接口是一個類型,它是一個struct,是一個或多個方法的集合。任何類型均可以實現接口,而且是隱式實現,能夠同時實現多個接口。接口內部只有方法聲明沒有實現。接口內部存儲的其實就是接口值的類型和值,一部分存儲類型等各類信息,另外一部分存儲指向值的指針。若是是將值傳給接口,那麼這裏第二個字段存儲的就是原值的副本的指針。接口能夠調用實現了接口的方法。

五、方法集

5.1 方法集定義

方法集:方法集定義了一組關聯到給定類型的值或者指針的方法。定義方法時使用的接受者的類型決定了這個方法是關聯到值,仍是關聯到指針,仍是兩個都關聯。

// 這個示例程序展現 Go 語言裏如何使用接口
 package main
 import (
     "fmt"
 )

 // notifier 是一個定義了
 // 通知類行爲的接口
 type notifier interface {
     notify()
 }

 // user 在程序裏定義一個用戶類型
 type user struct {
     name string
     email string
 }

 // notify 是使用指針接收者實現的方法
 func (u *user) notify() {
     fmt.Printf("Sending user email to %s<%s>\n",
     u.name,
     u.email)
 }

 // main 是應用程序的入口
 func main() {
 // 建立一個 user 類型的值,併發送通知30 
    u := user{"Bill", "bill@email.com"}
     sendNotification(u)
 // ./listing36.go:32: 不能將 u(類型是 user)做爲
 // sendNotification 的參數類型 notifier:
 // user 類型並無實現 notifier
 // (notify 方法使用指針接收者聲明)
 }

 // sendNotification 接受一個實現了 notifier 接口的值
 // 併發送通知
 func sendNotification(n notifier) {
     n.notify()
 }

如上面代碼,當爲struct實現接口的方法notify()方法時,定義的接受者receiver是一個指針類型,因此,它要遵循方法集的規則,若是方法集的receiver 是*T 即指針類型,那麼屬於接口的值必須一樣是*T 指針類型。

user 實現了notify 方法,也就是它實現了notifier 接口,當時若是將user 實例傳給notifier實例,必須是一個指針類型,由於它實現的方法的receiver是一個指針類型。因此方法集的做用也就是規範接口的實現。

5.2 方法集規則

Values                    Methods Receivers
-----------------------------------------------
T                         (t T)
*T                         (t T) and (t *T)



Methods Receivers          Values
-----------------------------------------------
(t T)                     T and *T
(t *T)                     *T

若是方法的接受者是 指針類型 ,那麼用指針接受者方式實現這個接口,只有指向那個類型的指針纔可以算實現對應的接口,因此接口值接收的只能也是一個指針類型。

若是方法的接受者是 值類型,那麼用值接收者實現接口,那個類型的值和指針都可以實現對應的接口。

簡單講就是,接受者是(t T),那麼T 和 *T 均可以實現接口,若是接受者是(t *T)那麼只有 *T纔算實現接口。

反過來看稍微複雜點,判斷這個類型變量是否實現了接口,看一下他是值類型仍是指針類型,若是是T 值類型,那就看它實現接口方法的receiver是什麼類型,若是也是值類型,那麼它就實現了接口,若是不是,就沒有實現,就不能進行傳遞。若是他是指針類型,那麼無論它的receiver是值仍是指針都實現了接口。因此記住上面的圖就好。

緣由:編譯器並非總能自動得到一個值的地址 。

六、嵌入類型時接口實現

重溫一下什麼是嵌入類型,go語言爲了實現相似繼承的代碼複用,經過組合的方式來提升面向對象的能力。經過嵌入類型來實現代碼複用和擴展類型字段和方法。

嵌入類型:是將已有的類型直接聲明在新的結構類型裏。被嵌入的類型被稱爲新的外部類型的內部類型

實現方法重寫:外部類型也能夠經過聲明與內部類型標識符同名的標識符來覆蓋內部標識符的字段或者方法。

  • 注意聲明字段和嵌入類型在語法上的不一樣 ,嵌入類型直接是寫個類型名就行
  • 內部類型的標識符提高到了外部類型,能夠直接經過外部類型的值來訪問內部類型的標識符。 也能夠經過內部類型的名間接訪問內部類型方法和標識符。
  • 內部類型實現接口外部類型默認也實現了該接口。注意方法集的規則。
  • 若是內部類型和外部類型同時實現一個接口,就近原則,外部類型不會直接調用內部類型實現的同名方法,而是本身的。固然能夠經過內部類型間接顯示的去調用內部類型的方法。

6.1嵌入類型的實現

// 這個示例程序展現如何將一個類型嵌入另外一個類型,以及
// 內部類型和外部類型之間的關係
package main

import (
    "fmt"
)

// user 在程序裏定義一個用戶類型
type user struct {
    name  string
    email string
}

// notify 實現了一個能夠經過 user 類型值的指針
// 調用的方法
func (u *user) notify() {
    fmt.Printf("Sending user email to %s<%s>\n",
        u.name,
        u.email)
}

// admin 表明一個擁有權限的管理員用戶
type admin struct {
    user  // 嵌入類型
    level string
}

// main 是應用程序的入口
func main() {
    // 建立一個 admin 用戶
    ad := admin{
        user: user{
            name:  "john smith",
            email: "john@yahoo.com",
        },
        level: "super",
    }
    // 咱們能夠直接訪問內部類型的方法
    ad.user.notify()
    // 內部類型的方法也被提高到外部類型
    ad.notify()
}

總之:嵌入類型,就是外部類型擁有內部類型全部的字段和方法,就比如直接定義在外部類型同樣。就像繼承。

在來看下,內部類型實現接口是什麼狀況?

6.2 嵌入類型實現接口,一樣應用到外部類型

// 這個示例程序展現如何將一個類型嵌入另外一個類型,以及
// 內部類型和外部類型之間的關係
package main
import (
    "fmt"
)
// notifier 是一個定義了
// 通知類行爲的接口
type notifier interface {
    notify()
}

// user 在程序裏定義一個用戶類型
type user struct {
    name  string
    email string
}
// 經過 user 類型值的指針
// 調用的方法
func (u *user) notify() {
    fmt.Printf("Sending user email to %s<%s>\n",
        u.name,
        u.email)
}

// admin 表明一個擁有權限的管理員用戶
type admin struct {
    user  // 嵌入類型
    level string
}
// main 是應用程序的入口
func main() {
    // 建立一個 admin 用戶
    ad := admin{
        user: user{
            name:  "john smith",
            email: "john@yahoo.com",
        },
        level: "super",
    }
    // 給 admin 用戶發送一個通知
    // 用於實現接口的內部類型的方法,被提高到
    // 外部類型
    sendNotification(&ad)
}
// sendNotification 接受一個實現了 notifier 接口的值
// 併發送通知
func sendNotification(n notifier) {
    n.notify()
}

因爲內部類型的提高,內部類型實現的接口會自動提高到外部類型,即外部類型一樣也實現了該接口。不過要注意的是方法集的規則。總結:內部類型實現接口外部類型默認也實現了該接口。

6.3 內部類型和外部類型同時實現接口

// 這個示例程序展現如何將一個類型嵌入另外一個類型,以及
// 內部類型和外部類型之間的關係
package main

import (
    "fmt"
)

// notifier 是一個定義了
// 通知類行爲的接口
type notifier interface {
    notify()
}

// user 在程序裏定義一個用戶類型
type user struct {
    name  string
    email string
}

// 經過 user 類型值的指針
// 調用的方法
func (u *user) notify() {
    fmt.Printf("Sending user email to %s<%s>\n",
        u.name,
        u.email)
}

// admin 表明一個擁有權限的管理員用戶
type admin struct {
    user  // 嵌入類型
    level string
}

// 經過 admin 類型值的指針
// 調用的方法
func (a *admin) notify() {
    fmt.Printf("Sending admin email to %s<%s>\n",
        a.name,
        a.email)
}

// main 是應用程序的入口
func main() {
    // 建立一個 admin 用戶
    ad := admin{
        user: user{
            name:  "john smith",
            email: "john@yahoo.com",
        },
        level: "super",
    }

    // 給 admin 用戶發送一個通知,就近原則
    sendNotification(&ad)
    // 咱們能夠直接訪問內部類型的方法
    ad.user.notify()

    // 內部類型的方法沒有被提高
    ad.notify()
}

// sendNotification 接受一個實現了 notifier 接口的值
// 併發送通知
func sendNotification(n notifier) {
    n.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>

外部類型和內部類型同時實現接口,就近原則,外部類型優先調用本身實現的方法。若是要調用內部類型的方法,須要用內部類型字段間接調用。相似於方法重寫的方式,實現和內部類型同名的方法,也是就近原則。

相關文章
相關標籤/搜索