• golang Context应用举例


     

    Context本质

    golang标准库里Context实际上是一个接口(即一种编程规范、 一种约定)。

    复制代码
    type Context interface {
          Deadline() (deadline time.Time, ok bool)
          Done() <-chan struct{}
          Err() error
          Value(key any) any
    }
    复制代码

     

    通过查看源码里的注释,我们得到如下约定:

    1. Done()函数返回一个只读管道,且管道里不存放任何元素(struct{}),所以用这个管道就是为了实现阻塞
    2. Deadline()用来记录到期时间,以及是否到期。
    3. Err()用来记录Done()管道关闭的原因,比如可能是因为超时,也可能是因为被强行Cancel了。
    4. Value()用来返回key对应的value,你可以想像成Context内部维护了一个map。

    Context实现

    go源码里提供了Context接口的一个具体实现,遗憾的是它只是一个空的Context,什么也没做。

    复制代码
    type emptyCtx int
    
    func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
        return
    }
    
    func (*emptyCtx) Done() <-chan struct{} {
        return nil
    }
    
    func (*emptyCtx) Err() error {
        return nil
    }
    
    func (*emptyCtx) Value(key any) any {
        return nil
    }
    复制代码

     

    emptyCtx以小写开头,包外不可见,所以golang又提供了Background和TODO这2个函数让我们能获取到emptyCtx。

    复制代码
    var (
            background = new(emptyCtx)
            todo       = new(emptyCtx)
    )
    func Background() Context {
            return background
    }
    func TODO() Context {
            return todo
    }
    复制代码

     

    backgroud和todo明明是一模一样的东西,就是emptyCtx,为什么要搞2个呢?真心求教,知道的同学请在评论区告诉我。

    emptyCtx有什么用?创建Context时通常需要传递一个父Context,emptyCtx用来充当最初的那个Root Context。

    With Value

    当业务逻辑比较复杂,函数调用链很长时,参数传递会很复杂,如下图:

    f1产生的参数b要传给f2,虽然f2并不需要参数b,但f3需要,所以b还是得往后传。

    如果把每一步产生的新变量都放到Context这个大容器里,函数之间只传递Context,需要什么变量时直接从Context里取,如下图:

     

    f2能从context里取到a和b,f4能从context里取到a、b、c、d。

    复制代码
    package main
    
    import (
        "context"
        "fmt"
    )
    
    func step1(ctx context.Context) context.Context {
        //根据父context创建子context,创建context时允许设置一个对,key和value可以是任意数据类型
        child := context.WithValue(ctx, "name", "大脸猫")
        return child
    }
    
    func step2(ctx context.Context) context.Context {
        fmt.Printf("name %s\n", ctx.Value("name"))
        //子context继承了父context里的所有key value
        child := context.WithValue(ctx, "age", 18)
        return child
    }
    
    func step3(ctx context.Context) {
        fmt.Printf("name %s\n", ctx.Value("name")) //取出key对应的value
        fmt.Printf("age %d\n", ctx.Value("age"))
    }
    
    func main1() {
        grandpa := context.Background() //空context
        father := step1(grandpa)        //father里有一对
        grandson := step2(father)       //grandson里有两对
        step3(grandson)
    }
    复制代码

     

    Timeout

    在视频 https://www.bilibili.com/video/BV1C14y127sv/ 里介绍了超时实现的核心原理,视频中演示的done管道可以用Context的Done()来替代,Context的Done()管道什么时候会被关系呢?2种情况:

    1. 通过context.WithCancel创建一个context,调用cancel()时会关闭context.Done()管道。

    复制代码
    func f1() {
        ctx, cancel := context.WithCancel(context.Background())
        go func() {
            time.Sleep(100 * time.Millisecond)
            cancel() //调用cancel,触发Done
        }()
        select {
        case <-time.After(300 * time.Millisecond):
            fmt.Println("未超时")
        case <-ctx.Done(): //ctx.Done()是一个管道,调用了cancel()都会关闭这个管道,然后读操作就会立即返回
            err := ctx.Err()        //如果发生Done(管道被关闭),Err返回Done的原因,可能是被Cancel了,也可能是超时了
            fmt.Println("超时:", err) //context canceled
        }
    }
    复制代码

     

    2. 通过context.WithTimeout创建一个context,当超过指定的时间或者调用cancel()时会关闭context.Done()管道。

    复制代码
    func f2() {
        ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100) //超时后会自动调用context的Deadline,Deadline会,触发Done
        defer cancel()
        select {
        case <-time.After(300 * time.Millisecond):
            fmt.Println("未超时")
        case <-ctx.Done(): //ctx.Done()是一个管道,context超时或者调用了cancel()都会关闭这个管道,然后读操作就会立即返回
            err := ctx.Err()        //如果发生Done(管道被关闭),Err返回Done的原因,可能是被Cancel了,也可能是超时了
            fmt.Println("超时:", err) //context deadline exceeded
        }
    }
    复制代码

     

    Timeout的继承问题

    通过context.WithTimeout创建的Context,其寿命不会超过父Context的寿命。比如:

    1. 父Context设置了10号到期,5号诞生了子Context,子Context设置了100天后到期,则实际上10号的时候子Context也会到期。
    2. 父Context设置了10号到期,5号诞生了子Context,子Context设置了1天后到期,则实际上6号的时候子Context就会到期。
    复制代码
    func inherit_timeout() {
        parent, cancel1 := context.WithTimeout(context.Background(), time.Millisecond*1000) //parent设置100ms超时
        t0 := time.Now()
        defer cancel1()
    
        time.Sleep(500 * time.Millisecond) //消耗掉500ms
    
        // child, cancel2 := context.WithTimeout(parent, time.Millisecond*1000) //parent还剩500ms,child设置了1000ms之后到期,child.Done()管道的关闭时刻以较早的为准,即500ms后到期
        child, cancel2 := context.WithTimeout(parent, time.Millisecond*100) //parent还剩500ms,child设置了100ms之后到期,child.Done()管道的关闭时刻以较早的为准,即100ms后到期
        t1 := time.Now()
        defer cancel2()
    
        select {
        case <-child.Done():
            t2 := time.Now()
            fmt.Println(t2.Sub(t0).Milliseconds(), t2.Sub(t1).Milliseconds())
            fmt.Println(child.Err()) //context deadline exceeded
        }
    }
    复制代码

     

    context超时在http请求中的实际应用

    定心丸来了,最后说一遍:”context在实践中真的很有用“

    客户端发起http请求时设置了一个2秒的超时时间:

    复制代码
    package main
    import (
        "fmt"
        "io/ioutil"
        "net/http"
        "time"
    )
    
    func main() {
        client := http.Client{
            Timeout: 2 * time.Second, //小于10秒,导致请求超时,会触发Server端的http.Request.Context的Done
        }
        if resp, err := client.Get("http://127.0.0.1:5678/"); err == nil {
            defer resp.Body.Close()
            fmt.Println(resp.StatusCode)
            if bs, err := ioutil.ReadAll(resp.Body); err == nil {
                fmt.Println(string(bs))
            }
        } else {
            fmt.Println(err) //Get "http://127.0.0.1:5678/": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
        }
    }
    复制代码

     

    服务端从Request里取提context,故意休息10秒钟,同时监听context.Done()管道有没有关闭。由于Request的context是2秒超时,所以服务端还没休息够context.Done()管道就关闭了。

    复制代码
    package main
    import (
        "fmt"
        "net/http"
        "time"
    )
    
    func welcome(w http.ResponseWriter, req *http.Request) {
        ctx := req.Context() //取得request的context
        select {
        case <-time.After(10 * time.Second): //故意慢一点,10秒后才返回结果
            fmt.Fprintf(w, "welcome")
        case <-ctx.Done(): //超时后client会撤销请求,触发ctx.cancel(),从而关闭Done()管道
            err := ctx.Err()            //如果发生Done(管道被关闭),Err返回Done的原因,可能是被Cancel了,也可能是超时了
            fmt.Println("server:", err) //context canceled
        }
    }
    
    func main() {
        http.HandleFunc("/", welcome)
        http.ListenAndServe(":5678", nil)
    }
    复制代码

     

     

  • 相关阅读:
    Azure DevOps (三) 实现和Jenkins的联动
    为了元宇宙,Facebook下周要改名了?
    金仓数据库 KingbaseGIS 使用手册(6.16. 聚类函数)
    Java 顺序控制、分支控制、循环控制详解
    数据结构 顺序表 ——— 链表
    PyScript运行Python第三方库
    面试题-React(十三):React中获取Refs的几种方式
    香港Web3媒体:Techub News
    【云原生Kubernetes系列第二篇】Kubernetes(k8s)核心组件(对的人兜兜转转最后还是会遇见)
    实现线程池
  • 原文地址:https://www.cnblogs.com/zhangchaoyang/p/17731952.html