https://python-web-guide.readthedocs.io/zh/latest/go-note/web.html#id1

Golang常见面试题

1.golang中make和new的区别?

  • make用于创建切片、映射和通道,并返回初始化后的(非零)值。
  • new用于分配内存,返回指向类型零值的指针。
特性 new make
返回值 返回指针 *T 返回类型本身 T
适用类型 所有类型 仅slice、map、channel
初始化 零值初始化 分配并初始化内部数据
是否可用 返回的指针指向零值 返回可直接使用的对象

1.1 new(T)

1
2
3
4
5
6
7
8

// 分配0值内存,返回指向类型T的指针 *T
p := new(int) // p是一个*int类型的指针,指向一个int类型的零值
fmt.Println(*p) // 输出0

// 等价于
var i int
p := &i

1.2 make(T, args)

1
2
3
4
5

s := make([]int, 5) // 创建一个长度为5的int切片
m := make(map[string]int) // 创建一个空的map
c := make(chan int, 10) // 创建一个缓冲区大小为10的int通道

1.3 代码对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package main

import "fmt"


func main(){

// new 示例
p1 := new(int)
fmt.Printf("new(int): %T, 值: %v, 地址: %p\n", p1, *p1, p1)
// 输出: new(int): *int, 值: 0, 地址: 0xc000014098

// 2. new 用于 slice(不推荐)
p2 := new([]int)
fmt.Printf("new([]int): %T, 值: %v, 是否nil: %v\n", p2, *p2, *p2 == nil)
// 输出: new([]int): *[]int, 值: [], 是否nil: true
// *p2 是 nil,不能直接使用!

// ============ make 示例 ============

// 3. make 用于 slice
s := make([]int, 5, 10)
fmt.Printf("make([]int): %T, len: %d, cap: %d\n", s, len(s), cap(s))
// 输出: make([]int): []int, len: 5, cap: 10
s[0] = 100 // 可以直接使用

// 4. make 用于 map
m := make(map[string]int)
m["key"] = 42 // 可以直接使用
fmt.Printf("make(map): %T, 值: %v\n", m, m)

// 5. make 用于 channel
ch := make(chan int, 3)
ch <- 1 // 可以直接使用
fmt.Printf("make(chan): %T\n", ch)

}


总结

  • new: 通用分配器,返回指针,零值初始化
  • make: 专用于 slice/map/channel,返回已初始化的可用对象

2、数组和切片的区别?

特性 数组(Array) 切片(Slice)
长度 固定,编译时确定 动态、可变
类型 值类型 引用类型
内存 直接存储元素 包含指针、长度、容量
传递 拷贝整个数组 拷贝切片头(24字节)

2.1 声明和初始化

1
2
3
4
5
6
7
8
9
10
// 数组: 长度是类型的一部分
var arr [5]int // [0 0 0 0 0]
arr2 :=[3]int{1,2,3} // [1 2 3]
arr3 := [...]int{4,5,6} // 自动计算长度

// 切片: 长度可变
var slice1 []int // nil 切片
slice2 := []int{1,2,3} // [1 2 3]
slice3 := make([]int, 5) // [0 0 0 0 0]
slice4 := make([]int, 3, 10) // 长度3,容量10

2.2 类型特性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

// 数组是值类型,赋值会拷贝整个数组
arr := [3] {1,2,3}
arr2 := arr // 完整拷贝
arr2[0] = 100
fmt.Println(arr) // [1 2 3] 不受影响
fmt.Println(arr2) // [100 2 3]

// 切片是引用类型,赋值会拷贝切片头
slice := []int{1,2,3}
slice2 := slice // 只拷贝切片头,底层数据共享
slice2[0] = 100
fmt.Println(slice) // [100 2 3] 受影响
fmt.Println(slice2) // [100 2 3]

2.3 长度和容量

1
2
3
4
5
6
7
8
9
10
11
12
13
14

// 数组: 长度固定
arr := [5]int{1,2,3,4,5}
fmt.Println(len(arr)) // 5
// arr = append(arr, 6) // 编译错误,不能改变长度

// 切片: 长度可变,有容量概念
slice := []int{1, 2, 3}
fmt.Println(len(slice)) //2 - 长度
fmt.Println(cap(slice)) //3 - 容量

slice = append(slice, 4) // 可以追加元素
fmt.Println(slice) // [1 2 3 4]

2.4 函数传递

1
2
3
4
5
6
7
8
9
10
11
12

// 数组传递:值拷贝,影响性能
func modifyArray(arr [10000] int){
arr[0] = 100 // 不会影响原数组
}


// 切片传递:只拷贝切片头(24字节),高效
func modifySlice(slice []int) {
slice[0] = 100 // 会影响原切片的底层数组
}

2.5 切片的底层结构

1
2
3
4
5
6
7
// 切片的内部结构
type slice struct {
array unsafe.Pointer // 指向底层数组的指针
len int // 长度
len int // 容量
}

2.6 使用场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

// 数组:长度固定, 明确已知
func ProcessFixedData(){
var buffer [1024]byte //固定大小的缓冲区
// ....
}

// 切片: 长度不确定,需要动态操作
func processDynamicData(){
data := []string{}
data = append(data, "item1")
data = append(data, "item2")
// ....
}

2.7 数组转切片

1
2
3
4
5
6
7
arr := [5]int{1,2,3,4,5}

slice1 := arr[:] //完整切片
slice2 := arr[1:4] // [2 3 4]
slice3 := arr[:3] // [1 2 3]
slice4 := arr[2:] // [3 4 5]

2.8 最佳实践建议

  1. 优先使用切片: 大多数情况下使用切片更灵活
  2. 明确长度时使用数组: 如固定大小的缓冲区,md5哈希值等
  3. 注意切片陷阱: 切片恭喜底层数组,丢修改要小心
  4. 预分配容量:使用 make([]T, len, cap) 可以减少扩容次数
1
2
3
4
5
// 好的做法:预分配容量
slice := make([]int, 0, 100)
for i := 0; i < 100; i++ {
slice = append(slice, i)
}

3、for range 的时候它的地址会发生变化么?

迭代变量的地址不会变化! 这是go非常重要的特性

3.1 基本现象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

package main

import "fmt"

func main(){
slice := []int{1,2,3,4,5}

for i,v := range slice {
fmt.Printf("i的地址: %p, v的地址: %p, v的值: %d\n", &i, &v, v)
}
}
// 输出:
// i的地址: 0xc000018090, v的地址: 0xc000018098, v的值: 1
// i的地址: 0xc000018090, v的地址: 0xc000018098, v的值: 2
// i的地址: 0xc000018090, v的地址: 0xc000018098, v的值: 3
// i的地址: 0xc000018090, v的地址: 0xc000018098, v的值: 4
// i的地址: 0xc000018090, v的地址: 0xc000018098, v的值: 5

3.2 常见陷阱-指针切片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

// ❌ 错误示例
func main() {
slice := []int{1, 2, 3, 4, 5}
var result []*int

for _, v := range slice {
result = append(result, &v) // 错误!所有指针都指向同一个地址
}

for _, p := range result {
fmt.Print(*p, " ") // 输出: 5 5 5 5 5
}
}

// ✅ 正确做法1:创建临时变量
func main() {
slice := []int{1, 2, 3, 4, 5}
var result []*int

for _, v := range slice {
temp := v // 创建新变量
result = append(result, &temp)
}

for _, p := range result {
fmt.Print(*p, " ") // 输出: 1 2 3 4 5
}
}

// ✅ 正确做法2:使用索引
func main() {
slice := []int{1, 2, 3, 4, 5}
var result []*int

for i := range slice {
result = append(result, &slice[i]) // 直接取原切片元素的地址
}

for _, p := range result {
fmt.Print(*p, " ") // 输出: 1 2 3 4 5
}
}

3.3 闭包陷阱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

// ❌ 错误示例
func main() {
var funcs []func()
slice := []int{1, 2, 3, 4, 5}

for _, v := range slice {
funcs = append(funcs, func() {
fmt.Print(v, " ") // 闭包捕获的是变量v的地址
})
}

for _, f := range funcs {
f() // 输出: 5 5 5 5 5
}
}

// ✅ 正确做法1:传参
func main() {
var funcs []func()
slice := []int{1, 2, 3, 4, 5}

for _, v := range slice {
v := v // 创建新变量(Go 1.22之前需要)
funcs = append(funcs, func() {
fmt.Print(v, " ")
})
}

for _, f := range funcs {
f() // 输出: 1 2 3 4 5
}
}

// ✅ 正确做法2:函数参数
func main() {
var funcs []func()
slice := []int{1, 2, 3, 4, 5}

for _, v := range slice {
funcs = append(funcs, func(val int) func() {
return func() {
fmt.Print(val, " ")
}
}(v)) // 立即执行,传入v的值
}

for _, f := range funcs {
f() // 输出: 1 2 3 4 5
}
}

3.4. goroutine 陷阱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

// ❌ 错误示例
func main() {
slice := []int{1, 2, 3, 4, 5}

for _, v := range slice {
go func() {
fmt.Print(v, " ") // 可能输出: 5 5 5 5 5(不确定)
}()
}

time.Sleep(time.Second)
}

// ✅ 正确做法:传参
func main() {
slice := []int{1, 2, 3, 4, 5}

for _, v := range slice {
go func(val int) {
fmt.Print(val, " ") // 输出顺序不定,但值正确
}(v)
}

time.Sleep(time.Second)
}

3.5. 结构体切片的陷阱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

type Person struct {
Name string
Age int
}

// ❌ 错误示例
func main() {
persons := []Person{
{"Alice", 25},
{"Bob", 30},
{"Charlie", 35},
}

var result []*Person
for _, p := range persons {
result = append(result, &p) // 错误!
}

for _, p := range result {
fmt.Println(p) // 全部输出: &{Charlie 35}
}
}

// ✅ 正确做法
func main() {
persons := []Person{
{"Alice", 25},
{"Bob", 30},
{"Charlie", 35},
}

var result []*Person
for i := range persons {
result = append(result, &persons[i]) // 正确
}

for _, p := range result {
fmt.Println(p) // 正确输出每个人的信息
}
}

