go 初學-想說

雪花飄發表於2019-08-30

現在經濟行情不行,很多公司裁員,身邊的朋友反饋現在工作不好找,當然不僅是程式設計師開發職業。開啟拉勾、boss直聘,php 招聘依然不少,但是工資大多在15k左右,想要上20k 要求相對高些。再看 go 崗位薪資則是20k的傾向,感覺 go 的未來幾年將是 php 的前幾年,學 go 似乎勢在改行。

因為公司技術棧是 PHP,想要在專案上實踐 go 那是不可能的事。看了 go 相關文件和書籍一兩遍,可是過不長時間可以又要拋腦後了。實踐是檢驗真理的惟一標準,之前給自己搭的部落格網站是借開源的專案來作二次開發的,一直嫌棄慢,這次利用學習 go 的機會對部落格網站進行重構。

網上搜集關於 go 的相關資料時,發現 go 也有幾個框架,其中提到目前 beego 是使用最多的,二次不說,先拿來用。

go 初學-想說

go 初學-想說

  • 參考文件

beego
go

  • 路由檔案

路由檔案內容其實和 lavaral 框架的路由檔案型別,這裡只是簡單示例

package routers
import (
    "blog-go/controllers"
    "blog-go/admin"
    "github.com/astaxie/beego"
)
func init() {
    beego.Include(&controllers.UsersController{})
    admin := beego.NewNamespace("/admin",
        beego.NSNamespace("/article",
            beego.NSInclude(
                &admin.ArticleController{},
            ),
        ),
    )
    beego.AddNamespace(admin)
}
  • 基礎控制器

因為 go 為強型別語言,初始 go 在型別處理這方面踩了好多坑

package admin

import (
    "github.com/astaxie/beego"
    "fmt"
    "strings"
    "blog-go/services"
)
const INVALID_PARAM = 40000
const NOT_FOUND = 40001

type BaseController struct {
    beego.Controller
}

func (this *BaseController) Prepare() {
    this.checkAuth()
}

// 返回key為字串的陣列
func (this *BaseController) endStrLines(data map[string]interface{}) {
    this.Data["json"] = map[string]interface{} {"code": 200, "msg": "ok", "data": data}
    this.ServeJSON()
    this.StopRun()
}
// 返回key為數字的陣列
func (this *BaseController) endIntLines(data map[int]interface{}) {
    this.Data["json"] = map[string]interface{} {"code": 200, "msg": "ok", "data": data}
    this.ServeJSON()
    this.StopRun()

}

// 返回字串或數字
func (this *BaseController) endLine(data interface{}) {
    this.Data["json"] = map[string]interface{} {"code": 200, "msg": "ok", "data": data}
    this.ServeJSON()
    this.StopRun()

}

// 返回錯誤資訊
func (this *BaseController) error(code int, msg error) {
    this.Data["json"] = map[string]interface{} {"code": code, "msg": fmt.Sprintf("%s", msg), "data": nil}
    this.ServeJSON()
    this.StopRun()

}

// 是否POST提交
func (this *BaseController) isPost() bool {
    return this.Ctx.Request.Method == "POST"
}

//獲取使用者IP地址
func (this *BaseController) getClientIp() string {
    if p := this.Ctx.Input.Proxy(); len(p) > 0 {
        return p[0]
    }
    return this.Ctx.Input.IP()
}

func (this *BaseController) checkAuth() bool {
    authString := this.Ctx.Input.Header("Authorization")
    beego.Debug("AuthString:", authString)
    kv := strings.Split(authString, " ")
    if authString == "" || len(kv) != 2 || kv[0] != "Bearer" {
        beego.Debug("no auth")
        return false
    } 
    us := new(services.UserSecretService)
    return us.VerifyToken(kv[1])
}
  • 基礎 model

這裡注意因為 timezone 為 Asia/Shanghai ,所以需要 url.QueryEscape 處理 "/"。
在 O 例項化之前需要需要 RegisterModel,那如果想要全域性使用 "O" ,則需要在這裡全部註冊 model,我的想法是按需註冊,用到 model 的時候再去註冊,一次性全部註冊會不會有效能消耗?

package models

import (
    "github.com/astaxie/beego/orm"
    _ "github.com/go-sql-driver/mysql"
    "github.com/astaxie/beego"
    "fmt"
    "net/url"
)
var O orm.Ormer

// 初始化資料庫連線
func init() {
    // set default database
    driver := fmt.Sprintf("%v:%v@(%v:%v)/%v?charset=%v&loc=%v", beego.AppConfig.String("user"), 
    beego.AppConfig.String("pass"), 
    beego.AppConfig.String("host"), 
    beego.AppConfig.String("port"), 
    beego.AppConfig.String("db"), 
    beego.AppConfig.String("charset"),
    url.QueryEscape(beego.AppConfig.String("timezone")))
    // orm.RegisterDataBase("default", "mysql", beego.BConfig.user + ":"@(192.168.99.171)/blog?charset=utf8")
    orm.RegisterDataBase("default", "mysql", driver)
    orm.Debug = true
    orm.RegisterModel(new(Users))
    orm.RegisterModel(new(UserSecret))
    O = orm.NewOrm()
}
  • 資料庫 model

