go context解析

zooeymoon · · 919 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

上下文context

描述

上下文context是1.7引进到标准库的包。官方的解释为:

Package context defines the Context type, which carries deadlines, cancellation signals, and other request-scoped values across API boundaries and between processes.

大致的意思是:

context包定义了Context类型,这个类型含有截止时间,取消信号和在进程中和跨api边界的其他跨域请求

context.Context类型接口需要实现4个方法:

  1. Deadline — 返回 context.Context 被取消的时间,也就是完成工作的截止日期;
  2. Done — 返回一个 Channel,这个 Channel 会在当前工作完成或者上下文被取消之后关闭,多次调用 Done 方法会返回同一个 Channel;
  3. Err — 返回 context.Context 结束的原因,它只会在 Done 返回的 Channel 被关闭时才会返回非空的值;

如果 context.Context 被取消,会返回 Canceled 错误;
如果 context.Context 超时,会返回 DeadlineExceeded 错误;

  1. Value — 从 context.Context 中获取键对应的值,对于同一个上下文来说,多次调用 Value 并传入相同的 Key 会返回相同的结果,该方法可以用来传递请求特定的数据;
type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}

context的设计的初衷是为了通过同步信号实现对goroutines的树结构进行信号同步,这样可以最大程度的减少对计算资源的浪费。

Go 服务的每一个请求的都是通过单独的 Goroutine 处理的,HTTP/RPC 请求的处理器会启动新的 Goroutine 访问数据库和其他服务。

代码讲解

代码入门

这里我们先走一个简单的事例:

首先我们创建一个方法sleepAndTalk,这个方法会在5秒钟后输出字符串,或者如果context关闭,则打印错误信息
func sleepAndTalk(ctx context.Context, duration time.Duration, s string) {

    select {
    case <-ctx.Done():
        fmt.Println("handle", ctx.Err())

    case <-time.After(duration):
        fmt.Println(s)
    }
}
接下来我们在main方法里面调用这个方法,创建一个context.Background同时使用context.WithCancel(ctx)方法
,同时开启一个协程用来扫描输入信号,如果在5秒中内有键盘输入,则出发cancel方法
func main() {

    ctx := context.Background()

    ctx , cancel := context.WithCancel(ctx)


    go func() {
        s := bufio.NewScanner(os.Stdin)
        s.Scan()
        cancel()
    }()

    sleepAndTalk(ctx, time.Second*5, "hello")

}
貌似这里我们感觉不太爽,我们应该修改为1秒钟之后让其触发cancel方法,修改代码为:
func main() {

    ctx := context.Background()

    ctx, cancel := context.WithTimeout(ctx, time.Second)
    defer cancel()

    sleepAndTalk(ctx, time.Second*5, "hello")

}

我们这一次把代码修改为在main中等待一秒中之后触发cancel方法。

http中使用context

首先我们创建两个文件夹:一个server和一个client,分别处理http的服务端和客户端的请求

在server文件夹下创建一个server.go,这里我们模拟一下请求很慢的操作,让其等待5秒钟后返回值

package main

import (
    "fmt"
    "log"
    "net/http"
    "time"
)

func main() {
    http.HandleFunc("/", handler)
    log.Fatal(http.ListenAndServe("127.0.0.1:8080", nil))
}

func handler(w http.ResponseWriter, r *http.Request) {

    log.Println("handler started")
    defer log.Println("handler stopped")

    time.Sleep(time.Second * 5)
    fmt.Fprintln(w , "hello world")
}

在client文件夹下我们创建一个client.go文件来处理服务端返回的请求

package main

import (
    "io"
    "log"
    "net/http"
    "os"
)

func main() {

    r ,err := http.Get("http://localhost:8080")
    if err != nil {
        log.Fatal(err.Error())
    }
    defer r.Body.Close()
    if r.StatusCode != http.StatusOK {
        log.Fatal(r.Status)
    }

    io.Copy(os.Stdout , r.Body)
}

当我们运行代码时,出现的效果为5秒钟后返回hello world

➜  server git:(master) ✗ go run server.go
2020/04/07 00:30:30 handler started
2020/04/07 00:30:35 handler stopped
➜  ~ curl 127.0.0.1:8080
hello world

