Go的标准库Context理解

作为一个才入门的菜鸟,还没写过真正的 go 项目,要理解这个 Context 还是有点难,不过还是要尝试一下。在 Go http包的Server中,每一个请求在都有一个对应的 goroutine 去处理。请求处理函数通常会启动额外的 goroutine 用来访问后端服务,比如数据库服务。用来处理一个请求的 goroutine 通常需要访问一些与请求特定的数据,比如终端用户的身份认证信息、验证相关的token、请求的截止时间。 当一个请求被取消或超时时,所有用来处理该请求的 goroutine 都应该迅速退出,然后系统才能释放这些 goroutine 占用的资源。

基本示例

基本示例中,本来想使用等待组来退出 goroutine ,奈何由于无限循环的原因,导致 wg.Done 根本没办法执行,也就导致主线程没版本 over 了。

Go 复制代码
package main

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

var wg sync.WaitGroup

// 初始的例子

func worker() {
	for {
		fmt.Println("worker")
		time.Sleep(time.Second)
	}
	// 如何接收外部命令实现退出
	wg.Done()
}

func main() {
	wg.Add(1)
	go worker()
	// 如何优雅的实现结束子goroutine
	wg.Wait()
	fmt.Println("over")
}

全局变量方式

为了让 goroutine 能正常退出,可以使用全局变量,将退出信号传递到 worker 里边,这样,worker 里的 wg.Done 就可以正常执行到了。但全局变量方式控制也有其缺陷,全局变量方式存在的问题:

  • 使用全局变量在跨包调用时不容易统一。
  • 如果worker中再启动goroutine,就不太好控制了。
Go 复制代码
package main

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

var wg sync.WaitGroup
var exit bool

// 全局变量方式存在的问题:
// 1. 使用全局变量在跨包调用时不容易统一
// 2. 如果worker中再启动goroutine,就不太好控制了。

func worker() {
	for {
		fmt.Println("worker")
		time.Sleep(time.Second)
		if exit {
			break
		}
	}
	wg.Done()
}

func main() {
	wg.Add(1)
	go worker()
	time.Sleep(3 * time.Second)
	exit = true
	wg.Wait()
	fmt.Println("over")
}
Go 复制代码
worker
worker
worker
over

通道方式

Go 复制代码
package main

import (
	"fmt"
	"sync"

	"time"
)

var wg sync.WaitGroup

// 管道方式存在的问题:
// 1. 使用全局变量在跨包调用时不容易实现规范和统一,需要维护一个共用的channel

func worker(exitChan chan struct{}) {
LOOP:
	for {
		fmt.Println("worker")
		time.Sleep(time.Second)
		select {
		case <-exitChan: // 等待接收上级通知
			break LOOP
		default:
		}
	}
	wg.Done()
}

func main() {
	var exitChan = make(chan struct{})
	wg.Add(1)
	go worker(exitChan)
	time.Sleep(time.Second * 3) // sleep3秒以免程序过快退出
	exitChan <- struct{}{}      // 给子goroutine发送退出信号
	close(exitChan)
	wg.Wait()
	fmt.Println("over")
}
Go 复制代码
worker
worker
worker
over

官方版本

Go 复制代码
package main

import (
	"context"
	"fmt"
	"sync"

	"time"
)

var wg sync.WaitGroup

func worker(ctx context.Context) {
LOOP:
	for {
		fmt.Println("worker")
		time.Sleep(time.Second)
		select {
		case <-ctx.Done(): // 等待上级通知
			break LOOP
		default:
		}
	}
	wg.Done()
}

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	wg.Add(1)
	go worker(ctx)
	time.Sleep(time.Second * 3)
	cancel() // 通知子goroutine结束
	wg.Wait()
	fmt.Println("over")
}
Go 复制代码
worker
worker
worker
over

当子goroutine又开启另外一个goroutine时,只需要将ctx传入即可。

