learngo 输入扫描实战:用 bufio.Scanner 与 map 完成 6 个经典练习(Uppercaser、Unique Words、Grep Clone、Log Parser)

发布时间:2026/10/3 1:48:14
learngo 输入扫描实战:用 bufio.Scanner 与 map 完成 6 个经典练习(Uppercaser、Unique Words、Grep Clone、Log Parser)
示例工程教程【免费下载链接】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.ToUpper2. 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.goin : 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 的三个要点NewScanner包装一个io.Reader这里传入os.Stdin因此程序天然支持go run main.go 文件的重定向输入方式for in.Scan()循环是扫描器的驱动核心——每次调用Scan()推进到下一个 token循环结束后可用in.Err()检查是否有错误该源码中甚至注释展示了如何模拟错误os.Stdin.Close()默认按行拆分即默认的SplitFunc是bufio.ScanLines。当需要按词而不是按行扫描时只需显式设置拆分函数in.Split(bufio.ScanWords)bufio包内置了ScanLines、ScanWords、ScanRunes、ScanBytes四种拆分策略ScanWords会把连续的非空白字符序列识别为一个词。这正是 Unique Words 系列练习的关键一步——它让 map 的键直接就是一个个单词。三、练习 1Uppercaser——逐行转换并打印题目要求见 01-uppercaser/main.go把shakespeare.txt喂给程序用 Scanner 扫描输入将每一行转为大写后打印。参考答案01-uppercaser/solution/main.gofunc 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逐行输出整个程序不依赖任何第三方库。四、练习 2Unique Words——用 map 统计唯一单词题目要求见 02-unique-words/main.go扫描输入中的每个单词用 map 统计唯一词数量最终输出There are 99 words, 70 of them are unique.。参考答案02-unique-words/solution/main.gofunc 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统计结果将完全错误。五、练习 3Unique 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.gofunc 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)) }三个新增知识点[^A-Za-z]是反向字符类[^...]表示匹配任何一个不在括号内的字符A-Za-z限定为英文字母末尾的表示连续匹配一个或多个。因此该模式能一次性吃掉一串连续的标点或数字regexp.MustCompile在包级表达式固定的情况下比regexp.Compile更简洁——它在编译失败时直接 panic适用于模式是编译期常量的场景rx.ReplaceAllString(in.Text(), )把匹配到的所有非字母片段替换为空串随后strings.ToLower归一化大小写使Come与come归并为同一个键这正是唯一词数从 70 降到 69 的原因。六、练习 4Grep 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.gofunc 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 的语义是按行匹配。七、练习 5Quit——用 map 做集合判断重复输入题目要求见 05-quit/main.go创建一个程序当用户连续两次输入同一个词时退出。参考答案05-quit/solution/main.gofunc 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]是判断技巧访问不存在的键返回falsebool 零值访问已存在的键返回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 60go 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次数是字符串而非数字。这个练习是对整章能力的综合检验期望你独立完成以下步骤逐行扫描输入用in.Split(bufio.ScanWords)或strings.Fields拆分域名 访问次数用 map 汇总每个域名的总访问量校验数据行内字段缺失、次数为负数、次数无法转换为整数时都要按格式输出wrong input: ... (line #N)并退出格式化输出用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、3map 计数words[word]len(words)2、3、6map 集合map[string]boolif words[w]5文本清洗regexp.MustCompile(\[^A-Za-z])ReplaceAllString3参数解析if args : os.Args[1:]; len(args) 14动手自测时可以依次追问自己Scan()返回false时如何区分输入结束与出错答案是in.Err()为什么 Unique Words 2 必须同时做正则清洗与ToLower如果 Grep Clone 需要忽略大小写strings.Contains该如何改造提示双方都先ToLowerLog 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点击查看免费下载相关推荐SwifterSwift中15个超好用的String扩展从trimmed修剪到emoji检测一网打尽SwifterSwift中15个超好用的String扩展从trimmed修剪到emoji检测一网打尽 SwifterSwift 是一个拥有 500 原生 S移动开发learngo 实战训练用三个练习掌握 Go 的包、作用域与导入重命名learngo 实战训练用三个练习掌握 Go 的包、作用域与导入重命名 本文基于 03 packages and scopes/exercises/READM示例工程教程掌握 Go 短变量声明基于 learngo 仓库的 6 个实战练习详解掌握 Go 短变量声明基于 learngo 仓库的 6 个实战练习详解 Go 的短变量声明 : 是日常编码中使用频率最高的语法之一也是许多初学者最容易示例工程教程上一篇从源码到部署Heroku-buildpack-static工作原理深度剖析下一篇OpenCore Legacy Patcher 安装指南在旧 Mac 上装新 macOS 的完整步骤创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考