3.6. map遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
m := map[string]int{
"a": 1,
"b": 2,
"c": 3,
}

for k, v := range m {
fmt.Printf("k地址: %p, v地址: %p\n", &k, &v)
}
// k和v的地址也是固定的
}

场景 陷阱原因 解决方案
指针切片 所有指针指向同一地址 创建临时变量或使用索引
闭包 捕获变量地址而非值 传参或创建副本
goroutine 并发访问同一变量 传参给goroutine
结构体 取迭代变量地址 使用索引取原切片元素地址

最佳实践:在 for range 循环中需要使用地址或闭包时,务必创建临时变量或使用索引直接访问原数据。

4、go defer,多个 defer 的顺序,defer 在什么时机会修改返回值?(for defer)

defer recover 的问题?(主要是能不能捕获)

4.1 defer基本原理

defer语句将会函数调用推入到一个栈中,函数返回时按照后进先出(LIFO)顺序执行这些推入的函数。

4.2 多个defer的执行顺序

1
2
3
4
5
6
7
func main() {
defer fmt.Println("First defer")
defer fmt.Println("Second defer")
defer fmt.Println("Third defer")
}
// 输出顺序: Third defer, Second defer , First defer

  • for循环中的defer
1
2
3
4
5
6
7
for deferInLoop(){
for i :=0;i<5;i++{
defer fmt.Println(i)
}
}
// 输出顺序:4 3 2 1 0

  • 注意事项
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

// ❌ 错误示例:defer在循环中累积
func readFiles() {
for i := 0; i < 1000; i++ {
f, _ := os.Open(fmt.Sprintf("file%d.txt", i))
defer f.Close() // 所有Close都会累积,文件不会及时关闭
}
} // 函数结束时才执行所有defer

// ✅ 正确做法:使用匿名函数
func readFiles() {
for i := 0; i < 1000; i++ {
func() {
f, _ := os.Open(fmt.Sprintf("file%d.txt", i))
defer f.Close() // 每次循环结束时执行
}()
}
}

4.3 defer修改返回值的时机

关键点: defer在return语句执行之后,函数真正返回之前执行。因此,如果defer函数修改了命名返回值,则会影响最终返回值。
函数返回过程:
1、给返回值赋值
2、执行defer语句 (可以修改返回值)
3、函数返回

4.3.1 命名返回值示例

1
2
3
4
5
6
7
8
func f1(result int) int {
defer func() {
result += 5 // 修改命名返回值
}()
return 10 // 第一步:result = 10
} // 第二步:执行defer,result变为15,第三步:返回15


4.3.2 匿名返回值示例

1
2
3
4
5
6
7
8
9
func f2() int {
var result int
defer func() {
result += 5 // 修改局部变量,不影响返回值
}()
return 10 // 返回值 = result (0), defer修改的是局部变量
}
// 最终返回10

4.3.3 返回指针/引用类型 - defer 可以修改

1
2
3
4
5
6
7
8
9
10
func f3() *int{

result :=0
defer func(){
result +=5 // 修改局部变量,修改指针指向的值
}()
return &result // 返回指针,defer修改了指针指向的值
}
// 最终返回指向5的指针, 返回*int 值为5

4 defer的参数求值时机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

func deferArgs(){
x := 0
defer fmt.Println("defer x:", x) // 参数在defer时求值
x = 1
fmt.Println("x:", x)
}
// 输出:0(不是1)


func deferClosure(){
i :=0
defer func(){
fmt.Println("defer i:",i) // 闭包捕获变量,延迟执行时使用最新值
}()
i ++
}
// 输出:1


避免在 for 循环中直接使用 defer(除非有意为之)
需要修改返回值时,使用命名返回值
defer 参数会立即求值,使用闭包捕获变量

5、uint 类型溢出

5.1 基本概念

在go语言中,uint是一种无符号整数类型,表示非负整数。它的大小依赖于具体的实现,通常是32位或64位。uint类型的取值范围是从0到2^n - 1,其中n是uint的位数(32或64)。

uint类型返回

1
2
3
4
5
uint8:  0 ~ 255 (2^8 - 1)
uint16: 0 ~ 65535 (2^16 - 1)
uint32: 0 ~ 4294967295 (2^32 - 1)
uint64: 0 ~ 18446744073709551615 (2^64 - 1)
uint: 取决于平台(32位或64位)

5.2 溢出行为

当对uint类型进行运算时,如果结果超出了其表示范围,就会发生溢出。溢出的结果会“环绕”回到最小值。例如,对于uint8类型,如果执行以下操作:
  • 上溢,超过最大值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

package main

import "fmt"

func main() {
var a uint8 = 255
fmt.Printf("原始值: %d\n", a)

a = a + 1 // 溢出,变为 0
fmt.Printf("255 + 1 = %d\n", a)

a = 255 + 2 // 溢出,变为 1
fmt.Printf("255 + 2 = %d\n", a)

// 输出:
// 原始值: 255
// 255 + 1 = 0
// 255 + 2 = 1
}
  • 下溢,低于最小值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

func main() {
var b uint8 = 0
fmt.Printf("原始值: %d\n", b)

b = b - 1 // 下溢,变为 255
fmt.Printf("0 - 1 = %d\n", b)

b = 0 - 5 // 下溢,变为 251
fmt.Printf("0 - 5 = %d\n", b)

// 输出:
// 原始值: 0
// 0 - 1 = 255
// 0 - 5 = 251
}

5.3 实际场景

  • 计数器:使用uint类型作为计数器时,需注意溢出问题,避免出现负数。
  • 内存大小:表示内存大小时,使用uint类型可以避免负值,但需防止溢出。
  • 位运算:在进行位运算时,需确保结果在uint类型范围内。
  • 数组索引:使用uint类型作为数组索引时,需确保索引值不会溢出。
  • 循环控制:在循环中使用uint类型作为循环变量时,需注意循环条件,防止溢出导致无限循环。
  • 时间计算:在进行时间计算时,使用uint类型表示时间戳或持续时间时,需防止溢出。
  • 网络编程:在处理网络数据包大小时,使用uint类型表示数据包长度时,需防止溢出。
  • 文件大小:在处理文件大小时,使用uint类型表示文件大小时,需防止溢出。
  • 图像处理:在处理图像像素值时,使用uint类型表示像素值时,需防止溢出。
  • 加密算法:在实现加密算法时,使用uint类型表示密钥或数据块时,需防止溢出。
  • 游戏开发:在游戏开发中,使用uint类型表示分数或生命值时,需防止溢出。

5.4 防止溢出的方法

  • 使用更大类型:如果预期值可能超过当前uint类型的范围,可以使用更大位数的uint类型(如uint64)。
  • 检查边界条件:在进行运算前,检查操作数是否会导致溢出。
  • 使用第三方库:一些第三方库提供了安全的整数类型,可以自动处理溢出问题。

使用 math/bits 包

注意事项
Go不会抛出溢出异常,需要手动检测
编译器不会警告溢出(除非使用工具如go vet)
溢出行为是确定的,遵循二进制补码规则
不同于其他语言,如Python会自动转换为大整数
性能考虑:溢出检查会增加开销,按需使用

6、介绍 rune 类型

6.1 基本概念

在Go语言中,rune是一个内置的数据类型,实际上是int32的别名。它用于表示Unicode代码点,即一个字符在Unicode标准中的唯一标识符。rune类型可以表示所有的Unicode字符,包括ASCII字符和非ASCII字符。

6.2 rune与字符的关系

在Go中,字符是以rune类型表示的。每个rune值对应一个Unicode代码点。例如,字符'a'的rune值是97,因为97是Unicode中'a'的代码点。
1
2

var r rune = 'a' // r的值是97

6.3 rune与字符串的关系

字符串是由一系列rune组成的序列。在Go中,字符串是不可变的字节序列,而rune则表示字符串中的单个字符。可以通过将字符串转换为rune切片来访问字符串中的每个字符。
1
2
3
4

s := "Hello, 世界"
runes := []rune(s) // 将字符串转换为rune切片
fmt.Println(runes) // 输出: [72 101 108 108 111 44 32 19990 30028]

6.4 rune的使用场景

  • 处理Unicode字符:rune类型可以表示所有Unicode字符,适用于需要处理多语言文本的场景。
  • 字符串遍历:使用rune切片可以方便地遍历字符串中的每个字符,尤其是包含非ASCII字符的字符串。
  • 字符操作:可以对rune进行各种字符操作,如转换大小写、判断字符类型等。
  • 字符编码转换:在处理不同字符编码时,rune可以作为中间表示,方便进行转换。
  • 正则表达式:在使用正则表达式时,rune可以用于匹配Unicode字符。
  • 文本处理:在文本处理任务中,rune可以用于分词、拼写检查等操作。
  • 输入法开发:在开发输入法时,rune可以用于表示用户输入的字符。
  • 编译器设计:在编译器中,rune可以用于表示源代码中的字符。

7性能考虑

1.[] rune转换开销

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
"fmt"
"time"
)

func main() {
s := "这是一个很长的中文字符串" + "重复很多次"
longString := ""
for i := 0; i < 1000; i++ {
longString += s
}

// 方式1: 多次转换 (慢)
start := time.Now()
for i := 0; i < 100; i++ {
runes := []rune(longString) // 每次都转换
_ = len(runes)
}
fmt.Printf("多次转换耗时: %v\n", time.Since(start))

// 方式2: 转换一次 (快)
start = time.Now()
runes := []rune(longString) // 只转换一次
for i := 0; i < 100; i++ {
_ = len(runes)
}
fmt.Printf("转换一次耗时: %v\n", time.Since(start))
}

2.使用strings.builder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

package main

import (
"fmt"
"strings"
)

func buildStringWithRunes() string {
var builder strings.Builder
runes := []rune{'H', 'e', 'l', 'l', 'o', '世', '界'}

for _, r := range runes {
builder.WriteRune(r) // 高效写入rune
}
return builder.String()
}

