golang的超時處理使用技巧

原文鏈接:https://www.zhoubotong.site/post/57.html

        大家知道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("https://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("https://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函數的定義:

Bash
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超時,就向客戶端返回錯誤,而不是一直等待響應了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章