- 示例工程
- 教程
【免费下载链接】learngo
❤️ 1000+ Hand-Crafted Go Examples, Exercises, and Quizzes. 🚀 Learn Go by fixing 1000+ tiny programs.
导读
本指南围绕learngo仓库中 15-project-retro-led-clock/exercises/README.md 展开,逐一拆解 Retro LED Clock 项目的四道进阶练习:多文件重构(Refactor)、每隔 10 秒显示闹钟(Set an Alarm)、显示十分之一秒的百分秒(Split Second),以及从右向左滑动的时钟动画(Ticker)。完成本指南后,你将掌握数组([...]T与[5]string)的声明、赋值、替换与索引运算在真实终端动画程序中的完整用法,并理解 Go 的包级作用域、短声明限制与多文件同包编译规则。
项目背景:Retro LED Clock 五步曲
Retro LED Clock 是learngo课程中用于强化"数组"知识的大型综合项目,项目主目录 15-project-retro-led-clock/README.md 将整个项目划分为五个循序渐进的目标步骤:
- 打印数字(01-printing-the-digits):定义占位符类型,为 0~9 每个数字创建
[5]string数组,放入digits数组并排输出; - 打印时钟(02-printing-the-clock):获取当前时间,构造时钟数组,加入冒号分隔符;
- 动画时钟(03-animating-the-clock):无限循环每秒刷新、清屏、将光标移到左上角;
- 冒号闪烁(04-blinking-the-separators):让分隔符按秒奇偶闪烁;
- 完整带注释解答(05-full-annotated-solution/main.go)。
而 exercises/README.md 正是在前四步成果之上的四道强化练习,目的是"证明你能写出易于维护的 Go 程序"。
练习一:Refactor —— 把占位符搬进独立文件
目标:将时钟项目的一部分代码移动到同包下的新文件中,使项目更易长期维护。具体分四步:
- 新建文件
placeholders.go; - 把
placeholder类型移到placeholders.go; - 把 0~9 及冒号的所有占位符移到
placeholders.go; - 把
digits数组也移到placeholders.go。
练习文件位于 01-refactor/main.go,其顶部注释给出了三条关键提示:
placeholders.go必须同样属于main包(重温"PART I — Packages, Scopes and Importing"课程);- 包作用域下短声明(
:=)不可用,必须改用var(这是"Short Declaration: Package Scope"课程的核心结论); - 若收到
undefined: placeholder/undefined: colon的编译错误,说明你没有同时编译多个文件,需要用go run .方式运行同包多文件。
重构后的 01-refactor/solution/placeholders.go 展示了答案的核心模式:
package main type placeholder [5]string var zero = placeholder{ "███", "█ █", "█ █", "█ █", "███", } // ... one 到 nine 同理 ... var colon = placeholder{ " ", " ░ ", " ", " ░ ", " ", } var digits = [...]placeholder{ zero, one, two, three, four, five, six, seven, eight, nine, }而 01-refactor/solution/main.go 只剩主循环逻辑:screen.Clear()清屏 →screen.MoveTopLeft()复位光标 → 取time.Now()的时/分/秒 → 用digits[hour/10]、digits[hour%10]等索引运算组装clock数组 → 逐行逐列打印,time.Sleep(time.Second)控制每秒刷新。
期望输出:重构前后运行结果完全一致。这一练习的价值在于验证你对"同一目录下多个.go文件同属一个包"和"包级变量必须用var声明"两个知识点的掌握。
练习二:Set an Alarm —— 每 10 秒打印 ALARM
目标:在时钟运行过程中,每隔 10 秒将画面切换为" ALARM! "字样。练习文件为 02-alarm/main.go 与 02-alarm/placeholders.go。
注释中给出提示:可以新建一个与clock数组等长的alarm数组,每 10 秒把alarm复制到clock数组。alarm数组由 8 个placeholder组成,其中首尾两个是空白占位符(用于对齐),中间 6 个分别对应 "ALARM" 六个字母的 LED 字形,见 02-alarm/solution/placeholders.go:
var alarm = [...]placeholder{ {" ", " ", " ", " ", " "}, // 空白对齐 {"███", "█ █", "███", "█ █", "█ █"}, // A {"█ ", "█ ", "█ ", "█ ", "███"}, // L {"███", "█ █", "███", "█ █", "█ █"}, // A {"██ ", "█ █", "██ ", "█ █", "█ █"}, // R {"█ █", "███", "█ █", "█ █", "█ █"}, // M {" █ ", " █ ", " █ ", " ", " █ "}, // ! {" ", " ", " ", " ", " "}, // 空白对齐 }核心判据是 02-alarm/solution/main.go 中的一行:
alarmed := sec%10 == 0当秒数能被 10 整除(即每 10 秒一次)时,在逐行打印之前执行clock = alarm,用alarm数组整体替换clock数组——这正是数组可以整体赋值、比较语义的直接体现:
for line := range clock[0] { if alarmed { clock = alarm } // ...打印逻辑不变... }期望输出中," ALARM! " 字形还带有░冒号闪烁效果,与前四步的sec%2 == 0逻辑保持一致。
练习三:Split Second —— 时钟加速到十分之一秒
目标:把刷新粒度从每秒提升到每 1/10 秒(100 毫秒),并在时钟右侧追加一个"百分秒"数字。练习文件为 03-split-second/main.go,注释给出了详细步骤:
- 计算当前的百分秒(split second);
- 在时钟中加入点字符(dot);
- 把百分秒数字加入时钟数组;
- 让点字符像冒号一样每两秒闪烁;
- 把刷新周期从 1 秒改为 1/10 秒。
注释提示:使用Time类型的Nanosecond方法,百分秒就是Nanosecond的第一位数字;并且time.Second是整数常量,可以直接除以数字。
03-split-second/solution/main.go 给出了完整答案:
now := time.Now() hour, min, sec := now.Hour(), now.Minute(), now.Second() ssec := now.Nanosecond() / 1e8 clock := [...]placeholder{ digits[hour/10], digits[hour%10], colon, digits[min/10], digits[min%10], colon, digits[sec/10], digits[sec%10], dot, digits[ssec], }关键点:
now.Nanosecond() / 1e8取出纳秒值的第一位数字(0~9),即为百分秒;clock数组扩为 10 个元素,末尾追加dot和百分秒数字;- 闪烁条件扩展为
if (digit == colon || digit == dot) && sec%2 == 0,点与冒号同节奏闪烁; - 刷新周期改为
const splitSecond = time.Second / 10; time.Sleep(splitSecond),印证了time.Second整数常量可参与除法运算的特性。
期望输出展示了多种百分秒与秒的组合形态,注意当时钟以十分之一秒刷新时,画面会以 10 倍于原版的速度动态更新。
练习四:Ticker(高难度)—— 从右向左滑动时钟
目标:让整个时钟动画从屏幕右侧向左侧连续滑动。这是四道练习中标注HARD EXERCISE的一道,练习文件为 04-ticker/main.go。
04-ticker/solution/main.go 的核心思路是引入一个无限递增的shift变量,用取模运算控制起始索引,同时利用"把时钟长度想象成两倍"的技巧实现回绕:
for shift := 0; ; shift++ { screen.Clear() screen.MoveTopLeft() now := time.Now() hour, min, sec := now.Hour(), now.Minute(), now.Second() clock := [...]placeholder{ digits[hour/10], digits[hour%10], colon, digits[min/10], digits[min%10], colon, digits[sec/10], digits[sec%10], } for line := range clock[0] { l := len(clock) // 用 shift%l 防止越界 s, e := shift%l, l // 当 shift%(l*2) >= l 时,说明时钟已完全滑出屏幕左端, // 重置起始位置为 0,结束位置为 s,形成回绕 if shift%(l*2) >= l { s, e = 0, s } // 打印空白占位,营造从右向左移动的效果 for j := 0; j < l-e; j++ { fmt.Print(" ") } // 从 s 到 e 绘制数字 for i := s; i < e; i++ { next := clock[i][line] if clock[i] == colon && sec%2 == 0 { next = " " } fmt.Print(next, " ") } fmt.Println() } time.Sleep(time.Second) }值得注意的工程细节(源码注释中均有说明):
- 每次循环都调用
screen.Clear(),否则上一帧的字符会残留在屏幕上;另一种替代方案是用空白行填满缺失的占位符; shift%l防止索引越界;shift%(l*2) >= l判定时钟是否已整体滑出左端,从而把(s, e)从(shift%l, l)重置为(0, s);l-e仅在上述if分支生效时才非零,此时前置空白行让"移出屏幕的占位符位置"由空位补齐,正是把时钟长度虚拟为两倍的收益所在。
完成这道练习后,for range遍历、%取模、数组整体比较与索引切片式的打印逻辑就全部串联起来了,这也是文档中所说的"你将真正感到自己掌握了此前学到的所有内容"。
如何运行与验证
每个练习目录都采用"题目 +solution/答案"的双目录结构,例如01-refactor/main.go与01-refactor/solution/main.go。由于重构后涉及同包多文件,运行方式为:
cd 15-project-retro-led-clock/exercises/01-refactor go run . # 或 go run *.go其中go run .会编译当前目录下同一包的所有.go文件,这正是练习一注释中针对undefined: placeholder错误给出的解决方案。程序运行依赖github.com/inancgumus/screen包(见仓库根目录 go.mod),用于终端清屏与光标定位,因此请在支持 ANSI 转义的终端中运行以获得最佳显示效果。
各练习的期望输出都写在对应main.go顶部的注释块中(如闹钟的 " ALARM! " 五层字形、百分秒时钟的多种组合形态),你可以据此逐帧比对验证自己的实现是否正确。
小结
这四道练习环环相扣:Refactor 强调代码组织与包级作用域;Alarm 展示数组整体赋值替换;Split Second 把time.Nanosecond、整数常量除法与数组扩围结合;Ticker 则以取模回绕算法完成高难度的滑动动画。它们共同覆盖了learngo课程中数组章节的全部核心知识点,适合在学习完 14-arrays 后通过"动手改错"的方式巩固掌握。若某一步卡住,项目主 README 还提供了分步解答(05-full-annotated-solution/main.go),可以按步骤对照而不必一次看完整答案。
- 示例工程
- 教程
【免费下载链接】learngo
❤️ 1000+ Hand-Crafted Go Examples, Exercises, and Quizzes. 🚀 Learn Go by fixing 1000+ tiny programs.
相关推荐
Zulip 部署在反向代理后面时如何配置信任代理和转发头
Zulip 部署在反向代理后面时如何配置信任代理和转发头 如果你的 Zulip 服务器不直接暴露给公网,而是要放在 nginx、Apache2 或 HAProx
示例工程教程如何快速上手BrainRender:神经解剖数据可视化的终极指南
如何快速上手BrainRender:神经解剖数据可视化的终极指南 BrainRender是一个强大的Python开源工具,专门用于在脑图谱空间中可视化神经解剖数
S3Proxy测试策略:使用本地文件系统模拟云端存储环境
S3Proxy测试策略:使用本地文件系统模拟云端存储环境 S3Proxy是一款强大的工具,它允许通过S3 API访问其他存储后端,为开发者提供了极大的便利。在开
后端API网关
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考