目 录CONTENT

文章目录
Go

Golang标准库Context! [还缺一个context例子]

Hello!你好!我是村望~!
2023-02-21 / 0 评论 / 0 点赞 / 156 阅读 / 2,814 字
温馨提示:
我不想探寻任何东西的意义,我只享受当下思考的快乐~

参考链接

情节需求

img

有一个获取订单详情的请求,会单独起一个 goroutine 去处理该请求。在该请求内部又有三个分支 goroutine 分别处理订单详情、推荐商品、物流信息;每个分支可能又需要单独调用DB、Redis等存储组件。那么面对这个场景我们需要哪些额外的事情呢?

  1. 三个分支 goroutine 可能是对应的三个不同服务,我们想要携带一些基础信息过去,比如:LogID、UserID、IP等;
  2. 每个分支我们需要设置过期时间,如果某个超时不影响整个流程;
  3. 如果主 goroutine 发生错误,取消了请求,对应的三个分支应该也都取消,避免资源浪费;

简单归纳就是传值、同步信号(取消、超时)。

channel方式

其实可以用 channel 来搞啊!那么我们看看 channel 是否可以满足。

想一个问题,如果是微服务架构,channel 怎么实现跨进程的边界呢?另外一个问题,就算不跨进程,如果嵌套很多个分支,想一想这个消息传递的复杂度。

比如下面这个例子,大致流程:

  • 首先一个 go 程去执行 getOrder 查询订单的信息
  • 然后这个go程 getOrder 的内部分别又开启两个 go 程 getOrderDetails & getTransportationInfo 去分别查询物流和订单详情信息
  • 内部所有go程之间的通信都需要channel去维护
  • 当物流和订单详情分别查询完了,然后向上逐层通知
  • 最后程序结束!
package main

import (
	"fmt"
	"sync"

	"time"
)

var wg sync.WaitGroup

// 获取订单信息!
func getOrder(exitChan chan struct{}) {
	fmt.Println("开始查询订单")
	var exitChan2order = make(chan struct{})
	var exitChan2Trans = make(chan struct{})
	defer close(exitChan2order)
	defer close(exitChan2Trans)
	go getOrderDetails(exitChan2order)
	go getTransportationInfo(exitChan2Trans)
	<-exitChan2order
	<-exitChan2Trans
	exitChan <- struct{}{}
	fmt.Println("查询订单结束")
	wg.Done()
}

// getOrderDetails 获取订单详细信息!
func getOrderDetails(exitChan chan<- struct{}) {
	fmt.Println("==	查询订单详细信息")
	time.Sleep(time.Second * 1)
	fmt.Println("==	查询订单详细信息结束")
	exitChan <- struct{}{}
	wg.Done()
}

// getTransportationInfo 获取物流详细信息
func getTransportationInfo(exitChan chan<- struct{}) {
	fmt.Println("==	查询物流信息")
	time.Sleep(time.Second * 1)
	fmt.Println("==	查询物流信息结束")
	exitChan <- struct{}{}
	wg.Done()
}
func main() {
	var exitChan = make(chan struct{})
	wg.Add(3)
	go getOrder(exitChan)
	<-exitChan
	close(exitChan)
	wg.Wait()
	fmt.Println("over")
}

执行!

Feb-21-2023 12-52-27

可以看到上面单纯是通知流程顺利完成的情况下(不包括异常处理,数据传递,取消请求)等这些操作!

使用CHANEL去做就已经挺复杂的了!这就是树形多嵌套 goroutine 通信太复杂的问题!

( 一个 goroutine 里面又开启新的多个 goroutine,像树枝分叉一样!)

(这是以我目前水平举出的例子了!可能会有更好的处理方式哈哈哈!)

context

上面的处理,如果使用context就会很简单了!先学习一下context!

context.Context 是 Go 语言在 1.7 版本中引入标准库的接口1,该接口定义了四个需要实现的方法,其中包括:

  • Deadline — 返回 context.Context 被取消的时间,也就是完成工作的截止日期;

  • Done — 返回一个 Channel,这个 Channel 会在当前工作完成或者上下文被取消后关闭,多次调用 Done 方法会返回同一个 Channel;

  • Err返回context.Context结束的原因,它只会在Done方法对应的 Channel 关闭时返回非空的值;

  • Value — 从 context.Context 中获取键对应的值,对于同一个上下文来说,多次调用 Value 并传入相同的 Key 会返回相同的结果,该方法可以用来传递请求特定的数据;

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

在 Goroutine 构成的树形结构中对信号进行同步以减少计算资源的浪费是 context.Context 的最大作用。

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

我们可能会创建多个 Goroutine 来处理一次请求,而 context.Context 的作用是在不同 Goroutine 之间同步请求特定数据、取消信号以及处理请求的截止日期。

golang-context-usage

Context 与 Goroutine 树

每一个 context.Context 都会从最顶层的 Goroutine 一层一层传递到最下层。context.Context 可以在上层 Goroutine 执行出现错误时,将信号及时同步给下层。

golang-without-context

如果不使用 Context 同步信号:当最上层的 Goroutine 因为某些原因执行失败时,下层的 Goroutine 由于没有接收到这个信号所以会继续工作;但是当我们正确地使用 context.Context 时,就可以在下层及时停掉无用的工作以减少额外资源的消耗:

默认上下文

context 包中最常用的方法还是 context.Backgroundcontext.TODO

这两个方法都会返回预先初始化好的私有变量 backgroundtodo,它们会在同一个 Go 程序中被复用:

// Background returns a non-nil, empty Context. It is never canceled, has no
// values, and has no deadline. It is typically used by the main function,
// initialization, and tests, and as the top-level Context for incoming
// requests.
func Background() Context {
	return background
}

