Golang相关知识总结
Golang 相关知识总结
1. Go 基础面试题
1.1 与其他语言相比,使用 Go 有什么好处?
- 语法简洁务实:Go 代码设计务实,语法更简洁,每个功能决策都旨在提高开发效率
- 并发优化:针对并发进行优化,支持协程,实现高效的 GMP 调度模型
- 可读性强:单一标准代码格式,代码更具可读性和一致性
- 高效垃圾回收:支持并行垃圾回收,回收效率比 Java 或 Python 更高
- 编译速度快:快速的编译时间,提高开发迭代效率
- 部署简单:编译为单一可执行文件,部署简单方便
1.2 什么是协程?
协程是用户态轻量级线程,是线程调度的基本单位。在函数前加上 go
关键字就能实现并发:
- 启动栈很小(2KB 或 4KB)
- 栈空间不足时自动伸缩
- 可轻易实现成千上万个 goroutine 同时启动
- 由 Go 运行时调度,非操作系统线程
1.3 协程和线程和进程的区别?
特性 | 进程 | 线程 | 协程 |
---|---|---|---|
资源分配 | 系统资源分配最小单位 | CPU 调度基本单位 | 用户态轻量级线程 |
内存空间 | 独立内存空间 | 共享进程内存 | 共享线程内存 |
上下文切换 | 开销大(栈、寄存器等) | 开销较小 | 开销极小 |
通信方式 | 进程间通信 | 共享内存 | Channel 通信 |
稳定性 | 稳定安全 | 相对不稳定 | 用户控制调度 |
创建数量 | 数十个 | 数百个 | 数万个 |
1.4 Golang 中 make 和 new 的区别?
make:
- 用于初始化并分配内存
- 只能用于创建 slice、map 和 channel 三种类型
- 返回的是初始化后的数据结构本身
- 会进行初始化操作
new:
- 用于分配内存但不初始化
- 可以用于任何类型的内存分配
- 返回的是指向该内存的指针
- 只分配内存,不进行初始化
// make 示例
s := make([]int, 5) // 创建长度为5的slice,初始化为[0,0,0,0,0]
m := make(map[string]int) // 创建空的map
ch := make(chan int, 10) // 创建缓冲大小为10的channel// new 示例
p := new(int) // 分配int类型内存,返回指针,*p为0
1.5 Golang 中数组和切片的区别?
数组:
- 固定长度,长度是类型的一部分
[3]int
和[4]int
是不同类型- 值传递,赋值或传参时会复制整个数组
- 需要指定大小或由初始化自动推算
切片:
- 可变长度,动态数组
- 包含指针、长度、容量三个属性
- 引用传递,底层共享数组
- 可通过数组初始化或 make() 创建
// 数组
var arr1 [3]int = [3]int{1, 2, 3}
arr2 := [...]int{1, 2, 3, 4} // 自动推断长度// 切片
var slice1 []int = make([]int, 3, 5) // 长度3,容量5
slice2 := arr1[0:2] // 从数组创建切片
底层结构:
type slice struct {array unsafe.Pointer // 指向底层数组len int // 长度cap int // 容量
}
1.6 使用 for range 的时候,它的地址会发生变化吗?
Go 1.22 之前:
- 迭代变量的内存地址保持不变
- 每次迭代将当前元素值复制到固定地址
- 可能导致并发问题
Go 1.22 及以后:
- 迭代变量地址是临时的,每次迭代重新生成
- 避免并发安全问题
- 更符合开发者预期
// Go 1.22 之前的问题示例
func main() {var wg sync.WaitGroupfor _, v := range []int{1, 2, 3} {wg.Add(1)go func() {defer wg.Done()fmt.Println(v) // 可能都输出3}()}wg.Wait()
}// 解决方案(在所有版本都有效)
for _, v := range []int{1, 2, 3} {v := v // 创建局部变量副本wg.Add(1)go func() {defer wg.Done()fmt.Println(v) // 正确输出1,2,3}()
}
1.7 如何高效地拼接字符串?
性能比较:
strings.Join ≈ strings.Builder > bytes.Buffer > "+" > fmt.Sprintf
各种拼接方式:
func main() {a := []string{"a", "b", "c"}// 方式1:+ (性能最差)ret1 := a[0] + a[1] + a[2]// 方式2:fmt.Sprintf (性能较差)ret2 := fmt.Sprintf("%s%s%s", a[0], a[1], a[2])// 方式3:strings.Builder (推荐)var sb strings.Buildersb.WriteString(a[0])sb.WriteString(a[1])sb.WriteString(a[2])ret3 := sb.String()// 方式4:bytes.Bufferbuf := new(bytes.Buffer)buf.WriteString(a[0])buf.WriteString(a[1])buf.WriteString(a[2])ret4 := buf.String()// 方式5:strings.Join (推荐,尤其对于切片)ret5 := strings.Join(a, "")
}
strings.Builder 优势:
- 内部使用
[]byte
缓冲区 String()
方法直接将[]byte
转换为string
- 避免不必要的内存分配和拷贝
1.8 defer 的执行顺序是怎样的?defer 的作用和使用场景?
执行顺序:
- 后进先出(LIFO)
- 多个 defer 语句按声明顺序相反的顺序执行
作用:
- 延迟执行函数,直到包含 defer 的函数执行完毕
- 无论函数正常返回还是 panic,defer 都会执行
使用场景:
- 资源释放(文件关闭、连接关闭、锁释放)
- 成对操作(打开/关闭、连接/断开)
- 错误处理和恢复
func test() int {i := 0defer func() {fmt.Println("defer1")}()defer func() {i += 1fmt.Println("defer2")}()return i
}func main() {fmt.Println("return", test()) // 输出:// defer2// defer1 // return 0
}
有名返回值的影响:
func test() (i int) {i = 0defer func() {i += 1fmt.Println("defer2")}()return i
}func main() {fmt.Println("return", test())// 输出:// defer2// return 1
}
1.9 什么是 rune 类型?
字符类型:
byte
:uint8 类型,代表 ASCII 码字符rune
:int32 类型,代表 UTF-8 字符
func main() {var str = "hello 你好"// golang中string底层通过byte数组实现// 按字节长度计算,汉字占3字节fmt.Println("len(str):", len(str)) // 输出: 12// 通过rune类型处理unicode字符fmt.Println("rune:", len([]rune(str))) // 输出: 8// 遍历字符串的两种方式for i := 0; i < len(str); i++ {fmt.Printf("%c ", str[i]) // 按字节遍历}fmt.Println()for _, r := range str {fmt.Printf("%c ", r) // 按rune遍历}
}
1.10 Go 语言 tag 有什么用?
常见用途:
json
:JSON 序列化/反序列化时的字段名db
:数据库字段名(sqlx 等 ORM 使用)form
:表单字段名(Gin 框架等)binding
:字段验证规则xml
:XML 序列化yaml
:YAML 序列化
type User struct {ID int `json:"id" db:"user_id" form:"id" binding:"required"`Username string `json:"username" db:"username" form:"username"`Password string `json:"-" db:"password"` // - 表示不序列化Email string `json:"email,omitempty"` // omitempty 表示空值时不序列化
}
1.11 go 打印时 %v %+v %#v 的区别?
type student struct {id int32name string
}func main() {a := &student{id: 1, name: "微客鸟窝"}fmt.Printf("a=%v \n", a) // a=&{1 微客鸟窝}fmt.Printf("a=%+v \n", a) // a=&{id:1 name:微客鸟窝} fmt.Printf("a=%#v \n", a) // a=&main.student{id:1, name:"微客鸟窝"}
}
区别:
%v
:只输出所有的值%+v
:先输出字段名,再输出字段值%#v
:先输出结构体名,再输出字段名和字段值
1.12 Go语言中空 struct{} 占用空间么?
package mainimport ("fmt""unsafe"
)func main() {fmt.Println(unsafe.Sizeof(struct{}{})) // 输出: 0
}
空结构体 struct{}
不占用任何内存空间。
1.13 Go语言中,空 struct{} 有什么用?
1. 实现 Set 集合
type Set map[string]struct{}func main() {set := make(Set)for _, item := range []string{"A", "A", "B", "C"} {set[item] = struct{}{}}fmt.Println(len(set)) // 输出: 3if _, ok := set["A"]; ok {fmt.Println("A exists") // 输出: A exists}
}
2. 通道信号
func main() {ch := make(chan struct{}, 1)go func() {<-ch// do something}()ch <- struct{}{}// ...
}
3. 仅有方法的结构体
type Lamp struct{}func (l Lamp) On() {fmt.Println("On")
}func (l Lamp) Off() {fmt.Println("Off")
}
1.14 init() 函数是什么时候执行的?
执行时机:
- 在
main
函数之前执行 - 由 runtime 初始化每个导入的包
初始化顺序:
- 导入的包(按依赖关系,无依赖的包最先初始化)
- 包作用域的常量(常量优先于变量)
- 包作用域的变量
- 包的
init()
函数 main()
函数
特点:
- 同一个包可以有多个
init()
函数 - 同一个源文件可以有多个
init()
函数 init()
函数没有入参和返回值- 不能被其他函数调用
- 同一个包内多个
init()
函数的执行顺序不作保证
// 执行顺序:import -> const -> var -> init() -> main()
1.15 2 个 interface 可以比较吗?
可以比较,但需满足以下条件:
- 两个 interface 均等于 nil
- 动态类型相同,且对应的动态值相等
type Stu struct {Name string
}type StuInt interface{}func main() {var stu1, stu2 StuInt = &Stu{"Tom"}, &Stu{"Tom"}var stu3, stu4 StuInt = Stu{"Tom"}, Stu{"Tom"}fmt.Println(stu1 == stu2) // false,指针地址不同fmt.Println(stu3 == stu4) // true,结构体值相同
}
1.16 2 个 nil 可能不相等吗?
可能不相等:
var p *int = nil
var i interface{} = nilif p == i {fmt.Println("Equal")
} else {fmt.Println("Not Equal") // 输出这个
}
总结: 两个 nil 只有在类型相同时才相等。
1.17 Go 语言函数传参是值类型还是引用类型?
Go 语言中只存在值传递:
- 要么是值的副本
- 要么是指针的副本
注意区分:
- 值传递 vs 引用传递:传递机制
- 值类型 vs 引用类型:数据类型特性
func modifySlice(s []int) {s[0] = 100 // 会影响外部,因为底层数组共享s = append(s, 200) // 不会影响外部,因为发生了扩容
}func modifyPtr(s *[]int) {(*s)[0] = 100 // 会影响外部*s = append(*s, 200) // 会影响外部
}
1.18 如何知道一个对象是分配在栈上还是堆上?
逃逸分析:
go build -gcflags '-m -m -l' xxx.go
逃逸的可能情况:
- 变量大小不确定
- 变量类型不确定
- 变量分配的内存超过用户栈最大值
- 暴露给了外部指针
- 闭包引用外部变量
示例:
// 栈分配
func stackAlloc() int {x := 10 // 可能在栈上分配return x
}// 堆分配
func heapAlloc() *int {x := 10 // 逃逸到堆上return &x
}
1.19 Go语言的多返回值是如何实现的?
实现机制:
- 函数调用时,编译器计算所有返回值的总大小
- 在调用方栈帧上预留连续内存空间
- 函数执行 return 时,将返回值复制到预留空间
- 调用方直接从自己的栈帧获取返回值
func multiReturn() (int, string, error) {return 1, "hello", nil
}// 底层类似:
// 调用方预留 [int, string, error] 的空间
// 被调用方将值复制到该空间
// 调用方直接读取
1.20 Go语言中"_"的作用
1. 忽略多返回值
func getValues() (int, string) {return 1, "hello"
}func main() {num, _ := getValues() // 忽略字符串返回值fmt.Println(num)
}
2. 匿名导入包
import ("fmt"_ "net/http/pprof" // 只执行init函数,注册profiling接口
)func main() {fmt.Println("Application started. Profiling tools are likely registered.")
}
3. 在循环中忽略索引或值
for _, value := range slice {fmt.Println(value)
}for index, _ := range slice {fmt.Println(index)
}
1.21 Go语言普通指针和unsafe.Pointer有什么区别?
普通指针:
- 有明确的类型信息(
*int
、*string
) - 编译器进行类型检查
- 受垃圾回收跟踪
- 不同类型指针不能直接转换
unsafe.Pointer:
- 通用指针类型,类似 C 的
void*
- 绕过 Go 的类型系统
- 可与任意类型指针相互转换
- 可与 uintptr 进行转换来做指针运算
- 仍受 GC 跟踪
var x int = 10
var p *int = &x// 普通指针转换(编译错误)
// var f *float64 = (*float64)(p)// 使用 unsafe.Pointer 转换
var f *float64 = (*float64)(unsafe.Pointer(p))
1.22 unsafe.Pointer与uintptr有什么区别和联系
联系:
- 可以相互转换
- 是 Go 中唯一合法的指针运算方式
区别:
unsafe.Pointer
:会被 GC 跟踪,有 GC 保护uintptr
:普通整数,GC 不知道其指向,无 GC 保护