Go 后端面试复盘:语言、并发与服务实践 · 5. 接口至7. 泛型
前置:Go 系统学习,并完成一个HTTP 工程练习。本页用于面试前查漏补缺,不按章节替代语言课程。
5. 接口
5.1 Go 接口的核心特点是什么?
Go 接口描述一组方法。类型无需显式声明“实现了接口”,只要方法集满足要求就自动实现,这称为隐式实现。
优点:
- 降低实现与使用方耦合。
- 接口可以由使用方按需要定义。
- 易于替换实现和测试。
常见原则:
- 接受接口,返回具体类型。
- 接口要小,优先一到几个相关方法。
- 不要为了“未来可能需要”提前抽象大接口。
5.2 接口的底层结构如何理解?
概念上,接口值保存两部分:
- 动态类型。
- 动态值。
空接口与含方法接口的运行时结构细节不同,但面试中最重要的是二元组模型:只有动态类型和动态值都为空时,接口才等于 nil。
5.3 为什么“接口不等于 nil”是经典坑?
type MyError struct{}
func (*MyError) Error() string { return "error" }
func f() error {
var p *MyError = nil
return p
}
fmt.Println(f() == nil) // false返回的 error 接口中:
- 动态类型是
*MyError。 - 动态值是 nil。
接口本身不是 nil。
正确做法是在没有错误时直接 return nil,不要把带类型的 nil 指针装入接口。
5.4 类型断言和 type switch 怎么用?
v, ok := x.(string)
if !ok {
// 动态类型不是 string
}不接收 ok 且断言失败会 panic。
switch v := x.(type) {
case string:
fmt.Println("string", v)
case int:
fmt.Println("int", v)
case nil:
fmt.Println("nil")
default:
fmt.Printf("%T\n", v)
}5.5 any 和 interface{} 有区别吗?
没有语义区别,any 是 interface{} 的预声明别名。any 更简洁,但并不代表动态语言;值放入接口后依然携带具体动态类型。
5.6 接口会带来哪些性能成本?
可能的成本包括:
- 间接调用,部分场景较难内联。
- 接口装箱可能导致分配。
- 动态类型判断。
但是否真的产生明显开销必须看编译器优化和实际热路径。不要为了微小理论成本破坏合理抽象,应通过 benchmark 和 pprof 判断。
5.7 编译期如何检查接口实现?
var _ io.Reader = (*MyReader)(nil)如果 *MyReader 未实现 io.Reader,编译失败。这不产生运行时对象,常用于清晰表达设计意图。
6. 函数、闭包、defer、panic 与 recover
6.1 Go 函数有什么特点?
- 函数是一等值,可赋值、传参和返回。
- 支持多返回值。
- 支持闭包。
- 不支持函数重载。
- 可变参数本质上以 slice 形式接收。
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}6.2 什么是闭包?
闭包是函数值和它引用的外部变量环境的组合。
func counter() func() int {
n := 0
return func() int {
n++
return n
}
}外部函数返回后,n 仍然存活,因此可能逃逸到堆。闭包共享变量时要考虑并发竞争和生命周期。
6.3 defer 的执行顺序是什么?
多个 defer 按后进先出顺序执行:
defer fmt.Println(1)
defer fmt.Println(2)
// 输出 2、1defer 常用于:
- 关闭文件、响应体、数据库行集。
- 解锁。
- 记录耗时。
- recover。
6.4 defer 的参数什么时候求值?
注册 defer 时立即计算函数值和实参:
x := 1
defer fmt.Println(x)
x = 2
// 输出 1闭包读取变量则通常在真正执行闭包时取值:
x := 1
defer func() { fmt.Println(x) }()
x = 2
// 输出 26.5 defer 能修改命名返回值吗?
可以:
func f() (n int) {
defer func() { n++ }()
return 10
}
// 返回 11return 10 可概念化为:
- 给返回变量 n 赋值 10。
- 执行 defer,n 变成 11。
- 真正返回。
不建议滥用这种技巧,否则影响可读性。
6.6 defer 放在循环中有什么风险?
defer 在当前函数返回时执行,不是在当前循环迭代结束时执行。循环中打开大量资源并 defer 关闭,可能导致资源长期不释放。
可以把单次迭代提取成函数:
for _, path := range paths {
if err := func() error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
return process(f)
}(); err != nil {
return err
}
}6.7 panic 和 error 应该如何选择?
- 可预期的业务失败、外部输入错误、网络错误:返回 error。
- 程序不变量被破坏、无法继续执行的内部错误:可能 panic。
- 库代码一般不应因普通输入错误 panic。
- 服务边界可以 recover,避免单个请求导致进程退出,但必须记录完整堆栈。
6.8 recover 有什么限制?
recover 只有在发生 panic 的 goroutine 中,并且在 defer 调用链中直接执行时才有效。
defer func() {
if r := recover(); r != nil {
log.Printf("panic: %v\n%s", r, debug.Stack())
}
}()一个 goroutine 无法 recover 另一个 goroutine 的 panic。每个业务 goroutine 若需要隔离,都应在自己的入口设置恢复边界。
7. 泛型
7.1 Go 泛型解决什么问题?
泛型允许函数和类型在保持静态类型安全的同时处理一组类型,减少重复代码和 interface{} 加类型断言。
func Max[T cmp.Ordered](a, b T) T {
if a > b {
return a
}
return b
}实际代码可使用标准库提供的泛型工具和合适的约束,也可以自定义类型集合。
7.2 什么是类型参数和约束?
type Number interface {
~int | ~int64 | ~float64
}
func Sum[T Number](items []T) T {
var total T
for _, v := range items {
total += v
}
return total
}T是类型参数。Number是约束。~int表示底层类型为 int 的自定义类型也满足约束。|表示类型集合并集。
7.3 comparable 是什么?
comparable 是预声明约束,表示支持 == 和 != 的类型,常用于泛型 map key:
func Contains[K comparable, V any](m map[K]V, key K) bool {
_, ok := m[key]
return ok
}7.4 泛型、接口和代码生成如何选择?
- 泛型:相同算法作用于一组类型,且需要编译期类型安全。
- 接口:关注行为抽象和运行时多态。
- 代码生成:需要生成大量协议、序列化、ORM 或极致特化代码。
- 普通函数:只有一两个具体类型时,往往最简单。
不要把所有代码都泛型化。复杂约束会降低可读性,接口行为抽象也不能完全被泛型替代。
继续阅读
- 排错
- 排错
- 排错