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/:

img

如果我们修改了 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
}

这里有一些使用的注意事项:

  1. 所有的Default*方法,在key不存在,或者查找的过程中,出现error,都会返回默认值;
  2. DIY直接返回对应的值,而没有做任何类型的转换。当你使用这个方法的时候,你应该自己确认值的类型。只有在极少数的情况下你才应该考虑使用这个方法;
  3. GetSection会返回section所对应的部分配置。section如何被解释,取决于具体的实现;
  4. Unmarshaler会尝试用当且配置的值来初始化obj。需要注意的是,prefix的概念类似于section
  5. Sub类似与GetSection,都是尝试返回配置的一部分。所不同的是,GetSection将结果组织成map,而Sub将结果组织成Config实例;
  6. OnChange主要用于监听配置的变化。对于大部分依赖于文件系统的实现来说,都不支持。具体而言,我们设计这个主要是为了考虑支持远程配置;
  7. SaveConfigFile尝试将配置导出成为一个文件;
  8. 某些实现支持分段式的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。那么:

  • 如果RouterCaseSensitivetrue,那么AutoRouter会注册两个路由,/user/helloworld/*/User/HelloWorld/*
  • 如果RouterCaseSensitivefalse,那么会注册一个路由,/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,这里注册的路由包含:

  • 如果RouterCaseSensitivetrue,那么AutoPrefix会注册两个路由,api/user/helloworld/*api/User/HelloWorld/*
  • 如果RouterCaseSensitivefalse,那么会注册一个路由,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()
}

目前来说,namespacefilter的支持是有限的,只能支持beforeafter两种。

因此要支持复杂的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

这意味着只要apiname之间至少存在一段非空路径,就会命中。

特别地,以下这种路径,将不会命中:

  • /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,那么我们最终得到的usernameoid123

总结一下:如果要使用*匹配,我们建议在整个路由里面应该只有一个*,也尽量避免包含参数路由或者正则路由。并且*命中的内容,可以通过:splat来获得。

参数路由

Beego 支持参数路由,或者说 Ant 风格的路由。它通常见于 RESTFul 风格的 API 中。其语法是在路径之中以:后面跟着参数的名字。

比如典型的例子:/api/:username/profile。该路由:username是指,位于apiprofile之间的数据,是用户名。/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/123id的值是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)

这里要注意到,GetStringGetStrings 本身在设计的时候并没有设计返回 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小,这种情况下两个参数合并在一起的效果则是:

  1. 如果文件大小小于MaxMemory,则直接在内存中处理;
  2. 如果文件大小介于MaxMemoryMaxUploadSize之间,那么比MaxMemory大的部分将会放在临时目录;
  3. 文件大小超出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 模块支持的后端引擎包括 memorycookiefilemysqlrediscouchbasememcachepostgres,用户也可以根据相应的接口实现自己的引擎。

在 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,目前支持还有 filemysqlredis 等,配置文件对应的参数名:sessionprovider
  • web.BConfig.WebConfig.Session.SessionName: 设置 cookies 的名字,Session 默认是保存在用户的浏览器 cookies 里面的,默认名是 beegosessionID,配置文件对应的参数名是:sessionname
  • web.BConfig.WebConfig.Session.SessionGCMaxLifetime: 设置 Session 过期的时间,默认值是 3600 秒,配置文件对应的参数:sessiongcmaxlifetime
  • web.BConfig.WebConfig.Session.SessionProviderConfig: 设置对应 filemysqlredis 引擎的保存路径或者链接地址,默认值是空,配置文件对应的参数: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

SessionProviderfile SessionProviderConfig 是指保存文件的目录,如下所示:

web.BConfig.WebConfig.Session.SessionProvider="file"
web.BConfig.WebConfig.Session.SessionProviderConfig = "./tmp"

MySQL

SessionProvidermysql 时,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

SessionProviderredis 时,SessionProviderConfigredis 的链接地址,采用了 redigo (opens new window),如下所示:

web.BConfig.WebConfig.Session.SessionProvider = "redis"
web.BConfig.WebConfig.Session.SessionProviderConfig = "127.0.0.1:6379"

memcache

SessionProvidermemcache 时,SessionProviderConfigmemcache 的链接地址,采用了 memcache (opens new window),如下所示:

web.BConfig.WebConfig.Session.SessionProvider = "memcache"
web.BConfig.WebConfig.Session.SessionProviderConfig = "127.0.0.1:7080"

Postgress

SessionProviderpostgres 时,SessionProviderConfigpostgres 的链接地址,采用了 postgres (opens new window),如下所示:

web.BConfig.WebConfig.Session.SessionProvider = "postgresql"
web.BConfig.WebConfig.Session.SessionProviderConfig = "postgres://pqgotest:password@localhost/pqgotest?sslmode=verify-full"

Couchbase

SessionProvidercouchbase 时,SessionProviderConfigcouchbase 的链接地址,采用了 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.goinit 里面注册需要保存的这些结构体,不然会引起应用重启之后出现无法解析的错误

单独使用 Session 模块

如果不想使用 beegoweb 模块,但是想使用 beegosession模块,也是可以的

首先你必须导入包:

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函数的参数的函数如下所示

  1. 引擎名字,可以是memoryfileMySQLRedis

  2. 一个

    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 设置是否开启 cookieSecure 设置

返回的 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: 配置文件如下所示,表示需要保存的目录,默认是两级目录新建文件,例如 sessionIDxsnkjklkjjkh27hjh78908,那么目录文件应该是 ./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 example(opens new window)

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 使用这个值计算ExpiresMax-Age两个值
  • 第二个代表Path,字符串类型,默认值是/
  • 第三个代表Domain,字符串类型
  • 第四个代表Secure,布尔类型
  • 第五个代表HttpOnly,布尔类型
  • 第六个代表SameSite,字符串类型

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") 之后的代码不会再执行,而且会默认显示给用户如下页面:

img

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,展现如下所示:

img

性能调试

你可以查看程序性能相关的信息, 进行性能调优.

健康检查

需要手工注册相应的健康检查逻辑,才能通过 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 使用了 SecureHTTP-ONLY 两个选项来保存 Cookie。因此在大部分情况下,这意味这你需要使用 HTTPS 协议,并且将无法在 JS 里面访问到 Cookie 的值。

在早期缺乏这两个选项的时候,攻击者可以轻易拿到我们设置的 Cookie 值,因此造成了安全问题。但是即便加上这两个选项,也不意味着万无一失。比如说,攻击者可以尝试用 HTTP 协议覆盖掉原有的 HTTP 协议设置的 Cookie。具体细节可以参考前面 secure 选项中的说明。

因为 Beego 需要拿到 Token 和 Cookie 里面的值进行比较,所以 Beego 要求用户必须在自己的请求里面带上 XSRF Token,你有两种方式:

  • 在表单里面携带一个叫做 _xsrf 的字段,里面是 XSRF 的 Token;
  • 在提交的请求的 HTTP HEADER 里面设置 X-XsrftokenX-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 默认情况下支持 tplhtml 后缀名的模板文件,如果你的后缀名不是这两种,请进行如下设置:

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 或 0
  • nil 的指针或 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 会逐一判断每个参数,将返回第一个非空的参数,否则就返回最后一个参数

print

对应 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}}
    

    可选的 configTypeString, 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"}}
    

模板分页处理

这里所说的分页,指的是大量数据显示时,每页显示固定的数量的数据,同时显示多个分页链接,用户点击翻页链接或页码时进入到对应的网页。 分页算法中需要处理的问题:

  1. 当前数据一共有多少条。
  2. 每页多少条,算出总页数。
  3. 根据总页数情况,处理翻页链接。
  4. 对页面上传入的 Get 或 Post 数据,需要从翻页链接中继续向后传。
  5. 在页面显示时,根据每页数量和当前传入的页码,设置查询的 Limit 和 Skip,选择需要的数据。
  6. 其他的操作,就是在 View 中显示翻页链接和数据列表的问题了。

模板处理过程中经常需要分页,那么如何进行有效的开发和操作呢? 我们开发组针对这个需求开发了如下的例子,希望对大家有用

静态文件

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.PostTagRelPostTagRel 表需要有到 PostTag 的关系

当设置 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 = true
  • set_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 -> UserManyToOne 的关系,也就是外键。

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

这两个方法类似于ReadReadWithCtx,所不同的是,这两个方法在查询的时候加上 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里面衍生出来的QuerySeterQueryM2Mer,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, MySQLTiDB的支持。

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 的条件查询。在没有指定操作符的情况下,会使用=作为操作符。

当前支持的操作符号:

后面以 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会查询得到两列,并且只有一行。在这种情况下,两列的值分别被赋值给idname

使用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也是按照列来返回,因此可以注意到在例子里面我们声明了两个切片,分别对应于idname两个列。

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 有:

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 每个文件保存的最大行数,默认值 1000000
    • maxsize 每个文件保存的最大尺寸,默认值是 1 << 28, 256 MB
    • daily 是否按照每天 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 每个文件保存的最大行数,默认值 1000000
    • maxsize 每个文件保存的最大尺寸,默认值是 1 << 28, //256 MB
    • daily 是否按照每天 logrotate,默认是 true
    • maxdays 文件最多保存多少天,默认保存 7 天
    • rotate 是否开启 logrotate,默认是 true
    • level 日志保存的时候的级别,默认是 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并不是线程安全的。在我们的设计理念中,注册这种自定义的方法,应该在系统初始化阶段完成。在该阶段,应当不存在竞争问题。

定时任务

  1. 初始化一个任务

    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: 执行的函数
  2. 可以测试开启运行。直接运行创建的 tk

    err := tk1.Run()
    	if err != nil {
    		t.Fatal(err)
    	}
    
  3. 加入全局的计划任务列表

    task.AddTask("tk1", tk1)
    
  4. 开始执行全局的任务

    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 表达式生成工具,可以直接使用。

posted @ 2022-02-10 03:30  DarkH  阅读(631)  评论(0编辑  收藏  举报