安裝本網站至主畫面:

[Go][Golang] 程式設計教學:介面 (Interface)

PUBLISHED ON OCT 8, 2017 — PROGRAMMING
Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

    在前一章中,我們介紹 Go 的物件系統。然而,我們在前文的最後面提到,Go 缺乏繼承的機制,我們無法透過繼承來達到多型的效果。為了處理這個議題,Go 引入介面的機制,也就是本文的主題。

    介面

    介面 (interface) 是只有方法宣告,但缺乏方法實作的型別。以 Point 類別為例,其介面如下:

    type IPoint interface {
        X() float64
        Y() float64
        SetX(float64)
        SetY(float64)
    }
    

    在這個介面中,我們宣告了四個方法 (method)。使用 IPoint 的開頭 I 只是為了要和原來的 Point 型別區別,不是強制性規定。宣告介面後,要再自行實作類別以滿足此介面。我們來看如何用介面解決我們於前文中所碰到的議題:

    package main
     
    import (
        "fmt"
    )
     
    type IPoint interface {
        X() float64
        Y() float64
        SetX(float64)
        SetY(float64)
    }
     
    type Point struct {
        x float64
        y float64
    }
     
    func NewPoint(x float64, y float64) *Point {
        p := new(Point)
     
        p.SetX(x)
        p.SetY(y)
     
        return p
    }
     
    func (p *Point) X() float64 {
        return p.x
    }
     
    func (p *Point) Y() float64 {
        return p.y
    }
     
    func (p *Point) SetX(x float64) {
        p.x = x
    }
     
    func (p *Point) SetY(y float64) {
        p.y = y
    }
     
    type Point3D struct {
        // Point is embedded
        Point
        z float64
    }
     
    func NewPoint3D(x float64, y float64, z float64) *Point3D {
        p := new(Point3D)
     
        p.SetX(x)
        p.SetY(y)
        p.SetZ(z)
     
        return p
    }
     
    func (p *Point3D) Z() float64 {
        return p.z
    }
     
    func (p *Point3D) SetZ(z float64) {
        p.z = z
    }
     
    func main() {
        // Make a slice of IPoint
        points := make([]IPoint, 0)
     
        p1 := NewPoint(3, 4)
        p2 := NewPoint3D(1, 2, 3)
        // No error!
        points = append(points, p1, p2)
     
        for _, p := range points {
            fmt.Println(fmt.Sprintf("(%.2f %.2f)", p.GetX(), p.GetY()))
        }
    }
    

    在本例中,我們建立一個以 IPoint 為型別的切片 points,並分別加入兩個相異的變數 p1p2,由於這兩個變數都符合我們宣告的介面,不會引發程式的錯誤。最後,我們分別呼叫兩個變數的 XY 方法,印出點所在的位置。

    嵌入介面

    如同類別,我們也可以在介面中嵌入另一個介面,如下例:

    type IPoint interface {
        X() float64
        Y() float64
        SetX(float64)
        SetY(float64)
    }
     
    type IPoint3D interface {
        IPoint
        Z() float64
        SetZ(float64)
    }
    

    在本例中,IPoint3D 除了繼承 IPoint 所有的方法宣告外,另外加入了自己特有的 ZSetZ 方法。

    介面的應用實例

    初學物件導向程式的程式設計者,往往無法馬上體會介面的用處。實際上,介面相當重要;對於 Go 或其他靜態型別語言來說,一方面要滿足型別檢查,一方面希望程式更加靈活;透過介面,可使得靜態語言程式某種程度上像動態語言程式般,像是要在同一個陣列中加入不同型別的物件時,就可以透過介面來處理。因為介面是在沒有繼承卻能實作子類型 (subtyping) 的手法。如果讀者去讀一些使用 Java 或 C# 等語言撰寫物件導向程式和設計模式的書,會發現這些書大量地使用介面來撰寫更易於維護的程式碼,反而較少使用繼承。

    在這裡我們舉一個設計模式 (design pattern) 的例子,設計模式是一套讓物件導向程式更易於維護的方法。在本例中,我們實作 Builder 模式,此模式的用意在於建立一群相關的物件,將建立的過程集中在同一個 Builder 函式中,使程式碼易於維護。如下例:

    package main
     
    import (
        "fmt"
    )
     
    type IAnimal interface {
        Speak()
    }
     
    type AnimalType int
     
    const (
        Duck AnimalType = iota
        Dog
        Tiger
    )
     
    type DuckClass struct{}
     
    func NewDuck() *DuckClass {
        return new(DuckClass)
    }
     
    func (d *DuckClass) Speak() {
        fmt.Println("Pack pack")
    }
     
    type DogClass struct{}
     
    func NewDog() *DogClass {
        return new(DogClass)
    }
     
    func (d *DogClass) Speak() {
        fmt.Println("Wow wow")
    }
     
    type TigerClass struct{}
     
    func NewTiger() *TigerClass {
        return new(TigerClass)
    }
     
    func (t *TigerClass) Speak() {
        fmt.Println("Halum halum")
    }
     
    func New(t AnimalType) IAnimal {
        switch t {
        case Duck:
            return NewDuck()
        case Dog:
            return NewDog()
        case Tiger:
            return NewTiger()
        default:
            panic("Unknown animal type")
        }
    }
     
    func main() {
        animals := make([]IAnimal, 0)
     
        duck := New(Duck)
        dog := New(Dog)
        tiger := New(Tiger)
     
        animals = append(animals, duck, dog, tiger)
     
        for _, a := range animals {
            a.Speak()
        }
    }
    

    運算子重載

    運算子重載是指物件可以用內建運算子來操作,如同使用內建型別般。Go 的設計是避免過多的語法魔術,也不支援運算子重載。少數的例外是 String 函式,滿足 String 函式即可用 fmt 套件內的函式將物件印出。以下為實例:

    package main
     
    import (
        "fmt"
        "strconv"
    )
     
    type Vector []float64
     
    func NewVector(args ...float64) Vector {
        return args
    }
     
    func (v Vector) String() string {
        out := "("
     
        for i, e := range v {
            out += strconv.FormatFloat(e, 'f', -2, 64)
     
            if i < len(v)-1 {
                out += ", "
            }
        }
     
        out += ")"
     
        return out
    }
     
    func main() {
        v := NewVector(1, 2, 3, 4, 5)
     
        fmt.Println(v)
    }
    

    空介面

    除了先前所介紹的介面外,還有另外一種沒有任何方法宣告的空介面,寫做 interface{},空介面也是一種特殊型別,該型別可放入任何值;基本上,空介面和介面使用的時機不同,要將其視為兩種不同的概念。以下是一個合法的 Go 程式:

    package main
     
    import (
        "fmt"
    )
     
    func main() {
        var v interface{}
     
        // v is an integer.
        v = 1
        fmt.Println(v)
     
        // v becomes a string now.
        v = "Hello"
        fmt.Println(v)
     
        // v becomes a boolean value now.
        v = true
        fmt.Println(v)
    }
    

    然而,空介面並不代表我們可以不理會原本的型別系統,例如,以下程式會引發錯誤:

    package main
     
    import (
        "fmt"
    )
     
    func main() {
        var m interface{}
        var n interface{}
     
        m = 3
        n = 2
     
        // Error!
        fmt.Println(m + n)
    }
    

    我們必需要透過型別判定 (type assertion),告訴 Go 程式該變數實際的型別後,才能使用該變數的值:

    package main
     
    import (
        "fmt"
    )
     
    func main() {
        var im interface{}
        var in interface{}
     
        im = 3
        in = 2
     
        // type assertion
        m := im.(int)
        n := in.(int)
     
        fmt.Println(m + n)
    }
    

    在進行型別判定時,若不確定型別是否正確,則要進行額外的檢查。以下為實例:

    package main
     
    import (
        "fmt"
        "log"
    )
     
    func main() {
        var im interface{}
        var in interface{}
     
        im = 3
        in = 2
     
        // type assertion
        m, ok := im.(int)
        if !ok {
            log.Fatal("Wrong type")
        }
     
        n, ok := in.(int)
        if !ok {
            log.Fatal("Wrong type")
        }
     
        fmt.Println(m + n)
    }
    

    如果無法預先知道變數的型別,可用 switch 敘述進行動態判定。實例如下:

    package main
     
    import (
        "fmt"
    )
     
    func main() {
        var t interface{}
        t = 3
        switch t := t.(type) {
        default:
            fmt.Printf("unexpected type %T\n", t) // %T prints whatever type t has
        case bool:
            fmt.Printf("boolean %t\n", t) // t has type bool
        case int:
            fmt.Printf("integer %d\n", t) // t has type int
        case *bool:
            fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool
        case *int:
            fmt.Printf("pointer to integer %d\n", *t) // t has type *int
        }
    }
    

    其實,Go 標準函式庫中也有使用空介面,例如,在 fmt 套件的 PrintfSprintf 函式中,以下是實例:

    package main
     
    import (
        "fmt"
    )
     
    func main() {
        fmt.Println(fmt.Sprintf("%d %s %t", 1, "Hello", true))
    }
    

    空介面的設計,某種程度上可使 Go 程式更靈活;然而,空介面會略去 Go 的型別檢查,使程式喪失靜態型別所帶來的益處。筆者的建議是,謹慎地使用空介面,只有在自己明確知道為什麼要用空介面時才使用。

    你或許對以下產品有興趣