Go项目结构整洁实现|GitHub 3.5k

一、前言

hi,大家好,这里是白泽。今天给大家分享一个GitHub 🌟 3.5k 的 Go项目:go-backend-clean-arch

https://github.com/amitshekhariitbhu/go-backend-clean-architecture

这个项目是一位老外写的,通过一个 HTTP demo 介绍了一个优雅的项目结构。

我也在b站出了一期30多分钟的视频,讲解了这个仓库,欢迎你的关注 📺 B站:白泽talk,qq交流群:622383022。

image-20240401202825006

🌟 当然,如果您是一位 Go 学习的新手,您可以在我开源的学习仓库:https://github.com/BaiZe1998/go-learning 中,找到我往期翻译的英文书籍,或者Go学习路线。

image-20240401222006030

二、项目架构

image-20240401202921385

三、目录详解

.
├── Dockerfile # 镜像文件
├── api
│   ├── controller # 接口
│   ├── middleware # 中间件(JWT鉴权)
│   └── route # 路由绑定
├── bootstrap
│   ├── app.go # 核心类
│   ├── database.go # 数据库
│   └── env.go # 配置类
├── cmd
│   └── main.go # 启动命令
├── docker-compose.yaml
├── domain # 实例层
├── go.mod
├── go.sum
├── internal # 内部工具
│   └── tokenutil
├── mongo # mongodb
│   └── mongo.go
├── repository # 仓储层
└── usecase # 业务层

3.1 参数配置 & 项目启动

./cmd/main.go

type Application struct {
    Env   *Env
    Mongo mongo.Client
}
​
type Env struct {
    AppEnv                 string `mapstructure:"APP_ENV"`
    ServerAddress          string `mapstructure:"SERVER_ADDRESS"`
    ContextTimeout         int    `mapstructure:"CONTEXT_TIMEOUT"`
    DBHost                 string `mapstructure:"DB_HOST"`
    DBPort                 string `mapstructure:"DB_PORT"`
    ...
}
​
func main() {
    // app 是整个应用的实例,管理生命周期中的重要资源
    app := bootstrap.App()
    // 配置变量
    env := app.Env
    // 数据库实例
    db := app.Mongo.Database(env.DBName)
    defer app.CloseDBConnection()
​
    timeout := time.Duration(env.ContextTimeout) * time.Second
    // gin 实例创建
    gin := gin.Default()
    // 路由绑定
    route.Setup(env, timeout, db, gin)
    // 运行服务
    gin.Run(env.ServerAddress)
}

🌟 接下来的讲解将以登陆逻辑为例,讲解三层架构。

3.2 接口层

./api/controller/login_controller.go

LoginController 持有配置类,以及 LoginUsecase 接口(定义了业务层的行为)

// 业务层接口
type SignupUsecase interface {
    Create(c context.Context, user *User) error
    GetUserByEmail(c context.Context, email string) (User, error)
    CreateAccessToken(user *User, secret string, expiry int) (accessToken string, err error)
    CreateRefreshToken(user *User, secret string, expiry int) (refreshToken string, err error)
}
​
type LoginController struct {
   LoginUsecase domain.LoginUsecase
   Env          *bootstrap.Env
}
​
func (lc *LoginController) Login(c *gin.Context) {
   var request domain.LoginRequest
​
   err := c.ShouldBind(&request)
   if err != nil {
      c.JSON(http.StatusBadRequest, domain.ErrorResponse{Message: err.Error()})
      return
   }
​
   user, err := lc.LoginUsecase.GetUserByEmail(c, request.Email)
   if err != nil {
      c.JSON(http.StatusNotFound, domain.ErrorResponse{Message: "User not found with the given email"})
      return
   }
​
   if bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(request.Password)) != nil {
      c.JSON(http.StatusUnauthorized, domain.ErrorResponse{Message: "Invalid credentials"})
      return
   }
​
   accessToken, err := lc.LoginUsecase.CreateAccessToken(&user, lc.Env.AccessTokenSecret, lc.Env.AccessTokenExpiryHour)
   if err != nil {
      c.JSON(http.StatusInternalServerError, domain.ErrorResponse{Message: err.Error()})
      return
   }
​
   refreshToken, err := lc.LoginUsecase.CreateRefreshToken(&user, lc.Env.RefreshTokenSecret, lc.Env.RefreshTokenExpiryHour)
   if err != nil {
      c.JSON(http.StatusInternalServerError, domain.ErrorResponse{Message: err.Error()})
      return
   }
