在 Go 语言中,Goroutines
(协程)和 Channels
(通道)是并发编程的核心组件。它们共同协作,简化了并发任务的管理和数据同步。以下通过详细示例说明它们的用法和常见模式。
1. Goroutines(协程)
Goroutine 是轻量级线程,由 Go 运行时调度,启动成本极低(通常仅几 KB 内存)。
基本用法
通过 go
关键字启动一个 Goroutine:
package mainimport ("fmt""time"
)func printNumbers() {for i := 1; i <= 3; i++ {fmt.Println("Number:", i)time.Sleep(100 * time.Millisecond)}
}func printLetters() {for c := 'a'; c <= 'c'; c++ {fmt.Println("Letter:", string(c))time.Sleep(100 * time.Millisecond)}
}func main() {go printNumbers() // 启动 Goroutinego printLetters()// 主 Goroutine 等待其他协程执行time.Sleep(1 * time.Second)fmt.Println("Main Goroutine 结束")
}
输出(顺序可能不同):
Letter: a
Number: 1
Number: 2
Letter: b
Number: 3
Letter: c
Main Goroutine 结束
2. Channels(通道)
Channel 是类型化的管道,用于 Goroutines 之间的通信和同步。
基本用法
func worker(done chan bool) {fmt.Println("Worker 开始工作...")time.Sleep(1 * time.Second)fmt.Println("Worker 完成工作")done <- true // 发送完成信号
}func main() {done := make(chan bool) // 创建布尔型通道go worker(done)<-done // 阻塞,直到接收到数据fmt.Println("主程序收到完成信号")
}
输出:
Worker 开始工作...
Worker 完成工作
主程序收到完成信号
3. 缓冲通道(Buffered Channels)
允许在没有接收者时缓存一定数量的数据。
func main() {messages := make(chan string, 2) // 缓冲容量为 2messages <- "消息1" // 不阻塞(缓存未满)messages <- "消息2"fmt.Println(<-messages) // 输出: 消息1fmt.Println(<-messages) // 输出: 消息2
}
4. 通道方向(Channel Direction)
限制通道在函数中的使用方式(只读或只写)。
// 只写通道参数
func sendData(ch chan<- string, msg string) {ch <- msg
}// 只读通道参数
func receiveData(ch <-chan string) {fmt.Println("收到消息:", <-ch)
}func main() {ch := make(chan string)go sendData(ch, "Hello")receiveData(ch)
}
输出:
收到消息: Hello
5. Select 语句
监听多个通道操作,处理第一个就绪的通道。
func main() {ch1 := make(chan string)ch2 := make(chan string)go func() {time.Sleep(1 * time.Second)ch1 <- "来自 ch1"}()go func() {time.Sleep(2 * time.Second)ch2 <- "来自 ch2"}()for i := 0; i < 2; i++ {select {case msg1 := <-ch1:fmt.Println(msg1)case msg2 := <-ch2:fmt.Println(msg2)}}
}
输出:
来自 ch1
来自 ch2
6. 关闭通道与遍历通道
通过 close
关闭通道,通过 range
遍历通道数据。
func produceNumbers(ch chan int) {for i := 0; i < 5; i++ {ch <- i}close(ch) // 关闭通道
}func main() {ch := make(chan int)go produceNumbers(ch)// 循环读取直到通道关闭for num := range ch {fmt.Println("收到数字:", num)}
}
输出:
收到数字: 0
收到数字: 1
收到数字: 2
收到数字: 3
收到数字: 4
7. 超时与错误处理
结合 select
和 time.After
实现超时控制。
func main() {ch := make(chan string)go func() {time.Sleep(3 * time.Second)ch <- "数据"}()select {case res := <-ch:fmt.Println("收到数据:", res)case <-time.After(2 * time.Second):fmt.Println("超时!")}
}
输出:
超时!
8. Worker Pool(工作池)
使用缓冲通道和多个 Goroutines 构建任务处理池。
func worker(id int, jobs <-chan int, results chan<- int) {for job := range jobs {fmt.Printf("Worker %d 开始处理任务 %d\n", id, job)time.Sleep(1 * time.Second)results <- job * 2}
}func main() {jobs := make(chan int, 10)results := make(chan int, 10)// 启动 3 个 Workerfor w := 1; w <= 3; w++ {go worker(w, jobs, results)}// 发送 5 个任务for j := 1; j <= 5; j++ {jobs <- j}close(jobs)// 收集结果for a := 1; a <= 5; a++ {<-results}
}
输出:
Worker 1 开始处理任务 1
Worker 2 开始处理任务 2
Worker 3 开始处理任务 3
Worker 1 开始处理任务 4
Worker 2 开始处理任务 5
总结
- Goroutines:
- 通过
go
关键字启动。 - 轻量级,适合高并发场景。
- 通过
- Channels:
- 同步通信:无缓冲通道需发送和接收同时就绪。
- 异步通信:缓冲通道允许暂存数据。
- 使用
close
关闭通道,range
遍历通道数据。
- 高级模式:
select
多路监听。- 超时控制、工作池、只读/只写通道。
- 注意事项:
- 避免死锁(如未关闭的通道或未接收的数据)。
- 使用
sync.WaitGroup
等待多个 Goroutines 完成。