基于DDD的golang实现

什么是DDD?

以下是考虑使用DDD的原因:

  • 提供解决困难问题的原则和模式

  • 将复杂的设计基于领域模型

  • 在技术和领域专家之间发起创造性的协作,以迭代地完善解决领域问题的概念模型。

DDD包含4个层:

  1. Domain:这是定义应用程序的域和业务逻辑的地方

  2. Infrastructure:此层包含独立于我们的应用程序而存在的所有内容:外部库,数据库引擎等。

  3. Application:该层用作域和界面层之间的通道。将请求从接口层发送到域层,由域层处理请求并返回响应。

  4. Interface:该层包含与其他系统交互的所有内容,例如Web服务,RMI接口或Web应用程序以及批处理前端。

DDD包含4个层:

  1. Domain:这是定义应用程序的域和业务逻辑的地方

  2. Infrastructure:此层包含独立于我们的应用程序而存在的所有内容:外部库,数据库引擎等。

  3. Application:该层用作域和界面层之间的通道。将请求从接口层发送到域层,由域层处理请求并返回响应。

  4. Interface:该层包含与其他系统交互的所有内容,例如Web服务,RMI接口或Web应用程序以及批处理前端。

1

开始

 

 

我们将构建一个食物推荐API。

首先要做的是初始化依赖关系管理。我们将使用go.mod。在根目录(路径:food-app /)中,初始化go.mod:

1
go mod init food-app

项目的组织结构:

在该应用中,我们将使用postgres和redis数据库持久化数据。先定义一个含有连接信息的.env文件。
.env文件内容:

#Postgres
APP_ENV=local
API_PORT=8888
DB_HOST=127.0.0.1
DB_DRIVER=postgres
ACCESS_SECRET=98hbun98h
REFRESH_SECRET=786dfdbjhsb
DB_USER=steven
DB_PASSWORD=password
DB_NAME=food-app
DB_PORT=5432

#Mysql
#DB_HOST=127.0.0.1
#DB_DRIVER=mysql
#DB_USER=steven
#DB_PASSWORD=here
#DB_NAME=food-app
#DB_PORT=3306


#Postgres Test DB
TEST_DB_DRIVER=postgres
TEST_DB_HOST=127.0.0.1
TEST_DB_PASSWORD=password
TEST_DB_USER=steven
TEST_DB_NAME=food-app-test
TEST_DB_PORT=5432

#Redis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASSWORD=

该文件应位于根目录中(路径:food-app /)

2

Domain 层

我们将首先考虑领域。

该域具有几种模式。其中一些是:实体,值,存储库,服务等。

由于我们在此处构建的应用比较简单,因此我们仅考虑两种域模式:实体和存储库。

实体

这是我们定义“Schema”的地方。
例如,我们可以定义用户的结构。将该实体视为域的蓝图。

package entity

import (
    "food-app/infrastructure/security"
    "github.com/badoux/checkmail"
    "html"
    "strings"
    "time"
)

type User struct {
    ID        uint64     `gorm:"primary_key;auto_increment" json:"id"`
    FirstName string     `gorm:"size:100;not null;" json:"first_name"`


    LastName  string     `gorm:"size:100;not null;" json:"last_name"`


    Email     string     `gorm:"size:100;not null;unique" json:"email"`
    Password  string     `gorm:"size:100;not null;" json:"password"`
    CreatedAt time.Time  `gorm:"default:CURRENT_TIMESTAMP" json:"created_at"`
    UpdatedAt time.Time  `gorm:"default:CURRENT_TIMESTAMP" json:"updated_at"`
    DeletedAt *time.Time `json:"deleted_at,omitempty"`
}

type PublicUser struct {
    ID        uint64 `gorm:"primary_key;auto_increment" json:"id"`
    FirstName string `gorm:"size:100;not null;" json:"first_name"`
    LastName  string `gorm:"size:100;not null;" json:"last_name"`
}

//BeforeSave is a gorm hook
func (u *User) BeforeSave() error {
    hashPassword, err := security.Hash(u.Password)
    if err != nil {
        return err
    }
    u.Password = string(hashPassword)
    return nil
}

type Users []User

//So that we dont expose the user's email address and password to the world
func (users Users) PublicUsers() []interface{} {
    result := make([]interface{}, len(users))
    for index, user := range users {
        result[index] = user.PublicUser()
    }
    return result
}

