Use Go to Complete User Business Logic

brief introduction

In the previous section, you've learned roughly how to use Gin to read and write requests.
This section is the practice, complete a user business logic processing.

It mainly includes the following functions:

  • Create user
  • delete user
  • Update user
  • Query User List
  • Query the information of the specified user

This section is the core part, because the main function of this project is realized in this part.

Routing overview

This part of the code changes a lot, after all, to complete the above functions will add a lot of code.
First, look at routing. router.go adds a lot of routing definitions.

u := g.Group("/v1/user")
{
  u.GET("", user.List)
  u.POST("", user.Create)
  u.GET("/:id", user.Get)
  u.PUT("/:id", user.Save)
  u.PATCH("/:id", user.Update)
  u.DELETE("/:id", user.Delete)
}

um := g.Group("/v1/username")
{
  um.GET("/:name", user.GetByName)
}

From the routing definition, we can see that user creation, update, query and deletion are all defined.
In addition, the function of obtaining user list is defined.

A little explanation is user updates. Two methods are defined here, one using PUT method.
The other uses the PATCH method. The difference between the two is that the former is completely updated and needs to provide all of them.
Fields, the new user data will complete the replacement of the old user, except that the ID remains unchanged. The latter is a partial update.
More flexible, just provide the fields you want to change.

When defining API interfaces, you usually need to control versions. In general, the first routing directory is
Version number. This best practice is also followed here.

Define handler

All user-related handlers are defined in the handler/user/directory.

Let's first look at how to create new users.

The steps to create a new user are as follows:

  • Getting parameters from the request
  • Calibration parameter
  • Encryption cipher
  • Stored in database
  • Return response

If getting parameters from the request has been described in the previous section,
The model binding used here.

Calibration parameter

Gin's model bindings also have validation, a common one is to specify the necessary fields.
Gin itself supports verification using go-playground/validator.v8.

I use gopkg.in/go-playground/validator.v9 here.

Firstly, the user model is defined in model/user.go, including the method of validation.

// Define user structure
type UserModel struct {
    BaseModel
    Username string `json:"username" gorm:"column:username;not null" binding:"required" validate:"min=1,max=32"`
    Password string `json:"password" gorm:"column:password;not null" binding:"required" validate:"min=5,max=128"`
}

// Validation field
func (u *UserModel) Validate() error {
    validate := validator.New()
    return validate.Struct(u)
}

When using model binding, we need to distinguish one thing: the structure of API interface and the data model itself
Data model refers more to the structure stored in the database, which is related to how to design the table structure.
And the core data model. The parameter structure in the request serves the API interface itself, that is, the API interface.
What parameters are needed?

You can view the structure of all user API interfaces in handler/user/user.go.

Encrypted Password and Data Storage

Many operations have been encapsulated in the user model, so in handler, only function calls are needed.
It's OK to decide if there's a mistake. Try not to put too much code in handler, usually just need it.
It's enough to show a clear process flow. The specific implementation is put in other files.

The process of encrypting and storing user data is very clear.

// Encryption cipher
if err := u.Encrypt(); err != nil {
  handler.SendResponse(ctx, errno.New(errno.ErrEncrypt, err), nil)
  return
}

// Insert user into database
if err := u.Create(); err != nil {
  handler.SendResponse(ctx, errno.New(errno.ErrDatabase, err), nil)
  return
}

Finally, the user name is returned as a response. So far, a handler that creates the user is complete.

Other handler s

User deletion and ID-based or name-based queries are also relatively easy, not to elaborate.

Get the user list

Let's look at an interface to get a list of users.

Following the preceding principles, large chunks of code should not be placed directly in handler.
Here we put the concrete implementation in a package called service, specifically
The service.ListUser function.

In fact, when defining the user model, a method of obtaining the same name from the database has been defined.
User List and Number of Users. Why not use it directly?

This is because only very general functions are usually defined in the model, that is, data is extracted from the database.
Do not do very specific data processing. Designed to specific business operations, should be handled elsewhere.

Look at the service.ListUser function specifically. The main function is to get users from the database.
The data is expanded to include fields corresponding to the model.UserInfo structure.

