Go 学习笔记03:goroutine、channel、WaitGroup 与 Mutex 并发入门
2026/8/27 9:05:49 网站建设 项目流程

上一篇主要整理了 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 之间传递数据的通道。

创建一个传递整数的 channel:

ch := 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

还可以创建有缓冲的 channel:

ch := 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 20

close(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 读取 channel

channel 也可以配合range

for 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 的执行顺序

如果有多个defer

func 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加 1:

package 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 -> 同时处理多个 channel
time.After -> 实现简单的超时控制

Go 的并发部分比前面的基础语法更抽象,目前还只是掌握了基本概念和简单用法。

后面还需要通过实际项目继续巩固。

下一阶段准备开始学习 Go 的工程基础,包括:

package go mod 项目目录结构 JSON 文件读写 go test

之后再进入 HTTP、Gin 和 MySQL,开始真正编写 Go 后端项目。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询