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)
// 分配0值内存,返回指向类型T的指针 *T
p := new(int) // p是一个*int类型的指针,指向一个int类型的零值
fmt.Println(*p) // 输出0
// 等价于
var i int
p := &i
1.2 make(T, args)
s := make([]int, 5) // 创建一个长度为5的int切片
m := make(map[string]int) // 创建一个空的map
c := make(chan int, 10) // 创建一个缓冲区大小为10的int通道
1.3 代码对比
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 声明和初始化
// 数组: 长度是类型的一部分
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 类型特性
// 数组是值类型,赋值会拷贝整个数组
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 长度和容量
// 数组: 长度固定
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 函数传递
// 数组传递:值拷贝,影响性能
func modifyArray(arr [10000] int){
arr[0] = 100 // 不会影响原数组
}
// 切片传递:只拷贝切片头(24字节),高效
func modifySlice(slice []int) {
slice[0] = 100 // 会影响原切片的底层数组
}
2.5 切片的底层结构
// 切片的内部结构
type slice struct {
array unsafe.Pointer // 指向底层数组的指针
len int // 长度
len int // 容量
}
2.6 使用场景
// 数组:长度固定, 明确已知
func ProcessFixedData(){
var buffer [1024]byte //固定大小的缓冲区
// ....
}
// 切片: 长度不确定,需要动态操作
func processDynamicData(){
data := []string{}
data = append(data, "item1")
data = append(data, "item2")
// ....
}
2.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 最佳实践建议
- 优先使用切片: 大多数情况下使用切片更灵活
- 明确长度时使用数组: 如固定大小的缓冲区,md5哈希值等
- 注意切片陷阱: 切片恭喜底层数组,丢修改要小心
- 预分配容量:使用 make([]T, len, cap) 可以减少扩容次数
// 好的做法:预分配容量
slice := make([]int, 0, 100)
for i := 0; i < 100; i++ {
slice = append(slice, i)
}
3、for range 的时候它的地址会发生变化么?
迭代变量的地址不会变化! 这是go非常重要的特性
3.1 基本现象
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 常见陷阱-指针切片
// ❌ 错误示例
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 闭包陷阱
// ❌ 错误示例
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 陷阱
// ❌ 错误示例
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. 结构体切片的陷阱
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遍历
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的执行顺序
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
for deferInLoop(){
for i :=0;i<5;i++{
defer fmt.Println(i)
}
}
// 输出顺序:4 3 2 1 0
- 注意事项
// ❌ 错误示例: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 命名返回值示例
func f1(result int) int {
defer func() {
result += 5 // 修改命名返回值
}()
return 10 // 第一步:result = 10
} // 第二步:执行defer,result变为15,第三步:返回15
4.3.2 匿名返回值示例
func f2() int {
var result int
defer func() {
result += 5 // 修改局部变量,不影响返回值
}()
return 10 // 返回值 = result (0), defer修改的是局部变量
}
// 最终返回10
4.3.3 返回指针/引用类型 - defer 可以修改
func f3() *int{
result :=0
defer func(){
result +=5 // 修改局部变量,修改指针指向的值
}()
return &result // 返回指针,defer修改了指针指向的值
}
// 最终返回指向5的指针, 返回*int 值为5
4 defer的参数求值时机
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类型返回
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类型,如果执行以下操作:
- 上溢,超过最大值
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
}
- 下溢,低于最小值
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'的代码点。
var r rune = 'a' // r的值是97
6.3 rune与字符串的关系
字符串是由一系列rune组成的序列。在Go中,字符串是不可变的字节序列,而rune则表示字符串中的单个字符。可以通过将字符串转换为rune切片来访问字符串中的每个字符。
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转换开销
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
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 是什么?为什么能被解析?
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的?
- 反射入口
t := reflect.TypeOf(User{})
field, ok := t.FieldByName("ID")
fmt.Println(field.Tag) // 输出: `json:"id" db:"user_id"`
- StructTag的定义
# reflect/type.go
typr StructTag string
也就是数tag就是一个字符换,没有任何的数据结构
- Get() 是怎么是实现的?
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语言中所有的函数调用都是值传递,这意味着当你将一个变量传递给函数时,函数接收到的是该变量的一个副本。然而,当我们谈论传递结构体时,传递指针实际上是传递了结构体在内存中的地址,这样做有几个重要的好处:
- 性能优化:结构体可能包含大量数据,传递整个结构体会涉及到数据的复制,这在性能上是昂贵的。通过传递指针,只需复制地址(通常是4或8字节),大大减少了内存开销和复制时间。
- 修改原始数据:当你传递结构体的指针时,函数可以直接修改原始结构体的数据,而不是其副本。这对于需要在函数中更新结构体状态的场景非常有用。
- 避免栈溢出:对于非常大的结构体,传递值可能会导致栈空间不足,从而引发栈溢出错误。传递指针可以避免这种风险。
- 一致性和习惯用法:在Go社区中,传递指针是处理结构体的常见做法,这有助于保持代码的一致性和可读性。
8.2 什么时候一定要传指针?
8.2.1 函数需要修改结构体(最重要)
func UpdateUserName(u *User, newName string) {
u.Name = newName // 直接修改原始结构体
}
# 如果你传值的话,修改不会反映到调用者
func UpdateUserNameValue(u User, newName string) {
u.Name = newName // 修改的是副本
}
# 修改必须是指针
8.2.2 结构体较大
type Big struct {
Data [1024 * 1024]byte // 1MB数据
}
- 传值:每次拷贝1kb
- 传指针:只拷贝8字节地址
struct ≥ 几十字节 → 优先指针
struct 含大数组 / map / slice / string → 几乎一定指针
8.2.3 需要保持语义一致性
type User struct{}
func (u *User) Save() {}
func Handle(u User) {
u.Save() // ❌ 编译不过
}
#User 的 method set 不包含 *User 的方法
#反过来可以(编译器自动取地址)
8.2.4 避免拷贝导致的“隐藏 Bug”
type Config struct {
Options map[string]string
}
func Modify(c Config) {
c.Options["x"] = "y"
}
# ⚠️ 这会修改原 map!
因为:
struct 被拷贝
map 是引用类型
修改 map 内容会影响原对象
👉 这种“半拷贝”行为非常容易误导
✔ 用指针,语义更清晰
8.3 什么时候传值?
8.3.1 结构体很小 + 不可变语义
type Point struct {
X, Y int
}
func Distance(a, b Point) float64
✔ 小
✔ 不修改
✔ 数学/值对象
👉 传值更清晰
8.3.2 只读场景 + 明确不修改
func PrintUser(u User) {
fmt.Println(u.Name)
}
✔ 不修改
✔ 只读
👉 传值更安全
8.3.3 避免 nil(稳定性)
func Print(u User)
func Print(u *User) // 可能 panic
如果函数逻辑不能接受 nil:
✔ 用值
❌ 不要强迫调用方构造指针
8.3.4 高并发 / 不希望共享状态
func Handle(req Request)
func Handle(req *Request)
那就要开始担心:
数据竞争
隐式共享
goroutine 安全
👉 值传递天然隔离
总结
状态型对象 → 指针
值对象 / DTO / 参数对象 → 值
9. silce 遇到过哪些问题?
9.1 append 导致对的数据悄悄被改掉
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]
- 修复方法
b := append([]int(nil), a[:2]...) // 复制一份数据
# 或者
b := slice.Clone(a[:2])
9.2 for range + append 导致死循环/逻辑错误
for _, v := range slice {
if someCondition(v) {
slice = append(slice, newValue) // 修改了 slice 导致死循环或逻辑错误
}
}
- 根本原因
range 在循环开始时就固定了 len
append 修改的是同一个 slice
逻辑极其容易出错
# ❌ 不要在 range 原 slice 时 append 同一个 slice
9.3 sub-slice 导致数据泄漏;
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失效(指针悬空)
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 导致数据竞态
go func(){
s = append(s, 1)
}()
go func(){
fmt.Println(s[0])
}()
问题
slice 不是线程安全的
append 涉及:
len
cap
底层数组写入
📌 这是 data race + panic 双重风险
9.8 make 参数理解错误
s := make([]int,5)
a = append(s, 1)
fmt.Println(s) // [0 0 0 0 0 1]
# 正确预分配
s := make([]int, 0, 5)
9.10、删除元素写错
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 基本规则
// ✅ 可以比较的情况
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 实际示例
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 特殊情况
// 空结构体比较
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 最佳实践
- 设计可比较结构体:避免slice、map、func字段
- 使用自定义比较:对于复杂结构体实现Equal()方法
- 注意nil值:指针类型的nil值比较
// 自定义比较方法
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 闭包的基本特性
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 循环变量陷阱
// ❌ 错误示例
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闭包陷阱
// ❌ 错误示例
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 函数工厂
// 加法器工厂
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 装饰器模式
// 日志装饰器
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 延迟计算
// 延迟计算斐波那契数列
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 闭包与内存管理
// 闭包可能导致内存泄漏
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 闭包的最佳实践
- 避免循环变量陷阱:使用传参或局部变量
- 注意内存泄漏:闭包会延长对象生命周期
- 合理使用闭包:在需要状态保持时使用
- 理解闭包本质:闭包=函数+引用的环境
12. Context
12.1、context 结构是什么样的?
12.1.1 Context接口定义
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的继承结构
// 空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. 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. 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使用注意事项
- 不要传递nil context:使用
context.Background()或context.TODO() - context是immutable的:总是返回新的context
- 及时调用cancel:使用defer确保cancel被调用
- 不要过度使用WithValue:只传递请求范围的数据
- 理解context的传播:context会自动传播到goroutine
// ✅ 正确的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 内部锁机制
// 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 安全操作与不安全操作
// ✅ 安全操作
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 底层实现数据结构
// 等待队列
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 详细代码示例
// 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: 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 发送操作流程
// 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 接收操作流程
// 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 基本特性
// 创建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 并发安全问题
// ❌ 错误示例:并发读写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: 使用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 无序性演示
// 演示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 确保有序遍历的方法
// 获取有序键的方法
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 内存释放机制
// 演示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 内存释放策略
// 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 方案选择策略
// 根据场景选择合适的并发安全方案
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 基本区别
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 的方法
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 底层实现结构
// 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 扩容机制
// 扩容条件
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 基本规则
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 基本概念
// 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 调度流程
// 调度器启动
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协程的优势
// 演示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 抢占触发条件
// 栈增长检查触发抢占
func morestack() {
// 检测到栈增长时,触发抢占
// 将goroutine状态标记为需要抢占
// 当goroutine下一次进入调度点时会被抢占
}
// 系统调用返回触发调度
func entersyscallblock() {
// 系统调用阻塞后,释放P
// 调度器可以将P分配给其他M
}
// 主动调用触发调度
func Gosched() {
// 主动放弃CPU,重新调度
}
15.3.2 抢占实现机制
// 定时器触发抢占
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 默认值与配置
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 最佳实践
// 根据任务类型选择合适的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: 正常执行完毕
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 处理协程的优雅退出
// 使用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 标准库方案
// 通用顺序读取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 使用结构体封装的方式
// 使用结构体封装的有序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 操作示例
// 使用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 底层原理
// 原子操作的核心 - 内存屏障
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 原子操作的类型
// 原子操作的类型支持
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 悲观锁与乐观锁
// 悲观锁实现(使用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 场景选择
// 根据场景选择合适的锁
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的状态
// 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 正常模式与饥饿模式
// 正常模式(默认模式)
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 自旋条件
// 自旋条件(来自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 自旋优化策略
// 避免过多自旋的优化策略
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 底层实现原理
// 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 状态转换
// 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(高频问题)
// 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
// 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 基本使用
// 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的高级用法
// 使用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 语言的并发编程模型。