go-backend-handbook 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/00-linux.md +175 -0
  2. package/01-go-basics.md +402 -0
  3. package/02-service-architecture.md +156 -0
  4. package/03-concurrency.md +278 -0
  5. package/04-mysql.md +362 -0
  6. package/05-database-selection.md +169 -0
  7. package/06-redis.md +197 -0
  8. package/07-message-queue.md +168 -0
  9. package/08-api-and-rpc.md +189 -0
  10. package/09-observability.md +164 -0
  11. package/10-resilience.md +173 -0
  12. package/11-monitoring.md +180 -0
  13. package/12-testing.md +176 -0
  14. package/13-performance-debug.md +174 -0
  15. package/14-security.md +172 -0
  16. package/15-development-process.md +157 -0
  17. package/16-api-docs.md +288 -0
  18. package/17-deploy-ops.md +273 -0
  19. package/18-ai-assisted.md +120 -0
  20. package/19-appendix.md +193 -0
  21. package/LICENSE +21 -0
  22. package/README.md +100 -2
  23. package/code/mini-coin/.gitlab-ci.yml +24 -0
  24. package/code/mini-coin/README.md +22 -0
  25. package/code/mini-coin/api/coin.proto +31 -0
  26. package/code/mini-coin/cmd/gateway/main.go +20 -0
  27. package/code/mini-coin/config/dev.json +8 -0
  28. package/code/mini-coin/config/prod.json +8 -0
  29. package/code/mini-coin/deploy/Dockerfile +12 -0
  30. package/code/mini-coin/docs/architecture.md +14 -0
  31. package/code/mini-coin/docs/changes/CHG-001-recharge/change-record.yaml +9 -0
  32. package/code/mini-coin/docs/changes/CHG-001-recharge/intent.md +10 -0
  33. package/code/mini-coin/docs/changes/CHG-001-recharge/plan.md +7 -0
  34. package/code/mini-coin/docs/changes/CHG-001-recharge/spec.md +10 -0
  35. package/code/mini-coin/docs/snippets/prometheus_metrics.go.txt +13 -0
  36. package/code/mini-coin/docs/snippets/redis_rate.go.txt +7 -0
  37. package/code/mini-coin/docs/snippets/zap_log.go.txt +12 -0
  38. package/code/mini-coin/go.mod +3 -0
  39. package/code/mini-coin/internal/cache/memory.go +43 -0
  40. package/code/mini-coin/internal/config/config.go +28 -0
  41. package/code/mini-coin/internal/httpapi/handler.go +88 -0
  42. package/code/mini-coin/internal/metrics/metrics.go +33 -0
  43. package/code/mini-coin/internal/metrics/metrics_test.go +17 -0
  44. package/code/mini-coin/internal/model/account.go +22 -0
  45. package/code/mini-coin/internal/mq/memory.go +31 -0
  46. package/code/mini-coin/internal/ratelimit/fixed_window.go +39 -0
  47. package/code/mini-coin/internal/ratelimit/fixed_window_test.go +19 -0
  48. package/code/mini-coin/internal/repository/memory.go +64 -0
  49. package/code/mini-coin/internal/security/sign.go +29 -0
  50. package/code/mini-coin/internal/security/sign_test.go +15 -0
  51. package/code/mini-coin/internal/service/recharge.go +97 -0
  52. package/code/mini-coin/internal/service/recharge_test.go +35 -0
  53. package/code/mini-coin/internal/worker/batch.go +44 -0
  54. package/code/mini-coin/migrations/001_create_account.sql +12 -0
  55. package/code/mini-coin/migrations/002_create_recharge_order.sql +12 -0
  56. package/code/mini-coin/migrations/003_add_channel.sql +4 -0
  57. package/code/mini-coin/scripts/e2e.sh +10 -0
  58. package/code/mini-coin/scripts/loadtest.js +26 -0
  59. package/exercises/README.md +73 -0
  60. package/package.json +14 -4
