Go 学习笔记03:goroutine、channel、WaitGroup 与 Mutex 并发入门

📅 2026/8/27 9:06:00
Go 学习笔记03:goroutine、channel、WaitGroup 与 Mutex 并发入门
上一篇主要整理了 Go 中函数、struct、指针和 interface 的基础用法。这一篇继续记录 Go 中非常有特色的一部分并发编程。本篇主要整理目前已经学习过的几个知识点goroutine channel 缓冲 channel close WaitGroup defer Mutex select timeout这些内容以后在 Go 后端、网络编程以及并发任务处理中都会经常遇到。一、goroutine 是什么goroutine 可以先简单理解成让一个函数并发执行。普通调用func work() { fmt.Println(working) } func main() { work() }这里work()会按照正常顺序执行。如果加上go work()就表示启动一个新的 goroutine 来执行work()。例如package main import fmt func work() { fmt.Println(working) } func main() { go work() }需要注意如果main()太快结束新的 goroutine 可能还没来得及执行整个程序就已经退出了。所以 goroutine 启动之后通常还需要配合其他机制进行等待或同步。二、channel 是什么channel 可以理解成goroutine 之间传递数据的通道。创建一个传递整数的 channelch : make(chan int)发送数据ch - 10接收数据x : -ch例如package main import fmt func main() { ch : make(chan int) go func() { ch - 20 }() x : -ch fmt.Println(x) }输出20这里ch - 20表示把20放进 channel。而x : -ch表示从 channel 取出一个值。三、channel 还可以用于等待如果x : -ch执行时 channel 里暂时没有数据那么当前 goroutine 会在这里等待。直到另一个 goroutine执行ch - 100程序才会继续。所以 channel 不仅能传递数据还能起到一定的同步作用。四、无缓冲 channel下面这种ch : make(chan int)叫做无缓冲 channel。它的特点是发送和接收通常需要配合进行。例如ch : make(chan int) ch - 10如果没有其他 goroutine 来接收数据那么ch - 10可能会一直阻塞。正确示例ch : make(chan int) go func() { ch - 10 }() fmt.Println(-ch)输出10五、有缓冲 channel还可以创建有缓冲的 channelch : make(chan int, 2)这里的2表示 channel 最多可以暂时存放 2 个整数。例如ch : make(chan int, 2) ch - 10 ch - 20 fmt.Println(-ch) fmt.Println(-ch)输出10 20可以先理解为[10, 20]先放进去的值先被取出来。如果缓冲区已经满了再继续发送数据就会阻塞直到有人先取走数据。六、关闭 channel可以使用close(ch)关闭 channel。需要注意关闭 channel 并不会立即清空里面已经存在的数据。例如ch : make(chan int, 2) ch - 10 ch - 20 close(ch) fmt.Println(-ch) fmt.Println(-ch)仍然可以得到10 20close(ch)更准确的意思是告诉接收方以后不会再发送新的数据了。七、判断 channel 是否已经读取完可以这样接收value, ok : -ch其中value - 读到的数据 ok - 是否还能正常读取例如ch : make(chan int, 1) ch - 8 close(ch) value, ok : -ch此时value 8 ok true如果数据已经全部读取完再读取一次value 0 ok false八、使用 range 读取 channelchannel 也可以配合rangefor value : range ch { fmt.Println(value) }例如ch : make(chan int, 3) ch - 1 ch - 2 ch - 3 close(ch) for value : range ch { fmt.Println(value) }输出1 2 3这个循环会一直读取直到channel 已经关闭并且里面的数据全部读取完。所以如果使用for value : range ch一般要注意 channel 是否会被关闭否则循环可能一直等待。九、WaitGroup 是什么如果启动了多个 goroutine有时希望主程序等它们全部执行结束。这时候可以使用sync.WaitGroup例如var wg sync.WaitGroup常见三个操作wg.Add(1) wg.Done() wg.Wait()可以理解为Add(n) - 登记 n 个任务 Done() - 当前任务完成 Wait() - 等待所有任务完成例如package main import ( fmt sync ) func main() { var wg sync.WaitGroup wg.Add(2) go func() { fmt.Println(任务1) wg.Done() }() go func() { fmt.Println(任务2) wg.Done() }() wg.Wait() fmt.Println(全部完成) }任务1和任务2的先后顺序不一定固定。但全部完成一定会在两个任务都结束之后输出。十、defer 的基础用法defer可以先理解成把某个操作延迟到当前函数结束之前执行。例如func test() { defer fmt.Println(B) fmt.Println(A) }输出A B因为defer fmt.Println(B)不会马上执行而是在函数结束之前执行。十一、多个 defer 的执行顺序如果有多个deferfunc test() { defer fmt.Println(A) defer fmt.Println(B) defer fmt.Println(C) fmt.Println(D) }输出D C B A多个defer是后进先出。也就是最后登记的defer最先执行。十二、defer 和 WaitGroup 配合实际代码里常写go func() { defer wg.Done() fmt.Println(working) }()这样可以保证当前 goroutine 结束时执行wg.Done()相比把wg.Done()手动放在函数最后更不容易忘记。十三、Mutex 是什么多个 goroutine 同时修改同一个变量时可能出现数据竞争。例如count看起来只有一行但实际可以理解成读取 count 加 1 写回 count如果多个 goroutine 同时进行就可能导致结果不准确。这时候可以使用sync.Mutex也就是互斥锁。基本写法mu.Lock() count mu.Unlock()可以理解为Lock() - 我现在开始修改其他 goroutine 先等一下 Unlock() - 我修改完成了其他 goroutine 可以继续十四、WaitGroup 和 Mutex 综合使用例如启动 5 个 goroutine每个都让count加 1package main import ( fmt sync ) func main() { var wg sync.WaitGroup var mu sync.Mutex count : 0 for i : 0; i 5; i { wg.Add(1) go func() { defer wg.Done() mu.Lock() count mu.Unlock() }() } wg.Wait() fmt.Println(count) }最终输出5这里WaitGroup负责等待 5 个 goroutine 全部完成。而Mutex负责保证count不会被多个 goroutine 同时修改。十五、goroutine 把结果通过 channel 返回例如func square(n int, ch chan int) { ch - n * n }调用ch : make(chan int) go square(5, ch) result : -ch fmt.Println(result)输出25这里square()在新的 goroutine 中进行计算。然后通过ch - n * n把结果传回主 goroutine。十六、并发任务完成顺序不固定例如go square(2, ch) go square(3, ch) a : -ch b : -ch最终得到的两个结果一定是4 9但a 4, b 9和a 9, b 4都有可能。原因是goroutine 的启动顺序不代表它们一定按照这个顺序完成。这是学习并发时比较重要的一点。十七、select 是什么select用来处理多个 channel。例如select { case msg1 : -ch1: fmt.Println(msg1) case msg2 : -ch2: fmt.Println(msg2) }可以理解为同时等待多个 channel哪个先准备好就先处理哪个。如果ch1先有数据就执行第一个case。如果ch2先有数据就执行第二个。十八、select 中的 default例如select { case value : -ch: fmt.Println(value) default: fmt.Println(no data) }如果当前 channel 没有数据那么不会一直等待而是直接执行no data所以default可以理解为如果当前没有任何 case 可以立即执行就执行 default。需要注意default不是最后一定会执行。只要有其他case可以执行就不会进入default。十九、select timeout 超时控制实际开发中经常不能一直等待某个任务。这时可以使用time.After()例如select { case msg : -ch: fmt.Println(msg) case -time.After(2 * time.Second): fmt.Println(timeout) }意思是等待 channel 的数据但最多等待 2 秒。如果 2 秒内收到数据就处理数据。如果 2 秒仍然没有收到就输出timeout二十、超时控制完整例子package main import ( fmt time ) func worker(ch chan string) { time.Sleep(1 * time.Second) ch - 任务完成 } func main() { ch : make(chan string) go worker(ch) select { case msg : -ch: fmt.Println(msg) case -time.After(2 * time.Second): fmt.Println(timeout) } }因为任务只需要等待 1 秒而超时时间是 2 秒所以最终输出任务完成如果把time.Sleep(1 * time.Second)改成time.Sleep(3 * time.Second)那么超时时间会先到最终输出timeout二十一、目前容易混淆的几个地方1. goroutine 和 channel 不是同一个东西goroutine 是执行任务的并发单元。channel 是goroutine 之间传递数据的通道。可以简单理解成goroutine - 干活的人 channel - 传递数据的通道2. goroutine 不是进程一开始容易把go func()理解成“创建新进程”。实际上更准确的说法是启动新的 goroutine。goroutine 是 Go 运行时管理的轻量级并发任务并不是操作系统进程。3. 启动顺序不等于完成顺序例如go task1() go task2()并不能保证task1 一定先完成 task2 一定后完成这也是并发程序需要特别注意的地方。4. WaitGroup 和 Mutex 作用不同WaitGroup等待多个 goroutine 全部完成。Mutex防止多个 goroutine 同时修改共享数据。它们解决的是两个不同的问题。二十二、学习总结这一阶段主要学习了 Go 的并发基础。目前我对几个核心概念的理解是goroutine - 并发执行任务channel - goroutine 之间传递数据WaitGroup - 等待多个 goroutine 完成Mutex - 保护共享数据避免数据竞争defer - 函数结束前执行指定操作select - 同时处理多个 channeltime.After - 实现简单的超时控制Go 的并发部分比前面的基础语法更抽象目前还只是掌握了基本概念和简单用法。后面还需要通过实际项目继续巩固。下一阶段准备开始学习 Go 的工程基础包括package go mod 项目目录结构 JSON 文件读写 go test之后再进入 HTTP、Gin 和 MySQL开始真正编写 Go 后端项目。