func main() {
result := buildStringWithRunes()
fmt.Println(result) // Hello世界
}

最佳实践
处理非ASCII字符串时使用rune
避免频繁的string和[]rune转换
使用range遍历字符串获取rune
使用utf8包进行底层操作
注意string索引操作的是字节而非字符

7、 golang 中解析 tag 是怎么实现的?反射原理是什么?(问的很少,但是代码中用的多)

7.1 struct tag 是什么?为什么能被解析?

1
2
3
4
type User struct {
ID int `json:"id" db:"user_id"`
Name string `json:"name,omitempty"`
}
  • tag的本质

👉 tag 是 struct 字段在编译期就写入的字符串元数据
👉 tag 存储在类型信息中,可以通过反射访问
它不是运行时生成的猫也不是map,本质就是
Field.Tag == json:"id" db:"user_id"

7.2 Go是怎么解析tag的?

  • 反射入口
1
2
3
t := reflect.TypeOf(User{})
field, ok := t.FieldByName("ID")
fmt.Println(field.Tag) // 输出: `json:"id" db:"user_id"`
  • StructTag的定义
1
2
3
# reflect/type.go

typr StructTag string

也就是数tag就是一个字符换,没有任何的数据结构

  • Get() 是怎么是实现的?
1
value := field.Tag.Get("json") // "id"

关键的结论:
tag不是mao,它只是一个字符串
是运行时用字符串操作来解析的
格式要求非常的严格:key:“value”

面试题解析
Go 的 struct tag 是编译期写入的字符串元数据,运行时通过反射从类型描述信息中读取,
reflect.StructTag 本质只是字符串,通过扫描解析 key:“value” 格式。
反射的底层原理是runtime 保存了完整的类型元信息(rtype),reflect.Type / Value 只是这些元数据的安全封装。

反射 = 运行时读取编译期生成的类型描述信息(runtime type metadata)

8、调用函数传入结构体时,应该传值还是指针?

结论

默认传指针,只有在明确不需要指针才传值

8.1 Go里面只有值传递,那为什么还有指针?

首先明显一件事,Go语言中所有的函数调用都是值传递,这意味着当你将一个变量传递给函数时,函数接收到的是该变量的一个副本。然而,当我们谈论传递结构体时,传递指针实际上是传递了结构体在内存中的地址,这样做有几个重要的好处:

  1. 性能优化:结构体可能包含大量数据,传递整个结构体会涉及到数据的复制,这在性能上是昂贵的。通过传递指针,只需复制地址(通常是4或8字节),大大减少了内存开销和复制时间。
  2. 修改原始数据:当你传递结构体的指针时,函数可以直接修改原始结构体的数据,而不是其副本。这对于需要在函数中更新结构体状态的场景非常有用。
  3. 避免栈溢出:对于非常大的结构体,传递值可能会导致栈空间不足,从而引发栈溢出错误。传递指针可以避免这种风险。
  4. 一致性和习惯用法:在Go社区中,传递指针是处理结构体的常见做法,这有助于保持代码的一致性和可读性。

8.2 什么时候一定要传指针?

8.2.1 函数需要修改结构体(最重要)

1
2
3
4
5
6
7
8
func UpdateUserName(u *User, newName string) {
u.Name = newName // 直接修改原始结构体
}
# 如果你传值的话,修改不会反映到调用者
func UpdateUserNameValue(u User, newName string) {
u.Name = newName // 修改的是副本
}
# 修改必须是指针

8.2.2 结构体较大

1
2
3
type Big struct {
Data [1024 * 1024]byte // 1MB数据
}
  • 传值:每次拷贝1kb
  • 传指针:只拷贝8字节地址

struct ≥ 几十字节 → 优先指针
struct 含大数组 / map / slice / string → 几乎一定指针

8.2.3 需要保持语义一致性

1
2
3
4
5
6
7
8
9
type User struct{}
func (u *User) Save() {}

func Handle(u User) {
u.Save() // ❌ 编译不过
}

#User 的 method set 不包含 *User 的方法
#反过来可以(编译器自动取地址)

8.2.4 避免拷贝导致的“隐藏 Bug”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type Config struct {
Options map[string]string
}

func Modify(c Config) {
c.Options["x"] = "y"
}

# ⚠️ 这会修改原 map
因为:
struct 被拷贝
map 是引用类型
修改 map 内容会影响原对象
👉 这种“半拷贝”行为非常容易误导
✔ 用指针,语义更清晰

8.3 什么时候传值?

8.3.1 结构体很小 + 不可变语义

1
2
3
4
5
6
7
8
9
10
11
type Point struct {
X, Y int
}

func Distance(a, b Point) float64

✔ 小
✔ 不修改
✔ 数学/值对象

👉 传值更清晰

8.3.2 只读场景 + 明确不修改

1
2
3
4
5
6
7

func PrintUser(u User) {
fmt.Println(u.Name)
}
✔ 不修改
✔ 只读
👉 传值更安全

8.3.3 避免 nil(稳定性)

1
2
3
4
5
6
7
func Print(u User)
func Print(u *User) // 可能 panic

如果函数逻辑不能接受 nil

✔ 用值
❌ 不要强迫调用方构造指针

8.3.4 高并发 / 不希望共享状态

1
2
3
4
5
6
7
8
9
10
func Handle(req Request)
func Handle(req *Request)

那就要开始担心:
数据竞争
隐式共享
goroutine 安全

👉 值传递天然隔离

总结

状态型对象 → 指针
值对象 / DTO / 参数对象 → 值

9. silce 遇到过哪些问题?

9.1 append 导致对的数据悄悄被改掉

1
2
3
4
5
6
7
8
9
10
11

a := []int{1, 2, 3}
b := a[:2] // b = [1 2]

b = append(b,100) // b = [1 2 100], 可能修改了 a 底层数组
fmt.Println(a) // 可能输出 [1 2 100]


a 和 b 共享底层数组
b append 时 容量够用
覆盖了 a[2]
  • 修复方法
1
2
3
4
5

b := append([]int(nil), a[:2]...) // 复制一份数据

# 或者
b := slice.Clone(a[:2])

9.2 for range + append 导致死循环/逻辑错误

1
2
3
4
5
6
7
8
9
10
11
12
for _, v := range slice {
if someCondition(v) {
slice = append(slice, newValue) // 修改了 slice 导致死循环或逻辑错误
}
}

- 根本原因
range 在循环开始时就固定了 len
append 修改的是同一个 slice
逻辑极其容易出错

# ❌ 不要在 range 原 slice 时 append 同一个 slice

9.3 sub-slice 导致数据泄漏;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
buf := make([]byte, 10 << 20) // 10MB 大缓冲区
small := buf[:1024] // 1KB 小切片
return small // 返回 small 导致 buf 无法被 GC 回收


# 问题
small 只用 100B
但 引用了 10MB 的底层数组
GC 不会回收
📌 这是 Go 服务内存暴涨的经典原因

# 解决方法
small := make([]byte, 100)
copy(small, buf[:100]) // 复制数据,断开引用

9.4 append 过程中slice失效(指针悬空)

1
2
3
4
5
6
7
8
s := make([]int, 0, 1)
p := &s[0]

s = append(s, 1)
s = append(s, 2) // 触发扩容

fmt.Println(*p) // ❌ 未定义行为

原因

  • append 可能触发 重新分配
  • 原地址失效
  • 指针变成“悬空指针”
    📌 禁止保存 slice 元素指针并 append

9.6 并发读写Slice 导致数据竞态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go func(){
s = append(s, 1)
}()

go func(){
fmt.Println(s[0])
}()


问题
slice 不是线程安全的
append 涉及:
len
cap
底层数组写入
📌 这是 data race + panic 双重风险

9.8 make 参数理解错误

1
2
3
4
5
6
7
8

s := make([]int,5)
a = append(s, 1)
fmt.Println(s) // [0 0 0 0 0 1]


# 正确预分配
s := make([]int, 0, 5)

9.10、删除元素写错

1
2
3
4
5
6
7
8
9
10
11
12

s := append(s[:i], s[i+1:]...) // 删除索引 i 元素

隐藏问题
底层数组仍然持有 s[i]
对大对象 → GC 无法回收

# 安全删除
copy(s[i:], s[i+1:])
s[len(s)-1] = zeroValue
s = s[:len(s)-1]

9.11. 总结

slice 三大核心认知

1️⃣ slice = header + 底层数组
2️⃣ sub-slice 默认共享内存
3️⃣ append 是否扩容决定一切

一句话工程原则
谁创建,谁负责扩容;
谁持有,谁避免共享。

10. go struct 能不能比较?

11. Go 闭包

10. go struct 能不能比较?

10.1 基本规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ✅ 可以比较的情况
type Point struct {
X, Y int
}

type User struct {
ID int
Name string
}

// ❌ 不能比较的情况
type Data struct {
m map[string]int // map不能比较
s []int // slice不能比较
f func() // 函数不能比较
}

10.2 比较规则详解

结构体字段类型 是否可比较 说明
基本类型(int, string等) 直接比较值
指针 比较指针地址
数组 逐元素比较
结构体 递归比较字段
slice 引用类型,不可比较
map 引用类型,不可比较
channel 比较channel地址
interface 比较类型和值
函数 函数不可比较

10.3 实际示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import "fmt"

// ✅ 完全可比较的结构体
type Person struct {
Name string
Age int
}

// ❌ 包含不可比较字段的结构体
type Container struct {
Data []int // slice不可比较
}

func main() {
p1 := Person{"Alice", 25}
p2 := Person{"Alice", 25}
p3 := Person{"Bob", 30}

fmt.Println(p1 == p2) // true
fmt.Println(p1 == p3) // false

// c1 := Container{[]int{1, 2}}
// c2 := Container{[]int{1, 2}}
// fmt.Println(c1 == c2) // 编译错误!
}

10.4 特殊情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 空结构体比较
type Empty struct{}

func main() {
var e1, e2 Empty
fmt.Println(e1 == e2) // true,所有空结构体都相等
}

