通过go 语言访问redis

主要记录的功能点

  • 配置解析
  • 连接redis
  • 单侧

配置模块

  1. 定义配置结构体
package config
type Configuration struct {
	// +++++++++++++++日志相关+++++++++++++++++
	// 日志级别,这里使用了 beego 的 log 包
	Log *LogConfigure `yaml:"log"`
	
	// 服务端消息chan长度
	ServerMsgChanBufferLength int `yaml:"server-msgchan-bufferlength"`
	ServerConnTimeout int `yaml:"server-conn-timeout"`
	//proxy agent读写server超时时间,单位秒
	ServerRWTimeout int `yaml:"server-rw-timeout"`
}
// 定义嵌套的块级配置
type LogConfigure struct{
	// [0:Emergency, 1:Alert, 2:Critical, 3:Error, 4:Warning, 5:Notice, 6:Informational, 7:Debug]
	LogLevel int `yaml:"log-level"`
	// 日志输出位置,默认日志输出到控制台
	// 目前只支持['console', 'file']两种形式,如非console形式这里需要指定文件的路径,可以是相对路径
	LogOutput string `yaml:"log-output"`
	// 日志最大保留天数
	LogMaxDays int `yaml:"log-maxdays"`
}
// 初始化默认值
var Config = &Configuration{
	ServerMsgChanBufferLength 10,
	ServerConnTimeout 1,
	ServerRWTimeout  1,
}
  1. 定义参数文件
log:
	# 日志级别,默认为3:Error
	# [0:Emergency, 1:Alert, 2:Critical, 3:Error, 4:Warning, 5:Notice, 6:Informational, 7:Debug]
	log-level: 7
	# 日志最大保留天数
	log-maxdays: 7
	# 日志输出位置,默认日志输出到控制台
	# 目前只支持['console', 'file']两种形式,如非console形式这里需要指定文件的路径,可以是相对路径
	log-output: log/redis-agent.log
server-msgchan-bufferlength: 3
# 连接server超时时间,单位秒
server-conn-timeout: 5
# 读写server超时时间,单位秒
server-rw-timeout: 5
  1. 解析参数文件,将配置绑定到结构体
// 加载配置文件
func (conf *Configuration) readConfigFile(path string) error {
	configFile, err := os.Open(path)
	if err != nil {
		os.Stderr.WriteString(fmt.Sprintf("readConfigFile(%s) os.Open failed: %v", path, err))
		return err
	}
	defer configFile.Close()
	content, err := ioutil.ReadAll(configFile)
	if err != nil {
		os.Stderr.WriteString(fmt.Sprintf("readConfigFile(%s) ioutil.ReadAll failed: %v", path, err))
		return err
	}
	// 解析byte编码的数据并将结果存入Config指向的值
	err = yaml.Unmarshal(content, Config)
	if err != nil {
		os.Stderr.WriteString(fmt.Sprintf("readConfigFile(%s) yaml.Unmarshal failed: %v", path, err))
		return err
	}
	return nil
}

// 配置初始化
func ParseConfig(configFile string) error {
	var err error
	// 如果未传入配置文件,则返回报错
	if "" == configFile {
		return errors.New("No config file input")
	}
	// 配置文件状态检查
	if _, err = os.Stat(configFile); err != nil {
		os.Stderr.WriteString(fmt.Sprintf("%v [redis-agent start failed] Check config file failed."+
			" err=%v ConfFile=%v \n", time.Now().Format(TIME_FORMAT), err, configFile))
		return err
	}
	// 配置文件解析
	if err = Config.readConfigFile(configFile); err != nil {
		os.Stderr.WriteString(fmt.Sprintf("%v [redis-agent start failed]"+
			" Parse config file failed. ConfFile=%v err=%v \n", time.Now().Format(TIME_FORMAT), err, configFile))
		return err
	}
	return LoggerInit()
}

访问Redis

redisgo 接口使用文档
一开始用尝试用go-redis包,但是依赖较多且遇到很多不兼容问题,于是转而该用redisgo。支持自定义命令,支持各种协议的redis命令,包括sentinel相关的命令。需要注意的地方

  • 返回的数据类型是interface{},即结构体类型不固定,所以如果依赖返回值则需要判断返回值类型并做对应的类型转化
  • 正确及时的关闭连接
  • redisgo支持连接池,我这里使用的短链接的方式
  • redis.Dial 和redis.DialContext方法都可以实现创建连接,后者是带有上下文的网络请求模式
