基础语法¶
结构体能不能比较¶
在 Go 语言中,结构体可以进行比较,但有一些注意事项。
-
可比较的结构体:如果结构体的所有字段都是可比较的,那么结构体本身也是可比较的。这意味着你可以使用
==和!=运算符来比较两个结构体变量是否具有相同的值。 -
不可比较的结构体:如果结构体包含不可比较的字段(例如 map 或 slice),则不能直接比较这些结构体。尝试这样做会在编译时导致错误。
new 和 make 的区别¶
在 Go 语言中,new 和 make 都是创建值的内建函数,但它们的用途和行为是有所不同的
-
用途:
new用于为任何类型分配内存,并返回该类型的零值的指针。make用于创建 slice、map 和 channel。
-
返回值:
new(T)返回一个指向类型T的新分配的零值的指针,返回的值是*T类型。make(T)返回一个已初始化的类型T的值,这意味着它返回的是相同的类型,不是指针。
-
零值和初始化:
new分配内存并返回指向该内存的指针,这块内存被初始化为类型的零值。make创建一个具有非零值的值。例如,创建一个切片时,它将为切片分配底层数组并准备好切片的长度和容量。
-
用法示例:
- 使用
new: - 使用
make:
- 使用
切片和数组的区别¶
在 Go 语言中,切片(slice)和数组(array)都是处理序列数据的重要数据结构
-
大小固定性:
- 数组:当定义数组时,需要指定其大小,而且该大小在整个生命周期中是固定的。一旦数组被定义,不能更改其大小。
- 切片:切片的大小是动态的。你可以向切片追加元素,使其增长,但内部它是通过重新分配一个更大的底层数组来实现的。
-
声明方式:
- 数组:指定其长度。
- 切片:不指定长度。
-
长度与容量:
- 数组:长度就是其声明时指定的大小。
- 切片:具有长度和容量两个属性。长度是切片中当前元素的数量,而容量是底层数组中的元素数量。由于切片是引用底层数组的,所以其容量可能大于其长度。
-
底层实现:
- 数组:是一个连续的内存块,由固定数目的元素组成。
- 切片:是一个引用类型,它有三个关键属性:指向底层数组的指针、长度和容量。
-
函数参数传递:
- 数组:作为函数参数传递时,是值传递,函数内对数组的修改不会影响原数组。
- 切片:作为函数参数传递时,是引用传递。函数内对切片的修改会影响原切片。
切片是如何扩容的?¶
-
扩容触发:当你使用
append向切片添加元素,并且切片的当前容量不足以容纳新的元素时,扩容会被触发。 -
扩容策略:
-
对于容量小于 256 的切片,新容量通常是当前容量的两倍。
-
对于容量大于或等于 256 的切片,\(\rm{newcap = oldcap+(oldcap+3 \times 256)/4}\)
-
但是,考虑到内存对齐,新容量会比上面的公式计算出来的容量更大
-
以上是 Go 1.18 及其之后版本的扩容策略,在旧版本中,扩容阈值和增长因子会有变化
-
-
扩容实现:创建一个新的底层数组、复制旧的数据,然后更新切片的引用以指向新数组。无其他引用的旧底层数组将被垃圾收集。
-
关于底层数组的共享:当从一个切片创建另一个切片时,它们可能会共享同一个底层数组。因此,在扩容之前,对其中一个切片的修改可能会影响到另一个切片。但一旦触发扩容,切片将引用一个新的底层数组,此时原切片和新切片就不再共享数据了。
race 检测是什么?¶
为了检测数据竞争,Go 提供了一个工具叫做 race detector。
以下是如何使用 race detector 的简要指南:
-
编译/运行代码时使用
-race选项:当编译、测试或运行你的程序时,加上
-race选项。或
-
分析输出:
如果
race detector检测到数据竞争,它将在标准输出中打印相关的警告,包括竞争发生的位置和涉及的 goroutines。 -
解决数据竞争:
如果你发现了数据竞争,通常你需要引入同步原语,例如
sync.Mutex或sync.RWMutex,来保护对数据的访问。另一种方法是使用并发安全的数据结构。示例:下面是一个简单的例子,展示了一个并发访问
map的数据竞争。package main import ( "fmt" "sync" ) func main() { var wg sync.WaitGroup m := make(map[int]int) wg.Add(2) go func() { for i := 0; i < 1000; i++ { m[i] = i } wg.Done() }() go func() { for i := 0; i < 1000; i++ { fmt.Println(m[i]) } wg.Done() }() wg.Wait() }如果你使用
race detector运行上面的代码,你会看到一个关于数据竞争的警告。
defer 的使用场景和原理?¶
使用场景
defer 语句在 Go 语言中用于确保函数调用在正常执行路径(包括出现错误时)结束后能被执行。主要使用场景包括:
-
资源清理:如关闭文件句柄、数据库连接、网络连接等。通过
defer确保资源无论如何都会被释放,防止资源泄露。 -
锁的释放:在处理并发时,确保互斥锁(mutex)在函数结束时被释放。
-
延迟执行代码:对于需要在函数结束时执行的代码,如日志记录、发送通知、错误处理等。
-
复杂逻辑的清理代码:简化有多个返回点的函数的清理代码。
原理
defer 的工作原理和执行机制如下:
-
后进先出(LIFO)顺序:
defer语句遵循 LIFO 顺序。最后一个被defer的语句将首先执行。 -
参数立即求值:在
defer语句被执行时,所有函数参数都会被立即求值。但是defer语句中的函数本身直到包含它的函数执行结束前才被调用。 -
函数作用域内有效:
defer语句仅在定义它的函数的作用域内有效。 -
返回值影响:如果在
defer语句中使用了命名返回值,它可以修改这些返回值。 -
panic处理:
defer语句即使在发生 panic 时也会执行,因此它们常用于恢复(recover)操作。 -
执行时机:
defer语句在包含它的函数执行的 return 语句之后、返回值返回给调用者之前执行。
defer 的这些特性使得它在资源管理和错误处理方面非常有用,帮助编写清晰和可维护的代码。
如果我要在 defer 里面修改 return 里面的值呢?这时怎么写?¶
如果你想在 defer 语句中修改函数的返回值,你可以使用命名返回值。在函数声明中为返回值命名后,这个命名变量在整个函数范围内都是有效的,包括 defer 语句中。
这里有个例子来展示如何在 defer 中修改返回值:
func example() (result int) {
defer func() {
// 修改返回值
result++
}()
// 设置返回值
result = 3
return // 这里实际返回的将是4
}
func main() {
fmt.Println(example()) // 输出:4
}
在这个例子中,函数 example 有一个命名返回值 result。在 defer 函数中,我们增加了 result 的值。当 example 函数返回时,由于 defer 已经修改了 result 的值,所以实际返回的值是修改后的值。