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.DBgorm.DB作为其字段吗?


更多关于Golang中如何设计API项目的结构的实战教程也可以访问 https://www.itying.com/category-94-b0.html

2 回复

嗯,我的主要建议是:避免golang-standards组织及其中的建议。在我看来,这是一个很好的起点,但需要注意,你应该只在需要时才添加内容(就像我自己从来不会在开始一个新应用时,首先就去创建一堆文件夹)。我认为这篇关于包命名的文章非常出色,值得一读:

go.dev

包命名 - Go编程语言

如何为你的包命名。

特别是这条建议,我尝试(以不同程度的成功)去遵循:

不要从用户那里窃取好的名称。 避免给包起一个在客户端代码中常用的名字。例如,缓冲I/O包被称为 bufio,而不是 buf,因为 buf 是一个很好的缓冲区变量名。

还有另一篇好文章:

go.dev

组织Go模块 - Go编程语言

好的——如果我吹毛求疵的话,这里有一些小的反馈:

  • 我认为 /db/crud/ 可能更适合泛化为 /db/queries,因为CRUD只意味着简单的(创建、读取、更新、删除)查询。在大多数项目中,你会有其他查询。
  • 我认为将每个服务都作为自己的包可能有些过度,更像是.NET的做法。你的服务是做什么的?为什么它们是一个“服务”,而不是像 /internal/auth 这样的东西?你的服务是像 type AuthService 这样的结构体吗?你是在管理生命周期(比如它是单例的、作用域限定在上下文中的,还是其他什么?)如果不是,我可能会说最好就用像 /internal/auth 这样的东西。
  • 你可以争辩说 utils 太通用了。不过,我假设你会在需要时在那里创建实用程序,它主要是“那些不太值得拥有自己的顶级文件夹或包的东西”的集合地。上面提到的官方命名指南说:“名为 utilcommonmisc 的包无法让客户端了解包中包含什么”。但是——我认为只要你对其中包含的内容做个说明(稍后会详细说明),这就没问题。

回到像 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)
}

这种结构的主要优势:

  1. 关注点分离:各层职责明确,便于测试和维护
  2. 依赖倒置:高层模块不依赖低层模块的具体实现
  3. 易于测试:可以通过mock仓储进行单元测试
  4. 可替换性:数据库实现可以轻松替换(如从PostgreSQL切换到MySQL)

对于依赖注入,Go社区常用以下方式:

  • 手动依赖注入(如上述示例)
  • 使用wire、fx等依赖注入框架
  • 通过构造函数传递依赖

这种结构避免了全局数据库连接,使每个组件显式声明其依赖,提高了代码的可测试性和可维护性。

回到顶部