前言
近期在学习Go相关知识体系,并且尝试写一些API,尽管已经学习过GO相关知识,并且可以照猫画虎,在真正设计组织代码时,感觉还是有所欠缺。 比如何时指针、何时用对象会有所犹豫。最终决定研究一些简单的框架,看他们是怎么组织代码的。
介绍的源码是 Go中gin框架session管理,下面是demo,看上去也是不复杂的。
代码地址是:github.com/gin-contrib/sessions/redis 与 github.com/boj/redistore 。
Sessions :用来对请求Session 的crud进行定义,并不关心数据是怎么存的。具体的实现是由下一级的redis包负责实现,当然下一级可能是其他的数据库。
redistore is A session store backend for gorilla/sessions
简单Demo
package main
import (
"fmt"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/redis"
"github.com/gin-gonic/gin"
"time"
)
func Logger() gin.HandlerFunc {
return func(context *gin.Context) {
host := context.Request.Host
url := context.Request.URL
method := context.Request.Method
fmt.Printf("%s::%s \t %s \t %s\n ", time.Now(), host, url, method)
context.Next()
fmt.Printf("request end: %v\n", context.Writer.Status())
}
}
func TestMiddleware() gin.HandlerFunc {
return func(context *gin.Context) {
fmt.Printf("TestMiddleware before.\n")
context.Next()
fmt.Printf("TestMIddleware end.\n")
}
}
func main() {
r := gin.Default()
// 使用了上面定义的中间件
r.Use(Logger(), TestMiddleware())
// 初始化基于redis的存储引擎
// 参数说明:
// 第1个参数 - redis最大的空闲连接数
// 第2个参数 - 数通信协议tcp或者udp
// 第3个参数 - redis地址, 格式,host:port
// 第4个参数 - redis密码
// 第5个参数 - session加密密钥
//这部分是我们后边要重点去探索的。 可以暂时不去理解
store, _ := redis.NewStore(10, "tcp", "localhost:6379", "", []byte(""))
r.Use(sessions.Sessions("mysession", store))
r.GET("/incr", func(c *gin.Context) {
session := sessions.Default(c)
var count int
v := session.Get("count")
if v == nil {
count = 0
} else {
count = v.(int)
count++
}
session.Set("count", count)
session.Save()
c.JSON(200, gin.H{"count": count})
})
r.Run(":8000")
}
上面代码输出为:
2023-02-17 08:43:42.137111 +0800 CST m=+3.086237661::localhost:8000 /incr GET
TestMiddleware before.
TestMIddleware end.
request end: 200
Go的中间件类似于Node.js 中koa框架洋葱模型。 context.Next() 之前的代码先被执行,之后的后被执行。
具体的编写方法以,可以参照上面Demo进行测试。
巨人肩膀
从sessions库的主要依赖开始介绍,主要用到下面几个:
1.Redis 操作库(缓存数据库Redis基本使用方法)
这部门我们先学习github.com/gomodule/redigo API方法,如果精力允许也可以看看里面实现,一般上这些库代码会比之上代码质量高一点。
API文档地址:pkg.go.dev/github.com/…
2.securecookie(对cookie进行加解密操作)
库从12年开始第一行代码,作者目前已经不维护。 gorilla/securecookie提供了一种安全的 cookie,通过在服务端给 cookie 加密,让其内容不可读,也不可伪造。当然,敏感信息还是强烈建议不要放在 cookie 中。
3.gorilla/sessions (对session进行维护管理库)
提供了基于 cookie 和本地文件系统的 session。同时预留扩展接口,可以使用其它的后端存储 session 数据。
包UML图
下面是session部分package主要UML图(go中以文件路径作为package,用UML表示有些时候有些不准确,可模块的依赖关系还是很清晰表达的)
主要代码
redis类是由 github.com/gin-contrib/sessions/redis 引入,其中还可以使用其他数据库类型作为session存储。
关于 github.com/gin-contrib/sessions/redis 源码分析。本质上是对Session中Store接口的实现。
package redis
import (
"errors"
"github.com/boj/redistore"
"github.com/gin-contrib/sessions"
"github.com/gomodule/redigo/redis"
)
type Store interface {
sessions.Store
}
// size: redis链接的最大空闲数 network: 链接类型 tcp/udp, address: 链接地址 host:port, password: 密码redis-password
// 创建入口,其调用了 另一个库中 boj/redistore中的方法
func NewStore(size int, network, address, password string, keyPairs ...[]byte) (Store, error) {
s, err := redistore.NewRediStore(size, network, address, password, keyPairs...)
if err != nil {
return nil, err
}
return &store{s}, nil
}
// NewStoreWithDB - like NewStore but accepts `DB` parameter to select
// redis DB instead of using the default one ("0")
//
// Ref: https://godoc.org/github.com/boj/redistore#NewRediStoreWithDB
func NewStoreWithDB(size int, network, address, password, DB string, keyPairs ...[]byte) (Store, error) {
s, err := redistore.NewRediStoreWithDB(size, network, address, password, DB, keyPairs...)
if err != nil {
return nil, err
}
return &store{s}, nil
}
// NewStoreWithPool instantiates a RediStore with a *redis.Pool passed in.
//
// Ref: https://godoc.org/github.com/boj/redistore#NewRediStoreWithPool
func NewStoreWithPool(pool *redis.Pool, keyPairs ...[]byte) (Store, error) {
s, err := redistore.NewRediStoreWithPool(pool, keyPairs...)
if err != nil {
return nil, err
}
return &store{s}, nil
}
type store struct {
*redistore.RediStore
}
// GetRedisStore get the actual woking store.
// Ref: https://godoc.org/github.com/boj/redistore#RediStore
func GetRedisStore(s Store) (err error, rediStore *redistore.RediStore) {
realStore, ok := s.(*store)
if !ok {
err = errors.New("unable to get the redis store: Store isn't *store")
return
}
rediStore = realStore.RediStore
return
}
// SetKeyPrefix sets the key prefix in the redis database.
func SetKeyPrefix(s Store, prefix string) error {
err, rediStore := GetRedisStore(s)
if err != nil {
return err
}
rediStore.SetKeyPrefix(prefix)
return nil
}
func (c *store) Options(options sessions.Options) {
c.RediStore.Options = options.ToGorillaOptions()
}
- 外部首先调用(
redisSotre仅仅 是sessions下的一种缓存存储方法,因此目录是子父关系)
store, \_ := redis.NewStore(10, "tcp", "localhost:6379", "", \[]byte(""))
注意这个函数定义的返回值 Store 接口:
func NewStore(size int, network, address, password string, keyPairs ...\[]byte) (Store, error)
- 关于Store Interface 定义 【这里比较绕,也比较关键,说明了作者设计的思路】
//redis store中 Store接口定义
type Store interface {
sessions.Store // sessions 是上一级中session
}
//上一级session中定义
type Store interface {
sessions.Store // Get, New, Save 接口来自 gorilla/session 中store 接口
Options(Options) // cookie配置项 // 在seesion中定义的cookie配置
}
// gorilla/session 中store
// See CookieStore and FilesystemStore for examples.
type Store interface {
// Get should return a cached session.
Get(r *http.Request, name string) (*Session, error)
// New should create and return a new session.
//
// Note that New should never return a nil session, even in the case of
// an error if using the Registry infrastructure to cache the session.
New(r *http.Request, name string) (*Session, error)
// Save should persist session to the underlying store implementation.
Save(r *http.Request, w http.ResponseWriter, s *Session) error
}
- 实际上的返回值 是通过 对 RedisStore 类实例化来实现的。 ReidsStore实现了Store中的基本方法。
目前一个这样的Store,已经有了数据库连接池 redis.pool , 实现了将请求传入 并返回 Session对象的GET方法。
- 完成gin框架的中间件功能
r.Use(sessions.Sessions("mysession", store))
这个时候session相当是对Sotore的管理。详细的类设计见上图中 session 结构体设计。 简单介绍 Session是他的接口实现约束。 session中相当于把strore引用作为结构体一部分引入对象中使用。进而去操作Stroe对象。
- 在请求中使用
func Default(c *gin.Context) Session 在上下文中使用。
r.GET("/incr", func(c *gin.Context) {
session := sessions.Default(c)
var count int
v := session.Get("count")
})
从上下文中取得session对象,这样业务逻辑中就可以对session进行处理了。
相关知识
-
strcut 和 interface 区别
zhuanlan.zhihu.com/p/341353253
Interface是编程中的另一个强大概念。 Interface与struct类似,但只包含一些抽象方法。 在Go中,Interface定义了通用行为的抽象。
- go中什么时候用对象、什么时候用指针?
- new(bytes.Buffer) 方法使用
bytes.Buffer 是 Golang 标准库中的缓冲区,具有读写方法和可变大小的字节存储功能。缓冲区的零值是一个待使用的空缓冲区。 new之后相当于是返回了一个buff变量。
gob是golang包自带的一个数据结构序列化的编码/解码工具。
gob的目的是什么?应用场景? 让数据结构能够在网络上传输或能够保存到文件中。gob可以通过json或gob来序列化struct对象,虽然json的序列化更为通用,但利用gob编码可以实现json所不能支持的struct方法序列化。 但是gob是golang提供的“私有”的编码方式,go服务之间通信可以使用gob传输。 一种典型的应用场景就是RPC。程序之间相互通信的。
总结思考
1.思考方式差异
一般上我写 功能逻辑,更喜欢从简单功能实现写起,然后再去思考向复杂功能写。例如实现一个简单的web服务: 先直接用gin实现一个简单api,然后 +session,然后+长链接,然后+upload,然后+数据库操作,然后+缓存等。 然后一系列操作。这种方式很容易陷入到面向过程中,导致越写越复杂。 或者说在后边再做简单的继承抽象。
源代码中学习中,看到这样的特点,大量通过Interface 用来约来资源方法。尤其是 sessions 和 sessions/redis 直接的依赖与定义,以及 resdisStore中 对redis数据查询封装。 封装让模块与模块之间的功能划分很清晰。 面向接口编程观念很强,如何锻炼这样的意识,感觉还需大量 阅读思考写代码。
- 学到知识点
- 代理模式要注意熟悉多用。一般方法都是委托给类自己去处理,然后用的时候去实例化类,但是在redisStroe中将seesion作为参数进行操作。
- 其他的与Go基础相关知识点(比较重要的在上文已经泄漏)。