news 2026/8/22 1:35:19

Go 语言入门笔记(六):goroutine 和 channel——并发编程的入门

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Go 语言入门笔记(六):goroutine 和 channel——并发编程的入门

终于到 Go 最吸引人的部分了。记得当初决定学 Go,很大原因就是听说了它“天生支持并发”“用 goroutine 写并发像开挂一样”。但真正开始写的时候才发现,并发编程哪有那么简单,goroutine 用起来爽,channel 用不好照样死锁、泄漏、数据竞争。这篇就把我入门并发时踩过的坑和搞懂的概念梳理一遍。


1. goroutine:比线程更轻量的“协程”

1.1 启动一个 goroutine

在 Go 里启动一个并发执行的函数非常简单,只需要在函数调用前加一个go关键字:

func sayHello() { fmt.Println("hello from goroutine") } func main() { go sayHello() // 启动一个 goroutine fmt.Println("hello from main") time.Sleep(time.Second) // 给 goroutine 一点执行时间 }

输出可能是:

hello from main hello from goroutine

也可能是:

hello from goroutine hello from main

顺序不确定,因为 goroutine 是并发执行的。

注意:如果main函数执行完了,所有 goroutine 都会被强制结束,不管它们有没有执行完。所以上面故意加了个time.Sleep,否则可能看不到 goroutine 的输出。实际项目中不会用time.Sleep来等待,而是用 channel 或者sync.WaitGroup(后面细说)。

1.2 goroutine 和线程的区别

我一开始以为 goroutine 就是 Go 版的线程,后来看了些资料才明白完全不是一回事。

  • 线程:由操作系统调度,每个线程有固定的栈空间(一般 1MB 以上),创建和切换的开销比较大。

  • goroutine:由 Go 运行时调度,初始栈空间只有 2KB 左右,可以动态增长。创建和销毁的成本极低,轻松启动成千上万个 goroutine。

Go 的调度器会把多个 goroutine 分配到几个操作系统线程上执行,这就是所谓的M:N 调度模型。所以写 Go 并发,通常不需要关心底层线程,直接go func()就完事。

1.3 匿名函数 goroutine

最常用的写法是直接 go 一个匿名函数:

go func() { fmt.Println("我在 goroutine 里") }()

但要注意闭包捕获变量的问题,前面讲 for 循环时提过,Go 1.22 之前循环变量捕获有坑,1.22 修复了。不过为了兼容老代码,了解这个坑很重要。

2. channel:goroutine 之间的通信管道

光有 goroutine 还不够,它们之间需要通信。Go 的哲学是:

不要通过共享内存来通信,而应该通过通信来共享内存。

这句话我花了好长时间才真正理解。简单说就是:与其用锁保护共享变量,不如把数据通过 channel 从一个 goroutine 传给另一个 goroutine,这样天然避免了数据竞争。

2.1 创建 channel

make创建:

ch := make(chan int) // 无缓冲 channel,只能传 int 类型

2.2 发送和接收

ch <- 42 // 发送数据到 channel value := <-ch // 从 channel 接收数据

箭头方向很直观:ch <-表示把数据塞进 channel,<-ch表示从 channel 取数据。

2.3 无缓冲 channel:同步阻塞

无缓冲 channel 的特点是发送和接收必须同时准备好,否则会阻塞。

看这个例子:

func main() { ch := make(chan int) go func() { fmt.Println("goroutine 准备发送") ch <- 100 // 发送后阻塞,直到有人接收 fmt.Println("发送完成") }() time.Sleep(time.Second) // 先让 goroutine 执行到发送 fmt.Println("main 准备接收") value := <-ch // 接收,唤醒发送者 fmt.Println("收到:", value) time.Sleep(time.Second) }

输出:

goroutine 准备发送 main 准备接收 收到: 100 发送完成

可以看到,发送者在接收者出现之前一直阻塞,这就是无缓冲 channel 的同步特性。这种特性天然适合用来做信号传递,比如“我完成了,你继续”。

2.4 有缓冲 channel:异步队列

创建时指定容量:

ch := make(chan int, 3) // 缓冲区大小 3

有缓冲 channel 就像一个队列,发送方在缓冲区满之前不会阻塞,接收方在缓冲区空之前不会阻塞。

ch <- 1 // 不会阻塞 ch <- 2 ch <- 3 // ch <- 4 // 缓冲区满了,会阻塞 fmt.Println(<-ch) // 1 fmt.Println(<-ch) // 2

有缓冲 channel 适用于异步通信,比如生产者消费者模型,生产者和消费者速度不匹配时可以暂时缓冲。

如何选择?我的经验是:除非有明确的性能需求,否则先用无缓冲 channel,因为无缓冲的同步语义更清晰,不容易写出隐藏的并发 bug。等需要优化吞吐量时再改成有缓冲。


3. select:监听多个 channel

如果同时要等待多个 channel,可以用select。它和switch语法类似,但每个 case 都是一个 channel 操作。

func main() { ch1 := make(chan string) ch2 := make(chan string) go func() { time.Sleep(time.Second) ch1 <- "从 ch1 来" }() go func() { time.Sleep(2 * time.Second) ch2 <- "从 ch2 来" }() select { case msg1 := <-ch1: fmt.Println(msg1) case msg2 := <-ch2: fmt.Println(msg2) } }

select会阻塞,直到某个 case 可以执行。如果多个 case 同时就绪,随机选择一个执行

3.1 default 分支:非阻塞操作

加上default后,如果所有 case 都阻塞,立刻执行 default:

select { case msg := <-ch: fmt.Println("收到:", msg) default: fmt.Println("没有消息,不阻塞") }

这个在写非阻塞 I/O超时控制时很有用。

3.2 超时控制

select { case msg := <-ch: fmt.Println(msg) case <-time.After(3 * time.Second): fmt.Println("超时了") }

time.After返回一个 channel,3 秒后会发送一个值,配合 select 实现超时等待。


4. channel 的关闭与遍历

4.1 关闭 channel

close函数关闭:

ch := make(chan int) go func() { for i := 0; i < 5; i++ { ch <- i } close(ch) // 发送完关闭 }() for v := range ch { // range 会自动在 channel 关闭后退出 fmt.Println(v) }

关闭规则

  • 只有发送方应该关闭 channel,接收方不要关闭。
  • 不能关闭已经关闭的 channel,会 panic。
  • 向已关闭的 channel 发送数据会 panic。
  • 从已关闭的 channel 接收数据,如果缓冲区还有数据,会先取完;取完后会得到零值,并且第二个返回值(ok)为 false。
v, ok := <-ch if !ok { fmt.Println("channel 已关闭且没有数据") }

我一开始总忘记检查ok,结果 channel 关闭后还一直收到零值,逻辑出错。这个要形成肌肉记忆。

4.2 判断 channel 是否关闭

没有直接的“isClosed”函数,只能用接收时的ok来判断。这是 Go 设计上的一个特点,避免竞态。


5. 几个必踩的坑

5.1 死锁

无缓冲 channel 在同一个 goroutine 里发送和接收会死锁

ch := make(chan int) ch <- 1 // 死锁!因为没有人接收,当前 goroutine 阻塞,程序崩溃 fmt.Println(<-ch)

报错:fatal error: all goroutines are asleep - deadlock!

所有 goroutine 都阻塞时,Go 运行时会检测到死锁并 panic。修复方法就是让发送和接收在不同的 goroutine 里,或者用有缓冲 channel。

5.2 向 nil channel 发送或接收

var ch chan int // nil channel ch <- 1 // 永久阻塞 <-ch // 永久阻塞

nil channel 是永久阻塞的,不会 panic,但也干不了活。这在 select 里其实有用,比如可以用一个 nil channel 暂时禁用某个 case。

5.3 循环变量捕获(Go 1.22 之前)

之前提过,这里再强调一下,因为并发场景下特别容易踩:

for i := 0; i < 5; i++ { go func() { fmt.Println(i) // 可能都打印 5 }() }

修复方法:在 goroutine 启动前复制变量:

for i := 0; i < 5; i++ { i := i // 复制 go func() { fmt.Println(i) }() }

或者通过参数传递:

for i := 0; i < 5; i++ { go func(n int) { fmt.Println(n) }(i) }

好在 Go 1.22 已经修复了,但如果面试被问到,能讲出这个历史问题和解决思路很加分。

5.4 goroutine 泄漏

如果 goroutine 因为某些原因永远阻塞,它就不会被回收,导致内存泄漏。比如:

func worker(ch chan int) { // 等待 channel 数据,但 channel 永远没人发,goroutine 永远阻塞 <-ch }

实际项目中,goroutine 泄漏通常发生在没有设置超时或者忘记关闭 channel的情况。排查方法用runtime.NumGoroutine()查看数量。


6. sync.WaitGroup:等待所有 goroutine 完成

虽然这篇主要讲 channel,但sync.WaitGroup也是并发必备工具,简单说两句。

var wg sync.WaitGroup for i := 0; i < 5; i++ { wg.Add(1) // 计数器 +1 go func(n int) { defer wg.Done() // 完成后计数器 -1 fmt.Println(n) }(i) } wg.Wait() // 阻塞,直到计数器为 0 fmt.Println("所有 goroutine 执行完毕")

AddDoneWait三件套。常用于等待一批 goroutine 完成。

Add要在 goroutine 启动前调用,不要在 goroutine 内部调用,否则可能Wait提前返回。


7. 小结

这篇把 goroutine 和 channel 的基础过了一遍,核心要点:

  • go关键字启动 goroutine,轻量、便宜,但需要管理生命周期。

  • channel 用于 goroutine 间通信,无缓冲同步,有缓冲异步。

  • select多路复用,配合time.After做超时。

  • 关闭 channel 要小心,只能发送方关闭,接收方用ok判断。

  • 死锁、nil channel、循环变量捕获、goroutine 泄漏都是新手常见坑。

  • sync.WaitGroup用于等待所有 goroutine 完成。

并发编程是 Go 的核心竞争力,也是面试必考。下一篇我准备写 Go 标准库里的几个常用包,比如sync的互斥锁、context的超时控制,以及实际项目中怎么组织并发代码。慢慢来,先把基础打牢。

如果这篇文章对你有帮助,欢迎点赞收藏,评论区一起交流。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/22 1:34:47

手把手把英雄联盟战绩查询工具 Seraphine 跑起来,三步装起来

手把手把英雄联盟战绩查询工具 Seraphine 跑起来&#xff0c;三步装起来 【免费下载链接】Seraphine 英雄联盟战绩查询工具 项目地址: https://gitcode.com/gh_mirrors/se/Seraphine Seraphine 是挂在英雄联盟客户端上的战绩查询工具&#xff0c;进 BP 自动拉队友和对手…

作者头像 李华
网站建设 2026/8/22 1:33:39

华数杯B题解题全攻略:从问题拆解到模型实现与论文撰写

1. 项目概述&#xff1a;从赛题到解题的完整路径又到了一年一度的华数杯国际赛&#xff0c;今年B题的题目一出来&#xff0c;就在我们几个老建模人之间引起了不小的讨论。这道题&#xff0c;说难不难&#xff0c;但想拿高分&#xff0c;思路的清晰度和细节的处理至关重要。很多…

作者头像 李华
网站建设 2026/8/22 1:32:43

408考研调度算法一图流:可视化总结与高效复习框架构建

这次我们来看一个专门为 408 计算机考研复习设计的调度算法笔记项目。它最大的特点不是讲解新算法&#xff0c;而是用“一图流”的方式&#xff0c;将操作系统、计算机组成原理等科目中繁杂的进程调度、内存调度、磁盘调度算法进行高度可视化总结&#xff0c;帮助考生快速抓住核…

作者头像 李华
网站建设 2026/8/22 1:30:48

markdown-it-vue 快速上手指南:一个 Vue 组件搞定 Markdown 渲染

markdown-it-vue 快速上手指南&#xff1a;一个 Vue 组件搞定 Markdown 渲染 【免费下载链接】markdown-it-vue The vue lib for markdown-it. 项目地址: https://gitcode.com/gh_mirrors/ma/markdown-it-vue markdown-it-vue 是一个基于 markdown-it 解析引擎的 Markdo…

作者头像 李华
网站建设 2026/8/22 1:29:41

AI漫剧制作工具实战指南:从环境配置到批量生产全流程解析

1. 先搞清楚“AI漫剧制作工具”到底能做什么&#xff0c;别被名字唬住看到“AI漫剧制作工具”这个名字&#xff0c;很多人第一反应可能是&#xff1a;这玩意儿是不是能一键把小说变成动画片&#xff1f;或者输入几个关键词就自动生成一部连续剧&#xff1f;我得先泼点冷水&…

作者头像 李华
网站建设 2026/8/22 1:26:45

JVS-Rules 实战解析:如何用三模块架构构建可审计、可配置的规则服务

本文以技术实现视角拆解 JVS-Rules 的设计原理&#xff0c;聚焦决策编排、规则表达、函数计算三大模块的技术闭环&#xff0c;说明其如何通过能力减法实现高频规则场景下的可配置性、跨系统一致性与全链路可追溯性&#xff0c;并提供可落地的集成验证方法。技术背景&#xff1a…

作者头像 李华