Files
cs-note/hzh/GolangStar/Go面试题库/基础面试题.md
T

13 KiB
Raw Blame History

tags, create time
tags create time
go
golang
interview
basic-questions
2026-06-07 14:30

基础面试题 🧱

概述

本文件涵盖 Go 语言基础语法的 22 道高频面试题,包括变量、类型、控制流、内存分配等核心概念。每道题都附有思考引导和面试答题技巧。

[!tip] 💡 使用建议 先遮住答案自己思考,再对照解析学习。遇到不确定的题目,标记后在对应原理篇中寻找更深入的解释。

关联笔记

正文

Q1:与其他语言相比,使用 Go 有什么好处? 🟢简单

[!question] ❓ 思考一下 如果让你向面试官介绍 Go 的四大优势,你会怎么组织语言?试着用自己的话概括。

参考答案

  1. 语法简洁务实:每个功能设计都以提升开发效率为目标
  2. 原生并发支持:Goroutine + Channel 实现高效的 GMP 调度模型
  3. 代码风格统一:单一标准格式(gofmt),可读性高
  4. 高效垃圾回收:支持并行 GC,效率优于 Java 或 Python

[!tip] 💡 面试技巧 回答时按照"语法 -> 并发 -> 工具链 -> 运行时"的逻辑展开,显得条理清晰。如果能提到"Go 的设计哲学是少即是多(less is more)",会给面试官留下深刻印象。

[!note] 📝 核心考点 Go 的核心竞争力在于:简洁语法 + 原生并发 + 快速编译 + 高效 GC。面试官希望确认你理解 Go 的设计哲学,而非死记硬背特性。


Q2:什么是协程? 🟢简单

参考答案

协程是用户态轻量级线程,是线程调度的基本单位。通常在函数前加上 go 关键字即可实现并发。一个 Goroutine 以极小的栈启动(2KB 或 4KB),栈空间不足时自动伸缩,因此可以轻松启动成千上万个 Goroutine。

[!warning] ⚠️ 高频陷阱 Goroutine 不是操作系统线程!它是用户态线程,由 Go 运行时(runtime)调度,而不是由操作系统内核调度。这是 Goroutine 轻量级的根本原因。

[!info] 🔗 延伸阅读


Q3:协程和线程、进程的区别? 🟢简单

参考答案

维度 进程 线程 协程(Goroutine)
调度单位 系统资源分配最小单位 CPU 调度基本单位 用户态调度
内存空间 独立内存空间 共享所属进程内存 独立栈,共享堆
切换开销 大(上下文复杂) 中等(内核态切换) 极小(用户态切换)
通信方式 IPC(管道/消息队列等) 共享内存 Channel / 共享变量
数量级 几十个 几百个 数十万到百万级

[!tip] 💡 面试技巧 可以用一句话总结:"进程重、线程中、协程轻。Go 用极小的栈空间(2KB)+ 用户态调度 = 海量并发。"


Q4:Go 中 make 和 new 的区别? 🟢简单

[!question] ❓ 思考一下 如果你要创建一个长度为 5 的 slice,你会用 make 还是 new?为什么?

参考答案

make 和 new 都是内存分配的内建函数,但使用场景不同:

make new
用途 初始化并分配内存 仅分配内存,不初始化
适用类型 仅限 slice、map、channel 任何类型
返回值 初始化后的数据结构本身 指向零值的指针
// make:创建已初始化的 slice
s := make([]int, 5) // [0 0 0 0 0]

// new:分配内存,返回 *int 指针
p := new(int)       // *p == 0

[!note] 📝 核心考点 make 返回的是初始化后的值(非指针),new 返回的是指向零值的指针。这是面试中最常混淆的点之一。


Q5:Go 中数组和切片的区别? 🟡中等

参考答案

数组 切片(Slice)
长度 固定,是类型的一部分 可变长
传递方式 值传递 引用传递(地址传递)
底层结构 连续内存块 array 指针 + len + cap
初始化 必须指定大小或推断 可用 make() 或数组截取
// runtime/slice.go
type slice struct {
    array unsafe.Pointer // 元素指针
    len   int           // 长度
    cap   int           // 容量
}

