深入浅出 Go-Map(上)

1.24 前后 map 的世界,在 1.24 后,go 的 map 换了实现方式,让我们来康康 1 追踪 map 的起始点 看下面这段代码,我们来定位一下 make 之后发生了什么? package main func main() { _ = make(map[string]int, 16) } 使用 go tool compile -S 编译文件,可以发现 map 实际上是定位到了底层的 runtime.makemap 中,这使得我们有了窥探 map 源码的机会。 ❯ go tool compile -S main.go | grep 'make' 0x0034 00052 (.../main.go:4) CALL runtime.makemap(SB) rel 52+4 t=R_CALLARM64 runtime.makemap+0 在进入具体方法之前,我们先了解一下 1.24 之前 go 的 map 的结构 2 1.24 前的世界 本节 go 的代码是基于 1.23.2 版本的 2.1 map 的基本构造 在 1.24 前,go 的 map 实现是基于桶数组 + 溢出桶实现的,具体而言是基于 runtime 下的这两个结构体实现的。 首先是 hmap,他是宏观上的 map,包含了 map 的键值对数量、状态、桶的数量以及桶的指针等等。当我们创建了一个 map 的时候,宏观来看就是创建 hmap。 ...

October 29, 2025 · 小石堆

Authorization & Authentication

在面 MoonShot 的时候被问到了这个,当时我只知道认证和鉴权的区别,第一遍听二者的英文名没听懂啥意思,然后就没答出来,我以为是什么其他的芝士点,可恶捏,我得饿补一下芝士点QAQ。 认证(Authentication) 我们常说的登录,这实际上就是认证:Authentication 的完整定义是身份认证,即系统确认你是谁的过程。其本质是用户提供某种可信凭证来证明自己的身份。 平时我们所说的 Jwt 鉴权、Cookie-Session 鉴权这些很多时候讲的就是认证。下面先简单讲讲两个基础认证的手段,后面后统一讲一些框架,比如说 SSO。 Cookie-Session Cookie 和 Session 算是认证的基本芝士了。众所周知,Http 是一个无状态的协议,想象一下,如果没有一种措施让服务器知道你是谁,就得每次操作都需要输一次账号密码了。虽然 Cookie 和 Session 不是专门为认证所打造的,但是其可以实现简单的认证。 下面是一个简单的 Cookie-Session 的图示: 过程相当的简单,这也正是 Cookie-Session 的优点所在,实现起来相当简单,同时 Session 的鉴权信息是保存在服务器上的,一般来说也比较安全,而缺点也很明显。 缺点: 天然非分布式。我们现在的服务器大多都是负载均衡的,而假设我们认证的信息被保存在了服务器 A 上,而后续请求都打到其他服务器上,其他服务器是没有对应的 SessionId 信息的,从而导致无法正常认证。要解决这个问题需要引入其他组件,比如说 Redis 等,进而增加了系统的复杂性。 前端未防护 XSS 的情况下,Cookie 可能会被盗取,从而导致安全问题。 Session 的生命周期管理复杂。一般来说,我们会给 Session 设置一个过期时间,认证时发现过期会拒绝请求,但这些过期的 Session 会造成一定的服务器压力,需要手动清理。 Jwt-Token JwtToken 全称是 Json-Web-Token,是一种无状态的认证手段。这个相较于 Cookie-Session 会更加符合“潮流”一些。我们来看看用 Jwt 是怎么做认证的。 上面就是一个完整的认证流程,从宏观上来看,似乎只是客户端携带的东西不同了。我认为最重要的不同点是服务器存储的信息粒度的变大,服务器无需再存储每个用户的信息,只需要存储一个服务或者一个业务的密钥,就能解决这个业务上所有用户的认证问题,这一个转变解决了下面这些问题: Cookie-Session 的天然不支持分布式的问题,只要一台服务器上有对应服务的密钥,那就 OK。 解决了 Session 的生命周期管理的问题,因为服务器无存储压力,服务器只需要看这个 Jwt 里包含的过期时间有没有过期。 如此完美的方案是否有问题呢?有的兄弟,有的: ...

October 22, 2025 · 小石堆

Mysql 索引以及锁初探