// 包含nil指针
type Node struct {
value int
next *Node
}

func main() {
n1 := Node{1, nil}
n2 := Node{1, nil}
fmt.Println(n1 == n2) // true
}

10.5 最佳实践

  1. 设计可比较结构体:避免slice、map、func字段
  2. 使用自定义比较:对于复杂结构体实现Equal()方法
  3. 注意nil值:指针类型的nil值比较
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 自定义比较方法
type ComplexData struct {
items []int
}

func (c *ComplexData) Equal(other *ComplexData) bool {
if len(c.items) != len(other.items) {
return false
}
for i, v := range c.items {
if v != other.items[i] {
return false
}
}
return true
}

11. Go 闭包

11.1 什么是闭包?

闭包是一个函数值,它引用了函数体之外的变量。该函数可以访问并赋予其引用的变量的值,换句话说,这些变量被"封闭"在该函数中。

11.2 闭包的基本特性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func main() {
// 基本闭包示例
x := 10
f := func() int {
return x + 1 // 捕获外部变量x
}

fmt.Println(f()) // 11
x = 20
fmt.Println(f()) // 21 - 闭包引用的是变量x本身
}

11.3 闭包的常见陷阱

11.3.1 循环变量陷阱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// ❌ 错误示例
func main() {
var funcs []func()
for i := 0; i < 3; i++ {
funcs = append(funcs, func() {
fmt.Println(i) // 所有函数都输出3
})
}

for _, f := range funcs {
f() // 输出: 3 3 3
}
}

// ✅ 正确做法1:传参
func main() {
var funcs []func()
for i := 0; i < 3; i++ {
funcs = append(funcs, func(val int) func() {
return func() {
fmt.Println(val)
}
}(i)) // 立即执行,传入i的值
}

for _, f := range funcs {
f() // 输出: 0 1 2
}
}

// ✅ 正确做法2:创建局部变量
func main() {
var funcs []func()
for i := 0; i < 3; i++ {
i := i // Go 1.22之前需要这样
funcs = append(funcs, func() {
fmt.Println(i)
})
}

for _, f := range funcs {
f() // 输出: 0 1 2
}
}

11.3.2 goroutine闭包陷阱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// ❌ 错误示例
func main() {
slice := []int{1, 2, 3, 4, 5}

for _, v := range slice {
go func() {
fmt.Println(v) // 可能输出多个5
}()
}

time.Sleep(time.Second)
}

// ✅ 正确做法:传参
func main() {
slice := []int{1, 2, 3, 4, 5}

for _, v := range slice {
go func(val int) {
fmt.Println(val) // 输出1 2 3 4 5(顺序不定)
}(v)
}

time.Sleep(time.Second)
}

11.4 闭包的实际应用

11.4.1 函数工厂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 加法器工厂
func makeAdder(x int) func(int) int {
return func(y int) int {
return x + y
}
}

func main() {
add5 := makeAdder(5)
add10 := makeAdder(10)

fmt.Println(add5(3)) // 8
fmt.Println(add10(3)) // 13
}

11.4.2 装饰器模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 日志装饰器
func withLogging(f func(int)) func(int) {
return func(x int) {
fmt.Printf("调用函数,参数: %d\n", x)
f(x)
fmt.Println("函数调用完成")
}
}

func process(x int) {
fmt.Printf("处理: %d\n", x * 2)
}

func main() {
decorated := withLogging(process)
decorated(5)
}

11.4.3 延迟计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 延迟计算斐波那契数列
func fibonacci() func() int {
a, b := 0, 1
return func() int {
result := a
a, b = b, a+b
return result
}
}

func main() {
fib := fibonacci()

for i := 0; i < 10; i++ {
fmt.Println(fib())
}
}

11.5 闭包与内存管理

1
2
3
4
5
6
7
8
9
10
11
12
// 闭包可能导致内存泄漏
func leakyFunction() func() {
data := make([]byte, 1024*1024) // 1MB数据
return func() {
fmt.Println(len(data)) // 闭包持有data引用
}
}

func main() {
f := leakyFunction()
f() // data不会被GC回收
}

11.6 闭包的最佳实践

  1. 避免循环变量陷阱:使用传参或局部变量
  2. 注意内存泄漏:闭包会延长对象生命周期
  3. 合理使用闭包:在需要状态保持时使用
  4. 理解闭包本质:闭包=函数+引用的环境

12. Context

12.1、context 结构是什么样的?

12.1.1 Context接口定义

1
2
3
4
5
6
7
8
9
10
11
12
13
type Context interface {
// Deadline 返回context应该被取消的时间
Deadline() (deadline time.Time, ok bool)

// Done 返回一个channel,当context被取消时关闭
Done() <-chan struct{}

// Err 返回context被取消的原因
Err() error

// Value 返回context中与key关联的值
Value(key interface{}) interface{}
}

12.1.2 Context的继承结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 空context - 所有context的根
type emptyCtx int

func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}

func (*emptyCtx) Done() <-chan struct{} {
return nil
}

func (*emptyCtx) Err() error {
return nil
}

func (*emptyCtx) Value(key interface{}) interface{} {
return nil
}

// 可取消的context
type cancelCtx struct {
Context
done chan struct{}
err error
}

// 带超时的context
type timerCtx struct {
cancelCtx
timer *time.Timer
}

// 带值的context
type valueCtx struct {
Context
key, val interface{}
}

12.2、context 使用场景和用途?(基本必问)

12.2.1 主要使用场景

场景 用途 示例
HTTP请求 传递请求ID、用户信息 req.Context()
数据库操作 设置查询超时 context.WithTimeout()
微服务调用 传递链路追踪信息 context.WithValue()
后台任务 优雅关闭 context.WithCancel()

12.2.2 实际应用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// 1. HTTP请求处理
func handleRequest(w http.ResponseWriter, r *http.Request) {
// 从请求获取context
ctx := r.Context()

// 设置超时
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()

// 传递给数据库操作
result, err := database.Query(ctx, "SELECT * FROM users")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

json.NewEncoder(w).Encode(result)
}

// 2. 微服务调用
func callUserService(ctx context.Context, userID string) (*User, error) {
// 添加链路追踪ID
traceID := ctx.Value("traceID").(string)
ctx = context.WithValue(ctx, "service", "user-service")

// 设置调用超时
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()

// 发起HTTP调用
req, _ := http.NewRequest("GET", fmt.Sprintf("http://user-service/users/%s", userID), nil)
req = req.WithContext(ctx)

resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

var user User
json.NewDecoder(resp.Body).Decode(&user)
return &user, nil
}

// 3. 后台任务
func startBackgroundTask(ctx context.Context) {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()

for {
select {
case <-ctx.Done():
// 收到取消信号,优雅退出
fmt.Println("后台任务被取消:", ctx.Err())
return
case <-ticker.C:
// 执行任务
doWork()
}
}
}

// 4. 数据库操作
func GetUserByID(ctx context.Context, db *sql.DB, id int) (*User, error) {
query := "SELECT id, name, email FROM users WHERE id = ?"

var user User
err := db.QueryRowContext(ctx, query, id).Scan(
&user.ID, &user.Name, &user.Email,
)

if err != nil {
return nil, err
}

return &user, nil
}

12.2.3 Context的最佳实践

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 1. Context作为第一个参数
func processData(ctx context.Context, data []byte) error {
// 正确:context作为第一个参数
return nil
}

// 2. 不要在struct中存储context
// ❌ 错误
type Service struct {
ctx context.Context
}

// ✅ 正确
type Service struct{}

func (s *Service) Process(ctx context.Context, data []byte) error {
return nil
}

// 3. 使用context传递请求范围的数据
type contextKey string

const userIDKey contextKey = "userID"

func WithUserID(ctx context.Context, userID string) context.Context {
return context.WithValue(ctx, userIDKey, userID)
}

func GetUserID(ctx context.Context) (string, bool) {
userID, ok := ctx.Value(userIDKey).(string)
return userID, ok
}

// 4. 正确处理context取消
func longRunningOperation(ctx context.Context) error {
done := make(chan error, 1)

go func() {
// 执行耗时操作
done <- doExpensiveWork()
}()

select {
case err := <-done:
return err
case <-ctx.Done():
// 操作被取消,清理资源
cleanup()
return ctx.Err()
}
}

12.2.4 Context使用注意事项

  1. 不要传递nil context:使用context.Background()context.TODO()
  2. context是immutable的:总是返回新的context
  3. 及时调用cancel:使用defer确保cancel被调用
  4. 不要过度使用WithValue:只传递请求范围的数据
  5. 理解context的传播:context会自动传播到goroutine
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ✅ 正确的context使用模式
func handleRequest(ctx context.Context, req Request) error {
// 创建子context
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()

// 传递给下游
result, err := processRequest(ctx, req)
if err != nil {
return err
}

return nil
}

13. Channel相关

13.1、channel 是纯线程安全?锁用在什么地方?

Channel 在 Go 中是并发安全的,但不是绝对安全的。它的实现依赖于以下机制:

13.1.1 内部锁机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 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 // 保护 hchan 的锁
}

13.1.2 安全操作与不安全操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ✅ 安全操作
func safeChannel() {
ch := make(chan int, 10)

// 发送和接收操作是原子的
ch <- 1
<-ch

// 关闭操作是安全的
close(ch)
}

// ❌ 不安全操作
func unsafeChannel() {
ch := make(chan int, 10)

// 对关闭后的 channel 发送数据会导致 panic
close(ch)
ch <- 1 // 会 panic

// 关闭 nil channel 会导致 panic
var chNil chan int
close(chNil) // 会 panic
}

13.1.3 适用场景

场景 安全性保证
单一发送者 & 单一接收者 非常安全
多个发送者 & 单一接收者 需要使用 sync.Once 关闭,或使用 context 取消
多个发送者 & 多个接收者 需要使用外部同步机制

13.2、go channel 的底层实现原理(数据结构)

13.2.1 底层实现数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// 等待队列
type waitq struct {
first *sudog
last *sudog
}

