Go Interface 的優(yōu)雅使用,讓代碼更整潔更容易測(cè)試
【公眾號(hào)回復(fù) “1024”,免費(fèi)領(lǐng)取程序員賺錢(qián)實(shí)操經(jīng)驗(yàn)】

大家好,我是你們的章魚(yú)貓。
在 Go 語(yǔ)言中,如果你還不會(huì)使用 Interface,那么你還沒(méi)有真正掌握 Go 語(yǔ)言,Interface 是 Go 語(yǔ)言的精華和靈魂所在。接下來(lái)我們從一下幾個(gè)方面介紹 Go 語(yǔ)言里面的 Interface。
Go Interface 是什么?
簡(jiǎn)單來(lái)說(shuō),Interface 是一組方法(Method)的集合,也是一種類(lèi)型。比如如下是一個(gè)簡(jiǎn)單的 Interface 定義。
type UserDataStore interface {
? ? GetUserScore(ctx context.Context, id string) (int, error)
? ? DeleteUser(ctx context.Context, id string) error
}
另外,在 Go 里面是允許沒(méi)有任何方法的 Interface,對(duì)于這樣的空 Interface,可以認(rèn)為任何的類(lèi)型都實(shí)現(xiàn)了空 Interface。
假設(shè),一個(gè)類(lèi)型 A 實(shí)現(xiàn)了上述 Interface(UserDataStore)的方法,我們就可以認(rèn)為 A 實(shí)現(xiàn)了上述 Interface,在實(shí)際的函數(shù)調(diào)用傳參中 A 是可以直接作為 UserDataStore 類(lèi)型的參數(shù)。是的,可以理解為這就是我們常說(shuō)的多態(tài)。
Go Interface 能做什么?
那么 Interface 除了可以實(shí)現(xiàn)多態(tài),實(shí)際可以用來(lái)做什么呢。以下是我認(rèn)為比較重要的亮點(diǎn)。
依賴(lài)反轉(zhuǎn),讓代碼結(jié)構(gòu)更整潔
我們來(lái)說(shuō)一個(gè)比較常見(jiàn)的場(chǎng)景,一個(gè) HTTP 接口,需要依賴(lài)數(shù)據(jù)庫(kù)來(lái)獲取用戶(hù)得分并返回給調(diào)用方。比較直接的寫(xiě)法如下。
db := orm.NewDatabaseConnection() ?// 建立數(shù)據(jù)庫(kù)鏈接
res := db.Query("select score from user where user == 'xxx'") ? // 通過(guò) SQL 語(yǔ)句查詢(xún)數(shù)據(jù)
return HTTP.Json(res) // 通過(guò) Json 返回給調(diào)用方
這樣寫(xiě)的壞處是,讓 HTTP 的接口依賴(lài)了具體數(shù)據(jù)庫(kù)底層的接口及實(shí)現(xiàn),在數(shù)據(jù)庫(kù)查詢(xún)的功能沒(méi)有開(kāi)發(fā)完成時(shí),HTTP 接口是不能開(kāi)始開(kāi)發(fā)的。同時(shí)對(duì)于如果后續(xù)存在更換數(shù)據(jù)庫(kù)的可能,也不是很容易的擴(kuò)展。比較推薦的寫(xiě)法是下面這樣。
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)))
? ? }
}
通過(guò)定義 Interface,將數(shù)據(jù)庫(kù)與 HTTP 接口進(jìn)行解耦,HTTP 接口不再依賴(lài)實(shí)際的數(shù)據(jù)庫(kù),代碼可以單獨(dú)的編寫(xiě)和編譯,代碼依賴(lài)和結(jié)構(gòu)更加的清晰了。數(shù)據(jù)具體的實(shí)現(xiàn)邏輯只需按 Interface 實(shí)現(xiàn)對(duì)應(yīng)的接口就可以了,最終實(shí)現(xiàn)了依賴(lài)的整體的反轉(zhuǎn)。
提高程序的可測(cè)試性
回到剛才那個(gè)例子,如果我要對(duì)這個(gè) HTTP 接口的邏輯做測(cè)試,我可以怎么做?如果你沒(méi)有使用 Interface,那么測(cè)試肯定要依賴(lài)一個(gè)實(shí)際的 DB,我想你會(huì)去新建一個(gè)測(cè)試庫(kù),同時(shí)新建一些測(cè)試數(shù)據(jù)。
真的需要這樣么?我們來(lái)一個(gè)比較好的實(shí)踐。通過(guò) Interface,可以很容易的實(shí)現(xiàn)一個(gè) Mock 版本的類(lèi)型,通過(guò)替換邏輯可以很方便的實(shí)現(xiàn)測(cè)試數(shù)據(jù)的構(gòu)造。
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
}
以上就可以很方便的去控制接口調(diào)用的時(shí)候,獲取用戶(hù)得分和刪除用戶(hù)的邏輯。實(shí)際的測(cè)試也就變得簡(jiǎn)單了,也不用依賴(lài)真實(shí)的 DB,讓測(cè)試更加的可靠了。
func TestGetUserScoreHandlerReturnsScore(t *testing.T) {
? ? req := httptest.NewRequest("GET", "/idk", nil)
? ? res := httptest.NewRecorder()
? ? userDataStore := &mockUserDataStore{
? ? ? ? pendingScore: 3, ?// mock 數(shù)據(jù)
? ? }
? ? handler := GetUserScoreHandler(userDataStore)? ?// 將 Mock 的方法傳遞到實(shí)際調(diào)用的地方,實(shí)現(xiàn)動(dòng)態(tài)的替換
? ? 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)
? ? }
}
以上單元測(cè)試是不是就很簡(jiǎn)單了。
如何優(yōu)雅的使用 Go Interface?
以上的樣例其實(shí)都來(lái)自今天要推薦的開(kāi)源項(xiàng)目。如果你非常關(guān)注架構(gòu)和代碼的整潔,以及代碼的可測(cè)試性,非常推薦大家看一下。
更多項(xiàng)目詳情請(qǐng)查看如下鏈接,尤其是項(xiàng)目中的代碼,很簡(jiǎn)單但非常值得看一下。
開(kāi)源項(xiàng)目地址:https://github.com/Evertras/go-interface-examples
---特別推薦---
特別推薦:一個(gè)新的優(yōu)質(zhì)的推薦高效工具,軟件,插件的公眾號(hào),每天給大家分享優(yōu)秀的效率工具,「程序員掘金」,專(zhuān)門(mén)為程序員挖掘好東西的一個(gè)公眾號(hào),非常值得大家關(guān)注。
