Golang项目结构最佳实践探讨

Golang项目结构最佳实践探讨 我正在做一个业余项目,是一个拳击网站。我不太确定应该如何组织不同的API。

我的API包括:

  • 管理API,它更多地依赖于从Rapid API网站加载数据,但我使用Templ和HTMX添加了一个前端,以防我需要手动更新拳击比赛、拳手等信息。
  • 网站API,主要供登录网站的用户使用。
  • 移动端API,用于移动应用程序。我目前倾向于使用Flutter。我也在考虑将网站API和移动端API合并,因为它们应该执行完全相同的功能。

我的核心问题是:我应该将所有内容都放在同一个目录结构下,还是像下面这样组织:

Boxing
-----Admin Folders for Admin API
-----Web Folders for Web API
-----Mobile Folders for Mobile API

任何帮助都将不胜感激。


更多关于Golang项目结构最佳实践探讨的实战教程也可以访问 https://www.itying.com/category-94-b0.html

5 回复

-----管理文件夹用于管理API -----Web文件夹用于Web API -----移动端文件夹用于移动端API

只是好奇。为什么不采用一个单一的API作为微服务,然后配合三个不同的前端呢?

更多关于Golang项目结构最佳实践探讨的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


我对这一切都是新手,我的背景是数据工程和报表开发。这是否意味着我应该在一个文件(比如 api.go 文件)中这样做:

router := http.NewServerMux()
router.Handle( "GET /admin/event" …)
router.Handle(" POST /admin/event" …)

router.Handle(" GET /web/event" …)
router.Handle(" GET /mobile/event" …)

类似这样,放在一个 api.go 文件里

在我看来,是的。你也可以在 REST 中使用一个“目标载体”,稍后再从 URL 中将其剥离。

/admin/event/…/…
/web/event/…/…
/mobile/event/…/…
Switch target:
  case admin:
     // do admin stuff /event/.../...
  etc...

mobile

我可以理解针对“管理”功能设置不同的路由。然而,你的移动应用和网页应用不都是在消费相同的“非管理”数据并处理相同的业务逻辑吗?只是通过不同的用户界面而已。你在问题中也暗示了这一点,它们做的是同一件事。构建API的主要原因之一,就是为了让不同的用户界面(网页、移动iOS、移动Android、Mac客户端、Windows客户端等等,根据你的需求而定)都能消费相同的端点,从而将逻辑层和数据层与表示层解耦。假设数据和业务逻辑是相同的,那么每个“非管理”资源应该只有一个基础路由,例如只有“/event”,并且每个用户界面都应该消费完全相同的这个路由。用户是否登录不应该影响这一点,你会有一个用户登录路由,可以用于任何用户界面,例如“/login”。

对于你的拳击网站项目,我建议采用按领域划分的模块化结构,而不是按API类型分离。这样可以更好地实现代码复用,特别是考虑到网站API和移动端API功能相同的情况。

以下是一个推荐的项目结构示例:

boxing/
├── cmd/
│   ├── admin/          # 管理API入口
│   │   └── main.go
│   ├── web/           # 网站API入口
│   │   └── main.go
│   └── mobile/        # 移动API入口(可复用web逻辑)
│       └── main.go
├── internal/
│   ├── domain/        # 核心领域模型
│   │   ├── fighter.go
│   │   ├── match.go
│   │   └── ranking.go
│   ├── service/       # 业务逻辑层
│   │   ├── fighter_service.go
│   │   ├── match_service.go
│   │   └── rapidapi_client.go
│   ├── handler/       # HTTP处理器
│   │   ├── admin/
│   │   │   ├── fighter_handler.go
│   │   │   └── match_handler.go
│   │   ├── web/
│   │   │   ├── fighter_handler.go
│   │   │   └── match_handler.go
│   │   └── mobile/
│   │       └── fighter_handler.go
│   ├── repository/    # 数据访问层
│   │   ├── fighter_repo.go
│   │   └── match_repo.go
│   └── middleware/    # 中间件
│       ├── auth.go
│       └── logging.go
├── pkg/
│   ├── api/           # 可复用的API组件
│   │   ├── response.go
│   │   └── validator.go
│   └── util/          # 工具函数
│       └── timeutil.go
├── web/               # 前端资源(Templ/HTMX)
│   ├── templates/
│   ├── static/
│   └── assets/
├── api/               # API定义(OpenAPI/Protobuf)
│   ├── openapi/
│   └── proto/
├── configs/           # 配置文件
├── deployments/       # 部署配置
├── go.mod
└── go.sum

