go 初学-想说

现在经济行情不行,很多公司裁员,身边的朋友反馈现在工作不好找,当然不仅是程序员开发职业。打开拉勾、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 协议》,转载必须注明作者和本文链接

雪花飘

本帖由系统于 8个月前 自动加精
讨论数量: 6

赞分享,问下BaseController前面加和不加区别是什么

func (this *BaseController) Prepare() {
    this.checkAuth()
}
8个月前 评论

@lovecn 你说的加 "" 吗?"" 是指针类型,为地址引用,不加是值复制,如果不加 "*",就无法实现实例方法的调用。

8个月前 评论

同样是深圳php,学go。人生好难哦。 :joy:

1周前 评论
wangchunbo

加油

6天前 评论
TonyF

php 学go +1

4天前 评论

请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!