组织 Go 模块

Go 新手开发者常问的一个问题是:“我该如何组织我的 Go 项目?”,这涉及到文件和文件夹的布局。本文档旨在提供一些指导方针,以帮助回答这个问题。要充分利用本文档,请务必通过阅读教程管理模块源来熟悉 Go 模块的基础知识。

Go 项目可以包含包、命令行程序或两者的组合。本指南按项目类型组织。

基本包

一个基本的 Go 包将其所有代码放在项目的根目录中。项目由一个模块组成,该模块由一个包组成。包名与模块名的最后一个路径组件匹配。对于一个只需要一个 Go 文件的非常简单的包,项目结构如下:

project-root-directory/
  go.mod
  modname.go
  modname_test.go

[在本文档中,文件/包名是完全任意的]

假设此目录已上传到 GitHub 仓库 github.com/someuser/modname,则 go.mod 文件中的 module 行应为 module github.com/someuser/modname

modname.go 中的代码使用以下方式声明包:

package modname

// ... package code here

用户可以通过在他们的 Go 代码中 import 它来依赖此包,如下所示:

import "github.com/someuser/modname"

一个 Go 包可以拆分成多个文件,所有文件都位于同一个目录中,例如:

project-root-directory/
  go.mod
  modname.go
  modname_test.go
  auth.go
  auth_test.go
  hash.go
  hash_test.go

目录中的所有文件都声明 package modname

基本命令

一个基本的执行程序(或命令行工具)根据其复杂性和代码大小进行结构化。最简单的程序可以由一个定义了 func main 的单个 Go 文件组成。较大的程序可以将代码拆分到多个文件中,所有文件都声明 package main

project-root-directory/
  go.mod
  auth.go
  auth_test.go
  client.go
  main.go

这里 main.go 文件包含 func main,但这只是一个约定。“主”文件也可以命名为 modname.go(对于适当的 modname 值)或任何其他名称。

假设此目录已上传到 GitHub 仓库 github.com/someuser/modname,则 go.mod 文件中的 module 行应为:

module github.com/someuser/modname

用户应该能够通过以下方式将其安装到他们的机器上:

$ go install github.com/someuser/modname@latest

带有支持包的包或命令

较大的包或命令可能会受益于将某些功能拆分到支持包中。最初,建议将此类包放入名为 internal 的目录中;这可以防止其他模块依赖于我们不一定希望暴露并支持外部使用的包。由于其他项目无法从我们的 internal 目录导入代码,我们可以自由地重构其 API 并通常移动事物而不会破坏外部用户。因此,包的项目结构如下:

project-root-directory/
  internal/
    auth/
      auth.go
      auth_test.go
    hash/
      hash.go
      hash_test.go
  go.mod
  modname.go
  modname_test.go

modname.go 文件声明 package modnameauth.go 声明 package auth,依此类推。modname.go 可以按如下方式导入 auth 包:

import "github.com/someuser/modname/internal/auth"

internal 目录中包含支持包的命令的布局非常相似,只是根目录中的文件声明 package main

多个包

一个模块可以由多个可导入的包组成;每个包都有自己的目录,并且可以分层结构。这是一个示例项目结构:

project-root-directory/
  go.mod
  modname.go
  modname_test.go
  auth/
    auth.go
    auth_test.go
    token/
      token.go
      token_test.go
  hash/
    hash.go
  internal/
    trace/
      trace.go

提醒一下,我们假设 go.mod 中的 module 行是:

module github.com/someuser/modname

modname 包位于根目录中,声明 package modname,用户可以通过以下方式导入:

import "github.com/someuser/modname"

用户可以按如下方式导入子包:

import "github.com/someuser/modname/auth"
import "github.com/someuser/modname/auth/token"
import "github.com/someuser/modname/hash"

位于 internal/tracetrace 包无法在此模块之外导入。建议尽可能将包保留在 internal 中。

多个命令

同一仓库中的多个程序通常会有单独的目录:

project-root-directory/
  go.mod
  internal/
    ... shared internal packages
  prog1/
    main.go
  prog2/
    main.go

在每个目录中,程序的 Go 文件声明 package main。顶级 internal 目录可以包含仓库中所有命令使用的共享包。

用户可以按如下方式安装这些程序:

$ go install github.com/someuser/modname/prog1@latest
$ go install github.com/someuser/modname/prog2@latest

一个常见的约定是将仓库中的所有命令放入 cmd 目录;虽然在仅由命令组成的仓库中这并非严格必要,但在包含命令和可导入包的混合仓库中,这非常有用,我们将在下一节讨论。

同一仓库中的包和命令

有时,一个仓库会提供具有相关功能的可导入包和可安装命令。这是一个此类仓库的示例项目结构:

project-root-directory/
  go.mod
  modname.go
  modname_test.go
  auth/
    auth.go
    auth_test.go
  internal/
    ... internal packages
  cmd/
    prog1/
      main.go
    prog2/
      main.go

假设此模块名为 github.com/someuser/modname,用户现在可以从中导入包:

import "github.com/someuser/modname"
import "github.com/someuser/modname/auth"

并从中安装程序:

$ go install github.com/someuser/modname/cmd/prog1@latest
$ go install github.com/someuser/modname/cmd/prog2@latest

服务器项目

Go 是实现服务器的常用语言选择。鉴于服务器开发的许多方面:协议(REST?gRPC?)、部署、前端文件、容器化、脚本等,此类项目的结构差异很大。我们在此将重点放在项目中使用 Go 编写的部分。

服务器项目通常不会有用于导出的包,因为服务器通常是一个自包含的二进制文件(或一组二进制文件)。因此,建议将实现服务器逻辑的 Go 包保存在 internal 目录中。此外,由于项目可能有很多其他非 Go 文件的目录,因此将所有 Go 命令保存在 cmd 目录中是一个好主意:

project-root-directory/
  go.mod
  internal/
    auth/
      ...
    metrics/
      ...
    model/
      ...
  cmd/
    api-server/
      main.go
    metrics-analyzer/
      main.go
    ...
  ... the project's other directories with non-Go code

如果服务器仓库中包含的包变得对与其他项目共享有用,最好将这些包拆分到单独的模块中。