跳到主要内容

基础知识

go init()

init()函数会在包被初始化后自动执行,并且在main()函数之前执行,但是需要注意的是init()以及main()函数都是无法被显式调用的。

那么init()是不是最先执行的呢? 答案是否定的,首先,在他之前会进行全局变量的初始化;

信息

当我们导入其他包时,会先初始化导入的包,而初始化包时,会先加载全局变量,而后从上到下加载init()函数,当被导入的包的init()函数执行完毕后,执行调用方的全局变量加载,init()函数的顺序加载,之后执行main()函数。

指针

Go 语言为程序员提供了控制数据结构的指针的能力;但是,你不能进行指针运算。通过给予程序员基本内存布局,Go 语言允许你控制特定集合的数据结构、分配的数量以及内存访问模式,这些对构建运行良好的系统是非常重要的:指针对于性能的影响是不言而喻的,而如果你想要做的是系统编程、操作系统或者网络应用,指针更是不可或缺的一部分。

一个指针变量可以指向任何一个值的内存地址 它指向那个值的内存地址,在 32 位机器上占用 4个字节,在 64 位机器上占用 8 个字节,并且与它所指向的值的大小无关。当然,可以声明指针指向任何类型的值来表明它的原始性或结构性;你可以在指针类型前面加上 号(前缀)来获取指针所指向的内容,这里的 号是一个类型更改器。使用一个指针引用一个值被称为间接引用。

在函数调用时,像切片(slice)、字典(map)、接口(interface)、通道(channel)这样的引用类型都是默认使用引用传递(即使没有显式的指出指针)。

常量

常量使用关键字 const 定义,用于存储不会改变的数据。

在 Go 语言中,你可以省略类型说明符 [type],因为编译器可以根据变量的值来推断其类型。

  • 显式类型定义: const b string = "abc"
  • 隐式类型定义: const b = "abc"

常量的值必须是能够在编译时就能够确定的;你可以在其赋值表达式中涉及计算过程,但是所有用于计算的值必须在编译期间就能获得。

因为在编译期间自定义函数均属于未知,因此无法用于常量的赋值,但内置函数可以使用,如:len() 常量还可以用作枚举:

const (
Unknown = 0
Female = 1
Male = 2
)

关键字

break default func interface select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var

append bool byte cap close complex complex64 complex128 uint16
copy false float32 float64 imag int int8 int16 uint32
int32 int64 iota len make new nil panic uint64
print println real recover string true uint uint8 uintptr

New和Make

new

new 是go 语言内置的函数,他的函数签名如下

fun new(Type) *Type

Goroutine Channel 的使用环境

什么场景使用 channel 合适

  1. 通过全局变量加锁同步来实现通讯,并不利于多个协程对全局变量的读写操作
  2. 加锁虽然可以解决 goroutine 对全局变量的抢占资源问题,但是影响性能
  3. channel 进行协程 goroutine 间的通信

操作系统中 线程和 goroutine 的关系

  1. 操作系统线程对应用户态多个 goroutine
  2. go 程序可以同时使用多个操作系统线程
  3. goroutine 和 OS 线程是多对多的关系 m:n

go 语言的并发模型是 CSP (Communicating Sequential Process),提倡通过通信共享内存 而不是通过共享内存而实现通信,引出了 channel

通道 channel 使用实例

for range 从通道中取值,通道关闭时 for range 退出

// channel练习 go  for range从chan中取值
ch1 := make(chan int)
ch2 := make(chan int)

// 开启goroutine 把0-100写入到ch1通道中
go func() {
for i := 0; i < 100; i++ {
ch1 <- i
}
close(ch1)
}()

// 开启goroutine 从ch1中取值,值的平方赋值给 ch2
go func() {
for {
i,ok := <-ch1 //通道取值后 再取值 ok = false
if ok {
ch2 <- i*i
}else {
break
}
}
close(ch2)
}()

