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 事务方式执行,可以使用 TxPipelineTxPipelined

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 key
  • ARGV[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 事务,请使用 TxPipelineTxPipelined

4. Pub/Sub 可以做可靠消息队列吗?

不推荐。

Redis Pub/Sub 不会持久化消息,订阅者离线时会丢消息。如果需要可靠消息,建议使用 Redis Streams、Kafka、RabbitMQ 等方案。


总结

本文介绍了 go-redis 的核心用法:

  • 使用 redis.NewClient 连接 Redis
  • 使用 SetGet 操作 String
  • 使用 HSetHGetAll 操作 Hash
  • 使用 List、Set、ZSet 处理队列、去重和排行榜
  • 使用 redis.Nil 区分 key 不存在
  • 使用 Context 控制超时
  • 使用 Pipeline 提升批量操作性能
  • 使用 TxPipelinedWATCH 处理事务与并发更新
  • 使用 Pub/Sub 实现简单消息广播
  • 使用 Lua 脚本保证复杂操作原子性

对于大多数 Go Web 项目来说,掌握这些内容已经足够应对常见的 Redis 使用场景。

曼波