--- tags: [go, golang, interview, code-questions] create time: 2026-06-07 14:30 --- # 编程算法面试题 💻 ## 概述 本文件涵盖 Go 并发编程场景下的 9 道代码实现题,涉及协程同步、通道协作、并发控制等实战场景。这些题目在面试中通常要求"现场手写代码",考察对 Channel、Mutex、WaitGroup 的综合运用能力。 > [!tip] 💡 使用建议 > 每道题先自己思考解题思路,再对照答案学习。重点理解"为什么用这种方式"而非死记代码。 ## 关联笔记 - [[hzh/GolangStar/Go面试题库/Channel面试题]] — Channel 原理 - [[hzh/GolangStar/Go面试题库/Sync面试题]] — Mutex/WaitGroup 原理 - [[hzh/GolangStar/Go面试题库/Context面试题]] — Context 超时控制 ## 正文 ### Q1:100 个协程顺序打印 1-1000 🟡中等 > [!question] ❓ 思考一下 > 100 个协程同时运行,如何让它们按 1-1000 的顺序依次打印?你会用什么机制来协调它们的执行顺序? ## 参考答案 **核心思路**:用 map 为每个协程分配独立的 channel,主循环按序发送信号到对应协程的 channel,通过阻塞保证顺序。 ```go func main() { s := make(chan struct{}) // 控制打印顺序的信号 m := make(map[int]chan int, 100) // 每个协程的入口 channel for i := 1; i <= 100; i++ { m[i] = make(chan int) go func(id int) { for { num := <-m[id] // 等待属于自己的数字 fmt.Println(num) s <- struct{}{} // 打印完成,通知主循环 } }(i) } for i := 1; i <= 1000; i++ { id := i % 100 if id == 0 { id = 100 } m[id] <- i // 发送给对应协程 <-s // 阻塞等待该协程打印完毕 } } ``` > [!note] 📝 解析思路 > - **map + channel 组合**:每个协程独占一个 channel,主循环精确投递 > - **信号量 s**:每次只发一个数,收到打印完成的信号才发下一个,保证严格顺序 > - **面试官可能会追问**:"如果不用 map 怎么做?"→ 可以用 100 个独立声明的 channel 或用 `sync.Mutex` 保护共享输出 --- ### Q2:三个 goroutine 交替打印 abc 10 次 🟡中等 ## 参考答案 **核心思路**:三个 channel 串联成环形,a -> b -> c -> a。 ```go func main() { ch1 := make(chan struct{}) // a → b ch2 := make(chan struct{}) // b → c ch3 := make(chan struct{}) // c → a var wg sync.WaitGroup wg.Add(3) // 打印 a go func() { defer wg.Done() for i := 0; i < 10; i++ { <-ch1 fmt.Print("a") ch2 <- struct{}{} } <-ch1 // 消费最后一次信号 }() // 打印 b go func() { defer wg.Done() for i := 0; i < 10; i++ { <-ch2 fmt.Print("b") ch3 <- struct{}{} } }() // 打印 c go func() { defer wg.Done() for i := 0; i < 10; i++ { <-ch3 fmt.Print("c") ch1 <- struct{}{} } }() ch1 <- struct{}{} // 启动信号 wg.Wait() close(ch1) close(ch2) close(ch3) fmt.Println("end") } ``` > [!warning] ⚠️ 高频陷阱 > 第 10 次打印 c 后会给 ch1 发信号,但 a 的循环已经结束了。需要额外消费一次 ch1 防止阻塞。 > [!tip] 💡 面试技巧 > 回答时可以画图展示三个 channel 的环形关系,然后说明"每次只有收到信号的 goroutine 才能执行,执行完给下一个发信号"。 --- ### Q3:不超过 10 个 goroutine 不重复打印 slice 中的 100 个元素 🟡中等 ## 参考答案 **方案一**:有缓冲 channel 控制并发度(无序) ```go ch := make(chan struct{}, 10) // 容量 10 = 最大并发数 for i := 0; i < 100; i++ { wg.Add(1) ch <- struct{}{} // 写满 10 个后阻塞 go func(idx int) { defer wg.Done() fmt.Printf("%d\n", ss[idx]) <-ch // 打印完释放一个槽位 }(i) } ``` **方案二**:固定 10 个 goroutine 顺序打印 ```go hashMap := make(map[int]chan int) // 10 个 channel 对应 10 个 goroutine sort := make(chan struct{}) // 控制顺序 for i := 0; i < 10; i++ { hashMap[i] = make(chan int) go func(idx int) { for val := range hashMap[idx] { fmt.Printf("go %d: %d\n", idx, val) sort <- struct{}{} // 打印完通知 } }(i) } for _, v := range ss { id := v % 10 hashMap[id] <- v <-sort // 等当前打印完再发下一个 } ``` > [!info] 🔗 延伸阅读 > - 方案一适合不需要顺序的场景(吞吐量优先) > - 方案二适合需要顺序的场景(可控性优先) --- ### Q4:两个协程交替打印奇偶数 🟢简单 ## 参考答案 ```go chan1 := make(chan struct{}) // 偶数协程 go func() { for i := 0; i < 10; i++ { chan1 <- struct{}{} // 发出信号 if i%2 == 0 { fmt.Println("偶数:", i) } } }() // 奇数协程 go func() { for i := 0; i < 10; i++ { <-chan1 // 等待信号 if i%2 == 1 { fmt.Println("奇数:", i) } } }() ``` --- ### Q5:用单个 channel 实现 0,1 交替打印 🟢简单 ## 参考答案 ```go msg := make(chan struct{}) go func() { for { <-msg // 接收信号 fmt.Println("0") msg <- struct{}{} // 回传信号 } }() go func() { for { <-msg // 接收信号 fmt.Println("1") msg <- struct{}{} // 回传信号 } }() msg <- struct{}{} // 初始信号 ``` > [!tip] 💡 面试技巧 > "用一个 channel 作为信号灯,0 和 1 各持一半——谁拿到灯谁就打印,打印完把灯放回 channel。" --- ### Q6:sync.Cond 实现多生产者多消费者 🟡中等 ## 参考答案 ```go var cond sync.Cond cond.L = new(sync.Mutex) msgCh := make(chan int, 5) // 生产者:缓冲区满时等待 producer := func(ctx context.Context, out chan<- int, idx int) { defer wg.Done() for { select { case <-ctx.Done(): cond.Broadcast() return default: cond.L.Lock() for len(msgCh) == 5 { cond.Wait() // 缓冲区满,等待 } out <- rand.Intn(500) cond.Signal() // 唤醒一个消费者 cond.L.Unlock() } } } // 消费者:缓冲区空时等待 consumer := func(ctx context.Context, in <-chan int, idx int) { defer wg.Done() for { select { case <-ctx.Done(): for len(msgCh) > 0 { /* 继续消费 */ } return default: cond.L.Lock() for len(msgCh) == 0 { cond.Wait() // 缓冲区空,等待 } num := <-in cond.Signal() // 唤醒一个生产者 cond.L.Unlock() } } } ``` > [!note] 📝 解析思路 > `sync.Cond` 的核心是:**Lock + Wait/Signal/Broadcast**。Wait 会释放锁并挂起 goroutine,Signal 唤醒一个等待者,Broadcast 唤醒全部。适合"条件满足才执行"的场景。 --- ### Q7:1000 个并发控制 + 1 秒超时 🟡中等 ## 参考答案 ```go tasks := make(chan int, 1000) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() var wg sync.WaitGroup for i := 0; i < 1000; i++ { wg.Add(1) tasks <- i go func(id int) { defer wg.Done() select { case <-ctx.Done(): return // 超时退出 default: fmt.Printf("goroutine %d\n", id) } }(i) } <-ctx.Done() fmt.Println("exec done") close(tasks) wg.Wait() ``` > [!warning] ⚠️ 高频陷阱 > 这段代码的缺陷是 1000 个 goroutine 几乎同时启动,1 秒内可能来不及全部执行完。实际场景中应限制并发数量(如用 semaphore channel)。 > [!tip] 💡 面试技巧 > 如果面试官指出问题,可以回答:"可以用有缓冲 channel 做信号量控制并发数:`sem := make(chan struct{}, 10)`,获取/释放 sem 来控制最多 10 个并发。" --- ### Q8:两个 Goroutine 交替打印字母与数字 a1b2c3 🟡中等 ## 参考答案 ```go numCh := make(chan struct{}) // 通知打印数字 strCh := make(chan struct{}) // 通知打印字母 var wg sync.WaitGroup wg.Add(2) // 打印字符 a-z go func() { defer wg.Done() for i := 'a'; i <= 'z'; i++ { fmt.Print(string(i)) numCh <- struct{}{} // 通知打印数字 <-strCh // 等待对方完成 } }() // 打印数字 1-26 go func() { defer wg.Done() for i := 1; i <= 26; i++ { <-numCh // 等待对方完成 fmt.Print(i) strCh <- struct{}{} // 通知打印字母 } }() wg.Wait() ``` --- ### Q9:限制 10 个 goroutine 执行,执行完一个放一个新进来 🟢简单 ## 参考答案 ```go ch := make(chan struct{}, 10) // 容量 10 = 并发上限 var wg sync.WaitGroup for i := 0; i < 20; i++ { wg.Add(1) ch <- struct{}{} // 写满 10 个后自动阻塞 go func(id int) { defer wg.Done() fmt.Printf("id: %d\n", id) <-ch // 执行完释放一个槽位 }(i) } wg.Wait() ``` > [!tip] 💡 面试技巧 > 这是最经典的**并发控制模式**——用有缓冲 channel 做信号量(Semaphore)。面试中如果只写出这个,说明掌握了基础;如果能补充"用 context 加超时"或"用 errgroup 处理错误",则是加分项。 ## 关联笔记 - [[hzh/GolangStar/Go面试题库/Channel面试题]] - [[hzh/GolangStar/Go面试题库/Sync面试题]] - [[hzh/GolangStar/Go面试题库/Context面试题]]