https://python-web-guide.readthedocs.io/zh/latest/go-note/web.html#id1
-https://markdown.lovejade.cn/
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 是怎么实现的?反射原理是什么?(问的很少,但是代码中用的多)
__END__