mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-28 21:56:36 +08:00
Compare commits
492 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 368df3f76b | |||
| 15e614b304 | |||
| 46941f65d5 | |||
| 0c2961ae6d | |||
| a61d55bb1b | |||
| 6b6c786cfe | |||
| 26be762c3a | |||
| 0548a8a5d4 | |||
| 60bc03f519 | |||
| 9dc3983e0f | |||
| 1eff7878a1 | |||
| 08e8eea932 | |||
| 85d5c8568c | |||
| 74ddf97b36 | |||
| a1a997bcda | |||
| d36409fbf9 | |||
| 4000366856 | |||
| af20e2e838 | |||
| 2fff30e188 | |||
| 74c2f57453 | |||
| 30e09f5985 | |||
| 43e293e062 | |||
| 439ac41da8 | |||
| d97581fb1e | |||
| 83f126795d | |||
| 69467914fc | |||
| c624512da6 | |||
| 50717d1baf | |||
| fc569d1758 | |||
| ec4f1d4d23 | |||
| 9268acb84c | |||
| 41cd23a64d | |||
| f02fc9676a | |||
| 31b4886a14 | |||
| efcf61e32d | |||
| d615d85a26 | |||
| 8afd103751 | |||
| 7ef84cce52 | |||
| 96b8ddc077 | |||
| 03b81e5f74 | |||
| 5b52acdd6c | |||
| fb3dd5afe6 | |||
| 1160d5846a | |||
| 350b433cc1 | |||
| d4d9bad74d | |||
| d0536fcdd5 | |||
| e51f1e583d | |||
| b835144cd0 | |||
| 53c868e99b | |||
| 50678756d4 | |||
| 3eb670c674 | |||
| d10132fb02 | |||
| 61cf581621 | |||
| e2ac531abb | |||
| c8b1289043 | |||
| 13c5073bf8 | |||
| da1dd92404 | |||
| 4b11279662 | |||
| bbadcca294 | |||
| 44ce6497a1 | |||
| 4b83f91b31 | |||
| 9d2fac5d4c | |||
| b4b93ff4ed | |||
| 160e63558f | |||
| 9b3555c569 | |||
| b928928958 | |||
| b312460ddf | |||
| 50f7257d93 | |||
| 336185f01c | |||
| 44bba0f19a | |||
| f0eca028f9 | |||
| 58624db397 | |||
| 38946d1af5 | |||
| caf2ffcff4 | |||
| 0e86fe3547 | |||
| 6525bef15d | |||
| 3e910f1961 | |||
| 28c14eb054 | |||
| ae618905a3 | |||
| 5ad151469c | |||
| 6467b32d8e | |||
| cf72420815 | |||
| 34225cb88a | |||
| 389f02b6b0 | |||
| 2fcbb945fb | |||
| 23501259b2 | |||
| c561e65cd3 | |||
| 97095e8f12 | |||
| 23be2f9296 | |||
| 113ea25aa4 | |||
| c230d5a744 | |||
| c02b649b46 | |||
| fb54d6da61 | |||
| 9c7896df50 | |||
| 01ebec6dfb | |||
| 2816152536 | |||
| b029714c7a | |||
| e0f452eaae | |||
| d721a8fd74 | |||
| 1e349e5cde | |||
| f03c88ffad | |||
| 3038304382 | |||
| 196bdabc80 | |||
| 77931c3c1e | |||
| a97d87acf5 | |||
| 8d8814b416 | |||
| 02ebb81929 | |||
| 60222acf7e | |||
| 7b1fea8194 | |||
| ac7b776378 | |||
| 13d6966cb5 | |||
| b48414e1fe | |||
| 894f8f1ea2 | |||
| b89dc9ec7e | |||
| 49eae80c78 | |||
| 8ed91dbf97 | |||
| 92ceecc6ce | |||
| d9b8dc81ee | |||
| deb232d840 | |||
| 346979344f | |||
| 1e5f35b9a3 | |||
| 346024f346 | |||
| ffe98f6307 | |||
| 6719c02d05 | |||
| 0a3cd250d1 | |||
| 53e6efa33b | |||
| 3d15c65afd | |||
| 16b02fd3f1 | |||
| e8778a8641 | |||
| 7b14e15afb | |||
| 3a2878d070 | |||
| df3bcd3d19 | |||
| 895dec208f | |||
| 9d56f02e64 | |||
| 40291136b7 | |||
| d3777eac2d | |||
| ee047cb351 | |||
| 6ed3c0c81f | |||
| 13a375e042 | |||
| 36f11c6ecb | |||
| 0f904b4b6d | |||
| e479ae75e6 | |||
| 6f267bbf21 | |||
| ce2b931a78 | |||
| 6e86901a58 | |||
| 42896a8473 | |||
| 665dd09e11 | |||
| b12a9b0185 | |||
| a343c7a605 | |||
| 117d473c27 | |||
| 99f6f3231a | |||
| b04a358e5e | |||
| 6fc39d9e80 | |||
| 889e79c8b8 | |||
| 9bf7e3cd1b | |||
| 8751c0dee3 | |||
| 498a9ed3ff | |||
| ec53629971 | |||
| 6c46f5d24f | |||
| e077b12328 | |||
| 570b639e07 | |||
| 3a368119e5 | |||
| 6975a6c290 | |||
| cdac1f8a45 | |||
| 8c872f9b32 | |||
| 2a0ebd16fa | |||
| b3a55d4ab5 | |||
| 5bed2bae9f | |||
| b6c4181c70 | |||
| 3187934f72 | |||
| 9491b2a744 | |||
| ca6c20ebd9 | |||
| 578110f615 | |||
| 32861c5db9 | |||
| 0b34792709 | |||
| db9a9f98fd | |||
| cc5e53c51e | |||
| 9eeeb09d2f | |||
| 84303d25b2 | |||
| 63cd906cfc | |||
| 19d476ed7f | |||
| 8506a03f1c | |||
| 8a00f53b16 | |||
| fff26aa343 | |||
| 425ca89765 | |||
| 7eb943f02f | |||
| d78449cbc9 | |||
| aa11c0f52a | |||
| 88360350a0 | |||
| e3353cd09d | |||
| 46123d62ae | |||
| 68ddad98fb | |||
| 33b4123444 | |||
| 16e97d0dc3 | |||
| 7aa4cc908d | |||
| 8fb988ba0a | |||
| 53bd451450 | |||
| 42ca0ec642 | |||
| 49d32d0b25 | |||
| 915c00eb34 | |||
| e3309df336 | |||
| c69378f0de | |||
| 71ebfa555f | |||
| 6bd7dc91fc | |||
| 399c1bc88d | |||
| aa348a1b48 | |||
| a2f838605c | |||
| fb7d7ff3a4 | |||
| 89b3f5d843 | |||
| 056c75a853 | |||
| 676899cab0 | |||
| cf5c2b7258 | |||
| 391823a915 | |||
| b56b4c93b6 | |||
| 29176da35f | |||
| 9ac5ff6925 | |||
| 1283aa5175 | |||
| 1208f7ee94 | |||
| 0c5136e59e | |||
| 8b3d220d73 | |||
| 134d279f0c | |||
| 2eb84fc7f8 | |||
| dde0117a37 | |||
| ce72de2d63 | |||
| bb5c626cd9 | |||
| 3342d3c39d | |||
| 49c40b20b7 | |||
| 194560c887 | |||
| 7b05e2dd3c | |||
| b59a2f3be3 | |||
| f1badac257 | |||
| 2cc0f3e673 | |||
| 769045af15 | |||
| 0d5fac1621 | |||
| fbc668c7bf | |||
| caa6649297 | |||
| e875ffd865 | |||
| e51ada4d60 | |||
| 22fbfc92c6 | |||
| 32d300ce58 | |||
| 1857fe4c98 | |||
| 0b646238e9 | |||
| 48421c12de | |||
| 75140fc3bb | |||
| 15fa943d41 | |||
| e7b8993181 | |||
| 31b114b0cc | |||
| a9fd0bd128 | |||
| d7fe4ef8be | |||
| 001f5c968e | |||
| 01c28bb88b | |||
| dfc480c73a | |||
| e3bfd9ca6d | |||
| 61cfacba55 | |||
| 5943372ce4 | |||
| e889247387 | |||
| 4ddda8e733 | |||
| 2480d74410 | |||
| 772962c2e9 | |||
| 3366edb3a1 | |||
| 99738bbc17 | |||
| d6a7011885 | |||
| 6ea2c90f75 | |||
| 959b134d67 | |||
| 314229b7ee | |||
| 7ed75e79ce | |||
| 52efc629b7 | |||
| 123140b762 | |||
| 6bf5023af1 | |||
| 4be7c19798 | |||
| 32e1a07157 | |||
| 2662c65f57 | |||
| 3cfefb4367 | |||
| ee1110b752 | |||
| 9959397934 | |||
| 5f82f72a6f | |||
| 43e8ef154f | |||
| 4dc4c745c8 | |||
| a5257be319 | |||
| 30f36efe5a | |||
| 5a6c5cf72c | |||
| 189916d1db | |||
| 546856594e | |||
| 457b3397fd | |||
| 47ff33c653 | |||
| 1d41b108fc | |||
| b7a101fc60 | |||
| c85d9104ec | |||
| f3cdbdd7d5 | |||
| e93131ab46 | |||
| 161e6c4e86 | |||
| 3dbc7b3045 | |||
| 4a4189705a | |||
| 6aa71a4da8 | |||
| bdc96f6d8e | |||
| 1c1063f448 | |||
| 29fdc378a1 | |||
| bd659d493d | |||
| 6a2a6028c3 | |||
| 6e85f1158b | |||
| e117e314d9 | |||
| fbb0909638 | |||
| 3eecd31868 | |||
| 68d70bbbe8 | |||
| 08ec945e59 | |||
| 4401cb0d66 | |||
| 36ae6247f9 | |||
| 1088086399 | |||
| 2c74d042ed | |||
| 3ec607106d | |||
| f671a96d8c | |||
| fe5cf9021f | |||
| 1be2461716 | |||
| a0e9484e37 | |||
| 4ca6f2957b | |||
| dfd040a9de | |||
| f29292dd81 | |||
| 4566fc1f53 | |||
| 4e58bdd85b | |||
| c009b9e283 | |||
| 4e33e0e521 | |||
| 7252fb6285 | |||
| 2220e45989 | |||
| 6158a487cf | |||
| 9f9c609809 | |||
| e4c6ce9062 | |||
| 81dd44c8fc | |||
| 3825a7f29a | |||
| d21643feed | |||
| 2525664013 | |||
| a850b0a188 | |||
| fd8148c0db | |||
| edd98f4ff0 | |||
| d8f98e218f | |||
| fefe205158 | |||
| d6e7e2baa2 | |||
| cc50cc695e | |||
| e43312d4c6 | |||
| 92df7d5c84 | |||
| 9632b4e3b8 | |||
| 3b979eb5d5 | |||
| 2635a47d29 | |||
| e00d67f2d9 | |||
| 581822d905 | |||
| b5ebfff19b | |||
| eed227b999 | |||
| 8f08a962e6 | |||
| d3ce26414c | |||
| 663da01bda | |||
| df63b0113a | |||
| d09e64ddc6 | |||
| b968043117 | |||
| 31d10195ca | |||
| 76f3428f5d | |||
| 9de33f7064 | |||
| fe13db95c2 | |||
| 6ddd2da2e8 | |||
| 054dc1a8a8 | |||
| 394e3c4855 | |||
| af36676e2e | |||
| 6fa31cafc7 | |||
| a8e8a940a0 | |||
| a092935623 | |||
| 5af13d0709 | |||
| 9a2616dc0e | |||
| c4e9e94117 | |||
| fce2e014e5 | |||
| 7372ac230b | |||
| 77bdb8bf0e | |||
| fd745d33cb | |||
| 65f899d334 | |||
| f034b73a47 | |||
| bd7f008322 | |||
| 2dc7e72621 | |||
| 2d542733f9 | |||
| c677edba06 | |||
| b827baf19f | |||
| 73beedfc09 | |||
| bc1b861841 | |||
| dfb3972b15 | |||
| 330771e7c7 | |||
| 9ded8c71da | |||
| 77ad3ea7e3 | |||
| 95d7045b4a | |||
| d5f46138d5 | |||
| 4196343ad3 | |||
| 78047d1b38 | |||
| c2bd416daf | |||
| 6e5d49c988 | |||
| 9da1ce8456 | |||
| 9f9cbd4ede | |||
| da1409fdac | |||
| 174198c283 | |||
| 796bf1c22f | |||
| 80dd5f8b31 | |||
| 14d41ad807 | |||
| fe2414ead5 | |||
| 649287a775 | |||
| 2514e7edc4 | |||
| 7ab11154e3 | |||
| 97c10b8d0b | |||
| ceae693a20 | |||
| edb356f40e | |||
| 81ba309650 | |||
| 5612403d48 | |||
| 4775e5cb73 | |||
| bc3d9ee285 | |||
| e654441127 | |||
| a987c0d681 | |||
| a85919fd9e | |||
| 4cb8928e4e | |||
| 57616626fd | |||
| 449d0a5c5b | |||
| 1c89db8ffa | |||
| 46f49cc349 | |||
| f365b3d331 | |||
| 4ae6c2718f | |||
| 8894620b92 | |||
| c2fcd2eddf | |||
| ec70794577 | |||
| bcd669722e | |||
| 9975ac90c4 | |||
| 632c455229 | |||
| b60cde02ac | |||
| 21ed214ba9 | |||
| f4a53d6b5f | |||
| cef3694d11 | |||
| 631d32e5d0 | |||
| c74b70b62e | |||
| fa9ecb5690 | |||
| b9cde88bf6 | |||
| f03718ce8c | |||
| 3423175006 | |||
| d619deec96 | |||
| e094f4a3b7 | |||
| 1bff2dadd4 | |||
| 602e7f5e9c | |||
| 9ec3d5b42d | |||
| 28b1305906 | |||
| a80376972c | |||
| 8300d3ec1c | |||
| 290ddd7b51 | |||
| 5d7a4469ea | |||
| 4e339caa9a | |||
| 2a00d21987 | |||
| f086edda3b | |||
| 899b4e6068 | |||
| fa23cad9e9 | |||
| 806863f303 | |||
| ab8e3d4705 | |||
| fe7f7da537 | |||
| 944b98d4d0 | |||
| 32dc7ef68e | |||
| 32762fdf3c | |||
| 79ed8fd6ab | |||
| 4257b6fd5a | |||
| 8dfe31c1c5 | |||
| 37486eb0c9 | |||
| 462deb4820 | |||
| f8509eed26 | |||
| 6e0b6df314 | |||
| 21962db3bf | |||
| b0117b7c84 | |||
| 95d58eb724 | |||
| c856faca50 | |||
| 5a0821274b | |||
| b69bdf838d | |||
| c35eb749c9 | |||
| e3c84c017a | |||
| 112694f860 | |||
| bd69ac51b5 | |||
| 46fb1a2b79 | |||
| c9a532db65 | |||
| be68b581e9 | |||
| 8853933adc | |||
| 048f6e4535 | |||
| 7b9c8996f9 | |||
| 8947bdc8d8 | |||
| baef42f920 | |||
| dd58e0df66 | |||
| bddf641bf1 | |||
| 83a426d3d6 | |||
| 4f698be0a5 | |||
| e9fb331214 | |||
| 5d6d68d0a1 | |||
| c8e2c3620e | |||
| af8e9b477e | |||
| 314f6fd3f4 | |||
| 7eee788720 | |||
| f6e4967a9a | |||
| 7afe4e5d78 | |||
| c6a055d5d3 |
@@ -0,0 +1,217 @@
|
||||
---
|
||||
name: "cache-framework"
|
||||
description: "Wavelet 项目专用:当新增或修改业务缓存(RAM/Redis/DB 三层读路径)、缓存失效、多节点 pub/sub 同步、或评估高频读是否应接入缓存时必须使用。本技能说明系统标准缓存框架、参考实现、禁止写法与分布式一致性要求。"
|
||||
---
|
||||
|
||||
# 系统三层缓存框架
|
||||
|
||||
开始前阅读根目录 `AGENTS.md`(含 **Skill 关联索引**)。Wavelet 标准读路径为 **本地 RAM → Redis → PostgreSQL**(由快到慢),不是 DB 优先。
|
||||
|
||||
详细性能背景见 `docs/PERFORMANCE.md`。
|
||||
|
||||
## 关联 Skill
|
||||
|
||||
| 关联 | 何时一并阅读 |
|
||||
| :--- | :--- |
|
||||
| [database-migration](../database-migration/SKILL.md) | 缓存对象对应新表/列/索引,或 seed 变更 |
|
||||
| [new-setting](../new-setting/SKILL.md) | 系统配置类缓存(`GetSystemConfigByKey`、`ListSystemConfigsByKeys`) |
|
||||
| [file-upload](../file-upload/SKILL.md) | 上传元数据 `upload:meta:{id}`、ingest/remove/cleanup 失效钩子 |
|
||||
| [clickhouse-batchwriter](../clickhouse-batchwriter/SKILL.md) | 分析写入走 batchwriter,**不要**用本技能模式缓存 CH flush 队列 |
|
||||
| [new-api](../new-api/SKILL.md) | 在 Handler 层接入 `GetXxxCached` 或评估高频读 |
|
||||
| [new-async-task](../new-async-task/SKILL.md) | Worker/定时任务变更数据后必须 `Invalidate*`(如 `system:cleanup`) |
|
||||
|
||||
## 标准模式(金标准)
|
||||
|
||||
参考:`internal/repository/system_config_cache.go` + `GetSystemConfigByKey` / `ListSystemConfigsByKeys`。
|
||||
|
||||
| 层级 | 技术 | 职责 |
|
||||
| :--- | :--- | :--- |
|
||||
| L1 本地 | `pkg/cache/ram`(Otter v2) | 进程内热数据,最低延迟 |
|
||||
| L2 共享 | Redis `db.GetJSON` / `SetJSON` / `HSetJSON` + `db.PrefixedKey` | 跨节点共享,带 TTL 或写穿 |
|
||||
| L3 权威 | PostgreSQL via `db.DB(ctx)` | 唯一数据源 |
|
||||
|
||||
### 读路径模板
|
||||
|
||||
```go
|
||||
func GetThingCached(ctx context.Context, key string) (Thing, error) {
|
||||
ensureThingCacheListener() // 订阅 pub/sub,仅 sync.Once
|
||||
|
||||
if v, ok := thingRAM.GetIfPresent(key); ok {
|
||||
return cloneThing(v), nil
|
||||
}
|
||||
if db.Redis != nil {
|
||||
var v Thing
|
||||
if err := db.GetJSON(ctx, redisKey(key), &v); err == nil {
|
||||
thingRAM.Set(key, cloneThing(v))
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
v, err := loadThingFromDB(ctx, key)
|
||||
if err != nil {
|
||||
return Thing{}, err
|
||||
}
|
||||
populateThingCache(ctx, v) // 回写 RAM + Redis
|
||||
return v, nil
|
||||
}
|
||||
```
|
||||
|
||||
### 写穿(populate)
|
||||
|
||||
DB miss 或业务创建成功后,**必须**回写上层:
|
||||
|
||||
```go
|
||||
func populateThingCache(ctx context.Context, v Thing) {
|
||||
thingRAM.Set(v.Key, cloneThing(v))
|
||||
if db.Redis != nil {
|
||||
_ = db.SetJSON(ctx, redisKey(v.Key), v, cacheTTL)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 失效(Invalidate)— 分布式必做三步
|
||||
|
||||
数据变更(Admin 更新、软删除、状态迁移)时:
|
||||
|
||||
1. **本机 RAM** — `thingRAM.Invalidate(key)` 或 `InvalidateAll()`
|
||||
2. **Redis** — `Del` / `HDel` 对应 key
|
||||
3. **pub/sub 广播** — 通知**其他节点**清除 RAM(Redis 已由写节点清掉)
|
||||
|
||||
```go
|
||||
func InvalidateThingCache(ctx context.Context, key string) error {
|
||||
ensureThingCacheListener()
|
||||
thingRAM.Invalidate(key)
|
||||
if db.Redis != nil {
|
||||
if err := db.Redis.Del(ctx, db.PrefixedKey(redisKey(key))).Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
publishThingRAMInvalidation(ctx, key) // 只广播 RAM 失效
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### pub/sub 监听模板
|
||||
|
||||
```go
|
||||
const thingInvalidationChannel = "domain:thing_invalidation"
|
||||
|
||||
func startThingCacheInvalidationListener() {
|
||||
if db.Redis == nil {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
pubsub := db.Redis.Subscribe(context.Background(), thingInvalidationChannel)
|
||||
defer func() { _ = pubsub.Close() }()
|
||||
for msg := range pubsub.Channel() {
|
||||
// 解析 payload,Invalidate RAM;勿重复 Del Redis
|
||||
thingRAM.Invalidate(parsedKey)
|
||||
}
|
||||
}()
|
||||
}
|
||||
```
|
||||
|
||||
- 使用 `sync.Once` 启动监听;**`ensureListener` 必须在 `db.Redis == nil` 时直接 return,不可消费 Once**(否则测试或 Redis 晚初始化时监听器永不启动)。
|
||||
- 测试可提供 `StopThingCacheListener` + 重置 `Once`(参考 `StopUploadMetaCacheListener`、`StopAuthSourceCacheListener`)。
|
||||
- 其他节点收到消息后**只清 RAM**,不再删 Redis。
|
||||
|
||||
## 现有实现速查
|
||||
|
||||
| 域 | 文件 | L1 | L2 | pub/sub |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| 系统配置 | `repository/system_config_cache.go` | `pkg/cache/store` | ❌ 无 Redis 缓存 | `system:config_broadcast` (别名 `system:config_invalidation`) ✅ |
|
||||
| CAPTCHA 运行时 | `apps/cap/runtime_settings.go` | atomic.Pointer | (借配置 Redis) | 订阅 `system:config_invalidation` ✅ |
|
||||
| 上传元数据 | `apps/upload/cache/meta_cache.go` | Otter | Redis JSON | `upload:meta_invalidation` ✅ |
|
||||
| 上传访问白名单 | `apps/upload/cache/access_cache.go` | 进程内 TTL | (借配置读路径) | `upload:file_access_invalidation` ✅ |
|
||||
| Auth Source | `repository/auth_source_cache.go` | Otter | Redis JSON | `oauth:auth_source_invalidation` ✅ |
|
||||
| OAuth 用户/Token | `apps/oauth/cache.go` | 自研 map | Redis JSON | ❌ 无 pub/sub(历史债) |
|
||||
| 推送渠道 | `repository/push_channel.go` | 无 | Redis JSON | ❌ 仅 Redis Del |
|
||||
| Storage 驱动 | `internal/storage/storage.go` | RWMutex 快照 | — | `storage:config_invalidation` ✅ |
|
||||
|
||||
## 新增缓存工作流
|
||||
|
||||
1. **判定是否需要缓存**:高频读、低变更、可容忍短暂 TTL;写路径必须能统一失效。
|
||||
2. **选型 L1**:优先 `pkg/cache/ram.MustNew`;**禁止**自研 `map+mutex+TTL`,除非有充分理由并文档说明。
|
||||
3. **选型 L2**:小对象 `SetJSON`;配置类多条目用 Redis Hash(`HSetJSON`)。
|
||||
4. **定义 Redis key**:小写蛇形,带业务前缀(`upload:meta:{id}`);统一 `db.PrefixedKey`。
|
||||
5. **实现 Invalidate + pub/sub**:凡多实例部署可读的 RAM 缓存**必须**有失效广播。
|
||||
6. **挂载变更钩子**:在所有 DB 变更入口调用 Invalidate(含 Worker/定时任务,不只 HTTP Handler)。
|
||||
7. **测试**:
|
||||
- RAM hit / Redis hit / DB fallback
|
||||
- Invalidate 清 L1+L2
|
||||
- pub/sub 触发他机 RAM 失效(可用 miniredis Publish 模拟)
|
||||
- `Reset*RAMCacheForTest` 仅清本机 RAM
|
||||
8. 运行 `go test` 相关包 + `make code-check`。
|
||||
|
||||
## 变更钩子清单(上传元数据示例)
|
||||
|
||||
| 入口 | 动作 |
|
||||
| :--- | :--- |
|
||||
| `ingest.persistUploadRecord` 创建成功 | `SetUploadMetaCache` |
|
||||
| `ingest.Remove` / `RemoveOwned` | `InvalidateUploadMetaCache` |
|
||||
| `task/cleanup.go` 软删除 pending 文件 | `InvalidateUploadMetaCache` |
|
||||
| 直接 `repository.SoftDeleteUpload` | **禁止** — 必须走 `upload.Remove` |
|
||||
|
||||
## 禁止写法
|
||||
|
||||
```go
|
||||
// ❌ 自研 L1,与 pkg/cache/ram 重复
|
||||
var mu sync.RWMutex
|
||||
var items = map[uint64]entry{}
|
||||
|
||||
// ❌ 只清本机 RAM + Redis,无 pub/sub(多节点 RAM 脏读)
|
||||
func Invalidate(ctx context.Context, id uint64) {
|
||||
localDelete(id)
|
||||
redis.Del(...)
|
||||
}
|
||||
|
||||
// ❌ DB 变更后忘记 Worker 路径
|
||||
// cleanup 任务删了 upload 行,但未 InvalidateUploadMetaCache
|
||||
|
||||
// ❌ 在 Handler 里直接查 DB,绕过已有 GetXxxCached
|
||||
|
||||
// ❌ Redis key 不用 PrefixedKey(多环境共 Redis 时冲突)
|
||||
|
||||
// ❌ 在 init() 里启动 pub/sub 监听 — 与 bootstrap 规范冲突;用 sync.Once 懒启动
|
||||
```
|
||||
|
||||
## 特殊场景
|
||||
|
||||
### 敏感字段(ClientSecret)
|
||||
|
||||
模型 `json:"-"` 时,Redis DTO 用独立 `*RedisRecord` struct 显式序列化字段(见 `auth_source_cache.go`)。
|
||||
|
||||
### 批量读配置
|
||||
|
||||
批量接口必须与单 key 一致走 Redis(`ListSystemConfigsByKeys` 在 RAM miss 后逐 key `HGetJSON`,再 DB `IN`)。
|
||||
|
||||
### 仅进程内、短 TTL、配置衍生
|
||||
|
||||
可用进程内快照 + 订阅上游 pub/sub(`access_cache.go`、`cap/runtime_settings.go`),不必强行 Redis L2。
|
||||
|
||||
### OAuth 用户/Token
|
||||
|
||||
沿用 `oauth/cache.go`;新增逻辑调用 `SetCachedUser` / `SetCachedToken` 预热,变更调用 `InvalidateCachedUser` / `InvalidateCachedToken`。
|
||||
|
||||
## 验证清单
|
||||
|
||||
```bash
|
||||
go test ./internal/repository/... ./internal/apps/upload/cache/...
|
||||
make code-check
|
||||
```
|
||||
|
||||
- [ ] L1 使用 `pkg/cache/ram`(或已文档化的例外)
|
||||
- [ ] 读路径:RAM → Redis → DB
|
||||
- [ ] 写穿 populate 在 DB load / 创建成功后
|
||||
- [ ] Invalidate:RAM + Redis + Publish
|
||||
- [ ] `ensureListener` + pub/sub 清他机 RAM
|
||||
- [ ] 所有变更入口(含 Worker)已挂钩
|
||||
- [ ] 测试含 Invalidate 与 pub/sub
|
||||
|
||||
## 相关文件
|
||||
|
||||
- L1 引擎:`pkg/cache/ram/cache.go`
|
||||
- DB/Redis 助手:`internal/db/redis.go`(`GetJSON`, `SetJSON`, `HGetJSON`, `PrefixedKey`)
|
||||
- 金标准:`internal/repository/system_config_cache.go`
|
||||
- 上传元数据:`internal/apps/upload/cache/meta_cache.go`
|
||||
- Auth Source:`internal/repository/auth_source_cache.go`
|
||||
- 性能文档:`docs/PERFORMANCE.md`
|
||||
@@ -0,0 +1,163 @@
|
||||
---
|
||||
name: "clickhouse-batchwriter"
|
||||
description: "Wavelet 项目专用:当新增或修改 ClickHouse 批量写入、接入 internal/db/batchwriter、将业务域异步 flush 到分析表、迁移 risk_control/节点访问日志/可观测时序写入、或评估 async_insert 与背压策略时必须使用。本技能指导分层职责、各域独立 Writer 实例、repository 批量 API 与禁止写法。"
|
||||
---
|
||||
|
||||
# ClickHouse 批量写入开发
|
||||
|
||||
开始前阅读根目录 `AGENTS.md`。ClickHouse 是辅助 OLAP 存储,**厌恶高频单条写入**(过多小 part);写入路径必须优先批量或异步聚合。
|
||||
|
||||
DDL 与表结构变更见 `database-migration` 技能;本技能只覆盖**运行时写入架构**。
|
||||
|
||||
## 分层职责
|
||||
|
||||
| 层级 | 路径 | 职责 |
|
||||
| :--- | :--- | :--- |
|
||||
| 连接 | `internal/db/clickhouse.go` | `ChConn`(原生批量写)、`ChDB`(GORM 查询);禁止在业务包直接 `clickhouse.Open` |
|
||||
| 批量框架 | `internal/db/batchwriter/` | 泛型队列 + 按条数/时间 flush + 非阻塞入队 + 优雅停机;**各业务域独立实例** |
|
||||
| Model | `internal/model/analytics/` | 列定义、`TableName()`、`BatchInsertSQL()`(及可选 `InsertColumns()`) |
|
||||
| Repository | `internal/repository/analytics/` | `BatchInsert*` / `BatchInsertNodeAccessLogs` 等;`PrepareBatch` + 多行 `Append` + 一次 `Send` |
|
||||
| Apps | `internal/apps/<domain>/` | 采集、入队、背压;`FlushFunc` 只调 repository,不写 SQL、不 `PrepareBatch` |
|
||||
| 装配 | `internal/bootstrap/bootstrap.go` | 进程启动时调用 `Writer.Start`;初始化时需调用 `lifecycle.OnShutdown` 挂载停机钩子 |
|
||||
| 生命周期 | `internal/lifecycle/lifecycle.go` | 统一协调全局并发优雅停机,业务包无需在 `bootstrap.go` 中硬编码 `Stop` 逻辑 |
|
||||
|
||||
**禁止**在 Handler / middleware 内直接 `db.ChConn.PrepareBatch`;**禁止**在 repository 内启动 goroutine 或维护全局 channel(队列生命周期由 apps + bootstrap 或专用 writer 包负责)。
|
||||
|
||||
## batchwriter 框架契约
|
||||
|
||||
```go
|
||||
writer, err := batchwriter.New[YourType](cfg, flushFunc, opts...)
|
||||
writer.Start(ctx)
|
||||
writer.TryEnqueue(item) // 非阻塞;满则 false
|
||||
writer.IsFull() // 背压探测
|
||||
writer.Stop(stopCtx) // close 队列 + drain + 最终 flush
|
||||
```
|
||||
|
||||
### Config 默认值(`batchwriter.DefaultConfig()`)
|
||||
|
||||
- `QueueSize`: 10_000
|
||||
- `MaxBatchSize`: 1_000
|
||||
- `FlushInterval`: 1s
|
||||
|
||||
各域可独立覆盖;可观测低频指标可用更小 `MaxBatchSize`(如 100)与更长 `FlushInterval`(如 2–5s),但**不要**退化为逐条 `Send`。
|
||||
|
||||
### 可选回调
|
||||
|
||||
- `WithFlushErrorHandler[T]`:flush 失败时记录日志;批次丢弃后 worker 继续
|
||||
- `WithDropHandler[T]`:队列满或未 `Start` 时丢弃项
|
||||
|
||||
### FlushFunc 规范
|
||||
|
||||
- 签名:`func(ctx context.Context, items []T) error`
|
||||
- 内部调用 `internal/repository/analytics` 的 `BatchInsert*`(传入 `[]analyticsmodel.X`)
|
||||
- 在 flush 边界记录一次错误日志,不要把 DB 驱动错误直接暴露给 HTTP 客户端
|
||||
- `Start` 使用 `context.WithoutCancel(parent)`,避免请求 ctx 取消中断后台 flush
|
||||
|
||||
## 各域独立实例(不共享队列)
|
||||
|
||||
每个业务域拥有自己的 `Writer`、配置与 `FlushFunc`:
|
||||
|
||||
| 域 | 表 | 现状 | 目标形态 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| 管理端审计 | `w_user_access_logs` | `risk_control` → `batchwriter` + `analyticsrepo.BatchInsert` | 已接入 |
|
||||
| 边缘访问日志 | `of_node_access_logs` | `openflare/chwriter` 异步 flush | 已接入 |
|
||||
| 可观测时序 | `of_node_metric_snapshots` 等 5 表 | `openflare/chwriter` 五表独立 writer + 进程内短 TTL 去重 | 已接入 |
|
||||
|
||||
**不要**把 audit、access log、observability 并入同一 channel。
|
||||
|
||||
## 新增 ClickHouse 写入工作流
|
||||
|
||||
1. **Model**:在 `internal/model/analytics/` 定义 struct 与 `BatchInsertSQL()`(列顺序与 goose DDL 一致)。
|
||||
2. **Goose DDL**:在 `internal/db/migrator/goose/clickhouse/` 新增迁移(见 `database-migration`)。
|
||||
3. **Repository**:实现 `BatchInsertX(ctx, []analyticsmodel.X) error`:
|
||||
- `len(items)==0` 直接返回
|
||||
- `db.ChConn == nil` 返回明确错误
|
||||
- 一次 `PrepareBatch` → 循环 `Append` → 一次 `Send`
|
||||
4. **Writer 胶水**(`internal/apps/<domain>/` 或 `internal/repository/analytics/<domain>_writer.go`):
|
||||
- `New` + `Start`,并在初始化逻辑内通过 `lifecycle.OnShutdown("your_writer_name", Stop)` 注册停机回调
|
||||
- 业务路径 `TryEnqueue`;HTTP 背压用 `IsFull()`
|
||||
5. **测试**:
|
||||
- repository:mock `ChConn` 验证 `BatchInsertSQL` 与 append 列数
|
||||
- batchwriter:`go test ./internal/db/batchwriter`
|
||||
6. 运行 `make code-check`;有 API 变更时 `make swagger`。
|
||||
|
||||
## 背压与丢弃策略
|
||||
|
||||
| 场景 | 推荐策略 |
|
||||
| :--- | :--- |
|
||||
| 管理端 API 审计 | 队列满 → `IsFull()` 触发 429(见 `risk_control` middleware) |
|
||||
| Agent 心跳指标 | 队列满 → `WithDropHandler` 记 warn;不阻塞心跳响应 |
|
||||
| 边缘 access log | 优先扩大队列与 batch;必要时丢弃最旧或采样 |
|
||||
|
||||
## 禁止写法
|
||||
|
||||
```go
|
||||
// ❌ 单条伪批量:每条都 PrepareBatch + Send
|
||||
batch.Append(oneRow)
|
||||
batch.Send()
|
||||
|
||||
// ❌ 写前 OLTP 式去重(高 RTT + 仍产生小 part)
|
||||
SELECT count() FROM ... WHERE node_id = ? AND captured_at = ?
|
||||
|
||||
// ❌ Handler 内直接写 ClickHouse
|
||||
db.ChConn.PrepareBatch(...)
|
||||
|
||||
// ❌ 全局单队列承载所有分析表
|
||||
var globalChan chan any
|
||||
```
|
||||
|
||||
去重应使用:`ReplacingMergeTree`、查询侧 `argMax`、或进程内短 TTL 去重缓存——**不要**在每次 insert 前 `SELECT count()`。
|
||||
|
||||
## async_insert(补充,非主方案)
|
||||
|
||||
可在 `internal/db/clickhouse.go` 的 `Settings` 增加服务端异步写入作为第二层防护:
|
||||
|
||||
```go
|
||||
"async_insert": 1,
|
||||
"wait_for_async_insert": 1,
|
||||
```
|
||||
|
||||
**不能替代**应用层批量;接入前需评估丢失可观测性与服务端负载。优先完成 `batchwriter` 接入后再考虑。
|
||||
|
||||
## Bootstrap 装配示例
|
||||
|
||||
```go
|
||||
// internal/bootstrap/bootstrap.go(示意)
|
||||
var userAccessLogWriter *batchwriter.Writer[*analytics.UserAccessLog]
|
||||
|
||||
func RegisterAPI(ctx context.Context) {
|
||||
// ...
|
||||
if config.Config.ClickHouse.Enabled {
|
||||
initUserAccessLogWriter(ctx) // Start writer
|
||||
risk_control.BindWriter(userAccessLogWriter) // 或逐步替换 InitLogWriter
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `RegisterAPI` / `RegisterAll`:`Start`
|
||||
- 进程优雅停机:业务模块在初始化时调用 `lifecycle.OnShutdown` 注册,由 `bootstrap.Stop()` 代理 `lifecycle.Stop()` 并发停机。
|
||||
- 使用 `sync.Once` 保证幂等
|
||||
|
||||
## 验证清单
|
||||
|
||||
```bash
|
||||
go test ./internal/db/batchwriter
|
||||
go test ./internal/repository/analytics
|
||||
make code-check
|
||||
```
|
||||
|
||||
- flush 按 `MaxBatchSize` 与 `FlushInterval` 触发
|
||||
- `Stop` 能 drain 队列内剩余项
|
||||
- repository 层无 goroutine、无 channel
|
||||
- `clickhouse.enabled: false` 时不 `Start` writer、不入队
|
||||
|
||||
## 相关文件速查
|
||||
|
||||
- 框架:`internal/db/batchwriter/{config,writer,errs}.go`
|
||||
- 连接:`internal/db/clickhouse.go`
|
||||
- 审计写入:`internal/apps/risk_control/logics.go`
|
||||
- OpenFlare 写入胶水:`internal/apps/openflare/chwriter/writer.go`
|
||||
- 节点访问日志 repository:`internal/repository/analytics/node_access_log_writer.go`
|
||||
- 可观测 repository:`internal/repository/analytics/node_observability_writer.go`
|
||||
- 生命周期管理器:`internal/lifecycle/lifecycle.go`
|
||||
- Bootstrap:`internal/bootstrap/bootstrap.go`
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Editor files
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.egg-info/
|
||||
.eggs/
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Local config
|
||||
.env
|
||||
.env.local
|
||||
Regular → Executable
+348
@@ -0,0 +1,348 @@
|
||||
# Contributing to AI Code Review Guide
|
||||
|
||||
Thank you for your interest in contributing! This document provides guidelines for contributing to this Claude Code Skill project.
|
||||
|
||||
## Claude Code Skill 开发规范
|
||||
|
||||
本项目是一个 Claude Code Skill,贡献者需要遵循以下规范。
|
||||
|
||||
### 目录结构
|
||||
|
||||
```
|
||||
code-review-skill/
|
||||
├── SKILL.md # Required: main file (always loaded)
|
||||
├── README.md
|
||||
├── CONTRIBUTING.md
|
||||
├── LICENSE
|
||||
├── reference/ # On-demand language/framework guides
|
||||
│ ├── react.md # React 19 / Next.js / TanStack Query v5
|
||||
│ ├── vue.md # Vue 3.5 Composition API
|
||||
│ ├── angular.md # Angular 17+, Signals, Standalone, RxJS
|
||||
│ ├── svelte.md # Svelte 5 / SvelteKit, runes, SSR boundary
|
||||
│ ├── rust.md # Ownership, async, unsafe, cancellation
|
||||
│ ├── typescript.md # Type safety, generics, strict mode
|
||||
│ ├── nestjs.md # NestJS DI, modules, Guards/Pipes, DTOs
|
||||
│ ├── python.md # Type hints, async, testing
|
||||
│ ├── django.md # Django / DRF, N+1, serializers, async views
|
||||
│ ├── fastapi.md # FastAPI, Depends, Pydantic v2, async
|
||||
│ ├── java.md # Java 17/21, Spring Boot 3, virtual threads
|
||||
│ ├── kotlin.md # Kotlin / Android, coroutines, Flow, Compose
|
||||
│ ├── go.md # Error handling, goroutines, context
|
||||
│ ├── csharp.md # C# / .NET 8, async, EF Core, ASP.NET Core
|
||||
│ ├── php.md # PHP 8.x, types, PDO, security, Composer
|
||||
│ ├── c.md # Memory safety, UB, error handling
|
||||
│ ├── cpp.md # RAII, move semantics, exception safety
|
||||
│ ├── qt.md # Object model, signals/slots, GUI perf
|
||||
│ ├── css-less-sass.md # Variables, responsive, performance
|
||||
│ ├── architecture-review-guide.md # SOLID, anti-patterns, coupling
|
||||
│ ├── performance-review-guide.md # Web Vitals, N+1, complexity
|
||||
│ ├── security-review-guide.md # OWASP Top 10, JWT, validation
|
||||
│ ├── common-bugs-checklist.md # Quick-reference bug patterns
|
||||
│ ├── code-quality-universal.md # Language-agnostic quality anti-patterns
|
||||
│ └── code-review-best-practices.md # Communication & process
|
||||
├── assets/ # Templates and quick reference
|
||||
│ ├── review-checklist.md
|
||||
│ └── pr-review-template.md
|
||||
└── scripts/
|
||||
└── pr-analyzer.py # PR complexity analyzer
|
||||
```
|
||||
|
||||
### Frontmatter 规范
|
||||
|
||||
SKILL.md 必须包含 YAML frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: skill-name
|
||||
description: |
|
||||
功能描述。触发条件说明。
|
||||
Use when [具体使用场景]。
|
||||
allowed-tools: ["Read", "Grep", "Glob"] # 可选:限制工具访问
|
||||
---
|
||||
```
|
||||
|
||||
#### 必需字段
|
||||
|
||||
| 字段 | 说明 | 约束 |
|
||||
|------|------|------|
|
||||
| `name` | Skill 标识符 | 小写字母、数字、连字符;最多 64 字符 |
|
||||
| `description` | 功能和激活条件 | 最多 1024 字符;必须包含 "Use when" |
|
||||
|
||||
#### 可选字段
|
||||
|
||||
| 字段 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `allowed-tools` | 限制工具访问 | `["Read", "Grep", "Glob"]` |
|
||||
|
||||
### 命名约定
|
||||
|
||||
**Skill 名称规则**:
|
||||
- 仅使用小写字母、数字和连字符(kebab-case)
|
||||
- 最多 64 个字符
|
||||
- 避免下划线或大写字母
|
||||
|
||||
```
|
||||
✅ 正确:code-review-skill, typescript-advanced-types
|
||||
❌ 错误:CodeReview, code_review, TYPESCRIPT
|
||||
```
|
||||
|
||||
**文件命名规则**:
|
||||
- reference 文件使用小写:`react.md`, `vue.md`
|
||||
- 多词文件使用连字符:`common-bugs-checklist.md`
|
||||
|
||||
### Description 写法规范
|
||||
|
||||
Description 必须包含两部分:
|
||||
|
||||
1. **功能陈述**:具体说明 Skill 能做什么
|
||||
2. **触发条件**:以 "Use when" 开头,说明何时激活
|
||||
|
||||
```yaml
|
||||
# ✅ 正确示例
|
||||
description: |
|
||||
Provides comprehensive code review guidance for React 19, Vue 3, Rust,
|
||||
TypeScript, Java, Python, and C/C++.
|
||||
Helps catch bugs, improve code quality, and give constructive feedback.
|
||||
Use when reviewing pull requests, conducting PR reviews, establishing
|
||||
review standards, or mentoring developers through code reviews.
|
||||
|
||||
# ❌ 错误示例(太模糊,缺少触发条件)
|
||||
description: |
|
||||
Helps with code review.
|
||||
```
|
||||
|
||||
### Progressive Disclosure(渐进式披露)
|
||||
|
||||
Claude 只在需要时加载支持文件,不会一次性加载所有内容。
|
||||
|
||||
#### 文件职责划分
|
||||
|
||||
| 文件 | 加载时机 | 内容 |
|
||||
|------|----------|------|
|
||||
| `SKILL.md` | 始终加载 | 核心原则、快速索引、何时使用 |
|
||||
| `reference/*.md` | 按需加载 | 语言/框架的详细指南 |
|
||||
| `assets/*.md` | 明确需要时 | 模板、清单 |
|
||||
| `scripts/*.py` | 明确指引时 | 工具脚本 |
|
||||
|
||||
#### 内容组织原则
|
||||
|
||||
**SKILL.md**(~200 行以内):
|
||||
- 简述:2-3 句话说明用途
|
||||
- 核心原则和方法论
|
||||
- 语言/框架索引表(链接到 reference/)
|
||||
- 何时使用此 Skill
|
||||
|
||||
**reference/*.md**(详细内容):
|
||||
- 完整的代码示例
|
||||
- 所有最佳实践
|
||||
- Review Checklist
|
||||
- 边界情况和陷阱
|
||||
|
||||
### 文件引用规范
|
||||
|
||||
在 SKILL.md 中引用其他文件时:
|
||||
|
||||
```markdown
|
||||
# ✅ 正确:使用 Markdown 链接格式
|
||||
| **React** | [React Guide](reference/react.md) | Hooks, React 19, RSC |
|
||||
| **Vue 3** | [Vue Guide](reference/vue.md) | Composition API |
|
||||
|
||||
详见 [React Guide](reference/react.md) 获取完整指南。
|
||||
|
||||
# ❌ 错误:使用代码块格式
|
||||
参考 `reference/react.md` 文件。
|
||||
```
|
||||
|
||||
**路径规则**:
|
||||
- 使用相对路径(相对于 Skill 目录)
|
||||
- 使用正斜杠 `/`,不使用反斜杠
|
||||
- 不需要 `./` 前缀
|
||||
|
||||
### 约定(Conventions)
|
||||
|
||||
**严重级别(severity)**:审查意见统一使用 SKILL.md「Technique 4」的标记方案,三档由红到绿表示优先级:
|
||||
|
||||
- 🔴 `[blocking]` - 合并前必须修复
|
||||
- 🟡 `[important]` - 应当修复,有异议可讨论
|
||||
- 🟢 `[nit]` - 可选优化,不阻塞合并
|
||||
|
||||
新增 reference 指南时请沿用这套标记,不要自创等价的名称(如 critical/warning/suggestion)。
|
||||
|
||||
**语言策略**:现有指南是中英混合的——部分通篇中文,部分(如 fastapi.md、php.md)以英文为主。新增内容时**跟随同一领域既有指南的语言**:改某个指南就用它的语言;新建指南可自行选择中文或英文,但单个文件内部保持一致。
|
||||
|
||||
---
|
||||
|
||||
## 贡献类型
|
||||
|
||||
### 添加新语言支持
|
||||
|
||||
1. 在 `reference/` 目录创建新文件(如 `go.md`)
|
||||
2. 遵循以下结构:
|
||||
|
||||
```markdown
|
||||
# [Language] Code Review Guide
|
||||
|
||||
> 简短描述,一句话说明覆盖内容。
|
||||
|
||||
## 目录
|
||||
- [主题1](#主题1)
|
||||
- [主题2](#主题2)
|
||||
- [Review Checklist](#review-checklist)
|
||||
|
||||
---
|
||||
|
||||
## 主题1
|
||||
|
||||
### 子主题
|
||||
|
||||
```[language]
|
||||
// ❌ Bad pattern - 说明为什么不好
|
||||
bad_code_example()
|
||||
|
||||
// ✅ Good pattern - 说明为什么好
|
||||
good_code_example()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Review Checklist
|
||||
|
||||
### 类别1
|
||||
- [ ] 检查项 1
|
||||
- [ ] 检查项 2
|
||||
```
|
||||
|
||||
3. 在 `SKILL.md` 的索引表中添加链接
|
||||
4. 更新 `README.md` 的统计信息
|
||||
|
||||
### 添加框架模式
|
||||
|
||||
1. 确保引用官方文档
|
||||
2. 包含版本号(如 "React 19", "Vue 3.5+")
|
||||
3. 提供可运行的代码示例
|
||||
4. 添加对应的 checklist 项
|
||||
|
||||
### 改进现有内容
|
||||
|
||||
- 修复拼写或语法错误
|
||||
- 更新过时的模式(注明版本变化)
|
||||
- 添加边界情况示例
|
||||
- 改进代码示例的清晰度
|
||||
|
||||
---
|
||||
|
||||
## 代码示例规范
|
||||
|
||||
### 格式要求
|
||||
|
||||
```markdown
|
||||
// ❌ 问题描述 - 解释为什么这样做不好
|
||||
problematic_code()
|
||||
|
||||
// ✅ 推荐做法 - 解释为什么这样做更好
|
||||
recommended_code()
|
||||
```
|
||||
|
||||
### 质量标准
|
||||
|
||||
- 示例应基于真实场景,避免人为构造
|
||||
- 同时展示问题和解决方案
|
||||
- 保持示例简洁聚焦
|
||||
- 包含必要的上下文(import 语句等)
|
||||
|
||||
---
|
||||
|
||||
## 提交流程
|
||||
|
||||
### Issue 报告
|
||||
|
||||
- 使用 GitHub Issues 报告问题或建议
|
||||
- 提供清晰的描述和示例
|
||||
- 标注相关的语言/框架
|
||||
|
||||
### Pull Request 流程
|
||||
|
||||
1. Fork 仓库
|
||||
2. 创建功能分支:`git checkout -b feature/add-go-support`
|
||||
3. 进行修改
|
||||
4. 提交(见下文 commit 格式)
|
||||
5. 推送到 fork:`git push origin feature/add-go-support`
|
||||
6. 创建 Pull Request
|
||||
|
||||
### Commit 消息格式
|
||||
|
||||
```
|
||||
类型: 简短描述
|
||||
|
||||
详细说明(如需要)
|
||||
|
||||
- 具体变更 1
|
||||
- 具体变更 2
|
||||
```
|
||||
|
||||
**类型**:
|
||||
- `feat`: 新功能或新内容
|
||||
- `fix`: 修复错误
|
||||
- `docs`: 仅文档变更
|
||||
- `refactor`: 重构(不改变功能)
|
||||
- `chore`: 维护性工作
|
||||
|
||||
**示例**:
|
||||
```
|
||||
feat: 添加 Go 语言代码审查指南
|
||||
|
||||
- 新增 reference/go.md
|
||||
- 覆盖错误处理、并发、接口设计
|
||||
- 更新 SKILL.md 索引表
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Skill 设计原则
|
||||
|
||||
### 单一职责
|
||||
|
||||
每个 Skill 专注一个核心能力。本 Skill 专注于**代码审查**,不应扩展到:
|
||||
- 代码生成
|
||||
- 项目初始化
|
||||
- 部署配置
|
||||
|
||||
### 版本管理
|
||||
|
||||
- 在 reference 文件中标注框架/语言版本
|
||||
- 更新时在 commit 中说明版本变化
|
||||
- 过时内容应更新而非删除(除非完全废弃)
|
||||
|
||||
### 内容质量
|
||||
|
||||
- 所有建议应有依据(官方文档、最佳实践)
|
||||
- 避免主观偏好(如代码风格),专注于客观问题
|
||||
- 优先覆盖常见陷阱和安全问题
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 如何测试我的更改?
|
||||
|
||||
将修改后的 Skill 复制到 `~/.claude/skills/` 目录,然后在 Claude Code 中测试:
|
||||
```bash
|
||||
cp -r code-review-skill ~/.claude/skills/code-review-skill
|
||||
```
|
||||
|
||||
### Q: 我应该更新 SKILL.md 还是 reference 文件?
|
||||
|
||||
- **SKILL.md**:只修改索引表或核心原则
|
||||
- **reference/*.md**:添加/更新具体的语言或框架内容
|
||||
|
||||
### Q: 如何处理过时的内容?
|
||||
|
||||
1. 标注版本变化(如 "React 18 → React 19")
|
||||
2. 保留旧版本内容(如果仍有用户使用)
|
||||
3. 在 checklist 中更新相关项
|
||||
|
||||
---
|
||||
|
||||
## 问题咨询
|
||||
|
||||
如有任何问题,欢迎在 GitHub Issues 中提问。
|
||||
Executable
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 tt-a1i
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
Executable
+658
@@ -0,0 +1,658 @@
|
||||
<div align="center">
|
||||
|
||||
<h1>🔍 Code Review Skill</h1>
|
||||
|
||||
<p>
|
||||
<strong>A comprehensive, modular code review skill for Claude Code</strong><br/>
|
||||
<strong>面向 Claude Code 的全面模块化代码审查技能</strong>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="https://github.com/awesome-skills/code-review-skill/blob/main/LICENSE">
|
||||
<img src="https://img.shields.io/badge/License-MIT-22c55e?style=flat-square" alt="License: MIT"/>
|
||||
</a>
|
||||
<img src="https://img.shields.io/badge/Claude_Code-Skill-7c3aed?style=flat-square&logo=anthropic&logoColor=white" alt="Claude Code Skill"/>
|
||||
<img src="https://img.shields.io/badge/Total_Lines-16%2C000%2B-3b82f6?style=flat-square" alt="16000+ lines"/>
|
||||
<img src="https://img.shields.io/badge/Languages-20%2B-f59e0b?style=flat-square" alt="20+ languages"/>
|
||||
<img src="https://img.shields.io/badge/PRs-Welcome-ec4899?style=flat-square" alt="PRs Welcome"/>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="#english">English</a>
|
||||
·
|
||||
<a href="#chinese">中文</a>
|
||||
·
|
||||
<a href="./CONTRIBUTING.md">Contributing</a>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
<a name="english"></a>
|
||||
|
||||
## English
|
||||
|
||||
### What is this?
|
||||
|
||||
**Code Review Skill** is a production-ready skill for [Claude Code](https://claude.ai/code) that transforms AI-assisted code review from vague suggestions into a **structured, consistent, and expert-level** process.
|
||||
|
||||
It covers **20+ languages and frameworks** with over **16,000 lines** of carefully curated review guidelines — loaded progressively to minimize context window usage.
|
||||
|
||||
---
|
||||
|
||||
### ✨ Key Features
|
||||
|
||||
- **Progressive Disclosure** — Core skill is ~190 lines; language guides (~200–1,000 lines each) load only when needed.
|
||||
- **Four-Phase Review Process** — Structured workflow from understanding scope to delivering clear feedback.
|
||||
- **Severity Labeling** — Every finding is categorized: `blocking` · `important` · `nit` · `suggestion` · `learning` · `praise`
|
||||
- **Security-First** — Dedicated security checklists per language ecosystem.
|
||||
- **Collaborative Tone** — Questions over commands, suggestions over mandates.
|
||||
- **Automation Awareness** — Clearly separates what human review should catch vs. what linters handle.
|
||||
|
||||
---
|
||||
|
||||
### 🌐 Supported Languages & Frameworks
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Category</th>
|
||||
<th>Technology</th>
|
||||
<th>Guide</th>
|
||||
<th>Lines</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td rowspan="6"><strong>Frontend</strong></td>
|
||||
<td>⚛️ React 19 / Next.js / TanStack Query v5</td>
|
||||
<td><code>reference/react.md</code></td>
|
||||
<td>~870</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>💚 Vue 3.5 + Composition API</td>
|
||||
<td><code>reference/vue.md</code></td>
|
||||
<td>~920</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>🔮 Angular 17+ / Signals / Zoneless</td>
|
||||
<td><code>reference/angular.md</code></td>
|
||||
<td>~420</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>🔥 Svelte 5 / SvelteKit</td>
|
||||
<td><code>reference/svelte.md</code></td>
|
||||
<td>~1,060</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>🎨 CSS / Less / Sass</td>
|
||||
<td><code>reference/css-less-sass.md</code></td>
|
||||
<td>~660</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>🔷 TypeScript</td>
|
||||
<td><code>reference/typescript.md</code></td>
|
||||
<td>~540</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="9"><strong>Backend</strong></td>
|
||||
<td>☕ Java 17/21 + Spring Boot 3</td>
|
||||
<td><code>reference/java.md</code></td>
|
||||
<td>~410</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>⚡ FastAPI</td>
|
||||
<td><code>reference/fastapi.md</code></td>
|
||||
<td>~590</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>PHP 8.x</td>
|
||||
<td><code>reference/php.md</code></td>
|
||||
<td>~700</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>📦 NestJS</td>
|
||||
<td><code>reference/nestjs.md</code></td>
|
||||
<td>~590</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>🐍 Django / DRF</td>
|
||||
<td><code>reference/django.md</code></td>
|
||||
<td>~1,030</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>🐹 Go</td>
|
||||
<td><code>reference/go.md</code></td>
|
||||
<td>~990</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>🦀 Rust</td>
|
||||
<td><code>reference/rust.md</code></td>
|
||||
<td>~840</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>💻 C# / .NET 8</td>
|
||||
<td><code>reference/csharp.md</code></td>
|
||||
<td>~520</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>🐍 Python</td>
|
||||
<td><code>reference/python.md</code></td>
|
||||
<td>~1,070</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="5"><strong>Mobile / Systems</strong></td>
|
||||
<td>📱 Kotlin / Android</td>
|
||||
<td><code>reference/kotlin.md</code></td>
|
||||
<td>~1,020</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>🍎 Swift / SwiftUI</td>
|
||||
<td><code>reference/swift.md</code></td>
|
||||
<td>~930</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>⚙️ C</td>
|
||||
<td><code>reference/c.md</code></td>
|
||||
<td>~290</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>🔩 C++</td>
|
||||
<td><code>reference/cpp.md</code></td>
|
||||
<td>~390</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>🖥️ Qt Framework</td>
|
||||
<td><code>reference/qt.md</code></td>
|
||||
<td>~190</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="3"><strong>Cross-Cutting</strong></td>
|
||||
<td>🏛️ Architecture Design Review</td>
|
||||
<td><code>reference/architecture-review-guide.md</code></td>
|
||||
<td>~470</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>⚡ Performance Review</td>
|
||||
<td><code>reference/performance-review-guide.md</code></td>
|
||||
<td>~820</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>🔍 Universal Quality Anti-Patterns</td>
|
||||
<td><code>reference/code-quality-universal.md</code></td>
|
||||
<td>~490</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
---
|
||||
|
||||
### 🔄 The Four-Phase Review Process
|
||||
|
||||
```
|
||||
Phase 1 - Context Gathering
|
||||
Understand PR scope, linked issues, and intent
|
||||
|
|
||||
v
|
||||
Phase 2 - High-Level Review
|
||||
Architecture - Performance impact - Test strategy
|
||||
|
|
||||
v
|
||||
Phase 3 - Line-by-Line Analysis
|
||||
Logic - Security - Maintainability - Edge cases
|
||||
|
|
||||
v
|
||||
Phase 4 - Summary & Decision
|
||||
Structured feedback - Approval status - Action items
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🏷️ Severity Labels
|
||||
|
||||
| Label | Meaning |
|
||||
|-------|---------|
|
||||
| 🔴 `blocking` | Must be fixed before merge |
|
||||
| 🟠 `important` | Should be fixed; may block depending on context |
|
||||
| 🟡 `nit` | Minor style or preference issue |
|
||||
| 🔵 `suggestion` | Optional improvement worth considering |
|
||||
| 📚 `learning` | Educational note for the author |
|
||||
| 🌟 `praise` | Explicitly highlight great work |
|
||||
|
||||
---
|
||||
|
||||
### 📁 Repository Structure
|
||||
|
||||
```
|
||||
code-review-skill/
|
||||
|
|
||||
+-- SKILL.md # Core skill - loaded on activation (~190 lines)
|
||||
+-- README.md
|
||||
+-- LICENSE
|
||||
+-- CONTRIBUTING.md
|
||||
|
|
||||
+-- reference/ # On-demand language guides
|
||||
| +-- react.md # React 19 / Next.js / TanStack Query v5
|
||||
| +-- vue.md # Vue 3.5 Composition API
|
||||
| +-- angular.md # Angular 17+ / Signals / Zoneless
|
||||
| +-- svelte.md # Svelte 5 / SvelteKit
|
||||
| +-- rust.md # Rust ownership, async/await, unsafe
|
||||
| +-- typescript.md # TypeScript strict mode, generics, ESLint
|
||||
| +-- nestjs.md # NestJS DI, Guards, Interceptors, DTOs
|
||||
| +-- java.md # Java 17/21 & Spring Boot 3
|
||||
| +-- php.md # PHP 8.x types, PDO, security, Composer
|
||||
| +-- python.md # Python async, typing, pytest
|
||||
| +-- django.md # Django / DRF security, serializers, async
|
||||
| +-- fastapi.md # FastAPI Depends, Pydantic v2, async, test-driven verification
|
||||
| +-- go.md # Go goroutines, channels, context, interfaces
|
||||
| +-- kotlin.md # Kotlin / Android coroutines, Compose, Flow
|
||||
| +-- swift.md # Swift 5.9+/6, SwiftUI, concurrency, optionals
|
||||
| +-- csharp.md # C# 12 / .NET 8, EF Core, ASP.NET Core
|
||||
| +-- c.md # C memory safety, UB, error handling
|
||||
| +-- cpp.md # C++ RAII, move semantics, exception safety
|
||||
| +-- qt.md # Qt object model, signals/slots, GUI perf
|
||||
| +-- css-less-sass.md # CSS/Less/Sass variables, responsive design
|
||||
| +-- architecture-review-guide.md # SOLID, anti-patterns, coupling/cohesion
|
||||
| +-- code-quality-universal.md # Reuse audit, parameter sprawl, TOCTOU, no-op updates
|
||||
| +-- performance-review-guide.md # Core Web Vitals, N+1, memory leaks
|
||||
| +-- security-review-guide.md # Security checklist (all languages)
|
||||
| +-- common-bugs-checklist.md # Language-specific bug patterns
|
||||
| +-- code-review-best-practices.md # Communication & process guidelines
|
||||
|
|
||||
+-- assets/
|
||||
| +-- review-checklist.md # Quick reference checklist
|
||||
| +-- pr-review-template.md # PR review comment template
|
||||
|
|
||||
+-- scripts/
|
||||
+-- pr-analyzer.py # PR complexity analyzer
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🚀 Installation
|
||||
|
||||
**Clone to your Claude Code skills directory:**
|
||||
|
||||
```bash
|
||||
# macOS / Linux
|
||||
git clone https://github.com/awesome-skills/code-review-skill.git \
|
||||
~/.claude/skills/code-review-skill
|
||||
|
||||
# Windows (PowerShell)
|
||||
git clone https://github.com/awesome-skills/code-review-skill.git `
|
||||
"$env:USERPROFILE\.claude\skills\code-review-skill"
|
||||
```
|
||||
|
||||
**Or add to an existing plugin:**
|
||||
|
||||
```bash
|
||||
cp -r code-review-skill ~/.claude/plugins/your-plugin/skills/code-review/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 💡 Usage
|
||||
|
||||
Once installed, activate the skill in your Claude Code session:
|
||||
|
||||
```
|
||||
Use code-review-skill to review this PR
|
||||
```
|
||||
|
||||
Or create a custom slash command in `.claude/commands/`:
|
||||
|
||||
```markdown
|
||||
<!-- .claude/commands/review.md -->
|
||||
Use code-review-skill to perform a thorough review of the changes in this PR.
|
||||
Focus on: security, performance, and maintainability.
|
||||
```
|
||||
|
||||
**Example prompts:**
|
||||
|
||||
| Prompt | What happens |
|
||||
|--------|-------------|
|
||||
| `Review this React component` | Loads `react.md` - checks hooks, Server Components, Suspense patterns |
|
||||
| `Review this Java PR` | Loads `java.md` - checks virtual threads, JPA, Spring Boot 3 patterns |
|
||||
| `Security review of this Go service` | Loads `go.md` + `security-review-guide.md` |
|
||||
| `Architecture review` | Loads `architecture-review-guide.md` - SOLID, anti-patterns, coupling |
|
||||
| `Performance review` | Loads `performance-review-guide.md` - Web Vitals, N+1, complexity |
|
||||
|
||||
---
|
||||
|
||||
### 🔬 Highlights by Language
|
||||
|
||||
<details>
|
||||
<summary><strong>⚛️ React 19</strong></summary>
|
||||
|
||||
- `useActionState` - Unified form state management
|
||||
- `useFormStatus` - Access parent form status without prop drilling
|
||||
- `useOptimistic` - Optimistic UI updates with automatic rollback
|
||||
- Server Components & Server Actions patterns (Next.js 15+)
|
||||
- Suspense boundary design, Error Boundary integration, streaming SSR
|
||||
- `use()` Hook for consuming Promises
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>☕ Java & Spring Boot 3</strong></summary>
|
||||
|
||||
- **Java 17/21**: Records, Pattern Matching for Switch, Text Blocks, Sealed Classes
|
||||
- **Virtual Threads** (Project Loom): High-throughput I/O patterns
|
||||
- **Spring Boot 3**: Constructor injection, `@ConfigurationProperties`, `ProblemDetail`
|
||||
- **JPA Performance**: Solving N+1, correct `equals`/`hashCode` on Entities
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>🦀 Rust</strong></summary>
|
||||
|
||||
- Ownership patterns and common pitfalls
|
||||
- `unsafe` code review requirements (mandatory `SAFETY` comments)
|
||||
- Async/await - avoiding blocking in async context, cancellation safety
|
||||
- Error handling: `thiserror` for libraries, `anyhow` for applications
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>🐹 Go</strong></summary>
|
||||
|
||||
- Goroutine lifecycle management and leak prevention
|
||||
- Channel patterns, select usage
|
||||
- `context.Context` propagation
|
||||
- Interface design (accept interfaces, return structs)
|
||||
- Error wrapping with `%w`
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>⚙️ C / C++</strong></summary>
|
||||
|
||||
- **C**: Pointer/buffer safety, undefined behavior, resource cleanup, integer overflow
|
||||
- **C++**: RAII ownership, Rule of 0/3/5, move semantics, exception safety, `noexcept`
|
||||
- **Qt**: Object parent/child memory model, thread-safe signal/slot connections, GUI performance
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### 🤝 Contributing
|
||||
|
||||
Contributions are welcome! See [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines.
|
||||
|
||||
**Ideas:**
|
||||
- New language guides (Ruby, Elixir, Scala...)
|
||||
- Framework-specific guides (Laravel, Spring WebFlux...)
|
||||
- Additional checklists and templates
|
||||
- Translations of core documentation
|
||||
|
||||
---
|
||||
|
||||
### 📄 License
|
||||
|
||||
MIT © [awesome-skills](https://github.com/awesome-skills)
|
||||
|
||||
---
|
||||
|
||||
<a name="chinese"></a>
|
||||
|
||||
## 中文
|
||||
|
||||
### 这是什么?
|
||||
|
||||
**Code Review Skill** 是专为 [Claude Code](https://claude.ai/code) 打造的生产级代码审查技能,将 AI 辅助的代码审查从模糊建议转变为**结构化、一致且专业级**的流程。
|
||||
|
||||
覆盖 **20+ 种语言和框架**,拥有超过 **16,000 行**精心整理的代码审查指南——按需加载,最大程度减少上下文占用。
|
||||
|
||||
---
|
||||
|
||||
### ✨ 核心特性
|
||||
|
||||
- **渐进式加载** — 核心技能仅 ~190 行,各语言指南(每份 200–1,000 行)仅在需要时才加载。
|
||||
- **四阶段审查流程** — 从理解 PR 范围到输出清晰反馈,每一步都有规可循。
|
||||
- **严重性标记** — 每条发现均分级:`blocking` · `important` · `nit` · `suggestion` · `learning` · `praise`
|
||||
- **安全优先** — 每个语言生态均配备专属安全检查清单。
|
||||
- **协作式语气** — 以提问替代命令,以建议替代指令。
|
||||
- **自动化感知** — 明确区分人工审查应关注的内容与 linter 自动处理的内容。
|
||||
|
||||
---
|
||||
|
||||
### 🌐 支持的语言与框架
|
||||
|
||||
| 分类 | 技术栈 | 指南文件 | 行数 |
|
||||
|------|--------|----------|------|
|
||||
| **前端** | ⚛️ React 19 / Next.js / TanStack Query v5 | `reference/react.md` | ~870 |
|
||||
| | 💚 Vue 3.5 Composition API | `reference/vue.md` | ~920 |
|
||||
| | 🔮 Angular 17+ / Signals / Zoneless | `reference/angular.md` | ~420 |
|
||||
| | 🔥 Svelte 5 / SvelteKit | `reference/svelte.md` | ~1,060 |
|
||||
| | 🎨 CSS / Less / Sass | `reference/css-less-sass.md` | ~660 |
|
||||
| | 🔷 TypeScript | `reference/typescript.md` | ~540 |
|
||||
| **后端** | ☕ Java 17/21 + Spring Boot 3 | `reference/java.md` | ~410 |
|
||||
| | ⚡ FastAPI | `reference/fastapi.md` | ~590 |
|
||||
| | PHP 8.x | `reference/php.md` | ~700 |
|
||||
| | 📦 NestJS | `reference/nestjs.md` | ~590 |
|
||||
| | 🐍 Django / DRF | `reference/django.md` | ~1,030 |
|
||||
| | 🐍 Python | `reference/python.md` | ~1,070 |
|
||||
| | 🐹 Go | `reference/go.md` | ~990 |
|
||||
| | 🦀 Rust | `reference/rust.md` | ~840 |
|
||||
| | 💻 C# / .NET 8 | `reference/csharp.md` | ~520 |
|
||||
| **移动 / 系统** | 📱 Kotlin / Android | `reference/kotlin.md` | ~1,020 |
|
||||
| | 🍎 Swift / SwiftUI | `reference/swift.md` | ~930 |
|
||||
| | ⚙️ C | `reference/c.md` | ~290 |
|
||||
| | 🔩 C++ | `reference/cpp.md` | ~390 |
|
||||
| | 🖥️ Qt 框架 | `reference/qt.md` | ~190 |
|
||||
| **架构** | 🏛️ 架构设计审查 | `reference/architecture-review-guide.md` | ~470 |
|
||||
| | ⚡ 性能审查 | `reference/performance-review-guide.md` | ~820 |
|
||||
| | 🔍 通用质量反模式 | `reference/code-quality-universal.md` | ~490 |
|
||||
|
||||
---
|
||||
|
||||
### 🔄 四阶段审查流程
|
||||
|
||||
```
|
||||
阶段一 - 上下文收集
|
||||
理解 PR 范围、关联 Issue 和实现意图
|
||||
|
|
||||
v
|
||||
阶段二 - 高层级审查
|
||||
架构设计 - 性能影响 - 测试策略
|
||||
|
|
||||
v
|
||||
阶段三 - 逐行深度分析
|
||||
逻辑正确性 - 安全漏洞 - 可维护性 - 边界情况
|
||||
|
|
||||
v
|
||||
阶段四 - 总结与决策
|
||||
结构化反馈 - 审批状态 - 后续行动项
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🏷️ 严重性标记说明
|
||||
|
||||
| 标记 | 含义 |
|
||||
|------|------|
|
||||
| 🔴 `blocking` | 合并前必须修复 |
|
||||
| 🟠 `important` | 应当修复,视情况可能阻塞合并 |
|
||||
| 🟡 `nit` | 风格或偏好上的小问题 |
|
||||
| 🔵 `suggestion` | 值得考虑的可选优化 |
|
||||
| 📚 `learning` | 给作者的教育性说明 |
|
||||
| 🌟 `praise` | 明确表扬优秀代码 |
|
||||
|
||||
---
|
||||
|
||||
### 📁 仓库结构
|
||||
|
||||
```
|
||||
code-review-skill/
|
||||
|
|
||||
+-- SKILL.md # 核心技能,激活时加载(~190 行)
|
||||
+-- README.md
|
||||
+-- LICENSE
|
||||
+-- CONTRIBUTING.md
|
||||
|
|
||||
+-- reference/ # 按需加载的语言指南
|
||||
| +-- react.md # React 19 / Next.js / TanStack Query v5
|
||||
| +-- vue.md # Vue 3.5 组合式 API
|
||||
| +-- angular.md # Angular 17+ / Signals / Zoneless
|
||||
| +-- svelte.md # Svelte 5 / SvelteKit
|
||||
| +-- rust.md # Rust 所有权、async/await、unsafe
|
||||
| +-- typescript.md # TypeScript strict 模式、泛型、ESLint
|
||||
| +-- nestjs.md # NestJS 依赖注入、Guard、Interceptor、DTO
|
||||
| +-- java.md # Java 17/21 & Spring Boot 3
|
||||
| +-- php.md # PHP 8.x 类型、PDO、安全、Composer
|
||||
| +-- python.md # Python async、类型注解、pytest
|
||||
| +-- django.md # Django / DRF 安全、Serializer、异步视图
|
||||
| +-- fastapi.md # FastAPI Depends、Pydantic v2、异步、测试驱动验证
|
||||
| +-- go.md # Go goroutine、channel、context、接口
|
||||
| +-- kotlin.md # Kotlin / Android 协程、Compose、Flow
|
||||
| +-- swift.md # Swift 5.9+/6、SwiftUI、并发、可选值
|
||||
| +-- csharp.md # C# 12 / .NET 8、EF Core、ASP.NET Core
|
||||
| +-- c.md # C 内存安全、UB、错误处理
|
||||
| +-- cpp.md # C++ RAII、移动语义、异常安全
|
||||
| +-- qt.md # Qt 对象模型、信号/槽、GUI 性能
|
||||
| +-- css-less-sass.md # CSS/Less/Sass 变量、响应式设计
|
||||
| +-- architecture-review-guide.md # SOLID、反模式、耦合度分析
|
||||
| +-- code-quality-universal.md # 复用审查、参数膨胀、抽象泄漏、TOCTOU
|
||||
| +-- performance-review-guide.md # Core Web Vitals、N+1、内存泄漏
|
||||
| +-- security-review-guide.md # 安全审查清单(全语言通用)
|
||||
| +-- common-bugs-checklist.md # 各语言常见 Bug 模式
|
||||
| +-- code-review-best-practices.md # 沟通与流程最佳实践
|
||||
|
|
||||
+-- assets/
|
||||
| +-- review-checklist.md # 快速参考清单
|
||||
| +-- pr-review-template.md # PR 审查评论模板
|
||||
|
|
||||
+-- scripts/
|
||||
+-- pr-analyzer.py # PR 复杂度分析工具
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🚀 安装方法
|
||||
|
||||
**克隆到 Claude Code skills 目录:**
|
||||
|
||||
```bash
|
||||
# macOS / Linux
|
||||
git clone https://github.com/awesome-skills/code-review-skill.git \
|
||||
~/.claude/skills/code-review-skill
|
||||
|
||||
# Windows(PowerShell)
|
||||
git clone https://github.com/awesome-skills/code-review-skill.git `
|
||||
"$env:USERPROFILE\.claude\skills\code-review-skill"
|
||||
```
|
||||
|
||||
**或添加到现有插件:**
|
||||
|
||||
```bash
|
||||
cp -r code-review-skill ~/.claude/plugins/your-plugin/skills/code-review/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 💡 使用方式
|
||||
|
||||
安装后,在 Claude Code 会话中激活技能:
|
||||
|
||||
```
|
||||
Use code-review-skill to review this PR
|
||||
```
|
||||
|
||||
或在 `.claude/commands/` 中创建自定义斜杠命令:
|
||||
|
||||
```markdown
|
||||
<!-- .claude/commands/review.md -->
|
||||
使用 code-review-skill 对这次 PR 的变更进行全面审查。
|
||||
重点关注:安全性、性能和可维护性。
|
||||
```
|
||||
|
||||
**示例提示词:**
|
||||
|
||||
| 提示词 | 效果 |
|
||||
|--------|------|
|
||||
| `审查这个 React 组件` | 加载 `react.md`,检查 Hooks、Server Components、Suspense |
|
||||
| `审查这个 Java PR` | 加载 `java.md`,检查虚拟线程、JPA、Spring Boot 3 |
|
||||
| `对这个 Go 服务进行安全审查` | 加载 `go.md` + `security-review-guide.md` |
|
||||
| `架构审查` | 加载 `architecture-review-guide.md`,检查 SOLID 与反模式 |
|
||||
| `性能审查` | 加载 `performance-review-guide.md`,分析 Web Vitals、N+1 等 |
|
||||
|
||||
---
|
||||
|
||||
### 🔬 各语言核心内容
|
||||
|
||||
<details>
|
||||
<summary><strong>⚛️ React 19</strong></summary>
|
||||
|
||||
- `useActionState` — 统一的表单状态管理
|
||||
- `useFormStatus` — 无需 props 透传即可访问父表单状态
|
||||
- `useOptimistic` — 带自动回滚的乐观 UI 更新
|
||||
- Server Components & Server Actions(Next.js 15+)
|
||||
- Suspense 边界设计、Error Boundary 集成、流式 SSR
|
||||
- `use()` Hook 消费 Promise
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>☕ Java & Spring Boot 3</strong></summary>
|
||||
|
||||
- **Java 17/21**:Records、Switch 模式匹配、文本块、Sealed Classes
|
||||
- **虚拟线程**(Project Loom):高吞吐量 I/O 模式
|
||||
- **Spring Boot 3**:构造器注入、`@ConfigurationProperties`、`ProblemDetail`
|
||||
- **JPA 性能**:解决 N+1、Entity 正确的 `equals`/`hashCode` 实现
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>🦀 Rust</strong></summary>
|
||||
|
||||
- 所有权模式与常见陷阱
|
||||
- `unsafe` 代码审查要求(必须有 `SAFETY` 注释)
|
||||
- Async/await — 避免在异步上下文中阻塞,取消安全性
|
||||
- 错误处理:库用 `thiserror`,应用用 `anyhow`
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>🐹 Go</strong></summary>
|
||||
|
||||
- Goroutine 生命周期管理与泄漏预防
|
||||
- Channel 模式、select 用法
|
||||
- `context.Context` 传播规范
|
||||
- 接口设计原则(接受接口,返回结构体)
|
||||
- 错误包装:使用 `%w`
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>⚙️ C / C++</strong></summary>
|
||||
|
||||
- **C**:指针/缓冲区安全、未定义行为、资源清理、整数溢出
|
||||
- **C++**:RAII 所有权、Rule of 0/3/5、移动语义、异常安全、`noexcept`
|
||||
- **Qt**:父子内存模型、线程安全的信号/槽连接、GUI 性能优化
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### 🤝 参与贡献
|
||||
|
||||
欢迎贡献!请查阅 [CONTRIBUTING.md](./CONTRIBUTING.md) 了解规范。
|
||||
|
||||
**可贡献方向:**
|
||||
- 新增语言指南(Ruby、Elixir、Scala...)
|
||||
- 框架专属指南(Laravel、Spring WebFlux...)
|
||||
- 补充检查清单和审查模板
|
||||
- 核心文档的多语言翻译
|
||||
|
||||
---
|
||||
|
||||
### 📄 开源协议
|
||||
|
||||
MIT © [awesome-skills](https://github.com/awesome-skills)
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
Made with ❤️ for developers who care about code quality
|
||||
</div>
|
||||
Executable
+220
@@ -0,0 +1,220 @@
|
||||
---
|
||||
name: code-review-skill
|
||||
description: |
|
||||
Provides comprehensive code review guidance for React 19, Vue 3, Angular 17+, Svelte 5, Rust, TypeScript, Java, PHP, Python, Django, Go, C#/.NET, Kotlin, Swift, NestJS, C/C++, and more.
|
||||
Helps catch bugs, improve code quality, and give constructive feedback.
|
||||
Use when: reviewing pull requests, conducting PR reviews, code review, reviewing code changes,
|
||||
establishing review standards, mentoring developers, architecture reviews, security audits,
|
||||
checking code quality, finding bugs, giving feedback on code.
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Grep
|
||||
- Glob
|
||||
- Bash # 运行 lint/test/build 命令验证代码质量
|
||||
- WebFetch # 查阅最新文档和最佳实践
|
||||
---
|
||||
|
||||
# Code Review Skill
|
||||
|
||||
Transform code reviews from gatekeeping to knowledge sharing through constructive feedback, systematic analysis, and collaborative improvement.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Reviewing pull requests and code changes
|
||||
- Establishing code review standards for teams
|
||||
- Mentoring junior developers through reviews
|
||||
- Conducting architecture reviews
|
||||
- Creating review checklists and guidelines
|
||||
- Improving team collaboration
|
||||
- Reducing code review cycle time
|
||||
- Maintaining code quality standards
|
||||
|
||||
## Core Principles
|
||||
|
||||
### 1. The Review Mindset
|
||||
|
||||
**Goals of Code Review:**
|
||||
- Catch bugs and edge cases
|
||||
- Ensure code maintainability
|
||||
- Share knowledge across team
|
||||
- Enforce coding standards
|
||||
- Improve design and architecture
|
||||
- Build team culture
|
||||
|
||||
**Not the Goals:**
|
||||
- Show off knowledge
|
||||
- Nitpick formatting (use linters)
|
||||
- Block progress unnecessarily
|
||||
- Rewrite to your preference
|
||||
|
||||
### 2. Effective Feedback
|
||||
|
||||
**Good Feedback is:**
|
||||
- Specific and actionable
|
||||
- Educational, not judgmental
|
||||
- Focused on the code, not the person
|
||||
- Balanced (praise good work too)
|
||||
- Prioritized (critical vs nice-to-have)
|
||||
|
||||
```markdown
|
||||
❌ Bad: "This is wrong."
|
||||
✅ Good: "This could cause a race condition when multiple users
|
||||
access simultaneously. Consider using a mutex here."
|
||||
|
||||
❌ Bad: "Why didn't you use X pattern?"
|
||||
✅ Good: "Have you considered the Repository pattern? It would
|
||||
make this easier to test. Here's an example: [link]"
|
||||
|
||||
❌ Bad: "Rename this variable."
|
||||
✅ Good: "[nit] Consider `userCount` instead of `uc` for
|
||||
clarity. Not blocking if you prefer to keep it."
|
||||
```
|
||||
|
||||
### 3. Review Scope
|
||||
|
||||
**What to Review:**
|
||||
- Logic correctness and edge cases
|
||||
- Security vulnerabilities
|
||||
- Performance implications
|
||||
- Test coverage and quality
|
||||
- Error handling
|
||||
- Documentation and comments
|
||||
- API design and naming
|
||||
- Architectural fit
|
||||
|
||||
**What Not to Review Manually:**
|
||||
- Code formatting (use Prettier, Black, etc.)
|
||||
- Import organization
|
||||
- Linting violations
|
||||
- Simple typos
|
||||
|
||||
## Review Process
|
||||
|
||||
### Phase 1: Context Gathering (2-3 minutes)
|
||||
|
||||
Before diving into code, understand:
|
||||
1. Read PR description and linked issue
|
||||
2. Check PR size (>400 lines? Ask to split)
|
||||
3. Review CI/CD status (tests passing?)
|
||||
4. Understand the business requirement
|
||||
5. Note any relevant architectural decisions
|
||||
|
||||
> For large diffs, pipe the diff through [`scripts/pr-analyzer.py`](scripts/pr-analyzer.py) (`git diff main...HEAD | python scripts/pr-analyzer.py`) to triage complexity and get a suggested review approach before reading.
|
||||
|
||||
### Phase 2: High-Level Review (5-10 minutes)
|
||||
|
||||
1. **Architecture & Design** - Does the solution fit the problem?
|
||||
- For significant changes, consult [Architecture Review Guide](reference/architecture-review-guide.md)
|
||||
- Check: SOLID principles, coupling/cohesion, anti-patterns
|
||||
2. **Performance Assessment** - Are there performance concerns?
|
||||
- For performance-critical code, consult [Performance Review Guide](reference/performance-review-guide.md)
|
||||
- Check: Algorithm complexity, N+1 queries, memory usage
|
||||
3. **File Organization** - Are new files in the right places?
|
||||
4. **Testing Strategy** - Are there tests covering edge cases?
|
||||
|
||||
### Phase 3: Line-by-Line Review (10-20 minutes)
|
||||
|
||||
For each file, check:
|
||||
- **Logic & Correctness** - Edge cases, off-by-one, null checks, race conditions
|
||||
- **Security** - Input validation, injection risks, XSS, sensitive data
|
||||
- **Performance** - N+1 queries, unnecessary loops, memory leaks
|
||||
- **Maintainability** - Clear names, single responsibility, comments
|
||||
- **Reuse** - Before accepting new code, search for existing utilities/helpers that could replace it. Check adjacent files and shared modules for similar patterns. See [Universal Quality Guide](reference/code-quality-universal.md) for anti-patterns like parameter sprawl, leaky abstractions, nested conditionals, stringly-typed code, TOCTOU, and no-op updates.
|
||||
|
||||
### Phase 4: Summary & Decision (2-3 minutes)
|
||||
|
||||
1. Summarize key concerns
|
||||
2. Highlight what you liked
|
||||
3. Make clear decision:
|
||||
- ✅ Approve
|
||||
- 💬 Comment (minor suggestions)
|
||||
- 🔄 Request Changes (must address)
|
||||
4. Offer to pair if complex
|
||||
|
||||
## Review Techniques
|
||||
|
||||
### Technique 1: The Checklist Method
|
||||
|
||||
Use checklists for consistent reviews. See [Security Review Guide](reference/security-review-guide.md) for comprehensive security checklist.
|
||||
|
||||
### Technique 2: The Question Approach
|
||||
|
||||
Instead of stating problems, ask questions:
|
||||
|
||||
```markdown
|
||||
❌ "This will fail if the list is empty."
|
||||
✅ "What happens if `items` is an empty array?"
|
||||
|
||||
❌ "You need error handling here."
|
||||
✅ "How should this behave if the API call fails?"
|
||||
```
|
||||
|
||||
### Technique 3: Suggest, Don't Command
|
||||
|
||||
Use collaborative language:
|
||||
|
||||
```markdown
|
||||
❌ "You must change this to use async/await"
|
||||
✅ "Suggestion: async/await might make this more readable. What do you think?"
|
||||
|
||||
❌ "Extract this into a function"
|
||||
✅ "This logic appears in 3 places. Would it make sense to extract it?"
|
||||
```
|
||||
|
||||
### Technique 4: Differentiate Severity
|
||||
|
||||
Use labels to indicate priority:
|
||||
|
||||
- 🔴 `[blocking]` - Must fix before merge
|
||||
- 🟡 `[important]` - Should fix, discuss if disagree
|
||||
- 🟢 `[nit]` - Nice to have, not blocking
|
||||
- 💡 `[suggestion]` - Alternative approach to consider
|
||||
- 📚 `[learning]` - Educational comment, no action needed
|
||||
- 🎉 `[praise]` - Good work, keep it up!
|
||||
|
||||
**Severity levels:** 🔴 / 🟡 / 🟢 are the three severity tiers used as the standard across all guides in this skill — 🔴 blocks the merge, 🟡 should be addressed, 🟢 is optional. The remaining markers (💡 / 📚 / 🎉) are non-blocking annotations.
|
||||
|
||||
## Language-Specific Guides
|
||||
|
||||
根据审查的代码语言,查阅对应的详细指南:
|
||||
|
||||
| Language/Framework | Reference File | Key Topics |
|
||||
|-------------------|----------------|------------|
|
||||
| **React** | [React Guide](reference/react.md) | Hooks, useEffect, React 19 Actions, RSC, Suspense, TanStack Query v5 |
|
||||
| **Vue 3** | [Vue Guide](reference/vue.md) | Composition API, 响应性系统, Props/Emits, Watchers, Composables |
|
||||
| **Angular 17+** | [Angular Guide](reference/angular.md) | Signals, Standalone 组件, RxJS, Zoneless 变更检测, 模板优化 |
|
||||
| **Rust** | [Rust Guide](reference/rust.md) | 所有权/借用, Unsafe 审查, 异步代码, 取消安全性, 错误处理 |
|
||||
| **TypeScript** | [TypeScript Guide](reference/typescript.md) | 类型安全, async/await, 不可变性 |
|
||||
| **Python** | [Python Guide](reference/python.md) | 可变默认参数, 异常处理, 类属性 |
|
||||
| **Django / DRF** | [Django Guide](reference/django.md) | 安全审查, N+1 查询, Serializer 反模式, ViewSet, 异步视图 |
|
||||
| **FastAPI** | [FastAPI Guide](reference/fastapi.md) | Depends, Pydantic v2 validation, async correctness, sessions/N+1, auth vs authorization, test-driven verification |
|
||||
| **Java** | [Java Guide](reference/java.md) | Java 17/21 新特性, Spring Boot 3, 虚拟线程, Stream/Optional |
|
||||
| **PHP** | [PHP Guide](reference/php.md) | PHP 8.x type system, PDO, security review, Composer, PHPUnit/PHPStan |
|
||||
| **C# / .NET** | [C# Guide](reference/csharp.md) | C# 12 特性, 异步编程, EF Core 性能, ASP.NET Core, LINQ |
|
||||
| **Go** | [Go Guide](reference/go.md) | 错误处理, goroutine/channel, context, 接口设计 |
|
||||
| **Kotlin / Android** | [Kotlin Guide](reference/kotlin.md) | 协程, Flow, Jetpack Compose, 空安全, 内存泄漏, 架构模式 |
|
||||
| **Swift / SwiftUI** | [Swift Guide](reference/swift.md) | Optionals, Swift Concurrency, Sendable/actors, SwiftUI property wrappers, value vs reference types, API design |
|
||||
| **NestJS** | [NestJS Guide](reference/nestjs.md) | 依赖注入, 分层架构, DTO 验证, Guard/Interceptor, 循环依赖 |
|
||||
| **Svelte / SvelteKit** | [Svelte Guide](reference/svelte.md) | Runes, Load 函数, Form Actions, Store 迁移, SSR/CSR 边界 |
|
||||
| **C** | [C Guide](reference/c.md) | 指针/缓冲区, 内存安全, UB, 错误处理 |
|
||||
| **C++** | [C++ Guide](reference/cpp.md) | RAII, 生命周期, Rule of 0/3/5, 异常安全 |
|
||||
| **CSS/Less/Sass** | [CSS Guide](reference/css-less-sass.md) | 变量规范, !important, 性能优化, 响应式, 兼容性 |
|
||||
| **Qt** | [Qt Guide](reference/qt.md) | 对象模型, 信号/槽, 内存管理, 线程安全, 性能 |
|
||||
|
||||
## Cross-Cutting Guides
|
||||
|
||||
Language-agnostic patterns applicable to all code reviews:
|
||||
|
||||
| Topic | Reference File | Key Topics |
|
||||
|-------|----------------|------------|
|
||||
| **Universal Quality** | [Universal Quality Guide](reference/code-quality-universal.md) | Reuse audit, parameter sprawl, leaky abstractions, nested conditionals, stringly-typed code, TOCTOU, no-op updates, redundant state |
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Architecture Review Guide](reference/architecture-review-guide.md) - 架构设计审查指南(SOLID、反模式、耦合度)
|
||||
- [Performance Review Guide](reference/performance-review-guide.md) - 性能审查指南(Web Vitals、N+1、复杂度)
|
||||
- [Common Bugs Checklist](reference/common-bugs-checklist.md) - 按语言分类的常见错误清单
|
||||
- [Security Review Guide](reference/security-review-guide.md) - 安全审查指南
|
||||
- [Code Review Best Practices](reference/code-review-best-practices.md) - 代码审查最佳实践
|
||||
- [PR Review Template](assets/pr-review-template.md) - PR 审查评论模板
|
||||
- [Review Checklist](assets/review-checklist.md) - 快速参考清单
|
||||
@@ -0,0 +1,114 @@
|
||||
# PR Review Template
|
||||
|
||||
Copy and use this template for your code reviews.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
[Brief overview of what was reviewed - 1-2 sentences]
|
||||
|
||||
**PR Size:** [Small/Medium/Large] (~X lines)
|
||||
**Review Time:** [X minutes]
|
||||
|
||||
## Strengths
|
||||
|
||||
- [What was done well]
|
||||
- [Good patterns or approaches used]
|
||||
- [Improvements from previous code]
|
||||
|
||||
## Required Changes
|
||||
|
||||
🔴 **[blocking]** [Issue description]
|
||||
> [Code location or example]
|
||||
> [Suggested fix or explanation]
|
||||
|
||||
🔴 **[blocking]** [Issue description]
|
||||
> [Details]
|
||||
|
||||
## Important Suggestions
|
||||
|
||||
🟡 **[important]** [Issue description]
|
||||
> [Why this matters]
|
||||
> [Suggested approach]
|
||||
|
||||
## Minor Suggestions
|
||||
|
||||
🟢 **[nit]** [Minor improvement suggestion]
|
||||
|
||||
💡 **[suggestion]** [Alternative approach to consider]
|
||||
|
||||
## Learning Notes
|
||||
|
||||
📚 [Educational context worth sharing about X]
|
||||
|
||||
📚 [Background behind design decision Y]
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- [ ] No hardcoded secrets
|
||||
- [ ] Input validation present
|
||||
- [ ] Authorization checks in place
|
||||
- [ ] No SQL/XSS injection risks
|
||||
|
||||
## Test Coverage
|
||||
|
||||
- [ ] Unit tests added/updated
|
||||
- [ ] Edge cases covered
|
||||
- [ ] Error cases tested
|
||||
|
||||
## Verdict
|
||||
|
||||
**[ ] ✅ Approve** - Ready to merge
|
||||
**[ ] 💬 Comment** - Minor suggestions, can merge
|
||||
**[ ] 🔄 Request Changes** - Must address blocking issues
|
||||
|
||||
---
|
||||
|
||||
## Quick Copy Templates
|
||||
|
||||
### Blocking Issue
|
||||
```
|
||||
🔴 **[blocking]** [Title]
|
||||
|
||||
[Description of the issue]
|
||||
|
||||
**Location:** `file.ts:123`
|
||||
|
||||
**Suggested fix:**
|
||||
\`\`\`typescript
|
||||
// Your suggested code
|
||||
\`\`\`
|
||||
```
|
||||
|
||||
### Important Suggestion
|
||||
```
|
||||
🟡 **[important]** [Title]
|
||||
|
||||
[Why this is important]
|
||||
|
||||
**Consider:**
|
||||
- Option A: [description]
|
||||
- Option B: [description]
|
||||
```
|
||||
|
||||
### Minor Suggestion
|
||||
```
|
||||
🟢 **[nit]** [Suggestion]
|
||||
|
||||
Not blocking, but consider [improvement].
|
||||
```
|
||||
|
||||
### Praise
|
||||
```
|
||||
🎉 **[praise]** Great work on [specific thing]!
|
||||
|
||||
[Why this is good]
|
||||
```
|
||||
|
||||
### Learning
|
||||
```
|
||||
📚 **[learning]** [Educational note]
|
||||
|
||||
For context, [X] works this way because [Y]. No action needed — just sharing.
|
||||
```
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
# Code Review Quick Checklist
|
||||
|
||||
Quick reference checklist for code reviews.
|
||||
|
||||
## Pre-Review (2 min)
|
||||
|
||||
- [ ] Read PR description and linked issue
|
||||
- [ ] Check PR size (<400 lines ideal)
|
||||
- [ ] Verify CI/CD status (tests passing?)
|
||||
- [ ] Understand the business requirement
|
||||
|
||||
## Architecture & Design (5 min)
|
||||
|
||||
- [ ] Solution fits the problem
|
||||
- [ ] Consistent with existing patterns
|
||||
- [ ] No simpler approach exists
|
||||
- [ ] Will it scale?
|
||||
- [ ] Changes in right location
|
||||
|
||||
## Logic & Correctness (10 min)
|
||||
|
||||
- [ ] Edge cases handled
|
||||
- [ ] Null/undefined checks present
|
||||
- [ ] Off-by-one errors checked
|
||||
- [ ] Race conditions considered
|
||||
- [ ] Error handling complete
|
||||
- [ ] Correct data types used
|
||||
|
||||
## Security (5 min)
|
||||
|
||||
- [ ] No hardcoded secrets
|
||||
- [ ] Input validated/sanitized
|
||||
- [ ] SQL injection prevented
|
||||
- [ ] XSS prevented
|
||||
- [ ] Authorization checks present
|
||||
- [ ] Sensitive data protected
|
||||
|
||||
## Performance (3 min)
|
||||
|
||||
- [ ] No N+1 queries
|
||||
- [ ] Expensive operations optimized
|
||||
- [ ] Large lists paginated
|
||||
- [ ] No memory leaks
|
||||
- [ ] Caching considered where appropriate
|
||||
|
||||
## Testing (5 min)
|
||||
|
||||
- [ ] Tests exist for new code
|
||||
- [ ] Edge cases tested
|
||||
- [ ] Error cases tested
|
||||
- [ ] Tests are readable
|
||||
- [ ] Tests are deterministic
|
||||
|
||||
## Code Quality (3 min)
|
||||
|
||||
- [ ] Clear variable/function names
|
||||
- [ ] No code duplication
|
||||
- [ ] Functions do one thing
|
||||
- [ ] Complex code commented
|
||||
- [ ] No magic numbers
|
||||
|
||||
## Documentation (2 min)
|
||||
|
||||
- [ ] Public APIs documented
|
||||
- [ ] README updated if needed
|
||||
- [ ] Breaking changes noted
|
||||
- [ ] Complex logic explained
|
||||
|
||||
---
|
||||
|
||||
## Severity Labels
|
||||
|
||||
| Label | Meaning | Action |
|
||||
|-------|---------|--------|
|
||||
| 🔴 `[blocking]` | Must fix | Block merge |
|
||||
| 🟡 `[important]` | Should fix | Discuss if disagree |
|
||||
| 🟢 `[nit]` | Nice to have | Non-blocking |
|
||||
| 💡 `[suggestion]` | Alternative | Consider |
|
||||
| 📚 `[learning]` | Educational comment | No action needed |
|
||||
| 🎉 `[praise]` | Good work | Celebrate! |
|
||||
|
||||
---
|
||||
|
||||
## Decision Matrix
|
||||
|
||||
| Situation | Decision |
|
||||
|-----------|----------|
|
||||
| Critical security issue | 🔴 Block, fix immediately |
|
||||
| Breaking change without migration | 🔴 Block |
|
||||
| Missing error handling | 🟡 Should fix |
|
||||
| No tests for new code | 🟡 Should fix |
|
||||
| Style preference | 🟢 Non-blocking |
|
||||
| Minor naming improvement | 🟢 Non-blocking |
|
||||
| Clever but working code | 💡 Suggest simpler |
|
||||
|
||||
---
|
||||
|
||||
## Time Budget
|
||||
|
||||
| PR Size | Target Time |
|
||||
|---------|-------------|
|
||||
| < 100 lines | 10-15 min |
|
||||
| 100-400 lines | 20-40 min |
|
||||
| > 400 lines | Ask to split |
|
||||
|
||||
---
|
||||
|
||||
## Red Flags
|
||||
|
||||
Watch for these patterns:
|
||||
|
||||
- `// TODO` in production code
|
||||
- `console.log` left in code
|
||||
- Commented out code
|
||||
- `any` type in TypeScript
|
||||
- Empty catch blocks
|
||||
- `unwrap()` in Rust production code
|
||||
- Magic numbers/strings
|
||||
- Copy-pasted code blocks
|
||||
- Missing null checks
|
||||
- Hardcoded URLs/credentials
|
||||
+702
@@ -0,0 +1,702 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>code-review-skill(1) — User Commands (en_US)</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
:root {
|
||||
--bg: #14110d;
|
||||
--bg-alt: #1a1611;
|
||||
--fg: #c4b596;
|
||||
--fg-bright:#e8d5a8;
|
||||
--fg-dim: #7a6f56;
|
||||
--fg-faint: #4a4334;
|
||||
--amber: #d8964a;
|
||||
--amber-2: #e8a455;
|
||||
--red: #d56350;
|
||||
--green: #8fae5a;
|
||||
--blue: #6b94c4;
|
||||
--rule: #2a2520;
|
||||
}
|
||||
|
||||
html { background: var(--bg); }
|
||||
|
||||
body {
|
||||
font-family: 'IBM Plex Mono', ui-monospace, 'SF Mono', Menlo, monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.65;
|
||||
color: var(--fg);
|
||||
background: var(--bg);
|
||||
min-height: 100vh;
|
||||
padding: 0 0 4rem;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* faint scanline-free phosphor texture — very subtle */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
background:
|
||||
radial-gradient(ellipse at 50% 0%, rgba(216,150,74,0.04) 0%, transparent 60%);
|
||||
}
|
||||
|
||||
/* ─── HEADER / FOOTER BAND ─── */
|
||||
.band {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--bg);
|
||||
border-bottom: 1px solid var(--rule);
|
||||
z-index: 10;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.band-inner {
|
||||
max-width: 820px;
|
||||
margin: 0 auto;
|
||||
padding: 0.625rem 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
color: var(--fg-dim);
|
||||
}
|
||||
|
||||
.band-l, .band-r {
|
||||
color: var(--fg-bright);
|
||||
letter-spacing: 0.04em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.band-c { color: var(--fg-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
|
||||
.band a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px dotted var(--fg-faint);
|
||||
}
|
||||
.band a:hover { color: var(--amber); border-bottom-color: var(--amber); }
|
||||
|
||||
/* ─── PAGE ─── */
|
||||
main {
|
||||
max-width: 820px;
|
||||
margin: 0 auto;
|
||||
padding: 3rem 2rem 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
pre, .pre {
|
||||
font-family: inherit;
|
||||
white-space: pre;
|
||||
color: inherit;
|
||||
background: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ─── SECTIONS ─── */
|
||||
h2.sec {
|
||||
color: var(--fg-bright);
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.04em;
|
||||
margin: 2.75rem 0 0.875rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
h2.sec::before { content: ''; }
|
||||
|
||||
section.body {
|
||||
padding-left: 7ch;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
section.body p {
|
||||
margin-bottom: 0.875rem;
|
||||
max-width: 70ch;
|
||||
}
|
||||
section.body p:last-child { margin-bottom: 0; }
|
||||
|
||||
.em { color: var(--fg-bright); }
|
||||
.dim { color: var(--fg-dim); }
|
||||
.faint { color: var(--fg-faint); }
|
||||
.amber { color: var(--amber); }
|
||||
.red { color: var(--red); }
|
||||
.green { color: var(--green); }
|
||||
.blue { color: var(--blue); }
|
||||
|
||||
a.link {
|
||||
color: var(--amber);
|
||||
text-decoration: none;
|
||||
border-bottom: 1px dotted var(--amber);
|
||||
}
|
||||
a.link:hover {
|
||||
color: var(--bg);
|
||||
background: var(--amber);
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
|
||||
/* ─── TITLE BLOCK ─── */
|
||||
.title-block {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.ascii-title {
|
||||
color: var(--amber);
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
margin: 1.5rem 0 2.25rem;
|
||||
white-space: pre;
|
||||
overflow-x: auto;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0;
|
||||
text-shadow: 0 0 12px rgba(216,150,74,0.25);
|
||||
}
|
||||
|
||||
.one-liner {
|
||||
color: var(--fg-bright);
|
||||
margin-bottom: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.lang-toggle {
|
||||
font-size: 12px;
|
||||
color: var(--fg-dim);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.lang-toggle a {
|
||||
color: var(--fg-dim);
|
||||
text-decoration: none;
|
||||
border-bottom: 1px dotted var(--fg-faint);
|
||||
padding-bottom: 1px;
|
||||
margin: 0 0.25em;
|
||||
}
|
||||
.lang-toggle a.on {
|
||||
color: var(--amber);
|
||||
border-bottom-color: var(--amber);
|
||||
}
|
||||
.lang-toggle a:hover { color: var(--amber); border-bottom-color: var(--amber); }
|
||||
.lang-toggle .sep { color: var(--fg-faint); }
|
||||
|
||||
.one-liner-sub {
|
||||
color: var(--fg-dim);
|
||||
}
|
||||
|
||||
/* ─── TABLES ─── */
|
||||
.lang-row {
|
||||
display: grid;
|
||||
grid-template-columns: 26ch 1fr 7ch;
|
||||
gap: 1ch;
|
||||
padding: 0.125rem 0;
|
||||
align-items: baseline;
|
||||
transition: background 0.1s;
|
||||
border-bottom: 1px dotted var(--rule);
|
||||
}
|
||||
|
||||
.lang-row:hover { background: var(--bg-alt); }
|
||||
|
||||
.lang-row .file { color: var(--amber); }
|
||||
.lang-row .desc { color: var(--fg); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.lang-row .desc .topics { color: var(--fg-dim); }
|
||||
.lang-row .lines { text-align: right; color: var(--fg-dim); font-variant-numeric: tabular-nums; }
|
||||
|
||||
.dotleader {
|
||||
color: var(--fg-faint);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cat-head {
|
||||
color: var(--fg-bright);
|
||||
margin: 1.25rem 0 0.5rem;
|
||||
padding-bottom: 0.25rem;
|
||||
border-bottom: 1px solid var(--rule);
|
||||
}
|
||||
.cat-head:first-child { margin-top: 0; }
|
||||
|
||||
/* ─── PHASE DIAGRAM ─── */
|
||||
.phase-flow {
|
||||
margin: 1rem 0 1.5rem;
|
||||
color: var(--fg-dim);
|
||||
line-height: 1.4;
|
||||
font-size: 13px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.phase-flow .box { color: var(--amber); }
|
||||
.phase-flow .arrow { color: var(--fg-bright); }
|
||||
|
||||
.phase-list dt {
|
||||
color: var(--fg-bright);
|
||||
margin-top: 0.875rem;
|
||||
}
|
||||
.phase-list dt:first-child { margin-top: 0; }
|
||||
.phase-list dd {
|
||||
color: var(--fg);
|
||||
max-width: 70ch;
|
||||
margin-bottom: 0.125rem;
|
||||
}
|
||||
.phase-list dd.t {
|
||||
color: var(--fg-dim);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ─── SEVERITY LIST ─── */
|
||||
.sev-list {
|
||||
list-style: none;
|
||||
}
|
||||
.sev-list li {
|
||||
display: grid;
|
||||
grid-template-columns: 16ch 1fr;
|
||||
gap: 1ch;
|
||||
padding: 0.25rem 0;
|
||||
border-bottom: 1px dotted var(--rule);
|
||||
align-items: baseline;
|
||||
}
|
||||
.sev-list li:last-child { border-bottom: none; }
|
||||
.sev-list li .label { color: var(--fg-bright); }
|
||||
.sev-list li .desc { color: var(--fg); }
|
||||
.sev-list li .desc .aside { color: var(--fg-dim); }
|
||||
|
||||
/* ─── CODE BLOCKS ─── */
|
||||
.codeblock {
|
||||
background: var(--bg-alt);
|
||||
border-left: 2px solid var(--amber);
|
||||
padding: 0.875rem 1.25rem;
|
||||
margin: 0.875rem 0;
|
||||
color: var(--fg);
|
||||
overflow-x: auto;
|
||||
max-width: 70ch;
|
||||
}
|
||||
|
||||
.codeblock .prompt { color: var(--green); }
|
||||
.codeblock .cmt { color: var(--fg-dim); }
|
||||
.codeblock .cmd { color: var(--amber); }
|
||||
.codeblock .arg { color: var(--fg-bright); }
|
||||
|
||||
.examples {
|
||||
list-style: none;
|
||||
max-width: 70ch;
|
||||
}
|
||||
.examples li {
|
||||
padding: 0.375rem 0;
|
||||
color: var(--fg);
|
||||
}
|
||||
.examples li::before {
|
||||
content: '$ ';
|
||||
color: var(--green);
|
||||
}
|
||||
.examples li .q { color: var(--fg-bright); }
|
||||
.examples li .note {
|
||||
display: block;
|
||||
margin-top: 0.125rem;
|
||||
padding-left: 2ch;
|
||||
color: var(--fg-dim);
|
||||
font-size: 13px;
|
||||
}
|
||||
.examples li .note::before { content: '↳ '; color: var(--fg-faint); }
|
||||
|
||||
/* ─── FILES TREE ─── */
|
||||
.tree {
|
||||
color: var(--fg);
|
||||
line-height: 1.55;
|
||||
}
|
||||
.tree .dir { color: var(--amber); }
|
||||
.tree .file { color: var(--fg); }
|
||||
.tree .cmt { color: var(--fg-dim); }
|
||||
.tree .branch { color: var(--fg-faint); }
|
||||
|
||||
/* ─── STATUS BAR / VIM-LIKE ─── */
|
||||
.statusbar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--amber);
|
||||
color: var(--bg);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.02em;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.statusbar-inner {
|
||||
max-width: 820px;
|
||||
margin: 0 auto;
|
||||
padding: 0.25rem 2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.statusbar-l, .statusbar-r { display: flex; gap: 1.25rem; align-items: center; }
|
||||
.statusbar-l > span:last-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 22ch;
|
||||
}
|
||||
.statusbar kbd {
|
||||
background: var(--bg);
|
||||
color: var(--amber);
|
||||
padding: 1px 5px;
|
||||
border-radius: 2px;
|
||||
font-family: inherit;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ─── CURSOR ─── */
|
||||
.cursor {
|
||||
display: inline-block;
|
||||
width: 0.55em;
|
||||
height: 1em;
|
||||
background: var(--amber);
|
||||
vertical-align: -2px;
|
||||
animation: blink 1.1s steps(1) infinite;
|
||||
margin-left: 1px;
|
||||
}
|
||||
@keyframes blink { 50% { opacity: 0; } }
|
||||
|
||||
/* ─── SEPARATOR ─── */
|
||||
.hr {
|
||||
color: var(--rule);
|
||||
margin: 2rem 0 0;
|
||||
max-width: 70ch;
|
||||
padding-left: 7ch;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* ─── BIB ─── */
|
||||
.bib {
|
||||
max-width: 70ch;
|
||||
}
|
||||
.bib dt {
|
||||
color: var(--fg-bright);
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.bib dt:first-child { margin-top: 0; }
|
||||
.bib dd { color: var(--fg-dim); }
|
||||
|
||||
/* ─── RESPONSIVE ─── */
|
||||
@media (max-width: 720px) {
|
||||
body { font-size: 13px; }
|
||||
main { padding: 2rem 1rem 0; }
|
||||
.band-inner, .statusbar-inner { padding: 0.5rem 1rem; font-size: 11px; }
|
||||
section.body { padding-left: 4ch; }
|
||||
.lang-row { grid-template-columns: 1fr 6ch; gap: 0.5ch; }
|
||||
.lang-row .desc { display: none; }
|
||||
.ascii-title { font-size: 9px; }
|
||||
.sev-list li { grid-template-columns: 14ch 1fr; }
|
||||
.phase-flow { font-size: 10px; }
|
||||
.band-c { display: none; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ═══ TOP BAND (man page header line) ═══ -->
|
||||
<div class="band">
|
||||
<div class="band-inner">
|
||||
<span class="band-l">CODE-REVIEW-SKILL(1)</span>
|
||||
<span class="band-c">User Commands · Edition 2026.01</span>
|
||||
<span class="band-r">CODE-REVIEW-SKILL(1)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main>
|
||||
|
||||
<!-- ═══ TITLE BLOCK ═══ -->
|
||||
<div class="title-block">
|
||||
<pre class="ascii-title"> ___ ___ ___ ___ ___ _____ _____ _____ __ ___ _ _____ _ _
|
||||
/ __/ _ \| \| __| ___ | _ \ __\ \ / /_ _| __\ \ /\ / / __/ __| |/ /_ _| | | |
|
||||
| (_| (_) | |) | _| |___|| / _| \ V / | || _| \ V V /__\__ \ ' < | || |__| |__
|
||||
\___\___/|___/|___| |_|_\___| \_/ |___|___| \_/\_/ |___/_|\_\___|____|____|</pre>
|
||||
|
||||
<div class="one-liner">
|
||||
<span>
|
||||
<span class="dim">$ </span><span class="em">man code-review-skill</span><span class="cursor"></span>
|
||||
</span>
|
||||
<span class="lang-toggle">
|
||||
<span class="dim">LANG=</span><a href="index.html">zh_CN</a><span class="sep"> | </span><a href="index.en.html" class="on">en_US</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="one-liner-sub">
|
||||
v1.0 · awesome-skills · MIT · 20 languages · 16,000+ lines
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ NAME ═══ -->
|
||||
<h2 class="sec">NAME</h2>
|
||||
<section class="body">
|
||||
<p>
|
||||
<span class="em">code-review-skill</span> — A comprehensive, modular code review skill for Claude Code
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- ═══ SYNOPSIS ═══ -->
|
||||
<h2 class="sec">SYNOPSIS</h2>
|
||||
<section class="body">
|
||||
<pre class="pre">
|
||||
<span class="amber">Use code-review-skill to</span> review this PR
|
||||
<span class="amber">Use code-review-skill to</span> review this <<span class="dim">component</span>>
|
||||
<span class="amber">Use code-review-skill for</span> <span class="dim">[</span>security <span class="dim">|</span> performance <span class="dim">|</span> architecture<span class="dim">]</span> review</pre>
|
||||
</section>
|
||||
|
||||
<!-- ═══ DESCRIPTION ═══ -->
|
||||
<h2 class="sec">DESCRIPTION</h2>
|
||||
<section class="body">
|
||||
<p>A production-grade code review skill. It transforms AI-assisted code review from vague suggestions into a structured, consistent, expert-level collaborative process.</p>
|
||||
<p>Core is only <span class="em">~190 lines</span>; the full <span class="em">16,000+ lines</span> of language guides load on demand. Covers <span class="em">20+</span> mainstream languages and frameworks — progressive loading, zero overhead.</p>
|
||||
<p>Every finding carries an explicit severity label. Every review proceeds through four phases: PR context · high-level assessment · line-by-line analysis · summary & decision.</p>
|
||||
</section>
|
||||
|
||||
<!-- ═══ LANGUAGES ═══ -->
|
||||
<h2 class="sec">LANGUAGES</h2>
|
||||
<section class="body">
|
||||
|
||||
<div class="cat-head">┌── frontend ──┘</div>
|
||||
<div class="lang-row"><span class="file">react.md</span><span class="desc">React 19, Hooks, Server Components, TanStack v5 <span class="dotleader">.................</span></span><span class="lines">870</span></div>
|
||||
<div class="lang-row"><span class="file">vue.md</span><span class="desc">Vue 3.5, Composition API, Composables, Watchers <span class="dotleader">.................</span></span><span class="lines">920</span></div>
|
||||
<div class="lang-row"><span class="file">angular.md</span><span class="desc">Angular 17+, Signals, Standalone, Zoneless <span class="dotleader">..........................</span></span><span class="lines">420</span></div>
|
||||
<div class="lang-row"><span class="file">svelte.md</span><span class="desc">Svelte 5, Runes, SvelteKit, SSR/CSR boundaries <span class="dotleader">..................</span></span><span class="lines">1,060</span></div>
|
||||
<div class="lang-row"><span class="file">typescript.md</span><span class="desc">TypeScript strict mode, generics, immutability <span class="dotleader">..................</span></span><span class="lines">540</span></div>
|
||||
<div class="lang-row"><span class="file">css-less-sass.md</span><span class="desc">CSS/Less/Sass variables, responsive, compatibility <span class="dotleader">..............</span></span><span class="lines">660</span></div>
|
||||
|
||||
<div class="cat-head">┌── backend ──┘</div>
|
||||
<div class="lang-row"><span class="file">python.md</span><span class="desc">Python async, typing, pytest, mutable defaults <span class="dotleader">.................</span></span><span class="lines">1,070</span></div>
|
||||
<div class="lang-row"><span class="file">django.md</span><span class="desc">Django/DRF security, N+1, serializers, async views <span class="dotleader">..............</span></span><span class="lines">1,030</span></div>
|
||||
<div class="lang-row"><span class="file">java.md</span><span class="desc">Java 17/21, Spring Boot 3, virtual threads, JPA <span class="dotleader">................</span></span><span class="lines">800</span></div>
|
||||
<div class="lang-row"><span class="file">php.md</span><span class="desc">PHP 8.x, types, PDO, security, Composer <span class="dotleader">...........................</span></span><span class="lines">700</span></div>
|
||||
<div class="lang-row"><span class="file">go.md</span><span class="desc">Goroutines, channels, context, interface design <span class="dotleader">.................</span></span><span class="lines">990</span></div>
|
||||
<div class="lang-row"><span class="file">rust.md</span><span class="desc">Ownership, async/await, unsafe, cancellation safety <span class="dotleader">.............</span></span><span class="lines">840</span></div>
|
||||
<div class="lang-row"><span class="file">csharp.md</span><span class="desc">C# 12 / .NET 8, EF Core, ASP.NET Core, LINQ <span class="dotleader">.....................</span></span><span class="lines">520</span></div>
|
||||
<div class="lang-row"><span class="file">nestjs.md</span><span class="desc">NestJS DI, guards, interceptors, DTO validation <span class="dotleader">.................</span></span><span class="lines">590</span></div>
|
||||
|
||||
<div class="cat-head">┌── mobile / systems ──┘</div>
|
||||
<div class="lang-row"><span class="file">kotlin.md</span><span class="desc">Kotlin/Android coroutines, Compose, Flow, null safety <span class="dotleader">...........</span></span><span class="lines">1,020</span></div>
|
||||
<div class="lang-row"><span class="file">swift.md</span><span class="desc">Swift 5.9+/6, SwiftUI, concurrency, Sendable, optionals <span class="dotleader">..........</span></span><span class="lines">930</span></div>
|
||||
<div class="lang-row"><span class="file">c.md</span><span class="desc">C pointer safety, undefined behavior, resources <span class="dotleader">.................</span></span><span class="lines">210</span></div>
|
||||
<div class="lang-row"><span class="file">cpp.md</span><span class="desc">C++ RAII, Rule of 0/3/5, move semantics, noexcept <span class="dotleader">...............</span></span><span class="lines">300</span></div>
|
||||
<div class="lang-row"><span class="file">qt.md</span><span class="desc">Qt object model, signals/slots, GUI performance <span class="dotleader">.................</span></span><span class="lines">190</span></div>
|
||||
|
||||
<div class="cat-head">┌── cross-cutting ──┘</div>
|
||||
<div class="lang-row"><span class="file">architecture-review-guide.md</span><span class="desc">SOLID, anti-patterns, coupling <span class="dotleader">...</span></span><span class="lines">470</span></div>
|
||||
<div class="lang-row"><span class="file">performance-review-guide.md</span><span class="desc">Web Vitals, N+1, complexity <span class="dotleader">.......</span></span><span class="lines">850</span></div>
|
||||
<div class="lang-row"><span class="file">code-quality-universal.md</span><span class="desc">TOCTOU, leaky abstractions, sprawl <span class="dotleader">....</span></span><span class="lines">320</span></div>
|
||||
<div class="lang-row"><span class="file">security-review-guide.md</span><span class="desc">Injection, XSS, secrets, all langs <span class="dotleader">.....</span></span><span class="lines">—</span></div>
|
||||
</section>
|
||||
|
||||
<!-- ═══ PHASES ═══ -->
|
||||
<h2 class="sec">PHASES</h2>
|
||||
<section class="body">
|
||||
|
||||
<pre class="phase-flow"> <span class="box">┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐</span>
|
||||
<span class="box">│ context │</span> <span class="arrow">─▶</span> <span class="box">│ high level │</span> <span class="arrow">─▶</span> <span class="box">│ line by line│</span> <span class="arrow">─▶</span> <span class="box">│ decide │</span>
|
||||
<span class="box">│ 2-3m │</span> <span class="box">│ 5-10m │</span> <span class="box">│ 10-20m │</span> <span class="box">│ 2-3m │</span>
|
||||
<span class="box">└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘</span></pre>
|
||||
|
||||
<dl class="phase-list" style="margin-top:1.5rem;">
|
||||
<dt>1. context gathering <span class="dim">— 2-3 min</span></dt>
|
||||
<dd>Read the PR description and linked issues, assess scope, check CI status, understand the business intent.</dd>
|
||||
<dt>2. high-level review <span class="dim">— 5-10 min</span></dt>
|
||||
<dd>Evaluate architectural fit, performance impact, file organization, test strategy. See the whole first.</dd>
|
||||
<dt>3. line-by-line analysis <span class="dim">— 10-20 min</span></dt>
|
||||
<dd>Logic correctness · security · performance · maintainability · edge cases. One by one.</dd>
|
||||
<dt>4. summary & decision <span class="dim">— 2-3 min</span></dt>
|
||||
<dd>Summarize findings, name what was done well, deliver approve / comment / request-changes.</dd>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<!-- ═══ SEVERITY ═══ -->
|
||||
<h2 class="sec">SEVERITY</h2>
|
||||
<section class="body">
|
||||
<ul class="sev-list">
|
||||
<li>
|
||||
<span class="label"><span class="red">●</span> [blocking]</span>
|
||||
<span class="desc">must fix <span class="aside">— resolve before merge; security / correctness / serious logic</span></span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="label"><span style="color:#d68a3d;">●</span> [important]</span>
|
||||
<span class="desc">should fix <span class="aside">— strongly recommended; discuss if you disagree</span></span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="label"><span style="color:#c7a648;">●</span> [nit]</span>
|
||||
<span class="desc">nice to have <span class="aside">— style or preference; non-blocking</span></span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="label"><span class="blue">●</span> [suggestion]</span>
|
||||
<span class="desc">alternative <span class="aside">— worth considering; author decides</span></span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="label"><span style="color:#9078b8;">●</span> [learning]</span>
|
||||
<span class="desc">educational <span class="aside">— no action needed; share knowledge</span></span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="label"><span class="green">●</span> [praise]</span>
|
||||
<span class="desc">good work <span class="aside">— say it out loud when you see it</span></span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- ═══ INSTALLATION ═══ -->
|
||||
<h2 class="sec">INSTALLATION</h2>
|
||||
<section class="body">
|
||||
|
||||
<p>Clone into the Claude Code skills directory. Two commands.</p>
|
||||
|
||||
<pre class="codeblock"><span class="cmt"># macOS / Linux</span>
|
||||
<span class="prompt">$</span> <span class="cmd">git clone</span> <span class="arg">https://github.com/awesome-skills/code-review-skill.git</span> \
|
||||
~/.claude/skills/code-review-skill
|
||||
|
||||
<span class="cmt"># Windows PowerShell</span>
|
||||
<span class="prompt">PS></span> <span class="cmd">git clone</span> <span class="arg">https://github.com/awesome-skills/code-review-skill.git</span> `
|
||||
"$env:USERPROFILE\.claude\skills\code-review-skill"</pre>
|
||||
</section>
|
||||
|
||||
<!-- ═══ EXAMPLES ═══ -->
|
||||
<h2 class="sec">EXAMPLES</h2>
|
||||
<section class="body">
|
||||
<ul class="examples">
|
||||
<li>
|
||||
<span class="q">Use code-review-skill to review this PR</span>
|
||||
<span class="note">runs the full four-phase review</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="q">Review this React component</span>
|
||||
<span class="note">loads react.md · checks Hooks · Server Components</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="q">Security review of this Go service</span>
|
||||
<span class="note">loads go.md + security-review-guide.md together</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="q">Architecture review</span>
|
||||
<span class="note">loads the architecture guide · SOLID · anti-patterns · coupling</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- ═══ FILES ═══ -->
|
||||
<h2 class="sec">FILES</h2>
|
||||
<section class="body">
|
||||
<pre class="tree">
|
||||
<span class="dir">~/.claude/skills/code-review-skill/</span>
|
||||
<span class="branch">├──</span> <span class="file">SKILL.md</span> <span class="cmt"># core, loaded on activation (~190 lines)</span>
|
||||
<span class="branch">├──</span> <span class="file">README.md</span>
|
||||
<span class="branch">├──</span> <span class="file">LICENSE</span> <span class="cmt"># MIT</span>
|
||||
<span class="branch">├──</span> <span class="dir">reference/</span> <span class="cmt"># on-demand language guides</span>
|
||||
<span class="branch">│ ├──</span> <span class="file">react.md</span> <span class="file">vue.md</span> <span class="file">angular.md</span> ...
|
||||
<span class="branch">│ └──</span> <span class="file">architecture-review-guide.md</span> ...
|
||||
<span class="branch">├──</span> <span class="dir">assets/</span>
|
||||
<span class="branch">│ ├──</span> <span class="file">review-checklist.md</span> <span class="cmt"># quick reference</span>
|
||||
<span class="branch">│ └──</span> <span class="file">pr-review-template.md</span> <span class="cmt"># PR comment template</span>
|
||||
<span class="branch">└──</span> <span class="dir">scripts/</span>
|
||||
<span class="branch">└──</span> <span class="file">pr-analyzer.py</span> <span class="cmt"># PR complexity analyzer</span></pre>
|
||||
</section>
|
||||
|
||||
<!-- ═══ SEE ALSO ═══ -->
|
||||
<h2 class="sec">SEE ALSO</h2>
|
||||
<section class="body">
|
||||
<p>
|
||||
<a class="link" href="https://claude.ai/code" target="_blank">claude-code(1)</a>,
|
||||
<a class="link" href="https://github.com/awesome-skills/code-review-skill" target="_blank">github / awesome-skills</a>,
|
||||
<a class="link" href="https://github.com/awesome-skills/code-review-skill/blob/main/CONTRIBUTING.md" target="_blank">CONTRIBUTING(7)</a>,
|
||||
<a class="link" href="https://github.com/awesome-skills/code-review-skill/blob/main/assets/review-checklist.md" target="_blank">review-checklist(7)</a>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- ═══ AUTHORS ═══ -->
|
||||
<h2 class="sec">AUTHORS</h2>
|
||||
<section class="body">
|
||||
<dl class="bib">
|
||||
<dt>awesome-skills</dt>
|
||||
<dd>maintainer, primary author</dd>
|
||||
<dt>contributors</dt>
|
||||
<dd>see <a class="link" href="https://github.com/awesome-skills/code-review-skill/graphs/contributors" target="_blank">graphs/contributors</a></dd>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<!-- ═══ COPYRIGHT ═══ -->
|
||||
<h2 class="sec">COPYRIGHT</h2>
|
||||
<section class="body">
|
||||
<p>
|
||||
<span class="dim">Copyright (c) 2025 awesome-skills.</span><br>
|
||||
Released under the MIT License.<br>
|
||||
<span class="dim">This is free software: you are free to change and redistribute it.</span><br>
|
||||
<span class="dim">There is NO WARRANTY, to the extent permitted by law.</span>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<div style="height: 4rem;"></div>
|
||||
|
||||
<!-- ═══ END-OF-PAGE BAND ═══ -->
|
||||
<div style="border-top:1px solid var(--rule); margin-top:2rem; padding:0.625rem 0;">
|
||||
<div style="display:flex; justify-content:space-between; color:var(--fg-dim); font-size:12px; white-space:nowrap; gap:1rem;">
|
||||
<span class="band-l" style="color:var(--fg-bright);">CODE-REVIEW-SKILL(1)</span>
|
||||
<span style="color:var(--fg-dim);">awesome-skills</span>
|
||||
<span class="band-r" style="color:var(--fg-bright);">CODE-REVIEW-SKILL(1)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- ═══ VIM-LIKE STATUS BAR ═══ -->
|
||||
<div class="statusbar">
|
||||
<div class="statusbar-inner">
|
||||
<div class="statusbar-l">
|
||||
<span>-- NORMAL --</span>
|
||||
<span>code-review-skill.1</span>
|
||||
</div>
|
||||
<div class="statusbar-r">
|
||||
<span><kbd>g</kbd> top</span>
|
||||
<span><kbd>G</kbd> end</span>
|
||||
<span><kbd>q</kbd> quit</span>
|
||||
<span id="pos">1,1</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Vim-like keyboard nav for the man-page vibe
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
||||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
||||
|
||||
if (e.key === 'g') {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
} else if (e.key === 'G') {
|
||||
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
|
||||
} else if (e.key === 'j') {
|
||||
window.scrollBy({ top: 60, behavior: 'smooth' });
|
||||
} else if (e.key === 'k') {
|
||||
window.scrollBy({ top: -60, behavior: 'smooth' });
|
||||
} else if (e.key === 'q') {
|
||||
const ok = confirm('Quit man page?');
|
||||
if (ok) window.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Update line/col-like indicator from scroll position
|
||||
const posEl = document.getElementById('pos');
|
||||
function updatePos() {
|
||||
const pct = Math.round((window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100) || 0;
|
||||
const line = Math.max(1, Math.round((window.scrollY / 20)));
|
||||
posEl.textContent = line + ',1 ' + (pct >= 99 ? 'Bot' : pct <= 1 ? 'Top' : pct + '%');
|
||||
}
|
||||
updatePos();
|
||||
window.addEventListener('scroll', updatePos, { passive: true });
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Executable
+702
@@ -0,0 +1,702 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>code-review-skill(1) — User Commands</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
:root {
|
||||
--bg: #14110d;
|
||||
--bg-alt: #1a1611;
|
||||
--fg: #c4b596;
|
||||
--fg-bright:#e8d5a8;
|
||||
--fg-dim: #7a6f56;
|
||||
--fg-faint: #4a4334;
|
||||
--amber: #d8964a;
|
||||
--amber-2: #e8a455;
|
||||
--red: #d56350;
|
||||
--green: #8fae5a;
|
||||
--blue: #6b94c4;
|
||||
--rule: #2a2520;
|
||||
}
|
||||
|
||||
html { background: var(--bg); }
|
||||
|
||||
body {
|
||||
font-family: 'IBM Plex Mono', ui-monospace, 'SF Mono', Menlo, monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.65;
|
||||
color: var(--fg);
|
||||
background: var(--bg);
|
||||
min-height: 100vh;
|
||||
padding: 0 0 4rem;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* faint scanline-free phosphor texture — very subtle */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
background:
|
||||
radial-gradient(ellipse at 50% 0%, rgba(216,150,74,0.04) 0%, transparent 60%);
|
||||
}
|
||||
|
||||
/* ─── HEADER / FOOTER BAND ─── */
|
||||
.band {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--bg);
|
||||
border-bottom: 1px solid var(--rule);
|
||||
z-index: 10;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.band-inner {
|
||||
max-width: 820px;
|
||||
margin: 0 auto;
|
||||
padding: 0.625rem 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
color: var(--fg-dim);
|
||||
}
|
||||
|
||||
.band-l, .band-r {
|
||||
color: var(--fg-bright);
|
||||
letter-spacing: 0.04em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.band-c { color: var(--fg-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
|
||||
.band a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px dotted var(--fg-faint);
|
||||
}
|
||||
.band a:hover { color: var(--amber); border-bottom-color: var(--amber); }
|
||||
|
||||
/* ─── PAGE ─── */
|
||||
main {
|
||||
max-width: 820px;
|
||||
margin: 0 auto;
|
||||
padding: 3rem 2rem 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
pre, .pre {
|
||||
font-family: inherit;
|
||||
white-space: pre;
|
||||
color: inherit;
|
||||
background: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ─── SECTIONS ─── */
|
||||
h2.sec {
|
||||
color: var(--fg-bright);
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.04em;
|
||||
margin: 2.75rem 0 0.875rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
h2.sec::before { content: ''; }
|
||||
|
||||
section.body {
|
||||
padding-left: 7ch;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
section.body p {
|
||||
margin-bottom: 0.875rem;
|
||||
max-width: 70ch;
|
||||
}
|
||||
section.body p:last-child { margin-bottom: 0; }
|
||||
|
||||
.em { color: var(--fg-bright); }
|
||||
.dim { color: var(--fg-dim); }
|
||||
.faint { color: var(--fg-faint); }
|
||||
.amber { color: var(--amber); }
|
||||
.red { color: var(--red); }
|
||||
.green { color: var(--green); }
|
||||
.blue { color: var(--blue); }
|
||||
|
||||
a.link {
|
||||
color: var(--amber);
|
||||
text-decoration: none;
|
||||
border-bottom: 1px dotted var(--amber);
|
||||
}
|
||||
a.link:hover {
|
||||
color: var(--bg);
|
||||
background: var(--amber);
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
|
||||
/* ─── TITLE BLOCK ─── */
|
||||
.title-block {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.ascii-title {
|
||||
color: var(--amber);
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
margin: 1.5rem 0 2.25rem;
|
||||
white-space: pre;
|
||||
overflow-x: auto;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0;
|
||||
text-shadow: 0 0 12px rgba(216,150,74,0.25);
|
||||
}
|
||||
|
||||
.one-liner {
|
||||
color: var(--fg-bright);
|
||||
margin-bottom: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.lang-toggle {
|
||||
font-size: 12px;
|
||||
color: var(--fg-dim);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.lang-toggle a {
|
||||
color: var(--fg-dim);
|
||||
text-decoration: none;
|
||||
border-bottom: 1px dotted var(--fg-faint);
|
||||
padding-bottom: 1px;
|
||||
margin: 0 0.25em;
|
||||
}
|
||||
.lang-toggle a.on {
|
||||
color: var(--amber);
|
||||
border-bottom-color: var(--amber);
|
||||
}
|
||||
.lang-toggle a:hover { color: var(--amber); border-bottom-color: var(--amber); }
|
||||
.lang-toggle .sep { color: var(--fg-faint); }
|
||||
|
||||
.one-liner-sub {
|
||||
color: var(--fg-dim);
|
||||
}
|
||||
|
||||
/* ─── TABLES ─── */
|
||||
.lang-row {
|
||||
display: grid;
|
||||
grid-template-columns: 26ch 1fr 7ch;
|
||||
gap: 1ch;
|
||||
padding: 0.125rem 0;
|
||||
align-items: baseline;
|
||||
transition: background 0.1s;
|
||||
border-bottom: 1px dotted var(--rule);
|
||||
}
|
||||
|
||||
.lang-row:hover { background: var(--bg-alt); }
|
||||
|
||||
.lang-row .file { color: var(--amber); }
|
||||
.lang-row .desc { color: var(--fg); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.lang-row .desc .topics { color: var(--fg-dim); }
|
||||
.lang-row .lines { text-align: right; color: var(--fg-dim); font-variant-numeric: tabular-nums; }
|
||||
|
||||
.dotleader {
|
||||
color: var(--fg-faint);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cat-head {
|
||||
color: var(--fg-bright);
|
||||
margin: 1.25rem 0 0.5rem;
|
||||
padding-bottom: 0.25rem;
|
||||
border-bottom: 1px solid var(--rule);
|
||||
}
|
||||
.cat-head:first-child { margin-top: 0; }
|
||||
|
||||
/* ─── PHASE DIAGRAM ─── */
|
||||
.phase-flow {
|
||||
margin: 1rem 0 1.5rem;
|
||||
color: var(--fg-dim);
|
||||
line-height: 1.4;
|
||||
font-size: 13px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.phase-flow .box { color: var(--amber); }
|
||||
.phase-flow .arrow { color: var(--fg-bright); }
|
||||
|
||||
.phase-list dt {
|
||||
color: var(--fg-bright);
|
||||
margin-top: 0.875rem;
|
||||
}
|
||||
.phase-list dt:first-child { margin-top: 0; }
|
||||
.phase-list dd {
|
||||
color: var(--fg);
|
||||
max-width: 70ch;
|
||||
margin-bottom: 0.125rem;
|
||||
}
|
||||
.phase-list dd.t {
|
||||
color: var(--fg-dim);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ─── SEVERITY LIST ─── */
|
||||
.sev-list {
|
||||
list-style: none;
|
||||
}
|
||||
.sev-list li {
|
||||
display: grid;
|
||||
grid-template-columns: 16ch 1fr;
|
||||
gap: 1ch;
|
||||
padding: 0.25rem 0;
|
||||
border-bottom: 1px dotted var(--rule);
|
||||
align-items: baseline;
|
||||
}
|
||||
.sev-list li:last-child { border-bottom: none; }
|
||||
.sev-list li .label { color: var(--fg-bright); }
|
||||
.sev-list li .desc { color: var(--fg); }
|
||||
.sev-list li .desc .aside { color: var(--fg-dim); }
|
||||
|
||||
/* ─── CODE BLOCKS ─── */
|
||||
.codeblock {
|
||||
background: var(--bg-alt);
|
||||
border-left: 2px solid var(--amber);
|
||||
padding: 0.875rem 1.25rem;
|
||||
margin: 0.875rem 0;
|
||||
color: var(--fg);
|
||||
overflow-x: auto;
|
||||
max-width: 70ch;
|
||||
}
|
||||
|
||||
.codeblock .prompt { color: var(--green); }
|
||||
.codeblock .cmt { color: var(--fg-dim); }
|
||||
.codeblock .cmd { color: var(--amber); }
|
||||
.codeblock .arg { color: var(--fg-bright); }
|
||||
|
||||
.examples {
|
||||
list-style: none;
|
||||
max-width: 70ch;
|
||||
}
|
||||
.examples li {
|
||||
padding: 0.375rem 0;
|
||||
color: var(--fg);
|
||||
}
|
||||
.examples li::before {
|
||||
content: '$ ';
|
||||
color: var(--green);
|
||||
}
|
||||
.examples li .q { color: var(--fg-bright); }
|
||||
.examples li .note {
|
||||
display: block;
|
||||
margin-top: 0.125rem;
|
||||
padding-left: 2ch;
|
||||
color: var(--fg-dim);
|
||||
font-size: 13px;
|
||||
}
|
||||
.examples li .note::before { content: '↳ '; color: var(--fg-faint); }
|
||||
|
||||
/* ─── FILES TREE ─── */
|
||||
.tree {
|
||||
color: var(--fg);
|
||||
line-height: 1.55;
|
||||
}
|
||||
.tree .dir { color: var(--amber); }
|
||||
.tree .file { color: var(--fg); }
|
||||
.tree .cmt { color: var(--fg-dim); }
|
||||
.tree .branch { color: var(--fg-faint); }
|
||||
|
||||
/* ─── STATUS BAR / VIM-LIKE ─── */
|
||||
.statusbar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--amber);
|
||||
color: var(--bg);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.02em;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.statusbar-inner {
|
||||
max-width: 820px;
|
||||
margin: 0 auto;
|
||||
padding: 0.25rem 2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.statusbar-l, .statusbar-r { display: flex; gap: 1.25rem; align-items: center; }
|
||||
.statusbar-l > span:last-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 22ch;
|
||||
}
|
||||
.statusbar kbd {
|
||||
background: var(--bg);
|
||||
color: var(--amber);
|
||||
padding: 1px 5px;
|
||||
border-radius: 2px;
|
||||
font-family: inherit;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ─── CURSOR ─── */
|
||||
.cursor {
|
||||
display: inline-block;
|
||||
width: 0.55em;
|
||||
height: 1em;
|
||||
background: var(--amber);
|
||||
vertical-align: -2px;
|
||||
animation: blink 1.1s steps(1) infinite;
|
||||
margin-left: 1px;
|
||||
}
|
||||
@keyframes blink { 50% { opacity: 0; } }
|
||||
|
||||
/* ─── SEPARATOR ─── */
|
||||
.hr {
|
||||
color: var(--rule);
|
||||
margin: 2rem 0 0;
|
||||
max-width: 70ch;
|
||||
padding-left: 7ch;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* ─── BIB ─── */
|
||||
.bib {
|
||||
max-width: 70ch;
|
||||
}
|
||||
.bib dt {
|
||||
color: var(--fg-bright);
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.bib dt:first-child { margin-top: 0; }
|
||||
.bib dd { color: var(--fg-dim); }
|
||||
|
||||
/* ─── RESPONSIVE ─── */
|
||||
@media (max-width: 720px) {
|
||||
body { font-size: 13px; }
|
||||
main { padding: 2rem 1rem 0; }
|
||||
.band-inner, .statusbar-inner { padding: 0.5rem 1rem; font-size: 11px; }
|
||||
section.body { padding-left: 4ch; }
|
||||
.lang-row { grid-template-columns: 1fr 6ch; gap: 0.5ch; }
|
||||
.lang-row .desc { display: none; }
|
||||
.ascii-title { font-size: 9px; }
|
||||
.sev-list li { grid-template-columns: 14ch 1fr; }
|
||||
.phase-flow { font-size: 10px; }
|
||||
.band-c { display: none; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ═══ TOP BAND (man page header line) ═══ -->
|
||||
<div class="band">
|
||||
<div class="band-inner">
|
||||
<span class="band-l">CODE-REVIEW-SKILL(1)</span>
|
||||
<span class="band-c">User Commands · Edition 2026.01</span>
|
||||
<span class="band-r">CODE-REVIEW-SKILL(1)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main>
|
||||
|
||||
<!-- ═══ TITLE BLOCK ═══ -->
|
||||
<div class="title-block">
|
||||
<pre class="ascii-title"> ___ ___ ___ ___ ___ _____ _____ _____ __ ___ _ _____ _ _
|
||||
/ __/ _ \| \| __| ___ | _ \ __\ \ / /_ _| __\ \ /\ / / __/ __| |/ /_ _| | | |
|
||||
| (_| (_) | |) | _| |___|| / _| \ V / | || _| \ V V /__\__ \ ' < | || |__| |__
|
||||
\___\___/|___/|___| |_|_\___| \_/ |___|___| \_/\_/ |___/_|\_\___|____|____|</pre>
|
||||
|
||||
<div class="one-liner">
|
||||
<span>
|
||||
<span class="dim">$ </span><span class="em">man code-review-skill</span><span class="cursor"></span>
|
||||
</span>
|
||||
<span class="lang-toggle">
|
||||
<span class="dim">LANG=</span><a href="index.html" class="on">zh_CN</a><span class="sep"> | </span><a href="index.en.html">en_US</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="one-liner-sub">
|
||||
v1.0 · awesome-skills · MIT · 20 languages · 16,000+ lines
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ NAME ═══ -->
|
||||
<h2 class="sec">NAME</h2>
|
||||
<section class="body">
|
||||
<p>
|
||||
<span class="em">code-review-skill</span> — 面向 Claude Code 的全面、模块化代码审查技能
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- ═══ SYNOPSIS ═══ -->
|
||||
<h2 class="sec">SYNOPSIS</h2>
|
||||
<section class="body">
|
||||
<pre class="pre">
|
||||
<span class="amber">Use code-review-skill to</span> review this PR
|
||||
<span class="amber">Use code-review-skill to</span> review this <<span class="dim">component</span>>
|
||||
<span class="amber">Use code-review-skill for</span> <span class="dim">[</span>security <span class="dim">|</span> performance <span class="dim">|</span> architecture<span class="dim">]</span> review</pre>
|
||||
</section>
|
||||
|
||||
<!-- ═══ DESCRIPTION ═══ -->
|
||||
<h2 class="sec">DESCRIPTION</h2>
|
||||
<section class="body">
|
||||
<p>一份生产级的代码审查技能。它把 AI 辅助的代码审查从模糊建议提升为结构化、一致、专业级的协作流程。</p>
|
||||
<p>核心仅约 <span class="em">190 行</span>,按需调阅共计 <span class="em">16,000+ 行</span> 的语言指南。覆盖 <span class="em">20+ 种</span> 主流语言与框架——按需加载,零冗余。</p>
|
||||
<p>每一条审查意见都带有明确的严重性标记。每一次审查都按四个阶段推进:从 PR 上下文 · 高层级评估 · 逐行分析 · 总结决策。</p>
|
||||
</section>
|
||||
|
||||
<!-- ═══ LANGUAGES ═══ -->
|
||||
<h2 class="sec">LANGUAGES</h2>
|
||||
<section class="body">
|
||||
|
||||
<div class="cat-head">┌── frontend ──┘</div>
|
||||
<div class="lang-row"><span class="file">react.md</span><span class="desc">React 19, Hooks, Server Components, TanStack v5 <span class="dotleader">.................</span></span><span class="lines">870</span></div>
|
||||
<div class="lang-row"><span class="file">vue.md</span><span class="desc">Vue 3.5, Composition API, Composables, Watchers <span class="dotleader">.................</span></span><span class="lines">920</span></div>
|
||||
<div class="lang-row"><span class="file">angular.md</span><span class="desc">Angular 17+, Signals, Standalone, Zoneless <span class="dotleader">..........................</span></span><span class="lines">420</span></div>
|
||||
<div class="lang-row"><span class="file">svelte.md</span><span class="desc">Svelte 5, Runes, SvelteKit, SSR/CSR boundaries <span class="dotleader">..................</span></span><span class="lines">1,060</span></div>
|
||||
<div class="lang-row"><span class="file">typescript.md</span><span class="desc">TypeScript strict mode, generics, immutability <span class="dotleader">..................</span></span><span class="lines">540</span></div>
|
||||
<div class="lang-row"><span class="file">css-less-sass.md</span><span class="desc">CSS/Less/Sass variables, responsive, compatibility <span class="dotleader">..............</span></span><span class="lines">660</span></div>
|
||||
|
||||
<div class="cat-head">┌── backend ──┘</div>
|
||||
<div class="lang-row"><span class="file">python.md</span><span class="desc">Python async, typing, pytest, mutable defaults <span class="dotleader">.................</span></span><span class="lines">1,070</span></div>
|
||||
<div class="lang-row"><span class="file">django.md</span><span class="desc">Django/DRF security, N+1, serializers, async views <span class="dotleader">..............</span></span><span class="lines">1,030</span></div>
|
||||
<div class="lang-row"><span class="file">java.md</span><span class="desc">Java 17/21, Spring Boot 3, virtual threads, JPA <span class="dotleader">................</span></span><span class="lines">800</span></div>
|
||||
<div class="lang-row"><span class="file">php.md</span><span class="desc">PHP 8.x, types, PDO, security, Composer <span class="dotleader">...........................</span></span><span class="lines">700</span></div>
|
||||
<div class="lang-row"><span class="file">go.md</span><span class="desc">Goroutines, channels, context, interface design <span class="dotleader">.................</span></span><span class="lines">990</span></div>
|
||||
<div class="lang-row"><span class="file">rust.md</span><span class="desc">Ownership, async/await, unsafe, cancellation safety <span class="dotleader">.............</span></span><span class="lines">840</span></div>
|
||||
<div class="lang-row"><span class="file">csharp.md</span><span class="desc">C# 12 / .NET 8, EF Core, ASP.NET Core, LINQ <span class="dotleader">.....................</span></span><span class="lines">520</span></div>
|
||||
<div class="lang-row"><span class="file">nestjs.md</span><span class="desc">NestJS DI, guards, interceptors, DTO validation <span class="dotleader">.................</span></span><span class="lines">590</span></div>
|
||||
|
||||
<div class="cat-head">┌── mobile / systems ──┘</div>
|
||||
<div class="lang-row"><span class="file">kotlin.md</span><span class="desc">Kotlin/Android coroutines, Compose, Flow, null safety <span class="dotleader">...........</span></span><span class="lines">1,020</span></div>
|
||||
<div class="lang-row"><span class="file">swift.md</span><span class="desc">Swift 5.9+/6, SwiftUI, concurrency, Sendable, optionals <span class="dotleader">..........</span></span><span class="lines">930</span></div>
|
||||
<div class="lang-row"><span class="file">c.md</span><span class="desc">C pointer safety, undefined behavior, resources <span class="dotleader">.................</span></span><span class="lines">210</span></div>
|
||||
<div class="lang-row"><span class="file">cpp.md</span><span class="desc">C++ RAII, Rule of 0/3/5, move semantics, noexcept <span class="dotleader">...............</span></span><span class="lines">300</span></div>
|
||||
<div class="lang-row"><span class="file">qt.md</span><span class="desc">Qt object model, signals/slots, GUI performance <span class="dotleader">.................</span></span><span class="lines">190</span></div>
|
||||
|
||||
<div class="cat-head">┌── cross-cutting ──┘</div>
|
||||
<div class="lang-row"><span class="file">architecture-review-guide.md</span><span class="desc">SOLID, anti-patterns, coupling <span class="dotleader">...</span></span><span class="lines">470</span></div>
|
||||
<div class="lang-row"><span class="file">performance-review-guide.md</span><span class="desc">Web Vitals, N+1, complexity <span class="dotleader">.......</span></span><span class="lines">850</span></div>
|
||||
<div class="lang-row"><span class="file">code-quality-universal.md</span><span class="desc">TOCTOU, leaky abstractions, sprawl <span class="dotleader">....</span></span><span class="lines">320</span></div>
|
||||
<div class="lang-row"><span class="file">security-review-guide.md</span><span class="desc">Injection, XSS, secrets, all langs <span class="dotleader">.....</span></span><span class="lines">—</span></div>
|
||||
</section>
|
||||
|
||||
<!-- ═══ PHASES ═══ -->
|
||||
<h2 class="sec">PHASES</h2>
|
||||
<section class="body">
|
||||
|
||||
<pre class="phase-flow"> <span class="box">┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐</span>
|
||||
<span class="box">│ context │</span> <span class="arrow">─▶</span> <span class="box">│ high level │</span> <span class="arrow">─▶</span> <span class="box">│ line by line│</span> <span class="arrow">─▶</span> <span class="box">│ decide │</span>
|
||||
<span class="box">│ 2-3m │</span> <span class="box">│ 5-10m │</span> <span class="box">│ 10-20m │</span> <span class="box">│ 2-3m │</span>
|
||||
<span class="box">└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘</span></pre>
|
||||
|
||||
<dl class="phase-list" style="margin-top:1.5rem;">
|
||||
<dt>1. context gathering <span class="dim">— 2-3 min</span></dt>
|
||||
<dd>读 PR 描述与关联 issue,评估规模,检查 CI 状态,理解业务需求。</dd>
|
||||
<dt>2. high-level review <span class="dim">— 5-10 min</span></dt>
|
||||
<dd>评估架构合理性、性能影响面、文件组织、测试策略。先看全局。</dd>
|
||||
<dt>3. line-by-line analysis <span class="dim">— 10-20 min</span></dt>
|
||||
<dd>逻辑正确性 · 安全 · 性能 · 可维护性 · 边界情况。一一过目。</dd>
|
||||
<dt>4. summary & decision <span class="dim">— 2-3 min</span></dt>
|
||||
<dd>汇总问题,表扬亮点,给出 approve / comment / request-changes。</dd>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<!-- ═══ SEVERITY ═══ -->
|
||||
<h2 class="sec">SEVERITY</h2>
|
||||
<section class="body">
|
||||
<ul class="sev-list">
|
||||
<li>
|
||||
<span class="label"><span class="red">●</span> [blocking]</span>
|
||||
<span class="desc">必须修复 <span class="aside">— 合并前解决;安全漏洞 / 数据正确性 / 严重逻辑</span></span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="label"><span style="color:#d68a3d;">●</span> [important]</span>
|
||||
<span class="desc">应当修复 <span class="aside">— 强烈建议;有分歧应讨论</span></span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="label"><span style="color:#c7a648;">●</span> [nit]</span>
|
||||
<span class="desc">细节建议 <span class="aside">— 风格或偏好,不阻塞合并</span></span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="label"><span class="blue">●</span> [suggestion]</span>
|
||||
<span class="desc">可选优化 <span class="aside">— 替代方案,由作者决定</span></span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="label"><span style="color:#9078b8;">●</span> [learning]</span>
|
||||
<span class="desc">知识分享 <span class="aside">— 教育性说明,无需采取行动</span></span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="label"><span class="green">●</span> [praise]</span>
|
||||
<span class="desc">表扬肯定 <span class="aside">— 看到好代码就说出来</span></span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- ═══ INSTALLATION ═══ -->
|
||||
<h2 class="sec">INSTALLATION</h2>
|
||||
<section class="body">
|
||||
|
||||
<p>克隆到 Claude Code skills 目录。两条命令即可。</p>
|
||||
|
||||
<pre class="codeblock"><span class="cmt"># macOS / Linux</span>
|
||||
<span class="prompt">$</span> <span class="cmd">git clone</span> <span class="arg">https://github.com/awesome-skills/code-review-skill.git</span> \
|
||||
~/.claude/skills/code-review-skill
|
||||
|
||||
<span class="cmt"># Windows PowerShell</span>
|
||||
<span class="prompt">PS></span> <span class="cmd">git clone</span> <span class="arg">https://github.com/awesome-skills/code-review-skill.git</span> `
|
||||
"$env:USERPROFILE\.claude\skills\code-review-skill"</pre>
|
||||
</section>
|
||||
|
||||
<!-- ═══ EXAMPLES ═══ -->
|
||||
<h2 class="sec">EXAMPLES</h2>
|
||||
<section class="body">
|
||||
<ul class="examples">
|
||||
<li>
|
||||
<span class="q">Use code-review-skill to review this PR</span>
|
||||
<span class="note">激活完整四阶段流程</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="q">Review this React component</span>
|
||||
<span class="note">加载 react.md · 检查 Hooks · Server Components</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="q">Security review of this Go service</span>
|
||||
<span class="note">同时加载 go.md + security-review-guide.md</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="q">Architecture review</span>
|
||||
<span class="note">加载架构指南 · SOLID · 反模式 · 耦合度</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- ═══ FILES ═══ -->
|
||||
<h2 class="sec">FILES</h2>
|
||||
<section class="body">
|
||||
<pre class="tree">
|
||||
<span class="dir">~/.claude/skills/code-review-skill/</span>
|
||||
<span class="branch">├──</span> <span class="file">SKILL.md</span> <span class="cmt"># 核心,激活时加载 (~190 行)</span>
|
||||
<span class="branch">├──</span> <span class="file">README.md</span>
|
||||
<span class="branch">├──</span> <span class="file">LICENSE</span> <span class="cmt"># MIT</span>
|
||||
<span class="branch">├──</span> <span class="dir">reference/</span> <span class="cmt"># 按需加载的语言指南</span>
|
||||
<span class="branch">│ ├──</span> <span class="file">react.md</span> <span class="file">vue.md</span> <span class="file">angular.md</span> ...
|
||||
<span class="branch">│ └──</span> <span class="file">architecture-review-guide.md</span> ...
|
||||
<span class="branch">├──</span> <span class="dir">assets/</span>
|
||||
<span class="branch">│ ├──</span> <span class="file">review-checklist.md</span> <span class="cmt"># 快速参考</span>
|
||||
<span class="branch">│ └──</span> <span class="file">pr-review-template.md</span> <span class="cmt"># 评论模板</span>
|
||||
<span class="branch">└──</span> <span class="dir">scripts/</span>
|
||||
<span class="branch">└──</span> <span class="file">pr-analyzer.py</span> <span class="cmt"># PR 复杂度分析</span></pre>
|
||||
</section>
|
||||
|
||||
<!-- ═══ SEE ALSO ═══ -->
|
||||
<h2 class="sec">SEE ALSO</h2>
|
||||
<section class="body">
|
||||
<p>
|
||||
<a class="link" href="https://claude.ai/code" target="_blank">claude-code(1)</a>,
|
||||
<a class="link" href="https://github.com/awesome-skills/code-review-skill" target="_blank">github / awesome-skills</a>,
|
||||
<a class="link" href="https://github.com/awesome-skills/code-review-skill/blob/main/CONTRIBUTING.md" target="_blank">CONTRIBUTING(7)</a>,
|
||||
<a class="link" href="https://github.com/awesome-skills/code-review-skill/blob/main/assets/review-checklist.md" target="_blank">review-checklist(7)</a>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- ═══ AUTHORS ═══ -->
|
||||
<h2 class="sec">AUTHORS</h2>
|
||||
<section class="body">
|
||||
<dl class="bib">
|
||||
<dt>awesome-skills</dt>
|
||||
<dd>maintainer, primary author</dd>
|
||||
<dt>contributors</dt>
|
||||
<dd>see <a class="link" href="https://github.com/awesome-skills/code-review-skill/graphs/contributors" target="_blank">graphs/contributors</a></dd>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<!-- ═══ COPYRIGHT ═══ -->
|
||||
<h2 class="sec">COPYRIGHT</h2>
|
||||
<section class="body">
|
||||
<p>
|
||||
<span class="dim">Copyright (c) 2025 awesome-skills.</span><br>
|
||||
Released under the MIT License.<br>
|
||||
<span class="dim">This is free software: you are free to change and redistribute it.</span><br>
|
||||
<span class="dim">There is NO WARRANTY, to the extent permitted by law.</span>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<div style="height: 4rem;"></div>
|
||||
|
||||
<!-- ═══ END-OF-PAGE BAND ═══ -->
|
||||
<div style="border-top:1px solid var(--rule); margin-top:2rem; padding:0.625rem 0;">
|
||||
<div style="display:flex; justify-content:space-between; color:var(--fg-dim); font-size:12px; white-space:nowrap; gap:1rem;">
|
||||
<span class="band-l" style="color:var(--fg-bright);">CODE-REVIEW-SKILL(1)</span>
|
||||
<span style="color:var(--fg-dim);">awesome-skills</span>
|
||||
<span class="band-r" style="color:var(--fg-bright);">CODE-REVIEW-SKILL(1)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- ═══ VIM-LIKE STATUS BAR ═══ -->
|
||||
<div class="statusbar">
|
||||
<div class="statusbar-inner">
|
||||
<div class="statusbar-l">
|
||||
<span>-- NORMAL --</span>
|
||||
<span>code-review-skill.1</span>
|
||||
</div>
|
||||
<div class="statusbar-r">
|
||||
<span><kbd>g</kbd> top</span>
|
||||
<span><kbd>G</kbd> end</span>
|
||||
<span><kbd>q</kbd> quit</span>
|
||||
<span id="pos">1,1</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Vim-like keyboard nav for the man-page vibe
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
||||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
||||
|
||||
if (e.key === 'g') {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
} else if (e.key === 'G') {
|
||||
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
|
||||
} else if (e.key === 'j') {
|
||||
window.scrollBy({ top: 60, behavior: 'smooth' });
|
||||
} else if (e.key === 'k') {
|
||||
window.scrollBy({ top: -60, behavior: 'smooth' });
|
||||
} else if (e.key === 'q') {
|
||||
const ok = confirm('Quit man page?');
|
||||
if (ok) window.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Update line/col-like indicator from scroll position
|
||||
const posEl = document.getElementById('pos');
|
||||
function updatePos() {
|
||||
const pct = Math.round((window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100) || 0;
|
||||
const line = Math.max(1, Math.round((window.scrollY / 20)));
|
||||
posEl.textContent = line + ',1 ' + (pct >= 99 ? 'Bot' : pct <= 1 ? 'Top' : pct + '%');
|
||||
}
|
||||
updatePos();
|
||||
window.addEventListener('scroll', updatePos, { passive: true });
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
+419
@@ -0,0 +1,419 @@
|
||||
# Angular Code Review Guide
|
||||
|
||||
> Angular 17+ 代码审查指南,覆盖 Signals、Standalone 组件、RxJS 反模式、Zoneless 变更检测、模板最佳实践及性能优化等核心主题。
|
||||
|
||||
## 目录
|
||||
|
||||
- [Signals 与变更检测](#signals-与变更检测)
|
||||
- [Standalone 组件迁移](#standalone-组件迁移)
|
||||
- [RxJS 反模式](#rxjs-反模式)
|
||||
- [Zoneless 变更检测](#zoneless-变更检测)
|
||||
- [模板最佳实践](#模板最佳实践)
|
||||
- [性能优化](#性能优化)
|
||||
- [Review Checklist](#review-checklist)
|
||||
|
||||
---
|
||||
|
||||
## Signals 与变更检测
|
||||
|
||||
### Signal + OnPush 自动触发变更检测
|
||||
|
||||
```typescript
|
||||
// ❌ 可变状态 + OnPush = 界面不更新
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `<p>{{ data.name }}</p>`,
|
||||
})
|
||||
export class UserProfile {
|
||||
data = { name: 'Alice' };
|
||||
changeName() { this.data.name = 'Bob'; } // UI 不会更新!
|
||||
}
|
||||
|
||||
// ✅ Signal + OnPush = 自动变更检测
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `<p>{{ name() }}</p>`,
|
||||
})
|
||||
export class UserProfile {
|
||||
name = signal('Alice');
|
||||
changeName() { this.name.set('Bob'); } // 自动触发 CD
|
||||
}
|
||||
```
|
||||
|
||||
### @Input() 对象变异不会被 OnPush 检测
|
||||
|
||||
```typescript
|
||||
// ❌ 变异 Input 对象——引用不变,OnPush 不检测
|
||||
@Input() config!: Config;
|
||||
updateConfig() { this.config.theme = 'dark'; }
|
||||
|
||||
// ✅ 创建新引用
|
||||
updateConfig() { this.config = { ...this.config, theme: 'dark' }; }
|
||||
```
|
||||
|
||||
### computed() 用于派生状态
|
||||
|
||||
```typescript
|
||||
// ❌ effect 用于同步状态——反模式,可能触发额外 CD 周期
|
||||
export class CartComponent {
|
||||
total = signal(0);
|
||||
discounted = signal(0);
|
||||
|
||||
constructor() {
|
||||
effect(() => this.discounted.set(this.total() * 0.9));
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ computed 用于派生状态——惰性计算,无副作用
|
||||
export class CartComponent {
|
||||
total = signal(0);
|
||||
discounted = computed(() => this.total() * 0.9);
|
||||
}
|
||||
```
|
||||
|
||||
### effect() 中 Signal 读取在 await 后不会被追踪
|
||||
|
||||
```typescript
|
||||
// ❌ await 之后读取 Signal——依赖未被追踪
|
||||
effect(async () => {
|
||||
const data = await fetchUserData();
|
||||
console.log(`Theme: ${theme()}`); // theme() 未被追踪!
|
||||
});
|
||||
|
||||
// ✅ 在 await 之前同步读取
|
||||
effect(async () => {
|
||||
const currentTheme = theme(); // 同步读取,被追踪
|
||||
const data = await fetchUserData();
|
||||
console.log(`Theme: ${currentTheme}`);
|
||||
});
|
||||
```
|
||||
|
||||
### effect 只在特定场景使用
|
||||
|
||||
```typescript
|
||||
// ❌ 用 effect 同步两个 Signal——永远用 computed
|
||||
effect(() => { this.filtered.set(this.items().filter(i => i.active)); });
|
||||
|
||||
// ✅ effect 的合理场景:DOM 操作、分析日志、订阅外部源
|
||||
effect(() => {
|
||||
const canvas = this.canvasRef.nativeElement;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = this.color();
|
||||
ctx.fillRect(0, 0, this.size(), this.size());
|
||||
});
|
||||
|
||||
// 💡 "There are no situations where effect is good,
|
||||
// only situations where it is appropriate."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Standalone 组件迁移
|
||||
|
||||
### Angular 19+ standalone 是默认值
|
||||
|
||||
```typescript
|
||||
// ❌ Legacy NgModule 组件
|
||||
@Component({
|
||||
selector: 'old-component',
|
||||
standalone: false,
|
||||
})
|
||||
export class OldComponent {}
|
||||
|
||||
// ✅ 现代 Standalone 组件(Angular 19+ standalone 是默认值)
|
||||
@Component({
|
||||
selector: 'user-profile',
|
||||
imports: [ProfilePhoto, RouterLink],
|
||||
template: `<profile-photo /><a routerLink="/edit">Edit</a>`,
|
||||
})
|
||||
export class UserProfile {}
|
||||
```
|
||||
|
||||
### 审查标记
|
||||
|
||||
```typescript
|
||||
// ⚠️ 需要迁移的信号:
|
||||
// 1. standalone: false
|
||||
// 2. @NgModule declarations
|
||||
// 3. 组件通过 NgModule 而非直接 import
|
||||
|
||||
// ✅ 迁移路径:
|
||||
// 1. 删除 standalone: false
|
||||
// 2. 将依赖添加到组件的 imports 数组
|
||||
// 3. 如果不再有 declarations,删除 NgModule
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## RxJS 反模式
|
||||
|
||||
### subscribe() 必须配 takeUntilDestroyed
|
||||
|
||||
```typescript
|
||||
// ❌ 裸 subscribe——内存泄漏!组件销毁后仍继续接收数据
|
||||
@Component({ /* ... */ })
|
||||
export class UserProfile implements OnInit {
|
||||
ngOnInit() {
|
||||
this.data$.subscribe(data => this.processData(data));
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ takeUntilDestroyed——自动在组件销毁时取消(需在构造函数或注入上下文中调用)
|
||||
@Component({ /* ... */ })
|
||||
export class UserProfile {
|
||||
constructor() {
|
||||
this.data$.pipe(takeUntilDestroyed()).subscribe(data => {
|
||||
this.processData(data);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 在构造函数外使用——传入 DestroyRef
|
||||
@Component({ /* ... */ })
|
||||
export class UserProfile {
|
||||
private destroyRef = inject(DestroyRef);
|
||||
|
||||
startListening() {
|
||||
this.data$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(/* ... */);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### toSignal 优于 AsyncPipe
|
||||
|
||||
```typescript
|
||||
// ❌ AsyncPipe——需要导入,模板中有 | async
|
||||
@Component({
|
||||
imports: [AsyncPipe],
|
||||
template: `{{ data$ | async }}`,
|
||||
})
|
||||
|
||||
// ✅ toSignal——自动取消订阅,可在任何地方使用
|
||||
export class UserProfile {
|
||||
data = toSignal(this.data$, { initialValue: null });
|
||||
// 模板直接用 data()
|
||||
}
|
||||
```
|
||||
|
||||
### 避免重复 toSignal 调用
|
||||
|
||||
```typescript
|
||||
// ❌ toSignal 每次调用都创建新订阅
|
||||
getData() {
|
||||
return toSignal(this.http.get('/api/data'));
|
||||
}
|
||||
|
||||
// ✅ 存储结果
|
||||
data = toSignal(this.http.get('/api/data'), { initialValue: null });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Zoneless 变更检测
|
||||
|
||||
### 普通属性变异不会被检测(Angular 21+)
|
||||
|
||||
```typescript
|
||||
// ❌ Zoneless 下普通属性赋值不触发 CD
|
||||
export class UserService {
|
||||
user: User | null = null;
|
||||
loadUser() { this.user = fetchResult; } // 不触发!
|
||||
}
|
||||
|
||||
// ✅ Signal 自动触发 CD
|
||||
export class UserService {
|
||||
private _user = signal<User | null>(null);
|
||||
readonly user = this._user.asReadonly();
|
||||
loadUser() { this._user.set(fetchResult); }
|
||||
}
|
||||
```
|
||||
|
||||
### NgZone API 在 Zoneless 中失效
|
||||
|
||||
```typescript
|
||||
// ❌ NgZone.onStable 在 zoneless 中永远不会触发
|
||||
ngZone.onStable.subscribe(() => { /* 永远不触发 */ });
|
||||
|
||||
// ✅ 使用 afterNextRender
|
||||
afterNextRender({ write: () => { /* CD 之后执行 */ } });
|
||||
```
|
||||
|
||||
### Reactive Forms 变异需要 markForCheck
|
||||
|
||||
```typescript
|
||||
// ❌ Reactive Forms 的 setValue/patchValue 在 zoneless 中不自动调度 CD
|
||||
this.form.patchValue({ name: 'Alice' }); // UI 可能不更新
|
||||
|
||||
// ✅ 手动标记或通过 Signal 反映
|
||||
this.form.patchValue({ name: 'Alice' });
|
||||
this.cdr.markForCheck();
|
||||
```
|
||||
|
||||
### Zoneless 下有效的 CD 触发器
|
||||
|
||||
| 触发器 | 说明 |
|
||||
|--------|------|
|
||||
| `signal.set()` / `.update()` | Signal 更新自动触发 |
|
||||
| `ChangeDetectorRef.markForCheck()` | 手动标记 |
|
||||
| `ComponentRef.setInput()` | 输入绑定 |
|
||||
| 模板事件监听器回调 | 用户交互 |
|
||||
|
||||
---
|
||||
|
||||
## 模板最佳实践
|
||||
|
||||
### 复杂逻辑提取为 computed Signal
|
||||
|
||||
```typescript
|
||||
// ❌ 模板中复杂表达式
|
||||
template: `<div *ngIf="items.filter(i => i.active).length > 0 && user.role === 'admin'">`
|
||||
|
||||
// ✅ 提取为 computed
|
||||
filteredItems = computed(() => this.items().filter(i => i.active));
|
||||
shouldShow = computed(() => this.filteredItems().length > 0 && this.user().role === 'admin');
|
||||
template: `@if (shouldShow()) { <div>...</div> }`
|
||||
```
|
||||
|
||||
### 原生绑定优于 NgClass / NgStyle
|
||||
|
||||
```typescript
|
||||
// ❌ NgClass/NgStyle——额外指令开销
|
||||
template: `<div [ngClass]="{active: isActive}" [ngStyle]="{'color': textColor}">`
|
||||
|
||||
// ✅ 原生 class/style 绑定——性能更好
|
||||
template: `<div [class.active]="isActive" [style.color]="textColor">`
|
||||
```
|
||||
|
||||
### 模板专用成员标记 protected
|
||||
|
||||
```typescript
|
||||
// ❂ 模板专用方法暴露为 public
|
||||
export class UserProfile {
|
||||
formatName(name: string) { return name.trim(); }
|
||||
}
|
||||
|
||||
// ✅ 模板专用成员用 protected
|
||||
export class UserProfile {
|
||||
protected formatName(name: string) { return name.trim(); }
|
||||
}
|
||||
```
|
||||
|
||||
### Angular 管理的属性标记 readonly
|
||||
|
||||
```typescript
|
||||
// ❌ input/output/model 可被意外覆盖
|
||||
userId = input<string>();
|
||||
userSaved = output<void>();
|
||||
|
||||
// ✅ readonly 防止意外赋值
|
||||
readonly userId = input<string>();
|
||||
readonly userSaved = output<void>();
|
||||
readonly userName = model<string>();
|
||||
```
|
||||
|
||||
### 命名规范:操作名而非事件名
|
||||
|
||||
```typescript
|
||||
// ❌ 以事件命名
|
||||
template: `<button (click)="handleClick()">Save</button>`
|
||||
|
||||
// ✅ 以操作命名
|
||||
template: `<button (click)="saveUserData()">Save</button>`
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 性能优化
|
||||
|
||||
### effect 是最后手段——优先 computed
|
||||
|
||||
```typescript
|
||||
// ❌ effect 用于状态同步——触发额外 CD,可能无限循环
|
||||
effect(() => {
|
||||
this.filteredItems.set(this.items().filter(i => i.active));
|
||||
});
|
||||
|
||||
// ✅ computed——惰性计算,无副作用,无额外 CD
|
||||
filteredItems = computed(() => this.items().filter(i => i.active));
|
||||
```
|
||||
|
||||
### afterRenderEffect 分离读写阶段
|
||||
|
||||
```typescript
|
||||
// ❌ 无阶段指定 = mixedReadWrite = 额外 DOM 回流
|
||||
afterRenderEffect(() => {
|
||||
const height = el.offsetHeight; // 读
|
||||
el.style.height = height + 10 + 'px'; // 写
|
||||
});
|
||||
|
||||
// ✅ 分离阶段减少回流
|
||||
afterRenderEffect({
|
||||
earlyRead: () => el.offsetHeight,
|
||||
write: (height) => { el.style.height = height() + 10 + 'px'; },
|
||||
read: () => verifyLayout(),
|
||||
});
|
||||
```
|
||||
|
||||
### inject() 优于构造函数注入
|
||||
|
||||
```typescript
|
||||
// ❌ 构造函数注入——多依赖时难以阅读
|
||||
export class UserService {
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private router: Router,
|
||||
private auth: AuthService,
|
||||
) {}
|
||||
}
|
||||
|
||||
// ✅ inject()——更好的类型推断和可读性
|
||||
export class UserService {
|
||||
private http = inject(HttpClient);
|
||||
private router = inject(Router);
|
||||
private auth = inject(AuthService);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Review Checklist
|
||||
|
||||
### Signals 与变更检测
|
||||
|
||||
- [ ] Signal + OnPush 用于模板状态(非可变对象)
|
||||
- [ ] `@Input()` 对象通过新引用更新(非变异)
|
||||
- [ ] 派生状态用 `computed()`,不用 `effect()`
|
||||
- [ ] `effect()` 中 Signal 读取在 `await` 之前
|
||||
- [ ] `effect()` 只用于 DOM 操作、日志、外部源订阅
|
||||
|
||||
### Standalone 组件
|
||||
|
||||
- [ ] 无 `standalone: false`(Angular 19+)
|
||||
- [ ] 组件通过 `imports` 数组导入依赖
|
||||
- [ ] 无不必要的 `@NgModule`
|
||||
|
||||
### RxJS
|
||||
|
||||
- [ ] `.subscribe()` 配 `takeUntilDestroyed` 或 `async` pipe
|
||||
- [ ] 优先 `toSignal` 而非 `AsyncPipe`
|
||||
- [ ] 无重复 `toSignal` 调用
|
||||
|
||||
### Zoneless
|
||||
|
||||
- [ ] 模板状态通过 Signal 管理(非普通属性)
|
||||
- [ ] 无 `NgZone.onStable` / `NgZone.onMicrotaskEmpty`
|
||||
- [ ] Reactive Forms 变异后有 `markForCheck()`
|
||||
|
||||
### 模板
|
||||
|
||||
- [ ] 复杂逻辑提取为 `computed` Signal
|
||||
- [ ] 使用原生 `[class]`/`[style]` 而非 `NgClass`/`NgStyle`
|
||||
- [ ] 模板专用成员标记 `protected`
|
||||
- [ ] `input`/`output`/`model` 属性标记 `readonly`
|
||||
- [ ] 事件处理器以操作命名(`saveData` 而非 `handleClick`)
|
||||
|
||||
### 性能
|
||||
|
||||
- [ ] `effect()` 不用于状态同步
|
||||
- [ ] `afterRenderEffect` 分离读写阶段
|
||||
- [ ] `inject()` 用于依赖注入
|
||||
@@ -0,0 +1,472 @@
|
||||
# Architecture Review Guide
|
||||
|
||||
架构设计审查指南,帮助评估代码的架构是否合理、设计是否恰当。
|
||||
|
||||
## SOLID 原则检查清单
|
||||
|
||||
### S - 单一职责原则 (SRP)
|
||||
|
||||
**检查要点:**
|
||||
- 这个类/模块是否只有一个改变的理由?
|
||||
- 类中的方法是否都服务于同一个目的?
|
||||
- 如果要向非技术人员描述这个类,能否用一句话说清楚?
|
||||
|
||||
**代码审查中的识别信号:**
|
||||
```
|
||||
⚠️ 类名包含 "And"、"Manager"、"Handler"、"Processor" 等泛化词汇
|
||||
⚠️ 一个类超过 200-300 行代码
|
||||
⚠️ 类有超过 5-7 个公共方法
|
||||
⚠️ 不同的方法操作完全不同的数据
|
||||
```
|
||||
|
||||
**审查问题:**
|
||||
- "这个类负责哪些事情?能否拆分?"
|
||||
- "如果 X 需求变化,哪些方法需要改?如果 Y 需求变化呢?"
|
||||
|
||||
### O - 开闭原则 (OCP)
|
||||
|
||||
**检查要点:**
|
||||
- 添加新功能时,是否需要修改现有代码?
|
||||
- 是否可以通过扩展(继承、组合)来添加新行为?
|
||||
- 是否存在大量的 if/else 或 switch 语句来处理不同类型?
|
||||
|
||||
**代码审查中的识别信号:**
|
||||
```
|
||||
⚠️ switch/if-else 链处理不同类型
|
||||
⚠️ 添加新功能需要修改核心类
|
||||
⚠️ 类型检查 (instanceof, typeof) 散布在代码中
|
||||
```
|
||||
|
||||
**审查问题:**
|
||||
- "如果要添加新的 X 类型,需要修改哪些文件?"
|
||||
- "这个 switch 语句会随着新类型增加而增长吗?"
|
||||
|
||||
### L - 里氏替换原则 (LSP)
|
||||
|
||||
**检查要点:**
|
||||
- 子类是否可以完全替代父类使用?
|
||||
- 子类是否改变了父类方法的预期行为?
|
||||
- 是否存在子类抛出父类未声明的异常?
|
||||
|
||||
**代码审查中的识别信号:**
|
||||
```
|
||||
⚠️ 显式类型转换 (casting)
|
||||
⚠️ 子类方法抛出 NotImplementedException
|
||||
⚠️ 子类方法为空实现或只有 return
|
||||
⚠️ 使用基类的地方需要检查具体类型
|
||||
```
|
||||
|
||||
**审查问题:**
|
||||
- "如果用子类替换父类,调用方代码是否需要修改?"
|
||||
- "这个方法在子类中的行为是否符合父类的契约?"
|
||||
|
||||
### I - 接口隔离原则 (ISP)
|
||||
|
||||
**检查要点:**
|
||||
- 接口是否足够小且专注?
|
||||
- 实现类是否被迫实现不需要的方法?
|
||||
- 客户端是否依赖了它不使用的方法?
|
||||
|
||||
**代码审查中的识别信号:**
|
||||
```
|
||||
⚠️ 接口超过 5-7 个方法
|
||||
⚠️ 实现类有空方法或抛出 NotImplementedException
|
||||
⚠️ 接口名称过于宽泛 (IManager, IService)
|
||||
⚠️ 不同的客户端只使用接口的部分方法
|
||||
```
|
||||
|
||||
**审查问题:**
|
||||
- "这个接口的所有方法是否都被每个实现类使用?"
|
||||
- "能否将这个大接口拆分为更小的专用接口?"
|
||||
|
||||
### D - 依赖倒置原则 (DIP)
|
||||
|
||||
**检查要点:**
|
||||
- 高层模块是否依赖于抽象而非具体实现?
|
||||
- 是否使用依赖注入而非直接 new 对象?
|
||||
- 抽象是否由高层模块定义而非低层模块?
|
||||
|
||||
**代码审查中的识别信号:**
|
||||
```
|
||||
⚠️ 高层模块直接 new 低层模块的具体类
|
||||
⚠️ 导入具体实现类而非接口/抽象类
|
||||
⚠️ 配置和连接字符串硬编码在业务逻辑中
|
||||
⚠️ 难以为某个类编写单元测试
|
||||
```
|
||||
|
||||
**审查问题:**
|
||||
- "这个类的依赖能否在测试时被 mock 替换?"
|
||||
- "如果要更换数据库/API 实现,需要修改多少地方?"
|
||||
|
||||
---
|
||||
|
||||
## 架构反模式识别
|
||||
|
||||
### 致命反模式
|
||||
|
||||
| 反模式 | 识别信号 | 影响 |
|
||||
|--------|----------|------|
|
||||
| **大泥球 (Big Ball of Mud)** | 没有清晰的模块边界,任何代码都可能调用任何其他代码 | 难以理解、修改和测试 |
|
||||
| **上帝类 (God Object)** | 单个类承担过多职责,知道太多、做太多 | 高耦合,难以重用和测试 |
|
||||
| **意大利面条代码** | 控制流程混乱,goto 或深层嵌套,难以追踪执行路径 | 难以理解和维护 |
|
||||
| **熔岩流 (Lava Flow)** | 没人敢动的古老代码,缺乏文档和测试 | 技术债务累积 |
|
||||
|
||||
### 设计反模式
|
||||
|
||||
| 反模式 | 识别信号 | 建议 |
|
||||
|--------|----------|------|
|
||||
| **金锤子 (Golden Hammer)** | 对所有问题使用同一种技术/模式 | 根据问题选择合适的解决方案 |
|
||||
| **过度工程 (Gas Factory)** | 简单问题用复杂方案解决,滥用设计模式 | YAGNI 原则,先简单后复杂 |
|
||||
| **船锚 (Boat Anchor)** | 为"将来可能需要"而写的未使用代码 | 删除未使用代码,需要时再写 |
|
||||
| **复制粘贴编程** | 相同逻辑出现在多处 | 提取公共方法或模块 |
|
||||
|
||||
### 审查问题
|
||||
|
||||
```markdown
|
||||
🔴 [blocking] "这个类有 2000 行代码,建议拆分为多个专注的类"
|
||||
🟡 [important] "这段逻辑在 3 个地方重复,考虑提取为公共方法?"
|
||||
💡 [suggestion] "这个 switch 语句可以用策略模式替代,更易扩展"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 耦合度与内聚性评估
|
||||
|
||||
### 耦合类型(从好到差)
|
||||
|
||||
| 类型 | 描述 | 示例 |
|
||||
|------|------|------|
|
||||
| **消息耦合** ✅ | 通过参数传递数据 | `calculate(price, quantity)` |
|
||||
| **数据耦合** ✅ | 共享简单数据结构 | `processOrder(orderDTO)` |
|
||||
| **印记耦合** ⚠️ | 共享复杂数据结构但只用部分 | 传入整个 User 对象但只用 name |
|
||||
| **控制耦合** ⚠️ | 传递控制标志影响行为 | `process(data, isAdmin=true)` |
|
||||
| **公共耦合** ❌ | 共享全局变量 | 多个模块读写同一个全局状态 |
|
||||
| **内容耦合** ❌ | 直接访问另一模块的内部 | 直接操作另一个类的私有属性 |
|
||||
|
||||
### 内聚类型(从好到差)
|
||||
|
||||
| 类型 | 描述 | 质量 |
|
||||
|------|------|------|
|
||||
| **功能内聚** | 所有元素完成单一任务 | ✅ 最佳 |
|
||||
| **顺序内聚** | 输出作为下一步输入 | ✅ 良好 |
|
||||
| **通信内聚** | 操作相同数据 | ⚠️ 可接受 |
|
||||
| **时间内聚** | 同时执行的任务 | ⚠️ 较差 |
|
||||
| **逻辑内聚** | 逻辑相关但功能不同 | ❌ 差 |
|
||||
| **偶然内聚** | 没有明显关系 | ❌ 最差 |
|
||||
|
||||
### 度量指标参考
|
||||
|
||||
```yaml
|
||||
耦合指标:
|
||||
CBO (类间耦合):
|
||||
好: < 5
|
||||
警告: 5-10
|
||||
危险: > 10
|
||||
|
||||
Ce (传出耦合):
|
||||
描述: 依赖多少外部类
|
||||
好: < 7
|
||||
|
||||
Ca (传入耦合):
|
||||
描述: 被多少类依赖
|
||||
高值意味着: 修改影响大,需要稳定
|
||||
|
||||
内聚指标:
|
||||
LCOM4 (方法缺乏内聚):
|
||||
1: 单一职责 ✅
|
||||
2-3: 可能需要拆分 ⚠️
|
||||
>3: 应该拆分 ❌
|
||||
```
|
||||
|
||||
### 审查问题
|
||||
|
||||
- "这个模块依赖了多少其他模块?能否减少?"
|
||||
- "修改这个类会影响多少其他地方?"
|
||||
- "这个类的方法是否都操作相同的数据?"
|
||||
|
||||
---
|
||||
|
||||
## 分层架构审查
|
||||
|
||||
### Clean Architecture 层次检查
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Frameworks & Drivers │ ← 最外层:Web、DB、UI
|
||||
├─────────────────────────────────────┤
|
||||
│ Interface Adapters │ ← Controllers、Gateways、Presenters
|
||||
├─────────────────────────────────────┤
|
||||
│ Application Layer │ ← Use Cases、Application Services
|
||||
├─────────────────────────────────────┤
|
||||
│ Domain Layer │ ← Entities、Domain Services
|
||||
└─────────────────────────────────────┘
|
||||
↑ 依赖方向只能向内 ↑
|
||||
```
|
||||
|
||||
### 依赖规则检查
|
||||
|
||||
**核心规则:源代码依赖只能指向内层**
|
||||
|
||||
```typescript
|
||||
// ❌ 违反依赖规则:Domain 层依赖 Infrastructure
|
||||
// domain/User.ts
|
||||
import { MySQLConnection } from '../infrastructure/database';
|
||||
|
||||
// ✅ 正确:Domain 层定义接口,Infrastructure 实现
|
||||
// domain/UserRepository.ts (接口)
|
||||
interface UserRepository {
|
||||
findById(id: string): Promise<User>;
|
||||
}
|
||||
|
||||
// infrastructure/MySQLUserRepository.ts (实现)
|
||||
class MySQLUserRepository implements UserRepository {
|
||||
findById(id: string): Promise<User> { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
### 审查清单
|
||||
|
||||
**层次边界检查:**
|
||||
- [ ] Domain 层是否有外部依赖(数据库、HTTP、文件系统)?
|
||||
- [ ] Application 层是否直接操作数据库或调用外部 API?
|
||||
- [ ] Controller 是否包含业务逻辑?
|
||||
- [ ] 是否存在跨层调用(UI 直接调用 Repository)?
|
||||
|
||||
**关注点分离检查:**
|
||||
- [ ] 业务逻辑是否与展示逻辑分离?
|
||||
- [ ] 数据访问是否封装在专门的层?
|
||||
- [ ] 配置和环境相关代码是否集中管理?
|
||||
|
||||
### 审查问题
|
||||
|
||||
```markdown
|
||||
🔴 [blocking] "Domain 实体直接导入了数据库连接,违反依赖规则"
|
||||
🟡 [important] "Controller 包含业务计算逻辑,建议移到 Service 层"
|
||||
💡 [suggestion] "考虑使用依赖注入来解耦这些组件"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 设计模式使用评估
|
||||
|
||||
### 何时使用设计模式
|
||||
|
||||
| 模式 | 适用场景 | 不适用场景 |
|
||||
|------|----------|------------|
|
||||
| **Factory** | 需要创建不同类型对象,类型在运行时确定 | 只有一种类型,或类型固定不变 |
|
||||
| **Strategy** | 算法需要在运行时切换,有多种可互换的行为 | 只有一种算法,或算法不会变化 |
|
||||
| **Observer** | 一对多依赖,状态变化需要通知多个对象 | 简单的直接调用即可满足需求 |
|
||||
| **Singleton** | 确实需要全局唯一实例,如配置管理 | 可以通过依赖注入传递的对象 |
|
||||
| **Decorator** | 需要动态添加职责,避免继承爆炸 | 职责固定,不需要动态组合 |
|
||||
|
||||
### 过度设计警告信号
|
||||
|
||||
```
|
||||
⚠️ Patternitis(模式炎)识别信号:
|
||||
|
||||
1. 简单的 if/else 被替换为策略模式 + 工厂 + 注册表
|
||||
2. 只有一个实现的接口
|
||||
3. 为了"将来可能需要"而添加的抽象层
|
||||
4. 代码行数因模式应用而大幅增加
|
||||
5. 新人需要很长时间才能理解代码结构
|
||||
```
|
||||
|
||||
### 审查原则
|
||||
|
||||
```markdown
|
||||
✅ 正确使用模式:
|
||||
- 解决了实际的可扩展性问题
|
||||
- 代码更容易理解和测试
|
||||
- 添加新功能变得更简单
|
||||
|
||||
❌ 过度使用模式:
|
||||
- 为了使用模式而使用
|
||||
- 增加了不必要的复杂度
|
||||
- 违反了 YAGNI 原则
|
||||
```
|
||||
|
||||
### 审查问题
|
||||
|
||||
- "使用这个模式解决了什么具体问题?"
|
||||
- "如果不用这个模式,代码会有什么问题?"
|
||||
- "这个抽象层带来的价值是否大于它的复杂度?"
|
||||
|
||||
---
|
||||
|
||||
## 可扩展性评估
|
||||
|
||||
### 扩展性检查清单
|
||||
|
||||
**功能扩展性:**
|
||||
- [ ] 添加新功能是否需要修改核心代码?
|
||||
- [ ] 是否提供了扩展点(hooks、plugins、events)?
|
||||
- [ ] 配置是否外部化(配置文件、环境变量)?
|
||||
|
||||
**数据扩展性:**
|
||||
- [ ] 数据模型是否支持新增字段?
|
||||
- [ ] 是否考虑了数据量增长的场景?
|
||||
- [ ] 查询是否有合适的索引?
|
||||
|
||||
**负载扩展性:**
|
||||
- [ ] 是否可以水平扩展(添加更多实例)?
|
||||
- [ ] 是否有状态依赖(session、本地缓存)?
|
||||
- [ ] 数据库连接是否使用连接池?
|
||||
|
||||
### 扩展点设计检查
|
||||
|
||||
```typescript
|
||||
// ✅ 好的扩展设计:使用事件/钩子
|
||||
class OrderService {
|
||||
private hooks: OrderHooks;
|
||||
|
||||
async createOrder(order: Order) {
|
||||
await this.hooks.beforeCreate?.(order);
|
||||
const result = await this.save(order);
|
||||
await this.hooks.afterCreate?.(result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ 差的扩展设计:硬编码所有行为
|
||||
class OrderService {
|
||||
async createOrder(order: Order) {
|
||||
await this.sendEmail(order); // 硬编码
|
||||
await this.updateInventory(order); // 硬编码
|
||||
await this.notifyWarehouse(order); // 硬编码
|
||||
return await this.save(order);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 审查问题
|
||||
|
||||
```markdown
|
||||
💡 [suggestion] "如果将来需要支持新的支付方式,这个设计是否容易扩展?"
|
||||
🟡 [important] "这里的逻辑是硬编码的,考虑使用配置或策略模式?"
|
||||
📚 [learning] "事件驱动架构可以让这个功能更容易扩展"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 代码结构最佳实践
|
||||
|
||||
### 目录组织
|
||||
|
||||
**按功能/领域组织(推荐):**
|
||||
```
|
||||
src/
|
||||
├── user/
|
||||
│ ├── User.ts (实体)
|
||||
│ ├── UserService.ts (服务)
|
||||
│ ├── UserRepository.ts (数据访问)
|
||||
│ └── UserController.ts (API)
|
||||
├── order/
|
||||
│ ├── Order.ts
|
||||
│ ├── OrderService.ts
|
||||
│ └── ...
|
||||
└── shared/
|
||||
├── utils/
|
||||
└── types/
|
||||
```
|
||||
|
||||
**按技术层组织(不推荐):**
|
||||
```
|
||||
src/
|
||||
├── controllers/ ← 不同领域混在一起
|
||||
│ ├── UserController.ts
|
||||
│ └── OrderController.ts
|
||||
├── services/
|
||||
├── repositories/
|
||||
└── models/
|
||||
```
|
||||
|
||||
### 命名约定检查
|
||||
|
||||
| 类型 | 约定 | 示例 |
|
||||
|------|------|------|
|
||||
| 类名 | PascalCase,名词 | `UserService`, `OrderRepository` |
|
||||
| 方法名 | camelCase,动词 | `createUser`, `findOrderById` |
|
||||
| 接口名 | I 前缀或无前缀 | `IUserService` 或 `UserService` |
|
||||
| 常量 | UPPER_SNAKE_CASE | `MAX_RETRY_COUNT` |
|
||||
| 私有属性 | 下划线前缀或无 | `_cache` 或 `#cache` |
|
||||
|
||||
### 文件大小指南
|
||||
|
||||
```yaml
|
||||
建议限制:
|
||||
单个文件: < 300 行
|
||||
单个函数: < 50 行
|
||||
单个类: < 200 行
|
||||
函数参数: < 4 个
|
||||
嵌套深度: < 4 层
|
||||
|
||||
超出限制时:
|
||||
- 考虑拆分为更小的单元
|
||||
- 使用组合而非继承
|
||||
- 提取辅助函数或类
|
||||
```
|
||||
|
||||
### 审查问题
|
||||
|
||||
```markdown
|
||||
🟢 [nit] "这个 500 行的文件可以考虑按职责拆分"
|
||||
🟡 [important] "建议按功能领域而非技术层组织目录结构"
|
||||
💡 [suggestion] "函数名 `process` 不够明确,考虑改为 `calculateOrderTotal`?"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 快速参考清单
|
||||
|
||||
### 架构审查 5 分钟速查
|
||||
|
||||
```markdown
|
||||
□ 依赖方向是否正确?(外层依赖内层)
|
||||
□ 是否存在循环依赖?
|
||||
□ 核心业务逻辑是否与框架/UI/数据库解耦?
|
||||
□ 是否遵循 SOLID 原则?
|
||||
□ 是否存在明显的反模式?
|
||||
```
|
||||
|
||||
### 红旗信号(必须处理)
|
||||
|
||||
```markdown
|
||||
🔴 God Object - 单个类超过 1000 行
|
||||
🔴 循环依赖 - A → B → C → A
|
||||
🔴 Domain 层包含框架依赖
|
||||
🔴 硬编码的配置和密钥
|
||||
🔴 没有接口的外部服务调用
|
||||
```
|
||||
|
||||
### 黄旗信号(建议处理)
|
||||
|
||||
```markdown
|
||||
🟡 类间耦合度 (CBO) > 10
|
||||
🟡 方法参数超过 5 个
|
||||
🟡 嵌套深度超过 4 层
|
||||
🟡 重复代码块 > 10 行
|
||||
🟡 只有一个实现的接口
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 工具推荐
|
||||
|
||||
| 工具 | 用途 | 语言支持 |
|
||||
|------|------|----------|
|
||||
| **SonarQube** | 代码质量、耦合度分析 | 多语言 |
|
||||
| **NDepend** | 依赖分析、架构规则 | .NET |
|
||||
| **JDepend** | 包依赖分析 | Java |
|
||||
| **Madge** | 模块依赖图 | JavaScript/TypeScript |
|
||||
| **ESLint** | 代码规范、复杂度检查 | JavaScript/TypeScript |
|
||||
| **CodeScene** | 技术债务、热点分析 | 多语言 |
|
||||
|
||||
---
|
||||
|
||||
## 参考资源
|
||||
|
||||
- [Clean Architecture - Uncle Bob](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
|
||||
- [SOLID Principles in Code Review - JetBrains](https://blog.jetbrains.com/upsource/2015/08/31/what-to-look-for-in-a-code-review-solid-principles-2/)
|
||||
- [Software Architecture Anti-Patterns](https://medium.com/@christophnissle/anti-patterns-in-software-architecture-3c8970c9c4f5)
|
||||
- [Coupling and Cohesion in System Design](https://www.geeksforgeeks.org/system-design/coupling-and-cohesion-in-system-design/)
|
||||
- [Design Patterns - Refactoring Guru](https://refactoring.guru/design-patterns)
|
||||
+285
@@ -0,0 +1,285 @@
|
||||
# C Code Review Guide
|
||||
|
||||
> C code review guide focused on memory safety, undefined behavior, and portability. Examples assume C11.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Pointer and Buffer Safety](#pointer-and-buffer-safety)
|
||||
- [Ownership and Resource Management](#ownership-and-resource-management)
|
||||
- [Undefined Behavior Pitfalls](#undefined-behavior-pitfalls)
|
||||
- [Integer Types and Overflow](#integer-types-and-overflow)
|
||||
- [Error Handling](#error-handling)
|
||||
- [Concurrency](#concurrency)
|
||||
- [Macros and Preprocessor](#macros-and-preprocessor)
|
||||
- [API Design and Const](#api-design-and-const)
|
||||
- [Tooling and Build Checks](#tooling-and-build-checks)
|
||||
- [Review Checklist](#review-checklist)
|
||||
|
||||
---
|
||||
|
||||
## Pointer and Buffer Safety
|
||||
|
||||
### Always carry size with buffers
|
||||
|
||||
```c
|
||||
// ❌ Bad: ignores destination size
|
||||
bool copy_name(char *dst, size_t dst_size, const char *src) {
|
||||
strcpy(dst, src);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ✅ Good: validate size and terminate
|
||||
bool copy_name(char *dst, size_t dst_size, const char *src) {
|
||||
size_t len = strlen(src);
|
||||
if (len + 1 > dst_size) {
|
||||
return false;
|
||||
}
|
||||
memcpy(dst, src, len + 1);
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### Avoid dangerous APIs
|
||||
|
||||
Prefer `snprintf`, `fgets`, and explicit bounds over `gets`, `strcpy`, or `sprintf`.
|
||||
|
||||
```c
|
||||
// ❌ Bad: unbounded write
|
||||
sprintf(buf, "%s", input);
|
||||
|
||||
// ✅ Good: bounded write
|
||||
snprintf(buf, buf_size, "%s", input);
|
||||
```
|
||||
|
||||
### Use the right copy primitive
|
||||
|
||||
```c
|
||||
// ❌ Bad: memcpy with overlapping regions
|
||||
memcpy(dst, src, len);
|
||||
|
||||
// ✅ Good: memmove handles overlap
|
||||
memmove(dst, src, len);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ownership and Resource Management
|
||||
|
||||
### One allocation, one free
|
||||
|
||||
Track ownership and clean up on every error path.
|
||||
|
||||
```c
|
||||
// ✅ Good: cleanup label avoids leaks
|
||||
int load_file(const char *path) {
|
||||
int rc = -1;
|
||||
FILE *f = NULL;
|
||||
char *buf = NULL;
|
||||
|
||||
f = fopen(path, "rb");
|
||||
if (!f) {
|
||||
goto cleanup;
|
||||
}
|
||||
buf = malloc(4096);
|
||||
if (!buf) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (fread(buf, 1, 4096, f) == 0) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
rc = 0;
|
||||
|
||||
cleanup:
|
||||
free(buf);
|
||||
if (f) {
|
||||
fclose(f);
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Undefined Behavior Pitfalls
|
||||
|
||||
### Common UB patterns
|
||||
|
||||
```c
|
||||
// ❌ Bad: use after free
|
||||
char *p = malloc(10);
|
||||
free(p);
|
||||
p[0] = 'a';
|
||||
|
||||
// ❌ Bad: uninitialized read
|
||||
int x;
|
||||
if (x > 0) { /* UB */ }
|
||||
|
||||
// ❌ Bad: signed overflow
|
||||
int sum = a + b;
|
||||
```
|
||||
|
||||
### Avoid pointer arithmetic past the object
|
||||
|
||||
```c
|
||||
// ❌ Bad: pointer past the end then dereference
|
||||
int arr[4];
|
||||
int *p = arr + 4;
|
||||
int v = *p; // UB
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integer Types and Overflow
|
||||
|
||||
### Avoid signed/unsigned surprises
|
||||
|
||||
```c
|
||||
// ❌ Bad: negative converted to large size_t
|
||||
int len = -1;
|
||||
size_t n = len;
|
||||
|
||||
// ✅ Good: validate before converting
|
||||
if (len < 0) {
|
||||
return -1;
|
||||
}
|
||||
size_t n = (size_t)len;
|
||||
```
|
||||
|
||||
### Check for overflow in size calculations
|
||||
|
||||
```c
|
||||
// ❌ Bad: potential overflow in multiplication
|
||||
size_t bytes = count * sizeof(Item);
|
||||
|
||||
// ✅ Good: check before multiplying
|
||||
if (count > SIZE_MAX / sizeof(Item)) {
|
||||
return NULL;
|
||||
}
|
||||
size_t bytes = count * sizeof(Item);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Always check return values
|
||||
|
||||
```c
|
||||
// ❌ Bad: ignore errors
|
||||
fread(buf, 1, size, f);
|
||||
|
||||
// ✅ Good: handle errors
|
||||
size_t read = fread(buf, 1, size, f);
|
||||
if (read != size && ferror(f)) {
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
|
||||
### Consistent error contracts
|
||||
|
||||
- Use a clear convention: 0 for success, negative for failure.
|
||||
- Document ownership rules on success and failure.
|
||||
- If using `errno`, set it only for actual failures.
|
||||
|
||||
---
|
||||
|
||||
## Concurrency
|
||||
|
||||
### volatile is not synchronization
|
||||
|
||||
```c
|
||||
// ❌ Bad: data race
|
||||
volatile int stop = 0;
|
||||
void worker(void) {
|
||||
while (!stop) { /* ... */ }
|
||||
}
|
||||
|
||||
// ✅ Good: C11 atomics
|
||||
_Atomic int stop = 0;
|
||||
void worker(void) {
|
||||
while (!atomic_load(&stop)) { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
### Use mutexes for shared state
|
||||
|
||||
Protect shared data with `pthread_mutex_t` or equivalent. Avoid holding locks while doing I/O.
|
||||
|
||||
---
|
||||
|
||||
## Macros and Preprocessor
|
||||
|
||||
### Parenthesize arguments
|
||||
|
||||
```c
|
||||
// ❌ Bad: macro with side effects
|
||||
#define MIN(a, b) ((a) < (b) ? (a) : (b))
|
||||
int x = MIN(i++, j++);
|
||||
|
||||
// ✅ Good: static inline function
|
||||
static inline int min_int(int a, int b) {
|
||||
return a < b ? a : b;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Design and Const
|
||||
|
||||
### Const-correctness and sizes
|
||||
|
||||
```c
|
||||
// ✅ Good: explicit size and const input
|
||||
int hash_bytes(const uint8_t *data, size_t len, uint8_t *out);
|
||||
```
|
||||
|
||||
### Document nullability
|
||||
|
||||
Clearly document whether pointers may be NULL. Prefer returning error codes instead of NULL when possible.
|
||||
|
||||
---
|
||||
|
||||
## Tooling and Build Checks
|
||||
|
||||
```bash
|
||||
# Warnings
|
||||
clang -Wall -Wextra -Werror -Wconversion -Wshadow -std=c11 ...
|
||||
|
||||
# Sanitizers (debug builds)
|
||||
clang -fsanitize=address,undefined -fno-omit-frame-pointer -g ...
|
||||
clang -fsanitize=thread -fno-omit-frame-pointer -g ...
|
||||
|
||||
# Static analysis
|
||||
clang-tidy src/*.c -- -std=c11
|
||||
cppcheck --enable=warning,performance,portability src/
|
||||
|
||||
# Formatting
|
||||
clang-format -i src/*.c include/*.h
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Review Checklist
|
||||
|
||||
### Memory and UB
|
||||
- [ ] All buffers have explicit size parameters
|
||||
- [ ] No out-of-bounds access or pointer arithmetic past objects
|
||||
- [ ] No use after free or uninitialized reads
|
||||
- [ ] Signed overflow and shift rules are respected
|
||||
|
||||
### API and Design
|
||||
- [ ] Ownership rules are documented and consistent
|
||||
- [ ] const-correctness is applied for inputs
|
||||
- [ ] Error contracts are clear and consistent
|
||||
|
||||
### Concurrency
|
||||
- [ ] No data races on shared state
|
||||
- [ ] volatile is not used for synchronization
|
||||
- [ ] Locks are held for minimal time
|
||||
|
||||
### Tooling and Tests
|
||||
- [ ] Builds clean with warnings enabled
|
||||
- [ ] Sanitizers run on critical code paths
|
||||
- [ ] Static analysis results are addressed
|
||||
@@ -0,0 +1,488 @@
|
||||
# Universal Code Quality Anti-Patterns
|
||||
|
||||
> 语言无关的代码质量反模式指南,覆盖代码复用、抽象泄漏、参数膨胀、嵌套条件、字符串类型化、TOCTOU、空操作更新等核心主题。适用于所有语言的 PR 审查。
|
||||
|
||||
## 目录
|
||||
|
||||
- [代码复用审查](#代码复用审查)
|
||||
- [参数膨胀](#参数膨胀)
|
||||
- [抽象泄漏](#抽象泄漏)
|
||||
- [字符串类型化](#字符串类型化)
|
||||
- [嵌套条件表达式](#嵌套条件表达式)
|
||||
- [复制粘贴变种](#复制粘贴变种)
|
||||
- [空操作更新](#空操作更新)
|
||||
- [TOCTOU 竞争条件](#toctou-竞争条件)
|
||||
- [过度宽泛操作](#过度宽泛操作)
|
||||
- [冗余状态](#冗余状态)
|
||||
- [通用质量审查清单](#通用质量审查清单)
|
||||
|
||||
---
|
||||
|
||||
## 代码复用审查
|
||||
|
||||
Before accepting new code, search the existing codebase for reusable utilities.
|
||||
|
||||
### 搜索现有工具函数
|
||||
|
||||
```python
|
||||
# ❌ 新写的路径拼接逻辑——项目中已有 PathBuilder
|
||||
def get_config_path(name):
|
||||
base = os.environ.get("APP_ROOT", ".")
|
||||
return os.path.join(base, "config", name + ".json")
|
||||
|
||||
# ✅ 使用已有的 PathBuilder
|
||||
def get_config_path(name):
|
||||
return PathBuilder.config(f"{name}.json")
|
||||
```
|
||||
|
||||
```javascript
|
||||
// ❌ 手写 debounce——项目已有 lodash 或 utils/debounce.ts
|
||||
function debounce(fn, ms) {
|
||||
let timer;
|
||||
return (...args) => {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => fn(...args), ms);
|
||||
};
|
||||
}
|
||||
|
||||
// ✅ 使用已有的工具函数
|
||||
import { debounce } from "@/utils/debounce";
|
||||
```
|
||||
|
||||
**审查要点:**
|
||||
- 新增函数是否与已有 utility 重名或功能重叠?
|
||||
- inline 逻辑是否可以提取为已有模块的调用?
|
||||
- 检查相邻文件和 shared/utils 目录
|
||||
|
||||
---
|
||||
|
||||
## 参数膨胀
|
||||
|
||||
### 函数参数不断增长
|
||||
|
||||
```python
|
||||
# ❌ 每次新需求加一个参数
|
||||
def create_user(name, email, role, team, active, avatar_url, timezone):
|
||||
...
|
||||
|
||||
# ✅ 使用配置对象 / dataclass
|
||||
@dataclass
|
||||
class CreateUserParams:
|
||||
name: str
|
||||
email: str
|
||||
role: Role = Role.MEMBER
|
||||
team: str | None = None
|
||||
active: bool = True
|
||||
avatar_url: str | None = None
|
||||
timezone: str = "UTC"
|
||||
|
||||
def create_user(params: CreateUserParams) -> User:
|
||||
...
|
||||
```
|
||||
|
||||
```typescript
|
||||
// ❌ 6+ 个 positional 参数
|
||||
function renderWidget(
|
||||
title: string, width: number, height: number,
|
||||
theme: string, collapsible: boolean, icon: string
|
||||
) { ... }
|
||||
|
||||
// ✅ Options object pattern
|
||||
interface WidgetOptions {
|
||||
title: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
theme?: "light" | "dark";
|
||||
collapsible?: boolean;
|
||||
icon?: string;
|
||||
}
|
||||
function renderWidget(options: WidgetOptions) { ... }
|
||||
```
|
||||
|
||||
**审查要点:**
|
||||
- 函数参数是否 ≥ 4 个?考虑 options object / dataclass
|
||||
- 新参数是否只是布尔标志?考虑 enum 或 strategy pattern
|
||||
- 是否有 `enable_x`, `disable_y` 这类互斥参数?
|
||||
|
||||
---
|
||||
|
||||
## 抽象泄漏
|
||||
|
||||
### 暴露内部实现细节
|
||||
|
||||
```python
|
||||
# ❌ 返回内部 ORM 对象——调用者被迫了解 SQLAlchemy
|
||||
def get_users():
|
||||
return session.query(User).filter(User.active == True).all()
|
||||
|
||||
# ✅ 返回 domain 对象,隐藏持久化层
|
||||
def get_active_users() -> list[UserDTO]:
|
||||
rows = user_repo.find_active()
|
||||
return [UserDTO.from_row(r) for r in rows]
|
||||
```
|
||||
|
||||
```typescript
|
||||
// ❌ 组件接收 API response 原始结构
|
||||
<UserCard user={apiResponse.data.results[0]} />
|
||||
|
||||
// ✅ 组件接收 domain 类型,adapter 处理映射
|
||||
interface UserSummary {
|
||||
displayName: string;
|
||||
avatarUrl: string;
|
||||
}
|
||||
<UserCard user={adaptUser(apiResponse)} />
|
||||
```
|
||||
|
||||
**审查要点:**
|
||||
- 函数返回类型是否泄露底层实现(ORM, HTTP client, file format)?
|
||||
- 组件/函数是否依赖外部系统的数据结构?
|
||||
- 是否破坏了已有的抽象边界?
|
||||
|
||||
---
|
||||
|
||||
## 字符串类型化
|
||||
|
||||
### 用原始字符串代替常量/枚举
|
||||
|
||||
```python
|
||||
# ❌ Magic strings 散落各处
|
||||
if status == "active":
|
||||
...
|
||||
if role == "admin":
|
||||
...
|
||||
|
||||
# ✅ 使用 enum
|
||||
class Status(StrEnum):
|
||||
ACTIVE = "active"
|
||||
SUSPENDED = "suspended"
|
||||
ARCHIVED = "archived"
|
||||
|
||||
if user.status == Status.ACTIVE:
|
||||
...
|
||||
```
|
||||
|
||||
```typescript
|
||||
// ❌ Raw string event names——拼写错误不会报错
|
||||
emitter.emit("userCreated", data);
|
||||
emitter.on("usercreated", handler); // bug: typo
|
||||
|
||||
// ✅ 常量或 branded type
|
||||
const Events = {
|
||||
USER_CREATED: "userCreated",
|
||||
USER_SUSPENDED: "userSuspended",
|
||||
} as const;
|
||||
emitter.emit(Events.USER_CREATED, data);
|
||||
```
|
||||
|
||||
**审查要点:**
|
||||
- 是否用字符串代替了已有的 enum/union type?
|
||||
- 事件名、action type、status 值是否散落在多个文件?
|
||||
- 字符串比较是否 case-sensitive 但未验证?
|
||||
|
||||
---
|
||||
|
||||
## 嵌套条件表达式
|
||||
|
||||
### 三元链和嵌套 if/else
|
||||
|
||||
```python
|
||||
# ❌ 三元链难以阅读
|
||||
label = (
|
||||
"Admin" if role == "admin" else
|
||||
"Manager" if role == "manager" else
|
||||
"Viewer" if role == "viewer" else
|
||||
"Unknown"
|
||||
)
|
||||
|
||||
# ✅ 查找表或 match
|
||||
ROLE_LABELS = {
|
||||
"admin": "Admin",
|
||||
"manager": "Manager",
|
||||
"viewer": "Viewer",
|
||||
}
|
||||
label = ROLE_LABELS.get(role, "Unknown")
|
||||
```
|
||||
|
||||
```typescript
|
||||
// ❌ 嵌套三元
|
||||
const bg = isHovered
|
||||
? isSelected ? "blue" : "gray"
|
||||
: isSelected ? "navy" : "white";
|
||||
|
||||
// ✅ 查找表(lookup map)
|
||||
const bgMap: Record<string, string> = {
|
||||
"true-true": "blue",
|
||||
"true-false": "gray",
|
||||
"false-true": "navy",
|
||||
"false-false": "white",
|
||||
};
|
||||
const bg = bgMap[`${isHovered}-${isSelected}`];
|
||||
```
|
||||
|
||||
```python
|
||||
# ❌ 嵌套 if 3+ 层
|
||||
def process(order):
|
||||
if order is not None:
|
||||
if order.items:
|
||||
for item in order.items:
|
||||
if item.price > 0:
|
||||
...
|
||||
|
||||
# ✅ Early return + guard clauses
|
||||
def process(order):
|
||||
if not order or not order.items:
|
||||
return
|
||||
for item in order.items:
|
||||
if item.price <= 0:
|
||||
continue
|
||||
...
|
||||
```
|
||||
|
||||
**审查要点:**
|
||||
- 三元表达式是否嵌套 ≥ 2 层?
|
||||
- if/else 嵌套是否 ≥ 3 层?
|
||||
- 能否用 lookup table、early return 或 match 替换?
|
||||
|
||||
---
|
||||
|
||||
## 复制粘贴变种
|
||||
|
||||
### 近乎重复的代码块
|
||||
|
||||
```python
|
||||
# ❌ 两个函数几乎一样,只有字段名不同
|
||||
def format_user(user):
|
||||
return f"{user.first_name} {user.last_name} ({user.email})"
|
||||
|
||||
def format_employee(emp):
|
||||
return f"{emp.first_name} {emp.last_name} ({emp.work_email})"
|
||||
|
||||
# ✅ 统一抽象
|
||||
def format_person(first: str, last: str, email: str) -> str:
|
||||
return f"{first} {last} ({email})"
|
||||
```
|
||||
|
||||
```typescript
|
||||
// ❌ Copy-paste handler 只改了 URL
|
||||
async function deletePost(id: string) {
|
||||
await fetch(`/api/posts/${id}`, { method: "DELETE" });
|
||||
router.push("/posts");
|
||||
}
|
||||
async function deleteComment(id: string) {
|
||||
await fetch(`/api/comments/${id}`, { method: "DELETE" });
|
||||
router.push("/comments");
|
||||
}
|
||||
|
||||
// ✅ 参数化
|
||||
async function deleteResource(resource: string, id: string) {
|
||||
await fetch(`/api/${resource}/${id}`, { method: "DELETE" });
|
||||
router.push(`/${resource}`);
|
||||
}
|
||||
```
|
||||
|
||||
**审查要点:**
|
||||
- 是否有 ≥ 2 段代码仅变量名/URL/字符串不同?
|
||||
- 能否提取参数化的共享函数?
|
||||
- 是否可以用 template method 或 strategy 消除变种?
|
||||
|
||||
---
|
||||
|
||||
## 空操作更新
|
||||
|
||||
### 无条件触发状态更新
|
||||
|
||||
```typescript
|
||||
// ❌ 每次 poll 都触发 update——即使数据未变
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
fetch("/api/status").then(r => r.json()).then(setStatus);
|
||||
}, 5000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
// ✅ 仅在值变化时更新
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
fetch("/api/status")
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
setStatus(prev => isEqual(prev, data) ? prev : data);
|
||||
});
|
||||
}, 5000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
```
|
||||
|
||||
```python
|
||||
# ❌ 每次 loop 都写 DB——即使值未变
|
||||
for item in items:
|
||||
item.status = compute_status(item)
|
||||
session.commit()
|
||||
|
||||
# ✅ 仅在变化时写入
|
||||
for item in items:
|
||||
new_status = compute_status(item)
|
||||
if item.status != new_status:
|
||||
item.status = new_status
|
||||
session.commit()
|
||||
```
|
||||
|
||||
**审查要点:**
|
||||
- polling / interval / event handler 是否无条件更新?
|
||||
- wrapper function 是否尊重 same-reference return?
|
||||
- DB 写入是否检查了实际变化?
|
||||
|
||||
---
|
||||
|
||||
## TOCTOU 竞争条件
|
||||
|
||||
### Time-of-Check-to-Time-of-Use
|
||||
|
||||
```python
|
||||
# ❌ 先检查后操作——中间文件可能被删除/创建
|
||||
if os.path.exists(path):
|
||||
with open(path) as f:
|
||||
data = f.read()
|
||||
|
||||
# ✅ 直接操作 + 处理异常
|
||||
try:
|
||||
with open(path) as f:
|
||||
data = f.read()
|
||||
except FileNotFoundError:
|
||||
data = None
|
||||
```
|
||||
|
||||
```python
|
||||
# ❌ 检查余额 → 扣款 两步操作不是原子的
|
||||
if account.balance >= amount:
|
||||
account.balance -= amount
|
||||
|
||||
# ✅ 原子操作或锁
|
||||
with account.lock:
|
||||
if account.balance < amount:
|
||||
raise InsufficientFundsError()
|
||||
account.balance -= amount
|
||||
```
|
||||
|
||||
```typescript
|
||||
// ❌ Check-then-act 在 async 环境中不安全
|
||||
if (!fileExists(path)) {
|
||||
await writeFile(path, content);
|
||||
}
|
||||
|
||||
// ✅ 直接操作 + catch
|
||||
try {
|
||||
await writeFile(path, content, { flag: "wx" });
|
||||
} catch (e) {
|
||||
if (e.code === "EEXIST") { /* handle */ }
|
||||
else throw e;
|
||||
}
|
||||
```
|
||||
|
||||
**审查要点:**
|
||||
- `if exists → operate` 模式是否可替换为 `try operate → catch`?
|
||||
- 多步状态变更是否在事务/锁内?
|
||||
- async 操作中 check 和 act 之间是否有 await?
|
||||
|
||||
---
|
||||
|
||||
## 过度宽泛操作
|
||||
|
||||
### 读取过多数据
|
||||
|
||||
```python
|
||||
# ❌ 读取整个文件再取第一行
|
||||
content = Path("log.txt").read_text()
|
||||
first_line = content.split("\n")[0]
|
||||
|
||||
# ✅ 只读第一行,不加载整个文件
|
||||
with open("log.txt") as f:
|
||||
first_line = f.readline()
|
||||
```
|
||||
|
||||
```typescript
|
||||
// ❌ 加载所有 items 再过滤
|
||||
const allItems = await db.query("SELECT * FROM orders");
|
||||
const pending = allItems.filter(o => o.status === "pending");
|
||||
|
||||
// ✅ 数据库层过滤
|
||||
const pending = await db.query(
|
||||
"SELECT * FROM orders WHERE status = ?", ["pending"]
|
||||
);
|
||||
```
|
||||
|
||||
```python
|
||||
# ❌ 读取整个列表找一条记录
|
||||
users = list(User.objects.all())
|
||||
user = next(u for u in users if u.id == user_id)
|
||||
|
||||
# ✅ 精确查询
|
||||
user = User.objects.get(id=user_id)
|
||||
```
|
||||
|
||||
**审查要点:**
|
||||
- 是否读取了整个集合/文件再只用一小部分?
|
||||
- 能否将过滤推到数据库/存储层?
|
||||
- API 调用是否支持 pagination/limit 参数?
|
||||
|
||||
---
|
||||
|
||||
## 冗余状态
|
||||
|
||||
### 状态可以被推导
|
||||
|
||||
```typescript
|
||||
// ❌ 同时存储 fullName 和 firstName + lastName
|
||||
interface User {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
fullName: string; // redundant
|
||||
}
|
||||
|
||||
// ✅ fullName 是推导值
|
||||
interface User {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
}
|
||||
const fullName = `${user.firstName} ${user.lastName}`;
|
||||
```
|
||||
|
||||
```python
|
||||
# ❌ 缓存值在源数据变化时可能过时
|
||||
class Order:
|
||||
total: float
|
||||
item_count: int # redundant if len(items) gives the same
|
||||
items: list[Item]
|
||||
|
||||
# ✅ 推导或 property
|
||||
class Order:
|
||||
items: list[Item]
|
||||
|
||||
@property
|
||||
def total(self) -> float:
|
||||
return sum(item.price for item in self.items)
|
||||
|
||||
@property
|
||||
def item_count(self) -> int:
|
||||
return len(self.items)
|
||||
```
|
||||
|
||||
**审查要点:**
|
||||
- 是否有字段可以从其他字段推导?
|
||||
- 缓存值是否有 invalidation 机制?
|
||||
- observer/effect 是否可以替换为直接调用?
|
||||
|
||||
---
|
||||
|
||||
## 通用质量审查清单
|
||||
|
||||
- [ ] **复用审查**: 搜索了现有 utility/helper,没有重复造轮子?
|
||||
- [ ] **参数数量**: 函数参数 ≤ 3 个?超过则用 options object / dataclass?
|
||||
- [ ] **抽象边界**: 返回类型没有暴露内部实现细节(ORM、HTTP client、file format)?
|
||||
- [ ] **类型安全**: 没有 magic strings 代替已有的 enum/constant/union type?
|
||||
- [ ] **条件深度**: 三元嵌套 ≤ 1 层?if/else 嵌套 ≤ 2 层?
|
||||
- [ ] **DRY**: 没有 copy-paste-with-variation(≥ 2 段近似代码)?
|
||||
- [ ] **空操作防护**: polling / interval / event handler 有 change-detection guard?
|
||||
- [ ] **TOCTOU**: `if exists → operate` 替换为 `try operate → catch`?
|
||||
- [ ] **数据精度**: 没有读取整个集合/文件只为了取子集?
|
||||
- [ ] **冗余状态**: 没有可以从其他字段推导的存储字段?
|
||||
@@ -0,0 +1,136 @@
|
||||
# Code Review Best Practices
|
||||
|
||||
Comprehensive guidelines for conducting effective code reviews.
|
||||
|
||||
## Review Philosophy
|
||||
|
||||
### Goals of Code Review
|
||||
|
||||
**Primary Goals:**
|
||||
- Catch bugs and edge cases before production
|
||||
- Ensure code maintainability and readability
|
||||
- Share knowledge across the team
|
||||
- Enforce coding standards consistently
|
||||
- Improve design and architecture decisions
|
||||
|
||||
**Secondary Goals:**
|
||||
- Mentor junior developers
|
||||
- Build team culture and trust
|
||||
- Document design decisions through discussions
|
||||
|
||||
### What Code Review is NOT
|
||||
|
||||
- A gatekeeping mechanism to block progress
|
||||
- An opportunity to show off knowledge
|
||||
- A place to nitpick formatting (use linters)
|
||||
- A way to rewrite code to personal preference
|
||||
|
||||
## Review Timing
|
||||
|
||||
### When to Review
|
||||
|
||||
| Trigger | Action |
|
||||
|---------|--------|
|
||||
| PR opened | Review within 24 hours, ideally same day |
|
||||
| Changes requested | Re-review within 4 hours |
|
||||
| Blocking issue found | Communicate immediately |
|
||||
|
||||
### Time Allocation
|
||||
|
||||
- **Small PR (<100 lines)**: 10-15 minutes
|
||||
- **Medium PR (100-400 lines)**: 20-40 minutes
|
||||
- **Large PR (>400 lines)**: Request to split, or 60+ minutes
|
||||
|
||||
## Review Depth Levels
|
||||
|
||||
### Level 1: Skim Review (5 minutes)
|
||||
- Check PR description and linked issues
|
||||
- Verify CI/CD status
|
||||
- Look at file changes overview
|
||||
- Identify if deeper review needed
|
||||
|
||||
### Level 2: Standard Review (20-30 minutes)
|
||||
- Full code walkthrough
|
||||
- Logic verification
|
||||
- Test coverage check
|
||||
- Security scan
|
||||
|
||||
### Level 3: Deep Review (60+ minutes)
|
||||
- Architecture evaluation
|
||||
- Performance analysis
|
||||
- Security audit
|
||||
- Edge case exploration
|
||||
|
||||
## Communication Guidelines
|
||||
|
||||
### Tone and Language
|
||||
|
||||
**Use collaborative language:**
|
||||
- "What do you think about..." instead of "You should..."
|
||||
- "Could we consider..." instead of "This is wrong"
|
||||
- "I'm curious about..." instead of "Why didn't you..."
|
||||
|
||||
**Be specific and actionable:**
|
||||
- Include code examples when suggesting changes
|
||||
- Link to documentation or past discussions
|
||||
- Explain the "why" behind suggestions
|
||||
|
||||
### Handling Disagreements
|
||||
|
||||
1. **Seek to understand**: Ask clarifying questions
|
||||
2. **Acknowledge valid points**: Show you've considered their perspective
|
||||
3. **Provide data**: Use benchmarks, docs, or examples
|
||||
4. **Escalate if needed**: Involve senior dev or architect
|
||||
5. **Know when to let go**: Not every hill is worth dying on
|
||||
|
||||
## Review Prioritization
|
||||
|
||||
### Must Fix (Blocking)
|
||||
- Security vulnerabilities
|
||||
- Data corruption risks
|
||||
- Breaking changes without migration
|
||||
- Critical performance issues
|
||||
- Missing error handling for user-facing features
|
||||
|
||||
### Should Fix (Important)
|
||||
- Test coverage gaps
|
||||
- Moderate performance concerns
|
||||
- Code duplication
|
||||
- Unclear naming or structure
|
||||
- Missing documentation for complex logic
|
||||
|
||||
### Nice to Have (Non-blocking)
|
||||
- Style preferences beyond linting
|
||||
- Minor optimizations
|
||||
- Additional test cases
|
||||
- Documentation improvements
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
### Reviewer Anti-Patterns
|
||||
- **Rubber stamping**: Approving without actually reviewing
|
||||
- **Bike shedding**: Debating trivial details extensively
|
||||
- **Scope creep**: "While you're at it, can you also..."
|
||||
- **Ghosting**: Requesting changes then disappearing
|
||||
- **Perfectionism**: Blocking for minor style preferences
|
||||
|
||||
### Author Anti-Patterns
|
||||
- **Mega PRs**: Submitting 1000+ line changes
|
||||
- **No context**: Missing PR description or linked issues
|
||||
- **Defensive responses**: Arguing every suggestion
|
||||
- **Silent updates**: Making changes without responding to comments
|
||||
|
||||
## Metrics and Improvement
|
||||
|
||||
### Track These Metrics
|
||||
- Time to first review
|
||||
- Review cycle time
|
||||
- Number of review rounds
|
||||
- Defect escape rate
|
||||
- Review coverage percentage
|
||||
|
||||
### Continuous Improvement
|
||||
- Hold retrospectives on review process
|
||||
- Share learnings from escaped bugs
|
||||
- Update checklists based on common issues
|
||||
- Celebrate good reviews and catches
|
||||
@@ -0,0 +1,248 @@
|
||||
# Common Bugs Checklist
|
||||
|
||||
Quick-reference bug patterns organized by category. For detailed code examples, explanations, and comprehensive review checklists, see the dedicated language guides linked below.
|
||||
|
||||
## Universal Issues
|
||||
|
||||
### Logic Errors
|
||||
- [ ] Off-by-one errors in loops and array access
|
||||
- [ ] Incorrect boolean logic (De Morgan's law violations)
|
||||
- [ ] Missing null/undefined checks
|
||||
- [ ] Race conditions in concurrent code
|
||||
- [ ] Incorrect comparison operators (`==` vs `===`, `=` vs `==`)
|
||||
- [ ] Integer overflow/underflow
|
||||
- [ ] Floating point comparison issues
|
||||
|
||||
### Resource Management
|
||||
- [ ] Memory leaks (unclosed connections, listeners)
|
||||
- [ ] File handles not closed
|
||||
- [ ] Database connections not released
|
||||
- [ ] Event listeners not removed
|
||||
- [ ] Timers/intervals not cleared
|
||||
|
||||
### Error Handling
|
||||
- [ ] Swallowed exceptions (empty catch blocks)
|
||||
- [ ] Generic exception handling hiding specific errors
|
||||
- [ ] Missing error propagation
|
||||
- [ ] Incorrect error types thrown
|
||||
- [ ] Missing finally/cleanup blocks
|
||||
|
||||
## TypeScript/JavaScript
|
||||
|
||||
- [ ] `==` instead of `===`
|
||||
- [ ] Using `any` — prefer proper types or `unknown` with type guards
|
||||
- [ ] Missing `await` on async calls
|
||||
- [ ] Unhandled promise rejections (no try-catch around await)
|
||||
- [ ] `this` context lost in callbacks
|
||||
- [ ] Missing `key` prop in lists
|
||||
- [ ] Closure capturing stale loop variable
|
||||
- [ ] `parseInt` without radix parameter
|
||||
- [ ] Modifying array/object during iteration
|
||||
|
||||
**Full guide:** [TypeScript Review Guide](typescript.md)
|
||||
|
||||
## React / React 19
|
||||
|
||||
- [ ] Hooks called conditionally or in loops (violates Rules of Hooks)
|
||||
- [ ] `useEffect` dependency array incomplete or incorrect
|
||||
- [ ] `useEffect` missing cleanup function (subscriptions, timers, fetches)
|
||||
- [ ] `useEffect` used for derived state (use `useMemo` instead)
|
||||
- [ ] `useMemo`/`useCallback` over-used or used without `React.memo`
|
||||
- [ ] Component defined inside another component (re-mounts every render)
|
||||
- [ ] Unstable props (inline objects/functions passed to memo components)
|
||||
- [ ] Direct mutation of props
|
||||
- [ ] List missing `key` or using array index as key (reorderable lists)
|
||||
- [ ] Server Component using client APIs (`useState`, `useEffect`, `onClick`)
|
||||
- [ ] `'use client'` on parent making entire subtree client-side
|
||||
- [ ] `useActionState` calling `setState` instead of returning new state
|
||||
- [ ] `useFormStatus` called in same component as `<form>` (must be in child)
|
||||
- [ ] `useOptimistic` used for critical operations (payments, deletions)
|
||||
- [ ] Single Suspense boundary for entire page (slow blocks fast)
|
||||
- [ ] Missing Error Boundary wrapping Suspense
|
||||
- [ ] `use()` Hook receiving a new Promise each render
|
||||
|
||||
**TanStack Query v5:**
|
||||
- [ ] `queryKey` missing parameters that affect data
|
||||
- [ ] Default `staleTime: 0` causing excessive refetches
|
||||
- [ ] `useSuspenseQuery` with `enabled` option (not supported)
|
||||
- [ ] Mutation not invalidating related queries on success
|
||||
- [ ] Optimistic update missing rollback in `onError`
|
||||
- [ ] Using v4 array syntax (`useQuery(['key'], fn)`) instead of v5 object syntax
|
||||
|
||||
**Testing:**
|
||||
- [ ] Using `container.querySelector` instead of `screen.getByRole`
|
||||
- [ ] Using `fireEvent` instead of `userEvent`
|
||||
- [ ] Testing implementation details instead of user-visible behavior
|
||||
- [ ] Using `getBy*` for async content (use `findBy*`)
|
||||
|
||||
**Full guide:** [React Review Guide](react.md)
|
||||
|
||||
## Vue 3
|
||||
|
||||
- [ ] Destructuring `reactive()` object loses reactivity (use `toRefs`)
|
||||
- [ ] Passing `props.x` to composable instead of `() => props.x` or `toRef(props, 'x')`
|
||||
- [ ] `watch` with async callback missing `onCleanup` (race condition)
|
||||
- [ ] `computed` with side effects (mutations, API calls)
|
||||
- [ ] `v-for` using index as `:key` when list can reorder
|
||||
- [ ] `v-if` and `v-for` on the same element
|
||||
- [ ] `defineProps` without TypeScript type declaration
|
||||
- [ ] `withDefaults` object default values not using factory functions
|
||||
- [ ] Directly mutating props instead of emitting events
|
||||
- [ ] `watchEffect` with unclear dependencies causing over-triggering
|
||||
|
||||
**Full guide:** [Vue 3 Review Guide](vue.md)
|
||||
|
||||
## Python
|
||||
|
||||
- [ ] Mutable default arguments (`def f(x=[])`)
|
||||
- [ ] Bare `except:` catching `KeyboardInterrupt` and `SystemExit`
|
||||
- [ ] Shared mutable class attributes (`class C: items = []`)
|
||||
- [ ] Using `is` instead of `==` for value comparison
|
||||
- [ ] Forgetting `self` parameter in methods
|
||||
- [ ] Modifying list while iterating
|
||||
- [ ] String concatenation in loops (use `"".join()`)
|
||||
- [ ] Not closing files (use `with` statement)
|
||||
- [ ] Missing type annotations on public functions
|
||||
|
||||
**Full guide:** [Python Review Guide](python.md)
|
||||
|
||||
## Rust
|
||||
|
||||
**Ownership & Borrowing:**
|
||||
- [ ] Unnecessary `clone()` to work around borrow checker
|
||||
- [ ] `Arc<Mutex<T>>` when single-owner would suffice
|
||||
- [ ] Storing borrows in structs when owned data is simpler
|
||||
- [ ] Unnecessary `RefCell` (runtime checks vs compile-time)
|
||||
|
||||
**Unsafe Code:**
|
||||
- [ ] `unsafe` block without `SAFETY:` comment explaining invariants
|
||||
- [ ] `unsafe fn` without `# Safety` doc section
|
||||
- [ ] Unsafe invariants split across modules
|
||||
|
||||
**Async & Concurrency:**
|
||||
- [ ] Blocking in async context (`std::fs`, `std::thread::sleep`)
|
||||
- [ ] Holding `std::sync::Mutex` across `.await`
|
||||
- [ ] Spawned task missing `'static` lifetime bound
|
||||
- [ ] Dropping a Future without awaiting (forgotten work)
|
||||
|
||||
**Error Handling:**
|
||||
- [ ] `unwrap()`/`expect()` in production code
|
||||
- [ ] Library using `anyhow` instead of `thiserror` (callers can't match)
|
||||
- [ ] Swallowing error context (`map_err(|_| ...)`)
|
||||
- [ ] Ignoring `must_use` return values
|
||||
|
||||
**Performance:**
|
||||
- [ ] Unnecessary `.collect()` — prefer lazy iterators
|
||||
- [ ] String concatenation in loops without `with_capacity`
|
||||
- [ ] `Box<dyn Trait>` when `impl Trait` would work
|
||||
|
||||
**Full guide:** [Rust Review Guide](rust.md)
|
||||
|
||||
## Go
|
||||
|
||||
- [ ] Ignoring errors (`result, _ := SomeFunction()`)
|
||||
- [ ] Goroutine with no exit mechanism (leak)
|
||||
- [ ] Missing or incorrect `context.Context` propagation
|
||||
- [ ] Loop variable capture issue (Go < 1.22)
|
||||
- [ ] `defer` in loops (deferred until function, not loop iteration)
|
||||
- [ ] Variable shadowing
|
||||
- [ ] Map used before initialization
|
||||
- [ ] Error wrapping with `%v` instead of `%w` (breaks `errors.Is`/`errors.As`)
|
||||
|
||||
**Full guide:** [Go Review Guide](go.md)
|
||||
|
||||
## Java / Spring Boot
|
||||
|
||||
- [ ] POJO/DTO with manual boilerplate instead of `record`
|
||||
- [ ] Traditional switch missing `break` (use switch expressions)
|
||||
- [ ] Field injection instead of constructor injection
|
||||
- [ ] JPA N+1 query (missing `fetch join` or `@EntityGraph`)
|
||||
- [ ] Incorrect `equals`/`hashCode` on JPA entities (use business key, not ID)
|
||||
- [ ] `Optional.get()` without `isPresent()` check
|
||||
- [ ] Stream operations with side effects
|
||||
|
||||
**Full guide:** [Java Review Guide](java.md)
|
||||
|
||||
## PHP
|
||||
|
||||
- [ ] Missing `declare(strict_types=1);` in new files
|
||||
- [ ] Weak comparison (`==`, `!=`) in auth, token, payment, or state logic
|
||||
- [ ] `in_array()` / `array_search()` used without strict mode
|
||||
- [ ] SQL built with string concatenation instead of prepared statements
|
||||
- [ ] User input echoed without context-aware escaping
|
||||
- [ ] Passwords stored with `md5()` / `sha1()` instead of `password_hash()`
|
||||
- [ ] Untrusted data passed to `unserialize()`
|
||||
- [ ] PHP 8.2+ dynamic properties used instead of declared properties
|
||||
- [ ] Errors hidden with `@` or swallowed in empty `catch` blocks
|
||||
- [ ] File uploads using client-provided names or missing MIME/size validation
|
||||
|
||||
**Full guide:** [PHP Review Guide](php.md)
|
||||
|
||||
## Swift
|
||||
|
||||
- [ ] Force-unwrap (`!`) or `try!` where safe unwrapping is possible
|
||||
- [ ] Closure capturing `self` strongly without `[weak self]` (retain cycle)
|
||||
- [ ] Reference type (`class`) used where a value type (`struct`) is intended
|
||||
- [ ] Errors swallowed instead of propagated via `throws` / `Result`
|
||||
- [ ] Data race across concurrency boundaries (missing `Sendable`, `@MainActor`, actor isolation)
|
||||
- [ ] Fire-and-forget `Task {}` that is never cancelled or leaks
|
||||
- [ ] `@ObservedObject` used where `@StateObject` is required for ownership
|
||||
- [ ] Implicitly unwrapped optional (`var x: T!`) outside IBOutlets
|
||||
- [ ] Over-broad access control (`public` / `open` where `internal` suffices)
|
||||
|
||||
**Full guide:** [Swift Review Guide](swift.md)
|
||||
|
||||
## C
|
||||
|
||||
- [ ] Pointer/buffer overflow or underflow
|
||||
- [ ] Undefined behavior (use-after-free, double-free, null deref)
|
||||
- [ ] Missing error handling after allocation (`malloc` can return `NULL`)
|
||||
- [ ] Integer overflow in size calculations
|
||||
- [ ] Resource leaks (missing `free`, `fclose`, etc.)
|
||||
- [ ] Missing `static` on file-local functions/variables
|
||||
|
||||
**Full guide:** [C Review Guide](c.md)
|
||||
|
||||
## C++
|
||||
|
||||
- [ ] Missing RAII wrapper for resources
|
||||
- [ ] Violating Rule of 0/3/5 (destructor, copy, move)
|
||||
- [ ] Exception safety issues (no `noexcept` where applicable)
|
||||
- [ ] Dangling references from returned iterators or references
|
||||
- [ ] Unnecessary copies (missing `std::move` or pass-by-reference)
|
||||
|
||||
**Full guide:** [C++ Review Guide](cpp.md)
|
||||
|
||||
## SQL
|
||||
|
||||
- [ ] String concatenation for queries (SQL injection risk) — use parameterized queries
|
||||
- [ ] Missing indexes on filtered/joined columns
|
||||
- [ ] `SELECT *` instead of specific columns
|
||||
- [ ] N+1 query patterns
|
||||
- [ ] Missing `LIMIT` on large tables
|
||||
- [ ] Not handling `NULL` comparisons correctly (`IS NULL` vs `= NULL`)
|
||||
- [ ] Missing transactions for related operations
|
||||
- [ ] Incorrect JOIN types
|
||||
- [ ] Collation / case sensitivity surprises across databases (MySQL vs Postgres defaults)
|
||||
- [ ] Date and timezone handling errors (naive timestamps, server-local `NOW()`, DST)
|
||||
|
||||
**See also:** [Security Review Guide](security-review-guide.md) for SQL injection prevention
|
||||
|
||||
## API Design
|
||||
|
||||
- [ ] Inconsistent resource naming
|
||||
- [ ] Wrong HTTP methods (POST for idempotent operations)
|
||||
- [ ] Missing pagination for list endpoints
|
||||
- [ ] Incorrect status codes
|
||||
- [ ] Missing rate limiting
|
||||
- [ ] Missing input validation and sanitization
|
||||
- [ ] Trusting client-side validation only
|
||||
|
||||
## Testing
|
||||
|
||||
- [ ] Testing implementation details instead of behavior
|
||||
- [ ] Missing edge case tests
|
||||
- [ ] Flaky tests (non-deterministic)
|
||||
- [ ] Tests with external dependencies (no mocks)
|
||||
- [ ] Missing negative tests (error cases)
|
||||
- [ ] Overly complex test setup
|
||||
+385
@@ -0,0 +1,385 @@
|
||||
# C++ Code Review Guide
|
||||
|
||||
> C++ code review guide focused on memory safety, lifetime, API design, and performance. Examples assume C++17/20.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Ownership and RAII](#ownership-and-raii)
|
||||
- [Lifetime and References](#lifetime-and-references)
|
||||
- [Copy and Move Semantics](#copy-and-move-semantics)
|
||||
- [Const-Correctness and API Design](#const-correctness-and-api-design)
|
||||
- [Error Handling and Exception Safety](#error-handling-and-exception-safety)
|
||||
- [Concurrency](#concurrency)
|
||||
- [Performance and Allocation](#performance-and-allocation)
|
||||
- [Templates and Type Safety](#templates-and-type-safety)
|
||||
- [Tooling and Build Checks](#tooling-and-build-checks)
|
||||
- [Review Checklist](#review-checklist)
|
||||
|
||||
---
|
||||
|
||||
## Ownership and RAII
|
||||
|
||||
### Prefer RAII and smart pointers
|
||||
|
||||
Use RAII to express ownership. Default to `std::unique_ptr`, use `std::shared_ptr` only for shared lifetime.
|
||||
|
||||
```cpp
|
||||
// ❌ Bad: manual new/delete with early returns
|
||||
Foo* make_foo() {
|
||||
Foo* foo = new Foo();
|
||||
if (!foo->Init()) {
|
||||
delete foo;
|
||||
return nullptr;
|
||||
}
|
||||
return foo;
|
||||
}
|
||||
|
||||
// ✅ Good: RAII with unique_ptr
|
||||
std::unique_ptr<Foo> make_foo() {
|
||||
auto foo = std::make_unique<Foo>();
|
||||
if (!foo->Init()) {
|
||||
return {};
|
||||
}
|
||||
return foo;
|
||||
}
|
||||
```
|
||||
|
||||
### Wrap C resources
|
||||
|
||||
```cpp
|
||||
// ✅ Good: wrap FILE* with unique_ptr
|
||||
using FilePtr = std::unique_ptr<FILE, decltype(&fclose)>;
|
||||
|
||||
FilePtr open_file(const char* path) {
|
||||
return FilePtr(fopen(path, "rb"), &fclose);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Lifetime and References
|
||||
|
||||
### Avoid dangling references and views
|
||||
|
||||
`std::string_view` and `std::span` do not own data. Make sure the owner outlives the view.
|
||||
|
||||
```cpp
|
||||
// ❌ Bad: returning string_view to a temporary
|
||||
std::string_view bad_view() {
|
||||
std::string s = make_name();
|
||||
return s; // dangling
|
||||
}
|
||||
|
||||
// ✅ Good: return owning string
|
||||
std::string good_name() {
|
||||
return make_name();
|
||||
}
|
||||
|
||||
// ✅ Good: view tied to caller-owned data
|
||||
std::string_view good_view(const std::string& s) {
|
||||
return s;
|
||||
}
|
||||
```
|
||||
|
||||
### Lambda captures
|
||||
|
||||
```cpp
|
||||
// ❌ Bad: capture reference that escapes
|
||||
std::function<void()> make_task() {
|
||||
int value = 42;
|
||||
return [&]() { use(value); }; // dangling
|
||||
}
|
||||
|
||||
// ✅ Good: capture by value
|
||||
std::function<void()> make_task() {
|
||||
int value = 42;
|
||||
return [value]() { use(value); };
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Copy and Move Semantics
|
||||
|
||||
### Rule of 0/3/5
|
||||
|
||||
Prefer the Rule of 0 by using RAII types. If you own a resource, define or delete copy and move operations.
|
||||
|
||||
```cpp
|
||||
// ❌ Bad: raw ownership with default copy
|
||||
struct Buffer {
|
||||
int* data;
|
||||
size_t size;
|
||||
explicit Buffer(size_t n) : data(new int[n]), size(n) {}
|
||||
~Buffer() { delete[] data; }
|
||||
// copy ctor/assign are implicitly generated -> double delete
|
||||
};
|
||||
|
||||
// ✅ Good: Rule of 0 with std::vector
|
||||
struct Buffer {
|
||||
std::vector<int> data;
|
||||
explicit Buffer(size_t n) : data(n) {}
|
||||
};
|
||||
```
|
||||
|
||||
### Delete unwanted copies
|
||||
|
||||
```cpp
|
||||
struct Socket {
|
||||
Socket() = default;
|
||||
~Socket() { close(); }
|
||||
|
||||
Socket(const Socket&) = delete;
|
||||
Socket& operator=(const Socket&) = delete;
|
||||
Socket(Socket&&) noexcept = default;
|
||||
Socket& operator=(Socket&&) noexcept = default;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Const-Correctness and API Design
|
||||
|
||||
### Use const and explicit
|
||||
|
||||
```cpp
|
||||
class User {
|
||||
public:
|
||||
const std::string& name() const { return name_; }
|
||||
void set_name(std::string name) { name_ = std::move(name); }
|
||||
|
||||
private:
|
||||
std::string name_;
|
||||
};
|
||||
|
||||
struct Millis {
|
||||
explicit Millis(int v) : value(v) {}
|
||||
int value;
|
||||
};
|
||||
```
|
||||
|
||||
### Avoid object slicing
|
||||
|
||||
```cpp
|
||||
struct Shape { virtual ~Shape() = default; };
|
||||
struct Circle : Shape { void draw() const; };
|
||||
|
||||
// ❌ Bad: slices Circle into Shape
|
||||
void draw(Shape shape);
|
||||
|
||||
// ✅ Good: pass by reference
|
||||
void draw(const Shape& shape);
|
||||
```
|
||||
|
||||
### Use override and final
|
||||
|
||||
```cpp
|
||||
struct Base {
|
||||
virtual void run() = 0;
|
||||
};
|
||||
|
||||
struct Worker final : Base {
|
||||
void run() override {}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling and Exception Safety
|
||||
|
||||
### Prefer RAII for cleanup
|
||||
|
||||
```cpp
|
||||
// ✅ Good: RAII handles cleanup on exceptions
|
||||
void process() {
|
||||
std::vector<int> data = load_data(); // safe cleanup
|
||||
do_work(data);
|
||||
}
|
||||
```
|
||||
|
||||
### Do not throw from destructors
|
||||
|
||||
```cpp
|
||||
struct File {
|
||||
~File() noexcept { close(); }
|
||||
void close();
|
||||
};
|
||||
```
|
||||
|
||||
### Use expected results for normal failures
|
||||
|
||||
```cpp
|
||||
// ✅ Expected error: use optional or expected
|
||||
std::optional<int> parse_int(const std::string& s) {
|
||||
try {
|
||||
return std::stoi(s);
|
||||
} catch (...) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Concurrency
|
||||
|
||||
### Protect shared data
|
||||
|
||||
```cpp
|
||||
// ❌ Bad: data race
|
||||
int counter = 0;
|
||||
void inc() { counter++; }
|
||||
|
||||
// ✅ Good: atomic
|
||||
std::atomic<int> counter{0};
|
||||
void inc() { counter.fetch_add(1, std::memory_order_relaxed); }
|
||||
```
|
||||
|
||||
### Use RAII locks
|
||||
|
||||
```cpp
|
||||
std::mutex mu;
|
||||
std::vector<int> data;
|
||||
|
||||
void add(int v) {
|
||||
std::lock_guard<std::mutex> lock(mu);
|
||||
data.push_back(v);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance and Allocation
|
||||
|
||||
### Avoid repeated allocations
|
||||
|
||||
```cpp
|
||||
// ❌ Bad: repeated reallocation
|
||||
std::vector<int> build(int n) {
|
||||
std::vector<int> out;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
out.push_back(i);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// ✅ Good: reserve upfront
|
||||
std::vector<int> build(int n) {
|
||||
std::vector<int> out;
|
||||
out.reserve(static_cast<size_t>(n));
|
||||
for (int i = 0; i < n; ++i) {
|
||||
out.push_back(i);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
```
|
||||
|
||||
### String concatenation
|
||||
|
||||
```cpp
|
||||
// ❌ Bad: repeated allocation
|
||||
std::string join(const std::vector<std::string>& parts) {
|
||||
std::string out;
|
||||
for (const auto& p : parts) {
|
||||
out += p;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// ✅ Good: reserve total size
|
||||
std::string join(const std::vector<std::string>& parts) {
|
||||
size_t total = 0;
|
||||
for (const auto& p : parts) {
|
||||
total += p.size();
|
||||
}
|
||||
std::string out;
|
||||
out.reserve(total);
|
||||
for (const auto& p : parts) {
|
||||
out += p;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Templates and Type Safety
|
||||
|
||||
### Prefer constrained templates (C++20)
|
||||
|
||||
```cpp
|
||||
// ❌ Bad: overly generic
|
||||
template <typename T>
|
||||
T add(T a, T b) {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
// ✅ Good: constrained
|
||||
template <typename T>
|
||||
requires std::is_integral_v<T>
|
||||
T add(T a, T b) {
|
||||
return a + b;
|
||||
}
|
||||
```
|
||||
|
||||
### Use static_assert for invariants
|
||||
|
||||
```cpp
|
||||
template <typename T>
|
||||
struct Packet {
|
||||
static_assert(std::is_trivially_copyable_v<T>,
|
||||
"Packet payload must be trivially copyable");
|
||||
T payload;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tooling and Build Checks
|
||||
|
||||
```bash
|
||||
# Warnings
|
||||
clang++ -Wall -Wextra -Werror -Wconversion -Wshadow -std=c++20 ...
|
||||
|
||||
# Sanitizers (debug builds)
|
||||
clang++ -fsanitize=address,undefined -fno-omit-frame-pointer -g ...
|
||||
clang++ -fsanitize=thread -fno-omit-frame-pointer -g ...
|
||||
|
||||
# Static analysis
|
||||
clang-tidy src/*.cpp -- -std=c++20
|
||||
|
||||
# Formatting
|
||||
clang-format -i src/*.cpp include/*.h
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Review Checklist
|
||||
|
||||
### Safety and Lifetime
|
||||
- [ ] Ownership is explicit (RAII, unique_ptr by default)
|
||||
- [ ] No dangling references or views
|
||||
- [ ] Rule of 0/3/5 followed for resource-owning types
|
||||
- [ ] No raw new/delete in business logic
|
||||
- [ ] Destructors are noexcept and do not throw
|
||||
|
||||
### API and Design
|
||||
- [ ] const-correctness is applied consistently
|
||||
- [ ] Constructors are explicit where needed
|
||||
- [ ] Override/final used for virtual functions
|
||||
- [ ] No object slicing (pass by ref or pointer)
|
||||
|
||||
### Concurrency
|
||||
- [ ] Shared data is protected (mutex or atomics)
|
||||
- [ ] Locking order is consistent
|
||||
- [ ] No blocking while holding locks
|
||||
|
||||
### Performance
|
||||
- [ ] Unnecessary allocations avoided (reserve, move)
|
||||
- [ ] Copies avoided in hot paths
|
||||
- [ ] Algorithmic complexity is reasonable
|
||||
|
||||
### Tooling and Tests
|
||||
- [ ] Builds clean with warnings enabled
|
||||
- [ ] Sanitizers run on critical code paths
|
||||
- [ ] Static analysis (clang-tidy) results are addressed
|
||||
+521
@@ -0,0 +1,521 @@
|
||||
# C# / .NET Code Review Guide
|
||||
|
||||
> C# / .NET 8 代码审查指南,覆盖 C# 12 新特性、异步编程、EF Core 性能、ASP.NET Core 最佳实践、依赖注入、LINQ 等核心主题。
|
||||
|
||||
## 目录
|
||||
|
||||
- [C# 12 新特性](#c-12-新特性)
|
||||
- [异步编程](#异步编程)
|
||||
- [EF Core 性能](#ef-core-性能)
|
||||
- [ASP.NET Core 最佳实践](#aspnet-core-最佳实践)
|
||||
- [依赖注入](#依赖注入)
|
||||
- [LINQ 最佳实践](#linq-最佳实践)
|
||||
- [Review Checklist](#review-checklist)
|
||||
|
||||
---
|
||||
|
||||
## C# 12 新特性
|
||||
|
||||
### Primary Constructors(非 record 类型)
|
||||
|
||||
```csharp
|
||||
// ❌ 样板代码过多的传统构造函数
|
||||
public class ProductService
|
||||
{
|
||||
private readonly ProductDbContext _db;
|
||||
private readonly ILogger<ProductService> _logger;
|
||||
|
||||
public ProductService(ProductDbContext db, ILogger<ProductService> logger)
|
||||
{
|
||||
_db = db;
|
||||
_logger = logger;
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Primary Constructor——简洁的依赖注入
|
||||
public class ProductService(ProductDbContext db, ILogger<ProductService> logger)
|
||||
{
|
||||
public async Task<Product?> GetAsync(int id)
|
||||
=> await db.Products.FindAsync(id);
|
||||
}
|
||||
|
||||
// ⚠️ 注意:primary constructor 参数不是属性,不能被重新赋值
|
||||
// ⚠️ 如果需要长期存储,显式声明字段
|
||||
public class OrderService(OrderDbContext db)
|
||||
{
|
||||
private readonly OrderDbContext _db = db; // 显式捕获
|
||||
}
|
||||
```
|
||||
|
||||
### Collection Expressions
|
||||
|
||||
```csharp
|
||||
// ❌ 传统集合初始化
|
||||
int[] nums = new int[] { 1, 2, 3 };
|
||||
List<string> names = new List<string> { "alice", "bob" };
|
||||
|
||||
// ✅ 集合表达式
|
||||
int[] nums = [1, 2, 3];
|
||||
List<string> names = ["alice", "bob"];
|
||||
Span<char> span = ['a', 'b'];
|
||||
|
||||
// ✅ 展开运算符
|
||||
int[] merged = [..nums, 4, 5];
|
||||
```
|
||||
|
||||
### Default Lambda Parameters
|
||||
|
||||
```csharp
|
||||
// ❌ 重载 lambda
|
||||
var add = (int a, int b) => a + b;
|
||||
var addDefault = (int a) => a + 1;
|
||||
|
||||
// ✅ 默认参数
|
||||
var add = (int a, int b = 1) => a + b;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 异步编程
|
||||
|
||||
### Task.Wait() / .Result / async void 是严重反模式
|
||||
|
||||
```csharp
|
||||
// ❌ Task.Wait() —— 死锁风险(同步阻塞异步操作)
|
||||
public ActionResult<Data> Get(int id)
|
||||
{
|
||||
var data = _service.GetDataAsync(id).Result; // 死锁!
|
||||
return Ok(data);
|
||||
}
|
||||
|
||||
// ❌ async void —— 异常无法捕获,会崩溃进程
|
||||
public async void HandleEvent()
|
||||
{
|
||||
await _service.ProcessAsync(); // 异常直接崩溃
|
||||
}
|
||||
|
||||
// ✅ async Task —— 全链路异步
|
||||
public async Task<ActionResult<Data>> Get(int id)
|
||||
{
|
||||
var data = await _service.GetDataAsync(id);
|
||||
return Ok(data);
|
||||
}
|
||||
```
|
||||
|
||||
### ConfigureAwait(false) 用于库代码
|
||||
|
||||
```csharp
|
||||
// ❌ 库代码不必要地捕获 SynchronizationContext
|
||||
public class LibraryService
|
||||
{
|
||||
public async Task<string> GetDataAsync()
|
||||
{
|
||||
var response = await _httpClient.GetAsync("/api/data");
|
||||
return await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 库代码使用 ConfigureAwait(false) 避免死锁
|
||||
public class LibraryService
|
||||
{
|
||||
public async Task<string> GetDataAsync()
|
||||
{
|
||||
var response = await _httpClient.GetAsync("/api/data").ConfigureAwait(false);
|
||||
return await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### CancellationToken 传播
|
||||
|
||||
```csharp
|
||||
// ❌ 丢弃 CancellationToken
|
||||
public async Task<List<User>> SearchAsync(string query)
|
||||
{
|
||||
return await _db.Users.Where(u => u.Name.Contains(query)).ToListAsync();
|
||||
}
|
||||
|
||||
// ✅ 全链路传递 CancellationToken
|
||||
public async Task<List<User>> SearchAsync(string query, CancellationToken ct = default)
|
||||
{
|
||||
return await _db.Users
|
||||
.Where(u => u.Name.Contains(query))
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
```
|
||||
|
||||
### Async Disposal
|
||||
|
||||
```csharp
|
||||
// ❌ 同步 dispose 异步资源
|
||||
public class DataClient : IDisposable
|
||||
{
|
||||
public void Dispose()
|
||||
{
|
||||
_httpClient.Dispose(); // 可能丢弃正在进行的请求
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ IAsyncDisposable
|
||||
public class DataClient : IAsyncDisposable
|
||||
{
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await _stream.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 调用方使用 await using
|
||||
await using var client = new DataClient();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## EF Core 性能
|
||||
|
||||
### N+1 查询问题
|
||||
|
||||
```csharp
|
||||
// ❌ 经典 N+1——每个 Blog 触发一次查询获取 Posts
|
||||
foreach (var blog in await context.Blogs.ToListAsync())
|
||||
{
|
||||
foreach (var post in blog.Posts) // 每次循环都查询数据库!
|
||||
{
|
||||
Console.WriteLine(post.Title);
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Eager Loading + 投影
|
||||
await foreach (var blog in context.Blogs
|
||||
.Select(b => new { b.Url, b.Posts })
|
||||
.AsAsyncEnumerable())
|
||||
{
|
||||
foreach (var post in blog.Posts)
|
||||
Console.WriteLine(post.Title);
|
||||
}
|
||||
```
|
||||
|
||||
### 过度获取(不投影)
|
||||
|
||||
```csharp
|
||||
// ❌ 加载所有列——只需要 Url 时加载了全部字段
|
||||
var urls = await context.Blogs.ToListAsync();
|
||||
|
||||
// ✅ 只投影需要的字段
|
||||
var urls = await context.Blogs
|
||||
.Select(b => b.Url)
|
||||
.ToListAsync();
|
||||
```
|
||||
|
||||
### 缺少分页
|
||||
|
||||
```csharp
|
||||
// ❌ 无界结果集
|
||||
var posts = await context.Posts
|
||||
.Where(p => p.Title.StartsWith("A"))
|
||||
.ToListAsync(); // 可能有百万条记录!
|
||||
|
||||
// ✅ 限制结果数量
|
||||
var posts = await context.Posts
|
||||
.Where(p => p.Title.StartsWith("A"))
|
||||
.OrderBy(p => p.Id)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
```
|
||||
|
||||
### Cartesian Explosion(JOIN 笛卡尔爆炸)
|
||||
|
||||
```csharp
|
||||
// ❌ 多个 Include 创建大量重复数据
|
||||
var blogs = await context.Blogs
|
||||
.Include(b => b.Posts)
|
||||
.Include(b => b.Tags)
|
||||
.ToListAsync(); // 每行重复 Blog 数据
|
||||
|
||||
// ✅ 使用 AsSplitQuery 拆分查询
|
||||
var blogs = await context.Blogs
|
||||
.Include(b => b.Posts)
|
||||
.Include(b => b.Tags)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
```
|
||||
|
||||
### 只读场景缺少 AsNoTracking
|
||||
|
||||
```csharp
|
||||
// ❌ 默认跟踪——只读查询也付出跟踪开销
|
||||
var products = await context.Products.ToListAsync();
|
||||
|
||||
// ✅ AsNoTracking——跳过变更跟踪,更快且更省内存
|
||||
var products = await context.Products
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
```
|
||||
|
||||
### 列上函数阻止索引使用
|
||||
|
||||
```csharp
|
||||
// ✅ 可以使用索引——sargable
|
||||
var posts1 = await context.Posts
|
||||
.Where(p => p.Title.StartsWith("A"))
|
||||
.ToListAsync();
|
||||
|
||||
// ❌ 无法使用索引——全表扫描
|
||||
var posts2 = await context.Posts
|
||||
.Where(p => p.Title.EndsWith("A"))
|
||||
.ToListAsync();
|
||||
|
||||
// ❌ 列上套函数——全表扫描
|
||||
var posts3 = await context.Posts
|
||||
.Where(p => p.Title.ToLower() == "foo")
|
||||
.ToListAsync();
|
||||
```
|
||||
|
||||
### 同步 vs 异步数据库访问
|
||||
|
||||
```csharp
|
||||
// ❌ 同步数据库调用——阻塞线程
|
||||
var products = context.Products.ToList();
|
||||
context.SaveChanges();
|
||||
|
||||
// ✅ 异步数据库调用
|
||||
var products = await context.Products.ToListAsync();
|
||||
await context.SaveChangesAsync();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ASP.NET Core 最佳实践
|
||||
|
||||
### HttpClient 误用
|
||||
|
||||
```csharp
|
||||
// ❌ 每次请求创建新的 HttpClient——socket 耗尽
|
||||
using var client = new HttpClient();
|
||||
var response = await client.GetAsync("https://api.example.com/data");
|
||||
|
||||
// ✅ IHttpClientFactory 注入
|
||||
public class MyService
|
||||
{
|
||||
private readonly HttpClient _client;
|
||||
public MyService(HttpClient client) => _client = client; // 从工厂注入
|
||||
}
|
||||
```
|
||||
|
||||
### HttpContext 在后台线程中使用
|
||||
|
||||
```csharp
|
||||
// ❌ 在后台任务中捕获 scoped 服务——请求结束后已释放
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await context.SaveChangesAsync(); // ObjectDisposedException!
|
||||
});
|
||||
|
||||
// ✅ 创建新的 scope
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await using var scope = serviceScopeFactory.CreateAsyncScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
await db.SaveChangesAsync();
|
||||
});
|
||||
```
|
||||
|
||||
### Request.Form 同步访问
|
||||
|
||||
```csharp
|
||||
// ❌ 同步读取 Form——sync over async
|
||||
var form = HttpContext.Request.Form;
|
||||
|
||||
// ✅ 异步读取
|
||||
var form = await HttpContext.Request.ReadFormAsync();
|
||||
```
|
||||
|
||||
### 异常用于控制流
|
||||
|
||||
```csharp
|
||||
// ❌ 用异常判断是否存在——异常开销大,比直接检查慢得多
|
||||
try
|
||||
{
|
||||
var user = await _db.Users.FirstAsync(u => u.Id == id);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
// ✅ 使用检查而非异常
|
||||
var user = await _db.Users.FirstOrDefaultAsync(u => u.Id == id);
|
||||
if (user is null) return NotFound();
|
||||
```
|
||||
|
||||
### 响应头在 Body 之后设置
|
||||
|
||||
```csharp
|
||||
// ❌ body 已发送后再设置 header——抛异常
|
||||
await next(context);
|
||||
context.Response.Headers["X-Custom"] = "value"; // 可能抛异常!
|
||||
|
||||
// ✅ 使用 OnStarting 回调
|
||||
context.Response.OnStarting(() =>
|
||||
{
|
||||
context.Response.Headers["X-Custom"] = "value";
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
await next(context);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 依赖注入
|
||||
|
||||
### Scoped 服务注入 Singleton
|
||||
|
||||
```csharp
|
||||
// ❌ Scoped 服务注入 Singleton——生命周期不匹配
|
||||
services.AddSingleton<BackgroundWorker>();
|
||||
services.AddScoped<IUserRepository, UserRepository>();
|
||||
|
||||
// BackgroundWorker 是 Singleton,UserRepository 是 Scoped
|
||||
// → UserRepository 在多个请求间共享或已释放
|
||||
|
||||
// ✅ 在 Singleton 中通过 IServiceProvider 创建 scope
|
||||
public class BackgroundWorker : BackgroundService
|
||||
{
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
|
||||
public BackgroundWorker(IServiceScopeFactory scopeFactory)
|
||||
=> _scopeFactory = scopeFactory;
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken ct)
|
||||
{
|
||||
await using var scope = _scopeFactory.CreateAsyncScope();
|
||||
var repo = scope.ServiceProvider.GetRequiredService<IUserRepository>();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## LINQ 最佳实践
|
||||
|
||||
### ToList 之后再 LINQ
|
||||
|
||||
```csharp
|
||||
// ❌ 先 ToList 再过滤——全表加载到内存
|
||||
var results = context.Posts
|
||||
.Where(p => p.Title.StartsWith("A"))
|
||||
.ToList()
|
||||
.Where(p => SomeClientFilter(p)); // 客户端过滤,已加载全部行
|
||||
|
||||
// ✅ 尽可能让数据库执行过滤
|
||||
var results = await context.Posts
|
||||
.Where(p => p.Title.StartsWith("A") && SomeDbFilter(p))
|
||||
.AsAsyncEnumerable()
|
||||
.Where(p => SomeClientFilter(p)) // 只过滤数据库返回的行
|
||||
.ToListAsync();
|
||||
```
|
||||
|
||||
### Count() vs Any()
|
||||
|
||||
```csharp
|
||||
// ❌ Count() 执行完整查询
|
||||
if (context.Users.Count() > 0) { /* ... */ }
|
||||
|
||||
// ✅ Any() 更高效——遇到第一条记录就返回
|
||||
if (await context.Users.AnyAsync()) { /* ... */ }
|
||||
```
|
||||
|
||||
### 多次枚举 IEnumerable
|
||||
|
||||
```csharp
|
||||
// ❌ IEnumerable 被枚举两次
|
||||
public void Process(IEnumerable<int> numbers)
|
||||
{
|
||||
if (numbers.Any()) // 第一次枚举
|
||||
{
|
||||
foreach (var n in numbers) // 第二次枚举(可能是重新查询)
|
||||
{
|
||||
Console.WriteLine(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 如果需要多次使用,先物化
|
||||
public void Process(IEnumerable<int> numbers)
|
||||
{
|
||||
var list = numbers.ToList(); // 只枚举一次
|
||||
if (list.Any())
|
||||
{
|
||||
foreach (var n in list)
|
||||
{
|
||||
Console.WriteLine(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Select 中的副作用
|
||||
|
||||
```csharp
|
||||
// ❌ Select 中执行副作用——不可预测的执行时机
|
||||
var results = users.Select(u =>
|
||||
{
|
||||
_logger.LogInformation($"Processing {u.Name}"); // 副作用!
|
||||
return u.Email;
|
||||
}).ToList();
|
||||
|
||||
// ✅ 副作用放在 foreach 中
|
||||
foreach (var user in users)
|
||||
{
|
||||
_logger.LogInformation("Processing {Name}", user.Name);
|
||||
}
|
||||
var results = users.Select(u => u.Email).ToList();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Review Checklist
|
||||
|
||||
### C# 12 新特性
|
||||
|
||||
- [ ] Primary constructor 参数不被重新赋值
|
||||
- [ ] 集合表达式语法一致(不混用新旧风格)
|
||||
|
||||
### 异步编程
|
||||
|
||||
- [ ] 无 `Task.Wait()`、`.Result`、`async void`
|
||||
- [ ] 库代码使用 `ConfigureAwait(false)`
|
||||
- [ ] `CancellationToken` 全链路传递
|
||||
- [ ] 异步资源使用 `IAsyncDisposable` / `await using`
|
||||
- [ ] 不混用同步和异步数据访问
|
||||
|
||||
### EF Core
|
||||
|
||||
- [ ] 无 N+1 查询(导航属性在循环中访问)
|
||||
- [ ] 投影 `Select()` 避免过度获取
|
||||
- [ ] 分页:`ToListAsync()` 前有 `Take()`/`Skip()`
|
||||
- [ ] 多个 `Include()` 使用 `AsSplitQuery()`
|
||||
- [ ] 只读查询使用 `AsNoTracking()`
|
||||
- [ ] 列上无函数调用阻止索引使用
|
||||
- [ ] 数据库调用全部异步
|
||||
|
||||
### ASP.NET Core
|
||||
|
||||
- [ ] HttpClient 通过 `IHttpClientFactory` 获取
|
||||
- [ ] 后台任务中不直接使用 scoped 服务
|
||||
- [ ] 使用 `ReadFormAsync` 代替 `Request.Form`
|
||||
- [ ] 异常不用于控制流
|
||||
- [ ] 响应头通过 `OnStarting` 设置
|
||||
|
||||
### 依赖注入
|
||||
|
||||
- [ ] Scoped 服务不注入 Singleton
|
||||
- [ ] 后台任务创建新 scope
|
||||
|
||||
### LINQ
|
||||
|
||||
- [ ] 无不必要的 `ToList()` 后再 LINQ
|
||||
- [ ] `Any()` 代替 `Count() > 0`
|
||||
- [ ] IEnumerable 不被多次枚举(或先物化)
|
||||
- [ ] Select 中无副作用
|
||||
+661
@@ -0,0 +1,661 @@
|
||||
# CSS / Less / Sass Review Guide
|
||||
|
||||
CSS 及预处理器代码审查指南,覆盖性能、可维护性、响应式设计和浏览器兼容性。
|
||||
|
||||
## CSS 变量 vs 硬编码
|
||||
|
||||
### 应该使用变量的场景
|
||||
|
||||
```css
|
||||
/* ❌ 硬编码 - 难以维护 */
|
||||
.button {
|
||||
background: #3b82f6;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.card {
|
||||
border: 1px solid #3b82f6;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* ✅ 使用 CSS 变量 */
|
||||
:root {
|
||||
--color-primary: #3b82f6;
|
||||
--radius-md: 8px;
|
||||
}
|
||||
.button {
|
||||
background: var(--color-primary);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
.card {
|
||||
border: 1px solid var(--color-primary);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
```
|
||||
|
||||
### 变量命名规范
|
||||
|
||||
```css
|
||||
/* 推荐的变量分类 */
|
||||
:root {
|
||||
/* 颜色 */
|
||||
--color-primary: #3b82f6;
|
||||
--color-primary-hover: #2563eb;
|
||||
--color-text: #1f2937;
|
||||
--color-text-muted: #6b7280;
|
||||
--color-bg: #ffffff;
|
||||
--color-border: #e5e7eb;
|
||||
|
||||
/* 间距 */
|
||||
--spacing-xs: 4px;
|
||||
--spacing-sm: 8px;
|
||||
--spacing-md: 16px;
|
||||
--spacing-lg: 24px;
|
||||
--spacing-xl: 32px;
|
||||
|
||||
/* 字体 */
|
||||
--font-size-sm: 14px;
|
||||
--font-size-base: 16px;
|
||||
--font-size-lg: 18px;
|
||||
--font-weight-normal: 400;
|
||||
--font-weight-bold: 700;
|
||||
|
||||
/* 圆角 */
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 8px;
|
||||
--radius-lg: 12px;
|
||||
--radius-full: 9999px;
|
||||
|
||||
/* 阴影 */
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
|
||||
/* 过渡 */
|
||||
--transition-fast: 150ms ease;
|
||||
--transition-normal: 300ms ease;
|
||||
}
|
||||
```
|
||||
|
||||
### 变量作用域建议
|
||||
|
||||
```css
|
||||
/* ✅ 组件级变量 - 减少全局污染 */
|
||||
.card {
|
||||
--card-padding: var(--spacing-md);
|
||||
--card-radius: var(--radius-md);
|
||||
|
||||
padding: var(--card-padding);
|
||||
border-radius: var(--card-radius);
|
||||
}
|
||||
|
||||
/* ⚠️ 避免频繁用 JS 动态修改变量 - 影响性能 */
|
||||
```
|
||||
|
||||
### 审查清单
|
||||
|
||||
- [ ] 颜色值是否使用变量?
|
||||
- [ ] 间距是否来自设计系统?
|
||||
- [ ] 重复值是否提取为变量?
|
||||
- [ ] 变量命名是否语义化?
|
||||
|
||||
---
|
||||
|
||||
## !important 使用规范
|
||||
|
||||
### 何时可以使用
|
||||
|
||||
```css
|
||||
/* ✅ 工具类 - 明确需要覆盖 */
|
||||
.hidden { display: none !important; }
|
||||
.sr-only { position: absolute !important; }
|
||||
|
||||
/* ✅ 覆盖第三方库样式(无法修改源码时) */
|
||||
.third-party-modal {
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
|
||||
/* ✅ 打印样式 */
|
||||
@media print {
|
||||
.no-print { display: none !important; }
|
||||
}
|
||||
```
|
||||
|
||||
### 何时禁止使用
|
||||
|
||||
```css
|
||||
/* ❌ 解决特异性问题 - 应该重构选择器 */
|
||||
.button {
|
||||
background: blue !important; /* 为什么需要 !important? */
|
||||
}
|
||||
|
||||
/* ❌ 覆盖自己写的样式 */
|
||||
.card { padding: 20px; }
|
||||
.card { padding: 30px !important; } /* 直接修改原规则 */
|
||||
|
||||
/* ❌ 在组件样式中 */
|
||||
.my-component .title {
|
||||
font-size: 24px !important; /* 破坏组件封装 */
|
||||
}
|
||||
```
|
||||
|
||||
### 替代方案
|
||||
|
||||
```css
|
||||
/* 问题:需要覆盖 .btn 的样式 */
|
||||
|
||||
/* ❌ 使用 !important */
|
||||
.my-btn {
|
||||
background: red !important;
|
||||
}
|
||||
|
||||
/* ✅ 提高特异性 */
|
||||
button.my-btn {
|
||||
background: red;
|
||||
}
|
||||
|
||||
/* ✅ 使用更具体的选择器 */
|
||||
.container .my-btn {
|
||||
background: red;
|
||||
}
|
||||
|
||||
/* ✅ 使用 :where() 降低被覆盖样式的特异性 */
|
||||
:where(.btn) {
|
||||
background: blue; /* 特异性为 0 */
|
||||
}
|
||||
.my-btn {
|
||||
background: red; /* 可以正常覆盖 */
|
||||
}
|
||||
```
|
||||
|
||||
### 审查问题
|
||||
|
||||
```markdown
|
||||
🔴 [blocking] "发现 15 处 !important,请说明每处的必要性"
|
||||
🟡 [important] "这个 !important 可以通过调整选择器特异性来解决"
|
||||
💡 [suggestion] "考虑使用 CSS Layers (@layer) 来管理样式优先级"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 性能考虑
|
||||
|
||||
### 🔴 高危性能问题
|
||||
|
||||
#### 1. `transition: all` 问题
|
||||
|
||||
```css
|
||||
/* ❌ 性能杀手 - 浏览器检查所有可动画属性 */
|
||||
.button {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* ✅ 明确指定属性 */
|
||||
.button {
|
||||
transition: background-color 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
|
||||
/* ✅ 多属性时使用变量 */
|
||||
.button {
|
||||
--transition-duration: 0.3s;
|
||||
transition:
|
||||
background-color var(--transition-duration) ease,
|
||||
box-shadow var(--transition-duration) ease,
|
||||
transform var(--transition-duration) ease;
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. box-shadow 动画
|
||||
|
||||
```css
|
||||
/* ❌ 每帧触发重绘 - 严重影响性能 */
|
||||
.card {
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
transition: box-shadow 0.3s ease;
|
||||
}
|
||||
.card:hover {
|
||||
box-shadow: 0 8px 16px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
/* ✅ 使用伪元素 + opacity */
|
||||
.card {
|
||||
position: relative;
|
||||
}
|
||||
.card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
box-shadow: 0 8px 16px rgba(0,0,0,0.2);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
pointer-events: none;
|
||||
border-radius: inherit;
|
||||
}
|
||||
.card:hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. 触发布局(Reflow)的属性
|
||||
|
||||
```css
|
||||
/* ❌ 动画这些属性会触发布局重计算 */
|
||||
.bad-animation {
|
||||
transition: width 0.3s, height 0.3s, top 0.3s, left 0.3s, margin 0.3s;
|
||||
}
|
||||
|
||||
/* ✅ 只动画 transform 和 opacity(仅触发合成) */
|
||||
.good-animation {
|
||||
transition: transform 0.3s, opacity 0.3s;
|
||||
}
|
||||
|
||||
/* 位移用 translate 代替 top/left */
|
||||
.move {
|
||||
transform: translateX(100px); /* ✅ */
|
||||
/* left: 100px; */ /* ❌ */
|
||||
}
|
||||
|
||||
/* 缩放用 scale 代替 width/height */
|
||||
.grow {
|
||||
transform: scale(1.1); /* ✅ */
|
||||
/* width: 110%; */ /* ❌ */
|
||||
}
|
||||
```
|
||||
|
||||
### 🟡 中等性能问题
|
||||
|
||||
#### 复杂选择器
|
||||
|
||||
```css
|
||||
/* ❌ 过深的嵌套 - 选择器匹配慢 */
|
||||
.page .container .content .article .section .paragraph span {
|
||||
color: red;
|
||||
}
|
||||
|
||||
/* ✅ 扁平化 */
|
||||
.article-text {
|
||||
color: red;
|
||||
}
|
||||
|
||||
/* ❌ 通配符选择器 */
|
||||
* { box-sizing: border-box; } /* 影响所有元素 */
|
||||
[class*="icon-"] { display: inline; } /* 属性选择器较慢 */
|
||||
|
||||
/* ✅ 限制范围 */
|
||||
.icon-box * { box-sizing: border-box; }
|
||||
```
|
||||
|
||||
#### 大量阴影和滤镜
|
||||
|
||||
```css
|
||||
/* ⚠️ 复杂阴影影响渲染性能 */
|
||||
.heavy-shadow {
|
||||
box-shadow:
|
||||
0 1px 2px rgba(0,0,0,0.1),
|
||||
0 2px 4px rgba(0,0,0,0.1),
|
||||
0 4px 8px rgba(0,0,0,0.1),
|
||||
0 8px 16px rgba(0,0,0,0.1),
|
||||
0 16px 32px rgba(0,0,0,0.1); /* 5 层阴影 */
|
||||
}
|
||||
|
||||
/* ⚠️ 滤镜消耗 GPU */
|
||||
.blur-heavy {
|
||||
filter: blur(20px) brightness(1.2) contrast(1.1);
|
||||
backdrop-filter: blur(10px); /* 更消耗性能 */
|
||||
}
|
||||
```
|
||||
|
||||
### 性能优化建议
|
||||
|
||||
```css
|
||||
/* 使用 will-change 提示浏览器(谨慎使用) */
|
||||
.animated-element {
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
/* 动画完成后移除 will-change */
|
||||
.animated-element.idle {
|
||||
will-change: auto;
|
||||
}
|
||||
|
||||
/* 使用 contain 限制重绘范围 */
|
||||
.card {
|
||||
contain: layout paint; /* 告诉浏览器内部变化不影响外部 */
|
||||
}
|
||||
```
|
||||
|
||||
### 审查清单
|
||||
|
||||
- [ ] 是否使用 `transition: all`?
|
||||
- [ ] 是否动画 width/height/top/left?
|
||||
- [ ] box-shadow 是否被动画?
|
||||
- [ ] 选择器嵌套是否超过 3 层?
|
||||
- [ ] 是否有不必要的 `will-change`?
|
||||
|
||||
---
|
||||
|
||||
## 响应式设计检查点
|
||||
|
||||
### Mobile First 原则
|
||||
|
||||
```css
|
||||
/* ✅ Mobile First - 基础样式针对移动端 */
|
||||
.container {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 逐步增强 */
|
||||
@media (min-width: 768px) {
|
||||
.container {
|
||||
padding: 24px;
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.container {
|
||||
padding: 32px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* ❌ Desktop First - 需要覆盖更多样式 */
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
padding: 32px;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
@media (max-width: 1023px) {
|
||||
.container {
|
||||
padding: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.container {
|
||||
padding: 16px;
|
||||
flex-direction: column;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 断点建议
|
||||
|
||||
```css
|
||||
/* 推荐断点(基于内容而非设备) */
|
||||
:root {
|
||||
--breakpoint-sm: 640px; /* 大手机 */
|
||||
--breakpoint-md: 768px; /* 平板竖屏 */
|
||||
--breakpoint-lg: 1024px; /* 平板横屏/小笔记本 */
|
||||
--breakpoint-xl: 1280px; /* 桌面 */
|
||||
--breakpoint-2xl: 1536px; /* 大桌面 */
|
||||
}
|
||||
|
||||
/* 使用示例 */
|
||||
@media (min-width: 768px) { /* md */ }
|
||||
@media (min-width: 1024px) { /* lg */ }
|
||||
```
|
||||
|
||||
### 响应式审查清单
|
||||
|
||||
- [ ] 是否采用 Mobile First?
|
||||
- [ ] 断点是否基于内容断裂点而非设备?
|
||||
- [ ] 是否避免断点重叠?
|
||||
- [ ] 文字是否使用相对单位(rem/em)?
|
||||
- [ ] 触摸目标是否足够大(≥44px)?
|
||||
- [ ] 是否测试了横竖屏切换?
|
||||
|
||||
### 常见问题
|
||||
|
||||
```css
|
||||
/* ❌ 固定宽度 */
|
||||
.container {
|
||||
width: 1200px;
|
||||
}
|
||||
|
||||
/* ✅ 最大宽度 + 弹性 */
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
padding-inline: 16px;
|
||||
}
|
||||
|
||||
/* ❌ 固定高度的文本容器 */
|
||||
.text-box {
|
||||
height: 100px; /* 文字可能溢出 */
|
||||
}
|
||||
|
||||
/* ✅ 最小高度 */
|
||||
.text-box {
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
/* ❌ 小触摸目标 */
|
||||
.small-button {
|
||||
padding: 4px 8px; /* 太小,难以点击 */
|
||||
}
|
||||
|
||||
/* ✅ 足够的触摸区域 */
|
||||
.touch-button {
|
||||
min-height: 44px;
|
||||
min-width: 44px;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 浏览器兼容性
|
||||
|
||||
### 需要检查的特性
|
||||
|
||||
| 特性 | 兼容性 | 建议 |
|
||||
|------|--------|------|
|
||||
| CSS Grid | 现代浏览器 ✅ | IE 需要 Autoprefixer + 测试 |
|
||||
| Flexbox | 广泛支持 ✅ | 旧版需要前缀 |
|
||||
| CSS Variables | 现代浏览器 ✅ | IE 不支持,需要回退 |
|
||||
| `gap` (flexbox) | 较新 ⚠️ | Safari 14.1+ |
|
||||
| `:has()` | 较新 ⚠️ | Firefox 121+ |
|
||||
| `container queries` | 较新 ⚠️ | 2023 年后的浏览器 |
|
||||
| `@layer` | 较新 ⚠️ | 检查目标浏览器 |
|
||||
|
||||
### 回退策略
|
||||
|
||||
```css
|
||||
/* CSS 变量回退 */
|
||||
.button {
|
||||
background: #3b82f6; /* 回退值 */
|
||||
background: var(--color-primary); /* 现代浏览器 */
|
||||
}
|
||||
|
||||
/* Flexbox gap 回退 */
|
||||
.flex-container {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
/* 旧浏览器回退 */
|
||||
.flex-container > * + * {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
/* Grid 回退 */
|
||||
.grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
@supports (display: grid) {
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Autoprefixer 配置
|
||||
|
||||
```javascript
|
||||
// postcss.config.js
|
||||
module.exports = {
|
||||
plugins: [
|
||||
require('autoprefixer')({
|
||||
// 根据 browserslist 配置
|
||||
grid: 'autoplace', // 启用 Grid 前缀(IE 支持)
|
||||
flexbox: 'no-2009', // 只用现代 flexbox 语法
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
// package.json
|
||||
{
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead",
|
||||
"not ie 11" // 根据项目需求
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 审查清单
|
||||
|
||||
- [ ] 是否检查了 [Can I Use](https://caniuse.com)?
|
||||
- [ ] 新特性是否有回退方案?
|
||||
- [ ] 是否配置了 Autoprefixer?
|
||||
- [ ] browserslist 是否符合项目要求?
|
||||
- [ ] 是否在目标浏览器中测试?
|
||||
|
||||
---
|
||||
|
||||
## Less / Sass 特定问题
|
||||
|
||||
### 嵌套深度
|
||||
|
||||
```scss
|
||||
/* ❌ 过深嵌套 - 编译后选择器过长 */
|
||||
.page {
|
||||
.container {
|
||||
.content {
|
||||
.article {
|
||||
.title {
|
||||
color: red; // 编译为 .page .container .content .article .title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ✅ 最多 3 层 */
|
||||
.article {
|
||||
&__title {
|
||||
color: red;
|
||||
}
|
||||
|
||||
&__content {
|
||||
p { margin-bottom: 1em; }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Mixin vs Extend vs 变量
|
||||
|
||||
```scss
|
||||
@use 'sass:color';
|
||||
|
||||
/* 变量 - 用于单个值 */
|
||||
$primary-color: #3b82f6;
|
||||
|
||||
/* Mixin - 用于可配置的代码块 */
|
||||
@mixin button-variant($bg, $text) {
|
||||
background: $bg;
|
||||
color: $text;
|
||||
&:hover {
|
||||
// Dart Sass 已弃用全局 darken()/lighten(),改用 color 模块
|
||||
background: color.adjust($bg, $lightness: -10%);
|
||||
// color.scale($bg, $lightness: -10%) 按比例调整,深浅过渡更自然
|
||||
}
|
||||
}
|
||||
|
||||
/* Extend - 用于共享相同样式(谨慎使用) */
|
||||
%visually-hidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip-path: inset(50%); /* clip: rect() 已弃用,改用 clip-path */
|
||||
white-space: nowrap; /* 避免内容被挤成一列后撑开布局 */
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
@extend %visually-hidden;
|
||||
}
|
||||
|
||||
/* ⚠️ @extend 的问题 */
|
||||
// 可能产生意外的选择器组合
|
||||
// 不能在 @media 中使用
|
||||
// 优先使用 mixin
|
||||
```
|
||||
|
||||
### 审查清单
|
||||
|
||||
- [ ] 嵌套是否超过 3 层?
|
||||
- [ ] 是否滥用 @extend?
|
||||
- [ ] Mixin 是否过于复杂?
|
||||
- [ ] 编译后的 CSS 大小是否合理?
|
||||
|
||||
---
|
||||
|
||||
## 快速审查清单
|
||||
|
||||
### 🔴 必须修复
|
||||
|
||||
```markdown
|
||||
□ transition: all
|
||||
□ 动画 width/height/top/left/margin
|
||||
□ 大量 !important
|
||||
□ 硬编码的颜色/间距重复 >3 次
|
||||
□ 选择器嵌套 >4 层
|
||||
```
|
||||
|
||||
### 🟡 建议修复
|
||||
|
||||
```markdown
|
||||
□ 缺少响应式处理
|
||||
□ 使用 Desktop First
|
||||
□ 复杂 box-shadow 被动画
|
||||
□ 缺少浏览器兼容回退
|
||||
□ CSS 变量作用域过大
|
||||
```
|
||||
|
||||
### 🟢 优化建议
|
||||
|
||||
```markdown
|
||||
□ 可以使用 CSS Grid 简化布局
|
||||
□ 可以使用 CSS 变量提取重复值
|
||||
□ 可以使用 @layer 管理优先级
|
||||
□ 可以添加 contain 优化性能
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 工具推荐
|
||||
|
||||
| 工具 | 用途 |
|
||||
|------|------|
|
||||
| [Stylelint](https://stylelint.io/) | CSS 代码检查 |
|
||||
| [PurgeCSS](https://purgecss.com/) | 移除未使用 CSS |
|
||||
| [Autoprefixer](https://autoprefixer.github.io/) | 自动添加前缀 |
|
||||
| [CSS Stats](https://cssstats.com/) | 分析 CSS 统计 |
|
||||
| [Can I Use](https://caniuse.com/) | 浏览器兼容性查询 |
|
||||
|
||||
---
|
||||
|
||||
## 参考资源
|
||||
|
||||
- [CSS Performance Optimization - MDN](https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Performance/CSS)
|
||||
- [What a CSS Code Review Might Look Like - CSS-Tricks](https://css-tricks.com/what-a-css-code-review-might-look-like/)
|
||||
- [How to Animate Box-Shadow - Tobias Ahlin](https://tobiasahlin.com/blog/how-to-animate-box-shadow/)
|
||||
- [Media Query Fundamentals - MDN](https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/CSS_layout/Media_queries)
|
||||
- [Autoprefixer - GitHub](https://github.com/postcss/autoprefixer)
|
||||
+1030
File diff suppressed because it is too large
Load Diff
+584
@@ -0,0 +1,584 @@
|
||||
# FastAPI Code Review Guide
|
||||
|
||||
> FastAPI code review guide covering dependency injection (`Depends`), Pydantic v2 validation boundaries, async correctness, database session lifecycle and N+1, security, and a test-driven verification workflow that turns the reviewer's in-process test client into a tool for *proving* bugs rather than guessing at them.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Dependency Injection (`Depends`)](#dependency-injection-depends)
|
||||
- [Pydantic v2 Models & Validation](#pydantic-v2-models--validation)
|
||||
- [Async Correctness](#async-correctness)
|
||||
- [Database Sessions & N+1](#database-sessions--n1)
|
||||
- [Security](#security)
|
||||
- [Test-Driven Verification](#test-driven-verification)
|
||||
- [Review Checklist](#review-checklist)
|
||||
- [References](#references)
|
||||
|
||||
---
|
||||
|
||||
## Dependency Injection (`Depends`)
|
||||
|
||||
FastAPI's `Depends` is the seam that keeps routes thin and testable. Most review problems here come from doing real work in the route function instead of behind a dependency.
|
||||
|
||||
### Business logic belongs behind a dependency or service, not in the route
|
||||
|
||||
```python
|
||||
# ❌ Bad — DB access, auth, and business rules all inline in the route
|
||||
@app.get("/orders/{order_id}")
|
||||
async def get_order(order_id: int):
|
||||
conn = await asyncpg.connect(DATABASE_URL) # connection created per request
|
||||
row = await conn.fetchrow("SELECT * FROM orders WHERE id = $1", order_id)
|
||||
await conn.close()
|
||||
if row is None:
|
||||
raise HTTPException(404)
|
||||
return dict(row)
|
||||
|
||||
# ✅ Good — the route declares what it needs; the session is injected and pooled
|
||||
async def get_session() -> AsyncIterator[AsyncSession]:
|
||||
async with SessionLocal() as session:
|
||||
yield session
|
||||
|
||||
@app.get("/orders/{order_id}", response_model=OrderOut)
|
||||
async def get_order(order_id: int, session: AsyncSession = Depends(get_session)):
|
||||
order = await session.get(Order, order_id)
|
||||
if order is None:
|
||||
raise HTTPException(status_code=404, detail="Order not found")
|
||||
return order
|
||||
```
|
||||
|
||||
The injected version is also the version you can override in tests (see [Test-Driven Verification](#test-driven-verification)).
|
||||
|
||||
### `yield` dependencies must clean up, and cleanup runs even on error
|
||||
|
||||
```python
|
||||
# ❌ Bad — no cleanup; the session leaks if the route raises
|
||||
async def get_session() -> AsyncSession:
|
||||
return SessionLocal()
|
||||
|
||||
# ✅ Good — the context manager closes the session on success AND on exception
|
||||
async def get_session() -> AsyncIterator[AsyncSession]:
|
||||
async with SessionLocal() as session:
|
||||
yield session
|
||||
```
|
||||
|
||||
Review point: confirm any `yield` dependency holding a resource (DB session, file handle, lock) releases it through a context manager or `try/finally`, so an exception in the route does not leak it.
|
||||
|
||||
### Don't re-create singletons per request
|
||||
|
||||
```python
|
||||
# ❌ Bad — a new HTTP client (and connection pool) per request
|
||||
@app.get("/proxy")
|
||||
async def proxy(client: httpx.AsyncClient = Depends(lambda: httpx.AsyncClient())):
|
||||
...
|
||||
|
||||
# ✅ Good — one client for the app lifetime, injected by reference
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
app.state.http = httpx.AsyncClient()
|
||||
yield
|
||||
await app.state.http.aclose()
|
||||
|
||||
def get_http(request: Request) -> httpx.AsyncClient:
|
||||
return request.app.state.http
|
||||
```
|
||||
|
||||
### Prefer the `Annotated` form and async dependencies
|
||||
|
||||
Since FastAPI 0.95 the idiomatic way to declare a dependency is `Annotated[T, Depends(...)]`, not the default-value form. It is reusable across routes and plays well with type checkers. Also prefer `async def` dependencies: a sync (`def`) dependency runs in the threadpool, which is wasted overhead for a small non-I/O check.
|
||||
|
||||
```python
|
||||
# ⚠️ Older form — still works, but not the current idiom
|
||||
@app.get("/items")
|
||||
async def list_items(session: AsyncSession = Depends(get_session)): ...
|
||||
|
||||
# ✅ Good — Annotated form; define once, reuse everywhere
|
||||
SessionDep = Annotated[AsyncSession, Depends(get_session)]
|
||||
|
||||
@app.get("/items")
|
||||
async def list_items(session: SessionDep): ...
|
||||
```
|
||||
|
||||
### Use dependencies to validate existence and permissions — they're cached per request
|
||||
|
||||
A dependency is the natural place to answer "does this resource exist and may this caller touch it?" Pydantic validates *shape*; a dependency validates against the database. FastAPI caches each dependency's result within a single request, so chaining small dependencies costs nothing extra and removes duplicated lookups.
|
||||
|
||||
```python
|
||||
# ✅ Good — small dependencies chain; valid_post is resolved once per request
|
||||
async def valid_post(post_id: int, session: SessionDep) -> Post:
|
||||
post = await session.get(Post, post_id)
|
||||
if post is None:
|
||||
raise HTTPException(status_code=404, detail="Post not found")
|
||||
return post
|
||||
|
||||
async def owned_post(post: Annotated[Post, Depends(valid_post)], user: CurrentUser) -> Post:
|
||||
if post.owner_id != user.id:
|
||||
raise HTTPException(status_code=403, detail="Forbidden")
|
||||
return post
|
||||
|
||||
@app.delete("/posts/{post_id}", status_code=204)
|
||||
async def delete_post(post: Annotated[Post, Depends(owned_post)], session: SessionDep):
|
||||
await session.delete(post) # existence + ownership already enforced
|
||||
await session.commit()
|
||||
```
|
||||
|
||||
This is also the cleanest place to fix the auth-vs-authorization bug from the [Security](#security) section: the ownership check moves into a reusable `owned_post` dependency.
|
||||
|
||||
---
|
||||
|
||||
## Pydantic v2 Models & Validation
|
||||
|
||||
### Separate input and output models; never echo the ORM object directly
|
||||
|
||||
```python
|
||||
# ❌ Bad — response_model is the DB model, so hashed_password leaks to the client
|
||||
@app.post("/users", response_model=UserTable)
|
||||
async def create_user(user: UserTable): # also accepts client-set id, is_admin...
|
||||
...
|
||||
|
||||
# ✅ Good — distinct schemas draw the trust boundary
|
||||
class UserCreate(BaseModel):
|
||||
email: EmailStr
|
||||
password: str
|
||||
|
||||
class UserOut(BaseModel):
|
||||
id: int
|
||||
email: EmailStr
|
||||
model_config = ConfigDict(from_attributes=True) # read from ORM safely
|
||||
|
||||
@app.post("/users", response_model=UserOut, status_code=201)
|
||||
async def create_user(payload: UserCreate, session: AsyncSession = Depends(get_session)):
|
||||
...
|
||||
```
|
||||
|
||||
`response_model` is a filter, not just documentation — fields absent from the output model are stripped from the response. Reusing the DB model as the response is the most common way sensitive fields leak.
|
||||
|
||||
### Use distinct Create and Update schemas
|
||||
|
||||
```python
|
||||
# ❌ Bad — one schema for create and update means every field is required on PATCH
|
||||
class ItemSchema(BaseModel):
|
||||
name: str
|
||||
price: float
|
||||
|
||||
# ✅ Good — update is a partial; create requires the full payload
|
||||
class ItemCreate(BaseModel):
|
||||
name: str
|
||||
price: float = Field(gt=0)
|
||||
|
||||
class ItemUpdate(BaseModel):
|
||||
name: str | None = None
|
||||
price: float | None = Field(default=None, gt=0)
|
||||
```
|
||||
|
||||
### Validate at the boundary, not after the DB write
|
||||
|
||||
```python
|
||||
# ❌ Bad — negative quantity reaches the database before anything checks it
|
||||
@app.post("/cart")
|
||||
async def add_to_cart(item_id: int, quantity: int):
|
||||
await save(item_id, quantity) # quantity = -5 silently accepted
|
||||
|
||||
# ✅ Good — the type system rejects it before the handler body runs
|
||||
class CartLine(BaseModel):
|
||||
item_id: int
|
||||
quantity: int = Field(gt=0)
|
||||
|
||||
@app.post("/cart")
|
||||
async def add_to_cart(line: CartLine):
|
||||
await save(line.item_id, line.quantity)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Async Correctness
|
||||
|
||||
This is the axis on which FastAPI differs most from Django and Flask, and the one most worth a reviewer's attention. FastAPI's throughput comes from a single event loop interleaving many concurrent requests. That model only holds if the loop is **never blocked**: one synchronous call on the loop stalls *every* in-flight request, not just its own. Get this wrong across the codebase and FastAPI does not just lose its edge — it performs *worse* than a sync framework like Flask, because Flask's worker-per-request model has no shared loop to choke. The reviewer's job is to keep work on the loop genuinely non-blocking and to treat every escape hatch as a cost, not a fix.
|
||||
|
||||
### Never call blocking code inside an `async def` route
|
||||
|
||||
```python
|
||||
# ❌ Bad — blocking I/O on the loop freezes ALL concurrent requests, not just this one
|
||||
@app.get("/report")
|
||||
async def report():
|
||||
data = requests.get("https://slow-api.example.com").json() # blocking socket
|
||||
time.sleep(2) # blocks the loop
|
||||
return data
|
||||
|
||||
# ✅ Good — await a native-async client; the loop serves other requests meanwhile
|
||||
@app.get("/report")
|
||||
async def report(client: httpx.AsyncClient = Depends(get_http)):
|
||||
resp = await client.get("https://slow-api.example.com")
|
||||
return resp.json()
|
||||
```
|
||||
|
||||
### Prefer native-async SDKs over sync libraries
|
||||
|
||||
The right fix for blocking I/O is almost always a library that speaks `async` natively — not wrapping a sync one. Reach for the async client first; the threadpool is the last resort, not the default.
|
||||
|
||||
| Sync (blocks the loop) | Native-async replacement |
|
||||
|------------------------|--------------------------|
|
||||
| `requests` | `httpx.AsyncClient`, `aiohttp` |
|
||||
| `psycopg2` (sync) | `asyncpg`, SQLAlchemy async engine |
|
||||
| `redis-py` (sync) | `redis.asyncio` |
|
||||
| `pymongo` | `motor` |
|
||||
| `boto3` | `aioboto3` |
|
||||
|
||||
If you find `asyncio.run(...)`, a new event loop, or a manually started thread *inside* a route, that is a red flag — it's an attempt to bolt sync code onto the loop. `asyncio.run()` inside a running loop raises `RuntimeError` outright; the rest quietly burns the performance you adopted FastAPI for.
|
||||
|
||||
```python
|
||||
# ❌ Bad — spinning up a loop/thread to call an async SDK from a sync context
|
||||
@app.get("/users/{uid}")
|
||||
def get_user(uid: int):
|
||||
return asyncio.run(repo.fetch(uid)) # RuntimeError under the running loop
|
||||
|
||||
# ✅ Good — let the route be async and await the native client directly
|
||||
@app.get("/users/{uid}")
|
||||
async def get_user(uid: int):
|
||||
return await repo.fetch(uid)
|
||||
```
|
||||
|
||||
### The threadpool is a bounded escape hatch, not a default
|
||||
|
||||
A plain `def` route — and `run_in_threadpool(...)` — does not run on the loop; FastAPI runs it in a **bounded** worker threadpool (AnyIO's default cap is 40 threads). For an occasional, genuinely-unavoidable blocking call this is the correct tool:
|
||||
|
||||
```python
|
||||
from fastapi.concurrency import run_in_threadpool
|
||||
|
||||
@app.get("/legacy")
|
||||
async def legacy():
|
||||
return await run_in_threadpool(blocking_library_call) # only if no async SDK exists
|
||||
```
|
||||
|
||||
But it does not scale the way the loop does. Route every hot path through the threadpool and, under load, all workers block at once; further requests queue behind the cap and throughput collapses. Spawning your own threads or processes to "add concurrency" makes it worse: once live threads exceed the machine's core count, context-switch and GIL contention degrade performance sharply rather than improving it. The escape hatch is for the rare blocking dependency you cannot replace — not a substitute for choosing async SDKs.
|
||||
|
||||
Review heuristic: a `def` route is acceptable for a low-traffic endpoint with no async equivalent. A high-traffic endpoint doing blocking work in a `def` route (or via `run_in_threadpool`) is a scaling bug — flag it and ask for an async SDK.
|
||||
|
||||
### CPU-bound work belongs in a worker process, not the loop or the threadpool
|
||||
|
||||
Neither the event loop nor the threadpool helps CPU-bound work: under the GIL only one thread runs Python bytecode at a time, so a heavy computation blocks just as badly from a threadpool as from the loop. Offload it to a separate process (Celery, Arq, RQ, or `multiprocessing`).
|
||||
|
||||
```python
|
||||
# ❌ Bad — a CPU-heavy job pins a worker; throughput drops for everyone
|
||||
@app.post("/render")
|
||||
async def render(doc: Doc):
|
||||
return heavy_pdf_render(doc) # seconds of pure CPU on the loop
|
||||
|
||||
# ✅ Good — enqueue to a worker process; return a job handle
|
||||
@app.post("/render", status_code=202)
|
||||
async def render(doc: Doc):
|
||||
job = await queue.enqueue(heavy_pdf_render, doc)
|
||||
return {"job_id": job.id}
|
||||
```
|
||||
|
||||
### Don't fire-and-forget unawaited coroutines
|
||||
|
||||
```python
|
||||
# ❌ Bad — coroutine never awaited; the email is never sent (and no error surfaces)
|
||||
@app.post("/signup")
|
||||
async def signup(user: UserCreate):
|
||||
send_welcome_email(user.email) # returns a coroutine, silently dropped
|
||||
|
||||
# ✅ Good — defer post-response work with BackgroundTasks
|
||||
@app.post("/signup")
|
||||
async def signup(user: UserCreate, tasks: BackgroundTasks):
|
||||
tasks.add_task(send_welcome_email, user.email)
|
||||
```
|
||||
|
||||
`BackgroundTasks` runs in-process and offers no retries or persistence — use it only for short, fire-and-forget work (send an email, log an event). Anything long-running or retry-critical (data processing, payments) belongs in a real task queue (Celery/Arq/RQ).
|
||||
|
||||
---
|
||||
|
||||
## Database Sessions & N+1
|
||||
|
||||
### One session per request, injected — not a global
|
||||
|
||||
```python
|
||||
# ❌ Bad — a module-level session is shared across concurrent requests (not safe)
|
||||
session = SessionLocal()
|
||||
|
||||
# ✅ Good — request-scoped session via dependency (see get_session above)
|
||||
@app.get("/items")
|
||||
async def list_items(session: AsyncSession = Depends(get_session)):
|
||||
...
|
||||
```
|
||||
|
||||
### Eager-load relationships to avoid N+1
|
||||
|
||||
```python
|
||||
# ❌ Bad — one query for orders, then one query per order for its customer
|
||||
orders = (await session.execute(select(Order))).scalars().all()
|
||||
return [{"id": o.id, "customer": o.customer.name} for o in orders] # N+1
|
||||
|
||||
# ✅ Good — a single query with the relationship eager-loaded
|
||||
stmt = select(Order).options(selectinload(Order.customer))
|
||||
orders = (await session.execute(stmt)).scalars().all()
|
||||
return [{"id": o.id, "customer": o.customer.name} for o in orders]
|
||||
```
|
||||
|
||||
With async SQLAlchemy, lazy attribute access outside the session often raises instead of silently querying — but the design issue is the same. Look for relationship access inside a loop without an `options(...)` eager load.
|
||||
|
||||
### Paginate list endpoints
|
||||
|
||||
```python
|
||||
# ❌ Bad — returns every row; degrades as the table grows
|
||||
@app.get("/users")
|
||||
async def list_users(session: AsyncSession = Depends(get_session)):
|
||||
return (await session.execute(select(User))).scalars().all()
|
||||
|
||||
# ✅ Good — bounded page with a sane cap
|
||||
@app.get("/users", response_model=list[UserOut])
|
||||
async def list_users(
|
||||
session: AsyncSession = Depends(get_session),
|
||||
limit: int = Query(default=50, le=100),
|
||||
offset: int = Query(default=0, ge=0),
|
||||
):
|
||||
stmt = select(User).limit(limit).offset(offset)
|
||||
return (await session.execute(stmt)).scalars().all()
|
||||
```
|
||||
|
||||
### Aggregate and join in SQL, not in Python
|
||||
|
||||
If a handler pulls rows into memory and then loops to group, count, or join them, the database is being used as dumb storage. Push the work down — the database does set operations far faster, and you transfer less data.
|
||||
|
||||
```python
|
||||
# ❌ Bad — fetch every order, then tally per customer in Python
|
||||
orders = (await session.execute(select(Order))).scalars().all()
|
||||
totals: dict[int, float] = {}
|
||||
for o in orders:
|
||||
totals[o.customer_id] = totals.get(o.customer_id, 0) + o.amount
|
||||
|
||||
# ✅ Good — let the database group and sum
|
||||
stmt = select(Order.customer_id, func.sum(Order.amount)).group_by(Order.customer_id)
|
||||
totals = dict((await session.execute(stmt)).all())
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security
|
||||
|
||||
### A declared auth dependency is not an enforced authorization check
|
||||
|
||||
This is the highest-value thing to look for. `Depends(get_current_user)` proves *who* the caller is — it does **not** prove they may touch *this* resource.
|
||||
|
||||
```python
|
||||
# ❌ Bad — any authenticated user can delete any other user's document
|
||||
@app.delete("/documents/{doc_id}")
|
||||
async def delete_document(
|
||||
doc_id: int,
|
||||
user: User = Depends(get_current_user),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
doc = await session.get(Document, doc_id)
|
||||
await session.delete(doc) # never checks doc.owner_id == user.id
|
||||
await session.commit()
|
||||
|
||||
# ✅ Good — ownership is verified before the mutation
|
||||
@app.delete("/documents/{doc_id}", status_code=204)
|
||||
async def delete_document(
|
||||
doc_id: int,
|
||||
user: User = Depends(get_current_user),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
doc = await session.get(Document, doc_id)
|
||||
if doc is None:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
if doc.owner_id != user.id:
|
||||
raise HTTPException(status_code=403, detail="Forbidden")
|
||||
await session.delete(doc)
|
||||
await session.commit()
|
||||
```
|
||||
|
||||
The [Test-Driven Verification](#test-driven-verification) section reproduces exactly this bug with a failing test.
|
||||
|
||||
### Parameterize SQL; never f-string user input
|
||||
|
||||
```python
|
||||
# ❌ Bad — SQL injection
|
||||
await session.execute(text(f"SELECT * FROM users WHERE email = '{email}'"))
|
||||
|
||||
# ✅ Good — bound parameter
|
||||
await session.execute(text("SELECT * FROM users WHERE email = :email"), {"email": email})
|
||||
```
|
||||
|
||||
### Don't widen CORS to credentials + wildcard
|
||||
|
||||
```python
|
||||
# ❌ Bad — wildcard origin together with credentials is rejected by browsers and unsafe
|
||||
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True)
|
||||
|
||||
# ✅ Good — enumerate trusted origins when credentials are allowed
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["https://app.example.com"],
|
||||
allow_credentials=True,
|
||||
)
|
||||
```
|
||||
|
||||
Also check: secrets read from config/env (not hard-coded), `HTTPException` details that don't leak internals (stack traces, SQL), and rate limiting on auth endpoints.
|
||||
|
||||
---
|
||||
|
||||
## Test-Driven Verification
|
||||
|
||||
> Inspired by the test-driven development discipline: *if you didn't watch the test fail, you don't know it tests the right thing.* This matters even more for a coding agent than for a human reviewer. An agent's reading and reasoning are fallible — it can misread control flow, hallucinate a guarantee that isn't there, or rationalize a comfortable conclusion — so a prose verdict like "this looks safe" carries little weight on its own. An executable test is the one piece of **objective ground truth** the agent fully controls: it either passes or it doesn't, regardless of how confident the reasoning felt. That is what makes tests the agent's anchor of confidence. Reviewing the same way the discipline writes code — reproduce, don't assert — turns a hunch into proof.
|
||||
|
||||
A natural-language review comment ("this might let users delete each other's data") is exactly that kind of fallible hypothesis. FastAPI makes the ground truth cheap to obtain: an in-process client (`httpx.AsyncClient` over `ASGITransport`) runs the whole app, and `app.dependency_overrides` swaps out auth and the database without patching internals. So instead of trusting its own read of the code, the agent settles the question by reproduction.
|
||||
|
||||
### Reproduce a suspected bug with a failing test (Verify RED)
|
||||
|
||||
Suppose the reviewer suspects the `DELETE /documents/{doc_id}` route above never checks ownership. Write the test that asserts the *secure* behavior, then run it and **watch it fail** — the failure is the proof.
|
||||
|
||||
```python
|
||||
# test_document_authorization.py
|
||||
import pytest
|
||||
from httpx import AsyncClient, ASGITransport
|
||||
from fastapi import Header
|
||||
from app.main import app
|
||||
from app.deps import get_current_user, get_session
|
||||
|
||||
# Two users; the override picks one based on a test header.
|
||||
USERS = {"alice": User(id=1, email="alice@example.com"),
|
||||
"bob": User(id=2, email="bob@example.com")}
|
||||
|
||||
def fake_current_user(x_test_user: str = Header(default="alice")) -> User:
|
||||
return USERS[x_test_user]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_cannot_delete_another_users_document(session): # async fixture
|
||||
# Arrange: a document owned by Alice (id=1)
|
||||
session.add(Document(id=10, owner_id=1, title="Alice's doc"))
|
||||
await session.commit()
|
||||
|
||||
app.dependency_overrides[get_current_user] = fake_current_user
|
||||
app.dependency_overrides[get_session] = lambda: session
|
||||
|
||||
# Act: Bob tries to delete Alice's document
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
resp = await client.delete("/documents/10", headers={"X-Test-User": "bob"})
|
||||
|
||||
# Assert the SECURE behavior we expect
|
||||
assert resp.status_code == 403
|
||||
|
||||
app.dependency_overrides.clear()
|
||||
```
|
||||
|
||||
Run it against the unfixed code and confirm the failure is the bug, not a typo:
|
||||
|
||||
```bash
|
||||
$ pytest test_document_authorization.py
|
||||
FAILED assert 204 == 403
|
||||
# ^ the endpoint deleted Alice's document for Bob — vulnerability confirmed
|
||||
```
|
||||
|
||||
A failure of `204 == 403` (not an import error, not a 404) is what makes the finding credible: the route returned success for an action that should have been forbidden. Now the fix from the [Security](#security) section turns it green:
|
||||
|
||||
```bash
|
||||
$ pytest test_document_authorization.py
|
||||
PASSED
|
||||
```
|
||||
|
||||
Attach this test to the review. It documents the vulnerability, proves the fix, and guards against regression — far stronger than "consider checking ownership here."
|
||||
|
||||
### Prefer `dependency_overrides` over `patch`/`mock`
|
||||
|
||||
FastAPI's DI is the seam the TDD discipline asks for: when something is hard to test without mocking everything, that usually signals coupling — and `Depends` already gives you the injection point, so you rarely need `unittest.mock.patch`.
|
||||
|
||||
```python
|
||||
# ❌ Bad — patching internals: brittle, couples the test to import paths
|
||||
@patch("app.routes.orders.asyncpg.connect")
|
||||
def test_get_order(mock_connect): ...
|
||||
|
||||
# ✅ Good — override the dependency with a real in-memory fake
|
||||
app.dependency_overrides[get_session] = lambda: in_memory_session
|
||||
app.dependency_overrides[get_current_user] = lambda: test_user
|
||||
```
|
||||
|
||||
Always reset overrides between tests (`app.dependency_overrides.clear()` in a fixture teardown) so state doesn't leak across tests.
|
||||
|
||||
The reproduction above uses `httpx.AsyncClient` over `ASGITransport` with `@pytest.mark.asyncio` — the community convention for an async app, so the suite shares the app's event loop and you avoid loop-mismatch errors later. The synchronous `TestClient` is simpler and fine for a fully sync app, but standardizing on the async client from the start saves a painful migration once any route or fixture becomes async.
|
||||
|
||||
### Critique the PR's own tests, not just its source
|
||||
|
||||
A PR that ships tests is not automatically safe. Apply these checks to the *tests* in the diff:
|
||||
|
||||
```python
|
||||
# ❌ Bad — happy-path only. Proves the route works when everything is correct,
|
||||
# says nothing about the validation and authorization paths.
|
||||
def test_create_item():
|
||||
resp = client.post("/items", json={"name": "x", "price": 5})
|
||||
assert resp.status_code == 201
|
||||
|
||||
# ✅ Good — the boundary and failure paths are where bugs live
|
||||
def test_create_item_rejects_negative_price():
|
||||
resp = client.post("/items", json={"name": "x", "price": -5})
|
||||
assert resp.status_code == 422
|
||||
|
||||
def test_create_item_requires_authentication():
|
||||
resp = client_without_auth.post("/items", json={"name": "x", "price": 5})
|
||||
assert resp.status_code == 401
|
||||
```
|
||||
|
||||
Review questions for the test suite:
|
||||
|
||||
- **Does it test behavior, or the mock?** An assertion that only confirms a mock was called proves the test's own setup, not the endpoint.
|
||||
- **Are the failure paths covered?** 401/403/404/422 — not just 200/201. Bugs cluster at the boundaries.
|
||||
- **Is the mock complete?** A partial mock of an external API response that omits fields the handler reads passes in the test and fails in production.
|
||||
- **Were the tests written after the fact?** Tests added alongside an implementation and passing on the first run never demonstrated that they can fail — and so prove little. A test that reproduces the bug (fails first, then passes) is worth more than one that was green from birth.
|
||||
|
||||
---
|
||||
|
||||
## Review Checklist
|
||||
|
||||
### Dependency Injection
|
||||
|
||||
- [ ] Routes stay thin — DB access and business rules live behind `Depends`/services
|
||||
- [ ] `yield` dependencies release resources via context manager or `try/finally`
|
||||
- [ ] Singletons (HTTP clients, pools) created once in `lifespan`, not per request
|
||||
- [ ] `Annotated[T, Depends(...)]` form used; dependencies are `async def` unless they do blocking I/O
|
||||
- [ ] Existence/permission checks live in (cached) dependencies, not copy-pasted into routes
|
||||
- [ ] Dependencies are overridable in tests (no resources created inline in the route)
|
||||
|
||||
### Validation
|
||||
|
||||
- [ ] Input and output use distinct Pydantic models; ORM objects are not the `response_model`
|
||||
- [ ] `response_model` set so sensitive fields can't leak
|
||||
- [ ] Separate Create vs Update schemas (update is partial)
|
||||
- [ ] Constraints (`gt`, `le`, `EmailStr`, ...) enforced at the boundary, before the DB write
|
||||
|
||||
### Async
|
||||
|
||||
- [ ] No blocking calls (`requests`, `time.sleep`, blocking DB drivers) inside `async def`
|
||||
- [ ] Native-async SDKs preferred (`httpx`, `asyncpg`, `redis.asyncio`, ...) over sync ones
|
||||
- [ ] No `asyncio.run`/manual event loops/manual threads inside routes
|
||||
- [ ] `run_in_threadpool`/`def` routes used only as a last resort, not on hot paths
|
||||
- [ ] CPU-bound work offloaded to a worker process (Celery/Arq/RQ), not the loop or threadpool
|
||||
- [ ] No unawaited coroutines; `BackgroundTasks` only for short fire-and-forget work
|
||||
|
||||
### Database
|
||||
|
||||
- [ ] One request-scoped session via dependency; no module-level shared session
|
||||
- [ ] Relationships eager-loaded (`selectinload`/`joinedload`) where accessed in a loop
|
||||
- [ ] Joins/aggregations done in SQL, not by looping in Python
|
||||
- [ ] List endpoints are paginated with a capped `limit`
|
||||
|
||||
### Security
|
||||
|
||||
- [ ] Authentication dependency is backed by an explicit **authorization** check (ownership/role)
|
||||
- [ ] All SQL parameterized; no f-string interpolation of user input
|
||||
- [ ] CORS does not combine `allow_origins=["*"]` with `allow_credentials=True`
|
||||
- [ ] Secrets come from config/env; error responses don't leak internals
|
||||
|
||||
### Tests
|
||||
|
||||
- [ ] Suspected bugs reproduced with a failing test (`TestClient`/`AsyncClient`) before being claimed
|
||||
- [ ] `dependency_overrides` used instead of patching internals; overrides reset between tests
|
||||
- [ ] Failure paths covered (401/403/404/422), not just the happy path
|
||||
- [ ] Mocks of external responses are complete, not partial
|
||||
- [ ] New tests demonstrate they can fail (reproduce-then-fix), not green from birth
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [FastAPI official documentation](https://fastapi.tiangolo.com/) — async, dependencies, testing
|
||||
- [zhanymkanov/fastapi-best-practices](https://github.com/zhanymkanov/fastapi-best-practices) — production conventions (async routes, dependency caching, project structure)
|
||||
+989
@@ -0,0 +1,989 @@
|
||||
# Go 代码审查指南
|
||||
|
||||
基于 Go 官方指南、Effective Go 和社区最佳实践的代码审查清单。
|
||||
|
||||
## 快速审查清单
|
||||
|
||||
### 必查项
|
||||
- [ ] 错误是否正确处理(不忽略、有上下文)
|
||||
- [ ] goroutine 是否有退出机制(避免泄漏)
|
||||
- [ ] context 是否正确传递和取消
|
||||
- [ ] 接收器类型选择是否合理(值/指针)
|
||||
- [ ] 是否使用 `gofmt` 格式化代码
|
||||
|
||||
### 高频问题
|
||||
- [ ] 循环变量捕获问题(Go < 1.22)
|
||||
- [ ] nil 检查是否完整
|
||||
- [ ] map 是否初始化后使用
|
||||
- [ ] defer 在循环中的使用
|
||||
- [ ] 变量遮蔽(shadowing)
|
||||
|
||||
---
|
||||
|
||||
## 1. 错误处理
|
||||
|
||||
### 1.1 永远不要忽略错误
|
||||
|
||||
```go
|
||||
// ❌ 错误:忽略错误
|
||||
result, _ := SomeFunction()
|
||||
|
||||
// ✅ 正确:处理错误
|
||||
result, err := SomeFunction()
|
||||
if err != nil {
|
||||
return fmt.Errorf("some function failed: %w", err)
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 错误包装与上下文
|
||||
|
||||
```go
|
||||
// ❌ 错误:丢失上下文
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// ❌ 错误:使用 %v 丢失错误链
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed: %v", err)
|
||||
}
|
||||
|
||||
// ✅ 正确:使用 %w 保留错误链
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to process user %d: %w", userID, err)
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3 使用 errors.Is 和 errors.As
|
||||
|
||||
```go
|
||||
// ❌ 错误:直接比较(无法处理包装错误)
|
||||
if err == sql.ErrNoRows {
|
||||
// ...
|
||||
}
|
||||
|
||||
// ✅ 正确:使用 errors.Is(支持错误链)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
// ✅ 正确:使用 errors.As 提取特定类型
|
||||
var pathErr *os.PathError
|
||||
if errors.As(err, &pathErr) {
|
||||
log.Printf("path error: %s", pathErr.Path)
|
||||
}
|
||||
```
|
||||
|
||||
### 1.4 自定义错误类型
|
||||
|
||||
```go
|
||||
// ✅ 推荐:定义 sentinel 错误
|
||||
var (
|
||||
ErrNotFound = errors.New("not found")
|
||||
ErrUnauthorized = errors.New("unauthorized")
|
||||
)
|
||||
|
||||
// ✅ 推荐:带上下文的自定义错误
|
||||
type ValidationError struct {
|
||||
Field string
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *ValidationError) Error() string {
|
||||
return fmt.Sprintf("validation error on %s: %s", e.Field, e.Message)
|
||||
}
|
||||
```
|
||||
|
||||
### 1.5 错误处理只做一次
|
||||
|
||||
```go
|
||||
// ❌ 错误:既记录又返回(重复处理)
|
||||
if err != nil {
|
||||
log.Printf("error: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// ✅ 正确:只返回,让调用者决定
|
||||
if err != nil {
|
||||
return fmt.Errorf("operation failed: %w", err)
|
||||
}
|
||||
|
||||
// ✅ 或者:只记录并处理(不返回)
|
||||
if err != nil {
|
||||
log.Printf("non-critical error: %v", err)
|
||||
// 继续执行备用逻辑
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 并发与 Goroutine
|
||||
|
||||
### 2.1 避免 Goroutine 泄漏
|
||||
|
||||
```go
|
||||
// ❌ 错误:goroutine 永远无法退出
|
||||
func bad() {
|
||||
ch := make(chan int)
|
||||
go func() {
|
||||
val := <-ch // 永远阻塞,无人发送
|
||||
fmt.Println(val)
|
||||
}()
|
||||
// 函数返回,goroutine 泄漏
|
||||
}
|
||||
|
||||
// ✅ 正确:使用 context 或 done channel
|
||||
func good(ctx context.Context) {
|
||||
ch := make(chan int)
|
||||
go func() {
|
||||
select {
|
||||
case val := <-ch:
|
||||
fmt.Println(val)
|
||||
case <-ctx.Done():
|
||||
return // 优雅退出
|
||||
}
|
||||
}()
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 Channel 使用规范
|
||||
|
||||
```go
|
||||
// ❌ 错误:向 nil channel 发送(永久阻塞)
|
||||
var ch chan int
|
||||
ch <- 1 // 永久阻塞
|
||||
|
||||
// ❌ 错误:向已关闭的 channel 发送(panic)
|
||||
close(ch)
|
||||
ch <- 1 // panic!
|
||||
|
||||
// ✅ 正确:发送方关闭 channel
|
||||
func producer(ch chan<- int) {
|
||||
defer close(ch) // 发送方负责关闭
|
||||
for i := 0; i < 10; i++ {
|
||||
ch <- i
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 正确:接收方检测关闭
|
||||
for val := range ch {
|
||||
process(val)
|
||||
}
|
||||
// 或者
|
||||
val, ok := <-ch
|
||||
if !ok {
|
||||
// channel 已关闭
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 使用 sync.WaitGroup
|
||||
|
||||
```go
|
||||
// ❌ 错误:Add 在 goroutine 内部
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 10; i++ {
|
||||
go func() {
|
||||
wg.Add(1) // 竞态条件!
|
||||
defer wg.Done()
|
||||
work()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// ✅ 正确:Add 在 goroutine 启动前
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 10; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
work()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
```
|
||||
|
||||
### 2.4 避免在循环中捕获变量(Go < 1.22)
|
||||
|
||||
```go
|
||||
// ❌ 错误(Go < 1.22):捕获循环变量
|
||||
for _, item := range items {
|
||||
go func() {
|
||||
process(item) // 所有 goroutine 可能使用同一个 item
|
||||
}()
|
||||
}
|
||||
|
||||
// ✅ 正确:传递参数
|
||||
for _, item := range items {
|
||||
go func(it Item) {
|
||||
process(it)
|
||||
}(item)
|
||||
}
|
||||
|
||||
// ✅ Go 1.22+:默认行为已修复,每次迭代创建新变量
|
||||
```
|
||||
|
||||
### 2.5 Worker Pool 模式
|
||||
|
||||
```go
|
||||
// ✅ 推荐:限制并发数量
|
||||
func processWithWorkerPool(ctx context.Context, items []Item, workers int) error {
|
||||
jobs := make(chan Item, len(items))
|
||||
results := make(chan error, len(items))
|
||||
|
||||
// 启动 worker
|
||||
for w := 0; w < workers; w++ {
|
||||
go func() {
|
||||
for item := range jobs {
|
||||
results <- process(item)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 发送任务
|
||||
for _, item := range items {
|
||||
jobs <- item
|
||||
}
|
||||
close(jobs)
|
||||
|
||||
// 收集结果
|
||||
for range items {
|
||||
if err := <-results; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Context 使用
|
||||
|
||||
### 3.1 Context 作为第一个参数
|
||||
|
||||
```go
|
||||
// ❌ 错误:context 不是第一个参数
|
||||
func Process(data []byte, ctx context.Context) error
|
||||
|
||||
// ❌ 错误:context 存储在 struct 中
|
||||
type Service struct {
|
||||
ctx context.Context // 不要这样做!
|
||||
}
|
||||
|
||||
// ✅ 正确:context 作为第一个参数,命名为 ctx
|
||||
func Process(ctx context.Context, data []byte) error
|
||||
```
|
||||
|
||||
### 3.2 传播而非创建新的根 Context
|
||||
|
||||
```go
|
||||
// ❌ 错误:在调用链中创建新的根 context
|
||||
func middleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.Background() // 丢失了请求的 context!
|
||||
process(ctx)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// ✅ 正确:从请求中获取并传播
|
||||
func middleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
ctx = context.WithValue(ctx, key, value)
|
||||
process(ctx)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 始终调用 cancel 函数
|
||||
|
||||
```go
|
||||
// ❌ 错误:未调用 cancel
|
||||
ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second)
|
||||
// 缺少 cancel() 调用,可能资源泄漏
|
||||
|
||||
// ✅ 正确:使用 defer 确保调用
|
||||
ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second)
|
||||
defer cancel() // 即使超时也要调用
|
||||
```
|
||||
|
||||
### 3.4 响应 Context 取消
|
||||
|
||||
```go
|
||||
// ✅ 推荐:在长时间操作中检查 context
|
||||
func LongRunningTask(ctx context.Context) error {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err() // 返回 context.Canceled 或 context.DeadlineExceeded
|
||||
default:
|
||||
// 执行一小部分工作
|
||||
if err := doChunk(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.5 区分取消原因
|
||||
|
||||
```go
|
||||
// ✅ 根据 ctx.Err() 区分取消原因
|
||||
if err := ctx.Err(); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, context.Canceled):
|
||||
log.Println("operation was canceled")
|
||||
case errors.Is(err, context.DeadlineExceeded):
|
||||
log.Println("operation timed out")
|
||||
}
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 接口设计
|
||||
|
||||
### 4.1 接受接口,返回结构体
|
||||
|
||||
```go
|
||||
// ❌ 不推荐:接受具体类型
|
||||
func SaveUser(db *sql.DB, user User) error
|
||||
|
||||
// ✅ 推荐:接受接口(解耦、易测试)
|
||||
type UserStore interface {
|
||||
Save(ctx context.Context, user User) error
|
||||
}
|
||||
|
||||
func SaveUser(store UserStore, user User) error
|
||||
|
||||
// ❌ 不推荐:返回接口
|
||||
func NewUserService() UserServiceInterface
|
||||
|
||||
// ✅ 推荐:返回具体类型
|
||||
func NewUserService(store UserStore) *UserService
|
||||
```
|
||||
|
||||
### 4.2 在消费者处定义接口
|
||||
|
||||
```go
|
||||
// ❌ 不推荐:在实现包中定义接口
|
||||
// package database
|
||||
type Database interface {
|
||||
Query(ctx context.Context, query string) ([]Row, error)
|
||||
// ... 20 个方法
|
||||
}
|
||||
|
||||
// ✅ 推荐:在消费者包中定义所需的最小接口
|
||||
// package userservice
|
||||
type UserQuerier interface {
|
||||
QueryUsers(ctx context.Context, filter Filter) ([]User, error)
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 保持接口小而专注
|
||||
|
||||
```go
|
||||
// ❌ 不推荐:大而全的接口
|
||||
type Repository interface {
|
||||
GetUser(id int) (*User, error)
|
||||
CreateUser(u *User) error
|
||||
UpdateUser(u *User) error
|
||||
DeleteUser(id int) error
|
||||
GetOrder(id int) (*Order, error)
|
||||
CreateOrder(o *Order) error
|
||||
// ... 更多方法
|
||||
}
|
||||
|
||||
// ✅ 推荐:小而专注的接口
|
||||
type UserReader interface {
|
||||
GetUser(ctx context.Context, id int) (*User, error)
|
||||
}
|
||||
|
||||
type UserWriter interface {
|
||||
CreateUser(ctx context.Context, u *User) error
|
||||
UpdateUser(ctx context.Context, u *User) error
|
||||
}
|
||||
|
||||
// 组合接口
|
||||
type UserRepository interface {
|
||||
UserReader
|
||||
UserWriter
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 避免空接口滥用
|
||||
|
||||
```go
|
||||
// ❌ 不推荐:过度使用 interface{}
|
||||
func Process(data interface{}) interface{}
|
||||
|
||||
// ✅ 推荐:使用泛型(Go 1.18+)
|
||||
func Process[T any](data T) T
|
||||
|
||||
// ✅ 推荐:定义具体接口
|
||||
type Processor interface {
|
||||
Process() Result
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 接收器类型选择
|
||||
|
||||
### 5.1 使用指针接收器的情况
|
||||
|
||||
```go
|
||||
// ✅ 需要修改接收器时
|
||||
func (u *User) SetName(name string) {
|
||||
u.Name = name
|
||||
}
|
||||
|
||||
// ✅ 接收器包含 sync.Mutex 等同步原语
|
||||
type SafeCounter struct {
|
||||
mu sync.Mutex
|
||||
count int
|
||||
}
|
||||
|
||||
func (c *SafeCounter) Inc() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.count++
|
||||
}
|
||||
|
||||
// ✅ 接收器是大型结构体(避免复制开销)
|
||||
type LargeStruct struct {
|
||||
Data [1024]byte
|
||||
// ...
|
||||
}
|
||||
|
||||
func (l *LargeStruct) Process() { /* ... */ }
|
||||
```
|
||||
|
||||
### 5.2 使用值接收器的情况
|
||||
|
||||
```go
|
||||
// ✅ 接收器是小型不可变结构体
|
||||
type Point struct {
|
||||
X, Y float64
|
||||
}
|
||||
|
||||
func (p Point) Distance(other Point) float64 {
|
||||
return math.Sqrt(math.Pow(p.X-other.X, 2) + math.Pow(p.Y-other.Y, 2))
|
||||
}
|
||||
|
||||
// ✅ 接收器是基本类型的别名
|
||||
type Counter int
|
||||
|
||||
func (c Counter) String() string {
|
||||
return fmt.Sprintf("%d", c)
|
||||
}
|
||||
|
||||
// ✅ 接收器是 map、func、chan(本身是引用类型)
|
||||
type StringSet map[string]struct{}
|
||||
|
||||
func (s StringSet) Contains(key string) bool {
|
||||
_, ok := s[key]
|
||||
return ok
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 一致性原则
|
||||
|
||||
```go
|
||||
// ❌ 不推荐:混合使用接收器类型
|
||||
func (u User) GetName() string // 值接收器
|
||||
func (u *User) SetName(n string) // 指针接收器
|
||||
|
||||
// ✅ 推荐:如果有任何方法需要指针接收器,全部使用指针
|
||||
func (u *User) GetName() string { return u.Name }
|
||||
func (u *User) SetName(n string) { u.Name = n }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 性能优化
|
||||
|
||||
### 6.1 预分配 Slice
|
||||
|
||||
```go
|
||||
// ❌ 不推荐:动态增长
|
||||
var result []int
|
||||
for i := 0; i < 10000; i++ {
|
||||
result = append(result, i) // 多次分配和复制
|
||||
}
|
||||
|
||||
// ✅ 推荐:预分配已知大小
|
||||
result := make([]int, 0, 10000)
|
||||
for i := 0; i < 10000; i++ {
|
||||
result = append(result, i)
|
||||
}
|
||||
|
||||
// ✅ 或者直接初始化
|
||||
result := make([]int, 10000)
|
||||
for i := 0; i < 10000; i++ {
|
||||
result[i] = i
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 避免不必要的堆分配
|
||||
|
||||
```go
|
||||
// ❌ 可能逃逸到堆
|
||||
func NewUser() *User {
|
||||
return &User{} // 逃逸到堆
|
||||
}
|
||||
|
||||
// ✅ 考虑返回值(如果适用)
|
||||
func NewUser() User {
|
||||
return User{} // 可能在栈上分配
|
||||
}
|
||||
|
||||
// 检查逃逸分析
|
||||
// go build -gcflags '-m -m' ./...
|
||||
```
|
||||
|
||||
### 6.3 使用 sync.Pool 复用对象
|
||||
|
||||
```go
|
||||
// ✅ 推荐:高频创建/销毁的对象使用 sync.Pool
|
||||
var bufferPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return new(bytes.Buffer)
|
||||
},
|
||||
}
|
||||
|
||||
func ProcessData(data []byte) string {
|
||||
buf := bufferPool.Get().(*bytes.Buffer)
|
||||
defer func() {
|
||||
buf.Reset()
|
||||
bufferPool.Put(buf)
|
||||
}()
|
||||
|
||||
buf.Write(data)
|
||||
return buf.String()
|
||||
}
|
||||
```
|
||||
|
||||
### 6.4 字符串拼接优化
|
||||
|
||||
```go
|
||||
// ❌ 不推荐:循环中使用 + 拼接
|
||||
var result string
|
||||
for _, s := range strings {
|
||||
result += s // 每次创建新字符串
|
||||
}
|
||||
|
||||
// ✅ 推荐:使用 strings.Builder
|
||||
var builder strings.Builder
|
||||
for _, s := range strings {
|
||||
builder.WriteString(s)
|
||||
}
|
||||
result := builder.String()
|
||||
|
||||
// ✅ 或者使用 strings.Join
|
||||
result := strings.Join(strings, "")
|
||||
```
|
||||
|
||||
### 6.5 避免 interface{} 转换开销
|
||||
|
||||
```go
|
||||
// ❌ 热路径中使用 interface{}
|
||||
func process(data interface{}) {
|
||||
switch v := data.(type) { // 类型断言有开销
|
||||
case int:
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 热路径中使用泛型或具体类型
|
||||
func process[T int | int64 | float64](data T) {
|
||||
// 编译时确定类型,无运行时开销
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 测试
|
||||
|
||||
### 7.1 表驱动测试
|
||||
|
||||
```go
|
||||
// ✅ 推荐:表驱动测试
|
||||
func TestAdd(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
a, b int
|
||||
expected int
|
||||
}{
|
||||
{"positive numbers", 1, 2, 3},
|
||||
{"with zero", 0, 5, 5},
|
||||
{"negative numbers", -1, -2, -3},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := Add(tt.a, tt.b)
|
||||
if result != tt.expected {
|
||||
t.Errorf("Add(%d, %d) = %d; want %d",
|
||||
tt.a, tt.b, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 并行测试
|
||||
|
||||
```go
|
||||
// ✅ 推荐:独立测试用例并行执行
|
||||
func TestParallel(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
}{
|
||||
{"test1", "input1"},
|
||||
{"test2", "input2"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt // Go < 1.22 需要复制
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel() // 标记为可并行
|
||||
result := Process(tt.input)
|
||||
// assertions...
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 使用接口进行 Mock
|
||||
|
||||
```go
|
||||
// ✅ 定义接口以便测试
|
||||
type EmailSender interface {
|
||||
Send(to, subject, body string) error
|
||||
}
|
||||
|
||||
// 生产实现
|
||||
type SMTPSender struct { /* ... */ }
|
||||
|
||||
// 测试 Mock
|
||||
type MockEmailSender struct {
|
||||
SendFunc func(to, subject, body string) error
|
||||
}
|
||||
|
||||
func (m *MockEmailSender) Send(to, subject, body string) error {
|
||||
return m.SendFunc(to, subject, body)
|
||||
}
|
||||
|
||||
func TestUserRegistration(t *testing.T) {
|
||||
mock := &MockEmailSender{
|
||||
SendFunc: func(to, subject, body string) error {
|
||||
if to != "test@example.com" {
|
||||
t.Errorf("unexpected recipient: %s", to)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
service := NewUserService(mock)
|
||||
// test...
|
||||
}
|
||||
```
|
||||
|
||||
### 7.4 测试辅助函数
|
||||
|
||||
```go
|
||||
// ✅ 使用 t.Helper() 标记辅助函数
|
||||
func assertEqual(t *testing.T, got, want interface{}) {
|
||||
t.Helper() // 错误报告时显示调用者位置
|
||||
if got != want {
|
||||
t.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 使用 t.Cleanup() 清理资源
|
||||
func TestWithTempFile(t *testing.T) {
|
||||
f, err := os.CreateTemp("", "test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
os.Remove(f.Name())
|
||||
})
|
||||
// test...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 常见陷阱
|
||||
|
||||
### 8.1 Nil Slice vs Empty Slice
|
||||
|
||||
```go
|
||||
var nilSlice []int // nil, len=0, cap=0
|
||||
emptySlice := []int{} // not nil, len=0, cap=0
|
||||
made := make([]int, 0) // not nil, len=0, cap=0
|
||||
|
||||
// ✅ JSON 编码差异
|
||||
json.Marshal(nilSlice) // null
|
||||
json.Marshal(emptySlice) // []
|
||||
|
||||
// ✅ 推荐:需要空数组 JSON 时显式初始化
|
||||
if slice == nil {
|
||||
slice = []int{}
|
||||
}
|
||||
```
|
||||
|
||||
### 8.2 Map 初始化
|
||||
|
||||
```go
|
||||
// ❌ 错误:未初始化的 map
|
||||
var m map[string]int
|
||||
m["key"] = 1 // panic: assignment to entry in nil map
|
||||
|
||||
// ✅ 正确:使用 make 初始化
|
||||
m := make(map[string]int)
|
||||
m["key"] = 1
|
||||
|
||||
// ✅ 或者使用字面量
|
||||
m := map[string]int{}
|
||||
```
|
||||
|
||||
### 8.3 Defer 在循环中
|
||||
|
||||
```go
|
||||
// ❌ 潜在问题:defer 在函数结束时才执行
|
||||
func processFiles(files []string) error {
|
||||
for _, file := range files {
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close() // 所有文件在函数结束时才关闭!
|
||||
// process...
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ✅ 正确:使用闭包或提取函数
|
||||
func processFiles(files []string) error {
|
||||
for _, file := range files {
|
||||
if err := processFile(file); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func processFile(file string) error {
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
// process...
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### 8.4 Slice 底层数组共享
|
||||
|
||||
```go
|
||||
// ❌ 潜在问题:切片共享底层数组
|
||||
original := []int{1, 2, 3, 4, 5}
|
||||
slice := original[1:3] // [2, 3]
|
||||
slice[0] = 100 // 修改了 original!
|
||||
// original 变成 [1, 100, 3, 4, 5]
|
||||
|
||||
// ✅ 正确:需要独立副本时显式复制
|
||||
slice := make([]int, 2)
|
||||
copy(slice, original[1:3])
|
||||
slice[0] = 100 // 不影响 original
|
||||
```
|
||||
|
||||
### 8.5 字符串子串内存泄漏
|
||||
|
||||
```go
|
||||
// ❌ 潜在问题:子串持有整个底层数组
|
||||
func getPrefix(s string) string {
|
||||
return s[:10] // 仍引用整个 s 的底层数组
|
||||
}
|
||||
|
||||
// ✅ 正确:创建独立副本(Go 1.18+)
|
||||
func getPrefix(s string) string {
|
||||
return strings.Clone(s[:10])
|
||||
}
|
||||
|
||||
// ✅ Go 1.18 之前
|
||||
func getPrefix(s string) string {
|
||||
return string([]byte(s[:10]))
|
||||
}
|
||||
```
|
||||
|
||||
### 8.6 Interface Nil 陷阱
|
||||
|
||||
```go
|
||||
// ❌ 陷阱:interface 的 nil 判断
|
||||
type MyError struct{}
|
||||
func (e *MyError) Error() string { return "error" }
|
||||
|
||||
func returnsError() error {
|
||||
var e *MyError = nil
|
||||
return e // 返回的 error 不是 nil!
|
||||
}
|
||||
|
||||
func main() {
|
||||
err := returnsError()
|
||||
if err != nil { // true! interface{type: *MyError, value: nil}
|
||||
fmt.Println("error:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 正确:显式返回 nil
|
||||
func returnsError() error {
|
||||
var e *MyError = nil
|
||||
if e == nil {
|
||||
return nil // 显式返回 nil
|
||||
}
|
||||
return e
|
||||
}
|
||||
```
|
||||
|
||||
### 8.7 Time 比较
|
||||
|
||||
```go
|
||||
// ❌ 不推荐:直接使用 == 比较 time.Time
|
||||
if t1 == t2 { // 可能因为单调时钟差异而失败
|
||||
// ...
|
||||
}
|
||||
|
||||
// ✅ 推荐:使用 Equal 方法
|
||||
if t1.Equal(t2) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// ✅ 比较时间范围
|
||||
if t1.Before(t2) || t1.After(t2) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 代码组织
|
||||
|
||||
### 9.1 包命名
|
||||
|
||||
```go
|
||||
// ❌ 不推荐
|
||||
package common // 过于宽泛
|
||||
package utils // 过于宽泛
|
||||
package helpers // 过于宽泛
|
||||
package models // 按类型分组
|
||||
|
||||
// ✅ 推荐:按功能命名
|
||||
package user // 用户相关功能
|
||||
package order // 订单相关功能
|
||||
package postgres // PostgreSQL 实现
|
||||
```
|
||||
|
||||
### 9.2 避免循环依赖
|
||||
|
||||
```go
|
||||
// ❌ 循环依赖
|
||||
// package a imports package b
|
||||
// package b imports package a
|
||||
|
||||
// ✅ 解决方案1:提取共享类型到独立包
|
||||
// package types (共享类型)
|
||||
// package a imports types
|
||||
// package b imports types
|
||||
|
||||
// ✅ 解决方案2:使用接口解耦
|
||||
// package a 定义接口
|
||||
// package b 实现接口
|
||||
```
|
||||
|
||||
### 9.3 导出标识符规范
|
||||
|
||||
```go
|
||||
// ✅ 只导出必要的标识符
|
||||
type UserService struct {
|
||||
db *sql.DB // 私有
|
||||
}
|
||||
|
||||
func (s *UserService) GetUser(id int) (*User, error) // 公开
|
||||
func (s *UserService) validate(u *User) error // 私有
|
||||
|
||||
// ✅ 内部包限制访问
|
||||
// internal/database/... 只能被同项目代码导入
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 工具与检查
|
||||
|
||||
### 10.1 必须使用的工具
|
||||
|
||||
```bash
|
||||
# 格式化(必须)
|
||||
gofmt -w .
|
||||
goimports -w .
|
||||
|
||||
# 静态分析
|
||||
go vet ./...
|
||||
|
||||
# 竞态检测
|
||||
go test -race ./...
|
||||
|
||||
# 逃逸分析
|
||||
go build -gcflags '-m -m' ./...
|
||||
```
|
||||
|
||||
### 10.2 推荐的 Linter
|
||||
|
||||
```bash
|
||||
# golangci-lint(集成多个 linter)
|
||||
golangci-lint run
|
||||
|
||||
# 常用检查项
|
||||
# - errcheck: 检查未处理的错误
|
||||
# - gosec: 安全检查
|
||||
# - ineffassign: 无效赋值
|
||||
# - staticcheck: 静态分析
|
||||
# - unused: 未使用的代码
|
||||
```
|
||||
|
||||
### 10.3 Benchmark 测试
|
||||
|
||||
```go
|
||||
// ✅ 性能基准测试
|
||||
func BenchmarkProcess(b *testing.B) {
|
||||
data := prepareData()
|
||||
b.ResetTimer() // 重置计时器
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
Process(data)
|
||||
}
|
||||
}
|
||||
|
||||
// 运行 benchmark
|
||||
// go test -bench=. -benchmem ./...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 参考资源
|
||||
|
||||
- [Effective Go](https://go.dev/doc/effective_go)
|
||||
- [Go Code Review Comments](https://go.dev/wiki/CodeReviewComments)
|
||||
- [Go Common Mistakes](https://go.dev/wiki/CommonMistakes)
|
||||
- [100 Go Mistakes](https://100go.co/)
|
||||
- [Go Proverbs](https://go-proverbs.github.io/)
|
||||
- [Uber Go Style Guide](https://github.com/uber-go/guide/blob/master/style.md)
|
||||
+405
@@ -0,0 +1,405 @@
|
||||
# Java Code Review Guide
|
||||
|
||||
Java 审查重点:Java 17/21 新特性、Spring Boot 3 最佳实践、并发编程(虚拟线程)、JPA 性能优化以及代码可维护性。
|
||||
|
||||
## 目录
|
||||
|
||||
- [现代 Java 特性 (17/21+)](#现代-java-特性-1721)
|
||||
- [Stream API & Optional](#stream-api--optional)
|
||||
- [Spring Boot 最佳实践](#spring-boot-最佳实践)
|
||||
- [JPA 与 数据库性能](#jpa-与-数据库性能)
|
||||
- [并发与虚拟线程](#并发与虚拟线程)
|
||||
- [Lombok 使用规范](#lombok-使用规范)
|
||||
- [异常处理](#异常处理)
|
||||
- [测试规范](#测试规范)
|
||||
- [Review Checklist](#review-checklist)
|
||||
|
||||
---
|
||||
|
||||
## 现代 Java 特性 (17/21+)
|
||||
|
||||
### Record (记录类)
|
||||
|
||||
```java
|
||||
// ❌ 传统的 POJO/DTO:样板代码多
|
||||
public class UserDto {
|
||||
private final String name;
|
||||
private final int age;
|
||||
|
||||
public UserDto(String name, int age) {
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
// getters, equals, hashCode, toString...
|
||||
}
|
||||
|
||||
// ✅ 使用 Record:简洁、不可变、语义清晰
|
||||
public record UserDto(String name, int age) {
|
||||
// 紧凑构造函数进行验证
|
||||
public UserDto {
|
||||
if (age < 0) throw new IllegalArgumentException("Age cannot be negative");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Switch 表达式与模式匹配
|
||||
|
||||
```java
|
||||
// ❌ 传统的 Switch:容易漏掉 break,不仅冗长且易错
|
||||
String type = "";
|
||||
switch (obj) {
|
||||
case Integer i: // Java 16+
|
||||
type = String.format("int %d", i);
|
||||
break;
|
||||
case String s:
|
||||
type = String.format("string %s", s);
|
||||
break;
|
||||
default:
|
||||
type = "unknown";
|
||||
}
|
||||
|
||||
// ✅ Switch 表达式:无穿透风险,强制返回值
|
||||
String type = switch (obj) {
|
||||
case Integer i -> "int %d".formatted(i);
|
||||
case String s -> "string %s".formatted(s);
|
||||
case null -> "null value"; // Java 21 处理 null
|
||||
default -> "unknown";
|
||||
};
|
||||
```
|
||||
|
||||
### 文本块 (Text Blocks)
|
||||
|
||||
```java
|
||||
// ❌ 拼接 SQL/JSON 字符串
|
||||
String json = "{\n" +
|
||||
" \"name\": \"Alice\",\n" +
|
||||
" \"age\": 20\n" +
|
||||
"}";
|
||||
|
||||
// ✅ 使用文本块:所见即所得
|
||||
String json = """
|
||||
{
|
||||
"name": "Alice",
|
||||
"age": 20
|
||||
}
|
||||
""";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Stream API & Optional
|
||||
|
||||
### 避免滥用 Stream
|
||||
|
||||
```java
|
||||
// ❌ 简单的循环不需要 Stream(性能开销 + 可读性差)
|
||||
items.stream().forEach(item -> {
|
||||
process(item);
|
||||
});
|
||||
|
||||
// ✅ 简单场景直接用 for-each
|
||||
for (var item : items) {
|
||||
process(item);
|
||||
}
|
||||
|
||||
// ❌ 极其复杂的 Stream 链
|
||||
List<Dto> result = list.stream()
|
||||
.filter(...)
|
||||
.map(...)
|
||||
.peek(...)
|
||||
.sorted(...)
|
||||
.collect(...); // 难以调试
|
||||
|
||||
// ✅ 拆分为有意义的步骤
|
||||
var filtered = list.stream().filter(...).toList();
|
||||
// ...
|
||||
```
|
||||
|
||||
### Optional 正确用法
|
||||
|
||||
```java
|
||||
// ❌ 将 Optional 用作参数或字段(序列化问题,增加调用复杂度)
|
||||
public void process(Optional<String> name) { ... }
|
||||
public class User {
|
||||
private Optional<String> email; // 不推荐
|
||||
}
|
||||
|
||||
// ✅ Optional 仅用于返回值
|
||||
public Optional<User> findUser(String id) { ... }
|
||||
|
||||
// ❌ 既然用了 Optional 还在用 isPresent() + get()
|
||||
Optional<User> userOpt = findUser(id);
|
||||
if (userOpt.isPresent()) {
|
||||
return userOpt.get().getName();
|
||||
} else {
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
// ✅ 使用函数式 API
|
||||
return findUser(id)
|
||||
.map(User::getName)
|
||||
.orElse("Unknown");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Spring Boot 最佳实践
|
||||
|
||||
### 依赖注入 (DI)
|
||||
|
||||
```java
|
||||
// ❌ 字段注入 (@Autowired)
|
||||
// 缺点:难以测试(需要反射注入),掩盖了依赖过多的问题,且不可变性差
|
||||
@Service
|
||||
public class UserService {
|
||||
@Autowired
|
||||
private UserRepository userRepo;
|
||||
}
|
||||
|
||||
// ✅ 构造器注入 (Constructor Injection)
|
||||
// 优点:依赖明确,易于单元测试 (Mock),字段可为 final
|
||||
@Service
|
||||
public class UserService {
|
||||
private final UserRepository userRepo;
|
||||
|
||||
public UserService(UserRepository userRepo) {
|
||||
this.userRepo = userRepo;
|
||||
}
|
||||
}
|
||||
// 💡 提示:结合 Lombok @RequiredArgsConstructor 可简化代码,但要小心循环依赖
|
||||
```
|
||||
|
||||
### 配置管理
|
||||
|
||||
```java
|
||||
// ❌ 硬编码配置值
|
||||
@Service
|
||||
public class PaymentService {
|
||||
private String apiKey = "sk_live_12345";
|
||||
}
|
||||
|
||||
// ❌ 直接使用 @Value 散落在代码中
|
||||
@Value("${app.payment.api-key}")
|
||||
private String apiKey;
|
||||
|
||||
// ✅ 使用 @ConfigurationProperties 类型安全配置
|
||||
@ConfigurationProperties(prefix = "app.payment")
|
||||
public record PaymentProperties(String apiKey, int timeout, String url) {}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## JPA 与 数据库性能
|
||||
|
||||
### N+1 查询问题
|
||||
|
||||
```java
|
||||
// ❌ FetchType.EAGER 或 循环中触发懒加载
|
||||
// Entity 定义
|
||||
@Entity
|
||||
public class User {
|
||||
@OneToMany(fetch = FetchType.EAGER) // 危险!
|
||||
private List<Order> orders;
|
||||
}
|
||||
|
||||
// 业务代码
|
||||
List<User> users = userRepo.findAll(); // 1 条 SQL
|
||||
for (User user : users) {
|
||||
// 如果是 Lazy,这里会触发 N 条 SQL
|
||||
System.out.println(user.getOrders().size());
|
||||
}
|
||||
|
||||
// ✅ 使用 @EntityGraph 或 JOIN FETCH
|
||||
@Query("SELECT u FROM User u JOIN FETCH u.orders")
|
||||
List<User> findAllWithOrders();
|
||||
```
|
||||
|
||||
### 事务管理
|
||||
|
||||
```java
|
||||
// ❌ 在 Controller 层开启事务(数据库连接占用时间过长)
|
||||
// ❌ 在 private 方法上加 @Transactional(AOP 不生效)
|
||||
@Transactional
|
||||
private void saveInternal() { ... }
|
||||
|
||||
// ✅ 在 Service 层公共方法加 @Transactional
|
||||
// ✅ 读操作显式标记 readOnly = true (性能优化)
|
||||
@Service
|
||||
public class UserService {
|
||||
@Transactional(readOnly = true)
|
||||
public User getUser(Long id) { ... }
|
||||
|
||||
@Transactional
|
||||
public void createUser(UserDto dto) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### Entity 设计
|
||||
|
||||
```java
|
||||
// ❌ 在 Entity 中使用 Lombok @Data
|
||||
// @Data 生成的 equals/hashCode 包含所有字段,可能触发懒加载导致性能问题或异常
|
||||
@Entity
|
||||
@Data
|
||||
public class User { ... }
|
||||
|
||||
// ✅ 仅使用 @Getter, @Setter
|
||||
// ✅ 自定义 equals/hashCode (通常基于 ID)
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
public class User {
|
||||
@Id
|
||||
private Long id;
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof User)) return false;
|
||||
return id != null && id.equals(((User) o).id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getClass().hashCode();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 并发与虚拟线程
|
||||
|
||||
### 虚拟线程 (Java 21+)
|
||||
|
||||
```java
|
||||
// ❌ 传统线程池处理大量 I/O 阻塞任务(资源耗尽)
|
||||
ExecutorService executor = Executors.newFixedThreadPool(100);
|
||||
|
||||
// ✅ 使用虚拟线程处理 I/O 密集型任务(高吞吐量)
|
||||
// Spring Boot 3.2+ 开启:spring.threads.virtual.enabled=true
|
||||
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
|
||||
|
||||
// 在虚拟线程中,阻塞操作(如 DB 查询、HTTP 请求)几乎不消耗 OS 线程资源
|
||||
```
|
||||
|
||||
### 线程安全
|
||||
|
||||
```java
|
||||
// ❌ SimpleDateFormat 是线程不安全的
|
||||
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
|
||||
|
||||
// ✅ 使用 DateTimeFormatter (Java 8+)
|
||||
private static final DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
|
||||
// ❌ HashMap 在多线程环境会数据丢失(Java 7 及之前 resize 还可能死循环,Java 8 修复了死循环但仍非线程安全)
|
||||
// ✅ 使用 ConcurrentHashMap
|
||||
Map<String, String> cache = new ConcurrentHashMap<>();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Lombok 使用规范
|
||||
|
||||
```java
|
||||
// ❌ 滥用 @Builder 导致无法强制校验必填字段
|
||||
@Builder
|
||||
public class Order {
|
||||
private String id; // 必填
|
||||
private String note; // 选填
|
||||
}
|
||||
// 调用者可能漏掉 id: Order.builder().note("hi").build();
|
||||
|
||||
// ✅ 关键业务对象建议手动编写 Builder 或构造函数以确保不变量
|
||||
// 或者在 build() 方法中添加校验逻辑 (Lombok @Builder.Default 等)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 异常处理
|
||||
|
||||
### 全局异常处理
|
||||
|
||||
```java
|
||||
// ❌ 到处 try-catch 吞掉异常或只打印日志
|
||||
try {
|
||||
userService.create(user);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace(); // 不应该在生产环境使用
|
||||
// return null; // 吞掉异常,上层不知道发生了什么
|
||||
}
|
||||
|
||||
// ✅ 自定义异常 + @ControllerAdvice (Spring Boot 3 ProblemDetail)
|
||||
public class UserNotFoundException extends RuntimeException { ... }
|
||||
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
@ExceptionHandler(UserNotFoundException.class)
|
||||
public ProblemDetail handleNotFound(UserNotFoundException e) {
|
||||
return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, e.getMessage());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 测试规范
|
||||
|
||||
### 单元测试 vs 集成测试
|
||||
|
||||
```java
|
||||
// ❌ 单元测试依赖真实数据库或外部服务
|
||||
@SpringBootTest // 启动整个 Context,慢
|
||||
public class UserServiceTest { ... }
|
||||
|
||||
// ✅ 单元测试使用 Mockito
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class UserServiceTest {
|
||||
@Mock UserRepository repo;
|
||||
@InjectMocks UserService service;
|
||||
|
||||
@Test
|
||||
void shouldCreateUser() { ... }
|
||||
}
|
||||
|
||||
// ✅ 集成测试使用 Testcontainers
|
||||
@Testcontainers
|
||||
@SpringBootTest
|
||||
class UserRepositoryTest {
|
||||
@Container
|
||||
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15");
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Review Checklist
|
||||
|
||||
### 基础与规范
|
||||
- [ ] 遵循 Java 17/21 新特性(Switch 表达式, Records, 文本块)
|
||||
- [ ] 避免使用已过时的类(Date, Calendar, SimpleDateFormat)
|
||||
- [ ] 集合操作是否优先使用了 Stream API 或 Collections 方法?
|
||||
- [ ] Optional 仅用于返回值,未用于字段或参数
|
||||
|
||||
### Spring Boot
|
||||
- [ ] 使用构造器注入而非 @Autowired 字段注入
|
||||
- [ ] 配置属性使用了 @ConfigurationProperties
|
||||
- [ ] Controller 职责单一,业务逻辑下沉到 Service
|
||||
- [ ] 全局异常处理使用了 @ControllerAdvice / ProblemDetail
|
||||
|
||||
### 数据库 & 事务
|
||||
- [ ] 读操作事务标记了 `@Transactional(readOnly = true)`
|
||||
- [ ] 检查是否存在 N+1 查询(EAGER fetch 或循环调用)
|
||||
- [ ] Entity 类未使用 @Data,正确实现了 equals/hashCode
|
||||
- [ ] 数据库索引是否覆盖了查询条件
|
||||
|
||||
### 并发与性能
|
||||
- [ ] I/O 密集型任务是否考虑了虚拟线程?
|
||||
- [ ] 线程安全类是否使用正确(ConcurrentHashMap vs HashMap)
|
||||
- [ ] 锁的粒度是否合理?避免在锁内进行 I/O 操作
|
||||
|
||||
### 可维护性
|
||||
- [ ] 关键业务逻辑有充分的单元测试
|
||||
- [ ] 日志记录恰当(使用 Slf4j,避免 System.out)
|
||||
- [ ] 魔法值提取为常量或枚举
|
||||
+1016
File diff suppressed because it is too large
Load Diff
+593
@@ -0,0 +1,593 @@
|
||||
# NestJS Code Review Guide
|
||||
|
||||
> NestJS 代码审查指南,覆盖依赖注入与分层架构、模块组织、Guard/Interceptor/Pipe、DTO 验证、错误处理、循环依赖及测试模式等核心主题。
|
||||
|
||||
## 目录
|
||||
|
||||
- [依赖注入与分层架构](#依赖注入与分层架构)
|
||||
- [模块组织](#模块组织)
|
||||
- [Guard / Interceptor / Pipe](#guard--interceptor--pipe)
|
||||
- [验证模式 (DTO)](#验证模式-dto)
|
||||
- [错误处理](#错误处理)
|
||||
- [循环依赖](#循环依赖)
|
||||
- [测试模式](#测试模式)
|
||||
- [Review Checklist](#review-checklist)
|
||||
|
||||
---
|
||||
|
||||
## 依赖注入与分层架构
|
||||
|
||||
### 三层架构:Controller → Service → Repository
|
||||
|
||||
```typescript
|
||||
// ❌ ORM 直接注入 Controller,跳过 Service 层
|
||||
@Controller('users')
|
||||
export class UsersController {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
@Get()
|
||||
findAll() {
|
||||
return this.prisma.user.findMany();
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Controller → Service → Repository
|
||||
@Controller('users')
|
||||
export class UsersController {
|
||||
constructor(private readonly usersService: UsersService) {}
|
||||
|
||||
@Get()
|
||||
findAll() {
|
||||
return this.usersService.findAll();
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(private readonly usersRepo: UsersRepository) {}
|
||||
|
||||
findAll() {
|
||||
return this.usersRepo.findAll();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Repository 之间不应互相注入
|
||||
|
||||
```typescript
|
||||
// ❌ Repository 导入另一个 Repository——编排逻辑属于 Service
|
||||
@Injectable()
|
||||
export class OrdersRepository {
|
||||
constructor(private readonly usersRepository: UsersRepository) {}
|
||||
}
|
||||
|
||||
// ✅ 跨 Repository 编排在 Service 中完成
|
||||
@Injectable()
|
||||
export class OrdersService {
|
||||
constructor(
|
||||
private readonly ordersRepo: OrdersRepository,
|
||||
private readonly usersRepo: UsersRepository,
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
### God Service:依赖超过 8 个时拆分
|
||||
|
||||
```typescript
|
||||
// ❌ 9 个依赖的巨型 Service
|
||||
@Injectable()
|
||||
export class OrdersService {
|
||||
constructor(
|
||||
private readonly ordersRepo: OrdersRepository,
|
||||
private readonly usersRepo: UsersRepository,
|
||||
private readonly productsRepo: ProductsRepository,
|
||||
private readonly paymentsService: PaymentsService,
|
||||
private readonly mailerService: MailerService,
|
||||
private readonly inventoryService: InventoryService,
|
||||
private readonly discountService: DiscountService,
|
||||
private readonly taxService: TaxService,
|
||||
private readonly auditService: AuditService,
|
||||
) {}
|
||||
}
|
||||
|
||||
// ✅ 拆分为 Use-Case Service(一个文件一个操作)
|
||||
@Injectable()
|
||||
export class CreateOrderService {
|
||||
constructor(
|
||||
private readonly ordersRepo: OrdersRepository,
|
||||
private readonly paymentsService: PaymentsService,
|
||||
) {}
|
||||
|
||||
async execute(dto: CreateOrderDto) { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
### Symbol Token 实现依赖反转
|
||||
|
||||
```typescript
|
||||
// ❌ 直接依赖具体实现——测试时无法替换
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(private readonly repo: TypeOrmUserRepository) {}
|
||||
}
|
||||
|
||||
// ✅ 接口 + Symbol Token——可替换为内存实现
|
||||
export const USER_REPOSITORY = Symbol('USER_REPOSITORY');
|
||||
|
||||
export interface UserRepository {
|
||||
findAll(): Promise<User[]>;
|
||||
findById(id: string): Promise<User | null>;
|
||||
}
|
||||
|
||||
// module:
|
||||
{
|
||||
provide: USER_REPOSITORY,
|
||||
useClass: TypeOrmUserRepository,
|
||||
}
|
||||
|
||||
// service:
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(@Inject(USER_REPOSITORY) private readonly repo: UserRepository) {}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 模块组织
|
||||
|
||||
### 推荐四层结构
|
||||
|
||||
```
|
||||
src/
|
||||
common/ ← 全局技术基础设施(Guards、Filters、Interceptors、Decorators)
|
||||
core/ ← 内部基础设施(Config、Database、Queue 配置)
|
||||
integrations/ ← 外部服务封装(Mailer、Storage、Stripe、SMS)
|
||||
modules/ ← 按领域组织的业务逻辑
|
||||
[feature]/
|
||||
dtos/
|
||||
repositories/
|
||||
services/
|
||||
internal/ ← 模块内共享 Service
|
||||
use-cases/ ← 一个文件 = 一个操作
|
||||
types/
|
||||
[feature].controller.ts
|
||||
[feature].module.ts
|
||||
```
|
||||
|
||||
### Domain 必须框架无关
|
||||
|
||||
```typescript
|
||||
// ❌ Domain Entity 依赖 NestJS——不可独立测试
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class User {
|
||||
constructor(private readonly email: string) {}
|
||||
}
|
||||
|
||||
// ✅ Domain 是纯类,无框架装饰器
|
||||
export class User {
|
||||
private constructor(private readonly email: string) {}
|
||||
|
||||
static create(email: string): User {
|
||||
return new User(email);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 关键规则
|
||||
|
||||
- `common/` 必须 **不涉及业务**——如果需要知道"订单",它不属于这里
|
||||
- `integrations/` 封装每个外部服务;换 SendGrid → AWS SES 只改一个目录
|
||||
- 使用 **Use-Case Service**(一个文件一个操作)而非 15 个方法的巨型 `XxxService`
|
||||
|
||||
---
|
||||
|
||||
## Guard / Interceptor / Pipe
|
||||
|
||||
### 业务逻辑不应放在 Guard 中
|
||||
|
||||
```typescript
|
||||
// ❌ Guard 中查询数据库 + 业务判断
|
||||
@Injectable()
|
||||
export class OrderOwnershipGuard implements CanActivate {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const req = context.switchToHttp().getRequest();
|
||||
const order = await this.prisma.order.findUnique({
|
||||
where: { id: req.params.id },
|
||||
});
|
||||
if (order.userId !== req.user.id) {
|
||||
return false; // 数据获取 + 业务规则判断都在 Guard 里
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Guard 只做授权检查(角色/权限)
|
||||
@Injectable()
|
||||
export class RolesGuard implements CanActivate {
|
||||
constructor(private readonly reflector: Reflector) {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
if (!requiredRoles) return true;
|
||||
const { user } = context.switchToHttp().getRequest();
|
||||
return requiredRoles.some((role) => user.roles?.includes(role));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Interceptor 只用于横切关注点
|
||||
|
||||
```typescript
|
||||
// ❌ Interceptor 中执行业务逻辑
|
||||
@Injectable()
|
||||
export class PricingInterceptor implements NestInterceptor {
|
||||
intercept(context: ExecutionContext, next: CallHandler) {
|
||||
// 计算折扣——这不是横切关注点!
|
||||
return next.handle().pipe(map(data => applyDiscount(data)));
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Interceptor 用于日志、缓存、响应转换、计时
|
||||
@Injectable()
|
||||
export class LoggingInterceptor implements NestInterceptor {
|
||||
intercept(context: ExecutionContext, next: CallHandler) {
|
||||
const now = Date.now();
|
||||
const req = context.switchToHttp().getRequest();
|
||||
return next.handle().pipe(
|
||||
tap(() => console.log(`${req.method} ${req.url} - ${Date.now() - now}ms`)),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 全局 ValidationPipe 必须配置 whitelist
|
||||
|
||||
```typescript
|
||||
// ❌ 没有 whitelist——请求体中的额外属性直接传入
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
await app.listen(3000);
|
||||
}
|
||||
|
||||
// ✅ 全局 ValidationPipe + whitelist 过滤未知属性
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
transform: true,
|
||||
}),
|
||||
);
|
||||
await app.listen(3000);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 验证模式 (DTO)
|
||||
|
||||
### @ValidateNested() 必须搭配 @Type()
|
||||
|
||||
```typescript
|
||||
// ❌ 只有 @ValidateNested——嵌套对象验证被静默跳过!
|
||||
export class CreateOrderDto {
|
||||
@ValidateNested()
|
||||
shipping: AddressDto;
|
||||
}
|
||||
|
||||
// ✅ @ValidateNested + @Type 配对使用
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class CreateOrderDto {
|
||||
@ValidateNested()
|
||||
@Type(() => AddressDto)
|
||||
shipping: AddressDto;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => OrderItemDto)
|
||||
items: OrderItemDto[];
|
||||
}
|
||||
```
|
||||
|
||||
### 禁止裸 any Body
|
||||
|
||||
```typescript
|
||||
// ❌ 没有 DTO——无验证、无类型安全、无 Swagger 文档
|
||||
@Post()
|
||||
create(@Body() body: any) {
|
||||
return this.service.create(body);
|
||||
}
|
||||
|
||||
// ✅ 为每个操作创建 DTO
|
||||
export class CreateUserDto {
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
@MaxLength(100)
|
||||
name: string;
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(@Body() dto: CreateUserDto) {
|
||||
return this.service.create(dto);
|
||||
}
|
||||
```
|
||||
|
||||
### Create 和 Update 应使用不同 DTO
|
||||
|
||||
```typescript
|
||||
// ❌ PATCH 也要求所有字段——不合理的 API 设计
|
||||
@Patch(':id')
|
||||
update(@Body() dto: CreateUserDto) { /* all fields required */ }
|
||||
|
||||
// ✅ Update 使用 PartialType
|
||||
export class UpdateUserDto extends PartialType(CreateUserDto) {}
|
||||
|
||||
@Patch(':id')
|
||||
update(@Body() dto: UpdateUserDto) { /* all fields optional */ }
|
||||
```
|
||||
|
||||
### 可选嵌套对象
|
||||
|
||||
```typescript
|
||||
// ❌ 可选嵌套对象缺少 @IsOptional
|
||||
export class UpdateOrderDto {
|
||||
@ValidateNested()
|
||||
@Type(() => AddressDto)
|
||||
shipping?: AddressDto; // undefined 时仍尝试验证
|
||||
}
|
||||
|
||||
// ✅ @IsOptional + @ValidateNested + @Type
|
||||
export class UpdateOrderDto {
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => AddressDto)
|
||||
shipping?: AddressDto;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 禁止吞掉错误
|
||||
|
||||
```typescript
|
||||
// ❌ catch { return null }——隐藏了问题,调用者无法区分"不存在"和"出错了"
|
||||
async findOne(id: string) {
|
||||
try {
|
||||
return await this.repo.findById(id);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 抛出有意义的异常
|
||||
async findOne(id: string): Promise<User> {
|
||||
const user = await this.repo.findById(id);
|
||||
if (!user) {
|
||||
throw new NotFoundException(`User ${id} not found`);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
```
|
||||
|
||||
### 使用内置异常类
|
||||
|
||||
```typescript
|
||||
// ❌ 手动构造 HTTP 响应
|
||||
throw new HttpException('Bad request', 400);
|
||||
|
||||
// ✅ 使用语义化的内置异常
|
||||
throw new BadRequestException('Invalid email format');
|
||||
throw new NotFoundException('User not found');
|
||||
throw new ConflictException('Email already taken');
|
||||
throw new ForbiddenException('Insufficient permissions');
|
||||
throw new UnauthorizedException('Invalid credentials');
|
||||
```
|
||||
|
||||
### 自定义异常过滤器
|
||||
|
||||
```typescript
|
||||
// ✅ 全局异常过滤器——统一响应格式
|
||||
@Catch()
|
||||
export class AllExceptionsFilter implements ExceptionFilter {
|
||||
private readonly logger = new Logger(AllExceptionsFilter.name);
|
||||
|
||||
catch(exception: unknown, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse();
|
||||
const request = ctx.getRequest();
|
||||
|
||||
const status =
|
||||
exception instanceof HttpException
|
||||
? exception.getStatus()
|
||||
: HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
|
||||
this.logger.error(`${request.method} ${request.url} - ${status}`, exception instanceof Error ? exception.stack : '');
|
||||
|
||||
response.status(status).json({
|
||||
statusCode: status,
|
||||
timestamp: new Date().toISOString(),
|
||||
path: request.url,
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 循环依赖
|
||||
|
||||
### 模块间循环引用
|
||||
|
||||
```typescript
|
||||
// ❌ Module A ↔ Module B
|
||||
@Module({ imports: [UsersModule] })
|
||||
export class OrdersModule {}
|
||||
|
||||
@Module({ imports: [OrdersModule] })
|
||||
export class UsersModule {}
|
||||
|
||||
// ✅ 提取共享逻辑到第三个模块
|
||||
@Module({
|
||||
providers: [SharedService],
|
||||
exports: [SharedService],
|
||||
})
|
||||
export class SharedModule {}
|
||||
|
||||
@Module({ imports: [SharedModule] })
|
||||
export class OrdersModule {}
|
||||
|
||||
@Module({ imports: [SharedModule] })
|
||||
export class UsersModule {}
|
||||
```
|
||||
|
||||
### forwardRef 是最后手段
|
||||
|
||||
```typescript
|
||||
// ⚠️ forwardRef 表示设计有问题——优先重新设计
|
||||
@Module({
|
||||
imports: [forwardRef(() => UsersModule)],
|
||||
})
|
||||
export class OrdersModule {}
|
||||
|
||||
// ✅ 重新设计消除循环:
|
||||
// 1. 提取共享模块
|
||||
// 2. 使用事件驱动(EventEmitter)代替直接调用
|
||||
// 3. 将共享逻辑提升到上层 Service
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 测试模式
|
||||
|
||||
### Use-Case 可脱离 NestJS 测试
|
||||
|
||||
```typescript
|
||||
// ✅ 无需 NestFactory——直接 new
|
||||
describe('CreateUserHandler', () => {
|
||||
let handler: CreateUserHandler;
|
||||
let repo: InMemoryUserRepository;
|
||||
|
||||
beforeEach(() => {
|
||||
repo = new InMemoryUserRepository();
|
||||
handler = new CreateUserHandler(repo);
|
||||
});
|
||||
|
||||
it('creates a user', async () => {
|
||||
const id = await handler.execute(
|
||||
new CreateUserCommand('user@example.com', 'Alice'),
|
||||
);
|
||||
expect(id).toBeDefined();
|
||||
});
|
||||
|
||||
it('rejects duplicate email', async () => {
|
||||
await handler.execute(new CreateUserCommand('user@example.com', 'Alice'));
|
||||
await expect(
|
||||
handler.execute(new CreateUserCommand('user@example.com', 'Bob')),
|
||||
).rejects.toThrow('already exists');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### E2E 测试应配置与生产一致的 Pipes
|
||||
|
||||
```typescript
|
||||
describe('UsersController (e2e)', () => {
|
||||
let app: INestApplication;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleFixture = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = moduleFixture.createNestApplication();
|
||||
// 必须与 main.ts 中相同的全局配置
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
transform: true,
|
||||
}),
|
||||
);
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it('/POST users - valid', () => {
|
||||
return request(app.getHttpServer())
|
||||
.post('/users')
|
||||
.send({ email: 'test@test.com', name: 'Test' })
|
||||
.expect(201);
|
||||
});
|
||||
|
||||
it('/POST users - extra fields rejected', () => {
|
||||
return request(app.getHttpServer())
|
||||
.post('/users')
|
||||
.send({ email: 'test@test.com', name: 'Test', role: 'admin' })
|
||||
.expect(400);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Review Checklist
|
||||
|
||||
### 分层架构
|
||||
|
||||
- [ ] ORM/Prisma 未直接注入 Controller
|
||||
- [ ] 业务逻辑不在 Controller 中
|
||||
- [ ] Repository 之间无互相注入
|
||||
- [ ] Service 依赖数 ≤ 8(超出则拆分为 Use-Case)
|
||||
|
||||
### 依赖注入
|
||||
|
||||
- [ ] 接口 + Symbol Token 用于可替换的依赖
|
||||
- [ ] 无 `forwardRef()`(如有,需设计文档说明原因)
|
||||
- [ ] Scoped 服务未注入到 Singleton 中
|
||||
|
||||
### 验证
|
||||
|
||||
- [ ] 每个 `@ValidateNested()` 都有对应的 `@Type()`
|
||||
- [ ] 全局 `ValidationPipe({ whitelist: true, forbidNonWhitelisted: true })` 已配置
|
||||
- [ ] 无 `@Body() body: any`——必须使用 DTO
|
||||
- [ ] Create 和 Update 使用不同 DTO(`PartialType`)
|
||||
- [ ] 数组验证使用 `{ each: true }`
|
||||
- [ ] 可选嵌套对象使用 `@IsOptional()` + `@ValidateNested()` + `@Type()`
|
||||
|
||||
### Guard / Interceptor / Pipe
|
||||
|
||||
- [ ] Guard 只做授权检查,不查询数据库
|
||||
- [ ] Interceptor 只用于横切关注点(日志、缓存、响应转换)
|
||||
- [ ] 业务规则在 Service 中
|
||||
|
||||
### 错误处理
|
||||
|
||||
- [ ] 无 `catch { return null }`——抛出有意义的异常
|
||||
- [ ] 使用 NestJS 内置异常类
|
||||
- [ ] 自定义异常过滤器在 `common/filters/` 中
|
||||
|
||||
### 模块
|
||||
|
||||
- [ ] 无循环模块引用
|
||||
- [ ] Domain Entity 无框架装饰器(`@Injectable` 等)
|
||||
- [ ] 外部服务调用在 `integrations/` 中
|
||||
|
||||
### 测试
|
||||
|
||||
- [ ] Use-Case Service 可脱离 NestJS 测试
|
||||
- [ ] E2E 测试配置与生产一致的全局 Pipes/Guards
|
||||
- [ ] Domain Entity 零框架依赖
|
||||
@@ -0,0 +1,816 @@
|
||||
# Performance Review Guide
|
||||
|
||||
性能审查指南,覆盖前端、后端、数据库、算法复杂度和 API 性能。
|
||||
|
||||
## 目录
|
||||
|
||||
- [前端性能 (Core Web Vitals)](#前端性能-core-web-vitals)
|
||||
- [JavaScript 性能](#javascript-性能)
|
||||
- [内存管理](#内存管理)
|
||||
- [数据库性能](#数据库性能)
|
||||
- [API 性能](#api-性能)
|
||||
- [算法复杂度](#算法复杂度)
|
||||
- [性能审查清单](#性能审查清单)
|
||||
|
||||
---
|
||||
|
||||
## 前端性能 (Core Web Vitals)
|
||||
|
||||
### 2024 核心指标
|
||||
|
||||
| 指标 | 全称 | 目标值 | 含义 |
|
||||
|------|------|--------|------|
|
||||
| **LCP** | Largest Contentful Paint | ≤ 2.5s | 最大内容绘制时间 |
|
||||
| **INP** | Interaction to Next Paint | ≤ 200ms | 交互响应时间(2024 年替代 FID)|
|
||||
| **CLS** | Cumulative Layout Shift | ≤ 0.1 | 累积布局偏移 |
|
||||
| **FCP** | First Contentful Paint | ≤ 1.8s | 首次内容绘制 |
|
||||
| **TBT** | Total Blocking Time | ≤ 200ms | 主线程阻塞时间 |
|
||||
|
||||
### LCP 优化检查
|
||||
|
||||
```javascript
|
||||
// ❌ LCP 图片懒加载 - 延迟关键内容
|
||||
<img src="hero.jpg" loading="lazy" />
|
||||
|
||||
// ✅ LCP 图片立即加载
|
||||
<img src="hero.jpg" fetchpriority="high" />
|
||||
|
||||
// ❌ 未优化的图片格式
|
||||
<img src="hero.png" /> // PNG 文件过大
|
||||
|
||||
// ✅ 现代图片格式 + 响应式
|
||||
<picture>
|
||||
<source srcset="hero.avif" type="image/avif" />
|
||||
<source srcset="hero.webp" type="image/webp" />
|
||||
<img src="hero.jpg" alt="Hero" />
|
||||
</picture>
|
||||
```
|
||||
|
||||
**审查要点:**
|
||||
- [ ] LCP 元素是否设置 `fetchpriority="high"`?
|
||||
- [ ] 是否使用 WebP/AVIF 格式?
|
||||
- [ ] 是否有服务端渲染或静态生成?
|
||||
- [ ] CDN 是否配置正确?
|
||||
|
||||
### FCP 优化检查
|
||||
|
||||
```html
|
||||
<!-- ❌ 阻塞渲染的 CSS -->
|
||||
<link rel="stylesheet" href="all-styles.css" />
|
||||
|
||||
<!-- ✅ 关键 CSS 内联 + 异步加载其余 -->
|
||||
<style>/* 首屏关键样式 */</style>
|
||||
<link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'" />
|
||||
|
||||
<!-- ❌ 阻塞渲染的字体 -->
|
||||
@font-face {
|
||||
font-family: 'CustomFont';
|
||||
src: url('font.woff2');
|
||||
}
|
||||
|
||||
<!-- ✅ 字体显示优化 -->
|
||||
@font-face {
|
||||
font-family: 'CustomFont';
|
||||
src: url('font.woff2');
|
||||
font-display: swap; /* 先用系统字体,加载后切换 */
|
||||
}
|
||||
```
|
||||
|
||||
### INP 优化检查
|
||||
|
||||
```javascript
|
||||
// ❌ 长任务阻塞主线程
|
||||
button.addEventListener('click', () => {
|
||||
// 耗时 500ms 的同步操作
|
||||
processLargeData(data);
|
||||
updateUI();
|
||||
});
|
||||
|
||||
// ✅ 拆分长任务
|
||||
button.addEventListener('click', async () => {
|
||||
// 让出主线程
|
||||
await scheduler.yield?.() ?? new Promise(r => setTimeout(r, 0));
|
||||
|
||||
// 分批处理
|
||||
for (const chunk of chunks) {
|
||||
processChunk(chunk);
|
||||
await scheduler.yield?.();
|
||||
}
|
||||
updateUI();
|
||||
});
|
||||
|
||||
// ✅ 使用 Web Worker 处理复杂计算
|
||||
const worker = new Worker('heavy-computation.js');
|
||||
worker.postMessage(data);
|
||||
worker.onmessage = (e) => updateUI(e.data);
|
||||
```
|
||||
|
||||
### CLS 优化检查
|
||||
|
||||
```css
|
||||
/* ❌ 未指定尺寸的媒体 */
|
||||
img { width: 100%; }
|
||||
|
||||
/* ✅ 预留空间 */
|
||||
img {
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
|
||||
/* ❌ 动态插入内容导致布局偏移 */
|
||||
.ad-container { }
|
||||
|
||||
/* ✅ 预留固定高度 */
|
||||
.ad-container {
|
||||
min-height: 250px;
|
||||
}
|
||||
```
|
||||
|
||||
**CLS 审查清单:**
|
||||
- [ ] 图片/视频是否有 width/height 或 aspect-ratio?
|
||||
- [ ] 字体加载是否使用 `font-display: swap`?
|
||||
- [ ] 动态内容是否预留空间?
|
||||
- [ ] 是否避免在现有内容上方插入内容?
|
||||
|
||||
---
|
||||
|
||||
## JavaScript 性能
|
||||
|
||||
### 代码分割与懒加载
|
||||
|
||||
```javascript
|
||||
// ❌ 一次性加载所有代码
|
||||
import { HeavyChart } from './charts';
|
||||
import { PDFExporter } from './pdf';
|
||||
import { AdminPanel } from './admin';
|
||||
|
||||
// ✅ 按需加载
|
||||
const HeavyChart = lazy(() => import('./charts'));
|
||||
const PDFExporter = lazy(() => import('./pdf'));
|
||||
|
||||
// ✅ 路由级代码分割
|
||||
const routes = [
|
||||
{
|
||||
path: '/dashboard',
|
||||
component: lazy(() => import('./pages/Dashboard')),
|
||||
},
|
||||
{
|
||||
path: '/admin',
|
||||
component: lazy(() => import('./pages/Admin')),
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
### Bundle 体积优化
|
||||
|
||||
```javascript
|
||||
// ❌ 导入整个库
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
|
||||
// ✅ 按需导入
|
||||
import debounce from 'lodash/debounce';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
// ❌ 未使用 Tree Shaking
|
||||
export default {
|
||||
fn1() {},
|
||||
fn2() {}, // 未使用但被打包
|
||||
};
|
||||
|
||||
// ✅ 命名导出支持 Tree Shaking
|
||||
export function fn1() {}
|
||||
export function fn2() {}
|
||||
```
|
||||
|
||||
**Bundle 审查清单:**
|
||||
- [ ] 是否使用动态 import() 进行代码分割?
|
||||
- [ ] 大型库是否按需导入?
|
||||
- [ ] 是否分析过 bundle 大小?(webpack-bundle-analyzer)
|
||||
- [ ] 是否有未使用的依赖?
|
||||
|
||||
### 列表渲染优化
|
||||
|
||||
```javascript
|
||||
// ❌ 渲染大列表
|
||||
function List({ items }) {
|
||||
return (
|
||||
<ul>
|
||||
{items.map(item => <li key={item.id}>{item.name}</li>)}
|
||||
</ul>
|
||||
); // 10000 条数据 = 10000 个 DOM 节点
|
||||
}
|
||||
|
||||
// ✅ 虚拟列表 - 只渲染可见项
|
||||
import { FixedSizeList } from 'react-window';
|
||||
|
||||
function VirtualList({ items }) {
|
||||
return (
|
||||
<FixedSizeList
|
||||
height={400}
|
||||
itemCount={items.length}
|
||||
itemSize={35}
|
||||
>
|
||||
{({ index, style }) => (
|
||||
<div style={style}>{items[index].name}</div>
|
||||
)}
|
||||
</FixedSizeList>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**大数据审查要点:**
|
||||
- [ ] 列表超过 100 项是否使用虚拟滚动?
|
||||
- [ ] 表格是否支持分页或虚拟化?
|
||||
- [ ] 是否有不必要的全量渲染?
|
||||
|
||||
---
|
||||
|
||||
## 内存管理
|
||||
|
||||
### 常见内存泄漏
|
||||
|
||||
#### 1. 未清理的事件监听
|
||||
|
||||
```javascript
|
||||
// ❌ 组件卸载后事件仍在监听
|
||||
useEffect(() => {
|
||||
window.addEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
// ✅ 清理事件监听
|
||||
useEffect(() => {
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
```
|
||||
|
||||
#### 2. 未清理的定时器
|
||||
|
||||
```javascript
|
||||
// ❌ 定时器未清理
|
||||
useEffect(() => {
|
||||
setInterval(fetchData, 5000);
|
||||
}, []);
|
||||
|
||||
// ✅ 清理定时器
|
||||
useEffect(() => {
|
||||
const timer = setInterval(fetchData, 5000);
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
```
|
||||
|
||||
#### 3. 闭包引用
|
||||
|
||||
```javascript
|
||||
// ❌ 闭包持有大对象引用
|
||||
function createHandler() {
|
||||
const largeData = new Array(1000000).fill('x');
|
||||
|
||||
return function handler() {
|
||||
// largeData 被闭包引用,无法被回收
|
||||
console.log(largeData.length);
|
||||
};
|
||||
}
|
||||
|
||||
// ✅ 只保留必要数据
|
||||
function createHandler() {
|
||||
const largeData = new Array(1000000).fill('x');
|
||||
const length = largeData.length; // 只保留需要的值
|
||||
|
||||
return function handler() {
|
||||
console.log(length);
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. 未清理的订阅
|
||||
|
||||
```javascript
|
||||
// ❌ WebSocket/EventSource 未关闭
|
||||
useEffect(() => {
|
||||
const ws = new WebSocket('wss://...');
|
||||
ws.onmessage = handleMessage;
|
||||
}, []);
|
||||
|
||||
// ✅ 清理连接
|
||||
useEffect(() => {
|
||||
const ws = new WebSocket('wss://...');
|
||||
ws.onmessage = handleMessage;
|
||||
return () => ws.close();
|
||||
}, []);
|
||||
```
|
||||
|
||||
### 内存审查清单
|
||||
|
||||
```markdown
|
||||
- [ ] useEffect 是否都有清理函数?
|
||||
- [ ] 事件监听是否在组件卸载时移除?
|
||||
- [ ] 定时器是否被清理?
|
||||
- [ ] WebSocket/SSE 连接是否关闭?
|
||||
- [ ] 大对象是否及时释放?
|
||||
- [ ] 是否有全局变量累积数据?
|
||||
```
|
||||
|
||||
### 检测工具
|
||||
|
||||
| 工具 | 用途 |
|
||||
|------|------|
|
||||
| Chrome DevTools Memory | 堆快照分析 |
|
||||
| MemLab (Meta) | 自动化内存泄漏检测 |
|
||||
| Performance Monitor | 实时内存监控 |
|
||||
|
||||
---
|
||||
|
||||
## 数据库性能
|
||||
|
||||
### N+1 查询问题
|
||||
|
||||
```python
|
||||
# ❌ N+1 问题 - 1 + N 次查询
|
||||
users = User.objects.all() # 1 次查询
|
||||
for user in users:
|
||||
print(user.profile.bio) # N 次查询(每个用户一次)
|
||||
|
||||
# ✅ Eager Loading - 2 次查询
|
||||
users = User.objects.select_related('profile').all()
|
||||
for user in users:
|
||||
print(user.profile.bio) # 无额外查询
|
||||
|
||||
# ✅ 多对多关系用 prefetch_related
|
||||
posts = Post.objects.prefetch_related('tags').all()
|
||||
```
|
||||
|
||||
```javascript
|
||||
// TypeORM 示例
|
||||
// ❌ N+1 问题
|
||||
const users = await userRepository.find();
|
||||
for (const user of users) {
|
||||
const posts = await user.posts; // 每次循环都查询
|
||||
}
|
||||
|
||||
// ✅ Eager Loading
|
||||
const users = await userRepository.find({
|
||||
relations: ['posts'],
|
||||
});
|
||||
```
|
||||
|
||||
### 索引优化
|
||||
|
||||
```sql
|
||||
-- ❌ 全表扫描
|
||||
SELECT * FROM orders WHERE status = 'pending';
|
||||
|
||||
-- ✅ 添加索引
|
||||
CREATE INDEX idx_orders_status ON orders(status);
|
||||
|
||||
-- ❌ 索引失效:函数操作
|
||||
SELECT * FROM users WHERE YEAR(created_at) = 2024;
|
||||
|
||||
-- ✅ 范围查询可用索引
|
||||
SELECT * FROM users
|
||||
WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01';
|
||||
|
||||
-- ❌ 索引失效:LIKE 前缀通配符
|
||||
SELECT * FROM products WHERE name LIKE '%phone%';
|
||||
|
||||
-- ✅ 前缀匹配可用索引
|
||||
SELECT * FROM products WHERE name LIKE 'phone%';
|
||||
```
|
||||
|
||||
### 查询优化
|
||||
|
||||
```sql
|
||||
-- ❌ SELECT * 获取不需要的列
|
||||
SELECT * FROM users WHERE id = 1;
|
||||
|
||||
-- ✅ 只查询需要的列
|
||||
SELECT id, name, email FROM users WHERE id = 1;
|
||||
|
||||
-- ❌ 大表无 LIMIT
|
||||
SELECT * FROM logs WHERE type = 'error';
|
||||
|
||||
-- ✅ 分页查询
|
||||
SELECT * FROM logs WHERE type = 'error' LIMIT 100 OFFSET 0;
|
||||
|
||||
-- ❌ 在循环中执行查询
|
||||
for id in user_ids:
|
||||
cursor.execute("SELECT * FROM users WHERE id = %s", (id,))
|
||||
|
||||
-- ✅ 批量查询
|
||||
cursor.execute("SELECT * FROM users WHERE id IN %s", (tuple(user_ids),))
|
||||
```
|
||||
|
||||
### 数据库审查清单
|
||||
|
||||
```markdown
|
||||
🔴 必须检查:
|
||||
- [ ] 是否存在 N+1 查询?
|
||||
- [ ] WHERE 子句列是否有索引?
|
||||
- [ ] 是否避免了 SELECT *?
|
||||
- [ ] 大表查询是否有 LIMIT?
|
||||
|
||||
🟡 建议检查:
|
||||
- [ ] 是否使用了 EXPLAIN 分析查询计划?
|
||||
- [ ] 复合索引列顺序是否正确?
|
||||
- [ ] 是否有未使用的索引?
|
||||
- [ ] 是否有慢查询日志监控?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API 性能
|
||||
|
||||
### 分页实现
|
||||
|
||||
```javascript
|
||||
// ❌ 返回全部数据
|
||||
app.get('/users', async (req, res) => {
|
||||
const users = await User.findAll(); // 可能返回 100000 条
|
||||
res.json(users);
|
||||
});
|
||||
|
||||
// ✅ 分页 + 限制最大数量
|
||||
app.get('/users', async (req, res) => {
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = Math.min(parseInt(req.query.limit) || 20, 100); // 最大 100
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const { rows, count } = await User.findAndCountAll({
|
||||
limit,
|
||||
offset,
|
||||
order: [['id', 'ASC']],
|
||||
});
|
||||
|
||||
res.json({
|
||||
data: rows,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total: count,
|
||||
totalPages: Math.ceil(count / limit),
|
||||
},
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 缓存策略
|
||||
|
||||
```javascript
|
||||
// ✅ Redis 缓存示例
|
||||
async function getUser(id) {
|
||||
const cacheKey = `user:${id}`;
|
||||
|
||||
// 1. 检查缓存
|
||||
const cached = await redis.get(cacheKey);
|
||||
if (cached) {
|
||||
return JSON.parse(cached);
|
||||
}
|
||||
|
||||
// 2. 查询数据库
|
||||
const user = await db.users.findById(id);
|
||||
|
||||
// 3. 写入缓存(设置过期时间)
|
||||
await redis.setex(cacheKey, 3600, JSON.stringify(user));
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
// ✅ HTTP 缓存头
|
||||
app.get('/static-data', (req, res) => {
|
||||
res.set({
|
||||
'Cache-Control': 'public, max-age=86400', // 24 小时
|
||||
'ETag': 'abc123',
|
||||
});
|
||||
res.json(data);
|
||||
});
|
||||
```
|
||||
|
||||
### 响应压缩
|
||||
|
||||
```javascript
|
||||
// ✅ 启用 Gzip/Brotli 压缩
|
||||
const compression = require('compression');
|
||||
app.use(compression());
|
||||
|
||||
// ✅ 只返回必要字段
|
||||
// 请求: GET /users?fields=id,name,email
|
||||
app.get('/users', async (req, res) => {
|
||||
const fields = req.query.fields?.split(',') || ['id', 'name'];
|
||||
const users = await User.findAll({
|
||||
attributes: fields,
|
||||
});
|
||||
res.json(users);
|
||||
});
|
||||
```
|
||||
|
||||
### 限流保护
|
||||
|
||||
```javascript
|
||||
// ✅ 速率限制
|
||||
const rateLimit = require('express-rate-limit');
|
||||
|
||||
const limiter = rateLimit({
|
||||
windowMs: 60 * 1000, // 1 分钟
|
||||
max: 100, // 最多 100 次请求
|
||||
message: { error: 'Too many requests, please try again later.' },
|
||||
});
|
||||
|
||||
app.use('/api/', limiter);
|
||||
```
|
||||
|
||||
### API 审查清单
|
||||
|
||||
```markdown
|
||||
- [ ] 列表接口是否有分页?
|
||||
- [ ] 是否限制了每页最大数量?
|
||||
- [ ] 热点数据是否有缓存?
|
||||
- [ ] 是否启用了响应压缩?
|
||||
- [ ] 是否有速率限制?
|
||||
- [ ] 是否只返回必要字段?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 算法复杂度
|
||||
|
||||
### 常见复杂度对比
|
||||
|
||||
| 复杂度 | 名称 | 10 条 | 1000 条 | 100 万条 | 示例 |
|
||||
|--------|------|-------|---------|----------|------|
|
||||
| O(1) | 常数 | 1 | 1 | 1 | 哈希查找 |
|
||||
| O(log n) | 对数 | 3 | 10 | 20 | 二分查找 |
|
||||
| O(n) | 线性 | 10 | 1000 | 100 万 | 遍历数组 |
|
||||
| O(n log n) | 线性对数 | 33 | 10000 | 2000 万 | 快速排序 |
|
||||
| O(n²) | 平方 | 100 | 100 万 | 1 万亿 | 嵌套循环 |
|
||||
| O(2ⁿ) | 指数 | 1024 | ∞ | ∞ | 递归斐波那契 |
|
||||
|
||||
### 代码审查中的识别
|
||||
|
||||
```javascript
|
||||
// ❌ O(n²) - 嵌套循环
|
||||
function findDuplicates(arr) {
|
||||
const duplicates = [];
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
for (let j = i + 1; j < arr.length; j++) {
|
||||
if (arr[i] === arr[j]) {
|
||||
duplicates.push(arr[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return duplicates;
|
||||
}
|
||||
|
||||
// ✅ O(n) - 使用 Set
|
||||
function findDuplicates(arr) {
|
||||
const seen = new Set();
|
||||
const duplicates = new Set();
|
||||
for (const item of arr) {
|
||||
if (seen.has(item)) {
|
||||
duplicates.add(item);
|
||||
}
|
||||
seen.add(item);
|
||||
}
|
||||
return [...duplicates];
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
// ❌ O(n²) - 每次循环都调用 includes
|
||||
function removeDuplicates(arr) {
|
||||
const result = [];
|
||||
for (const item of arr) {
|
||||
if (!result.includes(item)) { // includes 是 O(n)
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ✅ O(n) - 使用 Set
|
||||
function removeDuplicates(arr) {
|
||||
return [...new Set(arr)];
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
// ❌ O(n) 查找 - 每次都遍历
|
||||
const users = [{ id: 1, name: 'A' }, { id: 2, name: 'B' }, ...];
|
||||
|
||||
function getUser(id) {
|
||||
return users.find(u => u.id === id); // O(n)
|
||||
}
|
||||
|
||||
// ✅ O(1) 查找 - 使用 Map
|
||||
const userMap = new Map(users.map(u => [u.id, u]));
|
||||
|
||||
function getUser(id) {
|
||||
return userMap.get(id); // O(1)
|
||||
}
|
||||
```
|
||||
|
||||
### 空间复杂度考虑
|
||||
|
||||
```javascript
|
||||
// ⚠️ O(n) 空间 - 创建新数组
|
||||
const doubled = arr.map(x => x * 2);
|
||||
|
||||
// ✅ O(1) 空间 - 原地修改(如果允许)
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
arr[i] *= 2;
|
||||
}
|
||||
|
||||
// ⚠️ 递归深度过大可能栈溢出
|
||||
function factorial(n) {
|
||||
if (n <= 1) return 1;
|
||||
return n * factorial(n - 1); // O(n) 栈空间
|
||||
}
|
||||
|
||||
// ✅ 迭代版本 O(1) 空间
|
||||
function factorial(n) {
|
||||
let result = 1;
|
||||
for (let i = 2; i <= n; i++) {
|
||||
result *= i;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
### 复杂度审查问题
|
||||
|
||||
```markdown
|
||||
💡 "这个嵌套循环的复杂度是 O(n²),数据量大时会有性能问题"
|
||||
🔴 "这里用 Array.includes() 在循环中,整体是 O(n²),建议用 Set"
|
||||
🟡 "这个递归深度可能导致栈溢出,建议改为迭代或尾递归"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 性能审查清单
|
||||
|
||||
### 🔴 必须检查(阻塞级)
|
||||
|
||||
**前端:**
|
||||
- [ ] LCP 图片是否懒加载?(不应该)
|
||||
- [ ] 是否有 `transition: all`?
|
||||
- [ ] 是否动画 width/height/top/left?
|
||||
- [ ] 列表 >100 项是否虚拟化?
|
||||
|
||||
**后端:**
|
||||
- [ ] 是否存在 N+1 查询?
|
||||
- [ ] 列表接口是否有分页?
|
||||
- [ ] 是否有 SELECT * 查大表?
|
||||
|
||||
**通用:**
|
||||
- [ ] 是否有 O(n²) 或更差的嵌套循环?
|
||||
- [ ] useEffect/事件监听是否有清理?
|
||||
|
||||
### 🟡 建议检查(重要级)
|
||||
|
||||
**前端:**
|
||||
- [ ] 是否使用代码分割?
|
||||
- [ ] 大型库是否按需导入?
|
||||
- [ ] 图片是否使用 WebP/AVIF?
|
||||
- [ ] 是否有未使用的依赖?
|
||||
|
||||
**后端:**
|
||||
- [ ] 热点数据是否有缓存?
|
||||
- [ ] WHERE 列是否有索引?
|
||||
- [ ] 是否有慢查询监控?
|
||||
|
||||
**API:**
|
||||
- [ ] 是否启用响应压缩?
|
||||
- [ ] 是否有速率限制?
|
||||
- [ ] 是否只返回必要字段?
|
||||
|
||||
### 🟢 优化建议(建议级)
|
||||
|
||||
- [ ] 是否分析过 bundle 大小?
|
||||
- [ ] 是否使用 CDN?
|
||||
- [ ] 是否有性能监控?
|
||||
- [ ] 是否做过性能基准测试?
|
||||
|
||||
---
|
||||
|
||||
## 性能度量阈值
|
||||
|
||||
### 前端指标
|
||||
|
||||
| 指标 | 好 | 需改进 | 差 |
|
||||
|------|-----|--------|-----|
|
||||
| LCP | ≤ 2.5s | 2.5-4s | > 4s |
|
||||
| INP | ≤ 200ms | 200-500ms | > 500ms |
|
||||
| CLS | ≤ 0.1 | 0.1-0.25 | > 0.25 |
|
||||
| FCP | ≤ 1.8s | 1.8-3s | > 3s |
|
||||
| Bundle Size (JS) | < 200KB | 200-500KB | > 500KB |
|
||||
|
||||
### 后端指标
|
||||
|
||||
| 指标 | 好 | 需改进 | 差 |
|
||||
|------|-----|--------|-----|
|
||||
| API 响应时间 | < 100ms | 100-500ms | > 500ms |
|
||||
| 数据库查询 | < 50ms | 50-200ms | > 200ms |
|
||||
| 页面加载 | < 3s | 3-5s | > 5s |
|
||||
|
||||
---
|
||||
|
||||
## 工具推荐
|
||||
|
||||
### 前端性能
|
||||
|
||||
| 工具 | 用途 |
|
||||
|------|------|
|
||||
| [Lighthouse](https://developer.chrome.com/docs/lighthouse/) | Core Web Vitals 测试 |
|
||||
| [WebPageTest](https://www.webpagetest.org/) | 详细性能分析 |
|
||||
| [webpack-bundle-analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer) | Bundle 分析 |
|
||||
| [Chrome DevTools Performance](https://developer.chrome.com/docs/devtools/performance/) | 运行时性能分析 |
|
||||
|
||||
### 内存检测
|
||||
|
||||
| 工具 | 用途 |
|
||||
|------|------|
|
||||
| [MemLab](https://github.com/facebookincubator/memlab) | 自动化内存泄漏检测 |
|
||||
| Chrome Memory Tab | 堆快照分析 |
|
||||
|
||||
### 后端性能
|
||||
|
||||
| 工具 | 用途 |
|
||||
|------|------|
|
||||
| EXPLAIN | 数据库查询计划分析 |
|
||||
| [pganalyze](https://pganalyze.com/) | PostgreSQL 性能监控 |
|
||||
| [New Relic](https://newrelic.com/) / [Datadog](https://www.datadoghq.com/) | APM 监控 |
|
||||
|
||||
---
|
||||
|
||||
## 低级别效率反模式
|
||||
|
||||
代码层面的效率失误,独立于架构层面的性能问题。补充 [common-bugs-checklist.md](common-bugs-checklist.md) 中已涵盖的资源管理与并发缺陷。
|
||||
|
||||
### 不必要的重复工作
|
||||
|
||||
- [ ] 同一函数 / 查询是否在同一 request/render 中被重复调用?
|
||||
- [ ] 文件 / 配置是否在循环内重复读取(loop-invariant)?
|
||||
- [ ] 计算结果是否可以被缓存或向下游传递?
|
||||
|
||||
```typescript
|
||||
// ❌ loop-invariant 在循环内反复执行
|
||||
for (const path of paths) {
|
||||
const config = JSON.parse(fs.readFileSync("config.json", "utf-8"));
|
||||
processFile(path, config);
|
||||
}
|
||||
|
||||
// ✅ 提到循环外
|
||||
const config = JSON.parse(fs.readFileSync("config.json", "utf-8"));
|
||||
for (const path of paths) processFile(path, config);
|
||||
```
|
||||
|
||||
### 错失的并发机会
|
||||
|
||||
- [ ] 独立的 async 操作是否顺序 `await`?
|
||||
- [ ] 是否可以用 `Promise.all` / `asyncio.gather` / `tokio::join!` 并发?
|
||||
|
||||
```typescript
|
||||
// ❌ 顺序 await
|
||||
const a = await fetchA();
|
||||
const b = await fetchB();
|
||||
|
||||
// ✅ 并发
|
||||
const [a, b] = await Promise.all([fetchA(), fetchB()]);
|
||||
```
|
||||
|
||||
### 热路径膨胀
|
||||
|
||||
- [ ] 模块级 / import 时代码是否执行重操作(文件 I/O、网络、大对象构造)?
|
||||
- [ ] per-request 路径是否有可延迟的初始化?
|
||||
- [ ] 启动时代码是否阻塞首次请求?
|
||||
|
||||
### 无界数据结构
|
||||
|
||||
> 资源生命周期相关缺陷(未关闭的连接、未移除的监听器、未清除的定时器)见 [common-bugs-checklist.md → Resource Management](common-bugs-checklist.md#resource-management)。本节聚焦 *容量边界*。
|
||||
|
||||
- [ ] 全局 dict / list / 缓存是否有 `max-size` 或 TTL?
|
||||
- [ ] 累积型数据结构(队列、日志、metrics buffer)是否有上限?
|
||||
- [ ] 每请求分配的对象是否会被持久引用而无法 GC?
|
||||
|
||||
```python
|
||||
# ❌ 无界缓存
|
||||
_cache: dict[str, Any] = {}
|
||||
|
||||
# ✅ 有界 LRU
|
||||
from functools import lru_cache
|
||||
|
||||
@lru_cache(maxsize=256)
|
||||
def get_cached(key: str) -> Any:
|
||||
return expensive_computation(key)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 参考资源
|
||||
|
||||
- [Core Web Vitals - web.dev](https://web.dev/articles/vitals)
|
||||
- [Optimizing Core Web Vitals - Vercel](https://vercel.com/guides/optimizing-core-web-vitals-in-2024)
|
||||
- [MemLab - Meta Engineering](https://engineering.fb.com/2022/09/12/open-source/memlab/)
|
||||
- [Big O Cheat Sheet](https://www.bigocheatsheet.com/)
|
||||
- [N+1 Query Problem - Stack Overflow](https://stackoverflow.com/questions/97197/what-is-the-n1-selects-problem-in-orm-object-relational-mapping)
|
||||
- [API Performance Optimization](https://algorithmsin60days.com/blog/optimizing-api-performance/)
|
||||
+704
@@ -0,0 +1,704 @@
|
||||
# PHP Code Review Guide
|
||||
|
||||
> PHP 8.x code review guide covering the type system, modern language features, OOP modeling, PDO data access, security, error handling, Composer dependencies, performance, and testing.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Quick Review Checklist](#quick-review-checklist)
|
||||
- [Type System & Modern PHP](#type-system--modern-php)
|
||||
- [Object Modeling](#object-modeling)
|
||||
- [Input, Output & Security](#input-output--security)
|
||||
- [Database Access](#database-access)
|
||||
- [Error Handling](#error-handling)
|
||||
- [Composer & Dependencies](#composer--dependencies)
|
||||
- [Performance & Resource Management](#performance--resource-management)
|
||||
- [Testing & Static Analysis](#testing--static-analysis)
|
||||
- [Review Checklist](#review-checklist)
|
||||
- [References](#references)
|
||||
|
||||
---
|
||||
|
||||
## Quick Review Checklist
|
||||
|
||||
### Must-check
|
||||
|
||||
- [ ] New files enable `declare(strict_types=1);`
|
||||
- [ ] Public APIs have parameter, return, and property types
|
||||
- [ ] User input is validated; output is escaped per context
|
||||
- [ ] SQL uses parameterized queries or ORM binding
|
||||
- [ ] Passwords use `password_hash()` / `password_verify()`
|
||||
- [ ] File uploads validate MIME, size, extension, and storage path
|
||||
- [ ] `composer.lock` is committed; dependency ranges are reasonable
|
||||
- [ ] PHPUnit/Pest tests and PHPStan/Psalm static analysis are present
|
||||
|
||||
### Common issues
|
||||
|
||||
- [ ] Loose comparison `==` / `!=` causing type-juggling vulnerabilities
|
||||
- [ ] `md5()` / `sha1()` used to store passwords
|
||||
- [ ] Concatenating SQL, HTML, shell commands, or file paths
|
||||
- [ ] Using `@` to suppress errors
|
||||
- [ ] `unserialize()` on untrusted data
|
||||
- [ ] `$_GET` / `$_POST` / `$_FILES` flowing straight into business logic
|
||||
- [ ] PHP 8.2+ dynamic properties trigger a deprecation; PHP 9 may turn it into an error
|
||||
|
||||
---
|
||||
|
||||
## Type System & Modern PHP
|
||||
|
||||
### strict_types and explicit types
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ weak boundary: passing "42" gets silently coerced
|
||||
function findUser($id) {
|
||||
return User::find($id);
|
||||
}
|
||||
|
||||
// ✅ enable strict_types at the top of the file; type the public API
|
||||
declare(strict_types=1);
|
||||
|
||||
function findUser(int $id): ?User
|
||||
{
|
||||
return User::find($id);
|
||||
}
|
||||
```
|
||||
|
||||
Don't leave type checking entirely to runtime input validation. Type declarations express an internal contract; input validation expresses how much to trust the boundary. You need both.
|
||||
|
||||
### Avoid loose comparisons
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ strings like "0e12345" can be treated as 0 under loose comparison
|
||||
if ($providedHash == $storedHash) {
|
||||
grantAccess();
|
||||
}
|
||||
|
||||
// ✅ strict comparison; use hash_equals() for secrets or tokens
|
||||
if (hash_equals($storedHash, $providedHash)) {
|
||||
grantAccess();
|
||||
}
|
||||
|
||||
// ✅ match uses identity checks, so fewer type-juggling surprises than switch
|
||||
$status = match ($code) {
|
||||
200 => 'ok',
|
||||
404 => 'not_found',
|
||||
default => 'unknown',
|
||||
};
|
||||
```
|
||||
|
||||
Pay attention to `==`, `!=`, and `in_array($x, $list)` (loose by default) in auth, payment, state machine, and permission logic. Use `===`, `!==`, and `in_array($x, $list, true)` where it matters.
|
||||
|
||||
### Union / intersection / nullable types
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ mixed or untyped makes callers guess the return shape
|
||||
function loadConfig($source) {
|
||||
return parseConfig($source);
|
||||
}
|
||||
|
||||
// ✅ express the real contract with types
|
||||
function loadConfig(string|PathInfo $source): Config
|
||||
{
|
||||
return parseConfig($source);
|
||||
}
|
||||
|
||||
// ✅ make null explicit when it's a real business state
|
||||
function currentUser(): ?User
|
||||
{
|
||||
return Auth::user();
|
||||
}
|
||||
```
|
||||
|
||||
`mixed` can show up at the boundary or while migrating legacy code, but in core business services it usually signals missing modeling.
|
||||
|
||||
### The nullsafe operator shouldn't hide missing state
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ chained nullsafe blurs the reason for failure
|
||||
$country = $order?->customer?->profile?->country;
|
||||
|
||||
// ✅ branch explicitly on critical business state
|
||||
if ($order === null) {
|
||||
throw new OrderNotFound();
|
||||
}
|
||||
|
||||
$customer = $order->customer();
|
||||
if ($customer === null) {
|
||||
throw new MissingCustomer($order->id);
|
||||
}
|
||||
|
||||
$country = $customer->profile()?->country;
|
||||
```
|
||||
|
||||
Distinguish "optional display field" from "business invariant that must exist." The former is a good fit for `?->`; the latter should fail loudly.
|
||||
|
||||
---
|
||||
|
||||
## Object Modeling
|
||||
|
||||
### Use readonly properties and value objects
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ public mutable fields let callers change state at will
|
||||
class Money
|
||||
{
|
||||
public $amount;
|
||||
public $currency;
|
||||
}
|
||||
|
||||
// ✅ express an immutable value object with types and readonly
|
||||
final readonly class Money
|
||||
{
|
||||
public function __construct(
|
||||
public int $amount,
|
||||
public string $currency,
|
||||
) {
|
||||
if ($amount < 0) {
|
||||
throw new InvalidArgumentException('Amount must be non-negative');
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For DTOs, config, and domain value objects, check first whether a `readonly class` or readonly properties can remove hidden side effects.
|
||||
|
||||
### Enums instead of string states
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ string states are easy to typo and can't enumerate the legal set
|
||||
if ($order->status === 'paied') {
|
||||
ship($order);
|
||||
}
|
||||
|
||||
// ✅ an enum surfaces illegal states earlier
|
||||
enum OrderStatus: string
|
||||
{
|
||||
case Pending = 'pending';
|
||||
case Paid = 'paid';
|
||||
case Cancelled = 'cancelled';
|
||||
}
|
||||
|
||||
if ($order->status === OrderStatus::Paid) {
|
||||
ship($order);
|
||||
}
|
||||
```
|
||||
|
||||
When reviewing state machines, permissions, or type fields, look for "magic string values." If the value set is stable, suggest an enum; if it comes from an external system, convert it to an internal enum before it enters the business layer.
|
||||
|
||||
### Don't rely on dynamic properties
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ PHP 8.2+ triggers a deprecation when creating a dynamic property
|
||||
$user = new User();
|
||||
$user->emali = 'a@example.com'; // a typo also silently creates a property
|
||||
|
||||
// ✅ declare properties or use a dedicated data structure
|
||||
final class User
|
||||
{
|
||||
public function __construct(
|
||||
public string $email,
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
`#[AllowDynamicProperties]` should be an exception for legacy compatibility, not the default for new code. Watch for serialization, ORM hydration, and test doubles that secretly rely on dynamic properties.
|
||||
|
||||
### Don't do heavy I/O in constructors
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ quietly connecting to the DB on construction makes testing and error handling hard
|
||||
final class ReportService
|
||||
{
|
||||
private PDO $pdo;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->pdo = new PDO($_ENV['DSN']);
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ inject dependencies from the outside
|
||||
final class ReportService
|
||||
{
|
||||
public function __construct(
|
||||
private PDO $pdo,
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
A constructor should establish the object's invariants — not send HTTP requests, open connections, read large files, or run complex queries.
|
||||
|
||||
---
|
||||
|
||||
## Input, Output & Security
|
||||
|
||||
### Validate input at the boundary
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ superglobals flow straight into business logic
|
||||
$user = $service->create($_POST['email'], $_POST['age']);
|
||||
|
||||
// ✅ validate and coerce types at the boundary first
|
||||
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
|
||||
$age = filter_input(INPUT_POST, 'age', FILTER_VALIDATE_INT, [
|
||||
'options' => ['min_range' => 0, 'max_range' => 130],
|
||||
]);
|
||||
|
||||
if ($email === false || $email === null || $age === false || $age === null) {
|
||||
throw new InvalidInput();
|
||||
}
|
||||
|
||||
$user = $service->create($email, $age);
|
||||
```
|
||||
|
||||
`filter_input()` only handles a slice of basic validation. Complex rules, cross-field constraints, and business constraints still need a dedicated validator or request DTO.
|
||||
|
||||
### Escape output per context
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ user input goes straight into HTML
|
||||
echo "<h1>Hello {$_GET['name']}</h1>";
|
||||
|
||||
// ✅ use htmlspecialchars in an HTML text context
|
||||
$name = (string) ($_GET['name'] ?? '');
|
||||
echo '<h1>Hello ' . htmlspecialchars($name, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</h1>';
|
||||
```
|
||||
|
||||
Different contexts need different escaping: HTML text, HTML attributes, URLs, JavaScript strings, and CSS are all different. When a template engine's default escaping is turned off, treat it as a security risk.
|
||||
|
||||
### Passwords and randomness
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ md5/sha1 must not be used for password storage
|
||||
$hash = md5($password);
|
||||
|
||||
// ✅ use PHP's built-in password API
|
||||
$hash = password_hash($password, PASSWORD_DEFAULT);
|
||||
|
||||
if (!password_verify($password, $hash)) {
|
||||
throw new InvalidCredentials();
|
||||
}
|
||||
|
||||
// ✅ use a CSPRNG for tokens
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$code = random_int(100000, 999999);
|
||||
```
|
||||
|
||||
Don't hand-roll salts, round migration, or password comparison. Use `password_needs_rehash()` when you need to upgrade the cost factor.
|
||||
|
||||
### Deserialization and object injection
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ untrusted input into unserialize can trigger object injection
|
||||
$payload = unserialize($_COOKIE['state']);
|
||||
|
||||
// ✅ prefer JSON for external data, and validate its schema/shape
|
||||
$payload = json_decode($_COOKIE['state'] ?? '{}', true, flags: JSON_THROW_ON_ERROR);
|
||||
```
|
||||
|
||||
If you must process historical serialized data, at least restrict `allowed_classes` and make sure the relevant classes' magic methods can't produce dangerous side effects.
|
||||
|
||||
### File uploads and paths
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ building the path from the raw filename
|
||||
$target = __DIR__ . '/uploads/' . $_FILES['avatar']['name'];
|
||||
move_uploaded_file($_FILES['avatar']['tmp_name'], $target);
|
||||
|
||||
// ✅ generate a server-side filename, check the upload error and MIME
|
||||
$file = $_FILES['avatar'];
|
||||
if ($file['error'] !== UPLOAD_ERR_OK) {
|
||||
throw new UploadFailed();
|
||||
}
|
||||
|
||||
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
||||
$mime = $finfo->file($file['tmp_name']);
|
||||
if (!in_array($mime, ['image/png', 'image/jpeg'], true)) {
|
||||
throw new InvalidFileType();
|
||||
}
|
||||
|
||||
$target = __DIR__ . '/uploads/' . bin2hex(random_bytes(16)) . '.jpg';
|
||||
move_uploaded_file($file['tmp_name'], $target);
|
||||
```
|
||||
|
||||
When reviewing upload features, check size limits, MIME detection, extensions, a non-executable storage directory, path traversal, overwrite protection, and any virus-scan or async-processing requirements.
|
||||
|
||||
---
|
||||
|
||||
## Database Access
|
||||
|
||||
### Use parameterized queries
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ concatenated SQL is an injection risk
|
||||
$sql = "SELECT * FROM users WHERE email = '" . $_GET['email'] . "'";
|
||||
$user = $pdo->query($sql)->fetch();
|
||||
|
||||
// ✅ PDO prepared statement + bound value
|
||||
$stmt = $pdo->prepare('SELECT id, email FROM users WHERE email = :email');
|
||||
$stmt->execute(['email' => $email]);
|
||||
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
```
|
||||
|
||||
Parameters can only bind values — not table names, column names, or sort direction. Dynamic identifiers must go through a whitelist mapping.
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ✅ whitelist the dynamic sort column
|
||||
$columns = [
|
||||
'created' => 'created_at',
|
||||
'email' => 'email',
|
||||
];
|
||||
|
||||
$column = $columns[$_GET['sort'] ?? 'created'] ?? $columns['created'];
|
||||
$stmt = $pdo->query("SELECT id, email FROM users ORDER BY {$column} DESC");
|
||||
```
|
||||
|
||||
### Wrap multi-step writes in transactions
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ multi-step writes with no transaction leave half-finished state on failure
|
||||
$orderId = $orders->create($cart);
|
||||
$inventory->reserve($cart);
|
||||
$payments->charge($orderId);
|
||||
|
||||
// ✅ explicit transaction boundary
|
||||
$pdo->beginTransaction();
|
||||
try {
|
||||
$orderId = $orders->create($cart);
|
||||
$inventory->reserve($cart);
|
||||
$payments->recordIntent($orderId);
|
||||
$pdo->commit();
|
||||
} catch (Throwable $e) {
|
||||
$pdo->rollBack();
|
||||
throw $e;
|
||||
}
|
||||
```
|
||||
|
||||
Don't casually put external, non-rollbackable side effects (an actual charge, an email, a message dispatch) inside a database transaction. Common patterns are an outbox, an idempotency key, or triggering after the transaction commits.
|
||||
|
||||
### Avoid N+1 queries
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ querying inside a loop
|
||||
foreach ($orders as $order) {
|
||||
$customer = $customerRepo->find($order->customerId);
|
||||
render($order, $customer);
|
||||
}
|
||||
|
||||
// ✅ batch-load, then map
|
||||
$customerIds = array_unique(array_map(fn ($o) => $o->customerId, $orders));
|
||||
$customers = $customerRepo->findByIds($customerIds);
|
||||
|
||||
foreach ($orders as $order) {
|
||||
render($order, $customers[$order->customerId] ?? null);
|
||||
}
|
||||
```
|
||||
|
||||
In ORMs like Laravel/Doctrine, check eager loading, join fetch, selected columns, pagination, and indexes.
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Catch specific exceptions, keep context
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ swallowing the exception leaves callers unable to know it failed
|
||||
try {
|
||||
$mailer->send($message);
|
||||
} catch (Exception $e) {
|
||||
}
|
||||
|
||||
// ✅ catch a specific exception, keep context, and rethrow
|
||||
try {
|
||||
$mailer->send($message);
|
||||
} catch (TransportException $e) {
|
||||
throw new NotificationFailed($userId, previous: $e);
|
||||
}
|
||||
```
|
||||
|
||||
Empty `catch` blocks, `error_log()`-and-continue without surfacing the error, and turning every exception into `RuntimeException('failed')` in production code all deserve a question.
|
||||
|
||||
### Don't suppress errors with @
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ hides the real error and makes debugging hard
|
||||
$content = @file_get_contents($path);
|
||||
|
||||
// ✅ handle failure explicitly
|
||||
$content = file_get_contents($path);
|
||||
if ($content === false) {
|
||||
throw new RuntimeException("Unable to read file: {$path}");
|
||||
}
|
||||
```
|
||||
|
||||
`@` is common around file, network, array access, and legacy library calls. Push for an explicit branch, or convert third-party errors into project exceptions.
|
||||
|
||||
### Don't leak sensitive data in logs
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ writing tokens, passwords, or the full request body to the log
|
||||
$logger->error('Login failed', ['request' => $_POST]);
|
||||
|
||||
// ✅ log non-sensitive context that still helps locate the problem
|
||||
$logger->warning('Login failed', [
|
||||
'email_hash' => hash('sha256', strtolower($email)),
|
||||
'ip' => $requestIp,
|
||||
]);
|
||||
```
|
||||
|
||||
Check logs, exception messages, the debug toolbar, error pages, and failed-queue records. Sensitive data includes passwords, tokens, sessions, PII, payment data, and full cookies.
|
||||
|
||||
---
|
||||
|
||||
## Composer & Dependencies
|
||||
|
||||
### Lock reproducible dependencies
|
||||
|
||||
```json
|
||||
{
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"monolog/monolog": "^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^11.0",
|
||||
"phpstan/phpstan": "^1.10"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When reviewing `composer.json` / `composer.lock`, watch for:
|
||||
|
||||
- Application repos commit `composer.lock`; library repos usually don't
|
||||
- `require-dev` shouldn't make it into the production image
|
||||
- The PHP platform version matches the CI version
|
||||
- Autoload rules aren't too broad (don't load test or script directories)
|
||||
- `scripts` commands don't depend on a developer's local secret config
|
||||
|
||||
### Dependency security and maintenance
|
||||
|
||||
```bash
|
||||
composer audit
|
||||
composer outdated --direct
|
||||
composer validate --strict
|
||||
```
|
||||
|
||||
When adding a package, look at its maintenance status — download count isn't the only signal. What matters is its security history, release cadence, minimal dependency footprint, and whether it duplicates the standard library or a framework built-in.
|
||||
|
||||
---
|
||||
|
||||
## Performance & Resource Management
|
||||
|
||||
### Stream large datasets with generators or pagination
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ loading every record at once
|
||||
$rows = $repo->all();
|
||||
foreach ($rows as $row) {
|
||||
exportRow($row);
|
||||
}
|
||||
|
||||
// ✅ paginate or use a generator to avoid a memory spike
|
||||
foreach ($repo->cursor() as $row) {
|
||||
exportRow($row);
|
||||
}
|
||||
```
|
||||
|
||||
A PHP request lifecycle is short, but CLI jobs, queue workers, and export tasks run for a long time. For that kind of code, watch memory growth, unclosed resources, and global-state pollution especially closely.
|
||||
|
||||
### Avoid expensive work inside loops
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ re-parsing config or opening a connection on every iteration
|
||||
foreach ($items as $item) {
|
||||
$client = new ApiClient($_ENV['API_KEY']);
|
||||
$client->send($item);
|
||||
}
|
||||
|
||||
// ✅ create reusable dependencies outside the loop
|
||||
$client = new ApiClient($_ENV['API_KEY']);
|
||||
foreach ($items as $item) {
|
||||
$client->send($item);
|
||||
}
|
||||
```
|
||||
|
||||
Watch for database queries, HTTP requests, regex compilation, large array copies, accumulating `array_merge()` appends, and repeatedly reading env vars or config files inside loops.
|
||||
|
||||
### Release or scope resources
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ✅ close file handles after use
|
||||
$handle = fopen($path, 'rb');
|
||||
if ($handle === false) {
|
||||
throw new RuntimeException('Unable to open file');
|
||||
}
|
||||
|
||||
try {
|
||||
while (($line = fgets($handle)) !== false) {
|
||||
process($line);
|
||||
}
|
||||
} finally {
|
||||
fclose($handle);
|
||||
}
|
||||
```
|
||||
|
||||
PDO connections are usually managed by the container, but file handles, curl handles, temp files, locks, and cached objects in queue workers still need an explicit lifecycle.
|
||||
|
||||
---
|
||||
|
||||
## Testing & Static Analysis
|
||||
|
||||
### Test behavior, not implementation details
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ asserting an internal method call makes refactoring expensive
|
||||
$mailer->expects($this->once())->method('buildTemplate');
|
||||
|
||||
// ✅ assert observable results
|
||||
$service->sendWelcomeEmail($user);
|
||||
|
||||
$this->assertTrue($mailbox->hasMessageFor($user->email));
|
||||
```
|
||||
|
||||
For business services, controllers, and queue jobs, prefer covering observable behavior: inputs/outputs, database state, published events, and dispatched messages.
|
||||
|
||||
### Static analysis and formatting
|
||||
|
||||
```bash
|
||||
vendor/bin/phpunit
|
||||
vendor/bin/phpstan analyse
|
||||
vendor/bin/psalm
|
||||
vendor/bin/php-cs-fixer fix --dry-run --diff
|
||||
vendor/bin/rector process --dry-run
|
||||
```
|
||||
|
||||
When reviewing a PR, check whether the new code lowers the PHPStan/Psalm level, leans heavily on baseline ignores, or uses `@phpstan-ignore-next-line` to paper over a real type problem.
|
||||
|
||||
### Isolate test data
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
// ❌ the test depends on real time and external services
|
||||
$service->expireOldSessions();
|
||||
|
||||
// ✅ inject a clock and a fake gateway
|
||||
$clock->setNow(new DateTimeImmutable('2026-01-01T00:00:00Z'));
|
||||
$service->expireOldSessions();
|
||||
```
|
||||
|
||||
Watch for database transaction rollback, fixture cleanup, randomness, time, queues, caches, and external APIs. Slow PHP tests are usually not a language problem — it's that the boundaries aren't isolated.
|
||||
|
||||
---
|
||||
|
||||
## Review Checklist
|
||||
|
||||
### Types & modeling
|
||||
|
||||
- [ ] `declare(strict_types=1);` at the top of the file
|
||||
- [ ] Parameters, return values, and properties have explicit types
|
||||
- [ ] `===` / `!==` used; collection lookups use strict mode
|
||||
- [ ] Stable state sets use an enum, not magic strings
|
||||
- [ ] New code doesn't rely on dynamic properties
|
||||
- [ ] Value objects are readonly or otherwise immutable
|
||||
|
||||
### Security
|
||||
|
||||
- [ ] Input is validated and type-coerced at the boundary
|
||||
- [ ] Output is escaped per HTML/URL/JS/CSS context
|
||||
- [ ] SQL uses prepared statements or ORM binding
|
||||
- [ ] Dynamic table/column/sort names go through a whitelist
|
||||
- [ ] Passwords use `password_hash()` / `password_verify()`
|
||||
- [ ] Tokens, codes, and filenames use `random_bytes()` / `random_int()`
|
||||
- [ ] Untrusted input never reaches `unserialize()`
|
||||
- [ ] File uploads check the error code, size, MIME, extension, and storage directory
|
||||
- [ ] No injection or leakage risk in shell commands, path building, or log output
|
||||
|
||||
### Data & transactions
|
||||
|
||||
- [ ] Multi-step writes have a transaction or compensation mechanism
|
||||
- [ ] External side effects are designed to be idempotent
|
||||
- [ ] N+1 queries avoided
|
||||
- [ ] Pagination, indexes, and selected columns are reasonable
|
||||
- [ ] Database errors aren't swallowed
|
||||
|
||||
### Maintainability
|
||||
|
||||
- [ ] Constructors don't do heavy I/O
|
||||
- [ ] Dependency injection is clear; no hidden global state
|
||||
- [ ] No `@` error suppression
|
||||
- [ ] Exceptions preserve context and `previous`
|
||||
- [ ] Composer dependency ranges, autoload, and scripts are reasonable
|
||||
- [ ] Application repos commit `composer.lock`
|
||||
|
||||
### Testing & tooling
|
||||
|
||||
- [ ] PHPUnit/Pest cover the critical and failure paths
|
||||
- [ ] PHPStan/Psalm config doesn't lower strictness
|
||||
- [ ] New ignores/baselines are explained
|
||||
- [ ] Formatting tools and CI commands are reproducible
|
||||
- [ ] Tests isolate time, randomness, the database, queues, and external APIs
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [PHP Manual: Type declarations](https://www.php.net/manual/en/language.types.declarations.php)
|
||||
- [PHP Manual: match](https://www.php.net/match)
|
||||
- [PHP Manual: Enumerations](https://www.php.net/manual/en/language.enumerations.overview.php)
|
||||
- [PHP Manual: Properties](https://www.php.net/manual/en/language.oop5.properties.php)
|
||||
- [PHP Manual: PDO](https://www.php.net/manual/en/class.pdo.php)
|
||||
- [PHP Manual: password_hash](https://www.php.net/manual/en/function.password-hash.php)
|
||||
- [PHP Manual: random_bytes](https://www.php.net/manual/en/function.random-bytes.php)
|
||||
- [Composer documentation](https://getcomposer.org/doc/)
|
||||
- [PHPUnit documentation](https://docs.phpunit.de/)
|
||||
- [PHPStan documentation](https://phpstan.org/user-guide/getting-started)
|
||||
- [Psalm documentation](https://psalm.dev/docs/)
|
||||
+1069
File diff suppressed because it is too large
Load Diff
+186
@@ -0,0 +1,186 @@
|
||||
# Qt Code Review Guide
|
||||
|
||||
> Code review guidelines focusing on object model, signals/slots, event loop, and GUI performance. Examples based on Qt 5.15 / Qt 6.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Object Model & Memory Management](#object-model--memory-management)
|
||||
- [Signals & Slots](#signals--slots)
|
||||
- [Containers & Strings](#containers--strings)
|
||||
- [Threads & Concurrency](#threads--concurrency)
|
||||
- [GUI & Widgets](#gui--widgets)
|
||||
- [Meta-Object System](#meta-object-system)
|
||||
- [Review Checklist](#review-checklist)
|
||||
|
||||
---
|
||||
|
||||
## Object Model & Memory Management
|
||||
|
||||
### Use Parent-Child Ownership Mechanism
|
||||
Qt's `QObject` hierarchy automatically manages memory. For `QObject`, prefer setting a parent object over manual `delete` or smart pointers.
|
||||
|
||||
```cpp
|
||||
// ❌ Manual management prone to memory leaks
|
||||
QWidget* w = new QWidget();
|
||||
QLabel* l = new QLabel();
|
||||
l->setParent(w);
|
||||
// ... If w is deleted, l is automatically deleted. But if w leaks, l also leaks.
|
||||
|
||||
// ✅ Specify parent in constructor
|
||||
QWidget* w = new QWidget(this); // Owned by 'this'
|
||||
QLabel* l = new QLabel(w); // Owned by 'w'
|
||||
```
|
||||
|
||||
### Use Smart Pointers with QObject
|
||||
If a `QObject` has no parent, use `QScopedPointer` or `std::unique_ptr` with a custom deleter (use `deleteLater` if cross-thread). Avoid `std::shared_ptr` for `QObject` unless necessary, as it confuses the parent-child ownership system.
|
||||
|
||||
```cpp
|
||||
// ✅ Scoped pointer for local/member QObject without parent
|
||||
QScopedPointer<MyObject> obj(new MyObject());
|
||||
|
||||
// ✅ Safe pointer to prevent dangling pointers
|
||||
QPointer<MyObject> safePtr = obj.data();
|
||||
if (safePtr) {
|
||||
safePtr->doSomething();
|
||||
}
|
||||
```
|
||||
|
||||
### Use `deleteLater()`
|
||||
For asynchronous deletion, especially in slots or event handlers, use `deleteLater()` instead of `delete` to ensure pending events in the event loop are processed.
|
||||
|
||||
---
|
||||
|
||||
## Signals & Slots
|
||||
|
||||
### Prefer Function Pointer Syntax
|
||||
Use compile-time checked syntax (Qt 5+).
|
||||
|
||||
```cpp
|
||||
// ❌ String-based (runtime check only, slower)
|
||||
connect(sender, SIGNAL(valueChanged(int)), receiver, SLOT(updateValue(int)));
|
||||
|
||||
// ✅ Compile-time check
|
||||
connect(sender, &Sender::valueChanged, receiver, &Receiver::updateValue);
|
||||
```
|
||||
|
||||
### Connection Types
|
||||
Be explicit or aware of connection types when crossing threads.
|
||||
- `Qt::AutoConnection` (Default): Direct if same thread, Queued if different thread.
|
||||
- `Qt::QueuedConnection`: Always posts event (thread-safe across threads).
|
||||
- `Qt::DirectConnection`: Immediate call (dangerous if accessing non-thread-safe data across threads).
|
||||
|
||||
### Avoid Loops
|
||||
Check logic that might cause infinite signal loops (e.g., `valueChanged` -> `setValue` -> `valueChanged`). Block signals or check for equality before setting values.
|
||||
|
||||
```cpp
|
||||
void MyClass::setValue(int v) {
|
||||
if (m_value == v) return; // ✅ Good: Break loop
|
||||
m_value = v;
|
||||
emit valueChanged(v);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Containers & Strings
|
||||
|
||||
### QString Efficiency
|
||||
- Use `QStringLiteral("...")` for compile-time string creation to avoid runtime allocation.
|
||||
- Use `QLatin1String` for comparison with ASCII literals (in Qt 5).
|
||||
- Prefer `arg()` for formatting (or `QStringBuilder`'s `%` operator).
|
||||
|
||||
```cpp
|
||||
// ❌ Runtime conversion
|
||||
if (str == "test") ...
|
||||
|
||||
// ✅ Prefer QLatin1String for comparison with ASCII literals (in Qt 5)
|
||||
if (str == QLatin1String("test")) ... // Qt 5
|
||||
if (str == u"test"_s) ... // Qt 6
|
||||
```
|
||||
|
||||
### Container Selection
|
||||
- **Qt 6**: `QList` is now the default choice (unified with `QVector`).
|
||||
- **Qt 5**: Prefer `QVector` over `QList` for contiguous memory and cache performance, unless stable references are needed.
|
||||
- Be aware of Implicit Sharing (Copy-on-Write). Passing containers by value is cheap *until* modified. Use `const &` for read-only access.
|
||||
|
||||
```cpp
|
||||
// ❌ Forces deep copy if function modifies 'list'
|
||||
void process(QVector<int> list) {
|
||||
list[0] = 1;
|
||||
}
|
||||
|
||||
// ✅ Read-only reference
|
||||
void process(const QVector<int>& list) { ... }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Threads & Concurrency
|
||||
|
||||
### Subclassing QThread vs Worker Object
|
||||
Prefer the "Worker Object" pattern over subclassing `QThread` implementation details.
|
||||
|
||||
```cpp
|
||||
// ❌ Business logic inside QThread::run()
|
||||
class MyThread : public QThread {
|
||||
void run() override { ... }
|
||||
};
|
||||
|
||||
// ✅ Worker object moved to thread
|
||||
QThread* thread = new QThread;
|
||||
Worker* worker = new Worker;
|
||||
worker->moveToThread(thread);
|
||||
connect(thread, &QThread::started, worker, &Worker::process);
|
||||
thread->start();
|
||||
```
|
||||
|
||||
### GUI Thread Safety
|
||||
**NEVER** access UI widgets (`QWidget` and subclasses) from a background thread. Use signals/slots to communicate updates to the main thread.
|
||||
|
||||
---
|
||||
|
||||
## GUI & Widgets
|
||||
|
||||
### Logic Separation
|
||||
Keep business logic out of UI classes (`MainWindow`, `Dialog`). UI classes should only handle display and user input forwarding.
|
||||
|
||||
### Layouts
|
||||
Avoid fixed sizes (`setGeometry`, `resize`). Use layouts (`QVBoxLayout`, `QGridLayout`) to handle different DPIs and window resizing gracefully.
|
||||
|
||||
### Blocking Event Loop
|
||||
Never execute long-running operations on the main thread (freezes GUI).
|
||||
- **Bad**: `Sleep()`, `while(busy)`, synchronous network calls.
|
||||
- **Good**: `QProcess`, `QThread`, `QtConcurrent`, or asynchronous APIs (`QNetworkAccessManager`).
|
||||
|
||||
---
|
||||
|
||||
## Meta-Object System
|
||||
|
||||
### Properties & Enums
|
||||
Use `Q_PROPERTY` for values exposed to QML or needing introspection.
|
||||
Use `Q_ENUM` to enable string conversion for enums.
|
||||
|
||||
```cpp
|
||||
class MyObject : public QObject {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(int value READ value WRITE setValue NOTIFY valueChanged)
|
||||
public:
|
||||
enum State { Idle, Running };
|
||||
Q_ENUM(State)
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
### qobject_cast
|
||||
Use `qobject_cast<T*>` for QObjects instead of `dynamic_cast`. It is faster and doesn't require RTTI.
|
||||
|
||||
---
|
||||
|
||||
## Review Checklist
|
||||
|
||||
- [ ] **Memory**: Is parent-child relationship correct? Are dangling pointers avoided (using `QPointer`)?
|
||||
- [ ] **Signals**: Are connections checked? Do lambdas use safe captures (context object)?
|
||||
- [ ] **Threads**: Is UI accessed only from main thread? Are long tasks offloaded?
|
||||
- [ ] **Strings**: Are `QStringLiteral` or `tr()` used appropriately?
|
||||
- [ ] **Style**: Naming conventions (camelCase for methods, PascalCase for classes).
|
||||
- [ ] **Resources**: Are resources (images, styles) loaded from `.qrc`?
|
||||
+871
@@ -0,0 +1,871 @@
|
||||
# React Code Review Guide
|
||||
|
||||
React 审查重点:Hooks 规则、性能优化的适度性、组件设计、以及现代 React 19/RSC 模式。
|
||||
|
||||
## 目录
|
||||
|
||||
- [基础 Hooks 规则](#基础-hooks-规则)
|
||||
- [useEffect 模式](#useeffect-模式)
|
||||
- [useMemo / useCallback](#usememo--usecallback)
|
||||
- [组件设计](#组件设计)
|
||||
- [Error Boundaries & Suspense](#error-boundaries--suspense)
|
||||
- [Server Components (RSC)](#server-components-rsc)
|
||||
- [React 19 Actions & Forms](#react-19-actions--forms)
|
||||
- [Suspense & Streaming SSR](#suspense--streaming-ssr)
|
||||
- [TanStack Query v5](#tanstack-query-v5)
|
||||
- [Review Checklists](#review-checklists)
|
||||
|
||||
---
|
||||
|
||||
## 基础 Hooks 规则
|
||||
|
||||
```tsx
|
||||
// ❌ 条件调用 Hooks — 违反 Hooks 规则
|
||||
function BadComponent({ isLoggedIn }) {
|
||||
if (isLoggedIn) {
|
||||
const [user, setUser] = useState(null); // Error!
|
||||
}
|
||||
return <div>...</div>;
|
||||
}
|
||||
|
||||
// ✅ Hooks 必须在组件顶层调用
|
||||
function GoodComponent({ isLoggedIn }) {
|
||||
const [user, setUser] = useState(null);
|
||||
if (!isLoggedIn) return <LoginPrompt />;
|
||||
return <div>{user?.name}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## useEffect 模式
|
||||
|
||||
```tsx
|
||||
// ❌ 依赖数组缺失或不完整
|
||||
function BadEffect({ userId }) {
|
||||
const [user, setUser] = useState(null);
|
||||
useEffect(() => {
|
||||
fetchUser(userId).then(setUser);
|
||||
}, []); // 缺少 userId 依赖!
|
||||
}
|
||||
|
||||
// ✅ 完整的依赖数组
|
||||
function GoodEffect({ userId }) {
|
||||
const [user, setUser] = useState(null);
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
fetchUser(userId).then(data => {
|
||||
if (!cancelled) setUser(data);
|
||||
});
|
||||
return () => { cancelled = true; }; // 清理函数
|
||||
}, [userId]);
|
||||
}
|
||||
|
||||
// ❌ useEffect 用于派生状态(反模式)
|
||||
function BadDerived({ items }) {
|
||||
const [filteredItems, setFilteredItems] = useState([]);
|
||||
useEffect(() => {
|
||||
setFilteredItems(items.filter(i => i.active));
|
||||
}, [items]); // 不必要的 effect + 额外渲染
|
||||
return <List items={filteredItems} />;
|
||||
}
|
||||
|
||||
// ✅ 直接在渲染时计算,或用 useMemo
|
||||
function GoodDerived({ items }) {
|
||||
const filteredItems = useMemo(
|
||||
() => items.filter(i => i.active),
|
||||
[items]
|
||||
);
|
||||
return <List items={filteredItems} />;
|
||||
}
|
||||
|
||||
// ❌ useEffect 用于事件响应
|
||||
function BadEventEffect() {
|
||||
const [query, setQuery] = useState('');
|
||||
useEffect(() => {
|
||||
if (query) {
|
||||
analytics.track('search', { query }); // 应该在事件处理器中
|
||||
}
|
||||
}, [query]);
|
||||
}
|
||||
|
||||
// ✅ 在事件处理器中执行副作用
|
||||
function GoodEvent() {
|
||||
const [query, setQuery] = useState('');
|
||||
const handleSearch = (q: string) => {
|
||||
setQuery(q);
|
||||
analytics.track('search', { query: q });
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## useMemo / useCallback
|
||||
|
||||
```tsx
|
||||
// ❌ 过度优化 — 常量不需要 useMemo
|
||||
function OverOptimized() {
|
||||
const config = useMemo(() => ({ timeout: 5000 }), []); // 无意义
|
||||
const handleClick = useCallback(() => {
|
||||
console.log('clicked');
|
||||
}, []); // 如果不传给 memo 组件,无意义
|
||||
}
|
||||
|
||||
// ✅ 只在需要时优化
|
||||
function ProperlyOptimized() {
|
||||
const config = { timeout: 5000 }; // 简单对象直接定义
|
||||
const handleClick = () => console.log('clicked');
|
||||
}
|
||||
|
||||
// ❌ useCallback 依赖总是变化
|
||||
function BadCallback({ data }) {
|
||||
// data 每次渲染都是新对象,useCallback 无效
|
||||
const process = useCallback(() => {
|
||||
return data.map(transform);
|
||||
}, [data]);
|
||||
}
|
||||
|
||||
// ✅ useMemo + useCallback 配合 React.memo 使用
|
||||
const MemoizedChild = React.memo(function Child({ onClick, items }) {
|
||||
return <div onClick={onClick}>{items.length}</div>;
|
||||
});
|
||||
|
||||
function Parent({ rawItems }) {
|
||||
const items = useMemo(() => processItems(rawItems), [rawItems]);
|
||||
const handleClick = useCallback(() => {
|
||||
console.log(items.length);
|
||||
}, [items]);
|
||||
return <MemoizedChild onClick={handleClick} items={items} />;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 组件设计
|
||||
|
||||
```tsx
|
||||
// ❌ 在组件内定义组件 — 每次渲染都创建新组件
|
||||
function BadParent() {
|
||||
function ChildComponent() { // 每次渲染都是新函数!
|
||||
return <div>child</div>;
|
||||
}
|
||||
return <ChildComponent />;
|
||||
}
|
||||
|
||||
// ✅ 组件定义在外部
|
||||
function ChildComponent() {
|
||||
return <div>child</div>;
|
||||
}
|
||||
function GoodParent() {
|
||||
return <ChildComponent />;
|
||||
}
|
||||
|
||||
// ❌ Props 总是新对象引用
|
||||
function BadProps() {
|
||||
return (
|
||||
<MemoizedComponent
|
||||
style={{ color: 'red' }} // 每次渲染新对象
|
||||
onClick={() => {}} // 每次渲染新函数
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// ✅ 稳定的引用
|
||||
const style = { color: 'red' };
|
||||
function GoodProps() {
|
||||
const handleClick = useCallback(() => {}, []);
|
||||
return <MemoizedComponent style={style} onClick={handleClick} />;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Boundaries & Suspense
|
||||
|
||||
```tsx
|
||||
// ❌ 没有错误边界
|
||||
function BadApp() {
|
||||
return (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<DataComponent /> {/* 错误会导致整个应用崩溃 */}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
// ✅ Error Boundary 包裹 Suspense
|
||||
function GoodApp() {
|
||||
return (
|
||||
<ErrorBoundary fallback={<ErrorUI />}>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<DataComponent />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Server Components (RSC)
|
||||
|
||||
```tsx
|
||||
// ❌ 在 Server Component 中使用客户端特性
|
||||
// app/page.tsx (Server Component by default)
|
||||
function BadServerComponent() {
|
||||
const [count, setCount] = useState(0); // Error! No hooks in RSC
|
||||
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
|
||||
}
|
||||
|
||||
// ✅ 交互逻辑提取到 Client Component
|
||||
// app/counter.tsx
|
||||
'use client';
|
||||
function Counter() {
|
||||
const [count, setCount] = useState(0);
|
||||
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
|
||||
}
|
||||
|
||||
// app/page.tsx (Server Component)
|
||||
async function GoodServerComponent() {
|
||||
const data = await fetchData(); // 可以直接 await
|
||||
return (
|
||||
<div>
|
||||
<h1>{data.title}</h1>
|
||||
<Counter /> {/* 客户端组件 */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ❌ 'use client' 放置不当 — 整个树都变成客户端
|
||||
// layout.tsx
|
||||
'use client'; // 这会让所有子组件都成为客户端组件
|
||||
export default function Layout({ children }) { ... }
|
||||
|
||||
// ✅ 只在需要交互的组件使用 'use client'
|
||||
// 将客户端逻辑隔离到叶子组件
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## React 19 Actions & Forms
|
||||
|
||||
React 19 引入了 Actions 系统和新的表单处理 Hooks,简化异步操作和乐观更新。
|
||||
|
||||
### useActionState
|
||||
|
||||
```tsx
|
||||
// ❌ 传统方式:多个状态变量
|
||||
function OldForm() {
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [data, setData] = useState(null);
|
||||
|
||||
const handleSubmit = async (formData: FormData) => {
|
||||
setIsPending(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await submitForm(formData);
|
||||
setData(result);
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
} finally {
|
||||
setIsPending(false);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ✅ React 19: useActionState 统一管理
|
||||
import { useActionState } from 'react';
|
||||
|
||||
function NewForm() {
|
||||
const [state, formAction, isPending] = useActionState(
|
||||
async (prevState, formData: FormData) => {
|
||||
try {
|
||||
const result = await submitForm(formData);
|
||||
return { success: true, data: result };
|
||||
} catch (e) {
|
||||
return { success: false, error: e.message };
|
||||
}
|
||||
},
|
||||
{ success: false, data: null, error: null }
|
||||
);
|
||||
|
||||
return (
|
||||
<form action={formAction}>
|
||||
<input name="email" />
|
||||
<button disabled={isPending}>
|
||||
{isPending ? 'Submitting...' : 'Submit'}
|
||||
</button>
|
||||
{state.error && <p className="error">{state.error}</p>}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### useFormStatus
|
||||
|
||||
```tsx
|
||||
// ❌ Props 透传表单状态
|
||||
function BadSubmitButton({ isSubmitting }) {
|
||||
return <button disabled={isSubmitting}>Submit</button>;
|
||||
}
|
||||
|
||||
// ✅ useFormStatus 访问父 <form> 状态(无需 props)
|
||||
import { useFormStatus } from 'react-dom';
|
||||
|
||||
function SubmitButton() {
|
||||
const { pending, data, method, action } = useFormStatus();
|
||||
// 注意:必须在 <form> 内部的子组件中使用
|
||||
return (
|
||||
<button disabled={pending}>
|
||||
{pending ? 'Submitting...' : 'Submit'}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// ❌ useFormStatus 在 form 同级组件中调用——不工作
|
||||
function BadForm() {
|
||||
const { pending } = useFormStatus(); // 这里无法获取状态!
|
||||
return (
|
||||
<form action={action}>
|
||||
<button disabled={pending}>Submit</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
// ✅ useFormStatus 必须在 form 的子组件中
|
||||
function GoodForm() {
|
||||
return (
|
||||
<form action={action}>
|
||||
<SubmitButton /> {/* useFormStatus 在这里面调用 */}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### useOptimistic
|
||||
|
||||
```tsx
|
||||
// ❌ 等待服务器响应再更新 UI
|
||||
function SlowLike({ postId, likes }) {
|
||||
const [likeCount, setLikeCount] = useState(likes);
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
|
||||
const handleLike = async () => {
|
||||
setIsPending(true);
|
||||
const newCount = await likePost(postId); // 等待...
|
||||
setLikeCount(newCount);
|
||||
setIsPending(false);
|
||||
};
|
||||
}
|
||||
|
||||
// ✅ useOptimistic 即时反馈,失败自动回滚
|
||||
import { useOptimistic } from 'react';
|
||||
|
||||
function FastLike({ postId, likes }) {
|
||||
const [optimisticLikes, addOptimisticLike] = useOptimistic(
|
||||
likes,
|
||||
(currentLikes, increment: number) => currentLikes + increment
|
||||
);
|
||||
|
||||
const handleLike = async () => {
|
||||
addOptimisticLike(1); // 立即更新 UI
|
||||
try {
|
||||
await likePost(postId); // 后台同步
|
||||
} catch {
|
||||
// React 自动回滚到 likes 原值
|
||||
}
|
||||
};
|
||||
|
||||
return <button onClick={handleLike}>{optimisticLikes} likes</button>;
|
||||
}
|
||||
```
|
||||
|
||||
### Server Actions (Next.js 15+)
|
||||
|
||||
```tsx
|
||||
// ❌ 客户端调用 API
|
||||
'use client';
|
||||
function ClientForm() {
|
||||
const handleSubmit = async (formData: FormData) => {
|
||||
const res = await fetch('/api/submit', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
// ...
|
||||
};
|
||||
}
|
||||
|
||||
// ✅ Server Action + useActionState
|
||||
// actions.ts
|
||||
'use server';
|
||||
export async function createPost(prevState: any, formData: FormData) {
|
||||
const title = formData.get('title');
|
||||
await db.posts.create({ title });
|
||||
revalidatePath('/posts');
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// form.tsx
|
||||
'use client';
|
||||
import { createPost } from './actions';
|
||||
|
||||
function PostForm() {
|
||||
const [state, formAction, isPending] = useActionState(createPost, null);
|
||||
return (
|
||||
<form action={formAction}>
|
||||
<input name="title" />
|
||||
<SubmitButton />
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Suspense & Streaming SSR
|
||||
|
||||
Suspense 和 Streaming 是 React 18+ 的核心特性,在 2025 年的 Next.js 15 等框架中广泛使用。
|
||||
|
||||
### 基础 Suspense
|
||||
|
||||
```tsx
|
||||
// ❌ 传统加载状态管理
|
||||
function OldComponent() {
|
||||
const [data, setData] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData().then(setData).finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
|
||||
if (isLoading) return <Spinner />;
|
||||
return <DataView data={data} />;
|
||||
}
|
||||
|
||||
// ✅ Suspense 声明式加载状态
|
||||
function NewComponent() {
|
||||
return (
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<DataView /> {/* 内部使用 use() 或支持 Suspense 的数据获取 */}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 多个独立 Suspense 边界
|
||||
|
||||
```tsx
|
||||
// ❌ 单一边界——所有内容一起加载
|
||||
function BadLayout() {
|
||||
return (
|
||||
<Suspense fallback={<FullPageSpinner />}>
|
||||
<Header />
|
||||
<MainContent /> {/* 慢 */}
|
||||
<Sidebar /> {/* 快 */}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
// ✅ 独立边界——各部分独立流式传输
|
||||
function GoodLayout() {
|
||||
return (
|
||||
<>
|
||||
<Header /> {/* 立即显示 */}
|
||||
<div className="flex">
|
||||
<Suspense fallback={<ContentSkeleton />}>
|
||||
<MainContent /> {/* 独立加载 */}
|
||||
</Suspense>
|
||||
<Suspense fallback={<SidebarSkeleton />}>
|
||||
<Sidebar /> {/* 独立加载 */}
|
||||
</Suspense>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Next.js 15 Streaming
|
||||
|
||||
```tsx
|
||||
// app/page.tsx - 自动 Streaming
|
||||
export default async function Page() {
|
||||
// 这个 await 不会阻塞整个页面
|
||||
const data = await fetchSlowData();
|
||||
return <div>{data}</div>;
|
||||
}
|
||||
|
||||
// app/loading.tsx - 自动 Suspense 边界
|
||||
export default function Loading() {
|
||||
return <Skeleton />;
|
||||
}
|
||||
```
|
||||
|
||||
### use() Hook (React 19)
|
||||
|
||||
```tsx
|
||||
// ✅ 在组件中读取 Promise
|
||||
import { use } from 'react';
|
||||
|
||||
function Comments({ commentsPromise }) {
|
||||
const comments = use(commentsPromise); // 自动触发 Suspense
|
||||
return (
|
||||
<ul>
|
||||
{comments.map(c => <li key={c.id}>{c.text}</li>)}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
// 父组件创建 Promise,子组件消费
|
||||
function Post({ postId }) {
|
||||
const commentsPromise = fetchComments(postId); // 不 await
|
||||
return (
|
||||
<article>
|
||||
<PostContent id={postId} />
|
||||
<Suspense fallback={<CommentsSkeleton />}>
|
||||
<Comments commentsPromise={commentsPromise} />
|
||||
</Suspense>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## TanStack Query v5
|
||||
|
||||
TanStack Query 是 React 生态中最流行的数据获取库,v5 是当前稳定版本。
|
||||
|
||||
### 基础配置
|
||||
|
||||
```tsx
|
||||
// ❌ 不正确的默认配置
|
||||
const queryClient = new QueryClient(); // 默认配置可能不适合
|
||||
|
||||
// ✅ 生产环境推荐配置
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 1000 * 60 * 5, // 5 分钟内数据视为新鲜
|
||||
gcTime: 1000 * 60 * 30, // 30 分钟后垃圾回收(v5 重命名)
|
||||
retry: 3,
|
||||
refetchOnWindowFocus: false, // 根据需求决定
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### queryOptions (v5 新增)
|
||||
|
||||
```tsx
|
||||
// ❌ 重复定义 queryKey 和 queryFn
|
||||
function Component1() {
|
||||
const { data } = useQuery({
|
||||
queryKey: ['users', userId],
|
||||
queryFn: () => fetchUser(userId),
|
||||
});
|
||||
}
|
||||
|
||||
function prefetchUser(queryClient, userId) {
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ['users', userId], // 重复!
|
||||
queryFn: () => fetchUser(userId), // 重复!
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ queryOptions 统一定义,类型安全
|
||||
import { queryOptions } from '@tanstack/react-query';
|
||||
|
||||
const userQueryOptions = (userId: string) =>
|
||||
queryOptions({
|
||||
queryKey: ['users', userId],
|
||||
queryFn: () => fetchUser(userId),
|
||||
});
|
||||
|
||||
function Component1({ userId }) {
|
||||
const { data } = useQuery(userQueryOptions(userId));
|
||||
}
|
||||
|
||||
function prefetchUser(queryClient, userId) {
|
||||
queryClient.prefetchQuery(userQueryOptions(userId));
|
||||
}
|
||||
|
||||
// getQueryData 也是类型安全的
|
||||
const user = queryClient.getQueryData(userQueryOptions(userId).queryKey);
|
||||
```
|
||||
|
||||
### 常见陷阱
|
||||
|
||||
```tsx
|
||||
// ❌ staleTime 为 0 导致过度请求
|
||||
useQuery({
|
||||
queryKey: ['data'],
|
||||
queryFn: fetchData,
|
||||
// staleTime 默认为 0,每次组件挂载都会 refetch
|
||||
});
|
||||
|
||||
// ✅ 设置合理的 staleTime
|
||||
useQuery({
|
||||
queryKey: ['data'],
|
||||
queryFn: fetchData,
|
||||
staleTime: 1000 * 60, // 1 分钟内不会重新请求
|
||||
});
|
||||
|
||||
// ❌ 在 queryFn 中使用不稳定的引用
|
||||
function BadQuery({ filters }) {
|
||||
useQuery({
|
||||
queryKey: ['items'], // queryKey 没有包含 filters!
|
||||
queryFn: () => fetchItems(filters), // filters 变化不会触发重新请求
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ queryKey 包含所有影响数据的参数
|
||||
function GoodQuery({ filters }) {
|
||||
useQuery({
|
||||
queryKey: ['items', filters], // filters 是 queryKey 的一部分
|
||||
queryFn: () => fetchItems(filters),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### useSuspenseQuery
|
||||
|
||||
> **重要限制**:useSuspenseQuery 与 useQuery 有显著差异,选择前需了解其限制。
|
||||
|
||||
#### useSuspenseQuery 的限制
|
||||
|
||||
| 特性 | useQuery | useSuspenseQuery |
|
||||
|------|----------|------------------|
|
||||
| `enabled` 选项 | ✅ 支持 | ❌ 不支持 |
|
||||
| `placeholderData` | ✅ 支持 | ❌ 不支持 |
|
||||
| `data` 类型 | `T \| undefined` | `T`(保证有值)|
|
||||
| 错误处理 | `error` 属性 | 抛出到 Error Boundary |
|
||||
| 加载状态 | `isLoading` 属性 | 挂起到 Suspense |
|
||||
|
||||
#### 不支持 enabled 的替代方案
|
||||
|
||||
```tsx
|
||||
// ❌ 使用 useQuery + enabled 实现条件查询
|
||||
function BadSuspenseQuery({ userId }) {
|
||||
const { data } = useSuspenseQuery({
|
||||
queryKey: ['user', userId],
|
||||
queryFn: () => fetchUser(userId),
|
||||
enabled: !!userId, // useSuspenseQuery 不支持 enabled!
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ 组件组合实现条件渲染
|
||||
function GoodSuspenseQuery({ userId }) {
|
||||
// useSuspenseQuery 保证 data 是 T 不是 T | undefined
|
||||
const { data } = useSuspenseQuery({
|
||||
queryKey: ['user', userId],
|
||||
queryFn: () => fetchUser(userId),
|
||||
});
|
||||
return <UserProfile user={data} />;
|
||||
}
|
||||
|
||||
function Parent({ userId }) {
|
||||
if (!userId) return <NoUserSelected />;
|
||||
return (
|
||||
<Suspense fallback={<UserSkeleton />}>
|
||||
<GoodSuspenseQuery userId={userId} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误处理差异
|
||||
|
||||
```tsx
|
||||
// ❌ useSuspenseQuery 没有 error 属性
|
||||
function BadErrorHandling() {
|
||||
const { data, error } = useSuspenseQuery({...});
|
||||
if (error) return <Error />; // error 总是 null!
|
||||
}
|
||||
|
||||
// ✅ 使用 Error Boundary 处理错误
|
||||
function GoodErrorHandling() {
|
||||
return (
|
||||
<ErrorBoundary fallback={<ErrorMessage />}>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<DataComponent />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
function DataComponent() {
|
||||
// 错误会抛出到 Error Boundary
|
||||
const { data } = useSuspenseQuery({
|
||||
queryKey: ['data'],
|
||||
queryFn: fetchData,
|
||||
});
|
||||
return <Display data={data} />;
|
||||
}
|
||||
```
|
||||
|
||||
#### 何时选择 useSuspenseQuery
|
||||
|
||||
```tsx
|
||||
// ✅ 适合场景:
|
||||
// 1. 数据总是需要的(无条件查询)
|
||||
// 2. 组件必须有数据才能渲染
|
||||
// 3. 使用 React 19 的 Suspense 模式
|
||||
// 4. 服务端组件 + 客户端 hydration
|
||||
|
||||
// ❌ 不适合场景:
|
||||
// 1. 条件查询(根据用户操作触发)
|
||||
// 2. 需要 placeholderData 或初始数据
|
||||
// 3. 需要在组件内处理 loading/error 状态
|
||||
// 4. 多个查询有依赖关系
|
||||
|
||||
// ✅ 多个独立查询用 useSuspenseQueries
|
||||
function MultipleQueries({ userId }) {
|
||||
const [userQuery, postsQuery] = useSuspenseQueries({
|
||||
queries: [
|
||||
{ queryKey: ['user', userId], queryFn: () => fetchUser(userId) },
|
||||
{ queryKey: ['posts', userId], queryFn: () => fetchPosts(userId) },
|
||||
],
|
||||
});
|
||||
// 两个查询并行执行,都完成后组件渲染
|
||||
return <Profile user={userQuery.data} posts={postsQuery.data} />;
|
||||
}
|
||||
```
|
||||
|
||||
### 乐观更新 (v5 简化)
|
||||
|
||||
```tsx
|
||||
// ❌ 手动管理缓存的乐观更新(复杂)
|
||||
const mutation = useMutation({
|
||||
mutationFn: updateTodo,
|
||||
onMutate: async (newTodo) => {
|
||||
await queryClient.cancelQueries({ queryKey: ['todos'] });
|
||||
const previousTodos = queryClient.getQueryData(['todos']);
|
||||
queryClient.setQueryData(['todos'], (old) => [...old, newTodo]);
|
||||
return { previousTodos };
|
||||
},
|
||||
onError: (err, newTodo, context) => {
|
||||
queryClient.setQueryData(['todos'], context.previousTodos);
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['todos'] });
|
||||
},
|
||||
});
|
||||
|
||||
// ✅ v5 简化:使用 variables 进行乐观 UI
|
||||
function TodoList() {
|
||||
const { data: todos } = useQuery(todosQueryOptions);
|
||||
const { mutate, variables, isPending } = useMutation({
|
||||
mutationFn: addTodo,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['todos'] });
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{todos?.map(todo => <TodoItem key={todo.id} todo={todo} />)}
|
||||
{/* 乐观显示正在添加的 todo */}
|
||||
{isPending && <TodoItem todo={variables} isOptimistic />}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### v5 状态字段变化
|
||||
|
||||
```tsx
|
||||
// v4: isLoading 表示首次加载或后续获取
|
||||
// v5: isPending 表示没有数据,isLoading = isPending && isFetching
|
||||
|
||||
const { data, isPending, isFetching, isLoading } = useQuery({...});
|
||||
|
||||
// isPending: 缓存中没有数据(首次加载)
|
||||
// isFetching: 正在请求中(包括后台刷新)
|
||||
// isLoading: isPending && isFetching(首次加载中)
|
||||
|
||||
// ❌ v4 代码直接迁移
|
||||
if (isLoading) return <Spinner />; // v5 中行为可能不同
|
||||
|
||||
// ✅ 明确意图
|
||||
if (isPending) return <Spinner />; // 没有数据时显示加载
|
||||
// 或
|
||||
if (isLoading) return <Spinner />; // 首次加载中
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Review Checklists
|
||||
|
||||
### Hooks 规则
|
||||
|
||||
- [ ] Hooks 在组件/自定义 Hook 顶层调用
|
||||
- [ ] 没有条件/循环中调用 Hooks
|
||||
- [ ] useEffect 依赖数组完整
|
||||
- [ ] useEffect 有清理函数(订阅/定时器/请求)
|
||||
- [ ] 没有用 useEffect 计算派生状态
|
||||
|
||||
### 性能优化(适度原则)
|
||||
|
||||
- [ ] useMemo/useCallback 只用于真正需要的场景
|
||||
- [ ] React.memo 配合稳定的 props 引用
|
||||
- [ ] 没有在组件内定义子组件
|
||||
- [ ] 没有在 JSX 中创建新对象/函数(除非传给非 memo 组件)
|
||||
- [ ] 长列表使用虚拟化(react-window/react-virtual)
|
||||
|
||||
### 组件设计
|
||||
|
||||
- [ ] 组件职责单一,不超过 200 行
|
||||
- [ ] 逻辑与展示分离(Custom Hooks)
|
||||
- [ ] Props 接口清晰,使用 TypeScript
|
||||
- [ ] 避免 Props Drilling(考虑 Context 或组合)
|
||||
|
||||
### 状态管理
|
||||
|
||||
- [ ] 状态就近原则(最小必要范围)
|
||||
- [ ] 复杂状态用 useReducer
|
||||
- [ ] 全局状态用 Context 或状态库
|
||||
- [ ] 避免不必要的状态(派生 > 存储)
|
||||
|
||||
### 错误处理
|
||||
|
||||
- [ ] 关键区域有 Error Boundary
|
||||
- [ ] Suspense 配合 Error Boundary 使用
|
||||
- [ ] 异步操作有错误处理
|
||||
|
||||
### Server Components (RSC)
|
||||
|
||||
- [ ] 'use client' 只用于需要交互的组件
|
||||
- [ ] Server Component 不使用 Hooks/事件处理
|
||||
- [ ] 客户端组件尽量放在叶子节点
|
||||
- [ ] 数据获取在 Server Component 中进行
|
||||
|
||||
### React 19 Forms
|
||||
|
||||
- [ ] 使用 useActionState 替代多个 useState
|
||||
- [ ] useFormStatus 在 form 子组件中调用
|
||||
- [ ] useOptimistic 不用于关键业务(支付等)
|
||||
- [ ] Server Action 正确标记 'use server'
|
||||
|
||||
### Suspense & Streaming
|
||||
|
||||
- [ ] 按用户体验需求划分 Suspense 边界
|
||||
- [ ] 每个 Suspense 有对应的 Error Boundary
|
||||
- [ ] 提供有意义的 fallback(骨架屏 > Spinner)
|
||||
- [ ] 避免在 layout 层级 await 慢数据
|
||||
|
||||
### TanStack Query
|
||||
|
||||
- [ ] queryKey 包含所有影响数据的参数
|
||||
- [ ] 设置合理的 staleTime(不是默认 0)
|
||||
- [ ] useSuspenseQuery 不使用 enabled
|
||||
- [ ] Mutation 成功后 invalidate 相关查询
|
||||
- [ ] 理解 isPending vs isLoading 区别
|
||||
|
||||
### 测试
|
||||
|
||||
- [ ] 使用 @testing-library/react
|
||||
- [ ] 用 screen 查询元素
|
||||
- [ ] 用 userEvent 代替 fireEvent
|
||||
- [ ] 优先使用 *ByRole 查询
|
||||
- [ ] 测试行为而非实现细节
|
||||
+842
@@ -0,0 +1,842 @@
|
||||
# Rust Code Review Guide
|
||||
|
||||
> Rust 代码审查指南。编译器能捕获内存安全问题,但审查者需要关注编译器无法检测的问题——业务逻辑、API 设计、性能、取消安全性和可维护性。
|
||||
|
||||
## 目录
|
||||
|
||||
- [所有权与借用](#所有权与借用)
|
||||
- [Unsafe 代码审查](#unsafe-代码审查最关键)
|
||||
- [异步代码](#异步代码)
|
||||
- [取消安全性](#取消安全性)
|
||||
- [spawn vs await](#spawn-vs-await)
|
||||
- [错误处理](#错误处理)
|
||||
- [性能](#性能)
|
||||
- [Trait 设计](#trait-设计)
|
||||
- [Review Checklist](#rust-review-checklist)
|
||||
|
||||
---
|
||||
|
||||
## 所有权与借用
|
||||
|
||||
### 避免不必要的 clone()
|
||||
|
||||
```rust
|
||||
// ❌ clone() 是"Rust 的胶带"——用于绕过借用检查器
|
||||
fn bad_process(data: &Data) -> Result<()> {
|
||||
let owned = data.clone(); // 为什么需要 clone?
|
||||
expensive_operation(owned)
|
||||
}
|
||||
|
||||
// ✅ 审查时问:clone 是否必要?能否用借用?
|
||||
fn good_process(data: &Data) -> Result<()> {
|
||||
expensive_operation(data) // 传递引用
|
||||
}
|
||||
|
||||
// ✅ 如果确实需要 clone,添加注释说明原因
|
||||
fn justified_clone(data: &Data) -> Result<()> {
|
||||
// Clone needed: data will be moved to spawned task
|
||||
let owned = data.clone();
|
||||
tokio::spawn(async move {
|
||||
process(owned).await
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Arc<Mutex<T>> 的使用
|
||||
|
||||
```rust
|
||||
// ❌ Arc<Mutex<T>> 可能隐藏不必要的共享状态
|
||||
struct BadService {
|
||||
cache: Arc<Mutex<HashMap<String, Data>>>, // 真的需要共享?
|
||||
}
|
||||
|
||||
// ✅ 考虑是否需要共享,或者设计可以避免
|
||||
struct GoodService {
|
||||
cache: HashMap<String, Data>, // 单一所有者
|
||||
}
|
||||
|
||||
// ✅ 如果确实需要并发访问,考虑更好的数据结构
|
||||
use dashmap::DashMap;
|
||||
|
||||
struct ConcurrentService {
|
||||
cache: DashMap<String, Data>, // 更细粒度的锁
|
||||
}
|
||||
```
|
||||
|
||||
### Cow (Copy-on-Write) 模式
|
||||
|
||||
```rust
|
||||
use std::borrow::Cow;
|
||||
|
||||
// ❌ 总是分配新字符串
|
||||
fn bad_process_name(name: &str) -> String {
|
||||
if name.is_empty() {
|
||||
"Unknown".to_string() // 分配
|
||||
} else {
|
||||
name.to_string() // 不必要的分配
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 使用 Cow 避免不必要的分配
|
||||
fn good_process_name(name: &str) -> Cow<'_, str> {
|
||||
if name.is_empty() {
|
||||
Cow::Borrowed("Unknown") // 静态字符串,无分配
|
||||
} else {
|
||||
Cow::Borrowed(name) // 借用原始数据
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 只在需要修改时才分配
|
||||
fn normalize_name(name: &str) -> Cow<'_, str> {
|
||||
if name.chars().any(|c| c.is_uppercase()) {
|
||||
Cow::Owned(name.to_lowercase()) // 需要修改,分配
|
||||
} else {
|
||||
Cow::Borrowed(name) // 无需修改,借用
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Unsafe 代码审查(最关键!)
|
||||
|
||||
### 基本要求
|
||||
|
||||
```rust
|
||||
// ❌ unsafe 没有安全文档——这是红旗
|
||||
unsafe fn bad_transmute<T, U>(t: T) -> U {
|
||||
std::mem::transmute(t)
|
||||
}
|
||||
|
||||
// ✅ 每个 unsafe 必须解释:为什么安全?什么不变量?
|
||||
/// Transmutes `T` to `U`.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// - `T` and `U` must have the same size and alignment
|
||||
/// - `T` must be a valid bit pattern for `U`
|
||||
/// - The caller ensures no references to `t` exist after this call
|
||||
unsafe fn documented_transmute<T, U>(t: T) -> U {
|
||||
// SAFETY: Caller guarantees size/alignment match and bit validity
|
||||
std::mem::transmute(t)
|
||||
}
|
||||
```
|
||||
|
||||
### Unsafe 块注释
|
||||
|
||||
```rust
|
||||
// ❌ 没有解释的 unsafe 块
|
||||
fn bad_get_unchecked(slice: &[u8], index: usize) -> u8 {
|
||||
unsafe { *slice.get_unchecked(index) }
|
||||
}
|
||||
|
||||
// ✅ 每个 unsafe 块必须有 SAFETY 注释
|
||||
fn good_get_unchecked(slice: &[u8], index: usize) -> u8 {
|
||||
debug_assert!(index < slice.len(), "index out of bounds");
|
||||
// SAFETY: We verified index < slice.len() via debug_assert.
|
||||
// In release builds, callers must ensure valid index.
|
||||
unsafe { *slice.get_unchecked(index) }
|
||||
}
|
||||
|
||||
// ✅ 封装 unsafe 提供安全 API
|
||||
pub fn checked_get(slice: &[u8], index: usize) -> Option<u8> {
|
||||
if index < slice.len() {
|
||||
// SAFETY: bounds check performed above
|
||||
Some(unsafe { *slice.get_unchecked(index) })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 常见 unsafe 模式
|
||||
|
||||
```rust
|
||||
// ✅ FFI 边界
|
||||
extern "C" {
|
||||
fn external_function(ptr: *const u8, len: usize) -> i32;
|
||||
}
|
||||
|
||||
pub fn safe_wrapper(data: &[u8]) -> Result<i32, Error> {
|
||||
// SAFETY: data.as_ptr() is valid for data.len() bytes,
|
||||
// and external_function only reads from the buffer.
|
||||
let result = unsafe {
|
||||
external_function(data.as_ptr(), data.len())
|
||||
};
|
||||
if result < 0 {
|
||||
Err(Error::from_code(result))
|
||||
} else {
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 性能关键路径的 unsafe
|
||||
pub fn fast_copy(src: &[u8], dst: &mut [u8]) {
|
||||
assert_eq!(src.len(), dst.len(), "slices must be equal length");
|
||||
// SAFETY: src and dst are valid slices of equal length,
|
||||
// and dst is mutable so no aliasing.
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(
|
||||
src.as_ptr(),
|
||||
dst.as_mut_ptr(),
|
||||
src.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 异步代码
|
||||
|
||||
### 避免阻塞操作
|
||||
|
||||
```rust
|
||||
// ❌ 在 async 上下文中阻塞——会饿死其他任务
|
||||
async fn bad_async() {
|
||||
let data = std::fs::read_to_string("file.txt").unwrap(); // 阻塞!
|
||||
std::thread::sleep(Duration::from_secs(1)); // 阻塞!
|
||||
}
|
||||
|
||||
// ✅ 使用异步 API
|
||||
async fn good_async() -> Result<String> {
|
||||
let data = tokio::fs::read_to_string("file.txt").await?;
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
// ✅ 如果必须使用阻塞操作,用 spawn_blocking
|
||||
async fn with_blocking() -> Result<Data> {
|
||||
let result = tokio::task::spawn_blocking(|| {
|
||||
// 这里可以安全地进行阻塞操作
|
||||
expensive_cpu_computation()
|
||||
}).await?;
|
||||
Ok(result)
|
||||
}
|
||||
```
|
||||
|
||||
### Mutex 和 .await
|
||||
|
||||
```rust
|
||||
// ❌ 跨 .await 持有 std::sync::Mutex——可能死锁
|
||||
async fn bad_lock(mutex: &std::sync::Mutex<Data>) {
|
||||
let guard = mutex.lock().unwrap();
|
||||
async_operation().await; // 持锁等待!
|
||||
process(&guard);
|
||||
}
|
||||
|
||||
// ✅ 方案1:最小化锁范围
|
||||
async fn good_lock_scoped(mutex: &std::sync::Mutex<Data>) {
|
||||
let data = {
|
||||
let guard = mutex.lock().unwrap();
|
||||
guard.clone() // 立即释放锁
|
||||
};
|
||||
async_operation().await;
|
||||
process(&data);
|
||||
}
|
||||
|
||||
// ✅ 方案2:使用 tokio::sync::Mutex(可跨 await)
|
||||
async fn good_lock_tokio(mutex: &tokio::sync::Mutex<Data>) {
|
||||
let guard = mutex.lock().await;
|
||||
async_operation().await; // OK: tokio Mutex 设计为可跨 await
|
||||
process(&guard);
|
||||
}
|
||||
|
||||
// 💡 选择指南:
|
||||
// - std::sync::Mutex:低竞争、短临界区、不跨 await
|
||||
// - tokio::sync::Mutex:需要跨 await、高竞争场景
|
||||
```
|
||||
|
||||
### 异步 trait 方法
|
||||
|
||||
```rust
|
||||
// ❌ async trait 方法的陷阱(旧版本)
|
||||
#[async_trait]
|
||||
trait BadRepository {
|
||||
async fn find(&self, id: i64) -> Option<Entity>; // 隐式 Box
|
||||
}
|
||||
|
||||
// ✅ Rust 1.75+:原生 async trait 方法
|
||||
trait Repository {
|
||||
async fn find(&self, id: i64) -> Option<Entity>;
|
||||
|
||||
// 返回具体 Future 类型以避免 allocation
|
||||
fn find_many(&self, ids: &[i64]) -> impl Future<Output = Vec<Entity>> + Send;
|
||||
}
|
||||
|
||||
// ✅ 对于需要 dyn 的场景
|
||||
trait DynRepository: Send + Sync {
|
||||
fn find(&self, id: i64) -> Pin<Box<dyn Future<Output = Option<Entity>> + Send + '_>>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 取消安全性
|
||||
|
||||
### 什么是取消安全
|
||||
|
||||
```rust
|
||||
// 当一个 Future 在 .await 点被 drop 时,它处于什么状态?
|
||||
// 取消安全的 Future:可以在任何 await 点安全取消
|
||||
// 取消不安全的 Future:取消可能导致数据丢失或不一致状态
|
||||
|
||||
// ❌ 取消不安全的例子
|
||||
async fn cancel_unsafe(conn: &mut Connection) -> Result<()> {
|
||||
let data = receive_data().await; // 如果这里被取消...
|
||||
conn.send_ack().await; // ...确认永远不会发送,数据可能丢失
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ✅ 取消安全的版本
|
||||
async fn cancel_safe(conn: &mut Connection) -> Result<()> {
|
||||
// 使用事务或原子操作确保一致性
|
||||
let transaction = conn.begin_transaction().await?;
|
||||
let data = receive_data().await;
|
||||
transaction.commit_with_ack(data).await?; // 原子操作
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### select! 中的取消安全
|
||||
|
||||
```rust
|
||||
use tokio::select;
|
||||
|
||||
// ❌ 在 select! 中使用取消不安全的 Future
|
||||
async fn bad_select(stream: &mut TcpStream) {
|
||||
let mut buffer = vec![0u8; 1024];
|
||||
loop {
|
||||
select! {
|
||||
// read_exact 不是取消安全的:timeout 先完成时,
|
||||
// 已经读进 buffer 的部分字节会随 Future 一起丢弃
|
||||
result = stream.read_exact(&mut buffer) => {
|
||||
result?;
|
||||
handle_data(&buffer);
|
||||
}
|
||||
_ = tokio::time::sleep(Duration::from_secs(5)) => {
|
||||
println!("Timeout");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 使用取消安全的 API
|
||||
async fn good_select(stream: &mut TcpStream) {
|
||||
let mut buffer = vec![0u8; 1024];
|
||||
loop {
|
||||
select! {
|
||||
// read 是取消安全的:被取消时未读取的数据仍留在流中
|
||||
// 真的需要按定长读取时,把 read_exact 丢到单独的 task 里,
|
||||
// 这里 select! 它的 JoinHandle,取消就不会丢字节
|
||||
result = stream.read(&mut buffer) => {
|
||||
match result {
|
||||
Ok(0) => break, // EOF
|
||||
Ok(n) => handle_data(&buffer[..n]),
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
_ = tokio::time::sleep(Duration::from_secs(5)) => {
|
||||
println!("Timeout, retrying...");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 使用 tokio::pin! 确保 Future 可以安全重用
|
||||
async fn pinned_select() {
|
||||
let sleep = tokio::time::sleep(Duration::from_secs(10));
|
||||
tokio::pin!(sleep);
|
||||
|
||||
loop {
|
||||
select! {
|
||||
_ = &mut sleep => {
|
||||
println!("Timer elapsed");
|
||||
break;
|
||||
}
|
||||
data = receive_data() => {
|
||||
process(data).await;
|
||||
// sleep 继续倒计时,不会重置
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 文档化取消安全性
|
||||
|
||||
```rust
|
||||
/// Reads a complete message from the stream.
|
||||
///
|
||||
/// # Cancel Safety
|
||||
///
|
||||
/// This method is **not** cancel safe. If cancelled while reading,
|
||||
/// partial data may be lost and the stream state becomes undefined.
|
||||
/// Use `read_message_cancel_safe` if cancellation is expected.
|
||||
async fn read_message(stream: &mut TcpStream) -> Result<Message> {
|
||||
let len = stream.read_u32().await?;
|
||||
let mut buffer = vec![0u8; len as usize];
|
||||
stream.read_exact(&mut buffer).await?;
|
||||
Ok(Message::from_bytes(&buffer))
|
||||
}
|
||||
|
||||
/// Reads a message with cancel safety.
|
||||
///
|
||||
/// # Cancel Safety
|
||||
///
|
||||
/// This method is cancel safe. If cancelled, any partial data
|
||||
/// is preserved in the internal buffer for the next call.
|
||||
async fn read_message_cancel_safe(reader: &mut BufferedReader) -> Result<Message> {
|
||||
reader.read_message_buffered().await
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## spawn vs await
|
||||
|
||||
### 何时使用 spawn
|
||||
|
||||
```rust
|
||||
// ❌ 不必要的 spawn——增加开销,失去结构化并发
|
||||
async fn bad_unnecessary_spawn() {
|
||||
let handle = tokio::spawn(async {
|
||||
simple_operation().await
|
||||
});
|
||||
handle.await.unwrap(); // 为什么不直接 await?
|
||||
}
|
||||
|
||||
// ✅ 直接 await 简单操作
|
||||
async fn good_direct_await() {
|
||||
simple_operation().await;
|
||||
}
|
||||
|
||||
// ✅ spawn 用于真正的并行执行
|
||||
async fn good_parallel_spawn() {
|
||||
let task1 = tokio::spawn(fetch_from_service_a());
|
||||
let task2 = tokio::spawn(fetch_from_service_b());
|
||||
|
||||
// 两个请求并行执行
|
||||
let (result1, result2) = tokio::try_join!(task1, task2)?;
|
||||
}
|
||||
|
||||
// ✅ spawn 用于后台任务(fire-and-forget)
|
||||
async fn good_background_spawn() {
|
||||
// 启动后台任务,不等待完成
|
||||
tokio::spawn(async {
|
||||
cleanup_old_sessions().await;
|
||||
log_metrics().await;
|
||||
});
|
||||
|
||||
// 继续执行其他工作
|
||||
handle_request().await;
|
||||
}
|
||||
```
|
||||
|
||||
### spawn 的 'static 要求
|
||||
|
||||
```rust
|
||||
// ❌ spawn 的 Future 必须是 'static
|
||||
async fn bad_spawn_borrow(data: &Data) {
|
||||
tokio::spawn(async {
|
||||
process(data).await; // Error: `data` 不是 'static
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ 方案1:克隆数据
|
||||
async fn good_spawn_clone(data: &Data) {
|
||||
let owned = data.clone();
|
||||
tokio::spawn(async move {
|
||||
process(&owned).await;
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ 方案2:使用 Arc 共享
|
||||
async fn good_spawn_arc(data: Arc<Data>) {
|
||||
let data = Arc::clone(&data);
|
||||
tokio::spawn(async move {
|
||||
process(&data).await;
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ 方案3:使用作用域任务(tokio-scoped 或 async-scoped)
|
||||
async fn good_scoped_spawn(data: &Data) {
|
||||
// 假设使用 async-scoped crate
|
||||
async_scoped::scope(|s| async {
|
||||
s.spawn(async {
|
||||
process(data).await; // 可以借用
|
||||
});
|
||||
}).await;
|
||||
}
|
||||
```
|
||||
|
||||
### JoinHandle 错误处理
|
||||
|
||||
```rust
|
||||
// ❌ 忽略 spawn 的错误
|
||||
async fn bad_ignore_spawn_error() {
|
||||
let handle = tokio::spawn(async {
|
||||
risky_operation().await
|
||||
});
|
||||
let _ = handle.await; // 忽略了 panic 和错误
|
||||
}
|
||||
|
||||
// ✅ 正确处理 JoinHandle 结果
|
||||
async fn good_handle_spawn_error() -> Result<()> {
|
||||
let handle = tokio::spawn(async {
|
||||
risky_operation().await
|
||||
});
|
||||
|
||||
match handle.await {
|
||||
Ok(Ok(result)) => {
|
||||
// 任务成功完成
|
||||
process_result(result);
|
||||
Ok(())
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
// 任务内部错误
|
||||
Err(e.into())
|
||||
}
|
||||
Err(join_err) => {
|
||||
// 任务 panic 或被取消
|
||||
if join_err.is_panic() {
|
||||
error!("Task panicked: {:?}", join_err);
|
||||
}
|
||||
Err(anyhow!("Task failed: {}", join_err))
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 结构化并发 vs spawn
|
||||
|
||||
```rust
|
||||
// ✅ 优先使用 join!(结构化并发)
|
||||
async fn structured_concurrency() -> Result<(A, B, C)> {
|
||||
// 所有任务在同一个作用域内
|
||||
// 如果任何一个失败,其他的会被取消
|
||||
tokio::try_join!(
|
||||
fetch_a(),
|
||||
fetch_b(),
|
||||
fetch_c()
|
||||
)
|
||||
}
|
||||
|
||||
// ✅ 使用 spawn 时考虑任务生命周期
|
||||
struct TaskManager {
|
||||
handles: Vec<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl TaskManager {
|
||||
async fn shutdown(self) {
|
||||
// 优雅关闭:等待所有任务完成
|
||||
for handle in self.handles {
|
||||
if let Err(e) = handle.await {
|
||||
error!("Task failed during shutdown: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn abort_all(self) {
|
||||
// 强制关闭:取消所有任务
|
||||
for handle in self.handles {
|
||||
handle.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 库 vs 应用的错误类型
|
||||
|
||||
```rust
|
||||
// ❌ 库代码用 anyhow——调用者无法 match 错误
|
||||
pub fn parse_config(s: &str) -> anyhow::Result<Config> { ... }
|
||||
|
||||
// ✅ 库用 thiserror,应用用 anyhow
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ConfigError {
|
||||
#[error("invalid syntax at line {line}: {message}")]
|
||||
Syntax { line: usize, message: String },
|
||||
#[error("missing required field: {0}")]
|
||||
MissingField(String),
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
pub fn parse_config(s: &str) -> Result<Config, ConfigError> { ... }
|
||||
```
|
||||
|
||||
### 保留错误上下文
|
||||
|
||||
```rust
|
||||
// ❌ 吞掉错误上下文
|
||||
fn bad_error() -> Result<()> {
|
||||
operation().map_err(|_| anyhow!("failed"))?; // 原始错误丢失
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ✅ 使用 context 保留错误链
|
||||
fn good_error() -> Result<()> {
|
||||
operation().context("failed to perform operation")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ✅ 使用 with_context 进行懒计算
|
||||
fn good_error_lazy() -> Result<()> {
|
||||
operation()
|
||||
.with_context(|| format!("failed to process file: {}", filename))?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### 错误类型设计
|
||||
|
||||
```rust
|
||||
// ✅ 使用 #[source] 保留错误链
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ServiceError {
|
||||
#[error("database error")]
|
||||
Database(#[source] sqlx::Error),
|
||||
|
||||
#[error("network error: {message}")]
|
||||
Network {
|
||||
message: String,
|
||||
#[source]
|
||||
source: reqwest::Error,
|
||||
},
|
||||
|
||||
#[error("validation failed: {0}")]
|
||||
Validation(String),
|
||||
}
|
||||
|
||||
// ✅ 为常见转换实现 From
|
||||
impl From<sqlx::Error> for ServiceError {
|
||||
fn from(err: sqlx::Error) -> Self {
|
||||
ServiceError::Database(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 性能
|
||||
|
||||
### 避免不必要的 collect()
|
||||
|
||||
```rust
|
||||
// ❌ 不必要的 collect——中间分配
|
||||
fn bad_sum(items: &[i32]) -> i32 {
|
||||
items.iter()
|
||||
.filter(|x| **x > 0)
|
||||
.collect::<Vec<_>>() // 不必要!
|
||||
.iter()
|
||||
.sum()
|
||||
}
|
||||
|
||||
// ✅ 惰性迭代
|
||||
fn good_sum(items: &[i32]) -> i32 {
|
||||
items.iter().filter(|x| **x > 0).copied().sum()
|
||||
}
|
||||
```
|
||||
|
||||
### 字符串拼接
|
||||
|
||||
```rust
|
||||
// ❌ 字符串拼接在循环中重复分配
|
||||
fn bad_concat(items: &[&str]) -> String {
|
||||
let mut s = String::new();
|
||||
for item in items {
|
||||
s = s + item; // 每次都重新分配!
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
// ✅ 预分配或用 join
|
||||
fn good_concat(items: &[&str]) -> String {
|
||||
items.join("")
|
||||
}
|
||||
|
||||
// ✅ 使用 with_capacity 预分配
|
||||
fn good_concat_capacity(items: &[&str]) -> String {
|
||||
let total_len: usize = items.iter().map(|s| s.len()).sum();
|
||||
let mut result = String::with_capacity(total_len);
|
||||
for item in items {
|
||||
result.push_str(item);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
// ✅ 使用 write! 宏
|
||||
use std::fmt::Write;
|
||||
|
||||
fn good_concat_write(items: &[&str]) -> String {
|
||||
let mut result = String::new();
|
||||
for item in items {
|
||||
write!(result, "{}", item).unwrap();
|
||||
}
|
||||
result
|
||||
}
|
||||
```
|
||||
|
||||
### 避免不必要的分配
|
||||
|
||||
```rust
|
||||
// ❌ 不必要的 Vec 分配
|
||||
fn bad_check_any(items: &[Item]) -> bool {
|
||||
let filtered: Vec<_> = items.iter()
|
||||
.filter(|i| i.is_valid())
|
||||
.collect();
|
||||
!filtered.is_empty()
|
||||
}
|
||||
|
||||
// ✅ 使用迭代器方法
|
||||
fn good_check_any(items: &[Item]) -> bool {
|
||||
items.iter().any(|i| i.is_valid())
|
||||
}
|
||||
|
||||
// ❌ String::from 用于静态字符串
|
||||
fn bad_static() -> String {
|
||||
String::from("error message") // 运行时分配
|
||||
}
|
||||
|
||||
// ✅ 返回 &'static str
|
||||
fn good_static() -> &'static str {
|
||||
"error message" // 无分配
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Trait 设计
|
||||
|
||||
### 避免过度抽象
|
||||
|
||||
```rust
|
||||
// ❌ 过度抽象——不是 Java,不需要 Interface 一切
|
||||
trait Processor { fn process(&self); }
|
||||
trait Handler { fn handle(&self); }
|
||||
trait Manager { fn manage(&self); } // Trait 过多
|
||||
|
||||
// ✅ 只在需要多态时创建 trait
|
||||
// 具体类型通常更简单、更快
|
||||
struct DataProcessor {
|
||||
config: Config,
|
||||
}
|
||||
|
||||
impl DataProcessor {
|
||||
fn process(&self, data: &Data) -> Result<Output> {
|
||||
// 直接实现
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Trait 对象 vs 泛型
|
||||
|
||||
```rust
|
||||
// ❌ 不必要的 trait 对象(动态分发)
|
||||
fn bad_process(handler: &dyn Handler) {
|
||||
handler.handle(); // 虚表调用
|
||||
}
|
||||
|
||||
// ✅ 使用泛型(静态分发,可内联)
|
||||
fn good_process<H: Handler>(handler: &H) {
|
||||
handler.handle(); // 可能被内联
|
||||
}
|
||||
|
||||
// ✅ trait 对象适用场景:异构集合
|
||||
fn store_handlers(handlers: Vec<Box<dyn Handler>>) {
|
||||
// 需要存储不同类型的 handlers
|
||||
}
|
||||
|
||||
// ✅ 使用 impl Trait 返回类型
|
||||
fn create_handler() -> impl Handler {
|
||||
ConcreteHandler::new()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rust Review Checklist
|
||||
|
||||
### 编译器不能捕获的问题
|
||||
|
||||
**业务逻辑正确性**
|
||||
- [ ] 边界条件处理正确
|
||||
- [ ] 状态机转换完整
|
||||
- [ ] 并发场景下的竞态条件
|
||||
|
||||
**API 设计**
|
||||
- [ ] 公共 API 难以误用
|
||||
- [ ] 类型签名清晰表达意图
|
||||
- [ ] 错误类型粒度合适
|
||||
|
||||
### 所有权与借用
|
||||
|
||||
- [ ] clone() 是有意为之,文档说明了原因
|
||||
- [ ] Arc<Mutex<T>> 真的需要共享状态吗?
|
||||
- [ ] RefCell 的使用有正当理由
|
||||
- [ ] 生命周期不过度复杂
|
||||
- [ ] 考虑使用 Cow 避免不必要的分配
|
||||
|
||||
### Unsafe 代码(最重要)
|
||||
|
||||
- [ ] 每个 unsafe 块有 SAFETY 注释
|
||||
- [ ] unsafe fn 有 # Safety 文档节
|
||||
- [ ] 解释了为什么是安全的,不只是做什么
|
||||
- [ ] 列出了必须维护的不变量
|
||||
- [ ] unsafe 边界尽可能小
|
||||
- [ ] 考虑过是否有 safe 替代方案
|
||||
|
||||
### 异步/并发
|
||||
|
||||
- [ ] 没有在 async 中阻塞(std::fs、thread::sleep)
|
||||
- [ ] 没有跨 .await 持有 std::sync 锁
|
||||
- [ ] spawn 的任务满足 'static
|
||||
- [ ] 锁的获取顺序一致
|
||||
- [ ] Channel 缓冲区大小合理
|
||||
|
||||
### 取消安全性
|
||||
|
||||
- [ ] select! 中的 Future 是取消安全的
|
||||
- [ ] 文档化了 async 函数的取消安全性
|
||||
- [ ] 取消不会导致数据丢失或不一致状态
|
||||
- [ ] 使用 tokio::pin! 正确处理需要重用的 Future
|
||||
|
||||
### spawn vs await
|
||||
|
||||
- [ ] spawn 只用于真正需要并行的场景
|
||||
- [ ] 简单操作直接 await,不要 spawn
|
||||
- [ ] spawn 的 JoinHandle 结果被正确处理
|
||||
- [ ] 考虑任务的生命周期和关闭策略
|
||||
- [ ] 优先使用 join!/try_join! 进行结构化并发
|
||||
|
||||
### 错误处理
|
||||
|
||||
- [ ] 库:thiserror 定义结构化错误
|
||||
- [ ] 应用:anyhow + context
|
||||
- [ ] 没有生产代码 unwrap/expect
|
||||
- [ ] 错误消息对调试有帮助
|
||||
- [ ] must_use 返回值被处理
|
||||
- [ ] 使用 #[source] 保留错误链
|
||||
|
||||
### 性能
|
||||
|
||||
- [ ] 避免不必要的 collect()
|
||||
- [ ] 大数据传引用
|
||||
- [ ] 字符串用 with_capacity 或 write!
|
||||
- [ ] impl Trait vs Box<dyn Trait> 选择合理
|
||||
- [ ] 热路径避免分配
|
||||
- [ ] 考虑使用 Cow 减少克隆
|
||||
|
||||
### 代码质量
|
||||
|
||||
- [ ] cargo clippy 零警告
|
||||
- [ ] cargo fmt 格式化
|
||||
- [ ] 文档注释完整
|
||||
- [ ] 测试覆盖边界条件
|
||||
- [ ] 公共 API 有文档示例
|
||||
@@ -0,0 +1,266 @@
|
||||
# Security Review Guide
|
||||
|
||||
Security-focused code review checklist based on OWASP Top 10 and best practices.
|
||||
|
||||
## Authentication & Authorization
|
||||
|
||||
### Authentication
|
||||
- [ ] Passwords hashed with strong algorithm (bcrypt, argon2)
|
||||
- [ ] Password complexity requirements enforced
|
||||
- [ ] Account lockout after failed attempts
|
||||
- [ ] Secure password reset flow
|
||||
- [ ] Multi-factor authentication for sensitive operations
|
||||
- [ ] Session tokens are cryptographically random
|
||||
- [ ] Session timeout implemented
|
||||
|
||||
### Authorization
|
||||
- [ ] Authorization checks on every request
|
||||
- [ ] Principle of least privilege applied
|
||||
- [ ] Role-based access control (RBAC) properly implemented
|
||||
- [ ] No privilege escalation paths
|
||||
- [ ] Direct object reference checks (IDOR prevention)
|
||||
- [ ] API endpoints protected appropriately
|
||||
|
||||
### JWT Security
|
||||
```typescript
|
||||
// ❌ Insecure JWT configuration
|
||||
jwt.sign(payload, 'weak-secret');
|
||||
|
||||
// ✅ Secure JWT configuration
|
||||
jwt.sign(payload, process.env.JWT_SECRET, {
|
||||
algorithm: 'RS256',
|
||||
expiresIn: '15m',
|
||||
issuer: 'your-app',
|
||||
audience: 'your-api'
|
||||
});
|
||||
|
||||
// ❌ Not verifying JWT properly
|
||||
const decoded = jwt.decode(token); // No signature verification!
|
||||
|
||||
// ✅ Verify signature and claims
|
||||
const decoded = jwt.verify(token, publicKey, {
|
||||
algorithms: ['RS256'],
|
||||
issuer: 'your-app',
|
||||
audience: 'your-api'
|
||||
});
|
||||
```
|
||||
|
||||
## Input Validation
|
||||
|
||||
### SQL Injection Prevention
|
||||
```python
|
||||
# ❌ Vulnerable to SQL injection
|
||||
query = f"SELECT * FROM users WHERE id = {user_id}"
|
||||
|
||||
# ✅ Use parameterized queries
|
||||
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
|
||||
|
||||
# ✅ Use ORM with proper escaping
|
||||
User.objects.filter(id=user_id)
|
||||
```
|
||||
|
||||
### XSS Prevention
|
||||
```typescript
|
||||
// ❌ Vulnerable to XSS
|
||||
element.innerHTML = userInput;
|
||||
|
||||
// ✅ Use textContent for plain text
|
||||
element.textContent = userInput;
|
||||
|
||||
// ✅ Use DOMPurify for HTML
|
||||
element.innerHTML = DOMPurify.sanitize(userInput);
|
||||
|
||||
// ✅ React automatically escapes (but watch dangerouslySetInnerHTML)
|
||||
return <div>{userInput}</div>; // Safe
|
||||
return <div dangerouslySetInnerHTML={{__html: userInput}} />; // Dangerous!
|
||||
```
|
||||
|
||||
### Command Injection Prevention
|
||||
```python
|
||||
# ❌ Vulnerable to command injection
|
||||
os.system(f"convert {filename} output.png")
|
||||
|
||||
# ✅ Use subprocess with list arguments
|
||||
subprocess.run(['convert', filename, 'output.png'], check=True)
|
||||
|
||||
# ✅ Validate and sanitize input
|
||||
import shlex
|
||||
safe_filename = shlex.quote(filename)
|
||||
```
|
||||
|
||||
### Path Traversal Prevention
|
||||
```typescript
|
||||
// ❌ Vulnerable to path traversal
|
||||
const filePath = `./uploads/${req.params.filename}`;
|
||||
|
||||
// ✅ Validate and sanitize path
|
||||
const path = require('path');
|
||||
const safeName = path.basename(req.params.filename);
|
||||
const uploadsDir = path.resolve('./uploads');
|
||||
const filePath = path.resolve(uploadsDir, safeName);
|
||||
|
||||
// Verify it's still within uploads directory (both sides absolute)
|
||||
if (!filePath.startsWith(uploadsDir + path.sep)) {
|
||||
throw new Error('Invalid path');
|
||||
}
|
||||
```
|
||||
|
||||
## Data Protection
|
||||
|
||||
### Sensitive Data Handling
|
||||
- [ ] No secrets in source code
|
||||
- [ ] Secrets stored in environment variables or secret manager
|
||||
- [ ] Sensitive data encrypted at rest
|
||||
- [ ] Sensitive data encrypted in transit (HTTPS)
|
||||
- [ ] PII handled according to regulations (GDPR, etc.)
|
||||
- [ ] Sensitive data not logged
|
||||
- [ ] Secure data deletion when required
|
||||
|
||||
### Configuration Security
|
||||
```yaml
|
||||
# ❌ Secrets in config files
|
||||
database:
|
||||
password: "super-secret-password"
|
||||
|
||||
# ✅ Reference environment variables
|
||||
database:
|
||||
password: ${DATABASE_PASSWORD}
|
||||
```
|
||||
|
||||
### Error Messages
|
||||
```typescript
|
||||
// ❌ Leaking sensitive information
|
||||
catch (error) {
|
||||
return res.status(500).json({
|
||||
error: error.stack, // Exposes internal details
|
||||
query: sqlQuery // Exposes database structure
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ Generic error messages
|
||||
catch (error) {
|
||||
logger.error('Database error', { error, userId }); // Log internally
|
||||
return res.status(500).json({
|
||||
error: 'An unexpected error occurred'
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## API Security
|
||||
|
||||
### Rate Limiting
|
||||
- [ ] Rate limiting on all public endpoints
|
||||
- [ ] Stricter limits on authentication endpoints
|
||||
- [ ] Per-user and per-IP limits
|
||||
- [ ] Graceful handling when limits exceeded
|
||||
|
||||
### CORS Configuration
|
||||
```typescript
|
||||
// ❌ Overly permissive CORS
|
||||
app.use(cors({ origin: '*' }));
|
||||
|
||||
// ✅ Restrictive CORS
|
||||
app.use(cors({
|
||||
origin: ['https://your-app.com'],
|
||||
methods: ['GET', 'POST'],
|
||||
credentials: true
|
||||
}));
|
||||
```
|
||||
|
||||
### HTTP Headers
|
||||
```typescript
|
||||
// Security headers to set
|
||||
app.use(helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
scriptSrc: ["'self'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||
}
|
||||
},
|
||||
hsts: { maxAge: 31536000, includeSubDomains: true },
|
||||
noSniff: true,
|
||||
xssFilter: true,
|
||||
frameguard: { action: 'deny' }
|
||||
}));
|
||||
```
|
||||
|
||||
## Cryptography
|
||||
|
||||
### Secure Practices
|
||||
- [ ] Using well-established algorithms (AES-256, RSA-2048+)
|
||||
- [ ] Not implementing custom cryptography
|
||||
- [ ] Using cryptographically secure random number generation
|
||||
- [ ] Proper key management and rotation
|
||||
- [ ] Secure key storage (HSM, KMS)
|
||||
|
||||
### Common Mistakes
|
||||
```typescript
|
||||
// ❌ Weak random generation
|
||||
const token = Math.random().toString(36);
|
||||
|
||||
// ✅ Cryptographically secure random
|
||||
const crypto = require('crypto');
|
||||
const token = crypto.randomBytes(32).toString('hex');
|
||||
|
||||
// ❌ MD5/SHA1 for passwords
|
||||
const hash = crypto.createHash('md5').update(password).digest('hex');
|
||||
|
||||
// ✅ Use bcrypt or argon2
|
||||
const bcrypt = require('bcrypt');
|
||||
const hash = await bcrypt.hash(password, 12);
|
||||
```
|
||||
|
||||
## Dependency Security
|
||||
|
||||
### Checklist
|
||||
- [ ] Dependencies from trusted sources only
|
||||
- [ ] No known vulnerabilities (npm audit, cargo audit)
|
||||
- [ ] Dependencies kept up to date
|
||||
- [ ] Lock files committed (package-lock.json, Cargo.lock)
|
||||
- [ ] Minimal dependency usage
|
||||
- [ ] License compliance verified
|
||||
|
||||
### Audit Commands
|
||||
```bash
|
||||
# Node.js
|
||||
npm audit
|
||||
npm audit fix
|
||||
|
||||
# Python
|
||||
pip-audit
|
||||
safety check
|
||||
|
||||
# Rust
|
||||
cargo audit
|
||||
|
||||
# General
|
||||
snyk test
|
||||
```
|
||||
|
||||
## Logging & Monitoring
|
||||
|
||||
### Secure Logging
|
||||
- [ ] No sensitive data in logs (passwords, tokens, PII)
|
||||
- [ ] Logs protected from tampering
|
||||
- [ ] Appropriate log retention
|
||||
- [ ] Security events logged (login attempts, permission changes)
|
||||
- [ ] Log injection prevented
|
||||
|
||||
```typescript
|
||||
// ❌ Logging sensitive data
|
||||
logger.info(`User login: ${email}, password: ${password}`);
|
||||
|
||||
// ✅ Safe logging
|
||||
logger.info('User login attempt', { email, success: true });
|
||||
```
|
||||
|
||||
## Security Review Severity Levels
|
||||
|
||||
| Severity | Description | Action |
|
||||
|----------|-------------|--------|
|
||||
| **Critical** | Immediate exploitation possible, data breach risk | Block merge, fix immediately |
|
||||
| **High** | Significant vulnerability, requires specific conditions | Block merge, fix before release |
|
||||
| **Medium** | Moderate risk, defense in depth concern | Should fix, can merge with tracking |
|
||||
| **Low** | Minor issue, best practice violation | Nice to fix, non-blocking |
|
||||
| **Info** | Suggestion for improvement | Optional enhancement |
|
||||
+1060
File diff suppressed because it is too large
Load Diff
+932
@@ -0,0 +1,932 @@
|
||||
# Swift Code Review Guide
|
||||
|
||||
A code review checklist for modern Swift (5.9+/6), covering SwiftUI, Swift Concurrency, and the Swift API Design Guidelines.
|
||||
|
||||
## Quick Review Checklist
|
||||
|
||||
### Must-Check Items
|
||||
- [ ] Are force-unwraps (`!`) and `try!` avoided in favor of safe unwrapping
|
||||
- [ ] Do closures that capture `self` use `[weak self]` to avoid retain cycles
|
||||
- [ ] Is the value vs reference type choice intentional (struct vs class)
|
||||
- [ ] Are errors propagated with `throws`/`Result` instead of being swallowed
|
||||
- [ ] Are concurrency boundaries data-race-safe (`Sendable`, `@MainActor`, actors)
|
||||
|
||||
### Common Issues
|
||||
- [ ] Fire-and-forget `Task {}` that leaks or is never cancelled
|
||||
- [ ] Wrong SwiftUI property wrapper (`@ObservedObject` where `@StateObject` is needed)
|
||||
- [ ] O(n^2) lookups in loops that could use a `Set` or `Dictionary`
|
||||
- [ ] Implicitly unwrapped optionals (`var x: T!`) outside of IBOutlets
|
||||
- [ ] Over-broad access control (`public`/`open` where `internal` suffices)
|
||||
- [ ] Naming that ignores the Swift API Design Guidelines
|
||||
|
||||
---
|
||||
|
||||
## 1. Optionals and Unwrapping
|
||||
|
||||
### 1.1 Avoid Force-Unwrapping
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: crashes at runtime if nil
|
||||
let name = user.name!
|
||||
let url = URL(string: urlString)!
|
||||
|
||||
// ✅ Correct: bind with guard let / if let
|
||||
guard let name = user.name else {
|
||||
return
|
||||
}
|
||||
|
||||
if let url = URL(string: urlString) {
|
||||
load(url)
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 Use Nil-Coalescing for Defaults
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: verbose and crash-prone
|
||||
let count: Int
|
||||
if let c = dictionary["count"] {
|
||||
count = c
|
||||
} else {
|
||||
count = 0
|
||||
}
|
||||
|
||||
// ✅ Correct: nil-coalescing
|
||||
let count = dictionary["count"] ?? 0
|
||||
```
|
||||
|
||||
### 1.3 Prefer guard let for Early Exit
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: deep nesting (pyramid of doom)
|
||||
func process(_ input: String?) {
|
||||
if let input = input {
|
||||
if let value = Int(input) {
|
||||
if value > 0 {
|
||||
handle(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Correct: guard keeps the happy path unindented
|
||||
func process(_ input: String?) {
|
||||
guard let input,
|
||||
let value = Int(input),
|
||||
value > 0 else {
|
||||
return
|
||||
}
|
||||
handle(value)
|
||||
}
|
||||
```
|
||||
|
||||
### 1.4 Avoid Implicitly Unwrapped Optionals
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: T! is a hidden force-unwrap on every access
|
||||
class ViewModel {
|
||||
var service: NetworkService!
|
||||
}
|
||||
|
||||
// ✅ Correct: inject a non-optional dependency
|
||||
class ViewModel {
|
||||
private let service: NetworkService
|
||||
|
||||
init(service: NetworkService) {
|
||||
self.service = service
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 1.5 Use Optional Chaining and map/flatMap
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: manual unwrapping just to transform
|
||||
var initial: String?
|
||||
if let name = user.name {
|
||||
initial = String(name.prefix(1))
|
||||
}
|
||||
|
||||
// ✅ Correct: optional chaining + map
|
||||
let initial = user.name.map { String($0.prefix(1)) }
|
||||
|
||||
// ✅ Correct: flatMap to avoid double optionals
|
||||
let port: Int? = components.port.flatMap { Int(exactly: $0) }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Memory Management and Retain Cycles
|
||||
|
||||
### 2.1 Use [weak self] in Escaping Closures
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: closure strongly captures self, creating a retain cycle
|
||||
class ImageLoader {
|
||||
var onComplete: (() -> Void)?
|
||||
|
||||
func load() {
|
||||
service.fetch { data in
|
||||
self.cache = data // self is retained by the closure
|
||||
self.onComplete?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Correct: capture self weakly and guard
|
||||
class ImageLoader {
|
||||
var onComplete: (() -> Void)?
|
||||
|
||||
func load() {
|
||||
service.fetch { [weak self] data in
|
||||
guard let self else { return }
|
||||
self.cache = data
|
||||
self.onComplete?()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 weak vs unowned
|
||||
|
||||
```swift
|
||||
// ✅ Use weak when the reference can legitimately become nil
|
||||
class Controller {
|
||||
weak var delegate: ControllerDelegate?
|
||||
}
|
||||
|
||||
// ✅ Use unowned only when the captured object is guaranteed to
|
||||
// outlive the closure (e.g. self owns the closure tightly).
|
||||
// unowned crashes if accessed after deallocation.
|
||||
class Owner {
|
||||
lazy var describe: () -> String = { [unowned self] in
|
||||
self.name
|
||||
}
|
||||
let name = "owner"
|
||||
}
|
||||
|
||||
// ❌ Wrong: unowned on something that can outlive self -> crash
|
||||
networkClient.onResponse = { [unowned self] in self.update() }
|
||||
// Prefer [weak self] here, since onResponse may fire after self is gone.
|
||||
```
|
||||
|
||||
### 2.3 Break Delegate Retain Cycles
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: strong delegate keeps both objects alive forever
|
||||
protocol DataSourceDelegate: AnyObject {}
|
||||
|
||||
class DataSource {
|
||||
var delegate: DataSourceDelegate? // strong by default
|
||||
}
|
||||
|
||||
// ✅ Correct: delegates should be weak (and protocol AnyObject-bound)
|
||||
class DataSource {
|
||||
weak var delegate: DataSourceDelegate?
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 Closures Stored as Properties
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: stored closure captures self strongly -> permanent cycle
|
||||
class Timer {
|
||||
var tick: (() -> Void)!
|
||||
func configure() {
|
||||
tick = { self.count += 1 }
|
||||
}
|
||||
var count = 0
|
||||
}
|
||||
|
||||
// ✅ Correct: weak capture for stored closures referencing self
|
||||
class Timer {
|
||||
var tick: (() -> Void)?
|
||||
func configure() {
|
||||
tick = { [weak self] in self?.count += 1 }
|
||||
}
|
||||
var count = 0
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Value vs Reference Types
|
||||
|
||||
### 3.1 Prefer Structs by Default
|
||||
|
||||
```swift
|
||||
// ✅ Use a struct for data/models with value semantics
|
||||
struct Coordinate {
|
||||
var latitude: Double
|
||||
var longitude: Double
|
||||
}
|
||||
|
||||
// Copies are independent; no shared mutable state, thread-friendly.
|
||||
var a = Coordinate(latitude: 1, longitude: 2)
|
||||
var b = a
|
||||
b.latitude = 99 // a is unchanged
|
||||
```
|
||||
|
||||
### 3.2 Use a Class for Identity or Shared State
|
||||
|
||||
```swift
|
||||
// ✅ Use a class when instances have identity or must be shared/mutated
|
||||
// by reference, or when you need inheritance / Objective-C interop.
|
||||
final class DatabaseConnection {
|
||||
private(set) var isOpen = false
|
||||
func open() { isOpen = true }
|
||||
}
|
||||
|
||||
// Two references point to the same connection.
|
||||
let conn1 = DatabaseConnection()
|
||||
let conn2 = conn1
|
||||
conn1.open()
|
||||
// conn2.isOpen == true
|
||||
```
|
||||
|
||||
### 3.3 Mark Classes final When Not Subclassed
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: open to subclassing unintentionally (slower dispatch, fragile API)
|
||||
class UserViewModel {}
|
||||
|
||||
// ✅ Correct: final enables static dispatch and signals intent
|
||||
final class UserViewModel {}
|
||||
```
|
||||
|
||||
### 3.4 Beware Reference Types Inside Structs
|
||||
|
||||
```swift
|
||||
// ❌ Surprising: struct copy still shares the inner class instance
|
||||
final class Box { var value = 0 }
|
||||
struct Container { var box = Box() }
|
||||
|
||||
var x = Container()
|
||||
var y = x
|
||||
y.box.value = 42 // x.box.value is also 42 (shared reference!)
|
||||
|
||||
// ✅ Correct: use value semantics throughout, or copy on write deliberately
|
||||
struct Container {
|
||||
var value = 0 // plain value type, copies are independent
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Error Handling
|
||||
|
||||
### 4.1 Avoid try! and try?
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: try! crashes on any thrown error
|
||||
let data = try! Data(contentsOf: url)
|
||||
|
||||
// ❌ Often wrong: try? silently discards the error and the cause
|
||||
let data = try? Data(contentsOf: url) // data is nil, you lose "why"
|
||||
|
||||
// ✅ Correct: propagate or handle with do-catch
|
||||
do {
|
||||
let data = try Data(contentsOf: url)
|
||||
process(data)
|
||||
} catch {
|
||||
log.error("failed to read \(url): \(error)")
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 Define Meaningful Error Types
|
||||
|
||||
```swift
|
||||
// ✅ Recommended: an Error enum communicates failure modes precisely
|
||||
enum NetworkError: Error {
|
||||
case invalidURL
|
||||
case unauthorized
|
||||
case server(statusCode: Int)
|
||||
case decoding(underlying: Error)
|
||||
}
|
||||
|
||||
func fetch(_ path: String) throws -> Data {
|
||||
guard let url = URL(string: path) else {
|
||||
throw NetworkError.invalidURL
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 Use Result for Stored or Deferred Outcomes
|
||||
|
||||
```swift
|
||||
// ✅ Result is useful at callback boundaries or when storing an outcome
|
||||
func load(completion: @escaping (Result<User, NetworkError>) -> Void) {
|
||||
// completion(.success(user)) or completion(.failure(.unauthorized))
|
||||
}
|
||||
|
||||
// ✅ Convert between Result and throws as needed
|
||||
let user = try result.get()
|
||||
```
|
||||
|
||||
### 4.4 Typed Throws (Swift 6)
|
||||
|
||||
```swift
|
||||
// ✅ Typed throws constrains the error type when it is fully known.
|
||||
// Use it for closed, exhaustive error domains; prefer untyped
|
||||
// `throws` for library APIs that may grow new error cases.
|
||||
func parse(_ raw: String) throws(ParsingError) -> Token {
|
||||
guard let token = Token(raw) else {
|
||||
throw ParsingError.malformed
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
do {
|
||||
let token = try parse(input)
|
||||
} catch {
|
||||
// `error` is statically known to be ParsingError
|
||||
handle(error)
|
||||
}
|
||||
```
|
||||
|
||||
### 4.5 Don't Catch and Rethrow Without Value
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: catch that adds nothing but obscures the trace
|
||||
do {
|
||||
try work()
|
||||
} catch {
|
||||
throw error // pointless
|
||||
}
|
||||
|
||||
// ✅ Correct: only catch to add context or recover
|
||||
do {
|
||||
try work()
|
||||
} catch {
|
||||
throw AppError.workFailed(underlying: error)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Swift Concurrency
|
||||
|
||||
### 5.1 Prefer async/await Over Nested Callbacks
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: callback pyramid, error handling scattered
|
||||
func loadProfile(completion: @escaping (Result<Profile, Error>) -> Void) {
|
||||
fetchUser { userResult in
|
||||
switch userResult {
|
||||
case .success(let user):
|
||||
fetchAvatar(user) { avatarResult in /* ... */ }
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Correct: linear async/await
|
||||
func loadProfile() async throws -> Profile {
|
||||
let user = try await fetchUser()
|
||||
let avatar = try await fetchAvatar(user)
|
||||
return Profile(user: user, avatar: avatar)
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 Use @MainActor for UI State
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: mutating UI state from a background context (data race / crash)
|
||||
func refresh() async {
|
||||
let items = try? await api.load()
|
||||
self.items = items ?? [] // may run off the main thread
|
||||
}
|
||||
|
||||
// ✅ Correct: isolate UI-facing types to the main actor
|
||||
@MainActor
|
||||
final class FeedViewModel: ObservableObject {
|
||||
@Published var items: [Item] = []
|
||||
|
||||
func refresh() async {
|
||||
let loaded = (try? await api.load()) ?? []
|
||||
items = loaded // guaranteed on the main actor
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 Protect Mutable State with Actors
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: shared mutable state without synchronization (data race)
|
||||
final class Counter {
|
||||
var value = 0
|
||||
func increment() { value += 1 }
|
||||
}
|
||||
|
||||
// ✅ Correct: an actor serializes access to its mutable state
|
||||
actor Counter {
|
||||
private(set) var value = 0
|
||||
func increment() { value += 1 }
|
||||
}
|
||||
|
||||
let counter = Counter()
|
||||
await counter.increment() // access is awaited and serialized
|
||||
```
|
||||
|
||||
### 5.4 Conform Shared Types to Sendable
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: passing a non-Sendable class across actors (Swift 6 error)
|
||||
final class Config { // mutable, not Sendable
|
||||
var retries = 3
|
||||
}
|
||||
|
||||
// ✅ Correct: make shared types Sendable (immutable value type is ideal)
|
||||
struct Config: Sendable {
|
||||
let retries: Int
|
||||
}
|
||||
|
||||
// ✅ For reference types, use final + immutable stored properties,
|
||||
// or @unchecked Sendable only with manual synchronization.
|
||||
final class Cache: @unchecked Sendable {
|
||||
private let lock = NSLock()
|
||||
private var storage: [String: Data] = [:]
|
||||
// all access guarded by lock
|
||||
}
|
||||
```
|
||||
|
||||
### 5.5 Handle Task Cancellation
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: ignores cancellation, keeps working after the view is gone
|
||||
func search(_ query: String) async -> [Result] {
|
||||
var results: [Result] = []
|
||||
for page in 0..<100 {
|
||||
results += await fetchPage(query, page) // never stops
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// ✅ Correct: check for cancellation cooperatively
|
||||
func search(_ query: String) async throws -> [Result] {
|
||||
var results: [Result] = []
|
||||
for page in 0..<100 {
|
||||
try Task.checkCancellation()
|
||||
results += try await fetchPage(query, page)
|
||||
}
|
||||
return results
|
||||
}
|
||||
```
|
||||
|
||||
### 5.6 Don't Leak Fire-and-Forget Tasks
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: unstructured Task with no handle, never cancelled
|
||||
final class ViewModel {
|
||||
func onAppear() {
|
||||
Task {
|
||||
await self.stream() // runs forever even after dismissal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Correct: retain the handle and cancel it (or use .task in SwiftUI)
|
||||
final class ViewModel {
|
||||
private var streamTask: Task<Void, Never>?
|
||||
|
||||
func onAppear() {
|
||||
streamTask = Task { [weak self] in
|
||||
await self?.stream()
|
||||
}
|
||||
}
|
||||
|
||||
func onDisappear() {
|
||||
streamTask?.cancel()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.7 Use Structured Concurrency for Parallelism
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: sequential awaits where work could run concurrently
|
||||
let a = await loadA()
|
||||
let b = await loadB() // waits for A to finish first
|
||||
|
||||
// ✅ Correct: async let runs them concurrently
|
||||
async let a = loadA()
|
||||
async let b = loadB()
|
||||
let (resultA, resultB) = await (a, b)
|
||||
|
||||
// ✅ For a dynamic number of children, use a task group
|
||||
try await withThrowingTaskGroup(of: Item.self) { group in
|
||||
for id in ids {
|
||||
group.addTask { try await fetch(id) }
|
||||
}
|
||||
for try await item in group {
|
||||
store(item)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. SwiftUI
|
||||
|
||||
### 6.1 Choose the Right State Wrapper
|
||||
|
||||
```swift
|
||||
// ✅ @State: simple value-type state owned by this view
|
||||
struct Toggle: View {
|
||||
@State private var isOn = false
|
||||
var body: some View { /* ... */ }
|
||||
}
|
||||
|
||||
// ✅ @StateObject: the view CREATES and OWNS a reference-type model
|
||||
struct ProfileScreen: View {
|
||||
@StateObject private var model = ProfileViewModel()
|
||||
var body: some View { /* ... */ }
|
||||
}
|
||||
|
||||
// ✅ @ObservedObject: the model is OWNED elsewhere and passed in
|
||||
struct ProfileHeader: View {
|
||||
@ObservedObject var model: ProfileViewModel
|
||||
var body: some View { /* ... */ }
|
||||
}
|
||||
|
||||
// ✅ @Binding: a two-way reference to state owned by a parent
|
||||
struct SearchField: View {
|
||||
@Binding var text: String
|
||||
var body: some View { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 @StateObject vs @ObservedObject
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: @ObservedObject for an object the view itself creates.
|
||||
// SwiftUI may recreate the view, re-instantiating the model and
|
||||
// losing its state on every re-render.
|
||||
struct CounterView: View {
|
||||
@ObservedObject var model = CounterModel() // recreated unexpectedly
|
||||
}
|
||||
|
||||
// ✅ Correct: @StateObject ties the model's lifetime to the view
|
||||
struct CounterView: View {
|
||||
@StateObject private var model = CounterModel()
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 Preserve View Identity
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: index-based id reuses identity when the array reorders,
|
||||
// causing wrong animations and stale state.
|
||||
ForEach(0..<items.count, id: \.self) { i in
|
||||
ItemRow(item: items[i])
|
||||
}
|
||||
|
||||
// ✅ Correct: use a stable, unique identifier
|
||||
ForEach(items) { item in // Item: Identifiable
|
||||
ItemRow(item: item)
|
||||
}
|
||||
|
||||
// ✅ Use .id(...) to deliberately reset a view's state
|
||||
ProfileView(user: user)
|
||||
.id(user.id) // new identity per user -> fresh state
|
||||
```
|
||||
|
||||
### 6.4 Avoid Over-Rendering
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: a single huge body re-renders everything on any change
|
||||
struct Dashboard: View {
|
||||
@ObservedObject var model: DashboardModel
|
||||
var body: some View {
|
||||
VStack {
|
||||
// header + heavy chart + list all recompute together
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Correct: extract subviews so only the affected part re-renders.
|
||||
// Each child observes only the state it needs.
|
||||
struct Dashboard: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
HeaderView()
|
||||
ChartView()
|
||||
ItemList()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.5 Do Async Work with .task
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: kicking off work in onAppear without cancellation
|
||||
.onAppear {
|
||||
Task { await model.load() } // not cancelled when view disappears
|
||||
}
|
||||
|
||||
// ✅ Correct: .task is tied to the view's lifetime and auto-cancels
|
||||
.task {
|
||||
await model.load()
|
||||
}
|
||||
|
||||
// ✅ Re-run when an input changes
|
||||
.task(id: query) {
|
||||
await model.search(query)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Protocols and Generics
|
||||
|
||||
### 7.1 Protocol-Oriented Design
|
||||
|
||||
```swift
|
||||
// ✅ Compose behavior with protocols and default implementations
|
||||
protocol Identifiable2 {
|
||||
var id: String { get }
|
||||
}
|
||||
|
||||
protocol Describable {
|
||||
var description: String { get }
|
||||
}
|
||||
|
||||
extension Describable {
|
||||
var description: String { "no description" } // default
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 Prefer some Over any
|
||||
|
||||
```swift
|
||||
// ❌ Slower: `any` is an existential box with dynamic dispatch
|
||||
func makeShape() -> any Shape { Circle() }
|
||||
|
||||
// ✅ Faster: `some` is an opaque type resolved at compile time,
|
||||
// preserving the concrete type and enabling static dispatch.
|
||||
func makeShape() -> some Shape { Circle() }
|
||||
|
||||
// Use `any` only when you genuinely need heterogeneous values:
|
||||
let shapes: [any Shape] = [Circle(), Square()]
|
||||
```
|
||||
|
||||
### 7.3 Generic Constraints Over Existentials
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: existential parameter loses the concrete type and is slower
|
||||
func logTotal(_ items: [any Numeric]) {
|
||||
// awkward: the concrete numeric type is erased, so arithmetic needs casts
|
||||
}
|
||||
|
||||
// ✅ Correct: a generic constraint keeps full type information
|
||||
func total<T: Numeric>(_ items: [T]) -> T {
|
||||
items.reduce(.zero, +)
|
||||
}
|
||||
```
|
||||
|
||||
### 7.4 Associated Types with Primary Associated Types
|
||||
|
||||
```swift
|
||||
// ✅ Primary associated types (Swift 5.7+) allow lightweight constraints
|
||||
protocol Container<Item> {
|
||||
associatedtype Item
|
||||
var count: Int { get }
|
||||
subscript(_ index: Int) -> Item { get }
|
||||
}
|
||||
|
||||
// Constrain the element type without a where-clause:
|
||||
func first(in container: some Container<Int>) -> Int {
|
||||
container[0]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Access Control and API Design
|
||||
|
||||
### 8.1 Use the Narrowest Access Level
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: everything public exposes internal details as API surface
|
||||
public class Service {
|
||||
public var cache: [String: Data] = [:]
|
||||
public func reset() {}
|
||||
}
|
||||
|
||||
// ✅ Correct: expose only the intended API; hide the rest
|
||||
public final class Service {
|
||||
private var cache: [String: Data] = [:]
|
||||
public func reset() { cache.removeAll() }
|
||||
}
|
||||
```
|
||||
|
||||
### 8.2 private vs fileprivate vs internal vs public/open
|
||||
|
||||
```swift
|
||||
// private: visible only within the enclosing declaration (and its extensions in the same file)
|
||||
// fileprivate: visible within the same source file
|
||||
// internal: visible within the module (the default)
|
||||
// public: visible outside the module, but not subclassable/overridable
|
||||
// open: visible outside the module AND subclassable/overridable
|
||||
|
||||
// ✅ Use private(set) to expose read-only state
|
||||
public final class Account {
|
||||
public private(set) var balance: Decimal = 0
|
||||
}
|
||||
```
|
||||
|
||||
### 8.3 Follow the Swift API Design Guidelines
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: redundant words, unclear argument roles
|
||||
func insertObject(_ object: Element, atIndex index: Int)
|
||||
list.removeElement(at: 0)
|
||||
|
||||
// ✅ Correct: read at the call site like a phrase; omit needless words
|
||||
func insert(_ element: Element, at index: Int)
|
||||
list.insert(item, at: 0) // reads as "insert item at 0"
|
||||
list.remove(at: 0)
|
||||
|
||||
// ✅ Boolean properties read as assertions
|
||||
var isEmpty: Bool
|
||||
var hasChanges: Bool
|
||||
```
|
||||
|
||||
### 8.4 Name Methods by Side Effects
|
||||
|
||||
```swift
|
||||
// ✅ Mutating verb vs non-mutating noun pairs (the "ed/ing" rule)
|
||||
var sorted = array.sorted() // returns a new value (non-mutating)
|
||||
array.sort() // mutates in place (imperative verb)
|
||||
|
||||
let reversed = text.reversed()
|
||||
text.reverse()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Collections and Functional Style
|
||||
|
||||
### 9.1 Prefer map/filter/compactMap
|
||||
|
||||
```swift
|
||||
// ❌ Verbose: manual loop with mutable accumulator
|
||||
var names: [String] = []
|
||||
for user in users {
|
||||
if user.isActive {
|
||||
names.append(user.name)
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Correct: declarative transform
|
||||
let names = users.filter(\.isActive).map(\.name)
|
||||
```
|
||||
|
||||
### 9.2 compactMap to Drop nils
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: map leaves an [Int?] you then have to unwrap
|
||||
let numbers = strings.map { Int($0) } // [Int?]
|
||||
|
||||
// ✅ Correct: compactMap removes nils and unwraps
|
||||
let numbers = strings.compactMap { Int($0) } // [Int]
|
||||
```
|
||||
|
||||
### 9.3 Avoid O(n^2) Membership Checks
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: contains on an Array is O(n); the loop is O(n*m)
|
||||
let result = candidates.filter { blocked.contains($0) } // blocked: [ID]
|
||||
|
||||
// ✅ Correct: a Set makes membership O(1)
|
||||
let blockedSet = Set(blocked)
|
||||
let result = candidates.filter { blockedSet.contains($0) }
|
||||
```
|
||||
|
||||
### 9.4 reduce and Dictionary Grouping
|
||||
|
||||
```swift
|
||||
// ✅ Group with Dictionary(grouping:)
|
||||
let byFirstLetter = Dictionary(grouping: words) { $0.first }
|
||||
|
||||
// ❌ Wrong: reduce(into:) is preferred over reduce that copies each step
|
||||
let total = numbers.reduce(0) { $0 + $1 } // fine for scalars
|
||||
|
||||
// ✅ Use reduce(into:) when accumulating into a collection (avoids copies)
|
||||
let counts = words.reduce(into: [:]) { acc, word in
|
||||
acc[word, default: 0] += 1
|
||||
}
|
||||
```
|
||||
|
||||
### 9.5 Use lazy for Chained Transforms on Large Sequences
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: each step allocates an intermediate array
|
||||
let firstMatch = bigArray.map(expensive).filter(isValid).first
|
||||
|
||||
// ✅ Correct: lazy avoids intermediate arrays and stops early
|
||||
let firstMatch = bigArray.lazy.map(expensive).filter(isValid).first
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Testing
|
||||
|
||||
### 10.1 Arrange-Act-Assert with XCTest
|
||||
|
||||
```swift
|
||||
import XCTest
|
||||
@testable import MyApp
|
||||
|
||||
final class PriceCalculatorTests: XCTestCase {
|
||||
func testDiscountApplied() {
|
||||
// Arrange
|
||||
let calculator = PriceCalculator(discount: 0.1)
|
||||
// Act
|
||||
let total = calculator.total(for: 100)
|
||||
// Assert
|
||||
XCTAssertEqual(total, 90, accuracy: 0.001)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 10.2 Testing async Code
|
||||
|
||||
```swift
|
||||
// ✅ Mark the test method async and await directly
|
||||
func testFetchUser() async throws {
|
||||
let service = UserService(client: MockClient())
|
||||
let user = try await service.fetchUser(id: "42")
|
||||
XCTAssertEqual(user.id, "42")
|
||||
}
|
||||
|
||||
// ✅ Assert that an async call throws the expected error
|
||||
func testFetchUserUnauthorized() async {
|
||||
let service = UserService(client: UnauthorizedClient())
|
||||
do {
|
||||
_ = try await service.fetchUser(id: "42")
|
||||
XCTFail("expected to throw")
|
||||
} catch NetworkError.unauthorized {
|
||||
// expected
|
||||
} catch {
|
||||
XCTFail("unexpected error: \(error)")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 10.3 Inject Dependencies via Protocols
|
||||
|
||||
```swift
|
||||
// ✅ Depend on a protocol so tests can substitute a mock
|
||||
protocol HTTPClient {
|
||||
func get(_ url: URL) async throws -> Data
|
||||
}
|
||||
|
||||
struct MockClient: HTTPClient {
|
||||
var result: Result<Data, Error>
|
||||
func get(_ url: URL) async throws -> Data {
|
||||
try result.get()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 10.4 Avoid Sleeps; Await Expectations or Values
|
||||
|
||||
```swift
|
||||
// ❌ Wrong: arbitrary sleep makes tests slow and flaky
|
||||
func testCallback() {
|
||||
var done = false
|
||||
object.run { done = true }
|
||||
Thread.sleep(forTimeInterval: 1)
|
||||
XCTAssertTrue(done)
|
||||
}
|
||||
|
||||
// ✅ Correct: use XCTestExpectation for callback APIs
|
||||
func testCallback() {
|
||||
let expectation = expectation(description: "callback fired")
|
||||
object.run { expectation.fulfill() }
|
||||
wait(for: [expectation], timeout: 1.0)
|
||||
}
|
||||
|
||||
// ✅ Better: refactor to async and await the value directly
|
||||
func testCallback() async {
|
||||
let value = await object.run()
|
||||
XCTAssertEqual(value, expected)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [Swift API Design Guidelines](https://www.swift.org/documentation/api-design-guidelines/)
|
||||
- [The Swift Programming Language](https://docs.swift.org/swift-book/)
|
||||
- [Swift Concurrency (TSPL)](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/)
|
||||
- [Migrating to Swift 6](https://www.swift.org/migration/documentation/migrationguide/)
|
||||
- [Apple: Managing Model Data in Your App (SwiftUI)](https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app)
|
||||
- [Apple: Automatic Reference Counting](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/automaticreferencecounting/)
|
||||
- [WWDC: Protocol-Oriented Programming in Swift](https://developer.apple.com/videos/play/wwdc2015/408/)
|
||||
- [Swift Evolution](https://github.com/apple/swift-evolution)
|
||||
+553
@@ -0,0 +1,553 @@
|
||||
# TypeScript/JavaScript Code Review Guide
|
||||
|
||||
> TypeScript 代码审查指南,覆盖类型系统、泛型、条件类型、strict 模式、async/await 模式等核心主题。
|
||||
|
||||
## 目录
|
||||
|
||||
- [类型安全基础](#类型安全基础)
|
||||
- [泛型模式](#泛型模式)
|
||||
- [高级类型](#高级类型)
|
||||
- [Strict 模式配置](#strict-模式配置)
|
||||
- [异步处理](#异步处理)
|
||||
- [不可变性](#不可变性)
|
||||
- [ESLint 规则](#eslint-规则)
|
||||
- [Review Checklist](#review-checklist)
|
||||
|
||||
---
|
||||
|
||||
## 类型安全基础
|
||||
|
||||
### 避免使用 any
|
||||
|
||||
```typescript
|
||||
// ❌ Using any defeats type safety
|
||||
function processData(data: any) {
|
||||
return data.value; // 无类型检查,运行时可能崩溃
|
||||
}
|
||||
|
||||
// ✅ Use proper types
|
||||
interface DataPayload {
|
||||
value: string;
|
||||
}
|
||||
function processData(data: DataPayload) {
|
||||
return data.value;
|
||||
}
|
||||
|
||||
// ✅ 未知类型用 unknown + 类型守卫
|
||||
function processUnknown(data: unknown) {
|
||||
if (typeof data === 'object' && data !== null && 'value' in data) {
|
||||
return (data as { value: string }).value;
|
||||
}
|
||||
throw new Error('Invalid data');
|
||||
}
|
||||
```
|
||||
|
||||
### 类型收窄
|
||||
|
||||
```typescript
|
||||
// ❌ 不安全的类型断言
|
||||
function getLength(value: string | string[]) {
|
||||
return (value as string[]).length; // 如果是 string 会出错
|
||||
}
|
||||
|
||||
// ✅ 使用类型守卫
|
||||
function getLength(value: string | string[]): number {
|
||||
if (Array.isArray(value)) {
|
||||
return value.length;
|
||||
}
|
||||
return value.length;
|
||||
}
|
||||
|
||||
// ✅ 使用 in 操作符
|
||||
interface Dog { bark(): void }
|
||||
interface Cat { meow(): void }
|
||||
|
||||
function speak(animal: Dog | Cat) {
|
||||
if ('bark' in animal) {
|
||||
animal.bark();
|
||||
} else {
|
||||
animal.meow();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 字面量类型与 as const
|
||||
|
||||
```typescript
|
||||
// ❌ 类型过于宽泛
|
||||
const config = {
|
||||
endpoint: '/api',
|
||||
method: 'GET' // 类型是 string
|
||||
};
|
||||
|
||||
// ✅ 使用 as const 获得字面量类型
|
||||
const config = {
|
||||
endpoint: '/api',
|
||||
method: 'GET'
|
||||
} as const; // method 类型是 'GET'
|
||||
|
||||
// ✅ 用于函数参数
|
||||
function request(method: 'GET' | 'POST', url: string) { ... }
|
||||
request(config.method, config.endpoint); // 正确!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 泛型模式
|
||||
|
||||
### 基础泛型
|
||||
|
||||
```typescript
|
||||
// ❌ 重复代码
|
||||
function getFirstString(arr: string[]): string | undefined {
|
||||
return arr[0];
|
||||
}
|
||||
function getFirstNumber(arr: number[]): number | undefined {
|
||||
return arr[0];
|
||||
}
|
||||
|
||||
// ✅ 使用泛型
|
||||
function getFirst<T>(arr: T[]): T | undefined {
|
||||
return arr[0];
|
||||
}
|
||||
```
|
||||
|
||||
### 泛型约束
|
||||
|
||||
```typescript
|
||||
// ❌ 泛型没有约束,无法访问属性
|
||||
function getProperty<T>(obj: T, key: string) {
|
||||
return obj[key]; // Error: 无法索引
|
||||
}
|
||||
|
||||
// ✅ 使用 keyof 约束
|
||||
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
|
||||
return obj[key];
|
||||
}
|
||||
|
||||
const user = { name: 'Alice', age: 30 };
|
||||
getProperty(user, 'name'); // 返回类型是 string
|
||||
getProperty(user, 'age'); // 返回类型是 number
|
||||
getProperty(user, 'foo'); // Error: 'foo' 不在 keyof User
|
||||
```
|
||||
|
||||
### 泛型默认值
|
||||
|
||||
```typescript
|
||||
// ✅ 提供合理的默认类型
|
||||
interface ApiResponse<T = unknown> {
|
||||
data: T;
|
||||
status: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
// 可以不指定泛型参数
|
||||
const response: ApiResponse = { data: null, status: 200, message: 'OK' };
|
||||
// 也可以指定
|
||||
const userResponse: ApiResponse<User> = { ... };
|
||||
```
|
||||
|
||||
### 常见泛型工具类型
|
||||
|
||||
```typescript
|
||||
// ✅ 善用内置工具类型
|
||||
interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
type PartialUser = Partial<User>; // 所有属性可选
|
||||
type RequiredUser = Required<User>; // 所有属性必需
|
||||
type ReadonlyUser = Readonly<User>; // 所有属性只读
|
||||
type UserKeys = keyof User; // 'id' | 'name' | 'email'
|
||||
type NameOnly = Pick<User, 'name'>; // { name: string }
|
||||
type WithoutId = Omit<User, 'id'>; // { name: string; email: string }
|
||||
type UserRecord = Record<string, User>; // { [key: string]: User }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 高级类型
|
||||
|
||||
### 条件类型
|
||||
|
||||
```typescript
|
||||
// ✅ 根据输入类型返回不同类型
|
||||
type IsString<T> = T extends string ? true : false;
|
||||
|
||||
type A = IsString<string>; // true
|
||||
type B = IsString<number>; // false
|
||||
|
||||
// ✅ 提取数组元素类型
|
||||
type ElementType<T> = T extends (infer U)[] ? U : never;
|
||||
|
||||
type Elem = ElementType<string[]>; // string
|
||||
|
||||
// ✅ 提取函数返回类型(内置 ReturnType)
|
||||
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
|
||||
```
|
||||
|
||||
### 映射类型
|
||||
|
||||
```typescript
|
||||
// ✅ 转换对象类型的所有属性
|
||||
type Nullable<T> = {
|
||||
[K in keyof T]: T[K] | null;
|
||||
};
|
||||
|
||||
interface User {
|
||||
name: string;
|
||||
age: number;
|
||||
}
|
||||
|
||||
type NullableUser = Nullable<User>;
|
||||
// { name: string | null; age: number | null }
|
||||
|
||||
// ✅ 添加前缀
|
||||
type Getters<T> = {
|
||||
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
|
||||
};
|
||||
|
||||
type UserGetters = Getters<User>;
|
||||
// { getName: () => string; getAge: () => number }
|
||||
```
|
||||
|
||||
### 模板字面量类型
|
||||
|
||||
```typescript
|
||||
// ✅ 类型安全的事件名称
|
||||
type EventName = 'click' | 'focus' | 'blur';
|
||||
type HandlerName = `on${Capitalize<EventName>}`;
|
||||
// 'onClick' | 'onFocus' | 'onBlur'
|
||||
|
||||
// ✅ API 路由类型
|
||||
type ApiRoute = `/api/${string}`;
|
||||
const route: ApiRoute = '/api/users'; // OK
|
||||
const badRoute: ApiRoute = '/users'; // Error
|
||||
```
|
||||
|
||||
### Discriminated Unions
|
||||
|
||||
```typescript
|
||||
// ✅ 使用判别属性实现类型安全
|
||||
type Result<T, E> =
|
||||
| { success: true; data: T }
|
||||
| { success: false; error: E };
|
||||
|
||||
function handleResult(result: Result<User, Error>) {
|
||||
if (result.success) {
|
||||
console.log(result.data.name); // TypeScript 知道 data 存在
|
||||
} else {
|
||||
console.log(result.error.message); // TypeScript 知道 error 存在
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Redux Action 模式
|
||||
type Action =
|
||||
| { type: 'INCREMENT'; payload: number }
|
||||
| { type: 'DECREMENT'; payload: number }
|
||||
| { type: 'RESET' };
|
||||
|
||||
function reducer(state: number, action: Action): number {
|
||||
switch (action.type) {
|
||||
case 'INCREMENT':
|
||||
return state + action.payload; // payload 类型已知
|
||||
case 'DECREMENT':
|
||||
return state - action.payload;
|
||||
case 'RESET':
|
||||
return 0; // 这里没有 payload
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Strict 模式配置
|
||||
|
||||
### 推荐的 tsconfig.json
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
// ✅ 必须开启的 strict 选项
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictBindCallApply": true,
|
||||
"strictPropertyInitialization": true,
|
||||
"noImplicitThis": true,
|
||||
"useUnknownInCatchVariables": true,
|
||||
|
||||
// ✅ 额外推荐选项
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"noPropertyAccessFromIndexSignature": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### noUncheckedIndexedAccess 的影响
|
||||
|
||||
```typescript
|
||||
// tsconfig: "noUncheckedIndexedAccess": true
|
||||
|
||||
const arr = [1, 2, 3];
|
||||
const first = arr[0]; // 类型是 number | undefined
|
||||
|
||||
// ❌ 直接使用可能出错
|
||||
console.log(first.toFixed(2)); // Error: 可能是 undefined
|
||||
|
||||
// ✅ 先检查
|
||||
if (first !== undefined) {
|
||||
console.log(first.toFixed(2));
|
||||
}
|
||||
|
||||
// ✅ 或使用非空断言(确定时)
|
||||
console.log(arr[0]!.toFixed(2));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 异步处理
|
||||
|
||||
### Promise 错误处理
|
||||
|
||||
```typescript
|
||||
// ❌ Not handling async errors
|
||||
async function fetchUser(id: string) {
|
||||
const response = await fetch(`/api/users/${id}`);
|
||||
return response.json(); // 网络错误未处理
|
||||
}
|
||||
|
||||
// ✅ Handle errors properly
|
||||
async function fetchUser(id: string): Promise<User> {
|
||||
try {
|
||||
const response = await fetch(`/api/users/${id}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new Error(`Failed to fetch user: ${error.message}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Promise.all vs Promise.allSettled
|
||||
|
||||
```typescript
|
||||
// ❌ Promise.all 一个失败全部失败
|
||||
async function fetchAllUsers(ids: string[]) {
|
||||
const users = await Promise.all(ids.map(fetchUser));
|
||||
return users; // 一个失败就全部失败
|
||||
}
|
||||
|
||||
// ✅ Promise.allSettled 获取所有结果
|
||||
async function fetchAllUsers(ids: string[]) {
|
||||
const results = await Promise.allSettled(ids.map(fetchUser));
|
||||
|
||||
const users: User[] = [];
|
||||
const errors: Error[] = [];
|
||||
|
||||
for (const result of results) {
|
||||
if (result.status === 'fulfilled') {
|
||||
users.push(result.value);
|
||||
} else {
|
||||
errors.push(result.reason);
|
||||
}
|
||||
}
|
||||
|
||||
return { users, errors };
|
||||
}
|
||||
```
|
||||
|
||||
### 竞态条件处理
|
||||
|
||||
```typescript
|
||||
// ❌ 竞态条件:旧请求可能覆盖新请求
|
||||
function useSearch() {
|
||||
const [query, setQuery] = useState('');
|
||||
const [results, setResults] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`/api/search?q=${query}`)
|
||||
.then(r => r.json())
|
||||
.then(setResults); // 旧请求可能后返回!
|
||||
}, [query]);
|
||||
}
|
||||
|
||||
// ✅ 使用 AbortController
|
||||
function useSearch() {
|
||||
const [query, setQuery] = useState('');
|
||||
const [results, setResults] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
|
||||
fetch(`/api/search?q=${query}`, { signal: controller.signal })
|
||||
.then(r => r.json())
|
||||
.then(setResults)
|
||||
.catch(e => {
|
||||
if (e.name !== 'AbortError') throw e;
|
||||
});
|
||||
|
||||
return () => controller.abort();
|
||||
}, [query]);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 不可变性
|
||||
|
||||
### Readonly 与 ReadonlyArray
|
||||
|
||||
```typescript
|
||||
// ❌ 可变参数可能被意外修改
|
||||
function processUsers(users: User[]) {
|
||||
users.sort((a, b) => a.name.localeCompare(b.name)); // 修改了原数组!
|
||||
return users;
|
||||
}
|
||||
|
||||
// ✅ 使用 readonly 防止修改
|
||||
function processUsers(users: readonly User[]): User[] {
|
||||
return [...users].sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
// ✅ 深度只读
|
||||
type DeepReadonly<T> = {
|
||||
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
|
||||
};
|
||||
```
|
||||
|
||||
### 不变式函数参数
|
||||
|
||||
```typescript
|
||||
// ✅ 使用 as const 和 readonly 保护数据
|
||||
function createConfig<T extends readonly string[]>(routes: T) {
|
||||
return routes;
|
||||
}
|
||||
|
||||
const routes = createConfig(['home', 'about', 'contact'] as const);
|
||||
// 类型是 readonly ['home', 'about', 'contact']
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ESLint 规则
|
||||
|
||||
### 推荐的 @typescript-eslint 规则
|
||||
|
||||
```javascript
|
||||
// eslint.config.js(flat config,typescript-eslint v8)
|
||||
import eslint from '@eslint/js';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default tseslint.config(
|
||||
eslint.configs.recommended,
|
||||
// 需要类型信息的规则集,对应旧的 recommended-requiring-type-checking
|
||||
tseslint.configs.recommendedTypeChecked,
|
||||
tseslint.configs.strictTypeChecked,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
// 让带类型的规则自动找到对应 tsconfig
|
||||
projectService: true,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
// ✅ 类型安全
|
||||
'@typescript-eslint/no-explicit-any': 'error',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'error',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'error',
|
||||
'@typescript-eslint/no-unsafe-call': 'error',
|
||||
'@typescript-eslint/no-unsafe-return': 'error',
|
||||
|
||||
// ✅ 最佳实践
|
||||
'@typescript-eslint/explicit-function-return-type': 'warn',
|
||||
'@typescript-eslint/no-floating-promises': 'error',
|
||||
'@typescript-eslint/await-thenable': 'error',
|
||||
'@typescript-eslint/no-misused-promises': 'error',
|
||||
|
||||
// ✅ 代码风格
|
||||
'@typescript-eslint/consistent-type-imports': 'error',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'error',
|
||||
'@typescript-eslint/prefer-optional-chain': 'error',
|
||||
},
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
### 常见 ESLint 错误修复
|
||||
|
||||
```typescript
|
||||
// ❌ no-floating-promises: Promise 必须被处理
|
||||
async function save() { ... }
|
||||
save(); // Error: 未处理的 Promise
|
||||
|
||||
// ✅ 显式处理
|
||||
await save();
|
||||
// 或
|
||||
save().catch(console.error);
|
||||
// 或明确忽略
|
||||
void save();
|
||||
|
||||
// ❌ no-misused-promises: 不能在非 async 位置使用 Promise
|
||||
const items = [1, 2, 3];
|
||||
items.forEach(async (item) => { // Error!
|
||||
await processItem(item);
|
||||
});
|
||||
|
||||
// ✅ 使用 for...of
|
||||
for (const item of items) {
|
||||
await processItem(item);
|
||||
}
|
||||
// 或 Promise.all
|
||||
await Promise.all(items.map(processItem));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Review Checklist
|
||||
|
||||
### 类型系统
|
||||
- [ ] 没有使用 `any`(使用 `unknown` + 类型守卫代替)
|
||||
- [ ] 接口和类型定义完整且有意义的命名
|
||||
- [ ] 使用泛型提高代码复用性
|
||||
- [ ] 联合类型有正确的类型收窄
|
||||
- [ ] 善用工具类型(Partial、Pick、Omit 等)
|
||||
|
||||
### 泛型
|
||||
- [ ] 泛型有适当的约束(extends)
|
||||
- [ ] 泛型参数有合理的默认值
|
||||
- [ ] 避免过度泛型化(KISS 原则)
|
||||
|
||||
### Strict 模式
|
||||
- [ ] tsconfig.json 启用了 strict: true
|
||||
- [ ] 启用了 noUncheckedIndexedAccess
|
||||
- [ ] 没有使用 @ts-ignore(改用 @ts-expect-error)
|
||||
|
||||
### 异步代码
|
||||
- [ ] async 函数有错误处理
|
||||
- [ ] Promise rejection 被正确处理
|
||||
- [ ] 没有 floating promises(未处理的 Promise)
|
||||
- [ ] 并发请求使用 Promise.all 或 Promise.allSettled
|
||||
- [ ] 竞态条件使用 AbortController 处理
|
||||
|
||||
### 不可变性
|
||||
- [ ] 不直接修改函数参数
|
||||
- [ ] 使用 spread 操作符创建新对象/数组
|
||||
- [ ] 考虑使用 readonly 修饰符
|
||||
|
||||
### ESLint
|
||||
- [ ] 使用 @typescript-eslint/recommended
|
||||
- [ ] 没有 ESLint 警告或错误
|
||||
- [ ] 使用 consistent-type-imports
|
||||
+924
@@ -0,0 +1,924 @@
|
||||
# Vue 3 Code Review Guide
|
||||
|
||||
> Vue 3 Composition API 代码审查指南,覆盖响应性系统、Props/Emits、Watchers、Composables、Vue 3.5 新特性等核心主题。
|
||||
|
||||
## 目录
|
||||
|
||||
- [响应性系统](#响应性系统)
|
||||
- [Props & Emits](#props--emits)
|
||||
- [Vue 3.5 新特性](#vue-35-新特性)
|
||||
- [Watchers](#watchers)
|
||||
- [模板最佳实践](#模板最佳实践)
|
||||
- [Composables](#composables)
|
||||
- [性能优化](#性能优化)
|
||||
- [Review Checklist](#review-checklist)
|
||||
|
||||
---
|
||||
|
||||
## 响应性系统
|
||||
|
||||
### ref vs reactive 选择
|
||||
|
||||
```vue
|
||||
<!-- ✅ 基本类型用 ref -->
|
||||
<script setup lang="ts">
|
||||
const count = ref(0)
|
||||
const name = ref('Vue')
|
||||
|
||||
// ref 需要 .value 访问
|
||||
count.value++
|
||||
</script>
|
||||
|
||||
<!-- ✅ 对象/数组用 reactive(可选)-->
|
||||
<script setup lang="ts">
|
||||
const state = reactive({
|
||||
user: null,
|
||||
loading: false,
|
||||
error: null
|
||||
})
|
||||
|
||||
// reactive 直接访问
|
||||
state.loading = true
|
||||
</script>
|
||||
|
||||
<!-- 💡 现代最佳实践:全部使用 ref,保持一致性 -->
|
||||
<script setup lang="ts">
|
||||
const user = ref<User | null>(null)
|
||||
const loading = ref(false)
|
||||
const error = ref<Error | null>(null)
|
||||
</script>
|
||||
```
|
||||
|
||||
### 解构 reactive 对象
|
||||
|
||||
```vue
|
||||
<!-- ❌ 解构 reactive 会丢失响应性 -->
|
||||
<script setup lang="ts">
|
||||
const state = reactive({ count: 0, name: 'Vue' })
|
||||
const { count, name } = state // 丢失响应性!
|
||||
</script>
|
||||
|
||||
<!-- ✅ 使用 toRefs 保持响应性 -->
|
||||
<script setup lang="ts">
|
||||
const state = reactive({ count: 0, name: 'Vue' })
|
||||
const { count, name } = toRefs(state) // 保持响应性
|
||||
// 或者直接使用 ref
|
||||
const count = ref(0)
|
||||
const name = ref('Vue')
|
||||
</script>
|
||||
```
|
||||
|
||||
### computed 副作用
|
||||
|
||||
```vue
|
||||
<!-- ❌ computed 中产生副作用 -->
|
||||
<script setup lang="ts">
|
||||
const fullName = computed(() => {
|
||||
console.log('Computing...') // 副作用!
|
||||
otherRef.value = 'changed' // 修改其他状态!
|
||||
return `${firstName.value} ${lastName.value}`
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- ✅ computed 只用于派生状态 -->
|
||||
<script setup lang="ts">
|
||||
const fullName = computed(() => {
|
||||
return `${firstName.value} ${lastName.value}`
|
||||
})
|
||||
// 副作用放在 watch 或事件处理中
|
||||
watch(fullName, (name) => {
|
||||
console.log('Name changed:', name)
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### shallowRef 优化
|
||||
|
||||
```vue
|
||||
<!-- ❌ 大型对象使用 ref 会深度转换 -->
|
||||
<script setup lang="ts">
|
||||
const largeData = ref(hugeNestedObject) // 深度响应式,性能开销大
|
||||
</script>
|
||||
|
||||
<!-- ✅ 使用 shallowRef 避免深度转换 -->
|
||||
<script setup lang="ts">
|
||||
const largeData = shallowRef(hugeNestedObject)
|
||||
|
||||
// 整体替换才会触发更新
|
||||
function updateData(newData) {
|
||||
largeData.value = newData // ✅ 触发更新
|
||||
}
|
||||
|
||||
// ❌ 修改嵌套属性不会触发更新
|
||||
// largeData.value.nested.prop = 'new'
|
||||
|
||||
// 需要手动触发时使用 triggerRef
|
||||
import { triggerRef } from 'vue'
|
||||
largeData.value.nested.prop = 'new'
|
||||
triggerRef(largeData)
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Props & Emits
|
||||
|
||||
### 直接修改 props
|
||||
|
||||
```vue
|
||||
<!-- ❌ 直接修改 props -->
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ user: User }>()
|
||||
props.user.name = 'New Name' // 永远不要直接修改 props!
|
||||
</script>
|
||||
|
||||
<!-- ✅ 使用 emit 通知父组件更新 -->
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ user: User }>()
|
||||
const emit = defineEmits<{
|
||||
update: [name: string]
|
||||
}>()
|
||||
const updateName = (name: string) => emit('update', name)
|
||||
</script>
|
||||
```
|
||||
|
||||
### defineProps 类型声明
|
||||
|
||||
```vue
|
||||
<!-- ❌ defineProps 缺少类型声明 -->
|
||||
<script setup lang="ts">
|
||||
const props = defineProps(['title', 'count']) // 无类型检查
|
||||
</script>
|
||||
|
||||
<!-- ✅ 使用类型声明 + withDefaults -->
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
title: string
|
||||
count?: number
|
||||
items?: string[]
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
count: 0,
|
||||
items: () => [] // 对象/数组默认值需要工厂函数
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### defineEmits 类型安全
|
||||
|
||||
```vue
|
||||
<!-- ❌ defineEmits 缺少类型 -->
|
||||
<script setup lang="ts">
|
||||
const emit = defineEmits(['update', 'delete']) // 无类型检查
|
||||
emit('update', someValue) // 参数类型不安全
|
||||
</script>
|
||||
|
||||
<!-- ✅ 完整的类型定义 -->
|
||||
<script setup lang="ts">
|
||||
const emit = defineEmits<{
|
||||
update: [id: number, value: string]
|
||||
delete: [id: number]
|
||||
'custom-event': [payload: CustomPayload]
|
||||
}>()
|
||||
|
||||
// 现在有完整的类型检查
|
||||
emit('update', 1, 'new value') // ✅
|
||||
emit('update', 'wrong') // ❌ TypeScript 报错
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Vue 3.5 新特性
|
||||
|
||||
### Reactive Props Destructure (3.5+)
|
||||
|
||||
```vue
|
||||
<!-- Vue 3.5 之前:解构会丢失响应性 -->
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ count: number }>()
|
||||
// 需要使用 props.count 或 toRefs
|
||||
</script>
|
||||
|
||||
<!-- ✅ Vue 3.5+:解构保持响应性 -->
|
||||
<script setup lang="ts">
|
||||
const { count, name = 'default' } = defineProps<{
|
||||
count: number
|
||||
name?: string
|
||||
}>()
|
||||
|
||||
// count 和 name 自动保持响应性!
|
||||
// 可以直接在模板和 watch 中使用
|
||||
watch(() => count, (newCount) => {
|
||||
console.log('Count changed:', newCount)
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- ✅ 配合默认值使用 -->
|
||||
<script setup lang="ts">
|
||||
const {
|
||||
title,
|
||||
count = 0,
|
||||
items = () => [] // 函数作为默认值(对象/数组)
|
||||
} = defineProps<{
|
||||
title: string
|
||||
count?: number
|
||||
items?: () => string[]
|
||||
}>()
|
||||
</script>
|
||||
```
|
||||
|
||||
### defineModel (3.4+)
|
||||
|
||||
```vue
|
||||
<!-- ❌ 传统 v-model 实现:冗长 -->
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ modelValue: string }>()
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
|
||||
|
||||
// 需要 computed 来双向绑定
|
||||
const value = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- ✅ defineModel:简洁的 v-model 实现 -->
|
||||
<script setup lang="ts">
|
||||
// 自动处理 props 和 emit
|
||||
const model = defineModel<string>()
|
||||
|
||||
// 直接使用
|
||||
model.value = 'new value' // 自动 emit
|
||||
</script>
|
||||
<template>
|
||||
<input v-model="model" />
|
||||
</template>
|
||||
|
||||
<!-- ✅ 命名 v-model -->
|
||||
<script setup lang="ts">
|
||||
// v-model:title 的实现
|
||||
const title = defineModel<string>('title')
|
||||
|
||||
// 带默认值和选项
|
||||
const count = defineModel<number>('count', {
|
||||
default: 0,
|
||||
required: false
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- ✅ 多个 v-model -->
|
||||
<script setup lang="ts">
|
||||
const firstName = defineModel<string>('firstName')
|
||||
const lastName = defineModel<string>('lastName')
|
||||
</script>
|
||||
<template>
|
||||
<!-- 父组件使用:<MyInput v-model:first-name="first" v-model:last-name="last" /> -->
|
||||
</template>
|
||||
|
||||
<!-- ✅ v-model 修饰符 -->
|
||||
<script setup lang="ts">
|
||||
const [model, modifiers] = defineModel<string>()
|
||||
|
||||
// 检查修饰符
|
||||
if (modifiers.capitalize) {
|
||||
// 处理 .capitalize 修饰符
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### useTemplateRef (3.5+)
|
||||
|
||||
```vue
|
||||
<!-- 传统方式:ref 属性与变量同名 -->
|
||||
<script setup lang="ts">
|
||||
const inputRef = ref<HTMLInputElement | null>(null)
|
||||
</script>
|
||||
<template>
|
||||
<input ref="inputRef" />
|
||||
</template>
|
||||
|
||||
<!-- ✅ useTemplateRef:更清晰的模板引用 -->
|
||||
<script setup lang="ts">
|
||||
import { useTemplateRef } from 'vue'
|
||||
|
||||
const input = useTemplateRef<HTMLInputElement>('my-input')
|
||||
|
||||
onMounted(() => {
|
||||
input.value?.focus()
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<input ref="my-input" />
|
||||
</template>
|
||||
|
||||
<!-- ✅ 动态 ref -->
|
||||
<script setup lang="ts">
|
||||
const refKey = ref('input-a')
|
||||
const dynamicInput = useTemplateRef<HTMLInputElement>(refKey)
|
||||
</script>
|
||||
```
|
||||
|
||||
### useId (3.5+)
|
||||
|
||||
```vue
|
||||
<!-- ❌ 手动生成 ID 可能冲突 -->
|
||||
<script setup lang="ts">
|
||||
const id = `input-${Math.random()}` // SSR 不一致!
|
||||
</script>
|
||||
|
||||
<!-- ✅ useId:SSR 安全的唯一 ID -->
|
||||
<script setup lang="ts">
|
||||
import { useId } from 'vue'
|
||||
|
||||
const id = useId() // 例如:'v-0'
|
||||
</script>
|
||||
<template>
|
||||
<label :for="id">Name</label>
|
||||
<input :id="id" />
|
||||
</template>
|
||||
|
||||
<!-- ✅ 表单组件中使用 -->
|
||||
<script setup lang="ts">
|
||||
const inputId = useId()
|
||||
const errorId = useId()
|
||||
</script>
|
||||
<template>
|
||||
<label :for="inputId">Email</label>
|
||||
<input
|
||||
:id="inputId"
|
||||
:aria-describedby="errorId"
|
||||
/>
|
||||
<span :id="errorId" class="error">{{ error }}</span>
|
||||
</template>
|
||||
```
|
||||
|
||||
### onWatcherCleanup (3.5+)
|
||||
|
||||
```vue
|
||||
<!-- 传统方式:watch 第三个参数 -->
|
||||
<script setup lang="ts">
|
||||
watch(source, async (value, oldValue, onCleanup) => {
|
||||
const controller = new AbortController()
|
||||
onCleanup(() => controller.abort())
|
||||
// ...
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- ✅ onWatcherCleanup:更灵活的清理 -->
|
||||
<script setup lang="ts">
|
||||
import { onWatcherCleanup } from 'vue'
|
||||
|
||||
watch(source, async (value) => {
|
||||
const controller = new AbortController()
|
||||
onWatcherCleanup(() => controller.abort())
|
||||
|
||||
// 可以在任意位置调用,不限于回调开头
|
||||
if (someCondition) {
|
||||
const anotherResource = createResource()
|
||||
onWatcherCleanup(() => anotherResource.dispose())
|
||||
}
|
||||
|
||||
await fetchData(value, controller.signal)
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### Deferred Teleport (3.5+)
|
||||
|
||||
```vue
|
||||
<!-- ❌ Teleport 目标必须在挂载时存在 -->
|
||||
<template>
|
||||
<Teleport to="#modal-container">
|
||||
<!-- 如果 #modal-container 不存在会报错 -->
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<!-- ✅ defer 属性延迟挂载 -->
|
||||
<template>
|
||||
<Teleport to="#modal-container" defer>
|
||||
<!-- 等待目标元素存在后再挂载 -->
|
||||
<Modal />
|
||||
</Teleport>
|
||||
</template>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Watchers
|
||||
|
||||
### watch vs watchEffect
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// ✅ watch:明确指定依赖,惰性执行
|
||||
watch(
|
||||
() => props.userId,
|
||||
async (userId) => {
|
||||
user.value = await fetchUser(userId)
|
||||
}
|
||||
)
|
||||
|
||||
// ✅ watchEffect:自动收集依赖,立即执行
|
||||
watchEffect(async () => {
|
||||
// 自动追踪 props.userId
|
||||
user.value = await fetchUser(props.userId)
|
||||
})
|
||||
|
||||
// 💡 选择指南:
|
||||
// - 需要旧值?用 watch
|
||||
// - 需要惰性执行?用 watch
|
||||
// - 依赖复杂?用 watchEffect
|
||||
</script>
|
||||
```
|
||||
|
||||
### watch 清理函数
|
||||
|
||||
```vue
|
||||
<!-- ❌ watch 缺少清理函数,可能内存泄漏 -->
|
||||
<script setup lang="ts">
|
||||
watch(searchQuery, async (query) => {
|
||||
const controller = new AbortController()
|
||||
const data = await fetch(`/api/search?q=${query}`, {
|
||||
signal: controller.signal
|
||||
})
|
||||
results.value = await data.json()
|
||||
// 如果 query 快速变化,旧请求不会被取消!
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- ✅ 使用 onCleanup 清理副作用 -->
|
||||
<script setup lang="ts">
|
||||
watch(searchQuery, async (query, _, onCleanup) => {
|
||||
const controller = new AbortController()
|
||||
onCleanup(() => controller.abort()) // 取消旧请求
|
||||
|
||||
try {
|
||||
const data = await fetch(`/api/search?q=${query}`, {
|
||||
signal: controller.signal
|
||||
})
|
||||
results.value = await data.json()
|
||||
} catch (e) {
|
||||
if (e.name !== 'AbortError') throw e
|
||||
}
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### watch 选项
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// ✅ immediate:立即执行一次
|
||||
watch(
|
||||
userId,
|
||||
async (id) => {
|
||||
user.value = await fetchUser(id)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// ✅ deep:深度监听(性能开销大,谨慎使用)
|
||||
watch(
|
||||
state,
|
||||
(newState) => {
|
||||
console.log('State changed deeply')
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
// ✅ flush: 'post':DOM 更新后执行
|
||||
watch(
|
||||
source,
|
||||
() => {
|
||||
// 可以安全访问更新后的 DOM
|
||||
// nextTick 不再需要
|
||||
},
|
||||
{ flush: 'post' }
|
||||
)
|
||||
|
||||
// ✅ once: true (Vue 3.4+):只执行一次
|
||||
watch(
|
||||
source,
|
||||
(value) => {
|
||||
console.log('只会执行一次:', value)
|
||||
},
|
||||
{ once: true }
|
||||
)
|
||||
</script>
|
||||
```
|
||||
|
||||
### 监听多个源
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// ✅ 监听多个 ref
|
||||
watch(
|
||||
[firstName, lastName],
|
||||
([newFirst, newLast], [oldFirst, oldLast]) => {
|
||||
console.log(`Name changed from ${oldFirst} ${oldLast} to ${newFirst} ${newLast}`)
|
||||
}
|
||||
)
|
||||
|
||||
// ✅ 监听 reactive 对象的特定属性
|
||||
watch(
|
||||
() => [state.count, state.name],
|
||||
([count, name]) => {
|
||||
console.log(`count: ${count}, name: ${name}`)
|
||||
}
|
||||
)
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 模板最佳实践
|
||||
|
||||
### v-for 的 key
|
||||
|
||||
```vue
|
||||
<!-- ❌ v-for 中使用 index 作为 key -->
|
||||
<template>
|
||||
<li v-for="(item, index) in items" :key="index">
|
||||
{{ item.name }}
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<!-- ✅ 使用唯一标识作为 key -->
|
||||
<template>
|
||||
<li v-for="item in items" :key="item.id">
|
||||
{{ item.name }}
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<!-- ✅ 复合 key(当没有唯一 ID 时)-->
|
||||
<template>
|
||||
<li v-for="(item, index) in items" :key="`${item.name}-${item.type}-${index}`">
|
||||
{{ item.name }}
|
||||
</li>
|
||||
</template>
|
||||
```
|
||||
|
||||
### v-if 和 v-for 优先级
|
||||
|
||||
```vue
|
||||
<!-- ❌ v-if 和 v-for 同时使用 -->
|
||||
<template>
|
||||
<li v-for="user in users" v-if="user.active" :key="user.id">
|
||||
{{ user.name }}
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<!-- ✅ 使用 computed 过滤 -->
|
||||
<script setup lang="ts">
|
||||
const activeUsers = computed(() =>
|
||||
users.value.filter(user => user.active)
|
||||
)
|
||||
</script>
|
||||
<template>
|
||||
<li v-for="user in activeUsers" :key="user.id">
|
||||
{{ user.name }}
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<!-- ✅ 或用 template 包裹 -->
|
||||
<template>
|
||||
<template v-for="user in users" :key="user.id">
|
||||
<li v-if="user.active">
|
||||
{{ user.name }}
|
||||
</li>
|
||||
</template>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 事件处理
|
||||
|
||||
```vue
|
||||
<!-- ❌ 内联复杂逻辑 -->
|
||||
<template>
|
||||
<button @click="items = items.filter(i => i.id !== item.id); count--">
|
||||
Delete
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<!-- ✅ 使用方法 -->
|
||||
<script setup lang="ts">
|
||||
const deleteItem = (id: number) => {
|
||||
items.value = items.value.filter(i => i.id !== id)
|
||||
count.value--
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<button @click="deleteItem(item.id)">Delete</button>
|
||||
</template>
|
||||
|
||||
<!-- ✅ 事件修饰符 -->
|
||||
<template>
|
||||
<!-- 阻止默认行为 -->
|
||||
<form @submit.prevent="handleSubmit">...</form>
|
||||
|
||||
<!-- 阻止冒泡 -->
|
||||
<button @click.stop="handleClick">...</button>
|
||||
|
||||
<!-- 只执行一次 -->
|
||||
<button @click.once="handleOnce">...</button>
|
||||
|
||||
<!-- 键盘修饰符 -->
|
||||
<input @keyup.enter="submit" @keyup.esc="cancel" />
|
||||
</template>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Composables
|
||||
|
||||
### Composable 设计原则
|
||||
|
||||
```typescript
|
||||
// ✅ 好的 composable 设计
|
||||
export function useCounter(initialValue = 0) {
|
||||
const count = ref(initialValue)
|
||||
|
||||
const increment = () => count.value++
|
||||
const decrement = () => count.value--
|
||||
const reset = () => count.value = initialValue
|
||||
|
||||
// 返回响应式引用和方法
|
||||
return {
|
||||
count: readonly(count), // 只读防止外部修改
|
||||
increment,
|
||||
decrement,
|
||||
reset
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ 不要返回 .value
|
||||
export function useBadCounter() {
|
||||
const count = ref(0)
|
||||
return {
|
||||
count: count.value // ❌ 丢失响应性!
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Props 传递给 composable
|
||||
|
||||
```vue
|
||||
<!-- ❌ 传递 props 到 composable 丢失响应性 -->
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ userId: string }>()
|
||||
const { user } = useUser(props.userId) // 丢失响应性!
|
||||
</script>
|
||||
|
||||
<!-- ✅ 使用 toRef 或 computed 保持响应性 -->
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ userId: string }>()
|
||||
const userIdRef = toRef(props, 'userId')
|
||||
const { user } = useUser(userIdRef) // 保持响应性
|
||||
// 或使用 computed
|
||||
const { user } = useUser(computed(() => props.userId))
|
||||
|
||||
// ✅ Vue 3.5+:直接解构使用
|
||||
const { userId } = defineProps<{ userId: string }>()
|
||||
const { user } = useUser(() => userId) // getter 函数
|
||||
</script>
|
||||
```
|
||||
|
||||
### 异步 Composable
|
||||
|
||||
```typescript
|
||||
// ✅ 异步 composable 模式
|
||||
export function useFetch<T>(url: MaybeRefOrGetter<string>) {
|
||||
const data = ref<T | null>(null)
|
||||
const error = ref<Error | null>(null)
|
||||
const loading = ref(false)
|
||||
|
||||
const execute = async () => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const response = await fetch(toValue(url))
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`)
|
||||
}
|
||||
data.value = await response.json()
|
||||
} catch (e) {
|
||||
error.value = e as Error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式 URL 时自动重新获取
|
||||
watchEffect(() => {
|
||||
toValue(url) // 追踪依赖
|
||||
execute()
|
||||
})
|
||||
|
||||
return {
|
||||
data: readonly(data),
|
||||
error: readonly(error),
|
||||
loading: readonly(loading),
|
||||
refetch: execute
|
||||
}
|
||||
}
|
||||
|
||||
// 使用
|
||||
const { data, loading, error, refetch } = useFetch<User[]>('/api/users')
|
||||
```
|
||||
|
||||
### 生命周期与清理
|
||||
|
||||
```typescript
|
||||
// ✅ Composable 中正确处理生命周期
|
||||
export function useEventListener(
|
||||
target: MaybeRefOrGetter<EventTarget>,
|
||||
event: string,
|
||||
handler: EventListener
|
||||
) {
|
||||
// 组件挂载后添加
|
||||
onMounted(() => {
|
||||
toValue(target).addEventListener(event, handler)
|
||||
})
|
||||
|
||||
// 组件卸载时移除
|
||||
onUnmounted(() => {
|
||||
toValue(target).removeEventListener(event, handler)
|
||||
})
|
||||
}
|
||||
|
||||
// ✅ 使用 effectScope 管理副作用
|
||||
export function useFeature() {
|
||||
const scope = effectScope()
|
||||
|
||||
scope.run(() => {
|
||||
// 所有响应式效果都在这个 scope 内
|
||||
const state = ref(0)
|
||||
watch(state, () => { /* ... */ })
|
||||
watchEffect(() => { /* ... */ })
|
||||
})
|
||||
|
||||
// 清理所有效果
|
||||
onUnmounted(() => scope.stop())
|
||||
|
||||
return { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 性能优化
|
||||
|
||||
### v-memo
|
||||
|
||||
```vue
|
||||
<!-- ✅ v-memo:缓存子树,避免重复渲染 -->
|
||||
<template>
|
||||
<div v-for="item in list" :key="item.id" v-memo="[item.id === selected]">
|
||||
<!-- 只有当 item.id === selected 变化时才重新渲染 -->
|
||||
<ExpensiveComponent :item="item" :selected="item.id === selected" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ✅ 配合 v-for 使用 -->
|
||||
<template>
|
||||
<div
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
v-memo="[item.name, item.status]"
|
||||
>
|
||||
<!-- 只有 name 或 status 变化时重新渲染 -->
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### defineAsyncComponent
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
// ✅ 懒加载组件
|
||||
const HeavyChart = defineAsyncComponent(() =>
|
||||
import('./components/HeavyChart.vue')
|
||||
)
|
||||
|
||||
// ✅ 带加载和错误状态
|
||||
const AsyncModal = defineAsyncComponent({
|
||||
loader: () => import('./components/Modal.vue'),
|
||||
loadingComponent: LoadingSpinner,
|
||||
errorComponent: ErrorDisplay,
|
||||
delay: 200, // 延迟显示 loading(避免闪烁)
|
||||
timeout: 3000 // 超时时间
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### KeepAlive
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!-- ✅ 缓存动态组件 -->
|
||||
<KeepAlive>
|
||||
<component :is="currentTab" />
|
||||
</KeepAlive>
|
||||
|
||||
<!-- ✅ 指定缓存的组件 -->
|
||||
<KeepAlive include="TabA,TabB">
|
||||
<component :is="currentTab" />
|
||||
</KeepAlive>
|
||||
|
||||
<!-- ✅ 限制缓存数量 -->
|
||||
<KeepAlive :max="10">
|
||||
<component :is="currentTab" />
|
||||
</KeepAlive>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// KeepAlive 组件的生命周期钩子
|
||||
onActivated(() => {
|
||||
// 组件被激活时(从缓存恢复)
|
||||
refreshData()
|
||||
})
|
||||
|
||||
onDeactivated(() => {
|
||||
// 组件被停用时(进入缓存)
|
||||
pauseTimers()
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### 虚拟列表
|
||||
|
||||
```vue
|
||||
<!-- ✅ 大型列表使用虚拟滚动 -->
|
||||
<script setup lang="ts">
|
||||
import { useVirtualList } from '@vueuse/core'
|
||||
|
||||
const { list, containerProps, wrapperProps } = useVirtualList(
|
||||
items,
|
||||
{ itemHeight: 50 }
|
||||
)
|
||||
</script>
|
||||
<template>
|
||||
<div v-bind="containerProps" style="height: 400px; overflow: auto">
|
||||
<div v-bind="wrapperProps">
|
||||
<div v-for="item in list" :key="item.data.id" style="height: 50px">
|
||||
{{ item.data.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Review Checklist
|
||||
|
||||
### 响应性系统
|
||||
- [ ] ref 用于基本类型,reactive 用于对象(或统一用 ref)
|
||||
- [ ] 没有解构 reactive 对象(或使用了 toRefs)
|
||||
- [ ] props 传递给 composable 时保持了响应性
|
||||
- [ ] shallowRef/shallowReactive 用于大型对象优化
|
||||
- [ ] computed 中没有副作用
|
||||
|
||||
### Props & Emits
|
||||
- [ ] defineProps 使用 TypeScript 类型声明
|
||||
- [ ] 复杂默认值使用 withDefaults + 工厂函数
|
||||
- [ ] defineEmits 有完整的类型定义
|
||||
- [ ] 没有直接修改 props
|
||||
- [ ] 考虑使用 defineModel 简化 v-model(Vue 3.4+)
|
||||
|
||||
### Vue 3.5 新特性(如适用)
|
||||
- [ ] 使用 Reactive Props Destructure 简化 props 访问
|
||||
- [ ] 使用 useTemplateRef 替代 ref 属性
|
||||
- [ ] 表单使用 useId 生成 SSR 安全的 ID
|
||||
- [ ] 使用 onWatcherCleanup 处理复杂清理逻辑
|
||||
|
||||
### Watchers
|
||||
- [ ] watch/watchEffect 有适当的清理函数
|
||||
- [ ] 异步 watch 处理了竞态条件
|
||||
- [ ] flush: 'post' 用于 DOM 操作的 watcher
|
||||
- [ ] 避免过度使用 watcher(优先用 computed)
|
||||
- [ ] 考虑 once: true 用于一次性监听
|
||||
|
||||
### 模板
|
||||
- [ ] v-for 使用唯一且稳定的 key
|
||||
- [ ] v-if 和 v-for 没有在同一元素上
|
||||
- [ ] 事件处理使用方法而非内联复杂逻辑
|
||||
- [ ] 大型列表使用虚拟滚动
|
||||
|
||||
### Composables
|
||||
- [ ] 相关逻辑提取到 composables
|
||||
- [ ] composables 返回响应式引用(不是 .value)
|
||||
- [ ] 纯函数不要包装成 composable
|
||||
- [ ] 副作用在组件卸载时清理
|
||||
- [ ] 使用 effectScope 管理复杂副作用
|
||||
|
||||
### 性能
|
||||
- [ ] 大型组件拆分为小组件
|
||||
- [ ] 使用 defineAsyncComponent 懒加载
|
||||
- [ ] 避免不必要的响应式转换
|
||||
- [ ] v-memo 用于昂贵的列表渲染
|
||||
- [ ] KeepAlive 用于缓存动态组件
|
||||
+388
@@ -0,0 +1,388 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PR Analyzer - Analyze PR complexity and suggest review approach.
|
||||
|
||||
Usage:
|
||||
python pr-analyzer.py [--diff-file FILE] [--stats]
|
||||
|
||||
Or pipe diff directly:
|
||||
git diff main...HEAD | python pr-analyzer.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
import argparse
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
RISK_NO_TESTS = "NO_TEST_CHANGES"
|
||||
|
||||
|
||||
@dataclass
|
||||
class FileStats:
|
||||
"""Statistics for a single file."""
|
||||
filename: str
|
||||
additions: int = 0
|
||||
deletions: int = 0
|
||||
is_test: bool = False
|
||||
is_config: bool = False
|
||||
language: str = "unknown"
|
||||
|
||||
|
||||
@dataclass
|
||||
class PRAnalysis:
|
||||
"""Complete PR analysis results."""
|
||||
total_files: int
|
||||
total_additions: int
|
||||
total_deletions: int
|
||||
files: List[FileStats]
|
||||
complexity_score: float
|
||||
size_category: str
|
||||
estimated_review_time: int
|
||||
risk_factors: List[str]
|
||||
suggestions: List[str]
|
||||
|
||||
|
||||
def detect_language(filename: str) -> str:
|
||||
"""Detect programming language from filename."""
|
||||
_, ext = os.path.splitext(filename)
|
||||
extensions = {
|
||||
'.py': 'Python',
|
||||
'.js': 'JavaScript',
|
||||
'.ts': 'TypeScript',
|
||||
'.tsx': 'TypeScript/React',
|
||||
'.jsx': 'JavaScript/React',
|
||||
'.rs': 'Rust',
|
||||
'.go': 'Go',
|
||||
'.c': 'C',
|
||||
'.h': 'C/C++',
|
||||
'.cpp': 'C++',
|
||||
'.hpp': 'C++',
|
||||
'.cc': 'C++',
|
||||
'.cxx': 'C++',
|
||||
'.hh': 'C++',
|
||||
'.hxx': 'C++',
|
||||
'.java': 'Java',
|
||||
'.kt': 'Kotlin',
|
||||
'.swift': 'Swift',
|
||||
'.rb': 'Ruby',
|
||||
'.php': 'PHP',
|
||||
'.cs': 'C#',
|
||||
'.vue': 'Vue',
|
||||
'.svelte': 'Svelte',
|
||||
'.sql': 'SQL',
|
||||
'.md': 'Markdown',
|
||||
'.json': 'JSON',
|
||||
'.yaml': 'YAML',
|
||||
'.yml': 'YAML',
|
||||
'.toml': 'TOML',
|
||||
'.css': 'CSS',
|
||||
'.scss': 'SCSS',
|
||||
'.less': 'Less',
|
||||
'.html': 'HTML',
|
||||
'.zig': 'Zig',
|
||||
'.ex': 'Elixir',
|
||||
'.exs': 'Elixir',
|
||||
'.erl': 'Erlang',
|
||||
'.scala': 'Scala',
|
||||
'.lua': 'Lua',
|
||||
}
|
||||
return extensions.get(ext.lower(), 'unknown')
|
||||
|
||||
|
||||
def is_test_file(filename: str) -> bool:
|
||||
"""Check if file is a test file."""
|
||||
test_patterns = [
|
||||
r'test_.*\.py$',
|
||||
r'.*_test\.py$',
|
||||
r'.*\.test\.(js|ts|tsx)$',
|
||||
r'.*\.spec\.(js|ts|tsx)$',
|
||||
r'tests?/',
|
||||
r'__tests__/',
|
||||
]
|
||||
return any(re.search(p, filename) for p in test_patterns)
|
||||
|
||||
|
||||
def is_config_file(filename: str) -> bool:
|
||||
"""Check if file is a configuration file."""
|
||||
config_patterns = [
|
||||
r'\.env',
|
||||
r'config\.',
|
||||
r'\.json$',
|
||||
r'\.yaml$',
|
||||
r'\.yml$',
|
||||
r'\.toml$',
|
||||
r'Cargo\.toml$',
|
||||
r'package\.json$',
|
||||
r'tsconfig\.json$',
|
||||
]
|
||||
return any(re.search(p, filename) for p in config_patterns)
|
||||
|
||||
|
||||
def parse_diff(diff_content: str) -> List[FileStats]:
|
||||
"""Parse git diff output and extract file statistics."""
|
||||
files = []
|
||||
current_file = None
|
||||
|
||||
for line in diff_content.split('\n'):
|
||||
# New file header
|
||||
if line.startswith('diff --git'):
|
||||
if current_file:
|
||||
files.append(current_file)
|
||||
# "diff --git a/<path> b/<path>" — match the b/ side via a
|
||||
# backreference so a literal "b/" inside paths like lib/, web/ or
|
||||
# db/ can't be mistaken for the prefix. Renames have differing
|
||||
# paths, so fall back to the b/ side after the separating space.
|
||||
match = re.match(r'diff --git a/(.+?) b/\1', line)
|
||||
if not match:
|
||||
match = re.search(r' b/(.+)$', line)
|
||||
if match:
|
||||
filename = match.group(1)
|
||||
current_file = FileStats(
|
||||
filename=filename,
|
||||
language=detect_language(filename),
|
||||
is_test=is_test_file(filename),
|
||||
is_config=is_config_file(filename),
|
||||
)
|
||||
else:
|
||||
current_file = None
|
||||
elif current_file:
|
||||
if line.startswith('+') and not line.startswith('+++'):
|
||||
current_file.additions += 1
|
||||
elif line.startswith('-') and not line.startswith('---'):
|
||||
current_file.deletions += 1
|
||||
|
||||
if current_file:
|
||||
files.append(current_file)
|
||||
|
||||
return files
|
||||
|
||||
|
||||
def calculate_complexity(files: List[FileStats]) -> float:
|
||||
"""Calculate complexity score (0-1 scale)."""
|
||||
if not files:
|
||||
return 0.0
|
||||
|
||||
total_changes = sum(f.additions + f.deletions for f in files)
|
||||
|
||||
# Base complexity from size
|
||||
size_factor = min(total_changes / 1000, 1.0)
|
||||
|
||||
# Factor for number of files
|
||||
file_factor = min(len(files) / 20, 1.0)
|
||||
|
||||
# Factor for non-test code ratio
|
||||
test_lines = sum(f.additions + f.deletions for f in files if f.is_test)
|
||||
non_test_ratio = 1 - (test_lines / max(total_changes, 1))
|
||||
|
||||
# Factor for language diversity
|
||||
languages = set(f.language for f in files if f.language != 'unknown')
|
||||
lang_factor = min(len(languages) / 5, 1.0)
|
||||
|
||||
complexity = (
|
||||
size_factor * 0.4 +
|
||||
file_factor * 0.2 +
|
||||
non_test_ratio * 0.2 +
|
||||
lang_factor * 0.2
|
||||
)
|
||||
|
||||
return round(complexity, 2)
|
||||
|
||||
|
||||
def categorize_size(total_changes: int) -> str:
|
||||
"""Categorize PR size."""
|
||||
if total_changes < 50:
|
||||
return "XS (Extra Small)"
|
||||
elif total_changes < 200:
|
||||
return "S (Small)"
|
||||
elif total_changes < 400:
|
||||
return "M (Medium)"
|
||||
elif total_changes < 800:
|
||||
return "L (Large)"
|
||||
else:
|
||||
return "XL (Extra Large) - Consider splitting"
|
||||
|
||||
|
||||
def estimate_review_time(files: List[FileStats], complexity: float) -> int:
|
||||
"""Estimate review time in minutes."""
|
||||
total_changes = sum(f.additions + f.deletions for f in files)
|
||||
|
||||
# Base time: ~1 minute per 20 lines
|
||||
base_time = total_changes / 20
|
||||
|
||||
# Adjust for complexity
|
||||
adjusted_time = base_time * (1 + complexity)
|
||||
|
||||
# Minimum 5 minutes, maximum 120 minutes
|
||||
return max(5, min(120, int(adjusted_time)))
|
||||
|
||||
|
||||
def identify_risk_factors(files: List[FileStats]) -> List[str]:
|
||||
"""Identify potential risk factors in the PR."""
|
||||
risks = []
|
||||
|
||||
total_changes = sum(f.additions + f.deletions for f in files)
|
||||
test_changes = sum(f.additions + f.deletions for f in files if f.is_test)
|
||||
|
||||
if total_changes > 400:
|
||||
risks.append("Large PR (>400 lines) - harder to review thoroughly")
|
||||
|
||||
if test_changes == 0 and total_changes > 50:
|
||||
risks.append(f"{RISK_NO_TESTS}: No test changes - verify test coverage")
|
||||
|
||||
if total_changes > 100 and test_changes / max(total_changes, 1) < 0.2:
|
||||
risks.append("Low test ratio (<20%) - consider adding more tests")
|
||||
|
||||
# Security-sensitive files
|
||||
security_patterns = ['.env', 'auth', 'security', 'password', 'token', 'secret']
|
||||
for f in files:
|
||||
if any(p in f.filename.lower() for p in security_patterns):
|
||||
risks.append(f"Security-sensitive file: {f.filename}")
|
||||
break
|
||||
|
||||
# Database changes
|
||||
for f in files:
|
||||
if 'migration' in f.filename.lower() or f.language == 'SQL':
|
||||
risks.append("Database changes detected - review carefully")
|
||||
break
|
||||
|
||||
# Config changes
|
||||
config_files = [f for f in files if f.is_config]
|
||||
if config_files:
|
||||
risks.append(f"Configuration changes in {len(config_files)} file(s)")
|
||||
|
||||
return risks
|
||||
|
||||
|
||||
def generate_suggestions(files: List[FileStats], complexity: float, risks: List[str]) -> List[str]:
|
||||
"""Generate review suggestions."""
|
||||
suggestions = []
|
||||
|
||||
total_changes = sum(f.additions + f.deletions for f in files)
|
||||
|
||||
if total_changes > 800:
|
||||
suggestions.append("Consider splitting this PR into smaller, focused changes")
|
||||
|
||||
if complexity > 0.7:
|
||||
suggestions.append("High complexity - allocate extra review time")
|
||||
suggestions.append("Consider pair reviewing for critical sections")
|
||||
|
||||
if any(RISK_NO_TESTS in r for r in risks):
|
||||
suggestions.append("Request test additions before approval")
|
||||
|
||||
# Language-specific suggestions
|
||||
languages = set(f.language for f in files)
|
||||
if 'TypeScript' in languages or 'TypeScript/React' in languages:
|
||||
suggestions.append("Check for proper type usage (avoid 'any')")
|
||||
if 'Rust' in languages:
|
||||
suggestions.append("Check for unwrap() usage and error handling")
|
||||
if 'C' in languages or 'C++' in languages or 'C/C++' in languages:
|
||||
suggestions.append("Check for memory safety, bounds checks, and UB risks")
|
||||
if 'SQL' in languages:
|
||||
suggestions.append("Review for SQL injection and query performance")
|
||||
|
||||
if not suggestions:
|
||||
suggestions.append("Standard review process should suffice")
|
||||
|
||||
return suggestions
|
||||
|
||||
|
||||
def analyze_pr(diff_content: str) -> PRAnalysis:
|
||||
"""Perform complete PR analysis."""
|
||||
files = parse_diff(diff_content)
|
||||
|
||||
total_additions = sum(f.additions for f in files)
|
||||
total_deletions = sum(f.deletions for f in files)
|
||||
total_changes = total_additions + total_deletions
|
||||
|
||||
complexity = calculate_complexity(files)
|
||||
risks = identify_risk_factors(files)
|
||||
suggestions = generate_suggestions(files, complexity, risks)
|
||||
|
||||
return PRAnalysis(
|
||||
total_files=len(files),
|
||||
total_additions=total_additions,
|
||||
total_deletions=total_deletions,
|
||||
files=files,
|
||||
complexity_score=complexity,
|
||||
size_category=categorize_size(total_changes),
|
||||
estimated_review_time=estimate_review_time(files, complexity),
|
||||
risk_factors=risks,
|
||||
suggestions=suggestions,
|
||||
)
|
||||
|
||||
|
||||
def print_analysis(analysis: PRAnalysis, show_files: bool = False):
|
||||
"""Print analysis results."""
|
||||
print("\n" + "=" * 60)
|
||||
print("PR ANALYSIS REPORT")
|
||||
print("=" * 60)
|
||||
|
||||
print(f"\n📊 SUMMARY")
|
||||
print(f" Files changed: {analysis.total_files}")
|
||||
print(f" Additions: +{analysis.total_additions}")
|
||||
print(f" Deletions: -{analysis.total_deletions}")
|
||||
print(f" Total changes: {analysis.total_additions + analysis.total_deletions}")
|
||||
|
||||
print(f"\n📏 SIZE: {analysis.size_category}")
|
||||
print(f" Complexity score: {analysis.complexity_score}/1.0")
|
||||
print(f" Estimated review time: ~{analysis.estimated_review_time} minutes")
|
||||
|
||||
if analysis.risk_factors:
|
||||
print(f"\n⚠️ RISK FACTORS:")
|
||||
for risk in analysis.risk_factors:
|
||||
print(f" • {risk}")
|
||||
|
||||
print(f"\n💡 SUGGESTIONS:")
|
||||
for suggestion in analysis.suggestions:
|
||||
print(f" • {suggestion}")
|
||||
|
||||
if show_files:
|
||||
print(f"\n📁 FILES:")
|
||||
# Group by language
|
||||
by_lang: Dict[str, List[FileStats]] = defaultdict(list)
|
||||
for f in analysis.files:
|
||||
by_lang[f.language].append(f)
|
||||
|
||||
for lang, lang_files in sorted(by_lang.items()):
|
||||
print(f"\n [{lang}]")
|
||||
for f in lang_files:
|
||||
prefix = "🧪" if f.is_test else "⚙️" if f.is_config else "📄"
|
||||
print(f" {prefix} {f.filename} (+{f.additions}/-{f.deletions})")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Analyze PR complexity')
|
||||
parser.add_argument('--diff-file', '-f', help='Path to diff file')
|
||||
parser.add_argument('--stats', '-s', action='store_true', help='Show file details')
|
||||
args = parser.parse_args()
|
||||
|
||||
# Read diff from file or stdin
|
||||
try:
|
||||
if args.diff_file:
|
||||
with open(args.diff_file, 'r', encoding='utf-8', errors='replace') as f:
|
||||
diff_content = f.read()
|
||||
elif not sys.stdin.isatty():
|
||||
diff_content = sys.stdin.buffer.read().decode('utf-8', errors='replace')
|
||||
else:
|
||||
print("Usage: git diff main...HEAD | python pr-analyzer.py")
|
||||
print(" python pr-analyzer.py -f diff.txt")
|
||||
sys.exit(1)
|
||||
except OSError as e:
|
||||
print(f"Error reading diff input: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if not diff_content.strip():
|
||||
print("No diff content provided")
|
||||
sys.exit(1)
|
||||
|
||||
analysis = analyze_pr(diff_content)
|
||||
print_analysis(analysis, show_files=args.stats)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for pr-analyzer.py diff parsing (stdlib unittest, no extra deps)."""
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
import unittest
|
||||
|
||||
# The script has a hyphen in its name, so load it by path.
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
_spec = importlib.util.spec_from_file_location(
|
||||
'pr_analyzer', os.path.join(_HERE, 'pr-analyzer.py')
|
||||
)
|
||||
pr_analyzer = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(pr_analyzer)
|
||||
|
||||
|
||||
class ParseDiffFilenameTest(unittest.TestCase):
|
||||
def test_lib_prefixed_path(self):
|
||||
# "lib/" embeds a literal "b/" that the old regex swallowed.
|
||||
diff = (
|
||||
"diff --git a/lib/foo.py b/lib/foo.py\n"
|
||||
"index 1234567..89abcde 100644\n"
|
||||
"--- a/lib/foo.py\n"
|
||||
"+++ b/lib/foo.py\n"
|
||||
"@@ -1,2 +1,3 @@\n"
|
||||
" unchanged\n"
|
||||
"+added line\n"
|
||||
"-removed line\n"
|
||||
)
|
||||
files = pr_analyzer.parse_diff(diff)
|
||||
self.assertEqual(len(files), 1)
|
||||
self.assertEqual(files[0].filename, 'lib/foo.py')
|
||||
self.assertEqual(files[0].additions, 1)
|
||||
self.assertEqual(files[0].deletions, 1)
|
||||
|
||||
def test_normal_path(self):
|
||||
diff = (
|
||||
"diff --git a/src/main.py b/src/main.py\n"
|
||||
"index 1111111..2222222 100644\n"
|
||||
"--- a/src/main.py\n"
|
||||
"+++ b/src/main.py\n"
|
||||
"@@ -0,0 +1 @@\n"
|
||||
"+print('hi')\n"
|
||||
)
|
||||
files = pr_analyzer.parse_diff(diff)
|
||||
self.assertEqual(len(files), 1)
|
||||
self.assertEqual(files[0].filename, 'src/main.py')
|
||||
|
||||
def test_other_embedded_b_slash_prefixes(self):
|
||||
# web/ and db/ also contain a literal "b/".
|
||||
diff = (
|
||||
"diff --git a/web/x.js b/web/x.js\n"
|
||||
"+++ b/web/x.js\n"
|
||||
"+console.log(1)\n"
|
||||
"diff --git a/db/y.sql b/db/y.sql\n"
|
||||
"+++ b/db/y.sql\n"
|
||||
"+SELECT 1;\n"
|
||||
)
|
||||
files = pr_analyzer.parse_diff(diff)
|
||||
self.assertEqual([f.filename for f in files], ['web/x.js', 'db/y.sql'])
|
||||
|
||||
def test_rename_falls_back_to_b_side(self):
|
||||
diff = (
|
||||
"diff --git a/old/name.py b/new/name.py\n"
|
||||
"similarity index 100%\n"
|
||||
"rename from old/name.py\n"
|
||||
"rename to new/name.py\n"
|
||||
)
|
||||
files = pr_analyzer.parse_diff(diff)
|
||||
self.assertEqual(len(files), 1)
|
||||
self.assertEqual(files[0].filename, 'new/name.py')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,138 @@
|
||||
---
|
||||
name: "database-migration"
|
||||
description: "Wavelet 项目专用:当新增或修改数据库表结构、索引、初始化数据、系统配置 seed、模板 seed、默认管理员、goose SQL 迁移、internal/db/migrator、ClickHouse 分析库 DDL 或数据库升级流程时必须使用。本技能指导在 internal/db/migrator/goose 下编写 PostgreSQL/SQLite 双方言 SQL 迁移,以及在 goose/clickhouse 下编写 ClickHouse 单方言分析表迁移,并完成验证。"
|
||||
---
|
||||
|
||||
# Wavelet 数据库升级操作指南
|
||||
|
||||
Wavelet 使用 `github.com/pressly/goose/v3` 执行 SQL 迁移。迁移入口是 `internal/db/migrator.Migrate()`,SQL 文件嵌入在二进制中。
|
||||
|
||||
## 基本规则
|
||||
|
||||
- SQL 迁移文件放在:
|
||||
- `internal/db/migrator/goose/postgres/`
|
||||
- `internal/db/migrator/goose/sqlite/`
|
||||
- PostgreSQL 和 SQLite 必须使用同一个版本号、同一个语义文件名。
|
||||
- 迁移文件使用 goose SQL 标记:
|
||||
|
||||
```sql
|
||||
-- +goose Up
|
||||
...
|
||||
|
||||
-- +goose Down
|
||||
...
|
||||
```
|
||||
|
||||
- 不要把表结构、默认系统配置、默认模板、默认管理员初始化写回 Go 代码。
|
||||
- 编辑表结构(DDL)和插入表数据(DML/Seed)不要放在同一个 SQL 文件里,必须分成两个独立的 SQL 文件完成(例如,先通过一个文件修改表结构,再通过下一个递增版本号的文件插入/初始化数据)。
|
||||
- 插入定时任务(schedules 表数据)时绝对不能指定 `id`,必须依靠数据库自增(Identity 或 AUTOINCREMENT)自动分配,防止与用户手动或后续插入的定时任务产生 ID 冲突。
|
||||
- 不要添加物理外键;关系字段使用显式索引。
|
||||
- 数据库默认值应匹配 Go model 零值或业务兜底值。
|
||||
- 系统配置仍然保存字符串值;布尔值写 `"true"` / `"false"`,数字写十进制字符串,复杂结构写合法 JSON 字符串。
|
||||
|
||||
## 新增迁移流程
|
||||
|
||||
1. 先确认涉及的 Go model、读写路径和前端/接口消费方。
|
||||
2. 选择下一个递增版本号,格式建议 `YYYYMMDDNNNN`,例如:
|
||||
|
||||
```text
|
||||
202606090002_add_example_column.sql
|
||||
```
|
||||
|
||||
3. 在 PostgreSQL 和 SQLite 目录各新增同名 SQL 文件。
|
||||
4. 写 `Up`:
|
||||
- 表结构变更使用 SQL DDL。
|
||||
- 初始化/seed 数据使用 SQL `INSERT`。
|
||||
- 需要幂等时使用 `IF NOT EXISTS` 或 `ON CONFLICT ... DO NOTHING`。
|
||||
5. 写 `Down`:
|
||||
- 能安全回滚的结构变更写反向 DDL。
|
||||
- seed 数据按 key/name 等稳定标识删除。
|
||||
6. 如果变更 API handler,运行 `make swagger`。
|
||||
7. 至少运行:
|
||||
|
||||
```bash
|
||||
go test ./internal/db/migrator
|
||||
go test ./internal/model ./internal/apps/config ./internal/apps/admin/system_config
|
||||
make code-check
|
||||
```
|
||||
|
||||
## 方言注意事项
|
||||
|
||||
- PostgreSQL 自增主键用 `BIGSERIAL`;SQLite 自增主键用 `INTEGER PRIMARY KEY AUTOINCREMENT`。
|
||||
- PostgreSQL 时间类型优先 `TIMESTAMPTZ`;SQLite 使用 `DATETIME`。
|
||||
- PostgreSQL JSON 字段用 `JSONB`;SQLite 用 `JSON` 或 `TEXT`。
|
||||
- 两个方言目录的字段名、索引名、seed 数据语义必须保持一致。
|
||||
|
||||
## 修改默认系统配置
|
||||
|
||||
- 新增或调整系统配置 seed 时,更新两个方言的 SQL 文件。
|
||||
- `visibility` 使用常量语义:`0` 不公开,`1` 通过 `/api/v1/config/public` 返回。
|
||||
- 公共配置 API 直接返回所有 `visibility = 1` 的配置键值,不要在 handler 中重新硬编码 key 列表。
|
||||
|
||||
## 验证重点
|
||||
|
||||
- goose 能在空库上完整执行。
|
||||
- `system_configs`、默认 `admin`、内置模板能按预期初始化。
|
||||
- 新增表/列与 Go model 的列名、类型和默认值兼容。
|
||||
- 前端或接口消费的公共配置值仍按字符串解析。
|
||||
|
||||
## ClickHouse 分析库(辅助 OLAP)
|
||||
|
||||
ClickHouse 是**辅助 OLAP 存储**,与 PostgreSQL/SQLite 主库**完全独立**的迁移与访问管线:
|
||||
|
||||
- 主库(PG/SQLite):业务事务数据、`goose_db_version`、双方言 SQL。
|
||||
- 分析库(ClickHouse):访问日志、统计聚合等分析型数据、`goose_clickhouse_version`、单方言 SQL。
|
||||
|
||||
**不要**把 ClickHouse 表结构混入 PG/SQLite 迁移目录,也**不要**在 `support-files/`、`internal/apps/` 或 `internal/repository/` 中手写 DDL。
|
||||
|
||||
### 目录与职责
|
||||
|
||||
| 路径 | 职责 |
|
||||
| :--- | :--- |
|
||||
| `internal/db/migrator/goose/clickhouse/` | **唯一** ClickHouse DDL 来源(goose SQL,嵌入二进制) |
|
||||
| `internal/model/analytics/` | 分析表 Go model,列名须与 goose DDL 一致 |
|
||||
| `internal/repository/analytics/` | 所有 ClickHouse 读写(批量写入、查询、聚合) |
|
||||
| `internal/db/clickhouse.go` | 连接初始化(`ChConn` 原生批量、`ChDB` GORM 查询) |
|
||||
|
||||
### 迁移入口与版本表
|
||||
|
||||
- 入口:`migrator.MigrateClickHouse()`,在 `cmd/root.go` 的 `PreRun` 中于 `migrator.Migrate()` 之后调用。
|
||||
- 仅当 `clickhouse.enabled: true` 时执行;禁用时直接跳过(见 `TestMigrateClickHouseSkipsWhenDisabled`)。
|
||||
- 版本表:`goose_clickhouse_version`,与主库 `goose_db_version` **分离**,互不影响。
|
||||
- 方言:仅 ClickHouse,**无** SQLite 镜像目录。
|
||||
|
||||
### ClickHouse 迁移规则
|
||||
|
||||
1. **DDL 只写 goose SQL**:`CREATE TABLE IF NOT EXISTS ...`,禁止 GORM `AutoMigrate`、禁止在 repository 或 handler 中建表。
|
||||
2. **无事务**:ClickHouse 不支持 goose 事务包装;每个 `Up`/`Down` 语句独立提交。
|
||||
3. **幂等 Up**:表用 `IF NOT EXISTS`;`Down` 用 `DROP TABLE IF EXISTS`。
|
||||
4. **Down 谨慎**:MergeTree 等引擎上 `DROP TABLE` 会立即删除数据,生产环境通常只前滚;仅在开发/测试需要回滚时编写 `Down`。
|
||||
5. **DDL 与 DML 分离**:与主库相同,表结构变更与数据初始化分文件、分版本号;分析表通常无 seed,批量写入由 repository 在运行时完成。
|
||||
6. **引擎与排序键**:在 SQL 中显式声明 `ENGINE`、`PARTITION BY`、`ORDER BY` 等,与查询模式对齐(例如按 `created_at` 分区)。
|
||||
7. **禁止重复 DDL**:不要在 `support-files/`、`apps` 初始化逻辑或 `repository/analytics` 中复制建表语句。
|
||||
|
||||
### 新增分析表工作流
|
||||
|
||||
按以下顺序落地,避免列名或类型漂移:
|
||||
|
||||
1. **Model**:在 `internal/model/analytics/` 定义 struct,`gorm:"column:..."` 与 DDL 列名一一对应;实现 `TableName()`,批量写入表可提供 `InsertColumns()` / `BatchInsertSQL()`。
|
||||
2. **Goose SQL**:在 `internal/db/migrator/goose/clickhouse/` 新增递增版本文件(格式同主库,如 `YYYYMMDDNNNN_create_xxx.sql`),编写 `-- +goose Up` / `-- +goose Down`。
|
||||
3. **Repository**:在 `internal/repository/analytics/` 实现 `BatchInsert*`(`db.ChConn` 一次 `PrepareBatch` + 多行 `Append` + 一次 `Send`)与查询(`db.ChDB`);连接未初始化时返回明确错误,**不要**在 handler 写 SQL,**不要**在 repository 内维护 channel/goroutine。
|
||||
4. **Apps**:在 `internal/apps/<domain>/` 编排采集与入队;高频写入通过 `internal/db/batchwriter` 各域独立实例异步 flush(详见 `clickhouse-batchwriter` 技能),`FlushFunc` 只调 repository `BatchInsert*`;管理端统计 API 只读 repository,不触达 DDL。
|
||||
|
||||
### ClickHouse 验证
|
||||
|
||||
至少运行:
|
||||
|
||||
```bash
|
||||
go test ./internal/db/migrator
|
||||
go test ./internal/repository/analytics
|
||||
make code-check
|
||||
```
|
||||
|
||||
验证重点:
|
||||
|
||||
- goose 能在空 ClickHouse 实例上完整执行 `Up`。
|
||||
- `internal/model/analytics` 列名、类型与 goose SQL 一致。
|
||||
- repository 读写路径不依赖 handler 内联 SQL。
|
||||
- `clickhouse.enabled: false` 时启动不报错、不执行迁移。
|
||||
@@ -0,0 +1,265 @@
|
||||
---
|
||||
name: "file-upload"
|
||||
description: "Wavelet 项目专用:当业务需要上传文件、读取已上传文件、在 Worker/任务中程序化摄取字节流、选择存储引擎能力、或排查 w_uploads / 文件统计异常时必须使用。本技能指导 storage 与 upload 分层、upload.Ingest 策略选型、前后端接入与禁止旁路写表。"
|
||||
---
|
||||
|
||||
# 存储引擎与文件上传开发规范
|
||||
|
||||
本技能是 Wavelet **文件上传与对象存储**的唯一开发指导。开始开发前先阅读仓库根目录 [AGENTS.md](file:///Users/ryan/DEV/Go/Wavelet/AGENTS.md),遵守项目级核心规则。
|
||||
|
||||
---
|
||||
|
||||
## 架构分层(必须理解)
|
||||
|
||||
Wavelet 将「对象存储」与「上传业务」分为两层,**禁止混用职责**:
|
||||
|
||||
| 层级 | 包路径 | 职责 | 业务是否直接调用 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **对象存储引擎** | `internal/storage` | `Backend` 接口:`Put` / `Get` / `Delete` / `Test`;按配置切换 Local / S3 / R2 / OSS / WebDAV | **禁止**(仅 upload 域内部使用) |
|
||||
| **上传域服务** | `internal/apps/upload` | `w_uploads` 记录、权限、秒传、统计、文件服务、`upload.Ingest` | **必须** |
|
||||
| **上传 HTTP 入口** | `internal/apps/upload/handler` | `POST /api/v1/upload` 等 multipart 接口 | 前端 / 用户侧上传 |
|
||||
| **文件访问** | `internal/apps/upload/filesrv` | `GET /f/:id` 流式响应、访问控制、图片 WebP 压缩 | 展示 / 下载 |
|
||||
|
||||
```text
|
||||
业务模块 ──► upload.Ingest / upload.Remove(唯一写入门禁)
|
||||
├── storage.Backend.Put/Get/Delete
|
||||
├── repository.CreateUpload(仅 upload 内部)
|
||||
└── RecordUploadStatsAdd/Remove(ingest 内置,禁止业务直调)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 核心防线(Guardrails)
|
||||
|
||||
以下写法**一律禁止**:
|
||||
|
||||
```go
|
||||
// ❌ 业务包直接写 blob
|
||||
storage.Active(ctx); backend.Put(...)
|
||||
|
||||
// ❌ 旁路写 w_uploads
|
||||
db.DB(ctx).Create(&model.Upload{})
|
||||
repository.CreateUpload(ctx, upload) // 仅 internal/apps/upload 允许
|
||||
|
||||
// ❌ 手动维护统计
|
||||
upload.ApplyUploadStatsAdd(ctx, upload) // 已 Deprecated
|
||||
|
||||
// ❌ 业务表存物理路径
|
||||
invoice.FilePath = "uploads/2026/01/02/123.pdf"
|
||||
```
|
||||
|
||||
**正确做法**:业务表只存 `upload_id`(`uint64` / JSON string),通过 `/f/{id}` 或 `upload.OpenStoredObject` 访问。
|
||||
|
||||
---
|
||||
|
||||
## Ingest 策略选型(Policy Decision)
|
||||
|
||||
根据场景选择 `upload.Ingest` 的 `Policy`:
|
||||
|
||||
| 场景 | Policy | 哈希命中时 | 未命中时 | 典型调用方 |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| 用户 HTTP 上传(含秒传) | `PolicyDedupNewRecord` | 复用 path,**新建记录 + 统计** | 写 blob + 新建记录 + 统计 | `handler.UploadFile`(已内置) |
|
||||
| Worker 生成全新文件 | `PolicyCreate` | 不查重,始终写 blob + 记录 | 同左 | 报表导出、定时生成 |
|
||||
| 镜像 / 去重摄取(Pixez) | `PolicyResolveExisting` | **直接返回已有记录**,不建新记录、不加统计 | 写 blob + 新建记录 + 统计 | 异步镜像任务 |
|
||||
| 业务只需引用已有文件 | 不调 Ingest | — | — | 业务 API 校验 `upload_id` 即可 |
|
||||
|
||||
### Result 字段含义
|
||||
|
||||
| 字段 | 含义 |
|
||||
| :--- | :--- |
|
||||
| `Created` | 是否新建了 `w_uploads` 记录 |
|
||||
| `Stored` | 是否写入了新 blob |
|
||||
| `Resolved` | 是否通过哈希解析到已有记录(仅 `PolicyResolveExisting`) |
|
||||
|
||||
---
|
||||
|
||||
## 后端:程序化上传(Worker / 业务逻辑)
|
||||
|
||||
### 标准模板
|
||||
|
||||
在 `logics.go`(接受 `context.Context`,不依赖 `*gin.Context`)中调用:
|
||||
|
||||
```go
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/upload"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
)
|
||||
|
||||
func ingestMirrorFile(ctx context.Context, userID uint64, data []byte, hash, filename, mime, ext string) (model.Upload, error) {
|
||||
accessMode := 1
|
||||
result, err := upload.Ingest(ctx, upload.IngestRequest{
|
||||
UserID: userID,
|
||||
Reader: bytes.NewReader(data),
|
||||
Size: int64(len(data)),
|
||||
FileName: filename,
|
||||
MimeType: mime,
|
||||
Extension: ext,
|
||||
Hash: hash, // 必填:SHA-256 hex
|
||||
Type: "your_biz_type",
|
||||
AccessMode: &accessMode,
|
||||
Metadata: model.UploadMetadata{
|
||||
Extra: map[string]any{"source": "worker"},
|
||||
},
|
||||
Policy: upload.PolicyResolveExisting,
|
||||
})
|
||||
if err != nil {
|
||||
return model.Upload{}, err
|
||||
}
|
||||
return result.Upload, nil
|
||||
}
|
||||
```
|
||||
|
||||
### Request 关键字段
|
||||
|
||||
| 字段 | 说明 |
|
||||
| :--- | :--- |
|
||||
| `Hash` | **必填**,推荐 SHA-256 hex;用于秒传 / 镜像去重 |
|
||||
| `Type` | 业务分类(如 `avatar`、`invoice`、`pixez_mirror`),用于筛选与统计 |
|
||||
| `AccessMode` | `nil` 时按 type 默认:`avatar` → 公开(1),其余 → 私有(0) |
|
||||
| `SkipExtensionCheck` | Worker 场景若已自行校验扩展名,可设为 `true` |
|
||||
| `ObjectKeyFn` | 可选自定义存储路径;默认 `uploads/YYYY/MM/DD/{id}.{ext}` |
|
||||
|
||||
### 错误处理
|
||||
|
||||
| 错误 | 含义 | Handler 映射建议 |
|
||||
| :--- | :--- | :--- |
|
||||
| `upload.ErrIngestStorageReadOnly` | 存储迁移维护中 | `response.AbortConflict` |
|
||||
| `ingest.ErrForbidden` | 无权删除他人文件 | HTTP 403 |
|
||||
| `shared.ErrUnsupportedFormat` | 扩展名不在白名单 | `response.AbortBadRequest` |
|
||||
|
||||
### 删除
|
||||
|
||||
```go
|
||||
// 管理员 / 系统删除
|
||||
_, err := upload.Remove(ctx, uploadID)
|
||||
|
||||
// 用户删除自己的文件
|
||||
_, err := upload.RemoveOwned(ctx, userID, uploadID)
|
||||
```
|
||||
|
||||
### 读取已存储对象(不上传)
|
||||
|
||||
```go
|
||||
uploadRec, err := repository.GetActiveUploadByID(ctx, uploadID)
|
||||
obj, err := uploadstorage.OpenStoredObject(ctx, &uploadRec)
|
||||
defer obj.Body.Close()
|
||||
```
|
||||
|
||||
或通过门面(若已从 `exports` 暴露 `OpenStoredObject`)读取。HTTP 对外访问统一走 `GET /f/:id`。
|
||||
|
||||
---
|
||||
|
||||
## 后端:业务 API 引用已上传文件
|
||||
|
||||
推荐 **两步流程**(先上传、后提交业务):
|
||||
|
||||
1. 前端 `POST /api/v1/upload` → 获得 `upload.id`
|
||||
2. 业务 API 接收 `upload_id`,用 `repository.GetActiveUploadByID` 校验存在且 `status` 为 active
|
||||
3. (可选)校验 `upload.Type` 是否为预期业务类型
|
||||
4. 将 `upload_id` 写入业务表字段(如 `cover_file_id`)
|
||||
|
||||
**禁止**在业务 Handler 中重复实现 multipart 解析,除非有极强的特殊协议需求。
|
||||
|
||||
---
|
||||
|
||||
## 前端:用户侧上传
|
||||
|
||||
使用 `frontend/lib/services/upload/`:
|
||||
|
||||
```typescript
|
||||
import { services } from '@/lib/services'
|
||||
import { getFileUrl } from '@/lib/services/upload'
|
||||
|
||||
// 上传
|
||||
const upload = await services.upload.uploadFile(file, 'invoice', { orderId: '123' })
|
||||
|
||||
// 展示
|
||||
const url = getFileUrl(upload.id) // → /f/{id}
|
||||
|
||||
// Base64 图片(头像等)
|
||||
const res = await services.upload.uploadBase64Image(croppedBase64, 'avatar', 'avatar.png')
|
||||
```
|
||||
|
||||
### 前端规范
|
||||
|
||||
- 新增上传相关 API 时,扩展 `UploadService` / `AdminUploadService`,在 `frontend/lib/services/index.ts` 注册
|
||||
- 图片预览使用 `getFileUrl(id, quality?)` 或 `FileImagePreview` 组件
|
||||
- 业务表单项只提交 `upload_id`,不要提交 blob URL 或 `file_path`
|
||||
|
||||
---
|
||||
|
||||
## 统计与排查
|
||||
|
||||
`w_upload_stats` 由 `upload.Ingest` / `upload.Remove` **自动维护**,业务不得手动增量。
|
||||
|
||||
若发现 trend / total 与 `w_uploads` 不一致(常见于历史旁路写表):
|
||||
|
||||
```go
|
||||
upload.RebuildUploadStats(ctx) // 从 w_uploads 全量重建统计
|
||||
```
|
||||
|
||||
排查清单:
|
||||
|
||||
1. 业务是否绕过 `upload.Ingest` 直接 `db.Create(&model.Upload{})`?
|
||||
2. 是否手动调用已 Deprecated 的 `ApplyUploadStatsAdd`?
|
||||
3. 删除是否走 `upload.Remove`(须在软删**前**扣减统计)?
|
||||
|
||||
---
|
||||
|
||||
## 测试要求
|
||||
|
||||
### 后端 ingest 测试
|
||||
|
||||
- 使用 `testhelper.SetupTestEnvironment(t)` 初始化 DB
|
||||
- 存储 mock:`storage.MockStorage(...)` + `storage.IsEnabledFunc = func() bool { return true }`
|
||||
- **禁止**在源码目录硬编码 `uploads/test` 路径;本地文件测试用 `t.TempDir()` 或 mock backend
|
||||
- 覆盖:三种 Policy、Remove 后统计归零、ReadOnly 拒绝写入
|
||||
|
||||
参考:[internal/apps/upload/ingest/ingest_test.go](file:///Users/ryan/DEV/Go/Wavelet/internal/apps/upload/ingest/ingest_test.go)
|
||||
|
||||
### Handler 回归
|
||||
|
||||
修改 upload handler 后运行:
|
||||
|
||||
```bash
|
||||
go test ./internal/apps/upload/...
|
||||
make code-check
|
||||
```
|
||||
|
||||
若变更 HTTP 接口,运行 `make swagger`。
|
||||
|
||||
---
|
||||
|
||||
## 存量代码迁移(旁路写表 → Ingest)
|
||||
|
||||
将以下模式:
|
||||
|
||||
```go
|
||||
storage.Active(ctx)
|
||||
backend.Put(ctx, key, reader, size, mime)
|
||||
db.DB(ctx).Create(&upload)
|
||||
```
|
||||
|
||||
替换为:
|
||||
|
||||
```go
|
||||
upload.Ingest(ctx, upload.IngestRequest{ Policy: upload.PolicyResolveExisting, ... })
|
||||
```
|
||||
|
||||
迁移完成后执行一次 `upload.RebuildUploadStats(ctx)` 修复历史统计偏差。
|
||||
|
||||
---
|
||||
|
||||
## 质量门禁 Checklist
|
||||
|
||||
完成文件上传相关开发后,确认:
|
||||
|
||||
- [ ] 业务模块无 `repository.CreateUpload` / `SoftDeleteUpload` 调用
|
||||
- [ ] 业务模块无 `storage.Active` + `Put` 直接写文件
|
||||
- [ ] 业务表存 `upload_id`,不存 `file_path`
|
||||
- [ ] Worker 摄取使用正确的 `Policy`
|
||||
- [ ] 新增测试覆盖 ingest 路径
|
||||
- [ ] `make code-check` 通过
|
||||
- [ ] HTTP 变更已 `make swagger`
|
||||
@@ -0,0 +1,183 @@
|
||||
---
|
||||
name: go-code-review
|
||||
description: Use when reviewing Go code or checking code against community style standards. Also use proactively before submitting a Go PR or when reviewing any Go code changes, even if the user doesn't explicitly request a style review. Does not cover language-specific syntax — delegates to specialized skills.
|
||||
license: Apache-2.0
|
||||
compatibility: Web server example in references uses slog (Go 1.21+)
|
||||
metadata:
|
||||
sources: "Go Wiki CodeReviewComments, Uber Style Guide"
|
||||
allowed-tools: Bash(bash:*)
|
||||
---
|
||||
|
||||
# Go 代码审查清单
|
||||
|
||||
## 审查流程
|
||||
|
||||
> 使用 `assets/review-template.md` 格式化代码审查输出,确保结构与"必须修复 / 建议修复 / 吹毛求疵"的严重程度分组保持一致。
|
||||
|
||||
1. 运行 `gofmt -d .` 和 `go vet ./...` 先捕获机械性问题
|
||||
2. 逐文件阅读 diff;对于每个文件,按以下类别顺序检查
|
||||
3. 标记问题时需要包含具体行号引用和规则名称
|
||||
4. 审查完所有文件后,重新阅读标记项以确认它们是真实的问题
|
||||
5. 按严重程度分组汇总发现(必须修复、建议修复、吹毛求疵)
|
||||
|
||||
> **验证**:完成审查后,再次阅读 diff 以验证每个标记的问题都是真实的。删除任何无法用具体行号引用的发现。
|
||||
|
||||
---
|
||||
|
||||
## 格式化
|
||||
|
||||
- [ ] **gofmt**:代码已使用 `gofmt` 或 `goimports` 格式化 → [go-linting](../go-linting/SKILL.md)
|
||||
|
||||
---
|
||||
|
||||
## 文档
|
||||
|
||||
- [ ] **注释句子**:注释是完整的句子,以被描述的名称开头,以句号结尾 → [go-documentation](../go-documentation/SKILL.md)
|
||||
- [ ] **文档注释**:所有导出名称都有文档注释;非平凡的未导出声明也应有 → [go-documentation](../go-documentation/SKILL.md)
|
||||
- [ ] **包注释**:包注释出现在 package 子句附近,无空行 → [go-documentation](../go-documentation/SKILL.md)
|
||||
- [ ] **命名结果参数**:仅当它们能澄清含义时使用(例如,多个相同类型返回值),而不仅仅是为了启用裸返回 → [go-documentation](../go-documentation/SKILL.md)
|
||||
|
||||
---
|
||||
|
||||
## 错误处理
|
||||
|
||||
- [ ] **处理错误**:不使用 `_` 丢弃错误;处理、返回或(在特殊情况下)panic → [go-error-handling](../go-error-handling/SKILL.md)
|
||||
- [ ] **错误字符串**:小写开头,无标点(除非以专有名词/首字母缩略词开头) → [go-error-handling](../go-error-handling/SKILL.md)
|
||||
- [ ] **带内错误**:不使用魔术值(-1、""、nil);使用带 error 或 ok bool 的多返回值 → [go-error-handling](../go-error-handling/SKILL.md)
|
||||
- [ ] **错误流缩进**:先处理错误并返回;保持正常路径的缩进最小化 → [go-error-handling](../go-error-handling/SKILL.md)
|
||||
|
||||
---
|
||||
|
||||
## 命名
|
||||
|
||||
- [ ] **MixedCaps**:使用 `MixedCaps` 或 `mixedCaps`,不使用下划线;未导出使用 `maxLength` 而非 `MAX_LENGTH` → [go-naming](../go-naming/SKILL.md)
|
||||
- [ ] **首字母缩略词**:保持一致的大小写:`URL`/`url`、`ID`/`id`、`HTTP`/`http`(例如 `ServeHTTP`、`xmlHTTPRequest`) → [go-naming](../go-naming/SKILL.md)
|
||||
- [ ] **变量名**:有限作用域用短名称(`i`、`r`、`c`);更广作用域用较长名称 → [go-naming](../go-naming/SKILL.md)
|
||||
- [ ] **接收器名称**:类型的一两个字母缩写(`c` 代表 `Client`);不使用 `this`、`self`、`me`;各方法之间保持一致 → [go-naming](../go-naming/SKILL.md)
|
||||
- [ ] **包名**:不重复(使用 `chubby.File` 而非 `chubby.ChubbyFile`);避免 `util`、`common`、`misc` → [go-packages](../go-packages/SKILL.md)
|
||||
- [ ] **避免内置名称**:不遮蔽 `error`、`string`、`len`、`cap`、`append`、`copy`、`new`、`make` → [go-declarations](../go-declarations/SKILL.md)
|
||||
|
||||
---
|
||||
|
||||
## 并发
|
||||
|
||||
- [ ] **Goroutine 生命周期**:明确 goroutine 何时/是否退出;如不明显则添加文档 → [go-concurrency](../go-concurrency/SKILL.md)
|
||||
- [ ] **同步函数**:优先同步而非异步;让调用者在需要时添加并发 → [go-concurrency](../go-concurrency/SKILL.md)
|
||||
- [ ] **Context**:作为第一个参数;不放在 struct 中;不自定义 Context 类型;即使认为不需要也应传递 → [go-context](../go-context/SKILL.md)
|
||||
|
||||
---
|
||||
|
||||
## 接口
|
||||
|
||||
- [ ] **接口位置**:在消费方包中定义,而非实现方;生产者返回具体类型 → [go-interfaces](../go-interfaces/SKILL.md)
|
||||
- [ ] **不提前定义接口**:不在使用前定义;不在实现方"为了 mock"而定义 → [go-interfaces](../go-interfaces/SKILL.md)
|
||||
- [ ] **接收器类型**:如果会修改状态、有 sync 字段或体积大,使用指针;小的不可变类型使用值;不要混用 → [go-interfaces](../go-interfaces/SKILL.md)
|
||||
|
||||
---
|
||||
|
||||
## 数据结构
|
||||
|
||||
- [ ] **空切片**:优先使用 `var t []string`(nil)而非 `t := []string{}`(非 nil 零长度) → [go-data-structures](../go-data-structures/SKILL.md)
|
||||
- [ ] **复制**:小心复制含指针/切片字段的结构体;不按值复制 `*T` 方法的接收器 → [go-data-structures](../go-data-structures/SKILL.md)
|
||||
|
||||
---
|
||||
|
||||
## 安全性
|
||||
|
||||
- [ ] **加密随机数**:密钥使用 `crypto/rand`,不使用 `math/rand` → [go-defensive](../go-defensive/SKILL.md)
|
||||
- [ ] **不 panic**:常规错误处理使用 error 返回;仅在真正特殊的情况下 panic → [go-defensive](../go-defensive/SKILL.md)
|
||||
|
||||
---
|
||||
|
||||
## 声明与初始化
|
||||
|
||||
- [ ] **分组相似的**:相关的 `var`/`const`/`type` 放在括号块中;不相关的分开 → [go-declarations](../go-declarations/SKILL.md)
|
||||
- [ ] **var vs :=**:有意使用零值时用 `var`;显式赋值时用 `:=` → [go-declarations](../go-declarations/SKILL.md)
|
||||
- [ ] **缩小作用域**:将声明移到使用位置附近;使用 if-init 限制变量作用域 → [go-declarations](../go-declarations/SKILL.md)
|
||||
- [ ] **Struct 初始化**:始终使用字段名;省略零值字段;零值 struct 使用 `var` → [go-declarations](../go-declarations/SKILL.md)
|
||||
- [ ] **使用 `any`**:新代码中优先使用 `any` 而非 `interface{}` → [go-declarations](../go-declarations/SKILL.md)
|
||||
|
||||
---
|
||||
|
||||
## 函数
|
||||
|
||||
- [ ] **文件排序**:类型 → 构造函数 → 导出方法 → 未导出方法 → 工具函数 → [go-functions](../go-functions/SKILL.md)
|
||||
- [ ] **签名格式化**:换行时所有参数各占一行并带尾逗号 → [go-functions](../go-functions/SKILL.md)
|
||||
- [ ] **裸参数**:为含义不明确的 bool/int 参数添加 `/* name */` 注释,或使用自定义类型 → [go-functions](../go-functions/SKILL.md)
|
||||
- [ ] **Printf 命名**:接受格式字符串的函数以 `f` 结尾,以便 `go vet` 检查 → [go-functions](../go-functions/SKILL.md)
|
||||
|
||||
---
|
||||
|
||||
## 风格
|
||||
|
||||
- [ ] **行长度**:无硬性限制,但避免令人不适的长行;按语义断行,而非任意长度 → [go-style-core](../go-style-core/SKILL.md)
|
||||
- [ ] **裸返回**:仅在短函数中使用;中/大函数使用显式返回 → [go-style-core](../go-style-core/SKILL.md)
|
||||
- [ ] **传值**:不要仅为节省字节而使用指针;小的固定大小类型传 `string` 而非 `*string` → [go-performance](../go-performance/SKILL.md)
|
||||
- [ ] **字符串拼接**:简单拼接用 `+`;格式化用 `fmt.Sprintf`;循环中用 `strings.Builder` → [go-performance](../go-performance/SKILL.md)
|
||||
|
||||
---
|
||||
|
||||
## 日志
|
||||
|
||||
- [ ] **使用 slog**:新代码使用 `log/slog`,不使用 `log` 或 `fmt.Println` 进行运维日志记录 → [go-logging](../go-logging/SKILL.md)
|
||||
- [ ] **结构化字段**:日志消息使用静态字符串加键值属性,不使用 fmt.Sprintf → [go-logging](../go-logging/SKILL.md)
|
||||
- [ ] **适当的级别**:Debug 用于开发者追踪,Info 用于重要事件,Warn 用于可恢复的问题,Error 用于故障 → [go-logging](../go-logging/SKILL.md)
|
||||
- [ ] **日志中无敏感信息**:PII、凭证和令牌永远不记录在日志中 → [go-logging](../go-logging/SKILL.md)
|
||||
|
||||
---
|
||||
|
||||
## 导入
|
||||
|
||||
- [ ] **导入分组**:标准库优先,然后空行,再外部包 → [go-packages](../go-packages/SKILL.md)
|
||||
- [ ] **导入重命名**:除非冲突否则避免重命名;冲突时重命名本地/项目特定的导入 → [go-packages](../go-packages/SKILL.md)
|
||||
- [ ] **空白导入**:`import _ "pkg"` 仅在 main 包或测试中使用 → [go-packages](../go-packages/SKILL.md)
|
||||
- [ ] **点导入**:仅在测试中用于解决循环依赖 → [go-packages](../go-packages/SKILL.md)
|
||||
|
||||
---
|
||||
|
||||
## 泛型
|
||||
|
||||
- [ ] **何时使用**:仅当多个类型共享相同逻辑且接口不足时 → [go-generics](../go-generics/SKILL.md)
|
||||
- [ ] **类型别名**:使用定义创建新类型;别名仅用于包迁移 → [go-generics](../go-generics/SKILL.md)
|
||||
|
||||
---
|
||||
|
||||
## 测试
|
||||
|
||||
- [ ] **示例**:包含可运行的 `Example` 函数或演示用法的测试 → [go-documentation](../go-documentation/SKILL.md)
|
||||
- [ ] **有用的测试失败信息**:消息包含出了什么错、输入、实际值和期望值;顺序为 `got != want` → [go-testing](../go-testing/SKILL.md)
|
||||
- [ ] **TestMain**:仅当所有测试都需要带清理的公共设置时使用;优先使用作用域化的 helper → [go-testing](../go-testing/SKILL.md)
|
||||
- [ ] **真实传输**:优先使用 `httptest.NewServer` + 真实客户端而非 mock HTTP → [go-testing](../go-testing/SKILL.md)
|
||||
|
||||
---
|
||||
|
||||
## 自动化检查
|
||||
|
||||
运行自动化预审查检查:
|
||||
|
||||
```bash
|
||||
bash scripts/pre-review.sh ./... # 文本输出
|
||||
bash scripts/pre-review.sh --json ./... # 结构化 JSON 输出
|
||||
```
|
||||
|
||||
或手动:`gofmt -l <path> && go vet ./... && golangci-lint run ./...`
|
||||
|
||||
在进入上述清单之前修复所有问题。有关 linter 设置和配置,请参阅 [go-linting](../go-linting/SKILL.md)。
|
||||
|
||||
---
|
||||
|
||||
## 综合示例
|
||||
|
||||
> 在构建生产级 HTTP 服务器并希望验证代码是否正确应用了并发、错误处理、context、文档和命名规范时,阅读 [references/WEB-SERVER.md](references/WEB-SERVER.md)。
|
||||
|
||||
---
|
||||
|
||||
## 相关 Skill
|
||||
|
||||
- **风格基础**:在解决格式化争议或应用"清晰 > 简单 > 简洁"优先级时,请参阅 [go-style-core](../go-style-core/SKILL.md)
|
||||
- **Linting 设置**:在配置 golangci-lint 或将自动化检查添加到 CI 时,请参阅 [go-linting](../go-linting/SKILL.md)
|
||||
- **错误策略**:在审查错误包装、哨兵错误或 handle-once 模式时,请参阅 [go-error-handling](../go-error-handling/SKILL.md)
|
||||
- **命名规范**:在评估标识符名称、接收器名称或包-符号重复时,请参阅 [go-naming](../go-naming/SKILL.md)
|
||||
- **测试模式**:在审查表驱动结构、失败消息或 helper 使用的测试代码时,请参阅 [go-testing](../go-testing/SKILL.md)
|
||||
- **并发安全**:在审查 goroutine 生命周期、channel 使用或互斥锁放置时,请参阅 [go-concurrency](../go-concurrency/SKILL.md)
|
||||
- **日志实践**:在审查日志使用、结构化日志或 slog 配置时,请参阅 [go-logging](../go-logging/SKILL.md)
|
||||
@@ -0,0 +1,23 @@
|
||||
# Code Review: [PR Title]
|
||||
|
||||
## Summary
|
||||
[Brief description of the changes]
|
||||
|
||||
## Findings
|
||||
|
||||
### Must Fix
|
||||
- [ ] [file:line] Description of critical issue
|
||||
|
||||
### Should Fix
|
||||
- [ ] [file:line] Description of recommended improvement
|
||||
|
||||
### Nits
|
||||
- [ ] [file:line] Description of minor suggestion
|
||||
|
||||
## Automated Checks
|
||||
- [ ] `gofmt -d .` — clean
|
||||
- [ ] `go vet ./...` — clean
|
||||
- [ ] `golangci-lint run` — clean
|
||||
|
||||
## Skills Applied
|
||||
[List of go-* skills referenced during review]
|
||||
@@ -0,0 +1,119 @@
|
||||
# Web 服务器:Skill 的综合应用
|
||||
|
||||
本示例展示 Go skill 如何在真实的 HTTP 服务器中协同应用。每个部分
|
||||
引用相关的 skill 以获取详细指导。
|
||||
|
||||
## 结构
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"time"
|
||||
)
|
||||
|
||||
// --- 接口(go-interfaces) ---
|
||||
|
||||
// Store 定义了数据访问边界。定义在消费方包中,
|
||||
// 而非实现方包中。
|
||||
type Store interface {
|
||||
GetUser(ctx context.Context, id string) (*User, error)
|
||||
}
|
||||
|
||||
// --- 类型与构造函数(go-naming、go-declarations) ---
|
||||
|
||||
// Server 处理用户 API 的 HTTP 请求。
|
||||
type Server struct {
|
||||
store Store
|
||||
router *http.ServeMux
|
||||
}
|
||||
|
||||
// NewServer 使用给定的依赖创建 Server。
|
||||
// 调用者必须调用 Shutdown 来释放资源。
|
||||
func NewServer(store Store) *Server {
|
||||
s := &Server{store: store}
|
||||
s.router = http.NewServeMux()
|
||||
s.router.HandleFunc("GET /users/{id}", s.handleGetUser)
|
||||
return s
|
||||
}
|
||||
|
||||
// --- 错误处理(go-error-handling) ---
|
||||
|
||||
// 领域错误作为哨兵 —— 使用 errors.Is 进行检查。
|
||||
var ErrNotFound = errors.New("not found")
|
||||
|
||||
// --- HTTP 处理器(go-control-flow、go-context、go-error-handling) ---
|
||||
|
||||
func (s *Server) handleGetUser(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context() // go-context:从 request 派生
|
||||
id := r.PathValue("id")
|
||||
|
||||
user, err := s.store.GetUser(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrNotFound) { // go-error-handling:errors.Is
|
||||
http.Error(w, "user not found", http.StatusNotFound)
|
||||
return // go-control-flow:提前返回
|
||||
}
|
||||
// HTTP 处理器是"记录或返回"规则的例外:在服务端记录详细信息,向客户端返回脱敏错误。
|
||||
slog.Error("GetUser failed", "id", id, "err", err)
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(user)
|
||||
}
|
||||
|
||||
// --- 优雅关闭(go-concurrency、go-defensive) ---
|
||||
|
||||
func main() {
|
||||
store := NewDBStore(os.Getenv("DATABASE_URL"))
|
||||
srv := NewServer(store)
|
||||
|
||||
httpSrv := &http.Server{
|
||||
Addr: ":8080",
|
||||
Handler: srv.router,
|
||||
ReadTimeout: 5 * time.Second, // go-defensive:使用 time.Duration
|
||||
WriteTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
// go-concurrency:goroutine 生命周期清晰
|
||||
go func() {
|
||||
sigCh := make(chan os.Signal, 1) // go-concurrency:channel 大小为 1
|
||||
signal.Notify(sigCh, os.Interrupt)
|
||||
<-sigCh
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel() // go-defensive:defer 清理
|
||||
httpSrv.Shutdown(ctx)
|
||||
}()
|
||||
|
||||
slog.Info("starting server", "addr", httpSrv.Addr)
|
||||
if err := httpSrv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
|
||||
slog.Error("server error", "err", err)
|
||||
os.Exit(1) // go-packages:仅在 main 中退出
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 应用的 Skill
|
||||
|
||||
| 领域 | Skill | 演示内容 |
|
||||
|------|-------|----------|
|
||||
| 接口在消费方 | [go-interfaces](../../go-interfaces/SKILL.md) | `Store` 在使用处定义 |
|
||||
| 命名 | [go-naming](../../go-naming/SKILL.md) | MixedCaps、接收器缩写、清晰的函数名 |
|
||||
| 错误处理 | [go-error-handling](../../go-error-handling/SKILL.md) | 哨兵错误、`errors.Is`、记录或返回 |
|
||||
| Context | [go-context](../../go-context/SKILL.md) | 从 request 派生,逐层传递 |
|
||||
| 控制流 | [go-control-flow](../../go-control-flow/SKILL.md) | 错误情况的提前返回 |
|
||||
| 并发 | [go-concurrency](../../go-concurrency/SKILL.md) | 清晰的 goroutine 生命周期、channel 大小 |
|
||||
| 防御性 | [go-defensive](../../go-defensive/SKILL.md) | `defer cancel()`、`time.Duration`、优雅关闭 |
|
||||
| 包管理 | [go-packages](../../go-packages/SKILL.md) | 仅在 `main()` 中退出 |
|
||||
| 日志 | [go-error-handling](../../go-error-handling/SKILL.md) | 结构化 slog,错误只处理一次 |
|
||||
+246
@@ -0,0 +1,246 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="1.0.0"
|
||||
SCRIPT_NAME="$(basename "$0")"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
$SCRIPT_NAME v$VERSION — Run automated pre-review checks on Go code
|
||||
|
||||
USAGE
|
||||
bash $SCRIPT_NAME [options] [path]
|
||||
|
||||
DESCRIPTION
|
||||
Runs gofmt, go vet, and golangci-lint against the target path and
|
||||
reports any findings. Use before manual code review to catch
|
||||
mechanical issues early.
|
||||
|
||||
Exits 0 if all checks pass, 1 if issues found, 2 on error.
|
||||
|
||||
OPTIONS
|
||||
-h, --help Show this help message
|
||||
-v, --version Show version
|
||||
--json Output results as JSON
|
||||
--force Run even if golangci-lint is not installed (skip it)
|
||||
--limit N Max items reported per section (0 = unlimited, default: 0)
|
||||
|
||||
ARGUMENTS
|
||||
path Package pattern to check (default: ./...)
|
||||
|
||||
EXAMPLES
|
||||
bash $SCRIPT_NAME
|
||||
bash $SCRIPT_NAME ./pkg/...
|
||||
bash $SCRIPT_NAME --json ./cmd/server/...
|
||||
bash $SCRIPT_NAME --force ./...
|
||||
bash $SCRIPT_NAME --json --limit 10 ./...
|
||||
EOF
|
||||
}
|
||||
|
||||
json_escape() {
|
||||
local s="$1"
|
||||
s="${s//\\/\\\\}"
|
||||
s="${s//\"/\\\"}"
|
||||
s="${s//$'\t'/\\t}"
|
||||
s="${s//$'\r'/}"
|
||||
s="${s//$'\n'/\\n}"
|
||||
printf '%s' "$s"
|
||||
}
|
||||
|
||||
JSON_OUTPUT=false
|
||||
FORCE=false
|
||||
LIMIT=0
|
||||
TARGET=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help) usage; exit 0 ;;
|
||||
-v|--version) echo "$SCRIPT_NAME v$VERSION"; exit 0 ;;
|
||||
--json) JSON_OUTPUT=true; shift ;;
|
||||
--force) FORCE=true; shift ;;
|
||||
--limit) LIMIT="${2:?error: --limit requires a number}"; shift 2 ;;
|
||||
-*) echo "error: unknown option: $1" >&2; usage >&2; exit 2 ;;
|
||||
*) TARGET="$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
TARGET="${TARGET:-./...}"
|
||||
|
||||
if ! command -v go &>/dev/null; then
|
||||
echo "error: go is not installed or not in PATH" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if ! command -v gofmt &>/dev/null; then
|
||||
echo "error: gofmt is not installed or not in PATH" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
GOFMT_STATUS="pass"
|
||||
GOFMT_FINDINGS=()
|
||||
GOFMT_DIR="${TARGET%%/...}"
|
||||
GOFMT_DIR="${GOFMT_DIR:-.}"
|
||||
UNFORMATTED=$(gofmt -l "$GOFMT_DIR" 2>&1) || true
|
||||
if [[ -n "$UNFORMATTED" ]]; then
|
||||
GOFMT_STATUS="fail"
|
||||
while IFS= read -r f; do
|
||||
[[ -n "$f" ]] && GOFMT_FINDINGS+=("$f")
|
||||
done <<< "$UNFORMATTED"
|
||||
fi
|
||||
|
||||
GOVET_STATUS="pass"
|
||||
GOVET_OUTPUT=""
|
||||
if ! GOVET_OUTPUT=$(go vet "$TARGET" 2>&1); then
|
||||
GOVET_STATUS="fail"
|
||||
fi
|
||||
|
||||
LINT_STATUS="skip"
|
||||
LINT_OUTPUT=""
|
||||
if command -v golangci-lint &>/dev/null; then
|
||||
LINT_STATUS="pass"
|
||||
if ! LINT_OUTPUT=$(golangci-lint run "$TARGET" 2>&1); then
|
||||
LINT_STATUS="fail"
|
||||
fi
|
||||
elif ! $FORCE; then
|
||||
echo "error: golangci-lint not installed (use --force to skip)" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
FAILED=0
|
||||
[[ "$GOFMT_STATUS" == "fail" ]] && FAILED=1
|
||||
[[ "$GOVET_STATUS" == "fail" ]] && FAILED=1
|
||||
[[ "$LINT_STATUS" == "fail" ]] && FAILED=1
|
||||
|
||||
if $JSON_OUTPUT; then
|
||||
GOFMT_TRUNCATED=false
|
||||
GOFMT_DISPLAY=("${GOFMT_FINDINGS[@]+"${GOFMT_FINDINGS[@]}"}")
|
||||
if [[ $LIMIT -gt 0 && ${#GOFMT_DISPLAY[@]} -gt $LIMIT ]]; then
|
||||
GOFMT_DISPLAY=("${GOFMT_FINDINGS[@]:0:$LIMIT}")
|
||||
GOFMT_TRUNCATED=true
|
||||
fi
|
||||
|
||||
GOFMT_JSON="["
|
||||
first=true
|
||||
for f in "${GOFMT_DISPLAY[@]+"${GOFMT_DISPLAY[@]}"}"; do
|
||||
$first || GOFMT_JSON+=","
|
||||
first=false
|
||||
GOFMT_JSON+="\"$(json_escape "$f")\""
|
||||
done
|
||||
GOFMT_JSON+="]"
|
||||
|
||||
GOVET_TRUNCATED=false
|
||||
GOVET_DISPLAY="$GOVET_OUTPUT"
|
||||
if [[ $LIMIT -gt 0 && -n "$GOVET_OUTPUT" ]]; then
|
||||
GOVET_ARR=()
|
||||
while IFS= read -r line; do
|
||||
GOVET_ARR+=("$line")
|
||||
done <<< "$GOVET_OUTPUT"
|
||||
if [[ ${#GOVET_ARR[@]} -gt $LIMIT ]]; then
|
||||
GOVET_DISPLAY=""
|
||||
for (( i=0; i<LIMIT; i++ )); do
|
||||
[[ -n "$GOVET_DISPLAY" ]] && GOVET_DISPLAY+=$'\n'
|
||||
GOVET_DISPLAY+="${GOVET_ARR[$i]}"
|
||||
done
|
||||
GOVET_TRUNCATED=true
|
||||
fi
|
||||
fi
|
||||
GOVET_ESC="$(json_escape "$GOVET_DISPLAY")"
|
||||
|
||||
LINT_TRUNCATED=false
|
||||
LINT_DISPLAY="$LINT_OUTPUT"
|
||||
if [[ $LIMIT -gt 0 && -n "$LINT_OUTPUT" ]]; then
|
||||
LINT_ARR=()
|
||||
while IFS= read -r line; do
|
||||
LINT_ARR+=("$line")
|
||||
done <<< "$LINT_OUTPUT"
|
||||
if [[ ${#LINT_ARR[@]} -gt $LIMIT ]]; then
|
||||
LINT_DISPLAY=""
|
||||
for (( i=0; i<LIMIT; i++ )); do
|
||||
[[ -n "$LINT_DISPLAY" ]] && LINT_DISPLAY+=$'\n'
|
||||
LINT_DISPLAY+="${LINT_ARR[$i]}"
|
||||
done
|
||||
LINT_TRUNCATED=true
|
||||
fi
|
||||
fi
|
||||
LINT_ESC="$(json_escape "$LINT_DISPLAY")"
|
||||
|
||||
GOFMT_TRUNC=""
|
||||
$GOFMT_TRUNCATED && GOFMT_TRUNC=',"truncated":true'
|
||||
GOVET_TRUNC=""
|
||||
$GOVET_TRUNCATED && GOVET_TRUNC=',"truncated":true'
|
||||
LINT_TRUNC=""
|
||||
$LINT_TRUNCATED && LINT_TRUNC=',"truncated":true'
|
||||
|
||||
cat <<EOF
|
||||
{"gofmt":{"status":"$GOFMT_STATUS","files":$GOFMT_JSON$GOFMT_TRUNC},"govet":{"status":"$GOVET_STATUS","output":"$GOVET_ESC"$GOVET_TRUNC},"golangci_lint":{"status":"$LINT_STATUS","output":"$LINT_ESC"$LINT_TRUNC},"passed":$( [[ $FAILED -eq 0 ]] && echo true || echo false )}
|
||||
EOF
|
||||
else
|
||||
echo "=== gofmt ==="
|
||||
if [[ "$GOFMT_STATUS" == "fail" ]]; then
|
||||
echo "Unformatted files:"
|
||||
GOFMT_COUNT=0
|
||||
for f in "${GOFMT_FINDINGS[@]}"; do
|
||||
GOFMT_COUNT=$((GOFMT_COUNT + 1))
|
||||
if [[ $LIMIT -gt 0 && $GOFMT_COUNT -gt $LIMIT ]]; then
|
||||
echo " ... ($(( ${#GOFMT_FINDINGS[@]} - LIMIT )) more items truncated)"
|
||||
break
|
||||
fi
|
||||
echo " $f"
|
||||
done
|
||||
else
|
||||
echo "OK"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== go vet ==="
|
||||
if [[ "$GOVET_STATUS" == "fail" ]]; then
|
||||
if [[ $LIMIT -gt 0 ]]; then
|
||||
GOVET_ARR=()
|
||||
while IFS= read -r line; do
|
||||
GOVET_ARR+=("$line")
|
||||
done <<< "$GOVET_OUTPUT"
|
||||
for (( i=0; i<${#GOVET_ARR[@]} && i<LIMIT; i++ )); do
|
||||
echo "${GOVET_ARR[$i]}"
|
||||
done
|
||||
if [[ ${#GOVET_ARR[@]} -gt $LIMIT ]]; then
|
||||
echo "... ($(( ${#GOVET_ARR[@]} - LIMIT )) more items truncated)"
|
||||
fi
|
||||
else
|
||||
echo "$GOVET_OUTPUT"
|
||||
fi
|
||||
else
|
||||
echo "OK"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== golangci-lint ==="
|
||||
if [[ "$LINT_STATUS" == "skip" ]]; then
|
||||
echo "Skipped (not installed)"
|
||||
elif [[ "$LINT_STATUS" == "fail" ]]; then
|
||||
if [[ $LIMIT -gt 0 ]]; then
|
||||
LINT_ARR=()
|
||||
while IFS= read -r line; do
|
||||
LINT_ARR+=("$line")
|
||||
done <<< "$LINT_OUTPUT"
|
||||
for (( i=0; i<${#LINT_ARR[@]} && i<LIMIT; i++ )); do
|
||||
echo "${LINT_ARR[$i]}"
|
||||
done
|
||||
if [[ ${#LINT_ARR[@]} -gt $LIMIT ]]; then
|
||||
echo "... ($(( ${#LINT_ARR[@]} - LIMIT )) more items truncated)"
|
||||
fi
|
||||
else
|
||||
echo "$LINT_OUTPUT"
|
||||
fi
|
||||
else
|
||||
echo "OK"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
if [[ $FAILED -eq 1 ]]; then
|
||||
echo "Pre-review checks FAILED — fix issues before manual review."
|
||||
else
|
||||
echo "All pre-review checks passed."
|
||||
fi
|
||||
fi
|
||||
|
||||
exit $FAILED
|
||||
@@ -0,0 +1,191 @@
|
||||
---
|
||||
name: go-concurrency
|
||||
description: Use when writing concurrent Go code — goroutines, channels, mutexes, or thread-safety guarantees. Also use when parallelizing work, fixing data races, or protecting shared state, even if the user doesn't explicitly mention concurrency primitives. Does not cover context.Context patterns (see go-context).
|
||||
license: Apache-2.0
|
||||
compatibility: Requires go.uber.org/atomic for atomic operation wrappers
|
||||
metadata:
|
||||
sources: "Effective Go, Google Style Guide, Uber Style Guide"
|
||||
---
|
||||
|
||||
# Go 并发
|
||||
|
||||
## Goroutine 生命周期
|
||||
|
||||
> **规范**:当你启动 goroutine 时,要明确它们何时或是否退出。
|
||||
|
||||
Goroutine 可能因阻塞在 channel 的发送/接收上而泄漏。GC **不会终止**被阻塞的 goroutine,即使没有其他 goroutine 持有对该 channel 的引用。即使不泄漏的在途 goroutine 也会导致 panic(在已关闭的 channel 上发送)、数据竞争、内存问题和资源泄漏。
|
||||
|
||||
### 核心规则
|
||||
|
||||
1. **每个 goroutine 都需要停止机制** —— 可预测的结束时间、取消信号,或两者兼有
|
||||
2. **代码必须能够等待** goroutine 完成
|
||||
3. **不在 `init()` 中启动 goroutine** —— 改为暴露生命周期方法(`Close`、`Stop`、`Shutdown`)
|
||||
4. **保持同步作用域化** —— 限制在函数作用域内,将逻辑分解为同步函数
|
||||
|
||||
```go
|
||||
// 好:使用 WaitGroup 明确生命周期
|
||||
var wg sync.WaitGroup
|
||||
for item := range queue {
|
||||
wg.Add(1)
|
||||
go func() { defer wg.Done(); process(ctx, item) }()
|
||||
}
|
||||
wg.Wait()
|
||||
```
|
||||
|
||||
```go
|
||||
// 不好:无法停止或等待
|
||||
go func() { for { flush(); time.Sleep(delay) } }()
|
||||
```
|
||||
|
||||
使用 [go.uber.org/goleak](https://pkg.go.dev/go.uber.org/goleak) **检测泄漏**。
|
||||
|
||||
> **原则**:永远不要在不知道 goroutine 将如何停止的情况下启动它。
|
||||
|
||||
> 在实现 stop/done channel 模式、goroutine 等待策略或
|
||||
> 生命周期管理的 worker 时,阅读 [references/GOROUTINE-PATTERNS.md](references/GOROUTINE-PATTERNS.md)。
|
||||
|
||||
---
|
||||
|
||||
## 通过通信共享
|
||||
|
||||
> "不要通过共享内存来通信;而是通过通信来共享内存。"
|
||||
|
||||
这是 Go 并发设计的基础原则。使用 **channel** 进行所有权转移和协调 —— 当一个 goroutine 生产值,另一个消费它时使用。当多个 goroutine 访问共享状态且 channel 会增加不必要的复杂性时,使用 **互斥锁**。
|
||||
|
||||
**默认使用 channel。** 当问题本质上是保护共享数据结构(例如缓存或计数器)而非在 goroutine 之间传递数据时,退回到 `sync.Mutex` / `sync.RWMutex`。
|
||||
|
||||
---
|
||||
|
||||
## 同步函数
|
||||
|
||||
> **规范**:优先使用同步函数而非异步函数。
|
||||
|
||||
| 优势 | 原因 |
|
||||
|---|---|
|
||||
| 局部化 goroutine | 生命周期更容易推理 |
|
||||
| 避免泄漏和竞争 | 更容易防止资源泄漏和数据竞争 |
|
||||
| 更容易测试 | 直接检查输入/输出,无需轮询 |
|
||||
| 调用方灵活性 | 调用方在需要时添加并发 |
|
||||
|
||||
> **建议**:在调用方移除不必要的并发是相当困难的(有时是不可能的)。让调用方在需要时添加并发。
|
||||
|
||||
> 在编写同步优先的 API(调用方可以将其包装在 goroutine 中)时,
|
||||
> 阅读 [references/GOROUTINE-PATTERNS.md](references/GOROUTINE-PATTERNS.md)。
|
||||
|
||||
---
|
||||
|
||||
## 零值互斥锁
|
||||
|
||||
`sync.Mutex` 和 `sync.RWMutex` 的零值是有效的 —— 几乎不需要互斥锁的指针。
|
||||
|
||||
```go
|
||||
// 好:零值有效 // 不好:不必要的指针
|
||||
var mu sync.Mutex mu := new(sync.Mutex)
|
||||
```
|
||||
|
||||
**不要嵌入互斥锁** —— 使用命名的 `mu` 字段,使 `Lock`/`Unlock` 保持为实现细节,而非导出的 API。
|
||||
|
||||
> 在实现互斥锁保护的 struct 或决定如何组织互斥锁字段时,
|
||||
> 阅读 [references/SYNC-PRIMITIVES.md](references/SYNC-PRIMITIVES.md)。
|
||||
|
||||
---
|
||||
|
||||
## Channel 方向
|
||||
|
||||
> **规范**:尽可能指定 channel 方向。
|
||||
|
||||
方向可以防止错误(编译器会捕获对仅接收 channel 的关闭操作),传达所有权,并且具有自文档化效果。
|
||||
|
||||
```go
|
||||
func produce(out chan<- int) { /* 仅发送 */ }
|
||||
func consume(in <-chan int) { /* 仅接收 */ }
|
||||
func transform(in <-chan int, out chan<- int) { /* 双向 */ }
|
||||
```
|
||||
|
||||
### Channel 大小:一或零
|
||||
|
||||
Channel 的大小应为 **零**(无缓冲)或 **一**。其他任何大小都需要给出理由:
|
||||
|
||||
- 大小是如何确定的
|
||||
- 什么机制防止 channel 在负载下填满
|
||||
- 当写入者阻塞时会发生什么
|
||||
|
||||
```go
|
||||
c := make(chan int) // 无缓冲 —— 好
|
||||
c := make(chan int, 1) // 大小为 1 —— 好
|
||||
c := make(chan int, 64) // 任意大小 —— 需要给出理由
|
||||
```
|
||||
|
||||
> 在审查详细的 channel 方向示例及易出错模式时,
|
||||
> 阅读 [references/SYNC-PRIMITIVES.md](references/SYNC-PRIMITIVES.md)。
|
||||
|
||||
---
|
||||
|
||||
## 原子操作
|
||||
|
||||
使用 `atomic.Bool`、`atomic.Int64` 等(Go 1.19 起标准库 `sync/atomic` 提供,或 [go.uber.org/atomic](https://pkg.go.dev/go.uber.org/atomic))进行类型安全的原子操作。原始的 `int32`/`int64` 字段容易在某些代码路径上忘记原子访问。
|
||||
|
||||
```go
|
||||
// 好:类型安全 // 不好:容易忘记
|
||||
var running atomic.Bool var running int32 // 原子操作
|
||||
running.Store(true) atomic.StoreInt32(&running, 1)
|
||||
running.Load() running == 1 // 竞争!
|
||||
```
|
||||
|
||||
> 在 sync/atomic 和 go.uber.org/atomic 之间选择,或在 struct 中实现原子
|
||||
> 状态标志时,阅读 [references/SYNC-PRIMITIVES.md](references/SYNC-PRIMITIVES.md)。
|
||||
|
||||
---
|
||||
|
||||
## 并发文档
|
||||
|
||||
> **建议**:当线程安全性从操作类型不明显时,添加文档说明。
|
||||
|
||||
Go 用户假设只读操作可以安全地并发使用,而修改操作则不行。在以下情况添加并发文档:
|
||||
|
||||
1. **读取与修改不明确** —— 例如,会修改 LRU 状态的 `Lookup`
|
||||
2. **API 提供同步** —— 例如,线程安全的客户端
|
||||
3. **接口有并发要求** —— 在类型定义中添加文档
|
||||
|
||||
---
|
||||
|
||||
## Context 使用
|
||||
|
||||
> 有关 context.Context 的指导(参数位置、struct 存储、自定义
|
||||
> 类型、派生模式),请参阅专门的
|
||||
> [go-context](../go-context/SKILL.md) skill。
|
||||
|
||||
---
|
||||
|
||||
## 使用 Channel 的缓冲池
|
||||
|
||||
使用有缓冲 channel 作为空闲列表来复用已分配的缓冲区。这种"泄漏缓冲"模式使用带 `default` 的 `select` 进行非阻塞操作。
|
||||
|
||||
> 在实现带可复用缓冲区的 worker pool 或在基于 channel 的池和
|
||||
> `sync.Pool` 之间选择时,阅读 [references/BUFFER-POOLING.md](references/BUFFER-POOLING.md)。
|
||||
|
||||
---
|
||||
|
||||
## 高级模式
|
||||
|
||||
> 在实现使用 channel 的 channel 进行请求-响应多路复用,或
|
||||
> 跨核心的 CPU 密集型并行计算时,阅读 [references/ADVANCED-PATTERNS.md](references/ADVANCED-PATTERNS.md)。
|
||||
|
||||
---
|
||||
|
||||
## 相关 Skill
|
||||
|
||||
- **Context 传播**:在通过 goroutine 传递取消、截止时间或请求作用域值时,请参阅 [go-context](../go-context/SKILL.md)
|
||||
- **错误处理**:在从 goroutine 传播错误或使用 errgroup 时,请参阅 [go-error-handling](../go-error-handling/SKILL.md)
|
||||
- **防御性加固**:在 API 边界保护共享状态或使用 defer 清理时,请参阅 [go-defensive](../go-defensive/SKILL.md)
|
||||
- **接口设计**:在为包含 sync 原语的类型选择接收器类型时,请参阅 [go-interfaces](../go-interfaces/SKILL.md)
|
||||
|
||||
### 外部资源
|
||||
|
||||
- [永远不要在不知道 goroutine 将如何停止的情况下启动它](https://dave.cheney.net/2016/12/22/never-start-a-goroutine-without-knowing-how-it-will-stop)
|
||||
—— Dave Cheney
|
||||
- [重新思考经典并发模式](https://www.youtube.com/watch?v=5zXAHh5tJqQ) —— Bryan Mills
|
||||
(GopherCon 2018)
|
||||
- [Go 程序何时结束](https://changelog.com/gotime/165) —— Go Time 播客
|
||||
- [go.uber.org/goleak](https://pkg.go.dev/go.uber.org/goleak) —— 用于测试的 Goroutine 泄漏检测器
|
||||
- [go.uber.org/atomic](https://pkg.go.dev/go.uber.org/atomic) —— 类型安全的原子操作
|
||||
@@ -0,0 +1,132 @@
|
||||
# 高级并发模式
|
||||
|
||||
来自 Effective Go 的高级并发模式详细参考。这些模式适用于特定场景 —— 在需要请求/响应多路复用或 CPU 密集型并行化时使用。
|
||||
|
||||
---
|
||||
|
||||
## Channel 的 Channel
|
||||
|
||||
> **来源**:Effective Go
|
||||
|
||||
Channel 是一等公民值,可以像其他值一样被分配和传递。一个强大的模式是在请求结构体中嵌入 **回复 channel**,让每个客户端提供自己的应答路径:
|
||||
|
||||
```go
|
||||
type Request struct {
|
||||
args []int
|
||||
f func([]int) int
|
||||
resultChan chan int
|
||||
}
|
||||
```
|
||||
|
||||
客户端发送一个包含函数、参数和接收结果 channel 的请求:
|
||||
|
||||
```go
|
||||
request := &Request{[]int{3, 4, 5}, sum, make(chan int)}
|
||||
clientRequests <- request
|
||||
fmt.Printf("answer: %d\n", <-request.resultChan)
|
||||
```
|
||||
|
||||
服务端处理器从队列中读取请求,并将结果发送回每个请求的回复 channel:
|
||||
|
||||
```go
|
||||
func handle(queue chan *Request) {
|
||||
for req := range queue {
|
||||
req.resultChan <- req.f(req.args)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这个模式构成了限速、并行、非阻塞 RPC 系统的基础,无需任何互斥锁。
|
||||
|
||||
---
|
||||
|
||||
## CPU 密集型并行化
|
||||
|
||||
> **来源**:Effective Go(现代化版本)
|
||||
|
||||
当计算可以分解为独立的部分时,使用 `sync.WaitGroup` 等待完成,将其并行化到多个 CPU 核心上:
|
||||
|
||||
```go
|
||||
type Vector []float64
|
||||
|
||||
func (v Vector) DoSome(i, n int, u Vector) {
|
||||
for ; i < n; i++ {
|
||||
v[i] += u.Op(v[i])
|
||||
}
|
||||
}
|
||||
|
||||
func (v Vector) DoAll(u Vector) {
|
||||
numCPU := runtime.NumCPU()
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(numCPU)
|
||||
for i := 0; i < numCPU; i++ {
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
v.DoSome(i*len(v)/numCPU, (i+1)*len(v)/numCPU, u)
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
```
|
||||
|
||||
使用 `runtime.NumCPU()` 获取硬件核心数,或使用 `runtime.GOMAXPROCS(0)` 以遵循用户的资源配置。
|
||||
|
||||
> **重要**:不要混淆并发(将程序组织为独立执行的组件)和并行(在多个 CPU 上同时执行计算)。Go 是一门并发语言;并非所有并行化问题都适合它的模型。
|
||||
|
||||
---
|
||||
|
||||
## 常见错误
|
||||
|
||||
### 忘记通知完成
|
||||
|
||||
如果 goroutine 从未调用 `wg.Done()`(或从未在 done channel 上发送),等待的 goroutine 将永远阻塞:
|
||||
|
||||
```go
|
||||
// 不好:缺少 wg.Done —— 死锁
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
doWork()
|
||||
}()
|
||||
wg.Wait()
|
||||
|
||||
// 好:始终 defer wg.Done
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
doWork()
|
||||
}()
|
||||
wg.Wait()
|
||||
```
|
||||
|
||||
### 无限制的 goroutine 创建
|
||||
|
||||
为每个工作项无限制地启动 goroutine 可能会耗尽内存或压垮下游资源。使用信号量来限制并发数:
|
||||
|
||||
```go
|
||||
// 不好:一次性创建 len(items) 个 goroutine
|
||||
var wg sync.WaitGroup
|
||||
for _, item := range items {
|
||||
wg.Add(1)
|
||||
go func(it Item) {
|
||||
defer wg.Done()
|
||||
process(it)
|
||||
}(item)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// 好:信号量将并发限制为 maxWorkers
|
||||
var wg sync.WaitGroup
|
||||
sem := make(chan struct{}, maxWorkers)
|
||||
for _, item := range items {
|
||||
wg.Add(1)
|
||||
sem <- struct{}{}
|
||||
go func(it Item) {
|
||||
defer wg.Done()
|
||||
defer func() { <-sem }()
|
||||
process(it)
|
||||
}(item)
|
||||
}
|
||||
wg.Wait()
|
||||
```
|
||||
@@ -0,0 +1,73 @@
|
||||
# 使用 Channel 的缓冲池
|
||||
|
||||
使用有缓冲 channel 作为空闲列表来复用已分配的缓冲区,避免重复分配。这种"泄漏缓冲"模式使用带 `default` 的 `select` 进行非阻塞操作。
|
||||
|
||||
> **来源**:Effective Go
|
||||
|
||||
```go
|
||||
var freeList = make(chan *Buffer, 100) // 有缓冲 channel 作为空闲列表
|
||||
|
||||
// 客户端:从空闲列表获取缓冲区或分配新的
|
||||
func getBuffer() *Buffer {
|
||||
select {
|
||||
case b := <-freeList:
|
||||
return b // 复用已有缓冲区
|
||||
default:
|
||||
return new(Buffer) // 空闲列表为空;分配新缓冲区
|
||||
}
|
||||
}
|
||||
|
||||
// 服务端:如有空间则将缓冲区归还空闲列表,否则丢弃
|
||||
func putBuffer(b *Buffer) {
|
||||
b.Reset() // 为重用做准备
|
||||
select {
|
||||
case freeList <- b:
|
||||
// 缓冲区已归还空闲列表
|
||||
default:
|
||||
// 空闲列表已满;丢弃缓冲区(GC 会回收)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 工作原理
|
||||
|
||||
1. **非阻塞接收**:客户端尝试从 `freeList` 获取缓冲区。如果为空,`default` 分支运行并分配新缓冲区。
|
||||
2. **非阻塞发送**:服务端尝试归还缓冲区。如果 `freeList` 已满,`default` 分支运行,缓冲区被丢弃等待垃圾回收。
|
||||
3. **有限内存**:channel 容量(100)限制了池中缓冲区的数量,防止无限增长。
|
||||
|
||||
当分配成本较高且缓冲区复用有益,但你不希望在池空或池满时出现阻塞行为时,这种模式非常有用。
|
||||
|
||||
## 何时使用
|
||||
|
||||
- 高频分配相似大小的对象
|
||||
- 分配开销影响性能的代码路径
|
||||
- 需要限制内存使用量的场景
|
||||
|
||||
## 生产环境替代方案
|
||||
|
||||
对于生产代码,考虑使用 `sync.Pool`,它提供类似功能并与垃圾收集器有更好的集成:
|
||||
|
||||
```go
|
||||
var bufferPool = sync.Pool{
|
||||
New: func() any {
|
||||
return new(Buffer)
|
||||
},
|
||||
}
|
||||
|
||||
func getBuffer() *Buffer {
|
||||
return bufferPool.Get().(*Buffer)
|
||||
}
|
||||
|
||||
func putBuffer(b *Buffer) {
|
||||
b.Reset()
|
||||
bufferPool.Put(b)
|
||||
}
|
||||
```
|
||||
|
||||
`sync.Pool` 的优势:
|
||||
- 垃圾收集期间自动清理
|
||||
- 无需管理池大小
|
||||
- 天生线程安全
|
||||
- 高并发下性能更好
|
||||
|
||||
基于 channel 的方式对于理解 Go 的并发原语以及需要更多控制池行为的场景仍然很有价值。
|
||||
@@ -0,0 +1,126 @@
|
||||
# Goroutine 生命周期模式
|
||||
|
||||
管理 goroutine 生命周期的详细模式 —— 确保每个 goroutine 都有清晰的启动/停止机制并防止资源泄漏。
|
||||
|
||||
---
|
||||
|
||||
## 使生命周期清晰
|
||||
|
||||
> WaitGroup 示例和作用域规则在父 skill(SKILL.md § Goroutine 生命周期,核心规则)中。本参考涵盖:stop/done channel 模式、等待策略、init() 生命周期示例和同步 API 设计。
|
||||
|
||||
---
|
||||
|
||||
## Stop/Done Channel 模式
|
||||
|
||||
每个 goroutine 必须有可预测的停止机制。使用 stop channel 通知关闭,使用 done channel 确认退出:
|
||||
|
||||
```go
|
||||
var (
|
||||
stop = make(chan struct{}) // 通知 goroutine 停止
|
||||
done = make(chan struct{}) // 通知我们 goroutine 已退出
|
||||
)
|
||||
go func() {
|
||||
defer close(done)
|
||||
ticker := time.NewTicker(delay)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
flush()
|
||||
case <-stop:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// 关闭时:
|
||||
close(stop) // 通知 goroutine 停止
|
||||
<-done // 并等待它退出
|
||||
```
|
||||
|
||||
在已关闭的 channel 上发送会 panic —— 始终使用 `close()` 来发信号,不要直接发送:
|
||||
|
||||
```go
|
||||
ch := make(chan int)
|
||||
close(ch)
|
||||
ch <- 13 // panic: 在已关闭的 channel 上发送
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 等待 Goroutine
|
||||
|
||||
> 多 goroutine 的 `sync.WaitGroup` 模式在父 skill 中(SKILL.md § Goroutine 生命周期)。以下是单 goroutine 的 done-channel 替代方案。
|
||||
|
||||
为单个 goroutine 使用 done channel:
|
||||
|
||||
```go
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
// 工作...
|
||||
}()
|
||||
<-done // 等待 goroutine 完成
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 不在 init() 中使用 Goroutine
|
||||
|
||||
> 核心规则在父 skill 中(SKILL.md § 核心规则,规则 3)。以下是展示生命周期管理的扩展示例。
|
||||
|
||||
```go
|
||||
// 不好:创建了不可控的后台 goroutine
|
||||
func init() {
|
||||
go doWork()
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
// 好:显式的生命周期管理
|
||||
type Worker struct {
|
||||
stop chan struct{}
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func NewWorker() *Worker {
|
||||
w := &Worker{
|
||||
stop: make(chan struct{}),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
go w.doWork()
|
||||
return w
|
||||
}
|
||||
|
||||
func (w *Worker) Shutdown() {
|
||||
close(w.stop)
|
||||
<-w.done
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 优先使用同步函数
|
||||
|
||||
> 理由和优势表在父 skill 中(SKILL.md § 同步函数)。以下是具体的代码示例。
|
||||
|
||||
```go
|
||||
// 好:同步函数 - 调用方控制并发
|
||||
func ProcessItems(items []Item) ([]Result, error) {
|
||||
var results []Result
|
||||
for _, item := range items {
|
||||
result, err := processItem(item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results = append(results, result)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// 调用方可以在需要时添加并发:
|
||||
go func() {
|
||||
results, err := ProcessItems(items)
|
||||
// 处理结果
|
||||
}()
|
||||
```
|
||||
@@ -0,0 +1,110 @@
|
||||
# 同步原语模式
|
||||
|
||||
互斥锁和原子操作的详细模式 —— 涵盖互斥锁嵌入陷阱和类型安全的原子访问。
|
||||
|
||||
---
|
||||
|
||||
## 不要嵌入互斥锁
|
||||
|
||||
如果你通过指针使用结构体,互斥锁应该是非指针字段。不要在结构体中嵌入互斥锁,即使该结构体未被导出。
|
||||
|
||||
```go
|
||||
// 不好:嵌入的互斥锁将 Lock/Unlock 暴露为 API 的一部分
|
||||
type SMap struct {
|
||||
sync.Mutex // Lock() 和 Unlock() 成为 SMap 的方法
|
||||
data map[string]string
|
||||
}
|
||||
|
||||
func (m *SMap) Get(k string) string {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
return m.data[k]
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
// 好:命名字段使互斥锁保持为实现细节
|
||||
type SMap struct {
|
||||
mu sync.Mutex
|
||||
data map[string]string
|
||||
}
|
||||
|
||||
func (m *SMap) Get(k string) string {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return m.data[k]
|
||||
}
|
||||
```
|
||||
|
||||
在不好的示例中,`Lock` 和 `Unlock` 方法无意中成为了导出 API 的一部分。在好的示例中,互斥锁是对调用方隐藏的实现细节。
|
||||
|
||||
---
|
||||
|
||||
## 原子操作:完整示例
|
||||
|
||||
标准 `sync/atomic` 包操作原始类型(`int32`、`int64` 等),容易忘记一致地使用原子操作。
|
||||
|
||||
```go
|
||||
// 不好:容易忘记原子操作
|
||||
type foo struct {
|
||||
running int32 // 原子操作
|
||||
}
|
||||
|
||||
func (f *foo) start() {
|
||||
if atomic.SwapInt32(&f.running, 1) == 1 {
|
||||
return // 已在运行
|
||||
}
|
||||
// 启动 Foo
|
||||
}
|
||||
|
||||
func (f *foo) isRunning() bool {
|
||||
return f.running == 1 // 竞争!忘记使用 atomic.LoadInt32
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
// 好:类型安全的原子操作
|
||||
type foo struct {
|
||||
running atomic.Bool
|
||||
}
|
||||
|
||||
func (f *foo) start() {
|
||||
if f.running.Swap(true) {
|
||||
return // 已在运行
|
||||
}
|
||||
// 启动 Foo
|
||||
}
|
||||
|
||||
func (f *foo) isRunning() bool {
|
||||
return f.running.Load() // 不可能意外地非原子读取
|
||||
}
|
||||
```
|
||||
|
||||
`atomic.Bool`、`atomic.Int64` 等类型(Go 1.19 起在标准库 `sync/atomic` 中可用,或通过 [go.uber.org/atomic](https://pkg.go.dev/go.uber.org/atomic))通过隐藏底层类型来增加类型安全性。
|
||||
|
||||
---
|
||||
|
||||
## Channel 方向示例
|
||||
|
||||
指定方向可以防止意外误用:
|
||||
|
||||
```go
|
||||
// 好:指定方向 - 清晰的所有权
|
||||
func sum(values <-chan int) int {
|
||||
total := 0
|
||||
for v := range values {
|
||||
total += v
|
||||
}
|
||||
return total
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
// 不好:未指定方向 - 允许意外误用
|
||||
func sum(values chan int) (out int) {
|
||||
for v := range values {
|
||||
out += v
|
||||
}
|
||||
close(values) // 漏洞!这能通过编译但不应该发生。
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,122 @@
|
||||
---
|
||||
name: go-context
|
||||
description: 在 Go 中使用 context.Context 时使用 — 包括函数签名中的位置、传播取消和截止时间、以及在 context 中存储值与使用参数的对比。也适用于取消长时间运行的操作、设置超时或传递请求作用域数据,即使未直接提及 context.Context。不涵盖 goroutine 生命周期或 sync 原语(参见 go-concurrency)。
|
||||
license: Apache-2.0
|
||||
compatibility: 需要 Go 1.7+(context 在 Go 1.7 中移入标准库)
|
||||
metadata:
|
||||
sources: "Go Wiki CodeReviewComments"
|
||||
---
|
||||
|
||||
# Go Context 用法
|
||||
|
||||
## Context 作为第一个参数
|
||||
|
||||
使用 Context 的函数应将其作为**第一个参数**:
|
||||
|
||||
```go
|
||||
func F(ctx context.Context, /* 其他参数 */) error
|
||||
func ProcessRequest(ctx context.Context, req *Request) (*Response, error)
|
||||
```
|
||||
|
||||
这是 Go 中的一个强约定,使 context 的传递在代码库中可见且一致。
|
||||
|
||||
---
|
||||
|
||||
## 不要在结构体中存储 Context
|
||||
|
||||
不要在结构体类型中添加 Context 成员。相反,将 `ctx` 作为参数传递给每个需要它的方法:
|
||||
|
||||
```go
|
||||
// 不好:Context 存储在结构体中
|
||||
type Worker struct {
|
||||
ctx context.Context // 不要这样做
|
||||
}
|
||||
|
||||
// 好:Context 传递给方法
|
||||
type Worker struct{ /* ... */ }
|
||||
|
||||
func (w *Worker) Process(ctx context.Context) error {
|
||||
// Context 显式传递 — 生命周期清晰
|
||||
}
|
||||
```
|
||||
|
||||
**例外**:签名必须匹配标准库或第三方库中接口的方法可能需要变通处理。
|
||||
|
||||
---
|
||||
|
||||
## 不要创建自定义 Context 类型
|
||||
|
||||
不要创建自定义的 Context 类型或在函数签名中使用 `context.Context` 以外的接口:
|
||||
|
||||
```go
|
||||
// 不好:自定义 context 类型
|
||||
type MyContext interface {
|
||||
context.Context
|
||||
GetUserID() string
|
||||
}
|
||||
|
||||
// 好:使用标准 context.Context 并提取值
|
||||
func Process(ctx context.Context) error {
|
||||
userID := GetUserID(ctx)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 应用数据放在哪里
|
||||
|
||||
按以下优先级顺序考虑:
|
||||
|
||||
1. **函数参数** — 最明确且类型安全
|
||||
2. **接收者** — 适用于属于该类型的数据
|
||||
3. **全局变量** — 适用于真正的全局配置(谨慎使用)
|
||||
4. **Context 值** — 仅用于请求作用域数据
|
||||
|
||||
Context 值适用于:
|
||||
- 请求 ID 和追踪 ID
|
||||
- 随请求流动的认证/授权信息
|
||||
- 截止时间和取消信号
|
||||
|
||||
Context 值**不适用**于:
|
||||
- 可选的函数参数
|
||||
- 可以显式传递的数据
|
||||
- 不随请求变化的配置
|
||||
|
||||
---
|
||||
|
||||
## 常见模式
|
||||
|
||||
> 在派生 context(WithTimeout、WithCancel、WithDeadline)、在循环或 HTTP 处理器中检查取消、使用带类型键的 context 值、或需要快速参考表时,阅读 [references/PATTERNS.md](references/PATTERNS.md)。
|
||||
|
||||
### 派生 Context
|
||||
|
||||
创建派生 context 后,始终立即 `defer cancel()`:
|
||||
|
||||
```go
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
```
|
||||
|
||||
### 检查取消
|
||||
|
||||
```go
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
// 执行工作
|
||||
}
|
||||
```
|
||||
|
||||
### Context 不可变性
|
||||
|
||||
Context 是不可变的 — 将同一个 `ctx` 传递给共享相同截止时间和取消信号的多个并发调用是安全的。
|
||||
|
||||
---
|
||||
|
||||
## 相关技能
|
||||
|
||||
- **Goroutine 协调**:在使用 context 进行 goroutine 取消、基于 select 的超时或 errgroup 时,参见 [go-concurrency](../go-concurrency/SKILL.md)
|
||||
- **错误处理**:在决定如何包装或返回 `ctx.Err()` 取消错误时,参见 [go-error-handling](../go-error-handling/SKILL.md)
|
||||
- **接口设计**:在设计接受 context 并结合接口的 API 时,参见 [go-interfaces](../go-interfaces/SKILL.md)
|
||||
- **请求作用域日志**:在将 logger 注入 context 或将请求 ID 添加到结构化日志输出时,参见 [go-logging](../go-logging/SKILL.md)
|
||||
@@ -0,0 +1,227 @@
|
||||
# Context 模式
|
||||
|
||||
派生、检查和传播 `context.Context` 的常见模式。
|
||||
|
||||
---
|
||||
|
||||
## Context 不可变性
|
||||
|
||||
Context 是不可变的。将同一个 `ctx` 传递给共享相同截止时间、取消信号、凭据和父级追踪的多个调用是安全的:
|
||||
|
||||
```go
|
||||
// 安全:同一个 context 传递给顺序调用
|
||||
func ProcessBatch(ctx context.Context, items []Item) error {
|
||||
for _, item := range items {
|
||||
if err := process(ctx, item); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 安全:同一个 context 传递给并发调用
|
||||
func ProcessConcurrently(ctx context.Context, a, b *Data) error {
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
g.Go(func() error { return processA(ctx, a) })
|
||||
g.Go(func() error { return processB(ctx, b) })
|
||||
return g.Wait()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 何时使用 context.Background()
|
||||
|
||||
仅在**从不特定于请求**的函数中使用 `context.Background()`:
|
||||
|
||||
```go
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
if err := run(ctx); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func startBackgroundWorker() {
|
||||
ctx := context.Background()
|
||||
go worker(ctx)
|
||||
}
|
||||
```
|
||||
|
||||
**默认传递 Context**,即使你认为不需要。只有在有充分理由说明传递 context 是错误做法时,才直接使用 `context.Background()`:
|
||||
|
||||
```go
|
||||
func LoadConfig(ctx context.Context) (*Config, error) {
|
||||
// 即使现在不使用 ctx,接受它可以在未来添加功能时
|
||||
// 不需要修改 API
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 派生 Context
|
||||
|
||||
```go
|
||||
// 添加超时 — 持续时间结束后触发取消
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 添加取消 — 调用者控制何时取消
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
// 添加截止时间 — 在指定的墙钟时间触发取消
|
||||
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(time.Hour))
|
||||
defer cancel()
|
||||
|
||||
// 添加值(谨慎使用 — 仅用于请求作用域数据)
|
||||
ctx = context.WithValue(ctx, requestIDKey, reqID)
|
||||
```
|
||||
|
||||
创建派生 context 后,**始终立即 `defer cancel()`**。这确保即使函数提前返回,资源也会被释放。
|
||||
|
||||
### 嵌套派生
|
||||
|
||||
派生的 context 形成树状结构。取消父级会取消所有子级:
|
||||
|
||||
```go
|
||||
func handleRequest(ctx context.Context) error {
|
||||
// 整个请求的父级超时
|
||||
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 数据库调用的更短超时
|
||||
dbCtx, dbCancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer dbCancel()
|
||||
|
||||
data, err := queryDB(dbCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 父级 context 的剩余时间适用于此处
|
||||
return sendResponse(ctx, data)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 检查取消
|
||||
|
||||
### 在长时间运行的循环中
|
||||
|
||||
```go
|
||||
func LongRunningOperation(ctx context.Context) error {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
// 执行工作
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 在高开销操作之前
|
||||
|
||||
在开始无法中断的工作之前检查取消:
|
||||
|
||||
```go
|
||||
func ProcessItems(ctx context.Context, items []Item) error {
|
||||
for _, item := range items {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
if err := expensiveProcess(item); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### 区分取消原因
|
||||
|
||||
```go
|
||||
if err := ctx.Err(); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, context.Canceled):
|
||||
// 调用者显式取消(例如客户端断开连接)
|
||||
case errors.Is(err, context.DeadlineExceeded):
|
||||
// 超时或截止时间已过
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 在 HTTP 处理器中遵守取消
|
||||
|
||||
```go
|
||||
func handler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
result, err := slowOperation(ctx)
|
||||
if err != nil {
|
||||
if errors.Is(err, context.Canceled) {
|
||||
// 客户端已断开连接 — 无需写入
|
||||
return
|
||||
}
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(result)
|
||||
}
|
||||
```
|
||||
|
||||
`r.Context()` 在以下情况被取消:
|
||||
- 客户端关闭连接
|
||||
- `http.Server` 的 `ReadTimeout` 或 `WriteTimeout` 触发
|
||||
- `ServeHTTP` 方法返回
|
||||
|
||||
---
|
||||
|
||||
## Context 值的最佳实践
|
||||
|
||||
### 使用未导出的键类型
|
||||
|
||||
```go
|
||||
type contextKey struct{}
|
||||
|
||||
var userIDKey contextKey
|
||||
|
||||
func WithUserID(ctx context.Context, id string) context.Context {
|
||||
return context.WithValue(ctx, userIDKey, id)
|
||||
}
|
||||
|
||||
func UserIDFromContext(ctx context.Context) (string, bool) {
|
||||
id, ok := ctx.Value(userIDKey).(string)
|
||||
return id, ok
|
||||
}
|
||||
```
|
||||
|
||||
使用未导出的结构体类型作为键可以防止与其他包的键发生冲突 — 即使它们使用相同的 string 或 int 值。
|
||||
|
||||
### 提供访问器函数
|
||||
|
||||
始终将 `context.WithValue` 和 `ctx.Value` 包装在类型化的辅助函数中(如上所示),而不是暴露键。这提供了类型安全性和一个可以修改实现的单一位置。
|
||||
|
||||
---
|
||||
|
||||
## 快速参考
|
||||
|
||||
| 模式 | 指导 |
|
||||
|------|------|
|
||||
| 参数位置 | 始终第一个:`func F(ctx context.Context, ...)` |
|
||||
| 结构体存储 | 不要存储在结构体中;传递给方法 |
|
||||
| 自定义类型 | 不要创建;使用 `context.Context` 接口 |
|
||||
| 应用数据 | 优先选择 参数 > 接收者 > 全局变量 > context 值 |
|
||||
| 请求作用域数据 | 适用于 context 值 |
|
||||
| 共享 context | 安全 — context 是不可变的 |
|
||||
| `context.Background()` | 仅用于非请求特定的代码 |
|
||||
| 默认行为 | 即使认为不需要也要传递 context |
|
||||
| `defer cancel()` | 在 `WithTimeout`/`WithCancel`/`WithDeadline` 之后始终立即 defer |
|
||||
| 值键 | 使用未导出的结构体类型,提供访问器函数 |
|
||||
| 取消检查 | 在高开销操作前使用 `ctx.Err()`;在循环中使用 `select` 监听 `ctx.Done()` |
|
||||
@@ -0,0 +1,193 @@
|
||||
---
|
||||
name: go-control-flow
|
||||
description: Use when writing conditionals, loops, or switch statements in Go — including if with initialization, early returns, for loop forms, range, switch, type switches, and blank identifier patterns. Also use when writing a simple if/else or for loop, even if the user doesn't mention guard clauses or variable scoping. Does not cover error flow patterns (see go-error-handling).
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
sources: "Effective Go, Google Style Guide"
|
||||
---
|
||||
|
||||
# Go 控制流
|
||||
|
||||
> 在使用 switch 语句、类型 switch 或带标签的 break 时,阅读 [references/SWITCH-PATTERNS.md](references/SWITCH-PATTERNS.md)
|
||||
|
||||
> 在使用 `_`、空白标识符导入或编译时接口检查时,阅读 [references/BLANK-IDENTIFIER.md](references/BLANK-IDENTIFIER.md)
|
||||
|
||||
---
|
||||
|
||||
## 带初始化的 If
|
||||
|
||||
`if` 和 `switch` 接受可选的初始化语句。使用它将变量限定在条件块作用域内:
|
||||
|
||||
```go
|
||||
if err := file.Chmod(0664); err != nil {
|
||||
log.Print(err)
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
如果需要在 `if` 之后超出几行的范围使用该变量,请单独声明并使用标准 `if`:
|
||||
|
||||
```go
|
||||
x, err := f()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 大量使用 x 的代码
|
||||
```
|
||||
|
||||
## 缩进错误流(守卫子句)
|
||||
|
||||
当 `if` 主体以 `break`、`continue`、`goto` 或 `return` 结尾时,省略不必要的 `else`。保持成功路径不缩进:
|
||||
|
||||
```go
|
||||
f, err := os.Open(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d, err := f.Stat()
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
codeUsing(f, d)
|
||||
```
|
||||
|
||||
当 `if` 已经返回时,绝不要将正常流程埋在 `else` 中。
|
||||
|
||||
---
|
||||
|
||||
## 重新声明和重新赋值
|
||||
|
||||
`:=` 短声明允许在同一作用域中重新声明变量:
|
||||
|
||||
```go
|
||||
f, err := os.Open(name) // 声明 f 和 err
|
||||
d, err := f.Stat() // 声明 d,重新赋值 err
|
||||
```
|
||||
|
||||
变量 `v` 即使已经声明过,也可以出现在 `:=` 声明中,前提是:
|
||||
|
||||
1. 声明在与现有 `v` **相同的作用域**中
|
||||
2. 值**可赋值**给 `v`
|
||||
3. 声明中至少创建了**一个其他新变量**
|
||||
|
||||
### 变量遮蔽
|
||||
|
||||
**警告**:如果 `v` 在外层作用域中声明,`:=` 会创建一个**新的**遮蔽变量 — 这是常见的 bug 来源:
|
||||
|
||||
```go
|
||||
// Bug:if 块内的 ctx 遮蔽了外层的 ctx
|
||||
if *shortenDeadlines {
|
||||
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||
defer cancel()
|
||||
}
|
||||
// 此处的 ctx 仍然是原始的 — 被遮蔽的 ctx 没有逃逸
|
||||
|
||||
// 修复:使用 = 而不是 :=
|
||||
var cancel func()
|
||||
ctx, cancel = context.WithTimeout(ctx, 3*time.Second)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## For 循环
|
||||
|
||||
Go 的 `for` 是唯一的循环结构,统一了 `while`、`do-while` 和 C 风格的 `for`:
|
||||
|
||||
```go
|
||||
// 仅条件(Go 的 "while")
|
||||
for x > 0 {
|
||||
x = process(x)
|
||||
}
|
||||
|
||||
// 无限循环
|
||||
for {
|
||||
if done() { break }
|
||||
}
|
||||
|
||||
// C 风格的三组件形式
|
||||
for i := 0; i < n; i++ { ... }
|
||||
```
|
||||
|
||||
### Range
|
||||
|
||||
`range` 遍历切片、映射、字符串和通道:
|
||||
|
||||
```go
|
||||
for i, v := range slice { ... } // 索引 + 值
|
||||
for k, v := range myMap { ... } // 键 + 值(顺序不确定)
|
||||
for i, r := range "héllo" { ... } // 字节索引 + rune(不是字节)
|
||||
for v := range ch { ... } // 接收直到通道关闭
|
||||
```
|
||||
|
||||
**关键规则:**
|
||||
- 对字符串 range 产生 **rune**,不是字节 — `i` 是字节偏移量
|
||||
- 对映射 range 的顺序**不确定** — 不要依赖它
|
||||
- 使用 `_` 丢弃索引或值:`for _, v := range slice`
|
||||
|
||||
### 并行赋值
|
||||
|
||||
Go 没有逗号运算符。使用并行赋值处理多个循环变量:
|
||||
|
||||
```go
|
||||
for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 {
|
||||
a[i], a[j] = a[j], a[i]
|
||||
}
|
||||
```
|
||||
|
||||
`++` 和 `--` 是语句,不是表达式 — 它们不能出现在并行赋值中。
|
||||
|
||||
---
|
||||
|
||||
## Switch:带标签的 Break
|
||||
|
||||
`for` 循环内 `switch` 中的 `break` 只会中断 switch。使用带标签的 `break` 退出外层循环:
|
||||
|
||||
```go
|
||||
Loop:
|
||||
for _, v := range items {
|
||||
switch v.Type {
|
||||
case "done":
|
||||
break Loop // 中断 for 循环
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
关于类型 switch,参见 **go-interfaces**:类型 Switch。
|
||||
|
||||
---
|
||||
|
||||
## 空白标识符
|
||||
|
||||
**绝不要随意丢弃错误** — 空指针解引用 panic 可能随之而来。
|
||||
|
||||
在编译时验证接口实现:`var _ io.Writer = (*MyType)(nil)`。
|
||||
参见 **go-interfaces** 中的接口满足检查模式。
|
||||
|
||||
---
|
||||
|
||||
## 快速参考
|
||||
|
||||
| 模式 | Go 惯用法 |
|
||||
|------|-----------|
|
||||
| If 初始化 | `if err := f(); err != nil { }` |
|
||||
| 提前返回 | 当 if 主体返回时省略 `else` |
|
||||
| 重新声明 | `:=` 在相同作用域 + 新变量时重新赋值 |
|
||||
| 遮蔽陷阱 | `:=` 在内层作用域创建新变量 |
|
||||
| 并行赋值 | `i, j = i+1, j-1` |
|
||||
| 无表达式 switch | `switch { case cond: }` |
|
||||
| 逗号 case | `case 'a', 'b', 'c':` |
|
||||
| 无 fallthrough | 默认行为(需要时显式使用 `fallthrough`) |
|
||||
| 从 switch 中跳出循环 | `break Label` |
|
||||
| 丢弃值 | `_, err := f()` |
|
||||
| 副作用导入 | `import _ "pkg"` |
|
||||
| 接口检查 | `var _ Interface = (*Type)(nil)` |
|
||||
|
||||
---
|
||||
|
||||
## 相关技能
|
||||
|
||||
- **错误流程**:在构建守卫子句、提前返回或错误优先模式时,参见 [go-error-handling](../go-error-handling/SKILL.md)
|
||||
- **类型 switch**:在使用类型 switch、comma-ok 惯用法或接口满足检查时,参见 [go-interfaces](../go-interfaces/SKILL.md)
|
||||
- **减少嵌套**:在减少嵌套深度或解决格式问题时,参见 [go-style-core](../go-style-core/SKILL.md)
|
||||
- **变量作用域**:在使用 if 初始化、`:=` 重新声明或减少变量作用域时,参见 [go-declarations](../go-declarations/SKILL.md)
|
||||
@@ -0,0 +1,71 @@
|
||||
# 空白标识符模式
|
||||
|
||||
空白标识符 `_` 在 Go 中有多种用途:丢弃不需要的值、为副作用导入包、以及在编译时验证接口实现。
|
||||
|
||||
---
|
||||
|
||||
## 多重赋值
|
||||
|
||||
使用 `_` 丢弃多值表达式中不需要的值:
|
||||
|
||||
```go
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
fmt.Printf("%s does not exist\n", path)
|
||||
}
|
||||
```
|
||||
|
||||
### 绝不要随意丢弃错误
|
||||
|
||||
静默丢弃错误会引发空指针 panic:
|
||||
|
||||
```go
|
||||
// 不好:忽略错误会在路径不存在时崩溃
|
||||
fi, _ := os.Stat(path)
|
||||
if fi.IsDir() { ... } // 空指针解引用
|
||||
```
|
||||
|
||||
如果确实不需要错误,请记录原因:
|
||||
|
||||
```go
|
||||
_ = logger.Sync() // 尽力刷新;错误不可操作
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 副作用导入
|
||||
|
||||
使用空白标识符仅为了 `init()` 副作用而导入包:
|
||||
|
||||
```go
|
||||
import _ "net/http/pprof" // 注册 HTTP 处理器
|
||||
import _ "image/png" // 注册 PNG 解码器
|
||||
```
|
||||
|
||||
这通常用于注册驱动、编解码器或调试处理器,它们在 `init()` 期间将自己注册到注册表中。
|
||||
|
||||
---
|
||||
|
||||
## 接口实现检查
|
||||
|
||||
在编译时验证类型是否实现了接口,方法是将 nil 指针赋值给接口类型的空白标识符变量:
|
||||
|
||||
```go
|
||||
var _ io.Writer = (*MyType)(nil)
|
||||
```
|
||||
|
||||
如果 `*MyType` 不满足 `io.Writer`,这会产生编译错误,在运行时之前捕获缺失的方法。
|
||||
|
||||
**何时使用**:将此检查放在定义该类型的同一文件中,通常在类型声明之后。当类型必须满足另一个包中定义的接口时特别有用。
|
||||
|
||||
参见 [go-interfaces](../../go-interfaces/SKILL.md):接口满足检查,获取关于何时何地使用此模式的完整指导。
|
||||
|
||||
---
|
||||
|
||||
## 快速参考
|
||||
|
||||
| 模式 | 语法 |
|
||||
|------|------|
|
||||
| 丢弃值 | `_, err := f()` |
|
||||
| 在 if 初始化中丢弃 | `if _, err := f(); err != nil { }` |
|
||||
| 副作用导入 | `import _ "pkg"` |
|
||||
| 接口检查 | `var _ Interface = (*Type)(nil)` |
|
||||
@@ -0,0 +1,109 @@
|
||||
# Switch 模式
|
||||
|
||||
Go `switch` 语句的详细模式,包括无表达式 switch、逗号 case、break 行为和带标签的 break。
|
||||
|
||||
---
|
||||
|
||||
## 无自动 Fallthrough
|
||||
|
||||
Go `switch` 的 case 默认**不会** fall through(与 C/Java 不同)。每个 case 主体隐式地 break。仅在明确需要时使用 `fallthrough` — 这在惯用 Go 中很少见。
|
||||
|
||||
```go
|
||||
switch n {
|
||||
case 1:
|
||||
fmt.Println("one")
|
||||
// 无 fallthrough — 下一个 case 不会执行
|
||||
case 2:
|
||||
fmt.Println("two")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 无表达式 Switch
|
||||
|
||||
没有表达式的 `switch` 对 `true` 进行 switch。在将单个变量与多个条件进行比较时,用它来替代 if-else-if 链:
|
||||
|
||||
```go
|
||||
func unhex(c byte) byte {
|
||||
switch {
|
||||
case '0' <= c && c <= '9':
|
||||
return c - '0'
|
||||
case 'a' <= c && c <= 'f':
|
||||
return c - 'a' + 10
|
||||
case 'A' <= c && c <= 'F':
|
||||
return c - 'A' + 10
|
||||
}
|
||||
return 0
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 逗号分隔的 Case
|
||||
|
||||
多个值可以使用逗号共享一个 case 主体 — 不需要 `fallthrough`:
|
||||
|
||||
```go
|
||||
func shouldEscape(c byte) bool {
|
||||
switch c {
|
||||
case ' ', '?', '&', '=', '#', '+', '%':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 带标签的 Break
|
||||
|
||||
`switch` 中的 `break` 仅终止 switch,**不会**终止外层的 `for` 循环。使用标签来跳出循环:
|
||||
|
||||
```go
|
||||
Loop:
|
||||
for n := 0; n < len(src); n += size {
|
||||
switch {
|
||||
case src[n] < sizeOne:
|
||||
break // 仅中断 switch
|
||||
case src[n] < sizeTwo:
|
||||
if n+1 >= len(src) {
|
||||
break Loop // 跳出 for 循环
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
另一个常见模式 — 从 switch 内部中断 range 循环:
|
||||
|
||||
```go
|
||||
Loop:
|
||||
for _, v := range items {
|
||||
switch v.Type {
|
||||
case "done":
|
||||
break Loop // 中断 for 循环
|
||||
case "skip":
|
||||
break // 仅中断 switch
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**经验法则**:当 `for` 循环内有 `switch` 且需要从 case 中退出循环时,始终使用带标签的 break。
|
||||
|
||||
---
|
||||
|
||||
## 类型 Switch
|
||||
|
||||
关于类型 switch(`switch v := x.(type)`),参见 [go-interfaces](../../go-interfaces/SKILL.md):类型 Switch。
|
||||
|
||||
---
|
||||
|
||||
## 快速参考
|
||||
|
||||
| 模式 | 语法 |
|
||||
|------|------|
|
||||
| 无表达式 switch | `switch { case cond: }` |
|
||||
| 逗号 case | `case 'a', 'b', 'c':` |
|
||||
| 无 fallthrough | 默认行为;需要时使用 `fallthrough` 关键字 |
|
||||
| 仅中断 switch | case 内使用 `break` |
|
||||
| 中断外层循环 | 使用带标签的 `for` 和 `break Label` |
|
||||
@@ -0,0 +1,140 @@
|
||||
---
|
||||
name: go-data-structures
|
||||
description: Use when working with Go slices, maps, or arrays — choosing between new and make, using append, declaring empty slices (nil vs literal for JSON), implementing sets with maps, and copying data at boundaries. Also use when building or manipulating collections, even if the user doesn't ask about allocation idioms. Does not cover concurrent data structure safety (see go-concurrency).
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
sources: "Effective Go, Google Style Guide, Uber Style Guide, Go Wiki CodeReviewComments"
|
||||
---
|
||||
|
||||
# Go 数据结构
|
||||
|
||||
---
|
||||
|
||||
## 选择数据结构
|
||||
|
||||
```
|
||||
你需要什么?
|
||||
├─ 有序的元素集合
|
||||
│ ├─ 编译时已知固定大小 → 数组 [N]T
|
||||
│ └─ 动态大小 → 切片 []T
|
||||
│ ├─ 知道大概的大小?→ make([]T, 0, capacity)
|
||||
│ └─ 未知大小或需要 nil 安全的 JSON?→ var s []T (nil)
|
||||
├─ 键值查找
|
||||
│ └─ 映射 map[K]V
|
||||
│ ├─ 知道大概的大小?→ make(map[K]V, capacity)
|
||||
│ └─ 需要集合?→ map[T]struct{}(零大小值)
|
||||
└─ 需要传递给函数?
|
||||
└─ 如果调用者可能会修改它,则在边界处复制
|
||||
```
|
||||
|
||||
> **此技能不适用的场景**:对于数据结构的并发访问(互斥锁、原子操作),参见 [go-concurrency](../go-concurrency/SKILL.md)。对于 API 边界处的防御性复制,参见 [go-defensive](../go-defensive/SKILL.md)。对于为性能预分配容量,参见 [go-performance](../go-performance/SKILL.md)。
|
||||
|
||||
---
|
||||
|
||||
## 切片
|
||||
|
||||
### append 函数
|
||||
|
||||
**始终赋值结果** — 底层数组可能会改变:
|
||||
|
||||
```go
|
||||
x := []int{1, 2, 3}
|
||||
x = append(x, 4, 5, 6)
|
||||
|
||||
// 将切片追加到切片
|
||||
x = append(x, y...) // 注意 ...
|
||||
```
|
||||
|
||||
### 二维切片
|
||||
|
||||
**独立的内部切片**(可以独立增长/缩小):
|
||||
|
||||
```go
|
||||
picture := make([][]uint8, YSize)
|
||||
for i := range picture {
|
||||
picture[i] = make([]uint8, XSize)
|
||||
}
|
||||
```
|
||||
|
||||
**单次分配**(对于固定大小更高效):
|
||||
|
||||
```go
|
||||
picture := make([][]uint8, YSize)
|
||||
pixels := make([]uint8, XSize*YSize)
|
||||
for i := range picture {
|
||||
picture[i], pixels = pixels[:XSize], pixels[XSize:]
|
||||
}
|
||||
```
|
||||
|
||||
> 在调试意外的切片行为、跨 goroutine 共享切片或处理切片头时,阅读 [references/SLICES.md](references/SLICES.md)。
|
||||
|
||||
### 声明空切片
|
||||
|
||||
优先使用 nil 切片而非空字面量:
|
||||
|
||||
```go
|
||||
// 好:nil 切片
|
||||
var t []string
|
||||
|
||||
// 避免:非 nil 但零长度
|
||||
t := []string{}
|
||||
```
|
||||
|
||||
两者的 `len` 和 `cap` 都是零,但 nil 切片是首选风格。
|
||||
|
||||
**JSON 例外**:nil 切片编码为 `null`,而 `[]string{}` 编码为 `[]`。当需要 JSON 数组时使用非 nil。
|
||||
|
||||
在设计接口时,避免区分 nil 和非 nil 的零长度切片。
|
||||
|
||||
---
|
||||
|
||||
## 映射
|
||||
|
||||
### 实现集合
|
||||
|
||||
使用 `map[T]bool` — 惯用且阅读自然:
|
||||
|
||||
```go
|
||||
attended := map[string]bool{"Ann": true, "Joe": true}
|
||||
if attended[person] { // 不在映射中则为 false
|
||||
fmt.Println(person, "was at the meeting")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 复制
|
||||
|
||||
从另一个包复制结构体时要小心。如果类型的方法定义在指针类型(`*T`)上,复制值可能导致别名 bug。
|
||||
|
||||
**通用规则:** 如果类型 `T` 的方法与指针类型 `*T` 关联,则不要复制 `T` 的值。这适用于 `bytes.Buffer`、`sync.Mutex`、`sync.WaitGroup` 以及包含它们的类型。
|
||||
|
||||
```go
|
||||
// 不好:复制互斥锁
|
||||
var mu sync.Mutex
|
||||
mu2 := mu // 几乎总是 bug
|
||||
|
||||
// 好:通过指针传递
|
||||
func increment(sc *SafeCounter) {
|
||||
sc.mu.Lock()
|
||||
sc.count++
|
||||
sc.mu.Unlock()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 快速参考
|
||||
|
||||
| 主题 | 关键点 |
|
||||
|------|--------|
|
||||
| 切片 | 始终赋值 `append` 结果;`nil` 切片优于 `[]T{}` |
|
||||
| 集合 | `map[T]bool` 是惯用写法 |
|
||||
| 复制 | 如果方法在 `*T` 上则不要复制 `T`;注意别名问题 |
|
||||
|
||||
## 相关技能
|
||||
|
||||
- **防御性复制**:在 API 边界处复制切片或映射以防止修改时,参见 [go-defensive](../go-defensive/SKILL.md)
|
||||
- **容量提示**:为已知工作负载预分配切片或映射容量时,参见 [go-performance](../go-performance/SKILL.md)
|
||||
- **迭代模式**:在对切片、映射或通道使用 range 循环时,参见 [go-control-flow](../go-control-flow/SKILL.md)
|
||||
- **声明风格**:在 `new`、`make`、`var` 和复合字面量之间选择时,参见 [go-declarations](../go-declarations/SKILL.md)
|
||||
@@ -0,0 +1,146 @@
|
||||
# Go 切片内部原理
|
||||
|
||||
> **来源**:Effective Go
|
||||
|
||||
---
|
||||
|
||||
## 三项描述符
|
||||
|
||||
切片是一个运行时数据结构,包含三个组件:
|
||||
|
||||
- **指针**:第一个可访问元素的地址
|
||||
- **长度**:元素数量(`len(s)`)
|
||||
- **容量**:到底层数组末尾的最大元素数(`cap(s)`)
|
||||
|
||||
```go
|
||||
arr := [5]int{10, 20, 30, 40, 50}
|
||||
s := arr[1:4] // s = [20, 30, 40]
|
||||
// 指针:&arr[1],长度:3,容量:4
|
||||
```
|
||||
|
||||
`nil` 切片的三项均为零/nil。
|
||||
|
||||
---
|
||||
|
||||
## 切片引用底层数组
|
||||
|
||||
切片不存储数据 — 它们描述数组的一部分:
|
||||
|
||||
```go
|
||||
data := [4]int{1, 2, 3, 4}
|
||||
a := data[0:2] // [1, 2]
|
||||
b := data[1:3] // [2, 3]
|
||||
|
||||
b[0] = 99
|
||||
fmt.Println(a) // [1, 99] - 两者都看到变化
|
||||
fmt.Println(data) // [1, 99, 3, 4]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 切片运算符
|
||||
|
||||
`s[lo:hi]` 创建从索引 `lo` 到 `hi-1` 的切片:
|
||||
|
||||
```go
|
||||
s := []int{0, 1, 2, 3, 4, 5}
|
||||
s[2:4] // [2, 3]
|
||||
s[:3] // [0, 1, 2]
|
||||
s[3:] // [3, 4, 5]
|
||||
```
|
||||
|
||||
三索引形式 `s[lo:hi:max]` 将容量限制为 `max-lo`。
|
||||
|
||||
---
|
||||
|
||||
## 为什么 append 必须返回切片
|
||||
|
||||
切片头是**按值传递**的。函数可以修改元素但无法改变调用者的切片头:
|
||||
|
||||
```go
|
||||
func Append(slice, data []byte) []byte {
|
||||
l := len(slice)
|
||||
if l+len(data) > cap(slice) {
|
||||
newSlice := make([]byte, (l+len(data))*2)
|
||||
copy(newSlice, slice)
|
||||
slice = newSlice // 只改变局部变量
|
||||
}
|
||||
slice = slice[0 : l+len(data)]
|
||||
copy(slice[l:], data)
|
||||
return slice // 调用者必须接收新的切片头
|
||||
}
|
||||
```
|
||||
|
||||
当发生重新分配时,`slice` 指向新数组。调用者的原始引用仍指向旧数组 — 返回使调用者能够更新其引用。
|
||||
|
||||
---
|
||||
|
||||
## copy 函数
|
||||
|
||||
`copy(dst, src)` 复制元素并返回复制的数量:
|
||||
|
||||
```go
|
||||
src := []int{1, 2, 3, 4, 5}
|
||||
dst := make([]int, 3)
|
||||
n := copy(dst, src) // n=3, dst=[1,2,3]
|
||||
```
|
||||
|
||||
正确处理重叠切片。复制 `min(len(dst), len(src))` 个元素 — 不会发生重新分配。
|
||||
|
||||
---
|
||||
|
||||
## 切片常见陷阱
|
||||
|
||||
### 1. 共享底层数组
|
||||
|
||||
```go
|
||||
original := []int{1, 2, 3, 4, 5}
|
||||
subset := original[1:3]
|
||||
subset[0] = 99
|
||||
fmt.Println(original) // [1, 99, 3, 4, 5] - 被修改了!
|
||||
|
||||
// 修复:创建独立副本
|
||||
subset := make([]int, 2)
|
||||
copy(subset, original[1:3])
|
||||
```
|
||||
|
||||
### 2. append 可能重新分配也可能不
|
||||
|
||||
```go
|
||||
a := make([]int, 3, 5) // len=3, cap=5
|
||||
b := a[0:3]
|
||||
a = append(a, 4) // 在容量内 - 仍然共享
|
||||
a = append(a, 5, 6) // 超出容量 - 现在独立
|
||||
```
|
||||
|
||||
### 3. 大底层数组导致内存泄漏
|
||||
|
||||
```go
|
||||
// 不好:小切片将整个文件保留在内存中
|
||||
func getHeader(file []byte) []byte { return file[:100] }
|
||||
|
||||
// 好:复制以释放大数组
|
||||
func getHeader(file []byte) []byte {
|
||||
header := make([]byte, 100)
|
||||
copy(header, file)
|
||||
return header
|
||||
}
|
||||
```
|
||||
|
||||
### 4. nil vs 空切片
|
||||
|
||||
```go
|
||||
var nilSlice []int // nil, len=0, cap=0
|
||||
emptySlice := []int{} // 非 nil, len=0, cap=0
|
||||
// 两者在 len、cap、append、range 中表现相同
|
||||
// 未初始化状态优先使用 nil
|
||||
```
|
||||
|
||||
## 快速参考
|
||||
|
||||
| 操作 | 行为 |
|
||||
|------|------|
|
||||
| `s[lo:hi]` | 从 lo 到 hi-1 的切片 |
|
||||
| `s[lo:hi:max]` | 容量限制为 max-lo 的切片 |
|
||||
| `append(s, x...)` | 返回新切片;可能重新分配 |
|
||||
| `copy(dst, src)` | 返回复制数量;不重新分配 |
|
||||
@@ -0,0 +1,188 @@
|
||||
---
|
||||
name: go-defensive
|
||||
description: Use when hardening Go code at API boundaries — copying slices/maps, verifying interface compliance, using defer for cleanup, time.Time/time.Duration, or avoiding mutable globals. Also use when reviewing for robustness concerns like missing cleanup or unsafe crypto usage, even if the user doesn't mention "defensive programming." Does not cover error handling strategy (see go-error-handling).
|
||||
license: Apache-2.0
|
||||
compatibility: Uses crypto/rand.Text (Go 1.24+) in examples
|
||||
metadata:
|
||||
sources: "Effective Go, Uber Style Guide, Go Wiki CodeReviewComments"
|
||||
---
|
||||
|
||||
# Go 防御性编程模式
|
||||
|
||||
## 防御性检查清单优先级
|
||||
|
||||
在加固 API 边界代码时,按以下顺序检查:
|
||||
|
||||
```
|
||||
正在审查 API 边界?
|
||||
├─ 1. 错误处理 → 返回错误;不要 panic(参见 go-error-handling)
|
||||
├─ 2. 输入验证 → 复制从调用者接收的切片/map
|
||||
├─ 3. 输出安全 → 在返回给调用者之前复制切片/map
|
||||
├─ 4. 资源清理 → 使用 defer 进行 Close/Unlock/Cancel
|
||||
├─ 5. 接口检查 → var _ Interface = (*Type)(nil) 编译时验证
|
||||
├─ 6. 时间正确性 → 使用 time.Time 和 time.Duration,不要用 int/float
|
||||
├─ 7. 枚举安全 → iota 从 1 开始,使零值无效
|
||||
└─ 8. 加密安全 → 用 crypto/rand 生成密钥,绝不用 math/rand
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 快速参考
|
||||
|
||||
| 模式 | 规则 | 详情 |
|
||||
|------|------|------|
|
||||
| 边界复制 | 在接收和返回时复制切片/map | [BOUNDARY-COPYING.md](references/BOUNDARY-COPYING.md) |
|
||||
| Defer 清理 | 在 `os.Open` 之后立即 `defer f.Close()` | 见下文 |
|
||||
| 接口检查 | `var _ I = (*T)(nil)` | 参见 go-interfaces |
|
||||
| 时间类型 | `time.Time` / `time.Duration`,绝不用原始 int | [TIME-ENUMS-TAGS.md](references/TIME-ENUMS-TAGS.md) |
|
||||
| 枚举起始值 | `iota + 1` 使零值 = 无效 | 见下文 |
|
||||
| 加密随机数 | 用 `crypto/rand` 生成密钥,绝不用 `math/rand` | 见下文 |
|
||||
| Must 函数 | 仅在初始化时使用;失败时 panic | [MUST-FUNCTIONS.md](references/MUST-FUNCTIONS.md) |
|
||||
| Panic/recover | 绝不跨包暴露 panic | [PANIC-RECOVER.md](references/PANIC-RECOVER.md) |
|
||||
| 可变全局变量 | 用依赖注入替代 | 见下文 |
|
||||
|
||||
---
|
||||
|
||||
## 验证接口合规性
|
||||
|
||||
使用编译时检查来验证接口实现。完整模式请参见 **go-interfaces**:接口满足检查。
|
||||
|
||||
```go
|
||||
var _ http.Handler = (*Handler)(nil)
|
||||
```
|
||||
|
||||
## 在边界处复制切片和 Map
|
||||
|
||||
切片和 map 包含指向底层数据的指针。在 API 边界处复制,以防止意外修改。
|
||||
|
||||
```go
|
||||
// 接收:复制传入的切片
|
||||
d.trips = make([]Trip, len(trips))
|
||||
copy(d.trips, trips)
|
||||
|
||||
// 返回:在返回之前复制 map
|
||||
result := make(map[string]int, len(s.counters))
|
||||
for k, v := range s.counters { result[k] = v }
|
||||
```
|
||||
|
||||
> 在 API 边界处复制切片或 map,或决定何时需要防御性复制、何时可以跳过时,请阅读 [references/BOUNDARY-COPYING.md](references/BOUNDARY-COPYING.md)。
|
||||
|
||||
## 使用 Defer 清理资源
|
||||
|
||||
使用 `defer` 清理资源(文件、锁)。避免在多个返回路径中遗漏清理。
|
||||
|
||||
```go
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
|
||||
if p.count < 10 {
|
||||
return p.count
|
||||
}
|
||||
p.count++
|
||||
return p.count
|
||||
```
|
||||
|
||||
Defer 的开销可以忽略不计。在 `os.Open` 之后立即放置 `defer f.Close()` 以提高清晰度。延迟函数的参数在 `defer` 执行时求值,而非在函数运行时。多个 defer 按 LIFO 顺序执行。
|
||||
|
||||
## 结构体字段标签
|
||||
|
||||
> **建议**:始终为需要序列化或反序列化的结构体添加显式字段标签。
|
||||
|
||||
```go
|
||||
type User struct {
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Email string `json:"email" yaml:"email"`
|
||||
}
|
||||
```
|
||||
|
||||
字段标签是**序列化契约**——重命名结构体字段而不更新标签会悄然破坏线格式兼容性。对于任何跨越序列化边界的类型,应将标签视为公共 API 的一部分。
|
||||
|
||||
## 枚举从 1 开始
|
||||
|
||||
枚举从非零值开始,以区分未初始化的值和有效值。
|
||||
|
||||
```go
|
||||
const (
|
||||
Add Operation = iota + 1 // Add=1,零值 = 未初始化
|
||||
Subtract
|
||||
Multiply
|
||||
)
|
||||
```
|
||||
|
||||
**例外**:当零值是合理的默认值时(例如 `LogToStdout = iota`)。
|
||||
|
||||
## 时间、结构体标签和嵌入
|
||||
|
||||
> 在使用 `time.Time`/`time.Duration` 代替原始 int、为序列化结构体添加字段标签,或决定是否在公共结构体中嵌入类型时,请阅读 [references/TIME-ENUMS-TAGS.md](references/TIME-ENUMS-TAGS.md)。
|
||||
|
||||
## 避免可变全局变量
|
||||
|
||||
通过注入依赖代替修改包级变量。这使代码可以在不需要全局 save/restore 的情况下进行测试。
|
||||
|
||||
```go
|
||||
type signer struct {
|
||||
now func() time.Time // 注入的;测试中用固定时间替换
|
||||
}
|
||||
|
||||
func newSigner() *signer {
|
||||
return &signer{now: time.Now}
|
||||
}
|
||||
```
|
||||
|
||||
> 在决定全局变量是否合适、设计 New() + Default() 包状态模式,或用依赖注入替代可变全局变量时,请阅读 [references/GLOBAL-STATE.md](references/GLOBAL-STATE.md)。
|
||||
|
||||
## 加密随机数
|
||||
|
||||
不要使用 `math/rand` 或 `math/rand/v2` 生成密钥——这是一个**安全问题**。时间种子的生成器输出是可预测的。
|
||||
|
||||
```go
|
||||
import "crypto/rand"
|
||||
|
||||
func Key() string { return rand.Text() }
|
||||
```
|
||||
|
||||
对于文本输出,直接使用 `crypto/rand.Text`,或用 `encoding/hex` 或 `encoding/base64` 编码随机字节。
|
||||
|
||||
---
|
||||
|
||||
## Panic 与 Recover
|
||||
|
||||
仅在真正不可恢复的情况下使用 `panic`。库函数应避免 panic。
|
||||
|
||||
```go
|
||||
func safelyDo(work *Work) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Println("work failed:", err)
|
||||
}
|
||||
}()
|
||||
do(work)
|
||||
}
|
||||
```
|
||||
|
||||
**关键规则:**
|
||||
- 绝不跨包边界暴露 panic——始终转换为 error
|
||||
- 如果库确实无法在 `init()` 中完成初始化,可以接受 panic
|
||||
- 使用 recover 隔离服务器 goroutine 处理器中的 panic
|
||||
|
||||
> 在编写 HTTP 服务器中的 panic 恢复、在解析器中使用 panic 作为内部控制流机制,或在 log.Fatal 和 panic 之间做选择时,请阅读 [references/PANIC-RECOVER.md](references/PANIC-RECOVER.md)。
|
||||
|
||||
## Must 函数
|
||||
|
||||
`Must` 函数在出错时 panic——**仅**在程序初始化阶段使用,因为失败意味着程序无法运行。
|
||||
|
||||
```go
|
||||
var validID = regexp.MustCompile(`^[a-z][a-z0-9-]{0,62}$`)
|
||||
var tmpl = template.Must(template.ParseFiles("index.html"))
|
||||
```
|
||||
|
||||
> 在编写自定义 Must 函数、决定 Must 是否适用于特定调用点,或将可能失败的初始化包装在 panic 辅助函数中时,请阅读 [references/MUST-FUNCTIONS.md](references/MUST-FUNCTIONS.md)。
|
||||
|
||||
---
|
||||
|
||||
## 相关技能
|
||||
|
||||
- **错误处理**:在选择返回错误还是 panic,或在边界处包装错误时,参见 [go-error-handling](../go-error-handling/SKILL.md)
|
||||
- **并发安全**:在使用互斥锁、原子操作或通道保护共享状态时,参见 [go-concurrency](../go-concurrency/SKILL.md)
|
||||
- **接口检查**:在添加编译时接口满足检查(`var _ I = (*T)(nil)`)时,参见 [go-interfaces](../go-interfaces/SKILL.md)
|
||||
- **数据结构复制**:在处理切片/map 内部结构或指针别名时,参见 [go-data-structures](../go-data-structures/SKILL.md)
|
||||
@@ -0,0 +1,101 @@
|
||||
# 在 API 边界处复制切片和 Map
|
||||
|
||||
> **来源**:Uber 风格指南
|
||||
|
||||
切片和 map 包含对其底层数据的引用。在 API 边界处复制它们,以防止调用者修改内部状态(反之亦然)。
|
||||
|
||||
## 接收切片和 Map
|
||||
|
||||
当函数存储调用者传入的切片或 map 时,始终进行防御性复制。调用者保留原始引用,可以在函数返回后修改它。
|
||||
|
||||
### 切片
|
||||
|
||||
**不好**
|
||||
```go
|
||||
func (d *Driver) SetTrips(trips []Trip) {
|
||||
d.trips = trips // 调用者仍然可以修改 d.trips
|
||||
}
|
||||
```
|
||||
|
||||
**好**
|
||||
```go
|
||||
func (d *Driver) SetTrips(trips []Trip) {
|
||||
d.trips = make([]Trip, len(trips))
|
||||
copy(d.trips, trips)
|
||||
}
|
||||
```
|
||||
|
||||
### Map
|
||||
|
||||
**不好**
|
||||
```go
|
||||
func (s *Server) SetConfig(cfg map[string]string) {
|
||||
s.config = cfg // 调用者仍然可以修改 s.config
|
||||
}
|
||||
```
|
||||
|
||||
**好**
|
||||
```go
|
||||
func (s *Server) SetConfig(cfg map[string]string) {
|
||||
s.config = make(map[string]string, len(cfg))
|
||||
for k, v := range cfg {
|
||||
s.config[k] = v
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 返回切片和 Map
|
||||
|
||||
返回内部切片或 map 时,返回副本以防止调用者修改你的内部状态。
|
||||
|
||||
### 返回 Map
|
||||
|
||||
**不好**
|
||||
```go
|
||||
func (s *Stats) Snapshot() map[string]int {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.counters // 暴露了内部状态!
|
||||
}
|
||||
```
|
||||
|
||||
**好**
|
||||
```go
|
||||
func (s *Stats) Snapshot() map[string]int {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
result := make(map[string]int, len(s.counters))
|
||||
for k, v := range s.counters {
|
||||
result[k] = v
|
||||
}
|
||||
return result
|
||||
}
|
||||
```
|
||||
|
||||
### 返回切片
|
||||
|
||||
**不好**
|
||||
```go
|
||||
func (q *Queue) Items() []Item {
|
||||
return q.items // 调用者可以追加、修改或重新切片
|
||||
}
|
||||
```
|
||||
|
||||
**好**
|
||||
```go
|
||||
func (q *Queue) Items() []Item {
|
||||
result := make([]Item, len(q.items))
|
||||
copy(result, q.items)
|
||||
return result
|
||||
}
|
||||
```
|
||||
|
||||
## 何时不需要复制
|
||||
|
||||
防御性复制有开销。在以下情况下可以跳过:
|
||||
|
||||
- 数据**按约定是不可变的**,并且有清晰的文档说明
|
||||
- 切片/map 是**为调用者新创建的**(不在内部存储)
|
||||
- 性能分析表明复制在热路径中是瓶颈
|
||||
|
||||
如有疑问,就复制。与共享引用导致的 bug 相比,开销通常可以忽略不计。
|
||||
@@ -0,0 +1,144 @@
|
||||
# 全局状态模式
|
||||
|
||||
> **来源**:Google 风格指南, Effective Go
|
||||
|
||||
全局状态使程序更难以测试、推理和维护。依赖注入是首选替代方案,但某些全局状态在谨慎使用时是可以接受的。
|
||||
|
||||
## 何时可以接受全局状态
|
||||
|
||||
并非所有包级变量都有害。当全局状态是**真正进程级别的**且**不值得注入**时,它是合适的:
|
||||
|
||||
- **默认实例**——`http.DefaultClient`、`log.Default()`、`flag.CommandLine`
|
||||
- **一次编译的值**——包级别的 `regexp.MustCompile(...)`
|
||||
- **注册表**——`database/sql.Register`、`image.RegisterFormat`
|
||||
- **单例基础设施**——进程级别的指标收集器或追踪导出器
|
||||
|
||||
## 全局变量的试金石测试
|
||||
|
||||
在添加包级变量之前,请问自己:
|
||||
|
||||
1. **它是否真正是进程级别的?** 如果两个 goroutine 或测试可能需要不同的值,它不应该是全局的
|
||||
2. **它是否妨碍了测试?** 如果测试必须保存/恢复变量,或因此无法并行运行,应改为注入
|
||||
3. **它可以是常量吗?** 如果值在初始化后永远不会改变,优先使用 `const` 或未导出的只初始化一次的 `var`
|
||||
4. **它是否携带可变状态?** 可变全局变量是最危险的——仅在有完善文档、并发安全的单例情况下才可接受
|
||||
|
||||
## 包状态 API 模式:New() + Default()
|
||||
|
||||
标准库模式同时提供可定制的构造器和便捷的默认值。这使调用者可以在简单场景下使用默认值,在测试或特殊行为需求下注入自定义实例。
|
||||
|
||||
**好**
|
||||
```go
|
||||
package mylog
|
||||
|
||||
type Logger struct {
|
||||
prefix string
|
||||
out io.Writer
|
||||
}
|
||||
|
||||
func New(prefix string, out io.Writer) *Logger {
|
||||
return &Logger{prefix: prefix, out: out}
|
||||
}
|
||||
|
||||
var defaultLogger = New("", os.Stderr)
|
||||
|
||||
func Default() *Logger { return defaultLogger }
|
||||
|
||||
func (l *Logger) Info(msg string) {
|
||||
fmt.Fprintf(l.out, "%s%s\n", l.prefix, msg)
|
||||
}
|
||||
|
||||
// 包级便捷函数委托给默认实例。
|
||||
func Info(msg string) { defaultLogger.Info(msg) }
|
||||
```
|
||||
|
||||
```go
|
||||
// 调用者在简单场景下使用默认值
|
||||
mylog.Info("starting server")
|
||||
|
||||
// 测试或特殊代码创建自定义实例
|
||||
logger := mylog.New("[test] ", &buf)
|
||||
logger.Info("test message")
|
||||
```
|
||||
|
||||
此模式的标准库示例:
|
||||
- `log.New()` + `log.Default()` + `log.Println()`
|
||||
- `http.NewServeMux()` + `http.DefaultServeMux`
|
||||
- `flag.NewFlagSet()` + `flag.CommandLine`
|
||||
|
||||
## 依赖注入作为首选替代方案
|
||||
|
||||
当代码需要可配置行为时,通过构造器参数或结构体字段接受依赖,而非读取包级变量。
|
||||
|
||||
**不好**
|
||||
```go
|
||||
var db *sql.DB
|
||||
|
||||
func GetUser(id int) (*User, error) {
|
||||
return db.QueryRow("SELECT ...", id) // 依赖全局变量
|
||||
}
|
||||
```
|
||||
|
||||
**好**
|
||||
```go
|
||||
type UserStore struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewUserStore(db *sql.DB) *UserStore {
|
||||
return &UserStore{db: db}
|
||||
}
|
||||
|
||||
func (s *UserStore) GetUser(id int) (*User, error) {
|
||||
return s.db.QueryRow("SELECT ...", id)
|
||||
}
|
||||
```
|
||||
|
||||
注入的好处:
|
||||
- 测试可以提供 mock 或内存实现
|
||||
- 多个实例可以共存(例如,只读副本与主库)
|
||||
- 依赖在构造器签名中是显式的
|
||||
|
||||
## 注入时间
|
||||
|
||||
一个常见场景:替换 `time.Now` 以实现确定性测试。
|
||||
|
||||
**不好**
|
||||
```go
|
||||
func IsExpired(expiry time.Time) bool {
|
||||
return time.Now().After(expiry) // 不可测试
|
||||
}
|
||||
```
|
||||
|
||||
**好**
|
||||
```go
|
||||
type Checker struct {
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
func NewChecker() *Checker {
|
||||
return &Checker{now: time.Now}
|
||||
}
|
||||
|
||||
func (c *Checker) IsExpired(expiry time.Time) bool {
|
||||
return c.now().After(expiry)
|
||||
}
|
||||
```
|
||||
|
||||
测试用固定函数替换 `now`:
|
||||
|
||||
```go
|
||||
c := &Checker{now: func() time.Time {
|
||||
return time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
}}
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
| 场景 | 方法 |
|
||||
|------|------|
|
||||
| 进程级单例(日志、指标) | 默认实例 + `New()` 构造器 |
|
||||
| 一次编译的正则或模板 | 包级 `var` 配合 `MustCompile` |
|
||||
| 注册表(数据库驱动、编解码器) | 包级 `Register()` 函数 |
|
||||
| 可配置行为 | 通过构造器进行依赖注入 |
|
||||
| 时间相关逻辑 | 注入 `func() time.Time` |
|
||||
| 测试需要变化的任何东西 | 不要使用全局状态 |
|
||||
@@ -0,0 +1,92 @@
|
||||
# Must 函数
|
||||
|
||||
> **来源**:Uber 风格指南, Go 标准库约定
|
||||
|
||||
`Must` 函数包装一个可能失败的函数,在出错时 panic。**仅**在程序初始化阶段使用,因为失败意味着程序无法运行。
|
||||
|
||||
## 标准库示例
|
||||
|
||||
```go
|
||||
// regexp.MustCompile 在模式无效时 panic
|
||||
var validID = regexp.MustCompile(`^[a-z][a-z0-9-]{0,62}$`)
|
||||
|
||||
// template.Must 在模板解析失败时 panic
|
||||
var tmpl = template.Must(template.ParseFiles("index.html"))
|
||||
```
|
||||
|
||||
这些是安全的,因为它们在包初始化时运行——如果失败,程序无法正确运行。
|
||||
|
||||
## 何时使用 Must
|
||||
|
||||
```
|
||||
这是在程序初始化期间调用的吗(包级 var、init、main 设置)?
|
||||
├─ 是 → 失败是否不可恢复(配置、正则、模板)?
|
||||
│ ├─ 是 → 使用 Must 是合适的
|
||||
│ └─ 否 → 改为返回 error
|
||||
└─ 否 → 绝不使用 Must——返回 error
|
||||
```
|
||||
|
||||
### 适当的使用场景
|
||||
|
||||
- **包级 `var`**:编译正则表达式、解析模板、加载必需的配置
|
||||
- **`init()` 或 `main()` 早期**:设置程序运行所必需的资源
|
||||
- **测试辅助函数**:测试中优先使用 `t.Fatal`,但 Must 在测试 fixture 中是可以接受的
|
||||
|
||||
### 绝不使用 Must 的场景
|
||||
|
||||
- 运行时请求处理
|
||||
- 用户提供的输入
|
||||
- 可能合理失败的网络或文件操作
|
||||
- 程序启动后调用的任何代码
|
||||
|
||||
## 编写 Must 函数
|
||||
|
||||
遵循命名约定 `MustX`,其中 `X` 是可能失败的函数名:
|
||||
|
||||
```go
|
||||
func MustParseConfig(path string) *Config {
|
||||
cfg, err := ParseConfig(path)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("parsing config %s: %v", path, err))
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
```
|
||||
|
||||
### 指南
|
||||
|
||||
- **命名**:`Must` 前缀 + 可能失败的函数名(例如 `MustParse`、`MustNew`、`MustCompile`)
|
||||
- **Panic 消息**:包含输入和错误信息以便调试
|
||||
- **文档**:始终记录函数在出错时会 panic
|
||||
|
||||
```go
|
||||
// MustParseConfig 解析路径处的配置文件。
|
||||
// 如果文件无法读取或包含无效配置,则会 panic。
|
||||
func MustParseConfig(path string) *Config { ... }
|
||||
```
|
||||
|
||||
### 泛型 Must 辅助函数
|
||||
|
||||
对于一次性使用,泛型 Must 辅助函数可以避免样板代码:
|
||||
|
||||
```go
|
||||
func Must[T any](v T, err error) T {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// 在包级别使用
|
||||
var cfg = Must(ParseConfig("app.yaml"))
|
||||
```
|
||||
|
||||
## 与 Panic/Recover 的关系
|
||||
|
||||
Must 函数是对 `panic` 的受控使用。它们应该:
|
||||
|
||||
- 仅在初始化期间运行(因此不需要 recover)
|
||||
- 产生清晰、可操作的 panic 消息
|
||||
- 绝不在可以返回 error 的场景中使用
|
||||
|
||||
完整的 panic/recover 模式请参见 [PANIC-RECOVER.md](PANIC-RECOVER.md)。
|
||||
@@ -0,0 +1,161 @@
|
||||
# Panic 与 Recover 模式
|
||||
|
||||
> **来源**:Effective Go
|
||||
|
||||
## Panic 指南
|
||||
|
||||
`panic` 创建一个运行时错误来停止程序。仅在真正不可恢复的情况下使用。
|
||||
|
||||
### 何时 Panic
|
||||
|
||||
真正的库函数应**避免 panic**。如果问题可以被掩盖或绕过,让程序继续运行,而不是让整个程序崩溃。
|
||||
|
||||
```go
|
||||
// 可接受:真正不可能的情况
|
||||
func CubeRoot(x float64) float64 {
|
||||
z := x/3
|
||||
for i := 0; i < 1e6; i++ {
|
||||
prevz := z
|
||||
z -= (z*z*z-x) / (3*z*z)
|
||||
if veryClose(z, prevz) {
|
||||
return z
|
||||
}
|
||||
}
|
||||
// 百万次迭代仍未收敛;出了问题。
|
||||
panic(fmt.Sprintf("CubeRoot(%g) did not converge", x))
|
||||
}
|
||||
```
|
||||
|
||||
### 初始化中的 Panic
|
||||
|
||||
例外:如果库在 `init()` 期间确实无法完成初始化,panic 可能是合理的:
|
||||
|
||||
```go
|
||||
var user = os.Getenv("USER")
|
||||
|
||||
func init() {
|
||||
if user == "" {
|
||||
panic("no value for $USER")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 何时 Panic 是可接受的
|
||||
|
||||
除了初始化之外,panic 在以下窄泛场景中是可接受的:
|
||||
|
||||
1. **API 误用**——类似于核心语言对越界访问的 panic。`reflect` 包使用了这种方法。
|
||||
2. **带有匹配 `recover` 的内部实现细节**在包边界处。Panic 简化了深层嵌套的控制流,而公共 API 仍然返回 error(下方的 Parse/parseInt 模式)。
|
||||
3. **`panic("unreachable")`** 在 `log.Fatal` 之后,当编译器无法检测到不可达代码时。
|
||||
|
||||
#### Parse/parseInt 模式
|
||||
|
||||
在内部使用 panic 来回退复杂的递归,但始终在包边界处转换为 error:
|
||||
|
||||
```go
|
||||
func parseInt(in string) int {
|
||||
n, err := strconv.Atoi(in)
|
||||
if err != nil {
|
||||
panic(&syntaxError{"not a valid integer"})
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func Parse(in string) (_ *Node, err error) {
|
||||
defer func() {
|
||||
if p := recover(); p != nil {
|
||||
sErr, ok := p.(*syntaxError)
|
||||
if !ok {
|
||||
panic(p) // 不是我们的——重新 panic
|
||||
}
|
||||
err = fmt.Errorf("syntax error: %v", sErr.msg)
|
||||
}
|
||||
}()
|
||||
// ... 内部调用 parseInt
|
||||
}
|
||||
```
|
||||
|
||||
**关键**:类型检查 `p.(*syntaxError)` 确保只捕获*我们的* panic。意外的 panic(nil 指针等)正常传播。
|
||||
|
||||
---
|
||||
|
||||
## Recover 模式
|
||||
|
||||
`recover` 重新获得对正在 panic 的 goroutine 的控制。它只在延迟函数中有效。
|
||||
|
||||
### 基本恢复模式
|
||||
|
||||
```go
|
||||
func safelyDo(work *Work) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Println("work failed:", err)
|
||||
}
|
||||
}()
|
||||
do(work)
|
||||
}
|
||||
```
|
||||
|
||||
### 服务器 Goroutine 保护
|
||||
|
||||
在服务器中将 panic 隔离到各个 goroutine:
|
||||
|
||||
```go
|
||||
func server(workChan <-chan *Work) {
|
||||
for work := range workChan {
|
||||
go safelyDo(work) // 每个 worker 都受保护
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
如果 `do(work)` panic,结果会被记录,goroutine 干净退出而不影响其他 goroutine。
|
||||
|
||||
### 包内部的 Panic/Recover
|
||||
|
||||
在内部使用 panic 但在 API 边界处转换为 error:
|
||||
|
||||
```go
|
||||
// Error 是一个解析错误类型
|
||||
type Error string
|
||||
func (e Error) Error() string { return string(e) }
|
||||
|
||||
// 内部:使用 Error 类型 panic
|
||||
func (regexp *Regexp) error(err string) {
|
||||
panic(Error(err))
|
||||
}
|
||||
|
||||
// 外部 API:将 panic 转换为 error 返回
|
||||
func Compile(str string) (regexp *Regexp, err error) {
|
||||
regexp = new(Regexp)
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
regexp = nil
|
||||
err = e.(Error) // 如果不是我们的 Error 类型则重新 panic
|
||||
}
|
||||
}()
|
||||
return regexp.doParse(str), nil
|
||||
}
|
||||
```
|
||||
|
||||
**要点:**
|
||||
|
||||
- 延迟函数可以修改命名返回值
|
||||
- 类型断言 `e.(Error)` 对意外错误类型重新 panic
|
||||
- 绝不向客户端暴露 panic——始终在 API 边界处转换
|
||||
|
||||
---
|
||||
|
||||
## 快速参考
|
||||
|
||||
| 模式 | 描述 |
|
||||
|------|------|
|
||||
| 基本恢复 | `defer func() { if err := recover(); err != nil { ... } }()` |
|
||||
| 服务器保护 | 将每个 goroutine 处理器包装在 safelyDo 中 |
|
||||
| 包内部 | 内部 panic,在 API 边界处 recover 并返回 error |
|
||||
| 类型安全恢复 | 使用类型断言对意外错误重新 panic |
|
||||
|
||||
## 何时使用
|
||||
|
||||
- **Panic**:仅用于真正不可恢复的情况或初始化失败
|
||||
- **Recover**:服务器处理器、包内部错误简化
|
||||
- **绝不**:跨包边界暴露 panic——始终转换为 error
|
||||
@@ -0,0 +1,111 @@
|
||||
# 时间、结构体标签和嵌入模式
|
||||
|
||||
## 使用 time.Time 和 time.Duration
|
||||
|
||||
始终使用 `time` 包。避免使用原始 `int` 表示时间值。
|
||||
|
||||
### 时间点
|
||||
|
||||
**不好**
|
||||
```go
|
||||
func isActive(now, start, stop int) bool {
|
||||
return start <= now && now < stop
|
||||
}
|
||||
```
|
||||
|
||||
**好**
|
||||
```go
|
||||
func isActive(now, start, stop time.Time) bool {
|
||||
return (start.Before(now) || start.Equal(now)) && now.Before(stop)
|
||||
}
|
||||
```
|
||||
|
||||
### 时长
|
||||
|
||||
**不好**
|
||||
```go
|
||||
func poll(delay int) {
|
||||
time.Sleep(time.Duration(delay) * time.Millisecond)
|
||||
}
|
||||
poll(10) // 秒?毫秒?
|
||||
```
|
||||
|
||||
**好**
|
||||
```go
|
||||
func poll(delay time.Duration) {
|
||||
time.Sleep(delay)
|
||||
}
|
||||
poll(10 * time.Second)
|
||||
```
|
||||
|
||||
### JSON 字段
|
||||
|
||||
当无法使用 `time.Duration` 时,在字段名中包含单位:
|
||||
|
||||
**不好**
|
||||
```go
|
||||
type Config struct {
|
||||
Interval int `json:"interval"`
|
||||
}
|
||||
```
|
||||
|
||||
**好**
|
||||
```go
|
||||
type Config struct {
|
||||
IntervalMillis int `json:"intervalMillis"`
|
||||
}
|
||||
```
|
||||
|
||||
## 避免在公共结构体中嵌入类型
|
||||
|
||||
嵌入类型会泄露实现细节并阻碍类型演进。
|
||||
|
||||
**不好**
|
||||
```go
|
||||
type ConcreteList struct {
|
||||
*AbstractList
|
||||
}
|
||||
```
|
||||
|
||||
**好**
|
||||
```go
|
||||
type ConcreteList struct {
|
||||
list *AbstractList
|
||||
}
|
||||
|
||||
func (l *ConcreteList) Add(e Entity) {
|
||||
l.list.Add(e)
|
||||
}
|
||||
|
||||
func (l *ConcreteList) Remove(e Entity) {
|
||||
l.list.Remove(e)
|
||||
}
|
||||
```
|
||||
|
||||
嵌入的问题:
|
||||
- 向嵌入接口添加方法是破坏性变更
|
||||
- 从嵌入结构体移除方法是破坏性变更
|
||||
- 替换嵌入类型是破坏性变更
|
||||
|
||||
## 在序列化结构体中使用字段标签
|
||||
|
||||
始终为 JSON、YAML 等使用显式字段标签。
|
||||
|
||||
**不好**
|
||||
```go
|
||||
type Stock struct {
|
||||
Price int
|
||||
Name string
|
||||
}
|
||||
```
|
||||
|
||||
**好**
|
||||
```go
|
||||
type Stock struct {
|
||||
Price int `json:"price"`
|
||||
Name string `json:"name"`
|
||||
// 可以安全地将 Name 重命名为 Symbol
|
||||
}
|
||||
```
|
||||
|
||||
标签使序列化契约显式化,并可以安全地进行重构。
|
||||
@@ -0,0 +1,167 @@
|
||||
---
|
||||
name: go-documentation
|
||||
description: 在编写或审查 Go 包、类型、函数或方法的文档时使用。在创建新的导出类型、函数或包时也应主动使用,即使用户没有明确询问文档问题。不涵盖未导出符号的代码注释(参见 go-style-core)。
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
sources: "Google 风格指南"
|
||||
allowed-tools: Bash(bash:*)
|
||||
---
|
||||
|
||||
# Go 文档
|
||||
|
||||
## 可用脚本
|
||||
|
||||
- **`scripts/check-docs.sh`** — 报告缺少文档注释的导出函数、类型、方法、常量和包。运行 `bash scripts/check-docs.sh --help` 查看选项。
|
||||
|
||||
> 在为新包或导出类型编写文档注释并需要所有文档约定的完整参考时,请参阅 `assets/doc-template.go`。
|
||||
|
||||
---
|
||||
|
||||
## 文档注释
|
||||
|
||||
> **规范**:所有顶层导出名称必须有文档注释。
|
||||
|
||||
### 基本规则
|
||||
|
||||
1. 以被描述对象的名称开头
|
||||
2. 冠词("a"、"an"、"the")可以放在名称前面
|
||||
3. 使用完整句子(首字母大写,带标点符号)
|
||||
|
||||
```go
|
||||
// A Request represents a request to run a command.
|
||||
type Request struct { ...
|
||||
|
||||
// Encode writes the JSON encoding of req to w.
|
||||
func Encode(w io.Writer, req *Request) { ...
|
||||
```
|
||||
|
||||
行为不明显的未导出类型/函数也应有文档注释。
|
||||
|
||||
> **验证**:添加文档注释后,运行 `bash scripts/check-docs.sh` 验证是否有导出符号缺少文档。修复所有缺失后再继续。
|
||||
|
||||
---
|
||||
|
||||
## 注释语句
|
||||
|
||||
> **规范**:文档注释必须是完整的句子。
|
||||
|
||||
- 首字母大写,以标点符号结尾
|
||||
- 例外:如果含义清晰,可以以小写标识符开头
|
||||
- 结构体字段的行尾注释可以是短语
|
||||
|
||||
---
|
||||
|
||||
## 注释行长度
|
||||
|
||||
> **建议**:目标约 80 列,但不设硬性限制。
|
||||
|
||||
根据标点符号换行。不要拆分长 URL。
|
||||
|
||||
---
|
||||
|
||||
## 结构体文档
|
||||
|
||||
使用段落注释对字段分组。标记可选字段及默认值:
|
||||
|
||||
```go
|
||||
type Options struct {
|
||||
// 通用设置:
|
||||
Name string
|
||||
Group *FooGroup
|
||||
|
||||
// 自定义设置:
|
||||
LargeGroupThreshold int // 可选;默认值:10
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 包注释
|
||||
|
||||
> **规范**:每个包必须有且仅有一个包注释。
|
||||
|
||||
```go
|
||||
// Package math provides basic constants and mathematical functions.
|
||||
package math
|
||||
```
|
||||
|
||||
- 对于 `main` 包,使用二进制名称:`// The seed_generator command ...`
|
||||
- 对于较长的包注释,使用 `doc.go` 文件
|
||||
|
||||
> 在编写包级文档、main 包注释、doc.go 文件或可运行示例时,请阅读 [references/EXAMPLES.md](references/EXAMPLES.md)。
|
||||
|
||||
---
|
||||
|
||||
## 文档编写要点
|
||||
|
||||
> **建议**:记录非显而易见的行为,显而易见的行为无需记录。
|
||||
|
||||
| 主题 | 何时记录... | 何时跳过... |
|
||||
|------|------------|------------|
|
||||
| 参数 | 非显而易见的行为、边界情况 | 只是重复类型签名 |
|
||||
| 上下文 | 行为与标准取消不同 | 标准 `ctx.Err()` 返回 |
|
||||
| 并发 | 线程安全性不明确(例如,看似读取但内部修改) | 只读安全、修改不安全 |
|
||||
| 清理 | 始终记录资源释放要求 | — |
|
||||
| 错误 | 哨兵值、错误类型(使用 `*PathError`) | — |
|
||||
| 命名返回值 | 多个同类型参数、面向操作命名 | 类型本身已足够清晰 |
|
||||
|
||||
关键原则:
|
||||
|
||||
- 上下文取消返回 `ctx.Err()` 是隐含的 — 不要重复说明
|
||||
- 只读操作默认线程安全;修改操作默认不安全 — 不要重复说明
|
||||
- 始终记录清理要求(例如,`Call Stop to release resources`)
|
||||
- 在错误类型文档中使用指针(`*PathError`),以确保 `errors.Is`/`errors.As` 正确使用
|
||||
- 不要仅为启用裸返回而命名返回值 — 清晰性 > 简洁性
|
||||
|
||||
> 在记录参数行为、上下文取消、并发安全性、清理要求、错误返回或函数文档注释中的命名返回参数时,请阅读 [references/CONVENTIONS.md](references/CONVENTIONS.md)。
|
||||
|
||||
---
|
||||
|
||||
## 可运行示例
|
||||
|
||||
> **建议**:在测试文件(`*_test.go`)中提供可运行示例。
|
||||
|
||||
```go
|
||||
func ExampleConfig_WriteTo() {
|
||||
cfg := &Config{Name: "example"}
|
||||
cfg.WriteTo(os.Stdout)
|
||||
// Output:
|
||||
// {"name": "example"}
|
||||
}
|
||||
```
|
||||
|
||||
示例会出现在 Godoc 中,附加到对应的文档元素上。
|
||||
|
||||
> 在编写可运行 Example 函数、选择示例命名约定(Example vs ExampleType_Method)或添加包级 doc.go 文件时,请阅读 [references/EXAMPLES.md](references/EXAMPLES.md)。
|
||||
|
||||
---
|
||||
|
||||
## Godoc 格式化
|
||||
|
||||
> 在格式化 godoc 标题、链接、列表或代码块,使用信号增强来标记弃用通知,或在本地预览文档输出时,请阅读 [references/FORMATTING.md](references/FORMATTING.md)。
|
||||
|
||||
---
|
||||
|
||||
## 快速参考
|
||||
|
||||
| 主题 | 关键规则 |
|
||||
|------|---------|
|
||||
| 文档注释 | 以名称开头,使用完整句子 |
|
||||
| 行长度 | 约 80 字符,优先考虑可读性 |
|
||||
| 包注释 | 每个包一个,放在 `package` 声明之前 |
|
||||
| 参数 | 仅记录非显而易见的行为 |
|
||||
| 上下文 | 记录与隐含行为不同的例外情况 |
|
||||
| 并发 | 记录线程安全性不明确的情况 |
|
||||
| 清理 | 始终记录资源释放要求 |
|
||||
| 错误 | 记录哨兵值和类型(注意指针) |
|
||||
| 示例 | 在测试文件中使用可运行示例 |
|
||||
| 格式化 | 空行分隔段落,缩进表示代码 |
|
||||
|
||||
---
|
||||
|
||||
## 相关技能
|
||||
|
||||
- **命名约定**:在为文档注释描述的标识符选择名称时,参见 [go-naming](../go-naming/SKILL.md)
|
||||
- **测试示例**:在编写出现在 godoc 中的可运行 `Example` 测试函数时,参见 [go-testing](../go-testing/SKILL.md)
|
||||
- **Lint 强制执行**:在使用 revive 或其他 linter 强制执行文档注释存在性时,参见 [go-linting](../go-linting/SKILL.md)
|
||||
- **风格原则**:在平衡文档详细程度与清晰简洁时,参见 [go-style-core](../go-style-core/SKILL.md)
|
||||
@@ -0,0 +1,61 @@
|
||||
// Package example demonstrates proper Go documentation conventions.
|
||||
//
|
||||
// This package shows how to write doc comments for packages, types,
|
||||
// functions, methods, and constants following Google Go Style Guide
|
||||
// conventions.
|
||||
//
|
||||
// # Getting Started
|
||||
//
|
||||
// Create a new Widget with [NewWidget]:
|
||||
//
|
||||
// w := example.NewWidget("name")
|
||||
// defer w.Close()
|
||||
package example
|
||||
|
||||
import "errors"
|
||||
|
||||
// ErrNotFound is returned when a requested item does not exist.
|
||||
var ErrNotFound = errors.New("example: not found")
|
||||
|
||||
// MaxRetries is the default number of retry attempts.
|
||||
const MaxRetries = 3
|
||||
|
||||
// Widget processes items with configurable options.
|
||||
//
|
||||
// A zero-value Widget is not valid; use [NewWidget] to create one.
|
||||
// Widget is safe for concurrent use.
|
||||
//
|
||||
// # Cleanup
|
||||
//
|
||||
// Call [Widget.Close] when done to release resources.
|
||||
type Widget struct {
|
||||
name string
|
||||
}
|
||||
|
||||
// NewWidget creates a Widget with the given name.
|
||||
//
|
||||
// Name must be non-empty; NewWidget panics otherwise.
|
||||
func NewWidget(name string) *Widget {
|
||||
if name == "" {
|
||||
panic("example: name must be non-empty")
|
||||
}
|
||||
return &Widget{name: name}
|
||||
}
|
||||
|
||||
// Process handles the given input and returns the result.
|
||||
//
|
||||
// Process returns [ErrNotFound] if the input references
|
||||
// a missing item.
|
||||
func (w *Widget) Process(input string) (string, error) {
|
||||
return input, nil
|
||||
}
|
||||
|
||||
// Close releases resources held by the Widget.
|
||||
func (w *Widget) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deprecated: Use [NewWidget] with functional options instead.
|
||||
func NewWidgetLegacy(name string) *Widget {
|
||||
return NewWidget(name)
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
# 文档约定参考
|
||||
|
||||
## 参数和配置
|
||||
|
||||
> **建议**:记录容易出错或非显而易见的参数,而非所有参数。
|
||||
|
||||
```go
|
||||
// 不好:重复了显而易见的信息
|
||||
// Sprintf formats according to a format specifier and returns the resulting string.
|
||||
//
|
||||
// format is the format, and data is the interpolation data.
|
||||
func Sprintf(format string, data ...any) string
|
||||
|
||||
// 好:记录了非显而易见的行为
|
||||
// Sprintf formats according to a format specifier and returns the resulting string.
|
||||
//
|
||||
// The provided data is used to interpolate the format string. If the data does
|
||||
// not match the expected format verbs or the amount of data does not satisfy
|
||||
// the format specification, the function will inline warnings about formatting
|
||||
// errors into the output string.
|
||||
func Sprintf(format string, data ...any) string
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 上下文
|
||||
|
||||
> **建议**:不要重复隐含的上下文行为;记录例外情况。
|
||||
|
||||
上下文取消被隐含地认为会中断函数并返回 `ctx.Err()`。不要记录这一点。
|
||||
|
||||
```go
|
||||
// 不好:重复了隐含的行为
|
||||
// Run executes the worker's run loop.
|
||||
//
|
||||
// The method will process work until the context is cancelled.
|
||||
func (Worker) Run(ctx context.Context) error
|
||||
|
||||
// 好:只记录关键信息
|
||||
// Run executes the worker's run loop.
|
||||
func (Worker) Run(ctx context.Context) error
|
||||
```
|
||||
|
||||
**当行为不同时记录:**
|
||||
|
||||
```go
|
||||
// 好:非标准的取消行为
|
||||
// Run executes the worker's run loop.
|
||||
//
|
||||
// If the context is cancelled, Run returns a nil error.
|
||||
func (Worker) Run(ctx context.Context) error
|
||||
|
||||
// 好:特殊的上下文要求
|
||||
// NewReceiver starts receiving messages sent to the specified queue.
|
||||
// The context should not have a deadline.
|
||||
func NewReceiver(ctx context.Context) *Receiver
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 并发
|
||||
|
||||
> **建议**:记录非显而易见的线程安全特性。
|
||||
|
||||
只读操作被认为是安全的;修改操作被认为是不安全的。不要重复说明这一点。
|
||||
|
||||
**何时记录:**
|
||||
|
||||
```go
|
||||
// 不明确的操作(看似只读但内部有修改)
|
||||
// Lookup returns the data associated with the key from the cache.
|
||||
//
|
||||
// This operation is not safe for concurrent use.
|
||||
func (*Cache) Lookup(key string) (data []byte, ok bool)
|
||||
|
||||
// API 提供同步机制
|
||||
// NewFortuneTellerClient returns an *rpc.Client for the FortuneTeller service.
|
||||
// It is safe for simultaneous use by multiple goroutines.
|
||||
func NewFortuneTellerClient(cc *rpc.ClientConn) *FortuneTellerClient
|
||||
|
||||
// 接口有并发要求
|
||||
// A Watcher reports the health of some entity (usually a backend service).
|
||||
//
|
||||
// Watcher methods are safe for simultaneous use by multiple goroutines.
|
||||
type Watcher interface {
|
||||
Watch(changed chan<- bool) (unwatch func())
|
||||
Health() error
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 清理
|
||||
|
||||
> **建议**:始终记录显式清理要求。
|
||||
|
||||
```go
|
||||
// 好:
|
||||
// NewTicker returns a new Ticker containing a channel that will send the
|
||||
// current time on the channel after each tick.
|
||||
//
|
||||
// Call Stop to release the Ticker's associated resources when done.
|
||||
func NewTicker(d Duration) *Ticker
|
||||
|
||||
// 好:展示如何清理
|
||||
// Get issues a GET to the specified URL.
|
||||
//
|
||||
// When err is nil, resp always contains a non-nil resp.Body.
|
||||
// Caller should close resp.Body when done reading from it.
|
||||
//
|
||||
// resp, err := http.Get("http://example.com/")
|
||||
// if err != nil {
|
||||
// // handle error
|
||||
// }
|
||||
// defer resp.Body.Close()
|
||||
// body, err := io.ReadAll(resp.Body)
|
||||
func (c *Client) Get(url string) (resp *Response, err error)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 错误
|
||||
|
||||
> **建议**:记录重要的错误哨兵值和类型。
|
||||
|
||||
```go
|
||||
// 好:记录哨兵值
|
||||
// Read reads up to len(b) bytes from the File and stores them in b.
|
||||
//
|
||||
// At end of file, Read returns 0, io.EOF.
|
||||
func (*File) Read(b []byte) (n int, err error)
|
||||
|
||||
// 好:记录错误类型(包含指针接收者)
|
||||
// Chdir changes the current working directory to the named directory.
|
||||
//
|
||||
// If there is an error, it will be of type *PathError.
|
||||
func Chdir(dir string) error
|
||||
```
|
||||
|
||||
注意使用 `*PathError`(而非 `PathError`)可以确保 `errors.Is` 和 `errors.As` 的正确使用。
|
||||
|
||||
对于包级别的错误约定,在包注释中记录。
|
||||
|
||||
---
|
||||
|
||||
## 命名返回参数
|
||||
|
||||
> **建议**:在类型本身不够清晰时用于文档说明。
|
||||
|
||||
```go
|
||||
// 好:多个同类型参数
|
||||
func (n *Node) Children() (left, right *Node, err error)
|
||||
|
||||
// 好:面向操作的名称阐明了用法
|
||||
// The caller must arrange for the returned cancel function to be called.
|
||||
func WithTimeout(parent Context, d time.Duration) (ctx Context, cancel func())
|
||||
|
||||
// 不好:类型已经很清晰,命名没有增加信息
|
||||
func (n *Node) Parent1() (node *Node)
|
||||
func (n *Node) Parent2() (node *Node, err error)
|
||||
|
||||
// 好:类型已足够
|
||||
func (n *Node) Parent1() *Node
|
||||
func (n *Node) Parent2() (*Node, error)
|
||||
```
|
||||
|
||||
不要仅为启用裸返回而命名返回值。清晰性 > 简洁性。
|
||||
|
||||
---
|
||||
|
||||
## 弃用通知
|
||||
|
||||
> **建议**:使用 `// Deprecated:` 注释标记符号为已弃用。
|
||||
|
||||
`Deprecated:` 段落必须出现在文档注释中紧接在符号之前。应说明使用什么替代。
|
||||
|
||||
**标准格式:**
|
||||
|
||||
```
|
||||
// Deprecated: Use NewThing instead.
|
||||
```
|
||||
|
||||
Godoc 会以特殊的视觉样式渲染 `Deprecated:` 注释,使其容易被发现。
|
||||
|
||||
**函数弃用:**
|
||||
|
||||
```go
|
||||
// EstimateSize returns an approximate byte count.
|
||||
//
|
||||
// Deprecated: Use [Size] instead, which returns an exact count.
|
||||
func EstimateSize(r io.Reader) (int64, error)
|
||||
```
|
||||
|
||||
**类型弃用:**
|
||||
|
||||
```go
|
||||
// LegacyClient talks to the v1 API.
|
||||
//
|
||||
// Deprecated: Use [Client] instead, which supports v2.
|
||||
type LegacyClient struct{ /* ... */ }
|
||||
```
|
||||
|
||||
**包弃用** — 在包文档注释中添加 `Deprecated:`:
|
||||
|
||||
```go
|
||||
// Package old provides the original implementation.
|
||||
//
|
||||
// Deprecated: Use package example/new instead.
|
||||
package old
|
||||
```
|
||||
|
||||
始终建议具体的替代方案,让调用者知道迁移目标。
|
||||
|
||||
---
|
||||
|
||||
## 注释语句 — 详细说明
|
||||
|
||||
> **规范**:文档注释必须是完整的句子。
|
||||
|
||||
- 首字母大写,以标点符号结尾
|
||||
- 例外:如果含义清晰,可以以小写标识符开头
|
||||
- 结构体字段的行尾注释可以是短语:
|
||||
|
||||
```go
|
||||
// 好:
|
||||
// A Server handles serving quotes from Shakespeare.
|
||||
type Server struct {
|
||||
// BaseDir points to the base directory for Shakespeare's works.
|
||||
//
|
||||
// Expected structure:
|
||||
// {BaseDir}/manifest.json
|
||||
// {BaseDir}/{name}/{name}-part{number}.txt
|
||||
BaseDir string
|
||||
|
||||
WelcomeMessage string // 用户登录时显示
|
||||
ProtocolVersion string // 与传入请求进行校验
|
||||
PageLength int // 每页行数(可选;默认值:20)
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,107 @@
|
||||
# 包注释和示例参考
|
||||
|
||||
## 包注释
|
||||
|
||||
> **规范**:每个包必须有且仅有一个包注释。
|
||||
|
||||
```go
|
||||
// 好:
|
||||
// Package math provides basic constants and mathematical functions.
|
||||
//
|
||||
// This package does not guarantee bit-identical results across architectures.
|
||||
package math
|
||||
```
|
||||
|
||||
### Main 包
|
||||
|
||||
使用二进制名称(与 BUILD 文件匹配):
|
||||
|
||||
```go
|
||||
// 好:
|
||||
// The seed_generator command is a utility that generates a Finch seed file
|
||||
// from a set of JSON study configs.
|
||||
package main
|
||||
```
|
||||
|
||||
有效格式:`Binary seed_generator`、`Command seed_generator`、`The seed_generator command`、`Seed_generator ...`
|
||||
|
||||
### doc.go
|
||||
|
||||
- 对于较长的包注释,使用仅包含包注释和 `package` 声明的 `doc.go` 文件
|
||||
- 放在 import 之后的维护者注释不会出现在 Godoc 中
|
||||
- 保持 doc.go 文件专注于面向用户的文档
|
||||
|
||||
```go
|
||||
// Package complex provides advanced mathematical operations for
|
||||
// complex number arithmetic, including polar form conversion,
|
||||
// matrix operations, and numerical integration.
|
||||
//
|
||||
// Basic usage
|
||||
//
|
||||
// Create a complex number and perform operations:
|
||||
//
|
||||
// z := complex.New(3, 4)
|
||||
// magnitude := z.Abs() // 5.0
|
||||
// conjugate := z.Conj() // (3, -4)
|
||||
//
|
||||
// Matrix operations
|
||||
//
|
||||
// The package supports complex-valued matrices:
|
||||
//
|
||||
// m := complex.NewMatrix(2, 2)
|
||||
// m.Set(0, 0, complex.New(1, 0))
|
||||
// det := m.Det()
|
||||
package complex
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 可运行示例
|
||||
|
||||
> **建议**:提供可运行示例来展示包的用法。
|
||||
|
||||
将示例放在测试文件(`*_test.go`)中:
|
||||
|
||||
```go
|
||||
// 好:
|
||||
func ExampleConfig_WriteTo() {
|
||||
cfg := &Config{
|
||||
Name: "example",
|
||||
}
|
||||
if err := cfg.WriteTo(os.Stdout); err != nil {
|
||||
log.Exitf("Failed to write config: %s", err)
|
||||
}
|
||||
// Output:
|
||||
// {
|
||||
// "name": "example"
|
||||
// }
|
||||
}
|
||||
```
|
||||
|
||||
示例会出现在 Godoc 中,附加到对应的文档元素上。
|
||||
|
||||
### 命名约定
|
||||
|
||||
| 函数名称 | 文档对象 |
|
||||
|----------|---------|
|
||||
| `Example()` | 包级别示例 |
|
||||
| `ExampleFoo()` | 函数 `Foo` |
|
||||
| `ExampleBar_Baz()` | 方法 `Bar.Baz` |
|
||||
| `ExampleFoo_suffix()` | `Foo` 示例的命名变体 |
|
||||
|
||||
### 技巧
|
||||
|
||||
- 使用 `// Output:` 注释使示例可通过 `go test` 进行测试和验证
|
||||
- 保持示例专注于展示一个概念
|
||||
- 使用真实但精简的数据
|
||||
- 对于复杂的设置,使用 `testMain` 或辅助函数保持示例主体简洁
|
||||
- 同一符号的多个示例使用小写 `_suffix`:
|
||||
|
||||
```go
|
||||
func ExampleNewClient_withTimeout() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
client := NewClient(ctx)
|
||||
// ...
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,85 @@
|
||||
# Godoc 格式化参考
|
||||
|
||||
## Godoc 格式化
|
||||
|
||||
> **建议**:使用 godoc 语法编写格式良好的文档。
|
||||
|
||||
**段落** - 用空行分隔:
|
||||
|
||||
```go
|
||||
// 好:
|
||||
// LoadConfig reads a configuration out of the named file.
|
||||
//
|
||||
// See some/shortlink for config file format details.
|
||||
```
|
||||
|
||||
**逐字/代码块** - 额外缩进两个空格:
|
||||
|
||||
```go
|
||||
// 好:
|
||||
// Update runs the function in an atomic transaction.
|
||||
//
|
||||
// This is typically used with an anonymous TransactionFunc:
|
||||
//
|
||||
// if err := db.Update(func(state *State) { state.Foo = bar }); err != nil {
|
||||
// //...
|
||||
// }
|
||||
```
|
||||
|
||||
**列表和表格** - 使用逐字格式:
|
||||
|
||||
```go
|
||||
// 好:
|
||||
// LoadConfig treats the following keys in special ways:
|
||||
// "import" will make this configuration inherit from the named file.
|
||||
// "env" if present will be populated with the system environment.
|
||||
```
|
||||
|
||||
**标题** - 单行,首字母大写,无标点(括号/逗号除外),后跟段落:
|
||||
|
||||
```go
|
||||
// 好:
|
||||
// Using headings
|
||||
//
|
||||
// Headings come with autogenerated anchor tags for easy linking.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 信号增强
|
||||
|
||||
> **建议**:添加注释以突出不寻常或容易被忽略的模式。
|
||||
|
||||
以下两种情况很难区分:
|
||||
|
||||
```go
|
||||
if err := doSomething(); err != nil { // 常见
|
||||
// ...
|
||||
}
|
||||
|
||||
if err := doSomething(); err == nil { // 不寻常!
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
添加注释来增强信号:
|
||||
|
||||
```go
|
||||
// 好:
|
||||
if err := doSomething(); err == nil { // 如果没有错误
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 文档预览
|
||||
|
||||
> **建议**:在代码审查之前和期间预览文档。
|
||||
|
||||
```bash
|
||||
go install golang.org/x/pkgsite/cmd/pkgsite@latest
|
||||
pkgsite
|
||||
```
|
||||
|
||||
这可以验证 godoc 格式化是否正确渲染。
|
||||
+298
@@ -0,0 +1,298 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="1.0.0"
|
||||
SCRIPT_NAME="$(basename "$0")"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
$SCRIPT_NAME v$VERSION — Check for missing doc comments on exported Go symbols
|
||||
|
||||
USAGE
|
||||
bash $SCRIPT_NAME [options] [path]
|
||||
|
||||
DESCRIPTION
|
||||
Scans Go source files for exported functions, types, methods, constants,
|
||||
and variables that lack doc comments. Go convention requires all exported
|
||||
symbols to have a doc comment starting with the symbol name.
|
||||
|
||||
Exits 0 if all exports are documented, 1 if undocumented exports found,
|
||||
2 on error.
|
||||
|
||||
OPTIONS
|
||||
-h, --help Show this help message
|
||||
-v, --version Show version
|
||||
--json Output results as JSON
|
||||
--strict Also check unexported types/functions with 5+ lines
|
||||
--limit N Show at most N results (default: all)
|
||||
|
||||
ARGUMENTS
|
||||
path Directory or file to check (default: ./...)
|
||||
|
||||
EXAMPLES
|
||||
bash $SCRIPT_NAME
|
||||
bash $SCRIPT_NAME ./pkg/api
|
||||
bash $SCRIPT_NAME --json .
|
||||
bash $SCRIPT_NAME --strict ./internal/server
|
||||
EOF
|
||||
}
|
||||
|
||||
JSON_OUTPUT=false
|
||||
STRICT=false
|
||||
LIMIT=0
|
||||
TARGET=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help) usage; exit 0 ;;
|
||||
-v|--version) echo "$SCRIPT_NAME v$VERSION"; exit 0 ;;
|
||||
--json) JSON_OUTPUT=true; shift ;;
|
||||
--strict) STRICT=true; shift ;;
|
||||
--limit) LIMIT="${2:?error: --limit requires a number}"; shift 2 ;;
|
||||
-*) echo "error: unknown option: $1" >&2; usage >&2; exit 2 ;;
|
||||
*) TARGET="$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
TARGET="${TARGET:-./...}"
|
||||
|
||||
json_escape() {
|
||||
local s="$1"
|
||||
s="${s//\\/\\\\}"
|
||||
s="${s//\"/\\\"}"
|
||||
s="${s//$'\t'/\\t}"
|
||||
s="${s//$'\r'/}"
|
||||
s="${s//$'\n'/\\n}"
|
||||
printf '%s' "$s"
|
||||
}
|
||||
|
||||
find_go_files() {
|
||||
local t="$1"
|
||||
if [[ -f "$t" ]]; then
|
||||
echo "$t"
|
||||
elif [[ -d "$t" ]]; then
|
||||
find "$t" -name '*.go' ! -name '*_test.go' ! -path '*/vendor/*' ! -path '*/.git/*' 2>/dev/null
|
||||
else
|
||||
local dir="${t%%/...}"
|
||||
dir="${dir:-.}"
|
||||
if [[ -d "$dir" ]]; then
|
||||
find "$dir" -name '*.go' ! -name '*_test.go' ! -path '*/vendor/*' ! -path '*/.git/*' 2>/dev/null
|
||||
else
|
||||
echo "error: path not found: $t" >&2
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
MISSING=()
|
||||
|
||||
add_missing() {
|
||||
local file="$1" line="$2" kind="$3" name="$4"
|
||||
MISSING+=("${file}:${line}|${kind}|${name}")
|
||||
}
|
||||
|
||||
check_file() {
|
||||
local file="$1"
|
||||
local prev_line=""
|
||||
local prev_prev_line=""
|
||||
local line_num=0
|
||||
|
||||
local in_grouped_block=false
|
||||
local grouped_kind=""
|
||||
|
||||
local re_method='^func[[:space:]]+\([^)]+\)[[:space:]]+([A-Z][a-zA-Z0-9]*)\('
|
||||
local re_func='^func[[:space:]]+([A-Z][a-zA-Z0-9]*)\('
|
||||
local re_unexported_func='^func[[:space:]]+([a-z][a-zA-Z0-9]*)\('
|
||||
local re_grouped_open='^(const|var|type)[[:space:]]*\($'
|
||||
local re_exported_type='^type[[:space:]]+([A-Z][a-zA-Z0-9]*)[[:space:]]'
|
||||
local re_unexported_type='^type[[:space:]]+([a-z][a-zA-Z0-9]*)[[:space:]]'
|
||||
local re_exported_const='^const[[:space:]]+([A-Z][a-zA-Z0-9]*)[[:space:]]'
|
||||
local re_exported_var='^var[[:space:]]+([A-Z][a-zA-Z0-9]*)[[:space:]]'
|
||||
local re_grouped_exported='^[[:space:]]+([A-Z][a-zA-Z0-9]*)'
|
||||
local re_grouped_unexported='^[[:space:]]+([a-z][a-zA-Z0-9]*)'
|
||||
|
||||
while IFS= read -r line; do
|
||||
line_num=$((line_num + 1))
|
||||
|
||||
# Check exported function/method declarations
|
||||
if [[ "$line" =~ ^func[[:space:]] ]]; then
|
||||
local name=""
|
||||
local kind=""
|
||||
# Method: func (r *Type) Name(
|
||||
if [[ "$line" =~ $re_method ]]; then
|
||||
name="${BASH_REMATCH[1]}"
|
||||
kind="method"
|
||||
# Function: func Name(
|
||||
elif [[ "$line" =~ $re_func ]]; then
|
||||
name="${BASH_REMATCH[1]}"
|
||||
kind="function"
|
||||
fi
|
||||
|
||||
if [[ -n "$name" ]]; then
|
||||
if ! is_documented "$prev_line" "$prev_prev_line"; then
|
||||
add_missing "$file" "$line_num" "$kind" "$name"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Strict mode: also check unexported functions
|
||||
if $STRICT && [[ -z "$name" ]] && [[ "$line" =~ $re_unexported_func ]]; then
|
||||
name="${BASH_REMATCH[1]}"
|
||||
if ! is_documented "$prev_line" "$prev_prev_line"; then
|
||||
add_missing "$file" "$line_num" "function" "$name"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check exported type declarations
|
||||
if [[ "$line" =~ $re_exported_type ]]; then
|
||||
local name="${BASH_REMATCH[1]}"
|
||||
if ! is_documented "$prev_line" "$prev_prev_line"; then
|
||||
add_missing "$file" "$line_num" "type" "$name"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Strict mode: also check unexported type declarations
|
||||
if $STRICT && [[ "$line" =~ $re_unexported_type ]]; then
|
||||
local name="${BASH_REMATCH[1]}"
|
||||
if ! is_documented "$prev_line" "$prev_prev_line"; then
|
||||
add_missing "$file" "$line_num" "type" "$name"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check exported const (single-line, not in block)
|
||||
if [[ "$line" =~ $re_exported_const ]]; then
|
||||
local name="${BASH_REMATCH[1]}"
|
||||
if ! is_documented "$prev_line" "$prev_prev_line"; then
|
||||
add_missing "$file" "$line_num" "const" "$name"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check exported var (single-line, not blank identifier)
|
||||
if [[ "$line" =~ $re_exported_var ]]; then
|
||||
local name="${BASH_REMATCH[1]}"
|
||||
if ! is_documented "$prev_line" "$prev_prev_line"; then
|
||||
add_missing "$file" "$line_num" "var" "$name"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check package comment
|
||||
if [[ "$line" =~ ^package[[:space:]]+ ]]; then
|
||||
if ! is_documented "$prev_line" "$prev_prev_line"; then
|
||||
local pkg_name
|
||||
pkg_name=$(echo "$line" | sed 's/^package[[:space:]]*//;s/[[:space:]]*$//')
|
||||
add_missing "$file" "$line_num" "package" "$pkg_name"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Track grouped declaration blocks: const ( ... ), var ( ... ), type ( ... )
|
||||
if [[ "$line" =~ $re_grouped_open ]]; then
|
||||
in_grouped_block=true
|
||||
grouped_kind="${BASH_REMATCH[1]}"
|
||||
fi
|
||||
if $in_grouped_block && [[ "$line" =~ ^\)[[:space:]]*$ ]]; then
|
||||
in_grouped_block=false
|
||||
grouped_kind=""
|
||||
fi
|
||||
if $in_grouped_block && [[ -n "$grouped_kind" ]]; then
|
||||
# Check for exported names inside grouped block
|
||||
if [[ "$line" =~ $re_grouped_exported ]]; then
|
||||
local gname="${BASH_REMATCH[1]}"
|
||||
if ! is_documented "$prev_line" "$prev_prev_line"; then
|
||||
add_missing "$file" "$line_num" "$grouped_kind" "$gname"
|
||||
fi
|
||||
fi
|
||||
# Strict: also check unexported names in grouped blocks
|
||||
if $STRICT && [[ "$line" =~ $re_grouped_unexported ]]; then
|
||||
local gname="${BASH_REMATCH[1]}"
|
||||
if ! is_documented "$prev_line" "$prev_prev_line"; then
|
||||
add_missing "$file" "$line_num" "$grouped_kind" "$gname"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
prev_prev_line="$prev_line"
|
||||
prev_line="$line"
|
||||
done < "$file"
|
||||
}
|
||||
|
||||
is_documented() {
|
||||
local prev="$1"
|
||||
local prev_prev="$2"
|
||||
# Previous line is a comment (// or end of block comment */)
|
||||
if [[ "$prev" =~ ^[[:space:]]*//.* ]] || [[ "$prev" =~ \*/[[:space:]]*$ ]]; then
|
||||
return 0
|
||||
fi
|
||||
# Previous line might be empty but line before is comment (allow one blank line)
|
||||
if [[ -z "${prev// /}" ]] && [[ "$prev_prev" =~ ^[[:space:]]*//.* ]]; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
FILES=()
|
||||
while IFS= read -r f; do
|
||||
[[ -n "$f" ]] && FILES+=("$f")
|
||||
done < <(find_go_files "$TARGET")
|
||||
|
||||
if [[ ${#FILES[@]} -eq 0 ]]; then
|
||||
if $JSON_OUTPUT; then
|
||||
echo '{"missing":[],"count":0,"status":"no_go_files"}'
|
||||
else
|
||||
echo "No Go files found in: $TARGET"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for file in "${FILES[@]}"; do
|
||||
check_file "$file"
|
||||
done
|
||||
|
||||
# Truncation
|
||||
TOTAL=${#MISSING[@]}
|
||||
TRUNCATED=false
|
||||
if [[ $LIMIT -gt 0 && $TOTAL -gt $LIMIT ]]; then
|
||||
MISSING=("${MISSING[@]:0:$LIMIT}")
|
||||
TRUNCATED=true
|
||||
fi
|
||||
|
||||
if $JSON_OUTPUT; then
|
||||
echo "{"
|
||||
echo ' "missing": ['
|
||||
first=true
|
||||
for entry in "${MISSING[@]+"${MISSING[@]}"}"; do
|
||||
IFS='|' read -r location kind name <<< "$entry"
|
||||
file="${location%%:*}"
|
||||
line="${location#*:}"
|
||||
$first || echo ","
|
||||
first=false
|
||||
printf ' {"file":"%s","line":%s,"kind":"%s","name":"%s"}' \
|
||||
"$(json_escape "$file")" "$line" "$(json_escape "$kind")" "$(json_escape "$name")"
|
||||
done
|
||||
echo ""
|
||||
echo " ],"
|
||||
printf ' "total": %d,\n' "$TOTAL"
|
||||
printf ' "truncated": %s\n' "$TRUNCATED"
|
||||
echo "}"
|
||||
else
|
||||
if [[ $TOTAL -eq 0 ]]; then
|
||||
echo "All exported symbols are documented."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Undocumented exported symbols:"
|
||||
echo ""
|
||||
for entry in "${MISSING[@]}"; do
|
||||
IFS='|' read -r location kind name <<< "$entry"
|
||||
printf " %s [%s] %s\n" "$location" "$kind" "$name"
|
||||
done
|
||||
if $TRUNCATED; then
|
||||
echo " ... and $((TOTAL - LIMIT)) more (use --limit to adjust)"
|
||||
fi
|
||||
echo ""
|
||||
echo "Total: $TOTAL undocumented symbol(s)"
|
||||
fi
|
||||
|
||||
if [[ $TOTAL -gt 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
@@ -0,0 +1,168 @@
|
||||
---
|
||||
name: go-error-handling
|
||||
description: Use when writing Go code that returns, wraps, or handles errors — choosing between sentinel errors, custom types, and fmt.Errorf (%w vs %v), structuring error flow, or deciding whether to log or return. Also use when propagating errors across package boundaries or using errors.Is/As, even if the user doesn't ask about error strategy. Does not cover panic/recover patterns (see go-defensive).
|
||||
license: Apache-2.0
|
||||
compatibility: Requires Go 1.13+ for errors.Is/errors.As and fmt.Errorf %w wrapping. Structured logging examples use slog (Go 1.21+).
|
||||
metadata:
|
||||
sources: "Google Style Guide, Uber Style Guide"
|
||||
allowed-tools: Bash(bash:*)
|
||||
---
|
||||
|
||||
# Go 错误处理
|
||||
|
||||
## 可用脚本
|
||||
|
||||
- **`scripts/check-errors.sh`** — 检测错误处理反模式:对 `err.Error()` 进行字符串比较、没有上下文的裸 `return err`、以及日志并返回违规。运行 `bash scripts/check-errors.sh --help` 查看选项。
|
||||
|
||||
在 Go 中,[错误是值](https://go.dev/blog/errors-are-values) — 它们由代码创建,也由代码消费。
|
||||
|
||||
## 选择错误策略
|
||||
|
||||
1. 系统边界(RPC、IPC、存储)?→ 使用 `%v` 包装以避免泄露内部细节
|
||||
2. 调用者需要匹配特定条件?→ 哨兵或类型化错误,使用 `%w` 包装
|
||||
3. 调用者只需要调试上下文?→ `fmt.Errorf("...: %w", err)`
|
||||
4. 叶子函数,无需包装?→ 直接返回错误
|
||||
|
||||
**默认**:使用 `%w` 包装,并将其放在格式字符串的末尾。
|
||||
|
||||
---
|
||||
|
||||
## 核心规则
|
||||
|
||||
### 永不返回具体错误类型
|
||||
|
||||
**永不从导出函数返回具体错误类型** — 具体的 `nil` 指针可能变成非 nil 接口:
|
||||
|
||||
```go
|
||||
// 不好:具体类型可能导致微妙的 bug
|
||||
func Bad() *os.PathError { /*...*/ }
|
||||
|
||||
// 好:始终返回 error 接口
|
||||
func Good() error { /*...*/ }
|
||||
```
|
||||
|
||||
### 错误字符串
|
||||
|
||||
错误字符串**不应**大写,也**不应**以标点符号结尾。例外:导出名称、专有名词或缩写。
|
||||
|
||||
```go
|
||||
// 不好
|
||||
err := fmt.Errorf("Something bad happened.")
|
||||
|
||||
// 好
|
||||
err := fmt.Errorf("something bad happened")
|
||||
```
|
||||
|
||||
对于显示的消息(日志、测试失败、API 响应),大写是适当的。
|
||||
|
||||
### 出错时的返回值
|
||||
|
||||
当函数返回错误时,调用者必须将所有非错误返回值视为未指定,除非有明确文档说明。
|
||||
|
||||
**提示**:接受 `context.Context` 的函数通常应返回 `error`,以便调用者判断上下文是否被取消。
|
||||
|
||||
---
|
||||
|
||||
## 处理错误
|
||||
|
||||
遇到错误时,做出**深思熟虑的选择** — 不要用 `_` 丢弃:
|
||||
|
||||
1. **立即处理** — 解决错误并继续
|
||||
2. **返回给调用者** — 可选择用上下文包装
|
||||
3. **在特殊情况下** — `log.Fatal` 或 `panic`
|
||||
|
||||
有意忽略时:添加注释说明原因。
|
||||
|
||||
```go
|
||||
n, _ := b.Write(p) // 永不返回非 nil 错误
|
||||
```
|
||||
|
||||
对于相关的并发操作,使用 [`errgroup`](https://pkg.go.dev/golang.org/x/sync/errgroup):
|
||||
|
||||
```go
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
g.Go(func() error { return task1(ctx) })
|
||||
g.Go(func() error { return task2(ctx) })
|
||||
if err := g.Wait(); err != nil { return err }
|
||||
```
|
||||
|
||||
### 避免带内错误
|
||||
|
||||
不要返回 `-1`、`nil` 或空字符串来表示错误。使用多返回值:
|
||||
|
||||
```go
|
||||
// 不好:带内错误值
|
||||
func Lookup(key string) int // 缺失时返回 -1
|
||||
|
||||
// 好:显式的 error 或 ok 值
|
||||
func Lookup(key string) (string, bool)
|
||||
```
|
||||
|
||||
这可以防止调用者写出 `Parse(Lookup(key))` — 它会导致编译时错误,因为 `Lookup(key)` 有 2 个输出。
|
||||
|
||||
---
|
||||
|
||||
## 错误流程
|
||||
|
||||
在正常代码之前处理错误。提前返回使正常路径保持无缩进:
|
||||
|
||||
```go
|
||||
// 好:错误优先,正常代码无缩进
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 正常代码
|
||||
```
|
||||
|
||||
**错误只处理一次** — 记录日志或返回,不要两者都做:
|
||||
|
||||
```
|
||||
遇到错误?
|
||||
├─ 调用者可以采取行动?→ 返回(通过 %w 附带上下文)
|
||||
├─ 在调用链顶部?→ 记录日志并处理
|
||||
└─ 都不是?→ 以适当级别记录日志,继续执行
|
||||
```
|
||||
|
||||
> 在组织复杂的错误流程、决定记录日志还是返回、实现一次处理模式、或选择结构化日志级别时,请阅读 [references/ERROR-FLOW.md](references/ERROR-FLOW.md)。
|
||||
|
||||
---
|
||||
|
||||
## 错误类型
|
||||
|
||||
> **建议**:推荐的最佳实践。
|
||||
|
||||
| 调用者需要匹配? | 消息类型 | 使用方式 |
|
||||
|-----------------|---------|---------|
|
||||
| 否 | 静态 | `errors.New("message")` |
|
||||
| 否 | 动态 | `fmt.Errorf("msg: %v", val)` |
|
||||
| 是 | 静态 | `var ErrFoo = errors.New("...")` |
|
||||
| 是 | 动态 | 自定义 `error` 类型 |
|
||||
|
||||
**默认**:使用 `fmt.Errorf("...: %w", err)` 包装。升级为哨兵以使用 `errors.Is()`,升级为自定义类型以使用 `errors.As()`。
|
||||
|
||||
> 在定义哨兵错误、创建自定义错误类型、或为包 API 选择错误策略时,请阅读 [references/ERROR-TYPES.md](references/ERROR-TYPES.md)。
|
||||
|
||||
---
|
||||
|
||||
## 错误包装
|
||||
|
||||
> **建议**:推荐的最佳实践。
|
||||
|
||||
- **使用 `%v`**:在系统边界、用于日志记录、隐藏内部细节
|
||||
- **使用 `%w`**:保留错误链以供 `errors.Is`/`errors.As` 使用
|
||||
|
||||
**关键规则**:将 `%w` 放在末尾。添加调用者没有的上下文。如果注释没有增加信息,直接返回 `err`。
|
||||
|
||||
> 在决定使用 %v 还是 %w、跨包边界包装错误、或添加上下文信息时,请阅读 [references/WRAPPING.md](references/WRAPPING.md)。
|
||||
|
||||
> **验证**:实现错误处理后,运行 `bash scripts/check-errors.sh` 检测常见的反模式。然后运行 `go vet ./...` 捕获其他问题。
|
||||
|
||||
---
|
||||
|
||||
## 相关技能
|
||||
|
||||
- **错误命名**:在命名哨兵错误(`ErrFoo`)或自定义错误类型时,参见 [go-naming](../go-naming/SKILL.md)
|
||||
- **测试错误**:在使用 `errors.Is`/`errors.As` 测试错误语义或编写错误检查辅助函数时,参见 [go-testing](../go-testing/SKILL.md)
|
||||
- **Panic 处理**:在决定 panic 还是返回错误、或编写 recover 守卫时,参见 [go-defensive](../go-defensive/SKILL.md)
|
||||
- **守卫子句**:在组织提前返回的错误流程或减少嵌套时,参见 [go-control-flow](../go-control-flow/SKILL.md)
|
||||
- **日志决策**:在选择日志级别、配置结构化日志、或决定日志消息中包含什么上下文时,参见 [go-logging](../go-logging/SKILL.md)
|
||||
@@ -0,0 +1,153 @@
|
||||
# 错误流程模式
|
||||
|
||||
错误流程、一次处理原则和日志决策的详细模式。
|
||||
|
||||
## 缩进错误流程
|
||||
|
||||
在继续正常代码之前先处理错误。这通过使读者能够快速找到正常路径来提高可读性。
|
||||
|
||||
```go
|
||||
// 好:错误处理优先,正常代码无缩进
|
||||
if err != nil {
|
||||
// 错误处理
|
||||
return // 或 continue 等
|
||||
}
|
||||
// 正常代码
|
||||
```
|
||||
|
||||
```go
|
||||
// 不好:正常代码隐藏在 else 子句中
|
||||
if err != nil {
|
||||
// 错误处理
|
||||
} else {
|
||||
// 正常代码因缩进看起来不自然
|
||||
}
|
||||
```
|
||||
|
||||
### 避免对长期使用的变量使用 if 初始化语句
|
||||
|
||||
如果变量在多行中使用,将声明移出:
|
||||
|
||||
```go
|
||||
// 好:声明与错误检查分开
|
||||
x, err := f()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 大量使用 x 的代码
|
||||
// 跨越多行
|
||||
```
|
||||
|
||||
```go
|
||||
// 不好:变量作用域限制在 else 块中,难以阅读
|
||||
if x, err := f(); err != nil {
|
||||
return err
|
||||
} else {
|
||||
// 大量使用 x 的代码
|
||||
// 跨越多行
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 错误只处理一次
|
||||
|
||||
当调用者收到错误时,应该**只处理一次**。选择一种响应方式:
|
||||
|
||||
1. **返回错误**(包装或原文)让调用者处理
|
||||
2. **记录日志并优雅降级**(不返回错误)
|
||||
3. **匹配并处理**特定错误情况,返回其他错误
|
||||
|
||||
**如果返回了错误,就不要自己记录日志** — 让调用者处理。对同一错误既记录日志又返回是最常见的"一次处理"违规,导致重复噪音,因为调用栈上层的调用者也会处理该错误。
|
||||
|
||||
```go
|
||||
// 不好:既记录日志又返回 — 导致日志噪音
|
||||
u, err := getUser(id)
|
||||
if err != nil {
|
||||
log.Printf("Could not get user %q: %v", id, err)
|
||||
return err // 调用者也会记录这个!
|
||||
}
|
||||
|
||||
// 好:包装并返回 — 让调用者决定如何处理
|
||||
u, err := getUser(id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get user %q: %w", id, err)
|
||||
}
|
||||
|
||||
// 好:记录日志并优雅降级(不返回错误)
|
||||
if err := emitMetrics(); err != nil {
|
||||
// 写入指标失败不应影响应用程序
|
||||
log.Printf("Could not emit metrics: %v", err)
|
||||
}
|
||||
// 继续执行...
|
||||
|
||||
// 好:匹配特定错误,返回其他错误
|
||||
tz, err := getUserTimeZone(id)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrUserNotFound) {
|
||||
// 用户不存在,使用 UTC
|
||||
tz = time.UTC
|
||||
} else {
|
||||
return fmt.Errorf("get user %q: %w", id, err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 记录日志 vs 返回错误
|
||||
|
||||
> 错误只处理一次 — 记录日志或返回,不要两者都做。
|
||||
|
||||
### 决策流程
|
||||
|
||||
```
|
||||
遇到错误?
|
||||
├─ 调用者可以采取行动?→ 返回错误(通过 %w 附带上下文)
|
||||
├─ 在调用链顶部?→ 记录日志并处理(返回 HTTP 状态码、退出等)
|
||||
└─ 都不是?→ 以适当级别记录日志并继续
|
||||
```
|
||||
|
||||
### 不要既记录日志又返回
|
||||
|
||||
```go
|
||||
// 不好:错误既被记录又被返回 — 在日志中出现两次
|
||||
func process(ctx context.Context, id string) error {
|
||||
result, err := fetch(ctx, id)
|
||||
if err != nil {
|
||||
log.Printf("failed to fetch %s: %v", id, err)
|
||||
return fmt.Errorf("fetching %s: %w", id, err)
|
||||
}
|
||||
return handle(result)
|
||||
}
|
||||
|
||||
// 好:带上下文返回 — 让调用者决定是否记录日志
|
||||
func process(ctx context.Context, id string) error {
|
||||
result, err := fetch(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetching %s: %w", id, err)
|
||||
}
|
||||
return handle(result)
|
||||
}
|
||||
```
|
||||
|
||||
### 结构化日志
|
||||
|
||||
在生产代码中,优先使用结构化日志(Go 1.21+ 的 `slog`,或 `log/slog` 兼容库)而非 `log.Printf`:
|
||||
|
||||
```go
|
||||
// 好:结构化字段可被机器解析
|
||||
slog.Error("fetch failed", "id", id, "err", err)
|
||||
|
||||
// 避免:非结构化的字符串插值
|
||||
log.Printf("fetch failed for %s: %v", id, err)
|
||||
```
|
||||
|
||||
### 日志级别
|
||||
|
||||
| 级别 | 使用场景 |
|
||||
|------|---------|
|
||||
| Error | 需要关注的可操作故障 |
|
||||
| Warn | 不需要立即处理的降级行为 |
|
||||
| Info | 关键生命周期事件(启动、关闭、配置加载) |
|
||||
| Debug | 开发期间有用的诊断细节 |
|
||||
@@ -0,0 +1,151 @@
|
||||
# 错误类型参考
|
||||
|
||||
本参考涵盖结构化错误类型、哨兵错误,以及如何为你的用例选择正确的错误类型。
|
||||
|
||||
---
|
||||
|
||||
## 错误结构
|
||||
|
||||
> 错误类型决策表在父技能中(SKILL.md § 错误类型)。
|
||||
> 本参考涵盖:扩展的代码示例、哨兵错误、使用 `errors.Is`/`errors.As` 进行错误检查,以及结构化错误类型。
|
||||
|
||||
**关键考虑因素**:
|
||||
|
||||
- 调用者是否需要使用 `errors.Is` 或 `errors.As` 来匹配错误?
|
||||
- 错误消息是静态的还是需要运行时值?
|
||||
- 导出的错误变量/类型将成为公共 API 的一部分
|
||||
|
||||
```go
|
||||
// 无需匹配,静态消息
|
||||
func Open() error {
|
||||
return errors.New("could not open")
|
||||
}
|
||||
|
||||
// 需要匹配,静态消息 - 导出哨兵
|
||||
var ErrCouldNotOpen = errors.New("could not open")
|
||||
|
||||
func Open() error {
|
||||
return ErrCouldNotOpen
|
||||
}
|
||||
|
||||
// 需要匹配,动态消息 - 使用自定义类型
|
||||
type NotFoundError struct {
|
||||
File string
|
||||
}
|
||||
|
||||
func (e *NotFoundError) Error() string {
|
||||
return fmt.Sprintf("file %q not found", e.File)
|
||||
}
|
||||
|
||||
func Open(file string) error {
|
||||
return &NotFoundError{File: file}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 哨兵错误
|
||||
|
||||
最简单的结构化错误是无参数化的全局值:
|
||||
|
||||
```go
|
||||
// 好:用于程序化检查的哨兵错误
|
||||
var (
|
||||
// ErrDuplicate 在该动物已被见过时发生。
|
||||
ErrDuplicate = errors.New("duplicate")
|
||||
|
||||
// ErrMarsupial 因为我们不支持有袋类动物。
|
||||
ErrMarsupial = errors.New("marsupials are not supported")
|
||||
)
|
||||
|
||||
func process(animal Animal) error {
|
||||
switch {
|
||||
case seen[animal]:
|
||||
return ErrDuplicate
|
||||
case marsupial(animal):
|
||||
return ErrMarsupial
|
||||
}
|
||||
seen[animal] = true
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 检查错误
|
||||
|
||||
对于直接比较(当错误未被包装时):
|
||||
|
||||
```go
|
||||
// 好:与哨兵直接比较
|
||||
switch err := process(an); err {
|
||||
case ErrDuplicate:
|
||||
return fmt.Errorf("feed %q: %v", an, err)
|
||||
case ErrMarsupial:
|
||||
alternate := an.BackupAnimal()
|
||||
return handlePet(alternate)
|
||||
}
|
||||
```
|
||||
|
||||
当错误可能被包装时,使用 `errors.Is`:
|
||||
|
||||
```go
|
||||
// 好:适用于被包装的错误
|
||||
switch err := process(an); {
|
||||
case errors.Is(err, ErrDuplicate):
|
||||
return fmt.Errorf("feed %q: %v", an, err)
|
||||
case errors.Is(err, ErrMarsupial):
|
||||
// 尝试恢复...
|
||||
}
|
||||
```
|
||||
|
||||
**绝不**基于字符串内容匹配错误:
|
||||
|
||||
```go
|
||||
// 不好:脆弱的字符串匹配
|
||||
if regexp.MatchString(`duplicate`, err.Error()) {...}
|
||||
if regexp.MatchString(`marsupial`, err.Error()) {...}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 结构化错误类型
|
||||
|
||||
对于需要额外程序化信息的错误,使用结构体类型:
|
||||
|
||||
```go
|
||||
// 好:具有可访问字段的结构化错误
|
||||
type PathError struct {
|
||||
Op string
|
||||
Path string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *PathError) Error() string {
|
||||
return e.Op + " " + e.Path + ": " + e.Err.Error()
|
||||
}
|
||||
|
||||
func (e *PathError) Unwrap() error { return e.Err }
|
||||
```
|
||||
|
||||
调用者可以使用 `errors.As` 提取结构化错误:
|
||||
|
||||
```go
|
||||
var pathErr *os.PathError
|
||||
if errors.As(err, &pathErr) {
|
||||
fmt.Println("Failed path:", pathErr.Path)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 快速参考
|
||||
|
||||
| 场景 | 错误类型 |
|
||||
|------|---------|
|
||||
| 无需匹配,静态消息 | `errors.New("message")` |
|
||||
| 无需匹配,动态消息 | `fmt.Errorf("msg: %v", val)` |
|
||||
| 需要匹配,静态消息 | `var ErrFoo = errors.New(...)` |
|
||||
| 需要匹配,动态消息 | 自定义结构体类型 |
|
||||
| 检查哨兵错误 | `errors.Is(err, ErrFoo)` |
|
||||
| 提取结构化错误 | `errors.As(err, &target)` |
|
||||
@@ -0,0 +1,174 @@
|
||||
# 错误包装参考
|
||||
|
||||
本参考涵盖使用 `%v` vs `%w` 的错误包装、放置约定、向错误添加上下文以及日志最佳实践。
|
||||
|
||||
---
|
||||
|
||||
## 包装错误:%v vs %w
|
||||
|
||||
> **建议**:推荐的最佳实践。
|
||||
|
||||
`%v` 和 `%w` 的选择会显著影响错误的传播和检查方式。
|
||||
|
||||
### 使用 %v 进行简单注释
|
||||
|
||||
当你需要以下操作时使用 `%v`:
|
||||
|
||||
- 添加上下文但不保留错误链以供程序化检查
|
||||
- 创建全新的、独立的错误(特别是在 RPC/IPC 等系统边界)
|
||||
- 向人类记录或显示错误
|
||||
|
||||
```go
|
||||
// 好:%v 在系统边界 — 隐藏内部细节
|
||||
func (s *Server) SuggestFortune(ctx context.Context, req *pb.Request) (*pb.Response, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("couldn't find fortune database: %v", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用 %w 保留错误链
|
||||
|
||||
当你需要调用者以编程方式检查底层错误时使用 `%w`:
|
||||
|
||||
```go
|
||||
// 好:%w 保留错误链以供 errors.Is/errors.As 使用
|
||||
func (s *Server) internalFunction(ctx context.Context) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't find remote file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用者现在可以检查:
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
// 处理未找到的情况
|
||||
}
|
||||
```
|
||||
|
||||
### 何时使用哪种
|
||||
|
||||
**使用 %w 的场景**:
|
||||
- 在添加上下文的同时保留原始错误以供程序化检查
|
||||
- 你明确记录并测试了所暴露的底层错误
|
||||
|
||||
**使用 %v 的场景**:
|
||||
- 在系统边界(RPC、IPC、存储)转换为规范错误空间
|
||||
- 向人类记录日志或显示
|
||||
- 创建隐藏实现细节的独立错误
|
||||
|
||||
---
|
||||
|
||||
## %w 的放置位置
|
||||
|
||||
> **建议**:推荐的最佳实践。
|
||||
|
||||
将 `%w` 放在错误字符串的**末尾**,使错误文本反映错误链结构:
|
||||
|
||||
```go
|
||||
// 好:%w 在末尾 — 从最新到最旧打印
|
||||
err1 := fmt.Errorf("err1")
|
||||
err2 := fmt.Errorf("err2: %w", err1)
|
||||
err3 := fmt.Errorf("err3: %w", err2)
|
||||
fmt.Println(err3) // err3: err2: err1
|
||||
```
|
||||
|
||||
```go
|
||||
// 不好:%w 在开头 — 从最旧到最新打印(令人困惑)
|
||||
err1 := fmt.Errorf("err1")
|
||||
err2 := fmt.Errorf("%w: err2", err1)
|
||||
err3 := fmt.Errorf("%w: err3", err2)
|
||||
fmt.Println(err3) // err1: err2: err3
|
||||
```
|
||||
|
||||
```go
|
||||
// 不好:%w 在中间 — 不连贯的顺序
|
||||
err1 := fmt.Errorf("err1")
|
||||
err2 := fmt.Errorf("err2-1 %w err2-2", err1)
|
||||
err3 := fmt.Errorf("err3-1 %w err3-2", err2)
|
||||
fmt.Println(err3) // err3-1 err2-1 err1 err2-2 err3-2
|
||||
```
|
||||
|
||||
**模式**:使用 `context message: %w` 的形式
|
||||
|
||||
---
|
||||
|
||||
## 向错误添加信息
|
||||
|
||||
> **建议**:推荐的最佳实践。
|
||||
|
||||
### 添加上下文,而非冗余
|
||||
|
||||
添加你拥有但调用者/被调用者可能没有的信息。避免重复底层错误已提供的信息:
|
||||
|
||||
```go
|
||||
// 好:添加有意义的上下文
|
||||
if err := os.Open("settings.txt"); err != nil {
|
||||
return fmt.Errorf("launch codes unavailable: %v", err)
|
||||
}
|
||||
// 输出:launch codes unavailable: open settings.txt: no such file or directory
|
||||
```
|
||||
|
||||
```go
|
||||
// 不好:重复了文件名
|
||||
if err := os.Open("settings.txt"); err != nil {
|
||||
return fmt.Errorf("could not open settings.txt: %v", err)
|
||||
}
|
||||
// 输出:could not open settings.txt: open settings.txt: no such file or directory
|
||||
```
|
||||
|
||||
### 不要无目的地注释
|
||||
|
||||
如果注释仅表示失败而没有添加信息,直接返回错误:
|
||||
|
||||
```go
|
||||
// 不好:注释没有增加信息
|
||||
return fmt.Errorf("failed: %v", err)
|
||||
|
||||
// 好:直接返回错误
|
||||
return err
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 记录错误日志
|
||||
|
||||
> **建议**:推荐的最佳实践。
|
||||
|
||||
当需要记录错误时,使用 `log/slog`(Go 1.21+)配合结构化键值对和适当的日志级别:
|
||||
|
||||
- **`slog.Error`**:保留用于需要调查的可操作问题。
|
||||
- **`slog.Warn`**:用于可能需要关注但不可立即操作的问题。
|
||||
- **`slog.Debug`**:用于开发追踪 — 仅在 handler 级别设为 `LevelDebug` 时才输出。
|
||||
|
||||
```go
|
||||
// 好:使用适当级别的结构化日志
|
||||
for _, q := range queries {
|
||||
slog.Debug("handling query", "query", q)
|
||||
q.Run()
|
||||
}
|
||||
|
||||
// 好:在级别检查后保护昂贵的格式化操作
|
||||
if slog.Default().Enabled(context.Background(), slog.LevelDebug) {
|
||||
slog.Debug("query plan", "explain", q.Explain())
|
||||
}
|
||||
|
||||
// 不好:即使禁用了 debug 日志也会执行昂贵的调用
|
||||
slog.Debug("query plan", "explain", q.Explain())
|
||||
```
|
||||
|
||||
### 保护敏感信息
|
||||
|
||||
注意日志消息中的 PII(个人身份信息)。许多日志接收器不适合存放敏感用户数据。
|
||||
|
||||
---
|
||||
|
||||
## 快速参考
|
||||
|
||||
| 模式 | 指导 |
|
||||
|------|------|
|
||||
| `%v` | 在系统边界使用、用于日志记录、隐藏细节 |
|
||||
| `%w` | 保留错误链以供程序化检查 |
|
||||
| `%w` 放置 | 始终在末尾:`"context: %w"` |
|
||||
| 添加上下文 | 添加新信息,不要重复现有信息 |
|
||||
| 空注释 | 直接返回 `err` 而非 `fmt.Errorf("failed: %v", err)` |
|
||||
| 日志 | 不要既记录日志又返回;使用适当的日志级别 |
|
||||
+266
@@ -0,0 +1,266 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="1.0.0"
|
||||
SCRIPT_NAME="$(basename "$0")"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
$SCRIPT_NAME v$VERSION — Check Go code for common error handling anti-patterns
|
||||
|
||||
USAGE
|
||||
bash $SCRIPT_NAME [options] [path]
|
||||
|
||||
DESCRIPTION
|
||||
Scans Go source files for error handling anti-patterns:
|
||||
- err.Error() used in string comparison (should use errors.Is/As)
|
||||
- Bare 'return err' without wrapping context
|
||||
- Errors that are both logged and returned (handle once)
|
||||
|
||||
Exits 0 if no issues found, 1 if anti-patterns detected, 2 on error.
|
||||
|
||||
OPTIONS
|
||||
-h, --help Show this help message
|
||||
-v, --version Show version
|
||||
--json Output results as JSON
|
||||
--no-bare-return Skip the bare 'return err' check (high false-positive rate)
|
||||
--limit N Show at most N results (default: all)
|
||||
|
||||
ARGUMENTS
|
||||
path Directory or file to check (default: current directory)
|
||||
|
||||
EXAMPLES
|
||||
bash $SCRIPT_NAME
|
||||
bash $SCRIPT_NAME ./pkg/api
|
||||
bash $SCRIPT_NAME --json .
|
||||
bash $SCRIPT_NAME --no-bare-return ./internal
|
||||
EOF
|
||||
}
|
||||
|
||||
JSON_OUTPUT=false
|
||||
CHECK_BARE_RETURN=true
|
||||
LIMIT=0
|
||||
TARGET=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help) usage; exit 0 ;;
|
||||
-v|--version) echo "$SCRIPT_NAME v$VERSION"; exit 0 ;;
|
||||
--json) JSON_OUTPUT=true; shift ;;
|
||||
--no-bare-return) CHECK_BARE_RETURN=false; shift ;;
|
||||
--limit) LIMIT="${2:?error: --limit requires a number}"; shift 2 ;;
|
||||
-*) echo "error: unknown option: $1" >&2; usage >&2; exit 2 ;;
|
||||
*) TARGET="$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
TARGET="${TARGET:-.}"
|
||||
|
||||
json_escape() {
|
||||
local s="$1"
|
||||
s="${s//\\/\\\\}"
|
||||
s="${s//\"/\\\"}"
|
||||
s="${s//$'\t'/\\t}"
|
||||
s="${s//$'\r'/}"
|
||||
s="${s//$'\n'/\\n}"
|
||||
printf '%s' "$s"
|
||||
}
|
||||
|
||||
find_go_files() {
|
||||
local t="$1"
|
||||
if [[ -f "$t" ]]; then
|
||||
echo "$t"
|
||||
elif [[ -d "$t" ]]; then
|
||||
find "$t" -name '*.go' ! -name '*_test.go' ! -path '*/vendor/*' ! -path '*/.git/*' 2>/dev/null
|
||||
else
|
||||
local dir="${t%%/...}"
|
||||
dir="${dir:-.}"
|
||||
if [[ -d "$dir" ]]; then
|
||||
find "$dir" -name '*.go' ! -name '*_test.go' ! -path '*/vendor/*' ! -path '*/.git/*' 2>/dev/null
|
||||
else
|
||||
echo "error: path not found: $t" >&2
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
FINDINGS=()
|
||||
|
||||
add_finding() {
|
||||
local file="$1" line="$2" rule="$3" message="$4"
|
||||
FINDINGS+=("${file}:${line}|${rule}|${message}")
|
||||
}
|
||||
|
||||
# Rule 1: err.Error() in string comparison
|
||||
check_string_error_comparison() {
|
||||
local file="$1"
|
||||
local line_num=0
|
||||
while IFS= read -r line; do
|
||||
line_num=$((line_num + 1))
|
||||
|
||||
# Pattern: err.Error() == "..." or err.Error() != "..."
|
||||
pat='\.Error\(\)[[:space:]]*(==|!=)[[:space:]]*\"'
|
||||
if [[ "$line" =~ $pat ]]; then
|
||||
add_finding "$file" "$line_num" "string-error-compare" \
|
||||
"comparing err.Error() to string; use errors.Is() or errors.As() instead"
|
||||
fi
|
||||
|
||||
# Pattern: strings.Contains(err.Error(), "...")
|
||||
pat_contains='strings\.Contains\(.*\.Error\(\)'
|
||||
if [[ "$line" =~ $pat_contains ]]; then
|
||||
add_finding "$file" "$line_num" "string-error-compare" \
|
||||
"using strings.Contains on err.Error(); use errors.Is() or errors.As() instead"
|
||||
fi
|
||||
|
||||
# Pattern: "..." == err.Error()
|
||||
pat='\"[^\"]*\"[[:space:]]*(==|!=)[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*\.Error\(\)'
|
||||
if [[ "$line" =~ $pat ]]; then
|
||||
add_finding "$file" "$line_num" "string-error-compare" \
|
||||
"comparing string to err.Error(); use errors.Is() or errors.As() instead"
|
||||
fi
|
||||
done < "$file"
|
||||
}
|
||||
|
||||
# Rule 2: Bare return err (no wrapping)
|
||||
check_bare_return_err() {
|
||||
local file="$1"
|
||||
local line_num=0
|
||||
local in_error_block=false
|
||||
|
||||
while IFS= read -r line; do
|
||||
line_num=$((line_num + 1))
|
||||
|
||||
# Detect if err != nil { block
|
||||
pat='if[[:space:]]+(.*err[[:space:]]*(!=|==)[[:space:]]*nil|err[[:space:]]*:=)'
|
||||
if [[ "$line" =~ $pat ]]; then
|
||||
in_error_block=true
|
||||
fi
|
||||
|
||||
# Check for bare "return err" that is not wrapped
|
||||
pat='^[[:space:]]*return[[:space:]]+(.*,)?[[:space:]]*err[[:space:]]*$'
|
||||
if $in_error_block && [[ "$line" =~ $pat ]]; then
|
||||
# Exclude single-line functions and main error handlers
|
||||
# Only flag if the return is just "err" (not fmt.Errorf wrapped)
|
||||
local trimmed
|
||||
trimmed=$(echo "$line" | sed 's/^[[:space:]]*//')
|
||||
if [[ "$trimmed" == "return err" ]]; then
|
||||
add_finding "$file" "$line_num" "bare-return-err" \
|
||||
"bare 'return err' without wrapping context; consider fmt.Errorf('...: %w', err)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Reset error block tracking on closing brace at same indentation
|
||||
pat_close='^[[:space:]]*\}[[:space:]]*$'
|
||||
if $in_error_block && [[ "$line" =~ $pat_close ]]; then
|
||||
in_error_block=false
|
||||
fi
|
||||
done < "$file"
|
||||
}
|
||||
|
||||
# Rule 3: Log-and-return (handle errors once)
|
||||
check_log_and_return() {
|
||||
local file="$1"
|
||||
local line_num=0
|
||||
local prev_lines=()
|
||||
|
||||
while IFS= read -r line; do
|
||||
line_num=$((line_num + 1))
|
||||
prev_lines+=("$line")
|
||||
|
||||
# Keep a small window to detect log followed by return err
|
||||
if [[ ${#prev_lines[@]} -gt 5 ]]; then
|
||||
prev_lines=("${prev_lines[@]:1}")
|
||||
fi
|
||||
|
||||
# Check if current line is 'return ... err' and a recent line logged the error
|
||||
pat='^[[:space:]]*return[[:space:]]+(.*,)?[[:space:]]*err'
|
||||
if [[ "$line" =~ $pat ]]; then
|
||||
local window_size=${#prev_lines[@]}
|
||||
for ((i=0; i<window_size-1; i++)); do
|
||||
local prev="${prev_lines[$i]}"
|
||||
# Match log.Print/Printf/Println/Error/Errorf/Warn/Warnf with err
|
||||
pat_log1='(log\.|logger\.|slog\.)[a-zA-Z]*\(.*[^a-zA-Z]err[^a-zA-Z]'
|
||||
pat_log2='(log\.|logger\.|slog\.)[a-zA-Z]*\(err[,\)]'
|
||||
if [[ "$prev" =~ $pat_log1 ]] || \
|
||||
[[ "$prev" =~ $pat_log2 ]]; then
|
||||
local log_line=$((line_num - window_size + 1 + i))
|
||||
add_finding "$file" "$log_line" "log-and-return" \
|
||||
"error is both logged (line $log_line) and returned (line $line_num); handle errors once"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done < "$file"
|
||||
}
|
||||
|
||||
FILES=()
|
||||
while IFS= read -r f; do
|
||||
[[ -n "$f" ]] && FILES+=("$f")
|
||||
done < <(find_go_files "$TARGET")
|
||||
|
||||
if [[ ${#FILES[@]} -eq 0 ]]; then
|
||||
if $JSON_OUTPUT; then
|
||||
echo '{"findings":[],"count":0,"status":"no_go_files"}'
|
||||
else
|
||||
echo "No Go files found in: $TARGET"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for file in "${FILES[@]}"; do
|
||||
check_string_error_comparison "$file"
|
||||
if $CHECK_BARE_RETURN; then
|
||||
check_bare_return_err "$file"
|
||||
fi
|
||||
check_log_and_return "$file"
|
||||
done
|
||||
|
||||
# Truncation
|
||||
TOTAL=${#FINDINGS[@]}
|
||||
TRUNCATED=false
|
||||
if [[ $LIMIT -gt 0 && $TOTAL -gt $LIMIT ]]; then
|
||||
FINDINGS=("${FINDINGS[@]:0:$LIMIT}")
|
||||
TRUNCATED=true
|
||||
fi
|
||||
|
||||
if $JSON_OUTPUT; then
|
||||
echo "{"
|
||||
echo ' "findings": ['
|
||||
first=true
|
||||
for entry in "${FINDINGS[@]+"${FINDINGS[@]}"}"; do
|
||||
IFS='|' read -r location rule message <<< "$entry"
|
||||
file="${location%%:*}"
|
||||
line="${location#*:}"
|
||||
$first || echo ","
|
||||
first=false
|
||||
printf ' {"file":"%s","line":%s,"rule":"%s","message":"%s"}' \
|
||||
"$(json_escape "$file")" "$line" "$(json_escape "$rule")" "$(json_escape "$message")"
|
||||
done
|
||||
echo ""
|
||||
echo " ],"
|
||||
printf ' "total": %d,\n' "$TOTAL"
|
||||
printf ' "truncated": %s\n' "$TRUNCATED"
|
||||
echo "}"
|
||||
else
|
||||
if [[ $TOTAL -eq 0 ]]; then
|
||||
echo "No error handling anti-patterns found."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Error handling anti-patterns found:"
|
||||
echo ""
|
||||
for entry in "${FINDINGS[@]}"; do
|
||||
IFS='|' read -r location rule message <<< "$entry"
|
||||
printf " %s [%s] %s\n" "$location" "$rule" "$message"
|
||||
done
|
||||
if $TRUNCATED; then
|
||||
echo " ... and $((TOTAL - LIMIT)) more (use --limit to adjust)"
|
||||
fi
|
||||
echo ""
|
||||
echo "Total: $TOTAL finding(s)"
|
||||
fi
|
||||
|
||||
if [[ $TOTAL -gt 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
@@ -0,0 +1,210 @@
|
||||
---
|
||||
name: go-functional-options
|
||||
description: Use when designing a Go constructor or factory function with optional configuration — especially with 3+ optional parameters or extensible APIs. Also use when building a New* function that takes many settings, even if they don't mention "functional options" by name. Does not cover general function design (see go-functions).
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
sources: "Uber Style Guide"
|
||||
---
|
||||
|
||||
# 函数式选项模式
|
||||
|
||||
函数式选项是一种模式,你声明一个不透明的 `Option` 类型,在内部结构体中记录信息。构造函数接受可变数量的这些选项并将其应用于配置结果。
|
||||
|
||||
## 何时使用
|
||||
|
||||
在以下情况使用函数式选项:
|
||||
|
||||
- 构造函数或公共 API 上有 **3 个以上可选参数**
|
||||
- **可扩展 API**,可能随时间增加新选项
|
||||
- **良好的调用者体验**很重要(无需传递默认值)
|
||||
|
||||
## 模式
|
||||
|
||||
### 核心组件
|
||||
|
||||
1. **未导出的 `options` 结构体** - 保存所有配置
|
||||
2. **导出的 `Option` 接口** - 带有未导出的 `apply` 方法
|
||||
3. **Option 类型** - 实现接口
|
||||
4. **`With*` 构造函数** - 创建选项
|
||||
|
||||
### Option 接口
|
||||
|
||||
```go
|
||||
type Option interface {
|
||||
apply(*options)
|
||||
}
|
||||
```
|
||||
|
||||
未导出的 `apply` 方法确保只能使用来自本包的选项。
|
||||
|
||||
## 完整实现
|
||||
|
||||
```go
|
||||
package db
|
||||
|
||||
import "go.uber.org/zap"
|
||||
|
||||
// options 保存打开连接的所有配置。
|
||||
type options struct {
|
||||
cache bool
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// Option 配置我们如何打开连接。
|
||||
type Option interface {
|
||||
apply(*options)
|
||||
}
|
||||
|
||||
// cacheOption 为缓存设置实现 Option(简单类型别名)。
|
||||
type cacheOption bool
|
||||
|
||||
func (c cacheOption) apply(opts *options) {
|
||||
opts.cache = bool(c)
|
||||
}
|
||||
|
||||
// WithCache 启用或禁用缓存。
|
||||
func WithCache(c bool) Option {
|
||||
return cacheOption(c)
|
||||
}
|
||||
|
||||
// loggerOption 为日志设置实现 Option(用于指针的结构体)。
|
||||
type loggerOption struct {
|
||||
Log *zap.Logger
|
||||
}
|
||||
|
||||
func (l loggerOption) apply(opts *options) {
|
||||
opts.logger = l.Log
|
||||
}
|
||||
|
||||
// WithLogger 设置连接的日志记录器。
|
||||
func WithLogger(log *zap.Logger) Option {
|
||||
return loggerOption{Log: log}
|
||||
}
|
||||
|
||||
// Open 创建一个连接。
|
||||
func Open(addr string, opts ...Option) (*Connection, error) {
|
||||
// 从默认值开始
|
||||
options := options{
|
||||
cache: defaultCache,
|
||||
logger: zap.NewNop(),
|
||||
}
|
||||
|
||||
// 应用所有提供的选项
|
||||
for _, o := range opts {
|
||||
o.apply(&options)
|
||||
}
|
||||
|
||||
// 使用 options.cache 和 options.logger...
|
||||
return &Connection{}, nil
|
||||
}
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 不使用函数式选项(不好)
|
||||
|
||||
```go
|
||||
// 调用者必须始终提供所有参数,即使是默认值
|
||||
db.Open(addr, db.DefaultCache, zap.NewNop())
|
||||
db.Open(addr, db.DefaultCache, log)
|
||||
db.Open(addr, false /* cache */, zap.NewNop())
|
||||
db.Open(addr, false /* cache */, log)
|
||||
```
|
||||
|
||||
### 使用函数式选项(好)
|
||||
|
||||
```go
|
||||
// 只在需要时提供选项
|
||||
db.Open(addr)
|
||||
db.Open(addr, db.WithLogger(log))
|
||||
db.Open(addr, db.WithCache(false))
|
||||
db.Open(
|
||||
addr,
|
||||
db.WithCache(false),
|
||||
db.WithLogger(log),
|
||||
)
|
||||
```
|
||||
|
||||
## 比较:函数式选项 vs 配置结构体
|
||||
|
||||
| 方面 | 函数式选项 | 配置结构体 |
|
||||
|------|-----------|-----------|
|
||||
| **可扩展性** | 添加新的 `With*` 函数 | 添加新字段(可能破坏兼容性) |
|
||||
| **默认值** | 内置于构造函数 | 零值或单独的默认值 |
|
||||
| **调用者体验** | 只指定不同的部分 | 必须构造整个结构体 |
|
||||
| **可测试性** | 选项可比较 | 结构体比较 |
|
||||
| **复杂性** | 更多样板代码 | 更简单的设置 |
|
||||
|
||||
**优先使用配置结构体的场景**:少于 3 个选项、选项很少变化、所有选项通常一起指定、或仅用于内部 API。
|
||||
|
||||
> 在决定使用函数式选项还是配置结构体、设计具有适当默认值的配置结构体 API、或评估复杂构造函数的混合方法时,请阅读 [references/OPTIONS-VS-STRUCTS.md](references/OPTIONS-VS-STRUCTS.md)。
|
||||
|
||||
## 为什么不使用闭包?
|
||||
|
||||
另一种实现使用闭包:
|
||||
|
||||
```go
|
||||
// 闭包方法(不推荐)
|
||||
type Option func(*options)
|
||||
|
||||
func WithCache(c bool) Option {
|
||||
return func(o *options) { o.cache = c }
|
||||
}
|
||||
```
|
||||
|
||||
优先使用接口方法,因为:
|
||||
|
||||
1. **可测试性** - 选项可以在测试和 mock 中进行比较
|
||||
2. **可调试性** - 选项可以实现 `fmt.Stringer`
|
||||
3. **灵活性** - 选项可以实现额外的接口
|
||||
4. **可见性** - 选项类型在文档中可见
|
||||
|
||||
## 快速参考
|
||||
|
||||
```go
|
||||
// 1. 带有默认值的未导出 options 结构体
|
||||
type options struct {
|
||||
field1 Type1
|
||||
field2 Type2
|
||||
}
|
||||
|
||||
// 2. 导出的 Option 接口,未导出的方法
|
||||
type Option interface {
|
||||
apply(*options)
|
||||
}
|
||||
|
||||
// 3. Option 类型 + apply + With* 构造函数
|
||||
type field1Option Type1
|
||||
|
||||
func (o field1Option) apply(opts *options) { opts.field1 = Type1(o) }
|
||||
func WithField1(v Type1) Option { return field1Option(v) }
|
||||
|
||||
// 4. 构造函数在默认值之上应用选项
|
||||
func New(required string, opts ...Option) (*Thing, error) {
|
||||
o := options{field1: defaultField1, field2: defaultField2}
|
||||
for _, opt := range opts {
|
||||
opt.apply(&o)
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 检查清单
|
||||
|
||||
- [ ] `options` 结构体未导出
|
||||
- [ ] `Option` 接口有未导出的 `apply` 方法
|
||||
- [ ] 每个选项有 `With*` 构造函数
|
||||
- [ ] 默认值在应用选项之前设置
|
||||
- [ ] 必需参数与 `...Option` 分开
|
||||
|
||||
## 相关技能
|
||||
|
||||
- **接口设计**:在设计 `Option` 接口或选择接口与闭包方法时,参见 [go-interfaces](../go-interfaces/SKILL.md)
|
||||
- **命名约定**:在命名 `With*` 构造函数、选项类型或未导出的 options 结构体时,参见 [go-naming](../go-naming/SKILL.md)
|
||||
- **函数设计**:在组织文件中的构造函数或格式化可变参数签名时,参见 [go-functions](../go-functions/SKILL.md)
|
||||
- **文档**:在记录 `Option` 类型、`With*` 函数或构造函数行为时,参见 [go-documentation](../go-documentation/SKILL.md)
|
||||
|
||||
### 外部资源
|
||||
|
||||
- [Self-referential functions and the design of options](https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html) - Rob Pike
|
||||
- [Functional options for friendly APIs](https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis) - Dave Cheney
|
||||
@@ -0,0 +1,129 @@
|
||||
# 函数式选项 vs 配置结构体
|
||||
|
||||
> **来源**:Google 风格指南, Uber 风格指南
|
||||
|
||||
函数式选项和配置结构体解决相同的问题 — 构造函数的可选配置 — 但它们有不同的权衡。根据 API 受众、可扩展性需求和复杂性预算来选择。
|
||||
|
||||
## 决策框架
|
||||
|
||||
```
|
||||
需要可选配置?
|
||||
├─ 内部或仅测试 API?
|
||||
│ └─ 配置结构体(更简单,更少样板代码)
|
||||
├─ 具有 3 个以上选项的公共 API?
|
||||
│ └─ 函数式选项(可扩展,向后兼容)
|
||||
├─ 选项需要校验或有相互依赖?
|
||||
│ └─ 函数式选项(在 apply 或构造函数中校验)
|
||||
├─ 所有选项通常一起指定?
|
||||
│ └─ 配置结构体(一个字面量,无需 With* 仪式)
|
||||
└─ 选项可能随时间增长?
|
||||
└─ 函数式选项(添加 With* 不会破坏调用者)
|
||||
```
|
||||
|
||||
## 配置结构体模式
|
||||
|
||||
配置结构体将可选参数分组为传递给构造函数的单个结构体。零值作为默认值,或提供 `DefaultConfig()`。
|
||||
|
||||
**好**
|
||||
```go
|
||||
type Config struct {
|
||||
Timeout time.Duration // 零 = 无超时
|
||||
MaxRetry int // 零 = 无重试
|
||||
Logger *log.Logger // nil = 丢弃
|
||||
}
|
||||
|
||||
func NewClient(addr string, cfg Config) *Client {
|
||||
if cfg.Logger == nil {
|
||||
cfg.Logger = log.New(io.Discard, "", 0)
|
||||
}
|
||||
return &Client{addr: addr, cfg: cfg}
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
c := NewClient("localhost:8080", Config{
|
||||
Timeout: 5 * time.Second,
|
||||
MaxRetry: 3,
|
||||
})
|
||||
```
|
||||
|
||||
**不好** — 在公共 API 中依赖未导出的配置字段:
|
||||
```go
|
||||
type config struct { // 未导出:调用者无法构造
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func NewClient(addr string, cfg config) *Client { ... }
|
||||
```
|
||||
|
||||
### 当零值不适用时
|
||||
|
||||
如果零是一个有效的非默认值(例如,超时为 0 表示"无超时",但期望的默认值是 30s),使用指针字段或哨兵值:
|
||||
|
||||
```go
|
||||
type Config struct {
|
||||
Timeout *time.Duration // nil = 使用默认值(30s),零 = 无超时
|
||||
}
|
||||
```
|
||||
|
||||
## 比较
|
||||
|
||||
| 方面 | 函数式选项 | 配置结构体 |
|
||||
|------|-----------|-----------|
|
||||
| **样板代码** | 高(每个选项需要类型 + apply + With*) | 低(一个结构体) |
|
||||
| **可扩展性** | 添加 `With*` — 无破坏性变更 | 添加字段 — 无破坏性变更 |
|
||||
| **向后兼容** | 对公共 API 极好 | 好(新字段获得零值) |
|
||||
| **默认值** | 内置于构造函数 | 零值或 `DefaultConfig()` |
|
||||
| **校验** | 在 `apply` 或构造函数循环中 | 在接收到结构体后的构造函数中 |
|
||||
| **可发现性** | `With*` 函数出现在 godoc 中 | 所有字段在一个结构体中可见 |
|
||||
| **可测试性** | 比较选项或测试构造函数输出 | 比较结构体字面量 |
|
||||
| **调用者体验** | 只指定与默认值不同的部分 | 必须构造结构体字面量 |
|
||||
| **零值歧义** | 无 — 未设置的选项不应用 | 可能需要指针字段 |
|
||||
|
||||
## 何时优先使用配置结构体
|
||||
|
||||
- **内部 API** — 更少的仪式,在调用处更易读
|
||||
- **少量选项(1-3 个)** — 函数式选项的开销不值得
|
||||
- **所有选项通常一起设置** — 可变参数风格没有好处
|
||||
- **不需要校验** — 简单的字段赋值即可
|
||||
- **选项是数据而非行为** — 结构体字段自然映射
|
||||
|
||||
```go
|
||||
srv := NewServer(Config{
|
||||
Port: 8080,
|
||||
TLSCert: "/path/to/cert.pem",
|
||||
TLSKey: "/path/to/key.pem",
|
||||
})
|
||||
```
|
||||
|
||||
## 何时优先使用函数式选项
|
||||
|
||||
- **公共/库 API** — 调用者不应跟踪内部配置的演变
|
||||
- **3 个以上选项**,每个都是可选的
|
||||
- **复杂默认值** — 默认值计算依赖于其他选项
|
||||
- **按选项校验** — 在 apply 时拒绝无效值
|
||||
- **选项可能增长** — 新的 `With*` 函数是纯粹增量的
|
||||
|
||||
```go
|
||||
srv := NewServer(
|
||||
WithPort(8080),
|
||||
WithTLS("/path/to/cert.pem", "/path/to/key.pem"),
|
||||
WithLogger(logger),
|
||||
)
|
||||
```
|
||||
|
||||
## 混合方法
|
||||
|
||||
对于同时需要便利性和可扩展性的 API,接受配置结构体用于常见设置,函数式选项用于高级覆盖:
|
||||
|
||||
```go
|
||||
func NewServer(cfg Config, opts ...Option) *Server {
|
||||
s := &Server{cfg: cfg}
|
||||
for _, o := range opts {
|
||||
o.apply(&s.cfg)
|
||||
}
|
||||
return s
|
||||
}
|
||||
```
|
||||
|
||||
谨慎使用 — 它增加了复杂性。每个 API 优先使用一种方法。
|
||||
@@ -0,0 +1,107 @@
|
||||
---
|
||||
name: go-functions
|
||||
description: Use when organizing functions within a Go file, formatting function signatures, designing return values, or following Printf-style naming conventions. Also use when a user is adding or refactoring any Go function, even if they don't mention function design or signature formatting. Does not cover functional options constructors (see go-functional-options).
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
sources: "Effective Go, Google Style Guide, Uber Style Guide"
|
||||
---
|
||||
|
||||
# Go 函数设计
|
||||
|
||||
> **本技能不适用的场景**:对于函数选项构造函数(`WithTimeout`、`WithLogger`),参见 [go-functional-options](../go-functional-options/SKILL.md)。对于错误返回约定,参见 [go-error-handling](../go-error-handling/SKILL.md)。对于函数和方法的命名,参见 [go-naming](../go-naming/SKILL.md)。
|
||||
|
||||
---
|
||||
|
||||
## 函数分组与排序
|
||||
|
||||
按以下规则组织文件中的函数:
|
||||
|
||||
1. 函数按**大致调用顺序**排序
|
||||
2. 函数**按接收者分组**
|
||||
3. **导出**函数排在最前面,位于 `struct`/`const`/`var` 定义之后
|
||||
4. `NewXxx`/`newXxx` 构造函数紧跟在类型定义之后
|
||||
5. 普通工具函数排在文件末尾
|
||||
|
||||
```go
|
||||
type something struct{ ... }
|
||||
|
||||
func newSomething() *something { return &something{} }
|
||||
|
||||
func (s *something) Cost() int { return calcCost(s.weights) }
|
||||
|
||||
func (s *something) Stop() { ... }
|
||||
|
||||
func calcCost(n []int) int { ... }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 函数签名
|
||||
|
||||
> 在格式化多行签名、包装返回值、缩短调用点或用自定义类型替换裸 bool 参数时,阅读 [references/SIGNATURES.md](references/SIGNATURES.md)。
|
||||
|
||||
尽量将签名保持在一行内。当必须换行时,将**所有参数放在各自的行上**并加尾随逗号:
|
||||
|
||||
```go
|
||||
func (r *SomeType) SomeLongFunctionName(
|
||||
foo1, foo2, foo3 string,
|
||||
foo4, foo5, foo6 int,
|
||||
) {
|
||||
foo7 := bar(foo1)
|
||||
}
|
||||
```
|
||||
|
||||
为含义不明确的参数添加 `/* name */` 注释,或者更好的做法是用自定义类型替换裸 `bool` 参数。
|
||||
|
||||
---
|
||||
|
||||
## 接口指针
|
||||
|
||||
几乎不需要指向接口的指针。将接口作为值传递——底层数据仍然可以是指针。
|
||||
|
||||
```go
|
||||
// 不好:接口指针
|
||||
func process(r *io.Reader) { ... }
|
||||
|
||||
// 好:传递接口值
|
||||
func process(r io.Reader) { ... }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Printf 与 Stringer
|
||||
|
||||
> 在使用 %v/%s/%d 之外的 Printf 动词、实现 fmt.Stringer 或 fmt.GoStringer、编写自定义 Format() 方法或调试 String() 方法中的无限递归时,阅读 [references/PRINTF-STRINGER.md](references/PRINTF-STRINGER.md)。
|
||||
|
||||
### Printf 风格函数名
|
||||
|
||||
接受格式字符串的函数应以 `f` 结尾,以便 `go vet` 支持。在 `Printf` 调用之外使用格式字符串时,将其声明为 `const`。
|
||||
|
||||
在格式化日志或错误消息中的字符串时,优先使用 `%q` 而非手动加引号的 `%s`——它能安全地转义特殊字符并加上引号:
|
||||
|
||||
```go
|
||||
return fmt.Errorf("unknown key %q", key) // 输出:unknown key "foo\nbar"
|
||||
```
|
||||
|
||||
设计具有 3 个以上可选参数的构造函数时,参见 **go-functional-options**。
|
||||
|
||||
---
|
||||
|
||||
## 快速参考
|
||||
|
||||
| 主题 | 规则 |
|
||||
|------|------|
|
||||
| 文件排序 | 类型 -> 构造函数 -> 导出 -> 未导出 -> 工具函数 |
|
||||
| 签名换行 | 所有参数各占一行,加尾随逗号 |
|
||||
| 裸参数 | 添加 `/* name */` 注释或使用自定义类型 |
|
||||
| 接口指针 | 几乎不需要;按值传递接口 |
|
||||
| Printf 函数名 | 以 `f` 结尾以支持 `go vet` |
|
||||
|
||||
---
|
||||
|
||||
## 相关技能
|
||||
|
||||
- **错误返回**:在设计错误返回模式或在多返回值函数中包装错误时,参见 [go-error-handling](../go-error-handling/SKILL.md)
|
||||
- **命名约定**:在为函数、方法命名或选择 getter/setter 模式时,参见 [go-naming](../go-naming/SKILL.md)
|
||||
- **函数选项**:在设计具有 3 个以上可选参数的构造函数时,参见 [go-functional-options](../go-functional-options/SKILL.md)
|
||||
- **格式化原则**:在决定行长度、裸返回或签名格式时,参见 [go-style-core](../go-style-core/SKILL.md)
|
||||
@@ -0,0 +1,264 @@
|
||||
# Printf、Stringer 与自定义格式化
|
||||
|
||||
Go 的 `fmt` 打印动词、`Stringer` 和 `GoStringer` 接口、自定义 `Format()` 方法以及常见陷阱的深度参考。
|
||||
|
||||
---
|
||||
|
||||
## Printf 动词
|
||||
|
||||
### 通用动词
|
||||
|
||||
| 动词 | 用途 |
|
||||
|------|------|
|
||||
| `%v` | 默认格式(结构体字段、切片元素) |
|
||||
| `%+v` | 带字段名的结构体:`{Name:alice Age:30}` |
|
||||
| `%#v` | Go 语法表示:`main.User{Name:"alice", Age:30}` |
|
||||
| `%T` | 值的类型:`main.User` |
|
||||
| `%%` | 字面百分号 |
|
||||
|
||||
### 字符串与字节动词
|
||||
|
||||
| 动词 | 用途 |
|
||||
|------|------|
|
||||
| `%s` | 纯字符串或字节切片 |
|
||||
| `%q` | 带 Go 语法转义的引号字符串:`"hello\n"` |
|
||||
| `%x` | 十六进制编码,小写:`68656c6c6f` |
|
||||
| `%X` | 十六进制编码,大写:`68656C6C6F` |
|
||||
|
||||
### 整数动词
|
||||
|
||||
| 动词 | 用途 |
|
||||
|------|------|
|
||||
| `%d` | 十进制整数 |
|
||||
| `%b` | 二进制 |
|
||||
| `%o` | 八进制 |
|
||||
| `%O` | 带 `0o` 前缀的八进制 |
|
||||
| `%x` | 十六进制,小写 |
|
||||
| `%X` | 十六进制,大写 |
|
||||
|
||||
### 浮点数动词
|
||||
|
||||
| 动词 | 用途 |
|
||||
|------|------|
|
||||
| `%f` | 小数点,无指数:`123.456` |
|
||||
| `%e` | 科学计数法:`1.23456e+02` |
|
||||
| `%g` | 紧凑格式:大指数用 `%e`,否则用 `%f` |
|
||||
|
||||
### 宽度与精度
|
||||
|
||||
```go
|
||||
fmt.Sprintf("%10d", 42) // " 42" (宽度 10,右对齐)
|
||||
fmt.Sprintf("%-10d", 42) // "42 " (宽度 10,左对齐)
|
||||
fmt.Sprintf("%.2f", 3.14159) // "3.14" (2 位小数)
|
||||
fmt.Sprintf("%010d", 42) // "0000000042" (零填充)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 使用 `%q` 输出字符串
|
||||
|
||||
`%q` 动词在双引号内打印字符串,使空字符串和控制字符可见:
|
||||
|
||||
```go
|
||||
fmt.Printf("value %q looks like English text", someText)
|
||||
|
||||
// 不好:手动添加引号
|
||||
fmt.Printf("value \"%s\" looks like English text", someText)
|
||||
```
|
||||
|
||||
在面向人类的输出中,如果值可能为空或包含控制字符,优先使用 `%q`。
|
||||
|
||||
---
|
||||
|
||||
## Printf 之外的格式字符串
|
||||
|
||||
在 `Printf` 风格调用之外声明格式字符串时,使用 `const`。这样 `go vet` 可以进行静态分析:
|
||||
|
||||
```go
|
||||
// 不好:变量格式字符串——go vet 无法检查
|
||||
msg := "unexpected values %v, %v\n"
|
||||
fmt.Printf(msg, 1, 2)
|
||||
|
||||
// 好:常量格式字符串——go vet 可以验证
|
||||
const msg = "unexpected values %v, %v\n"
|
||||
fmt.Printf(msg, 1, 2)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Printf 风格函数的命名
|
||||
|
||||
接受格式字符串的函数应以 `f` 结尾。这样 `go vet` 可以自动检查格式字符串:
|
||||
|
||||
```go
|
||||
func Wrapf(err error, format string, args ...any) error
|
||||
```
|
||||
|
||||
如果使用非标准名称,需要告知 `go vet`:
|
||||
|
||||
```bash
|
||||
go vet -printfuncs=wrapf,statusf
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `fmt.Stringer` 接口
|
||||
|
||||
实现 `fmt.Stringer` 来控制类型在 `%v` 和 `%s` 下的显示方式:
|
||||
|
||||
```go
|
||||
type fmt.Stringer interface {
|
||||
String() string
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
type Point struct{ X, Y int }
|
||||
|
||||
func (p Point) String() string {
|
||||
return fmt.Sprintf("(%d, %d)", p.X, p.Y)
|
||||
}
|
||||
|
||||
// fmt.Println(Point{1, 2}) → "(1, 2)"
|
||||
// fmt.Sprintf("point: %v", p) → "point: (1, 2)"
|
||||
// fmt.Sprintf("point: %s", p) → "point: (1, 2)"
|
||||
```
|
||||
|
||||
### 何时实现 Stringer
|
||||
|
||||
- 类型将出现在日志消息或面向用户的输出中
|
||||
- 默认的 `%v` 输出(仅字段值)不够有意义
|
||||
- 需要一种区别于序列化的、对人类友好的表示
|
||||
|
||||
---
|
||||
|
||||
## `fmt.GoStringer` 接口
|
||||
|
||||
实现 `fmt.GoStringer` 来控制 `%#v` 输出。这对于默认 Go 语法表示具有误导性或过于冗长的类型很有用:
|
||||
|
||||
```go
|
||||
type fmt.GoStringer interface {
|
||||
GoString() string
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
type Color struct{ R, G, B uint8 }
|
||||
|
||||
func (c Color) GoString() string {
|
||||
return fmt.Sprintf("Color(%#02x, %#02x, %#02x)", c.R, c.G, c.B)
|
||||
}
|
||||
|
||||
// fmt.Sprintf("%#v", Color{255, 128, 0})
|
||||
// → "Color(0xff, 0x80, 0x00)" 而非 "main.Color{R:0xff, G:0x80, B:0x00}"
|
||||
```
|
||||
|
||||
`GoString()` 的输出应该是有效的 Go 语法或接近有效语法——它用于调试,而非面向用户的显示。
|
||||
|
||||
---
|
||||
|
||||
## 使用 `fmt.Formatter` 自定义格式化
|
||||
|
||||
要完全控制所有格式动词,实现 `fmt.Formatter`:
|
||||
|
||||
```go
|
||||
type fmt.Formatter interface {
|
||||
Format(f fmt.State, verb rune)
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
type Point struct{ X, Y int }
|
||||
|
||||
func (p Point) Format(f fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 'v':
|
||||
if f.Flag('#') {
|
||||
// %#v——Go 语法表示
|
||||
fmt.Fprintf(f, "Point{X: %d, Y: %d}", p.X, p.Y)
|
||||
return
|
||||
}
|
||||
if f.Flag('+') {
|
||||
// %+v——带字段名的详细格式
|
||||
fmt.Fprintf(f, "X:%d Y:%d", p.X, p.Y)
|
||||
return
|
||||
}
|
||||
// %v——默认
|
||||
fmt.Fprintf(f, "(%d, %d)", p.X, p.Y)
|
||||
case 's':
|
||||
fmt.Fprintf(f, "(%d, %d)", p.X, p.Y)
|
||||
case 'q':
|
||||
fmt.Fprintf(f, "%q", p.String())
|
||||
default:
|
||||
fmt.Fprintf(f, "%%!%c(Point=%d,%d)", verb, p.X, p.Y)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `fmt.State` 方法
|
||||
|
||||
| 方法 | 返回值 |
|
||||
|------|--------|
|
||||
| `Flag(c int) bool` | 标志(`+`、`-`、`#`、`0`、` `)是否设置 |
|
||||
| `Width() (int, bool)` | 宽度值以及是否指定了宽度 |
|
||||
| `Precision() (int, bool)` | 精度值以及是否指定了精度 |
|
||||
| `Write(b []byte) (int, error)` | 写入输出字节 |
|
||||
|
||||
仅在 `String()` 不够用时才实现 `fmt.Formatter`——很少需要这样做。常见原因:需要为 `%v`、`%+v`、`%#v` 提供不同输出,或者需要遵循宽度/精度标志。
|
||||
|
||||
---
|
||||
|
||||
## 无限递归陷阱
|
||||
|
||||
**在 `String()` 方法内部对接收者使用 `%s` 或 `%v` 调用 `fmt.Sprintf` 会导致无限递归:**
|
||||
|
||||
```go
|
||||
type MyString string
|
||||
|
||||
// BUG:无限递归——Sprintf 调用 String(),String() 又调用 Sprintf...
|
||||
func (m MyString) String() string {
|
||||
return fmt.Sprintf("MyString: %s", m) // 崩溃:栈溢出
|
||||
}
|
||||
```
|
||||
|
||||
修复方法——将接收者转换为其底层类型以打破方法集:
|
||||
|
||||
```go
|
||||
func (m MyString) String() string {
|
||||
return fmt.Sprintf("MyString: %s", string(m)) // 安全:string 没有 String()
|
||||
}
|
||||
```
|
||||
|
||||
此陷阱还适用于:
|
||||
- 底层类型为 string、[]byte 或另一个 Stringer 的类型
|
||||
- 任何使用 `%s` 或 `%v` 格式化 `self` 的 `String()` 方法
|
||||
- 使用 `%#v` 格式化 `self` 的 `GoString()` 方法
|
||||
|
||||
```go
|
||||
type IPAddr [4]byte
|
||||
|
||||
// BUG:%v 调用 String(),无限递归
|
||||
func (ip IPAddr) String() string {
|
||||
return fmt.Sprintf("%v.%v.%v.%v", ip[0], ip[1], ip[2], ip[3])
|
||||
// 这里安全——ip[0] 是 byte(uint8),没有 String() 方法。
|
||||
// 但如果 ip 是一个包装了 Stringer 的命名类型,就会递归。
|
||||
}
|
||||
```
|
||||
|
||||
**经验法则**:在 `String()` 内部,永远不要将接收者(或重新转换为自身类型的接收者)传递给 `%s` 或 `%v` 动词。先转换为底层原始类型。
|
||||
|
||||
---
|
||||
|
||||
## 快速参考
|
||||
|
||||
| 主题 | 规则 |
|
||||
|------|------|
|
||||
| `%q` | 用于人类可读的字符串输出 |
|
||||
| `%+v` | 带字段名的结构体 |
|
||||
| `%#v` | Go 语法表示;通过 `GoStringer` 自定义 |
|
||||
| 格式字符串存储 | 在 Printf 调用之外声明为 `const` |
|
||||
| Printf 函数名 | 以 `f` 结尾以支持 `go vet` |
|
||||
| `Stringer` | 实现 `String() string` 用于 `%v`/`%s` 输出 |
|
||||
| `GoStringer` | 实现 `GoString() string` 用于 `%#v` 输出 |
|
||||
| `Formatter` | 实现 `Format(fmt.State, rune)` 以完全控制动词 |
|
||||
| 递归陷阱 | 永远不要在 `String()` 内部使用 `Sprintf("%s", receiver)`;转换为底层类型 |
|
||||
@@ -0,0 +1,168 @@
|
||||
# 函数签名
|
||||
|
||||
格式化 Go 函数签名、避免裸参数以及保持调用点可读性的详细规则。
|
||||
|
||||
---
|
||||
|
||||
## 单行 vs 多行
|
||||
|
||||
当签名能轻松放在一行时保持单行。当必须换行时,将**所有参数放在各自的行上**并加尾随逗号:
|
||||
|
||||
**不好**——部分换行使对齐变得脆弱:
|
||||
|
||||
```go
|
||||
func (r *SomeType) SomeLongFunctionName(foo1, foo2, foo3 string,
|
||||
foo4, foo5, foo6 int) {
|
||||
foo7 := bar(foo1)
|
||||
}
|
||||
```
|
||||
|
||||
**好**——完全换行,尾随逗号:
|
||||
|
||||
```go
|
||||
func (r *SomeType) SomeLongFunctionName(
|
||||
foo1, foo2, foo3 string,
|
||||
foo4, foo5, foo6 int,
|
||||
) {
|
||||
foo7 := bar(foo1)
|
||||
}
|
||||
```
|
||||
|
||||
### 返回值
|
||||
|
||||
当返回值也需要换行时,遵循相同的模式:
|
||||
|
||||
```go
|
||||
func (r *SomeType) LongName(
|
||||
foo1, foo2, foo3 string,
|
||||
foo4, foo5, foo6 int,
|
||||
) (
|
||||
*Result,
|
||||
error,
|
||||
) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
对于更简单的情况,命名返回值可以与参数右括号在同一行:
|
||||
|
||||
```go
|
||||
func (r *SomeType) LongName(
|
||||
foo1, foo2, foo3 string,
|
||||
) (result *Result, err error) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 缩短调用点
|
||||
|
||||
提取局部变量,而不是将函数调用拆分到多行:
|
||||
|
||||
```go
|
||||
// 不好:过长的内联调用
|
||||
result := foo.Call(
|
||||
somePackage.ComplexFunction(arg1, arg2),
|
||||
anotherPackage.Transform(data),
|
||||
defaultOptions,
|
||||
)
|
||||
|
||||
// 好:提取局部变量以提高清晰度
|
||||
transformed := anotherPackage.Transform(data)
|
||||
computed := somePackage.ComplexFunction(arg1, arg2)
|
||||
result := foo.Call(computed, transformed, defaultOptions)
|
||||
```
|
||||
|
||||
这提高了可读性,并使中间值可用于调试。
|
||||
|
||||
---
|
||||
|
||||
## 避免裸参数
|
||||
|
||||
函数调用中的裸参数会降低可读性。为含义不明确的参数添加 C 风格注释:
|
||||
|
||||
```go
|
||||
// 不好:这些布尔值是什么意思?
|
||||
printInfo("foo", true, true)
|
||||
|
||||
// 好:内联注释说明了意图
|
||||
printInfo("foo", true /* isLocal */, true /* done */)
|
||||
```
|
||||
|
||||
更好的做法是用自定义类型替换裸 `bool` 参数:
|
||||
|
||||
```go
|
||||
type Region int
|
||||
|
||||
const (
|
||||
UnknownRegion Region = iota
|
||||
Local
|
||||
)
|
||||
|
||||
type Status int
|
||||
|
||||
const (
|
||||
Pending Status = iota
|
||||
Done
|
||||
)
|
||||
|
||||
func printInfo(name string, region Region, status Status)
|
||||
```
|
||||
|
||||
### 何时使用每种方法
|
||||
|
||||
| 方法 | 时机 |
|
||||
|------|------|
|
||||
| C 风格注释 | 快速修复;调用点少;无法修改的第三方 API |
|
||||
| 自定义类型 | 多个调用点;公开 API;多个 bool/int 参数 |
|
||||
| 函数选项 | 3 个以上可选参数;参见 [go-functional-options](../../go-functional-options/SKILL.md) |
|
||||
|
||||
---
|
||||
|
||||
## 分组相关参数
|
||||
|
||||
当函数接受多个相同类型的参数时,将它们分组:
|
||||
|
||||
```go
|
||||
// 可接受:将同类型参数分组
|
||||
func Copy(dst, src string) error
|
||||
|
||||
// 可接受:尽管类型相同,但含义不同时分开声明
|
||||
func Move(source string, destination string) error
|
||||
```
|
||||
|
||||
当参数名称能清楚表明角色时使用分组;当不能清楚表明时使用分开声明。
|
||||
|
||||
---
|
||||
|
||||
## 方法接收者的位置
|
||||
|
||||
接收者放在函数名之前,格式类似于参数:
|
||||
|
||||
```go
|
||||
// 短接收者——放在同一行
|
||||
func (s *Server) Start(ctx context.Context) error { ... }
|
||||
|
||||
// 长接收者类型——如果整行过长则考虑换行
|
||||
func (h *ComplicatedHandler) ServeHTTP(
|
||||
w http.ResponseWriter,
|
||||
r *http.Request,
|
||||
) { ... }
|
||||
```
|
||||
|
||||
参见 [go-naming](../../go-naming/SKILL.md) 了解接收者命名约定(简短的一到两个字母缩写)。
|
||||
|
||||
---
|
||||
|
||||
## 快速参考
|
||||
|
||||
| 主题 | 规则 |
|
||||
|------|------|
|
||||
| 单行 | 能放下时保持一行 |
|
||||
| 多行 | 所有参数各占一行,尾随逗号 |
|
||||
| 返回值换行 | 与参数相同的模式 |
|
||||
| 调用点 | 提取局部变量而不是拆分调用 |
|
||||
| 裸 bool | 添加 `/* name */` 注释或使用自定义类型 |
|
||||
| 分组参数 | 当名称能清楚表明角色时将同类型分组 |
|
||||
| 接收者 | 在函数名之前;简短缩写 |
|
||||
@@ -0,0 +1,173 @@
|
||||
---
|
||||
name: go-generics
|
||||
description: Use when deciding whether to use Go generics, writing generic functions or types, choosing constraints, or picking between type aliases and type definitions. Also use when a user is writing a utility function that could work with multiple types, even if they don't mention generics explicitly. Does not cover interface design without generics (see go-interfaces).
|
||||
license: Apache-2.0
|
||||
compatibility: Requires Go 1.18+ (generics were introduced in Go 1.18)
|
||||
metadata:
|
||||
sources: "Google Style Guide"
|
||||
---
|
||||
|
||||
# Go 泛型与类型参数
|
||||
|
||||
---
|
||||
|
||||
## 何时使用泛型
|
||||
|
||||
从具体类型开始。只在出现第二种类型时才进行泛化。
|
||||
|
||||
### 优先使用泛型的场景
|
||||
|
||||
- 多种类型共享相同的逻辑(排序、过滤、map/reduce)
|
||||
- 否则需要依赖 `any` 和大量的类型切换
|
||||
- 正在构建可复用的数据结构(并发安全的集合、有序映射)
|
||||
|
||||
### 避免使用泛型的场景
|
||||
|
||||
- 实践中只有一种类型被实例化
|
||||
- 接口已经能清晰地表达共享行为
|
||||
- 泛型代码比特定类型的替代方案更难阅读
|
||||
|
||||
> "写代码,不要设计类型。"—— Robert Griesemer 和 Ian Lance Taylor
|
||||
|
||||
### 决策流程
|
||||
|
||||
```
|
||||
多种类型是否共享相同的逻辑?
|
||||
├─ 否 → 使用具体类型
|
||||
├─ 是 → 它们是否共享一个有用的接口?
|
||||
│ ├─ 是 → 使用接口
|
||||
│ └─ 否 → 使用泛型
|
||||
```
|
||||
|
||||
**不好:**
|
||||
|
||||
```go
|
||||
// 过早使用泛型:只会被 int 调用
|
||||
func Sum[T constraints.Integer | constraints.Float](vals []T) T {
|
||||
var total T
|
||||
for _, v := range vals {
|
||||
total += v
|
||||
}
|
||||
return total
|
||||
}
|
||||
```
|
||||
|
||||
**好:**
|
||||
|
||||
```go
|
||||
func SumInts(vals []int) int {
|
||||
var total int
|
||||
for _, v := range vals {
|
||||
total += v
|
||||
}
|
||||
return total
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 类型参数命名
|
||||
|
||||
| 名称 | 典型用途 |
|
||||
|------|----------|
|
||||
| `T` | 通用类型参数 |
|
||||
| `K` | 映射键类型 |
|
||||
| `V` | 映射值类型 |
|
||||
| `E` | 元素/项目类型 |
|
||||
|
||||
对于复杂约束,可以使用简短的描述性名称:
|
||||
|
||||
```go
|
||||
func Marshal[Opts encoding.MarshalOptions](v any, opts Opts) ([]byte, error)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 类型别名 vs 类型定义
|
||||
|
||||
类型别名(`type Old = new.Name`)很少使用——仅用于包迁移或渐进式 API 重构。
|
||||
|
||||
---
|
||||
|
||||
## 约束组合
|
||||
|
||||
使用 `~`(底层类型)和 `|`(联合)组合约束:
|
||||
|
||||
```go
|
||||
type Numeric interface {
|
||||
~int | ~int8 | ~int16 | ~int32 | ~int64 |
|
||||
~float32 | ~float64
|
||||
}
|
||||
|
||||
func Sum[T Numeric](vals []T) T {
|
||||
var total T
|
||||
for _, v := range vals {
|
||||
total += v
|
||||
}
|
||||
return total
|
||||
}
|
||||
```
|
||||
|
||||
使用 `constraints` 包或 `cmp` 包(Go 1.21+)中的标准约束如 `cmp.Ordered`,而不是自己编写。
|
||||
|
||||
> 在编写自定义类型约束、使用 ~ 和 | 组合约束或调试类型推断问题时,阅读 [references/CONSTRAINTS.md](references/CONSTRAINTS.md)。
|
||||
|
||||
---
|
||||
|
||||
## 常见陷阱
|
||||
|
||||
### 不要包装标准库类型
|
||||
|
||||
```go
|
||||
// 不好:泛型包装器增加了复杂度但没有价值
|
||||
type Set[T comparable] struct {
|
||||
m map[T]struct{}
|
||||
}
|
||||
|
||||
// 更好:当用法简单时直接使用 map[T]struct{}
|
||||
seen := map[string]struct{}{}
|
||||
```
|
||||
|
||||
泛型在消除**多个调用点**之间的重复时才能证明其复杂度的合理性。单次使用的泛型只是多余的间接层。
|
||||
|
||||
### 不要为接口满足而使用泛型
|
||||
|
||||
```go
|
||||
// 不好:T 仅用于满足接口——直接使用接口即可
|
||||
func Process[T io.Reader](r T) error { ... }
|
||||
|
||||
// 好:直接接受接口
|
||||
func Process(r io.Reader) error { ... }
|
||||
```
|
||||
|
||||
### 避免过度约束
|
||||
|
||||
```go
|
||||
// 不好:约束比需要的更严格
|
||||
func Contains[T interface{ ~int | ~string }](slice []T, target T) bool { ... }
|
||||
|
||||
// 好:comparable 就足够了
|
||||
func Contains[T comparable](slice []T, target T) bool { ... }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 快速参考
|
||||
|
||||
| 主题 | 指导 |
|
||||
|------|------|
|
||||
| 何时使用泛型 | 仅在多种类型共享相同逻辑且接口不够用时 |
|
||||
| 起点 | 先写具体代码;之后再泛化 |
|
||||
| 命名 | 单个大写字母(`T`、`K`、`V`、`E`) |
|
||||
| 类型别名 | 相同类型,替代名称;仅用于迁移 |
|
||||
| 约束组合 | 使用 `~` 表示底层类型,`|` 表示联合;优先使用 `cmp.Ordered` 而非自定义 |
|
||||
| 常见陷阱 | 不要对单次使用的代码或接口已足够时使用泛型 |
|
||||
|
||||
---
|
||||
|
||||
## 相关技能
|
||||
|
||||
- **接口 vs 泛型**:在决定接口是否已经能表达共享行为而无需泛型时,参见 [go-interfaces](../go-interfaces/SKILL.md)
|
||||
- **类型声明**:在定义新类型、类型别名或在类型定义和别名之间选择时,参见 [go-declarations](../go-declarations/SKILL.md)
|
||||
- **文档化泛型 API**:在为泛型函数编写文档注释和可运行示例时,参见 [go-documentation](../go-documentation/SKILL.md)
|
||||
- **命名类型参数**:在为类型参数或约束接口选择名称时,参见 [go-naming](../go-naming/SKILL.md)
|
||||
@@ -0,0 +1,169 @@
|
||||
# Go 泛型中的类型约束
|
||||
|
||||
> **来源**:Google Go 风格指南、Go 语言规范
|
||||
|
||||
约束定义了类型参数支持的操作。选择满足函数需求的最窄约束——不要更多。
|
||||
|
||||
---
|
||||
|
||||
## 内置约束
|
||||
|
||||
> **规范**:在自行编写约束之前,优先使用标准约束。
|
||||
|
||||
| 约束 | 含义 |
|
||||
|------|------|
|
||||
| `any` | `interface{}` 的别名;对类型没有要求 |
|
||||
| `comparable` | 支持 `==` 和 `!=`;映射键所必需 |
|
||||
| `cmp.Ordered` | 支持 `<`、`<=`、`>=`、`>`(Go 1.21+,替代 `constraints.Ordered`) |
|
||||
|
||||
在新代码中优先使用 `cmp.Ordered`(来自 `cmp` 包),而不是已弃用的 `golang.org/x/exp/constraints.Ordered`。
|
||||
|
||||
---
|
||||
|
||||
## `~` 运算符(底层类型)
|
||||
|
||||
> **建议**:当你想接受基于原始类型构建的命名类型时使用 `~`。
|
||||
|
||||
`~T` 语法匹配任何**底层类型**为 `T` 的类型。没有 `~` 时,只有精确的类型匹配。
|
||||
|
||||
```go
|
||||
type Celsius float64
|
||||
|
||||
type ExactFloat interface{ float64 } // 拒绝 Celsius
|
||||
type AnyFloat64 interface{ ~float64 } // 接受 Celsius
|
||||
```
|
||||
|
||||
当调用者可能基于基础类型定义命名类型时使用 `~`。仅在需要限制为精确的内置类型时才省略 `~`。
|
||||
|
||||
---
|
||||
|
||||
## 组合与编写约束
|
||||
|
||||
> **建议**:仅在没有标准约束适用时才定义自定义约束。
|
||||
|
||||
使用 `|` 组合类型并嵌入约束来组合它们:
|
||||
|
||||
```go
|
||||
type Numeric interface {
|
||||
~int | ~int8 | ~int16 | ~int32 | ~int64 |
|
||||
~float32 | ~float64
|
||||
}
|
||||
|
||||
type Addable interface {
|
||||
Numeric | ~string // 数字和字符串拼接
|
||||
}
|
||||
```
|
||||
|
||||
约束可以同时要求方法和类型元素:
|
||||
|
||||
```go
|
||||
type Stringer interface {
|
||||
comparable
|
||||
String() string
|
||||
}
|
||||
```
|
||||
|
||||
满足 `Stringer` 的类型必须是可比较的 **并且** 具有 `String()` 方法。
|
||||
|
||||
---
|
||||
|
||||
## 避免过度约束
|
||||
|
||||
> **规范**:使用支持所执行操作的最小约束。
|
||||
|
||||
**不好**
|
||||
```go
|
||||
// 只使用了 == 但限制为 int 和 string
|
||||
func Contains[T interface{ ~int | ~string }](s []T, v T) bool { ... }
|
||||
```
|
||||
|
||||
**好**
|
||||
```go
|
||||
// comparable 是 == 的最小约束
|
||||
func Contains[T comparable](s []T, v T) bool { ... }
|
||||
```
|
||||
|
||||
过度约束限制了复用,并迫使调用者绕过实现中根本不需要的限制。
|
||||
|
||||
## 类型推断
|
||||
|
||||
> **建议**:当类型明确时让编译器推断类型参数。
|
||||
|
||||
编译器从函数参数推断类型参数:
|
||||
|
||||
```go
|
||||
result := slices.Contains[string](names, "alice") // 显式——不必要
|
||||
result := slices.Contains(names, "alice") // 推断——推荐
|
||||
```
|
||||
|
||||
仅在以下情况下才显式提供类型参数:没有可用于推断的函数参数、推断的类型不正确(例如无类型常量提升为错误的类型),或者将类型显式展示出来有助于可读性。
|
||||
|
||||
---
|
||||
|
||||
## 常见陷阱
|
||||
|
||||
### 接口已足够时不要使用泛型
|
||||
|
||||
> **规范**:来自 Google 风格指南——当类型共享一个有用的统一接口时,优先使用接口。
|
||||
|
||||
**不好**
|
||||
```go
|
||||
// T 仅用于满足 io.Reader——直接使用接口即可
|
||||
func Process[T io.Reader](r T) error { ... }
|
||||
```
|
||||
|
||||
**好**
|
||||
```go
|
||||
func Process(r io.Reader) error { ... }
|
||||
```
|
||||
|
||||
如果约束是单个已有接口,直接接受该接口。
|
||||
|
||||
### 不要泛型地包装标准库类型
|
||||
|
||||
> **建议**:单次使用的泛型只是多余的间接层。
|
||||
|
||||
**不好**
|
||||
```go
|
||||
type Set[T comparable] struct{ m map[T]struct{} } // 永远只是 Set[string]
|
||||
```
|
||||
|
||||
**好**
|
||||
```go
|
||||
seen := map[string]struct{}{} // 对于单次实例化直接使用 map
|
||||
```
|
||||
|
||||
泛型在消除**多个调用点**之间的重复时才能证明其复杂度的合理性。如果只使用一种类型,从具体类型开始。
|
||||
|
||||
### 方法集与类型约束
|
||||
|
||||
你只能调用约束允许的操作:
|
||||
|
||||
**不好**
|
||||
```go
|
||||
func Stringify[T any](v T) string {
|
||||
return v.String() // 编译错误:any 没有 String()
|
||||
}
|
||||
```
|
||||
|
||||
**好**
|
||||
```go
|
||||
func Stringify[T fmt.Stringer](v T) string {
|
||||
return v.String()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 快速参考
|
||||
|
||||
| 主题 | 指导 |
|
||||
|------|------|
|
||||
| 默认约束 | `any`——不需要对 T 进行任何操作时使用 |
|
||||
| 相等性检查 | `comparable`——`==`、`!=` 和映射键所必需 |
|
||||
| 排序 | `cmp.Ordered`(Go 1.21+)用于 `<`、`>` 比较 |
|
||||
| 命名类型 | 使用 `~T` 接受底层类型为 T 的类型 |
|
||||
| 联合类型 | 使用 `\|` 组合——例如 `~int \| ~float64` |
|
||||
| 自定义约束 | 定义为包含类型元素和/或方法的接口 |
|
||||
| 类型推断 | 当编译器可以推断时省略类型参数 |
|
||||
| 最小约束 | 使用函数实际需要的最窄约束 |
|
||||
@@ -0,0 +1,151 @@
|
||||
---
|
||||
name: go-interfaces
|
||||
description: Use when defining or implementing Go interfaces, designing abstractions, creating mockable boundaries for testing, or composing types through embedding. Also use when deciding whether to accept an interface or return a concrete type, or using type assertions or type switches, even if the user doesn't explicitly mention interfaces. Does not cover generics-based polymorphism (see go-generics).
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
sources: "Effective Go, Google Style Guide, Uber Style Guide"
|
||||
allowed-tools: Bash(bash:*)
|
||||
---
|
||||
|
||||
# Go 接口与组合
|
||||
|
||||
## 可用脚本
|
||||
|
||||
- **`scripts/check-interface-compliance.sh`**——查找缺少编译时合规性检查(`var _ I = (*T)(nil)`)的导出接口。运行 `bash scripts/check-interface-compliance.sh --help` 查看选项。
|
||||
|
||||
---
|
||||
|
||||
## 接受接口,返回具体类型
|
||||
|
||||
接口属于**消费**值的包,而不是**实现**值的包。从构造函数返回具体类型(通常是指针或结构体),这样可以在不重构的情况下添加新方法。
|
||||
|
||||
```go
|
||||
// 好:消费者定义自己需要的接口
|
||||
package consumer
|
||||
|
||||
type Thinger interface { Thing() bool }
|
||||
|
||||
func Foo(t Thinger) string { ... }
|
||||
```
|
||||
|
||||
```go
|
||||
// 好:生产者返回具体类型
|
||||
package producer
|
||||
|
||||
type Thinger struct{ ... }
|
||||
func (t Thinger) Thing() bool { ... }
|
||||
func NewThinger() Thinger { return Thinger{ ... } }
|
||||
```
|
||||
|
||||
```go
|
||||
// 不好:生产者定义并返回自己的接口
|
||||
package producer
|
||||
|
||||
type Thinger interface { Thing() bool }
|
||||
type defaultThinger struct{ ... }
|
||||
func NewThinger() Thinger { return defaultThinger{ ... } }
|
||||
```
|
||||
|
||||
**不要在接口被使用之前定义它。** 如果没有现实的使用示例,很难判断接口是否真的有必要。
|
||||
|
||||
---
|
||||
|
||||
## 通用性:隐藏实现,暴露接口
|
||||
|
||||
如果一个类型仅用于实现某个接口,且没有该接口之外的导出方法,则从构造函数返回接口以隐藏实现:
|
||||
|
||||
```go
|
||||
func NewHash() hash.Hash32 {
|
||||
return &myHash{} // 未导出的类型
|
||||
}
|
||||
```
|
||||
|
||||
好处:实现可以在不影响调用者的情况下更改,替换算法只需更改构造函数调用。
|
||||
|
||||
---
|
||||
|
||||
## 类型断言:Comma-Ok 模式
|
||||
|
||||
不进行检查的话,失败的断言会导致运行时 panic。始终使用 comma-ok 模式进行安全测试:
|
||||
|
||||
```go
|
||||
str, ok := value.(string)
|
||||
if ok {
|
||||
fmt.Printf("string value is: %q\n", str)
|
||||
}
|
||||
```
|
||||
|
||||
检查值是否实现了某个接口:
|
||||
|
||||
```go
|
||||
if _, ok := val.(json.Marshaler); ok {
|
||||
fmt.Printf("value %v implements json.Marshaler\n", val)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 类型切换
|
||||
|
||||
重用变量名是惯用做法(`t := t.(type)`)——变量在每个 case 分支中拥有正确的类型。当 case 列出多个类型(`case int, int64:`)时,变量拥有接口类型。
|
||||
|
||||
---
|
||||
|
||||
## 嵌入
|
||||
|
||||
避免在公开结构体中嵌入类型——内部类型的完整方法集将成为你公开 API 的一部分。改用未导出的字段。
|
||||
|
||||
> 在使用结构体嵌入进行组合、重写嵌入方法、解决名称冲突、应用 HandlerFunc 适配器模式或决定是否在公开 API 类型中使用嵌入时,阅读 [references/EMBEDDING.md](references/EMBEDDING.md)。
|
||||
|
||||
---
|
||||
|
||||
## 接口满足检查
|
||||
|
||||
使用空标识符赋值在编译时验证类型是否实现了接口:
|
||||
|
||||
```go
|
||||
var _ json.Marshaler = (*RawMessage)(nil)
|
||||
```
|
||||
|
||||
如果 `*RawMessage` 没有实现 `json.Marshaler`,这会导致编译错误。
|
||||
|
||||
在以下情况下使用此模式:
|
||||
- 没有能自动验证接口的静态转换
|
||||
- 类型必须满足接口才能正确运行(例如自定义 JSON 序列化)
|
||||
- 接口更改应该导致编译失败,而不是静默降级
|
||||
|
||||
**不要**为每个接口都添加这些检查——仅在没有其他静态转换能捕获错误时才使用。
|
||||
|
||||
> **验证**:在定义接口或实现后,运行 `bash scripts/check-interface-compliance.sh` 验证所有具体类型都有编译时的 `var _ I = (*T)(nil)` 检查。
|
||||
|
||||
---
|
||||
|
||||
## 接收者类型
|
||||
|
||||
如果不确定,使用指针接收者。不要在单个类型上混合接收者类型——如果任何方法需要指针,则所有方法都使用指针。仅在小型不可变类型(`Point`、`time.Time`)或基本类型上使用值接收者。
|
||||
|
||||
> 在为新类型决定使用指针接收者还是值接收者时,特别是对于包含 sync 原语或大型结构体的类型,阅读 [references/RECEIVER-TYPE.md](references/RECEIVER-TYPE.md)。
|
||||
|
||||
---
|
||||
|
||||
## 快速参考
|
||||
|
||||
| 概念 | 模式 | 说明 |
|
||||
|------|------|------|
|
||||
| 消费者拥有接口 | 在使用处定义接口 | 不在实现包中 |
|
||||
| 安全类型断言 | `v, ok := x.(Type)` | 返回零值 + false |
|
||||
| 类型切换 | `switch v := x.(type)` | 变量在每个 case 中拥有正确类型 |
|
||||
| 接口嵌入 | `type RW interface { Reader; Writer }` | 方法的并集 |
|
||||
| 结构体嵌入 | `type S struct { *T }` | 提升 T 的方法 |
|
||||
| 接口检查 | `var _ I = (*T)(nil)` | 编译时验证 |
|
||||
| 通用性 | 从构造函数返回接口 | 隐藏实现 |
|
||||
|
||||
---
|
||||
|
||||
## 相关技能
|
||||
|
||||
- **接口命名**:在为接口命名(`-er` 后缀约定)或选择接收者名称时,参见 [go-naming](../go-naming/SKILL.md)
|
||||
- **错误类型**:在实现 `error` 接口、自定义错误类型或 `errors.As` 匹配时,参见 [go-error-handling](../go-error-handling/SKILL.md)
|
||||
- **泛型 vs 接口**:在决定是否需要泛型或接口是否已足够时,参见 [go-generics](../go-generics/SKILL.md)
|
||||
- **函数选项**:在使用基于接口的 Option 模式实现灵活构造函数时,参见 [go-functional-options](../go-functional-options/SKILL.md)
|
||||
- **编译时检查**:在 API 边界添加 `var _ I = (*T)(nil)` 满足检查时,参见 [go-defensive](../go-defensive/SKILL.md)
|
||||
@@ -0,0 +1,138 @@
|
||||
# Go 中的嵌入模式
|
||||
|
||||
> **来源**:Effective Go、Uber 风格指南
|
||||
|
||||
Go 使用嵌入来实现组合而非继承。嵌入将内部类型的方法提升到外部类型,自动满足接口。
|
||||
|
||||
## 接口嵌入
|
||||
|
||||
通过嵌入来组合接口:
|
||||
|
||||
```go
|
||||
type ReadWriter interface {
|
||||
Reader
|
||||
Writer
|
||||
}
|
||||
```
|
||||
|
||||
`ReadWriter` 既能做 `Reader` 能做的事,*也能*做 `Writer` 能做的事。接口中只能嵌入接口。
|
||||
|
||||
## 结构体嵌入
|
||||
|
||||
嵌入将内部类型的方法提升到外部类型,无需显式转发。
|
||||
|
||||
```go
|
||||
type ReadWriter struct {
|
||||
*Reader // *bufio.Reader
|
||||
*Writer // *bufio.Writer
|
||||
}
|
||||
```
|
||||
|
||||
通过嵌入,`bufio.ReadWriter` 自动满足 `io.Reader`、`io.Writer` 和 `io.ReadWriter`。
|
||||
|
||||
混合使用嵌入字段和命名字段:
|
||||
|
||||
```go
|
||||
type Job struct {
|
||||
Command string
|
||||
*log.Logger
|
||||
}
|
||||
|
||||
job.Println("starting now...")
|
||||
job.Logger.SetPrefix("Job: ")
|
||||
```
|
||||
|
||||
## 方法重写
|
||||
|
||||
在外部类型上定义方法以重写提升的方法:
|
||||
|
||||
```go
|
||||
func (job *Job) Printf(format string, args ...any) {
|
||||
job.Logger.Printf("%q: %s", job.Command, fmt.Sprintf(format, args...))
|
||||
}
|
||||
```
|
||||
|
||||
外部方法优先——对 `job.Printf(...)` 的调用会调用外部方法,而嵌入方法仍可通过 `job.Logger.Printf(...)` 访问。
|
||||
|
||||
## 嵌入 vs 子类化
|
||||
|
||||
当调用嵌入方法时,接收者是**内部**类型,而非外部类型。嵌入类型不知道自己被嵌入——不存在类似于 `this` 或 `super` 的引用指向包含它的类型。
|
||||
|
||||
```go
|
||||
type Base struct{}
|
||||
func (b *Base) Name() string { return "Base" }
|
||||
|
||||
type Derived struct{ Base }
|
||||
|
||||
d := Derived{}
|
||||
d.Name() // 返回 "Base",而非 "Derived"
|
||||
```
|
||||
|
||||
## 名称冲突解决
|
||||
|
||||
1. **外部隐藏内部**——外部类型上的字段或方法会遮蔽嵌入类型在同名位置提升的字段或方法
|
||||
2. **同级冲突是错误**——如果两个同深度的嵌入类型提升了相同的名称,则为编译错误(除非该名称从未被访问)
|
||||
|
||||
```go
|
||||
type A struct{}
|
||||
func (A) Hello() string { return "A" }
|
||||
|
||||
type B struct{}
|
||||
func (B) Hello() string { return "B" }
|
||||
|
||||
type C struct {
|
||||
A
|
||||
B
|
||||
}
|
||||
|
||||
// c.Hello() // 编译错误:选择器不明确
|
||||
c.A.Hello() // 可以:显式消歧
|
||||
```
|
||||
|
||||
## 不要在公开结构体中嵌入
|
||||
|
||||
嵌入将内部类型的完整方法集暴露为你的公开 API 的一部分。这带来了维护负担:嵌入类型方法的更改会破坏 API 的兼容性保证。
|
||||
|
||||
**不好**
|
||||
```go
|
||||
type SMap struct {
|
||||
sync.Mutex // Lock 和 Unlock 现在是 SMap API 的一部分
|
||||
data map[string]string
|
||||
}
|
||||
```
|
||||
|
||||
**好**
|
||||
```go
|
||||
type SMap struct {
|
||||
mu sync.Mutex // 未导出的字段——实现细节
|
||||
data map[string]string
|
||||
}
|
||||
|
||||
func (m *SMap) Get(k string) string {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return m.data[k]
|
||||
}
|
||||
```
|
||||
|
||||
例外:在测试类型和 API 稳定性无关紧要的内部结构体中,嵌入是可以接受的。
|
||||
|
||||
## HandlerFunc 适配器模式
|
||||
|
||||
方法可以在任何命名类型上定义,不仅仅是结构体。`http.HandlerFunc` 模式将普通函数转换为接口实现:
|
||||
|
||||
```go
|
||||
type HandlerFunc func(ResponseWriter, *Request)
|
||||
|
||||
func (f HandlerFunc) ServeHTTP(w ResponseWriter, req *Request) {
|
||||
f(w, req)
|
||||
}
|
||||
```
|
||||
|
||||
任何具有正确签名的函数都可以成为 HTTP 处理器:
|
||||
|
||||
```go
|
||||
http.Handle("/args", http.HandlerFunc(ArgServer))
|
||||
```
|
||||
|
||||
这种适配器模式在需要让独立函数满足单方法接口时非常有用。
|
||||
@@ -0,0 +1,68 @@
|
||||
# 接收者类型:指针 vs 值
|
||||
|
||||
> **建议**:Go Wiki CodeReviewComments
|
||||
|
||||
选择在方法上使用值接收者还是指针接收者可能很困难。**如果不确定,使用指针**,但有时值接收者也是合理的。
|
||||
|
||||
## 何时使用指针接收者
|
||||
|
||||
- **方法修改接收者**:接收者必须是指针
|
||||
- **接收者包含 sync.Mutex 或类似类型**:必须使用指针以避免复制
|
||||
- **大型结构体或数组**:指针接收者更高效。如果将所有元素作为参数传递感觉太大,那对值接收者来说也太大了
|
||||
- **并发或被调方法可能修改**:如果更改必须对原始接收者可见,则必须使用指针
|
||||
- **元素是指向可变内容的指针**:优先使用指针接收者使意图更清晰
|
||||
|
||||
## 何时使用值接收者
|
||||
|
||||
- **小型不变的结构体或基本类型**:值接收者以提高效率
|
||||
- **Map、func 或 chan**:不要对它们使用指针
|
||||
- **不重新切片/重新分配的切片**:如果方法不重新切片或重新分配切片,不要使用指针
|
||||
- **没有可变字段的小型值类型**:像 `time.Time` 这样没有可变字段且没有指针的类型适合作为值接收者
|
||||
- **简单基本类型**:`int`、`string` 等
|
||||
|
||||
```go
|
||||
// 值接收者:小型、不可变类型
|
||||
type Point struct {
|
||||
X, Y float64
|
||||
}
|
||||
|
||||
func (p Point) Distance(q Point) float64 {
|
||||
return math.Hypot(q.X-p.X, q.Y-p.Y)
|
||||
}
|
||||
|
||||
// 指针接收者:方法修改接收者
|
||||
func (p *Point) ScaleBy(factor float64) {
|
||||
p.X *= factor
|
||||
p.Y *= factor
|
||||
}
|
||||
|
||||
// 指针接收者:包含 sync.Mutex
|
||||
type Counter struct {
|
||||
mu sync.Mutex
|
||||
count int
|
||||
}
|
||||
|
||||
func (c *Counter) Increment() {
|
||||
c.mu.Lock()
|
||||
c.count++
|
||||
c.mu.Unlock()
|
||||
}
|
||||
```
|
||||
|
||||
## 一致性规则
|
||||
|
||||
**不要混合接收者类型**。为类型上所有可用的方法统一选择指针或结构体类型。如果任何方法需要指针接收者,则所有方法都使用指针接收者。
|
||||
|
||||
```go
|
||||
// 好:一致的指针接收者
|
||||
type Buffer struct {
|
||||
data []byte
|
||||
}
|
||||
|
||||
func (b *Buffer) Write(p []byte) (int, error) { /* ... */ }
|
||||
func (b *Buffer) Read(p []byte) (int, error) { /* ... */ }
|
||||
func (b *Buffer) Len() int { return len(b.data) }
|
||||
|
||||
// 不好:混合接收者类型
|
||||
func (b Buffer) Len() int { return len(b.data) } // 不一致
|
||||
```
|
||||
@@ -0,0 +1,224 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="1.0.0"
|
||||
SCRIPT_NAME="$(basename "$0")"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
$SCRIPT_NAME v$VERSION — Check for missing compile-time interface compliance verifications
|
||||
|
||||
USAGE
|
||||
bash $SCRIPT_NAME [options] [path]
|
||||
|
||||
DESCRIPTION
|
||||
Scans Go files for exported interface definitions and checks whether each
|
||||
has a corresponding compile-time compliance assertion like:
|
||||
|
||||
var _ MyInterface = (*MyImpl)(nil)
|
||||
var _ MyInterface = MyImpl{}
|
||||
|
||||
Reports interfaces that lack such compile-time checks. This helps catch
|
||||
interface drift at compile time instead of runtime.
|
||||
|
||||
Exits 0 if all interfaces are verified, 1 if missing checks found, 2 on error.
|
||||
|
||||
OPTIONS
|
||||
-h, --help Show this help message
|
||||
-v, --version Show version
|
||||
--json Output results as JSON
|
||||
--include-test Also scan _test.go files for compliance checks
|
||||
--limit N Show at most N results (default: all)
|
||||
|
||||
ARGUMENTS
|
||||
path Directory to scan (default: current directory)
|
||||
|
||||
EXAMPLES
|
||||
bash $SCRIPT_NAME
|
||||
bash $SCRIPT_NAME ./pkg/storage
|
||||
bash $SCRIPT_NAME --json .
|
||||
bash $SCRIPT_NAME --include-test ./internal
|
||||
EOF
|
||||
}
|
||||
|
||||
JSON_OUTPUT=false
|
||||
INCLUDE_TEST=false
|
||||
LIMIT=0
|
||||
TARGET=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help) usage; exit 0 ;;
|
||||
-v|--version) echo "$SCRIPT_NAME v$VERSION"; exit 0 ;;
|
||||
--json) JSON_OUTPUT=true; shift ;;
|
||||
--include-test) INCLUDE_TEST=true; shift ;;
|
||||
--limit) LIMIT="${2:?error: --limit requires a number}"; shift 2 ;;
|
||||
-*) echo "error: unknown option: $1" >&2; usage >&2; exit 2 ;;
|
||||
*) TARGET="$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
TARGET="${TARGET:-.}"
|
||||
|
||||
if [[ ! -d "$TARGET" && ! -f "$TARGET" ]]; then
|
||||
# Handle ./... patterns
|
||||
dir="${TARGET%%/...}"
|
||||
dir="${dir:-.}"
|
||||
if [[ ! -d "$dir" ]]; then
|
||||
echo "error: path not found: $TARGET" >&2
|
||||
exit 2
|
||||
fi
|
||||
TARGET="$dir"
|
||||
fi
|
||||
|
||||
json_escape() {
|
||||
local s="$1"
|
||||
s="${s//\\/\\\\}"
|
||||
s="${s//\"/\\\"}"
|
||||
s="${s//$'\t'/\\t}"
|
||||
s="${s//$'\r'/}"
|
||||
s="${s//$'\n'/\\n}"
|
||||
printf '%s' "$s"
|
||||
}
|
||||
|
||||
# Collect all Go source files
|
||||
find_go_files() {
|
||||
local t="$1"
|
||||
if $INCLUDE_TEST; then
|
||||
find "$t" -name '*.go' ! -path '*/vendor/*' ! -path '*/.git/*' 2>/dev/null
|
||||
else
|
||||
find "$t" -name '*.go' ! -name '*_test.go' ! -path '*/vendor/*' ! -path '*/.git/*' 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
# Collect all Go files (including tests) for checking compliance vars
|
||||
find_all_go_files() {
|
||||
find "$1" -name '*.go' ! -path '*/vendor/*' ! -path '*/.git/*' 2>/dev/null
|
||||
}
|
||||
|
||||
# Step 1: Find all exported interface definitions
|
||||
IFACE_NAMES=()
|
||||
IFACE_LOCATIONS=()
|
||||
|
||||
while IFS= read -r file; do
|
||||
[[ -n "$file" ]] || continue
|
||||
line_num=0
|
||||
while IFS= read -r line; do
|
||||
line_num=$((line_num + 1))
|
||||
# Match: type ExportedName interface {
|
||||
pat='^[[:space:]]*type[[:space:]]+([A-Z][a-zA-Z0-9]*)[[:space:]]+interface[[:space:]]*\{'
|
||||
if [[ "$line" =~ $pat ]]; then
|
||||
iface_name="${BASH_REMATCH[1]}"
|
||||
IFACE_NAMES+=("$iface_name")
|
||||
IFACE_LOCATIONS+=("$file:$line_num")
|
||||
fi
|
||||
done < "$file"
|
||||
done < <(find_go_files "$TARGET")
|
||||
|
||||
if [[ ${#IFACE_NAMES[@]} -eq 0 ]]; then
|
||||
if $JSON_OUTPUT; then
|
||||
echo '{"interfaces":[],"missing":[],"count_interfaces":0,"count_missing":0}'
|
||||
else
|
||||
echo "No exported interfaces found in: $TARGET"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Step 2: Scan all Go files (including tests) for compliance checks
|
||||
# Pattern: var _ InterfaceName = ...
|
||||
ALL_GO_FILES=()
|
||||
while IFS= read -r f; do
|
||||
[[ -n "$f" ]] && ALL_GO_FILES+=("$f")
|
||||
done < <(find_all_go_files "$TARGET")
|
||||
|
||||
MISSING=()
|
||||
|
||||
for ((i=0; i<${#IFACE_NAMES[@]}; i++)); do
|
||||
iface_name="${IFACE_NAMES[$i]}"
|
||||
location="${IFACE_LOCATIONS[$i]}"
|
||||
# Look for: var _ InterfaceName = (various patterns)
|
||||
if ! grep -qlE "var[[:space:]]+_[[:space:]]+${iface_name}[[:space:]]*=" \
|
||||
"${ALL_GO_FILES[@]}" 2>/dev/null; then
|
||||
MISSING+=("${iface_name}|${location}")
|
||||
fi
|
||||
done
|
||||
|
||||
# Sort for stable output
|
||||
IFS=$'\n' MISSING=($(sort <<<"${MISSING[*]}")); unset IFS
|
||||
|
||||
# Truncation
|
||||
TOTAL=${#MISSING[@]}
|
||||
TRUNCATED=false
|
||||
if [[ $LIMIT -gt 0 && $TOTAL -gt $LIMIT ]]; then
|
||||
MISSING=("${MISSING[@]:0:$LIMIT}")
|
||||
TRUNCATED=true
|
||||
fi
|
||||
|
||||
# Output results
|
||||
if $JSON_OUTPUT; then
|
||||
echo "{"
|
||||
echo ' "interfaces": ['
|
||||
first=true
|
||||
SORTED_INDICES=()
|
||||
for ((i=0; i<${#IFACE_NAMES[@]}; i++)); do
|
||||
SORTED_INDICES+=("$i|${IFACE_NAMES[$i]}")
|
||||
done
|
||||
IFS=$'\n' SORTED_INDICES=($(sort -t'|' -k2 <<<"${SORTED_INDICES[*]}")); unset IFS
|
||||
|
||||
for entry in "${SORTED_INDICES[@]}"; do
|
||||
i="${entry%%|*}"
|
||||
iface_name="${IFACE_NAMES[$i]}"
|
||||
location="${IFACE_LOCATIONS[$i]}"
|
||||
file="${location%%:*}"
|
||||
line="${location#*:}"
|
||||
$first || echo ","
|
||||
first=false
|
||||
printf ' {"name":"%s","file":"%s","line":%s}' "$(json_escape "$iface_name")" "$(json_escape "$file")" "$line"
|
||||
done
|
||||
echo ""
|
||||
echo " ],"
|
||||
echo ' "missing": ['
|
||||
first=true
|
||||
for entry in "${MISSING[@]+"${MISSING[@]}"}"; do
|
||||
IFS='|' read -r name location <<< "$entry"
|
||||
file="${location%%:*}"
|
||||
line="${location#*:}"
|
||||
$first || echo ","
|
||||
first=false
|
||||
printf ' {"name":"%s","file":"%s","line":%s}' "$(json_escape "$name")" "$(json_escape "$file")" "$line"
|
||||
done
|
||||
echo ""
|
||||
echo " ],"
|
||||
printf ' "count_interfaces": %d,\n' "${#IFACE_NAMES[@]}"
|
||||
printf ' "count_missing": %d,\n' "$TOTAL"
|
||||
printf ' "truncated": %s\n' "$TRUNCATED"
|
||||
echo "}"
|
||||
else
|
||||
echo "Exported interfaces found: ${#IFACE_NAMES[@]}"
|
||||
echo ""
|
||||
|
||||
if [[ $TOTAL -eq 0 ]]; then
|
||||
echo "All interfaces have compile-time compliance checks."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Missing compile-time compliance checks:"
|
||||
echo ""
|
||||
for entry in "${MISSING[@]}"; do
|
||||
IFS='|' read -r name location <<< "$entry"
|
||||
printf " %s interface '%s' has no 'var _ %s = ...' assertion\n" "$location" "$name" "$name"
|
||||
done
|
||||
if $TRUNCATED; then
|
||||
echo " ... and $((TOTAL - LIMIT)) more (use --limit to adjust)"
|
||||
fi
|
||||
echo ""
|
||||
echo "Add compile-time checks like:"
|
||||
echo " var _ MyInterface = (*MyImpl)(nil)"
|
||||
echo ""
|
||||
echo "Total: $TOTAL interface(s) missing verification"
|
||||
fi
|
||||
|
||||
if [[ $TOTAL -gt 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
@@ -0,0 +1,209 @@
|
||||
---
|
||||
name: go-linting
|
||||
description: Use when setting up linting for a Go project, configuring golangci-lint, or adding Go checks to a CI/CD pipeline. Also use when starting a new Go project and deciding which linters to enable, even if the user only asks about "code quality" or "static analysis" without mentioning specific linter names. Does not cover code review process (see go-code-review).
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
sources: "Uber Style Guide"
|
||||
allowed-tools: Bash(bash:*)
|
||||
---
|
||||
|
||||
# Go Lint
|
||||
|
||||
## 核心原则
|
||||
|
||||
比任何"推荐"的 linter 集合更重要的是:**在整个代码库中一致地进行 lint**。
|
||||
|
||||
一致的 lint 有助于捕获常见问题,并在不过度限制的情况下建立高标准的代码质量。
|
||||
|
||||
---
|
||||
|
||||
## 设置步骤
|
||||
|
||||
1. 使用下面的配置创建 `.golangci.yml`
|
||||
2. 运行 `golangci-lint run ./...`
|
||||
3. 如果出现错误,按类别逐一修复(先格式化,再 vet,再风格)
|
||||
4. 重新运行直到通过
|
||||
|
||||
---
|
||||
|
||||
## 最低推荐 Linter
|
||||
|
||||
这些 linter 能捕获最常见的问题,同时保持高质量标准:
|
||||
|
||||
| Linter | 用途 |
|
||||
|--------|------|
|
||||
| [errcheck](https://github.com/kisielk/errcheck) | 确保错误被处理 |
|
||||
| [goimports](https://pkg.go.dev/golang.org/x/tools/cmd/goimports) | 格式化代码和管理导入 |
|
||||
| [revive](https://github.com/mgechev/revive) | 常见风格错误(golint 的现代替代品) |
|
||||
| [govet](https://pkg.go.dev/cmd/vet) | 分析代码中的常见错误 |
|
||||
| [staticcheck](https://staticcheck.dev) | 各种静态分析检查 |
|
||||
|
||||
> **注意**:`revive` 是现已弃用的 `golint` 的现代、更快的替代品。
|
||||
|
||||
---
|
||||
|
||||
## Lint 运行器:golangci-lint
|
||||
|
||||
使用 [golangci-lint](https://github.com/golangci/golangci-lint) 作为你的 lint 运行器。参见 uber-go/guide 的 [示例 .golangci.yml](https://github.com/uber-go/guide/blob/master/.golangci.yml)。
|
||||
|
||||
---
|
||||
|
||||
## 示例配置
|
||||
|
||||
> 在创建新的 `.golangci.yml` 或将现有配置与推荐基线进行比较时,参见 `assets/golangci.yml`。
|
||||
|
||||
在项目根目录创建 `.golangci.yml`:
|
||||
|
||||
```yaml
|
||||
linters:
|
||||
enable:
|
||||
- errcheck
|
||||
- goimports
|
||||
- revive
|
||||
- govet
|
||||
- staticcheck
|
||||
|
||||
linters-settings:
|
||||
goimports:
|
||||
local-prefixes: github.com/your-org/your-repo
|
||||
revive:
|
||||
rules:
|
||||
- name: blank-imports
|
||||
- name: context-as-argument
|
||||
- name: error-return
|
||||
- name: error-strings
|
||||
- name: exported
|
||||
|
||||
run:
|
||||
timeout: 5m
|
||||
```
|
||||
|
||||
### 运行
|
||||
|
||||
```bash
|
||||
# 安装
|
||||
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||
|
||||
# 运行所有 linter
|
||||
golangci-lint run
|
||||
|
||||
# 对特定路径运行
|
||||
golangci-lint run ./pkg/...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 额外推荐的 Linter
|
||||
|
||||
除了最低集合之外,在生产项目中可以考虑以下 linter:
|
||||
|
||||
| Linter | 用途 | 何时启用 |
|
||||
|--------|------|----------|
|
||||
| [gosec](https://github.com/securego/gosec) | 安全漏洞检测 | 处理用户输入的服务始终启用 |
|
||||
| [ineffassign](https://github.com/gordonklaus/ineffassign) | 检测无效赋值 | 始终——捕获死代码 |
|
||||
| [misspell](https://github.com/client9/misspell) | 纠正注释/字符串中的常见拼写错误 | 始终 |
|
||||
| [gocyclo](https://github.com/fzipp/gocyclo) | 圈复杂度阈值 | 当函数超过约 15 的复杂度时 |
|
||||
| [exhaustive](https://github.com/nishanths/exhaustive) | 确保 switch 覆盖所有枚举值 | 使用 iota 枚举时 |
|
||||
| [bodyclose](https://github.com/timakin/bodyclose) | 检测未关闭的 HTTP 响应体 | HTTP 客户端代码始终启用 |
|
||||
|
||||
---
|
||||
|
||||
## Nolint 指令
|
||||
|
||||
在抑制 lint 发现时,始终说明原因:
|
||||
|
||||
```go
|
||||
//nolint:errcheck // 即发即忘的日志;错误不可操作
|
||||
_ = logger.Sync()
|
||||
```
|
||||
|
||||
规则:
|
||||
- 使用 `//nolint:lintername`——永远不要使用裸 `//nolint`
|
||||
- 将注释放在与发现相同的行
|
||||
- 在 `//` 之后包含理由说明
|
||||
|
||||
---
|
||||
|
||||
## CI/CD 集成
|
||||
|
||||
### GitHub Actions
|
||||
|
||||
```yaml
|
||||
# .github/workflows/lint.yml
|
||||
name: Lint
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: stable
|
||||
- uses: golangci/golangci-lint-action@v6
|
||||
with:
|
||||
version: latest
|
||||
```
|
||||
|
||||
### Pre-commit Hook
|
||||
|
||||
```bash
|
||||
#!/bin/sh
|
||||
# .git/hooks/pre-commit
|
||||
golangci-lint run --new-from-rev=HEAD~1
|
||||
```
|
||||
|
||||
使用 `--new-from-rev` 只对更改的代码进行 lint,保持快速反馈循环。
|
||||
|
||||
---
|
||||
|
||||
## 可用脚本
|
||||
|
||||
- **`scripts/setup-lint.sh`**——生成 `.golangci.yml` 并运行初始 lint
|
||||
|
||||
```bash
|
||||
bash scripts/setup-lint.sh github.com/your-org/your-repo
|
||||
bash scripts/setup-lint.sh --force github.com/your-org/your-repo # 覆盖现有配置
|
||||
bash scripts/setup-lint.sh --dry-run # 预览配置
|
||||
bash scripts/setup-lint.sh --json # 结构化输出
|
||||
```
|
||||
|
||||
> **验证**:在生成 `.golangci.yml` 后,运行 `golangci-lint run ./...` 验证配置有效并产生预期输出。如果因配置错误而失败,修复后重试。
|
||||
|
||||
> `scripts/setup-lint.sh` 生成**最低**配置(5 个核心 linter)。
|
||||
> 对于已有项目,使用 `assets/golangci.yml` 作为起点——
|
||||
> 它增加了 gosec、ineffassign、misspell、gocyclo 和 bodyclose。
|
||||
|
||||
---
|
||||
|
||||
## 快速参考
|
||||
|
||||
| 任务 | 命令/操作 |
|
||||
|------|-----------|
|
||||
| 安装 golangci-lint | `go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest` |
|
||||
| 运行 linter | `golangci-lint run` |
|
||||
| 对路径运行 | `golangci-lint run ./pkg/...` |
|
||||
| 配置文件 | 项目根目录的 `.golangci.yml` |
|
||||
| CI 集成 | 在管道中运行 `golangci-lint run` |
|
||||
| Nolint 指令 | `//nolint:name // 原因`——永远不要使用裸 `//nolint` |
|
||||
| CI 集成 | 使用 `golangci/golangci-lint-action` 用于 GitHub Actions |
|
||||
| Pre-commit | `golangci-lint run --new-from-rev=HEAD~1` |
|
||||
|
||||
### Linter 选择指南
|
||||
|
||||
| 当你需要... | 使用 |
|
||||
|-------------|------|
|
||||
| 错误处理覆盖率 | errcheck |
|
||||
| 导入格式化 | goimports |
|
||||
| 风格一致性 | revive |
|
||||
| Bug 检测 | govet、staticcheck |
|
||||
| 以上全部 | golangci-lint 配合配置 |
|
||||
|
||||
---
|
||||
|
||||
## 相关技能
|
||||
|
||||
- **风格基础**:在解决 linter 执行的风格问题(格式化、嵌套、命名)时,参见 [go-style-core](../go-style-core/SKILL.md)
|
||||
- **代码审查**:在将 linter 输出与手动审查清单结合使用时,参见 [go-code-review](../go-code-review/SKILL.md)
|
||||
- **错误处理**:在 errcheck 标记未处理的错误并需要决定如何处理时,参见 [go-error-handling](../go-error-handling/SKILL.md)
|
||||
- **测试**:在 CI 管道中将 linter 与测试一起运行时,参见 [go-testing](../go-testing/SKILL.md)
|
||||
@@ -0,0 +1,31 @@
|
||||
run:
|
||||
timeout: 5m
|
||||
|
||||
linters:
|
||||
enable:
|
||||
# Minimum recommended
|
||||
- errcheck
|
||||
- goimports
|
||||
- revive
|
||||
- govet
|
||||
- staticcheck
|
||||
# Additional recommended
|
||||
- gosec
|
||||
- ineffassign
|
||||
- misspell
|
||||
- gocyclo
|
||||
- bodyclose
|
||||
|
||||
linters-settings:
|
||||
goimports:
|
||||
local-prefixes: "" # Set to your module path
|
||||
revive:
|
||||
rules:
|
||||
- name: exported
|
||||
gocyclo:
|
||||
min-complexity: 15
|
||||
|
||||
issues:
|
||||
exclude-use-default: false
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="1.0.0"
|
||||
SCRIPT_NAME="$(basename "$0")"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
$SCRIPT_NAME v$VERSION — Generate .golangci.yml and run initial lint
|
||||
|
||||
USAGE
|
||||
bash $SCRIPT_NAME [options] [local-prefix]
|
||||
|
||||
DESCRIPTION
|
||||
Creates a .golangci.yml with a curated set of linters (errcheck,
|
||||
goimports, revive, govet, staticcheck) and runs golangci-lint.
|
||||
If local-prefix is provided, configures goimports to group local
|
||||
imports separately.
|
||||
|
||||
Exits 0 if lint passes, 1 if lint issues found, 2 on error.
|
||||
|
||||
OPTIONS
|
||||
-h, --help Show this help message
|
||||
-v, --version Show version
|
||||
--json Output results as JSON
|
||||
--force Overwrite existing .golangci.yml
|
||||
--dry-run Print generated config to stdout without writing
|
||||
--limit N Max lint issue lines in JSON output (default: 50, 0 = unlimited)
|
||||
|
||||
ARGUMENTS
|
||||
local-prefix Module path prefix for goimports grouping
|
||||
(e.g., github.com/myorg/myrepo)
|
||||
|
||||
EXAMPLES
|
||||
bash $SCRIPT_NAME
|
||||
bash $SCRIPT_NAME github.com/myorg/myrepo
|
||||
bash $SCRIPT_NAME --force github.com/myorg/myrepo
|
||||
bash $SCRIPT_NAME --dry-run github.com/myorg/myrepo
|
||||
bash $SCRIPT_NAME --json
|
||||
bash $SCRIPT_NAME --json --limit 20
|
||||
EOF
|
||||
}
|
||||
|
||||
json_escape() {
|
||||
local s="$1"
|
||||
s="${s//\\/\\\\}"
|
||||
s="${s//\"/\\\"}"
|
||||
s="${s//$'\t'/\\t}"
|
||||
s="${s//$'\r'/}"
|
||||
s="${s//$'\n'/\\n}"
|
||||
printf '%s' "$s"
|
||||
}
|
||||
|
||||
JSON_OUTPUT=false
|
||||
FORCE=false
|
||||
DRY_RUN=false
|
||||
LIMIT=50
|
||||
LOCAL_PREFIX=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help) usage; exit 0 ;;
|
||||
-v|--version) echo "$SCRIPT_NAME v$VERSION"; exit 0 ;;
|
||||
--json) JSON_OUTPUT=true; shift ;;
|
||||
--force) FORCE=true; shift ;;
|
||||
--dry-run) DRY_RUN=true; shift ;;
|
||||
--limit) LIMIT="${2:?error: --limit requires a number}"; shift 2 ;;
|
||||
-*) echo "error: unknown option: $1" >&2; usage >&2; exit 2 ;;
|
||||
*) LOCAL_PREFIX="$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
generate_config() {
|
||||
cat <<'YAML'
|
||||
linters:
|
||||
enable:
|
||||
- errcheck
|
||||
- goimports
|
||||
- revive
|
||||
- govet
|
||||
- staticcheck
|
||||
|
||||
linters-settings:
|
||||
YAML
|
||||
|
||||
if [[ -n "$LOCAL_PREFIX" ]]; then
|
||||
cat <<YAML
|
||||
goimports:
|
||||
local-prefixes: ${LOCAL_PREFIX}
|
||||
YAML
|
||||
fi
|
||||
|
||||
cat <<'YAML'
|
||||
revive:
|
||||
rules:
|
||||
- name: blank-imports
|
||||
- name: context-as-argument
|
||||
- name: error-return
|
||||
- name: error-strings
|
||||
- name: exported
|
||||
|
||||
run:
|
||||
timeout: 5m
|
||||
YAML
|
||||
}
|
||||
|
||||
if $DRY_RUN; then
|
||||
generate_config
|
||||
exit 0
|
||||
fi
|
||||
|
||||
CONFIG_PATH=".golangci.yml"
|
||||
|
||||
if [[ -f "$CONFIG_PATH" ]] && ! $FORCE; then
|
||||
echo "error: $CONFIG_PATH already exists (use --force to overwrite)" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
generate_config > "$CONFIG_PATH"
|
||||
|
||||
LINT_OUTPUT=""
|
||||
LINT_EXIT=0
|
||||
if ! command -v golangci-lint &>/dev/null; then
|
||||
echo "error: golangci-lint is not installed" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
LINT_OUTPUT=$(golangci-lint run ./... 2>&1) || LINT_EXIT=$?
|
||||
|
||||
if $JSON_OUTPUT; then
|
||||
LINT_TRUNCATED=false
|
||||
LINT_DISPLAY="$LINT_OUTPUT"
|
||||
if [[ $LIMIT -gt 0 && -n "$LINT_OUTPUT" ]]; then
|
||||
LINT_ARR=()
|
||||
while IFS= read -r line; do
|
||||
LINT_ARR+=("$line")
|
||||
done <<< "$LINT_OUTPUT"
|
||||
if [[ ${#LINT_ARR[@]} -gt $LIMIT ]]; then
|
||||
LINT_DISPLAY=""
|
||||
for (( i=0; i<LIMIT; i++ )); do
|
||||
[[ -n "$LINT_DISPLAY" ]] && LINT_DISPLAY+=$'\n'
|
||||
LINT_DISPLAY+="${LINT_ARR[$i]}"
|
||||
done
|
||||
LINT_TRUNCATED=true
|
||||
fi
|
||||
fi
|
||||
LINT_ESC="$(json_escape "$LINT_DISPLAY")"
|
||||
CONFIG_ESC="$(json_escape "$CONFIG_PATH")"
|
||||
PREFIX_ESC="$(json_escape "$LOCAL_PREFIX")"
|
||||
CREATED=true
|
||||
HAS_ISSUES=$( [[ $LINT_EXIT -ne 0 ]] && echo true || echo false )
|
||||
TRUNC_FIELD=""
|
||||
$LINT_TRUNCATED && TRUNC_FIELD=',"truncated":true'
|
||||
cat <<EOF
|
||||
{"config_path":"$CONFIG_ESC","local_prefix":"$PREFIX_ESC","created":$CREATED,"lint_issues":$HAS_ISSUES,"lint_output":"$LINT_ESC"$TRUNC_FIELD}
|
||||
EOF
|
||||
else
|
||||
echo "Created $CONFIG_PATH"
|
||||
if [[ $LINT_EXIT -ne 0 ]]; then
|
||||
echo ""
|
||||
echo "$LINT_OUTPUT"
|
||||
echo ""
|
||||
echo "Lint issues found — fix them category by category (formatting first, then vet, then style)."
|
||||
else
|
||||
echo "golangci-lint: all clean."
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ $LINT_EXIT -ne 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
@@ -0,0 +1,187 @@
|
||||
---
|
||||
name: go-logging
|
||||
description: 在选择日志方案、配置 slog、编写结构化日志语句或决定日志级别时使用。也适用于设置生产日志、为日志添加请求作用域上下文或从 log 迁移到 slog 的场景,即使用户未明确提及日志。不涵盖错误处理策略(参见 go-error-handling)。
|
||||
license: Apache-2.0
|
||||
compatibility: slog requires Go 1.21+; slog/slogtest requires Go 1.22+
|
||||
metadata:
|
||||
sources: "Google Style Guide, Uber Style Guide"
|
||||
---
|
||||
|
||||
# Go 日志
|
||||
|
||||
## 核心原则
|
||||
|
||||
日志是给**运维人员**看的,不是给开发人员看的。每一行日志都应该帮助某人诊断生产问题。如果不能达到这个目的,就是噪音。
|
||||
|
||||
---
|
||||
|
||||
## 选择日志器
|
||||
|
||||
> **规范**:在新的 Go 代码中使用 `log/slog`。
|
||||
|
||||
`slog` 是结构化的、分级别的,并且在标准库中(Go 1.21+)。它涵盖了绝大多数生产日志需求。
|
||||
|
||||
```
|
||||
选择哪个日志器?
|
||||
├─ 新的生产代码 → log/slog
|
||||
├─ 简单 CLI / 一次性 → log(标准库)
|
||||
└─ 有性能瓶颈 → zerolog 或 zap(先做基准测试)
|
||||
```
|
||||
|
||||
除非性能分析显示 `slog` 在热路径中是瓶颈,否则不要引入第三方日志库。引入时,保持相同的结构化键值风格。
|
||||
|
||||
> 在设置 slog handler、配置 JSON/文本输出或从 log.Printf 迁移到 slog 时,阅读 [references/LOGGING-PATTERNS.md](references/LOGGING-PATTERNS.md)。
|
||||
|
||||
---
|
||||
|
||||
## 结构化日志
|
||||
|
||||
> **规范**:始终使用键值对。永远不要将值插值到消息字符串中。
|
||||
|
||||
消息是描述发生了什么的**静态描述**。动态数据放在键值属性中:
|
||||
|
||||
```go
|
||||
// 好:静态消息,结构化字段
|
||||
slog.Info("order placed", "order_id", orderID, "total", total)
|
||||
|
||||
// 不好:动态数据嵌入到消息字符串中
|
||||
slog.Info(fmt.Sprintf("order %d placed for $%.2f", orderID, total))
|
||||
```
|
||||
|
||||
### 键名
|
||||
|
||||
> **建议**:日志属性键使用 `snake_case`。
|
||||
|
||||
键应为小写、下划线分隔,并在整个代码库中保持一致:`user_id`、`request_id`、`elapsed_ms`。
|
||||
|
||||
### 类型化属性
|
||||
|
||||
对于性能关键路径,使用类型化构造函数以避免分配:
|
||||
|
||||
```go
|
||||
slog.LogAttrs(ctx, slog.LevelInfo, "request handled",
|
||||
slog.String("method", r.Method),
|
||||
slog.Int("status", code),
|
||||
slog.Duration("elapsed", elapsed),
|
||||
)
|
||||
```
|
||||
|
||||
> 在优化日志性能或使用 Enabled() 进行预检查时,阅读 [references/LEVELS-AND-CONTEXT.md](references/LEVELS-AND-CONTEXT.md)。
|
||||
|
||||
---
|
||||
|
||||
## 日志级别
|
||||
|
||||
> **建议**:一致地遵循这些级别语义。
|
||||
|
||||
| 级别 | 何时使用 | 生产默认 |
|
||||
|------|----------|----------|
|
||||
| Debug | 仅开发人员的诊断,跟踪内部状态 | 禁用 |
|
||||
| Info | 重要的生命周期事件:启动、关闭、配置加载 | 启用 |
|
||||
| Warn | 意外但可恢复:使用了弃用功能、重试成功 | 启用 |
|
||||
| Error | 操作失败,需要运维人员关注 | 启用 |
|
||||
|
||||
**经验法则**:
|
||||
- 如果没有人需要对其采取行动,那就不是 Error——使用 Warn 或 Info
|
||||
- 如果只在连接调试器时才有用,那就是 Debug
|
||||
- `slog.Error` 应始终包含 `"err"` 属性
|
||||
|
||||
```go
|
||||
slog.Error("payment failed", "err", err, "order_id", id)
|
||||
slog.Warn("retry succeeded", "attempt", n, "endpoint", url)
|
||||
slog.Info("server started", "addr", addr)
|
||||
slog.Debug("cache lookup", "key", key, "hit", hit)
|
||||
```
|
||||
|
||||
> 在 Warn 和 Error 之间选择或定义自定义详细级别时,阅读 [references/LEVELS-AND-CONTEXT.md](references/LEVELS-AND-CONTEXT.md)。
|
||||
|
||||
---
|
||||
|
||||
## 请求作用域日志
|
||||
|
||||
> **建议**:从 context 派生日志器以携带请求作用域字段。
|
||||
|
||||
使用中间件为日志器添加请求 ID、用户 ID 或跟踪 ID,然后通过 context 或作为显式参数将增强后的日志器传递给下游:
|
||||
|
||||
```go
|
||||
func middleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
logger := slog.With("request_id", requestID(r))
|
||||
ctx := context.WithValue(r.Context(), loggerKey, logger)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
该请求中所有后续的日志调用都会自动携带 `request_id`。
|
||||
|
||||
> 在实现日志中间件或通过 context 传递日志器时,阅读 [references/LOGGING-PATTERNS.md](references/LOGGING-PATTERNS.md)。
|
||||
|
||||
---
|
||||
|
||||
## 日志或返回,不要同时
|
||||
|
||||
> **规范**:每个错误恰好处理一次——要么记录它,要么返回它。
|
||||
|
||||
记录错误然后返回它会导致重复噪音,因为栈上游的调用者也会处理该错误。
|
||||
|
||||
```go
|
||||
// 不好:在这里记录,并且栈上游的每个调用者也会记录
|
||||
if err != nil {
|
||||
slog.Error("query failed", "err", err)
|
||||
return fmt.Errorf("query: %w", err)
|
||||
}
|
||||
|
||||
// 好:包装并返回——让调用者决定
|
||||
if err != nil {
|
||||
return fmt.Errorf("query: %w", err)
|
||||
}
|
||||
```
|
||||
|
||||
**例外**:HTTP 处理器和其他栈顶边界可以在服务端记录详细错误,同时向客户端返回脱敏消息:
|
||||
|
||||
```go
|
||||
if err != nil {
|
||||
slog.Error("checkout failed", "err", err, "user_id", uid)
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
参见 [go-error-handling](../go-error-handling/SKILL.md) 了解完整的处理一次模式和错误包装指导。
|
||||
|
||||
---
|
||||
|
||||
## 不应记录的内容
|
||||
|
||||
> **规范**:永远不要记录密钥、凭证、PII 或高基数无界数据。
|
||||
|
||||
- 密码、API 密钥、令牌、会话 ID
|
||||
- 完整的信用卡号、社会安全号
|
||||
- 可能包含用户数据的请求/响应体
|
||||
- 无界大小的完整切片或映射
|
||||
|
||||
> 在决定哪些数据可以安全包含在日志属性中时,阅读 [references/LEVELS-AND-CONTEXT.md](references/LEVELS-AND-CONTEXT.md)。
|
||||
|
||||
---
|
||||
|
||||
## 快速参考
|
||||
|
||||
| 应该 | 不应该 |
|
||||
|------|--------|
|
||||
| `slog.Info("msg", "key", val)` | `log.Printf("msg %v", val)` |
|
||||
| 静态消息 + 结构化字段 | 在消息中使用 `fmt.Sprintf` |
|
||||
| `snake_case` 键 | camelCase 或不一致的键 |
|
||||
| 日志或返回错误 | 同时日志和返回同一错误 |
|
||||
| 从 context 派生日志器 | 每次调用创建新日志器 |
|
||||
| `slog.Error` 配合 `"err"` 属性 | 用 `slog.Info` 记录错误 |
|
||||
| 在热路径上预检查 `Enabled()` | 始终分配日志参数 |
|
||||
|
||||
---
|
||||
|
||||
## 相关技能
|
||||
|
||||
- **错误处理**:在决定是记录还是返回错误,或了解处理一次模式时,参见 [go-error-handling](../go-error-handling/SKILL.md)
|
||||
- **上下文传播**:在通过 context 传递请求作用域值(包括日志器)时,参见 [go-context](../go-context/SKILL.md)
|
||||
- **性能**:在优化热路径日志或减少日志调用中的分配时,参见 [go-performance](../go-performance/SKILL.md)
|
||||
- **代码审查**:在审查 Go PR 中的日志实践时,参见 [go-code-review](../go-code-review/SKILL.md)
|
||||
@@ -0,0 +1,244 @@
|
||||
# 级别与上下文
|
||||
|
||||
关于日志级别语义、基于 context 的日志模式、性能考虑以及哪些内容不应出现在日志中的详细指导。
|
||||
|
||||
## 级别语义
|
||||
|
||||
### Debug
|
||||
|
||||
仅开发人员的诊断。生产中默认禁用。用于跟踪在开发或故障排查期间有帮助的内部状态:
|
||||
|
||||
```go
|
||||
slog.Debug("cache lookup", "key", key, "hit", hit)
|
||||
slog.Debug("parsed config", "fields", len(cfg.Fields))
|
||||
slog.Debug("SQL query", "query", q, "args", args)
|
||||
```
|
||||
|
||||
**何时使用**:内部状态转换、缓存行为、开发期间的详细请求/响应数据。
|
||||
|
||||
### Info
|
||||
|
||||
确认系统按预期运行的重要事件。这些应在生产中对理解系统行为有用:
|
||||
|
||||
```go
|
||||
slog.Info("server started", "addr", addr, "version", version)
|
||||
slog.Info("config loaded", "path", cfgPath, "env", env)
|
||||
slog.Info("migration completed", "version", v, "elapsed_ms", elapsed)
|
||||
slog.Info("user registered", "user_id", uid)
|
||||
```
|
||||
|
||||
**何时使用**:启动/关闭、配置变更、重要业务事件、周期性健康摘要。
|
||||
|
||||
### Warn
|
||||
|
||||
发生了意外的事情,但系统已恢复或优雅降级。运维人员可能想要调查但不需要立即行动:
|
||||
|
||||
```go
|
||||
slog.Warn("retry succeeded", "attempt", n, "endpoint", url)
|
||||
slog.Warn("deprecated endpoint called", "path", r.URL.Path, "user_id", uid)
|
||||
slog.Warn("rate limit approaching", "current", rate, "limit", max)
|
||||
slog.Warn("fallback to default config", "err", err)
|
||||
```
|
||||
|
||||
**何时使用**:最终成功的重试、弃用的代码路径、接近资源限制、回退行为。
|
||||
|
||||
### Error
|
||||
|
||||
操作失败并需要运维人员关注。系统无法完成请求或任务:
|
||||
|
||||
```go
|
||||
slog.Error("payment failed", "err", err, "order_id", id, "amount", amt)
|
||||
slog.Error("database connection lost", "err", err, "host", dbHost)
|
||||
slog.Error("message processing failed", "err", err, "msg_id", msgID)
|
||||
```
|
||||
|
||||
**何时使用**:影响用户的失败操作、丢失的连接、数据完整性问题、未恢复的外部服务故障。
|
||||
|
||||
**始终包含错误**:`slog.Error` 调用应始终带有包含实际错误值的 `"err"` 属性。
|
||||
|
||||
### 在 Warn 和 Error 之间选择
|
||||
|
||||
```
|
||||
操作最终是否成功?
|
||||
├─ 是(经过重试/回退后)→ Warn
|
||||
└─ 否(调用者收到错误)→ Error
|
||||
├─ 需要立即关注 → Error
|
||||
└─ 可以等到下次审查 → Warn
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 自定义详细级别
|
||||
|
||||
slog 级别是整数。在标准级别之间定义自定义子级别以实现细粒度控制:
|
||||
|
||||
```go
|
||||
const (
|
||||
LevelTrace = slog.Level(-8) // 低于 Debug
|
||||
LevelNotice = slog.Level(2) // 在 Info 和 Warn 之间
|
||||
)
|
||||
|
||||
slog.Log(ctx, LevelTrace, "detailed trace", "span_id", spanID)
|
||||
```
|
||||
|
||||
使用 `HandlerOptions.Level` 配合 `slog.LevelVar` 在运行时控制最低级别。
|
||||
|
||||
---
|
||||
|
||||
## 基于 Context 的日志
|
||||
|
||||
### 模式 1:Context 中的日志器
|
||||
|
||||
在 context 中存储增强后的 `*slog.Logger`。每个中间件层添加自己的字段:
|
||||
|
||||
```go
|
||||
func authMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
userID := authenticate(r)
|
||||
logger := loggerFromCtx(r.Context()).With("user_id", userID)
|
||||
ctx := context.WithValue(r.Context(), loggerKey, logger)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
**优点**:简单,与任何 handler 链配合使用。
|
||||
**缺点**:需要纪律来始终使用 `loggerFromCtx`。
|
||||
|
||||
### 模式 2:显式日志器参数
|
||||
|
||||
将 `*slog.Logger` 作为函数参数与 context 一起传递:
|
||||
|
||||
```go
|
||||
func processOrder(ctx context.Context, logger *slog.Logger, order *Order) error {
|
||||
logger.Info("processing order", "order_id", order.ID)
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**优点**:显式依赖,更易测试,无需 context 键。
|
||||
**缺点**:每个函数签名中都有额外参数。
|
||||
|
||||
### 何时使用哪种
|
||||
|
||||
| 场景 | 推荐 |
|
||||
|------|------|
|
||||
| HTTP 处理器 / 中间件链 | Context 中的日志器 |
|
||||
| 无 HTTP 依赖的库代码 | 显式参数 |
|
||||
| 后台工作器 / 批处理任务 | 显式参数 |
|
||||
| 深层调用链(5 层以上) | Context 中的日志器 |
|
||||
|
||||
---
|
||||
|
||||
## 性能考虑
|
||||
|
||||
### 使用 Enabled() 预检查
|
||||
|
||||
当日志级别被禁用时避免分配日志参数:
|
||||
|
||||
```go
|
||||
// 开销大:参数始终被求值,即使 Debug 被禁用
|
||||
slog.Debug("request details",
|
||||
"headers", fmt.Sprintf("%v", r.Header),
|
||||
"body", string(bodyBytes),
|
||||
)
|
||||
|
||||
// 更好:禁用时完全跳过
|
||||
if slog.Default().Enabled(ctx, slog.LevelDebug) {
|
||||
slog.Debug("request details",
|
||||
"headers", fmt.Sprintf("%v", r.Header),
|
||||
"body", string(bodyBytes),
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
当参数构造开销大(格式化、序列化或读取数据)时,这很重要。对于简单属性(`slog.String`、`slog.Int`),开销可以忽略不计。
|
||||
|
||||
### 在热路径上使用 LogAttrs
|
||||
|
||||
`slog.LogAttrs` 避免了便捷方法(`slog.Info` 等)产生的 `[]any` 分配:
|
||||
|
||||
```go
|
||||
// 标准——为键值对分配一个 []any
|
||||
slog.Info("request handled", "method", r.Method, "status", code)
|
||||
|
||||
// 更快——类型化属性,无 []any 分配
|
||||
slog.LogAttrs(ctx, slog.LevelInfo, "request handled",
|
||||
slog.String("method", r.Method),
|
||||
slog.Int("status", code),
|
||||
)
|
||||
```
|
||||
|
||||
### 避免在紧凑循环中记录日志
|
||||
|
||||
如果循环处理数千个项目,记录摘要而不是每次迭代:
|
||||
|
||||
```go
|
||||
// 不好:10k 项目批次中每个项目一条日志
|
||||
for _, item := range items {
|
||||
slog.Debug("processing item", "id", item.ID)
|
||||
process(item)
|
||||
}
|
||||
|
||||
// 好:记录摘要
|
||||
slog.Info("batch started", "count", len(items))
|
||||
processed, failed := processBatch(items)
|
||||
slog.Info("batch completed", "processed", processed, "failed", failed)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 不应记录的内容
|
||||
|
||||
### 密钥和凭证
|
||||
|
||||
永远不要记录:
|
||||
- 密码、API 密钥、令牌(OAuth、JWT、会话)
|
||||
- 私钥、证书
|
||||
- 包含凭证的数据库连接字符串
|
||||
|
||||
```go
|
||||
// 不好
|
||||
slog.Info("connecting", "dsn", dsn) // 可能包含密码
|
||||
|
||||
// 好
|
||||
slog.Info("connecting", "host", dbHost, "database", dbName)
|
||||
```
|
||||
|
||||
### 个人身份信息(PII)
|
||||
|
||||
除非调试所需且你的保留策略允许,否则避免记录:
|
||||
- 电子邮件地址、电话号码
|
||||
- 完整姓名、物理地址
|
||||
- IP 地址(在某些司法管辖区)
|
||||
- 信用卡号、社会安全号
|
||||
|
||||
如果必须记录用户标识符,使用不透明 ID 而非 PII。
|
||||
|
||||
### 高基数无界数据
|
||||
|
||||
不要记录完整的请求体、Info 级别的完整栈跟踪或无界集合:
|
||||
|
||||
```go
|
||||
// 不好:无界数据
|
||||
slog.Info("received", "body", string(requestBody))
|
||||
slog.Info("users loaded", "users", users) // 可能有 10 万条记录
|
||||
|
||||
// 好:有界摘要
|
||||
slog.Info("received", "content_length", len(requestBody), "content_type", ct)
|
||||
slog.Info("users loaded", "count", len(users))
|
||||
```
|
||||
|
||||
### 决策表
|
||||
|
||||
| 数据类型 | 记录吗? | 替代方案 |
|
||||
|----------|----------|----------|
|
||||
| 请求 ID / 跟踪 ID | 是 | — |
|
||||
| 用户 ID(不透明的) | 是 | — |
|
||||
| HTTP 方法、路径、状态 | 是 | — |
|
||||
| 错误消息 | 是 | — |
|
||||
| 密码 / 令牌 | **永不** | 记录令牌前缀或 "已脱敏" |
|
||||
| 完整请求体 | **否** | 记录内容长度和类型 |
|
||||
| PII(邮箱、姓名) | **避免** | 记录不透明用户 ID |
|
||||
| 大型集合 | **否** | 记录数量或摘要 |
|
||||
| 栈跟踪 | 仅 Debug | 使用 `slog.Debug` |
|
||||
@@ -0,0 +1,314 @@
|
||||
# 日志模式
|
||||
|
||||
关于 slog 设置、handler 配置、测试、HTTP 中间件以及从旧版 `log` 包迁移的详细模式。
|
||||
|
||||
## 设置 slog
|
||||
|
||||
### 基本配置
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// JSON handler 用于生产(机器可解析)
|
||||
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
slog.SetDefault(logger)
|
||||
|
||||
slog.Info("server started", "addr", ":8080")
|
||||
// 输出:{"time":"...","level":"INFO","msg":"server started","addr":":8080"}
|
||||
}
|
||||
```
|
||||
|
||||
### 用于开发的 Text Handler
|
||||
|
||||
```go
|
||||
// 本地开发的人类可读输出
|
||||
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
|
||||
Level: slog.LevelDebug,
|
||||
}))
|
||||
slog.SetDefault(logger)
|
||||
// 输出:time=... level=DEBUG msg="cache lookup" key=user:42 hit=true
|
||||
```
|
||||
|
||||
### 动态级别控制
|
||||
|
||||
使用 `slog.LevelVar` 在运行时更改最低级别(例如通过管理端点或信号处理器):
|
||||
|
||||
```go
|
||||
var programLevel = new(slog.LevelVar) // 默认 Info
|
||||
|
||||
func init() {
|
||||
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
|
||||
Level: programLevel,
|
||||
}))
|
||||
slog.SetDefault(logger)
|
||||
}
|
||||
|
||||
// 从管理端点或信号处理器调用
|
||||
func enableDebug() {
|
||||
programLevel.Set(slog.LevelDebug)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 自定义 Handler 模式
|
||||
|
||||
### 添加源位置
|
||||
|
||||
```go
|
||||
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
|
||||
AddSource: true,
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
// 输出包含:"source":{"function":"main.handleRequest","file":"server.go","line":42}
|
||||
```
|
||||
|
||||
### 使用默认属性包装 Handler
|
||||
|
||||
使用 `slog.Handler` 中间件向每条日志记录注入字段:
|
||||
|
||||
```go
|
||||
type contextHandler struct {
|
||||
inner slog.Handler
|
||||
attrs []slog.Attr
|
||||
}
|
||||
|
||||
func (h *contextHandler) Enabled(ctx context.Context, level slog.Level) bool {
|
||||
return h.inner.Enabled(ctx, level)
|
||||
}
|
||||
|
||||
func (h *contextHandler) Handle(ctx context.Context, r slog.Record) error {
|
||||
r.AddAttrs(h.attrs...)
|
||||
return h.inner.Handle(ctx, r)
|
||||
}
|
||||
|
||||
func (h *contextHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||
return &contextHandler{inner: h.inner.WithAttrs(attrs), attrs: h.attrs}
|
||||
}
|
||||
|
||||
func (h *contextHandler) WithGroup(name string) slog.Handler {
|
||||
return &contextHandler{inner: h.inner.WithGroup(name), attrs: h.attrs}
|
||||
}
|
||||
```
|
||||
|
||||
### 多 Handler(扇出)
|
||||
|
||||
写入多个目标(例如 stdout + 文件):
|
||||
|
||||
```go
|
||||
type multiHandler struct {
|
||||
handlers []slog.Handler
|
||||
}
|
||||
|
||||
func (m *multiHandler) Enabled(ctx context.Context, level slog.Level) bool {
|
||||
for _, h := range m.handlers {
|
||||
if h.Enabled(ctx, level) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *multiHandler) Handle(ctx context.Context, r slog.Record) error {
|
||||
var errs []error
|
||||
for _, h := range m.handlers {
|
||||
if h.Enabled(ctx, r.Level) {
|
||||
if err := h.Handle(ctx, r); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
func (m *multiHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||
handlers := make([]slog.Handler, len(m.handlers))
|
||||
for i, h := range m.handlers {
|
||||
handlers[i] = h.WithAttrs(attrs)
|
||||
}
|
||||
return &multiHandler{handlers: handlers}
|
||||
}
|
||||
|
||||
func (m *multiHandler) WithGroup(name string) slog.Handler {
|
||||
handlers := make([]slog.Handler, len(m.handlers))
|
||||
for i, h := range m.handlers {
|
||||
handlers[i] = h.WithGroup(name)
|
||||
}
|
||||
return &multiHandler{handlers: handlers}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 使用 slogtest 测试
|
||||
|
||||
Go 1.22+ 提供了 `testing/slogtest` 来验证 handler 实现:
|
||||
|
||||
```go
|
||||
package myhandler_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"testing/slogtest"
|
||||
)
|
||||
|
||||
func TestHandler(t *testing.T) {
|
||||
// newHandler 返回你的自定义 slog.Handler 和一个
|
||||
// 将输出解析为 []map[string]any 的函数用于验证。
|
||||
results := func(t *testing.T) map[string]any {
|
||||
// 在此解析你的 handler 输出
|
||||
}
|
||||
|
||||
h := NewMyHandler(buf, nil)
|
||||
slogtest.Run(t, func(t *testing.T) slog.Handler { return h }, results)
|
||||
}
|
||||
```
|
||||
|
||||
### 在测试中捕获日志
|
||||
|
||||
对于断言日志输出的单元测试,写入 buffer:
|
||||
|
||||
```go
|
||||
func TestOrderProcessing(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewJSONHandler(&buf, nil))
|
||||
|
||||
processOrder(logger, order)
|
||||
|
||||
if !strings.Contains(buf.String(), `"order_id"`) {
|
||||
t.Error("expected order_id in log output")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## HTTP 请求日志中间件
|
||||
|
||||
一个完整的中间件,记录每个请求的计时、状态和请求作用域字段:
|
||||
|
||||
```go
|
||||
func loggingMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
reqID := r.Header.Get("X-Request-ID")
|
||||
if reqID == "" {
|
||||
reqID = uuid.NewString()
|
||||
}
|
||||
|
||||
logger := slog.With(
|
||||
"request_id", reqID,
|
||||
"method", r.Method,
|
||||
"path", r.URL.Path,
|
||||
)
|
||||
|
||||
// 包装 response writer 以捕获状态码
|
||||
rw := &responseWriter{ResponseWriter: w, status: http.StatusOK}
|
||||
|
||||
// 将日志器存入 context 供下游 handler 使用
|
||||
ctx := context.WithValue(r.Context(), loggerKey, logger)
|
||||
next.ServeHTTP(rw, r.WithContext(ctx))
|
||||
|
||||
logger.Info("request completed",
|
||||
"status", rw.status,
|
||||
"elapsed_ms", time.Since(start).Milliseconds(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
type responseWriter struct {
|
||||
http.ResponseWriter
|
||||
status int
|
||||
}
|
||||
|
||||
func (rw *responseWriter) WriteHeader(code int) {
|
||||
rw.status = code
|
||||
rw.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
```
|
||||
|
||||
### 从 Context 获取日志器
|
||||
|
||||
```go
|
||||
type ctxKey struct{}
|
||||
|
||||
var loggerKey = ctxKey{}
|
||||
|
||||
func loggerFromCtx(ctx context.Context) *slog.Logger {
|
||||
if l, ok := ctx.Value(loggerKey).(*slog.Logger); ok {
|
||||
return l
|
||||
}
|
||||
return slog.Default()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 从 log.Printf 迁移到 slog
|
||||
|
||||
### 第 1 步:替换直接调用
|
||||
|
||||
```go
|
||||
// 迁移前
|
||||
log.Printf("user %s logged in from %s", userID, ip)
|
||||
|
||||
// 迁移后
|
||||
slog.Info("user logged in", "user_id", userID, "ip", ip)
|
||||
```
|
||||
|
||||
### 第 2 步:替换 main() 中的 log.Fatalf
|
||||
|
||||
```go
|
||||
// 迁移前
|
||||
log.Fatalf("failed to connect: %v", err)
|
||||
|
||||
// 迁移后——slog 没有 Fatal;在 main 中使用 slog + os.Exit
|
||||
slog.Error("failed to connect", "err", err)
|
||||
os.Exit(1)
|
||||
```
|
||||
|
||||
### 第 3 步:桥接旧代码
|
||||
|
||||
如果逐步迁移,将标准 `log` 包的输出通过 slog 重定向:
|
||||
|
||||
```go
|
||||
// 在 main() 中,设置 slog 之后:
|
||||
slog.SetDefault(logger)
|
||||
|
||||
// 标准 log 包现在通过 slog 的默认 handler 写入。
|
||||
// 这是因为 slog.SetDefault 也会更新 log.Default()。
|
||||
```
|
||||
|
||||
### 第 4 步:替换日志器参数
|
||||
|
||||
```go
|
||||
// 迁移前:传递 *log.Logger
|
||||
func NewServer(addr string, logger *log.Logger) *Server
|
||||
|
||||
// 迁移后:显式传递 *slog.Logger
|
||||
func NewServer(addr string, logger *slog.Logger) *Server
|
||||
|
||||
// 或从 handler 中的 context 派生
|
||||
func (s *Server) handleRequest(ctx context.Context) {
|
||||
logger := loggerFromCtx(ctx)
|
||||
logger.Info("handling request")
|
||||
}
|
||||
```
|
||||
|
||||
### 迁移清单
|
||||
|
||||
| 步骤 | 更改什么 | 验证 |
|
||||
|------|----------|------|
|
||||
| 1 | `log.Printf` → `slog.Info/Warn/Error` | `rg 'log\.Printf'` 返回 0 个匹配 |
|
||||
| 2 | `log.Fatalf` → `slog.Error` + `os.Exit(1)` 在 main 中 | 仅在 `main()` 中 |
|
||||
| 3 | 在 main 中尽早设置 `slog.SetDefault` | 旧版 `log` 调用通过 slog 路由 |
|
||||
| 4 | `*log.Logger` 参数 → `*slog.Logger` | 所有构造函数已更新 |
|
||||
| 5 | 移除已替换处的 `"log"` 导入 | `goimports` 会自动处理 |
|
||||
@@ -0,0 +1,138 @@
|
||||
---
|
||||
name: go-packages
|
||||
description: Use when creating Go packages, organizing imports, managing dependencies, or deciding how to structure Go code into packages. Also use when starting a new Go project or splitting a growing codebase into packages, even if the user doesn't explicitly ask about package organization. Does not cover naming individual identifiers (see go-naming).
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
sources: "Google Style Guide, Uber Style Guide, Go Wiki CodeReviewComments"
|
||||
---
|
||||
|
||||
# Go 包和 Import
|
||||
|
||||
> **本技能不适用的场景**:对于包内单个标识符的命名,参见 [go-naming](../go-naming/SKILL.md)。对于单文件中函数的组织,参见 [go-functions](../go-functions/SKILL.md)。对于强制执行 import 规则的 linter 配置,参见 [go-linting](../go-linting/SKILL.md)。
|
||||
|
||||
## 包组织
|
||||
|
||||
### 避免 Util 包
|
||||
|
||||
包名应描述包提供的内容。避免使用 `util`、`helper`、`common` 等泛化名称——它们会模糊含义并导致 import 冲突。
|
||||
|
||||
```go
|
||||
// 好:有意义的包名
|
||||
db := spannertest.NewDatabaseFromFile(...)
|
||||
_, err := f.Seek(0, io.SeekStart)
|
||||
|
||||
// 不好:模糊的名称遮蔽含义
|
||||
db := test.NewDatabaseFromFile(...)
|
||||
_, err := f.Seek(0, common.SeekStart)
|
||||
```
|
||||
|
||||
泛化名称可以作为名称的*一部分*(例如 `stringutil`),但不应成为整个包名。
|
||||
|
||||
### Package Size
|
||||
|
||||
| 问题 | 操作 |
|
||||
|------|------|
|
||||
| 你能用一句话描述它的用途吗? | 不能 → 按职责拆分 |
|
||||
| 文件中从未共享未导出的符号? | 这些文件可以是独立的包 |
|
||||
| 不同的用户群体使用不同部分? | 按用户边界拆分 |
|
||||
| Godoc 页面过于庞大? | 拆分以提高可发现性 |
|
||||
|
||||
**不要拆分**的原因仅仅是文件很长、创建只有单一类型的包,或会产生循环依赖。
|
||||
|
||||
> 在决定是否拆分或合并包、组织包内文件或构建 CLI 程序时,阅读 [references/PACKAGE-SIZE.md](references/PACKAGE-SIZE.md)。
|
||||
|
||||
---
|
||||
|
||||
## Import
|
||||
|
||||
Import 按组组织,组之间用空行分隔。标准库包始终放在第一组。使用
|
||||
[goimports](https://pkg.go.dev/golang.org/x/tools/cmd/goimports) 自动管理。
|
||||
|
||||
```go
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/foo/bar"
|
||||
"rsc.io/goversion/version"
|
||||
)
|
||||
```
|
||||
|
||||
**快速规则:**
|
||||
|
||||
| 规则 | 指导 |
|
||||
|------|------|
|
||||
| 分组 | 标准库优先,然后是外部包。扩展分组:标准库 → 其他 → proto → 副作用 |
|
||||
| 重命名 | 除非冲突,否则避免重命名。重命名最本地的 import。Proto 包加 `pb` 后缀 |
|
||||
| 空白 import(`import _`) | 仅在 `main` 包或测试中使用 |
|
||||
| 点 import(`import .`) | 永不使用,除非用于循环依赖的测试文件 |
|
||||
|
||||
> 在组织扩展分组的 import、重命名 proto 包或决定使用空白/点 import 时,阅读 [references/IMPORTS.md](references/IMPORTS.md)。
|
||||
|
||||
---
|
||||
|
||||
## 避免 init()
|
||||
|
||||
尽可能避免 `init()`。当不可避免时,它必须是:
|
||||
|
||||
1. 完全确定性的
|
||||
2. 不依赖于其他 `init()` 的执行顺序
|
||||
3. 不依赖环境状态(环境变量、工作目录、参数)
|
||||
4. 不进行 I/O(文件系统、网络、系统调用)
|
||||
|
||||
**可接受的使用场景**:无法用单个赋值完成的复杂表达式、可插拔钩子(例如 `database/sql` 方言)、确定性预计算。
|
||||
|
||||
> 在需要将 init() 重构为显式函数或理解可接受的 init() 使用场景时,阅读 [references/PACKAGE-SIZE.md](references/PACKAGE-SIZE.md)。
|
||||
|
||||
---
|
||||
|
||||
## Main 中的退出
|
||||
|
||||
仅在 `main()` 中调用 `os.Exit` 或 `log.Fatal*`。所有其他函数应返回 error。
|
||||
|
||||
**原因**:不明显的控制流、不可测试、`defer` 语句被跳过。
|
||||
|
||||
**最佳实践**:使用 `run()` 模式——将逻辑提取到
|
||||
`func run() error` 中,在 `main()` 中调用并使用单一退出点:
|
||||
|
||||
```go
|
||||
func main() {
|
||||
if err := run(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> 在实现 run() 模式、构建 CLI 子命令或选择 flag 命名约定时,阅读 [references/PACKAGE-SIZE.md](references/PACKAGE-SIZE.md)。
|
||||
|
||||
---
|
||||
|
||||
## 命令行 Flag
|
||||
|
||||
> **建议**:仅在 `package main` 中定义 flag。
|
||||
|
||||
- Flag 名称使用 `snake_case`:`--output_dir` 而非 `--outputDir`
|
||||
- 库应通过参数接收配置,而非直接读取 flag——
|
||||
这使它们可测试且可复用
|
||||
- 优先使用标准 `flag` 包;仅在需要 POSIX 约定
|
||||
(双破折号、单字符快捷方式)时使用 `pflag`
|
||||
|
||||
```go
|
||||
// 好:Flag 在 main 中定义,作为参数传递给库
|
||||
func main() {
|
||||
outputDir := flag.String("output_dir", ".", "directory for output files")
|
||||
flag.Parse()
|
||||
if err := mylib.Generate(*outputDir); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 相关技能
|
||||
|
||||
- **包命名**:在选择包名、避免名称重复或命名导出符号时,参见 [go-naming](../go-naming/SKILL.md)
|
||||
- **跨包的错误处理**:在使用 `%w` vs `%v` 在包边界包装错误时,参见 [go-error-handling](../go-error-handling/SKILL.md)
|
||||
- **Import linting**:在配置 goimports local-prefixes 或强制执行 import 分组时,参见 [go-linting](../go-linting/SKILL.md)
|
||||
- **全局状态**:在用显式初始化替换 `init()` 或避免可变全局变量时,参见 [go-defensive](../go-defensive/SKILL.md)
|
||||
@@ -0,0 +1,110 @@
|
||||
# Import 组织
|
||||
|
||||
Go import 组织的详细规则和示例。
|
||||
|
||||
## Import 分组
|
||||
|
||||
Import 按组组织,组之间用空行分隔。标准库包始终放在第一组。
|
||||
|
||||
**最小分组(Uber):** 标准库,然后其他所有。
|
||||
|
||||
**扩展分组(Google):** 标准库 → 其他 → protocol buffers → 副作用。
|
||||
|
||||
```go
|
||||
// 好:标准库与外部包分开
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"go.uber.org/atomic"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
```
|
||||
|
||||
```go
|
||||
// 好:完整分组,包含 proto 和副作用
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/dsnet/compress/flate"
|
||||
"golang.org/x/text/encoding"
|
||||
|
||||
foopb "myproj/foo/proto/proto"
|
||||
|
||||
_ "myproj/rpc/protocols/dial"
|
||||
)
|
||||
```
|
||||
|
||||
## Import 重命名
|
||||
|
||||
避免重命名 import,除非为了避免名称冲突;好的包名不需要重命名。
|
||||
在发生冲突时,**优先重命名最本地的或项目特定的 import**。
|
||||
|
||||
**必须重命名:** 与其他 import 冲突、生成的 protocol buffer 包
|
||||
(删除下划线,添加 `pb` 后缀)。
|
||||
|
||||
**可以重命名:** 无意义的名称(例如 `v1`)、与本地变量冲突。
|
||||
|
||||
```go
|
||||
// 好:Proto 包用 pb 后缀重命名
|
||||
import (
|
||||
foosvcpb "path/to/package/foo_service_go_proto"
|
||||
)
|
||||
|
||||
// 好:当需要 url 变量时使用 urlpkg
|
||||
import (
|
||||
urlpkg "net/url"
|
||||
)
|
||||
|
||||
func parseEndpoint(url string) (*urlpkg.URL, error) {
|
||||
return urlpkg.Parse(url)
|
||||
}
|
||||
```
|
||||
|
||||
## 空白 Import(`import _`)
|
||||
|
||||
仅为副作用而导入的包(使用 `import _ "pkg"`)
|
||||
应仅在程序的主包(main)或需要它们的测试中导入。
|
||||
|
||||
```go
|
||||
// 好:在主包中使用空白 import
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "time/tzdata"
|
||||
_ "image/jpeg"
|
||||
)
|
||||
```
|
||||
|
||||
## 点 Import(`import .`)
|
||||
|
||||
**不要**使用点 import。它们使程序难以阅读,因为不清楚
|
||||
`Quux` 这样的名称是当前包中的顶层标识符还是导入包中的。
|
||||
|
||||
**例外:** `import .` 形式在由于循环依赖而无法成为被测试包的一部分的测试文件中可能有用:
|
||||
|
||||
```go
|
||||
package foo_test
|
||||
|
||||
import (
|
||||
"bar/testutil" // 也导入了 "foo"
|
||||
. "foo"
|
||||
)
|
||||
```
|
||||
|
||||
在这种情况下,测试文件不能是 `foo` 包,因为它使用了
|
||||
`bar/testutil`,而后者导入了 `foo`。因此 `import .` 形式让文件
|
||||
假装是 `foo` 包的一部分,即使实际上不是。
|
||||
|
||||
**除了这一种情况外,不要在程序中使用 `import .`。**
|
||||
|
||||
```go
|
||||
// 不好:点 import 隐藏了来源
|
||||
import . "foo"
|
||||
var myThing = Bar() // Bar 来自哪里?
|
||||
|
||||
// 好:显式限定
|
||||
import "foo"
|
||||
var myThing = foo.Bar()
|
||||
```
|
||||
@@ -0,0 +1,214 @@
|
||||
# 包大小、程序结构和 CLI
|
||||
|
||||
关于包拆分、避免 init()、run() 模式和 CLI 结构的详细指南。
|
||||
|
||||
## 何时拆分包
|
||||
|
||||
```
|
||||
包是否变得太大?
|
||||
├─ 你能用一句话描述它的用途吗?
|
||||
│ ├─ 不能 → 按职责拆分
|
||||
│ └─ 能 → 保留,但检查以下内容
|
||||
├─ 包中的文件是否从未导入彼此的未导出符号?
|
||||
│ └─ 是 → 这些文件可以是独立的包
|
||||
├─ 包是否有不同的用户群体使用不同部分?
|
||||
│ └─ 是 → 按用户边界拆分
|
||||
└─ godoc 页面是否过于庞大?
|
||||
└─ 是 → 拆分以提高可发现性
|
||||
```
|
||||
|
||||
### 何时不应拆分
|
||||
|
||||
- 不要仅因为文件很长就拆分——聚焦的包中的大文件是可以的
|
||||
- 不要创建只包含一个类型或函数的包
|
||||
- 如果会产生循环依赖则不要拆分
|
||||
- 避免将内部辅助工具拆分到 `util` 或 `internal/helpers` 包中
|
||||
|
||||
### 何时合并包
|
||||
|
||||
- 如果客户端代码很可能需要两个类型交互,保持它们在一起
|
||||
- 如果类型有紧密耦合的实现
|
||||
- 如果用户需要同时导入两个包才能有意义地使用其中任何一个
|
||||
|
||||
### 文件组织
|
||||
|
||||
Go 中没有"一个类型一个文件"的惯例。文件应该足够聚焦以便知道哪个文件包含什么内容,且足够小以便轻松查找。
|
||||
|
||||
---
|
||||
|
||||
## 避免 init()
|
||||
|
||||
优先使用显式函数而非 `init()`:
|
||||
|
||||
```go
|
||||
// 不好:init() 带有 I/O 和环境依赖
|
||||
var _config Config
|
||||
|
||||
func init() {
|
||||
cwd, _ := os.Getwd()
|
||||
raw, _ := os.ReadFile(path.Join(cwd, "config.yaml"))
|
||||
yaml.Unmarshal(raw, &_config)
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
// 好:用于加载配置的显式函数
|
||||
func loadConfig() (Config, error) {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
raw, err := os.ReadFile(path.Join(cwd, "config.yaml"))
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
var config Config
|
||||
if err := yaml.Unmarshal(raw, &config); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
```
|
||||
|
||||
**init() 的可接受使用场景:**
|
||||
- 无法用单个赋值完成的复杂表达式
|
||||
- 可插拔钩子(例如 `database/sql` 方言、编码注册表)
|
||||
- 确定性预计算
|
||||
|
||||
---
|
||||
|
||||
## Main 中的退出
|
||||
|
||||
仅在 `main()` 中调用 `os.Exit` 或 `log.Fatal*`。所有其他函数应
|
||||
返回 error 来表示失败。
|
||||
|
||||
**为什么这很重要:**
|
||||
- 不明显的控制流:任何函数都可以退出程序
|
||||
- 难以测试:退出程序的函数也会退出测试
|
||||
- 跳过的清理:`defer` 语句会被跳过
|
||||
|
||||
```go
|
||||
// 不好:在辅助函数中使用 log.Fatal
|
||||
func readFile(path string) string {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
log.Fatal(err) // 退出程序,跳过 defer
|
||||
}
|
||||
b, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
// 好:返回 error,让 main() 决定是否退出
|
||||
func main() {
|
||||
body, err := readFile(path)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println(body)
|
||||
}
|
||||
|
||||
func readFile(path string) (string, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
b, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
```
|
||||
|
||||
### run() 模式
|
||||
|
||||
优先在 `main()` 中**最多调用一次** `os.Exit` 或 `log.Fatal`。将
|
||||
业务逻辑提取到返回 error 的独立函数中。
|
||||
|
||||
```go
|
||||
func main() {
|
||||
if err := run(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func run() error {
|
||||
args := os.Args[1:]
|
||||
if len(args) != 1 {
|
||||
return errors.New("missing file")
|
||||
}
|
||||
|
||||
f, err := os.Open(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close() // 将始终执行
|
||||
|
||||
b, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 处理 b...
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
**`run()` 模式的优势:**
|
||||
- 简短的 `main()` 函数,单一退出点
|
||||
- 所有业务逻辑都可测试
|
||||
- `defer` 语句始终执行
|
||||
|
||||
---
|
||||
|
||||
## 命令行接口
|
||||
|
||||
### Flag 命名
|
||||
|
||||
使用小写、连字符分隔的 flag 名称:
|
||||
|
||||
```go
|
||||
// 好
|
||||
flag.String("output-dir", ".", "directory for output files")
|
||||
flag.Bool("dry-run", false, "print actions without executing")
|
||||
|
||||
// 不好
|
||||
flag.String("outputDir", ".", "") // camelCase
|
||||
flag.String("output_dir", ".", "") // 下划线
|
||||
```
|
||||
|
||||
### 子命令
|
||||
|
||||
对于带有子命令的复杂 CLI,为每个子命令使用 `flag.NewFlagSet`:
|
||||
|
||||
```go
|
||||
func main() {
|
||||
serveCmd := flag.NewFlagSet("serve", flag.ExitOnError)
|
||||
port := serveCmd.Int("port", 8080, "listen port")
|
||||
|
||||
migrateCmd := flag.NewFlagSet("migrate", flag.ExitOnError)
|
||||
dryRun := migrateCmd.Bool("dry-run", false, "preview changes")
|
||||
|
||||
switch os.Args[1] {
|
||||
case "serve":
|
||||
serveCmd.Parse(os.Args[2:])
|
||||
runServe(*port)
|
||||
case "migrate":
|
||||
migrateCmd.Parse(os.Args[2:])
|
||||
runMigrate(*dryRun)
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unknown command: %s\n", os.Args[1])
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
对于更大的 CLI,考虑使用 `cobra` 或 `urfave/cli` 等库。仅从
|
||||
`main()` 退出。
|
||||
@@ -0,0 +1,152 @@
|
||||
---
|
||||
name: go-performance
|
||||
description: Use when optimizing Go code, investigating slow performance, or writing performance-critical sections. Also use when a user mentions slow Go code, string concatenation in loops, or asks about benchmarking, even if the user doesn't explicitly mention performance patterns. Does not cover concurrent performance patterns (see go-concurrency).
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
sources: "Uber Style Guide, Google Style Guide, Go Wiki CodeReviewComments"
|
||||
allowed-tools: Bash(bash:*)
|
||||
---
|
||||
|
||||
# Go 性能模式
|
||||
|
||||
## 可用脚本
|
||||
|
||||
- **`scripts/bench-compare.sh`** — 运行 Go 基准测试 N 次,并可选通过 benchstat 进行基线比较。支持保存结果以供未来比较。运行 `bash scripts/bench-compare.sh --help` 查看选项。
|
||||
|
||||
性能特定的指南仅适用于**热点路径**。不要过早优化——将这些模式集中在最重要的地方。
|
||||
|
||||
---
|
||||
|
||||
## 优先使用 strconv 而非 fmt
|
||||
|
||||
在基本类型和字符串之间转换时,`strconv` 比 `fmt` 更快:
|
||||
|
||||
```go
|
||||
s := strconv.Itoa(rand.Int()) // 比 fmt.Sprint() 快约 2 倍
|
||||
```
|
||||
|
||||
| 方式 | 速度 | 分配次数 |
|
||||
|------|------|---------|
|
||||
| `fmt.Sprint` | 143 ns/op | 2 allocs/op |
|
||||
| `strconv.Itoa` | 64.2 ns/op | 1 allocs/op |
|
||||
|
||||
> 在 strconv 和 fmt 之间选择类型转换方式时,或需要完整的转换对照表时,阅读 [references/STRING-OPTIMIZATION.md](references/STRING-OPTIMIZATION.md)。
|
||||
|
||||
---
|
||||
|
||||
## 避免重复的字符串到字节转换
|
||||
|
||||
将固定字符串在循环外转换为 `[]byte` 一次:
|
||||
|
||||
```go
|
||||
data := []byte("Hello world")
|
||||
for i := 0; i < b.N; i++ {
|
||||
w.Write(data) // 比每次迭代 []byte("...") 快约 7 倍
|
||||
}
|
||||
```
|
||||
|
||||
> 在优化热点循环中的重复字节转换时,阅读 [references/STRING-OPTIMIZATION.md](references/STRING-OPTIMIZATION.md)。
|
||||
|
||||
---
|
||||
|
||||
## 优先指定容器容量
|
||||
|
||||
尽可能指定容器容量,以便预先分配内存。这可以最大程度减少后续添加元素时因复制和调整大小而产生的分配。
|
||||
|
||||
### Map 容量提示
|
||||
|
||||
使用 `make()` 初始化 map 时提供容量提示:
|
||||
|
||||
```go
|
||||
m := make(map[string]os.DirEntry, len(files))
|
||||
```
|
||||
|
||||
**注意**:与 slice 不同,map 的容量提示不保证完整的预分配——它只是近似计算所需的哈希桶数量。
|
||||
|
||||
### Slice 容量
|
||||
|
||||
使用 `make()` 初始化 slice 时提供容量提示,特别是在追加时:
|
||||
|
||||
```go
|
||||
data := make([]int, 0, size)
|
||||
```
|
||||
|
||||
与 map 不同,slice 容量**不是提示**——编译器会精确分配那么多内存。后续的 `append()` 操作在达到容量之前不会产生任何分配。
|
||||
|
||||
| 方式 | 时间(1 亿次迭代) |
|
||||
|------|------------------------|
|
||||
| 无容量 | 2.48s |
|
||||
| 指定容量 | 0.21s |
|
||||
|
||||
指定容量的版本**快约 12 倍**,因为追加期间零重新分配。
|
||||
|
||||
---
|
||||
|
||||
## 传值
|
||||
|
||||
不要仅为了节省几个字节就将指针作为函数参数传递。如果函数在整个函数体中仅通过 `*x` 引用其参数 `x`,则该参数不应该是`指针。
|
||||
|
||||
```go
|
||||
func process(s string) { // 不是 *string —— string 是小的固定大小头部
|
||||
fmt.Println(s)
|
||||
}
|
||||
```
|
||||
|
||||
**常见的按值传递类型**:`string`、`io.Reader`、小结构体。
|
||||
|
||||
**例外**:
|
||||
- 复制代价高的大结构体
|
||||
- 未来可能增长的小结构体
|
||||
|
||||
---
|
||||
|
||||
## 字符串拼接
|
||||
|
||||
根据复杂度选择正确的策略:
|
||||
|
||||
| 方法 | 最佳用途 |
|
||||
|------|---------|
|
||||
| `+` | 少量字符串,简单拼接 |
|
||||
| `fmt.Sprintf` | 混合类型的格式化输出 |
|
||||
| `strings.Builder` | 循环/逐段构建 |
|
||||
| `strings.Join` | 连接 slice |
|
||||
| 反引号字面量 | 常量多行文本 |
|
||||
|
||||
> 在选择字符串拼接策略、在循环中使用 strings.Builder 或在 fmt.Sprintf 和手动拼接之间做决定时,阅读 [references/STRING-OPTIMIZATION.md](references/STRING-OPTIMIZATION.md)。
|
||||
|
||||
---
|
||||
|
||||
## 基准测试和性能分析
|
||||
|
||||
在优化前后始终要进行测量。使用 Go 内置的基准测试框架和性能分析工具。
|
||||
|
||||
```bash
|
||||
go test -bench=. -benchmem -count=10 ./...
|
||||
```
|
||||
|
||||
> 在编写基准测试、使用 benchstat 比较结果、使用 pprof 进行性能分析或解读基准测试输出时,阅读 [references/BENCHMARKS.md](references/BENCHMARKS.md)。
|
||||
|
||||
> **验证**:在应用优化后,运行 `bash scripts/bench-compare.sh` 测量实际影响。只保留有可衡量改进的优化。
|
||||
|
||||
---
|
||||
|
||||
## 快速参考
|
||||
|
||||
| 模式 | 不好 | 好 | 改进 |
|
||||
|------|-----|------|-------------|
|
||||
| 整数转字符串 | `fmt.Sprint(n)` | `strconv.Itoa(n)` | 快约 2 倍 |
|
||||
| 重复 `[]byte` | 循环中 `[]byte("str")` | 在循环外转换一次 | 快约 7 倍 |
|
||||
| Map 初始化 | `make(map[K]V)` | `make(map[K]V, size)` | 更少分配 |
|
||||
| Slice 初始化 | `make([]T, 0)` | `make([]T, 0, cap)` | 快约 12 倍 |
|
||||
| 小型固定大小参数 | `*string`、`*io.Reader` | `string`、`io.Reader` | 无间接引用 |
|
||||
| 简单字符串连接 | `s1 + " " + s2` | (已经很好) | 对少量字符串使用 `+` |
|
||||
| 循环构建字符串 | 重复 `+=` | `strings.Builder` | O(n) vs O(n²) |
|
||||
|
||||
---
|
||||
|
||||
## 相关技能
|
||||
|
||||
- **数据结构**:在 slice、map 和数组之间选择或理解分配语义时,参见 [go-data-structures](../go-data-structures/SKILL.md)
|
||||
- **声明模式**:在使用 `make` 配合容量提示或初始化 map 和 slice 时,参见 [go-declarations](../go-declarations/SKILL.md)
|
||||
- **并发**:在跨 goroutine 并行化工作或使用 sync.Pool 复用缓冲区时,参见 [go-concurrency](../go-concurrency/SKILL.md)
|
||||
- **风格原则**:在判断优化是否值得牺牲可读性时,参见 [go-style-core](../go-style-core/SKILL.md)
|
||||
@@ -0,0 +1,281 @@
|
||||
# 基准测试方法
|
||||
|
||||
## 编写基准测试
|
||||
|
||||
Go 基准测试使用 `testing.B` 类型,位于 `_test.go` 文件中。
|
||||
基准测试函数名必须以 `Benchmark` 开头。
|
||||
|
||||
```go
|
||||
func BenchmarkStrconv(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
s := strconv.Itoa(rand.Int())
|
||||
_ = s
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFmtSprint(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
s := fmt.Sprint(rand.Int())
|
||||
_ = s
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
关键规则:
|
||||
- 使用 `b.N` 作为循环边界——框架会调整它以获得稳定的计时
|
||||
- 将结果赋值给变量(或 `_`),防止编译器优化掉调用
|
||||
- 在不需要测量的昂贵设置之后使用 `b.ResetTimer()`
|
||||
- 使用 `b.ReportAllocs()` 或 `-benchmem` 标志跟踪分配情况
|
||||
|
||||
### 子基准测试
|
||||
|
||||
```go
|
||||
func BenchmarkConvert(b *testing.B) {
|
||||
for _, size := range []int{10, 100, 1000} {
|
||||
b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) {
|
||||
data := make([]byte, size)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = string(data)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 运行基准测试
|
||||
|
||||
```bash
|
||||
# 运行包中的所有基准测试
|
||||
go test -bench=. ./...
|
||||
|
||||
# 运行特定基准测试并显示内存统计
|
||||
go test -bench=BenchmarkStrconv -benchmem ./...
|
||||
|
||||
# 多次运行以获得统计显著性
|
||||
go test -bench=. -benchmem -count=10 ./...
|
||||
```
|
||||
|
||||
`-benchmem` 标志报告每次操作的分配次数。`-count` 标志将每个基准测试运行 N 次以获得统计显著性。
|
||||
|
||||
---
|
||||
|
||||
## 解读结果
|
||||
|
||||
```
|
||||
BenchmarkStrconv-8 18705042 64.2 ns/op 16 B/op 1 allocs/op
|
||||
BenchmarkFmtSprint-8 8249536 143.0 ns/op 16 B/op 2 allocs/op
|
||||
```
|
||||
|
||||
| 字段 | 含义 |
|
||||
|------|------|
|
||||
| `-8` | GOMAXPROCS |
|
||||
| `18705042` | 迭代次数 |
|
||||
| `64.2 ns/op` | 每次操作时间 |
|
||||
| `16 B/op` | 每次操作分配的字节数 |
|
||||
| `1 allocs/op` | 每次操作的堆分配次数 |
|
||||
|
||||
---
|
||||
|
||||
## 使用 benchstat 进行比较
|
||||
|
||||
`benchstat` 对基准测试结果进行统计比较。安装它并将基准测试输出保存到文件:
|
||||
|
||||
```bash
|
||||
# 安装 benchstat
|
||||
go install golang.org/x/perf/cmd/benchstat@latest
|
||||
|
||||
# 运行基准测试并保存结果
|
||||
go test -bench=. -benchmem -count=10 ./... > old.txt
|
||||
|
||||
# 进行修改后再次运行
|
||||
go test -bench=. -benchmem -count=10 ./... > new.txt
|
||||
|
||||
# 比较结果
|
||||
benchstat old.txt new.txt
|
||||
```
|
||||
|
||||
### 解读 benchstat 输出
|
||||
|
||||
```
|
||||
name old time/op new time/op delta
|
||||
Strconv-8 64.2ns ± 2% 61.8ns ± 1% -3.74% (p=0.001 n=10+10)
|
||||
```
|
||||
|
||||
- **delta**:变化百分比(负数 = 更快)
|
||||
- **p-value**:统计显著性(p < 0.05 为显著)
|
||||
- **n**:使用的有效样本数量
|
||||
|
||||
提示:
|
||||
- 始终使用 `-count=10` 或更高以获得可靠结果
|
||||
- 小的 p 值确认变化是真实的,而非噪声
|
||||
- 如果 benchstat 显示 `~`(波浪号),则差异不具有统计显著性
|
||||
|
||||
---
|
||||
|
||||
## 来自性能模式的基准测试示例
|
||||
|
||||
### strconv vs fmt
|
||||
|
||||
| 方式 | 速度 | 分配次数 |
|
||||
|------|------|---------|
|
||||
| `fmt.Sprint` | 143 ns/op | 2 allocs/op |
|
||||
| `strconv.Itoa` | 64.2 ns/op | 1 allocs/op |
|
||||
|
||||
### 重复字节转换
|
||||
|
||||
```go
|
||||
func BenchmarkRepeatedConversion(b *testing.B) {
|
||||
var buf bytes.Buffer
|
||||
for i := 0; i < b.N; i++ {
|
||||
buf.Write([]byte("Hello world"))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSingleConversion(b *testing.B) {
|
||||
var buf bytes.Buffer
|
||||
data := []byte("Hello world")
|
||||
for i := 0; i < b.N; i++ {
|
||||
buf.Write(data)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 方式 | 速度 |
|
||||
|------|------|
|
||||
| 重复转换 | 22.2 ns/op |
|
||||
| 单次转换 | 3.25 ns/op |
|
||||
|
||||
### Slice 容量
|
||||
|
||||
```go
|
||||
func BenchmarkNoCapacity(b *testing.B) {
|
||||
for n := 0; n < b.N; n++ {
|
||||
data := make([]int, 0)
|
||||
for k := 0; k < 1000; k++ {
|
||||
data = append(data, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkWithCapacity(b *testing.B) {
|
||||
for n := 0; n < b.N; n++ {
|
||||
data := make([]int, 0, 1000)
|
||||
for k := 0; k < 1000; k++ {
|
||||
data = append(data, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 方式 | 时间(1 亿次迭代) |
|
||||
|------|------------------------|
|
||||
| 无容量 | 2.48s |
|
||||
| 指定容量 | 0.21s |
|
||||
|
||||
---
|
||||
|
||||
## 使用 pprof 进行性能分析
|
||||
|
||||
使用 `pprof` 在优化前识别瓶颈。基准测试衡量改进效果;pprof 找到需要改进的地方。
|
||||
|
||||
### CPU 性能分析
|
||||
|
||||
```bash
|
||||
# 从基准测试生成 CPU 分析文件
|
||||
go test -bench=BenchmarkHotPath -cpuprofile=cpu.prof ./...
|
||||
|
||||
# 使用 pprof 分析
|
||||
go tool pprof cpu.prof
|
||||
```
|
||||
|
||||
常用 pprof 命令:
|
||||
|
||||
```
|
||||
(pprof) top10 # 按 CPU 时间排列的前 10 个函数
|
||||
(pprof) list funcName # 某个函数的带注释源码
|
||||
(pprof) web # 浏览器中的交互式图表
|
||||
```
|
||||
|
||||
### 内存性能分析
|
||||
|
||||
```bash
|
||||
# 生成内存分析文件
|
||||
go test -bench=BenchmarkHotPath -memprofile=mem.prof ./...
|
||||
|
||||
# 分析分配情况
|
||||
go tool pprof -alloc_space mem.prof
|
||||
```
|
||||
|
||||
### 运行中服务的 HTTP 性能分析
|
||||
|
||||
```go
|
||||
import _ "net/http/pprof"
|
||||
|
||||
func main() {
|
||||
go func() {
|
||||
log.Println(http.ListenAndServe("localhost:6060", nil))
|
||||
}()
|
||||
// ... 应用程序代码 ...
|
||||
}
|
||||
```
|
||||
|
||||
通过 `http://localhost:6060/debug/pprof/` 访问性能分析数据。
|
||||
|
||||
### 性能分析工作流
|
||||
|
||||
1. 对疑似热点路径进行**基准测试**
|
||||
2. 使用 pprof **分析**以确认时间花在了哪里
|
||||
3. 使用本技能中的模式进行**优化**
|
||||
4. **重新基准测试**以用 benchstat 验证改进
|
||||
5. **重新分析**以检查是否出现新的瓶颈
|
||||
|
||||
---
|
||||
|
||||
## 常见错误
|
||||
|
||||
### 忽略 b.N
|
||||
|
||||
测试框架会调整 `b.N` 以获得稳定的计时。使用固定迭代次数会产生无意义的结果:
|
||||
|
||||
```go
|
||||
// 不好:忽略 b.N —— 基准测试框架无法校准
|
||||
func BenchmarkFixed(b *testing.B) {
|
||||
for i := 0; i < 1000; i++ {
|
||||
doWork()
|
||||
}
|
||||
}
|
||||
|
||||
// 好:使用 b.N 作为循环边界
|
||||
func BenchmarkCorrect(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
doWork()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 未防止编译器优化消除
|
||||
|
||||
如果函数调用的结果未被使用,编译器可能会完全优化掉该调用。将结果赋值给包级变量:
|
||||
|
||||
```go
|
||||
// 不好:编译器可能会优化掉调用
|
||||
func BenchmarkElided(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
expensiveFunc()
|
||||
}
|
||||
}
|
||||
|
||||
// 好:赋值给包级变量以防止优化消除
|
||||
var benchResult int
|
||||
|
||||
func BenchmarkKept(b *testing.B) {
|
||||
var r int
|
||||
for i := 0; i < b.N; i++ {
|
||||
r = expensiveFunc()
|
||||
}
|
||||
benchResult = r
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,134 @@
|
||||
# 字符串优化模式
|
||||
|
||||
## strconv vs fmt
|
||||
|
||||
在基本类型和字符串之间转换时,`strconv` 比 `fmt` 更快,因为 `fmt` 使用反射并处理任意类型。
|
||||
|
||||
**不好:**
|
||||
|
||||
```go
|
||||
for i := 0; i < b.N; i++ {
|
||||
s := fmt.Sprint(rand.Int())
|
||||
}
|
||||
```
|
||||
|
||||
**好:**
|
||||
|
||||
```go
|
||||
for i := 0; i < b.N; i++ {
|
||||
s := strconv.Itoa(rand.Int())
|
||||
}
|
||||
```
|
||||
|
||||
**基准测试比较:**
|
||||
|
||||
| 方式 | 速度 | 分配次数 |
|
||||
|------|------|---------|
|
||||
| `fmt.Sprint` | 143 ns/op | 2 allocs/op |
|
||||
| `strconv.Itoa` | 64.2 ns/op | 1 allocs/op |
|
||||
|
||||
常用转换:
|
||||
|
||||
| 任务 | `fmt` | `strconv` |
|
||||
|------|-------|-----------|
|
||||
| Int → string | `fmt.Sprint(n)` | `strconv.Itoa(n)` |
|
||||
| Int64 → string | `fmt.Sprint(n)` | `strconv.FormatInt(n, 10)` |
|
||||
| Float → string | `fmt.Sprint(f)` | `strconv.FormatFloat(f, 'f', -1, 64)` |
|
||||
| String → int | — | `strconv.Atoi(s)` |
|
||||
| Bool → string | `fmt.Sprint(b)` | `strconv.FormatBool(b)` |
|
||||
|
||||
---
|
||||
|
||||
## 重复的字符串到字节转换
|
||||
|
||||
不要重复从固定字符串创建字节切片。应该只转换一次并保存结果。
|
||||
|
||||
**不好:**
|
||||
|
||||
```go
|
||||
for i := 0; i < b.N; i++ {
|
||||
w.Write([]byte("Hello world"))
|
||||
}
|
||||
```
|
||||
|
||||
**好:**
|
||||
|
||||
```go
|
||||
data := []byte("Hello world")
|
||||
for i := 0; i < b.N; i++ {
|
||||
w.Write(data)
|
||||
}
|
||||
```
|
||||
|
||||
**基准测试比较:**
|
||||
|
||||
| 方式 | 速度 |
|
||||
|------|------|
|
||||
| 重复转换 | 22.2 ns/op |
|
||||
| 单次转换 | 3.25 ns/op |
|
||||
|
||||
好的版本**快约 7 倍**,因为它避免了每次迭代都分配新的字节切片。
|
||||
|
||||
---
|
||||
|
||||
## 字符串拼接
|
||||
|
||||
根据复杂度选择正确的字符串构建策略。
|
||||
|
||||
### 简单场景使用 `+`
|
||||
|
||||
```go
|
||||
key := "projectid: " + p
|
||||
```
|
||||
|
||||
`+` 运算符对于少量、固定数量的字符串是高效的。编译器通常可以优化相邻的字符串字面量。
|
||||
|
||||
### 格式化使用 `fmt.Sprintf`
|
||||
|
||||
```go
|
||||
// 好:清晰的格式化
|
||||
str := fmt.Sprintf("%s [%s:%d]-> %s", src, qos, mtu, dst)
|
||||
|
||||
// 不好:使用 + 手动转换
|
||||
str := src.String() + " [" + qos.String() + ":" + strconv.Itoa(mtu) + "]-> " + dst.String()
|
||||
```
|
||||
|
||||
当写入 `io.Writer` 时,直接使用 `fmt.Fprintf` 而不是先用 `fmt.Sprintf` 构建临时字符串。
|
||||
|
||||
### 逐段构建使用 `strings.Builder`
|
||||
|
||||
`strings.Builder` 花费摊销线性时间,而重复使用 `+` 或
|
||||
`fmt.Sprintf` 在构建大字符串时花费二次时间:
|
||||
|
||||
```go
|
||||
b := new(strings.Builder)
|
||||
for i, d := range digitsOfPi {
|
||||
fmt.Fprintf(b, "the %d digit of pi is: %d\n", i, d)
|
||||
}
|
||||
str := b.String()
|
||||
```
|
||||
|
||||
### 常量多行字符串使用反引号
|
||||
|
||||
```go
|
||||
// 好:原始字符串字面量
|
||||
usage := `Usage:
|
||||
|
||||
custom_tool [args]`
|
||||
|
||||
// 不好:使用转义序列拼接
|
||||
usage := "" +
|
||||
"Usage:\n" +
|
||||
"\n" +
|
||||
"custom_tool [args]"
|
||||
```
|
||||
|
||||
### 策略总结
|
||||
|
||||
| 方法 | 最佳用途 | 性能 |
|
||||
|------|---------|------|
|
||||
| `+` | 少量字符串,简单拼接 | 小 n 时 O(n) |
|
||||
| `fmt.Sprintf` | 格式化输出 | 较慢,但更清晰 |
|
||||
| `strings.Builder` | 循环/逐段构建 | 摊销 O(n) |
|
||||
| `strings.Join` | 连接 slice | O(n) |
|
||||
| 反引号字面量 | 常量多行文本 | 零开销 |
|
||||
+252
@@ -0,0 +1,252 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="1.1.0"
|
||||
SCRIPT_NAME="$(basename "$0")"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
$SCRIPT_NAME v$VERSION — Run Go benchmarks with optional comparison
|
||||
|
||||
USAGE
|
||||
bash $SCRIPT_NAME [options] [package]
|
||||
|
||||
DESCRIPTION
|
||||
Wrapper around 'go test -bench' that runs benchmarks multiple times and
|
||||
optionally compares results against a saved baseline using benchstat.
|
||||
|
||||
Results can be saved to a file for future comparison. If benchstat is
|
||||
installed and a baseline is provided, a statistical comparison is shown.
|
||||
|
||||
EXIT CODES
|
||||
0 Benchmarks ran successfully
|
||||
1 go test failed (compilation error, test failure, no benchmarks found)
|
||||
2 Usage error (missing arguments, bad flags, file exists without --force)
|
||||
|
||||
OPTIONS
|
||||
-h, --help Show this help message
|
||||
-v, --version Show version
|
||||
-n, --count N Number of benchmark iterations (default: 5)
|
||||
-b, --baseline FILE Compare results against this baseline file
|
||||
-s, --save FILE Save benchmark results to this file
|
||||
-f, --filter REGEX Benchmark filter regex (default: ".")
|
||||
--json Output metadata as JSON (human output goes to stderr)
|
||||
--benchmem Include memory allocation stats (default: on)
|
||||
--no-benchmem Disable memory allocation stats
|
||||
--force Allow --save to overwrite existing files
|
||||
--limit N Max benchmark result lines to include (default: 0 = all)
|
||||
|
||||
ARGUMENTS
|
||||
package Go package to benchmark (default: ./...)
|
||||
|
||||
EXAMPLES
|
||||
bash $SCRIPT_NAME
|
||||
bash $SCRIPT_NAME -n 10 ./pkg/parser
|
||||
bash $SCRIPT_NAME --save baseline.txt ./...
|
||||
bash $SCRIPT_NAME --baseline baseline.txt --save current.txt ./...
|
||||
bash $SCRIPT_NAME --filter BenchmarkSort -n 3
|
||||
bash $SCRIPT_NAME --json --limit 5 ./...
|
||||
bash $SCRIPT_NAME --save results.txt --force ./...
|
||||
EOF
|
||||
}
|
||||
|
||||
json_escape() {
|
||||
local s="$1"
|
||||
s="${s//\\/\\\\}"
|
||||
s="${s//\"/\\\"}"
|
||||
s="${s//$'\t'/\\t}"
|
||||
s="${s//$'\r'/}"
|
||||
s="${s//$'\n'/\\n}"
|
||||
printf '%s' "$s"
|
||||
}
|
||||
|
||||
# Print human-readable output: stdout in text mode, stderr in JSON mode.
|
||||
log() {
|
||||
if $JSON_OUTPUT; then
|
||||
echo "$@" >&2
|
||||
else
|
||||
echo "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
COUNT=5
|
||||
BASELINE=""
|
||||
SAVE=""
|
||||
FILTER="."
|
||||
PACKAGE=""
|
||||
JSON_OUTPUT=false
|
||||
BENCHMEM=true
|
||||
FORCE=false
|
||||
LIMIT=0
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help) usage; exit 0 ;;
|
||||
-v|--version) echo "$SCRIPT_NAME v$VERSION"; exit 0 ;;
|
||||
-n|--count) COUNT="${2:?error: --count requires a number}"; shift 2 ;;
|
||||
-b|--baseline) BASELINE="${2:?error: --baseline requires a file path}"; shift 2 ;;
|
||||
-s|--save) SAVE="${2:?error: --save requires a file path}"; shift 2 ;;
|
||||
-f|--filter) FILTER="${2:?error: --filter requires a regex}"; shift 2 ;;
|
||||
--json) JSON_OUTPUT=true; shift ;;
|
||||
--benchmem) BENCHMEM=true; shift ;;
|
||||
--no-benchmem) BENCHMEM=false; shift ;;
|
||||
--force) FORCE=true; shift ;;
|
||||
--limit) LIMIT="${2:?error: --limit requires a number}"; shift 2 ;;
|
||||
-*) echo "error: unknown option: $1" >&2; usage >&2; exit 2 ;;
|
||||
*) PACKAGE="$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
PACKAGE="${PACKAGE:-./...}"
|
||||
|
||||
if ! command -v go &>/dev/null; then
|
||||
echo "error: 'go' command not found in PATH" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if ! [[ "$COUNT" =~ ^[1-9][0-9]*$ ]]; then
|
||||
echo "error: --count must be a positive integer, got: $COUNT" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if ! [[ "$LIMIT" =~ ^[0-9]+$ ]]; then
|
||||
echo "error: --limit must be a non-negative integer, got: $LIMIT" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ -n "$BASELINE" && ! -f "$BASELINE" ]]; then
|
||||
echo "error: baseline file not found: $BASELINE" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ -n "$SAVE" && -f "$SAVE" ]] && ! $FORCE; then
|
||||
echo "error: save target already exists: $SAVE (use --force to overwrite)" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
HAS_BENCHSTAT=false
|
||||
if command -v benchstat &>/dev/null; then
|
||||
HAS_BENCHSTAT=true
|
||||
fi
|
||||
|
||||
BENCH_ARGS=(-bench "$FILTER" -count "$COUNT" -run '^$')
|
||||
if $BENCHMEM; then
|
||||
BENCH_ARGS+=(-benchmem)
|
||||
fi
|
||||
|
||||
TMPFILE=$(mktemp "${TMPDIR:-/tmp}/bench-XXXXXX.txt")
|
||||
trap 'rm -f "$TMPFILE"' EXIT
|
||||
|
||||
log "Running benchmarks: go test ${BENCH_ARGS[*]} $PACKAGE"
|
||||
log "Iterations: $COUNT"
|
||||
log ""
|
||||
|
||||
GO_EXIT=0
|
||||
if $JSON_OUTPUT; then
|
||||
go test "${BENCH_ARGS[@]}" "$PACKAGE" 2>&1 | tee "$TMPFILE" >&2 || GO_EXIT=$?
|
||||
else
|
||||
go test "${BENCH_ARGS[@]}" "$PACKAGE" 2>&1 | tee "$TMPFILE" || GO_EXIT=$?
|
||||
fi
|
||||
|
||||
BENCH_COUNT=$(grep -cE '^Benchmark' "$TMPFILE" || true)
|
||||
|
||||
TRUNCATED=false
|
||||
if [[ $LIMIT -gt 0 && $BENCH_COUNT -gt $LIMIT ]]; then
|
||||
TRUNCATED=true
|
||||
fi
|
||||
|
||||
if ! $JSON_OUTPUT && $TRUNCATED; then
|
||||
log ""
|
||||
log "Note: $BENCH_COUNT benchmark results found, showing first $LIMIT (--limit $LIMIT)"
|
||||
fi
|
||||
|
||||
if [[ -n "$SAVE" ]]; then
|
||||
cp "$TMPFILE" "$SAVE"
|
||||
log ""
|
||||
log "Results saved to: $SAVE"
|
||||
fi
|
||||
|
||||
if [[ -n "$BASELINE" ]]; then
|
||||
log ""
|
||||
log "=== Comparison with baseline: $BASELINE ==="
|
||||
log ""
|
||||
if $HAS_BENCHSTAT; then
|
||||
if $JSON_OUTPUT; then
|
||||
benchstat "$BASELINE" "$TMPFILE" >&2 || true
|
||||
else
|
||||
benchstat "$BASELINE" "$TMPFILE" || true
|
||||
fi
|
||||
else
|
||||
log "note: install benchstat for statistical comparison:"
|
||||
log " go install golang.org/x/perf/cmd/benchstat@latest"
|
||||
log ""
|
||||
log "--- Baseline ---"
|
||||
if $JSON_OUTPUT; then
|
||||
grep -E '^Benchmark' "$BASELINE" >&2 || true
|
||||
else
|
||||
grep -E '^Benchmark' "$BASELINE" || true
|
||||
fi
|
||||
log ""
|
||||
log "--- Current ---"
|
||||
if $JSON_OUTPUT; then
|
||||
grep -E '^Benchmark' "$TMPFILE" >&2 || true
|
||||
else
|
||||
grep -E '^Benchmark' "$TMPFILE" || true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
FINAL_EXIT=0
|
||||
if [[ $GO_EXIT -ne 0 ]]; then
|
||||
FINAL_EXIT=1
|
||||
if ! $JSON_OUTPUT; then
|
||||
log ""
|
||||
log "error: go test exited with code $GO_EXIT"
|
||||
fi
|
||||
elif [[ $BENCH_COUNT -eq 0 ]]; then
|
||||
FINAL_EXIT=1
|
||||
if ! $JSON_OUTPUT; then
|
||||
log ""
|
||||
log "error: no benchmarks found matching filter: $FILTER"
|
||||
fi
|
||||
fi
|
||||
|
||||
if $JSON_OUTPUT; then
|
||||
BENCH_OUTPUT=$(<"$TMPFILE")
|
||||
if $TRUNCATED; then
|
||||
limited=""
|
||||
bench_seen=0
|
||||
while IFS= read -r line; do
|
||||
if [[ "$line" =~ ^Benchmark ]]; then
|
||||
bench_seen=$((bench_seen + 1))
|
||||
if [[ $bench_seen -le $LIMIT ]]; then
|
||||
limited+="$line"$'\n'
|
||||
fi
|
||||
else
|
||||
limited+="$line"$'\n'
|
||||
fi
|
||||
done < "$TMPFILE"
|
||||
BENCH_OUTPUT="$limited"
|
||||
fi
|
||||
|
||||
escaped_package=$(json_escape "$PACKAGE")
|
||||
escaped_filter=$(json_escape "$FILTER")
|
||||
escaped_baseline=$(json_escape "$BASELINE")
|
||||
escaped_save=$(json_escape "$SAVE")
|
||||
escaped_output=$(json_escape "$BENCH_OUTPUT")
|
||||
|
||||
printf '{"count":%d,' "$COUNT"
|
||||
printf '"package":"%s",' "$escaped_package"
|
||||
printf '"filter":"%s",' "$escaped_filter"
|
||||
printf '"benchmarks_found":%d,' "$BENCH_COUNT"
|
||||
printf '"baseline":"%s",' "$escaped_baseline"
|
||||
printf '"save":"%s",' "$escaped_save"
|
||||
printf '"exit_code":%d,' "$GO_EXIT"
|
||||
printf '"output":"%s"' "$escaped_output"
|
||||
if $TRUNCATED; then
|
||||
printf ',"truncated":true'
|
||||
fi
|
||||
printf '}\n'
|
||||
fi
|
||||
|
||||
exit $FINAL_EXIT
|
||||
@@ -0,0 +1,179 @@
|
||||
---
|
||||
name: go-style-core
|
||||
description: Use when working with Go formatting, line length, nesting, naked returns, semicolons, or core style principles. Also use when a style question isn't covered by a more specific skill, even if the user doesn't reference a specific style rule. Does not cover domain-specific patterns like error handling, naming, or testing (see specialized skills). Acts as fallback when no more specific style skill applies.
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
sources: "Effective Go, Google Style Guide, Uber Style Guide, Go Wiki CodeReviewComments"
|
||||
---
|
||||
|
||||
# Go 风格核心原则
|
||||
|
||||
## 风格原则(优先级顺序)
|
||||
|
||||
编写可读 Go 代码时,按以下重要性顺序应用这些原则:
|
||||
|
||||
### 优先级顺序
|
||||
|
||||
1. **清晰性** — 读者能否在没有额外上下文的情况下理解代码?
|
||||
2. **简洁性** — 这是否是实现目标的最简单方式?
|
||||
3. **精炼性** — 每一行是否都有其存在的价值?
|
||||
4. **可维护性** — 后续修改是否容易?
|
||||
5. **一致性** — 是否与周围代码和项目约定保持一致?
|
||||
|
||||
> 在解决清晰性、简洁性和精炼性之间的冲突时,或需要具体示例了解每个原则在实际 Go 代码中的应用时,请阅读 [references/PRINCIPLES.md](references/PRINCIPLES.md)。
|
||||
|
||||
---
|
||||
|
||||
## 格式化
|
||||
|
||||
运行 `gofmt` — 没有例外。**没有严格的行长度限制**,但 Uber 建议软限制为 99 个字符。按语义换行,而非按长度 — 选择重构而非仅仅换行。
|
||||
|
||||
> 在配置 gofmt、决定换行策略、应用 MixedCaps 规则或解决局部一致性问题时,请阅读 [references/FORMATTING.md](references/FORMATTING.md)。
|
||||
|
||||
---
|
||||
|
||||
## 减少嵌套
|
||||
|
||||
优先处理错误情况和特殊条件。提前返回或继续循环,使"正常路径"保持无缩进。
|
||||
|
||||
```go
|
||||
// 不好:深度嵌套
|
||||
for _, v := range data {
|
||||
if v.F1 == 1 {
|
||||
v = process(v)
|
||||
if err := v.Call(); err == nil {
|
||||
v.Send()
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Printf("Invalid v: %v", v)
|
||||
}
|
||||
}
|
||||
|
||||
// 好:扁平结构,提前返回
|
||||
for _, v := range data {
|
||||
if v.F1 != 1 {
|
||||
log.Printf("Invalid v: %v", v)
|
||||
continue
|
||||
}
|
||||
|
||||
v = process(v)
|
||||
if err := v.Call(); err != nil {
|
||||
return err
|
||||
}
|
||||
v.Send()
|
||||
}
|
||||
```
|
||||
|
||||
### 不必要的 Else
|
||||
|
||||
如果变量在 if 的两个分支中都被赋值,使用默认值 + 覆盖模式。
|
||||
|
||||
```go
|
||||
// 不好:在两个分支中都赋值
|
||||
var a int
|
||||
if b {
|
||||
a = 100
|
||||
} else {
|
||||
a = 10
|
||||
}
|
||||
|
||||
// 好:默认值 + 覆盖
|
||||
a := 10
|
||||
if b {
|
||||
a = 100
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 裸返回
|
||||
|
||||
没有参数的 `return` 语句会返回命名返回值。这被称为"裸"返回。
|
||||
|
||||
```go
|
||||
func split(sum int) (x, y int) {
|
||||
x = sum * 4 / 9
|
||||
y = sum - x
|
||||
return // 返回 x, y
|
||||
}
|
||||
```
|
||||
|
||||
### 裸返回的使用指南
|
||||
|
||||
- **在小型函数中可以使用**:裸返回在只有几行的函数中是没问题的
|
||||
- **在中大型函数中要明确**:一旦函数增长到中等大小,为了清晰起见应明确指定返回值
|
||||
- **不要仅为了裸返回而命名返回值**:文档的清晰性始终比节省一两行更重要
|
||||
|
||||
```go
|
||||
// 好:小型函数,裸返回很清晰
|
||||
func minMax(a, b int) (min, max int) {
|
||||
if a < b {
|
||||
min, max = a, b
|
||||
} else {
|
||||
min, max = b, a
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 好:较大的函数,显式返回
|
||||
func processData(data []byte) (result []byte, err error) {
|
||||
result = make([]byte, 0, len(data))
|
||||
|
||||
for _, b := range data {
|
||||
if b == 0 {
|
||||
return nil, errors.New("null byte in data")
|
||||
}
|
||||
result = append(result, transform(b))
|
||||
}
|
||||
|
||||
return result, nil // 显式返回:在较长的函数中更清晰
|
||||
}
|
||||
```
|
||||
|
||||
关于命名返回参数的指导,请参阅 **go-documentation**。
|
||||
|
||||
---
|
||||
|
||||
## 分号
|
||||
|
||||
Go 的词法分析器会在任何最后一个 token 是标识符、字面量或以下关键字之一的行后自动插入分号:`break continue fallthrough return ++ -- ) }`。
|
||||
|
||||
这意味着 **左花括号必须与控制结构在同一行**:
|
||||
|
||||
```go
|
||||
// 好:花括号在同一行
|
||||
if i < f() {
|
||||
g()
|
||||
}
|
||||
|
||||
// 不好:花括号在下一行 — 词法分析器会在 f() 后插入分号
|
||||
if i < f() // 错误!
|
||||
{ // 错误!
|
||||
g()
|
||||
}
|
||||
```
|
||||
|
||||
在惯用 Go 中,显式分号仅出现在 `for` 循环子句中和用于分隔单行上的多个语句。
|
||||
|
||||
---
|
||||
|
||||
## 快速参考
|
||||
|
||||
| 原则 | 核心问题 |
|
||||
|------|----------|
|
||||
| 清晰性 | 读者能否理解代码的意图和原因? |
|
||||
| 简洁性 | 这是否是最简单的方法? |
|
||||
| 精炼性 | 信噪比是否高? |
|
||||
| 可维护性 | 后续能否安全地修改? |
|
||||
| 一致性 | 是否与周围代码保持一致? |
|
||||
|
||||
## 相关 Skill
|
||||
|
||||
- **命名约定**:在应用 MixedCaps、选择标识符名称或解决命名争议时,请参阅 [go-naming](../go-naming/SKILL.md)
|
||||
- **错误流程**:在构建错误优先的守卫子句或通过提前返回减少嵌套时,请参阅 [go-error-handling](../go-error-handling/SKILL.md)
|
||||
- **文档**:在编写文档注释、命名返回参数或包级别文档时,请参阅 [go-documentation](../go-documentation/SKILL.md)
|
||||
- **Linting 执行**:在使用 golangci-lint 自动化风格检查或配置 CI 时,请参阅 [go-linting](../go-linting/SKILL.md)
|
||||
- **代码审查**:在系统性代码审查中应用风格原则时,请参阅 [go-code-review](../go-code-review/SKILL.md)
|
||||
- **日志风格**:在审查日志实践、在 log 和 slog 之间选择或组织日志输出时,请参阅 [go-logging](../go-logging/SKILL.md)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user