Go 复制代码
package main

import (
	"context"
	"fmt"
	"sync"

	"time"
)

var wg sync.WaitGroup

func worker(ctx context.Context) {
	go worker2(ctx)
LOOP:
	for {
		fmt.Println("worker")
		time.Sleep(time.Second)
		select {
		case <-ctx.Done(): // 等待上级通知
			break LOOP
		default:
		}
	}
	wg.Done()
}

func worker2(ctx context.Context) {
LOOP:
	for {
		fmt.Println("worker2")
		time.Sleep(time.Second)
		select {
		case <-ctx.Done(): // 等待上级通知
			break LOOP
		default:
		}
	}
}
func main() {
	ctx, cancel := context.WithCancel(context.Background())
	wg.Add(1)
	go worker(ctx)
	time.Sleep(time.Second * 3)
	cancel() // 通知子goroutine结束
	wg.Wait()
	fmt.Println("over")
}

With系列函数

WithCancel

Go 复制代码
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

WithCancel返回带有新Done通道的父节点的副本。当调用返回的cancel函数或当关闭父上下文的Done通道时,将关闭返回上下文的Done通道,无论先发生什么情况。取消此上下文将释放与其关联的资源,因此代码应该在此上下文中运行的操作完成后立即调用cancel。

Go 复制代码
package main

import (
	"context"
	"fmt"
)

func gen(ctx context.Context) <-chan int {
	dst := make(chan int)
	n := 1
	go func() {
		for {
			select {
			case <-ctx.Done():
				return // return结束该goroutine,防止泄露
			case dst <- n:
				n++
			}
		}
	}()
	return dst
}

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel() // 当我们取完需要的整数后调用cancel

	for n := range gen(ctx) {
		fmt.Println(n)
		if n == 5 {
			break
		}
	}
}

gen函数在单独的goroutine中生成整数并将它们发送到返回的通道。 gen的调用者在使用生成的整数之后需要取消上下文,以免gen启动的内部goroutine发生泄漏。

WithDeadline

Go 复制代码
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)

返回父上下文的副本,并将deadline调整为不迟于d(相当于截止时间到达时,会主动关闭 Done 通道)。如果父上下文的deadline已经早于d,则WithDeadline(parent, d)在语义上等同于父上下文。当截止日过期时,当调用返回的cancel函数时,或者当父上下文的Done通道关闭时,返回上下文的Done通道将被关闭,以最先发生的情况为准。取消此上下文将释放与其关联的资源,因此代码应该在此上下文中运行的操作完成后立即调用cancel。

Go 复制代码
package main

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

func main() {
	d := time.Now().Add(50 * time.Millisecond)
	ctx, cancel := context.WithDeadline(context.Background(), d)

	// 尽管ctx会过期,但在任何情况下调用它的cancel函数都是很好的实践。
	// 如果不这样做,可能会使上下文及其父类存活的时间超过必要的时间。
	defer cancel()

	select {
	case <-time.After(1 * time.Second):
		fmt.Println("overslept")
	case <-ctx.Done():
		fmt.Println(ctx.Err())
	}
}

上面的代码中,定义了一个50毫秒之后过期的deadline,然后我们调用context.WithDeadline(context.Background(), d)得到一个上下文(ctx)和一个取消函数(cancel),然后使用一个select让主程序陷入等待:等待1秒后打印overslept退出或者等待ctx过期后退出。在上面的示例代码中,因为ctx 50毫秒后就会过期,所以ctx.Done()会先接收到context到期通知,并且会打印ctx.Err()的内容。

WithTimeout

Go 复制代码
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

WithTimeout 返回 WithDeadline(parent, time.Now().Add(timeout))。相当于通过当前时间和超时时间来定义了 deadline 。取消此上下文将释放与其相关的资源,因此代码应该在此上下文中运行的操作完成后立即调用cancel,通常用于数据库或者网络连接的超时控制

