Go 的接口是隐式的——你不需要声明”我实现了某个接口”。这叫鸭子类型:如果它走起路来像鸭子,叫起来像鸭子,那它就是鸭子。但这个设计也埋了一些坑。


Q1:Go 的接口和 Java 的 interface 有什么本质区别?

A: 一张表说清楚:

特性 Java Interface Go Interface
实现方式 class Dog implements Animal 显式声明 隐式满足,无需声明
方法集 必须实现所有方法 同左,但编译器自动检测
值/指针语义 只有引用类型 值接收者 vs 指针接收者有区别(大坑!)
空接口 无等价物 interface{} 可存任意值
接口组合 extends 多继承 嵌入接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 定义接口
type Speaker interface {
Speak() string
}

// 注意:Dog 没有写 "implements Speaker"!
type Dog struct{ Name string }

func (d *Dog) Speak() string {
return d.Name + ": 汪汪!"
}

// 使用 —— 直接赋值,编译器自动判断
var s Speaker = &Dog{Name: "旺财"}
fmt.Println(s.Speak()) // "旺财: 汪汪!"

这就是隐式接口的核心:你只管写你的结构体和方法,接口在需要的地方自然被满足。

Q2:那个传说中的”值接收者 vs 指针接收者”坑到底怎么回事?

A: 这是 Go 接口最危险的陷阱,没有之一:

1
2
3
4
5
6
7
8
9
10
11
type Mover interface {
Move()
}

// 用值接收者实现
type Car struct{}
func (c Car) Move() { fmt.Println("汽车跑") }

// 用指针接收者实现
type Bike struct{}
func (b *Bike) Move() { fmt.Println("自行车骑") }

看起来都正常?看这里:

1
2
3
4
5
6
7
c := Car{}
var m1 Mover = c // ✅ 值→值接收者 OK
var m2 Mover = &c // ✅ 指针→值接收者 也OK

b := Bike{}
var m3 Mover = b // ❌ 编译错误!值不能赋给需要指针的接口
var m4 Mover = &b // ✅ 指针→指针接收者 OK

规则:

1
2
*T 实现 I    →   T 和 *T 都满足 I
T 实现 I → 只有 T 满足 I,*T 不满足!

实战建议:如果结构体可能通过接口使用,统一用指针接收者,避免这种不一致。

Q3:interface{} 空接口怎么用?不是什么都能塞进去吗?

A: 对,interface{} 可以存任何值,但取出来的时候要小心:

1
2
3
4
5
6
var anything interface{}

anything = 42
anything = "hello"
anything = []int{1, 2, 3}
anything = &Dog{Name: "旺财"}

取出来需要类型断言(Type Assertion):

1
2
3
4
5
6
7
8
9
10
11
anything := 42

// 方式一:安全断言
if v, ok := anything.(int); ok {
fmt.Println(v + 1) // 43
} else {
fmt.Println("不是 int")
}

// 方式二:直接断言(不安全,panic)
s := anything.(string) // panic: interface conversion: int is not string

type switch —— 更优雅的多类型处理

1
2
3
4
5
6
7
8
9
10
11
12
func describe(v interface{}) {
switch v.(type) {
case int:
fmt.Println("整数:", v)
case string:
fmt.Println("字符串:", v)
case *Dog:
fmt.Println("狗:", v.(*Dog).Name)
default:
fmt.Printf("未知类型: %T\n", v)
}
}

泛型出现后的变化

Go 1.18 之后,能用泛型(any)的场景尽量别用空接口了:

1
2
3
4
5
// 旧写法(Go 1.17 及之前)
func Print(v interface{})

// 新写法(Go 1.18+)
func Print[T any](v T)

Q4:error 本质就是一个接口?

A: 对,这是理解 Go 错误处理的钥匙:

1
2
3
4
// error 就是这个内置接口:
type error interface {
Error() string
}

就这么简单。所以任何实现了 Error() string 的类型都可以当 error 用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 自定义错误类型
type NotFoundError struct {
Resource string
ID int
}