关键代码示例:

共享的业务逻辑(internal/service/fighter_service.go)

package service

import (
    "context"
    "boxing/internal/domain"
    "boxing/internal/repository"
)

type FighterService struct {
    repo repository.FighterRepository
    rapidAPI RapidAPIClient
}

func NewFighterService(repo repository.FighterRepository, client RapidAPIClient) *FighterService {
    return &FighterService{repo: repo, rapidAPI: client}
}

func (s *FighterService) GetFighter(ctx context.Context, id string) (*domain.Fighter, error) {
    // 业务逻辑,可被所有API复用
    return s.repo.FindByID(ctx, id)
}

func (s *FighterService) UpdateFromRapidAPI(ctx context.Context, fighterID string) error {
    // 管理API专用的RapidAPI集成逻辑
    data, err := s.rapidAPI.FetchFighterData(ctx, fighterID)
    if err != nil {
        return err
    }
    return s.repo.Update(ctx, data)
}

不同的HTTP处理器(internal/handler/web/fighter_handler.go)

package web

import (
    "net/http"
    "boxing/internal/service"
    "boxing/pkg/api"
)

type FighterHandler struct {
    service *service.FighterService
}

func NewFighterHandler(svc *service.FighterService) *FighterHandler {
    return &FighterHandler{service: svc}
}

func (h *FighterHandler) GetFighter(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    fighter, err := h.service.GetFighter(r.Context(), id)
    if err != nil {
        api.RespondError(w, http.StatusNotFound, "fighter not found")
        return
    }
    api.RespondJSON(w, http.StatusOK, fighter)
}

管理API专用处理器(internal/handler/admin/fighter_handler.go)

package admin

import (
    "net/http"
    "boxing/internal/service"
    "boxing/pkg/api"
)

type FighterHandler struct {
    service *service.FighterService
}

func (h *FighterHandler) UpdateFromRapidAPI(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    err := h.service.UpdateFromRapidAPI(r.Context(), id)
    if err != nil {
        api.RespondError(w, http.StatusInternalServerError, "failed to update from RapidAPI")
        return
    }
    api.RespondJSON(w, http.StatusOK, map[string]string{"status": "updated"})
}

不同的入口点(cmd/web/main.go)

package main

import (
    "boxing/internal/handler/web"
    "boxing/internal/service"
    "boxing/internal/repository"
)

func main() {
    // 初始化共享依赖
    repo := repository.NewFighterRepository()
    svc := service.NewFighterService(repo, nil)
    
    // 创建Web专用的处理器
    handler := web.NewFighterHandler(svc)
    
    // 配置Web特有的路由和中间件
    mux := http.NewServeMux()
    mux.HandleFunc("GET /api/fighters/{id}", handler.GetFighter)
    
    http.ListenAndServe(":8080", mux)
}

这种结构的优势:

  1. 业务逻辑完全复用:网站API和移动端API共享相同的service
  2. 清晰的关注点分离:不同API的HTTP层可以有不同的认证、限流等逻辑
  3. 管理API特殊处理:RapidAPI集成逻辑只在管理API中暴露
  4. 独立部署能力:每个cmd/下的入口点都可以单独编译和部署
  5. 前端集成web/目录专门存放Templ模板和HTMX前端代码

对于移动端API,如果确实与网站API功能完全相同,可以直接复用internal/handler/web/中的处理器,或者通过条件编译来提供不同的序列化格式(如JSON和Protobuf)。

回到顶部