Go学习笔记:接口与面向接口编程——Go 最核心的设计

0 阅读7分钟

一、什么是接口

接口(interface)定义了一组方法签名(方法名、参数、返回值)。任何类型只要实现了这些方法,就自动"隐式"满足了这个接口。

这是 Go 和 Java/C# 最大的不同——不需要写 implements

// 定义一个接口
type Speaker interface {
    Speak() string
}

// Dog 和 Cat 都没写 "implements Speaker"
// 但只要有 Speak() 方法,它们就是 Speaker

type Dog struct{ Name string }
func (d Dog) Speak() string { return "汪汪!我是 " + d.Name }

type Cat struct{ Name string }
func (c Cat) Speak() string { return "喵~ 我是 " + c.Name }

接口怎么用?

// 接受接口参数的函数:任何 Speaker 都能传进来
func greet(s Speaker) {
    fmt.Println(s.Speak())
}

greet(Dog{Name: "旺财"})   // 汪汪!我是 旺财
greet(Cat{Name: "咪咪"})   // 喵~ 我是 咪咪

greet 函数不关心传进来的是狗还是猫,只关心它能不能 Speak()。这就是面向接口编程


二、隐式实现(Duck Typing)

Go 的接口实现是隐式的——类型不需要声明实现了哪个接口。

// 不需要像 Java 这样写:
// class Dog implements Speaker { ... }

// Go:类型有了对应的方法,自动就实现了
// 编译时检查,没实现就通不过编译

这就是经典的鸭子类型:如果一个东西走起来像鸭子、叫起来像鸭子,那它就是鸭子。

var s Speaker
s = Dog{Name: "旺财"}  // ✅ Dog 有 Speak(),编译通过
// s = Robot{}          // ❌ Robot 没有 Speak(),编译错误

三、接口匹配规则——必须是"复制粘贴"级别的一致

3.1 匹配条件

一个类型要实现接口,方法名、参数类型、返回值类型必须完全一致

type Speaker interface {
    Speak() string
}

下面这些都实现不了 Speaker:

// ❌ 返回值不同
func (d Dog) Speak() {
    fmt.Println("汪汪")
}

// ❌ 参数不同
func (d Dog) Speak(volume int) string {
    return "汪汪"
}

// ❌ 方法名不同
func (d Dog) Bark() string {
    return "汪汪"
}

// ✅ 完全匹配
func (d Dog) Speak() string {
    return "汪汪"
}

3.2 匹配规则表

条件要求
方法名必须一模一样
参数类型个数、顺序、类型都必须一致
返回值类型个数、顺序、类型都必须一致
参数名不要求,只看类型

重点是最后一条——参数名不影响匹配

type Speaker interface {
    Speak(message string) string
}

// 参数名叫 msg 也可以,不一定要叫 message
func (d Dog) Speak(msg string) string {
    return msg
}

// 只看类型:(string) string 对上了 ✅

💡 接口规定的是"签名形状"(signature),不是"参数名字"。


四、类型断言

有了接口后,有时候需要把接口还原为原始类型。这叫类型断言。

var s Speaker = Dog{Name: "旺财"}

// 语法:s.(类型)
dog := s.(Dog)           // 从接口中取出 Dog
fmt.Println(dog.Name)    // 现在可以访问 Dog 特有的字段了

安全的断言(带 ok)

// 不带 ok:如果猜错了,程序会 panic(崩溃)
// dog := s.(Cat)   // ❌ panic!

// 带 ok:安全,猜错也不会崩溃
cat, ok := s.(Cat)
if ok {
    fmt.Println("是猫:", cat.Name)
} else {
    fmt.Println("不是猫")
}

四、类型 switch

Go 为类型断言提供了专门的 switch 语法:switch v := x.(type)

这在需要根据接口的实际类型做不同处理时非常有用。

func describe(s Speaker) {
    switch v := s.(type) {
    case Dog:
        fmt.Printf("🐕 狗,名字: %s\n", v.Name)
    case Cat:
        fmt.Printf("🐈 猫,名字: %s\n", v.Name)
    default:
        fmt.Println("❓ 未知类型")
    }
}

类型 switch 的特点

特性说明
v := x.(type)特殊写法,只能在 switch 中使用
case Dog:匹配具体类型
default:兜底
变量 v在每个 case 中自动转换为对应类型

与普通 switch 的区别

// 普通 switch:匹配值
switch x {
case 1: ...
case 2: ...
}

// 类型 switch:匹配类型
switch v := x.(type) {
case int:    fmt.Println("整数:", v)
case string: fmt.Println("字符串:", v)
}

💡 每个 case 里的 v 自动变成了对应的类型,不需要再做类型断言。


六、空接口 / any

空接口就是没有任何方法要求的接口,所以任何类型都实现了它:

// 两种写法等价
var v1 interface{}
var v2 any      // Go 1.18+ 推荐用 any
func printValue(v any) {
    fmt.Printf("值: %v, 类型: %T\n", v, v)
}

