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.
- package/00-linux.md +175 -0
- package/01-go-basics.md +402 -0
- package/02-service-architecture.md +156 -0
- package/03-concurrency.md +278 -0
- package/04-mysql.md +362 -0
- package/05-database-selection.md +169 -0
- package/06-redis.md +197 -0
- package/07-message-queue.md +168 -0
- package/08-api-and-rpc.md +189 -0
- package/09-observability.md +164 -0
- package/10-resilience.md +173 -0
- package/11-monitoring.md +180 -0
- package/12-testing.md +176 -0
- package/13-performance-debug.md +174 -0
- package/14-security.md +172 -0
- package/15-development-process.md +157 -0
- package/16-api-docs.md +288 -0
- package/17-deploy-ops.md +273 -0
- package/18-ai-assisted.md +120 -0
- package/19-appendix.md +193 -0
- package/LICENSE +21 -0
- package/README.md +100 -2
- package/code/mini-coin/.gitlab-ci.yml +24 -0
- package/code/mini-coin/README.md +22 -0
- package/code/mini-coin/api/coin.proto +31 -0
- package/code/mini-coin/cmd/gateway/main.go +20 -0
- package/code/mini-coin/config/dev.json +8 -0
- package/code/mini-coin/config/prod.json +8 -0
- package/code/mini-coin/deploy/Dockerfile +12 -0
- package/code/mini-coin/docs/architecture.md +14 -0
- package/code/mini-coin/docs/changes/CHG-001-recharge/change-record.yaml +9 -0
- package/code/mini-coin/docs/changes/CHG-001-recharge/intent.md +10 -0
- package/code/mini-coin/docs/changes/CHG-001-recharge/plan.md +7 -0
- package/code/mini-coin/docs/changes/CHG-001-recharge/spec.md +10 -0
- package/code/mini-coin/docs/snippets/prometheus_metrics.go.txt +13 -0
- package/code/mini-coin/docs/snippets/redis_rate.go.txt +7 -0
- package/code/mini-coin/docs/snippets/zap_log.go.txt +12 -0
- package/code/mini-coin/go.mod +3 -0
- package/code/mini-coin/internal/cache/memory.go +43 -0
- package/code/mini-coin/internal/config/config.go +28 -0
- package/code/mini-coin/internal/httpapi/handler.go +88 -0
- package/code/mini-coin/internal/metrics/metrics.go +33 -0
- package/code/mini-coin/internal/metrics/metrics_test.go +17 -0
- package/code/mini-coin/internal/model/account.go +22 -0
- package/code/mini-coin/internal/mq/memory.go +31 -0
- package/code/mini-coin/internal/ratelimit/fixed_window.go +39 -0
- package/code/mini-coin/internal/ratelimit/fixed_window_test.go +19 -0
- package/code/mini-coin/internal/repository/memory.go +64 -0
- package/code/mini-coin/internal/security/sign.go +29 -0
- package/code/mini-coin/internal/security/sign_test.go +15 -0
- package/code/mini-coin/internal/service/recharge.go +97 -0
- package/code/mini-coin/internal/service/recharge_test.go +35 -0
- package/code/mini-coin/internal/worker/batch.go +44 -0
- package/code/mini-coin/migrations/001_create_account.sql +12 -0
- package/code/mini-coin/migrations/002_create_recharge_order.sql +12 -0
- package/code/mini-coin/migrations/003_add_channel.sql +4 -0
- package/code/mini-coin/scripts/e2e.sh +10 -0
- package/code/mini-coin/scripts/loadtest.js +26 -0
- package/exercises/README.md +73 -0
- 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>
|
package/01-go-basics.md
ADDED
|
@@ -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>
|