mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-02 23:06:36 +08:00
215 lines
4.9 KiB
Markdown
215 lines
4.9 KiB
Markdown
# 包大小、程序结构和 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()` 退出。
|