关于GoFrame框架中Context相关梳理及实例

什么是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的操作!!!

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值