新聞中心
不久前我和朋友們想出一個(gè)主意,準(zhǔn)備合并我們的 IRC bots,并用 Go 重寫它們。為了防止重寫大部分現(xiàn)有功能,我們?cè)噲D找到支持 bots 程序中使用的 Web API 的現(xiàn)有庫。我們的項(xiàng)目需要一個(gè) Reddit API 的庫。這篇文章啟發(fā)于我找到的前三個(gè)庫,我不打算說出它們的名字,以免羞辱它們的作者。

上面說的每一個(gè)庫都存在一些基本問題以至于它們?cè)谡鎸?shí)場(chǎng)景中不可用。并且每個(gè)庫都以這樣一種方式編寫:不以非向后兼容的方式修改現(xiàn)有庫的 API,這樣是不可能修復(fù)問題的。不幸的是,由于很多其他的庫也存在同樣的問題,所以我會(huì)在下面列出一些作者錯(cuò)誤的地方。
不要對(duì) HTTP 客戶端硬編碼
很對(duì)庫都包含了對(duì) http.DefaultClient 的硬編碼。雖然對(duì)庫本身來說這并不是問題,但是庫的作者并未理解應(yīng)該怎樣使用 http.DefaultClient 。正如 default client 建議它只在用戶沒有提供其他 http.Client 時(shí)才被使用。相反的是,許多庫作者樂意在他們代碼中涉及 http.DefaultClient 的部分采用硬編碼,而不是將它作為一個(gè)備選。這會(huì)導(dǎo)致在某些情況下這個(gè)庫不可用。
首先,我們很多人都讀過這篇講述 http.DefaultClient 不能自定義超時(shí)時(shí)間的文章《Don’t use Go’s default HTTP client (in production)[1]》,當(dāng)你沒法保證你的HTTP 請(qǐng)求一定會(huì)完成(或者至少要等一個(gè)完全無法預(yù)估時(shí)間的響應(yīng))時(shí),你的程序可能會(huì)遇到奇怪的 goroutine 泄漏和一些無法預(yù)知的行為。在我看來,這會(huì)是每一個(gè)對(duì) http.DefaultClient 采用硬編碼的庫不可用。
其次,網(wǎng)絡(luò)需要一些額外的配置。有時(shí)候需要用到代理,有時(shí)候需要對(duì) URL 進(jìn)行一丟丟的改寫,甚至可能 http.Transport 需要被一個(gè)定制的接口替換。當(dāng)一個(gè)程序員在你的庫里用他們自己的 http.Client 實(shí)例時(shí),以上這些都很容易被實(shí)現(xiàn)。
在你的庫中處理 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)求來講都是必須的,人們經(jīng)常感覺到需要用他們自己的實(shí)例來替換 http.Client。這是一個(gè)錯(cuò)誤的方法 — 如果你需要在你的請(qǐng)求中設(shè)置一些額外的 headers,或者在你的客戶端引入某類公共的特性,你只需要簡(jiǎn)單為每個(gè)請(qǐng)求進(jìn)行設(shè)置或者用組裝定制客戶端的方式來代替完全替換它。
不要引入全局變量
另一個(gè)反面模式是允許用戶在一個(gè)庫中設(shè)置全局變量。舉個(gè)例子,在你的庫中允許用戶設(shè)置一個(gè)全局的 http.Client 并被所有的 HTTP 調(diào)用執(zhí)行:
- var libraryClient *http.Client = http.DefaultClientfunc SetHttpClient(client *http.Client) { libraryClient = client}
通常在一個(gè)庫中不應(yīng)該存在一堆全局變量。當(dāng)你寫代碼的時(shí)候,你應(yīng)該想想用戶在他們的程序中多次使用你的這個(gè)庫會(huì)發(fā)生什么。全局變量會(huì)使不同的參數(shù)沒有辦法被使用。而且,在你的代碼中引入全局變量會(huì)引起測(cè)試上的問題并造成代碼上不必要的復(fù)雜度。使用全局變量可能會(huì)導(dǎo)致在你程序的不同模塊有不必要的依賴。在寫你的庫的時(shí)候,避免全局狀態(tài)是格外重要的。
返回 structs,而不是 interfaces
這是一個(gè)普遍的問題(實(shí)際上我在這一點(diǎn)上也犯過錯(cuò))。很多庫都有下面這類函數(shù):
- func New() LibraryInterface { ...}
在上面的 case 中,返回一個(gè) interface 使 struct 的特性在庫里被隱藏了。實(shí)際上應(yīng)該這么寫:
- func New() *LibraryStruct { ...}
在庫里不應(yīng)該存在接口的聲明,除非它被用在某個(gè)函數(shù)參數(shù)中。如果出現(xiàn)上面的 case,你就應(yīng)該想想你在寫這個(gè)庫的時(shí)候的約定。當(dāng)返回一個(gè) interface 時(shí),你基本上得聲明一系列可用的方法。如果有人想用這個(gè)接口來實(shí)現(xiàn)他們自己的功能(比如說為了測(cè)試),他得打亂他們的代碼來添加更多的方法。這意味著盡管在 struct 里添加方法是安全的,但在 interface 里不是。這個(gè)想法在這篇文章中被總結(jié)得很好《Accept Interfaces Return Struct in Go[2]》。這個(gè)方案也能解決配置的問題。你想修改庫中的一些特性,你可以簡(jiǎn)單的修改 struct 中一些公開的字段。但是如果你的庫只提供給用戶一個(gè) interface,這就玩不轉(zhuǎn)了。
使用配置結(jié)構(gòu)體來避免修改你的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ì)配置來說,我認(rèn)為允許用戶在返回的結(jié)構(gòu)體里設(shè)置不同的參數(shù)是一個(gè)更好的方法,并且只在編寫復(fù)雜方法時(shí)才使用這種特定方法。
總結(jié)
根據(jù)經(jīng)驗(yàn)來講,在寫一個(gè)庫的時(shí)候,你應(yīng)該總是允許用戶指定他們自己的 http.Client來執(zhí)行 HTTP 調(diào)用。而且考慮到未來迭代修改帶來的影響,你可以嘗試用可擴(kuò)展的方式編寫代碼。避免全局變量,庫不能存儲(chǔ)全局狀態(tài)。如果你有任何疑問-參考標(biāo)準(zhǔn)庫是怎么寫的。
我認(rèn)為有一個(gè)很好的想法,在你的程序中用你的庫來測(cè)試并問自己一些問題:
- 如果你嘗試多次引入庫會(huì)發(fā)生什么?
- 你的庫有沒有單元測(cè)試?
- 在不破壞原有代碼的前提下,有沒有一種非侵入式的方式來擴(kuò)展你的庫?
- 在不破壞原有代碼的前提下,是否可以添加額外配置參數(shù)?
分享文章:我們應(yīng)該怎么樣編寫 Go 語言庫,有哪些標(biāo)準(zhǔn)可以參考?
瀏覽路徑:http://www.5511xx.com/article/djdeocp.html


咨詢
建站咨詢
