go-redis 的 Eval 方法要求传入 *redis.Script 实例而非原始 Lua 字符串,以自动处理脚本缓存、参数序列化和返回值解析;直接传字符串会导致类型错误或 panic。
为什么 EVAL 和 EVALSHA 在 go-redis 里不能直接传字符串
go-redis 的 eval 方法不接受原始 lua 字符串作为第一个参数,而是要求你传入 *redis.script 实例。这是为了自动处理脚本缓存(sha 校验)、参数序列化和返回值解析。直接拼接字符串调用 eval 会触发类型错误或空指针 panic。
常见错误现象:panic: interface conversion: interface {} is string, not []interface {} —— 这通常是因为把 Lua 脚本当 string 直接塞进 Eval 第二个参数位置,而它实际期待的是 keys 和 args 分开传。
- 必须用
redis.NewScript()包装 Lua 字符串,再调用其Eval()或EvalSha()方法 -
keys参数只用于被 Redis 识别为“键”的参数(影响集群路由),非键参数一律走args - 脚本内通过
KEYS[1]、ARGV[1]访问,下标从 1 开始,不是 0
如何安全地加载并执行带参数的 Lua 脚本
推荐将 Lua 脚本定义为常量或从文件读取(避免硬编码在逻辑中),然后用 redis.NewScript() 初始化一次复用。每次执行时调用 script.Eval() 并显式传入 *redis.Client、keys([]string)和 args(interface{} 可变参)。
示例:原子递增并限流
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
const luaScript = `
if redis.call("incr", KEYS[1]) == 1 then
redis.call("expire", KEYS[1], ARGV[1])
end
return redis.call("get", KEYS[1])
`
rateLimitScript := redis.NewScript(luaScript)
val, err := rateLimitScript.Eval(ctx, rdb, []string{"rate:uid:123"}, 60).Int()
-
keys必须是非空切片,哪怕只传一个键也要写成[]string{"mykey"} -
args支持任意数量参数,但类型需能被json.Marshal(如string、int、bool),复杂结构慎用 - 返回值需手动调用
.Int()/.String()/.Result()等方法解析,否则得到的是interface{}原始值
使用 EVALSHA 避免重复传输脚本的注意事项
EvalSha 不是自动 fallback 机制,它只在脚本已存在于 Redis 服务器缓存中时才成功;若未加载过,会返回 redis.Nil 错误(不是 panic)。因此生产环境不能无条件用 EvalSha 替代 Eval。
- 首次部署或 Redis 重启后,需先调用一次
script.Load().Result()显式加载脚本 - 可封装一个带自动加载逻辑的执行函数,检查
redis.Nil后回退到Eval - 注意:Redis 集群模式下,
EVALSHA要求所有 key 路由到同一分片,否则报CROSSSLOT错误
返回值类型不匹配导致 panic 的真实原因
go-redis 对 Lua 返回值做弱类型映射:Lua 的 nil → Go 的 nil,number → float64,string → string,table → []interface{}。如果你调用 .Int() 却收到 string,就会 panic。
- 永远优先用
.Result()拿到(interface{}, error),再根据业务判断类型 - Lua 中明确用
tonumber()或tostring()统一输出类型,比在 Go 侧做类型断言更可靠 - 批量操作返回数组时,
redis.StringSlice(cmd.Result())比循环转string更安全
脚本的 SHA 值和执行上下文绑定紧密,调试时建议打印 script.Hash 并在 Redis CLI 手动 EVALSHA 验证逻辑,比反复改 Go 代码更快定位问题。










