「Go语言面试题库」(150+题)
由浅入深 · 全面覆盖 · 代码可运行
📋 目录
- 基础语法 (20题)
- 数据类型 (25题)
- 函数与方法 (20题)
- 接口与反射 (20题)
- 并发编程 (30题)
- 内存与GC (15题)
- 标准库 (15题)
- 工程实践 (15题)
- 源码与高级主题 (20题)
🚀 如何运行代码
题库中所有代码均可在 Go Playground 运行:
- 访问 https://go.dev/play
- 将代码复制到编辑器
- 点击 Run 按钮执行
- 如需分享,点击 Share 获取链接
一、基础语法 (20题)
第1题:变量声明与零值
问题:以下代码输出什么?为什么?
package main
import "fmt"
func main() {
var a int
var b string
var c bool
var d []int
fmt.Printf("a=%d, b=%q, c=%t, d==nil:%t\n", a, b, c, d == nil)
}
Playground:https://go.dev/play/p/ypEhyJMcd8Q
答案:
a=0, b="", c=false, d==nil:true
解析:
- Go中变量声明后会被赋予零值(zero value)
int的零值是0string的零值是空字符串""bool的零值是false- 引用类型(slice、map、channel、指针、interface)的零值是
nil
第2题:短变量声明的作用域
问题:以下代码能否编译通过?如果不能,如何修改?
package main
import "fmt"
func main() {
x := 10
if x > 5 {
x := 5 // 这里的x是新变量还是赋值?
fmt.Println("inner:", x)
}
fmt.Println("outer:", x)
}
答案:能编译通过,输出:
inner: 5
outer: 10
解析:
:=在内部作用域声明了新的局部变量x,遮蔽了外部的x- 这是Go语言中常见的**变量遮蔽(variable shadowing)**现象
- 如果意图是修改外部x,应使用赋值语句
x = 5
第3题:多变量赋值与交换
问题:以下代码输出什么?利用了什么特性?
package main
import "fmt"
func main() {
a, b := 1, 2
a, b = b, a
fmt.Println(a, b)
}
Playground:https://go.dev/play/p/5u012hcK1o0
答案:输出2 1,成功交换了两个变量的值。
解析:
- Go支持并行赋值(multiple assignment)
- 赋值前,右边的所有表达式先求值,然后同时赋给左边
- 不需要临时变量即可完成交换
第4题:const常量
问题:以下代码有什么问题?
package main
const (
A = iota
B
C
)
func main() {
const x = 100
x = 200 // 错误?
const y = len("hello") // 能否编译?
}
Playground:https://go.dev/play/p/LVHoQKmlJPf
答案:
x = 200编译错误:常量不能重新赋值const y = len("hello")可以编译(len在常量表达式中求值)
解析:
- Go常量必须在编译期确定值
iota是常量声明中的枚举计数器,从0开始- 内置函数
len、cap、real、imag、complex、unsafe.Sizeof在常量表达式中可用
第5题:iota枚举
问题:以下代码输出什么?
package main
import "fmt"
const (
_ = iota
KB = 1 << (10 * iota)
MB
GB
)
func main() {
fmt.Println(KB, MB, GB)
}
答案:输出1024 1048576 1073741824
解析:
_跳过iota=0的位置iota在同一const块中每行递增1<<是位左移运算符,实现单位换算- KB = 1 « 10 = 1024
- MB = 1 « 20 = 1048576
- GB = 1 « 30 = 1073741824
第6题:字符串与rune
问题:以下代码输出什么?为什么长度不同?
package main
import "fmt"
func main() {
s := "Hello, 世界"
fmt.Println(len(s)) // ?
fmt.Println(len([]rune(s))) // ?
}
Playground:https://go.dev/play/p/k38lW2uUHiJ
答案:输出13和9
解析:
- Go字符串是UTF-8编码的字节序列
len(s)返回字节数,中文字符占3字节[]rune(s)将字符串转为rune切片,每个rune代表一个Unicode码点- “Hello, “占7字节,“世界"占6字节,共13字节
- 但字符数是9个(7个ASCII + 2个中文)
第7题:for-range遍历
问题:以下代码输出什么?有什么陷阱?
package main
import "fmt"
func main() {
arr := []int{1, 2, 3}
for i, v := range arr {
if i == 0 {
arr[1], arr[2] = 999, 999
}
fmt.Printf("i=%d, v=%d\n", i, v)
}
}
Playground:https://go.dev/play/p/XAFb17qhXr_v
答案:
i=0, v=1
i=1, v=999
i=2, v=999
解析:
range遍历切片时,虽然会拷贝切片header,但底层数组是共享的- 当
i=0时修改了arr[1]和arr[2],后续迭代取到的v已经是修改后的值 - 遍历数组时才会拷贝整个数组,此时修改不影响遍历值
对比:
// 切片(底层数组共享)
arr := []int{1, 2, 3} // 输出: 1, 999, 999
// 数组(值拷贝)
arr := [3]int{1, 2, 3} // 输出: 1, 2, 3
第8题:range遍历的变量复用(⚠️ 版本差异题)
问题:以下代码有什么问题?如何修复?
package main
import "fmt"
func main() {
arr := []int{1, 2, 3}
var out []*int
for _, v := range arr {
out = append(out, &v)
}
for _, p := range out {
fmt.Println(*p)
}
}
Playground:https://go.dev/play/p/dySdpNSAXUs
答案:
- Go 1.21及之前:输出
3 3 3 - Go 1.22+:输出
1 2 3
解析:
-
Go 1.21及之前:range循环中的变量
v是复用的单一变量,不是每次迭代创建新变量。所有&v指向同一个内存地址,最终存储的是最后一次迭代的值3。 -
Go 1.22+:每次迭代都会创建新的变量,
v不再是同一个变量,所以&v指向不同的地址,输出预期的1 2 3。 -
向后兼容修复(推荐在所有版本中使用):
for i := range arr { v := arr[i] // 明确创建局部副本 out = append(out, &v) } // 或使用索引直接取地址 for i := range arr { out = append(out, &arr[i]) }
第9题:switch语句
问题:以下代码输出什么?switch在Go中有什么特点?
package main
import "fmt"
func main() {
x := 5
switch {
case x > 3:
fmt.Println(">3")
fallthrough
case x > 4:
fmt.Println(">4")
case x > 5:
fmt.Println(">5")
}
}
Playground:https://go.dev/play/p/IXx2n2a7RaP
答案:
>3
>4
解析:
- Go的switch默认不穿透(不需要break)
- 使用
fallthrough关键字可以强制执行下一个case - switch表达式可以省略(默认true,用于if-else链替代)
- case支持多个值、表达式,不需要是常量
第10题:switch与类型断言
问题:以下代码输出什么?
package main
import "fmt"
func checkType(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("int: %d\n", v)
case string:
fmt.Printf("string: %s\n", v)
case nil:
fmt.Println("nil")
default:
fmt.Printf("unknown: %T\n", v)
}
}
func main() {
checkType(42)
checkType("hello")
checkType(3.14)
}
Playground:https://go.dev/play/p/XdvKz5HO_F7
答案:
int: 42
string: hello
unknown: float64
解析:
switch v := i.(type)是类型开关(type switch)- 可以获取interface的动态类型
- 每个case中v的类型会自动转换为对应类型
第11题:goto语句
问题:Go中的goto有什么限制?以下代码能编译吗?
package main
import "fmt"
func main() {
i := 0
LOOP:
if i >= 5 {
return
}
fmt.Println(i)
i++
goto LOOP
}
Playground:https://go.dev/play/p/KbieuY6I3hD
答案:能编译,输出0到4。
解析:
- Go支持goto,但有严格限制:
- 不能跳转到其他函数
- 不能跳转到变量声明之后(跳过声明)
- 不能跳入代码块(会绕过某些初始化)
- 实践中很少使用,主要用于错误处理或跳出深层循环
第12题:标签与break/continue
问题:以下代码输出什么?标签有什么用?
package main
import "fmt"
func main() {
OUTER:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if i == 1 && j == 1 {
break OUTER
}
fmt.Printf("(%d,%d)", i, j)
}
}
}
答案:输出(0,0)(0,1)(0,2)(1,0)
解析:
break LABEL可以跳出指定标签标识的循环- 同理
continue LABEL可以跳到指定标签的下一轮迭代 - 避免使用flag变量或多次break跳出多层循环
第13题:指针基础
问题:以下代码输出什么?
package main
import "fmt"
func addOne(x int) {
x++
}
func addOnePtr(x *int) {
*x++
}
func main() {
a := 1
addOne(a)
fmt.Println(a)
addOnePtr(&a)
fmt.Println(a)
}
Playground:https://go.dev/play/p/2eftrWz2pBI
答案:
1
2
解析:
- Go是值传递,函数参数是原变量的副本
- 要修改原变量需要传递指针
&取地址,*解引用- 注意:Go不支持指针运算(如
p++移动指针位置)
第14题:new与make的区别
问题:以下代码有什么问题?
package main
func main() {
var s1 []int = new([]int) // 正确吗?
var m1 map[string]int = new(map[string]int) // 正确吗?
var s2 []int = make([]int, 5) // 正确吗?
var p *int = make(*int) // 正确吗?
}
Playground:https://go.dev/play/p/9xGW8_pL-jQ
答案:
new([]int)返回*[]int,不能赋值给[]intnew(map[string]int)语法正确但返回空map指针,很少这样用make([]int, 5)正确,返回已初始化的slicemake(*int)编译错误,make不能用于指针
解析:
| 特性 | new | make |
|---|---|---|
| 返回类型 | 指向类型的指针 | 类型本身(非指针) |
| 适用类型 | 任意类型 | slice、map、channel |
| 初始化 | 零值 | 非零值(已可用) |
| 示例 | new(int)返回*int |
make([]int, 5)返回[]int |
第15题:类型别名与类型定义
问题:以下代码输出什么?有什么区别?
package main
import "fmt"
type MyInt1 = int // 类型别名
type MyInt2 int // 新类型
func main() {
var i int = 10
var a MyInt1 = i
var b MyInt2 = i // 能编译吗?
fmt.Printf("a type: %T, b type: %T\n", a, b)
}
Playground:https://go.dev/play/p/-VT7eCEcnak
答案:var b MyInt2 = i编译错误:cannot use i (type int) as type MyInt2
解析:
type MyInt1 = int是类型别名(Go 1.9+),MyInt1和int完全相同type MyInt2 int是类型定义,创建了一个基于int的新类型- 新类型和原类型是不同类型,需要显式转换
- 类型别名主要用于代码重构时保持兼容性
第16题:类型转换(⚠️ 不推荐用法)
问题:以下代码输出什么?哪些转换会报错?
package main
import "fmt"
func main() {
var i int = 10
var f float64 = float64(i)
var s string = string(i) // 结果是什么?
// var b []byte = []byte(i) // 能编译吗?
fmt.Println(f, s)
}
Playground:https://go.dev/play/p/0q0IomU5zKe
答案:输出10和一个换行符(ASCII 10是换行符)。[]byte(i)编译错误。
解析:
- 数值类型之间可以显式转换
string(int)将整数转为对应的Unicode字符,不是数字字符串string(65)= “A”string(10)是换行符(ASCII 10)
⚠️ Go 1.15+警告:
go vet会对string(int)发出警告:“conversion from int to string yields a string of one rune, not a string of digits”- 现代推荐做法:
// 将整数转为数字字符串 s := strconv.Itoa(10) // "10" s := fmt.Sprintf("%d", 10) // "10" s := string(rune(65)) // "A"(明确意图) - 不同类型之间不能隐式转换,必须显式转换
第17题:可变参数
问题:以下代码输出什么?如何实现可变参数?
package main
import "fmt"
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
func main() {
fmt.Println(sum(1, 2, 3))
fmt.Println(sum())
arr := []int{4, 5, 6}
fmt.Println(sum(arr...))
}
Playground:https://go.dev/play/p/qcITJJnr8Z5
答案:
6
0
15
解析:
...int表示可变参数,函数内部视为[]int- 可以传0个或多个参数
- 可以用
slice...展开slice作为参数 - 可变参数必须是最后一个参数
第18题:init函数
问题:以下代码输出顺序是什么?
package main
import "fmt"
var _ = initVar()
func initVar() int {
fmt.Println("initVar")
return 0
}
func init() {
fmt.Println("init 1")
}
func init() {
fmt.Println("init 2")
}
func main() {
fmt.Println("main")
}
Playground:https://go.dev/play/p/0Hj0aeBlCvM
答案:
initVar
init 1
init 2
main
解析:
- 程序执行顺序:导入包 → 包级变量初始化 → init函数 → main函数
- 一个包可以有多个init函数,按声明顺序执行
- init函数在包被导入时自动执行,不能被显式调用
- 每个包可以被多个文件包含,每个文件的init按文件名排序执行
第19题:main包的特殊性
问题:以下代码能运行吗?为什么?
package mymain
import "fmt"
func main() {
fmt.Println("hello")
}
Playground:https://go.dev/play/p/aqlpxGgsExH
答案:编译错误:
- 如果文件名是main.go:
function main is undeclared in the main package(package名不是main) - 或者:
cannot find main function in main package(如果包名改成main但文件不对)
解析:
- 可执行程序的入口包必须是
package main func main()是程序入口点,无参数无返回值- 库包不能使用package main
第20题:代码格式化规范
问题:Go有哪些强制的代码格式规范?
答案:
- 缩进:必须使用Tab(非空格)
- 括号:
{必须与函数声明在同一行(不能换行) - 行尾:不需要分号(自动插入)
- 导入:未使用的导入会编译错误
- 变量:未使用的局部变量会编译错误(下划线
_可忽略)
解析:
gofmt是官方格式化工具,统一代码风格- 强制规则通过编译器检查,确保代码整洁
- 使用
go fmt命令格式化代码 - 编辑器应配置保存时自动格式化
二、数据类型 (25题)
第21题:数组与切片的区别
问题:以下代码输出什么?数组和切片有什么区别?
package main
import "fmt"
func modifyArray(arr [3]int) {
arr[0] = 100
}
func modifySlice(s []int) {
s[0] = 100
}
func main() {
arr := [3]int{1, 2, 3}
slice := []int{1, 2, 3}
modifyArray(arr)
modifySlice(slice)
fmt.Println(arr)
fmt.Println(slice)
}
答案:
[1 2 3]
[100 2 3]
解析:
| 特性 | 数组 | 切片 |
|---|---|---|
| 长度 | 固定,类型的一部分 | 可变 |
| 传递 | 值传递(拷贝整个数组) | 引用传递(拷贝header) |
| 底层 | 直接存储数据 | 指向底层数组的header |
| 比较 | 可比较(元素可比较时) | 不可比较(只能用nil) |
第22题:切片的底层结构
问题:以下代码输出什么?为什么?
package main
import "fmt"
func main() {
s := []int{0, 1, 2, 3, 4}
s1 := s[1:3]
fmt.Println(len(s1), cap(s1))
s1 = append(s1, 100)
fmt.Println(s)
fmt.Println(s1)
}
Playground:https://go.dev/play/p/p4Bj2I9iGU-
答案:
2 4
[0 1 2 100 4]
[1 2 100]
解析:
- 切片header包含:指针、长度(len)、容量(cap)
s[1:3]创建的切片:len=3-1=2,cap=5-1=4- append在容量足够时直接修改底层数组,会影响原切片
- 这是一个常见的副作用bug
第23题:append导致重新分配
问题:以下代码输出什么?什么时候会重新分配?
package main
import "fmt"
func main() {
s := []int{0, 1, 2, 3, 4}
s1 := s[1:3:3] // 注意第三个参数
fmt.Println(len(s1), cap(s1))
s1 = append(s1, 100)
fmt.Println(s)
fmt.Println(s1)
}
Playground:https://go.dev/play/p/HoiDSIAKUNY
答案:
2 2
[0 1 2 3 4]
[1 2 100]
解析:
s[low:high:max]是完整切片表达式,max限制容量cap = max - low = 3 - 1 = 2- append时发现容量不够(cap=2, len=2),会分配新数组
- 新数组与原数组脱离关系,修改不影响原切片
第24题:nil切片与空切片
问题:以下代码输出什么?nil切片和空切片有什么区别?
package main
import "fmt"
func main() {
var s1 []int // nil切片
s2 := []int{} // 空切片
s3 := make([]int, 0) // 空切片
fmt.Printf("s1 == nil: %t, len=%d, cap=%d\n", s1 == nil, len(s1), cap(s1))
fmt.Printf("s2 == nil: %t, len=%d, cap=%d\n", s2 == nil, len(s2), cap(s2))
fmt.Printf("s3 == nil: %t, len=%d, cap=%d\n", s3 == nil, len(s3), cap(s3))
}
Playground:https://go.dev/play/p/oX2jD0M4WTW
答案:
s1 == nil: true, len=0, cap=0
s2 == nil: false, len=0, cap=0
s3 == nil: false, len=0, cap=0
解析:
- nil切片:未初始化,指针为nil,
== nil为true - 空切片:已初始化但长度为0,
== nil为false - JSON序列化时:nil切片为
null,空切片为[] - 通常建议返回空切片而非nil(避免前端null判断)
第25题:切片扩容机制
问题:切片的扩容策略是什么?以下代码输出什么?
package main
import "fmt"
func main() {
s := make([]int, 0, 2)
fmt.Printf("len=%d, cap=%d\n", len(s), cap(s))
s = append(s, 1)
fmt.Printf("len=%d, cap=%d\n", len(s), cap(s))
s = append(s, 2)
fmt.Printf("len=%d, cap=%d\n", len(s), cap(s))
s = append(s, 3)
fmt.Printf("len=%d, cap=%d\n", len(s), cap(s))
}
Playground:https://go.dev/play/p/b2wxc0IO8UC
答案:
len=0, cap=2
len=1, cap=2
len=2, cap=2
len=3, cap=4
解析:
- 扩容策略(Go 1.18+):
- 原容量 < 256:新容量 = 原容量 × 2
- 原容量 ≥ 256:新容量 = 原容量 + (原容量 + 3×256) / 4
- 新数组分配后,旧数据会拷贝到新数组
- 频繁扩容性能差,应预分配容量
- 注意:Go 1.23+对切片扩容策略有进一步微调,但对于大多数应用场景行为一致
第26题:copy函数
问题:以下代码输出什么?copy有什么特点?
package main
import "fmt"
func main() {
src := []int{1, 2, 3, 4, 5}
dst := make([]int, 3)
n := copy(dst, src)
fmt.Printf("n=%d, dst=%v\n", n, dst)
// 切片复制自身
copy(src[1:], src[2:])
fmt.Println(src)
}
Playground:https://go.dev/play/p/uC_pSA4nfXI
答案:
n=3, dst=[1 2 3]
[1 3 4 5 5]
解析:
copy(dst, src)返回实际复制的元素个数(min(len(dst), len(src)))- 支持同一切片内的复制(如删除元素)
- copy是按字节复制的,即使是重叠区域也能正确处理
- 第一个输出是3,因为dst长度为3
- 第二个是删除索引1元素的效果
第27题:map基础
问题:以下代码有什么问题?
package main
import "fmt"
func main() {
var m map[string]int
m["key"] = 1 // 有什么问题?
m2 := make(map[string]int)
m2["key"] = 2
fmt.Println(m, m2)
}
Playground:https://go.dev/play/p/5dJcD25GmZp
答案:m["key"] = 1运行时panic:assignment to entry in nil map
解析:
- 未初始化的map是nil,不能写入
- 必须用make初始化或用字面量创建
- 但可以读取nil map,返回零值:
v := m["key"]不会panic - 检查key是否存在:
v, ok := m["key"]
第28题:map的并发安全
问题:以下代码有什么问题?如何修复?
package main
import "sync"
func main() {
m := make(map[int]int)
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
m[n] = n // 并发写入
}(i)
}
wg.Wait()
}
答案:运行时fatal error:concurrent map writes
解析:
- Go map不是并发安全的
- 并发读写会导致数据竞争和panic
- 解决方案:
- 使用
sync.RWMutex加锁 - 使用
sync.Map(Go 1.9+,特定场景) - 使用channel串行化访问
- 使用
第29题:map遍历顺序
问题:以下代码有什么特点?输出顺序是固定的吗?
package main
import "fmt"
func main() {
m := map[string]int{
"a": 1,
"b": 2,
"c": 3,
}
for k, v := range m {
fmt.Printf("%s:%d ", k, v)
}
}
答案:输出顺序不固定,每次运行可能不同。
解析:
- Go故意随机化map遍历顺序,防止开发者依赖特定顺序
- 这是有意的设计决策,鼓励编写顺序无关的代码
- 如果需要固定顺序,必须手动排序:
keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } sort.Strings(keys)
Playground:https://go.dev/play/p/JuVHWJUX8En
第30题:map的key类型
问题:以下代码哪些能编译?
package main
func main() {
// 哪些可以作为map的key?
_ = make(map[[]int]int) // 1
_ = make(map[[3]int]int) // 2
_ = make(map[map[int]int]int) // 3
_ = make(map[struct{ X int }]int) // 4
_ = make(map[interface{}]int) // 5
}
答案:2、4、5能编译;1、3不能。
解析:
- map的key必须是可比较类型(支持==操作)
- 可比较类型:bool、数字、string、指针、channel、interface、array(元素可比较)
- 不可比较类型:slice、map、function
- struct如果所有字段都可比较,则可比较
第31题:struct标签
问题:以下代码输出什么?struct标签有什么用?
package main
import (
"encoding/json"
"fmt"
"reflect"
)
type User struct {
Name string `json:"name" db:"user_name"`
Age int `json:"age,omitempty"`
}
func main() {
u := User{Name: "Tom", Age: 0}
b, _ := json.Marshal(u)
fmt.Println(string(b))
t := reflect.TypeOf(u)
field, _ := t.FieldByName("Name")
fmt.Println(field.Tag.Get("json"))
}
Playground:https://go.dev/play/p/HzQDPSkVSUU
答案:
{"name":"Tom"}
name
解析:
- struct标签是反射可读取的元数据字符串
json:"name"指定JSON序列化的字段名omitempty表示零值时忽略该字段(Age=0被省略)- 通过
reflect包可以读取标签值
第32题:struct嵌入
问题:以下代码输出什么?什么是嵌入字段?
package main
import "fmt"
type Person struct {
Name string
}
func (p Person) SayHi() {
fmt.Println("Hi, I'm", p.Name)
}
type Student struct {
Person // 嵌入字段
Grade int
}
func main() {
s := Student{Person: Person{Name: "Tom"}, Grade: 90}
fmt.Println(s.Name) // 能访问吗?
s.SayHi() // 能调用吗?
}
答案:都能访问,输出:
Tom
Hi, I'm Tom
解析:
- 嵌入字段(匿名字段)让外层struct自动获得内层的方法
- 这是一种组合而非继承,但语法上像继承
- 可以
s.Name直接访问,等价于s.Person.Name - 方法也会被提升,可以直接调用
第33题:嵌入与同名字段
问题:以下代码输出什么?如何区分?
package main
import "fmt"
type A struct { Name string }
type B struct { Name string }
type C struct {
A
B
}
func main() {
c := C{A{Name: "A_name"}, B{Name: "B_name"}}
fmt.Println(c.Name) // 编译错误?
}
Playground:https://go.dev/play/p/12Zitezk21L
答案:编译错误:ambiguous selector c.Name
解析:
- 当多个嵌入字段有同名成员时,必须通过完整路径访问
- 正确写法:
c.A.Name或c.B.Name - 这是一种设计保护,避免歧义
第34题:struct比较
问题:以下代码能否编译?输出什么?
package main
import "fmt"
type S1 struct {
A int
B string
}
type S2 struct {
A int
B string
}
type S3 struct {
A []int
}
func main() {
a := S1{A: 1, B: "x"}
b := S2{A: 1, B: "x"}
c := S3{A: []int{1}}
d := S3{A: []int{1}}
fmt.Println(a == S1(b)) // 能比较吗?
fmt.Println(c == d) // 能比较吗?
}
Playground:https://go.dev/play/p/kwjhlDqDLpF
答案:
a == S1(b)可以,输出true(类型转换后比较)c == d编译错误:invalid operation: c == d (struct containing []int cannot be compared)
解析:
- struct可比较的条件:所有字段都可比较
- 不同类型的struct不能直接比较,需显式转换
- 包含slice、map、function的struct不可比较
第35题:空struct
问题:以下代码输出什么?空struct有什么特点?
package main
import (
"fmt"
"unsafe"
)
func main() {
var s struct{}
fmt.Println("size:", unsafe.Sizeof(s))
// 常用于信号传递
ch := make(chan struct{})
go func() {
fmt.Println("working")
ch <- struct{}{}
}()
<-ch
fmt.Println("done")
}
Playground:https://go.dev/play/p/lTiFDI7WEUn
答案:
size: 0
working
done
解析:
- 空struct
struct{}不占用内存(size为0) - 常用于:
- 信号channel(只关心事件,不关心数据)
- 实现Set:
map[string]struct{} - context取消信号
- 所有空struct变量共享同一个内存地址
第36题:数组作为函数参数
问题:以下代码输出什么?
package main
import "fmt"
func modify(arr [3]int) {
arr[0] = 999
fmt.Println("in modify:", arr)
}
func modifyPtr(arr *[3]int) {
arr[0] = 888
}
func main() {
a := [3]int{1, 2, 3}
modify(a)
fmt.Println("after modify:", a)
modifyPtr(&a)
fmt.Println("after modifyPtr:", a)
}
答案:
in modify: [999 2 3]
after modify: [1 2 3]
after modifyPtr: [888 2 3]
解析:
- 数组是值类型,作为参数会复制整个数组
- 大数组传递应考虑使用指针或切片
[3]int和[5]int是不同的类型
第37题:切片作为map的value
问题:以下代码能否编译?有什么问题?
package main
import "fmt"
func main() {
m := make(map[string][]int)
// 追加元素
m["key"] = append(m["key"], 1)
m["key"] = append(m["key"], 2)
// 直接修改
s := m["key"]
s[0] = 999
fmt.Println(m["key"])
}
答案:能编译,输出[999 2]。
解析:
- slice是引用类型,即使从map取值出来也是指向同一切片header
s[0] = 999修改会影响map中的值- 注意
append可能重新分配数组,此时s和map中的切片会分离
第38题:delete函数
问题:以下代码输出什么?delete有什么特点?
package main
import "fmt"
func main() {
m := map[string]int{"a": 1, "b": 2}
delete(m, "a")
delete(m, "c") // 删除不存在的key
delete(m, "b")
delete(m, "a") // 再次删除
fmt.Println(m)
fmt.Println(len(m))
}
Playground:https://go.dev/play/p/k9PZ_P-VCwT
答案:
map[]
0
解析:
delete(map, key)删除map中的键值对- 删除不存在的key不会报错,是安全的空操作
- 重复删除同一个key也是安全的
- map不是线程安全的,并发delete需要加锁
第39题:map值类型的方法调用
问题:以下代码能否编译?
package main
import "fmt"
type Counter struct {
count int
}
func (c *Counter) Inc() {
c.count++
}
func main() {
m := map[string]Counter{
"a": {count: 0},
}
m["a"].Inc() // 能编译吗?
}
Playground:https://go.dev/play/p/AS1hsBC1XfK
答案:编译错误:cannot call pointer method on m[“a”] or cannot take address of m[“a”]
解析:
- map的值是不可寻址的(not addressable)
m["a"]返回的是值的副本,无法获取地址调用指针方法- 解决方法:
- map定义为
map[string]*Counter - 先取值,修改后再赋值回去
- map定义为
第40题:二维切片
问题:以下代码有什么问题?如何正确初始化?
package main
import "fmt"
func main() {
// 尝试创建 3x3 的二维切片
m := make([][]int, 3)
for i := range m {
m[i] = make([]int, 3)
}
m[1][1] = 999
for i := range m {
fmt.Println(m[i])
}
}
Playground:https://go.dev/play/p/z3BQnel_F7t
答案:代码正确,输出:
[0 0 0]
[0 999 0]
[0 0 0]
解析:
- 二维切片是"切片的切片”,需要逐层初始化
make([][]int, 3)只创建了外层切片(3个nil)- 每个内层切片需要单独make
- 常见错误是只make外层,访问
m[0][0]时panic
第41题:切片的截取与共享
问题:以下代码输出什么?
package main
import "fmt"
func main() {
s := []int{0, 1, 2, 3, 4}
// 截取
s1 := s[1:4]
// append导致重新分配
s2 := append(s1, 100)
fmt.Println("s:", s)
fmt.Println("s1:", s1)
fmt.Println("s2:", s2)
// 再append
s3 := append(s2, 200)
fmt.Println("s3:", s3)
fmt.Println("s:", s)
}
Playground:https://go.dev/play/p/KUw7s2qPfZS
答案:
s: [0 1 2 3 100]
s1: [1 2 3]
s2: [1 2 3 100]
s3: [1 2 3 100 200]
s: [0 1 2 3 100]
解析:
s1的cap=4(从索引1到末尾),还有1个容量- 第一次append填充了s[4]的位置,影响原切片
- 第二次append时容量不足,重新分配,s3指向新数组
第42题:字符串不可变性
问题:以下代码能否编译?
package main
func main() {
s := "hello"
s[0] = 'H' // 能编译吗?
}
答案:编译错误:cannot assign to s[0]
解析:
- Go字符串是不可变的(immutable)
- 修改字符串必须转换为
[]byte或[]rune,修改后再转回 - 字符串可以重新赋值(s = “Hello”),但不能修改内容
- 不可变性使得字符串可以安全共享
第43题:字符串与byte切片转换
问题:以下代码有什么问题?
package main
import "fmt"
func main() {
s := "Hello, 世界"
b := []byte(s)
r := []rune(s)
fmt.Println(len(b), len(r))
// 修改后再转回
b[0] = 'h'
s2 := string(b)
r[7] = '中'
s3 := string(r)
fmt.Println(s2)
fmt.Println(s3)
}
Playground:https://go.dev/play/p/fr5FOspXdPw
答案:输出:
13 9
hello, 世界
Hello, 中国
解析:
[]byte(s)按字节转换,包含所有UTF-8字节[]rune(s)按字符转换,每个rune代表一个Unicode码点- 转换会复制数据,修改不影响原字符串
- 选择
[]byte还是[]rune取决于按字节还是按字符处理
第44题:数组初始化
问题:以下代码输出什么?
package main
import "fmt"
func main() {
// 各种初始化方式
a1 := [5]int{1, 2, 3} // 索引0,1,2有值
a2 := [...]int{1, 2, 3, 4} // 长度自动推断
a3 := [5]int{2: 100, 4: 200} // 按索引赋值
a4 := [5]int{1: 10, 3: 30} // 其他为0
fmt.Println(a1)
fmt.Println(a2)
fmt.Println(a3)
fmt.Println(a4)
}
Playground:https://go.dev/play/p/rudzheEZFNn
答案:
[1 2 3 0 0]
[1 2 3 4]
[0 0 100 0 200]
[0 10 0 30 0]
解析:
[n]int{...}指定长度的数组初始化[...]int{...}让编译器根据初始值数量推断长度{index: value}语法可以按索引位置赋值- 未指定的元素为对应类型的零值
第45题:指针数组与数组指针
问题:以下代码输出什么?
package main
import "fmt"
func main() {
// 数组指针:指向数组的指针
arr := [3]int{1, 2, 3}
p := &arr
// 指针数组:元素为指针的数组
a, b, c := 1, 2, 3
arr2 := [3]*int{&a, &b, &c}
fmt.Println((*p)[0]) // 解引用后访问
fmt.Println(*arr2[0]) // 访问指针元素
// 修改
(*p)[0] = 100
*arr2[0] = 10
fmt.Println(arr)
fmt.Println(a)
}
Playground:https://go.dev/play/p/S0nBMzjfD_K
答案:
1
1
[100 2 3]
10
解析:
*[3]int是指向数组的指针,解引用后可索引[3]*int是元素为指针的数组- 注意运算符优先级:
*p[0]是*(p[0]),应该写(*p)[0]
三、函数与方法 (20题)
第46题:defer执行顺序
问题:以下代码输出什么?defer的执行顺序是什么?
package main
import "fmt"
func main() {
defer fmt.Println("defer 1")
defer fmt.Println("defer 2")
defer fmt.Println("defer 3")
fmt.Println("main body")
}
Playground:https://go.dev/play/p/v6piDHYjIrb
答案:
main body
defer 3
defer 2
defer 1
解析:
- defer遵循**后进先出(LIFO)**的栈顺序
- defer在函数返回前执行,可用于资源清理
- 多个defer按声明的逆序执行
- defer的参数在声明时求值,不是执行时
第47题:defer参数求值时机
问题:以下代码输出什么?为什么?
package main
import "fmt"
func main() {
i := 0
defer fmt.Println("defer:", i) // 输出什么?
i++
fmt.Println("main:", i)
}
Playground:https://go.dev/play/p/Gj-U2nIyBaw
答案:
main: 1
defer: 0
解析:
- defer的参数在defer语句处立即求值,不是延迟执行时
defer fmt.Println(i)中的i在声明时被计算为0- 即使后面i变成1,defer输出仍是0
- 要获取最终值,可用闭包:
defer func() { fmt.Println(i) }()
第48题:defer闭包陷阱(⚠️ 版本差异题)
问题:以下代码输出什么?
package main
import "fmt"
func main() {
for i := 0; i < 3; i++ {
defer func() {
fmt.Println(i)
}()
}
}
Playground:https://go.dev/play/p/m_HuJCx7mP6
答案:
- Go 1.21及之前:输出
3 3 3 - Go 1.22+:输出
2 1 0(逆序,因为defer是LIFO)
解析:
-
Go 1.21及之前:defer中的闭包捕获的是变量
i的引用,不是值。函数返回时i已经是3(循环结束后的值),所以输出3 3 3。 -
Go 1.22+:每次迭代创建新的
i变量,defer捕获的是各自的值。由于defer是LIFO(后进先出),所以输出2 1 0。 -
向后兼容修复(推荐):
for i := 0; i < 3; i++ { defer func(n int) { fmt.Println(n) }(i) }
第49题:defer与return
问题:以下代码输出什么?defer能否修改返回值?
package main
import "fmt"
func f() (result int) {
defer func() {
result++ // 修改命名返回值
}()
return 0 // 实际执行:result = 0; return
}
func g() int {
result := 0
defer func() {
result++
}()
return result
}
func main() {
fmt.Println("f():", f())
fmt.Println("g():", g())
}
答案:
f(): 1
g(): 0
解析:
return xxx实际上分两步执行:先给返回值赋值,再执行defer,最后真正返回- 对于命名返回值,defer可以修改result,影响最终返回值
- 对于非命名返回值,defer修改的是局部变量,不影响返回值
第50题:defer与panic
问题:以下代码输出什么?defer在panic时会执行吗?
package main
import "fmt"
func main() {
defer fmt.Println("defer 1")
defer func() {
fmt.Println("defer 2")
if r := recover(); r != nil {
fmt.Println("recovered:", r)
}
}()
defer fmt.Println("defer 3")
panic("something wrong")
fmt.Println("after panic") // 不会执行
}
Playground:https://go.dev/play/p/3VL5G2CSvr_7
答案:
defer 3
defer 2
recovered: something wrong
defer 1
解析:
- panic会立即停止当前函数执行,但已注册的defer仍会执行
- defer按LIFO顺序执行
recover()必须在defer中调用,可捕获panic并恢复- recover后程序从panic点恢复继续执行
第51题:panic与recover规则
问题:以下代码能否捕获panic?输出什么?
package main
import "fmt"
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("recovered:", r)
}
}()
go func() {
panic("goroutine panic") // 能被捕获吗?
}()
select {} // 阻塞
}
Playground:https://go.dev/play/p/TP4rKKzcp1S
答案:不能捕获,程序会崩溃。
解析:
- recover只能捕获当前goroutine的panic
- 其他goroutine的panic无法被当前goroutine捕获
- 每个goroutine需要自己的recover处理
- 未捕获的panic会导致整个程序退出
第52题:闭包与变量捕获(⚠️ 版本差异题)
问题:以下代码输出什么?
package main
import "fmt"
func makeFuncs() []func() {
funcs := make([]func(), 3)
for i := 0; i < 3; i++ {
funcs[i] = func() {
fmt.Println(i)
}
}
return funcs
}
func main() {
funcs := makeFuncs()
for _, f := range funcs {
f()
}
}
Playground:https://go.dev/play/p/E1_ivuky9I8
答案:
- Go 1.21及之前:输出
3 3 3 - Go 1.22+:输出
0 1 2
解析:
-
Go 1.21及之前:for循环中的变量
i在整个循环中是同一个变量,闭包捕获的是变量的引用。所有函数都引用同一个i,调用时i已经是3。 -
Go 1.22+:每次迭代都会创建新的
i变量,闭包捕获的是各自迭代的值,输出0 1 2。 -
向后兼容修复(推荐):
for i := 0; i < 3; i++ { i := i // 创建局部副本 funcs[i] = func() { fmt.Println(i) } } // 或传递参数 for i := 0; i < 3; i++ { funcs[i] = func(n int) func() { return func() { fmt.Println(n) } }(i) }
第53题:方法接收者
问题:以下代码输出什么?值接收者和指针接收者有什么区别?
package main
import "fmt"
type Counter struct {
count int
}
func (c Counter) Value() int {
return c.count
}
func (c *Counter) Inc() {
c.count++
}
func main() {
var c Counter
c.Inc()
fmt.Println(c.Value())
// 以下都能编译吗?
p := &Counter{}
p.Inc()
fmt.Println(p.Value())
}
答案:输出1和1。都能编译。
解析:
- 值接收者:方法内是值的副本,修改不影响原值
- 指针接收者:可以修改原值
- Go自动处理取地址和解引用:
c.Inc()等价于(&c).Inc(),p.Value()等价于(*p).Value() - 通常:需要修改用指针接收者,只读取用值接收者
第54题:方法集
问题:以下哪些调用是合法的?
package main
type T struct{}
func (T) M1() {} // 值接收者
func (*T) M2() {} // 指针接收者
func main() {
var t T
var p = &t
t.M1() // ?
t.M2() // ?
p.M1() // ?
p.M2() // ?
}
Playground:https://go.dev/play/p/6Wfg2p5D1HY
答案:全部合法!
解析:
| 调用 | 合法性 | 实际调用 |
|---|---|---|
t.M1() |
✅ | T.M1(t) |
t.M2() |
✅ | (*T).M2(&t)(自动取地址) |
p.M1() |
✅ | T.M1(*p)(自动解引用) |
p.M2() |
✅ | (*T).M2(p) |
- 方法调用时,Go自动进行取地址(&)或解引用(*)转换
- 但interface的方法集有区别(见下一题)
第55题:接口的方法集
问题:以下代码能否编译?
package main
type MyInterface interface {
M1()
M2()
}
type T struct{}
func (T) M1() {}
func (*T) M2() {}
func main() {
var t T
var i MyInterface = t // 能编译吗?
_ = i
}
Playground:https://go.dev/play/p/h912-IdG0-y
答案:编译错误:T does not implement MyInterface (M2 method has pointer receiver)
解析:
- 值类型T的方法集:只有值接收者的方法(M1)
- 指针类型*T的方法集:值接收者和指针接收者的方法(M1+M2)
- interface赋值时:
i = t要求T实现所有方法(不包括仅指针接收者的方法)i = &t要求*T实现所有方法
第56题:nil接收者
问题:以下代码会panic吗?输出什么?
package main
import "fmt"
type Node struct {
Value int
Next *Node
}
func (n *Node) Sum() int {
if n == nil {
return 0
}
return n.Value + n.Next.Sum()
}
func main() {
var n *Node
fmt.Println(n.Sum()) // 会panic吗?
}
Playground:https://go.dev/play/p/2GdXhY7MKjw
答案:输出0,不会panic。
解析:
- 方法调用
n.Sum()本质上是Node.Sum(n) - n可以为nil,在方法内部检查即可
- 这是一种Go惯用法(如
error接口的Error()方法) - 但调用
n.Value如果n为nil会panic
第57题:函数是一等公民
问题:以下代码输出什么?
package main
import "fmt"
type Handler func(int) int
func (h Handler) Process(n int) int {
return h(n) * 2
}
func addOne(x int) int {
return x + 1
}
func main() {
h := Handler(addOne)
fmt.Println(h.Process(5))
}
Playground:https://go.dev/play/p/cpAvjKQ9b9C
答案:输出12((5+1)*2)
解析:
- 函数类型可以定义方法
- 可以实现接口(使函数具有面向对象能力)
- 常用于http.Handler等场景
第58题:匿名函数与递归
问题:以下代码输出什么?如何写递归匿名函数?
package main
import "fmt"
func main() {
// 递归匿名函数
var fib func(int) int
fib = func(n int) int {
if n < 2 {
return n
}
return fib(n-1) + fib(n-2)
}
fmt.Println(fib(10))
}
Playground:https://go.dev/play/p/u3bMOPrLbHm
答案:输出55(第10个斐波那契数)
解析:
- 匿名函数要递归,必须先声明变量
var fib func(int) int - 然后再赋值,才能在函数体内引用自己
- 不能直接在
:=定义的匿名函数内调用自己
第59题:函数作为参数和返回值
问题:以下代码实现了什么设计模式?输出什么?
package main
import "fmt"
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
func main() {
pos, neg := adder(), adder()
for i := 0; i < 3; i++ {
fmt.Println(pos(i), neg(-2*i))
}
}
Playground:https://go.dev/play/p/WdTiyfDzFJM
答案:
0 0
1 -2
3 -6
解析:
- 实现了闭包(Closure)和工厂模式
- 每个adder()返回的函数有自己的sum变量(词法作用域)
- pos和neg是两个独立的闭包,互不影响
- 闭包可以记住并访问外部函数的变量
第60题:高阶函数
问题:以下代码实现了什么功能?
package main
import "fmt"
func Map(slice []int, fn func(int) int) []int {
result := make([]int, len(slice))
for i, v := range slice {
result[i] = fn(v)
}
return result
}
func Filter(slice []int, fn func(int) bool) []int {
result := []int{}
for _, v := range slice {
if fn(v) {
result = append(result, v)
}
}
return result
}
func main() {
nums := []int{1, 2, 3, 4, 5}
doubled := Map(nums, func(x int) int { return x * 2 })
evens := Filter(nums, func(x int) bool { return x%2 == 0 })
fmt.Println(doubled)
fmt.Println(evens)
}
Playground:https://go.dev/play/p/jB03ivVAbKJ
答案:
[2 4 6 8 10]
[2 4]
解析:
- 实现了函数式编程的Map和Filter操作
- 高阶函数:接收函数作为参数或返回函数的函数
- Go不是纯函数式语言,但可以借鉴函数式风格
第61题:方法表达式
问题:以下代码输出什么?方法表达式有什么用?
package main
import "fmt"
type Person struct {
Name string
}
func (p Person) Greet() string {
return "Hello, " + p.Name
}
func main() {
p := Person{Name: "Tom"}
// 方法值
greetFunc := p.Greet
fmt.Println(greetFunc())
// 方法表达式
greetExpr := Person.Greet
fmt.Println(greetExpr(p))
}
Playground:https://go.dev/play/p/WSDeic4Q02-
答案:都输出Hello, Tom
解析:
- 方法值:
p.Greet,接收者已绑定,调用时无需传接收者 - 方法表达式:
Person.Greet,接收者未绑定,调用时需传接收者 - 方法表达式在需要延迟绑定接收者时有用
第62题:defer与循环
问题:以下代码有什么问题?输出什么?
package main
import (
"fmt"
"os"
)
func processFiles(files []string) error {
for _, file := range files {
f, err := os.Open(file)
if err != nil {
return err
}
defer f.Close() // 有问题吗?
// 处理文件...
fmt.Println("processing", file)
}
return nil
}
func main() {
_ = processFiles([]string{"/etc/passwd"})
}
答案:能运行,但有资源泄漏风险。
解析:
- defer在函数返回时才执行,不是在循环结束时
- 如果files有100个文件,会同时打开100个文件句柄
- 修复:将文件处理逻辑抽成函数
func processFile(file string) error { f, err := os.Open(file) if err != nil { return err } defer f.Close() // 处理... return nil }
Playground:https://go.dev/play/p/UJkG-186P4F
第63题:函数返回值命名
问题:以下代码有什么问题?
package main
import "fmt"
func divide(a, b int) (result int, err error) {
if b == 0 {
err = fmt.Errorf("cannot divide by zero")
return // 裸return
}
result = a / b
return // 裸return
}
func main() {
r, e := divide(10, 0)
fmt.Println(r, e)
}
Playground:https://go.dev/play/p/uCoT52KwFEN
答案:输出0 cannot divide by zero
解析:
- 命名返回值可以作为函数内的局部变量使用
return(裸return)自动返回命名返回值- 优点:文档化返回值含义,方便修改
- 缺点:裸return可能降低可读性,长函数慎用
第65题:init函数的执行顺序
问题:假设有两个文件a.go和b.go,init执行顺序是什么?
// a.go
package main
import "fmt"
func init() {
fmt.Println("a.go init")
}
// b.go
package main
import "fmt"
func init() {
fmt.Println("b.go init")
}
// main.go
package main
import "fmt"
func init() {
fmt.Println("main.go init")
}
func main() {
fmt.Println("main")
}
Playground:https://go.dev/play/p/vSs1-Z6I6O_i
答案:按文件名字母顺序执行init,然后是main:
a.go init
b.go init
main.go init
main
解析:
- 同包内多个文件的init按文件名排序执行
- 不同包之间:先执行导入包的init(递归),再执行当前包
- 一个文件可以有多个init函数,按出现顺序执行
Playground:需要多文件,略
第66题:context.WithTimeout
问题:以下代码会输出什么?
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
select {
case <-time.After(2 * time.Second):
fmt.Println("work done")
case <-ctx.Done():
fmt.Println("timeout:", ctx.Err())
}
}
Playground:https://go.dev/play/p/32DeShyJLau
答案:输出timeout: context deadline exceeded
解析:
- context超时后,
ctx.Done()channel会被关闭 ctx.Err()返回context取消的原因defer cancel()即使超时也会执行,释放资源- 超时设置1秒,工作需2秒,触发超时分支
四、接口与反射 (20题)
第67题:接口基础
问题:以下代码输出什么?
package main
import "fmt"
type Animal interface {
Speak() string
}
type Dog struct{}
func (d Dog) Speak() string {
return "Woof!"
}
type Cat struct{}
func (c Cat) Speak() string {
return "Meow!"
}
func MakeSound(a Animal) {
fmt.Println(a.Speak())
}
func main() {
MakeSound(Dog{})
MakeSound(Cat{})
}
答案:
Woof!
Meow!
解析:
- Go接口是隐式实现,不需要显式声明
implements - 只要类型实现了接口的所有方法,就自动实现该接口
- 接口值由两部分组成:类型指针和值指针(动态类型+动态值)
第68题:接口的nil判断
问题:以下代码输出什么?
package main
import "fmt"
func main() {
var p *int = nil
var i interface{} = p
fmt.Println(p == nil) // ?
fmt.Println(i == nil) // ?
}
Playground:https://go.dev/play/p/vqDbxpGbnat
答案:
true
false
解析:
- 接口值nil需要类型和值都为nil
i的类型是*int,值是nil,所以接口值不是nil- 这是Go接口的一个常见陷阱
- 判断接口内部值是否为nil:先用类型断言取出再判断
第69题:类型断言
问题:以下代码输出什么?
package main
import "fmt"
func main() {
var i interface{} = "hello"
s := i.(string) // 直接断言
fmt.Println(s)
n, ok := i.(int) // 安全断言
fmt.Println(n, ok)
// f := i.(float64) // 会怎样?
}
Playground:https://go.dev/play/p/JYjuOWRessb
答案:
hello
0 false
解析:
- 类型断言
i.(T)获取接口的动态值 - 直接断言失败会panic
- 安全断言
v, ok := i.(T)失败时ok=false,v为零值 i.(float64)会panic(运行时panic)
第70题:空接口interface
问题:以下代码有什么问题?空接口有什么用?
package main
import "fmt"
func PrintAny(v interface{}) {
fmt.Printf("value: %v, type: %T\n", v, v)
}
func main() {
PrintAny(42)
PrintAny("hello")
PrintAny([]int{1, 2, 3})
PrintAny(nil)
}
Playground:https://go.dev/play/p/jqhV8TSO63M
答案:无问题,都能正常输出。
解析:
interface{}是空接口,可以保存任意类型的值- 类似于C的
void*或Java的Object - 常用于:
fmt.Println的参数- 泛型编程(Go 1.18前)
- JSON序列化/反序列化
- 使用类型断言或反射获取具体类型
第71题:接口嵌套
问题:以下代码展示了什么特性?
package main
import "fmt"
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type ReadWriter interface {
Reader
Writer
}
type MyIO struct{}
func (m MyIO) Read(p []byte) (int, error) {
return 0, nil
}
func (m MyIO) Write(p []byte) (int, error) {
return len(p), nil
}
func main() {
var rw ReadWriter = MyIO{}
fmt.Printf("%T implements ReadWriter\n", rw)
}
Playground:https://go.dev/play/p/uwYGv1KGIQo
答案:输出main.MyIO implements ReadWriter
解析:
- 接口可以嵌套其他接口
ReadWriter自动拥有Reader和Writer的所有方法- 实现了Reader和Writer的类型自动实现ReadWriter
- Go标准库io包大量使用了这种模式
第72题:反射获取类型信息
问题:以下代码输出什么?
package main
import (
"fmt"
"reflect"
)
type Person struct {
Name string
Age int
}
func main() {
p := Person{Name: "Tom", Age: 30}
t := reflect.TypeOf(p)
v := reflect.ValueOf(p)
fmt.Println("Type:", t)
fmt.Println("Kind:", t.Kind())
fmt.Println("Value:", v)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
value := v.Field(i)
fmt.Printf("%s: %v\n", field.Name, value)
}
}
Playground:https://go.dev/play/p/hxksj0uJvW3
答案:
Type: main.Person
Kind: struct
Value: {Tom 30}
Name: Tom
Age: 30
解析:
reflect.TypeOf()获取类型信息reflect.ValueOf()获取值信息Kind()返回基本类型(struct、int、slice等)- 可遍历结构体字段,获取标签等信息
第73题:反射修改值
问题:以下代码能否成功修改?
package main
import (
"fmt"
"reflect"
)
func main() {
x := 10
v := reflect.ValueOf(x)
// v.SetInt(20) // 会怎样?
v2 := reflect.ValueOf(&x).Elem()
v2.SetInt(20)
fmt.Println(x)
}
Playground:https://go.dev/play/p/Rar3_AAhPBf
答案:v.SetInt(20)会panic,v2.SetInt(20)成功,x变为20
解析:
reflect.ValueOf(x)返回的是值的副本,不可修改reflect.ValueOf(&x).Elem()获取指针指向的元素,可修改- 修改前需要检查
CanSet() - 只有可寻址的值才能修改
第74题:反射调用方法
问题:以下代码输出什么?
package main
import (
"fmt"
"reflect"
)
type Calculator struct{}
func (c Calculator) Add(a, b int) int {
return a + b
}
func (c Calculator) Subtract(a, b int) int {
return a - b
}
func main() {
c := Calculator{}
v := reflect.ValueOf(c)
method := v.MethodByName("Add")
args := []reflect.Value{
reflect.ValueOf(10),
reflect.ValueOf(5),
}
result := method.Call(args)
fmt.Println(result[0].Int())
}
Playground:https://go.dev/play/p/eata3f-j-F-
答案:输出15
解析:
MethodByName获取方法(值接收者方法)Call调用方法,传入[]reflect.Value参数- 返回
[]reflect.Value结果 - 注意:值接收者和指针接收者方法的获取方式略有不同
第75题:反射与JSON标签
问题:以下代码展示了什么用法?
package main
import (
"encoding/json"
"fmt"
"reflect"
)
type User struct {
Name string `json:"name" db:"user_name"`
Email string `json:"email,omitempty"`
Age int `json:"-"`
}
func main() {
u := User{Name: "Tom", Email: "tom@example.com", Age: 30}
t := reflect.TypeOf(u)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fmt.Printf("Field: %s, JSON: %s\n", field.Name, field.Tag.Get("json"))
}
b, _ := json.Marshal(u)
fmt.Println(string(b))
}
Playground:https://go.dev/play/p/vT-lVb13MN8
答案:
Field: Name, JSON: name
Field: Email, JSON: email,omitempty
Field: Age, JSON: -
{"name":"Tom","email":"tom@example.com"}
解析:
`json:"name"`是结构体标签,反射可读取json:"-"表示该字段不参与JSON序列化omitempty表示零值时省略- 反射广泛用于ORM、序列化库等
第76题:接口比较
问题:以下代码能否编译?输出什么?
package main
import "fmt"
func main() {
var a interface{} = []int{1, 2, 3}
var b interface{} = []int{1, 2, 3}
fmt.Println(a == b) // ?
}
Playground:https://go.dev/play/p/Z1ehsXDV9Nb
答案:运行时panic
解析:
- 接口值可比较,但内部值必须是可比较类型
- slice是不可比较类型(不能用==)
- 运行时会panic:
runtime error: comparing uncomparable type []int - 比较前先判断类型,或使用反射
第77题:类型switch
问题:以下代码输出什么?
package main
import "fmt"
func printType(v interface{}) {
switch x := v.(type) {
case int:
fmt.Printf("int: %d\n", x)
case string:
fmt.Printf("string: %s\n", x)
case []int:
fmt.Printf("[]int with len=%d\n", len(x))
default:
fmt.Printf("unknown: %T\n", x)
}
}
func main() {
printType(42)
printType("hello")
printType([]int{1, 2, 3})
printType(3.14)
}
Playground:https://go.dev/play/p/VBPgahU_Zbk
答案:
int: 42
string: hello
[]int with len=3
unknown: float64
解析:
- 类型switch可以同时判断类型并获取值
x在每个case中具有对应类型- 比多次类型断言更简洁
- 可以用
case nil:判断nil值
第78题:反射创建实例
问题:以下代码输出什么?
package main
import (
"fmt"
"reflect"
)
type User struct {
Name string
Age int
}
func main() {
t := reflect.TypeOf(User{})
// 创建实例
v := reflect.New(t).Elem()
v.FieldByName("Name").SetString("Alice")
v.FieldByName("Age").SetInt(25)
user := v.Interface().(User)
fmt.Println(user)
}
Playground:https://go.dev/play/p/hxRUqPYgy0e
答案:输出{Alice 25}
解析:
reflect.New(t)创建指向类型的指针Elem()获取指针指向的元素- 设置字段值后,用
Interface()转回interface{} - 最后类型断言获取具体类型
第79题:接口的底层结构
问题:接口值在内存中是如何表示的?
答案:
interface {
type uintptr // 指向类型信息的指针
data uintptr // 指向值的指针
}
或详细结构:
type iface struct {
tab *itab // 类型信息+方法表
data unsafe.Pointer
}
type eface struct { // 空接口
_type *_type
data unsafe.Pointer
}
Playground:https://go.dev/play/p/SHpwUfEOCnN
解析:
- 接口值由两部分组成:类型描述符和数据指针
- 非空接口(
interface{Method()})使用iface - 空接口(
interface{})使用eface - 理解这个结构有助于理解接口nil判断等问题
第80题:断言链式调用
问题:以下代码输出什么?
package main
import "fmt"
func main() {
var i interface{} = map[string]interface{}{
"user": map[string]interface{}{
"name": "Tom",
"age": 30,
},
}
if m, ok := i.(map[string]interface{}); ok {
if user, ok := m["user"].(map[string]interface{}); ok {
if name, ok := user["name"].(string); ok {
fmt.Println("Name:", name)
}
}
}
}
Playground:https://go.dev/play/p/frHYYF_W434
答案:输出Name: Tom
解析:
- JSON反序列化后常是
map[string]interface{}结构 - 需要层层类型断言才能访问深层字段
- 代码冗长且易出错,实际项目中建议使用结构体或第三方库
- Go 1.18+泛型可以部分改善这种情况
第81题:反射的局限性
问题:反射有哪些缺点?在什么情况下应避免使用?
答案:
- 性能损耗:反射比直接调用慢10-100倍
- 类型安全:编译时无法检查,运行时可能panic
- 代码可读性差:难以理解维护
- 无法获取私有字段(包外):未导出字段无法访问
应避免场景:
- 性能敏感的代码路径
- 可以用类型参数(泛型)替代时
- 简单类型转换场景
适合场景:
- 序列化/反序列化(JSON、XML等)
- ORM框架
- 依赖注入框架
- 需要处理多种类型的通用工具函数
第82题:接口赋值给接口
问题:以下代码能否编译?
package main
import "fmt"
type Stringer interface {
String() string
}
type Printer interface {
String() string
Print()
}
type MyType struct{}
func (m MyType) String() string { return "mytype" }
func (m MyType) Print() { fmt.Println(m.String()) }
func main() {
var p Printer = MyType{}
var s Stringer = p // 能赋值吗?
fmt.Println(s.String())
}
Playground:https://go.dev/play/p/p9vA78921kZ
答案:能编译,输出mytype
解析:
- Printer实现了Stringer的所有方法,所以Printer可以赋值给Stringer
- 接口可以向上转型(小接口赋值给大接口)
- 但大接口不能赋值给小接口(方法可能不全)
第83题:reflect.DeepEqual
问题:以下代码输出什么?DeepEqual有什么作用?
package main
import (
"fmt"
"reflect"
)
func main() {
a := []int{1, 2, 3}
b := []int{1, 2, 3}
fmt.Println(a == b) // 能编译吗?
fmt.Println(reflect.DeepEqual(a, b))
m1 := map[string]int{"a": 1}
m2 := map[string]int{"a": 1}
fmt.Println(reflect.DeepEqual(m1, m2))
}
Playground:https://go.dev/play/p/5f2KXrd4oSo
答案:
a == b编译错误reflect.DeepEqual(a, b)输出truereflect.DeepEqual(m1, m2)输出true(顺序可能不同)
解析:
reflect.DeepEqual可以比较不可比较类型(slice、map、struct含slice等)- 递归比较,支持嵌套结构
- 注意:对于float的NaN处理有特殊情况
第84题:接口的类型擦除
问题:以下代码能否获取原始类型?
package main
import (
"fmt"
"reflect"
)
type MyInt int
func main() {
var i interface{} = MyInt(10)
fmt.Printf("%T\n", i) // 输出什么?
t := reflect.TypeOf(i)
fmt.Println(t.Name()) // 输出什么?
fmt.Println(t.Kind()) // 输出什么?
}
Playground:https://go.dev/play/p/ROE4Gkq0CXc
答案:
main.MyInt
MyInt
int
解析:
%T和reflect.TypeOf可以获取原始类型信息Name()返回自定义类型名Kind()返回底层基础类型(int、struct等)- 接口保留了完整的类型信息,不是类型擦除
第85题:接口与指针接收者
问题:以下代码有什么问题?
package main
import "fmt"
type Sayer interface {
Say()
}
type Cat struct {
name string
}
func (c *Cat) Say() {
fmt.Println(c.name, "says meow")
}
func main() {
var c Cat = Cat{name: "Kitty"}
var s Sayer = c // 能编译吗?
s.Say()
}
Playground:https://go.dev/play/p/NkkNC4U4zWt
答案:编译错误:Cat does not implement Sayer (Say method has pointer receiver)
解析:
Say()方法有指针接收者,只有*Cat实现了SayerCat(值类型)没有实现Sayer- 修复:
var s Sayer = &c
第86题:反射与函数
问题:以下代码输出什么?
package main
import (
"fmt"
"reflect"
)
func Add(a, b int) int {
return a + b
}
func main() {
f := reflect.ValueOf(Add)
fmt.Println("Kind:", f.Kind())
fmt.Println("Type:", f.Type())
args := []reflect.Value{
reflect.ValueOf(10),
reflect.ValueOf(20),
}
result := f.Call(args)
fmt.Println("Result:", result[0])
}
Playground:https://go.dev/play/p/krDxL4L9c9h
答案:
Kind: func
Type: func(int, int) int
Result: 30
解析:
- 函数也可以通过反射调用
Kind()返回funcType()返回完整的函数签名- 参数和返回值都是
[]reflect.Value
五、并发编程 (30题)
第87题:Goroutine基础
问题:以下代码输出什么?main函数会等待goroutine吗?
package main
import (
"fmt"
"time"
)
func sayHello() {
time.Sleep(100 * time.Millisecond)
fmt.Println("Hello from goroutine")
}
func main() {
go sayHello()
fmt.Println("Hello from main")
// 没有等待
}
Playground:https://go.dev/play/p/3Paq6heGhAk
答案:可能只输出Hello from main,goroutine来不及执行。
解析:
- main函数结束会立即终止程序,不管goroutine是否完成
- 需要使用
sync.WaitGroup或channel等待goroutine - goroutine是Go协程,由Go运行时调度,非OS线程
第88题:WaitGroup等待
问题:修正上一题的代码,确保goroutine完成。
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
time.Sleep(100 * time.Millisecond)
fmt.Println("Hello from goroutine")
}()
fmt.Println("Hello from main")
wg.Wait() // 等待
}
Playground:https://go.dev/play/p/BNYrE-SFXqQ
答案:两个消息都输出。
解析:
wg.Add(n)添加n个等待计数wg.Done()完成一个,减1wg.Wait()阻塞直到计数为0defer wg.Done()确保即使panic也会调用
第89题:Channel基础
问题:以下代码输出什么?
package main
import "fmt"
func main() {
ch := make(chan int)
go func() {
ch <- 42
}()
value := <-ch
fmt.Println(value)
}
答案:输出42
解析:
- channel是goroutine间通信的管道
ch <- 42发送数据到channel<-ch从channel接收数据- 无缓冲channel的发送和接收会阻塞,直到配对
第90题:有缓冲Channel
问题:以下代码会死锁吗?
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int, 2) // 缓冲区大小2
ch <- 1
ch <- 2
// ch <- 3 // 会怎样?
go func() {
time.Sleep(time.Second)
fmt.Println(<-ch)
}()
fmt.Println(<-ch)
time.Sleep(2 * time.Second)
}
Playground:https://go.dev/play/p/El6VYZE7Usf
答案:不会死锁。如果发送第3个值会阻塞。
解析:
- 有缓冲channel只在缓冲区满时阻塞发送
- 只在缓冲区空时阻塞接收
ch <- 3会阻塞,因为没有接收者- 容量(capacity)和长度(length)概念类似slice
第91题:Channel关闭与range
问题:以下代码输出什么?
package main
import "fmt"
func producer(ch chan<- int) {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch)
}
func main() {
ch := make(chan int)
go producer(ch)
for v := range ch {
fmt.Println(v)
}
fmt.Println("done")
}
Playground:https://go.dev/play/p/QY_CBtf3JP3
答案:输出0到4,然后done。
解析:
range ch从channel接收值直到channel关闭- 关闭channel后,
range结束循环 - 接收关闭的channel会返回零值,可用
v, ok := <-ch判断是否关闭 - 只能关闭一次,关闭后不能再发送
第92题:Select多路复用
问题:以下代码输出什么?
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(100 * time.Millisecond)
ch1 <- "from ch1"
}()
go func() {
time.Sleep(200 * time.Millisecond)
ch2 <- "from ch2"
}()
for i := 0; i < 2; i++ {
select {
case msg := <-ch1:
fmt.Println(msg)
case msg := <-ch2:
fmt.Println(msg)
}
}
}
答案:
from ch1
from ch2
解析:
select同时等待多个channel操作- 哪个channel就绪就执行哪个case
- 多个就绪时随机选择一个(避免饥饿)
- 没有default且都阻塞时会等待
第93题:Select默认case
问题:以下代码输出什么?
package main
import "fmt"
func main() {
ch := make(chan int)
select {
case v := <-ch:
fmt.Println("received:", v)
default:
fmt.Println("no data")
}
}
答案:输出no data
解析:
defaultcase使select变为非阻塞- 如果没有channel就绪,执行default
- 可用于非阻塞发送:
select { case ch <- v: default: } - 常用于超时处理、心跳检测等
第94题:Select超时
问题:以下代码实现了什么功能?
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
select {
case v := <-ch:
fmt.Println("received:", v)
case <-time.After(500 * time.Millisecond):
fmt.Println("timeout")
}
}
答案:输出timeout(500ms后)
解析:
time.After返回一个超时channel- select等待接收或超时,哪个先到执行哪个
- 常用模式:防止goroutine永久阻塞
- 注意:
time.After会创建新goroutine,大量使用时考虑context.WithTimeout
第95题:Mutex互斥锁
问题:以下代码有什么问题?
package main
import (
"fmt"
"sync"
)
func main() {
var counter int
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter++
}()
}
wg.Wait()
fmt.Println(counter)
}
答案:输出可能小于1000,有数据竞争。
解析:
counter++不是原子操作(读取-增加-写入)- 多个goroutine同时操作会产生竞争
- 修复:使用
sync.Mutex或sync/atomic
第96题:Mutex正确使用
问题:修正上一题的代码。
package main
import (
"fmt"
"sync"
)
func main() {
var counter int
var mu sync.Mutex
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
counter++
mu.Unlock()
}()
}
wg.Wait()
fmt.Println(counter)
}
答案:输出1000
解析:
mu.Lock()获取锁,mu.Unlock()释放锁- 确保同时只有一个goroutine访问临界区
- 建议用
defer mu.Unlock()避免遗漏 - 锁粒度应尽可能小,减少竞争
第97题:RWMutex读写锁
问题:以下代码展示了什么用法?
package main
import (
"fmt"
"sync"
"time"
)
type Cache struct {
mu sync.RWMutex
data map[string]string
}
func (c *Cache) Get(key string) string {
c.mu.RLock()
defer c.mu.RUnlock()
return c.data[key]
}
func (c *Cache) Set(key, value string) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = value
}
func main() {
c := &Cache{data: make(map[string]string)}
c.Set("a", "1")
fmt.Println(c.Get("a"))
}
答案:输出1
解析:
RWMutex区分读锁和写锁RLock/RUnlock:多个读可以并行Lock/Unlock:写独占,读写互斥- 读多写少场景性能优于Mutex
第98题:死锁检测
问题:以下代码有什么问题?
package main
import "sync"
func main() {
var mu sync.Mutex
mu.Lock()
mu.Lock() // 会怎样?
mu.Unlock()
mu.Unlock()
}
答案:第二次Lock会永久阻塞,造成死锁。
解析:
- 标准
Mutex不支持重入(递归锁) - 同一线程重复Lock会导致死锁
- 需要重入的场景可以:
- 重新设计避免重入
- 使用channel替代
- 记录goroutine ID实现可重入锁(不推荐)
第99题:Once单例
问题:以下代码输出什么?Once有什么特点?
package main
import (
"fmt"
"sync"
)
func main() {
var once sync.Once
for i := 0; i < 10; i++ {
go func(n int) {
once.Do(func() {
fmt.Println("Executed by goroutine", n)
})
}(i)
}
// 等待所有goroutine
var wg sync.WaitGroup
wg.Add(10)
for i := 0; i < 10; i++ {
go func(n int) {
defer wg.Done()
once.Do(func() {
fmt.Println("Should not print")
})
}(i)
}
wg.Wait()
}
答案:Executed by goroutine只输出一次。
解析:
sync.Once确保函数只执行一次- 常用于单例模式、初始化操作
- 即使函数panic,Once也认为已执行,不会重试
- 并发安全,无需额外锁
第100题:Pool对象池
问题:以下代码展示了什么用法?
package main
import (
"bytes"
"fmt"
"sync"
)
var pool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
func main() {
// 获取对象
buf := pool.Get().(*bytes.Buffer)
buf.WriteString("hello")
fmt.Println(buf.String())
// 归还对象
buf.Reset()
pool.Put(buf)
// 再次获取(可能是同一个)
buf2 := pool.Get().(*bytes.Buffer)
fmt.Println(buf2.String())
}
答案:可能输出hello和空字符串(取决于是否获取到同一个对象)
解析:
sync.Pool用于缓存临时对象,减少GC压力Get获取对象,Put归还对象- Pool中的对象可能被GC随时回收,不保证持久
- 适合用于缓冲区、临时对象等
第101题:Cond条件变量
问题:以下代码输出什么?
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var mu sync.Mutex
cond := sync.NewCond(&mu)
ready := false
go func() {
time.Sleep(100 * time.Millisecond)
mu.Lock()
ready = true
cond.Broadcast() // 通知所有等待者
mu.Unlock()
}()
mu.Lock()
for !ready {
cond.Wait() // 等待条件
}
fmt.Println("ready!")
mu.Unlock()
}
答案:输出ready!
解析:
sync.Cond用于复杂的等待/通知场景Wait()会释放锁并阻塞,被唤醒后重新获取锁Signal()唤醒一个等待者,Broadcast()唤醒所有- 必须用
for检查条件(防止虚假唤醒) - 在Go中,更多场景可用channel替代Cond
第102题:Context取消
问题:以下代码展示了什么模式?
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context, id int) {
for {
select {
case <-ctx.Done():
fmt.Printf("worker %d stopped\n", id)
return
default:
fmt.Printf("worker %d working\n", id)
time.Sleep(100 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
for i := 0; i < 3; i++ {
go worker(ctx, i)
}
time.Sleep(250 * time.Millisecond)
cancel() // 取消所有
time.Sleep(100 * time.Millisecond)
}
答案:主协程取消后,所有worker停止。
解析:
context.WithCancel创建可取消的上下文cancel()关闭ctx.Done()channel,通知所有监听者- 用于级联取消多个goroutine
- context应该作为函数第一个参数
第103题:Context超时与值
问题:以下代码输出什么?
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx := context.WithValue(context.Background(), "user", "Alice")
ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
defer cancel()
// 获取值
if user := ctx.Value("user"); user != nil {
fmt.Println("user:", user)
}
select {
case <-time.After(200 * time.Millisecond):
fmt.Println("work done")
case <-ctx.Done():
fmt.Println("timeout:", ctx.Err())
}
}
答案:
user: Alice
timeout: context deadline exceeded
解析:
context.WithValue传递请求相关的元数据- context可以链式组合(超时+值)
ctx.Value获取值,键可以是任何可比较类型- 值传递应该只用于请求元数据,不要传业务参数
第104题:原子操作
问题:以下代码输出什么?原子操作有什么优势?
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
var counter int64
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
atomic.AddInt64(&counter, 1)
}()
}
wg.Wait()
fmt.Println(counter)
fmt.Println(atomic.LoadInt64(&counter))
}
答案:输出1000两次。
解析:
sync/atomic提供原子操作,比锁更高效AddInt64、LoadInt64、StoreInt64、CompareAndSwapInt64等- 适用于简单计数器、标志位等场景
- Go 1.19+增加了
atomic.Int64等类型,更易用
第105题:channel关闭原则
问题:channel关闭的最佳实践是什么?
答案:
- 谁发送谁关闭:发送方负责关闭channel
- 不要重复关闭:关闭已关闭的channel会panic
- 不要向关闭的channel发送:会panic
- 接收方检查关闭状态:
v, ok := <-ch,ok为false表示关闭
示例:
// 生产者关闭
func producer(ch chan<- int) {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)
}
// 消费者检测
func consumer(ch <-chan int) {
for v := range ch {
fmt.Println(v)
}
}
第106题:扇出Fan-out
问题:以下代码展示了什么并发模式?
package main
import (
"fmt"
"sync"
"time"
)
func producer(ch chan<- int) {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)
}
func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for j := range jobs {
time.Sleep(100 * time.Millisecond) // 模拟工作
results <- j * 2
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
go producer(jobs)
var wg sync.WaitGroup
for w := 0; w < 3; w++ {
wg.Add(1)
go worker(w, jobs, results, &wg)
}
go func() {
wg.Wait()
close(results)
}()
for r := range results {
fmt.Println(r)
}
}
答案:展示了**扇出(Fan-out)**模式,多个worker并发处理任务。
解析:
- 一个生产者,多个消费者(worker)
- 实现并行处理,提高吞吐量
- 使用WaitGroup等待所有worker完成
- 适用于CPU密集型或IO密集型任务并行化
第107题:扇入Fan-in
问题:以下代码展示了什么并发模式?
package main
import (
"fmt"
"sync"
)
func producer(name string, ch chan<- int) {
for i := 0; i < 3; i++ {
ch <- i
}
}
func fanIn(channels ...<-chan int) <-chan int {
out := make(chan int)
var wg sync.WaitGroup
wg.Add(len(channels))
for _, ch := range channels {
go func(c <-chan int) {
defer wg.Done()
for v := range c {
out <- v
}
}(ch)
}
go func() {
wg.Wait()
close(out)
}()
return out
}
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
producer("A", ch1)
close(ch1)
}()
go func() {
producer("B", ch2)
close(ch2)
}()
for v := range fanIn(ch1, ch2) {
fmt.Println(v)
}
}
答案:展示了**扇入(Fan-in)**模式,多个channel合并为一个。
解析:
- 将多个输入channel合并到单个输出channel
- 无需关心数据来自哪个源
- 常用于聚合多个服务/数据源的结果
- 与扇出配合使用,形成Pipeline
第108题:Pipeline管道
问题:以下代码展示了什么并发模式?
package main
import "fmt"
func generator(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}
func main() {
// pipeline: generator -> square
for n := range square(generator(2, 3, 4)) {
fmt.Println(n)
}
}
答案:展示了Pipeline模式,数据流经多个处理阶段。
解析:
- 每个阶段:读取输入channel,处理,发送到输出channel
- 阶段可串联形成管道
- 天然支持并发,每个阶段可独立goroutine运行
- 函数式编程风格,代码清晰易测试
第109题:优雅退出
问题:如何实现程序的优雅退出,确保资源清理?
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
done := make(chan bool, 1)
go func() {
sig := <-sigs
fmt.Println("\nReceived signal:", sig)
// 清理工作
fmt.Println("Cleaning up...")
time.Sleep(500 * time.Millisecond)
done <- true
}()
fmt.Println("Running... Press Ctrl+C to exit")
<-done
fmt.Println("Exiting gracefully")
}
答案:捕获系统信号,执行清理后退出。
解析:
signal.Notify注册感兴趣的信号- 在goroutine中等待信号
- 执行资源清理(关闭连接、保存状态等)
- 用于服务器、长时间运行程序
第110题:Or-Done模式
问题:以下代码实现了什么功能?
package main
import (
"fmt"
"time"
)
func or(channels ...<-chan interface{}) <-chan interface{} {
switch len(channels) {
case 0:
return nil
case 1:
return channels[0]
}
orDone := make(chan interface{})
go func() {
defer close(orDone)
switch len(channels) {
case 2:
select {
case <-channels[0]:
case <-channels[1]:
}
default:
select {
case <-channels[0]:
case <-channels[1]:
case <-channels[2]:
case <-or(append(channels[3:], orDone)...):
}
}
}()
return orDone
}
func main() {
sig := func(after time.Duration) <-chan interface{} {
c := make(chan interface{})
go func() {
defer close(c)
time.Sleep(after)
}()
return c
}
start := time.Now()
<-or(sig(2*time.Second), sig(1*time.Second), sig(3*time.Second))
fmt.Printf("done after %v\n", time.Since(start))
}
答案:输出done after ~1s,实现了Or-Channel模式。
解析:
- 多个channel,任意一个关闭则返回
- 类似于Unix的
select系统调用 - 用于组合多个取消信号或超时
第111题:Tee模式
问题:以下代码实现了什么功能?
package main
import "fmt"
func tee(in <-chan int) (<-chan int, <-chan int) {
out1 := make(chan int)
out2 := make(chan int)
go func() {
defer close(out1)
defer close(out2)
for v := range in {
out1, out2 := out1, out2 // 局部变量
out1 <- v
out2 <- v
}
}()
return out1, out2
}
func main() {
in := make(chan int)
go func() {
for i := 0; i < 3; i++ {
in <- i
}
close(in)
}()
out1, out2 := tee(in)
for v := range out1 {
fmt.Println("out1:", v)
}
for v := range out2 {
fmt.Println("out2:", v)
}
}
答案:将一个channel的数据同时发送到两个channel(类似于Unix的tee命令)。
解析:
- 实现数据多路复制
- 两个消费者独立接收相同数据
- 注意:如果某个消费者不接收,会阻塞
第112题:Goroutine泄漏检测
问题:以下代码有什么问题?
package main
import (
"fmt"
"time"
)
func leak() {
ch := make(chan int)
go func() {
val := <-ch // 永远等待
fmt.Println(val)
}()
// ch未关闭,goroutine永远不会退出
}
func main() {
for i := 0; i < 1000; i++ {
leak()
}
time.Sleep(time.Second)
fmt.Println("done")
}
答案:Goroutine泄漏,创建了1000个永远不会退出的goroutine。
解析:
- goroutine等待永远不会到来的channel数据
- 应确保每个创建的goroutine都能退出
- 使用context取消、关闭channel、超时等手段
- 生产环境可用
runtime.NumGoroutine()监控
第113题:竞态检测器
问题:如何检测数据竞争?
答案:使用go run -race或go test -race。
go run -race main.go
go test -race ./...
注意:
- Race detector有性能开销,不要用于生产
- 只能在编译时使用,不是运行时保护
- 可以检测到大部分数据竞争,但不是全部
示例代码:
package main
import "fmt"
func main() {
var counter int
for i := 0; i < 1000; i++ {
go func() {
counter++ // 数据竞争
}()
}
fmt.Println(counter)
}
运行go run -race main.go会报告:WARNING: DATA RACE
第114题:Ticker与Timer
问题:Ticker和Timer有什么区别?
答案:
| 特性 | Timer | Ticker |
|---|---|---|
| 次数 | 触发一次 | 周期性触发 |
| 停止 | Stop()停止 |
Stop()停止,不回收需Drain |
| 用途 | 延迟执行、超时 | 定时任务、心跳 |
| 内存 | 自动回收 | 需手动Stop,否则泄漏 |
示例:
// Timer - 延迟执行
timer := time.NewTimer(2 * time.Second)
<-timer.C // 阻塞2秒
// Ticker - 周期性
ticker := time.NewTicker(1 * time.Second)
for t := range ticker.C {
fmt.Println("Tick at", t)
}
ticker.Stop() // 必须停止,否则goroutine泄漏
第115题:并发安全的Map
问题:如何实现并发安全的Map?
答案:三种方式:
- sync.RWMutex + map:
type SafeMap struct {
mu sync.RWMutex
m map[string]int
}
- sync.Map(Go 1.9+):
var m sync.Map
m.Store("key", value)
value, ok := m.Load("key")
- 分片锁(减少锁竞争):
type ShardedMap struct {
shards [32]struct {
sync.RWMutex
m map[string]interface{}
}
}
sync.Map适用场景:
- 只追加不删除
- 读多写少
- 多个goroutine访问不相交的key集合
第116题:Worker Pool
问题:实现一个固定大小的worker pool。
package main
import (
"fmt"
"sync"
"time"
)
type Task func()
type Pool struct {
tasks chan Task
wg sync.WaitGroup
}
func NewPool(size int) *Pool {
p := &Pool{
tasks: make(chan Task),
}
p.wg.Add(size)
for i := 0; i < size; i++ {
go p.worker()
}
return p
}
func (p *Pool) worker() {
defer p.wg.Done()
for task := range p.tasks {
task()
}
}
func (p *Pool) Submit(task Task) {
p.tasks <- task
}
func (p *Pool) Shutdown() {
close(p.tasks)
p.wg.Wait()
}
func main() {
pool := NewPool(3)
for i := 0; i < 10; i++ {
n := i
pool.Submit(func() {
fmt.Printf("Task %d executed\n", n)
time.Sleep(100 * time.Millisecond)
})
}
pool.Shutdown()
}
答案:创建了3个worker的pool,处理10个任务。
解析:
- 限制并发数,防止资源耗尽
- 任务队列解耦生产和消费
- 优雅关闭:关闭channel,等待所有worker完成
六、内存与GC (15题)
第117题:逃逸分析
问题:什么是逃逸分析?以下变量会分配在堆还是栈?
package main
func foo() *int {
x := 42
return &x // 会逃逸吗?
}
func bar() int {
x := 42
return x // 会逃逸吗?
}
func main() {
_ = foo()
_ = bar()
}
答案:
foo中的x会逃逸到堆:返回了指针,函数结束后仍需访问bar中的x分配在栈:只是返回值,可以复制
解析:
- 逃逸分析:编译器分析变量的作用域,决定分配在栈还是堆
- 逃逸到堆的场景:
- 返回局部变量的指针
- 闭包捕获局部变量
- 发送到channel的指针
- 接口类型的值(需要动态分发)
- 大对象(>32KB)
- 栈分配更快,函数结束自动回收
- 查看逃逸分析:
go build -gcflags="-m"
第118题:垃圾回收算法
问题:Go使用什么垃圾回收算法?有什么特点?
答案: Go使用**并发三色标记-清除(Concurrent Tri-color Mark-Sweep)**算法。
特点:
- 并发:GC与程序并发运行,减少STW(Stop The World)
- 三色标记:
- 白色:未访问,可能回收
- 灰色:已访问,引用未扫描
- 黑色:已访问,引用已扫描
- 写屏障(Write Barrier):标记期间修改的指针处理
- 混合写屏障:Go 1.8+引入,进一步减少STW
STW时间:
- Go 1.5: 几十到几百毫秒
- Go 1.8+: 通常<100微秒
第119题:GC触发时机
问题:GC什么时候触发?如何调优?
答案:
触发时机:
- 内存分配量达到阈值:默认当堆内存达到上次GC后的2倍
- 手动触发:
runtime.GC() - 定时触发:2分钟未GC强制触发
调优参数:
// 设置GC目标百分比(默认100,即2倍)
// 100表示堆内存增长到2倍时触发GC
debug.SetGCPercent(100)
// 设置内存限制(Go 1.19+)
debug.SetMemoryLimit(10 << 30) // 10GB
最佳实践:
- 不要频繁调用
runtime.GC() - 减少堆上对象分配(使用sync.Pool)
- 避免不必要的指针(值类型替代指针)
第120题:内存分配器
问题:Go的内存分配器有什么特点?
答案: Go使用**TCMalloc(Thread-Caching Malloc)**启发式内存分配器。
核心概念:
- mspan:内存管理基本单位,包含若干页
- mcache:每个P(Processor)的本地缓存,无锁分配
- mcentral:中心缓存,按size class组织
- mheap:全局堆,大对象直接分配
分配策略:
- 小对象(≤32KB):从mcache分配,无锁
- 大对象(>32KB):直接从mheap分配
优势:
- 减少锁竞争(每个P独立cache)
- 减少内存碎片(size class)
第121题:指针与性能
问题:指针一定比值快吗?什么时候用值类型更好?
答案:
指针的优势:
- 避免大对象复制
- 可以修改原值
- 实现共享和引用语义
指针的劣势:
- 逃逸到堆,GC压力
- 缓存不友好(数据分散)
- 额外的内存访问(解引用)
值类型更好的场景:
- 小结构体(≤64字节)
- 读多写少
- 不需要修改原值
- 对缓存友好要求高
示例:
type Point struct { X, Y float64 } // 16字节,用值类型
type BigStruct struct { data [1024]int } // 大结构体,用指针
第122题:内存对齐
问题:以下结构体占用多少内存?如何优化?
package main
import (
"fmt"
"unsafe"
)
type BadStruct struct {
A bool // 1字节
B int32 // 4字节
C bool // 1字节
D int64 // 8字节
E bool // 1字节
}
type GoodStruct struct {
D int64 // 8字节
B int32 // 4字节
A bool // 1字节
C bool // 1字节
E bool // 1字节
}
func main() {
fmt.Println("BadStruct:", unsafe.Sizeof(BadStruct{}))
fmt.Println("GoodStruct:", unsafe.Sizeof(GoodStruct{}))
}
答案:
BadStruct: 40
GoodStruct: 16
解析:
- Go按照字段大小对齐(int64对齐到8字节)
- 字段顺序影响内存填充(padding)
- 优化原则:大字段在前,小字段在后,减少填充
第123题:堆与栈的区别
问题:Go中堆和栈有什么区别?
| 特性 | 栈 | 堆 |
|---|---|---|
| 管理 | 编译器自动 | GC管理 |
| 速度 | 极快(移动SP指针) | 较慢(分配+GC) |
| 大小 | 有限(初始2KB,可增长) | 仅受限于系统内存 |
| 生命周期 | 函数返回即释放 | GC决定 |
| 线程安全 | 协程私有 | 需同步机制 |
| 用途 | 局部变量 | 全局、逃逸变量 |
第124题:内存泄漏场景
问题:Go中常见的内存泄漏场景有哪些?
答案:
- Goroutine泄漏:
// 泄漏:等待永远不会到来的channel数据
ch := make(chan int)
go func() {
<-ch // 永远阻塞
}()
- Map持续增长:
// 只添加不删除
m := make(map[string][]byte)
// m[key] = largeData
- 全局缓存无限制:
var cache = make(map[string]interface{})
// 没有淘汰策略,无限增长
- time.After滥用:
for {
select {
case <-time.After(time.Minute): // 每次循环创建新Timer
}
}
- 字符串拼接:
// 大数据拷贝
s := ""
for _, part := range parts {
s += part // 每次都分配新内存
}
// 应使用strings.Builder
第125题:大对象分配
问题:Go如何处理大对象分配?
答案:
- 大对象:>32KB
- 直接跳过mcache和mcentral
- 从mheap分配,使用空闲列表或向OS申请
- 可能触发GC
优化建议:
- 避免频繁分配大对象
- 使用sync.Pool重用对象
- 考虑使用
make([]byte, 0, capacity)预分配
第126题:GC调优实践
问题:如何诊断和优化GC问题?
答案:
诊断工具:
# 查看GC统计
GODEBUG=gctrace=1 go run main.go
# 输出示例
gc 1 @0.015s 2%: 0.015+0.32+0.045 ms clock, 0.12+0.15/0.32/0.55+0.36 ms cpu, 4->4->0 MB, 5 MB goal, 8 P
优化策略:
- 减少堆分配(使用
go test -benchmem检测) - 对象池复用(sync.Pool)
- 避免不必要的接口(接口导致值逃逸)
- 调整
GOGC环境变量 - 设置内存限制(Go 1.19+)
Playground:略(需要环境变量)
第127题:Finalizer
问题:什么是Finalizer?有什么使用限制?
答案:
runtime.SetFinalizer(obj, func(obj *Type) {
// 对象被GC前执行
})
特点:
- 对象被GC前执行清理操作
- 不保证执行时间(可能延迟)
- 不保证一定执行(程序退出时不执行)
限制:
- 可能延长对象生命周期
- 循环引用导致无法GC
- 难以调试
- 建议:显式Close/Stop,不要用Finalizer
第128题:内存屏障
问题:什么是写屏障?有什么作用?
答案:
写屏障(Write Barrier):
- GC标记阶段开启的特殊机制
- 记录指针修改,确保三色标记正确
作用:
- 防止漏标(丢失存活对象)
- 允许GC与mutator(程序)并发执行
类型:
- Dijkstra写屏障:保守,可能多标
- Yuasa写屏障:精确
- 混合写屏障:Go 1.8+使用,结合两者优点
第129题:runtime.ReadMemStats
问题:如何获取程序内存使用情况?
package main
import (
"fmt"
"runtime"
)
func main() {
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("Alloc = %v MB\n", m.Alloc/1024/1024)
fmt.Printf("TotalAlloc = %v MB\n", m.TotalAlloc/1024/1024)
fmt.Printf("Sys = %v MB\n", m.Sys/1024/1024)
fmt.Printf("NumGC = %v\n", m.NumGC)
}
答案:输出内存分配、系统内存、GC次数等统计。
解析:
Alloc:当前堆分配的对象大小TotalAlloc:累计分配大小Sys:从OS申请的内存NumGC:GC执行次数- 可用于监控和调试内存问题
第130题:Go的栈管理
问题:Go的栈有什么特点?
答案:
-
动态增长:
- 初始2KB
- 需要时自动增长(分段栈,Go 1.3后改为连续栈)
-
协程私有:
- 每个goroutine独立栈
- 栈切换简单高效
-
栈拷贝:
- 栈增长时,旧栈数据复制到新栈
- 更新所有指向栈的指针
-
无溢出风险:
- 不像C有固定栈大小
- 栈空间理论无限(受限于内存)
第131题:对象生命周期
问题:以下对象的生命周期如何?
package main
func create() []int {
data := make([]int, 1000) // 逃逸到堆
return data
}
func process() {
buf := make([]byte, 1024) // 可能分配在栈
_ = buf
}
func main() {
_ = create()
process()
}
答案:
data:分配在堆,直到GC回收buf:可能分配在栈,函数返回即释放
解析:
- 逃逸分析决定对象位置
go build -gcflags="-m -l"查看逃逸情况- 尽量让对象在栈上分配,减少GC压力
七、标准库 (15题)
第132题:strings.Builder
问题:strings.Builder相比直接拼接有什么优势?
package main
import (
"fmt"
"strings"
)
func main() {
var b strings.Builder
b.Grow(100) // 预分配
for i := 0; i < 10; i++ {
b.WriteString("hello ")
}
fmt.Println(b.String())
}
答案:更高效,减少内存分配。
解析:
- 直接拼接
+:每次创建新字符串,O(n²) strings.Builder:内部使用切片,动态扩容Grow预分配可避免多次扩容- 比
bytes.Buffer更轻量(不实现Writer以外的接口)
第133题:bytes.Buffer
问题:bytes.Buffer和strings.Builder有什么区别?
| 特性 | bytes.Buffer | strings.Builder |
|---|---|---|
| 目的 | 通用字节缓冲 | 高效字符串拼接 |
| 实现接口 | io.Reader/Writer等 | io.Writer/StringWriter |
| 线程安全 | 否 | 否 |
| Reset重用 | 是 | 是 |
| 使用场景 | IO操作 | 字符串构建 |
建议:字符串拼接用strings.Builder,其他用bytes.Buffer。
第134题:json序列化
问题:以下代码输出什么?
package main
import (
"encoding/json"
"fmt"
)
type User struct {
Name string `json:"name"`
Age int `json:"age,omitempty"`
Email string `json:"-"` // 忽略
}
func main() {
u := User{Name: "Tom", Age: 0}
b, _ := json.Marshal(u)
fmt.Println(string(b))
}
答案:{"name":"Tom"}(Age为0被omitempty忽略)
解析:
- 结构体标签控制序列化行为
omitempty:零值时忽略-:完全忽略该字段- 自定义序列化:实现
json.Marshaler接口
第135题:json反序列化
问题:以下代码有什么问题?
package main
import (
"encoding/json"
"fmt"
)
func main() {
data := `{"name":"Tom","age":30}`
var m map[string]interface{}
json.Unmarshal([]byte(data), &m)
age := m["age"].(int) // 能成功吗?
fmt.Println(age)
}
答案:运行时panic:interface {} is float64, not int
解析:
json.Unmarshal将数字转为float64(为了兼容JSON number)- 不能直接断言为int
- 解决方案:
- 使用具体结构体:
var user struct{ Age int } - 使用
json.Decoder.UseNumber() - 类型断言为
float64再转int
- 使用具体结构体:
第136题:time包
问题:以下代码有什么问题?
package main
import (
"fmt"
"time"
)
func main() {
t1 := time.Now()
time.Sleep(100 * time.Millisecond)
t2 := time.Now()
if t2.After(t1) {
fmt.Println("t2 is later")
}
// 时间比较
fmt.Println(t1 == t2) // 能比较吗?
}
答案:可以比较,time.Time可比较。
解析:
time.Time内部是结构体,包含wall time和monotonic reading- 可以用
==、Before、After、Equal比较 Equal考虑时区,比==更推荐- 建议使用
time.Now().Sub(t1)计算耗时
第137题:文件操作
问题:以下代码有什么问题?
package main
import (
"fmt"
"os"
)
func main() {
f, err := os.Open("test.txt")
if err != nil {
panic(err)
}
// 忘记关闭
buf := make([]byte, 1024)
n, _ := f.Read(buf)
fmt.Println(string(buf[:n]))
}
答案:文件句柄泄漏。
解析:
- 必须使用
defer f.Close()关闭文件 - 即使发生错误也要关闭
- 标准做法:
f, err := os.Open("test.txt")
if err != nil {
return err
}
defer f.Close()
第138题:filepath与path
问题:filepath和path包有什么区别?
答案:
| 包 | 用途 | 示例 |
|---|---|---|
path |
URL路径,正斜杠 | path.Join("a", "b") → “a/b” |
filepath |
文件系统路径,跨平台 | filepath.Join("a", "b") → “a\b”(Windows) |
建议:
- 处理URL用
path - 处理文件用
filepath
第139题:正则表达式
问题:以下代码有什么问题?
package main
import (
"fmt"
"regexp"
)
func main() {
for i := 0; i < 1000; i++ {
matched, _ := regexp.MatchString(`\d+`, "12345")
fmt.Println(matched)
}
}
答案:每次循环编译正则,性能差。
解析:
regexp.MatchString每次编译正则表达式- 应预编译正则:
var digitRegexp = regexp.MustCompile(`\d+`)
func main() {
for i := 0; i < 1000; i++ {
matched := digitRegexp.MatchString("12345")
fmt.Println(matched)
}
}
第140题:sort排序
问题:以下代码如何实现自定义排序?
package main
import (
"fmt"
"sort"
)
type Person struct {
Name string
Age int
}
type ByAge []Person
func (a ByAge) Len() int { return len(a) }
func (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }
func main() {
people := []Person{
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35},
}
sort.Sort(ByAge(people))
fmt.Println(people)
}
答案:实现sort.Interface接口(Len、Swap、Less)。
解析:
- 实现三个方法即可自定义排序
- Go 1.8+可以用
sort.Slice简化:
sort.Slice(people, func(i, j int) bool {
return people[i].Age < people[j].Age
})
第141题:HTTP客户端
问题:以下代码有什么问题?
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
resp, err := http.Get("https://example.com")
if err != nil {
panic(err)
}
// defer resp.Body.Close() // 忘记关闭?
body, _ := io.ReadAll(resp.Body)
fmt.Println(len(body))
}
答案:必须关闭resp.Body,否则连接泄漏。
解析:
- 即使不读取也要关闭Body
- 标准做法:
defer resp.Body.Close() - 长连接会复用,关闭是归还连接池
第142题:HTTP服务端
问题:以下代码有什么问题?
package main
import (
"fmt"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello")
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
答案:没有错误处理,无法优雅关闭。
解析:
- 应使用
http.Server提供关闭能力:
srv := &http.Server{Addr: ":8080"}
http.HandleFunc("/", handler)
// 优雅关闭
go func() {
if err := srv.ListenAndServe(); err != nil {
log.Fatal(err)
}
}()
// 等待信号
<-sig
srv.Shutdown(ctx)
第143题:context使用
问题:context的值传递应该传什么?
答案:
适合传递:
- 请求ID(追踪)
- 用户身份信息(认证后)
- 截止时间/超时信息
不适合传递:
- 数据库连接
- 业务参数
- 大对象
最佳实践:
// 在middleware中设置
ctx = context.WithValue(ctx, "request_id", generateID())
// 在handler中获取
requestID, ok := ctx.Value("request_id").(string)
第144题:strconv
问题:strconv和fmt.Sprint有什么区别?
package main
import (
"fmt"
"strconv"
)
func main() {
// 性能测试
n := 12345
// 方式1:fmt
s1 := fmt.Sprintf("%d", n)
// 方式2:strconv
s2 := strconv.Itoa(n)
fmt.Println(s1, s2)
}
答案:strconv更快,专门用于字符串转换。
解析:
| 函数 | 用途 |
|---|---|
strconv.Itoa |
int → string |
strconv.Atoi |
string → int |
strconv.FormatInt |
int64 → string(可指定进制) |
strconv.ParseFloat |
string → float64 |
建议:转换用strconv,格式化用fmt。
第145题:log包
问题:如何定制log输出格式?
package main
import (
"log"
"os"
)
func main() {
// 设置前缀和格式
logger := log.New(os.Stdout, "[APP] ", log.LstdFlags|log.Lshortfile)
logger.Println("info message")
logger.Printf("user %s logged in", "alice")
}
答案:使用log.New创建自定义logger。
解析:
LstdFlags:标准时间格式Lshortfile:文件名和行号Lmicroseconds:微秒精度- 生产环境建议使用
log/slog(Go 1.21+)或第三方库
第146题:flag解析
问题:如何解析命令行参数?
package main
import (
"flag"
"fmt"
)
func main() {
var (
host = flag.String("host", "localhost", "server host")
port = flag.Int("port", 8080, "server port")
debug = flag.Bool("debug", false, "enable debug")
)
flag.Parse()
fmt.Printf("Host: %s, Port: %d, Debug: %t\n", *host, *port, *debug)
}
答案:使用flag包。
解析:
flag.String返回指针,需要解引用- 调用
flag.Parse()解析参数 - 可以使用
flag.Var自定义类型 - Go 1.22+可用
flag.Func处理复杂场景
八、工程实践 (15题)
第147题:错误处理
问题:Go如何处理错误?与异常有什么区别?
答案:
Go使用显式错误返回,不用异常:
result, err := doSomething()
if err != nil {
return err
}
与异常的区别:
| 特性 | Go错误 | 异常(try-catch) |
|---|---|---|
| 显式性 | 必须处理 | 可忽略 |
| 流程控制 | 正常返回 | 跳转栈 |
| 性能 | 无开销 | 有开销 |
| 代码可读性 | 错误就在调用处 | 可能在远处捕获 |
panic/recover:仅用于不可恢复的错误。
第148题:错误包装
问题:Go 1.13+的错误包装有什么好处?
package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("not found")
func findUser(id int) error {
// 模拟查找失败
return fmt.Errorf("find user %d: %w", id, ErrNotFound)
}
func main() {
err := findUser(42)
if errors.Is(err, ErrNotFound) {
fmt.Println("User not found")
}
fmt.Println(err)
}
答案:可以保留原始错误信息,用errors.Is判断错误类型。
解析:
%w包装错误errors.Is:检查错误链中是否包含某错误errors.As:将错误转为特定类型- 比
err.Error()字符串比较更可靠
第149题:单元测试
问题:如何编写和运行单元测试?
// math.go
package math
func Add(a, b int) int {
return a + b
}
// math_test.go
package math
import "testing"
func TestAdd(t *testing.T) {
result := Add(1, 2)
if result != 3 {
t.Errorf("Add(1, 2) = %d, want 3", result)
}
}
func TestAddTable(t *testing.T) {
tests := []struct {
a, b, want int
}{
{1, 2, 3},
{0, 0, 0},
{-1, 1, 0},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("%d+%d", tt.a, tt.b), func(t *testing.T) {
got := Add(tt.a, tt.b)
if got != tt.want {
t.Errorf("got %d, want %d", got, tt.want)
}
})
}
}
运行:go test -v
Playground:略(需要多文件)
第150题:基准测试
问题:如何编写基准测试?
package math
import "testing"
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(1, 2)
}
}
func BenchmarkAddParallel(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
Add(1, 2)
}
})
}
运行:
go test -bench=. -benchmem
go test -bench=. -cpuprofile=cpu.prof
go tool pprof cpu.prof
第151题:表驱动测试
问题:什么是表驱动测试?有什么好处?
答案:
func TestDivide(t *testing.T) {
tests := []struct {
name string
a, b int
want int
wantErr bool
}{
{"normal", 10, 2, 5, false},
{"negative", -10, 2, -5, false},
{"zero_divisor", 10, 0, 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Divide(tt.a, tt.b)
if (err != nil) != tt.wantErr {
t.Errorf("Divide() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("Divide() = %v, want %v", got, tt.want)
}
})
}
}
好处:
- 易于添加新测试用例
- 测试逻辑统一,减少重复
- 输出清晰(t.Run子测试)
第152题:项目结构
问题:Go项目推荐的项目结构是什么?
答案(标准布局):
myproject/
├── cmd/ # 可执行文件
│ ├── server/
│ │ └── main.go
│ └── cli/
│ └── main.go
├── internal/ # 私有代码
│ ├── service/
│ └── repository/
├── pkg/ # 公共库(可选)
├── api/ # API定义
├── configs/ # 配置文件
├── deployments/ # 部署脚本
├── scripts/ # 脚本
├── go.mod
└── README.md
说明:
cmd:每个子目录一个可执行文件internal:不允许外部导入pkg:可以被外部导入的库
第153题:模块管理
问题:Go Modules如何解决依赖管理?常用命令有哪些?
答案:
常用命令:
go mod init myproject # 初始化
go get github.com/pkg/name # 添加依赖
go get -u ./... # 更新所有依赖
go mod tidy # 清理未使用依赖
go mod vendor # 创建vendor目录
go mod graph # 查看依赖图
go.mod文件:
module myproject
go 1.21
require (
github.com/gin-gonic/gin v1.9.1
github.com/stretchr/testify v1.8.4
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
)
第154题:接口设计原则
问题:Go接口设计有什么原则?
答案:
-
小接口原则:接口越小越好
// 好 type Reader interface { Read(p []byte) (n int, err error) } -
组合优于继承:
type ReadWriter interface { Reader Writer } -
接口在消费者端定义:
// 不要预先定义,让使用者定义需要的接口 type Stringer interface { String() string } -
接受接口,返回具体类型
第155题:性能优化原则
问题:Go性能优化的一般步骤是什么?
答案:
- 先写正确的代码:不要过早优化
- 用benchmark测量:找到瓶颈
- 用pprof分析:
import _ "net/http/pprof" go func() { log.Println(http.ListenAndServe("localhost:6060", nil)) }() - 常见优化点:
- 减少堆分配(避免逃逸)
- 使用sync.Pool
- 预分配slice/map容量
- 避免不必要的字符串转换
- 使用strconv代替fmt
第156题:优雅关闭
问题:如何实现HTTP服务的优雅关闭?
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
srv := &http.Server{Addr: ":8080"}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(2 * time.Second)
w.Write([]byte("Hello"))
})
// 启动服务
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}()
// 等待中断信号
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
<-sig
// 优雅关闭
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal(err)
}
log.Println("Server stopped")
}
答案:使用http.Server.Shutdown,给正在处理的请求完成时间。
第157题:配置管理
问题:Go项目如何管理配置?
答案:
-
环境变量:
port := os.Getenv("PORT") if port == "" { port = "8080" } -
配置文件(推荐Viper):
viper.SetConfigName("config") viper.SetConfigType("yaml") viper.AddConfigPath(".") viper.ReadInConfig() port := viper.GetInt("server.port") -
命令行参数:
flag包 -
12-Factor原则:配置存储在环境中
第158题:日志规范
问题:Go项目日志有什么最佳实践?
答案:
-
结构化日志(log/slog):
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) logger.Info("request processed", "method", r.Method, "path", r.URL.Path, "duration", time.Since(start), ) -
日志级别:Debug、Info、Warn、Error、Fatal
-
包含上下文:时间、请求ID、用户、堆栈等
-
不要:
- 打印敏感信息
- 在循环中大量打印
- 使用
+拼接字符串(用slog的键值对)
第159题:数据库连接
问题:如何管理数据库连接池?
db, err := sql.Open("mysql", dsn)
if err != nil {
log.Fatal(err)
}
defer db.Close()
// 连接池配置
db.SetMaxOpenConns(25) // 最大连接数
db.SetMaxIdleConns(5) // 最大空闲连接
db.SetConnMaxLifetime(5 * time.Minute) // 连接最大生命周期
// 验证连接
if err := db.Ping(); err != nil {
log.Fatal(err)
}
答案:合理配置连接池参数,避免连接耗尽或过多。
第160题:API设计
问题:Go REST API设计有什么建议?
答案:
-
路由清晰:
GET /users # 列表 GET /users/:id # 详情 POST /users # 创建 PUT /users/:id # 更新 DELETE /users/:id # 删除 -
统一响应格式:
{ "code": 0, "message": "success", "data": {...} } -
错误处理:HTTP状态码 + 错误信息
-
版本控制:
/v1/users -
使用中间件:日志、认证、恢复
第161题:代码质量
问题:如何保证Go代码质量?
答案:
工具:
# 格式化
go fmt ./...
# 代码检查
go vet ./...
golint ./...
staticcheck ./...
# 测试
go test -race -cover ./...
# CI/CD集成
实践:
- Code Review
- 单元测试覆盖率>80%
- 使用gofmt统一格式
- 静态分析发现潜在问题
- 文档注释(godoc)
九、源码与高级主题 (20题)
第162题:Goroutine调度器
问题:Goroutine是如何调度的?
答案:
Go使用GMP模型:
- G(Goroutine):轻量级协程,初始2KB栈
- M(Machine):OS线程,执行G的载体
- P(Processor):逻辑处理器,管理G队列
调度过程:
- 新建G放入P的本地队列
- M绑定P,从P获取G执行
- G阻塞时(系统调用/channel),M与P分离
- P从全局队列或其他P偷取G(Work Stealing)
优势:
- 用户态调度,切换快(~200ns)
- 少量M支撑大量G(M:N模型)
- 利用多核并行
第163题:调度策略
问题:什么是Work Stealing和Handoff?
答案:
Work Stealing(工作窃取):
- 当P的本地队列为空时
- 从全局队列或其他P的队列偷取G
- 保持负载均衡
Handoff(交接):
- G发生系统调用阻塞时
- M释放P,P可以绑定其他M继续执行
- 阻塞的M等待系统调用返回
- 避免阻塞一个G就浪费一个线程
第164题:channel底层实现
问题:channel是如何实现的?
答案:
type hchan struct {
qcount uint // 队列中元素个数
dataqsiz uint // 环形队列大小
buf unsafe.Pointer // 环形队列指针
elemsize uint16 // 元素大小
closed uint32 // 是否关闭
elemtype *_type // 元素类型
sendx uint // 发送索引
recvx uint // 接收索引
recvq waitq // 等待接收的goroutine队列
sendq waitq // 等待发送的goroutine队列
lock mutex // 互斥锁
}
关键:
- 有缓冲:环形队列存储数据
- 无缓冲:直接交换,需要配对
- 阻塞时goroutine进入waitq等待
第165题:select底层实现
问题:select是如何实现的?
答案:
编译器转换:
// 源代码
select {
case c1 <- v1:
case v2 = <-c2:
case v3, ok = <-c3:
default:
}
执行步骤:
- 打乱case顺序(随机选择)
- 尝试所有case,有可执行的立即执行
- 无可执行的:
- 有default:执行default
- 无default:当前G加入所有channel的等待队列,阻塞
- 被唤醒后,从等待队列移除,继续执行
第166题:map底层实现
问题:map是如何实现的?扩容机制是什么?
答案:
结构:
type hmap struct {
count int
flags uint8
B uint8 // 桶数 = 2^B
noverflow uint16
hash0 uint32 // 哈希种子
buckets unsafe.Pointer // 桶数组
oldbuckets unsafe.Pointer // 扩容时的旧桶
nevacuate uintptr // 扩容进度
}
实现:
- 哈希表 + 拉链法
- 每个桶存储8个key-value对
- 超过8个用overflow桶
扩容:
- 翻倍扩容:负载因子>6.5时
- 等量扩容:overflow桶太多时(整理碎片)
- 渐进式扩容,每次操作迁移部分数据
第167题:interface底层
问题:interface的底层结构是什么?
答案:
非空接口:
type iface struct {
tab *itab // 类型+方法表
data unsafe.Pointer // 数据指针
}
空接口:
type eface struct {
_type *_type
data unsafe.Pointer
}
itab:
- 接口类型
- 动态类型
- 方法表(接口方法到具体类型方法的映射)
第168题:reflect性能
问题:反射为什么慢?如何优化?
答案:
慢的原因:
- 绕过编译器优化
- 类型检查(interface{}转具体类型)
- 内存分配(装箱/拆箱)
- 间接调用(通过函数指针)
优化方法:
- 减少反射使用(用codegen)
- 缓存Type/Value(避免重复反射)
- 使用
map[string]any替代struct反射 - 使用unsafe(谨慎)
第169题:unsafe包
问题:unsafe包有什么使用场景?有什么风险?
package main
import (
"fmt"
"unsafe"
)
func main() {
s := "hello"
// 获取string底层指针
ptr := (*[5]byte)(unsafe.Pointer(
(*[2]uintptr)(unsafe.Pointer(&s))[0]))
fmt.Printf("%c\n", ptr[0])
}
答案:用于底层内存操作,如性能优化、与C交互。
风险:
- 破坏类型安全
- 可能panic
- 不保证跨版本兼容
- 可能被GC误回收
使用原则:
- 只在必要时使用
- 充分测试
- 添加详细注释
第170题:cgo使用
问题:如何在Go中调用C代码?
package main
/*
#include <stdio.h>
void hello() {
printf("Hello from C!\n");
}
*/
import "C"
import "fmt"
func main() {
C.hello()
fmt.Println("Hello from Go!")
}
答案:使用import "C"和注释中的C代码。
注意:
- 有C调用开销
- 破坏跨平台编译
- CGO_ENABLED=0禁用
- 复杂项目用swig
第171题:内存模型
问题:Go的内存模型是什么?happens-before关系有哪些?
答案:
Happens-Before:
- 如果A happens-before B,则A的修改对B可见
保证:
- ** Goroutine创建**:
go语句happens-before goroutine执行 - Channel:
- 发送happens-before接收完成
- 关闭happens-before接收零值
- Mutex:Unlock happens-before 后续Lock
- Once:Do返回happens-before 后续Do返回
- WaitGroup:Wait返回happens-before 所有Done
建议:不要依赖复杂同步,用channel和sync包。
第172题:逃逸分析原理
问题:编译器如何判断变量是否逃逸?
答案:
逃逸场景:
- 返回局部变量指针
- 发送到channel的指针
- 闭包捕获局部变量
- 接口类型的值(需要动态分发)
- 大对象(>32KB)
分析过程:
- 指针分析:追踪指针的传递
- 如果指针可能超出函数生命周期 → 逃逸到堆
查看:go build -gcflags="-m"
第173题:编译过程
问题:Go程序是如何编译的?
答案:
编译步骤:
- 词法分析:源代码 → Token
- 语法分析:Token → AST(抽象语法树)
- 类型检查:AST语义分析
- 中间代码:AST → SSA(静态单赋值)
- 优化:死代码消除、内联、逃逸分析
- 机器码:SSA → 目标架构机器码
- 链接:合并目标文件
工具链:go tool compile、go tool link
第174题:内联优化
问题:什么是函数内联?有什么优缺点?
答案:
内联:编译器将函数调用替换为函数体,消除调用开销。
Go内联规则:
- 小函数(行数少、复杂度低)自动内联
- 有
//go:noinline注释不内联 - 递归函数不内联
有defer/runtime包的函数不内联(⚠️ Go 1.14+已改变,见下方)
关于defer的内联(Go 1.14+):
- Go 1.14引入"开放编码的defer(open-coded defers)”
- Go 1.14+带有defer的函数也可以被内联(满足其他条件时)
- open-coded defer显著降低了defer的开销
- 但defer在循环中仍可能分配内存
优点:
- 消除调用开销
- 允许更多优化
缺点:
- 代码膨胀
- 编译时间增加
查看内联:go build -gcflags="-m"
第175题:编译标签
问题:如何实现条件编译?
答案:
文件标签:
// +build linux
package main
文件名后缀:
xxx_linux.go:Linux专用xxx_windows.go:Windows专用xxx_amd64.go:64位专用
使用:
// 编译指定标签
go build -tags debug
第176题:init执行顺序
问题:多个包有init函数时,执行顺序是什么?
答案:
顺序:
- 先执行导入包的init(递归,深度优先)
- 按文件名字母顺序执行同包init
- 最后执行当前包init
- 然后执行main函数
依赖包:先执行被依赖包的init
示例:
main → A → B
→ C
执行顺序:B.init → A.init → C.init → main.init → main
第177题:plugin包
问题:如何实现Go代码的动态加载?
// 编译为插件
go build -buildmode=plugin -o plugin.so plugin.go
// 加载插件
p, err := plugin.Open("plugin.so")
sym, err := p.Lookup("VarName")
答案:使用plugin包。
限制:
- 仅Linux、macOS
- 不支持Windows
- 复杂且不稳定
- 生产环境建议用RPC/HTTP
第178题:go:generate
问题:如何自动化代码生成?
答案:
//go:generate protoc --go_out=. *.proto
//go:generate go run github.com/golang/mock/mockgen -source=iface.go -destination=mock.go
package main
运行:
go generate ./...
用途:
- Protocol Buffers代码生成
- Mock生成
- 字符串方法生成
- 配置解析生成
第179题:pprof使用
问题:如何用pprof诊断性能问题?
答案:
import _ "net/http/pprof"
func main() {
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// ...
}
分析:
# CPU分析
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
# 内存分析
go tool pprof http://localhost:6060/debug/pprof/heap
# 火焰图
go tool pprof -http=:8080 profile.pb.gz
类型:
- profile:CPU
- heap:内存
- goroutine:goroutine堆栈
- mutex:锁竞争
- block:阻塞
第180题:trace工具
问题:如何分析程序执行时间线?
答案:
# 生成trace
curl http://localhost:6060/debug/pprof/trace?seconds=5 > trace.out
# 分析
go tool trace trace.out
可以看到:
- goroutine调度
- GC事件
- 阻塞事件
- Syscall
- 每个goroutine的时间线
第181题:运行时调试
问题:有哪些运行时调优参数?
答案:
环境变量:
# GC目标百分比(默认100,即2倍增长触发)
GOGC=100
# 最大处理器数
GOMAXPROCS=8
# 调试输出
GODEBUG=gctrace=1 # GC追踪
GODEBUG=schedtrace=1000 # 调度追踪
runtime函数:
runtime.GOMAXPROCS(n) // 设置P数量
runtime.SetGCPercent(100) // 设置GC百分比
runtime.ReadMemStats(&m) // 读取内存统计
runtime.NumGoroutine() // goroutine数量
runtime.NumCPU() // CPU核心数
十、Go版本新特性 (10题)
第182题:Go 1.22 for循环整数范围
问题:Go 1.22引入了哪些for循环新特性?
package main
import "fmt"
func main() {
// Go 1.22+ 支持直接遍历整数范围
for i := range 5 {
fmt.Println(i) // 输出 0 1 2 3 4
}
// 也可以指定起始和结束
for i := range 2, 5 {
fmt.Println(i) // 输出 2 3 4
}
}
答案:
- Go 1.22+支持
for i := range N语法,等效于for i := 0; i < N; i++ - 支持
for i := range start, end,遍历[start, end)区间
注意:
- 这是Go 1.22的实验性特性,Go 1.23+正式支持
- 需要
GOEXPERIMENT=rangefunc环境变量(Go 1.22)
第183题:Go 1.21 log/slog结构化日志
问题:Go 1.21引入的slog包有什么优势?
package main
import (
"log/slog"
"os"
)
func main() {
// 创建JSON格式logger
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
// 结构化日志
logger.Info("request processed",
"method", "GET",
"path", "/api/users",
"duration_ms", 42,
)
}
答案:
优势:
- 结构化:支持key-value格式,便于机器解析
- 多格式:支持JSON和文本格式
- 高性能:比fmt.Sprintf更快
- 标准化:统一标准库日志接口
输出:
{"time":"2024-01-01T12:00:00Z","level":"INFO","msg":"request processed","method":"GET","path":"/api/users","duration_ms":42}
第184题:Go 1.21 slices、maps、cmp包
问题:Go 1.21引入了哪些新的标准库包?
答案:
slices包:
import "slices"
s := []int{3, 1, 4, 1, 5}
slices.Sort(s) // 排序
slices.Contains(s, 4) // 是否包含
slices.Equal(a, b) // 比较相等
slices.BinarySearch(s, 4) // 二分查找
maps包:
import "maps"
m1 := map[string]int{"a": 1, "b": 2}
m2 := map[string]int{"b": 2, "a": 1}
maps.Equal(m1, m2) // true(比较内容,不关心顺序)
m3 := maps.Clone(m1) // 深拷贝
cmp包:
import "cmp"
cmp.Compare(1, 2) // 返回 -1(小于)、0(等于)、1(大于)
cmp.Less(1, 2) // true
cmp.Or(a, b, c) // 返回第一个非零值
第185题:Go 1.22 math/rand/v2
问题:Go 1.22的math/rand/v2有什么改进?
答案:
import "math/rand/v2"
// 新的全局函数,更简洁
n := rand.IntN(100) // [0, 100)
// 新的随机源接口
type Source interface {
Uint64() uint64
}
// 性能更好,算法改进(PCG算法)
改进点:
- 性能提升:PCG算法比之前的LCG更快
- API简化:
IntN(n)代替Intn(n)(注意大小写) - 更清晰:
rand/v2明确表示v2版本
第186题:Go 1.23 iter包与range-over-func
问题:Go 1.23的iter包和range over func是什么?
package main
import (
"fmt"
"iter"
)
// 定义一个迭代器函数
func Count(n int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := 0; i < n; i++ {
if !yield(i) {
return
}
}
}
}
func main() {
// 使用range遍历自定义迭代器
for i := range Count(5) {
fmt.Println(i)
}
}
答案:
iter.Seq[T]:表示一个序列的迭代器函数类型
range over func:
- Go 1.23支持
for v := range func(yield func(T)bool)语法 - 允许自定义类型支持range遍历
- 标准库的
slices.All、maps.All等返回iter.Seq
使用场景:
- 自定义集合类型
- 延迟生成数据
- 链式操作
第187题:Go 1.23 unique包
问题:Go 1.23的unique包有什么用途?
答案:
import "unique"
// 创建可比较值的唯一句柄
s1 := unique.Make("hello")
s2 := unique.Make("hello")
s3 := unique.Make("world")
// 相同值的句柄相等
fmt.Println(s1 == s2) // true
fmt.Println(s1 == s3) // false
用途:
- 字符串驻留(interning):相同字符串只存储一份
- 节省内存:大量重复字符串的场景
- 快速比较:句柄比较比字符串比较更快
适用场景:
- 大量重复字符串(如日志标签、HTTP头名)
- 需要去重的集合实现
第188题:Go 1.24 new关键字冲突
问题:Go 1.24有哪些值得注意的变化?
答案:
1. 泛型类型别名:
type MySlice[T any] = []T // Go 1.24+支持
2. go.mod工具依赖:
// 在go.mod中管理工具依赖
require (
golang.org/x/tools/cmd/goimports v0.21.0
)
3. 字符串优化:
- 字符串拼接性能改进
strings.Builder进一步优化
4. 新的约束:
// 清晰的类型约束语法
func Min[T ~int | ~float64](a, b T) T {
if a < b {
return a
}
return b
}
第189题:Go 1.22+循环变量语义变化总结
问题:Go 1.22循环变量语义变化有哪些影响?
答案:
变化:
- for i := 0; i < n; i++:每次迭代创建新的
i变量 - for k, v := range …:每次迭代创建新的
k和v变量 - for i := range N:新增支持整数范围遍历
影响:
// 旧代码(Go 1.21及之前可能有问题)
for _, v := range items {
go func() {
use(v) // 可能全是最后一个值
}()
}
// 新代码(Go 1.22+)没问题,但为了兼容旧版本建议:
for _, v := range items {
go func(v Item) {
use(v)
}(v)
}
建议:
- 新项目直接使用Go 1.22+
- 需要兼容旧版本的库代码,显式传递参数
第190题:Go 1.23+ timers/tickers停止
问题:Go 1.23对timer/ticker有什么改进?
答案:
timer/ticker现在可以正确停止并回收资源:
// Go 1.23之前:Stop()不保证回收
t := time.NewTimer(5 * time.Minute)
if !t.Stop() {
<-t.C // 需要Drain
}
// Go 1.23+:Stop()自动回收,无需Drain
t := time.NewTimer(5 * time.Minute)
t.Stop() // 直接停止,无内存泄漏风险
改进:
Stop()后无需再Drain channel- 减少goroutine泄漏风险
- 简化代码
第191题:Go版本迁移建议
问题:从旧版本Go迁移到新版本有哪些注意事项?
答案:
Go 1.18 → 1.21:
- 使用新的slog替代log
- 使用slices、maps、cmp包
- 泛型代码更稳定
Go 1.21 → 1.22(重点):
- 检查循环变量相关的闭包代码
- 旧代码可能依赖变量复用行为
- 使用
GOEXPERIMENT测试新特性
Go 1.22 → 1.23:
- 可以使用iter包简化代码
- timer/ticker使用更简洁
- 使用unique包优化字符串内存
通用建议:
- 升级前运行完整测试
- 使用
go mod tidy清理依赖 - 关注
go fix自动修复 - 逐步升级,不要跳大版本
📝 总结
本面试题库包含190道题,涵盖:
| 章节 | 题数 | 核心内容 |
|---|---|---|
| 基础语法 | 20 | 变量、控制流、defer、panic/recover |
| 数据类型 | 25 | 数组、切片、map、struct、内存布局 |
| 函数与方法 | 20 | 闭包、方法集、接口实现 |
| 接口与反射 | 20 | 隐式实现、类型断言、反射原理 |
| 并发编程 | 30 | Goroutine、Channel、Sync、Context |
| 内存与GC | 15 | 逃逸分析、垃圾回收、内存分配 |
| 标准库 | 15 | strings、bytes、json、time、http |
| 工程实践 | 15 | 错误处理、测试、项目结构 |
| 源码与高级主题 | 20 | 调度器、map/channel底层、优化 |
| Go版本新特性 | 10 | Go 1.21+、1.22+、1.23+新功能 |
⚠️ 版本差异题标记:
- 第8题、第48题、第52题:Go 1.22+循环变量语义变化
学习建议:
- 从基础开始,循序渐进
- 每道题在Go Playground运行验证
- 理解原理,不要死记硬背
- 结合实际项目加深理解
- 关注Go版本更新,了解新特性
题库整理完成,共计190题,覆盖Go语言面试的全面考察点。