# golang 框架 GoFrame 重要知识点整理 {#golang-框架-goframe-重要知识点整理}
本文介绍 golang 框架 GoFrame 的重要知识点。本文不会前面介绍 goframe,仅罗列该框架的特性以及重要的知识点。
# 1. 路由注册 {#_1-路由注册}
GoFrame 支持各种各样的路由注册方式,非常灵活。
但实际上只需要掌握官方推荐的一种注册方式即可: 以嵌套的方式定义分组路由。
这种方式的推荐理由:
-
以分组方式,方便管理路由
-
以嵌套方式,方便从代码角度一眼看出路由的上下级层次。
示例如下:package main
import ( "net/http"
"github.com/gogf/gf/frame/g" "github.com/gogf/gf/net/ghttp"
)
func MiddlewareAuth(r *ghttp.Request) { token := r.Get("token") if token == "123456" { r.Middleware.Next() } else { r.Response.WriteStatus(http.StatusForbidden) } }
func MiddlewareCORS(r *ghttp.Request) { r.Response.CORSDefault() r.Middleware.Next() }
func MiddlewareLog(r *ghttp.Request) { r.Middleware.Next() g.Log().Println(r.Response.Status, r.URL.Path) }
func main() { s := g.Server() s.Use(MiddlewareLog) s.Group("/api.v2", func(group *ghttp.RouterGroup) { group.Middleware(MiddlewareAuth, MiddlewareCORS) group.GET("/test", func(r *ghttp.Request) { r.Response.Write("test") }) group.Group("/order", func(group *ghttp.RouterGroup) { group.GET("/list", func(r *ghttp.Request) { r.Response.Write("list") }) group.PUT("/update", func(r *ghttp.Request) { r.Response.Write("update") }) }) group.Group("/user", func(group *ghttp.RouterGroup) { group.GET("/info", func(r *ghttp.Request) { r.Response.Write("info") }) group.POST("/edit", func(r *ghttp.Request) { r.Response.Write("edit") }) group.DELETE("/drop", func(r *ghttp.Request) { r.Response.Write("drop") }) }) group.Group("/hook", func(group ghttp.RouterGroup) { group.Hook("/", ghttp.HOOK_BEFORE_SERVE, func(r *ghttp.Request) { r.Response.Write("hook any") }) group.Hook("/:name", ghttp.HOOK_BEFORE_SERVE, func(r *ghttp.Request) { r.Response.Write("hook name") }) }) }) s.SetPort(8199) s.Run() }
# 2. 请求输入 {#_2-请求输入}
# 2.1 按参数类型获取参数 {#_2-1-按参数类型获取参数}
Get*Struct 和 GetBody/GetBodyString 的区别
接口参数中有一种特殊的参数: 自定义参数,往往在服务端的中间件、服务函数中通过 SetParam/GetParam 方法管理
Get*(或 GetRequset*)方法存在优先级:Router < Query < Body < Form < Custom GetQuery*方法存在优先级: Query > Body
# 2.2 参数绑定到对象(推荐) {#_2-2-参数绑定到对象-推荐}
若想将接口参数(支持所有类型的参数: query、form、json 等)绑定到 struct 上,框架默认支持,无需指定 tag。
若想自定义绑定规则,则可使用 tag 标签自定义。
package main
import (
"github.com/gogf/gf/frame/g"
"github.com/gogf/gf/net/ghttp"
)
type RegisterReq struct {
Name string
Pass string `p:"password1"`
Pass2 string `p:"password2"`
}
type RegisterRes struct {
Code int `json:"code"`
Error string `json:"error"`
Data interface{} `json:"data"`
}
func main() {
s := g.Server()
s.BindHandler("/register", func(r *ghttp.Request) {
var req *RegisterReq
if err := r.Parse(&req); err != nil {
r.Response.WriteJsonExit(RegisterRes{
Code: 1,
Error: err.Error(),
})
}
// ...
r.Response.WriteJsonExit(RegisterRes{
Data: req,
})
})
s.SetPort(8199)
s.Run()
}
# 2.3 请求校验 {#_2-3-请求校验}
推荐的错误校验方法: 当产生错误时,我们可以将校验错误转换为*gvalid.Error
对象,随后可以通过灵活的方法控制错误的返回。
package main
import (
"github.com/gogf/gf/frame/g"
"github.com/gogf/gf/net/ghttp"
"github.com/gogf/gf/util/gvalid"
)
type RegisterReq struct {
Name string `p:"username" v:"required|length:6,30#请输入账号|账号长度为:min到:max位"`
Pass string `p:"password1" v:"required|length:6,30#请输入密码|密码长度不够"`
Pass2 string `p:"password2" v:"required|length:6,30|same:password1#请确认密码|密码长度不够|两次密码不一致"`
}
type RegisterRes struct {
Code int `json:"code"`
Error string `json:"error"`
Data interface{} `json:"data"`
}
func main() {
s := g.Server()
s.BindHandler("/register", func(r *ghttp.Request) {
var req *RegisterReq
if err := r.Parse(&req); err != nil {
// Validation error.
if v, ok := err.(*gvalid.Error); ok {
r.Response.WriteJsonExit(RegisterRes{
Code: 1,
Error: v.FirstString(),
})
}
// Other error.
r.Response.WriteJsonExit(RegisterRes{
Code: 1,
Error: err.Error(),
})
}
// ...
r.Response.WriteJsonExit(RegisterRes{
Data: req,
})
})
s.SetPort(8199)
s.Run()
}
测试结果:
$ curl "http://127.0.0.1:8199/register"
{"code":1,"error":"请输入账号","data":null}
$ curl "http://127.0.0.1:8199/register?name=john&password1=123456&password2=12345"
{"code":1,"error":"两次密码不一致","data":null}
# 2.4 自定义变量 {#_2-4-自定义变量}
开发者可以在请求中自定义一些变量设置,自定义变量的获取优先级是最高的,可以覆盖原有的客户端提交参数。
自定义变量可以通过 SetParam 方法进行设置。
自定义变量的获取可以通过请求参数的获取方法获得到,例如:Get/GetVar/GetMap 等等,也可以通过特定的自定义变量方法获取到 GetParam/GetParamVar
package main
import (
"github.com/gogf/gf/frame/g"
"github.com/gogf/gf/net/ghttp"
)
// 前置中间件1
func MiddlewareBefore1(r *ghttp.Request) {
r.SetParam("name", "GoFrame")
r.Response.Writeln("set name")
r.Middleware.Next()
}
// 前置中间件2
func MiddlewareBefore2(r *ghttp.Request) {
r.SetParam("site", "https://goframe.org")
r.Response.Writeln("set site")
r.Middleware.Next()
}
func main() {
s := g.Server()
s.Group("/", func(group *ghttp.RouterGroup) {
group.Middleware(MiddlewareBefore1, MiddlewareBefore2)
group.ALL("/", func(r *ghttp.Request) {
r.Response.Writefln(
"%s: %s",
r.GetParamVar("name").String(),
r.GetParamVar("site").String(),
)
})
})
s.SetPort(8199)
s.Run()
}
结果:
set name
set site
GoFrame: https://goframe.org
# 2.5 上下文变量 {#_2-5-上下文变量}
上下文变量 和自定义变量的区别:自定义变量会覆盖接口参数,而上下文变量不会覆盖。
# 3. 请求输出 {#_3-请求输出}
# 3.1 缓冲区 {#_3-1-缓冲区}
Response 输出采用了缓冲控制,输出的内容预先写入到一块缓冲区,等待服务方法执行完毕后才真正地输出到客户端。该特性在提高执行效率同时为输出内容的控制提供了更高的灵活性。
可应用于全局异常处理场景: 接口出现异常, 重新定义接口的输出内容。
package main
import (
"github.com/gogf/gf/frame/g"
"github.com/gogf/gf/net/ghttp"
"net/http"
)
func MiddlewareErrorHandler(r *ghttp.Request) {
r.Middleware.Next()
if r.Response.Status >= http.StatusInternalServerError {
r.Response.ClearBuffer()
r.Response.Writeln("服务器居然开小差了,请稍后再试吧!")
}
}
func main() {
s := g.Server()
s.Group("/api.v2", func(group *ghttp.RouterGroup) {
group.Middleware(MiddlewareErrorHandler)
group.ALL("/user/list", func(r *ghttp.Request) {
panic("db error: sql is xxxxxxx")
})
})
s.SetPort(8199)
s.Run()
}
# 4. session {#_4-session}
GF 框架的 Session 默认过期时间是 24 小时
SessionId 默认通过 Cookie 来传递,并且也支持客户端通过 Header 传递 SessionId,SessionId 的识别名称可以通过 ghttp.Server 的 SetSessionIdName 进行修改。
ghttp.Server 中的 SessionId 使用的是客户端的 RemoteAddr + Header 请求信息通过 guid 模块来生成的,保证随机及唯一性
自定义 session 的过期时间:
s := g.Server()
s.SetConfigWithMap(g.Map{
"SessionMaxAge": time.Minute,
})
gsession 实现并为开发者提供了常见的三种 Session 存储实现方式:
-
基于文件存储(默认)
单节点部署方式下比较高效的持久化存储方式; 重启程序后,session 数据不丢失,自动加载到内存。 -
基于纯内存存储
性能最高效,但是无法持久化保存,重启即丢失;s := g.Server() s.SetConfigWithMap(g.Map{ "SessionMaxAge": time.Minute, "SessionStorage": gsession.NewStorageMemory(), })
-
基于 Redis 存储
适用于多节点部署的场景;
# 5. 配置管理 {#_5-配置管理}
2 种配置方式: 代码或配置文件
服务启动后不允许修改配置
用户提交的数据大小限制的配置项:
[server]
MaxHeaderBytes = "20KB"
ClientMaxBodySize = "200MB"
# 6. 全局异常处理 {#_6-全局异常处理}
默认情况下,全局异常会记录到日志中。
开发者也可以自定义异常处理逻辑。异常信息的获取: Request 对象中的 GetError 方法
疑问:即使开发者有自己捕获记录异常错误的日志,但是Server依旧会打印到Server自己的错误日志文件中
中的 server 自己的错误日志文件指的是?
# 7. HTTP 客户端 {#_7-http-客户端}
推荐使用单例对象g.Client()
基于连接池。
支持链式操作
支持对请求或响应的原始信息的打印,类似于 curl -I 的功能
若默认的单例客户端不满足需求,可以自定义客户端。
*Bytes
及*Content
方法:
以 Bytes 及 Content 后缀结尾的请求方法为直接获取返回内容的快捷方法,这些方法将会自动读取服务端返回内容并自动关闭请求连接。需要注意的是,如果请求执行失败,返回内容将会为空。
非*Bytes
及*Content
方法:
必须手动调用 Close 方法关闭
*Var
方法:
若服务端返回 json 或 xml 数据时, 可使用该方法转换为对象
struct 对象作为 json 输入参数的处理方法:
若输入参数为 struct,默认的 content-type 是表单方式,但不符合我们的预期。
-
显性指定 content-type 为 application/json
g.Client().SetHeader("Content-Type", "application/json").PostVar("https://fk.estar.net.cn:3887/my/adDeviceThirdBegin", reqGetEstarVideoAd).Scan(&respGetEstarVideoAd)
-
将 struct 转为 json 字符串
para, _ := jsoniter.MarshalToString(reqGetEstarVideoAd) g.Client().PostVar("https://fk.estar.net.cn:3887/my/adDeviceThirdBegin", para).Scan(&respGetEstarVideoAd)
# 8. 静态文件服务器 {#_8-静态文件服务器}
支持按路由自定义发布目录(有优先级规则)。
支持和 rewrite。
# 9. 日志 {#_9-日志}
分为 2 类日志:
- server 日志
类似于 nginx 的日志,属于请求日志,包括 access.log 和 error.log, 可以自定义日志格式,但无法控制日志的内容 - 业务日志
# 10. 跨域 {#_10-跨域}
Allowdomain 和 alloworigin 区别是?
不执行非法的跨域请求: 默认情况下,服务端收到了非法跨域的请求,也会执行接口逻辑。若期望不执行非法跨域请求的接口逻辑, 则额外补充个 CORSAllowedOrigin 方法判断即可。
# 11. 同时支持 http 和 https {#_11-同时支持-http-和-https}
gf 本身支持 https, 若不想在程序端支持 https,也可以在 nginx 端支持 https。
- 在本地生成测试证书
可以在本地生成测试证书
# 12. Websocket {#_12-websocket}
默认支持跨域访问,若需做跨域控制则需要自定义实现逻辑。
# 13. 自定义状态码处理 {#_13-自定义状态码处理}
等同于同后置中间件,为了处理方便而专门定义的一组处理器。
在自定义状态码处理方法中如果涉及到内容的输出,往往需要使用 r.Response.ClearBuffer()方法将原本缓冲区的输出内容清空。
# 14. 热重启 {#_14-热重启}
重启程序而服务不中断。
默认关闭,可通过配置文件开启。
会额外占用本地的 10000 端口
版本更新时很有用
可以通过管理页面或命令行进行重启或关闭
支持自定义管理页面(对应一个管理接口)的路由
通过命令行:
- 重启
kill -SIGUSR1 进程ID
- 关闭
kill -SIGTERM 进程ID
热更新步骤:
更新程序时,无法直接覆盖正在运行的程序。
- rm 旧程序
- 更新程序
- 通过管理页面或命令行热重启
新程序接管后的处理逻辑是对当前未完成的请求重新处理还是从当前逻辑位置继续处理: 重新处理。
# 15. 性能分析 {#_15-性能分析}
支持自定义性能分析页面的路由
# 16. 事件回调 {#_16-事件回调}
类似中间件功能,但更简单
同一个路由支持绑定多个回调
按照路由优先级及回调注册顺序进行回调方法调用
中间件与事件回调的区别:
- 全局中间件无法拦截静态文件的路由,但全局事件回调可以
- 事件回调中的路由对象是空,只能取得 url 地址
- 事件回调拦截粒度更小
# 17. GF 工具链 {#_17-gf-工具链}
# 17.1 安装方法 {#_17-1-安装方法}
执行如下命令安装:
wget https://goframe.org/cli/linux_amd64/gf && chmod +x gf && ./gf install
安装过程中,会提示输入安装目录位置, 输入安装位置的序号即可。
安装过程如下:
(py3.6) wangshibiao@wangshibiao:/data2/programInstaller/golang$ wget https://goframe.org/cli/linux_amd64/gf && chmod +x gf && ./gf install
--2020-11-09 16:15:49-- https://goframe.org/cli/linux_amd64/gf
正在解析主机 goframe.org (goframe.org)... 47.244.3.49
正在连接 goframe.org (goframe.org)|47.244.3.49|:443... 已连接。
已发出 HTTP 请求,正在等待回应... 302 Found
位置:https://gfcdn.johng.cn/cli/linux_amd64/gf?0e49c415a70e03d511cba98476b07a03 [跟随至新的 URL]
--2020-11-09 16:15:50-- https://gfcdn.johng.cn/cli/linux_amd64/gf?0e49c415a70e03d511cba98476b07a03
正在解析主机 gfcdn.johng.cn (gfcdn.johng.cn)... 43.241.240.104, 43.241.240.105, 43.241.240.106, ...
正在连接 gfcdn.johng.cn (gfcdn.johng.cn)|43.241.240.104|:443... 已连接。
已发出 HTTP 请求,正在等待回应... 200 OK
长度: 12026104 (11M) [application/octet-stream]
正在保存至: "gf"
gf 100%[=============================================================================================>] 11.47M 186KB/s in 47s
2020-11-09 16:16:37 (248 KB/s) - 已保存 "gf" [12026104/12026104])
I found some installable paths for you:
Id | Writable | Installed | Path
0 | true | false | /home/wangshibiao/programs/Anaconda3-5.2.0/envs/py3.6/bin
1 | true | false | /home/wangshibiao/.phpbrew/php/php-7.2.4/bin
2 | true | false | /home/wangshibiao/.phpbrew/bin
3 | true | false | /home/wangshibiao/.nvm/versions/node/v15.0.1/bin
4 | true | false | /home/wangshibiao/.local/bin
5 | true | false | /home/wangshibiao/programs/VSCode-linux-x64/bin
6 | true | false | /home/wangshibiao/programs/phpbrew
7 | true | false | /home/wangshibiao/programs/geckodriver
8 | true | false | /home/wangshibiao/programs/Anaconda3-5.2.0/bin
9 | true | false | /home/wangshibiao/.config/yarn/global/node_modules/.bin
10 | true | false | /home/wangshibiao/programs/phantomjs-2.1.1-linux-x86_64/bin
11 | true | false | /home/wangshibiao/programs/android-sdk/emulator
12 | true | false | /home/wangshibiao/programs/android-sdk/platform-tools
13 | true | false | /home/wangshibiao/programs/android-sdk/tools
14 | true | false | /home/wangshibiao/programs/android-sdk/tools/bin
15 | true | false | /home/wangshibiao/programs/go/bin
16 | true | false | /home/wangshibiao/go/bin
17 | true | false | /home/wangshibiao/programs/python3/bin
18 | true | false | /home/wangshibiao/programs/apache-maven-3.0.5/bin
19 | true | false | /home/wangshibiao/programs/gradle-4.7/bin
20 | true | false | /home/wangshibiao/programs/jdk1.8.0_144/bin
21 | true | false | /home/wangshibiao/programs/Bento4-SDK-1-5-1-628.x86_64-unknown-linux/bin
22 | true | false | /home/wangshibiao/programs/ffmpeg-4.0-64bit-static
23 | true | false | /home/wangshibiao/programs
please choose one installation destination [default 0]: 23
gf binary is successfully installed to: /home/wangshibiao/programs
(py3.6) wangshibiao@wangshibiao:/data2/programInstaller/golang$ which gf
/home/wangshibiao/programs/gf
(py3.6) wangshibiao@wangshibiao:/data2/programInstaller/golang$
# 17.2 初始化工程 {#_17-2-初始化工程}
-
创建项目根目录
-
在项目根目录执行如下命令初始化项目
gf init 项目名称
执行过程如下:(py3.6) wangshibiao@wangshibiao:/data/workspace/github/go/private/ad$ ll 总用量 8 drwxrwxr-x 2 wangshibiao wangshibiao 4096 11月 9 17:22 ./ drwxrwxr-x 10 wangshibiao wangshibiao 4096 11月 9 17:22 ../ (py3.6) wangshibiao@wangshibiao:/data/workspace/github/go/private/ad$ gf init ad initializing... initialization done! you can now run 'gf run main.go' to start your journey, enjoy! (py3.6) wangshibiao@wangshibiao:/data/workspace/github/go/private/ad$ ll 总用量 76 drwxrwxr-x 13 wangshibiao wangshibiao 4096 11月 9 17:23 ./ drwxrwxr-x 10 wangshibiao wangshibiao 4096 11月 9 17:22 ../ drwxrwxr-x 5 wangshibiao wangshibiao 4096 11月 9 17:22 app/ drwxrwxr-x 2 wangshibiao wangshibiao 4096 11月 9 17:22 boot/ drwxrwxr-x 2 wangshibiao wangshibiao 4096 11月 9 17:22 config/ drwxrwxr-x 2 wangshibiao wangshibiao 4096 11月 9 17:22 docker/ -rw-rw-r-- 1 wangshibiao wangshibiao 867 11月 9 17:22 Dockerfile drwxrwxr-x 2 wangshibiao wangshibiao 4096 11月 9 17:22 document/ -rw-rw-r-- 1 wangshibiao wangshibiao 23 11月 9 17:22 .gitattributes -rw-rw-r-- 1 wangshibiao wangshibiao 158 11月 9 17:22 .gitignore -rw-rw-r-- 1 wangshibiao wangshibiao 55 11月 9 17:26 go.mod drwxrwxr-x 2 wangshibiao wangshibiao 4096 11月 9 17:22 i18n/ drwxrwxr-x 2 wangshibiao wangshibiao 4096 11月 9 17:27 .idea/ -rw-rw-r-- 1 wangshibiao wangshibiao 118 11月 9 17:22 main.go drwxrwxr-x 2 wangshibiao wangshibiao 4096 11月 9 17:22 packed/ drwxrwxr-x 5 wangshibiao wangshibiao 4096 11月 9 17:22 public/ -rw-rw-r-- 1 wangshibiao wangshibiao 39 11月 9 17:22 README.MD drwxrwxr-x 2 wangshibiao wangshibiao 4096 11月 9 17:22 router/ drwxrwxr-x 2 wangshibiao wangshibiao 4096 11月 9 17:22 template/ (py3.6) wangshibiao@wangshibiao:/data/workspace/github/go/private/ad$
# 18. 数据类型 {#_18-数据类型}
预定义了常用的数据类型:泛型、map、slice、slice map
# 19. 配置对象 {#_19-配置对象}
支持多种文件类型: config.json, cfg.yaml, cfg.xml, cfg.ini
默认的文件类型:toml
配置文件搜索目录:
- 支持添加搜索目录
按添加目录的顺序作为优先级搜索。 - 支持修改默认的搜索目录 有多种方法, 如下
-
通过配置管理器的 SetPath 方法手动修改
g.Cfg().SetPath("/opt/config")
-
修改命令行启动参数 - gf.gcfg.path
./main --gf.gcfg.path=/opt/config/
-
修改指定的环境变量 - GF_GCFG_PATH
GF_GCFG_PATH=/opt/config/; ./main
支持配置内容动态更新: 修改配置文件会动态加载配置对象
# 20. 日志管理 {#_20-日志管理}
# 20.1 CtxKeys 的用法? {#_20-1-ctxkeys-的用法}
# 20.2 支持滚动切分并压缩归档 {#_20-2-支持滚动切分并压缩归档}
支持按文件大小或过期时长切分
支持配置切分文件的保存数量
支持切分文件的过期时长,超出过期时间则自动删除切分文件
支持切分文件的压缩。
RotateSize: 单位字节,也支持字符串类型,如: "100M", "200MB", "500KB", "1Gib"等
RotateExpire: 单位秒,也支持字符串类型,如:"1h", "30m", "1d6h", "7d"等。
RotateBackupExpire 的单位同 RotateExpire。
# 20.3 支持以自动生成子目录的方式归类日志文件。 {#_20-3-支持以自动生成子目录的方式归类日志文件。}
# 20.4 配置方法 {#_20-4-配置方法}
下面围绕 2 个概念讲解日志的配置方法: 日志级别常量和日志级别字符串。
- 日志级别常量
只能
用于代码中
日志级别常量包括:
LEVEL_ALL
LEVEL_DEV
LEVEL_PROD
LEVEL_DEBU
LEVEL_INFO
LEVEL_NOTI
LEVEL_WARN
LEVEL_ERRO
LEVEL_CRIT
可以通过位操作组合使用这几种级别, 实现各种日志级别的打印需求,
,例如其中 LEVEL_ALL 等价于 LEVEL_DEBU | LEVEL_INFO | LEVEL_NOTI | LEVEL_WARN | LEVEL_ERRO | LEVEL_CRIT。我们还可以通过 LEVEL_ALL & ^LEVEL_DEBU & ^LEVEL_INFO & ^LEVEL_NOTI 来过滤掉 LEVEL_DEBU/LEVEL_INFO/LEVEL_NOTI 日志内容
l := glog.New()
l.SetLevel(glog.LEVEL_ALL^glog.LEVEL_INFO)
- 日志级别字符串
日志级别字符串其实就是预定义的常用的有限的若干日志级别的组合
用于代码或配置文件中
日志级别字符串包括:
"ALL": LEVEL_DEBU | LEVEL_INFO | LEVEL_NOTI | LEVEL_WARN | LEVEL_ERRO | LEVEL_CRIT,
"DEV": LEVEL_DEBU | LEVEL_INFO | LEVEL_NOTI | LEVEL_WARN | LEVEL_ERRO | LEVEL_CRIT,
"DEVELOP": LEVEL_DEBU | LEVEL_INFO | LEVEL_NOTI | LEVEL_WARN | LEVEL_ERRO | LEVEL_CRIT,
"PROD": LEVEL_WARN | LEVEL_ERRO | LEVEL_CRIT,
"PRODUCT": LEVEL_WARN | LEVEL_ERRO | LEVEL_CRIT,
"DEBU": LEVEL_DEBU | LEVEL_INFO | LEVEL_NOTI | LEVEL_WARN | LEVEL_ERRO | LEVEL_CRIT,
"DEBUG": LEVEL_DEBU | LEVEL_INFO | LEVEL_NOTI | LEVEL_WARN | LEVEL_ERRO | LEVEL_CRIT,
"INFO": LEVEL_INFO | LEVEL_NOTI | LEVEL_WARN | LEVEL_ERRO | LEVEL_CRIT,
"NOTI": LEVEL_NOTI | LEVEL_WARN | LEVEL_ERRO | LEVEL_CRIT,
"NOTICE": LEVEL_NOTI | LEVEL_WARN | LEVEL_ERRO | LEVEL_CRIT,
"WARN": LEVEL_WARN | LEVEL_ERRO | LEVEL_CRIT,
"WARNING": LEVEL_WARN | LEVEL_ERRO | LEVEL_CRIT,
"ERRO": LEVEL_ERRO | LEVEL_CRIT,
"ERROR": LEVEL_ERRO | LEVEL_CRIT,
"CRIT": LEVEL_CRIT,
"CRITICAL": LEVEL_CRIT,
可以看出,预定义这些日志级别字符串有个规则, 是按照日志级别优先级(DEBU < INFO < NOTI < WARN < ERRO < CRIT)定义的。 2 种方式的区别: 通过日志级别常量可以采用灵活组合来实现各种日志级别的打印需求,但是通过日志级别字符串只能使用有限> 的几种预定义的配置方式。
日志级别常量只能通过代码中配置,不支持配置文件方式
# 20.5 封装日志工具函数 {#_20-5-封装日志工具函数}
若想封装通用的日志函数,那么日志函数有 2 种实现方法。
-
返回 logger 对象
func Logger() *glog.Logger { return g.Log().Async().Line(true) }
-
若直接封装打印方法,那么需要调用一次 skip 方法,否则打印的文件位置不正确。
g.Log().Skip(1).Line(true).Info(content)
# 20.6 异步打印 {#_20-6-异步打印}
注意,要么都同步打印,要么都异步打印,要保持统一,同步方式和异步方式不能同时使用,否则打印出的日志会出现乱序。
# 20.7 json 日志 {#_20-7-json-日志}
支持打印 json 格式的日志。
给打印方法提供 map/struct 即可。
默认打印的 json 日志会有个头信息,建议禁用头信息(见配置文件的 HeaderPrint 配置项),这样每一行日志都是个合法的 json 对象,方便后续解析(推荐使用linux json 解析工具 jq
)。
如下是比较封装比较完善的日志工具函数,支持自动打印 requestId 等信息:
package log_util
import (
"context"
"github.com/gogf/gf/frame/g"
"github.com/gogf/gf/os/gtime"
)
/**
日志上下文: 用于跟踪用户行为
*/
type LogContext struct {
RequestId string `json:"requestId"`
}
/**
向参数detailMap追加context上下文数据(如requestId)
*/
func appendContextData(ctx *context.Context, detailMap *g.Map) {
logContext := &LogContext{}
logContext.RequestId = (*ctx).Value("requestId").(string)
(*detailMap)["context"] = logContext
}
/**
补充日志数据
*/
func appendLogData(ctx *context.Context, level string, pTitle string, sTitle string, detailMapList ...*g.Map) (detailMap *g.Map){
//若detailMapList没有传或为空, 则新创建
if len(detailMapList) > 0 {
detailMap = detailMapList[0]
}
if g.IsNil(detailMap) {
detailMap = &g.Map{}
}
//补充context数据
appendContextData(ctx, detailMap)
//补充其它信息
(*detailMap)["level"] = level
(*detailMap)["time"] = gtime.Datetime()
(*detailMap)["pTitle"] = pTitle
(*detailMap)["sTitle"] = sTitle
return detailMap
}
/**
打印Debug日志
pTitle: 父标题, 建议传函数的标题注释
sTitle: 子标题, 建议传当前日志的标题
*/
func Debug(ctx context.Context, pTitle string, sTitle string, detailMapList ...*g.Map) {
//补充日志数据
detailMap := appendLogData(&ctx, "debug", pTitle, sTitle, detailMapList...)
g.Log().Skip(1).Line(true).Debug(detailMap)
}
/**
打印Info日志
pTitle: 父标题, 建议传函数的标题注释
sTitle: 子标题, 建议传当前日志的标题
*/
func Info(ctx context.Context, pTitle string, sTitle string, detailMapList ...*g.Map) {
//补充日志数据
detailMap := appendLogData(&ctx, "info", pTitle, sTitle, detailMapList...)
g.Log().Skip(1).Line(true).Info(detailMap)
}
/**
打印Notice日志
pTitle: 父标题, 建议传函数的标题注释
sTitle: 子标题, 建议传当前日志的标题
*/
func Notice(ctx context.Context, pTitle string, sTitle string, detailMapList ...*g.Map) {
//补充日志数据
detailMap := appendLogData(&ctx, "notice", pTitle, sTitle, detailMapList...)
g.Log().Skip(1).Line(true).Notice(detailMap)
}
/**
打印Warning日志
pTitle: 父标题, 建议传函数的标题注释
sTitle: 子标题, 建议传当前日志的标题
*/
func Warning(ctx context.Context, pTitle string, sTitle string, detailMapList ...*g.Map) {
//补充日志数据
detailMap := appendLogData(&ctx, "warning", pTitle, sTitle, detailMapList...)
g.Log().Skip(1).Line(true).Warning(detailMap)
}
/**
打印Error日志
pTitle: 父标题, 建议传函数的标题注释
sTitle: 子标题, 建议传当前日志的标题
*/
func Error(ctx context.Context, pTitle string, sTitle string, detailMapList ...*g.Map) {
//补充日志数据
detailMap := appendLogData(&ctx, "error", pTitle, sTitle, detailMapList...)
g.Log().Skip(1).Line(true).Error(detailMap)
}
/**
打印Critical日志
pTitle: 父标题, 建议传函数的标题注释
sTitle: 子标题, 建议传当前日志的标题
*/
func Critical(ctx context.Context, pTitle string, sTitle string, detailMapList ...*g.Map) {
//补充日志数据
detailMap := appendLogData(&ctx, "critical", pTitle, sTitle, detailMapList...)
g.Log().Skip(1).Line(true).Critical(detailMap)
}
# 20.8 堆栈打印 {#_20-8-堆栈打印}
Notice*/Warning*/Error*/Critical*/Panic*/Fatal*等错误日志输出方法
会自动打印堆栈信息, 便于程序调试。
示例代码如下:
log_util.Logger().Error("我出错了")
log_util.Logger().Info("继续执行")
程序输出结果如下:
2020-11-12 13:11:46.706 [ERRO] /data/workspace/github/go/private/ad/app/api/estar_controller/estar.go:45: 我出错了
Stack:
1. ad/app/api/estar_controller.(*Estar).TrackEvent
/data/workspace/github/go/private/ad/app/api/estar_controller/estar.go:45
2. github.com/gogf/gf/net/ghttp.(*Middleware).Next.func1.6
/home/wangshibiao/go/pkg/mod/github.com/gogf/gf@v1.14.2/net/ghttp/ghttp_request_middleware.go:95
3. github.com/gogf/gf/net/ghttp.niceCallFunc
/home/wangshibiao/go/pkg/mod/github.com/gogf/gf@v1.14.2/net/ghttp/ghttp_func.go:89
4. github.com/gogf/gf/net/ghttp.(*Middleware).Next.func1
/home/wangshibiao/go/pkg/mod/github.com/gogf/gf@v1.14.2/net/ghttp/ghttp_request_middleware.go:94
5. github.com/gogf/gf/util/gutil.TryCatch
/home/wangshibiao/go/pkg/mod/github.com/gogf/gf@v1.14.2/util/gutil/gutil.go:40
6. github.com/gogf/gf/net/ghttp.(*Middleware).Next
/home/wangshibiao/go/pkg/mod/github.com/gogf/gf@v1.14.2/net/ghttp/ghttp_request_middleware.go:47
7. github.com/gogf/gf/net/ghttp.(*Server).ServeHTTP
/home/wangshibiao/go/pkg/mod/github.com/gogf/gf@v1.14.2/net/ghttp/ghttp_server_handler.go:118
2020-11-12 13:11:46.706 [INFO] /data/workspace/github/go/private/ad/app/api/estar_controller/estar.go:46: 继续执行
# 20.9 debug 日志开关 {#_20-9-debug-日志开关}
debug 日志仅用于调试使用,所以推荐做法是: 在开发或测试环境中开启 debug,生产环境中禁用 debug。
debug 开关共有 3 种配置方法:
-
- 通过代码
glog.SetDebug(false)
-
- 通过命令行参数
gf.glog.debug=false
- 通过命令行参数
-
- 通过环境变量
GF_GLOG_DEBUG=false
# 20.10 glog 包方法和 g.log()对象方法的区别 {#_20-10-glog-包方法和-g-log-对象方法的区别}
这 2 个对象的区别是?
通过测试发现, 配置文件不会对 glog 包方法,但是对 g.log()对象是生效的。
# 20.11 自定义日志内容处理逻辑 {#_20-11-自定义日志内容处理逻辑}
可以自定义日志内容处理逻辑,实现接口 Writer 即可。
可以实现将日志推送到第三方日志平台上。
通过测试发现一个问题 : 发送日志到第三方平台的同时,并没有
按日志配置文件的配置规则打印本地日志
# 20.12 缓存管理 {#_20-12-缓存管理}
- 框架提供了一个
内存类型的
缓存对象的实现:
-
获取数据时使用 get*包方法
-
过期时间取值
=0 表示不过期, < 0 表示立即过期 -
支持 LRU
支持 LRU,即最近最久未使用淘汰规则,使得系统始终保留热数据,扔掉冷数据。默认的
全局缓存对象实例不支持 LRU 策略,因为 LRU 队列的长度为 0 若想启用 LRU 算法,需要 new 一个新的缓存对象, 并指定队列长度。gcache.New(2)
-
提供默认的全局缓存对象实例
不支持 LRU,前面已提过。
- 支持 redis
gf 缓存定义了一套接口,只要实现该接口即可无痛切换任何形式的缓存实现。
将默认的内存缓存切换为 redis 方法详见gcache-adapter (opens new window) 可以将 orm 的默认内存缓存切换为 redis 缓存