应定义统一storage接口,包含get、put、delete、exists方法,参数和错误语义一致;各后端独立实现并封装细节;通过factory按配置创建实例;业务层依赖接口注入,确保可测试与可替换。

用 interface 定义统一的 Storage 接口
Go 里做多后端抽象,核心不是写一堆 if-else 切换逻辑,而是先想清楚「它们共同要做什么」。比如读、写、删除、是否存在——这些行为必须收敛到一个 Storage 接口里,否则后续所有扩展都会散掉。
常见错误是把具体实现细节(如 S3 的 PutObjectInput、本地文件的 os.OpenFile)提前暴露进接口,导致调用方被迫感知底层差异。正确做法是只暴露语义一致的方法:
Get(ctx context.Context, key string) ([]byte, error)Put(ctx context.Context, key string, data []byte) errorDelete(ctx context.Context, key string) errorExists(ctx context.Context, key string) (bool, error)
每个方法签名都保持相同参数结构和错误语义,这样上层代码才能真正「不关心后端是谁」。
为 S3、本地文件、内存等后端分别实现 Storage
不同后端的初始化方式、依赖项、错误处理逻辑差异很大,但只要实现了上面的接口,就能被同一套业务逻辑使用。比如:
- S3 实现需要传入
*s3.Client和bucket名; - 本地文件实现需指定
rootDir路径,并注意路径拼接安全(避免../路径穿越); - 内存实现适合测试,用
sync.Map存储,但要注意它不支持原子性批量操作。
关键点:各实现内部自行处理重试、超时、错误映射(例如把 s3.ErrCodeNoSuchKey 映射为 os.ErrNotExist),对外只返回标准 error。别让调用方去 switch 具体错误类型。
用 Factory 模式按配置创建对应 Storage 实例
运行时决定用哪个后端,靠硬编码 new 是不可维护的。应该从配置(如 YAML/ENV)读取 type: s3 或 type: fs,再由工厂函数分发:
func NewStorage(cfg Config) (Storage, error) {
switch cfg.Type {
case "s3":
return NewS3Storage(cfg.S3Config), nil
case "fs":
return NewFSStorage(cfg.FSConfig), nil
case "mem":
return NewMemStorage(), nil
default:
return nil, fmt.Errorf("unknown storage type: %s", cfg.Type)
}
}
这里容易踩的坑:cfg 结构体字段必须按需拆分,S3 的 Region、Endpoint 不该出现在 FS 配置里;更推荐用嵌套结构或 interface{} + 类型断言,避免零值污染。
在 HTTP handler 或 service 层注入 Storage 实例
最终使用时,不要在 handler 里 new Storage,而应通过构造函数或依赖注入传入。比如:
type UploadService struct {
store Storage
}
func (s *UploadService) HandleUpload(w http.ResponseWriter, r *http.Request) {
data, _ := io.ReadAll(r.Body)
s.store.Put(r.Context(), "uploads/"+uuid.New().String(), data) // 统一调用
}
这样单元测试就能轻松替换为 MemStorage,无需启动 S3 模拟器;压测时也能快速切到本地磁盘绕过网络开销。真正麻烦的从来不是接口定义,而是初始化时机和生命周期管理——比如 S3 client 应该复用,而不是每次请求都 new 一个。











