深入理解 Golang: 聚合、引用和接口类型的底层数据结构

news2024/9/22 15:47:22

Go 中有基础类型、聚合类型、引用类型和接口类型。基础类型包括整数、浮点数、布尔值、字符串;聚合类型包括数组、结构体;引用类型包括指针、切片、map、function、channel。在本文中,介绍部分聚合类型、引用类型和接口类型的底层表示及原理。

空结构体

空结构体长度为 0,主要为了节约内存。

  1. 当我们想要构造一个 hashset 时,可用空结构体:
type hashSet map[string]struct{}
aHashSet := hashSet{
    "a": struct{}{},
}
  1. 只想让 channel 作为纯型号,不携带信息时:
myChannel := make(chan struct{})

package main

import (
 "fmt"
 "unsafe"
)

type K struct{}

func main() {
 c := K{}
 d := K{}
 fmt.Println(unsafe.Sizeof(c)) // 0
 fmt.Println(unsafe.Sizeof(d)) // 0
}

空结构体独立存在,即不被包含到其他结构体中时,指针指向相同的地址空间:

fmt.Printf("%p", &c) // 0xf3a418
fmt.Printf("%p", &d) // 0xf3a418

该地址称为 zerobase

// base address for all 0-byte allocations
var zerobase uintptr

非独立情况,比如:

type Test struct {
    kInstance K
    num int
}

t := &Test{}
fmt.Printf("%p", &t.kInstance) // 0xc0000a6078

字符串

在 go 中,对字符串的操作实际上是操作的 stringStruct 结构体:

type stringStruct struct {
    str unsafe.Pointer // 指向底层 Byte 数组
    len int            // Byte 数组的长度,非字符数量
}

当我们获取字符串的空间大小时,得到的是 16,因为 unsafe.Pointer 占 8 字节,int 类型占 8 字节:

fmt.Println(unsafe.Sizeof("haha")) // 16

关于结构体大小计算,参考类型内存对齐

注意

  1. 在 go 中,int8 : 1字节,int16 : 2字节,int32 : 4字节,int64 : 8字节,int 默认 1 个机器字。
  2. 对 string 类型取 len() 时得到的时字节数,而不是长度。
  3. 对字符串直接下标访问,得到的是字节。
  4. 字符串 range 遍历时,被解码成 rune 类型。

一般做字符串切分时,先将字符串转为 rune 数组,再切分:

s := "abcdefg"
fmt.Printf("%c", []rune(s)[:2])

切片

在 go 中,对切片的操作实际上是操作的 slice 结构体:

type slice struct {
    array unsafe.Pointer
    len int
    cap int
}
  1. 通过数组创建切片,此时 len() 为切片内元素个数,cap() 为 arr 数组从被切分的初始位置到最后一个元素的长度
arr := []int{1,2,3,4,5,6}
mySlice := arr[1:3]

unsafe.Sizeof(mySlice) // 24
  1. 通过字面量创建时,会先新建一个数组,再创建结构体:
    在这里插入图片描述
slice1 := []int{1,2,3}
  1. 通过 make 创建切片时,会调用 runtime 中的 makeslice 方法。

切片追加

  • 切片内元素还未撑满容量时,对切片追加元素则直接添加到最后。
  • 当切片长度等于容量时,内部会调用 runtime.growslice() 默认重新生成初始容量两倍的新切片,再进行追加。
  • 如果期望容量大于当前容量的两倍,就用期望容量。
  • 如果当前容量小于 1024,则容量翻倍。
  • 容量大于 1024 时,每次增加 25%。
  • 切片扩容时,并发不安全,需要加锁。

Map

go 中构建 HashMap 采用的拉链法。

// 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 // # live cells == size of map.  Must be first (used by len() builtin)
 flags     uint8
 B         uint8  // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
 noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
 hash0     uint32 // hash seed

 buckets    unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
 oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
 nevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)

 extra *mapextra // optional fields
}

buckets 的数据结构如下:

// A bucket for a Go map.
type bmap struct {
 // tophash generally contains the top byte of the hash value
 // for each key in this bucket. If tophash[0] < minTopHash,
 // tophash[0] is a bucket evacuation state instead.
 tophash [bucketCnt]uint8
 // Followed by bucketCnt keys and then bucketCnt elems.
 // NOTE: packing all the keys together and then all the elems together makes the
 // code a bit more complicated than alternating key/elem/key/elem/... but it allows
 // us to eliminate padding which would be needed for, e.g., map[int64]int8.
 // Followed by an overflow pointer.
}

