1. 程式人生 > >Golang 學習之路九:介面(Interface)

Golang 學習之路九:介面(Interface)

Golang學習之路:介面(interface)

一、前言

  Go 語言和傳統的OO語言概念思想上不同,它在語法上不支援類與整合的概念。但是為了實現類似於C++等語言中多型行為,Go語言引入了interface型別,實現了類似於多型的功能。注意interfacemethod關係密切,在學習interface之前需要將method的概念理解清楚。可以參考前面的博文Golang 學習之路七:面向物件-方法(Method),language specificationEffective Go

二、介面的定義

  介面是一個或多個方法簽名的集合,任何型別的方法集中只要擁有與之對應的全部方法,它就表示“實現”了該介面,無需在該型別上顯式新增介面宣告。而所謂的對應方法,是指有相同的名稱、引數列表(不包括引數名)以及返回值。當然,該型別還可以有其它方法。

  • 介面的命名通常使用er結尾,結構體。
  • 介面只有方法簽名,沒有實現。
  • 介面沒有資料欄位。
  • 可以在介面中嵌入其它介面。
  • 型別可以實現多個介面
package main

import "fmt"

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

輸出結果:

user 1, Tom
  • 空interface(interface{})不包含任何的method,正因為如此,所有的型別都實現了空interface。空interface對於描述起不到任何的作用(因為它不包含任何的method),但是空interface在我們需要儲存任意型別的數值的時候相當有用,因為它可以儲存任意型別的數值。它有點類似於C語言的void*型別。
package main

import "fmt"

func Print(v interface{}) {
    fmt.Printf("%T: %v\n", v, v)
}
func main() {
    Print(1)
    Print("Hello World")
}

輸出結果:

int: 1
string: Hello World
  • 匿名介面可用作變數型別,或結構成員。
package main

import "fmt"

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

三、介面的執行機制

  Go新版使用Go重寫,《Go學習筆記》使用了原來C/C++。下面貼出的是分別貼出兩個interface部分原始碼。

// @file /Go/src/runtime/runtime2.go (Go 1.8)
type iface struct {
    tab  *itab
    data unsafe.Pointer
}

type itab struct {
    inter  *interfacetype
    _type  *_type
    link   *itab
    bad    int32
    inhash int32      // has this itab been added to hash?
    fun    [1]uintptr // variable sized
}
// Go 舊版本原始碼:
struct Iface
{
    Itab* tab;
    void* data;
};
struct Itab
{
    InterfaceType* inter;
    Type* type;
    void (*fun[])(void);
};
  • 介面表儲存元資料資訊,包括介面型別、動態型別,以及實現介面的方法指標。

  • 資料指標持有的是目標物件的只讀複製品,複製完整物件或指標。

package main

import "fmt"

type User struct {
    id   int
    name string
}

func main() {
    u := User{1, "Tom"}
    var i interface{} = u
    u.id = 2
    u.name = "Jack"
    fmt.Printf("%v\n", u)
    fmt.Printf("%v\n", i.(User))
}

輸出結果:

{2 Jack}
{1 Tom}
  • 介面轉型返回臨時物件,只有使用指標才能修改其狀態。
package main

import "fmt"

type User struct {
    id   int
    name string
}

func main() {
    u := User{1, "Tom"}
    var i interface{} = &u
    u.id = 2
    u.name = "Jack"
    fmt.Printf("%v\n", u)
    fmt.Printf("%v\n", i.(*User))
}

輸出結果:

{2 Jack}
&{2 Jack}
  • 只有tabdata都為nil時,接口才等於nil
package main // tab = nil, data = nil
import (
    "fmt"
    "reflect"
    "unsafe"
)

var a interface{} = nil
var b interface{} = (*int)(nil) // tab 包含 *int 型別資訊, data = nil
type iface struct {
    itab, data uintptr
}

func main() {
    ia := *(*iface)(unsafe.Pointer(&a))
    ib := *(*iface)(unsafe.Pointer(&b))
    fmt.Println(a == nil, ia)
    fmt.Println(b == nil, ib, reflect.ValueOf(b).IsNil())
}

輸出結果:

true {0 0}
false {4701888 0} true

四、介面變數儲存的型別

  interface變數裡面可以儲存任意型別的數值(非介面),只要這個值實現了介面的方法。但是,如何知道變數裡面實際儲存的是哪種型別的物件?

1. Comma-ok斷言

  Go語言裡面有一個語法,可以直接判斷是否是該型別的變數: value, ok =element.(T),這裡value就是變數的值,ok是一個bool型別,element是interface變數,T是斷言的型別。