//So that we dont expose the user's email address and password to the world
func (u *User) PublicUser() interface{} {
    return &PublicUser{
        ID:        u.ID,
        FirstName: u.FirstName,
        LastName:  u.LastName,
    }
}

func (u *User) Prepare() {
    u.FirstName = html.EscapeString(strings.TrimSpace(u.FirstName))
    u.LastName = html.EscapeString(strings.TrimSpace(u.LastName))


    u.Email = html.EscapeString(strings.TrimSpace(u.Email))
    u.CreatedAt = time.Now()
    u.UpdatedAt = time.Now()
}

func (u *User) Validate(action string) map[string]string {
    var errorMessages = make(map[string]string)
    var err error

    switch strings.ToLower(action) {
    case "update":
        if u.Email == "" {
            errorMessages["email_required"] = "email required"
        }
        if u.Email != "" {
            if err = checkmail.ValidateFormat(u.Email); err != nil {
                errorMessages["invalid_email"] = "email email"
            }
        }

    case "login":
        if u.Password == "" {
            errorMessages["password_required"] = "password is required"
        }
        if u.Email == "" {
            errorMessages["email_required"] = "email is required"
        }
        if u.Email != "" {
            if err = checkmail.ValidateFormat(u.Email); err != nil {
                errorMessages["invalid_email"] = "please provide a valid email"
            }
        }
    case "forgotpassword":
        if u.Email == "" {
            errorMessages["email_required"] = "email required"
        }
        if u.Email != "" {
            if err = checkmail.ValidateFormat(u.Email); err != nil {
                errorMessages["invalid_email"] = "please provide a valid email"
            }
        }
    default:
        if u.FirstName == "" {
            errorMessages["firstname_required"] = "first name is required"
        }
        if u.LastName == "" {
            errorMessages["lastname_required"] = "last name is required"
        }
        if u.Password == "" {
            errorMessages["password_required"] = "password is required"
        }
        if u.Password != "" && len(u.Password) < 6 {
            errorMessages["invalid_password"] = "password should be at least 6 characters"
        }
        if u.Email == "" {
            errorMessages["email_required"] = "email is required"
        }
        if u.Email != "" {
            if err = checkmail.ValidateFormat(u.Email); err != nil {
                errorMessages["invalid_email"] = "please provide a valid email"
            }
        }
    }
    return errorMessages
}

在上面的文件中,定义了包含用户信息的用户结构,我们还添加了帮助程序功能,这些功能将验证和清理输入。调用了一种哈希方法,该方法用于哈希密码。这是在基础结构层中定义的。

定义 food 实体时采用相同的方法。

存储库

存储库定义了基础结构实现的方法的集合。这描绘了与给定数据库或第三方API交互的方法数量。

user 存储库如下所示:

package repository

import (
    "food-app/domain/entity"
)

type UserRepository interface {
    SaveUser(*entity.User) (*entity.User, map[string]string)
    GetUser(uint64) (*entity.User, error)
    GetUsers() ([]entity.User, error)
    GetUserByEmailAndPassword(*entity.User) (*entity.User, map[string]string)
}

方法在接口中定义。这些方法稍后将在基础结构层中实现。

food 库几乎相同。

3

Infrastructure 层

该层实现存储库中定义的方法。这些方法与数据库或第三方API交互。本文中仅考虑数据库交互。

我们可以看到 user 存储库实现如下所示:

package persistence

import (
    "errors"
    "food-app/domain/entity"
    "food-app/domain/repository"
    "food-app/infrastructure/security"
    "github.com/jinzhu/gorm"
    "golang.org/x/crypto/bcrypt"
    "strings"
)

type UserRepo struct {
    db *gorm.DB
}

func NewUserRepository(db *gorm.DB) *UserRepo {
    return &UserRepo{db}
}
//UserRepo implements the repository.UserRepository interface
var _ repository.UserRepository = &UserRepo{}

