Files
cs-note/hzh/GolangStar/Go面试题库/代码面试题.md
T

391 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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面试题]]