func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s (id=%d) not found", e.Resource, e.ID)
}

// 使用
func getUser(id int) (*User, error) {
if id <= 0 {
return nil, &NotFoundError{Resource: "User", ID: id}
}
// ...
}

错误包装(Error Wrapping)是 Go 1.13+ 的标准做法:

1
2
3
4
5
6
7
8
9
// %w 格式化,保留错误链
if err != nil {
return fmt.Errorf("get user failed: %w", err)
}

// 解包
if errors.Is(err, &NotFoundError{}) { ... } // 精确匹配
var nfe *NotFoundError
if errors.As(err, &nfe) { ... } // 类型匹配并提取

Q5:接口可以组合吗?

A: 可以,而且非常优雅——嵌入接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
type Reader interface {
Read(p []byte) (n int, err error)
}

type Writer interface {
Write(p []byte) (n int, err error)
}

// 组合接口
type ReadWriter interface {
Reader // 嵌入
Writer // 嵌入
}

只要同时实现了 Read() 和 Write() 的类型就自动满足 ReadWriter。这就是标准库 io.ReadWriter 的定义方式。

对比 Java 的多接口实现:

1
2
3
4
5
6
7
8
// Java:显式 implements
public class Buffer implements Reader, Writer { ... }

// Go:隐式,只要方法对上了就行
type Buffer struct {}
func (b *Buffer) Read(p []byte) (int, error) { ... }
func (b *Buffer) Write(p []byte) (int, error) { ... }
// 自动满足 Reader、Writer、ReadWriter

Q6:nil 接口是什么鬼?我见过 interface{}(nil) != nil 这种 bug!

A: 这是个经典面试题:

1
2
3
4
5
6
var p *Dog           // p == nil (指针为 nil)
var i interface{} = p // i != nil !!!

fmt.Println(p == nil) // true
fmt.Println(i == nil) // false !!!
fmt.Println(i == interface{}(nil)) // false !!!!

为什么?

接口内部由两部分组成 (type, value):

  • p == nil → (type=nil, value=nil)
  • i = p 后 → (type=*Dog, value=nil)
  • 接口判 nil 要求 type 和 value 都为 nil
  • 现在 type 不是 nil 了,所以 i 不是 nil!
1
2
3
4
5
6
7
8
9
10
11
func process(data interface{}) {
if data == nil {
fmt.Println("真的是空的")
return
}
// 如果传进来一个 nil 指针,走不到这里!
fmt.Println("有数据")
}

process(nil) // "真的是空的" ✅
process((*Dog)(nil)) // "有数据" ❌❌❌ 陷阱!

防御写法:

1
2
3
4
5
6
func process(data interface{}) {
if data == nil || reflect.ValueOf(data).IsNil() {
fmt.Println("真的是空的")
return
}
}

或者更简单——不要传 nil 指针进 interface{},设计上避免这种情况。

Q7:什么时候该定义接口?什么时候不该?

A: Go 社区有个原则叫 Accept interfaces, return structs(接受接口,返回结构体):

场景 建议
函数参数 用接口(解耦)
返回值 用具体结构体(灵活性留给调用方)
包的边界(导出 API) 定义接口
包的内部 不需要接口,直接用具体类型
1
2
3
4
5
6
// ✅ 好:参数用接口
func SaveUser(db UserSaver, u User) error { ... }

// ❌ 过度设计:只有一种实现也搞个接口
type DogBarker interface { Bark() }
// 如果整个项目只有一个 Dog 类型,这接口就是多余的

经验法则:如果你发现自己在问”要不要加个接口”,答案通常是先不加。等到真的有第二个实现时再提取接口也不迟——Go 的隐式接口让你重构零成本。

下期预告

下一篇聊 组合优于继承——Go 没有 extends 怎么做代码复用?嵌入的多种模式、以及什么时候该用接口替代组合。然后我们转入工程实践篇:error 处理最佳实践。

TODO: 回顾一下你 slg-go 项目里的接口定义——有没有哪个地方用了不必要的接口?或者反过来,某个函数参数应该用接口却写了具体类型?