​
   loginResponse := domain.LoginResponse{
      AccessToken:  accessToken,
      RefreshToken: refreshToken,
   }
​
   c.JSON(http.StatusOK, loginResponse)
}

3.3 业务层

./usecase/login_usecase.go

loginUsecase 结构实现 LoginUsecase 接口,同时在 loginUsecase 结构中,持有了 UserRepository 接口(定义了仓储层的行为)。

// 数据防腐层接口
type UserRepository interface {
    Create(c context.Context, user *User) error
    Fetch(c context.Context) ([]User, error)
    GetByEmail(c context.Context, email string) (User, error)
    GetByID(c context.Context, id string) (User, error)
}
​
type loginUsecase struct {
   userRepository domain.UserRepository
   contextTimeout time.Duration
}
​
func NewLoginUsecase(userRepository domain.UserRepository, timeout time.Duration) domain.LoginUsecase {
   return &loginUsecase{
      userRepository: userRepository,
      contextTimeout: timeout,
   }
}
​
func (lu *loginUsecase) GetUserByEmail(c context.Context, email string) (domain.User, error) {
   ctx, cancel := context.WithTimeout(c, lu.contextTimeout)
   defer cancel()
   return lu.userRepository.GetByEmail(ctx, email)
}
​
func (lu *loginUsecase) CreateAccessToken(user *domain.User, secret string, expiry int) (accessToken string, err error) {
   return tokenutil.CreateAccessToken(user, secret, expiry)
}
​
func (lu *loginUsecase) CreateRefreshToken(user *domain.User, secret string, expiry int) (refreshToken string, err error) {
   return tokenutil.CreateRefreshToken(user, secret, expiry)
}

3.4 防腐层

./repository/user_repository.go

userRepository 结构实现了 UserRepository 接口,内部持有 mongo.Database 接口(定义数据层行为),以及 collection 实例的名称。

// 数据操作层接口
type Database interface {
    Collection(string) Collection
    Client() Client
}
​
type userRepository struct {
   database   mongo.Database
   collection string
}
​
func NewUserRepository(db mongo.Database, collection string) domain.UserRepository {
   return &userRepository{
      database:   db,
      collection: collection,
   }
}
​
func (ur *userRepository) Create(c context.Context, user *domain.User) error {
   collection := ur.database.Collection(ur.collection)
​
   _, err := collection.InsertOne(c, user)
​
   return err
}
​
func (ur *userRepository) Fetch(c context.Context) ([]domain.User, error) {
   collection := ur.database.Collection(ur.collection)
​
   opts := options.Find().SetProjection(bson.D{{Key: "password", Value: 0}})
   cursor, err := collection.Find(c, bson.D{}, opts)
​
   if err != nil {
      return nil, err
   }
​
   var users []domain.User
​
   err = cursor.All(c, &users)
   if users == nil {
      return []domain.User{}, err
   }
​
   return users, err
}
​
func (ur *userRepository) GetByEmail(c context.Context, email string) (domain.User, error) {
   collection := ur.database.Collection(ur.collection)
   var user domain.User
   err := collection.FindOne(c, bson.M{"email": email}).Decode(&user)
   return user, err
}
​
func (ur *userRepository) GetByID(c context.Context, id string) (domain.User, error) {
   collection := ur.database.Collection(ur.collection)
​
   var user domain.User
​
   idHex, err := primitive.ObjectIDFromHex(id)
   if err != nil {
      return user, err
   }
​
   err = collection.FindOne(c, bson.M{"_id": idHex}).Decode(&user)
   return user, err
}

3.5 数据层

./mongo/mongo.go

实现了 mongo.Database 接口,通过 mongoDatabase 结构体的两个方法可以获取对应的 Client 实例和 Collection 实例,从而操作数据库。

type mongoDatabase struct {
   db *mongo.Database
}
​
func (md *mongoDatabase) Collection(colName string) Collection {
    collection := md.db.Collection(colName)
    return &mongoCollection{coll: collection}
}
​
func (md *mongoDatabase) Client() Client {
    client := md.db.Client()
    return &mongoClient{cl: client}
}

四、单例与封装

查看 ./cmd/main.go 的路由绑定逻辑:route.Setup(env, timeout, db, gin)。