func (r *UserRepo) SaveUser(user *entity.User) (*entity.User, map[string]string) {
    dbErr := map[string]string{}
    err := r.db.Debug().Create(&user).Error
    if err != nil {
        //If the email is already taken
        if strings.Contains(err.Error(), "duplicate") || strings.Contains(err.Error(), "Duplicate") {
            dbErr["email_taken"] = "email already taken"
            return nil, dbErr
        }
        //any other db error
        dbErr["db_error"] = "database error"
        return nil, dbErr
    }
    return user, nil
}

func (r *UserRepo) GetUser(id uint64) (*entity.User, error) {
    var user entity.User
    err := r.db.Debug().Where("id = ?", id).Take(&user).Error
    if err != nil {
        return nil, err
    }
    if gorm.IsRecordNotFoundError(err) {
        return nil, errors.New("user not found")
    }
    return &user, nil
}

func (r *UserRepo) GetUsers() ([]entity.User, error) {
    var users []entity.User
    err := r.db.Debug().Find(&users).Error
    if err != nil {
        return nil, err
    }
    if gorm.IsRecordNotFoundError(err) {
        return nil, errors.New("user not found")
    }
    return users, nil
}

func (r *UserRepo) GetUserByEmailAndPassword(u *entity.User) (*entity.User, map[string]string) {
    var user entity.User
    dbErr := map[string]string{}
    err := r.db.Debug().Where("email = ?", u.Email).Take(&user).Error
    if gorm.IsRecordNotFoundError(err) {
        dbErr["no_user"] = "user not found"
        return nil, dbErr
    }
    if err != nil {
        dbErr["db_error"] = "database error"
        return nil, dbErr
    }
    //Verify the password
    err = security.VerifyPassword(user.Password, u.Password)
    if err != nil && err == bcrypt.ErrMismatchedHashAndPassword {
        dbErr["incorrect_password"] = "incorrect password"
        return nil, dbErr
    }
    return &user, nil
}

可以看到我们实现了存储库中定义的方法。使用实现了UserRepository接口的UserRepo结构可以做到这一点,如下行所示:

//UserRepo implements the repository.UserRepository interface
var _ repository.UserRepository = &UserRepo{}

因此,我们通过创建包含以下内容的db.go文件来配置数据库:

package persistence

import (
    "fmt"
    "food-app/domain/entity"
    "food-app/domain/repository"
    "github.com/jinzhu/gorm"
    _ "github.com/jinzhu/gorm/dialects/postgres"
)

type Repositories struct {
    User repository.UserRepository
    Food repository.FoodRepository
    db   *gorm.DB
}

func NewRepositories(Dbdriver, DbUser, DbPassword, DbPort, DbHost, DbName string) (*Repositories, error) {
    DBURL := fmt.Sprintf("host=%s port=%s user=%s dbname=%s sslmode=disable password=%s", DbHost, DbPort, DbUser, DbName, DbPassword)
    db, err := gorm.Open(Dbdriver, DBURL)
    if err != nil {
        return nil, err
    }
    db.LogMode(true)

    return &Repositories{
        User: NewUserRepository(db),
        Food: NewFoodRepository(db),
        db:   db,
    }, nil
}

//closes the  database connection
func (s *Repositories) Close() error {
    return s.db.Close()
}

//This migrate all tables
func (s *Repositories) Automigrate() error {
    return s.db.AutoMigrate(&entity.User{}, &entity.Food{}).Error
}

 


在上面的文件中,我们定义了Repositories结构,该结构保存了应用中的所有存储库。我们有 user 和 food 库。该存储库还具有一个db实例,该实例被传递给 user 和 food(即NewUserRepository和NewFoodRepository)的“constructors”。

4

Application 层

我们已经在域中定义了API业务逻辑。该层连接 domain 和 interfaces 层。

以下是 user 的应用层:

package application

import (
    "food-app/domain/entity"
    "food-app/domain/repository"
)

type userApp struct {
    us repository.UserRepository
}

//UserApp implements the UserAppInterface
var _ UserAppInterface = &userApp{}

type UserAppInterface interface {
    SaveUser(*entity.User) (*entity.User, map[string]string)
    GetUsers() ([]entity.User, error)
    GetUser(uint64) (*entity.User, error)
    GetUserByEmailAndPassword(*entity.User) (*entity.User, map[string]string)
}

func (u *userApp) SaveUser(user *entity.User) (*entity.User, map[string]string) {
    return u.us.SaveUser(user)
}

