Go初级之七:并发与Goroutine

发布于:2025-09-04 ⋅ 阅读:(22) ⋅ 点赞:(0)

Go初级之七:并发与Goroutine” 是 Go 语言学习系列教程中的第七讲,主要介绍 Go 语言中非常核心且强大的特性——并发编程(Concurrency)Goroutine。以下是该主题的详细讲解内容,适合初学者理解。


🌟 一、什么是并发(Concurrency)?

并发是指多个任务在同一时间段内交替执行(不一定是同时),它强调的是程序的结构设计,能够处理多个任务的逻辑。

Go 语言原生支持并发,通过 GoroutineChannel 实现,语法简洁、性能高效。

⚠️ 注意:并发(concurrency) ≠ 并行(parallelism)

  • 并发:多个任务交替执行,可能在单核上完成
  • 并行:多个任务同时执行,通常需要多核支持

🚀 二、Goroutine 是什么?

Goroutine 是 Go 运行时管理的轻量级线程,由 Go Runtime 调度,开销极小。

  • 启动一个 Goroutine 只需要在函数调用前加 go 关键字
  • 比操作系统线程更轻量(初始栈仅 2KB)
  • 数量可以成千上万,不会导致系统崩溃

✅ 示例:启动一个 Goroutine

package main

import (
    "fmt"
    "time"
)

func sayHello() {
    fmt.Println("Hello from Goroutine!")
}

func main() {
    go sayHello() // 启动一个 Goroutine
    time.Sleep(100 * time.Millisecond) // 主协程等待,否则程序可能提前退出
    fmt.Println("Main function")
}

🔍 输出可能是:

Hello from Goroutine!
Main function

也可能是:

Main function
Hello from Goroutine!

因为 Goroutine 是异步执行的。


⏳ 三、主 Goroutine 与子 Goroutine

  • main() 函数运行在 主 Goroutine
  • 当主 Goroutine 结束时,所有其他 Goroutine 都会被强制终止
  • 所以要确保主 Goroutine 不要过早退出

❌ 错误示例(子 Goroutine 来不及执行)

func main() {
    go sayHello()
    // 没有等待,主 Goroutine 立即结束
}

✅ 正确做法:使用 time.Sleepsync.WaitGroup

方法一:使用 time.Sleep(不推荐,仅用于演示)
time.Sleep(1 * time.Second)
方法二:使用 sync.WaitGroup(推荐)
package main

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

var wg sync.WaitGroup

func worker(id int) {
    defer wg.Done() // 任务完成,通知 WaitGroup
    fmt.Printf("Worker %d starting\n", id)
    time.Sleep(1 * time.Second)
    fmt.Printf("Worker %d done\n", id)
}

func main() {
    for i := 1; i <= 3; i++ {
        wg.Add(1)           // 增加计数器
        go worker(i)        // 启动 Goroutine
    }
    wg.Wait() // 等待所有 Goroutine 完成
    fmt.Println("All workers done.")
}

✅ 输出:

Worker 1 starting
Worker 2 starting
Worker 3 starting
Worker 1 done
Worker 2 done
Worker 3 done
All workers done.

🔗 四、Goroutine 与闭包的陷阱

for 循环中启动 Goroutine 时,要注意变量捕获问题。

❌ 常见错误:共享变量

func main() {
    for i := 0; i < 3; i++ {
        go func() {
            fmt.Println(i) // 所有 Goroutine 都打印 3
        }()
    }
    time.Sleep(1 * time.Second)
}

❗ 原因:i 是同一个变量,被所有闭包共享

✅ 正确做法:传参或局部变量

for i := 0; i < 3; i++ {
    go func(num int) {
        fmt.Println(num)
    }(i) // 立即传值
}

或:

for i := 0; i < 3; i++ {
    i := i // 创建局部副本
    go func() {
        fmt.Println(i)
    }()
}

🧵 五、Goroutine 的适用场景

  • 处理大量并发请求(如 Web 服务器)
  • 并行计算(如数据处理)
  • 定时任务
  • 监听事件(如信号、网络连接)

示例:并发请求多个 URL

package main

import (
    "fmt"
    "net/http"
    "sync"
)

func fetch(url string, wg *sync.WaitGroup) {
    defer wg.Done()
    resp, err := http.Get(url)
    if err != nil {
        fmt.Printf("Error fetching %s: %v\n", url, err)
        return
    }
    defer resp.Body.Close()
    fmt.Printf("Fetched %s -> Status: %s\n", url, resp.Status)
}

func main() {
    urls := []string{
        "https://httpbin.org/delay/1",
        "https://httpbin.org/status/200",
        "https://httpbin.org/json",
    }

    var wg sync.WaitGroup
    for _, url := range urls {
        wg.Add(1)
        go fetch(url, &wg)
    }
    wg.Wait()
    fmt.Println("All requests completed.")
}

🧠 六、Goroutine 的注意事项

问题 建议
主 Goroutine 提前退出 使用 sync.WaitGroup 等待
数据竞争(Data Race) 使用 mutexchannel 同步
大量 Goroutine 泄露 使用 context 控制生命周期
调试困难 使用 go run -race 检测竞态条件

✅ 七、小结

特性 说明
go 关键字 启动一个 Goroutine
轻量 每个 Goroutine 初始栈小,可创建成千上万
调度器 Go Runtime 自动调度,无需手动管理线程
协作式 + 抢占式 高效利用 CPU
配合 channel 实现安全的通信与同步

📚 下一讲预告:Go初级之八:Channel 与并发通信

我们将学习:

  • 什么是 Channel
  • 如何用 Channel 在 Goroutine 之间通信
  • 缓冲 Channel、关闭 Channel
  • select 语句处理多个 Channel

📝 练习题(巩固理解)

  1. 写一个程序,启动 5 个 Goroutine,每个打印自己的编号(1~5),并等待全部完成。
  2. 修改上面的程序,让每个 Goroutine 执行一个耗时任务(如 sleep 500ms)。
  3. 尝试不用 WaitGroup,只用 time.Sleep,观察结果是否稳定。
  4. for 循环中启动 Goroutine 打印 i,故意制造闭包问题并修复。


网站公告

今日签到

点亮在社区的每一天
去签到