Skip to content

Go 面试题

1 基础篇

1.1 Go包管理的方式有哪些

发展历史:

  • < Go1.5:GOPATH
    • 通过统一包存放的路径实现包管理
    • 不支持依赖包的版本控制
  • >= Go1.5:GO Vendor
    • Go1.5 引入,需要通过环境变量 GO15VENDOREXPERIMENT=1 开启
    • Go1.6 Vendor 机制默认开启
    • 把源码拷贝到 vendor 目录并维护 vendor.json 文件,指定版本
  • >= Go1.11:GO Modules
    • Go1.11 中,GO111MODULE 的默认值是 auto
      • 当项目在 GOPATH 之外,且包含 go.mod 文件时,自动启用 Modules;
      • 当项目在 GOPATH 内时,默认不启用(仍使用 GOPATH 模式)。
    • Go1.13 起,GO111MODULE 默认值改为 on,彻底以 Modules 为主要包管理方式。

1.2 init() 是什么时候执行的

init() 函数的作用:

  • 程序执行前包的初始化

init() 函数的执行顺序:

  • 在同一个 Go 文件中的多个 init 方法,按照代码顺序依次执行
  • 同一个包内不同文件中的 init() 函数,按照文件名顺序执行
  • 不同的包且不相互依赖,按照 import 顺序执行
  • 存在依赖关系的包,被依赖的包先执行 init()

go 文件的初始化顺序:

  1. 引入的包
  2. 当前包中的常量
  3. 当前包中的变量
  4. 当前包的 init() 函数
  5. 若为 main 包,最终执行 main() 函数

1.3 newmake 的区别

  • make 不仅分配内存,还会初始化。new 只会分配零值填充的值(例如,int 的零值是 0*int 的零值是 nil[]int 的零值是 nil
  • make 只适用于 slicemapchannel 的数据,new 没有限制
  • make 返回原始类型(T),new 返回类型的指针(*T)

1.4 内存逃逸

1.4.1 什么是内存逃逸

Go 中,函数内的局部变量默认分配在栈上(栈内存由编译器自动分配和释放,效率极高)。但在某些情况下,变量会被移动到堆上分配,这种现象称为内存逃逸。逃逸分析是编译器决定变量分配位置的过程。

核心区别:

  • 栈分配:函数退出后,栈内存自动释放,无需垃圾回收(GC)。
  • 堆分配:变量生命周期不确定,需由 GC 管理,会增加 GC 压力。

1.4.2 发生内存逃逸的常见场景

编译器进行逃逸分析时,若发现变量的生命周期无法在编译期确定或栈无法容纳,就会将其分配到堆上。常见场景包括:

  1. 变量被外部引用(跨函数生命周期)

若函数返回变量的指针或引用,且该指针被外部持有(变量需在函数退出后继续存在),变量会逃逸到堆。

go
func create() *int {
	x := 10   // x 会逃逸到堆
	return &x // 返回指针,x 需在函数外存活
}

func main() {
	p := create()
	fmt.Println(*p)
}

原因:函数退出后,栈会被销毁,若变量仍被外部引用,必须放在堆上。

  1. 变量大小超过栈的承载能力

栈的空间有限,若变量体积过大(如超大数组),编译器会将其分配到堆上。

go
func bigData() {
	data := [1000000]int64{} // 大小超过栈限制,逃逸到堆
}

原因:避免栈溢出(stack overflow),堆的空间更大且动态分配。

  1. 闭包引用并修改外部变量

闭包会捕获外部变量的引用,若闭包的生命周期长于变量的原始作用域,变量会逃逸到堆。

go
func closure() func() {
	x := 10
	return func() {
		x++ // 闭包修改 x,x 需在 closure 退出后存活
		fmt.Println(x)
	}
}

func main() {
	f := closure()
	f() // 调用闭包时,x 仍需存在
}

原因:闭包可能在函数退出后被调用,变量需脱离原函数栈存活。

  1. 变量类型为接口(动态类型不确定)

当变量被赋值给接口类型,且编译器无法在编译期确定其具体类型(动态类型),变量会逃逸到堆。

go
type animal interface {
	run()
}

type dog struct{}

func (d dog) run() {}

func main() {
	// 声明的同时赋值了,不会发生逃逸
	var a1 animal = dog{}
	a1.run()

	// 接口类型声明时没赋值,动态类型不确定,发生内存逃逸
	var a2 animal
	a2 = dog{}
	a2.run()
}

原因:接口的动态类型处理需要 runtime 支持,堆分配更灵活。

  1. 切片 / 映射的动态扩容或长度不确定

若切片的长度是动态计算的(非编译期常量),或可能发生扩容(底层数组需更换),其底层数组可能逃逸到堆。

go
func main() {
	n, _ := strconv.Atoi(os.Args[1])
	_ = make([]int, n)
}