解剖了一个Github 5k Star项目
今天分享一个github 4.7k star的go整洁架构的项目go-backend-clean-architecture
,项目技术栈:Gin
、MongoDB
、JWT Authentication Middleware
、Test
、Docker
等
项目通过一个HTTP
服务的demo来做演示。
工程结构
目录结构
整个工程的目录结构大致包含了:启动入口、环境变量配置、控制器层、业务层、反腐层、数据存储层。接下来我们逐层展开介绍。
.
├── Dockerfile
├── api
│ ├── controller
│ │ ├── login_controller.go
│ │ ├── profile_controller.go
│ │ ├── profile_controller_test.go
│ │ ├── refresh_token_controller.go
│ │ ├── signup_controller.go
│ │ └── task_controller.go
│ ├── middleware
│ │ └── jwt_auth_middleware.go
│ └── route
│ ├── login_route.go
│ ├── profile_route.go
│ ├── refresh_token_route.go
│ ├── route.go
│ ├── signup_route.go
│ └── task_route.go
├── bootstrap
│ ├── app.go
│ ├── database.go
│ └── env.go
├── cmd
│ └── main.go
├── docker-compose.yaml
├── domain
│ ├── error_response.go
│ ├── jwt_custom.go
│ ├── login.go
│ ├── profile.go
│ ├── refresh_token.go
│ ├── signup.go
│ ├── success_response.go
│ ├── task.go
│ └── user.go
├── go.mod
├── go.sum
├── internal
│ └── tokenutil
│ └── tokenutil.go
├── mongo
│ └── mongo.go
├── repository
│ ├── task_repository.go
│ ├── user_repository.go
│ └── user_repository_test.go
└── usecase
├── login_usecase.go
├── profile_usecase.go
├── refresh_token_usecase.go
├── signup_usecase.go
├── task_usecase.go
└── task_usecase_test.go
系统参数配置、启动入口
接触一个新工程,从入口开始: ./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)
// 启动server
gin.Run(env.ServerAddress)
}
下面我们通过登录逻辑为例来详细介绍一下整洁架构的三层架构。
控制器层
以登录逻辑的controller
层为例:
./api/controller/login_controller.go
LoginController
持有Configuration
类和LoginUsecase
接口,LoginUsecase
定义了登录的具体业务。
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)
}
业务层
./usecase/login_usecase.go
loginUsecase
实现了LoginUsecase
接口,完成了具体的登录逻辑。
//数据反腐层接口
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)
}
反腐层
“
反腐层(Anti-corruption layer,简称 ACL)介于新应用和旧应用之间,用于确保新应用的设计不受老应用的限制。是一种在不同应用间转换的机制。创建一个反腐层,以根据客户端自己的域模型为客户提供功能。该层通过其现有接口与另一个系统进行通信,几乎不需要对其进行任何修改。因此,反腐层隔离不仅是为了保护你的系统免受异常代码的侵害,还在于分离不同的域并确保它们在将来保持分离。反腐层是将一个域映射到另一个域,这样使用第二个域的服务就不必被第一个域的概念“破坏”。
这里引入防腐层主要是为了应对未来可能存在的多种存储方式。
./repository/user_repository.go
userRepository
实现了UserRepository
接口。并且它的内部持有了mongo.Database
接口,mongo.Database
定义了数据存储层的一系列操作。
//数据存储层接口
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
}
数据存储层
./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 发现,在注册由路由触发的控制器方法时,所需的数据库已在数据层内创建并共享。
此外,反腐层、业务层和控制器层的实例在服务启动之前创建,并按顺序嵌套和保存。
因此,所有结构都是单例,类似于树状结构,按顺序链接。
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)
}
这种方法强制实施资源限制,阻止开发人员跨模块调用实例,从而尽可能规避循环依赖和其他安全问题。
题外话
这种三层结构应对业务不是那么复杂的小型项目,比较游刃有余。一旦出现业务比较复杂的大型项目,你会发现业务层会极其复杂,业务逻辑盘根错节。如果继续采用三层结构,维护难度会指数攀升。
如何解决?大家可以评论区说说你们的解法,我们之后再单起一篇单独聊聊我们的解法