go-redis Scan 报错后,结构体有时已经改了一半
HGETALL解码某个字段失败,其他字段却随机更新,可能是map遍历中途返回。完整快照可先解码到全新对象,成功后再替换,避免浅复制继续共享指针。
HGetAll(...).Scan(&config) 返回了数字转换错误,但 config 里的某些字符串已经变成新值;再跑一次,更新的字段又不一样。先别把它认作 Redis 返回了随机数据,检查目标对象是否在失败前已经被逐字段修改。
go-redis v9.23.0 的 MapStringStringCmd.Scan() 遍历结果map,并在第一个字段解码错误处返回。Go的map遍历顺序不固定,所以错误字段之前恰好处理了哪些字段,也可能不同。
返回错误不代表目标保持原样
例如,一个Hash里的 port 是 not-a-number,而目标字段是整数。转换这个字段失败前,host、name 等字段可能已经被赋值。指针字段还有一层变化:扫描器可能先分配指针,再尝试解码;失败后,原来的nil可能已经变成指向零值的非nil指针。
因此,不能写成“只要err不为nil就继续使用原config”,除非原对象根本没交给扫描器。也不要把这次部分更新结果当成一份有效配置缓存起来。
这与遍历不同Redis类型时的WRONGTYPE不同。这里Hash可以完整读出,失败发生在客户端把文本放入Go字段的阶段。
完整配置快照先解码再提交
如果这次读取的契约是“一份完整快照,缺失字段采用零值”,可以先使用全新的目标对象:
type Config struct {
Host string `redis:"host"`
Port int `redis:"port"`
}
func loadConfig(ctx context.Context, rdb *redis.Client, key string) (Config, error) {
var candidate Config
if err := rdb.HGetAll(ctx, key).Scan(&candidate); err != nil {
return Config{}, err
}
return candidate, nil
}
该片段需要项目原有的 context 与 github.com/redis/go-redis/v9 导入。调用方只在成功后替换当前配置,失败时保留原对象并按既有错误流程处理。若配置必须具有非空host和合法端口,还应在提交前完成这些业务校验。
不要只写 candidate := current 就认为已经隔离。结构体里的指针、slice和map仍可能共享底层对象,自定义扫描逻辑也可能修改它们。完整隔离需要按真实数据模型创建独立状态,不能用一层浅复制代替。
如果Hash只提供补丁字段,未出现的字段应保留旧值,上面的零值快照方法就不适用。此时应先把输入完整解析为独立的变更集,全部校验成功后再合入;并明确“未出现”和“显式清零”的区别。
检查失败路径,不只检查成功读取
在隔离数据上分别覆盖非法数字、缺失字段、nil指针字段、合法完整数据及连接错误。记录错误类型,确认失败后当前有效配置完全未变,成功后才切换到新值。
不要为消除异常把非法文本统一转成0,也不要在日志中打印整份可能包含密钥的Hash。当前处理保护的是应用提交边界;它不保证读取期间其他客户端没有修改Hash,跨请求的一致性仍需按原有版本或更新机制保证。


