主题
第07章 面试题精选 —— 接口与多态
★ 基础题
Q1:Go 的接口和 Java 的接口有什么不同?
| Java | Go | |
|---|---|---|
| 实现声明 | 显式 implements | 隐式(有方法即实现) |
| 接口包含 | 方法签名 | 方法签名 + 可嵌入接口 |
| 接口字段 | 可有常量 | 没有 |
| 默认方法 | Java 8+ 支持 | 没有 |
| 多重实现 | 可同时 implements 多个 | 自然支持 |
Q2:什么是 any?
any 是 Go 1.18 引入的 interface{} 别名。语义完全等价:
go
type any = interface{}
func print(v any) { ... }
// 等价于
func print(v interface{}) { ... }新代码推荐用 any,更短且语义清晰。
Q3:以下代码输出什么?
go
type Animal interface { Speak() string }
type Dog struct{}
func (d Dog) Speak() string { return "Woof" }
var a Animal = Dog{}
fmt.Println(a.Speak())答:Woof
注意:没有 class Dog implements Animal,但 Dog 因为有 Speak() 方法而自动实现了 Animal。
Q4:怎么判断一个接口变量的具体类型?
go
var v any = "hello"
// 方式1:类型断言
if s, ok := v.(string); ok { ... }
// 方式2:类型 switch(推荐)
switch x := v.(type) {
case int: ...
case string: ...
}
// 方式3:反射(重武器)
reflect.TypeOf(v).Name()Q5:以下代码 panic 吗?
go
var v any = 42
s := v.(string)
fmt.Println(s)答:会 panic:interface conversion: interface {} is int, not string。
修复:用安全断言 s, ok := v.(string)。
★★ 进阶题
Q6:解释 Go 著名的"nil 接口陷阱"。
go
type MyError struct { msg string }
func (e *MyError) Error() string { return e.msg }
func mightFail() error {
var p *MyError
return p // 返回 (*MyError)(nil)
}
func main() {
if err := mightFail(); err != nil {
fmt.Println("失败") // 居然进了!
}
}原因:接口的"非 nil"判断同时考虑类型和值。
p的类型是*MyError,值是 nil- 赋给
error接口后,接口的 Type 部分是*MyError(不为 nil),Value 是 nil err != nil中的 nil 是"Type 和 Value 都为 nil",所以判断结果是true
修复:
go
func mightFail() error {
var p *MyError
if p != nil {
return p
}
return nil
}Q7:方法集(method set)影响接口实现,怎么理解?
go
type Logger interface { Log() }
type S struct{}
func (s *S) Log() { ... } // 注意是 *S
var l Logger
l = &S{} // ✅ *S 的方法集包含 Log
l = S{} // ❌ S 的方法集不包含 Log规则:
T的方法集 = receiver 是T的所有方法*T的方法集 = receiver 是T或*T的所有方法
Q8:接口可以嵌入吗?
可以。Go 标准库经典例子:
go
type Reader interface { Read(p []byte) (n int, err error) }
type Writer interface { Write(p []byte) (n int, err error) }
type Closer interface { Close() error }
type ReadCloser interface {
Reader
Closer
}
type ReadWriteCloser interface {
Reader
Writer
Closer
}实现 ReadWriteCloser 需要同时有 Read、Write、Close 三个方法。
Q9:为什么 io.Reader 只定义了一个方法?
答:Single Responsibility(单一职责)+ 组合优于继承。
io.Reader 只表达"能读"。io.Writer 只表达"能写"。任何能读+能写的东西可以用 io.ReadWriter 组合。
Rob Pike 的名言:"The bigger the interface, the weaker the abstraction."(接口越大,抽象越弱)
实践中,Go 标准库里大量接口只有 1-2 个方法。
Q10:如何检查一个类型是否实现了接口?
方法1:编译期断言(推荐,性能为零)
go
var _ MyInterface = (*MyType)(nil)
// 如果 *MyType 没实现 MyInterface,这行编译失败方法2:运行时反射
go
ifaceType := reflect.TypeOf((*MyInterface)(nil)).Elem()
implements := reflect.TypeOf(MyType{}).Implements(ifaceType)★★★ 深度题
Q11:以下代码 panic 还是输出什么?
go
type T struct{}
func (t *T) M() string { return "M" }
var p *T
var i interface{ M() string } = p
fmt.Println(i.M())答:取决于方法实现。这里输出 "M"(不 panic),因为 M 没解引用 t。
但如果 M 内访问 t.field,就会 panic。
关键点:方法调用本质是函数调用,传 nil 进去函数体,函数体不操作就不出问题。
Q12:你怎么用接口写出"对扩展开放,对修改关闭"的代码?
经典例子(OCP 原则):
go
type Notifier interface {
Send(msg string) error
}
type EmailNotifier struct{}
type SMSNotifier struct{}
type SlackNotifier struct{}
// 都实现 Send 方法...
func Notify(n Notifier, msg string) error {
return n.Send(msg)
}关键:Notify 函数依赖抽象(Notifier 接口),新增"钉钉通知"、"飞书通知"时只要写新类型,Notify 一行代码不用改。
Q13:什么是空接口?空结构体接口?
go
interface{} // 空接口,任何类型都实现它
struct{} // 空结构体,零字节,常用作 set 的 valuego
seen := map[string]struct{}{} // 当 set 用,省内存
ch := make(chan struct{}) // 当信号用,零字节Q14:Go 1.18 泛型出来后,接口还有用吗?
有用。
| 场景 | 泛型 | 接口 |
|---|---|---|
| 编译期类型已知 | 用泛型,零开销 | 接口需运行时分发 |
| 运行时多态(如插件) | 不行 | 必须用接口 |
| 标准库 io/sql/encoding | 仍用接口 | - |
简言之:能用泛型就用泛型(更快),需要运行时多态用接口。
Q15:解释下面代码为什么 panic?
go
var s []int = nil
var i interface{} = s
if i == nil { fmt.Println("nil") } else { fmt.Println("not nil") }答:输出 not nil。
原因:s 是 nil slice,但 i 是 (类型=[]int, 值=nil) 的接口包装。Type 不为 nil,所以 i != nil。
📌 速记口诀
"接口隐式实现;any 是 interface{};类型断言用 .();nil 接口看类型”
🎯 自测练习
- 实现一个
Sortable接口,让多种类型可排序 - 写一个
func Marshal(v any) []byte,根据类型用不同序列化方法 - 解释
var _ MyInterface = (*MyType)(nil)这行代码的作用 - 设计一个插件系统:接口
Plugin,多种实现,运行时根据配置选用
进入第08章 🚀