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

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

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

1.2 make(T, args)

1
2
3
4
5

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

1.3 代码对比

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

import "fmt"


func main(){

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

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

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

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

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

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

}


总结

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

2、数组和切片的区别?

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

2.1 声明和初始化

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

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

2.2 类型特性

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

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

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

2.3 长度和容量

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

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

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

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

2.4 函数传递

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

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


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

2.5 切片的底层结构

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

2.6 使用场景

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

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

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

2.7 数组转切片

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

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

2.8 最佳实践建议

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

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

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

3.1 基本现象

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

package main

import "fmt"

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

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

3.2 常见陷阱-指针切片

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

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

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

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

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

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

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

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

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

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

3.3 闭包陷阱

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

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

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

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

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

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

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

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

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

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

3.4. goroutine 陷阱

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

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

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

time.Sleep(time.Second)
}

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

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

time.Sleep(time.Second)
}

3.5. 结构体切片的陷阱

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

type Person struct {
Name string
Age int
}

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

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

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

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

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

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

3.6. map遍历

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

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

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

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

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

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

4.1 defer基本原理

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

4.2 多个defer的执行顺序

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

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

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

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

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

4.3 defer修改返回值的时机

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

4.3.1 命名返回值示例

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


4.3.2 匿名返回值示例

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

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

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

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

4 defer的参数求值时机

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

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


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


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

5、uint 类型溢出

5.1 基本概念

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

uint类型返回

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

5.2 溢出行为

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

package main

import "fmt"

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

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

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

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

import "fmt"

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

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

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

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

5.3 实际场景

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

5.4 防止溢出的方法

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

使用 math/bits 包

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

6、介绍 rune 类型

6.1 基本概念

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

6.2 rune与字符的关系

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

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

6.3 rune与字符串的关系

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

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

6.4 rune的使用场景

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

7性能考虑

1.[] rune转换开销

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

import (
"fmt"
"time"
)

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

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

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

2.使用strings.builder

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

package main

import (
"fmt"
"strings"
)

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

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

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

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

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

__END__