Beego框架

本地目录

C:\Users\Moebius\go\webApp

/home/goproject/beego_blog-master

https://github.com/Echosong/beego_blog

 

golang单元测试:https://blog.csdn.net/gubeichengxuyuan/article/details/126826813

Vscode-golang快捷键:https://blog.csdn.net/m0_48186564/article/details/126697430

1.4快速生成代码片段
pkgm main包+main主函数
ff fmt.Printf(“”, var)
for for i := 0; i < count; i++ {}
forr for _, v := range v {}
fmain func main() {}
a.print! fmt.Printf(“a: %v\n”, a)
————————————————

 

Beego框架

Beego 用于在 Go 中快速开发企业应用程序,包括 RESTful API、Web 应用程序和后端服务。

它的灵感来自 Tornado、Sinatra 和 Flask。beego 具有一些 Go 特定的功能,例如接口和结构嵌入。

资料

github:https://github.com/beego/beego

中文文档1:https://beego.gocn.vip/beego/zh/developing/bee/

中文文档2:https://zhuanlan.zhihu.com/p/65869449

web框架 : https://zhuanlan.zhihu.com/p/463264052

项目:http://docs.beego.layui.easygoadmin.vip/#/started?id=intro

beego框架:https://zhuanlan.zhihu.com/p/65869449

下载安装:go install github.com/beego/bee/v2@latest

go get github.com/beego/beego/v2@latest

验证Windows下的安装:

go install github.com/beego/bee/v2

 

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.

创建项目-启动项目

bee new webApp

bee run webApp

 

启动成功效果:

我们打开浏览器就可以看到效果 http://localhost:8080/:

 

创建后项目目录结构

webApp
├── 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

 

Beego框架流程分析

beego由四部分组成:

  1. 基础模块:包括log模块、config模块、governor模块;

  2. Task:用于运行定时任务或周期性任务;

  3. 客户端:包括ORM模块、httplib模块、缓存模块;

  4. 服务器:包括网页模块。未来我们会支持gRPC;

建筑学

Web应用程序

img

 

bee常用命令

new 命令

new 命令是新建一个 Web 项目,我们在命令行下执行 bee new <项目名> 就可以创建一个新的项目。但是注意该命令必须在 $GOPATH/src 下执行。最后会在 $GOPATH/src 相应目录下生成如下目录结构的项目:

api 命令

上面的 new 命令是用来新建 Web 项目,不过很多用户使用 beego 来开发 API 应用。所以这个 api 命令就是用来创建 API 应用的

run 命令

我们在开发 Go 项目的时候最大的问题是经常需要自己手动去编译再运行,bee run 命令是监控 beego 的项目,通过 fsnotify (opens new window)监控文件系统。但是注意该命令必须在 $GOPATH/src/appname 下执行。 这样我们在开发过程中就可以实时的看到项目修改之后的效果:

pack 命令

pack 目录用来发布应用的时候打包,会把项目打包成 zip 包,这样我们部署的时候直接把打包之后的项目上传,解压就可以部署了:

bale 命令

这个命令目前仅限内部使用,具体实现方案未完善,主要用来压缩所有的静态文件变成一个变量申明文件,全部编译到二进制文件里面,用户发布的时候携带静态文件,包括 js、css、img 和 views。最后在启动运行时进行非覆盖式的自解压。

version 命令

这个命令是动态获取 bee、beego 和 Go 的版本,这样一旦用户出现错误,可以通过该命令来查看当前的版本

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

示例:

image-20221212142451335

#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 dockerrize -image="library/golang:1.17.3" -expose=8080

FROM library/golang:1.17.3

# Godep for vendoring
RUN go get github.com/tools/godep

# Recompile the standard library without CGO
RUN CGO_ENABLED=0 go install -a std

ENV APP_DIR $GOPATH\webApp
RUN mkdir -p $APP_DIR

# Set the entrypoint
ENTRYPOINT (cd $APP_DIR && ./\webApp)
ADD . $APP_DIR

# Compile the binary and statically link
RUN cd $APP_DIR && CGO_ENABLED=0 godep go build -ldflags '-d -w -s'

EXPOSE 8080

 

#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": []:如果你需要在每次启动时设置临时环境变量参数,则可以使用该选项。

 

配置模块

大致上有两种用法:

  • 使用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}"

bee new新创建的项目配置文件默认如下:

image-20221213150357754

支持的格式

注意,所以的相对文件路径,都是从你的工作目录开始计算! 其次,除了默认的 INI 格式,其余格式都需要采用匿名引入的方式引入对应的包。

#INI 格式

INI 是配置模块的默认格式。同时它支持使用include的方式,加载多个配置文件。

app.ini:

	appname = beepkg
httpaddr = "127.0.0.1"
httpport = 9090

include "app2.ini"

app2.ini:

	runmode ="dev"
autorender = false
recoverpanic = false
viewspath = "myview"
[dev]
httpport = 8080
[prod]
httpport = 8088
[test]
httpport = 8888
func main() {
cfg, err := config.NewConfig("ini", "app.ini")
if err != nil {
logs.Error(err)
}
val, _ := cfg.String("appname")
logs.Info("auto load config name is", val)
}

#JSON

JSON 只需要指定格式,并且不要忘了使用匿名引入的方式引入 JSON 的实现:

import (
"github.com/beego/beego/v2/core/config"
// 千万不要忘了
_ "github.com/beego/beego/v2/core/config/json"
"github.com/beego/beego/v2/core/logs"
)

var (
ConfigFile = "./app.json"
)

func main() {
err := config.InitGlobalInstance("json", ConfigFile)
if err != nil {
logs.Critical("An error occurred:", err)
panic(err)
}

val, _ := config.String("name")

logs.Info("load config name is", val)
}

#YAML

别忘了匿名引入 YAML 的实现!

import (
"github.com/beego/beego/v2/core/config"
// never forget this
_ "github.com/beego/beego/v2/core/config/yaml"
"github.com/beego/beego/v2/core/logs"
)

var (
ConfigFile = "./app.yaml"
)

func main() {
err := config.InitGlobalInstance("yaml", ConfigFile)
if err != nil {
logs.Critical("An error occurred:", err)
panic(err)
}

val, _ := config.String("name")

logs.Info("load config name is", val)
}

#XML

别忘了匿名引入 XML 的实现!

import (
"github.com/beego/beego/v2/core/config"
// never forget this
_ "github.com/beego/beego/v2/core/config/xml"
"github.com/beego/beego/v2/core/logs"
)

var (
ConfigFile = "./app.xml"
)

func main() {
err := config.InitGlobalInstance("xml", ConfigFile)
if err != nil {
logs.Critical("An error occurred:", err)
panic(err)
}

val, _ := config.String("name")

logs.Info("load config name is", val)
}

要注意,所有的配置项都要放在config这个顶级节点之内:

<?xml version="1.0" encoding="UTF-8" ?>
<config>
<name>beego</name>
</config>

#TOML

别忘了匿名引入 TOML 的实现!

import (
"github.com/beego/beego/v2/core/config"
// never forget this
_ "github.com/beego/beego/v2/core/config/toml"
"github.com/beego/beego/v2/core/logs"
)

var (
ConfigFile = "./app.toml"
)

func main() {
err := config.InitGlobalInstance("toml", ConfigFile)
if err != nil {
logs.Critical("An error occurred:", err)
panic(err)
}

val, _ := config.String("name")

logs.Info("load config name is", val)
}

#Etcd

别忘了匿名引入 ETCD 的实现!

import (
"github.com/beego/beego/v2/core/config"
// never forget this
_ "github.com/beego/beego/v2/core/config/toml"
"github.com/beego/beego/v2/core/logs"
)

func main() {
err := config.InitGlobalInstance("etcd", "your_config")
if err != nil {
logs.Critical("An error occurred:", err)
panic(err)
}

val, _ := config.String("name")

logs.Info("load config name is", val)
}

其中 your_config 是一个 JSON 配置,它对应于:

type Config struct {
// Endpoints is a list of URLs.
Endpoints []string `json:"endpoints"`

// AutoSyncInterval is the interval to update endpoints with its latest members.
// 0 disables auto-sync. By default auto-sync is disabled.
AutoSyncInterval time.Duration `json:"auto-sync-interval"`

// DialTimeout is the timeout for failing to establish a connection.
DialTimeout time.Duration `json:"dial-timeout"`

// DialKeepAliveTime is the time after which client pings the server to see if
// transport is alive.
DialKeepAliveTime time.Duration `json:"dial-keep-alive-time"`

// DialKeepAliveTimeout is the time that the client waits for a response for the
// keep-alive probe. If the response is not received in this time, the connection is closed.
DialKeepAliveTimeout time.Duration `json:"dial-keep-alive-timeout"`

// MaxCallSendMsgSize is the client-side request send limit in bytes.
// If 0, it defaults to 2.0 MiB (2 * 1024 * 1024).
// Make sure that "MaxCallSendMsgSize" < server-side default send/recv limit.
// ("--max-request-bytes" flag to etcd or "embed.Config.MaxRequestBytes").
MaxCallSendMsgSize int

// MaxCallRecvMsgSize is the client-side response receive limit.
// If 0, it defaults to "math.MaxInt32", because range response can
// easily exceed request send limits.
// Make sure that "MaxCallRecvMsgSize" >= server-side default send/recv limit.
// ("--max-request-bytes" flag to etcd or "embed.Config.MaxRequestBytes").
MaxCallRecvMsgSize int

// TLS holds the client secure credentials, if any.
TLS *tls.Config

// Username is a user name for authentication.
Username string `json:"username"`

// Password is a password for authentication.
Password string `json:"password"`

// RejectOldCluster when set will refuse to create a client against an outdated cluster.
RejectOldCluster bool `json:"reject-old-cluster"`

// DialOptions is a list of dial options for the grpc client (e.g., for interceptors).
// For example, pass "grpc.WithBlock()" to block until the underlying connection is up.
// Without this, Dial returns immediately and connecting the server happens in background.
DialOptions []grpc.DialOption

// Context is the default client context; it can be used to cancel grpc dial out and
// other operations that do not have an explicit context.
Context context.Context

// Logger sets client-side logger.
// If nil, fallback to building LogConfig.
Logger *zap.Logger

// LogConfig configures client-side logger.
// If nil, use the default logger.
// TODO: configure gRPC logger
LogConfig *zap.Config

// PermitWithoutStream when set will allow client to send keepalive pings to server without any active streams(RPCs).
PermitWithoutStream bool `json:"permit-without-stream"`

// TODO: support custom balancer picker
}

