赞
踩
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
// 如果达到了最大的负载因子或者有太多的溢出桶
// 或是是已经在扩容中
if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
hashGrow(t, h)
goto again // Growing the table invalidates everything, so try again
}
}
判断负载因子超过 6.5:[golang笔记——map底层原理]( )
func overLoadFactor(count int, B uint8) bool {
return count > bucketCnt && uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen)
}
判断overflow buckets 太多:[golang笔记——map底层原理]( )
func tooManyOverflowBuckets(noverflow uint16, B uint8) bool {
// If the threshold is too low, we do extraneous work.
// If the threshold is too high, maps that grow and shrink can hold on to lots of unused memory.
// “too many” means (approximately) as many overflow buckets as regular buckets.
// See incrnoverflow for more details.
if B > 15 {
B = 15
}
// The compiler doesn’t see here that B < 16; mask B to generate shorter shift code.
return noverflow >= uint16(1)<<(B&15)
}
#### 5.4.3 map扩容的类型:翻倍扩容、等量扩容 map的两个扩容的时机,都会发生扩容。但是扩容的策略并不相同,毕竟两种条件应对的场景不同。`但map扩容采用的都是渐进式,桶被操作(增删改)时才会重新分配。` [Golang Map 底层实现]( ) ![map扩容规则](https://img-blog.csdnimg.cn/62f806d313c246759b4b62f6afe857ca.png#pic_center) 1. `翻倍扩容`:针对的是 达到最大的负载因子 的情况,扩容后桶的数量为原来的两倍。[Golang源码探究 —— map]( ) 对于达到最大的负载因子的扩容,它是因为元素太多,而bucket数量太少,解决办法很简单:将B加 1,bucket 最大数量(2^ B)直接变成原来bucket数量的2倍。于是,就有新老bucket了。 **注意:** 这时候元素都在老bucket里,还没迁移到新的bucket来。而且,新bucket只是最大数量变为原来最大数量(2^ B)的 2 倍(2^B \* 2)。[golang笔记——map底层原理]( ) 2. `等量扩容`:针对的是溢出桶的数量太多的情况,溢出桶太多了,导致查询效率低。扩容时,桶的数量不增加。[Golang源码探究 —— map]( ) 对于溢出桶的数量太多的扩容,其实元素没那么多,但是overflow bucket数特别多,说明很多bucket都没装满。解决办法就是开辟一个新bucket空间,将老bucket中的元素移动到新bucket,使得同一个bucket 中的key排列地更紧密。这样,原来在overflow bucket中的key可以移动到bucket中来。节省空间,提高bucket利用率,map的查找和插入效率自然就会提升。[golang笔记——map底层原理]( ) #### 5.4.4 map扩容的步骤 [Golang源码探究 —— map]( ) `步骤一:` 1. 创建一组新桶。 2. oldbuckets指向原有的桶数组。 3. buckets指向新的桶的数组。 4. map标记为扩容状态。 `步骤二:`迁移数据 1. 将所有的数据从旧桶驱逐到新桶。 2. 采用渐进式驱逐。 3. 每次操作一个旧桶时(插入、删除数据),将旧桶数据驱逐到新桶。 4. 读取时不进行驱逐,只判断读取新桶还是旧桶。 `步骤三:`所有旧桶驱逐完成后,回收所有旧桶(oldbuckets)。 #### 5.4.5 map为什么采用渐进式扩容 [golang笔记——map底层原理]( ) 由于map扩容需要将原有的key/value重新搬迁到新的内存地址,如果有大量的key/value需要搬迁,会非常影响性能。因此Go map的扩容采取了一种称为“渐进式”地方式,每次最多只会搬迁2个bucket。 #### 5.4.6 翻倍扩容、等量扩容中Key的变化 **翻倍扩容(达到最大的负载因子):**【可能会变,也可能不会变】因为新的buckets数量是之前的一倍,所以在迁移时要重新计算 key的哈希,才能决定它到底落在哪个bucket。例如,原来 B = 5,计算出key的哈希后,只用看它的低 5 位,就能决定它落在哪个bucket。扩容后,B变成了 6,因此需要多看一位,它的低 6 位决定key落在哪个bucket。因此,某个key在搬迁前后bucket序号可能和原来相等,也可能是相比原来加上 2^B(原来的 B 值),取决于hash值 第6位bit位是 0 还是 1。[golang笔记——map底层原理]( ) **等量扩容(溢出桶的数量太多):**【可能会变,也可能不会变】从老的buckets搬迁到新的buckets,由于bucktes数量不变,因此可以按序号来搬,比如原来在0号bucktes,到新的地方后,仍然放在0号buckets。【如果迁移后是紧密的按顺序排列,则不变;如果不按顺序排列,会变】[golang笔记——map底层原理]( ) ### 5.5 map为什么是无序的 #### 5.5.1 map不扩容的时候for循环取值,为什么每次取到的都是无序 参考1:[为什么说Go的Map是无序的?]( ) 首先是`For ... Range ...` 遍历Map的索引的起点是随机的。 其次,往map中存入时就不是按顺序存储的,所以是无序的。 --- 翻倍扩容和等量扩容都可能会发生无序的情况,原因看 `5.3.6 翻倍扩容、等量扩容中Key的变化`。 [golang笔记——map底层原理]( ) map在扩容后,会发生key的搬迁,原来落在同一个bucket中的key,搬迁后,有些key就要远走高飞了(bucket序号加上了 2^B)。而遍历的过程,就是按顺序遍历bucket,同时按顺序遍历bucket中的key。搬迁后,key的位置发生了重大的变化,有些 key飞上高枝,有些key则原地不动。这样,遍历map的结果就不可能按原来的顺序了。 当我们在遍历go中的map时,并不是固定地从0号bucket开始遍历,每次都是从一个随机值序号的bucket开始遍历,并且是从这个 bucket的一个随机序号的cell开始遍历。这样,即使你是一个写死的map,仅仅只是遍历它,也不太可能会返回一个固定序列的 key/value对了。 ### 5.6 float类型是否可以作为map的key [golang笔记——map底层原理]( ) 从语法上看,是可以的。Go 语言中只要是可比较的类型都可以作为 key。除开 slice,map,functions 这几种类型,其他类型都是 OK 的。具体包括:布尔值、数字、字符串、指针、通道、接口类型、结构体、只包含上述类型的数组。这些类型的共同特征是支持 == 和 != 操作符,k1 == k2 时,可认为 k1 和 k2 是同一个 key。如果是结构体,只有 hash 后的值相等以及字面值相等,才被认为是相同的 key。很多字面值相等的,hash出来的值不一定相等,比如引用。 `float 型可以作为 key,但是由于精度的问题,会导致一些诡异的问题,慎用之。` ### 5.7 map可以遍历的同时删除吗 [golang笔记——map底层原理]( ) map 并不是一个线程安全的数据结构。多个协程同时读写同时读写一个 map,如果被检测到,会直接 panic。 如果在同一个协程内边遍历边删除,并不会检测到同时读写,理论上是可以这样做的。但是,遍历的结果就可能不会是相同的了,有可能结果遍历结果集中包含了删除的 key,也有可能不包含,这取决于删除 key 的时间:是在遍历到 key 所在的 bucket 时刻前或者后。 如果想要并发安全的读写,可以通过读写锁来解决:sync.RWMutex。 读之前调用 RLock() 函数,读完之后调用 RUnlock() 函数解锁;写之前调用 Lock() 函数,写完之后,调用 Unlock() 解锁。 ### 5.8 可以对map元素取地址吗 [golang笔记——map底层原理]( ) 无法对 map 的 key 或 value 进行取址,将无法通过编译。 如果通过其他 hack 的方式,例如 unsafe.Pointer 等获取到了 key 或 value 的地址,也不能长期持有,因为一旦发生扩容,key 和 value 的位置就会改变,之前保存的地址也就失效了。 ### 5.9 如何比较两个map是否相等 [golang笔记——map底层原理]( ) 1. 都为 nil。 2. 非空、长度相等,指向同一个 map 实体对象。 3. 相应的 key 指向的 value “深度”相等 直接将使用 map1 == map2 是错误的。这种写法只能比较 map 是否为 nil。 因此只能是遍历map 的每个元素,比较元素是否都是深度相等。 ### 5.10 map是线程安全的吗 [golang笔记——map底层原理]( ) 不安全,只读是线程安全的,主要是不支持并发写操作的,原因是 map 写操作不是并发安全的,当尝试多个 Goroutine 操作同一个 map,会产生报错:`fatal error: concurrent map writes`。所以map适用于读多写少的场景。 `解决办法`:要么加锁,要么使用sync包中提供了并发安全的map,也就是sync.Map,其内部实现上已经做了互斥处理。 ### 5.11 map底层是hash,它是如何解决冲突的 golang的map用的是hashmap,是使用数组+链表的形式实现的,使用`拉链法`消除hash冲突。拉链法见:`5.2.2 拉链法(map使用的方式)` ### 5.12 map如何判断是否并发写的 参考1:<https://www.jianshu.com/p/1132055d708b> map是检查是否有另外线程修改`h.flag`来判断,是否有并发问题。
// 在更新map的函数里检查并发写
if h.flags&hashWriting == 0 {
throw(“concurrent map writes”)
}
// 在读map的函数里检查是否有并发写
if h.flags&hashWriting != 0 {
throw(“concurrent map read and map write”)
}
### 5.13 map并发读写会panic吗 参考1:<http://c.biancheng.net/view/34.html> map 在并发情况下,只读是线程安全的,同时读写是线程不安全的。会报`panic:fatal error: concurrent map read and map write`,因为Go语言原生的map并不是并发安全的,对它进行并发读写操作的时候,需要加锁。 ### 5.14 map遍历是否有序 参考1:[golang对map排序]( ) golang中map元素是随机无序的,所以在对map range遍历的时候也是随机的,如果想按顺序读取map中的值,可以结合切片来实现。 ### 5.15 map怎么变得有序 如果想按顺序读取map中的值,可以结合切片来实现。 ### 5.16 多个协程读写map的panic可以被捕获吗 参考1:<https://www.cnblogs.com/wuchangblog/p/16393070.html> 不能,每个协程只能捕获到自己的 panic 不能捕获其它协程。 ## 6 sync.Map sync.Map是并发安全的。底层通过分离读写map和原子指令来实现读的近似无锁,并通过延迟更新的方式来保证读的无锁化。 ### 6.1 sync.Map的基本操作 **`sync.Map`特性:** 1. 无须初始化,直接声明即可使用。 2. sync.Map不能使用普通map的方法进行读写操作,而是使用sync.Map自己的方法进行操作,Store表示存储,Load表示读取,Delete表示删除。 3. 使用Range配合一个回调函数进行遍历操作,通过回调函数返回内部遍历出来的值,Range参数中回调函数的返回值在需要继续遍历时,需要返回true,终止遍历时,返回false。 **`sync.Map`的基本操作的完整代码:**
package main
import (
“fmt”
“sync”
)
func main() {
//1、初始化
var sMap sync.Map
//2、插入数据 sMap.Store(1,"a") sMap.Store("AA",10) sMap.Store("BB",20) sMap.Store(3,"CC") //3、访问数据 fmt.Println("Load方法") //Load:①如果待查找的key存在,则返回key对应的value,true; lv1,ok1 := sMap.Load(1) fmt.Println(ok1,lv1) //输出结果:true a //Load:②如果待查找的key不存在,则返回nil,false lv2,ok2 := sMap.Load(2) fmt.Println(ok2,lv2) //输出结果:false <nil> fmt.Println() fmt.Println("LoadOrStore方法") //LoadOrStore:①如果待查找的key存在,则返回key对应的value,true; losv1,ok1 := sMap.LoadOrStore(1,"aaa") fmt.Println(ok1,losv1) //输出结果:true a //LoadOrStore:②如果待查找的key不存在,则返回添加的value,false losv2,ok2 := sMap.LoadOrStore(2,"bbb") fmt.Println(ok2,losv2) //输出结果:false bbb fmt.Println() fmt.Println("LoadAndDelete方法") //LoadAndDelete:①如果待查找的key存在,则返回key对应的value,true,同时删除该key-value; ladv1,ok1 := sMap.LoadAndDelete(1) fmt.Println(ok1,ladv1) //输出结果:true a //LoadAndDelete:②如果待查找的key不存在,则返回nil,false ladv2,ok2 := sMap.LoadAndDelete(1) fmt.Println(ok2,ladv2) //输出结果:false <nil> //4、删除 fmt.Println() fmt.Println("Delete方法") sMap.Delete(2) fmt.Println() fmt.Println("Range方法") // 5、遍历所有sync.Map中的键值对 sMap.Range(func(k, v interface{}) bool { fmt.Println("k-v:", k, v) return true })
}
#### 6.1.1 sync.Map初始化
sync.Map无须初始化,直接声明即可使用。
var sMap sync.Map
#### 6.1.2 sync.Map插入数据
sync.Map插入数据使用自带的Store(key,value)。[源码解读 Golang 的 sync.Map 实现原理]( ) 有对`Store`的源码分析。
sMap.Store(1,"a")
sMap.Store("AA",10)
注意:`Store(key, value interface{})`参数都是interface{}类型,所以同一个sync.Map能存储不同类型的数据。`源码:`
func (m *Map) Store(key, value interface{}) {
}
#### 6.1.3 访问sync.Map中的数据
sync.Map访问有三个方法:Load()、LoadOrStore()、LoadAndDelete()
1. `Load(key interface{}) (value interface{}, ok bool)` [源码解读 Golang 的 sync.Map 实现原理]( ) 有对 `Load` 的源码分析。
* 如果待查找的key存在,则返回key对应的value,true;
lv1,ok1 := sMap.Load(1)
fmt.Println(ok1,lv1) //输出结果:true a
* 如果待查找的key不存在,则返回nil,false;
lv2,ok2 := sMap.Load(2)
fmt.Println(ok2,lv2) //输出结果:false <nil>
2. `LoadOrStore(key, value interface{}) (actual interface{}, loaded bool)`
* 如果待查找的key存在,则返回key对应的value,true,不会修改原来key对应的value;
losv1,ok1 := sMap.LoadOrStore(1,"aaa")
fmt.Println(ok1,losv1) //输出结果:true a
* 如果待查找的key不存在,则返回添加的value,false;
losv2,ok2 := sMap.LoadOrStore(2,"bbb")
fmt.Println(ok2,losv2) //输出结果:false bbb
3. `LoadAndDelete(key interface{}) (value interface{}, loaded bool)`
* 如果待查找的key存在,则返回key对应的value,true,同时删除该key-value;
ladv1,ok1 := sMap.LoadAndDelete(1)
fmt.Println(ok1,ladv1) //输出结果:true a
* 如果待查找的key不存在,则返回nil,false;
ladv2,ok2 := sMap.LoadAndDelete(1)
fmt.Println(ok2,ladv2) //输出结果:false <nil>
#### 6.1.4 删除sync.Map中的数据
sync.Map删除用 `Delete(key interface{})`,查看源码会发现它是调用的`LoadAndDelete(key)`最终来实现的。[源码解读 Golang 的sync.Map实现原理]( ) 有对`Delete`的源码分析。
`源码:`
func (m *Map) Delete(key interface{}) {
m.LoadAndDelete(key)
}
#### 6.1.5 清空sync.Map中的数据
同map一样,Go语言也没有为sync.Map提供任何清空所有元素的函数、方法,清空sync.Map的唯一办法就是重新声明一个新的sync.Map。
#### 6.1.6 遍历sync.Map
sync.Map使用`Range`配合一个回调函数进行遍历操作,通过回调函数返回内部遍历出来的值,Range参数中回调函数的返回值在需要继续迭代遍历时,返回 true,终止迭代遍历时,返回 false。
sMap.Range(func(k, v interface{}) bool {
fmt.Println("k-v:", k, v)
return true
})
### 6.2 sync.Map的数据结构 #### 6.2.1 sync.Map底层是如何保证线程安全(实现原理) **sync.Map 的实现原理可概括为:** 1. 通过read和dirty两个字段将读写分离,读取的数据在只读字段read上,写入的数据则存在dirty字段上。 2. 读取时会先查询read,read中不存在时,再查询dirty,写入时则只写入dirty。 3. 读取read并不需要加锁,因为read只负责读,而读或写dirty都需要加锁。 4. 另外有misses字段来统计read被穿透的次数(被穿透指当从Map中读取entry的时候,如果read中不包含这个entry,需要读dirty的情况),超过一定次数则将dirty晋升为read 。(`保证读写一致`) 5. 延迟删除,删除一个key值时只是打标记,只有在将dirty晋升为read后的时候才清理数据。对于删除数据则直接通过标记来延迟删除。 #### 6.2.2 sync.Map的数据结构 参考1:[源码解读 Golang 的 sync.Map 实现原理]( ) 参考2:[Golang的Map并发性能以及原理分析]( ) sync.Map是在`sync/map.go`:
type Map struct {
mu Mutex
read atomic.Pointer[readOnly]
dirty map[any]*entry
misses int
}
**`sync.Map`各字段解读:** 1. `mu`:互斥锁,保护dirty字段,当涉及到dirty数据的操作的时候,需要使用这个锁。 2. `read`:只读的数据,**实际数据类型为readOnly**,也是一个map,因为只读,所以不会有读写冲突。实际上,实际也会更新read的entries,如果entry是未删除的(unexpunged),并不需要加锁。如果entry已经被删除了,需要加锁,以便更新dirty数据。 3. `dirty`:dirty中的数据除了包含当前的entries,它也包含最新的entries(包括read中未删除的数据,虽有冗余,但是提升dirty字段为read的时候非常快,不用一个一个的复制,而是直接将这个数据结构作为read字段的一部分),有些数据还可能没有移动到read字段中`(即直接将dirty晋升为read)`。 * 对于dirty的操作需要加锁,因为对它的操作可能会有读写竞争。 * 当dirty为空的时候,比如初始化或者刚提升完,下一次的写操作会复制read字段中未删除的数据到这个数据中。 4. `misses`:当从Map中读取entry的时候,如果read中不包含这个entry,会尝试从dirty中读取,这个时候会将misses加一,当misses累积到dirty的长度的时候, 就会将dirty晋升为read,避免从dirty中miss太多次。因为操作dirty需要加锁。【`保证读写一致`】 `readOnly`结构体:
type readOnly struct {
m map[interface{}]*entry
amended bool
}
**`readOnly`各字段解读:**
`m`:内建map,m的value的类型为`*entry`。
`amended`:用于判断`dirty`里是否存在`read`里没有的`key`,通过该字段决定是否加锁读`dirty`,如果有则为true。
`readOnly.m`和`Map.dirty`存储的值类型是\*entry,它包含一个指针p,指向用户存储的value值。
`entry` 数据结构则用于存储sync.Map中值的指针:
type entry struct {
p unsafe.Pointer // 等同于 *interface{}
}
当p指针指向expunged这个指针的时候,则表明该元素被删除,但不会立即从map中删除,如果在未删除之前又重新赋值则会重新使用该元素。 **`entry`各字段解读:** `p`:指向用户存储的value值,p有三种状态。 * nil: 键值已经被删除,且m.dirty == nil。 * expunged: 键值已经被删除,但是m.dirty!=nil且m.dirty不存在该键值(expunged 实际是空接口指针)。 * 除以上情况,则键值对存在,存在于m.read中,如果m.dirty!=nil则也存在于m.dirty。 #### 6.2.3 read map与dirty map的关系 参考1:[Golang的Map并发性能以及原理分析]( ) ![read map与dirty map的关系](https://img-blog.csdnimg.cn/fcc8c95b7cc2438187abff21d1c96ec5.jpeg#pic_center) 从图中可以看出,`read map`和`dirty map`中含有相同的一部分`entry`,我们称作是`normal entries`,是双方共享的。状态是p的值为`nil`和`unexpunged`时。 但是`read map`中含有一部分`entry`是不属于`dirty map`的,而这部分`entry`就是状态为`expunged`状态的`entry`。而`dirty map`中有一部分`entry` 也是不属于`read map`的,而这部分其实是来自`Store`操作形成的(也就是新增的 `entry`),换句话说就是新增的`entry`是出现在`dirty map`中的。 读取数据时首先从`m.read`中读取,不存在的情况下,并且`m.dirty`中有新数据,对`m.dirty`加锁,然后从`m.dirty`中读取。 #### 6.2.4 read map、dirty map的作用 参考1:[Golang的Map并发性能以及原理分析]( ) `read map`:是用来进行lock free操作的(其实可以读写,但是不能做删除操作,因为一旦做了删除操作,就不是线程安全的了,也就无法 lock free)。 `dirty map`:是用来在无法进行lock free操作的情况下,需要lock来做一些更新工作的对象。 ### 6.3 sync.Map的缺陷 参考1:[Golang的Map并发性能以及原理分析]( ) 当需要不停地新增和删除的时候,会导致dirty map不停地更新,甚至在misses过多之后,导致dirty成为nil,并进入重建的过程,所以`sync.Map适用于读多写少的场景`。 ### 6.4 sync.Map与map的区别 是否支持多协程并发安全。 ### 6.5 sync.Map的使用场景 参考1:[sync.Map详解]( ) `sync.Map 适用于读多写少的场景。`对于写多的场景,会导致不断地从dirty map中读取,导致dirty map晋升为read map,这是一个 O(N) 的操作,会进一步降低性能。 ## 7 interface接口 ### 7.1 interface的数据结构 接口的底层实现结构有两个结构体`iface`和`eface`,区别在于`iface`类型的接口包含方法,而`eface`则是不包含任何方法的空接口:`interface{}`。这两个结构体都在`runtime/runtime2.go`中。([Golang之接口底层分析]( )) #### 7.1.1 接口之iface 参考1:[Go interface的底层实现研究(1)]( ) `iface`结构体,是在`runtime/runtime2.go`中,它的所有字段如下:
type iface struct {
tab *itab
data unsafe.Pointer
}
**`iface` 各字段解读:**
1. `tab` :指针类型,指向一个itab实体,它表示接口的类型以及赋给这个接口的实体类型。
2. `data` :则指向接口具体的值,一般而言是一个指向堆内存的指针。
`itab`结构体,是在`runtime/runtime2.go`中,它的所有字段如下:
type itab struct {
inter *interfacetype
_type *_type
hash uint32 // copy of _type.hash. Used for type switches.
_ [4]byte
fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}
**`itab`各字段解读:** 1. `inter`:接口自身定义的类型信息,用于定位到具体`interface`类型。 2. `_type`:接口实际指向值的类型信息,即实际对象类型,用于定义具体`interface`类型; 3. `hash`:`_type.hash`的拷贝,是类型的哈希值,用于快速查询和判断目标类型和接口中类型是否一致。 4. `fun`:动态数组,接口方法实现列表(方法集),即函数地址列表,按字典序排序,如果数组中的内容为空表示`_type`没有实现`inter`接口。 `itab.inter`是`interface`的类型元数据,它里面记录了这个接口类型的描述信息,接口要求的方法列表就记录在`interfacetype.mhdr`里。 `interfacetype`结构体,是在`runtime/type.go`中,它的所有字段如下:
type interfacetype struct {
typ _type
pkgpath name
mhdr []imethod
}
**`interfacetype`各字段解读:** 1. `typ`:接口的信息。 2. `pkgpath`:接口的包路径。 3. `mhdr`:接口要求的方法列表。 **`iface`结构体详解:** `tab._type`就是接口的动态类型,也就是被赋给接口类型的那个变量的类型元数据。`itab`中的`_type`和`iface`中的`data`能简要描述一个变量。`_type`是这个变量对应的类型,`data`是这个变量的值。 `itab.fun`记录的是动态类型实现的那些接口要求的方法的地址,是从方法元数据中拷贝来的,为的是快速定位到方法。如果`itab._type`对应的类型没有实现这个接口,则`itab.fun[0]=0`,这在类型断言时会用到。 当`fun[0]`为0时,说明`_type`并没有实现该接口,当有实现接口时,`fun`存放了第一个接口方法的地址,其他方法依次往下存放,这里就简单用空间换时间,其实方法都在`_type`字段中能找到,实际在这记录下,每次调用的时候就不用动态查找了。 #### 7.1.2 接口之eface 参考1:[Go interface的底层实现研究(1)]( ) `eface` 结构体,是在`runtime/runtime2.go`中,它的所有字段如下:
type eface struct {
_type *_type
data unsafe.Pointer
}
**`eface` 各字段解读:**
1. `_type`:类型信息。
2. `data`:数据信息,指向数据指针。
`_type`结构体,是在`runtime/type.go`中,它的所有字段如下:
type _type struct {
size uintptr
ptrdata uintptr // size of memory prefix holding all pointers
hash uint32
tflag tflag
align uint8
fieldAlign uint8
kind uint8
// function for comparing objects of this type
// (ptr to object A, ptr to object B) -> ==?
equal func(unsafe.Pointer, unsafe.Pointer) bool
// gcdata stores the GC type data for the garbage collector.
// If the KindGCProg bit is set in kind, gcdata is a GC program.
// Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
gcdata *byte
str nameOff
ptrToThis typeOff
}
**`_type` 各字段解读:** 1. `size`:类型占用内存大小。 2. `ptrdata`:包含所有指针的内存前缀大小。 3. `hash`:类型hash。 4. `tflag`:标记位,主要用于反射。 5. `align`:对齐字节信息。 6. `fieldAlign`:当前结构字段的对齐字节数。 7. `kind`:基础类型枚举值。 8. `equal`:比较两个形参对应对象的类型是否相等。 9. `gcdata`:GC类型的数据。 10. `str`:类型名称字符串在二进制文件段中的偏移量。 11. `ptrToThis`:类型元信息指针在二进制文件段中的偏移量。 **重点说明:** 1. kind:这个字段描述的是如何解析基础类型。在Go语言中,基础类型是一个枚举常量,有26个基础类型,如下。枚举值通过`kindMask`取出特殊标记位。
const (
kindBool = 1 + iota
kindInt
kindInt8
kindInt16
kindInt32
kindInt64
kindUint
kindUint8
kindUint16
kindUint32
kindUint64
kindUintptr
kindFloat32
kindFloat64
kindComplex64
kindComplex128
kindArray
kindChan
kindFunc
kindInterface
kindMap
kindPtr
kindSlice
kindString
kindStruct
kindUnsafePointer
kindDirectIface = 1 << 5
kindGCProg = 1 << 6
kindMask = (1 << 5) - 1
)
2. `str`和`ptrToThis`,对应的类型是`nameoff` 和`typeOff`。分表表示`name`和`type`针对最终输出文件所在段内的偏移量。在编译的链接步骤中,链接器将各个`.o`文件中的段合并到输出文件,会进行段合并,有的放入`.text`段,有的放入`.data`段,有的放入`.bss`段。`nameoff`和`typeoff`就是记录了对应段的偏移量。 ### 7.2 接口的nil判断(interface可以和nil比较吗) 参考1:[Go语言接口的nil判断]( ) 答:可以比较,因为`nil`在Go语言中只能被赋值给指针和接口。接口在底层的实现主要考虑`eface`结构体,它有两个部分:`type`和`data`。 **两种情况:** 1. 显式地将`nil`赋值给接口时,接口的`type`和`data`都将为`nil`。此时,接口与`nil`值判断是相等的。 2. 将一个带有类型的`nil`赋值给接口时,只有`data`为`nil`,而`type`不为`nil`,此时,接口与`nil`判断将不相等。 ### 7.3 两个interface可以比较吗 参考1:[golang中接口值(interface)的比较]( ) 这个问题,接口在底层的实现主要考虑`eface` 结构体,它有两个部分:`type`和`data`。`interface`可以使用`==`或`!=`比较。 **2个interface 相等有以下 2 种情况:** 1. 两个interface均等于nil(此时V和T都处于unset状态) 2. 类型T相同,且对应的值V相等。 ## 8 Golang中的Context ### 8.1 Context 简介 参考1:[golang的context]( ) 在`Golang`的`http`包的`Server`中,每一个请求都有一个对应的`goroutine`负责处理,请求处理函数通常会启动额外的`goroutine`去处理,当一个请求被取消或者超时,所有用来处理该请求的goroutine都应该及时退出,这样系统才能释放这些goroutine占用的资源,就不会有大量的goroutine去占用资源。 > > 注意:go1.6及之前版本请使用golang.org/x/net/context。go1.7及之后已移到标准库context。 > > > ### 8.2 Context 原理 参考1:[golang 系列:context 详解]( ) 参考2:[快速掌握 Golang context 包,简单示例]( ) 1. 从`Context`的功能可以看出来,它是用来`传递信息`的。这种传递并不仅仅是将数据塞给被调用者,它还能进行`链式传递`,通过保存父子`Context`关系,不断的迭代遍历来获取数据。 2. 因为`Context`可以链式传递,这就使得`goroutine`之间能够进行链式的信号通知了,从而进而达到自上而下的通知效果。`例如通知所有跟当前context有关系的goroutine进行取消处理。` 3. 因为`Context`的调用是链式的,所以通过`WithCancel`,`WithDeadline`,`WithTimeout`或`WithValue`派生出新的`Context`。当父`Context`被取消时,其派生的所有`Context`都将取消。 4. 通过`context.WithXXX`都将返回新的`Context`和`CancelFunc`。调用`CancelFunc`将取消子代,移除父代对子代的引用,并且停止所有定时器。未能调用`CancelFunc`将泄漏子代,直到父代被取消或定时器触发。`go vet`工具检查所有流程控制路径上使用`CancelFuncs`。 ### 8.3 使用场景 参考1:<https://www.qycn.com/xzx/article/9390.html> **本文中的四种使用场景的分析和相关代码同参考1完全相同。** * RPC调用 * PipeLine:pipeline模式就是流水线模型。 * 超时请求 * HTTP服务器的request互相传递数据 --- **1. RPC调用** 在主goroutine上有4个RPC,RPC2/3/4是并行请求的,我们这里希望在RPC2请求失败之后,直接返回错误,并且让RPC3/4停止继续计算。这个时候,就使用的到Context。 `代码:`
package main
import (
“context”
“sync”
“github.com/pkg/errors”
)
func Rpc(ctx context.Context, url string) error {
result := make(chan int)
err := make(chan error)
go func() { // 进行RPC调用,并且返回是否成功,成功通过result传递成功信息,错误通过error传递错误信息 isSuccess := true if isSuccess { result <- 1 } else { err <- errors.New("some error happen") } }() select { case <- ctx.Done(): // 其他RPC调用调用失败 return ctx.Err() case e := <- err: // 本RPC调用失败,返回错误信息 return e case <- result: // 本RPC调用成功,不返回错误信息 return nil }
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
// RPC1调用 err := Rpc(ctx, "http://rpc\_1\_url") if err != nil { return } wg := sync.WaitGroup{} // RPC2调用 wg.Add(1) go func(){ defer wg.Done() err := Rpc(ctx, "http://rpc\_2\_url") if err != nil { cancel() } }() // RPC3调用 wg.Add(1) go func(){ defer wg.Done() err := Rpc(ctx, "http://rpc\_3\_url") if err != nil { cancel() } }() // RPC4调用 wg.Add(1) go func(){ defer wg.Done() err := Rpc(ctx, "http://rpc\_4\_url") if err != nil { cancel() } }() wg.Wait()
}
这里使用了`waitGroup`来保证main函数在所有RPC调用完成之后才退出。 在Rpc函数中,第一个参数是一个CancelContext,这个Context形象的说,就是一个传话筒,在创建CancelContext的时候,返回了一个听声器(ctx)和话筒(cancel函数)。所有的goroutine都拿着这个听声器(ctx),当主goroutine想要告诉所有goroutine要结束的时候,通过cancel函数把结束的信息告诉给所有的goroutine。当然所有的goroutine都需要内置处理这个听声器结束信号的逻辑(ctx->Done())。我们可以看Rpc函数内部,通过一个select来判断ctx的done和当前的rpc调用哪个先结束。 这个WaitGroup和其中一个RPC调用就通知所有RPC的逻辑,其实有一个包已经帮我们做好了。errorGroup。具体这个errorGroup包的使用可以看这个包的test例子。 有人可能会担心我们这里的cancel()会被多次调用,context包的cancel调用是幂等的。可以放心多次调用。 我们这里不妨品一下,这里的Rpc函数,实际上我们的这个例子里面是一个“阻塞式”的请求,这个请求如果是使用http.Get或者http.Post来实现,实际上Rpc函数的Goroutine结束了,内部的那个实际的http.Get却没有结束。所以,需要理解下,这里的函数最好是“非阻塞”的,比如是http.Do,然后可以通过某种方式进行中断。 比如像这篇文章Cancel http.Request using Context中的这个例子:
func httpRequest(
ctx context.Context,
client *http.Client,
req *http.Request,
respChan chan []byte,
errChan chan error
) {
req = req.WithContext(ctx)
tr := &http.Transport{}
client.Transport = tr
go func() {
resp, err := client.Do(req)
if err != nil {
errChan <- err
}
if resp != nil {
defer resp.Body.Close()
respData, err := ioutil.ReadAll(resp.Body)
if err != nil {
errChan <- err
}
respChan <- respData
} else {
errChan <- errors.New(“HTTP request failed”)
}
}()
for {
select {
case <-ctx.Done():
tr.CancelRequest(req)
errChan <- errors.New(“HTTP request cancelled”)
return
case <-errChan:
tr.CancelRequest(req)
return
}
}
}
它使用了http.Client.Do,然后接收到ctx.Done的时候,通过调用transport.CancelRequest来进行结束。 我们还可以参考net/dail/DialContext。 换而言之,如果希望实现的包是“可中止/可控制”的,那么在包实现的函数里面,最好是能接收一个`Context`函数,并且处理了Context.Done。 **2. PipeLine** `pipeline`模式就是流水线模型,流水线上的几个工人,有n个产品,一个一个产品进行组装。其实pipeline模型的实现和Context并无关系,没有context我们也能用chan实现pipeline模型。但是对于整条流水线的控制,则是需要使用上Context的。这篇文章Pipeline Patterns in Go的例子是非常好的说明。这里就大致对这个代码进行下说明。 runSimplePipeline的流水线工人有三个,lineListSource负责将参数一个个分割进行传输,lineParser负责将字符串处理成int64,sink根据具体的值判断这个数据是否可用。他们所有的返回值基本上都有两个chan,一个用于传递数据,一个用于传递错误。(<-chan string, <-chan error)输入基本上也都有两个值,一个是Context,用于传声控制的,一个是(in <-chan)输入产品的。 我们可以看到,这三个工人的具体函数里面,都使用switch处理了case <-ctx.Done()。这个就是生产线上的命令控制。
func lineParser(ctx context.Context, base int, in <-chan string) (
<-chan int64, <-chan error, error) {
…
go func() {
defer close(out)
defer close(errc)
for line := range in { n, err := strconv.ParseInt(line, base, 64) if err != nil { errc <- err return } select { case out <- n: case <-ctx.Done(): return } } }() return out, errc, nil
}
**3. 超时请求**
我们发送RPC请求的时候,往往希望对这个请求进行一个超时的限制。当一个RPC请求超过10s的请求,自动断开。当然我们使用CancelContext,也能实现这个功能(开启一个新的goroutine,这个goroutine拿着cancel函数,当时间到了,就调用cancel函数)。
鉴于这个需求是非常常见的,context包也实现了这个需求:timerCtx。具体实例化的方法是 WithDeadline 和 WithTimeout。
具体的timerCtx里面的逻辑也就是通过time.AfterFunc来调用ctx.cancel的。
官方的例子:
package main
import (
“context”
“fmt”
“time”
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
select {
case <-time.After(1 \* time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err()) // prints "context deadline exceeded"
}
}
在http的客户端里面加上timeout也是一个常见的办法。
uri := “https://httpbin.org/delay/3”
req, err := http.NewRequest(“GET”, uri, nil)
if err != nil {
log.Fatalf(“http.NewRequest() failed with ‘%s’\n”, err)
}
ctx, _ := context.WithTimeout(context.Background(), time.Millisecond*100)
req = req.WithContext(ctx)
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatalf(“http.DefaultClient.Do() failed with:\n’%s’\n”, err)
}
defer resp.Body.Close()
在http服务端设置一个timeout如何做呢?
package main
import (
“net/http”
“time”
)
func test(w http.ResponseWriter, r *http.Request) {
time.Sleep(20 * time.Second)
w.Write([]byte(“test”))
}
func main() {
http.HandleFunc(“/”, test)
timeoutHandler := http.TimeoutHandler(http.DefaultServeMux, 5 * time.Second, “timeout”)
http.ListenAndServe(“:8080”, timeoutHandler)
}
我们看看TimeoutHandler的内部,本质上也是通过context.WithTimeout来做处理。
func (h *timeoutHandler) ServeHTTP(w ResponseWriter, r *Request) {
…
ctx, cancelCtx = context.WithTimeout(r.Context(), h.dt)
defer cancelCtx()
…
go func() {
…
h.handler.ServeHTTP(tw, r)
}()
select {
…
case <-ctx.Done():
…
}
}
4. HTTP服务器的request互相传递数据
context还提供了valueCtx的数据结构。这个valueCtx最经常使用的场景就是在一个http服务器中,在request中传递一个特定值,比如有一个中间件,做cookie验证,然后把验证后的用户名存放在request中。
我们可以看到,官方的request里面是包含了Context的,并且提供了WithContext的方法进行context的替换。
package main
import (
“net/http”
“context”
)
type FooKey string
var UserName = FooKey(“user-name”)
var UserId = FooKey(“user-id”)
func foo(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), UserId, “1”)
ctx2 := context.WithValue(ctx, UserName, “yejianfeng”)
next(w, r.WithContext(ctx2))
}
}
func GetUserName(context context.Context) string {
if ret, ok := context.Value(UserName).(string); ok {
return ret
}
return “”
}
func GetUserId(context context.Context) string {
if ret, ok := context.Value(UserId).(string); ok {
return ret
}
return “”
}
func test(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("welcome: “))
w.Write([]byte(GetUserId(r.Context())))
w.Write([]byte(” "))
w.Write([]byte(GetUserName(r.Context())))
}
func main() {
http.Handle(“/”, foo(test))
http.ListenAndServe(“:8080”, nil)
}
在使用ValueCtx的时候需要注意一点,这里的key不应该设置成为普通的String或者Int类型,为了防止不同的中间件对这个key的覆盖。最好的情况是每个中间件使用一个自定义的key类型,比如这里的FooKey,而且获取Value的逻辑尽量也抽取出来作为一个函数,放在这个middleware的同包中。这样,就会有效避免不同包设置相同的key的冲突问题了。 ### 8.4 Context使用规则 参考1:[快速掌握 Golang context 包,简单示例]( ) 1. 不要将`Context`放入结构体,相反`context`应该作为第一个参数传入,命名为`ctx`。 `func DoSomething(ctx context.Context,arg Arg)error { // ... use ctx ... }`。 2. 即使函数允许,也不要传入`nil`的`Context`。如果不知道用哪种`Context`,可以使用`context.TODO()`。 3. 使用`context`的`Value`相关方法只应该用于在程序和接口中传递的和请求相关的元数据,不要用它来传递一些可选的参数。 4. 相同的`Context`可以传递给在不同的`goroutine`;**`Context` 是并发安全的**。 5. `context`的`Done()`方法往往需要配合`select { case }`使用,以监听退出。 6. 一旦`context`执行取消动作,所有派生的`context`都会触发取消。 ### 8.5 Context的数据结构 参考1:[快速掌握 Golang context 包,简单示例]( ) 参考2:[golang 系列:context 详解]( ) 参考3:[golang的context]( ) `Context是一个接口`,是在`context/context.go`中,它的所有抽象方法如下:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
**`Context`接口中抽象方法解读:** `Deadline()`:返回截止时间和ok。 * 如果有截止时间的话,到了这个时间点,Context会自动触发Cancel动作,返回对应`deadline`时间,同时ok为`true`是表示设置了截止时间; * 如果没有设置截止时间,则ok的值为false是表示没有设置截止时间,就要手动调用cancel函数取消Context。 `Done()`:返回一个只读`channel`(只有在被cancel后才会返回),它的数据类型是`struct{}`,一个空结构体。当`times out`或者`父级Context`调用`cancel`方法后,将会`close channel`来进行通知,但是不会涉及具体数据传输,根据这个信号,开发者就可以做一些清理动作,比如退出`goroutine`。多次调用`Done`方法会返回的是同一个`Channel`。 `Err()`:返回一个错误。如果上面的`Done()`的`channel`没被`close`,则`error`为`nil`;如果`channel`已被`close`,则`error`将会返回`close`的原因,说明该`context`为什么被关掉,比如`超时`或`手动取消`。 `Value()`:返回被绑定到`Context`的值,是一个键值对,所以要通过一个`Key`才可以获取对应的值,这个值一般是线程安全的。对于同一个上下文来说,多次调用`Value`并传入相同的`Key`会返回相同的结果,该方法仅用于`传递跨API和进程间请求域的数据`。 ### 8.6 Context的具体实现类型 参考1:[golang中的context]( ) 参考2:[golang的context]( ) 参考3:[golang 系列:context 详解]( ) 1. `Background()&TODO()` `Background():`是所有`派生Context`的`根Context`,该`Context`通常由接收`request`的第一个`goroutine`创建。它不能被取消、没有值、也没有过期时间,常作为处理`request`的顶层`context`存在。 `TODO():`也是返回一个没有值的`Context`,目前不知道它具体的使用场景,如果我们不知道该传什么类型的`Context`的时候,可以使用这个。 `Background()`和`TODO()`本质上是`emptyCtx`结构体类型,是一个不可取消,没有设置截止时间,没有携带任何值的空`Context`,是直接return默认值,没有具体功能代码。一般的将它们作为Context的根,往下派生。 2. `WithCancel(parent Context) (ctx Context, cancel CancelFunc)`:用来取消通知用的`context`。 返回一个继承的`Context`和`CancelFunc取消方法`,在父协程`context`的`Done`函数被关闭时会关闭自己的`Done`通道,或者在执行了`CancelFunc取消方法`之后,会关闭自己的Done通道。这种关闭的通道可以作为一种广播的通知操作,告诉所有`context`相关的函数停止当前的工作直接返回。通常使用场景用于主协程用于控制子协程的退出,用于一对多处理。 3. `WithDeadline(parent Context, d time.Time) (Context, CancelFunc)`:`timerCtx`类型的`context`,用来超时通知。 参数是传递一个上下文,等待超时时间,超时后,会返回超时时间,并且会关闭context的Done通道,其他传递的context收到Done关闭的消息的,直接返回即可。同样用户通知消息出来。 `以下三种情况会取消该创建的context:` 1、到达指定时间点; 2、调用了CancelFunc取消方法; 3、父节点context关闭。 4. `WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)`:`timerCtx`类型的`context`,用来超时通知。 `WithTimeout()`里是直接调用并返回的`WithDeadline()`,所以它和`WithDeadline()`功能是一样,只是传递的时间是从当前时间加上超时时间。
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
5. `WithValue(parent Context, key, val interface{}) Context`:`valueCtx`类型的`context`,用来传值的`context`。 传递上下文的信息,将需要传递的信息从一个协程传递到另外协程。 每个`context`都可以放一个`key-value`对, 通过`WithValue`方法可以找`key`对应的`value`值,如果没有找到,就从`父context`中找,直到找到为止。 `WithCancel`、`WithDeadline`、 `WithTimeout`、`WithValue`四个方法在创建的时候都会要求传`父级context`进来,以此达到链式传递信息的目的。 ### 8.7 context并发安全吗 参考1:<https://blog.csdn.net/weixin_38664232/article/details/123663759> context本身是线程安全的,所以context携带value也是线程安全的。 context包提供两种创建根context的方式: * context.Backgroud() * context.TODO() 又提供了四个函数(`WithCancel`、`WithDeadline`、`WithTimeout`、`WithValue`)基于`父Context`牌生,其中使用`WithValue`函数派生的`context`来携带数据,每次调用`WithValue`函数都会基于当前`context`派生一个新的`子context`,`WithValue`内部主要就是调用valueCtx类:
func WithValue(parent Context, key, val interface{}) Context {
if parent == nil {
panic(“cannot create context from nil parent”)
}
if key == nil {
panic(“nil key”)
}
if !reflectlite.TypeOf(key).Comparable() {
panic(“key is not comparable”)
}
return &valueCtx{parent, key, val}
}
说明:参数中的`parent`是当前`valueContext`的父节点。
`valueCtx`结构如下:
type valueCtx struct {
Context
key, val interface{}
}
`valueContext`继承`父Context`,这种是采用匿名接口的继承实现方式,`key`、`val`用来存储携带的键值对。 通过上面的代码分析,可以发现: 1. 添加键值对不是在原来的`父Context`结构体上直接添加,而是以此`context`作为父节点,重新创建一个新的`valueContext子节点`,将键值对添加到子节点上,由此形成一条`context`链。 2. 获取键值对的过程也是层层向上调用,直到首次设置key的父节点,如果没有找到首次设置key的父节点,会向上遍历直到根节点,如果根节点找到了key就会返回,否则就会找到最终的`根Context(emptyCtx)`返回nil。如下图所示: ![在这里插入图片描述](https://img-blog.csdnimg.cn/98b80994ace04645aaf90d7b0efe5da8.jpeg#pic_center) **总结:** context添加的键值对是一个链式的,会不断衍生新的context,所以context本身是不可变的,因此是线程安全的。 ### 8.8 context为什么可以实现并发安全? `context`包是`Go`语言中用于在协程之间传递取消信号、截止时间和共享数据的一种机制。`context`的并发安全性体现在以下几个方面: 1. **不可变性(Immutability)**:`context`的设计中鼓励不可变性,也就是说,一旦创建了`context`,它的值就不会被改变。这确保了在协程之间传递`context`时的线程安全性,因为不会有并发修改的情况。 2. **值的复制**:当一个协程创建一个新的`context`时,它可以基于已有的`context`创建一个新的实例,并向其中添加或修改一些值。这个过程中,原始的`context`实例不会受到影响,保证了并发安全。 3. **不可变的部分**:一些`context`的方法返回一个新的`context`,而不是修改原始的`context`。例如,`WithValue`方法就是返回一个带有新值的新`context`实例,而不是在原始的`context`上修改。这样的设计符合不可变性原则,从而确保并发安全。 4. **取消信号的传递**:通过`context`的取消机制,一个协程可以通知其他协程停止工作。这是通过`context`的`Done`通道来实现的。当一个协程调用`cancel`函数时,`Done`通道会被关闭,所有基于该`context`的协程都能感知到取消信号。 总体来说,`context`的设计强调了不可变性和值的复制,这使得它在并发环境下能够提供一种安全而有效的机制,用于在协程之间传递相关信息,控制取消,以及传递截止时间等。在并发编程中,使用`context`能够更容易地管理和传递与协程相关的信息,同时避免了共享状态带来的并发安全性问题。 ## 9 select语句 ### 9.1 介绍、使用规则 参考1:[go中select语句]( ) `select`语句是用来监听和`channel`有关的`IO`操作的,当`IO`操作发生时,触发对应的`case`动作。有了`select`语句,可以实现`main主线程`与`goroutine线程`之间的互动。
//for {
select {
case <-ch1 : // 检测有没有数据可读
// 一旦成功读取到数据,则进行该case处理语句
case ch2 <- 1 : // 检测有没有数据可写
// 一旦成功向ch2写入数据,则进行该case处理语句
default:
// 如果以上都没有符合条件,那么进入default处理流程
}
}//
select语句外面可使用for循环来实现不断监听IO的目的。 **注意事项:** 1. `select`语句只能用于`channel`的IO操作,每个`case`都必须是一个channel。 2. 如果不设置`default`条件,在没有`IO`操作发生时,`select`语句就会一直阻塞; 3. `如果有一个或多个IO操作同时发生时,Go运行时会随机选择一个case执行,但此时将无法保证执行顺序;` 4. `对于case语句,如果存在channel值为nil的读写操作,则该分支将被忽略,可以理解为相当于从select语句中删除了这个case;` 5. 对于既不设置`default`条件,又一直没有`IO`操作发生的情况,`select`语句会引起死锁(`fatal error: all goroutines are asleep - deadlock!`),如果不希望出现死锁,可以设置一个超时时间的case来解决; 6. 对于在`for`中的`select`语句,不能添加`default`,否则会引起`CPU`占用过高的问题; ### 9.2 如何给select的case设定优先级 参考1:[go语言中select实现优先级]( ) 在 `9.1 注意事项3`中已知无法保证执行顺序的情况。 问题描述:我们有一个函数会持续不间断地从`ch1`和`ch2`中分别接收`任务1`和`任务2`,如何确保当`ch1`和`ch2`同时达到就绪状态时,优先执行`任务1`,在没有`任务1`的时候再去执行`任务2`呢? 实现代码:
func worker2(ch1, ch2 <-chan int, stopCh chan struct{}) {
for {
select {
case <-stopCh:
return
case job1 := <-ch1:
fmt.Println(job1)
case job2 := <-ch2:
priority:
for {
select {
case job1 := <-ch1:
fmt.Println(job1)
default:
break priority
}
}
fmt.Println(job2)
}
}
}
使用了嵌套的select,还组合使用了`for`循环和`label`来解决问题。上面的代码在外层select选中执行`job2 := <-ch2`时,进入到内层`select`循环继续尝试执行`job1 := <-ch1`,当`ch1`就绪时就会一直执行,否则跳出内层`select`,继续执行`job2`。 这是两个任务的情况,在任务数可数的情况下可以层层嵌套来实现对多个任务排序,对于有规律的任务可以使用递归的。 ### 9.3 如何判断select的某个通道是关闭的 参考1:<https://blog.csdn.net/eddycjy/article/details/122053524> `注意:`关闭的`channel`不是`nil`,所以在`select`语句中依然可以监听并执行对应的`case`,只不过在读取关闭后的`channel`时,读取到的数据是零值,ok是false。要想知道某个通道是否关闭,判断ok是否为false即可。 要想判断某个通道是否关闭,当返回的ok为false时,执行c = nil 将通道置为nil,相当于读一个未初始化的通道,则会一直阻塞。至于为什么读一个未初始化的通道会出现阻塞,可以看我的另一篇 对未初始化的的chan进行读写,会怎么样?为什么? 。select中如果任意某个通道有值可读时,它就会被执行,其他被忽略。则select会跳过这个阻塞case,可以解决不断读已关闭通道的问题。 ### 9.4 如何屏蔽已关闭的channel 参考1:<https://blog.csdn.net/eddycjy/article/details/122053524> 要想屏蔽某个已经关闭的通道,判断通道的ok是`false`后,将`channel`置为`nil`,`select`再监听该通道时,相当于监听一个未初始化的通道,则会一直阻塞,`select`会跳过这个阻塞,从而达到屏蔽的目的。 ### 9.5 select里只有一个已经关闭的channel会怎么样 参考1:<https://blog.csdn.net/eddycjy/article/details/122053524> 只有一个case的情况下,则会死循环。 关闭的`channel`不是`nil`,所以在`select`语句中依然可以监听并执行对应的`case`,只不过在读取关闭后的`channel`时,读取到的数据是零值,ok是false。 ### 9.6 select里只有一个已经关闭的channel,且置为nil,会怎么样 参考1:<https://blog.csdn.net/eddycjy/article/details/122053524> 答:因为只有一个已经关闭的channel,且已经置为了nil,这时select会先阻塞,最后发生死锁(`fatal error: all goroutines are asleep - deadlock!`)。 对于既不设置`default`条件,又一直没有`IO`操作发生的情况,`select`语句会引起死锁(`fatal error: all goroutines are asleep - deadlock!`),如果不希望出现死锁,可以设置一个超时时间的case来解决; ## 10 defer `defer`的作用就是把`defer`关键字之后的函数执行压入一个栈中`延迟执行`,多个`defer`的执行顺序是`后进先出`LIFO,也就是先执行最后一个defer,最后执行第一个defer。 ### 10.1 使用场景 1. 打开和关闭文件; 2. 接收请求和回复请求; 3. 加锁和解锁等。 在这些操作中,最容易忽略的就是在每个函数退出处正确地释放和关闭资源。 ### 10.2 一个函数中多个defer的执行顺序【defer之间】 参考1:[go defer、return的执行顺序]( ) 多个`defer`的执行顺序是`后进先出`LIFO,也就是先执行最后一个defer,最后执行第一个defer。 ### 10.3 defer、return、返回值 的执行返回值顺序 参考1:[go defer、return的执行顺序]( ) 参考2:[Go语言中defer和return执行顺序解析]( ) `return返回值的运行机制:`return并非原子操作,共分为赋值、返回值两步操作。 `defer、return、返回值三者的执行是:`return最先执行,先将结果写入返回值中(即赋值);接着defer开始执行一些收尾工作;最后函数携带当前返回值退出(即返回值)。 1. 无名返回值(即函数返回值为没有命名的返回值) 如果函数的返回值是无名的(不带命名返回值),则go语言会在执行return的时候会执行一个类似创建一个临时变量作为保存return值的动作,`所以defer里面的操作不会影响返回值`。
package main
import (
“fmt”
)
func main() {
fmt.Println(“return:”, Demo()) // 打印结果为 return: 0
}
func Demo() int {
var i int
defer func() {
i++
fmt.Println(“defer2:”, i) // 打印结果为 defer: 2
}()
defer func() {
i++
fmt.Println(“defer1:”, i) // 打印结果为 defer: 1
}()
return i
}
代码示例,实际上一共执行了3步操作:
1)赋值,因为返回值没有命名,所以return 默认指定了一个返回值(假设为s),首先将i赋值给s,i初始值是0,所以s也是0。
2)后续的defer操作因为是针对i,进行的,所以不会影响s,此后因为s不会更新,所以s不会变还是0。
3)返回值,return s,也就是return 0
相当于:
var i int
s := i
return s
2. 有名返回值(函数返回值为已经命名的返回值)
有名返回值的函数,由于返回值在函数定义的时候已经将该变量进行定义,在执行return的时候会先执行返回值保存操作,而后续的defer函数会改变这个返回值(虽然`defer是在return之后执行的,但是由于使用的函数定义的变量,所以执行defer操作后对该变量的修改会影响到return的值`)。
由于返回值已经提前定义了,不会产生临时零值变量,返回值就是提前定义的变量,后续所有的操作也都是基于已经定义的变量,任何对于返回值变量的修改都会影响到返回值本身。
package main
import (
“fmt”
)
func main() {
fmt.Println(“return:”, Demo2()) // 打印结果为 return: 2
}
func Demo2() (i int) {
defer func() {
i++
fmt.Println(“defer2:”, i) // 打印结果为 defer: 2
}()
defer func() {
i++
fmt.Println(“defer1:”, i) // 打印结果为 defer: 1
}()
return i // 或者直接 return 效果相同
}
### 10.4 defer能否修改return的值 可以,在 `10.2.2 defer、return、返回值 的执行返回值顺序`下`有名返回值(函数返回值为已经命名的返回值)`的讲解中可以知道,可以更改。 有名返回值的函数,由于返回值在函数定义的时候已经将该变量进行定义,在执行return的时候会先执行返回值保存操作,而后续的defer函数会改变这个返回值(虽然`defer是在return之后执行的,但是由于使用的函数定义的变量,所以执行defer操作后对该变量的修改会影响到return的值`)。 由于返回值已经提前定义了,不会产生临时零值变量,返回值就是提前定义的变量,后续所有的操作也都是基于已经定义的变量,任何对于返回值变量的修改都会影响到返回值本身。 ### 10.5 在循环打开很多个文件,怎么使用defer关闭文件,defer应该写在哪个位置 参考1:[循环内部使用defer的正确姿势]( ) 重点是理解defer的执行机制,`defer是在函数退出的时候才执行的`,所以可以将打开关闭、文件等操作单独写到一个函数里,或者是写到匿名函数中。 ## 11 Golang的反射 参考1:[golang之反射]( ) ### 11.1 反射基础知识 **反射基本介绍:** 1. 反射可以在运行时动态获取变量的各种信息,比如变量的类型(type),类别(kind) 2. 如果是结构体变量,还可以获取到结构体本身的信息(包括结构体的字段,方法)。 3. 通过反射,可以修改变量的值,可以调用关联的方法。 4. 使用反射,需要导入reflect包。 **反射重要的函数:** 1. `reflect.TypeOf`(变量名),获取变量的类型,返回`reflect.Type`类型; 2. `reflect.ValueOf`(变量名),获取变量的值,返回`reflect.Value`类型,`reflect.Value`是一个结构体类型。通过`reflect.Value`,可以获取到关于该变量的很多信息。 3. 变量、`interface{}`和`reflect.Value`是可以相互转换的,这点在实际开发中,会经常使用到。
interface{} ——> reflect.Value:
rVal := reflect.ValueOf(b)
reflect.Value ——> interface{}:
iVal := rVal.Interface()
interface{} ——> 原来的变量(类型断言):
v := iVal.(Stu)
**反射的注意事项:**
1. reflect.Value.kind,获取变量的类别,返回的是一个常量。
2. Type是类型,kind是类别,可能相同,也可能不相同。
//比如:
var num int = 10 //num的Type是int,Kind也是int
var stu Student //stu的Type是包名.Student,Kind是struct
3. 通过反射可以让变量在`interface{}`和`Reflect.Value`之间相互转换:
变量 <——> interface{} <——> reflect.Value
4. 使用反射的方式来获取变量的值(并返回对应的类型),要求数据类型匹配,比如x是int,那么就应该使用`reflect.Value(x).Int()`,而不能使用其它的,否则报painc。
5. 通过反射来修改变量,注意当使用`SetXxx`方法来设置需要通过对应的指针类型来完成,这样才能改变传入的变量的值,同时需要使用到`reflect.Value.Elem()`方法。
### 11.2 反射如何获取结构体中字段的jsonTag
参考1:[Go语言反射(reflection)简述]( )
看参考1的`使用反射获取结构体的成员类型`部分。
package main
import (
“fmt”
“reflect”
)
func main() {
// 声明一个空结构体
type cat struct {
Name string
// 带有结构体tag的字段
Type int json:"type" id:"100"
}
// 创建cat的实例
ins := cat{Name: “mimi”, Type: 1}
// 获取结构体实例的反射类型对象
typeOfCat := reflect.TypeOf(ins)
// 遍历结构体所有成员
for i := 0; i < typeOfCat.NumField(); i++ {
// 获取每个成员的结构体字段类型
fieldType := typeOfCat.Field(i)
// 输出成员名和tag
fmt.Printf(“name: %v tag: ‘%v’\n”, fieldType.Name, fieldType.Tag)
}
// 通过字段名, 找到字段类型信息
if catType, ok := typeOfCat.FieldByName(“Type”); ok {
// 从tag中取出需要的tag
fmt.Println(catType.Tag.Get(“json”), catType.Tag.Get(“id”))
}
}
输出结果: > > name: Name tag: ‘’ > name: Type tag: ‘json:“type” id:“100”’ > type 100 > > > ### 11.3 结构体里的变量不加tag能正常转成json里的字段吗 参考1:[golang面试题:json包变量不加tag会怎么样?]( ) 1. 若是变量首字母小写,则为`private`。由于取不到反射信息,所以不能转成json。 2. 若是变量首字母大写,则为`public`,可以转为json: * json不加tag,能够正常转为json里的字段,json内字段名跟结构体内字段原名一致。 * json加了tag,从struct转json的时候,json的字段名就是tag里的字段名,原字段名已经没用。 `代码:`
package main
import (
“encoding/json”
“fmt”
)
type JsonTest struct {
aa string //小写无tag
bb string json:"BB"
//小写+tag
CC string //大写无tag
DD string json:"DJson"
//大写+tag
}
func main() {
jsonTest := JsonTest{aa: “1”, bb: “2”, CC: “3”, DD: “4”}
fmt.Printf(“转为json前jsonTest结构体的内容 = %+v\n”, jsonTest)
jsonInfo, _ := json.Marshal(jsonTest)
fmt.Printf(“转为json后的内容 = %+v\n”, string(jsonInfo))
}
### 11.4 反射操作指针类型
typ := reflect.TypeOf(data).Elem() //指针类型需要加 Elem()
val := reflect.ValueOf(data).Elem() //指针类型需要加 Elem()
### 11.5 Golang反射的使用场景和限制。 **使用场景:** 1. **通用数据处理**:反射可以用于处理未知类型的数据,例如`JSON`解析、数据库操作等。通过反射,可以动态地获取和修改数据的类型和值。 2. **动态调用方法**:反射可以用于动态调用结构体的方法,尤其在需要通过方法名字符串来调用方法时,反射提供了一种实现机制。 3. **类型检查和类型转换**:通过反射,可以在运行时获取变量的类型信息,进行类型检查和类型转换。这对于泛型编程和接口实现时可能很有用。 4. **代码工具和框架**:一些代码生成工具、`ORM`框架、测试框架等利用反射来提供通用的、可扩展的功能。 **限制和注意事项:** 1. **性能影响**:反射操作通常比直接的类型断言和静态编码效率低,因为它需要在运行时进行类型检查。在性能敏感的代码中要谨慎使用反射。 2. **可读性和维护性**:反射的代码通常更加复杂和难以理解,可能会降低代码的可读性和维护性。因此,只有在确实需要在运行时获取类型信息或进行动态操作时才应该使用反射。 3. **类型安全问题**:反射可能会导致类型不安全的问题,因为在编译时无法检查反射操作的正确性。这可能导致一些潜在的运行时错误。 4. **不支持编译时优化**:由于反射的特性,Go编译器无法对涉及反射的代码进行很好的优化。这可能会导致一些性能上的损失。 在实际应用中,要慎重使用反射,并确保它真的是解决问题的最佳选择。在大多数情况下,通过静态编码和类型安全的方式实现更为清晰和高效。 ## 12 Golang哪些情况会导致内存泄漏 参考1:[golang容易导致内存泄漏的几种情况]( ) ### 12.1 内存泄漏的本质 参考1:[内存泄漏]( ) 内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。 **内存泄漏的危害:** 长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。 ### 12.2 几种情况 1. 定时器使用不当 2. select阻塞 3. channel阻塞 4. goroutine导致的内存泄漏 5. slice引起的内存泄漏 6. 数组的值传递 ### 12.3 定时器使用不当 #### 12.3.1 time.After()的使用 默认的`time.After()`是会有内存泄露问题的,因为每次`time.After(duration x)`会产生`NewTimer()`,在`duration x`到期之前,新创建的`timer`不会被垃圾回收,到期之后才会垃圾回收。 随着时间推移,尤其是`duration x`很大的话,会产生内存泄露的问题,应特别注意。
for true {
select {
case <-time.After(time.Minute * 3):
// do something
default:
time.Sleep(time.Duration(1) * time.Second)
}
}
为了保险起见,使用`NewTimer()`或者`NewTicker()`代替的方式主动释放资源。
timer := time.NewTicker(time.Duration(2) * time.Second)
defer timer.Stop()
for true {
select {
case <-timer.C:
// do something
default:
time.Sleep(time.Duration(1) * time.Second)
}
}
#### 12.3.2 time.NewTicker资源未及时释放
在使用time.NewTicker时需要手动调用Stop()方法释放资源,否则将会造成永久性的内存泄漏。
timer := time.NewTicker(time.Duration(2) * time.Second)
// defer timer.Stop()
for true {
select {
case <-timer.C:
// do something
default:
time.Sleep(time.Duration(1) * time.Second)
}
}
### 12.4 select阻塞
使用select时如果有case没有覆盖完全的情况且没有default分支进行处理,会出现阻塞,最终导致内存泄漏。
#### 12.4.1 导致`goroutine`阻塞的情况
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
ch3 := make(chan int)
go Getdata(“https://www.baidu.com”,ch1)
go Getdata(“https://www.baidu.com”,ch2)
go Getdata(“https://www.baidu.com”,ch3)
select{
case v:=<- ch1:
fmt.Println(v)
case v:=<- ch2:
fmt.Println(v)
}
}
上面代码中这种情况会阻塞在ch3的消费处导致内存泄漏。
#### 12.4.2 循环空转导致CPU暴涨
func main() {
fmt.Println(“main start”)
msgList := make(chan int, 100)
go func() {
for {
select {
case <-msgList:
default:
}
}
}()
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, os.Kill)
s := <-c
fmt.Println("main exit.get signal:", s)
}
上述for循环条件一旦命中default则会出现循环空转的情况,并最终导致CPU暴涨。 ### 12.5 channel阻塞 channel阻塞主要分为`写阻塞`和`读阻塞`两种情况。 #### 12.5.1 空channel 读写均会堵塞。
func channelTest() {
//声明未初始化的channel读写都会阻塞
var c chan int
//向channel中写数据
go func() {
c <- 1
fmt.Println(“g1 send succeed”)
time.Sleep(1 * time.Second)
}()
//从channel中读数据
go func() {
<-c
fmt.Println(“g2 receive succeed”)
time.Sleep(1 * time.Second)
}()
time.Sleep(10 * time.Second)
}
#### 12.5.2 写阻塞
1. 无缓冲channel的阻塞通常是写操作因为没有读而阻塞。
func channelTest() {
var c = make(chan int)
//10个协程向channel中写数据
for i := 0; i < 10; i++ {
go func() {
<- c
fmt.Println(“g1 receive succeed”)
time.Sleep(1 * time.Second)
}()
}
//1个协程丛channel读数据
go func() {
c <- 1
fmt.Println(“g2 send succeed”)
time.Sleep(1 * time.Second)
}()
//会有写的9个协程阻塞得不到释放
time.Sleep(10 * time.Second)
}
2. 有缓冲的channel因为缓冲区满了,写操作阻塞。
func channelTest() {
var c = make(chan int, 8)
//10个协程向channel中写数据
for i := 0; i < 10; i++ {
go func() {
<- c
fmt.Println(“g1 receive succeed”)
time.Sleep(1 * time.Second)
}()
}
//1个协程丛channel读数据
go func() {
c <- 1
fmt.Println(“g2 send succeed”)
time.Sleep(1 * time.Second)
}()
//会有写的几个协程阻塞写不进去
time.Sleep(10 * time.Second)
}
#### 12.5.3 读阻塞
从channel读数据,但是没有goroutine往进写数据。
func channelTest() {
var c = make(chan int)
//1个协程向channel中写数据
go func() {
<- c
fmt.Println(“g1 receive succeed”)
time.Sleep(1 * time.Second)
}()
//10个协程丛channel读数据
for i := 0; i < 10; i++ {
go func() {
c <- 1
fmt.Println(“g2 send succeed”)
time.Sleep(1 * time.Second)
}()
}
//会有读的9个协程阻塞得不到释放
time.Sleep(10 * time.Second)
}
### 12.6 goroutine导致的内存泄漏 #### 12.6.1 申请过多的goroutine 例如在for循环中申请过多的goroutine来不及释放导致内存泄漏。 #### 12.6.2 goroutine阻塞 ##### 12.6.2.1 I/O问题 I/O连接未设置超时时间,导致goroutine一直在等待,代码会一直阻塞。 ##### 12.6.2.2 互斥锁未释放 goroutine无法获取到锁资源,导致goroutine阻塞。 ##### 12.6.2.3 死锁 当程序死锁时其他goroutine也会阻塞。
func mutexTest() {
m1, m2 := sync.Mutex{}, sync.RWMutex{}
//g1得到锁1去获取锁2
go func() {
m1.Lock()
fmt.Println(“g1 get m1”)
time.Sleep(1 * time.Second)
m2.Lock()
fmt.Println(“g1 get m2”)
}()
//g2得到锁2去获取锁1
go func() {
m2.Lock()
fmt.Println(“g2 get m2”)
time.Sleep(1 * time.Second)
m1.Lock()
fmt.Println(“g2 get m1”)
}()
//其余协程获取锁都会失败
go func() {
m1.Lock()
fmt.Println(“g3 get m1”)
}()
time.Sleep(10 * time.Second)
}
##### 12.6.2.4 WaitGroup使用不当 WaitGroup的Add、Done和wait数量不匹配会导致wait一直在等待。 ### 12.7 slice 引起的内存泄漏 当两个slice共享地址,其中一个为全局变量,另一个也无法被GC; append slice后一直使用,没有进行清理。
var a []int
func test(b []int) {
a = b[:3]
return
}
### 12.8 数组的值传递
由于数组时Golang的基本数据类型,每个数组占用不通的内存空间,生命周期互不干扰,很难出现内存泄漏的情况,但是数组作为形参传输时,遵循的时`值拷贝`,如果函数被多个goroutine调用且数组过大时,则会导致内存使用激增。
//统计nums中target出现的次数
func countTarget(nums [1000000]int, target int) int {
num := 0
for i := 0; i < len(nums) && nums[i] == target; i++ {
num++
}
return num
}
因此对于大数组放在形参场景下通常使用切片或者指针进行传递,避免短时间的内存使用激增。 ## 13 Golang的并发实现方式 参考1:<https://www.jb51.net/article/243510.htm> 一共四种: 1. goroutine:Golang在语言层面对并发编程进行了支持, 使用go关键字来使用协程。 2. channel:Golang语言在语言级别提供了对goroutine之间通信的支持,我们可以使用channel在两个或者多个goroutine之间进行信息传递,能过channel传递对像的过程和调用函数时的参数传递行为一样,可以传递普通参数和指针。 3. select:当我们在实际开发中,我们一般同时处理两个或者多个channel的数据,我们想要完成一个那个channel先来数据,我们先来处理个那channel,避免等待。 4. 传统的并发控制:sync.Mutex加锁和sync.WaitGroup等待组。 ## 14 Golang里的结构体可以直接使用双等号作比较吗 参考1:[Golang = 比较与赋值]( ) 参考2:[golang中如何比较struct,slice,map是否相等以及几种对比方法的区别]( ) > > * 结构体只能比较是否相等,但是不能比较大小。 > * 相同类型的结构体才能够进行比较,结构体是否相同不但与属性类型有关,还与属性顺序相关,sn3 与 sn1 就是不同的结构体; > * 如果 struct 的所有成员都可以比较,则该 struct 就可以通过 == 或 != 进行比较是否相等,比较时逐个项进行比较,如果每一项都相等,则两个结构体才相等,否则不相等;(像切片、map、函数等是不能比较的) > > > ## 15 Golang里有Set结构体吗?如果没有怎么设计一个Set结构体 参考1:[Golang数据结构实现(二)集合Set]( )
//定义1个set结构体 内部主要是使用了map
type set struct {
elements map[interface{}]bool
}
## 16 Golang的runtime 参考1:[说说Golang的runtime]( ) `runtime`包含Go运行时的系统交互的操作,例如控制`goruntine`的功能。还有`debug`,`pprof`进行排查问题和运行时性能分析,`tracer`来抓取异常事件信息,如 `goroutine`的创建,加锁解锁状态,系统调用进入推出和锁定还有GC相关的事件,堆栈大小的改变以及进程的退出和开始事件等等;race进行竞态关系检查以及`CGO`的实现。总的来说运行时是调度器和GC。 ## 17 Golang死锁的场景及解决办法 参考1:[详解Golang并发操作中常见的死锁情形]( ) 1. 无缓存能力的管道,自己写完自己读。 会报死锁:`fatal error: all goroutines are asleep - deadlock!` 解决办法很简单,开辟两条协程,一条协程写,一条协程读。
func main() {
ch := make(chan int, 0)
ch <- 666
x := <- ch
fmt.Println(x)
}
2. 协程来晚了
func main() {
ch := make(chan int,0)
ch <- 666
go func() {
<- ch
}()
}
我们可以看到,这条协程开辟在将数字写入到管道之后,因为没有人读,管道就不能写,然后写入管道的操作就一直阻塞。这时候就有疑惑了,不是开辟了一条协程在读吗?但是那条协程开辟在写入管道之后,如果不能写入管道,就开辟不了协程。
3. 管道读写时,相互要求对方先读/写
func main() {
chHusband := make(chan int,0)
chWife := make(chan int,0)
go func() {
select {
case <- chHusband:
chWife<-888
}
}()
select {
case <- chWife:
chHusband <- 888
}
}
* 先来看看老婆协程,chWife只要能读出来,也就是老婆有钱,就给老公发个八百八十八的大红包。
* 再看看老公的协程,一看不得了,咋啦?老公也说只要他有钱就给老婆包个八百八十八的大红包。
* 两个人都说自己没钱,老公也给老婆发不了红包,老婆也给老公发不了红包,这就是死锁!
4. 读写锁相互阻塞,形成隐形死锁
func main() {
var rmw09 sync.RWMutex
ch := make(chan int,0)
go func() {
rmw09.Lock()
ch <- 123
rmw09.Unlock()
}()
go func() {
rmw09.RLock()
x := <- ch
fmt.Println(“读到”,x)
rmw09.RUnlock()
}()
for {
runtime.GC()
}
}
* 这两条协程,如果第一条协程先抢到了只写锁,另一条协程就不能抢只读锁了,那么因为另外一条协程没有读,所以第一条协程就写不进。 * 如果第二条协程先抢到了只读锁,另一条协程就不能抢只写锁了,那么因为另外一条协程没有写,所以第二条协程就读不到。 ## 18 Golang的僵尸进程 参考1:[Go Exec 僵尸与孤儿进程]( ) 僵尸进程(zombie process)指:完成执行(通过exit系统调用,或运行时发生致命错误或收到终止信号所致),但在操作系统的进程表中仍然存在其进程控制块,处于“终止状态”的进程。 **解决&预防:** 收割僵尸进程的方法是通过`kill`命令手工向其父进程发送SIGCHLD信号。如果其父进程仍然拒绝收割僵尸进程,则终止父进程,使得`init`进程收养僵尸进程。`init`进程周期执行`wait`系统调用收割其收养的所有僵尸进程。 ## 19 Golang函数的入参是值传递还是引用传递 参考1:[Golang函数参数的值传递和引用传递]( ) 值传递和引用传递都有,看入参的类型。 `值传递:`是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。 `引用传递:`引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数,由于引用类型(slice、map、interface、channel)自身就是指针,所以这些类型的值拷贝给函数参数,函数内部的参数仍然指向它们的底层数据结构。 ## 20 Golang的引用类型有哪几种 参考1:<https://www.jianshu.com/p/93e205e70e83> Golang的引用类型包括 `slice`、`map` 和 `channel`。 ## 21 Golang的make和new的区别 参考1:[make和new的区别]( ) `new` 和 `make` 主要区别如下: 1. make只能用来分配及初始化类型为slice、map、chan的数据。new可以分配任意类型的数据; 2. new分配返回的是指针,即类型\*Type。make返回引用,即Type; 3. new分配的空间被清零。make分配空间后,会进行初始化; 在讲`new`和`make`的使用场景之前,先介绍一下golang中的值类型和引用类型。 **`引用类型和值类型`** `值类型:` `int`、`float`、`bool`和`string`这些类型都属于值类型,使用这些类型的变量直接指向存在内存中的值,值类型的变量的值存储在栈中。当使用等号`=`将一个变量的值赋给另一个变量时,如 `j = i` ,实际上是在内存中将 i 的值进行了拷贝。可以通过 &i 获取变量 i 的内存地址。 (struct在方法中传参时是值类型而非引用类型) `引用类型:`特指`slice`、`map`、`channel`这三种预定义类型。能够通过`make()`函数创建的都是引用类型,比如`slice`和`map`,`slice`虽然看起来像数组,但是他其实是一个指向数组内存空间的一个指针类型。 **使用场景:** 1. 如果方法内部会修改当前对象的字段或改变其值,需要用指针。 2. 由于值传递是(内存)复制,因此,如果对象比较大,应该使用指针(地址),避免内存拷贝(值类型等变量指向内存中的值,如果有值类型变量存放大量元素,或造成内存的大量拷贝) ## 22 Mutex读写锁和互斥锁的区别 参考1:[互斥锁机制,互斥锁与读写锁区别]( ) **互斥锁和读写锁的区别:** 1. 读写锁区分读者和写者,而互斥锁不区分。 2. 互斥锁同一时间只允许一个线程访问该对象,无论读写;读写锁同一时间内只允许一个写者,但是允许多个读者同时读对象。 ## 23 NewTicker和NewTimer的区别 参考1:[go定时器NewTicker&NewTimer]( ) 1. NewTimer是延迟d时间后触发,如果需要循环则需要Reset。NewTimer的延迟时间并不是精确、稳定的,比如设置30ms,有可能会35、40ms后才触发,即使在系统资源充足的情况下,所以一个循环的timer在60ms内并不能保证会触发2两次,而ticker会。 2. 它会调整时间间隔或者丢弃 tick 信息以适应反应慢的接者,所以回调触发不是稳定的,有可能在小于d的时间段触发,也有可能大于d的时间段触发,即使应用什么都不做。但在一段时间内,触发次数是保证的,比如在系统资源充足的情况下,设定触发间隔30ms,上一ticket触发间隔是44ms,下一触发间隔可能就是16ms,所以60ms内还是会触发两个ticket。 **区别:** ticker的稳定性不如timer,一个空转的go程序,tickter也是不稳定的,触发间隔并不会稳定在d时间段,在ms级别上;而timer相对稳定,但也不是绝对的,timer也会在大于d的时间后触发。 ## 24 遇到高并发场景怎么处理 回答:Golang语言上是使用多协程,还有架构设计方面等等。 ## 25 哪些场景有使用到Goroutine、channel 答:并发处理。有缓冲的channel可以控制并发数目,从而实现多线程并发处理。 ## 26 Golang把int转为string的方式(strconv包) 参考1:[Go语言strconv包实现字符串和数值类型的相互转换]( ) **strconv包里有相关的转换方法:** 1. string 与 int 类型之间的转换 `Itoa():`整型转字符串。 `Atoi():`字符串转整型。 2. Parse 系列函数 `Parse`系列函数用于将字符串转换为指定类型的值,其中包括 `ParseBool()`、`ParseFloat()`、`ParseInt()`、`ParseUint()`。 3. Format 系列函数 `Format`系列函数实现了将给定类型数据格式化为字符串类型的功能,其中包括 `FormatBool()`、`FormatInt()`、`FormatUint()`、`FormatFloat()`。 4. Append 系列函数 * `Append`系列函数用于将指定类型转换成字符串后追加到一个切片中,其中包含 `AppendBool()`、`AppendFloat()`、`AppendInt()`、`AppendUint()`。 * `Append`系列函数和`Format`系列函数的使用方法类似,只不过是将转换后的结果追加到一个切片中。 ## 27 使用过Golang的sync包里哪些函数或方法 参考1:[golang标准库-sync包使用和应用场景]( ) 参考2:[Golang - sync包的使用]( ) 1. Locker:Locker接口,包含Lock()和Unlock()两个方法,用于代表一个能被加锁和解锁的对象。 2. Once:Once是只执行一次动作的对象,使用后不得复制,Once只有一个Do方法。 3. Mutex:Mutex是一个互斥锁,可以创建为其他结构体的字段;零值为解锁状态。Mutex类型的锁和线程无关,可以由不同的线程加锁和解锁。实现了Locker()接口的UnLock()和Locker()方法,同一时刻一段代码只能被一个线程运行。 4. RWMutex:读写互斥锁,该锁可以被同时多个读取者持有或唯一个写入者持有。 5. WaitGroup:WaitGroup 对象内部有一个计数器,最初从0开始,它有三个方法:Add(), Done(), Wait() 用来控制计数器的数量。Add(n) 把计数器设置为n ,Done() 每次把计数器-1 ,wait() 会阻塞代码的运行,直到计数器的值减为0。 6. Pool:Pool是一个可以分别存取的临时对象的集合,可以被看作是一个存放可重用对象的值的容器、过减少GC来提升性能,是Goroutine并发安全的。有两个方法 Get()、Set()。 `WaitGroup、Once、Mutex、RWMutex、Cond、Pool、Map`。 ## 28 Golang实现字符串拼接有几种方式及其性能 参考1:[golang 几种字符串的拼接方式]( ) 参考2:[Golang的五种字符串拼接方式]( ) 1. `+`号
func BenchmarkAddStringWithOperator(b *testing.B) {
hello := “hello”
world := “world”
for i := 0; i < b.N; i++ {
_ = hello + “,” + world
}
}
Golang里面的字符串都是不可变的,每次运算都会产生一个新的字符串,所以会产生很多临时的无用的字符串,不仅没有用,还会给GC带来额外的负担,所以性能比较差。
2. `fmt.Sprintf()`
func BenchmarkAddStringWithSprintf(b *testing.B) {
hello := “hello”
world := “world”
for i := 0; i < b.N; i++ {
_ = fmt.Sprintf(“%s,%s”, hello, world)
}
}
内部使用 `[]byte` 实现,不像直接运算符这种会产生很多临时的字符串,但是内部的逻辑比较复杂,有很多额外的判断,还用到了 `interface`,所以性能也不是很好。
3. `strings.Join()`
func BenchmarkAddStringWithJoin(b *testing.B) {
hello := “hello”
world := “world”
for i := 0; i < b.N; i++ {
_ = strings.Join([]string{hello, world}, “,”)
}
}
`join`会先根据字符串数组的内容,计算出一个拼接之后的长度,然后申请对应大小的内存,一个一个字符串填入,在已有一个数组的情况下,这种效率会很高,但是如果本来没有的话,去构造这个数据的代价也不小,效率也不高。
4. `buffer.WriteString()`
func BenchmarkAddStringWithBuffer(b *testing.B) {
hello := “hello”
world := “world”
for i := 0; i < 1000; i++ {
var buffer bytes.Buffer
buffer.WriteString(hello)
buffer.WriteString(“,”)
buffer.WriteString(world)
_ = buffer.String()
}
}
这个比较理想,可以当成可变字符使用,对内存的增长也有优化,如果能预估字符串的长度,还可以用 buffer.Grow() 接口来设置 `capacity`。 **几种方式的性能:** 1. 在已有字符串数组的场合,使用 `strings.Join()` 能有比较好的性能。 2. 在一些性能要求较高的场合,尽量使用 `buffer.WriteString()`以获得更好的性能。 3. 性能要求不太高的场合,直接使用运算符,代码更简短清晰,能获得比较好的可读性。 4. 如果需要拼接的不仅仅是字符串,还有数字之类的其他需求的话,可以考虑 `fmt.Sprintf()`。 ## 29 Golang的int和int32区别 参考1:[Golang中int, int8, int16, int32, int64和uint区别]( ) 答: 1. int类型的大小与操作系统有关 2. int8类型大小为 1 字节【8代表8位】 3. int16类型大小为 2 字节【16代表16位】 4. int32类型大小为 4 字节【32代表32位】 5. int64类型大小为 8 字节【64代表64位】 我们看一下官方文档 `int is a signed integer type that is at least 32 bits in size. It is a distinct type, however, and not an alias for, say, int32.` 意思是 int 是一个至少32位的有符号整数类型。但是,它是一个不同的类型,而不是int32的别名。int 和 int32 是两码事。 `uint is a variable sized type, on your 64 bit computer uint is 64 bits wide.` uint 是一种可变大小的类型,在64位计算机上,uint 是64位宽的。uint 和 uint8 等都属于无符号 int 类型。uint 类型长度取决于 CPU,如果是32位CPU就是4个字节,如果是64位就是8个字节。 **总结:** go语言中的int的大小是和操作系统位数相关的,32位操作系统,int类型的大小是4字节【int32类型】。64位操作系统,int类型的大小是8个字节【int64类型】。 ## 30 go.sum和go.mod的区别 参考1:[深入理解 Go Modules 的 go.mod 与 go.sum]( ) `go.mod`: 1. `go.mod`:它用来标记一个 module 和它的依赖库以及依赖库的版本。会放在 module 的主文件夹下,一般以 go.mod 命名。 上面我们说到,Golang在做依赖管理时会创建两个文件,go.mod和go.sum。 2. `go.sum`:go.sum 则是记录了所有依赖的 module 的校验信息,以防下载的依赖被恶意篡改,主要用于安全校验。 ## 31 Golang依赖包的引用查询机制 参考1:[Go 包管理与依赖查找顺序]( ) ## 32 `go mod tidy`做了什么事情 参考1:[go mod tidy的作用]( ) 1. 引用项目需要的依赖增加到go.mod文件。 2. 去掉go.mod文件中项目不需要的依赖。 ## 33 定时任务除了time.Tick(time.Second),其他的实现 Asynq,基于Redis实现的。 ## 34 Golang为什么会有指针,指针的主要作用是什么 参考1:[GO:理解指针的作用]( ) `指针是指向了一个值的内存地址。` **指针的作用:** 1. 指针类型用于传递地址,而不是传递值,因为golang的函数,所有的参数都是传递一个复制的值。如果值的体积过大,,那么就会严重降低效率,而传递一个地址, 就会大大提高效率,另外传递指针也能让golang函数实现对变量值的修改。 2. 如果一个复杂类型的值被传递了若干次后,和自己比较,虽然用于保存的容器和名称变了,但用于保存值的地址不变,这个时候,只要使用指针进行对比,就知道还是原来的东西。 ## 35 项目中错误处理是怎么做的,比如执行了空指针异常 参考1:[golang 错误处理]( ) **panic** panic的引发: 1. 程序主动调用panic函数。 2. 程序产生运行时错误,由运行时检测并抛出。 发生`panic`后,程序会从调用`panic`的函数位置或发生`panic`的地方立即返回,逐层向上执行函数的`defer`语句,然后逐层打印函数调用堆栈,直到被`recover`捕获或运行到最外层函数而退出。 此外,`defer`逻辑里也可以再次调用`panic`或抛出`panic`。defer里面的 `panic` 能够被后续执行的 `defer` 捕获。 **recover** `recover()`用来捕获`panic`,阻止`panic`继续向上传递。`recover()`和`defer`一起使用,但是`recover()`只有在`defer`后面的函数体内被直接调用才能捕获`panic`终止异常,否则返回`nil`,异常继续向外传递。 即在defer中使用recover捕获并处理异常。 **error** Go语言内置错误接口类型`error`。任何类型只要实现`Error() string` 方法,都可以传递`error` 接口类型变量。Go语言典型的错误处理方式是将`error`作为函数最后一个返回值。在调用函数时,通过检测其返回的`error`值是否为`nil`来进行错误处理。 ## 36 Golang面向对象的继承、多态、封装 参考1:[golang实现面向对象的封装、继承、多态]( ) ## 37 Golang的mutex等各种锁的原理 参考1:[Golang 的锁机制]( ) `Golang`中的锁分为`互斥锁`、`读写锁`、`原子锁`即原子操作。 `Golang` 里有专门的方法来实现锁,就是 `sync` 包,这个包有两个很重要的锁类型。一个叫 `Mutex`, 利用它可以实现`互斥锁`。一个叫 `RWMutex`,利用它可以实现`读写锁`。 `sync.Mutex`:互斥锁是同一时刻某一资源只能上一个锁,上锁后只能被此线程使用,直至解锁。加锁后即不能读也不能写。 `sync.RWMutex`:读写锁将使用者分为`读者`和`写者`两个概念,支持同时多个读者一起读共享资源,但写时只能有一个,并且在写时不可以读。理论上来说,sync.RWMutex 的 Lock() 也是个互斥锁。 ## 38 Golang系统中哪些panic是不能被捕获的 参考1:[使用Golang时遇到的一些坑]( ) 1. 并发操作map实例 ## 39 Golang服务的优雅重启有哪些方式 参考1:[Go项目实现优雅关机与平滑重启]( ) **优雅的关机:** 优雅关机就是服务端关机命令发出后不是立即关机,而是等待当前还在处理的请求全部处理完毕后再退出程序,是一种对客户端友好的关机方式。而执行`Ctrl+C`关闭服务端时,会强制结束进程导致正在访问的请求出现问题。 **实现原理:** Go 1.8版本之后,在 `os/signal` 包中, `http.Server` 内置的 `Shutdown()` 方法就支持优雅地关机。 **Shutdown工作的机制**:当程序检测到中断信号时,我们调用`http.Server`中的`Shutdown()`方法,该方法将阻止新的请求进来,同时保持当前的连接,直到当前连接完成则终止程序! ![信号列表](https://img-blog.csdnimg.cn/585ed24102c64cf084eef7bca21ba351.png#pic_center) **流程**: 8080端口开启了一个web服务,并且只注册了一条路由,“/”, 但客户端访问127.0.0.1:8080/时,过10秒才会响应,如果这时我们按下ctrl+c,给程序发送syscall.SIGINT信号,他会等待10秒将当前请求处理完,他才会消亡,当然也取决于创建的5秒的context超时时间。 **代码**:
package main
import (
“context”
“github.com/gin-gonic/gin”
“log”
“net/http”
“os”
“os/signal”
“syscall”
“time”
)
// 实现优雅关机和平滑重启
func main() {
router := gin.Default()
router.GET(“/”, func(c *gin.Context) {
// 这个10秒的延时。是为了演示操作方便,实际上线一定注释掉
time.Sleep(time.Second * 10)
c.String(http.StatusOK, “hello xiaosheng”)
})
srv := &http.Server{
Addr: “:8080”,
Handler: router,
}
// 必须开启一个go routine 因为如果不开起,下面会一直listen and serve,进入死循环
// err != http.ErrServerClosed这个很重要
go func() {
// 开启一个goroutine启动服务
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf(“listen : %s\n”, err)
}
}()
// 等待中断信号来优雅关掉服务器, 为关闭服务器做一个5秒的延时
quit := make(chan os.Signal, 1)
// kill 默认会发送syscall.SIGTREN信号
// kill -2发送syscall.SIGINT信号,我们常用的ctrl+c就是触发系统SIGINT信号
// kill -9发送syscall.SIGKILL信号,但是不能被捕获,所以不需要添加他
// signal.Notify把收到的syscall.SIGINT或syscall.SIGTREN信号传给quit
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) // 此处不会阻塞
<-quit // 阻塞在此,当收到上述两种信号的时候才会往下执行
log.Println(“ShutDown Server …”)
// 创建一个5秒超时的context
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
// 5秒内优雅关闭服务, (将未处理完的请求处理完再关闭服务), 超过5秒就退出
if err := srv.Shutdown(ctx); err != nil {
log.Fatal(“shut down:”, err)
}
log.Println(“Server exiting…”)
}
**优雅的重启:**(实际使用的比较少) 可以使用`fvbock/endless` 来替换默认的 `ListenAndServe`启动服务来实现。 **流程** * 在终端执行`go build -o graceful_restart`编译,并执行`./graceful_restart`,终端输出当前pid(假设为44444)。 将代码中处理请求函数返回的`hello xiaosheng!`修改为`hello world!`,再次编译`go build -o graceful_restart`。 * 打开浏览器访问`127.0.0.1:8080/`,此时客户端浏览器等待服务端返回响应。 * 在终端执行`kill -1 44444`命令给程序发送`syscall.SIGHUP`重启信号。 * 等第3步客户端浏览器收到响应信息hello xiaosheng!后,再次访问127.0.0.1:8080/会收到hello world!的响应。 **代码**:
import (
“log”
“net/http”
“time”
"github.com/fvbock/endless"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET(“/”, func(c *gin.Context) {
// 这个5秒的延时。是为了演示操作方便,实际上线一定注释掉
time.Sleep(5 * time.Second)
c.String(http.StatusOK, “hello gin!”)
})
// 默认endless服务器会监听下列信号:
// syscall.SIGHUP,syscall.SIGUSR1,syscall.SIGUSR2,syscall.SIGINT,syscall.SIGTERM和syscall.SIGTSTP
// 接收到 SIGHUP 信号将触发fork/restart
实现优雅重启(kill -1 pid会发送SIGHUP信号)
// 接收到 syscall.SIGINT或syscall.SIGTERM 信号将触发优雅关机
// 接收到 SIGUSR2 信号将触发HammerTime
// SIGUSR1 和 SIGTSTP 被用来触发一些用户自定义的hook函数
if err := endless.ListenAndServe(“:8080”, router); err!=nil{
log.Fatalf(“listen: %s\n”, err)
}
log.Println("Server exiting...")
这样做在不影响当前未处理完请求的同时完成了程序代码的替换,实现了平滑重启。`但实际上用的不多`,因为实际都是多台服务器,或者说有类似supervisor的软件管理进程时就不适用这种方式,因为他进程pid变了,他自己重启和supervisor的软件管理进程给他重启就冲突了。
## ※※※※※※※※※ 2023年面试 ※※※※※※※※※※※※
## 40 Golang怎么排查golang的内存泄露?
1. **使用内置工具pprof**:
* `Golang`提供了内置的性能分析工具,如`pprof`,用于分析内存使用情况。
* 通过导入`net/http/pprof`包,并在应用程序中启动一个`HTTP`服务器,可以使用浏览器访问`/debug/pprof`端点来查看内存分析数据。
* 例如,使用`go tool pprof`工具来生成内存使用的火焰图:
go tool pprof http://localhost:6060/debug/pprof/heap
2. **使用第三方工具**: * 第三方工具如pprof以外的工具,例如`Prometheus`、`Grafana`等,也可以用于内存分析。 * 这些工具可以帮助监控应用程序的内存使用情况,并可视化展示内存数据。 3. **检查goroutine泄漏**: * 内存泄漏有时与未正确关闭的`goroutine`相关。 * 使用`go tool pprof`或`runtime/pprof`包来查看当前正在运行的`goroutine`以及其状态,以确定是否有未释放的资源。 4. **使用内存分析工具**: * `Golang`的标准库中包含了`runtime/debug`包,可以用于检查堆中的对象数量和大小。 * 使用`debug.FreeOSMemory`来手动释放不再需要的内存。 5. **使用Go的内存分析工具**: * `go tool trace`工具可以用于生成跟踪文件,该文件提供了详细的时间线信息,可用于识别内存问题。 6. **代码审查**: * 仔细检查代码以确保没有存储指向对象的引用,但不再需要这些引用。 7. **使用静态分析工具**: * 一些静态代码分析工具,如`go vet`和`golint`,可以帮助识别潜在的内存泄漏问题。 8. **监控和测试**: * 集成监控和测试,确保应用程序在长时间运行时不会出现内存泄漏。 要排查内存泄漏,通常需要综合使用多种方法,包括运行时工具、静态分析、代码审查和性能测试。请注意,内存泄漏问题可能会相当复杂,因此可能需要耐心和时间来诊断和解决。 ## 41 Golang的类型转换,怎么将interface类型的数据转为string或者指定类型的结构体? 在`Go`中,要将`interface{}`类型的数据转换为其他类型,可以使用`类型断言(type assertion)`来完成。下面是将`interface{}`类型的数据转换为`string`和指定类型的结构体的示例: **将interface{}转换为string:**
var val interface{} = “Hello, World” // 一个interface{}类型的变量
if str, ok := val.(string); ok {
// 转换成功,str 现在是一个 string
fmt.Println(str)
} else {
// 转换失败
fmt.Println(“转换失败,不是一个字符串”)
}
使用类型断言将其转换为`string`类型。如果转换成功,`ok`将为`true`,并且`str`将包含字符串的值。
**将`interface{}`转换为指定类型的结构体:**
//结构体
type Person struct {
Name string
Age int
}
//将interface{}类型的数据转换为这个结构体类型
var val interface{} = Person{Name: “Alice”, Age: 30} // 一个interface{}类型的变量
if person, ok := val.(Person); ok {
// 转换成功,person 现在是一个 Person 结构体
fmt.Println(“Name:”, person.Name)
fmt.Println(“Age:”, person.Age)
} else {
// 转换失败
fmt.Println(“转换失败,不是一个 Person 结构体”)
}
如果转换成功,`ok`将为`true`,并且`person`将包含转换后的结构体。如果转换失败,则`ok`为`false`。 ## 42 go目前用的是哪个版本,有哪些特点? `Go 1.20.5`版本包含了三个安全修复,如下: 1. **cmd/go: cgo代码注入** 当使用`cgo`时,`go`命令可能在构建时生成意外的代码,导致运行一个使用`cgo`的`go`程序时出现意外的行为。这可能发生在运行一个包含有换行符的目录名的不可信模块时。使用`go`命令获取的模块(即通过`"go get"`)不受影响(使用`GOPATH`模式获取的模块,即`GO111MODULE=off`,可能受影响)。issue https://go.dev/issue/60167。 2. **runtime: setuid/setgid二进制文件的意外行为** `Go`运行时在一个二进制文件设置`setuid/setgid`位时没有做任何不同的处理。在`Unix`平台上,如果一个`setuid/setgid`二进制文件在执行时标准输入/输出文件描述符被关闭,打开任何文件可能导致以提升的权限读写意外的内容。类似地,如果一个`setuid/setgid`程序被终止,无论是通过`panic`还是信号,它可能泄露它的寄存器内容。issue https://go.dev/issue/60272。 3. **cmd/go: LDFLAGS的不恰当处理** 当使用`cgo`时,`go`命令可能在构建时执行任意代码。这可能发生在运行`"go get"`获取一个恶意模块时,或者运行任何其他构建不可信代码的命令时。这可以通过链接器标志触发,通过`"#cgo LDFLAGS"`指令指定。issues https://go.dev/issue/60305 和 https://go.dev/issue/60306。 ## 43 golang的slice? 然后我比如说现在有一个切片的一个对象,我是不断地扩容,然后可能从最开始的几个小k,然后几个小b,然后随着不断的扩容变成了几个大KB,然后但是这个对象我后来不引用了,或者说我这个对象我觉得它太大了,它 go 的话会有一种机制给它把它的占用的内存变小吗? 答:对于不再使用的切片,垃圾回收机制会自动回收。 ## 44 对Goroutine进行一个读写,然后阻塞了它,逻辑处理器P和操作系统线程M会发生什么样的一个对应的一个操作? 在`Go`中,`Goroutine`是轻量级的用户态线程,而逻辑处理器`P`是`Go`运行时调度器的一部分,它负责管理`Goroutine`的执行。操作系统线程M是运行时的底层执行单元,`Go`运行时会维护一组`M`以执行`Goroutine`。 当一个`Goroutine`发生读写并被阻塞时,以下是可能的情况: 1. **Goroutine阻塞**:如果`Goroutine`需要等待某些资源,例如文件`I/O`、网络请求、锁等,它会进入阻塞状态,不再执行。此时,该`Goroutine`不会占用逻辑处理器`P`的执行时间,以便其他`Goroutine`可以在该`P`上运行。 2. **逻辑处理器P空闲**:当`Goroutine`阻塞时,其所在的逻辑处理器`P`可能会变为空闲状态,因为没有可执行的任务。`Go`运行时的调度器将会选择一个新的`Goroutine`来运行,如果有多个逻辑处理器可用,那么其他逻辑处理器上的`Goroutine`也会被执行。 3. **操作系统线程M仍在运行**:尽管`Goroutine`阻塞了,但底层的操作系统线程`M`通常不会因此而阻塞。`Go`运行时的调度器会继续管理操作系统线程`M`,并确保它们在需要时可以运行其他`Goroutine`。 4. **等待事件通知**:当阻塞的`Goroutine`等待某个事件发生(如文件可读、网络连接成功等),它可能会进入等待事件通知的状态,这是一种非常高效的方式,因为它不会消耗`CPU`时间,只有在事件发生时才会被唤醒。 **总结:** 当一个`Goroutine`阻塞时,逻辑处理器`P`会寻找其他可运行的`Goroutine`来填充其空闲时间,而操作系统线程`M`仍然会保持活动状态以继续执行其他`Goroutine`。这是`Go`并发模型的一个关键特点,可以有效地管理大量`Goroutine`,确保程序在并发执行中高效运行。 ## 45 Golang有哪些数据类型 1. 布尔型,值只可以是常量`true`或`false` 2. 数字类型,支持整型和浮点型数字,并且支持复数 3. 字符串类型,是一串固定长度的字符连接起来的字符序列 4. 指针类型 5. 数组类型 6. 结构化类型 7. `Channel`类型 8. 函数类型 9. 切片类型 10. 接口类型 11. `Map`类型 ## 46 Golang开发中的几种引号就是单引号、双引号、反引号的区别 * 单引号在`Golang`语言中表示`Golang`中的`rune`(`int32`)类型,`byte`(`int8`别称),单引号里面是单个字符,对应的值为改字符的`ASCII`值。 * 双引号对应数据类型是`string`,单个字符也是字符串,字符串可以有转义字符,如`\n`、`\r`、`\t`等。 * 反引号中的字符表示其原生字符串,在单引号中的内容可以是多行内容,不支持转义。 ## 47 Golang的空结构一般怎么使用 参考:[详解 Go 空结构体的 3 种使用场景]( ) 在`Golang`语言中的每个值都有一个类型,值的宽度由其类型定义,并且总是8 bits的倍数。借助`unsafe.Sizeof`方法,来获取值的宽度。 空结构体在各类系统中频繁出现的原因之一,就是需要一个占位符,空结构体不占据内存空间,即便是变形处理也一样。 **使用场景**: 1. 实现集合类型 2. 实现空通道 3. 实现方法接收者 **实现集合类型**: `Golang`语言本身是没有集合类型(`Set`),通常是使用`map`来替代。但有个问题:就是集合类型,只需要用到`key`(键),不需要用到`value`(值),如果`value`使用`bool`来表示,实际会占用1个字节的空间,为了节省空间,这时就可以使用空结构体来替代。
package main
import “fmt”
type Set map[string]struct{}
func (s Set) Append(k string) {
s[k] = struct{}{}
}
func (s Set) Remove(k string) {
delete(s, k)
}
func (s Set) Exist(k string) bool {
_, ok := s[k]
return ok
}
func main() {
set := Set{}
set.Append(“煎鱼”)
set.Append(“咸鱼”)
set.Append(“蒸鱼”)
set.Remove(“煎鱼”)
fmt.Println(set.Exist("煎鱼"))
}
**实现空通道**:
在`Golang`语言`channel`的使用场景中,常常会遇到通知型`channel`,其不需要发送任何数据,只是用于协调`Goroutine`的运行,用于流转各类状态或是控制并发情况。
这类情况就特别适合使用空结构体,只做个占位,不浪费内存空间。
package main
func main() {
ch := make(chan struct{})
go worker(ch)
// Send a message to a worker.
ch <- struct{}{}
// Receive a message from the worker.
<-ch
println(“AAA”)
//输出:
//BBB
//AAA
}
func worker(ch chan struct{}) {
// Receive a message from the main program.
<-ch
println(“BBB”)
// Send a message to the main program.
close(ch)
}
**实现方法接收者**:
使用结构体类型的变量作为方法接收者,有时结构体可以不包含任何字段属性。这种情况,可以用int或者string来替代,但它们都会占用内存空间,所以使用空结构体是比较合适的。
并且也有利于未来针对该类型进行公共字段等的增加,容易扩展和维护。
package main
import “fmt”
type T struct{}
func methodUse() {
t := T{}
t.Print()
t.Print2()
//输出:
//哈哈哈Print
//哈哈哈Print2
}
func (t T) Print() {
fmt.Println(“哈哈哈Print”)
}
func (t T) Print2() {
fmt.Println(“哈哈哈Print2”)
}
**总结**:只用占位不用实际含义,那么我们就都可以使用空结构体,可以极大的节省不必要的内存开销。 ## 48 哪些情况下会使用深度拷贝 1. **避免共享引用**:当你需要确保修改一个数据结构不会影响其他引用该数据结构的地方时,深度拷贝是必要的。如果你只做浅拷贝,多个变量可能会引用相同的底层数据,这样一个地方的修改会影响其他地方。 2. **数据传递**:当你需要将一个数据结构传递给一个函数,并且希望在函数内部对该数据进行修改而不影响原始数据时,你应该使用深度拷贝。否则,传递的数据可能会被多个地方共享。 3. **数据快照**:有时,你可能需要创建一个数据的快照,以记录某个时间点的数据状态。深度拷贝可以用于创建不同时间点的数据副本,以便后续比较或还原数据状态。 4. **递归数据结构**:当你处理递归数据结构,如树或图,深度拷贝通常是必要的,以确保整个结构及其子结构都被复制。 5. **结构体中包含引用类型**:如果一个结构体中包含切片、映射、通道或指针等引用类型,并且你希望复制整个结构体以及其引用的数据,而不是仅复制引用,那么深度拷贝是必要的。 在Go中,深度拷贝可以通过自定义函数来实现,遍历数据结构并创建一个完全独立的副本。有一些第三方库,如`github.com/mohae/deepcopy`,也提供了深度拷贝的实现。需要注意的是,深度拷贝可能会涉及到性能和内存开销,因此在使用时需要谨慎考虑。 ## 49 Golang反射的运行速度怎么样 `Go`的反射(`Reflection`)机制提供了在运行时动态检查和操作变量、类型和结构的能力。虽然反射是一项非常强大的功能,但它通常比静态类型检查和编译时优化的代码要慢。这是因为反射需要在运行时进行类型检查和操作,而这些操作会引入额外的开销。 **具体来说,`Go`的反射速度相对较慢主要有以下原因:** 1. **动态类型检查**:反射需要在运行时进行类型检查,以确定变量的类型。这会导致额外的性能开销,因为在编译时无法进行类型优化。 2. **间接访问**:通过反射,你需要通过接口类型来访问变量的值,这通常涉及到额外的间接访问,会增加访问变量值的时间成本。 3. **运行时分配和释放内存**:反射通常涉及在运行时分配内存来存储反射对象的信息,这会增加内存分配和垃圾回收的负担。 4. **编译器优化受限**:`Go`的编译器无法在反射代码上执行像静态代码一样的优化,因为反射的行为在编译时是未知的。 尽管反射的性能较低,但在某些情况下仍然是有用的,例如编写通用库、处理未知类型的数据、实现`ORM`(对象关系映射)等。然而,应该避免不必要的反射操作,尤其是在性能敏感的代码中。 如果性能是关键问题,应该尽量避免过度使用反射,而是在编译时尽可能静态类型检查和优化你的代码。在`Go`中,反射通常被视为最后的选择,仅在必要时才使用它。 ## 50 项目需要做代码覆盖测试吗 在`Go`中,可以使用内置的测试工具和`go test`命令来执行代码覆盖测试。代码覆盖测试可以帮助确定测试用例是否覆盖了代码库中的所有代码路径。以下是如何进行代码覆盖测试的步骤: 1. `编写测试用例`:首先,编写测试用例,通常在与被测试的代码文件相同的目录中创建一个以 \_test.go 结尾的文件,并编写测试函数。确保测试覆盖到要测试的各个功能和代码路径。 2. **启用代码覆盖率分析**:在测试文件中,可以使用`testing`包提供的`Cover`函数来启用代码覆盖率分析。例如:
import “testing”
import “testing/quick”
func TestMyFunction(t *testing.T) {
// 启用代码覆盖率分析
c := testing.Cover(func() { myFunctionToTest() })
// 运行测试用例
if testing.Short() {
t.Skip("skipping test in short mode.")
}
// 运行快速检查测试
quick.Check(t, c, nil)
}
3. **运行代码覆盖测试**:使用`go test`命令来运行代码覆盖测试。确保使用`-cover`标志来启用代码覆盖测试:
go test -cover
运行测试后,`Go`将生成一份代码覆盖率报告,显示哪些代码路径被测试覆盖,哪些没有。 4. **查看代码覆盖率报告**:`go test`命令运行后,会生成代码覆盖率报告,显示测试覆盖的代码行和百分比。可以查看报告,以确定哪些代码路径没有被覆盖,需要进一步的测试。 代码覆盖测试是一种重要的测试工具,可以帮助发现未被测试的代码路径和潜在的问题。它可以在开发周期中定期运行,以确保代码库的稳健性。同时,还可以使用一些工具来生成更详细的代码覆盖率报告,如`go tool cover`。这些工具可以帮助更好地理解测试覆盖情况。 ## 51 Golang里面的逃逸分析是什么? 在`Go`语言中,逃逸分析(Escape Analysis)是一种编译器优化技术,用于决定一个变量是否逃逸到堆上分配内存,或者可以在栈上分配。 逃逸分析的目标是尽可能地在栈上分配内存,因为在栈上分配内存比在堆上分配更加高效。栈上分配内存通常比堆上分配更快,因为它只是简单的移动栈指针,而不需要进行垃圾回收。 以下是逃逸分析的一些基本概念和规则: 1. **栈上分配**:如果一个变量在函数内部定义,并且在函数结束后不会被引用(即不会逃逸到函数外部),那么编译器会尝试在栈上分配内存,而不是在堆上。 2. **逃逸到堆**:如果一个变量在函数内部定义,但在函数结束后仍然被引用(逃逸到函数外部),那么编译器可能会决定在堆上分配内存,以保证在函数返回后变量仍然可用。 逃逸分析的结果对于编译器的性能优化和程序的运行时行为有重要影响。通过减少对堆的使用,可以提高程序的性能,减小垃圾回收的压力。 在`Go`语言中,可以使用`-gcflags`标志来查看逃逸分析的结果,例如:
go build -gcflags=“-m”
上述命令会输出关于逃逸分析的详细信息,包括哪些变量逃逸到堆上。这对于优化代码、理解代码的内存使用和性能分析都是有帮助的。
## 52 Go语言中内存逃逸是什么,产生内存逃逸的原因是什么?
在Go语言中,内存逃逸(memory escape)是指在函数中分配的变量或对象被函数外部引用,从而导致这些变量或对象在堆上分配而不是在栈上。在Go中,栈上分配的变量有更短的生命周期,而堆上分配的变量则可能存活得更久。
产生内存逃逸的主要原因是编译器无法保证变量的生命周期仅限于函数的执行期间,因此将其分配到栈上可能导致在函数返回后仍然被引用。在这种情况下,编译器会将变量分配到堆上,以确保其在函数返回后仍然有效。
1. **函数返回指针**:当一个函数返回一个局部变量的指针时,这个指针可能被保存到函数外部,导致变量逃逸到堆上。
func createObject() *Object {
obj := Object{} // 局部变量
return &obj // 返回局部变量的指针
}
2. **闭包**:当一个函数返回一个闭包,并且该闭包引用了外部函数的局部变量时,这些变量可能逃逸到堆上。
func closureExample() func() int {
x := 0
return func() int {
x++
return x
}
}
3. **并发**:在并发程序中,如果一个goroutine分配的变量被其他goroutine访问,那么这个变量可能逃逸到堆上。
func concurrentExample() {
var data []int
go func() {
data = make([]int, 100)
// 修改 data
}()
// 在其他 goroutine 中使用 data
}
4. **接口**:当变量被分配给一个接口类型并被传递到函数外部时,它可能逃逸到堆上。
func interfaceExample() interface{} {
obj := Object{} // 局部变量
return obj // 返回局部变量的接口类型
}
编译器通过逃逸分析来判断是否将变量分配到堆上,以及何时将其分配到堆上。Go语言的编译器在编译时会进行逃逸分析,以尽可能减少堆分配,提高性能。然而,了解内存逃逸的原因仍然对于理解代码的性能特征和调试性能问题是有帮助的。 ## 53 Golang里面的函数,返回一个局部变量,这种是正常的吗?会不会有问题? 在`Go`语言中,返回局部变量的函数是正常的,并且不会导致内存安全问题。`Go`的编译器和运行时系统在处理这种情况时会进行逃逸分析,以确定是否需要在堆上分配内存。 如果一个局部变量没有逃逸到函数外部,即没有被函数外的代码引用,编译器会进行栈上分配。栈上分配内存是一种非常高效的方式,因为它只涉及移动栈指针,而不需要进行垃圾回收。局部变量在函数返回时会被自动销毁,不需要程序员手动释放。 **以下是一个例子:**
package main
import “fmt”
func localVariable() int {
// x 是局部变量,会在函数返回时自动销毁
x := 42
return x
}
func main() {
result := localVariable()
fmt.Println(result)
}
在这个例子中,`localVariable`函数返回一个局部变量`x`。由于`x`没有逃逸到函数外部,因此编译器可以选择在栈上分配内存。
总体来说,`Go`语言的设计和编译器优化使得返回局部变量是安全而高效的。在编写代码时,可以专注于代码的清晰性和可读性,而不必过度关心内存管理的细节。
## 54 Golang用过哪些锁?阻塞锁、饥饿锁?
Go语言提供了几种锁的实现,其中包括阻塞锁、饥饿锁等。以下是Go语言中常见的锁的种类:
1. **Mutex(互斥锁)**:sync.Mutex是Go语言中最基本的锁,用于保护共享资源,防止多个goroutine同时访问。它是一个阻塞锁,当一个goroutine获得锁后,其他 goroutine将被阻塞直到该goroutine释放锁。
var mu sync.Mutex
// 加锁
mu.Lock()
// 操作共享资源
// 解锁
mu.Unlock()
2. **RWMutex(读写锁)**:sync.RWMutex是一个读写锁,它允许多个goroutine同时读取共享资源,但只有一个goroutine能够写入。在读锁定时,其他读操作可以同时进行,但写操作会被阻塞。在写锁定时,所有读和写操作都会被阻塞。
var rwmu sync.RWMutex
// 读锁定
rwmu.RLock()
// 读取共享资源
// 读解锁
rwmu.RUnlock()
// 写锁定
rwmu.Lock()
// 写入共享资源
// 写解锁
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
运行测试后,Go
将生成一份代码覆盖率报告,显示哪些代码路径被测试覆盖,哪些没有。
4. 查看代码覆盖率报告:go test
命令运行后,会生成代码覆盖率报告,显示测试覆盖的代码行和百分比。可以查看报告,以确定哪些代码路径没有被覆盖,需要进一步的测试。
代码覆盖测试是一种重要的测试工具,可以帮助发现未被测试的代码路径和潜在的问题。它可以在开发周期中定期运行,以确保代码库的稳健性。同时,还可以使用一些工具来生成更详细的代码覆盖率报告,如go tool cover
。这些工具可以帮助更好地理解测试覆盖情况。
在Go
语言中,逃逸分析(Escape Analysis)是一种编译器优化技术,用于决定一个变量是否逃逸到堆上分配内存,或者可以在栈上分配。
逃逸分析的目标是尽可能地在栈上分配内存,因为在栈上分配内存比在堆上分配更加高效。栈上分配内存通常比堆上分配更快,因为它只是简单的移动栈指针,而不需要进行垃圾回收。
以下是逃逸分析的一些基本概念和规则:
逃逸分析的结果对于编译器的性能优化和程序的运行时行为有重要影响。通过减少对堆的使用,可以提高程序的性能,减小垃圾回收的压力。
在Go
语言中,可以使用-gcflags
标志来查看逃逸分析的结果,例如:
go build -gcflags="-m"
上述命令会输出关于逃逸分析的详细信息,包括哪些变量逃逸到堆上。这对于优化代码、理解代码的内存使用和性能分析都是有帮助的。
在Go语言中,内存逃逸(memory escape)是指在函数中分配的变量或对象被函数外部引用,从而导致这些变量或对象在堆上分配而不是在栈上。在Go中,栈上分配的变量有更短的生命周期,而堆上分配的变量则可能存活得更久。
产生内存逃逸的主要原因是编译器无法保证变量的生命周期仅限于函数的执行期间,因此将其分配到栈上可能导致在函数返回后仍然被引用。在这种情况下,编译器会将变量分配到堆上,以确保其在函数返回后仍然有效。
func createObject() \*Object {
obj := Object{} // 局部变量
return &obj // 返回局部变量的指针
}
func closureExample() func() int {
x := 0
return func() int {
x++
return x
}
}
func concurrentExample() {
var data []int
go func() {
data = make([]int, 100)
// 修改 data
}()
// 在其他 goroutine 中使用 data
}
func interfaceExample() interface{} {
obj := Object{} // 局部变量
return obj // 返回局部变量的接口类型
}
编译器通过逃逸分析来判断是否将变量分配到堆上,以及何时将其分配到堆上。Go语言的编译器在编译时会进行逃逸分析,以尽可能减少堆分配,提高性能。然而,了解内存逃逸的原因仍然对于理解代码的性能特征和调试性能问题是有帮助的。
在Go
语言中,返回局部变量的函数是正常的,并且不会导致内存安全问题。Go
的编译器和运行时系统在处理这种情况时会进行逃逸分析,以确定是否需要在堆上分配内存。
如果一个局部变量没有逃逸到函数外部,即没有被函数外的代码引用,编译器会进行栈上分配。栈上分配内存是一种非常高效的方式,因为它只涉及移动栈指针,而不需要进行垃圾回收。局部变量在函数返回时会被自动销毁,不需要程序员手动释放。
以下是一个例子:
package main
import "fmt"
func localVariable() int {
// x 是局部变量,会在函数返回时自动销毁
x := 42
return x
}
func main() {
result := localVariable()
fmt.Println(result)
}
在这个例子中,localVariable
函数返回一个局部变量x
。由于x
没有逃逸到函数外部,因此编译器可以选择在栈上分配内存。
总体来说,Go
语言的设计和编译器优化使得返回局部变量是安全而高效的。在编写代码时,可以专注于代码的清晰性和可读性,而不必过度关心内存管理的细节。
Go语言提供了几种锁的实现,其中包括阻塞锁、饥饿锁等。以下是Go语言中常见的锁的种类:
var mu sync.Mutex
// 加锁
mu.Lock()
// 操作共享资源
// 解锁
mu.Unlock()
var rwmu sync.RWMutex // 读锁定 rwmu.RLock() // 读取共享资源 // 读解锁 rwmu.RUnlock() // 写锁定 rwmu.Lock() // 写入共享资源 // 写解锁 [外链图片转存中...(img-B6jhM542-1715556378091)] [外链图片转存中...(img-g5wVE8HN-1715556378091)] **网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。** **[需要这份系统化的资料的朋友,可以添加戳这里获取](https://bbs.csdn.net/topics/618658159)** **一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。