Golang中如何设计API项目的结构
Golang中如何设计API项目的结构 我来自C#/.NET背景,由于该技术栈严重依赖命名空间/类和依赖注入,类似的项目结构方式在Go中可能并不直观。我想知道大家是如何构建API项目结构的。
我目前采用的结构大致如下,但由于我还没有部署过任何生产应用,所以这一切都显得非常业余。
- cmd/
- api/
- internal/
- api/
- handlers/
- db/
- models/
- crud/ // 使用GORM时不需要
- services/
- serviceA/
- serviceB/
- ...
- utils/
- docker-compose.yml
- Dockerfile
- Makefile
- go.mod
- go.sum
向服务层提供数据库访问的最佳方式是什么?我应该创建一个结构体,然后将db.DB或gorm.DB作为其字段吗?
更多关于Golang中如何设计API项目的结构的实战教程也可以访问 https://www.itying.com/category-94-b0.html
嗯,我的主要建议是:避免golang-standards组织及其中的建议。在我看来,这是一个很好的起点,但需要注意,你应该只在需要时才添加内容(就像我自己从来不会在开始一个新应用时,首先就去创建一堆文件夹)。我认为这篇关于包命名的文章非常出色,值得一读:
包命名 - Go编程语言
如何为你的包命名。
特别是这条建议,我尝试(以不同程度的成功)去遵循:
不要从用户那里窃取好的名称。 避免给包起一个在客户端代码中常用的名字。例如,缓冲I/O包被称为
bufio,而不是buf,因为buf是一个很好的缓冲区变量名。
还有另一篇好文章:
组织Go模块 - Go编程语言
好的——如果我吹毛求疵的话,这里有一些小的反馈:
- 我认为
/db/crud/可能更适合泛化为/db/queries,因为CRUD只意味着简单的(创建、读取、更新、删除)查询。在大多数项目中,你会有其他查询。 - 我认为将每个服务都作为自己的包可能有些过度,更像是.NET的做法。你的服务是做什么的?为什么它们是一个“服务”,而不是像
/internal/auth这样的东西?你的服务是像type AuthService这样的结构体吗?你是在管理生命周期(比如它是单例的、作用域限定在上下文中的,还是其他什么?)如果不是,我可能会说最好就用像/internal/auth这样的东西。 - 你可以争辩说
utils太通用了。不过,我假设你会在需要时在那里创建实用程序,它主要是“那些不太值得拥有自己的顶级文件夹或包的东西”的集合地。上面提到的官方命名指南说:“名为util、common或misc的包无法让客户端了解包中包含什么”。但是——我认为只要你对其中包含的内容做个说明(稍后会详细说明),这就没问题。
回到像 util 和文档这样的事情:我发现,即使一个包只在内部使用,提供顶级的包文档也是有帮助的。例如,如果你决定保留 util 这个名称,尽管它违反了官方的命名指南,你可以提供一个类似这样的理由:
/*
Package util 包含所有可能不值得拥有自己包的杂项内容。
它违反了官方的命名标准(#YOLO),但我们发现它很有用(无意双关)。
随着内容增长并证明其需要独立的包时,可以随时将其重构出此包。
关于为什么这可能被认为是一个糟糕的包名的进一步阅读:
https://go.dev/blog/package-names#bad-package-names
*/
package util
这类文档向未来的开发者表明:“是的,我确实考虑过这个包名,它存在是有原因的”。
总而言之:你做得非常好,如果我遇到一个像这样的项目结构,我会说它是符合Go语言习惯的,并且仅通过阅读你的项目结构我就能理解其意图。
更多关于Golang中如何设计API项目的结构的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
在Go中构建API项目结构时,通常遵循标准布局模式,但会根据具体需求调整。你的结构基本合理,以下是一个更成熟的生产级结构示例:
- cmd/
- api/
- main.go # 应用入口,依赖初始化
- internal/
- api/
- handlers/ # HTTP处理器
- user_handler.go
- product_handler.go
- middleware/ # 中间件
- routes/ # 路由定义
- domain/
- models/ # 领域模型
- user.go
- product.go
- repositories/ # 仓储接口
- user_repository.go
- product_repository.go
- services/ # 业务逻辑层
- user_service.go
- product_service.go
- infrastructure/
- db/ # 数据访问实现
- postgres/
- user_repository_impl.go
- product_repository_impl.go
- migrations/ # 数据库迁移
- cache/ # 缓存实现
- pkg/ # 可公开的库代码
- utils/
- logger/
- config/ # 配置文件
- scripts/ # 部署脚本
- go.mod
- go.sum
关于数据库访问的最佳实践,推荐使用依赖注入方式。以下是具体实现示例:
1. 仓储层接口定义 (internal/domain/repositories/user_repository.go):
package repositories
import "your-project/internal/domain/models"
type UserRepository interface {
FindByID(id uint) (*models.User, error)
Create(user *models.User) error
Update(user *models.User) error
Delete(id uint) error
}
2. 仓储实现 (internal/infrastructure/db/postgres/user_repository_impl.go):
package postgres
import (
"gorm.io/gorm"
"your-project/internal/domain/models"
"your-project/internal/domain/repositories"
)
type userRepository struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) repositories.UserRepository {
return &userRepository{db: db}
}
func (r *userRepository) FindByID(id uint) (*models.User, error) {
var user models.User
result := r.db.First(&user, id)
return &user, result.Error
}
func (r *userRepository) Create(user *models.User) error {
return r.db.Create(user).Error
}
3. 服务层 (internal/services/user_service.go):
package services
import (
"your-project/internal/domain/models"
"your-project/internal/domain/repositories"
)
type UserService struct {
userRepo repositories.UserRepository
}
func NewUserService(userRepo repositories.UserRepository) *UserService {
return &UserService{userRepo: userRepo}
}
func (s *UserService) GetUser(id uint) (*models.User, error) {
return s.userRepo.FindByID(id)
}
func (s *UserService) CreateUser(user *models.User) error {
// 业务逻辑验证
if user.Name == "" {
return errors.New("user name is required")
}
return s.userRepo.Create(user)
}
4. 处理器层 (internal/api/handlers/user_handler.go):
package handlers
import (
"net/http"
"strconv"
"your-project/internal/services"
)
type UserHandler struct {
userService *services.UserService
}
func NewUserHandler(userService *services.UserService) *UserHandler {
return &UserHandler{userService: userService}
}
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
idStr := r.URL.Query().Get("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
http.Error(w, "invalid id", http.StatusBadRequest)
return
}
user, err := h.userService.GetUser(uint(id))
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
5. 依赖初始化 (cmd/api/main.go):
package main
import (
"gorm.io/driver/postgres"
"gorm.io/gorm"
"your-project/internal/api/handlers"
"your-project/internal/api/routes"
"your-project/internal/infrastructure/db/postgres"
"your-project/internal/services"
)
func main() {
// 初始化数据库连接
dsn := "host=localhost user=postgres password=postgres dbname=mydb port=5432"
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
// 初始化仓储
userRepo := postgres.NewUserRepository(db)
// 初始化服务
userService := services.NewUserService(userRepo)
// 初始化处理器
userHandler := handlers.NewUserHandler(userService)
// 设置路由
router := routes.NewRouter(userHandler)
// 启动服务器
http.ListenAndServe(":8080", router)
}
这种结构的主要优势:
- 关注点分离:各层职责明确,便于测试和维护
- 依赖倒置:高层模块不依赖低层模块的具体实现
- 易于测试:可以通过mock仓储进行单元测试
- 可替换性:数据库实现可以轻松替换(如从PostgreSQL切换到MySQL)
对于依赖注入,Go社区常用以下方式:
- 手动依赖注入(如上述示例)
- 使用wire、fx等依赖注入框架
- 通过构造函数传递依赖
这种结构避免了全局数据库连接,使每个组件显式声明其依赖,提高了代码的可测试性和可维护性。

