前言
在Golang中main函数是程序执行的入口和主线,无论怎样,main函数早于它开启的Goroutines结束执行都不属于正常的程序执行流程!
使用以下3种方式,可以控制Goroutines的执行顺序
1.sync.WaitGroup
main函数等待,开启的子Goroutines正常执行完毕。

package main import ( "fmt" "golang.org/x/sys/windows" "sync" ) var naturalNumberCh = make(chan int, 100) var wg sync.WaitGroup func write() { threadID := windows.GetCurrentThreadId() defer func() { wg.Done() //记得channel写入完成关闭,否则for range循环读一直不结束! close(naturalNumberCh) fmt.Printf("----write-%d结束\n", threadID) }() for i := 0; i <= 100; i++ { naturalNumberCh <- i } } func read() { defer wg.Done() threadID := windows.GetCurrentThreadId() //记得写完了关闭Channel,否则for range循环不结束! for n := range naturalNumberCh { fmt.Printf("----read-goroutine-%d读取到值%d\n", threadID, n) } } func main() { defer fmt.Println("mian函数结束") readerCount := 3 writeCount := 1 //开1个写go程 for i := 0; i < writeCount; i++ { go write() } //开3个读go程执行结束后主动通知main函数,发请求结束的请求! for i := 0; i < readerCount; i++ { go read() } gocount := readerCount + writeCount wg.Add(gocount) wg.Wait() }
2.channel
main函数等待开启的子Goroutines正常执行完毕,利用channel的阻塞特性+约定channel传输值的个数机制,

package main import ( "fmt" "golang.org/x/sys/windows" ) var naturalNumberCh = make(chan int, 100) var readDoneCh = make(chan bool, 3) func write() { threadID := windows.GetCurrentThreadId() defer func() { //记得channel写入完成关闭,否则for range循环读一直不结束! close(naturalNumberCh) fmt.Printf("----write-%d结束\n", threadID) }() for i := 0; i <= 100; i++ { naturalNumberCh <- i } } func read() { threadID := windows.GetCurrentThreadId() //记得写完了关闭Channel,否则for range循环不结束! for n := range naturalNumberCh { fmt.Printf("----read-goroutine-%d读取到值%d\n", threadID, n) } readDoneCh <- true } func main() { defer fmt.Println("mian函数结束") readerCount := 3 writeCount := 1 //开1个写go程 for i := 0; i < writeCount; i++ { go write() } //开3个读go程执行结束后主动通知main函数,发请求结束的请求! for i := 0; i < readerCount; i++ { go read() } //等待3个读go程都执行完毕了,main函数再结束 for i := 0; i < readerCount; i++ { fmt.Println(<-readDoneCh) } }
3.context
main函数主动终止开启的子Goroutines,子Goroutines接收到终止信号,被动退出。

package main import ( "context" "fmt" "golang.org/x/sys/windows" "time" ) var naturalNumberCh = make(chan int, 100) func write() { threadID := windows.GetCurrentThreadId() defer func() { //记得channel写入完成关闭,否则for range循环读一直不结束! close(naturalNumberCh) fmt.Printf("----write-%d结束\n", threadID) }() for i := 0; i <= 100; i++ { naturalNumberCh <- i } } func read(ctx context.Context) { threadID := windows.GetCurrentThreadId() for { select { case <-ctx.Done(): fmt.Printf("----read-goroutine-%d需要强制结束!\n", threadID) return default: //记得写完了关闭Channel,否则for range循环不结束! for n := range naturalNumberCh { fmt.Printf("----read-goroutine-%d读取到值%d\n", threadID, n) } } } } func main() { defer fmt.Println("mian函数结束") readerCount := 3 writeCount := 1 ctx, cancel := context.WithCancel(context.Background()) //开1个写go程 for i := 0; i < writeCount; i++ { go write() } //开3个读go程执行结束后主动通知main函数,发请求结束的请求! for i := 0; i < readerCount; i++ { go read(ctx) } //等读go程读取完成 time.Sleep(5 * time.Second) cancel() //等读go程全部关闭 time.Sleep(5 * time.Second) }
问题
package main import ( "fmt" "time" ) import ( "sync" ) var wg sync.WaitGroup //toSchool 子goroutine func toSchool() { defer wg.Done() for { fmt.Println( "walking~~~~" ) time.Sleep(time.Second * 1) } } func main() { wg.Add(1) //当1个子goroutine开启的时候.... go toSchool() fmt.Println( "台风来了!" ) //我们如何在不结束自己的前提下结束这些衍生的子goroutine呢? wg.Wait() } |
自由解决方案
既然是context是golang官方提出的标准方案,相对而言也会有自由解决方案。
1.通过全局变量控制子goroutine退出
package main import ( "fmt" "time" "sync" ) var wg sync.WaitGroup var canToSignel=true func toSchool() { defer wg.Done() //不断地监测这个信号 for canToSignel { fmt.Println( "I'm on the way walking to scholl~~~~" ) time.Sleep(time.Second * 1) } } func main() { wg.Add(1) go toSchool() time.Sleep(time.Second*10) fmt.Println( "The typhoon is coming!" ) //修改全局变量(信号) canToSignel=false wg.Wait() } |
2.通过全局channel控制goroutine退出
package main import ( "fmt" "sync" "time" ) var wg sync.WaitGroup //全局channel var signalChan = make( chan bool, 1) func child() { defer wg.Done() for { fmt.Println( "我是child goroutine" ) time.Sleep(time.Second * 2) //检测全局channel中是否有消息推送 select { case <-signalChan: return default : } } } func main() { wg.Add(1) go child() time.Sleep(time.Second * 10) signalChan <- true wg.Wait() } |
or
//通过往channel中发送信号的方式 var canToChannel = make( chan bool, 1) func toSchool() { defer wg.Done() //不断地监测这个信号 select { case <-canToChannel: break default : fmt.Println( "I'm on the way walking to scholl~~~~" ) time.Sleep(time.Second * 1) } } func main() { wg.Add(1) go toSchool() time.Sleep(time.Second * 10) fmt.Println( "The typhoon is coming!" ) //提交退出信号 canToChannel<-true wg.Wait() } |
退出和追踪衍生goroutines的方式有很多,我们不使用go内置的context也能完全解决这一问题,但是每个程序员使用的解决方案不同的话,就会增加代码的阅读难度。
官方解决方案(context)
Package context defines the Context type, which carries deadlines, cancelation signals, and other request-scoped values across API boundaries and between processes.
Incoming requests to a server should create a Context, and outgoing calls to servers should accept a Context.
The chain of function calls between them must propagate the Context,
optionally replacing it with a derived Context
created using WithCancel, WithDeadline, WithTimeout, or WithValue.
When a Context is canceled, all Contexts derived from it are also canceled.
什么是context?
context.Context
是一个接口,该接口定义了四个需要实现的方法。具体签名如下:
1 2 3 4 5 6 | type Context interface { Deadline() (deadline time.Time, ok bool) Done() <- chan struct {} Err() error Value(key interface {}) interface {} } |
context可以定义Context
类型,专门用来简化 对于处理1个请求的N个 goroutine 之间与请求域的数据、取消信号、截止时间等相关操作,这些操作可能涉及多个 API 调用。
创建使用context
Background()和TODO() (根节点)
Go内置两个函数:Background()
和TODO()
,这两个函数分别返回一个实现了Context
接口的background
和todo
。我们代码中最开始都是以这两个内置的上下文对象作为最顶层的partent context
,衍生出更多的子上下文对象。
Background()
主要用于main函数、初始化以及测试代码中,作为Context这个树结构的最顶层的Context,也就是根Context。
TODO()
,它目前还不知道具体的使用场景,如果我们不知道该使用什么Context的时候,可以使用这个。(必须要传递1个context类型的参数)
todo
本质上也是emptyCtx
结构体类型,是一个不可取消,没有设置截止时间,没有携带任何值的Context。
1 2 3 4 5 6 7 8 9 | //1个具体的日志收集任务(TaillTask) type TaillTask struct { path string topic string instance *tail.Tail //exit task ctx context.Context exit context.CancelFunc } |
more
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | package taillog import ( "context" "github.com/hpcloud/tail" "fmt" "jd.com/logagent/kafka" ) //1个具体的日志收集任务(TaillTask) type TaillTask struct { path string topic string instance *tail.Tail //exit task ctx context.Context exit context.CancelFunc } //实例化1个具体的日志收集任务(TaillTask) func (T *TaillTask) NewTaillTask(path, topic string)(task *TaillTask,err error){ ctx, cancel := context.WithCancel(context.Background()) task=&TaillTask{path: path, topic: topic, ctx: ctx, exit: cancel,} //taill 文件配置 config := tail.Config{ ReOpen: true, //重新打开文件 Follow: true, //跟随文件 Location: &tail.SeekInfo{Offset: 0, Whence: 2}, //从文件的哪个地方开始读 MustExist: false, //文件不存在不报错 Poll: true, } //给task任务填充taill(1个具体打开文件的taillobj) task.instance, err = tail.TailFile(task.path, config) if err != nil { fmt.Println( "文件打开失败" , err) } //直接去采集日志 go task.run() return } //从tailobj中读取日志内容---->kafka topic方法 func (T *TaillTask)run() { fmt.Printf( "开始收集%s日志\n" ,T.path) for { select { //父进程调用了cancel case <-T.ctx.Done(): fmt.Printf( "taill任务%s%s退出了...\n" ,T.topic,T.path) return case line := <-T.instance.Lines: fmt.Printf( "从%s文件中获取到内容%s" ,T.path,line.Text) //taill采集到数据-----channel------>kafka 异步 kafka.SendToChan(T.topic, line.Text) } } } |
package main import ( "context" "fmt" "sync" "time" ) var wg sync.WaitGroup func grandChild(ctx context.Context) { defer wg.Done() for { time.Sleep(time.Second*1) fmt.Println( "grandchild function " ) select { //<-chan struct{} case <-ctx.Done(): return default : } } } func child(ctx context.Context) { defer wg.Done() go grandChild(ctx) for { time.Sleep(time.Second*5) fmt.Println( "child function " ) select { //<-chan struct{} case <-ctx.Done(): return default : } } } func main() { //定义1个全局的context类型的变量 ctx, cancel := context.WithCancel(context.Background()) wg.Add(2) go child(ctx) time.Sleep(time.Second * 10) //退出 /* cancel closes c.done, cancels each of c's children, and, if removeFromParent is true, removes c from its parent's children. */ cancel() wg.Wait() } |
案例2
package main import ( "context" "fmt" ) func gen(ctx context.Context) <- chan int { //定义1个destnation channel dest := make( chan int) n := 1 //匿名函数协程不断得给这个dest channel中输入数字 go func () { for { select { //context结束该匿名函数协程结束 case <-ctx.Done(): return case dest <- n: n++ } } }() return dest } func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() //调用 for n := range gen(ctx) { fmt.Println(n) if n == 5 { //main函数结束之后,调用了context取消 return } } } |
当context的截止日过期时, ctx.Done()返回后context deadline exceeded。
import ( "context" "fmt" "sync" "time" ) var wg sync.WaitGroup func connectMyql(ctx context.Context) { defer wg.Done() for { time.Sleep(time.Second * 1) fmt.Println( "我连我连...我连莲莲...." ) select { case <-ctx.Done(): fmt.Println(ctx.Err()) return default : } } } func main() { //设置context 10秒钟之后过期 d := time.Now().Add(time.Second * 10) ctx, cancel := context.WithDeadline(context.Background(), d) /* 尽管ctx会过期,但在任何情况下调用它的cancel函数都是很好的实践 如果不这样做,可能会使上下文及其父类存活的时间超过必要的时间。 */ defer cancel() wg.Add(1) go connectMyql(ctx) wg.Wait() } |
package main import ( "context" "fmt" "sync" "time" ) var wg sync.WaitGroup func connectMyql(ctx context.Context) { defer wg.Done() for { time.Sleep(time.Second * 1) fmt.Println( "我连我连...我连莲莲...." ) select { case <-ctx.Done(): fmt.Println(ctx.Err()) return default : } } } func main() { //设置context 从当前时间开始10秒钟之后过期(决对时间) // d := time.Now().Add(time.Second * 10) // ctx, cancel := context.WithDeadline(context.Background(), d) //设置相对时间 5秒钟后过期(相对时间) ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) /* 尽管ctx会过期,但在任何情况下调用它的cancel函数都是很好的实践 如果不这样做,可能会使上下文及其父类存活的时间超过必要的时间。 */ defer cancel() wg.Add(1) go connectMyql(ctx) wg.Wait() } |
WithValue(传递值)
WithCancel、WithDeadline、WithTimeout,With 这个verb 就是context可以追溯和退出其衍生子goroutines 的关键所在! 在子goroutine开启时就与生俱来一些元数据!
WithValue可以保证1个gorutin繁衍了N代子goroutines之后,它的后代goroutines都能with(携带)1个固定值,这样我就可以自上而下追溯这个繁衍链了!
这也是微服务链路追踪的核心思想。
1 | func WithValue(parent Context, key, val interface {}) Context |
WithValue returns a copy of parent in which the value associated with key is val.
WithValue
返回父节点的副本,其中与key关联的值为val。
Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions.
仅对API和进程间传递请求域的数据使用上下文值,而不是使用它来传递可选参数给函数。我把它当成session来看!
The provided key must be comparable and should not be of type string or any other built-in type to avoid collisions between packages using context.
所提供的键必须是可比较的,并且不应该是string
类型或任何其他内置类型,以避免使用上下文在包之间发生冲突。
Users of WithValue should define their own types for keys. To avoid allocating when assigning to an interface{}, context keys often have concrete type struct{}. Alternatively, exported context key variables' static type should be a pointer or interface.
WithValue
的用户应该为键定义自己的类型。为了避免在分配给interface{}时进行分配,上下文键通常具有具体类型struct{}
。或者,导出的上下文关键变量的静态类型应该是指针或接口
package main import ( "context" "fmt" "sync" "time" ) // context.WithValue //TraceCode 自定义类型 type TraceCode string var wg sync.WaitGroup func worker(ctx context.Context) { defer wg.Done() key := TraceCode( "TRACE_CODE" ) // 在子goroutine中获取trace code,(string)是类型断言! traceCode, ok := ctx.Value(key).(string) if !ok { fmt.Println( "invalid trace code" ) } for { fmt.Printf( "worker, trace code:%s\n" , traceCode) // 假设正常连接数据库耗时1秒 time.Sleep(time.Second * 1) // 10秒后自动调用 select { case <-ctx.Done(): fmt.Println( "worker done!" ) return default : } } } func main() { // 设置1个10秒的超时 ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) //在系统的入口中设置trace code传递给后续启动的goroutine实现微服务日志数据聚合 ctx = context.WithValue(ctx, TraceCode( "TRACE_CODE" ), "666" ) wg.Add(1) go worker(ctx) //主线程等待10秒后 time.Sleep(time.Second * 10) //通知子goroutine结束 cancel() wg.Wait() fmt.Println( "over" ) } |
context应用场景(微服务链路追踪)
作为1个微服务架构,微服务之间session不共享的服务端,如何追踪客户端1次request都调用了哪些微服务组件?并且聚合日志。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
2017-05-06 ATM