// goroutine在等待队列中的表示
type sudog struct {
g *g
next *sudog
prev *sudog
elem unsafe.Pointer
isSelect bool
c *hchan
}

// channel的核心操作流程
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
// 1. 对hchan.lock加锁

// 2. 检查是否已关闭,已关闭则解锁并panic

// 3. 检查是否有等待的接收者
if c.recvq.first != nil {
// 直接发送给接收者,解锁
send(c, sg, ep, func() { unlock(&c.lock) })
return true
}

// 4. 检查缓冲区是否已满
if c.dataqsiz > 0 && c.qcount < c.dataqsiz {
// 发送到缓冲区,解锁
qp := chanbuf(c, c.sendx)
typedmemmove(c.elemtype, qp, ep)
c.sendx++
if c.sendx == c.dataqsiz {
c.sendx = 0
}
c.qcount++
unlock(&c.lock)
return true
}

// 5. 检查是否阻塞
if !block {
unlock(&c.lock)
return false
}

// 6. 阻塞当前goroutine
gp := getg()
mysg := acquireSudog()
mysg.elem = ep
mysg.c = c
gp.waiting = mysg
c.sendq.enqueue(mysg)
goparkunlock(&c.lock, waitReasonChanSend, traceEvGoBlockSend, 3)

return true
}

13.2.2 channel类型性能对比

类型 发送操作(ns/op) 接收操作(ns/op) 适用场景
无缓冲 channel 20-30 20-30 同步通信
有缓冲 channel(10) 10-15 10-15 异步通信/排队
无缓冲 channel(多个goroutine) 50-100 50-100 竞争场景

13.3、nil、关闭的 channel、有数据的 channel,再进行读、写、关闭会怎么样?(各类变种题型)

13.3.1 各类操作汇总表

操作类型 nil channel 已关闭 channel 有数据 channel 无数据 channel
发送数据 永久阻塞 panic 正常发送或阻塞 阻塞或写入
接收数据 永久阻塞 返回零值 + false 正常接收 阻塞
关闭操作 panic panic 正常关闭 正常关闭

13.3.2 详细代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// nil channel 操作
func nilChannel() {
var ch chan int // nil

// 发送操作:永久阻塞
go func() {
ch <- 1
fmt.Println("send successful") // 永远不会执行
}()

// 接收操作:永久阻塞
go func() {
<-ch
fmt.Println("receive successful") // 永远不会执行
}()

// 关闭操作:panic
// close(ch) // 会导致程序崩溃
}

// 已关闭 channel 操作
func closedChannel() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
close(ch)

// 读取操作:正常读取剩余数据
v1, ok1 := <-ch
v2, ok2 := <-ch
v3, ok3 := <-ch
fmt.Println(v1, ok1) // 1, true
fmt.Println(v2, ok2) // 2, true
fmt.Println(v3, ok3) // 0, false

// 发送操作:panic
// ch <- 3 // 会panic
}

13.3.3 常见面试题变种

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 面试题1: channel close后读的问题
func channelReadAfterClose() {
ch := make(chan int)
close(ch)

v, ok := <-ch
fmt.Printf("v=%v, ok=%v\n", v, ok) // 0, false
}

// 面试题2: 向为nil的channel发送数据会怎么样?
func sendToNilChannel() {
var ch chan int
// ch <- 1 // 永久阻塞,会导致程序死锁
}

// 面试题3: 向关闭的channel发送数据会怎么样?
func sendToClosedChannel() {
ch := make(chan int)
close(ch)
// ch <- 1 // 会panic: send on closed channel
}

13.4、向 channel 发送数据和从 channel 读数据的流程是什么样的?

13.4.1 发送操作流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// chansend 函数简化版
func sendFlow(c *hchan, value interface{}) bool {
// 1. 对 channel 加锁
lock()

// 2. 检查 channel 是否已关闭
if c.closed > 0 {
panic("send on closed channel")
}

// 3. 如果有 goroutine 在等待接收,直接发送
if !c.recvq.empty() {
sg := c.recvq.dequeue()
// 将数据直接发送给接收者的栈空间
*((*int)(sg.elem)) = value
// 唤醒接收者
goready(sg.g, 3)
unlock()
return true
}

// 4. 如果缓冲区未满,写入缓冲区
if c.qcount < c.dataqsiz {
// 计算写入位置
pos := c.sendx
c.buf[pos] = value
c.sendx++
c.qcount++
unlock()
return true
}

// 5. 发送阻塞,将当前 goroutine 加入发送队列
sg := acquireSudog()
sg.elem = &value
sg.c = c
c.sendq.enqueue(sg)
// 阻塞当前 goroutine
gopark()

return true
}

13.4.2 接收操作流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// chanrecv 函数简化版
func receiveFlow(c *hchan) (interface{}, bool) {
// 1. 对 channel 加锁
lock()

// 2. 如果有 goroutine 在等待发送,直接接收
if !c.sendq.empty() {
sg := c.sendq.dequeue()
value := *(int*)(sg.elem)
// 唤醒发送者
goready(sg.g, 3)
unlock()
return value, true
}

// 3. 如果缓冲区有数据,直接读取
if c.qcount > 0 {
value := c.buf[c.recvx]
c.recvx++
c.qcount--
unlock()
return value, true
}

// 4. 接收阻塞,将当前 goroutine 加入接收队列
sg := acquireSudog()
var value int
sg.elem = &value
sg.c = c
c.recvq.enqueue(sg)
gopark()

return value, true
}

14. Map相关

14.1、map 使用注意的点,并发安全?

14.1.1 基本特性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 创建map的不同方式
func createMap() {
// 方法1: 声明nil map
var m1 map[string]int // nil,不能直接使用

// 方法2: 初始化空map
m2 := make(map[string]int) // 空map,可以使用

// 方法3: 带初始化容量
m3 := make(map[string]int, 100) // 预分配100的容量

// 方法4: 字面量初始化
m4 := map[string]int{"a": 1, "b": 2} // 直接初始化
}

// map使用的基本操作
func mapOperations() {
m := make(map[string]int)

// 插入/更新
m["a"] = 1
m["b"] = 2

// 读取
v1 := m["a"]
fmt.Println(v1) // 1

// 删除
delete(m, "a")

// 判断键是否存在
v2, ok := m["b"]
fmt.Printf("v: %v, ok: %v\n", v2, ok)
}

14.1.2 并发安全问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ❌ 错误示例:并发读写map
func concurrentMapAccess() {
m := make(map[string]int)

// 启动多个goroutine进行读写操作
for i := 0; i < 10; i++ {
go func(key string, value int) {
// 写入
m[key] = value

// 读取
fmt.Println(m["a"])
}(fmt.Sprintf("key%d", i), i)
}

time.Sleep(1 * time.Second)
}

14.1.3 解决并发安全的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// ✅ 方法1: 使用sync.RWMutex(推荐)
func safeMap1() {
type SafeMap struct {
sync.RWMutex
data map[string]int
}

sm := SafeMap{
data: make(map[string]int),
}

for i := 0; i < 10; i++ {
go func(key string, value int) {
// 写操作,使用互斥锁
sm.Lock()
sm.data[key] = value
sm.Unlock()

// 读操作,使用读写锁
sm.RLock()
fmt.Println(sm.data["a"])
sm.RUnlock()
}(fmt.Sprintf("key%d", i), i)
}

time.Sleep(1 * time.Second)
}

// ✅ 方法2: 使用sync.Map(Go 1.9+)
func safeMap2() {
var sm sync.Map

for i := 0; i < 10; i++ {
go func(key string, value int) {
// 写入
sm.Store(key, value)

// 读取
v, ok := sm.Load("a")
if ok {
fmt.Println(v)
}
}(fmt.Sprintf("key%d", i), i)
}

time.Sleep(1 * time.Second)
}

// ✅ 方法3: 使用channel序列化
func safeMap3() {
type Operation struct {
key string
value int
op string // "set" or "get"
result chan int
}

// 操作通道
opsCh := make(chan Operation, 100)

// 内部 goroutine 处理操作
go func() {
m := make(map[string]int)
for op := range opsCh {
switch op.op {
case "set":
m[op.key] = op.value
case "get":
op.result <- m[op.key]
}
}
}()

for i := 0; i < 10; i++ {
go func(key string, value int) {
opsCh <- Operation{key, value, "set", nil}
}(fmt.Sprintf("key%d", i), i)
}

time.Sleep(1 * time.Second)
}

14.2、map 循环是有序的还是无序的?

14.2.1 无序性演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 演示map遍历的无序性
func mapUnordered() {
m := map[string]int{"a":1, "b":2, "c":3, "d":4, "e":5}

fmt.Println("第一次遍历:")
for k, v := range m {
fmt.Printf("k:%s, v:%d\n", k, v)
}

fmt.Println("\n第二次遍历:")
for k, v := range m {
fmt.Printf("k:%s, v:%d\n", k, v)
}

// 输出通常会不同,因为map的遍历顺序是随机的
}

14.2.2 确保有序遍历的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 获取有序键的方法
func mapSortedKeys(m map[string]int) []string {
// 获取所有键
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}

// 排序
sort.Strings(keys)

return keys
}

// 有序遍历map
func sortedMapIteration() {
m := map[string]int{"a":1, "b":2, "c":3, "d":4, "e":5}

// 获取排序后的键
sortedKeys := mapSortedKeys(m)

// 按顺序遍历
for _, k := range sortedKeys {
fmt.Printf("k:%s, v:%d\n", k, m[k])
}
}

14.3、map 中删除一个 key,它的内存会释放么?

14.3.1 内存释放机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 演示map的内存释放
func mapMemory() {
// 创建一个大map
m := make(map[int]*struct{}, 1000000)
for i := 0; i < 1000000; i++ {
m[i] = &struct{}{}
}

fmt.Println("创建map后内存使用:")
printMemUsage() // 打印当前内存使用

// 删除所有元素
for i := 0; i < 1000000; i++ {
delete(m, i)
}

fmt.Println("删除所有元素后内存使用:")
printMemUsage() // 内存并没有立即释放

// 手动触发GC
runtime.GC()
fmt.Println("手动GC后内存使用:")
printMemUsage() // 内存释放了
}

