两个方式管理基础服务连接
2.0 管理数据连接,两个方式
这里简单介绍管理连接的两个方式。
我们建立这个缓存的链接管理对象,假如我们已经实现了客户端对象redis.client
type Caches struct {
Client *redis.Client
}
func (db *Caches) GetConn() (*redis.Conn, context.Context) {
return db.Client.Conn(Ctx), Ctx
}
当然,我们还可以把sql数据库也一并集中在一起, 全部数据库
type DBMangers struct {
DBPgsql *pgxpool.Pool
DBRedis *Caches
}
2.0.1 方式一 这里我们直接为缓存操作提供基本方法
type RedisOp struct {
*Caches
}
func NewRedisOp(nc *Caches) *RedisOp {
newCash := RedisOp{nc}
return &newCash
}
然后我们提供方法应对基础指令 SET,GET,DEL
func (db *RedisOp) Set(Ctx context.Context, key string, value string) error {
err := db.Client.Set(Ctx, key, value, 0).Err()
if err != nil {
log.Panic(err)
}
log.Println(key, value, " set success exist")
return nil
}
func (db *RedisOp) Get(Ctx context.Context,key string) string {
val, err := db.Client.Get(Ctx, key).Result()
if err == redis.Nil {
log.Println(key, " does not exist")
log.Panic(err)
return ""
} else if err != nil {
panic(err)
} else {
log.Println(key, " : ", val)
}
return fmt.Sprintf("%v", val)
}
func (db *RedisOp) Del(Ctx context.Context,key string) error {
err := db.Client.Del(Ctx, key).Err()
if err != nil {
log.Panic(err)
}
log.Println(key, " del success ")
return nil
}
这种方式是直接对redis 的操作进行的封装,它让用户提供指令和内容,最终执行,如果失败则直接退出。
也可以使用一个统一入口 Do,对任意的key操作进行分别操作,可以做某些优化。
func (db *RedisOp) Do(Ctx context.Context,key string, value string) error {
if key == "set" {
return db.Set(Ctx,key , value )
} else if key == "get" {
return db.Get(Ctx,key , value )
} else if key == "del" {
return db.Del(Ctx,key , value )
}
msg := fmt.Errorf( " key:%v not support.",key)
return msg
}
2.1 方式二 使用接口
一种数据类型即接口,它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口
interface 是一种类型,从它的定义可以看出来用了 type 关键字,更准确的说 interface 是一种具有一组方法的类型,这些方法定义了 interface 的行为
interface可以是一组method的集合,是duck-type programming的一种体现(不关心属性(数据),只关心行为(方法))。
我们可以自己定义interface类型的struct,并提供方法。
go 允许不带任何方法的 interface ,这种类型的 interface 叫 empty interface。
空接口 interface{}
本身可以表示任何类型,因此它其实就是一个全量的泛型,我们可以结合反射在运行时对实际传入的参数做类型检查,让泛型变得可控,从而确保程序的健壮性,否则很容易因为传递进来的参数类型不合法导致程序崩溃。
如果一个类型实现了一个 interface 中所有方法,必须是所有的方法,我们说类型实现了该 interface,所以所有类型都实现了 empty interface,因为任何一种类型至少实现了 0 个方法。
go 没有显式的关键字用来实现 interface,只需要实现 interface 包含的方法即可。
interface还可以作为返回值使用。 判断interface变量存储的是哪种类型。 比如下:
type HashTable interface {
Write(p []byte) (n int, err error)
NewSum(b []byte) []byte
}
//使用基础类
func NewHashTable() HashTable {
return &Base{}
}
//使用子类
func NewHashWriter() HashTable {
return &ChWriter{}
}
使用基类可以使用全部函数 和 组合的类的函数
//使用Reader子类 全部方法
func NewReader() Reader {
return &ChReader{}
}
type error interface {
Error() string
}
type ChReader struct{ Base }
func (b *ChReader) Read(p []byte) (n int, err error) { return 2, nil }
func (b *ChReader) ReportError() string { return "err" }
使用接口,如果方法没有封装入接口,则不能使用全部函数,如反馈错误
//使用接口只有子类函数,
fmt.Println(r1.Read([]byte{}))
//读者不能反馈问题
//fmt.Println(r1.ReportError())
使用基类可以反应全部错误
fmt.Println("struct of Reader:")
//使用本类可以使用全部函数 和 组合的类的函数
r2 := ChReader{}
//读者反应有错误
fmt.Println(r2.ReportError())
fmt.Println(r2.NewSum([]byte{}))
2.1.1 使用接口提供对外的业务方法
日常中使用interface,有时候需要判断原来是什么类型的值转成了interface。一般如下方式:
这里的接口是一组方法定义,任意实现了这一组方法的结构体都可以称其为对其的实现。
type HashTable interface {
Write(p []byte) (n int, err error)
NewSum(b []byte) []byte
}
一个类型通常会满足许多接口,每个接口对应于它的方法一个子集
比如任何满足接口的类型HashTable也满足Writer接口
type Writer interface {
Write(p []byte) (n int, err error)
}
go的系统各个部分以交互方式随着它的增长而适应。
如下Reader接口,Write任何具有此方法的项目都可以与Reader接口互补一起使用:
type Reader interface {
Read(p []byte) (n int, err error)
}
go 没有专门的错误处理语句 try except
需要预定义一个接口类型 error,它表示一个具有Error返回字符串的方法的值
type error interface {
Error() string
}
我们打算对业务逻辑进行部分保留和选择性的暴露,使得用户可以知道他们正在做什么事情。
type cacheRepo struct {
Client *Caches
}
var Ctx = context.Background()
type CacheRepo interface {
//基础操作
Set(key, value string, ttl time.Duration, options ...Option) error
Get(key string, options ...Option) (string, error)
MGet(keys []string, options ...Option) ([]interface{}, error)
TTL(key string) (time.Duration, error)
Expire(key string, ttl time.Duration) bool
ExpireAt(key string, ttl time.Time) bool
//有序集操作 创建操作的流水线管道等
ZaddArgs(k string, kv string, options ...Option) ([]string, error)
PipelinedCreate(priKeys string, ex map[string]string, options ...Option) ([]any, error)
ZRange(k string, start, end int64, options ...Option) ([]string, error)
ZCard(k string, options ...Option) (int64, error)
ZRangeScoreByindex(k string, start, end int64, options ...Option) ([]redis.Z, error)
}
在具体函数调用执行时,我们跟踪错误并回溯它们
if opt.Trace != nil {
opt.Redis.Timestamp = timeutil.CSTLayoutString()
opt.Redis.Handle = "set"
opt.Redis.Key = key
opt.Redis.Value = value
opt.Redis.TTL = ttl.Minutes()
opt.Redis.CostSeconds = time.Since(ts).Seconds()
opt.Trace.AppendRedis(opt.Redis)
}
然后调用内部方法去执行
if err := c.Client.set(Ctx, key, value, ttl).Err(); err != nil {
return errors.Wrapf(err, "redis set key: %s err", key)
}
...
如果从第一章开始看到现在的朋友,一定不会陌生这个方式.
3 小结
实际使用时有很多特殊问题需要处理,比如如何分离大键,如何模拟关系数据的数据管理方式。也有异常问题 如缓存值批量过期和查询数据不存在的问题。 实际解决方案如只保留单向通道,从db到缓存,使用不同的过期方式。
有兴趣的可以看到示例
https://github.com/hahamx/examples/blob/main/tcps/3_with_tcp_service/with_interface.go
- 点赞
- 收藏
- 关注作者
评论(0)