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:@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" ?>

    beego

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

保存的代码例子如下:

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 的值。

  • secure(opens new window)
  • http-only(opens new window)

在早期缺乏这两个选项的时候,攻击者可以轻易拿到我们设置的 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的核心代码是:

{{ .xsrfdata }}

.xsrfdata就是mc.Data["xsrfdata"],详情可以参考模板引擎

页面设置 meta

比较简单的是通过扩展 Ajax 给每个请求加入 XSRF 的 HEADER

需要你在 HTML 里保存一个 _xsrf

func (this *HomeController) Get(){
    this.Data["xsrf_token"] = this.XSRFToken()
}

放在你的页面 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:



  
    Lin Li
    
    
    
    
    {{.HtmlHead}}
  
  
    
{{.LayoutContent}}
{{.SideBar}}
{{.Scripts}}

html_head.tpl:


scripts.tpl


逻辑处理如下所示:

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 | renderform}}

上面的代码生成的表单为:

	Name: 
年龄:
Sex:
Intro:

模板语法

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"}}
  • {{.Name}}
  • {{end}}

    使用 template 调用模板

      {{range .Items}} {{template "loop" .}} {{end}}

    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 文件生成一个