web模块

路由

所谓的注册控制器风格路由,可以理解为典型的 MVC 风格代码。即我们会在web服务中声明各式各样的Controller

具体Controller里面有什么 API,可以查看Controller API

#基本用法

在 Beego 里面注册这种风格的路由很简单,只需要声明一个Controller就可以:

import "github.com/beego/beego/v2/server/web"

type UserController struct {
web.Controller
}

bee new新创建的项目:

image-20221213150851540

这样我们就写好了一个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的时候,可以看到结果:

 

直接输出字符串

通过beego.Controller.Ctx.WriteString()方法可以直接向http response body中输出字符串

beego中的函数定义如下:

// WriteString Write string to response body.

// it sends response body.


func (ctx *Context) WriteString(content string) {

ctx.ResponseWriter.Write([]byte(content))

}

示例:直接在response body中输出Hello World!

package controllers

import (
"github.com/astaxie/beego"
)

type MainController struct {
beego.Controller
}

func (c *MainController) Get() {
c.Ctx.WriteString("Hello World!")
}

 

需要注意的是,控制器里面处理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()
}

我们不再推荐使用这种方式,因为可读性和可维护性都不太好。特别是重构进行方法重命名的时候,容易出错。

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 提供的方法。而具体输入可以来源于:

  • 路径参数:这一部分主要是指参数路由

  • 查询参数

  • 请求体:要想从请求体里面读取数据,大多数时候将BConfig.CopyRequestBody 设置为true就足够了。而如果你是创建了多个 web.Server,那么必须每一个Server实例里面的配置都将CopyRequestBody设置为true

而获取参数的方法可以分成两大类:

  • 第一类是以 Get 为前缀的方法:这一大类的方法,主要获得某个特定参数的值

  • 第二类是以 Bind 为前缀的方法:这一大类的方法,试图将输入转化为结构体

#Get 类方法

针对这一类方法,Beego 主要从两个地方读取:查询参数和表单,如果两个地方都有相同名字的参数,那么 Beego 会返回表单里面的数据。例如最简单的例子:

type MainController struct {
web.Controller
}

func (ctrl *MainController) Post() {
name := ctrl.GetString("name")
if name == "" {
ctrl.Ctx.WriteString("Hello World")
return
}
ctrl.Ctx.WriteString("Hello " + name)
}

当我们访问:

  • 路径 localhost:8080?name=a: 这是使用查询参数的形式,那么会输出 Hello, a

  • 路径 localhost:8080,而后表单里面提交了name=b,那么会输出b

  • 路径 localhost:8080?name=a,并且表单提交了name=b,那么会输出b

这一类的方法也允许传入默认值,例如:

func (ctrl *MainController) Get() {
name := ctrl.GetString("name", "Tom")
ctrl.Ctx.WriteString("Hello " + name)
}

如果我们没有传入name参数,那么就会使用Tom作为name的值,例如我们访问GET localhost:8080的时候,就会输出 Hello Tom

需要注意的是,GetString的方法签名是:

func (c *Controller) GetString(key string, def ...string) string {
// ...
}

要注意的是,虽然def被声明为不定参数,但是实际上,Beego 只会使用第一个默认值,后面的都会被忽略。

这一类方法签名和行为都是类似的,它们有:

  • GetString(key string, def ...string) string

  • GetStrings(key string, def ...[]string) []string

  • GetInt(key string, def ...int) (int, error)

  • GetInt8(key string, def ...int8) (int8, error)

  • GetUint8(key string, def ...uint8) (uint8, error)

  • GetInt16(key string, def ...int16) (int16, error)

  • GetUint16(key string, def ...uint16) (uint16, error)

  • GetInt32(key string, def ...int32) (int32, error)

  • GetUint32(key string, def ...uint32) (uint32, error)

  • GetInt64(key string, def ...int64) (int64, error)

  • GetUint64(key string, def ...uint64) (uint64, error)

  • GetBool(key string, def ...bool) (bool, error)

  • GetFloat(key string, def ...float64) (float64, error)

这里要注意到,GetStringGetStrings 本身在设计的时候并没有设计返回 error,所以无法拿到错误。

Bind 类方法

大多数时候,我们还需要把输入转换为结构体,Beego 提供了一系列的方法来完成输入到结构体的绑定。

这部分方法是直接定义在 Context 结构体上的,所以用户可以直接操作 Context 实例。为了简化操作,我们在Controller上也定义了类似的方法。

例如:

// 要设置 web.BConfig.CopyRequestBody = true

type MainController struct {
web.Controller
}

func (ctrl *MainController) Post() {
user := User{}
err := ctrl.BindJSON(&user)
if err != nil {
ctrl.Ctx.WriteString(err.Error())
return
}
ctrl.Ctx.WriteString(fmt.Sprintf("%v", user))
}

type User struct {
Age int `json:"age"`
Name string `json:"name"`
}

Bind这一大类有多个方法:

  • Bind(obj interface{}) error: 默认是依据输入的 Content-Type字段,来判断该如何反序列化;

  • BindYAML(obj interface{}) error: 处理YAML输入

  • BindForm(obj interface{}) error: 处理表单输入

  • BindJSON(obj interface{}) error: 处理JSON输入

  • BindProtobuf(obj proto.Message) error: 处理proto输入

  • BindXML(obj interface{}) error: 处理XML输入

在使用特定格式的输入的时候,别忘记设置标签(Tag),例如我们例子里面的json:"age",不同格式的输入,其标签是不是一样的。

需要注意的是,虽然我们提供了一个根据Content-Type来判断如何绑定的,但是我们更加推荐用户使用指定格式的绑定方法。

一个接口,应该只接收特定某种格式的输入,例如只接收 JSON,而不应该可以处理多种输入

在早期,Beego 还有一个类似于BindForm的方法:ParseForm(obj interface{}) error,这两个方法效果是一致的。

#路径参数

我们在路由定义——参数路由里面介绍过了如何获取路径参数。

#早期 Bind 方法

在 Beego 的Input中定义了一系列的方法,用于读取参数。这一类方法很类似于 Get 一族的方法。所以用户可以酌情使用。

例如请求地址如下

?id=123&isok=true&ft=1.2&ol[0]=1&ol[1]=2&ul[]=str&ul[]=array&user.Name=astaxie
var id int
this.Ctx.Input.Bind(&id, "id") //id ==123

var isok bool
this.Ctx.Input.Bind(&isok, "isok") //isok ==true

var ft float64
this.Ctx.Input.Bind(&ft, "ft") //ft ==1.2

ol := make([]int, 0, 2)
this.Ctx.Input.Bind(&ol, "ol") //ol ==[1 2]

ul := make([]string, 0, 2)
this.Ctx.Input.Bind(&ul, "ul") //ul ==[str array]

user struct{Name}
this.Ctx.Input.Bind(&user, "user") //user =={Name:"astaxie"}

 

Web 文件上传下载

#文件上传

在 Beego 中你可以很容易的处理文件上传,就是别忘记在你的表单中增加这个属性 enctype="multipart/form-data",否则你的浏览器不会传输你的上传文件。

文件上传之后一般是放在系统的内存里面,如果文件的 size 大于设置的缓存内存大小,那么就放在临时文件中,默认的缓存内存是 64M,你可以通过如下来调整这个缓存内存大小:

web.MaxMemory = 1<<22

或者在配置文件中通过如下设置:

maxmemory = 1<<22

与此同时,Beego 提供了另外一个参数,MaxUploadSize来限制最大上传文件大小——如果你一次长传多个文件,那么它限制的就是这些所有文件合并在一起的大小。

默认情况下,MaxMemory应该设置得比MaxUploadSize小,这种情况下两个参数合并在一起的效果则是:

  1. 如果文件大小小于MaxMemory,则直接在内存中处理;

  2. 如果文件大小介于MaxMemoryMaxUploadSize之间,那么比MaxMemory大的部分将会放在临时目录;

  3. 文件大小超出MaxUploadSize,直接拒绝请求,返回响应码 413

Beego 提供了两个很方便的方法来处理文件上传:

  • GetFile(key string) (multipart.File, *multipart.FileHeader, error):该方法主要用于用户读取表单中的文件名 the_file,然后返回相应的信息,用户根据这些变量来处理文件上传、过滤、保存文件等。

  • SaveToFile(fromfile, tofile string) error:该方法是在 GetFile 的基础上实现了快速保存的功能。fromfile是提交时候表单中的name

<form enctype="multipart/form-data" method="post">
<input type="file" name="uploadname" />
<input type="submit" />
</form>

保存的代码例子如下:

func (c *FormController) Post() {
f, h, err := c.GetFile("uploadname")
if err != nil {
log.Fatal("getfile err ", err)
}
defer f.Close()
c.SaveToFile("uploadname", "static/upload/" + h.Filename) // 保存位置在 static/upload, 没有文件夹要先创建
}

#文件下载

Beego 直接提供了一个下载文件的方法Download

func (output *BeegoOutput) Download(file string, filename ...string) {}

使用也很简单:

func (ctrl *MainController) DownloadFile() {
// The file LICENSE is under root path.
// and the downloaded file name is license.txt
ctrl.Ctx.Output.Download("LICENSE", "license.txt")
}

尤其要注意的是,Download方法的第一个参数,是文件路径,也就是要下载的文件;第二个参数是不定参数,代表的是用户保存到本地时候的文件名。

如果第一个参数使用的是相对路径,那么它代表的是从当前工作目录开始计算的相对路径。

 

Session

beego 内置了 session 模块,目前 session 模块支持的后端引擎包括 memorycookiefilemysqlrediscouchbasememcachepostgres,用户也可以根据相应的接口实现自己的引擎。

#在 Web 中使用 Session

web模块中使用 session 相当方便,只要在 main 入口函数中设置如下:

web.BConfig.WebConfig.Session.SessionOn = true

或者通过配置文件配置如下:

sessionon = true

通过这种方式就可以开启 session,如何使用 session,请看下面的例子:

func (this *MainController) Get() {
v := this.GetSession("asta")
if v == nil {
this.SetSession("asta", int(1))
this.Data["num"] = 0
} else {
this.SetSession("asta", v.(int)+1)
this.Data["num"] = v.(int)
}
this.TplName = "index.tpl"
}

session 有几个方便的方法:

  • SetSession(name string, value interface{})

  • GetSession(name string) interface{}

  • DelSession(name string)

  • SessionRegenerateID()

  • DestroySession()

session 操作主要有设置 session、获取 session,删除 session