Go 复制代码
package main

import (
	"context"
	"fmt"
	"sync"

	"time"
)

// context.WithTimeout

var wg sync.WaitGroup

func worker(ctx context.Context) {
LOOP:
	for {
		fmt.Println("db connecting ...")
		time.Sleep(time.Millisecond * 10) // 假设正常连接数据库耗时10毫秒
		select {
		case <-ctx.Done(): // 50毫秒后自动调用
			break LOOP
		default:
		}
	}
	fmt.Println("worker done!")
	wg.Done()
}

func main() {
	// 设置一个50毫秒的超时
	ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
	wg.Add(1)
	go worker(ctx)
	time.Sleep(time.Second * 5)
	cancel() // 通知子goroutine结束
	wg.Wait()
	fmt.Println("over")
}
Go 复制代码
db connecting ...
db connecting ...
db connecting ...
db connecting ...
db connecting ...
worker done!
over

WithValue

WithValue返回父节点的副本,其中与key关联的值为val。仅对API和进程间传递请求域的数据使用上下文值,而不是使用它来传递可选参数给函数。所提供的键必须是可比较的,并且不应该是string类型或任何其他内置类型,以避免使用上下文在包之间发生冲突。WithValue的用户应该为键定义自己的类型。为了避免在分配给interface{}时进行分配,上下文键通常具有具体类型struct{}。或者,导出的上下文关键变量的静态类型应该是指针或接口。

Go 复制代码
package main

import (
	"context"
	"fmt"
	"sync"

	"time"
)

// context.WithValue

type TraceCode string

var wg sync.WaitGroup

func worker(ctx context.Context) {
	key := TraceCode("TRACE_CODE")
	traceCode, ok := ctx.Value(key).(string) // 在子goroutine中获取trace code
	if !ok {
		fmt.Println("invalid trace code")
	}
LOOP:
	for {
		fmt.Printf("worker, trace code:%s\n", traceCode)
		time.Sleep(time.Millisecond * 10) // 假设正常连接数据库耗时10毫秒
		select {
		case <-ctx.Done(): // 50毫秒后自动调用
			break LOOP
		default:
		}
	}
	fmt.Println("worker done!")
	wg.Done()
}

func main() {
	// 设置一个50毫秒的超时
	ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
	// 在系统的入口中设置trace code传递给后续启动的goroutine实现日志数据聚合
	ctx = context.WithValue(ctx, TraceCode("TRACE_CODE"), "12345")
	wg.Add(1)
	go worker(ctx)
	time.Sleep(time.Second * 5)
	cancel() // 通知子goroutine结束
	wg.Wait()
	fmt.Println("over")
}
Go 复制代码
worker, trace code:12345
worker, trace code:12345
worker, trace code:12345
worker done!
over
相关推荐
{⌐■_■}27 分钟前
【git】工作场景下的 工作区 <-> 暂存区<-> 本地仓库 命令实战 具体案例
大数据·git·elasticsearch·golang·iphone·ip·etcd
我不是程序猿儿30 分钟前
【C】识别一份嵌入式工程文件
c语言·开发语言
软件开发技术局1 小时前
撕碎QT面具(8):对控件采用自动增加函数(转到槽)的方式,发现函数不能被调用的解决方案
开发语言·qt
周杰伦fans3 小时前
C#中修饰符
开发语言·c#
yngsqq3 小时前
c# —— StringBuilder 类
java·开发语言
赔罪3 小时前
Python 高级特性-切片
开发语言·python
Asthenia04123 小时前
浏览器缓存机制深度解析:电商场景下的性能优化实践
后端
子豪-中国机器人4 小时前
2月17日c语言框架
c语言·开发语言
夏天的阳光吖4 小时前
C++蓝桥杯基础篇(四)
开发语言·c++·蓝桥杯
databook4 小时前
『Python底层原理』--Python对象系统探秘
后端·python