主题
Go 学习
目标:一周上手,能读懂/修改 k8s-installer。教学方式:Hermes 陪练(概念讲透 → 自己写 → 反馈纠错),每课必须亲手敲代码并运行。
- 环境:Go 1.26.5,GOROOT
D:\data\env\go,GOPROXYgoproxy.cn - 沙盒:
D:\data\project\go-learn(hello / vars / slice / struct 等目录,一课一目录) - 实战项目:
D:\data\project\k8s-installer
语法速查卡
- Go 语法速查卡(持续更新,所有课的知识点汇总)
课程路线图
主线课程,严格按此推进——不从"项目里出现了什么语法"去重排顺序(项目需要什么 ≠ 课程该教什么)。每课必亲手敲代码 + 运行验证 + 同步笔记。
| 阶段 | 内容 | 状态 |
|---|---|---|
| 第 1 周 | 语法骨架:package/main、变量与类型、函数、流程控制、struct | ✅ 完成(7 课) |
| 第 2 周 | slice、map、指针、error 处理 | ✅ 完成 |
| 第 3 周 | 接口、goroutine/channel、文件读写 | ✅ 完成(5 课) |
| 第 4 周 | 实战:在 k8s-installer 里挑模块改造 | ⬜ 下一阶段 |
第 3 周细分(已完成)
| 顺序 | 内容 | 练习 | 状态 |
|---|---|---|---|
| 1 | 接口 | interface-basics / runner-interface | ✅ |
| 2 | 并发 goroutine + WaitGroup | concurrent-precheck | ✅ |
| 3 | 数据竞争与 Mutex(原路线图外新增,并发必修延伸) | race-demo | ✅ |
| 4 | channel | channel-precheck | ✅ |
| 5 | 文件读写(os.ReadFile / WriteFile / MkdirAll + defer 关文件) | file-report | ✅ |
第 4 周:整周实战。零碎语法(闭包 / %w / flag / 包组织 / 标准库细节)不单独设课,在实战中撞到再补。
诚实口径:时间不是关键变量——只"看课不敲"学一年也是零基础;每课必敲 + 30~60 分钟/天才有意义。
学习进度
2026-09-07(Day 1)语法骨架
| 课 | 内容 | 练习 | 状态 |
|---|---|---|---|
| 1 | 程序结构 package/import/main、Println、转义字符 | hello 输出三行 | ✅ |
| 2 | 变量(var/:=/=)、类型、Printf 占位符、静态类型检查 | 打印集群信息 | ✅ |
| 3 | 函数:签名、参数类型、多返回值 | printCluster 复用 | ✅ |
| 4 | if/else、多返回值接收、err 判断 | checkCluster 健康检查 | ✅ |
| 5 | for range、slice、append、len | 节点巡检 | ✅ |
| 6 | struct 定义/实例化/嵌套、方法(值接收者)、yaml 标签 | Node.describe() | ✅ |
| 7 | 指针接收者、... 展开、AllNodes 合并 | Config.AllNodes() | ✅ |
真实代码通读(k8s-installer)
| 文件 | 学到的模式 | 状态 |
|---|---|---|
internal/config/config.go | struct 映射 YAML、Load 读文件、错误处理模式 | ✅ |
main.go runPrecheck | for range + err 判断 + failed 计数 + os.Exit(1) | ✅ |
main.go runInit | 匿名函数/闭包(step 执行器)、7 阶段流水线 | ✅ |
internal/phase/prepare.go | error 契约:成功 nil、失败非 nil | ✅ |
main.go runRemoveNode / runReset | 指针查找(&/*/nil)、break、fmt.Scanln 二次确认 | ✅ |
2026-09-08(Day 2+)真实改码实战
| 内容 | 学到 | 状态 |
|---|---|---|
| 修 master.go 重复上传 + kubeconfig base64 化 | buildKubeconfig 签名修改 → 连带 node.go 调用点;改一个函数签名要搜全所有调用者 | ✅ |
| 修 docker/etcd 幂等误判 | err == nil vs out != "" 判断命令成功;test -x 查固定路径 | ✅ |
| ssh-error 实验课 | 方法签名、*ssh.Client 指针返回、&ssh.ClientConfig、[]ssh.AuthMethod 切片 | ✅ 待运行 |
2026-09-09(Day 3)连接生命周期收尾 + 错误分支
| 内容 | 学到 | 状态 |
|---|---|---|
| conn-lifecycle 收尾 | 建连→用→关 生命周期;fmt.Sprintf vs fmt.Printf(返回 (int,error) 不是 string);返回值不接就丢 | ✅ 实跑验收 |
| ssh-error 运行 | error 值是具体失败原因;%w 把底层错误串上来;失败走错误分支、程序不崩 | ✅ |
| defer 课 | defer 注册"函数返回时执行"、LIFO 顺序、清理动作不遗漏;defer 那行只登记不执行 | ✅ 实跑验收 |
| map 课 | 键值对、comma-ok、delete、range 返回 (键,值)、nil map 写入 panic;值可以是 struct | ✅ 实跑验收 |
| interface 课 | 接口=方法集合的契约、隐式实现、静态类型 vs 动态类型、多态 | ✅ 实跑验收 |
| runner-interface 课 | 给 FakeConn/真实连接抽共同接口,依赖注入(生产塞真的、测试塞假的) | ✅ 实跑验收 |
2026-09-10(Day 4)并发第一课
| 内容 | 学到 | 状态 |
|---|---|---|
| goroutine 入门 | go f() 这一行不再阻塞;启动顺序 ≠ 代码顺序(实测启动 master-1,worker-2,worker-1、收尾 worker-1,worker-2,master-1) | ✅ 实跑验收 |
| 主 goroutine 退出 = 进程退出 | 只加 go 不加等待 → 输出丢失(实测只打了一行,总耗时 0s) | ✅ 实跑验收 |
| sync.WaitGroup | 计数闸门 Add/Done/Wait、零值即可用、必须传指针、Add 要在 go 之前 | ✅ 实跑验收 |
| nil 指针 panic | var wg *sync.WaitGroup 只给 nil 零值 → Add 崩;学会读 panic 三步 | ✅ 实跑验收 |
| 串行 vs 并发实测 | 一次运行同时测:3.0023738s → 1.0012883s;收益来自"等"不是"算"(I/O 密集近线性) | ✅ 实跑验收 |
| 计时:时刻 vs 时长 | time.Now() 是时刻、time.Since 才是时长;Duration 字面量只认纳秒,要写 1 * time.Second | ✅ 实跑验收 |
| 顺带洞察 | 每轮两次 Println ≈550µs ≫ Sleep(1µs) → 日志 I/O 比计算贵几百倍 | ✅ 实测 |
练习:go-learn/concurrent-precheck/main.go(串行段 / 并发段并列,一次跑出两个耗时对比)
2026-09-10(Day 4 续)并发第二课:数据竞争
| 内容 | 学到 | 状态 |
|---|---|---|
| 数据竞争现象 | 1000 个 goroutine 抢 count++ → 981/979/984/979/991(丢 1~2%),不报错、退出码 0 | ✅ 实跑验收 |
-race 检测器 | 装上 mingw + CGO_ENABLED=1 打通;报 WARNING: DATA RACE 定位到行 + Found 2 data race(s) + exit status 66 | ✅ 实跑验收 |
| 读 race 报告 | 相同地址=同一个变量;gowrap1 是编译器生成的壳;Found N 是去重后的模式数不是总数 | ✅ 实跑验收 |
sync.Mutex 修复 | Lock + defer Unlock 只夹共享变量;LIFO 让 Unlock 先于 Done;连跑 10 次精确 1000、-race 零 WARNING | ✅ 实跑验收 |
| 方法论:证据不对称 | "N 次全对"不能证明没 bug,只有 -race 干净才算数;"重复跑 N 次"是调并发 bug 的基本功 | ✅ 建立 |
练习:go-learn/race-demo/main.go(无锁丢数 → -race 抓现行 → Mutex 修 → 双验收)
环境变更:winget 装 WinLibs mingw-w64 gcc 16.1.0 + go env -w CGO_ENABLED=1,解锁 Windows 上的 -race
2026-09-11(Day 5)channel
| 内容 | 学到 | 状态 |
|---|---|---|
| channel 基础 | make(chan string) 带类型的管道;ch <- v 塞、<-ch 拿;无缓冲 = 当面交货(没人接就卡住) | ✅ 实跑验收 |
| 两种收法 | 知道条数用固定次数 <-ch;不知道条数用 range,但 range 只认 close 这个信号 | ✅ 实跑验收 |
| 不 close → 死锁 | fatal error: all goroutines are asleep - deadlock!,goroutine 1 [chan receive],退出码 2 | ✅ 实测 |
worker 各自 close | 只收到 1 条结果(3 个节点),另外 2 个 panic: send on closed channel;程序还照样打印"全部检查完成" | ✅ 实测 |
wg.Wait() 写在 main 里 | 主等 wg、worker 等接收 → 互相等,死锁闭环(实测报告两边栈都指向自己代码) | ✅ 实测 |
| 标准收尾模式 | WaitGroup + 专职 close 的 goroutine(go func(){ wg.Wait(); close(ch) }()):主 goroutine 专心接收,worker 发送不阻塞 | ✅ 实跑验收 |
| 匿名函数 | 没名字的 func();是值,要么赋值后用、要么末尾加 () 当场执行;go func(){...}() = 起完就扔 | ✅ 实跑验收 |
Go 裸块 { } 的真相 | 只是作用域,不改变执行权 —— 写不写,执行者都是主 goroutine | ✅ 实测 |
| 货升级为 struct | chan string → chan checkResult(节点 + 通过/失败);换货要连带改四处类型,wg/close/range 收尾一个字不用动 | ✅ 实跑验收 |
| 一个布尔值怎么算 | 比较运算结果就是 bool(ok := nodeName != "worker-2"),不需要 if —— if 决定"做什么",比较运算决定"是什么" | ✅ 实跑验收 |
练习:go-learn/channel-precheck/main.go(3 节点并发 → 结果经 channel 回传 → WaitGroup 收尾 + 专职 close;货物从裸名字升级为 struct;最终 -race 干净、退出码 0、总耗时 1.0004007s,输出 master-1 true / worker-1 true / worker-2 false)
关键认知:close 不是"作废管道",是广播"不会再有新货了";多发送方场景必须有人等所有人发完才能关 —— 这就是 WaitGroup 和 channel 缝在一起的原因。
2026-09-11(Day 5 续)文件读写 · 第 3 周收尾
| 内容 | 学到 | 状态 |
|---|---|---|
| 两类 API | 便捷函数 os.WriteFile/ReadFile(自己开关文件,不用 defer) vs 手动 os.Create(返回 *os.File,必须 defer f.Close()) | ✅ 实跑验收 |
| 为什么便捷函数不用 defer | 看标准库源码 os/file.go:ReadFile 内部 Open + defer f.Close() —— 谁攥着 *os.File 谁负责关 | ✅ 源码取证 |
Mkdir vs MkdirAll | Mkdir 目录已存在就报 file exists(跑第二次就炸);MkdirAll 幂等 —— 同 docker/etcd 幂等问题一类 | ✅ 实测 |
| 权限八进制 | 0755/0644 前导 0 不能省;写 644 编译通过、权限乱掉(静默错) | ✅ |
[]byte ↔ string | 写要 []byte(s)、读回来要 string(b) 才能打印 | ✅ 实跑验收 |
| 错误契约的两种走法 | 依赖它 → return err/os.Exit(1)(失败即终止);不依赖 → 打印后继续(退出码 0)。反例实测:只 Println 不 return → 目录没建成却打印"报告已写入"、退出码 0,随后读到的还是上一次的旧文件 | ✅ 实测 |
f, _ := os.Create(...) 的坑 | 丢 err → f 为 nil 却照样 defer+WriteString;os.File 有 nil 保护只报 invalid argument,真正原因被藏起来 | ✅ 实测 |
| 相对路径的基准 | = 进程的工作目录,不是源码/exe 目录。实测同一份代码:GoLand 跑到 go-learn\reports\(项目根),命令行 go run . 跑到 file-report\reports\ | ✅ 实测对比 |
| 排障探针 | wd, _ := os.Getwd() —— 文件"跑没了/跑歪了"先打这一行 | ✅ |
练习:go-learn/file-report/main.go(MkdirAll + WriteFile + ReadFile 回读 + 故意读失败 + os.Create + defer f.Close();验证:正常跑退出码 0、两文件各 44 字节,把目标换成同名目录 → open reports/manual.txt: is a directory + 退出码 1)
关键认知:错误被打印 ≠ 错误被处理。if err != nil { fmt.Println(err) } 之后继续往下走,等于把失败藏起来 —— 判断标准只有一条:后面的代码还依赖这个结果吗?
下一步(按课程路线图)
第 1~3 周(语法骨架 / slice-map-指针-error / 接口-并发-文件读写)已全部完成。下一阶段:
- [ ] 第 4 周实战:把
runPrecheck改成并发版 —— 9 节点并行 +chan checkResult回传结果 + WaitGroup 收尾 + 日志不乱(前面 5 课练的正是这条链) - [ ] 顺带把管道里的货做成"节点 + 结果 + 耗时/错误详情"的 struct(练过,加字段比改签名便宜)
- [ ] 挑第二个模块改造收尾(候选:离线镜像导入
phase.PreloadImages)