结论:别用goroutine并行跑单个字符串对的Levenshtein距离,更别用channel收集结果;真要批量算相似度,优先走词向量(TF-IDF + cosineSimilarity)+ 单goroutine流式预处理,仅当输入是成千上万个独立短文本时,才考虑按chunk分发给固定worker数量的goroutine。

直接说结论:别用 goroutine 并行跑单个字符串对的 Levenshtein 距离,更别用 channel 收集结果;真要批量算相似度,优先走词向量(TF-IDF + cosineSimilarity)+ 单 goroutine 流式预处理,仅当输入是成千上万个独立短文本时,才考虑按 chunk 分发给固定 worker 数量的 goroutine。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
为什么不能 for-range + go func() 算一堆字符串对
常见错误写法:for i := range texts { go func() { score := levenshtein(texts[i], query) }() } —— 所有 goroutine 共享同一个 i,最终全在比 texts[len(texts)-1]。即使修复闭包问题,每个 goroutine 都调一次 levenshtein 仍会:
- 为每个字符串对重复分配二维
dp切片(make([][]int, m+1)),GC 压力陡增 - 中文/emoji 下若没转
[]rune,len(s)返回字节数而非字符数,距离值完全失真 - 500 字符以上字符串,O(m×n) 时间+空间直接卡住 HTTP handler
批量短文本匹配该用什么算法
场景如:1000 个用户昵称 vs 1 个关键词、日志中提取 50+ 敏感词位置、API 请求参数校验拼写容错。这时应放弃逐对 Levenshtein:
- 用
regexp.MustCompile("(?i)abc|def|xyz")—— Go 标准库对简单 alternation 自动编译为 Aho-Corasick,单次扫描完成全部匹配,比循环调用快 3–10 倍 - 第三方库
github.com/BurntSushi/ahocorasick更适合规则引擎:支持预构建、复用实例、返回 pattern ID 和位置 - 自己手写 Trie + BFS 失败指针仅当需定制行为(如忽略空格、大小写混合策略)才值得投入
长文本语义相似度必须转向词向量
“今天天气不错” vs “今日气候良好”,Levenshtein 返回 12(字字不同),但语义高度接近。此时必须:
- 清洗 → 分词(中文用
gojieba或空格切分英文)→ 统计词频 → 构建map[string]int向量 - 用
cosineSimilarity计算夹角余弦,公式是(A · B) / (||A|| × ||B||),注意分母为 0 时 panic - 单次比对可简化
idf=1先跑通;线上服务常混用:先用 Levenshtein 快速过滤明显不相关项,再对候选集跑 TF-IDF 排序
真要并行,只在两类场景下做且必须控制粒度
第一类:输入是成千上万个独立短字符串(如 UUID 列表、HTTP header 值),需统一做小写/校验/编码;第二类:日志文件已按行切分,每行处理耗时 >1ms(如正则提取 + JSON 解析)。这时:
- 用
sync.WaitGroup+ 预切分,chunkSize := (len(lines) + workers - 1) / workers向上取整 -
workers设为runtime.NumCPU(),不是len(lines);避免无缓冲chan string,改用预分配切片 + 索引传入 - 拼接最终结果必须回到主线程用
strings.Builder一次性聚合,goroutine 内只做局部处理
真正麻烦的从来不是写对一个 levenshtein 函数,而是判断该不该并行、该用编辑距离还是词向量——前者快但无语义,后者有语义但要搭整套预处理链。线上服务里混用两者很常见,跳过这层判断逻辑,相似度数字再准也没用。

















