2.1. Goroutine 深入理解

2.1.1. 什么是 Goroutine

Goroutine 是 Go 语言中的轻量级线程,由 Go 运行时管理。与操作系统线程相比,Goroutine 具有以下特点:

特性

OS 线程

Goroutine

内存占用

~1MB

~2KB

创建成本

切换成本

高(内核态)

低(用户态)

调度

OS 调度器

Go 运行时调度器

2.1.2. Goroutine 调度模型 (GMP)

Go 使用 GMP 模型 进行 Goroutine 调度:

  • G (Goroutine):Goroutine,包含栈、指令指针等信息

  • M (Machine):操作系统线程,执行 Goroutine 的载体

  • P (Processor):逻辑处理器,维护本地运行队列

        graph TD
    subgraph "Go Runtime"
        GQ[Global Queue]
        subgraph "P1"
            LQ1[Local Queue]
            G1[G1]
            G2[G2]
        end
        subgraph "P2"
            LQ2[Local Queue]
            G3[G3]
            G4[G4]
        end
        M1[M1 - OS Thread]
        M2[M2 - OS Thread]
    end
    
    P1 --> M1
    P2 --> M2
    GQ --> P1
    GQ --> P2
    

2.1.2.1. 设置 P 的数量

import "runtime"

func main() {
    // 获取当前 GOMAXPROCS
    n := runtime.GOMAXPROCS(0)
    fmt.Println("GOMAXPROCS:", n)
    
    // 设置 GOMAXPROCS(通常不需要手动设置)
    runtime.GOMAXPROCS(4)
}

2.1.3. Goroutine 的创建与生命周期

2.1.3.1. 基本用法

func main() {
    go func() {
        fmt.Println("Hello from goroutine")
    }()
    
    // 等待 goroutine 执行完毕
    time.Sleep(time.Second)
}

2.1.3.2. ⚠️ 常见陷阱:主 Goroutine 退出

// ❌ 错误示例:主 goroutine 退出,子 goroutine 被强制终止
func main() {
    go func() {
        time.Sleep(time.Second)
        fmt.Println("这行代码可能永远不会执行")
    }()
    // main 函数立即返回,程序退出
}

// ✅ 正确做法:使用 WaitGroup 等待
func main() {
    var wg sync.WaitGroup
    wg.Add(1)
    
    go func() {
        defer wg.Done()
        time.Sleep(time.Second)
        fmt.Println("正常执行")
    }()
    
    wg.Wait() // 等待所有 goroutine 完成
}

2.1.4. Goroutine 泄漏

Goroutine 泄漏是 Go 程序中最常见的问题之一。

2.1.4.1. 泄漏场景 1:Channel 阻塞

// ❌ 泄漏示例:channel 永远不会被读取
func leak1() {
    ch := make(chan int)
    go func() {
        ch <- 42 // 永远阻塞,goroutine 泄漏
    }()
    // 没有接收者
}

// ✅ 修复:使用 buffered channel 或确保有接收者
func fixed1() {
    ch := make(chan int, 1) // buffered channel
    go func() {
        ch <- 42
    }()
    // 或者确保读取
}

2.1.4.2. 泄漏场景 2:无限循环没有退出条件

// ❌ 泄漏示例:无法停止的 goroutine
func leak2() {
    go func() {
        for {
            doSomething()
            time.Sleep(time.Second)
        }
    }()
}

// ✅ 修复:使用 context 或 done channel
func fixed2(ctx context.Context) {
    go func() {
        for {
            select {
            case <-ctx.Done():
                return // 优雅退出
            default:
                doSomething()
                time.Sleep(time.Second)
            }
        }
    }()
}

2.1.4.3. 泄漏场景 3:select 中没有 default 或超时

// ❌ 泄漏示例:可能永远阻塞
func leak3(ch1, ch2 chan int) {
    go func() {
        select {
        case v := <-ch1:
            process(v)
        case v := <-ch2:
            process(v)
        // 如果两个 channel 都没有数据,永远阻塞
        }
    }()
}

// ✅ 修复:添加超时或 context
func fixed3(ctx context.Context, ch1, ch2 chan int) {
    go func() {
        select {
        case v := <-ch1:
            process(v)
        case v := <-ch2:
            process(v)
        case <-ctx.Done():
            return
        case <-time.After(5 * time.Second):
            return // 超时退出
        }
    }()
}

2.1.5. 检测 Goroutine 泄漏

2.1.5.1. 使用 runtime 包

func monitorGoroutines() {
    ticker := time.NewTicker(time.Second)
    for range ticker.C {
        fmt.Printf("Goroutines: %d\n", runtime.NumGoroutine())
    }
}

2.1.5.2. 使用 goleak 库(推荐)

import "go.uber.org/goleak"

func TestMain(m *testing.M) {
    goleak.VerifyTestMain(m)
}

func TestNoLeak(t *testing.T) {
    defer goleak.VerifyNone(t)
    // 测试代码
}

2.1.6. Goroutine 的栈增长

Go 的 Goroutine 使用动态栈,初始大小为 2KB,可以按需增长。

// 递归深度测试
func recursiveFunc(depth int) {
    if depth == 0 {
        return
    }
    var arr [1024]byte // 1KB 局部变量
    _ = arr
    recursiveFunc(depth - 1)
}

func main() {
    // 这会触发栈增长
    recursiveFunc(1000)
}

2.1.7. 最佳实践

2.1.7.1. 1. 总是考虑 Goroutine 如何退出

// 使用 context 控制生命周期
func worker(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("worker exiting")
            return
        default:
            // 工作逻辑
        }
    }
}

2.1.7.2. 2. 限制并发数量

// 使用 semaphore 限制并发
func limitedConcurrency(tasks []Task, limit int) {
    sem := make(chan struct{}, limit)
    var wg sync.WaitGroup
    
    for _, task := range tasks {
        wg.Add(1)
        sem <- struct{}{} // 获取信号量
        
        go func(t Task) {
            defer wg.Done()
            defer func() { <-sem }() // 释放信号量
            
            t.Execute()
        }(task)
    }
    
    wg.Wait()
}

2.1.7.3. 3. 使用 errgroup 管理一组 Goroutine

import "golang.org/x/sync/errgroup"

func fetchAll(ctx context.Context, urls []string) error {
    g, ctx := errgroup.WithContext(ctx)
    
    for _, url := range urls {
        url := url // 避免闭包陷阱
        g.Go(func() error {
            return fetch(ctx, url)
        })
    }
    
    return g.Wait() // 等待所有完成,返回第一个错误
}

2.1.8. 参考资源