在一个 bucket 中存在 3 个数组:tophashkeyselems。tophash 中存放的是容量为 bucketCnt 的 hash 值,具体来说是记录的 key 的 hash 值得高 8 位;bucketCnt 个 keys 和 bucketCnt 个 elems 分别放在两个数组(keys, elems)中。

如果容量超过 bucketCnt,则 overflow.nextOverflow 指针指向其他 bmap(溢出桶中的bmap)。

通过 make 创建 map 时,内部调用 runtime.makemap() 方法:

// make 创建 map
myMap := make([string]int, 10)

// map.go 中
func makemap(t *maptype, hint int, h *hmap) *hmap {
 mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
 if overflow || mem > maxAlloc {
  hint = 0
 }

 // 初始化 hmap
 if h == nil {
  h = new(hmap)
 }
 h.hash0 = fastrand()

// 根据 hint 计算 B 的大小,通过 B 计算桶的容量
 B := uint8(0)
 for overLoadFactor(hint, B) {
  B++
 }
 h.B = B

 if h.B != 0 {
  var nextOverflow *bmap

  // 创建数组来放置桶
  h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
  // 存放溢出桶
  if nextOverflow != nil {
   h.extra = new(mapextra)
   h.extra.nextOverflow = nextOverflow
  }
 }

 return h
}

Map 扩容

  • 当溢出桶过多时,会采取扩容。
  • runtime.mapassign() 可能触发得扩容情况:
    • 装载因子大于 6.5
    • 溢出桶超过普通桶
  • 扩容方式:
    • 等量扩容
    • 翻倍扩容

扩容步骤1:
runtime.hashGrow():

func hashGrow(t *maptype, h *hmap) {
 bigger := uint8(1)
 if !overLoadFactor(h.count+1, h.B) {
  bigger = 0
  h.flags |= sameSizeGrow
 }
 // 1.
 oldbuckets := h.buckets
 // 2.
 newbuckets, nextOverflow := makeBucketArray(t, h.B+bigger, nil)

 flags := h.flags &^ (iterator | oldIterator)
 if h.flags&iterator != 0 {
  flags |= oldIterator
 }
 h.B += bigger
 // 4.
 h.flags = flags
 // 1.
 h.oldbuckets = oldbuckets
 // 3.
 h.buckets = newbuckets
 h.nevacuate = 0
 h.noverflow = 0

 if h.extra != nil && h.extra.overflow != nil {
  if h.extra.oldoverflow != nil {
   throw("oldoverflow is not nil")
  }
  // 5.
  h.extra.oldoverflow = h.extra.overflow
  h.extra.overflow = nil
 }
 if nextOverflow != nil {
  if h.extra == nil {
   h.extra = new(mapextra)
    }
  h.extra.nextOverflow = nextOverflow
 }
}
  1. oldbuckets 指向原有的桶组
  2. 创建一组新桶
  3. buckets 指向新的桶组
  4. 标记 map 为扩容状态
  5. 更新溢出桶信息

扩容步骤2:

  1. 将所有数据从旧桶迁移到新桶
  2. 采用渐进式驱逐
  3. 每次操作一个旧桶时,将旧桶数据迁移到新桶

扩容步骤3:

  1. 所有的旧桶驱逐完成后,回收 oldbuckets

tips:扩容并不一定都是增大,也可能是整理

普通 Map 是并发不安全的,用 sync.Map 代替
比如 A 协程读桶数据时,B 驱逐了桶的数据。sync.Map 结构体如下:

// %GOROOT%/src/sync/map.go
type Map struct {
 mu Mutex
 read atomic.Pointer[readOnly]
 dirty map[any]*entry
// 未命中时自加
 misses int
}

type readOnly struct {
 m       map[any]*entry
// 是否追加数据
 amended bool // true if the dirty map contains some key not in m.
}

type entry struct {
 p atomic.Pointer[any]
}

syncMap 数据存放示意图如下:
在这里插入图片描述

  • 读/改数据时,先进入 read 的 map,如果数据不存在则表示未命中,判断 amended 是否为 true,为 true 则去读取 dirty 的 map,misses 加 1,否则退出。
  • 追加数据时,先进入 read 的 map,如果相应数据不存在则表示需要追加,退出 m,使用 mu 给 dirty 上锁,然后新增键值对,设置 amended 为 true:
    在这里插入图片描述
  • misses 的值等于 dirty 的长度时,进行 dirty 提升,dirty(map) 取代原有的 m(map),新的 dirty 置空,另外重置 misses 为 0,amended 为 false;后续需要追加时再重建 dirty:
    在这里插入图片描述
  • dirty 提升前正常删除,比如删除键 “c”,找到 “c” 并把 *entry 指向空 nil。
  • 删除 “c” 后 dirty 出现了提升情况,在重建 dirty 时,被删除的 “c” 就不会再次重建,并且 nil 改为 expunged 描述:
    在这里插入图片描述