func (u *userApp) GetUser(userId uint64) (*entity.User, error) {
    return u.us.GetUser(userId)
}

func (u *userApp) GetUsers() ([]entity.User, error) {
    return u.us.GetUsers()
}

func (u *userApp) GetUserByEmailAndPassword(user *entity.User) (*entity.User, map[string]string) {
    return u.us.GetUserByEmailAndPassword(user)
}

 


上面有保存和检索用户数据的方法。UserApp结构具有UserRepository接口,从而可以调用用户存储库方法。

5

Interfaces 层

接口是处理HTTP请求和响应的层。这里我们收到身份验证,与用户相关的内容和与食品相关的内容的传入请求。

用户处理

我们定义了保存用户,获取所有用户和获取特定用户的方法。这些可以在user_handler.go文件中找到。

package interfaces

import (
    "food-app/application"
    "food-app/domain/entity"
    "food-app/infrastructure/auth"
    "github.com/gin-gonic/gin"
    "net/http"
    "strconv"
)

//Users struct defines the dependencies that will be used
type Users struct {
    us application.UserAppInterface
    rd auth.AuthInterface
    tk auth.TokenInterface
}

//Users constructor
func NewUsers(us application.UserAppInterface, rd auth.AuthInterface, tk auth.TokenInterface) *Users {
    return &Users{
        us: us,
        rd: rd,
        tk: tk,
    }
}

func (s *Users) SaveUser(c *gin.Context) {
    var user entity.User
    if err := c.ShouldBindJSON(&user); err != nil {
        c.JSON(http.StatusUnprocessableEntity, gin.H{
            "invalid_json": "invalid json",
        })
        return
    }
    //validate the request:
    validateErr := user.Validate("")
    if len(validateErr) > 0 {
        c.JSON(http.StatusUnprocessableEntity, validateErr)
        return
    }
    newUser, err := s.us.SaveUser(&user)
    if err != nil {
        c.JSON(http.StatusInternalServerError, err)
        return
    }
    c.JSON(http.StatusCreated, newUser.PublicUser())
}

func (s *Users) GetUsers(c *gin.Context) {
    users := entity.Users{} //customize user
    var err error
    //us, err = application.UserApp.GetUsers()
    users, err = s.us.GetUsers()
    if err != nil {
        c.JSON(http.StatusInternalServerError, err.Error())
        return
    }
    c.JSON(http.StatusOK, users.PublicUsers())
}

func (s *Users) GetUser(c *gin.Context) {
    userId, err := strconv.ParseUint(c.Param("user_id"), 10, 64)
    if err != nil {
        c.JSON(http.StatusBadRequest, err.Error())
        return
    }
    user, err := s.us.GetUser(userId)
    if err != nil {
        c.JSON(http.StatusInternalServerError, err.Error())
        return
    }
    c.JSON(http.StatusOK, user.PublicUser())
}

 

观察到返回用户时,我们仅返回一个公共用户(在实体中定义)。公共用户没有敏感的用户详细信息,例如电子邮件和密码。

授权处理

login_handler负责登录,注销和刷新令牌方法。在各自文件中定义的某些方法在此文件中被调用。最好在它们的文件路径之后在存储库中检出它们。package interfaces

import (
    "fmt"
    "food-app/application"
    "food-app/domain/entity"
    "food-app/infrastructure/auth"
    "github.com/dgrijalva/jwt-go"
    "github.com/gin-gonic/gin"
    "net/http"
    "os"
    "strconv"
)

type Authenticate struct {
    us application.UserAppInterface
    rd auth.AuthInterface
    tk auth.TokenInterface
}

//Authenticate constructor
func NewAuthenticate(uApp application.UserAppInterface, rd auth.AuthInterface, tk auth.TokenInterface) *Authenticate {
    return &Authenticate{
        us: uApp,
        rd: rd,
        tk: tk,
    }
}

