10.7.1 context使用的高级示例

使用 useContext.go 程序代码将更好,更深入的说明 context 包的功能,该代码分为五部分来介绍。

这个例子中,您将创建一个快速响应的 HTTP 客户端,这是一个很常见的需求。事实上,几乎所有的 HTTP 客户端都支持这个功能,您将学习另一个 HTTP 请求超时的技巧,在第12章(Go网络编程基础)。

useContext.go 程序需要两个命令行参数:要连接的服务器 URL 和应该等待的延迟。如果该程序只有一个命名行参数,那么延迟将是 5 秒。

useContext.go 的第一段代码如下:

package main

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

var (
    myUrl   string
    delay   int = 5
    w       sync.WaitGroup
)

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

myUrldelay 都是全局变量,因此它们能从代码的任意位置访问。另外,有一个名为 wsync..WaitGroup 变量也是全局的,还有一个名为 myData 的结构体用于把 web 服务器的响应和一个错误变量绑在一起。

useContext.go 的第二部分代码如下:

func connect(c context.Context) error {
    defer w.Done()
    data := make(chan myData, 1)
    tr := &http.Transport{}
    httpClient := &http.Client{Transport: tr}

    req, _ := http.NewRequest("GET", myUrl, nil)

上面的代码处理 HTTP 连接。

您会了解更多关于开发 web 服务器和客户端的内容,在第12章(Go网络编程基础)。

useContext.go 的第三段代码如下:

    go func() {
        response, err := httpClient.Do(req)
        if err != nil {
            fmt.Println(err)
            data <- myData{nil, err}
            return
        } else {
            pack := myData{response, err}
            data <- pack
        }
    }()

useContext.go 的第四段代码如下:

    select {
        case <-c.Done():
            tr.CancelRequest(req)
            <-data
            fmt.Println("The request was cancelled!")
            return c.Err()
        case ok := <-data:
            err := ok.err
            resp := ok.r
            if err != nil {
                fmt.Println("Error select:", err)
                return err
            }
            defer resp.Body.Close()

            realHTTPData, err := ioutil.ReadAll(resp.Body)
            if err != nil {
                fmt.Println("Error select:", err)
                return err
            }
            fmt.Printf("Server Response: %s\n", realHTTPData)
    }
    return nil
}

useContext.go 的其余代码实现了 main() 函数,如下:

func main() {
    if len(os.Args) == 1 {
        fmt.Println("Need a URL and a delay!")
        return
    }

    myUrl = os.Args[1]
    if len(os.Args) == 3 {
        t. err := strconv.Atoi(os.Args[2])
        if err != nil {
            fmt.Println(err)
            return
        }
        delay = t
    }

    fmt.Println("Delay:", delay)
    c := context.Background()
    c, cancel := context.WithTimeout(c, time.Duration(delay)*time.Second)
    defer cancel()

    fmt.Printf("Connecting to %s \n", myUrl)
    w.Add(1)
    go connect(c)
    w.Wait()
    fmt.Println("Exiting...")
}

context.WithTimeout() 方法定义了超时期限。connect() 函数作为一个 goroutine 执行,将会正常终止或 cancel() 函数执行时终止。

尽管没必要知道服务端的操作,但看一下 Go 版本的随机减速的 web 服务器也是好的;一个随机数生成器决定您的 web 服务器有多慢。slowWWW.go 的源码内容如下:

package main

import (
    "fmt"
    "math/rand"
    "net/http"
    "os"
    "time"
)

func random(min, max int) int {
    return rand.Intn(max-min) + min
}

func myHandler(w http.ResponseWriter, r *http.Request) {
    delay := random(0, 15)
    time.Sleep(time.Duration(delay) * time.Second)

    fmt.Fprintf(w, "Servring: %s\n", r.URL.Path)
    fmt.Fprintf(w, "Dealy: %d\n", delay)
    fmt.Printf("Served: %s\n", r.Host)
}

func main() {
    seed := time.Now().Unix()
    rand.Seed(seed)

    PORT := ":8001"
    arguments := os.Args
    if len(arguments) == 1 {
        fmt.Println("Using default port nubmer: ", PORT)
    } else {
        PORT = ":" + arguments[1]
    }

    http.HandleFunc("/", myHandler)
    err := http.ListenAndServer(PORT, nil)
    if err != nil {
        fmt.Println(err)
        os.Exit(10)
    }
}

如您所见,您不需要在 slowWWW.go 文件中使用 context 包,因为那是 web 客户端的工作,它会决定需要多长时间等待响应。

myHandler() 函数的代码负责 web 服务器的减速处理。如介绍的那样,延迟可以由 random(0,15) 函数在 0 到 14 秒随机产生。

如果您使用如 wget(1) 的工具访问 slowWWW.go 服务器的话,会收到如下输出:

$wget -qO- http://localhost:8001/
Serving: /
Delay: 4
$wget -qO- http://localhost:8001/
Serving: /
Delay: 13

这是因为 wget(1) 的默认超时时间比较大。

slowWWW.go 已经在另一个 Unix shell 里运行后,用方便的 time(1) 工具处理执行 useContext.go 的输出如下:

这个输出显示只有第三个命令确实从 HTTP 服务器获得了响应——第一和第二个命令都超时了!

Last updated