go语言学习-什么是Context? | 青训营

71 阅读3分钟

看到 context在不少go的框架(go-zero,hertz等等)中频繁出现,在 flutter 也是,一直都对它感到模糊,不知道为什么需要它。

学习视频链接:golang context该怎么玩儿_哔哩哔哩_bilibili

本质

在golang的标准库中以接口的形式出现。

type Context interface {
      Deadline() (deadline time.Time, ok bool)
      Done() <-chan struct{}
      Err() error
      Value(key any) any
} 
  • Deadline() 用来记录到期时间,以及是否到期。
  • Done() 返回一个只读管道,且不存放任何元素,为了阻塞。
  • Err() 记录 Done() 管道关闭的原因。
  • Value(key) 用来返回key对应的value,类似于map。

标准库实现

在标准库中实现了一个emptyCtx,但其函数返回的都是nil ,通过BackgroundTODO这2个函数能在包外获取到emptyCtx

var (
    background = new(emptyCtx)
    todo       = new(emptyCtx)
)
func Background() Context {
    return background
}
func TODO() Context {
    return todo
} 

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

虽然 context.Background() 和 context.TODO() 都是 "emptyCtx",但使用它们的场景和语义是有区别的。context.Background() 通常用于整个应用程序的根节点,而 context.TODO() 则用于在编写代码时临时标记还未确定的 Context 部分,需要在后续进行替换。--来自ChatGPT

作用

可以从本质看到,context 有着存储kv记录超时的结构,那它是为了解决什么问题而存在的呢?

如果我们要传递参数,像上面的调用关系会很麻烦,如果使用 context 来传递参数,要什么参数就从 context 里面取。 十分的方便,我想这也是为什么被称为上下文。

//简单实例:
package main

import (
	"context"
	"fmt"
)

func step1(ctx context.Context) context.Context {
	//根据父context创建子context,创建context时允许设置一个<key,value>对,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里有一对<key,value>
	grandson := step2(father)       //grandson里有两对<key,value>
	step3(grandson)
} 

可能会有疑惑(至少我有),如果只是传递参数那为什么有要关于过期时间方法。

那是因为时间也是上下函数间应该传递的信息,就像我等你,如果时间到了,那我就走了,我不可能在原地等一辈子。在函数调用中,被调用者超时,就会触发 Done() 同时把 Err() 传递报错信息。

//简单实例:
func makeHTTPRequest(ctx context.Context) (string, error) {
    // 假设这里有一些实际的 HTTP 请求代码

    // 模拟一个耗时的操作,持续 5 秒钟
    time.Sleep(5 * time.Second)

    // 假设请求成功返回
    return "Response from server", nil
}


func main() {
    ctx := context.Background()
    // 设置请求的过期时间为 3 秒钟
    ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
    defer cancel()

    response, err := makeHTTPRequest(ctx)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }

    fmt.Println("Response:", response)
}

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

Error: context deadline exceeded

如果我们没有设置截至时间,而是继续等待 5 秒钟,程序会一直阻塞等待响应,从而导致用户体验不佳,而且如果多个请求同时进行,可能会占用过多的资源。

总结

基本上上下文在各种各样的框架和库中经常看到,虽然说它十分方便,但是我觉得一定程度上降低了程序的透明度,参数中出现context感觉就像一个黑盒一样不知道发生了什么,不过呢也降低了开发者的心智负担把当前逻辑无关的上下文给隐藏了。