<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Posts on Ther 的博客</title>
        <link>https://blog.ther.cool/posts/</link>
        <description>Recent content in Posts on Ther 的博客</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>en-us</language>
        <lastBuildDate>Sat, 04 Jan 2025 22:33:52 +0800</lastBuildDate><atom:link href="https://blog.ther.cool/posts/index.xml" rel="self" type="application/rss+xml" /><item>
        <title>现代 Web 布局 - (3) Flexbox 布局基础使用</title>
        <link>https://blog.ther.cool/posts/%E7%8E%B0%E4%BB%A3-web-%E5%B8%83%E5%B1%80-3-flexbox-%E5%B8%83%E5%B1%80%E5%9F%BA%E7%A1%80%E4%BD%BF%E7%94%A8/</link>
        <pubDate>Sat, 04 Jan 2025 22:33:52 +0800</pubDate>
        
        <guid>https://blog.ther.cool/posts/%E7%8E%B0%E4%BB%A3-web-%E5%B8%83%E5%B1%80-3-flexbox-%E5%B8%83%E5%B1%80%E5%9F%BA%E7%A1%80%E4%BD%BF%E7%94%A8/</guid>
        <description>&lt;p&gt;自 2009 年 W3C 发布 &lt;strong&gt;Flexible Box Layout Module&lt;/strong&gt; （WD 版本）至今已有十多年了，在近十年来，该模块得到了快速发展，现已成为最流行的 Web 布局技术之一。在 Web 开发者中，该模块也常称为 &lt;strong&gt;Flexbox 布局&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;Flexbox 是一种布局机制，它被设计为一维布局模型，并作为一种可以提供界面中项目之间的空间分配和强大功能的方法。正因如此，Flexbox 布局对于很多 Web 开发者而言，并不是容易的，甚至有很多困惑。&lt;/p&gt;
&lt;p&gt;接下来，将分为几节课和大家一起开启 Flexbox 布局的探讨，帮助大家更好地掌握 Flexbox 布局技术。&lt;/p&gt;
&lt;h2 id=&#34;flexbox-布局简介&#34;&gt;Flexbox 布局简介&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Flexbox 布局 是一种布局机制，用于在一个维度上为项目组设置布局！&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Flexbox 模块中的主要功能就是 Web 布局。Flexbox 布局可以明确地指明容器空间的分布方式、内容对齐和元素的视觉顺序。使用 Flexbox 布局，可以轻易地实现横向或纵向布局，还可以沿着一个轴布局，或折断成多行。可以说，&lt;strong&gt;使用 Flexbox 布局可以轻易地构建你想要的任何布局&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;另外，使用 Flexbox 布局还可以让 Web 内容的渲染不再受 HTML 文档源码顺序的限制。然而，这只是视觉上的调整，Flexbox 模块中的相关属性并不会改变屏幕阅读器对内容的读取顺序。&lt;/p&gt;
&lt;p&gt;和以往的 Web 布局技术相比，Flexbox 布局所涉及的概念更多、更复杂，如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/04/KJhMHedsBwaTRv8.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;看上去有很多东西，其实也没有大家想象的那么难。接下来的内容能很好地让你掌握 Flexbox 布局。&lt;/p&gt;
&lt;h2 id=&#34;一些术语和概念&#34;&gt;一些术语和概念&lt;/h2&gt;
&lt;p&gt;我想你对 Flexbox 布局有一定的了解，而且在互联网上有关于 Flexbox 布局的教程也是玲琅满目，为此我想从 Flexbox 布局相关的术语和概念开始，因为术语的统一更有助于我们后面更好地讨论和解决问题。&lt;/p&gt;
&lt;p&gt;用下图来描述 Flexbox 中的术语：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/04/zsnBlO6RWEdUgV2.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;首先要理解的概念就是 &lt;strong&gt;Flex&lt;/strong&gt; &lt;strong&gt;容器&lt;/strong&gt; （也常称为 &lt;strong&gt;Flexbox&lt;/strong&gt; &lt;strong&gt;容器&lt;/strong&gt; ）。简单地说，HTML 上的大多数元素都可以是 Flex 容器，比如 &lt;code&gt;div&lt;/code&gt; 、&lt;code&gt;ul&lt;/code&gt; 、&lt;code&gt;main&lt;/code&gt; 块元素，&lt;code&gt;span&lt;/code&gt; 、&lt;code&gt;em&lt;/code&gt; 这样的内联元素。只需要在 HTML 元素上显式设置 &lt;code&gt;display&lt;/code&gt; 的值为 &lt;code&gt;flex&lt;/code&gt; 或 &lt;code&gt;inline-flex&lt;/code&gt; 即可。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意，HTML 中的可替代元素是无法成为 Flex 容器的，比如&lt;code&gt;img&lt;/code&gt;、 &lt;code&gt;input&lt;/code&gt;、 &lt;code&gt;select&lt;/code&gt;等元素！&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;当一个元素变成了 Flex 容器之后，它的子元素，包括其伪元素 &lt;code&gt;::before&lt;/code&gt; 、&lt;code&gt;::after&lt;/code&gt; 和 文本节点 都将成为 &lt;strong&gt;Flex 项目&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/04/Aufxt6aYl8XJISi.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;有一点非常的重要，&lt;strong&gt;在 Flexbox 布局中， Flex 容器和 Flex 项目之间的关系永远是父子关系。&lt;/strong&gt; 因此，Flex 项目也可以是它的子元素的 Flex 容器，即 显式地在 Flex 项目设置 &lt;code&gt;display&lt;/code&gt; 属性值为 &lt;code&gt;flex&lt;/code&gt; 或 &lt;code&gt;inline-flex&lt;/code&gt; ，该 Flex 项目就成为一个 Flex 容器，而它的子元素就成为 Flex 项目。但它将是一个单独的 Flex 容器，它不会继承祖辈的 Flex 容器上的属性（Flexbox属性）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/04/18tQAzIwu95XS4G.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;在 CSS 坐标系中，物理坐标系有 &lt;code&gt;x&lt;/code&gt; 轴（水平轴）和 &lt;code&gt;y&lt;/code&gt; 轴（垂直轴）之分，逻辑坐标系有内联轴（Inline Axis） 和块轴（Block Axis）之分。在 Flexbox 中，Flex 容器内也有两个轴，而且这两个轴只存在于 Flex 容器中，分别叫 &lt;strong&gt;主轴&lt;/strong&gt; （Main Axis）和 &lt;strong&gt;侧轴&lt;/strong&gt; （Cross Axis）。&lt;/p&gt;
&lt;p&gt;Flexbox 中的主轴由 &lt;code&gt;flex-direction&lt;/code&gt; 属性设置，默认情况下，主轴沿行方向（内联轴 Inline Axis）分布，如果该属性为 &lt;code&gt;column&lt;/code&gt; ，则主轴沿列方向（块轴 Block Axis）分布：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/04/sB6VKDhbPrXoYli.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;需要注意的是，Flexbox 布局中的主轴、主方向、侧轴和侧方向不是固定不变的，它们会随着&lt;code&gt;writing-mode&lt;/code&gt;（书写模式）和 &lt;code&gt;direction&lt;/code&gt;（阅读方向）而改变。 也就是说，Flex 项目在 Flex 容器中的排列方向同时会受 &lt;code&gt;flex-direction&lt;/code&gt; 属性和 CSS 的书写模式 &lt;code&gt;writing-mode&lt;/code&gt; 或 阅读模式 &lt;code&gt;direction&lt;/code&gt; 影响。&lt;/p&gt;
&lt;p&gt;另外，在 Flexbox 布局中，不管是主轴还是侧轴，都有方向性。既然有方向，就有开始处（即起点）和结束处（即终点）之分。根据起点和终点之分，Flex 容器中的每根轴又有 &lt;strong&gt;主轴起点&lt;/strong&gt; 、&lt;strong&gt;主轴终点&lt;/strong&gt; 、&lt;strong&gt;侧轴起点&lt;/strong&gt; 和 &lt;strong&gt;侧轴终点&lt;/strong&gt; 之分。而且每根轴的起点和终点是由 &lt;code&gt;flex-direction&lt;/code&gt; 和 &lt;code&gt;writing-mode&lt;/code&gt; (或 &lt;code&gt;direction&lt;/code&gt;) 来决定的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/04/Uisacme71nS9RtL.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;如果 &lt;code&gt;flex-direction&lt;/code&gt; 为默认值 &lt;code&gt;row&lt;/code&gt; 时，书写模式和阅读模式分别是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ltr&lt;/code&gt; （Left-To-Right），如英文，主轴起点在 Flex 容器左侧边缘，主轴的终点在 Flex 容器右侧边缘；&lt;/li&gt;
&lt;/ul&gt;
&lt;!-- raw HTML omitted --&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;rtl&lt;/code&gt; （Right-To-Left），如阿拉伯文，主轴起点在 Flex 容器右侧边缘，主轴的终点在 Flex 容器的左侧边缘。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在这两种情况下，侧轴的起点都在 Flex 容器的顶部，而终点都在 Flex 容器的底部，这主要是因为两种语言都是水平书写模式。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意，Flex 容器两轴的起点和终端同样受 &lt;code&gt;flex-direction&lt;/code&gt; 、&lt;code&gt;writing-mode&lt;/code&gt; 或 &lt;code&gt;direction&lt;/code&gt; 属性值的影响。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;众所周之， CSS 中的每个元素都是一个容器，是容器它就有大小。Flexbox 布局中的 Flex 容器 和 Flex 项目同样是元素，它们也有大小。不同的是，对于 Flex 容器而言，它有 &lt;strong&gt;主轴尺寸&lt;/strong&gt; （Main Size）和 &lt;strong&gt;侧轴尺寸&lt;/strong&gt; （Cross Size）之分。它们的差别是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;主轴尺寸&lt;/strong&gt; 是指主轴起点到终点之间的距离；&lt;/li&gt;
&lt;/ul&gt;
&lt;!-- raw HTML omitted --&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;侧轴尺寸&lt;/strong&gt; 是指侧轴起点到终点之间的距离 。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;也就是说，主轴尺寸和侧轴尺寸可以用来决定一个 Flex 容器的大小。但它们并不完全等同于 Flex 容器的宽高（&lt;code&gt;width x height&lt;/code&gt; ）。这是因为 &lt;code&gt;flex-direction&lt;/code&gt; 和 &lt;code&gt;writing-mode&lt;/code&gt; 或 &lt;code&gt;direction&lt;/code&gt; 属性值不同时，用于描述 Flex 容器的物理属性 &lt;code&gt;width&lt;/code&gt; 和 &lt;code&gt;height&lt;/code&gt; 有可能会互换的。比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当 &lt;code&gt;flex-direction&lt;/code&gt; 为 &lt;code&gt;row&lt;/code&gt; ，且书写模式和阅读模式是 LTR 时，主轴的尺寸对应的就是 Flex 容器的宽度，侧轴的尺寸对应的则是 Flex 容器的高度；&lt;/li&gt;
&lt;/ul&gt;
&lt;!-- raw HTML omitted --&gt;
&lt;ul&gt;
&lt;li&gt;当 &lt;code&gt;flex-direction&lt;/code&gt; 为 &lt;code&gt;column&lt;/code&gt; ，且书写模式和阅读模式是 LTR 时，主轴的尺寸对应的就是 Flex 容器的高度，侧轴的尺寸对应的则是 Flex 容器的宽度。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/04/TqV3b1lvcDkBRmg.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;另外，可以在 Flex 容器上显式使用 CSS 的物理属性 &lt;code&gt;width&lt;/code&gt; 和 &lt;code&gt;height&lt;/code&gt; ，或使用 CSS 的逻辑属性 &lt;code&gt;inline-size&lt;/code&gt; 和 &lt;code&gt;block-size&lt;/code&gt; 设置 Flex 容器主轴和侧轴的尺寸 ，也可以使用 &lt;code&gt;min-*&lt;/code&gt; 和 &lt;code&gt;max-*&lt;/code&gt; 对 Flex 容器主轴和侧轴的尺寸加以限制。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/04/Yj3SgEhfZrePHa7.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;fig-02-09.png&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;如果没有显式给 Flex 容器设置尺寸，则会根据所有 Flex 项目的大小来决定，或根据 Flex 容器的父容器来决定。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意，如果需要显式设置 Flex 容器尺寸的话，使用逻辑属性&lt;/strong&gt; &lt;strong&gt;&lt;code&gt;inline-size&lt;/code&gt;&lt;/strong&gt; &lt;strong&gt;或&lt;/strong&gt; &lt;strong&gt;&lt;code&gt;block-size&lt;/code&gt;&lt;/strong&gt; &lt;strong&gt;更符合多语言的 Web 布局！&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;现在，我们已经知道了， &lt;strong&gt;&lt;code&gt;主轴尺寸 x 侧轴尺寸&lt;/code&gt;&lt;/strong&gt; &lt;strong&gt;可以决定一个 Flex 容器的大小。&lt;/strong&gt; 在一个 Flex 容器中可能会包含一个或多个 Flex 项目，且每个 Flex 项目也会有其自身的尺寸大小，这样一来，就有可能造成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所有 Flex 项目宽度（或高度）小于 Flex 容器的宽度（或高度），Flex 容器就会有多余的空间没有被填充，那么这个多出来的空间常称为 &lt;strong&gt;Flex 容器的剩余空间&lt;/strong&gt; （Positive Free Space）。&lt;/li&gt;
&lt;/ul&gt;
&lt;!-- raw HTML omitted --&gt;
&lt;ul&gt;
&lt;li&gt;所有 Flex 项目宽度（或高度）大于 Flex 容器的宽度（或高度），Flex 项目将会溢出 Flex 容器，那么这个溢出的空间常称为 &lt;strong&gt;Flex 容器的不足空间&lt;/strong&gt; （Negative Free Space），也称为 &lt;strong&gt;负空间&lt;/strong&gt; 。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/04/gPKl6Vx9kYRJCQ5.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;Flexbox 布局中有一个强大的特性，当 Flex 容器有剩余空间时：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可以使用主轴的对齐方式 &lt;code&gt;justify-content&lt;/code&gt; 来分配主尺寸的剩余空间；&lt;/li&gt;
&lt;/ul&gt;
&lt;!-- raw HTML omitted --&gt;
&lt;ul&gt;
&lt;li&gt;可以使用侧轴的对齐方式 &lt;code&gt;align-content&lt;/code&gt; 来分配侧尺寸的剩余空间。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;也可以使用 &lt;code&gt;flex&lt;/code&gt; 属性中的 &lt;code&gt;flex-grow&lt;/code&gt; 按比例因子来扩展 Flex 项目的尺寸；当 Flex 容器是不足空间（Flex 项目溢出 Flex 容器），你可以使用 &lt;code&gt;flex&lt;/code&gt; 属性中的 &lt;code&gt;flex-shrink&lt;/code&gt; 按比例因子来对 Flex 项目进行收缩。这个计算在 Flexbox 布局中是复杂的，而且会涉及一定的数学计算，后面我们将会有一个节课专门来介绍这方面的知识。&lt;/p&gt;
&lt;p&gt;到此，你对 Flexbox 布局中的主要术语和概念有了一定的了解了，接下来，我们一起来探讨 Flexbox 布局中的几个重要特性。&lt;/p&gt;
&lt;h2 id=&#34;flexbox-布局模块相关特性&#34;&gt;Flexbox 布局模块相关特性&lt;/h2&gt;
&lt;p&gt;Flexbox 布局模块除了概念多之外，就是可用于 Flexbox 布局的属性也多，这些属性分为两个部分，其中一部分用于 &lt;strong&gt;Flex 容器&lt;/strong&gt; 上，另一部分用于 &lt;strong&gt;Flex 项目&lt;/strong&gt; 上。&lt;/p&gt;
&lt;p&gt;可用于 Flex 容器上的属性主要有：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/04/NLq8bVldnjtES57.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;可用于 Flex 项目上的属性相比于 Flex 容器上要少一点，它主要有：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/04/k61K4qg5QNjhRvM.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意，在这里我们并不会针对每个属性进行介绍！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;控制-flex-项目的方向&#34;&gt;控制 Flex 项目的方向&lt;/h2&gt;
&lt;p&gt;在 Flex 容器中，即使你没有显式设置 &lt;code&gt;flex-direction&lt;/code&gt; 属性的值，Flex 容器中的所有 Flex 项目也会显式为一行，因为 &lt;code&gt;flex-direction&lt;/code&gt; 属性的初始值是 &lt;code&gt;row&lt;/code&gt; 。如果你希望 Flex 项目在 Flex 容器中不是按行呈现，而是按列呈现，则可以将其设置为 &lt;code&gt;column&lt;/code&gt; 或 &lt;code&gt;column-reverse&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/04/SUb4cCKwY3PjvH9.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;来看一个简单的示例：&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;&amp;lt;div class=&amp;#34;container&amp;#34;&amp;gt;
    &amp;lt;div class=&amp;#34;item&amp;#34;&amp;gt;Flex项目1&amp;lt;/div&amp;gt;
    &amp;lt;div class=&amp;#34;item&amp;#34;&amp;gt;Flex项目2&amp;lt;/div&amp;gt;
    &amp;lt;div class=&amp;#34;item&amp;#34;&amp;gt;Flex项目3&amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-CSS&#34; data-lang=&#34;CSS&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nc&#34;&gt;container&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nv&#34;&gt;--direction&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;row&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;display&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;flex&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;flex-direction&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;var&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;--&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;direction&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;Demo 地址： &lt;a class=&#34;link&#34; href=&#34;https://codepen.io/airen/full/MWGeyLw&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;https://codepen.io/airen/full/MWGeyLw&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;尝试改变 Demo 中 &lt;code&gt;flex-direction&lt;/code&gt; 属性的值，你将看到的效果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/04/TDAtK934YjlhPab.gif&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;前面我们说过，默认情况下，Flex 项目排成一行，这一行与书写模式（或阅读模式）的方向是相同的，比如上面示例，使用的是英文，它的阅读模式是 &lt;code&gt;ltr&lt;/code&gt; ，Flex 项目紧挨着 Flex 容器左侧边缘（左对齐）。这意味着，如果我们使用的是阿拉伯语系，即书写模式是 &lt;code&gt;rtl&lt;/code&gt; ，则 Flex 项目将紧挨着 Flex 容器的右侧边缘（右侧对齐）：&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;&amp;lt;div class=&amp;#34;container&amp;#34;&amp;gt;
    &amp;lt;div class=&amp;#34;item&amp;#34;&amp;gt;Flex项目1&amp;lt;/div&amp;gt;
    &amp;lt;div class=&amp;#34;item&amp;#34;&amp;gt;Flex项目2&amp;lt;/div&amp;gt;
    &amp;lt;div class=&amp;#34;item&amp;#34;&amp;gt;Flex项目3&amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
​
&amp;lt;div class=&amp;#34;container&amp;#34; dir=&amp;#34;rtl&amp;#34;&amp;gt;
    &amp;lt;div class=&amp;#34;item&amp;#34;&amp;gt;عنصر فليكس1&amp;lt;/div&amp;gt;
    &amp;lt;div class=&amp;#34;item&amp;#34;&amp;gt;عنصر فليكس2&amp;lt;/div&amp;gt;
    &amp;lt;div class=&amp;#34;item&amp;#34;&amp;gt;عنصر فليكس3&amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-CSS&#34; data-lang=&#34;CSS&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nc&#34;&gt;container&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nv&#34;&gt;--direction&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;row&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;display&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;flex&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;flex-direction&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;var&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;--&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;direction&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;Demo 地址：&lt;a class=&#34;link&#34; href=&#34;https://codepen.io/airen/full/MWGeeLo&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;https://codepen.io/airen/full/MWGeeLo&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/04/LqA42sQjo3n5HlJ.gif&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;另外，CSS 的 &lt;code&gt;writing-mode&lt;/code&gt; 属性的值也将影响 &lt;code&gt;flex-direction&lt;/code&gt; 属性最终呈现的效果：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/04/XExKoV7Jpza8cD3.gif&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Demo 地址：&lt;a class=&#34;link&#34; href=&#34;https://codepen.io/airen/full/rNvLLgJ&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;https://codepen.io/airen/full/rNvLLgJ&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;因此，默认情况下，Flex 项目的行为与文档的书写模式相关。大多数教程使用英语编写，或者采用另一种从左到右的水平书写模式编写。这样就可以很容易地假设弹性项目&lt;strong&gt;在左侧&lt;/strong&gt;对齐并沿&lt;strong&gt;水平方向&lt;/strong&gt;分布。&lt;/p&gt;
&lt;p&gt;在实际的业务开发中，使用 Flexbox 布局时，&lt;code&gt;flex-direction&lt;/code&gt; 是非常有用的，比如下图这个布局效果，红色虚线框中的 Flex 项目是按行排列，&lt;code&gt;flex-direction&lt;/code&gt; 不需要显式设置，但黑色虚线框是按列排列，需要显式将 &lt;code&gt;flex-direction&lt;/code&gt; 属性的值设置为 &lt;code&gt;column&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/04/GMzYvhUQFICAO2N.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;flex-direction&lt;/code&gt; 中的 &lt;code&gt;row&lt;/code&gt; 和 &lt;code&gt;row-reverse&lt;/code&gt; （或 &lt;code&gt;column&lt;/code&gt; 和 &lt;code&gt;column-reverse&lt;/code&gt;）可以让主轴的起点和终点（或侧轴的起点和终点）互换。它们在一些布局中也是非常有用的，比如像下图这样的效果，希望双数的卡片的缩略图靠右，单数的缩略图居左。我们只需要一行代码即可：&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;.card:nth-child(2n) {
    flex-direction: row-reverse;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/04/6W8xoOlF57QsZHD.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Demo 地址： &lt;a class=&#34;link&#34; href=&#34;https://codepen.io/airen/full/oNdLzov&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;https://codepen.io/airen/full/oNdLzov&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;不过，在使用 &lt;code&gt;flex-direction&lt;/code&gt; 属性值 &lt;code&gt;row-reverse&lt;/code&gt; 和 &lt;code&gt;column-reverse&lt;/code&gt; 时，会对 Web 可访问性造成负面影响，因为该属性只是对&lt;strong&gt;视觉呈现&lt;/strong&gt;进行重排，其对应的 HTML 文档的源码顺序是不受该属性影响的。&lt;/p&gt;
&lt;h2 id=&#34;flex-项目换行&#34;&gt;Flex 项目换行&lt;/h2&gt;
&lt;p&gt;默认情况之下，Flex 容器中的所有 Flex 项目沿着主轴方向依次排列（不会换行的或换列），即使是 Flex 项目溢出了 Flex 容器也是如此。这主要是因为 &lt;code&gt;flex-wrap&lt;/code&gt; 属性的默认值为 &lt;code&gt;nowrap&lt;/code&gt; ：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/04/BQb63iqjlGxwgKf.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;需要注意的是，如果 Flex 容器没有足够多的空间，Flex 项目在溢出之前，每一个 Flex 项目将会尽可能缩小到其最小内容（&lt;code&gt;min-content&lt;/code&gt;）的尺寸。即 &lt;strong&gt;Flex 项目一旦达到最小内容（&lt;code&gt;min-content&lt;/code&gt;）大小， Flex 项目将开始溢出 Flex 容器&lt;/strong&gt; ！&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/04/lVoktH3bnxPE2qQ.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Demo 地址：&lt;a class=&#34;link&#34; href=&#34;https://codepen.io/airen/full/ZEoOpPV&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;https://codepen.io/airen/full/ZEoOpPV&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果你希望避免这个现象，只需要在 Flex 容器上显式设置 &lt;code&gt;flex-wrap&lt;/code&gt; 属性的值为 &lt;code&gt;wrap&lt;/code&gt; ：&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;.flex-container {
    flex-wrap: wrap;
 }
&lt;/code&gt;&lt;/pre&gt;&lt;blockquote&gt;
&lt;p&gt;Demo 地址：&lt;a class=&#34;link&#34; href=&#34;https://codepen.io/airen/full/XWqKNrL&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;https://codepen.io/airen/full/XWqKNrL&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/04/mXWJpKj5OPf3Gub.gif&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;Flex 容器在换行后会创建多个 &lt;strong&gt;Flex 行&lt;/strong&gt; 。在空间分布方面，每一行就像一个新的 Flex 容器。因此，如果你要换行，则无法让第 2 行中的某些内容与它上面第 1 行中的某些内容对齐。这就是所谓的 Flex 容器是一维框（盒子）。你只可以在独立的轴上（主轴或侧轴）也就是一行或一列上对齐 Flex 项目，但不能像 CSS Grid 那样同时在两个轴上控制 Grid 项目。&lt;/p&gt;
&lt;p&gt;为此，在使用 Flexbox 布局时，为了让你的布局更具灵活性（代码更健壮，具有一定的防御性），个人建议你在显式声明的 Flex 容器上同时加上 &lt;code&gt;flex-wrap&lt;/code&gt; 的值为 &lt;code&gt;wrap&lt;/code&gt; :&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;/* 不具防御性的 CSS */
.flex-container {
    display: flex; /* 或 inline-flex */
}
​
/* 具有防御性的 CSS */
.flex-container {
    display: flex; /* 或 inline-flex */
    flex-wrap: wrap;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;当然，如果你的布局不需要换行，那么 Flex 容器的 &lt;code&gt;flex-wrap&lt;/code&gt; 采用默认值 &lt;code&gt;nowrap&lt;/code&gt; 更理想。如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/04/6gFT97KzVlDjZ8R.gif&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;事实上它并不是我们所期望的效果，我们所期望的效果是下图这样的，布局需要换行，就需要显式的设置 &lt;code&gt;flex-wrap&lt;/code&gt; 为 &lt;code&gt;wrap&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/04/GPqNfvhCgEYXks7.gif&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;flex-wrap&lt;/code&gt; 属性除了取值为 &lt;code&gt;wrap&lt;/code&gt; 会让 Flex 项目换行排列之外，其另一个属性 &lt;code&gt;wrap-reverse&lt;/code&gt; 也会让 Flex 项目换行排列，只不过行的排列方向和 &lt;code&gt;wrap&lt;/code&gt; 相反。假如你使用的语言是英文，即书写模式和阅读模式都是 &lt;code&gt;ltr&lt;/code&gt; ，那么 &lt;code&gt;flex-wrap&lt;/code&gt; 取值为 &lt;code&gt;wrap&lt;/code&gt; 时，Flex 行的排列将会沿着 Flex 容器侧轴方向从开始处（Flex 容器顶部）向下排列；反之 &lt;code&gt;flex-wrap&lt;/code&gt; 取值为 &lt;code&gt;wrap-reverse&lt;/code&gt; 时， Flex 行的排列将会沿着 Flex 容器侧轴方向从终点处（Flex 容器底部）向上排列：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/04/1eXCfBcQAmWVTH4.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;flex-wrap: wrap-reverse&lt;/code&gt; 同样也受 &lt;code&gt;flex-direction&lt;/code&gt; 属性取值的影响：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/04/XQy2uCR6qkUNWx9.gif&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Demo 地址： &lt;a class=&#34;link&#34; href=&#34;https://codepen.io/airen/full/KKRadzb&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;https://codepen.io/airen/full/KKRadzb&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;还有一点需要特别的注意，&lt;strong&gt;&lt;code&gt;flex-wrap: wrap&lt;/code&gt;&lt;/strong&gt; &lt;strong&gt;(或&lt;/strong&gt; &lt;strong&gt;&lt;code&gt;flex-wrap: wrap-reverse&lt;/code&gt;)碰到了设置 &lt;code&gt;flex:1&lt;/code&gt; 项目时，只有在 Flex 容器没有足够空间容纳 Flex 项目时（即，同一 Flex 行所有 Flex 项目最小内容宽度总和大于 Flex 容器宽度），才会让 Flex 项目换行（或列），另外使用&lt;/strong&gt; &lt;strong&gt;&lt;code&gt;flex-wrap: wrap&lt;/code&gt;&lt;/strong&gt; &lt;strong&gt;要有一个心理预判，不然也有可能会让&lt;/strong&gt; &lt;strong&gt;UI&lt;/strong&gt; &lt;strong&gt;视觉上不美，但不会撑破布局（如上例所示）！&lt;/strong&gt; 选择总是痛苦的（^_^）。&lt;/p&gt;
&lt;p&gt;CSS 中有很多简写属性，简写属性可以包含多个子属性。 如果你在编写 CSS 的时候，需要同时显式设置 &lt;code&gt;flex-direction&lt;/code&gt; 和 &lt;code&gt;flex-wrap&lt;/code&gt; 属性时，那么可以使用它们的简写属性 &lt;strong&gt;&lt;code&gt;flex-flow&lt;/code&gt;&lt;/strong&gt; :&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;.flex-container {
    display: flex;
    flex-flow: column wrap;
}
​
/* 等同于 */
.flex-container {
    display: flex;
    flex-direction: column;
    flex-wrap: wrap;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;我们在使用 &lt;code&gt;flex-flow&lt;/code&gt; 属性时，&lt;strong&gt;可以只显式设置一个值，也可以显式设置两个值：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;flex-flow&lt;/code&gt; 只显式设置一个值，并且该值和 &lt;code&gt;flex-direction&lt;/code&gt; 相匹配时， &lt;code&gt;flex-wrap&lt;/code&gt; 会取值 &lt;code&gt;initial&lt;/code&gt; ；&lt;/li&gt;
&lt;/ul&gt;
&lt;!-- raw HTML omitted --&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;flex-flow&lt;/code&gt; 只显式设置一个值，并且该值和 &lt;code&gt;flex-wrap&lt;/code&gt; 相匹配时，&lt;code&gt;flex-direction&lt;/code&gt; 会取值 &lt;code&gt;initial&lt;/code&gt; ；&lt;/li&gt;
&lt;/ul&gt;
&lt;!-- raw HTML omitted --&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;flex-flow&lt;/code&gt; 显式设置两个值时， &lt;code&gt;flex-direction&lt;/code&gt; 和 &lt;code&gt;flow-wrap&lt;/code&gt; 没有先后顺序之分，即 &lt;code&gt;flex-flow: column wrap&lt;/code&gt; 和 &lt;code&gt;flex-flow: wrap column&lt;/code&gt; 所起作用是等同的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;来看具体代码：&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;.flex-container {
    display: flex;
    flex-flow: column;
    
    /* flex-flow 等同于 */
    flex-direction: colmun;
    flex-wrap: initial;
}
​
.flex-container {
    display: flex;
    flex-flow: wrap;
    
    /* flex-flow 等同于 */
    flex-direction: initial;
    flex-wrap: wrap;
}
​
.flex-container {
    display: flex;
    flex-flow: column wrap;
    
    /* flex-flow 等同于 */
    flex-flow: wrap column;
    
    /* flex-flow 还等同于 */
    flex-direction: column;
    flex-wrap: wrap;
}
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;flex-项目排序&#34;&gt;Flex 项目排序&lt;/h2&gt;
&lt;p&gt;Web 页面是由多个 HTML 元素组建而成，HTML 文档中的元素是按照其在文档中出现的先后顺序决定的，比如下面这样的一个 HTML 文档：&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;&amp;lt;body&amp;gt;
    &amp;lt;header&amp;gt;&amp;lt;/header&amp;gt;
    &amp;lt;main&amp;gt;&amp;lt;/main&amp;gt;
    &amp;lt;aside&amp;gt;&amp;lt;/aside&amp;gt;
    &amp;lt;footer&amp;gt;&amp;lt;/footer&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;在没有任何 CSS 约束之下，它呈现的顺序将会是如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/04/v6tm8q4pAcRVEOy.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;fig-03-23.jpg&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;通过前面的内容我们知道了 Flexbox 布局中，我们可以在 Flex 容器上使用 &lt;code&gt;flex-direction&lt;/code&gt; 属性的值 &lt;code&gt;row-reverse&lt;/code&gt; 和 &lt;code&gt;column-reverse&lt;/code&gt; 来改变主轴和侧轴上 Flex 项目的排列顺序。但不能单独改变某个 Flex 项目的顺序。如果仅是单独对某个（或某几个） Flex 项目重新排序的话，就需要使用可用于 Flex 项目上的 &lt;code&gt;order&lt;/code&gt; 属性。&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;order&lt;/code&gt; 属性可以为 Flex 容器中的项目重新排序。此属性可用于对&lt;strong&gt;有序组&lt;/strong&gt;中的项目进行排序。项目按照 &lt;code&gt;flex-direction&lt;/code&gt; 指定的方向排列，最小值在最前面。如果多个项目具有相同的值，它将与具有该值的其他项目一起显示（按其在源码文档的顺序排列）。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;order&lt;/code&gt; 初始值是 &lt;code&gt;0&lt;/code&gt; ，可以是正值，也可以是负值，属性值越大，越排在主轴的后面，反之越在主轴的前面。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/04/COWu9vSdfHnNMGY.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;如上图所示。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一组中所有 Flex 项目未显式设置 &lt;code&gt;order&lt;/code&gt; 值（即默认值为&lt;code&gt;0&lt;/code&gt;），Flex 项目按照 HTML 文档的源码顺序沿着主轴排列 。&lt;/li&gt;
&lt;/ul&gt;
&lt;!-- raw HTML omitted --&gt;
&lt;ul&gt;
&lt;li&gt;第二组中第二个 Flex 项目显式设置 &lt;code&gt;order&lt;/code&gt; 的值为 &lt;code&gt;1&lt;/code&gt;，这个时候该 Flex 项目会排列在最末尾 。&lt;/li&gt;
&lt;/ul&gt;
&lt;!-- raw HTML omitted --&gt;
&lt;ul&gt;
&lt;li&gt;第三组中第四个 Flex 项目显式设置 &lt;code&gt;order&lt;/code&gt; 的值为 &lt;code&gt;-1&lt;/code&gt;，这个时候该 Flex 项目会排列在最前面 。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;来看一个简单的示例：&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;&amp;lt;div class=&amp;#34;container&amp;#34;&amp;gt;
    &amp;lt;div class=&amp;#34;item&amp;#34;&amp;gt;Flex Item1&amp;lt;/div&amp;gt;
    &amp;lt;div class=&amp;#34;item&amp;#34;&amp;gt;Flex Item2&amp;lt;/div&amp;gt;
    &amp;lt;div class=&amp;#34;item&amp;#34;&amp;gt;Flex Item3&amp;lt;/div&amp;gt;
    &amp;lt;div class=&amp;#34;item&amp;#34;&amp;gt;Flex Item4&amp;lt;/div&amp;gt;
    &amp;lt;div class=&amp;#34;item&amp;#34;&amp;gt;Flex Item5&amp;lt;/div&amp;gt;
    &amp;lt;div class=&amp;#34;item&amp;#34;&amp;gt;Flex Item6&amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-CSS&#34; data-lang=&#34;CSS&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nc&#34;&gt;container&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;display&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;flex&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kc&#34;&gt;flex&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;wrap&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;wrap&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;​&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;nd&#34;&gt;root&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nv&#34;&gt;--order&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;​&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nc&#34;&gt;item&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;nd&#34;&gt;nth-child&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;order&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;-1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;​&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nc&#34;&gt;item&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;nd&#34;&gt;nth-child&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;3&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;order&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;var&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;--&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;order&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;​&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nc&#34;&gt;item&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;nd&#34;&gt;nth-child&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;4&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;order&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;3&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;Demo 地址： &lt;a class=&#34;link&#34; href=&#34;https://codepen.io/airen/full/ZEoORVY&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;https://codepen.io/airen/full/ZEoORVY&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;示例中我们显式指定了第二个 Flex 项目的 &lt;code&gt;order&lt;/code&gt; 值为 &lt;code&gt;-1&lt;/code&gt; ，第四个 Flex 项目的 &lt;code&gt;order&lt;/code&gt; 值为 &lt;code&gt;3&lt;/code&gt; ，并且动态调整第三个 Flex 项目的 &lt;code&gt;order&lt;/code&gt; 值，你将看到的效果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/04/I4TKbUAznBJwYoS.gif&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;在 Flexbox 布局中，在 Flex 项目上使用 &lt;code&gt;order&lt;/code&gt; 属性可以和在 Flex 容器上使用 &lt;code&gt;flex-direction&lt;/code&gt; 的 &lt;code&gt;row-reverse&lt;/code&gt; （或 &lt;code&gt;column-reverse&lt;/code&gt; ）等同的效果：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/04/Ojne2t5B8XfbUar.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;order&lt;/code&gt; 属性也适用于页面级别的布局，比如下面这个示例：&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;&amp;lt;!-- HTML --&amp;gt; 
&amp;lt;header&amp;gt;Header Section&amp;lt;/header&amp;gt; 
&amp;lt;main&amp;gt; 
    &amp;lt;article&amp;gt;Article Section&amp;lt;/article&amp;gt; 
    &amp;lt;nav&amp;gt;Nav Section&amp;lt;/nav&amp;gt; 
    &amp;lt;aside&amp;gt;Aside Section&amp;lt;/aside&amp;gt; 
&amp;lt;/main&amp;gt; 
&amp;lt;footer&amp;gt;Footer Section&amp;lt;/footer&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;其中 &lt;code&gt;&amp;lt;article&amp;gt;&lt;/code&gt; 放在 &lt;code&gt;&amp;lt;nav&amp;gt;&lt;/code&gt; 和 &lt;code&gt;&amp;lt;aside&amp;gt;&lt;/code&gt; 前面，主要是为了内容为先。针对这样的 DOM 结构，如果我们希望 &lt;code&gt;&amp;lt;nav&amp;gt;&lt;/code&gt; 在 &lt;code&gt;&amp;lt;article&amp;gt;&lt;/code&gt; 左侧，&lt;code&gt;&amp;lt;aside&amp;gt;&lt;/code&gt; 在 &lt;code&gt;&amp;lt;article&amp;gt;&lt;/code&gt; 右侧时，&lt;code&gt;order&lt;/code&gt; 属性就可以起关键性的作用：&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;nav {
    order: -1;
}
​
main:hover nav {
    order: 1;
}
​
main:hover aside {
    order: -1;
}
&lt;/code&gt;&lt;/pre&gt;&lt;blockquote&gt;
&lt;p&gt;Demo 地址：&lt;a class=&#34;link&#34; href=&#34;https://codepen.io/airen/full/MWGeBKa&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;https://codepen.io/airen/full/MWGeBKa&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/04/9eQ4KYhDPtBvb5q.gif&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;需要注意的是，&lt;code&gt;order&lt;/code&gt; 在使用时与 &lt;code&gt;flex-direction&lt;/code&gt; 的值 &lt;code&gt;row-reverse&lt;/code&gt; 和 &lt;code&gt;column-reverse&lt;/code&gt; 存在相同的问题。它对 Web 可访问性是不友好的。请勿使用 &lt;code&gt;order&lt;/code&gt;，因为你需要修复文档中的乱序问题。&lt;/p&gt;
&lt;h2 id=&#34;flex-项目之间的间距&#34;&gt;Flex 项目之间的间距&lt;/h2&gt;
&lt;p&gt;以往在 CSS 中，常常使用 &lt;code&gt;margin&lt;/code&gt; 属性来设置元素与元素之间的间距。在今天，Flexbox 布局中，你可以使用 &lt;code&gt;gap&lt;/code&gt; 属性来设置元素与元素之间的间距。实质上，&lt;code&gt;gap&lt;/code&gt; 是用来定义&lt;strong&gt;列与列&lt;/strong&gt; 或 &lt;strong&gt;行与行&lt;/strong&gt; 之间的间距。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/04/Ktq7rsfLOXWBNdJ.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;gap&lt;/code&gt; 属性的使用非常的简单，只需要在 Flex 容器显式指定&lt;code&gt;gap&lt;/code&gt; 属性值即可：&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;:root {
  --gap: 1rem;
  --columns: 5;
}
​
.container {
  gap: var(--gap);
}
​
.item {
  flex-basis: calc((100% - (var(--columns) - 1) * var(--gap)) / var(--columns));
}
&lt;/code&gt;&lt;/pre&gt;&lt;blockquote&gt;
&lt;p&gt;Demo 地址：&lt;a class=&#34;link&#34; href=&#34;https://codepen.io/airen/full/LYmZJjo&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;https://codepen.io/airen/full/LYmZJjo&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;效果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/04/p89bLexdAiQ4HD5.gif&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;gap&lt;/code&gt; 属性可接受一个值，也可以接受两个值，当只显式设置一个值时，那么第二个值和第一个值等同，如果显式设置两个值，第一个值是 &lt;code&gt;row-gap&lt;/code&gt; 属性的值，第二个则是 &lt;code&gt;column-gap&lt;/code&gt; 属性的值：&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;.flex-container {
    gap: 10px;
    
    /* 等同于 */
    row-gap: 10px;
    column-gap: 10px;
}
​
.flex-container {
    gap: 10px 20px;
    
    /* 等同于 */
    row-gap: 10px;
    column-gap: 20px;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;在上面我们介绍 Flexbox 术语和概念时提到过，&lt;code&gt;gap&lt;/code&gt; 和 &lt;code&gt;margin&lt;/code&gt; 虽然都可以设置元素与元素之间的间距，但它们之间有明显的差异：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/04/Zk2EYaI8356Nu4H.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;margin&lt;/code&gt; 除了难以达到设计预期效果之外，它们使用的地方也略有差异：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;gap&lt;/code&gt; 运用在 Flex 容器上，但它无法给 Flex 项目设置不同的外间距；&lt;/li&gt;
&lt;/ul&gt;
&lt;!-- raw HTML omitted --&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;margin&lt;/code&gt; 运用在 Flex 项目上，可以给 Flex 项目设置不同的外间距。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;另外，使用 &lt;code&gt;margin&lt;/code&gt; 会让 Flex 项目与 Flex 容器之间有空白间距：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/04/1SF4HdEUOa25khR.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;gap&lt;/code&gt; 属性还没出现之前，往往都是使用 &lt;code&gt;margin&lt;/code&gt; 来模拟 &lt;code&gt;gap&lt;/code&gt; 属性的效果：&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;&amp;lt;div class=&amp;#34;container&amp;#34;&amp;gt;
  &amp;lt;div class=&amp;#34;flex__container flex__container--margin&amp;#34; data-gutter=&amp;#34;margin&amp;#34;&amp;gt;
    &amp;lt;div class=&amp;#34;flex__item&amp;#34;&amp;gt;Flex Item 1&amp;lt;/div&amp;gt;
    &amp;lt;div class=&amp;#34;flex__item&amp;#34;&amp;gt;Flex Item 2&amp;lt;/div&amp;gt;
    &amp;lt;div class=&amp;#34;flex__item&amp;#34;&amp;gt;Flex Item 3&amp;lt;/div&amp;gt;
    &amp;lt;div class=&amp;#34;flex__item&amp;#34;&amp;gt;Flex Item 4&amp;lt;/div&amp;gt;
    &amp;lt;div class=&amp;#34;flex__item&amp;#34;&amp;gt;Flex Item 5&amp;lt;/div&amp;gt;
    &amp;lt;div class=&amp;#34;flex__item&amp;#34;&amp;gt;Flex Item 1&amp;lt;/div&amp;gt;
    &amp;lt;div class=&amp;#34;flex__item&amp;#34;&amp;gt;Flex Item 2&amp;lt;/div&amp;gt;
    &amp;lt;div class=&amp;#34;flex__item&amp;#34;&amp;gt;Flex Item 3&amp;lt;/div&amp;gt;
    &amp;lt;div class=&amp;#34;flex__item&amp;#34;&amp;gt;Flex Item 4&amp;lt;/div&amp;gt;
    &amp;lt;div class=&amp;#34;flex__item&amp;#34;&amp;gt;Flex Item 5&amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&amp;lt;div class=&amp;#34;container&amp;#34;&amp;gt;
  &amp;lt;div class=&amp;#34;flex__container flex__container--gap&amp;#34; data-gutter=&amp;#34;gap&amp;#34;&amp;gt;
    &amp;lt;div class=&amp;#34;flex__item&amp;#34;&amp;gt;Flex Item 1&amp;lt;/div&amp;gt;
    &amp;lt;div class=&amp;#34;flex__item&amp;#34;&amp;gt;Flex Item 2&amp;lt;/div&amp;gt;
    &amp;lt;div class=&amp;#34;flex__item&amp;#34;&amp;gt;Flex Item 3&amp;lt;/div&amp;gt;
    &amp;lt;div class=&amp;#34;flex__item&amp;#34;&amp;gt;Flex Item 4&amp;lt;/div&amp;gt;
    &amp;lt;div class=&amp;#34;flex__item&amp;#34;&amp;gt;Flex Item 5&amp;lt;/div&amp;gt;
    &amp;lt;div class=&amp;#34;flex__item&amp;#34;&amp;gt;Flex Item 1&amp;lt;/div&amp;gt;
    &amp;lt;div class=&amp;#34;flex__item&amp;#34;&amp;gt;Flex Item 2&amp;lt;/div&amp;gt;
    &amp;lt;div class=&amp;#34;flex__item&amp;#34;&amp;gt;Flex Item 3&amp;lt;/div&amp;gt;
    &amp;lt;div class=&amp;#34;flex__item&amp;#34;&amp;gt;Flex Item 4&amp;lt;/div&amp;gt;
    &amp;lt;div class=&amp;#34;flex__item&amp;#34;&amp;gt;Flex Item 5&amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-CSS&#34; data-lang=&#34;CSS&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;nd&#34;&gt;root&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nv&#34;&gt;--flexDirection&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;row&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nv&#34;&gt;--flexWrap&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;wrap&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nv&#34;&gt;--item-basis&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;100&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;px&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nv&#34;&gt;--gap&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;rem&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nv&#34;&gt;--columns&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;5&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;​&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nc&#34;&gt;container&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;width&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;calc&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nf&#34;&gt;var&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;--&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;item&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;basis&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;var&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;--&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;columns&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;var&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;--&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;gap&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;var&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;--&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;columns&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;6&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;px&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;​&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nc&#34;&gt;flex__container&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;display&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;flex&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;flex-flow&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;var&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;--&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;flexDirection&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;var&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;--&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;flexWrap&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;​&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nc&#34;&gt;flex__container--margin&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;margin&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;calc&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;-1&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;var&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;--&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;gap&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;calc&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;-1&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;var&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;--&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;gap&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;​&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nc&#34;&gt;flex__container--margin&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nc&#34;&gt;flex__item&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;margin&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;var&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;--&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;gap&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;var&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;--&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;gap&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;​&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nc&#34;&gt;flex__container--gap&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;gap&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;var&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;--&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;gap&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;​&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nc&#34;&gt;flex__item&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;n&#34;&gt;inline-size&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;var&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;--&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;item&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;basis&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;n&#34;&gt;aspect-ratio&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;Demo 地址： &lt;a class=&#34;link&#34; href=&#34;https://codepen.io/airen/full/wvjWERe&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;https://codepen.io/airen/full/wvjWERe&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;效果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/04/YqzTtlZMKU2RByu.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;h2 id=&#34;小结&#34;&gt;小结&lt;/h2&gt;
&lt;p&gt;通过这一节的学习，我想你对 Flexbox 布局应该有了进一步的了解。知道了如何使用 &lt;code&gt;flex-direction&lt;/code&gt; 和 &lt;code&gt;order&lt;/code&gt; 来控制 Flex 项目的排序；如何使用 &lt;code&gt;flex-wrap&lt;/code&gt; 属性让 Flex 项目换行以及使用 &lt;code&gt;gap&lt;/code&gt; （或 &lt;code&gt;margin&lt;/code&gt;）控制 Flex 项目之间的间距等。&lt;/p&gt;
&lt;p&gt;这些只是 Flexbox 布局基础知识的几个关键功能，其实 Flexbox 布局中还有另一个关键功能，那就是 Flex 容器空间分配。接下来的课程中，我将着重和大家一起探讨 Flex 容器的空间是如何分配的，也就是 Flexbox 布局中对齐模式。&lt;/p&gt;
</description>
        </item>
        <item>
        <title>现代 Web 布局 - (2) 现代 Web 布局技术术语</title>
        <link>https://blog.ther.cool/posts/%E7%8E%B0%E4%BB%A3-web-%E5%B8%83%E5%B1%80-2-%E7%8E%B0%E4%BB%A3-web-%E5%B8%83%E5%B1%80%E6%8A%80%E6%9C%AF%E6%9C%AF%E8%AF%AD/</link>
        <pubDate>Wed, 01 Jan 2025 19:07:52 +0800</pubDate>
        
        <guid>https://blog.ther.cool/posts/%E7%8E%B0%E4%BB%A3-web-%E5%B8%83%E5%B1%80-2-%E7%8E%B0%E4%BB%A3-web-%E5%B8%83%E5%B1%80%E6%8A%80%E6%9C%AF%E6%9C%AF%E8%AF%AD/</guid>
        <description>&lt;p&gt;Web 发展的每个不同时期都有新的技术为 Web 布局提供支持，但不管是哪个时期，Web 布局相关的概念和术语都是相同的。如果你想彻底或者更好地掌握 Web 布局，那么首先需要对 Web 布局相关的技术术语有所了解。&lt;/p&gt;
&lt;p&gt;在这一节中，我们一起来探讨 Web 布局相关的术语。&lt;/p&gt;
&lt;h2 id=&#34;web-坐标轴&#34;&gt;Web 坐标轴&lt;/h2&gt;
&lt;p&gt;坐标轴不只是存在于数学中，它同样存在于 Web 世界中。在 Web 中，我们常称之为 &lt;strong&gt;Web&lt;/strong&gt; &lt;strong&gt;坐标轴&lt;/strong&gt; 或 &lt;strong&gt;CSS&lt;/strong&gt; &lt;strong&gt;坐标系统&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;在 Web 中，默认原点是给定上下文的左上角，也就是元素盒子的左上角，它分为 &lt;code&gt;x&lt;/code&gt; 轴（也称为水平轴），向右为正值，向左为负值；&lt;code&gt;y&lt;/code&gt; 轴（垂直轴），向上为负值，向下为正值：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/gLY8d46vxbTsE1H.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;请注意，这与大多数数学模型不同，其中原点位于&lt;em&gt;左下角&lt;/em&gt;，正 &lt;code&gt;y&lt;/code&gt; 坐标值位于原点上方。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;事实上，除了大家熟悉的平面画布中的 &lt;code&gt;x&lt;/code&gt; 和 &lt;code&gt;y&lt;/code&gt; 轴之外，还有控制第三维度的 &lt;code&gt;z&lt;/code&gt; 轴。比如使用 CSS 的 &lt;code&gt;transform&lt;/code&gt; 绘制 3D 图形或使用第三维度从前往后对对象进行分层：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/Ny5LuUeDQrHkRoX.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;也会在定位元素（显式使用&lt;code&gt;position&lt;/code&gt; 属性值为非 &lt;code&gt;static&lt;/code&gt; 的元素）上使用 &lt;code&gt;z-index&lt;/code&gt; 控制其层叠的顺序（&lt;code&gt;z&lt;/code&gt; 轴上的层叠顺序），它表示的是用户与屏幕的这条看不见的垂直线：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/ILYnV51ElOouaNA.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;上面我们看到的 CSS 坐标系，只是最初的坐标系的定义。随着 CSS 的逻辑属性出现，CSS 的坐标系也随之改变。它分为 &lt;strong&gt;内联轴&lt;/strong&gt; （Inline Axis）和 &lt;strong&gt;块轴&lt;/strong&gt; （Block Axis）。&lt;/p&gt;
&lt;p&gt;众所周知，在 CSS 中，每个元素都是一个盒子，默认情况之下，盒子会根据元素类型分为块盒子（比如块元素 &lt;code&gt;div&lt;/code&gt; ）和 内联盒子（比如&lt;code&gt;span&lt;/code&gt;）。其中块盒会在垂直方向从上往下堆叠，内联盒子将会按照书写方式从左往右排列：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/GKz6MRdTHsWC9kc.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;当我们的书写方式改变时，块盒子和内联盒子也会有相应的变化，就拿内联盒子为例，书写模式改变之后，它的方向也会随之改变：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/hy8b6zAWXkEnYCQ.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;简而言之，&lt;strong&gt;块元素遵循流方向，内联元素遵循写入方向&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/ZIskyAjaeUFYq8d.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;也就是说，随着 CSS 的逻辑属性的出现，CSS 的坐标系就不再以 &lt;code&gt;x&lt;/code&gt; 轴 和 &lt;code&gt;y&lt;/code&gt; 轴来定义，而是以 &lt;strong&gt;内联&lt;/strong&gt; （Inline）和 &lt;strong&gt;块&lt;/strong&gt; （Block）来区分，并且内联方向的称之为 &lt;strong&gt;内联轴&lt;/strong&gt; （Inline Axis），也就是书写模式的方向；块方向的称之为 &lt;strong&gt;块轴&lt;/strong&gt; （Block Axis），也就是块盒子自然流的方向。它们随着 CSS 的书写模式改变，如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/U3jaCf6ytqu4Nhp.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;fig-02-07.jpg&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;如此一来，在 CSS 中就有&lt;strong&gt;物理坐标系&lt;/strong&gt; 和 &lt;strong&gt;逻辑坐标系&lt;/strong&gt; 之分，它们的对应关系如下：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;物理属性&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;逻辑属性(&lt;code&gt;horizontal-tb&lt;/code&gt;)&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;逻辑属性(&lt;code&gt;vertical-lr&lt;/code&gt;)&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;逻辑属性(&lt;code&gt;vertical-rl&lt;/code&gt;)&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;x&lt;/code&gt; 轴（水平轴）&lt;/td&gt;
&lt;td&gt;Inline 轴（内联轴）&lt;/td&gt;
&lt;td&gt;Block 轴（块轴）&lt;/td&gt;
&lt;td&gt;Block 轴（块轴）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;y&lt;/code&gt; 轴（垂直轴）&lt;/td&gt;
&lt;td&gt;Block 轴（块轴）&lt;/td&gt;
&lt;td&gt;Inline 轴（内联轴）&lt;/td&gt;
&lt;td&gt;Inline 轴（内联轴）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt; ，不管是在物理坐标系还是逻辑坐标系中，&lt;code&gt;z&lt;/code&gt; 轴是不变的！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;容器和容器空间&#34;&gt;容器和容器空间&lt;/h2&gt;
&lt;p&gt;容器这个概念很简单。熟悉 CSS 的同学都知道，HTML 的每一个元素在 CSS 中都是一个盒子，这个盒子又被称为 &lt;strong&gt;容器&lt;/strong&gt; 。只不过，这个容器会随着盒子的类型不同，容器的称呼也会有不同。它主要由 CSS 的 &lt;code&gt;display&lt;/code&gt; 属性的值来决定，比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;block&lt;/code&gt; 时称为块容器；&lt;/li&gt;
&lt;/ul&gt;
&lt;!-- raw HTML omitted --&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;inline&lt;/code&gt; 时称为内联容器；&lt;/li&gt;
&lt;/ul&gt;
&lt;!-- raw HTML omitted --&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;flex&lt;/code&gt; 或 &lt;code&gt;inline-flex&lt;/code&gt; 时称为Flexbox容器；&lt;/li&gt;
&lt;/ul&gt;
&lt;!-- raw HTML omitted --&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;grid&lt;/code&gt; 或 &lt;code&gt;inline-grid&lt;/code&gt; 时称为 Grid 容器（网格容器）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;每个容器就像生活中的器皿一样，不管是什么类型的容器，它都有空间。只不过这个空间的大小是由 CSS 盒模型相关的属性来决定的：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/nu2jQlNk6ZbeqOV.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;只不过，Web 开发者习惯性以 &lt;code&gt;width&lt;/code&gt; 、&lt;code&gt;height&lt;/code&gt; 、 &lt;code&gt;min-*&lt;/code&gt; 或 &lt;code&gt;max-*&lt;/code&gt; 以及它们对应的逻辑属性来显式指定一个容器空间的大小：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/jn2XmQkz6pM5VgE.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;fig-02-09.png&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;而且容器大小计算方式也会受 CSS 的 &lt;code&gt;box-sizing&lt;/code&gt; 属性的值影响：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/cXIn1JupesN6q3h.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;每个容器中都有可能会放置内容（文本内容或其他元素），随着容器中放置的内容多少，可能会造成指定大小的容器无法容纳嵌套的内容，造成内容溢出（超出指定容器的大小）；也有可能放置的内容较少，无法填充满整个容器。&lt;/p&gt;
&lt;p&gt;按此呈现模式，每个容器的大小（空间）又有&lt;strong&gt;可用空间&lt;/strong&gt; （也称为 &lt;strong&gt;剩余空间&lt;/strong&gt; ）和 &lt;strong&gt;不可用空间&lt;/strong&gt; （也称为 &lt;strong&gt;不足空间&lt;/strong&gt; ）：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/K5YAwU6gtBhNv7C.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;h2 id=&#34;间距&#34;&gt;间距&lt;/h2&gt;
&lt;p&gt;Web 是由很多个元素堆叠而成的，为了让 Web 页面给用户提供更好的体验，Web 设计师在设计时，会根据美学相关的理论来设计元素与元素之间，元素内容与元素盒子边缘之间的间距：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/mBb69vWgfP2JViT.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;只不过，在 Web 布局中，我们常常是使用 CSS 的 &lt;code&gt;margin&lt;/code&gt; 、&lt;code&gt;padding&lt;/code&gt; 和 &lt;code&gt;gap&lt;/code&gt; 三个属性来设置间距。不同的是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;框与框（也就是元素与元素）之间的间距一般采用 &lt;code&gt;margin&lt;/code&gt; 或 &lt;code&gt;gap&lt;/code&gt; 属性来设置，也常称为 &lt;strong&gt;外距&lt;/strong&gt; ；&lt;/li&gt;
&lt;/ul&gt;
&lt;!-- raw HTML omitted --&gt;
&lt;ul&gt;
&lt;li&gt;内容与框的边缘（元素内容与元素框边缘）之间的间距一般采用 &lt;code&gt;padding&lt;/code&gt; 来设置，也常称为 &lt;strong&gt;内距&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/ICbxNsp8cvoVUKH.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;特别要提出来的是，CSS 中的 &lt;code&gt;margin&lt;/code&gt; 和 &lt;code&gt;gap&lt;/code&gt; 表现形式是有较大差异的，它们之间的差异用下图来阐述：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/DxOWbQIRYVB8Tda.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;h2 id=&#34;书写模式&#34;&gt;书写模式&lt;/h2&gt;
&lt;p&gt;世界上有很多种语言（简称&lt;a class=&#34;link&#34; href=&#34;https://zh.wikipedia.org/wiki/%E4%B8%96%E7%95%8C%E8%AA%9E%E8%A8%80&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;世界语言&lt;/a&gt;），比如汉语、英语、印度斯坦语、西班牙语、阿拉伯语、俄语、葡萄牙语、德语和法语等。在互联中也有一些常用语言，比如英语、汉语、西班牙语、葡萄牙语、印尼语（马来语）、法语、日语、俄语和德语等。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/pLHVGUQlgoqehwO.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;不同的语系，它们的书写模式（阅读模式）是有差异的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;拉丁语体系&lt;/strong&gt; 是从左往右，比如英语、西班牙语、德语、法语等；&lt;/li&gt;
&lt;/ul&gt;
&lt;!-- raw HTML omitted --&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;阿拉伯语体系&lt;/strong&gt; 是从右往左，比如阿拉伯语、希伯来语等；&lt;/li&gt;
&lt;/ul&gt;
&lt;!-- raw HTML omitted --&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;汉语体系&lt;/strong&gt; 有两种方式，有可能是从左往右，也有从上向往下，比如中文、日文、韩文等。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;正因为语言的书写方式不同，在 Web 中呈现不同语系时，CSS 中的块（Block）和 内联（Inline）表现的方式也会不同。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/DWCFiMxNAc2pjRy.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;在 Web 布局中，尤其是针对多语言的 Web 布局，我们可以通过 HTML 元素的 &lt;code&gt;dir&lt;/code&gt; 属性或 CSS 的 &lt;code&gt;direction&lt;/code&gt; 属性来控制书写模式，比如 &lt;code&gt;ltr&lt;/code&gt; （Left-To-Right，也就是从左往右的书写方式），&lt;code&gt;rtl&lt;/code&gt; （Right-To-Left，也就是从右往左的书写方式）。除此之外，还可以使用 CSS 的 &lt;code&gt;writing-mode&lt;/code&gt; 属性来控制：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/Ictp5gBKswfC9ju.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;正因为语系不同书写模式不同，也将造成 CSS 中布局相关属性最终呈现给用户的效果有所差异，比如 Flexbox 中的 &lt;code&gt;flex-direction&lt;/code&gt; 属性，CSS Box Alignment 模块中的属性以及 Grid 布局等。&lt;/p&gt;
&lt;h2 id=&#34;逻辑属性&#34;&gt;逻辑属性&lt;/h2&gt;
&lt;p&gt;我们构建的 Web 页面不仅是局限于单语种中，你负责的业务有可能是国际业务。这样一来，你构建的 Web 页面是一个多语种的 Web 页面，那么你的布局会因为语种不同有所差异。比如 Facebook 的登录页面：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/TrE1lH5wquXV7MF.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;上图中左侧是汉语体系的布局效果，右侧是阿拉伯语体系的布局效果。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;众所周知，CSS 中有很多属性和值是遵循 &lt;strong&gt;TRBL&lt;/strong&gt; (Top、Right、Bottom 和 Left ) 模式的：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/Nd4X63MTjDvqhfc.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;比如，我们熟悉的元素位置会映射到 &lt;code&gt;top&lt;/code&gt; 、&lt;code&gt;right&lt;/code&gt;、&lt;code&gt;bottom&lt;/code&gt; 和 &lt;code&gt;left&lt;/code&gt;，除此之外，像 &lt;code&gt;border&lt;/code&gt; 、 &lt;code&gt;margin&lt;/code&gt; 、&lt;code&gt;padding&lt;/code&gt; 和 &lt;code&gt;border-radius&lt;/code&gt;等属性的子属性也会映射到 TRBL 上，如 &lt;code&gt;margin-top&lt;/code&gt; 、&lt;code&gt;margin-right&lt;/code&gt; 、&lt;code&gt;margin-bottom&lt;/code&gt; 和 &lt;code&gt;margin-left&lt;/code&gt; 。它们带有明确的方向性。只不过，针对多语言布局时，它给布局带来很大的局限性，比如下面这个简单地示例：&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;.thumb {
    margin-right: 1rem;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;在&lt;code&gt;ltr&lt;/code&gt; 模式（比如英文）和 &lt;code&gt;rtl&lt;/code&gt; 模式（比如阿拉伯文），效果将会是像下图一样：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/xVI9F4AmnrSekas.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;这个效果并不是我们所期待的，如果希望达到预期的效果，在以往编码的时候，需要做额外的处理：&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;.thumb {
    margin-right: 1rem;
}
​
[dir=&amp;#34;rtl&amp;#34;] .thumb {
    margin-left: 1rem;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/BzreVQl2JkdhmFN.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;为了解决类似上图这样的问题，2017 年 5 月 18 日，W3C 的 CSS 工作组（CSS Working Group） 发布了 &lt;a class=&#34;link&#34; href=&#34;https://www.w3.org/TR/css-logical-1/#intro&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;CSS逻辑属性和值&lt;/a&gt;（CSS Logical Properties and Values Level 1） 的首份工作草案（First Public Working Draft）。&lt;/p&gt;
&lt;p&gt;在这个模块中并没有方向性的概念，只有开始（&lt;code&gt;start&lt;/code&gt; ）、结束（&lt;code&gt;end&lt;/code&gt;）、块（&lt;code&gt;block&lt;/code&gt;）和 内联（&lt;code&gt;inline&lt;/code&gt; ）的概念。这样一来，在从左到右的（&lt;code&gt;ltr&lt;/code&gt;）中，&lt;code&gt;start&lt;/code&gt; 对应的是 &lt;code&gt;left&lt;/code&gt; ，但在从右到左（&lt;code&gt;rtl&lt;/code&gt;）中，&lt;code&gt;start&lt;/code&gt; 对应的是 &lt;code&gt;right&lt;/code&gt; 。也就是说，逻辑属性更易于适应不同的书写模式。&lt;/p&gt;
&lt;p&gt;当然，逻辑属性出现之后，很多 CSS 属性和属性值也随之有了变化，在原有的物理属性的基础上映射了一份逻辑属性。尤其是 CSS 的盒模型相关的属性（比如 &lt;code&gt;width&lt;/code&gt;、&lt;code&gt;height&lt;/code&gt; 、&lt;code&gt;min-*&lt;/code&gt; 、&lt;code&gt;max-*&lt;/code&gt; 、&lt;code&gt;border&lt;/code&gt; 、&lt;code&gt;margin&lt;/code&gt; 、&lt;code&gt;padding&lt;/code&gt;）、定位位移相关的属性（比如 &lt;code&gt;top&lt;/code&gt; 、&lt;code&gt;right&lt;/code&gt; 、&lt;code&gt;bottom&lt;/code&gt; 和 &lt;code&gt;left&lt;/code&gt;）、排版方面的（比如&lt;code&gt;float&lt;/code&gt; 属性的值 &lt;code&gt;left&lt;/code&gt; 和 &lt;code&gt;right&lt;/code&gt;）以及圆角 &lt;code&gt;border-radius&lt;/code&gt; 等：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/2OxfvVMNuygrZ9d.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;而且映射关系与 CSS 的 &lt;code&gt;writing-mode&lt;/code&gt; 属性值也有关系，对应关系如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/hM53Gy7Jcg6oRPt.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;fig-02-23.png&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;有了逻辑属性之后，构建多语言 Web 的布局就要方便得多：&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;.thumb {
    margin-inline-end: 1rem;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/kcSuMVt5vT3Eal4.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;h2 id=&#34;对齐方式&#34;&gt;对齐方式&lt;/h2&gt;
&lt;p&gt;这里所说的对齐方式指的是 &lt;a class=&#34;link&#34; href=&#34;https://www.w3.org/TR/css-align-3/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;CSS Box Alignment 模块&lt;/a&gt;，该模块的出现可以说改善了 CSS 中非常有限的对齐能力。在以往，我们控制对齐方式主要是依赖于 CSS 的 &lt;code&gt;text-align&lt;/code&gt; （水平方向文本对齐）和 &lt;code&gt;vertical-align&lt;/code&gt; （垂直方向文本对齐）两个属性，对于块的对齐方式主要依赖于 &lt;code&gt;float&lt;/code&gt; 属性。它们是无法满足 Web 布局中的对齐控制。&lt;/p&gt;
&lt;p&gt;庆幸的是，随着 CSS Flexbox 特性出现之后，CSS 新增了像 &lt;code&gt;justify-content&lt;/code&gt; 、&lt;code&gt;align-content&lt;/code&gt; 、&lt;code&gt;align-items&lt;/code&gt; 、&lt;code&gt;justify-items&lt;/code&gt; 和 &lt;code&gt;justify-self&lt;/code&gt; 以及 &lt;code&gt;align-self&lt;/code&gt; 等属性，用来控制 Web 布局上的对齐方式。最初这些属性是在 Flexbox 相关规范中定义的，但随着 CSS Grid 布局出现之后，W3C 的 CSS 工作组将这些属性单独划分到一个模块中，即 CSS Box Alignment 模块。&lt;/p&gt;
&lt;p&gt;正如 &lt;a class=&#34;link&#34; href=&#34;https://twitter.com/Prathkum&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;@Pratham&lt;/a&gt; 发的 &lt;a class=&#34;link&#34; href=&#34;https://twitter.com/prathkum/status/1388118908326928392?lang=gl&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Twitter 信息上所说&lt;/a&gt;：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;CSS Box Alignment 模块是 Web 布局不可或缺的部分，而且其中有很多概念是极易于混淆的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/vjbsAFViyuPO2wC.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;CSS Box Alignment 模块中，取不同的值时，能得到不同的对齐结果，比如靠左（开始）、靠右（结束）、中间对齐、两端对齐等：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/jZ1lT9GAgw6ORCW.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意，上图由 &lt;a class=&#34;link&#34; href=&#34;https://twitter.com/Prathkum&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;@Pratham&lt;/a&gt; 绘制！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;需要注意的是， CSS Box Alignment 模块中的属性同时可以运用于 CSS Flexbox 和 CSS Grid 布局中，在运用于 CSS Flexbox 和 CSS Grid 布局中时略有差异，具体我们会在后面介绍 Flexbox 和 Grid 布局中会提到，在这里不详细阐述，大家只要知道，在现代 Web 布局技术中，对齐方式新增了该模块！&lt;/p&gt;
&lt;h2 id=&#34;小结&#34;&gt;小结&lt;/h2&gt;
&lt;p&gt;在这篇文章中，我们主要和大家一起探讨了 Web 布局相关的概念和术语。从我们最为熟悉的坐标系统开始，到我们熟悉的容器以及容器空间，再到新增的书写模式、逻辑属性以及对齐方式等。我们花一个节课来介绍这方面，主要是为了和大家把布局相关的概念统一起来，为后续布局打下基础。&lt;/p&gt;
&lt;p&gt;有了这些基础和认识之后，就可以开启现代布局中的 Flexbox 布局了 ！Let&amp;rsquo;s Go! (^_^)&lt;/p&gt;
</description>
        </item>
        <item>
        <title>现代 Web 布局 - (1) Web 布局技术演进：了解 Web 布局发展史</title>
        <link>https://blog.ther.cool/posts/%E7%8E%B0%E4%BB%A3-web-%E5%B8%83%E5%B1%80-1-web-%E5%B8%83%E5%B1%80%E6%8A%80%E6%9C%AF%E6%BC%94%E8%BF%9B%E4%BA%86%E8%A7%A3-web-%E5%B8%83%E5%B1%80%E5%8F%91%E5%B1%95%E5%8F%B2/</link>
        <pubDate>Wed, 01 Jan 2025 17:47:52 +0800</pubDate>
        
        <guid>https://blog.ther.cool/posts/%E7%8E%B0%E4%BB%A3-web-%E5%B8%83%E5%B1%80-1-web-%E5%B8%83%E5%B1%80%E6%8A%80%E6%9C%AF%E6%BC%94%E8%BF%9B%E4%BA%86%E8%A7%A3-web-%E5%B8%83%E5%B1%80%E5%8F%91%E5%B1%95%E5%8F%B2/</guid>
        <description>&lt;p&gt;自 1989 年至今，Web 已经有 30 多年的历程了。30 多年来，Web 也发生了翻天覆地的变化，Web 开发工作也衍生出多种工作岗位，而我们作为 Web 前端开发者的其中一员，更应该感谢 Web 给我们带来的机遇和未来。从今天开始，我们来一起探讨 Web 开发中一小部分相关的技能（或者说知识），即 &lt;strong&gt;Web&lt;/strong&gt; &lt;strong&gt;布局&lt;/strong&gt; 。&lt;/p&gt;
&lt;h2 id=&#34;web-布局演变过程&#34;&gt;Web 布局演变过程&lt;/h2&gt;
&lt;p&gt;多年以来，Web 布局已经改变了互联网。那么， Web 布局是从什么时候开始的，又是如何走到今天的呢？我们将深入研究 Web 布局的历史和演变，看看几十年来 Web 布局最佳实践是如何变化的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Web 布局的历史关键点的可以通过《&lt;a class=&#34;link&#34; href=&#34;https://www.webdesignmuseum.org/web-design-history&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Web Design History Timeline&lt;/a&gt;》来了解！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;自1991 年 Berners-Lee 创建出世界上首个 Web 页面到如今天，Web 布局主要可以分为六个关键点：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/7iFVvw8Bnmx3YSK.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;正如上图所示，每个关键点都有新的布局技术出现，也正因如此，Web 布局也随着技术的变革变得越来越灵活，越来越强大。&lt;strong&gt;其主要表现在，Web&lt;/strong&gt; &lt;strong&gt;布局的适配性更强、美感更强、用户体验更好等等。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;除此之外，我们还可以换过一个角度来看。因为不同时代，Web布局（设计）有着自己不同的定义：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/upiFBYKyrsMHTAh.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;在 Web 布局（或者说 Web 设计）中，每个版本有着自己清晰的定义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Web 设计 1.0（Web Design 1.0）是“&lt;strong&gt;一维的&lt;/strong&gt; ”：设计元素大多是按顺序排列的（按文档流的自然顺序排列） ；&lt;/li&gt;
&lt;/ul&gt;
&lt;!-- raw HTML omitted --&gt;
&lt;ul&gt;
&lt;li&gt;Web 设计 2.0（Web Design 2.0）是“&lt;strong&gt;二维的&lt;/strong&gt; ”：单元格中有放置元素的网格，具有更多的自由性 ；&lt;/li&gt;
&lt;/ul&gt;
&lt;!-- raw HTML omitted --&gt;
&lt;ul&gt;
&lt;li&gt;Web 设计 3.0（Web Design 3.0）是一个“&lt;strong&gt;新的维度&lt;/strong&gt; ”：它可以像平面设计工具一样，自由地定位元素、重叠，为网页设计开辟了新的前景，也将开启新的 Web 页面设计时代 。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;注意，这里所说的“二维”布局仅是坐标轴上来描述的，它和后面要介绍的 CSS Grid 布局中提到的二维布局概念是不一样的！到目前为止，&lt;strong&gt;只有&lt;/strong&gt; &lt;strong&gt;CSS Grid 布局才是二维布局！&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;刚才我们也提到过了，Web 布局的演进有六个关键点，这样划分是因为，在这个时候有新的 Web 布局技术出现，使用不同的布局技术，对 Web 布局也有着不同的称谓。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/mv8eoWaxHAi3tQK.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;有了这个基础，接下来我们将围绕着 Web 布局这六个关键点来展开。和大家一起探讨每个关键点中采用的主流 Web 布局技术。&lt;/p&gt;
&lt;h2 id=&#34;无任何布局模式&#34;&gt;无任何布局模式&lt;/h2&gt;
&lt;p&gt;互联网上的第一个网站是由 Tim Berners-Lee 创建的，他在瑞士研究中心 &lt;a class=&#34;link&#34; href=&#34;http://info.cern.ch/hypertext/WWW/TheProject.html&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;CERN&lt;/a&gt;（欧洲核研究组织）设计了该&lt;a class=&#34;link&#34; href=&#34;http://info.cern.ch/hypertext/WWW/TheProject.html&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;网站&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/rc8NDv9BPZlqXxi.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;Tim Berners-Lee 不仅被誉为第一位 Web 设计师，他还创建了超文本标记语言 (HTML 规范)，并使用该语言对 CERN 网站进行了编码。他还编写了第一个 Web 服务器规范 (HTTP) 和 Web 浏览器。他将继续创建&lt;a class=&#34;link&#34; href=&#34;https://www.w3.org/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;万维网联盟&lt;/a&gt;，该联盟至今仍负责监督 Web 标准的开发。&lt;/p&gt;
&lt;p&gt;Tim Berners-Lee 对漂亮的格式不太感兴趣。出于这个原因，HTML 的原始规范不包含任何真正能够准确控制页面布局的能力。&lt;/p&gt;
&lt;p&gt;也就是说，在早期的 Web 设计时代，Web 页面上的信息（元素）通常按照正常的流顺序（出现在 HTML 文档的源顺序）来组织。它没有色块、没有图像、没有图形，只有文字。与我们今天所认为的“网页设计”相去甚远。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;这个阶段（大约1991年~1994年）没有任何&lt;/strong&gt; &lt;strong&gt;Web&lt;/strong&gt; &lt;strong&gt;技术可言，他仅仅依靠&lt;/strong&gt; &lt;strong&gt;HTML&lt;/strong&gt; &lt;strong&gt;文档的顺序来组织和展示&lt;/strong&gt; &lt;strong&gt;Web&lt;/strong&gt; &lt;strong&gt;页面信息&lt;/strong&gt; ！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在这个阶段只有 &lt;strong&gt;展示性的&lt;/strong&gt; &lt;strong&gt;HTML&lt;/strong&gt; &lt;strong&gt;标签&lt;/strong&gt; 和 &lt;strong&gt;单像素的&lt;/strong&gt; &lt;strong&gt;GIF&lt;/strong&gt; &lt;strong&gt;技巧&lt;/strong&gt; 。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;h3 id=&#34;展示性的-html-标签&#34;&gt;&lt;strong&gt;展示性的&lt;/strong&gt; &lt;strong&gt;HTML&lt;/strong&gt; &lt;strong&gt;标签&lt;/strong&gt;&lt;/h3&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;早期的 HTML 规范仅提供了 &lt;code&gt;18&lt;/code&gt; 个 HTML 标签的描述：&lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;nextid&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;isindex&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;plaintext&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;listing&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt;～&lt;code&gt;&amp;lt;h6&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;address&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;hp1&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;hp2&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;dl&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;dt&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;dd&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;ul&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;li&amp;gt;&lt;/code&gt;,&lt;code&gt;&amp;lt;menu&amp;gt;&lt;/code&gt;和&lt;code&gt;&amp;lt;dir&amp;gt;&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在这同一时间，主流浏览器制造商（Netscape 和 Microsoft）添加一些标签，允许 Web 开发人员使用一些具有“展示性”的 HTML 标签来控制页面外观，例如粗体的&lt;code&gt;&amp;lt;b&amp;gt;&lt;/code&gt;和斜体的&lt;code&gt;&amp;lt;i&amp;gt;&lt;/code&gt;。有些标签还允许更改字号&lt;code&gt;&amp;lt;font size=&amp;quot;+2&amp;quot;&amp;gt;&lt;/code&gt;、设置字体&lt;code&gt;&amp;lt;font face=&amp;quot;verdana&amp;quot;&amp;gt;&lt;/code&gt;以及文本颜色&lt;code&gt;&amp;lt;font color=&amp;quot;red&amp;quot;&amp;gt;&lt;/code&gt;和背景颜色&lt;code&gt;&amp;lt;body bgcolor=&amp;quot;blue&amp;quot;&amp;gt;&lt;/code&gt;。甚至某种程度的图像对齐也可以通过&lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt;标签的属性来完成，例如&lt;code&gt;&amp;lt;img src=&amp;quot;logo.gif&amp;quot; align=&amp;quot;center&amp;quot; /&amp;gt;&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;只不过，这种方式很快成为 Web 开发人员维护页面的一个巨大问题，必须在站点的多个页面上的多个实例中进行更改。此外，Web 的创始人，例如 Tim Berners-Lee，曾设想 HTML 仅用于定义文档的结构，而不是用于控制展示。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意，&lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt;标签大约在 1995 年才进入 HTML 规范的！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol start=&#34;2&#34;&gt;
&lt;li&gt;
&lt;h3 id=&#34;单一像素的-gif-技巧&#34;&gt;&lt;strong&gt;单一像素的 GIF 技巧&lt;/strong&gt;&lt;/h3&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;虽然首个 Web 页面仅仅是文本和超链接的展示，但在第二年（1992年）开始，图片就可以通过&lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt;标签在 Web 页面中呈现。下图是世界上首先被运用于 Web 页面的图片之一。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/l5ELgC6jbZI1VRK.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;不过在当时，Web 开发者更喜欢使用单像素透明图片（GIF）来设置元素之间的间距。作为透明图像，该像素后面的任何内容，例如背景图像或颜色，都会显示出来。&lt;/p&gt;
&lt;p&gt;单像素图像下载速度很快，因为它是不可见的（透明的），设计师可以通过添加间距属性（例如 &lt;code&gt;&amp;lt;img src=&amp;quot;smiley.gif&amp;quot; hspace=&amp;quot;75&amp;quot; &amp;gt;&lt;/code&gt; 或&lt;code&gt;width&lt;/code&gt;或&lt;code&gt;height&lt;/code&gt;）来简单地改变水平和（或）垂直间距，例如 &lt;code&gt;&amp;lt;img src=&amp;quot;single.gif&amp;quot; width=&amp;quot;150&amp;quot; &amp;gt;&lt;/code&gt; 以创建所需的任意数量的空白空间。&lt;/p&gt;
&lt;h2 id=&#34;表格布局模式&#34;&gt;表格布局模式&lt;/h2&gt;
&lt;p&gt;互联网的发展是快速的，仅仅几年之后，HTML 2.0 （大约在 1995 年）就新增了&lt;strong&gt;图片&lt;/strong&gt; （&lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt;）、&lt;strong&gt;表单&lt;/strong&gt; （&lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt;）和 &lt;strong&gt;表格&lt;/strong&gt; （&lt;code&gt;&amp;lt;table&amp;gt;&lt;/code&gt;）等标签。这为 Web 开发人员在页面组织方面提供了更多的创造性自由。&lt;/p&gt;
&lt;p&gt;就在 HTML 2.0 表布之后的第二年（1996年），用于展示数据的表格就被用于 Web 的布局上，而且越来越受欢迎，也就在这个时候，&lt;strong&gt;表格布局模式&lt;/strong&gt; 就随之开启了。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;&amp;lt;table&amp;gt;
    &amp;lt;tr&amp;gt;
        &amp;lt;td colspan=&amp;#34;2&amp;#34;&amp;gt;顶栏&amp;lt;/td&amp;gt;
    &amp;lt;/tr&amp;gt;
    &amp;lt;tr&amp;gt;
        &amp;lt;td&amp;gt;左列&amp;lt;/td&amp;gt;
        &amp;lt;td&amp;gt;右列&amp;lt;/td&amp;gt;
    &amp;lt;/tr&amp;gt;
&amp;lt;/table&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;效果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/f29oVqLrwNjSJQP.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;早期的 Netscape 站点就是使用表格进行布局的：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/aMeBlWimX58d6wD.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;直到如今，在互联网上依旧能看到使用表格布局的 Web 页面！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;尽管表格（&lt;code&gt;&amp;lt;table&amp;gt;&lt;/code&gt;）的设计初衷是用于数据展示，而不是为 Web 布局而设计的，但表格在 2000 年代成为 Web 布局的“首选”方案。虽然使用表格来对 Web 布局，可以让 Web 页面以一定的结构性来呈现页面信息，但对于 Web 开发人员来说依旧是痛苦的，而且对于页面的性能也是致命的。&lt;/p&gt;
&lt;p&gt;虽然表格布局有着天生的不足，但它对于 Web 的设计和开发的演变有着革命性的影响，它将彻底改变 Web 设计的历史！即有 &lt;strong&gt;Web&lt;/strong&gt; &lt;strong&gt;布局&lt;/strong&gt;这样的一个概念！&lt;/p&gt;
&lt;p&gt;大约在使用表格进行布局的同时，HTML 框架标签（&lt;code&gt;&amp;lt;frameset&amp;gt;&lt;/code&gt;）的使用也很流行。使用&lt;code&gt;&amp;lt;frameset&amp;gt;&lt;/code&gt;标签，HTML 文档可以指定页面区域，使用&lt;code&gt;&amp;lt;frame src&amp;gt;&lt;/code&gt;标签可以包含其他 HTML 文件（&lt;code&gt;.html&lt;/code&gt;）。框架集甚至可以嵌套以获得更好的控制。&lt;/p&gt;
&lt;p&gt;下面的示例显示了五个单独的页面如何包含在主&lt;code&gt;&amp;lt;frameset&amp;gt;&lt;/code&gt;标记中。&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;&amp;lt;frameset cols=&amp;#34;30%,*&amp;#34;&amp;gt;
    &amp;lt;frameset rows=&amp;#34;40%,*&amp;#34;&amp;gt;
        &amp;lt;frame src=&amp;#34;frame1.html&amp;#34;&amp;gt;
        &amp;lt;frame src=&amp;#34;frame2.html&amp;#34;&amp;gt;
    &amp;lt;/frameset&amp;gt;
    &amp;lt;frameset rows=&amp;#34;33%,33%,*&amp;#34;&amp;gt;
        &amp;lt;frame src=&amp;#34;frame3.html&amp;#34;&amp;gt;
        &amp;lt;frame src=&amp;#34;frame4.html&amp;#34;&amp;gt;
        &amp;lt;frame src=&amp;#34;frame5.html&amp;#34;&amp;gt;
    &amp;lt;/frameset&amp;gt;
&amp;lt;/frameset&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;大致效果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/usBnz9UfFZQxY3t.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;当然，这个系统意味着开发人员必须为一个布局管理多个 HTML 文件。由于其他原因，框架也存在问题。比如页面添加书签效果不佳、页面超链接会令人常感到困惑，而且它们对搜索引擎也不友好。&lt;/p&gt;
&lt;p&gt;随着设计和开发工具变得越来越复杂，Web 用户开始对他们的在线体验寄予更多的期望。因此，苹果公司（Apple）在其计算机系统会议上创造了 &lt;strong&gt;“用户体验”&lt;/strong&gt; 一词。&lt;/p&gt;
&lt;p&gt;在 90 年代末期，浏览器大战中的大玩家（网景和微软）聚在一起对 HTML 发起了一场新的变革，即 &lt;strong&gt;逐步淘汰（弃用）展示性的&lt;/strong&gt; &lt;strong&gt;HTML&lt;/strong&gt; &lt;strong&gt;标签&lt;/strong&gt; 。同时，&lt;strong&gt;与&lt;/strong&gt; &lt;strong&gt;HTML&lt;/strong&gt; &lt;strong&gt;结构分离的级联样式表&lt;/strong&gt; 概念随之到来，并且得到快速的推进。&lt;/p&gt;
&lt;p&gt;1996 年，层叠样式表（CSS）面世，允许我们将 Web 页面的结构（HTML）和样式信息分开。样式规则可以保存在单独的文档（外部样式表，即&lt;code&gt;.css&lt;/code&gt;文件）中，样式表（CSS）可以用来设置 HTML 标签元素的展示信息，比如颜色、布局和排版等。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;HTML&lt;/strong&gt; &lt;strong&gt;和&lt;/strong&gt; &lt;strong&gt;CSS&lt;/strong&gt; &lt;strong&gt;是一组强大的组合，也将&lt;/strong&gt; &lt;strong&gt;Web&lt;/strong&gt; &lt;strong&gt;设计和开发推入到新的一个时代&lt;/strong&gt; 。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;与此同时，Web 动画紧随其后，Macromedia Flash 1.0 席卷了互联网。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/pEXVatDsINYrQvi.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;但它并非没有缺点。动画只有在网站配备了 Flash 扩展插件时才能运行，否则动画会显示为空白。这给完全基于 Flash 构建的网站带来了严重的问题。此外，Web 动画的开发需要繁重的工作，而且页面加载速度较慢。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;CSS&lt;/strong&gt; &lt;strong&gt;的诞生，让&lt;/strong&gt; &lt;strong&gt;Web&lt;/strong&gt; &lt;strong&gt;逐渐告别表格布局模式，开始迎来新的布局模式&lt;/strong&gt; ！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;浮动定位和框架布局模式&#34;&gt;浮动、定位和框架布局模式&lt;/h2&gt;
&lt;p&gt;自 1996 年发布了第一个&lt;a class=&#34;link&#34; href=&#34;https://www.w3.org/TR/REC-CSS1/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt; CSS 建议&lt;/a&gt; 之后，1998 年就发布了 CSS 第二版本（CSS 2.0），该版本提供了新的 Web 布局特性，比如定位（&lt;code&gt;position&lt;/code&gt; ）。与此同时，随着 CSS 的到来，Web 页面才真正有了“美感”这么一说：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/MkQCrT6hiUvzAFE.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;也是在这个时候（大约是 90 年代末、20 世纪初），表格布局开始用于其初衷（数据展示），随之在 Web 布局中涌现出很多新的布局模式。比如&lt;strong&gt;浮动布局&lt;/strong&gt; 、&lt;strong&gt;定位布局&lt;/strong&gt; 和 &lt;strong&gt;框架布局&lt;/strong&gt; 等。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里所说的框架布局是指 CSS Frameworks，而不是 HTML 中的框架标签！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;h3 id=&#34;浮动布局&#34;&gt;浮动布局&lt;/h3&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;浮动布局指的是使用 CSS 的浮动（&lt;code&gt;float&lt;/code&gt;属性）特性来构建 Web 页面。它和 HTML 的表格（&lt;code&gt;&amp;lt;table&amp;gt;&lt;/code&gt;）标签有点类似，它的初衷也不是用于 Web 布局的，是用来对 Web 进行排版的。即，&lt;strong&gt;用来处理文本围绕图片（或某个元素）的一种排版方式&lt;/strong&gt; ，就好比 Word 这样的排版软件中的排版方式，如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/eyjfOLcURuMAtHK.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;只不过，广大的 CSSer 发挥其无穷的智慧，硬是将其用于 Web 的布局中，而且这种布局方式一度成为一种主流的布局方式，并且持续了很多年，甚至直到今天，还有不少的同学在使用浮动来布局。直到 Flexbox 布局的出现和移动端的兴起，浮动布局才慢慢的被其取替。&lt;/p&gt;
&lt;p&gt;在 CSS 的布局模式当中，浮动布局经历的时期是最长的（至少到目前还没有什么方式超过它），持续了十多年的历史。在这个过程中，也积累了很多不同的布局方法。在早期，Web 开发人员主要采用 &lt;strong&gt;固定宽度&lt;/strong&gt; 和 &lt;strong&gt;流式布局&lt;/strong&gt; 两种布局方案来实现 Web 页面的布局。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/iuh7enZ2yrsRVM8.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;在这个时期也演变出很多经典的布局。其中要属“&lt;strong&gt;圣杯&lt;/strong&gt; ”和“&lt;strong&gt;双飞翼&lt;/strong&gt; ”两者最为经典。这两种方法实现的都是以三列布局为主，而且两边的宽度是固定的，中间列是自适应，它们实现的效果是一样的，只是实现的思路不同。&lt;/p&gt;
&lt;ol start=&#34;2&#34;&gt;
&lt;li&gt;
&lt;h3 id=&#34;定位布局&#34;&gt;定位布局&lt;/h3&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;随着 CSS 的定位（&lt;code&gt;position&lt;/code&gt;）属性的出现，Web 布局除了浮动布局之外，又新增了 &lt;strong&gt;定位布局&lt;/strong&gt; 。这种方式的布局能让你快速达到想要的布局效果。当然也有很多同学直接尝试采用 PSD2HTML 这样的工具，直接将设计图转换成 Web 页面。虽然这种方式能快速实现 Web 布局的效果，但也受到很多的局限性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;需要明确指定元素的大小 ；&lt;/li&gt;
&lt;/ul&gt;
&lt;!-- raw HTML omitted --&gt;
&lt;ul&gt;
&lt;li&gt;需要明确计算元素位置坐标 ；&lt;/li&gt;
&lt;/ul&gt;
&lt;!-- raw HTML omitted --&gt;
&lt;ul&gt;
&lt;li&gt;难于维护 。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;或许其中还有很多其他不利的因素。因此，这样的布局也不算是一种好的布局模式，但对于不太懂 CSS 的同学而言，这是一种简单易懂的布局。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;早期使用 PSD2HTML 的工具或软件（比如 Photoshop 或者 Firework 制图软件的切处导出的页面），基本上采用的都是定位布局。如今也有很多类似的工具，比如一些 AI 智能还图的运用，从设计稿中导出来的 HTML 和 CSS，不全是采用的定位布局。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol start=&#34;3&#34;&gt;
&lt;li&gt;
&lt;h3 id=&#34;框架布局&#34;&gt;框架布局&lt;/h3&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;这里所说的框架布局，指的是 CSS Frameworks（CSS 框架），比如 Bootstrap！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;随着这么多年的发展，现在大量的 Web 设计都是基于网格布局。虽然人们通常注意不到它，但事实上杂乱无章的布局时代确实已经过去了，现在是整齐结构化的天下。无论从理论、美学和整齐来说，这样的布局都很好平衡。网格结构是所有现代网站的基础，它最终总能给用户完美无暇的设计。&lt;/p&gt;
&lt;p&gt;对于网格系统，其也经历了一个漫长的演变。表格布局虽然痛苦，但可以说表格是网格系统布局的最初模型。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/xRwlhtPBpqeCTdc.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;正因为有表格布局的存在，才有了后面的 CSS 网格系统，不管是早期基于浮动完成的网格系统，还是后期依赖于 Flexbox 完成的网格系统。当然，你可能会说，网格系统的鼻祖不是 &lt;a class=&#34;link&#34; href=&#34;http://960.gs/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;960gs&lt;/a&gt;？&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/wyBPOzF2xQvUheE.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;虽然 960gs 是最早出现的网格系统（基于浮动布局），但其网格的思路是来源于表格的。因为表格具有明显的栅格风格，只不过是使用其它的布局方式，快速模拟了表格的风格，甚至是嵌套表格的风格。加上网格系统让 Web 的设计变得结构整齐、布局平衡等，受到众多设计师的青眯，也让 Web 开发者更易实现，并且可以依据此思路制定一套系统。基于此系统，可以快速完成 Web 布局，并且达到较好的效果，甚至还可以基于此系统制作工具，通过工具帮助大家快速完成布局。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;网格框架中的网格系统，依旧是使用浮动或者后面出现的 Flexbox 构建的布局系统，它和原生的 CSS Grid 模块有着本质的区别！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;响应式-web-布局rwd&#34;&gt;响应式 Web 布局（RWD）&lt;/h2&gt;
&lt;p&gt;直到 20 年代初，Web 网站都是为桌面浏览器创建的。虽然在这个阶段，Web 开发者会采用不同的布局技术，比如流式布局（也常称&lt;strong&gt;百分比布局&lt;/strong&gt; ）让 Web 页面适配浏览器不同尺寸，但还未进入真正的 &lt;strong&gt;响应式&lt;/strong&gt; &lt;strong&gt;Web&lt;/strong&gt; &lt;strong&gt;设计&lt;/strong&gt; 时代。&lt;/p&gt;
&lt;p&gt;哪怕是&lt;a class=&#34;link&#34; href=&#34;https://twitter.com/themaninblue&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt; Cameron Adams（@themaninblue）&lt;/a&gt; 在他的博文《&lt;a class=&#34;link&#34; href=&#34;https://www.themaninblue.com/writing/perspective/2004/09/21/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Resolution dependent layout&lt;/a&gt;》提出了&lt;strong&gt;基于屏幕分辨率来动态构建&lt;/strong&gt; &lt;strong&gt;Web&lt;/strong&gt; &lt;strong&gt;布局&lt;/strong&gt; （ &lt;strong&gt;使用&lt;/strong&gt; &lt;strong&gt;JavaScript&lt;/strong&gt; &lt;strong&gt;根据浏览器窗口大小加载不同&lt;/strong&gt; &lt;strong&gt;CSS&lt;/strong&gt; &lt;strong&gt;文件&lt;/strong&gt; ），也称不上是响应式 Web 布局。&lt;/p&gt;
&lt;p&gt;虽然这种依赖动态分辨率布局的方案，可以在不同的分辨率下提供更佳的体验，但随着 2005 年 08 月 10 日 Opera 软件公司推出 Opera Mini ，和 2007 年 01 月 09 日第一台 iPhone 手机的出现，市场上不同品牌，不同分辨率的移动端以及品牌商自己的 Web 浏览器越来越多。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/914RoNBkuLPC2Ti.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;在这种环境之下，基于动态分辨率加载不同的样式表已不太现实，Web 开发者不得不想出其他的方案来适应不同的屏幕尺寸。&lt;/p&gt;
&lt;p&gt;在很长一段时间，甚至到今日，为了适应不同屏幕的尺寸适配，为移动端单独创建一个网站，即 移动子域名网站。比如 Facebook 的桌面版本和移动端版本，采用两个不同的域名来访问：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/YJlAKxC7eFPkN68.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;如此一来，开发人员要开发两个版本，相应的工作量就更大了，特别对于要快速响应和试错的 Web 应用来说，难度变得更大。&lt;/p&gt;
&lt;p&gt;Web 开发人员为了能改善这种现象，在 2010 年的时候，&lt;a class=&#34;link&#34; href=&#34;https://twitter.com/beep&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Ethan Marcotte（@beep）&lt;/a&gt; 基于 &lt;a class=&#34;link&#34; href=&#34;https://twitter.com/johnallsopp&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;John Allsopp（@johnallsopp）&lt;/a&gt; 的 《&lt;a class=&#34;link&#34; href=&#34;https://alistapart.com/article/dao/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;网页设计的道（A Dao of Web Design）&lt;/a&gt;》，提出响应式 Web 设计思路（《&lt;a class=&#34;link&#34; href=&#34;https://alistapart.com/article/responsive-web-design/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Responsive Web Design&lt;/a&gt;》）。从此&lt;strong&gt;响应式&lt;/strong&gt; &lt;strong&gt;Web&lt;/strong&gt; &lt;strong&gt;设计&lt;/strong&gt; （Responsive Web Design，简称 &lt;strong&gt;RWD&lt;/strong&gt; ）的身影就出现在了公众面前。&lt;/p&gt;
&lt;p&gt;Ethan Marcotte（@beep） 在《&lt;a class=&#34;link&#34; href=&#34;https://alistapart.com/article/responsive-web-design/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Responsive Web Design&lt;/a&gt;》中提到：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;响应式这个词源自于建筑学领域，原本指的是建筑物本身会“响应”实际的使用情况，来自我调整。在Web开发领域，“响应式”的意思就变成了，我们开发的 Web 页面会“响应”用户的设备尺寸而自动调整布局。在这篇文章中提到过，我们可以基于 流体网格（Fluid Grids）、灵活的图片（Flexible Images）和媒体查询（Media Queries） 三种技术来构建一个响应式 Web 网站或 Web 应用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;另外，Ethan Marcotte（@beep） 构建了第一个具有响应式的 Web 网页，可以说是响应式 Web 设计经典案例之一（只可惜现在打不开了）：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/QZSjEDGHC72MO4T.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;h2 id=&#34;内在-web-设计iwd&#34;&gt;内在 Web 设计（IWD）&lt;/h2&gt;
&lt;p&gt;内在的 Web 设计这个概念，是在 2018 年由 &lt;a class=&#34;link&#34; href=&#34;http://twitter.com/jensimmons&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Jen Simmons&lt;/a&gt; 提出的，这个概念是 Web 设计中的一个新概念！&lt;a class=&#34;link&#34; href=&#34;http://aneventapart.com/news/post/designing-intrinsic-layouts-aea-video&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;她在 2018 年的 An Event Apart 大会上分享了该话题&lt;/a&gt;（该话题的 &lt;a class=&#34;link&#34; href=&#34;http://talks.jensimmons.com/15TjNW#sGTkg4c&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;PPT请点击这里获取&lt;/a&gt;），她分享时曾表示：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我们现在正处于 Web 设计发展的另一个转折点，创意比增长更重要。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/inJVkw1hT6Ptb5g.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;Jen 说，“内在 Web 设计”（&amp;ldquo;Intrinsic Web Design&amp;rdquo;）可能是 Web 设计历史上的新关键点，一切都在改变，在以技术和经验为基础，希望以最少的代码量来实现复杂的 Web 设计，或者说，&lt;strong&gt;Web&lt;/strong&gt; &lt;strong&gt;开发者希望在用最少的代码和复杂&lt;/strong&gt; &lt;strong&gt;Web&lt;/strong&gt; &lt;strong&gt;设计之间取得完美的平衡&lt;/strong&gt; 。她意识到，以“内在 Web 设计”将可以把这种平衡趋向于完美。那么什么是内在 Web 设计？&lt;/p&gt;
&lt;h3 id=&#34;什么是内在-web-设计&#34;&gt;什么是内在 Web 设计？&lt;/h3&gt;
&lt;p&gt;自从 Web 诞生以来，Web 开发者一直在使用大量的技巧来完成所有与布局有关的事情。无论是使用浮动（&lt;code&gt;float&lt;/code&gt;）还是引用外部第三方 CSS 框架（CSS Frameworks）和库（比如Bootstrap）将内容放置在 Web 页面上想要的任何位置（即布局），几乎都有一些 Hack 的身影存在！&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;浮动的初衷是用于排版的，只不过在那个年代，Web 开发者利用其特性来构建 Web 的布局，而且运用于 Web 布局很多年。其中大多数第三方的 CSS 框架和库都是采用浮动来完成 Web 的布局！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;然而，像 Flexbox 和 Grid 这样的 CSS 模块的出现，使我们能够正确构建我们想要的 Web 布局（设计），而且没有任何 Hack 代码、第三方 CSS 框架或 JavaScript 脚本（指完成 Web 布局方面）。从本质上讲：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;能够以最少的 Hack 和技巧构建任何你想要的 Web 布局（或设计）！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;也就是说，与将设计人员和开发人员都限制在 Web 的“预定义规则”中不同，内在 Web 设计（Intrinsic Web Design）使他们能够灵活地将传统的、久经考验的 Web 布局技术和现代布局方法和工具（比如 Flexbox、Grid等）结合起来，以便根据 Web 的内在内容创造独特的布局。&lt;/p&gt;
&lt;p&gt;鼓励设计人员和开发人员将内容放在首位，并允许他们利用所有可用的布局技术和方法，以最佳方式在 Web 页面上显示内容，同时保持代码干净和更高效。用最简单的术语来说：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;内在 Web 设计（Intrinsic Web Design）不是内容以设计为导向（Content Design-Driven），而是只专注于让设计受内容驱动（Design Content-Driven）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;通俗地说，直到现在，大多数的 Web 设计和布局都是以设计为导向，因为在构建 Web 布局时，都是基于设计师提供的设计稿（模板）来完成。因此，你不难发现，现存于线上的很多 Web 页面上的元素大小（尺寸）基本上都设置了固定的尺寸，而且这些尺寸是根据最初设计师提供的稿子定义的。&lt;/p&gt;
&lt;p&gt;事实上呢？Web 的数据是动态的，服务端吐出的数据与最初设计稿内容有可能并不匹配（有多，也有少），此时呈现给用户的 Web 页面并不是最佳的（有可能很多空白空间未利用，有可能内容溢出容器，打破布局）。反之，Web 的内在尺寸设计就不同，在 Web 布局时，页面元素大小是根据真实内容（服务端吐出的数据）来决定的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;内在&lt;/strong&gt; &lt;strong&gt;Web&lt;/strong&gt; &lt;strong&gt;设计是&lt;/strong&gt; &lt;strong&gt;Web&lt;/strong&gt; &lt;strong&gt;布局的新时代，&lt;/strong&gt;* &lt;em&gt;它&lt;/em&gt;***超越了响应式设计。我们正在使用** &lt;strong&gt;Web&lt;/strong&gt; &lt;strong&gt;本身作为一种媒介（设计受内容驱动），而不是试图模拟印刷设计（内容以设计为导向）。内在&lt;/strong&gt; &lt;strong&gt;Web&lt;/strong&gt; &lt;strong&gt;设计更为重要的是，不仅上下文的流畅，适应性高，还能够在&lt;/strong&gt; &lt;strong&gt;Web&lt;/strong&gt; &lt;strong&gt;布局和当前的&lt;/strong&gt; &lt;strong&gt;CSS&lt;/strong&gt; &lt;strong&gt;功能集上发挥真正的创造力&lt;/strong&gt; 。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;组件驱动式-web-设计cdwd&#34;&gt;组件驱动式 Web 设计（CDWD）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;组件驱动式 Web 设计被称为下一代响应式 Web 设计!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Web 生态是不断向前发展的。在这十多年来，CSS 也发生了巨大的变化，新增了很多新的特性，近两年尤其如此。这些变化，对于响应式 Web 设计的开发也有较大的改变。就在&lt;a class=&#34;link&#34; href=&#34;https://io.google/2021/session/a1760fa3-879a-4e98-a616-994ca8d3aab5/?lng=zh-CN&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;2021 年 Google 的 I/O 大会上&lt;/a&gt;，Una Kravets（@Una）提出新的响应式：组件驱动式 Web 设计（&lt;strong&gt;Component-Driven Web Design&lt;/strong&gt; ，即 &lt;strong&gt;CDWD&lt;/strong&gt; ）。&lt;/p&gt;
&lt;p&gt;简单地说， 组件驱动 Web 设计（Component-Driven Web Design），基于组件驱动的开发，即 &lt;strong&gt;CSS&lt;/strong&gt; &lt;strong&gt;新增的特性将直接基于组件而不是基于页面注入样式响应能力&lt;/strong&gt; 。用下图来简单概述组件驱动式 Web 设计：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/xhz7Feft8WgsCD5.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;简要地说，我们使用 CSS 新特性可以做到：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;响应用户的需求；&lt;/li&gt;
&lt;/ul&gt;
&lt;!-- raw HTML omitted --&gt;
&lt;ul&gt;
&lt;li&gt;响应容器的需求；&lt;/li&gt;
&lt;/ul&gt;
&lt;!-- raw HTML omitted --&gt;
&lt;ul&gt;
&lt;li&gt;响应外形的需求。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;而且我们不再仅限于基于媒体查询来构建页面级的布局，还可以基于容器查询构建组件组的布局。这对于以往的布局来说，是质的变化。&lt;/p&gt;
&lt;h2 id=&#34;2022-年及以后-css-布局技术&#34;&gt;2022 年及以后 CSS 布局技术&lt;/h2&gt;
&lt;p&gt;自从第一张 Web 页面诞生至今，Web 的布局已经经过了多次迭代：无布局 » 表格布局 » 浮动布局 » 框架布局 » 现代布局 » 未来布局：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/rzYDXO2J4jAHbxi.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;整个布局演变过程中，有不同的名词来定义布局（一般根据采用的布局技术来命名）：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/uF5MAyIZaJQmlth.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;其中浮动布局（主要是 CSS 的 &lt;code&gt;float&lt;/code&gt; 属性）技术曾也占据较长时间，在当初那个年代可以说是主流的布局技术，直到 Flexbox 的出现以及浏览器对 Flexbox 越来越完善时，浮动布局技术才被 Flexbox 布局技术替代下来。&lt;/p&gt;
&lt;p&gt;虽然时下 Flexbox 布局技术是一个主流布局技术，但并不代表着 CSS 的浮动（&lt;code&gt;float&lt;/code&gt;）就没有存在的必要了（有些布局效果还是离不开浮动的，比如不规则布局）。&lt;/p&gt;
&lt;p&gt;随着 CSS 技术不断向前发展，尤其是这几年，可以用 Web 布局的特性明显增多：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/sC8cYlIMw1FfmNz.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;正如上图所示，其中多列布局（Multi-column）和 Flexbox 已经是很成熟的技术，只不过多列布局（Multi-column）使用的较少，对于像 CSS 自定义属性、CSS 网格（它也很早就有了）、宽高比（&lt;code&gt;aspect-ratio&lt;/code&gt;）、CSS 比较函数（&lt;code&gt;min()&lt;/code&gt;、&lt;code&gt;max()&lt;/code&gt; 和 &lt;code&gt;clamp()&lt;/code&gt;）、CSS 逻辑属性、CSS书写模式 和 CSS 视窗单位，是近两年才得到主流浏览器支持，其他很多特性对于 Web 开发者来说是“只闻其名，未见其身”。&lt;/p&gt;
&lt;p&gt;换句话来说，时至今日，这些特性都可以用于 Web 布局当中，它们都是 Web 布局工具箱中的一员。在未来，我们还可以使用像子网格（&lt;code&gt;subgrid&lt;/code&gt;）、容器查询 和 父选择器 &lt;code&gt;:has()&lt;/code&gt; 等特性（这三个特性，已经得到部分主流浏览器的支持）。&lt;/p&gt;
&lt;p&gt;&lt;a class=&#34;link&#34; href=&#34;http://2021.stateofcss.com/en-US/opinions/#currently_missing_from_css_wins&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;如果你有关注过CSS相关的发展报告的话&lt;/a&gt;，你可能也知道，这几个 CSS 特性一直以来也是 CSSer 最为期待的三个特性，尤其是容器查询和父选择器：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2025/01/01/RjWnHJkrI9S7st4.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;也就是说，这些 CSS 特性已成为时下，或将成为 Web 布局的主流技术。&lt;/p&gt;
&lt;h2 id=&#34;小结&#34;&gt;小结&lt;/h2&gt;
&lt;p&gt;历史是不断向前的，技术也是如此。虽然 CSS 在众多同学眼里是一件轻松的事情，但事实并非如此。正如此文所介绍的 CSS 实现 Web 布局，这就不是一件轻松的事情，很多工作两、三年的同学，都不一定能对 Web 布局实现达到手到擒来。&lt;/p&gt;
&lt;p&gt;除此之外，在未来，Web 布局的模式将会越来越多，越来越强大，比如不久的将来，CSS Shapes 能帮助我们打破矩形的布局模式，CSS 的多列布局能让我们在 Web 中实现报纸排版的效果。或许还将会有其它的布局模式。&lt;/p&gt;
&lt;p&gt;如果想彻底掌握上面提到这些布局模式，那么你需要对 CSS 有一定的基础了解。有了这些相关的基础，你会更好理解其中的一些概念和相关理论。如果你是初次接触 CSS，或者说对 CSS 了解还不足够深，那也无妨，因为我们接下来的内容将会带领大家一起来探讨现代 Web 布局技术。比如 Flexbox、Grid、容器查询等！&lt;/p&gt;
</description>
        </item>
        <item>
        <title>Kafka高吞吐设计</title>
        <link>https://blog.ther.cool/posts/kafka%E9%AB%98%E5%90%9E%E5%90%90%E8%AE%BE%E8%AE%A1/</link>
        <pubDate>Sat, 29 Jul 2023 22:43:16 +0800</pubDate>
        
        <guid>https://blog.ther.cool/posts/kafka%E9%AB%98%E5%90%9E%E5%90%90%E8%AE%BE%E8%AE%A1/</guid>
        <description>&lt;p&gt;Kafka 采用了一系列的技术优化来保证高吞吐，这其中包括批量处理、压缩、零拷贝、磁盘顺序读写、页面缓存技术、Reactor 网络架构设计模式等。接下来主要从生产端、服务端和消费端三个方面来剖析和讨论。同时讨论一些高性能的设计方法，以及操作系统底层的工作模式，这些都有利于高效地设计出一个高吞吐的系统。&lt;/p&gt;
&lt;h2 id=&#34;生产端&#34;&gt;生产端&lt;/h2&gt;
&lt;p&gt;Kafka 高吞吐量的特性在生产端这里是怎么体现的呢？要想回答这个问题，首先得了解下生产端是如何发送消息的，这属于铺垫知识。下图详细描述了生产端发送消息的全部流程。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s0.lgstatic.com/i/image6/M00/51/01/CioPOWEDbJeAZ9G6AAhIbEVyoo0362.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;!-- raw HTML omitted --&gt;
&lt;p&gt;结合该图，我们可以看到发送一条消息需要经历 7 个步骤，这些步骤可以分为三大块，分别是 KafkaProducer 主线程、RecordAccumulator 缓存和 Sender 子线程。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;KafkaProducer 主线程&lt;/strong&gt;，主要负责创建信息，并调用拦截器、序列化器、分区器分别对消息进行拦截、序列化和路由分区，然后对消息进行压缩，把压缩过的消息放入 RecordAccumulator 缓存中。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;RecordAccumulator 缓存&lt;/strong&gt;，为每个分区创建了一个队列，这个队列是要发送到某个分区的消息集合。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Sender 子线程&lt;/strong&gt;，是真正发送消息的线程。满足一定条件时，KafkaProducer 主线程会激活 Sender 子线程。Sender 子线程从 RecordAccumulator 缓存中拿到要发送的消息，并把消息交给底层网络组件来发送。对于网络接收和网络发送的数据，网络组件会通过两个缓存集合来维护：completedReceives 是负责保存完成的网络接收的集合，completedSends 是负责保存完成的网络发送的集合。服务端成功响应返回给 Sender 子线程后，Sender 子线程就会删除 RecordAccumulator 缓存内已经发送成功的消息。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;介绍完生产端的这个架构设计后，接下来就从以下三点解释一下这个架构从哪些方面提升了消息的吞吐量。&lt;/p&gt;
&lt;h3 id=&#34;1-多线程异步的设计&#34;&gt;1. 多线程异步的设计&lt;/h3&gt;
&lt;p&gt;生产端在异步的设计上体现到了两个方面。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一个方面，KafkaProducer 主线程和 Sender 子线程各司其职，通过 RecordAccumulator 缓存交互数据。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;KafkaProducer 主线程有同步和异步两种发送方式，但是这两者底层的实现是相同的，都是通过 Sender 子线程异步发送消息实现的。不同的地方是同步场景下主线程会等待 Sender 子线程发送完消息再返回，而异步是不等待 Sender 子线程发送完消息就返回了。&lt;/p&gt;
&lt;p&gt;KafkaProducer 主线程发送消息时并不是真正的网络发送，而是将消息放入 RecordAccumulator 中缓存，然后主线程就从 send() 方法返回，之后 KafkaProducer 主线程会不断调用 send() 方法把消息缓存到 RecordAccumulator 中，而不去在意消息是否发送出去了。真正发送消息的是 Sender 子线程，Sender 子线程从 RecordAccumulator 缓存中取出消息，然后调用底层网络组件完成消息的发送。&lt;/p&gt;
&lt;p&gt;有的同学可能会有疑问：为什么不能把主线程和 Sender 子线程放到一个线程呢？一个线程里会有什么问题呢？&lt;/p&gt;
&lt;p&gt;生产端发送消息有两个过程：创建消息和网络发送消息。这两个过程都有可能出现阻塞，比如，消息的创建依赖远程数据库或缓存，如果网络不好，线程就会阻塞在消息创建上；而生产端和服务端的通信不好时，也会导致出现阻塞的问题。如果这两个过程放到一个线程里的话，那么其中有一个发送阻塞，就会影响另一个过程的执行。&lt;/p&gt;
&lt;p&gt;但是如果我们把创建消息交给主线程负责，发送消息交给子线程负责，这样这两个过程相互不影响，同时有缓存作为缓冲，很好地起到“削峰填谷”的作用。&lt;/p&gt;
&lt;p&gt;第&lt;strong&gt;二个方面，Sender 子线程和 Kafka 底层通信模块解耦。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Sender 子线程最终是调用 Kafka 底层通信模块实现消息的发送和接收的。&lt;/p&gt;
&lt;p&gt;我们知道 Java NIO 本质上是调用了 Linux 通信模块，Kafka 底层封装了 Java NIO 组件，特别是 org.apache.kafka.common.network.Selector（简称 KSelector）封装了 Java NIO 的 Selector 类，KSelector 在 Selector 的基础上增加了两个集合做缓冲，分别是 completedReceives 集合和 completedSends 集合，KSelector 发送成功和接收成功的消息都会放到这两个集合里。而 Sender 子线程通过 while(true) 循环不断地尝试从这两个集合获取消息，从而实现了这两个组件的解耦，道理也是一样，也是起到“削峰填谷”的作用，进而有利于高吞吐。&lt;/p&gt;
&lt;h3 id=&#34;2-在缓存中批量地获取数据并做到高效的空间利用&#34;&gt;2. 在缓存中批量地获取数据，并做到高效的空间利用&lt;/h3&gt;
&lt;p&gt;这一点与 RecordAccumulator 类的设计关系很大，RecordAccumulator 类的设计图如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s0.lgstatic.com/i/image6/M00/50/F9/Cgp9HWEDbK6AMBwCAAPPZPimeXE261.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;!-- raw HTML omitted --&gt;
&lt;p&gt;由图可以看到，在 RecordAccumulator 中有一个 CopyOnWriteMap 集合 batches。key 是主题分区，value 是 ProducerBatch 队列，每个分区都对应一个队列。队列中的元素是批次 ProducerBatch，消息就是封装在这些批次里进行缓存的。而消息发送的最小单位是 batch，也就是说一次消息发送可能不止一条消息，这样的设计大大减少了网络请求的次数，从而提升了网络读写的效率，进而提高了吞吐量。&lt;/p&gt;
&lt;p&gt;接下来我们再来分析下消息的发送时机和逻辑。代码在 RecordAccumulator.drain() 方法内，其源码和源码注释如下：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;//五个判断条件决定是否是能发送的node
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;boolean&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;sendable&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;full&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;||&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;expired&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;||&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;exhausted&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;||&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;closed&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;||&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;flushInProgress&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;//能发送且没有正在退避
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;sendable&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;!&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;backingOff&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;//如果是能发送就加入readyNodes集合
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;    &lt;span class=&#34;n&#34;&gt;readyNodes&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;add&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;leader&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;o&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kt&#34;&gt;long&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;timeLeftMs&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Math&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;max&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;timeToWaitMs&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;waitedTimeMs&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;//还剩多久：需要等待的时间-已经等待的时间
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;    &lt;span class=&#34;n&#34;&gt;nextReadyCheckDelayMs&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Math&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;min&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;timeLeftMs&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;nextReadyCheckDelayMs&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;o&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这里我重点解释下决定是否发送的布尔型变量 sendable 的判断逻辑：五个判断条件只要有一个满足就能发送消息。这五个条件可总结为如下。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;full：deque 是否大于 1，或 deque 的第一个 ProducerBatch 是否满了。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;expired：ProducerBatch 在 deque 里是否超时。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;exhausted：BufferPool 是否正在释放空间。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;closed：生产者是否准备正常关闭了。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;flushInProgress：是否在 flush 操作，这个 flush 是把暂存消息立即发送的标记。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;第一个判断条件是 deque 是否大于 1，或 deque 的第一个 ProducerBatch 是否满了，在 Broker 负载没满的情况下，deque 的第一个 ProducerBatch 是否满了是大部分情况下发送消息的时机。所以说，生产者发送消息并不是一条条发送的，而是一个一个 batch 发送的。&lt;/p&gt;
&lt;p&gt;接下来我们再分析下&lt;strong&gt;生产端高效的空间利用特性&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;缓存的空间分配是由 BufferPool 组件完成的，下面是其工作原理图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s0.lgstatic.com/i/image6/M00/51/02/CioPOWEDbMmAcpiVAALwOMYqWmQ225.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;!-- raw HTML omitted --&gt;
&lt;p&gt;整个 BufferPool 的大小默认为 32M，内部内存区域分为两块：固定大小内存块集合 free 和非池化缓存 nonPooledAvailableMemory。固定大小内存块默认大小为 16K。当 ProducerBatch 向 BufferPool 申请一个大小为 size 的内存块时，BufferPool 会根据 size 的大小判断由哪个内存区域分配内存块。&lt;/p&gt;
&lt;p&gt;当 ProducerBatch 的数据发送成功后，ProducerBatch 并不会销毁，而是继续留在集合 free 中，这样需要 ProducerBatch 的时候就直接从集合中拿出，就不用频繁地销毁和重建了。其实 ProducerBatch 的底层是 Java NIO ByteBuffer，ByteBuffer 的创建和销毁是很消耗 CPU 资源的，这样的设计实现了 ByteBuffer 的重用，从而大大减少了对资源的消耗。&lt;/p&gt;
&lt;h3 id=&#34;3-消息的压缩&#34;&gt;3. 消息的压缩&lt;/h3&gt;
&lt;p&gt;消息压缩是在业务主线程 KafkaProducer 完成的，消息的压缩大大减少了本地内存、网络通信和服务端存储的压力。&lt;/p&gt;
&lt;p&gt;目前主要有 4 种压缩算法，分别是 gzip、snappy、lz4 和 zstd。你可以根据生产环境实际情况来配置适合自己的压缩算法，评估一个压缩算法一般是从压缩解压的速度和压缩率两方面去权衡的。当机器 CPU 配置比较高而带宽比较低的时候，可以考虑压缩率高而压缩速度低的算法；相反，当机器 CPU 配置比较低而带宽比较高的时候，可以考虑压缩率低而解压速度比较高的算法。&lt;/p&gt;
&lt;p&gt;Kafka 生产端对于高吞吐的设计我就介绍到这里，接下来我继续介绍 Kafka 服务端针对高吞吐的设计特点。&lt;/p&gt;
&lt;h2 id=&#34;服务端&#34;&gt;服务端&lt;/h2&gt;
&lt;p&gt;服务端针对高吞吐有几个设计特点：网络层的 Reactor 设计模式、顺序写、页缓存和零拷贝。接下来我会按照顺序分别为你详细讲解。&lt;/p&gt;
&lt;h3 id=&#34;1-网络层的-reactor-设计模式&#34;&gt;1. 网络层的 Reactor 设计模式&lt;/h3&gt;
&lt;p&gt;网络层的设计图如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s0.lgstatic.com/i/image6/M00/51/02/CioPOWEDbNqARKWBAAJy6zlp8aI724.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;!-- raw HTML omitted --&gt;
&lt;p&gt;这里我就结合该图解释一下网络层的架构设计。&lt;/p&gt;
&lt;p&gt;整个服务端的网络架构分为 4 个层次：①Acceptor 线程构成的&lt;strong&gt;连接创建层&lt;/strong&gt;，负责创建和客户端的连接；②Processor 线程类构成的&lt;strong&gt;网络事件处理层&lt;/strong&gt;；③由 RequestChannel 构成的请求和响应的&lt;strong&gt;缓冲层&lt;/strong&gt;；④由 KafkaRequestHandler 和 KafkaApis 构成的真正的&lt;strong&gt;业务处理层&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这样的设计有什么优势呢？&lt;/p&gt;
&lt;p&gt;第一，我们先思考为什么要把 Acceptor 线程和 Processor 线程分开。如果不分开，网络读写的量很大势必造成大量线程阻塞，导致服务端对 OP_ACCEPT 事件响应不及时，进而连接失败。同时，如果服务端刚启动瞬时来了很多连接，大量的线程都去建立新的连接了，那么网络读写事件的处理就会慢下来，也会引起读写超时等问题。&lt;/p&gt;
&lt;p&gt;Acceptor 线程和 Processor 线程分为两层这样的设计让连接的创建和网络读写事件的处理分开，同时还可以配置 Processor 线程的数量，这样做不会被极端场景影响到整体的响应时间，同时也是符合 Reactor 设计模式的。（Reactor 模式又被称为反应器模式或应答者模式，是基于事件驱动的设计模式，如有需要你可以查阅相关的资料来学习，这里就不过多赘述了。）&lt;/p&gt;
&lt;p&gt;第二，Processor 线程解析完请求后并不是直接交给业务线程处理，而是放到 RequestChannel 的请求队列里，这样做避免在高并发场景下业务线程（即调用底层组件的线程）工作过于饱和而造成超时的情况出现。&lt;/p&gt;
&lt;p&gt;第三，KafkaRequestHandlerPool 线程池先消费 RequestChannel 类里的请求队列，然后通过调用 KafkaApis 实现对底层组件的调用。这样做既可以实现网络处理和调用底层组件的解耦，也可以根据实际请求，随时调整 KafkaRequestHandlerPool 线程池的线程数，调整调用底层组件的能力。&lt;/p&gt;
&lt;p&gt;第四，KafkaApis 类会把响应放入对应的 Processor 线程里的响应集合里，而不是直接让 Processor 把响应发送给客户端，这样做实现了业务线程和网络操作线程的解耦，避免了高并发时线程工作过于饱和而造成的延迟问题。&lt;/p&gt;
&lt;h3 id=&#34;2-顺序写&#34;&gt;2. 顺序写&lt;/h3&gt;
&lt;p&gt;Kafka 写日志文件的时候用的是追加消息的形式，只在文件尾部顺序写消息，同时在文件头部顺序读取消息。消息队列不涉及修改消息，所以不需要随机写。这样的设计即使用的是传统的磁盘，吞吐量也会很大。主要原因是操作系统对于顺序写和顺序读有优化，具体采用的是后写（对于写消息优化）和预读（对于读消息优化）。生产环境上经过测试，顺序写比随机写快 3 个数量级。&lt;/p&gt;
&lt;h3 id=&#34;3-页缓存&#34;&gt;3. 页缓存&lt;/h3&gt;
&lt;p&gt;页缓存简单说就是把缓存当磁盘用，这样就避免了频繁地读写磁盘。&lt;/p&gt;
&lt;p&gt;当一个进程要读取或写入磁盘文件的时候，系统会判断数据是否在内存中，如果在，就直接把内存中的数据返回给进程；如果不在，就读取磁盘文件，同时会多读一些连续的磁盘页放到内存中。这样下次再读取或写入时，系统会判断数据是否在内存中，只要是顺序地读写消息，命中率会很高的，大大减少了磁盘访问的次数，提高了服务端的吞吐量。&lt;/p&gt;
&lt;h3 id=&#34;4-零拷贝&#34;&gt;4. 零拷贝&lt;/h3&gt;
&lt;p&gt;这里我们以消费者读取消息为例，服务端要从磁盘拷贝数据然后网络发送，如果不采用零拷贝的话，会发生什么样的事情呢？如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s0.lgstatic.com/i/image6/M00/51/02/CioPOWEDbOuAD4z_AAE6h6G4b-o112.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;!-- raw HTML omitted --&gt;
&lt;p&gt;首先，应用程序调用 read() 方法时需要从用户态切换到内核态，将数据从磁盘上取出来保存到内核缓冲区中；然后，内核缓冲区中的数据传输到应用程序，此时 read() 方法调用结束，从内核态切换到用户态。之后，应用程序执行 send() 方法，需要从用户态切换到内核态，将数据传输给 Socket Buffer；最后，内核会将 Socket Buffer 中的数据发送到网卡，再发送到远程节点，此时 send() 方法结束，从内核态切换到用户态。&lt;/p&gt;
&lt;p&gt;可以看到，这个过程共涉及四次 CPU 上下文切换和四次数据复制，并且有两次复制操作是由 CPU 完成的，另外两次由 DMA 完成。在这个过程中，数据本身没有任何修改，仅仅是从磁盘复制到了网卡缓冲区中，于是会浪费大量的 CPU 周期。&lt;/p&gt;
&lt;p&gt;那采用零拷贝又会发生什么呢？如下过程图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s0.lgstatic.com/i/image6/M00/50/F9/Cgp9HWEDbPiAUHBIAAEMXenJgF4404.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;!-- raw HTML omitted --&gt;
&lt;p&gt;首先，应用程序调用 transferTo() 方法，从用户态转换为内核态，DMA 会将文件数据发送到内核缓冲区；然后，Socket Buffer 追加数据的描述信息；最后，DMA 将内核缓冲区的数据发送到网卡缓冲区，这样就完全解放了 CPU，实现了零拷贝。&lt;/p&gt;
&lt;p&gt;也就是说，所谓“零拷贝”是 CPU 不参与拷贝数据的工作，可以节省大量的 CPU 周期，同时减少了两次 CPU 在用户态和内核态的切换。这样大大减少了 CPU 的负载，从而提升了吞吐量。&lt;/p&gt;
&lt;h2 id=&#34;消费端&#34;&gt;消费端&lt;/h2&gt;
&lt;p&gt;相较生产端和服务端，消费端提升吞吐量的策略就没那么复杂了。&lt;/p&gt;
&lt;p&gt;一般来讲，消费端提升吞吐量的方式主要就是通过解耦，消费者在消费消息的时候，也是有两个线程分别来拉取消息任务线程和网络 IO 任务线程。下图描述了拉取消息的过程：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s0.lgstatic.com/i/image6/M00/50/F9/Cgp9HWEDbQeAGhdjAAsVGWUzvKw810.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;!-- raw HTML omitted --&gt;
&lt;p&gt;通过该图可以看到，消费者拉取完消息后并不是直接处理，而是放到一个缓存里，等待其他任务处理。&lt;/p&gt;
&lt;p&gt;那消费者为什么不直接从 Broker 拉取消息，而是先把消息拉取过来放入缓存再等着获取呢？可以看下面的关系图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s0.lgstatic.com/i/image6/M00/51/02/CioPOWEDbRmAbW47AAxkOLj86Is054.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;!-- raw HTML omitted --&gt;
&lt;p&gt;如图所示，拉取消息任务和网络 IO 任务是解耦的，网络 IO 任务会事先把消息拉取到消费者缓存里，然后等待拉取消息任务读取缓存里的消息。这样做的好处是拉取消息任务拉取消息的时候不会造成 IO 阻塞，可以提高拉取消息任务的效率，并最终提升整体的吞吐量。&lt;/p&gt;
&lt;h2 id=&#34;总结&#34;&gt;总结&lt;/h2&gt;
&lt;p&gt;今天我主要从生产端、服务端和消费端三个方面给你介绍了 Kafka 为提高吞吐量而做的一些设计。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;生产端主要是通过消息压缩、消息缓存批量发送、异步解耦等方面提升吞吐量的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;服务端采用的优化技术比较多，比如，网络层的 Reactor 设计提升了网络层的吞吐，顺序写、页缓存、零拷贝这些是利用操作系统的优化点来实现存储层读写的吞吐量。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;消费端主要是通过线程异步解耦的方式提升了拉取消息的效率，进而提升消费者的吞吐量。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;结合我多年的工作经验来看，搞清楚 Kafka 对于高吞吐的设计思路是有很多好处的。&lt;/p&gt;
&lt;p&gt;首先，了解这个设计原理后能更好地调优 Kafka 的性能，比如服务端网络层有 Acceptor 和 Processor 两种线程，当网络读写比较频繁的时候，你可以通过增加 Processor 线程数来提升网络吞吐。&lt;/p&gt;
&lt;p&gt;另外，这些设计原理对你设计其他系统的时候也有很大的借鉴意义。比如，你在设计高吞吐系统的时候，完全可以借鉴生产者用不同的线程完成不同的任务，实现任务的解耦，防止某个任务延迟造成整体变慢。还有，利用操作系统本身的特性优化吞吐量也是值得学习的，比如，页缓存、顺序读写、零拷贝等，大大提升了系统的吞吐量。在工作中，你都可以好好利用这些优秀的设计来实现高吞吐、高性能的系统。&lt;/p&gt;
</description>
        </item>
        <item>
        <title>消息队列模型</title>
        <link>https://blog.ther.cool/posts/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97%E6%A8%A1%E5%9E%8B/</link>
        <pubDate>Mon, 12 Jun 2023 09:46:30 +0800</pubDate>
        
        <guid>https://blog.ther.cool/posts/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97%E6%A8%A1%E5%9E%8B/</guid>
        <description>&lt;h2 id=&#34;主题和队列&#34;&gt;主题和队列&lt;/h2&gt;
&lt;p&gt;最初的消息队列，就是一个严格意义上的队列。在计算机领域，“队列（Queue）”是一种数据结构，有完整而严格的定义。在维基百科中，队列的定义是这样的：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;队列是先进先出（FIFO, First-In-First-Out）的线性表（Linear List）。在具体应用中通常用链表或者数组来实现。队列只允许在后端（称为 rear）进行插入操作，在前端（称为 front）进行删除操作。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这个定义里面包含几个关键点，第一个是先进先出，这里面隐含着的一个要求是，在消息入队出队过程中，需要保证这些消息&lt;strong&gt;严格有序&lt;/strong&gt;，按照什么顺序写进队列，必须按照同样的顺序从队列中读出来。不过，队列是没有“读”这个操作的，“读”就是出队，也就是从队列中“删除”这条消息。&lt;/p&gt;
&lt;p&gt;**早期的消息队列，就是按照“队列”的数据结构来设计的。**生产者（Producer）发消息就是入队操作，消费者（Consumer）收消息就是出队也就是删除操作，服务端存放消息的容器自然就称为“队列”。&lt;/p&gt;
&lt;p&gt;这就是最初的一种消息模型：队列模型。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2023/06/14/pH5PDXOdf6mMV1n.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;如果有多个生产者往同一个队列里面发送消息，这个队列中可以消费到的消息，就是这些生产者生产的所有消息的合集。消息的顺序就是这些生产者发送消息的自然顺序。如果有多个消费者接收同一个队列的消息，这些消费者之间实际上是竞争的关系，每个消费者只能收到队列中的一部分消息，也就是说任何一条消息只能被其中的一个消费者收到。&lt;/p&gt;
&lt;p&gt;如果需要将一份消息数据分发给多个消费者，要求每个消费者都能收到全量的消息，例如，对于一份订单数据，风控系统、分析系统、支付系统等都需要接收消息。这个时候，单个队列就满足不了需求，一个可行的解决方式是，为每个消费者创建一个单独的队列，让生产者发送多份。这显然这是个比较蠢的做法，同样的一份消息数据被复制到多个队列中会浪费资源，更重要的是，生产者必须知道有多少个消费者。为每个消费者单独发送一份消息，这实际上违背了消息队列“解耦”这个设计初衷。&lt;/p&gt;
&lt;p&gt;为了解决这个问题，演化出了另外一种消息模型：“&lt;strong&gt;发布 - 订阅模型（Publish-Subscribe Pattern）&lt;/strong&gt;”。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2023/06/14/lfibw3hr9yTdqB2.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;在发布 - 订阅模型中，消息的发送方称为发布者（Publisher），消息的接收方称为订阅者（Subscriber），服务端存放消息的容器称为主题（Topic）。发布者将消息发送到主题中，订阅者在接收消息之前需要先“订阅主题”。“订阅”在这里既是一个动作，同时还可以认为是主题在消费时的一个逻辑副本，每份订阅中，订阅者都可以接收到主题的所有消息。&lt;/p&gt;
&lt;p&gt;在消息领域的历史上很长的一段时间，队列模式和发布 - 订阅模式是并存的，有些消息队列同时支持这两种消息模型，比如 ActiveMQ。我们仔细对比一下这两种模型，生产者就是发布者，消费者就是订阅者，队列就是主题，并没有本质的区别。&lt;strong&gt;它们最大的区别其实就是，一份消息数据能不能被消费多次的问题。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;实际上，在这种发布 - 订阅模型中，如果只有一个订阅者，那它和队列模型就基本是一样的了。也就是说，发布 - 订阅模型在功能层面上是可以兼容队列模型的。&lt;/p&gt;
&lt;p&gt;现代的消息队列产品使用的消息模型大多是这种发布 - 订阅模型，当然也有例外，RabbitMQ 是少数依然坚持使用队列模型的产品之一。&lt;/p&gt;
&lt;h3 id=&#34;rocketmq-的消息模型&#34;&gt;RocketMQ 的消息模型&lt;/h3&gt;
&lt;p&gt;RocketMQ 使用的消息模型是标准的发布 - 订阅模型，在 RocketMQ 的术语表中，生产者、消费者和主题与上述发布 - 订阅模型中的概念是完全一样的。但是在 RocketMQ 还有队列（Queue）这个概念，队列在 RocketMQ 中的作用是什么呢？这就要从消息队列的消费机制说起。&lt;/p&gt;
&lt;p&gt;几乎所有的消息队列产品都使用一种非常朴素的“请求 - 确认”机制，确保消息不会在传递过程中由于网络或服务器故障丢失。具体的做法也非常简单。在生产端，生产者先将消息发送给服务端，也就是 Broker，服务端在收到消息并将消息写入主题或者队列中后，会给生产者发送确认的响应。&lt;/p&gt;
&lt;p&gt;如果生产者没有收到服务端的确认或者收到失败的响应，则会重新发送消息；在消费端，消费者在收到消息并完成自己的消费业务逻辑（比如，将数据保存到数据库中）后，也会给服务端发送消费成功的确认，服务端只有收到消费确认后，才认为一条消息被成功消费，否则它会给消费者重新发送这条消息，直到收到对应的消费成功确认。&lt;/p&gt;
&lt;p&gt;这个确认机制很好地保证了消息传递过程中的可靠性，但是，引入这个机制在消费端带来了一个问题。为了确保消息的有序性，在某一条消息被成功消费之前，下一条消息是不能被消费的，否则就会出现消息空洞，违背了有序性这个原则。&lt;/p&gt;
&lt;p&gt;也就是说，每个主题在任意时刻，至多只能有一个消费者实例在进行消费，那就没法通过水平扩展消费者的数量来提升消费端总体的消费性能。为了解决这个问题，RocketMQ 在主题下面增加了队列的概念。&lt;/p&gt;
&lt;p&gt;**每个主题包含多个队列，通过多个队列来实现多实例并行生产和消费。**需要注意的是，&lt;strong&gt;RocketMQ 只在队列上保证消息的有序性&lt;/strong&gt;，主题层面是无法保证消息的严格顺序的。&lt;/p&gt;
&lt;p&gt;RocketMQ 中，订阅者的概念是通过消费组（Consumer Group）来体现的。每个消费组都消费主题中一份完整的消息，不同消费组之间消费进度彼此不受影响，也就是说，一条消息被 Consumer Group1 消费过，也会再给 Consumer Group2 消费。&lt;/p&gt;
&lt;p&gt;消费组中包含多个消费者，同一个组内的消费者是竞争消费的关系，每个消费者负责消费组内的一部分消息。如果一条消息被消费者 Consumer1 消费了，那同组的其他消费者就不会再收到这条消息。&lt;/p&gt;
&lt;p&gt;在 Topic 的消费过程中，由于消息需要被不同的组进行多次消费，所以消费完的消息并不会立即被删除，这就需要 RocketMQ 为每个消费组在每个队列上维护一个消费位置（Consumer Offset），这个位置之前的消息都被消费过，之后的消息都没有被消费过，每成功消费一条消息，消费位置就加一。这个消费位置是非常重要的概念，我们在使用消息队列的时候，丢消息的原因大多是由于消费位置处理不当导致的。&lt;/p&gt;
&lt;p&gt;RocketMQ 的消息模型如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2023/06/14/3EUwqMuVBp6bncg.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;h3 id=&#34;kafka-的消息模型&#34;&gt;Kafka 的消息模型&lt;/h3&gt;
&lt;p&gt;Kafka 的消息模型和 RocketMQ 是完全一致，所有 RocketMQ 中对应的概念，和生产消费过程中的确认机制，都完全适用于 Kafka。唯一的区别是，在 Kafka 中，队列这个概念的名称不一样，Kafka 中对应的名称是“分区（Partition）”，含义和功能是没有任何区别的。&lt;/p&gt;
&lt;h3 id=&#34;小结&#34;&gt;小结&lt;/h3&gt;
&lt;p&gt;首先我们讲了队列和主题的区别，这两个概念的背后实际上对应着两种不同的消息模型：队列模型和发布 - 订阅模型。然后你需要理解，这两种消息模型其实并没有本质上的区别，都可以通过一些扩展或者变化来互相替代。&lt;/p&gt;
&lt;p&gt;常用的消息队列中，RabbitMQ 采用的是队列模型，但是它一样可以实现发布 - 订阅的功能。RocketMQ 和 Kafka 采用的是发布 - 订阅模型，并且二者的消息模型是基本一致的。&lt;/p&gt;
&lt;p&gt;最后提醒你一点，我这节课讲的消息模型和相关的概念是业务层面的模型，深刻理解业务模型有助于你用最佳的姿势去使用消息队列。&lt;/p&gt;
&lt;p&gt;但业务模型不等于就是实现层面的模型。比如说 MySQL 和 Hbase 同样是支持 SQL 的数据库，它们的业务模型中，存放数据的单元都是“表”，但是在实现层面，没有哪个数据库是以二维表的方式去存储数据的，MySQL 使用 B+ 树来存储数据，而 HBase 使用的是 KV 的结构来存储。同样，像 Kafka 和 RocketMQ 的业务模型基本是一样的，并不是说他们的实现就是一样的，实际上这两个消息队列的实现是完全不同的。&lt;/p&gt;
&lt;h3 id=&#34;思考题&#34;&gt;思考题&lt;/h3&gt;
&lt;p&gt;最后给大家留一个思考题。刚刚我在介绍 RocketMQ 的消息模型时讲过，在消费的时候，为了保证消息的不丢失和严格顺序，每个队列只能串行消费，无法做到并发，否则会出现消费空洞的问题。那如果放宽一下限制，不要求严格顺序，能否做到单个队列的并行消费呢？如果可以，该如何实现？欢迎在留言区与我分享讨论。&lt;/p&gt;
&lt;p&gt;评论回答：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;把消息队列的先进先出，改成数组的随机访问，用offset来控制消息组具体要消费哪条消息，mq不主动删除消息，消息有过期时间，如果到了过期时间，只能确认不能重新该消费，只保留最大可设置天数的消息。超过该天数则删除，还要维护客户端确认信息，如果有客户端没确认，需要有重发机制。&lt;/li&gt;
&lt;/ul&gt;
</description>
        </item>
        <item>
        <title>消息队列知识笔记</title>
        <link>https://blog.ther.cool/posts/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97%E7%9F%A5%E8%AF%86%E7%AC%94%E8%AE%B0/</link>
        <pubDate>Tue, 06 Jun 2023 09:25:03 +0800</pubDate>
        
        <guid>https://blog.ther.cool/posts/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97%E7%9F%A5%E8%AF%86%E7%AC%94%E8%AE%B0/</guid>
        <description>&lt;h2 id=&#34;如何确保消息不会丢失&#34;&gt;如何确保消息不会丢失?&lt;/h2&gt;
&lt;p&gt;现在主流的消息队列产品都提供了非常完善的消息可靠性保证机制，完全可以做到在消息传递过程中，即使发生网络中断或者硬件故障，也能确保消息的可靠传递，不丢消息。&lt;/p&gt;
&lt;p&gt;绝大部分丢消息的原因都是由于开发者不熟悉消息队列，没有正确使用和配置消息队列导致的。&lt;/p&gt;
&lt;h3 id=&#34;检测消息丢失的方法&#34;&gt;检测消息丢失的方法&lt;/h3&gt;
&lt;p&gt;如果是 IT 基础设施比较完善的公司，一般都有分布式链路追踪系统，使用类似的追踪系统可以很方便地追踪每一条消息。如果没有这样的追踪系统，这里我提供一个比较简单的方法，来检查是否有消息丢失的情况。&lt;/p&gt;
&lt;p&gt;**我们可以利用消息队列的有序性来验证是否有消息丢失。**原理非常简单，在 Producer 端，我们给每个发出的消息附加一个连续递增的序号，然后在 Consumer 端来检查这个序号的连续性。&lt;/p&gt;
&lt;p&gt;如果没有消息丢失，Consumer 收到消息的序号必然是连续递增的，或者说收到的消息，其中的序号必然是上一条消息的序号 +1。如果检测到序号不连续，那就是丢消息了。还可以通过缺失的序号来确定丢失的是哪条消息，方便进一步排查原因。&lt;/p&gt;
&lt;p&gt;大多数消息队列的客户端都支持拦截器机制，你可以利用这个拦截器机制，在 Producer 发送消息之前的拦截器中将序号注入到消息中，在 Consumer 收到消息的拦截器中检测序号的连续性，这样实现的好处是消息检测的代码不会侵入到你的业务代码中，待你的系统稳定后，也方便将这部分检测的逻辑关闭或者删除。&lt;/p&gt;
&lt;p&gt;如果是在一个分布式系统中实现这个检测方法，有几个问题需要你注意。&lt;/p&gt;
&lt;p&gt;首先，像 Kafka 和 RocketMQ 这样的消息队列，它是不保证在 Topic 上的严格顺序的，只能保证分区上的消息是有序的，所以我们在发消息的时候必须要指定分区，并且，在每个分区单独检测消息序号的连续性。&lt;/p&gt;
&lt;p&gt;如果你的系统中 Producer 是多实例的，由于并不好协调多个 Producer 之间的发送顺序，所以也需要每个 Producer 分别生成各自的消息序号，并且需要附加上 Producer 的标识，在 Consumer 端按照每个 Producer 分别来检测序号的连续性。&lt;/p&gt;
&lt;p&gt;Consumer 实例的数量最好和分区数量一致，做到 Consumer 和分区一一对应，这样会比较方便地在 Consumer 内检测消息序号的连续性。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;疑问：怎么保存上一条消息的序号？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;确保消息可靠传递&#34;&gt;确保消息可靠传递&lt;/h3&gt;
&lt;p&gt;，整个消息从生产到消费的过程中，哪些地方可能会导致丢消息，以及应该如何避免消息丢失。&lt;/p&gt;
&lt;p&gt;你可以看下这个图，一条消息从生产到消费完成这个过程，可以划分三个阶段，为了方便描述，我给每个阶段分别起了个名字。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;生产阶段&lt;/strong&gt;: 在这个阶段，从消息在 Producer 创建出来，经过网络传输发送到 Broker 端。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;存储阶段&lt;/strong&gt;: 在这个阶段，消息在 Broker 端存储，如果是集群，消息会在这个阶段被复制到其他的副本上。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;消费阶段&lt;/strong&gt;: 在这个阶段，Consumer 从 Broker 上拉取消息，经过网络传输发送到 Consumer 上。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;1. 生产阶段&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在生产阶段，消息队列通过最常用的请求确认机制，来保证消息的可靠传递：当你的代码调用发消息方法时，消息队列的客户端会把消息发送到 Broker，Broker 收到消息后，会给客户端返回一个确认响应，表明消息已经收到了。客户端收到响应后，完成了一次正常消息的发送。&lt;/p&gt;
&lt;p&gt;只要 Producer 收到了 Broker 的确认响应，就可以保证消息在生产阶段不会丢失。有些消息队列在长时间没收到发送确认响应后，会自动重试，如果重试再失败，就会以返回值或者异常的方式告知用户。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;你在编写发送消息代码时，需要注意，正确处理返回值或者捕获异常，就可以保证这个阶段的消息不会丢失。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 存储阶段&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在存储阶段正常情况下，只要 Broker 在正常运行，就不会出现丢失消息的问题，但是如果 Broker 出现了故障，比如进程死掉了或者服务器宕机了，还是可能会丢失消息的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果对消息的可靠性要求非常高，可以通过配置 Broker 参数来避免因为宕机丢消息。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;对于单个节点的 Broker，需要配置 Broker 参数，在收到消息后，将消息写入磁盘后再给 Producer 返回确认响应，这样即使发生宕机，由于消息已经被写入磁盘，就不会丢失消息，恢复后还可以继续消费。例如，在 RocketMQ 中，需要将刷盘方式 flushDiskType 配置为 SYNC_FLUSH 同步刷盘。&lt;/p&gt;
&lt;p&gt;如果是 Broker 是由多个节点组成的集群，需要将 Broker 集群配置成：至少将消息发送到 2 个以上的节点，再给客户端回复发送确认响应。这样当某个 Broker 宕机时，其他的 Broker 可以替代宕机的 Broker，也不会发生消息丢失。后面我会专门安排一节课，来讲解在集群模式下，消息队列是如何通过消息复制来确保消息的可靠性的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 消费阶段&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;消费阶段采用和生产阶段类似的确认机制来保证消息的可靠传递，客户端从 Broker 拉取消息后，执行用户的消费业务逻辑，成功后，才会给 Broker 发送消费确认响应。如果 Broker 没有收到消费确认响应，下次拉消息的时候还会返回同一条消息，确保消息不会在网络传输过程中丢失，也不会因为客户端在执行消费逻辑中出错导致丢失。&lt;/p&gt;
&lt;p&gt;你在编写消费代码时需要注意的是，&lt;strong&gt;不要在收到消息后就立即发送消费确认，而是应该在执行完所有消费业务逻辑之后，再发送消费确认。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&#34;小结&#34;&gt;小结&lt;/h3&gt;
&lt;p&gt;这节课我带大家分析了一条消息从发送到消费整个流程中，消息队列是如何确保消息的可靠性，不会丢失的。这个过程可以分为分三个阶段，每个阶段都需要正确的编写代码并且设置正确的配置项，才能配合消息队列的可靠性机制，确保消息不会丢失。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在生产阶段，你需要捕获消息发送的错误，并重发消息。&lt;/li&gt;
&lt;li&gt;在存储阶段，你可以通过配置刷盘和复制相关的参数，让消息写入到多个副本的磁盘上，来确保消息不会因为某个 Broker 宕机或者磁盘损坏而丢失。&lt;/li&gt;
&lt;li&gt;在消费阶段，你需要在处理完全部消费业务逻辑之后，再发送消费确认。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;思考题&#34;&gt;思考题&lt;/h3&gt;
&lt;p&gt;如果消息在网络传输过程中发送错误，由于发送方收不到确认，会通过重发来保证消息不丢失。但是，如果确认响应在网络传输时丢失，也会导致重发消息。也就是说，&lt;strong&gt;无论是 Broker 还是 Consumer 都是有可能收到重复消息的。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&#34;如何处理消费过程中的重复消息&#34;&gt;如何处理消费过程中的重复消息？&lt;/h2&gt;
&lt;h3 id=&#34;消息重复的情况必然存在&#34;&gt;消息重复的情况必然存在&lt;/h3&gt;
&lt;p&gt;在 MQTT 协议中，给出了三种传递消息时能够提供的服务质量标准，这三种服务质量从低到高依次是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;At most once&lt;/strong&gt;: 至多一次。消息在传递时，最多会被送达一次。换一个说法就是，没什么消息可靠性保证，允许丢消息。一般都是一些对消息可靠性要求不太高的监控场景使用，比如每分钟上报一次机房温度数据，可以接受数据少量丢失。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;At least once&lt;/strong&gt;: 至少一次。消息在传递时，至少会被送达一次。也就是说，不允许丢消息，但是允许有少量重复消息出现。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Exactly once&lt;/strong&gt;：恰好一次。消息在传递时，只会被送达一次，不允许丢失也不允许重复，这个是最高的等级。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个服务质量标准不仅适用于 MQTT，对所有的消息队列都是适用的。现在常用的绝大部分消息队列提供的服务质量都是 At least once，包括 RocketMQ、RabbitMQ 和 Kafka 都是这样。也就是说，消息队列很难保证消息不重复。&lt;/p&gt;
&lt;h3 id=&#34;用幂等性解决重复消息问题&#34;&gt;用幂等性解决重复消息问题&lt;/h3&gt;
&lt;p&gt;一般解决重复消息的办法是，在消费端，让消费消息的操作具备幂等性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;幂等（Idempotence）&lt;/strong&gt; 本来是一个数学上的概念，它是这样定义的：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果一个函数 f(x) 满足：f(f(x)) = f(x)，则函数 f(x) 满足幂等性。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这个概念被拓展到计算机领域，被用来描述一个操作、方法或者服务。一个幂等操作的特点是，&lt;strong&gt;其任意多次执行所产生的影响均与一次执行的影响相同。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;从对系统的影响结果来说：&lt;strong&gt;At least once + 幂等消费 = Exactly once。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;那么如何实现幂等操作呢？最好的方式就是，**从业务逻辑设计上入手，将消费的业务逻辑设计成具备幂等性的操作。**但是，不是所有的业务都能设计成天然幂等的，这里就需要一些方法和技巧来实现幂等。&lt;/p&gt;
&lt;p&gt;下面介绍几种常用的设计幂等操作的方法：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 利用数据库的唯一约束实现幂等&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;例如这个不具备幂等特性的转账的例子：将账户 X 的余额加 100 元。在这个例子中，可以通过改造业务逻辑来具备幂等性。&lt;/p&gt;
&lt;p&gt;首先限定对于每个转账单每个账户只可以执行一次变更操作，在分布式系统中，这个限制实现的方法非常多，最简单的是在数据库中建一张转账流水表，这个表有三个字段：转账单 ID、账户 ID 和变更金额，然后给转账单 ID 和账户 ID 这两个字段联合起来创建一个唯一约束，这样对于相同的转账单 ID 和账户 ID，表里至多只能存在一条记录。&lt;/p&gt;
&lt;p&gt;这样消费消息的逻辑可以变为：“在转账流水表中增加一条转账记录，然后再根据转账记录，异步操作更新用户余额即可。”在转账流水表增加一条转账记录这个操作中，由于我们在这个表中预先定义了“账户 ID 转账单 ID”的唯一约束，对于同一个转账单同一个账户只能插入一条记录，后续重复的插入操作都会失败，这样就实现了一个幂等的操作。我们只要写一个 SQL，正确地实现它就可以了。&lt;/p&gt;
&lt;p&gt;基于这个思路，不光是可以使用关系型数据库，只要是支持类似“INSERT IF NOT EXIST”语义的存储类系统都可以用于实现幂等，比如，你可以用 Redis 的 SETNX 命令来替代数据库中的唯一约束，来实现幂等消费。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 为更新的数据设置前置条件&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;另外一种实现幂等的思路是，给数据变更设置一个前置条件，如果满足条件就更新数据，否则拒绝更新数据，在更新数据的时候，同时变更前置条件中需要判断的数据。这样，重复执行这个操作时，由于第一次更新数据的时候已经变更了前置条件中需要判断的数据，不满足前置条件，则不会重复执行更新数据操作。&lt;/p&gt;
&lt;p&gt;比如“将账户 X 的余额增加 100 元”这个操作并不满足幂等性，但是将这个操作加上一个前置条件，变为：“如果账户 X 当前的余额为 500 元，将余额加 100 元”，这个操作就具备了幂等性。对应到消息队列中的使用时，可以在发消息时在消息体中带上当前的余额，在消费的时候进行判断数据库中，当前余额是否与消息中的余额相等，只有相等才执行变更操作。&lt;/p&gt;
&lt;p&gt;但是，如果我们要更新的数据不是数值，或者我们要做一个比较复杂的更新操作怎么办？用什么作为前置判断条件呢？更加通用的方法是，给你的数据增加一个版本号属性，每次更数据前，比较当前数据的版本号是否和消息中的版本号一致，如果不一致就拒绝更新数据，更新数据的同时将版本号 +1，一样可以实现幂等更新。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 记录并检查操作&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果上面提到的两种实现幂等方法都不能适用于你的场景，还有一种通用性最强，适用范围最广的实现幂等性方法：记录并检查操作，也称为“Token 机制或者 GUID（全局唯一 ID）机制”，实现的思路特别简单：在执行数据更新操作之前，先检查一下是否执行过这个更新操作。&lt;/p&gt;
&lt;p&gt;具体的实现方法是，在发送消息时，给每条消息指定一个全局唯一的 ID。消费时，先根据这个 ID 检查这条消息是否有被消费过，如果没有消费过，才更新数据，然后将消费状态置为已消费。&lt;/p&gt;
&lt;p&gt;原理和实现是不是很简单？其实一点儿都不简单，在分布式系统中，这个方法其实是非常难实现的。首先，给每个消息指定一个全局唯一的 ID 就是一件不那么简单的事儿，方法有很多，但都不太好同时满足简单、高可用和高性能，或多或少都要有些牺牲。更加麻烦的是，在“检查消费状态，然后更新数据并且设置消费状态”中，三个操作必须作为一组操作保证原子性，才能真正实现幂等，否则就会出现 Bug。&lt;/p&gt;
&lt;p&gt;比如说，对于同一条消息：“全局 ID 为 8，操作为：给 ID 为 666 账户增加 100 元”，有可能出现这样的情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;t0 时刻：Consumer A 收到条消息，检查消息执行状态，发现消息未处理过，开始执行“账户增加 100 元”；&lt;/li&gt;
&lt;li&gt;t1 时刻：Consumer B 收到条消息，检查消息执行状态，发现消息未处理过，因为这个时刻，Consumer A 还未来得及更新消息执行状态。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样就会导致账户被错误地增加了两次 100 元，这是一个在分布式系统中非常容易犯的错误，一定要引以为戒。&lt;/p&gt;
&lt;p&gt;对于这个问题，当然我们可以用事务来实现，也可以用锁来实现，但是在分布式系统中，无论是分布式事务还是分布式锁都是比较难解决的问题。&lt;/p&gt;
&lt;h3 id=&#34;小结-1&#34;&gt;小结&lt;/h3&gt;
&lt;p&gt;这节课我们主要介绍了通过幂等消费来解决消息重复的问题，然后我重点讲了几种实现幂等操作的方法，你可以利用数据库的约束来防止重复更新数据，也可以为数据更新设置一次性的前置条件，来防止重复消息，如果这两种方法都不适用于你的场景，还可以用“记录并检查操作”的方式来保证幂等，这种方法适用范围最广，但是实现难度和复杂度也比较高，一般不推荐使用。&lt;/p&gt;
&lt;p&gt;这些实现幂等的方法，不仅可以用于解决重复消息的问题，也同样适用于，在其他场景中来解决重复请求或者重复调用的问题。比如，可以将 HTTP 服务设计成幂等的，解决前端或者 APP 重复提交表单数据的问题；也可以将一个微服务设计成幂等的，解决 RPC 框架自动重试导致的重复调用问题。&lt;/p&gt;
&lt;h3 id=&#34;思考题-1&#34;&gt;思考题&lt;/h3&gt;
&lt;p&gt;为什么大部分消息队列都选择只提供 At least once 的服务质量，而不是级别更高的 Exactly once 呢？&lt;/p&gt;
&lt;p&gt;评论回答：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;我觉得最重要的原因是消息队列即使做到了Exactly once级别，consumer也还是要做幂等。因为在consumer从消息队列取消息这里，如果consumer消费成功，但是ack失败，consumer还是会取到重复的消息，所以消息队列花大力气做成Exactly once并不能解决业务侧消息重复的问题。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;我感觉是消息队列没有办法做到exactly once吧。原因是网络环境太复杂，底层的tcp都做不到exactly once，上层的应用更加做不到了。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;exactly once的要想实现，一种选择是效仿mqtt qos2，做多次确认。这种方法理论上并不能100%避免消息重复，却使得性能大幅下降，得不偿失。另一种是mq的consumer实现框架在内部对消息id做记录，并做重复性检查，但这又引来了新问题，框架的实现无法知道一条长时间没ack的消息发生了什么，没有进行去重的依据。看来这些是还是交给应用层更合适。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;如何处理消息积压&#34;&gt;如何处理消息积压？&lt;/h2&gt;
&lt;p&gt;消息积压的直接原因，一定是系统中的某个部分出现了性能问题，来不及处理上游发送的消息，才会导致消息积压。所以先分析下在使用消息队列时，如何优化代码的性能，避免出现消息积压。然后再看如果线上系统出现了消息积压该如何进行紧急处理，最大程度地避免消息积压对业务的影响。&lt;/p&gt;
&lt;h3 id=&#34;优化性能来避免消息积压&#34;&gt;优化性能来避免消息积压&lt;/h3&gt;
&lt;p&gt;在使用消息队列的系统中，对于性能的优化，主要体现在生产者和消费者这一收一发两部分的业务逻辑中。对于消息队列本身的性能，你作为使用者，不需要太关注。为什么这么说呢？&lt;/p&gt;
&lt;p&gt;主要原因是，对于绝大多数使用消息队列的业务来说，消息队列本身的处理能力要远大于业务系统的处理能力。主流消息队列的单个节点，消息收发的性能可以达到每秒钟处理几万至几十万条消息的水平，还可以通过水平扩展 Broker 的实例数成倍地提升处理能力。&lt;/p&gt;
&lt;p&gt;而一般的业务系统需要处理的业务逻辑远比消息队列要复杂，单个节点每秒钟可以处理几百到几千次请求，已经可以算是性能非常好的了。所以，对于消息队列的性能优化，我们更关注的是，&lt;strong&gt;在消息的收发两端，我们的业务代码怎么和消息队列配合，达到一个最佳的性能。&lt;/strong&gt;&lt;/p&gt;
&lt;h4 id=&#34;1-发送端性能优化&#34;&gt;1. 发送端性能优化&lt;/h4&gt;
&lt;p&gt;发送端业务代码的处理性能，实际上和消息队列的关系不大，因为一般发送端都是先执行自己的业务逻辑，最后再发送消息。&lt;strong&gt;如果说，你的代码发送消息的性能上不去，你需要优先检查一下，是不是发消息之前的业务逻辑耗时太多导致的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;对于发送消息的业务逻辑，只需要注意设置合适的并发和批量大小，就可以达到很好的发送性能。为什么这么说呢？&lt;/p&gt;
&lt;p&gt;我们之前的课程中讲过 Producer 发送消息的过程，Producer 发消息给 Broker，Broker 收到消息后返回确认响应，这是一次完整的交互。假设这一次交互的平均时延是 1ms，我们把这 1ms 的时间分解开，它包括了下面这些步骤的耗时：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;发送端准备数据、序列化消息、构造请求等逻辑的时间，也就是发送端在发送网络请求之前的耗时；&lt;/li&gt;
&lt;li&gt;发送消息和返回响应在网络传输中的耗时；&lt;/li&gt;
&lt;li&gt;Broker 处理消息的时延。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果是单线程发送，每次只发送 1 条消息，那么每秒只能发送 1000ms / 1ms * 1 条 /ms = 1000 条 消息，这种情况下并不能发挥出消息队列的全部实力。&lt;/p&gt;
&lt;p&gt;无论是增加每次发送消息的批量大小，还是增加并发，都能成倍地提升发送性能。至于到底是选择批量发送还是增加并发，主要取决于发送端程序的业务性质。简单来说，只要能够满足你的性能要求，怎么实现方便就怎么实现。&lt;/p&gt;
&lt;p&gt;比如说，你的消息发送端是一个微服务，主要接受 RPC 请求处理在线业务。很自然的，微服务在处理每次请求的时候，就在当前线程直接发送消息就可以了，因为所有 RPC 框架都是多线程支持多并发的，自然也就实现了并行发送消息。并且在线业务比较在意的是请求响应时延，选择批量发送必然会影响 RPC 服务的时延。这种情况，比较明智的方式就是通过并发来提升发送性能。&lt;/p&gt;
&lt;p&gt;如果你的系统是一个离线分析系统，离线系统在性能上的需求是什么呢？它不关心时延，更注重整个系统的吞吐量。发送端的数据都是来自于数据库，这种情况就更适合批量发送，你可以批量从数据库读取数据，然后批量来发送消息，同样用少量的并发就可以获得非常高的吞吐量。&lt;/p&gt;
&lt;h4 id=&#34;2-消费端性能优化&#34;&gt;2. 消费端性能优化&lt;/h4&gt;
&lt;p&gt;使用消息队列的时候，大部分的性能问题都出现在消费端，如果消费的速度跟不上发送端生产消息的速度，就会造成消息积压。如果这种性能倒挂的问题只是暂时的，那问题不大，只要消费端的性能恢复之后，超过发送端的性能，那积压的消息是可以逐渐被消化掉的。&lt;/p&gt;
&lt;p&gt;要是消费速度一直比生产速度慢，时间长了，整个系统就会出现问题，要么，消息队列的存储被填满无法提供服务，要么消息丢失，这对于整个系统来说都是严重故障。&lt;/p&gt;
&lt;p&gt;所以，我们在设计系统的时候，&lt;strong&gt;一定要保证消费端的消费性能要高于生产端的发送性能，这样的系统才能健康的持续运行。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;消费端的性能优化除了优化消费业务逻辑以外，也可以通过水平扩容，增加消费端的并发数来提升总体的消费性能。特别需要注意的一点是，**在扩容 Consumer 的实例数量的同时，必须同步扩容主题中的分区（也叫队列）数量，确保 Consumer 的实例数和分区数量是相等的。**如果 Consumer 的实例数量超过分区数量，这样的扩容实际上是没有效果的。原因我们之前讲过，因为对于消费者来说，在每个分区上实际上只能支持单线程消费。&lt;/p&gt;
&lt;p&gt;我见到过很多消费程序，他们是这样来解决消费慢的问题的：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2023/06/12/sUeGAqKcj4Y2OZo.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;它收消息处理的业务逻辑可能比较慢，也很难再优化了，为了避免消息积压，在收到消息的 OnMessage 方法中，不处理任何业务逻辑，把这个消息放到一个内存队列里面就返回了。然后它可以启动很多的业务线程，这些业务线程里面是真正处理消息的业务逻辑，这些线程从内存队列里取消息处理，这样它就解决了单个 Consumer 不能并行消费的问题。&lt;/p&gt;
&lt;p&gt;这个方法是不是很完美地实现了并发消费？请注意，这是一个非常常见的错误方法！ 为什么错误？因为会丢消息。如果收消息的节点发生宕机，在内存队列中还没来及处理的这些消息就会丢失。&lt;/p&gt;
&lt;h3 id=&#34;消息积压了该如何处理&#34;&gt;消息积压了该如何处理？&lt;/h3&gt;
&lt;p&gt;能导致积压突然增加，最粗粒度的原因，只有两种：要么是发送变快了，要么是消费变慢了。&lt;/p&gt;
&lt;p&gt;大部分消息队列都内置了监控的功能，只要通过监控数据，很容易确定是哪种原因。如果是单位时间发送的消息增多，比如说是赶上大促或者抢购，短时间内不太可能优化消费端的代码来提升消费性能，唯一的方法是通过扩容消费端的实例数来提升总体的消费能力。&lt;/p&gt;
&lt;p&gt;如果短时间内没有足够的服务器资源进行扩容，没办法的办法是，将系统降级，通过关闭一些不重要的业务，减少发送方发送的数据量，最低限度让系统还能正常运转，服务一些重要业务。&lt;/p&gt;
&lt;p&gt;还有一种不太常见的情况，你通过监控发现，无论是发送消息的速度还是消费消息的速度和原来都没什么变化，这时候你需要检查一下你的消费端，是不是&lt;strong&gt;消费失败导致的一条消息反复消费&lt;/strong&gt;这种情况比较多，这种情况也会拖慢整个系统的消费速度。&lt;/p&gt;
&lt;p&gt;如果监控到消费变慢了，你需要检查你的消费实例，分析一下是什么原因导致消费变慢。优先检查一下日志是否有大量的消费错误，如果没有错误的话，可以通过打印堆栈信息，看一下你的&lt;strong&gt;消费线程&lt;/strong&gt;是不是卡在什么地方不动了，比如&lt;strong&gt;触发了死锁或者卡在等待某些资源&lt;/strong&gt;上了。&lt;/p&gt;
&lt;h3 id=&#34;小结-2&#34;&gt;小结&lt;/h3&gt;
&lt;p&gt;优化消息收发性能，预防消息积压的方法有两种，增加批量或者是增加并发，在发送端这两种方法都可以使用，在消费端需要注意的是，增加并发需要同步扩容分区数量，否则是起不到效果的。&lt;/p&gt;
&lt;p&gt;对于系统发生消息积压的情况，需要先解决积压，再分析原因，毕竟保证系统的可用性是首先要解决的问题。快速解决积压的方法就是通过水平扩容增加 Consumer 的实例数量。&lt;/p&gt;
&lt;h3 id=&#34;思考题-2&#34;&gt;思考题&lt;/h3&gt;
&lt;p&gt;在消费端是否可以通过批量消费的方式来提升消费性能？在什么样场景下适合使用这种方法？有什么局限性？&lt;/p&gt;
&lt;p&gt;评论解答：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;批量消费有意义的场景要求：1.要么消费端对消息的处理支持批量处理，比如批量入库 2. 要么消费端支持多线程/协程并发处理，业务上也允许消息无序。3. 或者网络带宽在考虑因素内，需要减少消息的overhead。
批量消费的局限性：1. 需要一个整体ack的机制，一旦一条靠前的消息消费失败，可能会引起很多消息重试。2. 多线程下批量消费速度受限于最慢的那个线程。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;1、要求消费端能够批量处理或者开启多线程进行单条处理
2、批量消费一旦某一条数据消费失败会导致整批数据重复消费
3、对实时性要求不能太高，批量消费需要Broker积累到一定消费数据才会发送到Consumer&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;1.无法提升消费业务效率（仅受消费业务自身逻辑影响），但可以提高mq中堆积消息消费的整体吞吐量（批推比单推mq耗时较短）。
2.数据增量同步，监控信息采集。（非核心业务的稳定大数据流操作）。
3.批处理意味数据积累和大数据传输，这会让单次消费的最长时延变长。同时批量操作为了保证当前批量操作一致性，在个别失败的情况下会引发批量操作重试。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</description>
        </item>
        <item>
        <title>Go in action-笔记</title>
        <link>https://blog.ther.cool/posts/go-in-action-%E7%AC%94%E8%AE%B0/</link>
        <pubDate>Sun, 19 Mar 2023 20:04:52 +0800</pubDate>
        
        <guid>https://blog.ther.cool/posts/go-in-action-%E7%AC%94%E8%AE%B0/</guid>
        <description>&lt;h1 id=&#34;一介绍&#34;&gt;一、介绍&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;Go 语言是现代的、快速的，带有一个强大的&lt;strong&gt;标准库&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;Go 语言内置对&lt;strong&gt;并发&lt;/strong&gt;的支持。&lt;/li&gt;
&lt;li&gt;Go 语言使用&lt;strong&gt;接口&lt;/strong&gt;作为代码复用的基础模块。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;开发速度&#34;&gt;开发速度&lt;/h3&gt;
&lt;p&gt;编译 Go 程序时，编译器只会关注那些直接被引用的库，而不是像 Java、C 和 C++ 那样，要遍历依赖链中所有依赖的库。&lt;/p&gt;
&lt;p&gt;因为没有从编译代码到执行代码的中间过程，用动态语言编写应用程序可以快速看到输出。代价是，动态语言不提供静态语言提供的&lt;strong&gt;类型安全特性&lt;/strong&gt;，不得不经常用大量的测试套件来避免在运行的时候出现类型错误这类 bug。&lt;/p&gt;
&lt;h3 id=&#34;并发&#34;&gt;并发&lt;/h3&gt;
&lt;p&gt;现代计算机都拥有多个核，但是大部分编程语言都没有有效的工具让程序可以轻易利用这些资源。这些语言需要
写大量的&lt;strong&gt;线程同步代码&lt;/strong&gt;来利用&lt;strong&gt;多个核&lt;/strong&gt;，很容易导致错误。&lt;/p&gt;
&lt;p&gt;Go 语言对并发的支持是这门语言最重要的特性之一。goroutine 很像线程，但是它&lt;strong&gt;占用的内存远少于线程&lt;/strong&gt;，使用它需要的代码更少。通道（channel）是一种内置的数据结构，可以让用户在不同的 goroutine 之间同步发送具有类型的&lt;strong&gt;消息&lt;/strong&gt;。这让编程模型更倾向于在 goroutine之间发送消息，而不是让多个 goroutine 争夺同一个数据的使用权。&lt;/p&gt;
&lt;h3 id=&#34;goroutine&#34;&gt;goroutine&lt;/h3&gt;
&lt;p&gt;goroutine 是可以与其他 goroutine 并行执行的&lt;strong&gt;函数&lt;/strong&gt;，同时也会&lt;strong&gt;与主程序（程序的入口）并行执行&lt;/strong&gt;。在其他编程语言中，需要用&lt;strong&gt;线程&lt;/strong&gt;来完成同样的事情，而在 Go 语言中会使用&lt;strong&gt;同一个线程&lt;/strong&gt;来执行&lt;strong&gt;多个 goroutine&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;例如，用户在写一个 Web 服务器，希望同时处理不同的 Web 请求，如果使用 C 或者 Java，不得不写大量的额外代码来使用线程。在 Go 语言中，net/http 库直接使用了内置的 goroutine。每个接收到的请求都自动在其自己的 goroutine 里处理。goroutine 使用的内存比线程更少，Go 语言运行时会自动在配置的一组&lt;strong&gt;逻辑处理器&lt;/strong&gt;上调度执行 goroutine。&lt;strong&gt;每个逻辑处理器绑定到一个操作系统线程上&lt;/strong&gt;（下图）。这让用户的应用程序执行效率更高，而开发工作量显著减少。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2023/03/19/GFsrxnJjk4BC1TD.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;image-20200611094338474&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;如果想在执行一段代码的同时，并行去做另外一些事情，goroutine 是很好的选择。下面是一个简单的例子：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;log&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;msg&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;c1&#34;&gt;// ...这里是一些记录日志的代码
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 代码里有些地方检测到了错误
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;go&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;log&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;发生了可怕的事情&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;关键字 go 是唯一需要去编写的代码，调度 log 函数作为独立的 goroutine 去运行，以便与其他 goroutine 并行执行。这意味着应用程序的其余部分会与记录日志并行执行，通常这种并行能让最终用户觉得性能更好。goroutine 占用的资源更少，所以常常能启动成千上万个 goroutine。&lt;/p&gt;
&lt;h3 id=&#34;通道&#34;&gt;通道&lt;/h3&gt;
&lt;p&gt;通道是&lt;strong&gt;一种数据结构&lt;/strong&gt;，可以让 goroutine 之间进行安全的数据通信。通道可以帮用户避免其他语言里常见的&lt;strong&gt;共享内存访问&lt;/strong&gt;的问题。&lt;/p&gt;
&lt;p&gt;并发的最难的部分就是要确保其他并发运行的进程、线程或 goroutine 不会意外修改用户的数据。当不同的线程在没有同步保护的情况下修改同一个数据时，总会发生灾难。在其他语言中，如果使用全局变量或者共享内存，必须使用复杂的&lt;strong&gt;锁规则&lt;/strong&gt;来防止对同一个变量的不同步修改。&lt;/p&gt;
&lt;p&gt;为了解决这个问题，通道提供了一种新模式，从而保证并发修改时的数据安全。&lt;strong&gt;通道这一模式保证同一时刻只会有一个 goroutine 修改数据&lt;/strong&gt;。通道用于在几个运行的 goroutine 之间发送数据。&lt;/p&gt;
&lt;p&gt;通道并不提供跨 goroutine 的数据访问保护机制。如果通过通道传输数据的一份副本，那么每个 goroutine 都持有一份副本，各自对自己的副本做修改是安全的。当传输的是指向数据的指针时，如果读和写是由不同的 goroutine 完成的，每个 goroutine 依旧需要额外的同步动作。&lt;/p&gt;
&lt;h3 id=&#34;类型系统&#34;&gt;类型系统&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;组合模式&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Go 开发者使用&lt;strong&gt;组合（composition）设计模式&lt;/strong&gt;，只需简单地将一个类型嵌入到另一个类型，就能复用所有的功能。其他语言也能使用组合，但是不得不和继承绑在一起使用，结果使整个用法非常复杂。在 Go 语言中，一个类型由其他更微小的类型组合而成，避免了传统的基于继承的模型。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;接口&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Go 语言还具有独特的接口实现机制，允许用户对行为进行建模，而不是对类型进行建模。在 Go 语言中，&lt;strong&gt;不需要声明某个类型实现了某个接口&lt;/strong&gt;，&lt;strong&gt;编译器&lt;/strong&gt;会判断一个类型的实例是否符合正在使用的接口。在 Go 语言中，如果一个类型实现了一个接口的&lt;strong&gt;所有方法&lt;/strong&gt;，那么这个类型的实例就可以存储在这个接口类型的实例中，不需要额外声明。Go 标准库里的很多接口都非常简单，只开放几个函数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;类型&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Go 语言不仅有类似 int 和 string 这样的内置类型，还支持用户定义的类型。在 Go 语言中，用户定义的类型通常包含一组带类型的字段，用于存储数据。 Go 语言的用户定义的类型看起来和 C 语言的&lt;strong&gt;结构&lt;/strong&gt;很像，用起来也很相似。不过 Go 语言的类型&lt;strong&gt;可以声明操作该类型数据的方法&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;内存管理&#34;&gt;内存管理&lt;/h3&gt;
&lt;p&gt;Go 语言拥有现代化的垃圾回收机制。在其他系统语言（如 C 或者 C++）中，使用内存前要先分配这段内存，而且使用完毕后要将其释放掉。哪怕只做错了一件事，都可能导致程序崩溃或者内存泄漏。可惜，追踪内存是否还被使用本身就是十分艰难的事情，而要想支持多线程和高并发，更是让这件事难上加难。虽然 Go 语言的垃圾回收会有一些额外的开销，但是编程时，能显著降低开发难度。 Go 语言把无趣的内存管理交给专业的编译器去做，而让程序员专注于更有趣的事情。&lt;/p&gt;
&lt;h1 id=&#34;二快速开始&#34;&gt;二、快速开始&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;每个代码文件都属于一个包，而包名应该与代码文件所在的文件夹同名。&lt;/li&gt;
&lt;li&gt;Go 语言提供了多种声明和初始化变量的方式。如果变量的值没有显式初始化，编译器会将变量初始化为零值。&lt;/li&gt;
&lt;li&gt;使用&lt;strong&gt;指针&lt;/strong&gt;可以在&lt;strong&gt;函数&lt;/strong&gt;间或者 &lt;strong&gt;goroutine&lt;/strong&gt; 间共享数据。&lt;/li&gt;
&lt;li&gt;通过启动 &lt;strong&gt;goroutine&lt;/strong&gt; 和使用&lt;strong&gt;通道&lt;/strong&gt;完成&lt;strong&gt;并发&lt;/strong&gt;和&lt;strong&gt;同步&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;Go 语言提供了内置函数来支持 Go 语言内部的数据结构。&lt;/li&gt;
&lt;li&gt;标准库包含很多包，能做很多很有用的事情。&lt;/li&gt;
&lt;li&gt;使用 Go 接口可以编写通用的代码和框架。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;21-main-包&#34;&gt;2.1 main 包&lt;/h3&gt;
&lt;p&gt;main 函数保存在名为 main 的包里。如果 main 函数不在 main 包里，构建工具就不会生成可执行的文件。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Go 语言的每个代码文件都属于一个包&lt;/strong&gt;， main.go 也不例外。一个包定义一组编译过的代码，&lt;strong&gt;包的名字类似命名空间&lt;/strong&gt;，可以用来间接访问包内声明的标识符。这个特性可以把不同包中定义的同名标识符区别开。&lt;/p&gt;
&lt;p&gt;关键字 import 就是导入一段代码，让用户可以访问其中的&lt;strong&gt;标识符&lt;/strong&gt;，如类型、函数、常量和接口。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;所有处于同一个文件夹里的代码文件，必须使用同一个包名&lt;/strong&gt;。按照惯例，&lt;strong&gt;包和文件夹同名&lt;/strong&gt;。就像之前说的，一个包定义一组编译后的代码，每段代码都描述包的一部分。&lt;/p&gt;
&lt;p&gt;导包的时候可以在导入的路径前面加一个下划线，如：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;s&#34;&gt;&amp;#34;os&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nx&#34;&gt;_&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;github.com/goinaction/code/chapter2/sample/matchers&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这个技术是为了让 Go 语言&lt;strong&gt;对包做初始化操作&lt;/strong&gt;，但是并不使用包里的标识符。为了让程序的&lt;strong&gt;可读性&lt;/strong&gt;更强， &lt;strong&gt;Go 编译器不允许声明导入某个包却不使用&lt;/strong&gt;。下划线让编译器接受这类导入，并且调用对应包内的&lt;strong&gt;所有代码文件里定义的 init 函数&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;（在 main.go 中导入的）程序中每个代码文件里的 init 函数都会在 main 函数执行前调用。&lt;/p&gt;
&lt;h3 id=&#34;22-search-包&#34;&gt;2.2 search 包&lt;/h3&gt;
&lt;p&gt;与第三方包不同，从标准库中导入代码时，只需要给出要导入的包名。编译器查找包的时候，总是会到 GOROOT 和 GOPATH 环境变量引用的位置去查找。&lt;/p&gt;
&lt;p&gt;变量没有定义在任何函数作用域内，所以会被当成&lt;strong&gt;包级变量&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在 Go 语言里，&lt;strong&gt;标识符要么从包里公开，要么不从包里公开&lt;/strong&gt;。当代码导入了一个包时，程序可以直接访问这个包中任意一个公开的标识符。这些标识符以大写字母开头。&lt;/p&gt;
&lt;p&gt;以小写字母开头的标识符是不公开的，不能被其他包中的代码直接访问。但是，其他包可以间接访问不公开的标识符。例如，一个函数可以返回一个未公开类型的值，那么这个函数的任何调用者，哪怕调用者不是在这个包里声明的，都可以访问这个值。&lt;strong&gt;有疑问&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;map 是 Go 语言里的一个引用类型，需要使用 make 来构造。如果不先构造 map 并将构造后的值赋值给变量，会在试图使用这个 map 变量时收到出错信息。这是因为 map 变量默认的零值是 nil。&lt;/p&gt;
&lt;p&gt;在 Go 语言中，所有变量都被初始化为其零值。对于数值类型，零值是 0；对于字符串类型，零值是空字符串；对于布尔类型，零值是 false；对于指针，零值是 nil。对于引用类型来说，所引用的底层数据结构会被初始化为对应的零值。但是被声明为其零值的引用类型的变量，会返回 nil 作为其值。&lt;strong&gt;有疑问&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;切片是一种实现了一个&lt;strong&gt;动态数组&lt;/strong&gt;的引用类型。在 Go 语言里可以用切片来操作一组数据。&lt;/p&gt;
&lt;p&gt;简化变量声明运算符 &lt;strong&gt;:=&lt;/strong&gt; 用于声明一个变量，同时给这个变量赋予初始值。简化变量声明运算符只是一
种简化记法，让代码可读性更高。这个运算符声明的变量和其他使用关键字 &lt;strong&gt;var&lt;/strong&gt; 声明的变量没有任何区别。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;根据经验&lt;/strong&gt;，如果需要声明初始值为零值的变量，应该使用 var 关键字声明变量；如果提供确切的非零值初始化变量或者使用函数返回值创建变量，应该使用简化变量声明运算符。&lt;/p&gt;
&lt;p&gt;在 Go 语言中，通道（channel）和映射（map）与切片（slice）一样，也是引用类型，不过通道本身实现的是一组带类型的值，这组值用于在 goroutine 之间传递数据。通道内置同步机制，从而保证通信安全。&lt;/p&gt;
&lt;p&gt;在 Go 语言中，如果 main 函数返回，整个程序也就终止了。 Go 程序终止时，还会关闭所有之前启动且还在运行的 goroutine。写并发程序的时候，&lt;strong&gt;最佳做法&lt;/strong&gt;是，在 main 函数返回前，清理并终止所有之前启动的 goroutine。编写启动和终止时的状态都很清晰的程序，有助减少 bug，防止资源异常。&lt;/p&gt;
&lt;p&gt;查找 map 里的键时，有两个选择：要么赋值给一个变量，要么为了精确查找，赋值给两个变量。赋值给两个变量时第一个值和赋值给一个变量时的值一样，是 map 查找的结果值。如果指定了第二个值，就会返回一个布尔标志，来表示查找的键是否存在于 map 里。如果这个键不存在， map 会返回其值类型的零值作为返回值，如果这个键存在， map 会返回键所对应值的副本。&lt;/p&gt;
&lt;p&gt;一个 goroutine 是一个独立于其他函数运行的&lt;strong&gt;函数&lt;/strong&gt;。使用关键字 go 启动一个 goroutine，并对这个 goroutine 做并发调度。&lt;/p&gt;
&lt;p&gt;在 Go 语言中，&lt;strong&gt;所有的变量都以值的方式传递&lt;/strong&gt;。因为指针变量的值是所指向的内存地址，在函数间传递指针变量，是在传递这个地址值，所以依旧被看作以值的方式在传递。&lt;/p&gt;
&lt;p&gt;Go 语言支持&lt;strong&gt;闭包&lt;/strong&gt;，有了闭包，函数可以直接访问到那些没有作为参数传入的变量。匿名函数并没有拿到这些变量的副本，而是直接访问外层函数作用域中声明的这些&lt;strong&gt;变量本身&lt;/strong&gt;。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;mi&#34;&gt;10&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// Feed 包含我们需要处理的数据源的信息
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;11&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;type&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;Feed&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;mi&#34;&gt;12&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;Name&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;`json:&amp;#34;site&amp;#34;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;mi&#34;&gt;13&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;URI&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;`json:&amp;#34;link&amp;#34;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;mi&#34;&gt;14&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;Type&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;`json:&amp;#34;type&amp;#34;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;mi&#34;&gt;15&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;在第 11 行到第 15 行声明了一个名叫 Feed 的结构类型。这个类型会对外暴露。这个类型里面声明了 3 个字段，每个字段的类型都是字符串，对应于数据文件中各个文档的不同字段。每个字段的声明最后 ` 引号里的部分被称作&lt;strong&gt;标记（tag）&lt;/strong&gt;。这个标记里描述了 JSON 解码的元数据，用于创建 Feed 类型值的切片。每个标记将结构类型里字段对应到 JSON 文档里指定名字的字段。&lt;/p&gt;
&lt;p&gt;通过返回 error 类型值来表示函数是否调用成功。这种用法在标准库里也很常见。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;RetrieveFeeds&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;([]&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Feed&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;error&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;    &lt;span class=&#34;c1&#34;&gt;// 当函数返回时
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;	&lt;span class=&#34;c1&#34;&gt;// 关闭文件
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;	&lt;span class=&#34;k&#34;&gt;defer&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;file&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Close&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;关键字 defer 会安排随后的函数调用在函数返回时才执行&lt;/strong&gt;。在使用完文件后，需要主动关闭文件。使用关键字 defer 来安排调用 Close 方法，可以保证这个函数一定会被调用。哪怕函数意外崩溃终止，也能保证关键字 defer 安排调用的函数会被执行。关键字 defer 可以缩短打开文件和关闭文件之间间隔的代码行数，有助提高代码可读性，减少错误。&lt;/p&gt;
&lt;p&gt;interface 关键字声明了一个接口，这个接口声明了结构类型或者具名类型需要实现的行为。一个接口的行为最终由在这个接口类型中声明的方法决定。&lt;/p&gt;
&lt;p&gt;命名接口的时候，也需要遵守 Go 语言的命名惯例。如果接口类型只包含一个方法，那么这个类型的名字以 er 结尾。我们的例子里就是这么做的，所以这个接口的名字叫作 Matcher。如果接口类型内部声明了多个方法，其名字需要与其行为关联。&lt;/p&gt;
&lt;p&gt;空结构在创建实例时，不会分配任何内存。这种结构很适合创建没有任何状态的类型。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;m&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;defaultMatcher&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;Search&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Search 方法的声明也声明了 defaultMatcher 类型的值的接收者。如果声明函数的时候带有接收者，则意味着声明了一个方法。这个方法会和指定的接收者的类型绑在一起。在例子里， Search 方法与 defaultMatcher 类型的值绑在一起。这意味着可以使用 defaultMatcher 类型的值或者指向这个类型值的指针来调用 Search 方法。无论是使用接收者类型的值来调用这个方，还是使用接收者类型值的指针来调用这个方法，编译器都会正确地引用或者解引用对应的值，作为接收者传递给 Search 方法。&lt;/p&gt;
&lt;p&gt;如：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 方法声明为使用 defaultMatcher 类型的值作为接收者
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;m&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;defaultMatcher&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;Search&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;feed&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Feed&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;searchTerm&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 声明一个指向 defaultMatcher 类型值的指针
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;dm&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;new&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;defaultMatch&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 编译器会解开 dm 指针的引用，使用对应的值调用方法
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;dm&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Search&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;feed&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;test&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 方法声明为使用指向 defaultMatcher 类型值的指针作为接收者
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;m&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;defaultMatcher&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;Search&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;feed&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Feed&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;searchTerm&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 声明一个 defaultMatcher 类型的值
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;dm&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;defaultMatch&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 编译器会自动生成指针引用 dm 值，使用指针调用方法
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;dm&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Search&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;feed&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;test&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;因为大部分方法在被调用后都需要维护接收者的值的状态，所以一个最佳实践是将方法的接收者声明为指针。&lt;/p&gt;
&lt;h1 id=&#34;三打包和工具链&#34;&gt;三、打包和工具链&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;在 Go 语言中包是组织代码的基本单位。&lt;/li&gt;
&lt;li&gt;环境变量 GOPATH 决定了 Go 源代码在磁盘上被保存、编译和安装的位置。&lt;/li&gt;
&lt;li&gt;可以为每个工程设置不同的 GOPATH，以保持源代码和依赖的隔离。&lt;/li&gt;
&lt;li&gt;go 工具是在命令行上工作的最好工具。&lt;/li&gt;
&lt;li&gt;开发人员可以使用 go get 来获取别人的包并将其安装到自己的 GOPATH 指定的目录。&lt;/li&gt;
&lt;li&gt;想要为别人创建包很简单，只要把源代码放到公用代码库，并遵守一些简单规则就可以了。&lt;/li&gt;
&lt;li&gt;Go 语言在设计时将分享代码作为语言的核心特性和驱动力。&lt;/li&gt;
&lt;li&gt;推荐使用依赖管理工具来管理依赖。&lt;/li&gt;
&lt;li&gt;有很多社区开发的依赖管理工具，如 godep、vender 和 gb。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;31-包&#34;&gt;3.1 包&lt;/h3&gt;
&lt;p&gt;在 Go 语言里，包是个非常重要的概念。其设计理念是使用包来封装不同语义单元的功能。这样做，能够更好地复用代码，并对每个包内的数据的使用有更好的控制。&lt;/p&gt;
&lt;p&gt;所有 Go 语言的程序都会组织成若干组文件，每组文件被称为一个包。&lt;/p&gt;
&lt;p&gt;所有的 .go 文件，除了空行和注释，都应该在第一行声明自己所属的包。每个包都在一个单独的目录里。不能把多个包放到同一个目录中，也不能把同一个包的文件分拆到多个不同目录中。这意味着，&lt;strong&gt;同一个目录下的所有 .go 文件必须声明同一个包名&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;包名惯例&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;给包命名的惯例是使用包所在目录的名字。这让用户在导入包的时候，就能清晰地知道包名。给包及其目录命名时，应该使用简洁、清晰且&lt;strong&gt;全小写&lt;/strong&gt;的名字。&lt;/p&gt;
&lt;p&gt;记住，并不需要所有包的名字都与别的包不同，因为&lt;strong&gt;导入包时是使用全路径的&lt;/strong&gt;，所以可以区分同名的不同包。一般情况下，包被导入后会使用你的包名作为默认的名字，不过这个导入后的名字可以修改。这个特性在需要导入不同目录的同名包时很有用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;main 包&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在 Go 语言里，命名为 main 的包具有特殊的含义。 Go 语言的编译程序会试图把这种名字的包编译为&lt;strong&gt;二进制可执行文件&lt;/strong&gt;。所有用 Go 语言编译的可执行程序都必须有一个名叫 main 的包。&lt;/p&gt;
&lt;p&gt;当编译器发现某个包的名字为 main 时，它一定也会发现名为 main() 的函数，否则不会创建可执行文件。 main()函数是程序的入口，所以如果没有这个函数，程序就没有办法开始执行。程序编译时，会使用声明 main 包的代码所在的目录的&lt;strong&gt;目录名&lt;/strong&gt;作为二进制可执行文件的&lt;strong&gt;文件名&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;命令和包&lt;/p&gt;
&lt;p&gt;Go 文档里经常使用命令（command）这个词来指代可执行程序，如命令行应用程序。这会让新手在阅读文档时产生困惑。记住，在 Go 语言里，命令是指任何可执行程序。作为对比，包更常用来指语义上可导入的功能单元。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;获取包的文档&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;例如：可以访问 &lt;a class=&#34;link&#34; href=&#34;http://golang.org/pkg/fmt/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;http://golang.org/pkg/fmt/&lt;/a&gt; 或者在终端输入 &lt;code&gt;go doc fmt&lt;/code&gt; 来了解更多关于 fmt 包的细节。&lt;/p&gt;
&lt;p&gt;执行命令 go build，会生成一个二进制文件。在 UNIX、Linux 和 Mac OS X 系统上，这个文件会命名为 hello，而在 Windows 系统上会命名为 hello.exe。&lt;/p&gt;
&lt;h3 id=&#34;32-导入&#34;&gt;3.2 导入&lt;/h3&gt;
&lt;p&gt;如果需要导入多个包，习惯上是将 import 语句包装在一个导入块。例如：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;s&#34;&gt;&amp;#34;fmt&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;s&#34;&gt;&amp;#34;strings&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;)
编译器会使用 Go &lt;strong&gt;环境变量&lt;/strong&gt;设置的路径，通过引入的&lt;strong&gt;相对路径&lt;/strong&gt;来查找磁盘上的包。标准库中的包会在&lt;strong&gt;安装 Go 的位置&lt;/strong&gt;找到。 Go 开发者创建的包会在 &lt;strong&gt;GOPATH 环境变量&lt;/strong&gt;指定的目录里查找。GOPATH 指定的这些目录就是开发者的个人工作空间。&lt;/p&gt;
&lt;p&gt;举个例子。如果 Go 安装在 &lt;code&gt;/usr/local/go&lt;/code&gt;，并且环境变量 GOPATH 设置为 &lt;code&gt;/home/myproject:/home/ mylibraries&lt;/code&gt;，编译器就会按照下面的顺序查找 net/http 包：&lt;strong&gt;注意顺序。&lt;/strong&gt;&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;/usr/local/go/src/pkg/net/http
/home/myproject/src/net/http
/home/mylibraries/src/net/http
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;一旦编译器找到&lt;strong&gt;一个&lt;/strong&gt;满足 import 语句的包，&lt;strong&gt;就停止&lt;/strong&gt;进一步查找。有一件重要的事需要记住， &lt;strong&gt;编译器会首先查找 Go 的安装目录，然后才会按顺序查找 GOPATH 变量里列出的目录&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;如果编译器查遍 GOPATH 也没有找到要导入的包，那么在试图对程序执行 &lt;code&gt;run&lt;/code&gt; 或者 &lt;code&gt;build&lt;/code&gt; 的时候就会出错。可以通过 &lt;code&gt;go get&lt;/code&gt; 命令来修正这种错误。&lt;/p&gt;
&lt;h4 id=&#34;321-远程导入&#34;&gt;3.2.1 远程导入&lt;/h4&gt;
&lt;p&gt;目前的大势所趋是，使用分布式版本控制系统（Distributed Version Control Systems， DVCS）来分享代码，如 GitHub、 Launchpad 还有 Bitbucket。 Go 语言的工具链本身就支持从这些网站及类似网站获取源代码。 Go 工具链会使用导入路径确定需要获取的代码在网络的什么地方。例如：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;github.com/spf13/viper&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;用导入路径编译程序时， &lt;code&gt;go build&lt;/code&gt; 命令会使用 GOPATH 的设置，在磁盘上搜索这个包。事实上，这个导入路径代表一个 URL，指向 GitHub 上的代码库。&lt;strong&gt;如果路径包含 URL，可以使用 Go 工具链从DVCS 获取包，并把包的源代码保存在 GOPATH 指向的路径里与 URL 匹配的目录里。这个获取过程使用 &lt;code&gt;go get&lt;/code&gt; 命令完成。&lt;/strong&gt; &lt;code&gt;go get&lt;/code&gt; 将获取任意指定的 URL 的包，或者一个已经导入的包所依赖的其他包。由于 &lt;code&gt;go get&lt;/code&gt; 的这种&lt;strong&gt;递归&lt;/strong&gt;特性，这个命令会&lt;strong&gt;扫描某个包的源码树&lt;/strong&gt;，获取能找到的所有依赖包。&lt;/p&gt;
&lt;h4 id=&#34;322-命名导入&#34;&gt;3.2.2 命名导入&lt;/h4&gt;
&lt;p&gt;如果要导入的多个包具有相同的名字，会发生什么？例如，既需要 network/convert 包来转换从网络读取的数据，又需要 file/convert 包来转换从文本文件读取的数据时，就会同时导入两个名叫 convert 的包。这种情况下，重名的包可以通过命名导入来导入。命名导入是指，在 import 语句给出的包路径的&lt;strong&gt;左侧&lt;/strong&gt;定义一个名字，将导入的包命名为新名字。&lt;/p&gt;
&lt;p&gt;例如，若用户已经使用了标准库里的 fmt 包，现在要导入自己项目里名叫 fmt 的包，就可以如下所示的命名导入方式，在导入时重新命名自己的包：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kn&#34;&gt;package&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;main&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;s&#34;&gt;&amp;#34;fmt&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nx&#34;&gt;myfmt&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;mylib/fmt&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;main&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nx&#34;&gt;fmt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Println&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;Standard Library&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nx&#34;&gt;myfmt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Println&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;mylib/fmt&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;当导入了一个不在代码里使用的包时，Go 编译器会编译失败，并输出一个错误。 Go 开发团队认为，这个特性可以防止导入了未被使用的包，避免代码变得臃肿。虽然这个特性会让人觉得很烦，但 Go 开发团队仍然花了很大的力气说服自己，决定加入这个特性，用来避免其他编程语言里常常遇到的一些问题，如得到一个塞满未使用库的超大可执行文件。很多语言在这种情况会使用警告做提示，而 Go 开发团队认为，与其让编译器告警，不如直接失败更有意义。&lt;/p&gt;
&lt;p&gt;每个编译过大型 C 程序的人都知道，在浩如烟海的编译器警告里找到一条有用的信息是多么困难的一件事。这种情况下编译失败会更加明确。有时，用户可能需要导入一个包，但是不需要引用这个包的标识符。在这种情况，可以使用&lt;strong&gt;空白标识符 _&lt;/strong&gt; 来重命名这个导入。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;空白标识符&lt;/strong&gt;：下划线字符（_）在 Go 语言里称为空白标识符，有很多用法。这个标识符用来抛弃不想继续使用的值，如&lt;strong&gt;给导入的包赋予一个空名字&lt;/strong&gt;，或者&lt;strong&gt;忽略函数返回的不感兴趣的值&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;33-init-函数&#34;&gt;3.3 init 函数&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;每个包可以包含任意多个 init 函数，这些函数都会在程序执行开始的时候被调用&lt;/strong&gt;。所有被编译器发现的 init 函数都会安排在 main 函数之前执行。 init 函数用在&lt;strong&gt;设置包&lt;/strong&gt;、&lt;strong&gt;初始化变量&lt;/strong&gt;或者&lt;strong&gt;其他要在程序运行前优先完成的引导工作&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;以数据库驱动为例， database 下的驱动在启动时执行 init 函数会将自身注册到 sql 包里，因为 sql 包在编译时并不知道这些驱动的存在，等启动之后 sql 才能调用这些驱动。&lt;/p&gt;
&lt;h3 id=&#34;34-go-的工具&#34;&gt;3.4 Go 的工具&lt;/h3&gt;
&lt;p&gt;build 和 clean 命令会执行编译和清理的工作。当用户将代码签入源码库里的时候，开发人员可能并不想签入编译生成的文件。可以用 clean 命令解决这个问题调用 clean 后会删除编译生成的可执行文件。&lt;/p&gt;
&lt;p&gt;大部分 Go 工具的命令都会接受一个包名作为参数。build 命令可以简写。在不包含文件名时，go 工具会默认使用当前目录来编译。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;go&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;build&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;因为构建包是很常用的动作，所以也可以直接指定包：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;go&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;build&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;github&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;com&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;/&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;goinaction&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;/&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;code&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;/&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;chapter3&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;/&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;wordcount&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;也可以在指定包的时候使用通配符。3 个点表示匹配所有的字符串。&lt;/p&gt;
&lt;p&gt;例如，下面的命令会编译 chapter3 目录下的所有包：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;go&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;build&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;github&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;com&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;/&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;goinaction&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;/&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;code&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;/&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;chapter3&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;/...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;除了指定包，大部分 Go 命令使用短路径作为参数。&lt;/p&gt;
&lt;p&gt;例如，下面两条命令的效果相同：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;go&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;build&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;wordcount&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;go&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;go&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;build&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;要执行程序，需要首先编译，然后执行编译创建的 wordcount 或者 wordcount.exe 程序。不过这里有一个命令可以在一次调用中完成这两个操作：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;go&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;run&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;wordcount&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;go&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;go run 命令会先构建 wordcount.go 里包含的程序，然后执行构建后的程序。这样可以节省好多录入工作量。&lt;/p&gt;
&lt;h4 id=&#34;341-go-vet&#34;&gt;3.4.1 go vet&lt;/h4&gt;
&lt;p&gt;这个命令不会帮开发人员写代码，但如果开发人员已经写了一些代码，vet 命令会帮开发人员检测代码的常见错误：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Printf 类函数调用时，类型匹配错误的参数。&lt;/li&gt;
&lt;li&gt;定义常用的方法时，方法签名的错误。&lt;/li&gt;
&lt;li&gt;错误的结构标签。&lt;/li&gt;
&lt;li&gt;没有指定字段名的结构字面量。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;go vet 工具不能让开发者避免严重的逻辑错误，或者避免编写充满小错的代码。不过这个工具可以很好地捕获一部分常见错误。每次对代码先执行 govet 再将其签入源代码库是一个很好的习惯。&lt;/p&gt;
&lt;h4 id=&#34;342-go-fmt&#34;&gt;3.4.2 go fmt&lt;/h4&gt;
&lt;p&gt;fmt 是 Go 语言社区很喜欢的一个命令。fmt 工具会将开发人员的代码布局成和 Go 源代码类似的风格，不用再为了大括号是不是要放到行尾，或者用 tab（制表符）还是空格来做缩进而争论不休。使用 go fmt 后面跟文件名或者包名，就可以调用这个代码格式化工具。fmt 命令会自动格式化开发人员指定的源代码文件并保存。下面是一个代码执行 go fmt 前和执行 go fmt 后几行代码的对比：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;err&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;!=&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;err&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;在对这段代码执行 go fmt 后，会得到：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;err&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;!=&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;err&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;很多 Go 开发人员会配置他们的开发环境，在保存文件或者提交到代码库前执行 go fmt。&lt;/p&gt;
&lt;h4 id=&#34;343-go-doc&#34;&gt;3.4.3 go doc&lt;/h4&gt;
&lt;p&gt;Go 语言有两种方法为开发者生成文档。如果开发人员使用命令行提示符工作，可以在终端上直接使用 go doc 命令来打印文档。无需离开终端，即可快速浏览命令或者包的帮助。不过，如果开发人员认为一个浏览器界面会更有效率，可以使用 godoc 程序来启动一个 Web 服务器，通过点击的方式来查看 Go 语言的包的文档。Web 服务器 godoc 能让开发人员以网页的方式浏览自己的系统里的所有 Go 语言源代码的文档。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 从命令行获取文档&lt;/strong&gt;
对那种总会打开一个终端和一个文本编辑器（或者在终端内打开文本编辑器）的开发人员来说，go doc 是很好的选择。假设要用 Go 语言第一次开发读取 UNIX tar 文件的应用程序，想要看看 archive/tar 包的相关文档，就可以输入：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;go&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;doc&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;tar&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;2. 浏览文档 (未找到 godoc)&lt;/strong&gt;
Go 语言的文档也提供了浏览器版本。有时候，通过跳转到文档，查阅相关的细节，能更容易理解整个包或者某个函数。在这种情况下，会想使用 godoc 作为 Web 服务器。如果想通过 Web浏览器查看可以点击跳转的文档，下面就是得到这种文档的好方式。开发人员启动自己的文档服务器，只需要在终端会话中输入如下命令：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;godoc&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;http&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;=:&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;6060&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这个命令通知 godoc 在端口 6060 启动 Web 服务器。如果浏览器已经打开，导航到http://localhost:6060 可以看到一个页面，包含所有 Go 标准库和你的 GOPATH 下的 Go 源代码的文档。&lt;/p&gt;
&lt;p&gt;Go 官网就是通过一个略微修改过的 godoc 来提供文档服务的。要进入某个特定包的文档，只需要点击页面顶端的Packages。&lt;/p&gt;
&lt;p&gt;Go 文档工具最棒的地方在于，它也支持开发人员自己写的代码。如果开发人员遵从一个简单的规则来写代码，这些代码就会自动包含在 godoc 生成的文档里。为了在 godoc 生成的文档里包含自己的代码文档，开发人员需要用一些规则来写代码和注释。&lt;/p&gt;
&lt;h1 id=&#34;四数组切片和映射&#34;&gt;四、数组、切片和映射&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;数组是构造切片和映射的基石。&lt;/li&gt;
&lt;li&gt;Go 语言里切片经常用来处理数据的集合，映射用来处理具有键值对结构的数据。&lt;/li&gt;
&lt;li&gt;内置函数 make 可以创建切片和映射，并指定原始的长度和容量。也可以直接使用切片和映射字面量，或者使用字面量作为变量的初始值。&lt;/li&gt;
&lt;li&gt;切片有容量限制，不过可以使用内置的 append 函数扩展容量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;映射的增长没有容量或者任何限制&lt;/strong&gt;。&lt;strong&gt;（有疑问）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;内置函数 len 可以用来获取切片或者映射的长度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内置函数 cap 只能用于切片&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;通过组合，可以创建多维数组和多维切片。也可以使用切片或者其他映射作为映射的值。但是切片不能用作映射的键。&lt;/li&gt;
&lt;li&gt;将切片或者映射传递给函数成本很小，并且不会复制底层的数据结构。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Go 语言有 3 种数据结构可以让用户&lt;strong&gt;管理集合数据&lt;/strong&gt;：数组、切片和映射。这 3 种数据结构是语言核心的一部分，在标准库里被广泛使用。&lt;/p&gt;
&lt;h3 id=&#34;41-数组&#34;&gt;4.1 数组&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;数组是切片和映射的基础数据结构&lt;/strong&gt;。理解了数组的工作原理，有助于理解切片和映射提供的优雅和强大的功能。&lt;/p&gt;
&lt;h4 id=&#34;411-内部实现&#34;&gt;4.1.1 内部实现&lt;/h4&gt;
&lt;p&gt;在 Go 语言里，数组是一个&lt;strong&gt;长度固定&lt;/strong&gt;的数据类型，用于存储一段具有&lt;strong&gt;相同类型&lt;/strong&gt;的元素的&lt;strong&gt;连续块&lt;/strong&gt;。数组存储的类型可以是内置类型，如整型或者字符串，也可以是某种结构类型。&lt;/p&gt;
&lt;p&gt;数组占用的内存是&lt;strong&gt;连续分配的&lt;/strong&gt;。由于内存连续，CPU 能把正在使用的数据缓存更久的时间**（有疑问）**。而且内存连续很容易计算索引，可以快速迭代数组里的所有元素。数组的类型信息可以提供每次访问一个元素时需要在内存中移动的距离。既然数组的每个元素类型相同，又是连续分配，就可以以固定速度索引数组中的任意数据，速度非常快。&lt;/p&gt;
&lt;h4 id=&#34;412-声明和初始化&#34;&gt;4.1.2 声明和初始化&lt;/h4&gt;
&lt;p&gt;声明数组时需要指定内部存储的数据的类型，以及需要存储的元素的数量，这个数量也称为数组的长度。&lt;/p&gt;
&lt;p&gt;声明一个数组，并设置为零值。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 声明一个包含 5 个元素的整型数组
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;array&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;5&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;一旦声明，数组里存储的数据类型和数组长度就都不能改变了。如果需要存储更多的元素，就需要先创建一个更长的数组，再把原来数组里的值&lt;strong&gt;复制&lt;/strong&gt;到新数组里。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在 Go 语言中声明变量时，总会使用对应类型的零值来对变量进行初始化。数组也不例外&lt;/strong&gt;。当数组初始化时，数组内每个元素都初始化为对应类型的零值。&lt;/p&gt;
&lt;p&gt;一种快速创建数组并初始化的方式是使用数组字面量。数组字面量允许声明数组里元素的数
量同时指定每个元素的值。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 声明一个包含 5 个元素的整型数组
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 用具体值初始化每个元素
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;array&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;5&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;10&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;20&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;30&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;40&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;50&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如果使用&amp;hellip;替代数组的长度，Go 语言会根据初始化时数组元素的数量来确定该数组的长度。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 声明一个整型数组
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 用具体值初始化每个元素
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 容量由初始化值的数量决定
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;array&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;...&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;10&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;20&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;30&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;40&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;50&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;注&lt;/strong&gt;：也可以直接不写 &amp;lsquo;&amp;hellip;&amp;rsquo;&lt;/p&gt;
&lt;p&gt;如果知道数组的长度而且准备给某些值指定具体值，可以使用代码以下语法。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 声明一个有 5 个元素的数组
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 用具体值初始化索引为 1 和 2 的元素
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 其余元素保持零值
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;array&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;5&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;10&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;20&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;注&lt;/strong&gt;：也可以不写数组长度，此时数组的长度为索引最大值 + 1。&lt;/p&gt;
&lt;p&gt;声明一个所有元素都是指针的数组。使用 * 运算符就可以访问元素指针所指向的值。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 声明包含 5 个元素的指向整数指针的数组
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 用整型指针初始化索引为 0 和 1 的数组元素
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;array&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;5&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;new&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;new&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 为索引为 0 和 1 的元素赋值
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;array&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;array&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;20&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id=&#34;413-复制&#34;&gt;4.1.3 复制&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;在 Go 语言里，数组是一个值&lt;/strong&gt;。这意味着数组可以用在赋值操作中。变量名代表整个数组，因此，同样类型的数组可以赋值给另一个数组。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 声明第一个包含 5 个元素的字符串数组
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;array1&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;5&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 声明第二个包含 5 个元素的字符串数组
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 用颜色初始化数组
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;array2&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;5&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;Red&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;Blue&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;Green&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;Yellow&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;Pink&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 把 array2 的值复制到 array1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;array1&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;array2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;复制之后，两个数组的值完全一样。&lt;/p&gt;
&lt;p&gt;数组变量的类型包括数组长度和每个元素的类型。只有这两部分都相同的数组，才是类型相同的数组，才能互相赋值。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;复制数组指针，只会复制指针的值，而不会复制指针所指向的值&lt;/strong&gt;。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 声明第一个包含 3 个元素的指向字符串的指针数组
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;array1&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;3&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 声明第二个包含 3 个元素的指向字符串的指针数组
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 使用字符串指针初始化这个数组
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;array2&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;3&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;new&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;new&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;new&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 使用颜色为每个元素赋值
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;array2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;Red&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;array2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;Blue&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;array2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;Green&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 将 array2 复制给 array1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;array1&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;array2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;复制之后，两个数组指向&lt;strong&gt;同一组字符串&lt;/strong&gt;。&lt;/p&gt;
&lt;h4 id=&#34;414-多维数组&#34;&gt;4.1.4 多维数组&lt;/h4&gt;
&lt;p&gt;数组本身只有一个维度，不过可以组合多个数组创建多维数组。多维数组很容易管理具有父子关系的数据或者与坐标系相关联的数据。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 声明一个二维整型数组，两个维度分别存储 4 个元素和 2 个元素
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;array&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;4&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;][&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 使用数组字面量来声明并初始化一个二维整型数组
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;array&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;4&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;][&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{{&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;10&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;11&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;},&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;20&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;21&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;},&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;30&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;31&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;},&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;40&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;41&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 声明并初始化外层数组中索引为 1 个和 3 的元素
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;array&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;4&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;][&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;20&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;21&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;},&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;3&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;40&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;41&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 声明并初始化外层数组和内层数组的单个元素
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;array&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;4&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;][&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;20&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;},&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;3&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;41&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;只要类型一致，就可以将多维数组互相赋值。多维数组的类型包括&lt;strong&gt;每一维度的长度&lt;/strong&gt;以及最终存储在元素中的数据的类型。&lt;/p&gt;
&lt;p&gt;下面是同样类型的多维数组赋值。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 声明两个不同的二维整型数组
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;array1&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;][&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;array2&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;][&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 为每个元素赋值
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;array2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;][&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;array2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;][&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;20&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;array2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;][&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;30&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;array2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;][&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;40&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 将 array2 的值复制给 array1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;array1&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;array2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;因为每个数组都是一个值，所以&lt;strong&gt;可以独立复制某个维度&lt;/strong&gt;。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 将 array1 的索引为 1 的维度复制到一个同类型的新数组里
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;array3&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;array1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id=&#34;415-在函数间传递数组&#34;&gt;4.1.5 在函数间传递数组&lt;/h4&gt;
&lt;p&gt;根据内存和性能来看，在函数间传递数组是一个&lt;strong&gt;开销很大&lt;/strong&gt;的操作。&lt;strong&gt;在函数之间传递变量时，总是以值的方式传递的&lt;/strong&gt;。如果这个变量是一个数组，意味着整个数组，不管有多长，都会完整复制，并传递给函数。&lt;/p&gt;
&lt;p&gt;为了考察这个操作，我们来创建一个包含 100 万个 int 类型元素的数组。在 64 位架构上，
这将需要 800 万字节，即 8 MB 的内存。&lt;/p&gt;
&lt;p&gt;使用值传递，在函数间传递大数组。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 声明一个需要 8 MB 的数组
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;array&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mf&#34;&gt;1e6&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 将数组传递给函数 foo
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;foo&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;array&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 函数 foo 接受一个 100 万个整型值的数组
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;foo&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;array&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mf&#34;&gt;1e6&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;o&#34;&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;注&lt;/strong&gt;：foo 函数声明的参数列表里的 1e6 必须要写。&lt;/p&gt;
&lt;p&gt;每次函数 foo 被调用时，必须在栈上分配 8 MB 的内存。之后，整个数组的值（8 MB 的内存）被复制到刚分配的内存里。虽然 Go 语言自己会处理这个复制操作，不过还有一种更好且更有效的方法来处理这个操作。&lt;/p&gt;
&lt;p&gt;可以只传入指向数组的指针，这样只需要复制 &lt;strong&gt;8 字节&lt;/strong&gt;的数据而不是 8 MB 的内存数据到栈上。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 分配一个需要 8 MB 的数组
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;array&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mf&#34;&gt;1e6&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 将数组的地址传递给函数 foo
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;foo&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;array&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 函数 foo 接受一个指向 100 万个整型值的数组的指针
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;foo&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;array&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mf&#34;&gt;1e6&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;o&#34;&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这次函数 foo 接受一个指向 100 万个整型值的数组的指针。现在将数组的地址传入函数，只需要在栈上分配 8 字节的内存给指针就可以。&lt;/p&gt;
&lt;p&gt;这个操作会更有效地利用内存，性能也更好。不过要意识到，因为现在传递的是指针，所以如果改变指针指向的值，会改变共享的内存。此时使用&lt;strong&gt;切片&lt;/strong&gt;能更好地处理这类共享问题。&lt;/p&gt;
&lt;h3 id=&#34;42-切片&#34;&gt;4.2 切片&lt;/h3&gt;
&lt;p&gt;切片是一种数据结构，这种数据结构便于使用和管理数据集合。切片是围绕&lt;strong&gt;动态数组&lt;/strong&gt;的概念构建的，&lt;strong&gt;可以按需自动增长和缩小&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;切片的动态增长是通过&lt;strong&gt;内置函数 append&lt;/strong&gt; 来实现的。这个函数可以快速且高效地增长切片。&lt;strong&gt;还可以通过对切片再次切片来缩小一个切片的大小&lt;/strong&gt;。因为切片的底层内存也是在&lt;strong&gt;连续块&lt;/strong&gt;中分配的，所以切片还能获得&lt;strong&gt;索引&lt;/strong&gt;、&lt;strong&gt;迭代&lt;/strong&gt;以及&lt;strong&gt;为垃圾回收优化&lt;/strong&gt;的好处。&lt;/p&gt;
&lt;h4 id=&#34;421-内部实现&#34;&gt;4.2.1 内部实现&lt;/h4&gt;
&lt;p&gt;切片是一个很小的对象，对底层&lt;strong&gt;数组&lt;/strong&gt;进行了抽象，并提供相关的操作方法。切片有 3 个字段，包含了 Go 语言需要操作底层数组的元数据。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2023/03/19/dqlH6OpUDCcatZj.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;这 3 个字段分别是指向底层数组的指针、切片访问的元素的个数（即长度）和切片允许增长到的元素个数（即容量）。&lt;/p&gt;
&lt;h4 id=&#34;422-创建和初始化&#34;&gt;4.2.2 创建和初始化&lt;/h4&gt;
&lt;p&gt;Go 语言中有几种方法可以创建和初始化切片。是否能提前知道切片需要的容量通常会决定要如何创建切片。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. make 和切片字面量&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;一种创建切片的方法是使用&lt;strong&gt;内置的 make 函数&lt;/strong&gt;。当使用 make 时，需要传入一个参数，指定切片的长度。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 创建一个字符串切片
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 其长度和容量都是 5 个元素
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;slice&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;make&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;([]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;5&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如果只指定长度，那么切片的容量和长度相等。也可以分别指定长度和容量。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 创建一个整型切片
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 其长度为 3 个元素，容量为 5 个元素
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;slice&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;make&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;([]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;3&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;5&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;分别指定长度和容量时创建的切片，&lt;strong&gt;底层数组的长度是指定的&lt;!-- raw HTML omitted --&gt;容量&lt;!-- raw HTML omitted --&gt;&lt;/strong&gt;，但是初始化后并不能访问所有的数组元素。下图描述了上述代码里声明的整型切片在初始化并存入一些值后的样子。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2023/03/19/dqlH6OpUDCcatZj.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;上述代码的切片可以访问 3 个元素，而底层数组拥有 5 个元素。剩余的 2 个元素可以在后期操作中&lt;strong&gt;合并&lt;/strong&gt;到切片，可以通过切片访问这些元素。&lt;strong&gt;如果基于这个切片创建新的切片，新切片会和原有切片共享底层数组，也能通过后期操作来访问多余容量的元素&lt;/strong&gt;。&lt;strong&gt;有疑问&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;不允许创建容量小于长度的切片。&lt;/p&gt;
&lt;p&gt;另一种常用的创建切片的方法是使用&lt;strong&gt;切片字面量&lt;/strong&gt;。这种方法和创建数组类似，只是不需要指定[]运算符里的值。&lt;strong&gt;初始的长度和容量会基于初始化时提供的元素的个数确定&lt;/strong&gt;。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 创建字符串切片
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 其长度和容量都是 5 个元素
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;slice&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;Red&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;Blue&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;Green&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;Yellow&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;Pink&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 创建一个整型切片
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 其长度和容量都是 3 个元素
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;slice&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;10&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;20&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;30&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;当使用切片字面量时，可以设置初始长度和容量。要做的就是&lt;strong&gt;在初始化时给出所需的长度和容量作为索引&lt;/strong&gt;。下面的语法展示了如何创建长度和容量都是 100 个元素的切片。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 创建字符串切片
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 使用空字符串初始化第 100 个元素
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;slice&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;99&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;记住，如果在 [] 运算符里指定了一个值，那么创建的就是数组而不是切片。只有不指定值的时候，才会创建切片。&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 创建有 3 个元素的整型数组
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;array&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;3&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;10&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;20&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;30&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 创建长度和容量都是 3 的整型切片
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;slice&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;10&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;20&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;30&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;2. nil 和空切片&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;有时，程序可能需要声明一个值为 nil 的切片（也称 nil 切片）。只要在声明时不做任何初始化，就会创建一个 nil 切片。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 创建 nil 整型切片
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;slice&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;在 Go 语言里，nil 切片是很常见的创建切片的方法。nil 切片可以用于很多标准库和内置函数。在需要描述一个不存在的切片时，nil 切片会很好用。例如，函数要求返回一个切片但是发生异常的时候。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2023/03/19/uPS8yLlhWUNOsrz.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;利用初始化，通过声明一个切片可以创建一个空切片。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 使用 make 创建空的整型切片
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;slice&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;make&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;([]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 使用切片字面量创建空的整型切片
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;slice&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;空切片在底层数组包含 0 个元素，也&lt;strong&gt;没有分配任何存储空间&lt;/strong&gt;。想表示空集合时空切片很有用，比如数据库查询返回 0 个查询结果时。&lt;/p&gt;
&lt;p&gt;不管是使用 nil 切片还是空切片，对其调用内置函数 append、len 和 cap 的效果都是一样的。&lt;/p&gt;
&lt;h4 id=&#34;423-使用切片&#34;&gt;4.2.3 使用切片&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;1．赋值和切片&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;对切片里某个索引指向的元素赋值和对数组里某个索引指向的元素赋值的方法完全一样。使用 [] 操作符就可以改变某个元素的值。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 创建一个整型切片
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 其容量和长度都是 5 个元素
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;slice&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;10&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;20&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;30&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;40&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;50&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 改变索引为 1 的元素的值
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;slice&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;25&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;切片之所以被称为切片，是因为创建一个新的切片就是把底层数组切出一部分&lt;/strong&gt;。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 创建一个整型切片
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 其长度和容量都是 5 个元素
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;slice&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;10&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;20&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;30&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;40&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;50&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 创建一个新切片
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 其长度为 2 个元素，容量为 4 个元素
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;newSlice&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;slice&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;3&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;执行完上面的切片动作后，我们有了两个切片，它们&lt;strong&gt;共享同一段底层数组&lt;/strong&gt;，但通过不同的切片会看到底层数组的不同部分。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2023/03/19/c3yRkASKgLCE7DM.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;第一个切片 slice 能够看到底层数组全部 5 个元素的容量，不过之后的 newSlice 就看不到。对于 newSlice ，底层数组的容量只有 4 个元素。newSlice 无法访问到它所指向的底层数组的第一个元素之前的部分。所以，对 newSlice 来说，之前的那些元素就是不存在的。&lt;/p&gt;
&lt;p&gt;使用下面的公式，可以计算出任意切片的长度和容量。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对底层数组容量是 k 的切片 slice[i : j] 来说：
长度 = j - i
容量 = k - i&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如，对 newSlice 应用这个公式。对底层数组容量是 5 的切片 slice[1:3] 来说
长度: 3 - 1 = 2
容量: 5 - 1 = 4&lt;/p&gt;
&lt;p&gt;需要记住的是，现在两个切片共享同一个底层数组。如果一个切片修改了该底层数组的共享部分，另一个切片也能感知到。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 创建一个整型切片
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 其长度和容量都是 5 个元素
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;slice&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;10&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;20&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;30&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;40&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;50&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 创建一个新切片
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 其长度是 2 个元素，容量是 4 个元素
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;newSlice&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;slice&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;3&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 修改 newSlice 索引为 1 的元素
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 同时也修改了原来的 slice 的索引为 2 的元素
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;newSlice&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;35&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;把 35 赋值给 newSlice 的第二个元素（索引为 1 的元素）的同时也是在修改原来的 slice的第 3 个元素（索引为 2 的元素）。&lt;/p&gt;
&lt;p&gt;切片只能访问到其&lt;strong&gt;长度&lt;/strong&gt;内的元素。试图访问超出其长度的元素将会导致语言运行时异常。与切片的&lt;strong&gt;容量&lt;/strong&gt;相关联的元素只能用于增长切片。在使用这部分元素前，必须将其&lt;strong&gt;合并&lt;/strong&gt;到切片的&lt;strong&gt;长度&lt;/strong&gt;里。&lt;/p&gt;
&lt;p&gt;切片有额外的&lt;strong&gt;容量&lt;/strong&gt;是很好，但是如果不能把这些容量合并到切片的长度里，这些容量就没有用处。好在可以用 Go 语言的&lt;strong&gt;内置函数 append&lt;/strong&gt; 来做这种合并很容易。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2．切片增长&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;相对于数组而言，使用切片的一个好处是，可以按需增加切片的容量。Go 语言内置的 append 函数会处理增加长度时的所有操作细节。&lt;/p&gt;
&lt;p&gt;要使用 append，需要一个被操作的切片和一个要追加的值。当 append 调用返回时，会返回一个包含修改结果的新切片。函数 append 总是会增加新切片的长度，而&lt;strong&gt;容量有可能会改变，也可能不会改变&lt;/strong&gt;，这取决于被操作的切片的可用容量。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 创建一个整型切片
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 其长度和容量都是 5 个元素
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;slice&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;10&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;20&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;30&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;40&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;50&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 创建一个新切片
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 其长度为 2 个元素，容量为 4 个元素
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;newSlice&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;slice&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;3&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 使用原有的容量来分配一个新元素
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 将新元素赋值为 60
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;newSlice&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;append&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;newSlice&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;60&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;append 操作完成后，两个切片和底层数组的布局如图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2023/03/19/z3IQfS8BHaPig1G.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;因为 newSlice 在底层数组里还有额外的容量可用，append 操作将可用的元素合并到切片的长度，并对其进行赋值。由于和原始的 slice 共享同一个底层数组，&lt;!-- raw HTML omitted --&gt;slice 中索引为 3 的元素的值也被改动了&lt;!-- raw HTML omitted --&gt;。&lt;/p&gt;
&lt;p&gt;如果切片的底层数组没有足够的可用容量，append 函数会创建一个&lt;strong&gt;新的底层数组&lt;/strong&gt;，将被引用的现有的值&lt;strong&gt;复制&lt;/strong&gt;到新数组里，再追加新的值。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 创建一个整型切片
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 其长度和容量都是 4 个元素
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;slice&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;10&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;20&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;30&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;40&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 向切片追加一个新元素
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 将新元素赋值为 50
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;newSlice&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;append&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;slice&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;50&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;当这个 append 操作完成后，newSlice 拥有一个全新的底层数组，这个数组的容量是原来的两倍。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2023/03/19/H1PAVMxrWEFNGeZ.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注&lt;/strong&gt;：注意新的长度和容量并不是原来的两倍。&lt;/p&gt;
&lt;p&gt;函数 append 会智能地处理底层数组的容量增长。&lt;strong&gt;在切片的容量小于 1000 个元素时，总是会成倍地增加容量。一旦元素个数超过 1000，容量的增长因子会设为 1.25，也就是会每次增加 25%的容量&lt;/strong&gt;。随着语言的演化，这种增长算法可能会有所改变。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 创建切片时的 第 3 个索引&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;略&lt;/p&gt;
&lt;p&gt;其中一些重要的：&lt;/p&gt;
&lt;p&gt;如果在创建切片时设置切片的容量和长度一样，就可以强制让新切片的第一个 append 操作创建新的底层数组，与原有的底层数组&lt;strong&gt;分离&lt;/strong&gt;。新切片与原有的底层数组分离后，可以安全地进行后续修改。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;内置函数 append 是一个可变参数的函数&lt;/strong&gt;。这意味着可以在一次调用传递多个追加的值。如果使用 &amp;hellip;运算符，可以将一个切片的所有元素追加到另一个切片里。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 创建两个切片，并分别用两个整数进行初始化
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;s1&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;s2&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;3&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;4&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 将两个切片追加在一起，并显示结果
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;fmt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Printf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;%v\n&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;append&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;s1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;s2&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;...&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Output:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;2&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;3&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;4&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;4. 迭代切片&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;既然切片是一个集合，可以迭代其中的元素。Go 语言有个特殊的关键字 &lt;strong&gt;range&lt;/strong&gt;，它可以配合关键字 for 来迭代切片里的元素。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 创建一个整型切片
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 其长度和容量都是 4 个元素
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;slice&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;10&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;20&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;30&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;40&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 迭代每一个元素，并显示其值
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;index&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;value&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;range&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;slice&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;nx&#34;&gt;fmt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Printf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;Index: %d Value: %d\n&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;index&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;value&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Output:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;Index: 0 Value: 10
Index: 1 Value: 20
Index: 2 Value: 30
Index: 3 Value: 40
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;当迭代切片时，关键字 range 会返回两个值。第一个值是当前迭代到的索引位置，第二个值是该位置对应元素值的一份&lt;strong&gt;副本&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2023/03/19/5lX6upz1vgyfR8Q.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;需要强调的是，range 创建了每个元素的&lt;strong&gt;副本&lt;/strong&gt;，而不是直接返回对该元素的引用。&lt;strong&gt;如果使用该值变量的地址作为指向每个元素的指针，就会造成错误&lt;/strong&gt;。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 创建一个整型切片
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 其长度和容量都是 4 个元素
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;slice&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;10&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;20&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;30&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;40&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 迭代每个元素，并显示值和地址
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;index&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;value&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;range&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;slice&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;nx&#34;&gt;fmt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Printf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;Value: %d Value-Addr: %X ElemAddr: %X\n&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nx&#34;&gt;value&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;value&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;slice&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;index&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;])&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Output:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;Value: 10 Value-Addr: C00000A1A0 ElemAddr: C000010440
Value: 20 Value-Addr: C00000A1A0 ElemAddr: C000010448
Value: 30 Value-Addr: C00000A1A0 ElemAddr: C000010450
Value: 40 Value-Addr: C00000A1A0 ElemAddr: C000010458
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;因为&lt;strong&gt;迭代返回的变量是一个迭代过程中根据切片依次赋值的新变量&lt;/strong&gt;，所以 value 的地址总是相同的。&lt;strong&gt;要想获取每个元素的地址，可以使用切片变量和索引值&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;如果不需要索引值，可以使用占位字符来忽略这个值。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 创建一个整型切片
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 其长度和容量都是 4 个元素
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;slice&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;10&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;20&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;30&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;40&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 迭代每个元素，并显示其值
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;_&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;value&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;range&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;slice&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;nx&#34;&gt;fmt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Printf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;Value: %d\n&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;value&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Output:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;Value: 10
Value: 20
Value: 30
Value: 40
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;关键字 range 总是会从&lt;strong&gt;切片头部&lt;/strong&gt;开始迭代。如果想对迭代做更多的控制，依旧可以使用传统的 for 环。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 创建一个整型切片
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 其长度和容量都是 4 个元素
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;slice&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;10&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;20&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;30&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;40&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 从第三个元素开始迭代每个元素
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;index&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;index&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;len&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;slice&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;index&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;++&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;nx&#34;&gt;fmt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Printf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;Index: %d Value: %d\n&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;index&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;slice&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;index&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;])&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Output:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;Index: 2 Value: 30
Index: 3 Value: 40
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;有两个特殊的&lt;strong&gt;内置函数 len 和 cap&lt;/strong&gt;，可以用于处理&lt;strong&gt;数组&lt;/strong&gt;、&lt;strong&gt;切片&lt;/strong&gt;和&lt;strong&gt;通道&lt;/strong&gt;。对于切片，函数 len 返回切片的&lt;strong&gt;长度&lt;/strong&gt;，函数 cap 返回切片的&lt;strong&gt;容量&lt;/strong&gt;。&lt;/p&gt;
&lt;h4 id=&#34;424-多维切片&#34;&gt;4.2.4 多维切片&lt;/h4&gt;
&lt;p&gt;和数组一样，切片是一维的。不过，可以组合多个切片形成多维切片。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 创建一个整型切片的切片
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;slice&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[][]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{{&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;10&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;},&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;100&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;200&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;内置函数 append 也可以应用到组合后的切片上。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 创建一个整型切片的切片
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;slice&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[][]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{{&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;10&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;},&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;100&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;200&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 为第一个切片追加值为 20 的元素
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;slice&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;append&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;slice&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;],&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;20&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;当上述代码操作完成后，会为新的整型切片分配新的底层数组，然后将切片复制到外层切片的索引为 0 的元素。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2023/03/19/xNGH6nAKcXhMQmy.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;即便是这么简单的多维切片，操作时也会涉及众多布局和值。看起来在函数间像这样传递数据结构也会很复杂。不过切片本身结构很简单，可以以很小的成本在函数间传递。&lt;/p&gt;
&lt;h4 id=&#34;425-在函数间传递切片&#34;&gt;4.2.5 在函数间传递切片&lt;/h4&gt;
&lt;p&gt;在函数间传递切片就是要在&lt;strong&gt;函数间以值的方式传递切片&lt;/strong&gt;。由于切片的尺寸很小，在函数间复制和传递切片成本也很低。&lt;/p&gt;
&lt;p&gt;下例创建一个大切片，并将这个切片以值的方式传递给函数 foo。函数调用之后两个切片指向同一个底层数组。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 分配包含 100 万个整型值的切片
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;slice&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;make&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;([]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;1e6&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 将 slice 传递到函数 foo
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;slice&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;foo&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;slice&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 函数 foo 接收一个整型切片，并返回这个切片
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;foo&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;slice&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;o&#34;&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;slice&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;在 64 位架构的机器上，一个切片需要 &lt;strong&gt;24 字节&lt;/strong&gt;的内存：指针字段需要 8 字节，长度和容量字段分别需要 8 字节。由于&lt;strong&gt;与切片关联的数据包含在底层数组里，不属于切片本身&lt;/strong&gt;，所以将切片复制到任意函数的时候，对底层数组大小都不会有影响。复制时只会复制切片本身，不会涉及底层数组。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2023/03/19/3q27lQdtkFB6zCc.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;在函数间传递 24 个字节的数据会非常快速、简单。这也是切片效率高的地方。不需要传递指针和处理复杂的语法，只需要复制切片，按想要的方式修改数据，然后传递回一份新的切片副本。&lt;/p&gt;
&lt;h3 id=&#34;43-映射&#34;&gt;4.3 映射&lt;/h3&gt;
&lt;p&gt;映射是一个存储键值对的无序集合。&lt;/p&gt;
&lt;p&gt;映射是一种数据结构，用于存储一系列无序的键值对。映射里基于键来存储值。映射功能强大的地方是，能够基于键快速检索数据。键就像索引一样，指向与该键关联的值。&lt;/p&gt;
&lt;h4 id=&#34;431-内部实现&#34;&gt;4.3.1 内部实现&lt;/h4&gt;
&lt;p&gt;映射是一个集合，可以使用类似处理数组和切片的方式迭代映射中的元素。但映射是&lt;strong&gt;无序的集合&lt;/strong&gt;，意味着没有办法预测键值对被返回的&lt;strong&gt;顺序&lt;/strong&gt;。&lt;strong&gt;即便使用同样的顺序保存键值对，每次迭代映射的时候顺序也可能不一样&lt;/strong&gt;。（&lt;strong&gt;有疑问&lt;/strong&gt;,&lt;strong&gt;事实确实如此&lt;/strong&gt;）无序的原因是映射的实现使用了&lt;strong&gt;散列表&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2023/03/19/AbHx8FUh6GYaoDt.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;映射的散列表包含一组桶。在存储、删除或者查找键值对的时候，所有操作都要先选择一个桶。把操作映射时指定的键传给映射的散列函数，就能选中对应的桶。这个散列函数的目的是生成一个索引，这个索引最终将键值对分布到所有可用的桶里。&lt;/p&gt;
&lt;p&gt;随着映射存储的增加，索引分布越均匀，访问键值对的速度就越快。如果你在映射里存储了 10 000 个元素，你不希望每次查找都要访问 10 000 个键值对才能找到需要的元素，你希望查找键值对的次数越少越好。对于有 10000 个元素的映射，每次查找只需要查找 8 个键值对才是一个分布得比较好的映射。映射通过合理数量的桶来平衡键值对的分布。&lt;/p&gt;
&lt;p&gt;Go 语言的映射生成散列键的过程比图 4-25 展示的过程要稍微长一些，不过大体过程是类似的。在我们的例子里，键是字符串，代表颜色。这些字符串会转换为一个数值（散列值）。这个数值落在映射已有桶的序号范围内表示一个可以用于存储的桶的序号。之后，这个数值就被用于选择桶，用于存储或者查找指定的键值对。&lt;strong&gt;对 Go 语言的映射来说，生成的散列键的一部分，具体来说是低位（LOB），被用来选择桶&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;如果再仔细看看图 4-24，就能看出桶的内部实现。映射使用两个数据结构来存储数据。第一个数据结构是一个数组，内部存储的是用于选择桶的散列键的高八位值。这个数组用于区分每个键值对要存在哪个桶里。第二个数据结构是一个字节数组，用于存储键值对。该字节数组先依次存储了这个桶里所有的键，之后依次存储了这个桶里所有的值。实现这种键值对的存储方式目的在于减少每个桶所需的内存。&lt;strong&gt;有疑问&lt;/strong&gt;&lt;/p&gt;
&lt;h4 id=&#34;432-创建和初始化&#34;&gt;4.3.2 创建和初始化&lt;/h4&gt;
&lt;p&gt;Go 语言中有很多种方法可以创建并初始化映射，可以使用&lt;strong&gt;内置的 make 函数&lt;/strong&gt;，也可以使用&lt;strong&gt;映射字面量&lt;/strong&gt;。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 创建一个映射，键的类型是 string，值的类型是 int
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;dict&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;make&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;map&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 创建一个映射，键和值的类型都是 string
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 使用两个键值对初始化映射
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;dict&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;map&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;Red&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;#da1337&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;Orange&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;#e95a22&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;创建映射时更常用的方法是使用映射字面量。映射的初始长度会根据初始化时指定的键值对的数量来确定。&lt;/p&gt;
&lt;p&gt;映射的键可以是任何值。这个值的类型可以是内置的类型，也可以是结构类型，&lt;strong&gt;只要这个值可以使用 == 运算符做比较&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;切片、函数以及包含切片的结构类型&lt;/strong&gt;这些类型由于具有&lt;strong&gt;引用语义&lt;/strong&gt;，&lt;strong&gt;不能作为映射的键&lt;/strong&gt;，使用这些类型会造成编译错误。&lt;strong&gt;有疑问&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;dict&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;map&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[[]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Output:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;Compiler Exception:
invalid map key type []string
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;可以使用切片作为映射的值。这个在使用一个映射键对应一组数据时，会非常有用。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 创建一个映射，使用字符串切片作为值
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;dict&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;map&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;][]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id=&#34;433-使用映射&#34;&gt;4.3.3 使用映射&lt;/h4&gt;
&lt;p&gt;键值对赋值给映射，是通过指定适当类型的键并给这个键赋一个值来完成的。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 创建一个空映射，用来存储颜色以及颜色对应的十六进制代码
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;colors&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;map&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 将 Red 的代码加入到映射
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;colors&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;Red&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;#da1337&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;可以通过声明一个未初始化的映射来创建一个值为 nil 的映射（称为 nil 映射 ）。nil 映射不能用于存储键值对，否则，会产生一个语言运行时错误。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 通过声明映射创建一个 nil 映射
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;colors&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;map&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 将 Red 的代码加入到映射
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;colors&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;Red&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;#da1337&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Output:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;Runtime Error:
panic: runtime error: assignment to entry in nil map
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;测试映射里是否存在某个键是映射的一个重要操作。这个操作允许用户写一些逻辑来确定是否完成了某个操作或者是否在映射里缓存了特定数据。这个操作也可以用来比较两个映射，来确定哪些键值对互相匹配，哪些键值对不匹配。&lt;/p&gt;
&lt;p&gt;从映射取值时有两个选择。第一个选择是，可以同时获得值，以及一个表示这个键是否存在的标志。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 获取键 Blue 对应的值
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;value&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;exists&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;colors&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;Blue&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 这个键存在吗？
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;exists&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;nx&#34;&gt;fmt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Println&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;value&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;在 Go 语言里，通过键来索引映射时，即便这个键不存在也总会返回一个值。在这种情况下，返回的是该值对应的类型的零值。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;迭代映射里的所有值和迭代数组或切片一样，使用关键字 range。但对映射来说，range 返回的不是索引和值，而是&lt;strong&gt;键值对&lt;/strong&gt;。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 创建一个映射，存储颜色以及颜色对应的十六进制代码
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;colors&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;map&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;s&#34;&gt;&amp;#34;AliceBlue&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;#f0f8ff&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;s&#34;&gt;&amp;#34;Coral&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;#ff7F50&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;s&#34;&gt;&amp;#34;DarkGray&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;#a9a9a9&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;s&#34;&gt;&amp;#34;ForestGreen&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;#228b22&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 显示映射里的所有颜色
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;key&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;value&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;range&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;colors&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;nx&#34;&gt;fmt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Printf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;Key: %s Value: %s\n&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;key&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;value&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如果想把一个键值对从映射里删除，就使用&lt;strong&gt;内置的 delete 函数&lt;/strong&gt;。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 删除键为 Coral 的键值对
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;delete&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;colors&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;Coral&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id=&#34;434-在函数间传递映射&#34;&gt;4.3.4 在函数间传递映射&lt;/h4&gt;
&lt;p&gt;在函数间传递映射&lt;strong&gt;并不会制造出该映射的一个副本&lt;/strong&gt;。实际上，当传递映射给一个函数，并对这个映射做了修改时，所有对这个映射的引用都会察觉到这个修改。这个特性和切片类似，保证可以用很小的成本来复制映射。&lt;/p&gt;
&lt;h1 id=&#34;五go-语言的类型系统&#34;&gt;五、Go 语言的类型系统&lt;/h1&gt;
&lt;p&gt;Go 语言是一种&lt;strong&gt;静态类型&lt;/strong&gt;的编程语言。这意味着，编译器需要在编译时知晓程序里每个值的类型。如果提前知道类型信息，编译器就可以确保程序合理地使用值。这有助于减少潜在的内存异常和 bug，并且使编译器有机会对代码进行一些性能优化，提高执行效率。&lt;/p&gt;
&lt;p&gt;值的类型给编译器提供两部分信息：第一部分，需要分配多少内存给这个值（即值的规模）；第二部分，这段内存表示什么。对于许多内置类型的情况来说，规模和表示是类型名的一部分。int64 类型的值需要 8 字节（64 位），表示一个整数值；float32 类型的值需要 4 字节（32 位），表示一个 IEEE-754 定义的二进制浮点数；bool 类型的值需要 1 字节（8 位），表示布尔值 true 和 false。&lt;/p&gt;
&lt;p&gt;有些类型的内部表示与编译代码的机器的体系结构有关。例如，根据编译所在的机器的体系结构，一个 int 值的大小可能是 8 字节（64 位），也可能是 4 字节（32 位）。还有一些与体系结构相关的类型，如 Go 语言里的所有引用类型。好在创建和使用这些类型的值的时候，不需要了解这些与体系结构相关的信息。但是，如果编译器不知道这些信息，就无法阻止用户做一些导致程序受损甚至机器故障的事情。&lt;/p&gt;
&lt;h3 id=&#34;51-用户定义的类型&#34;&gt;5.1 用户定义的类型&lt;/h3&gt;
&lt;p&gt;Go 语言允许用户定义类型。当用户声明一个新类型时，这个声明就给编译器提供了一个框架，告知必要的内存大小和表示信息。声明后的类型与内置类型的运作方式类似。Go 语言里声明用户定义的类型有两种方法。最常用的方法是使用&lt;strong&gt;关键字 struct&lt;/strong&gt;，它可以让用户创建一个结构类型。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;type&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;user&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;nx&#34;&gt;name&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;nx&#34;&gt;email&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;nx&#34;&gt;ext&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;nx&#34;&gt;privileged&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;bool&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;结构类型通过组合一系列固定且唯一的字段来声明。结构里每个字段都会用一个已知类型声明。这个已知类型可以是内置类型，也可以是其他用户定义的类型。&lt;/p&gt;
&lt;p&gt;一旦声明了类型，就可以使用这个类型创建值。当声明变量时，这个变量对应的值总是会被初始化。这个值要么用指定的值初始化，要么用零值（即变量类型的默认值）做初始化。对数值类型来说，零值是 0；对字符串来说，零值是空字符串；对布尔类型，零值是 false。&lt;/p&gt;
&lt;p&gt;任何时候，创建一个变量并初始化为其零值，习惯是使用关键字 var。这种用法是为了更明确地表示一个变量被设置为零值。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;bill&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;user&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如果变量被初始化为某个非零值，就配合&lt;strong&gt;结构字面量&lt;/strong&gt;和&lt;strong&gt;短变量声明操作符&lt;/strong&gt;来创建变量。&lt;/p&gt;
&lt;p&gt;一个短变量声明操作符在一次操作中完成两件事情：声明一个变量，并初始化。短变量声明操作符会使用右侧给出的类型信息作为声明变量的类型。&lt;/p&gt;
&lt;p&gt;结构字面量可以对结构类型采用两种形式。第一种形式在不同行声明每个字段的名字以及对应的值。字段名与值用冒号分隔，每一行以逗号结尾。这种形式对字段的声明顺序没有要求。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;alice&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;user&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;nx&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;Alice&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;nx&#34;&gt;email&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;alice@foxmail.com&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;nx&#34;&gt;ext&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;123&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;nx&#34;&gt;privileged&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;第二种形式没有字段名，只声明对应的值。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;lisa&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;user&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;Lisa&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;lisa@email.com&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;123&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;每个值也可以分别占一行，不过习惯上这种形式会写在一行里，结尾不需要逗号。这种形式下，值的顺序很重要，必须要和结构声明中字段的顺序一致。&lt;/p&gt;
&lt;p&gt;另一种声明用户定义的类型的方法是，基于一个已有的类型，将其&lt;strong&gt;作为新类型的类型说明&lt;/strong&gt;。当需要一个可以用已有类型表示的新类型的时候，这种方法会非常好用。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;type&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;Duration&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;int64&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;标准库使用这种声明类型的方法，从内置类型创建出很多更加明确的类型，并赋予更高级的功能。&lt;/p&gt;
&lt;p&gt;上述代码展示的是标准库的 time 包里的一个类型的声明。Duration 是一种描述时间间隔的类型，单位是纳秒（ns）。这个类型使用内置的 int64 类型作为其表示。在 Duration 类型的声明中，把 int64 类型叫作 Duration 的基础类型。&lt;/p&gt;
&lt;p&gt;不过，虽然 int64 是基础类型，Go 并不认为 Duration 和 int64 是同一种类型。这两个类型是完全不同的有区别的类型。类型 int64 的值不能作为类型 Duration 的值来用。换句话说，虽然 int64 类型是基础类型，Duration 类型依然是一个独立的类型。两种不同类型的值即便互相兼容，也不能互相赋值。&lt;strong&gt;编译器不会对不同类型的值做隐式转换&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;52-方法&#34;&gt;5.2 方法&lt;/h3&gt;
&lt;p&gt;方法能给用户定义的类型添加新的行为。&lt;strong&gt;方法实际上也是函数&lt;/strong&gt;，只是在声明时，&lt;strong&gt;在关键字 func 和方法名之间增加了一个参数&lt;/strong&gt;。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// user 在程序里定义一个用户类型
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;type&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;user&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;nx&#34;&gt;name&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;nx&#34;&gt;email&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// notify 使用值接收者实现了一个方法
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;u&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;user&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;notify&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;nx&#34;&gt;fmt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Printf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;Sending User Email To %s&amp;lt;%s&amp;gt;\n&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;u&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;u&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;email&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// changeEmail 使用指针接收者实现了一个方法
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;u&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;user&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;changeEmail&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;email&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;nx&#34;&gt;u&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;email&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;email&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;main&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;  &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;c1&#34;&gt;// user 类型的值可以用来调用使用值接收者声明的方法
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;	&lt;span class=&#34;nx&#34;&gt;bill&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;user&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;Bill&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;bill@email.com&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;nx&#34;&gt;bill&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;notify&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;c1&#34;&gt;// 指向 user 类型值的指针也可以用来调用使用值接收者声明的方法
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;	&lt;span class=&#34;nx&#34;&gt;lisa&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;user&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;Lisa&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;lisa@email.com&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;nx&#34;&gt;lisa&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;notify&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;c1&#34;&gt;// user 类型的值可以用来调用使用指针接收者声明的方法
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;	&lt;span class=&#34;nx&#34;&gt;bill&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;changeEmail&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;bill@newdomain.com&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;nx&#34;&gt;bill&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;notify&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;c1&#34;&gt;// 指向 user 类型值的指针可以用来调用使用指针接收者声明的方法
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;	&lt;span class=&#34;nx&#34;&gt;lisa&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;changeEmail&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;lisa@newdomain.com&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;nx&#34;&gt;lisa&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;notify&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;上述代码展示了两种类型的方法。关键字 func 和函数名之间的参数被称作&lt;strong&gt;接收者&lt;/strong&gt;，将函数与接收者的类型绑在一起。如果一个函数有接收者，这个&lt;strong&gt;函数&lt;/strong&gt;就被称为&lt;strong&gt;方法&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;Go 语言里有两种类型的接收者：&lt;strong&gt;值接收者&lt;/strong&gt;和&lt;strong&gt;指针接收者&lt;/strong&gt;。如果使用值接收者声明方法，调用时会使用这个值的一个&lt;strong&gt;副本&lt;/strong&gt;来执行。&lt;/p&gt;
&lt;p&gt;也可以使用指针来调用使用值接收者声明的方法，为了支持这种方法调用，Go 语言调整了指针的值，来符合方法接收者的定义。可以认为 Go 语言执行了下面的操作。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;lisa&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;).&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;notify&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;指针被解引用为值&lt;/strong&gt;，这样就符合了值接收者的要求。再强调一次，notify 操作的是一个副本，只不过这次操作的是从 lisa 指针指向的值的副本。&lt;/p&gt;
&lt;p&gt;也可以使用指针接收者声明方法，当调用使用指针接收者声明的方法时，这个方法会&lt;strong&gt;共享调用方法时接收者所指向的值&lt;/strong&gt;。总结一下，&lt;strong&gt;值接收者&lt;/strong&gt;使用&lt;strong&gt;值的副本&lt;/strong&gt;来调用方法，而&lt;strong&gt;指针接受者&lt;/strong&gt;使用&lt;strong&gt;实际值&lt;/strong&gt;来调用方法。&lt;/p&gt;
&lt;p&gt;也可以使用一个值来调用使用指针接收者声明的方法，Go 语言再一次对值做了调整，使之符合函数的接收者，进行调用。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;bill&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;).&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;changeEmail&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;bill@newdomain.com&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Go语言既允许使用值，也允许使用指针来调用方法，不必严格符合接收者的类型。这个支持非常方便开发者编写程序。&lt;/p&gt;
&lt;p&gt;应该使用值接收者，还是应该使用指针接收者，这个问题有时会比较迷惑人。可以遵从标准库里一些基本的指导方针来做决定。&lt;/p&gt;
&lt;h3 id=&#34;53-类型的本质&#34;&gt;5.3 类型的本质&lt;/h3&gt;
&lt;p&gt;在声明一个新类型之后，声明一个该类型的方法之前，需要先回答一个问题：这个类型的本质是什么。如果给这个类型增加或者删除某个值，是要创建一个新值，还是要更改当前的值？如果是要创建一个新值，该类型的方法就使用值接收者。如果是要修改当前值，就使用指针接收者。这个答案也会影响程序内部传递这个类型的值的方式：是按值做传递，还是按指针做传递。保持传递的一致性很重要。这个背后的原则是，&lt;strong&gt;不要只关注某个方法是如何处理这个值，而是要关注这个值的本质是什么&lt;/strong&gt;。&lt;/p&gt;
&lt;h4 id=&#34;531-内置类型&#34;&gt;5.3.1 内置类型&lt;/h4&gt;
&lt;p&gt;内置类型是由语言提供的一组类型，它们分别是&lt;strong&gt;数值类型&lt;/strong&gt;、&lt;strong&gt;字符串类型&lt;/strong&gt;和&lt;strong&gt;布尔类型&lt;/strong&gt;。这些类型本质上是原始的类型。因此，当对这些值进行增加或者删除的时候，会&lt;strong&gt;创建一个新值&lt;/strong&gt;。基于这个结论，当把这些类型的值传递给方法或者函数时，&lt;strong&gt;应该传递一个对应值的副本&lt;/strong&gt;。&lt;/p&gt;
&lt;h4 id=&#34;532-引用类型&#34;&gt;5.3.2 引用类型&lt;/h4&gt;
&lt;p&gt;Go 语言里的引用类型有如下几个：&lt;strong&gt;切片&lt;/strong&gt;、&lt;strong&gt;映射&lt;/strong&gt;、&lt;strong&gt;通道&lt;/strong&gt;、&lt;strong&gt;接口&lt;/strong&gt;和&lt;strong&gt;函数&lt;/strong&gt;类型。当声明上述类型的变量时，创建的变量被称作&lt;strong&gt;标头 (header) 值&lt;/strong&gt;。&lt;strong&gt;从技术细节上说，字符串也是一种引用类型&lt;/strong&gt;。每个引用类型创建的标头值是&lt;strong&gt;一个指向底层数据结构的指针&lt;/strong&gt;。每个引用类型还包含一组独特的字段，用于管理底层数据结构。因为&lt;strong&gt;标头值是为复制而设计的&lt;/strong&gt;，所以&lt;strong&gt;永远不需要共享一个引用类型的值&lt;/strong&gt;。标头值里包含一个指针，因此&lt;strong&gt;通过复制来传递一个引用类型的值的副本，本质上就是在共享底层数据结构&lt;/strong&gt;。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;type&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;IP&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;byte&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;上述代码展示了一个名为 IP 的类型，这个类型被声明为字节切片。当要围绕相关的内置类型或者引用类型来声明用户定义的行为时，直接基于已有类型来声明用户定义的类型会很好用。&lt;strong&gt;编译器只允许为命名的用户定义的类型声明方法&lt;/strong&gt;。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;ip&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;IP&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;MarshalText&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;([]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;byte&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;error&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;len&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;ip&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;		&lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;byte&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;len&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;ip&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;!=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;IPv4len&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;len&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;ip&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;!=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;IPv6len&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;		&lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;errors&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;New&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;invalid IP address&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;byte&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;ip&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;String&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()),&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;上述代码里定义的 MarshalText 方法是用 IP 类型的值接收者声明的。一个值接收者，正像预期的那样&lt;strong&gt;通过复制来传递引用&lt;/strong&gt;，从而不需要通过指针来共享引用类型的值。这种传递方法也可以应用到函数或者方法的参数传递：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// ipEmptyString 像 ip.String 一样，
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 只不过在没有设置 ip 时会返回一个空字符串
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;ipEmptyString&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;ip&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;IP&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;len&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;ip&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;		&lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;ip&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;String&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;ipEmptyString 函数。这个函数需要传入一个 IP 类型的值。再一次可以看到调用者传入的是这个引用类型的值，而不是通过引用共享给这个函数。调用者将引用类型的值的副本传入这个函数。这种方法也适用于函数的返回值。&lt;/p&gt;
&lt;p&gt;最后要说的是，引用类型的值在其他方面像原始的数据类型的值一样对待。&lt;/p&gt;
&lt;h4 id=&#34;533-结构类型-以下简略&#34;&gt;5.3.3 结构类型 （以下简略）&lt;/h4&gt;
&lt;p&gt;结构类型可以用来描述一组数据值，这组值的本质即可以是&lt;strong&gt;原始的&lt;/strong&gt;，也可以是&lt;strong&gt;非原始的&lt;/strong&gt;。如果决定在某些东西需要删除或者添加某个结构类型的值时该结构类型的值不应该被更改，那么需要遵守之前提到的内置类型和引用类型的规范。&lt;/p&gt;
&lt;p&gt;如果一个创建用的工厂函数返回了一个指针，就表示这个被返回的值的本质是非原始的。即便函数或者方法没有直接改变非原始的值的状态，依旧应该使用共享的方式传递。&lt;strong&gt;非原始的总是应该被共享，而不是被复制&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;是使用值接收者还是指针接收者，不应该由该方法是否修改了接收到的值来决定。这个决策应该基于该类型的本质&lt;/strong&gt;。这条规则的一个例外是，需要让类型值符合某个接口的时候，即便类型的本质是非原始本质的，也可以选择使用值接收者声明方法。这样做完全符合接口值调用方法的机制。&lt;/p&gt;
&lt;h3 id=&#34;54-接口&#34;&gt;5.4 接口&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;多态是指代码可以根据类型的具体实现采取不同行为的能力&lt;/strong&gt;。如果一个类型实现了某个接口，所有使用这个接口的地方，都可以支持这种类型的值。标准库里有很好的例子，如 io 包里实现的流式处理接口。io 包提供了一组构造得非常好的接口和函数，来让代码轻松支持流式数据处理。只要实现两个接口，就能利用整个 io 包背后的所有强大能力。&lt;/p&gt;
&lt;h1 id=&#34;六并发&#34;&gt;六、并发&lt;/h1&gt;
&lt;p&gt;通常程序会被编写为一个顺序执行并完成一个独立任务的代码。如果没有特别的需求，最好总是这样写代码，因为这种类型的程序通常很容易写，也很容易维护。不过也有一些情况下，并行执行多个任务会有更大的好处。&lt;/p&gt;
&lt;p&gt;一个例子是，Web 服务需要在各自独立的套接字（socket）上同时接收多个数据请求。每个套接字请求都是独立的，可以完全独立于其他套接字进行处理。具有并行执行多个请求的能力可以显著提高这类系统的性能。考虑到这一点，Go 语言的语法和运行时直接内置了对并发的支持。&lt;/p&gt;
&lt;p&gt;Go 语言里的并发指的是&lt;strong&gt;能让某个函数独立于其他函数运行的能力&lt;/strong&gt;。当一个函数创建为 &lt;strong&gt;goroutine&lt;/strong&gt;时，Go 会将其视为一个独立的工作单元。这个单元会被调度到可用的&lt;strong&gt;逻辑处理器&lt;/strong&gt;上执行。Go 语言运行时的调度器是一个复杂的软件，能管理被创建的所有 goroutine 并为其分配执行时间。这个调度器在操作系统之上，将操作系统的&lt;strong&gt;线程&lt;/strong&gt;与语言运行时的逻辑处理器绑定，并在逻辑处理器上运行goroutine。调度器在任何给定的时间，都会全面控制哪个 goroutine 要在哪个逻辑处理器上运行。&lt;/p&gt;
&lt;p&gt;Go 语言的&lt;strong&gt;并发同步模型&lt;/strong&gt;来自一个叫作&lt;strong&gt;通信顺序进程&lt;/strong&gt;（Communicating Sequential Processes，&lt;strong&gt;CSP&lt;/strong&gt;）的&lt;strong&gt;范型&lt;/strong&gt;（paradigm）。CSP 是一种消息传递模型，通过&lt;strong&gt;在 goroutine 之间传递数据来传递消息&lt;/strong&gt;，&lt;strong&gt;而不是对数据进行加锁来实现同步访问&lt;/strong&gt;。用于在 goroutine 之间同步和传递数据的关键数据类型叫作&lt;strong&gt;通道&lt;/strong&gt;（channel）。&lt;/p&gt;
&lt;h3 id=&#34;61-并发与并行&#34;&gt;6.1 并发与并行&lt;/h3&gt;
&lt;p&gt;当运行一个应用程序（如一个 IDE 或者编辑器）的时候，操作系统会为这个应用程序启动一个进程（&lt;strong&gt;process&lt;/strong&gt;）。可以将这个进程看作一个包含了应用程序在运行中需要用到和维护的各种资源的容器。&lt;/p&gt;
&lt;p&gt;图 6-1 展示了一个包含所有可能分配的常用资源的进程。这些资源包括但不限于内存地址空间、文件和设备的句柄以及线程。一个线程（&lt;strong&gt;thread&lt;/strong&gt;）是一个执行空间，这个空间会被操作系统调度来运行函数中所写的代码。每个进程至少包含一个线程，每个进程的初始线程被称作&lt;strong&gt;主线程&lt;/strong&gt;。因为执行这个线程的空间是应用程序的本身的空间，所以当主线程终止时，应用程序也会终止。操作系统将线程调度到某个处理器上运行，这个处理器&lt;strong&gt;并不一定是进程所在的处理器&lt;/strong&gt;。不同操作系统使用的线程调度算法一般都不一样，但是这种不同会被操作系统屏蔽，并不会展示给程序员。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2023/03/19/PX5GcYTqnmHRCaD.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;操作系统会在物理处理器上调度线程来运行，而 Go 语言的运行时会在逻辑处理器上调度 goroutine 来运行。每个逻辑处理器都分别绑定到单个操作系统线程。在 1.5 版本上，Go语言的运行时默认会为每个可用的物理处理器分配一个逻辑处理器。在 1.5 版本之前的版本中，默认给整个应用程序只分配一个逻辑处理器。这些逻辑处理器会用于执行所有被创建 goroutine。即便只有一个逻辑处理器，Go也可以以神奇的效率和性能，并发调度无数个goroutine。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2023/03/19/dM1vkPXbjYrcmLl.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;在图 6-2 中，可以看到操作系统线程、逻辑处理器和本地运行队列之间的关系。如果创建一
个 goroutine 并准备运行，这个 goroutine 就会被放到调度器的全局运行队列中。之后，调度器就将这些队列中的 goroutine 分配给一个逻辑处理器，并放到这个逻辑处理器对应的本地运行队列中。本地运行队列中的 goroutine 会一直等待直到自己被分配的逻辑处理器执行。&lt;/p&gt;
&lt;p&gt;有时，正在运行的 goroutine 需要执行一个阻塞的系统调用，如打开一个文件。当这类调用
发生时，线程和 goroutine 会从逻辑处理器上分离，该线程会继续阻塞，等待系统调用的返回。与此同时，这个逻辑处理器就失去了用来运行的线程。所以，调度器会创建一个新线程，并将其绑定到该逻辑处理器上。之后，调度器会从本地运行队列里选择另一个 goroutine 来运行。一旦被阻塞的系统调用执行完成并返回，对应的 goroutine 会放回到本地运行队列，而之前的线程会保存好，以便之后可以继续使用。&lt;/p&gt;
&lt;p&gt;如果一个 goroutine 需要做一个网络 I/O 调用，流程上会有些不一样。在这种情况下，goroutine会和逻辑处理器分离，并移到集成了网络轮询器的运行时。一旦该轮询器指示某个网络读或者写操作已经就绪，对应的 goroutine 就会重新分配到逻辑处理器上来完成操作。调度器对可以创建的逻辑处理器的数量没有限制，但语言运行时默认限制每个程序最多创建 10 000 个线程。这个限制值可以通过调用 runtime/debug 包的 SetMaxThreads 方法来更改。如果程序试图使用更多的线程，就会崩溃。&lt;/p&gt;
&lt;p&gt;并发（&lt;strong&gt;concurrency&lt;/strong&gt;）不是并行（&lt;strong&gt;parallelism&lt;/strong&gt;）。并行是让不同的代码片段同时在不同的物理处理器上执行。并行的关键是同时&lt;strong&gt;做&lt;/strong&gt;很多事情，而并发是指同时&lt;strong&gt;管理&lt;/strong&gt;很多事情，这些事情可能只做了一半就被暂停去做别的事情了。在很多情况下，并发的效果比并行好，因为操作系统和硬件的总资源一般很少，但能支持系统同时做很多事情。这种“使用较少的资源做更多的事情”的哲学，也是指导 Go 语言设计的哲学。&lt;/p&gt;
&lt;p&gt;如果希望让 goroutine 并行，必须使用多于一个逻辑处理器。当有多个逻辑处理器时，调度器会将 goroutine 平等分配到每个逻辑处理器上。这会让 goroutine 在不同的线程上运行。不过要想真的实现并行的效果，用户需要让自己的程序运行在有多个物理处理器的机器上。否则，哪怕 Go 语言运行时使用多个线程，goroutine 依然会在同一个物理处理器上并发运行，达不到并行的效果。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2023/03/19/RgK7ybIaiBvYnm1.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;图 6-3 展示了在一个逻辑处理器上并发运行 goroutine 和在两个逻辑处理器上并行运行两个并
发的 goroutine 之间的区别。调度器包含一些聪明的算法，这些算法会随着 Go 语言的发布被更新和改进，所以不推荐盲目修改语言运行时对逻辑处理器的默认设置。如果真的认为修改逻辑处理器的数量可以改进性能，也可以对语言运行时的参数进行细微调整。后面会介绍如何做这种修改。&lt;/p&gt;
&lt;h3 id=&#34;62-goroutine&#34;&gt;6.2 goroutine&lt;/h3&gt;
&lt;p&gt;runtime 包的 GOMAXPROCS 函数允许程序更改调度器可以使用的逻辑处理器的数量。如果不想在代码里做这个调用，也可以通过修改和这个函数名字一样的环境变量的值来更改逻辑处理器的数量。给这个函数传入 1，是通知调度器只能为该程序使用一个逻辑处理器。&lt;/p&gt;
&lt;p&gt;WaitGroup 是一个计数信号量，可以用来记录并维护运行的 goroutine。如果 WaitGroup
的值大于 0，Wait 方法就会阻塞。为了减小WaitGroup 的值并最终释放 main 函数，使用 defer 声明在函数退出时调用 Done 方法。&lt;/p&gt;
&lt;p&gt;关键字 defer 会修改函数调用时机，在正在执行的函数返回时才真正调用 defer 声明的函数。&lt;/p&gt;
&lt;p&gt;基于调度器的内部算法，一个正运行的 goroutine 在工作结束前，可以被停止并重新调度。
调度器这样做的目的是防止某个 goroutine 长时间占用逻辑处理器。当 goroutine 占用时间过长时，调度器会停止当前正运行的 goroutine，并给其他可运行的 goroutine 运行的机会。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2023/03/19/3M1JicrDoQ4yRhE.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;图 6-4 从逻辑处理器的角度展示了这一场景。在第 1 步，调度器开始运行 goroutine A，而 goroutine B 在运行队列里等待调度。之后，在第 2 步，调度器交换了 goroutine A 和 goroutine B。由于 goroutine A 并没有完成工作，因此被放回到运行队列。之后，在第 3 步，goroutine B 完成了它的工作并被系统销毁。这也让 goroutine A 继续之前的工作。&lt;/p&gt;
&lt;p&gt;和其他函数调用一样，创建为 goroutine 的函数调用时可以传入参数。不过 goroutine 终止时无法获取函数的返回值。&lt;/p&gt;
&lt;p&gt;Go 标准库的 runtime 包里有一个名为 GOMAXPROCS 的函数，通过它可以指定调度器可用的逻辑处理器的数量。用这个函数，可以给每个可用的物理处理器在运行的时候分配一个逻辑处理器。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;runtime&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 给每个可用的核心分配一个逻辑处理器
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;runtime&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;GOMAXPROCS&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;runtime&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;NumCPU&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;())&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;包 runtime 提供了修改 Go 语言运行时配置参数的能力。上述代码我们使用两个 runtime 包的函数来修改调度器使用的逻辑处理器的数量。函数 NumCPU 返回可以使用的物理处理器的数量。因此，调用 GOMAXPROCS 函数就为每个可用的物理处理器创建一个逻辑处理器。需要强调的是，使用多个逻辑处理器并不意味着性能更好。在修改任何语言运行时配置参数的时候，都需要配合基准测试来评估程序的运行效果。&lt;/p&gt;
</description>
        </item>
        <item>
        <title>定时任务框架设计</title>
        <link>https://blog.ther.cool/posts/%E5%AE%9A%E6%97%B6%E4%BB%BB%E5%8A%A1%E6%A1%86%E6%9E%B6%E8%AE%BE%E8%AE%A1/</link>
        <pubDate>Sun, 18 Sep 2022 20:52:42 +0800</pubDate>
        
        <guid>https://blog.ther.cool/posts/%E5%AE%9A%E6%97%B6%E4%BB%BB%E5%8A%A1%E6%A1%86%E6%9E%B6%E8%AE%BE%E8%AE%A1/</guid>
        <description>&lt;p&gt;定时任务，顾名思义，是指在某个时刻运行的任务。&lt;/p&gt;
&lt;p&gt;使用 Go 实现一个定时任务。规定定时任务的最小间隙为 1 s，则核心代码为&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;select&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;case&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;lt;-&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;time&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;NewTicker&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;time&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Second&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;).&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;C&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        	&lt;span class=&#34;c1&#34;&gt;// 检查当前可运行的任务并运行
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;        &lt;span class=&#34;k&#34;&gt;case&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;lt;-&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;stopped&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        	&lt;span class=&#34;c1&#34;&gt;// 终止信号
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;     		&lt;span class=&#34;k&#34;&gt;break&lt;/span&gt;   	
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;代码其实很简单，重点是怎么组织任务。&lt;/p&gt;
&lt;p&gt;关键实体&lt;/p&gt;
&lt;p&gt;任务&lt;/p&gt;
&lt;p&gt;Job 任务&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;type&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;Job&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nx&#34;&gt;interval&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;uint64&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// 指定时间单位下的任务执行间隔
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;    &lt;span class=&#34;nx&#34;&gt;name&lt;/span&gt;     &lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// 任务名称
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;    &lt;span class=&#34;nx&#34;&gt;unit&lt;/span&gt;     &lt;span class=&#34;nx&#34;&gt;timeUnit&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// 任务时间间隔
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;    &lt;span class=&#34;c1&#34;&gt;// err error // 任务关联的错误
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;    &lt;span class=&#34;c1&#34;&gt;// loc *time.Location
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;    &lt;span class=&#34;nx&#34;&gt;lastRun&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;time&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Time&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// 任务上一次的执行时刻
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;    &lt;span class=&#34;nx&#34;&gt;nextRun&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;time&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Time&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// 任务下一次的执行时刻
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;    &lt;span class=&#34;nx&#34;&gt;f&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;interface&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{}&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// 任务的执行函数
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;    &lt;span class=&#34;nx&#34;&gt;fParams&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;interface&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{}&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// 任务执行函数的参数
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;    &lt;span class=&#34;c1&#34;&gt;// lock bool
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;    &lt;span class=&#34;c1&#34;&gt;// tags []string
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Scheduler 调度器&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;type&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;Scheduler&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nx&#34;&gt;jobs&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Job&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nx&#34;&gt;loc&lt;/span&gt;  &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;time&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Location&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;1.初始化调度器&lt;/p&gt;
&lt;p&gt;2.添加任务&lt;/p&gt;
&lt;p&gt;每次定时触发：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;根据任务的下次运行时间戳从小到大，将调度器中的任务排序。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;从头开始遍历任务，判断任务是否应该运行（当前时间  &amp;gt;= 任务的下次运行时间），因为任务已排序过，所以一旦遇到不符合的任务，直接中断遍历。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将步骤 2 中得到的将要执行的任务，每个任务使用一个协程运行。记录 lastRun 为当前时间，并计算任务的下次运行时间 nextRun。&lt;/p&gt;
&lt;p&gt;3.1 任务的运行。判断当前任务是否有锁，有则尝试区获取锁。运行任务，传入函数及其参数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;调度器的启动&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;s&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Scheduler&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;Start&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;chan&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;bool&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nx&#34;&gt;stopped&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;make&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;chan&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;bool&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nx&#34;&gt;ticker&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;time&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;NewTicker&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;time&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Second&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;go&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;select&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;case&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;lt;-&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;ticker&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;C&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;nx&#34;&gt;s&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;RunPending&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;case&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;lt;-&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;stopped&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;nx&#34;&gt;ticker&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Stop&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;stopped&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Start() 方法返回了一个 bool channel，stopped 作为一个容量为 1 的 channel， 用于控制调度器的终止。定时任务框架的使用着在决定将调度器停止后，可以通过向通道中的传入值来达到目的。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;stopped&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;s&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Start&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// decide to stop scheduel
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;stopeed&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;lt;-&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;执行任务&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;callJobFuncWithParams&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;jobFunc&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;interface&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{},&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;params&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;interface&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{})&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;([]&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;reflect&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Value&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;error&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nx&#34;&gt;f&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;reflect&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;ValueOf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;jobFunc&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;len&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;params&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;!=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;f&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Type&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;().&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;NumIn&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;ErrParamsNotAdapted&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nx&#34;&gt;in&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;make&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;([]&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;reflect&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Value&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;len&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;params&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;i&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;p&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;range&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;params&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nx&#34;&gt;in&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;i&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;reflect&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;ValueOf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;p&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;f&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Call&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;in&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;计算任务下次执行的时间&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;scheduleNextRun&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;error&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nx&#34;&gt;now&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;time&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Now&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;j&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;lastRun&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;time&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Unix&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nx&#34;&gt;j&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;lastRun&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;now&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description>
        </item>
        <item>
        <title>MySQL 数据类型</title>
        <link>https://blog.ther.cool/posts/mysql-%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B/</link>
        <pubDate>Sun, 07 Aug 2022 18:18:27 +0800</pubDate>
        
        <guid>https://blog.ther.cool/posts/mysql-%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B/</guid>
        <description>&lt;h2 id=&#34;1-数字类型&#34;&gt;1. 数字类型&lt;/h2&gt;
&lt;h3 id=&#34;整型类型&#34;&gt;整型类型&lt;/h3&gt;
&lt;p&gt;MySQL 数据库支持 SQL 标准支持的整型类型：INT、SMALLINT。此外，MySQL 数据库也支持诸如 TINYINT、MEDIUMINT 和 BIGINT 整型类型（表 1 显示了各种整型所占用的存储空间及取值范围）：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;占用空间（字节）&lt;/th&gt;
&lt;th&gt;最小值~最大值 [signed]&lt;/th&gt;
&lt;th&gt;最小值~最大值 [unsigned]&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TINYINT&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;[-128, 127]&lt;/td&gt;
&lt;td&gt;[0, 255]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SMALLINT&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;[-32768, 32767]&lt;/td&gt;
&lt;td&gt;[0, 65535]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MEDIUMINT&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;[-8388608, 8388607]&lt;/td&gt;
&lt;td&gt;[0, 16777215]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;INT&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;[-2147483648, 2147483647]&lt;/td&gt;
&lt;td&gt;[0, 4294967295]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BIGINT&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;[-9223372036854775808, 9223372036854775807]&lt;/td&gt;
&lt;td&gt;[0, 18446744073709551615]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;!-- raw HTML omitted --&gt;
&lt;p&gt;在整型类型中，有 signed 和 unsigned 属性，其表示的是整型的取值范围，默认为 signed。在设计时不建议刻意去用 unsigned 属性，因为在做一些数据分析时，SQL 可能返回的结果并不是想要得到的结果。例如 MySQL 要求 unsigned 数值相减之后依然为 unsigned，否则就会报错。为了避免这个错误，需要对数据库参数 sql_mode 设置为 NO_UNSIGNED_SUBTRACTION，允许相减的结果为 signed。&lt;/p&gt;
&lt;h3 id=&#34;整型类型与自增设计&#34;&gt;整型类型与自增设计&lt;/h3&gt;
&lt;p&gt;整型类型最常见的就是在业务中用来表示某件物品的数量。例如上述表的销售数量，或电商中的库存数量、购买次数等。另一个常见且重要的用法是作为表的主键，即用来唯一标识一行数据。&lt;/p&gt;
&lt;p&gt;整型结合属性 auto_increment，可以实现自增功能，但在表结构设计时用自增做主键，要注意以下两点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;用 BIGINT 做主键，而不是 INT；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;自增值并不持久化，可能会有回溯现象（MySQL 8.0 版本前）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从表 1 可以发现，INT 的范围最大在 42 亿的级别，在真实的互联网业务场景的应用中，很容易达到最大值。例如一些流水表、日志表，每天 1000W 数据量，420 天后，INT 类型的上限即可达到。因此用自增整型做主键，一律使用 BIGINT，而不是 INT。不要为了节省 4 个字节使用 INT，当达到上限时，再进行表结构的变更，将是巨大的负担与痛苦。&lt;/p&gt;
&lt;p&gt;当达到 INT 上限后，再次进行自增插入时，会报重复错误。&lt;/p&gt;
&lt;p&gt;在 MySQL 8.0 前，例如删除自增为 3 的记录后，下一个自增值为 4（AUTO_INCREMENT=4）。但若这时数据库发生重启，则启动后表 t 的自增起始值将再次变为 3，即自增值发生回溯。8.0 版本将每张表的自增值进行了持久化，不会出现这种问题。&lt;/p&gt;
&lt;p&gt;其实，在海量互联网架构设计过程中，为了之后更好的分布式架构扩展性，不建议使用整型类型做主键，更为推荐的是字符串类型。&lt;/p&gt;
&lt;h3 id=&#34;浮点类型和高精度型&#34;&gt;浮点类型和高精度型&lt;/h3&gt;
&lt;p&gt;MySQL 之前的版本中存在浮点类型 Float 和 Double，但这些类型因为不是高精度，也不是 SQL 标准的类型，所以在真实的生产环境中不推荐使用，否则在计算时，由于精度类型问题，会导致最终的计算结果出错。&lt;/p&gt;
&lt;p&gt;更重要的是，从 MySQL 8.0.17 版本开始，当创建表用到类型 Float 或 Double 时，会抛出下面的警告：MySQL 提醒用户不该用上述浮点类型，甚至提醒将在之后版本中废弃浮点类型。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-mysql&#34; data-lang=&#34;mysql&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;Specifying&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;number&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;of&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;digits&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;for&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;floating&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;point&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;types&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;is&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;deprecated&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;and&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;will&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;be&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;removed&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;in&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;a&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;future&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;release&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;而数字类型中的高精度 DECIMAL 类型可以使用，当声明该类型时，可以（并且通常必须要）指定精度和标度，例如：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-mysql&#34; data-lang=&#34;mysql&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;salary&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;DECIMAL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;8&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;其中，8 是精度（精度表示保存值的主要位数），2 是标度（标度表示小数点后面保存的位数）。通常在表结构设计中，类型 DECIMAL 可以用来表示用户的工资、账户的余额等精确到小数点后 2 位的业务字段。&lt;/p&gt;
&lt;h3 id=&#34;资金字段设计&#34;&gt;资金字段设计&lt;/h3&gt;
&lt;p&gt;资金字段并不推荐用 DECIMAL 类型，而是更推荐将 DECIMAL 转化为整型。使用分作为单位来存储，而不是用元单位存储。如 1 元在数据库中用整型类型 100 存储。&lt;/p&gt;
&lt;p&gt;用 DECIMAL 存储金额有以下的缺点：&lt;/p&gt;
&lt;p&gt;金额字段的取值范围如果用 DECIMAL 表示的，如何定义长度呢？因为类型 DECIMAL 是个变长字段，若要定义金额字段，则定义为 DECIMAL(8,2) 是远远不够的。这样只能表示存储最大值为 999999.99，百万级的资金存储。用户的金额至少要存储百亿的字段，而统计局的 GDP 金额字段则可能达到数十万亿级别。用类型 DECIMAL 定义，不好统一。&lt;/p&gt;
&lt;p&gt;另外重要的是，类型 DECIMAL 是通过二进制实现的一种编码方式，计算效率远不如整型高效。因此推荐使用 BIGINT 来存储金额相关的字段。&lt;/p&gt;
&lt;p&gt;字段存储时采用分存储，即便这样 BIGINT 也能存储千万亿级别的金额。&lt;/p&gt;
&lt;p&gt;存储为 BIGINT 的另一个好处是所有金额相关字段都是定长字段，占用 8 个字节，存储高效&lt;/p&gt;
&lt;p&gt;注意，在数据库设计中，我们非常强调定长存储，因为定长存储的性能更好。&lt;/p&gt;
&lt;h2 id=&#34;2-字符串类型&#34;&gt;2. 字符串类型&lt;/h2&gt;
&lt;p&gt;MySQL 数据库的字符串类型有 CHAR、VARCHAR、BINARY、BLOB、TEXT、ENUM、SET。不同的类型在业务设计、数据库性能方面的表现完全不同，其中最常使用的是 CHAR、VARCHAR。&lt;/p&gt;
&lt;h3 id=&#34;char-和-varchar&#34;&gt;CHAR 和 VARCHAR&lt;/h3&gt;
&lt;p&gt;CHAR(N) 用来保存固定长度的字符，N 的范围是 0 ~ 255。&lt;strong&gt;注意，N 表示的是字符，而不是字节&lt;/strong&gt;。VARCHAR(N) 用来保存变长字符，N 的范围为 0 ~ 65536， N 表示字符。&lt;/p&gt;
&lt;p&gt;在超出 65536 个字符的情况下，可以考虑使用更大的字符类型 TEXT 或 BLOB，两者最大存储长度为 4G，其区别是 BLOB 没有字符集属性，使用二进制存储。&lt;/p&gt;
&lt;p&gt;和 Oracle、Microsoft SQL Server 等传统关系型数据库不同的是，MySQL 数据库的 VARCHAR 字符类型最大能够存储 65536 个字符，所以在 MySQL 数据库下，绝大部分场景使用类型 VARCHAR 就足够了。&lt;/p&gt;
&lt;h3 id=&#34;字符集&#34;&gt;字符集&lt;/h3&gt;
&lt;p&gt;在表结构设计中，除了将列定义为 CHAR 和 VARCHAR 用以存储字符以外，还需要额外定义字符对应的字符集，因为每种字符在不同字符集编码下，对应着不同的二进制值。常见的字符集有 GBK、UTF8 等。&lt;/p&gt;
&lt;p&gt;推荐把 MySQL 的默认字符集设置为 UTF8MB4，否则某些 emoji 表情字符无法在 UTF8 字符集下存储，比如 emoji 笑脸表情，对应的字符编码为 0xF09F988E（4 字节）。&lt;/p&gt;
&lt;p&gt;若强行在字符集为 UTF8 的列上插入 emoji 表情字符， MySQL 会抛出如下错误信息：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-mysql&#34; data-lang=&#34;mysql&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;ERROR&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1366&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;HY000&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;):&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Incorrect&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;value&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;\xF0\x9F\x98\x8E&amp;#39;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;for&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;column&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;a&amp;#39;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;at&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;row&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;包括 MySQL 8.0 版本在内，字符集默认为 UTF8MB4，8.0 版本之前默认的字符集为 Latin1。因为不同版本默认字符集的不同，你要显式地在配置文件中进行相关参数的配置：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-mysql&#34; data-lang=&#34;mysql&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;mysqld&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;character&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;set&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;server&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;utf8mb4&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;此外不同的字符集下 CHAR(N)、VARCHAR(N) 对应最长的字节也不相同。如 GBK 字符集 1 个字符最大存储 2 个字节，UTF8MB4 字符集 1 个字符最大存储 4 个字节。所以从底层存储内核看，在多字节字符集下，CHAR 和 VARCHAR 底层的实现完全相同，都是变长存储。例如 CHAR(1) 既可以存储 1 个 &amp;lsquo;a&amp;rsquo; 字节，也可以存储 4 个字节的 emoji 笑脸表情，因此 CHAR 本质也是变长的。&lt;/p&gt;
&lt;p&gt;鉴于目前默认字符集推荐设置为 UTF8MB4，所以在表结构设计时，可以把 CHAR 全部用 VARCHAR 替换，底层存储的本质实现一模一样。对于变长字符集（如 GBK、UTF8MB4），其本质是一样的，都是变长，&lt;strong&gt;设计时完全可以用 VARCHAR 替代 CHAR；&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&#34;排序规则&#34;&gt;排序规则&lt;/h3&gt;
&lt;p&gt;排序规则（Collation）是比较和排序字符串的一种规则，每个字符集都会有默认的排序规则，你可以用命令 SHOW CHARSET 来查看。&lt;/p&gt;
&lt;p&gt;排序规则以 &lt;code&gt;_ci&lt;/code&gt; 结尾，表示不区分大小写（Case Insentive），&lt;code&gt;_cs&lt;/code&gt; 表示大小写敏感，&lt;code&gt;_bin&lt;/code&gt; 表示通过存储字符的二进制进行比较。需要注意的是，比较 MySQL 字符串，默认采用不区分大小的排序规则。&lt;strong&gt;绝大部分业务的表结构设计无须设置排序规则为大小写敏感&lt;/strong&gt;，除非业务真正需要。&lt;/p&gt;
&lt;h3 id=&#34;修改字符集&#34;&gt;修改字符集&lt;/h3&gt;
&lt;p&gt;业务在设计时没有考虑到字符集对于业务数据存储的影响，所以后期需要进行字符集转换，但很多同学会发现执行如下操作后，依然无法插入 emoji 这类 UTF8MB4 字符：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;ALTER&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;TABLE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;emoji_test&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;CHARSET&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;utf8mb4&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;其实，上述修改只是将表的字符集修改为 UTF8MB4，下次新增列时，若不显式地指定字符集，新列的字符集会变更为 UTF8MB4，&lt;strong&gt;但对于已经存在的列，其默认字符集并不做修改&lt;/strong&gt;，正确修改列字符集的命令应该使用 ALTER TABLE &amp;hellip; CONVERT TO&amp;hellip;这样才能将之前的列字符集从 UTF8 修改为 UTF8MB4：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;ALTER&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;TABLE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;emoji_test&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;CONVERT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;TO&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;CHARSET&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;utf8mb4&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;3-日期类型&#34;&gt;3. 日期类型&lt;/h2&gt;
&lt;p&gt;MySQL 数据库中常见的日期类型有 YEAR、DATE、TIME、DATETIME、TIMESTAMEP。因为业务绝大部分场景都需要将日期精确到秒，所以在表结构设计中，常见使用的日期类型为DATETIME 和 TIMESTAMP。&lt;/p&gt;
&lt;h4 id=&#34;datetime&#34;&gt;DATETIME&lt;/h4&gt;
&lt;p&gt;类型 DATETIME 最终展现的形式为：yyyy-MM-dd HH:mm:ss，占用 5 个字节。&lt;/p&gt;
&lt;p&gt;从 MySQL 5.6 版本开始，DATETIME 类型支持毫秒，DATETIME(N) 中的 &lt;strong&gt;N 表示毫秒的精度&lt;/strong&gt;。例如，DATETIME(6) 表示可以存储 6 位的毫秒值，此时占8个字节。同时，一些日期函数也支持精确到毫秒，例如常见的函数 NOW、SYSDATE。&lt;/p&gt;
&lt;p&gt;可以将 DATETIME 初始化值设置为当前时间，并设置自动更新当前时间的属性：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;CREATE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;TABLE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;User&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;created_at&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;DATETIME&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;6&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NOT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NULL&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;DEFAULT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;6&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;updated_at&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;DATETIME&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;6&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NOT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NULL&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;DEFAULT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;6&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;ON&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;UPDATE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;6&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id=&#34;timestamp&#34;&gt;TIMESTAMP&lt;/h4&gt;
&lt;p&gt;TIMESTAMP 实际存储的内容为 1970-01-01 00:00:00 到现在的毫秒数。在 MySQL 中，由于类型 TIMESTAMP 占用 4 个字节，因此其存储的时间上限只能到 &amp;lsquo;2038-01-19 03:14:07&amp;rsquo;。&lt;/p&gt;
&lt;p&gt;同类型 DATETIME 一样，从 MySQL 5.6 版本开始，类型 TIMESTAMP 也能支持毫秒。带有毫秒时，类型TIMESTAMP 占用 7 个字节。TIMESTAMP 占用 7 个字节的时候和占用 4 个字节时，上限一样到 2038 。&lt;/p&gt;
&lt;p&gt;类型 TIMESTAMP 最大的优点是可以带有时区属性，因为它本质上是从毫秒转化而来。如果你的业务需要对应不同的国家时区，那么类型 TIMESTAMP 是一种不错的选择。比如新闻类的业务，通常用户想知道这篇新闻发布时对应的自己国家时间，那么 TIMESTAMP 是一种选择。另外，有些国家会执行夏令时。根据不同的季节，人为地调快或调慢 1 个小时，带有时区属性的 TIMESTAMP 类型本身就能解决这个问题。&lt;/p&gt;
&lt;p&gt;参数 time_zone 指定了当前使用的时区，默认为 SYSTEM 使用操作系统时区，用户可以通过该参数指定所需要的时区：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;SET&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;time_zone&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;-08:00&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;在 MySQL 中可以直接设置时区的名字：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;SET&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;time_zone&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;America/Los_Angeles&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;可以通过下面的语句将字段类型从 DATETIME(6) 修改为 TIMESTAMP(6)：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;ALTER&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;TABLE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;User&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;CHANGE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;created_at&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;created_at&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;TIMESTAMP&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;6&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NOT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NULL&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;DEFAULT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;6&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id=&#34;timestamp-的性能问题&#34;&gt;TIMESTAMP 的性能问题&lt;/h4&gt;
&lt;p&gt;TIMESTAMP 的上限值 2038 年很快就会到来，那时业务又将面临一次类似千年虫的问题。另外，TIMESTAMP 还存在潜在的性能问题。&lt;/p&gt;
&lt;p&gt;虽然从毫秒数转换到类型 TIMESTAMP 本身需要的 CPU 指令并不多，这并不会带来直接的性能问题。但是如果使用默认的操作系统时区，则每次通过时区计算时间时，要调用操作系统底层系统函数 __tz_convert()，而这个函数需要额外的加锁操作，以确保这时操作系统时区没有修改。所以，当大规模并发访问时，由于热点资源竞争，会产生两个问题。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;性能不如 DATETIME：&lt;/strong&gt; DATETIME 不存在时区转化问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;性能抖动：&lt;/strong&gt; 海量并发时，存在性能抖动问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了优化 TIMESTAMP 的使用，强烈建议使用显式的时区，而不是操作系统时区。比如在配置文件中显示地设置时区，而不要使用系统时区：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-mysql&#34; data-lang=&#34;mysql&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;mysqld&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;time_zone&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;+08:00&amp;#34;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id=&#34;datetime-与-timestamp&#34;&gt;DATETIME 与 TIMESTAMP&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;MySQL 5.6 版本开始 DATETIME 和 TIMESTAMP 精度支持到毫秒；&lt;/li&gt;
&lt;li&gt;DATETIME 占用 8 个字节，TIMESTAMP 占用 4 个字节，DATETIME(6) 依然占用 8 个字节，TIMESTAMP(6) 占用 7 个字节；&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;TIMESTAMP 日期存储的上限为 2038-01-19 03:14:07，业务用 TIMESTAMP 存在风险；&lt;/li&gt;
&lt;li&gt;使用 TIMESTAMP 必须显式地设置时区，不要使用默认系统时区，否则存在性能问题，推荐在配置文件中设置参数 time_zone = &amp;lsquo;+08:00&amp;rsquo;；&lt;/li&gt;
&lt;li&gt;推荐日期类型使用 DATETIME，而不是 TIMESTAMP 和 INT 类型；&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;4-非结构存储json&#34;&gt;4. 非结构存储：JSON&lt;/h2&gt;
&lt;p&gt;JSON（JavaScript Object Notation）主要用于互联网应用服务之间的数据交换。MySQL 支持 &lt;a class=&#34;link&#34; href=&#34;https://tools.ietf.org/html/rfc7159?fileGuid=xxQTRXtVcqtHK6j8&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;RFC 7159 &lt;/a&gt;定义的 JSON 规范，主要有 &lt;strong&gt;JSON 对象&lt;/strong&gt;和 &lt;strong&gt;JSON 数组&lt;/strong&gt;两种类型。&lt;/p&gt;
&lt;p&gt;JSON 类型从表面上来看像是字段串类型，但本质上 JSON 是一种新的类型，有自己的存储格式，还能在每个对应的字段上创建索引，做特定的优化，这是传统字段串无法实现的。JSON 类型的另一个好处是&lt;strong&gt;无须预定义字段&lt;/strong&gt;，字段可以无限扩展。而传统关系型数据库的列都需预先定义，想要扩展需要执行 ALTER TABLE &amp;hellip; ADD COLUMN &amp;hellip; 这样比较重的操作。&lt;/p&gt;
&lt;p&gt;需要注意是，JSON 类型是从 MySQL 5.7 版本开始支持的功能，而 8.0 版本解决了更新 JSON 的日志性能瓶颈。如果要在生产环境中使用 JSON 数据类型，强烈推荐使用 MySQL 8.0 版本。&lt;/p&gt;
&lt;p&gt;在数据库中，&lt;strong&gt;JSON 类型比较适合存储一些修改较少、相对静态的数据&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;JSON_EXTRACT，它用来从 JSON 数据中提取所需要的字段内容，如下面的这条 SQL 语句就查询用户的手机和微信信息：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;SELECT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;userId&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;JSON_UNQUOTE&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;JSON_EXTRACT&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;loginInfo&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;$.cellphone&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;cellphone&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;JSON_UNQUOTE&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;JSON_EXTRACT&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;loginInfo&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;$.wxchat&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;wxchat&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;FROM&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;UserLogin&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;JSON_UNQUOTE&lt;/code&gt; 用于将原 json 串的引号去掉转成字符串类型。&lt;/p&gt;
&lt;p&gt;每次写 JSON_EXTRACT、JSON_UNQUOTE 非常麻烦，MySQL 还提供了 -&amp;raquo; 表达式，和上述 SQL 效果完全一样：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;SELECT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;userId&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;loginInfo&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;$.cellphone&amp;#34;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;cellphone&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;loginInfo&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;$.wxchat&amp;#34;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;wxchat&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;FROM&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;UserLogin&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;当 JSON 数据量非常大，用户希望对 JSON 数据进行有效检索时，可以利用 MySQL 的&lt;strong&gt;函数索引&lt;/strong&gt;功能对 JSON 中的某个字段进行索引。&lt;/p&gt;
&lt;p&gt;比如在上面的用户登录示例中，假设用户必须绑定唯一手机号，且希望未来能用手机号码进行用户检索时，可以创建下面的索引：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;ALTER&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;TABLE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;UserLogin&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;ADD&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;COLUMN&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;cellphone&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;VARCHAR&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;255&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;AS&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;loginInfo&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;$.cellphone&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;ALTER&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;TABLE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;UserLogin&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;ADD&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;UNIQUE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;INDEX&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;idx_cellphone&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;cellphone&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;上述 SQL 首先创建了一个虚拟列 cellphone，这个列是由函数 loginInfo-&amp;raquo;&amp;quot;$.cellphone&amp;quot; 计算得到的。然后在这个虚拟列上创建一个唯一索引 idx_cellphone。这时再通过虚拟列 cellphone 进行查询，就可以看到优化器会使用到新创建的 idx_cellphone 索引：&lt;/p&gt;
&lt;p&gt;当然也可以在一开始创建表的时候，就完成虚拟列及函数索引的创建。如下表创建的列 cellphone 对应的就是 JSON 中的内容，是个虚拟列；uk_idx_cellphone 就是在虚拟列 cellphone 上所创建的索引。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;CREATE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;TABLE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;User&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;userId&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;BIGINT&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;loginInfo&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;JSON&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;cellphone&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;VARCHAR&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;255&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;AS&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;loginInfo&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;$.cellphone&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;PRIMARY&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;KEY&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;userId&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;UNIQUE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;KEY&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;uk_idx_cellphone&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;cellphone&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;MySQL 8.0.17 版本开始支持 Multi-Valued Indexes，用于在 JSON 数组上创建索引，并通过函数 member of、json_contains、json_overlaps 来快速检索索引数据。&lt;/p&gt;
</description>
        </item>
        <item>
        <title>评论系统架构设计</title>
        <link>https://blog.ther.cool/posts/%E8%AF%84%E8%AE%BA%E7%B3%BB%E7%BB%9F%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1/</link>
        <pubDate>Sat, 16 Jul 2022 22:05:41 +0800</pubDate>
        
        <guid>https://blog.ther.cool/posts/%E8%AF%84%E8%AE%BA%E7%B3%BB%E7%BB%9F%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1/</guid>
        <description>&lt;h2 id=&#34;功能模块&#34;&gt;功能模块&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;发布评论: 支持回复楼层、楼中楼。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;读取评论: 按照时间、热度排序。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;删除评论: 用户删除、作者删除。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;管理评论: 作者置顶、后台运营管理(搜索、删除、审核等)。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;架构设计&#34;&gt;架构设计&lt;/h2&gt;
&lt;p&gt;架构设计等同于数据设计，梳理清楚数据的走向和逻辑。尽量&lt;strong&gt;避免环形依赖&lt;/strong&gt;、&lt;strong&gt;数据双向请求&lt;/strong&gt;等。&lt;/p&gt;
&lt;p&gt;概览图如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/16/UZyRVMrTXdxPNHs.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;image-20220716200753155&#34;
	
	
&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;BFF: comment&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;复杂评论业务的服务编排，比如访问账号服务进行等级判定，同时需要在 BFF 面向移动端/WEB场景来设计 API，这一层抽象把评论的本身的内容列表处理(加载、分页、排序等)进行了隔离，关注在业务平台化逻辑上。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Service: comment-service&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;服务层，去平台业务的逻辑，专注在评论功能的 API 实现上，比如发布、读取、删除等，关注在稳定性、可用性上，这样让上游可以灵活组织逻辑把基础能力和业务能力剥离。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Job: comment-job&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;消息队列的最大用途是削峰处理。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Admin: comment-admin&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;管理平台，按照安全等级划分服务，尤其划分运营平台，它们会共享服务层的存储层(MySQL、Redis)。运营体系的数据大量都是检索，使用 Canal 将 MySQL 中的数据同步到 ES 中，整个数据的展示都是通过 ES，再通过业务主键更新业务数据层，这样运营端的查询压力就下方给了独立的 fulltext search 系统。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Dependency: account-service、filter-service&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;评论服务还会依赖一些外部 gRPC 服务，统一的平台业务逻辑在 comment BFF 层收敛，这里 account-service 主要是账号服务，filter-service 是敏感词过滤服务。&lt;/p&gt;
&lt;h3 id=&#34;comment-service&#34;&gt;comment-service&lt;/h3&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/16/eKAs2cDvXox1dbU.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;image-20220716201722693&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;comment-service，专注在评论数据处理(认真想下 Separation of Concerns)。&lt;/p&gt;
&lt;p&gt;最开始 comment-service 和 comment 是一层，业务和功能耦合在一起，非常不利于迭代，当然在设计层面可以考虑目录结构进行拆分，但是架构层次来说，迭代隔离也是好的。&lt;/p&gt;
&lt;h4 id=&#34;读的核心逻辑&#34;&gt;读的核心逻辑&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Cache-Aside&lt;/strong&gt; 模式，先读取缓存，再读取存储。早期 cache rebuild 是做到服务里的，对于重建逻辑，一般会使用 read ahead 的思路，即预读，用户访问了第一页，很有可能访问第二页，所以缓存会超前加载，避免频繁 cache miss。但是当缓存抖动时，这种做法特别容易引起集群 thundering herd 现象，大量的请求会触发 cache rebuild，因为使用了预加载，容易导致加载大量数据到服务中，最终导致 OOM。&lt;/p&gt;
&lt;p&gt;所以回源的逻辑使用了消息队列来进行异步化。当缓存 miss 时，对于当前请求只返回 MySql 中部分数据即止，同时向 KafKa 中发送(回源)指令消息。&lt;/p&gt;
&lt;h4 id=&#34;写的核心逻辑&#34;&gt;写的核心逻辑&lt;/h4&gt;
&lt;p&gt;写和读相比较，写可以认为是透穿到存储层的，系统的瓶颈往往就来自于存储层，或者说是有状态层。对于写的设计上，可以认为刚发布的评论有极短的延迟(通常小于几 ms)对用户可见是可接受的，把对存储的直接冲击下放到消息队列，按照&lt;strong&gt;消息反压&lt;/strong&gt;的思路，即如果存储 latency 升高，消费能力就下降，自然消息容易堆积，系统始终以最大化方式消费。&lt;/p&gt;
&lt;p&gt;Kafka 存在 partition 概念，可以认为是物理上的一个小队列，一个 topic 由一组 partition 组成，所以 Kafka 的吞吐模型理解为：全局并行，局部串行的生产消费方式。对于入队的消息，可以按照 &lt;code&gt;hash(comment_subject) % N(partitions)&lt;/code&gt; 的方式进行分发。那么某个 partition 中的评论主题的数据一定都在一起，这样方便进行串行消费。在更新时，先更新 DB，再更新缓存。&lt;/p&gt;
&lt;p&gt;同样的，处理回源消息也是类似的思路。接收到消息后，可以先判断下缓存是否已经 rebuild，如果是则直接返回消息处理成功。&lt;/p&gt;
&lt;h3 id=&#34;comment-admin&#34;&gt;comment-admin&lt;/h3&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/16/hQOptoklDLRbeB8.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;image-20220716204038573&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;MySQL binlog 中的数据被 Canal 中间件流式消费，获取到业务的原始 CUD 操作(没有Retrive)，需要回放写入到 es 中，但是 es 中的数据最终是面向运营体系提供服务能力，需要检索的数据维度比较多，在写入 es 前需要做一个异构的 joiner，把单表变宽，预处理好 join 逻辑，然后写入到 es中。&lt;/p&gt;
&lt;p&gt;一般来说，运营后台的检索条件都是组合的，使用 es 的好处是避免依赖 mysql 来做多条件组合检索，同时 mysql 毕竟是 OLTP 面向线上联机事务处理的。通过冗余数据的方式，将多条件组合检索需求通过其他引擎来实现。&lt;/p&gt;
&lt;p&gt;此外 es 一般会存储检索、展示 primary key 等数据，当操作编辑的时候，找到记录的 primary key，交由 comment-admin 进行运营侧的 CRUD 操作。写操作对 DB 进行操作，经由 Canal 同步至 ES，而不是直接操作写 ES。&lt;/p&gt;
&lt;h3 id=&#34;comment&#34;&gt;comment&lt;/h3&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/16/5VEWsuA2yh1qIDr.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;image-20220716204817745&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;comment 作为 BFF，是面向端、平台、业务组合的服务。所以平台扩展的能力都在 comment 服务实现，方便以统一的接口形式提供平台化的能力。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;依赖其他 gRPC 服务，整合统一平台测的逻辑(比如发布评论用户等级限定)。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;直接向端上提供接口，提供数据的读写接口，甚至可以整合端上，提供统一的端上 SDK。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;需要在非核心依赖的 gRPC 服务不稳定时进行降级。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;存储设计&#34;&gt;存储设计&lt;/h2&gt;
&lt;h3 id=&#34;数据库设计&#34;&gt;数据库设计&lt;/h3&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/16/u4PwJjbIzds2YLV.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;image-20220716221909268&#34;
	
	
&gt;&lt;/p&gt;
&lt;h4 id=&#34;数据写入&#34;&gt;数据写入&lt;/h4&gt;
&lt;p&gt;事务更新 comment_subject，comment_index，comment_content 三张表，其中 content 属于非强制需要一致性考虑的。可以先写入 content，之后事务更新其他表。即便 content 先成功，后续失败，也仅存在一条 ghost 数据。&lt;/p&gt;
&lt;h4 id=&#34;数据读取&#34;&gt;数据读取&lt;/h4&gt;
&lt;p&gt;基于 obj_id + obj_type 在 comment_index 表找到评论列表，WHERE root = 0 ORDER BY floor。之后根据 comment_index 的 id 字段捞出 comment_content 的评论内容。对于二级的子楼层，WHERE parent/root IN (id&amp;hellip;)。&lt;/p&gt;
&lt;p&gt;因为产品形态上只存在二级列表，因此只需要迭代查询两次即可。对于嵌套层次多的，产品上，可以通过二次点击支持。&lt;/p&gt;
&lt;p&gt;Tips: 是不是可以使用 Graph 存储？DGraph、HugeGraph 类似的图存储思路。&lt;/p&gt;
&lt;h4 id=&#34;索引与内容分离&#34;&gt;索引与内容分离&lt;/h4&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/30/bkxO2q6iTB5Wnyc.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;image-20220717182945045&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;comment_index: 评论楼层的索引组织表，实际并不包含内容。
comment_content: 评论内容的表，包含评论的具体内容。其中 comment_index 的 id 字段和 comment_content 是 1 对 1 的关系，这里面包含几种设计思想。&lt;/p&gt;
&lt;p&gt;表都有主键，即 cluster index，是物理组织形式存放的，comment_content 没有 id，是为了减少一次二级索引查找，直接基于主键检索，同时 comment_id 在写入要尽可能的顺序自增。&lt;/p&gt;
&lt;p&gt;索引、内容分离，方便 MySQL datapage 缓存更多的 row ，如果和 context 耦合，会导致更大的  IO。长远来看 content 信息可以直接使用 KV storage 存储。&lt;/p&gt;
&lt;h3 id=&#34;缓存设计&#34;&gt;缓存设计&lt;/h3&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/17/7ArYIHQ21il6Gtg.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;image-20220717225941315&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;comment_subject_cache: 对应主题的缓存，value 使用 protobuf 序列化的方式存入。早期使用 memcache 来进行缓存，因为 Redis 早期单线程模型，吞吐能力不高。&lt;/p&gt;
&lt;p&gt;comment_index_cache: 使用 Redis sortedset 进行索引的缓存，索引即数据的组织顺序，而非数据内容。参考过百度的贴吧，他们使用自己研发的拉链存储来组织索引，我认为 mysql 作为主力存储，利用 redis 来做加速完全足够，因为 cache miss 的构建，我们前面讲过使用 kafka 的消费者中处理，预加载少量数据，通过增量加载的方式逐渐预热填充缓存，而 redis sortedset skiplist 的实现，可以做到 &lt;code&gt;O(logN) + O(M)&lt;/code&gt; 的时间复杂度，效率很高。&lt;/p&gt;
&lt;p&gt;sorted set 是要增量追加的，因此必须判定 key 存在，才能 zdd。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;为什么判定 key 存在后才能 add？&lt;/p&gt;
&lt;p&gt;若直接 add，则可能 key 已不存在，结果就是只 add 了要追加的一部分。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;comment_content_cache: 对应评论内容数据，使用 protobuf 序列化的方式存入。类似的我们早期使用 memcache 进行缓存。&lt;/p&gt;
&lt;p&gt;增量加载 + lazy 加载。&lt;/p&gt;
&lt;h2 id=&#34;可用性设计&#34;&gt;可用性设计&lt;/h2&gt;
&lt;h3 id=&#34;singleflight&#34;&gt;Singleflight&lt;/h3&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/17/y4qRxKWImXkAvpg.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;image-20220717230755891&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;对于热门的主题，如果存在缓存穿透的情况，会导致大量的同进程、跨进程的数据回源到存储层，可能会引起存储过载的情况，如何只交给同进程内，一个人去做加载存储？&lt;/p&gt;
&lt;p&gt;使用归并回源的思路:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;https://pkg.go.dev/golang.org/x/sync/singleflight&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;同进程只交给一个人去获取 mysql 数据，然后批量返回。同时这个 lease owner 投递一个 kafka 消息，做 index cache 的 recovery 操作。这样可以大大减少 mysql 的压力，以及大量透穿导致的密集写 kafka 的问题。&lt;/p&gt;
&lt;p&gt;更进一步的，后续连续的请求，仍然可能会短时 cache miss，我们可以在进程内设置一个 short-lived flag，标记最近有一个人投递了 cache rebuild 的消息，直接 drop。&lt;/p&gt;
&lt;p&gt;为什么我们不用分布式锁之类的思路？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;分布式锁不好做，且太重。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;热点&#34;&gt;热点&lt;/h3&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/17/sHDIyce7glU1V5E.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;image-20220717231201889&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;流量热点是因为突然热门的主题，被高频次的访问，因为底层的 cache 设计，一般是按照主题 key 进行一致性 hash 来进行分片，但是热点 key 一定命中某一个节点，这时候 remote cache 可能会变为瓶颈，因此做 cache 的升级 local cache 是有必要的，我们一般使用单进程自适应发现热点的思路，附加一个短时的 ttl local cache，可以在进程内吞掉大量的读请求。&lt;/p&gt;
&lt;p&gt;在内存中使用 hashmap 统计每个 key 的访问频次，这里可以使用滑动窗口统计，即每个窗口中，维护一个 hashmap，之后统计所有未过去的 bucket，汇总所有 key 的数据。之后使用小堆计算 TopK 的数据，自动进行热点识别。&lt;/p&gt;</description>
        </item>
        <item>
        <title>Go 并发编程 内存模型</title>
        <link>https://blog.ther.cool/posts/go-%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B-%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B/</link>
        <pubDate>Sat, 09 Jul 2022 22:33:20 +0800</pubDate>
        
        <guid>https://blog.ther.cool/posts/go-%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B-%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B/</guid>
        <description>&lt;h2 id=&#34;内存模型&#34;&gt;内存模型&lt;/h2&gt;
&lt;p&gt;本节的内存模型它并不是指 Go 对象的内存分配、内存回收和内存整理的规范，它描述的是并发环境中多 goroutine 读相同变量的时候，变量的可见性条件。具体点说，是指在什么条件下， goroutine 在读取一个变量的值的时候，能够看到其它 goroutine 对这个变量进行的写的结果。&lt;/p&gt;
&lt;p&gt;由于 CPU 指令重排和多级 Cache 的存在，保证多核访问同一个变量这件事儿变得非常复 杂。毕竟，不同 CPU 架构（x86/amd64、ARM、Power 等）的处理方式也不一样，再加 上编译器的优化也可能对指令进行重排，所以编程语言需要一个规范，来明确多线程同时 访问同一个变量的可见性和顺序。在编程语言中，这个规 范被叫做内存模型。&lt;/p&gt;
&lt;p&gt;除了 Go，Java、C++、C、C#、Rust 等编程语言也有内存模型。为什么这些编程语言都要定义内存模型呢？主要是两个目的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;向广大的程序员提供一种保证，以便他们在做设计和开发程序时，面对同一个数据同时被多个 goroutine 访问的情况，可以做一些串行化访问的控制，比如使用 channel 或者 sync 包和 sync/atomic 包中的并发原语。&lt;/li&gt;
&lt;li&gt;允许编译器和硬件对程序做一些优化。这一点其实主要是为编译器开发者提供的保证，这样可以方便他们对 Go 的编译器做优化。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;重排与可见性&#34;&gt;重排与可见性&lt;/h2&gt;
&lt;p&gt;由于指令重排，代码并不一定会按照书写的顺序执行。&lt;/p&gt;
&lt;p&gt;举个例子，当两个 goroutine 同时对一个数据进行读写时，假设 goroutine g1 对这个变 量进行写操作 w，goroutine g2 同时对这个变量进行读操作 r，那么，如果 g2 在执行读 操作 r 的时候，已经看到了 g1 写操作 w 的结果，那么，也不意味着 g2 能看到在 w 之前 的其它的写操作。这是一个反直观的结果，不过的确可能会存在。&lt;/p&gt;
&lt;p&gt;程序在运行的时候，两个操作的顺序可能不会得到保证，那该怎么办呢？接下 来，我要带你了解一下 Go 内存模型中很重要的一个概念：happens-before，这是用来描 述两个时间的顺序关系的。如果某些操作能提供 happens-before 关系，那么，我们就可以 100% 保证它们之间的顺序。&lt;/p&gt;
&lt;h2 id=&#34;happens-before&#34;&gt;happens-before&lt;/h2&gt;
&lt;p&gt;在一个 goroutine 内部，程序的执行顺序和它们的代码指定的顺序是一样的，即使编译器 或者 CPU 重排了读写顺序，从行为上来看也和代码指定的顺序一样。&lt;/p&gt;
&lt;p&gt;但是，对于另一个 goroutine 来说，重排却会产生非常大的影响。因为 Go 只保证 goroutine 内部重排对读写的顺序没有影响，比如刚刚我们在讲“可见性”问题时提到的三个例子，那该怎么办呢？这就要用到 happens-before 关系了。&lt;/p&gt;</description>
        </item>
        <item>
        <title>MySQL varchar 的最大长度</title>
        <link>https://blog.ther.cool/posts/mysql-varchar-%E7%9A%84%E6%9C%80%E5%A4%A7%E9%95%BF%E5%BA%A6/</link>
        <pubDate>Thu, 03 Feb 2022 19:17:31 +0800</pubDate>
        
        <guid>https://blog.ther.cool/posts/mysql-varchar-%E7%9A%84%E6%9C%80%E5%A4%A7%E9%95%BF%E5%BA%A6/</guid>
        <description>&lt;p&gt;&lt;strong&gt;备注&lt;/strong&gt;：全文示例皆在 8.0.17 版本下进行。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;在MySQL官方定义中，常用的 &lt;code&gt;COMPACT&lt;/code&gt;、&lt;code&gt;DYNAMIC&lt;/code&gt;行模式下，varchar 的最大长度并不是固定数值，取决于以下限制：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;行长度限制；&lt;/li&gt;
&lt;li&gt;编码长度限制；&lt;/li&gt;
&lt;li&gt;存储限制。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;行长度限制&#34;&gt;行长度限制&lt;/h2&gt;
&lt;p&gt;MySQL 要求一行的定义长度不能超过 65535 字节（约 64 KB）。若定义长度超过这个值，则提示：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-mysql&#34; data-lang=&#34;mysql&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;mi&#34;&gt;1118&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Row&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;size&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;too&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;large&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;The&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;maximum&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;row&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;size&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;for&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;the&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;used&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;table&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;type&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;not&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;counting&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;BLOBs&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;is&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;65535&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;This&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;includes&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;storage&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;overhead&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;check&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;the&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;manual&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;You&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;have&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;to&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;change&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;some&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;columns&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;to&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;TEXT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;or&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;BLOBs&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;编码长度限制&#34;&gt;编码长度限制&lt;/h2&gt;
&lt;p&gt;每个字符的实际占据的空间与字符所使用的字符集相关。下表列举的一些例子：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;字符集&lt;/th&gt;
&lt;th&gt;每个字符占用存储空间（单位：字节）&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;latin1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GBK&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UTF8&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UTF8MB4&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;!-- raw HTML omitted --&gt;
&lt;h2 id=&#34;存储限制&#34;&gt;存储限制&lt;/h2&gt;
&lt;p&gt;根据限制 1，每行最多能存储 65535 字节的数据，这包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;所有字段的长度；&lt;/li&gt;
&lt;li&gt;每个变长字段的长度，当存储的长度小于 255 字节时，需要 1 字节记录，大于 255 字节时，需要 2 字节；&lt;/li&gt;
&lt;li&gt;NULL 标识位的累计长度。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;其中，NULL 标记位用于表示某个字段是否允许为 NULL。每个字段使用 1 个 bit 位来表示。每 8 个标记组成一个字段。若一行中有 N 个字段，则一行中用于 NULL 标记位所消耗的空间计算公式为：&lt;/p&gt;
&lt;!-- raw HTML omitted --&gt;
&lt;p&gt;或者这样表示：⌈ N / 8 ⌉ （向上取整）。&lt;/p&gt;
&lt;h2 id=&#34;长度计算&#34;&gt;长度计算&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;最大长度(字符数) = （&lt;code&gt;行存储最大字节数&lt;/code&gt; - &lt;code&gt;NULL标识列占用字节数&lt;/code&gt; - &lt;code&gt;长度标识字节数&lt;/code&gt;） / &lt;code&gt;字符集单字符最大字节数&lt;/code&gt;&lt;/strong&gt;。有余数时向下取整。&lt;/p&gt;
&lt;p&gt;长度标识位表示的是字节数。大于 255 后使用两字节，是因为按照可能的数据大小，分为 0~255(2^8)、256~65535(2^16)，分别对应 1 字节和 2 字节。&lt;/p&gt;
&lt;p&gt;根据字段声明的字符长度，计算可能的字节数，再决定长度标志的字节数。如 VARCHAR(100)，字符集为 UTF8，可能的字节数为 300，则长度标识为 2 字节。&lt;/p&gt;
&lt;p&gt;长度标志位只是存储开销，不影响长度约束。长度约束的是数据的字符数，允许的最大字符数与字符集有关。&lt;/p&gt;
&lt;p&gt;如列指定字符集为 UTF8、每个字符最大可占用 3 个字节，行最大长度为 65535 字节，那么该列可设置的最大列大小为65535 ÷ 3 ≈ 21844（向下取整）。&lt;/p&gt;
&lt;p&gt;同理如果是 UTF8MB4，单字符最大占 4 个字节，可设置最大列大小为 65535 ÷ 4 ≈ 16383（向下取整）。&lt;/p&gt;
</description>
        </item>
        <item>
        <title>测试文档</title>
        <link>https://blog.ther.cool/posts/%E6%B5%8B%E8%AF%95/</link>
        <pubDate>Sat, 06 Nov 2021 17:30:31 +0800</pubDate>
        
        <guid>https://blog.ther.cool/posts/%E6%B5%8B%E8%AF%95/</guid>
        <description>&lt;h1 id=&#34;测试一级标题&#34;&gt;测试一级标题&lt;/h1&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;fmt&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;main&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nx&#34;&gt;fmt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Println&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;Hello, my blog!&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;网站地址 &lt;code&gt;https://blog.ther.cool&lt;/code&gt;&lt;/p&gt;
</description>
        </item>
        <item>
        <title>秒杀系统的设计</title>
        <link>https://blog.ther.cool/posts/%E7%A7%92%E6%9D%80%E7%B3%BB%E7%BB%9F%E7%9A%84%E8%AE%BE%E8%AE%A1/</link>
        <pubDate>Sat, 21 Nov 2020 22:26:24 +0800</pubDate>
        
        <guid>https://blog.ther.cool/posts/%E7%A7%92%E6%9D%80%E7%B3%BB%E7%BB%9F%E7%9A%84%E8%AE%BE%E8%AE%A1/</guid>
        <description>&lt;p&gt;秒杀系统本质上就是一个满足&lt;strong&gt;高并发&lt;/strong&gt;、&lt;strong&gt;高性能&lt;/strong&gt;和&lt;strong&gt;高可用&lt;/strong&gt;的&lt;strong&gt;分布式系统&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;秒杀其实主要解决两个问题，一个是&lt;strong&gt;并发读&lt;/strong&gt;，一个是&lt;strong&gt;并发写&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;并发读的核心优化理念是尽量减少用户到服务端来“读”数据，或者让他们读更少的数据；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;并发写的处理原则也一样，它要求我们&lt;strong&gt;在数据库层面独立出来一个库&lt;/strong&gt;，做特殊的处理。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;另外，我们还要针对秒杀系统做一些保护，针对意料之外的情况设计兜底方案，以防止最坏的情况发生。&lt;/p&gt;
&lt;p&gt;从一个架构师的角度来看，要想打造并维护一个超大流量并发读写、高性能、高可用的系统，在整个用户请求路径上从浏览器到服务端我们要遵循几个原则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;保证用户请求的数据尽量少&lt;/li&gt;
&lt;li&gt;请求数尽量少&lt;/li&gt;
&lt;li&gt;路径尽量短&lt;/li&gt;
&lt;li&gt;依赖尽量少&lt;/li&gt;
&lt;li&gt;不要有单点&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;秒杀的整体架构可以概括为“稳、准、快”几个关键字。&lt;/p&gt;
&lt;p&gt;“稳”，就是整个系统架构要&lt;strong&gt;满足高可用&lt;/strong&gt;，流量符合预期时肯定要稳定，就是超出预期时也同样不能掉链子，你要保证秒杀活动顺利完成，即秒杀商品顺利地卖出去，这个是最基本的前提。&lt;/p&gt;
&lt;p&gt;“准”，就是秒杀 10 台 iPhone，那就只能成交 10 台，多一台少一台都不行。一旦库存不对，那平台就要承担损失，所以“准”就是要求&lt;strong&gt;保证数据的一致性&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;“快”，它就是说系统的&lt;strong&gt;性能要足够高&lt;/strong&gt;，否则你怎么支撑这么大的流量呢？不光是服务端要做极致的性能优化，而且在整个请求链路上都要做协同的优化，每个地方快一点，整个系统就完美了。&lt;/p&gt;
&lt;p&gt;所以从技术角度上看“稳、准、快”，就对应了我们架构上的&lt;strong&gt;高可用&lt;/strong&gt;、&lt;strong&gt;一致性&lt;/strong&gt;和&lt;strong&gt;高性能&lt;/strong&gt;的要求。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;高性能。&lt;/strong&gt; 秒杀涉及大量的&lt;strong&gt;并发读&lt;/strong&gt;和&lt;strong&gt;并发写&lt;/strong&gt;，因此支持高并发访问这点非常关键。将从设计&lt;strong&gt;数据的动静分离&lt;/strong&gt;方案、&lt;strong&gt;热点的发现与隔离&lt;/strong&gt;、&lt;strong&gt;请求的削峰与分层过滤&lt;/strong&gt;、&lt;strong&gt;服务端的极致优化&lt;/strong&gt;这 4 个方面优化。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一致性。&lt;/strong&gt; 秒杀中商品减库存的实现方式同样关键。可想而知，有限数量的商品在同一时刻被很多倍的请求同时来减库存，减库存又分为“&lt;strong&gt;拍下减库存&lt;/strong&gt;”“&lt;strong&gt;付款减库存&lt;/strong&gt;”以及&lt;strong&gt;预扣库存&lt;/strong&gt;等几种，在大并发更新的过程中都要保证数据的准确性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高可用。&lt;/strong&gt; 虽然有很多极致的优化思路，但现实中总难免出现一些考虑不到的情况，所以要保证系统的高可用和正确性，我们还要设计一个 PlanB 来&lt;strong&gt;兜底&lt;/strong&gt;，以便在最坏情况发生时仍然能够从容应对。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;1-架构原则4-要-1-不要&#34;&gt;1. 架构原则：“4 要 1 不要”&lt;/h2&gt;
&lt;h3 id=&#34;数据要尽量少&#34;&gt;数据要尽量少&lt;/h3&gt;
&lt;p&gt;所谓“数据要尽量少”，首先是指用户请求的数据能少就少。请求的数据包括&lt;strong&gt;上传给系统的数据&lt;/strong&gt;和&lt;strong&gt;系统返回给用户的数据&lt;/strong&gt;（通常就是网页）。&lt;/p&gt;
&lt;p&gt;为什么“数据要尽量少”呢？因为首先这些数据在网络上传输需要时间，其次不管是请求数据还是返回数据都需要服务器做处理，而服务器在写网络时通常都要做压缩和字符编码，这些都非常消耗 CPU，所以减少传输的数据量可以显著减少 CPU 的使用。例如，我们可以简化秒杀页面的大小，去掉不必要的页面装饰效果等等。&lt;/p&gt;
&lt;p&gt;其次，“数据要尽量少”还要求&lt;strong&gt;系统依赖的数据&lt;/strong&gt;能少就少，包括系统完成某些业务逻辑需要读取和保存的数据，这些数据一般是和后台服务以及数据库打交道的。&lt;strong&gt;调用其他服务会涉及数据的序列化和反序列化&lt;/strong&gt;，而这也是 CPU 的一大杀手，同样也会增加延时。而且，数据库本身也容易成为一个瓶颈，所以和数据库打交道越少越好，数据越简单、越小则越好。&lt;/p&gt;
&lt;h3 id=&#34;请求数要尽量少&#34;&gt;请求数要尽量少&lt;/h3&gt;
&lt;p&gt;用户请求的页面返回后，浏览器渲染这个页面还要包含其他的额外请求，比如说，这个页面依赖的 CSS / JavaScript、图片，以及 Ajax 请求等等都定义为“&lt;strong&gt;额外请求&lt;/strong&gt;”，这些额外请求应该尽量少。因为浏览器每发出一个请求都多少会有一些消耗，例如建立连接要做三次握手，有的时候有页面依赖或者连接数限制，一些请求（例如 JavaScript）还需要串行加载等。另外，如果不同请求的域名不一样的话，还涉及这些&lt;strong&gt;域名的 DNS 解析&lt;/strong&gt;，可能会耗时更久。所以减少请求数可以显著减少以上这些因素导致的资源消耗。&lt;/p&gt;
&lt;p&gt;例如，&lt;strong&gt;减少请求数最常用的一个实践&lt;/strong&gt;就是&lt;strong&gt;合并 CSS 和 JavaScript 文件&lt;/strong&gt;，&lt;strong&gt;把多个 JavaScript 文件合并成一个文件&lt;/strong&gt;，&lt;strong&gt;在 URL 中用逗号隔开&lt;/strong&gt;，如：&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;https://g.xxx.com/tm/xx-b/4.0.94/mods/??module-preview/index.xtpl.js,module-jhs/index.xtpl.js,module-focus/index.xtpl.js
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这种方式&lt;strong&gt;在服务端仍然是单个文件各自存放&lt;/strong&gt;，只是&lt;strong&gt;服务端会有一个组件解析这个 URL&lt;/strong&gt;，然后动态把这些&lt;strong&gt;文件合并&lt;/strong&gt;起来一起返回。&lt;/p&gt;
&lt;h3 id=&#34;路径要尽量短&#34;&gt;路径要尽量短&lt;/h3&gt;
&lt;p&gt;所谓“路径”，就是用户发出请求到返回数据这个过程中，需求经过的中间的节点数。&lt;/p&gt;
&lt;p&gt;通常，这些节点可以表示为一个系统或者一个新的 Socket 连接（比如代理服务器只是创建一个新的 Socket 连接来转发请求）。每经过一个节点，一般都会产生一个新的 Socket 连接。&lt;/p&gt;
&lt;p&gt;然而，每增加一个连接都会增加新的不确定性。从概率统计上来说，假如一次请求经过 5 个节点，每个节点的可用性是 99.9% 的话，那么整个请求的可用性是：99.9% 的 5 次方，约等于 99.5%。&lt;/p&gt;
&lt;p&gt;所以&lt;strong&gt;缩短请求路径不仅可以增加可用性&lt;/strong&gt;，同样可以有效&lt;strong&gt;提升性能&lt;/strong&gt;（减少中间节点可以&lt;strong&gt;减少数据的序列化与反序列化&lt;/strong&gt;），并&lt;strong&gt;减少延时&lt;/strong&gt;（可以减少网络传输耗时）。&lt;/p&gt;
&lt;p&gt;要缩短访问路径有一种办法，就是&lt;strong&gt;多个相互强依赖的应用合并部署在一起&lt;/strong&gt;，把&lt;strong&gt;远程过程调用（RPC）变成 JVM 内部之间的方法调用&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;依赖要尽量少&#34;&gt;依赖要尽量少&lt;/h3&gt;
&lt;p&gt;所谓依赖，指的是要完成一次用户请求必须依赖的系统或者服务，这里的依赖指的是&lt;strong&gt;强依赖&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;举个例子，比如说要展示秒杀页面，而这个页面必须强依赖商品信息、用户信息，还有其他如优惠券、成交列表等这些对秒杀不是非要不可的信息（弱依赖），这些弱依赖在紧急情况下就可以去掉。&lt;/p&gt;
&lt;p&gt;要减少依赖，我们可以给系统进行分级，比如 0 级系统、1 级系统、2 级系统、3 级系统，0 级系统如果是最重要的系统，那么 0 级系统强依赖的系统也同样是最重要的系统，以此类推。&lt;/p&gt;
&lt;p&gt;注意，0 级系统要尽量减少对 1 级系统的强依赖，防止重要的系统被不重要的系统拖垮。例如支付系统是 0 级系统，而优惠券是 1 级系统的话，在极端情况下可以把优惠券给降级，防止支付系统被优惠券这个 1 级系统给拖垮。&lt;/p&gt;
&lt;h3 id=&#34;不要有单点&#34;&gt;不要有单点&lt;/h3&gt;
&lt;p&gt;系统中的单点可以说是系统架构上的一个大忌，因为单点意味着没有备份，风险不可控，设计分布式系统最重要的原则就是“消除单点”。&lt;/p&gt;
&lt;p&gt;那如何避免单点呢？关键是&lt;strong&gt;避免将服务的状态和机器绑定&lt;/strong&gt;，即把&lt;strong&gt;服务无状态化&lt;/strong&gt;，这样服务就可以在机器中随意移动。&lt;/p&gt;
&lt;p&gt;如何那把服务的状态和机器解耦呢？这里也有很多实现方式。例如把&lt;strong&gt;和机器相关的配置动态化&lt;/strong&gt;，这些参数可以通过&lt;strong&gt;配置中心&lt;/strong&gt;来&lt;strong&gt;动态推送&lt;/strong&gt;，在服务启动时动态拉取下来，在这些配置中心设置一些规则来方便地改变这些映射关系。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;应用无状态化是有效避免单点的一种方式&lt;/strong&gt;，但是像&lt;strong&gt;存储服务&lt;/strong&gt;本身很难无状态化，因为数据要存储在磁盘上，本身就要和机器绑定，那么这种场景一般要通过&lt;strong&gt;冗余多个备份&lt;/strong&gt;的方式来解决单点问题。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;那是不是请求最少就一定最好？不一定。把 CSS 内联进页面里，这样做可以减少依赖一个 CSS 的请求从而加快首页的渲染，但是同样也增大了页面的大小，又不符合“数据要尽量少”的原则，这种情况下我们为了提升首屏的渲染速度，只把首屏的 HTML 依赖的 CSS 内联进来，其他 CSS 仍然放到文件中作为依赖加载，尽量实现首屏的打开速度与整个页面加载性能的平衡。&lt;/p&gt;
&lt;p&gt;所以说，&lt;strong&gt;架构是一种平衡的艺术，而最好的架构一旦脱离了它所适应的场景，一切都将是空谈&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;不同场景下的不同架构案例&#34;&gt;不同场景下的不同架构案例&lt;/h3&gt;
&lt;p&gt;以淘宝早期秒杀系统架构的演进为主线，来梳理不同的请求体量下的最佳秒杀系统架构。&lt;/p&gt;
&lt;p&gt;如果你想快速搭建一个简单的秒杀系统，只需要把你的商品购买页面增加一个“定时上架”功能，仅在秒杀开始时才让用户看到购买按钮，当商品的库存卖完了也就结束了。这就是当时第一个版本的秒杀系统实现方式。&lt;/p&gt;
&lt;p&gt;但随着请求量的加大（比如从 1w/s 到了 10w/s 的量级），这个简单的架构很快就遇到了瓶颈，因此需要做架构改造来提升系统性能。这些架构改造包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;把秒杀系统独立出来&lt;strong&gt;单独打造一个系统&lt;/strong&gt;，这样可以有针对性地做优化，例如这个独立出来的系统就减少了店铺装修的功能，减少了页面的复杂度；&lt;/li&gt;
&lt;li&gt;在系统部署上也&lt;strong&gt;独立做一个机器集群&lt;/strong&gt;，这样秒杀的大流量就不会影响到正常的商品购买集群的机器负载；&lt;/li&gt;
&lt;li&gt;将&lt;strong&gt;热点数据&lt;/strong&gt;（如库存数据）&lt;strong&gt;单独放到一个缓存系统&lt;/strong&gt;中，以提高“读性能”；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;增加秒杀答题&lt;/strong&gt;，防止有秒杀器抢单。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;此时的系统架构变成了下图这个样子。最重要的就是，秒杀详情成为了一个独立的新系统，另外核心的一些数据放到了缓存（Cache）中，其他的关联系统也都以独立集群的方式进行部署。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/03/2pUyQZhVG8B4YFH.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;然而这个架构仍然支持不了超过 100w/s 的请求量，所以为了进一步提升秒杀系统的性能，又对架构做进一步升级，比如：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对&lt;strong&gt;页面&lt;/strong&gt;进行彻底的&lt;strong&gt;动静分离&lt;/strong&gt;，使得用户秒杀时不需要刷新整个页面，而只需要点击抢宝按钮，借此把页面刷新的数据降到最少；&lt;/li&gt;
&lt;li&gt;在服务端对秒杀商品进行&lt;strong&gt;本地缓存&lt;/strong&gt;，&lt;strong&gt;不需要再调用依赖系统的后台服务&lt;/strong&gt;获取数据，甚至&lt;strong&gt;不需要去公共的缓存集群中查询&lt;/strong&gt;数据，这样不仅可以减少系统调用，而且能够避免压垮公共缓存集群。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;增加系统限流保护&lt;/strong&gt;，防止最坏情况发生。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;注：数据缓存在机器内存中的话，集群内如何实现多台机器数据一致性？&lt;/p&gt;
&lt;p&gt;答：&lt;/p&gt;
&lt;p&gt;在内存的数据是&lt;strong&gt;静态数据&lt;/strong&gt;，不会更新，没有一致性问题。&lt;/p&gt;
&lt;p&gt;库存不会放在本地缓存，本地缓存只放静态数据。库存是放在独立的缓存系统里，如 Redis，库存是采用&lt;strong&gt;主动失效&lt;/strong&gt;的方式来失效缓存。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;被动失效：数据被访问时如果发现已经失效就删除。&lt;/p&gt;
&lt;p&gt;主动失效：周期性地从设置了失效时间的数据中选择一部分失效的数据删除。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;经过这些优化，系统架构变成了下图中的样子。在这里，对页面进行了进一步的&lt;strong&gt;静态化&lt;/strong&gt;，秒杀过程中不需要刷新整个页面，而只需要向服务端请求很少的动态数据。而且，最关键的详情和交易系统都增加了&lt;strong&gt;本地缓存&lt;/strong&gt;，来提前缓存秒杀商品的信息，&lt;strong&gt;热点数据库也做了独立部署&lt;/strong&gt;，等等。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/03/URivWHnoEXJfL7e.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;从前面的几次升级来看，其实越到后面需要定制的地方越多，也就是越“&lt;strong&gt;不通用&lt;/strong&gt;”。例如，把秒杀商品缓存在每台机器的内存中，这种方式显然不适合太多的商品同时进行秒杀的情况，因为单机的内存始终有限。&lt;strong&gt;所以要取得极致的性能，就要在其他地方（比如，通用性、易用性、成本等方面）有所牺牲&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id=&#34;2-动静分离&#34;&gt;2. 动静分离&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;秒杀的场景中，对于系统的要求其实就三个字：快、准、稳&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;那怎么才能“&lt;strong&gt;快&lt;/strong&gt;”起来呢？抽象起来讲就只有两点，一点是&lt;strong&gt;提高单次请求的效率&lt;/strong&gt;，一点是&lt;strong&gt;减少没必要的请求&lt;/strong&gt;。“动静分离”就是瞄着这个大方向去的。&lt;/p&gt;
&lt;p&gt;最早的秒杀系统其实是要刷新整体页面的，但后来秒杀的时候，只要点击“刷新抢宝”按钮就够了，这种变化的本质就是动静分离，分离之后，客户端大幅度减少了请求的数据量。这不自然就“快”了吗？&lt;/p&gt;
&lt;h3 id=&#34;何为动静数据&#34;&gt;何为动静数据&lt;/h3&gt;
&lt;p&gt;那到底什么才是动静分离呢？所谓“动静分离”，其实就是&lt;strong&gt;把用户请求的数据（如 HTML 页面）划分为“动态数据”和“静态数据”。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;简单来说，&lt;strong&gt;“动态数据”和“静态数据”的主要区别就是看页面中输出的数据是否和 URL、浏览者、时间、地域相关，以及是否含有 Cookie 等私密数据&lt;/strong&gt;。比如说：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;很多媒体类的网站，某一篇文章的内容不管是你访问还是我访问，它都是一样的。所以它就是一个&lt;strong&gt;典型的静态数据&lt;/strong&gt;，&lt;strong&gt;但是它是个动态页面&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;如果现在访问淘宝的首页，每个人看到的页面可能都是不一样的，淘宝首页中包含了很多根据访问者特征推荐的信息，而这些个性化的数据就可以理解为动态数据了。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这里再强调一下，我们所说的&lt;strong&gt;静态数据&lt;/strong&gt;，不能仅仅理解为传统意义上完全存在磁盘上的 HTML 页面，它&lt;strong&gt;也可能是经过 Java 系统产生的页面&lt;/strong&gt;，但是它输出的页面本身不包含上面所说的那些因素。也就是所谓“动态”还是“静态”，并&lt;strong&gt;不是说数据本身是否动静&lt;/strong&gt;，而是数据中&lt;strong&gt;是否含有和访问者相关的个性化数据&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;还有一点要注意，就是页面中“不包含”，指的是“页面的 HTML 源码中不含有”，这一点务必要清楚。&lt;/p&gt;
&lt;p&gt;分离了动静数据，就可以&lt;strong&gt;对分离出来的静态数据做缓存&lt;/strong&gt;，有了缓存之后，静态数据的“访问效率”自然就提高了。&lt;/p&gt;
&lt;p&gt;那么怎样对静态数据做缓存呢？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一，应该把静态数据缓存到离用户最近的地方&lt;/strong&gt;。静态数据就是那些相对不会变化的数据，因此我们可以把它们缓存起来。缓存到哪里呢？常见的就三种：用户&lt;strong&gt;浏览器&lt;/strong&gt;里、&lt;strong&gt;CDN&lt;/strong&gt; 上或者在&lt;strong&gt;服务端的 Cache&lt;/strong&gt; 中。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二，静态化改造就是要直接缓存 HTTP 连接&lt;/strong&gt;。相较于普通的数据缓存而言，你肯定还听过系统的静态化改造。静态化改造是&lt;strong&gt;直接缓存 HTTP 连接&lt;/strong&gt;而不是仅仅缓存数据，如下图所示，Web 代理服务器根据请求 URL，直接取出对应的 HTTP 响应头和响应体然后直接返回，这个响应过程简单得连 HTTP 协议都不用重新组装，甚至连 HTTP 请求头也不需要解析。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/03/rZUGlije38XvSMV.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;第三，&lt;strong&gt;让谁来缓存静态数据&lt;/strong&gt;也很重要。不同语言写的 Cache 软件处理缓存数据的效率也各不相同。以 Java 为例，因为 Java 系统本身也有其弱点（比如不擅长处理大量连接请求，每个连接消耗的内存较多，Servlet 容器解析 HTTP 协议较慢），所以可以不在 Java 层做缓存，而是直接在 &lt;strong&gt;Web 服务器层&lt;/strong&gt;上做，这样你就可以屏蔽 Java 语言层面的一些弱点；而相比起来，Web 服务器（如 Nginx、Apache、Varnish)也更擅长处理大并发的静态文件请求。&lt;/p&gt;
&lt;h3 id=&#34;如何做动静分离的改造&#34;&gt;如何做动静分离的改造&lt;/h3&gt;
&lt;p&gt;如何把动态页面改造成适合缓存的静态页面呢？其实也很简单，就是去除前面所说的那几个影响因素，把它们单独分离出来，做动静分离。&lt;/p&gt;
&lt;p&gt;下面，以典型的商品详情系统为例来详细介绍。这里，可以先打开京东或者淘宝的商品详情页，看看这个页面里都有哪些动静数据。从以下 5 个方面来分离出动态内容。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;URL 唯一化&lt;/strong&gt;。商品详情系统天然地就可以做到 URL 唯一化，比如每个商品都由 ID 来标识，那么 &lt;a class=&#34;link&#34; href=&#34;http://item.xxx.com/item.htm?id=xxxx&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;http://item.xxx.com/item.htm?id=xxxx&lt;/a&gt; 就可以作为唯一的 URL 标识。为啥要 URL 唯一呢？前面说了我们是&lt;strong&gt;要缓存整个 HTTP 连接&lt;/strong&gt;，那么以什么作为 Key 呢？就&lt;strong&gt;以 URL 作为缓存的 Key&lt;/strong&gt;，例如以 id=xxx 这个格式进行区分。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分离浏览者相关的因素&lt;/strong&gt;。浏览者相关的因素包括是否已登录，以及登录身份等，这些相关因素可以单独拆分出来，通过动态请求来获取。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分离时间因素&lt;/strong&gt;。服务端输出的时间也通过动态请求获取。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异步化地域因素&lt;/strong&gt;。详情页面上与地域相关的因素做成异步方式获取，当然也可以通过动态请求方式获取，只是这里通过异步获取更合适。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;去掉 Cookie&lt;/strong&gt;。服务端输出的页面包含的 Cookie 可以通过代码软件来删除，如 Web 服务器 Varnish 可以通过 unset req.http.cookie 命令去掉 Cookie。注意，这里说的去掉 Cookie 并不是用户端收到的页面就不含 Cookie 了，而是说，&lt;strong&gt;在缓存的静态数据中不含有 Cookie&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;分离出动态内容之后，如何组织这些内容页就变得非常关键了。注意：因为这其中很多动态内容都会被页面中的其他模块用到，如判断该用户是否已登录、用户 ID 是否匹配等，所以这个时候应该将这些&lt;strong&gt;信息 JSON 化&lt;/strong&gt;（用 JSON 格式组织这些数据），以方便前端获取。&lt;/p&gt;
&lt;p&gt;前面介绍里用缓存的方式来处理静态数据。而动态内容的处理通常有两种方案：ESI（Edge Side Includes）方案和 CSI（Client Side Include）方案。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;ESI 方案（或者 SSI）&lt;/strong&gt;：即在 &lt;strong&gt;Web 代理服务器&lt;/strong&gt;上做动态内容请求，并将请求插入到静态页面中，当用户拿到页面时已经是一个完整的页面了。这种方式对服务端性能有些影响，但是用户体验较好。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CSI 方案&lt;/strong&gt;。即单独发起一个异步 JavaScript 请求，以向服务端获取动态内容。这种方式服务端性能更佳，但是用户端页面可能会延时，体验稍差。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;动静分离的几种架构方案&#34;&gt;动静分离的几种架构方案&lt;/h3&gt;
&lt;p&gt;前面通过改造把静态数据和动态数据做了分离，那么如何在系统架构上进一步对这些动态和静态数据重新组合，再完整地输出给用户呢？&lt;/p&gt;
&lt;p&gt;这就涉及对用户请求路径进行合理的架构了。根据架构上的复杂度，有 3 种方案可选：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;实体机单机部署；&lt;/li&gt;
&lt;li&gt;统一 Cache 层；&lt;/li&gt;
&lt;li&gt;上 CDN。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&#34;方案-1实体机单机部署&#34;&gt;方案 1：实体机单机部署&lt;/h4&gt;
&lt;p&gt;这种方案是&lt;strong&gt;将虚拟机改为实体机&lt;/strong&gt;，以增大 Cache 的容量，并且采用了&lt;strong&gt;一致性 Hash 分组&lt;/strong&gt;的方式来提升命中率。这里将 Cache 分成若干组，是希望能达到命中率和访问热点的平衡。Hash 分组越少，缓存的命中率肯定就会越高，但短板是也会使单个商品集中在一个分组中，容易导致 Cache 被击穿，所以我们应该适当增加多个相同的分组，来&lt;strong&gt;平衡访问热点和命中率&lt;/strong&gt;的问题。&lt;/p&gt;
&lt;p&gt;实体机单机部署方案的结构图如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/03/KHhAgkmZQiFcTUt.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注&lt;/strong&gt;：LVS（Linux Virtual Server），Linux 虚拟服务器。在Linux内核中实现了基于 IP 的数据请求&lt;strong&gt;负载均衡&lt;/strong&gt;调度方案。&lt;/p&gt;
&lt;p&gt;实体机单机部署有以下几个优点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;没有网络瓶颈，而且能使用大内存；&lt;/li&gt;
&lt;li&gt;既能提升命中率，又能减少 Gzip 压缩；&lt;/li&gt;
&lt;li&gt;减少 Cache 失效压力，因为采用定时失效方式，例如只缓存 3 秒钟，过期即自动失效。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这个方案中，虽然把通常只需要虚拟机或者容器运行的 Java 应用换成实体机，优势很明显，它会增加单机的内存容量，但是一定程度上也造成了 CPU 的浪费，因为单个的 Java 进程很难用完整个实体机的 CPU。&lt;/p&gt;
&lt;p&gt;另外就是，一个实体机上部署了 Java 应用又作为 Cache 来使用，这造成了运维上的高复杂度，所以这是一个折中的方案。如果没有更多的系统有类似需求，那么这样做也比较合适，如果有多个业务系统都有静态化改造的需求，那还是建议把 Cache 层单独抽出来公用比较合理，如下面的方案 2 。&lt;/p&gt;
&lt;h4 id=&#34;方案-2统一-cache-层&#34;&gt;方案 2：统一 Cache 层&lt;/h4&gt;
&lt;p&gt;所谓统一 Cache 层，就是将单机的 Cache 统一分离出来，形成一个单独的 Cache 集群。统一 Cache 层是个更理想的可推广方案，该方案的结构图如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/03/jXUiGbA538CDrlu.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;将 Cache 层单独拿出来统一管理可以&lt;strong&gt;减少运维成本&lt;/strong&gt;，同时也&lt;strong&gt;方便接入其他静态化系统&lt;/strong&gt;。此外，它还有一些优点。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;单独一个 Cache 层，可以减少多个应用接入时使用 Cache 的成本。这样接入的应用只要维护自己的 Java 系统就好，不需要单独维护 Cache，而只关心如何使用即可。&lt;/li&gt;
&lt;li&gt;统一 Cache 的方案更易于维护，如后面加强监控、配置的自动化，只需要一套解决方案就行，统一起来维护升级也比较方便。&lt;/li&gt;
&lt;li&gt;可以共享内存，最大化利用内存，不同系统之间的内存可以动态切换，从而能够有效应对各种攻击。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这种方案虽然维护上更方便了，但是也带来了其他一些问题，比如缓存更加集中，导致：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Cache 层内部交换网络成为瓶颈；&lt;/li&gt;
&lt;li&gt;缓存服务器的网卡也会是瓶颈；&lt;/li&gt;
&lt;li&gt;机器少风险较大，挂掉一台就会影响很大一部分缓存数据。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;要解决上面这些问题，&lt;strong&gt;可以再对 Cache 做 Hash 分组&lt;/strong&gt;，即一组 Cache 缓存的内容相同，这样能够避免热点数据过度集中导致新的瓶颈产生。&lt;/p&gt;
&lt;h4 id=&#34;方案-3上-cdn&#34;&gt;方案 3：上 CDN&lt;/h4&gt;
&lt;p&gt;在将整个系统做动静分离后，我们自然会想到更进一步的方案，就是将 Cache 进一步前移到 CDN 上，因为 CDN 离用户最近，效果会更好。&lt;/p&gt;
&lt;p&gt;但是要想这么做，有以下几个问题需要解决。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;失效问题&lt;/strong&gt;。前面也有提到过缓存时效的问题。谈到静态数据时，一个关键词叫“相对不变”，它的言外之意是“可能会变化”。比如一篇文章，现在不变，但如果发现个错别字，是不是就会变化了？如果缓存时效很长，那用户端在很长一段时间内看到的都是错的。所以，这个方案中也是，我们需要保证 CDN 可以在秒级时间内，让分布在全国各地的 Cache 同时失效，这对 CDN 的失效系统要求很高。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;命中率问题&lt;/strong&gt;。Cache 最重要的一个衡量指标就是“高命中率”，不然 Cache 的存在就失去了意义。同样，如果将数据全部放到全国的 CDN 上，必然导致 Cache 分散，而 Cache 分散又会导致访问请求命中同一个 Cache 的可能性降低，那么命中率就成为一个问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;发布更新问题&lt;/strong&gt;。如果一个业务系统每周都有日常业务需要发布，那么发布系统必须足够简洁高效，而且你还要考虑有问题时快速回滚和排查问题的简便性。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;从前面的分析来看，将商品详情系统放到全国的所有 CDN 节点上是不太现实的，因为存在失效问题、命中率问题以及系统的发布更新问题。那么是否可以选择若干个节点来尝试实施呢？答案是“可以”，但是这样的节点需要满足几个条件：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;靠近访问量比较集中的地区；&lt;/li&gt;
&lt;li&gt;离主站相对较远；&lt;/li&gt;
&lt;li&gt;节点到主站间的网络比较好，而且稳定；&lt;/li&gt;
&lt;li&gt;节点容量比较大，不会占用其他 CDN 太多的资源。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最后，还有一点也很重要，那就是：节点不要太多。&lt;/p&gt;
&lt;p&gt;基于上面几个因素，选择 &lt;strong&gt;CDN 的二级 Cache&lt;/strong&gt; 比较合适，因为二级 Cache 数量偏少，容量也更大，让用户的请求先&lt;strong&gt;回源&lt;/strong&gt;的 CDN 的二级 Cache 中，如果没命中再回源站获取数据，部署方式如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/03/qdC5rTNyPnYHw8I.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;使用 CDN 的二级 Cache 作为缓存，可以达到和当前服务端静态化 Cache 类似的命中率，因为节点数不多，Cache 不是很分散，访问量也比较集中，这样也就解决了命中率问题，同时能够给用户最好的访问体验，是当前比较理想的一种 CDN 化方案。&lt;/p&gt;
&lt;p&gt;除此之外，CDN 化部署方案还有以下几个特点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;把整个页面缓存在用户浏览器中；&lt;/li&gt;
&lt;li&gt;如果强制刷新整个页面，也会请求 CDN；&lt;/li&gt;
&lt;li&gt;实际有效请求，只是用户对“刷新抢宝”按钮的点击。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这样就把 90% 的静态数据缓存在了用户端或者 CDN 上，当真正秒杀时，用户只需要点击特殊的“刷新抢宝”按钮，而不需要刷新整个页面。这样一来，系统只是向服务端请求很少的有效数据，而不需要重复请求大量的静态数据。&lt;/p&gt;
&lt;p&gt;秒杀的动态数据和普通详情页面的动态数据相比更少，性能也提升了 3 倍以上。所以“抢宝”这种设计思路，不用刷新页面就能够很好地请求到服务端最新的动态数据。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1.存储在浏览器或 CDN 上，有多大区别？&lt;/p&gt;
&lt;p&gt;区别很大。因为在 CDN 上，可以做&lt;strong&gt;主动失效&lt;/strong&gt;，而在用户的浏览器里就更不可控，如果用户不主动刷新的话，很难主动地把消息推送给用户的浏览器。&lt;/p&gt;
&lt;p&gt;2.在什么地方把静态数据和动态数据合并并渲染出一个完整的页面？&lt;/p&gt;
&lt;p&gt;假如在用户的浏览器里合并，那么服务端可以减少渲染整个页面的 CPU 消耗。如果在服务端合并的话，就要考虑缓存的数据是否进行 Gzip 压缩了：如果缓存 Gzip 压缩后的静态数据可以减少缓存的数据量，但是进行页面合并渲染时就要先解压，然后再压缩完整的页面数据输出给用户；如果缓存未压缩的静态数据，这样不用解压静态数据，但是会增加缓存容量。虽然这些都是细节问题，但你在设计架构方案时都需要考虑清楚。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;3-处理系统热点数据&#34;&gt;3. 处理系统热点数据&lt;/h2&gt;
&lt;p&gt;假设系统中存储有几十亿上百亿的商品，而每天有千万级的商品被上亿的用户访问，那么肯定有一部分被大量用户访问的热卖商品，这就是我们常说的“热点商品”。&lt;/p&gt;
&lt;p&gt;这些热点商品中最极端的例子就是秒杀商品，它们在很短时间内被大量用户执行访问、添加购物车、下单等操作，这些操作就称为“热点操作”。那么问题来了：这些热点对系统有啥影响，我们为什么要关注这些热点？&lt;/p&gt;
&lt;h3 id=&#34;为什么要关注热点&#34;&gt;为什么要关注热点&lt;/h3&gt;
&lt;p&gt;我们一定要关注热点，因为热点会对系统产生一系列的影响。&lt;/p&gt;
&lt;p&gt;首先，热点请求会&lt;strong&gt;大量占用服务器处理资源&lt;/strong&gt;，虽然这个热点可能只占请求总量的亿分之一，然而却可能抢占 90% 的服务器资源，如果这个热点请求还是没有价值的无效请求，那么对系统资源来说完全是浪费。&lt;/p&gt;
&lt;p&gt;其次，即使这些热点是有效的请求，我们也要识别出来做针对性的优化，从而用更低的代价来支撑这些热点请求。&lt;/p&gt;
&lt;h3 id=&#34;什么是热点&#34;&gt;什么是“热点”&lt;/h3&gt;
&lt;p&gt;热点分为&lt;strong&gt;热点操作&lt;/strong&gt;和&lt;strong&gt;热点数据&lt;/strong&gt;。所谓“热点操作”，例如大量的刷新页面、大量的添加购物车、双十一零点大量的下单等都属于此类操作。对系统来说，这些操作可以抽象为“&lt;strong&gt;读请求&lt;/strong&gt;”和“&lt;strong&gt;写请求&lt;/strong&gt;”，这两种热点请求的处理方式大相径庭，读请求的优化空间要大一些，而&lt;strong&gt;写请求的瓶颈一般都在存储层&lt;/strong&gt;，&lt;strong&gt;优化的思路就是根据 CAP 理论做平衡&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;而“热点数据”比较好理解，那就是用户的热点请求对应的数据。而热点数据又分为“静态热点数据”和“动态热点数据”。&lt;/p&gt;
&lt;p&gt;所谓“静态热点数据”，就是能够提前预测的热点数据。例如，可以通过卖家报名的方式提前筛选出来，通过报名系统对这些热点商品进行打标。另外，还可以通过大数据分析来提前发现热点商品，比如分析历史成交记录、用户的购物车记录，来发现哪些商品可能更热门、更好卖，这些都是可以提前分析出来的热点。&lt;/p&gt;
&lt;p&gt;所谓“动态热点数据”，就是不能被提前预测到的，系统在运行过程中临时产生的热点。例如，卖家在抖音上做了广告，然后商品一下就火了，导致它在短时间内被大量购买。&lt;/p&gt;
&lt;p&gt;由于热点操作是用户的行为，不好改变，但能做一些限制和保护，所以&lt;strong&gt;主要针对热点数据&lt;/strong&gt;来介绍如何进行&lt;strong&gt;优化&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;发现热点数据&#34;&gt;发现热点数据&lt;/h3&gt;
&lt;p&gt;前面介绍了如何对单个秒杀商品的页面数据进行动静分离，以便针对性地对静态数据做优化处理，那么另外一个关键的问题来了：如何发现这些秒杀商品，或者更准确地说，如何发现热点商品呢？&lt;/p&gt;
&lt;p&gt;你可能会说“参加秒杀的商品就是秒杀商品啊”，没错，关键是系统怎么知道哪些商品参加了秒杀活动呢？所以，你要有一个机制提前来区分普通商品和秒杀商品。&lt;/p&gt;
&lt;p&gt;从发现静态热点和发现动态热点两个方面来看一下。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;发现静态热点数据&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如前面讲的，静态热点数据可以通过商业手段，例如强制让卖家通过报名参加的方式提前把热点商品筛选出来，实现方式是通过一个运营系统，把参加活动的商品数据进行打标，然后通过一个后台系统对这些热点商品进行预处理，如提前进行缓存。但是这种通过报名提前筛选的方式也会带来新的问题，即增加卖家的使用成本，而且实时性较差，也不太灵活。&lt;/p&gt;
&lt;p&gt;不过，除了提前报名筛选这种方式，你还可以通过技术手段提前预测，例如对买家每天访问的商品进行大数据计算，然后统计出 TOP N 的商品，可以认为这些 TOP N 的商品就是热点商品。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;发现动态热点数据&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;可以通过卖家报名或者大数据预测这些手段来提前预测静态热点数据，但这其中有一个痛点，就是实时性较差，如果我们的系统能在秒级内自动发现热点商品那就完美了。&lt;/p&gt;
&lt;p&gt;能够动态地实时发现热点不仅对秒杀商品，对其他热卖商品也同样有价值，所以需要想办法实现热点的动态发现功能。&lt;/p&gt;
&lt;p&gt;这里给出一个动态热点发现系统的具体实现。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;构建一个异步的系统，它可以收集交易链路上各个环节中的中间件产品的热点 Key，如 Nginx、缓存、RPC 服务框架等这些中间件（一些中间件产品本身已经有热点统计模块）。&lt;/li&gt;
&lt;li&gt;建立一个热点上报和可以按照需求订阅的热点服务的下发规范，主要目的是通过交易链路上各个系统（包括详情、购物车、交易、优惠、库存、物流等）访问的时间差，把上游已经发现的热点透传给下游系统，提前做好保护。比如，对于大促高峰期，详情系统是最早知道的，在统一接入层上 Nginx 模块统计的热点 URL。&lt;/li&gt;
&lt;li&gt;将上游系统收集的热点数据发送到热点服务台，然后下游系统（如交易系统）就会知道哪些商品会被频繁调用，然后做热点保护。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这里给出了一个图，其中用户访问商品时经过的路径有很多，主要是依赖前面的导购页面（包括首页、搜索页面、商品详情、购物车等）提前识别哪些商品的访问量高，通过这些系统中的中间件来收集热点数据，并记录到日志中。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/03/SvY7UzjJwq1ycE6.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;通过部署在每台机器上的 Agent 把日志汇总到聚合和分析集群中，然后把符合一定规则的热点数据，通过订阅分发系统再推送到相应的系统中。可以把热点数据填充到 Cache 中，或者直接推送到应用服务器的内存中，还可以对这些数据进行拦截，总之下游系统可以订阅这些数据，然后根据自己的需求决定如何处理这些数据。&lt;/p&gt;
&lt;p&gt;打造热点发现系统时，注意以下几点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;这个热点服务后台抓取热点数据日志最好采用&lt;strong&gt;异步&lt;/strong&gt;方式，因为“异步”一方面便于保证通用性，另一方面又不影响业务系统和中间件产品的主流程。&lt;/li&gt;
&lt;li&gt;热点服务发现和中间件自身的热点保护模块并存，每个中间件和应用还需要保护自己。热点服务台提供热点数据的收集和订阅服务，便于把各个系统的热点数据透明出来。&lt;/li&gt;
&lt;li&gt;热点发现要做到&lt;strong&gt;接近实时&lt;/strong&gt;（3s 内完成热点数据的发现），因为只有做到接近实时，动态发现才有意义，才能实时地对下游系统提供保护。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;处理热点数据&#34;&gt;处理热点数据&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;处理热点数据通常有几种思路：一是优化，二是限制，三是隔离&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;优化：优化热点数据最有效的办法就是缓存热点数据，如果热点数据做了动静分离，那么可以长期缓存静态数据。但是，缓存热点数据更多的是“临时”缓存，即不管是静态数据还是动态数据，都用一个队列短暂地缓存数秒钟，由于队列长度有限，可以采用 &lt;strong&gt;LRU 淘汰算法&lt;/strong&gt;替换。&lt;/p&gt;
&lt;p&gt;限制：限制更多的是一种保护机制，限制的办法也有很多，例如&lt;strong&gt;对被访问商品的 ID 做一致性 Hash&lt;/strong&gt;，&lt;strong&gt;然后根据 Hash 做分桶&lt;/strong&gt;，&lt;strong&gt;每个分桶设置一个处理队列&lt;/strong&gt;，这样可以把热点商品限制在一个请求队列里，防止因某些热点商品占用太多的服务器资源，而使其他请求始终得不到服务器的处理资源。&lt;/p&gt;
&lt;p&gt;隔离：秒杀系统设计的第一个原则就是将这种热点数据隔离出来，不要让 1% 的请求影响到另外的 99%，隔离出来后也更方便对这 1% 的请求做针对性的优化。&lt;/p&gt;
&lt;p&gt;具体到“秒杀”业务，我们可以在以下几个层次实现隔离。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;业务隔离&lt;/strong&gt;。把秒杀做成一种营销活动，卖家要参加秒杀这种营销活动需要单独报名，从技术上来说，卖家报名后对我们来说就有了已知热点，因此可以提前做好预热。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;系统隔离&lt;/strong&gt;。系统隔离更多的是运行时的隔离，可以通过&lt;strong&gt;分组部署&lt;/strong&gt;的方式和另外 99% 分开。秒杀可以申请单独的域名，目的也是&lt;strong&gt;让请求落到不同的集群&lt;/strong&gt;中。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据隔离&lt;/strong&gt;。秒杀所调用的数据大部分都是热点数据，比如会启用单独的 Cache 集群或者 MySQL 数据库来放热点数据，目的也是不想 0.01% 的数据有机会影响 99.99% 数据。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当然了，实现隔离有很多种办法。比如，可以按照用户来区分，给不同的用户分配不同的 Cookie，在接入层，路由到不同的服务接口中；再比如，还可以在接入层针对 URL 中的不同 Path 来设置限流策略。服务层调用不同的服务接口，以及数据层通过给数据打标来区分等等这些措施，其目的都是把已经识别出来的热点请求和普通的请求区分开。&lt;/p&gt;
&lt;h2 id=&#34;4-流量削峰&#34;&gt;4. 流量削峰&lt;/h2&gt;
&lt;p&gt;如果看过秒杀系统的流量监控图的话，你会发现它是一条直线，就在秒杀开始那一秒是一条很直很直的线，这是因为秒杀请求在时间上高度集中于某一特定的时间点。这样一来，就会导致一个特别高的流量峰值，它对资源的消耗是瞬时的。&lt;/p&gt;
&lt;p&gt;但是对秒杀这个场景来说，最终能够抢到商品的人数是固定的，也就是说 100 人和 10000 人发起请求的结果都是一样的，并发度越高，无效请求也越多。&lt;/p&gt;
&lt;p&gt;但是从业务上来说，秒杀活动是希望更多的人来参与的，也就是开始之前希望有更多的人来刷页面，但是真正开始下单时，秒杀请求并不是越多越好。因此可以设计一些规则，让并发的请求更多地延缓，而且甚至可以过滤掉一些无效请求。&lt;/p&gt;
&lt;h3 id=&#34;为什么要削峰&#34;&gt;为什么要削峰&lt;/h3&gt;
&lt;p&gt;为什么要削峰呢？或者说峰值会带来哪些坏处？&lt;/p&gt;
&lt;p&gt;服务器的处理资源是恒定的，用或者不用它的处理能力都是一样的，所以出现峰值的话，很容易导致忙到处理不过来，闲的时候却又没有什么要处理。但是由于要保证服务质量，很多处理资源只能按照忙的时候来预估，而这会导致资源的一个浪费。&lt;/p&gt;
&lt;p&gt;这就好比因为存在早高峰和晚高峰的问题，所以有了错峰限行的解决方案。削峰的存在，一是可以&lt;strong&gt;让服务端处理变得更加平稳&lt;/strong&gt;，二是&lt;strong&gt;可以节省服务器的资源成本&lt;/strong&gt;。针对秒杀这一场景，削峰从本质上来说就是更多地延缓用户请求的发出，以便减少和过滤掉一些无效请求，它遵从“请求数要尽量少”的原则。&lt;/p&gt;
&lt;p&gt;流量削峰的一些操作思路：&lt;strong&gt;排队&lt;/strong&gt;、&lt;strong&gt;答题&lt;/strong&gt;、&lt;strong&gt;分层过滤&lt;/strong&gt;。这几种方式都是&lt;strong&gt;无损&lt;/strong&gt;（即不会损失用户的发出请求）的实现方案，当然还有些&lt;strong&gt;有损&lt;/strong&gt;的实现方案，比如&lt;strong&gt;限流&lt;/strong&gt;和&lt;strong&gt;机器负载保护&lt;/strong&gt;等一些强制措施也能达到削峰保护的目的，当然这都是不得已的一些措施。&lt;/p&gt;
&lt;h3 id=&#34;排队&#34;&gt;排队&lt;/h3&gt;
&lt;p&gt;要对流量进行削峰，最容易想到的解决方案就是用&lt;strong&gt;消息队列&lt;/strong&gt;来缓冲瞬时流量，把同步的直接调用转换成&lt;strong&gt;异步&lt;/strong&gt;的间接推送，中间通过一个队列在一端承接瞬时的流量洪峰，在另一端平滑地将消息推送出去。在这里，消息队列就像“水库”一样， 拦蓄上游的洪水，削减进入下游河道的洪峰流量，从而达到减免洪水灾害的目的。&lt;/p&gt;
&lt;p&gt;用消息队列来缓冲瞬时流量的方案，如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/03/8MshEuyCFjSUq3g.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;但是，如果流量峰值持续一段时间达到了消息队列的处理上限，例如本机的消息积压达到了存储空间的上限，消息队列同样也会被压垮，这样虽然保护了下游的系统，但是和直接把请求丢弃也没多大的区别。就像遇到洪水爆发时，即使是有水库恐怕也无济于事。&lt;/p&gt;
&lt;p&gt;除了消息队列，类似的&lt;strong&gt;排队方式&lt;/strong&gt;还有很多，例如：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;利用&lt;strong&gt;线程池加锁等待&lt;/strong&gt;也是一种常用的排队方式；&lt;/li&gt;
&lt;li&gt;先进先出、先进后出等常用的&lt;strong&gt;内存排队算法&lt;/strong&gt;的实现方式；&lt;/li&gt;
&lt;li&gt;把&lt;strong&gt;请求序列化到文件中&lt;/strong&gt;，然后再顺序地读文件（例如&lt;strong&gt;基于 MySQL binlog 的同步机制&lt;/strong&gt;）来&lt;strong&gt;恢复请求&lt;/strong&gt;等方式。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;可以看到，这些方式都有一个共同特征，就是把“一步的操作”变成“两步的操作”，其中增加的一步操作用来起到缓冲的作用。&lt;/p&gt;
&lt;h3 id=&#34;答题&#34;&gt;答题&lt;/h3&gt;
&lt;p&gt;增加答题功能呢主要是为了增加购买的复杂度，从而达到两个目的：&lt;/p&gt;
&lt;p&gt;第一个目的是&lt;strong&gt;防止&lt;/strong&gt;部分买家使用秒杀器在参加秒杀时&lt;strong&gt;作弊&lt;/strong&gt;。2011 年秒杀非常火的时候，秒杀器也比较猖獗，因而没有达到全民参与和营销的目的，所以系统增加了答题来限制秒杀器。增加答题后，下单的时间基本控制在 2s 后，秒杀器的下单比例也大大下降。&lt;/p&gt;
&lt;p&gt;第二个目的其实就是&lt;strong&gt;延缓请求&lt;/strong&gt;，起到对请求流量进行削峰的作用，从而让系统能够更好地支持瞬时的流量高峰。这个重要的功能就是把峰值的下单请求拉长，从以前的 1s 之内延长到 2s ~ 10s。这样一来，&lt;strong&gt;请求峰值基于时间分片了&lt;/strong&gt;。这个时间的分片对服务端处理并发非常重要，会大大减轻压力。而且，由于请求具有先后顺序，靠后的请求到来时自然也就没有库存了，因此根本到不了最后的下单步骤，所以真正的并发写就非常有限了。这种设计思路目前用得非常普遍，如当年支付宝的“咻一咻”、微信的“摇一摇”都是类似的方式。&lt;/p&gt;
&lt;p&gt;这里说一下秒杀答题的设计思路。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/03/i1IdEKObBXea6YZ.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;如上图所示，整个秒杀答题的逻辑主要分为 3 部分。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;题库生成模块&lt;/strong&gt;，这个部分主要就是生成一个个问题和答案，其实题目和答案本身并不需要很复杂，重要的是能够防止由机器来算出结果，即防止秒杀器来答题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;题库的推送模块&lt;/strong&gt;，用于在秒杀答题前，把题目提前推送给详情系统和交易系统。题库的推送主要是为了保证每次用户请求的题目是唯一的，目的也是防止答题作弊。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;题目的图片生成模块&lt;/strong&gt;，用于把题目生成为图片格式，并且在图片里增加一些干扰因素。这也同样是为防止机器直接来答题，它要求只有人才能理解题目本身的含义。这里还要注意一点，由于答题时网络比较拥挤，我们应该把题目的图片&lt;strong&gt;提前推送到 CDN&lt;/strong&gt; 上并且要进行&lt;strong&gt;预热&lt;/strong&gt;，不然的话当用户真正请求题目时，图片可能加载比较慢，从而影响答题的体验。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;其实真正答题的逻辑比较简单，很好理解：当用户提交的答案和题目对应的答案做比较，如果通过了就继续进行下一步的下单逻辑，否则就失败。可以把问题和答案用下面这样的 key 来进行 MD5 加密：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;问题 key：userId + itemId + question_Id + time + PK&lt;/li&gt;
&lt;li&gt;答案 key：userId + itemId + answer + PK&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;验证的逻辑如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/03/5gX9NwfcPqKWOao.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;注意，这里面的验证逻辑，除了验证问题的答案以外，还包括用户本身身份的验证，例如是否已经登录、用户的 Cookie 是否完整、用户是否重复频繁提交等。&lt;/p&gt;
&lt;p&gt;除了做正确性验证，还可以对提交答案的时间做些限制，例如从开始答题到接受答案要超过 1s，因为小于 1s 是人为操作的可能性很小，这样也能防止机器答题的情况。&lt;/p&gt;
&lt;h3 id=&#34;分层过滤&#34;&gt;分层过滤&lt;/h3&gt;
&lt;p&gt;前面介绍的排队和答题要么是少发请求，要么对发出来的请求进行缓冲，而针对秒杀场景还有一种方法，就是对请求进行分层过滤，从而过滤掉一些无效的请求。分层过滤其实就是采用“漏斗”式设计来处理请求的，如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/03/r1nGDtkuNxH9KMZ.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;假如请求分别经过 CDN、前台读系统（如商品详情系统）、后台系统（如交易系统）和数据库这几层，那么：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;大部分数据和流量在用户浏览器或者 CDN 上获取，这一层可以拦截大部分数据的读取；&lt;/li&gt;
&lt;li&gt;经过第二层（即前台系统）时数据（包括强一致性的数据）尽量得走 Cache，过滤一些无效的请求；&lt;/li&gt;
&lt;li&gt;再到第三层后台系统，主要做数据的二次检验，对系统做好保护和限流，这样数据量和请求就进一步减少；&lt;/li&gt;
&lt;li&gt;最后在数据层完成数据的强一致性校验。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样就像漏斗一样，尽量把数据量和请求量一层一层地过滤和减少了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;分层过滤的核心思想是：在不同的层次尽可能地过滤掉无效请求，让“漏斗”最末端的才是有效请求&lt;/strong&gt;。而要达到这种效果，就必须对数据做分层的校验。&lt;/p&gt;
&lt;p&gt;分层校验的基本原则是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将动态请求的读数据缓存（Cache）在 Web 端，过滤掉无效的数据读；&lt;/li&gt;
&lt;li&gt;对读数据不做强一致性校验，减少因为一致性校验产生瓶颈的问题；&lt;/li&gt;
&lt;li&gt;对写数据进行&lt;strong&gt;基于时间的合理分片&lt;/strong&gt;，过滤掉过期的失效请求；&lt;/li&gt;
&lt;li&gt;对写请求做限流保护，将超出系统承载能力的请求过滤掉；&lt;/li&gt;
&lt;li&gt;对写数据进行强一致性校验，只保留最后有效的数据。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;分层校验的目的是：在读系统中，尽量减少由于一致性校验带来的系统瓶颈，但是尽量将不影响性能的检查条件提前，如用户是否具有秒杀资格、商品状态是否正常、用户答题是否正确、秒杀是否已经结束、是否非法请求、营销等价物是否充足等；在写数据系统中，主要对写的数据（如“库存”）做一致性检查，最后在数据库层保证数据的最终准确性（如“库存”不能减为负数）。&lt;/p&gt;
&lt;h3 id=&#34;总结&#34;&gt;总结&lt;/h3&gt;
&lt;p&gt;削峰的 3 种处理方式：一个是通过队列来缓冲请求，即&lt;strong&gt;控制请求的发出&lt;/strong&gt;；一个是通过答题来&lt;strong&gt;延长请求发出的时间&lt;/strong&gt;，在请求发出后承接请求时进行控制，最后再对不符合条件的请求进行过滤；最后一种是&lt;strong&gt;对请求进行分层过滤&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;其中，队列缓冲方式更加通用，它适用于内部上下游系统之间调用请求不平缓的场景，由于内部系统的服务质量要求不能随意丢弃请求，所以使用消息队列能起到很好的削峰和缓冲作用。&lt;/p&gt;
&lt;p&gt;而答题更适用于秒杀或者营销活动等应用场景，在请求发起端就控制发起请求的速度，因为越到后面无效请求也会越多，所以配合后面介绍的分层拦截的方式，可以更进一步减少无效请求对系统资源的消耗。&lt;/p&gt;
&lt;p&gt;分层过滤非常适合交易性的写请求，比如减库存或者拼车这种场景，在读的时候需要知道还有没有库存或者是否还有剩余空座位。但是由于库存和座位又是不停变化的，所以读的数据是否一定要非常准确呢？其实不一定，可以放一些请求过去，然后在真正减的时候再做强一致性保证，这样既过滤一些请求又解决了强一致性读的瓶颈。&lt;/p&gt;
&lt;p&gt;不过，在削峰的处理方式上除了采用技术手段，其实还可以&lt;strong&gt;采用业务手段&lt;/strong&gt;来达到一定效果，例如在零点开启大促的时候由于流量太大导致支付系统阻塞，这个时候可以采用发放优惠券、发起抽奖活动等方式，将一部分流量分散到其他地方，这样也能起到缓冲流量的作用。&lt;/p&gt;
&lt;h2 id=&#34;5-性能优化&#34;&gt;5. 性能优化&lt;/h2&gt;
&lt;p&gt;结合秒杀这一场景，重点介绍下服务端的一些优化技巧。&lt;/p&gt;
&lt;h3 id=&#34;影响性能的因素&#34;&gt;影响性能的因素&lt;/h3&gt;
&lt;p&gt;想要提升性能，首先肯定要知道哪些因素对于系统性能的影响最大，然后再针对这些具体的因素想办法做优化。&lt;/p&gt;
&lt;p&gt;那么，哪些因素对性能有影响呢？在回答这个问题之前，我们先定义一下“性能”，服务设备不同对性能的定义也是不一样的，例如 CPU 主要看主频、磁盘主要看 IOPS（Input/Output Operations Per Second，即每秒进行读写操作的次数）。&lt;/p&gt;
&lt;p&gt;而今天讨论的主要是系统服务端性能，一般用 &lt;strong&gt;QPS（Query Per Second，每秒请求数）&lt;strong&gt;来衡量，还有一个影响和 QPS 也息息相关，那就是&lt;/strong&gt;响应时间（Response Time，RT）&lt;/strong&gt;，它可以理解为服务器处理响应的耗时。&lt;/p&gt;
&lt;p&gt;正常情况下响应时间（RT）越短，一秒钟处理的请求数（QPS）自然也就会越多，这在单线程处理的情况下看起来是线性的关系，即我们只要把每个请求的响应时间降到最低，那么性能就会最高。&lt;/p&gt;
&lt;p&gt;但是响应时间总有一个极限，不可能无限下降，所以又出现了另外一个维度，即通过多线程，来处理请求。这样理论上就变成了“总 QPS =（1000ms / 响应时间）× 线程数量”，这样性能就和两个因素相关了，一个是一次响应的服务端耗时，一个是处理请求的线程数。&lt;/p&gt;
&lt;p&gt;接下来看看这个两个因素到底会造成什么样的影响。&lt;/p&gt;
&lt;h4 id=&#34;响应时间和-qps-的关系&#34;&gt;响应时间和 QPS 的关系&lt;/h4&gt;
&lt;p&gt;对于大部分的 Web 系统而言，响应时间一般都是由 CPU 执行时间和线程等待时间（比如 RPC、IO 等待、Sleep、Wait 等）组成，即服务器在处理一个请求时，一部分是 CPU 本身在做运算，还有一部分是在各种等待。&lt;/p&gt;
&lt;p&gt;理解了服务器处理请求的逻辑，那为什么我们不去减少这种等待时间。很遗憾，根据实际的测试发现，减少线程等待时间对提升性能的影响没有想象得那么大，它并不是线性的提升关系，这点在很多代理服务器（Proxy）上可以做验证。&lt;/p&gt;
&lt;p&gt;如果代理服务器本身没有 CPU 消耗，在每次给代理服务器代理的请求加个延时，即增加响应时间，但是这对代理服务器本身的吞吐量并没有多大的影响，因为代理服务器本身的资源并没有被消耗，可以通过增加代理服务器的处理线程数，来弥补响应时间对代理服务器的 QPS 的影响。&lt;/p&gt;
&lt;p&gt;其实，真正对性能有影响的是 CPU 的执行时间。这也很好理解，因为 CPU 的执行真正消耗了服务器的资源。经过实际的测试，如果减少 CPU 一半的执行时间，就可以增加一倍的 QPS。&lt;/p&gt;
&lt;p&gt;也就是说，应该致力于减少 CPU 的执行时间。&lt;/p&gt;
&lt;h4 id=&#34;线程数对-qps-的影响&#34;&gt;线程数对 QPS 的影响&lt;/h4&gt;
&lt;p&gt;单看“总 QPS”的计算公式，会觉得线程数越多 QPS 也就会越高，但这会一直正确吗？显然不是，线程数不是越多越好，因为线程本身也消耗资源，也受到其他因素的制约。例如，线程越多系统的线程切换成本就会越高，而且每个线程也都会耗费一定内存。&lt;/p&gt;
&lt;p&gt;那么，设置什么样的线程数最合理呢？其实&lt;strong&gt;很多多线程的场景都有一个默认配置，即“线程数 = 2 * CPU 核数 + 1”&lt;/strong&gt;。除去这个配置，还有一个根据最佳实践得出来的公式：&lt;/p&gt;
&lt;p&gt;线程数 = [(线程等待时间 + 线程 CPU 时间) / 线程 CPU 时间] × CPU 数量&lt;/p&gt;
&lt;p&gt;当然，最好的办法是通过性能测试来发现最佳的线程数。&lt;/p&gt;
&lt;p&gt;换句话说，要提升性能就要&lt;strong&gt;减少 CPU 的执行时间&lt;/strong&gt;，另外就是要&lt;strong&gt;设置一个合理的并发线程数&lt;/strong&gt;，通过这两方面来显著提升服务器的性能。&lt;/p&gt;
&lt;p&gt;现在知道了如何来快速提升性能，那应该怎么发现系统哪里最消耗 CPU 资源呢？&lt;/p&gt;
&lt;h3 id=&#34;如何发现瓶颈&#34;&gt;如何发现瓶颈&lt;/h3&gt;
&lt;p&gt;就服务器而言，会出现瓶颈的地方有很多，例如 CPU、内存、磁盘以及网络等都可能会导致瓶颈。此外，不同的系统对瓶颈的关注度也不一样，例如&lt;strong&gt;对缓存系统而言，制约它的是内存&lt;/strong&gt;，而&lt;strong&gt;对存储型系统来说 I/O 更容易是瓶颈&lt;/strong&gt;。&lt;strong&gt;秒杀场景它的瓶颈更多地发生在 CPU 上&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;那么，如何发现 CPU 的瓶颈呢？其实有很多 CPU 诊断工具可以发现 CPU 的消耗，最常用的就是 JProfiler 和 Yourkit 这两个工具，它们可以列出整个请求中每个函数的 CPU 执行时间，可以发现哪个函数消耗的 CPU 时间最多，以便有针对性地做优化。&lt;/p&gt;
&lt;p&gt;当然还有一些办法也可以近似地统计 CPU 的耗时，例如通过 jstack 定时地打印调用栈，如果某些函数调用频繁或者耗时较多，那么那些函数就会多次出现在系统调用栈里，这样相当于采样的方式也能够发现耗时较多的函数。&lt;/p&gt;
&lt;p&gt;虽说秒杀系统的瓶颈大部分在 CPU，但这并不表示其他方面就一定不出现瓶颈。例如，如果海量请求涌过来，页面又比较大，那么网络就有可能出现瓶颈。&lt;/p&gt;
&lt;p&gt;怎样简单地判断 CPU 是不是瓶颈呢？一个办法就是看当 QPS 达到极限时，服务器的 CPU 使用率是不是超过了 95%，如果没有超过，那么表示 CPU 还有提升的空间，要么是有锁限制，要么是有过多的本地 I/O 等待发生。&lt;/p&gt;
&lt;h3 id=&#34;如何优化系统&#34;&gt;如何优化系统&lt;/h3&gt;
&lt;p&gt;对 Java 系统来说，可以优化的地方很多，这里重点说一下比较有效的几种手段，它们是：减少编码、减少序列化、Java 极致优化、并发读优化。&lt;/p&gt;
&lt;h4 id=&#34;减少编码&#34;&gt;减少编码&lt;/h4&gt;
&lt;p&gt;Java 的编码运行比较慢，这是 Java 的一大硬伤。在很多场景下，只要涉及字符串的操作（如输入输出操作、I/O 操作）都比较耗 CPU 资源，不管它是磁盘 I/O 还是网络 I/O，因为都需要&lt;strong&gt;将字符转换成字节&lt;/strong&gt;，而这个转换必须&lt;strong&gt;编码&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;每个字符的编码都需要查表，而这种查表的操作非常耗资源，所以减少字符到字节或者相反的转换、减少字符编码会非常有成效。减少编码就可以大大提升性能。&lt;/p&gt;
&lt;p&gt;那么如何才能减少编码呢？例如，网页输出是可以直接进行流输出的，即用 resp.getOutputStream() 函数写数据，把一些静态的数据提前转化成字节，等到真正往外写的时候再直接用 OutputStream() 函数写，就可以减少静态数据的编码转换。&lt;/p&gt;
&lt;h4 id=&#34;减少序列化&#34;&gt;减少序列化&lt;/h4&gt;
&lt;p&gt;序列化也是 Java 性能的一大天敌，减少 Java 中的序列化操作也能大大提升性能。又因为&lt;strong&gt;序列化往往是和编码同时发生的&lt;/strong&gt;，所以减少序列化也就减少了编码。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;序列化大部分是在 RPC 中发生的&lt;/strong&gt;，因此避免或者减少 RPC 就可以减少序列化，当然当前的序列化协议也已经做了很多优化来提升性能。有一种新的方案，就是可以将多个关联性比较强的应用进行“&lt;strong&gt;合并部署&lt;/strong&gt;”，而减少不同应用之间的 RPC 也可以减少序列化的消耗。&lt;/p&gt;
&lt;p&gt;所谓“合并部署”，就是把两个原本在不同机器上的不同应用合并部署到一台机器上，当然不仅仅是部署在一台机器上，还要在同一个 Tomcat 容器中，且不能走本机的 Socket，这样才能避免序列化的产生。&lt;/p&gt;
&lt;h4 id=&#34;java-极致优化&#34;&gt;Java 极致优化&lt;/h4&gt;
&lt;p&gt;Java 和通用的 Web 服务器（如 Nginx 或 Apache 服务器）相比，在处理大并发的 HTTP 请求时要弱一点，所以一般我们都会对大流量的 Web 系统做&lt;strong&gt;静态化改造&lt;/strong&gt;，让大部分请求和数据直接在 Nginx 服务器或者 Web 代理服务器（如 Varnish、Squid 等）上直接返回（这样可以&lt;strong&gt;减少数据的序列化与反序列化&lt;/strong&gt;），而 Java 层只需处理少量数据的动态请求。针对这些请求，可以使用以下手段进行优化：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;直接使用 Servlet 处理请求&lt;/strong&gt;。避免使用传统的 MVC 框架，这样可以绕过一大堆复杂且用处不大的处理逻辑，节省 1ms 时间（具体取决于你对 MVC 框架的依赖程度）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;直接输出流数据&lt;/strong&gt;。使用 resp.getOutputStream() 而不是 resp.getWriter() 函数，可以省掉一些不变字符数据的编码，从而提升性能；数据输出时&lt;strong&gt;推荐使用 JSON&lt;/strong&gt; 而不是**模板引擎（一般都是解释执行）**来输出页面。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&#34;并发读优化&#34;&gt;并发读优化&lt;/h4&gt;
&lt;p&gt;也许有人会觉得这个问题很容易解决，无非就是放到 Tair 缓存里面。集中式缓存为了&lt;strong&gt;保证命中率&lt;/strong&gt;一般都会采用&lt;strong&gt;一致性 Hash&lt;/strong&gt;，所以同一个 key 会落到同一台机器上。虽然单台缓存机器也能支撑 30w/s 的请求，但还是远不足以应对像“大秒”这种级别的热点商品。那么，该如何彻底解决单点的瓶颈呢？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Tair 是一个 Key / Value 结构数据的解决方案，它默认支持基于内存和文件的两种存储方式。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;答案是采用&lt;strong&gt;应用层的 LocalCache&lt;/strong&gt;，即在秒杀系统的&lt;strong&gt;单机上缓存&lt;/strong&gt;商品相关的数据。&lt;/p&gt;
&lt;p&gt;那么，又如何缓存（Cache）数据呢？需要划分成动态数据和静态数据分别进行处理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;像商品中的“标题”和“描述”这些本身不变的数据，会在秒杀开始之前全量推送到秒杀机器上，并一直缓存到秒杀结束；&lt;/li&gt;
&lt;li&gt;像库存这类动态数据，会采用“&lt;strong&gt;被动失效&lt;/strong&gt;”的方式缓存一定时间（一般是数秒），失效后再去缓存拉取最新的数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;像库存这种频繁更新的数据，一旦数据不一致，会不会导致超卖？&lt;/p&gt;
&lt;p&gt;这就要用到前面介绍的读数据的分层校验原则了，&lt;strong&gt;读的场景可以允许一定的脏数据&lt;/strong&gt;，因为这里的误判只会导致少量原本无库存的下单请求被误认为有库存，可以等到真正写数据时再保证&lt;strong&gt;最终一致性&lt;/strong&gt;，通过在数据的&lt;strong&gt;高可用性&lt;/strong&gt;和&lt;strong&gt;一致性&lt;/strong&gt;之间的平衡，来解决高并发的数据读取问题。&lt;/p&gt;
&lt;h4 id=&#34;总结-1&#34;&gt;总结&lt;/h4&gt;
&lt;p&gt;性能优化的过程首先要从发现短板开始，除了介绍的一些优化措施外，还可以在减少数据、数据分级（动静分离），以及减少中间环节、增加预处理等这些环节上做优化。&lt;/p&gt;
&lt;p&gt;首先是“发现短板”，比如考虑以下因素的一些限制：光速（光速：C = 30 万千米 / 秒；光纤：V = C/1.5=20 万千米 / 秒，即数据传输是有物理距离的限制的）、网速（2017 年 11 月知名测速网站 Ookla 发布报告，全国平均上网带宽达到 61.24 Mbps，千兆带宽下 10KB 数据的极限 QPS 为 1.25 万 QPS=1000Mbps/8/10KB）、网络结构（交换机 / 网卡的限制）、TCP/IP、虚拟机（内存 / CPU / IO 等资源的限制）和应用本身的一些瓶颈等。&lt;/p&gt;
&lt;p&gt;其次是减少数据。事实上，有两个地方特别影响性能，一是服务端在处理数据时不可避免地存在字符到字节的相互转化，二是 &lt;strong&gt;HTTP 请求时要做 Gzip 压缩&lt;/strong&gt;，还有网络传输的耗时，这些都和数据大小密切相关。&lt;/p&gt;
&lt;p&gt;再次，就是数据分级，也就是要保证首屏为先、重要信息为先，次要信息则异步加载，以这种方式提升用户获取数据的体验。&lt;/p&gt;
&lt;p&gt;最后就是要减少中间环节，减少字符到字节的转换，增加预处理（提前做字符到字节的转换）去掉不需要的操作。&lt;/p&gt;
&lt;p&gt;此外，要做好优化，还需要做好应用基线，比如性能基线（何时性能突然下降）、成本基线（去年双 11 用了多少台机器）、链路基线（系统发生了哪些变化），可以通过这些基线持续关注系统的性能，做到在代码上提升编码质量，在业务上改掉不合理的调用，在架构和调用链路上不断的改进。&lt;/p&gt;
&lt;h2 id=&#34;6-减库存逻辑&#34;&gt;6. 减库存逻辑&lt;/h2&gt;
&lt;p&gt;系统是用户下单了就算这个商品卖出去了，还是等到用户真正付款了才算卖出了呢？这是个问题。&lt;/p&gt;
&lt;p&gt;可以先根据减库存是发生在下单阶段还是付款阶段，把减库存做一下划分。&lt;/p&gt;
&lt;h3 id=&#34;减库存有哪几种方式&#34;&gt;减库存有哪几种方式&lt;/h3&gt;
&lt;p&gt;在正常的电商平台购物场景中，用户的实际购买过程一般分为两步：下单和付款。你想买一台 iPhone 手机，在商品页面点了“立即购买”按钮，核对信息之后点击“提交订单”，这一步称为下单操作。下单之后，只有真正完成付款操作才能算真正购买，也就是俗话说的“落袋为安”。&lt;/p&gt;
&lt;p&gt;那如果你是架构师，你会在哪个环节完成减库存的操作呢？总结来说，减库存操作一般有如下几个方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;下单减库存&lt;/strong&gt;，即当买家下单后，在商品的总库存中减去买家购买数量。下单减库存是最简单的减库存方式，也是控制最精确的一种，下单时直接通过&lt;strong&gt;数据库的事务机制&lt;/strong&gt;控制商品库存，这样一定不会出现超卖的情况。但是要知道，有些人下完单可能并不会付款。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;付款减库存&lt;/strong&gt;，即买家下单后，并不立即减库存，而是等到有用户付款后才真正减库存，否则库存一直保留给其他买家。但因为付款时才减库存，&lt;strong&gt;如果并发比较高，有可能出现买家下单后付不了款的情况&lt;/strong&gt;，因为可能商品已经被其他人买走了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;预扣库存&lt;/strong&gt;，这种方式相对复杂一些，买家下单后，库存为其保留一定的时间（如 10 分钟），超过这个时间，库存将会自动释放，释放后其他买家就可以继续购买。在买家付款前，系统会校验该订单的库存是否还有保留：如果没有保留，则再次尝试预扣；如果库存不足（也就是预扣失败）则不允许继续付款；如果预扣成功，则完成付款并实际地减去库存。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以上这几种减库存的方式都会存在一些问题。&lt;/p&gt;
&lt;h3 id=&#34;减库存可能存在的问题&#34;&gt;减库存可能存在的问题&lt;/h3&gt;
&lt;p&gt;由于购物过程中存在两步或者多步的操作，因此在不同的操作步骤中减库存，就会存在一些可能被恶意买家利用的漏洞，例如发生&lt;strong&gt;恶意下单&lt;/strong&gt;的情况。&lt;/p&gt;
&lt;p&gt;假如我们采用“下单减库存”的方式，即用户下单后就减去库存，正常情况下，买家下单后付款的概率会很高，所以不会有太大问题。但是有一种场景例外，就是当卖家参加某个活动时，此时活动的有效时间是商品的黄金售卖时间，如果有竞争对手通过恶意下单的方式将该卖家的商品全部下单，让这款商品的库存减为零，那么这款商品就不能正常售卖了。要知道，这些恶意下单的人是不会真正付款的，这正是“下单减库存”方式的不足之处。&lt;/p&gt;
&lt;p&gt;既然“下单减库存”可能导致恶意下单，从而影响卖家的商品销售，那么有没有办法解决呢？你可能会想，采用“付款减库存”的方式是不是就可以了？的确可以。但是，“付款减库存”又会导致另外一个问题：&lt;strong&gt;库存超卖&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;假如有 100 件商品，就可能出现 300 人下单成功的情况，因为下单时不会减库存，所以也就可能出现下单成功数远远超过真正库存数的情况，这尤其会发生在做活动的热门商品上。这样一来，就会导致很多买家下单成功但是付不了款，买家的购物体验自然比较差。&lt;/p&gt;
&lt;p&gt;可以看到，不管是“下单减库存”还是“付款减库存”，都会导致商品库存不能完全和实际售卖情况对应起来的情况。&lt;/p&gt;
&lt;p&gt;那么，既然“下单减库存”和“付款减库存”都有缺点，能否把两者相结合，将两次操作进行前后关联起来，下单时先预扣，在规定时间内不付款再释放库存，即采用“&lt;strong&gt;预扣库存&lt;/strong&gt;”这种方式呢？&lt;/p&gt;
&lt;p&gt;这种方案确实可以在一定程度上缓解上面的问题。但是否就彻底解决了呢？其实没有！针对恶意下单这种情况，虽然把有效的付款时间设置为 10 分钟，但是恶意买家完全可以在 10 分钟后再次下单，或者采用一次下单很多件的方式把库存减完。针对这种情况，解决办法还是要结合安全和反作弊的措施来制止。&lt;/p&gt;
&lt;p&gt;例如，给经常下单不付款的买家进行识别打标（可以在被打标的买家下单时不减库存）、给某些类目设置最大购买件数（例如，参加活动的商品一人最多只能买 3 件），以及对重复下单不付款的操作进行次数限制等。&lt;/p&gt;
&lt;p&gt;针对“库存超卖”这种情况，在 10 分钟时间内下单的数量仍然有可能超过库存数量，遇到这种情况我们只能区别对待：对普通的商品下单数量超过库存数量的情况，可以通过补货来解决；但是有些卖家完全不允许库存为负数的情况，那只能在买家付款时提示库存不足。&lt;/p&gt;
&lt;h3 id=&#34;大型秒杀中如何减库存&#34;&gt;大型秒杀中如何减库存？&lt;/h3&gt;
&lt;p&gt;目前来看，&lt;strong&gt;业务系统中最常见的就是预扣库存方案&lt;/strong&gt;，像你在买机票、买电影票时，下单后一般都有个“&lt;strong&gt;有效付款时间&lt;/strong&gt;”，超过这个时间订单自动释放，这都是典型的预扣库存方案。而具体到秒杀这个场景，应该采用哪种方案比较好呢？&lt;/p&gt;
&lt;p&gt;由于参加秒杀的商品，一般都是“抢到就是赚到”，所以成功下单后却不付款的情况比较少，再加上卖家对秒杀商品的库存有严格限制，所以&lt;strong&gt;秒杀商品采用“下单减库存”更加合理&lt;/strong&gt;。另外，理论上由于“下单减库存”比“预扣库存”以及涉及第三方支付的“付款减库存”在&lt;strong&gt;逻辑上更为简单&lt;/strong&gt;，所以&lt;strong&gt;性能上更占优势&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;“下单减库存”在数据一致性上，主要就是保证大并发请求时库存数据不能为负数，也就是要保证数据库中的库存字段值不能为负数，一般我们有多种解决方案：&lt;/p&gt;
&lt;p&gt;一种是在应用程序中通过事务来判断，即保证减后库存不能为负数，否则就回滚；另一种办法是直接设置数据库的字段数据为无符号整数，这样减后库存字段值小于零时会直接执行 SQL 语句来报错；再有一种就是使用 CASE WHEN 判断语句，例如这样的 SQL 语句：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;UPDATE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;item&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;SET&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;inventory&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;CASE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;WHEN&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;inventory&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;xxx&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;THEN&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;inventory&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;xxx&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;ELSE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;inventory&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;END&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;注&lt;/strong&gt;：上述语句必须在&lt;strong&gt;串行隔离&lt;/strong&gt;级别才能用，否则就会超卖。&lt;/p&gt;
&lt;h3 id=&#34;秒杀减库存的极致优化&#34;&gt;秒杀减库存的极致优化&lt;/h3&gt;
&lt;p&gt;在交易环节中，“库存”是个关键数据，也是个热点数据，因为交易的各个环节中都可能涉及对库存的查询。但是，在前面介绍分层过滤时提到过，&lt;strong&gt;秒杀中并不需要对库存有精确的一致性读&lt;/strong&gt;，把库存数据放到缓存（Cache）中，可以大大提升读性能。&lt;/p&gt;
&lt;p&gt;解决大并发读问题，可以采用 &lt;strong&gt;LocalCache&lt;/strong&gt;（即在秒杀系统的单机上缓存商品相关的数据）和对数据进行分层过滤的方式，但是像减库存这种&lt;strong&gt;大并发写&lt;/strong&gt;无论如何还是避免不了，这也是秒杀场景下最为核心的一个技术难题。&lt;/p&gt;
&lt;p&gt;因此这里专门来说一下秒杀场景下减库存的极致优化思路，包括如何在缓存中减库存以及如何在数据库中减库存。&lt;/p&gt;
&lt;p&gt;秒杀商品和普通商品的减库存还是有些差异的，例如商品数量比较少，交易时间段也比较短，因此这里有一个大胆的假设，即能否&lt;strong&gt;把秒杀商品减库存直接放到缓存系统中实现&lt;/strong&gt;，也就是直接在缓存中减库存或者在一个带有持久化功能的缓存系统（如 Redis）中完成呢？&lt;/p&gt;
&lt;p&gt;如果你的秒杀商品的减库存逻辑非常单一，比如没有复杂的 SKU 库存和总库存这种联动关系的话，完全可以。但是如果有比较复杂的减库存逻辑，或者需要使用事务，还是必须在数据库中完成减库存。&lt;/p&gt;
&lt;p&gt;由于 MySQL 存储数据的特点，同一数据在数据库里肯定是一行存储（MySQL），因此会有大量线程来竞争 &lt;strong&gt;InnoDB 行锁&lt;/strong&gt;，而并发度越高时等待线程会越多，**TPS（Transaction Per Second，即每秒处理的消息数）**会下降，**响应时间（RT）**会上升，数据库的吞吐量就会严重受影响。&lt;/p&gt;
&lt;p&gt;这就可能引发一个问题，就是单个热点商品会影响整个数据库的性能， 导致 0.01% 的商品影响 99.99% 的商品的售卖，这是我们不愿意看到的情况。一个解决思路是遵循前面介绍的原则进行&lt;strong&gt;隔离&lt;/strong&gt;，把热点商品放到单独的热点库中。但是这无疑会带来维护上的麻烦，比如要做热点数据的动态迁移以及单独的数据库等。&lt;/p&gt;
&lt;p&gt;而分离热点商品到单独的数据库还是没有解决&lt;strong&gt;并发锁&lt;/strong&gt;的问题，应该怎么办呢？要解决并发锁的问题，有两种办法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;应用层做排队&lt;/strong&gt;。按照商品维度设置队列顺序执行，这样能减少同一台机器对数据库同一行记录进行操作的并发度，同时也能控制单个商品占用数据库连接的数量，防止热点商品占用太多的数据库连接。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据库层做排队&lt;/strong&gt;。应用层只能做到单机的排队，但是应用机器数本身很多，这种排队方式控制并发的能力仍然有限，所以如果能在数据库层做全局排队是最理想的。阿里的数据库团队开发了针对这种 MySQL 的 InnoDB 层上的补丁程序（patch），可以在数据库层上&lt;strong&gt;对单行记录做到并发排队&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;“按照商品维度设置队列顺序执行”的意思就是，为了防止同一个商品对数据库的操作占用太多的数据库资源，所以采用队列的方式，让其他商品也有公平的机会得到数据的响应，例如如果秒杀的时候，秒杀商品肯定占用大量的请求，数据库的连接池有可能都被秒杀商品占用了，如果不做队列的话，那么其他商品就得不到数据库执行机会了。加入我们分10个队列，那么秒杀商品就会落在这10个队列中的一个，那么最多也就占用机器10分之一的资源。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;排队和锁竞争不都是要等待吗，有什么区别？&lt;/p&gt;
&lt;p&gt;如果熟悉 MySQL 的话，会知道 InnoDB 内部的死锁检测，以及 MySQL Server 和 InnoDB 的切换会比较消耗性能，淘宝的 MySQL 核心团队还做了很多其他方面的优化，如 COMMIT_ON_SUCCESS 和 ROLLBACK_ON_FAIL 的补丁程序，配合在 SQL 里面加提示（hint），在事务里不需要等待应用层提交（COMMIT），而在数据执行完最后一条 SQL 后，直接根据 TARGET_AFFECT_ROW 的结果进行提交或回滚，可以减少网络等待时间（平均约 0.7ms）。&lt;/p&gt;
&lt;p&gt;另外，数据更新问题除了前面介绍的热点隔离和排队处理之外，还有些场景（如对商品的 lastmodifytime 字段的）更新会非常频繁，在某些场景下这些&lt;strong&gt;多条 SQL 是可以合并&lt;/strong&gt;的，一定时间内只要执行最后一条 SQL 就行了，以便减少对数据库的更新操作。&lt;/p&gt;
&lt;h2 id=&#34;7-plan-b兜底方案&#34;&gt;7. Plan B：兜底方案&lt;/h2&gt;
&lt;p&gt;为了保证系统的&lt;strong&gt;高可用&lt;/strong&gt;，必须设计一个 Plan B 方案来兜底，这样在最坏情况发生时仍然能够从容应对。&lt;/p&gt;
&lt;h3 id=&#34;高可用建设应该从哪里着手&#34;&gt;高可用建设应该从哪里着手&lt;/h3&gt;
&lt;p&gt;说到系统的高可用建设，它其实是一个系统工程，需要考虑到系统建设的各个阶段，也就是说它其实贯穿了系统建设的整个生命周期，如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/03/UkoHtAJ7ewsx6jK.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;具体来说，系统的高可用建设涉及架构阶段、编码阶段、测试阶段、发布阶段、运行阶段，以及故障发生时。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;架构阶段&lt;/strong&gt;：架构阶段主要考虑系统的&lt;strong&gt;可扩展性&lt;/strong&gt;和&lt;strong&gt;容错性&lt;/strong&gt;，要避免系统出现&lt;strong&gt;单点问题&lt;/strong&gt;。例如&lt;strong&gt;多机房单元化部署&lt;/strong&gt;，即使某个城市的某个机房出现整体故障，仍然不会影响整体网站的运转。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;编码阶段&lt;/strong&gt;：编码最重要的是保证代码的健壮性，例如涉及远程调用问题时，要设置合理的&lt;strong&gt;超时退出机制&lt;/strong&gt;，防止被其他系统拖垮，也要对调用的返回结果集有预期，防止返回的结果超出程序处理范围，&lt;strong&gt;最常见的做法就是对错误异常进行捕获，对无法预料的错误要有默认处理结果&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;测试阶段&lt;/strong&gt;：测试主要是保证测试用例的覆盖度，保证最坏情况发生时，也有相应的处理流程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;发布阶段&lt;/strong&gt;：发布时也有一些地方需要注意，因为发布时最容易出现错误，因此要有&lt;strong&gt;紧急回滚机制&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;运行阶段&lt;/strong&gt;：运行时是系统的常态，系统大部分时间都会处于运行态，运行态最重要的是对系统的监控要准确及时，发现问题能够准确报警并且报警数据要准确详细，以便于排查问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;故障发生&lt;/strong&gt;：故障发生时首先最重要的就是及时止损，例如由于程序问题导致商品价格错误，那就要及时下架商品或者关闭购买链接，防止造成重大资产损失。然后就是要能够及时恢复服务，并定位原因解决问题。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;为什么系统的高可用建设要放到整个生命周期中全面考虑？因为我们在每个环节中都可能犯错，而有些环节犯的错，在后面是无法弥补的。例如在架构阶段没有消除单点问题，那么系统上线后，遇到突发流量把单点给挂了，就只能干瞪眼，有时候想加机器都加不进去。所以高可用建设是一个系统工程，必须在每个环节都做好。&lt;/p&gt;
&lt;p&gt;那么针对秒杀系统，重点介绍在遇到大流量时，应该从哪些方面来保障系统的稳定运行，所以更多的是看如何针对&lt;strong&gt;运行阶段&lt;/strong&gt;进行处理，这就引出了接下来的内容：&lt;strong&gt;降级&lt;/strong&gt;、&lt;strong&gt;限流&lt;/strong&gt;和&lt;strong&gt;拒绝服务&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;降级&#34;&gt;降级&lt;/h3&gt;
&lt;p&gt;所谓“降级”，就是当系统的容量达到一定程度时，&lt;strong&gt;限制或者关闭系统的某些非核心功能&lt;/strong&gt;，从而把有限的资源保留给更核心的业务。它是一个有目的、有计划的执行过程，所以对降级一般需要有一套预案来配合执行。如果把它系统化，就可以通过预案系统和开关系统来实现降级。&lt;/p&gt;
&lt;p&gt;降级方案可以这样设计：当秒杀流量达到 5w/s 时，把成交记录的获取从展示 20 条降级到只展示 5 条。“从 20 改到 5”这个操作由一个开关来实现，也就是设置一个能够从&lt;strong&gt;开关系统&lt;/strong&gt;动态获取的系统参数。&lt;/p&gt;
&lt;p&gt;下图是开关系统的示意图。它分为两部分，一部分是开关控制台，它保存了开关的具体配置信息，以及具体执行开关所对应的机器列表；另一部分是执行下发开关数据的 Agent，主要任务就是保证开关被正确执行，即使系统重启后也会生效。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/03/RBJnOL1XjClYgNu.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;执行降级无疑是在系统性能和用户体验之间选择了前者，降级后肯定会影响一部分用户的体验，例如在双 11 零点时，如果优惠券系统扛不住，可能会临时降级商品详情的优惠信息展示，把有限的系统资源用在保障交易系统正确展示优惠信息上，即保障用户真正下单时的价格是正确的。所以降级的核心目标是牺牲次要的功能和用户体验来保证核心业务流程的稳定，是一个不得已而为之的举措。&lt;/p&gt;
&lt;h3 id=&#34;限流&#34;&gt;限流&lt;/h3&gt;
&lt;p&gt;如果说降级是牺牲了一部分次要的功能和用户的体验效果，那么限流就是更极端的一种保护措施了。限流就是当系统容量达到瓶颈时，需要通过限制一部分流量来保护系统，并做到既可以人工执行开关，也支持自动化保护的措施。&lt;/p&gt;
&lt;p&gt;下图是限流系统的示意图。总体来说，限流既可以是在客户端限流，也可以是在服务端限流。此外，限流的实现方式既要支持 URL 以及方法级别的限流，也要支持基于 QPS 和线程的限流。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;客户端和服务端限流是针对 rpc 调用来说的，发起方可以理解为客户端，调用方可以理解为服务端。&lt;/p&gt;
&lt;p&gt;QPS，Queries-per-second，每秒查询率。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;以内部的系统调用为例，来分别说下客户端限流和服务端限流的优缺点。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;客户端限流&lt;/strong&gt;，好处可以限制请求的发出，通过减少发出无用请求从而减少对系统的消耗。缺点就是当客户端比较分散时，没法设置合理的限流阈值：如果阈值设的太小，会导致服务端没有达到瓶颈时客户端已经被限制；而如果设的太大，则起不到限制的作用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服务端限流&lt;/strong&gt;，好处是可以根据服务端的性能设置合理的阈值，而缺点就是被限制的请求都是无效的请求，处理这些无效的请求本身也会消耗服务器资源。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/03/Yo38vDIal5XfRui.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;在限流的实现手段上来讲，&lt;strong&gt;基于 QPS 和线程数的限流应用最多&lt;/strong&gt;，最大 QPS 很容易通过压测提前获取，例如我们的系统最高支持 1w QPS 时，可以设置 8000 来进行限流保护。线程数限流在客户端比较有效，例如在远程调用时设置连接池的线程数，超出这个并发线程请求，就将线程进行&lt;strong&gt;排队&lt;/strong&gt;或者&lt;strong&gt;直接超时丢弃&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;限流无疑会影响用户的正常请求，所以&lt;strong&gt;必然会导致一部分用户请求失败&lt;/strong&gt;，因此在系统处理这种异常时一定要设置&lt;strong&gt;超时时间&lt;/strong&gt;，防止因被限流的请求不能 **fast fail（快速失败）**而拖垮系统。&lt;/p&gt;
&lt;h3 id=&#34;拒绝服务&#34;&gt;拒绝服务&lt;/h3&gt;
&lt;p&gt;如果限流还不能解决问题，最后一招就是直接拒绝服务了。&lt;/p&gt;
&lt;p&gt;当系统负载达到一定阈值时，例如 CPU 使用率达到 90% 或者系统 load 值达到 2 * CPU 核数时，系统直接拒绝所有请求，这种方式是最暴力但也最有效的系统保护方式。例如秒杀系统在如下几个环节设计过载保护：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在最前端的 Nginx 上设置过载保护，当机器负载达到某个值时直接拒绝 HTTP 请求并返回 503 错误码，在 Java 层同样也可以设计过载保护。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;拒绝服务可以说是一种不得已的兜底方案，用以防止最坏情况发生，防止因把服务器压跨而长时间彻底无法提供服务。像这种系统过载保护虽然在过载时无法提供服务，但是系统仍然可以运作，当负载下降时又很容易恢复，所以每个系统和每个环节都应该设置这个兜底方案，对系统做最坏情况下的保护。&lt;/p&gt;
&lt;h3 id=&#34;总结-2&#34;&gt;总结&lt;/h3&gt;
&lt;p&gt;网站的高可用建设是基础，可以说要深入到各个环节，更要长期规划并进行体系化建设，要在预防（建立常态的压力体系，例如上线前的单机压测到上线后的全链路压测）、管控（做好线上运行时的降级、限流和兜底保护）、监控（建立性能基线来记录性能的变化趋势以及线上机器的负载报警体系，发现问题及时预警）和恢复体系（遇到故障要及时止损，并提供快速的数据订正工具等）等这些地方加强建设，每一个环节可能都有很多事情要做。&lt;/p&gt;
&lt;p&gt;另外，要保证高可用建设的落实，不仅要做系统建设，还要在组织上做好保障。高可用其实就是在说“稳定性”。稳定性是一个平时不重要，但真出了问题就会要命的事儿，所以很可能平时业务发展良好，稳定性建设就会给业务让路，相关的稳定性负责人员平时根本得不到重视，一旦遇到故障却又成了“背锅侠”。&lt;/p&gt;
&lt;p&gt;而要防止出现这种情况，就必须在组织上有所保障，例如可以让业务负责人背上稳定性 KPI 考核指标，然后在技术部门中建立稳定性建设小组，小组成员由每个业务线的核心力量兼任，他们的 KPI 由稳定性负责人来打分，这样稳定性小组就可以把一些体系化的建设任务落实到具体的业务系统中了。&lt;/p&gt;
&lt;h2 id=&#34;8-一些问题&#34;&gt;8. 一些问题&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;1. “06 | 秒杀系统‘减库存’设计的核心逻辑”一文中，很多用户比较关注应用层排队的问题，大家主要的疑问就是应用层用队列接受请求，然后结果怎么返回的问题。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;其实我这里所说的排队，更多地是说在服务端的服务调用之间采用排队的策略。例如，秒杀需要调用商品服务、调用价格优惠服务或者是创建订单服务，由于调用这些服务出现性能瓶颈，或者由于热点请求过于集中导致远程调用的连接数都被热点请求占据，那么那些正常的商品请求（非秒杀商品）就得不到服务器的资源了，这样对整个网站来说是不公平的。&lt;/p&gt;
&lt;p&gt;再比如说，正常整个网站上每秒只有几万个请求，这几万个请求可能是非常分散的，那么假如现在有一个秒杀商品，这个秒杀商品带来的瞬间请求一下子就打满了我们的服务器资源，这样就会导致那些正常的几万个请求得不到正常的服务，这个情况对系统来说是绝对不合理的，也是应该避免的。&lt;/p&gt;
&lt;p&gt;所以我们设计了一些策略，把秒杀系统独立出来，部署单独的一些服务器，也隔离了一些热点的数据库，等等。但是实际上不能把整个秒杀系统涉及的所有系统都独立部署一套，不然这样代价太大。&lt;/p&gt;
&lt;p&gt;既然不能所有系统都独立部署一套，势必就会存在一部分系统不能区分秒杀请求和正常请求，那么要如何防止前面所说的问题出现呢？通常的解决方案就是在部分服务调用的地方对请求进行 Hash 分组，来限制一部分热点请求过多地占用服务器资源，分组的策略就可以根据商品 ID 来进行 Hash，热点商品的请求始终会进入一个分组中，这样就解决了前面的问题。&lt;/p&gt;
&lt;p&gt;我看问的问题很多是说对秒杀的请求进行排队如何把结果通知给用户，我并不是说在用户 HTTP 请求时采用排队的策略（也就是把用户的所有秒杀请求都放到一个队列进行排队，然后在队列里按照进入队列的顺序进行选择，先到先得），虽然这看起来还是一个挺合理的设计，但是实际上并没有必要这么做！&lt;/p&gt;
&lt;p&gt;为什么？因为我们服务端接受请求本身就是按照请求顺序处理的，而且这个处理在 Web 层是实时同步的，处理的结果也会立马就返回给用户。但是我前面也说了，整个请求的处理涉及很多服务调用也涉及很多其他的系统，也会有部分的处理需要排队，所以可能有部分先到的请求由于后面的一些排队的服务拖慢，导致最终整个请求处理完成的时间反而比较后面的请求慢的情况。&lt;/p&gt;
&lt;p&gt;这种情况理论上的确存在，你可能会说这样可能会不公平，但是这的确没有办法，这种所谓的“不公平”，并不是由于人为设置的因素导致的。&lt;/p&gt;
&lt;p&gt;你可能会问（如果你一定要问），采用请求队列的方式能不能做？我会说“能”，但是有两点问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一是体验会比较差，因为是异步的方式，在页面中搞个倒计时，处理的时间会长一点；&lt;/li&gt;
&lt;li&gt;二是如果是根据入队列的时间来判断谁获得秒杀商品，那也太没有意思了，没有运气成分不也就没有惊喜了？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;至于大家在纠结异步请求如何返回结果的问题，其实有多种方案。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一是页面中采用轮询的方式定时主动去服务端查询结果，例如每秒请求一次服务端看看有没有处理结果（现在很多支付页面都采用了这种策略），这种方式的缺点是服务端的请求数会增加不少。&lt;/li&gt;
&lt;li&gt;二是采用主动 push 的方式，这种就要求服务端和客户端保持连接了，服务端处理完请求主动 push 给客户端，这种方式的缺点是服务端的连接数会比较多。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;还有一个问题，就是如果异步的请求失败了，怎么办？对秒杀来说，我觉得如果失败了直接丢弃就好了，最坏的结果就是这个人没有抢到而已。但是你非要纠结的话，就要做异步消息的持久化以及重试机制了，要保证异步请求的最终正确处理一般都要借助消息系统，即消息的最终可达，例如阿里的消息中间件是能承诺只要客户端消息发送成功，那么消息系统一定会保证消息最终被送到目的地，即消息不会丢。因为客户端只要成功发送一条消息，下游消费方就一定会消费这条消息，所以也就不存在消息发送失败的问题了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 在“02 | 如何才能做好动静分离？有哪些方案可选？”一文中，有介绍静态化的方案中关于 Hash 分组的问题。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;大家可能通常理解 Hash 分组，像 Cache 这种可能一个 key 对应的数据只存在于一个实例中，这样做其实是为了保证缓存命中率，因为所有请求都被路由到一个缓存实例中，除了第一次没有命中外，后面的都会命中。&lt;/p&gt;
&lt;p&gt;但是这样也存在一个问题，就是如果热点商品过于集中，Cache 就会成为瓶颈，这时单个实例也支撑不了。像秒杀这个场景中，单个商品对 Cache 的访问会超过 20w 次，一般单 Cache 实例都扛不住这么大的请求量。所以需要采用一个分组中有多个实例缓存相同的数据（冗余）的办法来支撑更大的访问量。&lt;/p&gt;
&lt;p&gt;你可能会问：一个商品数据存储在多个 Cache 实例中，如何保证数据一致性呢？（关于失效问题大家问得也比较多，后面再回答。）这个专栏中提的 Hash 分组都是基于 Nginx+Varnish 实现的，Nginx 把请求的 URL 中的商品 ID 进行 Hash 并路由到一个 upstream 中，这个 upstream 挂载一个 Varnish 分组（如下图所示）。这样，一个相同的商品就可以随机访问一个分组的任意一台 Varnish 机器了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/03/iwzv1lULr6pQjEa.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;另外一个问题，关于 Hash 分组大家关注比较多的是命中率的问题，就是 Cache 机器越多命中率会越低。&lt;/p&gt;
&lt;p&gt;这个其实很好理解，Cache 实例越多，那么这些 Cache 缓存数据需要访问的次数也就越多。例如我有 3 个 Redis 实例，需要 3 个 Redis 实例都缓存商品 A，那么至少需要访问 3 次才行，而且是这 3 次访问刚好落到不同的 Redis 实例中。那么从第 4 次访问开始才会被命中，如果仅仅是一个 Redis 实例，那么第二次访问时其实就能命中了。所以理论上 Cache 实例多会影响命中率。&lt;/p&gt;
&lt;p&gt;你可能还会问，如果访问量足够大，那么只是影响前几次命中率而已，是的，如果 Cache 一直不失效的话是这样的，但是在实际的生产环境中 Cache 失效是很频繁发生的事情。很多情况下，还没等到所有 Cache 实例填满，该商品就已经失效了。所以，我们要根据商品的重复访问量来合理地设置 Cache 分组。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 在“02 | 如何才能做好动静分离？有哪些方案可选？”和“04 | 流量削峰这事应该怎么做？”两篇文章中，关于 Cache 失效的问题。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;首先，咱们要有个共识，有 Cache 的地方就必然存在失效问题。为啥要失效？因为要保证数据的一致性。所以要用到 Cache 必然会问如何保证 Cache 和 DB 的数据一致性，如果 Cache 有分组的话，还要保证一个分组中多个实例之间数据的一致性，就像保证 MySQL 的主从一致一样。&lt;/p&gt;
&lt;p&gt;其实，失效有主动失效和被动失效两种方式。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;被动失效&lt;/strong&gt;，主要处理如模板变更和一些对时效性不太敏感数据的失效，采用&lt;strong&gt;设置一定时间长度&lt;/strong&gt;（如只缓存 3 秒钟）这种&lt;strong&gt;自动失效&lt;/strong&gt;的方式。当然，你也要开发一个后台管理界面，以便能够在紧急情况下手工失效某些 Cache。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;主动失效&lt;/strong&gt;，一般有 Cache 失效中心监控数据库表变化发送失效请求、系统发布也需要清空 Cache 数据等几种场景。其中失效中心承担了主要的失效功能，这个失效中心的逻辑图如下：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/03/nt8DjHQf5OJ9oze.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;失效中心会监控关键数据表的变更（有个中间件来解析 MySQL 的 binglog，然后发现有 Insert、Update、Delete 等操作时，会把变更前的数据以及要变更的数据转成一个消息发送给订阅方），通过这种方式来发送失效请求给 Cache，从而清除 Cache 数据。如果 Cache 数据放在 CDN 上，那么也可以采用类似的方式来设计级联的失效结构，采用主动发请求给 Cache 软件失效的方式，如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/07/03/XPkevyN8zx2qrS7.jpg&#34;
	
	
	
	loading=&#34;lazy&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;这种失效由失效中心将失效请求发送给每个 CDN 节点上的 Console 机，然后 Console 机来发送失效请求给每台 Cache 机器。&lt;/p&gt;
</description>
        </item>
        <item>
        <title>赛马问题</title>
        <link>https://blog.ther.cool/posts/%E8%B5%9B%E9%A9%AC%E9%97%AE%E9%A2%98/</link>
        <pubDate>Thu, 28 May 2020 22:50:42 +0800</pubDate>
        
        <guid>https://blog.ther.cool/posts/%E8%B5%9B%E9%A9%AC%E9%97%AE%E9%A2%98/</guid>
        <description>&lt;h2 id=&#34;问题&#34;&gt;问题&lt;/h2&gt;
&lt;p&gt;有 64 匹马，速度都不同，但每匹马的速度都是定值。每次只能 8 匹马进行比赛，每次比赛只能得到 8 匹马之间的快慢程度，而不是具体速度，即每赛一场最多只能知道 8 匹马的相对快慢。请问最少要比多少次才能获得最快的前 4 匹马 ？&lt;/p&gt;
&lt;h2 id=&#34;解答&#34;&gt;解答&lt;/h2&gt;
&lt;p&gt;1.首先把 64 匹马随机分成 8 组，分别进行 8 次比赛，记录成绩。&lt;/p&gt;
&lt;p&gt;2.再将每组第 1 名集合起来进行 1 次比赛。这是第 9 次比赛。&lt;/p&gt;
&lt;p&gt;3.留下第 9 次比赛前四名的马所在的组。因为对于后 4 名的马所在的组来说，没有一匹有机会进入前四。&lt;/p&gt;
&lt;p&gt;4.剩下的 4 组按照每组第 1 名在第 9 次比赛的成绩排序，这样按照判断原则，只剩下10匹马（如下）可能进入前 4，而第 1 组的第 1 名肯定是跑最快的马。&lt;/p&gt;
&lt;p&gt;第一组： 1 2 3 4
第二组： 1 2 3
第三组： 1 2
第四组： 1&lt;/p&gt;
&lt;p&gt;这样问题就变成 9 匹马找出前 3 快的。情况好的话跑 1 次可以得到，不行的话 2 次。&lt;/p&gt;
&lt;p&gt;第一组： 2 3 4
第二组： 1 2 3
第三组： 1 2
第四组： 1&lt;/p&gt;
&lt;p&gt;方案如下：&lt;/p&gt;
&lt;p&gt;第一组：2 3 4&lt;/p&gt;
&lt;p&gt;第二组：1 2 3&lt;/p&gt;
&lt;p&gt;第三组：1 2&lt;/p&gt;
&lt;p&gt;共 8 匹马进行比赛。&lt;/p&gt;
&lt;p&gt;1）如果第三组的 1 为本轮第 2 名 (最多也是第 2 名，因为第二组的 1 比它快)，则说明第二组的 1 为最终的第 2 名，第三组的 1 为最终的第 3 名。此时剩下 7 匹马再比一场决出最终的第 4 名，共 &lt;strong&gt;11&lt;/strong&gt; 场。&lt;/p&gt;
&lt;p&gt;2）如果第三组的 1 为本轮第 3 名，即最终的第 4 名，则说明第 4 组的 1 肯定不是最终的前 4 名。所以最终的 2、3、4 名为本场的 1、2、3 名。&lt;strong&gt;共 10 场&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;3）如果第三组的 1 为本轮第 4 名及之后的，则说明第 4 组的 1 肯定不是最终的前 4 名。所以最终的 2、3、4 名为本场的 1、2、3 名。&lt;strong&gt;共 10 场&lt;/strong&gt;。&lt;/p&gt;
</description>
        </item>
        <item>
        <title>设计模式</title>
        <link>https://blog.ther.cool/posts/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/</link>
        <pubDate>Thu, 02 Jan 2020 16:57:10 +0800</pubDate>
        
        <guid>https://blog.ther.cool/posts/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/</guid>
        <description>&lt;h2 id=&#34;面向对象设计原则&#34;&gt;面向对象设计原则&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;可维护性(Maintainability)&lt;/strong&gt;：指软件能够被理解、改正、适应及扩展的难易程度。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;可复用性(Reusability)&lt;/strong&gt;：指软件能够被重复使用的难易程度。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/11/26/rQXUKy9lV6kOPc8.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;p&gt;面向对象设计的目标之一在于&lt;strong&gt;支持可维护性复用&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id=&#34;设计模式&#34;&gt;设计模式&lt;/h2&gt;
&lt;h3 id=&#34;设计模式的分类&#34;&gt;设计模式的分类&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;根据目的（模式是用来做什么的）可分为&lt;strong&gt;创建型&lt;/strong&gt;(Creational)，&lt;strong&gt;结构型&lt;/strong&gt;(Structural)和&lt;strong&gt;行为型&lt;/strong&gt;(Behavioral)三类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;创建型模式主要用于&lt;strong&gt;创建对象&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;结构型模式主要用于&lt;strong&gt;处理类或对象的组合&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;行为型模式主要用于描述类或对象如何&lt;strong&gt;交互&lt;/strong&gt;和怎样&lt;strong&gt;分配职责&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;根据范围，即模式主要是处理类之间的关系还是处理对象之间的关系，可分为&lt;strong&gt;类模式&lt;/strong&gt;和&lt;strong&gt;对象模式&lt;/strong&gt;两种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;类模式处理类和子类之间的关系，这些关系通过继承建立，在编译时刻就被确定下来，是一种&lt;strong&gt;静态&lt;/strong&gt;关系；&lt;/li&gt;
&lt;li&gt;对象模式处理对象间的关系，这些关系在运行时变化，更具&lt;strong&gt;动态&lt;/strong&gt;性。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/11/26/unQKGlX4xp6gMvr.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;h3 id=&#34;创建型模式&#34;&gt;创建型模式&lt;/h3&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/11/26/18tq7SsnegLZVfb.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;h3 id=&#34;结构型模式&#34;&gt;结构型模式&lt;/h3&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/11/26/Yr2zK5eNXg1T6wq.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;h3 id=&#34;行为型模式&#34;&gt;行为型模式&lt;/h3&gt;
&lt;p&gt;&lt;img src=&#34;https://s2.loli.net/2022/11/26/E1cFZ4Nx2iWwD6Q.png&#34;
	
	
	
	loading=&#34;lazy&#34;
	
		alt=&#34;img&#34;
	
	
&gt;&lt;/p&gt;
&lt;h3 id=&#34;一些名词&#34;&gt;一些名词&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;模式(Pattern)&lt;/strong&gt; 起源于建筑业。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;模式：&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Context&lt;/strong&gt;（模式可适用的前提条件）；&lt;/p&gt;
&lt;p&gt;Theme或&lt;strong&gt;Problem&lt;/strong&gt;（在特定条件下要解决的目标问题）；&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;（对目标问题求解过程中各种物理关系的记述）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;模式是&lt;strong&gt;特定环境下&lt;/strong&gt;人们解决某类重复出现&lt;strong&gt;问题&lt;/strong&gt;的一套成功或有效的&lt;strong&gt;解决方案&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A pattern is a successful or efficient &lt;strong&gt;solution&lt;/strong&gt; to a recurring &lt;strong&gt;problem&lt;/strong&gt; within a &lt;strong&gt;context&lt;/strong&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;软件模式&lt;/strong&gt;：在&lt;strong&gt;一定条件下&lt;/strong&gt;的&lt;strong&gt;软件开发问题&lt;/strong&gt;及其&lt;strong&gt;解法&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;问题描述；前提条件（环境或约束条件）；解法；效果。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;大三律(Rule of Three)&lt;/strong&gt;：只有经过3个以上不同类型（或不同领域）的系统的校验，一个解决方案才能从候选模式升格为模式。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;设计模式(Design Pattern)&lt;/strong&gt;：一套&lt;strong&gt;被反复使用的&lt;/strong&gt;、&lt;strong&gt;多数人知晓的&lt;/strong&gt;、&lt;strong&gt;经过分类编目的&lt;/strong&gt;、&lt;strong&gt;代码设计经验的&lt;/strong&gt;总结。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;是一种用于对软件系统中不断重现的设计问题的&lt;strong&gt;解决方案&lt;/strong&gt;进行&lt;strong&gt;文档化&lt;/strong&gt;的技术。&lt;/p&gt;
&lt;p&gt;是一种&lt;strong&gt;共享专家设计经验&lt;/strong&gt;的技术。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;目的：为了可重用代码、让代码更容易被他人理解、提高代码可靠性。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设计模式的&lt;strong&gt;定义&lt;/strong&gt;：&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;设计模式是在&lt;strong&gt;特定环境下&lt;/strong&gt;为解决&lt;strong&gt;某一通用软件设计问题&lt;/strong&gt;提供的&lt;strong&gt;一套定制的解决方案&lt;/strong&gt;，该方案描述了对象和类之间的相互作用。&lt;/p&gt;
&lt;p&gt;Design patterns are descriptions of communicating objects and classes that are customized to &lt;strong&gt;solve&lt;/strong&gt; a general &lt;strong&gt;design&lt;/strong&gt; &lt;strong&gt;problem&lt;/strong&gt; in a particular &lt;strong&gt;context&lt;/strong&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;设计模式的基本要素：&lt;strong&gt;关键要素&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;模式名称&lt;/strong&gt;、&lt;strong&gt;问题&lt;/strong&gt;、目的、&lt;strong&gt;解决方案&lt;/strong&gt;、&lt;strong&gt;效果&lt;/strong&gt;、实例代码和相关设计模式。&lt;/p&gt;
</description>
        </item>
        
    </channel>
</rss>
