一、什么是接口
接口(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) 从接口取出原始类型 |
| 类型 switch | switch v := s.(type) 按类型分支处理 |
| 空接口 any | any = 任何类型,但不要滥用 |
| 接口嵌套 | 小接口组合成大接口,比继承更灵活 |
| 面向接口编程 | 业务依赖接口不依赖实现,切换实现零成本 |