Go實戰-基於Go協程和channel的使用

棋佈發表於2020-10-27

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 協議》,轉載必須註明作者和本文連結
歡迎轉載分享提意見,一起code

相關文章