Go 的秘密生活:接口

发布: (2026年1月12日 GMT+8 10:53)
9 min read
原文: Dev.to

I’m happy to translate the article for you, but I’ll need the full text you’d like translated. Could you please paste the content (excluding the source line you already provided) here? Once I have the text, I’ll translate it into Simplified Chinese while preserving all formatting, markdown, and code blocks.

第14章:行为的形状

档案馆在那个星期四异常寒冷。暖气片嗞嗞作响,发出金属碰撞声,徒劳地与渗透进百年砖墙的冷风作斗争。

伊森穿着厚外套,戴着无指手套坐在桌前敲键。他看起来很沮丧。

“是重复,”他喃喃自语,盯着屏幕。“我感觉自己在写两遍相同的代码。”

埃莉诺把一小盘点心放在桌上。

“千层酥,”她说。“成千上万层的酥皮,被奶油隔开。各自独立,却又统一。”

伊森盯着点心。

“我真希望我的代码也能这么有条理。看看这个。”

他把笔记本电脑转了过去。

type Admin struct {
    Name  string
    Level int
}

type Guest struct {
    Name string
}

func SaveAdmin(a Admin) error {
    // Logic to save admin to database...
    return nil
}

func SaveGuest(g Guest) error {
    // Logic to save guest to text file...
    return nil
}

func ProcessAdmin(a Admin) {
    if err := SaveAdmin(a); err != nil {
        log.Println(err)
    }
}

func ProcessGuest(g Guest) {
    if err := SaveGuest(g); err != nil {
        log.Println(err)
    }
}

“我有两种用户,”伊森解释道。“管理员写入数据库,访客写入日志文件。但现在我的经理想要一个SuperAdmin,我得写 ProcessSuperAdminSaveSuperAdmin。感觉不对。”

“感觉不对是因为你执着于身份,”埃莉诺倒茶说。“你在问‘这东西是什么?’它是管理员吗?是访客吗?但 Process 函数并不在乎它是什么,它只在乎它能做什么。”

她指向 Save 调用。

“你真正需要的行为是什么?”

“我需要它保存。”

“正是如此。在 Go 语言里,我们用接口来描述行为。”

埃莉诺打开了一个新文件。

“在其他语言里,你可能会创建复杂的层级结构。AbstractUser 继承自 BaseEntity。在 Go 里,我们只需要描述一组方法。”

type Saver interface {
    Save() error
}

“就是它,”她说。“任何拥有 Save() error 方法的类型自动实现了 Saver。你不需要implements Saver。你不需要签署合同。只要把工作做好就行。”

她重构了伊森的代码:

// 1. 在结构体上定义行为(方法)
func (a Admin) Save() error {
    fmt.Println("Saving admin to DB...")
    return nil
}

func (g Guest) Save() error {
    fmt.Println("Writing guest to file...")
    return nil
}

// 2. 编写接受接口的唯一函数
func ProcessUser(s Saver) {
    // 这个函数不知道 s 是 Admin 还是 Guest。
    // 它不在乎,只知道可以调用 Save()。
    if err := s.Save(); err != nil {
        log.Println(err)
    }
}

伊森眨了眨眼。

“等等。Admin 并没有任何地方提到 Saver?”

“不,这叫鸭子类型。如果它走路像鸭子,叫声像鸭子,Go 就把它当成鸭子。因为 AdminSave 方法,它就是一个 Saver。这让你的代码解耦。ProcessUser 不再依赖 AdminGuest,它只依赖行为本身。”

“那我应该为所有东西都写接口吗?”伊森边拿千层酥边问。“我应该写一个 AdminInterface 吗?”

“绝对不要,”埃莉诺严厉地说。“那是 Java 的思维方式。在 Go 里,我们有一条黄金法则:接受接口,返回结构体。”

她在便签本上写下:

  • 接受接口:函数应该请求它们需要的抽象行为(SaverReaderValidator),这样更灵活。
  • 返回结构体:创建对象的函数应该返回具体类型(*Admin*File*Server)。

