Go Interface 的優雅使用,讓程式碼更整潔更容易測試

相守之路發表於2020-10-20

在 Go 語言中,如果你還不會使用 Interface,那麼你還沒有真正掌握 Go 語言,Interface 是 Go 語言的精華和靈魂所在。接下來我們從一下幾個方面介紹 Go 語言裡面的 Interface。

Go Interface 是什麼?

簡單來說,Interface 是一組方法(Method)的集合,也是一種型別。比如如下是一個簡單的 Interface 定義。

type UserDataStore interface {
    GetUserScore(ctx context.Context, id string) (int, error)
    DeleteUser(ctx context.Context, id string) error
}

另外,在 Go 裡面是允許沒有任何方法的 Interface,對於這樣的空 Interface,可以認為任何的型別都實現了空 Interface。

假設,一個型別 A 實現了上述 Interface(UserDataStore)的方法,我們就可以認為 A 實現了上述 Interface,在實際的函式呼叫傳參中 A 是可以直接作為 UserDataStore 型別的引數。是的,可以理解為這就是我們常說的多型。

Go Interface 能做什麼?

那麼 Interface 除了可以實現多型,實際可以用來做什麼呢。以下是我認為比較重要的亮點。

1.依賴反轉,讓程式碼結構更整潔

我們來說一個比較常見的場景,一個 HTTP 介面,需要依賴資料庫來獲取使用者得分並返回給呼叫方。比較直接的寫法如下。

db := orm.NewDatabaseConnection()  // 建立資料庫連結
res := db.Query("select score from user where user == 'xxx'")   // 通過 SQL 語句查詢資料
return HTTP.Json(res) // 通過 Json 返回給呼叫方

這樣寫的壞處是,讓 HTTP 的介面依賴了具體資料庫底層的介面及實現,在資料庫查詢的功能沒有開發完成時,HTTP 介面是不能開始開發的。同時對於如果後續存在更換資料庫的可能,也不是很容易的擴充套件。比較推薦的寫法是下面這樣。

type UserDataStore interface {

    GetUserScore(ctx context.Context, id string) (int, error)
    DeleteUser(ctx context.Context, id string) error
}

// GetUserScoreHandler creates an HTTP handler that can get a user's score
func GetUserScoreHandler(userDataStore UserDataStore) http.HandlerFunc {
    return func(res http.ResponseWriter, req *http.Request) {
        id := req.Header.Get("x-user-id")
        score, err := userDataStore.GetUserScore(req.Context(), id)
        if err != nil {
            fmt.Println("userDataStore.GetUserScore: ", err)
            res.WriteHeader(500)
            return
        }

        res.Write([]byte(fmt.Sprintf("%d", score)))
    }
}

通過定義 Interface,將資料庫與 HTTP 介面進行解耦,HTTP 介面不再依賴實際的資料庫,程式碼可以單獨的編寫和編譯,程式碼依賴和結構更加的清晰了。資料具體的實現邏輯只需按 Interface 實現對應的介面就可以了,最終實現了依賴的整體的反轉。

2.提高程式的可測試性

回到剛才那個例子,如果我要對這個 HTTP 介面的邏輯做測試,我可以怎麼做?如果你沒有使用 Interface,那麼測試肯定要依賴一個實際的 DB,我想你會去新建一個測試庫,同時新建一些測試資料。

真的需要這樣麼?我們來一個比較好的實踐。通過 Interface,可以很容易的實現一個 Mock 版本的型別,通過替換邏輯可以很方便的實現測試資料的構造。

type mockUserDataStore struct {
    pendingError error
    pendingScore int

    deletedUsers []string
}

func (m *mockUserDataStore) GetUserScore(ctx context.Context, id string) (int, error) {
    return m.pendingScore, m.pendingError
}

func (m *mockUserDataStore) DeleteUser(ctx context.Context, id string) error {
    if m.pendingError != nil {
        return m.pendingError
    }

    m.deletedUsers = append(m.deletedUsers, id)
    return nil
}

以上就可以很方便的去控制介面呼叫的時候,獲取使用者得分和刪除使用者的邏輯。實際的測試也就變得簡單了,也不用依賴真實的 DB,讓測試更加的可靠了。

func TestGetUserScoreHandlerReturnsScore(t *testing.T) {
    req := httptest.NewRequest("GET", "/idk", nil)
    res := httptest.NewRecorder()

    userDataStore := &mockUserDataStore{
        pendingScore: 3,  // mock 資料
    }

    handler := GetUserScoreHandler(userDataStore)   // 將 Mock 的方法傳遞到實際呼叫的地方,實現動態的替換
    handler(res, req)

    resultStr := string(res.Body.Bytes())
    expected := fmt.Sprintf("%d", userDataStore.pendingScore)

    if res.Code != 200 {
        t.Errorf("Expected HTTP response 200 but got %d", res.Code)
    }

    if resultStr != expected {
        t.Errorf("Expected body to contain value %q but got %q", expected, resultStr)
    }
}

以上單元測試是不是就很簡單了。

如何優雅的使用 Go Interface?

以上的樣例其實都來自今天要推薦的開源專案。如果你非常關注架構和程式碼的整潔,以及程式碼的可測試性,非常推薦大家看一下。

更多專案詳情請檢視如下連結,尤其是專案中的程式碼,很簡單但非常值得看一下。

開源專案地址:https://github.com/Evertras/go-interface-examples

轉自:章魚貓——Go Interface 的優雅使用,讓程式碼更整潔更容易測試述

相關文章