我們應(yīng)該怎么樣編寫 Go 語言庫,有哪些標(biāo)準可以參考?
不久前我和朋友們想出一個主意,準備合并我們的 IRC bots,并用 Go 重寫它們。為了防止重寫大部分現(xiàn)有功能,我們試圖找到支持 bots 程序中使用的 Web API 的現(xiàn)有庫。我們的項目需要一個 Reddit API 的庫。這篇文章啟發(fā)于我找到的前三個庫,我不打算說出它們的名字,以免羞辱它們的作者。
上面說的每一個庫都存在一些基本問題以至于它們在真實場景中不可用。并且每個庫都以這樣一種方式編寫:不以非向后兼容的方式修改現(xiàn)有庫的 API,這樣是不可能修復(fù)問題的。不幸的是,由于很多其他的庫也存在同樣的問題,所以我會在下面列出一些作者錯誤的地方。
不要對 HTTP 客戶端硬編碼
很對庫都包含了對 http.DefaultClient 的硬編碼。雖然對庫本身來說這并不是問題,但是庫的作者并未理解應(yīng)該怎樣使用 http.DefaultClient 。正如 default client 建議它只在用戶沒有提供其他 http.Client 時才被使用。相反的是,許多庫作者樂意在他們代碼中涉及 http.DefaultClient 的部分采用硬編碼,而不是將它作為一個備選。這會導(dǎo)致在某些情況下這個庫不可用。
首先,我們很多人都讀過這篇講述 http.DefaultClient 不能自定義超時時間的文章《Don’t use Go’s default HTTP client (in production)[1]》,當(dāng)你沒法保證你的HTTP 請求一定會完成(或者至少要等一個完全無法預(yù)估時間的響應(yīng))時,你的程序可能會遇到奇怪的 goroutine 泄漏和一些無法預(yù)知的行為。在我看來,這會是每一個對 http.DefaultClient 采用硬編碼的庫不可用。
其次,網(wǎng)絡(luò)需要一些額外的配置。有時候需要用到代理,有時候需要對 URL 進行一丟丟的改寫,甚至可能 http.Transport 需要被一個定制的接口替換。當(dāng)一個程序員在你的庫里用他們自己的 http.Client 實例時,以上這些都很容易被實現(xiàn)。
在你的庫中處理 http.Client 的推薦方式是使用提供的客戶端,但是如果需要的話,有一個默認的備選:
- func CreateLibrary(client *http.Client) *Library { if client == nil { client = http.DefaultClient } ...}
或者如果你想從工廠函數(shù)中移除參數(shù),請在你的 struct 中定義一個輔助方法,并且讓用戶在需要時設(shè)置其屬性:
- type Library struct { Client *http.Client}func (l *Library) getClient() *http.Client { if l.Client == nil { return http.DefaultClient } return l.Client}
另外,如果一些全局的特性對于每個請求來講都是必須的,人們經(jīng)常感覺到需要用他們自己的實例來替換 http.Client。這是一個錯誤的方法 — 如果你需要在你的請求中設(shè)置一些額外的 headers,或者在你的客戶端引入某類公共的特性,你只需要簡單為每個請求進行設(shè)置或者用組裝定制客戶端的方式來代替完全替換它。
不要引入全局變量
另一個反面模式是允許用戶在一個庫中設(shè)置全局變量。舉個例子,在你的庫中允許用戶設(shè)置一個全局的 http.Client 并被所有的 HTTP 調(diào)用執(zhí)行:
- var libraryClient *http.Client = http.DefaultClientfunc SetHttpClient(client *http.Client) { libraryClient = client}
通常在一個庫中不應(yīng)該存在一堆全局變量。當(dāng)你寫代碼的時候,你應(yīng)該想想用戶在他們的程序中多次使用你的這個庫會發(fā)生什么。全局變量會使不同的參數(shù)沒有辦法被使用。而且,在你的代碼中引入全局變量會引起測試上的問題并造成代碼上不必要的復(fù)雜度。使用全局變量可能會導(dǎo)致在你程序的不同模塊有不必要的依賴。在寫你的庫的時候,避免全局狀態(tài)是格外重要的。
返回 structs,而不是 interfaces
這是一個普遍的問題(實際上我在這一點上也犯過錯)。很多庫都有下面這類函數(shù):
- func New() LibraryInterface { ...}
在上面的 case 中,返回一個 interface 使 struct 的特性在庫里被隱藏了。實際上應(yīng)該這么寫:
- func New() *LibraryStruct { ...}
在庫里不應(yīng)該存在接口的聲明,除非它被用在某個函數(shù)參數(shù)中。如果出現(xiàn)上面的 case,你就應(yīng)該想想你在寫這個庫的時候的約定。當(dāng)返回一個 interface 時,你基本上得聲明一系列可用的方法。如果有人想用這個接口來實現(xiàn)他們自己的功能(比如說為了測試),他得打亂他們的代碼來添加更多的方法。這意味著盡管在 struct 里添加方法是安全的,但在 interface 里不是。這個想法在這篇文章中被總結(jié)得很好《Accept Interfaces Return Struct in Go[2]》。這個方案也能解決配置的問題。你想修改庫中的一些特性,你可以簡單的修改 struct 中一些公開的字段。但是如果你的庫只提供給用戶一個 interface,這就玩不轉(zhuǎn)了。
使用配置結(jié)構(gòu)體來避免修改你的APIs
另一種配置方法是在你的工廠函數(shù)中接收一個配置結(jié)構(gòu)體,而不是直接傳配置參數(shù)。你可以很隨意的添加新的參數(shù)而不用破壞現(xiàn)有的 API。你只需要做一件事情,在Config結(jié)構(gòu)體中添加一個新的字段,并且確保不會影響它原本的特性。
- func New(config Config) *LibraryStruct { ...}
下面是一種添加結(jié)構(gòu)體字段的正確的場景,如果一個用戶初始化結(jié)構(gòu)體的時候忘了添加字段名,這是一種我認為修改他們的代碼能得到原諒的場景。為了維護兼容性,你應(yīng)該在你的代碼中用 person{name: "Alice", age: 30} 而不是 person{"Alice", 30}。
你能在 golang.org/x/crypto[4] 包里看到對上面的補充??傊?,對配置來說,我認為允許用戶在返回的結(jié)構(gòu)體里設(shè)置不同的參數(shù)是一個更好的方法,并且只在編寫復(fù)雜方法時才使用這種特定方法。
總結(jié)
根據(jù)經(jīng)驗來講,在寫一個庫的時候,你應(yīng)該總是允許用戶指定他們自己的 http.Client來執(zhí)行 HTTP 調(diào)用。而且考慮到未來迭代修改帶來的影響,你可以嘗試用可擴展的方式編寫代碼。避免全局變量,庫不能存儲全局狀態(tài)。如果你有任何疑問-參考標(biāo)準庫是怎么寫的。
我認為有一個很好的想法,在你的程序中用你的庫來測試并問自己一些問題:
- 如果你嘗試多次引入庫會發(fā)生什么?
- 你的庫有沒有單元測試?
- 在不破壞原有代碼的前提下,有沒有一種非侵入式的方式來擴展你的庫?
- 在不破壞原有代碼的前提下,是否可以添加額外配置參數(shù)?