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

news2024/11/25 0:42:04

一、前言

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)
}

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

HTML基本元素

文章目录 如何制作标题如何制作文字如何做粗体字检查我们程序码给输出文字添加属性 HTML 一个HTML标签包含着&#xff1a; 起始标签&#xff1a;它包含了元素的名字&#xff0c;夹在一对 <、>&#xff08;尖括号&#xff09;之间。它指明元素从何处开始生效。结束标签&am…

《Git版本控制管理》笔记

第三章 git --version查看版本号git --help查看帮助文档裸双破折号分离参数 git diff -w master origin – tools/Makefile将当前目录或任何目录转化为Git版本库 git init 初始化之后项目目录中&#xff0c;有名为.git的文件git status 查看git状态git commit 提供日志消息和作…

整型之韵,数之舞:大小端与浮点数的内存之旅

✨✨欢迎&#x1f44d;&#x1f44d;点赞☕️☕️收藏✍✍评论 个人主页&#xff1a;秋邱’博客 所属栏目&#xff1a;人工智能 &#xff08;感谢您的光临&#xff0c;您的光临蓬荜生辉&#xff09; 1.0 整形提升 我们先来看看代码。 int main() {char a 3;char b 127;char …

Java基础核心Map

在Java中&#xff0c;Map是一种用于存储键值对&#xff08;key-value pairs&#xff09;的集合类型。它提供了一种将键映射到值的方式&#xff0c;其中每个键在Map中都是唯一的。Map接口是java.util包中的一部分。 常用实现类&#xff1a; HashMap: 基于哈希表实现的Map&#…

java中split(“.“)失效问题

来源&#xff1a;比较版本号_牛客题霸_牛客网 在写到这道算法题的时候&#xff0c;发现一个问题&#xff0c; String[] leftversion1.split("."); 返回结果为空&#xff0c;经过查阅得知&#xff0c;是split中的正则表达式里的问题&#xff0c;这个 . 代表的意思是…

KIMI官方精选提示词,好牛的感觉啊啊啊!

晚上好&#xff0c;我是云桃桃。一个希望帮助更多朋友快速入门 WEB 前端的程序媛。 云桃桃 1枚程序媛&#xff0c;大专生&#xff0c;2年时间从1800到月入过万&#xff0c;工作5年买房。 分享成长心得。 255篇原创内容-公众号 后台回复“前端工具”可获取开发工具&#xff0…

图论- 最小生成树

一、最小生成树-prim算法 1.1 最小生成树概念 一幅图可以有很多不同的生成树&#xff0c;比如下面这幅图&#xff0c;红色的边就组成了两棵不同的生成树&#xff1a; 对于加权图&#xff0c;每条边都有权重&#xff08;用最小生成树算法的现实场景中&#xff0c;图的边权重…

使用Vue实现CSS过渡和动画

01-初识动画和过渡 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta name"viewport" content"widthdevice-width, initial-scale1.0" /><title>使用vue实现css过渡和动画&l…

Ethernet修改网卡名称

如何修改Ethernet网卡名称 kernel/common/net/core/dev.c ---------------------------------------------------------------------- 8849/** 8850 * register_netdev - register a network device 8851 * @dev: device to register 8852 * 8853 * Take a completed network d…

github本地仓库push到远程仓库

1.从远程仓库clone到本地 2.生成SSH秘钥&#xff0c;为push做准备 在Ubuntu命令行输入一下内容 [rootlocalhost ~]# ssh-keygen -t rsa < 建立密钥对&#xff0c;-t代表类型&#xff0c;有RSA和DSA两种 Generating public/private rsa key pair. Enter file in whi…

什么是数据库的下一站?

SU7 能突出重围&#xff1f; 3 月 28 日&#xff0c;小米发布 SU7。罗永浩在社交平台发文谈小米SU7&#xff0c;“各种补贴拿完了差不多20万吧&#xff0c;足以把这个价位段的大部分油车、杂牌电车、不够智能的电车都淘汰掉了。小米很可能继平价手机市场之后&#xff0c;在大众…

微信小程序开发学习笔记——4.6tabBar底部tab栏配置用法

>>跟着b站up主“咸虾米_”学习微信小程序开发中&#xff0c;把学习记录存到这方便后续查找。 一、tabBar https://developers.weixin.qq.com/miniprogram/dev/reference/configuration/app.html#tabBar 二、icon下载 https://www.iconfont.cn/collections/index?spma…

js实现websocket断线重连功能

在项目开发中我们可能经常要使用websocket技术&#xff0c;当连接发生断线后&#xff0c;如果不进行页面刷新将不能正常接收来自服务端的推送消息。为了有效避免这种问题&#xff0c;我们需要在客户端做断线重连处理。当网络或服务出现问题后&#xff0c;客户端会不断检测网络状…

一个在线的NodeJS环境和终端运行界面

WebContainer 是一个基于浏览器的运行时&#xff0c;用于执行 Node.js 应用程序和操作系统命令&#xff0c;完全在浏览器选项卡内。以前需要云虚拟机来执行用户代码的应用程序&#xff0c;在 WebContainers 中可以完全在客户端运行与传统云虚拟机相比具有许多优势。 WebContai…

卡尔曼滤波笔记

资料&#xff1a;https://www.zhihu.com/question/47559783/answer/2988744371 https://www.zhihu.com/question/47559783 https://blog.csdn.net/seek97/article/details/120012667 一、基本思想 在对一个状态值进行估计的时候&#xff0c;如果想测量值更准&#xff0c;很自然…

数据结构进阶篇 之 【插入排序】详细讲解(直接插入排序,希尔排序)

千万不要因为一件事不会做而失去信心&#xff0c;你又不是只有这一件事不会&#xff0c;你还有很多呢 一、插入排序 1.直接插入排序 InsertSort 1.1 基本思想 1.2 实现原理 1.3 代码实现 1.4 直接插入排序的特性总结 2.希尔排序 ShellSort 2.1 基本思想 2.2 实现原理 …

6.7物联网RK3399项目开发实录-驱动开发之Camera摄像头的使用(wulianjishu666)

90款行业常用传感器单片机程序及资料【stm32,stc89c52,arduino适用】 链接&#xff1a;https://pan.baidu.com/s/1M3u8lcznKuXfN8NRoLYtTA?pwdc53f Camera 使用 简介 AIO-3399J 开发板分别带有两个 MIPI&#xff0c;MIPI 支持最高 4K 拍照&#xff0c;并支持 1080P 30fp…

282: 数字的秘密

收藏 难度&#xff1a;简单 标签&#xff1a;卓越班选拔 题目描述 在数字王国中&#xff0c;人人都有一个号码&#xff0c;人人都想知道这个号码对自己意味着什么。国王便做了很多个盒子&#xff0c;每个盒子拥有一个号码。人们需要通过一定的方法才能确定哪个盒子有自己的…

【Java 集合进阶】单练集合顶层接口collction迭代器

&#x1f36c; 博主介绍&#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;我是 hacker-routing &#xff0c;很高兴认识大家~ ✨主攻领域&#xff1a;【渗透领域】【应急响应】 【Java】 【VulnHub靶场复现】【面试分析】 &#x1f389;点赞➕评论➕收藏 …

jsp将一个文本输入框改成下拉单选框,选项为字典表配置,通过后端查询

一&#xff0c;业务场景&#xff1a; 一个人员信息管理页面&#xff0c;原来有个最高学历是文本输入框&#xff0c;可以随意填写&#xff0c;现在业务想改成下拉单选框进行规范化&#xff0c;在专科及以下、本科、研究生三个选项中选择&#xff1b; 二&#xff0c;需要解决问…