golang 框架 GoFrame 重要知识点整理

本文介绍 golang 框架 GoFrame 的重要知识点。本文不会前面介绍 goframe,仅罗列该框架的特性以及重要的知识点。

# 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.1 按参数类型获取参数

Get*Struct 和 GetBody/GetBodyString 的区别
接口参数中有一种特殊的参数: 自定义参数,往往在服务端的中间件、服务函数中通过 SetParam/GetParam 方法管理

Get*(或 GetRequset*)方法存在优先级:Router < Query < Body < Form < Custom GetQuery*方法存在优先级: Query > Body

# 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 请求校验

*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 自定义变量

开发者可以在请求中自定义一些变量设置,自定义变量的获取优先级是最高的,可以覆盖原有的客户端提交参数。

自定义变量可以通过 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 上下文变量

上下文变量自定义变量的区别:自定义变量会覆盖接口参数,而上下文变量不会覆盖。

# 3. 请求输出

# 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

GF 框架的 Session 默认过期时间是 24 小时

SessionId 默认通过 Cookie 来传递,并且也支持客户端通过 Header 传递 SessionId,SessionId 的识别名称可以通过 ghttp.Server 的 SetSessionIdName 进行修改。
ghttp.Server 中的 SessionId 使用的是客户端的 RemoteAddr + Header 请求信息通过 guid 模块来生成的,保证随机及唯一性

    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. 配置管理

2 种配置方式: 代码或配置文件
服务启动后不允许修改配置
用户提交的数据大小限制的配置项:

[server]
    MaxHeaderBytes    = "20KB"
    ClientMaxBodySize = "200MB"

# 6. 全局异常处理

默认情况下,全局异常会记录到日志中。
开发者也可以自定义异常处理逻辑。异常信息的获取: Request 对象中的 GetError 方法

即使开发者有自己捕获记录异常错误的日志,但是Server依旧会打印到Server自己的错误日志文件中

# 7. HTTP 客户端

g.Client()
*Bytes*Content
*Bytes*Content
*Var

struct 对象作为 json 输入参数的处理方法:
若输入参数为 struct,默认的 content-type 是表单方式,但不符合我们的预期。

  1. 显性指定 content-type 为 application/json
g.Client().SetHeader("Content-Type", "application/json").PostVar("https://fk.estar.net.cn:3887/my/adDeviceThirdBegin", reqGetEstarVideoAd).Scan(&respGetEstarVideoAd)
  1. 将 struct 转为 json 字符串
	para, _ := jsoniter.MarshalToString(reqGetEstarVideoAd)
	g.Client().PostVar("https://fk.estar.net.cn:3887/my/adDeviceThirdBegin", para).Scan(&respGetEstarVideoAd)

# 8. 静态文件服务器

支持按路由自定义发布目录(有优先级规则)。
支持和 rewrite。

# 9. 日志

分为 2 类日志:

  • server 日志
    类似于 nginx 的日志,属于请求日志,包括 access.log 和 error.log, 可以自定义日志格式,但无法控制日志的内容
  • 业务日志

# 10. 跨域

Allowdomain 和 alloworigin 区别是?
不执行非法的跨域请求: 默认情况下,服务端收到了非法跨域的请求,也会执行接口逻辑。若期望不执行非法跨域请求的接口逻辑, 则额外补充个 CORSAllowedOrigin 方法判断即可。

# 11. 同时支持 http 和 https

gf 本身支持 https, 若不想在程序端支持 https,也可以在 nginx 端支持 https。

  • 在本地生成测试证书
    可以在本地生成测试证书

# 12. Websocket

默认支持跨域访问,若需做跨域控制则需要自定义实现逻辑。

# 13. 自定义状态码处理

等同于同后置中间件,为了处理方便而专门定义的一组处理器。
在自定义状态码处理方法中如果涉及到内容的输出,往往需要使用 r.Response.ClearBuffer()方法将原本缓冲区的输出内容清空。

# 14. 热重启

