Day29 Gin with Async

前情提要

由於在POST /v1/users/ 时我们会需要透过smtp寄出通知信再回response,
导致执行时间如下,有过长的倾向

ironman-2021    | [GIN-debug] redirecting request 307: /v1/users/ --> /v1/users/
ironman-2021    | [GIN] 2021/10/13 - 15:07:51 | 200 |     2.804489s |   192.168.176.1 | POST     "/v1/users/"

我们可以发现加入smtp寄信功能後,response time竟然已经长达2.8秒了,也因此我们将加入Async来让smtp寄信变成非同步任务,以此来降低Client等待的时间,提升使用者体验。

Gin with Async

其实非常的简单,想要第一时间启动goroutine的话,其实只要在原本task function前面加上关键字go就行了

// CreateUser @Summary
// @Tags user
// @version 1.0
// @produce application/json
// @param language header string true "language"
// @param register body Register true "register"
// @Success 200 string successful return value
// @Router /v1/users [post]
func (u UsersController) CreateUser(c *gin.Context) {
	t := gi18n.New()
	var form Register
	bindErr := c.BindJSON(&form)

	lan := c.Request.Header.Get("language")
	if lan == "" {
		lan = "en"
	}
	t.SetLanguage(lan)
	if bindErr == nil {
		err := service.RegisterOneUser(form.Account, form.Password, form.Email)
		if err == nil {
			go service.Send("Register Notification", "Welcome to become our membership", form.Email)
			c.JSON(http.StatusOK, gin.H{
				"status": 1,
				"msg":    t.Translate(c, "Response_Success"),
				"data":   nil,
			})
		} else {
			c.JSON(http.StatusInternalServerError, gin.H{
				"status": -1,
				"msg":    "Register Failed" + err.Error(),
				"data":   nil,
			})
		}
	} else {
		c.JSON(http.StatusBadRequest, gin.H{
			"status": -1,
			"msg":    "Failed to parse register data" + bindErr.Error(),
			"data":   nil,
		})
	}
}

那完成後我们来测试一下response time吧!