printValue(42)          // 值: 42, 类型: int
printValue("hello")     // 值: hello, 类型: string
printValue(3.14)        // 值: 3.14, 类型: float64
printValue(Dog{})       // 值: {}, 类型: main.Dog

什么时候用 any

  • 日志记录、序列化等需要处理任意类型的场景
  • 容器类型(如 json.Marshal 接受 any
  • 不要滥用——能用具体接口就用具体接口,用 any 会失去编译时类型检查

七、接口嵌套(组合)

Go 的接口可以通过嵌套来组合多个接口:

type Speaker interface { Speak() string }
type Runner  interface { Run() string }

// 组合成新的接口
type Animal interface {
    Speaker
    Runner
}

Go 推荐**组合(composition)**而不是继承(inheritance)。小接口组合成大接口,比一个大而全的接口更灵活。

// 只依赖 Speaker 的函数
func greet(s Speaker) { ... }

// 只依赖 Runner 的函数
func race(r Runner) { ... }

// 需要两者
func show(a Animal) {
    fmt.Println(a.Speak())
    fmt.Println(a.Run())
}

八、接口值的本质

一个接口变量在底层存的是两个东西:

┌─────────────┐
│  类型指针   │  ← 存着原始类型(Dog / Cat / ...)
│  值指针     │  ← 存着原始值
└─────────────┘
var s Speaker       // nil 接口:类型=nil,值=nil
s = Dog{Name: "旺财"} // 类型=Dog,值=旺财

所以接口值判 nil 要注意:

var s Speaker
fmt.Println(s == nil)  // true

s = Dog{Name: "旺财"}
fmt.Println(s == nil)  // false

// 注意:接口里存了 *Dog(nil) 时,接口本身 != nil
var p *Dog = nil
s = p
fmt.Println(s == nil)  // false!因为接口的类型不是 nil

九、实战:支付接口

用一个贴近业务的例子来收尾。

定义接口

// Order 代表一笔订单
type Order struct {
    ID       string
    Amount   float64
    Currency string
}

// Payer 支付接口——任何支付方式都需要实现 Pay 方法
type Payer interface {
    Pay(order Order) (string, error)
}

微信支付实现

type WeChatPay struct {
    AppID string
}

func (w WeChatPay) Pay(order Order) (string, error) {
    // 调用微信支付 API
    fmt.Printf("[微信支付] 订单 %s,金额 %.2f %s\n",
        order.ID, order.Amount, order.Currency)
    return "微信交易号: wx_" + order.ID, nil
}

支付宝实现

type AliPay struct {
    AppID string
}

func (a AliPay) Pay(order Order) (string, error) {
    // 调用支付宝 API
    fmt.Printf("[支付宝] 订单 %s,金额 %.2f %s\n",
        order.ID, order.Amount, order.Currency)
    return "支付宝交易号: ali_" + order.ID, nil
}

信用卡支付实现

type CreditCard struct {
    CardNumber string
}

func (c CreditCard) Pay(order Order) (string, error) {
    fmt.Printf("[信用卡] 订单 %s,金额 %.2f %s\n",
        order.ID, order.Amount, order.Currency)
    return "信用卡交易号: cc_" + order.ID, nil
}

业务代码只面对接口

// checkout 只依赖 Payer 接口
// 传微信、支付宝、信用卡都行
func checkout(payer Payer, order Order) {
    receipt, err := payer.Pay(order)
    if err != nil {
        fmt.Println("❌ 支付失败:", err)
        return
    }
    fmt.Printf("✅ 支付成功!凭证: %s\n", receipt)
}

func main() {
    order := Order{ID: "20240722001", Amount: 99.9, Currency: "CNY"}

    // 同一个 checkout 函数,三种支付方式
    checkout(WeChatPay{AppID: "wx_xxx"}, order)
    checkout(AliPay{AppID: "ali_xxx"}, order)
    checkout(CreditCard{CardNumber: "6222****1234"}, order)
}

输出:

[微信支付] 订单 20240722001,金额 99.90 CNY
✅ 支付成功!凭证: 微信交易号: wx_20240722001
[支付宝] 订单 20240722001,金额 99.90 CNY
✅ 支付成功!凭证: 支付宝交易号: ali_20240722001
[信用卡] 订单 20240722001,金额 99.90 CNY
✅ 支付成功!凭证: 信用卡交易号: cc_20240722001

为什么这样设计好

场景需要做的事
新增一种支付方式写一个新 struct,实现 Pay 方法即可
切换支付方式调用时换一个实现类就行
写单元测试可以写一个 MockPayer,不用真的扣钱
业务代码完全不用改

这就是面向接口编程的核心价值——对扩展开放,对修改关闭(开闭原则)。


十、总结

知识点一句话
接口定义一组方法签名
隐式实现有方法就自动实现,不需要写 implements
类型断言s.(Dog) 从接口取出原始类型
类型 switchswitch v := s.(type) 按类型分支处理
空接口 anyany = 任何类型,但不要滥用
接口嵌套小接口组合成大接口,比继承更灵活
面向接口编程业务依赖接口不依赖实现,切换实现零成本