理解Go Interface

1 概述

Go語言中的接口很特別,並且提供了難以置信的一系列靈活性和抽象性。接口是一個自定義類型,它是一組方法的集合,要有方法爲接口類型就被認爲是該接口。從定義上來看,接口有兩個特色:html

  • 接口本質是一種自定義類型,所以不要將Go語言中的接口簡單理解爲C++/Java中的接口,後者僅用於聲明方法簽名。
  • 接口是一種特殊的自定義類型,其中沒有數據成員,只有方法(也能夠爲空)。

接口是徹底抽象的,所以不能將其實例化。然而,能夠建立一個其類型爲接口的變量,它能夠被賦值爲任何知足該接口類型的實際類型的值。接口的重要特性是:golang

  1. 只要某個類型實現了接口全部的方法,那麼咱們就說該類型實現了此接口。該類型的值能夠賦給該接口的值。
  2. 做爲1的推論,任何類型的值均可以賦值給空接口interface{}。

接口的特性是Go語言支持鴨子類型的基礎,即「若是它走起來像鴨子,叫起來像鴨子(實現了接口要的方法),它就是一隻鴨子(能夠被賦值給接口的值)」。憑藉接口機制和鴨子類型,Go語言提供了一種有利於類、繼承、模板以外的更加靈活強大的選擇。只要類型T的公開方法徹底知足接口I的要求,就能夠把類型T的對象用在須要接口I的地方。這種作法的學名叫作」Structural Typing「。c#

2 方法

Go語言中同時有函數和方法。一個方法就是一個包含了接受者的函數,接受者能夠是命名類型或者結構體類型的一個值或者是一個指針。全部給定類型的方法屬於該類型的方法集。函數

type User struct {
  Name  string
  Email string
}

func (u User) Notify() error

// User 類型的值能夠調用接受者是值的方法
damon := User{"AriesDevil", "ariesdevil@xxoo.com"}
damon.Notify()

// User 類型的指針一樣能夠調用接受者是值的方法
alimon := &User{"A-limon", "alimon@ooxx.com"}
alimon.Notify()