func Setup(env *bootstrap.Env, timeout time.Duration, db mongo.Database, gin *gin.Engine) {
   publicRouter := gin.Group("")
   // All Public APIs
   NewSignupRouter(env, timeout, db, publicRouter)
   NewLoginRouter(env, timeout, db, publicRouter)
   NewRefreshTokenRouter(env, timeout, db, publicRouter)
​
   protectedRouter := gin.Group("")
   // Middleware to verify AccessToken
   protectedRouter.Use(middleware.JwtAuthMiddleware(env.AccessTokenSecret))
   // All Private APIs
   NewProfileRouter(env, timeout, db, protectedRouter)
   NewTaskRouter(env, timeout, db, protectedRouter)
}

进一步查看 NewLoginRouter,会发现,在注册路由触发的 controller 方法的时候,已经将所需要的 db 创建出来,并且在数据层共享,同时防腐层、业务层、控制层的实例,在服务启动前创建,依次嵌套持有,因此所有的结构都是单例,且类似树形结构,依次串联。

func NewLoginRouter(env *bootstrap.Env, timeout time.Duration, db mongo.Database, group *gin.RouterGroup) {
   ur := repository.NewUserRepository(db, domain.CollectionUser)
   lc := &controller.LoginController{
      LoginUsecase: usecase.NewLoginUsecase(ur, timeout),
      Env:          env,
   }
   group.POST("/login", lc.Login)
}

通过这种方式,实现了资源的约束,使得开发者无法跨模块调用实例,导致循环依赖等安全问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/510775.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【MySQL】DML的表操作详解:添加数据&修改数据&删除数据(可cv例题语句)

前言 大家好吖,欢迎来到 YY 滴MySQL系列 ,热烈欢迎! 本章主要内容面向接触过C Linux的老铁 主要内容含: 欢迎订阅 YY滴C专栏!更多干货持续更新!以下是传送门! YY的《C》专栏YY的《C11》专栏YY的…

FastAPI Web框架教程 第6章 表单和上传文件

6-1 什么是Form表单 需求场景 很多网站都支持上传文件,比如说:注册时上传头像;填写问卷时上传附件等等。 那么FastAPI是如何来解决文件上传的需求呢? 其实,这个需求不是FastAPI要解决的问题,这是很常见…

阿赵UE学习笔记——23、动画蒙太奇

阿赵UE学习笔记目录   大家好,我是阿赵。   继续学习虚幻引擎的使用方法。上一篇介绍了动画合成功能,这次介绍的动画蒙太奇,和动画合成有很多类似的东西,但本质上却又不同。   蒙太奇是法语“剪接”的意思。所以动画蒙太奇&…

ARM FVP平台的terminal窗口大小如何设置

当启动ARM FVP平台时,terminal窗口太小怎么办?看起来非常累眼睛,本博客来解决这个问题。 首先看下ARM FVP平台对Host主机的需求: 通过上图可知,UART默认使用的是xterm。因此,我们需要修改xterm的默认字体设…

STM32 M3内核寄存器概念

内容主要来自<<M3内核权威指南>> 汇编程序中的最低有效位&#xff08;Least Significant Bit&#xff09;。LSB是二进制数中最右边的位&#xff0c;它代表了数值中的最小单位。在汇编程序中&#xff0c;LSB通常用于表示数据的最小精度或者作为标志位。 ---------…

element-ui 修改el-form-item样式

文章目录 form结构修改el-form-item所有样式只修改label只修改content只修改input只修改button form结构 <el-form :model"formData" label-width"80px"> <el-form-item label"label1"> <el-input v-model"formData.valu…

新手如何用Postman做接口自动化测试

1、什么是自动化测试 把人对软件的测试行为转化为由机器执行测试行为的一种实践。 例如GUI自动化测试&#xff0c;模拟人去操作软件界面&#xff0c;把人从简单重复的劳动中解放出来&#xff0c;本质是用代码去测试另一段代码&#xff0c;属于一种软件开发工作&#xff0c;已…

二叉树 - 栈 - 计数 - leetcode 331. 验证二叉树的前序序列化 | 中等难度

题目 - 点击直达 leetcode 331. 验证二叉树的前序序列化 | 中等难度1. 题目详情1. 原题链接2. 基础框架 2. 解题思路1. 题目分析2. 算法原理方法1&#xff1a;栈方法2&#xff1a;计数 3. 时间复杂度 3. 代码实现方法1&#xff1a;栈方法2&#xff1a;计数 leetcode 331. 验证二…

免费Linux系统和生信宝典原创学习教程

