Golang基础笔记十三之context

本文首发于公众号:Hunter后端

原文链接:Golang基础笔记十三之context

在 Golang 里,context 包提供了很多比如传递截止时间、取消信号、传递数据等操作的标准方式,用于在跨 API 边界、进程和 goroutine之间进行。

这一篇笔记详细介绍一下 context 包相关的一些操作。

以下是本篇笔记目录:

  1. Context 接口及作用
  2. 取消传播
  3. 超时控制
  4. 截止时间
  5. 传递数据

1、Context 接口及作用

1. Context 接口

Context 是 context 包下的一个接口,其定义如下:

go 复制代码
type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key any) any
}
  1. Deadline() 返回上下文被取消的时间
  2. Done() 返回一个通道,当上下文被取消时关闭
  3. Error() 返回上下文被取消的原因
  4. Value() 可以获取相应的键值对数据

下面所有的操作都是基于 context.Context 实现。

2. context 相关函数

我们可以使用 context 创建一些函数,用来实现取消、超时控制、传递数据等操作。

1) context.Background()

根上下文,可以作为所有上下文的起点,当我们需要实现取消、超时控制等功能,都需要定义一个父级上下文,这个时候我们就可以使用 context.Backgroud() 来创建。

2) context.TODO()

当我们不确定使用哪个上下文时,就可以使用 context.TODO(),但是在源代码中,它的实现与 context.Backgroud() 是一样的逻辑。

3) context.WithCancel(parent)

创建可以取消的上下文,参数是父级上下文,当我们调用一个函数,并且希望在某些时候取消这个调用,比如超时,这个时候我们可以使用这个函数,并手动进行取消。

4) context.WithTimeout(parent, timeout)

创建有超时的上下文,参数是父级上下文和 time.Duration,当我们调用一个函数,并且希望在多久以后可以自动取消,可以使用这个函数。

5) context.WithDeadline(parent, d)

创建有截止时间的上下文,参数是父级上下文和 time.Time,当我们调用一个函数,并且希望在某个具体的时间点可以自动取消,可以使用这个函数。

6) context.WithValue(parent, key, value)

创建带有键值对的上下文,我们希望通过上下文传递数据,就可以使用这个函数。

这里介绍了 context 相关的一些函数,接下来我们以具体的代码为示例,分别用这些函数来实现对应的功能。

2、 取消传播

我们使用 context.WithCancel() 函数实现取消传播的功能。

其实就是取消函数执行的操作,为什么会说取消传播,因为上下文可以在函数调用中一层一层传递,当我们取消了根上下文,所有调用链中的上下文都会被取消。

context.WithCancel() 的使用代码示例如下:

go 复制代码
ctx, cancel := context.WithCancel(context.Background())

这个函数返回两个结果,一个是上下文参数,一个是取消函数,我们可以使用取消函数在特定节点取消这个上下文,这里取消操作需要我们手动执行。

下面我们实现一个取消操作,我们调用两个函数,这两个函数随机执行一段时间,然后某个函数先返回结果,返回之后我们立马执行上下文的取消操作,

go 复制代码
package main

import (
    "context"
    "fmt"
    "math/rand"
    "time"
)

func F1(ctx context.Context, ch chan string) {
    sleepSeconds := rand.Intn(3)
    time.Sleep(time.Duration(sleepSeconds) * time.Second)
    ch <- "f1 result"
}

func F2(ctx context.Context, ch chan string) {
    sleepSeconds := rand.Intn(3)
    time.Sleep(time.Duration(sleepSeconds) * time.Second)
    ch <- "f2 result"
}

func CallF1(ctx context.Context, ch chan string) {
    chF1 := make(chan string)
    go F1(ctx, chF1)

    select {
    case result := <-chF1:
        ch <- result
    case <-ctx.Done():
        fmt.Println("F1 函数调用超时")
    }
}

