Skip to content

Go 后端面试复盘:语言、并发与服务实践 · 5. 接口至7. 泛型 ​

返回分篇目录

前置:Go 系统学习,并完成一个HTTP 工程练习。本页用于面试前查漏补缺,不按章节替代语言课程。

5. 接口 ​

5.1 Go 接口的核心特点是什么? ​

Go 接口描述一组方法。类型无需显式声明“实现了接口”,只要方法集满足要求就自动实现,这称为隐式实现。

优点:

  • 降低实现与使用方耦合。
  • 接口可以由使用方按需要定义。
  • 易于替换实现和测试。

常见原则:

  • 接受接口,返回具体类型。
  • 接口要小,优先一到几个相关方法。
  • 不要为了“未来可能需要”提前抽象大接口。

5.2 接口的底层结构如何理解? ​

概念上,接口值保存两部分:

  1. 动态类型。
  2. 动态值。

空接口与含方法接口的运行时结构细节不同,但面试中最重要的是二元组模型:只有动态类型和动态值都为空时,接口才等于 nil。

5.3 为什么“接口不等于 nil”是经典坑? ​

go
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 怎么用? ​

go
v, ok := x.(string)
if !ok {
	// 动态类型不是 string
}

不接收 ok 且断言失败会 panic。

go
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 编译期如何检查接口实现? ​

go
var _ io.Reader = (*MyReader)(nil)

如果 *MyReader 未实现 io.Reader,编译失败。这不产生运行时对象,常用于清晰表达设计意图。


6. 函数、闭包、defer、panic 与 recover ​

6.1 Go 函数有什么特点? ​

  • 函数是一等值,可赋值、传参和返回。
  • 支持多返回值。
  • 支持闭包。
  • 不支持函数重载。
  • 可变参数本质上以 slice 形式接收。
go
func sum(nums ...int) int {
	total := 0
	for _, n := range nums {
		total += n
	}
	return total
}

6.2 什么是闭包? ​

闭包是函数值和它引用的外部变量环境的组合。

go
func counter() func() int {
	n := 0
	return func() int {
		n++
		return n
	}
}

外部函数返回后,n 仍然存活,因此可能逃逸到堆。闭包共享变量时要考虑并发竞争和生命周期。

6.3 defer 的执行顺序是什么? ​

多个 defer 按后进先出顺序执行:

go
defer fmt.Println(1)
defer fmt.Println(2)
// 输出 2、1

defer 常用于:

  • 关闭文件、响应体、数据库行集。
  • 解锁。
  • 记录耗时。
  • recover。

6.4 defer 的参数什么时候求值? ​

注册 defer 时立即计算函数值和实参:

go
x := 1
defer fmt.Println(x)
x = 2
// 输出 1

闭包读取变量则通常在真正执行闭包时取值:

go
x := 1
defer func() { fmt.Println(x) }()
x = 2
// 输出 2

6.5 defer 能修改命名返回值吗? ​

可以:

go
func f() (n int) {
	defer func() { n++ }()
	return 10
}
// 返回 11

return 10 可概念化为:

  1. 给返回变量 n 赋值 10。
  2. 执行 defer,n 变成 11。
  3. 真正返回。

不建议滥用这种技巧,否则影响可读性。

6.6 defer 放在循环中有什么风险? ​

defer 在当前函数返回时执行,不是在当前循环迭代结束时执行。循环中打开大量资源并 defer 关闭,可能导致资源长期不释放。

可以把单次迭代提取成函数:

go
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 调用链中直接执行时才有效。

go
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{} 加类型断言。

go
func Max[T cmp.Ordered](a, b T) T {
	if a > b {
		return a
	}
	return b
}

实际代码可使用标准库提供的泛型工具和合适的约束,也可以自定义类型集合。

7.2 什么是类型参数和约束? ​

go
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:

go
func Contains[K comparable, V any](m map[K]V, key K) bool {
	_, ok := m[key]
	return ok
}

7.4 泛型、接口和代码生成如何选择? ​

  • 泛型:相同算法作用于一组类型,且需要编译期类型安全。
  • 接口:关注行为抽象和运行时多态。
  • 代码生成:需要生成大量协议、序列化、ORM 或极致特化代码。
  • 普通函数:只有一两个具体类型时,往往最简单。

不要把所有代码都泛型化。复杂约束会降低可读性,接口行为抽象也不能完全被泛型替代。


继续阅读