Skip to main content

Go 中的 SOLID 实践

好的,下面用 Go 语言来演示 SOLID 原则的实践。Go 并非经典的面向对象语言(没有类和继承),但可以通过 结构体 + 接口 + 组合 来实现这些设计思想。


1. S – 单一职责原则

一个结构体/包应该只有一个引起它变化的原因。

反例:一个结构体做了太多事情

// 违反:User 既负责存储用户数据,又负责持久化,还负责发邮件
type User struct {
Name string
Email string
}

func (u *User) SaveToDB() error {
// 保存到数据库的逻辑
return nil
}

func (u *User) SendWelcomeEmail() error {
// 发送邮件的逻辑
return nil
}

正确做法:拆分职责

// 只负责用户数据定义
type User struct {
Name string
Email string
}

// 持久化职责
type UserRepository struct{}

func (r *UserRepository) Save(u User) error {
// 保存到数据库
return nil
}

// 邮件职责
type EmailService struct{}

func (s *EmailService) SendWelcome(u User) error {
// 发送邮件
return nil
}

2. O – 开闭原则

对扩展开放,对修改关闭。新增功能时,尽量通过添加新代码实现,而不是修改已有代码。

反例:用类型分支添加新功能

type Order struct {
Amount float64
Type string // "regular", "vip"
}

func CalculateDiscount(o Order) float64 {
if o.Type == "regular" {
return o.Amount * 0.05
} else if o.Type == "vip" {
return o.Amount * 0.1
}
return 0
}
// 每增加一种会员类型,就需要修改 CalculateDiscount 函数

正确做法:策略模式 + 接口

// 定义折扣策略接口
type DiscountStrategy interface {
Calculate(amount float64) float64
}

type RegularDiscount struct{}
func (RegularDiscount) Calculate(amount float64) float64 {
return amount * 0.05
}

type VIPDiscount struct{}
func (VIPDiscount) Calculate(amount float64) float64 {
return amount * 0.1
}

type SuperVIPDiscount struct{}
func (SuperVIPDiscount) Calculate(amount float64) float64 {
return amount * 0.2
}

// Order 依赖接口,不依赖具体策略
type Order struct {
Amount float64
Strategy DiscountStrategy
}

func (o Order) Discount() float64 {
return o.Strategy.Calculate(o.Amount)
}

// 使用时注入策略,新增 SuperVIPDiscount 无需修改 Order 或已有策略代码。

3. L – 里氏替换原则

如果接口类型 T 被类型 S 替换,程序行为不应改变。
在 Go 中:只要实现了同一个接口的类型,应该能够互相替换且保持正确行为

反例:破坏预期行为的“正方形/矩形”问题(通过组合模拟)

// 定义一个矩形(接口)
type Rectangle interface {
SetWidth(w float64)
SetHeight(h float64)
Area() float64
}

// 具体实现:普通矩形
type Rect struct {
width, height float64
}
func (r *Rect) SetWidth(w float64) { r.width = w }
func (r *Rect) SetHeight(h float64) { r.height = h }
func (r *Rect) Area() float64 { return r.width * r.height }

// 一个“正方形”实现,但破坏了矩形行为:SetWidth 和 SetHeight 必须同时修改
type Square struct {
side float64
}
func (s *Square) SetWidth(w float64) { s.side = w }
func (s *Square) SetHeight(h float64) { s.side = h }
func (s *Square) Area() float64 { return s.side * s.side }

// 某个函数期望一个矩形
func PrintArea(r Rectangle) {
r.SetWidth(4)
r.SetHeight(5)
fmt.Println(r.Area()) // 对于 Rect 输出 20,对于 Square 输出 25(不符合预期)
}

正确做法:不强行建立不合理的继承(组合)关系,使用更抽象的接口

// 定义更抽象的 Shape 接口
type Shape interface {
Area() float64
}

type Rect struct {
width, height float64
}
func (r Rect) Area() float64 { return r.width * r.height }

