Go 最大的卖点就是并发。一个 go 关键字就能启动一个协程,几万个协程也就几十 MB 内存。比 Java 的线程轻量太多了。
goroutine
go
package main
import (
"fmt"
"time"
)
func sayHello(name string) {
for i := 0; i < 3; i++ {
fmt.Printf("Hello from %s\n", name)
time.Sleep(100 * time.Millisecond)
}
}
func main() {
go sayHello("goroutine") // 启动一个 goroutine
sayHello("main") // main 函数自己也在跑
}
go 关键字 + 函数调用 = 启动一个 goroutine。就这么简单。
输出会是交替的(两个 goroutine 并发执行)。
goroutine 很轻
go
func main() {
for i := 0; i < 100000; i++ {
go func(n int) {
// 10 万个 goroutine
}(i)
}
time.Sleep(time.Second)
}
10 万个 goroutine 大约占 100MB 内存(每个 goroutine 初始栈只有 2KB)。Java 里 10 万个线程?直接 OOM。
channel --- goroutine 之间的通信
Go 的并发哲学是"不要通过共享内存来通信,而是通过通信来共享内存"。
go
// 创建 channel
ch := make(chan int) // 无缓冲
ch := make(chan int, 10) // 有缓冲(容量 10)
// 发送
ch <- 42
// 接收
value := <-ch
基本用法
go
func producer(ch chan int) {
for i := 0; i < 5; i++ {
ch <- i
fmt.Println("produced:", i)
}
close(ch) // 生产完了,关闭 channel
}
func consumer(ch chan int) {
for v := range ch { // 从 channel 读取,直到关闭
fmt.Println("consumed:", v)
time.Sleep(200 * time.Millisecond)
}
}
func main() {
ch := make(chan int, 5)
go producer(ch)
go consumer(ch)
time.Sleep(2 * time.Second)
}
select 多路复用
go
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(100 * time.Millisecond)
ch1 <- "from ch1"
}()
go func() {
time.Sleep(200 * time.Millisecond)
ch2 <- "from ch2"
}()
// 等待任意一个 channel 有数据
select {
case msg := <-ch1:
fmt.Println(msg)
case msg := <-ch2:
fmt.Println(msg)
case <-time.After(500 * time.Millisecond):
fmt.Println("timeout")
}
}
select 类似 switch,但是监听的是 channel 操作。哪个 channel 先有数据就走哪个 case。
WaitGroup --- 等待一组 goroutine 完成
go
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
fmt.Println("goroutine", n, "done")
time.Sleep(time.Duration(n) * 100 * time.Millisecond)
}(i)
}
wg.Wait() // 阻塞,直到所有 wg.Done() 被调用
fmt.Println("all done!")
}
Mutex --- 保护共享资源
虽然 Go 推荐用 channel 通信,但有时候还是需要对共享变量加锁:
go
type Counter struct {
mu sync.Mutex
count int
}
func (c *Counter) Increment() {
c.mu.Lock()
defer c.mu.Unlock() // 函数退出时自动解锁
c.count++
}
func (c *Counter) Get() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
func main() {
counter := &Counter{}
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Increment()
}()
}
wg.Wait()
fmt.Println("count:", counter.Get()) // 1000
}
常见模式
生产者-消费者
go
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
// 3 个 worker
for w := 0; w < 3; w++ {
go func(id int) {
for j := range jobs {
fmt.Printf("worker %d processing job %d\n", id, j)
time.Sleep(200 * time.Millisecond)
results <- j * 2
}
}(w)
}
// 发送 5 个任务
for j := 0; j < 5; j++ {
jobs <- j
}
close(jobs)
// 收集结果
for r := 0; r < 5; r++ {
fmt.Println("result:", <-results)
}
}
超时控制
go
func fetchWithTimeout(url string, timeout time.Duration) (string, error) {
ch := make(chan string, 1)
go func() {
// 模拟 HTTP 请求
time.Sleep(2 * time.Second)
ch <- "response data"
}()
select {
case result := <-ch:
return result, nil
case <-time.After(timeout):
return "", fmt.Errorf("request timeout after %v", timeout)
}
}
总结
Go 并发三板斧:goroutine(轻量协程)、channel(协程间通信)、select(多路复用)。加锁用 sync.Mutex,等待用 sync.WaitGroup。goroutine 很便宜,随便起;channel 是 Go 并发的灵魂,优先用 channel 通信而不是共享内存加锁。