func (au *Authenticate) Login(c *gin.Context) {
    var user *entity.User
    var tokenErr = map[string]string{}

    if err := c.ShouldBindJSON(&user); err != nil {
        c.JSON(http.StatusUnprocessableEntity, "Invalid json provided")
        return
    }
    //validate request:
    validateUser := user.Validate("login")
    if len(validateUser) > 0 {
        c.JSON(http.StatusUnprocessableEntity, validateUser)
        return
    }
    u, userErr := au.us.GetUserByEmailAndPassword(user)
    if userErr != nil {
        c.JSON(http.StatusInternalServerError, userErr)
        return
    }
    ts, tErr := au.tk.CreateToken(u.ID)
    if tErr != nil {
        tokenErr["token_error"] = tErr.Error()
        c.JSON(http.StatusUnprocessableEntity, tErr.Error())
        return
    }
    saveErr := au.rd.CreateAuth(u.ID, ts)
    if saveErr != nil {
        c.JSON(http.StatusInternalServerError, saveErr.Error())
        return
    }
    userData := make(map[string]interface{})
    userData["access_token"] = ts.AccessToken
    userData["refresh_token"] = ts.RefreshToken
    userData["id"] = u.ID
    userData["first_name"] = u.FirstName
    userData["last_name"] = u.LastName

    c.JSON(http.StatusOK, userData)
}

func (au *Authenticate) Logout(c *gin.Context) {
    //check is the user is authenticated first
    metadata, err := au.tk.ExtractTokenMetadata(c.Request)
    if err != nil {
        c.JSON(http.StatusUnauthorized, "Unauthorized")
        return
    }
    //if the access token exist and it is still valid, then delete both the access token and the refresh token
    deleteErr := au.rd.DeleteTokens(metadata)
    if deleteErr != nil {
        c.JSON(http.StatusUnauthorized, deleteErr.Error())
        return
    }
    c.JSON(http.StatusOK, "Successfully logged out")
}

//Refresh is the function that uses the refresh_token to generate new pairs of refresh and access tokens.
func (au *Authenticate) Refresh(c *gin.Context) {
    mapToken := map[string]string{}
    if err := c.ShouldBindJSON(&mapToken); err != nil {
        c.JSON(http.StatusUnprocessableEntity, err.Error())
        return
    }
    refreshToken := mapToken["refresh_token"]

    //verify the token
    token, err := jwt.Parse(refreshToken, func(token *jwt.Token) (interface{}, error) {
        //Make sure that the token method conform to "SigningMethodHMAC"


        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
        }
        return []byte(os.Getenv("REFRESH_SECRET")), nil
    })
    //any error may be due to token expiration
    if err != nil {
        c.JSON(http.StatusUnauthorized, err.Error())
        return
    }
    //is token valid?
    if _, ok := token.Claims.(jwt.Claims); !ok && !token.Valid {
        c.JSON(http.StatusUnauthorized, err)
        return
    }
    //Since token is valid, get the uuid:
    claims, ok := token.Claims.(jwt.MapClaims)
    if ok && token.Valid {
        refreshUuid, ok := claims["refresh_uuid"].(string) //convert the interface to string
        if !ok {
            c.JSON(http.StatusUnprocessableEntity, "Cannot get uuid")
            return
        }
        userId, err := strconv.ParseUint(fmt.Sprintf("%.f", claims["user_id"]), 10, 64)
        if err != nil {
            c.JSON(http.StatusUnprocessableEntity, "Error occurred")
            return
        }
        //Delete the previous Refresh Token
        delErr := au.rd.DeleteRefresh(refreshUuid)
        if delErr != nil { //if any goes wrong
            c.JSON(http.StatusUnauthorized, "unauthorized")
            return
        }
        //Create new pairs of refresh and access tokens
        ts, createErr := au.tk.CreateToken(userId)
        if createErr != nil {
            c.JSON(http.StatusForbidden, createErr.Error())
            return
        }
        //save the tokens metadata to redis
        saveErr := au.rd.CreateAuth(userId, ts)
        if saveErr != nil {
            c.JSON(http.StatusForbidden, saveErr.Error())
            return
        }
        tokens := map[string]string{
            "access_token":  ts.AccessToken,
            "refresh_token": ts.RefreshToken,
        }
        c.JSON(http.StatusCreated, tokens)
    } else {
        c.JSON(http.StatusUnauthorized, "refresh token expired")
    }
}

 


6

运行程序

我们测试一下该应用。我们将连接路由,连接到数据库并启动应用程序。

在根目录中定义的main.go文件中完成。

package main