type Square struct {
side float64
}
func (s Square) Area() float64 { return s.side * s.side }

// 任何 Shape 都能正确输出面积,没有奇怪的副作用。
func PrintArea(s Shape) {
fmt.Println(s.Area())
}

要点:在 Go 中,里氏替换原则的核心是不要让实现接口的类型对接口契约产生意外行为。如果 Square 不能完全遵循 Rectangle 的隐含行为(set width 不影响 height),就不应该冒充 Rectangle


4. I – 接口隔离原则

不要强迫用户依赖他们不用的方法。大接口应拆分为多个小接口。

反例:一个臃肿的接口

type Worker interface {
Work()
Eat()
Sleep()
}

// 机器人不需要 Eat 和 Sleep,但不得不实现空方法
type Robot struct{}
func (Robot) Work() { fmt.Println("working") }
func (Robot) Eat() {} // 空实现
func (Robot) Sleep(){} // 空实现

// 人类可以实现所有方法
type Human struct{}
func (Human) Work() { fmt.Println("working") }
func (Human) Eat() { fmt.Println("eating") }
func (Human) Sleep() { fmt.Println("sleeping") }

正确做法:拆分接口

type Workable interface {
Work()
}

type Eatable interface {
Eat()
}

type Sleepable interface {
Sleep()
}

// 机器人只实现需要的接口
type Robot struct{}
func (Robot) Work() { fmt.Println("working") }

// 人类组合多个接口
type Human struct{}
func (Human) Work() { fmt.Println("working") }
func (Human) Eat() { fmt.Println("eating") }
func (Human) Sleep() { fmt.Println("sleeping") }

// 使用时根据需要传入具体接口
func DoWork(w Workable) { w.Work() }
func TakeBreak(e Eatable, s Sleepable) { e.Eat(); s.Sleep() }

5. D – 依赖倒置原则

高层模块不应依赖低层模块,两者都应依赖抽象(接口)。
细节应依赖抽象,而不是抽象依赖细节。

反例:高层直接依赖具体实现

// 低层模块:邮件发送器
type EmailSender struct{}

func (e EmailSender) Send(msg string) {
fmt.Println("Sending email:", msg)
}

// 高层模块:通知服务,直接依赖 EmailSender
type NotificationService struct {
sender EmailSender // 直接依赖具体类型
}

func (n NotificationService) Notify(msg string) {
n.sender.Send(msg)
}

// 如果未来需要改成短信发送,必须修改 NotificationService 结构体

正确做法:依赖抽象接口

// 定义抽象接口
type MessageSender interface {
Send(msg string)
}

// 低层实现1:邮件
type EmailSender struct{}
func (EmailSender) Send(msg string) {
fmt.Println("Email:", msg)
}

// 低层实现2:短信
type SmsSender struct{}
func (SmsSender) Send(msg string) {
fmt.Println("SMS:", msg)
}

// 高层模块依赖接口,而不是具体类型
type NotificationService struct {
sender MessageSender // 依赖抽象
}

func (n NotificationService) Notify(msg string) {
n.sender.Send(msg)
}

// 使用时注入具体实现
func main() {
email := EmailSender{}
svc := NotificationService{sender: email}
svc.Notify("hello")

sms := SmsSender{}
svc2 := NotificationService{sender: sms}
svc2.Notify("hello")
}

总结:Go 中实现 SOLID 的常用手法

原则Go 实现方式
S将一个大的结构体拆分为多个小的结构体,每个有自己的方法集
O使用接口 + 策略模式,通过组合而非修改扩展现有行为
L确保实现同一接口的不同类型可以互换且不破坏预期行为
I将大接口拆分为多个小接口,按需组合
D高层模块依赖接口指针/值,而不是具体类型;通过构造函数注入依赖

面试时如果被问到 SOLID,用 Go 举例会展现你不仅理解理论,还能在非经典 OOP 语言中灵活应用。需要我针对某个原则再提供更完整的、可运行的代码示例吗?