現在經濟行情不行,很多公司裁員,身邊的朋友反饋現在工作不好找,當然不僅是程式設計師開發職業。開啟拉勾、boss直聘,php 招聘依然不少,但是工資大多在15k左右,想要上20k 要求相對高些。再看 go 崗位薪資則是20k的傾向,感覺 go 的未來幾年將是 php 的前幾年,學 go 似乎勢在改行。
因為公司技術棧是 PHP,想要在專案上實踐 go 那是不可能的事。看了 go 相關文件和書籍一兩遍,可是過不長時間可以又要拋腦後了。實踐是檢驗真理的惟一標準,之前給自己搭的部落格網站是借開源的專案來作二次開發的,一直嫌棄慢,這次利用學習 go 的機會對部落格網站進行重構。
網上搜集關於 go 的相關資料時,發現 go 也有幾個框架,其中提到目前 beego 是使用最多的,二次不說,先拿來用。
- 參考文件
- 路由檔案
路由檔案內容其實和 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 對公開變數的引用首欄位必須大寫,而且使用在寫字母作單詞分隔區分資料庫欄位的 "_"。
- 當從資料表中取出記錄時會對映到這些欄位,因為我們資料庫優化當中有一個規則是select中最好把需要的 field 帶上,這裡就涉及到能不能只取固定欄位的問題,即使能取出固定欄位,struct 裡的其他欄位就會有是預設值,感覺會干擾使用。
- user := Users{"Username":"song"},例項化一個 Users 時,如果未指定其他欄位值時有預設初始值,這就有個問題,如果我沒有設定手機號,插入表中的手機號就是 0 ,這就導致在使用手機號時要額外處理判斷一下。
- 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")
}
程式碼細節就不多說了,除了以上說的,還想吐槽一些痛點。
- beego 異常捕獲處理還沒有很好的封裝
- go 對沒有使用的變數,或未使用的包引用,都會報錯,在除錯總是會列印一些變數,沒有問題後會去掉,這就涉及到檔案要不斷的引用與註釋 fmt或者 beego 包了
- 習慣了其他語言設定預設值,go 函式或方法不能設定預設值,網上有方法說可以實現,但那隻能算是曲線救國
- 變數問題 ":=" 的形式,經常會忘記敲 ":"
本來有很多問題想吐槽的,可以夜深了,不想寫了,也暫時想不出其它問題了。寫這篇博文的目的希望大家分享經驗,相信這裡有很多 go 高手,分享一下你們學習的方法,學習的資料。希望站在你們的肩膀上,少走彎路,晚安!!!