14.3.2 内存释放策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// map的负载因子
// 当负载因子 < 6.5 时,删除不会导致缩容
// 只有当负载因子 < 2.5 且有大量删除时,才会缩容
func mapResizing() {
// 负载因子 = 元素数量 / 桶数量
// 扩容阈值: 6.5,缩容阈值: 2.5

m := make(map[int]int, 100) // 初始4个桶

// 插入到负载因子接近6.5
for i := 0; i < 26; i++ {
m[i] = i // 26个元素,4个桶,负载因子6.5
}

// 删除元素到负载因子 < 2.5
for i := 0; i < 18; i++ {
delete(m, i) // 8个元素,4个桶,负载因子2
}

// 此时执行GC会触发缩容到2个桶
runtime.GC()
}

14.4、怎么处理对 map 进行并发访问?有没有其他方案?区别是什么?

14.4.1 各种方案对比

方案 实现方式 读性能 写性能 内存开销 适用场景
sync.Mutex 互斥锁 一般 一般 读写频率均匀
sync.RWMutex 读写锁 一般 读多写少
sync.Map 原子操作+分片 高频读写
channel 序列化 发送到单goroutine处理 一般 一般 需要复杂操作

14.4.2 方案选择策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 根据场景选择合适的并发安全方案
func chooseConcurrentMap() {
// 场景1: 读多写少 (如配置)
configMap := struct {
sync.RWMutex
data map[string]string
}{}

// 场景2: 高频读写 (如缓存)
cacheMap := sync.Map{}

// 场景3: 需要复杂计算的操作
type Operation struct {
key string
value string
op string
result chan string
}
opsCh := make(chan Operation, 100)
go func() {
m := make(map[string]string)
for op := range opsCh {
switch op.op {
case "get":
op.result <- m[op.key]
case "set":
m[op.key] = op.value
}
}
}()
}

14.5、nil map 和空 map 有何不同?

14.5.1 基本区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func mapNilVsEmpty() {
var m1 map[string]int // nil map
m2 := make(map[string]int) // 空 map

// 比较nil
fmt.Println("m1 == nil:", m1 == nil) // true
fmt.Println("m2 == nil:", m2 == nil) // false

// 比较长度
fmt.Println("len(m1):", len(m1)) // 0
fmt.Println("len(m2):", len(m2)) // 0

// 删除操作
delete(m1, "key") // 允许
delete(m2, "key") // 允许

// 插入操作
// m1["key"] = 1 // 会导致 panic

m2["key"] = 1 // 正常
}

14.5.2 安全操作 nil map 的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func safeNilMap() {
var m map[string]int

// 正确做法:检查nil并初始化
if m == nil {
m = make(map[string]int)
}

m["key"] = 1

// 或者使用指针避免nil检查
type MapPtr *map[string]int

var mp MapPtr = new(map[string]int)
if *mp == nil {
*mp = make(map[string]int)
}

(*mp)["key"] = 1
}

14.6、map 的数据结构是什么?是怎么实现扩容的?

14.6.1 底层实现结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// map的bucket结构
type bmap struct {
tophash [8]uint8 // 哈希值的高8位用于快速查找
// 实际数据存储区,包含8个key-value对,key顺序存储
}

// map的核心结构
type hmap struct {
count int // 元素数量
flags uint8 // 状态标志
B uint8 // log2(桶数量)
noverflow uint16 // 溢出桶数量
hash0 uint32 // 哈希种子

buckets unsafe.Pointer // 指向桶数组的指针
oldbuckets unsafe.Pointer // 扩容时的旧桶
nevacuate uintptr // 扩容进度

extra *mapextra // 溢出桶信息
}

14.6.2 扩容机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 扩容条件
func shouldGrow(m *hmap) bool {
loadFactor := float64(m.count) / (float64(1) << m.B)
// 负载因子超过6.5或有大量溢出桶
return loadFactor > 6.5 || (m.noverflow > uint16(m.B) && m.B > 15)
}

// 扩容过程
func growWork(t *maptype, h *hmap, bucket uintptr) {
// 如果oldbuckets存在,表明正在扩容
if h.oldbuckets != nil {
// 迁移一个桶
evacuate(t, h, bucket&h.oldbucketmask())
// 清空oldbuckets
if h.nevacuate == bucket {
h.nevacuate++
}
}
}

// 具体迁移逻辑
func evacuate(t *maptype, h *hmap, oldbucket uintptr) {
// 1. 查找旧桶的所有元素
// 2. 计算新的位置
// 3. 迁移到新桶
// 4. 处理溢出桶
}

14.7、map 取一个 key,然后修改这个值,原 map 数据的值会不会变化

14.7.1 基本规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func mapValueMutation() {
// 情况1: 基本类型
m1 := map[string]int{"a": 1}
v := m1["a"]
v = 100
fmt.Println(m1["a"]) // 1,没有变化

// 情况2: 指针类型
m2 := map[string]*int{"a": new(int)}
*m2["a"] = 1
v2 := m2["a"]
*v2 = 100
fmt.Println(*m2["a"]) // 100,变化了

// 情况3: 结构体类型(不可直接修改)
type Point struct { X, Y int }
m3 := map[string]Point{"a": {X: 1, Y: 2}}
// p := m3["a"]
// p.X = 100 // 编译错误!

// 情况4: 结构体指针
m4 := map[string]*Point{"a": {X: 1, Y: 2}}
m4["a"].X = 100 // 直接修改,会变化
}

15. GMP相关

15.1、什么是 GMP?(必问)调度流程是什么样的?(对流程熟悉,要求更高,问的较多)

15.1.1 基本概念

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// GMP模型的三个核心组件
type G struct {
goid int64 // goroutine ID
stack stack // 堆栈信息
sched gobuf // 调度信息
gopc uintptr // 创建位置
startpc uintptr // 函数入口
param unsafe.Pointer // 传递参数
atomicstatus uint32 // 状态
}

type M struct {
id int32 // 线程ID
g0 *g // 调度goroutine
gsignal *g // 信号处理goroutine
tls [6]uintptr // 线程本地存储
mstartfn func() // 启动函数
curg *g // 当前正在执行的goroutine
}

type P struct {
id int32 // 逻辑 processor ID
status uint32 // 状态
runqhead uint32 // 本地队列头
runqtail uint32 // 本地队列尾
runq [256]guintptr // 本地goroutine队列
syscalltick uint32 // 系统调用计数
}

15.1.2 调度流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 调度器启动
func runtime·schedinit() {
// 初始化调度器状态
// 创建P并启动M
// 设置调度器参数
}

// goroutine调度流程
func schedule() {
// 1. 检查本地队列
gp, inheritTime := findRunnable()

// 2. 执行goroutine
execute(gp, inheritTime)
}

// 执行goroutine
func execute(gp *g, inheritTime bool) {
_g_ := getg()
_g_.m.curg = gp
gp.m = _g_.m

// 执行前处理
gogo(&gp.sched)

// 执行后处理
gp.m = nil
_g_.m.curg = nil
}

15.1.3 调度流程详解

阶段 主要操作 时间点
初始化 初始化P队列、创建系统线程 程序启动
调度准备 查找可运行goroutine、抢占处理 每次调度时
执行 设置上下文、执行函数 goroutine切换时
恢复 保存状态、继续调度 goroutine阻塞或结束

15.2、进程、线程、协程有什么区别?

15.2.1 基本概念对比

特性 进程 线程 协程
拥有资源 有独立地址空间 共享进程资源 共享进程资源
调度 系统调度 系统调度 用户调度
切换开销 大(毫秒级) 中(微秒级) 小(纳秒级)
并发数量 有限(百级) 有限(千级) 大量(百万级)
通信方式 IPC(管道、共享内存等) 共享内存 通道或消息传递

15.2.2 Go协程的优势

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 演示goroutine的高效性
func goroutineEfficiency() {
// 启动100万个goroutine
var wg sync.WaitGroup
ch := make(chan int, 1000000)

for i := 0; i < 1000000; i++ {
wg.Add(1)
go func(x int) {
defer wg.Done()
ch <- x * x
}(i)
}

go func() {
wg.Wait()
close(ch)
}()

sum := 0
for x := range ch {
sum += x
}

fmt.Printf("1到999999平方和: %d\n", sum)
}

15.3、抢占式调度是如何抢占的?

15.3.1 抢占触发条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 栈增长检查触发抢占
func morestack() {
// 检测到栈增长时,触发抢占
// 将goroutine状态标记为需要抢占
// 当goroutine下一次进入调度点时会被抢占
}

// 系统调用返回触发调度
func entersyscallblock() {
// 系统调用阻塞后,释放P
// 调度器可以将P分配给其他M
}

// 主动调用触发调度
func Gosched() {
// 主动放弃CPU,重新调度
}

15.3.2 抢占实现机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 定时器触发抢占
func sysmon() {
// 系统监控goroutine,定期检查长时间运行的goroutine
for {
now := nanotime()

// 检查是否有长时间运行的goroutine(> 10ms)
for _, p := range allp {
if gp := p.curg; gp != nil {
if now-gp.schedwhen > 10*1e9 {
// 标记为需要抢占
casgstatus(gp, _Grunning, _Gpreempted)
}
}
}

osyield()
}
}

// 执行前检查是否需要抢占
func execute(gp *g, inheritTime bool) {
_g_ := getg()
_g_.m.curg = gp
gp.m = _g_.m

// 执行前检查是否需要抢占
if gp.atomicstatus == _Gpreempted {
// 进入调度流程
goexit1()
}
}

15.4、M 和 P 的数量问题?

15.4.1 默认值与配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func printConfig() {
// GOMAXPROCS是P的数量,默认是CPU核数
fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0))

// 正在运行的P数量
fmt.Println("NumCPU:", runtime.NumCPU())