[!warning] ⚠️ 高频陷阱 [3]int 和 [4]int 是两种不同的类型!因为数组的长度是类型的一部分。这意味着你不能将 [3]int 赋值给 [4]int。


Q6:for range 迭代变量的地址会变化吗? 🟡中等

[!question] ❓ 思考一下 在 for range 循环中取迭代变量的地址,存入 slice 后遍历,Go 1.22 前后行为会有什么不同?

参考答案

Go 版本 行为 说明
Go 1.22 之前 地址不变 每次迭代复用同一块内存
Go 1.22 及之后 地址变化 每次迭代生成新的迭代变量
// Go 1.22 之前:所有元素的地址相同
// Go 1.22 及之后:每个元素的地址不同
for index, value := range collection {
    _ = &value // 地址行为因版本而异
}

[!info] 🔗 延伸阅读 Go 1.22 改变了 for range 的语义,使迭代变量每次迭代都有独立的内存地址,避免了经典的"闭包捕获同一变量"陷阱。


Q7:如何高效地拼接字符串? 🟡中等

[!question] ❓ 思考一下 如果要拼接 10000 个字符串,你会选择哪种方法?为什么不用 +?

参考答案

性能排序:strings.Join ≈ strings.Builder > bytes.Buffer > "+" > fmt.Sprintf

方法 特点 适用场景
+ 每次拼接都遍历+开辟新空间 少量字符串拼接
fmt.Sprintf 需要反射,有性能损耗 需要格式化的场景
strings.Builder 指针+切片,避免拷贝 大量字符串拼接推荐
bytes.Buffer 缓冲 byte 类型数据 字节级别操作
strings.Join 基于 Builder,可自定义分隔符 已知切片长度的场景
a := []string{"a", "b", "c"}

// 推荐:strings.Join(已知长度,预分配容量)
ret := strings.Join(a, "")

// 推荐:strings.Builder(未知长度)
var sb strings.Builder
for _, s := range a {
    sb.WriteString(s)
}
ret := sb.String()

[!tip] 💡 面试技巧 如果面试官追问"strings.Join 为什么比 + 快",可以回答:"Join 提前计算总长度并一次性分配内存,而 + 每次都要重新分配。"


Q8:defer 的执行顺序和使用场景? 🟡中等

[!question] ❓ 思考一下 如果有 3 条 defer 语句,它们以什么顺序执行?有名返回值和匿名返回值对 defer 有什么影响?

参考答案

执行顺序:后进先出(LIFO),与声明顺序相反。

func test() int {
    i := 0
    defer func() { fmt.Println("defer1") }()     // 最后执行
    defer func() { i += 1; fmt.Println("defer2") }() // 最先执行
    return i
}
// 输出:defer2 → defer1 → return 0

[!warning] ⚠️ 高频陷阱 有名返回值 vs 匿名返回值:

  • 匿名返回值:return 创建临时变量保存结果,defer 修改不影响返回值
  • 有名返回值:return 直接写入命名变量,defer 可以修改返回值
func test() (i int) { // 有名返回值
    i = 0
    defer func() { i += 1 }()
    return i // 实际返回 1
}

[!note] 📝 核心考点 defer 的三个关键点:LIFO 顺序、延迟计算(参数在 defer 声明时就求值)、有名返回值可被修改。


Q9:什么是 rune 类型? 🟢简单

参考答案

Go 的字符有两种表示:

类型 别名 含义 字节数
byte uint8 ASCII 码的一个字符 1
rune int32 UTF-8 字符(Unicode) 4
str := "hello 你好"
fmt.Println(len(str))        // 12(按字节计算:7个英文+5个汉字×3)
fmt.Println(len([]rune(str))) // 8(按字符计算)

[!tip] 💡 面试技巧 记住关键数字:UTF-8 下一个汉字占 3 个字节,len() 按字节计算,要统计字符数需转为 []rune。


Q10:Go 语言 tag 有什么用? 🟢简单

参考答案

tag 为结构体成员提供元数据属性,常见用途:

Tag 作用 示例
json JSON 序列化/反序列化字段名 `json:"user_name"`
db SQLX 数据库字段名 `db:"id"`
form Gin 框架前端字段映射 `form:"username"`
binding 参数校验规则 `binding:"required"`

