iT邦幫忙

2023 iThome 鐵人賽

DAY 17
0
自我挑戰組

Go in 3o系列 第 17

[Day17] Go in 30 - 介面(interface)

  • 分享至 

  • xImage
  •  

一、本篇提要

在Go語言中,介面的實作是隱性的(implicit),不會像其他語言要求你明確實作介面,在介面這個主題下會開始介紹,如何宣告介面、實作介面、何謂 duck typing 以及 polymorphism,以及如何在函式中接收介面、並抽取出它底下的結構。

  • Go 語言與物件導向
  • Go 語言中的介面
  • 介面實作
  • 隱性介面實作優點
  • 值接收器與指標接收器在介面實作的差異

二、Go vs. 物件導向簡述

之前提到,Go 語言不是傳統的物件導向語言,但它仍然具有一些物件導向的特性和思維,關於 Go 語言和物件導向的一些觀點:

  1. 不完全的物件導向:Go 確實支援一些物件導向的基礎概念,例如結構體(Structs)可以被視為「物件」,而結構體的方法(Methods)相當於物件的行為。 但 Go 不支援傳統的類別與繼承。

  2. 介面(Interfaces):Go 使用介面為主要的多態性手段。 在 Go 中,interface是隱式實現的,這意味著你不需要明確地指出一個結構體實現了哪個interface,只要它的方法符合該 interface 的定義即可。

  3. 不支援繼承:Go 不支援傳統意義上的繼承,但允許組合(Composition)。 你可以嵌套或組合其他的結構體來擴展其功能。

  4. 不支援類別:Go 不具備類別(Class)這個概念。 相反,它有結構體和介面作為主要的數據和行為模塊。

  5. 提倡組合而非繼承:Go 的設計哲學是傾向於使用組合而非繼承,這使得代碼更加模塊化,並容易進行組件間的互動。

所以,Go 不是傳統意義上的物件導向語言,但它提供了一種獨特的方式來模擬某些物件導向的行為。 它選擇了簡單性和效率,而放棄了一些傳統物件導向語言的特性。

三、Go 介面簡介

Go 語言中,介面被定義為一組方法的集合,簡單說就是用來定義物件(某類事物)具備那些行為。
而這些方法的具體實現由擁有的型別來提供。

更精確地說 :

介面型別會包含一系列函式與方法特徵(method signatures),而其他型別只要定義相同方法,就等於**實作(implement)**此介面,並可以當成該介面型別來看待。

type 型別名稱 interface {
    
    <方法1特徵>
    <方法2特徵>
    ...
}

下面一個例子,示範一個介面應實作的方法,這些方法只有特徵(名稱、參數、傳回值)而不帶實作。


type Speaker interface {

    Speaker(message string) string //方法特徵
    Greet() string //方法特徵

}

定義介面步驟 :

  1. 首先是關鍵字type,緊接著是介面名稱(動詞),接著是另一個關鍵字interface
  2. 慣例上介面名稱會拿其中一個方法的名稱加上er結尾,特別是介面中只有一個方法的時候。
  3. 在介面的{}中定義方法特徵

介面中的方法特徵不限個數,但如果要成為此介面型別,就必須實作該介面所有方法。

四、介面實作

以小弟我Java仔來說,Java 介面實作通常要以明確的方式來實作介面,像是這樣 :

class whale implements Animal 

Java 中,這樣的語法確實表示 "whale" 這個類別實現了 "Animal" 這個介面。

但在 Go 語言中,沒有 "class" 的概念,也不需要明確地使用 "implements" 關鍵字來說明某個型別實現了某個介面。這就是前面所說,Go 的介面實作式隱性的,只要該型別的方法滿足了介面的定義,它就自動地實現了該介面。

以下是兩個 Go 的例子:

例子一 :
在這個例子中,Whale 型別實現了 Animal 介面,因為它有一個 Speak 方法,與 Animal 介面的定義相匹配。我們沒有明確地說 Whale "implements" Animal,但由於它的方法滿足了介面的定義,所以它自動地實現了該介面。

package main

import "fmt"

// Animal 介面
type Animal interface {
    Speak() string
}

// Whale 型別
type Whale struct{}

// Whale 的 Speak 方法,使其實現 Animal 介面
func (w Whale) Speak() string {
    return "Whoooa!"
}

func main() {
    var a Animal
    a = Whale{}
    fmt.Println(a.Speak())
}

如果在介面再多一個方法,但故意不實作,會變這樣 :

https://ithelp.ithome.com.tw/upload/images/20231002/20162693rlqw1UD7ci.png

cannot use Whale{} (value of type Whale) as Animal value in assignment: Whale does not implement Animal (missing method move)compilerInvalidIfaceAssign

這說明你正在嘗試將 Whale{}(一個 Whale 型別的值)分配給一個 Animal 型別變數 a,但 Whale 型別沒有實現 Animal 介面中所有方法,所以它並非Animal型別。

例子二 :
我們定義了一個介面 Greeter,它需要一個方法 Greet()。我們還定義了兩個型別 English 和 Spanish,它們都實現了 Greet() 方法。這意味著 English 和 Spanish 都被認為實現了 Greeter 介面,即使我們沒有明確地指明這一點。
SayHello 函式接受一個 Greeter 介面型別的參數,所以我們可以將任何實現了 Greeter 介面的值傳遞給它。

package main

import "fmt"

