Goroutine、Channel、Select——Go 并发的核心原语
一、Goroutine
1.1 什么是 Goroutine
Goroutine 是 Go 运行时管理的轻量级线程。创建成本极低(约 2KB 栈空间),可以轻松创建成千上万个。
// 启动一个 goroutine:在函数调用前加 go 关键字
go doSomething()
go func() {
fmt.Println("running in goroutine")
}()
1.2 基本使用
package main
import (
"fmt"
"time"
)
func printNumbers() {
for i := 1; i <= 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Printf("number: %d\n", i)
}
}
func printLetters() {
for i := 'a'; i <= 'e'; i++ {
time.Sleep(150 * time.Millisecond)
fmt.Printf("letter: %c\n", i)
}
}
func main() {
go printNumbers() // 并发执行
go printLetters() // 并发执行
time.Sleep(2 * time.Second) // 等待 goroutine 完成
fmt.Println("done")
}
// 输出交错出现:number: 1, letter: a, number: 2, letter: b...
1.3 主 Goroutine 与 WaitGroup
上面用 time.Sleep 等待是不可靠的。正确做法是用 sync.WaitGroup:
import "sync"
func main() {
var wg sync.WaitGroup
wg.Add(2) // 声明要等待的 goroutine 数量
go func() {
defer wg.Done() // 完成时通知 WaitGroup
printNumbers()
}()
go func() {
defer wg.Done()
printLetters()
}()
wg.Wait() // 阻塞直到所有 goroutine 完成
fmt.Println("done")
}
重要:main goroutine 退出时,所有其他 goroutine 会被强制终止。不要让 main 提前退出。
二、Channel
Channel 是 goroutine 之间的通信管道,用于传递数据和同步。
2.1 基本用法
// 创建 channel
ch := make(chan int) // 无缓冲 channel(同步)
ch := make(chan int, 5) // 有缓冲 channel(异步,容量5)
// 发送数据
ch <- 42 // 将 42 发送到 channel
// 接收数据
value := <-ch // 从 channel 接收数据
<-ch // 丢弃接收到的值
// 关闭 channel
close(ch) // 发送方关闭,接收方可以继续读取剩余数据
// 只有发送方应该关闭 channel,接收方关闭会 panic
2.2 无缓冲 Channel(同步)
// 无缓冲 channel 是同步的:发送方阻塞直到接收方准备好
func main() {
ch := make(chan string)
go func() {
ch <- "hello from goroutine" // 发送,阻塞等待接收
}()
msg := <-ch // 接收,阻塞等待发送
fmt.Println(msg)
}
2.3 有缓冲 Channel(异步)
// 有缓冲 channel:缓冲区满时发送方阻塞,空时接收方阻塞
ch := make(chan int, 3) // 缓冲区大小为 3
ch <- 1 // 不阻塞
ch <- 2 // 不阻塞
ch <- 3 // 不阻塞
// ch <- 4 // 阻塞!缓冲区已满
fmt.Println(<-ch) // 1
fmt.Println(<-ch) // 2
fmt.Println(<-ch) // 3
2.4 Channel 方向
// 只能发送的 channel
func producer(ch chan<- int) {
ch <- 42
close(ch)
}
// 只能接收的 channel
func consumer(ch <-chan int) {
for v := range ch {
fmt.Println(v)
}
}
// 双向 channel 可以隐式转换为单向 channel
ch := make(chan int)
go producer(ch) // chan int -> chan<- int
go consumer(ch) // chan int -> <-chan int
2.5 遍历 Channel
// range 遍历 channel,直到 channel 关闭
ch := make(chan int, 5)
ch <- 1
ch <- 2
ch <- 3
close(ch)
for v := range ch {
fmt.Println(v) // 1, 2, 3
}
// channel 关闭后 range 自动退出
三、Select 多路复用
select 用于同时监听多个 channel 操作,类似 switch 但针对 channel。
3.1 基本用法
select {
case msg := <-ch1:
fmt.Println("received from ch1:", msg)
case msg := <-ch2:
fmt.Println("received from ch2:", msg)
case ch3 <- value:
fmt.Println("sent to ch3")
default:
fmt.Println("no channel ready")
}
3.2 超时控制
func fetchData(ch chan string) string {
select {
case result := <-ch:
return result
case <-time.After(3 * time.Second):
return "timeout"
}
}
3.3 非阻塞操作
// default 分支使 select 成为非阻塞
select {
case msg := <-ch:
fmt.Println("got:", msg)
default:
fmt.Println("no message available")
}
3.4 done channel 模式
func worker(done chan bool) {
fmt.Println("working...")
time.Sleep(time.Second)
done <- true // 通知完成
}
func main() {
done := make(chan bool)
go worker(done)
select {
case <-done:
fmt.Println("worker finished")
case <-time.After(5 * time.Second):
fmt.Println("timeout waiting for worker")
}
}
四、经典并发模式
4.1 Fan-Out / Fan-In
// Fan-Out:多个 goroutine 从同一 channel 读取
// Fan-In:多个 goroutine 写入同一 channel
func fanOut(input <-chan int, workers int) []<-chan int {
channels := make([]<-chan int, workers)
for i := 0; i < workers; i++ {
channels[i] = process(input)
}
return channels
}
func fanIn(channels ...<-chan int) <-chan int {
var wg sync.WaitGroup
merged := make(chan int)
for _, ch := range channels {
wg.Add(1)
go func(c <-chan int) {
defer wg.Done()
for v := range c {
merged <- v
}
}(ch)
}
go func() {
wg.Wait()
close(merged)
}()
return merged
}
4.2 工作池(Worker Pool)
func workerPool(jobs <-chan Job, results chan<- Result, numWorkers int) {
var wg sync.WaitGroup
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for job := range jobs {
result := process(job)
results <- result
}
}(i)
}
wg.Wait()
close(results)
}
4.3 Context 超时与取消
import "context"
func longOperation(ctx context.Context) error {
select {
case <-time.After(10 * time.Second):
fmt.Println("operation completed")
return nil
case <-ctx.Done():
return ctx.Err() // context canceled 或 deadline exceeded
}
}
// 使用
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
err := longOperation(ctx)
if err != nil {
fmt.Println("operation canceled:", err)
}
五、sync 包常用工具
5.1 Mutex 互斥锁
import "sync"
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
func (c *SafeCounter) Get() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
5.2 RWMutex 读写锁
type Cache struct {
mu sync.RWMutex
data map[string]string
}
func (c *Cache) Get(key string) (string, bool) {
c.mu.RLock() // 读锁(多个读可以并发)
defer c.mu.RUnlock()
v, ok := c.data[key]
return v, ok
}
func (c *Cache) Set(key, value string) {
c.mu.Lock() // 写锁(独占)
defer c.mu.Unlock()
c.data[key] = value
}
5.3 Once — 只执行一次
var once sync.Once
var instance *Database
func GetDB() *Database {
once.Do(func() {
instance = connectDB() // 只会执行一次,即使多个 goroutine 并发调用
})
return instance
}
5.4 atomic — 原子操作
import "sync/atomic"
var count int64
// 原子加操作,无需锁
atomic.AddInt64(&count, 1)
atomic.LoadInt64(&count)
atomic.StoreInt64(&count, 0)
六、并发陷阱
6.1 Race Condition(竞态条件)
// 多个 goroutine 同时读写共享变量 = 数据竞争
count := 0
for i := 0; i < 1000; i++ {
go func() {
count++ // 数据竞争!
}()
}
// 正确:使用 atomic 或 channel 或 Mutex
var count int64
for i := 0; i < 1000; i++ {
go func() {
atomic.AddInt64(&count, 1)
}()
}
// 检测竞态条件
// go run -race main.go
// go test -race ./...
6.2 Goroutine 泄漏
// goroutine 永远阻塞,无法退出 = 泄漏
func leak() {
ch := make(chan int)
go func() {
ch <- 42 // 没有人接收,永远阻塞
}()
}
// 正确:使用 context 或 done channel 确保 goroutine 可以退出
func noLeak(ctx context.Context) {
ch := make(chan int)
go func() {
select {
case ch <- 42:
case <-ctx.Done():
return // context 取消时退出
}
}()
}
6.3 关闭已关闭的 Channel
ch := make(chan int)
close(ch)
// close(ch) // panic: close of closed channel
// 如果不确定是否已关闭,不要关闭 channel
// 或者只由发送方关闭
七、本节要点速查
| 概念 | 要点 |
|---|---|
| goroutine | go func() 启动,轻量级(~2KB) |
| WaitGroup | Add/Done/Wait 等待 goroutine 完成 |
| 无缓冲 channel | make(chan T) 同步通信 |
| 有缓冲 channel | make(chan T, n) 异步通信 |
| channel 方向 | chan<- 只发,<-chan 只收 |
| select | 多路复用,可加 default/timeout |
| Context | 超时控制与取消传播 |
| Mutex | Lock/Unlock 互斥锁 |
| RWMutex | 多读单写锁 |
| Once | 只执行一次(单例模式) |
| atomic | 无锁原子操作 |
go run -race | 检测竞态条件 |
上一篇:08-包与模块 下一篇:10-测试与工程实践
豫公网安备41108202000982号