// 主goroutine 从ch2中取值 打印输出
// for x := chan 有值取值,通道关闭时跳出goroutine
for i :=range ch2{
fmt.Println(i)
}

channel 升级,单通道,只读通道和只写通道

func counter(in chan<- int) {
defer close(in)
for i := 0; i < 100; i++ {
in <- i
}
}

func square(in chan<- int, out <-chan int) {
defer close(in)
for i := range out {
in <- i * i
}
}

func output(out <-chan int) {
for i:=range out{
fmt.Println(i)
}
}

// 改写成单向通道
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go counter(ch1)
go square(ch2, ch1)
output(ch2)
}

goroutine work pool,可以防止 goroutine 暴涨或者泄露

//使用work pool 防止goroutine的泄露和暴涨
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
fmt.Printf("worker:%d start job:%d\n", id, j)
time.Sleep(time.Second)
fmt.Printf("worker:%d end job:%d\n", id, j)
results <- j * 2
}
}

func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
// 开启3个goroutine
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// 5个任务
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs)
// 输出结果
for a := 1; a <= 5; a++ {
<-results
}
}

goroutine 使用 select case 多路复用,满足我们同时从多个通道接收值的需求

//使用select语句能提高代码的可读性。
//可处理一个或多个channel的发送/接收操作。
//如果多个case同时满足,select会随机选择一个。
//对于没有case的select{}会一直等待,可用于阻塞main函数。
ch := make(chan int, 1)
go func() {
for i := 0; i < 10; i++ {
select {
case x := <-ch:
fmt.Println(x)
case ch <- i:
}
}
}()

goroutine 加锁 排它锁 读写锁

var x int64
var wg sync.WaitGroup
//添加互斥锁
var lock sync.Mutex

func main() {
wg.Add(2)
go add()
go add()
wg.Wait()
fmt.Println(x)
}

func add() {
for i := 0; i < 5000; i++ {
lock.Lock() //加锁
x = x + 1
lock.Unlock() //解锁
}
wg.Done()
}

GMP 模型

进程

在程序启动时,操作系统会给该程序分配一块内存空间,对于程序但看到的是一整块连续的储存空间,简称虚拟储存空间,落实到操作系统内核则是一块一块的内存碎片的东西,为了是节省内核空间,方便对内存管理,对于这边内存空间,又划分为用户空间与内核空间,用户空间只用于用户程序的执行,若要执行各种 io 操作,就会通过系统调用等进入内核空间进行操作 .每个进程都有自己的 pid,可以通过 ps 命令查某个进程的 pid,进入/proc/ 可以查看该进程的详细信息

线程

线程是进程的执行单元,一个进程可以包含多个线程,只有拥有了线程的进程才会被 cpu 执行,所以一个进程至少有一个主进程由于多个线程可以共享一个进程的内存空间,切换虚拟地址空间的开销巨大,为什么进程切换需要开销巨大:简单理解 就是因为进程切换要保存的线程太多如寄存器,栈,代码段执行位置,而线程切换只需要上下文切换,保存线程执行的上下文就行.线程的切换只需要保存线程的执行现场,保存在该线程的栈里,CPU 把栈指针,指令寄存器的值指向下一个线程 ,相比线程更加轻量级可以理解为 进程面向内存分配管理,线程主要面向 CPU 调度

协程

虽然线程比进程更轻量级,但是每个线程依然占有 1M 左右的空间,在高并发场景下 非常吃机器内存,比如构建一个 http 服务器,如果每一次请求分配一个线程,请求数量增 容易 OOM ,而线程切换的开销也很大,同时线程的创建和销毁都是系统开销,因此用内核来做,解决的方法 可以通过线程池或协程解决

协程是用户态,比如线程更加轻量级,操作系统对其没有任何感知,之所以没有感知,是由于处于协程的用户栈感知的服务,是由用户创建而非操作系统