sync.Map 的基础用法:

var m sync.Map
// 1. 写入
m.Store("a", 1)

// 2. 读取
a, _ := m.Load("a")

// 3. 遍历
m.Range(func(key, value interface{}) bool {
    k := key.(string)
    v := value.(int)
    fmt.Println(k, v)
    return true
})

// 4. 删除
m.Delete("a")
a, ok := m.Load("qcrao")
fmt.Println(a, ok)

// 5. 读取或写入
m.LoadOrStore("b", 2)
b, _ = m.Load("b")
fmt.Println(b)

接口

type Car interface {
 Drive()
}
type Truck struct {
}

func (t Truck) Drive() {

}

var t Car = Truck{}
t.Drive()

一个接口的值 t 的底层表示:

// %GOROOT%/src/runtime/runtime2.go
type iface struct {
 tab  *itab
 data unsafe.Pointer // 指向 Truck{}
}

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.
}

空接口 interface{} 的底层表示:

type eface struct {
 _type *_type // 没有方法
 data  unsafe.Pointer
}

interface{} 一般用于接收任意类型,类似泛型的作用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/697593.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何在Microsoft Excel中快速筛选数据

你通常如何在 Excel 中进行筛选?在大多数情况下,通过使用自动筛选,以及在更复杂的场景中使用高级过滤器。 使用自动筛选或 Excel 中的内置比较运算符(如“大于”和“前10项”)来显示所需数据并隐藏其余数据。筛选单元格或表范围中的数据后,可以重新应用筛选器以获取最新…

数据结构与算法基础-学习-25-图之MST(最小代价生成树)之Prim(普利姆)算法

一、生成树概念 1、所有顶点均由边连接在一起&#xff0c;但不存在回路的图。 2、一个图可以有许多棵不同的生成树。 二、生成树特点 1、生成树的顶点个数与图的顶点个数相同。 2、生成树是图的极小连通子图&#xff0c;去掉一条边则非连通。 3、一个有n个顶点的连通图的生…

stm32f103c8t6移植U8g2

U8g2代码下载&#xff1a; https://github.com/olikraus/u8g2 1&#xff0c;准备一个正常运行的KEIL5 MDK模板 2&#xff0c;下载u8g2的源码和 u8g2的STM32实例模板 源码: https://github.com/olikraus/u8g2 STM32实例模板: https://github.com/nikola-v/u8g2_template_stm32f…

100天精通Golang(基础入门篇)——第11天:深入解析Go语言中的切片(Slice)及常用函数应用

&#x1f337; 博主 libin9iOak带您 Go to Golang Language.✨ &#x1f984; 个人主页——libin9iOak的博客&#x1f390; &#x1f433; 《面试题大全》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &#x1f30a; 《I…

期望最大化注意力网络 EMANet

论文&#xff1a;Expectation-Maximization Attention Networks for Semantic Segmentation Github&#xff1a;https://github.com/XiaLiPKU/EMANet ICCV2019 oral 论文提出的期望最大化注意力机制Expectation- Maximization Attention (EMA)&#xff0c;摒弃了在全图上计算注…

再述时序约束

再述时序约束 一、为什么要加时序约束&#xff1f;二、时序分析是什么&#xff1f;三、时序分析的一些基本概念三、 时序分析的一些基本公式 一、为什么要加时序约束&#xff1f; 一次笔者在调试HDMI输出彩条&#xff0c;出现彩条时有时无现象&#xff0c;笔者视频输出芯片的驱…

leecode-数组多数-摩尔投票法

题目 题目 分析 最开始思路&#xff1a;排序&#xff0c;然后取nums[n/2]&#xff0c;但是时间复杂度不过关。 摩尔投票法&#xff0c;学到了&#xff01; 代码 class Solution { public:int majorityElement(vector<int>& nums) {//摩尔投票int cnt0;int targ…

计算机二级c语言题库

计算机C语言二级考试&#xff08;60道程序设计&#xff09; 第1道 请编写一个函数fun,它的功能是:将ss所指字符串中所有下标为奇数位置上的字母转换成大写&#xff08;若该位置上不是字母&#xff0c;则不转换&#xff09;。 例如&#xff0c;若输入"abc4EFG"&…