现在我们需要把context加入到服务端中,在server.go文件中context隐藏在r *http.Request中,我们可以调用r.Context()返回context。之后使用select语句处理请求

修改的server.go文件:

package main

import (
    "fmt"
    "log"
    "net/http"
    "time"
)

func main() {
    http.HandleFunc("/", handler)
    log.Fatal(http.ListenAndServe("127.0.0.1:8080", nil))
}

func handler(w http.ResponseWriter, r *http.Request) {

    ctx := r.Context()

    log.Println("handler started")
    defer log.Println("handler stopped")
    select {
    case <-time.After(5 *time.Second):
        fmt.Fprintln(w , "hello world")
    case <-ctx.Done():
        err := ctx.Err()
        log.Print(err)
        http.Error(w , err.Error(),http.StatusInternalServerError)
    }

}

当我们运行这段代码时,不需要等待5秒钟强行把请求关闭,则在终端会输出取消

➜  server git:(master) ✗ go run server.go
2020/04/07 00:58:30 handler started
2020/04/07 00:58:31 context canceled
2020/04/07 00:58:31 handler stopped

那么我们如何在client中使用context呢?

这里我们需要创建一个新的http.NewRequest()请求,之后创建一个context,把context包含到请求中

package main

import (
    "context"
    "io"
    "log"
    "net/http"
    "os"
)

func main() {

    ctx := context.Background()
    req , err := http.NewRequest(http.MethodGet , "http://localhost:8080" , nil)
    if err != nil {
        log.Fatal(err)
    }

    req = req.WithContext(ctx)

    res ,err := http.DefaultClient.Do(req)
    if err != nil {
        log.Fatal(err.Error())
    }
    defer res.Body.Close()
    if res.StatusCode != http.StatusOK {
        log.Fatal(res.Status)
    }

    io.Copy(os.Stdout , res.Body)
}

这时候我们在运行代码,在一秒钟之后强制退出

终端会打印:

➜  server git:(master) ✗ go run server.go
2020/04/07 01:09:53 handler started
2020/04/07 01:09:58 handler stopped
2020/04/07 01:10:10 handler started
2020/04/07 01:10:10 context canceled
2020/04/07 01:10:10 handler stopped

可以在真是的场景里,可能用户会因为等待时间过长,而取消请求,这时候我们需要在client.go使用withTimeout方法来模拟服务器处理时间太长导致用户失去耐心,取消请求

package main

import (
    "context"
    "io"
    "log"
    "net/http"
    "os"
    "time"
)

func main() {

    ctx := context.Background()

    ctx, cancel := context.WithTimeout(ctx, time.Second)
    defer cancel()
    req, err := http.NewRequest(http.MethodGet, "http://localhost:8080", nil)
    if err != nil {
        log.Fatal(err)
    }

    req = req.WithContext(ctx)

    res, err := http.DefaultClient.Do(req)
    if err != nil {
        log.Fatal(err.Error())
    }
    defer res.Body.Close()
    if res.StatusCode != http.StatusOK {
        log.Fatal(res.Status)
    }

    io.Copy(os.Stdout, res.Body)
}

一秒钟之后,客户端终端会打印context取消的错误信息

2020/04/07 01:15:30 Get "http://localhost:8080": context deadline exceeded

context传递value

理论上不推荐在context中传递value,因为可能会导致系统不可控。在真正使用传值的功能时我们也应该非常谨慎,使用 context.Context进行传递参数请求的所有参数一种非常差的设计,比较常见的使用场景是传递请求对应用户的认证令牌以及用于进行分布式追踪的请求 ID。

下面我们创建一个log文件夹,创建一个log.go文件,这个文件的主要作用是打印日志,把context引用到print方法中。同时创建一个Decorate方法,重写handlerfunc方法,在请求的request中添加context.withValue产生一个随机数,让Println方法中打印这个随机数

log.go代码:

package log

import (
    "context"
    "log"
    "math/rand"
    "net/http"
)

const RequestIDKey = 42

func Println(ctx context.Context , msg string) {

    id, ok := ctx.Value(RequestIDKey).(int64)
    if !ok {
        log.Println("could not find request ID in context")
    }

    log.Printf("[%d] %s" , id , msg)
}