ironman-2021    | [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
ironman-2021    | 
ironman-2021    | [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
ironman-2021    |  - using env: export GIN_MODE=release
ironman-2021    |  - using code:        gin.SetMode(gin.ReleaseMode)
ironman-2021    | 
ironman-2021    | [GIN-debug] GET    /hc                       --> main.SetRouter.func1 (5 handlers)
ironman-2021    | [GIN-debug] GET    /crawler                  --> main.SetRouter.func2 (5 handlers)
ironman-2021    | [GIN-debug] POST   /v1/users/                --> ironman-2021/app/controller.UsersController.CreateUser-fm (5 handlers)
ironman-2021    | [GIN-debug] GET    /v1/users/:id             --> ironman-2021/app/controller.UsersController.GetUser-fm (7 handlers)
ironman-2021    | [GIN-debug] POST   /v1/users/login           --> ironman-2021/app/controller.UsersController.AuthHandler-fm (5 handlers)
ironman-2021    | [GIN-debug] GET    /swagger/*any             --> github.com/swaggo/gin-swagger.CustomWrapHandler.func1 (5 handlers)
ironman-2021    | [GIN-debug] Listening and serving HTTP on :8080

ironman-2021    | [GIN-debug] redirecting request 307: /v1/users/ --> /v1/users/
ironman-2021    | [GIN] 2021/10/13 - 15:50:39 | 200 |     12.7741ms |   192.168.192.1 | POST     "/v1/users/"

这边我们可以发现response time从原本的2.8秒s 变成了 12.8ms了,这也证实我们的smtp寄信功能变成了个异步的任务!

Multiple Async Tasks...

但假设有多个异步任务呢? 如果我们除了通知信以外还要同时再寄出一份我们的规则表给使用者呢? 我们要如何确保每次都有将信件都寄出并且异步处理呢?

此时之前说过的sync.WaitGroup就能登场了

app/service/smtp.go

package service

import (
	"github.com/joho/godotenv"
	"github.com/sirupsen/logrus"
	"ironman-2021/app/middleware"
	"net/smtp"
	"os"
	"sync"
)

var wg sync.WaitGroup
func Send(title string, body string, to string) {
	envErr := godotenv.Load()
	if envErr != nil {
		panic(envErr)
	}

	from := os.Getenv("MAIL_USERNAME")
	pass := os.Getenv("MAIL_PASSWORD")
	port := os.Getenv("MAIL_PORT")
	server := os.Getenv("MAIL_SERVER")

	msg := "From: " + from + "\n" +
		"To: " + to + "\n" +
		"Subject: " + title + "\n" +
		body

	err := smtp.SendMail(server+":"+port,
		smtp.PlainAuth("", from, pass, server),
		from, []string{to}, []byte(msg))
	if err != nil {
		middleware.Logger().WithFields(logrus.Fields{
			"name": "Smtp",
		}).Error("error: ", err)
	}
	middleware.Logger().WithFields(logrus.Fields{
		"name": "Smtp",
	}).Info("Send from: ", from+", To: ", to)
	wg.Done()
}

func MultiSend(email string) {
	wg.Add(2)
	go Send("Register Notification", "Welcome to become our membership", email)
	go Send("Please review the rules", "Rules1:..........", email)
	wg.Wait()
	middleware.Logger().WithFields(logrus.Fields{
		"name": "Smtp",
	}).Info("Finished all tasks")
}
  • 我们写了一个MultiSend()来让API触发
  • MultiSend()里头透过WaitGroup来等待两个goroutine tasks都确定完成才写Log

/app/controller/user.go

最後一样在触发处使用关键字go来呼叫MultiSend()

// CreateUser @Summary
// @Tags user
// @version 1.0
// @produce application/json
// @param language header string true "language"
// @param register body Register true "register"
// @Success 200 string successful return value
// @Router /v1/users [post]
func (u UsersController) CreateUser(c *gin.Context) {
	t := gi18n.New()
	var form Register
	bindErr := c.BindJSON(&form)

	lan := c.Request.Header.Get("language")
	if lan == "" {
		lan = "en"
	}
	t.SetLanguage(lan)
	if bindErr == nil {
		err := service.RegisterOneUser(form.Account, form.Password, form.Email)
		if err == nil {
			go service.MultiSend(form.Email)
			c.JSON(http.StatusOK, gin.H{
				"status": 1,
				"msg":    t.Translate(c, "Response_Success"),
				"data":   nil,
			})
		} else {
			c.JSON(http.StatusInternalServerError, gin.H{
				"status": -1,
				"msg":    "Register Failed" + err.Error(),
				"data":   nil,
			})
		}
	} else {
		c.JSON(http.StatusBadRequest, gin.H{
			"status": -1,
			"msg":    "Failed to parse register data" + bindErr.Error(),
			"data":   nil,
		})
	}
}

Summary

这章节我们成功的结合goroutine来让Gin执行异步任务执行,也确实地改善了使用者体验。

这次的程序码我也会放在下方连结提供参考!

https://github.com/Neskem/Ironman-2021/tree/Day-29


<<:  JS Library 学习笔记:Three.js 初见面,在2D画面创造三维世界 (四)

>>:  Day 30 - 游艇网页专案完成後的优化方向 - ASP.NET Web Forms C#

[VSCodeVim] 推荐的Vim、VSCodeVim的参考资源

推荐的Vim、VSCodeVim的参考资源 [系列文目录] 这篇文章推荐几个Vim与VSCodeVi...

第 22 集:Bootstrap 客制化 utilities(下)

此篇延续 Bootstrap 客制化 Sass utilities(上)最後尚未介绍的 gener...

Day 15 讯息伫列的储存、接收及传送

关於讯息伫列怎麽去储存呢?大致分成下列两种: 1.系统池(system pool):如果能确定讯息伫...

[Day4]Fibonaccimal Base

今天一样来讲解一星的Fibonaccimal Base 附上程序码 import static ja...

DAY14 资料室--Vuex项目结构

前言 Vuex 并不会限制我们的代码结构,只是有三大原则需要遵守: 应用层级的状态应该集中到单个 s...