如一个进程可拥有以有多个线程一样,一个线程也可以拥有多个协程。协程之于线程如同线程之于 cpu,拥有自己的协程队列,每个协程拥有自己的栈空间,在协程切换时候只需要保存协程的上下文,开销要比内核态的线程切换要小很多。

定义

Goroutine 的并发编程模型基于 GMP 模型,简要解释一下 GMP 的含义:

G:表示 goroutine,每个 goroutine 都有自己的栈空间,定时器,初始化的栈空间在 2k 左右,空间会随着需求增长

M:抽象化代表内核线程,记录内核线程栈信息,当 goroutine 调度到线程时,使用该 goroutine 自己的栈信息。

P:代表调度器,负责调度 goroutine,维护一个本地 goroutine 队列,M 从 P 上获得 goroutine 并执行,同时还负责部分内存的管理。

模型

M 代表一个工作线程,在 M 上有一个 P 和 G,P 是绑定到 M 上的,G 是通过 P 的调度获取的,在某一时刻,一个 M 上只有一个 G(g0 除外)。在 P 上拥有一个 G 队列,里面是已经就绪的 G,是可以被调度到线程栈上执行的协程,称为运行队列。

接下来看一下程序中 GMP 的分布。

每个进程都有一个全局的 G 队列,也拥有 P 的本地执行队列,同时也有不在运行队列中的 G。如正处于 channel 的阻塞状态的 G,还有脱离 P 绑定在 M 的(系统调用)G,还有执行结束后进入 P 的 gFree 列表中的 G 等等,接下来列举一下常见的几种状态。

状态汇总

G 状态

G 的主要几种状态:

本文基于 Go1.13,具体代码见 src/runtime/runtime2.go

_Gidle:刚刚被分配并且还没有被初始化,值为 0,为创建 goroutine 后的默认值

_Grunnable: 没有执行代码,没有栈的所有权,存储在运行队列中,可能在某个 P 的本地队列或全局队列中(如上图)。

_Grunning: 正在执行代码的 goroutine,拥有栈的所有权(如上图)。

_Gsyscall:正在执行系统调用,拥有栈的所有权,与 P 脱离,但是与某个 M 绑定,会在调用结束后被分配到运行队列(如上图)。

_Gwaiting:被阻塞的 goroutine,阻塞在某个 channel 的发送或者接收队列(如上图)。

_Gdead: 当前 goroutine 未被使用,没有执行代码,可能有分配的栈,分布在空闲列表 gFree,可能是一个刚刚初始化的 goroutine,也可能是执行了 goexit 退出的 goroutine(如上图)。

_Gcopystac:栈正在被拷贝,没有执行代码,不在运行队列上,执行权在

_Gscan : GC 正在扫描栈空间,没有执行代码,可以与其他状态同时存在

P 的状态

_Pidle :处理器没有运行用户代码或者调度器,被空闲队列或者改变其状态的结构持有,运行队列为空

_Prunning :被线程 M 持有,并且正在执行用户代码或者调度器(如上图)

_Psyscall:没有执行用户代码,当前线程陷入系统调用(如上图)

_Pgcstop :被线程 M 持有,当前处理器由于垃圾回收被停止

_Pdead :当前处理器已经不被使用

M 的状态

自旋线程:处于运行状态但是没有可执行 goroutine 的线程(如下图),数量最多为 GOMAXPROC,若是数量大于 GOMAXPROC 就会进入休眠。

非自旋线程:处于运行状态有可执行 goroutine 的线程。

调度场景

Channel 阻塞:当 goroutine 读写 channel 发生阻塞时候,会调用 gopark 函数,该 G 会脱离当前的 M 与 P,调度器会执行 schedule 函数调度新的 G 到当前 M。可参考上一篇文章 channel 探秘。

系统调用:当某个 G 由于系统调用陷入内核态时,该 P 就会脱离当前的 M,此时 P 会更新自己的状态为 Psyscall,M 与 G 互相绑定,进行系统调用。结束以后若该 P 状态还是 Psyscall,则直接关联该 M 和 G,否则使用闲置的处理器处理该 G。

