# Go Goroutine

Goroutine 是 Go 语言并发模型的核心。它是一种由 Go 运行时调度的轻量级线程，创建成本极低（初始栈仅 2KB），可以轻松创建数十万个并发执行。通过 `go` 关键字即可启动一个 goroutine，让函数在后台并发执行。

## 并发 vs 并行

理解并发和并行的区别是学习 goroutine 的前提：

- **并发（Concurrency）**：同时处理多件事（关注结构设计），是任务的逻辑组合
- **并行（Parallelism）**：同时执行多件事（关注运行时执行），需要多核 CPU 支持

Go 的设计理念是"并发编程更容易"。通过 goroutine 和 channel，Go 让开发者可以用简洁的方式构建并发程序，运行时会自动将并发任务映射到可用的 CPU 核心上并行执行。

## go 关键字启动 goroutine

在函数调用前加上 `go` 关键字，即可在新的 goroutine 中执行该函数。

```go
package main

import (
	"fmt"
	"time"
)

func sayHello(name string) {
	for i := 0; i < 3; i++ {
		fmt.Printf("Hello from %s (goroutine)\n", name)
		time.Sleep(100 * time.Millisecond)
	}
}

func main() {
	// 启动 goroutine
	go sayHello("goroutine-1")
	go sayHello("goroutine-2")

	// 主 goroutine 也在执行
	for i := 0; i < 3; i++ {
		fmt.Println("Hello from main")
		time.Sleep(100 * time.Millisecond)
	}

	// 等待 goroutine 完成（实际项目中应使用 WaitGroup）
	time.Sleep(500 * time.Millisecond)
}
```

每个 goroutine 都是独立执行的。主 goroutine（`main` 函数）退出时，程序结束，其他 goroutine 也会被终止。

## goroutine 的栈空间

goroutine 的栈初始只有 2KB（OS 线程通常是 1-8MB），并且可以动态增长和收缩。这使得 Go 可以轻松创建大量 goroutine。

```go
package main

import (
	"fmt"
	"runtime"
	"sync"
)

func printGoroutineInfo(id int, wg *sync.WaitGroup) {
	defer wg.Done()
	fmt.Printf("Goroutine %d 正在执行\n", id)
}

func main() {
	// 查看当前 goroutine 数量
	fmt.Printf("初始 goroutine 数: %d\n", runtime.NumGoroutine())

	var wg sync.WaitGroup

	// 创建 100 个 goroutine
	for i := 0; i < 100; i++ {
		wg.Add(1)
		go printGoroutineInfo(i, &wg)
	}

	fmt.Printf("创建后 goroutine 数: %d\n", runtime.NumGoroutine())

	wg.Wait()
	fmt.Printf("完成后 goroutine 数: %d\n", runtime.NumGoroutine())
}
```

100 个 goroutine 仅占用约 200KB 的栈空间，而相同数量的 OS 线程则需要数百 MB。

## runtime.GOMAXPROCS

`GOMAXPROCS` 控制可以同时执行用户代码的 OS 线程数量，默认等于 CPU 核心数。

```go
package main

import (
	"fmt"
	"runtime"
	"time"
)

func worker(id int, duration time.Duration) {
	start := time.Now()
	for time.Since(start) < duration {
		// 模拟计算密集型工作
	}
	fmt.Printf("Worker %d 完成\n", id)
}

func main() {
	fmt.Printf("CPU 核心数: %d\n", runtime.NumCPU())
	fmt.Printf("GOMAXPROCS: %d\n", runtime.GOMAXPROCS(0)) // 0 表示查询当前值

	// 设置 GOMAXPROCS（通常不需要手动设置）
	runtime.GOMAXPROCS(4)
	fmt.Printf("设置后 GOMAXPROCS: %d\n", runtime.GOMAXPROCS(0))

	// 启动 8 个 worker
	for i := 0; i < 8; i++ {
		go worker(i, 100*time.Millisecond)
	}

	time.Sleep(200 * time.Millisecond)
}
```

在大多数情况下，保持默认的 `GOMAXPROCS` 即可。Go 运行时会自动管理线程调度。

## GMP 调度模型

Go 的 goroutine 调度使用 GMP 模型：