“为什么?”伊森问。

“因为波特尔法则,”埃莉诺回答。“‘对自己要保守,对外要宽容。’如果你返回一个接口,就会剥夺功能,隐藏数据。如果你返回……”

“看到了吗?” Eleanor 在代码行间指着说。“我们创建具体的东西。但我们把它们作为抽象行为传递。”

“如果我想接受任何东西怎么办?” Ethan 问道。“比如 print(anything)?”

“那就使用 空接口interface{}。在现代 Go 中,也可以用 any。”

func PrintAnything(v any) {
    fmt.Println(v)
}

“因为 any 没有任何方法,Go 中的每一种类型都满足它——整数、结构体、指针——它们都有至少零个方法。”

“听起来很强大,” Ethan 说。

“这很危险,” Eleanor 纠正道。“当你使用 any 时,你会丢失所有的类型安全。你在告诉编译器,‘我不在乎这是什么。’要谨慎使用。只有在真的不关心数据的情况下才使用它,比如在 fmt.Printf 或 JSON 序列化时。”

Ethan 吃完了糕点。

“所以,大接口更好?比如带有 SaveDeleteUpdateValidateUserBehavior?”

“不。接口越大,抽象就越弱,” Eleanor 说。“我们更倾向于单方法接口:ReaderWriterStringerSaver。如果我要求一个 Saver,我可以传入 AdminGuest,甚至是 CloudBackup。如果我要求一个庞大的 UserBehavior,只能传入实现了全部二十个方法的东西。保持接口小巧。”

她合上了笔记本电脑。

“不要定义整个世界,Ethan。只定义你现在需要的行为。”

Ethan 看着他重构后的代码。重复的函数消失了,取而代之的是一个简洁的 ProcessUser(s Saver)

“这就是脱耦,” 他领悟到。“函数不需要知道数据的身份。”

“正是如此,” Eleanor 微笑着,双手环抱着温热的茶杯。“这是一种礼貌的软件设计。我们不问‘你是谁?’我们只问,‘你能保存吗?’”。

隐式实现

实现了所需方法的结构体 符合接口
隐喻: 鸭子类型 – “如果它会呱呱叫,那它就是鸭子。”

定义接口

消费类型时使用接口,而不是在定义它们时使用接口。

type Saver interface {
    Save() error
}

黄金法则

“接受接口,返回结构体。”

  • 接受: 函数输入应使用接口。

    func Do(r io.Reader) { /* … */ }

    这为调用者提供了灵活性。

  • 返回: 函数输出(尤其是构造函数)应使用具体类型。

    func New() *MyStruct { /* … */ }

    这让调用者能够完整访问返回的值。

小接口

  • 示例:io.Readerfmt.Stringer
  • 小接口更容易满足并组合。

空接口 (any)

interface{}   // or the alias `any`
  • 所有 类型满足。
  • 仅在必要时使用(例如,通用打印、容器),因为它会绕过编译时类型检查。

解耦

设计能够与你甚至尚未发明的未来类型一起工作的函数。

func ProcessUser(u User) { /* … */ }

下一章:并发

Ethan 认为一次做两件事很容易——直到 Eleanor 向他展示了竞争条件的混乱——以及通道的禅意。

关于作者

Aaron Rosetech‑reader.blog 的软件工程师和技术作家,也是 Think Like a Genius 的作者。

Back to Blog

相关文章

阅读更多 »

Go 中优雅的领域驱动设计对象

❓ 你如何在 Go 中定义你的领域对象?Go 并不是典型的面向对象语言。当你尝试实现 Domain‑Driven Design(DDD)概念,如 Entity …

Go的秘密生活:测试

第13章:真理表 周三的雨在档案室的窗户上敲出稳定的节奏,把曼哈顿的天际线模糊成灰色的斑块和条纹……

装饰性密码学

请提供您希望翻译的具体摘录或摘要文本,我才能为您进行翻译。