你需要了解的Go的类型(空结构体,字符串,切片,map,接口,nil,内存对齐)

go当中的空结构体

1.空结构体的占用内存空间大小是0字节

2.独立的空结构体变量指针地址都是统一的zerobase,

3.用途:节约内存

4.应用场景1:hashset map只要key不要val m1:=map[string]struct{}{} m[“a”] = struct{}{}

5.应用场景2:channel只用来传输信号时 a := make(chan struct{})

go字符串

1.go当中的字符串是runtime.stringStruct结构体,所以unsafe.sizeof(一个字符串)都是16字节。

type stringStruct struct {
    str unsafe.Pointer //指针指向底层Byte数组
    len int			   //表示 Byte数组长度,而不是字符个数
}

反射包里也有个相同成员的结构体,首字母大写,向外暴露

type StringHeader struct {
    Data uintptr
    Len  int
}

2.对字符串直接使用len得到的是字节数,而不是字符数,对字符串直接下标访问也是字节

3.字符串被for range遍历时,被解码成rune类型的字符

4.UTF-8编码解码算法在runtime包下的utf8.go

5.字符串需要切分时,可以先转为rune数组,再转为切片,最后转成string

e.g s = string([]rune(s)[:3])

go切片

1.go的切片底层是runtime包的slice结构体,本质是对数组的引用,占24个字节

type slice struct {
    array unsafe.Pointer //指针指向底层Byte数组
    len   int			 //切片的字节长度
    cap   int			 //切片的最大容量
}

2.切片的创建

  • 可以根据数组创建 array[1:3], 这个时候,切片的指针指向数组的第二个元素,len为2,cap为数组的长度-1
  • 字面量创建 s := []int{1, 2, 3}
  • make创建 s := make([]int, 10)

3.切片的append

1.18以前的策略

  • 不需要扩容时,只调整len

  • 需要扩容时,调用runtime.growslice(),放弃原数组,新开一个原数组

    如果原切片长度小于1024,将容量翻倍

    如果期望容量大于原容量的两倍,那直接使用期望容量

    如果原切片长度大于1024,每次增加25%

  • 切片扩容的时候,并发不安全,注意切片并发要加锁。因为假如a扩容切片,b要读该切片,扩容的时候会放弃原数组,新开辟一个数组,而b还是读的原数组,这就会产生问题

1.18以后的策略

  • 如果期望长度大于原容量两倍,直接使用期望容量
  • 接下来的情况都建立在,期望容量小于等于原容量两倍的情况
  • 如果原容量小于256,直接将原容量翻倍
  • 否则,进入for循环,每次给原容量+= (原容量+3*256)/4
  • 然后判断+完的值是否大于等于期望的新长度,如果大于等于就break返回
  • 否则继续for循环

大家感兴趣可以阅读一下源码,很简单的逻辑。

官方的说法新的扩容机制可以更平滑的进行扩容。

// nextslicecap computes the next appropriate slice length.
func nextslicecap(newLen, oldCap int) int {
    newcap := oldCap
    doublecap := newcap + newcap
    if newLen > doublecap {
       return newLen
    }

    const threshold = 256
    if oldCap < threshold {
       return doublecap
    }
    for {
       // Transition from growing 2x for small slices
       // to growing 1.25x for large slices. This formula
       // gives a smooth-ish transition between the two.
       newcap += (newcap + 3*threshold) >> 2

       // We need to check `newcap >= newLen` and whether `newcap` overflowed.
       // newLen is guaranteed to be larger than zero, hence
       // when newcap overflows then `uint(newcap) > uint(newLen)`.
       // This allows to check for both with the same comparison.
       if uint(newcap) >= uint(newLen) {
          break
       }
    }

    // Set newcap to the requested cap when
    // the newcap calculation overflowed.
    if newcap <= 0 {
       return newLen
    }
    return newcap
}

go map

1.底层用的hashmap,hashmap有两种常用的方案。

  • 开放寻址法

    key进行hash函数,然后取模,去对应的slot槽存储,如果该下标已经被一对kv占用,则向下一位存储,以此类推,直到有空闲位置。读取的时候同样的原理。开放寻址法中对性能影响最大的是装载因子,它是数组中元素的数量与数组大小的比值。随着装载因子的增加,线性探测的平均用时就会逐渐增加,这会影响哈希表的读写性能。当装载率超过 70% 之后,哈希表的性能就会急剧下降,而一旦装载率达到 100%,整个哈希表就会完全失效

  • 拉链法

    key进行hash函数,然后取模,去对应的槽slot存储,不同的是,下标存储的都是指针,指向一个链表(Bucket桶),链表存储的一系列kv,选择好slot槽后,就开始遍历链表,如果有对应的key就更新value,否则在链表尾部追加新的kv。拉链法装载因子:=元素数量÷桶数量

index := hash("Key6") % array.len

通俗的解释一下两者的区别。可以理解为hashmap就是一个个房间,房间里放置的是你要存储的value,key就是一个个房间的密码锁。你要知道密码才能解开对应的房间门锁。密码怎么来呢,就是key进行一个hash函数计算再取模,得到密码。

开放寻址法就是得到密码后你去对应的房间开锁,如果有人,就去下一个房间直到找到一个空房间然后放置value

拉链法就是得到密码后你去对应的房间开锁,如果有人,就在房间里面加一张床,大家挤一挤

2.runtime.hamp结构体

