跳转到内容

Go 学习

目标:一周上手,能读懂/修改 k8s-installer。教学方式:Hermes 陪练(概念讲透 → 自己写 → 反馈纠错),每课必须亲手敲代码并运行。

  • 环境:Go 1.26.5,GOROOT D:\data\env\go,GOPROXY goproxy.cn
  • 沙盒:D:\data\project\go-learn(hello / vars / slice / struct 等目录,一课一目录)
  • 实战项目:D:\data\project\k8s-installer

语法速查卡

课程路线图

主线课程,严格按此推进——不从"项目里出现了什么语法"去重排顺序(项目需要什么 ≠ 课程该教什么)。每课必亲手敲代码 + 运行验证 + 同步笔记。

阶段内容状态
第 1 周语法骨架:package/main、变量与类型、函数、流程控制、struct✅ 完成(7 课)
第 2 周slice、map、指针、error 处理✅ 完成
第 3 周接口、goroutine/channel、文件读写✅ 完成(5 课)
第 4 周实战:在 k8s-installer 里挑模块改造下一阶段

第 3 周细分(已完成)

顺序内容练习状态
1接口interface-basics / runner-interface
2并发 goroutine + WaitGroupconcurrent-precheck
3数据竞争与 Mutex(原路线图外新增,并发必修延伸)race-demo
4channelchannel-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 复用
4if/else、多返回值接收、err 判断checkCluster 健康检查
5for range、slice、append、len节点巡检
6struct 定义/实例化/嵌套、方法(值接收者)、yaml 标签Node.describe()
7指针接收者、... 展开、AllNodes 合并Config.AllNodes()

真实代码通读(k8s-installer)

文件学到的模式状态
internal/config/config.gostruct 映射 YAML、Load 读文件、错误处理模式
main.go runPrecheckfor range + err 判断 + failed 计数 + os.Exit(1)
main.go runInit匿名函数/闭包(step 执行器)、7 阶段流水线
internal/phase/prepare.goerror 契约:成功 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 指针 panicvar wg *sync.WaitGroup 只给 nil 零值 → Add 崩;学会读 panic 三步✅ 实跑验收
串行 vs 并发实测一次运行同时测:3.0023738s1.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✅ 实测
货升级为 structchan stringchan 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 MkdirAllMkdir 目录已存在就报 file exists(跑第二次就炸);MkdirAll 幂等 —— 同 docker/etcd 幂等问题一类✅ 实测
权限八进制0755/0644 前导 0 不能省;写 644 编译通过、权限乱掉(静默错)
[]bytestring写要 []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)

基于 MIT 许可发布