Ch3nyang's blog collections_bookmark

post

data_object

github

category

category

local_offer

tag

rss_feed

rss

Go 项目组织

calendar_month 2025-06
archive 编程
tag go

前段时间读了微信技术团队的一篇文章(从微信后端仓库发展史谈谈单仓和多仓),讲微信是如何在一步步壮大的过程中,逐步从大仓中剥离公众号、小程序等功能到小仓中,从而解决了各种公共库全都塞在同一个文件夹下导致的耦合、权限等问题。

在这个过程中,微信团队总结了一些经验教训,包括提出了合理规划项目文件结构的重要性:如果一股脑地将所有代码按照功能划分到不同的文件夹中,可能会因为未来的需求,导致文件夹之间产生强耦合,从而影响到代码的可维护性和可扩展性。

看完此文,我想就此谈一谈我对大型项目目录组织的看法。正好,最近 Go 写得比较多,就先从 Go 的项目组织谈起。

不过,谈论 Go 的项目组织前,我不得不搬出 Go 语言作者 Russ CoxRuss Cox 的一则 comment:

There are two problems with this GitHub repo:

  1. it claims to host Go standards and does not, in the sense that these are in no way official standards
  2. the project-layout standard it puts forth is far too complex and not a standard

Regarding “why not tell us the standard Go project layout and we’ll update the doc?”, that only addresses point 2. If there really were standards, they would be in the main Go project doc tree. The standard for project layout would also be a lot shorter. I appreciate your trying to provide a useful resource, but calling it ‘golang-standards’ is claiming more than it is.

But for the record, the minimal standard layout for an importable Go repo is really:

  • Put a LICENSE file in your root
  • Put a go.mod file in your root
  • Put Go code in your repo, in the root or organized into a directory tree as you see fit

That’s it. That’s the “standard”.

In particular:

  • It is not required to put commands in cmd/.
  • It is not required to put packages in pkg/.
  • It is not required to put web stuff in web/.
  • It is not required to put APIs in api/.
  • It is not required to put web stuff in web/.
  • It is not required to put configurations in configs/.
  • It is not required to put systemd scripts in init/.
  • It is not required to put shell scripts in scripts/.
  • It is not required to put Docker files in build/package/.
  • It is not required to put CI configs in build/ci/.
  • It is not required to put deployment configs in deployments/.
  • It is not required to put test support in test/.
  • It is not required to put documentation in docs/.
  • It is not required to put supporting tools in tools/.
  • It is not required to put examples in examples/.
  • It is not required to put third_party code in third_party/.
  • It is not required to put git hooks in githooks/.
  • It is not required to put static assets in assets/.

The importable golang.org/x repos break every one of these “rules”.

可以看到,Go 项目并没有强制要求特定的目录结构,开发者可以根据实际情况灵活组织项目————事实上,如果观察流行的 Go 开源项目,同样也会发现它们的目录结构五花八门。

Go 官方文档 给出了一个基础的目录结构,不过看起来只适用于很小的项目;而 golang-standards/project-layoutgolang-standards/project-layout 给出了一个较为通用的项目结构示例,我觉得还不错:

.
├── cmd
│   └── myapp
│       └── main.go
├── internal
│   ├── myapp
│   │   └── app.go
│   └── mylib
│       └── lib.go
├── pkg
│   └── utils
│       └── helper.go
├── api
│   ├── myapp.proto
│   └── myapp.pb.go
├── web
│   ├── static
│   │   ├── css
│   │   ├── js
│   │   └── images
│   └── templates
│       └── index.html
├── build
│   ├── Dockerfile
│   └── Makefile
├── configs
│   ├── config.yaml
│   └── config.json
├── init
│   └── db.sql
├── scripts
│   ├── migrate.sh
│   └── deploy.sh
├── deploy
│   └── k8s
│       ├── deployment.yaml
│       └── service.yaml
├── test
│   ├── integration
│   └── testdata
├── docs
│   └── README.md
├── examples
│   └── simple
│       └── main.go
├── go.mod
├── go.sum
└── LICENSE

