Files
OpenFlare/.agent/skills/go-packages/references/PACKAGE-SIZE.md
T
2026-06-18 15:24:48 +08:00

215 lines
4.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 包大小、程序结构和 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()` 退出。