package/00-linux.md ADDED
@@ -0,0 +1,175 @@
1
+ # 第 0 章 开发机与 Linux 入门
2
+
3
+ > 目标:能连上远程开发机、看懂机器状态、出问题能自己排查第一层。
4
+
5
+ ## 0.1 为什么后端要有一台"开发机"
6
+
7
+ 前端本地跑个 dev server 就够了;后端不行,因为依赖太多:
8
+
9
+ - MySQL / Redis / Kafka 通常跑在机房或容器平台;
10
+ - 服务要访问测试环境、注册中心、配置中心;
11
+ - 编译产物、日志、抓包、压测都需要一台长期在线的 Linux。
12
+
13
+ 所以团队一般给每人一台**远程 Linux 开发机**,代码、编译、联调都在上面做。
14
+
15
+ ## 0.2 怎么连
16
+
17
+ ```bash
18
+ # 1. 看本机 SSH 配置里有没有目标主机
19
+ cat ~/.ssh/config
20
+
21
+ # 2. 连接(主机别名来自 ssh config)
22
+ ssh devbox
23
+
24
+ # 3. 首次连接会问 yes/no,输入 yes
25
+ # 4. 免密登录靠公钥:把 ~/.ssh/id_ed25519.pub 内容加到目标机 ~/.ssh/authorized_keys
26
+ ```
27
+
28
+ `~/.ssh/config` 示例(**用占位符,不要照抄真实内网地址**):
29
+
30
+ ```
31
+ Host devbox
32
+ HostName devbox.example.com
33
+ Port 22
34
+ User demo
35
+ IdentityFile ~/.ssh/id_ed25519_demo
36
+ IdentitiesOnly yes
37
+ ServerAliveInterval 30
38
+ ```
39
+
40
+ `ServerAliveInterval 30` 能防止长时间不操作被防火墙断开。
41
+
42
+ ### 连不上,按这个顺序排查
43
+
44
+ | 现象 | 排查命令 | 常见原因 |
45
+ |---|---|---|
46
+ | `Connection timed out` | `ping devbox.example.com`、`nc -vz host 22` | 不在内网/VPN、防火墙、安全组 |
47
+ | `Connection refused` | `nc -vz host 22` | sshd 没启动、端口错 |
48
+ | `Permission denied (publickey)` | `ssh -vvv devbox` | 公钥没放对、权限不是 600 |
49
+ | `Host key verification failed` | `ssh-keygen -R host` | 机器重装,主机指纹变了 |
50
+ | 连上就断 | `ssh -vvv`,看服务端 `/var/log/auth.log` | 账号被锁、磁盘满、sshd 限制 |
51
+ | 卡在 `Connecting` | `curl -v telnet://host:22` | 代理/跳板机问题 |
52
+
53
+ 固定排查套路:**网络通不通 → 端口开不开 → 认证过不过 → 登录后环境对不对**。
54
+
55
+ ```bash
56
+ # 一次性确认四层
57
+ ping -c 2 devbox.example.com
58
+ nc -vz devbox.example.com 22
59
+ ssh -o BatchMode=yes devbox 'echo ok'
60
+ ssh devbox 'hostname; whoami; uptime'
61
+ ```
62
+
63
+ ## 0.3 登录后先看什么
64
+
65
+ ```bash
66
+ hostnamectl # 主机名、系统版本、内核
67
+ whoami; id # 我是谁、在哪些组
68
+ pwd; df -h # 当前目录、磁盘
69
+ free -h # 内存
70
+ nproc # CPU 核数
71
+ uptime # 负载
72
+ top -b -n1 | head -20
73
+ ```
74
+
75
+ **负载(load average)怎么读**:三个数分别是 1/5/15 分钟平均。经验值:`load > CPU 核数` 说明有排队,`load > 2 × 核数` 说明明显过载。但 IO 密集时 load 高不一定 CPU 忙,要结合 `%wa` 看。
76
+
77
+ ## 0.4 磁盘和内存满了怎么办
78
+
79
+ ### 磁盘
80
+
81
+ ```bash
82
+ df -h # 哪个分区满
83
+ du -sh /var/* 2>/dev/null | sort -h # 一级目录谁大
84
+ du -sh /var/log/* | sort -h | tail # 日志大户
85
+ lsof +L1 # 已删除但被进程占用的文件(空间不释放的元凶)
86
+ ```
87
+
88
+ 处理顺序:
89
+
90
+ 1. **先止血**:清日志前先确认不是审计/事故证据,优先 `truncate -s 0 file` 而不是 `rm`(`rm` 对正在写的文件不释放空间)。
91
+ 2. **找根因**:日志没轮转?临时文件没清?core dump?
92
+ 3. **加防线**:logrotate、磁盘告警阈值 80%/90%、容器日志大小限制。
93
+
94
+ ```bash
95
+ # 安全清空一个正在被写的日志
96
+ truncate -s 0 /var/log/app/app.log
97
+
98
+ # 找 7 天前的临时文件(先看再删)
99
+ find /tmp -type f -mtime +7 -ls | head
100
+ ```
101
+
102
+ ### 内存
103
+
104
+ ```bash
105
+ free -h
106
+ ps aux --sort=-%mem | head
107
+ cat /proc/meminfo | head
108
+ ```
109
+
110
+ Linux 会把空闲内存用作 page cache,`free` 的 `available` 才是真正可用的。如果 `available` 很低且 `swap` 在涨,就是真紧张。处理:限容器内存、查泄漏(pprof)、加 swap 只是缓冲不是方案。
111
+
112
+ ## 0.5 必会命令清单
113
+
114
+ ```bash
115
+ # 文件
116
+ ls -lah; cd; pwd; cp -r; mv; mkdir -p; find . -name '*.go'
117
+ # 文本
118
+ cat; less; head -n 50; tail -f; grep -rn 'keyword' .; sed -n '10,20p' file
119
+ # 进程
120
+ ps aux | grep app; top; kill -15 PID; kill -9 PID # 先 15 再 9
121
+ # 端口
122
+ ss -lntp | grep 8080; lsof -i:8080
123
+ # 网络
124
+ curl -v; curl -sS -w '%{http_code} %{time_total}\n' -o /dev/null URL
125
+ # 压缩
126
+ tar -czf x.tar.gz dir/; tar -xzf x.tar.gz
127
+ # 权限
128
+ chmod 600 key; chown user:group file
129
+ ```
130
+
131
+ `kill -15`(SIGTERM)让程序优雅退出,`kill -9` 强杀不给清理机会,生产环境优先 15。
132
+
133
+ ## 0.6 环境变量与 shell
134
+
135
+ ```bash
136
+ export APP_ENV=dev
137
+ env | grep APP_
138
+ echo $PATH
139
+ which go
140
+ ```
141
+
142
+ `~/.bashrc` 放交互式配置,`~/.bash_profile` 放登录配置。**不要把密码写进 shell 历史**:用 `read -s` 或密钥管理。
143
+
144
+ ## 习题
145
+
146
+ 1. 写出连接 `devbox.example.com`、用户 `demo`、密钥 `~/.ssh/id_ed25519_demo` 的 ssh config 片段。
147
+ 2. `ssh` 报 `Permission denied (publickey)`,列出至少 4 个可能原因和验证命令。
148
+ 3. 磁盘 100%,`du` 显示 `/var/log` 只有 2G,但 `df` 显示少了 20G,怎么查?
149
+ 4. `free -h` 显示 free 很低但 available 很高,正常吗?为什么?
150
+ 5. load average = 16,机器 8 核,说明什么?还要看哪个指标才能判断是 CPU 还是 IO 问题?
151
+ 6. 用一条命令找出当前监听 8080 端口的进程。
152
+ 7. 为什么清理正在写入的日志推荐 `truncate` 而不是 `rm`?
153
+ 8. 写一个命令,输出某 URL 的 HTTP 状态码和总耗时。
154
+
155
+ ## 参考答案
156
+
157
+ <details>
158
+ <summary>展开</summary>
159
+
160
+ 1.
161
+ ```
162
+ Host devbox
163
+ HostName devbox.example.com
164
+ User demo
165
+ IdentityFile ~/.ssh/id_ed25519_demo
166
+ IdentitiesOnly yes
167
+ ```
168
+ 2. 公钥未加入 `authorized_keys`;`~/.ssh` 或公钥权限过宽(应为 700/600);ssh config 指错 IdentityFile;服务端禁用该用户或 `PubkeyAuthentication no`;密钥类型不被服务端支持。验证:`ssh -vvv devbox` 看 `Offering public key` 与 `Authentications that can continue`;`ssh devbox 'ls -ld ~ ~/.ssh ~/.ssh/authorized_keys'`。
169
+ 3. `lsof +L1` 找 deleted 但被进程占用的文件;常见是应用日志被 `rm` 后进程仍持有 fd。
170
+ 4. 正常。page cache 会占用 free,`available` 估算可回收部分。真紧张看 `available` 和 swap 使用。
171
+ 5. 有明显排队/过载。还要看 `top` 的 `%us/%sy/%wa`、`iostat -x 1` 的 `%util` 和 await。
172
+ 6. `ss -lntp | grep :8080` 或 `lsof -i:8080`。
173
+ 7. `rm` 只删目录项,进程仍持有 fd,空间不释放;`truncate` 直接把文件长度置 0,立即释放。
174
+ 8. `curl -sS -o /dev/null -w '%{http_code} %{time_total}\n' https://api.example.com/healthz`
175
+ </details>
@@ -0,0 +1,402 @@
1
+ # 第 1 章 Go 语言基础(JS 对照版)
2
+
3
+ > 目标:会 JS 的人,2 小时能读懂 Go 业务代码,1 周能写。
4
+ > 重点不是穷举语法,而是**JS 有但 Go 没有的东西**,以及**Go 有而 JS 没有的东西**。
5
+
6
+ ## 1.1 先建立一个心智模型
7
+
8
+ | JS | Go |
9
+ |---|---|
10
+ | 动态类型,运行时才知道类型 | 静态类型,编译期就检查 |
11
+ | 单线程 + 事件循环 | 多协程 + 调度器,真并发 |
12
+ | 函数是第一公民 | 函数也是值,但更常用接口 |
13
+ | `class` + 原型 | `struct` + 方法,没有继承 |
14
+ | 异常 `try/catch` | 错误是返回值 `if err != nil` |
15
+ | `null` / `undefined` | 每种类型有零值,`nil` 只用于指针/接口/map/slice/channel/func |
16
+ | `npm` | `go mod` |
17
+ | `node app.js` | `go run .` 开发 / `go build` 出二进制 |
18
+ | 自动 GC | 自动 GC(并发三色标记) |
19
+
20
+ ## 1.2 一个最小程序
21
+
22
+ ```go
23
+ package main
24
+
25
+ import "fmt"
26
+
27
+ func main() {
28
+ fmt.Println("Hello, 后端")
29
+ }
30
+ ```
31
+
32
+ ```bash
33
+ go run . # 编译到临时目录并立即执行
34
+ go build -o app . # 只编译,产出 ./app 二进制
35
+ ./app # 运行产物
36
+ ```
37
+
38
+ ### `go run` 和 `go build` 到底差在哪
39
+
40
+ | | `go run` | `go build` |
41
+ |---|---|---|
42
+ | 产物 | 编译到临时目录,执行完通常不可见 | 当前目录生成可执行文件(`-o` 指定名) |
43
+ | 用途 | 本地快速调试 | 打发布包 / CI / Docker 镜像 |
44
+ | 速度 | 每次都要编译(有缓存会快) | 编译一次,可以反复跑 |
45
+ | 环境 | 需要 Go 工具链 | 只需要二进制(静态链接时无需 Go) |
46
+ | 能不能上线 | 不能 | 能 |
47
+
48
+ > 上线永远用 `go build` 的产物。`go run` 只是开发工具,它不会热更新,改了代码要重新跑。
49
+
50
+ 交叉编译(在 Linux 上给别的平台出包):
51
+
52
+ ```bash
53
+ GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o app .
54
+ ```
55
+
56
+ `-trimpath` 去掉本机路径,`-s -w` 去符号表减小体积,`CGO_ENABLED=0` 产出静态二进制、方便塞进 scratch 镜像。
57
+
58
+ ## 1.3 变量、常量、零值
59
+
60
+ ```go
61
+ var a int = 1
62
+ b := 2 // := 只能在函数内
63
+ const MaxRetry = 3
64
+
65
+ var s string // ""
66
+ var i int // 0
67
+ var p *int // nil
68
+ var ok bool // false
69
+ ```
70
+
71
+ **零值设计**让 Go 不需要构造函数也能安全使用很多类型:`var buf bytes.Buffer` 直接能用。
72
+
73
+ JS 里 `let x;` 是 `undefined`,Go 里是类型的零值,不会出现 `undefined is not a function`。
74
+
75
+ ## 1.4 基础类型
76
+
77
+ ```go
78
+ bool
79
+ string
80
+ int int8 int16 int32 int64
81
+ uint uint8 ... uintptr
82
+ float32 float64
83
+ byte // = uint8
84
+ rune // = int32,表示一个 Unicode 码点
85
+ ```
86
+
87
+ **钱不要用 float**。用 `int64` 存"分",或用 `decimal` 库。
88
+
89
+ ```go
90
+ type Money int64 // 单位:分
91
+
92
+ func (m Money) Yuan() string {
93
+ return fmt.Sprintf("%d.%02d", int64(m)/100, int64(m)%100)
94
+ }
95
+ ```
96
+
97
+ 字符串在 Go 里是**不可变字节序列**,`len(s)` 是字节数不是字符数:
98
+
99
+ ```go
100
+ s := "你好"
101
+ fmt.Println(len(s)) // 6(UTF-8 字节)
102
+ fmt.Println(utf8.RuneCountInString(s)) // 2
103
+ ```
104
+
105
+ ## 1.5 数组、切片、map
106
+
107
+ ```go
108
+ arr := [3]int{1, 2, 3} // 数组:长度是类型的一部分,少用
109
+ s := []int{1, 2, 3} // 切片:最常用
110
+ s = append(s, 4)
111
+
112
+ m := map[string]int{"a": 1}
113
+ m["b"] = 2
114
+ v, ok := m["c"] // 不存在时 v=0, ok=false
115
+ if !ok { /* 处理 */ }
116
+ delete(m, "a")
117
+ ```
118
+
119
+ ### 切片最常见的坑:共享底层数组
120
+
121
+ ```go
122
+ a := []int{1, 2, 3, 4}
123
+ b := a[1:3] // b 和 a 共用底层数组
124
+ b[0] = 99
125
+ fmt.Println(a) // [1 99 3 4]
126
+ ```
127
+
128
+ 要独立副本就 `append([]int(nil), b...)` 或 `slices.Clone(b)`。
129
+
130
+ ### nil 切片 vs 空切片
131
+
132
+ ```go
133
+ var a []int // nil,len=0,可 append
134
+ b := []int{} // 非 nil,len=0
135
+ ```
136
+
137
+ JSON 序列化时 `nil` 变 `null`,空切片变 `[]`。接口返回列表时通常要保证是 `[]` 而不是 `null`。
138
+
139
+ ## 1.6 结构体与方法
140
+
141
+ ```go
142
+ type Account struct {
143
+ UID string
144
+ Balance int64 // 分
145
+ }
146
+
147
+ func (a *Account) Deposit(amount int64) error {
148
+ if amount <= 0 {
149
+ return fmt.Errorf("amount must be positive: %d", amount)
150
+ }
151
+ a.Balance += amount
152
+ return nil
153
+ }
154
+ ```
155
+
156
+ - **值接收者**:拷贝一份,改不到原对象。
157
+ - **指针接收者**:能修改原对象,且避免大结构体拷贝。
158
+ - 一个类型的方法集要求一致:要么都用指针接收者,要么都用值,混用容易踩接口实现问题。
159
+
160
+ JS 的 `this` 在 Go 里是显式接收者,没有 `this` 绑定问题。
161
+
162
+ ### 嵌入(组合代替继承)
163
+
164
+ ```go
165
+ type Base struct{ ID int64 }
166
+ func (b Base) Describe() string { return fmt.Sprintf("id=%d", b.ID) }
167
+
168
+ type User struct {
169
+ Base // 匿名嵌入,User 自动获得 Describe 方法
170
+ Name string
171
+ }
172
+ ```
173
+
174
+ Go 没有 `extends`,只有组合。接口也是隐式实现:
175
+
176
+ ```go
177
+ type Reader interface{ Read(p []byte) (int, error) }
178
+
179
+ type MyReader struct{}
180
+ func (MyReader) Read(p []byte) (int, error) { return 0, io.EOF }
181
+
182
+ var r Reader = MyReader{} // 不用 implements 声明
183
+ ```
184
+
185
+ ## 1.7 错误处理
186
+
187
+ Go 的错误是普通值:
188
+
189
+ ```go
190
+ func Load(id string) (*Account, error) {
191
+ if id == "" {
192
+ return nil, errors.New("empty id")
193
+ }
194
+ a, err := repo.Get(id)
195
+ if err != nil {
196
+ return nil, fmt.Errorf("load account %s: %w", id, err)
197
+ }
198
+ return a, nil
199
+ }
200
+ ```
201
+
202
+ - `%w` 包装错误,保留链,可用 `errors.Is/As` 判断。
203
+ - 不要写 `panic` 处理业务错误;panic 只用于不可恢复的程序错误。
204
+ - 错误信息要带**上下文**,但不要带密码、token、完整身份证号。
205
+
206
+ ```go
207
+ if errors.Is(err, sql.ErrNoRows) {
208
+ return ErrNotFound
209
+ }
210
+
211
+ var bizErr *BizError
212
+ if errors.As(err, &bizErr) {
213
+ log.Println(bizErr.Code)
214
+ }
215
+ ```
216
+
217
+ ### 自定义错误
218
+
219
+ ```go
220
+ type BizError struct {
221
+ Code string
222
+ Msg string
223
+ }
224
+ func (e *BizError) Error() string { return e.Code + ": " + e.Msg }
225
+ ```
226
+
227
+ ## 1.8 defer / panic / recover
228
+
229
+ ```go
230
+ func ReadFile(path string) error {
231
+ f, err := os.Open(path)
232
+ if err != nil { return err }
233
+ defer f.Close() // 函数返回前执行,后进先出
234
+ // ...
235
+ return nil
236
+ }
237
+ ```
238
+
239
+ `defer` 常用于释放资源、解锁、记录耗时。注意:`defer` 在循环里会堆积,循环内要显式调用或用小函数包一层。
240
+
241
+ ```go
242
+ func SafeGo(fn func()) {
243
+ go func() {
244
+ defer func() {
245
+ if r := recover(); r != nil {
246
+ log.Printf("panic: %v", r)
247
+ }
248
+ }()
249
+ fn()
250
+ }()
251
+ }
252
+ ```
253
+
254
+ goroutine 里 panic 不 recover 会让**整个进程退出**,这一点和 JS 的单线程错误处理完全不同。
255
+
256
+ ## 1.9 接口与依赖注入
257
+
258
+ ```go
259
+ type AccountRepo interface {
260
+ Get(ctx context.Context, uid string) (*Account, error)
261
+ Save(ctx context.Context, a *Account) error
262
+ }
263
+
264
+ type RechargeService struct {
265
+ repo AccountRepo
266
+ }
267
+
268
+ func NewRechargeService(repo AccountRepo) *RechargeService {
269
+ return &RechargeService{repo: repo}
270
+ }
271
+ ```
272
+
273
+ 测试时传内存实现,生产传 MySQL 实现。这就是 `mini-coin` 的结构。
274
+
275
+ ## 1.10 泛型(了解即可)
276
+
277
+ ```go
278
+ func Map[T, U any](in []T, f func(T) U) []U {
279
+ out := make([]U, 0, len(in))
280
+ for _, v := range in {
281
+ out = append(out, f(v))
282
+ }
283
+ return out
284
+ }
285
+ ```
286
+
287
+ Go 1.18+ 支持泛型,但业务代码里不要滥用,接口和代码生成往往更直观。
288
+
289
+ ## 1.11 包与模块
290
+
291
+ ```bash
292
+ go mod init minicoin # 生成 go.mod
293
+ go get github.com/redis/go-redis/v9
294
+ go mod tidy # 清理/补齐依赖
295
+ ```
296
+
297
+ 目录约定:
298
+
299
+ ```
300
+ cmd/server/main.go # 可执行入口
301
+ internal/service/ # 只允许本模块引用
302
+ pkg/ # 允许外部引用(可选)
303
+ api/ # proto / openapi
304
+ migrations/ # SQL 迁移
305
+ ```
306
+
307
+ `internal` 是 Go 的强制访问控制:模块外的代码无法 import。
308
+
309
+ ## 1.12 代码格式与静态检查
310
+
311
+ ```bash
312
+ gofmt -w .
313
+ go vet ./...
314
+ staticcheck ./... # 可选,需要安装
315
+ golangci-lint run # 团队常用聚合工具
316
+ ```
317
+
318
+ **`gofmt` 不是可选风格问题**,它是 Go 社区统一的格式,CI 里通常要求 `gofmt -l .` 输出为空。
319
+
320
+ ## 1.13 一个完整的业务小例子
321
+
322
+ ```go
323
+ package service
324
+
325
+ import (
326
+ "context"
327
+ "errors"
328
+ "fmt"
329
+ )
330
+
331
+ var ErrInvalidAmount = errors.New("invalid amount")
332
+
333
+ type RechargeRequest struct {
334
+ ReqID string
335
+ UID string
336
+ Amount int64
337
+ }
338
+
339
+ type RechargeResult struct {
340
+ OrderID string
341
+ Balance int64
342
+ Duplicated bool
343
+ }
344
+
345
+ func (s *RechargeService) Recharge(ctx context.Context, req RechargeRequest) (RechargeResult, error) {
346
+ if req.ReqID == "" || req.UID == "" {
347
+ return RechargeResult{}, fmt.Errorf("req_id/uid required")
348
+ }
349
+ if req.Amount <= 0 {
350
+ return RechargeResult{}, ErrInvalidAmount
351
+ }
352
+ return s.doRecharge(ctx, req)
353
+ }
354
+ ```
355
+
356
+ ## 习题
357
+
358
+ 1. `var s []int` 和 `s := []int{}` 有什么区别?JSON 序列化结果分别是什么?
359
+ 2. 下面代码输出什么?为什么?
360
+ ```go
361
+ a := []int{1,2,3,4}
362
+ b := a[:2]
363
+ b = append(b, 99)
364
+ fmt.Println(a)
365
+ ```
366
+ 3. 写一个 `Money` 类型,方法 `String()` 返回 `"12.34"`,输入 1234。
367
+ 4. `errors.New` 和 `fmt.Errorf("%w")` 的区别是什么?什么时候用哪个?
368
+ 5. 为什么业务错误不建议用 `panic`?
369
+ 6. 写一个函数 `Divide(a, b int) (int, error)`,b 为 0 时返回错误。
370
+ 7. 解释 `go run .` 和 `go build .` 的区别,上线应该用哪个?
371
+ 8. 一个方法用指针接收者,另一个用值接收者,会有什么潜在问题?
372
+ 9. `defer` 在循环里写 10 万次会怎样?怎么改?
373
+ 10. 接口 `AccountRepo` 在测试和生产分别怎么用?这体现了什么设计原则?
374
+
375
+ ## 参考答案
376
+
377
+ <details>
378
+ <summary>展开</summary>
379
+
380
+ 1. 前者是 nil 切片,JSON 为 `null`;后者是空切片,JSON 为 `[]`。API 返回列表时通常用后者。
381
+ 2. `[1 2 99 4]`。`b` 与 `a` 共享底层数组,`append` 在容量足够时直接写回原数组。
382
+ 3.
383
+ ```go
384
+ type Money int64
385
+ func (m Money) String() string {
386
+ return fmt.Sprintf("%d.%02d", int64(m)/100, int64(m)%100)
387
+ }
388
+ ```
389
+ 4. `errors.New` 创建无上下文的哨兵错误;`fmt.Errorf("%w")` 包装已有错误并附加上下文,保留 `errors.Is/As` 链。需要携带上下文用后者。
390
+ 5. panic 会中断正常控制流,未 recover 时进程退出;业务错误是可预期的,应作为返回值逐层处理。
391
+ 6.
392
+ ```go
393
+ func Divide(a, b int) (int, error) {
394
+ if b == 0 { return 0, errors.New("division by zero") }
395
+ return a / b, nil
396
+ }
397
+ ```
398
+ 7. `go run` 编译到临时目录立即执行,适合开发;`go build` 产出可交付二进制,上线、镜像、CI 都用它。
399
+ 8. 方法集不一致:值类型只拥有值接收者方法,指针类型拥有两者。若接口要求指针方法,传值会编译失败;也容易误以为修改生效实际没生效。
400
+ 9. 每次 `defer` 都注册一个延迟调用,循环内会堆积到函数返回才执行,可能内存暴涨或耗尽 fd。应把循环体抽成函数,在函数内 defer。
401
+ 10. 测试传内存实现,生产传 MySQL 实现;体现依赖倒置/面向接口编程,业务层不依赖具体存储。
402
+ </details>