import (
    "food-app/infrastructure/auth"
    "food-app/infrastructure/persistence"
    "food-app/interfaces"
    "food-app/interfaces/fileupload"
    "food-app/interfaces/middleware"
    "github.com/gin-gonic/gin"
    "github.com/joho/godotenv"
    "log"
    "os"
)

func init() {
    //To load our environmental variables.
    if err := godotenv.Load(); err != nil {
        log.Println("no env gotten")
    }
}

func main() {

    dbdriver := os.Getenv("DB_DRIVER")
    host := os.Getenv("DB_HOST")
    password := os.Getenv("DB_PASSWORD")
    user := os.Getenv("DB_USER")
    dbname := os.Getenv("DB_NAME")
    port := os.Getenv("DB_PORT")

    //redis details
    redis_host := os.Getenv("REDIS_HOST")
    redis_port := os.Getenv("REDIS_PORT")
    redis_password := os.Getenv("REDIS_PASSWORD")


    services, err := persistence.NewRepositories(dbdriver, user, password, port, host, dbname)
    if err != nil {
        panic(err)
    }
    defer services.Close()
    services.Automigrate()

    redisService, err := auth.NewRedisDB(redis_host, redis_port, redis_password)
    if err != nil {
        log.Fatal(err)
    }

    tk := auth.NewToken()
    fd := fileupload.NewFileUpload()

    users := interfaces.NewUsers(services.User, redisService.Auth, tk)
    foods := interfaces.NewFood(services.Food, services.User, fd, redisService.Auth, tk)


    authenticate := interfaces.NewAuthenticate(services.User, redisService.Auth, tk)

    r := gin.Default()
    r.Use(middleware.CORSMiddleware()) //For CORS

    //user routes
    r.POST("/users", users.SaveUser)
    r.GET("/users", users.GetUsers)
    r.GET("/users/:user_id", users.GetUser)

    //post routes
    r.POST("/food", middleware.AuthMiddleware(), middleware.MaxSizeAllowed(8192000), foods.SaveFood)
    r.PUT("/food/:food_id", middleware.AuthMiddleware(), middleware.MaxSizeAllowed(8192000), foods.UpdateFood)
    r.GET("/food/:food_id", foods.GetFoodAndCreator)
    r.DELETE("/food/:food_id", middleware.AuthMiddleware(), foods.DeleteFood)
    r.GET("/food", foods.GetAllFood)

    //authentication routes
    r.POST("/login", authenticate.Login)
    r.POST("/logout", authenticate.Logout)
    r.POST("/refresh", authenticate.Refresh)


    //Starting the application
    app_port := os.Getenv("PORT") //using heroku host
    if app_port == "" {
        app_port = "8888" //localhost
    }
    log.Fatal(r.Run(":"+app_port))
}

 


其中的中间件也是定义在 interfaces 层。

package middleware

import (
    "bytes"
    "food-app/infrastructure/auth"
    "github.com/gin-gonic/gin"
    "io/ioutil"
    "net/http"
)

func AuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        err := auth.TokenValid(c.Request)
        if err != nil {
            c.JSON(http.StatusUnauthorized, gin.H{
                "status": http.StatusUnauthorized,
                "error":  err.Error(),
            })
            c.Abort()
            return
        }
        c.Next()
    }
}

func CORSMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
        c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
        c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
        c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, PATCH, DELETE")

        if c.Request.Method == "OPTIONS" {
            c.AbortWithStatus(204)
            return
        }
        c.Next()
    }
}

//Avoid a large file from loading into memory
//If the file size is greater than 8MB dont allow it to even load into memory and waste our time.
func MaxSizeAllowed(n int64) gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, n)
        buff, errRead := c.GetRawData()
        if errRead != nil {
            //c.JSON(http.StatusRequestEntityTooLarge,"too large")
            c.JSON(http.StatusRequestEntityTooLarge, gin.H{
                "status":     http.StatusRequestEntityTooLarge,
                "upload_err": "too large: upload an image less than 8MB",
            })
            c.Abort()
            return
        }
        buf := bytes.NewBuffer(buff)
        c.Request.Body = ioutil.NopCloser(buf)
    }
}

 

我们现在可以使用以下命令运行该应用:

go run main.go

 

转自 https://www.codenong.com/cs106393739/


posted @ 2023-03-07 19:37  韩梦芫  阅读(695)  评论(0编辑  收藏  举报