func Decorate(f http.HandlerFunc) http.HandlerFunc {

    return func(w http.ResponseWriter, r *http.Request) {
        ctx := r.Context()
        id := rand.Int63()
        ctx = context.WithValue(ctx , RequestIDKey , id)
        f(w , r.WithContext(ctx))
    }
}

下面修改server文件夹下的main.go文件,修改handlefunc。

package main

import (
    "fmt"
    "github.com/my/repo/concurrency/context/log"
    "net/http"
    "time"
)

func main() {
    http.HandleFunc("/", log.Decorate(handler))
    panic(http.ListenAndServe("127.0.0.1:8080", nil))
}

func handler(w http.ResponseWriter, r *http.Request) {

    ctx := r.Context()

    log.Println(ctx, "handler started")
    defer log.Println(ctx, "handler stopped")
    select {
    case <-time.After(5 * time.Second):
        fmt.Fprintln(w, "hello world")
    case <-ctx.Done():
        err := ctx.Err()
        log.Println(ctx, err.Error())
        http.Error(w, err.Error(), http.StatusInternalServerError)
    }

}

重新启动服务端,会发现后台输出随机数字和传递的字符串

➜  server git:(master) ✗ go run server.go
2020/04/07 22:32:22 [5577006791947779410] handler started
2020/04/07 22:32:23 [5577006791947779410] context canceled
2020/04/07 22:32:23 [5577006791947779410] handler stopped

我们现在需要考虑一下这个问题,如果RequestIDKey值被重新设置了,这样的话我们可能不会生成一个随机数

package main

import (
    "context"
    "fmt"
    "github.com/my/repo/concurrency/context/log"
    "net/http"
    "time"
)

func main() {
    http.HandleFunc("/", log.Decorate(handler))
    panic(http.ListenAndServe("127.0.0.1:8080", nil))
}

func handler(w http.ResponseWriter, r *http.Request) {

    ctx := r.Context()
    ctx = context.WithValue(ctx , int(42) , int64(100))
    log.Println(ctx, "handler started")
    defer log.Println(ctx, "handler stopped")
    select {
    case <-time.After(5 * time.Second):
        fmt.Fprintln(w, "hello world")
    case <-ctx.Done():
        err := ctx.Err()
        log.Println(ctx, err.Error())
        http.Error(w, err.Error(), http.StatusInternalServerError)
    }

}
这里我们设置了
ctx = context.WithValue(ctx , int(42) , int64(100))

这时输出的结果为

➜  server git:(master) ✗ go run server.go
2020/04/07 23:18:20 [100] handler started
2020/04/07 23:18:21 [100] context canceled
2020/04/07 23:18:21 [100] handler stopped

我们如果防止RequestIDKey 被篡改呢?其实我们可以对这个常量设置类型,让其阻断修改

package log

import (
    "context"
    "log"
    "math/rand"
    "net/http"
)

type key int

const RequestIDKey = key(42)

func Println(ctx context.Context , msg string) {

    id, ok := ctx.Value(RequestIDKey).(int64)
    if !ok {
        log.Println("could not find request ID in context")
    }

    log.Printf("[%d] %s" , id , msg)
}

func Decorate(f http.HandlerFunc) http.HandlerFunc {

    return func(w http.ResponseWriter, r *http.Request) {
        ctx := r.Context()
        id := rand.Int63()
        ctx = context.WithValue(ctx , RequestIDKey , id)
        f(w , r.WithContext(ctx))
    }
}

这里我们设置了一个类型key为int类型,这样这个key类型只能在log.go文件里面使用,这样几可以阻止外部对其的修改
这回我们的输出结果为:

➜  server git:(master) ✗ go run server.go
2020/04/07 23:52:45 [5577006791947779410] handler started
2020/04/07 23:52:46 [5577006791947779410] context canceled
2020/04/07 23:52:46 [5577006791947779410] handler stopped

参考文献:

  1. Go 语言并发编程与 Context
  2. justforfunc #9: The Context Package

有疑问加站长微信联系(非本文作者)

本文来自:Segmentfault

感谢作者:zooeymoon

查看原文:go context解析

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

919 次点击  ∙  1 赞  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传