带你全面了解 Git 系列 01 - 深入 Git 原理
对大多数程序员同学来说,Git 应该是日常工作中接触的最多的工具之一了。我们每天都在和 Git 打交道,通过 Git 提交代码,在 Github 上寻找优秀的开源项目,参与技术讨论。那不知道大家有没有真正去了解过 Git,它为什么会出现?它到底是什么?它又拥有哪些“魔力”,能成为当今最受欢迎的版本控制系统呢?
在接下来的文章里,我会带大家从 Git 版本控制系统发展史、Git 版本控制原理、Git 分支原理、Git 暂存区原理、Git 数据完整性这 6 个方面对 Git 进行详细剖析,力图为大家揭开隐藏在 Git 背后的原理。也希望在读完这篇文章后,大家能对 Git 产生不一样的认知。
一 版本控制系统发展史
要系统的了解一项技术,首先应该去了解它的历史,以及它出现的背景和原因。
Git 是一种版本控制系统,要想全面了解它,首先要了解什么是版本控制系统以及版本控制系统的演变过程。
版本控制是一种记录一个或若干个文件内容变化,以便将来能查阅特定版本修订情况的系统。 -百度百科
简单来说,版本控制系统的好处在于就算你乱来一气把整个项目的文件改的改,删的删,你也仅仅只需要很少的时间就能把项目恢复到原先的状态。
那在 git 出现之前,历史上还出现过哪些版本控制系统?以及它们各自的特点都有哪些呢?
版本控制系统的演进过程大致可以分为以下 3 个阶段,第一阶段是用本地文件系统作为版本控制系统的阶段,第二阶段是像 CVS, SVN 这种有集中式的服务器来管理版本的阶段,第三阶段则是像 Git, Mercurial 这种用分布式方式来管理版本的阶段。
1.1 本地版本控制系统
在第一个阶段里,没有专门的版本控制系统,人们通常通过拷贝整个项目目录的方式来保存文件不同的版本。
这么做的好处在于上手非常简单,而缺点在于特别容易犯错,有时会混淆所在的工作目录,一不小心就会写错或者覆盖意想之外的文件。
之后虽然也发展出了一些能记录文件的历次更新差异的本地版本控制系统(如 RCS,一个针对单独文件的版本管理工具),但这种方式还是存在不少问题,特别是不利于团队成员之间的协同工作。
1.2 集中式版本控制系统
为了解决团队成员之间协同工作的问题,集中化的版本控制系统(CVCS)出现了。这类系统(如 CVS、Subversion)都有一个单一的集中管理的服务器,用来保存所有文件的修订版本。
集中式的版本控制系统具备版本管理和分支管理的能力。采用这种版本管理方式,每个人都可以在一定程度上看到项目中的其他人正在做些什么。而管理员也可以轻松掌控每个开发者的权限,并且管理一个集中式的版本管理系统要远比在各个客户端上维护本地数据库来得轻松容易。
集中式的版本管理方式从公司管理项目的角度出发是非常合适的,不仅让开发者能围绕一个项目来协同进行开发,同时也能很精细的控制相关人员的访问和操作权限。即使发生了由于某个员工的疏忽导致的代码外泄,那外泄的版本也仅仅只是整个代码仓库的一部分(只要外泄的不是中央服务器的数据)。
那既然集中式的版本管理方式已经这么好了,那为什么现在反而是分布式的版本控制系统在大行其道呢?我们接下来看看分布式版本控制系统的特点:
1.3 分布式管理系统
分布式版本控制系统,像 Git、Mercurial 等,最大的特点就是分布式了,客户端并不只是最新版本的文件快照,而是把整个代码仓库完整的镜像下来。
它的分布式带来的最大好处就是“去中心化”,因为每个客户端都是一个完整的代码仓库,所以所有人都可以在别人的基础上去构建更强大或更有针对性的功能,你可以 fork(严格意义上 fork 是 Github 等托管平台提供的功能,但是其核心也是 Git 的去中心化设计)任何一个你喜欢的项目,然后按需要修改成适合自己的项目,也可以修改之后发起 pull request 把你的修改 merge 回之前的项目里去,为原来的项目做贡献。
大家的初衷不是为了开发某个项目而来(这是集中式版本控制系统设计的初衷),而是为了贡献更强大的功能给社区,这里没有中心化服务器,没有主力和权威,所有人都是平等的。这样最终的项目将是整个社区所有人共同努力的结果,同时也让项目本身极具生命力。
分布式版本控制系统让开源项目的代码管理变得更加方便,而近些年流行的开源运动又反过来推动了分布式版本控制系统的发展,使其逐渐成为了如今版本控制系统的主流。
1.4 Git
介绍完以上三种版本控制系统之后,我们继续来探究为什么会诞生 Git?
时间回到 2005 年,这时候大名鼎鼎的 linux 项目在管理它们的代码仓库时面临如下几个问题:
BitKeeper (当时的一款分布式版本控制系统) 授权到期,可供选择的有 VCS,RCS,SVN 等。
此时的 Linux 项目已经是一个拥有数百万行代码的巨型仓库。
非线性开发需求很多,同时需要并行开发多项功能。
参与 Linux 维护的人员很多,而且来自世界各地。
要解决以上几个问题,linux 社区就不得不选择一款完全分布式,同时强力支持非线形开发(允许成百上千个并行分支),并且能保证操作速度和存储容量的版本控制系统。
而当时市面上还没有版本控制系统能同时满足这几个特性,于是 linux 社区(其实也就是 linux 创始人 linus)选择了自研版本控制系统,也就是如今的 Git。
linus 基于之前使用 BitKeeper(也是一个分布式版本控制系统)时的经验教训,以一个文件系统专家和内核设计者的视角对 Git 进行了设计,这些设计让 Git 拥有了如下特性:
强大的多分支管理能力
操作速度快,性能好
拥有“暂存区”
完全分布式
保障数据完整性
近些年,也就是凭借以上优点,Git 越来越受到开发者的欢迎,时至今日,Git 也逐渐成长为当今最受欢迎的版本控制系统。
接下来,我会带大家深入 Git 内部,为大家解读 Git 是如何设计它的版本控制功能,又是如何实现方便快速的分支管理,以及它的暂存区是什么,它又是如何保障存储数据完整性的?
为了更方便的讲解,这里准备了一个 demo 项目为大家进行相关的演示,它的目录结构如下:
目前对这个 demo 仅执行了 git init
操作,接下来我们会基于这个 demo 文件对 Git 的原理进行更加详细的剖析。
二 Git 版本控制原理
Git 是一种版本控制系统,那它是如何实现版本控制的呢?要回答这个问题,我们就要回到 Git 是什么这个本质问题上。
2.1 Git 到底是什么
Git 其实是一个内容寻址(content-addressable)文件系统,并在此之上搭建了版本控制系统的功能。
注意这里有两个关键字——内容寻址和版本控制,我们先来看看什么是内容寻址?
对于一个内容寻址系统来说,系统记录的是一个内容地址(content-address),该内容地址是对应数据的一个唯一且持久的识别符,它是通过加密哈希算法(如,SHA-1 或 MD5)计算出来的一串值,当我们需要数据时,提供该内容地址,系统即可通过该地址获取数据的物理地址,返回数据;同时,对于数据的任何变更都将导致内容地址发生变化。 -Pro git
内容寻址系统其实就是一个键值对数据库,你可以向 Git 仓库中插入任意类型的内容,它会返回一个唯一的识别符,通过该识别符可以在任意时刻再次取回该内容。
那大家可能会好奇了,我们日常使用 Git 的时候怎么好像从来没看到过这个数据库呢?
2.2 Git 的内容寻址数据库
那是因为 Git 对键值对数据库进行了一定的包装,我们在日常使用 Git 的时候一般不会直接接触它。
这个数据库存放在.git
目录下,该目录存储了项目中几乎所有文件的所有版本的数据。当你从某个 Git 仓库中 clone 项目时,实际上也就是拷贝该项目的.git
目录。
我们来看看项目里(特指上面的 demo 项目, 下同)的.git
目录下都有哪些内容?
以上其实就是一个最典型的 Git 仓库所具有的结构。config
存储的是项目的一些配置信息。description
供 GitWeb 程序使用,hooks
存储的是包含客户端和服务端的钩子脚本(比如在提交时校验 git commit
格式的脚本就放在这里),info
目录存储的是不想被记录在.Gitignore
文件中的忽略模式,COMMIT_EDITMSG
保存最近一次 commit 的提交信息,logs
保存的是 commit 相关的日志信息,index
文件保存的则是暂存区相关的信息,refs
目录存储指向数据(分支、远程仓库和标签等)的提交对象的指针。HEAD
存储的是目前被检出的分支。
最后是objects
目录了,这个目录下就是 Git 的键值对数据库,它存储了 Git 中所有文件及其历史版本。那这个目录里到底是不是如我们所想是通过键值对的方式存储文件的呢?我们可以通过 demo 项目进行验证。
回到 demo 项目中,我们查看 Git 的键值对数据库(.git/objects
)目录,发现它里面除了系统自动创建的info
和pack
目录之外(该目录下也均为空),没有存储其他数据。
接下来,我们执行git add index.html
, 然后再次查看该目录就能发现 .git/objects
目录中多了一条记录,这条记录由一连串哈希码组成。这时我们知道了,git 可以通过git add
命令把文件插入到 Git 数据库中,并获取到一个唯一哈希码。(关于唯一哈希码后面章节会有介绍)
那这个哈希码是不是对应我们添加的 index.html
文件呢?我们可以通过 git cat-file
命令来查看一下,一旦你把内容存储到 Git 数据库中,你就可以通过 git cat-file
命令从 Git 数据库中取回数据。( -p 参数可以获取到数据的内容)。
我们执行下列命令:
可以看到通过前面生成的哈希码确实取到了存入 Git 数据库中的 index.html
文件。
至此,我们知道了 Git 存储数据确实符合了键值对数据库的特征。接下来我们继续看看 Git 又是如何基于它来进行版本控制呢?
2.3 Git 版本控制
要知道 Git 是如何进行版本控制的,就必须要先了解 Git 处理数据的方式。
2.3.1 Git 处理数据的方式
Git 和其它版本控制系统(SVN 和其他相似工具)的主要区别在于 Git 对待数据的方式。其他大多数版本管理系统(CVS,SVN)是一种基于差异的版本控制系统,它们以文件变更列表的方式存储数据。
Git 则不一样,它更像是把数据看作是对小型文件系统的一系列快照。 在 Git 中,每当你提交更新,它就会对当时的全部文件创建一个快照并保存这个快照的索引。 为了效率,如果文件没有修改,Git 不再重新存储该文件,而是只保留一个链接指向之前存储的文件。Git 更像是以快照流的方式来存储数据。
那这个快照流中都有哪些快照?Git 又是怎么样表示这些快照的呢?
2.3.2 Git 对象
在 Git 中有三种不同类型的 Git 对象,就分别对应了 Git 快照流中三种不同类型的快照。
数据对象(blob): 表示单个文件内容,对应 Git 快照流中单个文件的快照。
树对象(tree): 表示多个文件组织的信息,对应 Git 快照流中多个文件的快照。
提交对象(commit):表示一次提交相关的信息,对应 Git 快照流中一次 Version 的快照。
(注意:上面说的树对象也不仅仅是表示多个文件组织的信息,也可以表示包含文件名的单个文件的信息,这里为了大家理解方便,先把它归结为表示多个文件组织的信息)
下面从 demo 项目里看看这些 Git 对象
前面说到 git cat-file
是重量级武器,前面我们也使用过 git cat-file -p
来取回文件的内容。而当使用 git cat-file -t
命令时,则可以让 Git 根据给定的哈希值给出对应的文件对象类型。
我们来看看上一节中 index.html
这个文件对象的类型
可以看出它是一个 blob 类型的对象,那我们上一节也验证了它保存的是确实是一个文件相关的信息。
我们继续对项目进行操作,执行git commit
,把暂存区(后面会为大家详细剖析暂存区)的文件提交到 Git 版本库中。
这时我们再查看.objects
目录,发现该目录中多了两个文件。
先看新增的第一个文件,我们使用git cat-file
命令来查看一下这个文件对应的内容。
这个文件的内容里出现了我们之前提交文件的一些基本信息。我们再来看看该文件的类型。
这就是树类型,它主要解决了多个文件组织的问题,同时它也解决了 blob 对象只有文件内容,没有文件名的问题。
接下来再看看新增的第二个文件,使用git cat-file
查看该文件的内容如下:
该文件的内容里包含了上面说的 tree 类型及其对应的哈希值,除此之外还有提交者的信息和时间,以及提交的描述文案。
这是提交类型,提交类型里主要包含了一些和提交相关的信息,以及该次提交对应的树对象信息。
以上这三种 Git 对象之间的关系如下图所示:
2.3.3 实现版本控制
再来看 2.3.1 小节中提到的这张 Git 如何处理数据的图:
试想一下,如果仅有 blob 这种类型的对象,那怎么样能表示快照流呢?快照流一定是多个文件对象的集合,而 blob 类型只能表示单个文件,所以为了表示多个文件,Git 就引入 tree 类型的对象,tree 类型的对象能解决多个文件组织在一起的问题。
除了组织多个文件的问题,我们肯定也想知道,是谁保存了这些快照,在什么时刻保存的,以及为什么保存这些快照,以及这次快照是基于哪次快照来的,这次快照又产生了哪些新快照呢?而以上这些,正是提交对象(commit)能为你保存的基本信息。
下图是涉及到多次提交时,Git 三种对象类型之间的关系图。
通过以下这张图,我们就能大致知道 Git 是如何做版本控制的。
上图右边不同的 commit 对象就对应着左边不同的 Version,而每个 commit 对象都关联着一个树对象,而树对象也就是一个文件系统的快照,即左边的 FileA, FileB, FileC 的集合。而树对象下又关联着不同的 blob 对象。
通过以上三种类型的互相组合,再配合前面讲到的内容寻址系统,Git 就在这两者之上,巧妙的实现了版本控制的功能。
那了解 Git 到底是如何做版本控制之后,其他的概念就比较好理解了,接下来给大家介绍下 Git 的分支原理。
三 Git 分支原理
这里先给出结论——Git 分支本质上就是指向提交对象的指针。
接下来借助 demo 项目帮助大家理解:
在 2.2 节里说过,.git/refs
目录里存储的是指向数据(分支、远程仓库和标签等)的提交对象的指针,其中分支相关的指针存储在.git/refs/heads
里,我们来看看项目里这个目录的内容。
因为此时该项目只有 master 分支(没有切过其他分支),所以该目录下只有 master 这个文件。
我们查看 这个文件,发现它的内容是一串 Git 对象对应的哈希值。
使用 git cat-file
查看该哈希值对应的内容,发现该哈希值对应的对象是一个 commit 对象。
这时,我们再基于 master 分支新建一个 test 分支,再次查看 refs/heads
目录,发现该目录下新增了一个 test 文件。
查看 test 文件的内容如下:
和前面的 master 内容完全一样,该指针文件也是指向同一次的提交对象。
我们用下面的图来回顾上述的过程(为了更好的描述这个过程,在图中虚拟了几次提交):
综上,我们证实了 Git 的分支就是指向提交对象的指针。
也正因为如此,所有分支的操作都是非常快的,比如创建新分支,其实也就是往一个文件中写入一串关联 commit 对象的 40 位哈希值,这速度能不快么?
那我们再来看看分支的切换,分支的切换和分支的创建有一点不一样。
它不一样的地方就在于HEAD
这个特殊指针。在 Git 中,HEAD
是指向当前检出分支的指针,所以切换分支的操作,其实就是HEAD
指针移动的操作。
比如我们现在切换到新创建的 test 分支去:
(1) 切换 test 分支之前,如果没有做切换分支的操作,HEAD 还是指向 master 分支。
(2) 切换 test 分支之后,HEAD 的指针就指向了 test 分支
从上面的内容我们可以看出,Git 分支的新建,销毁和切换其实本质上都是对指针的操作。这也是 Git 在分支管理上优于其他版本控制系统的地方。不像其他版本控制系统的分支管理,如 svn,它的分支创建虽然说不是完全的 copy 源目录(基于 Linux 的软链接),但是它操作分支的速度和便捷性还是和 Git 有着较大的差距。
四 Git 暂存区
讲 Git 暂存区之前,我们先回顾一下经典的 Git 工作流程。
在工作区中修改文件。
将你想要下次提交的更改选择性地暂存。
提交更新,找到暂存区的文件,将快照永久性存储到 Git 版本库中。
以上流程中出现了三个概念 —— 版本库,暂存区,工作区,那这三者分别表示什么呢?
版本库:Git 用来保存项目的元数据和对象数据库的地方, 这是 Git 中最重要的部分。
工作区:针对项目的某个版本提取出来的内容。 这些从 Git 数据库中提取出来的文件,放在磁盘上供你使用或修改。
暂存区: 是一个“中间区域”,在真正提交到 Git 版本库之前,提供一个缓冲区域。
暂存区本质上是一个保存了下次将要提交的文件列表信息的文件,其内容存储在 .git/Index
目录下。
我们来看看 demo 项目里的暂存区文件,由于.git/index
这个文件是一个二进制文件,如果直接通过文本打开的话会发现全是乱码,我们需要通过 hexdump 以十六进制的方式显示该文件。
下面是上述暂存区文件(暂存区仅有单个文件)大致对应的内容:
其中各项的含义如下:
DIRC: 索引头信息,其中包含了合法索引文件的标识
DIRC
及索引文件版本和索引文件中文件数目等信息。ctime: 文件创建时间。
mtime: 文件修改时间。
device: 文件存储设备编号
inode: inode 编号。
mode: 文件模式。
UID: 所属用户 UID。
GID: 所属用户组。
File Size: 文件大小。
Entry SHA-1: Git 对象文件的 SHA-1 哈希。
Flags: 标识位,包含假设不变标识,扩展标识及文件名等信息。
事实上 Git 的索引文件是相当复杂的,除了上面提到的文件索引之外,还有目录索引(对于有目录的 Git 仓库,通常还会额外引入目录索引,以实现更快的工作目录重建)以及其他一些索引,包括 UNTR (用于缓存在工作区但未提交的文件索引),FSMN(通过文件系统所提供信息的变更来判断文件是否发生了改变)等。
(关于这部分更详细的内容可查看 这篇文章)
那了解完暂存区大致都有哪些内容之后,我们接下来结合 demo 项目以及上面说的 Git 经典工作流程给大家演示一下暂存区的工作原理。
接着上面的 demo 来,此时工作区、暂存区和 Git 版本库的状态如下图所示:
现在我们在工作区新增一个文件index.md
,然后运行git status
, 此时会出现 “Changes not staged for commit” 提示信息,这是因为 Git 判断工作区的文件与暂存区里的文件存在不同。此时三者的状态如下:
接下来我们运行 git add index.md
来将修改添加到暂存区中。此时三者的状态如下:
这时工作区的文件和暂存区的文件已经保持一致,但是由于暂存区和版本库不同,运行 git status
的话会看到 “Changes to be committed” 。——也就是说,现在预期的下一次提交与上一次提交不同。
最后我们运行 git commit 来进行提交。此时三者的状态如下:
现在运行 git status 就没有任何输出了,因为此时三个区域的文件又变得相同了。以上过程就为大家揭示了 git 暂存区的秘密。那现在我们再来看,暂存区其实也并没有我们想象的那么复杂。
那说了这么多暂存区的原理,我们回到 Git 为什么要设计暂存区这个问题上来?
这么设计的好处在于可以对提交的文件有更多的控制。如果你在一次开发工作中,修改了好几个没有什么逻辑关联性的文件,这时候如果没有“暂存区”的话,你就需要把这些不相关的文件放在一起提交,而如果有暂存区的话,就不存在这些问题了,你可以根据任何你想要的维度进行提交。
所以,Git 通过暂存区的设计,在工作区和版本库之前,提供了给用户“缓冲”的区域,用户可以结合自己的需要灵活的使用暂存区。
五 Git 数据完整性
最后我们来看看 Git 是如何保障数据完整性的。
Git 中所有的数据在存储前都会计算校验和,以此来保障数据完整性,做到内容不被窜改的。
它用来计算校验和的机制叫做 SHA-1 散列(hash,哈希)。 这是一个由 40 个十六进制字符(0-9 和 a-f)组成的字符串,Git 会以识别出的对象的类型作为开头来构造一个头部信息,接着 Git 会在头部的第一部分添加一个空格,随后是数据内容的字节数,最后是一个空字节(null byte)。
git hash = SHA1(对象类型 + 空格 + 数据内容的字节数 + 空字节 + 原始数据)
(因为文件头部存储了数据长度等信息,又能在一定程度增加 SHA1 散列的安全性)
最终生成的 SHA-1 哈希看起来是这样:
24b9da6552252987aa493b52f8696cd6d3b00373
如果想伪造一个一摸一样的 SHA-1 值,难度很大,可以看下面的例子。
每个十六进制的数字用于表示一个 4 位的二进制数字,因此 40 位的 SHA1 哈希值的输出为实为 160bit。拿双色球博彩打一个比喻,要想制造相同的 SHA1 哈希值就相当于要选出 32 个“红色球”,每个红球有 1 到 32 个(5 位的二进制数字)选择,而且红球之间可以重复。相比“双色球博彩”总共只需选出 7 颗球,SHA1“中奖”的难度就相当于要连续购买五期“双色球”并且必须每一期都要中一等奖。当然由于算法上的问题,制造冲突(相同数字摘要)的几率没有那么小,但是已经足够小,能够满足 Git 对不同对象的进行区分和标识了。
-Git 权威指南
下面我们就以 commit hash 为例来演示一下 git 是如何生成对应的 commit 哈希值的。
回到 demo 项目中
先使用
git cat-file
命令来看看此时 HEAD 对应的内容。计算提交信息中包含的字符数,这里总共包含 181 个字符。
在提交信息的前面加上内容 commit 181<null>(<null>为空字符),然后执行 SHA1 哈希算法。
上面命令得到的哈希值和用 Git log 看到的是一样的。
以上就是哈希的生成方法,所有 Git 对象的哈希都是这么生成,区别就在不同的对象类型的头部信息有些许不同,树对象以 tree 开头,文件对象以 blob 开头。
六 总结
到这里,这篇文章的内容也就基本结束了,最后带大家一起来回顾一下整篇文章的内容。
文章一开始带大家了解了版本控制系统的演变史,从本地版本控制系统,到集中式版本控制系统,再到分布式版本控制系统。我们知道了分布式版本控制系统的发展能有今天这种程度,究其原因,还是因为它和开源运动之间的相互成就,分布式版本管理系统的“去中心化”特性天生就适合开源项目的代码管理。
接着我们介绍了分布式版本管理系统中的明星——Git 的诞生,它被 linux 之父设计,有着很多其他版本控制系统不具有的优点,比如它对待数据的方式,它的分支模型,它的操作速度,暂存区的设计以及数据完整性的保障。这些都逐渐让它成为如今最炙手可热的版本控制系统。
之后,我们从 Git 的版本控制,Git 的分支模型,Git 的暂存区设计以及 Git 的数据完整性这 4 方面详细带大家了解了 Git。Git 的本质是一个内容寻址系统,基于内容寻址系统之上,巧妙的结合了三种 Git 对象( blob 对象,tree 对象,commit 对象),实现了版本控制的能力。而 Git 的分支,本质上就是指向提交对象的指针,新建分支不过是创建一个指向某个 commit 记录的指针,而切换分支,本质上也就是指针的移动罢了,所以分支的操作,比如新建,切换会非常的快。
Git 暂存区也是 Git 一大精妙的设计,它是一个位于工作区和 Git 版本库的中间区域,它能让你更好的控制要提交的文件,它是一个包含文件索引的目录树,其中记录了文件相关的信息,以及文件对应的哈希值。
最后我们讲到了 Git 是如何实现数据完整性的,Git 通过 sha1 散列计算文件校验和,这种算法得到的值很难被伪造,另外一旦文件发生变化,生成的哈希值就会发生改变,所以在很大程度上,sha1 散列算法能保证数据的完整性,此外,我们还动手对一个 commit 对象实现了哈希的计算。
最后通过一张图,来描述上面说的整个 Git 的工作流程。
当 clone 一个新项目或者切换一个新分支时,首先会修改 HEAD 的指向,使其指向新的分支引用。然后将暂存区的内容填充为该分支最新提交的快照,之后将暂存区的内容复制到工作区中。
工作区修改文件之后,通过 git add
把修改的内容同步到暂存区,此时生成了新的 blob 和 tree (有目录的话) 对象,之后再通过git commit
命令把暂存区的文件添加到版本库中,此时生成新的 tree 对象和 commit 对象。而这时新的 commit 对象又通过 parent 参数,和之前的 commit 对象进行关联。
以上就是文章的所有内容,谢谢大家的观看,也希望大家在阅读完文章之后,能有所收获,同样也希望大家可以指出文章中出现的问题,欢迎大家在评论区进行讨论。
七 参考文献
版权声明: 本文为 InfoQ 作者【淼💦 淼】的原创文章。
原文链接:【http://xie.infoq.cn/article/308a9fe53f0d7dc51283e4347】。文章转载请联系作者。
评论 (1 条评论)