Golang无统一存储方案,需据数据特性选方式:临时状态用sync.Map或加锁map;文件存储需原子写入;数据库应抽象接口;Redis存值须序列化且带命名空间前缀。
直接说结论:在Go语言里,你找不到一个“万能”的存储方案。怎么存,完全取决于你要存什么、存多久、谁来读、并发压力有多大。选错了方式,轻则性能掉坑里,重则线上数据丢失,后果可一点不轻松。

存临时状态用 map,但别忘了并发安全
像本地缓存用户会话ID、请求上下文标记这类短生命周期的数据,map 无疑是速度最快、也最轻量的选择。
不过,一个常见的“翻车”现场就是:fatal error: concurrent map writes —— 多个 goroutine 同时写同一个 map,程序会直接 panic。
- 高并发读多写少:直接用
sync.Map替代原生map,它专为这类场景做了优化。 - 写操作频繁:比如实现计数器,改用
sync.RWMutex配合普通map,控制粒度更细,性能也更可控。 - 结构体设计:切忌在结构体里直接暴露
map字段供外部修改。正确的做法是封装Get/Set方法,在内部处理加锁逻辑,或者直接使用sync.Map。
存结构化数据到文件,json.Marshal + ioutil.WriteFile 不够健壮
开发期快速落盘配置、写调试日志或者做离线备份,这么干没问题。但一旦上了生产环境,就必须升级方案。
这里有几个典型的坑:程序崩溃时文件只写了一半;多个进程同时写一个文件导致内容损坏;还有所谓的“中文乱码”,其实很多时候是终端没有正确支持 UTF-8 编码。
立即学习“go语言免费学习笔记(深入)”;
- 更明确的文件操作:使用
os.OpenFile并配合明确的标志,如os.O_CREATE | os.O_WRONLY | os.O_TRUNC和0644权限。这比已弃用的ioutil.WriteFile更清晰。 - 先序列化,再写入:写入前,先用
json.Marshal将数据转为[]byte,再调用file.Write。避免边序列化边写文件,否则出错时很难回滚。 - 保证原子性:如果需要原子写入(确保文件内容要么全旧,要么全新),可以先将数据写入一个临时文件(例如
config.json.tmp),然后使用os.Rename来替换原文件——在 Linux 系统下,这个操作是原子的。
连数据库别直接裸用 database/sql 的 Exec/Query
必须明确一点:database/sql 是一个驱动适配层,而不是业务抽象层。一旦你的代码涉及事务、超时控制、读写分离,就会立刻和底层的 MySQL 或 PostgreSQL 绑定,难以抽离。
常见的尴尬情况:本地测试用 SQLite,结果一句 INSERT ... RETURNING 直接导致 panic;想对 *sql.DB 做单元测试 Mock,却发现 BeginTx 返回的是 pgx.Tx 这种驱动私有的类型,根本无法断言。
- 定义行为接口:业务层应该依赖如
StoreUser(ctx context.Context, u User) error这样的接口,而不是直接暴露ExecContext这类底层方法。 - 拆分连接与事务:将连接获取(如
GetConn(ctx))和事务控制(如BeginTx(ctx, opts))的逻辑拆开。让 PostgreSQL 和 MySQL 的驱动各自去实现这些接口,封装底层细节。 - 超时控制是必须:
GetConn这类方法必须接受context.Context参数以支持超时。否则,连接池卡住时,应用将毫无感知。
存 Session 或缓存,Redis 是默认选择,但 client.Set 很容易用错
首先别被名字误导:client.Set 操作的是 Redis 的 String 类型,而不是 Set 集合。更重要的是,它只接收 string 类型的值,直接传入 struct 或 map 是会出错的。
常见的错误现象:存进去的是类似 &{} 的指针字符串,取出来时得到 redis: nil 报错;用 redis-cli GET 一看,全是乱码或空值。
- 值必须序列化为字符串:结构体先用
json.Marshal转成[]byte,再转为string(b);整型值用strconv.Itoa,千万别用string(i)(那是 ASCII 转换)。 - 上下文传递:
ctx不能总是context.Background()。在 HTTP handler 中,应优先复用r.Context(),并为其加上超时控制,例如WithTimeout(..., 300*time.Millisecond)。 - 过期时间单位:过期时间的参数是
time.Duration类型。要写3600 * time.Second,而不是直接传整数3600。 - 键名命名空间:为键名加上前缀,比如
session:abc123或user:456:profile。这是避免不同业务数据冲突的最佳实践。
说到底,真正的难点从来不是“怎么写一行存数据的代码”,而是决定这一行代码该在什么时机执行、由谁来负责清理、超时后如何优雅降级、失败时是否允许重试。这些关键决策,都隐藏在存储方式的选择背后,而不是简单的 API 调用里。