User的結構體類型,定義了一個該類型的方法叫作Notify,該方法的接受者是一個User類型的值。要調用Notify方法咱們須要一個 User類型的值或者指針。Go調用和解引用指針使得調用能夠被執行。注意,當接受者不是一個指針時,該方法操做對應接受者的值的副本(意思就是即便你使用了指針調用函數,可是函數的接受者是值類型,因此函數內部操做仍是對副本的操做,而不是指針操做。post

咱們能夠修改Notify方法,讓它的接受者使用指針類型:學習

func (u *User) Notify() error

再來一次以前的調用(注意:當接受者是指針時,即便用值類型調用那麼函數內部也是對指針的操做。測試

總結:ui

  • 一個結構體的方法的接收者多是類型值或指針
  • 若是接收者是值,不管調用者是類型值仍是類型指針,修改都是值的副本
  • 若是接收者是指針,則調用者修改的是指針指向的值自己。

3 接口實現

type Notifier interface {
  Notify() error
}

func SendNotification(notify Notifier) error {
  return notify.Notify()
}

unc (u *User) Notify() error {
  log.Printf("User: Sending User Email To %s<%s>\n",
      u.Name,
      u.Email)
  return nil
}

func main() {
  user := User{
    Name:  "AriesDevil",
    Email: "ariesdevil@xxoo.com",
  }
  
  SendNotification(user)
}

// Output:
cannot use user (type User) as type Notifier in function argument:
User does not implement Notifier (Notify method has pointer receiver)

上述代碼是編譯不過的,見Output,編譯錯誤關鍵信息Notify method has pointer receiver。 編譯器不考慮咱們的是實現該接口的類型,接口的調用規則是創建在這些方法的接受者和接口如何被調用的基礎上。下面的是語言規範裏定義的規則,這些規則用來講明是否咱們一個類型的值或者指針實現了該接口:url

  • 類型 *T 的可調用方法集包含接受者爲 *T 或 T 的全部方法集
  • 類型 T 的可調用方法集包含接受者爲 T 的全部方法
  • 類型 T 的可調用方法集包含接受者爲 *T 的方法

也就是說:spa

  • 接收者是指針 *T 時,接口的實例必須是指針
  • 接收者是值 T 時,接口的實例能夠是指針也能夠是

4 空接口與nil

空接口(interface{})不包含任何的method,正由於如此,全部的類型都實現了interface{}interface{}對於描述起不到任何的做用(由於它不包含任何的method),可是interface{}在咱們須要存儲任意類型的數值的時候至關有用,由於它能夠存儲任意類型的數值。它有點相似於C語言的void*類型。

Go語言中的nil在概念上和其它語言的null、None、nil、NULL同樣,都指代零值或空值。nil是預先說明的標識符,也即一般意義上的關鍵字。nil只能賦值給指針、channel、func、interface、map或slice類型的變量。若是未遵循這個規則,則會引起panic。

在底層,interface做爲兩個成員來實現,一個類型(type)和一個值(data)。參考官方文檔翻譯Go中error類型的nil值和nil

import (
    "fmt"
    "reflect"
)
 
func main() {
    var val interface{} = int64(58)
    fmt.Println(reflect.TypeOf(val))
    val = 50
    fmt.Println(reflect.TypeOf(val))
}

type用於存儲變量的動態類型,data用於存儲變量的具體數據。在上面的例子中,第一條打印語句輸出的是:int64。這是由於已經顯示的將類型爲int64的數據58賦值給了interface類型的變量val,因此val的底層結構應該是:(int64, 58)。咱們暫且用這種二元組的方式來描述,二元組的第一個成員爲type,第二個成員爲data。第二條打印語句輸出的是:int。這是由於字面量的整數在golang中默認的類型是int,因此這個時候val的底層結構就變成了:(int, 50)。

func main() {
    var val interface{} = nil
    if val == nil {
        fmt.Println("val is nil")
    } else {
        fmt.Println("val is not nil")
    }
}

變量val是interface類型,它的底層結構必然是(type, data)。因爲nil是untyped(無類型),而又將nil賦值給了變量val,因此val實際上存儲的是(nil, nil)。所以很容易就知道val和nil的相等比較是爲true的。

進一步驗證:

func main() {
    var val interface{} = (*interface{})(nil)
    if val == nil {
        fmt.Println("val is nil")
    } else {
        fmt.Println("val is not nil")
    }
}

(*interface{})(nil)是將nil轉成interface類型的指針,其實獲得的結果僅僅是空接口類型指針而且它指向無效的地址。也就是空接口類型指針而不是空指針,這二者的區別蠻大的。

對於(*int)(nil)(*byte)(nil)等等來講是同樣的。上面的代碼定義了接口指針類型變量val,它指向無效的地址(0x0),所以val持有無效的數據。但它是有類型的(*interface{})。因此val的底層結構應該是:(*interface{}, nil)

有時候您會看到(*interface{})(nil)的應用,好比var ptrIface = (*interface{})(nil),若是您接下來將ptrIface指向其它類型的指針,將通不過編譯。或者您這樣賦值:*ptrIface = 123,那樣的話編譯是經過了,但在運行時仍是會panic的,這是由於ptrIface指向的是無效的內存地址。其實聲明相似ptrIface這樣的變量,是由於使用者只是關心指針的類型,而忽略它存儲的值是什麼。

小結: 不管該指針的值是什麼:(*interface{}, nil),這樣的接口值老是非nil的,即便在該指針的內部爲nil。

5 接口變量存儲的類型

接口的變量裏面能夠存儲任意類型的數值(該類型實現了某interface)。那麼咱們怎麼反向知道這個變量裏面實際保存了的是哪一個類型的對象呢?目前經常使用的有兩種方法:

  • comma-ok斷言

    value, ok = element.(T),這裏value就是變量的值,ok是一個bool類型,element是interface變量,T是斷言的類型。若是element裏面確實存儲了T類型的數值,那麼ok返回true,不然返回false。

  • switch測試

    switch value := element.(type) {
        case int:
            fmt.Printf("list[%d] is an int and its value is %d\n", index, value)
        case string:
             fmt.Printf("list[%d] is a string and its value is %s\n", index, value)
        ...

    element.(type)語法不能在switch外的任何邏輯裏面使用,若是你要在switch外面判斷一個類型就使用comma-ok。

6 接口與反射

反射是程序運行時檢查其所擁有的結構,尤爲是類型的一種能力。Go語言也提供對反射的支持。

在前面的interface{}與nil的底層實現已提到,在reflect包中有兩個類型須要瞭解:TypeValue。這兩個類型使得能夠訪問接口變量的內容,還有兩個簡單的函數,reflect.TypeOfreflect.ValueOf,從接口值中分別獲取reflect.Type 和reflect.Value

如同物理中的反射,在Go語言中的反射也存在它本身的鏡像。從reflect.Value可使用Interface方法還原接口值:

var x float64 = 3.4
v := reflect.ValueOf(x)

// Interface 以 interface{} 返回 v 的值。
// func (v Value) Interface() interface{}

// y 將爲類型 float64
y := v.Interface().(float64) 
fmt.Println(y)

聲明:本文是收集網上一些關於Go語言中接口(interface)的說明,是一篇學習筆記,文中多處引用,參考文章列表在最後,可直接訪問了解詳情。

參考:
[1] Go 語言中的方法,接口和嵌入類型
[2] 詳解interface和nil
[3] Go語言interface詳解

相關文章
相關標籤/搜索