wavelet init

This commit is contained in:
ryan
2026-06-18 15:24:48 +08:00
parent d6a7011885
commit 99738bbc17
714 changed files with 139987 additions and 0 deletions
+138
View File
@@ -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()` 退出。