OpenCV学习笔记 | ROI区域选择提取 | Python

摘要 ROI区域是指图像中我们感兴趣的特定区域&#xff0c;OpenCV提供了一些函数来选择和提取ROI区域&#xff0c;我们可以使用OpenCV的鼠标事件绑定函数&#xff0c;然后通过鼠标操作在图像上绘制一个矩形框&#xff0c;该矩形框即为ROI区域。本文将介绍代码的实现以及四个主要…

opencv编译

文章目录 一、编译前工作二、编译安装1、Windows2、Linux 一、编译前工作 进入下载页面https://github.com/opencv/opencv&#xff0c;下载指定.tar.gz源码包&#xff0c;例如&#xff1a;opencv-4.7.0.tar.gz。解压到指定目录。 二、编译安装 opencv构建时&#xff0c;需要…

使用docker搭建hadoop集群

1.下载安装docker 2.启动docker 3.配置docker镜像 4.获取hadoop镜像 5.拉取hadoop镜像 6.运行容器 7.进入容器 8.配置免密 9.格式化节点 10.启动节点 11.查看节点信息 (img-CBr9VbGk-1687962511910)] 11.查看节点信息

javascript原型、原型链、继承详解

一、原型和原型链的基本概念 在JavaScript中&#xff0c;每个对象都有一个原型对象&#xff08;prototype&#xff09;。原型对象就是一个普通的对象&#xff0c;在创建新对象时&#xff0c;可以将该对象作为新对象的原型。原型对象可以包含共享的属性和方法&#xff0c;这些属…

Appium自动化-ADB连接手机提示unauthorized

目录 开头&#xff1a; 问题&#xff1a; 调研&#xff1a; 重启大法 终极大法 总结&#xff1a; 开头&#xff1a; 当使用ADB&#xff08;Android Debug Bridge&#xff09;连接手机时&#xff0c;如果提示"unauthorized"&#xff08;未授权&#xff09;错误&a…

javaee HttpSessionListener监听器统计在线人数

先创建ServletContextListener 在全局对象application中设置count属性 package com.yyy.listener;import java.util.ArrayList;import javax.servlet.ServletContext; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import javax…

易基因|表观遗传学与脑卒中:DNA甲基化的作用及衰老对血脑屏障修复的影响

大家好&#xff0c;这里是专注表观组学十余年&#xff0c;领跑多组学科研服务的易基因。 脑卒中&#xff08;俗称中风&#xff09;是导致死亡和长期残疾的主要原因&#xff0c;尤其是对于老龄人来说。脑卒中的平均生存时间为6-7年&#xff0c;许多患者存在身体残疾和晚期认知功…

技术岗/算法岗面试如何准备?5000字长文、6个角度以2023秋招经历分享面试经验

技术岗/算法岗面试流程是什么样的&#xff1f;技术面都干什么&#xff1f;Coding 机试如何准备&#xff1f;技术面考察哪些知识&#xff0c;如何准备&#xff1f;项目八股如何准备&#xff1f;简历要注意什么&#xff1f;怎么做&#xff1f; 大家好&#xff0c;我是卷了又没卷…

uniapp 适配全面屏

1、manifest.json 文件修改 app-plus 下 添加 "safearea": {"background": "#00000000","bottom": {"offset": "auto"}},2、部分页面设置全屏&#xff08;登录页面&#xff09; methods: {//设置页面全屏onShow(…

SpringBoot(二)starter介绍

做Java后端的同学可能都知道&#xff0c;在SpringBoot诞生之前&#xff0c;还有传统的Spring。这种Spring项目想要运行&#xff0c;需要导入各种依赖&#xff0c;而且还要在 XML 配置文件中一顿配置&#xff0c;非常痛苦。但通过上篇博客我们可以看到&#xff0c;SpringBoot项目…

事务与隔离级别

事务四要素 原子性&#xff08;Atomicity&#xff09;&#xff1a;要么全部完成&#xff0c;要么全部不完成&#xff1b;一致性&#xff08;Consistency&#xff09;&#xff1a;一个事务单元需要提交之后才会被其他事务可见&#xff1b;隔离性&#xff08;Isolation&#xff…

azure databricks因为notebook 日志打多或者打印图片太多,往下拉卡死怎么处理

1、同事碰到个问题&#xff0c;databricks 页面卡死不动了 2、我。。。。。。。。测试了下搞不定&#xff0c;找azure的工程师&#xff0c;特此笔记如下图 !](https://img-blog.csdnimg.cn/5db9756d0e224d15a9a607561b47591f.png)