package redis

import (
	"context"
	"fmt"
	"redis-agent/common"
	"time"

	"github.com/gomodule/redigo/redis"
)

// 初始化proxy的密码设置
var ProxyPwd = &common.Config.ProxyPwd
var Conf = common.Config

func InitRedis(serverType string, port int64) (redis.Conn, error) {
	// 初始化连接参数
	var options []redis.DialOption
	redisOpt := append([]redis.DialOption{
		redis.DialReadTimeout(time.Duration(Conf.ServerRWTimeout) * time.Second),
		redis.DialWriteTimeout(time.Duration(Conf.ServerRWTimeout) * time.Second),
		redis.DialConnectTimeout(time.Duration(Conf.ServerConnTimeout) * time.Second)},
		options...)
	var ctx = context.Background()
	var addr = fmt.Sprintf("127.0.0.1:%d", port)
	c, err := redis.DialContext(ctx, "tcp", addr, redisOpt...)
	if err != nil {
		return nil, err
	}
	// 如果服务类型为proxy则设置密码
	var password = ""
	if serverType == "Proxy" {
		// 如果密码不为空则进行权限认证
		if password = *ProxyPwd; password != "" {
			if _, err := c.Do("AUTH", password); err != nil {
				return nil, err
			}
		}
	}
	return c, nil
}

func Exec(serverType string, port int64, cmd string, key interface{}, args ...interface{}) (interface{}, error) {
	var con redis.Conn
	var err error
	con, err = InitRedis(serverType, port)
	if err != nil {
		return nil, err
	}
	defer con.Close()
	parmas := make([]interface{}, 0)
	parmas = append(parmas, key)
	if len(args) > 0 {
		for _, v := range args {
			parmas = append(parmas, v)
		}
	}
	reply, err := con.Do(cmd, parmas...)
	if err != nil {
		errMsg := fmt.Sprintf("exec cmd error,cmd=%v parmas=%v error=%v", cmd, parmas, err)
		fmt.Println(errMsg)
		return reply, err
	}
	/* 可视化结果集
	string类型的key的访问及管理命令默认返回的数据类型为[]uint8,将其转化为string显示
	集合类的结果集通过redis.Strings转化为[]string
	 */
	switch reply.(type) {
	case []uint8:
		reply, err = redis.String(reply, err)
	case []interface{}:
		reply, err = redis.Strings(reply, err)
	}
	return reply, err
}

单元测试

在代码文件所在的同一目录下创建$(文件名)_test.go文件。
使用了测试框架convey,使得测试代码也更简洁了一些,
Convery有三个参数,第一个是描述,第二个是测试表testing.T结构体,第三个是闭包,
So是固定用法,用来校验所给参数是否符合预期

package redis

import (
	"fmt"
	"testing"

	. "github.com/smartystreets/goconvey/convey"
)

func TestExec(t *testing.T) {
	res, err := Exec("proxy", 1111, "get", "a")
	fmt.Printf("res: %v,type=%T\n", res, res)
	Convey("Exec", t, func() {
		// So(res, ShouldNotBeNil)
		So(err, ShouldBeNil)
	})
}

命令行执行测试代码

# 进入到项目一级目录下,执行,这里代码和测试代码均在二级目录redis下,以下命令会自动执行指定目录下的所有测试文件
go test -v ./redis/
# 输出结果
=== RUN   TestExec
res: aa,type=string

  Exec ✔


1 total assertion

--- PASS: TestExec (0.02s)
PASS
ok      redis-agent/redis       0.040s

如果只执行特定的测试文件,指定测试文件执行

go test -v ./redis/connect_test.go ./redis/connect.go

如果只执行特定测试文件中的某些测试函数

# -run 指定待执行函数名的前缀
go test -v ./redis/  -run TestExec

一定要注意测试结果默认是被缓存的,不适用于动态修改测试代码debug结果的场景,关闭缓存功能加参数-count=1

你可能感兴趣的:(Go笔记,go)