Go語言12

context 使用介紹

主要功能:

  • 控制超時時間
  • 保存上下文數據

使用 context 處理超時

基本語法結構

import ""context""

// 生成和釋放定時器
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()

// 超時控制
select {
case <- ctx.Done():
    // 超時時要執行的代碼
default:
    // 其他情況執行的代碼
}

示例-訪問網站超時

這裏用了底層的request來發送GET請求。&http.Client{} 的結構體裏本身也有 Timeout 的設置,默認的0值就是不設置超時。並且Clinet要求它的 Transport 必須實現 CancelRequest 方法,默認的 Transport 是有這個方法的。所以下面的示例就是把底層的邏輯模擬了一遍,超時後手動調用 Transport 的 CancelRequest 方法:

package main

import (
    "context"
    "fmt"
    "os"
    "io/ioutil"
    "net/http"
    "time"
)

type Result struct {
    r *http.Response
    err error
}

func process(host string) {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()  // cancel是一個函數,執行後取消上面生成的定時器

    c := make(chan Result)
    tr := &http.Transport{}
    client := &http.Client{Transport: tr}
    req, err := http.NewRequest("GET", "http://" + host, nil)
    if err != nil {
        fmt.Fprintf(os.Stderr, "HTTP GET ERROR: %v\n", err)
        return
    }
    go func() {
        resp, err := client.Do(req)
        c <- Result{r: resp, err: err}
    }()
    select {
    case <- ctx.Done():
        tr.CancelRequest(req)  // 取消請求
        res := <- c
        fmt.Println("Timeout...", res.err)
    case res := <- c:
        defer res.r.Body.Close()
        out, _ := ioutil.ReadAll(res.r.Body)  // 第二個參數是err,這裏忽略錯誤
        fmt.Printf("Server Response:\n%s\n", out)
    }
    return
}

func main() {
    host := os.Args[1]
    process(host)
}

命令行接收第一個參數作爲請求的服務器地址,執行結果:

PS H:\Go\src\go_dev\day12\context> go run main.go google.com
Timeout... Get http://google.com: net/http: request canceled while waiting for connection
PS H:\Go\src\go_dev\day12\context> go run main.go baidu.com
Server Response:
<html>
<meta http-equiv="refresh" content="0;url=http://www.baidu.com/">
</html>

PS H:\Go\src\go_dev\day12\context>

一次請求超時,一次有返回結果。

使用 context 保存上下文

使用 context 還可以做一些自定的參數傳遞。以key-value的形式存儲到 context 的變量中,用的時候再取出來。
下面的例子試了 context 來傳遞變量:

package main

import (
    "fmt"
    "context"
)

func process(ctx context.Context) {
    age, ok := ctx.Value("age").(int)  // 取出的值在使用之前做一下類型斷言是比較好的做法
    if !ok {
        age = 18  // 如果有錯誤,就默認設置成18
    }
    fmt.Println("Age:", age)
    name, _ := ctx.Value("name").(string)  // 忽略錯誤
    fmt.Println("Name:", name)
    fmt.Println(ctx.Value("gender"))  // ctx不包括也不需要類型斷言,但是使用前做一下類型斷言比較好
    fmt.Println(ctx.Value("gender1"))  // 如果沒有對應的key,就返回nil
}

func main() {
    ctx := context.WithValue(context.Background(), "name", "Adam")  // 存的是鍵值對,後2個參數分別是key和value
    ctx = context.WithValue(ctx, "age", 23)  // 追加值,就使用之前的ctx,這樣所有的值都有了
    ctx = context.WithValue(ctx, "gender", "Male")
    process(ctx)  // 調用函數,把ctx傳進去,函數裏可以取出相應的值
}

這裏只是演示用法,例子裏的這類明確的變量,還是應該以傳統的方式來傳遞的。那些全局需要用到的變量,可以使用 context 來進行維護。因爲用了 context 之後,就把變量的信息給隱藏了,代碼的可讀性會變差。而且隱藏了變量的類型,也不符合go的習慣,所以上面在用之前,都在了類型斷言。

使用 context 結束 goroutine

通過 context.WithCancel 方法,還可以控制goroutine的生命週期:

// 定義結束控制
ctx, cancel:= context.WithCancel(context.Background())
defer cancel()

// 執行cancel()後,ctx.Done()這個管道里就能取到值
go func() {
    // 下面是一個無限循環,直到context返回,否則就一直循環下去
    for {
        select {
        case <-ctx.Done():
            return  // 從管道里取到值,就退出
        default:
            // 其他情況執行的代碼
        }
    }
}

這裏定義了2個變量,ctx和cancel。cancel是一個可以調用的函數,調用執行後。ctx.Done這個管道就能取出一個值了。在goroutine裏就可以通過這個管道來控制退出goroutine。完整示例如下:

package main

import (
    "time"
    "fmt"
    "context"
)

func test() {
    // 這是一個匿名函數的閉包,下面會調用
    gen := func(ctx context.Context) <-chan int {
        dst := make(chan int)
        n := 1
        go func() {
            for {
                select {
                case <-ctx.Done():
                    fmt.Println("goroutine 結束")
                    return
                case dst <- n:
                    n++
                }
            }
        }()
        return dst
    }

    ctx, cancel:= context.WithCancel(context.Background())
    defer cancel()

    for n:= range gen(ctx) {
        fmt.Println(n)
        if n == 5 {
            break
        }
    }
}

