接口与反射
Go 的接口是隐式实现(鸭子类型:结构满足方法集就自动实现接口)。面试主线:接口底层结构、断言、空接口、反射为什么慢。
接口底层:eface 与 iface
iface 带方法表, eface 只有类型
- eface(空接口
interface{}/any):_type(类型信息)+data(数据指针) - iface(有方法的接口):
tab(接口类型 + 具体类型的方法表)+data - 接口值 = 类型 + 数据:一个接口变量是否 nil,要看类型是否为 nil(见下)
nil 接口陷阱(必考):
var p *int = nil
var i interface{} = p // i != nil! 因为 _type 不是 nil
if i == nil { // false
}判断接口是否为 nil 必须用反射(reflect.ValueOf(i).IsNil())或类型断言。
类型断言
var i any = "hello"
s := i.(string) // 断言成功: s = "hello"
n := i.(int) // 失败: panic
s, ok := i.(string) // 安全形式: ok = false 不 panic- 两种形式:单返回值(失败 panic)、双返回值(ok 模式,推荐)
type switch:多类型分支断言- 断言本质:检查接口里的动态类型是否匹配
空接口
interface{}/any:不约束方法,任何类型都满足- 用途:通用容器(
map[string]any)、函数入参(fmt.Println 接收 any) - 代价:类型信息丢失,取值要断言;装箱(堆分配)有开销
- 工程原则:空接口是兜底手段,能明确类型就别用 any(失去编译期检查)
反射:reflect
v := reflect.ValueOf(x)
t := v.Type() // 类型信息
v.Kind() // 基础种类(Struct/Slice/...)
v.Field(i).Set(...) // 运行时修改(需可寻址)反射的代价(考点):
| 代价 | 原因 |
|---|---|
| 慢 | 运行时类型检查 + 动态分发,比直接调用慢一个量级 |
| 逃逸 | 反射操作的值逃逸到堆,增加 GC 压力 |
| 不安全 | 编译期检查失效,错误延迟到运行时 |
反射的正确用途:序列化(encoding/json)、ORM(gorm 标签)、依赖注入、测试断言。热点路径别用反射(可以生成代码替代:stringer、gen 工具)。
面试追问
- eface 和 iface? eface 是空接口(_type + data),iface 是有方法接口(tab 方法表 + data)
- nil 接口的陷阱? 带类型的 nil 赋给接口后接口不为 nil(类型非 nil)。判空用反射或断言
- 类型断言两种形式? 单返回值失败 panic,双返回值(ok)安全。生产用 ok 形式
- 反射为什么慢? 运行时类型检查 + 动态分发 + 逃逸到堆。热点路径避免
- 空接口什么时候用? 通用容器/入参。能明确类型就别用,失去编译期安全