当然你可以通过下面的方式自己控制这些逻辑:

sess:=this.StartSession()
defer sess.SessionRelease()

sess 对象具有如下方法:

  • sess.Set()

  • sess.Get()

  • sess.Delete()

  • sess.SessionID()

  • sess.Flush()

但是我还是建议大家采用 SetSession、GetSession、DelSession 三个方法来操作,避免自己在操作的过程中资源没释放的问题。

关于 Session 模块使用中的一些参数设置:

  • web.BConfig.WebConfig.Session.SessionOn: 设置是否开启 Session,默认是 false,配置文件对应的参数名:sessionon

  • web.BConfig.WebConfig.Session.SessionProvider: 设置 Session 的引擎,默认是 memory,目前支持还有 filemysqlredis 等,配置文件对应的参数名:sessionprovider

  • web.BConfig.WebConfig.Session.SessionName: 设置 cookies 的名字,Session 默认是保存在用户的浏览器 cookies 里面的,默认名是 beegosessionID,配置文件对应的参数名是:sessionname

  • web.BConfig.WebConfig.Session.SessionGCMaxLifetime: 设置 Session 过期的时间,默认值是 3600 秒,配置文件对应的参数:sessiongcmaxlifetime

  • web.BConfig.WebConfig.Session.SessionProviderConfig: 设置对应 filemysqlredis 引擎的保存路径或者链接地址,默认值是空,配置文件对应的参数:sessionproviderconfig

  • web.BConfig.WebConfig.Session.SessionHashFunc: 默认值为 sha1,采用 sha1 加密算法生产 sessionid

  • web.BConfig.WebConfig.Session.SessionCookieLifeTime: 设置 cookie 的过期时间,cookie 是用来存储保存在客户端的数据。

在使用某种特定引擎的时候,需要匿名引入该引擎对应的包,以完成初始化工作:

import _ "github.com/beego/beego/v2/server/web/session/mysql"

#不同引擎的初始化工作

#File

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

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

#MySQL

SessionProvidermysql 时,SessionProviderConfig 是链接地址,采用 go-sql-driver (opens new window),如下所示:

web.BConfig.WebConfig.Session.SessionProvider = "mysql"
web.BConfig.WebConfig.Session.SessionProviderConfig = "username:password@protocol(address)/dbname?param=value"

需要特别注意的是,在使用 mysql 存储 session 信息的时候,需要事先在 mysql 创建表,建表语句如下