![GMP调度模型](https://img.zhaojq.top/20260729161728667.png "GMP调度模型")

- **G（Goroutine）**：用户态的轻量级线程，包含执行栈、状态等信息
- **M（Machine）**：操作系统线程，由 OS 调度，负责执行 G
- **P（Processor）**：逻辑处理器，维护一个本地 goroutine 队列。P 的数量等于 `GOMAXPROCS`

调度流程：
1. 每个 P 维护一个本地 G 队列
2. M 必须绑定 P 才能执行 G
3. 当 M 执行 G 发生阻塞（如系统调用）时，M 和 P 分离，P 会绑定新的 M
4. **工作窃取**：空闲的 P 会从其他 P 的队列中"窃取" G 来执行，实现负载均衡

```go
package main

import (
	"fmt"
	"runtime"
	"sync"
)

func main() {
	// 查看调度器统计信息
	var stats runtime.MemStats
	runtime.ReadMemStats(&stats)

	fmt.Printf("Goroutine 数量: %d\n", runtime.NumGoroutine())
	fmt.Printf("CPU 核心数: %d\n", runtime.NumCPU())
	fmt.Printf("CGo 调用次数: %d\n", stats.NumCgoCall)

	// 通过 trace 包可以查看调度细节
	// go tool trace 可以可视化 GMP 调度过程
	var wg sync.WaitGroup
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			fmt.Printf("Goroutine %d 在运行\n", id)
		}(i)
	}
	wg.Wait()
}
```

## sync.WaitGroup 等待 goroutine 完成

`sync.WaitGroup` 是等待一组 goroutine 完成的标准方式。

```go
package main

import (
	"fmt"
	"sync"
	"time"
)

func downloadFile(id int, wg *sync.WaitGroup) {
	defer wg.Done() // 函数结束时计数器减 1

	fmt.Printf("开始下载文件 %d\n", id)
	time.Sleep(time.Duration(id*100) * time.Millisecond) // 模拟下载
	fmt.Printf("文件 %d 下载完成\n", id)
}

func main() {
	var wg sync.WaitGroup

	// 启动多个下载任务
	for i := 1; i <= 5; i++ {
		wg.Add(1) // 计数器加 1
		go downloadFile(i, &wg)
	}

	// 等待所有 goroutine 完成
	wg.Wait()
	fmt.Println("所有文件下载完成！")
}
```

`WaitGroup` 有三个方法：
- `Add(delta int)`：计数器增加 delta
- `Done()`：等价于 `Add(-1)`
- `Wait()`：阻塞直到计数器归零

注意：`Add` 应在 goroutine 启动之前调用，避免竞态条件。

## goroutine 泄漏问题

goroutine 泄漏是指 goroutine 永远无法退出，持续占用资源。常见原因包括：

```go
package main

import (
	"fmt"
	"runtime"
	"time"
)

// 泄漏示例 1：channel 阻塞（发送后无人接收）
func leakByChannel() {
	ch := make(chan int)
	go func() {
		ch <- 1 // 永远阻塞，无人接收
	}()
}

// 泄漏示例 2：无限循环无退出条件
func leakByLoop() {
	go func() {
		for {
			// 没有退出条件，永远运行
			time.Sleep(time.Second)
		}
	}()
}

// 正确做法：使用 context 或 done channel 控制退出
func safeGoroutine(done <-chan struct{}) {
	go func() {
		for {
			select {
			case <-done:
				fmt.Println("goroutine 安全退出")
				return
			default:
				// 执行工作
				time.Sleep(100 * time.Millisecond)
			}
		}
	}()
}

func main() {
	leakByChannel()
	leakByLoop()

	done := make(chan struct{})
	safeGoroutine(done)

	time.Sleep(300 * time.Millisecond)
	fmt.Printf("当前 goroutine 数: %d\n", runtime.NumGoroutine())

	close(done) // 通知 goroutine 退出
	time.Sleep(200 * time.Millisecond)
	fmt.Printf("退出后 goroutine 数: %d\n", runtime.NumGoroutine())
}
```

防止 goroutine 泄漏的最佳实践：
1. 为每个 goroutine 设计明确的退出机制（done channel 或 context）
2. 使用 `context.WithTimeout` 设置超时
3. 使用 `runtime.NumGoroutine()` 监控 goroutine 数量
4. 避免在 goroutine 中向无接收者的 channel 发送数据

## 总结

Goroutine 是 Go 并发的基石。它由 Go 运行时调度，初始栈仅 2KB，可以轻松创建数十万个。通过 `go` 关键字启动，使用 `sync.WaitGroup` 等待完成。Go 的 GMP 调度模型将 goroutine 高效映射到 OS 线程上执行，支持工作窃取实现负载均衡。使用 goroutine 时需注意避免泄漏，始终为 goroutine 提供退出机制。goroutine 配合 channel 构成了 Go 强大的并发编程能力。