系统监控:当某个 G 在 P 上运行的时间超过 10ms 时候,或者 P 处于 Psyscall 状态过长等情况就会调用 retake 函数,触发新的调度。

主动让出:由于是协作式调度,该 G 会主动让出当前的 P,更新状态为 Grunnable,该 P 会调度队列中的 G 运行。

总结

Go 语言中通过 GMP 模型实现了对 CPU 和内存的合理利用,使得用户在不用担心内存的情况下体验到线程的好处。虽说协程的空间很小,但是也需要关注一下协程的生命周期,防止过多的协程滞留造成 OOM。最近遇到了线上的 OOM 问题,等待下期一起分析

GO 打包

打包 linux

CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o  "xxxx" xxxx.go

打包 windows

CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o  "xxxx" xxxx.go

打包 mac

CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o  "xxxx" xxxx.go

Go 之 context

为什么需要 context

在并发程序中,由于超时、取消操作或者一些异常情况,往往需要进行抢占操作或者中断后续操作。熟悉 channel 的朋友应该都见过使用 done channel

context 接口

type Context interface {

Deadline() (deadline time.Time, ok bool)

Done() <-chan struct{}

Err() error

Value(key interface{}) interface{}
}

Context 接口包含四个方法:

  • Deadline 返回绑定当前 context 的任务被取消的截止时间;如果没有设定期限,将返回 ok == false。
  • Done 当绑定当前 context 的任务被取消时,将返回一个关闭的 channel;如果当前 context 不会被取消,将返回 nil。
  • Err 如果 Done 返回的 channel 没有关闭,将返回 nil;如果 Done 返回的 channel 已经关闭,将返回非空的值表示任务结束的原因。如果是 context 被取消,Err 将返回 Canceled;如果是 context 超时,Err 将返回 DeadlineExceeded。
  • Value 返回 context 存储的键值对中当前 key 对应的值,如果没有对应的 key,则返回 nil。

emptyCtx

emptyCtx 是一个 int 类型的变量,但实现了 context 的接口。emptyCtx 没有超时时间,不能取消,也不能存储任何额外信息,所以 emptyCtx 用来作为 context 树的根节点。

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 interface{}) interface{} {
return nil
}

func (e *emptyCtx) String() string {
switch e {
case background:
return "context.Background"
case todo:
return "context.TODO"
}
return "unknown empty Context"
}

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

func Background() Context {
return background
}

func TODO() Context {
return todo
}

但我们一般不会直接使用 emptyCtx,而是使用由 emptyCtx 实例化的两个变量,分别可以通过调用 Background 和 TODO 方法得到,但这两个 context 在实现上是一样的。那么 Background 和 TODO 方法得到的 context 有什么区别呢?可以看一下官方的解释: Background 和 TODO 只是用于不同场景下: Background 通常被用于主函数、初始化以及测试中,作为一个顶层的 context,也就是说一般我们创建的 context 都是基于 Background;而 TODO 是在不确定使用什么 context 的时候才会使用下面将介绍两种不同功能的基础 context 类型:valueCtx 和 cancelCtx。

valueCtx

valueCtx 结构体

type valueCtx struct {
Context
key, val interface{}
}

func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}

valueCtx 利用一个 Context 类型的变量来表示父节点 context,所以当前 context 继承了父 context 的所有信息;valueCtx 类型还携带一组键值对,也就是说这种 context 可以携带额外的信息。valueCtx 实现了 Value 方法,用以在 context 链路上获取 key 对应的值,如果当前 context 上不存在需要的 key,会沿着 context 链向上寻找 key 对应的值,直到根节点。

WithValue

WithValue 用以向 context 添加键值对:

func WithValue(parent Context, key, val interface{}) Context {
if key == nil {
panic("nil key")
}
if !reflect.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}

