Beego
Beego
本文主要是对官方文档搬运
中文新版文档网站 https://beego.gocn.vip/
快速开始
首先确保自己已经安装了 GO,版本在 1.16 之上,同时设置了 GOPATH 环境变量,并且将 GOPATH/bin 加入到了环境变量。
我们建议你直接使用最新的稳定版本,因为我们会尽量保持使用最新版本的 Go 版本。
如果你已经安装好了开发环境,那么你可以考虑使用我们的快速安装脚本。
Mac or Linux
在控制台直接执行以下语句:
bash <(curl -s https://raw.githubusercontent.com/beego/beego-doc/main/scripts/quickstart.sh)
如果没有安装curl
,那么可以使用wget
,执行:
bash <(wget -qO- https://raw.githubusercontent.com/beego/beego-doc/main/scripts/quickstart.sh)
如果你无法使用这两个命令,那么可以尝试直接下载这两个文件,而后执行。
Windows
使用curl
:
bash <(curl -s https://raw.githubusercontent.com/beego/beego-doc/main/scripts/quickstart.bat)
如果你没有安装curl
命令,可以使用wget
命令:
bash <(wget -qO- https://raw.githubusercontent.com/beego/beego-doc/main/scripts/quickstart.bat)
如果你无法通过命令下载脚本,可以尝试自己下载脚本而后执行。
手动安装
在这一章节,我们会使用到go get
命令,如果你还不熟悉它,我们建议你可以先阅读Go get
千万记住,如果你遇到了网络问题,或者超时问题,请务必确保自己设定了GOPROXY
代理。
安装 Bee
我们来看一下手动如何安装Bee
。在命令行里面执行:
go get -u github.com/beego/bee/v2@master
而后运行
bee version
你将看到类似输出:
| ___ \
| |_/ / ___ ___
| ___ \ / _ \ / _ \
| |_/ /| __/| __/
\____/ \___| \___| v2.0.2
├── Beego : Beego is not installed. Please do consider installing it first: https://github.com/beego/beego/v2. If you are using go mod, and you don't install the beego under $GOPATH/src/github.com/beego, just ignore this.
├── GoVersion : go1.16
├── GOOS : linux
├── GOARCH : amd64
├── NumCPU : 12
├── GOPATH : /home/xxx/go
├── GOROOT : /home/aaa/bbb/go
├── Compiler : gc
└── Published : 2020-12-16
创建项目
执行:
bee new hello
这会在当前目录下创建一个名叫hello
的文件夹。
而后进入文件夹:
cd hello
而后我们执行go mod tidy命令,来生成go.sum
文件。
go mod tidy
而后,我们尝试启动:
bee run
如果没有错误的话,你会看到类似的输出:
2021/03/31 23:29:19 SUCCESS ▶ 0004 Built Successfully!
2021/03/31 23:29:19 INFO ▶ 0005 Restarting 'hello'...
2021/03/31 23:29:19 SUCCESS ▶ 0006 './hello' is running...
2021/03/31 23:29:22.016 [I] [parser.go:413] generate router from comments
2021/03/31 23:29:22.016 [I] [server.go:241] http server Running on http://:8080
如果你启动不成功,请先确认自己的 8080
端口是否被占用了。
Bee 工具
bee 工具是一个为了协助快速开发 Beego 项目而创建的项目,通过 bee 你可以很容易的进行 Beego 项目的创建、热编译、开发、测试、和部署。
bee 工具的安装
你可以通过如下的方式安装 bee 工具:
go get -u github.com/beego/bee/v2
安装完之后,bee
可执行文件默认存放在 $GOPATH/bin
里面,所以你需要把 $GOPATH/bin
添加到你的环境变量中,才可以进行下一步。
如果你本机设置了
GOBIN
,那么上面的bee
命令就会安装到GOBIN
目录下,所以我们需要在环境变量中添加相关的配置信息,如何添加可以查看这篇文档: bee 环境变量配置
bee 工具命令详解
我们在命令行输入 bee
,可以看到如下的信息:
Bee is a Fast and Flexible tool for managing your Beego Web Application.
Usage:
bee command [arguments]
The commands are:
version show the bee & beego version
migrate run database migrations
api create an api application base on beego framework
bale packs non-Go files to Go source files
new create an application base on beego framework
run run the app which can hot compile
pack compress an beego project
fix Fixes your application by making it compatible with newer versions of Beego
dlv Start a debugging session using Delve
dockerize Generates a Dockerfile for your Beego application
generate Source code generator
hprose Creates an RPC application based on Hprose and Beego frameworks
pack Compresses a Beego application into a single file
rs Run customized scripts
run Run the application by starting a local development server
server serving static content over HTTP on port
Use bee help [command] for more information about a command.
new
命令
new
命令是新建一个 Web 项目,我们在命令行下执行 bee new <项目名>
就可以创建一个新的项目。但是注意该命令必须在 $GOPATH/src
下执行。最后会在 $GOPATH/src
相应目录下生成如下目录结构的项目:
bee new myproject
[INFO] Creating application...
/gopath/src/myproject/
/gopath/src/myproject/conf/
/gopath/src/myproject/controllers/
/gopath/src/myproject/models/
/gopath/src/myproject/static/
/gopath/src/myproject/static/js/
/gopath/src/myproject/static/css/
/gopath/src/myproject/static/img/
/gopath/src/myproject/views/
/gopath/src/myproject/conf/app.conf
/gopath/src/myproject/controllers/default.go
/gopath/src/myproject/views/index.tpl
/gopath/src/myproject/main.go
13-11-25 09:50:39 [SUCC] New application successfully created!
myproject
├── conf
│ └── app.conf
├── controllers
│ └── default.go
├── main.go
├── models
├── routers
│ └── router.go
├── static
│ ├── css
│ ├── img
│ └── js
├── tests
│ └── default_test.go
└── views
└── index.tpl
8 directories, 4 files
api
命令
上面的 new
命令是用来新建 Web 项目,不过很多用户使用 beego 来开发 API 应用。所以这个 api
命令就是用来创建 API 应用的,执行命令之后如下所示:
bee api apiproject
create app folder: /gopath/src/apiproject
create conf: /gopath/src/apiproject/conf
create controllers: /gopath/src/apiproject/controllers
create models: /gopath/src/apiproject/models
create tests: /gopath/src/apiproject/tests
create conf app.conf: /gopath/src/apiproject/conf/app.conf
create controllers default.go: /gopath/src/apiproject/controllers/default.go
create tests default.go: /gopath/src/apiproject/tests/default_test.go
create models object.go: /gopath/src/apiproject/models/object.go
create main.go: /gopath/src/apiproject/main.go
这个项目的目录结构如下:
apiproject
├── conf
│ └── app.conf
├── controllers
│ └── object.go
│ └── user.go
├── docs
│ └── doc.go
├── main.go
├── models
│ └── object.go
│ └── user.go
├── routers
│ └── router.go
└── tests
└── default_test.go
从上面的目录我们可以看到和 Web 项目相比,少了 static 和 views 目录,多了一个 test 模块,用来做单元测试的。
同时,该命令还支持一些自定义参数自动连接数据库创建相关 model 和 controller: bee api [appname] [-tables=""] [-driver=mysql] [-conn="root:<password>@tcp(127.0.0.1:3306)/test"]
如果 conn 参数为空则创建一个示例项目,否则将基于链接信息链接数据库创建项目。
run
命令
我们在开发 Go 项目的时候最大的问题是经常需要自己手动去编译再运行,bee run
命令是监控 beego 的项目,通过 fsnotify (opens new window)监控文件系统。但是注意该命令必须在 $GOPATH/src/appname
下执行。 这样我们在开发过程中就可以实时的看到项目修改之后的效果:
bee run
13-11-25 09:53:04 [INFO] Uses 'myproject' as 'appname'
13-11-25 09:53:04 [INFO] Initializing watcher...
13-11-25 09:53:04 [TRAC] Directory(/gopath/src/myproject/controllers)
13-11-25 09:53:04 [TRAC] Directory(/gopath/src/myproject/models)
13-11-25 09:53:04 [TRAC] Directory(/gopath/src/myproject)
13-11-25 09:53:04 [INFO] Start building...
13-11-25 09:53:16 [SUCC] Build was successful
13-11-25 09:53:16 [INFO] Restarting myproject ...
13-11-25 09:53:16 [INFO] ./myproject is running...
我们打开浏览器就可以看到效果 http://localhost:8080/
:
如果我们修改了 Controller
下面的 default.go
文件,我们就可以看到命令行输出:
13-11-25 10:11:20 [EVEN] "/gopath/src/myproject/controllers/default.go": DELETE|MODIFY
13-11-25 10:11:20 [INFO] Start building...
13-11-25 10:11:20 [SKIP] "/gopath/src/myproject/controllers/default.go": CREATE
13-11-25 10:11:23 [SKIP] "/gopath/src/myproject/controllers/default.go": MODIFY
13-11-25 10:11:23 [SUCC] Build was successful
13-11-25 10:11:23 [INFO] Restarting myproject ...
13-11-25 10:11:23 [INFO] ./myproject is running...
刷新浏览器我们看到新的修改内容已经输出。
pack
命令
pack
目录用来发布应用的时候打包,会把项目打包成 zip 包,这样我们部署的时候直接把打包之后的项目上传,解压就可以部署了:
bee pack
app path: /gopath/src/apiproject
GOOS darwin GOARCH amd64
build apiproject
build success
exclude prefix:
exclude suffix: .go:.DS_Store:.tmp
file write to `/gopath/src/apiproject/apiproject.tar.gz`
我们可以看到目录下有如下的压缩文件:
rwxr-xr-x 1 astaxie staff 8995376 11 25 22:46 apiproject
-rw-r--r-- 1 astaxie staff 2240288 11 25 22:58 apiproject.tar.gz
drwxr-xr-x 3 astaxie staff 102 11 25 22:31 conf
drwxr-xr-x 3 astaxie staff 102 11 25 22:31 controllers
-rw-r--r-- 1 astaxie staff 509 11 25 22:31 main.go
drwxr-xr-x 3 astaxie staff 102 11 25 22:31 models
drwxr-xr-x 3 astaxie staff 102 11 25 22:31 tests
bale
命令
这个命令目前仅限内部使用,具体实现方案未完善,主要用来压缩所有的静态文件变成一个变量申明文件,全部编译到二进制文件里面,用户发布的时候携带静态文件,包括 js、css、img 和 views。最后在启动运行时进行非覆盖式的自解压。
version
命令
这个命令是动态获取 bee、beego 和 Go 的版本,这样一旦用户出现错误,可以通过该命令来查看当前的版本
$ bee version
bee :1.2.2
beego :1.4.2
Go :go version go1.3.3 darwin/amd64
需要注意的是,目前 bee version
会试图输出当前beego
的版本。
但是目前这个实现有点坑,它是通过读取$GOPATH/src/astaxie/beego
下的文件来进行的。
这意味着,如果你本地并没有下载beego
源码,或者放置的位置不对,bee
都无法输出beego
的版本信息。
generate
命令
这个命令是用来自动化的生成代码的,包含了从数据库一键生成 model,还包含了 scaffold 的,通过这个命令,让大家开发代码不再慢
generate scaffold
bee generate scaffold [scaffoldname] [-fields=""] [-driver=mysql] [-conn="root:@tcp(127.0.0.1:3306)/test"]
The generate scaffold command will do a number of things for you.
-fields: a list of table fields. Format: field:type, ...
-driver: [mysql | postgres | sqlite], the default is mysql
-conn: the connection string used by the driver, the default is root:@tcp(127.0.0.1:3306)/test
example: bee generate scaffold post -fields="title:string,body:text"
generate model
bee generate model [modelname] [-fields=""]
generate RESTful model based on fields
-fields: a list of table fields. Format: field:type, ...
generate controller
bee generate controller [controllerfile]
generate RESTful controllers
generate view
bee generate view [viewpath]
generate CRUD view in viewpath
generate migration
bee generate migration [migrationfile] [-fields=""]
generate migration file for making database schema update
-fields: a list of table fields. Format: field:type, ...
generate docs
bee generate docs
generate swagger doc file
generate routers
generate routers
是从原来beego
中剥离出来的功能。在早期,beego
的项目必须在启动的时候才会触发生成路由文件。
现在我们把这个东西挪了出来,那么用户可以有更好的控制感。
bee generate routers [-ctrlDir=/path/to/controller/directory] [-routersFile=/path/to/routers/file.go] [-routersPkg=myPackage]
-ctrlDir: the directory contains controllers definition. Bee scans this directory and its subdirectory to generate routers info
-routersFile: output file. All generated routers info will be output into this file.
If file not found, Bee create new one, or Bee truncates it.
The default value is "routers/commentRouters.go"
-routersPkg: package declaration.The default value is "routers".
When you pass routersFile parameter, youd better pass this parameter
generate test
bee generate test [routerfile]
generate testcase
generate appcode
bee generate appcode [-tables=""] [-driver=mysql] [-conn="root:@tcp(127.0.0.1:3306)/test"] [-level=3]
generate appcode based on an existing database
-tables: a list of table names separated by ',', default is empty, indicating all tables
-driver: [mysql | postgres | sqlite], the default is mysql
-conn: the connection string used by the driver.
default for mysql: root:@tcp(127.0.0.1:3306)/test
default for postgres: postgres://postgres:postgres@127.0.0.1:5432/postgres
-level: [1 | 2 | 3], 1 = models; 2 = models,controllers; 3 = models,controllers,router
generate router
migrate
命令
这个命令是应用的数据库迁移命令,主要是用来每次应用升级,降级的 SQL 管理。
bee migrate [-driver=mysql] [-conn="root:@tcp(127.0.0.1:3306)/test"]
run all outstanding migrations
-driver: [mysql | postgresql | sqlite], the default is mysql
-conn: the connection string used by the driver, the default is root:@tcp(127.0.0.1:3306)/test
bee migrate rollback [-driver=mysql] [-conn="root:@tcp(127.0.0.1:3306)/test"]
rollback the last migration operation
-driver: [mysql | postgresql | sqlite], the default is mysql
-conn: the connection string used by the driver, the default is root:@tcp(127.0.0.1:3306)/test
bee migrate reset [-driver=mysql] [-conn="root:@tcp(127.0.0.1:3306)/test"]
rollback all migrations
-driver: [mysql | postgresql | sqlite], the default is mysql
-conn: the connection string used by the driver, the default is root:@tcp(127.0.0.1:3306)/test
bee migrate refresh [-driver=mysql] [-conn="root:@tcp(127.0.0.1:3306)/test"]
rollback all migrations and run them all again
-driver: [mysql | postgresql | sqlite], the default is mysql
-conn: the connection string used by the driver, the default is root:@tcp(127.0.0.1:3306)/test
dockerize
命令
这个命令可以通过生成 Dockerfile 文件来实现 docker 化你的应用。
例子:
生成一个以 1.6.4 版本 Go 环境为基础镜像的 Dockerfile,并暴露 9000 端口:
$ bee dockerize -image="library/golang:1.6.4" -expose=9000
______
| ___ \
| |_/ / ___ ___
| ___ \ / _ \ / _ \
| |_/ /| __/| __/
\____/ \___| \___| v1.6.2
2016/12/26 22:34:54 INFO ▶ 0001 Generating Dockerfile...
2016/12/26 22:34:54 SUCCESS ▶ 0002 Dockerfile generated.
更多帮助信息可执行bee help dockerize
.
bee 工具配置文件
你可能已经注意到,在 bee 工具的源码目录下有一个 bee.json
文件,这个文件是针对 bee 工具的一些行为进行配置。该功能还未完全开发完成,不过其中的一些选项已经可以使用:
"version": 0
:配置文件版本,用于对比是否发生不兼容的配置格式版本。"go_install": false
:如果你的包均使用完整的导入路径(例如:github.com/user/repo/subpkg
),则可以启用该选项来进行go install
操作,加快构建操作。"watch_ext": []
:用于监控其它类型的文件(默认只监控后缀为.go
的文件)。"dir_structure":{}
:如果你的目录名与默认的 MVC 架构的不同,则可以使用该选项进行修改。"cmd_args": []
:如果你需要在每次启动时加入启动参数,则可以使用该选项。"envs": []
:如果你需要在每次启动时设置临时环境变量参数,则可以使用该选项。
配置模块
配置模块是基础模块之一,对不同类型的配置文件提供了一种抽象。该章节内容都可以在配置模块例子(opens new window)
Beego 目前支持 INI、XML、JSON、YAML 格式的配置文件解析,也支持以 etcd
作为远程配置中心。默认采用了 INI 格式解析,用户可以通过简单的配置就可以获得很大的灵活性。
它们拥有的方法都是一样的,具体可以参考Config API (opens new window)。主要方法有:
// Configer defines how to get and set value from configuration raw data.
type Configer interface {
// support section::key type in given key when using ini type.
Set(key, val string) error
// support section::key type in key string when using ini and json type; Int,Int64,Bool,Float,DIY are same.
String(key string) (string, error)
// get string slice
Strings(key string) ([]string, error)
Int(key string) (int, error)
Int64(key string) (int64, error)
Bool(key string) (bool, error)
Float(key string) (float64, error)
// support section::key type in key string when using ini and json type; Int,Int64,Bool,Float,DIY are same.
DefaultString(key string, defaultVal string) string
// get string slice
DefaultStrings(key string, defaultVal []string) []string
DefaultInt(key string, defaultVal int) int
DefaultInt64(key string, defaultVal int64) int64
DefaultBool(key string, defaultVal bool) bool
DefaultFloat(key string, defaultVal float64) float64
// DIY return the original value
DIY(key string) (interface{}, error)
GetSection(section string) (map[string]string, error)
Unmarshaler(prefix string, obj interface{}, opt ...DecodeOption) error
Sub(key string) (Configer, error)
OnChange(key string, fn func(value string))
SaveConfigFile(filename string) error
}
这里有一些使用的注意事项:
- 所有的
Default*
方法,在key
不存在,或者查找的过程中,出现error
,都会返回默认值; DIY
直接返回对应的值,而没有做任何类型的转换。当你使用这个方法的时候,你应该自己确认值的类型。只有在极少数的情况下你才应该考虑使用这个方法;GetSection
会返回section
所对应的部分配置。section
如何被解释,取决于具体的实现;Unmarshaler
会尝试用当且配置的值来初始化obj
。需要注意的是,prefix
的概念类似于section
;Sub
类似与GetSection
,都是尝试返回配置的一部分。所不同的是,GetSection
将结果组织成map
,而Sub
将结果组织成Config
实例;OnChange
主要用于监听配置的变化。对于大部分依赖于文件系统的实现来说,都不支持。具体而言,我们设计这个主要是为了考虑支持远程配置;SaveConfigFile
尝试将配置导出成为一个文件;- 某些实现支持分段式的
key
。比如说a.b.c
这种,但是,并不是所有的实现都支持,也不是所有的实现都采用.
作为分隔符。这是一个历史遗留问题,为了保留兼容性,我们无法在这方面保持一致。
Web 模块封装了配置模块,可以参考Web 配置
初始化方法
大致上有两种用法:
- 使用
config.XXXX
:这是依赖于全局配置实例 - 使用
Configer
实例
全局实例
Beego 默认会解析当前应用下的 conf/app.conf
文件,后面就可以通过config
包名直接使用:
import (
"github.com/beego/beego/v2/core/config"
"github.com/beego/beego/v2/core/logs"
)
func main() {
val, _ := config.String("name")
logs.Info("auto load config name is", val)
}
也可以手动初始化全局实例,以指定不同的配置类型,例如说启用etcd
:
config.InitGlobalInstance("etcd", "etcd address")
使用Configer
实例
如果要从多个源头读取配置,或者说自己不想依赖于全局配置,那么可以自己初始化一个配置实例:
func main() {
cfg, err := config.NewConfig("ini", "my_config.ini")
if err != nil {
logs.Error(err)
}
val, _ := cfg.String("appname")
logs.Info("auto load config name is", val)
}
环境变量支持
配置文件解析支持从环境变量中获取配置项,配置项格式:${环境变量}
。例如下面的配置中优先使用环境变量中配置的 runmode 和 httpport,如果有配置环境变量 ProRunMode 则优先使用该环境变量值。如果不存在或者为空,则使用 "dev" 作为 runmode。例如使用 INI 的时候指定环境变量:
runmode = "${ProRunMode||dev}"
httpport = "${ProPort||9090}"
支持的格式
注意,所以的相对文件路径,都是从你的工作目录开始计算! 其次,除了默认的 INI 格式,其余格式都需要采用匿名引入的方式引入对应的包。
NI 格式
INI 是配置模块的默认格式。同时它支持使用include
的方式,加载多个配置文件。
app.ini:
appname = beepkg
httpaddr = "127.0.0.1"
httpport = 9090
include "app2.ini"
app2.ini:
runmode ="dev"
autorender = false
recoverpanic = false
viewspath = "myview"
[dev]
httpport = 8080
[prod]
httpport = 8088
[test]
httpport = 8888
func main() {
cfg, err := config.NewConfig("ini", "app.ini")
if err != nil {
logs.Error(err)
}
val, _ := cfg.String("appname")
logs.Info("auto load config name is", val)
}
JSON
JSON 只需要指定格式,并且不要忘了使用匿名引入的方式引入 JSON 的实现:
import (
"github.com/beego/beego/v2/core/config"
// 千万不要忘了
_ "github.com/beego/beego/v2/core/config/json"
"github.com/beego/beego/v2/core/logs"
)
var (
ConfigFile = "./app.json"
)
func main() {
err := config.InitGlobalInstance("json", ConfigFile)
if err != nil {
logs.Critical("An error occurred:", err)
panic(err)
}
val, _ := config.String("name")
logs.Info("load config name is", val)
}
YAML
别忘了匿名引入 YAML 的实现!
import (
"github.com/beego/beego/v2/core/config"
// never forget this
_ "github.com/beego/beego/v2/core/config/yaml"
"github.com/beego/beego/v2/core/logs"
)
var (
ConfigFile = "./app.yaml"
)
func main() {
err := config.InitGlobalInstance("yaml", ConfigFile)
if err != nil {
logs.Critical("An error occurred:", err)
panic(err)
}
val, _ := config.String("name")
logs.Info("load config name is", val)
}
XML
别忘了匿名引入 XML 的实现!
import (
"github.com/beego/beego/v2/core/config"
// never forget this
_ "github.com/beego/beego/v2/core/config/xml"
"github.com/beego/beego/v2/core/logs"
)
var (
ConfigFile = "./app.xml"
)
func main() {
err := config.InitGlobalInstance("xml", ConfigFile)
if err != nil {
logs.Critical("An error occurred:", err)
panic(err)
}
val, _ := config.String("name")
logs.Info("load config name is", val)
}
要注意,所有的配置项都要放在config
这个顶级节点之内:
<?xml version="1.0" encoding="UTF-8" ?>
<config>
<name>beego</name>
</config>
TOML
别忘了匿名引入 TOML 的实现!
import (
"github.com/beego/beego/v2/core/config"
// never forget this
_ "github.com/beego/beego/v2/core/config/toml"
"github.com/beego/beego/v2/core/logs"
)
var (
ConfigFile = "./app.toml"
)
func main() {
err := config.InitGlobalInstance("toml", ConfigFile)
if err != nil {
logs.Critical("An error occurred:", err)
panic(err)
}
val, _ := config.String("name")
logs.Info("load config name is", val)
}
Etcd
别忘了匿名引入 ETCD 的实现!
import (
"github.com/beego/beego/v2/core/config"
// never forget this
_ "github.com/beego/beego/v2/core/config/toml"
"github.com/beego/beego/v2/core/logs"
)
func main() {
err := config.InitGlobalInstance("etcd", "your_config")
if err != nil {
logs.Critical("An error occurred:", err)
panic(err)
}
val, _ := config.String("name")
logs.Info("load config name is", val)
}
其中 your_config
是一个 JSON 配置,它对应于:
type Config struct {
// Endpoints is a list of URLs.
Endpoints []string `json:"endpoints"`
// AutoSyncInterval is the interval to update endpoints with its latest members.
// 0 disables auto-sync. By default auto-sync is disabled.
AutoSyncInterval time.Duration `json:"auto-sync-interval"`
// DialTimeout is the timeout for failing to establish a connection.
DialTimeout time.Duration `json:"dial-timeout"`
// DialKeepAliveTime is the time after which client pings the server to see if
// transport is alive.
DialKeepAliveTime time.Duration `json:"dial-keep-alive-time"`
// DialKeepAliveTimeout is the time that the client waits for a response for the
// keep-alive probe. If the response is not received in this time, the connection is closed.
DialKeepAliveTimeout time.Duration `json:"dial-keep-alive-timeout"`
// MaxCallSendMsgSize is the client-side request send limit in bytes.
// If 0, it defaults to 2.0 MiB (2 * 1024 * 1024).
// Make sure that "MaxCallSendMsgSize" < server-side default send/recv limit.
// ("--max-request-bytes" flag to etcd or "embed.Config.MaxRequestBytes").
MaxCallSendMsgSize int
// MaxCallRecvMsgSize is the client-side response receive limit.
// If 0, it defaults to "math.MaxInt32", because range response can
// easily exceed request send limits.
// Make sure that "MaxCallRecvMsgSize" >= server-side default send/recv limit.
// ("--max-request-bytes" flag to etcd or "embed.Config.MaxRequestBytes").
MaxCallRecvMsgSize int
// TLS holds the client secure credentials, if any.
TLS *tls.Config
// Username is a user name for authentication.
Username string `json:"username"`
// Password is a password for authentication.
Password string `json:"password"`
// RejectOldCluster when set will refuse to create a client against an outdated cluster.
RejectOldCluster bool `json:"reject-old-cluster"`
// DialOptions is a list of dial options for the grpc client (e.g., for interceptors).
// For example, pass "grpc.WithBlock()" to block until the underlying connection is up.
// Without this, Dial returns immediately and connecting the server happens in background.
DialOptions []grpc.DialOption
// Context is the default client context; it can be used to cancel grpc dial out and
// other operations that do not have an explicit context.
Context context.Context
// Logger sets client-side logger.
// If nil, fallback to building LogConfig.
Logger *zap.Logger
// LogConfig configures client-side logger.
// If nil, use the default logger.
// TODO: configure gRPC logger
LogConfig *zap.Config
// PermitWithoutStream when set will allow client to send keepalive pings to server without any active streams(RPCs).
PermitWithoutStream bool `json:"permit-without-stream"`
// TODO: support custom balancer picker
}
Web模块
路由
注册控制器风格的路由
所谓的注册控制器风格路由,可以理解为典型的 MVC 风格代码。即我们会在web
服务中声明各式各样的Controller
。
具体Controller
里面有什么 API,可以查看Controller API
基本用法
在 Beego 里面注册这种风格的路由很简单,只需要声明一个Controller
就可以:
import "github.com/beego/beego/v2/server/web"
type UserController struct {
web.Controller
}
这样我们就写好了一个Controller
。
如果我们要想添加一个方法,那么可以:
import "github.com/beego/beego/v2/server/web"
type UserController struct {
web.Controller
}
func (u *UserController) HelloWorld() {
u.Ctx.WriteString("hello, world")
}
func main() {
web.AutoRouter(&UserController{})
web.Run()
}
当我们访问 URL http://127.0.0.1:8080/user/helloworld
的时候,可以看到结果:
需要注意的是,控制器里面处理http
请求的方法必须是公共方法——即首字母大写,并且没有参数,没有返回值。如果你的方法不符合这个要求,大多数情况下,会发生panic
,例如你的方法有参数:
func (u *UserController) HelloWorldNoPtr(name string) {
u.Ctx.WriteString("don't work")
}
注意比较,在函数式注册风格里面,我们的
HandleFunc
其实是接收一个*Context
参数的
如果你的方法接收器不是指针:
golangci-lint run
func (u UserController) HelloWorldNoPtr() {
u.Ctx.WriteString("don't work")
}
这种写法也是可以的。一般的惯例是使用指针接收器,但是这并不强制。关于接收器的讨论,可以参考选择什么作为方法接收器
Controller 的名字
在一些比较智能的 API 里面,我们会使用Controller
的名字来作为前缀、命名空间等。
那么,Controller
的名字是如何确定的呢?
在 Beego 里面,我们认为,一个Controller
的定义是形如:
type CtrlNameController struct {
}
比如说,我们定义了一个UserController
,那么Controller
的名字就是User
。如果大小写不敏感,那么user
也是合法的名字。
再比如说我们定义了一个BuyerRefundController
,那么BuyerRefund
就是名字,大小写不敏感的时候,buyerrefund
也是合法的名字。
AutoRouter
刚才我们使用的是web
模块里面一个很实用的注册路由的方法AutoRouter
。
AutoRouter
解析出来的路由规则由RouterCaseSensitive
的值,Controller
的名字和方法名字共同决定。
其中UserController
它的名字是User
,而方法名字是HelloWorld
。那么:
- 如果
RouterCaseSensitive
为true
,那么AutoRouter
会注册两个路由,/user/helloworld/*
,/User/HelloWorld/*
; - 如果
RouterCaseSensitive
为false
,那么会注册一个路由,/user/helloworld/*
;
总而言之,在使用AutoRouter
的情况下,使用全小写的路径总是没错的。
AutoPrefix
AutoRouter
内部是基于AutoPrefix
实现的,可以说,Controller
的名字,就是注册的前缀(prefix)。
下面我们来看一个简单的例子:
import (
"github.com/beego/beego/v2/server/web"
)
type UserController struct {
web.Controller
}
func (u *UserController) HelloWorld() {
u.Ctx.WriteString("Hello, world")
}
func main() {
// get http://localhost:8080/api/user/helloworld
// you will see return "Hello, world"
ctrl := &UserController{}
web.AutoPrefix("api", ctrl)
web.Run()
}
在运行之后,浏览器里面输入http://localhost:8080/api/user/helloworld
,就能看到返回的响应"Hello, world"。
类似于我们前面提到的AutoRoute
,这里注册的路由包含:
- 如果
RouterCaseSensitive
为true
,那么AutoPrefix
会注册两个路由,api/user/helloworld/*
,api/User/HelloWorld/*
; - 如果
RouterCaseSensitive
为false
,那么会注册一个路由,api/user/helloworld/*
;
这里我们可以总结出来一般性质的规则: 当我们使用AutoPrefix
的时候,注册的路由符合prefix/ctrlName/methodName
这种模式。
手动路由
如果我们并不想利用AutoRoute
或者AutoPrefix
来注册路由,因为这两个都依赖于Controller
的名字,也依赖于方法的名字。某些时候我们可能期望在路由上,有更强的灵活性。
在这种场景下,我们可以考虑说,采用手工注册的方式,挨个注册路由。
在 v2.0.2 我们引入了全新的注册方式。下面我们来看一个完整的例子
import (
"github.com/beego/beego/v2/server/web"
)
type UserController struct {
web.Controller
}
func (u *UserController) GetUserById() {
u.Ctx.WriteString("GetUserById")
}
func (u *UserController) UpdateUser() {
u.Ctx.WriteString("UpdateUser")
}
func (u *UserController) UserHome() {
u.Ctx.WriteString("UserHome")
}
func (u *UserController) DeleteUser() {
u.Ctx.WriteString("DeleteUser")
}
func (u *UserController) HeadUser() {
u.Ctx.WriteString("HeadUser")
}
func (u *UserController) OptionUsers() {
u.Ctx.WriteString("OptionUsers")
}
func (u *UserController) PatchUsers() {
u.Ctx.WriteString("PatchUsers")
}
func (u *UserController) PutUsers() {
u.Ctx.WriteString("PutUsers")
}
func main() {
// get http://localhost:8080/api/user/123
web.CtrlGet("api/user/:id", (*UserController).GetUserById)
// post http://localhost:8080/api/user/update
web.CtrlPost("api/user/update", (*UserController).UpdateUser)
// http://localhost:8080/api/user/home
web.CtrlAny("api/user/home", (*UserController).UserHome)
// delete http://localhost:8080/api/user/delete
web.CtrlDelete("api/user/delete", (*UserController).DeleteUser)
// head http://localhost:8080/api/user/head
web.CtrlHead("api/user/head", (*UserController).HeadUser)
// patch http://localhost:8080/api/user/options
web.CtrlOptions("api/user/options", (*UserController).OptionUsers)
// patch http://localhost:8080/api/user/patch
web.CtrlPatch("api/user/patch", (*UserController).PatchUsers)
// put http://localhost:8080/api/user/put
web.CtrlPut("api/user/put", (*UserController).PutUsers)
web.Run()
}
需要注意的是,我们新的注册方法,要求我们传入方法的时候,传入的是(*YourController).MethodName
。这是因为 Go 语言特性,要求在接收器是指针的时候,如果你希望拿到这个方法,那么应该用(*YourController)
的形式。
那么,如果我们不用指针接收器:
import (
"github.com/beego/beego/v2/server/web"
)
type UserController struct {
web.Controller
}
func (u UserController) GetUserById() {
u.Ctx.WriteString("GetUserById")
}
func (u UserController) UpdateUser() {
u.Ctx.WriteString("UpdateUser")
}
func (u UserController) UserHome() {
u.Ctx.WriteString("UserHome")
}
func (u UserController) DeleteUser() {
u.Ctx.WriteString("DeleteUser")
}
func (u UserController) HeadUser() {
u.Ctx.WriteString("HeadUser")
}
func (u UserController) OptionUsers() {
u.Ctx.WriteString("OptionUsers")
}
func (u UserController) PatchUsers() {
u.Ctx.WriteString("PatchUsers")
}
func (u UserController) PutUsers() {
u.Ctx.WriteString("PutUsers")
}
func main() {
// get http://localhost:8080/api/user/123
web.CtrlGet("api/user/:id", UserController.GetUserById)
// post http://localhost:8080/api/user/update
web.CtrlPost("api/user/update", UserController.UpdateUser)
// http://localhost:8080/api/user/home
web.CtrlAny("api/user/home", UserController.UserHome)
// delete http://localhost:8080/api/user/delete
web.CtrlDelete("api/user/delete", UserController.DeleteUser)
// head http://localhost:8080/api/user/head
web.CtrlHead("api/user/head", UserController.HeadUser)
// patch http://localhost:8080/api/user/options
web.CtrlOptions("api/user/options", UserController.OptionUsers)
// patch http://localhost:8080/api/user/patch
web.CtrlPatch("api/user/patch", UserController.PatchUsers)
// put http://localhost:8080/api/user/put
web.CtrlPut("api/user/put", UserController.PutUsers)
web.Run()
}
我们建议如果使用这个系列的方法,那么应该选择使用结构体接收器,这样代码看上去要清爽很多。
要额外引起注意的是CtrlAny
方法,这意味着,任意的http
方法都可以被处理。
注解路由
历史注册路由方式
和之前的注册路由方式比起来,我们这一次的改进,让用户可以在现代 IDE 中,点击方法名进行跳转。
历史上,我们的注册方式是这样的:
func main() {
ctrl := &MainController{}
// we register the path / to &MainController
// if we don't pass methodName as third param
// web will use the default mappingMethods
// GET http://localhost:8080 -> Get()
// POST http://localhost:8080 -> Post()
// ...
web.Router("/", ctrl)
// GET http://localhost:8080/health => ctrl.Health()
web.Router("/health", ctrl, "get:Health")
// POST http://localhost:8080/update => ctrl.Update()
web.Router("/update", ctrl, "post:Update")
// support multiple http methods.
// POST or GET http://localhost:8080/update => ctrl.GetOrPost()
web.Router("/getOrPost", ctrl, "get,post:GetOrPost")
// support any http method
// POST, GET, PUT, DELETE... http://localhost:8080/update => ctrl.Any()
web.Router("/any", ctrl, "*:Any")
web.Run()
}
我们不再推荐使用这种方式,因为可读性和可维护性都不太好。特别是重构进行方法重命名的时候,容易出错。
注册函数式风格路由注册
该风格比较接近 Go 本身的语法特性,所以我们倾向于建议大家使用该路由注册方式。
使用该风格,非常简单,可以直接采用函数式的写法:
func main() {
web.Get("/hello", func(ctx *context.Context) {
ctx.WriteString("hello, world")
})
web.Run()
}
注意,在函数式写法里面,我们只需要提供一个使用*context.Context
参数的方法就可以。这个context
不是 GO 的context
包,而是 Beego 的context
包。
大多数情况下,我们可能不太想这么写,那么我们可以在别的地方定义方法,然后再注册进来:
func main() {
web.Post("/post/hello", PostHello)
web.Run()
}
func PostHello(ctx *context.Context) {
ctx.WriteString("hello")
}
这可能会符合我们的一般习惯。
函数式注册,基本上就是各个 HTTP 方法都有一个对应的方法:
Get(rootpath string, f HandleFunc)
Post(rootpath string, f HandleFunc)
Delete(rootpath string, f HandleFunc)
Put(rootpath string, f HandleFunc)
Head(rootpath string, f HandleFunc)
Options(rootpath string, f HandleFunc)
Patch(rootpath string, f HandleFunc)
Any(rootpath string, f HandleFunc)
它们用法都是一样的,非常简单。
一些建议
在使用函数式注册的方式的时候,要面临的一个困难是,这些方法该如何组织。
在控制器风格里面,所有的方法天然被控制器所分割。举例来说,在一个简单的系统里面,我们可以有一个UserController
,而后所有的和User
有关的方法,都放在这个Controller
里面;所有的和订单有关的方法,都可以放到OrderController
里面。
因此从这个角度来看,Controller
提供了一种自然的组织这些方法的方式。
那么在函数式风格里面,我们缺乏这样一个实体。
一种直觉的方式,是按照文件来组织。例如所有的用户相关的都放在自己项目的api/user.go
里面;
如果方法更多,那么应该进一步考虑按照包来组织,也是我们所推荐的。例如在api
之下创建一个user
目录,所有的处理用户相关请求的方法都定义在这里;
最后一种方式,是自己定义一个Controller
,但是这个Controller
只起到一个组织代码的效果,并不是我们在控制器风格里面强调的那种。
例如我们完全可以不组合web.Controller
写一个Controller
:
type UserController struct {
}
func (ctrl UserController) AddUser(ctx *context.Context) {
// you business code
}
这种Controller
不能用于控制器风格的理由注册。只是为了提供一种类似的方式来组织代码而已。
查看已注册路由
在排查问题的时候,我们可能想知道,整个系统究竟注册了哪些路由。Web 提供了一个非常有用的方法web.PrintTree()
,该方法会把所有注册的路由信息返回,而后我们就可以依次遍历打印:
package main
import (
"fmt"
"github.com/beego/beego/v2/server/web"
)
type UserController struct {
web.Controller
}
func (u *UserController) HelloWorld() {
u.Ctx.WriteString("hello, world")
}
func main() {
web.BConfig.RouterCaseSensitive = false
web.AutoRouter(&UserController{})
tree := web.PrintTree()
methods := tree["Data"].(web.M)
for k, v := range methods {
fmt.Printf("%s => %v\n", k, v)
}
}
如果要是注册的路由,使用了*
作为方法,也就是匹配任何 HTTP 方法,那么就会每个方法打印出来一个。AutoRouter
就是匹配任何的 HTTP 方法,所以最终会打印出来一堆内容:
MKCOL => &[[/user/helloworld/* map[*:HelloWorld] main.UserController]]
CONNECT => &[[/user/helloworld/* map[*:HelloWorld] main.UserController]]
POST => &[[/user/helloworld/* map[*:HelloWorld] main.UserController]]
UNLOCK => &[[/user/helloworld/* map[*:HelloWorld] main.UserController]]
PROPFIND => &[[/user/helloworld/* map[*:HelloWorld] main.UserController]]
PATCH => &[[/user/helloworld/* map[*:HelloWorld] main.UserController]]
GET => &[[/user/helloworld/* map[*:HelloWorld] main.UserController]]
DELETE => &[[/user/helloworld/* map[*:HelloWorld] main.UserController]]
PROPPATCH => &[[/user/helloworld/* map[*:HelloWorld] main.UserController]]
COPY => &[[/user/helloworld/* map[*:HelloWorld] main.UserController]]
OPTIONS => &[[/user/helloworld/* map[*:HelloWorld] main.UserController]]
HEAD => &[[/user/helloworld/* map[*:HelloWorld] main.UserController]]
LOCK => &[[/user/helloworld/* map[*:HelloWorld] main.UserController]]
PUT => &[[/user/helloworld/* map[*:HelloWorld] main.UserController]]
TRACE => &[[/user/helloworld/* map[*:HelloWorld] main.UserController]]
MOVE => &[[/user/helloworld/* map[*:HelloWorld] main.UserController]]
我们用POST => &[[/user/helloworld/* map[*:HelloWorld] main.UserController]]
作为例子来展示该如何解读: 它表示的是,POST 方法访问 /user/helloworld/*
这种模式的路径, 那么它会执行main.UserController
里面的HelloWorld
方法。
Namespace
namespace
,也叫做命名空间,是 Beego 提供的一种逻辑上的组织 API 的手段。 大多数时候,我们注册路由的时候,会按照一定的规律组织,那么使用namespace
就会使代码更加简洁可维护。
例如,我们整个应用分成两大块,一个是对安卓提供的 API,一个是对 IOS 提供的 API。那么整体上,就可以划分成两个命名空间;有一些应用会有版本概念,比如说早期是 V1,后面引入了 V2,再后来又引入了 V3,那么整个应用就有三个命名空间。不同版本的 API 注册在不同的命名空间之下。
namespace
稍微有点复杂,所以你可能需要多写几个简单的demo
来掌握它的用法。
例子
func main() {
uc := &UserController{}
// 创建 namespace
ns := web.NewNamespace("/v1",
web.NSCtrlGet("/home", (*MainController).Home),
web.NSRouter("/user", uc),
web.NSGet("/health", Health),
)
//注册 namespace
web.AddNamespace(ns)
web.Run()
}
type MainController struct {
web.Controller
}
func (mc *MainController) Home() {
mc.Ctx.WriteString("this is home")
}
type UserController struct {
web.Controller
}
func (uc *UserController) Get() {
uc.Ctx.WriteString("get user")
}
func Health(ctx *context.Context) {
ctx.WriteString("health")
}
在我们启动服务器之后,分别访问下面三个地址:
GET http://localhost:8080/v1/home
GET http://localhost:8080/v1/user
GET http://localhost:8080/v1/health
都能得到对应输出。这些地址的规律可以总结为就是分段加起来。例如这个例子我们的namespace
的前缀是v1
,所以就是在注册的路由之前加上一段v1
。
注意到,在main
函数里面,我们采用了不同的方式来注册路由。可以说,不管是函数式路由注册 还是 控制器路由注册,对于namespace
来说都是可以的。整体规律就是多了NS
这个前缀。
例如说web.Get
对应到namespace
内部注册,就是web.NSGet
。
同样的,我们也可以注册多个namespace
,例如我们创建一个v2
前缀的namespace
。
namespace 嵌套
有时候我们会希望namespace
内部嵌套namespace
。这个时候我们可以使用web.NSNamespace
方法来注入一个子namespace
。
例如:
func main() {
uc := &UserController{}
// 初始化 namespace
ns := web.NewNamespace("/v1",
web.NSCtrlGet("/home", (*MainController).Home),
web.NSRouter("/user", uc),
web.NSGet("/health", Health),
// 嵌套 namespace
web.NSNamespace("/admin",
web.NSRouter("/user", uc),
),
)
//注册 namespace
web.AddNamespace(ns)
web.Run()
}
启动服务器,访问GET http://localhost:8080/v1/admin/user
就能看到输出。我们依旧可以看到,路径是各层namespace
拼接起来的。
namespace 的条件执行
Beego 的namespace
提供了一种条件判断机制。只有在符合条件的情况下,注册在该namespace
下的路由才会被执行。本质上,这只是一个filter
的应用。
例如,我们希望用户的请求的头部里面一定要带上一个x-trace-id
才会被后续的请求处理:
func main() {
uc := &UserController{}
// 初始化 namespace
ns := web.NewNamespace("/v1",
web.NSCond(func(b *context.Context) bool {
return b.Request.Header["x-trace-id"][0] != ""
}),
web.NSCtrlGet("/home", (*MainController).Home),
web.NSRouter("/user", uc),
web.NSGet("/health", Health),
// 嵌套 namespace
web.NSNamespace("/admin",
web.NSRouter("/user", uc),
),
)
//注册 namespace
web.AddNamespace(ns)
web.Run()
}
一般来说,我们现在也不推荐使用这个特性,因为它的功能和filter
存在重合,我们建议大家如果有需要,应该考虑自己正常实现一个filter
,代码可理解性会更高。该特性会考虑在未来的版本用一个filter
来替代,而后移除该方法。
Filter
namespace
同样支持filter
。该filter
只会作用于这个namespace
之下注册的路由,而对别的路由没有影响。
我们有两种方式添加Filter
,一个是在NewNamespace
中,调用web.NSBefore
或者web.NSAfter
,也可以调用ns.Filter()
func main() {
uc := &UserController{}
// 初始化 namespace
ns := web.NewNamespace("/v1",
web.NSCond(func(b *context.Context) bool {
return b.Request.Header["x-trace-id"][0] != ""
}),
web.NSBefore(func(ctx *context.Context) {
fmt.Println("before filter")
}),
web.NSAfter(func(ctx *context.Context) {
fmt.Println("after filter")
}),
web.NSCtrlGet("/home", (*MainController).Home),
web.NSRouter("/user", uc),
web.NSGet("/health", Health),
// 嵌套 namespace
web.NSNamespace("/admin",
web.NSRouter("/user", uc),
),
)
ns.Filter("before", func(ctx *context.Context) {
fmt.Println("this is filter for health")
})
//注册 namespace
web.AddNamespace(ns)
web.Run()
}
目前来说,namespace
对filter
的支持是有限的,只能支持before
和after
两种。
因此要支持复杂的filter
,或者filter-chain
,请参考过滤器
NSInclude
接下来我们讨论一个有点奇怪的东西,web.NSInclude
方法。该方法是注解路由的配套方法。
也就是意味着,它只对注解路由生效。
让我们来看一个简单的例子:
func init() {
api := web.NewNamespace("/api/v1",
web.NSNamespace("/goods",
web.NSInclude(
&controllers.GoodsController{},
),
),
)
web.AddNamespace(api)
}
注意到,我们这里的GoodsController
必然是一个注解路由的Controller
,而且已经使用bee
命令生成注解路由了。
如果不知道怎么定义注解路由controller
,或者使用bee
命令生成注解路由,请参考相关文档。
优先使用函数式风格的路由注册
最核心的理由就是这种注册风格最为便捷,并且贴近 Go 语言本身特性。目前的主流 Web 框架基本上都是支持这种注册风格。
路由规则
路由规则是指,当我们注册了一个路由的时候,什么样的请求才会被处理?并且,如果我的请求路径里面含有参数信息,那么我该怎么从路径里面拿出来参数?
首先,我们会首先匹配http
方法。例如,如果你对于路径api/user/*
只注册了get
方法,那么意味着,只有get
请求,才会被处理。
其次,在http
方法匹配上之后,我们会进一步匹配路径。
此外,为了方便大家快速写对路由,我们在这里征集各种路由规则的写法,请直接提交 PR 到 github,附在本页面的最后章节。
路由规则详解
固定路由
固定匹配表示只匹配特定路由,也可以理解为完全匹配,即你的路径和你注册的路径必须一模一样,否则不会命中。
例如/api/user/update
代表只有请求路径是http://your.com/api/user/update
会被匹配,而类似http://your.com/api/user/update/aa
则不会被匹配。
*
匹配
在 Beego 里面,可以用*
来表达匹配一段路由。
例如注册/api/user/name/*
,以下这些路径都能命中该路径:
/api/user/name
/api/user/name/tom
/api/user/name/jerry/home
即,只要前缀符合/api/user/name
,那么就会命中。
当然,*
也可以出现在中间,例如我们注册一个路由/api/*/name
,那么以下这些路径都能命中:
/api/tom/name
/api/tom/jerry/name
这意味着只要api
和name
之间至少存在一段非空路径,就会命中。
特别地,以下这种路径,将不会命中:
/api//name
/api/name
当*
出现在中间的时候,要尤其小心。例如当我们同时注册了两个路由/api/*/name
和 /api/*/mid/name
的时候:
/api/tom/mid/name
将命中/api/*/mid/name
/api/mid/name
将命中/api/*/name
从这个例子也可以看出来,Beego 匹配遵循了最精确匹配的原则。
我们允许一个路由里面含有多个*
。例如我们注册了一个路由/api/*/name/*/detail
,那么以下路径会命中:
/api/tom/name/profile/detail
/api/jerry/name/order/detail
从实践上来说,我们是不推荐大家使用多段*
的,它体现的是 API 其实并没有设计得很好。正常的情况下,它应该只出现在末尾。
一般来说出现在中段,多半是因为这个地方其实是一个参数,例如常见的 RESTFul API 的路由形式:/api/order/:id/detail
。它和 /api/order/*/detail
从实现上来说,效果是一样的,但是前者注册的表达的是中间是一个 ID,明确有含义,而后者,只是为了匹配特定的路由而已。
本质上来说,可以将这种路由理解为一种特殊的正则路由。
当我们使用这种注册路由方式的时候,我们可以使用:splat
来获得*
所命中的数据:
// web.Router("/user/name/*", ctrl, "post:Post")
func (ctrl *MainController) Post() {
username := ctrl.Ctx.Input.Param(":splat")
ctrl.Ctx.WriteString("Your router param username is:" + username)
}
如果是多段的,例如/api/*/name/*/detail
,那么:splat
只能获得最后一段的数据:
// web.Router("/api/*/name/*/detail", ctrl, "post:Post")
func (ctrl *MainController) Post() {
username := ctrl.Ctx.Input.Param(":splat")
ctrl.Ctx.WriteString("Your router param username is:" + username)
}
如果我们输入的路径是http://localhost:8080/api/tom/name/oid123/detail
,那么我们最终得到的username
是oid123
总结一下:如果要使用*
匹配,我们建议在整个路由里面应该只有一个*
,也尽量避免包含参数路由或者正则路由。并且*
命中的内容,可以通过:splat
来获得。
参数路由
Beego 支持参数路由,或者说 Ant 风格的路由。它通常见于 RESTFul 风格的 API 中。其语法是在路径之中以:
后面跟着参数的名字。
比如典型的例子:/api/:username/profile
。该路由:username
是指,位于api
和profile
之间的数据,是用户名。/api/:username/profile
能够命中:
/api/flycash/profile
/api/astaxie/profile
/api/123456/profile
但是无法命中:
/api//profile
/api/tom/jerry/profile
中间的 username
参数可以通过 Beego 提供的 API 来获取:
func (ctrl *MainController) Get() {
username := ctrl.Ctx.Input.Param(":username")
ctrl.Ctx.WriteString("Your router param username is:" + username)
}
另外一个例子/api/:id
,它能够命中api/123
,但是无法命中api/
。如果我们想要命中api/
那么我们需要修改路由为:/api/?:id
。注意/api/?:id
和/api/:id
的区别。后者要求在api
之后必须要要再跟着一段路径,否则不会匹配。
正则路由
Beego 支持正则路由。这是一个功能很强大的特性。其实,我们前面提到的参数路由,也可以理解为是正则路由的一种。只不过上面的参数路由实际中使用非常多,所以我们单独在文档中列出来讨论。
正则路由的核心语法是:param(reg)
。其中param
是参数名字,你可以通过Ctx.Input.Param(":param")
来获取值。而reg
则是正则表达式。我们看一个例子/api/:id([0-9]+)
这里表示,只有命中了路由规则的[0-9]+
的路径才会被认为是id
的值。因此:
/api/123
中id
的值是123
/api/tom
则无法命中这条路由,因为tom
不符合规则[0-9]+
鉴于大部分时候,我们使用的正则表达式并不会很多,所以我们内置了一些:
/:id:int
。:int
和[0-9]+
是等价的;/:hi:string
。:string
和[\w]+
是等价的;
还有一些比较复杂的正则表达式用法,我们不太推荐大家使用这些东西
/cms_:id([0-9]+).html
:这种方式,是在路径的中间加入一个正则表达式,某些场景下会很好用;/download/\*.\*
:这种相当于我们帮你解析了一个文件路径,例如/download/file/api.xml
可以匹配成功,此时变量:path
值为file/api
,:ext
值为xml
,在需要处理文件的场景下会有用;
但是,功能强大则意味着学习成本比较高。我们强烈建议大家尽量避免使用这一类复杂的路由,这相当于将部分业务逻辑泄露到了路由注册中,这本身就不是一个很好的设计。
Web输入处理
输入处理
总体来说,处理输入主要依赖于 Controller
提供的方法。而具体输入可以来源于:
- 路径参数:这一部分主要是指参数路由
- 查询参数
- 请求体:要想从请求体里面读取数据,大多数时候将
BConfig.CopyRequestBody
设置为true
就足够了。而如果你是创建了多个web.Server
,那么必须每一个Server
实例里面的配置都将CopyRequestBody
设置为true
了
而获取参数的方法可以分成两大类:
- 第一类是以 Get 为前缀的方法:这一大类的方法,主要获得某个特定参数的值
- 第二类是以 Bind 为前缀的方法:这一大类的方法,试图将输入转化为结构体
Get 类方法
针对这一类方法,Beego 主要从两个地方读取:查询参数和表单,如果两个地方都有相同名字的参数,那么 Beego 会返回表单里面的数据。例如最简单的例子:
type MainController struct {
web.Controller
}
func (ctrl *MainController) Post() {
name := ctrl.GetString("name")
if name == "" {
ctrl.Ctx.WriteString("Hello World")
return
}
ctrl.Ctx.WriteString("Hello " + name)
}
当我们访问:
- 路径
localhost:8080?name=a
: 这是使用查询参数的形式,那么会输出Hello, a
- 路径
localhost:8080
,而后表单里面提交了name=b
,那么会输出b
- 路径
localhost:8080?name=a
,并且表单提交了name=b
,那么会输出b
这一类的方法也允许传入默认值,例如:
func (ctrl *MainController) Get() {
name := ctrl.GetString("name", "Tom")
ctrl.Ctx.WriteString("Hello " + name)
}
如果我们没有传入name
参数,那么就会使用Tom
作为name
的值,例如我们访问GET localhost:8080
的时候,就会输出 Hello Tom
。
需要注意的是,GetString
的方法签名是:
func (c *Controller) GetString(key string, def ...string) string {
// ...
}
要注意的是,虽然def
被声明为不定参数,但是实际上,Beego 只会使用第一个默认值,后面的都会被忽略。
这一类方法签名和行为都是类似的,它们有:
GetString(key string, def ...string) string
GetStrings(key string, def ...[]string) []string
GetInt(key string, def ...int) (int, error)
GetInt8(key string, def ...int8) (int8, error)
GetUint8(key string, def ...uint8) (uint8, error)
GetInt16(key string, def ...int16) (int16, error)
GetUint16(key string, def ...uint16) (uint16, error)
GetInt32(key string, def ...int32) (int32, error)
GetUint32(key string, def ...uint32) (uint32, error)
GetInt64(key string, def ...int64) (int64, error)
GetUint64(key string, def ...uint64) (uint64, error)
GetBool(key string, def ...bool) (bool, error)
GetFloat(key string, def ...float64) (float64, error)
这里要注意到,GetString
和 GetStrings
本身在设计的时候并没有设计返回 error
,所以无法拿到错误。
Bind 类方法
大多数时候,我们还需要把输入转换为结构体,Beego 提供了一系列的方法来完成输入到结构体的绑定。
这部分方法是直接定义在 Context
结构体上的,所以用户可以直接操作 Context
实例。为了简化操作,我们在Controller
上也定义了类似的方法。
例如:
// 要设置 web.BConfig.CopyRequestBody = true
type MainController struct {
web.Controller
}
func (ctrl *MainController) Post() {
user := User{}
err := ctrl.BindJSON(&user)
if err != nil {
ctrl.Ctx.WriteString(err.Error())
return
}
ctrl.Ctx.WriteString(fmt.Sprintf("%v", user))
}
type User struct {
Age int `json:"age"`
Name string `json:"name"`
}
Bind
这一大类有多个方法:
Bind(obj interface{}) error
: 默认是依据输入的Content-Type
字段,来判断该如何反序列化;BindYAML(obj interface{}) error
: 处理YAML
输入BindForm(obj interface{}) error
: 处理表单输入BindJSON(obj interface{}) error
: 处理JSON
输入BindProtobuf(obj proto.Message) error
: 处理proto
输入BindXML(obj interface{}) error
: 处理XML
输入
在使用特定格式的输入的时候,别忘记设置标签(Tag
),例如我们例子里面的json:"age"
,不同格式的输入,其标签是不是一样的。
需要注意的是,虽然我们提供了一个根据Content-Type
来判断如何绑定的,但是我们更加推荐用户使用指定格式的绑定方法。
一个接口,应该只接收特定某种格式的输入,例如只接收 JSON,而不应该可以处理多种输入
在早期,Beego 还有一个类似于BindForm
的方法:ParseForm(obj interface{}) error
,这两个方法效果是一致的。
路径参数
我们在[路由定义——参数路由里面介绍过了如何获取路径参数。
早期 Bind 方法
在 Beego 的Input
中定义了一系列的方法,用于读取参数。这一类方法很类似于 Get
一族的方法。所以用户可以酌情使用。
例如请求地址如下
?id=123&isok=true&ft=1.2&ol[0]=1&ol[1]=2&ul[]=str&ul[]=array&user.Name=astaxie
var id int
this.Ctx.Input.Bind(&id, "id") //id ==123
var isok bool
this.Ctx.Input.Bind(&isok, "isok") //isok ==true
var ft float64
this.Ctx.Input.Bind(&ft, "ft") //ft ==1.2
ol := make([]int, 0, 2)
this.Ctx.Input.Bind(&ol, "ol") //ol ==[1 2]
ul := make([]string, 0, 2)
this.Ctx.Input.Bind(&ul, "ul") //ul ==[str array]
user struct{Name}
this.Ctx.Input.Bind(&user, "user") //user =={Name:"astaxie"}
Web 文件上传下载
文件上传
在 Beego 中你可以很容易的处理文件上传,就是别忘记在你的表单中增加这个属性 enctype="multipart/form-data"
,否则你的浏览器不会传输你的上传文件。
文件上传之后一般是放在系统的内存里面,如果文件的 size 大于设置的缓存内存大小,那么就放在临时文件中,默认的缓存内存是 64M,你可以通过如下来调整这个缓存内存大小:
web.MaxMemory = 1<<22
或者在配置文件中通过如下设置:
maxmemory = 1<<22
与此同时,Beego 提供了另外一个参数,MaxUploadSize
来限制最大上传文件大小——如果你一次长传多个文件,那么它限制的就是这些所有文件合并在一起的大小。
默认情况下,MaxMemory
应该设置得比MaxUploadSize
小,这种情况下两个参数合并在一起的效果则是:
- 如果文件大小小于
MaxMemory
,则直接在内存中处理; - 如果文件大小介于
MaxMemory
和MaxUploadSize
之间,那么比MaxMemory
大的部分将会放在临时目录; - 文件大小超出
MaxUploadSize
,直接拒绝请求,返回响应码 413
Beego 提供了两个很方便的方法来处理文件上传:
GetFile(key string) (multipart.File, *multipart.FileHeader, error)
:该方法主要用于用户读取表单中的文件名the_file
,然后返回相应的信息,用户根据这些变量来处理文件上传、过滤、保存文件等。SaveToFile(fromfile, tofile string) error
:该方法是在 GetFile 的基础上实现了快速保存的功能。fromfile
是提交时候表单中的name
<form enctype="multipart/form-data" method="post">
<input type="file" name="uploadname" />
<input type="submit" />
</form>
保存的代码例子如下:
func (c *FormController) Post() {
f, h, err := c.GetFile("uploadname")
if err != nil {
log.Fatal("getfile err ", err)
}
defer f.Close()
c.SaveToFile("uploadname", "static/upload/" + h.Filename) // 保存位置在 static/upload, 没有文件夹要先创建
}
文件下载
Beego 直接提供了一个下载文件的方法Download
:
func (output *BeegoOutput) Download(file string, filename ...string) {}
使用也很简单:
func (ctrl *MainController) DownloadFile() {
// The file LICENSE is under root path.
// and the downloaded file name is license.txt
ctrl.Ctx.Output.Download("LICENSE", "license.txt")
}
尤其要注意的是,Download
方法的第一个参数,是文件路径,也就是要下载的文件;第二个参数是不定参数,代表的是用户保存到本地时候的文件名。
如果第一个参数使用的是相对路径,那么它代表的是从当前工作目录开始计算的相对路径。
Session
beego
内置了 session
模块,目前 session
模块支持的后端引擎包括 memory
、cookie
、file
、mysql
、redis
、couchbase
、memcache
、postgres
,用户也可以根据相应的接口实现自己的引擎。
在 Web 中使用 Session
在 web
模块中使用 session
相当方便,只要在 main
入口函数中设置如下:
web.BConfig.WebConfig.Session.SessionOn = true
或者通过配置文件配置如下:
sessionon = true
通过这种方式就可以开启 session
,如何使用 session
,请看下面的例子:
func (this *MainController) Get() {
v := this.GetSession("asta")
if v == nil {
this.SetSession("asta", int(1))
this.Data["num"] = 0
} else {
this.SetSession("asta", v.(int)+1)
this.Data["num"] = v.(int)
}
this.TplName = "index.tpl"
}
session
有几个方便的方法:
SetSession(name string, value interface{})
GetSession(name string) interface{}
DelSession(name string)
SessionRegenerateID()
DestroySession()
session
操作主要有设置 session
、获取 session
,删除 session
。
当然你可以通过下面的方式自己控制这些逻辑:
sess:=this.StartSession()
defer sess.SessionRelease()
sess
对象具有如下方法:
sess.Set()
sess.Get()
sess.Delete()
sess.SessionID()
sess.Flush()
但是我还是建议大家采用 SetSession、GetSession、DelSession
三个方法来操作,避免自己在操作的过程中资源没释放的问题。
关于 Session
模块使用中的一些参数设置:
web.BConfig.WebConfig.Session.SessionOn
: 设置是否开启Session
,默认是false
,配置文件对应的参数名:sessionon
。web.BConfig.WebConfig.Session.SessionProvider
: 设置Session
的引擎,默认是memory
,目前支持还有file
、mysql
、redis
等,配置文件对应的参数名:sessionprovider
。web.BConfig.WebConfig.Session.SessionName
: 设置cookies
的名字,Session
默认是保存在用户的浏览器cookies
里面的,默认名是beegosessionID
,配置文件对应的参数名是:sessionname
。web.BConfig.WebConfig.Session.SessionGCMaxLifetime
: 设置Session
过期的时间,默认值是3600
秒,配置文件对应的参数:sessiongcmaxlifetime
。web.BConfig.WebConfig.Session.SessionProviderConfig
: 设置对应file
、mysql
、redis
引擎的保存路径或者链接地址,默认值是空,配置文件对应的参数:sessionproviderconfig
。web.BConfig.WebConfig.Session.SessionHashFunc
: 默认值为sha1
,采用sha1
加密算法生产sessionid
web.BConfig.WebConfig.Session.SessionCookieLifeTime
: 设置cookie
的过期时间,cookie
是用来存储保存在客户端的数据。
在使用某种特定引擎的时候,需要匿名引入该引擎对应的包,以完成初始化工作:
import _ "github.com/beego/beego/v2/server/web/session/mysql"
不同引擎的初始化工作
File
当 SessionProvider
为 file
SessionProviderConfig
是指保存文件的目录,如下所示:
web.BConfig.WebConfig.Session.SessionProvider="file"
web.BConfig.WebConfig.Session.SessionProviderConfig = "./tmp"
MySQL
当 SessionProvider
为 mysql
时,SessionProviderConfig
是链接地址,采用 go-sql-driver (opens new window),如下所示:
web.BConfig.WebConfig.Session.SessionProvider = "mysql"
web.BConfig.WebConfig.Session.SessionProviderConfig = "username:password@protocol(address)/dbname?param=value"
需要特别注意的是,在使用 mysql
存储 session
信息的时候,需要事先在 mysql
创建表,建表语句如下
CREATE TABLE `session` (
`session_key` char(64) NOT NULL,
`session_data` blob,
`session_expiry` int(11) unsigned NOT NULL,
PRIMARY KEY (`session_key`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
Redis
当 SessionProvider
为 redis
时,SessionProviderConfig
是 redis
的链接地址,采用了 redigo (opens new window),如下所示:
web.BConfig.WebConfig.Session.SessionProvider = "redis"
web.BConfig.WebConfig.Session.SessionProviderConfig = "127.0.0.1:6379"
memcache
当 SessionProvider
为 memcache
时,SessionProviderConfig
是 memcache
的链接地址,采用了 memcache (opens new window),如下所示:
web.BConfig.WebConfig.Session.SessionProvider = "memcache"
web.BConfig.WebConfig.Session.SessionProviderConfig = "127.0.0.1:7080"
Postgress
当 SessionProvider
为 postgres
时,SessionProviderConfig
是 postgres
的链接地址,采用了 postgres (opens new window),如下所示:
web.BConfig.WebConfig.Session.SessionProvider = "postgresql"
web.BConfig.WebConfig.Session.SessionProviderConfig = "postgres://pqgotest:password@localhost/pqgotest?sslmode=verify-full"
Couchbase
当 SessionProvider
为 couchbase
时,SessionProviderConfig
是 couchbase
的链接地址,采用了 couchbase (opens new window),如下所示:
web.BConfig.WebConfig.Session.SessionProvider = "couchbase"
web.BConfig.WebConfig.Session.SessionProviderConfig = "http://bucketname:bucketpass@myserver:8091"
特别注意点
因为 session
内部采用了 gob
来注册存储的对象,例如 struct
,所以如果你采用了非 memory
的引擎,请自己在 main.go
的 init
里面注册需要保存的这些结构体,不然会引起应用重启之后出现无法解析的错误
单独使用 Session 模块
如果不想使用 beego
的 web
模块,但是想使用 beego
的 session
模块,也是可以的
首先你必须导入包:
import (
"github.com/beego/beego/v2/server/web/session"
)
然后你初始化一个全局的变量用来存储 session
控制器:
var globalSessions *session.Manager
接着在你的入口函数中初始化数据:
func init() {
sessionConfig := &session.ManagerConfig{
CookieName:"gosessionid",
EnableSetCookie: true,
Gclifetime:3600,
Maxlifetime: 3600,
Secure: false,
CookieLifeTime: 3600,
ProviderConfig: "./tmp",
}
globalSessions, _ = session.NewManager("memory",sessionConfig)
go globalSessions.GC()
}
NewManager
函数的参数的函数如下所示
-
引擎名字,可以是
memory
、file
、MySQL
或Redis
。 -
一个
JSON
字符串,传入
Manager
的配置信息
cookieName
: 客户端存储cookie
的名字。enableSetCookie
,omitempty
: 是否开启SetCookie
,omitempty
这个设置gclifetime
: 触发GC
的时间。maxLifetime
: 服务器端存储的数据的过期时间secure
: 是否开启HTTPS
,在cookie
中设置cookie.Secure
。sessionIDHashFunc
:sessionID
生产的函数,默认是sha1
算法。sessionIDHashKey
:hash
算法中的key
。cookieLifeTime
: 客户端存储的cookie
的时间,默认值是0
,即浏览器生命周期。providerConfig
: 配置信息,根据不同的引擎设置不同的配置信息,详细的配置请看下面的引擎设置
最后我们的业务逻辑处理函数中可以这样调用:
func login(w http.ResponseWriter, r *http.Request) {
sess, _ := globalSessions.SessionStart(w, r)
defer sess.SessionRelease(w)
username := sess.Get("username")
if r.Method == "GET" {
t, _ := template.ParseFiles("login.gtpl")
t.Execute(w, nil)
} else {
sess.Set("username", r.Form["username"])
}
}
globalSessions
有多个函数如下所示:
SessionStart
根据当前请求返回session
对象SessionDestroy
销毁当前session
对象SessionRegenerateId
重新生成sessionID
GetActiveSession
获取当前活跃的session
用户SetHashFunc
设置sessionID
生成的函数SetSecure
设置是否开启cookie
的Secure
设置
返回的 session
对象是一个 Interface
,包含下面的方法
- Set(key, value interface{}) error
- Get(key interface{}) interface{}
- Delete(key interface{}) error
- SessionID() string
- SessionRelease()
- Flush() error
引擎设置
上面已经展示了 memory
的设置,接下来我们看一下其他三种引擎的设置方式:
-
mysql
: 其他参数一样,只是第四个参数配置设置如下所示,详细的配置请参考 mysql (opens new window):username:password@protocol(address)/dbname?param=value
-
Redis
: 配置文件信息如下所示,表示链接的地址,连接池,访问密码,没有保持为空:注意:若使用 Redis 等引擎作为 session backend,请在使用前导入 < _ "github.com/beego/beego/v2/server/web/session/redis" >
否则会在运行时发生错误,使用其他引擎时也是同理。
127.0.0.1:6379,100,astaxie
-
file
: 配置文件如下所示,表示需要保存的目录,默认是两级目录新建文件,例如sessionID
是xsnkjklkjjkh27hjh78908
,那么目录文件应该是./tmp/x/s/xsnkjklkjjkh27hjh78908
:./tmp
如何创建自己的引擎
在开发应用中,你可能需要实现自己的 session
引擎,例如 memcache
的引擎。
// Store contains all data for one session process with specific id.
type Store interface {
Set(ctx context.Context, key, value interface{}) error //set session value
Get(ctx context.Context, key interface{}) interface{} //get session value
Delete(ctx context.Context, key interface{}) error //delete session value
SessionID(ctx context.Context) string //back current sessionID
SessionRelease(ctx context.Context, w http.ResponseWriter) // release the resource & save data to provider & return the data
Flush(ctx context.Context) error //delete all data
}
// Provider contains global session methods and saved SessionStores.
// it can operate a SessionStore by its id.
type Provider interface {
SessionInit(ctx context.Context, gclifetime int64, config string) error
SessionRead(ctx context.Context, sid string) (Store, error)
SessionExist(ctx context.Context, sid string) (bool, error)
SessionRegenerate(ctx context.Context, oldsid, sid string) (Store, error)
SessionDestroy(ctx context.Context, sid string) error
SessionAll(ctx context.Context) int // get all active session
SessionGC(ctx context.Context)
}
最后需要注册自己写的引擎:
func init() {
Register("own", ownadaper)
}
Cookie 处理
这部分的例子在Cookie example(opens new window)
普通 Cookie 处理
Beego 通过Context
直接封装了对普通 Cookie 的处理方法,可以直接使用:
GetCookie(key string)
SetCookie(name string, value string, others ...interface{})
例子:
type MainController struct {
web.Controller
}
func (ctrl *MainController) PutCookie() {
// put something into cookie,set Expires time
ctrl.Ctx.SetCookie("name", "web cookie", 10)
// web-example/views/hello_world.html
ctrl.TplName = "hello_world.html"
ctrl.Data["name"] = "PutCookie"
_ = ctrl.Render()
}
func (ctrl *MainController) ReadCookie() {
// web-example/views/hello_world.html
ctrl.TplName = "hello_world.html"
ctrl.Data["name"] = ctrl.Ctx.GetCookie("name")
// don't forget this
_ = ctrl.Render()
}
others
参数含义依次是:
- 第一个代表
maxAge
,Beego 使用这个值计算Expires
和Max-Age
两个值 - 第二个代表
Path
,字符串类型,默认值是/
- 第三个代表
Domain
,字符串类型 - 第四个代表
Secure
,布尔类型 - 第五个代表
HttpOnly
,布尔类型 - 第六个代表
SameSite
,字符串类型
加密 Cookie 处理
Beego 提供了两个方法用于辅助 Cookie 加密处理,它采用了sha256
来作为加密算法,下面Secret
则是加密的密钥:
GetSecureCookie(Secret, key string) (string, bool)
:用于从 Cookie 中读取数据SetSecureCookie(Secret, name, value string, others ...interface{})
:用于写入数据到 Cookie。
type MainController struct {
web.Controller
}
func (ctrl *MainController) PutSecureCookie() {
// put something into cookie,set Expires time
ctrl.Ctx.SetSecureCookie("my-secret", "name", "web cookie")
// web-example/views/hello_world.html
ctrl.TplName = "hello_world.html"
ctrl.Data["name"] = "PutCookie"
_ = ctrl.Render()
}
func (ctrl *MainController) ReadSecureCookie() {
// web-example/views/hello_world.html
ctrl.TplName = "hello_world.html"
ctrl.Data["name"], _ = ctrl.Ctx.GetSecureCookie("my-secret", "name")
// don't forget this
_ = ctrl.Render()
}
others
参数和普通 Cookie 一样。
错误处理
我们在做 Web 开发的时候,经常需要页面跳转和错误处理,Beego 这方面也进行了考虑,通过 Redirect
方法来进行跳转:
func (this *AddController) Get() {
this.Redirect("/", 302)
}
如何中止此次请求并抛出异常,Beego 可以在控制器中这样操作:
func (this *MainController) Get() {
this.Abort("401")
v := this.GetSession("asta")
if v == nil {
this.SetSession("asta", int(1))
this.Data["Email"] = 0
} else {
this.SetSession("asta", v.(int)+1)
this.Data["Email"] = v.(int)
}
this.TplName = "index.tpl"
}
这样 this.Abort("401")
之后的代码不会再执行,而且会默认显示给用户如下页面:
web 框架默认支持 401、403、404、500、503 这几种错误的处理。用户可以自定义相应的错误处理,例如下面重新定义 404 页面:
func page_not_found(rw http.ResponseWriter, r *http.Request){
t,_:= template.New("404.html").ParseFiles(web.BConfig.WebConfig.ViewsPath+"/404.html")
data :=make(map[string]interface{})
data["content"] = "page not found"
t.Execute(rw, data)
}
func main() {
web.ErrorHandler("404",page_not_found)
web.Router("/", &controllers.MainController{})
web.Run()
}
我们可以通过自定义错误页面 404.html
来处理 404 错误。
Beego 更加人性化的还有一个设计就是支持用户自定义字符串错误类型处理函数,例如下面的代码,用户注册了一个数据库出错的处理页面:
func dbError(rw http.ResponseWriter, r *http.Request){
t,_:= template.New("dberror.html").ParseFiles(web.BConfig.WebConfig.ViewsPath+"/dberror.html")
data :=make(map[string]interface{})
data["content"] = "database is now down"
t.Execute(rw, data)
}
func main() {
web.ErrorHandler("dbError",dbError)
web.Router("/", &controllers.MainController{})
web.Run()
}
一旦在入口注册该错误处理代码,那么你可以在任何你的逻辑中遇到数据库错误调用 this.Abort("dbError")
来进行异常页面处理。
Controller 定义 Error
从 1.4.3 版本开始,支持 Controller 方式定义 Error 错误处理函数,这样就可以充分利用系统自带的模板处理,以及 context 等方法。
package controllers
import (
"github.com/beego/beego/v2/server/web"
)
type ErrorController struct {
web.Controller
}
func (c *ErrorController) Error404() {
c.Data["content"] = "page not found"
c.TplName = "404.tpl"
}
func (c *ErrorController) Error501() {
c.Data["content"] = "server error"
c.TplName = "501.tpl"
}
func (c *ErrorController) ErrorDb() {
c.Data["content"] = "database is now down"
c.TplName = "dberror.tpl"
}
通过上面的例子我们可以看到,所有的函数都是有一定规律的,都是 Error
开头,后面的名字就是我们调用 Abort
的名字,例如 Error404
函数其实调用对应的就是 Abort("404")
我们就只要在 web.Run
之前采用 web.ErrorController
注册这个错误处理函数就可以了
package main
import (
_ "btest/routers"
"btest/controllers"
"github.com/Beego/Beego/v2/server/web"
)
func main() {
web.ErrorController(&controllers.ErrorController{})
web.Run()
}
从 panic 中恢复
如果你希望用户在服务器处理请求过程中,即便发生了 panic 依旧能够返回响应,那么可以使用 Beego 的恢复机制。该机制是默认开启的。依赖于配置项:
web.BConfig.RecoverPanic = true
如果你需要关闭,那么将这个配置项设置为false
就可以。
如果你想自定义panic
之后的处理行为,那么可以重新设置web.BConfig.RecoverFunc
。
例如:
web.BConfig.RecoverFunc = func(context *context.Context, config *web.Config) {
if err := recover(); err != nil {
context.WriteString(fmt.Sprintf("you panic, err: %v", err))
}
}
千万要注意:你永远需要检测recover
的结果,并且将从panic
中恢复过来的逻辑放在检测到recover
返回不为nil
的代码里面。
Admin 管理后台
默认 Admin 是关闭的,你可以通过配置开启监控:
web.BConfig.Listen.EnableAdmin = true
而且你还可以修改监听的地址和端口:
web.BConfig.Listen.AdminAddr = "localhost"
web.BConfig.Listen.AdminPort = 8088
打开浏览器,输入 URL:http://localhost:8088/
,你会看到一句欢迎词:Welcome to Admin Dashboard
。
请求统计信息
访问统计的 URL 地址 http://localhost:8088/qps
,展现如下所示:
性能调试
你可以查看程序性能相关的信息, 进行性能调优.
健康检查
需要手工注册相应的健康检查逻辑,才能通过 URLhttp://localhost:8088/healthcheck
获取当前执行的健康检查的状态。
定时任务
用户需要在应用中添加了 定时任务,才能执行相应的任务检查和手工触发任务。
- 检查任务状态 URL:
http://localhost:8088/task
- 手工执行任务 URL:
http://localhost:8088/task?taskname=任务名
配置信息
应用开发完毕之后,我们可能需要知道在运行的进程到底是怎么样的配置,beego 的监控模块提供了这一功能。
- 显示所有的配置信息:
http://localhost:8088/listconf?command=conf
- 显示所有的路由配置信息:
http://localhost:8088/listconf?command=router
- 显示所有的过滤设置信息:
http://localhost:8088/listconf?command=filter
跨站请求伪造
跨站请求伪造(Cross-site request forgery) (opens new window), 简称为 XSRF,是 Web 应用中常见的一个安全问题。前面的链接也详细讲述了 XSRF 攻击的实现方式。
当前防范 XSRF 的一种通用的方法,是对每一个用户都记录一个无法预知的 cookie 数据,然后要求所有提交的请求(POST/PUT/DELETE)中都必须带有这个 cookie 数据。如果此数据不匹配 ,那么这个请求就可能是被伪造的。
Beego 有内建的 XSRF 的防范机制,要使用此机制,你需要在应用配置文件中加上 enablexsrf
设定:
enablexsrf = true
xsrfkey = 61oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o
xsrfexpire = 3600
或者直接在 main 入口处这样设置:
web.EnableXSRF = true
web.XSRFKEY = "61oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o"
web.XSRFExpire = 3600 //过期时间,默认1小时
如果开启了 XSRF,那么 Beego 的 Web 应用将对所有用户设置一个 _xsrf
的 Cookie 值(默认过期 1 小时),如果 POST PUT DELET
请求中没有这个 Cookie 值,那么这个请求会被直接拒绝。
Beego 使用了 Secure
和 HTTP-ONLY
两个选项来保存 Cookie。因此在大部分情况下,这意味这你需要使用 HTTPS 协议,并且将无法在 JS 里面访问到 Cookie 的值。
在早期缺乏这两个选项的时候,攻击者可以轻易拿到我们设置的 Cookie 值,因此造成了安全问题。但是即便加上这两个选项,也不意味着万无一失。比如说,攻击者可以尝试用 HTTP 协议覆盖掉原有的 HTTP 协议设置的 Cookie。具体细节可以参考前面
secure
选项中的说明。
因为 Beego 需要拿到 Token 和 Cookie 里面的值进行比较,所以 Beego 要求用户必须在自己的请求里面带上 XSRF Token,你有两种方式:
- 在表单里面携带一个叫做
_xsrf
的字段,里面是 XSRF 的 Token; - 在提交的请求的 HTTP HEADER 里面设置
X-Xsrftoken
或X-Csrftoken
,值就是 Token;
下面是使用这两种方式的简单例子
表单中携带 Token
最简单的做法,是利用 Beego 的方法,在表单中加入一个字段,将 XSRF Token 带回来,例如:
func (mc *MainController) XsrfPage() {
mc.XSRFExpire = 7200
mc.Data["xsrfdata"] = template.HTML(mc.XSRFFormHTML())
mc.TplName = "xsrf.html"
}
其中xsrf.html
的核心代码是:
<form action="/new_message" method="post">
{{ .xsrfdata }}
<input type="text" name="message" />
<input type="submit" value="Post" />
</form>
.xsrfdata
就是mc.Data["xsrfdata"]
,详情可以参考模板引擎
页面设置 meta
比较简单的是通过扩展 Ajax 给每个请求加入 XSRF 的 HEADER
需要你在 HTML 里保存一个 _xsrf
值
func (this *HomeController) Get(){
this.Data["xsrf_token"] = this.XSRFToken()
}
放在你的页面 HEAD 中
<head>
<meta name="_xsrf" content="{{.xsrf_token}}" />
</head>
扩展 ajax 方法,将 _xsrf
值加入 header,扩展后支持 jquery post/get 等内部使用了 ajax 的方法
var ajax = $.ajax;
$.extend({
ajax: function (url, options) {
if (typeof url === "object") {
options = url;
url = undefined;
}
options = options || {};
url = options.url;
var xsrftoken = $("meta[name=_xsrf]").attr("content");
var headers = options.headers || {};
var domain = document.domain.replace(/\./gi, "\\.");
if (
!/^(http:|https:).*/.test(url) ||
eval("/^(http:|https:)\\/\\/(.+\\.)*" + domain + ".*/").test(url)
) {
headers = $.extend(headers, { "X-Xsrftoken": xsrftoken });
}
options.headers = headers;
return ajax(url, options);
},
});
注意的是,这里你可以将ajax
或者JQuery
替换为你自己的前端框架,因为核心在于要设置头部headers, {'X-Xsrftoken':xsrftoken}
。
而这个xsrftoken
可以是存在 HTML 的一个标签里面,也可是直接从之前响应里面读取出来,而后再提交表单的时候带过来。例如:
func (mc *MainController) XsrfJSON() {
mc.XSRFExpire = 7200
type data struct {
XsrfToken string `json:"xsrfToken"`
}
// 提交请求的时候用前端 JS 操作将这个 xsrfToken 再次带回来
_ = mc.JSONResp(&data{XsrfToken: mc.XSRFToken()})
}
Controller 级别的 XSRF 屏蔽
XSRF 之前是全局设置的一个参数,如果设置了那么所有的 API 请求都会进行验证,但是有些时候 API 逻辑是不需要进行验证的,因此现在支持在 Controller 级别设置屏蔽:
type AdminController struct{
web.Controller
}
func (a *AdminController) Prepare() {
a.EnableXSRF = false
}
其中Prepare
方法是 Controller 的一个钩子方法,详情参考Controller API-钩子方法
同样地,过期时间上面我们设置了全局的过期时间 web.XSRFExpire
,但是有些时候我们也可以在控制器中修改这个过期时间,专门针对某一类处理逻辑:
func (this *HomeController) Get(){
this.XSRFExpire = 7200
// ...
}
视图
模板引擎
Beego 的模板处理引擎采用的是 Go 内置的 html/template
包进行处理,而且 Beego 的模板处理逻辑是采用了缓存编译方式,也就是所有的模板会在 Beego 应用启动的时候全部编译然后缓存在 map 里面。
模板目录
Beego 中默认的模板目录是 views
,用户可以把模板文件放到该目录下,Beego 会自动在该目录下的所有模板文件进行解析并缓存,开发模式下每次都会重新解析,不做缓存。当然,用户也可以通过如下的方式改变模板的目录(只能指定一个目录为模板目录):
web.ViewsPath = "myviewpath"
自动渲染
用户无需手动的调用渲染输出模板,Beego 会自动的在调用完相应的 method
方法之后调用 Render
函数,当然如果您的应用是不需要模板输出的,那么可以在配置文件或者在 main.go
中设置关闭自动渲染。
配置文件配置如下:
autorender = false
或者在程序里面设置如下:
web.AutoRender = false
模板标签
Go 语言的默认模板采用了 {{
和 }}
作为左右标签,但是我们有时候在开发中可能界面是采用了 AngularJS 开发,他的模板也是这个标签,故而引起了冲突。在 Beego 中你可以通过配置文件或者直接设置配置变量修改:
web.TemplateLeft = "<<<"
web.TemplateRight = ">>>"
模板数据
模板中的数据是通过在 Controller 中 this.Data
获取的,所以如果你想在模板中获取内容,那么你需要在 Controller 中如下设置:
this.Data["Content"] = "value"
对应的 HTML 内容是:
{{ .Content }}
如何使用各种类型的数据渲染:
-
结构体:结构体定义
type A struct{ Name string Age int }
控制器数据赋值
this.Data["a"]=&A{Name:"astaxie",Age:25}
模板渲染数据如下:
the username is {{.a.Name}} the age is {{.a.Age}}
-
map 控制器数据赋值
mp["name"]="astaxie" mp["nickname"] = "haha" this.Data["m"]=mp
模板渲染数据如下:
the username is {{.m.name}} the username is {{.m.nickname}}
-
slice
控制器数据赋值
ss :=[]string{"a","b","c"} this.Data["s"]=ss
模板渲染数据如下:
{{range $key, $val := .s}} {{$key}} {{$val}} {{end}}
总结下来,可以看到.
定位到了 Go 程序 Data
字段。而后 .xxx
则是定位到了 Data
包含的元素。
模板名称
Beego 采用了 Go 语言内置的模板引擎,所有模板的语法和 Go 的一模一样,至于如何写模板文件,详细的请参考 模板教程 (opens new window)。
用户通过在 Controller 的对应方法中设置相应的模板名称,Beego 会自动的在 viewpath
目录下查询该文件并渲染,例如下面的设置,Beego 会在 admin
下面找 add.tpl
文件进行渲染:
this.TplName = "admin/add.tpl"
我们看到上面的模板后缀名是tpl
,Beego 默认情况下支持 tpl
和 html
后缀名的模板文件,如果你的后缀名不是这两种,请进行如下设置:
web.AddTemplateExt("你文件的后缀名")
当你设置了自动渲染,然后在你的 Controller 中没有设置任何的 TplName
,那么 Beego 会自动设置你的模板文件如下:
c.TplName = strings.ToLower(c.controllerName) + "/" + strings.ToLower(c.actionName) + "." + c.TplExt
也就是你对应的 Controller 名字+请求方法名.模板后缀,也就是如果你的 Controller 名是 AddController
,请求方法是 POST
,默认的文件后缀是 tpl
,那么就会默认请求 /viewpath/AddController/post.tpl
文件。
Layout 设计
Beego 支持 layout 设计,例如你在管理系统中,整个管理界面是固定的,只会变化中间的部分,那么你可以通过如下的设置:
this.Layout = "admin/layout.html"
this.TplName = "admin/add.tpl"
在 layout.html
中你必须设置如下的变量:
{{.LayoutContent}}
Beego 就会首先解析 TplName
指定的文件,获取内容赋值给 LayoutContent
,然后最后渲染 layout.html
文件。
目前采用首先把目录下所有的文件进行缓存,所以用户还可以通过类似这样的方式实现 layout
:
{{template "header.html" .}} Logic code {{template "footer.html" .}}
特别注意后面的
.
,这是传递当前参数到子模板
LayoutSection
对于一个复杂的 LayoutContent
,其中可能包括有 javascript 脚本、CSS 引用等,根据惯例,通常 css 会放到 Head 元素中,javascript 脚本需要放到 body 元素的末尾,而其它内容则根据需要放在合适的位置。在 Layout
页中仅有一个 LayoutContent
是不够的。所以在 Controller
中增加了一个 LayoutSections
属性,可以允许 Layout
页中设置多个 section
,然后每个 section
可以分别包含各自的子模板页。
layout_blog.tpl
:
<!DOCTYPE html>
<html>
<head>
<title>Lin Li</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link
rel="stylesheet"
href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css"
/>
<link
rel="stylesheet"
href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap-theme.min.css"
/>
{{.HtmlHead}}
</head>
<body>
<div class="container">{{.LayoutContent}}</div>
<div>{{.SideBar}}</div>
<script
type="text/javascript"
src="http://code.jquery.com/jquery-2.0.3.min.js"
></script>
<script src="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/js/bootstrap.min.js"></script>
{{.Scripts}}
</body>
</html>
html_head.tpl
:
<style>
h1 {
color: red;
}
</style>
scripts.tpl
:
<script type="text/javascript">
$(document).ready(function () {
// bla bla bla
});
</script>
逻辑处理如下所示:
type BlogsController struct {
web.Controller
}
func (this *BlogsController) Get() {
this.Layout = "layout_blog.tpl"
this.TplName = "blogs/index.tpl"
this.LayoutSections = make(map[string]string)
this.LayoutSections["HtmlHead"] = "blogs/html_head.tpl"
this.LayoutSections["Scripts"] = "blogs/scripts.tpl"
this.LayoutSections["Sidebar"] = ""
}
renderform 使用
定义 struct:
type User struct {
Id int `form:"-"`
Name interface{} `form:"username"`
Age int `form:"age,text,年龄:"`
Sex string
Intro string `form:",textarea"`
}
- StructTag 的定义用的标签用为
form
,和 ParseForm 方法 共用一个标签,标签后面有三个可选参数,用,
分割。第一个参数为表单中类型的name
的值,如果为空,则以struct field name
为值。第二个参数为表单组件的类型,如果为空,则为text
。表单组件的标签默认为struct field name
的值,否则为第三个值。 - 如果
form
标签只有一个值,则为表单中类型name
的值,除了最后一个值可以忽略外,其他位置的必须要有,
号分割,如:form:",,姓名:"
- 如果要忽略一个字段,有两种办法,一是:字段名小写开头,二是:
form
标签的值设置为-
- 现在的代码版本只能实现固定的格式,用 br 标签实现换行,无法实现 css 和 class 等代码的插入。所以,要实现 form 的高级排版,不能使用 renderform 的方法,而需要手动处理每一个字段。
controller:
func (this *AddController) Get() {
this.Data["Form"] = &User{}
this.TplName = "index.tpl"
}
Form 的参数必须是一个 struct 的指针。
template:
<form action="" method="post">{{.Form | renderform}}</form>
上面的代码生成的表单为:
Name: <input name="username" type="text" value="test"></br>
年龄:<input name="age" type="text" value="0"></br>
Sex: <input name="Sex" type="text" value=""></br>
Intro: <input name="Intro" type="textarea" value="">
模板语法
go 统一使用了 {{
和 }}
作为左右标签,没有其他的标签符号。如果您想要修改为其它符号,可以参考 模板标签 (opens new window)。
使用 .
来访问当前位置的上下文
使用 $
来引用当前模板根级的上下文
使用 $var
来访问创建的变量
模板中支持的 go 语言符号
{{"string"}} // 一般 string {{`raw string`}} // 原始 string {{'c'}} // byte
{{print nil}} // nil 也被支持
模板中的 pipeline
可以是上下文的变量输出,也可以是函数通过管道传递的返回值
{{. | FuncA | FuncB | FuncC}}
当 pipeline
的值等于:
false
或 0nil
的指针或interface
- 长度为 0 的
array
,slice
,map
,string
那么这个 pipeline
被认为是空
if ... else ... end
{{if pipeline}}{{end}}
if` 判断时,`pipeline` 为空时,相当于判断为 `False
this.Data["IsLogin"] = true
this.Data["IsHome"] = true
this.Data["IsAbout"] = true
支持嵌套的循环
{{if .IsHome}}
{{else}}
{{if .IsAbout}}{{end}}
{{end}}
也可以使用 else if
进行
{{if .IsHome}} {{else if .IsAbout}} {{else}} {{end}}
range ... end
{{range pipeline}}{{.}}{{end}}
pipeline` 支持的类型为 `array`, `slice`, `map`, `channel
range 循环内部的 .
改变为以上类型的子元素
对应的值长度为 0 时,range
不会执行,.
不会改变
pages := []struct {
Num int
}{{10}, {20}, {30}}
this.Data["Total"] = 100
this.Data["Pages"] = pages
使用 .Num
输出子元素的 Num 属性,使用 $.
引用模板中的根级上下文
{{range .Pages}} {{.Num}} of {{$.Total}} {{end}}
使用创建的变量,在这里和 go 中的 range 用法是相同的。
{{range $index, $elem := .Pages}} {{$index}} - {{$elem.Num}} - {{.Num}} of
{{$.Total}} {{end}}
range` 也支持 `else
{{range .Pages}} {{else}} {{/* 当 .Pages 为空 或者 长度为 0 时会执行这里 */}}
{{end}}
with ... end
{{with pipeline}}{{end}}
with` 用于重定向 `pipeline
{{with .Field.NestField.SubField}} {{.Var}} {{end}}
也可以对变量赋值操作
{{with $value := "My name is %s"}} {{printf . "slene"}} {{end}}
with 也支持 else
{{with pipeline}} {{else}} {{/* 当 pipeline 为空时会执行这里 */}} {{end}}
define
define 可以用来定义自模板,可用于模块定义和模板嵌套
{{define "loop"}}
<li>{{.Name}}</li>
{{end}}
使用 template
调用模板
<ul>
{{range .Items}} {{template "loop" .}} {{end}}
</ul>
template
{{template "模板名" pipeline}}
将对应的上下文 pipeline
传给模板,才可以在模板中调用
Beego 中支持直接载入文件模板
{{template "path/to/head.html" .}}
Beego 会依据你设置的模板路径读取 head.html
在模板中可以接着载入其他模板,对于模板的分模块处理很有用处
注释
允许多行文本注释,不允许嵌套
{{/* comment content support new line */}}
基本函数
变量可以使用符号 |
在函数间传递
{{.Con | markdown | addlinks}}
{{.Name | printf "%s"}}
使用括号
{{printf "nums is %s %d" (printf "%d %d" 1 2) 3}}
and
{{and .X .Y .Z}}
and 会逐一判断每个参数,将返回第一个为空的参数,否则就返回最后一个非空参数
call
{{call .Field.Func .Arg1 .Arg2}}
call 可以调用函数,并传入参数
调用的函数需要返回 1 个值 或者 2 个值,返回两个值时,第二个值用于返回 error 类型的错误。返回的错误不等于 nil 时,执行将终止。
index
index 支持 map, slice, array, string,读取指定类型对应下标的值
this.Data["Maps"] = map[string]string{"name": "Beego"}
{{index .Maps "name"}}
len
{{printf "The content length is %d" (.Content|len)}}
返回对应类型的长度,支持类型:map, slice, array, string, chan
not
not` 返回输入参数的否定值,`if true then false else true
or
{{or .X .Y .Z}}
or 会逐一判断每个参数,将返回第一个非空的参数,否则就返回最后一个参数
对应 fmt.Sprint
printf
对应 fmt.Sprintf
println
对应 fmt.Sprintln
urlquery
{{urlquery "http://beego.vip"}}
将返回
http%3A%2F%2Fbeego.vip
eq / ne / lt / le / gt / ge
这类函数一般配合在 if 中使用
eq`: `arg1 == arg2` `ne`: `arg1 != arg2` `lt`: `arg1 < arg2` `le`: `arg1 <= arg2` `gt`: `arg1 > arg2` `ge`: `arg1 >= arg2
eq 和其他函数不一样的地方是,支持多个参数,和下面的逻辑判断相同
arg1==arg2 || arg1==arg3 || arg1==arg4 ...
与 if 一起使用
{{if eq true .Var1 .Var2 .Var3}}{{end}}
{{if lt 100 200}}{{end}}
模板函数
Beego 支持用户定义模板函数,但是必须在 web.Run()
调用之前,设置如下:
func hello(in string)(out string){
out = in + "world"
return
}
web.AddFuncMap("hi",hello)
定义之后你就可以在模板中这样使用了:
{{.Content | hi}}
目前 Beego 内置的模板函数如下所示:
-
dateformat
实现了时间的格式化,返回字符串,使用方法
{{dateformat .Time "2006-01-02T15:04:05Z07:00"}}
-
date
实现了类似 PHP 的 date 函数,可以很方便的根据字符串返回时间,使用方法:
{{date .T "Y-m-d H:i:s"}}
-
compare
实现了比较两个对象的比较,如果相同返回 true,否者 false,使用方法
{{compare .A .B}}
-
substr
实现了字符串的截取,支持中文截取的完美截取,使用方法
{{substr .Str 0 30}}
-
html2str
实现了把 html 转化为字符串,剔除一些 script、css 之类的元素,返回纯文本信息,使用方法
{{html2str .Htmlinfo}}
-
str2html
实现了把相应的字符串当作 HTML 来输出,不转义,使用方法
{{str2html .Strhtml}}
-
htmlquote
实现了基本的 html 字符转义,使用方法
{{htmlquote .quote}}
-
htmlunquote
实现了基本的反转移字符,使用方法
{{htmlunquote .unquote}}
-
renderform
根据 StructTag 直接生成对应的表单,使用方法
{{&struct | renderform}}
-
assets_js
为 js 文件生成一个
<script>
标签. 使用方法{{assets_js src}}
-
assets_css
为 css 文件生成一个
<link>
标签. 使用方法{{assets_css src}}
-
config
获取
AppConfig
的值. 使用方法{{config configType configKey defaultValue}}
可选的
configType
有String, Bool, Int, Int64, Float, DIY
-
map_get
获取
map
的值用法:
// In controller Data["m"] = map[string]interface{} { "a": 1, "1": map[string]float64{ "c": 4, }, } // In view {{ map_get .m "a" }} // return 1 {{ map_get .m 1 "c" }} // return 4
-
urlfor
获取控制器方法的 URL
{{urlfor "TestController.List"}}
模板分页处理
这里所说的分页,指的是大量数据显示时,每页显示固定的数量的数据,同时显示多个分页链接,用户点击翻页链接或页码时进入到对应的网页。 分页算法中需要处理的问题:
- 当前数据一共有多少条。
- 每页多少条,算出总页数。
- 根据总页数情况,处理翻页链接。
- 对页面上传入的 Get 或 Post 数据,需要从翻页链接中继续向后传。
- 在页面显示时,根据每页数量和当前传入的页码,设置查询的 Limit 和 Skip,选择需要的数据。
- 其他的操作,就是在 View 中显示翻页链接和数据列表的问题了。
模板处理过程中经常需要分页,那么如何进行有效的开发和操作呢? 我们开发组针对这个需求开发了如下的例子,希望对大家有用
- 工具类 https://github.com/beego/wetalk/blob/master/modules/utils/paginator.go
- 模板 https://github.com/beego/wetalk/blob/master/views/base/paginator.html
- 使用方法 https://github.com/beego/wetalk/blob/master/routers/base/base.go#L458
静态文件
Go 语言内部其实已经提供了 http.ServeFile
,通过这个函数可以实现静态文件的服务。beego 针对这个功能进行了一层封装,通过下面的方式进行静态文件注册:
web.SetStaticPath("/static","public")
- 第一个参数是路径,url 路径信息
- 第二个参数是静态文件目录(相对应用所在的目录)
beego 支持多个目录的静态文件注册,用户可以注册如下的静态文件目录:
web.SetStaticPath("/images","images")
web.SetStaticPath("/css","css")
web.SetStaticPath("/js","js")
设置了如上的静态目录之后,用户访问 /images/login/login.png
,那么就会访问应用对应的目录下面的 images/login/login.png
文件。如果是访问 /static/img/logo.png
,那么就访问 public/img/logo.png
文件。
默认情况下 beego 会判断目录下文件是否存在,不存在直接返回 404 页面,如果请求的是 index.html
,那么由于 http.ServeFile
默认是会跳转的,不提供该页面的显示。因此 beego 可以设置 web.BConfig.WebConfig.DirectoryIndex=true
这样来使得显示 index.html
页面。而且开启该功能之后,用户访问目录就会显示该目录下所有的文件列表。
热升级
热升级是什么呢?了解 nginx 的同学都知道,nginx 是支持热升级的,可以用老进程服务先前链接的链接,使用新进程服务新的链接,即在不停止服务的情况下完成系统的升级与运行参数修改。那么热升级和热编译是不同的概念,热编译是通过监控文件的变化重新编译,然后重启进程,例如 bee run
就是这样的工具
Beego 主要的思路来源于: http://grisha.org/blog/2014/06/03/graceful-restart-in-golang/
import(
"log"
"net/http"
"os"
"strconv"
"github.com/beego/beego/v2/server/web/grace"
)
func handler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("WORLD!"))
w.Write([]byte("ospid:" + strconv.Itoa(os.Getpid())))
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/hello", handler)
err := grace.ListenAndServe("localhost:8080", mux)
if err != nil {
log.Println(err)
}
log.Println("Server on 8080 stopped")
os.Exit(0)
}
打开两个终端
一个终端输入:ps -ef|grep 应用名
一个终端输入请求:curl "http://127.0.0.1:8080/hello"
热升级
kill -HUP 进程 ID
打开一个终端输入请求:curl "http://127.0.0.1:8080/hello?sleep=0"
ORM
ORM 的例子在这里(opens new window)
Beego 的 ORM 被设计成为两种:
- 普通的
Orm
实例:这种实例是无状态的,因此你应该尽可能保持一个数据库只有一个实例。当然,即便每次你都创建新的实例,问题也不大,只是没有必要而已; TxOrm
:这是启动事务之后得到的Orm
对象,它只能被用于事务内,提交或者回滚之后就要丢弃,不能复用。每一个事务都需要创建一个新的实例;
快速开始
这是一个最简单的 ORM 例子:
import (
"github.com/beego/beego/v2/client/orm"
// don't forget this
_ "github.com/go-sql-driver/mysql"
)
// User -
type User struct {
ID int `orm:"column(id)"`
Name string `orm:"column(name)"`
}
func init() {
// need to register models in init
orm.RegisterModel(new(User))
// need to register default database
orm.RegisterDataBase("default", "mysql", "root:123456@tcp(127.0.0.1:3306)/beego?charset=utf8")
}
func main() {
// automatically build table
orm.RunSyncdb("default", false, true)
// create orm object
o := orm.NewOrm()
// data
user := new(User)
user.Name = "mike"
// insert data
o.Insert(user)
}
总体来说,可以分成以下几步:
需要注意的是,一定要根据自己使用的数据库来匿名引入驱动,例如引入 "github.com/go-sql-driver/mysql"
调试查询日志
在开发环境下,可以设置输出 DEBUG 的 SQL 语句:
func main() {
orm.Debug = true
开启后将会输出所有查询语句,包括执行、准备、事务等。注意,在生产环境不应该开启这个选项,因为输出日志会严重影响性能。
数据库设置与注册
Beego ORM 要求显式注册数据库的信息,而后才可以自由使用。
当然,永远不要忘了匿名引入驱动:
import (
_ "github.com/go-sql-driver/mysql"
_ "github.com/lib/pq"
_ "github.com/mattn/go-sqlite3"
)
上面三种,你根据自己需要引入一种就可以。
最简单的例子:
// 参数1 数据库的别名,用来在 ORM 中切换数据库使用
// 参数2 driverName
// 参数3 对应的链接字符串
orm.RegisterDataBase("default", "mysql", "root:root@/orm_test?charset=utf8")
// 参数4(可选) 设置最大空闲连接
// 参数5(可选) 设置最大数据库连接 (go >= 1.2)
maxIdle := 30
maxConn := 30
orm.RegisterDataBase("default", "mysql", "root:root@/orm_test?charset=utf8", orm.MaxIdleConnections(maxIdle), orm.MaxOpenConnections(maxConn))
ORM 要求必须要注册一个default
的数据库。并且,Beego 的 ORM 并没有自己管理连接,而是直接依赖于驱动。
数据库设置
最大连接数
最大连接数的设置有两种方式,一种方式是在注册数据库的时候,使用MaxOpenConnections
选项:
orm.RegisterDataBase("default", "mysql", "root:root@/orm_test?charset=utf8", orm.MaxOpenConnections(100))
也可以在注册之后修改:
orm.SetMaxOpenConns("default", 30)
最大空闲连接数
最大空闲连接数的设置有两种方式,一种方式是在注册数据库的时候,使用MaxIdleConnections
选项:
orm.RegisterDataBase("default", "mysql", "root:root@/orm_test?charset=utf8", orm.MaxIdleConnections(20))
时区
ORM 默认使用 time.Local
本地时区
- 作用于 ORM 自动创建的时间
- 从数据库中取回的时间转换成 ORM 本地时间
如果需要的话,你也可以进行更改
// 设置为 UTC 时间
orm.DefaultTimeLoc = time.UTC
ORM 在进行 RegisterDataBase
的同时,会获取数据库使用的时区,然后在 time.Time
类型存取时做相应转换,以匹配时间系统,从而保证时间不会出错。
注意:
- 鉴于 Sqlite3 的设计,存取默认都为 UTC 时间
- 使用 go-sql-driver 驱动时,请注意参数设置 从某一版本开始,驱动默认使用 UTC 时间,而非本地时间,所以请指定时区参数或者全部以 UTC 时间存取 例如:
root:root@/orm_test?charset=utf8&loc=Asia%2FShanghai
参见 loc (opens new window)/ parseTime(opens new window)
注册驱动
大多数时候,你只需要使用默认的那些驱动,有:
DRMySQL // mysql
DRSqlite // sqlite
DROracle // oracle
DRPostgres // pgsql
DRTiDB // TiDB
如果你需要注册自定义的驱动,可以使用:
// 参数1 driverName
// 参数2 数据库类型
// 这个用来设置 driverName 对应的数据库类型
// mysql / sqlite3 / postgres / tidb 这几种是默认已经注册过的,所以可以无需设置
orm.RegisterDriver("mysql", yourDriver)
ORM 模型定义与注册
Beego 的 ORM 模块要求在使用之前要先注册好模型,并且 Beego 会执行一定的校验,用于辅助检查模型和模型之间的约束。并且模型定义也会影响自动建表功能自动建表
Beego 的模型定义,大部分都是依赖于 Go 标签特性,可以设置多个特性,用;
分隔。同一个特性的不同值使用,
来分隔。
例如:
orm:"null;rel(fk)"
注册模型
注册模型有三个方法:
RegisterModel(models ...interface{})
RegisterModelWithPrefix(prefix string, models ...interface{})
:该方法会为表名加上前缀,例如RegisterModelWithPrefix("tab_", &User{})
,那么表名是tab_user
;RegisterModelWithSuffix(suffix string, models ...interface{})
:该方法会为表名加上后缀,例如RegisterModelWithSuffix("_tab", &User{})
,那么表名是user_tab
模型基本设置
表名
默认的表名规则,使用驼峰转蛇形:
AuthUser -> auth_user
Auth_User -> auth__user
DB_AuthUser -> d_b__auth_user
除了开头的大写字母以外,遇到大写会增加 _
,原名称中的下划线保留。
也可以自定义表名,只需要实现接口TableName
:
type User struct {
Id int
Name string
}
func (u *User) TableName() string {
return "auth_user"
}
同时,也可以在注册模型的时候为表名加上前缀或者后缀,参考注册模型一节。
列
为字段设置 DB 列的名称
Name string `orm:"column(user_name)"`
忽略字段
设置 -
即可忽略模型中的字段
type User struct {
// ...
AnyField string `orm:"-"`
//...
}
索引
默认情况下,可以在字段定义里面使用 Go 的标签功能指定索引,包括指定唯一索引。
例如,为单个字段增加索引:
Name string `orm:"index"`
或者,为单个字段增加 unique 键
Name string `orm:"unique"`
实现接口TableIndexI
,可以为单个或多个字段增加索引:
type User struct {
Id int
Name string
Email string
}
// 多字段索引
func (u *User) TableIndex() [][]string {
return [][]string{
[]string{"Id", "Name"},
}
}
// 多字段唯一键
func (u *User) TableUnique() [][]string {
return [][]string{
[]string{"Name", "Email"},
}
}
主键
可以用auto
显示指定一个字段为自增主键,该字段必须是 int, int32, int64, uint, uint32, 或者 uint64。
MyId int32 `orm:"auto"`
如果一个模型没有定义主键,那么 符合上述类型且名称为 Id
的模型字段将被视为自增主键。
如果不想使用自增主键,那么可以使用pk
设置为主键。
Name string `orm:"pk"`
注意,目前 Beego 的非自增主键和联合主键支持得不是特别好。建议普遍使用自增主键
鉴于 go 目前的设计,即使使用了 uint64,但你也不能存储到他的最大值。依然会作为 int64 处理。
参见 issue 6113(opens new window)
默认值
默认值是一个扩展功能,必须要显示注册默认值的Filter
,而后在模型定义里面加上default
的设置。
import (
"github.com/beego/beego/v2/client/orm/filter/bean"
"github.com/beego/beego/v2/client/orm"
)
type DefaultValueTestEntity struct {
Id int
Age int `default:"12"`
AgeInOldStyle int `orm:"default(13);bee()"`
AgeIgnore int
}
func XXX() {
builder := bean.NewDefaultValueFilterChainBuilder(nil, true, true)
orm.AddGlobalFilterChain(builder.FilterChain)
o := orm.NewOrm()
_, _ = o.Insert(&User{
ID: 1,
Name: "Tom",
})
}
自动更新时间
Created time.Time `orm:"auto_now_add;type(datetime)"`
Updated time.Time `orm:"auto_now;type(datetime)"`
- auto_now 每次 model 保存时都会对时间自动更新
- auto_now_add 第一次保存时才设置时间
对于批量的 update 此设置是不生效的
引擎
仅支持 MySQL,只需要实现接口TableEngine
。
默认使用的引擎,为当前数据库的默认引擎,这个是由你的 mysql 配置参数决定的。
你可以在模型里设置 TableEngine 函数,指定使用的引擎
type User struct {
Id int
Name string
Email string
}
// 设置引擎为 INNODB
func (u *User) TableEngine() string {
return "INNODB"
}
模型高级设置
null
数据库表默认为 NOT NULL
,设置 null 代表 ALLOW NULL
Name string `orm:"null"`
size
string 类型字段默认为 varchar(255)
设置 size 以后,db type 将使用 varchar(size)
Title string `orm:"size(60)"`
digits / decimals
设置 float32, float64 类型的浮点精度
Money float64 `orm:"digits(12);decimals(4)"`
总长度 12 小数点后 4 位 eg: 99999999.9999
type
设置为 date 时,time.Time 字段的对应 db 类型使用 date
Created time.Time `orm:"auto_now_add;type(date)"`
设置为 datetime 时,time.Time 字段的对应 db 类型使用 datetime
Created time.Time `orm:"auto_now_add;type(datetime)"`
Precision
为datetime
字段设置精度值位数,不同 DB 支持最大精度值位数也不一致。
type User struct {
...
Created time.Time `orm:"type(datetime);precision(4)"`
...
}
Comment
为字段添加注释
type User struct {
...
Status int `orm:"default(1);description(这是状态字段)"`
...
}
注意: 注释中禁止包含引号
模型字段与数据库类型的映射
在此列出 ORM 推荐的对应数据库类型,自动建表功能也会以此为标准。
默认所有的字段都是 NOT NULL
MySQL
go | mysql |
---|---|
int, int32 - 设置 auto 或者名称为 Id 时 |
integer AUTO_INCREMENT |
int64 - 设置 auto 或者名称为 Id 时 |
bigint AUTO_INCREMENT |
uint, uint32 - 设置 auto 或者名称为 Id 时 |
integer unsigned AUTO_INCREMENT |
uint64 - 设置 auto 或者名称为 Id 时 |
bigint unsigned AUTO_INCREMENT |
bool | bool |
string - 默认为 size 255 | varchar(size) |
string - 设置 type(char) 时 | char(size) |
string - 设置 type(text) 时 | longtext |
time.Time - 设置 type 为 date 时 | date |
time.Time | datetime |
byte | tinyint unsigned |
rune | integer |
int | integer |
int8 | tinyint |
int16 | smallint |
int32 | integer |
int64 | bigint |
uint | integer unsigned |
uint8 | tinyint unsigned |
uint16 | smallint unsigned |
uint32 | integer unsigned |
uint64 | bigint unsigned |
float32 | double precision |
float64 | double precision |
float64 - 设置 digits, decimals 时 | numeric(digits, decimals) |
Sqlite3
go | sqlite3 |
---|---|
int, int32, int64, uint, uint32, uint64 - 设置 auto 或者名称为 Id 时 |
integer AUTOINCREMENT |
bool | bool |
string - 默认为 size 255 | varchar(size) |
string - 设置 type(char) 时 | character(size) |
string - 设置 type(text) 时 | text |
time.Time - 设置 type 为 date 时 | date |
time.Time | datetime |
byte | tinyint unsigned |
rune | integer |
int | integer |
int8 | tinyint |
int16 | smallint |
int32 | integer |
int64 | bigint |
uint | integer unsigned |
uint8 | tinyint unsigned |
uint16 | smallint unsigned |
uint32 | integer unsigned |
uint64 | bigint unsigned |
float32 | real |
float64 | real |
float64 - 设置 digits, decimals 时 | decimal |
PostgreSQL
go | postgres |
---|---|
int, int32, int64, uint, uint32, uint64 - 设置 auto 或者名称为 Id 时 |
serial |
bool | bool |
string - 若没有指定 size 默认为 text | varchar(size) |
string - 设置 type(char) 时 | char(size) |
string - 设置 type(text) 时 | text |
string - 设置 type(json) 时 | json |
string - 设置 type(jsonb) 时 | jsonb |
time.Time - 设置 type 为 date 时 | date |
time.Time | timestamp with time zone |
byte | smallint CHECK("column" >= 0 AND "column" <= 255) |
rune | integer |
int | integer |
int8 | smallint CHECK("column" >= -127 AND "column" <= 128) |
int16 | smallint |
int32 | integer |
int64 | bigint |
uint | bigint CHECK("column" >= 0) |
uint8 | smallint CHECK("column" >= 0 AND "column" <= 255) |
uint16 | integer CHECK("column" >= 0) |
uint32 | bigint CHECK("column" >= 0) |
uint64 | bigint CHECK("column" >= 0) |
float32 | double precision |
float64 | double precision |
float64 - 设置 digits, decimals 时 | numeric(digits, decimals) |
表关系设置
rel / reverse
RelOneToOne:
type User struct {
...
Profile *Profile `orm:"null;rel(one);on_delete(set_null)"`
...
}
对应的反向关系 RelReverseOne:
type Profile struct {
...
User *User `orm:"reverse(one)"`
...
}
RelForeignKey:
type Post struct {
...
User *User `orm:"rel(fk)"` // RelForeignKey relation
...
}
对应的反向关系 RelReverseMany:
type User struct {
...
Posts []*Post `orm:"reverse(many)"` // fk 的反向关系
...
}
RelManyToMany:
type Post struct {
...
Tags []*Tag `orm:"rel(m2m)"` // ManyToMany relation
...
}
对应的反向关系 RelReverseMany:
type Tag struct {
...
Posts []*Post `orm:"reverse(many)"`
...
}
rel_table / rel_through
此设置针对 orm:"rel(m2m)"
的关系字段
rel_table
: 设置自动生成的 m2m 关系表的名称rel_through
: 如果要在 m2m 关系中使用自定义的 m2m 关系表,通过这个设置其名称,格式为pkg.path.ModelName
,例如:app.models.PostTagRel
,PostTagRel
表需要有到Post
和Tag
的关系
当设置 rel_table
时会忽略 rel_through
设置方法:
orm:"rel(m2m);rel_table(the_table_name)"
orm:"rel(m2m);rel_through(pkg.path.ModelName)"
on_delete
设置对应的 rel 关系删除时,如何处理关系字段。
cascade
: 级联删除(默认值)set_null
: 设置为 NULL,需要设置 null = trueset_default
: 设置为默认值,需要设置 default 值do_nothing
: 什么也不做,忽略
type User struct {
...
Profile *Profile `orm:"null;rel(one);on_delete(set_null)"`
...
}
type Profile struct {
...
User *User `orm:"reverse(one)"`
...
}
// 删除 Profile 时将设置 User.Profile 的数据库字段为 NULL
例子
type User struct {
Id int
Name string
}
type Post struct {
Id int
Title string
User *User `orm:"rel(fk)"`
}
假设 Post -> User
是 ManyToOne
的关系,也就是外键。
o.Filter("Id", 1).Delete()
这个时候即会删除 Id
为 1 的 User
也会删除其发布的 Post
不想删除的话,需要设置 set_null
type Post struct {
Id int
Title string
User *User `orm:"rel(fk);null;on_delete(set_null)"`
}
那这个时候,删除 User
只会把对应的 Post.user_id
设置为 NULL
当然有时候为了高性能的需要,多存点数据无所谓啊,造成批量删除才是问题。
type Post struct {
Id int
Title string
User *User `orm:"rel(fk);null;on_delete(do_nothing)"`
}
那么只要删除的时候,不操作 Post 就可以了。
Orm 增删改查
如下就可以创建一个简单的Orm
实例:
var o orm.Ormer
o = orm.NewOrm() // 创建一个 Ormer
// NewOrm 的同时会执行 orm.BootStrap (整个 app 只执行一次),用以验证模型之间的定义并缓存。
大多数情况下,你应该尽量复用Orm
实例,因为本身Orm
实例被设计为无状态的,一个数据库对应一个Orm
实例。
但是在使用事务的时候,我们会返回TxOrm
的实例,它本身是有状态的,一个事务对应一个TxOrm
实例。在使用TxOrm
时候,任何衍生查询都是在该事务内。
Insert 和 InsertWithCtx
定义:
Insert(md interface{}) (int64, error)
InsertWithCtx(ctx context.Context, md interface{}) (int64, error)
例如:
user := new(User)
id, err = Ormer.Insert(user)
这两个方法都只接收指针做为参数。
InsertOrUpdate 和 InsertOrUpdateWithCtx
定义:
InsertOrUpdate(md interface{}, colConflitAndArgs ...string) (int64, error)
InsertOrUpdateWithCtx(ctx context.Context, md interface{}, colConflitAndArgs ...string) (int64, error)
这两个方法在不同的方言之下有不同的效果:
- 对于 MySQL 来说,是执行
ON DUPLICATE KEY
。因此最后一个参数colConflictAndArgs
不需要传; - 对于 PostgreSQL 来说,是执行
ON CONFLICT cols DO UPDATE SET
,因此最后一个参数colConflictAndArgs
可以传入具体的列名; - 对于别的方言来说,你需要确认它们支持类似的语法;
InsertMulti 和 InsertMultiWithCtx
用于执行批量插入:
InsertMulti(bulk int, mds interface{}) (int64, error)
InsertMultiWithCtx(ctx context.Context, bulk int, mds interface{}) (int64, error)
参数bulk
是每一次批量插入的时候插入的数量。例如bulk<=1
代表每一批插入一条数据,而如果bulk=3
代表每次插入三条数据。你需要仔细选择批次大小,它对插入性能有很大影响。大多数情况下,你可以把bulk
设置成数据量大小。
mds
必须是一个数组,或者是一个切片。
第一个返回值表示最终插入了多少数据。
Update 和 UpdateWithCtx
使用主键来更新数据。也就是如果你使用这个方法,Beego 会尝试读取里面的主键值,而后将主键作为更新的条件。
定义是:
Update(md interface{}, cols ...string) (int64, error)
UpdateWithCtx(ctx context.Context, md interface{}, cols ...string) (int64, error)
如果你没有指定 cols
参数,那么所有的列都会被更新。
第一个返回值是受影响的行数。
Delete 和 DeleteWithCtx
使用主键来删除数据,定义:
Delete(md interface{}, cols ...string) (int64, error)
DeleteWithCtx(ctx context.Context, md interface{}, cols ...string) (int64, error)
第一个返回值是受影响的行数。
Read 和 ReadWithCtx
方法定义为:
Read(md interface{}, cols ...string) error
ReadWithCtx(ctx context.Context, md interface{}, cols ...string) error
该方法的特点是:
- 读取到的数据会被放到
md
; - 如果传入了
cols
参数,那么只会选取特定的列;
例如:
// 读取全部列
u = &User{Id: user.Id}
err = Ormer.Read(u)
// 只读取用户名这一个列
u = &User{}
err = Ormer.Read(u, "UserName")
ReadForUpdate 和 ReadForUpdateWithCtx
这两个方法的定义是:
ReadForUpdate(md interface{}, cols ...string) error
ReadForUpdateWithCtx(ctx context.Context, md interface{}, cols ...string) error
这两个方法类似于Read
和ReadWithCtx
,所不同的是,这两个方法在查询的时候加上 FOR UPDATE,因此常用于事务内部。
但是并不是所有的数据库都支持 FOR UPDATE 语句,所以你在使用的时候要首先确认自己的数据库支持 FOR UPDATE 的用法。
ReadOrCreate 和 ReadOrCreateWithCtx
它们的定义是:
ReadOrCreate(md interface{}, col1 string, cols ...string) (bool, int64, error)
ReadOrCreateWithCtx(ctx context.Context, md interface{}, col1 string, cols ...string) (bool, int64, error)
从数据库中查找数据,如果数据不存在,那么就插入。
需要注意的是,“查找-判断-插入”这三个动作并不是原子的,也不是线程安全的。因此在并发环境下,它的行为可能会超出你的预期,比如说有两个 goroutine 同时判断到数据不存在,那么它们都会尝试插入。
Raw 和 RawWithContext
Raw(query string, args ...interface{}) RawSeter
RawWithCtx(ctx context.Context, query string, args ...interface{}) RawSeter
执行原生查询。Beego 并不可能支持所有的 SQL 语法特性,因此在某些特殊情况下,你需要使用原生查询。
它会返回一个RawSeter
,你可以参阅RawSeter来确认该如何处理查询返回的结果集。
LoadRelated 和 LoadRelatedWithCtx
它们的定义是:
LoadRelated(md interface{}, name string, args ...utils.KV) (int64, error)
LoadRelatedWithCtx(ctx context.Context, md interface{}, name string, args ...utils.KV) (int64, error)
LoadRelatedWithCtx
已经被弃用。
这两个方法用于加载关联表的数据,例如:
o.LoadRelated(post,"Tags")
for _,tag := range post.Tags{
// 业务代码
}
该方法对
注意到,这两个方法最后一个参数都是传入 KV 值,目前这些 KV 值被定义在 hints
包里面,有:
hints.DefaultRelDepth
:设置关联表的解析深度为默认值 2;hints.RelDepth
:设置自定义的关联表深度;hints.Limit
:设置查询返回的行数;hints.Offset
:设置查询结果的偏移量;hints.OrderBy
:设置查询的排序;
这个方法要谨慎使用,尤其是在偏移量或者深度设置的值比较大的情况下,响应时间会比较长。
QueryM2M 和 QueryM2MWithCtx
定义是:
QueryM2M(md interface{}, name string) QueryM2Mer
QueryM2MWithCtx(ctx context.Context, md interface{}, name string) QueryM2Mer
QueryM2MWithCtx
已经不建议使用了,因为ctx
参数毫无效果。
这两个方法都是返回一个QueryM2Mer
,用于查询多对多关联关系的数据。可以参考[./query.md#QueryM2Mer]
事务
事务依赖于 Orm
实例。Orm
的用法可以参考Orm 增删改查
ORM 操作事务,支持两种范式。一种通过闭包的方式,由 Beego 本身来管理事务的生命周期。
// Beego will manage the transaction's lifecycle
// if the @param task return error, the transaction will be rollback
// or the transaction will be committed
err := o.DoTx(func(ctx context.Context, txOrm orm.TxOrmer) error {
// data
user := new(User)
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
})
在这种方式里面,第一个参数是task
,即该事务所有完成的动作。注意的是,如果它返回了 error,那么 Beego 会将整个事务回滚。
否则提交事务。
另外一个要注意的是,如果在task
执行过程中,发生了panic
,那么 Beego 会回滚事务。
我们推荐使用这种方式。
另外一种方式,则是传统的由开发自己手动管理事务的生命周期
o := orm.NewOrm()
to, err := o.Begin()
if err != nil {
logs.Error("start the transaction failed")
return
}
user := new(User)
user.Name = "test_transaction"
// do something with to. to is an instance of TxOrm
// insert data
// Using txOrm to execute SQL
_, err = to.Insert(user)
if err != nil {
logs.Error("execute transaction's sql fail, rollback.", err)
err = to.Rollback()
if err != nil {
logs.Error("roll back transaction failed", err)
}
} else {
err = to.Commit()
if err != nil {
logs.Error("commit transaction failed.", err)
}
}
无论使用哪种方式,都应该注意到,只有通过TxOrm
执行的 SQL 才会被认为是在一个事务里面。
o := orm.NewOrm()
to, err := o.Begin()
// outside the txn
o.Insert(xxx)
// inside the txn
to.Insert(xxx)
当然,从TxOrm
里面衍生出来的QuerySeter
和QueryM2Mer
,RawSeter
也是被认为在事务里面。
和事务相关的方法有:
// 需要自己管理事务生命周期
Begin() (TxOrmer, error)
BeginWithCtx(ctx context.Context) (TxOrmer, error)
BeginWithOpts(opts *sql.TxOptions) (TxOrmer, error)
BeginWithCtxAndOpts(ctx context.Context, opts *sql.TxOptions) (TxOrmer, error)
// Beego 利用闭包管理生命周期
DoTx(task func(ctx context.Context, txOrm TxOrmer) error) error
DoTxWithCtx(ctx context.Context, task func(ctx context.Context, txOrm TxOrmer) error) error
DoTxWithOpts(opts *sql.TxOptions, task func(ctx context.Context, txOrm TxOrmer) error) error
DoTxWithCtxAndOpts(ctx context.Context, opts *sql.TxOptions, task func(ctx context.Context, txOrm TxOrmer) error) error
QueryBuilder 构造复杂查询
QueryBuilder 提供了一个简便,流畅的 SQL 查询构造器。在不影响代码可读性的前提下用来快速的建立 SQL 语句。
QueryBuilder 在功能上与 ORM 重合, 但是各有利弊。ORM 更适用于简单的 CRUD 操作,而 QueryBuilder 则更适用于复杂的查询,例如查询中包含子查询和多重联结。
使用方法:
// User 包装了下面的查询结果
type User struct {
Name string
Age int
}
var users []User
// 获取 QueryBuilder 对象. 需要指定数据库驱动参数。
// 第二个返回值是错误对象,在这里略过
qb, _ := orm.NewQueryBuilder("mysql")
// 构建查询对象
qb.Select("user.name",
"profile.age").
From("user").
InnerJoin("profile").On("user.id_user = profile.fk_user").
Where("age > ?").
OrderBy("name").Desc().
Limit(10).Offset(0)
// 导出 SQL 语句
sql := qb.String()
// 执行 SQL 语句
o := orm.NewOrm()
o.Raw(sql, 20).QueryRows(&users)
完整 API 接口:
type QueryBuilder interface {
Select(fields ...string) QueryBuilder
ForUpdate() QueryBuilder
From(tables ...string) QueryBuilder
InnerJoin(table string) QueryBuilder
LeftJoin(table string) QueryBuilder
RightJoin(table string) QueryBuilder
On(cond string) QueryBuilder
Where(cond string) QueryBuilder
And(cond string) QueryBuilder
Or(cond string) QueryBuilder
In(vals ...string) QueryBuilder
OrderBy(fields ...string) QueryBuilder
Asc() QueryBuilder
Desc() QueryBuilder
Limit(limit int) QueryBuilder
Offset(offset int) QueryBuilder
GroupBy(fields ...string) QueryBuilder
Having(cond string) QueryBuilder
Update(tables ...string) QueryBuilder
Set(kv ...string) QueryBuilder
Delete(tables ...string) QueryBuilder
InsertInto(table string, fields ...string) QueryBuilder
Values(vals ...string) QueryBuilder
Subquery(sub string, alias string) string
String() string
}
目前支持Postgress
, MySQL
和TiDB
的支持。
QuerySeter 复杂查询
ORM 以 QuerySeter 来组织查询,每个返回 QuerySeter 的方法都会获得一个新的 QuerySeter 对象。
基本使用方法:
o := orm.NewOrm()
// 获取 QuerySeter 对象,user 为表名
qs := o.QueryTable("user")
// 也可以直接使用 Model 结构体作为表名
qs = o.QueryTable(&User)
// 也可以直接使用对象作为表名
user := new(User)
qs = o.QueryTable(user) // 返回 QuerySeter
// 后面可以调用qs上的方法,执行复杂查询。
QuerySeter
的方法大体上可以分成两类:
-
中间方法:用于构造查询
-
终结方法:用于执行查询并且封装结果
-
每个返回 QuerySeter 的 api 调用时都会新建一个 QuerySeter,不影响之前创建的。
-
高级查询使用 Filter 和 Exclude 来做常用的条件查询。囊括两种清晰的过滤规则:包含, 排除
查询表达式
Beego 设计了自己的查询表达式,这些表达式可以用在很多方法上。
一般来说,你可以对单表的字段使用表达式,也可以在关联表上使用表达式。例如单个使用:
qs.Filter("id", 1) // WHERE id = 1
或者在关联表里面使用:
qs.Filter("profile__age", 18) // WHERE profile.age = 18
qs.Filter("Profile__Age", 18) // 使用字段名和 Field 名都是允许的
qs.Filter("profile__age__gt", 18) // WHERE profile.age > 18
// WHERE profile.age IN (18, 20) AND NOT profile_id < 1000
字段组合的前后顺序依照表的关系,比如 User 表拥有 Profile 的外键,那么对 User 表查询对应的 Profile.Age 为条件,则使用 Profile__Age
注意,字段的分隔符号使用双下划线 __
。
除了描述字段, 表达式的尾部可以增加操作符以执行对应的 sql 操作。比如 Profile__Age__gt
代表 Profile.Age > 18 的条件查询。在没有指定操作符的情况下,会使用=
作为操作符。
当前支持的操作符号:
- exact / iexact 等于
- contains / icontains 包含
- gt / gte 大于 / 大于等于
- lt / lte 小于 / 小于等于
- startswith / istartswith 以...起始
- endswith / iendswith 以...结束
- in
- isnull
后面以 i
开头的表示:大小写不敏感
exact
Filter / Exclude / Condition expr 的默认值
qs.Filter("name", "slene") // WHERE name = 'slene'
qs.Filter("name__exact", "slene") // WHERE name = 'slene'
// 使用 = 匹配,大小写是否敏感取决于数据表使用的 collation
qs.Filter("profile_id", nil) // WHERE profile_id IS NULL
iexact
qs.Filter("name__iexact", "slene")
// WHERE name LIKE 'slene'
// 大小写不敏感,匹配任意 'Slene' 'sLENE'
contains
qs.Filter("name__contains", "slene")
// WHERE name LIKE BINARY '%slene%'
// 大小写敏感, 匹配包含 slene 的字符
icontains
qs.Filter("name__icontains", "slene")
// WHERE name LIKE '%slene%'
// 大小写不敏感, 匹配任意 'im Slene', 'im sLENE'
in
qs.Filter("age__in", 17, 18, 19, 20)
// WHERE age IN (17, 18, 19, 20)
ids:=[]int{17,18,19,20}
qs.Filter("age__in", ids)
// WHERE age IN (17, 18, 19, 20)
// 同上效果
gt / gte
qs.Filter("profile__age__gt", 17)
// WHERE profile.age > 17
qs.Filter("profile__age__gte", 18)
// WHERE profile.age >= 18
lt / lte
qs.Filter("profile__age__lt", 17)
// WHERE profile.age < 17
qs.Filter("profile__age__lte", 18)
// WHERE profile.age <= 18
startswith
qs.Filter("name__startswith", "slene")
// WHERE name LIKE BINARY 'slene%'
// 大小写敏感, 匹配以 'slene' 起始的字符串
istartswith
qs.Filter("name__istartswith", "slene")
// WHERE name LIKE 'slene%'
// 大小写不敏感, 匹配任意以 'slene', 'Slene' 起始的字符串
endswith
qs.Filter("name__endswith", "slene")
// WHERE name LIKE BINARY '%slene'
// 大小写敏感, 匹配以 'slene' 结束的字符串
iendswith
qs.Filter("name__iendswithi", "slene")
// WHERE name LIKE '%slene'
// 大小写不敏感, 匹配任意以 'slene', 'Slene' 结束的字符串
isnull
qs.Filter("profile__isnull", true)
qs.Filter("profile_id__isnull", true)
// WHERE profile_id IS NULL
qs.Filter("profile__isnull", false)
// WHERE profile_id IS NOT NULL
中间方法
Flter
Filter(string, ...interface{}) QuerySeter
多次调用Filter
方法,会使用AND
将它们连起来。
qs.Filter("profile__isnull", true).Filter("name", "slene")
// WHERE profile_id IS NULL AND name = 'slene'
FilterRaw
FilterRaw(string, string) QuerySeter
该方法会直接把输入当做是一个查询条件,因此如果输入有错误,那么拼接得来的 SQL 则无法运行。Beego 本身并不会执行任何的检查。
例如:
qs.FilterRaw("user_id IN (SELECT id FROM profile WHERE age>=18)")
//sql-> WHERE user_id IN (SELECT id FROM profile WHERE age>=18)
Exclude
Exclude(string, ...interface{}) QuerySeter
准确来说,Exclude
表达的是NOT
的语义:
qs.Filter("profile__age__in", 18, 20).Exclude("profile__lt", 1000)
// WHERE profile.age IN (18, 20) AND NOT profile_id < 1000
SetCond
SetCond(*Condition) QuerySeter
设置查询条件:
cond := orm.NewCondition()
cond1 := cond.And("profile__isnull", false).AndNot("status__in", 1).Or("profile__age__gt", 2000)
//sql-> WHERE T0.`profile_id` IS NOT NULL AND NOT T0.`Status` IN (?) OR T1.`age` > 2000
num, err := qs.SetCond(cond1).Count()
Condition
中使用的表达式,可以参考查询表达式
GetCond
GetCond() *Condition
获得查询条件。例如:
cond := orm.NewCondition()
cond = cond.And("profile__isnull", false).AndNot("status__in", 1)
qs = qs.SetCond(cond)
cond = qs.GetCond()
cond := cond.Or("profile__age__gt", 2000)
//sql-> WHERE T0.`profile_id` IS NOT NULL AND NOT T0.`Status` IN (?) OR T1.`age` > 2000
num, err := qs.SetCond(cond).Count()
Limit
Limit(limit interface{}, args ...interface{}) QuerySeter
该方法第二个参数args
实际上只是表达偏移量。也就是说:
- 如果你只传了
limit
,例如说 10,那么相当于LIMIT 10
- 如果你同时传了
args
为 2, 那么相当于LIMIT 10 OFFSET 2
,或者说LIMIT 2, 10
var DefaultRowsLimit = 1000 // ORM 默认的 limit 值为 1000
// 默认情况下 select 查询的最大行数为 1000
// LIMIT 1000
qs.Limit(10)
// LIMIT 10
qs.Limit(10, 20)
// LIMIT 10 OFFSET 20 注意跟 SQL 反过来的
qs.Limit(-1)
// no limit
qs.Limit(-1, 100)
// LIMIT 18446744073709551615 OFFSET 100
// 18446744073709551615 是 1<<64 - 1 用来指定无 limit 限制 但有 offset 偏移的情况
如果你没有调用该方法,或者调用了该方法,但是传入了一个负数,Beego 会使用默认的值,例如 1000。
Offset
Offset(offset interface{}) QuerySeter
设置偏移量,等同于Limit
方法的第二个参数。
GroupBy
GroupBy(exprs ...string) QuerySeter
设置分组,参数是列名。
OrderBy
OrderBy(exprs ...string) QuerySeter
设置排序,使用的是一种特殊的表达:
- 如果传入的是列名,那么代表的是按照列名 ASC 排序;
- 如果传入的列名前面有一个负号,那么代表的是按照列名 DESC 排序;
例如:
// ORDER BY STATUS DESC
qs.OrderBy("-status")
// ORDER BY ID ASC, STATUS DESC
qs.OrderBy("id", "-status")
同样地,也可以使用查询表达式,例如:
qs.OrderBy("id", "-profile__age")
// ORDER BY id ASC, profile.age DESC
qs.OrderBy("-profile__age", "profile")
// ORDER BY profile.age DESC, profile_id ASC
ForceIndex
qs.ForceIndex(`idx_name1`,`idx_name2`)
强制使用某个索引。你需要确认自己使用的数据库支持该特性,并且确认该特性在数据库上的语义。
参数是索引的名字。
UseIndex
UseIndex(indexes ...string) QuerySeter
使用某个索引。你需要确认自己使用的数据库支持该特性,并且确认该特性在数据库上的语义。比如说在一些数据库上,该特性是“建议使用某个索引”,但是数据库在真实执行查询的时候,完全可能不使用这里指定的索引。
参数是索引的名字。
IgnoreIndex
IgnoreIndex(indexes ...string) QuerySeter
忽略某个索引。你需要确认自己使用的数据库支持该特性,并且确认该特性在数据库上的语义。比如说在一些数据库上,该特性是“建议不使用某个索引”,但是数据库在真实执行查询的时候,完全可能使用这里指定的索引。
参数是索引的名字。
RelatedSel
RelatedSel(params ...interface{}) QuerySeter
加载关联表的数据。如果没有传入参数,那么 Beego 加载所有关联表的数据。而如果传入了参数,那么只会加载特定的关联表数据。
在加载的时候,如果对应的字段是可以为 NULL 的,那么会使用 LEFT JOIN,否则使用 JOIN。
例如:
// 使用 LEFT JOIN 加载 user 里面的所有关联表数据
qs.RelatedSel().One(&user)
// 使用 LEFT JOIN 只加载 user 里面 profile 的数据
qs.RelatedSel("profile").One(&user)
user.Profile.Age = 32
默认情况下直接调用 RelatedSel 将进行最大DefaultRelsDepth
层的关系查询
Distinct
Distinct() QuerySeter
为查询加上 DISTINCT 关键字
ForUpdate
ForUpdate() QuerySeter
为查询加上 FOR UPDATE 片段。
PrepareInsert
PrepareInsert() (Inserter, error)
用于一次 prepare 多次 insert 插入,以提高批量插入的速度。
var users []*User
...
qs := o.QueryTable("user")
i, _ := qs.PrepareInsert()
for _, user := range users {
id, err := i.Insert(user)
if err == nil {
...
}
}
// PREPARE INSERT INTO user (`name`, ...) VALUES (?, ...)
// EXECUTE INSERT INTO user (`name`, ...) VALUES ("slene", ...)
// EXECUTE ...
// ...
i.Close() // 别忘记关闭 statement
Aggregate
Aggregate(s string) QuerySeter
指定聚合函数。例如:
type result struct {
DeptName string
Total int
}
var res []result
o.QueryTable("dept_info").Aggregate("dept_name,sum(salary) as total").GroupBy("dept_name").All(&res)
终结方法
Count
Count() (int64, error)
执行查询并且返回结果集的大小。
Exist
Exist() bool
判断查询是否返回数据。等效于Count()
返回大于 0 的值。
Update
Update(values Params) (int64, error)
依据当前查询条件,进行批量更新操作。
num, err := o.QueryTable("user").Filter("name", "slene").Update(orm.Params{
"name": "astaxie",
})
fmt.Printf("Affected Num: %s, %s", num, err)
// SET name = "astaixe" WHERE name = "slene"
原子操作增加字段值
// 假设 user struct 里有一个 nums int 字段
num, err := o.QueryTable("user").Update(orm.Params{
"nums": orm.ColValue(orm.ColAdd, 100),
})
// SET nums = nums + 100
orm.ColValue
支持以下操作
ColAdd // 加
ColMinus // 减
ColMultiply // 乘
ColExcept // 除
Delete
Delete() (int64, error)
删除数据,返回被删除的数据行数。
All
All(container interface{}, cols ...string) (int64, error)
返回对应的结果集对象。参数支持 *[]Type
和 *[]*Type
两种形式的切片
var users []*User
num, err := o.QueryTable("user").Filter("name", "slene").All(&users)
fmt.Printf("Returned Rows Num: %s, %s", num, err)
All / Values / ValuesList / ValuesFlat
受到 Limit 的限制,默认最大行数为 1000
可以指定返回的字段:
type Post struct {
Id int
Title string
Content string
Status int
}
// 只返回 Id 和 Title
var posts []Post
o.QueryTable("post").Filter("Status", 1).All(&posts, "Id", "Title")
对象的其他字段值将会是对应类型的默认值。
One
One(container interface{}, cols ...string) error
尝试返回单条记录:
var user User
err := o.QueryTable("user").Filter("name", "slene").One(&user)
if err == orm.ErrMultiRows {
// 多条的时候报错
fmt.Printf("Returned Multi Rows Not One")
}
if err == orm.ErrNoRows {
// 没有找到记录
fmt.Printf("Not row found")
}
Values
Values(results *[]Params, exprs ...string) (int64, error)
返回结果集的 key => value
值
key 为模型里的字段名, value 是interface{}
类型,例如,如果你要将 value 赋值给 struct 中的某字段,需要根据结构体对应字段类型使用断言 (opens new window)获取真实值。:Name : m["Name"].(string)
var maps []orm.Params
num, err := o.QueryTable("user").Values(&maps)
if err == nil {
fmt.Printf("Result Nums: %d\n", num)
for _, m := range maps {
fmt.Println(m["Id"], m["Name"])
}
}
TODO: 暂不支持级联查询 RelatedSel 直接返回 Values
第二个参数可以是列名,也可以是查询表达式:
var maps []orm.Params
num, err := o.QueryTable("user").Values(&maps, "id", "name", "profile", "profile__age")
if err == nil {
fmt.Printf("Result Nums: %d\n", num)
for _, m := range maps {
fmt.Println(m["Id"], m["Name"], m["Profile"], m["Profile__Age"])
// map 中的数据都是展开的,没有复杂的嵌套
}
}
ValuesList
ValuesList(results *[]ParamsList, exprs ...string) (int64, error)
顾名思义,返回的结果集以切片存储,其排列与模型中定义的字段顺序一致,每个元素值是 string 类型。
var lists []orm.ParamsList
num, err := o.QueryTable("user").ValuesList(&lists)
if err == nil {
fmt.Printf("Result Nums: %d\n", num)
for _, row := range lists {
fmt.Println(row)
}
}
当然也可以指定查询表达式返回指定的字段:
var lists []orm.ParamsList
num, err := o.QueryTable("user").ValuesList(&lists, "name", "profile__age")
if err == nil {
fmt.Printf("Result Nums: %d\n", num)
for _, row := range lists {
fmt.Printf("Name: %s, Age: %s\m", row[0], row[1])
}
}
ValuesFlat
ValuesFlat(result *ParamsList, expr string) (int64, error)
只返回特定的字段的值,将结果集展开到单个切片里。
var list orm.ParamsList
num, err := o.QueryTable("user").ValuesFlat(&list, "name")
if err == nil {
fmt.Printf("Result Nums: %d\n", num)
fmt.Printf("All User Names: %s", strings.Join(list, ", "))
}
RowsToMap 和 RowsToStruct
这两个方法都没有实现。
原生查询
大多数时候,你都不应该使用原生查询。只有在无可奈何的情况下才应该考虑原生查询。使用原生查询可以:
- 无需使用 ORM 表定义
- 多数据库,都可直接使用占位符号
?
,自动转换 - 查询时的参数,支持使用 Model Struct 和 Slice, Array
例如:
o := orm.NewOrm()
ids := []int{1, 2, 3}
var r RawSter
r = o.Raw("SELECT name FROM user WHERE id IN (?, ?, ?)", ids)
这里得到一个RawSeter
的实例,它包含极多的方法。
Exec
执行 sql 语句,返回 sql.Result (opens new window)对象。
res, err := o.Raw("UPDATE user SET name = ?", "your").Exec()
if err == nil {
num, _ := res.RowsAffected()
fmt.Println("mysql row affected nums: ", num)
}
一般来说,使用该方法的应该是非 SELECT 语句。
QueryRow 和 QueryRows
这两个方法的定义是:
QueryRow(containers ...interface{}) error
QueryRows(containers ...interface{}) (int64, error)
这两个方法会把返回的数据赋值给container
。
例如:
var name string
var id int
// id==2 name=="slene"
dORM.Raw("SELECT 'id','name' FROM `user`").QueryRow(&id,&name)
在这个例子里面,QueryRow
会查询得到两列,并且只有一行。在这种情况下,两列的值分别被赋值给id
和name
。
使用QueryRows
的例子:
var ids []int
var names []int
query = "SELECT 'id','name' FROM `user`"
// ids=>{1,2},names=>{"nobody","slene"}
num, err = dORM.Raw(query).QueryRows(&ids,&names)
同样地,QueryRows
也是按照列来返回,因此可以注意到在例子里面我们声明了两个切片,分别对应于id
和name
两个列。
SetArgs
该方法用于设置参数。注意的是,参数个数必须和占位符?
的数量保持一致。其定义:
SetArgs(...interface{}) RawSeter
例如:
var name string
var id int
query := "SELECT 'id','name' FROM `user` WHERE `id`=?"
// id==2 name=="slene"
// 等效于"SELECT 'id','name' FROM `user` WHERE `id`=1"
dORM.Raw(query).SetArgs(1).QueryRow(&id,&name)
也可以用于单条 sql 语句,重复利用,替换参数然后执行。
res, err := r.SetArgs("arg1", "arg2").Exec()
res, err := r.SetArgs("arg1", "arg2").Exec()
Values / ValuesList / ValuesFlat
Values(container *[]Params, cols ...string) (int64, error)
ValuesList(container *[]ParamsList, cols ...string) (int64, error)
ValuesFlat(container *ParamsList, cols ...string) (int64, error)
参考QuerySeter
中的:
RowsToMap
RowsToMap(result *Params, keyCol, valueCol string) (int64, error)
SQL 查询结果是这样:
name | value |
---|---|
total | 100 |
found | 200 |
查询结果匹配到 map 里
res := make(orm.Params)
nums, err := o.Raw("SELECT name, value FROM options_table").RowsToMap(&res, "name", "value")
// res is a map[string]interface{}{
// "total": 100,
// "found": 200,
// }
RowsToStruct
RowsToStruct(ptrStruct interface{}, keyCol, valueCol string) (int64, error)
SQL 查询结果是这样
name | value |
---|---|
total | 100 |
found | 200 |
查询结果匹配到 struct 里
type Options struct {
Total int
Found int
}
res := new(Options)
nums, err := o.Raw("SELECT name, value FROM options_table").RowsToStruct(res, "name", "value")
fmt.Println(res.Total) // 100
fmt.Println(res.Found) // 200
匹配支持的名称转换为 snake -> camel, eg: SELECT user_name ... 需要你的 struct 中定义有 UserName
Prepare
Prepare() (RawPreparer, error)
用于一次 prepare 多次 exec,以提高批量执行的速度。
p, err := o.Raw("UPDATE user SET name = ? WHERE name = ?").Prepare()
res, err := p.Exec("testing", "slene")
res, err = p.Exec("testing", "astaxie")
// ...
p.Close() // 别忘记关闭 statement
关联表查询
关联表的查询,一方面可以使用QuerySeter,一方面也可以使用QueryM2Mer
。
创建一个 QueryM2Mer
对象:
o := orm.NewOrm()
post := Post{Id: 1}
m2m := o.QueryM2M(&post, "Tags")
// 第一个参数的对象,主键必须有值
// 第二个参数为对象需要操作的 M2M 字段
// QueryM2Mer 的 api 将作用于 Id 为 1 的 Post
它的具体 API 有:
- Add(...interface{}) (int64, error)
- Remove(...interface{}) (int64, error)
- Exist(interface{}) bool
- Clear() (int64, error)
- Count() (int64, error)
QueryM2Mer Add
tag := &Tag{Name: "golang"}
o.Insert(tag)
num, err := m2m.Add(tag)
if err == nil {
fmt.Println("Added nums: ", num)
}
Add
支持多种类型 Tag
,*Tag
,[]*Tag
,[]Tag
,[]interface{}
。
var tags []*Tag
...
// 读取 tags 以后
...
num, err := m2m.Add(tags)
if err == nil {
fmt.Println("Added nums: ", num)
}
// 也可以多个作为参数传入
// m2m.Add(tag1, tag2, tag3)
QueryM2Mer Remove
从 M2M 关系中删除 tag
Remove` 支持多种类型 `Tag` `*Tag` `[]*Tag` `[]Tag` `[]interface{}
var tags []*Tag
...
// 读取 tags 以后
...
num, err := m2m.Remove(tags)
if err == nil {
fmt.Println("Removed nums: ", num)
}
// 也可以多个作为参数传入
// m2m.Remove(tag1, tag2, tag3)
QueryM2Mer Exist
判断 Tag 是否存在于 M2M 关系中
if m2m.Exist(&Tag{Id: 2}) {
fmt.Println("Tag Exist")
}
QueryM2Mer Clear
清除所有 M2M 关系
nums, err := m2m.Clear()
if err == nil {
fmt.Println("Removed Tag Nums: ", nums)
}
QueryM2Mer Count
计算 Tag 的数量
nums, err := m2m.Count()
if err == nil {
fmt.Println("Total Nums: ", nums)
}
日志模块
这是一个用来处理日志的库,它的设计思路来自于 database/sql
,目前支持的引擎有 file、console、net、smtp、es、slack。
例子参考beego-example (opens new window)下的logs
部分
快速开始
首先引入包:
import (
"github.com/beego/beego/v2/core/logs"
)
然后添加输出引擎(log 支持同时输出到多个引擎),这里我们以 console 为例,第一个参数是引擎名:
logs.SetLogger(logs.AdapterConsole)
添加输出引擎也支持第二个参数,用来表示配置信息,对于不同的引擎来说,其配置也是不同的。详细的配置请看下面介绍:
logs.SetLogger(logs.AdapterFile,`{"filename":"project.log","level":7,"maxlines":0,"maxsize":0,"daily":true,"maxdays":10,"color":true}`)
然后我们就可以在我们的逻辑中开始任意的使用了:
package main
import (
"github.com/beego/beego/v2/core/logs"
)
func main() {
//an official log.Logger
l := logs.GetLogger()
l.Println("this is a message of http")
//an official log.Logger with prefix ORM
logs.GetLogger("ORM").Println("this is a message of orm")
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")
}
多个实例
一般推荐使用通用方式进行日志,但依然支持单独声明来使用独立的日志
package main
import (
"github.com/beego/beego/v2/core/logs"
)
func main() {
log := logs.NewLogger()
log.SetLogger(logs.AdapterConsole)
log.Debug("this is a debug message")
}
输出文件名和行号
日志默认不输出调用的文件名和文件行号,如果你期望输出调用的文件名和文件行号,可以如下设置
logs.EnableFuncCallDepth(true)
开启传入参数 true,关闭传入参数 false,默认是关闭的.
如果你的应用自己封装了调用 log 包,那么需要设置 SetLogFuncCallDepth,默认是 2,也就是直接调用的层级,如果你封装了多层,那么需要根据自己的需求进行调整.
logs.SetLogFuncCallDepth(3)
异步输出日志
为了提升性能, 可以设置异步输出:
logs.Async()
异步输出允许设置缓冲 chan 的大小
logs.Async(1e3)
自定义日志格式
在一些情况下,我们可能需要自己定义自己的日志格式规范。这种时候,可以考虑通过扩展LogFormatter
。
type LogFormatter interface {
Format(lm *LogMsg) string
}
LogMsg
包含了一条日志的所有部分。需要注意的是,如果你希望输出文件名和行号,那么应该参考输出文件名和行号,设置对应的参数。
例子:PatternLogFormatter
该实现的设计思路,是希望能够使用类似于占位符的东西来定义一条日志应该如何输出。
例子:
package main
import (
"github.com/beego/beego/v2/core/logs"
)
func main() {
f := &logs.PatternLogFormatter{
Pattern: "%F:%n|%w%t>> %m",
WhenFormat: "2006-01-02",
}
logs.RegisterFormatter("pattern", f)
_ = logs.SetGlobalFormatter("pattern")
logs.Info("hello, world")
}
我们先初始化了一个PatternLogFormatter
实例,而后注册为pattern
。
再然后我们使用logs.SetGlobalFormatter("pattern")
设置全局所有的引擎都使用这个格式。
最终我们输出日志/beego-example/logger/formatter/pattern/main.go:31|2020-10-29[I]>> hello, world
如果我们只希望在某个特定的引擎上使用这个格式,我们可以通过初始化引擎的时候,设置:
_ = logs.SetLogger("console",`{"formatter": "pattern"}`)
PatternLogFormatter
支持的占位符及其含义:
- 'w' 时间
- 'm' 消息
- 'f' 文件名
- 'F' 文件全路径
- 'n' 行数
- 'l' 消息级别,数字表示
- 't' 消息级别,简写,例如
[I]
代表 INFO - 'T' 消息级别,全称
引擎配置设置
-
console: 命令行输出,默认输出到
os.Stdout
:logs.SetLogger(logs.AdapterConsole, `{"level":1,"color":true}`)
主要的参数如下说明:
- level 输出的日志级别
- color 是否开启打印日志彩色打印(需环境支持彩色输出)
-
file:输出到文件,设置的例子如下所示:
logs.SetLogger(logs.AdapterFile, `{"filename":"test.log"}`)
主要的参数如下说明:
filename
保存的文件名maxlines
每个文件保存的最大行数,默认值 1000000maxsize
每个文件保存的最大尺寸,默认值是 1 << 28, 256 MBdaily
是否按照每天logrotate
,默认是true
maxdays
文件最多保存多少天,默认保存 7 天rotate
是否开启logrotate
,默认是true
level
日志保存的时候的级别,默认是Trace
级别perm
日志文件权限
-
multifile:不同级别的日志会输出到不同的文件中:
设置的例子如下所示:
logs.SetLogger(logs.AdapterMultiFile, `{"filename":"test.log","separate":["emergency", "alert", "critical", "error", "warning", "notice", "info", "debug"]}`)
主要的参数如下说明(除
separate
外,均与 file 相同):filename
保存的文件名maxlines
每个文件保存的最大行数,默认值 1000000maxsize
每个文件保存的最大尺寸,默认值是 1 << 28, //256 MBdaily
是否按照每天 logrotate,默认是 truemaxdays
文件最多保存多少天,默认保存 7 天rotate
是否开启 logrotate,默认是 truelevel
日志保存的时候的级别,默认是 Trace 级别perm
日志文件权限separate
需要单独写入文件的日志级别,设置后命名类似 test.error.log
-
conn: 网络输出,设置的例子如下所示:
logs.SetLogger(logs.AdapterConn, `{"net":"tcp","addr":":7020"}`)
主要的参数说明如下:
- reconnectOnMsg 是否每次链接都重新打开链接,默认是 false
- reconnect 是否自动重新链接地址,默认是 false
- net 发开网络链接的方式,可以使用 tcp、unix、udp 等
- addr 网络链接的地址
- level 日志保存的时候的级别,默认是 Trace 级别
-
smtp: 邮件发送,设置的例子如下所示:
logs.SetLogger(logs.AdapterMail, `{"username":"beegotest@gmail.com","password":"xxxxxxxx","host":"smtp.gmail.com:587","sendTos":["xiemengjun@gmail.com"]}`)
主要的参数说明如下:
username
:smtp
验证的用户名password
:smtp
验证密码host
: 发送的邮箱地址sendTos
: 邮件需要发送的人,支持多个subject
: 发送邮件的标题,默认是Diagnostic message from server
level
: 日志发送的级别,默认是 Trace 级别
-
ElasticSearch:输出到 ElasticSearch:
logs.SetLogger(logs.AdapterEs, `{"dsn":"http://localhost:9200/","level":1}`)
-
简聊: 输出到简聊:
logs.SetLogger(logs.AdapterJianLiao, `{"authorname":"xxx","title":"beego", "webhookurl":"https://jianliao.com/xxx", "redirecturl":"https://jianliao.com/xxx","imageurl":"https://jianliao.com/xxx","level":1}`)
-
slack: 输出到 slack
logs.SetLogger(logs.AdapterSlack, `{"webhookurl":"https://slack.com/xxx","level":1}`)
数据校验
数据校验是用于数据验证和错误收集的模块。数据校验可以用于前端输入数据校验,或者后端拿到下游响应校验。某些时候也可以用来验证数据库数据完整性。
这部分例子在Validation 例子(opens new window)
安装及测试
安装:
go get github.com/beego/beego/v2/core/validation
测试:
go test 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)
}
用户也可以通过声明式的写法来表达某个字段需要遵守的校验规则,声明式写法是通过结构体的标签来实现的:
- 验证函数写在 "valid" 的标签里
- 各个函数之间用分号 ";" 分隔,分号后面可以有空格
- 参数用括号 "()" 括起来,多个参数之间用逗号 "," 分开,逗号后面可以有空格
- 正则函数(Match)的匹配模式用两斜杠 "/" 括起来
- 各个函数的结果的 key 值为字段名.验证函数名
import (
"log"
"strings"
"github.com/beego/beego/v2/core/validation"
)
type user struct {
Id int
Name string `valid:"Required;Match(/^Bee.*/)"` // Name 不能为空并且以 Bee 开头
Age int `valid:"Range(1, 140)"` // 1 <= Age <= 140,超出此范围即为不合法
Email string `valid:"Email; MaxSize(100)"` // Email 字段需要符合邮箱格式,并且最大长度不能大于 100 个字符
Mobile string `valid:"Mobile"` // Mobile 必须为正确的手机号
IP string `valid:"IP"` // IP 必须为一个正确的 IPv4 地址
}
// 如果你的 struct 实现了接口 validation.ValidFormer
// 当 StructTag 中的测试都成功时,将会执行 Valid 函数进行自定义验证
func (u *user) Valid(v *validation.Validation) {
if strings.Index(u.Name, "admin") != -1 {
// 通过 SetError 设置 Name 的错误信息,HasErrors 将会返回 true
v.SetError("Name", "名称里不能含有 admin")
}
}
func main() {
valid := validation.Validation{}
u := user{Name: "Beego", Age: 2, Email: "dev@web.me"}
b, err := valid.Valid(&u)
if err != nil {
// handle error
}
if !b {
// validation does not pass
// blabla...
for _, err := range valid.Errors {
log.Println(err.Key, err.Message)
}
}
}
需要注意的是,Valid
方法是用户自定义验证方法,它接收两个参数:
- 字段名字
- 错误原因
在实现该接口的时候,只需要将错误信息写入validation.Validation
.
StructTag
可用的验证函数:
Required
不为空,即各个类型要求不为其零值Min(min int)
最小值,有效类型:int
,其他类型都将不能通过验证Max(max int)
最大值,有效类型:int
,其他类型都将不能通过验证Range(min, max int)
数值的范围,有效类型:int
,他类型都将不能通过验证MinSize(min int)
最小长度,有效类型:string slice
,其他类型都将不能通过验证MaxSize(max int)
最大长度,有效类型:string slice
,其他类型都将不能通过验证Length(length int)
指定长度,有效类型:string slice
,其他类型都将不能通过验证Alpha
alpha 字符,有效类型:string
,其他类型都将不能通过验证Numeric
数字,有效类型:string
,其他类型都将不能通过验证AlphaNumeric
alpha 字符或数字,有效类型:string
,其他类型都将不能通过验证Match(pattern string)
正则匹配,有效类型:string
,其他类型都将被转成字符串再匹配(fmt.Sprintf("%v", obj).Match)AlphaDash
alpha 字符或数字或横杠-_
,有效类型:string
,其他类型都将不能通过验证Email
邮箱格式,有效类型:string
,其他类型都将不能通过验证IP
IP 格式,目前只支持 IPv4 格式验证,有效类型:string
,其他类型都将不能通过验证Base64
base64 编码,有效类型:string
,其他类型都将不能通过验证Mobile
手机号,有效类型:string
,其他类型都将不能通过验证Tel
固定电话号,有效类型:string
,其他类型都将不能通过验证Phone
手机号或固定电话号,有效类型:string
,其他类型都将不能通过验证ZipCode
邮政编码,有效类型:string
,其他类型都将不能通过验证
自定义验证
我们允许自己注册验证逻辑。使用方法:
AddCustomFunc(name string, f CustomFunc) error
例如:
type user struct {
// ...
Address string `valid:"ChinaAddress"`
}
func main() {
_ = validation.AddCustomFunc("ChinaAddress", func(v *validation.Validation, obj interface{}, key string) {
addr, ok := obj.(string)
if !ok {
return
}
if !strings.HasPrefix(addr, "China") {
v.AddError(key, "China address only")
}
})
// ...
}
注意的是,AddCustomFunc
并不是线程安全的。在我们的设计理念中,注册这种自定义的方法,应该在系统初始化阶段完成。在该阶段,应当不存在竞争问题。
定时任务
-
初始化一个任务
tk1 := task.NewTask("tk1", "0 12 * * * *", func(ctx context.Context) error { fmt.Println("tk1"); return nil })
函数原型:
NewTask(tname string, spec string, f TaskFunc) *Task
tname
: 任务名称spec
: 定时任务格式,请参考下面的详细介绍f
: 执行的函数
-
可以测试开启运行。直接运行创建的
tk
err := tk1.Run() if err != nil { t.Fatal(err) }
-
加入全局的计划任务列表
task.AddTask("tk1", tk1)
-
开始执行全局的任务
task.StartTask() defer task.StopTask()
查看注册的任务,或者手动调动执行,可以参考Admin 后台
spec 详解
spec
格式是参照 crontab
做的,详细的解释如下所示:
// 前6个字段分别表示:
// 秒钟:0-59
// 分钟:0-59
// 小时:1-23
// 日期:1-31
// 月份:1-12
// 星期:0-6(0 表示周日)
//还可以用一些特殊符号:
// *: 表示任何时刻
// ,: 表示分割,如第三段里:2,4,表示 2 点和 4 点执行
// -:表示一个段,如第三端里: 1-5,就表示 1 到 5 点
// /n : 表示每个n的单位执行一次,如第三段里,*/1, 就表示每隔 1 个小时执行一次命令。也可以写成1-23/1.
/////////////////////////////////////////////////////////
// 0/30 * * * * * 每 30 秒 执行
// 0 43 21 * * * 21:43 执行
// 0 15 05 * * * 05:15 执行
// 0 0 17 * * * 17:00 执行
// 0 0 17 * * 1 每周一的 17:00 执行
// 0 0,10 17 * * 0,2,3 每周日,周二,周三的 17:00和 17:10 执行
// 0 0-10 17 1 * * 毎月1日从 17:00 到 7:10 毎隔 1 分钟 执行
// 0 0 0 1,15 * 1 毎月1日和 15 日和 一日的 0:00 执行
// 0 42 4 1 * * 毎月1日的 4:42 分 执行
// 0 0 21 * * 1-6 周一到周六 21:00 执行
// 0 0,10,20,30,40,50 * * * * 每隔 10 分 执行
// 0 */10 * * * * 每隔 10 分 执行
// 0 * 1 * * * 从 1:0 到 1:59 每隔 1 分钟 执行
// 0 0 1 * * * 1:00 执行
// 0 0 */1 * * * 毎时 0 分 每隔 1 小时 执行
// 0 0 * * * * 毎时 0 分 每隔 1 小时 执行
// 0 2 8-20/3 * * * 8:02,11:02,14:02,17:02,20:02 执行
// 0 30 5 1,15 * * 1 日 和 15 日的 5:30 执行
一般网络上有自动的 crontab
表达式生成工具,可以直接使用。