手记

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步:

使用Add方法设置等待的数量,计数加1

使用Done方法设置等待数量,计数减1

当等待数量等于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 场景比较单一,但是更好用。

©著作权归作者所有:来自51CTO博客作者骑士救兵的原创作品,如需转载,请注明出处,否则将追究法律责任


0人推荐
随时随地看视频
慕课网APP