你有没有写过这样的代码:加个新功能要改五处地方?测试写到怀疑人生?重构时手抖删错一行,整个服务崩了?
别慌,今天我们就用 SOLID 原则 给你的 Go 项目“打疫苗”——让你的代码更健壮、易扩展、好测试,还能在同事面前装个稳重老司机 😎。
虽然 Go 不是传统面向对象语言(没有 class、继承那一套),但 SOLID 的灵魂依然适用。我们用一个“在线问卷系统”的小例子,边吃泡面边学设计!
🧱 S - Single Responsibility Principle(单一职责)
一个结构体/包,只干一件事。
想象你让一个同事既当产品经理又当 DBA 还要写前端——他迟早会辞职。代码也一样!
❌ 反面教材
type Survey struct {
Title string
Questions []string
}
func (s *Survey) Save() error {
// 直接连数据库保存...
return nil
}
Survey 既要管问卷数据,又要管存数据库?这就像让煎饼果子摊主顺便修空调——离谱!
✅ 正确姿势:拆!
// survey/survey.go
type Survey struct {
title string // 小写:私有字段
questions []string
}
func (s *Survey) Title() string {
return s.title }
// survey/repository.go
type Repository interface {
Save(*Survey) error
}
type InMemoryRepo struct{
/* ... */ }
func (r *InMemoryRepo) Save(s *Survey) error {
/* ... */ }
现在:
Survey只负责“我是谁”Repository负责“我存哪”
好处:想换数据库?只改 Repo 实现,Survey 动都不用动!
🔓 O - Open-Closed Principle(开闭原则)
对扩展开放,对修改关闭。
你想支持导出问卷到 S3、GCS、阿里云 OSS……难道每次都要改 switch?
❌ 硬编码地狱
func Export(s *Survey, dst string) error {
switch dst {
case "s3": // ...
case "gcs": // ...
case "oss": // 又得改这里!
}
}
✅ 接口抽象大法
type Exporter interface {
Export(*Survey) error
}
type S3Exporter struct{
}
func (e *S3Exporter) Export(s *Survey) error {
/* 上传到 S3 */ }
type AliOSSExporter struct{
} // 阿里云用户狂喜 😄
func (e *AliOSSExporter) Export(s *Survey) error {
/* 上传到 OSS */ }
func ExportSurvey(s *Survey, e Exporter) error {
return e.Export(s)
}
新增导出方式? 只需实现 Exporter 接口,完全不用碰旧代码!这就是“开闭”的魅力。
🔄 L - Liskov Substitution Principle(里氏替换)
子类型必须能无缝替换父类型。
Go 没有继承,但接口天然支持 LSP。经典例子:io.Writer。
func WriteSurvey(s *Survey, w io.Writer) error {
data, _ := json.Marshal(s)
_, err := w.Write(data)
return err
}
你可以传:
os.File(写文件)bytes.Buffer(写内存)http.ResponseWriter(直接返回 HTTP)
只要实现了 Write([]byte) (int, error),就能塞进去——这就是“替换不崩”。
🧩 I - Interface Segregation Principle(接口隔离)
别强迫别人实现一堆没用的方法!
假设你定义了一个万能 Question 接口:
type Question interface {
SetTitle(string)
AddOption(string) // 文本题:???
}
文本题根本不需要选项!但它被迫实现一个空方法——这叫“接口污染”。
✅ 拆成小接口
type Question interface {
SetTitle(string)
}
type OptionedQuestion interface {
Question
AddOption(string)
}
现在:
TextQuestion只实现QuestionDropdownQuestion实现OptionedQuestion
各取所需,干净清爽!
⬇️ D - Dependency Inversion Principle(依赖倒置)
高层模块别直接依赖底层细节,大家都依赖抽象。
比如你的 SurveyManager 直接 new 了一个 InMemoryRepo:
type SurveyManager struct {
store InMemoryRepo // ❌ 硬编码!
}
想换 MySQL?哭着改代码吧……
✅ 依赖注入 + 接口
type SurveyManager struct {
store Repository // ← 依赖抽象!
}
func NewSurveyManager(repo Repository) *SurveyManager {
return &SurveyManager{
store: repo}
}
启动时注入具体实现:
// main.go
repo := &MySQLRepo{
...}
mgr := NewSurveyManager(repo)
测试时? 注入一个 mock repo 就行,无需真实数据库!
🎯 总结:SOLID 不是教条,而是“防坑指南”
| 原则 | 核心思想 | Go 中怎么用 |
|---|---|---|
| S | 一个东西只干一件事 | 结构体 + 包职责清晰 |
| O | 扩展靠新增,不改旧代码 | 接口 + 多态 |
| L | 子类能无缝替换父类 | 利用 io.Writer 等标准接口 |
| I | 接口要小而专 | 拆分大接口 |
| D | 依赖抽象,不依赖具体 | 构造函数注入接口 |
💡 小贴士:SOLID 不是为了炫技,而是为了 减少未来加班的次数。
下次写 Go 代码前,问自己一句:
“如果老板明天让我加个新功能,我会不会想辞职?”
如果答案是“会”,那可能该看看 SOLID 了。