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 p := new (int ) fmt.Println(*p) var i int p := &i
1.2 make(T, args)
1 2 3 4 5 s := make ([]int , 5 ) m := make (map [string ]int ) c := make (chan int , 10 )
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 mainimport "fmt" func main () { p1 := new (int ) fmt.Printf("new(int): %T, 值: %v, 地址: %p\n" , p1, *p1, p1) p2 := new ([]int ) fmt.Printf("new([]int): %T, 值: %v, 是否nil: %v\n" , p2, *p2, *p2 == nil ) s := make ([]int , 5 , 10 ) fmt.Printf("make([]int): %T, len: %d, cap: %d\n" , s, len (s), cap (s)) s[0 ] = 100 m := make (map [string ]int ) m["key" ] = 42 fmt.Printf("make(map): %T, 值: %v\n" , m, m) 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 arr2 :=[3 ]int {1 ,2 ,3 } arr3 := [...]int {4 ,5 ,6 } var slice1 []int slice2 := []int {1 ,2 ,3 } slice3 := make ([]int , 5 ) slice4 := make ([]int , 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) fmt.Println(arr2) slice := []int {1 ,2 ,3 } slice2 := slice slice2[0 ] = 100 fmt.Println(slice) fmt.Println(slice2)
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)) slice := []int {1 , 2 , 3 } fmt.Println(len (slice)) fmt.Println(cap (slice)) slice = append (slice, 4 ) fmt.Println(slice)
2.4 函数传递
1 2 3 4 5 6 7 8 9 10 11 12 func modifyArray (arr [10000] int ) { arr[0 ] = 100 } 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 ] slice3 := arr[:3 ] slice4 := arr[2 :]
2.8 最佳实践建议
优先使用切片: 大多数情况下使用切片更灵活
明确长度时使用数组: 如固定大小的缓冲区,md5哈希值等
注意切片陷阱: 切片恭喜底层数组,丢修改要小心
预分配容量:使用 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) } }
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, " " ) } } 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, " " ) } } 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, " " ) } }
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, " " ) }) } for _, f := range funcs { f() } } func main () { var funcs []func () slice := []int {1 , 2 , 3 , 4 , 5 } for _, v := range slice { v := v funcs = append (funcs, func () { fmt.Print(v, " " ) }) } for _, f := range funcs { f() } } 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)) } for _, f := range funcs { f() } }
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, " " ) }() } 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) } } 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) } }
场景 陷阱原因 解决方案
指针切片 所有指针指向同一地址 创建临时变量或使用索引
闭包 捕获变量地址而非值 传参或创建副本
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" ) }
1 2 3 4 5 6 7 for deferInLoop(){ for i :=0 ;i<5 ;i++{ defer fmt.Println(i) } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func readFiles () { for i := 0 ; i < 1000 ; i++ { f, _ := os.Open(fmt.Sprintf("file%d.txt" , i)) defer f.Close() } } 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 }
4.3.2 匿名返回值示例
1 2 3 4 5 6 7 8 9 func f2 () int { var result int defer func () { result += 5 }() return 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 }
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) x = 1 fmt.Println("x:" , x) } func deferClosure () { i :=0 defer func () { fmt.Println("defer i:" ,i) }() i ++ }
避免在 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 mainimport "fmt" func main () { var a uint8 = 255 fmt.Printf("原始值: %d\n" , a) a = a + 1 fmt.Printf("255 + 1 = %d\n" , a) a = 255 + 2 fmt.Printf("255 + 2 = %d\n" , a) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package mainimport "fmt" func main () { var b uint8 = 0 fmt.Printf("原始值: %d\n" , b) b = b - 1 fmt.Printf("0 - 1 = %d\n" , b) b = 0 - 5 fmt.Printf("0 - 5 = %d\n" , b) }
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'的代码点。
6.3 rune与字符串的关系
字符串是由一系列rune组成的序列。在Go中,字符串是不可变的字节序列,而rune则表示字符串中的单个字符。可以通过将字符串转换为rune切片来访问字符串中的每个字符。
1 2 3 4 s := "Hello, 世界" runes := []rune (s) fmt.Println(runes)
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 mainimport ( "fmt" "time" ) func main () { s := "这是一个很长的中文字符串" + "重复很多次" longString := "" for i := 0 ; i < 1000 ; i++ { longString += s } start := time.Now() for i := 0 ; i < 100 ; i++ { runes := []rune (longString) _ = len (runes) } fmt.Printf("多次转换耗时: %v\n" , time.Since(start)) 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 mainimport ( "fmt" "strings" ) func buildStringWithRunes () string { var builder strings.Builder runes := []rune {'H' , 'e' , 'l' , 'l' , 'o' , '世' , '界' } for _, r := range runes { builder.WriteRune(r) } return builder.String() } func main () { result := buildStringWithRunes() fmt.Println(result) }
最佳实践
处理非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 是 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)
1 2 3 # reflect/type .go typr StructTag string
也就是数tag就是一个字符换,没有任何的数据结构
1 value := field.Tag.Get("json" )
关键的结论:
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 函数需要修改结构体(最重要)
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 }
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) 如果函数逻辑不能接受 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 = append (b,100 ) fmt.Println(a) 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) } } - 根本原因 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 ) small := buf[:1024 ] return small # 问题 small 只用 100 B 但 引用了 10 MB 的底层数组 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) # 正确预分配 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 :]...) 隐藏问题 底层数组仍然持有 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 s []int 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 mainimport "fmt" type Person struct { Name string Age int } type Container struct { Data []int } func main () { p1 := Person{"Alice" , 25 } p2 := Person{"Alice" , 25 } p3 := Person{"Bob" , 30 } fmt.Println(p1 == p2) fmt.Println(p1 == p3) }
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) } type Node struct { value int next *Node } func main () { n1 := Node{1 , nil } n2 := Node{1 , nil } fmt.Println(n1 == n2) }
10.5 最佳实践
设计可比较结构体 :避免slice、map、func字段
使用自定义比较 :对于复杂结构体实现Equal()方法
注意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 mainimport "fmt" func main () { x := 10 f := func () int { return x + 1 } fmt.Println(f()) x = 20 fmt.Println(f()) }
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) }) } for _, f := range funcs { f() } } func main () { var funcs []func () for i := 0 ; i < 3 ; i++ { funcs = append (funcs, func (val int ) func () { return func () { fmt.Println(val) } }(i)) } for _, f := range funcs { f() } } func main () { var funcs []func () for i := 0 ; i < 3 ; i++ { i := i funcs = append (funcs, func () { fmt.Println(i) }) } for _, f := range funcs { f() } }
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) }() } time.Sleep(time.Second) } func main () { slice := []int {1 , 2 , 3 , 4 , 5 } for _, v := range slice { go func (val int ) { fmt.Println(val) }(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 )) fmt.Println(add10(3 )) }
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 ) return func () { fmt.Println(len (data)) } } func main () { f := leakyFunction() f() }
11.6 闭包的最佳实践
避免循环变量陷阱 :使用传参或局部变量
注意内存泄漏 :闭包会延长对象生命周期
合理使用闭包 :在需要状态保持时使用
理解闭包本质 :闭包=函数+引用的环境
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() (deadline time.Time, ok bool ) Done() <-chan struct {} Err() error 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 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 } type cancelCtx struct { Context done chan struct {} err error } type timerCtx struct { cancelCtx timer *time.Timer } 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 func handleRequest (w http.ResponseWriter, r *http.Request) { 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) } func callUserService (ctx context.Context, userID string ) (*User, error ) { traceID := ctx.Value("traceID" ).(string ) ctx = context.WithValue(ctx, "service" , "user-service" ) ctx, cancel := context.WithTimeout(ctx, 3 *time.Second) defer cancel() 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 } 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() } } } 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 func processData (ctx context.Context, data []byte ) error { return nil } type Service struct { ctx context.Context } type Service struct {}func (s *Service) Process(ctx context.Context, data []byte ) error { return nil } 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 } 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func handleRequest (ctx context.Context, req Request) error { 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 type hchan struct { qcount uint dataqsiz uint buf unsafe.Pointer elemsize uint16 closed uint32 elemtype *_type sendx uint recvx uint recvq waitq sendq waitq lock mutex }
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 ) close (ch) ch <- 1 var chNil chan int close (chNil) }
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 } type sudog struct { g *g next *sudog prev *sudog elem unsafe.Pointer isSelect bool c *hchan } func chansend (c *hchan, ep unsafe.Pointer, block bool , callerpc uintptr ) bool { if c.recvq.first != nil { send(c, sg, ep, func () { unlock(&c.lock) }) return true } 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 } if !block { unlock(&c.lock) return false } 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 func nilChannel () { var ch chan int go func () { ch <- 1 fmt.Println("send successful" ) }() go func () { <-ch fmt.Println("receive successful" ) }() } 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) fmt.Println(v2, ok2) fmt.Println(v3, ok3) }
13.3.3 常见面试题变种
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 func channelReadAfterClose () { ch := make (chan int ) close (ch) v, ok := <-ch fmt.Printf("v=%v, ok=%v\n" , v, ok) } func sendToNilChannel () { var ch chan int } func sendToClosedChannel () { ch := make (chan int ) close (ch) }
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 func sendFlow (c *hchan, value interface {}) bool { lock() if c.closed > 0 { panic ("send on closed channel" ) } if !c.recvq.empty() { sg := c.recvq.dequeue() *((*int )(sg.elem)) = value goready(sg.g, 3 ) unlock() return true } if c.qcount < c.dataqsiz { pos := c.sendx c.buf[pos] = value c.sendx++ c.qcount++ unlock() return true } sg := acquireSudog() sg.elem = &value sg.c = c c.sendq.enqueue(sg) 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 func receiveFlow (c *hchan) (interface {}, bool ) { lock() if !c.sendq.empty() { sg := c.sendq.dequeue() value := *(int *)(sg.elem) goready(sg.g, 3 ) unlock() return value, true } if c.qcount > 0 { value := c.buf[c.recvx] c.recvx++ c.qcount-- unlock() return value, true } 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 func createMap () { var m1 map [string ]int m2 := make (map [string ]int ) m3 := make (map [string ]int , 100 ) m4 := map [string ]int {"a" : 1 , "b" : 2 } } func mapOperations () { m := make (map [string ]int ) m["a" ] = 1 m["b" ] = 2 v1 := m["a" ] fmt.Println(v1) 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 func concurrentMapAccess () { m := make (map [string ]int ) 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 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) } 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) } func safeMap3 () { type Operation struct { key string value int op string result chan int } opsCh := make (chan Operation, 100 ) 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 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) } }
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 } 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 func mapMemory () { 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() 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 func mapResizing () { m := make (map [int ]int , 100 ) for i := 0 ; i < 26 ; i++ { m[i] = i } for i := 0 ; i < 18 ; i++ { delete (m, i) } 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 () { configMap := struct { sync.RWMutex data map [string ]string }{} cacheMap := sync.Map{} 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 m2 := make (map [string ]int ) fmt.Println("m1 == nil:" , m1 == nil ) fmt.Println("m2 == nil:" , m2 == nil ) fmt.Println("len(m1):" , len (m1)) fmt.Println("len(m2):" , len (m2)) delete (m1, "key" ) delete (m2, "key" ) 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 if m == nil { m = make (map [string ]int ) } m["key" ] = 1 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 type bmap struct { tophash [8 ]uint8 } type hmap struct { count int flags uint8 B uint8 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) return loadFactor > 6.5 || (m.noverflow > uint16 (m.B) && m.B > 15 ) } func growWork (t *maptype, h *hmap, bucket uintptr ) { if h.oldbuckets != nil { evacuate(t, h, bucket&h.oldbucketmask()) if h.nevacuate == bucket { h.nevacuate++ } } } func evacuate (t *maptype, h *hmap, oldbucket uintptr ) { }
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 () { m1 := map [string ]int {"a" : 1 } v := m1["a" ] v = 100 fmt.Println(m1["a" ]) m2 := map [string ]*int {"a" : new (int )} *m2["a" ] = 1 v2 := m2["a" ] *v2 = 100 fmt.Println(*m2["a" ]) type Point struct { X, Y int } m3 := map [string ]Point{"a" : {X: 1 , Y: 2 }} 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 type G struct { goid int64 stack stack sched gobuf gopc uintptr startpc uintptr param unsafe.Pointer atomicstatus uint32 } type M struct { id int32 g0 *g gsignal *g tls [6 ]uintptr mstartfn func () curg *g } type P struct { id int32 status uint32 runqhead uint32 runqtail uint32 runq [256 ]guintptr 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 () { } func schedule () { gp, inheritTime := findRunnable() execute(gp, inheritTime) } 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 func goroutineEfficiency () { 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 () { } func entersyscallblock () { } func Gosched () { }
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 () { for { now := nanotime() 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 () { fmt.Println("GOMAXPROCS:" , runtime.GOMAXPROCS(0 )) fmt.Println("NumCPU:" , runtime.NumCPU()) fmt.Println("NumGoroutine:" , runtime.NumGoroutine()) } func setGOMAXPROCS () { 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 func chooseGOMAXPROCS () { runtime.GOMAXPROCS(runtime.NumCPU()) }
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 func normalExit () { go func () { fmt.Println("goroutine 正在执行" ) }() } func returnExit () { go func () int { fmt.Println("goroutine 正在执行" ) return 100 }() } func goexitExit () { go func () { defer fmt.Println("defer 会执行" ) runtime.Goexit() fmt.Println("不会执行" ) }() } 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 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) } 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 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 }) 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 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 func atomicCounter () { var counter int64 = 0 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)) } 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() { } func (mb *memoryBarrier) Store() { } func atomicAdd (addr *int64 , delta int64 ) int64 { for { old := *addr newVal := old + delta if cas(addr, old, newVal) { return newVal } } } 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 func pessimisticLock () { var m sync.Mutex var data int 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) } func optimisticLock () { var data int64 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 () { var m sync.Mutex var counter int64 var rw sync.RWMutex 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 type Mutex struct { state int32 sema uint32 } const ( mutexLocked = 1 << 0 mutexWoken = 1 << 1 mutexStarving = 1 << 2 mutexWaiterShift = 3 ) func (m *Mutex) Lock() { if atomic.CompareAndSwapInt32(&m.state, 0 , mutexLocked) { return } 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 () { } func starvingMode () { } func forceStarvingMode () { var m sync.Mutex 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 func canSpin (i int ) bool { if gomaxprocs <= 1 { return false } if ncpu == 1 { return false } if i >= active_spin { return false } 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 () { var m sync.Mutex var data int go func () { m.Lock() data++ m.Unlock() }() 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 type RWMutex struct { w Mutex readerCount int32 readerWait int32 } 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() { rw.w.Lock() r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) if r != 0 { atomic.StoreInt32(&rw.readerWait, r) runtime_SemacquireMutex(&rw.w.sema, false , 0 ) } } func (rw *RWMutex) Unlock() { atomic.StoreInt32(&rw.readerCount, rwmutexMaxReaders) for i := int32 (0 ); i < rw.readerCount; i++ { runtime_Semrelease(&rw.w.sema, false , 0 ) } 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 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 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) } func poolPerformance () { var pool sync.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) _, 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 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 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("所有任务完成!" ) } 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() 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 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__