Golang项目结构最佳实践推荐
Golang项目结构最佳实践推荐 我正在使用 Fiber 框架开发一个项目,并且采用了我在 express.js 中使用的文件夹和文件结构: config - 用于数据库配置 controllers models - 用于数据库表定义 routes services - 用于与数据库交互 .
你能为我推荐一个 Golang/Fiber 的项目结构吗?学习这门语言惯用法的最佳方式是什么?
关于项目结构有很多混杂的建议,因为从未有过官方指南,但不久前Go团队发布了一份文档来解决这个问题:组织Go模块 - Go编程语言,因此你可以从这里开始。
要学习惯用法,一个好的起点是Effective Go - Go编程语言
更多关于Golang项目结构最佳实践推荐的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
你好,我也在基于 Fiber 开发一个 REST API 样板项目,其中包含预定义的模块,如用户、群组、待办事项等,并带有 HTMX 用户界面。希望这对你有所帮助。
人们使用各种项目结构。这相当主观。但说实话,我觉得这没那么重要。
我曾经对这类事情也有同样的疑问,什么是最好的项目结构,我应该把这段逻辑放在哪里,我应该做依赖注入吗,我的代码遵循SOLID原则吗,我的代码是否已经是整洁架构,我的代码干净吗,等等。我也曾在多家公司工作过,没有一家公司使用相同的结构。有些公司把实体放在一个特殊的目录里,有些公司把所有接口放在一个包里,有些公司为每个模块设置2个包,一个放接口,一个放实现。 但是,你知道吗?后来我意识到,所有这些都行得通。这没那么重要。它们都是可维护、可读、可测试、性能足够好且能工作的。在做个人项目时,我开始意识到我花了太多时间在初期构建代码结构,结果最后却做了大量重构。
所以,我的第一个建议是:不要过于纠结项目结构。试着看大局。项目结构、模式、最佳实践和架构的意义在于让你的代码能够工作、可维护、可测试、性能好、可扩展。你可以问自己,你当前的项目结构达到这些目标了吗?如果是,那么它就已经是最好的项目结构了。
这有点跑题,但我注意到关于“干净”代码的一点是,人们通常认为为了拥有“干净”的代码,你必须做A、B、C、D。但是,很少讨论“干净”是什么意思,以及A、B、C、D如何使其“干净”。如果我们做了A、B、C、D,但代码不可维护且难以更改,我们能说它“干净”吗?就我个人而言,如果代码可读、可维护、可测试、能工作、性能足够好,我就会说它是干净的。如果有人构建了一个包含单个10K行代码文件的项目,但它能工作、可以被理解、可以被维护、性能足够好,我会说它是干净的。
现在,我通常的做法是:
- 如果我使用特定的框架,并且该框架强制我使用特定的项目结构(或者不遵循它会很痛苦),我会遵循它。
- 如果我在一个团队中工作,我会遵循团队内的任何共识。
- 如果我在一个现有项目上工作,我会遵循该项目使用的任何结构。
- 如果我独自从头开始,我会把所有东西放在一个包里,直到我觉得必须有多个包为止。
我如何知道何时需要多个文件和包?
- 当维护上下文变得困难时。通过拥有不同的包,我可以在不同的上下文中使用相同的符号名称,因为包提供了上下文。
- 例如,认证上下文中的“用户”可能与结账上下文中的“用户”不同。在认证中,用户就只是用户。在结账上下文中,用户可能是卖家、买家或其他什么。我们在谈论认证和结账时,心智模型是不同的。将它们拆分成不同的包可能有助于我们的大脑在更小的上下文中工作。
- 当维护者规模变大时。我不希望人们在处理不同功能时编辑同一个文件。这会使解决冲突变得困难。
- 这是我不喜欢将我的包按角色(如
config、models、services)分离的原因之一。大多数更改会同时更改这三个部分。但如果我将其改为基于“功能”的划分,如auth、logistics、payments,如果人们处理不同的功能,他们很可能不会发生冲突。例如,处理重置密码流程的人不会影响处理新支付方式的人所更改的任何文件。
- 这是我不喜欢将我的包按角色(如
- 当编译时间变慢时。在Go中,一个包就是一个编译单元。
要知道,有很多成功的项目使用单个文件夹来存储大部分源代码,比如boltdb、sqlite、redis。嗯,反正也没有太多理由将它们拆分成多个包。
拥有多个包的另一个原因是当你在团队中工作时。一个目录可以用于某种管辖范围。例如,在auth目录中包含了理解项目认证所需的所有逻辑和知识。当有人想了解认证时,很明显他们应该去auth目录,而如果你有model、config和services这样的目录,就不清楚应该从哪里开始搜索。但再说一次,这也没那么重要,因为人们可以直接询问,或者使用他们的IDE搜索,这已经不那么困难了。
你也可以为特定目录设置特殊规则。例如,在github和gitlab上,你可以配置你的仓库,使得每次有人编辑payments目录内的任何文件时,某些人会被添加为审阅者,并且没有这些人的批准,PR就不能被合并。这再次说明了为什么我不喜欢将目录拆分为model、config和services,因为这些目录将被所有人拥有。但同样,这也不是什么大问题,总有解决办法。
在Go中还有一个特殊的目录叫internal。如果你有一个名为internal的目录,这个目录不能被其他模块或其他非其父级的包导入。这对于隐藏一些内部逻辑很有用。
对于Fiber项目,我推荐以下符合Go惯用法的项目结构:
// cmd/
// main.go
// internal/
// config/
// config.go
// handlers/
// user_handler.go
// models/
// user.go
// services/
// user_service.go
// repositories/
// user_repository.go
// routes/
// routes.go
// pkg/
// database/
// database.go
// middleware/
// auth.go
// utils/
// validator.go
具体实现示例:
// cmd/main.go
package main
import (
"github.com/yourproject/internal/config"
"github.com/yourproject/internal/routes"
"github.com/gofiber/fiber/v2"
)
func main() {
app := fiber.New()
// 初始化配置
cfg := config.Load()
// 设置路由
routes.Setup(app, cfg)
app.Listen(":3000")
}
// internal/config/config.go
package config
type Config struct {
DBHost string
DBPort string
DBUser string
DBPassword string
DBName string
}
func Load() *Config {
return &Config{
DBHost: "localhost",
DBPort: "5432",
DBUser: "postgres",
DBPassword: "password",
DBName: "mydb",
}
}
// internal/handlers/user_handler.go
package handlers
import (
"github.com/yourproject/internal/services"
"github.com/gofiber/fiber/v2"
)
type UserHandler struct {
userService services.UserService
}
func NewUserHandler(us services.UserService) *UserHandler {
return &UserHandler{userService: us}
}
func (h *UserHandler) GetUser(c *fiber.Ctx) error {
id := c.Params("id")
user, err := h.userService.GetUser(id)
if err != nil {
return c.Status(404).JSON(fiber.Map{"error": "User not found"})
}
return c.JSON(user)
}
// internal/services/user_service.go
package services
import (
"github.com/yourproject/internal/models"
"github.com/yourproject/internal/repositories"
)
type UserService interface {
GetUser(id string) (*models.User, error)
}
type userService struct {
userRepo repositories.UserRepository
}
func NewUserService(repo repositories.UserRepository) UserService {
return &userService{userRepo: repo}
}
func (s *userService) GetUser(id string) (*models.User, error) {
return s.userRepo.FindByID(id)
}
// internal/repositories/user_repository.go
package repositories
import (
"github.com/yourproject/internal/models"
"github.com/yourproject/pkg/database"
)
type UserRepository interface {
FindByID(id string) (*models.User, error)
}
type userRepository struct {
db *database.DB
}
func NewUserRepository(db *database.DB) UserRepository {
return &userRepository{db: db}
}
func (r *userRepository) FindByID(id string) (*models.User, error) {
var user models.User
err := r.db.Where("id = ?", id).First(&user).Error
return &user, err
}
// internal/routes/routes.go
package routes
import (
"github.com/yourproject/internal/config"
"github.com/yourproject/internal/handlers"
"github.com/yourproject/internal/repositories"
"github.com/yourproject/internal/services"
"github.com/yourproject/pkg/database"
"github.com/yourproject/pkg/middleware"
"github.com/gofiber/fiber/v2"
)
func Setup(app *fiber.App, cfg *config.Config) {
// 初始化数据库
db := database.Connect(cfg)
// 初始化依赖
userRepo := repositories.NewUserRepository(db)
userService := services.NewUserService(userRepo)
userHandler := handlers.NewUserHandler(userService)
// 路由分组
api := app.Group("/api")
api.Use(middleware.Logger())
// 用户路由
users := api.Group("/users")
users.Get("/:id", userHandler.GetUser)
}
这个结构遵循Go项目的标准布局:
cmd/:包含应用程序入口点internal/:私有应用程序代码,外部项目无法导入pkg/:可导出的库代码- 使用依赖注入实现松耦合
- 清晰的关注点分离(handler/service/repository)
学习Go惯用法的最佳方式是:
- 阅读官方文档和Effective Go
- 研究标准库的源代码
- 查看知名开源项目的结构(如Docker、Kubernetes、Prometheus)
- 遵循Go社区的约定和模式

