不要寫破壞性的 Go 庫

不久前我和朋友們想出一個主意,準備合併我們的 IRC bots,並用 Go 重寫它們。爲了防止重寫大部分現有功能,我們試圖找到支持 bots 程序中使用的 Web API 的現有庫。我們的項目需要一個 Reddit API 的庫。這篇文章啓發於我找到的前三個庫,我不打算說出它們的名字,以免羞辱它們的作者。

上面說的每一個庫都存在一些基本問題以至於它們在真實場景中不可用。並且每個庫都以這樣一種方式編寫:不以非向後兼容的方式修改現有庫的 API,這樣是不可能修復問題的。不幸的是,由於很多其他的庫也存在同樣的問題,所以我會在下面列出一些作者錯誤的地方。

不要對 HTTP 客戶端硬編碼

很對庫都包含了對 http.DefaultClient 的硬編碼。雖然對庫本身來說這並不是問題,但是庫的作者並未理解應該怎樣使用 http.DefaultClient 。正如 default client 建議它只在用戶沒有提供其他 http.Client 時才被使用。相反的是,許多庫作者樂意在他們代碼中涉及 http.DefaultClient 的部分採用硬編碼,而不是將它作爲一個備選。這會導致在某些情況下這個庫不可用。

首先,我們很多人都讀過這篇講述 http.DefaultClient 不能自定義超時時間的文章《Don’t use Go’s default HTTP client (in production)[1]》,當你沒法保證你的 HTTP 請求一定會完成(或者至少要等一個完全無法預估時間的響應)時,你的程序可能會遇到奇怪的 Goroutine 泄漏和一些無法預知的行爲。在我看來,這會使每一個對 http.DefaultClient 採用硬編碼的庫不可用。

其次,網絡需要一些額外的配置。有時候需要用到代理,有時候需要對 URL 進行一丟丟的改寫,甚至可能 http.Transport 需要被一個定製的接口替換。當一個程序員在你的庫裏用他們自己的 http.Client 實例時,以上這些都很容易被實現。

在你的庫中處理 http.Client 的推薦方式是使用提供的客戶端,但是如果需要的話,有一個默認的備選:

func CreateLibrary(client *http.Client) *Library {
    if client == nil {
        client = http.DefaultClient
    }
    ...
}

或者如果你想從工廠函數中移除參數,請在你的 struct 中定義一個輔助方法,並且讓用戶在需要時設置其屬性:

type Library struct {
    Client *http.Client
}

func (l *Library) getClient() *http.Client {
    if l.Client == nil {
        return http.DefaultClient
    }
    return l.Client
}

另外,如果一些全局的特性對於每個請求來講都是必須的,人們經常感覺到需要用他們自己的實例來替換 http.Client。這是一個錯誤的方法  —  如果你需要在你的請求中設置一些額外的 headers,或者在你的客戶端引入某類公共的特性,你只需要簡單爲每個請求進行設置或者用組裝定製客戶端的方式來代替完全替換它。

不要引入全局變量

另一個反面模式是允許用戶在一個庫中設置全局變量。舉個例子,在你的庫中允許用戶設置一個全局的 http.Client 並被所有的 HTTP 調用執行:

var libraryClient *http.Client = http.DefaultClient

func SetHttpClient(client *http.Client) {
    libraryClient = client
}

通常在一個庫中不應該存在一堆全局變量。當你寫代碼的時候,你應該想想用戶在他們的程序中多次使用你的這個庫會發生什麼。全局變量會使不同的參數沒有辦法被使用。而且,在你的代碼中引入全局變量會引起測試上的問題並造成代碼上不必要的複雜度。使用全局變量可能會導致在你程序的不同模塊有不必要的依賴。在寫你的庫的時候,避免全局狀態是格外重要的。

返回 structs,而不是 interfaces

這是一個普遍的問題(實際上我在這一點上也犯過錯)。很多庫都有下面這類函數:

func New() LibraryInterface {
    ...
}

在上面的 case 中,返回一個 interface 使 struct 的特性在庫裏被隱藏了。實際上應該這麼寫:

func New() *LibraryStruct {
    ...
}

在庫裏不應該存在接口的聲明,除非它被用在某個函數參數中。如果出現上面的 case,你就應該想想你在寫這個庫的時候的約定。當返回一個 interface 時,你基本上得聲明一系列可用的方法。如果有人想用這個接口來實現他們自己的功能 (比如說爲了測試),他得打亂他們的代碼來添加更多的方法。這意味着儘管在 struct 裏添加方法是安全的,但在 interface 裏不是。這個想法在這篇文章中被總結得很好《Accept Interfaces Return Struct in Go[2]》。這個方案也能解決配置的問題。你想修改庫中的一些特性,你可以簡單的修改 struct 中一些公開的字段。但是如果你的庫只提供給用戶一個 interface,這就玩不轉了。

詳情請參見 Go  http.Client[3]。

使用配置結構體來避免修改你的 APIs

另一種配置方法是在你的工廠函數中接收一個配置結構體,而不是直接傳配置參數。你可以很隨意的添加新的參數而不用破壞現有的 API。你只需要做一件事情,在 Config 結構體中添加一個新的字段,並且確保不會影響它原本的特性。

func New(config Config) *LibraryStruct {
    ...
}

下面是一種添加結構體字段的正確的場景,如果一個用戶初始化結構體的時候忘了添加字段名,這是一種我認爲修改他們的代碼能得到原諒的場景。爲了維護兼容性,你應該在你的代碼中用  person{name: "Alice", age: 30} 而不是 person{"Alice", 30}

你能在 golang.org/x/crypto[4] 包裏看到對上面的補充。總之,對配置來說,我認爲允許用戶在返回的結構體裏設置不同的參數是一個更好的方法,並且只在編寫複雜方法時才使用這種特定方法。

總結

根據經驗來講,在寫一個庫的時候,你應該總是允許用戶指定他們自己的  http.Client 來執行 HTTP 調用。而且考慮到未來迭代修改帶來的影響,你可以嘗試用可擴展的方式編寫代碼。避免全局變量,庫不能存儲全局狀態。如果你有任何疑問 - 參考標準庫是怎麼寫的。

我認爲有一個很好的想法,在你的程序中用你的庫來測試並問自己一些問題:


via: https://0x46.net/thoughts/2018/12/29/go-libraries/

作者:Filip Borkiewicz[5] 譯者:Alihanniba[6] 校對:zhoudingding[7]

本文由 GCTT[8] 原創編譯,Go 中文網 [9] 榮譽推出,發佈在 Go 語言中文網公衆號,轉載請聯繫我們授權。

參考資料

[1]

Don’t use Go’s default HTTP client (in production): https://medium.com/@nate510/don-t-use-go-s-default-http-client-4804cb19f779

[2]

Accept Interfaces Return Struct in Go: https://mycodesmells.com/post/accept-interfaces-return-struct-in-go

[3]

http.Client: https://golang.org/pkg/net/http/#Client

[4]

golang.org/x/crypto: https://godoc.org/golang.org/x/crypto/openpgp#Sign

[5]

Filip Borkiewicz: https://0x46.net/

[6]

Alihanniba: https://github.com/Alihanniba

[7]

zhoudingding: https://github.com/dingdingzhou

[8]

GCTT: https://github.com/studygolang/GCTT

[9]

Go 中文網: https://studygolang.com/

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/opscI_MMHo1Y7LkYdUcdcQ