Skip to content

第07章 面试题精选 —— 接口与多态


★ 基础题

Q1:Go 的接口和 Java 的接口有什么不同?

JavaGo
实现声明显式 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 的 value
go
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 接口看类型”


🎯 自测练习

  1. 实现一个 Sortable 接口,让多种类型可排序
  2. 写一个 func Marshal(v any) []byte,根据类型用不同序列化方法
  3. 解释 var _ MyInterface = (*MyType)(nil) 这行代码的作用
  4. 设计一个插件系统:接口 Plugin,多种实现,运行时根据配置选用

进入第08章 🚀