map
1.slice和map分别作为函数参数时有什么区别吗?
slice是一个结构体,做函数参数传递的时候会产生一个结构体的副本,但因为这个结构体里包含了一个指向底层数组的指针,修改底层数组会影响原来的slice,如果进行了append扩容,那么与原切片就无关了。 map就是指向hamp结构体的指针,所以传参后仍然指向同一个底层数组,所以操作map会影响实参。
2.map如何顺序读取?
func main() {
m := map[int]string{11: "a", 2: "b", 3: "c", 0: "d"}
var ks []int
for i := range m {
ks = append(ks, i)
}
sort.Ints(ks)
for _, v := range ks {
fmt.Println(m[v])
}
}
3.map使用注意的点,并发安全吗?
注意的点:1、map不是线程安全的,并发读写不安全。 2、map的零值是nil,对nil的读操作是安全的,但是写操作会引发panic。因此,注意使用make前要初始化。 3、key使用多重返回值来检查。 4、key的类型必须是可比较类型。 5、删除一个不存在的key是不会panic。
map不是并发安全的。
4.map中删除一个key,内存会释放吗?
不会。
map中删除一个key,只是将这个key在底层的tophash、key、value清零,但是内存是没有被释放的,只是修改一个标记,将tophash置为emptyOne,如果后边没有tophash了,就设置当前的tophash为emptyReset,然后向前检查。如果key没有存在的话,也不会panic。
5.怎么处理对map进行并发访问?有没有其他方案?区别是什么?
1.上锁解决并发安全问题
package main
import (
"fmt"
"sync"
)
var mu sync.Mutex
func main() {
m := make(map[int]string)
var wg sync.WaitGroup
wg.Add(2)
m[0] = "你好"
go func() {
defer wg.Done()
mu.Lock()
m[0] = "a"
mu.Unlock()
}()
go func() {
defer wg.Done()
mu.Lock()
fmt.Println(m[0])
mu.Unlock()
}()
wg.Wait()
fmt.Println(m[0])
}
2.使用自带的sync.Map进行并发读写
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
m.Store(1, "i")
}()
go func() {
defer wg.Done()
v, _ := m.Load(1)
fmt.Println(v)
}()
wg.Wait()
fmt.Println(m.Load(1))
}
6.nil map和空 map有何不同?
nil map是一个未初始化的map,其值为nil。不能向其中添加任何元素,否则会panic。但是可以从nil map中获取元素,这不会引发错误,但总是返回元素类型的零值。
空map是一个已经初始化但没有包含任何元素的map。可以向空map添加元素,也可以从空map中获取元素。
7.map的数据结构是什么?是怎么实现扩容的呢?
Go 语言中的 map
是哈希表的实现,底层结构是通过 hmap
和 bmap
组合起来的。map
的核心结构是 hmap
,
type hmap struct {
count int // map 中的元素个数
B uint8 // bucket 数量的对数,2^B 表示 bucket 的数量
buckets unsafe.Pointer // 指向一个数组,每个元素是一个 bucket
oldbuckets unsafe.Pointer // 指向旧的 bucket 数组,用于扩容过程中的数据搬迁
...
}
每个 hmap
通过 buckets
指向一个数组,数组的每个元素是一个 bucket
,bucket
中存储多个键值对,bucket
的定义为:
type bmap struct {
tophash [8]uint8 // 哈希值的高 8 位,用于加快查找
keys [8]KeyType // 存储 key
values [8]ValueType // 存储 value
}
每个 bucket
存储 8 个键值对,这使得即使哈希冲突时,查找效率依然较高。
扩容机制
map触发扩容的条件是负载因子大于6.5(双倍扩容)或溢出桶数量过多(等量扩容)。触发扩容后进行数据迁移,但是map的数据迁移采用渐进式迁移策略,在每次对map进行写操作的时候,逐步将旧的bucket中的数据迁移到新的bucket,同时顺带处理一部分未迁移旧的bucket。
总结:map扩容策略就是当负载因子大于6.5时,发生双倍扩容;当溢出桶过多,发生等量扩容。溢出桶过多的标志:溢出桶数量大于正常桶或溢出桶数量大于2^15 。
8.map的key为什么是可比较类型?
Go 的 map 是基于哈希表实现的。为了能够将键值对存储在正确的 bucket 中,必须通过对键计算哈希值来确定其存储位置。这要求 map 的键必须是可比较的,以确保:
哈希值稳定:键的哈希值必须是确定且可重复的,只有可比较的类型才能保证键在不同操作中生成相同的哈希值,从而能够正确定位键值对。 冲突处理:当两个键的哈希值相同时,哈希表会使用键的比较来区分它们。如果键不可比较,则无法判断两个键是否相等,也就无法正确处理哈希冲突。
9 为什么map遍历是无序的?
map在遍历的时候会随机选择一个桶号和槽位开始,因为在map扩容后,会发生key的迁移,原来在同一个bucket中的key,迁移后,有些key的位置就会发生改变。而遍历的过程,就是按顺序遍历bucket。迁移后,key的位置发生了重大变化,就会导致map遍历的结果是无序的了。
一方面,因为go的扩容是渐进式的,在遍历map的时候,可能发生扩容,一旦发生扩容,key的位置就发生了变化; 另一方面,hash表中数据每次插入的位置是变化的,同一个map变量内,数据删除再添加的位置也有可能变化,因为在同一个桶及溢出链表中数据的位置不分先后,所以map的遍历结果是无序的。
10 为什么map的负载因子是6.5?
负载因子 = 哈希表存储的kv数量 / 桶个数
负载因子越大,添加的数据就越多,发生hash冲突的几率就更大。
负载因子越小,添加的数据就越少,冲突发生的几率减小,空间浪费比较大,还会提高扩容次数。
每个bucket有8个空位,当负载因子是6.5的时候也就是一个数组桶快用完的时候,所以在此时有必要扩容。
11 sync.Map和map谁的性能高?
map性能高。
因为sync.Map为了保证线程安全,以空间换时间,采用read和dirty两个map,用了原子操作和锁来实现线程安全,在操作过程加锁会造成性能损耗。而map适配的场景都是简单的,不需要在并发环境下访问,因此不需要锁的操作,这也导致了map是非线程安全的。
interface
1.iface和eface有什么区别?
iface是普通接口类型,表示某种方法集的接口,结构体包含了两个字段,一个是描述接口类型,另一个是data指向具体的数据。eface是指空接口类型,表示空接口interface{},即没有任何方法的接口。普通接口主要用于约束类型,确保满足某个方法集合,从而进行多态操作。空接口常用于存储任何类型的数据,因为它没有方法约束,所以能够泛化处理各种类型。
context
1.context结构是什么?
context是1.17版本引入的一个接口,里面有四个方法:
Deadline()(deadline time.Time,ok bool) | 返回Contex被取消的时间 |
---|---|
done() <-chan struct{} | 返回一个channel,当context取消或达到截止时间,channel关闭 |
Err() error | 返回context结束的原因 |
Value(key any)any | 获取键值对,是通过WithValue写入的 |
2.context的使用场景和用途?
1、context主要用来在goroutine之间传递上下文信息,比如传递请求的trace_id,以便于追踪全局唯一请求。
2、也可以做取消控制,通过取消信号和超时时间来控制子goroutine的退出,防止goroutine泄露。
3.context有哪几种数据结构?
四种,emptyContext、cancelContext、timerContext、valueContex。
emptyContext
emptyContext是一个基本的Context。
cancelContext
cancelContext同时实现了Context和Canceler接口,通过取消函数cancelFunc实现退出通知。其退出通知机制不但通知自己,同时也通知其children节点。
timerContext
timerContext是一个实现了Context接口的具体类型,其内部封装了cancelContext类型实例,同时也有deadline变量,用来实现定时退出通知。
valueContext
valueContext是一个实现了Context接口的具体类型,其内部封装了Context接口类型,同时也封装了一个k/v的存储变量,实现了数据传递。