在 Go 語言中,我為什麼使用介面

polarisxu發表於2020-08-15

強調一下是我個人的見解以及介面在 Go 語言中的意義。

如果您寫程式碼已經有了一段時間,我可能不需要過多解釋介面所帶來的好處,但是在深入探討 Go 語言中的介面前,我想花一兩分鐘先來簡單介紹一下介面。 如果您對介面很熟悉,請先跳過下面這段。

介面的簡單介紹

在任一程式語言中,介面——方法或行為的集合,在功能和該功能的使用者之間構建了一層薄薄的抽象層。在使用介面時,並不需要了解底層函式是如何實現的,因為介面隔離了各個部分(劃重點)。

跟不使用介面相比,使用介面的最大好處就是可以使程式碼變得簡潔。例如,您可以建立多個元件,通過介面讓它們以統一的方式互動,儘管這些元件的底層實現差異很大。這樣就可以在編譯甚至執行的時候動態替換這些元件。

用 Go 的 io.Reader 介面舉個例子。io.Reader 介面的所有實現都有 Read(p []byte) (n int, err error) 函式。使用 io.Reader 介面的使用者不需要知道使用這個 Read 函式的時候那些位元組從何而來。

具體到 Go 語言

在我使用 Go 語言的過程中,與我使用過的其他任何程式語言相比,我經常發現其他的、不那麼明顯的使用介面的原因。今天,我將介紹一個很普遍的,也是我遇到了很多次的使用介面的原因。

Go 語言沒有建構函式

很多程式語言都有建構函式。建構函式是定義自定義型別(即 OO 語言中的類)時使用的一種建立物件的方法,它可以確保必須執行的任何初始化邏輯均已執行。

例如,假設所有 widgets 都必須有一個不變的,系統分配的識別符號。在 Java 中,這很容易實現:

package io.krancour.widget;

import java.util.UUID;

public class Widget {

    private String id;

    // 使用建構函式初始化
    public Widget() {
        id = UUID.randomUUID().toString();
    }

    public String getId() {
        return id;
    }
}
class App {
    public static void main( String[] args ){
        Widget w = new Widget();
        System.out.println(w.getId());
    }
}

從上面這個例子可以看到,沒有執行初始化邏輯就無法例項化一個新的 Widget

但是 Go 語言沒有此功能。 :frowning:

在 Go 語言中,可以直接例項化一個自定義型別。

定義一個 Widget 型別:

package widgets

type Widget struct {
    id string
}

func (w Widget) ID() string {
    return w.id
}

可以像這樣例項化和使用一個 widget

package main

import (
    "fmt"
    "github.com/krancour/widgets"
)

func main() {
    w := widgets.Widget{}
    fmt.Println(w.ID())
}

如果執行此示例,那麼(也許)意料之中的結果是,列印出的 ID 是空字串,因為它從未被初始化,而空字串是字串的“零值”。 我們可以在 widgets 包中新增一個類似於建構函式的函式來處理初始化:

package widgets

import uuid "github.com/satori/go.uuid"

type Widget struct {
    id string
}

func NewWidget() Widget {
    return Widget{
        id: uuid.NewV4().String(),
    }
}

func (w Widget) ID() string {
    return w.id
}

然後我們簡單地修改 main 來使用這個類似於建構函式的新函式:

package main

import (
    "fmt"
    "github.com/krancour/widgets"
)

func main() {
    w := widgets.NewWidget()
    fmt.Println(w.ID())
}

執行該程式,我們得到了想要的結果。

但是仍然存在一個嚴重問題!我們的 widgets 包沒有強制使用者在初始一個 widget 的時候使用我們的建構函式。

變數私有化

首先我們嘗試把自定義型別的變數私有化,以此來強制使用者使用我們規定的建構函式來初始化 widget。在 Go 語言中,型別名、函式名的首字母是否大寫決定它們是否可被其他包訪問。名稱首字母大寫的可被訪問(也就是 public ),而名稱首字母小寫的不可被訪問(也就是 private )。所以我們把型別 Widget 改為型別 widget

package widgets

import uuid "github.com/satori/go.uuid"

type widget struct {
    id string
}

func NewWidget() widget {
    return widget{
        id: uuid.NewV4().String(),
    }
}

func (w widget) ID() string {
    return w.id
}

我們的 main 程式碼保持不變,這次我們得到了一個 ID 。這比我們想要的要近了一步,但是我們在此過程中犯了一個不太明顯的錯誤。類似於建構函式的 NewWidget 函式返回了一個私有的例項。儘管編譯器對此不會報錯,但這是一種不好的做法,下面是原因解釋。

在 Go 語言中,包*是複用的基本單位。其他語言中的類*是複用的基本單位。如前所述,任何無法被外部訪問的內容實質上都是“包私有”,是該包的內部實現細節,對於使用這個包的使用者來說不重要。因此,Go 的文件生成工具 godoc 不會為私有的函式、型別等生成文件。

當一個公開的建構函式返回一個私有的 widget 例項,實際上就陷入了一條死衚衕。呼叫這個函式的人哪怕有這個例項,也絕對在文件裡找不到任何關於這個例項型別的描述,也更不知道 ID() 這個函式。Go 社群非常重視文件,所以這樣做是不會被接受的。

輪到介面上場了

回顧一下,到目前為止,我們寫了一個類似於建構函式的函式來解決 Go 語言缺乏建構函式的問題,但是為了確保人們用該函式而不是直接例項化 Widget ,我們更改了該型別的可見性——將其重新命名為 widget,即私有化了。雖然編譯器不會報錯,但是文件中不會出現對這個私有型別的描述。不過,我們距離想要的目標還近了一步。接下來就要使用介面來完成後續的了。

通過建立一個*可被訪問的*widget 型別可以實現的介面,我們的建構函式可以返回一個公開的型別例項,並且會顯示在 godoc 文件中。同時,這個介面的底層實現依然是私有的,使用者無法直接建立一個例項。

package widgets

import uuid "github.com/satori/go.uuid"

// Widget is a ...
type Widget interface {
    // ID 返回這個 widget 的唯一識別符號
    ID() string
}

type widget struct {
    id string
}

// NewWidget() 返回一個新的 Widget 例項
func NewWidget() Widget {
    return widget{
        id: uuid.NewV4().String(),
    }
}

func (w widget) ID() string {
    return w.id
}

總結

我希望我已經充分地闡述了 Go 語言的這一特質——建構函式的缺失反而促進了介面的使用。

在我的下一篇文章中,我將介紹一種幾乎與之相反的場景——在其他語言中要使用介面但是在 Go 語言中卻不必。


via: https://medium.com/@kent.rancourt/go-pointers-why-i-use-interfaces-in-go-338ae0bdc9e4

作者:Kent Rancourt 譯者:zhiyu-tracy-yang 校對:polaris1119

本文由 GCTT 原創編譯,Go語言中文網 榮譽推出

相關文章