// 正在运行的M数量
fmt.Println("NumGoroutine:", runtime.NumGoroutine())
}

// 设置GOMAXPROCS
func setGOMAXPROCS() {
// 设置为CPU核数的2倍
runtime.GOMAXPROCS(runtime.NumCPU() * 2)

fmt.Println("New GOMAXPROCS:", runtime.GOMAXPROCS(0))
}

15.4.2 最佳实践

1
2
3
4
5
6
7
8
9
10
11
// 根据任务类型选择合适的GOMAXPROCS值
func chooseGOMAXPROCS() {
// 计算密集型任务:设置为CPU核数
runtime.GOMAXPROCS(runtime.NumCPU())

// IO密集型任务:可以设置更大的值
// runtime.GOMAXPROCS(runtime.NumCPU() * 2)

// 大量并发请求场景:设置更大的值
// runtime.GOMAXPROCS(runtime.NumCPU() * 4)
}

15.5、协程怎么退出?

15.5.1 协程退出的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 方式1: 正常执行完毕
func normalExit() {
go func() {
fmt.Println("goroutine 正在执行")
// 函数执行完毕后自动退出
}()
}

// 方式2: 返回值方式
func returnExit() {
go func() int {
fmt.Println("goroutine 正在执行")
return 100 // 返回后退出
}()
}

// 方式3: 使用runtime.Goexit()
func goexitExit() {
go func() {
defer fmt.Println("defer 会执行")
runtime.Goexit() // 直接退出
fmt.Println("不会执行")
}()
}

// 方式4: 主goroutine结束
func mainExit() {
go func() {
time.Sleep(1 * time.Second)
fmt.Println("不会执行")
}()

fmt.Println("main goroutine 结束")
}

15.5.2 处理协程的优雅退出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 使用context优雅退出
func gracefulExitWithContext() {
ctx, cancel := context.WithCancel(context.Background())

go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("收到取消信号,退出")
return
default:
fmt.Println("正在工作...")
time.Sleep(500 * time.Millisecond)
}
}
}(ctx)

time.Sleep(2 * time.Second)
cancel()
time.Sleep(1 * time.Second)
}

// 使用channel优雅退出
func gracefulExitWithChannel() {
quitCh := make(chan struct{})

go func(quit chan struct{}) {
for {
select {
case <-quit:
fmt.Println("收到退出信号")
return
default:
fmt.Println("正在工作...")
time.Sleep(500 * time.Millisecond)
}
}
}(quitCh)

time.Sleep(2 * time.Second)
quitCh <- struct{}{}
time.Sleep(1 * time.Second)
}

15.6、map 如何顺序读取?

15.6.1 标准库方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 通用顺序读取map的方法
func iterateMapOrderly() {
m := map[string]int{"c":1, "a":2, "b":3}

// 获取所有键并排序
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}

// 排序
sort.Strings(keys)

// 按顺序访问
for _, k := range keys {
fmt.Printf("k: %s, v: %d\n", k, m[k])
}
}

// 按值排序
func iterateMapByValue() {
m := map[string]int{"c":1, "a":2, "b":3}

type kv struct {
Key string
Value int
}

var pairs []kv
for k, v := range m {
pairs = append(pairs, kv{k, v})
}

sort.Slice(pairs, func(i, j int) bool {
return pairs[i].Value < pairs[j].Value // 按值升序
// return pairs[i].Value > pairs[j].Value // 按值降序
})

for _, p := range pairs {
fmt.Printf("k: %s, v: %d\n", p.Key, p.Value)
}
}

15.6.2 使用结构体封装的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// 使用结构体封装的有序map
type OrderedMap struct {
sync.RWMutex
keys []string
data map[string]interface{}
}

func NewOrderedMap() *OrderedMap {
return &OrderedMap{
data: make(map[string]interface{}),
}
}

func (om *OrderedMap) Set(key string, value interface{}) {
om.Lock()
defer om.Unlock()

if _, exists := om.data[key]; !exists {
om.keys = append(om.keys, key)
}

om.data[key] = value
}

func (om *OrderedMap) Get(key string) interface{} {
om.RLock()
defer om.RUnlock()
return om.data[key]
}

func (om *OrderedMap) Delete(key string) {
om.Lock()
defer om.Unlock()

if _, exists := om.data[key]; exists {
delete(om.data, key)
for i, k := range om.keys {
if k == key {
om.keys = append(om.keys[:i], om.keys[i+1:]...)
break
}
}
}
}

func (om *OrderedMap) Iterate() func() (string, interface{}, bool) {
om.RLock()
defer om.RUnlock()

i := 0
keys := append([]string(nil), om.keys...)

return func() (string, interface{}, bool) {
if i >= len(keys) {
return "", nil, false
}
k := keys[i]
v := om.data[k]
i++
return k, v, true
}
}

func useOrderedMap() {
om := NewOrderedMap()
om.Set("c", 1)
om.Set("a", 2)
om.Set("b", 3)

iter := om.Iterate()
for {
k, v, ok := iter()
if !ok {
break
}
fmt.Printf("k: %s, v: %v\n", k, v)
}
}

16. 锁相关

16.1、除了 mutex 以外还有那些方式安全读写共享变量?

16.1.1 各种同步原语对比

方式 实现原理 适用场景 优点 缺点
sync.Mutex 互斥锁 任意场景 简单、通用 性能一般、不能升级
sync.RWMutex 读写锁 读多写少 读性能高 写性能一般
sync.Once 单例模式 初始化 保证只执行一次 功能单一
sync.WaitGroup 等待组 任务同步 简单易用 不能取消
sync.Cond 条件变量 条件等待 灵活控制 实现复杂
atomic 操作 原子指令 简单操作 高性能、无锁 操作单一

16.1.2 atomic 操作示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 使用atomic操作实现计数器
func atomicCounter() {
var counter int64 = 0

// 启动100个goroutine进行加法操作
for i := 0; i < 100; i++ {
go func() {
for j := 0; j < 1000; j++ {
atomic.AddInt64(&counter, 1)
}
}()
}

time.Sleep(1 * time.Second)
fmt.Println("最终计数:", atomic.LoadInt64(&counter))
}

// 使用atomic操作实现并发安全的状态管理
func atomicState() {
var state int32 = 0

go func() {
// 原子写入状态
atomic.StoreInt32(&state, 1)
}()

go func() {
// 原子读取状态
currentState := atomic.LoadInt32(&state)
fmt.Println("当前状态:", currentState)
}()

time.Sleep(1 * time.Second)
}

16.2、Go 如何实现原子操作?

16.2.1 底层原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 原子操作的核心 - 内存屏障
type memoryBarrier struct{}

func (mb *memoryBarrier) Load() {
// 强制刷新CPU缓存
// 确保其他CPU看到最新的内存状态
}

func (mb *memoryBarrier) Store() {
// 强制刷新写缓冲区
// 确保内存操作的顺序性
}

// 原子操作实现的基本原理
func atomicAdd(addr *int64, delta int64) int64 {
for {
// 1. 读取变量的当前值
old := *addr

// 2. 计算新值
newVal := old + delta

// 3. CAS操作:如果变量的值还是old,就更新为newVal
if cas(addr, old, newVal) {
return newVal
}
}
}

// 模拟CPU的比较和交换指令
func cas(addr *int64, old, newVal int64) bool {
return atomic.CompareAndSwapInt64(addr, old, newVal)
}

16.2.2 原子操作的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 原子操作的类型支持
func atomicTypes() {
// 基本类型的原子操作
var (
i32 int32
i64 int64
u32 uint32
u64 uint64
ptr unsafe.Pointer
flag uint32
)

// 加法操作
atomic.AddInt32(&i32, 1)
atomic.AddInt64(&i64, 1)
atomic.AddUint32(&u32, 1)
atomic.AddUint64(&u64, 1)

// 存储操作
atomic.StoreInt32(&i32, 100)
atomic.StorePointer(&ptr, unsafe.Pointer(&i32))

// 读取操作
v32 := atomic.LoadInt32(&i32)
v64 := atomic.LoadInt64(&i64)

// 比较和交换
atomic.CompareAndSwapInt32(&i32, 100, 200)

// 交换操作
oldV := atomic.SwapInt32(&i32, 300)

// 位操作
atomic.OrUint32(&u32, 0x01)
atomic.AndUint32(&u32, 0x00)

// 指针操作
atomic.PointerInt32(&ptr)
}

16.3、Mutex 是悲观锁还是乐观锁?悲观锁、乐观锁是什么?

16.3.1 悲观锁与乐观锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 悲观锁实现(使用Mutex)
func pessimisticLock() {
var m sync.Mutex
var data int

// 每个goroutine都假设会发生冲突
for i := 0; i < 10; i++ {
go func(id int) {
m.Lock() // 先获取锁
defer m.Unlock()
data++ // 安全访问共享数据
fmt.Printf("goroutine %d 完成,数据: %d\n", id, data)
}(i)
}

time.Sleep(1 * time.Second)
}

// 乐观锁实现(使用atomic操作)
func optimisticLock() {
var data int64

// 每个goroutine假设不会发生冲突
for i := 0; i < 10; i++ {
go func(id int) {
for {
old := atomic.LoadInt64(&data)
newVal := old + 1

// 尝试更新,如果失败说明有冲突,重试
if atomic.CompareAndSwapInt64(&data, old, newVal) {
fmt.Printf("goroutine %d 完成,数据: %d\n", id, newVal)
break
}
}
}(i)
}

time.Sleep(1 * time.Second)
}

16.3.2 场景选择

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 根据场景选择合适的锁
func chooseLock() {
// 场景1: 操作频繁,冲突概率高(如热点数据)
var m sync.Mutex

// 场景2: 操作简单,冲突概率低(如计数器)
var counter int64

// 场景3: 读多写少(如配置读取)
var rw sync.RWMutex

// 场景4: 需要条件判断的同步
var cond = sync.NewCond(&sync.Mutex{})
}

16.4、Mutex 有几种模式?

16.4.1 Mutex的状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Mutex的核心结构
type Mutex struct {
state int32 // 状态位
sema uint32 // 信号量
}