// Business Processing Function to Get User List
func ListUser(username string, offset, limit int) ([]*model.UserInfo, uint, error) {
    infos := make([]*model.UserInfo, 0)
    users, count, err := model.ListUser(username, offset, limit)
    if err != nil {
        return nil, count, err
    }

    ids := []uint{}
    for _, user := range users {
        ids = append(ids, user.ID)
    }

    wg := sync.WaitGroup{}
    userList := model.UserList{
        Lock:  new(sync.Mutex),
        IdMap: make(map[uint]*model.UserInfo, len(users)),
    }

    errChan := make(chan error, 1)
    finished := make(chan bool, 1)

    // Parallel transformation
    for _, u := range users {
        wg.Add(1)
        go func(u *model.UserModel) {
            defer wg.Done()

            shortId, err := util.GenShortID()
            if err != nil {
                errChan <- err
                return
            }

            // Lock when updating data to maintain consistency
            userList.Lock.Lock()
            defer userList.Lock.Unlock()

            userList.IdMap[u.ID] = &model.UserInfo{
                ID:        u.ID,
                Username:  u.Username,
                SayHello:  fmt.Sprintf("Hello %s", shortId),
                Password:  u.Password,
                CreatedAt: util.TimeToStr(&u.CreatedAt),
                UpdatedAt: util.TimeToStr(&u.UpdatedAt),
                DeletedAt: util.TimeToStr(u.DeletedAt),
            }
        }(u)
    }

    go func() {
        wg.Wait()
        close(finished)
    }()

    // Waiting for completion
    select {
    case <-finished:
    case err := <-errChan:
        return nil, count, err
    }

    for _, id := range ids {
        infos = append(infos, userList.IdMap[id])
    }

    return infos, count, nil
}

In fact, in order to speed up the processing, goroutine is used for parallel processing:

In the ListUser() function, the sync package is used for parallel queries to reduce response latency. In practical development, after querying data, it is usually necessary to do some processing on the data, such as the ListUser() function returns a sayHello field to each user record. SayHello simply outputs a Hello shortId string, where shortId is generated by util.GenShortId() (GenShortId implementation is detailed in demo07/util/util.go). Such operations usually increase the response delay of API. If there are too many items in the list, each record in the list should do some similar logical processing, which will make the whole API delay very high, so I usually do parallel processing in actual development. According to the author's experience, the effect has been greatly improved.

Readers should have noticed that in ListUser() implementations, there are some codes such as sync.Mutex and IdMap. Using sync.Mutex is because in concurrent processing, updating the same variable to ensure data consistency usually requires lock processing.

IdMap is used because the list to be queried usually needs to be sorted in chronological order. Generally, the list after database query has been sorted, but in order to reduce the delay, concurrency is used in the program, which will disrupt the sorting. Therefore, IdMap is used to record the sequence before concurrent processing, and reset after processing.

There are a lot of knowledge points in it, involving goroutine, lock and synchronization, range, select, chanel.
I think I can see it several times more.

Update user

There are two ways to update users, complete update and partial update, which correspond to PUT and PATCH respectively.

For the user model, the operation under GORM is very convenient.

// Save the user and update all fields
func (u *UserModel) Save() error {
    return DB.Self.Save(u).Error
}

// Update fields using map[string]interface {} format
func (u *UserModel) Update(data map[string]interface{}) error {
    return DB.Self.Model(u).Updates(data).Error
}

Emphasis is placed on the stage of data acquisition and validation.

For complete updates, except that the ID is known,
The other parts are the same as when creating users. They are also validating fields and encrypting passwords, and finally updating the database.

For partial updates, we need to guess the passed fields and process each field one by one.
A new validation method validateandapteuser is written.

// Validate AndUpdate User validates the map structure and encrypts the password (if it exists)
func ValidateAndUpdateUser(data *map[string]interface{}) error {
    validate := validator.New()
    usernameTag, _ := util.GetTag(UserModel{}, "Username", "validate")
    passwordTag, _ := util.GetTag(UserModel{}, "Password", "validate")
    // Verify username
    if username, ok := (*data)["username"]; ok {
        if err := validate.Var(username, usernameTag); err != nil {
            return err
        }
    }
    // Verify password
    if password, ok := (*data)["password"]; ok {
        if err := validate.Var(password, passwordTag); err != nil {
            return err
        }
        // Encryption cipher
        newPassword, err := auth.Encrypt(password.(string))
        if err != nil {
            return err
        }
        (*data)["password"] = newPassword
    }

    return nil
}

It's a little tedious to verify every field.

summary

This is the core logic of the user. At first glance, this part of the code changes a lot.
So far, most of the core code has been completed. This API server is
It can start and receive the call.

Of course, there are still many imperfections, such as authority authentication, interface documents, etc.
It will be introduced in the next article.

The current part of the code

As version v0.7.0

Keywords: Go Database JSON

Added by prasadharischandra on Sat, 12 Oct 2019 07:16:37 +0300