beego 是一个快速开发 Go 应用的 HTTP 框架,他可以用来快速开发 API、Web 及后端服务等各种应用,是一个 RESTful 的框架,主要设计灵感来源于 tornado、sinatra 和 flask 这三个框架,但是结合了 Go 本身的一些特性(interface、struct 嵌入等)而设计的一个框架。
beego 的整体设计架构如下所示:
beego 是基于八大独立的模块构建的,是一个高度解耦的框架。当初设计 beego 的时候就是考虑功能模块化,用户即使不使用 beego 的 HTTP 逻辑,也依旧可以使用这些独立模块,例如:你可以使用 cache 模块来做你的缓存逻辑;使用日志模块来记录你的操作信息;使用 config 模块来解析你各种格式的文件。所以 beego 不仅可以用于 HTTP 类的应用开发,在你的 socket 游戏开发中也是很有用的模块,这也是 beego 如此受欢迎的一个原因。大家如果玩过乐高的话,应该知道很多高级的东西都是一块一块的积木搭建出来的,而设计 beego 的时候,这些模块就是积木,高级机器人就是 beego。
既然 beego 是基于这些模块构建的,那么它的执行逻辑是怎么样的呢?beego 是一个典型的 MVC 架构,它的执行逻辑如下图所示:
一般的 beego 项目的目录如下所示:
├── conf
│ └── app.conf
├── controllers
│ ├── admin
│ └── default.go
├── main.go
├── models
│ └── models.go
├── static
│ ├── css
│ ├── ico
│ ├── img
│ └── js
└── views
├── admin
└── index.tpl
从上面的目录结构我们可以看出来 M(models 目录)、V(views 目录)和 C(controllers 目录)的结构, main.go
是入口文件。
beego 的安装是典型的 Go 安装包的形式:
go get github.com/beego/beego/v2@latest
beego 升级分为 go 方式升级和源码下载升级:
Go 升级,通过该方式用户可以升级 beego 框架,强烈推荐该方式:
go get -u github.com/beego/beego/v2
源码下载升级,用户访问 https://github.com/beego/beego
,下载源码,然后覆盖到 $GOPATH/src/github.com/beego/beego
目录,然后通过本地执行安装就可以升级了:
go install github.com/beego/beego/v2
bee 工具是一个为了协助快速开发 beego 项目而创建的项目,通过 bee 您可以很容易的进行 beego 项目的创建、热编译、开发、测试、和部署。
您可以通过如下的方式安装 bee 工具:
# go 1.16 以前的版本
go get -u github.com/beego/bee/v2
# go 1.16及以后的版本
go install github.com/beego/bee/v2@latest
安装完之后,bee
可执行文件默认存放在 $GOPATH/bin
里面,所以您需要把 $GOPATH/bin
添加到您的环境变量中,才可以进行下一步。
我们在命令行输入 bee
,可以看到如下的信息:
version Prints the current Bee version
migrate Runs database migrations
api Creates a Beego API application
bale Transforms non-Go files to Go source files
fix Fixes your application by making it compatible with newer versions of Beego
pro Source code generator
dev Commands which used to help to develop beego and bee
dlv Start a debugging session using Delve
dockerize Generates a Dockerfile and docker-compose.yaml for your Beego application
generate Source code generator
hprose Creates an RPC application based on Hprose and Beego frameworks
new Creates a Beego application
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
update Update Bee
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
以上是官方文档的描述,但是我亲测可以在任意路径使用
bee new <项目名>
命令,不一定要在$GOPATH/src
。
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:
如果 conn 参数为空则创建一个示例项目,否则将基于链接信息链接数据库创建项目。
同样笔者亲测该命令也不一定要在
$GOPATH/src
运行
run
命令我们在开发 Go 项目的时候最大的问题是经常需要自己手动去编译再运行,bee run
命令是监控 beego 的项目,通过 fsnotify监控文件系统。但是注意该命令必须在 $GOPATH/src/appname
下执行。
这样我们在开发过程中就可以实时的看到项目修改之后的效果:
bee run
13-11-25 09:53:04 [INFO] Uses 'myproject' as 'appname'
13-11-25 09:53:04 [INFO] Initializing watcher...
13-11-25 09:53:04 [TRAC] Directory(/gopath/src/myproject/controllers)
13-11-25 09:53:04 [TRAC] Directory(/gopath/src/myproject/models)
13-11-25 09:53:04 [TRAC] Directory(/gopath/src/myproject)
13-11-25 09:53:04 [INFO] Start building...
13-11-25 09:53:16 [SUCC] Build was successful
13-11-25 09:53:16 [INFO] Restarting myproject ...
13-11-25 09:53:16 [INFO] ./myproject is running...
我们打开浏览器就可以看到效果 http://localhost:8080/
:
如果我们修改了 Controller
下面的 default.go
文件,我们就可以看到命令行输出:
13-11-25 10:11:20 [EVEN] "/gopath/src/myproject/controllers/default.go": DELETE|MODIFY
13-11-25 10:11:20 [INFO] Start building...
13-11-25 10:11:20 [SKIP] "/gopath/src/myproject/controllers/default.go": CREATE
13-11-25 10:11:23 [SKIP] "/gopath/src/myproject/controllers/default.go": MODIFY
13-11-25 10:11:23 [SUCC] Build was successful
13-11-25 10:11:23 [INFO] Restarting myproject ...
13-11-25 10:11:23 [INFO] ./myproject is running...
刷新浏览器我们看到新的修改内容已经输出。
pack
命令pack
目录用来发布应用的时候打包,会把项目打包成 zip 包,这样我们部署的时候直接把打包之后的项目上传,解压就可以部署了:
bee pack
app path: /gopath/src/apiproject
GOOS darwin GOARCH amd64
build apiproject
build success
exclude prefix:
exclude suffix: .go:.DS_Store:.tmp
file write to `/gopath/src/apiproject/apiproject.tar.gz`
我们可以看到目录下有如下的压缩文件:
rwxr-xr-x 1 astaxie staff 8995376 11 25 22:46 apiproject
-rw-r--r-- 1 astaxie staff 2240288 11 25 22:58 apiproject.tar.gz
drwxr-xr-x 3 astaxie staff 102 11 25 22:31 conf
drwxr-xr-x 3 astaxie staff 102 11 25 22:31 controllers
-rw-r--r-- 1 astaxie staff 509 11 25 22:31 main.go
drwxr-xr-x 3 astaxie staff 102 11 25 22:31 models
drwxr-xr-x 3 astaxie staff 102 11 25 22:31 tests
version
命令$ bee version
______
| ___ \
| |_/ / ___ ___
| ___ \ / _ \ / _ \
| |_/ /| __/| __/
\____/ \___| \___| v2.0.4
├── GoVersion : go1.20.4
├── GOOS : windows
├── GOARCH : amd64
├── NumCPU : 16
├── GOPATH : C:\Users\19393\sdk\go1.20.4\go_workspace
├── GOROOT : C:/Users/19393/sdk/go1.20.4
├── Compiler : gc
└── Date : Friday, 21 Jul 2023
generate
命令这个命令是用来自动化的生成代码的,包含了从数据库一键生成 model,还包含了 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"
bee generate model [modelname] [-fields=""]
generate RESTful model based on fields
-fields: a list of table fields. Format: field:type, ...
bee generate controller [controllerfile]
generate RESTful controllers
bee generate view [viewpath]
generate CRUD view in viewpath
bee generate migration [migrationfile] [-fields=""]
generate migration file for making database schema update
-fields: a list of table fields. Format: field:type, ...
bee generate docs
generate swagger doc file
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, you'd better pass this parameter
bee generate test [routerfile]
generate testcase
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:[email protected]:5432/postgres
-level: [1 | 2 | 3], 1 = models; 2 = models,controllers; 3 = models,controllers,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
beego 是一个能够进行快速开发应用的框架,快速入门将带领你快速的建立一个 beego 的项目,然后运行起来。接着修改代码,让我们了解整个 MVC 的运行机制。那么让我们开始创建项目吧!
beego 的项目基本都是通过 bee
命令来创建的,所以在创建项目之前确保你已经安装了 bee 工具和 beego。
现在一切就绪我们就可以开始创建项目了,打开终端,进入 $GOPATH/src
所在的目录:
➜ src bee new quickstart
[INFO] Creating application...
/gopath/src/quickstart/
/gopath/src/quickstart/conf/
/gopath/src/quickstart/controllers/
/gopath/src/quickstart/models/
/gopath/src/quickstart/routers/
/gopath/src/quickstart/tests/
/gopath/src/quickstart/static/
/gopath/src/quickstart/static/js/
/gopath/src/quickstart/static/css/
/gopath/src/quickstart/static/img/
/gopath/src/quickstart/views/
/gopath/src/quickstart/conf/app.conf
/gopath/src/quickstart/controllers/default.go
/gopath/src/quickstart/views/index.tpl
/gopath/src/quickstart/routers/router.go
/gopath/src/quickstart/tests/default_test.go
/gopath/src/quickstart/main.go
2014/11/06 18:17:09 [SUCC] New application successfully created!
通过一个简单的命令就创建了一个 beego 项目。他的目录结构如下所示
quickstart
|-- conf
| `-- app.conf
|-- controllers
| `-- default.go
|-- main.go
|-- models
|-- routers
| `-- router.go
|-- static
| |-- css
| |-- img
| `-- js
|-- tests
| `-- default_test.go
`-- views
`-- index.tpl
从目录结构中我们也可以看出来这是一个典型的 MVC 架构的应用,main.go
是入口文件。
beego 项目创建之后,我们还需要初始化go.mod
文件。进入目录之后,使用go mod init
初始化模块依赖。
接着我们就开始运行项目,首先进入创建的项目,我们使用 bee run
来运行该项目,这样就可以做到热编译的效果:
➜ src cd quickstart
➜ quickstart bee run
2014/11/06 18:18:34 [INFO] Uses 'quickstart' as 'appname'
2014/11/06 18:18:34 [INFO] Initializing watcher...
2014/11/06 18:18:34 [TRAC] Directory(/gopath/src/quickstart/controllers)
2014/11/06 18:18:34 [TRAC] Directory(/gopath/src/quickstart)
2014/11/06 18:18:34 [TRAC] Directory(/gopath/src/quickstart/routers)
2014/11/06 18:18:34 [TRAC] Directory(/gopath/src/quickstart/tests)
2014/11/06 18:18:34 [INFO] Start building...
2014/11/06 18:18:35 [SUCC] Build was successful
2014/11/06 18:18:35 [INFO] Restarting quickstart ...
2014/11/06 18:18:35 [INFO] ./quickstart is running...
2014/11/06 18:18:35 [app.go:96] [I] http server Running on :8080
这样我们的应用已经在 8080
端口(beego 的默认端口)跑起来了.你是不是觉得很神奇,为什么没有 nginx 和 apache 居然可以自己干这个事情?是的,Go 其实已经做了网络层的东西,beego 只是封装了一下,所以可以做到不需要 nginx 和 apache。让我们打开浏览器看看效果吧:
前面我们已经创建了 beego 项目,而且我们也看到它已经运行起来了,那么是如何运行起来的呢?让我们从入口文件先分析起来吧:
package main
import (
_ "quickstart/routers"
"github.com/beego/beego/v2/server/web"
)
func main() {
web.Run()
}
我们看到 main 函数是入口函数,但是我们知道 Go 的执行过程是如下图所示的方式:
这里我们就看到了我们引入了一个包 _ "quickstart/routers"
,这个包只引入执行了里面的 init 函数,那么让我们看看这个里面做了什么事情:
package routers
import (
"quickstart/controllers"
"github.com/beego/beego/v2/server/web"
)
func init() {
web.Router("/", &controllers.MainController{})
}
路由包里面我们看到执行了路由注册 web.Router
, 这个函数的功能是映射 URL 到 controller,第一个参数是 URL (用户请求的地址),这里我们注册的是 /
,也就是我们访问的不带任何参数的 URL,第二个参数是对应的 Controller,也就是我们即将把请求分发到那个控制器来执行相应的逻辑,我们可以执行类似的方式注册如下路由:
web.Router("/user", &controllers.UserController{})
这样用户就可以通过访问 /user
去执行 UserController
的逻辑。这就是我们所谓的路由。
一旦 run 起来之后,我们的服务就监听在两个端口了,一个服务端口 8080 作为对外服务,另一个 8088 端口实行对内监控。
前面我们了解了如何把用户的请求分发到控制器,这小节我们就介绍大家如何来写控制器,首先我们还是从源码分析入手:
package controllers
import (
"github.com/beego/beego/v2/server/web"
)
type MainController struct {
web.Controller
}
func (this *MainController) Get() {
this.Data["Website"] = "beego.vip"
this.Data["Email"] = "[email protected]"
this.TplName = "index.tpl"
}
上面的代码显示首先我们声明了一个控制器 MainController
,这个控制器里面组合了 web.Controller
,这就是 Go 的组合方式,也就是 MainController
自动拥有了所有 web.Controller
的方法。
而 web.Controller
拥有很多方法,其中包括 Init
、Prepare
、Post
、Get
、Delete
、Head
等方法。我们可以通过重写的方式来实现这些方法,而我们上面的代码就是重写了 Get
方法。
我们先前介绍过 beego 是一个 RESTful 的框架,所以我们的请求默认是执行对应 req.Method
的方法。例如浏览器的是 GET
请求,那么默认就会执行 MainController
下的 Get
方法。这样我们上面的 Get 方法就会被执行到,这样就进入了我们的逻辑处理。(用户可以改变这个行为,通过注册自定义的函数名。)
里面的代码是需要执行的逻辑,这里只是简单的输出数据,我们可以通过各种方式获取数据,然后赋值到 this.Data
中,这是一个用来存储输出数据的 map,可以赋值任意类型的值,这里我们只是简单举例输出两个字符串。
最后一个就是需要去渲染的模板,this.TplName
就是需要渲染的模板,这里指定了 index.tpl
,如果用户不设置该参数,那么默认会去到模板目录的 Controller/<方法名>.tpl
查找,例如上面的方法会去 maincontroller/get.tpl
(文件、文件夹必须小写)。
用户设置了模板之后系统会自动的调用 Render
函数(这个函数是在 web.Controller
中实现的),所以无需用户自己来调用渲染。
当然也可以不使用模版,直接用 this.Ctx.WriteString
输出字符串,如:
func (this *MainController) Get() {
this.Ctx.WriteString("hello")
}
至此我们的控制器分析基本完成了,接下来让我们看看如何来编写 model。
我们知道 Web 应用中我们用的最多的就是数据库操作,而 model 层一般用来做这些操作,我们的 bee new
例子不存在 Model 的演示,但是 bee api
应用中存在 model 的应用。说的简单一点,如果您的应用足够简单,那么 Controller 可以处理一切的逻辑,如果您的逻辑里面存在着可以复用的东西,那么就抽取出来变成一个模块。因此 Model 就是逐步抽象的过程,一般我们会在 Model 里面处理一些数据读取,如下是一个日志分析应用中的代码片段:
package models
import (
"path/filepath"
"strings"
)
var (
NotPV []string = []string{"css", "js", "class", "gif", "jpg", "jpeg", "png", "bmp", "ico", "rss", "xml", "swf"}
)
const big = 0xFFFFFF
func LogPV(urls string) bool {
ext := filepath.Ext(urls)
if ext == "" {
return true
}
for _, v := range NotPV {
if v == strings.ToLower(ext) {
return false
}
}
return true
}
所以如果您的应用足够简单,那么就不需要 Model 了;如果你的模块开始多了,需要复用,需要逻辑分离了,那么 Model 是必不可少的。接下来我们将分析如何编写 View 层的东西。
在前面编写 Controller 的时候,我们在 Get 里面写过这样的语句 this.TplName = "index.tpl"
,设置显示的模板文件,默认支持 tpl
和 html
的后缀名,如果想设置其他后缀你可以调用 beego.AddTemplateExt
接口设置,那么模板如何来显示相应的数据呢?beego 采用了 Go 语言默认的模板引擎,所以和 Go 的模板语法一样,Go 模板的详细使用方法在我这篇文章有讲解。
我们看看快速入门里面的代码(去掉了 css 样式):
<!DOCTYPE html>
<html>
<head>
<title>Beego</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
</head>
<body>
<header class="hero-unit" style="background-color:#A9F16C">
<div class="container">
<div class="row">
<div class="hero-text">
<h1>Welcome to Beego!</h1>
<p class="description">
Beego is a simple & powerful Go web framework which is inspired by tornado and sinatra.
<br />
Official website: <a href="http://{{.Website}}">{{.Website}}</a>
<br />
Contact me: {{.Email}}
</p>
</div>
</div>
</div>
</header>
</body>
</html>
我们在 Controller 里面把数据赋值给了 data(map 类型),然后我们在模板中就直接通过 key 访问 .Website
和 .Email
。这样就做到了数据的输出。接下来我们讲解如何让静态文件输出。
前面我们介绍了如何输出静态页面,但是我们的网页往往包含了很多的静态文件,包括图片、JS、CSS 等,刚才创建的应用里面就创建了如下目录:
├── static
│ ├── css
│ ├── img
│ └── js
beego 默认注册了 static 目录为静态处理的目录,注册样式:URL 前缀和映射的目录(在/main.go
文件中web.Run()
之前加入):
StaticDir["/static"] = "static"
用户可以设置多个静态文件处理目录,例如你有多个文件下载目录 download1、download2,你可以这样映射(在 /main.go
文件中 web.Run()
之前加入):
web.SetStaticPath("/down1", "download1")
web.SetStaticPath("/down2", "download2")
这样用户访问 URL http://localhost:8080/down1/123.txt
则会请求 download1 目录下的 123.txt 文件。
beego 是一个典型的 MVC 框架,它的整个执行逻辑如下图所示:
通过文字来描述如下:
http
包中默认的 ServeFile 来处理静态文件。/hello/world
,那么固定规则中 /hello
是不会匹配的,只有完全匹配才算匹配。如果匹配的话就进入逻辑执行,如果不匹配进入下一环节的正则匹配。controller/method
这样的方式去查找对应的 Controller 和他内置的方法,如果找到就开始执行逻辑,如果找不到就跳转到监控判断。beego 目前支持 INI、XML、JSON、YAML 格式的配置文件解析,但是默认采用了 INI 格式解析,用户可以通过简单的配置就可以获得很大的灵活性。
beego 默认会解析当前应用下的 conf/app.conf
文件。
通过这个文件你可以初始化很多 beego 的默认参数:
appname = beepkg
httpaddr = "127.0.0.1"
httpport = 9090
runmode ="dev"
autorender = false
recoverpanic = false
viewspath = "myview"
它们都维护在结构体 beego/server/web#Config
。
BConfig 就是 beego 里面的默认的配置,也是结构体 beego/server/web#Config
的实例。
你也可以直接通过web.BConfig.AppName="beepkg"
这样来修改,和上面的配置效果一样,只是一个在代码里面写死了,而配置文件就会显得更加灵活。
你也可以在配置文件中配置应用需要用的一些配置信息,例如下面所示的数据库信息:
mysqluser = "root"
mysqlpass = "rootpass"
mysqlurls = "127.0.0.1"
mysqldb = "beego"
那么你就可以通过如下的方式获取设置的配置信息:
web.AppConfig.String("mysqluser")
web.AppConfig.String("mysqlpass")
web.AppConfig.String("mysqlurls")
web.AppConfig.String("mysqldb")
在配置文件里面支持 section,可以有不同的 Runmode 的配置,默认优先读取 runmode 下的配置信息,例如下面的配置文件:
appname = beepkg
httpaddr = "127.0.0.1"
httpport = 9090
runmode ="dev"
autorender = false
recoverpanic = false
viewspath = "myview"
[dev]
httpport = 8080
[prod]
httpport = 8088
[test]
httpport = 8888
上面的配置文件就是在不同的 runmode 下解析不同的配置,例如在 dev 模式下,httpport 是 8080,在 prod 模式下是 8088,在 test 模式下是 8888。其他配置文件同理。解析的时候优先解析 runmode 下的配置,然后解析默认的配置。
INI 格式配置支持 include
方式,引用多个配置文件,例如下面的两个配置文件效果同上:
app.conf
appname = beepkg
httpaddr = "127.0.0.1"
httpport = 9090
include "app2.conf"
app2.conf
runmode ="dev"
autorender = false
recoverpanic = false
viewspath = "myview"
[dev]
httpport = 8080
[prod]
httpport = 8088
[test]
httpport = 8888
关于系统的默认参数,参考官方文档:https://git-books.github.io/books/beego/?p=mvc/controller/config.md
从 beego 1.2 版本开始支持了基本的 RESTful 函数式路由,应用中的大多数路由都会定义在 routers/router.go
文件中。最简单的 beego 路由由 URI 和闭包函数组成。
web.Get("/",func(ctx *context.Context){
ctx.Output.Body([]byte("hello world"))
})
web.Post("/alice",func(ctx *context.Context){
ctx.Output.Body([]byte("bob"))
})
web.Any("/foo",func(ctx *context.Context){
ctx.Output.Body([]byte("bar"))
})
固定路由也就是全匹配的路由,如下所示:
web.Router("/", &controllers.MainController{})
web.Router("/admin", &admin.UserController{})
web.Router("/admin/index", &admin.ArticleController{})
web.Router("/admin/addpkg", &admin.AddController{})
如上所示的路由就是我们最常用的路由方式,一个固定的路由,一个控制器,然后根据用户请求方法不同请求控制器中对应的方法,典型的 RESTful 方式。
为了用户更加方便的路由设置,beego 参考了 sinatra 的路由实现,支持多种方式的路由:
web.Router(“/api/?:id”, &controllers.RController{})
默认匹配 //例如对于URL"/api/123"可以匹配成功,此时变量":id"值为"123",URL"/api/"可正常匹配
web.Router(“/api/:id”, &controllers.RController{})
默认匹配 //例如对于URL"/api/123"可以匹配成功,此时变量":id"值为"123",但URL"/api/"匹配失败
web.Router(“/api/:id([0-9]+)”, &controllers.RController{})
自定义正则匹配 //例如对于URL"/api/123"可以匹配成功,此时变量":id"值为"123"
web.Router(“/user/:username([\w]+)”, &controllers.RController{})
正则字符串匹配 //例如对于URL"/user/astaxie"可以匹配成功,此时变量":username"值为"astaxie"
web.Router(“/download/.”, &controllers.RController{})
*匹配方式 //例如对于URL"/download/file/api.xml"可以匹配成功,此时变量":path"值为"file/api", “:ext"值为"xml”
web.Router(“/download/ceshi/*”, &controllers.RController{})
*全匹配方式 //例如对于URL"/download/ceshi/file/api.json"可以匹配成功,此时变量":splat"值为"file/api.json"
web.Router(“/int”, &controllers.RController{})
int 类型设置方式,匹配 :id为int 类型,框架帮你实现了正则 ([0-9]+)
web.Router(“/:hi:string”, &controllers.RController{})
string 类型设置方式,匹配 :hi 为 string 类型。框架帮你实现了正则 ([\w]+)
web.Router(“/cms_:id([0-9]+).html”, &controllers.CmsController{})
带有前缀的自定义正则 //匹配 :id 为正则类型。匹配 cms_123.html 这样的 url :id = 123
可以在 Controller 中通过如下方式获取上面的变量:
this.Ctx.Input.Param(":id")
this.Ctx.Input.Param(":username")
this.Ctx.Input.Param(":splat")
this.Ctx.Input.Param(":path")
this.Ctx.Input.Param(":ext")
上面列举的是默认的请求方法名(请求的 method 和函数名一致,例如 GET
请求执行 Get
函数,POST
请求执行 Post
函数),如果用户期望自定义函数名,那么可以使用如下方式:
web.Router("/",&IndexController{},"*:Index")
使用第三个参数,第三个参数就是用来设置对应 method 到函数名,定义如下
*
表示任意的 method 都执行该函数;
分割,
来分割以下是一个 RESTful 的设计示例:
web.Router("/api/food",&RestController{},"get:ListFood")
web.Router("/api/food",&RestController{},"post:CreateFood")
web.Router("/api/food",&RestController{},"put:UpdateFood")
web.Router("/api/food",&RestController{},"delete:DeleteFood")
以下是多个 HTTP Method 指向同一个函数的示例:
web.Router("/api",&RestController{},"get,post:ApiFunc")
以下是不同的 method 对应不同的函数,通过 ; 进行分割的示例:
web.Router("/api/food",&RestController{},"get:ListFood;post:CreateFood;put:UpdateFood;delete:DeleteFood")
可用的 HTTP Method:
如果同时存在 * 和对应的 HTTP Method,那么优先执行 HTTP Method 的方法,例如同时注册了如下所示的路由:
web.Router("/simple",&SimpleController{},"*:AllFunc;post:PostFunc")
那么执行 POST
请求的时候,执行 PostFunc
而不执行 AllFunc
。
自定义函数的路由默认不支持 RESTful 的方法,也就是如果你设置了
web.Router("/api",&RestController{},"post:ApiFunc")
这样的路由,如果请求的方法是POST
,那么不会默认去执行Post
函数。
用户首先需要把需要路由的控制器注册到自动路由中:
web.AutoRouter(&controllers.ObjectController{})
那么 beego 就会通过反射获取该结构体中所有的实现方法,你就可以通过如下的方式访问到对应的方法中:
/object/login 调用 ObjectController 中的 Login 方法
/object/logout 调用 ObjectController 中的 Logout 方法
除了前缀两个 /:controller/:method
的匹配之外,剩下的 url beego 会帮你自动化解析为参数,保存在 this.Ctx.Input.Params
当中:
/object/blog/2013/09/12 调用 ObjectController 中的 Blog 方法,参数如下:map[0:2013 1:09 2:12]
方法名在内部是保存了用户设置的,例如 Login,url 匹配的时候都会转化为小写,所以,/object/LOGIN
这样的 url
也一样可以路由到用户定义的 Login
方法中。
现在已经可以通过自动识别出来下面类似的所有 url,都会把请求分发到 controller
的 simple
方法:
/controller/simple
/controller/simple.html
/controller/simple.json
/controller/simple.xml
可以通过 this.Ctx.Input.Param(":ext")
获取后缀名。
从2.0开始,我们使用配置CommentRouterPath
来配置注解路由的扫描路径。在dev
环境下,我们将自动扫描该配置指向的目录及其子目录,生成路由文件。
生成之后,用户需要显示 Include 相应的 controller。注意, controller 的 method 方法上面须有 router 注释(// @router),详细的使用请看下面的例子:
// CMS API
type CMSController struct {
web.Controller
}
func (c *CMSController) URLMapping() {
c.Mapping("StaticBlock", c.StaticBlock)
c.Mapping("AllBlock", c.AllBlock)
}
// @router /staticblock/:key [get]
func (this *CMSController) StaticBlock() {
}
// @router /all/:key [get]
func (this *CMSController) AllBlock() {
}
可以在 router.go
中通过如下方式注册路由:
web.Include(&CMSController{})
web 自动会进行源码分析,注意只会在 dev 模式下进行生成,生成的路由放在 “/routers/commentsRouter.go” 文件中。
这样上面的路由就支持了如下的路由:
其实效果和自己通过 Router 函数注册是一样的:
web.Router("/staticblock/:key", &CMSController{}, "get:StaticBlock")
web.Router("/all/:key", &CMSController{}, "get:AllBlock")
同时大家注意到新版本里面增加了 URLMapping 这个函数,这是新增加的函数,用户如果没有进行注册,那么就会通过反射来执行对应的函数,如果注册了就会通过 interface 来进行执行函数,性能上面会提升很多。
方法表达式路由与上面的RESTful基本相似,区别是无需在传入http method和controller方法(如:"get:StaticBlock"
)。
只需要通过golang的method expression进行传入方法表达式。如果方法是receiver是非指针,则直接使用 包名.Controller.Method
方法 传入,
如果receiver是指针,则使用 (*包名.Controller).Method
进行传参。假如在同包下,包名可进行省略。
type BaseController struct {
web.Controller
}
func (b BaseController) Ping() {
b.Data["json"] = "pong"
b.ServeJSON()
}
func (b *BaseController) PingPointer() {
b.Data["json"] = "pong_pointer"
b.ServeJSON()
}
func main() {
web.CtrlGet("/ping", BaseController.Ping)
web.CtrlGet("/ping_pointer", (*BaseController).PingPointer)
web.Run()
}
共有以下几种函数:
基于 beego 的 Controller 设计,只需要匿名组合 beego.Controller
就可以了,如下所示:
package your_package
import (
"github.com/beego/beego/v2/server/web"
)
type xxxController struct {
web.Controller
}
web.Controller
实现了接口 web.ControllerInterface
,web.ControllerInterface
定义了如下函数:
Init(ctx *context.Context, controllerName, actionName string, app interface{})
这个函数主要初始化了 Context、相应的 Controller 名称,模板名,初始化模板参数的容器 Data,app 即为当前执行的 Controller 的 reflecttype,这个 app 可以用来执行子类的方法。
Prepare()
这个函数主要是为了用户扩展用的,这个函数会在下面定义的这些 Method 方法之前执行,用户可以重写这个函数实现类似用户验证之类。
Get()
如果用户请求的 HTTP Method 是 GET,那么就执行该函数,默认是 405,用户继承的子 struct 中可以实现了该方法以处理 Get 请求。
Post()
如果用户请求的 HTTP Method 是 POST,那么就执行该函数,默认是 405,用户继承的子 struct 中可以实现了该方法以处理 Post 请求。
Delete()
如果用户请求的 HTTP Method 是 DELETE,那么就执行该函数,默认是 405,用户继承的子 struct 中可以实现了该方法以处理 Delete 请求。
Put()
如果用户请求的 HTTP Method 是 PUT,那么就执行该函数,默认是 405,用户继承的子 struct 中可以实现了该方法以处理 Put 请求.
Head()
如果用户请求的 HTTP Method 是 HEAD,那么就执行该函数,默认是 405,用户继承的子 struct 中可以实现了该方法以处理 Head 请求。
Patch()
如果用户请求的 HTTP Method 是 PATCH,那么就执行该函数,默认是 405,用户继承的子 struct 中可以实现了该方法以处理 Patch 请求.
Options()
如果用户请求的HTTP Method是OPTIONS,那么就执行该函数,默认是 405,用户继承的子 struct 中可以实现了该方法以处理 Options 请求。
Finish()
这个函数是在执行完相应的 HTTP Method 方法之后执行的,默认是空,用户可以在子 struct 中重写这个函数,执行例如数据库关闭,清理数据之类的工作。
Render() error
这个函数主要用来实现渲染模板,如果 beego.AutoRender 为 true 的情况下才会执行。
Redirect(url string, code int)
重定向。url
是目的地址。
通过子 struct 的方法重写,用户就可以实现自己的逻辑,接下来我们看一个实际的例子:
type AddController struct {
web.Controller
}
func (this *AddController) Prepare() {
}
func (this *AddController) Get() {
this.Data["content"] = "value"
this.Layout = "admin/layout.html"
this.TplName = "admin/add.tpl"
}
func (this *AddController) Post() {
pkgname := this.GetString("pkgname")
content := this.GetString("content")
pk := models.GetCruPkg(pkgname)
if pk.Id == 0 {
var pp models.PkgEntity
pp.Pid = 0
pp.Pathname = pkgname
pp.Intro = pkgname
models.InsertPkg(pp)
pk = models.GetCruPkg(pkgname)
}
var at models.Article
at.Pkgid = pk.Id
at.Content = content
models.InsertArticle(at)
this.Ctx.Redirect(302, "/admin/index")
}
从上面的例子可以看出来,通过重写方法可以实现对应 method 的逻辑,实现 RESTFul 结构的逻辑处理。
我们应用中经常会遇到这样的情况,在 Prepare 阶段进行判断,如果用户认证不通过,就输出一段信息,然后直接中止进程,之后的 Post、Get 之类的不再执行,那么如何终止呢?可以使用 StopRun
来终止执行逻辑,可以在任意的地方执行。
type RController struct {
beego.Controller
}
func (this *RController) Prepare() {
this.Data["json"] = map[string]interface{}{"name": "astaxie"}
this.ServeJSON()
this.StopRun()
}
调用 StopRun 之后,如果你还定义了 Finish 函数就不会再执行,如果需要释放资源,那么请自己在调用 StopRun 之前手工调用 Finish 函数。
跨站请求伪造(Cross-site request forgery), 简称为 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 值,那么这个请求会被直接拒绝。如果你开启了这个机制,那么在所有被提交的表单中,你都需要加上一个域来提供这个值。你可以通过在模板中使用 专门的函数 XSRFFormHTML()
来做到这一点:
过期时间上面我们设置了全局的过期时间 web.XSRFExpire
,但是有些时候我们也可以在控制器中修改这个过期时间,专门针对某一类处理逻辑:
func (this *HomeController) Get(){
this.XSRFExpire = 7200
this.Data["xsrfdata"]=template.HTML(this.XSRFFormHTML())
}
XSRF 之前是全局设置的一个参数,如果设置了那么所有的 API 请求都会进行验证,但是有些时候API 逻辑是不需要进行验证的,因此现在支持在controller 级别设置屏蔽:
type AdminController struct{
web.Controller
}
func (a *AdminController) Prepare() {
a.EnableXSRF = false
}
我们经常需要获取用户传递的数据,包括 Get、POST 等方式的请求,beego 里面会自动解析这些数据,你可以通过如下方式获取数据:
使用例子如下:
func (this *MainController) Post() {
jsoninfo := this.GetString("jsoninfo")
if jsoninfo == "" {
this.Ctx.WriteString("jsoninfo is empty")
return
}
}
如果你需要的数据可能是其他类型的,例如是 int 类型而不是 int64,那么你需要这样处理:
func (this *MainController) Post() {
id := this.Input().Get("id")
intid, err := strconv.Atoi(id)
}
更多其他的 request 的信息,用户可以通过 this.Ctx.Request
获取信息。
如果要把表单里的内容赋值到一个 struct 里,除了用上面的方法一个一个获取再赋值外,beego 提供了通过另外一个更便捷的方式,就是通过 struct 的字段名或 tag 与表单字段对应直接解析到 struct。
定义 struct:
type user struct {
Id int `form:"-"`
Name interface{} `form:"username"`
Age int `form:"age"`
Email string
}
表单:
<form id="user">
名字:<input name="username" type="text" />
年龄:<input name="age" type="text" />
邮箱:<input name="Email" type="text" />
<input type="submit" value="提交" />
form>
Controller 里解析:
func (this *MainController) Post() {
u := user{}
if err := this.ParseForm(&u); err != nil {
//handle error
}
}
注意:
xx must be a struct pointer
的错误。form
标签的值设置为 -
在 API 的开发中,我们经常会用到 JSON
或 XML
来作为数据交互的格式,如何在 beego 中获取 Request Body 里的 JSON 或 XML 的数据呢?
copyrequestbody = true
func (this *ObjectController) Post() {
var ob models.Object
var err error
if err = json.Unmarshal(this.Ctx.Input.RequestBody, &ob); err == nil {
objectid := models.AddOne(ob)
this.Data["json"] = "{\"ObjectId\":\"" + objectid + "\"}"
} else {
this.Data["json"] = err.Error()
}
this.ServeJSON()
}
在 beego 中你可以很容易的处理文件上传,就是别忘记在你的 form 表单中增加这个属性 enctype="multipart/form-data"
,否则你的浏览器不会传输你的上传文件。
文件上传之后一般是放在系统的内存里面,如果文件的 size 大于设置的缓存内存大小,那么就放在临时文件中,默认的缓存内存是 64M,你可以通过如下来调整这个缓存内存大小:
web.MaxMemory = 1<<22
或者在配置文件中通过如下设置:
maxmemory = 1<<22
与此同时,beego 提供了另外一个参数,MaxUploadSize
来限制最大上传文件大小——如果你一次长传多个文件,那么它限制的就是这些所有文件合并在一起的大小。
默认情况下,MaxMemory
应该设置得比MaxUploadSize
小,这种情况下两个参数合并在一起的效果则是:
MaxMemory
,则直接在内存中处理;MaxMemory
和MaxUploadSize
之间,那么比MaxMemory
大的部分将会放在临时目录;MaxUploadSize
,直接拒绝请求,返回响应码 413Beego 提供了两个很方便的方法来处理文件上传:
GetFile(key string) (multipart.File, *multipart.FileHeader, error)
该方法主要用于用户读取表单中的文件名 the_file
,然后返回相应的信息,用户根据这些变量来处理文件上传:过滤、保存文件等。
SaveToFile(fromfile, tofile string) error
该方法是在 GetFile 的基础上实现了快速保存的功能
fromfile 是提交时候的 html 表单中的 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, 没有文件夹要先创建
}
支持从用户请求中直接数据 bind 到指定的对象,例如请求地址如下:
?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"}
beego 内置了 session 模块,使用 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 有几个方便的方法:
session 操作主要有设置 session、获取 session、删除 session。
当然你可以通过下面的方式自己控制这些逻辑:
sess:=this.StartSession()
defer sess.SessionRelease()
sess 对象具有如下方法:
但是我还是建议大家采用 SetSession、GetSession、DelSession 三个方法来操作,避免自己在操作的过程中资源没释放的问题。
关于 Session 模块使用中的一些参数设置:
web.BConfig.WebConfig.Session.SessionOn
设置是否开启 Session,默认是 false,配置文件对应的参数名:sessionon。
web.BConfig.WebConfig.Session.SessionName
设置 cookies 的名字,Session 默认是保存在用户的浏览器 cookies 里面的,默认名是 beegosessionID,配置文件对应的参数名是:sessionname。
web.BConfig.WebConfig.Session.SessionGCMaxLifetime
设置 Session 过期的时间,默认值是 3600 秒,配置文件对应的参数:sessiongcmaxlifetime。
web.BConfig.WebConfig.Session.SessionHashFunc
默认值为 sha1,采用 sha1 加密算法生产 sessionid
web.BConfig.WebConfig.Session.SessionCookieLifeTime
设置 cookie 的过期时间,cookie 是用来存储保存在客户端的数据。
beego 支持自定义过滤中间件,例如安全验证,强制跳转等。
过滤器函数如下所示:
web.InsertFilter(pattern string, pos int, filter FilterFunc, opts ...FilterOpt)
InsertFilter 函数的三个必填参数,一个可选参数
*
如下例子所示,验证用户是否已经登录,应用于全部的请求:
var FilterUser = func(ctx *context.Context) {
_, ok := ctx.Input.Session("uid").(int)
if !ok && ctx.Request.RequestURI != "/login" {
ctx.Redirect(302, "/login")
}
}
web.InsertFilter("/*", web.BeforeRouter, FilterUser)
这里需要特别注意使用 session 的 Filter 必须在 BeforeStatic 之后才能获取,因为 session 没有在这之前初始化。
还可以通过正则路由进行过滤,如果匹配参数就执行:
var FilterUser = func(ctx *context.Context) {
_, ok := ctx.Input.Session("uid").(int)
if !ok {
ctx.Redirect(302, "/login")
}
}
web.InsertFilter("/user/:id([0-9]+)", web.BeforeRouter, FilterUser)
这个 flash 与 Adobe/Macromedia Flash 没有任何关系。它主要用于在两个逻辑间传递临时数据,flash 中存放的所有数据会在紧接着的下一个逻辑中调用后清除。一般用于传递提示和错误消息。它适合 Post/Redirect/Get 模式。下面看使用的例子:
// 显示设置信息
func (c *MainController) Get() {
flash:=web.ReadFromRequest(&c.Controller)
if n,ok:=flash.Data["notice"];ok{
// 显示设置成功
c.TplName = "set_success.html"
}else if n,ok=flash.Data["error"];ok{
// 显示错误
c.TplName = "set_error.html"
}else{
// 不然默认显示设置页面
c.Data["list"]=GetInfo()
c.TplName = "setting_list.html"
}
}
// 处理设置信息
func (c *MainController) Post() {
flash:=web.NewFlash()
setting:=Settings{}
valid := Validation{}
c.ParseForm(&setting)
if b, err := valid.Valid(setting);err!=nil {
flash.Error("Settings invalid!")
flash.Store(&c.Controller)
c.Redirect("/setting",302)
return
}else if b!=nil{
flash.Error("validation err!")
flash.Store(&c.Controller)
c.Redirect("/setting",302)
return
}
saveSetting(setting)
flash.Notice("Settings saved!")
flash.Store(&c.Controller)
c.Redirect("/setting",302)
}
上面的代码执行的大概逻辑是这样的:
默认情况下 ReadFromRequest
函数已经实现了读取的数据赋值给 flash,所以在你的模板里面你可以这样读取数据:
{{.flash.error}}
{{.flash.warning}}
{{.flash.notice}}
flash 对象有三个级别的设置:
如果可以匹配 URL ,那么 beego 也可以生成 URL 吗?当然可以。 UrlFor() 函数就是用于构建指定函数的 URL 的。它把对应控制器和函数名结合的字符串作为第一个参数,其余参数对应 URL 中的变量。未知变量将添加到 URL 中作为查询参数。 例如:
下面定义了一个相应的控制器
type TestController struct {
web.Controller
}
func (this *TestController) Get() {
this.Data["Username"] = "astaxie"
this.Ctx.Output.Body([]byte("ok"))
}
func (this *TestController) List() {
this.Ctx.Output.Body([]byte("i am list"))
}
func (this *TestController) Params() {
this.Ctx.Output.Body([]byte(this.Ctx.Input.Params()["0"] + this.Ctx.Input.Params()["1"] + this.Ctx.Input.Params()["2"]))
}
func (this *TestController) Myext() {
this.Ctx.Output.Body([]byte(this.Ctx.Input.Param(":ext")))
}
func (this *TestController) GetUrl() {
this.Ctx.Output.Body([]byte(this.UrlFor(".Myext")))
}
下面是我们注册的路由:
web.Router("/api/list", &TestController{}, "*:List")
web.Router("/person/:last/:first", &TestController{})
web.AutoRouter(&TestController{})
那么通过方式可以获取相应的URL地址:
web.URLFor("TestController.List")
// 输出 /api/list
web.URLFor("TestController.Get", ":last", "xie", ":first", "asta")
// 输出 /person/xie/asta
web.URLFor("TestController.Myext")
// 输出 /Test/Myext
web.URLFor("TestController.GetUrl")
// 输出 /Test/GetUrl
默认情况下,beego 已经注册了 urlfor 函数,用户可以通过如下的代码进行调用
{{urlfor "TestController.List"}}
为什么不在把 URL 写死在模板中,反而要动态构建?有两个很好的理由:
beego 当初设计的时候就考虑了 API 功能的设计,而我们在设计 API 的时候经常是输出 JSON 或者 XML 数据,那么 beego 提供了这样的方式直接输出:
注意 struct 属性应该 为 exported Identifier
首字母应该大写
JSON 数据直接输出:
func (this *AddController) Get() {
mystruct := { ... }
this.Data["json"] = &mystruct
this.ServeJSON()
}
调用 ServeJSON 之后,会设置 content-type
为 application/json
,然后同时把数据进行 JSON 序列化输出。
XML 数据直接输出:
func (this *AddController) Get() {
mystruct := { ... }
this.Data["xml"]=&mystruct
this.ServeXML()
}
调用 ServeXML 之后,会设置 content-type
为 application/xml
,同时数据会进行 XML 序列化输出。
jsonp 调用
func (this *AddController) Get() {
mystruct := { ... }
this.Data["jsonp"] = &mystruct
this.ServeJSONP()
}
调用 ServeJSONP 之后,会设置 content-type
为 application/javascript
,然后同时把数据进行 JSON 序列化,然后根据请求的 callback 参数设置 jsonp 输出。
开发模式下序列化后输出的是格式化易阅读的 JSON 或 XML 字符串;在生产模式下序列化后输出的是压缩的字符串。
安装:
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)
}
通过 StructTag 使用示例:
import (
"log"
"strings"
"github.com/beego/beego/v2/core/validation"
)
// 验证函数写在 "valid" tag 的标签里
// 各个函数之间用分号 ";" 分隔,分号后面可以有空格
// 参数用括号 "()" 括起来,多个参数之间用逗号 "," 分开,逗号后面可以有空格
// 正则函数(Match)的匹配模式用两斜杠 "/" 括起来
// 各个函数的结果的 key 值为字段名.验证函数名
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: "[email protected]"}
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)
}
}
}
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
我们在做 Web 开发的时候,经常需要页面跳转和错误处理,beego 这方面也进行了考虑,通过 Redirect
方法来进行跳转:
func (this *AddController) Get() {
this.Redirect("/", 302)
}
如何中止此次请求并抛出异常,beego 可以在控制器中这样操作:
func (this *MainController) Get() {
this.Abort("401")
v := this.GetSession("asta")
if v == nil {
this.SetSession("asta", int(1))
this.Data["Email"] = 0
} else {
this.SetSession("asta", v.(int)+1)
this.Data["Email"] = v.(int)
}
this.TplName = "index.tpl"
}
这样 this.Abort("401")
之后的代码不会再执行,而且会默认显示给用户如下页面:
web 框架默认支持 401、403、404、500、503 这几种错误的处理。用户可以自定义相应的错误处理,例如下面重新定义 404 页面:
func page_not_found(rw http.ResponseWriter, r *http.Request){
t,_:= template.New("404.html").ParseFiles(web.BConfig.WebConfig.ViewsPath+"/404.html")
data :=make(map[string]interface{})
data["content"] = "page not found"
t.Execute(rw, data)
}
func main() {
web.ErrorHandler("404",page_not_found)
web.Router("/", &controllers.MainController{})
web.Run()
}
我们可以通过自定义错误页面 404.html
来处理 404 错误。
beego 更加人性化的还有一个设计就是支持用户自定义字符串错误类型处理函数,例如下面的代码,用户注册了一个数据库出错的处理页面:
func dbError(rw http.ResponseWriter, r *http.Request){
t,_:= template.New("dberror.html").ParseFiles(web.BConfig.WebConfig.ViewsPath+"/dberror.html")
data :=make(map[string]interface{})
data["content"] = "database is now down"
t.Execute(rw, data)
}
func main() {
web.ErrorHandler("dbError",dbError)
web.Router("/", &controllers.MainController{})
web.Run()
}
一旦在入口注册该错误处理代码,那么你可以在任何你的逻辑中遇到数据库错误调用 this.Abort("dbError")
来进行异常页面处理。
从 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()
}
beego ORM 是一个强大的 Go 语言 ORM 框架。她的灵感主要来自 Django ORM 和 SQLAlchemy。
目前该框架仍处于开发阶段,可能发生任何导致不兼容的改动。
已支持数据库驱动:
ORM 特性:
安装 ORM:
go get github.com/beego/beego/v2/client/orm
package main
import (
"fmt"
"github.com/beego/beego/v2/client/orm"
_ "github.com/go-sql-driver/mysql" // import your used driver
)
// Model Struct
type User struct {
Id int
Name string `orm:"size(100)"`
}
func init() {
// set default database
orm.RegisterDataBase("default", "mysql", "username:password@tcp(127.0.0.1:3306)/db_name?charset=utf8&loc=Local")
// register model
orm.RegisterModel(new(User))
// create table
orm.RunSyncdb("default", false, true)
}
func main() {
o := orm.NewOrm()
user := User{Name: "slene"}
// insert
id, err := o.Insert(&user)
fmt.Printf("ID: %d, ERR: %v\n", id, err)
// update
user.Name = "astaxie"
num, err := o.Update(&user)
fmt.Printf("NUM: %d, ERR: %v\n", num, err)
// read one
u := User{Id: user.Id}
err = o.Read(&u)
fmt.Printf("ERR: %v\n", err)
// delete
num, err = o.Delete(&u)
fmt.Printf("NUM: %d, ERR: %v\n", num, err)
}
type Post struct {
Id int `orm:"auto"`
Title string `orm:"size(100)"`
User *User `orm:"rel(fk)"`
}
var posts []*Post
qs := o.QueryTable("post")
num, err := qs.Filter("User__Name", "slene").All(&posts)
当您无法使用 ORM 来达到您的需求时,也可以直接使用 SQL 来完成查询/映射操作。
var maps []orm.Params
num, err := o.Raw("SELECT * FROM user").Values(&maps)
for _,term := range maps{
fmt.Println(term["id"],":",term["name"])
}
我们建议在使用过程中,一个数据库应该只存在一个ORM
对象。
models.go
package main
import (
"github.com/beego/beego/v2/client/orm"
)
type User struct {
Id int
Name string
Profile *Profile `orm:"rel(one)"` // OneToOne relation
Post []*Post `orm:"reverse(many)"` // 设置一对多的反向关系
}
type Profile struct {
Id int
Age int16
User *User `orm:"reverse(one)"` // 设置一对一反向关系(可选)
}
type Post struct {
Id int
Title string
User *User `orm:"rel(fk)"` //设置一对多关系
Tags []*Tag `orm:"rel(m2m)"`
}
type Tag struct {
Id int
Name string
Posts []*Post `orm:"reverse(many)"` //设置多对多反向关系
}
func init() {
// 需要在init中注册定义的model
orm.RegisterModel(new(User), new(Post), new(Profile), new(Tag))
}
main.go
package main
import (
"fmt"
"github.com/beego/beego/v2/client/orm"
_ "github.com/go-sql-driver/mysql"
)
func init() {
orm.RegisterDriver("mysql", orm.DRMySQL)
orm.RegisterDataBase("default", "mysql", "root:root@/orm_test?charset=utf8")
}
func main() {
// 默认使用 default,你可以指定为其他数据库
o := orm.NewOrm()
profile := new(Profile)
profile.Age = 30
user := new(User)
user.Profile = profile
user.Name = "slene"
fmt.Println(o.Insert(profile))
fmt.Println(o.Insert(user))
}
默认数据库类型
orm.DRMySQL
orm.DRSqlite
orm.DRPostgres
orm.DRTiDB
// < 1.6
orm.DR_MySQL
orm.DR_Sqlite
orm.DR_Postgres
// 参数1 driverName
// 参数2 数据库类型
// 这个用来设置 driverName 对应的数据库类型
// mysql / sqlite3 / postgres / tidb 这几种是默认已经注册过的,所以可以无需设置
orm.RegisterDriver("mysql", orm.DRMySQL)
ORM 必须注册一个别名为 default
的数据库,作为默认使用。
ORM 使用 golang 自己的连接池
// 参数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))
将你定义的 Model 进行注册,最佳设计是有单独的 models.go 文件,在他的 init 函数中进行注册。
迷你版 models.go
package main
import "github.com/beego/beego/v2/client/orm"
type User struct {
Id int
Name string
}
func init(){
orm.RegisterModel(new(User))
}
RegisterModel 也可以同时注册多个 model
orm.RegisterModel(new(User), new(Profile), new(Post))
使用表名前缀
orm.RegisterModelWithPrefix("prefix_", new(User))
创建后的表名为 prefix_user
传入表名,或者 Model 对象,返回一个 QuerySeter
o := orm.NewOrm()
var qs orm.QuerySeter
qs = o.QueryTable("user")
// 如果表没有定义过,会立刻 panic
使用 sql 语句直接进行操作
Raw 函数,返回一个 RawSeter 用以对设置的 sql 语句和参数进行操作
o := orm.NewOrm()
var r orm.RawSeter
r = o.Raw("UPDATE user SET name = ? WHERE name = ?", "testing", "slene")
如果已知主键的值,那么可以使用这些方法进行 CRUD 操作
对 object 操作的四个方法 Read / Insert / Update / Delete
o := orm.NewOrm()
user := new(User)
user.Name = "slene"
fmt.Println(o.Insert(user))
user.Name = "Your"
fmt.Println(o.Update(user))
fmt.Println(o.Read(user))
fmt.Println(o.Delete(user))
o := orm.NewOrm()
user := User{Id: 1}
err := o.Read(&user)
if err == orm.ErrNoRows {
fmt.Println("查询不到")
} else if err == orm.ErrMissPK {
fmt.Println("找不到主键")
} else {
fmt.Println(user.Id, user.Name)
}
Read 默认通过查询主键赋值,可以使用指定的字段进行查询:
user := User{Name: "slene"}
err = o.Read(&user, "Name")
...
尝试从数据库读取,不存在的话就创建一个。
它的实现机制,和具体的数据库有关。例如在 MYSQL 中,是使用
默认必须传入一个参数作为条件字段,同时也支持多个参数多个条件字段
o := orm.NewOrm()
user := User{Name: "slene"}
// 三个返回参数依次为:是否新创建的,对象 Id 值,错误
if created, id, err := o.ReadOrCreate(&user, "Name"); err == nil {
if created {
fmt.Println("New Insert an object. Id:", id)
} else {
fmt.Println("Get an object. Id:", id)
}
}
第一个返回值为自增健 Id 的值
o := orm.NewOrm()
var user User
user.Name = "slene"
user.IsActive = true
id, err := o.Insert(&user)
if err == nil {
fmt.Println(id)
}
创建后会自动对 auto 的 field 赋值
同时插入多个对象
类似sql语句
insert into table (name, age) values("slene", 28),("astaxie", 30),("unknown", 20)
第一个参数 bulk 为并列插入的数量,第二个为对象的slice
返回值为成功插入的数量
users := []User{
{Name: "slene"},
{Name: "astaxie"},
{Name: "unknown"},
...
}
successNums, err := o.InsertMulti(100, users)
bulk 为 1 时,将会顺序插入 slice 中的数据
第一个返回值为影响的行数
o := orm.NewOrm()
user := User{Id: 1}
if o.Read(&user) == nil {
user.Name = "MyName"
if num, err := o.Update(&user); err == nil {
fmt.Println(num)
}
}
Update 默认更新所有的字段,可以更新指定的字段:
// 只更新 Name
o.Update(&user, "Name")
// 指定多个字段
// o.Update(&user, "Field1", "Field2", ...)
...
根据复杂条件更新字段值参见 Update
第一个返回值为影响的行数
o := orm.NewOrm()
if num, err := o.Delete(&User{Id: 1}); err == nil {
fmt.Println(num)
}
Delete 操作会对反向关系进行操作,此例中 Post 拥有一个到 User 的外键。删除 User 的时候。如果 on_delete 设置为默认的级联操作,将删除对应的 Post
Changed in 1.0.3 删除以后不会删除 auto field 的值
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
QuerySeter 中用于描述字段和 sql 操作符,使用简单的 expr 查询方法
字段组合的前后顺序依照表的关系,比如 User 表拥有 Profile 的外键,那么对 User 表查询对应的 Profile.Age 为条件,则使用 Profile__Age
注意,字段的分隔符号使用双下划线 __
,除了描述字段, expr 的尾部可以增加操作符以执行对应的 sql 操作。比如 Profile__Age__gt
代表 Profile.Age > 18 的条件查询。
注释后面将描述对应的 sql 语句,仅仅是描述 expr 的类似结果,并不代表实际生成的语句。
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
qs.Filter("profile__age__gte", 18) // WHERE profile.age >= 18
qs.Filter("profile__age__in", 18, 20) // WHERE profile.age IN (18, 20)
qs.Filter("profile__age__in", 18, 20).Exclude("profile__lt", 1000)
// WHERE profile.age IN (18, 20) AND NOT profile_id < 1000
当前支持的操作符号:
后面以 i
开头的表示:大小写不敏感
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
qs.Filter("name__iexact", "slene")
// WHERE name LIKE 'slene'
// 大小写不敏感,匹配任意 'Slene' 'sLENE'
qs.Filter("name__contains", "slene")
// WHERE name LIKE BINARY '%slene%'
// 大小写敏感, 匹配包含 slene 的字符
qs.Filter("name__icontains", "slene")
// WHERE name LIKE '%slene%'
// 大小写不敏感, 匹配任意 'im Slene', 'im sLENE'
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)
// 同上效果
qs.Filter("profile__age__gt", 17)
// WHERE profile.age > 17
qs.Filter("profile__age__gte", 18)
// WHERE profile.age >= 18
qs.Filter("profile__age__lt", 17)
// WHERE profile.age < 17
qs.Filter("profile__age__lte", 18)
// WHERE profile.age <= 18
qs.Filter("name__startswith", "slene")
// WHERE name LIKE BINARY 'slene%'
// 大小写敏感, 匹配以 'slene' 起始的字符串
qs.Filter("name__istartswith", "slene")
// WHERE name LIKE 'slene%'
// 大小写不敏感, 匹配任意以 'slene', 'Slene' 起始的字符串
qs.Filter("name__endswith", "slene")
// WHERE name LIKE BINARY '%slene'
// 大小写敏感, 匹配以 'slene' 结束的字符串
qs.Filter("name__iendswithi", "slene")
// WHERE name LIKE '%slene'
// 大小写不敏感, 匹配任意以 'slene', 'Slene' 结束的字符串
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
用来过滤查询结果,起到 包含条件 的作用
多个 Filter 之间使用 AND
连接
qs.Filter("profile__isnull", true).Filter("name", "slene")
// WHERE profile_id IS NULL AND name = 'slene'
用来过滤查询结果,起到 排除条件 的作用
使用 NOT
排除条件
多个 Exclude 之间使用 AND
连接
qs.Exclude("profile__isnull", true).Filter("name", "slene")
// WHERE NOT profile_id IS NULL AND name = 'slene'
自定义条件表达式
cond := orm.NewCondition()
cond1 := cond.And("profile__isnull", false).AndNot("status__in", 1).Or("profile__age__gt", 2000)
qs := orm.QueryTable("user")
qs = qs.SetCond(cond1)
// WHERE ... AND ... AND NOT ... OR ...
cond2 := cond.AndCond(cond1).OrCond(cond.And("name", "slene"))
qs = qs.SetCond(cond2).Count()
// WHERE (... AND ... AND NOT ... OR ...) OR ( ... )
限制最大返回数据行数,第二个参数可以设置 Offset
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 偏移的情况
设置 偏移行数
qs.Offset(20)
// LIMIT 1000 OFFSET 20
qs.GroupBy("id", "age")
// GROUP BY id,age
参数使用 expr
在 expr 前使用减号 -
表示 DESC
的排列
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
对应 sql 的 distinct
语句, 返回指定字段不重复的值.
qs.Distinct()
// SELECT DISTINCT
关系查询,参数使用 expr
var DefaultRelsDepth = 5 // 默认情况下直接调用 RelatedSel 将进行最大 5 层的关系查询
qs := o.QueryTable("post")
qs.RelatedSel()
// INNER JOIN user ... LEFT OUTER JOIN profile ...
qs.RelatedSel("user")
// INNER JOIN user ...
// 设置 expr 只对设置的字段进行关系查询
// 对设置 null 属性的 Field 将使用 LEFT OUTER JOIN
依据当前的查询条件,返回结果行数
cnt, err := o.QueryTable("user").Count() // SELECT COUNT(*) FROM USER
fmt.Printf("Count Num: %s, %s", cnt, err)
exist := o.QueryTable("user").Filter("UserName", "Name").Exist()
fmt.Printf("Is Exist: %s", exist)
依据当前查询条件,进行批量更新操作
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 // 除
依据当前查询条件,进行批量删除操作
num, err := o.QueryTable("user").Filter("name", "slene").Delete()
fmt.Printf("Affected Num: %s, %s", num, err)
// DELETE FROM user WHERE name = "slene"
用于一次 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
返回对应的结果集对象
All 的参数支持 *[]Type 和 *[]*Type 两种形式的 slice
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")
对象的其他字段值将会是对应类型的默认值
尝试返回单条记录
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")
}
可以指定返回的字段:
// 只返回 Id 和 Title
var post Post
o.QueryTable("post").Filter("Content__istartswith", "prefix string").One(&post, "Id", "Title")
对象的其他字段值将会是对应类型的默认值
返回结果集的 key => value 值
key 为Model里的Field name, value的值是interface{}类型,例如,如果你要将value赋值给struct中的某字段,需要根据结构体对应字段类型使用断言获取真实值。举例: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"])
}
}
返回指定的 Field 数据
TODO: 暂不支持级联查询 RelatedSel 直接返回 Values
但可以直接指定 expr 级联返回需要的数据
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 中的数据都是展开的,没有复杂的嵌套
}
}
顾名思义,返回的结果集以slice存储
结果的排列与 Model 中定义的 Field 顺序一致
返回的每个元素值以 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)
}
}
当然也可以指定 expr 返回指定的 Field
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])
}
}
只返回特定的 Field 值,将结果集展开到单个 slice 里
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, ", "))
}
已经取得了 User 对象,查询 Profile:
user := &User{Id: 1}
o.Read(user)
if user.Profile != nil {
o.Read(user.Profile)
}
直接关联查询:
user := &User{}
o.QueryTable("user").Filter("Id", 1).RelatedSel().One(user)
// 自动查询到 Profile
fmt.Println(user.Profile)
// 因为在 Profile 里定义了反向关系的 User,所以 Profile 里的 User 也是自动赋值过的,可以直接取用。
fmt.Println(user.Profile.User)
// [SELECT T0.`id`, T0.`name`, T0.`profile_id`, T1.`id`, T1.`age` FROM `user` T0 INNER JOIN `profile` T1 ON T1.`id` = T0.`profile_id` WHERE T0.`id` = ? LIMIT 1000] - `1`
通过 User 反向查询 Profile:
var profile Profile
err := o.QueryTable("profile").Filter("User__Id", 1).One(&profile)
if err == nil {
fmt.Println(profile)
}
type Post struct {
Id int
Title string
User *User `orm:"rel(fk)"`
Tags []*Tag `orm:"rel(m2m)"`
}
var posts []*Post
num, err := o.QueryTable("post").Filter("User", 1).RelatedSel().All(&posts)
if err == nil {
fmt.Printf("%d posts read\n", num)
for _, post := range posts {
fmt.Printf("Id: %d, UserName: %d, Title: %s\n", post.Id, post.User.UserName, post.Title)
}
}
// [SELECT T0.`id`, T0.`title`, T0.`user_id`, T1.`id`, T1.`name`, T1.`profile_id`, T2.`id`, T2.`age` FROM `post` T0 INNER JOIN `user` T1 ON T1.`id` = T0.`user_id` INNER JOIN `profile` T2 ON T2.`id` = T1.`profile_id` WHERE T0.`user_id` = ? LIMIT 1000] - `1`
根据 Post.Title 查询对应的 User:
RegisterModel 时,ORM 也会自动建立 User 中 Post 的反向关系,所以可以直接进行查询
var user User
err := o.QueryTable("user").Filter("Post__Title", "The Title").Limit(1).One(&user)
if err == nil {
fmt.Printf(user)
}
设置 rel(m2m) 以后,ORM 会自动创建中间表
type Post struct {
Id int
Title string
User *User `orm:"rel(fk)"`
Tags []*Tag `orm:"rel(m2m)"`
}
type Tag struct {
Id int
Name string
Posts []*Post `orm:"reverse(many)"`
}
一条 Post 纪录可能对应不同的 Tag 纪录,一条 Tag 纪录可能对应不同的 Post 纪录,所以 Post 和 Tag 属于多对多关系,通过 tag name 查询哪些 post 使用了这个 tag
var posts []*Post
num, err := dORM.QueryTable("post").Filter("Tags__Tag__Name", "golang").All(&posts)
通过 post title 查询这个 post 有哪些 tag
var tags []*Tag
num, err := dORM.QueryTable("tag").Filter("Posts__Post__Title", "Introduce Beego ORM").All(&tags)
LoadRelated 用于载入模型的关系字段,包括所有的 rel/reverse - one/many 关系
ManyToMany 关系字段载入
// 载入相应的 Tags
post := Post{Id: 1}
err := o.Read(&post)
num, err := o.LoadRelated(&post, "Tags")
// 载入相应的 Posts
tag := Tag{Id: 1}
err := o.Read(&tag)
num, err := o.LoadRelated(&tag, "Posts")
User 是 Post 的 ForeignKey,对应的 ReverseMany 关系字段载入
type User struct {
Id int
Name string
Posts []*Post `orm:"reverse(many)"`
}
user := User{Id: 1}
err := dORM.Read(&user)
num, err := dORM.LoadRelated(&user, "Posts")
for _, post := range user.Posts {
//...
}
创建一个 QueryM2Mer 对象
o := orm.NewOrm()
post := Post{Id: 1}
m2m := o.QueryM2M(&post, "Tags")
// 第一个参数的对象,主键必须有值
// 第二个参数为对象需要操作的 M2M 字段
// QueryM2Mer 的 api 将作用于 Id 为 1 的 Post
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)
从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)
判断 Tag 是否存在于 M2M 关系中
if m2m.Exist(&Tag{Id: 2}) {
fmt.Println("Tag Exist")
}
清除所有 M2M 关系
nums, err := m2m.Clear()
if err == nil {
fmt.Println("Removed Tag Nums: ", nums)
}
计算 Tag 的数量
nums, err := m2m.Count()
if err == nil {
fmt.Println("Total Nums: ", nums)
}
?
,自动转换o := orm.NewOrm()
ids := []int{1, 2, 3}
var r RawSter
r = o.Raw("SELECT name FROM user WHERE id IN (?, ?, ?)", ids)
创建一个 RawSeter
o := orm.NewOrm()
var r RawSeter
r = o.Raw("UPDATE user SET name = ? WHERE name = ?", "testing", "slene")
执行 sql 语句,返回 sql.Result 对象
res, err := o.Raw("UPDATE user SET name = ?", "your").Exec()
if err == nil {
num, _ := res.RowsAffected()
fmt.Println("mysql row affected nums: ", num)
}
QueryRow 和 QueryRows 提供高级 sql mapper 功能
支持 struct
type User struct {
Id int
UserName string
}
var user User
err := o.Raw("SELECT id, user_name FROM user WHERE id = ?", 1).QueryRow(&user)
QueryRows 支持的对象还有 map 规则是和 QueryRow 一样的,但都是 slice
type User struct {
Id int
UserName string
}
var users []User
num, err := o.Raw("SELECT id, user_name FROM user WHERE id = ?", 1).QueryRows(&users)
if err == nil {
fmt.Println("user nums: ", num)
}
改变 Raw(sql, args…) 中的 args 参数,返回一个新的 RawSeter
用于单条 sql 语句,重复利用,替换参数然后执行。
res, err := r.SetArgs("arg1", "arg2").Exec()
res, err := r.SetArgs("arg1", "arg2").Exec()
...
Raw SQL 查询获得的结果集 Value 为 string
类型,NULL 字段的值为空 ``
返回结果集的 key => value 值
var maps []orm.Params
num, err := o.Raw("SELECT user_name FROM user WHERE status = ?", 1).Values(&maps)
if err == nil && num > 0 {
fmt.Println(maps[0]["user_name"]) // slene
}
返回结果集 slice
var lists []orm.ParamsList
num, err := o.Raw("SELECT user_name FROM user WHERE status = ?", 1).ValuesList(&lists)
if err == nil && num > 0 {
fmt.Println(lists[0][0]) // slene
}
返回单一字段的平铺 slice 数据
var list orm.ParamsList
num, err := o.Raw("SELECT id FROM user WHERE id < ?", 10).ValuesFlat(&list)
if err == nil && num > 0 {
fmt.Println(list) // []{"1","2","3",...}
}
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,
// }
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 多次 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
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)
复杂的模型定义不是必须的,此功能用作数据库数据转换和自动建表
默认的表名规则,使用驼峰转蛇形:
AuthUser -> auth_user
Auth_User -> auth__user
DB_AuthUser -> d_b__auth_user
除了开头的大写字母以外,遇到大写会增加 _
,原名称中的下划线保留。
实现接口TableNameI
:
type User struct {
Id int
Name string
}
func (u *User) TableName() string {
return "auth_user"
}
如果前缀设置为 prefix_
那么表名为:prefix_auth_user
orm:"null;rel(fk)"
多个设置间使用 ;
分隔,设置的值如果是多个,使用 ,
分隔。
设置 -
即可忽略 struct 中的字段
type User struct {
// ...
AnyField string `orm:"-"`
//...
}
当 Field 类型为 int, int32, int64, uint, uint32, uint64 时,可以设置字段为自增健
Id
的 Field 将被视为自增健。鉴于 go 目前的设计,即使使用了 uint64,但你也不能存储到他的最大值。依然会作为 int64 处理。
设置为主键,适用于自定义其他类型为主键
数据库表默认为 NOT NULL
,设置 null 代表 ALLOW NULL
Name string `orm:"null"`
为单个字段增加索引
Name string `orm:"index"`
为单个字段增加 unique 键
Name string `orm:"unique"`
为字段设置 db 字段的名称
Name string `orm:"column(user_name)"`
string 类型字段默认为 varchar(255)
设置 size 以后,db type 将使用 varchar(size)
Title string `orm:"size(60)"`
设置 float32, float64 类型的浮点精度
Money float64 `orm:"digits(12);decimals(4)"`
总长度 12 小数点后 4 位 eg: 99999999.9999
Created time.Time `orm:"auto_now_add;type(datetime)"`
Updated time.Time `orm:"auto_now;type(datetime)"`
对于批量的 update 此设置是不生效的
设置为 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)"`
在 v2.x 里面,我们提供了默认值的Filter:
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",
})
}
我们一个很重要的认识,是认为显式设置值才是更加优雅的实践,所以我们将默认值的功能做成了一种可选的filter
实现。
为字段添加注释
type User struct {
...
Status int `orm:"default(1);description(这是状态字段)"`
...
}
注意: 注释中禁止包含引号
为datetime
字段设置精度值位数,不同DB支持最大精度值位数也不一致。
type User struct {
…
Created time.Time orm:"type(datetime);precision(4)"
…
}
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)"`
...
}
此设置针对 orm:"rel(m2m)"
的关系字段
rel_table 设置自动生成的 m2m 关系表的名称
rel_through 如果要在 m2m 关系中使用自定义的 m2m 关系表
通过这个设置其名称,格式为 pkg.path.ModelName
eg: app.models.PostTagRel
PostTagRel 表需要有到 Post 和 Tag 的关系
当设置 rel_table 时会忽略 rel_through
设置方法:
orm:"rel(m2m);rel_table(the_table_name)"
orm:"rel(m2m);rel_through(pkg.path.ModelName)"
设置对应的 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 -> User 是 ManyToOne 的关系,也就是外键。
o.Filter("Id", 1).Delete()
这个时候即会删除 Id 为 1 的 User 也会删除其发布的 Post
不想删除的话,需要设置 set_null
type Post struct {
Id int
Title string
User *User `orm:"rel(fk);null;on_delete(set_null)"`
}
那这个时候,删除 User 只会把对应的 Post.user_id 设置为 NULL
当然有时候为了高性能的需要,多存点数据无所谓啊,造成批量删除才是问题。
type Post struct {
Id int
Title string
User *User `orm:"rel(fk);null;on_delete(do_nothing)"`
}
那么只要删除的时候,不操作 Post 就可以了。
在此列出 ORM 推荐的对应数据库类型,自动建表功能也会以此为标准。
默认所有的字段都是 NOT NULL
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) |
其字段类型取决于对应的主键。
注册模型与数据库以后,调用 RunCommand 执行 orm 命令。
func main() {
// orm.RegisterModel...
// orm.RegisterDataBase...
// ...
orm.RunCommand()
}
go build main.go
./main orm
# 直接执行可以显示帮助
# 如果你的程序可以支持的话,直接运行 go run main.go orm 也是一样的效果
./main orm syncdb -h
Usage of orm command: syncdb:
-db="default": DataBase alias name
-force=false: drop tables before create
-v=false: verbose info
使用 -force=1
可以 drop table 后再建表
使用 -v
可以查看执行的 sql 语句
在程序中直接调用自动建表:
// 数据库别名
name := "default"
// drop table 后再建表
force := true
// 打印执行过程
verbose := true
// 遇到错误立即返回
err := orm.RunSyncdb(name, force, verbose)
if err != nil {
fmt.Println(err)
}
自动建表功能在非 force 模式下,是会自动创建新增加的字段的。也会创建新增加的索引。
对于改动过的旧字段,旧索引,需要用户自行进行处理。
./main orm sqlall -h
Usage of orm command: syncdb:
-db="default": DataBase alias name
默认使用别名为 default 的数据库。
本文讲述 beego 中使用的模板语法,与 go 模板语法基本相同。
go 统一使用了 {{
和 }}
作为左右标签,没有其他的标签符号。如果您想要修改为其它符号,可以参考 模板标签。
使用 .
来访问当前位置的上下文
使用 $
来引用当前模板根级的上下文
使用 $var
来访问创建的变量
[more]
模板中支持的 go 语言符号
{{"string"}} // 一般 string
{{`raw string`}} // 原始 string
{{'c'}} // byte
{{print nil}} // nil 也被支持
模板中的 pipeline
可以是上下文的变量输出,也可以是函数通过管道传递的返回值
{{. | FuncA | FuncB | FuncC}}
当 pipeline 的值等于:
那么这个 pipeline 被认为是空
{{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 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 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 "loop"}}
{{.Name}}
{{end}}
使用 template 调用模板
{{range .Items}}
{{template "loop" .}}
{{end}}
{{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 .X .Y .Z}}
and 会逐一判断每个参数,将返回第一个为空的参数,否则就返回最后一个非空参数
{{call .Field.Func .Arg1 .Arg2}}
call 可以调用函数,并传入参数
调用的函数需要返回 1 个值 或者 2 个值,返回两个值时,第二个值用于返回 error 类型的错误。返回的错误不等于 nil 时,执行将终止。
index 支持 map, slice, array, string,读取指定类型对应下标的值
this.Data["Maps"] = map[string]string{"name": "Beego"}
{{index .Maps "name"}}
{{printf "The content length is %d" (.Content|len)}}
返回对应类型的长度,支持类型:map, slice, array, string, chan
not 返回输入参数的否定值,if true then false else true
{{or .X .Y .Z}}
or 会逐一判断每个参数,将返回第一个非空的参数,否则就返回最后一个参数
对应 fmt.Sprint
对应 fmt.Sprintf
对应 fmt.Sprintln
{{urlquery "http://beego.vip"}}
将返回
http%3A%2F%2Fbeego.vip
这类函数一般配合在 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 的模板处理引擎采用的是 Go 内置的 html/template
包进行处理,而且 beego 的模板处理逻辑是采用了缓存编译方式,也就是所有的模板会在 beego 应用启动的时候全部编译然后缓存在 map 里面。
beego 中默认的模板目录是 views
,用户可以把模板文件放到该目录下,beego 会自动在该目录下的所有模板文件进行解析并缓存,开发模式下每次都会重新解析,不做缓存。当然,用户也可以通过如下的方式改变模板的目录(只能指定一个目录为模板目录):
beego.ViewsPath = "myviewpath"
用户无需手动的调用渲染输出模板,beego 会自动的在调用完相应的 method 方法之后调用 Render 函数,当然如果您的应用是不需要模板输出的,那么可以在配置文件或者在 main.go
中设置关闭自动渲染。
配置文件配置如下:
autorender = false
main.go 文件中设置如下:
web.AutoRender = false
Go 语言的默认模板采用了 {{
和 }}
作为左右标签,但是我们有时候在开发中可能界面是采用了 AngularJS 开发,他的模板也是这个标签,故而引起了冲突。在 beego 中你可以通过配置文件或者直接设置配置变量修改:
web.TemplateLeft = "<<<"
web.TemplateRight = ">>>"
模板中的数据是通过在 Controller 中 this.Data
获取的,所以如果你想在模板中获取内容 {{.Content}}
,那么你需要在 Controller 中如下设置:
this.Data["Content"] = "value"
如何使用各种类型的数据渲染:
结构体
结构体结构
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}}
beego 采用了 Go 语言内置的模板引擎,所有模板的语法和 Go 的一模一样,至于如何写模板文件,详细的请参考 模板教程。
用户通过在 Controller 的对应方法中设置相应的模板名称,beego 会自动的在 viewpath 目录下查询该文件并渲染,例如下面的设置,beego 会在 admin 下面找 add.tpl 文件进行渲染:
this.TplName = "admin/add.tpl"
我们看到上面的模板后缀名是 tpl,beego 默认情况下支持 tpl 和 html 后缀名的模板文件,如果你的后缀名不是这两种,请进行如下设置:
web.AddTemplateExt("你文件的后缀名")
当你设置了自动渲染,然后在你的 Controller 中没有设置任何的 TplName,那么 beego 会自动设置你的模板文件如下:
c.TplName = strings.ToLower(c.controllerName) + "/" + strings.ToLower(c.actionName) + "." + c.TplExt
也就是你对应的 Controller 名字+请求方法名.模板后缀,也就是如果你的 Controller 名是 AddController
,请求方法是 POST
,默认的文件后缀是 tpl
,那么就会默认请求 /viewpath/AddController/post.tpl
文件。
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" .}}
特别注意后面的
.
,这是传递当前参数到子模板
对于一个复杂的 LayoutContent
,其中可能包括有javascript脚本、CSS 引用等,根据惯例,通常 css 会放到 Head 元素中,javascript 脚本需要放到 body 元素的末尾,而其它内容则根据需要放在合适的位置。在 Layout
页中仅有一个 LayoutContent
是不够的。所以在 Controller
中增加了一个 LayoutSections
属性,可以允许 Layout
页中设置多个 section
,然后每个 section
可以分别包含各自的子模板页。
layout_blog.tpl:
DOCTYPE html>
<html>
<head>
<title>Lin Lititle>
<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:
逻辑处理如下所示:
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"] = ""
}
定义 struct:
type User struct {
Id int `form:"-"`
Name interface{} `form:"username"`
Age int `form:"age,text,年龄:"`
Sex string
Intro string `form:",textarea"`
}
form
,和 ParseForm 方法 共用一个标签,标签后面有三个可选参数,用 ,
分割。第一个参数为表单中类型的 name
的值,如果为空,则以 struct field name
为值。第二个参数为表单组件的类型,如果为空,则为 text
。表单组件的标签默认为 struct field name
的值,否则为第三个值。form
标签只有一个值,则为表单中类型 name
的值,除了最后一个值可以忽略外,其他位置的必须要有 ,
号分割,如:form:",,姓名:"
form
标签的值设置为 -
controller:
func (this *AddController) Get() {
this.Data["Form"] = &User{}
this.TplName = "index.tpl"
}
Form 的参数必须是一个 struct 的指针。
template:
上面的代码生成的表单为:
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="">
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 文件生成一个 标签. 使用方法 {{assets_js src}}
assets_css
为 css 文件生成一个 标签. 使用方法 {{assets_css src}}
config
获取 AppConfig 的值. 使用方法 {{config configType configKey defaultValue}}. 可选的 configType 有 String, Bool, Int, Int64, Float, DIY
map_get
获取 map
的值
用法:
// In controller
Data["m"] = map[string]interface{} {
"a": 1,
"1": map[string]float64{
"c": 4,
},
}
// In view
{{ map_get .m "a" }} // return 1
{{ map_get .m 1 "c" }} // return 4
urlfor
获取控制器方法的 URL
{{urlfor "TestController.List"}}
Go 语言内部其实已经提供了 http.ServeFile
,通过这个函数可以实现静态文件的服务。beego 针对这个功能进行了一层封装,通过下面的方式进行静态文件注册:
web.SetStaticPath("/static","public")
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
页面。而且开启该功能之后,用户访问目录就会显示该目录下所有的文件列表。