[{"content":"Go 并发编程详细讲解 Go 的并发模型基于 CSP（Communicating Sequential Processes），核心思想是：不要通过共享内存来通信，而要通过通信来共享内存。\n下面从基础到进阶，系统讲解 Go 并发的所有核心概念，每个概念都配上可运行的示例。\n一、Goroutine：轻量级线程 1.1 基础用法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; ) func say(s string) { for i := 0; i \u0026lt; 3; i++ { fmt.Println(s, i) time.Sleep(100 * time.Millisecond) } } func main() { go say(\u0026#34;world\u0026#34;) // 启动一个 goroutine say(\u0026#34;hello\u0026#34;) // 主 goroutine 继续执行 time.Sleep(500 * time.Millisecond) // 等 goroutine 跑完 } 关键点：\ngo 关键字启动一个 goroutine，立即返回，不阻塞。 主 goroutine 结束时，所有其他 goroutine 都会被强制终止。所以 main 里需要 Sleep 或同步机制等待。 Goroutine 初始栈只有 2KB，可动态增长，所以可以轻松启动数十万个。 1.2 为什么 goroutine 比线程轻 对比项 线程 Goroutine 初始栈大小 1-8 MB 2 KB 切换成本 内核态切换，微秒级 用户态切换，纳秒级 数量级 几千 百万级 调度 操作系统 Go runtime（GMP 模型） 1.3 用 WaitGroup 等待 time.Sleep 不靠谱，生产代码用 sync.WaitGroup：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;sync\u0026#34; ) func main() { var wg sync.WaitGroup for i := 0; i \u0026lt; 5; i++ { wg.Add(1) // 计数 +1 go func(n int) { defer wg.Done() // 计数 -1（defer 保证一定执行） fmt.Println(\u0026#34;worker\u0026#34;, n) }(i) // 注意：i 作为参数传入，避免闭包捕获问题 } wg.Wait() // 阻塞直到计数归零 fmt.Println(\u0026#34;全部完成\u0026#34;) } 易错点：不要写成 go func() { ... }(i) 里的 i 直接捕获。Go 1.22 之前循环变量是共享的，需要用参数传入或 i := i 复制。Go 1.22 起每次迭代都是新变量，但显式传参更清晰。\n二、Channel：goroutine 之间的通信管道 Channel 是 Go 并发的核心。可以把它想象成一条类型安全的队列。\n2.1 无缓冲 Channel 1 2 3 4 5 6 7 ch := make(chan int) // 无缓冲 go func() { ch \u0026lt;- 42 // 发送：阻塞直到有人接收 }() v := \u0026lt;-ch // 接收：阻塞直到有人发送 fmt.Println(v) // 42 特点：发送和接收必须同时就绪，否则阻塞。这叫同步通信。\n2.2 有缓冲 Channel 1 2 3 4 5 6 7 8 9 10 ch := make(chan int, 3) // 缓冲大小 3 ch \u0026lt;- 1 // 不阻塞 ch \u0026lt;- 2 // 不阻塞 ch \u0026lt;- 3 // 不阻塞 // ch \u0026lt;- 4 // 会阻塞，缓冲满了 fmt.Println(\u0026lt;-ch) // 1 fmt.Println(\u0026lt;-ch) // 2 fmt.Println(\u0026lt;-ch) // 3 特点：缓冲未满时发送不阻塞，缓冲非空时接收不阻塞。缓冲满了发送阻塞，缓冲空了接收阻塞。\n2.3 关闭 Channel 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ch := make(chan int, 3) ch \u0026lt;- 1 ch \u0026lt;- 2 close(ch) // 方式一：range 自动结束 for v := range ch { fmt.Println(v) // 1, 2，然后循环自动结束 } // 方式二：逗号 ok 判断 v, ok := \u0026lt;-ch if !ok { fmt.Println(\u0026#34;channel 已关闭\u0026#34;) } 规则：\n只有发送方应该关闭 channel，接收方关闭会 panic。 关闭后不能发送，但可以继续接收剩余数据。 重复关闭会 panic。 向 nil channel 发送/接收会永久阻塞。 2.4 Channel 作为同步信号 1 2 3 4 5 6 7 8 9 done := make(chan struct{}) // struct{} 不占内存 go func() { // 做一些工作 fmt.Println(\u0026#34;工作完成\u0026#34;) close(done) // 或 done \u0026lt;- struct{}{} }() \u0026lt;-done // 等待完成 struct{} 是零大小类型，作为纯信号使用时比 bool 更省内存。\n2.5 Channel 方向 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func producer(out chan\u0026lt;- int) { // 只发送 for i := 0; i \u0026lt; 5; i++ { out \u0026lt;- i } close(out) } func consumer(in \u0026lt;-chan int) { // 只接收 for v := range in { fmt.Println(v) } } func main() { ch := make(chan int) go producer(ch) consumer(ch) } 方向约束让编译器帮你检查错误，是良好的 API 设计习惯。\n三、Select：多路复用 select 让一个 goroutine 同时等待多个 channel 操作。\n3.1 基础用法 1 2 3 4 5 6 7 8 9 10 select { case v := \u0026lt;-ch1: fmt.Println(\u0026#34;收到 ch1:\u0026#34;, v) case ch2 \u0026lt;- 42: fmt.Println(\u0026#34;发送到 ch2\u0026#34;) case \u0026lt;-time.After(time.Second): fmt.Println(\u0026#34;超时\u0026#34;) default: fmt.Println(\u0026#34;都没有就绪，立即执行\u0026#34;) } 规则：\n多个 case 同时就绪时，随机选一个（防止饥饿）。 有 default 时不阻塞；没有 default 时阻塞直到某个 case 就绪。 3.2 超时控制 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func fetchData() (string, error) { ch := make(chan string, 1) go func() { time.Sleep(2 * time.Second) // 模拟慢操作 ch \u0026lt;- \u0026#34;data\u0026#34; }() select { case v := \u0026lt;-ch: return v, nil case \u0026lt;-time.After(1 * time.Second): return \u0026#34;\u0026#34;, fmt.Errorf(\u0026#34;超时\u0026#34;) } } 3.3 非阻塞接收 1 2 3 4 5 6 select { case v := \u0026lt;-ch: fmt.Println(\u0026#34;收到:\u0026#34;, v) default: fmt.Println(\u0026#34;没有数据\u0026#34;) } 3.4 退出信号 1 2 3 4 5 6 7 8 9 10 11 12 func worker(stop \u0026lt;-chan struct{}) { for { select { case \u0026lt;-stop: fmt.Println(\u0026#34;收到退出信号\u0026#34;) return default: // 正常工作 time.Sleep(100 * time.Millisecond) } } } 四、Context：取消与超时 context 是 Go 官方推荐的跨 goroutine 传递取消信号和超时的机制。\n4.1 四种 Context 1 2 3 4 5 ctx := context.Background() // 根 context，永不取消 ctx, cancel := context.WithCancel(parent) // 手动取消 ctx, cancel := context.WithTimeout(parent, 3*time.Second) // 超时取消 ctx, cancel := context.WithDeadline(parent, time.Now().Add(3*time.Second)) // 截止时间 defer cancel() // 必须调用，防止资源泄漏 4.2 取消传播 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 func main() { ctx, cancel := context.WithCancel(context.Background()) go worker(ctx, \u0026#34;A\u0026#34;) go worker(ctx, \u0026#34;B\u0026#34;) time.Sleep(2 * time.Second) cancel() // 取消信号传播到所有子 goroutine time.Sleep(500 * time.Millisecond) } func worker(ctx context.Context, name string) { for { select { case \u0026lt;-ctx.Done(): fmt.Println(name, \u0026#34;退出:\u0026#34;, ctx.Err()) return default: fmt.Println(name, \u0026#34;工作中\u0026#34;) time.Sleep(500 * time.Millisecond) } } } 4.3 配合 HTTP 请求 1 2 3 4 5 6 7 8 9 10 func callAPI(ctx context.Context) error { req, _ := http.NewRequestWithContext(ctx, \u0026#34;GET\u0026#34;, \u0026#34;https://api.example.com\u0026#34;, nil) resp, err := http.DefaultClient.Do(req) if err != nil { return err // 如果 ctx 被取消，err 会是 context.Canceled } defer resp.Body.Close() // ... return nil } 4.4 Context 使用原则 原则 说明 作为函数第一个参数 func Do(ctx context.Context, ...) 不放进 struct Context 应该随调用链传递，不是对象状态 只用于取消、超时、传值 不要用它传业务参数 cancel() 必须调用 defer cancel() 防止 goroutine 泄漏 context.Value 少用 只用于 request-scoped 的元数据（如 traceID、用户身份） 五、sync 包：共享内存同步 虽然 Go 提倡用 channel 通信，但有些场景用共享内存更自然。\n5.1 Mutex：互斥锁 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 type Counter struct { mu sync.Mutex n int } func (c *Counter) Inc() { c.mu.Lock() defer c.mu.Unlock() c.n++ } func (c *Counter) Value() int { c.mu.Lock() defer c.mu.Unlock() return c.n } 5.2 RWMutex：读写锁 读多写少的场景，允许多个读者并发：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 type Cache struct { mu sync.RWMutex m map[string]string } func (c *Cache) Get(k string) (string, bool) { c.mu.RLock() // 读锁，可多个 goroutine 同时持有 defer c.mu.RUnlock() v, ok := c.m[k] return v, ok } func (c *Cache) Set(k, v string) { c.mu.Lock() // 写锁，独占 defer c.mu.Unlock() c.m[k] = v } 5.3 Once：只执行一次 1 2 3 4 5 6 7 8 9 10 11 var ( instance *DB once sync.Once ) func GetDB() *DB { once.Do(func() { instance = \u0026amp;DB{} // 只执行一次，其他 goroutine 阻塞等待 }) return instance } 单例模式的标准实现，比手动加锁更安全。\n5.4 sync.Map：并发安全的 Map 普通 map 并发读写会 panic。sync.Map 适合读多写少、key 相对固定的场景：\n1 2 3 4 5 6 7 8 9 var m sync.Map m.Store(\u0026#34;key\u0026#34;, \u0026#34;value\u0026#34;) v, ok := m.Load(\u0026#34;key\u0026#34;) m.Delete(\u0026#34;key\u0026#34;) m.Range(func(k, v any) bool { fmt.Println(k, v) return true }) 注意：sync.Map 不是万能替代。大部分场景用 sync.RWMutex + map 性能更好。sync.Map 只在特定场景（如缓存、注册表）有优势。\n5.5 sync.Pool：对象复用 减少 GC 压力，适合频繁创建销毁的对象：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 var bufPool = sync.Pool{ New: func() any { return new(bytes.Buffer) }, } func process() { buf := bufPool.Get().(*bytes.Buffer) defer func() { buf.Reset() bufPool.Put(buf) }() buf.WriteString(\u0026#34;data\u0026#34;) // ... } 注意：sync.Pool 里的对象可能在任意 GC 时被回收，不能用于持久化状态。\n六、经典并发模式 6.1 Worker Pool（工作池） 控制并发数量，避免资源耗尽：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 func workerPool() { jobs := make(chan int, 100) results := make(chan int, 100) // 启动 3 个 worker var wg sync.WaitGroup for w := 0; w \u0026lt; 3; w++ { wg.Add(1) go func() { defer wg.Done() for j := range jobs { results \u0026lt;- j * j // 处理任务 } }() } // 发送任务 go func() { for i := 0; i \u0026lt; 10; i++ { jobs \u0026lt;- i } close(jobs) }() // 等待 worker 完成后关闭 results go func() { wg.Wait() close(results) }() // 收集结果 for r := range results { fmt.Println(r) } } 6.2 Fan-out / Fan-in（扇出扇入） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // Fan-out：一个输入分发给多个 worker // Fan-in：多个 worker 输出汇聚成一个 channel func fanIn(chs ...\u0026lt;-chan int) \u0026lt;-chan int { out := make(chan int) var wg sync.WaitGroup for _, ch := range chs { wg.Add(1) go func(c \u0026lt;-chan int) { defer wg.Done() for v := range c { out \u0026lt;- v } }(ch) } go func() { wg.Wait() close(out) }() return out } 6.3 Pipeline（流水线） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 func gen(nums ...int) \u0026lt;-chan int { out := make(chan int) go func() { for _, n := range nums { out \u0026lt;- n } close(out) }() return out } func square(in \u0026lt;-chan int) \u0026lt;-chan int { out := make(chan int) go func() { for n := range in { out \u0026lt;- n * n } close(out) }() return out } func main() { // gen → square → 输出 for v := range square(gen(1, 2, 3, 4, 5)) { fmt.Println(v) // 1, 4, 9, 16, 25 } } 6.4 限流（Semaphore） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func limited() { sem := make(chan struct{}, 3) // 最多 3 个并发 var wg sync.WaitGroup for i := 0; i \u0026lt; 10; i++ { wg.Add(1) go func(n int) { defer wg.Done() sem \u0026lt;- struct{}{} // 获取令牌 defer func() { \u0026lt;-sem }() // 释放令牌 fmt.Println(\u0026#34;处理\u0026#34;, n) time.Sleep(time.Second) }(i) } wg.Wait() } 用 golang.org/x/sync/semaphore 可以支持带权重的限流。\n6.5 errgroup：带错误传播的并发 1 2 3 4 5 6 7 8 9 10 11 12 13 import \u0026#34;golang.org/x/sync/errgroup\u0026#34; 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) // 任一失败会取消 ctx }) } return g.Wait() // 返回第一个错误 } errgroup 是并发任务编排的利器，比手写 WaitGroup + channel 简洁得多。\n七、数据竞争与检测 7.1 竞争示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 func main() { n := 0 var wg sync.WaitGroup for i := 0; i \u0026lt; 1000; i++ { wg.Add(1) go func() { defer wg.Done() n++ // 数据竞争！ }() } wg.Wait() fmt.Println(n) // 通常小于 1000 } 7.2 用 -race 检测 1 2 go run -race main.go go test -race ./... -race 会在运行时检测数据竞争，输出冲突的读写位置。生产构建不要开 -race（性能损失约 10 倍），但开发和 CI 一定要开。\n7.3 修复方式 方式一：加锁\n1 2 3 4 var mu sync.Mutex mu.Lock() n++ mu.Unlock() 方式二：用 atomic\n1 2 var n int64 atomic.AddInt64(\u0026amp;n, 1) 方式三：用 channel\n1 2 3 ch := make(chan int) go func() { ch \u0026lt;- 1 }() n += \u0026lt;-ch 八、GMP 调度模型 理解 Go 调度的核心：\n组件 含义 G (Goroutine) 用户态协程，包含栈、指令指针等 M (Machine) 操作系统线程 P (Processor) 逻辑处理器，持有可运行 G 的本地队列 调度流程：\n每个 P 有一个本地运行队列，M 绑定 P 后从中取 G 执行。 本地队列空时，M 会从全局队列或其他 P 的队列窃取 G（work stealing）。 G 遇到阻塞系统调用时，M 会释放 P，让其他 M 绑定 P 继续执行。 网络 I/O 由 netpoller 处理，不阻塞 M。 关键参数：\nGOMAXPROCS：P 的数量，默认等于 CPU 核数。 runtime.NumGoroutine()：当前 goroutine 数量，用于排查泄漏。 九、常见坑与最佳实践 9.1 Goroutine 泄漏 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // 错误：goroutine 永远阻塞 func bad() { ch := make(chan int) go func() { ch \u0026lt;- 42 // 没人接收，永久阻塞 }() // 函数返回，goroutine 泄漏 } // 正确：用 context 或 buffered channel func good(ctx context.Context) { ch := make(chan int, 1) // 缓冲 1，发送不阻塞 go func() { select { case ch \u0026lt;- 42: case \u0026lt;-ctx.Done(): } }() } 9.2 循环变量捕获 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // Go 1.22 之前：所有 goroutine 打印 5 for i := 0; i \u0026lt; 5; i++ { go func() { fmt.Println(i) }() } // 修复方式一：传参 for i := 0; i \u0026lt; 5; i++ { go func(n int) { fmt.Println(n) }(i) } // 修复方式二：局部变量 for i := 0; i \u0026lt; 5; i++ { i := i go func() { fmt.Println(i) }() } Go 1.22 起循环变量每次迭代都是新的，不再有此问题。\n9.3 关闭已关闭的 Channel 1 2 3 // 错误：重复关闭 panic close(ch) close(ch) // panic: close of closed channel 用 sync.Once 保证只关一次：\n1 2 var once sync.Once once.Do(func() { close(ch) }) 9.4 向已关闭的 Channel 发送 1 2 close(ch) ch \u0026lt;- 1 // panic: send on closed channel 规则：只有发送方关闭，且关闭后不再发送。\n9.5 用 Buffered Channel 避免泄漏 如果 goroutine 发送后可能没人接收，用缓冲 channel：\n1 2 ch := make(chan int, 1) // 缓冲 1，发送方不会阻塞 go func() { ch \u0026lt;- 42 }() 9.6 超时与重试 1 2 3 4 5 6 7 8 9 10 11 12 13 func retry(ctx context.Context, fn func() error) error { for i := 0; i \u0026lt; 3; i++ { if err := fn(); err == nil { return nil } select { case \u0026lt;-ctx.Done(): return ctx.Err() case \u0026lt;-time.After(time.Duration(i+1) * time.Second): } } return fmt.Errorf(\u0026#34;重试 3 次仍失败\u0026#34;) } 十、决策指南：用 Channel 还是 Mutex 场景 推荐 传递数据所有权 Channel 分发任务给多个 worker Channel 通知事件（signal） Channel 保护共享状态（计数器、缓存） Mutex / RWMutex 单例初始化 sync.Once 对象池 sync.Pool 并发 map sync.Map 或 RWMutex + map 简单判断：如果你在问\u0026quot;谁拥有这份数据\u0026quot;，用 Channel；如果你在问\u0026quot;谁能同时访问这份数据\u0026quot;，用 Mutex。\n十一、AI 应用中的并发模式 结合你之前的项目，几个实际场景：\n11.1 流式响应 + 超时 1 2 3 4 5 6 7 8 9 func streamWithTimeout(ctx context.Context, client *llm.Client) error { ctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() _, err := client.ChatStream(ctx, msgs, func(tok string) { fmt.Print(tok) }) return err } 11.2 并发调用多个模型对比 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 func compareModels(ctx context.Context, prompt string, models []string) (map[string]string, error) { g, ctx := errgroup.WithContext(ctx) results := sync.Map{} for _, m := range models { m := m g.Go(func() error { client := llm.NewClient(baseURL, apiKey, m, httpClient) reply, err := client.Chat(ctx, []llm.Message{{Role: \u0026#34;user\u0026#34;, Content: prompt}}) if err != nil { return err } results.Store(m, reply) return nil }) } if err := g.Wait(); err != nil { return nil, err } out := map[string]string{} results.Range(func(k, v any) bool { out[k.(string)] = v.(string) return true }) return out, nil } 11.3 Token 计数限流 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 type TokenLimiter struct { sem chan struct{} } func NewTokenLimiter(maxConcurrent int) *TokenLimiter { return \u0026amp;TokenLimiter{sem: make(chan struct{}, maxConcurrent)} } func (l *TokenLimiter) Acquire(ctx context.Context) error { select { case l.sem \u0026lt;- struct{}{}: return nil case \u0026lt;-ctx.Done(): return ctx.Err() } } func (l *TokenLimiter) Release() { \u0026lt;-l.sem } 十二、总结 主题 核心要点 Goroutine 轻量、go 关键字、主 goroutine 退出会杀死所有 Channel 无缓冲同步、有缓冲异步、关闭规则、方向约束 Select 多路复用、超时、default 非阻塞 Context 取消传播、超时、作为第一个参数 sync 包 Mutex/RWMutex/Once/Pool/Map 经典模式 Worker Pool、Fan-in/out、Pipeline、Semaphore、errgroup 数据竞争 用 -race 检测，加锁或 atomic 修复 GMP G=协程、M=线程、P=逻辑处理器，work stealing 常见坑 goroutine 泄漏、循环变量、重复关闭、死锁 决策 传数据用 channel，护状态用 mutex 一句话：Go 并发的精髓是 \u0026ldquo;用 channel 传递数据，用 context 传递取消，用 sync 保护状态\u0026rdquo;。理解这三者的适用场景，就能写出健壮的并发程序。\n","permalink":"https://zwdart.pages.dev/posts/goroutine/","summary":"\u003ch1 id=\"go-并发编程详细讲解\"\u003eGo 并发编程详细讲解\u003c/h1\u003e\n\u003cp\u003eGo 的并发模型基于 \u003cstrong\u003eCSP（Communicating Sequential Processes）\u003c/strong\u003e，核心思想是：\u003cstrong\u003e不要通过共享内存来通信，而要通过通信来共享内存。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e下面从基础到进阶，系统讲解 Go 并发的所有核心概念，每个概念都配上可运行的示例。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"一goroutine轻量级线程\"\u003e一、Goroutine：轻量级线程\u003c/h2\u003e\n\u003ch3 id=\"11-基础用法\"\u003e1.1 基础用法\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e15\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e16\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e17\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e18\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e19\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e20\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kn\"\u003epackage\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nx\"\u003emain\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kn\"\u003eimport\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\t\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;fmt\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\t\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;time\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003esay\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003es\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\t\u003c/span\u003e\u003cspan class=\"k\"\u003efor\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nx\"\u003ei\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e:=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nx\"\u003ei\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"mi\"\u003e3\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nx\"\u003ei\u003c/span\u003e\u003cspan class=\"o\"\u003e++\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\t\t\u003c/span\u003e\u003cspan class=\"nx\"\u003efmt\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003ePrintln\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nx\"\u003es\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nx\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\t\t\u003c/span\u003e\u003cspan class=\"nx\"\u003etime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eSleep\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e100\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nx\"\u003etime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eMillisecond\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\t\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003efunc\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003emain\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\t\u003c/span\u003e\u003cspan class=\"k\"\u003ego\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003esay\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;world\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e   \u003c/span\u003e\u003cspan class=\"c1\"\u003e// 启动一个 goroutine\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\t\u003c/span\u003e\u003cspan class=\"nf\"\u003esay\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;hello\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e      \u003c/span\u003e\u003cspan class=\"c1\"\u003e// 主 goroutine 继续执行\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\t\u003c/span\u003e\u003cspan class=\"nx\"\u003etime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nf\"\u003eSleep\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e500\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nx\"\u003etime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"nx\"\u003eMillisecond\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"c1\"\u003e// 等 goroutine 跑完\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e\u003cstrong\u003e关键点\u003c/strong\u003e：\u003c/p\u003e","title":"Goroutine详细：并发编程详细讲解"}]