Q11:go 打印时 %v %+v %#v 的区别? 🟢简单

参考答案

type student struct { id int32; name string }
a := &student{id: 1, name: "微客鸟窝"}

fmt.Printf("%v\n", a)   // &{1 微客鸟窝}         -- 只输出值
fmt.Printf("%+v\n", a)  // &{id:1 name:微客鸟窝}  -- 显示字段名
fmt.Printf("%#v\n", a)  // &main.student{id:1,name:"微客鸟窝"} -- 完整类型信息

Q12-13:空 struct{} 的空间占用和用途 🟢简单

参考答案

空 struct 不占用任何空间:

unsafe.Sizeof(struct{}{}) // 输出: 0

三大用途:

  1. 模拟 Set:map[string]struct{} 代替 map[string]bool,节省内存
  2. Channel 信号:chan struct{} 仅用于通知,不传数据
  3. 仅有方法的类型:如工具类 type Logger struct{}

Q14:init() 函数什么时候执行? 🟡中等

[!question] ❓ 思考一下 main 包可以有 init() 吗?多个包的 init() 执行顺序是什么?

参考答案

执行顺序:import -> const -> var -> init() -> main()

  1. 在 main() 之前执行
  2. 按依赖关系初始化(无依赖的最先初始化)
  3. 同一个包内可以有多个 init(),执行顺序不保证
  4. init() 无参数、无返回值、不能被调用

[!warning] ⚠️ 高频陷阱 不要在 init() 中做耗时操作或依赖外部服务!它应该在编译期就能确定执行逻辑。


Q15-16:接口比较与 nil 问题 🟡中等

参考答案

两个 interface 相等的条件:

  1. 两者均为 nil
  2. 类型 T 相同且值 V 相等

两个 nil 可能不相等! 只有类型相同时才相等:

var p *int = nil
var i interface{} = nil
// p == i → false(*int != interface{})

nil interface 的经典陷阱:

var c Coder = (*Gopher)(nil) // 动态类型为 *Gopher,值为 nil
c == nil // false!因为动态类型不为 nil

[!note] 📝 核心考点 interface == nil 的条件是动态类型 AND 动态值都为 nil,缺一不可。这是 Go 面试中出现频率最高的陷阱题之一。


Q17:Go 函数传参是值传递还是引用传递? 🟢简单

参考答案

Go 只有值传递。 无论是值类型、引用类型还是指针类型,传递的都是副本:

  • 值类型:复制实际值
  • 引用类型(slice/map/chan):复制指向底层数据的指针
  • 指针类型:复制指针的值

[!tip] 💡 面试技巧 "引用类型能修改外部数据不是因为引用传递,而是因为复制了指针,新旧变量指向同一块内存。"


Q18:如何判断变量分配在栈上还是堆上? 🟡中等

参考答案

通过逃逸分析判断:

go build -gcflags '-m -m -l' xxx.go

逃逸场景:

  • 返回局部变量指针
  • 传递给 interface{} 参数
  • 被闭包引用
  • 超过栈大小限制的大对象

Q19:Go 多返回值如何实现? 🟡中等

参考答案

Go 的多返回值通过在调用方栈帧上预留空间实现:

  1. 编译器计算所有返回值的总大小
  2. 在 caller 的栈帧上预留连续空间
  3. callee 执行 return 时将值复制到预留空间
  4. caller 直接从自己的栈帧读取

[!info] 🔗 延伸阅读


Q20:Go 中 _ 的作用 🟢简单

参考答案

  1. 忽略返回值:x, _ := m["key"]
  2. 匿名导入:_ "net/http/pprof"(仅执行 init 注册 profiling)
  3. 忽略循环变量:for _, v := range slice

Q21-22:指针相关概念 🟡中等

参考答案

对比项 普通指针 (*int) unsafe.Pointer uintptr
类型安全 是,受编译器检查 否,通用指针 是(整数类型)
GC 跟踪 是 是 否
指针运算 不支持 不支持 支持(需转回 Pointer)
类比 C 强类型指针 void* 裸整数地址

[!warning] ⚠️ 高频陷阱 uintptr 只是一个整数,GC 不知道它指向内存,对应内存可能被回收。unsafe.Pointer 受 GC 保护。这是最本质的区别。

关联笔记