iT邦幫忙

2021 iThome 鐵人賽

DAY 29
0
Modern Web

fmt.Println("從零開始的Golang生活")系列 第 29

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


上一篇
Day28 Gin with SMTP Server
下一篇
Day30 Gin with Drone
系列文
fmt.Println("從零開始的Golang生活")30

尚未有邦友留言

立即登入留言