直接用 map[string]int 统计词频足够,分词前需清洗标点、转小写;纯英文用正则提取字母,中英文混排逐字符判断 Unicode 字母或汉字;大文件须用 bufio.Scanner 流式读取,避免 OOM。

直接用 map[string]int 统计词频就足够了,别碰 sync.Map 或其他并发结构——99% 的本地文件分析场景都是单阶段串行流程,加并发反而引入竞态、拖慢速度、掩盖分词逻辑缺陷。
分词前必须清洗标点和大小写
只靠 strings.Fields 切空格,会把 "hello,world!" 当成一个词;不转小写会导致 "Go" 和 "go" 被算作两个 key。真实文本里还有缩写(don't)、连字符(state-of-the-art)、中英夹杂(iPhone 15发布会)等边界问题。
- 纯英文:用
regexp.MustCompile(`[a-zA-Z]+`)提取连续字母,比切空格更鲁棒 - 中英文混排:逐字符判断
unicode.IsLetter或unicode.IsHan,聚合成 token - 统一预处理:先
strings.ToLower,再提取,避免 key 不一致 - 绝对别用
strings.Split(text, " ")—— 多个空格会产生空字符串"",最终在 map 里多出"" : 42这种脏数据
大文件必须用 bufio.Scanner 流式读取
读几百 MB 的日志或语料时,os.ReadFile 容易触发 OOM;bufio.Scanner 按行缓冲,内存可控,默认 64KB 缓冲区够用。
- 设置扫描器拆分函数可提升性能:
scanner.Split(bufio.ScanWords)直接按单词切,省去手动 split - 遇到超长行(如 minified JSON)需调大缓冲区:
scanner.Buffer(make([]byte, 4096), 1 - 错误要判
scanner.Err(),不是只看Scan()返回值;io.EOF是正常结束,其他err才该 log
排序输出前必须转 slice,map 本身无序
Go 的 map 遍历顺序随机是语言特性,不是 bug。想按频次从高到低、或按字母序输出,必须先把键值对拎出来。
立即学习“go语言免费学习笔记(深入)”;
- 定义结构体:
type kv struct{ word string; count int } - 转 slice:
var items []kv; for w, c := range freqMap { items = append(items, kv{w, c}) } - 按频次降序:
sort.Slice(items, func(i, j int) bool { return items[i].count > items[j].count }) - 限制 Top N:
items = items[:min(n, len(items))],别在 map 上循环找最大——那是O(n²)
最容易被忽略的是分词边界处理:正则 [a-zA-Z]+ 对缩写、连字符、中英夹杂完全失效。真有这类需求,宁可加个轻量分词库如 github.com/go-ego/gse,也别硬改正则凑合——后者很快变成不可维护的字符串拼接黑洞。


















