资讯动态

learngo 函数实战:将游戏商店与日志解析器重构为 Go 函数的四步练习指南

发布时间:2026/10/3 2:02:32 来源:尧图企业网站定制
示例工程教程【免费下载链接】learngo❤️ 1000 Hand-Crafted Go Examples, Exercises, and Quizzes. Learn Go by fixing 1000 tiny programs.项目地址https://gitcode.com/gh_mirrors/le/learngo点击查看免费下载本指南基于 learngo 仓库 25-functions/exercises/README.md 展开聚焦用函数重构已有程序这一 Go 学习关键技能。你将沿着四个递进式练习把基于结构体的游戏商店程序逐步重构为函数驱动先是抽取数据结构与创建/装载逻辑再是命令分发逻辑然后是 JSON 编解码最后独立重写日志解析器。读完本文你将掌握 Go 函数的签名设计、返回值约定bool 控制流程、跨文件组织、encoding/json编解码以及基于bufio.Scanner的逐行解析模式并能在 25-functions/exercises 目录中对照练习与参考答案逐题验证。练习背景为什么用函数重构在 25-functions 章节中先前的程序例如 03-refactor-to-funcs 和 04-pass-by-value-semantics演示了把日志解析逻辑拆进独立函数文件parser.go的做法。本节练习则反其道而行给你一段已经能运行但全部逻辑堆在main()里的程序要求你亲手把它拆成一组职责清晰的函数。这种先有完整功能、再做函数化拆分的训练价值在于函数化之前代码中的逻辑只能靠注释和阅读顺序理解函数化之后每个行为有了名字、签名和可复用的入口main()只负责组装与调度通过bool返回值把是否继续运行的流程控制显式化为后续学习错误处理与接口奠定基础。四个练习文件都位于 25-functions/exercises每个练习目录下有带// --- 提示 ---注释的main.go起始文件以及完整的solution/参考答案目录可直接go run验证。练习一抽取数据结构与游戏装载逻辑Step #1第一个练习要求新建games.go文件把结构体和数据相关的函数从main.go中搬走。起始代码位于 25-functions/exercises/refactor-to-funcs-1/main.go它的main()中内联定义了item与game两个结构体并直接构造了游戏切片、手写循环建立 ID 索引、在switch分支内联打印游戏信息。需要完成的 6 个函数函数名输入输出职责newGameid, price int, name, genre stringgame构造并返回一个游戏addGamegames []game, g game[]game追加一个游戏并返回新切片load无[]game装载整个游戏商店数据indexByID[]gamemap[int]game按 ID 建立游戏索引printGamegame无格式化打印单个游戏参考答案解析games.go见 solution/games.go展示了完整的实现思路type item struct { id int name string price int } type game struct { item genre string } func load() (games []game) { games addGame(games, newGame(1, 50, god of war, action adventure)) games addGame(games, newGame(2, 40, x-com 2, strategy)) games addGame(games, newGame(3, 20, minecraft, sandbox)) return } func addGame(games []game, g game) []game { return append(games, g) } func newGame(id, price int, name, genre string) game { return game{ item: item{id: id, name: name, price: price}, genre: genre, } } func indexByID(games []game) (byID map[int]game) { byID make(map[int]game) for _, g : range games { byID[g.id] g } return } func printGame(g game) { fmt.Printf(#%d: %-15q %-20s $%d\n, g.id, g.name, (g.genre), g.price) }值得注意的 Go 细节组合而非继承game内嵌item因此g.id、g.name、g.price可以直接访问这正是 24-structs 章节中04-embedding的延续切片追加模式addGame采用append并返回新切片——由于切片头是值传递必须把返回值重新赋给调用方games addGame(...)这与 16-slices 的slice 内部共享底层数组机制完全一致命名返回值load()与indexByID()使用命名返回值并裸return让函数意图返回装载好的游戏列表直接写在签名上格式化打印%-15q带引号左对齐 15 宽度%-20s左对齐 20 宽度$%d输出价格。重构后的 solution/main.go 只保留三行装配逻辑games : load() byID : indexByID(games)switch的list与id分支都改为调用printGame(g)。通过对比起始文件与答案可以直观看到main()从100 行逻辑收缩为装配 命令分发。练习二重构命令分发逻辑Step #2第二个练习要求新建commands.go把命令处理逻辑从main()中搬走。这是本组练习的核心亮点引入bool返回值来显式表达程序是否继续运行。起始文件 refactor-to-funcs-2/main.go 的main()中有一大段被// --- runCmd start ---与// --- runCmd end ---标记的switch逻辑同时用continue/return控制流程。4 个待实现函数函数名输入输出语义runCmdinput string, games []game, byID map[int]gamebool分发并执行命令true继续、false终止cmdQuit无bool打印bye!并返回falsecmdList[]gamebool列出所有游戏返回truecmdByIDcmd []string, games []game, byID map[int]gamebool按 ID 查询游戏返回true参考答案解析commands.go 的关键实现如下func runCmd(input string, games []game, byID map[int]game) bool { fmt.Println() cmd : strings.Fields(input) if len(cmd) 0 { return true } switch cmd[0] { case quit: return cmdQuit() case list: return cmdList(games) case id: return cmdByID(cmd, games, byID) } return true } func cmdQuit() bool { fmt.Println(bye!) return false } func cmdByID(cmd []string, games []game, byID map[int]game) bool { if len(cmd) ! 2 { fmt.Println(wrong id) return true } id, err : strconv.Atoi(cmd[1]) if err ! nil { fmt.Println(wrong id) return true } g, ok : byID[id] if !ok { fmt.Println(sorry. I dont have the game) return true } printGame(g) return true }这里蕴含的设计模式值得反复咀嚼统一出口约定每个cmdXXX都返回booltrue表示本次命令不打断主循环false仅cmdQuit表示程序应终止。这让main()的主循环精简为一行判断if !in.Scan() || !runCmd(in.Text(), games, byID) { break }注意||的短路求值——in.Scan()失败时runCmd根本不会被调用防御性校验内聚到函数内参数个数校验len(cmd) ! 2、strconv.Atoi错误、map查询 miss 三条出错路径都集中在一个函数里打印可读错误消息后返回true继续运行避免main()里散落continuestrings.Fields的作用把用户输入按空白切分成命令词cmd[0]是命令名cmd[1]是id命令的参数。重构后的main()solution/main.go仅剩 13 行可执行逻辑命令集合从switch内联体收敛为一张命令表。这一步之后main()就完全变成了一个输入 → 分发 → 输出的壳。练习三引入 JSON 编解码Step #3第三个练习做两件事新增cmdSave命令把游戏数据编码为 JSON 并退出把load()改为从常量data中的 JSON 文本解码初始数据。起始文件与答案分别位于 refactor-to-funcs-3 目录。1. cmdSave编码游戏商店为 JSONcommands.go 中的新增函数func cmdSave(games []game) bool { // load the data into encodable game values var jg []jsonGame for _, g : range games { jg append(jg, jsonGame{g.id, g.name, g.genre, g.price}) } out, err : json.MarshalIndent(jg, , \t) if err ! nil { fmt.Println(sorry, cant save:, err) return true } fmt.Println(string(out)) return false }要点runCmd的switch增加case save: return cmdSave(games)菜单提示同步增加 save : exports the data to json and quits见 solution/main.gojson.MarshalIndent生成带\t缩进的可读 JSON若编码失败例如数据含不可序列化字段则打印错误并返回true继续运行编码成功后打印 JSON 并返回false终止程序——这延续了false即终止的既定约定。2. load()从 JSON 常量解码solution/games.go 定义了解码专用的结构体与数据源const data [ { id: 1, name: god of war, genre: action adventure, price: 50 }, ... ] // encodable and decodable game type type jsonGame struct { ID int json:id Name string json:name Genre string json:genre Price int json:price } func load() (games []game) { var decoded []jsonGame if err : json.Unmarshal([]byte(data), decoded); err ! nil { fmt.Println(Sorry, there is a problem:, err) return } for _, dg : range decoded { games addGame(games, newGame(dg.ID, dg.Price, dg.Name, dg.Genre)) } return games }设计上的精妙之处为什么需要jsonGame而非直接编解码gamegame内嵌的item字段是未导出的小写字段encoding/json默认无法序列化/反序列化未导出字段。因此专门定义一个字段全部大写ID、Name、Genre、Price且带json:...标签的jsonGame作为传输格式再通过newGame桥接到内部game类型——这正是 24-structs 章节06-encoding/07-decoding练习所训练的模式双层数据映射data常量原始 JSON 文本→[]jsonGame解码→[]game内部业务类型每一层都职责清晰错误处理前置解码失败时打印Sorry, there is a problem: ...并返回nil切片命名返回值的默认零值避免携带不完整数据继续运行。运行go run .后交互菜单变为list/id N/save/quit四条命令输入save即可看到格式化 JSON 输出。练习四独立重写日志解析器rewrite-log-parser-using-funcs第四个练习不再给出步骤拆解而是要求你在看完讲解后独立把日志解析程序改写成函数式版本。相关讲解正是 25-functions/03-refactor-to-funcs其把 23-input-scanning/03-project-log-parser 的程序重构为main.goparser.go两个文件。目标程序行为练习目录 rewrite-log-parser-using-funcs 提供了四个测试数据文件log.txt正常数据每行域名 访问量可执行go run . log.txt查看输出log_err_missing.txt某行缺少字段log_err_negative.txt访问量为负数log_err_str.txt访问量是非数字字符串。函数化前的形态练习出发点未函数化版本见 rewrite-log-parser-using-funcs/main.go其核心结构是type result struct { domain string visits int } type parser struct { sum map[string]result // metrics per domain domains []string // unique domain names total int // total visits for all domains lines int // number of parsed lines (for the error messages) }所有逻辑字段拆分、strconv.Atoi、去重收集、累加、排序输出、in.Err()检查全部堆在main()里通过return提前退出处理错误。函数化后的目标形态讲解版答案parser.go 给出了函数化后的核心// newParser constructs, initializes and returns a new parser func newParser() parser { return parser{sum: make(map[string]result)} } // parse parses a log line and returns the parsed result with an error func parse(p parser, line string) (parsed result, err error) { fields : strings.Fields(line) if len(fields) ! 2 { err fmt.Errorf(wrong input: %v (line #%d), fields, p.lines) return } parsed.domain fields[0] parsed.visits, err strconv.Atoi(fields[1]) if parsed.visits 0 || err ! nil { err fmt.Errorf(wrong input: %q (line #%d), fields[1], p.lines) return } return }对照要点这也是你在练习四中要独立完成的构造函数模式newParser()负责初始化make(map[string]result)避免调用方忘记初始化 map 而 panic错误作为返回值parse返回(parsed result, err error)把解析结果与失败原因同时显式化错误信息携带行号p.lines便于定位result不可直接累加Go 的复合类型不能直接做p.sum[domain].visits visits源码注释明确指出了这一点需要读取旧值、构造新result再整体写回 map数据流分工main()负责扫描 stdin、调用parse、聚合结果、排序输出parser.go只负责构造解析器和解析单行两个纯职责。练习目录未提供solution/答案而是让你对照讲解自行完成并验证——这是从跟着改到独立写的关键一跃。验证命令go run . log.txt、go run . log_err_missing.txt观察错误信息与输出格式是否与未函数化版本一致。四步练习的能力递进与自测要点练习目录核心能力自测方法Step #1refactor-to-funcs-1结构体抽取、构造函数、切片追加、map 索引go run .后依次输入list、id 2、quit输出与重构前一致Step #2refactor-to-funcs-2命令分发、bool 返回值流程控制输入id缺参数应打印wrong id而非退出输入id 99应打印sorry. I dont have the gameStep #3refactor-to-funcs-3JSON 编解码、json标签、常量数据源输入save输出缩进 JSON 并退出load()从data解码出 3 款游戏Log Parserrewrite-log-parser-using-funcs独立函数化、错误返回值、逐行扫描四种log_*.txt数据全部正确处理输出与 03-refactor-to-funcs 讲解一致四个练习共用同一条主线先把数据和行为分开Step #1再把行为按命令细分Step #2再让数据具备持久化形态Step #3最后脱离脚手架独立完成同类重构练习四。这条路径对应的章节脉络是 24-structs → 25-functions → 26-pointers函数化是承上启下的枢纽它让main()回归装配职责为后续基于指针的值语义改造04-pass-by-value-semantics和更复杂的错误处理打下基础。完成全部练习后建议回到 25-functions/questions/README.md 检验自己对函数签名、作用域与值传递的理解是否到位。赞分享示例工程教程【免费下载链接】learngo❤️ 1000 Hand-Crafted Go Examples, Exercises, and Quizzes. Learn Go by fixing 1000 tiny programs.项目地址https://gitcode.com/gh_mirrors/le/learngo点击查看免费下载相关推荐RxJS Rx.Observable.toAsync 深度指南将同步函数转换为异步函数的核心原理与实战RxJS Rx.Observable.toAsync 深度指南将同步函数转换为异步函数的核心原理与实战 Rx.Observable.toAsync 是 RxJ后端函数与参数传递learngo 项目函数编程指南函数与参数传递learngo 项目函数编程指南 Go语言函数编程是学习Go语言的重要环节learngo项目通过1000手写示例、练习和测验帮助开发者掌握示例工程教程深入 Go 循环与随机数learngo「幸运数字游戏」6 个进阶练习全解析深入 Go 循环与随机数learngo「幸运数字游戏」6 个进阶练习全解析 在 learngo https://link.gitcode.com/i/2445示例工程教程上一篇PyTorch Geometric HeteroConv 实战维度报错、关系漏配与聚合不匹配的排障全流程下一篇Claude Agent SDK 发布流程的 Changelog 自动生成深入解析 .claude/commands/generate-changelog.md创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