go-redis 教程:在 Go 中优雅地操作 Redis
Redis 是后端开发中非常常见的内存数据库,常用于缓存、排行榜、分布式锁、消息发布订阅、计数器等场景。
在 Go 项目中操作 Redis,最常用的客户端之一就是 go-redis。目前官方推荐使用 v9 版本,包路径为:
github.com/redis/go-redis/v9
本文会从环境准备、连接 Redis、基础 CRUD、常见数据结构、Pipeline、事务、Pub/Sub、Lua 脚本到最佳实践,带你完整入门 go-redis。
一、go-redis 是什么?
go-redis 是 Redis 官方维护的 Go 客户端库,提供了非常贴近 Redis 命令的 API。
它支持:
- Redis 单机连接
- 连接池
- Context 超时控制
- String、Hash、List、Set、ZSet 等常用数据结构
- Pipeline 批量命令
- Redis 事务
- WATCH 乐观锁
- Pub/Sub 发布订阅
- Lua 脚本
- Redis Sentinel
- Redis Cluster
简单来说,如果你熟悉 Redis 命令,就能很快上手 go-redis。
二、准备 Redis 环境
如果你本机已经安装 Redis,可以直接启动 Redis 服务。
也可以使用 Docker 快速启动一个 Redis:
docker run --name redis-demo -p 6379:6379 -d redis:7
测试 Redis 是否可用:
redis-cli ping
如果返回:
PONG
说明 Redis 已经启动成功。
三、创建 Go 项目并安装 go-redis
创建一个示例项目:
mkdir go-redis-demo
cd go-redis-demo
go mod init go-redis-demo
安装 go-redis:
go get github.com/redis/go-redis/v9
安装完成后,go.mod 中会出现类似依赖:
require github.com/redis/go-redis/v9 v9.x.x
四、连接 Redis
新建 main.go:
package main
import (
"context"
"fmt"
"log"
"github.com/redis/go-redis/v9"
)
func main() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // 没有密码就留空
DB: 0, // 使用默认 DB
})
defer rdb.Close()
pong, err := rdb.Ping(ctx).Result()
if err != nil {
log.Fatal("连接 Redis 失败:", err)
}
fmt.Println(pong)
}
运行:
go run main.go
输出:
PONG
这就说明 Go 程序已经成功连接 Redis。
五、String:最基础的缓存读写
Redis 中最常用的数据类型就是 String,经常用于保存缓存数据、验证码、Token、计数器等。
1. Set 写入数据
err := rdb.Set(ctx, "user:1:name", "张三", 0).Err()
if err != nil {
log.Fatal(err)
}
Set 的第四个参数是过期时间。
0表示永不过期time.Minute表示 1 分钟后过期10 * time.Second表示 10 秒后过期
例如:
err := rdb.Set(ctx, "code:login:1001", "9527", 5*time.Minute).Err()
if err != nil {
log.Fatal(err)
}
2. Get 读取数据
val, err := rdb.Get(ctx, "user:1:name").Result()
if err != nil {
log.Fatal(err)
}
fmt.Println(val)
3. 处理 key 不存在的情况
当 key 不存在时,go-redis 会返回 redis.Nil。
val, err := rdb.Get(ctx, "user:2:name").Result()
if err == redis.Nil {
fmt.Println("key 不存在")
} else if err != nil {
log.Fatal(err)
} else {
fmt.Println("value:", val)
}
注意:redis.Nil 不是程序异常,而是 Redis 返回的空结果。
六、封装一个简单的缓存函数
实际项目中,我们通常会把 Redis 操作封装起来。
func SetCache(ctx context.Context, rdb *redis.Client, key string, value any, ttl time.Duration) error {
return rdb.Set(ctx, key, value, ttl).Err()
}
func GetCache(ctx context.Context, rdb *redis.Client, key string) (string, bool, error) {
val, err := rdb.Get(ctx, key).Result()
if err == redis.Nil {
return "", false, nil
}
if err != nil {
return "", false, err
}
return val, true, nil
}
使用:
err := SetCache(ctx, rdb, "article:1:title", "Go Redis 入门", time.Hour)
if err != nil {
log.Fatal(err)
}
val, exists, err := GetCache(ctx, rdb, "article:1:title")
if err != nil {
log.Fatal(err)
}
if !exists {
fmt.Println("缓存不存在")
return
}
fmt.Println(val)
这种写法的好处是可以把“不存在”和“真正的错误”区分开。
七、常见数据结构操作
1. Hash:保存对象数据
Hash 很适合保存用户信息、配置项等结构化数据。
err := rdb.HSet(ctx, "user:1", map[string]any{
"name": "张三",
"age": 18,
"email": "zhangsan@example.com",
}).Err()
if err != nil {
log.Fatal(err)
}
读取单个字段:
name, err := rdb.HGet(ctx, "user:1", "name").Result()
if err != nil {
log.Fatal(err)
}
fmt.Println(name)
读取全部字段:
user, err := rdb.HGetAll(ctx, "user:1").Result()
if err != nil {
log.Fatal(err)
}
fmt.Println(user)
2. List:队列和时间线
List 常用于消息队列、最新动态列表等场景。
err := rdb.LPush(ctx, "tasks", "task1", "task2", "task3").Err()
if err != nil {
log.Fatal(err)
}
从右侧弹出一个任务:
task, err := rdb.RPop(ctx, "tasks").Result()
if err == redis.Nil {
fmt.Println("队列为空")
} else if err != nil {
log.Fatal(err)
} else {
fmt.Println("处理任务:", task)
}
读取列表范围:
tasks, err := rdb.LRange(ctx, "tasks", 0, -1).Result()
if err != nil {
log.Fatal(err)
}
fmt.Println(tasks)
3. Set:去重集合
Set 适合标签、点赞用户、去重数据等场景。
err := rdb.SAdd(ctx, "article:1:likes", "user1", "user2", "user3").Err()
if err != nil {
log.Fatal(err)
}
判断成员是否存在:
ok, err := rdb.SIsMember(ctx, "article:1:likes", "user1").Result()
if err != nil {
log.Fatal(err)
}
fmt.Println(ok)
获取集合所有成员:
members, err := rdb.SMembers(ctx, "article:1:likes").Result()
if err != nil {
log.Fatal(err)
}
fmt.Println(members)
4. ZSet:排行榜
ZSet 是有序集合,非常适合做排行榜。
err := rdb.ZAdd(ctx, "rank:game", redis.Z{
Score: 100,
Member: "Alice",
}, redis.Z{
Score: 88,
Member: "Bob",
}, redis.Z{
Score: 95,
Member: "Tom",
}).Err()
if err != nil {
log.Fatal(err)
}
获取分数从高到低的排行榜:
rankList, err := rdb.ZRevRangeWithScores(ctx, "rank:game", 0, 9).Result()
if err != nil {
log.Fatal(err)
}
for i, item := range rankList {
fmt.Printf("第 %d 名:%v,分数:%.0f\n", i+1, item.Member, item.Score)
}
给用户增加分数:
score, err := rdb.ZIncrBy(ctx, "rank:game", 10, "Alice").Result()
if err != nil {
log.Fatal(err)
}
fmt.Println("Alice 最新分数:", score)
八、Context 和超时控制
生产环境中不要无脑使用 context.Background() 贯穿所有 Redis 请求。
更推荐为一次操作设置超时时间:
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
val, err := rdb.Get(ctx, "some:key").Result()
if err != nil {
log.Fatal(err)
}
fmt.Println(val)
这样当 Redis 网络异常或响应过慢时,请求会及时取消,避免程序长时间阻塞。
九、Pipeline:批量执行命令
如果你需要连续执行很多 Redis 命令,每条命令都单独发送会产生多次网络往返。
Pipeline 可以把多条命令打包发送,减少网络开销。
pipe := rdb.Pipeline()
setName := pipe.Set(ctx, "user:2:name", "李四", 0)
setAge := pipe.Set(ctx, "user:2:age", 20, 0)
getName := pipe.Get(ctx, "user:2:name")
_, err := pipe.Exec(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Println(setName.Val())
fmt.Println(setAge.Val())
fmt.Println(getName.Val())
也可以使用闭包形式:
_, err := rdb.Pipelined(ctx, func(pipe redis.Pipeliner) error {
pipe.HSet(ctx, "user:3", "name", "王五")
pipe.HSet(ctx, "user:3", "age", 22)
pipe.Expire(ctx, "user:3", time.Hour)
return nil
})
if err != nil {
log.Fatal(err)
}
需要注意:普通 Pipeline 只是减少网络往返,不保证事务原子性。
十、事务:TxPipeline 和 TxPipelined
如果你希望多条命令以 Redis 事务方式执行,可以使用 TxPipeline 或 TxPipelined。
cmds, err := rdb.TxPipelined(ctx, func(tx redis.Pipeliner) error {
tx.IncrBy(ctx, "balance:alice", -100)
tx.IncrBy(ctx, "balance:bob", 100)
return nil
})
if err != nil {
log.Fatal(err)
}
for _, cmd := range cmds {
fmt.Println(cmd.String())
}
TxPipelined 会使用 Redis 的 MULTI/EXEC。
不过要注意:Redis 事务和关系型数据库事务不完全一样。它可以保证命令按顺序执行,但不提供传统数据库那种复杂的回滚机制。
十一、WATCH:实现乐观锁
如果要实现“读取旧值,根据旧值更新,并且期间不能被别人修改”的逻辑,可以使用 WATCH。
下面是一个安全递增计数器的示例:
const maxRetries = 100
func IncrementWithWatch(ctx context.Context, rdb *redis.Client, key string) error {
for i := 0; i < maxRetries; i++ {
err := rdb.Watch(ctx, func(tx *redis.Tx) error {
current, err := tx.Get(ctx, key).Int()
if err == redis.Nil {
current = 0
} else if err != nil {
return err
}
_, err = tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {
pipe.Set(ctx, key, current+1, 0)
return nil
})
return err
}, key)
if err == nil {
return nil
}
if err == redis.TxFailedErr {
continue
}
return err
}
return fmt.Errorf("更新失败,重试次数过多")
}
当被监听的 key 在事务提交前被其他客户端修改时,go-redis 会返回 redis.TxFailedErr,这时可以重试。
十二、Pub/Sub:发布订阅
Redis 的 Pub/Sub 可以实现简单的消息广播。
1. 订阅消息
pubsub := rdb.Subscribe(ctx, "news")
defer pubsub.Close()
ch := pubsub.Channel()
for msg := range ch {
fmt.Println("channel:", msg.Channel)
fmt.Println("payload:", msg.Payload)
}
2. 发布消息
n, err := rdb.Publish(ctx, "news", "Go Redis 教程发布啦").Result()
if err != nil {
log.Fatal(err)
}
fmt.Println("收到消息的订阅者数量:", n)
Pub/Sub 更适合实时通知和广播,不适合作为可靠消息队列。如果你需要消息持久化和消费确认,可以考虑 Redis Streams、Kafka 或 RabbitMQ。
十三、Lua 脚本:保证复杂操作原子性
Redis 执行 Lua 脚本时具有原子性,适合做库存扣减、限流、分布式锁释放等场景。
下面示例实现一个简单的计数器递增:
var incrScript = redis.NewScript(`
local key = KEYS[1]
local step = tonumber(ARGV[1])
local value = redis.call("GET", key)
if not value then
value = 0
end
value = tonumber(value) + step
redis.call("SET", key, value)
return value
`)
result, err := incrScript.Run(ctx, rdb, []string{"counter:lua"}, 5).Int()
if err != nil {
log.Fatal(err)
}
fmt.Println(result)
这里的参数含义是:
KEYS[1]:Redis keyARGV[1]:脚本参数Run(ctx, rdb, []string{"counter:lua"}, 5):传入 key 和参数
Lua 脚本可以减少多次网络请求,并保证复杂逻辑在 Redis 中原子执行。
十四、连接配置和连接池
go-redis 默认会维护连接池。实际项目中可以根据业务并发量调整配置。
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Username: "",
Password: "",
DB: 0,
PoolSize: 20,
MinIdleConns: 5,
})
如果 Redis 开启了密码认证:
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Username: "default",
Password: "your-password",
DB: 0,
})
如果密码来自配置中心或密钥管理服务,也可以使用凭证提供函数:
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
CredentialsProvider: func() (string, string) {
return "default", "your-password"
},
})
十五、完整示例
下面给出一个可以直接运行的完整示例:
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/redis/go-redis/v9"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
})
defer rdb.Close()
if err := rdb.Ping(ctx).Err(); err != nil {
log.Fatal("Redis 连接失败:", err)
}
// String
if err := rdb.Set(ctx, "blog:title", "go-redis 教程", time.Hour).Err(); err != nil {
log.Fatal(err)
}
title, err := rdb.Get(ctx, "blog:title").Result()
if err != nil {
log.Fatal(err)
}
fmt.Println("文章标题:", title)
// Hash
if err := rdb.HSet(ctx, "blog:1", map[string]any{
"title": "go-redis 教程",
"author": "Claude",
"views": 100,
}).Err(); err != nil {
log.Fatal(err)
}
blog, err := rdb.HGetAll(ctx, "blog:1").Result()
if err != nil {
log.Fatal(err)
}
fmt.Println("文章信息:", blog)
// ZSet 排行榜
if err := rdb.ZAdd(ctx, "rank:article", redis.Z{
Score: 100,
Member: "article:1",
}, redis.Z{
Score: 80,
Member: "article:2",
}).Err(); err != nil {
log.Fatal(err)
}
ranks, err := rdb.ZRevRangeWithScores(ctx, "rank:article", 0, 9).Result()
if err != nil {
log.Fatal(err)
}
for i, item := range ranks {
fmt.Printf("第 %d 名:%v,热度:%.0f\n", i+1, item.Member, item.Score)
}
}
运行:
go run main.go
十六、项目中的最佳实践
1. Redis Client 建议全局复用
redis.Client 内部已经维护连接池,不需要每次请求都创建一个新的 client。
推荐:
var RDB *redis.Client
func InitRedis() {
RDB = redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
}
不推荐:
func GetUser() {
rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
defer rdb.Close()
}
2. 正确处理 redis.Nil
val, err := rdb.Get(ctx, key).Result()
if err == redis.Nil {
// key 不存在,不一定是错误
} else if err != nil {
// 网络错误、权限错误、Redis 服务异常等
} else {
// 正常拿到 value
_ = val
}
3. 给缓存设置过期时间
缓存如果没有过期时间,很容易出现脏数据或占用大量内存。
rdb.Set(ctx, "user:1", "data", 30*time.Minute)
4. 使用统一的 key 命名规范
推荐使用冒号分隔:
业务:实体:ID:字段
例如:
user:1001:profile
article:2001:likes
rank:game:daily
5. 批量操作优先考虑 Pipeline
当你要连续执行大量 Redis 命令时,Pipeline 可以显著减少网络往返。
6. 不要把 Redis 当成唯一数据库
Redis 通常用于缓存和加速访问。除非你明确使用 Redis 持久化能力并接受相关限制,否则不要把关键业务数据只保存在 Redis 中。
十七、常见问题
1. redis: nil 是什么错误?
这表示 key 不存在。它对应 redis.Nil,需要单独判断。
2. 为什么连接 Redis 失败?
常见原因:
- Redis 服务没有启动
- 地址或端口写错
- Docker 端口没有映射
- Redis 设置了密码但代码没有配置
- 防火墙或网络策略阻止连接
可以先用下面命令测试:
redis-cli -h localhost -p 6379 ping
3. Pipeline 是事务吗?
不是。
普通 Pipeline 只是批量发送命令,减少网络开销,不保证事务原子性。
如果需要 Redis 事务,请使用 TxPipeline 或 TxPipelined。
4. Pub/Sub 可以做可靠消息队列吗?
不推荐。
Redis Pub/Sub 不会持久化消息,订阅者离线时会丢消息。如果需要可靠消息,建议使用 Redis Streams、Kafka、RabbitMQ 等方案。
总结
本文介绍了 go-redis 的核心用法:
- 使用
redis.NewClient连接 Redis - 使用
Set、Get操作 String - 使用
HSet、HGetAll操作 Hash - 使用 List、Set、ZSet 处理队列、去重和排行榜
- 使用
redis.Nil区分 key 不存在 - 使用 Context 控制超时
- 使用 Pipeline 提升批量操作性能
- 使用
TxPipelined和WATCH处理事务与并发更新 - 使用 Pub/Sub 实现简单消息广播
- 使用 Lua 脚本保证复杂操作原子性
对于大多数 Go Web 项目来说,掌握这些内容已经足够应对常见的 Redis 使用场景。