interface

interface

Go 語言裡面設計最精妙的應該算 interface,它讓物件導向,內容組織實現非常的方便,當你看完這一章,你就會被 interface 的巧妙設計所折服。

什麼是 interface

簡單的說,interface 是一組 method 簽名的組合,我們透過 interface 來定義物件的一組行為。

我們前面一章最後一個例子中 Student 和 Employee 都能 SayHi,雖然他們的內部實現不一樣,但是那不重要,重要的是他們都能say hi

讓我們來繼續做更多的擴充套件,Student 和 Employee 實現另一個方法Sing,然後 Student 實現方法 BorrowMoney 而 Employee 實現 SpendSalary。

這樣 Student 實現了三個方法:SayHi、Sing、BorrowMoney;而 Employee 實現了 SayHi、Sing、SpendSalary。

上面這些方法的組合稱為 interface(被物件 Student 和 Employee 實現)。例如 Student 和 Employee 都實現了 interface:SayHi 和 Sing,也就是這兩個物件是該 interface 型別。而 Employee 沒有實現這個 interface:SayHi、Sing 和 BorrowMoney,因為 Employee 沒有實現 BorrowMoney 這個方法。

interface 型別

interface 型別定義了一組方法,如果某個物件實現了某個介面的所有方法,則此物件就實現了此介面。詳細的語法參考下面這個例子

type Human struct {
    name string
    age int
    phone string
}

type Student struct {
    Human //匿名欄位 Human

    school string
    loan float32
}

type Employee struct {
    Human //匿名欄位 Human

    company string
    money float32
}

//Human 物件實現 Sayhi 方法
func (h *Human) SayHi() {
    fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}

// Human 物件實現 Sing 方法
func (h *Human) Sing(lyrics string) {
    fmt.Println("La la, la la la, la la la la la...", lyrics)
}

//Human 物件實現 Guzzle 方法
func (h *Human) Guzzle(beerStein string) {
    fmt.Println("Guzzle Guzzle Guzzle...", beerStein)
}

// Employee 過載 Human 的 Sayhi 方法
func (e *Employee) SayHi() {
    fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
        e.company, e.phone) //此句可以分成多行
}

//Student 實現 BorrowMoney 方法
func (s *Student) BorrowMoney(amount float32) {
    s.loan += amount // (again and again and...)
}

//Employee 實現 SpendSalary 方法
func (e *Employee) SpendSalary(amount float32) {
    e.money -= amount // More vodka please!!! Get me through the day!
}

// 定義 interface

type Men interface {
    SayHi()
    Sing(lyrics string)
    Guzzle(beerStein string)
}

type YoungChap interface {
    SayHi()
    Sing(song string)
    BorrowMoney(amount float32)
}

type ElderlyGent interface {
    SayHi()
    Sing(song string)
    SpendSalary(amount float32)
}

透過上面的程式碼我們可以知道,interface 可以被任意的物件實現。我們看到上面的 Men interface 被 Human、Student 和 Employee 實現。同理,一個物件可以實現任意多個 interface,例如上面的 Student 實現了 Men 和 YoungChap 兩個 interface。

最後,任意的型別都實現了空 interface(我們這樣定義:interface{}),也就是包含 0 個 method 的 interface。

interface 值

那麼 interface 裡面到底能存什麼值呢?如果我們定義了一個 interface 的變數,那麼這個變數裡面可以存實現這個 interface 的任意型別的物件。例如上面例子中,我們定義了一個 Men interface 型別的變數 m,那麼 m 裡面可以存 Human、Student 或者 Employee 值。

因為 m 能夠持有這三種類型的物件,所以我們可以定義一個包含 Men 型別元素的 slice,這個 slice 可以被賦予實現了 Men 介面的任意結構的物件,這個和我們傳統意義上面的 slice 有所不同。

讓我們來看一下下面這個例子:

package main

import "fmt"

type Human struct {
    name string
    age int
    phone string
}

type Student struct {
    Human //匿名欄位
    school string
    loan float32
}

type Employee struct {
    Human //匿名欄位
    company string
    money float32
}

//Human 實現 SayHi 方法
func (h Human) SayHi() {
    fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}

