go 初学-想说
现在经济行情不行,很多公司裁员,身边的朋友反馈现在工作不好找,当然不仅是程序员开发职业。打开拉勾、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 高手,分享一下你们学习的方法,学习的资料。希望站在你们的肩膀上,少走弯路,晚安!!!
本作品采用《CC 协议》,转载必须注明作者和本文链接
赞分享,问下BaseController前面加和不加区别是什么
@lovecn 你说的加 "" 吗?"" 是指针类型,为地址引用,不加是值复制,如果不加 "*",就无法实现实例方法的调用。
mark,写的很好哦
同样是深圳php,学go。人生好难哦。 :joy:
加油
php 学go +1
test