這裡想要吐槽的是資料庫欄位對映的問題,每一個 struct 結構體對映一張表。因為 go 對公開變數的引用首欄位必須大寫,而且使用在寫字母作單詞分隔區分資料庫欄位的 "_"。

  1. 當從資料表中取出記錄時會對映到這些欄位,因為我們資料庫最佳化當中有一個規則是select中最好把需要的 field 帶上,這裡就涉及到能不能只取固定欄位的問題,即使能取出固定欄位,struct 裡的其他欄位就會有是預設值,感覺會干擾使用。
  2. user := Users{"Username":"song"},例項化一個 Users 時,如果未指定其他欄位值時有預設初始值,這就有個問題,如果我沒有設定手機號,插入表中的手機號就是 0 ,這就導致在使用手機號時要額外處理判斷一下。
  3. struct 結構體取出來的資料欄位都是大寫駝峰,前端如果也使用這種寫法感覺特別彆扭,所以資料返回前端時需要另外的轉化函式,將大寫駝峰轉化為小寫加下劃線。
    package models
    import (
    "time"
    "github.com/astaxie/beego/orm"
    "regexp"
    )
    type Users struct {
    Id       int64
    Username string `valid:"Required"`
    Pwd string  `valid:"Required"`
    Mobile  uint64 `valid:"Required;Mobile"`
    Email string `valid:"Required;Email"`
    IsActive uint8
    IsAdmin uint8
    CreatedAt time.Time `orm:"auto_now_add;type(datetime)"`
    UpdatedAt time.Time `orm:"auto_now;type(datetime)"`
    }
    func (m *Users) Tablename() string {
    return "users"
    }
    func (m *Users) Create(u *Users) (int64, error) {
    return O.Insert(u)
    }
    // 根據刪除郵箱或手機號獲取使用者
    func (m *Users) FindByAccount(accountName string) (*Users, error) {
    cond := orm.NewCondition()
    pattern := "\\d+"
    result, _ := regexp.MatchString(pattern, accountName)
    cond_and := cond.And("is_active", 1)
    cond_or := cond.Or("email", accountName)
    if result && len(accountName) == 11 {
        cond_or = cond_or.Or("mobile", accountName)
    }
    condition := cond.AndCond(cond_and).AndCond(cond_or)
    model,err := m.FindOneByCond(condition)
    return model, err
    }
    // 根據條件獲取一條記錄
    func (m *Users) FindOneByCond(cond *orm.Condition) (*Users, error) {
    user := &Users{}
    err := O.QueryTable(m.Tablename()).SetCond(cond).One(user)
    return user, err
    }
    // 根據郵箱獲取
    func (m *Users) FindOneByEmail(email string) (user *Users, err error) {
    cond := orm.NewCondition()
    condition := cond.And("is_active", 1).And("email", email)
    user, err = m.FindOneByCond(condition)
    return user, err
    }
    // 根據手機號獲取
    func (m *Users) FindOneByMobile(mobile uint64) (user *Users, err error) {
    cond := orm.NewCondition()
    condition := cond.And("is_active", 1).And("mobile", mobile)
    user, err = m.FindOneByCond(condition)
    return user, err
    }
  • 使用者註冊及登入程式碼
package services

import (
    "blog-go/models"
    "github.com/astaxie/beego"
    "golang.org/x/crypto/bcrypt"
    "github.com/astaxie/beego/validation"
    "errors"

)

var uModel *models.Users
func init() {
    uModel = new(models.Users)
}
type UserService struct {}

// 新增使用者
func (s *UserService) AddUser(username, pwd, email string, mobile uint64) (int64, error) {
    user, err := uModel.FindOneByEmail(email)
    if user.Id > 0 {
        return user.Id, errors.New("郵箱已經存在")
    }
    user, err = uModel.FindOneByMobile(mobile)
    if (user.Id > 0) {
        return user.Id, errors.New("手機號已經存在")
    }

    model := &models.Users{}
    model.Username = username
    hash, err := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost)
    if err != nil {
        return 0, err
    }

    model.Pwd = string(hash)
    model.Email = email
    model.IsActive = 1
    model.IsAdmin = 0
    model.Mobile = mobile
    valid := validation.Validation{}
    valid.Valid(model)
    if valid.HasErrors() {
        beego.Warning(valid.Errors)
        return 0, errors.New("引數錯誤")
    }
    id, err := uModel.Create(model)
    return id, err
}

