Go 項目結構的簡潔實現

今天分享一個在 GitHub 上有 3.5k stars 的 Go 項目:go-backend-clean-arch[1]。

該項目展示了一個 Golang 後端項目如何設計優雅的項目結構。

  1. 項目架構

3.Directory Explanation

.├── Dockerfile           # Image file
├── api
│   ├── controller       # Controllers
│   ├── middleware       # Middleware (JWT authentication)
│   └── route            # Route binding
├── bootstrap
│   ├── app.go           # Core class
│   ├── database.go      # Database
│   └── env.go           # Configuration class
├── cmd
│   └── main.go          # Startup command
├── docker-compose.yaml  
├── domain               # Domain layer
├── go.mod               
├── go.sum               
├── internal             # Internal utilities
│   └── tokenutil        
├── mongo                # MongoDB
│   └── mongo.go         
├── repository           # Repository layer
└── usecase              # Use case layer

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 is the instance of the entire application, managing key resources throughout its lifecycle
    app := bootstrap.App()
    
    // Configuration variables
    env := app.Env
    
    // Database instance
    db := app.Mongo.Database(env.DBName)
    defer app.CloseDBConnection()
    
    timeout := time.Duration(env.ContextTimeout) * time.Second
    
    // Creating a gin instance
    gin := gin.Default()
    
    // Route binding
    route.Setup(env, timeout, db, gin)
    
    // Running the server
    gin.Run(env.ServerAddress)
}

下面將以登錄邏輯爲例,說明三層架構。

3.2 接口層

./api/controller/login_controller.go

LoginController 包含配置類和 LoginUsecase 接口(定義業務層的行爲)。

// Business layer interface


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 接口。

// Data anti-corruption layer interface
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 Anti-Corruption 層

./repository/user_repository.go userRepository 結構實現了 UserRepository 接口。它內部持有 mongo.Database 接口(該接口定義了數據層的行爲)和集合實例的名稱。

// Data operation layer interface


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}
}
  1. 單例和封裝

檢查 ./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)
}

這種方法可以強制執行資源限制,防止開發人員跨模塊調用實例, 這可能會導致循環依賴等問題和其他安全問題。

以上就是一個標準 Golang 後端的簡潔架構,大家可以參考。

參考資料

[1]

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

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Ivrg1_0afSRZOkY-VYs0Vg