- 如果element裡面確實儲存了T型別的數值,那麼ok返回true,否則返回false。

package main

import "fmt"

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 value, ok := o.(fmt.Stringer); ok { // ok-idiom
        fmt.Println(value, ok)
    }
    u := o.(*User)
    // u := o.(User) // panic: interface is *main.User, not main.User
    fmt.Println(u)
}

輸出結果:

1, Tom true
1, Tom

2. switch測試(不支援fallthrough)

  • element.(type)語法不能在switch外的任何邏裡面使用,如果你要在switch外面判斷一個型別就使用comma-ok。
package main

import "fmt"

type User struct {
    id   int
    name string
}

func (self *User) String() string {
    return fmt.Sprintf("%d, %s", self.id, self.name)
}

func main() {
    var element interface{} = &User{1, "Tom"}
    switch v := element.(type) {
    case nil: // element == 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. 補充

  • 超集介面物件可轉換為子集介面,反之出錯。
package main

import "fmt"

type Stringer interface {
    String() string
}
type Printer interface {
    String() string
    Print()
}
type User struct {
    id   int
    name string
}

func (self *User) String() string {
    return fmt.Sprintf("%d, %v", self.id, self.name)
}
func (self *User) Print() {
    fmt.Println(self.String())
}
func main() {
    var o Printer = &User{1, "Tom"}
    var s Stringer = o
    fmt.Println(s.String())
}

輸出結果:

1, Tom

五、嵌入介面

  Go裡面真正吸引人的是他內建的邏輯語法,就像我們在學習Struct時學習的匿名欄位,非常的優雅,那麼相同的邏輯引入到interface裡面,那不是更加完美了。如果一個interface1作為interface2的一個嵌入欄位,那麼
interface2隱式的包含了interface1裡面的method。
- 檢視原始碼包中的一個例子

//@file /Go/src/container/heap/heap.go
type Interface interface {
    sort.Interface
    Push(x interface{}) // add x as element Len()
    Pop() interface{}   // remove and return element Len() - 1.
}
  • 找到sort.Interface的原始碼如下:
//@file /Go/src/sort/sort.go
type Interface interface {
    // Len is the number of elements in the collection.
    Len() int
    // Less reports whether the element with
    // index i should sort before the element with index j.
    Less(i, j int) bool
    // Swap swaps the elements with indexes i and j.
    Swap(i, j int)
}
  • heap.goInterface嵌入sort。Interface就把sort.Interface的所有method給隱式的包含進來了。

六、反射簡單介紹

  反射是在執行時反射是程式檢查其所擁有的結構,尤其是型別的一種能力;這是超程式設計的一種形式。它同時也是造成混淆的重要來源。我們一般需要使用reflect包。
使用reflect一般分成三步
- 要去反射是一個型別的值(這些值都實現了空interface),首先需要把它轉化成reflect物件(reflect.Type或者reflect.Value,根據不同的情況呼叫不同的函式)。這兩種獲取方式如下:

t := reflect.TypeOf(i) //得到型別的元資料,通過t我們能獲取型別定義裡面的所有元素。
v := reflect.ValueOf(i) //得到實際的值,通過v我們獲取儲存在裡面的值,還可以去改變值
  • 轉化為reflect物件之後我們就可以進行一些操作了,也就是將reflect物件轉化成相應的值,例如
tag := t.Elem().Field(0).Tag //獲取定義在struct裡面的標籤
name := v.Elem().Field(0).String() //獲取儲存在第一個欄位裡面的值

獲取反射值能返回相應的型別和數值

var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())
fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
fmt.Println("value:", v.Float())
  • 最後,反射的話,那麼反射的欄位必須是可修改的,我們前面學習過傳值和傳引用,這個裡面也是一樣的道理,反射的欄位必須是可讀寫的意思是,如果下面這樣寫,那麼會發生錯誤。
var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1)

如果要修改相應的值,需要進行下面的修改

var x float64 = 3.4
p := reflect.ValueOf(&x)
v := p.Elem()
v.SetFloat(7.1)

上面只是摘錄《Go Web程式設計》對反射的簡單介紹,更深入的理解可以檢視laws of reflection,可以檢視轉載的中文翻譯版本[翻譯]反射的規則原始翻譯版本

七、總結

  Go語言的interface是它比較具有特色的地方。Go語言的主要設計者之一羅布·派克( Rob Pike)曾經說過,如果只能選擇一個Go語言的特 性移植到其他語言中,他會選擇介面。它在Go開發中無處不在,從Go原始碼中,你可以看到這些。

八、參考資料