// 使用者登入
func (s *UserService) Login(accountName, pwd string) (*models.Users, error) {
    user, err := uModel.FindByAccount(accountName)
    if user.Id > 0 {
        err = bcrypt.CompareHashAndPassword([]byte(user.Pwd), []byte(pwd))
        beego.Debug(err)
        if err != nil {
            err = errors.New("密碼錯誤")
        }

    } else {
        err = errors.New("使用者不存在")
    }
    return user, err
}
package services

import (
    "blog-go/models"
    "blog-go/utils"
    "github.com/dgrijalva/jwt-go"
    "time"
    "errors"
    "github.com/astaxie/beego"
    "strings"
    "encoding/json"
)

var m *models.UserSecret
func init() {
    m = new(models.UserSecret)
}
type UserSecretService struct {}

// 生成jwt token
func (s *UserSecretService) GenerateToken(id int64) (string, string, error) {
    claims := make(jwt.MapClaims)
    claims["user_id"] = id
    claims["exp"] = time.Now().In(utils.Tz).Add(time.Hour * 24).Unix() 
  token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    secret := utils.GetRandomString(32)
    beego.Warning(secret)
  // 使用自定義字串加密 and get the complete encoded token as a string
    tokenString, err := token.SignedString([]byte(secret))
    if err != nil {
        beego.Error("token生成失敗", err)
        return "","", errors.New("token生成失敗")
    }
    return tokenString, secret, nil
}

//建立
func (s *UserSecretService) Create(secret string, userId int64, ip string) (int64, error){
    expiredAt := time.Now().Add(60*30*1e9)
    beego.Debug(expiredAt)
    model := models.UserSecret{Secret: secret, Ip: ip, UserId: userId, ExpiredAt: expiredAt}
    return m.Create(&model)
}

// token驗證
func (s *UserSecretService) VerifyToken(tokenString string) bool {
    segments := strings.Split(tokenString, ".")
    res, _ := jwt.DecodeSegment(segments[1])
    var mapResult map[string]int
    //使用 json.Unmarshal(data []byte, v interface{})進行轉換,返回 error 資訊
    if err := json.Unmarshal([]byte(res), &mapResult); err != nil {
            return false
    }
    userId := mapResult["user_id"]
    beego.Debug(userId)
    model, err:= m.FindLastByUserId(userId)
    if err != nil {
        return false
    }
    secret := model.Secret
    //驗證token合法性
    token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
        return []byte(secret), nil
    })
    if (err != nil || !token.Valid) {
        return false
    }
    //更新過期時間為30分鐘後
    model.ExpiredAt = time.Now().In(utils.Tz).Add(60*30*1e9)
    beego.Debug(model.ExpiredAt)
    m.UpdateExpired(model)
    return true
}
package models

import (
    "time"
    "github.com/astaxie/beego/orm"
)

type UserSecret struct {
    Secret  string `orm:"pk"`
    CreatedAt time.Time `orm:"auto_now_add;type(datetime)"`
    UpdatedAt time.Time `orm:"auto_now;type(datetime)"`
    ExpiredAt time.Time `orm:"auto_now;type(datetime)"`
    Ip string
    UserId int64
}

func (m *UserSecret) Tablename() string {
    return "user_secret"
}

// 建立記錄
func (m *UserSecret) Create(model *UserSecret) (int64, error) {
    return O.Insert(model)
}

func (m *UserSecret) FindLastByUserId(userId int) (*UserSecret, error) {
    cond := orm.NewCondition()
    condition := cond.And("user_id", userId)
    model := new(UserSecret)
    err := O.QueryTable(m.Tablename()).SetCond(condition).OrderBy("-updated_at").One(model)
    return model, err
}

func (m *UserSecret) UpdateExpired(model *UserSecret) {
    O.Update(model, "expired_at")
}

程式碼細節就不多說了,除了以上說的,還想吐槽一些痛點。

  1. beego 異常捕獲處理還沒有很好的封裝
  2. go 對沒有使用的變數,或未使用的包引用,都會報錯,在除錯總是會列印一些變數,沒有問題後會去掉,這就涉及到檔案要不斷的引用與註釋 fmt或者 beego 包了
  3. 習慣了其他語言設定預設值,go 函式或方法不能設定預設值,網上有方法說可以實現,但那隻能算是曲線救國
  4. 變數問題 ":=" 的形式,經常會忘記敲 ":"

本來有很多問題想吐槽的,可以夜深了,不想寫了,也暫時想不出其它問題了。寫這篇博文的目的希望大家分享經驗,相信這裡有很多 go 高手,分享一下你們學習的方法,學習的資料。希望站在你們的肩膀上,少走彎路,晚安!!!

本作品採用《CC 協議》,轉載必須註明作者和本文連結
雪花飄

相關文章