前段时间在面试字节的时候,面试官拿着 Sql 问我这个锁和索引之间的联系是什么的时候?人懵住了,这玩意还有联系?之后意识到是自己学习东西太过于肤浅,没有建立体系,导致这么简单的问题没答出来,悔不当初 QAQ。 Mysql 存储数据的方式 Mysql 底层存储数据是通过一个一个数据页所组成的,一页中存储的数据属于同一张表,但同一张表的数据不一定分布在同一页,从粗到细粒度大致是: 表空间 (Tablespace) └── 段 (Segment) └── 区 (Extent) └── 页 (Page, 16KB) └── 行记录 (Row) 我们这里主要分析页和行的结构 数据页(Data Page) 上图展示的就是一页的数据内容,Mysql 的行数据被单向链表的形式组织起来,为了提高页内检索效率,所以有了槽和分组的概念。他们的对应关系是,一个数据页中有多个槽,每个槽中有一个或多个数据行,数据行以单向链表的形式组织起来,然后链表头部和尾部分别是记录中的最小记录和最大记录标识。通过这样的形式组织起来的数据有什么好处呢?答曰提高检索效率。如果只单纯使用单向链表,那就意味着每次查找只能单向搜索。而使用槽,槽指针指向了一个分组内的最大记录,那就意味着在一页内可以用二分查找来提高检索的时间复杂度。 行记录(Data Row) 上面讲述了一页是如何组织的,下面来看看一行是如何组织的。 这就是一行数据,从左到右分别记载了: 变长字段列表:记录行数据中变长字段占用的字节数 Null 值列表:记录行数据中,可以为 Null 的列是否为 Null,也就是说一列对应一个 Bit 位,如果为 Null,在对应的 bit 位为 0 记录头:这里主要包含了一些辅助信息,比如记录的类型(是否是 B+树的叶子)、下一条记录的位置以及是否被删除标记。 RowId:当表设置了主键或者唯一性约束,则这一列隐藏,如果没设置,则 Mysql 会默认生成 RowId 字段。 TrxId:这条记录是由哪行所生成的。 Rollptr:UndoLog 的列表挂载,用于回滚和 MVCC。 上面就是一行记录,也就是说,当你创建一个表,Mysql 底层就是这么组织的。 索引 Mysql 索引分类 按照数据结构:Hash 索引、FullText 索引、B+ 树索引 按照底层存储:聚簇索引、非聚簇索引 按照建立索引的字段类型:主键索引、唯一索引、前缀索引、普通索引 按照字段数量:单列索引、组合索引 我们主要来关注聚簇索引和非聚簇索引,我们知道聚簇索引就是主键索引,其存储了主键和整行的数据,而非聚簇索引的一行第一列是索引值,第二行是主键值。为什么?Why?为什么第存全量数据?这是因为 Mysql 的底层数据组织结构造成的,B+ 树存储索引是要存储全量的数据页的,如果说非聚簇索引也存储全量数据,就等于在建立索引的时候,需要将所有数据页进行一次全量拷贝 + 组织,就意味着底层会出现大量重复的数据,所以 InnoDB 选择第二列存储主键值,这样当检索到索引后,只需要再去聚簇索引中,找到对应的数据即可。 Mysql 索引的数据组织结构 我们知道 InnoDB 的索引底层是基于 B+ 树的,而索引和页的关系是什么呢?答案是索引由数据页组成。 Mysql 的数据是这样存的,当你创建一个表并存入数据时,Mysql 会默认生成一个基于主键的聚簇索引,就是无论你主动创建索引与否,Mysql 都会生成聚簇索引,这不只是为了提高检索速度,而是 InnoDB 的行锁机制是基于索引的。所以这里也就 CallBack 到了开头的,索引和锁的关系。 ...

October 17, 2025 · 小石堆

Java ArrayList & Go Slice 扩容

Java 中的 ArrayList 类似 Golang 里的 Slice,都是动态数组 ArrayList 什么是 ArrayList ArrayList 是 Java 中最常见的动态数组之一,他实现了 List 接口,底层基于数组实现,但能自动扩容,可以理解为变长数组的一种。 List<String> list = new ArrayList<>(); list.add("zhangsan"); list.add("lisi"); System.out.println(list); ArrayList 的扩容原理 当创建一个 ArrayList 的时候,默认初始的容量为 10,其内部实现类似 Object[] elementData = new Object[10] 当元素超过容量时,arraylist 就会触发扩容机制,其触发条件为 if(size == elementData.length) 扩容策略如下 newCapacity = oldCapacity + (oldCapacity + 1),简言之就是扩容为原来的 1.5 倍。 这里涉及到扩容过程的主要函数是 grow() private void grow(int minCapacity) { int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity + 1); if(newCapacity - minCapacity < 0) { newCapacity = minCapacity; } // 创建新数组,复制旧数据 elementData = Arrays.copyOf(elementData, newCapacity) } 这里的扩容规则实际上并没有 Go 的复杂,但是要理解 minCapacity ...

September 3, 2025 · 小石堆

浅谈 IO 多路复用

之前面试准备过这里的八股,但觉得理解不够深,故打算再探深些 五种 IO 模型 阻塞 IO 非阻塞 IO IO 多路复用 信号驱动 IO 异步 IO 我们先来看看 DMA 存在的必要性 当数据到达设备端,比如说网卡。网卡接收到数据后,传统非 DMA 的形式,此时会直接触发中断,此时 CPU 介入,负责解析数据,并且将数据 Copy 到 CPU 寄存器中,然后再将 CPU 寄存器中的数据 Copy 到内核内存中,然后解析再送到 Socket 缓冲区中,最后唤醒被阻塞的线程。分析上述流程,CPU 作为整个计算机的大脑,让他来处理这种傻白甜的反复读写操作,过于浪费,这就是 DMA 存在的必要性。当网卡收到数据后,通知 DMA,DMA 负责直接将数据 Copy 到内核内存中,然后网卡触发中断,CPU 将数据进行网络协议栈的解析,然后 Copy 到对应 Socket 的缓冲区中,然后唤醒线程。等到线程被调度的时候,会进入内核态,发现数据已经准备好,然后 Copy 到内存中直接使用,这就是现代计算机的一次阻塞 IO 过程。 阻塞 IO 最简单,最易懂 厨子做菜,做不好服务生也等着,等到菜做好再上菜。 上面其实已经讲过一次阻塞 IO 的流程了,只不过是站在 DMA 和 CPU 的角度来看的。我们来看看阻塞 IO 的瓶颈有哪些? 线程在 Read 数据的时候,如果数据没有准备好,那就陷入阻塞。假设数据不是必要的,有其他重要的事情做,那这里就会成为瓶颈,线程被阻塞,无法做事。 在阻塞 IO 的模型,一个 Scoket 对应一个线程,如果此时有上万个网络连接,那内核线程能否支撑这样大的并发量,所以这里也会成为瓶颈。 所以,阻塞 IO 虽然实现起来傻白甜,那是内核要考虑的问题就多了。 非阻塞 IO 厨子做菜,服务生不断轮询反问厨子有没有做好。 这一点对应到 CPU 就是进程不断调用 read(),如果数据准备好了,read()出数据,如果数据没有准备好,read()出 fasle,得到 false 的进程就去继续调用 read()。 ...

August 11, 2025 · 小石堆