什么是Context?
Context指的是标准库的context.Context,是一个接口对象,常用于异步IO控制以及上下文流程变量的传递。在GoFrame中Context主要是用来给协程之间共享数据。
Context有什么用?
GoFrame是网络应用开发框架,在网络应用中存在大量IO操作,使用Context可以减少IO操作,提高运行效率。
比如用户登录时,通常需要连接到数据库比对用户名和密码,另外Web应用中很多模块在使用前都要判断账户信息确认权限等级,即用户每次点击要先检验权限然后才能进行后续操作。使用Context可以将用户权限信息存储在内存中供所有Web应用使用,这样Web应用检验权限就不涉及IO,可大大提高运行效率。
再比如博客网站的栏目和文章清单、饭店的菜单、工厂的产品清单,这些信息在Web应用中非常多的模块都要获得它,此时也可以使用Context来缓存和传递这些信息。
总之在Web应用中大量改动不频繁、读取频繁的公共数据都可以使用Context缓存到内存中来提高运行效率。不过当系统用户非常多时,例如几万、几十万用户在线的系统,通常不再把登录信息缓存在Context中,而是使用Redis来缓存。
Context该怎么用?
在Goframe中数据描述、数据公用方法、业务逻辑要区分开,按先后顺序编写。
以下我们通过一个实战案例来详细了解Context的使用步骤,首选搭建好运行环境,然后通过命令行进入~/go/src目录,执行初始化项目和下载相关依赖,命令如下:
gf init contextdemo
cd contextdemo
go mod tidy
接下来用Goland打开contextdemo目录,跟着一步步做。
数据定义
即Context中存储的数据结构描述。文件存放路径app/api/model/context.go。
package model
const (
// ContextKey 上下文变量存储键名
ContextKey = "ContextKey"
)
// Context 请求上下文结构
type Context struct {
LoginName string // 登录用户名
Authority string // 用户权限
List []string // 产品清单
}
方法定义
即Context中数据的初始化、读取、修改及其他公用接口/方法。文件存放在app/api/service/context.go。
这里关于Context方法的定义是参照官方模板写的,请大家注意看该文件中定义了一个全局变量Context,它是contextService类型,后面的定义若干方法均是该类型可以调用的专属方法。这种写法的效果相当于Python中面向对象的单例模式!即在其它应用中要使用Context需要引入本文件使用Context这个变量!
package service
import (
"context"
"contextdemo/app/model"
"errors"
"github.com/gogf/gf/net/ghttp"
"io/ioutil"
"strings"
)
// Context 上下文管理服务
var Context = contextService{}
type contextService struct{}
// Init 初始化上下文对象指针到上下文对象中,以便后续的请求流程中可以修改。
func (s *contextService) Init(r *ghttp.Request, customCtx *model.Context) {
r.SetCtxVar(model.ContextKey, customCtx)
}
// Get 获得上下文变量,如果没有设置,那么返回nil
func (s *contextService) Get(ctx context.Context) *model.Context {
value := ctx.Value(model.ContextKey)
if value == nil {
return nil
}
if localCtx, ok := value.(*model.Context); ok {
return localCtx
}
return nil
}
// SetUser 将上下文信息设置到上下文请求中,注意是完整覆盖
func (s *contextService) SetUser(ctx context.Context, name string) error {
save := s.Get(ctx)
if save == nil {
return errors.New("context信息获取失败")
}
users := readUser()
if _, ok := users[name]; ok {
save.LoginName = name
save.Authority = users[name]
} else {
return errors.New("用户不存在")
}
save.List = readProduct()
return nil
}
// 模拟读取用户信息,文件路径请根据运行环境自行修改。
func readUser() map[string]string {
f, err := ioutil.ReadFile("/home/windf/go/src/contextdemo/app/model/users.txt")
if err != nil {
return nil
}
userList := strings.Split(string(f), "\n")
list := make(map[string]string)
for _, i := range userList {
line := strings.Fields(i)
list[line[0]] = line[1]
}
return list
}
// 模拟读取产品信息,文件路径请根据运行环境自行修改。
func readProduct() []string {
f, err := ioutil.ReadFile("/home/windf/go/src/contextdemo/app/model/list.txt")
if err != nil {
return nil
}
list := strings.Split(string(f), "\n")
return list
}
// 模拟登录
var loginName = "张三"
func login() string {
return loginName
}
// ListTable 获取context中的产品列表
func (s *contextService) ListTable(ctx context.Context) []string {
if v := Context.Get(ctx); v != nil {
return v.List
}
return nil
}
// AuthZero 更改context中的用户权限
func (s *contextService) AuthZero(ctx context.Context) {
if v := Context.Get(ctx); v != nil {
v.Authority = "0"
}
}
备注:
上述代码中有2个文本文件,请根据运行环境自行修改路径。另文件内容如下,
list.txt
鼠标
键盘
显示器
音箱
users.txt
张三 1
李四 2
王五 3
中间件定义
Context中间件是为了完成Context中数据的初始化与传递。文件存放在app/api/service/middleware.go。
package service
import (
"contextdemo/app/model"
"fmt"
"github.com/gogf/gf/frame/g"
"github.com/gogf/gf/net/ghttp"
)
// Middleware 中间件管理服务
var Middleware = middlewareService{}
type middlewareService struct{}
// Ctx 自定义上下文对象
func (s *middlewareService) Ctx(r *ghttp.Request) {
// 初始化,务必最开始执行
name := login()
users:=readUser()
if _, ok := users[name]; ok {
customCtx := model.Context{
LoginName: name,
Authority: users[name],
List: readProduct(),
}
Context.Init(r, &customCtx)
// 给模板传递上下文对象中的键值对
r.Assigns(g.Map{
"user":customCtx.LoginName,
"auth":customCtx.Authority,
"list":customCtx.List,
})
}else{
fmt.Println("用户不存在,请重新登录") // 此处是为了演示做简化,正常情况应该在登录验证函数做好处理
}
// 执行后续中间件
r.Middleware.Next()
}
中间件注册
中间件必须注册到对应的router里才能生效。文件存放在router/router.go。
package router
import (
"context/app/api"
"context/app/service"
"github.com/gogf/gf/frame/g"
"github.com/gogf/gf/net/ghttp"
)
func init() {
s := g.Server()
s.Group("/", func(group *ghttp.RouterGroup) {
group.Middleware(service.Middleware.Ctx) // 注册context相关中间件
group.ALL("/hello", api.Hello)
})
}
在应用中使用Context
模板文件中使用Context信息
注意,这个要在中间件中注册r.Assigns,将中间件中的变量注册到模板文件可访问的变量列表中。
编写模板文件hello.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
用户:{{.user}}<br>
权限:{{.auth}}<br>
产品:{{.list}}
</body>
</html>
在应用中读取Context信息
读取方式有2种,请看代码:
package api
import (
"contextdemo/app/service"
"fmt"
"github.com/gogf/gf/frame/g"
"github.com/gogf/gf/net/ghttp"
)
var Hello = helloApi{}
type helloApi struct {}
// Index is a demonstration route handler for output "Hello World!".
func (*helloApi) Index(r *ghttp.Request) {
list1 := r.GetCtxVar("ContextKey").Map()["List"] // 此方法可以获得Context中的数据
list2 :=service.Context.ListTable(r.Context()) // 也可以通过自定义的方法来提取所需数据
fmt.Println(list1)
fmt.Println(list2)
service.Context.AuthZero(r.Context()) // 调用方法更改context中的用户权限
auth := r.GetCtxVar("ContextKey").Map()["Authority"]
fmt.Println(auth) // 注意,此时虽然auth值修改了,但未执行r.Assigns模板中是不会生效的
r.Assigns(g.Map{
"user":r.GetCtxVar("ContextKey").Map()["LoginName"],
"auth":r.GetCtxVar("ContextKey").Map()["Authority"],
"list":r.GetCtxVar("ContextKey").Map()["List"],
}) // 执行r.Assigns方法后模板文件中会生效
err := r.Response.WriteTpl("hello.html")
if err != nil {
r.Response.Writeln(err)
return
}
}
在应用中修改Context信息
通常涉及到Context信息的修改方法会写在service/context.go中,一般不在应用中直接修改Context信息,建议通过调用service对应的方法来修改信息。
补充
上述代码编辑完毕后,编译执行时若遇到如下报错:
go: github.com/gogf/gf@v1.16.4: missing go.sum entry; to add it:
go mod download github.com/gogf/gf
请参照提示在命令行执行对应的指令,如上面的提示应该执行go mod download github.com/gogf/gf。
若遇到下图中的错误,亦是如此:
请参看报错信息依次在命令行中执行相应的语句:
go get github.com/gogf/gf/encoding/gyaml@v1.16.4
go get github.com/gogf/gf/encoding/gcharset@v1.16.4
go get github.com/gogf/gf/net/ghttp/internal/client@v1.16.4
go get github.com/gogf/gf/net/ghttp@v1.16.4
go get github.com/gogf/gf/net/gtrace@v1.16.4
go get github.com/gogf/gf/encoding/ghtml@v1.16.4
go get github.com/gogf/gf/database/gdb@v1.16.4
go get github.com/gogf/gf/os/gfsnotify@v1.16.4
注意:以上报错,可以进入项目路径后执行go mod tidy命令来解决,一条命令搞定更方便!
收尾
访问http://127.0.0.1:8199/hello,可以看到如下图信息,表示context数据已成功获取:
关于Context数据安全的问题
在Context中可以存放多个web应用公用的数据和信息,但要注意数据安全问题!!!
下面举一个关于数据安全的例子:
例如,在Context增加一个统计在线人数的Count字段。
type Context struct {
LoginName string // 登录用户名
Authority string // 用户权限
List []string // 产品清单
Count int // 统计在线人数
}
当某用户登录时Count +1、退出时Count -1。这样的逻辑没有错!但运行起来会发生Count计数错误的情况!
假设:
当A用户登录时,此时假设Count当前值是1000,读取到1000后加1的结果是1001,再将1001存回Count;
此时B用户也在登录,它读取到Count值也是1000,读取到1000后加1的结果也是1001,再将1001存回Count。
这就产生了Count应该是1002而结果是1001的情况!!!
所以绝对不要有从Context中取数据计算再存Context的操作!!!