这里添加键值对不是在原 context 结构体上直接添加,而是以此 context 作为父节点,重新创建一个新的 valueCtx 子节点,将键值对添加在子节点上,由此形成一条 context 链。获取 value 的过程就是在这条 context 链上由尾部上前搜寻:

cancelCtx

cancelCtx 结构体

type cancelCtx struct {
Context

mu sync.Mutex // protects following fields
done chan struct{} // created lazily, closed by first cancel call
children map[canceler]struct{} // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
}

type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}

跟 valueCtx 类似,cancelCtx 中也有一个 context 变量作为父节点;变量 done 表示一个 channel,用来表示传递关闭信号;children 表示一个 map,存储了当前 context 节点下的子节点;err 用于存储错误信息表示任务结束的原因。

再来看一下cancelCtx实现的方法:

func (c *cancelCtx) Done() <-chan struct{} {
c.mu.Lock()
if c.done == nil {
c.done = make(chan struct{})
}
d := c.done
c.mu.Unlock()
return d
}

func (c *cancelCtx) Err() error {
c.mu.Lock()
err := c.err
c.mu.Unlock()
return err
}

func (c *cancelCtx) cancel(removeFromParent bool, err error) {
if err == nil {
panic("context: internal error: missing cancel error")
}
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // already canceled
}
// 设置取消原因
c.err = err
设置一个关闭的channel或者将done channel关闭,用以发送关闭信号
if c.done == nil {
c.done = closedchan
} else {
close(c.done)
}
// 将子节点context依次取消
for child := range c.children {
// NOTE: acquiring the child's lock while holding parent's lock.
child.cancel(false, err)
}
c.children = nil
c.mu.Unlock()

if removeFromParent {
// 将当前context节点从父节点上移除
removeChild(c.Context, c)
}
}

可以发现 cancelCtx 类型变量其实也是 canceler 类型,因为 cancelCtx 实现了 canceler 接口。 Done 方法和 Err 方法没必要说了,cancelCtx 类型的 context 在调用 cancel 方法时会设置取消原因,将 done channel 设置为一个关闭 channel 或者关闭 channel,然后将子节点 context 依次取消,如果有需要还会将当前节点从父节点上移除。

WithCancel

WithCancel 函数用来创建一个可取消的 context,即 cancelCtx 类型的 context。WithCancel 返回一个 context 和一个 CancelFunc,调用 CancelFunc 即可触发 cancel 操作。直接看源码:

type CancelFunc func()

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}

// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
// 将parent作为父节点context生成一个新的子节点
return cancelCtx{Context: parent}
}

func propagateCancel(parent Context, child canceler) {
if parent.Done() == nil {
// parent.Done()返回nil表明父节点以上的路径上没有可取消的context
return // parent is never canceled
}
// 获取最近的类型为cancelCtx的祖先节点
if p, ok := parentCancelCtx(parent); ok {
p.mu.Lock()
if p.err != nil {
// parent has already been canceled
child.cancel(false, p.err)
} else {
if p.children == nil {
p.children = make(map[canceler]struct{})
}
// 将当前子节点加入最近cancelCtx祖先节点的children中
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}

func parentCancelCtx(parent Context) (*cancelCtx, bool) {
for {
switch c := parent.(type) {
case *cancelCtx:
return c, true
case *timerCtx:
return &c.cancelCtx, true
case *valueCtx:
parent = c.Context
default:
return nil, false
}
}
}

之前说到 cancelCtx 取消时,会将后代节点中所有的 cancelCtx 都取消,propagateCancel 即用来建立当前节点与祖先节点这个取消关联逻辑。

  1. 如果 parent.Done()返回 nil,表明父节点以上的路径上没有可取消的 context,不需要处理;
  2. 如果在 context 链上找到到 cancelCtx 类型的祖先节点,则判断这个祖先节点是否已经取消,如果已经取消就取消当前节点;否则将当前节点加入到祖先节点的 children 列表。
  3. 否则开启一个协程,监听 parent.Done()和 child.Done(),一旦 parent.Done()返回的 channel 关闭,即 context 链中某个祖先节点 context 被取消,则将当前 context 也取消。

当前 cancelCtx 的父节点 context 并不是一个可取消的 context,也就没法记录 children。

timerCtx

timerCtx 是一种基于 cancelCtx 的 context 类型,从字面上就能看出,这是一种可以定时取消的 context。

type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.

deadline time.Time
}

func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
return c.deadline, true
}