// Mutex的状态位定义
const (
mutexLocked = 1 << 0 // 锁定状态
mutexWoken = 1 << 1 // 唤醒状态
mutexStarving = 1 << 2 // 饥饿状态
mutexWaiterShift = 3 // 等待者数量偏移
)

// Mutex的工作流程
func (m *Mutex) Lock() {
// 1. 快速路径:直接获取锁
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
return
}

// 2. 慢速路径:复杂逻辑处理
m.lockSlow()
}

16.4.2 正常模式与饥饿模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 正常模式(默认模式)
func normalMode() {
// 特点:
// - 等待时间短的goroutine更可能获得锁
// - 使用先入先出队列管理等待者
// - 等待者会自旋尝试获取锁(避免上下文切换)
}

// 饥饿模式(当有goroutine等待时间超过1ms时触发)
func starvingMode() {
// 特点:
// - 锁直接传递给等待时间最长的goroutine
// - 新到达的goroutine会进入队列的尾部
// - 当没有等待者或最后一个等待者已被服务时,切换回正常模式
}

// 强制饥饿模式(用于测试)
func forceStarvingMode() {
var m sync.Mutex

// 启动多个长时间等待的goroutine
for i := 0; i < 10; i++ {
go func(id int) {
m.Lock()
time.Sleep(100 * time.Millisecond) // 长时间持有锁
fmt.Printf("goroutine %d 释放锁\n", id)
m.Unlock()
}(i)

time.Sleep(50 * time.Millisecond)
}
}

16.5、goroutine 的自旋占用资源如何解决

16.5.1 自旋条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 自旋条件(来自Go源代码)
func canSpin(i int) bool {
// 只能在多核CPU上自旋
if gomaxprocs <= 1 {
return false
}

// 如果有空闲的P,就直接调度
if ncpu == 1 {
return false
}

// 自旋次数限制
if i >= active_spin {
return false
}

// 如果P的队列没有可运行的goroutine,说明我们可以尝试自旋
if pp := getg().m.p.ptr(); !runqempty(pp) {
return false
}

return true
}

16.5.2 自旋优化策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 避免过多自旋的优化策略
func spinOptimization() {
// 1. 减少临界区代码
var m sync.Mutex
var data int

go func() {
m.Lock()
// 只包含必要的操作
data++
m.Unlock()
}()

// 2. 使用读写锁减少锁竞争
var rw sync.RWMutex

go func() {
rw.RLock()
// 只读操作
fmt.Println(data)
rw.RUnlock()
}()

go func() {
rw.Lock()
// 只包含必要的操作
data++
rw.Unlock()
}()
}

16.6、读写锁底层是怎么实现的?

16.6.1 底层实现原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// RWMutex核心结构
type RWMutex struct {
w Mutex // 互斥锁,用于写操作
readerCount int32 // 正在读的goroutine数量
readerWait int32 // 等待写完成的goroutine数量
}

// 读操作获取锁
func (rw *RWMutex) RLock() {
if atomic.AddInt32(&rw.readerCount, 1) < 0 {
// 有写操作正在进行,阻塞
runtime_SemacquireMutex(&rw.w.sema, false, 0)
}
}

// 读操作释放锁
func (rw *RWMutex) RUnlock() {
if atomic.AddInt32(&rw.readerCount, -1) < 0 {
// 有写操作正在等待,需要唤醒
rw.rUnlockSlow()
}
}

// 写操作获取锁
func (rw *RWMutex) Lock() {
// 1. 首先获取互斥锁
rw.w.Lock()

// 2. 计算等待读操作完成的数量
r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders)

// 3. 等待所有读操作完成
if r != 0 {
atomic.StoreInt32(&rw.readerWait, r)
runtime_SemacquireMutex(&rw.w.sema, false, 0)
}
}

// 写操作释放锁
func (rw *RWMutex) Unlock() {
// 1. 重置readerCount为正数
atomic.StoreInt32(&rw.readerCount, rwmutexMaxReaders)

// 2. 唤醒所有正在等待的读操作
for i := int32(0); i < rw.readerCount; i++ {
runtime_Semrelease(&rw.w.sema, false, 0)
}

// 3. 释放互斥锁
rw.w.Unlock()
}

16.6.2 状态转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// RWMutex状态转换示意图
func rwMutexStates() {
const (
StateFree = 0x00 // 无锁状态
StateReading = 0x01 // 读状态(有读操作)
StateWriting = 0x02 // 写状态(有写操作)
StateWaiting = 0x03 // 写等待状态(有写在等待,读继续)
)

// 初始状态
state := StateFree

// 读操作
go func() {
state = StateReading
// 读操作
state = StateFree
}()

// 写操作
go func() {
state = StateWaiting
// 等待所有读完成
state = StateWriting
// 写操作
state = StateFree
}()
}

17. 同步原语相关

17.1、知道哪些 sync 同步原语?各有什么作用?

17.1.1 sync.Pool(高频问题)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// sync.Pool的基本使用
func poolBasic() {
p := sync.Pool{
New: func() interface{} {
fmt.Println("创建新对象")
return new(int)
},
}

// 从池中获取对象
obj := p.Get().(*int)
*obj = 42

// 放回到池中
p.Put(obj)

// 再次从池中获取(很可能是刚才的对象)
obj2 := p.Get().(*int)
fmt.Println("获取到的对象值:", *obj2) // 0,因为Get()会重置为零值
}

// 使用sync.Pool提高性能
func poolPerformance() {
var pool sync.Pool

// 使用pool创建对象
create := func() interface{} {
return make([]byte, 1024)
}
pool.New = create

var wg sync.WaitGroup
var mu sync.Mutex
var totalAlloc int

// 模拟高并发场景
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()

// 从池获取
buf := pool.Get().([]byte)
defer pool.Put(buf)

// 使用buf
_, err := io.ReadFull(rand.Reader, buf)
if err != nil {
return
}

// 更新分配计数器
mu.Lock()
totalAlloc += len(buf)
mu.Unlock()
}()
}

wg.Wait()
fmt.Printf("总分配: %d bytes\n", totalAlloc)
}

17.1.2 sync.Cond

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// sync.Cond的基本使用
func condBasic() {
c := sync.NewCond(&sync.Mutex{})
queue := make([]int, 0, 10)

// 生产者
go func() {
for i := 0; i < 10; i++ {
c.L.Lock()

for len(queue) == 10 {
c.Wait() // 队列满了,等待消费者通知
}

queue = append(queue, i)
fmt.Printf("生产: %d\n", i)

c.Signal() // 通知一个消费者
c.L.Unlock()

time.Sleep(100 * time.Millisecond)
}
}()

// 消费者
go func() {
for {
c.L.Lock()

for len(queue) == 0 {
c.Wait() // 队列空了,等待生产者通知
}

item := queue[0]
queue = queue[1:]
fmt.Printf("消费: %d\n", item)

c.Signal() // 通知一个生产者
c.L.Unlock()

time.Sleep(150 * time.Millisecond)
}
}()

time.Sleep(5 * time.Second)
}

17.2、sync.WaitGroup

17.2.1 基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// sync.WaitGroup的基本使用
func wgBasic() {
var wg sync.WaitGroup
tasks := []string{"task1", "task2", "task3"}

// 设置等待任务数
wg.Add(len(tasks))

for _, task := range tasks {
go func(name string) {
defer wg.Done()

fmt.Printf("开始执行: %s\n", name)
time.Sleep(time.Second) // 模拟耗时操作
fmt.Printf("完成: %s\n", name)
}(task)
}

fmt.Println("等待所有任务完成...")
wg.Wait()
fmt.Println("所有任务完成!")
}

// WaitGroup与匿名函数
func wgAnonymous() {
var wg sync.WaitGroup
data := []int{1, 2, 3, 4, 5}

for i, v := range data {
wg.Add(1)
// 直接使用闭包捕获变量会导致问题
go func() {
defer wg.Done()
// 错误:i和v会是循环的最后一个值
fmt.Printf("索引: %d, 值: %d\n", i, v)
}()
}

wg.Wait()
}

// 正确的做法
func wgCorrect() {
var wg sync.WaitGroup
data := []int{1, 2, 3, 4, 5}

for i, v := range data {
wg.Add(1)
// 使用参数传递避免闭包捕获变量的问题
go func(idx, val int) {
defer wg.Done()
fmt.Printf("索引: %d, 值: %d\n", idx, val)
}(i, v)
}

wg.Wait()
}

17.2.2 WaitGroup的高级用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 使用WaitGroup分组处理
func wgGrouping() {
var wg sync.WaitGroup

// 第一组任务
for i := 0; i < 3; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Printf("第一组任务 %d 完成\n", id)
}(i)
}

// 等待第一组完成
wg.Wait()
fmt.Println("第一组任务全部完成!")

// 第二组任务
for i := 3; i < 6; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Printf("第二组任务 %d 完成\n", id)
}(i)
}

// 等待第二组完成
wg.Wait()
fmt.Println("所有任务完成!")
}

总结

本章节补充了 Channel、Map 和 GMP 相关的重要内容,涵盖了:

Channel 相关

  • 线程安全机制
  • 底层数据结构
  • nil/关闭/有数据的 channel 操作
  • 发送/接收流程

Map 相关

  • 线程安全解决方案(Mutex、RWMutex、sync.Map、分片锁)
  • 循环顺序问题
  • 内存释放机制
  • nil map 和空 map 的区别
  • 底层数据结构和扩容机制
  • 取值修改对原 map 的影响

GMP 相关

  • 调度器组成(G、M、P)
  • 调度流程
  • 任务偷取机制
  • goroutine 栈特性
  • goroutine 与操作系统线程的区别
  • GOMAXPROCS 的影响
  • 调度器的发展历史(协作式到用户态抢占)

这些补充内容全面覆盖了 Go 语言面试中的重要考点,包括理论知识、底层实现和实际应用建议,帮助面试者更好地理解和掌握 Go 语言的并发编程模型。

__END__