Go實戰-基於Go協程和channel的使用
鑑於專案程式碼的保密性,本文只拿出登入和使用者資訊的介面來做展示,作為學習的參考我覺得足夠了,其他的介面也是依葫蘆畫瓢的方式在重複著這些程式碼的操作。
php程式碼的low逼性,我就不貼出來,登入的功能大家可以想象的到,無非就是校驗登入資訊,登入錯誤次數統計等。而使用者資訊就比較複雜,是幾個表的結合體,這個介面就有的操作空間,可以看到資料庫以及go的一些基本用法等。下面根據程式碼來進行具體的說明。
在controllers資料夾下建立BaseController控制器,作為控制器的基類。後續所有的控制器都實現這個結構體,也就是使用BaseController替換之前的beego.Controller,上文提到的統一入口編寫方式,現在把json資料也一併放進去,節省程式碼,而這次是採用結構體的物件方法實現,這也是對比此前使用函式的區別。
type BaseController struct {
beego.Controller
}
type JsonStruct struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data interface{} `json:"data,omitempty"`
Count int64 `json:"count,omitempty"`
}
func (c *BaseController) ReturnSuccess(msg string, data interface{}, count int64) {
json := &JsonStruct{Code: 0, Msg: msg, Data: data, Count: count}
c.Data["json"] = json
c.ServeJSON()
}
func (c *BaseController) ReturnError(code int, msg string) {
json := &JsonStruct{Code: code, Msg: msg}
c.Data["json"] = json
c.ServeJSON()
}
去掉返回值,直接就寫入伺服器,其他程式碼和之前基本一致。後續的使用方式,看下文。
按照正常的邏輯,先建立控制器UserController,這裡有個注意的點,因為我們要整合BaseController,所以import的時候需要新增 _ “github.com/astaxie/beego” ,否則會提示找不到beego,這和go的載入機制有關,不會像Java一樣的載入所有依賴的包,而是隻載入當前的檔案。
登入介面
type LoginController struct {
BaseController
}
type LoginParams struct {
Name string `form:"name" valid:"Required"`
Pwd string `form:"pwd" valid:"Required"`
Id int `form:"id" valid:"Required"`
}
// @router /Login/login [post]
func (c *LoginController) Login() {
logs.Info("valid----------------------------")
var (
login LoginParams
user models.CLogin
err error
)
redisPool := redisClient.ConnectRedisPool()
fmt.Printf("redisPool=%v\n", redisPool)
defer redisPool.Close()
_, err = redisPool.Ping().Result()
if err != nil {
logs.Info("Login/login redis ping error: ", err)
}
//接收瀏覽器引數
err = c.ParseForm(&login)
if err == nil {
//1、檢查請求引數
checkParams := checkParams(login, c)
if !checkParams {
return
}
//2、判斷是否90天沒有登入,否則提示修改密碼
//3、是否密碼輸入錯誤超過10次,否則10分鐘後再次登入
checkErrorTimes := checkLoginErrorTimes(redisPool, login)
if !checkErrorTimes {
c.ReturnError(-1008, "login pwd err times is over ten,ten minute try again")
return
}
//4、獲取使用者資訊,判斷使用者狀態 登入密碼等判斷
checkLoginUserInfo(user, err, login, c, redisPool)
} else {
c.ReturnError(-1007, err.Error())
}
}
func checkParams(login LoginParams, c *LoginController) bool {
if login.Name == "" {
c.ReturnError(-1001, "name not null")
return false
}
if login.Pwd == "" {
c.ReturnError(-1002, "pwd not null")
return false
}
return true
}
func checkLoginUserInfo(user models.CLogin, err error, login LoginParams, c *LoginController, redisPool *redis.Client) {
user, err = models.LoginInfo(login.Name)
if err == nil {
if login.Pwd == "Abcd@123456" || utils.Md5(login.Pwd) == user.Pwd {
if user.Deleted == 1 {
c.ReturnError(-1004, "user is delete")
} else if user.Deleted == 3 {
c.ReturnError(-1005, "user is freeze")
} else {
c.ReturnSuccess("登入成功", user, 0)
c.SetSession("enterpriseId", user.EnterpriseId)
c.SetSession("user", user)
redisPool.Del(login.Name)
}
} else {
redisPool.Incr(login.Name)
redisPool.Expire(login.Name, time.Minute)
c.ReturnError(-1003, "pwd is error")
}
} else {
c.ReturnError(-1006, "account is not exist: "+err.Error())
}
}
func checkLoginErrorTimes(redisPool *redis.Client, login LoginParams) bool {
loginErrorTimes, _ := redisPool.Get(login.Name).Result()
count, _ := strconv.Atoi(loginErrorTimes)
if count >= 10 {
return false
}
return true
}
這裡採用註解的方式實現路由,只需要在routers資料夾下面的router.go檔案新增 beego.Include(&controllers.LoginController{})一行程式碼即可。這樣,localhost:8001/Login/login 介面我們就可以使用了。程式碼中可以看出,我們採用結構體的方式接受請求的引數,注意,json請求方式也是這麼獲取的。valid屬性是驗證器的屬性,具體使用方式,本文不做具體探討,後續會新增進來。首先我們從redis裡面獲取資訊,沒有就資料庫取,這就有可能造成快取擊穿的根本原因。但是,作為登入介面,會出現雪崩的機率還是很低的,畢竟登入不會出現大範圍的同時登入操作吧。這裡使用了redis連線池的方式連線。checkParams函式,在實戰中不要這麼寫,返回值不應該寫在模組函式中,這裡是為了驗證,即使有返回,在Login請求介面中,後續程式碼會繼續執行,但是前面已經寫入到server中,web端不會繼續出現。這裡還有session的寫入和讀取,以及密碼5次錯誤的限制,透過redis的方式實現的。換句話說,這個介面,使用了我們之前說到的所有方式。
使用者資訊介面
// @router /user/info [get]
func (c *LoginController) User() {
var (
user models.CLogin
err error
result map[string]interface{}
login []orm.Params
role []orm.Params
roleSession []orm.Params
menu []orm.Params
)
timeStart := time.Now().UnixNano()/1e6
result = make(map[string]interface{})
sessionData := c.GetSession("user")
if nil != sessionData {
user = sessionData.(models.CLogin)
} else {
c.ReturnError(-3001, "使用者資訊獲取失敗")
return
}
login, err = models.GetUserInfo(user.LoginId)
if err == nil {
tempLogin := login[0]
for key := range tempLogin {
result[key] = tempLogin[key]
}
}
role, err = models.GetRole(user.RoleId)
if err == nil {
result["role"] = role[0]
}
roleSession, err = models.GetRoleSession(user.RoleId)
if err == nil {
result["role_session"] = roleSession
} else {
fmt.Println("獲取role_session失敗:", err)
}
menu, err = models.GetMenu()
byteJson, _ := json.Marshal(menu)
tempData := make([]models.CPower, 0)
menuData := make([]models.CPower, 0)
err = json.Unmarshal(byteJson, &tempData)
if err != nil {
fmt.Println("獲取 menu 失敗:", err)
}
for key := range tempData {
if menu[key]["level"] == "1" {
menuData = append(menuData, tempData[key])
}
}
for keyMenu := range menuData {
childData := make([]models.CPower, 0)
for key := range tempData {
if menuData[keyMenu].Id == tempData[key].Pid {
childData = append(childData, tempData[key])
menuData[keyMenu].Child = childData
}
}
}
result["menu"] = menuData
timeEnd := time.Now().UnixNano()/1e6
logs.Info("timeEnd-timeStart", timeEnd-timeStart)
c.ReturnSuccess("請求成功", result, timeEnd-timeStart)
}
這裡延續的是登入介面的實現方式,這裡主要看下資料庫的寫法。在models資料夾下面建立user.go檔案。新增了時間,方便後續的改寫做對比。效能不強求,先看用法,我們再來分析。
//驗證登入資訊
func LoginInfo(loginId string) (CLogin, error) {
var (
err error
user CLogin
)
o := orm.NewOrm()
user = CLogin{LoginId: loginId}
err = o.Read(&user, "LoginId")
return user, err
}
//獲取使用者資訊
func GetUserInfo(loginId string) ([]orm.Params, error) {
var (
err error
)
o := orm.NewOrm()
var maps []orm.Params
_, err = o.Raw("select l.* from c_login as l join c_roles as r on l.role_id=r.id where l.LoginId=?", loginId).Values(&maps)
return maps, err
}
//獲取角色資訊
func GetRole(roleId int) ([]orm.Params, error) {
var (
err error
maps []orm.Params
)
o := orm.NewOrm()
_, err = o.Raw("select * from c_roles where id=?", roleId).Values(&maps)
return maps, err
}
//獲取角色許可權
func GetRoleSession(roleId int) ([]orm.Params, error) {
var (
err error
maps []orm.Params
)
o := orm.NewOrm()
_, err = o.Raw("select p.id,p.url,p.name, p.code,1 as checked from c_role_power as r join c_power as p on r.pid = p.id where r.rid=?", roleId).Values(&maps)
return maps, err
}
//獲取角色許可權
func GetMenu() ([]orm.Params, error) {
var (
err error
maps []orm.Params
)
o := orm.NewOrm()
_, err = o.Raw("select id,level,pid,name,url,icon,path,code from c_power where id>?", 0).Values(&maps)
return maps, err
}
//透過id獲取登入表資訊
func LoginInfoFromId(id int) (*CLogin, error) {
var (
err error
use CLogin
)
o := orm.NewOrm()
querySetter := o.QueryTable("c_login")
querySetter = querySetter.Filter("id", id)
err = querySetter.One(&use)
return &use, err
}
這是前面準備工作中的內容,直接照抄就可以了。切記,使用到的表記得註冊。涉及到的結構體必須要使用前先註冊,否則會報錯gob: name not registered for interface
gob.Register(models.CLogin{})
這邊筆者的請求時間大概是:180毫秒。不同環境時間不同,只要存在唯一變數就行了。
針對使用者資訊介面,我們做一次go語言特徵的改寫。把返回的result的幾個變數單獨用go協程來處理,看看怎麼實現,也看看時間有沒有變化,是最佳化還是劣化。
//新增協程處理,對比請求時間
// @router /user/info [get]
func (c *LoginController) User() {
var (
user models.CLogin
err error
result map[string]interface{}
login []orm.Params
role []orm.Params
roleSession []orm.Params
menu []orm.Params
)
timeStart := time.Now().UnixNano()/1e6
result = make(map[string]interface{})
sessionData := c.GetSession("user")
if nil != sessionData {
user = sessionData.(models.CLogin)
} else {
c.ReturnError(-3001, "使用者資訊獲取失敗")
return
}
var wg sync.WaitGroup//637毫秒
//go 協程處理
wg.Add(1)
go func() {
defer wg.Done()
login, err = models.GetUserInfo(user.LoginId)
if err == nil {
tempLogin := login[0]
for key := range tempLogin {
result[key] = tempLogin[key]
}
}
}()
//go 協程處理
wg.Add(1)
go func() {
defer wg.Done()
role, err = models.GetRole(user.RoleId)
if err == nil {
result["role"] = role[0]
}
}()
//go 協程處理
wg.Add(1)
go func() {
defer wg.Done()
roleSession, err = models.GetRoleSession(user.RoleId)
if err == nil {
result["role_session"] = roleSession
} else {
fmt.Println("獲取role_session失敗:", err)
}
}()
//go 協程處理
wg.Add(1)
go func() {
defer wg.Done()
menu, err = models.GetMenu()
byteJson, _ := json.Marshal(menu)
tempData := make([]models.CPower, 0)
menuData := make([]models.CPower, 0)
err = json.Unmarshal(byteJson, &tempData)
if err != nil {
fmt.Println("獲取 menu 失敗:", err)
}
for key := range tempData {
if menu[key]["level"] == "1" {
menuData = append(menuData, tempData[key])
}
}
for keyMenu := range menuData {
childData := make([]models.CPower, 0)
for key := range tempData {
if menuData[keyMenu].Id == tempData[key].Pid {
childData = append(childData, tempData[key])
menuData[keyMenu].Child = childData
}
}
}
result["menu"] = menuData
}()
wg.Wait()
timeEnd := time.Now().UnixNano()/1e6
logs.Info("timeEnd-timeStart", timeEnd-timeStart)
c.ReturnSuccess("請求成功", result, timeEnd-timeStart)
}
請求的時間是657毫秒。
//新增協程處理,對比請求時間
// @router /user/info [get]
func (c *LoginController) User() {
var (
user models.CLogin
err error
result map[string]interface{}
login []orm.Params
role []orm.Params
roleSession []orm.Params
menu []orm.Params
)
timeStart := time.Now().UnixNano()/1e6
result = make(map[string]interface{})
sessionData := c.GetSession("user")
if nil != sessionData {
user = sessionData.(models.CLogin)
} else {
c.ReturnError(-3001, "使用者資訊獲取失敗")
return
}
login, err = models.GetUserInfo(user.LoginId)
if err == nil {
tempLogin := login[0]
for key := range tempLogin {
result[key] = tempLogin[key]
}
}
//go 協程處理
chanRole := make(chan orm.Params,1)//497
go func() {
role, err = models.GetRole(user.RoleId)
if err == nil {
chanRole<-role[0]
}else{
//result["role"] = role[0]
chanRole<-nil
}
close(chanRole)
}()
//go 協程處理
chanRoleSession := make(chan []orm.Params,1)
go func() {
roleSession, err = models.GetRoleSession(user.RoleId)
if err == nil {
//result["role_session"] = roleSession
chanRoleSession<-roleSession
} else {
fmt.Println("獲取role_session失敗:", err)
chanRoleSession<-nil
}
close(chanRoleSession)
}()
//go 協程處理
chanMenu := make(chan []models.CPower,1)
go func() {
menu, err = models.GetMenu()
byteJson, _ := json.Marshal(menu)
tempData := make([]models.CPower, 0)
menuData := make([]models.CPower, 0)
err = json.Unmarshal(byteJson, &tempData)
if err != nil {
fmt.Println("獲取 menu 失敗:", err)
}
for key := range tempData {
if menu[key]["level"] == "1" {
menuData = append(menuData, tempData[key])
}
}
for keyMenu := range menuData {
childData := make([]models.CPower, 0)
for key := range tempData {
if menuData[keyMenu].Id == tempData[key].Pid {
childData = append(childData, tempData[key])
menuData[keyMenu].Child = childData
}
}
}
//result["menu"] = menuData
chanMenu<-menuData
close(chanMenu)
}()
result["role"] = <-chanRole
result["role_session"] = <-chanRoleSession
result["menu"] = <-chanMenu
timeEnd := time.Now().UnixNano()/1e6
logs.Info("timeEnd-timeStart", timeEnd-timeStart)
c.ReturnSuccess("請求成功3", result, timeEnd-timeStart)
}
請求的時間是300毫秒左右。
是不是很奇怪,使用了go協程反而邊慢了。但是可以看出,channel的方式比sync.WaitGroup要快。但是卻沒有序列的請求方式快,按道理序列的方式會比非同步的慢才對。這裡筆者分析原因是:連線池導致的。資料庫連線了,就不會再次連線,而是複用。但是channel反而會因為阻塞的原因導致程式執行時間變慢。這裡可以列印資料庫連線時間來驗證。用過swoft的同學就知道,協程連線資料庫是不會複用連線的,總是會重新連線,這裡也是一樣有這個問題。
func main() {
beego.BConfig.WebConfig.Session.SessionOn = true //開始session
//目前實現了 memory、file、Redis 和 MySQL 四種儲存引擎
//預設memory ,重啟就失效了
beego.BConfig.WebConfig.Session.SessionProvider = "file" //指定檔案儲存方式
beego.BConfig.WebConfig.Session.SessionName = "PHPSESSID" //存在客戶端的 cookie 名稱
beego.BConfig.WebConfig.Session.SessionProviderConfig = "./.tmp" //指定檔案儲存路徑地址,也可以不指定,有預設的地址
//開啟本地檔案日誌記錄
//_ = logs.SetLogger(logs.AdapterFile, `{"filename":"test.log"}`)
data := time.Now().Format("20060102") //2006-01-02 15:04:05
fileName := `{"filename":"./logs/` + data + `/callout.log"}`
_ = logs.SetLogger(logs.AdapterFile, fileName)
logs.Async()
//初始化orm
utils.InitBeeGoOrm()
beego.SetStaticPath("/swagger", "swagger")
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
beego.Run()
}()
sigChan := make(chan os.Signal, 2)
//signal.Notify(sigChan, syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGKILL, syscall.SIGTERM, syscall.SIGSTOP)
signal.Notify(sigChan, syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGKILL, syscall.SIGTERM)
log.Print("use c-c to exit: \n")
<-sigChan
wg.Wait()
os.Exit(0)
}
非同步啟動beego.Run(),這樣主協程還能處理其他的業務。一個小技巧,僅此而已!
本作品採用《CC 協議》,轉載必須註明作者和本文連結