與 ModuleInternal 介面一樣,下載器介面 Downloader 也內嵌了 Module 介面,它額外宣告了一個 Download 方法。有了 ModuleInternal 介面及其實現型別,實現下載器時只需關注它的特色功能,其他的都交給內嵌的 stub.ModuleInternal 就可以了。
下載器的實現型別名為 myDownloader,其宣告如下:
//下載器的實現型別
type myDownloader struet {
//元件基礎範例
stub.ModuleInternal
//下載用的HTTP用戶端
httpClient http.Client
}
可以看到,我匿名地嵌入了一個 stub.ModuleInternal 型別的欄位,這種只有型別而沒有名稱的欄位稱為匿名欄位。如此一來,myDownloader 型別的方法集合中就包含了 stub.ModuleInternal 型別的所有方法。因而,*myDownloader 型別已經實現了 Module 介面。
另一個 http.Client 型別的欄位用於對目標伺服器傳送 HTTP 請求並接收響應。http.Client 型別是做 HTTP 用戶端程式的必選,它開箱即用,同時又很開放,有很多可客製化的地方。
myDownloader 型別的這兩個欄位的值都需要使用方直接或間接提供。關於第一個欄位的值,可以很容易通過 stub.NewModuleInternal 函數生成。而第二個欄位的值,可以直接通過複合字面量 http.Client{} 生成。
不過,我強烈建議你對它進行一些客製化,如果你想讓下載器跑得更快的話。後面講網路爬蟲程式範例的時候,我們會給出一些建議。
程式碼包 gopcp.v2/chapter6/webcrawler/module/local/downloader 中存放了所有與下載 器實現有關的程式碼,大家可以從我的網路硬碟中下載相關程式碼包(連結:https://pan.baidu.com/s/1yzWHnK1t2jLDIcTPFMLPCA 提取碼:slm5)。為了方便使用方建立下載器的範例,可以在其中編寫了一個名為 New 的函數:
//用於建立一個下載器範例
func New(
mid module.MID,
client *http.Client,
scoreCalculator module.CalculateScore) (module.Downloader, error) {
moduleBase, err := stub.NewModuleInternal(mid, scoreCalculator)
if err != nil {
return nil, err
}
if client == nil {
return nil, genParameterError( "nil http client")
}
return &myDownloader{
ModuleInternal: moduleBase,
httpClient: *client,
}, nil
}
上述程式碼中,stub.NewModuleInternal 函數需要元件 ID 和元件評分計算器來生成元件內部基礎型別的值,那我就讓 New 函數的引數宣告列表包含它們。對這兩個引數的校驗 由 stub.NewModuleInternal 函數全權負責。
注意,這裡還隱藏著一個 Go語言的命名慣例。由於下載器的實現程式碼獨佔一個程式碼包,所以可以讓這個函數的名稱足夠簡單,只有一個單詞 New。這不同於前面提到的函數 NewPool 和 NewMultipleReader,這兩個函數所建立的範例的含義無法由其所在程式碼包的名稱 buffer 和 reader 表達。
另外,雖然函數 NewBuffer 所建立的範例的含義可以由其所在的程式碼包 buffer 表達,但是該包中用於建立範例的函數不止它一個。如果把它們的名稱簡化為 New,恐怕會造成表達上的不清晰。而 downloader 包中唯一用於建立範例的函數 New,可以讓你馬上明白它就是用於建立下載器範例的,並不需要過多解釋。這就是命名方面的慣用法,也是一種技巧。
下面來看下載器的 Download 方法的實現:
func (downloader *myDownloader) Download(req *module.Request) (*module.Response, error) {
downloader.ModuleInternal.IncrHandlingNumber()
defer downloader.ModuleInternal.DecrHandlingNumber()
downloader.ModuleInternal.IncrCalledCount()
if req == nil {
return nil, genParameterError("nil request")
}
httpReq := req.HTTPReq()
if httpReq == nil {
return nil, genParameterError("nil HTTP request")
}
downloader.ModuleInternal.IncrAcceptedCount()
logger.Infof("Do the request (URL: %s, depth: %d)... n", httpReq.URL, req.Depth()) httpResp, err := downloader.httpClient.Do(httpReq)
if err != nil {
return nil, err
}
downloader.ModuleInternal.IncrCompletedCount()
return module.NewResponse(httpResp, req.Depth()), nil
}
這個方法的功能實現起來很簡單,不過要注意對那 4 個元件計數的操作。在方法的開始處,要遞增實時處理數,並利用 defer 語句保證方法執行結束時遞減這個計數。同時, 還要遞增呼叫計數。
在所有引數檢查都通過後,要遞增接受計數以表明該方法接受了這次呼叫。一旦目標伺服器發回了 HTTP 響應並且未發生錯誤,就可以遞增成功完成計數 To 這代表當前元件範例又有效地為使用者提供了一次服務。