go-redis 强制要求所有 lua 脚本必须通过 redis.newscript() 封装后调用 eval() 或 run(),否则因类型不匹配编译失败或 panic;其核心目的是自动实现 sha1 缓存复用、参数 json 序列化和返回值类型安全解包。

go-redis 的 Script 类型不是可选优化,而是强制入口——所有 Lua 脚本必须通过 redis.NewScript() 封装后调用 Eval() 或 Run(),否则编译失败或 panic。直接传字符串给 Eval() 会触发类型错误:cannot use string as *redis.Script。
为什么必须用 redis.NewScript() 而不能拼接字符串
go-redis 剥离了裸 EVAL 接口,是因为它要自动处理三件事:脚本 SHA1 缓存复用、参数 JSON 序列化、返回值类型安全解包。手动拼字符串绕过这层,等于放弃原子性保障和错误分类能力。
- 常见翻车点:
client.Eval(ctx, "return 1", []string{}, nil)直接报错,因为第二个参数必须是*redis.Script -
redis.NewScript()返回的对象可复用,建议定义为包级变量,避免每次请求都 new - 多行脚本用反引号包裹更安全,比如限流逻辑含换行和注释时,Go 不会误删空格或转义
- 脚本内容变更后,SHA1 自动更新,无需手动清 Redis 缓存;但旧 SHA1 在 Redis 中仍存在,不冲突
Script.Load() 和 Script.Eval() 的分工与陷阱
Load() 只向 Redis 发送 SCRIPT LOAD 命令并返回 SHA1,不执行;Eval() 才真正运行——但它内部会先尝试 EVALSHA,失败再 fallback 到 EVAL。多数场景下你不需要显式 Load(),除非部署在 Redis Cluster。
在 Go 中使用 google/wire 实现编译时依赖注入——wire.NewSet、wire.Build、wire.Bind(接口→实现)、wire.Struct、wire.Value、wire.Interface
- Redis Cluster 要求脚本必须提前加载到所有分片节点,否则
EVAL可能因某节点无脚本而报MOVED或CROSSSLOT -
Load()是幂等操作,重复调用不会报错,但热路径里反复调用有网络开销,应放在服务启动阶段 - 若
Load()失败(如节点不可达),Eval()后续 fallback 仍可能成功,但性能下降——这不是容错,是退化 - 拿到的 SHA1 是
[]byte,需用fmt.Sprintf("%x", sha)转成小写十六进制字符串,才能用于EVALSHA
参数传入必须严格区分 KEYS 和 ARGV,且类型不能错
脚本里 KEYS[1] 对应 Go 侧 keys []string 第一个元素,ARGV[1] 对应 args 第一个参数。顺序、类型、空值处理任意一项出错,都会导致脚本内 nil 访问或类型转换失败。
-
keys必须是非空[]string,哪怕只用一个 key,也得写成[]string{"user:123"};传nil或空切片在单机可能不报错,但在 Cluster 下直接触发wrong number of arguments -
args支持int、string、bool、struct等,内部用json.Marshal序列化;但nil会变成 Lua 的nil,而redis.call("GET", nil)会 panic - 数字类参数(如限流阈值)建议在 Go 侧转成
string再传,Lua 里用tonumber(ARGV[1])转,避免 float64 精度丢失或越界 - 复杂结构(如日志对象)先
json.Marshal成字符串,Lua 里用cjson.decode()解析——前提是 Redis 启用了cjson模块
返回值解析不判空就 .Int() 会 panic
Script.Eval() 返回的是命令对象(如 *redis.IntCmd),不是原始值。.Val() 前必须先检查 .Err() == nil,否则 .Int() 在返回 nil 或非整数时直接 panic。
- Lua 脚本最后一行决定 Go 侧类型:返回数字 →
int64,字符串 →string,数组 →[]interface{},nil→ Go 的nil - 安全做法是用
type switch分支处理:case int64:、case string:、case []interface{}:、case nil: - 如果脚本用
redis.pcall包裹命令,错误会被吞成 Lua 表,Go 侧收到的是map[string]interface{},需手动检查v["err"] != nil - 集群环境下,即使脚本逻辑正确,也可能因 key 路由不一致返回
CROSSSLOT错误,这个错误不会出现在.Val(),而在.Err()里
lua-time-limit 是 5 秒,超时后整个脚本中断,且后续请求会卡在 BUSY Redis is busy running a script,必须人工 SCRIPT KILL;而原子性只在单个 Redis 实例或同一槽位内成立,跨 key 操作前必须确认它们带相同哈希标签(如 {order1001}:stock 和 {order1001}:log)。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