免费Linux系统和生信宝典原创学习教程 生物信息的学习离不开Linux系统&#xff0c;不管自己写命令处理数据&#xff0c;还是使用现有的工具。Linux对我们来讲最重要的是它强大的命令行功能&#xff0c;可以快速、批量、灵活的处理数据的提取、统计和整理等耗时耗力的重复性工作…

CTF wed安全 (攻防世界)练习题

一、disabled_button 步骤一&#xff1a;进入网站发现按钮按不了 步骤二&#xff1a;按F12会查看源代码&#xff0c;会发现disabled disable属性 在HTML中&#xff0c; disabled 属性只有两个值&#xff1a;一个是不带值&#xff08;例如&#xff1a;disabled&#xff09;&…

4.2学习总结

一.java学习总结 (本次java学习总结,主要总结了抽象类和接口的一些知识,和它们之间的联系和区别) 一.抽象类 1.1定义: 抽象类主要用来抽取子类的通用特性&#xff0c;作为子类的模板&#xff0c;它不能被实例化&#xff0c;只能被用作为子类的超类。 2.概括: 有方法声明&…

【隐私计算实训营008——SCQL】

1.SCQL使用/集成最佳实践 目前SCQL只开放API供用户使用/集成 使用SCDBClient上手体验可以基于SCQL API开发封装白屏产品&#xff0c;或集成到业务链路中 1.1 部署系统 环境配置&#xff1a; 机器配置&#xff1a;CPU/MEM最低8C16G机构之间的网络互通 镜像&#xff1a;secret…

Redis实现高可用之持久化介绍

前言 在生产环境中&#xff0c;为了实现Redis的高可用性&#xff0c;可以采用持久化、主从复制、哨兵模式和 Cluster集群的方法确保数据的持久性和可靠性。这里首先介绍一下使用持久化实现服务器的高可用。 目录 一、Redis 高可用方法 1. 持久化 2. 主从复制 3. 哨兵 4.…

二轮电动自行车充电桩开源系统

文章目录 一、产品功能部分截图1.手机端&#xff08;小程序、安卓、ios&#xff09;2.PC端 二、小程序体验账号以及PC后台体验账号1.小程序体验账号2.PC后台体验账号关注公众号获取最新资讯 三、产品简介&#xff1f;1. 充电桩云平台&#xff08;含硬件充电桩&#xff09;&…

曲线降采样之道格拉斯-普克算法Douglas–Peucker

曲线降采样之道格拉斯-普克算法Douglas–Peucker 该算法的目的是&#xff0c;给定一条由线段构成的曲线&#xff0c;找到一条点数较少的相似曲线&#xff0c;来近似描述原始的曲线&#xff0c;达到降低时间、空间复杂度和平滑曲线的目的。 附赠自动驾驶学习资料和量产经验&…

【与C++的邂逅】---- 函数重载与引用

关注小庄 顿顿解馋(▿) 喜欢的小伙伴可以多多支持小庄的文章哦 &#x1f4d2; 数据结构 &#x1f4d2; C 引言 : 上一篇博客我们了解了C入门语法的一部分&#xff0c;今天我们来了解函数重载&#xff0c;引用的技术&#xff0c;请放心食用 ~ 文章目录 一. &#x1f3e0; 函数重…

windows搭建ftp实现局域网共享文件

一、开启ftp服务 1.使用 win Q 键&#xff0c;快捷打开搜索框 2.搜索框内搜索 “控制面板” 3. 进入控制面板内选择 ”程序“ 4. 单击进入 “启用或关闭windows功能” 5. 找到并展开“internet information services”、 6. 建议展开后全选 “FTP服务器” 和 “web管理工…

OpenHarmony实战:轻量系统芯片移植

本文从芯片适配的端到端视角&#xff0c;为芯片/模组制造商提供基于OpenHarmony的芯片适配指导。典型的芯片架构&#xff0c;例如cortex-m、risc-v系列都可以按照本文档进行适配移植。 约束与限制 本文档适用于OpenHarmony LTS 3.0.1及之前版本的轻量系统的适配。 说明&#…

Redis中的复制功能(三)

复制 服务器运行ID 除了复制偏移量和复制积压缓冲区之外&#xff0c;实现部分重同步还需要用到服务器运行ID(run ID): 1.每隔Redis服务器&#xff0c;不论主服务器还是从服务&#xff0c;都会有自己的运行ID2.运行ID在服务器启动时自动生成&#xff0c;由40个随机的十六进制…

ndk ffmpeg

报错&#xff1a; 解决办法&#xff1a; 报错 解决办法&#xff1a;