golang的超時處理使用技巧
原文鏈接://www.zhoubotong.site/post/57.html
golang的超時處理
大家知道Select 是 Go 中的一個控制結構,每個 case 必須是一個通訊操作,要麼是發送要麼是接收操作。 select是 隨機執行一個可運行的 case。
如果沒有 case 可運行,程式可能會阻塞,直到有 case 可運行。當然有一個默認的子句(default子句)在沒有任何條件滿足的時候總是可運行的。
對於處理資源密集型的應用程式,超時處理是不可避免的。檢查超時是有必要的,以確保超時運行的任務不會消耗應用程式的其他服務組件可能需要的資源或網路頻寬。
Golang處理超時的方法非常簡單。不需要複雜的程式碼,我們可以用channel通訊和使用select語句作出超時決策來處理超時的問題。
在Go中,Select主要是和channel有關,其大概的格式如下:
select{
case <- ch1: // 讀取ch1
// 執行操作
case i := <- ch2 // 讀取ch2
// 使用i 執行操作
default:
//
}
Go的select與channel配合使用進行超時處理。channel必須是有緩衝channel,不然就是同步操作了。
select用於等待一個或者多個channel的輸出。
應用場景
主goroutine等待子goroutine完成,但是子goroutine無限運行,導致主goroutine會一直等待下去(注意main也是一個攜程)。而主執行緒想超過了一定的時間如果沒有返回的話,
這時候可以進行超時判斷然後繼續運行下去。
package main
import (
"fmt"
"time"
)
func main() {
chn := make(chan bool, 1)
// 並發執行一個函數,等待3s後向chn寫入true
go func() {
time.Sleep(3 * time.Second)
chn <- true
}()
/*
這裡會等待chn或timeout讀出數據
因為一直沒有向chn寫入數據
在3s後向chn寫入了數據
所以執行了timeout的case
利用這個技巧可以實現超時操作
*/
select {
case chn1 := <-chn:
fmt.Println(chn1)
return
case <-time.After(4 * time.Second): //超時判斷(程式執行4s後,因為3s內chn已經發送了true,所以輸出 true)
fmt.Println("超時timeout")
//如果將time.After中改為1*time.Second,則輸出為:
return
}
}
我再舉個開發中經常用到的例子,比如模擬網路連接,我們從一個模擬get請求的服務中讀取響應。
如下面我編寫一個簡單結構體來接收服務的響應內容(這個例子沒有考慮超時問題,稍後我後面說明補上)。
type Result struct {
UserID int `json:"user_id"`
ID int `json:"id"`
Title string `json:"title"`
Completed bool `json:"completed"`
}
這裡我直截了當地寫了一個快速方法來獲取服務中的響應,並返回給客戶端,完整程式碼如下:
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
)
type Result struct {
UserID int `json:"user_id"`
ID int `json:"id"`
Title string `json:"title"`
Completed bool `json:"completed"`
}
// 發送http get請求獲取相應數據
func GetHttpResult(URL string) (*Result, error) {
resp, err := http.Get(URL)
if err != nil {
return nil, fmt.Errorf(err.Error())
}
defer resp.Body.Close()
byteResp, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf(err.Error())
}
structResp := &Result{}
err = json.Unmarshal(byteResp, structResp) // 解析json數據到Result結構體指向的值
if err != nil {
return nil, fmt.Errorf("error in unmarshalling Result")
}
return structResp, nil
}
func main() {
res, err := GetHttpResult("//jsonplaceholder.typicode.com/todos/1") // 正常該請求毫秒回
if err != nil {
fmt.Printf("err %v", err)
} else {
fmt.Printf("res %v", res)
}
}
這是非常簡單的方法。只是使用Golang原生http庫讀取http調用中的資訊,並將響應內容存放在結構體中,在不同的步驟中處理錯誤。非常簡單!
結果輸出了一個來自模擬服務的虛擬響應資訊如下(未超時):
res &{0 1 delectus aut autem false}
現在來看請求正常,假設連接需要很長時間才能從伺服器中獲得響應,那麼main函數將等待不確定時間了。
在實際應用程式中,這是沒法接受的,因為這會消耗很多資源。要解決這個問題,我們在GetHttpResult函數中添加一個context參數。
func GetHttpResult(ctx context.Context) (*Result, error)
這個context可以告我們何時停止嘗試從網路中獲取的結果。為了驗證這一點,先編寫一個幫助函數,執行和前面相同的操作,返回結果並將結果寫入channel,
並使用一個獨立的goroutine來執行實際的工作。為了簡單起見,可以將響應和錯誤包裝在一個CallResult結構體中,完整程式碼如下:
package main
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"time"
)
// 定義響應結構體
type Result struct {
UserID int `json:"user_id"`
ID int `json:"id"`
Title string `json:"title"`
Completed bool `json:"completed"`
}
type CallResult struct {
Resp *Result
Err error
}
func helper(ctx context.Context) <-chan *CallResult {
respChan := make(chan *CallResult, 1)
go func() {
resp, err := http.Get("//jsonplaceholder.typicode.com/todos/1")
time.Sleep(2000 * time.Millisecond) // 模擬超時請求 Millisecond表示1毫秒的時間間隔
//比如睡眠1小時10分5秒:time.Sleep(1*time.Hour + 10*time.Minute + 5*time.Second)
if err != nil {
respChan <- &CallResult{nil, fmt.Errorf(err.Error())}
return
}
defer resp.Body.Close()
byteResp, err := ioutil.ReadAll(resp.Body)
if err != nil {
respChan <- &CallResult{nil, fmt.Errorf(err.Error())}
return
}
structResp := &Result{}
err = json.Unmarshal(byteResp, structResp)
if err != nil {
respChan <- &CallResult{nil, fmt.Errorf("error in unmarshalling Result")}
}
respChan <- &CallResult{structResp, nil}
}()
return respChan
}
func GetHttpResult(ctx context.Context) (*Result, error) {
select {
case <-ctx.Done():
return nil, fmt.Errorf("context timeout, ran out of time")
case respChan := <-helper(ctx):
return respChan.Resp, respChan.Err
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) // 超過1s響應標記為超時
defer cancel()
res, err := GetHttpResult(ctx)
if err != nil {
fmt.Printf("err %v", err)
} else {
fmt.Printf("res %v", res)
}
}
運行上面程式碼可以得到和之前相同的響應資訊(注釋掉time.Sleep)正常輸出:
res &{0 1 delectus aut autem false}
上面所示程式碼中,創建了一個帶快取的respChan通道。然後執行和GetHttpResult函數相同的工作,但是用一個CallResult來代替返迴響應和錯誤。
函數結束返回resChan。然後在單獨的goroutine中執行網路連接,並將結果寫入channel。這樣程式碼實現非阻塞。
可以看到GetHttpResult函數現在變的更簡單了,因為它必須做一個簡單的選擇。要麼從通道中讀取響應要麼超時退出。
上面實現超時策略是通過select語句來完成的。以下是Done函數的定義:
Done() <-chan struct{}
Done返回一個channel,當涉及的context被取消,channel就會關閉。當context中有超時,就會在超時的時候對通道進行寫操作。
在這種情況下,程式碼返回一個表示超時的錯誤響應資訊。
另一個case是,helper函數能夠在超時之前完成服務的響應讀取,並寫入channel。在這種情況下,在respChan變數中得到結果並返回給客戶端。
上面main函數中調用GetHttpResult並傳入一個1秒超時的context參數。再將超時減少到1毫秒(
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond)
),因為1毫秒內不足以完成網路調用。因此不會過多佔用任何資源,而且只要context超時,就向客戶端返回錯誤,而不是一直等待響應了。