重启程序而服务不中断。
默认关闭,可通过配置文件开启。
会额外占用本地的 10000 端口
版本更新时很有用
可以通过管理页面或命令行进行重启或关闭
支持自定义管理页面(对应一个管理接口)的路由

通过命令行:

kill -SIGUSR1 进程IDkill -SIGTERM 进程ID

热更新步骤:

更新程序时,无法直接覆盖正在运行的程序。

  1. rm 旧程序
  2. 更新程序
  3. 通过管理页面或命令行热重启

新程序接管后的处理逻辑是对当前未完成的请求重新处理还是从当前逻辑位置继续处理: 重新处理。

# 15. 性能分析

支持自定义性能分析页面的路由

# 16. 事件回调

类似中间件功能,但更简单
同一个路由支持绑定多个回调
按照路由优先级及回调注册顺序进行回调方法调用

中间件与事件回调的区别:

  • 全局中间件无法拦截静态文件的路由,但全局事件回调可以
  • 事件回调中的路由对象是空,只能取得 url 地址
  • 事件回调拦截粒度更小

# 17. GF 工具链

# 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 初始化工程

  1. 创建项目根目录
  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. 数据类型

预定义了常用的数据类型:泛型、map、slice、slice map

# 19. 配置对象

支持多种文件类型: config.json, cfg.yaml, cfg.xml, cfg.ini
默认的文件类型:toml
配置文件搜索目录:

  • 支持添加搜索目录
    按添加目录的顺序作为优先级搜索。
  • 支持修改默认的搜索目录 有多种方法, 如下
  1. 通过配置管理器的 SetPath 方法手动修改
g.Cfg().SetPath("/opt/config")
  1. 修改命令行启动参数 - gf.gcfg.path
 ./main --gf.gcfg.path=/opt/config/
  1. 修改指定的环境变量 - GF_GCFG_PATH
GF_GCFG_PATH=/opt/config/; ./main

支持配置内容动态更新: 修改配置文件会动态加载配置对象

# 20. 日志管理

# 20.1 CtxKeys 的用法?

# 20.2 支持滚动切分并压缩归档

切分文件

# 20.3 支持以自动生成子目录的方式归类日志文件。

# 20.4 配置方法

下面围绕 2 个概念讲解日志的配置方法: 日志级别常量和日志级别字符串。

  • 日志级别常量
只能
LEVEL_ALL
LEVEL_DEV
LEVEL_PROD
LEVEL_DEBU
LEVEL_INFO
LEVEL_NOTI
LEVEL_WARN
LEVEL_ERRO
LEVEL_CRIT
可以通过位操作组合使用这几种级别, 实现各种日志级别的打印需求,
    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 封装日志工具函数

若想封装通用的日志函数,那么日志函数有 2 种实现方法。

  1. 返回 logger 对象
func Logger() *glog.Logger {
	return g.Log().Async().Line(true)
}
  1. 若直接封装打印方法,那么需要调用一次 skip 方法,否则打印的文件位置不正确。
g.Log().Skip(1).Line(true).Info(content)

# 20.6 异步打印

注意,要么都同步打印,要么都异步打印,要保持统一,同步方式和异步方式不能同时使用,否则打印出的日志会出现乱序。

# 20.7 json 日志

linux json 解析工具 jq
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 堆栈打印

错误日志输出方法

示例代码如下:

	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 日志开关

debug 日志仅用于调试使用,所以推荐做法是: 在开发或测试环境中开启 debug,生产环境中禁用 debug。
debug 开关共有 3 种配置方法:

    1. 通过代码
glog.SetDebug(false)
gf.glog.debug=false
GF_GLOG_DEBUG=false

# 20.10 glog 包方法和 g.log()对象方法的区别

这 2 个对象的区别是?
通过测试发现, 配置文件不会对 glog 包方法,但是对 g.log()对象是生效的。

# 20.11 自定义日志内容处理逻辑

可以自定义日志内容处理逻辑,实现接口 Writer 即可。
可以实现将日志推送到第三方日志平台上。

按日志配置文件的配置规则打印本地日志

# 20.12 缓存管理

内存类型的
gcache.New(2)