CREATE TABLE `session` (
`session_key` char(64) NOT NULL,
`session_data` blob,
`session_expiry` int(11) unsigned NOT NULL,
PRIMARY KEY (`session_key`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

#Redis

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

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

#memcache

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

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

#Postgress

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

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

#Couchbase

SessionProvidercouchbase 时,SessionProviderConfigcouchbase 的链接地址,采用了 couchbase (opens new window),如下所示:

web.BConfig.WebConfig.Session.SessionProvider = "couchbase"
web.BConfig.WebConfig.Session.SessionProviderConfig = "http://bucketname:bucketpass@myserver:8091"

#特别注意点

因为 session 内部采用了 gob 来注册存储的对象,例如 struct,所以如果你采用了非 memory 的引擎,请自己在 main.goinit 里面注册需要保存的这些结构体,不然会引起应用重启之后出现无法解析的错误

#单独使用 Session 模块

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

首先你必须导入包:

import (
"github.com/beego/beego/v2/server/web/session"
)

然后你初始化一个全局的变量用来存储 session 控制器:

var globalSessions *session.Manager

接着在你的入口函数中初始化数据:

func init() {
sessionConfig := &session.ManagerConfig{
CookieName:"gosessionid",
EnableSetCookie: true,
Gclifetime:3600,
Maxlifetime: 3600,
Secure: false,
CookieLifeTime: 3600,
ProviderConfig: "./tmp",
}
globalSessions, _ = session.NewManager("memory",sessionConfig)
go globalSessions.GC()
}

NewManager函数的参数的函数如下所示

  1. 引擎名字,可以是memoryfileMySQLRedis

  2. 一个

    JSON

    字符串,传入

    Manager

    的配置信息

    • cookieName: 客户端存储cookie的名字。

    • enableSetCookie, omitempty: 是否开启 SetCookie, omitempty这个设置

    • gclifetime: 触发 GC 的时间。

    • maxLifetime: 服务器端存储的数据的过期时间

    • secure: 是否开启HTTPS,在cookie中设置cookie.Secure

    • sessionIDHashFunc: sessionID生产的函数,默认是sha1算法。

    • sessionIDHashKey: hash算法中的key

    • cookieLifeTime: 客户端存储的 cookie 的时间,默认值是 0,即浏览器生命周期。

    • providerConfig: 配置信息,根据不同的引擎设置不同的配置信息,详细的配置请看下面的引擎设置

最后我们的业务逻辑处理函数中可以这样调用:

func login(w http.ResponseWriter, r *http.Request) {
sess, _ := globalSessions.SessionStart(w, r)
defer sess.SessionRelease(w)
username := sess.Get("username")
if r.Method == "GET" {
t, _ := template.ParseFiles("login.gtpl")
t.Execute(w, nil)
} else {
sess.Set("username", r.Form["username"])
}
}

globalSessions 有多个函数如下所示:

  • SessionStart 根据当前请求返回 session 对象

  • SessionDestroy 销毁当前 session 对象

  • SessionRegenerateId 重新生成 sessionID

  • GetActiveSession 获取当前活跃的 session 用户

  • SetHashFunc 设置 sessionID 生成的函数

  • SetSecure 设置是否开启 cookieSecure 设置

返回的 session 对象是一个 Interface,包含下面的方法

  • Set(key, value interface{}) error

  • Get(key interface{}) interface{}

  • Delete(key interface{}) error

  • SessionID() string

  • SessionRelease()

  • Flush() error

#引擎设置

上面已经展示了 memory 的设置,接下来我们看一下其他三种引擎的设置方式:

  • mysql: 其他参数一样,只是第四个参数配置设置如下所示,详细的配置请参考 mysql (opens new window)

    username:password@protocol(address)/dbname?param=value
  • Redis: 配置文件信息如下所示,表示链接的地址,连接池,访问密码,没有保持为空:

    注意:若使用 Redis 等引擎作为 session backend,请在使用前导入 < _ "github.com/beego/beego/v2/server/web/session/redis" >

        否则会在运行时发生错误,使用其他引擎时也是同理。
      127.0.0.1:6379,100,astaxie
  • file: 配置文件如下所示,表示需要保存的目录,默认是两级目录新建文件,例如 sessionIDxsnkjklkjjkh27hjh78908,那么目录文件应该是 ./tmp/x/s/xsnkjklkjjkh27hjh78908

    ./tmp

#如何创建自己的引擎

在开发应用中,你可能需要实现自己的 session 引擎,例如 memcache 的引擎。

// Store contains all data for one session process with specific id.
type Store interface {
Set(ctx context.Context, key, value interface{}) error //set session value
Get(ctx context.Context, key interface{}) interface{} //get session value
Delete(ctx context.Context, key interface{}) error //delete session value
SessionID(ctx context.Context) string //back current sessionID
SessionRelease(ctx context.Context, w http.ResponseWriter) // release the resource & save data to provider & return the data
Flush(ctx context.Context) error //delete all data
}

// Provider contains global session methods and saved SessionStores.
// it can operate a SessionStore by its id.
type Provider interface {
SessionInit(ctx context.Context, gclifetime int64, config string) error
SessionRead(ctx context.Context, sid string) (Store, error)
SessionExist(ctx context.Context, sid string) (bool, error)
SessionRegenerate(ctx context.Context, oldsid, sid string) (Store, error)
SessionDestroy(ctx context.Context, sid string) error
SessionAll(ctx context.Context) int // get all active session
SessionGC(ctx context.Context)
}

最后需要注册自己写的引擎:

func init() {
Register("own", ownadaper)
}

Cookie 处理

这部分的例子在Cookie example(opens new window)

#普通 Cookie 处理

Beego 通过Context直接封装了对普通 Cookie 的处理方法,可以直接使用:

  • GetCookie(key string)

  • SetCookie(name string, value string, others ...interface{})

例子:


type MainController struct {
web.Controller
}

func (ctrl *MainController) PutCookie() {
// put something into cookie,set Expires time
ctrl.Ctx.SetCookie("name", "web cookie", 10)

// web-example/views/hello_world.html
ctrl.TplName = "hello_world.html"
ctrl.Data["name"] = "PutCookie"
_ = ctrl.Render()
}

func (ctrl *MainController) ReadCookie() {
// web-example/views/hello_world.html
ctrl.TplName = "hello_world.html"
ctrl.Data["name"] = ctrl.Ctx.GetCookie("name")
// don't forget this
_ = ctrl.Render()
}

others参数含义依次是:

  • 第一个代表 maxAge,Beego 使用这个值计算ExpiresMax-Age两个值

  • 第二个代表Path,字符串类型,默认值是/

  • 第三个代表Domain,字符串类型

  • 第四个代表Secure,布尔类型

  • 第五个代表HttpOnly,布尔类型

  • 第六个代表SameSite,字符串类型

#加密 Cookie 处理

Beego 提供了两个方法用于辅助 Cookie 加密处理,它采用了sha256来作为加密算法,下面Secret则是加密的密钥:

  • GetSecureCookie(Secret, key string) (string, bool):用于从 Cookie 中读取数据

  • SetSecureCookie(Secret, name, value string, others ...interface{}):用于写入数据到 Cookie。

type MainController struct {
web.Controller
}

func (ctrl *MainController) PutSecureCookie() {
// put something into cookie,set Expires time
ctrl.Ctx.SetSecureCookie("my-secret", "name", "web cookie")

// web-example/views/hello_world.html
ctrl.TplName = "hello_world.html"
ctrl.Data["name"] = "PutCookie"
_ = ctrl.Render()
}

func (ctrl *MainController) ReadSecureCookie() {
// web-example/views/hello_world.html
ctrl.TplName = "hello_world.html"
ctrl.Data["name"], _ = ctrl.Ctx.GetSecureCookie("my-secret", "name")
// don't forget this
_ = ctrl.Render()
}

others参数和普通 Cookie 一样。

 

错误处理

我们在做 Web 开发的时候,经常需要页面跳转和错误处理,Beego 这方面也进行了考虑,通过 Redirect 方法来进行跳转:

func (this *AddController) Get() {
this.Redirect("/", 302)
}

如何中止此次请求并抛出异常,Beego 可以在控制器中这样操作:

func (this *MainController) Get() {
this.Abort("401")
v := this.GetSession("asta")
if v == nil {
this.SetSession("asta", int(1))
this.Data["Email"] = 0
} else {
this.SetSession("asta", v.(int)+1)
this.Data["Email"] = v.(int)
}
this.TplName = "index.tpl"
}

这样 this.Abort("401") 之后的代码不会再执行,而且会默认显示给用户如下页面:

img

web 框架默认支持 401、403、404、500、503 这几种错误的处理。用户可以自定义相应的错误处理,例如下面重新定义 404 页面:

func page_not_found(rw http.ResponseWriter, r *http.Request){
t,_:= template.New("404.html").ParseFiles(web.BConfig.WebConfig.ViewsPath+"/404.html")
data :=make(map[string]interface{})
data["content"] = "page not found"
t.Execute(rw, data)
}

func main() {
web.ErrorHandler("404",page_not_found)
web.Router("/", &controllers.MainController{})
web.Run()
}

我们可以通过自定义错误页面 404.html 来处理 404 错误。

Beego 更加人性化的还有一个设计就是支持用户自定义字符串错误类型处理函数,例如下面的代码,用户注册了一个数据库出错的处理页面:

func dbError(rw http.ResponseWriter, r *http.Request){
t,_:= template.New("dberror.html").ParseFiles(web.BConfig.WebConfig.ViewsPath+"/dberror.html")
data :=make(map[string]interface{})
data["content"] = "database is now down"
t.Execute(rw, data)
}

func main() {
web.ErrorHandler("dbError",dbError)
web.Router("/", &controllers.MainController{})
web.Run()
}

一旦在入口注册该错误处理代码,那么你可以在任何你的逻辑中遇到数据库错误调用 this.Abort("dbError") 来进行异常页面处理。

#Controller 定义 Error

从 1.4.3 版本开始,支持 Controller 方式定义 Error 错误处理函数,这样就可以充分利用系统自带的模板处理,以及 context 等方法。

package controllers

import (
"github.com/beego/beego/v2/server/web"
)

type ErrorController struct {
web.Controller
}

func (c *ErrorController) Error404() {
c.Data["content"] = "page not found"
c.TplName = "404.tpl"
}

func (c *ErrorController) Error501() {
c.Data["content"] = "server error"
c.TplName = "501.tpl"
}


func (c *ErrorController) ErrorDb() {
c.Data["content"] = "database is now down"
c.TplName = "dberror.tpl"
}

通过上面的例子我们可以看到,所有的函数都是有一定规律的,都是 Error 开头,后面的名字就是我们调用 Abort 的名字,例如 Error404 函数其实调用对应的就是 Abort("404")

我们就只要在 web.Run 之前采用 web.ErrorController 注册这个错误处理函数就可以了

package main

import (
_ "btest/routers"
"btest/controllers"

"github.com/Beego/Beego/v2/server/web"
)

func main() {
web.ErrorController(&controllers.ErrorController{})
web.Run()
}

#从 panic 中恢复

如果你希望用户在服务器处理请求过程中,即便发生了 panic 依旧能够返回响应,那么可以使用 Beego 的恢复机制。该机制是默认开启的。依赖于配置项:

web.BConfig.RecoverPanic = true

如果你需要关闭,那么将这个配置项设置为false就可以。

如果你想自定义panic之后的处理行为,那么可以重新设置web.BConfig.RecoverFunc

例如:

	web.BConfig.RecoverFunc = func(context *context.Context, config *web.Config) {
if err := recover(); err != nil {
context.WriteString(fmt.Sprintf("you panic, err: %v", err))
}
}

千万要注意:你永远需要检测recover的结果,并且将从panic中恢复过来的逻辑放在检测到recover返回不为nil的代码里面。

 

Admin 管理后台

默认 Admin 是关闭的,你可以通过配置开启监控:

web.BConfig.Listen.EnableAdmin = true

而且你还可以修改监听的地址和端口:

web.BConfig.Listen.AdminAddr = "localhost"
web.BConfig.Listen.AdminPort = 8088

打开浏览器,输入 URL:http://localhost:8088/,你会看到一句欢迎词:Welcome to Admin Dashboard

#请求统计信息

访问统计的 URL 地址 http://localhost:8088/qps,展现如下所示:

img

#性能调试

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

#健康检查

需要手工注册相应的健康检查逻辑,才能通过 URLhttp://localhost:8088/healthcheck 获取当前执行的健康检查的状态。

#定时任务

用户需要在应用中添加了 定时任务,才能执行相应的任务检查和手工触发任务。

  • 检查任务状态 URL:http://localhost:8088/task

  • 手工执行任务 URL:http://localhost:8088/task?taskname=任务名

#配置信息

应用开发完毕之后,我们可能需要知道在运行的进程到底是怎么样的配置,beego 的监控模块提供了这一功能。

  • 显示所有的配置信息: http://localhost:8088/listconf?command=conf

  • 显示所有的路由配置信息: http://localhost:8088/listconf?command=router

  • 显示所有的过滤设置信息: http://localhost:8088/listconf?command=filter

 

跨站请求伪造

跨站请求伪造(Cross-site request forgery) (opens new window), 简称为 XSRF,是 Web 应用中常见的一个安全问题。前面的链接也详细讲述了 XSRF 攻击的实现方式。

当前防范 XSRF 的一种通用的方法,是对每一个用户都记录一个无法预知的 cookie 数据,然后要求所有提交的请求(POST/PUT/DELETE)中都必须带有这个 cookie 数据。如果此数据不匹配 ,那么这个请求就可能是被伪造的。

Beego 有内建的 XSRF 的防范机制,要使用此机制,你需要在应用配置文件中加上 enablexsrf 设定:

enablexsrf = true
xsrfkey = 61oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o
xsrfexpire = 3600

或者直接在 main 入口处这样设置:

  web.EnableXSRF = true
web.XSRFKEY = "61oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o"
web.XSRFExpire = 3600 //过期时间,默认1小时

如果开启了 XSRF,那么 Beego 的 Web 应用将对所有用户设置一个 _xsrf 的 Cookie 值(默认过期 1 小时),如果 POST PUT DELET 请求中没有这个 Cookie 值,那么这个请求会被直接拒绝。

Beego 使用了 SecureHTTP-ONLY 两个选项来保存 Cookie。因此在大部分情况下,这意味这你需要使用 HTTPS 协议,并且将无法在 JS 里面访问到 Cookie 的值。

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

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

  • 在表单里面携带一个叫做 _xsrf 的字段,里面是 XSRF 的 Token;

  • 在提交的请求的 HTTP HEADER 里面设置 X-XsrftokenX-Csrftoken,值就是 Token;

下面是使用这两种方式的简单例子

#表单中携带 Token

最简单的做法,是利用 Beego 的方法,在表单中加入一个字段,将 XSRF Token 带回来,例如:

func (mc *MainController) XsrfPage() {
mc.XSRFExpire = 7200
mc.Data["xsrfdata"] = template.HTML(mc.XSRFFormHTML())
mc.TplName = "xsrf.html"
}

其中xsrf.html的核心代码是:

<form action="/new_message" method="post">
{{ .xsrfdata }}
<input type="text" name="message" />
<input type="submit" value="Post" />
</form>

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

#页面设置 meta

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

需要你在 HTML 里保存一个 _xsrf

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

放在你的页面 HEAD 中

<head>
<meta name="_xsrf" content="{{.xsrf_token}}" />
</head>

扩展 ajax 方法,将 _xsrf 值加入 header,扩展后支持 jquery post/get 等内部使用了 ajax 的方法

var ajax = $.ajax;
$.extend({
ajax: function (url, options) {
if (typeof url === "object") {
options = url;
url = undefined;
}
options = options || {};
url = options.url;
var xsrftoken = $("meta[name=_xsrf]").attr("content");
var headers = options.headers || {};
var domain = document.domain.replace(/\./gi, "\\.");
if (
!/^(http:|https:).*/.test(url) ||
eval("/^(http:|https:)\\/\\/(.+\\.)*" + domain + ".*/").test(url)
) {
headers = $.extend(headers, { "X-Xsrftoken": xsrftoken });
}
options.headers = headers;
return ajax(url, options);
},
});

注意的是,这里你可以将ajax或者JQuery替换为你自己的前端框架,因为核心在于要设置头部headers, {'X-Xsrftoken':xsrftoken}

而这个xsrftoken可以是存在 HTML 的一个标签里面,也可是直接从之前响应里面读取出来,而后再提交表单的时候带过来。例如:

func (mc *MainController) XsrfJSON() {
mc.XSRFExpire = 7200
type data struct {
XsrfToken string `json:"xsrfToken"`
}
// 提交请求的时候用前端 JS 操作将这个 xsrfToken 再次带回来
_ = mc.JSONResp(&data{XsrfToken: mc.XSRFToken()})
}

#Controller 级别的 XSRF 屏蔽

XSRF 之前是全局设置的一个参数,如果设置了那么所有的 API 请求都会进行验证,但是有些时候 API 逻辑是不需要进行验证的,因此现在支持在 Controller 级别设置屏蔽:

type AdminController struct{
web.Controller
}

func (a *AdminController) Prepare() {
a.EnableXSRF = false
}

其中Prepare方法是 Controller 的一个钩子方法,详情参考Controller API-钩子方法

同样地,过期时间上面我们设置了全局的过期时间 web.XSRFExpire,但是有些时候我们也可以在控制器中修改这个过期时间,专门针对某一类处理逻辑:

func (this *HomeController) Get(){
this.XSRFExpire = 7200
// ...
}

 

模板引擎

Beego 的模板处理引擎采用的是 Go 内置的 html/template 包进行处理,而且 Beego 的模板处理逻辑是采用了缓存编译方式,也就是所有的模板会在 Beego 应用启动的时候全部编译然后缓存在 map 里面。

#模板目录

Beego 中默认的模板目录是 views,用户可以把模板文件放到该目录下,Beego 会自动在该目录下的所有模板文件进行解析并缓存,开发模式下每次都会重新解析,不做缓存。当然,用户也可以通过如下的方式改变模板的目录(只能指定一个目录为模板目录):

web.ViewsPath = "myviewpath"

#自动渲染

用户无需手动的调用渲染输出模板,Beego 会自动的在调用完相应的 method 方法之后调用 Render 函数,当然如果您的应用是不需要模板输出的,那么可以在配置文件或者在 main.go 中设置关闭自动渲染。

配置文件配置如下:

autorender = false

或者在程序里面设置如下:

web.AutoRender = false

#模板标签

Go 语言的默认模板采用了 {{}} 作为左右标签,但是我们有时候在开发中可能界面是采用了 AngularJS 开发,他的模板也是这个标签,故而引起了冲突。在 Beego 中你可以通过配置文件或者直接设置配置变量修改:

web.TemplateLeft = "<<<"
web.TemplateRight = ">>>"

#模板数据

模板中的数据是通过在 Controller 中 this.Data 获取的,所以如果你想在模板中获取内容,那么你需要在 Controller 中如下设置:

this.Data["Content"] = "value"

对应的 HTML 内容是:

{{ .Content }}

如何使用各种类型的数据渲染:

  • 结构体:结构体定义

    type A struct{
    Name string
    Age int
    }

    控制器数据赋值

      this.Data["a"]=&A{Name:"astaxie",Age:25}

    模板渲染数据如下:

    the username is {{.a.Name}} the age is {{.a.Age}}
  • map 控制器数据赋值

    mp["name"]="astaxie"
    mp["nickname"] = "haha"
    this.Data["m"]=mp

    模板渲染数据如下:

    the username is {{.m.name}} the username is {{.m.nickname}}
  • slice

    控制器数据赋值

    ss :=[]string{"a","b","c"}
    this.Data["s"]=ss

    模板渲染数据如下:

    {{range $key, $val := .s}} {{$key}} {{$val}} {{end}}

总结下来,可以看到.定位到了 Go 程序 Data 字段。而后 .xxx 则是定位到了 Data 包含的元素。

#模板名称

Beego 采用了 Go 语言内置的模板引擎,所有模板的语法和 Go 的一模一样,至于如何写模板文件,详细的请参考 模板教程 (opens new window)

用户通过在 Controller 的对应方法中设置相应的模板名称,Beego 会自动的在 viewpath 目录下查询该文件并渲染,例如下面的设置,Beego 会在 admin 下面找 add.tpl 文件进行渲染:

this.TplName = "admin/add.tpl"

我们看到上面的模板后缀名是tpl,Beego 默认情况下支持 tplhtml 后缀名的模板文件,如果你的后缀名不是这两种,请进行如下设置:

web.AddTemplateExt("你文件的后缀名")

当你设置了自动渲染,然后在你的 Controller 中没有设置任何的 TplName,那么 Beego 会自动设置你的模板文件如下:

c.TplName = strings.ToLower(c.controllerName) + "/" + strings.ToLower(c.actionName) + "." + c.TplExt

也就是你对应的 Controller 名字+请求方法名.模板后缀,也就是如果你的 Controller 名是 AddController,请求方法是 POST,默认的文件后缀是 tpl,那么就会默认请求 /viewpath/AddController/post.tpl 文件。

#Layout 设计

Beego 支持 layout 设计,例如你在管理系统中,整个管理界面是固定的,只会变化中间的部分,那么你可以通过如下的设置:

this.Layout = "admin/layout.html"
this.TplName = "admin/add.tpl"

layout.html 中你必须设置如下的变量:

{{.LayoutContent}}

Beego 就会首先解析 TplName 指定的文件,获取内容赋值给 LayoutContent,然后最后渲染 layout.html 文件。

目前采用首先把目录下所有的文件进行缓存,所以用户还可以通过类似这样的方式实现 layout

{{template "header.html" .}} Logic code {{template "footer.html" .}}

特别注意后面的.,这是传递当前参数到子模板

#LayoutSection

对于一个复杂的 LayoutContent,其中可能包括有 javascript 脚本、CSS 引用等,根据惯例,通常 css 会放到 Head 元素中,javascript 脚本需要放到 body 元素的末尾,而其它内容则根据需要放在合适的位置。在 Layout 页中仅有一个 LayoutContent 是不够的。所以在 Controller 中增加了一个 LayoutSections属性,可以允许 Layout 页中设置多个 section,然后每个 section 可以分别包含各自的子模板页。

layout_blog.tpl:

<!DOCTYPE html>
<html>
<head>
<title>Lin Li</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link
rel="stylesheet"
href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css"
/>
<link
rel="stylesheet"
href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap-theme.min.css"
/>
{{.HtmlHead}}
</head>
<body>
<div class="container">{{.LayoutContent}}</div>
<div>{{.SideBar}}</div>
<script
type="text/javascript"
src="http://code.jquery.com/jquery-2.0.3.min.js"
></script>
<script src="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/js/bootstrap.min.js"></script>
{{.Scripts}}
</body>
</html>

html_head.tpl:

<style>
h1 {
color: red;
}
</style>

scripts.tpl

<script type="text/javascript">
$(document).ready(function () {
// bla bla bla
});
</script>

逻辑处理如下所示:

type BlogsController struct {
web.Controller
}

func (this *BlogsController) Get() {
this.Layout = "layout_blog.tpl"
this.TplName = "blogs/index.tpl"
this.LayoutSections = make(map[string]string)
this.LayoutSections["HtmlHead"] = "blogs/html_head.tpl"
this.LayoutSections["Scripts"] = "blogs/scripts.tpl"
this.LayoutSections["Sidebar"] = ""
}

#renderform 使用

定义 struct:

type User struct {
Id int `form:"-"`
Name interface{} `form:"username"`
Age int `form:"age,text,年龄:"`
Sex string
Intro string `form:",textarea"`
}
  • StructTag 的定义用的标签用为 form,和 ParseForm 方法 共用一个标签,标签后面有三个可选参数,用 , 分割。第一个参数为表单中类型的 name 的值,如果为空,则以 struct field name 为值。第二个参数为表单组件的类型,如果为空,则为 text。表单组件的标签默认为 struct field name 的值,否则为第三个值。

  • 如果 form 标签只有一个值,则为表单中类型 name 的值,除了最后一个值可以忽略外,其他位置的必须要有 , 号分割,如:form:",,姓名:"

  • 如果要忽略一个字段,有两种办法,一是:字段名小写开头,二是:form 标签的值设置为 -

  • 现在的代码版本只能实现固定的格式,用 br 标签实现换行,无法实现 css 和 class 等代码的插入。所以,要实现 form 的高级排版,不能使用 renderform 的方法,而需要手动处理每一个字段。

controller:

func (this *AddController) Get() {
this.Data["Form"] = &User{}
this.TplName = "index.tpl"
}

Form 的参数必须是一个 struct 的指针。

template:

<form action="" method="post">{{.Form | renderform}}</form>

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

	Name: <input name="username" type="text" value="test"></br>
年龄:<input name="age" type="text" value="0"></br>
Sex: <input name="Sex" type="text" value=""></br>
Intro: <input name="Intro" type="textarea" value="">

 

ORM

Beego 的 ORM 被设计成为两种:

  • 普通的 Orm 实例:这种实例是无状态的,因此你应该尽可能保持一个数据库只有一个实例。当然,即便每次你都创建新的实例,问题也不大,只是没有必要而已;

  • TxOrm:这是启动事务之后得到的Orm对象,它只能被用于事务内,提交或者回滚之后就要丢弃,不能复用。每一个事务都需要创建一个新的实例;

#快速开始

这是一个最简单的 ORM 例子:

import (
"github.com/beego/beego/v2/client/orm"
// don't forget this
_ "github.com/go-sql-driver/mysql"
)

// User -
type User struct {
ID int `orm:"column(id)"`
Name string `orm:"column(name)"`
}

func init() {
// need to register models in init
orm.RegisterModel(new(User))

// need to register default database
orm.RegisterDataBase("default", "mysql", "root:123456@tcp(127.0.0.1:3306)/beego?charset=utf8")
}

func main() {
// automatically build table
orm.RunSyncdb("default", false, true)

// create orm object
o := orm.NewOrm()

// data
user := new(User)
user.Name = "mike"

// insert data
o.Insert(user)
}

总体来说,可以分成以下几步:

需要注意的是,一定要根据自己使用的数据库来匿名引入驱动,例如引入 "github.com/go-sql-driver/mysql"

#调试查询日志

在开发环境下,可以设置输出 DEBUG 的 SQL 语句:

func main() {
orm.Debug = true

开启后将会输出所有查询语句,包括执行、准备、事务等。注意,在生产环境不应该开启这个选项,因为输出日志会严重影响性能。

 

数据库设置与注册

Beego ORM 要求显式注册数据库的信息,而后才可以自由使用。

当然,永远不要忘了匿名引入驱动:

import (
_ "github.com/go-sql-driver/mysql"
_ "github.com/lib/pq"
_ "github.com/mattn/go-sqlite3"
)

上面三种,你根据自己需要引入一种就可以。

最简单的例子:

// 参数1        数据库的别名,用来在 ORM 中切换数据库使用
// 参数2 driverName
// 参数3 对应的链接字符串
orm.RegisterDataBase("default", "mysql", "root:root@/orm_test?charset=utf8")

// 参数4(可选) 设置最大空闲连接
// 参数5(可选) 设置最大数据库连接 (go >= 1.2)
maxIdle := 30
maxConn := 30
orm.RegisterDataBase("default", "mysql", "root:root@/orm_test?charset=utf8", orm.MaxIdleConnections(maxIdle), orm.MaxOpenConnections(maxConn))

ORM 要求必须要注册一个default的数据库。并且,Beego 的 ORM 并没有自己管理连接,而是直接依赖于驱动。

#数据库设置

#最大连接数

最大连接数的设置有两种方式,一种方式是在注册数据库的时候,使用MaxOpenConnections 选项:

orm.RegisterDataBase("default", "mysql", "root:root@/orm_test?charset=utf8", orm.MaxOpenConnections(100))

也可以在注册之后修改:

orm.SetMaxOpenConns("default", 30)

#最大空闲连接数

最大空闲连接数的设置有两种方式,一种方式是在注册数据库的时候,使用MaxIdleConnections选项:

orm.RegisterDataBase("default", "mysql", "root:root@/orm_test?charset=utf8", orm.MaxIdleConnections(20))

#时区

ORM 默认使用 time.Local 本地时区

  • 作用于 ORM 自动创建的时间

  • 从数据库中取回的时间转换成 ORM 本地时间

如果需要的话,你也可以进行更改

// 设置为 UTC 时间
orm.DefaultTimeLoc = time.UTC

ORM 在进行 RegisterDataBase 的同时,会获取数据库使用的时区,然后在 time.Time 类型存取时做相应转换,以匹配时间系统,从而保证时间不会出错。

注意:

  • 鉴于 Sqlite3 的设计,存取默认都为 UTC 时间

  • 使用 go-sql-driver 驱动时,请注意参数设置 从某一版本开始,驱动默认使用 UTC 时间,而非本地时间,所以请指定时区参数或者全部以 UTC 时间存取 例如:root:root@/orm_test?charset=utf8&loc=Asia%2FShanghai 参见 loc (opens new window)/ parseTime(opens new window)

#注册驱动

大多数时候,你只需要使用默认的那些驱动,有:

	DRMySQL                      // mysql
DRSqlite // sqlite
DROracle // oracle
DRPostgres // pgsql
DRTiDB // TiDB

如果你需要注册自定义的驱动,可以使用:

// 参数1   driverName
// 参数2 数据库类型
// 这个用来设置 driverName 对应的数据库类型
// mysql / sqlite3 / postgres / tidb 这几种是默认已经注册过的,所以可以无需设置
orm.RegisterDriver("mysql", yourDriver)

 

ORM 模型

Beego 的 ORM 模块要求在使用之前要先注册好模型,并且 Beego 会执行一定的校验,用于辅助检查模型和模型之间的约束。并且模型定义也会影响自动建表功能自动建表

Beego 的模型定义,大部分都是依赖于 Go 标签特性,可以设置多个特性,用;分隔。同一个特性的不同值使用,来分隔。

例如:

orm:"null;rel(fk)"

#注册模型

注册模型有三个方法:

  • RegisterModel(models ...interface{})

  • RegisterModelWithPrefix(prefix string, models ...interface{}):该方法会为表名加上前缀,例如RegisterModelWithPrefix("tab_", &User{}),那么表名是tab_user

  • RegisterModelWithSuffix(suffix string, models ...interface{}):该方法会为表名加上后缀,例如RegisterModelWithSuffix("_tab", &User{}),那么表名是user_tab

#模型基本设置

#表名

默认的表名规则,使用驼峰转蛇形:

AuthUser -> auth_user
Auth_User -> auth__user
DB_AuthUser -> d_b__auth_user

除了开头的大写字母以外,遇到大写会增加 _,原名称中的下划线保留。

也可以自定义表名,只需要实现接口TableNameI:

type User struct {
Id int
Name string
}

func (u *User) TableName() string {
return "auth_user"
}

同时,也可以在注册模型的时候为表名加上前缀或者后缀,参考注册模型一节。

#

为字段设置 DB 列的名称

Name string `orm:"column(user_name)"`

#忽略字段

设置 - 即可忽略模型中的字段

type User struct {
// ...
AnyField string `orm:"-"`
//...
}

#索引

默认情况下,可以在字段定义里面使用 Go 的标签功能指定索引,包括指定唯一索引。

例如,为单个字段增加索引:

Name string `orm:"index"`

或者,为单个字段增加 unique 键

Name string `orm:"unique"`

实现接口TableIndexI,可以为单个或多个字段增加索引:

type User struct {
Id int
Name string
Email string
}

// 多字段索引
func (u *User) TableIndex() [][]string {
return [][]string{
[]string{"Id", "Name"},
}
}

// 多字段唯一键
func (u *User) TableUnique() [][]string {
return [][]string{
[]string{"Name", "Email"},
}
}

#主键

可以用auto显示指定一个字段为自增主键,该字段必须是 int, int32, int64, uint, uint32, 或者 uint64。

MyId int32 `orm:"auto"`

如果一个模型没有定义主键,那么 符合上述类型且名称为 Id 的模型字段将被视为自增主键。

如果不想使用自增主键,那么可以使用pk设置为主键。

Name string `orm:"pk"`

注意,目前 Beego 的非自增主键和联合主键支持得不是特别好。建议普遍使用自增主键

鉴于 go 目前的设计,即使使用了 uint64,但你也不能存储到他的最大值。依然会作为 int64 处理。

参见 issue 6113(opens new window)

#默认值

默认值是一个扩展功能,必须要显示注册默认值的Filter,而后在模型定义里面加上default的设置。

import (
"github.com/beego/beego/v2/client/orm/filter/bean"
"github.com/beego/beego/v2/client/orm"
)

type DefaultValueTestEntity struct {
Id int
Age int `default:"12"`
AgeInOldStyle int `orm:"default(13);bee()"`
AgeIgnore int
}

func XXX() {
builder := bean.NewDefaultValueFilterChainBuilder(nil, true, true)
orm.AddGlobalFilterChain(builder.FilterChain)
o := orm.NewOrm()
_, _ = o.Insert(&User{
ID: 1,
Name: "Tom",
})
}

#自动更新时间

Created time.Time `orm:"auto_now_add;type(datetime)"`
Updated time.Time `orm:"auto_now;type(datetime)"`
  • auto_now 每次 model 保存时都会对时间自动更新

  • auto_now_add 第一次保存时才设置时间

对于批量的 update 此设置是不生效的

#引擎

仅支持 MySQL,只需要实现接口TableEngineI

默认使用的引擎,为当前数据库的默认引擎,这个是由你的 mysql 配置参数决定的。

你可以在模型里设置 TableEngine 函数,指定使用的引擎

type User struct {
Id int
Name string
Email string
}

// 设置引擎为 INNODB
func (u *User) TableEngine() string {
return "INNODB"
}

#模型高级设置

#null

数据库表默认为 NOT NULL,设置 null 代表 ALLOW NULL

Name string `orm:"null"`

#size

string 类型字段默认为 varchar(255)

设置 size 以后,db type 将使用 varchar(size)

Title string `orm:"size(60)"`

#digits / decimals

设置 float32, float64 类型的浮点精度

Money float64 `orm:"digits(12);decimals(4)"`

总长度 12 小数点后 4 位 eg: 99999999.9999

#type

设置为 date 时,time.Time 字段的对应 db 类型使用 date

Created time.Time `orm:"auto_now_add;type(date)"`

设置为 datetime 时,time.Time 字段的对应 db 类型使用 datetime

Created time.Time `orm:"auto_now_add;type(datetime)"`

#Precision

datetime字段设置精度值位数,不同 DB 支持最大精度值位数也不一致。

type User struct {
...
Created time.Time `orm:"type(datetime);precision(4)"`
...
}

#Comment

为字段添加注释

type User struct {
...
Status int `orm:"default(1);description(这是状态字段)"`
...
}

注意: 注释中禁止包含引号

#模型字段与数据库类型的映射

在此列出 ORM 推荐的对应数据库类型,自动建表功能也会以此为标准。

默认所有的字段都是 NOT NULL

##z## MySQL

gomysql
int, int32 - 设置 auto 或者名称为 Id integer AUTO_INCREMENT
int64 - 设置 auto 或者名称为 Id bigint AUTO_INCREMENT
uint, uint32 - 设置 auto 或者名称为 Id integer unsigned AUTO_INCREMENT
uint64 - 设置 auto 或者名称为 Id bigint unsigned AUTO_INCREMENT
bool bool
string - 默认为 size 255 varchar(size)
string - 设置 type(char) 时 char(size)
string - 设置 type(text) 时 longtext
time.Time - 设置 type 为 date 时 date
time.Time datetime
byte tinyint unsigned
rune integer
int integer
int8 tinyint
int16 smallint
int32 integer
int64 bigint
uint integer unsigned
uint8 tinyint unsigned
uint16 smallint unsigned
uint32 integer unsigned
uint64 bigint unsigned
float32 double precision
float64 double precision
float64 - 设置 digits, decimals 时 numeric(digits, decimals)

#Sqlite3

gosqlite3
int, int32, int64, uint, uint32, uint64 - 设置 auto 或者名称为 Id integer AUTOINCREMENT
bool bool
string - 默认为 size 255 varchar(size)
string - 设置 type(char) 时 character(size)
string - 设置 type(text) 时 text
time.Time - 设置 type 为 date 时 date
time.Time datetime
byte tinyint unsigned
rune integer
int integer
int8 tinyint
int16 smallint
int32 integer
int64 bigint
uint integer unsigned
uint8 tinyint unsigned
uint16 smallint unsigned
uint32 integer unsigned
uint64 bigint unsigned
float32 real
float64 real
float64 - 设置 digits, decimals 时 decimal

#PostgreSQL

gopostgres
int, int32, int64, uint, uint32, uint64 - 设置 auto 或者名称为 Id serial
bool bool
string - 若没有指定 size 默认为 text varchar(size)
string - 设置 type(char) 时 char(size)
string - 设置 type(text) 时 text
string - 设置 type(json) 时 json
string - 设置 type(jsonb) 时 jsonb
time.Time - 设置 type 为 date 时 date
time.Time timestamp with time zone
byte smallint CHECK("column" >= 0 AND "column" <= 255)
rune integer
int integer
int8 smallint CHECK("column" >= -127 AND "column" <= 128)
int16 smallint
int32 integer
int64 bigint
uint bigint CHECK("column" >= 0)
uint8 smallint CHECK("column" >= 0 AND "column" <= 255)
uint16 integer CHECK("column" >= 0)
uint32 bigint CHECK("column" >= 0)
uint64 bigint CHECK("column" >= 0)
float32 double precision
float64 double precision
float64 - 设置 digits, decimals 时 numeric(digits, decimals)

#表关系设置

#rel / reverse

RelOneToOne:

type User struct {
...
Profile *Profile `orm:"null;rel(one);on_delete(set_null)"`
...
}

对应的反向关系 RelReverseOne:

type Profile struct {
...
User *User `orm:"reverse(one)"`
...
}

RelForeignKey:

type Post struct {
...
User *User `orm:"rel(fk)"` // RelForeignKey relation
...
}

对应的反向关系 RelReverseMany:

type User struct {
...
Posts []*Post `orm:"reverse(many)"` // fk 的反向关系
...
}

RelManyToMany:

type Post struct {
...
Tags []*Tag `orm:"rel(m2m)"` // ManyToMany relation
...
}

对应的反向关系 RelReverseMany:

type Tag struct {
...
Posts []*Post `orm:"reverse(many)"`
...
}

#rel_table / rel_through

此设置针对 orm:"rel(m2m)" 的关系字段

  • rel_table: 设置自动生成的 m2m 关系表的名称

  • rel_through: 如果要在 m2m 关系中使用自定义的 m2m 关系表,通过这个设置其名称,格式为 pkg.path.ModelName,例如: app.models.PostTagRelPostTagRel 表需要有到 PostTag 的关系

当设置 rel_table 时会忽略 rel_through

设置方法:

orm:"rel(m2m);rel_table(the_table_name)"
orm:"rel(m2m);rel_through(pkg.path.ModelName)"

#on_delete

设置对应的 rel 关系删除时,如何处理关系字段。

  • cascade: 级联删除(默认值)

  • set_null: 设置为 NULL,需要设置 null = true

  • set_default: 设置为默认值,需要设置 default 值

  • do_nothing: 什么也不做,忽略

type User struct {
...
Profile *Profile `orm:"null;rel(one);on_delete(set_null)"`
...
}
type Profile struct {
...
User *User `orm:"reverse(one)"`
...
}

// 删除 Profile 时将设置 User.Profile 的数据库字段为 NULL

#例子

type User struct {
Id int
Name string
}

type Post struct {
Id int
Title string
User *User `orm:"rel(fk)"`
}

假设 Post -> UserManyToOne 的关系,也就是外键。

o.Filter("Id", 1).Delete()

这个时候即会删除 Id 为 1 的 User 也会删除其发布的 Post

不想删除的话,需要设置 set_null

type Post struct {
Id int
Title string
User *User `orm:"rel(fk);null;on_delete(set_null)"`
}

那这个时候,删除 User 只会把对应的 Post.user_id 设置为 NULL

当然有时候为了高性能的需要,多存点数据无所谓啊,造成批量删除才是问题。

type Post struct {
Id int
Title string
User *User `orm:"rel(fk);null;on_delete(do_nothing)"`
}

那么只要删除的时候,不操作 Post 就可以了。

 

 

Orm 增删改查

如下就可以创建一个简单的Orm实例:

var o orm.Ormer
o = orm.NewOrm() // 创建一个 Ormer
// NewOrm 的同时会执行 orm.BootStrap (整个 app 只执行一次),用以验证模型之间的定义并缓存。

大多数情况下,你应该尽量复用Orm 实例,因为本身Orm实例被设计为无状态的,一个数据库对应一个Orm实例。

但是在使用事务的时候,我们会返回TxOrm的实例,它本身是有状态的,一个事务对应一个TxOrm实例。在使用TxOrm时候,任何衍生查询都是在该事务内。

#Insert 和 InsertWithCtx

定义:

Insert(md interface{}) (int64, error)
InsertWithCtx(ctx context.Context, md interface{}) (int64, error)

例如:

user := new(User)
id, err = Ormer.Insert(user)

这两个方法都只接收指针做为参数。

#InsertOrUpdate 和 InsertOrUpdateWithCtx

定义:

InsertOrUpdate(md interface{}, colConflitAndArgs ...string) (int64, error)
InsertOrUpdateWithCtx(ctx context.Context, md interface{}, colConflitAndArgs ...string) (int64, error)

这两个方法在不同的方言之下有不同的效果:

  • 对于 MySQL 来说,是执行 ON DUPLICATE KEY。因此最后一个参数colConflictAndArgs 不需要传;

  • 对于 PostgreSQL 来说,是执行 ON CONFLICT cols DO UPDATE SET,因此最后一个参数colConflictAndArgs可以传入具体的列名;

  • 对于别的方言来说,你需要确认它们支持类似的语法;

#InsertMulti 和 InsertMultiWithCtx

用于执行批量插入:

InsertMulti(bulk int, mds interface{}) (int64, error)
InsertMultiWithCtx(ctx context.Context, bulk int, mds interface{}) (int64, error)

参数bulk是每一次批量插入的时候插入的数量。例如bulk<=1代表每一批插入一条数据,而如果bulk=3代表每次插入三条数据。你需要仔细选择批次大小,它对插入性能有很大影响。大多数情况下,你可以把bulk设置成数据量大小。

mds必须是一个数组,或者是一个切片。

第一个返回值表示最终插入了多少数据。

#Update 和 UpdateWithCtx

使用主键来更新数据。也就是如果你使用这个方法,Beego 会尝试读取里面的主键值,而后将主键作为更新的条件。

定义是:

Update(md interface{}, cols ...string) (int64, error)
UpdateWithCtx(ctx context.Context, md interface{}, cols ...string) (int64, error)

如果你没有指定 cols 参数,那么所有的列都会被更新。

第一个返回值是受影响的行数。

#Delete 和 DeleteWithCtx

使用主键来删除数据,定义:

Delete(md interface{}, cols ...string) (int64, error)
DeleteWithCtx(ctx context.Context, md interface{}, cols ...string) (int64, error)

第一个返回值是受影响的行数。

#Read 和 ReadWithCtx

方法定义为:

Read(md interface{}, cols ...string) error
ReadWithCtx(ctx context.Context, md interface{}, cols ...string) error

该方法的特点是:

  • 读取到的数据会被放到 md

  • 如果传入了 cols 参数,那么只会选取特定的列;

例如:

// 读取全部列
u = &User{Id: user.Id}
err = Ormer.Read(u)

// 只读取用户名这一个列
u = &User{}
err = Ormer.Read(u, "UserName")

#ReadForUpdate 和 ReadForUpdateWithCtx

这两个方法的定义是:

ReadForUpdate(md interface{}, cols ...string) error
ReadForUpdateWithCtx(ctx context.Context, md interface{}, cols ...string) error

这两个方法类似于ReadReadWithCtx,所不同的是,这两个方法在查询的时候加上 FOR UPDATE,因此常用于事务内部。

但是并不是所有的数据库都支持 FOR UPDATE 语句,所以你在使用的时候要首先确认自己的数据库支持 FOR UPDATE 的用法。

#ReadOrCreate 和 ReadOrCreateWithCtx

它们的定义是:

ReadOrCreate(md interface{}, col1 string, cols ...string) (bool, int64, error)
ReadOrCreateWithCtx(ctx context.Context, md interface{}, col1 string, cols ...string) (bool, int64, error)

从数据库中查找数据,如果数据不存在,那么就插入。

需要注意的是,“查找-判断-插入”这三个动作并不是原子的,也不是线程安全的。因此在并发环境下,它的行为可能会超出你的预期,比如说有两个 goroutine 同时判断到数据不存在,那么它们都会尝试插入。

#Raw 和 RawWithContext

	Raw(query string, args ...interface{}) RawSeter
RawWithCtx(ctx context.Context, query string, args ...interface{}) RawSeter

执行原生查询。Beego 并不可能支持所有的 SQL 语法特性,因此在某些特殊情况下,你需要使用原生查询。

它会返回一个RawSeter,你可以参阅RawSeter来确认该如何处理查询返回的结果集。

#LoadRelated 和 LoadRelatedWithCtx

它们的定义是:

LoadRelated(md interface{}, name string, args ...utils.KV) (int64, error)
LoadRelatedWithCtx(ctx context.Context, md interface{}, name string, args ...utils.KV) (int64, error)

LoadRelatedWithCtx 已经被弃用。

这两个方法用于加载关联表的数据,例如:

o.LoadRelated(post,"Tags")
for _,tag := range post.Tags{
// 业务代码
}

该方法对

注意到,这两个方法最后一个参数都是传入 KV 值,目前这些 KV 值被定义在 hints 包里面,有:

  • hints.DefaultRelDepth:设置关联表的解析深度为默认值 2;

  • hints.RelDepth:设置自定义的关联表深度;

  • hints.Limit:设置查询返回的行数;

  • hints.Offset:设置查询结果的偏移量;

  • hints.OrderBy:设置查询的排序;

这个方法要谨慎使用,尤其是在偏移量或者深度设置的值比较大的情况下,响应时间会比较长。

#QueryM2M 和 QueryM2MWithCtx

定义是:

	QueryM2M(md interface{}, name string) QueryM2Mer
QueryM2MWithCtx(ctx context.Context, md interface{}, name string) QueryM2Mer

QueryM2MWithCtx已经不建议使用了,因为ctx参数毫无效果。

这两个方法都是返回一个QueryM2Mer,用于查询多对多关联关系的数据。可以参考[./query.md#QueryM2Mer]

 

事务

事务依赖于 Orm 实例。Orm的用法可以参考Orm 增删改查

ORM 操作事务,支持两种范式。一种通过闭包的方式,由 Beego 本身来管理事务的生命周期。

	// Beego will manage the transaction's lifecycle
// if the @param task return error, the transaction will be rollback
// or the transaction will be committed
err := o.DoTx(func(ctx context.Context, txOrm orm.TxOrmer) error {
// data
user := new(User)
user.Name = "test_transaction"

// insert data
// Using txOrm to execute SQL
_, e := txOrm.Insert(user)
// if e != nil the transaction will be rollback
// or it will be committed
return e
})

在这种方式里面,第一个参数是task,即该事务所有完成的动作。注意的是,如果它返回了 error,那么 Beego 会将整个事务回滚。

否则提交事务。

另外一个要注意的是,如果在task执行过程中,发生了panic,那么 Beego 会回滚事务。

我们推荐使用这种方式。

另外一种方式,则是传统的由开发自己手动管理事务的生命周期

	o := orm.NewOrm()
to, err := o.Begin()
if err != nil {
logs.Error("start the transaction failed")
return
}

user := new(User)
user.Name = "test_transaction"

// do something with to. to is an instance of TxOrm

// insert data
// Using txOrm to execute SQL
_, err = to.Insert(user)

if err != nil {
logs.Error("execute transaction's sql fail, rollback.", err)
err = to.Rollback()
if err != nil {
logs.Error("roll back transaction failed", err)
}
} else {
err = to.Commit()
if err != nil {
logs.Error("commit transaction failed.", err)
}
}

无论使用哪种方式,都应该注意到,只有通过TxOrm执行的 SQL 才会被认为是在一个事务里面。

o := orm.NewOrm()
to, err := o.Begin()

// outside the txn
o.Insert(xxx)

// inside the txn
to.Insert(xxx)

当然,从TxOrm里面衍生出来的QuerySeterQueryM2Mer,RawSeter也是被认为在事务里面。

和事务相关的方法有:

	// 需要自己管理事务生命周期
Begin() (TxOrmer, error)
BeginWithCtx(ctx context.Context) (TxOrmer, error)
BeginWithOpts(opts *sql.TxOptions) (TxOrmer, error)
BeginWithCtxAndOpts(ctx context.Context, opts *sql.TxOptions) (TxOrmer, error)

// Beego 利用闭包管理生命周期
DoTx(task func(ctx context.Context, txOrm TxOrmer) error) error
DoTxWithCtx(ctx context.Context, task func(ctx context.Context, txOrm TxOrmer) error) error
DoTxWithOpts(opts *sql.TxOptions, task func(ctx context.Context, txOrm TxOrmer) error) error
DoTxWithCtxAndOpts(ctx context.Context, opts *sql.TxOptions, task func(ctx context.Context, txOrm TxOrmer) error) error

 

日志模块

这是一个用来处理日志的库,它的设计思路来自于 database/sql,目前支持的引擎有 file、console、net、smtp、es、slack。

例子参考beego-example (opens new window)下的logs部分

#快速开始

首先引入包:

import (
"github.com/beego/beego/v2/core/logs"
)

然后添加输出引擎(log 支持同时输出到多个引擎),这里我们以 console 为例,第一个参数是引擎名:

logs.SetLogger(logs.AdapterConsole)

添加输出引擎也支持第二个参数,用来表示配置信息,对于不同的引擎来说,其配置也是不同的。详细的配置请看下面介绍:

logs.SetLogger(logs.AdapterFile,`{"filename":"project.log","level":7,"maxlines":0,"maxsize":0,"daily":true,"maxdays":10,"color":true}`)

然后我们就可以在我们的逻辑中开始任意的使用了:

package main

import (
"github.com/beego/beego/v2/core/logs"
)

func main() {
//an official log.Logger
l := logs.GetLogger()
l.Println("this is a message of http")
//an official log.Logger with prefix ORM
logs.GetLogger("ORM").Println("this is a message of orm")

logs.Debug("my book is bought in the year of ", 2016)
logs.Info("this %s cat is %v years old", "yellow", 3)
logs.Warn("json is a type of kv like", map[string]int{"key": 2016})
logs.Error(1024, "is a very", "good game")
logs.Critical("oh,crash")
}

#多个实例

一般推荐使用通用方式进行日志,但依然支持单独声明来使用独立的日志

package main

import (
"github.com/beego/beego/v2/core/logs"
)

func main() {
log := logs.NewLogger()
log.SetLogger(logs.AdapterConsole)
log.Debug("this is a debug message")
}

#输出文件名和行号

日志默认不输出调用的文件名和文件行号,如果你期望输出调用的文件名和文件行号,可以如下设置

logs.EnableFuncCallDepth(true)

开启传入参数 true,关闭传入参数 false,默认是关闭的.

如果你的应用自己封装了调用 log 包,那么需要设置 SetLogFuncCallDepth,默认是 2,也就是直接调用的层级,如果你封装了多层,那么需要根据自己的需求进行调整.

logs.SetLogFuncCallDepth(3)

#异步输出日志

为了提升性能, 可以设置异步输出:

logs.Async()

异步输出允许设置缓冲 chan 的大小

logs.Async(1e3)

#自定义日志格式

在一些情况下,我们可能需要自己定义自己的日志格式规范。这种时候,可以考虑通过扩展LogFormatter

type LogFormatter interface {
Format(lm *LogMsg) string
}

LogMsg包含了一条日志的所有部分。需要注意的是,如果你希望输出文件名和行号,那么应该参考输出文件名和行号,设置对应的参数。

#例子:PatternLogFormatter

该实现的设计思路,是希望能够使用类似于占位符的东西来定义一条日志应该如何输出。

例子:

package main

import (
"github.com/beego/beego/v2/core/logs"
)

func main() {

f := &logs.PatternLogFormatter{
Pattern: "%F:%n|%w%t>> %m",
WhenFormat: "2006-01-02",
}
logs.RegisterFormatter("pattern", f)

_ = logs.SetGlobalFormatter("pattern")

logs.Info("hello, world")
}

我们先初始化了一个PatternLogFormatter实例,而后注册为pattern

再然后我们使用logs.SetGlobalFormatter("pattern")设置全局所有的引擎都使用这个格式。

最终我们输出日志/beego-example/logger/formatter/pattern/main.go:31|2020-10-29[I]>> hello, world

如果我们只希望在某个特定的引擎上使用这个格式,我们可以通过初始化引擎的时候,设置:

	_ = logs.SetLogger("console",`{"formatter": "pattern"}`)

PatternLogFormatter支持的占位符及其含义:

  • 'w' 时间

  • 'm' 消息

  • 'f' 文件名

  • 'F' 文件全路径

  • 'n' 行数

  • 'l' 消息级别,数字表示

  • 't' 消息级别,简写,例如[I]代表 INFO

  • 'T' 消息级别,全称

#引擎配置设置

  • console: 命令行输出,默认输出到os.Stdout

    logs.SetLogger(logs.AdapterConsole, `{"level":1,"color":true}`)

    主要的参数如下说明:

    • level 输出的日志级别

    • color 是否开启打印日志彩色打印(需环境支持彩色输出)

  • file:输出到文件,设置的例子如下所示:

    logs.SetLogger(logs.AdapterFile, `{"filename":"test.log"}`)

    主要的参数如下说明:

    • filename 保存的文件名

    • maxlines 每个文件保存的最大行数,默认值 1000000

    • maxsize 每个文件保存的最大尺寸,默认值是 1 << 28, 256 MB

    • daily 是否按照每天 logrotate,默认是 true

    • maxdays 文件最多保存多少天,默认保存 7 天

    • rotate 是否开启 logrotate,默认是 true

    • level 日志保存的时候的级别,默认是 Trace 级别

    • perm 日志文件权限

  • multifile:不同级别的日志会输出到不同的文件中:

    设置的例子如下所示:

    logs.SetLogger(logs.AdapterMultiFile, `{"filename":"test.log","separate":["emergency", "alert", "critical", "error", "warning", "notice", "info", "debug"]}`)

    主要的参数如下说明(除 separate 外,均与 file 相同):

    • filename 保存的文件名

    • maxlines 每个文件保存的最大行数,默认值 1000000

    • maxsize 每个文件保存的最大尺寸,默认值是 1 << 28, //256 MB

    • daily 是否按照每天 logrotate,默认是 true

    • maxdays 文件最多保存多少天,默认保存 7 天

    • rotate 是否开启 logrotate,默认是 true

    • level 日志保存的时候的级别,默认是 Trace 级别

    • perm 日志文件权限

    • separate 需要单独写入文件的日志级别,设置后命名类似 test.error.log

  • conn: 网络输出,设置的例子如下所示:

      logs.SetLogger(logs.AdapterConn, `{"net":"tcp","addr":":7020"}`)

    主要的参数说明如下:

    • reconnectOnMsg 是否每次链接都重新打开链接,默认是 false

    • reconnect 是否自动重新链接地址,默认是 false

    • net 发开网络链接的方式,可以使用 tcp、unix、udp 等

    • addr 网络链接的地址

    • level 日志保存的时候的级别,默认是 Trace 级别

  • smtp: 邮件发送,设置的例子如下所示:

    logs.SetLogger(logs.AdapterMail, `{"username":"beegotest@gmail.com","password":"xxxxxxxx","host":"smtp.gmail.com:587","sendTos":["xiemengjun@gmail.com"]}`)

    主要的参数说明如下:

    • username: smtp 验证的用户名

    • password: smtp 验证密码

    • host: 发送的邮箱地址

    • sendTos: 邮件需要发送的人,支持多个

    • subject: 发送邮件的标题,默认是 Diagnostic message from server

    • level: 日志发送的级别,默认是 Trace 级别

  • ElasticSearch:输出到 ElasticSearch:

    logs.SetLogger(logs.AdapterEs, `{"dsn":"http://localhost:9200/","level":1}`)
  • 简聊: 输出到简聊:

    logs.SetLogger(logs.AdapterJianLiao, `{"authorname":"xxx","title":"beego", "webhookurl":"https://jianliao.com/xxx", "redirecturl":"https://jianliao.com/xxx","imageurl":"https://jianliao.com/xxx","level":1}`)
  • slack: 输出到 slack

    logs.SetLogger(logs.AdapterSlack, `{"webhookurl":"https://slack.com/xxx","level":1}`)

 

定时任务

  1. 初始化一个任务

    tk1 := task.NewTask("tk1", "0 12 * * * *", func(ctx context.Context) error { fmt.Println("tk1"); return nil })

    函数原型:

    NewTask(tname string, spec string, f TaskFunc) *Task

    • tname: 任务名称

    • spec: 定时任务格式,请参考下面的详细介绍

    • f: 执行的函数

  2. 可以测试开启运行。直接运行创建的 tk

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

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

    task.StartTask()
    defer task.StopTask()

查看注册的任务,或者手动调动执行,可以参考Admin 后台

#spec 详解

spec 格式是参照 crontab 做的,详细的解释如下所示:

// 前6个字段分别表示:
// 秒钟:0-59
// 分钟:0-59
// 小时:1-23
// 日期:1-31
// 月份:1-12
// 星期:0-6(0 表示周日)

//还可以用一些特殊符号:
// *: 表示任何时刻
// ,: 表示分割,如第三段里:2,4,表示 2 点和 4 点执行
//   -:表示一个段,如第三端里: 1-5,就表示 1 到 5 点
// /n : 表示每个n的单位执行一次,如第三段里,*/1, 就表示每隔 1 个小时执行一次命令。也可以写成1-23/1.
/////////////////////////////////////////////////////////
// 0/30 * * * * * 每 30 秒 执行
// 0 43 21 * * * 21:43 执行
// 0 15 05 * * *    05:15 执行
// 0 0 17 * * * 17:00 执行
// 0 0 17 * * 1 每周一的 17:00 执行
// 0 0,10 17 * * 0,2,3 每周日,周二,周三的 17:00和 17:10 执行
// 0 0-10 17 1 * * 毎月1日从 17:00 到 7:10 毎隔 1 分钟 执行
// 0 0 0 1,15 * 1 毎月1日和 15 日和 一日的 0:00 执行
// 0 42 4 1 * *     毎月1日的 4:42 分 执行
// 0 0 21 * * 1-6   周一到周六 21:00 执行
// 0 0,10,20,30,40,50 * * * *  每隔 10 分 执行
// 0 */10 * * * *        每隔 10 分 执行
// 0 * 1 * * *         从 1:0 到 1:59 每隔 1 分钟 执行
// 0 0 1 * * *         1:00 执行
// 0 0 */1 * * *        毎时 0 分 每隔 1 小时 执行
// 0 0 * * * *         毎时 0 分 每隔 1 小时 执行
// 0 2 8-20/3 * * *       8:02,11:02,14:02,17:02,20:02 执行
// 0 30 5 1,15 * *       1 日 和 15 日的 5:30 执行

一般网络上有自动的 crontab 表达式生成工具,可以直接使用。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

posted @ 2023-02-03 16:54  游走De提莫  阅读(488)  评论(0编辑  收藏  举报