☰
learngo 输入扫描实战:用 bufio.Scanner 与 map 完成 6 个经典练习(Uppercaser、Unique Words、Grep Clone、Log Parser)
2026/10/3 1:47:43 网站建设 项目流程
  • 示例工程
  • 教程

【免费下载链接】learngo

❤️ 1000+ Hand-Crafted Go Examples, Exercises, and Quizzes. 🚀 Learn Go by fixing 1000+ tiny programs.

项目地址:https://gitcode.com/gh_mirrors/le/learngo
点击查看免费下载

导读

本篇围绕 learngo 课程仓库 23-input-scanning/exercises/README.md 展开,系统讲解如何用 Go 标准库中的bufio.Scanner读取标准输入流、用map做集合与计数,并以此为工具完成 6 个循序渐进的实战练习:从逐行转大写、统计唯一单词、清洗标点、实现 grep 克隆、检测重复输入,到最后从零复刻一个完整的日志解析器。读完本篇,你将掌握Scanner的分词原理、SplitFunc的用法、正则清洗文本、命令行参数解析,以及 map 作为集合/计数器的典型模式,并能独立写出可处理真实文本输入的 Go 命令行程序。

一、练习总览:从输入流到输出的一整套阶梯

这一章的练习全部围绕一个共同主题:把一段标准输入(通常是文本文件)读进程序,用bufio.Scanner逐行或逐词扫描,再用map完成统计、去重或判断。六个练习难度逐级递增,恰好覆盖了输入扫描的两大核心知识点:

练习目录核心技能点
1. Uppercaser01-uppercaser逐行扫描 +strings.ToUpper
2. Unique Words02-unique-wordsin.Split(bufio.ScanWords)+ map 计数
3. Unique Words 203-unique-words-2正则[^A-Za-z]+清洗 + 大小写归一
4. Grep Clone04-grep命令行参数 +strings.Contains过滤
5. Quit05-quitmap 作为集合判断重复
6. Log Parser(从零实现)06-log-parser综合:Scanner + map + 错误处理 + 格式化输出

每个练习目录都采用"题目 + 参考答案"的结构:main.go是带完整题目描述与预期输出的空壳,solution/main.go是可直接运行的答案,部分练习还附带了用于喂给程序的shakespeare.txt。下面的实现细节均以仓库中 solution/main.go 等参考答案文件为准。

二、先看懂工具:Scanner 的两种扫描模式

所有练习都建立在bufio.Scanner之上。基础用法见 01-scanning/main.go:

in := bufio.NewScanner(os.Stdin) var lines int for in.Scan() { lines++ in.Text() // 获取当前行文本;in.Bytes() 可获取原始字节 } fmt.Printf("There are %d line(s)\n", lines) if err := in.Err(); err != nil { fmt.Println("ERROR:", err) }

这段代码揭示了 Scanner 的三个要点:

  1. NewScanner包装一个io.Reader,这里传入os.Stdin,因此程序天然支持go run main.go < 文件的重定向输入方式;
  2. for in.Scan()循环是扫描器的驱动核心——每次调用Scan()推进到下一个 token,循环结束后可用in.Err()检查是否有错误(该源码中甚至注释展示了如何模拟错误:os.Stdin.Close());
  3. 默认按行拆分,即默认的SplitFunc是bufio.ScanLines。

当需要按"词"而不是按"行"扫描时,只需显式设置拆分函数:

in.Split(bufio.ScanWords)

bufio包内置了ScanLines、ScanWords、ScanRunes、ScanBytes四种拆分策略,ScanWords会把连续的非空白字符序列识别为一个词。这正是 Unique Words 系列练习的关键一步——它让 map 的键直接就是一个个单词。

三、练习 1:Uppercaser——逐行转换并打印

题目要求(见 01-uppercaser/main.go):把shakespeare.txt喂给程序,用 Scanner 扫描输入,将每一行转为大写后打印。

参考答案(01-uppercaser/solution/main.go):

func main() { in := bufio.NewScanner(os.Stdin) for in.Scan() { fmt.Println(strings.ToUpper(in.Text())) } }

运行方式:

go run main.go < shakespeare.txt

这个练习虽然只有几行,却完整演示了"扫描—取文本—转换—输出"的最小管道。strings.ToUpper(in.Text())接收当前行的字符串并返回全大写版本,配合fmt.Println逐行输出,整个程序不依赖任何第三方库。

四、练习 2:Unique Words——用 map 统计唯一单词

题目要求(见 02-unique-words/main.go):扫描输入中的每个单词,用 map 统计唯一词数量,最终输出There are 99 words, 70 of them are unique.。

参考答案(02-unique-words/solution/main.go):

func main() { in := bufio.NewScanner(os.Stdin) in.Split(bufio.ScanWords) total, words := 0, make(map[string]int) for in.Scan() { total++ words[in.Text()]++ } fmt.Printf("There are %d words, %d of them are unique.\n", total, len(words)) }

技术要点拆解:

  • total统计总词数:每扫描到一个 token 就total++,等价于len(words)再加重复词的出现次数;
  • words[in.Text()]++是经典的"map 作为计数器"模式:Go 中访问不存在的 map 键会返回零值,因此首次遇到某词时words[词]为 0,自增后变成 1,之后再遇到同一个词就继续累加;
  • len(words)即唯一词数量,因为 map 的每个键只存在一份。

这里可以看到in.Split(bufio.ScanWords)与默认逐行模式的关键区别:若沿用逐行扫描,整行文本会被当作一个"词"计入 map,统计结果将完全错误。

五、练习 3:Unique Words 2——用正则清洗标点与数字

题目要求(见 03-unique-words-2/main.go):在上一题基础上,将单词加入 map之前先移除其中的标点符号和数字;同时注意此时shakespeare.txt中同时含有大写和小写字母,因此还需要统一大小写。预期输出为There are 100 words, 69 of them are unique.。

题目注释直接给出了所需的正则模式:

// [^A-Za-z]+ // Matches to any character but upper case and lower case letters

参考答案(03-unique-words-2/solution/main.go):

func main() { in := bufio.NewScanner(os.Stdin) in.Split(bufio.ScanWords) rx := regexp.MustCompile(`[^A-Za-z]+`) total, words := 0, make(map[string]int) for in.Scan() { total++ word := rx.ReplaceAllString(in.Text(), "") word = strings.ToLower(word) words[word]++ } fmt.Printf("There are %d words, %d of them are unique.\n", total, len(words)) }

三个新增知识点:

  1. [^A-Za-z]+是反向字符类:[^...]表示"匹配任何一个不在括号内的字符",A-Za-z限定为英文字母,末尾的+表示连续匹配一个或多个。因此该模式能一次性吃掉一串连续的标点或数字;
  2. regexp.MustCompile在包级表达式固定的情况下比regexp.Compile更简洁——它在编译失败时直接 panic,适用于模式是编译期常量的场景;
  3. rx.ReplaceAllString(in.Text(), "")把匹配到的所有"非字母片段"替换为空串,随后strings.ToLower归一化大小写,使Come与come归并为同一个键,这正是唯一词数从 70 降到 69 的原因。

六、练习 4:Grep Clone——用命令行参数过滤行

题目要求(见 04-grep/main.go):实现一个 grep 克隆。grep 是用于在纯文本数据中搜索匹配特定模式的行的命令行工具。具体要求:从命令行参数接受模式,只打印包含该模式的行;若未提供模式则打印所有行。预期运行与输出:

go run main.go come < shakespeare.txt come night come romeo come thou day in night come gentle night come loving black-browed night

参考答案(04-grep/solution/main.go):

func main() { in := bufio.NewScanner(os.Stdin) var pattern string if args := os.Args[1:]; len(args) == 1 { pattern = args[0] } for in.Scan() { s := in.Text() if strings.Contains(s, pattern) { fmt.Println(s) } } }

实现技巧分析:

  • os.Args[1:]取出所有命令行参数,os.Args[0]是程序名本身,因此从下标 1 开始;
  • if args := os.Args[1:]; len(args) == 1使用 if 初始化语句,把"取参数"与"校验参数个数"合并成一步,是 Go 惯用的短路写法——恰好一个参数才设置 pattern,否则 pattern 保持空串;
  • strings.Contains(s, pattern):当 pattern 为空串时,任何字符串都包含空串,于是所有行都被打印,自然满足了"无模式则全量输出"的要求;
  • 扫描模式这里保持默认的逐行,因为 grep 的语义是"按行匹配"。

七、练习 5:Quit——用 map 做集合判断重复输入

题目要求(见 05-quit/main.go):创建一个程序,当用户连续两次输入同一个词时退出。

参考答案(05-quit/solution/main.go):

func main() { in := bufio.NewScanner(os.Stdin) words := make(map[string]bool) for in.Scan() { w := strings.ToLower(in.Text()) if words[w] { fmt.Println("TWICE!") return } words[in.Text()] = true } }

这个练习展示了map 作为集合(Set)的惯用法:

  • map[string]bool的值不需要计数,只关心"这个键存不存在"——这是 Go 中模拟集合的标准做法;
  • if words[w]是判断技巧:访问不存在的键返回false(bool 零值),访问已存在的键返回true,因此一行即可完成"是否见过"的判断;
  • 注意这里的细微差异:判断时用strings.ToLower(in.Text())归一化,写入时却用原始in.Text()。这表示大小写不同的输入仍会被视为"同一个词"从而触发退出(比如输入Hello后再输入hello会输出TWICE!并退出),这是刻意设计的容错行为;
  • 触发重复后return直接结束main,程序退出。

八、练习 6:从零实现 Log Parser——综合大考

题目要求(见 06-log-parser/main.go):你已观看过讲解视频,现在不参考讲解和已有源码,独立实现同样的日志解析器。题目给出了完整的多组预期输出,覆盖正常与三类错误场景:

go run main.go < log.txt DOMAIN VISITS --------------------------------------------- blog.golang.org 30 golang.org 10 learngoprogramming.com 20 TOTAL 60
go run main.go < log_err_missing.txt wrong input: [golang.org] (line #3)
go run main.go < log_err_negative.txt wrong input: "-100" (line #3)
go run main.go < log_err_str.txt wrong input: "FOUR" (line #3)

练习目录内提供了四个输入文件——log.txt(正常数据)、log_err_missing.txt(某行缺少访问次数)、log_err_negative.txt(出现负数)、log_err_str.txt(次数是字符串而非数字)。

这个练习是对整章能力的综合检验,期望你独立完成以下步骤:

  1. 逐行扫描输入,用in.Split(bufio.ScanWords)或strings.Fields拆分"域名 + 访问次数";
  2. 用 map 汇总每个域名的总访问量;
  3. 校验数据:行内字段缺失、次数为负数、次数无法转换为整数时,都要按格式输出wrong input: "..." (line #N)并退出;
  4. 格式化输出:用fmt.Printf对齐 DOMAIN 与 VISITS 两列,并在末尾打印 TOTAL。

作为对照,仓库 03-project-log-parser/main.go 提供了完整的讲师版本实现,其中用到了strconv.Atoi解析数字、fmt.Printf的宽度对齐(如%-*s)等技巧;后续章节 24-structs/05-project-log-parser-structs、25-functions、26-pointers 还会用结构体、函数与指针反复重构这个解析器。如果在此卡壳,建议先自己完成再对照阅读,这正是该练习"从零实现"的设计初衷。

九、关键模式速查与自测清单

六个练习沉淀下来四组可复用的 Go 惯用模式:

模式关键代码出自练习
逐行扫描in := bufio.NewScanner(os.Stdin); for in.Scan()1、4、5、6
按词扫描in.Split(bufio.ScanWords)2、3
map 计数words[word]+++len(words)2、3、6
map 集合map[string]bool+if words[w]5
文本清洗regexp.MustCompile(\[^A-Za-z]+`)+ReplaceAllString`3
参数解析if args := os.Args[1:]; len(args) == 14

动手自测时可以依次追问自己:Scan()返回false时如何区分"输入结束"与"出错"(答案是in.Err())?为什么 Unique Words 2 必须同时做正则清洗与ToLower?如果 Grep Clone 需要忽略大小写,strings.Contains该如何改造(提示:双方都先ToLower)?Log Parser 中某行只有域名没有数字时,ScanWords模式下该行会扫描出几个 token?

十、延伸阅读

  • 扫描器基础讲解:23-input-scanning/01-scanning/main.go(含错误检查示范)
  • map 作为集合:23-input-scanning/02-map-as-sets/main.go
  • 日志解析器完整实现:23-input-scanning/03-project-log-parser/main.go
  • 本章问答自测:23-input-scanning/questions/README.md
  • 后续重构路线:结构体版 24-structs/05-project-log-parser-structs/main.go、函数版 25-functions/03-refactor-to-funcs、指针版 26-pointers/04-log-parser-pointers

完成这六个练习后,你不仅掌握了bufio.Scanner与map的组合用法,也具备了"接收标准输入 → 清洗与校验 → 统计与过滤 → 格式化输出"这条完整的命令行文本处理流水线能力,可以直接迁移到真实的日志分析、词频统计等场景中。

  • 示例工程
  • 教程

【免费下载链接】learngo

❤️ 1000+ Hand-Crafted Go Examples, Exercises, and Quizzes. 🚀 Learn Go by fixing 1000+ tiny programs.

项目地址:https://gitcode.com/gh_mirrors/le/learngo
点击查看免费下载
上一篇:从源码到部署:Heroku-buildpack-static工作原理深度剖析
下一篇:OpenCore Legacy Patcher 安装指南:在旧 Mac 上装新 macOS 的完整步骤

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询