// TODO returns a non-nil, empty Context. Code should use context.TODO when
// it's unclear which Context to use or it is not yet available (because the
// surrounding function has not yet been extended to accept a Context
// parameter).
func TODO() Context {
	return todo
}

其实从代码从面上来看这两个没有什么区别,只是在使用和语义上稍有不同:

  • context.Background 是上下文的默认值,所有其他的上下文都应该从它衍生出来;
  • context.TODO 应该仅在不确定应该使用哪种上下文时使用;

这两个私有变量都是通过 new(emptyCtx) 语句初始化的,它们是指向私有结构体 context.emptyCtx 的指针

var (
	background = new(emptyCtx)
	todo       = new(emptyCtx)
)

// An emptyCtx is never canceled, has no values, and has no deadline. It is not
// struct{}, since vars of this type must have distinct addresses.
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
}

从上述代码中,我们不难发现 context.emptyCtx 通过空方法实现了 context.Context 接口中的所有方法,它没有任何功能。

在多数情况下,如果当前函数没有上下文作为入参,我们都会使用 context.Background 作为起始的上下文向下传递。

目前对 context 的理解

Goroutine 就像并行流水线的工人~ context 负责在工人之间传递一些信息~控制一些指令!

Background && TODO 是一个全局的ctx!包含上面功能的最基本的context,对context接口最基本的实现!

通常在 main 函数、初始化和测试代码中创建,作为顶层 Context。

下面提到的一些 延伸的 context 都是基于context基础功能标准的扩展!

WithCancel

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) 

context.WithCancel() 创建可取消的 Context 对象,即可以主动通知子协程退出。

package main

import (
	"context"
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup

func main() {
	wg.Add(1)
	ctx, cancel := context.WithCancel(context.Background())
	go handle(ctx)

	// 3s 后通知 handle 停止执行!
	time.Sleep(time.Second * 2)
	cancel()
	wg.Wait()
}

func handle(ctx context.Context) {
LABEL:
	for {
		select {
		case <-ctx.Done():
			fmt.Println("handle done", ctx.Err())
			wg.Done()
			break LABEL
		default:
		}
	}
}
Feb-21-2023 16-15-34
  • context.Backgroud() 创建根 Context,通常在 main 函数、初始化和测试代码中创建,作为顶层 Context。
  • context.WithCancel(parent) 创建可取消的子 Context,同时返回函数 cancel
  • 在子协程中,使用 select 调用 <-ctx.Done() 判断是否需要退出。
  • 主协程中,调用 cancel() 函数通知子协程退出。cancel调用时 ,Done函数返回一个 输出的空结构体!

同时 cancel 所有 goroutine

package main

import (
	"context"
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup

func main() {
	wg.Add(3)
	ctx, cancel := context.WithCancel(context.Background())
	go handle(ctx, "work1")
	go handle(ctx, "work2")
	go handle(ctx, "work3")

	// 3s 后通知 handle 停止执行!
	time.Sleep(time.Second * 2)
	cancel()
	wg.Wait()
}

func handle(ctx context.Context, workName string) {
LABEL:
	for {
		select {
		case <-ctx.Done():
			fmt.Println("handle done"+workName, ctx.Err())
			break LABEL
		default:
			time.Sleep(time.Second)
			fmt.Println(workName)
		}
	}
	wg.Done()
}

为每个子协程传递相同的上下文 ctx 即可,调用 cancel() 函数后该 Context 控制的所有子协程都会退出。

WithValue

func WithValue(parent Context, key, val interface{}) Context

传入的数据与 Context 对象建立关系:

通俗的说就是,用这个可以根据传入的parent context ,额外放点 key value形式的值进parent context,返回父节点parent的副本,其中与key关联的值为val。

package main

import (
	"context"
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup

func main() {
	wg.Add(1)
	ctx, cancel := context.WithCancel(context.Background())
	ctxV := context.WithValue(ctx, "production_id", 111)
	go handle(ctxV, "work1")

	// 3s 后通知 handle 停止执行!
	time.Sleep(time.Second * 2)
	cancel()
	wg.Wait()
}

func handle(ctx context.Context, workName string) {
LABEL:
	for {
		select {
		case <-ctx.Done():
			fmt.Println("handle done"+workName, ctx.Err())
			break LABEL
		default:
			fmt.Println(fmt.Sprintf("production_id==%v", ctx.Value("production_id")))
			time.Sleep(time.Second)
		}
	}
	wg.Done()
}

Feb-21-2023 16-48-04

WithDeadline

func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)

WithDeadline 指定一个时间!到了指定时间会自动执行超时函数!

还是之前的代码,handle函数不用变,我们不去手动调用 cancel 了,时间节点设为当前时间 + 5s !

func main() {
	wg.Add(1)
	ctx, _ := context.WithDeadline(context.Background(), time.Now().Add(time.Second*5))
	ctxV := context.WithValue(ctx, "production_id", 111)
	go handle(ctxV, "work1")
	// 3s 后通知 handle 停止执行!
	wg.Wait()
}

Feb-21-2023 17-02-50

可以看到5s后自己就帮我们执行了 cancel

image-20230221170500559

WithTimeout

WithTimeout和WithDeadLine差不多,只不过传入的是一个时间段 duration

ctx, _ := context.WithTimeout(context.Background(), time.Second*5)

Feb-21-2023 17-16-08

使用Context的注意事项

  • 推荐以参数的方式显示传递Context
  • 以Context作为参数的函数方法,应该把Context作为第一个参数。
  • 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO()
  • Context的Value相关方法应该传递请求域的必要数据,不应该用于传递可选参数
  • Context是线程安全的,可以放心的在多个goroutine中传递
0

评论区