//Human 實現 Sing 方法
func (h Human) Sing(lyrics string) {
    fmt.Println("La la la la...", lyrics)
}

//Employee 過載 Human 的 SayHi 方法
func (e Employee) SayHi() {
    fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
        e.company, e.phone)
    }

// Interface Men 被 Human,Student 和 Employee 實現
// 因為這三個型別都實現了這兩個方法
type Men interface {
    SayHi()
    Sing(lyrics string)
}

func main() {
    mike := Student{Human{"Mike", 25, "222-222-XXX"}, "MIT", 0.00}
    paul := Student{Human{"Paul", 26, "111-222-XXX"}, "Harvard", 100}
    sam := Employee{Human{"Sam", 36, "444-222-XXX"}, "Golang Inc.", 1000}
    tom := Employee{Human{"Tom", 37, "222-444-XXX"}, "Things Ltd.", 5000}

    //定義 Men 型別的變數 i

    var i Men

    //i 能儲存 Student

    i = mike
    fmt.Println("This is Mike, a Student:")
    i.SayHi()
    i.Sing("November rain")

    //i 也能儲存 Employee

    i = tom
    fmt.Println("This is tom, an Employee:")
    i.SayHi()
    i.Sing("Born to be wild")

    //定義了 slice Men
    fmt.Println("Let's use a slice of Men and see what happens")
    x := make([]Men, 3)
    //這三個都是不同型別的元素,但是他們實現了 interface 同一個介面
    x[0], x[1], x[2] = paul, sam, mike

    for _, value := range x{
        value.SayHi()
    }
}

透過上面的程式碼,你會發現 interface 就是一組抽象方法的集合,它必須由其他非 interface 型別實現,而不能自我實現, Go 透過 interface 實現了 duck-typing:即"當看到一隻鳥走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,那麼這隻鳥就可以被稱為鴨子"。

空 interface

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

// 定義 a 為空介面
var a interface{}
var i int = 5
s := "Hello world"
// a 可以儲存任意型別的數值
a = i
a = s

一個函式把 interface{} 作為參數,那麼他可以接受任意型別的值作為參數,如果一個函式回傳 interface{},那麼也就可以回傳任意型別的值。是不是很有用啊!

interface 函式參數

interface 的變數可以持有任意實現該 interface 型別的物件,這給我們編寫函式(包括 method)提供了一些額外的思考,我們是不是可以透過定義 interface 參數,讓函式接受各種型別的參數。

舉個例子:fmt.Println 是我們常用的一個函式,但是你是否注意到它可以接受任意型別的資料。開啟 fmt 的原始碼檔案,你會看到這樣一個定義:

type Stringer interface {
     String() string
}

也就是說,任何實現了 String 方法的型別都能作為參數被 fmt.Println 呼叫,讓我們來試一試

package main
import (
    "fmt"
    "strconv"
)

type Human struct {
    name string
    age int
    phone string
}

// 透過這個方法 Human 實現了 fmt.Stringer
func (h Human) String() string {
    return "❰"+h.name+" - "+strconv.Itoa(h.age)+" years -  ✆ " +h.phone+"❱"
}

func main() {
    Bob := Human{"Bob", 39, "000-7777-XXX"}
    fmt.Println("This Human is : ", Bob)
}

現在我們再回顧一下前面的 Box 範例,你會發現 Color 結構也定義了一個 method:String。其實這也是實現了 fmt.Stringer 這個 interface,即如果需要某個型別能被 fmt 套件以特殊的格式輸出,你就必須實現 Stringer 這個介面。如果沒有實現這個介面,fmt 將以預設的方式輸出。

//實現同樣的功能
fmt.Println("The biggest one is", boxes.BiggestsColor().String())
fmt.Println("The biggest one is", boxes.BiggestsColor())

注:實現了 error 介面的物件(即實現了 Error() string 的物件),使用 fmt 輸出時,會呼叫 Error()方法,因此不必再定義 String()方法了。

interface 變數儲存的型別