func CallF2(ctx context.Context, ch chan string) {
    chF2 := make(chan string)
    go F2(ctx, chF2)

    select {
    case result := <-chF2:
        ch <- result
    case <-ctx.Done():
        fmt.Println("F2 函数调用超时")
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    ch1 := make(chan string)
    ch2 := make(chan string)
    go CallF1(ctx, ch1)
    go CallF2(ctx, ch2)

    select {
    case r1 := <-ch1:
        fmt.Println("f1 调用完成: ", r1)
        cancel()
    case r2 := <-ch2:
        fmt.Println("f2 调用完成: ", r2)
        cancel()
    }
}

在这里,我们目标调用函数为 F1()F2(),使用 CallF1()CallF2() 作为中间调用函数,在其中使用 select-case 等待目标函数返回结果,并同时监听 ctx.Done() 判断 ctx 是否已经取消。

同时在 main() 函数中监听 ch1 和 ch2 判断哪个通道先返回结果,监听到返回结果马上取消 ctx 上下文,main() 函数就可以接着往下执行,避免两个目标函数其中一个长时间执行。

这里需要注意的是,中间函数 CallF1() 和 CallF2() 虽然被取消了,但 F1()、F2() 作为执行的 goroutine 并没有取消执行,如果有取消的需求,可以在 F() 函数内部伺机监听 ctx.Done() 以提前退出函数。

3、 超时控制

我们使用 context.WithTimeout(parent, timeout) 可以实现超时控制。

下面实现一个功能为,调用某个函数,并给一秒的超时时间,超过时间则进行超时处理,避免长时间堵塞,其中目标执行函数为 TargetFunc,其中,随机 sleep 三秒内的时间。

整体代码如下:

go 复制代码
package main

import (
    "context"
    "fmt"
    "math/rand"
    "time"
)

func TargetFunc() string {
    sleepSeconds := rand.Intn(3)
    time.Sleep(time.Duration(sleepSeconds) * time.Second)
    return "result"
}

func CallFunc(ch chan string) {
    ch <- TargetFunc()
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
    defer cancel()

    ch := make(chan string)
    defer close(ch)

    go CallFunc(ch)

    select {
    case result := <-ch:
        fmt.Println("result: ", result)
    case <-ctx.Done():
        fmt.Println("函数调用超时")
    }
}

其中,TargetFunc 函数内部的逻辑为随机休息三秒内的时间,可能导致超时,也可能不超时。

CallFunc() 函数用作中间函数,用于获取目标函数调用结果并通过 channel 返回。

在 main 函数中,首先定义一个有超时时间的上下文,设置了一秒超时,然后定义一个通道,将其传给 CallFunc() 用于返回数据。

之后通过 select-case 操作,进入循环等待状态,用于判断 goroutine 中的 channel 和 ctx 的超时哪个先返回结果。

如果 TargetFunc 函数调用时间过长,ctx 超时控制的上下文先到期,那么则会打印出 函数调用超时 的信息,否则会打印出函数调用返回的结果。

4、 截止时间

我们可以使用 context.WithDeadline(parent, d) 函数实现一个有截止时间的上下文,它的调用参数第二个为 d,是一个具体的 time.Time 类型。

但其实,在背后的源码中,context.WithTimeout() 函数会将其中的 time.Duration 参数处理通过 time.Now().Add(timeout) 的方式处理成 time.Time 然后再调用 WithDeadline 函数,所以超时控制和截止时间这两个函数在本质上的逻辑是一致的。

所以这里我们的代码示例,也只是把输入的参数修改一下,即可实现功能,如下:

go 复制代码
deadline := time.Now().Add(1 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)

5、 传递数据

我们可以通过 context.WithValue() 函数实现传递数据的功能,这个函数接收三个参数,父级上下文,key 和 value,以下是使用示例:

go 复制代码
ctx := context.Background()

ctxWithValue := context.WithValue(ctx, "str1", "value1")
ctxWithValue = context.WithValue(ctxWithValue, "slice", []int{1, 2, 3})

在获取数据的时候,我们可以使用 ctx.Value() 函数获取:

go 复制代码
func ProcessCtxValue(ctx context.Context) {
    value1 := ctx.Value("str1")
    fmt.Println("str1 value: ", value1)

    value2 := ctx.Value("slice")
    fmt.Println("slice value: ", value2)
}

但是在真正用到这些数据的时候,我们还需要对 value 进行类型判断,可以直接如下操作:

go 复制代码
func ProcessCtxValue(ctx context.Context) {
    value1, ok := ctx.Value("str1").(string)
    if ok {
        fmt.Println("str1 value: ", value1)
    }

    value2 := ctx.Value("slice").([]int)
    if ok {
        fmt.Println("slice value: ", value2, value2[1])
    }
}
相关推荐
XHunter5 天前
Golang基础笔记十二之defer、panic、error
golang基础笔记
XHunter7 天前
Golang基础笔记十一之日期与时间处理
golang基础笔记
XHunter12 天前
Golang基础笔记十之goroutine和channel
golang基础笔记
XHunter14 天前
Golang基础笔记九之方法与接口
golang基础笔记
XHunter1 个月前
Golang基础笔记三之数组和切片
golang基础笔记
XHunter1 个月前
Golang基础笔记二之字符串及其操作
golang基础笔记