组织 Go 模块

对于 Go 新手开发人员来说,一个常见的问题是“如何组织我的 Go 项目?”,这是指文件和文件夹的布局。本文档的目的是提供一些指导原则,帮助回答这个问题。为了充分利用本文档,请确保您已通过阅读教程管理模块源熟悉 Go 模块的基础知识。

Go 项目可以包括软件包、命令行程序或两者结合。本指南按项目类型进行组织。

基本软件包

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

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

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

假设此目录已上传到 github.com/someuser/modname 的 GitHub 存储库,则 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,但这只是一个约定。还可以将“main”文件称为 modname.go(对于 modname 的适当值)或其他任何名称。

假设此目录已上传到 github.com/someuser/modname 的 GitHub 存储库,则 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/trace 中的包 trace 无法在该模块外部导入。建议尽可能将包保留在 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

如果服务器存储库不断增长,并且包变得对与其他项目共享有用,则最好将它们拆分为单独的模块。