func (c *timerCtx) cancel(removeFromParent bool, err error) {
将内部的cancelCtx取消
c.cancelCtx.cancel(false, err)
if removeFromParent {
// Remove this timerCtx from its parent cancelCtx's children.
removeChild(c.cancelCtx.Context, c)
}
c.mu.Lock()
if c.timer != nil {
取消计时器
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}

timerCtx 内部使用 cancelCtx 实现取消,另外使用定时器 timer 和过期时间 deadline 实现定时取消的功能。timerCtx 在调用 cancel 方法,会先将内部的 cancelCtx 取消,如果需要则将自己从 cancelCtx 祖先节点上移除,最后取消计时器。

WithDeadline

WithDeadline 返回一个基于 parent 的可取消的 context,并且其过期时间 deadline 不晚于所设置时间 d。

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
// The current deadline is already sooner than the new one.
return WithCancel(parent)
}
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
// 建立新建context与可取消context祖先节点的取消关联关系
propagateCancel(parent, c)
dur := time.Until(d)
if dur <= 0 {
c.cancel(true, DeadlineExceeded) // deadline has already passed
return c, func() { c.cancel(false, Canceled) }
}
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil {
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}
  1. 如果父节点 parent 有过期时间并且过期时间早于给定时间 d,那么新建的子节点 context 无需设置过期时间,使用 WithCancel 创建一个可取消的 context 即可;
  2. 否则,就要利用 parent 和过期时间 d 创建一个定时取消的 timerCtx,并建立新建 context 与可取消 context 祖先节点的取消关联关系,接下来判断当前时间距离过期时间 d 的时长 dur:
  3. 如果 dur 小于 0,即当前已经过了过期时间,则直接取消新建的 timerCtx,原因为 DeadlineExceeded;
  4. 否则,为新建的 timerCtx 设置定时器,一旦到达过期时间即取消当前 timerCtx。

WithTimeout

与 WithDeadline 类似,WithTimeout 也是创建一个定时取消的 context,只不过 WithDeadline 是接收一个过期时间点,而 WithTimeout 接收一个相对当前时间的过期时长 timeout:

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}

context 的使用

首先使用 context 实现文章开头 done channel 的例子来示范一下如何更优雅实现协程间取消信号的同步:

func main() {
messages := make(chan int, 10)

// producer
for i := 0; i < 10; i++ {
messages <- i
}

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

// consumer
go func(ctx context.Context) {
ticker := time.NewTicker(1 * time.Second)
for _ = range ticker.C {
select {
case <-ctx.Done():
fmt.Println("child process interrupt...")
return
default:
fmt.Printf("send message: %d\n", <-messages)
}
}
}(ctx)

defer close(messages)
defer cancel()

select {
case <-ctx.Done():
time.Sleep(1 * time.Second)
fmt.Println("main process exit!")
}
}

只要让子线程监听主线程传入的 ctx,一旦 ctx.Done()返回空 channel,子线程即可取消执行任务。但这个例子还无法展现 context 的传递取消信息的强大优势.

总结

context 主要用于父子任务之间的同步取消信号,本质上是一种协程调度的方式。另外在使用 context 时有两点值得注意:上游任务仅仅使用 context 通知下游任务不再需要,但不会直接干涉和中断下游任务的执行,由下游任务自行决定后续的处理操作,也就是说 context 的取消操作是无侵入的;context 是线程安全的,因为 context 本身是不可变的(immutable),因此可以放心地在多个协程中传递使用。