Files
OpenFlare/.agents/skills/go-packages/references/PACKAGE-SIZE.md
T
2026-08-04 11:40:32 +08:00

4.9 KiB
Raw Blame History

包大小、程序结构和 CLI

关于包拆分、避免 init()、run() 模式和 CLI 结构的详细指南。

何时拆分包

包是否变得太大?
├─ 你能用一句话描述它的用途吗?
│  ├─ 不能 → 按职责拆分
│  └─ 能 → 保留,但检查以下内容
├─ 包中的文件是否从未导入彼此的未导出符号?
│  └─ 是 → 这些文件可以是独立的包
├─ 包是否有不同的用户群体使用不同部分?
│  └─ 是 → 按用户边界拆分
└─ godoc 页面是否过于庞大?
   └─ 是 → 拆分以提高可发现性

何时不应拆分

  • 不要仅因为文件很长就拆分——聚焦的包中的大文件是可以的
  • 不要创建只包含一个类型或函数的包
  • 如果会产生循环依赖则不要拆分
  • 避免将内部辅助工具拆分到 util 或 internal/helpers 包中

何时合并包

  • 如果客户端代码很可能需要两个类型交互,保持它们在一起
  • 如果类型有紧密耦合的实现
  • 如果用户需要同时导入两个包才能有意义地使用其中任何一个

文件组织

Go 中没有"一个类型一个文件"的惯例。文件应该足够聚焦以便知道哪个文件包含什么内容,且足够小以便轻松查找。


避免 init()

优先使用显式函数而非 init():

// 不好:init() 带有 I/O 和环境依赖
var _config Config

func init() {
    cwd, _ := os.Getwd()
    raw, _ := os.ReadFile(path.Join(cwd, "config.yaml"))
    yaml.Unmarshal(raw, &_config)
}
// 好:用于加载配置的显式函数
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 语句会被跳过
// 不好:在辅助函数中使用 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)
}
// 好:返回 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 的独立函数中。

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 名称:

// 好
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:

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() 退出。