下面来详细解释各个目录的作用:

  • cmd

    cmd 目录通常用于存放应用程序的入口点。每个子目录对应一个可执行程序,通常包含一个 main.go 文件。

    例如,项目中有一个名为 myapp 的 RPC 应用,cmd/myapp/main.go 可能如下所示:

    package main
    
    import (
        "github.com/myorg/myapp/internal/myapp"
        "github.com/myorg/myapp/internal/mylib"
    )
    
    func main() {
        myapp.Start()
        mylib.Start()
    }
    

    不过,如果整个项目只有一个 main.go,那么放到根目录也无妨。

  • internal

    internal 目录用于存放不希望被外部引用的代码。Go 语言的包管理机制会阻止外部包引用 internal 目录下的内容。

    例如,对于前文所述项目,internal/myapp/*.go 存放有该应用的实现,可能如下所示:

    package myapp
    
    import "fmt"
    
    func Start() {
        fmt.Println("myapp started")
    }
    

    对于多个应用共用的包,也同样可以将其放在 internal 目录下的一个子目录中,例如 internal/mylib,然后在需要使用这些共享代码的应用中引用即可。

  • pkg

    pkg 目录用于存放可以被外部引用的代码。与 internal 目录不同,pkg 目录下的内容可以被其他项目引用。

    fatedier/frpfatedier/frp 为例:

    pkg
    ├── auth
    ├── config
    ├── errors
    ├── featuregate
    ├── metrics
    ├── msg
    └── ...
    

    可以看到,它是把本该属于 internal 的公共库代码提取到了 pkg 目录下。

    通常在两种情况下会使用 pkg 目录:

    1. 如果你的项目希望别人可以向其添加插件——而不用修改内部代码,那么将相关的功能抽象成 pkg 目录下的库是一个不错的选择。

    2. 别的项目需要使用当前项目中的库。例如,你的一个项目做了企业权限管理系统,这时候你又做了一个新的项目,需要用到这个权限管理系统的某些功能,这时就可以直接引用 pkg 目录下的库,而不需要复制粘贴代码。

    目前主流的 Go 项目似乎很少使用 pkg 目录,我看了一圈也只有 fatedier/frpfatedier/frp 采用了这种方式。

    此外,对于上述第二种情况,更好的做法是将共享的功能抽象成一个独立的库,与具体项目进行解耦。

    这里,我想引用 Russ CoxRuss Cox 的一则 Issue 来佐证以上内容:

For the initial development of module support, we’ve kept everything in internal directories to make it easier to make changes as our understanding of what the pieces should look like becomes clearer. But now I think the basic pieces have become pretty clear, and we should think about exporting some packages to help other people who want to write tools working directly with module mechanics.

  • api

    api 目录用于存放对外提供的接口定义,通常会包括:

    • OpenAPI / Swagger specs
    • JSON Schema
    • GraphQL Schema
    • Protobuf

    kubernetes/kuberneteskubernetes/kubernetes 为例:

    api
    ├── api-rules
    │   └── *.list
    ├── discovery
    │   └── *.json
    └── openapi-spec
        └── *.json
    

    对于前文所述项目,api/myapp.proto 存放有该应用的 gRPC 接口定义,可能如下所示:

    syntax = "proto3";
    
    package myapp;
    
    service MyApp {
        rpc Start(StartRequest) returns (StartResponse);
    }
    
    message StartRequest {
        string name = 1;
    }
    
    message StartResponse {
        string message = 1;
    }
    

    同时,也包括了对应的 api/myapp.pb.go 文件。

  • web

    web 目录用于存放与 Web 相关的代码,通常包括:

    • 静态文件(如 HTML、CSS、JavaScript)
    • 模板文件
    • Web 服务器相关代码

    prometheus/prometheusprometheus/prometheus 为例,其 web 目录中就包含了网页服务程序、API 接口、UI 组件等。

    对于前文所述项目,web/static 目录可能存放有静态文件,web/templates 目录可能存放有模板文件。

    这一目录不仅可以用在全栈项目中,对于当前较为流行的服务器端渲染(SSR)框架同样适用。

  • build

    build 目录用于存放与项目构建相关的文件,通常包括:

    • Dockerfile
    • CI/CD 配置文件
    • 打包脚本

    kubernetes/kuberneteskubernetes/kubernetes 为例,其 build 目录中就包含了大量的 .sh 脚本和 Dockerfile 文件。

  • config

    config 目录用于存放项目的配置文件。

    prometheus/prometheusprometheus/prometheus 为例,其 config 目录中就包含了 config.goreload.go 两个文件及对应的测试文件,用来处理配置文件的加载和重载。

  • init

    init 目录用于存放项目的初始化代码,通常包括:

    • 数据库初始化
    • 配置文件加载
    • 日志初始化
  • scripts

    scripts 目录用于存放项目的脚本文件,通常包括:

    • 数据库迁移脚本
    • 备份脚本
    • 部署脚本

    这里也会经常放一些供 Makefile 使用的脚本。

    gohugoio/hugogohugoio/hugo 为例,其 scripts 目录中包含了 Docker 需要使用的 entrypoint.sh 脚本。

  • deploy

    deploy 目录用于存放与项目部署相关的文件,通常包括:

    • K8s 配置文件
    • Docker Compose 文件

    有些地方也会用 deployments 目录。

    gohugoio/hugogohugoio/hugo 为例,其 deploy 目录中包含了 deploy.go 和部属用的配置文件。

  • test

    test 目录用于存放项目的测试代码。运行测试时,Go 会自动识别该目录下的测试文件并执行相应的测试用例。

    对于不需要 Go 运行的文件,可以放入 test/datatest/testdata 目录中,也可以在文件名开头加上 ._

    syncthing/syncthingsyncthing/syncthing 为例,其 test 目录中包含了各类测试文件及测试需要用到的辅助文件。

  • docs & examples

    docs 目录用于存放项目的文档(不是 go doc 生成的文档);examples 目录用于存放项目的示例代码。

    gin-gonic/gingin-gonic/gin 为例,其 docs 目录中包含了文档 doc.md;而 examples 目录中则……好吧,它们只留了一个 README.md 在里面,告诉你示例代码被移动到了一个单独的仓库中。

  • 其它

    其它目录可以根据项目需要自行定义,例如:

    • assets:用于存放静态资源,如图片、字体等
    • third_party:用于存放第三方依赖的代码和工具
    • vendor:用于存放项目的依赖包
    • tools:用于存放项目的开发工具
    • githooks:用于存放 Git 钩子脚本

以上便是一个较好的 Go 项目目录结构示例。

当然,具体的项目结构还需要根据实际情况进行调整和优化。比如,ollama/ollamaollama/ollama 作为一个 AI 项目,根目录下就多了诸如 llmlmmodelopenai 这样的子目录。

总之,合理的目录结构是良好架构的重要组成部分。当代码规模扩大时,它能够有效地降低模块之间的耦合度,提高代码的可维护性和可扩展性。

Comments

Share This Post