// Greeter 介面定義了一個方法:Greet()
type Greeter interface {
    Greet() string
}

// English 實現了 Greeter 介面
type English struct{}

func (e English) Greet() string {
    return "Hello"
}

// Spanish 也實現了 Greeter 介面
type Spanish struct{}

func (s Spanish) Greet() string {
    return "Hola"
}

func SayHello(g Greeter) {
    fmt.Println(g.Greet())
}

func main() {
    e := English{}
    s := Spanish{}
    
    SayHello(e)
    SayHello(s)
}

五、隱性介面實作優點

在Go中,只要型別的行為滿足了介面,就自動實作了該介面,而如果你後來修改了介面的方法集合,那麼不符合資格的實作就會自動失效(但這並不影響該型別的其他方面)。,如果是Java,假設你新增了介面中的方法,那麼只要有實作這個介面的類別就必須要實作這個新方法。

另一個優點是,可以讓型別實作其他套件內定義的介面,這麼一來就能將介面與其實作型別定義分開(去耦合decouple),(會在後續篇幅解釋 : 有關如何用自訂套件將程式功能分類)

fmt 套件中 Stringer 是一個例子。

package main

import "fmt"

func main() {
    
    type Stringer interface {
        String() string
    }
}

範例:

package main

import "fmt"


type Speaker interface {
    Speak() string
}

type cat struct {
    name string
    age int
}

func (c cat) Speak() string { //cat 實作了 Speaker 介面
    return "Purrrrr Meow"
}

func (c cat) String() string { //cat 實作了 String 介面
    return fmt.Sprintf("%v (%v years old)", c.name, c.age)
}

func main() {
    c := cat{name: "Oreo", age: 9}
    fmt.Println(c.Speak())
    fmt.Println(c)
}

結果 :
https://ithelp.ithome.com.tw/upload/images/20231002/201626932uCkTluaAC.png

可以從上述例子看到,cat 實作了兩個介面,一個是在自訂的 Speaker 以及來自 fmt 的 Stringer。
可以發現使用 fmt.Println() 來列印 cat 結構變數 c 時,Println() 自動呼叫了它 String() 的方法。這代表 c 符合並實作Stringer介面,使得它能夠被 Println() 接受和表現出特定的行為。

在 Go 中,當你使用 fmt.Println() 函式(或其他 fmt 套件中的格式化輸出函式)來列印一個物件時,該函式會檢查物件是否實作了 Stringer 介面。

如果一個型別實作了 String() 方法,那麼這個型別就被認為實作了 Stringer 介面。當你嘗試使用 fmt.Println() 或相關函式列印該型別的值時,會自動呼叫這個 String() 方法來取得該物件的字串表示形式。

在例子中,cat 型別實作了 String() 方法,所以當你使用 fmt.Println(c) 列印 cat 型別的 c 變數時,會自動呼叫 c.String() 來取得其字串表示形式。

這是 Go 的 fmt 套件的一個功能,它允許物件自定義如何被列印。

六、值接收器、指標接收器與介面

簡單複習下值接收器和指標接收器:
當定義結構的方法時,我們可以選擇使用

  1. 值接收器(如 func (c cat) Speak() string)
  2. 指標接收器(如 func (c *cat) Speak() string)。

拿上一頁 cat 範例來改 :

package main

import "fmt"


type Speaker interface {
    Speak() string
}

type cat struct {
    name string
    age int
}

func (c cat) Speak() string { //cat 實作了 Speaker 介面
    return "Purrrrr Meow"
}

func (c cat) String() string { //cat 實作了 String 介面
    return fmt.Sprintf("%v (%v years old)", c.name, c.age)
}

func main() {
    c := cat{name: "Oreo", age: 9}
    fmt.Println(c.Speak())
    fmt.Println(c)
}

我們將改寫這個例子 :

改為指標接收器的形式來指向 cat 結構變數。


func (c *cat) Speak() string{
    return "Purrrrr Meow"
}

func (c *cat) String() string { //cat 實作了 String 介面
    return fmt.Sprintf("%v (%v years old)", c.name, c.age)
}

結果 :

https://ithelp.ithome.com.tw/upload/images/20231002/20162693CtuVwF4BhK.png

第一行 fmt.Println(c.Speak()) 輸出還是一樣的,因為它是 cat 結構的方法,可是
第二行 fmt.Println() 就沒有呼叫 cat.String() 了,只單純印出結構內容。

這是因為加上指標接收器後,就變成指標型別 *cat 而不是 cat 實作了 Stringer 介面,使得在此建立的 cat 變數,就不再被認為符合 Stringer 介面了 !

解決方式 :

c := &cat{name:"Oreo", age: 9}

這樣,c 就是一個指向 cat 的指標,所以可以正確地呼叫 String() 方法,並符合 fmt.Stringer 介面。

方法使用指標接收器時,只有結構的指標可以調用該方法。方法如果使用值接收器,該型別不論是值或是指標,都可以正確實作介面。
簡單來說,當使用指標接收器定義方法時,只有該結構的指標會被認為實作了相對應的介面;當使用值接收器時,該結構的值和指標都會被認為實作了介面。

那麼以上就是本篇Go語言中介面介紹與整理~


上一篇
[Day16] Go in 30 - 錯誤處理 - recover
下一篇
[Day18] Go in 30 - 介面 - Duck Typing 與 Polymorphism
系列文
Go in 3o30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言