
在Go语言中,goroutine是实现并发执行的基本单元,它是一种极其轻量级的执行线程。值得注意的是,程序的主函数 main() 本身也运行在一个goroutine中,这是Go程序的默认执行上下文。
在同一个goroutine内部,代码会严格地按照顺序从上到下执行。如果遇到了一个执行非常缓慢的操作(例如耗时的I/O或复杂计算),这个goroutine就会被阻塞,其后的所有操作都必须等待,这就像单车道发生了交通堵塞,后面的车辆只能干等。
func slowFunc() {
fmt.Println("耗时操作开始")
// 暂停 goroutine 两秒,模拟耗时的操作
time.Sleep(time.Second * 2)
fmt.Println("耗时操作结束")
}
func main() {
fmt.Println("主函数开始")
slowFunc()
fmt.Println("主函数结束")
}


这种阻塞无疑会严重拖累程序的整体运行效率。为了提升代码性能,一个常见的策略是将耗时操作放入一个独立的 goroutine 中执行。这样,主goroutine(即主函数)就不会被阻塞,可以继续执行后续的代码。这好比在多车道的高速公路上,即使一条车道发生事故,其他车道上的车辆依然可以正常通行(前提是车道间有隔离带)。
在函数调用前加上 go 关键字,就能让这个函数在一个全新的、独立的goroutine中异步执行。
func slowFunc() {
fmt.Println("耗时操作开始")
time.Sleep(time.Second * 2)
fmt.Println("耗时操作结束")
}
func main() {
fmt.Println("主函数开始")
// 把慢函数放在新 goroutine 中执行
// 不要在主函数中挡道
go slowFunc()
fmt.Println("在主函数中干点别的")
fmt.Println("主函数结束")
}


从执行结果可以清楚地看到,主函数果然没有等待耗时的 slowFunc 函数,而是直接执行了后续的打印语句,速度大大提升。
然而,这带来了一个新问题:主函数运行速度是快了,但那个被“放飞”的耗时操作却两手一摊,无奈地表示:“我还没执行完呢,你怎么就结束了?” 主函数可以追求速度,但不能对其它goroutine的生死置之不理。在主函数结束前,我们最好能等待一下其它关键的goroutine完成任务。
这时,就需要在两个goroutine之间建立通信机制,而Go语言提供的解决方案就是 channel(通道)。我们来改造一下代码,让耗时操作结束后,能通过channel通知主函数:“老大,我的任务完成了,你可以安心结束了”。
func slowFunc(c chan string) {
fmt.Println("耗时操作开始")
time.Sleep(time.Second * 2)
fmt.Println("耗时操作结束")
// 向通道中发送信息
c <- "老大,可以结束了"
}
func main() {
// 创建一个 channel,只能传递字符串
c := make(chan string)
fmt.Println("主函数开始")
go slowFunc(c)
fmt.Println("在主函数中干点别的")
// 从 channel 中读取信息
msg := <-c
fmt.Printf("收到慢函数的信息:%v\n", msg)
fmt.Println("主函数结束")
}


现在,主函数在打印完“在主函数中干点别的”之后,执行到 msg := <-c 时会主动等待,直到从channel中接收到 slowFunc 发来的消息,才会继续执行并最终结束。程序的逻辑变得完整且可控。
channel的基本用法可以简单总结为以下几点:
- 使用
make(chan T) 创建通道,其中 T 指定了通道所能传递消息的数据类型。
- 发送方使用
c <- data 语法,将数据 data 写入通道实例 c。
- 接收方使用
variable := <-c 语法,从通道实例 c 中读取数据并赋值给变量。
- 接收操作是阻塞的:如果通道中没有数据,接收方的goroutine会一直等待,直到有数据可读或通道被关闭。
- 发送方可以通过
close(c) 函数来主动关闭通道,通知接收方不再有数据发送。
那么,如果发送方提前关闭了通道,会发生什么?下面的代码展示了这种场景及其后果。
func slowFunc(c chan string) {
fmt.Println("耗时操作开始")
close(c) // 慢操作主动结束 channel
time.Sleep(time.Second * 2)
fmt.Println("耗时操作结束")
}
func main() {
c := make(chan string)
fmt.Println("主函数开始")
go slowFunc(c)
fmt.Println("在主函数中干点别的")
msg := <-c
fmt.Printf("收到慢函数的信息:%v\n", msg)
fmt.Println("主函数结束")
}


可以看到,一旦主函数从已关闭的channel中读取到零值(对于字符串是空字符串),它便不再等待 slowFunc 中那个漫长的 Sleep 操作,直接宣告程序结束。而 slowFunc 这个goroutine在休眠结束后,虽然打印了“耗时操作结束”,但此时主进程可能早已退出。
goroutine与channel共同构成了Go语言强大且优雅的并发编程基石。goroutine的启动开销极小,初始栈大小仅为2KB,并且会根据需要自动扩容。一个简单的计算可以让我们感受到它的轻量级:如果你的电脑内存是4GB,那么理论上可以创建的goroutine数量约为 4GB / 2KB ≈ 200万 个。
当然,实际运行中受限于CPU调度、系统资源等因素,不可能真正达到这个数字,但支撑起十万、百万级别的并发任务对于Go来说并非难事。这不禁让人联想到孙悟空“拔一根毫毛,吹出猴万个”的神通,而Go语言在“吹”出并发执行体这方面,潜力或许比猴哥还要惊人。
掌握goroutine和channel,是深入Go并发编程世界的关键一步。如果你想了解更多关于Go语言或其他后端架构的深度内容,欢迎来到 云栈社区 与更多开发者一同交流探讨。
参考资料