Menu
CoddyTech

接口满足规则

本课属于 Coddy GO 学习旅程的“面向对象编程”单元。第 43 课,共 107 课。

虽然 Go 使用鸭子类型来实现隐式接口满足,但仍有一些具体规则决定某个类型是否真正满足某个接口。了解这些规则可以帮助你避免不易察觉的 bug。

最重要的规则与指针接收者有关。如果方法使用指针接收者定义,那么只有指向该类型的指针才满足接口要求,值本身不满足:

type Saver interface {
    Save() string
}

type Document struct{ Name string }

func (d *Document) Save() string {  // 指针接收者
    return "Saved: " + d.Name
}

func Process(s Saver) {
    fmt.Println(s.Save())
}

由于 Save() 使用指针接收者,只有 *Document 才满足 Saver:

func main() {
    doc := Document{Name: "report.txt"}
    
    Process(&doc)  // 可以运行:指针满足接口要求
    // Process(doc)  // 编译错误:值不满足接口要求
}

不过,反过来会更灵活。如果方法使用值接收者,值类型和指针类型都满足该接口。调用值接收者方法时,Go 会自动解引用指针:

func (d Document) Info() string {  // 值接收者
    return d.Name
}

// Document 和 *Document 都满足要求实现 Info() 的接口

这种不对称性存在的原因是,Go 总能从指针获取值(通过解引用),但不一定总能从值获取指针(该值可能无法寻址)。记住这条规则,可以避免在使用接口时遇到令人困惑的编译器错误。

challenge icon

挑战

简单

我们来构建一个配置系统,演示指针接收者和值接收者如何影响接口实现。你将创建一些类型,接收者的选择会决定值、指针或两者是否都能用于某个接口。

你将把代码分成三个文件:

  • config.go:定义一个 Configurable 接口,要求实现两个方法:GetValue() string 和 SetValue(string)。然后创建两种配置类型:
    • ReadOnlyConfig,包含一个 Value 字段:为 GetValue() 使用值接收者(返回 Value),为 SetValue() 使用指针接收者(更新 Value)
    • Setting,包含一个 Data 字段:两个方法都使用值接收者(GetValue 返回 Data,SetValue 只打印 "Cannot modify",不做任何更改)
  • processor.go:创建一个名为 ProcessConfig 的函数,它接收一个 Configurable 和一个新的字符串值。函数应使用 GetValue() 打印当前值,使用新值调用 SetValue(),然后再次打印该值,以显示发生的任何变化。
  • main.go:从输入中读取配置信息,并演示接口实现规则:
    1. 创建一个 ReadOnlyConfig,并将一个指针传给 ProcessConfig(这是必需的,因为 SetValue 使用指针接收者)
    2. 创建一个 Setting,并将值本身直接传给 ProcessConfig(这是可行的,因为两个方法都使用值接收者)

将提供以下输入:

  • 第 1 行:ReadOnlyConfig 的初始值
  • 第 2 行:要为 ReadOnlyConfig 设置的新值
  • 第 3 行:Setting 的初始值
  • 第 4 行:尝试为 Setting 设置的新值

你的 ProcessConfig 函数应按以下格式打印:

Current: [value]
Current: [value after SetValue]

例如,输入 debug、production、localhost 和 remote 后,输出应为:

Current: debug
Current: production
Current: localhost
Cannot modify
Current: localhost

注意,ReadOnlyConfig 的值确实发生了变化(因为我们传入了一个指针),而 Setting 保持不变(它的 SetValue 使用值接收者,无法修改原始值)。关键在于,单独的 ReadOnlyConfig 值无法实现 Configurable 接口,只有指针可以;而 Setting 值可以直接使用,因为它的所有方法都使用值接收者。

亲手试试

package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	scanner := bufio.NewScanner(os.Stdin)
	
	// 读取输入
	scanner.Scan()
	rocInitial := scanner.Text()
	scanner.Scan()
	rocNew := scanner.Text()
	scanner.Scan()
	settingInitial := scanner.Text()
	scanner.Scan()
	settingNew := scanner.Text()
	
	// TODO: 用 rocInitial 的值创建一个 ReadOnlyConfig
	// TODO: 向 ProcessConfig 传入指针(因为 SetValue 的接收者是指针)
	
	// TODO: 用 settingInitial 的值创建一个 Setting
	// TODO: 直接向 ProcessConfig 传入值(因为两个方法的接收者都是值类型)
	
	// 使用这些变量,避免出现变量未使用的错误
	_ = rocInitial
	_ = rocNew
	_ = settingInitial
	_ = settingNew
}
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: Go 在线编译器