<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>第 5 章 数据结构 on Go 语言原本</title><link>http://golang.design/under-the-hood/zh-cn/part2lang/ch05data/</link><description>Recent content in 第 5 章 数据结构 on Go 语言原本</description><generator>Hugo</generator><language>zh-cn</language><atom:link href="http://golang.design/under-the-hood/zh-cn/part2lang/ch05data/index.xml" rel="self" type="application/rss+xml"/><item><title>5.1 数组、切片与字符串</title><link>http://golang.design/under-the-hood/zh-cn/part2lang/ch05data/slice/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part2lang/ch05data/slice/</guid><description>&lt;h1 id="51-数组切片与字符串"&gt;5.1 数组、切片与字符串&lt;/h1&gt;
&lt;p&gt;数组、切片、字符串是 Go 里最基础的三种序列类型。它们看起来相似，内存模型却各不相同，理解这点
能一举解释 append 的种种「惊喜」、切片别名的陷阱、以及字符串为何不可变。三者共享一个主题：
一个小小的&lt;strong&gt;头部&lt;/strong&gt;描述一段连续的后备内存。差异全在头部里装了什么、谁拥有那段内存、以及它可不
可写。本节先把三种布局摆清楚，再从「动态数组」这一经典抽象出发，看 Go 的 &lt;code&gt;append&lt;/code&gt; 如何在摊还
意义下做到 $O(1)$，最后落到别名、字符串转换与跨语言对照这些日常会撞上的角落。&lt;/p&gt;
&lt;h2 id="511-三种内存布局"&gt;5.1.1 三种内存布局&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;数组是值。&lt;/strong&gt; &lt;code&gt;[5]int&lt;/code&gt; 就是连续排布的 5 个 &lt;code&gt;int&lt;/code&gt;，长度是类型的一部分：&lt;code&gt;[5]int&lt;/code&gt; 与 &lt;code&gt;[6]int&lt;/code&gt;
是两个不同的类型。赋值、传参、作为 struct 字段，数组都&lt;strong&gt;整份拷贝&lt;/strong&gt;。正因如此，大数组在 Go 里
反而少用，传来传去太贵，真要传通常传它的切片或指针。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;切片是对某段底层数组的视图。&lt;/strong&gt; 运行时里它就是一个三字的头部，可对照 &lt;code&gt;runtime/slice.go&lt;/code&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;1
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;2
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;3
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;4
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;5
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#008000"&gt;// runtime: 切片的运行时表示（slice.go）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#00f"&gt;type&lt;/span&gt; slice &lt;span style="color:#00f"&gt;struct&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; array unsafe.Pointer &lt;span style="color:#008000"&gt;// 指向底层数组中本切片的首元素&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; len &lt;span style="color:#2b91af"&gt;int&lt;/span&gt; &lt;span style="color:#008000"&gt;// 长度：可见元素个数&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cap &lt;span style="color:#2b91af"&gt;int&lt;/span&gt; &lt;span style="color:#008000"&gt;// 容量：从 array 起到底层数组末尾的元素个数&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;len&lt;/code&gt; 是你能索引到的范围，&lt;code&gt;cap&lt;/code&gt; 是「在不重新分配的前提下还能涨到多大」。两者分离正是切片能做
「视图」的关键：&lt;code&gt;s[1:3]&lt;/code&gt; 只是改头部里的三个字段，不碰底层数组。&lt;/p&gt;</description></item><item><title>5.2 散列表</title><link>http://golang.design/under-the-hood/zh-cn/part2lang/ch05data/map/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part2lang/ch05data/map/</guid><description>&lt;h1 id="52-散列表"&gt;5.2 散列表&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;本节内容对标 Go 1.26。Go 的 &lt;code&gt;map&lt;/code&gt; 在 2024 年随 Go 1.24 完成了一次罕见的彻底重写，
从沿用十四年的经典桶式散列表换成了基于 Swiss Table 的实现。本节在讲清散列表的一般原理
之后，会同时交代旧设计与这次重写的来龙去脉（见 &lt;a href="#524-go-124-%E7%9A%84-swiss-table-%E9%87%8D%E5%86%99"&gt;5.2.4&lt;/a&gt;）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;map&lt;/code&gt; 是 Go 仅有的两种泛型容器之一（另一种是 slice）。它由运行时实现、编译器辅助布局，
本质是一张散列表。读者写下 &lt;code&gt;m[k]&lt;/code&gt; 时，编译器把它翻译成对 &lt;code&gt;runtime.mapaccess&lt;/code&gt;、
&lt;code&gt;runtime.mapassign&lt;/code&gt; 一族函数的调用，真正的存储、查找、扩容都发生在
&lt;code&gt;internal/runtime/maps&lt;/code&gt; 包里。这一节先把散列表的一般原理与攻防讲清楚，再落到 Go 自己的
两代实现：1.0 至 1.23 的经典桶式设计，以及 1.24 起的 Swiss Table 设计。理解了前者的取舍，
才能看清后者为何值得一次伤筋动骨的重写。&lt;/p&gt;
&lt;h2 id="521-散列表的两条路线链地址与开放定址"&gt;5.2.1 散列表的两条路线：链地址与开放定址&lt;/h2&gt;
&lt;p&gt;散列表要解决的核心矛盾，是把一个几乎无穷大的键空间压进一段有限的连续内存。哈希函数
$h$ 把键映射到 $[0, m)$ 的槽位下标，理想情况下一次访存即可定位。但映射不可能单射，
两个键算出同一下标即是「碰撞」，如何安置碰撞的键，分出了两条历史悠久的路线。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;链地址法&lt;/strong&gt;（chaining）让每个槽位挂一条链表，碰撞的键依次串在链上。它实现简单、删除干净、
对哈希质量不敏感，代价是每个元素多一个指针、缓存局部性差（链表节点散落在堆上）。设装填因子
$\alpha = n/m$（元素数比槽位数），在均匀哈希假设下，一次不成功查找平均要比较 $\approx \alpha$ 个
元素，成功查找 $\approx 1 + \alpha/2$ 个。链地址法允许 $\alpha &gt; 1$，性能随 $\alpha$ 线性退化。&lt;/p&gt;</description></item></channel></rss>