我們知道 interface 的變數裡面可以儲存任意型別的數值(該型別實現了 interface)。那麼我們怎麼反向知道這個變數裡面實際儲存了的是哪個型別的物件呢?目前常用的有兩種方法:

  • Comma-ok 斷言

    Go 語言裡面有一個語法,可以直接判斷是否是該型別的變數: value, ok = element.(T),這裡 value 就是變數的值,ok 是一個 bool 型別,element 是 interface 變數,T 是斷言的型別。

    如果 element 裡面確實儲存了 T 型別的數值,那麼 ok 回傳 true,否則回傳 false。

    讓我們透過一個例子來更加深入的理解。

    package main

    import (
        "fmt"
        "strconv"
    )

    type Element interface{}
    type List [] Element

    type Person struct {
        name string
        age int
    }

    //定義了 String 方法,實現了 fmt.Stringer
    func (p Person) String() string {
        return "(name: " + p.name + " - age: "+strconv.Itoa(p.age)+ " years)"
    }

    func main() {
        list := make(List, 3)
        list[0] = 1 // an int
        list[1] = "Hello" // a string
        list[2] = Person{"Dennis", 70}

        for index, element := range list {
            if value, ok := element.(int); ok {
                fmt.Printf("list[%d] is an int and its value is %d\n", index, value)
            } else if value, ok := element.(string); ok {
                fmt.Printf("list[%d] is a string and its value is %s\n", index, value)
            } else if value, ok := element.(Person); ok {
                fmt.Printf("list[%d] is a Person and its value is %s\n", index, value)
            } else {
                fmt.Printf("list[%d] is of a different type\n", index)
            }
        }
    }
是不是很簡單啊,同時你是否注意到了多個 if 裡面,還記得我前面介紹流程時講過,if 裡面允許初始化變數。

也許你注意到了,我們斷言的型別越多,那麼 if else 也就越多,所以才引出了下面要介紹的 switch。
  • switch 測試

    最好的講解就是程式碼例子,現在讓我們重寫上面的這個實現

    package main

    import (
        "fmt"
        "strconv"
    )

    type Element interface{}
    type List [] Element

    type Person struct {
        name string
        age int
    }

    //列印
    func (p Person) String() string {
        return "(name: " + p.name + " - age: "+strconv.Itoa(p.age)+ " years)"
    }

    func main() {
        list := make(List, 3)
        list[0] = 1 //an int
        list[1] = "Hello" //a string
        list[2] = Person{"Dennis", 70}

        for index, element := range list{
            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)
                case Person:
                    fmt.Printf("list[%d] is a Person and its value is %s\n", index, value)
                default:
                    fmt.Println("list[%d] is of a different type", index)
            }
        }
    }
這裡有一點需要強調的是:`element.(type)`語法不能在 switch 外的任何邏輯裡面使用,如果你要在 switch 外面判斷一個型別就使用`comma-ok`。

嵌入 interface

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

我們可以看到原始碼套件 container/heap 裡面有這樣的一個定義

type Interface interface {
    sort.Interface //嵌入欄位 sort.Interface
    Push(x interface{}) //a Push method to push elements into the heap
    Pop() interface{} //a Pop elements that pops elements from the heap
}

我們看到 sort.Interface 其實就是嵌入欄位,把 sort.Interface 的所有 method 給隱式的包含進來了。也就是下面三個方法:

type Interface interface {
    // Len is the number of elements in the collection.
    Len() int
    // Less returns 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)
}

另一個例子就是 io 套件下面的 io.ReadWriter ,它包含了 io 套件下面的 Reader 和 Writer 兩個 interface:

// io.ReadWriter
type ReadWriter interface {
    Reader
    Writer
}

反射

Go 語言實現了反射,所謂反射就是能檢查程式在執行時的狀態。我們一般用到的套件是 reflect 套件。如何運用 reflect 套件,官方的這篇文章詳細的講解了 reflect 套件的實現原理,laws of reflection

使用 reflect 一般分成三步,下面簡要的講解一下:要去反射是一個型別的值(這些值都實現了空 interface),首先需要把它轉化成 reflect 物件(reflect.Type 或者 reflect.Value,根據不同的情況呼叫不同的函式)。這兩種取得方式如下:

t := reflect.TypeOf(i)    //得到型別的 Meta 資料,透過 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)

上面只是對反射的簡單介紹,更深入的理解還需要自己在程式設計中不斷的實踐。

Last updated