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