第一种方式:
1、可以先定义key对应类型的切片,然后将map中的key存入到切片中,接着对切片排序。
2、循环切片,通过map访问切片值(key),这样就有序了
func main() {
m := map[int]string{
1001: "张三",
9012: "李四",
1010: "王武",
1011: "订单",
1000: "大男",
7610: "李四",
8910: "李四",
}
var s = make([]int, 0, len(m))
for i := range m {
s = append(s, i)
}
sort.Ints(s)
for _, val := range s {
fmt.Println(m[val])
}
}
第二种方式:
1、定义结构体用于存储map中的key和value
2、自定义排序规则,重新排序
type Dict struct {
Key int
Val string
}
func main() {
m := map[int]string{
1001: "张三",
9012: "李四",
1010: "王武",
1011: "订单",
1000: "大男",
7610: "李四",
8910: "李四",
}
var dicts = make([]Dict, 0, len(m))
for i := range m {
dicts = append(dicts, Dict{
Key: i,
Val: m[i],
})
}
// 自定义排序的规则, < 表示从小到大进行排序
sort.Slice(dicts, func(i, j int) bool {
return dicts[i].Key < dicts[j].Key
})
for _, v := range dicts {
println(v.Key, v.Val)
}
}
1、map底层是用hash表实现的,一个hash表有多个bucket,而每个bucket保存map中的一个或多组键值对
2、map底层结构是hmap
type hmap struct {
count int //当前保存元素的个数
B uint //bucket 数组大小
buckets unsafe.Pointer //bucket数组,数组长度为2^B
oldbuckets unsafe.Pointer //老旧bucket数组,用于扩容
}
每个元素经过hash运算后会落到每个桶中
3、bucket数据结构
type bmap struct {
tophash [8]uint8 //存储hash值的高8位
data []byte //key value 数据:key/key/key/key/key..../value/value/value/value
overflow *bmap //溢出bucket的地址
}
1)hash值相同的键存入当前桶的时候,会将hash值高8位存储在该数组中,以便后续匹配
2)data存放的是key-value数据,存放顺序是key/key/key/key/key..../value/value/value/value ,
如此存放是为了节省字节对齐带来的空间浪费
4、hash冲突
当有两个或者以上数量的键被hash到了同一个桶时,我们称这些键发生了冲突,go使用链地址法来解决键冲突。
由于每个bucket可以存放8个键值对,所以同一个bucket存放超过8个键值对时就会再创建一个键值对,用类似
链表的方式将bucket连接起来。
5、负载因子
负载因子用于衡量一个hash表冲突的情况,公示为
负载因子 = 键数量/bucket 数量
负载因子过小或过大都不理想
负载因子过大,说明冲突严重,存储效率低
负载因子过小,可能预分配的空间空间太大,也可能是大部分元素被删除造成的。随着元素不断的添加到map中
负载因子会逐渐升高
负载因子过大,需要申请更多的bucket,并对所有的键值对重新组织,使其均匀的分布到这些bucket中,这个过程叫rehash.
go语言中map的负载因子达到6.5会触发rehash。
6、扩容条件
负载因子大于6.5
overflow的数量大于2^15
7、增量扩容
当负载因子过大时,就会新建一个bucket数组,新的bucket数组的长度是原来的2倍,然后旧的bucket数组中的数据搬迁到新的
bucket数组中。
考虑到map存储了数亿亿计的键值对,那么一次搬迁会造成较大的延迟,go采用逐步搬迁策略,每次访问map的时会触发一次搬迁,每次
搬迁两个键值对
8、等量扩容
就是重新搬迁,把松散的键值对重新排列一次,以使bucket的使用率更高,进而保证更快的存储速度。
9、增删改查
查找过程
1)根据key值计算hash值
2)取hash值低位与hmap.B取模来确定bucket的位置
3)取hash值高位,在tophash数组中查询定位到key高八位位置
4)在data数组中通过key在tophash高8位对应的位置找到对应的key
5) 比较要找的key是否在data数组中,如果存在,就取key对应的value
6) 如果没有找到,就继续从溢出的bucket中查找
7)如果当前map处于搬迁的过程,那么优先从oldbuckets 数组中查找,不再从新的buckets数组中查找
8) 如果查询不到,那么也不会返回nil,而是会返回相应类型的零值。
调度模型
1、线程模型
线程可分为用户线程和内核线程,用户线程由用户创建,同步和销毁,内核线程由内核来管理,根据用户线程管理方式不同
分为三种线程模型。
一种是N:1的线程模型,也就是说N个线程模型运行在一个内核线程中,优点是用户线程上下文切换快,缺点是无法充分利用CPU的
多核算力。
另一种是1:1的线程模型,每个用户线程对应一个内核线程,充分利用cpu算力,缺点上下文切换比较慢。
Go实现的是M:N模型,也就是前两种模型的结合,M个用户线程运行在N个内核线程中
2、调度器模型
M machine: 工作线程,它由操作系统调度
P processor: 包含运行go代码的重要资源,也有调度goroutine的能力
G goroutine: GO协程,每个go关键字都会创建一个协程
全局队列(Global Queue):存放等待运行的 G。
P 的本地队列:同全局队列类似,存放的也是等待运行的 G,存的数量有限,不超过 256 个。新建 G’时,G’优先加入到 P 的本地队列,如果队列满了,则会把本地队列中一半的 G 移动到全局队列。
P 列表:所有的 P 都在程序启动时创建,并保存在数组中,最多有 GOMAXPROCS(可配置) 个。
M:线程想运行任务就得获取 P,从 P 的本地队列获取 G,P 队列为空时,M 也会尝试从全局队列拿一批 G 放到 P 的本地队列,或从其他 P 的本地队列偷一半放到自己 P 的本地队列。M 运行 G,G 执行之后,M 会从 P 获取下一个 G,不断重复下去。
1、1.3 stw 标记清除
2、1.5 三色标记
3、1.8 混合写屏障,栈无需stw
go语言也实现了内存分配器,原理与tcmalloc类似,简单的说维护了一块全局内存,每个处理器P维护一小块私有内存
私有内存不足的时候,再从全局申请
有缓冲管道:如果数据可以读取到具体数据,没有数据,只能读取到管道类型对应零值。
无换筹管道:只能读取到管道类型对应零值。
1、指针逃逸
我们知道Go可以返回局部变量指针,这其实是一个典型的变量逃逸案例。
type Student struct {
Name string
}
func Say() *Student {
stu := Student{}
stu.Name = "张三"
return &stu
}
2、栈空间不足逃逸
很多函数的参数为interface 类型,比如 fmt.Println(a ...interface{}),编译期间很难确定其参数的具体类型,也会产生逃逸
func slice() {
s := make([]int, 10000, 10000)
for index, _ := range s {
s[index] = index
}
}
3、动态类型逃逸
s := "sbc"
fmt.Println(s)
4、闭包引用对象逃逸
func bibao() func() int {
a, b := 100, 10
return func() int {
return a + b
}
}
该函数返回一个闭包,闭包引用了函数的局部变量a和b,使用时通过该函数获取闭包
小结:
栈上分配内存比在堆中分配内存有更高的效率;
栈上分配的内存不需要GC处理;
逃逸分析的目的是决定分配地址是栈还是堆;
逃逸分析在编译阶段完成;
defer不仅可以用于资源释放,也可以用于流程控制和异常处理,但defer关键字只能用于函数或函数调用。
defer关键字后接一个匿名函数
defer func(){
fmt.Println("hello world!")
}
defer关键字后接一个函数调用:
file,err :=os.open(name)
if err != nil {
return nil,err
}
defer file.Close()
1、使用场景
1)释放资源
m.mutex.Lock()
defer m.mutex.Unlock()
defer 常用于文件句柄、数据库连接,停止定时器 Ticker 以及关闭管道等资源清理场景 。
2)流程控制
var wg wait.group
defer wg.Wait()
defer 也用于控制函数的执行顺序,比如配合wait.Group实现等待携程的退出。
3)异常处理
defer 常用于处理异常,与recover配合可以消除panic,另外recover只能用于defer函数。
2、行为规则
1)规则一:延迟函数的参数在defer语句出现的时候就已经确定了
官方给出的例子如下图所示:
func f(){
i :=0
defer fmt.Println(i)
i++
return
}
defer语句中的fmt.Println()参数i值在defer出现时就已经确定了,实际上是复制了一份。后面对变量i的修改
不会影响fmt.Println()函数的执行,仍然打印0。
2) 规则二:延迟函数按照先进后出的顺序执行,也就是说在函数中,最先出现的defer最后执行。
3)规则三:延迟函数可能影响主函数的返回值
定义defer的函数可能有名字的返回值,也可能是没名字的返回值!延迟函数可能会影响到返回值。
那如果想要理解延迟函数是如何影响主函数返回值的,只要明白函数是如何返回就够了。
(1)有个事实必须要清楚,关键字return不是一个原子操作,实际上return只代表汇编指令ret,即跳转程序的执行。
比如return i,实际上是分两步执行,即先将i值存入栈中作为返回值,然后执行跳转,而defer的执行时机介于返回值
入栈与跳转之间。这意味着defer是有机会操作返回值的。
那举个例子:
func deferResult() (res int){
i :=1
defer func(){
res++
}
return i
}
该函数的return语句可以拆分成下面两行:
result = i
return
前面说过延迟函数在return之前执行
result = i //1
result++ //2
return
所以上面的返回值是 result++ 的值
(2)主函数有匿名函数返回值,则返回字面值。
一个主函数拥有一个匿名返回值,比如说1,2,3,"hello"等,那么defer是无法操作返回值的
比如下面一个返回字面值的函数:
func f() int{
var i int
defer func(){
i++
}
return 1
}
上面的return语句直接把1写入栈中作为返回值,延迟函数无法操作该返回值,索引就无法影响到该返回值。
(3)主函数拥有匿名返回值,返回变量。
一个主函数拥有一个匿名返回值,返回本地变量或者局部变量,这种情况下defer可以引用返回值,但不会改变返回值。
一个返回本地变量的函数如下:
func f() int{
var i int
defer func (){
i++
}
return i
}
上面的函数返回一个局部变量,同时defer函数也会操作这个局部变量。对于一个匿名返回值来说,可以假定一个匿名变量
来存储该返回值,那么上面的返回值可以拆分以下步骤:
anony = i
i++
return
由于i是整数,会将值复制给anony,所以defer语句中的修改不会函数返回值造成影响
题目一:公司有n个组,>=5,每组人数相同,>=2人,需要进行随机的组队吃饭。
要求:
1. 两两一队或三人一队,不能落单
2. 两人队、三人队各自的队伍数均不得少于2
3. 一个人只出现一次
4. 队伍中所有人不能来自相同组
5. 随机组队,重复执行程序得到的结果不一样,总队伍数也不能一样
6. 注释注释注释
注:要同时满足条件1-6,