当前环境
[!tip]
之前用的go 1.16很顺畅,这次用的go 1.18,遇到一些bug,都记录一下
GO111MODULE="on"
GOARCH="amd64"
GOBIN=""
GOCACHE="/Users/d4m1ts/Library/Caches/go-build"
GOENV="/Users/d4m1ts/Library/Application Support/go/env"
GOEXE=""
GOEXPERIMENT=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GOINSECURE=""
GOMODCACHE="/Users/d4m1ts/go/pkg/mod"
GONOPROXY=""
GONOSUMDB=""
GOOS="darwin"
GOPATH="/Users/d4m1ts/go"
GOPRIVATE=""
GOPROXY="https://goproxy.cn,direct"
GOROOT="/usr/local/go"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/usr/local/go/pkg/tool/darwin_amd64"
GOVCS=""
GOVERSION="go1.18.2"
GCCGO="gccgo"
GOAMD64="v1"
AR="ar"
CC="clang"
CXX="clang++"
CGO_ENABLED="1"
GOMOD="/dev/null"
GOWORK=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -arch x86_64 -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/fw/tddtsjp91wb9q64l5xt7jd540000gn/T/go-build2788719336=/tmp/go-build -gno-record-gcc-switches -fno-common"
- 注意:在
Beego
V2 之后,我们要求使用go mod
特性,请务必确保开启了go mod
特性,即设置了GO111MODULE=on
项目创建
bee api githubMonitor
创建后直接bee run
启动,可能会出现如下的错误
▶ 0003 Failed to build the application: # golang.org/x/sys/unix
../../../../pkg/mod/golang.org/x/sys@v0.0.0-20200930185726-fdedc70b468f/unix/syscall_darwin.1_13.go:29:3: //go:linkname must refer to declared function or variable
../../../../pkg/mod/golang.org/x/sys@v0.0.0-20200930185726-fdedc70b468f/unix/zsyscall_darwin_amd64.1_13.go:27:3: //go:linkname must refer to declared function or variable
../../../../pkg/mod/golang.org/x/sys@v0.0.0-20200930185726-fdedc70b468f/unix/zsyscall_darwin_amd64.1_13.go:40:3: //go:linkname must refer to declared function or variable
../../../../pkg/mod/golang.org/x/sys@v0.0.0-20200930185726-fdedc70b468f/unix/zsyscall_darwin_amd64.go:28:3: //go:linkname must refer to declared function or variable
../../../../pkg/mod/golang.org/x/sys@v0.0.0-20200930185726-fdedc70b468f/unix/zsyscall_darwin_amd64.go:43:3: //go:linkname must refer to declared function or variable
../../../../pkg/mod/golang.org/x/sys@v0.0.0-20200930185726-fdedc70b468f/unix/zsyscall_darwin_amd64.go:59:3: //go:linkname must refer to declared function or variable
../../../../pkg/mod/golang.org/x/sys@v0.0.0-20200930185726-fdedc70b468f/unix/zsyscall_darwin_amd64.go:75:3: //go:linkname must refer to declared function or variable
../../../../pkg/mod/golang.org/x/sys@v0.0.0-20200930185726-fdedc70b468f/unix/zsyscall_darwin_amd64.go:90:3: //go:linkname must refer to declared function or variable
../../../../pkg/mod/golang.org/x/sys@v0.0.0-20200930185726-fdedc70b468f/unix/zsyscall_darwin_amd64.go:105:3: //go:linkname must refer to declared function or variable
../../../../pkg/mod/golang.org/x/sys@v0.0.0-20200930185726-fdedc70b468f/unix/zsyscall_darwin_amd64.go:121:3: //go:linkname must refer to declared function or variable
../../../../pkg/mod/golang.org/x/sys@v0.0.0-20200930185726-fdedc70b468f/unix/zsyscall_darwin_amd64.go:121:3: too many errors
解决方案如下:
go get -u golang.org/x/sys
然后就可以正常启动了,创建的目录树如下:
.
├── conf
│ └── app.conf
├── controllers
│ ├── object.go
│ └── user.go
├── go.mod
├── go.sum
├── lastupdate.tmp
├── main.go
├── models
│ ├── object.go
│ └── user.go
├── routers
│ ├── commentsRouter_controllers.go
│ └── router.go
└── tests
└── default_test.go
简单分析
提供1个可访问的路由,http://127.0.0.1:8080/v1/user/login?username=astaxie&password=11111
,结合roters
中的文件分析一下就知道是个什么道理了。
在router.go
中创建了一个命名空间,所以前置路由就是 /v1/user
,有点类似于springboot中的@RequestMapping("/v1/user")
然后routers/
下还有另一个文件commentsRouter_controllers.go
,可以看到是对路由的划分
最后再看看Login
方法,参数来自于param.Make()
,到这里再理一下就差不多基础够了
个人总结
所以我们如果要开发的话,一般分为如下几个步骤
- 数据库ORM(当前数据的结构体一般写到对应的model里面)
- model层
- controller层
- router层
[!note]
注意:结构体里面的元素也尽量要大写开头,这样表示可以被其他地方调用,不然比如在序列化的时候,因为读取不到元素导致序列化出来为空!!!
其他功能
其他一些功能,如上传下载,需要的时候再回过头来补充,可参考官方文档:https://beego.gocn.vip/beego/zh/developing/web/router/ctrl_style/
ORM
为了方便使用,且项目比较小,所以准备先用sqlite来,避免给别人用还一大堆麻烦的配置
所有内容详细参考:https://beego.gocn.vip/beego/zh/developing/orm/
主键
可以用auto
显示指定一个字段为自增主键,该字段必须是 int, int32, int64, uint, uint32, 或者 uint64。
MyId int32 `orm:"auto"`
如果一个模型没有定义主键,那么 符合上述类型且名称为 Id
的模型字段将被视为自增主键。
如果不想使用自增主键,那么可以使用pk
设置为主键。
Name string `orm:"pk"`
自动更新时间
Created time.Time `orm:"auto_now_add;type(datetime)"`
Updated time.Time `orm:"auto_now;type(datetime)"`
- auto_now 每次 model 保存时都会对时间自动更新
- auto_now_add 第一次保存时才设置时间
对于批量的 update 此设置是不生效的
其他
null
数据库表默认为 NOT NULL
,设置 null 代表 ALLOW NULL
Name string `orm:"null"`
size
string 类型字段默认为 varchar(255)
设置 size 以后,db type 将使用 varchar(size)
Title string `orm:"size(60)"`
增删改查
官方文档包含内容:增删改查、批量增删改查、以及一些合并操作,如不存在就创建等
高级查询参考(好用,默认只返回1条数据):beego——高级查询
- Insert
- Update
- Delete
- Read
实例
实例一:(完整)
package models
import (
"context"
"fmt"
"github.com/beego/beego/v2/client/orm"
_ "github.com/mattn/go-sqlite3"
"time"
)
// 结构体一般写道对应的model里面
type TestUser struct {
ID int `orm:"column(id)"`
Name string `orm:"column(name);size(60)"`
Age int `orm:"column(age);null"`
Created time.Time `orm:"auto_now_add;type(datetime)"`
Updated time.Time `orm:"auto_now;type(datetime)"`
}
func init(){
// 注册模型(参数为表名,会自动转换为 test_user)
orm.RegisterModel(new(TestUser))
// 参数1 数据库的别名,用来在 ORM 中切换数据库使用
// 参数2 driverName
// 参数3 对应的链接字符串
// 参数4(可选) 设置最大空闲连接 orm.MaxIdleConnections(maxIdle)
// 参数5(可选) 设置最大数据库连接 (go >= 1.2) orm.MaxOpenConnections(maxConn)
_ = orm.RegisterDataBase("default", "sqlite3", "data.db")
}
func OrmUsage() {
// 自动创建表
orm.RunSyncdb("default", false, true)
// 创建orm对象
o := orm.NewOrm()
// 增
user := new(TestUser)
user.Name = "mike"
o.Insert(user)
// 查
user1 := new(TestUser)
user1.ID = 1
o.Read(user1)
fmt.Print(user1.Name) // mike
// 事务
o.DoTx(
func(ctx context.Context, txOrm orm.TxOrmer) error {
// data
user := new(TestUser)
user.Name = "test_transaction"
// insert data
// Using txOrm to execute SQL
_, e := txOrm.Insert(user)
// if e != nil the transaction will be rollback
// or it will be committed
return e
})
}
效果如下:
实例二:(可能自用比较多)
给ormer实例化了在其他地方用。
package models
import (
"github.com/beego/beego/v2/client/orm"
_ "github.com/mattn/go-sqlite3"
)
var Ormer orm.Ormer
func init(){
orm.RegisterModel(new(TaskInfo))
_ = orm.RegisterDataBase("default", "sqlite3", "data.db")
_ = orm.RunSyncdb("default", false, true)
Ormer = orm.NewOrm()
}
实例三:(高级查询)
构建多个or like 语句
func FetchTask(keyword string) {
var results []TaskInfo
qs := Ormer.QueryTable(new(TaskInfo))
condition := orm.NewCondition()
condition1 := condition.Or("name__icontains", keyword).Or("keyword__icontains", keyword).Or("ignoreUser__icontains", keyword).Or("ignoreRepo__icontains", keyword).Or("noticeEmail__icontains", keyword)
filter := qs.SetCond(condition1).OrderBy("id")
filter.All(&results)
fmt.Print(results)
}
注意事项
- ormer在更新数据库时,如插入数据,传入的不是结构体变量,而是结构体变量指针,如果传入的格式不对,会提示
cannot use non-ptr model struct
日志
很简单,更多的操作直接看文档,简单的记录一下
https://beego.gocn.vip/beego/zh/developing/logs/#%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B
import (
"github.com/beego/beego/v2/core/logs"
)
logs.SetLogger(logs.AdapterConsole)
logs.Debug("my book is bought in the year of ", 2016)
logs.Info("this %s cat is %v years old", "yellow", 3)
logs.Warn("json is a type of kv like", map[string]int{"key": 2016})
logs.Error(1024, "is a very", "good game")
logs.Critical("oh,crash")
数据类型验证
有时候需要验证用户输入的内容,如不能为空、只能是数字等,但是如果每次都用if
去判断,工作量确实有点大,所以beego提供了一个额外的组件validation
来辅助验证
go get github.com/beego/beego/v2/core/validation
实例
import (
"github.com/beego/beego/v2/core/validation"
"log"
)
type User struct {
Name string
Age int
}
func main() {
u := User{"man", 40}
valid := validation.Validation{}
valid.Required(u.Name, "name")
valid.MaxSize(u.Name, 15, "nameMax")
valid.Range(u.Age, 0, 18, "age")
if valid.HasErrors() {
// 如果有错误信息,证明验证没通过
// 打印错误信息
for _, err := range valid.Errors {
log.Println(err.Key, err.Message)
}
}
// or use like this
if v := valid.Max(u.Age, 140, "age"); !v.Ok {
log.Println(v.Error.Key, v.Error.Message)
}
// 定制错误信息
minAge := 18
valid.Min(u.Age, minAge, "age").Message("少儿不宜!")
// 错误信息格式化
valid.Min(u.Age, minAge, "age").Message("%d不禁", minAge)
}
发起web请求
beego也给提供了一个httplib
,可以直接用,底层也是net/http
没啥区别,封装了一层吧
go get github.com/beego/beego/v2/client/httplib
过滤器
也可以理解为拦截器,就是在访问路由前执行一个操作,比如安全检查、登陆检查等。
loginFilter.go
package filters
import (
"github.com/beego/beego/v2/server/web/context"
"githubMonitor/controllers"
)
var FilterUser = func(ctx *context.Context) {
token := ctx.Input.Header("token")
if token == "" {
result := controllers.ReturnRes{
Code: 401,
Message: "未登录",
}
ctx.Output.JSON(result, true, false)
}
}
router.go
import "githubMonitor/filters"
beego.InsertFilter("/*", beego.BeforeRouter, filters2.FilterUser)
web.InsertFilter(pattern string, pos int, filter FilterFunc, opts ...FilterOpt)
InsertFilter 函数的三个必填参数,一个可选参数
- pattern 路由规则,可以根据一定的规则进行路由,如果你全匹配可以用
*
- position 执行 Filter 的地方,五个固定参数如下,分别表示不同的执行过程
- BeforeStatic 静态地址之前
- BeforeRouter 寻找路由之前
- BeforeExec 找到路由之后,开始执行相应的 Controller 之前
- AfterExec 执行完 Controller 逻辑之后执行的过滤器
- FinishRouter 执行完逻辑之后执行的过滤器
- filter filter 函数
type FilterFunc func(*context.Context)
- opts
- web.WithReturnOnOutput: 设置 returnOnOutput 的值(默认 true), 如果在进行到此过滤之前已经有输出,是否不再继续执行此过滤器,默认设置为如果前面已有输出(参数为true),则不再执行此过滤器
- web.WithResetParams: 是否重置 filters 的参数,默认是 false,因为在 filters 的 pattern 和本身的路由的 pattern 冲突的时候,可以把 filters 的参数重置,这样可以保证在后续的逻辑中获取到正确的参数,例如设置了
/api/*
的 filter,同时又设置了/api/docs/*
的 router,那么在访问/api/docs/swagger/abc.js
的时候,在执行 filters 的时候设置:splat
参数为docs/swagger/abc.js
,但是如果不清楚 filter 的这个路由参数,就会在执行路由逻辑的时候保持docs/swagger/abc.js
,如果设置了 true,就会重置:splat
参数. - web.WithCaseSensitive: 是否大小写敏感。
补充
restful返回struct
- 定义结构体
// ReturnRes 返回的结果模板,注意开头要大写
type ReturnRes struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
- 返回结果
fetchResult := models.FetchTask(query)
if fetchResult != nil {
result.Code = 200
result.Message = "查询成功"
result.Data = fetchResult
} else {
result.Code = 0
result.Message = "查询失败"
}
o.Data["json"] = result
o.ServeJSON()
- 浏览器结果
复杂的json
复杂的内容,可以拆分成很多个struct
,然后再层层嵌套,一定要注意类型不能错!!!
数据库值类型
在数据库里面,只能指定以下有的类型,如 https://beego.gocn.vip/beego/zh/developing/orm/model.html#sqlite3
如果不在这些类型里面,用自己创建的一些类型如结构体,可能会抛出如下异常
field: models.GitInfo.Data, unsupport field type &{%!s(int=0) []}, may be miss setting tag
我的解决办法,就是序列化成string
多个init()
当一个package
中同时存在多个init()
时,一定要注意顺序问题,不然很有可能因为先后问题出现空指针的问题
因此强烈建议只要1个init()
反序列化坑点
有时候,传入的数据是正确的,但是一直反序列化不出来!!!
可以分析一下这个数据前面的数据是否正常,可能是前面反序列化异常,导致后面都以默认数据返回了,大坑。。。
404
分析后,发现存在正则表达式的路由匹配,所以编写好对应的controller
后,像下面这样注册就可以了,记得放到路由最下面
beego.Router("/", &controllers.NotFoundController{}, "*:NotFound")
beego.Router("/*.*", &controllers.NotFoundController{}, "*:NotFound")
设置返回响应头
在控制器中设置,输入是Input
,输出就是Output
o.Ctx.Output.Header("Server", "GitHub Monitor")
推荐批量包装输出,抽象一个方法出来
// WrapOutput 包装输出
func WrapOutput(output context.BeegoOutput) *context.BeegoOutput {
output.Header("Server", "GitHub Monitor")
return &output
}
附加一:验证码
beego自带的验证码,实在看不懂,所以找了个其他的,也挺好用
- 安装
go get -u github.com/mojocn/base64Captcha
- 完整生成验证码和验证实例
package main
import (
"fmt"
"github.com/mojocn/base64Captcha"
"log"
)
//configJsonBody json request body.
type configJsonBody struct {
Id string
CaptchaType string
VerifyValue string
DriverAudio *base64Captcha.DriverAudio
DriverString *base64Captcha.DriverString
DriverChinese *base64Captcha.DriverChinese
DriverMath *base64Captcha.DriverMath
DriverDigit *base64Captcha.DriverDigit
}
var store = base64Captcha.DefaultMemStore
// GetCaptcha base64Captcha create return id, b64s, err
func GetCaptcha() (string, string, error) {
// Driver配置
// {
// ShowLineOptions: [],
// CaptchaType: "string",
// Id: '',
// VerifyValue: '',
// DriverAudio: {
// Length: 6,
// Language: 'zh'
// },
// DriverString: {
// Height: 60,
// Width: 240,
// ShowLineOptions: 0,
// NoiseCount: 0,
// Source: "1234567890qwertyuioplkjhgfdsazxcvbnm",
// Length: 6,
// Fonts: ["wqy-microhei.ttc"],
// BgColor: {R: 0, G: 0, B: 0, A: 0},
// },
// DriverMath: {
// Height: 60,
// Width: 240,
// ShowLineOptions: 0,
// NoiseCount: 0,
// Length: 6,
// Fonts: ["wqy-microhei.ttc"],
// BgColor: {R: 0, G: 0, B: 0, A: 0},
// },
// DriverChinese: {
// Height: 60,
// Width: 320,
// ShowLineOptions: 0,
// NoiseCount: 0,
// Source: "设想,你在,处理,消费者,的音,频输,出音,频可,能无,论什,么都,没有,任何,输出,或者,它可,能是,单声道,立体声,或是,环绕立,体声的,,不想要,的值",
// Length: 2,
// Fonts: ["wqy-microhei.ttc"],
// BgColor: {R: 125, G: 125, B: 0, A: 118},
// },
// DriverDigit: {
// Height: 80,
// Width: 240,
// Length: 5,
// MaxSkew: 0.7,
// DotCount: 80
// }
// },
// blob: "",
// loading: false
// }
// 调试配置,生成的种类
var param = configJsonBody{
Id: "",
CaptchaType: "string",
VerifyValue: "",
DriverAudio: &base64Captcha.DriverAudio{},
DriverString: &base64Captcha.DriverString{
Length: 4,
Height: 60,
Width: 240,
ShowLineOptions: 2,
NoiseCount: 0,
Source: "1234567890qwertyuioplkjhgfdsazxcvbnm",
},
DriverChinese: &base64Captcha.DriverChinese{},
DriverMath: &base64Captcha.DriverMath{
Height: 60,
Width: 240,
ShowLineOptions: 0,
NoiseCount: 0,
},
DriverDigit: &base64Captcha.DriverDigit{},
}
var driver base64Captcha.Driver
//create base64 encoding captcha
switch param.CaptchaType {
case "audio":
driver = param.DriverAudio
case "string":
driver = param.DriverString.ConvertFonts()
case "math":
driver = param.DriverMath.ConvertFonts()
case "chinese":
driver = param.DriverChinese.ConvertFonts()
default:
driver = param.DriverDigit
}
c := base64Captcha.NewCaptcha(driver, store)
return c.Generate()
// id, b64s, err := c.Generate()
// body := map[string]interface{}{"code": 1, "data": b64s, "captchaId": id, "msg": "success"}
// if err != nil {
// body = map[string]interface{}{"code": 0, "msg": err.Error()}
// }
// var _ = body
// // log.Println(body)
// log.Println(1)
// log.Println(id)
// log.Printf("store =%+v\n", store)
}
// base64Captcha verify
func VerifyCaptcha(id, VerifyValue string) bool {
return store.Verify(id, VerifyValue, true)
}
func main() {
id, b64s, err := GetCaptcha()
fmt.Println(b64s) // 图形验证码base64
if err != nil {
return
}
var _ = b64s
log.Println("id =", id)
log.Println("VerifyValue =", store.Get(id, true))
result := VerifyCaptcha(id, store.Get(id, true))
log.Println("result =", result)
}