说明: 上下文的英文是context, 其英文简写约定俗成是
ctx
上下文处理
上下文在go中有一个约定俗成的写法ctx
, 如果你用过python应该知道self
, 这是python类初始化的一个对象, 在python的类中任何函数都可以去读取和使用它, 并且会把它当作第一个参数传入。而ctx
也有点类似self
, 他也是一个约定俗成的叫法, 并没有强制性, 并且如果使用,一般也是作为函数的第一个变量传入, 一般我们在go的主进程中生成上下文, 并且所有需要的go程都可以获取上下文。
创建上下文
在Go中, 上下文除了传递消息对象外, 还有一个非常重要的作用, 就是用它来传递取消关闭信号。特别是在众多Go程中, 我们可以用上下文来同时控制其的运行状态(开启或结束)。
上下文中, 内置了四个函数, 可以对已有的上下文进行操作
Deadline() (过期时间 time.Time, ok bool)
: 查看是否有过期时间, 如果有就返回过期时间Done() <-chan struct{}
: 用于做退出判断, 如果过期时间或者持续时间结束, 或手动执行CancelFunc
, 则会向此处传入一个空结构体。Err() error
: 用来解释退出的原因。Value(key any) any
: 用来读取上下文中的值。
我们常用创建上下文的方法常用的有五种
context.Background() Context
: 一般我们最底层创建上下文会用到这个, 可以看出获取一个空上下文的方法。它及不包含信息, 也没有传递关闭信号的功能。context.WithCancel(父级上下文 Context) (Context, CancelFunc)
: 这算是比较常规的上下文, 他会返回当前的上下文和一个取消函数。
func 常规上下文() {
上下文, 常规取消 := context.WithCancel(空上下文)
获取过期时间, ok := 上下文.Deadline()
if ok {
fmt.Println("过期时间为:", 获取过期时间)
} else {
fmt.Println("未设置过期时间")
}
for i := 0; i < 2; i++ {
select {
case <-time.After(time.Second):
常规取消()
fmt.Println("执行取消")
case 取消信息 := <-上下文.Done():
fmt.Println("已取消:", 取消信息, 上下文.Err())
}
}
}
func main() {
fmt.Println("开始时间: ", time.Now())
常规上下文()
}
3. context.WithDeadline(父级上下文 Context, 过期时间 time.Time) (Context, CancelFunc)
: 这种上下文可以设置一个过期时间, 在到达过期时间的时候让相关进程结束, 比如我们可以给一个用户的登录信息放在上下文中, 并设置过期时间, 设置过过期时间的上下文可以从上下文.Deadline()
获取到截止日期
func 过期上下文(过期时间 time.Time) {
上下文, 主动取消 := context.WithDeadline(空上下文, 过期时间)
defer 主动取消()
for {
select {
case <-time.After(1 * time.Second):
获取过期时间, ok := 上下文.Deadline()
if ok {
fmt.Println("过期时间为:", 获取过期时间)
}
fmt.Println("正常执行中!")
case <-上下文.Done():
fmt.Println("已取消:", 上下文.Err())
return
}
}
}
func main() {
fmt.Println("开始时间: ", time.Now())
时间 := time.UnixMicro(time.Now().UnixMicro() + (time.Second.Microseconds() * 3))
过期上下文(时间)
}
4. context.WithTimeout(父级上下文 Context, 持续时间 time.Duration) (Context, CancelFunc)
: 这样用法和上述过期时间类似, 只不过把过期时间改为了倒计时的形式, 这种方法通用可以从上下文.Deadline()
获取到截止日期
func 超时上下文(倒计时 time.Duration) {
上下文, 取消 := context.WithTimeout(空上下文, 倒计时)
上下文.Deadline()
defer 取消()
for {
select {
case <-time.After(1 * time.Second):
获取超时时间, ok := 上下文.Deadline()
if ok {
fmt.Println("超时时间: ", 获取超时时间)
}
fmt.Println("正常执行中")
case <-上下文.Done():
fmt.Println(上下文.Err())
return
}
}
}
func main() {
fmt.Println("开始时间: ", time.Now())
计时 := 3 * time.Second
超时上下文(计时)
}
5. context.WithValue(父级上下文, 键, 值 any) Context
: 用于上下文传递信息保存键值对, 但要注意的是, 上下文一般传播范围会非常广, 为了尽量节省资源, 务必只传入一些必要的信息, 一些可选信息就没有必要进行传播了。比如用户ID
, 用户key
, 系统os类型
等程序启动后基本不会变更的数据, 运行时参数的各种杂项数据是不推荐放入上下文的。
func 传值上下文(键, 值 any) {
上下文 := context.WithValue(空上下文, 键, 值)
fmt.Println("当前上下文的键为: ", 上下文.Value(键))
}
func main() {
fmt.Println("开始时间: ", time.Now())
传值上下文("我是键", "寻觅")
}
PS: 除此之外, 还有一个context.TODO() Context
这个方法在代码层面和context.Background() Context
实现的东西是一模一样的, 但TODO主要是用来占位的, 可以看作为一个占位符, 而Background则是可以看成一个空的上下文, 用来做我们的创建模板用的。
上下文继承
Go上下文的继承很好理解, 父级会影响子级, 父级一旦关闭, 所有子级也都会随之关闭, 子集的值会覆盖父级, 但子集的过期时间和持续时间是会被父级影响的。
但上下文关闭不会影响使用Value(key any) any
取值, 父级上下文关闭后, 子集上下文还是可以正常取值
func 上下文传递测试() {
上下文1, 取消 := context.WithCancel(空上下文)
defer 取消()
上下文2 := context.WithValue(上下文1, "测试键", "上下文1的键")
上下文3, _ := context.WithTimeout(上下文2, 5*time.Second)
for {
select {
case <-time.After(2 * time.Second):
fmt.Println("上下文1取消前的上下文3: ", 上下文3.Value("测试键"))
取消()
fmt.Println("上下文1 已被取消")
fmt.Println("上下文1取消前的上下文3: ", 上下文3.Value("测试键"))
case <-上下文3.Done():
fmt.Println("上下文1: ", 上下文1.Err(), <-上下文1.Done())
fmt.Println("上下文2: ", 上下文2.Err(), <-上下文2.Done())
fmt.Println("上下文3: ", 上下文3.Err(), <-上下文3.Done())
return
}
}
}
func main() {
fmt.Println("开始时间: ", time.Now())
上下文传递测试()
}
线程安全
这里的线程安全是指, 多个Go程同时操作上下文, 进行读写等操作, 是否会出现异常。首先, Go的上下文并没有提供一个写Set
接口, 而只有一个类似读Value(键)
的接口, 所有数据的写入操作都在上下文定义的那一刻就决定了。
想要变更数据, 就只能通过创建子上下文, 在子上下文中添加新值, 或覆盖老值, 但父上下文中的值是不会改变的。不只是父子上下文, 所有上下文中的数据都是独立的, 互不影响的
func 创建上下文(上下文 context.Context, 值 string) context.Context {
for {
_ = context.WithValue(上下文, "测试键", 值)
}
}
func 上下文线程安全() {
上下文 := context.WithValue(context.Background(), "测试键", "---")
go 创建上下文(上下文, "+++")
go 创建上下文(上下文, "***")
go func() {
for {
fmt.Println(上下文.Value("测试键"))
time.Sleep(time.Second)
}
}()
time.Sleep(time.Second * 10)
}
func main() {
fmt.Println("开始时间: ", time.Now())
上下文线程安全()
}