func main() {
    test()
    time.Sleep(time.Second * 3)
    fmt.Println("main 結束")
}

上面這個示例的應用場景就是:當你要啓用一個goroutine執行任務,並且還需要通知這個goroutine結束的時候,就可以通過這裏的 context 來實現。

DeadLine超時

WithDeadline 和 WithTimeout 是相似的。都是通過設置,會在某個時間自動觸發,就是ctx.Done()能夠取到值。差別是,DeadLine是設置一個時間點,時間對上了就到期。Timeout是設置一段時間,比如幾秒,過個這段時間,就超時。其實底層的Timeout也是通過Deadlin實現的,在Timeout裏,直接 return WithDeadline(parent, time.Now().Add(timeout))。
下面的例子設置了50毫秒超時,通過WithDeadline設置。運行程序接收一個命令行參數,傳入一個整數,進過這段毫秒的時間後,會輸出自定義的內容。如果數字大了(大於50),就會超時,這裏會輸出context的Err方法返回的信息:

package main

import (
    "context"
    "fmt"
    "time"
    "os"
    "strconv"
)

func main() {
    n, _ := strconv.Atoi(os.Args[1])  // 把第一個參數轉成整數,忽略錯誤
    d := time.Now().Add(50 * time.Millisecond)  // 50毫秒後過期
    ctx, cancel := context.WithDeadline(context.Background(), d)  // 如果是Timeout,就只直接傳上面Add裏的部分
    defer cancel()

    select {
    case <- time.After(time.Millisecond * time.Duration(n)):  // 第一個參數小於50,進入這個分支。參數不是數字就當做0
        fmt.Println("時間到了")
    case <- ctx.Done():
        fmt.Println(ctx.Err())
    }
}

/* 執行結果
PS H:\Go\src\go_dev\day12\context\deadline> go run main.go 123
context deadline exceeded
PS H:\Go\src\go_dev\day12\context\deadline> go run main.go 12
時間到了
PS H:\Go\src\go_dev\day12\context\deadline>
*/

這個例子還是使用Timeout更方便,不過這裏主要演示Deadline的用法。兩個方法的效果一樣,根據實際請求選擇合適的方法。TImeout應該更好用,所以纔會對Deadine再封裝一層,提供一個Timeout方法來給更多的應用場景使用。

sync.WaitGroup 介紹

之前都是通過管道來和goroutine傳遞數據的,也能通過管道實現等待。不過如果只是等待goroutine執行完畢,現在還有個方法可以實現。
通過使用 sync.WaitGroup ,可以方便的等待一組goroutine結束,具體就是下面的3步:

  1. 使用Add方法設置等待的數量,計數加1
  2. 使用Done方法設置等待數量,計數減1
  3. 當等待數量等於0時,Wait方法返回

示例1

下面是一個發 http 請求的示例,等待所有請求返回後,纔會退出主函數:

package main

import (
    "os"
    "sync"
    "fmt"
    "net/http"
)

var wg sync.WaitGroup

func main() {
    var urls = []string {
        "baidu.com",
        "51cto.com",
        "go-zh.org",
    }
    for _, url := range urls {
        wg.Add(1)  // 每開一個goroutine,計數加1
        go func(url string) {
            defer wg.Done()  // 退出時計數減1
            resp, err := http.Head("http://" + url)
            if err != nil {
                fmt.Fprintf(os.Stderr, "%s Head ERROR: %v", url, err)
                return
            }
            fmt.Println(*resp)
        }(url)
    }
    wg.Wait()  // 在這裏等待,所有任務完成,才繼續
    fmt.Println("All Requests Down")
}

示例2

下面的例子,展示另外一種風格的寫法:

package main

import (
    "fmt"
    "sync"
    "time"
)

func calc(w *sync.WaitGroup ,i int) {
    defer w.Done()
    fmt.Println("calc:", i)
    time.Sleep(time.Second)
}

func main() {
    // 另一種定義wg的方法,函數裏一般用短變量聲明
    // 這次不是全局變量了,下面還要傳參
    wg := sync.WaitGroup{}
    wg.Add(10)  // 一次加10,不在for循環裏每次加1了
    for i := 0; i < 10; i++ {
        go calc(&wg, i)  // 結構體是值類型,用地址來傳參
    }
    wg.Wait()
    fmt.Println("All goroutine Done")
}

小結

注意:Add方法不能放在goroutine裏面。看似沒問題,不過有可能還沒等goroutine運行起來,主函數就運行到Wait了。效果就是計數還沒開始也就是0,主函數就可以繼續執行下去了。是主函數先執行Wait還是goroutine先執行到Add就看運氣了
相比管道用起來更方便也更好理解一些,而且可以等待一組goroutine。這個方法只能實現主函數等待goroutine執行結束。如果需要通知某個goroutine退出,還是要用管道來實現。管道可以用來交互數據,所以所有的情況都適用。而 sync.WaitGroup 場景比較單一,但是更好用。

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