外观
常见手写题与并发练习
- 状态:未准备
- 适用方向:Go 全栈 / Go 后端
- 最近复习:待填写
- 下次复习:待安排
一句话回答
手写题先约定输入、顺序、错误和取消语义,再写实现;并发题重点是容量与退出路径,不能只证明正常输入能跑通。
原理解释
有界并发处理
以下完整示例固定创建 worker,不按任务数无界创建 goroutine;全部任务由同一调用等待完成。fn 必须线程安全,若需及时取消也要响应 ctx。
go
package main
import (
"context"
"fmt"
"sync"
)
func Run(ctx context.Context, workers int, jobs []int,
fn func(context.Context, int) error) error {
if workers <= 0 || fn == nil {
return fmt.Errorf("invalid workers or function")
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
queue := make(chan int)
var wg sync.WaitGroup
var once sync.Once
var firstErr error
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case <-ctx.Done():
return
case job, ok := <-queue:
if !ok { return }
if ctx.Err() != nil { return }
if err := fn(ctx, job); err != nil {
once.Do(func() { firstErr = err; cancel() })
return
}
}
}
}()
}
sendLoop:
for _, job := range jobs {
select {
case <-ctx.Done():
break sendLoop
case queue <- job:
}
}
close(queue) // 唯一生产者关闭,无并发发送者。
wg.Wait() // 等待后再读 firstErr。
if firstErr != nil { return firstErr }
return ctx.Err()
}
func main() {
err := Run(context.Background(), 2, []int{1, 2, 3},
func(ctx context.Context, n int) error {
return ctx.Err() // 练习时替换为实际任务。
})
fmt.Println(err)
}保证同时执行 fn 的数量不超过 workers;不保证结果顺序,也不保证取消瞬间再无任务进入 fn。首次记录的任务错误优先返回,否则返回 context 错误。若 fn 永久阻塞,Wait 仍会等待;不能靠外层取消强杀。
其他高频练习
| 题目 | 先约定 | 必测边界 |
|---|---|---|
| 合并多个 channel | 输出何时关闭、如何取消 | 零输入、输入提前关闭、下游停止 |
| 三协程轮流输出 | 总次数与首个执行者 | 0 次、最后一轮退出、无多余发送 |
| LRU 缓存 | map + 双向链表,Get 是否更新顺序 | 容量 0、更新已有 key、淘汰与并发 |
| 字符串反转 | 字节、码点或完整字符 | 空串、中文、组合字符 |
| 切片原地去重 | 是否保序、是否允许改输入 | 空输入、全重复、共享数组 |
| 限流器 | 固定窗口、滑动窗口或令牌桶 | 边界突发、时间源、并发更新 |
场景与边界
以上是教学实现,不含结果汇总、panic 恢复、持久化重试和线上指标;需要时按业务扩展。错误、取消和完成同时发生时先定义允许的行为,避免写出无法保证的承诺。
项目中的应用
结合 SimulSpeak 模型调用思考不同会话的并发上限;不代表项目目前使用此示例。
常见追问
- 为什么不用每个任务先 go 再抢信号量? 那仍会创建大量等待 goroutine,固定 worker 更直接控制数量。
- 怎么保序? 为输入编号并按序汇总,说明慢任务导致的队首等待与缓冲上限。
- 如何验证? 空任务、非法 worker、首次错误、预先取消、处理中取消;用原子计数记录同时在途峰值,任务阻塞用信号协调。