// A header for a Go map.
type hmap struct {
    // Note: the format of the hmap is also encoded in cmd/compile/internal/reflectdata/reflect.go.
    // Make sure this stays in sync with the compiler's definition.
    count     int // 代表哈希表里的元素个数,使用len(map)返回的就是该字段值
    flags     uint8 //一个状态值,表示是否处于正在读写的状态
    B         uint8  // buckets的对数。例如B=6,那么buckets数组的长度为2^6=64,代表有64个桶
    noverflow uint16 // 溢出桶的数量
    hash0     uint32 // hash随机数种子,能为哈希函数的结果引入随机性,这个值在创建哈希表时确定,并在调用哈希函数时作为参数传入

    buckets    unsafe.Pointer // 指向buckets数组的指针,如果count=0,值为nil。数组每个元素都是bmap
    oldbuckets unsafe.Pointer // 如果发生过扩容,该字段是指向旧桶数组的指针,该数组为新桶数组的一半,非扩容下为nil
    nevacuate  uintptr        // 代表扩容的进度,小于此指针地址的buckets代表已经迁移完成

    extra *mapextra // 存储一些额外的信息
}

buckets里面包含若干个bmap。

bmap 就是我们常说的 “桶”,一个桶里面会最多装 8 个 key,这些 key 之所以会落入同一个桶,是因为它们经过哈希计算后,哈希结果的低 B 位是相同的。

bmap包含四个字段,tophash:存储8个 key hash值的前8位,有助于快速遍历 keys:存储8个keys elems:存储8个val overflow:存储溢出桶指针,一个bmap只能放8个kv

 type bmap struct {
	tophash  [8]uint8
    keys     [8]keytype  // 对应的键类型
    values   [8]elemtype // 对应的值类型
    overflow uintptr 	 //overflow 存储的是指向溢出桶的指针,使用 uintptr 类型而非 *bmap 是为了避免被 GC 扫描到。
}

3.哈希碰撞

runtime.mapassign()可能会触发扩容情况:

  • 装载因子超过6.5(平均每个槽6.5个key)
  • 溢出桶超过普通桶的数量

扩容可能并不会增加桶数,而是整理

扩容:

  • 创建一组新桶
  • oldbuckets指向原有桶数组
  • buckets指向新的桶数组
  • map标记为扩容状态
  • 将所有的数据从旧桶驱逐到新桶
  • 采用渐进式驱逐
  • 每操作的时候遇到一个旧桶时,将旧桶数据驱逐到新桶
  • 所有的旧桶驱逐完成后
  • oldbuckets回收

4.map的并发问题

  • map不支持并发的读写

  • A协程在桶中读数据,B协程驱逐了这个桶

  • A协程会读取到错误的数据或者找不到数据

解决方案:

  • 给map加锁(mutex),但是牺牲了性能,因为map很可能是要经常读写的场景,加锁会极大地影响效率
  • 使用sync.Map

接口

runtime2.iface和eface

iface表示拥有方法的接口类型

eface表示空接口类型

type iface struct {
    tab  *itab //储存接口本身的信息
    data unsafe.Pointer //指向被赋值给接口类型变量的具体值
}

type itab struct {
	inter *interfacetype //存储接口本身的信息(接口类型,方法集等)
	_type *_type //具体的类型信息
	hash  uint32 // _type.hash的缓存,当需要将接口类型转换成具体的类型时,使用该字段判断转换的目标类型是否和具体类型_type一样
	_     [4]byte
	fun   [1]uintptr // 具体类型实现了接口方法的调用地址
}

type eface struct {
    _type *_type //表示具体类型的类型信息
    data  unsafe.Pointer //执行具体类型变量的值
}

1.接口的值使用runtime.iface结构体表示

2.iface记录了数据的地址

3.iface记录了接口类型信息和实现的方法

4.结构体和结构体指针都可以实现接口,如果用结构体实现一个方法的话,编译后会帮你也用该结构体的指针实现该方法。但是如果你用结构体指针实现一个方法的话,编译不会帮你用结构体实现该方法

5.空接口使用runtime.eface结构体表示,函数调用的时候,空接口可以传任意类型参数,编译的时候会生成一个eface再传入

nil

1.nil是空,但不一定是空指针。nil在go里是一个变量,类型是Type。它表示指针,管道,函数,接口,map,切片的零值。注意这其中不包括结构体,所以nil永远不可能等于一个结构体

2.空结构体是Go中非常特殊的类型,不仅空结构体的值不是nil,空结构体指针也不是nil,而是同一个地址zerobase

3.如果a是一个空接口,b是一个*int,这俩都是nil。如果把a=b,那么a不是nil,因为空接口是eafce结构体,有类型和数据组成,a=b之后,a这个就空接口就有了类型,就不是nil了。空接口只有类型和数据都是空,才为nil

内存对齐

1.由于cpu读取数据是按照64bit8字节长读取的,如果没有内存对齐,可能存在一个int32跨越了两个字长,需要读取两次的情况,内存的原子性和效率会受到影响。

2.go提供对齐系数函数,方便内存对齐。unsafe.Alignof()

**对齐系数的含义是变量的内存地址必须被对其系数整除。**如果对齐系数为4,表示变量内存地址必须是4的倍数

3.基本类型的对齐系数等于其内存占用字节大小

结构体内部对齐

1.指的是结构体内部成员的相对偏移量

2.每个成员的偏移量是自身大小与其对齐系数二者之间较小值的倍数

3.结构体长度填充是指通过增加结构体长度,对齐系统字长

4**.结构体的长度是最大成员长度和系统字长之间较小值的整数倍**

5.因为结构体成员定义顺序决定存放位置,且不可变。所以可以尝试通过调整成员顺序,节约空间

6.结构体整体的对齐系数是其成员的最大对齐系数。结构体对齐系数决定其地址

7.空结构体作为成员时,地址跟随前一个成员。如果出现在末尾时,需要补齐字长

  • 25
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值