<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>程序设计 | 酷 壳 - CoolShell</title>
	<atom:link href="https://coolshell.cn/category/progdesign/feed" rel="self" type="application/rss+xml" />
	<link>https://coolshell.cn</link>
	<description>享受编程和技术所带来的快乐 - Coding Your Ambition</description>
	<lastBuildDate>Thu, 09 Feb 2023 03:41:05 +0000</lastBuildDate>
	<language>zh-CN</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.2</generator>
	<item>
		<title>ETCD的内存问题</title>
		<link>https://coolshell.cn/articles/22242.html</link>
					<comments>https://coolshell.cn/articles/22242.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Thu, 05 May 2022 08:13:37 +0000</pubDate>
				<category><![CDATA[Go 语言]]></category>
		<category><![CDATA[程序设计]]></category>
		<category><![CDATA[easegress]]></category>
		<category><![CDATA[etcd]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Performance]]></category>
		<guid isPermaLink="false">https://coolshell.cn/?p=22242</guid>

					<description><![CDATA[<p>今天跟大家分享一个etcd的内存大量占用的问题，这是前段时间在我们开源软件Easegress中遇到的问题，问题是比较简单的，但是我还想把前因后果说一下，包括，为...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/22242.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/22242.html">ETCD的内存问题</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><img decoding="async" loading="lazy" class="alignright size-full wp-image-22247" src="https://coolshell.cn/wp-content/uploads/2022/05/etcd.png" alt="" width="275" height="183" />今天跟大家分享一个etcd的内存大量占用的问题，这是前段时间在我们开源软件Easegress中遇到的问题，问题是比较简单的，但是我还想把前因后果说一下，包括，为什么要用etcd，使用etcd的用户场景，包括etcd的一些导致内存占用比较大的设计，以及最后一些建议。希望这篇文章不仅仅只是让你看到了一个简单的内存问题，还能让你有更多的收获。当然，也欢迎您关注我们的开源软件，给我们一些鼓励。</p>
<h4>为什么要用ETCD</h4>
<p>先说一下为什么要用etcd。先从一个我们自己做的一个API网关 &#8211; Easegress（<a href="https://github.com/megaease/easegress">源码</a>）说起。</p>
<p><a href="https://github.com/megaease/easegress" target="_blank" rel="noopener">Easegress</a> 是我们开发并开源的一个API应用网关产品，这个API应用网关不仅仅只是像nginx那样用来做一个反向代理，这个网关可以做的事很多，比如：API编排、服务发现、弹力设计（熔断、限流、重试等）、认证鉴权（JWT，OAuth2，HMAC等）、同样支持各种Cloud Native的架构如：微服务架构，Service Mesh，Serverless/FaaS的集成，并可以用于扛高并发、灰度发布、全链路压力测试、物联网……等更为高级的企业级的解决方案。所以，为了达到这些目标，在2017年的时候，我们觉得在现有的网关如Nginx上是无法演进出来这样的软件的，必需重新写一个（后来其他人也应该跟我们的想法一样，所以，Lyft写了一个Envoy。只不过，Envoy是用C++写的，而我用了技术门槛更低的Go语言）</p>
<p>另外，Easegress最核心的设计主要有三个：</p>
<p><span id="more-22242"></span></p>
<ul>
<li>一是无第三方依赖的自己选主组集群的能力</li>
<li>二是像Linux管道命令行那样pipeline式的插件流式处理（支持Go/WebAssembly）</li>
<li>三是内置一个Data Store用于集群控制和数据共享。</li>
</ul>
<p>对于任何一个分布式系统，都需要有一个强一制性的基于Paxos/Raft的可以自动选主机制，并且需要在整个集群间同步一些关键的控制/配置和相关的共享数据，以保证整个集群的行为是统一一致的。如果没有这么一个东西的话，就没有办法玩分布式系统的。这就是为什么会有像Zookeeper/etcd这样的组件出现并流行的原因。注意，Zookeeper他们主要不是给你存数据的，而是给你组集群的。</p>
<p>Zookeeper是一个很流行的开源软件，也被用于各大公司的生产线，包括一些开源软件，比如：Kafka。但是，这会让其它软件有一个依赖，并且在运维上带来很大的复杂度。所以，Kafka在最新的版本也通过内置了选主的算法，而抛弃了外挂zookeeper的设计。Etcd是Go语言社区这边的主力，也是kubernetes组建集群的关键组件。Easegress在一开始（5年前）使用了gossip协议同步状态（当时想的过于超前，想做广域网的集群），但是后发现这个协议太过于复杂，而且很难调试，而广域网的API Gateway也没遇到相应的场景。所以，在3年前的时候，为了稳定性的考量，我们把其换成了内嵌版本的etcd，这个设计一直沿用到今天。</p>
<p>Easegress会把所有的配置信息都放到etcd里，还包括一些统计监控数据，以及一些用户的自定义数据（这样用户自己的plugin不但可以在一条pipeline内，还可以在整个集群内共享数据），这对于用户进行扩展来说是非常方便的。软件代码的扩展性一直是我们追求的首要目标，尤其是开源软件更要想方设法降低技术门槛让技术易扩展，这就是为什么Google的很多开源软件都会选使用Go语言的原因，也是为什么Go正在取代C/C++的做PaaS基础组件的原因。</p>
<h4>背景问题</h4>
<p>好了，在介绍完为什么要用etcd以后，我开始分享一个实际的问题了。我们有个用户在使用 Easegress 的时候，在Easegress内配置了上千条pipeline，导致 Easegress的内存飙升的非常厉害- 10+GB 以上，而且长时间还下不来。</p>
<p>用户报告的问题是——</p>
<blockquote><p>在Easegress 1.4.1 上创建一个HTTP对象，1000个Pipeline，在Easegres初始化启动完成时的内存占用大概为400M，运行80分钟后2GB，运行200分钟后达到了4GB，这期间什么也没有干，对Easegress没有进行过一次请求。</p></blockquote>
<p>一般来说，就算是API再多也不应该配置这么多的处理管道pipeline的，通常我们会使用HTTP API的前缀把一组属于一个类别的API配置在一个管道内是比较合理的，就像nginx下的location的配置，一般来说不会太多的。但是，在用户的这个场景下配置了上千个pipeline，我们也是头一次见，应该是用户想做更细粒度的控制。</p>
<p>经过调查后，我们发现内存使用基本全部来自etcd，我们实在没有想到，因为我们往etcd里放的数据也没有多少个key，感觉不会超过10M，但不知道为什么会占用了10GB的内存。这种时候，一般会怀疑etcd有内存泄漏，上etcd上的github上搜了一下，发现etcd在3.2和3.3的版本上都有内存泄露的问题，但都修改了，而 Easegress 使用的是3.5的最新版本，另外，一般来说内存泄漏的问题不会是这么大的，我们开始怀疑是我们哪里误用了etcd。要知道是否误用了etcd，那么只有一条路了，沉下心来，把etcd的设计好好地看一遍。</p>
<div class="p-rich_text_section">
<p>大概花了两天左右的时间看了一下etcd的设计，我发现了etcd有下面这些消耗内存的设计，老实说，还是非常昂贵的，这里分享出来，避免后面的同学再次掉坑。</p>
<p><b data-stringify-type="bold">首当其冲是——RaftLog</b>。etcd用Raft Log，主要是用于帮助follower同步数据，这个log的底层实现不是文件，而是内存。所以，而且还至少要保留 <code>5000</code> 条最新的请求。如果key的size很大，这 <code>5000</code>条就会产生大量的内存开销。比如，不断更新一个 1M的key，哪怕是同一个key，这 5000 条Log就是 5000MB = 5GB 的内存开销。这个问题在etcd的issue列表中也有人提到过  <a href="https://github.com/etcd-io/etcd/issues/12548" target="_blank" rel="noopener">issue #12548 </a>，不过，这个问题不了了之了。这个5000还是一个hardcode，无法改。（参看 <code>DefaultSnapshotCatchUpEntries</code> <a class="c-link" tabindex="-1" href="https://github.com/etcd-io/etcd/blob/29c3b0f307107fd95a6eb43ddff20db952eeb2fa/server/etcdserver/server.go#L80" target="_blank" rel="noopener noreferrer" data-stringify-link="https://github.com/etcd-io/etcd/blob/29c3b0f307107fd95a6eb43ddff20db952eeb2fa/server/etcdserver/server.go#L80" data-sk="tooltip_parent" data-remove-tab-index="true">相关源码</a>）</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">// DefaultSnapshotCatchUpEntries is the number of entries for a slow follower
// to catch-up after compacting the raft storage entries.
// We expect the follower has a millisecond level latency with the leader.
// The max throughput is around 10K. Keep a 5K entries is enough for helping
// follower to catch up.
DefaultSnapshotCatchUpEntries uint64 = 5000</pre>
<p>另外，我们还发现，这个设计在历史上etcd的官方团队把这个默认值从10000降到了5000，我们估计etcd官方团队也意识到10000有点太耗内存了，所以，降了一半，但是又怕follwer同步不上，所以，保留了 5000条……（在这里，我个人感觉还有更好的方法，至少不用全放在内存里吧……）</p>
<p>另外还有下面几项也会导致etcd的内存会增加</p>
<ol class="p-rich_text_list p-rich_text_list__ordered" data-stringify-type="ordered-list" data-indent="0" data-border="0">
<li data-stringify-indent="0" data-stringify-border="0"><strong>索引</strong>。etcd的每一对 key-value 都会在内存中有一个 B-tree 索引。这个索引的开销跟key的长度有关，etcd还会保存版本。所以B-tree的内存跟key的长度以及历史版本号数量也有关系。</li>
<li data-stringify-indent="0" data-stringify-border="0"><b data-stringify-type="bold">mmap</b>。还有，etcd 使用 mmap 这样上古的unix技术做文件映射，会把他的blotdb的内存map到虚拟内存中，所以，db-size越大，内存越大。</li>
<li data-stringify-indent="0" data-stringify-border="0"><b data-stringify-type="bold">Watcher</b>。watch也会占用很大的内存，如果watch很多，连接数多，都会堆积内存。</li>
</ol>
<p>（很明显，etcd这么做就是为了一个高性能的考虑）</p>
<div class="p-rich_text_section">
<p>Easegress中的问题更多的应该是Raft Log 的问题。后面三种问题我们觉得不会是用户这个问题的原因，对于索引和mmap，使用 <a href="https://etcd.io/docs/v3.2/op-guide/maintenance/" target="_blank" rel="noopener">etcd 的 compact 和 defreg</a> （压缩和碎片整理应该可以降低内存，但用户那边不应该是这个问题的核心原因）。</p>
<p>针对用户的问题，大约有1000多条pipeline，因为Easegress会对每一条pipeline进行数据统计（如：M1, M5, M15， P99, P90, P50等这样的统计数据），统计信息可能会有1KB-2KB左右，但Easegress会把这1000条pipeline的统计数据合并起来写到一个key中，这1000多条的统计数据合并后会导致出现一个平均尺寸为2MB的key，而5000个in-memory的RaftLog导致etcd要消耗了10GB的内存。之前没有这么多的pipeline的场景，所以，这个内存问题没有暴露出来。</p>
<p>于是，我们最终的解决方案也很简单，我们修改我们的策略，不再写这么大的Value的数据了，虽然以前只写在一个key上，但是Key的值太大，现在把这个大Key值拆分成多个小的key来写，这样，实际保存的数据没有发生变化，但是RaftLog的每条数据量就小了，所以，以前是5000条 2M（10GB），现在是5000条 1K（500MB），就这样解决了这个问题。相关的PR在这里 <a href="https://github.com/megaease/easegress/pull/542" target="_blank" rel="noopener">PR#542</a> 。</p>
<h4>总结</h4>
<p>要用好 etcd，有如下的实践</p>
<ul>
<li>避免大尺寸的key和value，一方面会通过一个内存级的 Raft Log 占大量内存，另一方面，B-tree的多版本索引也会因为这样耗内存。</li>
<li>避免DB的尺寸太大，并通过 compact和defreg来压缩和碎片整理降低内存。</li>
<li>避免大量的Watch Client 和 Watch数。这个开销也是比较大的。</li>
<li>最后还有一个，就是尽可能使用新的版本，无论是go语言还是etcd，这样会少很多内存问题。比如：golang的这个跟LInux内核心相关的<a href="https://github.com/golang/go/issues/42330" target="_blank" rel="noopener">内存问题</a> —— golang 1.12的版sget的是 <code>MADV_FREE</code> 的内存回收机制，而在1.16的时候，改成了 <code>MADV_DONTNEED</code> ，这两者的差别是，<code>FREE</code>表示，虽然进程标记内存不要了，但是操作系统会保留之，直到需要更多的内存，而 <code>DONTNEED</code> 则是立马回收，你可以看到，在常驻内存RSS 上，前者虽然在golang的进程上回收了内存，但是RSS值不变，而后者会看到RSS直立马变化。Linux下对 <code>MADV_FREE</code> 的实现在某些情况下有一定的问题，所以，在go 1.16的时候，默认值改成了 <code>MADV_DONTNEED</code> 。而 etcd 3.4 是用 来1.12 编译的。</li>
</ul>
<p>最后，欢迎大家关注我们的开源软件！ <a href="https://github.com/megaease/" target="_blank" rel="noopener">https://github.com/megaease/ </a></p>
<div class="p-rich_text_section">
<p>（全文完）</p>
</div>
</div>
</div>
<p><!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" id="wp_rp_first"><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/21615.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2021/09/go-generics-150x150.png" alt="Go编程模式 ： 泛型编程" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21615.html" class="wp_rp_title">Go编程模式 ： 泛型编程</a></li><li ><a href="https://coolshell.cn/articles/21263.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.k8s-150x150.png" alt="Go 编程模式：k8s Visitor 模式" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21263.html" class="wp_rp_title">Go 编程模式：k8s Visitor 模式</a></li><li ><a href="https://coolshell.cn/articles/21228.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.line_.-150x150.png" alt="Go编程模式：Pipeline" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21228.html" class="wp_rp_title">Go编程模式：Pipeline</a></li><li ><a href="https://coolshell.cn/articles/21214.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.pair_-150x150.png" alt="Go编程模式：委托和反转控制" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21214.html" class="wp_rp_title">Go编程模式：委托和反转控制</a></li><li ><a href="https://coolshell.cn/articles/21179.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.generate-150x150.png" alt="Go 编程模式：Go Generation" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21179.html" class="wp_rp_title">Go 编程模式：Go Generation</a></li><li ><a href="https://coolshell.cn/articles/21164.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.map_.reduce-150x150.png" alt="Go编程模式：Map-Reduce" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21164.html" class="wp_rp_title">Go编程模式：Map-Reduce</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/22242.html">ETCD的内存问题</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/22242.html/feed</wfw:commentRss>
			<slash:comments>39</slash:comments>
		
		
			</item>
		<item>
		<title>“一把梭：REST API 全用 POST”</title>
		<link>https://coolshell.cn/articles/22173.html</link>
					<comments>https://coolshell.cn/articles/22173.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Sun, 13 Feb 2022 04:28:47 +0000</pubDate>
				<category><![CDATA[技术新闻]]></category>
		<category><![CDATA[程序设计]]></category>
		<category><![CDATA[HTTP]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[Restful]]></category>
		<category><![CDATA[程序员]]></category>
		<guid isPermaLink="false">https://coolshell.cn/?p=22173</guid>

					<description><![CDATA[<p>写这篇文章的原因主要还是因为V2EX上的这个贴子，这个贴子中说—— “对接同事的接口，他定义的所有接口都是 post 请求，理由是 https 用 post 更...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/22173.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/22173.html">“一把梭：REST API 全用 POST”</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><img decoding="async" loading="lazy" class="alignright wp-image-22176" src="https://coolshell.cn/wp-content/uploads/2022/02/http_method-300x169.png" alt="" width="325" height="183" srcset="https://coolshell.cn/wp-content/uploads/2022/02/http_method-300x169.png 300w, https://coolshell.cn/wp-content/uploads/2022/02/http_method-1024x576.png 1024w, https://coolshell.cn/wp-content/uploads/2022/02/http_method-768x432.png 768w, https://coolshell.cn/wp-content/uploads/2022/02/http_method-480x270.png 480w, https://coolshell.cn/wp-content/uploads/2022/02/http_method.png 1200w" sizes="(max-width: 325px) 100vw, 325px" /></p>
<p>写这篇文章的原因主要还是因为V2EX上的这个<a href="https://www.v2ex.com/t/830030?p=1" target="_blank" rel="noopener">贴子</a>，这个贴子中说——</p>
<blockquote><p>“对接同事的接口，他定义的所有接口都是 post 请求，理由是 https 用 post 更安全，之前习惯使用 restful api ，如果说 https 只有 post 请求是安全的话？那为啥还需要 get 、put 、delete ？我该如何反驳他。”</p></blockquote>
<p>然后该贴中大量的回复大概有这么几种论调，1）POST挺好的，就应该这么干，沟通少，2）一把梭，早点干完早点回家，3）吵赢了又怎么样？工作而已，优雅不能当饭吃。虽然评论没有一边倒，但是也有大量的人支持。然后，我在Twitter上嘲讽了一下，用POST干一切就像看到了来你家装修工人说，“老子干活就是用钉子钉一切，什么螺丝、螺栓、卡扣、插销……通通不用，钉枪一把梭，方便，快捷，安全，干完早回家……不过，还是有一些网友觉得用POST挺好的，而且可以节约时间。所以，正好，我在《<a title="我做系统架构的一些原则" href="https://coolshell.cn/articles/21672.html" target="_blank" rel="noopener">我做系统架构的原则</a>》中的“<a href="https://coolshell.cn/articles/21672.html#%E5%8E%9F%E5%88%99%E4%BA%94%EF%BC%9A%E5%88%B6%E5%AE%9A%E5%B9%B6%E9%81%B5%E5%BE%AA%E6%9C%8D%E4%BB%8E%E6%A0%87%E5%87%86%E3%80%81%E8%A7%84%E8%8C%83%E5%92%8C%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5" target="_blank" rel="noopener">原则五</a>”中反对API返回码无论对错全是200的返回那，我专门写下这一篇文章，以正视听。</p>
<p>这篇文章主要分成下面这几个部分：</p>
<ol>
<li>为什么要用不同的HTTP动词？</li>
<li>Restful 进行复杂查询</li>
<li>几个主要问题的回应
<ul>
<li>POST 更安全吗？</li>
<li>全用 POST 可以节省时间沟通少吗？</li>
<li>早点回家的正确姿势</li>
<li>工作而已，优雅不能当饭吃</li>
</ul>
</li>
</ol>
<p><span id="more-22173"></span></p>
<h4>为什么要用不同的HTTP动词</h4>
<p>编程世界通常来说有两种逻辑：“<strong>业务逻辑</strong>” 和 “<strong>控制逻辑</strong>”。</p>
<ul>
<li><strong>业务逻辑</strong>。就是你实现业务需求的功能的代码，就是跟用户需求强相关的代码。比如，把用户提交的数据保存起来，查询用户的数据，完成一个订单交易，为用户退款……等等，这些是业务逻辑</li>
<li><strong>控制逻辑</strong>。就是我们用于控制程序运行的非功能性的代码。比如，用于控制程序循环的变量和条件，使用多线程或分布式的技术，使用HTTP/TCP协议，使用什么样数据库，什么样的中间件……等等，这些跟用户需求完全没关系的东西。</li>
</ul>
<p>网络协议也是一样的，一般来说，<strong>几乎所有的主流网络协议都有两个部分，一个是协议头，一个是协议体。协议头中是协议自己要用的数据，协议体才是用户的数据。所以，协议头主要是用于协议的控制逻辑，而协议体则是业务逻辑。</strong></p>
<p>HTTP的动词（或是Method）是在协议头中，所以，其主要用于控制逻辑。</p>
<p dir="auto">下面是HTTP的动词规范，一般来说，REST API 需要开发人员严格遵循下面的标准规范（参看<a href="https://www.rfc-editor.org/rfc/rfc7231#section-4.2.2" target="_blank" rel="noopener">RFC7231 章节4.2.2 &#8211; Idempotent Methods</a>）</p>
<table>
<thead>
<tr>
<th>方法</th>
<th>描述</th>
<th>幂等</th>
</tr>
</thead>
<tbody>
<tr>
<td>GET</td>
<td>用于查询操作，对应于数据库的 <code>select</code> 操作</td>
<td style="text-align: center;"><img src="https://s.w.org/images/core/emoji/14.0.0/72x72/2714.png" alt="✔" class="wp-smiley" style="height: 1em; max-height: 1em;" />︎</td>
</tr>
<tr>
<td>PUT</td>
<td>用于所有的信息更新，对应于数据库的 <code>update </code>操作</td>
<td style="text-align: center;"><img src="https://s.w.org/images/core/emoji/14.0.0/72x72/2714.png" alt="✔" class="wp-smiley" style="height: 1em; max-height: 1em;" />︎︎</td>
</tr>
<tr>
<td>DELETE</td>
<td>用于更新操作，对应于数据库的 <code>delete</code> 操作</td>
<td style="text-align: center;"><img src="https://s.w.org/images/core/emoji/14.0.0/72x72/2714.png" alt="✔" class="wp-smiley" style="height: 1em; max-height: 1em;" />︎︎</td>
</tr>
<tr>
<td>POST</td>
<td>用于新增操作，对应于数据库的 <code>insert</code> 操作</td>
<td style="text-align: center;">✘</td>
</tr>
<tr>
<td>HEAD</td>
<td>用于返回一个资源对象的“元数据”，或是用于探测API是否健康</td>
<td style="text-align: center;"><img src="https://s.w.org/images/core/emoji/14.0.0/72x72/2714.png" alt="✔" class="wp-smiley" style="height: 1em; max-height: 1em;" />︎</td>
</tr>
<tr>
<td>PATCH</td>
<td>用于局部信息的更新，对应于数据库的 <code>update</code> 操作</td>
<td style="text-align: center;">✘</td>
</tr>
<tr>
<td>OPTIONS</td>
<td>获取API的相关的信息。</td>
<td style="text-align: center;"><img src="https://s.w.org/images/core/emoji/14.0.0/72x72/2714.png" alt="✔" class="wp-smiley" style="height: 1em; max-height: 1em;" />︎</td>
</tr>
</tbody>
</table>
<p>其中，<code>PUT</code> 和 <code>PACTH</code> 都是更新业务资源信息，如果资源对象不存在则可以新建一个，但他们两者的区别是，<code>PUT</code> 用于更新一个业务对象的所有完整信息，就像是我们通过表单提交所有的数据，而 <code>PACTH</code> 则对更为API化的数据更新操作，只需要更需要更新的字段（参看 <a href="http://tools.ietf.org/html/rfc5789" rel="nofollow">RFC 5789</a> ）。</p>
<p>当然，现实世界中，可能并不一定严格地按照数据库操作的CRUD来理解API，比如，你有一个登录的API <code>/login</code> 你觉得这个API应该是 <code>GET</code> ，<code>POST</code>，<code>PUT</code> 还是 <code>PATCH</code> ?登录的时候用户需要输入用户名和密码，然后跟数据库里的对比（select操作）后反回一个登录的session token，然后这个token作为用户登录的状态令牌。如果按上面表格来说，应该是 select 操作进行 <code>GET</code> ，但是从语义上来说，登录并不是查询信息，应该是用户状态的更新或是新增操作（新增session），所以还是应该使用 <code>POST</code>，而 <code>/logout</code> 你可以使用 <code>DELETE</code> 。<strong>这里相说明一下，不要机械地通过数据库的CRUD来对应这些动词，很多时候，还是要分析一下业务语义。</strong></p>
<p><strong>另外，我们注意到，在这个表格的最后一列中加入了“是否幂等”的，API的幂等对于控制逻辑来说是一件很重要的事。</strong>所谓幂等，就是该API执行多次和执行一次的结果是完全一样的，没有副作用。</p>
<ul>
<li><code>POST</code> 用于新增加数据，比如，新增一个交易订单，这肯定不能是幂等的</li>
<li><code>DELETE</code> 用于删除数据，一个数据删除多次和删除一次的结果是一样的，所以，是幂等的</li>
<li><code>PUT</code> 用于全部数更新，所以，是幂等的。</li>
<li><code>PATCH</code>用于局部更新，比如，更新某个字段 cnt = cnt+1，明显不可能是幂等操作。</li>
</ul>
<p>幂等这个特性对于远程调用是一件非常关键的事，就是说，远程调用有很多时候会因为网络原因导致调用timeout，对于timeout的请求，我们是无法知道服务端是否已经是收到请求并执行了，此时，我们不能贸然重试请求，对于不是幂等的调用来说，这会是灾难性的。比如像转帐这样的业务逻辑，转一次和转多次结果是不一样的，如果重新的话有可能就会多转了一次。所以，这个时候，如果你的API遵从了HTTP动词的规范，那么你写起程序来就可以明白在哪些动词下可以重试，而在哪些动词下不能重试。如果你把所有的API都用POST来表达的话，就完全失控了。</p>
<p>除了幂等这样的控制逻辑之外，你可能还会有如下的这些控制逻辑的需求：</p>
<ul>
<li><strong>缓存</strong>。通过CDN或是网关对API进行缓存，很显然，我们要在查询<code>GET</code> 操作上建议缓存。</li>
<li><strong>流控</strong>。你可以通过HTTP的动词进行更粒度的流控，比如：限制API的请用频率，在读操作上和写操作上应该是不一样的。</li>
<li><strong>路由</strong>。比如：写请求路由到写服务上，读请求路由到读服务上。</li>
<li><strong>权限</strong>。可以获得更细粒度的权限控制和审计。</li>
<li><strong>监控</strong>。因为不同的方法的API的性能都不一样，所以，可以区分做性能分析。</li>
<li><strong>压测</strong>。当你需要压力测试API时，如果没有动词的区分的话，我相信你的压力测试很难搞吧。</li>
<li>……等等</li>
</ul>
<p>也许，你会说，我的业务太简单了，没有必要搞这么复杂。OK，没有问题，但<strong>是我觉得你最差的情况下，也是需要做到“读写分离”的，就是说，至少要有两个动词，<code>GET</code> 表示是读操作，<code>POST</code>表示是写操作。</strong></p>
<h4>Restful 复杂查询</h4>
<p>一般来说，对于查询类的API，主要就是要完成四种操作：排序，过滤，搜索，分页。下面是一些相关的规范。参考于两个我觉得写的最好的Restful API的规范文档，<a href="https://github.com/microsoft/api-guidelines/blob/vNext/Guidelines.md" target="_blank" rel="noopener">Microsoft REST API Guidelines</a>，<a href="https://github.com/paypal/api-standards/blob/master/api-style-guide.md" target="_blank" rel="noopener">Paypal API Design Guidelines</a>。</p>
<ul dir="auto">
<li>
<p dir="auto"><strong>排序</strong>。对于结果集的排序，使用 <code>sort</code> 关键字，以及 <code>{field_name}|{asc|desc},{field_name}|{asc|desc}</code> 的相关语法。比如，某API需要返回公司的列表，并按照某些字段排序，如：<code>GET /admin/companies?sort=rank|asc</code> 或是 <code>GET /admin/companies?sort=rank|asc,zip_code|desc</code></p>
</li>
<li>
<p dir="auto"><strong>过滤</strong>。对于结果集的过滤，使用 <code>filter</code> 关键字，以及 <code>{field_name} op{value}</code> 的语法。比如： <code>GET /companies?category=banking&amp;location=china</code> 。但是，有些时候，我们需要更为灵活的表达式，我们就需要在URL上构造我们的表达式。这里需要定义六个比较操作：<code>=</code>，<code>&lt;</code>，<code>&gt;</code>，<code>&lt;=</code>，<code>&gt;=</code>，以及三个逻辑操作：<code>and</code>，<code>or</code>，<code>not</code>。（表达式中的一些特殊字符需要做一定的转义，比如：<code>&gt;=</code> 转成 <code>ge</code>）于是，我们就会有如下的查询表达式：<code>GET /products?$filter=name eq 'Milk' and price lt 2.55</code> 查找所有的价柗小于2.55的牛奶。</p>
</li>
<li>
<p dir="auto"><strong>搜索</strong>。对于相关的搜索，使用 <code>search</code> 关键字，以及关键词。如：<code>GET /books/search?description=algorithm</code> 或是直接就是全文搜索 <code>GET /books/search?key=algorithm</code> 。</p>
</li>
<li>
<p dir="auto"><strong>分页</strong>。对于结果集进行分页处理，分页必需是一个默认行为，这样不会产生大量的返回数据。</p>
<ul dir="auto">
<li>使用<code>page</code>和<code>per_page</code>代表页码和每页数据量，比如：<code>GET /books?page=3&amp;per_page=20</code>。</li>
<li><strong>可选</strong>。上面提到的<code>page</code>方式为使用相对位置来获取数据，可能会存在两个问题：性能（大数据量）与数据偏差（高频更新）。此时可以使用绝对位置来获取数据：事先记录下当前已获取数据里最后一条数据的<code>ID</code>、<code>时间</code>等信息，以此获取 “<strong>该ID之前的数据</strong>” 或 “<strong>该时刻之前的数据</strong>”。示例：<code>GET /news?max_id=23454345&amp;per_page=20</code> 或 <code>GET /news?published_before=2011-01-01T00:00:00Z&amp;per_page=20</code>。
<p dir="auto">
</li>
</ul>
</li>
</ul>
<p dir="auto"><strong>注意：这里需要注意一下，在理论上来说<code>GET</code>是可以带 body 的，但是很多程序的类库或是中间件并不支持 GET 带 body，导致你只能用 POST 来传递参数。这里的原则是：</strong></p>
<ol dir="auto">
<li>
<p dir="auto"><strong>对于简单的查询，很多参数都设计在 restful API 的路径上了，而 filter/sort/pagination 也不会带来很多的复杂，所以应该使用 <code>GET</code> </strong></p>
</li>
<li><strong>对于复杂的查询来说，可能会有很复杂的查询参数，比如：ElasticSearch 上的 <code>index/_search</code>里的 DSL，你也应该尽可能的使用 <code>GET</code>，而不是<code>POST</code> 除非客观条件上不支持<code>GET</code>。ElasticSearch 的<a href="https://www.elastic.co/guide/en/elasticsearch/guide/current/_empty_search.html" target="_blank" rel="noopener">官方文档</a>里也是这么说的。</strong></li>
</ol>
<blockquote><p>The authors of Elasticsearch prefer using GET for a search request because they feel that it describes the action—​retrieving information—​better than the POST verb. （我们推荐使用 GET而不是 POST，因为语义更清楚）However, because GET with a request body is not universally supported, the search API also accepts POST requests （除非你的类库或是服务器不支持 GET带参数 ，你再用POST，我们两个都支持）</p>
<p><strong>陈皓注：但是在 ElasticSearch 7.11 后，GET 也不支持 body 了。这是 ElasticSearch 的设计和实现不对应了。</strong></p></blockquote>
<div id="message-accessories-1073072655571370085" class="container-2sjPya">
<div class="messageAttachment-CZp8Iv messageAttachmentNoJustify-lIzP9c">
<div class="imageContent-3Av-9c embedWrapper-1MtIDg attachmentContentItem-UKeiCx">
<div class="imageContainer-10XenG">
<div class="imageWrapper-oMkQl4 imageZoom-3yLCXY clickable-LksVCf">另外，对于一些更为复杂的操作，建议通过分别调用多个API的方式来完成，虽然这样会增加网络请求的次数，但是这样的可以让后端程序和数据耦合度更小，更容易成为微服务的架构。</div>
</div>
</div>
</div>
</div>
<p>最后，如果你想在Rest中使用像GraphQL那样的查询语言，你可以考虑一下类似 <a href="https://www.odata.org/" target="_blank" rel="noopener">OData</a> 的解决方案。OData 是 Open Data Protocol 的缩写，最初由 Microsoft 于 2007 年开发。它是一种开放协议，使您能够以简单和标准的方式创建和使用可查询和可互操作的 RESTful API。</p>
<h4>几个主要问题的回应</h4>
<p>下面是对几个问题的直接回应，如果大家需要我回应更多的问题，可以在后面留言，我会把问题和我的回应添加到下面。</p>
<h5>1）为什么API 要Restful，并符合规范？</h5>
<p><strong>Restful API算是一个HTTP的规范和标准了，你要说是最佳实践也好，总之，它是一个全世界对HTTP API的一个共识。在这个共识上，你可以无成本地享受很多的技术红利，比如：CDN，API网关，服务治理，监控……等等。这些都是可以让你大幅度降低研发成本，避免踩坑的原因。</strong></p>
<h5>2）为什么“过早优化”不适用于API设计？</h5>
<p>因为API是一种契约，一旦被使用上，就很难再变更了，就算你发行新的版本的API，你还要驱动各种调用方升级他们的调用方式。所以，接口设计就像数据库模式设计一下，一旦设计好了，未来再变更就比较难了。所以，还是要好好设计。正如前面我给的几个文档——<a href="https://github.com/microsoft/api-guidelines/blob/vNext/Guidelines.md" target="_blank" rel="noopener">Microsoft REST API Guidelines</a>，<a href="https://github.com/paypal/api-standards/blob/master/api-style-guide.md" target="_blank" rel="noopener">Paypal API Design Guidelines</a> 或是 <a href="https://cloud.google.com/apis/design" target="_blank" rel="noopener">Google API Design Guide</a> 都是让你好好设计API的不错的 Guidelines.</p>
<h5>3）POST 更安全吗？</h5>
<p>不会。</p>
<p>很多同学以为 <code>GET</code> 的请求数据在URL中，而 <code>POST</code> 的则不是，所以以为 <code>POST</code> 更安全。不是这样的，整个请求的HTTP URL PATH会全部封装在HTTP的协议头中。只要是HTTPS，就是安全的。当然，有些网关如nginx会把URL打到日志中，或是会放在浏览器的历史记录中，所以有人会说 <code>GET</code> 请求不安全，但是，<code>POST</code> 也没有好到哪里去，在 <a href="https://en.wikipedia.org/wiki/Cross-site_request_forgery" target="_blank" rel="noopener">CSRF</a> 这个最常见的安全问题上，则完全就是针对 <code>POST</code> 的。  安全是一件很复杂的事，无论你用哪方法或动词都会不能代表你会更安全。</p>
<p>另外，</p>
<ul>
<li>如果你要 防止你的 <code>GET</code> 上有敏感信息，应该加个密，这个跟 <code>POST</code>是一样的。</li>
<li>如果你要防止 <code>GET</code> 会被中间人修改，你应该做一个URL签名。（通常来说， 我们都在 <code>GET</code> 上做签名，<code>POST</code> 就忘做了）</li>
<li>如果你要防止有人发一些恶意链接来 hack 你的用户（传说中的 <code>GET</code> 不如 <code>POST</code> 安全的一个问题），你应该用 HMAC 之类的认证技术做好认证（参看 <a title="HTTP API 认证授权术" href="https://coolshell.cn/articles/19395.html" target="_blank" rel="noopener">HTTP API 认证授权术</a>）。</li>
</ul>
<p>总之，你要明白，<code>GET</code> 和 <code>POST</code> 的安全问题都一样的，不要有谁比谁更安全，然后你就可以掉以轻心的这样的想法，安全都是要很严肃对待的。</p>
<h5>4）全用 POST 可以节省时间减少沟通吗？</h5>
<p>不但不会，反而更糟糕。</p>
<p>说这种话的人，我感觉是不会思考问题。</p>
<ul>
<li>其一，为API赋于不同的动词，这个几乎不需要时间。把CRUD写在不同的函数下也是一种很好的编程风格。另外现在几乎所有的开发框架都支持很快速的CRUD的开发，比如Spring Boot，写数据库的CRUD基本上就不需要写SQL语言相关的查询代码，非常之方便。</li>
<li>其二，使用规范的方式，可以节约新加入团队人员的学习成本，而且可以大大减少跨团队的沟能成本。规范和标准其实就是在节约团队时间提升整体效率的，这个我们整个人类进行协作的基础。所以，这个世界上有很多的标准，你只要照着这个标准来，你的所生产的零件就可以适配到其它厂商的产品上。而不需要相互沟通。</li>
<li>其三，全用POST接口一把梭，不规范不标准，使用你的这个山寨API的人就得来不断的问你，反而增加了沟通。另外，也许你开发业务功能很快了，但是你在做控制逻辑的时候，你就要返工了，从长期上来讲，你的欠下了技术债，这个债反而导致了更大的成本。</li>
</ul>
<h5>5）早点回家的正确姿势</h5>
<p>不要以为你回家早就没事了，如果你的代码有这样那样的问题，别人看懂，或是出误用了你的代码出了问题，那么，你早回家有什么意义呢？你一样要被打扰，甚至被叫到公司来处理问题。所以，你应该做的是为了“长期的早回家”，而不是“短期的早回家”，要像长期的早回家，通常来说是这样的：</p>
<ul>
<li><strong>把代码组织设计好，有更好的扩展性</strong>。这样在面对新需求的时候，你就可以做到少改代码，甚至不改代码。这样你才可能早回家。不然，每次需求一来，你得重新写，你怎么可能早回家？</li>
<li><strong>你的代码质量是不错的，有不错的文档和注释</strong>。所以，别人不会老有问题来找你，或是你下班后，叫你来处理问题。甚至任何人都可以很容易地接手你的代码，这样你才可能真正不被打扰</li>
</ul>
<h5>6）工作而已，优雅不能当饭吃</h5>
<p>回应两点：</p>
<p>其一，遵循个规范而已，把“正常”叫“优雅”，可见标准有多低。这么低的标准也只能“为了吃饭而生存了”。</p>
<p>其二，<strong>作为一个“职业程序员”，要学会热爱和尊重自己的职业，热爱自己职业最重要的就是不要让外行人看扁这个职业，自己都不尊重这个职业，你让别人怎么尊重？尊重自己的职业，不仅仅只是能够获得让人羡慕的报酬，而更是要让自己的这个职业的更有含金量</strong>。</p>
<p><strong>希望大家都能尊重自己从事的这个职业，成为真正的职业化的程序员，而不是一个码农！</strong></p>
<figure id="attachment_22177" aria-describedby="caption-attachment-22177" style="width: 834px" class="wp-caption aligncenter"><img decoding="async" loading="lazy" class="wp-image-22177 size-full" src="https://coolshell.cn/wp-content/uploads/2022/02/quote-your-job-gives-you-authority-your-behavior-gives-you-respect-irwin-federman-73-55-75.jpeg" alt="" width="834" height="319" srcset="https://coolshell.cn/wp-content/uploads/2022/02/quote-your-job-gives-you-authority-your-behavior-gives-you-respect-irwin-federman-73-55-75.jpeg 834w, https://coolshell.cn/wp-content/uploads/2022/02/quote-your-job-gives-you-authority-your-behavior-gives-you-respect-irwin-federman-73-55-75-300x115.jpeg 300w, https://coolshell.cn/wp-content/uploads/2022/02/quote-your-job-gives-you-authority-your-behavior-gives-you-respect-irwin-federman-73-55-75-768x294.jpeg 768w, https://coolshell.cn/wp-content/uploads/2022/02/quote-your-job-gives-you-authority-your-behavior-gives-you-respect-irwin-federman-73-55-75-604x231.jpeg 604w" sizes="(max-width: 834px) 100vw, 834px" /><figcaption id="caption-attachment-22177" class="wp-caption-text">你的工作给你权力，而只有你的行为才会给你尊重</figcaption></figure>
<p>（全文完）<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/22298.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2022/10/communication-150x150.png" alt="聊聊团队协同和协同工具" width="150" height="150" /></a><a href="https://coolshell.cn/articles/22298.html" class="wp_rp_title">聊聊团队协同和协同工具</a></li><li ><a href="https://coolshell.cn/articles/22157.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2022/02/monitoring-150x150.jpeg" alt="谈谈公司对员工的监控" width="150" height="150" /></a><a href="https://coolshell.cn/articles/22157.html" class="wp_rp_title">谈谈公司对员工的监控</a></li><li ><a href="https://coolshell.cn/articles/21589.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2021/07/knowledge_sharing-300x169-1-150x150.jpeg" alt="如何做一个有质量的技术分享" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21589.html" class="wp_rp_title">如何做一个有质量的技术分享</a></li><li ><a href="https://coolshell.cn/articles/20977.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/08/programmer.01-e1596792460687-150x150.png" alt="程序员如何把控自己的职业" width="150" height="150" /></a><a href="https://coolshell.cn/articles/20977.html" class="wp_rp_title">程序员如何把控自己的职业</a></li><li ><a href="https://coolshell.cn/articles/20765.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/01/remote-150x150.jpg" alt="MegaEase的远程工作文化" width="150" height="150" /></a><a href="https://coolshell.cn/articles/20765.html" class="wp_rp_title">MegaEase的远程工作文化</a></li><li ><a href="https://coolshell.cn/articles/20276.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2019/12/open-your-creative-mind-150x150.jpg" alt="别让自己“墙”了自己" width="150" height="150" /></a><a href="https://coolshell.cn/articles/20276.html" class="wp_rp_title">别让自己“墙”了自己</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/22173.html">“一把梭：REST API 全用 POST”</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/22173.html/feed</wfw:commentRss>
			<slash:comments>119</slash:comments>
		
		
			</item>
		<item>
		<title>网络数字身份认证术</title>
		<link>https://coolshell.cn/articles/21708.html</link>
					<comments>https://coolshell.cn/articles/21708.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Sun, 02 Jan 2022 08:38:13 +0000</pubDate>
				<category><![CDATA[程序设计]]></category>
		<category><![CDATA[网络安全]]></category>
		<category><![CDATA[mTLS]]></category>
		<category><![CDATA[Security]]></category>
		<category><![CDATA[TLS]]></category>
		<category><![CDATA[安全]]></category>
		<guid isPermaLink="false">https://coolshell.cn/?p=21708</guid>

					<description><![CDATA[<p>这篇文章是《HTTP API 认证授权术》的姊妹篇，在那篇文章中，主要介绍了 HTTP API 认证和授权技术中用到的 HTTP Basic, Digest A...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/21708.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/21708.html">网络数字身份认证术</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><img decoding="async" loading="lazy" class="alignright size-medium wp-image-21716" src="https://coolshell.cn/wp-content/uploads/2022/01/iStock-1175502114-300x201.png" alt="" width="300" height="201" srcset="https://coolshell.cn/wp-content/uploads/2022/01/iStock-1175502114-300x201.png 300w, https://coolshell.cn/wp-content/uploads/2022/01/iStock-1175502114-768x514.png 768w, https://coolshell.cn/wp-content/uploads/2022/01/iStock-1175502114-404x270.png 404w, https://coolshell.cn/wp-content/uploads/2022/01/iStock-1175502114.png 800w" sizes="(max-width: 300px) 100vw, 300px" />这篇文章是《<a title="HTTP API 认证授权术" href="https://coolshell.cn/articles/19395.html" target="_blank" rel="noopener">HTTP API 认证授权术</a>》的姊妹篇，在那篇文章中，主要介绍了 HTTP API 认证和授权技术中用到的 HTTP Basic, Digest Access, HMAC, OAuth, JWT 等各种方式，主要是 API 上用到的一些技术，这篇文章主要想说的是另一个话题——身份认证。也就是说，怎么确认这个数据就是这个人发出来的？</p>
<h4>用户密码</h4>
<p>要解决这个问题，我们先来看一个最简单的解——使用密码，通常来说，在网络上要证明一个人的身份的话，都需要这个人的一些私密而唯一的东西。比如，像密码这样的东西，很多地方，只要你提供了你的用户名+密码，就可以确定这个人是你（注明：关于密码管理，强密码设定，密码泄漏，密码破解以及密码哄骗不在这篇文章的话题中），也就是说，这个密码是非常私密的事，我们可以假设，这个事全世界只能有当事人一个人知道，所以，当事人得供正确的密码，我们就可以认证这个人了。</p>
<p>为了加强密码的安全程度，一般会使用 2FA（Two-factor authentication）或 MFA（Multi-factor authentication），双因认证或多因认证，这需要用户提供一个唯一的可信设备，比如用户的手机，然后通过验证手机短信，或是像 <a href="https://en.wikipedia.org/wiki/Google_Authenticator" target="_blank" rel="noopener">Google Authenticator</a>  这样的动态口令来完成。这样的安全级别已经算是比较高了。如果能够再加上经常性的变更密码，那么安全级别就更好了。</p>
<p><span id="more-21708"></span></p>
<p>另外，一些公司还使用了生物密码来进行用户的身份验证，比如人脸识别。但是，我个人觉得人脸识别或是生物识别是比较糟糕的方式，因为：</p>
<ul>
<li>目前能被验证的生物信息（如人脸和指纹）太容易被别人获得和伪造了。</li>
<li>这样东西不能被变更和吊销，密码可以被吊销和重置，人脸则不能。</li>
</ul>
<h4>密钥对和证书</h4>
<p>密码可以解决身证认证的问题有很多问题，最重要的一个问题就是，你要把你的密码提供给对方，对方才能验证你的身份。你不可能把你的密码提供给全世界的人吧，这样的话，全世界的人都有你的密码了，那么任何人都能变成你了。所以，用户密码这个事只能存在于权威机构和普通用户之间，不能存在于普遍应用中。所以，这里需要使用更好的解决方案。</p>
<p>使用 ECC（<a title="Elliptic-Curve Cryptography" href="https://en.wikipedia.org/wiki/Elliptic-curve_cryptography" target="_blank" rel="noopener">Elliptic-Curve Cryptography</a>）椭圆曲线密码术，可以通过一个“密钥对”进行非对称加密。这种技术，在对信息进行加密和解密时，使用两个不同的密钥，其中一个用来做加密，另一个做解密。这样一来，我们就可以把其中一个密钥公布出去，称之为公钥，另一个密钥私密地保管好，称之为私钥。</p>
<p>比如，我用我的私钥加密信息，然后，我把这个私钥所配对的公钥发布给所有人，大家都用公钥解密信息，不用我的公钥你解密不了这个信息。这样一来，就可以保证这个信息是我发出来的，不但保证了信息安全，还完成了身份认证。</p>
<p><img decoding="async" loading="lazy" class="aligncenter size-large wp-image-21710" src="https://coolshell.cn/wp-content/uploads/2022/01/key.pair_-1024x390.png" alt="" width="640" height="244" srcset="https://coolshell.cn/wp-content/uploads/2022/01/key.pair_-1024x390.png 1024w, https://coolshell.cn/wp-content/uploads/2022/01/key.pair_-300x114.png 300w, https://coolshell.cn/wp-content/uploads/2022/01/key.pair_-768x293.png 768w, https://coolshell.cn/wp-content/uploads/2022/01/key.pair_-604x230.png 604w, https://coolshell.cn/wp-content/uploads/2022/01/key.pair_.png 1304w" sizes="(max-width: 640px) 100vw, 640px" /></p>
<p>这样的现实案例一般用于网站，也就是用户得要知道我访问的这个网站是真实的，不是别人做的。因为 DNS 很容易被 hack，你连上一个不可信的网络，这个网络里的 DNS 把这个网站的 IP 地址解析成什么 就是什么了。但是有了这个加密的机制后，网站把自己的信息加密后连同公钥给到访问者，访问解密后就知道是不是这个网站了。</p>
<p>但是，这里还是会有一个很严重的问题，那就是中间人攻击。如下图所示：</p>
<p><img decoding="async" loading="lazy" class="aligncenter wp-image-21712 size-full" src="https://coolshell.cn/wp-content/uploads/2022/01/middle.man_-e1641105543137.png" alt="" width="600" height="252" /></p>
<p>中间人 Chad 把自己伪装成 Bob 向 Alice 要信息，然后，再伪装成 Alice 对 Bob 说，这就是 Alice 的公钥，于是 Bob 也无法验证是不是 Alice 的公钥，因为公钥里就是一堆乱七八糟的数据，我们完全不能分辨哪个公钥属于 Alice 的。试想，如果我们收到声称属于银行的密钥。我们怎么知道它确实属于你的银行？</p>
<p>这里的答案就是<strong>使用数字证书</strong>。证书跟我们的身份证非常类似，其需要一个可信机构来颁发和验证的。这个证书机构 CA（Certificate Authority）是一个是大家都相信的权威机构，他用他的人品保证（当然一般会被严格管理和审计），CA 机构同样使用这样的非对称加密的技术来完成颁发和验证的事。下图展示了这一过程。</p>
<p><img decoding="async" loading="lazy" class="aligncenter size-large wp-image-21713" src="https://coolshell.cn/wp-content/uploads/2022/01/certificate-1024x532.png" alt="" width="640" height="333" srcset="https://coolshell.cn/wp-content/uploads/2022/01/certificate-1024x532.png 1024w, https://coolshell.cn/wp-content/uploads/2022/01/certificate-300x156.png 300w, https://coolshell.cn/wp-content/uploads/2022/01/certificate-768x399.png 768w, https://coolshell.cn/wp-content/uploads/2022/01/certificate-519x270.png 519w, https://coolshell.cn/wp-content/uploads/2022/01/certificate.png 1362w" sizes="(max-width: 640px) 100vw, 640px" /></p>
<p>说明一下上面这个图：</p>
<ol>
<li>为了解决公钥认证的问题的，我们需要一个权威的CA 机构。</li>
<li>Alice 把自己的信息（姓名、组织，地址，电邮，网址等）和自己的公钥打包成一个 CSR 的文件，发给 CA 机构，</li>
<li>CA 机构会来找 Alice 做物理世界的认证，如果通过后，就会用自己的机构私钥，把CSR 变成一个签名证书。</li>
<li>Bob 同学拿到 Alice 的证书，用 CA 机构的公钥解密后，得到 Alice 的公钥</li>
<li>后面就可以签证 信息是否来自 Alice 了。</li>
</ol>
<p>是的，这个过程就是在“套娃”，这种证书机构还可以给下级的证书机构发证，于是就会一层套一层地，形成一个证书链，顶层的叫根证书，你得绝对信任之。对于验证证书真实性的客户端，它需要能够验证链中所有 CA 的签名，这意味着客户端需要访问链中所有 CA 的证书。</p>
<h4>证书生成过程演示</h4>
<p>并不是所有的场景都需要向这些大型的 CA 机构申请公钥证书，在任何一个企业，组织或是团体内都可以自己形这样的“小王国”，也就是说，你可以自行生成这样的证书，只需要你自己保证自己的生成证书的私钥的安全，以及不需要扩散到整个互联网。下面，我们用 <code>openssl</code>命令来演示这个过程。</p>
<p>1）生成 CA 的证书（公钥） <code>ca.crt</code> 和私钥 <code>ca.key</code></p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">openssl req -newkey rsa:2048 \
    -new -nodes -x509 \
    -days 365 \
    -out ca.crt \
    -keyout ca.key \
    -subj "/C=SO/ST=Earth/L=Mountain/O=CoolShell/OU=HQ/CN=localhost"</pre>
<p>2)  生成 alice 的私钥</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">openssl genrsa -out alice.key 2048</pre>
<p>3）生成 Alice 的 CSR &#8211; Certificate Signing Request</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">openssl req -new -key alice.key 365 -out alice.csr \
    -subj "/C=CN/ST=Beijing/L=Haidian/O=CoolShell/OU=Test/CN=localhost.alice"</pre>
<p>4）使用 CA 给 Alice 签名证书</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">openssl x509  -req -in alice.csr \
    -extfile &lt;(printf "subjectAltName=DNS:localhost.alice") \ 
    -CA ca.crt -CAkey ca.key  \
    -days 365 -sha256 -CAcreateserial \
    -out alice.crt</pre>
<h4>双向认证 mTLS</h4>
<p>上面，我们说的基本上都是单向认证，大量的场景都是确保用户方访问的是真正的服务方，如：银行，电商网站，等。这样可以保证用户不会被钓鱼网站或是中间人攻击。但是，很多时候，我们也是需要双向认证的。下面是一个典型的场景——微信支付和商户间交互</p>
<ul>
<li>用户到商家那边买东西，商家要求用户进行支付。</li>
<li>用户选择了微信支付，于是，界面从商户侧切到了微信侧</li>
<li>微信那边支付完成后，商户这边收到微信那边支付完成的通知，于是开始发货。</li>
</ul>
<p>这个过程中有件事非常重要——就是微信通知商户支付完成的时候。</p>
<ul>
<li>微信得确保通知到的就是用户所支付商户，而不是别个。</li>
<li>商户也得要能确认，来通知我的就是微信，不是别人。</li>
</ul>
<p>一般来说，微信会给商户一个 AppID和一个 AppSerct，用这个来确保是我认证过的商户来调用我，然后，需要商户在自己的系统里填一个回调的 URL，并通过平台设置的 key来做 MD5/HMAC的签名来确保是官方的回调。这都是在《<a title="HTTP API 认证授权术" href="https://coolshell.cn/articles/19395.html" target="_blank" rel="noopener">HTTP API 认证授权术</a>》中提到过的技术，是相对传统的技术。</p>
<p>如今，<b>mTLS是</b>确保云原生应用程序中服务之间的通信安全的首选协议。 也就是双向认证。</p>
<p>传统的 TLS 认证过程是：</p>
<ol dir="auto">
<li>客户端连接到服务器</li>
<li>服务器提供其 TLS 证书</li>
<li>客户端验证服务器的证书</li>
<li>客户端和服务器通过加密的 TLS 连接交换信息</li>
</ol>
<p dir="auto">在 mTLS 中，客户端和服务器都有一个证书，双方都使用他们的公钥/私钥对进行身份验证。与常规 TLS 相比，mTLS 中有额外的步骤来验证双方（以<strong>粗体显示的</strong>额外步骤）：</p>
<ol dir="auto">
<li>客户端连接到服务器</li>
<li>服务器提供其 TLS 证书</li>
<li>客户端验证服务器的证书</li>
<li><strong>客户端出示其 TLS 证书</strong></li>
<li><strong>服务器验证客户端的证书</strong></li>
<li><strong>服务器授予访问权限</strong></li>
<li>客户端和服务器通过加密的 TLS 连接交换信息</li>
</ol>
<p>mTLS 需要“根”TLS 证书；这我们自己来完成证书颁发机构的职责。授权客户端和服务器使用的证书必须与此根证书相对应。根证书是自签名的，这意味着我们需要自己创建它。（注：此方法不适用于公共 Internet 上的单向 TLS，因为外部证书颁发机构必须颁发这些证书）</p>
<p>那么，为什么整个互联网上都用了 TLS 了，为什么 不升级一下使用 mTLS？这里有两方面的原因：</p>
<ul>
<li>公共互联网上要解决的问题是：A) 确保用户访问到的是正确的网站，而不是钓鱼网站。B）网站传输的内容是安全和私密且不会被篡改的。</li>
<li>将 TLS 证书分发到所有最终用户设备将非常困难。生成、管理和验证为此所需的数十亿个证书几乎是不可能的任务。</li>
</ul>
<p>在较小的范围内，mTLS 对于单个组织非常有用且非常实用，尤其是当这些组织采用零信任方法来确保网络安全时。由于默认情况下零信任方法不信任任何用户、设备或请求，因此组织必须能够在每次尝试访问网络中的任何点时对每个用户、设备和请求进行身份验证。mTLS 通过对用户进行身份验证和设备验证来帮助实现这一目标。</p>
<p>关于 mTLS，这里有一个我用 Golang 写的示例 &#8211; <a href="https://github.com/haoel/mTLS" target="_blank" rel="noopener">https://github.com/haoel/mTLS</a>，大家可以参考一下。</p>
<p>P.S. 本文图版中的卡司来自安全圈的标准 Cast，参看<a href="https://en.wikipedia.org/wiki/Alice_and_Bob" target="_blank" rel="noopener"> Alice and Bob</a>。</p>
<p>（全文完）<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/19395.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2019/05/Authorization-360x200-1-150x150.png" alt="HTTP API 认证授权术" width="150" height="150" /></a><a href="https://coolshell.cn/articles/19395.html" class="wp_rp_title">HTTP API 认证授权术</a></li><li ><a href="https://coolshell.cn/articles/21003.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/06/time-bomb-150x150.png" alt="计时攻击 Timing Attacks" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21003.html" class="wp_rp_title">计时攻击 Timing Attacks</a></li><li ><a href="https://coolshell.cn/articles/19840.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2019/10/HTTP-770x513-300x200-1-150x150.jpg" alt="HTTP的前世今生" width="150" height="150" /></a><a href="https://coolshell.cn/articles/19840.html" class="wp_rp_title">HTTP的前世今生</a></li><li ><a href="https://coolshell.cn/articles/18094.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2017/08/enable-https-banner-150x150.png" alt="如何免费的让网站启用HTTPS" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18094.html" class="wp_rp_title">如何免费的让网站启用HTTPS</a></li><li ><a href="https://coolshell.cn/articles/17607.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2017/01/MongoDB-150x150.jpg" alt="从 MongoDB “赎金事件” 看安全问题" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17607.html" class="wp_rp_title">从 MongoDB “赎金事件” 看安全问题</a></li><li ><a href="https://coolshell.cn/articles/17066.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2015/04/phishing-1-150x150.jpg" alt="关于移动端的钓鱼式攻击" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17066.html" class="wp_rp_title">关于移动端的钓鱼式攻击</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/21708.html">网络数字身份认证术</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/21708.html/feed</wfw:commentRss>
			<slash:comments>14</slash:comments>
		
		
			</item>
		<item>
		<title>我做系统架构的一些原则</title>
		<link>https://coolshell.cn/articles/21672.html</link>
					<comments>https://coolshell.cn/articles/21672.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Tue, 21 Dec 2021 07:46:41 +0000</pubDate>
				<category><![CDATA[程序设计]]></category>
		<category><![CDATA[系统架构]]></category>
		<category><![CDATA[Architecture]]></category>
		<category><![CDATA[Design]]></category>
		<category><![CDATA[程序员]]></category>
		<guid isPermaLink="false">https://coolshell.cn/?p=21672</guid>

					<description><![CDATA[<p>工作 20 多年了，这 20 来年看到了很多公司系统架构，也看到了很多问题，在跟这些公司进行交流和讨论的时候，包括进行实施和方案比较的时候，都有很多各种方案的比...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/21672.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/21672.html">我做系统架构的一些原则</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><img decoding="async" loading="lazy" class="alignright size-full wp-image-21682" src="https://coolshell.cn/wp-content/uploads/2021/12/bachelor-mechanical-eng-icon@72x.png" alt="" width="250" height="250" srcset="https://coolshell.cn/wp-content/uploads/2021/12/bachelor-mechanical-eng-icon@72x.png 250w, https://coolshell.cn/wp-content/uploads/2021/12/bachelor-mechanical-eng-icon@72x-150x150.png 150w" sizes="(max-width: 250px) 100vw, 250px" />工作 20 多年了，这 20 来年看到了很多公司系统架构，也看到了很多问题，在跟这些公司进行交流和讨论的时候，包括进行实施和方案比较的时候，都有很多各种方案的比较和妥协，因为相关的经历越来越多，所以，逐渐形成了自己的逻辑和方法论。今天，想写下这篇文章，把我的这些个人的经验和想法总结下来，希望能够让更多的人可以参考和借鉴，并能够做出更好的架构来。另外，我的这些思维方式和原则都针对于现有市面上众多不合理的架构和方案，所以，也算是一种“纠正”……（注意，这篇文章所说的这些架构上的原则，一般适用于相对比较复杂的业务，如果只是一些简单和访问量不大的应用，那么你可能会得出相反的结论）</p>
<h4>原则一：关注于真正的收益而不是技术本身</h4>
<p>对于软件架构来说，我觉得第一重要的是架构的收益，如果不说收益，只是为了技术而技术，而没有任何意义。对于技术收益来说，我觉得下面这几个收益是非常重要的：</p>
<ul>
<li><strong>是否可以降低技术门槛加快整个团队的开发流程</strong>。能够加快整个团队的工程流程，快速发布，是软件工程一直在解决的问题，所以，系统架构需要能够进行并行开发，并行上线和并行运维，而不会让某个团队成为瓶颈点。（注：就算拖累团队的原因是组织构架，也不妨碍我们做出并行的系统架构设计）</li>
<li><strong>是否可以让整个系统可以运行的更稳定</strong>。要让整个系统可以运行的更为的稳定，提升整个系统的 SLA，就需要对有计划和无计划的停机做相应的解决方案（参看《<a title="关于高可用的系统" href="https://coolshell.cn/articles/17459.html" target="_blank" rel="noopener">关于高可用的架构</a>》）</li>
<li><strong>是否可以通过简化和自动化降低成本</strong>。最高优化的成本是人力成本，人的成本除了慢和贵，还有经常不断的 human error。如果不能降低人力成本，反而需要更多的人，那么这个架构设计一定是失败的。除此之外，是时间成本，资金成本。</li>
</ul>
<p>如果一个系统架构不能在上面三个事上起到作用，那就没有意义了。</p>
<p><span id="more-21672"></span></p>
<h4>原则二：以应用服务和 API 为视角，而不是以资源和技术为视角</h4>
<p>国内很多公司都会有很多分工，基本上都会分成运维和开发，运维又会分成基础运维和应用运维，开发则会分成基础核心开发和业务开发。不同的分工会导致完全不同的视角和出发点。比如，基础运维和开发的同学更多的只是关注资源的利用率和性能，而应用运维和业务开发则更多关注的是应用和服务上的东西。这两者本来相关无事，但是因为分布式架构的演进，导致有一些系统已经说不清楚是基础层的还是应用层的了，比如像服务治理上的东西，里面即有底层基础技术，也需要业务的同学来配合，包括 k8s 也样，里面即有底层的如网络这样的技术，也有需要业务配合的 readniess和 liveness 这样的健康检查，以及业务应用需要 configMap 等等 ……</p>
<p><strong>这些东西都让我感觉到所谓 DevOps，其实就是因为很多技术和组件已经分不清是 Dev 还是 Ops 的了，所以，需要合并 Dev和 Ops</strong>。而且，整个组织和架构的优化，已经不能通过调优单一分工或是单一组件能够有很大提升的了。其需要有一种自顶向下的，整体规划，统一设计的方式，才能做到整体的提升（可以试想一下城市交通的优化，当城市规模到一定程度的时候，整体的性能你是无法通过优化几条路或是几条街区来完成的，你需要对整个城市做整体的功能体的规划才可能达到整体效率的提升）。而为了做到整体的提升，需要所有的人都要有一个统一的视角和目标，这几年来，我觉得这个目标就是——<strong>要站在服务和 对外API的视角来看问题，而不是技术和底层的角度。</strong></p>
<h4>原则三：选择最主流和成熟的技术</h4>
<p>技术选型是一件很重要的事，技术一旦选错，那会导致整个架构需要做调整，而对架构的调整重来都不是一件简单的事，我在过去几年内，当系统越来越复杂的时候，用户把他们的  PHP，Python, .NET，或 Node.js 的架构完全都迁移到 Java + Go 的架构上来的案例不断的发生。这个过程还是非常痛苦的，但是你没有办法，当你的系统越来越复杂，越来越大时，你就再也不能在一些玩具技术上玩了，你需要的更为工业化的技术。</p>
<ul>
<li><strong>尽可能的使用更为成熟更为工业化的技术栈，而不是自己熟悉的技术栈</strong>。 所谓工业化的技术栈，你可以看看大多数公司使用的技术栈，比如：互联网，金融，电信……等等 ，大公司会有更多的技术投入，也需要更大规模的生产，所以，他们使用的技术通常来说都是比较工业化的。在技术选型上，千万不要被——“你看某个视频公司也在用这个技术”，或是一些在论坛上看到的一些程序员吐槽技术的观点（没有任何的数据，只有自己的喜好）来决定自己的技术，还是看看主流大多数公司实际在用的技术栈，会更靠谱一些。</li>
<li><strong>选择全球流行的技术，而不是中国流行的技术</strong>。技术这个东西一定是一个全球化的东西，不是一个局域化的事。所以，一定要选国际化的会更好。另外，千万不要被某些公司的“特别案例”骗过去了，那怕这个案例很性感，关键还是要看解决问题的思路和采用的技术是否具有普世性。只有普世性的技术有更强的生命力。</li>
<li><strong>尽可能的使用红利大的主流技术，而不要自己发明轮子，更不要魔改</strong>。我见过好些个公司魔改开源软件，比如有个公司同魔改mesos，最后改着改着发现自己发明另一个 kubernetes。我还见过很多公司或技术团队喜欢自己发明自己的专用轮子，最后都会被主流开源软件所取代。完全没有必要。不重新发明轮子，不魔改，不是因为自己技术不能，而是因为，这个世界早已不是自己干所有事的年代了，这个时代是要想尽方法跟整个产业，整个技术社区融合和合作，这样才会有最大的收益。那些试图因为某个特例需要自成一套的玩法，短期没问题，但长期来说，我都不看好。</li>
<li><strong>绝大多数情况下，如无非常特殊要求，选 Java基本是不会错的</strong>。一方面，这是因为 Java 的业务开发的生产力是非常好的，而且有 Spring 框架保障，代码很难写烂，另外，Java 的社区太成熟了，你需要的各种架构和技术都可以很容易获得，技术红利实在是太大。这种运行在JVM上的语言有太多太多的好处了。在 Java 的技术栈上，你的架构风险和架构的成本（无论是人力成本，时间成本和资金成本）从长期来说都是最优的</li>
</ul>
<p>在我见过的公司中，好些公司的架构都被技术负责人个人的喜好、擅长和个人经验给绑架了，完全不是从一个客观的角度来进行技术选型。其实，从 0 到 1 的阶段，你用什么样的技术都行，如果你做一个简单的应用，没有事务处理没有复杂的交易流程，比如一些论坛、社交之类的应用，你用任何语言都行。但是如果有一天你的系统变复杂了，需要处理交易了，量也上来了，从 1 到 10，甚至从 10 到 100，你的开发团队也变大了，需要构建的系统越来越大，你可能会发现你只有一个选择，就是 Java。想想京东从.NET 到 Java，淘宝从 PHP 到 Java……</p>
<p>注，一些有主观喜好的人一定会对我上述对 Java 的描述感到不适，我还用一些证据说明一下——全中国所有的电商平台，几百家银行，三大电信运营商，所有的保险公司，劵商的系统，医院里的系统，电子政府系统，等等，基本都是用 Java 开发的，包括 AWS 的主流语言也是 Java，阿里云一开始用 C++/Python 写控制系统，后面也开始用 Java ……你可能会说 B站是用 go语言，但是你可能不知道 B 站的电商和大数据是用 Java……懂着数据分析的同学，建议上各大招聘网站上搜一下 Java 的职位数量，你就知道某个技术是否主流和热门……</p>
<h4>原则四：完备性会比性能更重要</h4>
<p>我发现好些公司的架构师做架构的时候，首要考虑的是架构的性能是否能够撑得住多大多大的流量，而不是考虑系统的完备性和扩展性。所以，我已经多次见过这样的案例了，一开始直接使用 MongoDB 这样的非关系型数据库，或是把数据直接放在 Redis 里，而直接放弃关系型数据库的数据完备性的模型，而在后来需要在数据上进行关系查询的时候，发现 NoSQL 的数据库在 Join 上都表现的太差，然后就开始各种飞线，为了不做 Join 就开始冗余数据，然而自己又维护不好冗余数据后带来的数据一致性的问题，导致数据上的各种错乱丢失。</p>
<p>所以，我给如下的一些如下的架构原则：</p>
<ul>
<li><strong>使用最科学严谨的技术模型为主，并以不严谨的模型作为补充</strong>。对于上面那个案例来说，就是——永远使用完备支持 ACID 的关系型数据库，然后用 NoSQL 作补充，而不是完全放弃关系型数据库。这里的原则就是所谓的“先紧后松”，一开始紧了，你可以慢慢松，但是开始松了，以后你想紧再也紧不过来了。</li>
<li><strong>性能上的东西，总是有很多解的</strong>。我这么多年的经历告诉我，性能上的事，总是有解的，手段也是最多的，这个比起架构的完备性和扩展性来说真的不必太过担心。</li>
</ul>
<p>为了追求所谓的性能，把整个系统的完备性丢失掉，相当地得不偿失。</p>
<h4>原则五：制定并遵循服从标准、规范和最佳实践</h4>
<p>这个原则是非常重要的，因为只有服从了标准，你的架构才能够有更好的扩展性。比如：我经常性的见到很多公司的系统既没有服从业界标准，也没有形成自己公司的标准，感觉就像一群乌合之众一样。最典型的例子就是 HTTP 调用的状态返回码。业内给你的标准是 200表示成功，3xx 跳转，4xx 表示调用端出错，5xx 表示服务端出错，我实在是不明白为什么无论成功和失败大家都喜欢返回 200，然后在 body 里指出是否error（前两年我在微信公众号里看到一个有一定名气的互联网老兵推荐使用无论正确还是出错都返回 200 的做法，我在后台再三确认后，我发现这样的架构师真是害人不浅）。这样做最大的问题是——监控系统将在一种低效的状态下工作。监控系统需要把所有的网络请求包打开后才知道是否是错误，而且完全不知道是调用端出错还是服务端出错，于是一些像重试或熔断这样的控制系统完全不知道怎么搞（如果是 4xx错，那么重试或熔断是没有意义的，只有 5xx 才有意义）。<strong>有时候，我会有种越活越退步的感觉，错误码设计这种最基本最基础的东西为什么会没有？并且一个公司会任由着大家乱来？这些基础技能怎么就这样丢掉了？</strong></p>
<p>还有，我还见过一些公司，他们整个组织没有一个统一的用户 ID 的设计，各个系统之间同步用户的数据是通过用户的身份证 ID，是的，就是现实世界的身份证 ID，包括在网关上设置的用户白名单居然也是用身份证 ID。我对这个公司的内的用户隐私管理有很大的担忧。一个企业，一个组织，如果没有标准和规范，也就会有抽象，这一定是要出各种乱子的。</p>
<p>下面，我罗列一些你需要注意的标准和规范（包括但不限于）：</p>
<ul>
<li><strong>服务间调用的协议标准和规范</strong>。这其中包括 Restful API路径, HTTP 方法、状态码、标准头、自定义头等，返回数据 JSon Scheme……等。</li>
<li><strong>一些命名的标准和规范</strong>。这其中包括如：用户 ID，服务名、标签名、状态名、错误码、消息、数据库……等等</li>
<li><strong>日志和监控的规范</strong>。这其中包括：日志格式，监控数据，采样要求，报警……等等</li>
<li><strong>配置上的规范</strong>。这其中包括：操作系统配置、中间件配置，软件包……等等</li>
<li><strong>中间件使用的规范</strong>。数据库，缓存、消息队列……等等</li>
<li><strong>软件和开发库版本统一</strong>。整个组织架构内，软件或开发库的版本最好每年都升一次级，然后在各团队内统一。</li>
</ul>
<p>这里重要说一下两个事：</p>
<ul>
<li><strong>Restful API 的规范</strong>。我觉得是非常重要的，这里给两个我觉得写得最好的参考：<a href="https://github.com/paypal/api-standards/blob/master/api-style-guide.md" target="_blank" rel="noopener">Paypal</a> 和 <a href="https://github.com/microsoft/api-guidelines" target="_blank" rel="noopener">Microsoft</a> 。Restful API 有一个标准和规范最大的好处就是监视可以很容易地做各种统计分析，控制系统可以很容易的做流量编排和调度。</li>
<li><strong>另一个是服务调用链追踪</strong>。对于服务调用链追踪来说，基本上都是参考于 <a href="https://research.google/pubs/pub36356/" target="_blank" rel="noopener">Google Dapper</a> 这篇论文，目前有很多的实现，最严格的实现是 <a href="https://zipkin.io/" target="_blank" rel="noopener">Zipkin</a>，这也是 Spring Cloud Sleuth 的底层实现。Zipkin 贴近 Google Dapper 论文的好处在于——无状态，快速地把 Span 发出来，不消耗服务应用侧的内存和 CPU。这意味着，监控系统宁可自己死了也不能干扰实际应用。</li>
<li><strong>软件升级</strong>。我发现很多公司包括 BAT，他们完全没有软件升级的活动，全靠开发人员自发。然而，这种成体系的活动，是永远不可能靠大众的自发形成的。一个公司至少一年要有一次软件版本升级的review，然后形成软件版本的统一和一致，这样会极太简化系统架构的复杂度。</li>
</ul>
<h4>原则六：重视架构扩展性和可运维性</h4>
<p>在我见过很多架构里，技术人员只考虑当下，但从来不考虑系统的未来扩展性和可运维性。所谓的管生不管养。如果你生下来的孩子胳膊少腿，严重畸形，那么未来是很难玩的。因为架构和软件不是写好就完的，是需要不断修改不断维护的，80%的软件成本都是在维护上。所以，如何让你的架构有更好的扩展性，可以更容易地运维，这个是比较重要的。所谓的扩展性，意味着，我可以很容易地加更多的功能，或是加入更多的系统，而所谓可运维，就是说我可以对线上的系统做任意的变更。扩展性要求的是有标准规范且不耦合的业务架构，可运维性要求的则是可控的能力，也就是一组各式各样的控制系统。</p>
<ul>
<li><strong>通过服务编排架构来降低服务间的耦合</strong>。比如：通过一个业务流程的专用服务，或是像 Workflow，Event Driven Architecture ， Broker，Gateway，Service Discovery 等这类的的中间件来降低服务间的依赖关系。</li>
<li><strong>通过服务发现或服务网关来降低服务依赖所带来的运维复杂度</strong>。服务发现可以很好的降低相关依赖服务的运维复杂度，让你可以很轻松的上线或下线服务，或是进行服务伸缩。</li>
<li><strong>一定要使用各种软件设计的原则</strong>。比如：像SOLID这样的原则（参看《<a title="一些软件设计的原则" href="https://coolshell.cn/articles/4535.html">一些软件设计的原则</a>》），IoC/DIP，SOA 或 Spring Cloud 等 架构的最佳实践（参看《<a title="SteveY对Amazon和Google平台的吐槽 - 67,710 人阅读" href="https://coolshell.cn/articles/5701.html" target="_blank" rel="noopener">SteveY对Amazon和Google平台的吐槽</a>》中的 Service Interface 的那几条军规），分布式系统架构的相关实践（参看：《<a title="分布式系统的事务处理" href="https://coolshell.cn/articles/10910.html" target="_blank" rel="noopener">分布式系统的事务处理</a>》，或微软件的 《<a href="https://docs.microsoft.com/en-us/azure/architecture/patterns/" target="_blank" rel="noopener">Cloud Design Patterns</a>》）……等等</li>
</ul>
<h4>原则七：对控制逻辑进行全面收口</h4>
<p>所有的程序都会有两种逻辑，一种是业务逻辑，一种是控制逻辑，业务逻辑就是完成业务的逻辑，控制逻辑是辅助，比如你用多线程，还是用分布式，是用数据库还是用文件，如何配置、部署，运维、监控，事务控制，服务发现，弹性伸缩，灰度发布，高并发，等等，等等 ……这些都是控制逻辑，跟业务逻辑没有一毛钱关系。控制逻辑的技术深度会通常会比业务逻辑要深一些，门槛也会要高一些，所以，最好要专业的程序员来负责控制逻辑的开发，统一规划统一管理，进行收口。这其中包括：</p>
<ul>
<li><strong>流量收口</strong>。包括南北向和东西向的流量的调度，主要通过流量网关，开发框架 SDK或 Service Mesh 这样的技术。</li>
<li><strong>服务治理收口</strong>。包括：服务发现、健康检查，配置管理、事务、事件、重试、熔断、限流……主要通过开发框架 SDK &#8211; 如：Spring Cloud，或服务网格Service Mesh等技术。</li>
<li><strong>监控数据收口</strong>。包括：日志、指标、调用链……主要通过一些标准主流的探针，再加上后台的数据清洗和数据存储来完成，最好是使用无侵入式的技术。监控的数据必须统一在一个地方进行关联，这样才会产生信息。</li>
<li><strong>资源调度有应用部署的收口</strong>。包括：计算、网络和存储的收口，主要是通过容器化的方案，如k8s来完成。</li>
<li><strong>中间件的收口</strong>。包括：数据库，消息，缓存，服务发现，网关……等等。这类的收口方式一般要在企业内部统一建立一个共享的云化的中间件资源池。</li>
</ul>
<p>对此，这里的原则是：</p>
<ul>
<li><strong>你要选择容易进行业务逻辑和控制逻辑分离的技术</strong>。这里，Java 的 JVM+字节码注入+AOP 式的Spring 开发框架，会带给你太多的优势。</li>
<li><strong>你要选择可以享受“前人种树，后人乘凉”的有技术红利的技术</strong>。如：有庞大社区而且相互兼容的技术，如：Java, Docker,  Ansible，HTTP，Telegraf/Collectd……</li>
<li><strong>中间件你要使用可以 支持HA集群和多租户的技术</strong>。这里基本上所有的主流中间件都会支持 HA 集群方式的。</li>
</ul>
<h4>原则八：不要迁就老旧系统的技术债务</h4>
<p>我发现很多公司都很非常大的技术债务，这些债务具体表现如下：</p>
<ul>
<li><strong>使用老旧的技术</strong>。比如，使用HTTP1.0， Java 1.6，Websphere，ESB，基于 socket的通讯协议，过时的模型……等等</li>
<li><strong>不合理的设计</strong>。比如，在 gateway 中写大量的业务逻辑，单体架构，数据和业务逻辑深度耦合，错误的系统架构（把缓存当数据库，用消息队列同步数据）……等等</li>
<li> <strong>缺少配套设施</strong>。比如，没有自动化测试，没有好的软件文档，没有质量好的代码，没有标准和规范……等等</li>
</ul>
<p>来找我寻求技术帮助的人都有各种各样的问题。我都会对他们苦口婆心地说同样的一句话——“<strong>如果你是来找我 case-by-case 解决问题，我兴趣不大，因为，你们千万不要寄希望能够很简单的把一辆夏利车改成一辆法拉利跑车，或是把一栋地基没打好的歪楼搞正。以前欠下的技术债，都得要还，没打好的地基要重新打，没建配套设施都要建。这些基础设施如果不按照正确科学的方式建立的话，你是不可能有一个好的的系统，我也没办法帮你 case-by-case 的解决问题……</strong>”，一开始，他们都会对我说，没问题，我们就是要还债，但是，最后发现要还的债真多，有点承受不了，就开始现原形了。</p>
<p>他们开始为自己的“欠的技术债”找各种合理化的理由——给你解释各种各样的历史原因和不得以而为之的理由。谈着谈着，让我有一种感觉——他们希望得到一种什么都不改什么都不付出的方式就可以进步的心态，他们宁可让新的技术 low 下来迁就于这些技术债，把新的技术滥用地乱七八糟的。有一个公司，他们的系统架构和技术选型基本都搞错了，使用错误的模型构建系统，导致整个系统的性能非常之差，也才几千万条数据，但他们想的不是还债，不是把地基和配套设施建好，而且要把楼修的更高，上更多的系统——他们觉得现有的系统挺好，性能问题的原因是他们没一个大数据平台，所以要建大数据平台……</p>
<p>我见过很多很多公司，包括大如 BAT 这样的公司，都会在原来的技术债上进行更多的建设，然后，技术债越来越大，利息越来越大，最终成为一个高利贷，再也还不了（我在《<a href="https://coolshell.cn/articles/11656.html" target="_blank" rel="noopener">开发团队的效率</a>》一文中讲过一个 WatchDog 的架构模式，一个系统烂了，不是去改这个系统，而是在旁边建一个系统来看着它，我很难理解为什么会有这样的逻辑，也许是为了要解决更多的就业……）</p>
<p>这里有几个原则和方法我是非常坚持的，分享给大家：</p>
<ul>
<li><strong>与其花大力气迁就技术债务，不如直接还技术债。是所谓的长痛不如短痛。</strong></li>
<li><strong>建设没有技术债的“新城区”，并通过“<a href="https://docs.microsoft.com/en-us/azure/architecture/patterns/anti-corruption-layer" target="_blank" rel="noopener">防腐层</a> ”的架构模型，不要让技术债侵入“新城区”</strong>。</li>
</ul>
<h4>原则九：不要依赖自己的经验，要依赖于数据和学习</h4>
<p>有好些人来找我跟我说他们的技术问题，然后希望我能够给他们一个答案。我说，我需要了解一下你现有系统的情况，也就是需要先做个诊断，我只有得到这些数据后，我才可能明白真正的原因是什么 ，我才可能给你做出一个比较好的技术方案。我个人觉得这是一种对对方负责的方法，因为技术手段太多了，所有的技术手段都有适应的场景，并且有各种 trade-off，所以，只有调研完后才能做出决定。这跟医生看病是一样的，确诊病因不能靠经验，还是要靠诊断数据。在科学面前，所有的经验都是靠不住的……</p>
<p>另外，如果有一天你在做技术决定的时候，开始凭自己以往的经验，那么你就已经不可能再成长了。人都是不可能通过不断重复过去而进步的，人的进步从来都是通过学习自己不知道的东西。所以，千万不要依赖于自己的经验做决定。做任何决定之前，最好花上一点时间，上网查一下相关的资料，技术博客，文章，论文等 ，同时，也看看各个公司，或是各个开源软件他们是怎么做的？然后，比较多种方案的 Pros/Cons，最终形成自己的决定，这样，才可能做出一个更好的决定。</p>
<h4>原则十：千万要小心 X &#8211; Y 问题，要追问原始需求</h4>
<p>对于 <a title="X-Y Problem" href="https://coolshell.cn/articles/10804.html">X-Y 问题</a>，也就是说，用户为了解决 X问题，他觉得用 Y 可以解，于是问我 Y 怎么搞，结果搞到最后，发现原来要解决的 X 问题，这个时候最好的解决方案不是 Y，而是 Z。 这种 X-Y 问题真是相当之多，见的太多太多了。所以，每次用户来找我的时候，我都要不断地追问什么是 X 问题。</p>
<p>比如，好些用户都会来问我他们要一个大数据流式处理，结果追问具体要解决什么样的问题时，才发现他们的问题是因为服务中有大量的状态，需要把相同用户的数据请求放在同一个服务上处理，而且设计上导致一个慢函数拖慢整个应用服务。最终就是做一下性能调优就好了，根本没有必要上什么大数据的流式处理。</p>
<p>我很喜欢追问为什么 ，这种追问，会让客户也跟着来一起重新思考。比如，有个客户来找我评估的一个技术架构的决定，从理论上来说，好像这个架构在用户的这个场景下非常不错。但是，这个场景和这个架构是我职业生涯从来没有见过的。于是，我开始追问这个为什么会是这么一个场景？当我追问的时候，我发现用户都感到这个场景的各种不合理。最后引起了大家非常深刻的研讨，最终用户把那个场景修正后，而架构就突然就变成了一个常见且成熟的的模型……</p>
<h4>原则十一：激进胜于保守，创新与实用并不冲突</h4>
<p>我对技术的态度是比较激进的，但是，所谓的激进并不是瞎搞，也不是见新技术就上，而是积极拥抱会改变未来的新技术，如：Docker/Go，我就非常快地跟进，但是像区块链或是 Rust 这样的，我就不是很积极。因为，其并没有命中我认为的技术趋势的几个特征（参看《<a title="Go语言、Docker 和新技术" href="https://coolshell.cn/articles/18190.html" target="_blank" rel="noopener">Go,Docker 和新技术</a> 》）。当然，我也不是不喜欢的就不学了，我对区块链和 Rust 我一样学习，我也知道这些技术的优势，但我不会大规模使用它们。另外，我也尊重保守的决定，这里面没有对和错。但是，我个人觉得对技术激进的态度比起保守来说有太多的好处了。一方面来说，对于用户来说，很大程度上来说，新技术通常都表面有很好的竞争力，而且我见太多这样成功的公司都在积极拥抱新的技术的，而保守的通常来说都越来越不好。</p>
<p>有一些人会跟我说，我们是实用主义，我们不需要创新，能解决当下的问题就好，所以，我们不需要新技术，现有的技术用好就行了。这类的公司，他们的技术设计第一天就在负债，虽然可以解决当下问题，但是马上就会出现新的问题，然后他们会疲于解决各种问题。最后呢，最后还是会走到新的技术上。</p>
<p>这里的逻辑很简单 —— <strong>进步永远来自于探索，探索是要付出代价的，但是收益更大</strong>。对我而言，不敢冒险才是最大的冒险，不敢犯错才是最大的错误，害怕失去会让你失去的更多……</p>
<p>（全文完）<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/18024.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2017/07/api-design-300x278-2-150x150.jpg" alt="API设计原则 &#8211; Qt官网的设计实践总结" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18024.html" class="wp_rp_title">API设计原则 &#8211; Qt官网的设计实践总结</a></li><li ><a href="https://coolshell.cn/articles/17680.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2017/02/gitlab-600-150x150.jpg" alt="从Gitlab误删除数据库想到的" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17680.html" class="wp_rp_title">从Gitlab误删除数据库想到的</a></li><li ><a href="https://coolshell.cn/articles/17459.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2016/08/HighAvailability-BK-150x150.png" alt="关于高可用的系统" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17459.html" class="wp_rp_title">关于高可用的系统</a></li><li ><a href="https://coolshell.cn/articles/9949.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2013/07/inverted-bookshelf_thumb-150x150.jpg" alt="IoC/DIP其实是一种管理思想" width="150" height="150" /></a><a href="https://coolshell.cn/articles/9949.html" class="wp_rp_title">IoC/DIP其实是一种管理思想</a></li><li ><a href="https://coolshell.cn/articles/6775.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/24.jpg" alt="Bret Victor &#8211; Inventing on Principle" width="150" height="150" /></a><a href="https://coolshell.cn/articles/6775.html" class="wp_rp_title">Bret Victor &#8211; Inventing on Principle</a></li><li ><a href="https://coolshell.cn/articles/5686.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/15.jpg" alt="多些时间能少写些代码" width="150" height="150" /></a><a href="https://coolshell.cn/articles/5686.html" class="wp_rp_title">多些时间能少写些代码</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/21672.html">我做系统架构的一些原则</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/21672.html/feed</wfw:commentRss>
			<slash:comments>163</slash:comments>
		
		
			</item>
		<item>
		<title>Go 编程模式：k8s Visitor 模式</title>
		<link>https://coolshell.cn/articles/21263.html</link>
					<comments>https://coolshell.cn/articles/21263.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Sat, 26 Dec 2020 11:25:46 +0000</pubDate>
				<category><![CDATA[Go 语言]]></category>
		<category><![CDATA[程序设计]]></category>
		<category><![CDATA[编程语言]]></category>
		<category><![CDATA[design pattern]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[golang]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Visitor Pattern]]></category>
		<guid isPermaLink="false">https://coolshell.cn/?p=21263</guid>

					<description><![CDATA[<p>本篇文章主要想讨论一下，Kubernetes 的 kubectl 命令中的使用到到的一个编程模式 &#8211; Visitor（注：其实，kubectl 主要...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/21263.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/21263.html">Go 编程模式：k8s Visitor 模式</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><img decoding="async" loading="lazy" class="alignright size-medium wp-image-21270" src="https://coolshell.cn/wp-content/uploads/2020/12/go.k8s-265x300.png" alt="" width="265" height="300" srcset="https://coolshell.cn/wp-content/uploads/2020/12/go.k8s-265x300.png 265w, https://coolshell.cn/wp-content/uploads/2020/12/go.k8s-906x1024.png 906w, https://coolshell.cn/wp-content/uploads/2020/12/go.k8s-768x868.png 768w, https://coolshell.cn/wp-content/uploads/2020/12/go.k8s-239x270.png 239w, https://coolshell.cn/wp-content/uploads/2020/12/go.k8s.png 1333w" sizes="(max-width: 265px) 100vw, 265px" />本篇文章主要想讨论一下，Kubernetes 的 <code>kubectl</code> 命令中的使用到到的一个编程模式 &#8211; Visitor（注：其实，<code>kubectl</code> 主要使用到了两个一个是Builder，另一个是Visitor）。本来，Visitor 是面向对象设计模英中一个很重要的设计模款（参看Wikipedia<a href="https://en.wikipedia.org/wiki/Visitor_pattern" target="_blank" rel="noopener"> Visitor Pattern词条</a>），这个模式是一种将算法与操作对象的结构分离的一种方法。这种分离的实际结果是能够在不修改结构的情况下向现有对象结构添加新操作，是遵循开放/封闭原则的一种方法。这篇文章我们重点看一下 <code>kubelet</code> 中是怎么使用函数式的方法来实现这个模式的。</p>
<section class="post-series"><h3 class="post-series-title">本文是全系列中第9 / 10篇：<a href="https://coolshell.cn/articles/series/go%e7%bc%96%e7%a8%8b%e6%a8%a1%e5%bc%8f">Go编程模式</a></h3><ul class="post-series-list"><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21128.html">Go编程模式：切片，接口，时间和性能</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21140.html">Go 编程模式：错误处理</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21146.html">Go 编程模式：Functional Options</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21214.html">Go编程模式：委托和反转控制</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21164.html">Go编程模式：Map-Reduce</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21179.html">Go 编程模式：Go Generation</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/17929.html">Go编程模式：修饰器</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21228.html">Go编程模式：Pipeline</a></span></li><li class="post-series-item-current post-series-item"><span class="post-series-item-title">Go 编程模式：k8s Visitor 模式</span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21615.html">Go编程模式 ： 泛型编程</a></span></li></ul><nav class="post-series-nav"><span class="post-series-nav-prev">&laquo; <a href="https://coolshell.cn/articles/21228.html" rel="prev" title="Go编程模式：Pipeline">上一篇文章</a></span><span class="post-series-nav-next"><a href="https://coolshell.cn/articles/21615.html" rel="next" title="Go编程模式 ： 泛型编程">下一篇文章</a> &raquo;</span></nav></section>
<h4>一个简单示例</h4>
<p>我们还是先来看一个简单设计模式的Visitor的示例。</p>
<ul>
<li>我们的代码中有一个<code>Visitor</code>的函数定义，还有一个<code>Shape</code>接口，其需要使用 <code>Visitor</code>函数做为参数。</li>
<li>我们的实例的对象 <code>Circle</code>和 <code>Rectangle</code>实现了 <code>Shape</code> 的接口的 <code>accept()</code> 方法，这个方法就是等外面给我传递一个Visitor。</li>
</ul>
<p><span id="more-21263"></span></p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">package main

import (
    "encoding/json"
    "encoding/xml"
    "fmt"
)

type Visitor func(shape Shape)

type Shape interface {
    accept(Visitor)
}

type Circle struct {
    Radius int
}

func (c Circle) accept(v Visitor) {
    v(c)
}

type Rectangle struct {
    Width, Heigh int
}

func (r Rectangle) accept(v Visitor) {
    v(r)
}
</pre>
<p>然后，我们实现两个Visitor，一个是用来做JSON序列化的，另一个是用来做XML序列化的</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func JsonVisitor(shape Shape) {
    bytes, err := json.Marshal(shape)
    if err != nil {
        panic(err)
    }
    fmt.Println(string(bytes))
}

func XmlVisitor(shape Shape) {
    bytes, err := xml.Marshal(shape)
    if err != nil {
        panic(err)
    }
    fmt.Println(string(bytes))
}
</pre>
<p>下面是我们的使用Visitor这个模式的代码</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func main() {
  c := Circle{10}
  r :=  Rectangle{100, 200}
  shapes := []Shape{c, r}

  for _, s := range shapes {
    s.accept(JsonVisitor)
    s.accept(XmlVisitor)
  }

}</pre>
<p>其实，这段代码的目的就是想解耦 数据结构和 算法，使用 Strategy 模式也是可以完成的，而且会比较干净。<strong>但是在有些情况下，多个Visitor是来访问一个数据结构的不同部分，这种情况下，数据结构有点像一个数据库，而各个Visitor会成为一个个小应用。</strong> <code>kubectl</code>就是这种情况。</p>
<h4>k8s相关背景</h4>
<p>接下来，我们再来了解一下相关的知识背景：</p>
<ul>
<li>对于Kubernetes，其抽象了很多种的Resource，比如：Pod, ReplicaSet, ConfigMap, Volumes, Namespace, Roles &#8230;. 种类非常繁多，这些东西构成为了Kubernetes的数据模型（点击 <a href="https://github.com/kubernauts/practical-kubernetes-problems/blob/master/images/k8s-resources-map.png" target="_blank" rel="noopener">Kubernetes Resources 地图</a> 查看其有多复杂）</li>
<li><code>kubectl</code> 是Kubernetes中的一个客户端命令，操作人员用这个命令来操作Kubernetes。<code>kubectl</code> 会联系到 Kubernetes 的API Server，API Server会联系每个节点上的 <code>kubelet</code> ，从而达到控制每个结点。</li>
<li><code>kubectl</code> 主要的工作是处理用户提交的东西（包括，命令行参数，yaml文件等），然后其会把用户提交的这些东西组织成一个数据结构体，然后把其发送给 API Server。</li>
<li>相关的源代码在 <code>src/k8s.io/cli-runtime/pkg/resource/visitor.go</code> 中（<a href="https://github.com/kubernetes/kubernetes/blob/cea1d4e20b4a7886d8ff65f34c6d4f95efcb4742/staging/src/k8s.io/cli-runtime/pkg/resource/visitor.go" target="_blank" rel="noopener">源码链接</a>）</li>
</ul>
<p><code>kubectl</code> 的代码比较复杂，不过，其本原理简单来说，<strong>它从命令行和yaml文件中获取信息，通过Builder模式并把其转成一系列的资源，最后用 Visitor 模式模式来迭代处理这些Reources</strong>。</p>
<p>下面我们来看看 <code>kubectl</code> 的实现，为了简化，我用一个小的示例来表明 ，而不是直接分析复杂的源码。</p>
<h4>kubectl的实现方法</h4>
<h5>Visitor模式定义</h5>
<p>首先，<code>kubectl</code> 主要是用来处理 <code>Info</code>结构体，下面是相关的定义：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">type VisitorFunc func(*Info, error) error

type Visitor interface {
    Visit(VisitorFunc) error
}

type Info struct {
    Namespace   string
    Name        string
    OtherThings string
}
func (info *Info) Visit(fn VisitorFunc) error {
  return fn(info, nil)
}</pre>
<p>我们可以看到，</p>
<ul>
<li>有一个 <code>VisitorFunc</code> 的函数类型的定义</li>
<li>一个 <code>Visitor</code> 的接口，其中需要 <code>Visit(VisitorFunc) error</code>  的方法（这就像是我们上面那个例子的 <code>Shape</code> ）</li>
<li>最后，为<code>Info</code> 实现 <code>Visitor</code> 接口中的 <code>Visit()</code> 方法，实现就是直接调用传进来的方法（与前面的例子相仿）</li>
</ul>
<p>我们再来定义几种不同类型的 Visitor。</p>
<h5>Name Visitor</h5>
<p>这个Visitor 主要是用来访问 <code>Info</code> 结构中的 <code>Name</code> 和 <code>NameSpace</code> 成员</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">type NameVisitor struct {
  visitor Visitor
}

func (v NameVisitor) Visit(fn VisitorFunc) error {
  return v.visitor.Visit(func(info *Info, err error) error {
    fmt.Println("NameVisitor() before call function")
    err = fn(info, err)
    if err == nil {
      fmt.Printf("==&gt; Name=%s, NameSpace=%s\n", info.Name, info.Namespace)
    }
    fmt.Println("NameVisitor() after call function")
    return err
  })
}</pre>
<p>我们可以看到，上面的代码：</p>
<ul>
<li>声明了一个 <code>NameVisitor</code> 的结构体，这个结构体里有一个 <code>Visitor</code> 接口成员，这里意味着多态。</li>
<li>在实现 <code>Visit()</code> 方法时，其调用了自己结构体内的那个 <code>Visitor</code>的 <code>Visitor()</code> 方法，这其实是一种修饰器的模式，用另一个Visitor修饰了自己（关于修饰器模式，参看《<a title="Go编程模式：修饰器" href="https://coolshell.cn/articles/17929.html" target="_blank" rel="noopener">Go编程模式：修饰器</a>》）</li>
</ul>
<h5>Other Visitor</h5>
<p>这个Visitor主要用来访问 <code>Info</code> 结构中的 <code>OtherThings</code> 成员</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">type OtherThingsVisitor struct {
  visitor Visitor
}

func (v OtherThingsVisitor) Visit(fn VisitorFunc) error {
  return v.visitor.Visit(func(info *Info, err error) error {
    fmt.Println("OtherThingsVisitor() before call function")
    err = fn(info, err)
    if err == nil {
      fmt.Printf("==&gt; OtherThings=%s\n", info.OtherThings)
    }
    fmt.Println("OtherThingsVisitor() after call function")
    return err
  })
}</pre>
<p>实现逻辑同上，我就不再重新讲了</p>
<h5>Log Visitor</h5>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">type LogVisitor struct {
  visitor Visitor
}

func (v LogVisitor) Visit(fn VisitorFunc) error {
  return v.visitor.Visit(func(info *Info, err error) error {
    fmt.Println("LogVisitor() before call function")
    err = fn(info, err)
    fmt.Println("LogVisitor() after call function")
    return err
  })
}</pre>
<h5>使用方代码</h5>
<p>现在我们看看如果使用上面的代码：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func main() {
  info := Info{}
  var v Visitor = &amp;info
  v = LogVisitor{v}
  v = NameVisitor{v}
  v = OtherThingsVisitor{v}

  loadFile := func(info *Info, err error) error {
    info.Name = "Hao Chen"
    info.Namespace = "MegaEase"
    info.OtherThings = "We are running as remote team."
    return nil
  }
  v.Visit(loadFile)
}</pre>
<p>上面的代码，我们可以看到</p>
<ul>
<li>Visitor们一层套一层</li>
<li>我用 <code>loadFile</code> 假装从文件中读如数据</li>
<li>最后一条 <code>v.Visit(loadfile)</code> 我们上面的代码就全部开始激活工作了。</li>
</ul>
<p>上面的代码输出如下的信息，你可以看到代码的执行顺序是怎么执行起来了</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">LogVisitor() before call function
NameVisitor() before call function
OtherThingsVisitor() before call function
==&gt; OtherThings=We are running as remote team.
OtherThingsVisitor() after call function
==&gt; Name=Hao Chen, NameSpace=MegaEase
NameVisitor() after call function
LogVisitor() after call function</pre>
<p>我们可以看到，上面的代码有以下几种功效：</p>
<ul>
<li>解耦了数据和程序。</li>
<li>使用了修饰器模式</li>
<li>还做出来pipeline的模式</li>
</ul>
<p>所以，其实，我们是可以把上面的代码重构一下的。</p>
<h5>Visitor修饰器</h5>
<p>下面，我们用<a title="Go编程模式：修饰器" href="https://coolshell.cn/articles/17929.html" target="_blank" rel="noopener">修饰器模式</a>来重构一下上面的代码。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">type DecoratedVisitor struct {
  visitor    Visitor
  decorators []VisitorFunc
}

func NewDecoratedVisitor(v Visitor, fn ...VisitorFunc) Visitor {
  if len(fn) == 0 {
    return v
  }
  return DecoratedVisitor{v, fn}
}

// Visit implements Visitor
func (v DecoratedVisitor) Visit(fn VisitorFunc) error {
  return v.visitor.Visit(func(info *Info, err error) error {
    if err != nil {
      return err
    }
    if err := fn(info, nil); err != nil {
      return err
    }
    for i := range v.decorators {
      if err := v.decorators[i](info, nil); err != nil {
        return err
      }
    }
    return nil
  })
}</pre>
<p>上面的代码并不复杂，</p>
<ul>
<li>用一个 <code>DecoratedVisitor</code> 的结构来存放所有的<code>VistorFunc</code>函数</li>
<li><code>NewDecoratedVisitor</code> 可以把所有的 <code>VisitorFunc</code>转给它，构造 <code>DecoratedVisitor</code> 对象。</li>
<li><code>DecoratedVisitor</code>实现了 <code>Visit()</code> 方法，里面就是来做一个for-loop，顺着调用所有的 <code>VisitorFunc</code></li>
</ul>
<p>于是，我们的代码就可以这样运作了：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">info := Info{}
var v Visitor = &amp;info
v = NewDecoratedVisitor(v, NameVisitor, OtherVisitor)

v.Visit(LoadFile)</pre>
<p>是不是比之前的那个简单？注意，这个<code>DecoratedVisitor</code> 同样可以成为一个Visitor来使用。</p>
<p>好，上面的这些代码全部存在于 <code>kubectl</code> 的代码中，你看懂了这里面的代码逻辑，相信你也能够看懂 <code>kubectl</code> 的代码了。</p>
<p>（全文完）<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/21615.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2021/09/go-generics-150x150.png" alt="Go编程模式 ： 泛型编程" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21615.html" class="wp_rp_title">Go编程模式 ： 泛型编程</a></li><li ><a href="https://coolshell.cn/articles/21228.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.line_.-150x150.png" alt="Go编程模式：Pipeline" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21228.html" class="wp_rp_title">Go编程模式：Pipeline</a></li><li ><a href="https://coolshell.cn/articles/21214.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.pair_-150x150.png" alt="Go编程模式：委托和反转控制" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21214.html" class="wp_rp_title">Go编程模式：委托和反转控制</a></li><li ><a href="https://coolshell.cn/articles/21179.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.generate-150x150.png" alt="Go 编程模式：Go Generation" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21179.html" class="wp_rp_title">Go 编程模式：Go Generation</a></li><li ><a href="https://coolshell.cn/articles/21164.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.map_.reduce-150x150.png" alt="Go编程模式：Map-Reduce" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21164.html" class="wp_rp_title">Go编程模式：Map-Reduce</a></li><li ><a href="https://coolshell.cn/articles/21146.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.options-150x150.png" alt="Go 编程模式：Functional Options" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21146.html" class="wp_rp_title">Go 编程模式：Functional Options</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/21263.html">Go 编程模式：k8s Visitor 模式</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/21263.html/feed</wfw:commentRss>
			<slash:comments>19</slash:comments>
		
		
			</item>
		<item>
		<title>Go编程模式：Pipeline</title>
		<link>https://coolshell.cn/articles/21228.html</link>
					<comments>https://coolshell.cn/articles/21228.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Sat, 26 Dec 2020 09:04:59 +0000</pubDate>
				<category><![CDATA[Go 语言]]></category>
		<category><![CDATA[程序设计]]></category>
		<category><![CDATA[编程语言]]></category>
		<category><![CDATA[channel]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[golang]]></category>
		<category><![CDATA[pipeline]]></category>
		<guid isPermaLink="false">https://coolshell.cn/?p=21228</guid>

					<description><![CDATA[<p>本篇文章，我们着重介绍Go编程中的Pipeline模式。对于Pipeline用过Unix/Linux命令行的人都不会陌生，他是一种把各种命令拼接起来完成一个更强...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/21228.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/21228.html">Go编程模式：Pipeline</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><img decoding="async" loading="lazy" class="aligncenter wp-image-21258 size-large" src="https://coolshell.cn/wp-content/uploads/2020/12/go.line_.-1024x191.png" alt="" width="640" height="119" srcset="https://coolshell.cn/wp-content/uploads/2020/12/go.line_.-1024x191.png 1024w, https://coolshell.cn/wp-content/uploads/2020/12/go.line_.-300x56.png 300w, https://coolshell.cn/wp-content/uploads/2020/12/go.line_.-768x143.png 768w, https://coolshell.cn/wp-content/uploads/2020/12/go.line_.-1536x286.png 1536w, https://coolshell.cn/wp-content/uploads/2020/12/go.line_.-604x113.png 604w, https://coolshell.cn/wp-content/uploads/2020/12/go.line_..png 1920w" sizes="(max-width: 640px) 100vw, 640px" /></p>
<p>本篇文章，我们着重介绍Go编程中的Pipeline模式。对于Pipeline用过Unix/Linux命令行的人都不会陌生，他是一种把各种命令拼接起来完成一个更强功能的技术方法。在今天，流式处理，函数式编程，以及应用网关对微服务进行简单的API编排，其实都是受pipeline这种技术方式的影响，Pipeline这种技术在可以很容易的把代码按单一职责的原则拆分成多个高内聚低耦合的小模块，然后可以很方便地拼装起来去完成比较复杂的功能。</p>
<section class="post-series"><h3 class="post-series-title">本文是全系列中第8 / 10篇：<a href="https://coolshell.cn/articles/series/go%e7%bc%96%e7%a8%8b%e6%a8%a1%e5%bc%8f">Go编程模式</a></h3><ul class="post-series-list"><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21128.html">Go编程模式：切片，接口，时间和性能</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21140.html">Go 编程模式：错误处理</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21146.html">Go 编程模式：Functional Options</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21214.html">Go编程模式：委托和反转控制</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21164.html">Go编程模式：Map-Reduce</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21179.html">Go 编程模式：Go Generation</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/17929.html">Go编程模式：修饰器</a></span></li><li class="post-series-item-current post-series-item"><span class="post-series-item-title">Go编程模式：Pipeline</span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21263.html">Go 编程模式：k8s Visitor 模式</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21615.html">Go编程模式 ： 泛型编程</a></span></li></ul><nav class="post-series-nav"><span class="post-series-nav-prev">&laquo; <a href="https://coolshell.cn/articles/17929.html" rel="prev" title="Go编程模式：修饰器">上一篇文章</a></span><span class="post-series-nav-next"><a href="https://coolshell.cn/articles/21263.html" rel="next" title="Go 编程模式：k8s Visitor 模式">下一篇文章</a> &raquo;</span></nav></section>
<h4>HTTP 处理</h4>
<p>这种Pipeline的模式，我们在《<a title="Go编程模式：修饰器" href="https://coolshell.cn/articles/17929.html" target="_blank" rel="noopener">Go编程模式：修饰器</a>》中有过一个示例，我们在这里再重温一下。在那篇文章中，我们有一堆如 <code>WithServerHead()</code> 、<code>WithBasicAuth()</code> 、<code>WithDebugLog()</code>这样的小功能代码，在我们需要实现某个HTTP API 的时候，我们就可以很容易的组织起来。</p>
<p>原来的代码是下面这个样子：</p>
<p><span id="more-21228"></span></p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">http.HandleFunc("/v1/hello", WithServerHeader(WithAuthCookie(hello)))
http.HandleFunc("/v2/hello", WithServerHeader(WithBasicAuth(hello)))
http.HandleFunc("/v3/hello", WithServerHeader(WithBasicAuth(WithDebugLog(hello))))</pre>
<p>通过一个代理函数：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">type HttpHandlerDecorator func(http.HandlerFunc) http.HandlerFunc
func Handler(h http.HandlerFunc, decors ...HttpHandlerDecorator) http.HandlerFunc {
    for i := range decors {
        d := decors[len(decors)-1-i] // iterate in reverse
        h = d(h)
    }
    return h
}</pre>
<p>我们就可以移除不断的嵌套像下面这样使用了：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">http.HandleFunc("/v4/hello", Handler(hello,
                WithServerHeader, WithBasicAuth, WithDebugLog))</pre>
<h4>Channel 管理</h4>
<p>当然，如果你要写出一个<a href="https://coolshell.cn/articles/17929.html#%E6%B3%9B%E5%9E%8B%E7%9A%84%E4%BF%AE%E9%A5%B0%E5%99%A8" target="_blank" rel="noopener">泛型的pipeline框架</a>并不容易，而使用<a title="GO 编程模式：Go Generation" href="https://coolshell.cn/articles/21179.html" target="_blank" rel="noopener">Go Generation</a>，但是，我们别忘了Go语言最具特色的 Go Routine 和 Channel 这两个神器完全也可以被我们用来构造这种编程。</p>
<p>Rob Pike在 <a href="https://blog.golang.org/pipelines" target="_blank" rel="noopener">Go Concurrency Patterns: Pipelines and cancellation</a> 这篇blog中介绍了如下的一种编程模式。</p>
<h5>Channel转发函数</h5>
<p>首先，我们需一个 <code>echo()</code>函数，其会把一个整型数组放到一个Channel中，并返回这个Channel</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func echo(nums []int) &lt;-chan int {
  out := make(chan int)
  go func() {
    for _, n := range nums {
      out &lt;- n
    }
    close(out)
  }()
  return out
}</pre>
<p>然后，我们依照这个模式，我们可以写下这个函数。</p>
<h5>平方函数</h5>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func sq(in &lt;-chan int) &lt;-chan int {
  out := make(chan int)
  go func() {
    for n := range in {
      out &lt;- n * n
    }
    close(out)
  }()
  return out
}
</pre>
<h5>过滤奇数函数</h5>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func odd(in &lt;-chan int) &lt;-chan int {
  out := make(chan int)
  go func() {
    for n := range in {
      if n%2 != 0 {
        out &lt;- n
      }
    }
    close(out)
  }()
  return out
}
</pre>
<h5>求和函数</h5>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func sum(in &lt;-chan int) &lt;-chan int {
  out := make(chan int)
  go func() {
    var sum = 0
    for n := range in {
      sum += n
    }
    out &lt;- sum
    close(out)
  }()
  return out
}</pre>
<p>然后，我们的用户端的代码如下所示：（注：<strong>你可能会觉得，<code>sum()</code>，<code>odd()</code> 和 <code>sq()</code>太过于相似。你其实可以通过我们之前的<a href="https://coolshell.cn/articles/21164.html" target="_blank" rel="noopener">Map/Reduce编程模式</a>或是<a href="https://coolshell.cn/articles/21179.html" target="_blank" rel="noopener">Go Generation的方式</a>来合并一下</strong>）</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">var nums = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
for n := range sum(sq(odd(echo(nums)))) {
  fmt.Println(n)
}</pre>
<p>上面的代码类似于我们执行了Unix/Linux命令： <code>echo $nums | sq | sum</code></p>
<p>同样，如果你不想有那么多的函数嵌套，你可以使用一个代理函数来完成。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">type EchoFunc func ([]int) (&lt;- chan int) 
type PipeFunc func (&lt;- chan int) (&lt;- chan int) 

func pipeline(nums []int, echo EchoFunc, pipeFns ... PipeFunc) &lt;- chan int {
  ch  := echo(nums)
  for i := range pipeFns {
    ch = pipeFns[i](ch)
  }
  return ch
}</pre>
<p>然后，就可以这样做了：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">var nums = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}    
for n := range pipeline(nums, gen, odd, sq, sum) {
    fmt.Println(n)
  }</pre>
<h4>Fan in/Out</h4>
<p>动用Go语言的 Go Routine和 Channel还有一个好处，就是可以写出1对多，或多对1的pipeline，也就是Fan In/ Fan Out。下面，我们来看一个Fan in的示例：</p>
<p>我们想通过并发的方式来对一个很长的数组中的质数进行求和运算，我们想先把数组分段求和，然后再把其集中起来。</p>
<p>下面是我们的主函数：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func makeRange(min, max int) []int {
  a := make([]int, max-min+1)
  for i := range a {
    a[i] = min + i
  }
  return a
}

func main() {
  nums := makeRange(1, 10000)
  in := echo(nums)

  const nProcess = 5
  var chans [nProcess]&lt;-chan int
  for i := range chans {
    chans[i] = sum(prime(in))
  }

  for n := range sum(merge(chans[:])) {
    fmt.Println(n)
  }
}</pre>
<p>再看我们的 <code>prime()</code> 函数的实现 ：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func is_prime(value int) bool {
  for i := 2; i &lt;= int(math.Floor(float64(value) / 2)); i++ {
    if value%i == 0 {
      return false
    }
  }
  return value &gt; 1
}

func prime(in &lt;-chan int) &lt;-chan int {
  out := make(chan int)
  go func ()  {
    for n := range in {
      if is_prime(n) {
        out &lt;- n
      }
    }
    close(out)
  }()
  return out
}</pre>
<p>我们可以看到，</p>
<ul>
<li>我们先制造了从1到10000的一个数组，</li>
<li>然后，把这堆数组全部 <code>echo</code>到一个channel里 &#8211; <code>in</code></li>
<li>此时，生成 5 个 Channel，然后都调用 <code>sum(prime(in))</code> ，于是每个Sum的Go Routine都会开始计算和</li>
<li>最后再把所有的结果再求和拼起来，得到最终的结果。</li>
</ul>
<p>其中的merge代码如下：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func merge(cs []&lt;-chan int) &lt;-chan int {
  var wg sync.WaitGroup
  out := make(chan int)

  wg.Add(len(cs))
  for _, c := range cs {
    go func(c &lt;-chan int) {
      for n := range c {
        out &lt;- n
      }
      wg.Done()
    }(c)
  }
  go func() {
    wg.Wait()
    close(out)
  }()
  return out
}</pre>
<p>用图片表示一下，整个程序的结构如下所示：</p>
<p><img decoding="async" loading="lazy" class="aligncenter size-large wp-image-21231" src="https://coolshell.cn/wp-content/uploads/2020/12/pipeline-1024x425.png" alt="" width="640" height="266" srcset="https://coolshell.cn/wp-content/uploads/2020/12/pipeline-1024x425.png 1024w, https://coolshell.cn/wp-content/uploads/2020/12/pipeline-300x124.png 300w, https://coolshell.cn/wp-content/uploads/2020/12/pipeline-768x319.png 768w, https://coolshell.cn/wp-content/uploads/2020/12/pipeline-1536x637.png 1536w, https://coolshell.cn/wp-content/uploads/2020/12/pipeline-604x251.png 604w, https://coolshell.cn/wp-content/uploads/2020/12/pipeline.png 1572w" sizes="(max-width: 640px) 100vw, 640px" /></p>
<h4>延伸阅读</h4>
<p>如果你还想了解更多的这样的与并发相关的技术，可以参看下面这些资源：</p>
<ul>
<li><b>Go</b> <b>Concurrency</b> <b>Patterns</b><b></b> &#8211; <b>Rob</b> <b>Pike &#8211;</b> 2012 Google I/O <b><br />
</b>presents the basics of Go‘s concurrency primitives and several ways to apply them.<br />
<u><a href="https://www.youtube.com/watch?v=f6kdp27TYZs">https://www.youtube.com/watch?v=f6kdp27TYZs</a></u></li>
<li><b>Advanced Go Concurrency Patterns </b>&#8211; <b>Rob</b> <b>Pike</b> – 2013 Google I/O <b><br />
</b>covers more complex uses of Go&#8217;s primitives, especially select.<br />
<a href="https://blog.golang.org/advanced-go-concurrency-patterns">https://blog.golang.org/advanced-go-concurrency-patterns</a></li>
<li><b>Squinting at Power Series </b>&#8211; <b>Douglas McIlroy</b>‘s paper <b><br />
</b>shows how Go-like concurrency provides elegant support for complex calculations.<br />
<a href="https://swtch.com/~rsc/thread/squint.pdf">https://swtch.com/~rsc/thread/squint.pdf</a></li>
</ul>
<p>（全文完）<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/21615.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2021/09/go-generics-150x150.png" alt="Go编程模式 ： 泛型编程" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21615.html" class="wp_rp_title">Go编程模式 ： 泛型编程</a></li><li ><a href="https://coolshell.cn/articles/21263.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.k8s-150x150.png" alt="Go 编程模式：k8s Visitor 模式" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21263.html" class="wp_rp_title">Go 编程模式：k8s Visitor 模式</a></li><li ><a href="https://coolshell.cn/articles/21214.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.pair_-150x150.png" alt="Go编程模式：委托和反转控制" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21214.html" class="wp_rp_title">Go编程模式：委托和反转控制</a></li><li ><a href="https://coolshell.cn/articles/21179.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.generate-150x150.png" alt="Go 编程模式：Go Generation" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21179.html" class="wp_rp_title">Go 编程模式：Go Generation</a></li><li ><a href="https://coolshell.cn/articles/21164.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.map_.reduce-150x150.png" alt="Go编程模式：Map-Reduce" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21164.html" class="wp_rp_title">Go编程模式：Map-Reduce</a></li><li ><a href="https://coolshell.cn/articles/21146.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.options-150x150.png" alt="Go 编程模式：Functional Options" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21146.html" class="wp_rp_title">Go 编程模式：Functional Options</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/21228.html">Go编程模式：Pipeline</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/21228.html/feed</wfw:commentRss>
			<slash:comments>14</slash:comments>
		
		
			</item>
		<item>
		<title>Go编程模式：委托和反转控制</title>
		<link>https://coolshell.cn/articles/21214.html</link>
					<comments>https://coolshell.cn/articles/21214.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Sat, 26 Dec 2020 08:57:48 +0000</pubDate>
				<category><![CDATA[Go 语言]]></category>
		<category><![CDATA[程序设计]]></category>
		<category><![CDATA[编程语言]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[golang]]></category>
		<category><![CDATA[IoC]]></category>
		<guid isPermaLink="false">https://coolshell.cn/?p=21214</guid>

					<description><![CDATA[<p>反转控制IoC &#8211; Inversion of Control 是一种软件设计的方法，其主要的思想是把控制逻辑与业务逻辑分享，不要在业务逻辑里写控制逻...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/21214.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/21214.html">Go编程模式：委托和反转控制</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><figure id="attachment_21256" aria-describedby="caption-attachment-21256" style="width: 300px" class="wp-caption alignright"><img decoding="async" loading="lazy" class="wp-image-21256 size-medium" src="https://coolshell.cn/wp-content/uploads/2020/12/go.pair_-300x298.png" alt="" width="300" height="298" srcset="https://coolshell.cn/wp-content/uploads/2020/12/go.pair_-300x298.png 300w, https://coolshell.cn/wp-content/uploads/2020/12/go.pair_-1024x1017.png 1024w, https://coolshell.cn/wp-content/uploads/2020/12/go.pair_-150x150.png 150w, https://coolshell.cn/wp-content/uploads/2020/12/go.pair_-768x762.png 768w, https://coolshell.cn/wp-content/uploads/2020/12/go.pair_-1536x1525.png 1536w, https://coolshell.cn/wp-content/uploads/2020/12/go.pair_-2048x2033.png 2048w, https://coolshell.cn/wp-content/uploads/2020/12/go.pair_-272x270.png 272w" sizes="(max-width: 300px) 100vw, 300px" /><figcaption id="caption-attachment-21256" class="wp-caption-text">图片来源：<a href="https://gophersource.com/" target="_blank" rel="noopener">GopherSource</a></figcaption></figure></p>
<p>反转控制<a title="IoC - Inversion of Control" href="https://en.wikipedia.org/wiki/Inversion_of_control" target="_blank" rel="noopener">IoC &#8211; Inversion of Control</a> 是一种软件设计的方法，其主要的思想是把控制逻辑与业务逻辑分享，不要在业务逻辑里写控制逻辑，这样会让控制逻辑依赖于业务逻辑，而是反过来，让业务逻辑依赖控制逻辑。在《<a href="https://coolshell.cn/articles/9949.html" target="_blank" rel="noopener">IoC/DIP其实是一种管理思想</a>》中的那个开关和电灯的示例一样，开关是控制逻辑，电器是业务逻辑，不要在电器中实现开关，而是把开关抽象成一种协议，让电器都依赖之。这样的编程方式可以有效的降低程序复杂度，并提升代码重用。</p>
<section class="post-series"><h3 class="post-series-title">本文是全系列中第4 / 10篇：<a href="https://coolshell.cn/articles/series/go%e7%bc%96%e7%a8%8b%e6%a8%a1%e5%bc%8f">Go编程模式</a></h3><ul class="post-series-list"><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21128.html">Go编程模式：切片，接口，时间和性能</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21140.html">Go 编程模式：错误处理</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21146.html">Go 编程模式：Functional Options</a></span></li><li class="post-series-item-current post-series-item"><span class="post-series-item-title">Go编程模式：委托和反转控制</span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21164.html">Go编程模式：Map-Reduce</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21179.html">Go 编程模式：Go Generation</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/17929.html">Go编程模式：修饰器</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21228.html">Go编程模式：Pipeline</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21263.html">Go 编程模式：k8s Visitor 模式</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21615.html">Go编程模式 ： 泛型编程</a></span></li></ul><nav class="post-series-nav"><span class="post-series-nav-prev">&laquo; <a href="https://coolshell.cn/articles/21146.html" rel="prev" title="Go 编程模式：Functional Options">上一篇文章</a></span><span class="post-series-nav-next"><a href="https://coolshell.cn/articles/21164.html" rel="next" title="Go编程模式：Map-Reduce">下一篇文章</a> &raquo;</span></nav></section>
<p>面向对象的设计模式这里不提了，我们来看看Go语言使用Embed结构的一个示例。</p>
<p><span id="more-21214"></span></p>
<h4>嵌入和委托</h4>
<h5>结构体嵌入</h5>
<p>在Go语言中，我们可以很方便的把一个结构体给嵌到另一个结构体中。如下所示：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">type Widget struct {
    X, Y int
}
type Label struct {
    Widget        // Embedding (delegation)
    Text   string // Aggregation
}</pre>
<p>上面的示例中，我们把 <code>Widget</code>嵌入到了 <code>Label</code> 中，于是，我们可以这样使用：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">label := Label{Widget{10, 10}, "State:"}

label.X = 11
label.Y = 12</pre>
<p>如果在 <code>Label</code> 结构体里出现了重名，就需要解决重名，例如，如果 成员 <code>X</code> 重名，用 <code>label.X</code>表明 是自己的<code>X</code> ，用  <code>label.Wedget.X</code> 表示嵌入过来的。</p>
<p>有了这样的嵌入，就可以像UI组件一样的在结构构的设计上进行层层分解。比如，我可以新出来两个结构体 <code>Button</code> 和 <code>ListBox</code>：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">type Button struct {
    Label // Embedding (delegation)
}

type ListBox struct {
    Widget          // Embedding (delegation)
    Texts  []string // Aggregation
    Index  int      // Aggregation
}</pre>
<h5>方法重写</h5>
<p>然后，我们需要两个接口 <code>Painter</code> 用于把组件画出来，<code>Clicker</code> 用于表明点击事件：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">type Painter interface {
    Paint()
}
 
type Clicker interface {
    Click()
}</pre>
<p>当然，</p>
<ul>
<li>对于 <code>Lable</code> 来说，只有 <code>Painter</code> ，没有<code>Clicker</code></li>
<li>对于 <code>Button</code> 和 <code>ListBox</code>来说，<code>Painter</code> 和<code>Clicker</code>都有。</li>
</ul>
<p>下面是一些实现：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func (label Label) Paint() {
  fmt.Printf("%p:Label.Paint(%q)\n", &amp;label, label.Text)
}

//因为这个接口可以通过 Label 的嵌入带到新的结构体，
//所以，可以在 Button 中可以重载这个接口方法以
func (button Button) Paint() { // Override
    fmt.Printf("Button.Paint(%s)\n", button.Text)
}
func (button Button) Click() {
    fmt.Printf("Button.Click(%s)\n", button.Text)
}


func (listBox ListBox) Paint() {
    fmt.Printf("ListBox.Paint(%q)\n", listBox.Texts)
}
func (listBox ListBox) Click() {
    fmt.Printf("ListBox.Click(%q)\n", listBox.Texts)
}</pre>
<p>这里，需要重点提示一下，<strong><code>Button.Paint()</code> 接口可以通过 Label 的嵌入带到新的结构体，如果 <code>Button.Paint()</code> 不实现的话，会调用 <code>Label.Paint()</code> ，所以，在 <code>Button</code> 中声明 <code>Paint()</code> 方法，相当于Override</strong>。</p>
<h5>嵌入结构多态</h5>
<p>通过下面的程序可以看到，整个多态是怎么执行的。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">button1 := Button{Label{Widget{10, 70}, "OK"}}
button2 := NewButton(50, 70, "Cancel")
listBox := ListBox{Widget{10, 40}, 
    []string{"AL", "AK", "AZ", "AR"}, 0}

for _, painter := range []Painter{label, listBox, button1, button2} {
    painter.Paint()
}
 
for _, widget := range []interface{}{label, listBox, button1, button2} {
  widget.(Painter).Paint()
  if clicker, ok := widget.(Clicker); ok {
    clicker.Click()
  }
  fmt.Println() // print a empty line 
}</pre>
<p>我们可以看到，我们可以使用接口来多态，也可以使用 泛型的 <code>interface{}</code> 来多态，但是需要有一个类型转换。</p>
<h4>反转控制</h4>
<p>我们再来看一个示例，我们有一个存放整数的数据结构，如下所示：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">type IntSet struct {
    data map[int]bool
}
func NewIntSet() IntSet {
    return IntSet{make(map[int]bool)}
}
func (set *IntSet) Add(x int) {
    set.data[x] = true
}
func (set *IntSet) Delete(x int) {
    delete(set.data, x)
}
func (set *IntSet) Contains(x int) bool {
    return set.data[x]
}</pre>
<p>其中实现了 <code>Add()</code> 、<code>Delete()</code> 和 <code>Contains()</code> 三个操作，前两个是写操作，后一个是读操作。</p>
<h5>实现Undo功能</h5>
<p>现在我们想实现一个 Undo 的功能。我们可以把把 <code>IntSet</code> 再包装一下变成 <code>UndoableIntSet</code> 代码如下所示：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">type UndoableIntSet struct { // Poor style
    IntSet    // Embedding (delegation)
    functions []func()
}
 
func NewUndoableIntSet() UndoableIntSet {
    return UndoableIntSet{NewIntSet(), nil}
}
 

func (set *UndoableIntSet) Add(x int) { // Override
    if !set.Contains(x) {
        set.data[x] = true
        set.functions = append(set.functions, func() { set.Delete(x) })
    } else {
        set.functions = append(set.functions, nil)
    }
}


func (set *UndoableIntSet) Delete(x int) { // Override
    if set.Contains(x) {
        delete(set.data, x)
        set.functions = append(set.functions, func() { set.Add(x) })
    } else {
        set.functions = append(set.functions, nil)
    }
}

func (set *UndoableIntSet) Undo() error {
    if len(set.functions) == 0 {
        return errors.New("No functions to undo")
    }
    index := len(set.functions) - 1
    if function := set.functions[index]; function != nil {
        function()
        set.functions[index] = nil // For garbage collection
    }
    set.functions = set.functions[:index]
    return nil
}</pre>
<p>在上面的代码中，我们可以看到</p>
<ul>
<li>我们在 <code>UndoableIntSet</code> 中嵌入了<code>IntSet</code> ，然后Override了 它的 <code>Add()</code>和 <code>Delete()</code> 方法。</li>
<li><code>Contains()</code> 方法没有Override，所以，会被带到 <code>UndoableInSet</code> 中来了。</li>
<li>在Override的 <code>Add()</code>中，记录 <code>Delete</code> 操作</li>
<li>在Override的 <code>Delete()</code> 中，记录 <code>Add</code> 操作</li>
<li>在新加入 <code>Undo()</code> 中进行Undo操作。</li>
</ul>
<p>通过这样的方式来为已有的代码扩展新的功能是一个很好的选择，这样，可以在重用原有代码功能和重新新的功能中达到一个平衡。但是，这种方式最大的问题是，Undo操作其实是一种控制逻辑，并不是业务逻辑，所以，在复用 Undo这个功能上是有问题。因为其中加入了大量跟 <code>IntSet</code> 相关的业务逻辑。</p>
<h5>反转依赖</h5>
<p>现在我们来看另一种方法：</p>
<p>我们先声明一种函数接口，表现我们的Undo控制可以接受的函数签名是什么样的：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">type Undo []func()</pre>
<p>有了上面这个协议后，我们的Undo控制逻辑就可以写成如下：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func (undo *Undo) Add(function func()) {
  *undo = append(*undo, function)
}

func (undo *Undo) Undo() error {
  functions := *undo
  if len(functions) == 0 {
    return errors.New("No functions to undo")
  }
  index := len(functions) - 1
  if function := functions[index]; function != nil {
    function()
    functions[index] = nil // For garbage collection
  }
  *undo = functions[:index]
  return nil
}</pre>
<p>这里你不必觉得奇怪， <code>Undo</code> 本来就是一个类型，不必是一个结构体，是一个函数数组也没什么问题。</p>
<p>然后，我们在我们的IntSet里嵌入 Undo，然后，再在 <code>Add()</code> 和 <code>Delete()</code> 里使用上面的方法，就可以完成功能。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang" data-enlighter-highlight="3">type IntSet struct {
    data map[int]bool
    undo Undo
}
 
func NewIntSet() IntSet {
    return IntSet{data: make(map[int]bool)}
}

func (set *IntSet) Undo() error {
    return set.undo.Undo()
}
 
func (set *IntSet) Contains(x int) bool {
    return set.data[x]
}

func (set *IntSet) Add(x int) {
    if !set.Contains(x) {
        set.data[x] = true
        set.undo.Add(func() { set.Delete(x) })
    } else {
        set.undo.Add(nil)
    }
}
 
func (set *IntSet) Delete(x int) {
    if set.Contains(x) {
        delete(set.data, x)
        set.undo.Add(func() { set.Add(x) })
    } else {
        set.undo.Add(nil)
    }
}</pre>
<p>这个就是控制反转，不再由 控制逻辑 <code>Undo</code> 来依赖业务逻辑 <code>IntSet</code>，而是由业务逻辑 <code>IntSet</code> 来依赖 <code>Undo</code> 。其依赖的是其实是一个协议，这个协议是一个没有参数的函数数组。我们也可以看到，我们 Undo 的代码就可以复用了。</p>
<p>（全文完）<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/21615.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2021/09/go-generics-150x150.png" alt="Go编程模式 ： 泛型编程" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21615.html" class="wp_rp_title">Go编程模式 ： 泛型编程</a></li><li ><a href="https://coolshell.cn/articles/21263.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.k8s-150x150.png" alt="Go 编程模式：k8s Visitor 模式" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21263.html" class="wp_rp_title">Go 编程模式：k8s Visitor 模式</a></li><li ><a href="https://coolshell.cn/articles/21228.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.line_.-150x150.png" alt="Go编程模式：Pipeline" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21228.html" class="wp_rp_title">Go编程模式：Pipeline</a></li><li ><a href="https://coolshell.cn/articles/21179.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.generate-150x150.png" alt="Go 编程模式：Go Generation" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21179.html" class="wp_rp_title">Go 编程模式：Go Generation</a></li><li ><a href="https://coolshell.cn/articles/21164.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.map_.reduce-150x150.png" alt="Go编程模式：Map-Reduce" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21164.html" class="wp_rp_title">Go编程模式：Map-Reduce</a></li><li ><a href="https://coolshell.cn/articles/21146.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.options-150x150.png" alt="Go 编程模式：Functional Options" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21146.html" class="wp_rp_title">Go 编程模式：Functional Options</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/21214.html">Go编程模式：委托和反转控制</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/21214.html/feed</wfw:commentRss>
			<slash:comments>15</slash:comments>
		
		
			</item>
		<item>
		<title>Go 编程模式：Go Generation</title>
		<link>https://coolshell.cn/articles/21179.html</link>
					<comments>https://coolshell.cn/articles/21179.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Fri, 25 Dec 2020 09:06:36 +0000</pubDate>
				<category><![CDATA[Go 语言]]></category>
		<category><![CDATA[程序设计]]></category>
		<category><![CDATA[编程语言]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[golang]]></category>
		<category><![CDATA[Template]]></category>
		<guid isPermaLink="false">https://coolshell.cn/?p=21179</guid>

					<description><![CDATA[<p>在本篇文章中，我们将要学习一下Go语言的代码生成的玩法。Go语言代码生成主要还是用来解决编程泛型的问题，泛型编程主要解决的问题是因为静态类型语言有类型，所以，相...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/21179.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/21179.html">Go 编程模式：Go Generation</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><figure id="attachment_21254" aria-describedby="caption-attachment-21254" style="width: 296px" class="wp-caption alignright"><img decoding="async" loading="lazy" class="wp-image-21254 size-medium" src="https://coolshell.cn/wp-content/uploads/2020/12/go.generate-296x300.png" alt="" width="296" height="300" srcset="https://coolshell.cn/wp-content/uploads/2020/12/go.generate-296x300.png 296w, https://coolshell.cn/wp-content/uploads/2020/12/go.generate-1011x1024.png 1011w, https://coolshell.cn/wp-content/uploads/2020/12/go.generate-768x778.png 768w, https://coolshell.cn/wp-content/uploads/2020/12/go.generate-1516x1536.png 1516w, https://coolshell.cn/wp-content/uploads/2020/12/go.generate-267x270.png 267w, https://coolshell.cn/wp-content/uploads/2020/12/go.generate.png 1524w" sizes="(max-width: 296px) 100vw, 296px" /><figcaption id="caption-attachment-21254" class="wp-caption-text">图片来源：<a href="https://gophersource.com/" target="_blank" rel="noopener">GopherSource</a></figcaption></figure></p>
<p>在本篇文章中，我们将要学习一下Go语言的代码生成的玩法。Go语言代码生成主要还是用来解决编程泛型的问题，泛型编程主要解决的问题是因为静态类型语言有类型，所以，相关的算法或是对数据处理的程序会因为类型不同而需要复制一份，这样导致数据类型和算法功能耦合的问题。泛型编程可以解决这样的问题，就是说，在写代码的时候，不用关心处理数据的类型，只需要关心相当处理逻辑。泛型编程是静态语言中非常非常重要的特征，如果没有泛型，我们很难做到多态，也很难完成抽象，会导致我们的代码冗余量很大。</p>
<section class="post-series"><h3 class="post-series-title">本文是全系列中第6 / 10篇：<a href="https://coolshell.cn/articles/series/go%e7%bc%96%e7%a8%8b%e6%a8%a1%e5%bc%8f">Go编程模式</a></h3><ul class="post-series-list"><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21128.html">Go编程模式：切片，接口，时间和性能</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21140.html">Go 编程模式：错误处理</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21146.html">Go 编程模式：Functional Options</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21214.html">Go编程模式：委托和反转控制</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21164.html">Go编程模式：Map-Reduce</a></span></li><li class="post-series-item-current post-series-item"><span class="post-series-item-title">Go 编程模式：Go Generation</span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/17929.html">Go编程模式：修饰器</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21228.html">Go编程模式：Pipeline</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21263.html">Go 编程模式：k8s Visitor 模式</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21615.html">Go编程模式 ： 泛型编程</a></span></li></ul><nav class="post-series-nav"><span class="post-series-nav-prev">&laquo; <a href="https://coolshell.cn/articles/21164.html" rel="prev" title="Go编程模式：Map-Reduce">上一篇文章</a></span><span class="post-series-nav-next"><a href="https://coolshell.cn/articles/17929.html" rel="next" title="Go编程模式：修饰器">下一篇文章</a> &raquo;</span></nav></section>
<h4>现实中的类比</h4>
<p>举个现实当中的例子，用螺丝刀来做具比方，螺丝刀本来就是一个拧螺丝的动作，但是因为螺丝的类型太多，有平口的，有十字口的，有六角的……螺丝还有大小尺寸，导致我们的螺丝刀为了要适配各种千奇百怪的螺丝类型（样式和尺寸），导致要做出各种各样的螺丝刀。</p>
<table>
<tbody>
<tr>
<td><img decoding="async" loading="lazy" class="aligncenter wp-image-21180 size-medium" src="https://coolshell.cn/wp-content/uploads/2020/12/type01-300x225.png" alt="" width="300" height="225" srcset="https://coolshell.cn/wp-content/uploads/2020/12/type01-300x225.png 300w, https://coolshell.cn/wp-content/uploads/2020/12/type01-360x270.png 360w, https://coolshell.cn/wp-content/uploads/2020/12/type01.png 750w" sizes="(max-width: 300px) 100vw, 300px" /></td>
<td><img decoding="async" loading="lazy" class="aligncenter wp-image-21181 size-medium" src="https://coolshell.cn/wp-content/uploads/2020/12/type02-300x225.png" alt="" width="300" height="225" srcset="https://coolshell.cn/wp-content/uploads/2020/12/type02-300x225.png 300w, https://coolshell.cn/wp-content/uploads/2020/12/type02-768x576.png 768w, https://coolshell.cn/wp-content/uploads/2020/12/type02-360x270.png 360w, https://coolshell.cn/wp-content/uploads/2020/12/type02.png 802w" sizes="(max-width: 300px) 100vw, 300px" /></td>
</tr>
</tbody>
</table>
<p>而真正的抽象是螺丝刀不应该关心螺丝的类型，只要关注好自己的功能是否完备，并让自己可以适配于不同类型的螺丝，如下所示，这就是所谓的泛型编程要解决的实际问题。</p>
<p><span id="more-21179"></span></p>
<table>
<tbody>
<tr>
<td colspan="2"><img decoding="async" loading="lazy" class="aligncenter wp-image-21182 size-medium" src="https://coolshell.cn/wp-content/uploads/2020/12/type03-300x226.png" alt="" width="300" height="226" srcset="https://coolshell.cn/wp-content/uploads/2020/12/type03-300x226.png 300w, https://coolshell.cn/wp-content/uploads/2020/12/type03-359x270.png 359w, https://coolshell.cn/wp-content/uploads/2020/12/type03.png 750w" sizes="(max-width: 300px) 100vw, 300px" /></td>
</tr>
</tbody>
</table>
<h4>Go语方的类型检查</h4>
<p>因为Go语言目前并不支持真正的泛型，所以，只能用 <code>interface{}</code> 这样的类似于 <code>void*</code> 这种过度泛型来玩这就导致了我们在实际过程中就需要进行类型检查。Go语言的类型检查有两种技术，一种是 Type Assert，一种是Reflection。</p>
<h5>Type Assert</h5>
<p>这种技术，一般是对某个变量进行 <code>.(type)</code>的转型操作，其会返回两个值， <code>variable, error</code>，第一个返回值是被转换好的类型，第二个是如果不能转换类型，则会报错。</p>
<p>比如下面的示例，我们有一个通用类型的容器，可以进行 <code>Put(val)</code>和 <code>Get()</code>，注意，其使用了 <code>interface{}</code>作泛型</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">//Container is a generic container, accepting anything.
type Container []interface{}

//Put adds an element to the container.
func (c *Container) Put(elem interface{}) {
    *c = append(*c, elem)
}
//Get gets an element from the container.
func (c *Container) Get() interface{} {
    elem := (*c)[0]
    *c = (*c)[1:]
    return elem
}</pre>
<p>在使用中，我们可以这样使用</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">intContainer := &amp;Container{}
intContainer.Put(7)
intContainer.Put(42)</pre>
<p>但是，在把数据取出来时，因为类型是 <code>interface{}</code> ，所以，你还要做一个转型，如果转型成功能才能进行后续操作（因为 <code>interface{}</code>太泛了，泛到什么类型都可以放）下在是一个Type Assert的示例：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang" data-enlighter-highlight="2">// assert that the actual type is int
elem, ok := intContainer.Get().(int)
if !ok {
    fmt.Println("Unable to read an int from intContainer")
}

fmt.Printf("assertExample: %d (%T)\n", elem, elem)
</pre>
<h5>Reflection</h5>
<p>对于反射，我们需要把上面的代码修改如下：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">type Container struct {
    s reflect.Value
}
func NewContainer(t reflect.Type, size int) *Container {
    if size &lt;=0  { size=64 }
    return &amp;Container{
        s: reflect.MakeSlice(reflect.SliceOf(t), 0, size), 
    }
}
func (c *Container) Put(val interface{})  error {
    if reflect.ValueOf(val).Type() != c.s.Type().Elem() {
        return fmt.Errorf(“Put: cannot put a %T into a slice of %s", 
            val, c.s.Type().Elem()))
    }
    c.s = reflect.Append(c.s, reflect.ValueOf(val))
    return nil
}
func (c *Container) Get(refval interface{}) error {
    if reflect.ValueOf(refval).Kind() != reflect.Ptr ||
        reflect.ValueOf(refval).Elem().Type() != c.s.Type().Elem() {
        return fmt.Errorf("Get: needs *%s but got %T", c.s.Type().Elem(), refval)
    }
    reflect.ValueOf(refval).Elem().Set( c.s.Index(0) )
    c.s = c.s.Slice(1, c.s.Len())
    return nil
}</pre>
<p>上面的代码并不难读，这是完全使用 reflection的玩法，其中</p>
<ul>
<li>在 <code>NewContainer()</code>会根据参数的类型初始化一个Slice</li>
<li>在 <code>Put()</code>时候，会检查 <code>val</code> 是否和Slice的类型一致。</li>
<li>在 <code>Get()</code>时，我们需要用一个入参的方式，因为我们没有办法返回 <code>reflect.Value</code> 或是 <code>interface{}</code>，不然还要做Type Assert</li>
<li>但是有类型检查，所以，必然会有检查不对的道理 ，因此，需要返回 <code>error</code></li>
</ul>
<p>于是在使用上面这段代码的时候，会是下面这个样子：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">f1 := 3.1415926
f2 := 1.41421356237

c := NewMyContainer(reflect.TypeOf(f1), 16)

if err := c.Put(f1); err != nil {
  panic(err)
}
if err := c.Put(f2); err != nil {
  panic(err)
}

g := 0.0

if err := c.Get(&amp;g); err != nil {
  panic(err)
}
fmt.Printf("%v (%T)\n", g, g) //3.1415926 (float64)
fmt.Println(c.s.Index(0)) //1.4142135623</pre>
<p>我们可以看到，Type Assert是不用了，但是用反射写出来的代码还是有点复杂的。那么有没有什么好的方法？</p>
<h4>它山之石</h4>
<p>对于泛型编程最牛的语言 C++ 来说，这类的问题都是使用 Template来解决的。</p>
<table style="border: 0px;">
<tbody>
<tr style="background: none;">
<td style="border: 0px;">
<pre class="EnlighterJSRAW" data-enlighter-language="cpp">//用&lt;class T&gt;来描述泛型
template &lt;class T&gt; 
T GetMax (T a, T b)  { 
    T result; 
    result = (a&gt;b)? a : b; 
    return (result); 
} 
</pre>
</td>
<td style="border: 0px;">
<pre class="EnlighterJSRAW" data-enlighter-language="cpp">int i=5, j=6, k; 
//生成int类型的函数
k=GetMax&lt;int&gt;(i,j);
 
long l=10, m=5, n; 
//生成long类型的函数
n=GetMax&lt;long&gt;(l,m); 
</pre>
</td>
</tr>
</tbody>
</table>
<p>C++的编译器会在编译时分析代码，根据不同的变量类型来自动化的生成相关类型的函数或类。C++叫模板的具体化。</p>
<p>这个技术是编译时的问题，所以，不需要我们在运行时进行任何的运行的类型识别，我们的程序也会变得比较的干净。</p>
<p>那么，我们是否可以在Go中使用C++的这种技术呢？答案是肯定的，只是Go的编译器不帮你干，你需要自己动手。</p>
<h4>Go Generator</h4>
<p>要玩 Go的代码生成，你需要三件事：</p>
<ol>
<li>一个函数模板，其中设置好相应的占位符。</li>
<li>一个脚本，用于按规则来替换文本并生成新的代码。</li>
<li>一行注释代码。</li>
</ol>
<h5>函数模板</h5>
<p>我们把我们之前的示例改成模板。取名为 <code>container.tmp.go</code> 放在 <code>./template/</code>下</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">package PACKAGE_NAME
type GENERIC_NAMEContainer struct {
    s []GENERIC_TYPE
}
func NewGENERIC_NAMEContainer() *GENERIC_NAMEContainer {
    return &amp;GENERIC_NAMEContainer{s: []GENERIC_TYPE{}}
}
func (c *GENERIC_NAMEContainer) Put(val GENERIC_TYPE) {
    c.s = append(c.s, val)
}
func (c *GENERIC_NAMEContainer) Get() GENERIC_TYPE {
    r := c.s[0]
    c.s = c.s[1:]
    return r
}</pre>
<p>我们可以看到函数模板中我们有如下的占位符：</p>
<ul>
<li><code>PACKAGE_NAME</code> &#8211; 包名</li>
<li><code>GENERIC_NAME</code> &#8211; 名字</li>
<li><code>GENERIC_TYPE</code> &#8211; 实际的类型</li>
</ul>
<p>其它的代码都是一样的。</p>
<h5>函数生成脚本</h5>
<p>然后，我们有一个叫<code>gen.sh</code>的生成脚本，如下所示：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-title="gen.sh">#!/bin/bash

set -e

SRC_FILE=${1}
PACKAGE=${2}
TYPE=${3}
DES=${4}
#uppcase the first char
PREFIX="$(tr '[:lower:]' '[:upper:]' &lt;&lt;&lt; ${TYPE:0:1})${TYPE:1}"

DES_FILE=$(echo ${TYPE}| tr '[:upper:]' '[:lower:]')_${DES}.go

sed 's/PACKAGE_NAME/'"${PACKAGE}"'/g' ${SRC_FILE} | \
    sed 's/GENERIC_TYPE/'"${TYPE}"'/g' | \
    sed 's/GENERIC_NAME/'"${PREFIX}"'/g' &gt; ${DES_FILE}</pre>
<p>其需要4个参数：</p>
<ul>
<li>模板源文件</li>
<li>包名</li>
<li>实际需要具体化的类型</li>
<li>用于构造目标文件名的后缀</li>
</ul>
<p>然后其会用 <code>sed</code> 命令去替换我们的上面的函数模板，并生成到目标文件中。（关于sed命令请参看本站的《<a title="sed 简明教程" href="https://coolshell.cn/articles/9104.html" target="_blank" rel="noopener">sed 简明教程</a>》）</p>
<h5>生成代码</h5>
<p>接下来，我们只需要在代码中打一个特殊的注释：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang" data-enlighter-highlight="1,10">//go:generate ./gen.sh ./template/container.tmp.go gen uint32 container
func generateUint32Example() {
    var u uint32 = 42
    c := NewUint32Container()
    c.Put(u)
    v := c.Get()
    fmt.Printf("generateExample: %d (%T)\n", v, v)
}

//go:generate ./gen.sh ./template/container.tmp.go gen string container
func generateStringExample() {
    var s string = "Hello"
    c := NewStringContainer()
    c.Put(s)
    v := c.Get()
    fmt.Printf("generateExample: %s (%T)\n", v, v)
}</pre>
<p>其中，</p>
<ul>
<li>第一个注释是生成包名为 <code>gen</code> 类型为 <code>uint32</code> 目标文件名以 <code>container</code> 为后缀</li>
<li>第二个注释是生成包名为 <code>gen</code> 类型为 <code>string</code> 目标文件名以 <code>container</code> 为后缀</li>
</ul>
<p>然后，在工程目录中直接执行 <code> go generate</code> 命令，就会生成如下两份代码，</p>
<p>一份文件名为<code>uint32_container.go</code></p>
<pre class="EnlighterJSRAW" data-enlighter-language="cpp" data-enlighter-title="uint32_container.go">package gen

type Uint32Container struct {
    s []uint32
}
func NewUint32Container() *Uint32Container {
    return &amp;Uint32Container{s: []uint32{}}
}
func (c *Uint32Container) Put(val uint32) {
    c.s = append(c.s, val)
}
func (c *Uint32Container) Get() uint32 {
    r := c.s[0]
    c.s = c.s[1:]
    return r
}</pre>
<p>另一份的文件名为 <code>string_container.go</code></p>
<pre class="EnlighterJSRAW" data-enlighter-language="cpp" data-enlighter-title="string_container.go">package gen

type StringContainer struct {
    s []string
}
func NewStringContainer() *StringContainer {
    return &amp;StringContainer{s: []string{}}
}
func (c *StringContainer) Put(val string) {
    c.s = append(c.s, val)
}
func (c *StringContainer) Get() string {
    r := c.s[0]
    c.s = c.s[1:]
    return r
}
</pre>
<p>这两份代码可以让我们的代码完全编译通过，所付出的代价就是需要多执行一步 <code>go generate</code> 命令。</p>
<h4>新版Filter</h4>
<p>现在我们再回头看看我们之前《<a href="https://coolshell.cn/articles/21164.html" target="_blank" rel="noopener">Go编程模式：Map-Reduce</a>》中的那些个用反射整出来的例子，有了这样的技术，我就不必在代码里用那些晦涩难懂的反射来做运行时的类型检查了。我们可以写下很干净的代码，让编译器在编译时检查类型对不对。下面是一个Fitler的模板文件 <code>filter.tmp.go</code>：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">package PACKAGE_NAME

type GENERIC_NAMEList []GENERIC_TYPE

type GENERIC_NAMEToBool func(*GENERIC_TYPE) bool

func (al GENERIC_NAMEList) Filter(f GENERIC_NAMEToBool) GENERIC_NAMEList {
    var ret GENERIC_NAMEList
    for _, a := range al {
        if f(&amp;a) {
            ret = append(ret, a)
        }
    }
    return ret
}
</pre>
<p>于是我们可在需要使用这个的地方，加上相关的 go generate 的注释</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-highlight="8">type Employee struct {
  Name     string
  Age      int
  Vacation int
  Salary   int
}

//go:generate ./gen.sh ./template/filter.tmp.go gen Employee filter
func filterEmployeeExample() {

  var list = EmployeeList{
    {"Hao", 44, 0, 8000},
    {"Bob", 34, 10, 5000},
    {"Alice", 23, 5, 9000},
    {"Jack", 26, 0, 4000},
    {"Tom", 48, 9, 7500},
  }

  var filter EmployeeList
  filter = list.Filter(func(e *Employee) bool {
    return e.Age &gt; 40
  })

  fmt.Println("----- Employee.Age &gt; 40 ------")
  for _, e := range filter {
    fmt.Println(e)
  }

  filter = list.Filter(func(e *Employee) bool {
    return e.Salary &lt;= 5000
  })

  fmt.Println("----- Employee.Salary &lt;= 5000 ------")
  for _, e := range filter {
    fmt.Println(e)
  }
}</pre>
<h4>第三方工具</h4>
<p>我们并不需要自己手写 <code>gen.sh</code> 这样的工具类，已经有很多第三方的已经写好的可以使用。下面是一个列表：</p>
<ul>
<li>Genny &#8211;  <a href="https://github.com/cheekybits/genny">https://github.com/cheekybits/genny</a></li>
<li>Generic &#8211; <a href="https://github.com/taylorchu/generic">https://github.com/taylorchu/generic</a></li>
<li>GenGen &#8211; <a href="https://github.com/joeshaw/gengen">https://github.com/joeshaw/gengen</a></li>
<li>Gen &#8211; <a href="https://github.com/clipperhouse/gen">https://github.com/clipperhouse/gen</a></li>
</ul>
<p>（全文完）<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/21615.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2021/09/go-generics-150x150.png" alt="Go编程模式 ： 泛型编程" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21615.html" class="wp_rp_title">Go编程模式 ： 泛型编程</a></li><li ><a href="https://coolshell.cn/articles/21263.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.k8s-150x150.png" alt="Go 编程模式：k8s Visitor 模式" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21263.html" class="wp_rp_title">Go 编程模式：k8s Visitor 模式</a></li><li ><a href="https://coolshell.cn/articles/21228.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.line_.-150x150.png" alt="Go编程模式：Pipeline" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21228.html" class="wp_rp_title">Go编程模式：Pipeline</a></li><li ><a href="https://coolshell.cn/articles/21214.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.pair_-150x150.png" alt="Go编程模式：委托和反转控制" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21214.html" class="wp_rp_title">Go编程模式：委托和反转控制</a></li><li ><a href="https://coolshell.cn/articles/21164.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.map_.reduce-150x150.png" alt="Go编程模式：Map-Reduce" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21164.html" class="wp_rp_title">Go编程模式：Map-Reduce</a></li><li ><a href="https://coolshell.cn/articles/21146.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.options-150x150.png" alt="Go 编程模式：Functional Options" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21146.html" class="wp_rp_title">Go 编程模式：Functional Options</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/21179.html">Go 编程模式：Go Generation</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/21179.html/feed</wfw:commentRss>
			<slash:comments>18</slash:comments>
		
		
			</item>
		<item>
		<title>Go编程模式：Map-Reduce</title>
		<link>https://coolshell.cn/articles/21164.html</link>
					<comments>https://coolshell.cn/articles/21164.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Thu, 24 Dec 2020 07:13:52 +0000</pubDate>
				<category><![CDATA[Go 语言]]></category>
		<category><![CDATA[程序设计]]></category>
		<category><![CDATA[编程语言]]></category>
		<category><![CDATA[functional]]></category>
		<category><![CDATA[functional-programming]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[golang]]></category>
		<category><![CDATA[MapReduce]]></category>
		<category><![CDATA[函数式]]></category>
		<category><![CDATA[函数式编程]]></category>
		<guid isPermaLink="false">https://coolshell.cn/?p=21164</guid>

					<description><![CDATA[<p>在本篇文章中，我们学习一下函数式编程的中非常重要的Map、Reduce、Filter的三种操作，这三种操作可以让我们非常方便灵活地进行一些数据处理——我们的程序...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/21164.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/21164.html">Go编程模式：Map-Reduce</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><img decoding="async" loading="lazy" class="alignright size-medium wp-image-21251" src="https://coolshell.cn/wp-content/uploads/2020/12/go.map_.reduce-300x192.png" alt="" width="300" height="192" srcset="https://coolshell.cn/wp-content/uploads/2020/12/go.map_.reduce-300x192.png 300w, https://coolshell.cn/wp-content/uploads/2020/12/go.map_.reduce-768x491.png 768w, https://coolshell.cn/wp-content/uploads/2020/12/go.map_.reduce-422x270.png 422w, https://coolshell.cn/wp-content/uploads/2020/12/go.map_.reduce.png 992w" sizes="(max-width: 300px) 100vw, 300px" />在本篇文章中，我们学习一下函数式编程的中非常重要的Map、Reduce、Filter的三种操作，这三种操作可以让我们非常方便灵活地进行一些数据处理——我们的程序中大多数情况下都是在到倒腾数据，尤其对于一些需要统计的业务场景，Map/Reduce/Filter是非常通用的玩法。下面先来看几个例子：</p>
<section class="post-series"><h3 class="post-series-title">本文是全系列中第5 / 10篇：<a href="https://coolshell.cn/articles/series/go%e7%bc%96%e7%a8%8b%e6%a8%a1%e5%bc%8f">Go编程模式</a></h3><ul class="post-series-list"><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21128.html">Go编程模式：切片，接口，时间和性能</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21140.html">Go 编程模式：错误处理</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21146.html">Go 编程模式：Functional Options</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21214.html">Go编程模式：委托和反转控制</a></span></li><li class="post-series-item-current post-series-item"><span class="post-series-item-title">Go编程模式：Map-Reduce</span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21179.html">Go 编程模式：Go Generation</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/17929.html">Go编程模式：修饰器</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21228.html">Go编程模式：Pipeline</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21263.html">Go 编程模式：k8s Visitor 模式</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21615.html">Go编程模式 ： 泛型编程</a></span></li></ul><nav class="post-series-nav"><span class="post-series-nav-prev">&laquo; <a href="https://coolshell.cn/articles/21214.html" rel="prev" title="Go编程模式：委托和反转控制">上一篇文章</a></span><span class="post-series-nav-next"><a href="https://coolshell.cn/articles/21179.html" rel="next" title="Go 编程模式：Go Generation">下一篇文章</a> &raquo;</span></nav></section>
<h4>基本示例</h4>
<h5>Map示例</h5>
<p>下面的程序代码中，我们写了两个Map函数，这两个函数需要两个参数，</p>
<ul>
<li>一个是字符串数组 <code>[]string</code>，说明需要处理的数据一个字符串</li>
<li>另一个是一个函数<code>func(s string) string</code> 或 <code>func(s string) int</code></li>
</ul>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func MapStrToStr(arr []string, fn func(s string) string) []string {
    var newArray = []string{}
    for _, it := range arr {
        newArray = append(newArray, fn(it))
    }
    return newArray
}

func MapStrToInt(arr []string, fn func(s string) int) []int {
    var newArray = []int{}
    for _, it := range arr {
        newArray = append(newArray, fn(it))
    }
    return newArray
}</pre>
<p>整个Map函数运行逻辑都很相似，函数体都是在遍历第一个参数的数组，然后，调用第二个参数的函数，然后把其值组合成另一个数组返回。</p>
<p><span id="more-21164"></span></p>
<p>于是我们就可以这样使用这两个函数：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">var list = []string{"Hao", "Chen", "MegaEase"}

x := MapStrToStr(list, func(s string) string {
    return strings.ToUpper(s)
})
fmt.Printf("%v\n", x)
//["HAO", "CHEN", "MEGAEASE"]

y := MapStrToInt(list, func(s string) int {
    return len(s)
})
fmt.Printf("%v\n", y)
//[3, 4, 8]</pre>
<p>我们可以看到，我们给第一个 <code>MapStrToStr()</code> 传了函数做的是 转大写，于是出来的数组就成了全大写的，给<code>MapStrToInt()</code> 传的是算其长度，所以出来的数组是每个字符串的长度。</p>
<p>我们再来看一下Reduce和Filter的函数是什么样的。</p>
<h5><strong>Reduce 示例</strong></h5>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func Reduce(arr []string, fn func(s string) int) int {
    sum := 0
    for _, it := range arr {
        sum += fn(it)
    }
    return sum
}

var list = []string{"Hao", "Chen", "MegaEase"}

x := Reduce(list, func(s string) int {
    return len(s)
})
fmt.Printf("%v\n", x)
// 15
</pre>
<h5><strong>Filter示例</strong></h5>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func Filter(arr []int, fn func(n int) bool) []int {
    var newArray = []int{}
    for _, it := range arr {
        if fn(it) {
            newArray = append(newArray, it)
        }
    }
    return newArray
}

var intset = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
out := Filter(intset, func(n int) bool {
   return n%2 == 1
})
fmt.Printf("%v\n", out)

out = Filter(intset, func(n int) bool {
    return n &gt; 5
})
fmt.Printf("%v\n", out)
</pre>
<p>下图是一个比喻，其非常形象地说明了Map-Reduce是的业务语义，其在数据处理中非常有用。</p>
<p><img decoding="async" loading="lazy" class="aligncenter size-full wp-image-21169" src="https://coolshell.cn/wp-content/uploads/2020/12/map-reduce.png" alt="" width="794" height="442" srcset="https://coolshell.cn/wp-content/uploads/2020/12/map-reduce.png 794w, https://coolshell.cn/wp-content/uploads/2020/12/map-reduce-300x167.png 300w, https://coolshell.cn/wp-content/uploads/2020/12/map-reduce-768x428.png 768w, https://coolshell.cn/wp-content/uploads/2020/12/map-reduce-360x200.png 360w, https://coolshell.cn/wp-content/uploads/2020/12/map-reduce-485x270.png 485w" sizes="(max-width: 794px) 100vw, 794px" /></p>
<h4>业务示例</h4>
<p>通过上面的一些示例，你可能有一些明白，Map/Reduce/Filter只是一种控制逻辑，真正的业务逻辑是在传给他们的数据和那个函数来定义的。是的，这是一个很经典的“业务逻辑”和“控制逻辑”分离解耦的编程模式。下面我们来看一个有业务意义的代码，来让大家强化理解一下什么叫“控制逻辑”与业务逻辑分离。</p>
<h5>员工信息</h5>
<p>首先，我们一个员工对象，以及一些数据</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">type Employee struct {
    Name     string
    Age      int
    Vacation int
    Salary   int
}

var list = []Employee{
    {"Hao", 44, 0, 8000},
    {"Bob", 34, 10, 5000},
    {"Alice", 23, 5, 9000},
    {"Jack", 26, 0, 4000},
    {"Tom", 48, 9, 7500},
    {"Marry", 29, 0, 6000},
    {"Mike", 32, 8, 4000},
}</pre>
<h5>相关的Reduce/Fitler函数</h5>
<p>然后，我们有如下的几个函数：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">func EmployeeCountIf(list []Employee, fn func(e *Employee) bool) int {
    count := 0
    for i, _ := range list {
        if fn(&amp;list[i]) {
            count += 1
        }
    }
    return count
}

func EmployeeFilterIn(list []Employee, fn func(e *Employee) bool) []Employee {
    var newList []Employee
    for i, _ := range list {
        if fn(&amp;list[i]) {
            newList = append(newList, list[i])
        }
    }
    return newList
}

func EmployeeSumIf(list []Employee, fn func(e *Employee) int) int {
    var sum = 0
    for i, _ := range list {
        sum += fn(&amp;list[i])
    }
    return sum
}</pre>
<p>简单说明一下：</p>
<ul>
<li><code>EmployeeConutIf</code> 和 <code>EmployeeSumIf</code> 分别用于统满足某个条件的个数或总数。它们都是Filter + Reduce的语义。</li>
<li><code>EmployeeFilterIn</code> 就是按某种条件过虑。就是Fitler的语义。</li>
</ul>
<h5>各种自定义的统计示例</h5>
<p>于是我们就可以有如下的代码。</p>
<p><strong>1）统计有多少员工大于40岁</strong></p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">old := EmployeeCountIf(list, func(e *Employee) bool {
    return e.Age &gt; 40
})
fmt.Printf("old people: %d\n", old)
//old people: 2
</pre>
<p><strong>2）统计有多少员工薪水大于6000</strong></p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">high_pay := EmployeeCountIf(list, func(e *Employee) bool {
    return e.Salary &gt;= 6000
})
fmt.Printf("High Salary people: %d\n", high_pay)
//High Salary people: 4
</pre>
<p><strong>3）列出有没有休假的员工</strong></p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">no_vacation := EmployeeFilterIn(list, func(e *Employee) bool {
    return e.Vacation == 0
})
fmt.Printf("People no vacation: %v\n", no_vacation)
//People no vacation: [{Hao 44 0 8000} {Jack 26 0 4000} {Marry 29 0 6000}]
</pre>
<p><strong>4）统计所有员工的薪资总和</strong></p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">total_pay := EmployeeSumIf(list, func(e *Employee) int {
    return e.Salary
})

fmt.Printf("Total Salary: %d\n", total_pay)
//Total Salary: 43500
</pre>
<p><strong>5）统计30岁以下员工的薪资总和</strong></p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">younger_pay := EmployeeSumIf(list, func(e *Employee) int {
    if e.Age &lt; 30 {
        return e.Salary
    } 
    return 0
})</pre>
<h4>泛型Map-Reduce</h4>
<p>我们可以看到，上面的Map-Reduce都因为要处理数据的类型不同而需要写出不同版本的Map-Reduce，虽然他们的代码看上去是很类似的。所以，这里就要带出来泛型编程了，Go语言在本文写作的时候还不支持泛型（注：Go开发团队技术负责人Russ Cox在2012年11月21golang-dev上的mail确认了Go泛型(type parameter)将在Go 1.18版本落地，即2022.2月份）。</p>
<h5>简单版 Generic Map</h5>
<p>所以，目前的Go语言的泛型只能用 <code>interface{}</code> + <code>reflect</code>来完成，<code>interface{}</code> 可以理解为C中的 <code>void*</code>，Java中的 <code>Object</code> ，<code>reflect</code>是Go的反射机制包，用于在运行时检查类型。</p>
<p>下面我们来看一下一个非常简单不作任何类型检查的泛型的Map函数怎么写。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">func Map(data interface{}, fn interface{}) []interface{} {
    vfn := reflect.ValueOf(fn)
    vdata := reflect.ValueOf(data)
    result := make([]interface{}, vdata.Len())

    for i := 0; i &lt; vdata.Len(); i++ {
        result[i] = vfn.Call([]reflect.Value{vdata.Index(i)})[0].Interface()
    }
    return result
}</pre>
<p>上面的代码中，</p>
<ul>
<li>通过 <code>reflect.ValueOf()</code> 来获得 <code>interface{}</code> 的值，其中一个是数据 <code>vdata</code>，另一个是函数 <code>vfn</code>，</li>
<li>然后通过 <code>vfn.Call()</code> 方法来调用函数，通过 <code>[]refelct.Value{vdata.Index(i)}</code>来获得数据。</li>
</ul>
<p>Go语言中的反射的语法还是有点令人费解的，但是简单看一下手册还是能够读懂的。我这篇文章不讲反射，所以相关的基础知识还请大家自行Google相关的教程。</p>
<p>于是，我们就可以有下面的代码——不同类型的数据可以使用相同逻辑的<code>Map()</code>代码。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">square := func(x int) int {
  return x * x
}
nums := []int{1, 2, 3, 4}

squared_arr := Map(nums,square)
fmt.Println(squared_arr)
//[1 4 9 16]



upcase := func(s string) string {
  return strings.ToUpper(s)
}
strs := []string{"Hao", "Chen", "MegaEase"}
upstrs := Map(strs, upcase);
fmt.Println(upstrs)
//[HAO CHEN MEGAEASE]</pre>
<p>但是因为反射是运行时的事，所以，如果类型什么出问题的话，就会有运行时的错误。比如：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">x := Map(5, 5)
fmt.Println(x)</pre>
<p>上面的代码可以很轻松的编译通过，但是在运行时就出问题了，还是panic错误……</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">panic: reflect: call of reflect.Value.Len on int Value

goroutine 1 [running]:
reflect.Value.Len(0x10b5240, 0x10eeb58, 0x82, 0x10716bc)
        /usr/local/Cellar/go/1.15.3/libexec/src/reflect/value.go:1162 +0x185
main.Map(0x10b5240, 0x10eeb58, 0x10b5240, 0x10eeb60, 0x1, 0x14, 0x0)
        /Users/chenhao/.../map.go:12 +0x16b
main.main()
        /Users/chenhao/.../map.go:42 +0x465
exit status 2</pre>
<h5>健壮版的Generic Map</h5>
<p>所以，如果要写一个健壮的程序，对于这种用<code>interface{}</code> 的“过度泛型”，就需要我们自己来做类型检查。下面是一个有类型检查的Map代码：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func Transform(slice, function interface{}) interface{} {
  return transform(slice, function, false)
}

func TransformInPlace(slice, function interface{}) interface{} {
  return transform(slice, function, true)
}

func transform(slice, function interface{}, inPlace bool) interface{} {
 
  //check the <code data-enlighter-language="raw" class="EnlighterJSRAW">slice</code> type is Slice
  sliceInType := reflect.ValueOf(slice)
  if sliceInType.Kind() != reflect.Slice {
    panic("transform: not slice")
  }

  //check the function signature
  fn := reflect.ValueOf(function)
  elemType := sliceInType.Type().Elem()
  if !verifyFuncSignature(fn, elemType, nil) {
    panic("trasform: function must be of type func(" + sliceInType.Type().Elem().String() + ") outputElemType")
  }

  sliceOutType := sliceInType
  if !inPlace {
    sliceOutType = reflect.MakeSlice(reflect.SliceOf(fn.Type().Out(0)), sliceInType.Len(), sliceInType.Len())
  }
  for i := 0; i &lt; sliceInType.Len(); i++ {
    sliceOutType.Index(i).Set(fn.Call([]reflect.Value{sliceInType.Index(i)})[0])
  }
  return sliceOutType.Interface()

}

func verifyFuncSignature(fn reflect.Value, types ...reflect.Type) bool {

  //Check it is a funciton
  if fn.Kind() != reflect.Func {
    return false
  }
  // NumIn() - returns a function type's input parameter count.
  // NumOut() - returns a function type's output parameter count.
  if (fn.Type().NumIn() != len(types)-1) || (fn.Type().NumOut() != 1) {
    return false
  }
  // In() - returns the type of a function type's i'th input parameter.
  for i := 0; i &lt; len(types)-1; i++ {
    if fn.Type().In(i) != types[i] {
      return false
    }
  }
  // Out() - returns the type of a function type's i'th output parameter.
  outType := types[len(types)-1]
  if outType != nil &amp;&amp; fn.Type().Out(0) != outType {
    return false
  }
  return true
}
</pre>
<p>上面的代码一下子就复杂起来了，可见，复杂的代码都是在处理异常的地方。我不打算Walk through 所有的代码，别看代码多，但是还是可以读懂的，下面列几个代码中的要点：</p>
<ul>
<li>代码中没有使用Map函数，因为和数据结构和关键有含义冲突的问题，所以使用<code>Transform</code>，这个来源于 C++ STL库中的命名。</li>
<li>有两个版本的函数，一个是返回一个全新的数组 &#8211; <code>Transform()</code>，一个是“就地完成” &#8211; <code>TransformInPlace()</code></li>
<li>在主函数中，用 <code>Kind()</code> 方法检查了数据类型是不是 Slice，函数类型是不是Func</li>
<li>检查函数的参数和返回类型是通过 <code>verifyFuncSignature()</code> 来完成的，其中：
<ul>
<li><code>NumIn()</code> &#8211; 用来检查函数的“入参”</li>
<li> <code>NumOut()</code> 用来检查函数的“返回值”</li>
</ul>
</li>
<li>如果需要新生成一个Slice，会使用 <code>reflect.MakeSlice()</code> 来完成。</li>
</ul>
<p>好了，有了上面的这段代码，我们的代码就很可以很开心的使用了：</p>
<p>可以用于字符串数组</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">list := []string{"1", "2", "3", "4", "5", "6"}
result := Transform(list, func(a string) string{
    return a +a +a
})
//{"111","222","333","444","555","666"}
</pre>
<p>可以用于整形数组</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">list := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
TransformInPlace(list, func (a int) int {
  return a*3
})
//{3, 6, 9, 12, 15, 18, 21, 24, 27}
</pre>
<p>可以用于结构体</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">var list = []Employee{
    {"Hao", 44, 0, 8000},
    {"Bob", 34, 10, 5000},
    {"Alice", 23, 5, 9000},
    {"Jack", 26, 0, 4000},
    {"Tom", 48, 9, 7500},
}

result := TransformInPlace(list, func(e Employee) Employee {
    e.Salary += 1000
    e.Age += 1
    return e
})</pre>
<h5>健壮版的 Generic Reduce</h5>
<p>同样，泛型版的 Reduce 代码如下：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func Reduce(slice, pairFunc, zero interface{}) interface{} {
  sliceInType := reflect.ValueOf(slice)
  if sliceInType.Kind() != reflect.Slice {
    panic("reduce: wrong type, not slice")
  }

  len := sliceInType.Len()
  if len == 0 {
    return zero
  } else if len == 1 {
    return sliceInType.Index(0)
  }

  elemType := sliceInType.Type().Elem()
  fn := reflect.ValueOf(pairFunc)
  if !verifyFuncSignature(fn, elemType, elemType, elemType) {
    t := elemType.String()
    panic("reduce: function must be of type func(" + t + ", " + t + ") " + t)
  }

  var ins [2]reflect.Value
  ins[0] = sliceInType.Index(0)
  ins[1] = sliceInType.Index(1)
  out := fn.Call(ins[:])[0]

  for i := 2; i &lt; len; i++ {
    ins[0] = out
    ins[1] = sliceInType.Index(i)
    out = fn.Call(ins[:])[0]
  }
  return out.Interface()
}</pre>
<h5>健壮版的 Generic Filter</h5>
<p>同样，泛型版的 Filter 代码如下（同样分是否“就地计算”的两个版本）：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">func Filter(slice, function interface{}) interface{} {
  result, _ := filter(slice, function, false)
  return result
}

func FilterInPlace(slicePtr, function interface{}) {
  in := reflect.ValueOf(slicePtr)
  if in.Kind() != reflect.Ptr {
    panic("FilterInPlace: wrong type, " +
      "not a pointer to slice")
  }
  _, n := filter(in.Elem().Interface(), function, true)
  in.Elem().SetLen(n)
}

var boolType = reflect.ValueOf(true).Type()

func filter(slice, function interface{}, inPlace bool) (interface{}, int) {

  sliceInType := reflect.ValueOf(slice)
  if sliceInType.Kind() != reflect.Slice {
    panic("filter: wrong type, not a slice")
  }

  fn := reflect.ValueOf(function)
  elemType := sliceInType.Type().Elem()
  if !verifyFuncSignature(fn, elemType, boolType) {
    panic("filter: function must be of type func(" + elemType.String() + ") bool")
  }

  var which []int
  for i := 0; i &lt; sliceInType.Len(); i++ {
    if fn.Call([]reflect.Value{sliceInType.Index(i)})[0].Bool() {
      which = append(which, i)
    }
  }

  out := sliceInType

  if !inPlace {
    out = reflect.MakeSlice(sliceInType.Type(), len(which), len(which))
  }
  for i := range which {
    out.Index(i).Set(sliceInType.Index(which[i]))
  }

  return out.Interface(), len(which)
}</pre>
<h4>后记</h4>
<p>还有几个未尽事宜：</p>
<p>1）使用反射来做这些东西，会有一个问题，<strong>那就是代码的性能会很差。所以，上面的代码不能用于你需要高性能的地方</strong>。怎么解决这个问题，我们会在本系列文章的下一篇文章中讨论。</p>
<p>2）上面的代码大量的参考了 Rob Pike的版本，他的代码在 <a href="https://github.com/robpike/filter" target="_blank" rel="noopener">https://github.com/robpike/filter</a></p>
<p>3）其实，在全世界范围内，有大量的程序员都在问Go语言官方什么时候在标准库中支持 Map/Reduce，Rob Pike说，这种东西难写吗？还要我们官方来帮你们写么？这种代码我多少年前就写过了，但是，我从来一次都没有用过，我还是喜欢用“For循环”，我觉得你最好也跟我一起用 “For循环”。</p>
<p>我个人觉得，Map/Reduce在数据处理的时候还是很有用的，Rob Pike可能平时也不怎么写“业务逻辑”的代码，所以，对他来说可能也不太了解业务的变化有多么的频繁……</p>
<p>当然，好还是不好，由你来判断，但多学一些编程模式是对自己的帮助也是很有帮助的。</p>
<p>（全文完）<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/21146.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.options-150x150.png" alt="Go 编程模式：Functional Options" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21146.html" class="wp_rp_title">Go 编程模式：Functional Options</a></li><li ><a href="https://coolshell.cn/articles/17929.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2017/06/go-hardhat-150x150.png" alt="Go编程模式：修饰器" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17929.html" class="wp_rp_title">Go编程模式：修饰器</a></li><li ><a href="https://coolshell.cn/articles/21615.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2021/09/go-generics-150x150.png" alt="Go编程模式 ： 泛型编程" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21615.html" class="wp_rp_title">Go编程模式 ： 泛型编程</a></li><li ><a href="https://coolshell.cn/articles/21263.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.k8s-150x150.png" alt="Go 编程模式：k8s Visitor 模式" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21263.html" class="wp_rp_title">Go 编程模式：k8s Visitor 模式</a></li><li ><a href="https://coolshell.cn/articles/21228.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.line_.-150x150.png" alt="Go编程模式：Pipeline" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21228.html" class="wp_rp_title">Go编程模式：Pipeline</a></li><li ><a href="https://coolshell.cn/articles/21214.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.pair_-150x150.png" alt="Go编程模式：委托和反转控制" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21214.html" class="wp_rp_title">Go编程模式：委托和反转控制</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/21164.html">Go编程模式：Map-Reduce</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/21164.html/feed</wfw:commentRss>
			<slash:comments>19</slash:comments>
		
		
			</item>
		<item>
		<title>Go 编程模式：Functional Options</title>
		<link>https://coolshell.cn/articles/21146.html</link>
					<comments>https://coolshell.cn/articles/21146.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Tue, 22 Dec 2020 15:23:52 +0000</pubDate>
				<category><![CDATA[Go 语言]]></category>
		<category><![CDATA[程序设计]]></category>
		<category><![CDATA[编程语言]]></category>
		<category><![CDATA[functional]]></category>
		<category><![CDATA[functional-programming]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[golang]]></category>
		<category><![CDATA[Pattern]]></category>
		<category><![CDATA[函数式编程]]></category>
		<guid isPermaLink="false">https://coolshell.cn/?p=21146</guid>

					<description><![CDATA[<p>在本篇文章中，我们来讨论一下Functional Options这个编程模式。这是一个函数式编程的应用案例，编程技巧也很好，是目前在Go语言中最流行的一种编程模...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/21146.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/21146.html">Go 编程模式：Functional Options</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><img decoding="async" loading="lazy" class="alignright size-medium wp-image-21241" src="https://coolshell.cn/wp-content/uploads/2020/12/go.options-300x186.png" alt="" width="300" height="186" srcset="https://coolshell.cn/wp-content/uploads/2020/12/go.options-300x186.png 300w, https://coolshell.cn/wp-content/uploads/2020/12/go.options-768x476.png 768w, https://coolshell.cn/wp-content/uploads/2020/12/go.options-436x270.png 436w, https://coolshell.cn/wp-content/uploads/2020/12/go.options.png 1015w" sizes="(max-width: 300px) 100vw, 300px" />在本篇文章中，我们来讨论一下Functional Options这个编程模式。这是一个函数式编程的应用案例，编程技巧也很好，是目前在Go语言中最流行的一种编程模式。但是，在我们正式讨论这个模式之前，我们需要先来看看要解决什么样的问题。</p>
<section class="post-series"><h3 class="post-series-title">本文是全系列中第3 / 10篇：<a href="https://coolshell.cn/articles/series/go%e7%bc%96%e7%a8%8b%e6%a8%a1%e5%bc%8f">Go编程模式</a></h3><ul class="post-series-list"><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21128.html">Go编程模式：切片，接口，时间和性能</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21140.html">Go 编程模式：错误处理</a></span></li><li class="post-series-item-current post-series-item"><span class="post-series-item-title">Go 编程模式：Functional Options</span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21214.html">Go编程模式：委托和反转控制</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21164.html">Go编程模式：Map-Reduce</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21179.html">Go 编程模式：Go Generation</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/17929.html">Go编程模式：修饰器</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21228.html">Go编程模式：Pipeline</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21263.html">Go 编程模式：k8s Visitor 模式</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21615.html">Go编程模式 ： 泛型编程</a></span></li></ul><nav class="post-series-nav"><span class="post-series-nav-prev">&laquo; <a href="https://coolshell.cn/articles/21140.html" rel="prev" title="Go 编程模式：错误处理">上一篇文章</a></span><span class="post-series-nav-next"><a href="https://coolshell.cn/articles/21214.html" rel="next" title="Go编程模式：委托和反转控制">下一篇文章</a> &raquo;</span></nav></section>
<h4>配置选项问题</h4>
<p>在我们编程中，我们会经常性的需要对一个对象（或是业务实体）进行相关的配置。比如下面这个业务实体（注意，这仅只是一个示例）：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">type Server struct {
    Addr     string
    Port     int
    Protocol string
    Timeout  time.Duration
    MaxConns int
    TLS      *tls.Config
}</pre>
<p>在这个 <code>Server</code> 对象中，我们可以看到：</p>
<p><span id="more-21146"></span></p>
<ul>
<li>要有侦听的IP地址 <code>Addr</code> 和端口号 <code>Port</code> ，这两个配置选项是必填的（当然，IP地址和端口号都可以有默认值，当这里我们用于举例认为是没有默认值，而且不能为空，需要必填的）。</li>
<li>然后，还有协议 <code>Protocol</code> 、 <code>Timeout</code> 和<code>MaxConns</code> 字段，这几个字段是不能为空的，但是有默认值的，比如：协议是<code>tcp</code>, 超时<code>30</code>秒 和 最大链接数<code>1024</code>个。</li>
<li>还有一个 <code>TLS</code> 这个是安全链接，需要配置相关的证书和私钥。这个是可以为空的。</li>
</ul>
<p>所以，针对于上述这样的配置，我们需要有多种不同的创建不同配置 <code>Server</code> 的函数签名，如下所示（代码比较宽，需要左右滚动浏览）：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func NewDefaultServer(addr string, port int) (*Server, error) {
  return &amp;Server{addr, port, "tcp", 30 * time.Second, 100, nil}, nil
}

func NewTLSServer(addr string, port int, tls *tls.Config) (*Server, error) {
  return &amp;Server{addr, port, "tcp", 30 * time.Second, 100, tls}, nil
}

func NewServerWithTimeout(addr string, port int, timeout time.Duration) (*Server, error) {
  return &amp;Server{addr, port, "tcp", timeout, 100, nil}, nil
}

func NewTLSServerWithMaxConnAndTimeout(addr string, port int, maxconns int, timeout time.Duration, tls *tls.Config) (*Server, error) {
  return &amp;Server{addr, port, "tcp", 30 * time.Second, maxconns, tls}, nil
}</pre>
<p>因为Go语言不支持重载函数，所以，你得用不同的函数名来应对不同的配置选项。</p>
<h4>配置对象方案</h4>
<p>要解决这个问题，最常见的方式是使用一个配置对象，如下所示：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">type Config struct {
    Protocol string
    Timeout  time.Duration
    Maxconns int
    TLS      *tls.Config
}</pre>
<p>我们把那些非必输的选项都移到一个结构体里，于是 <code>Server</code> 对象变成了：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">type Server struct {
    Addr string
    Port int
    Conf *Config
}</pre>
<p>于是，我们只需要一个 <code>NewServer()</code> 的函数了，在使用前需要构造 <code>Config</code> 对象。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func NewServer(addr string, port int, conf *Config) (*Server, error) {
    //...
}

//Using the default configuratrion
srv1, _ := NewServer("localhost", 9000, nil) 

conf := ServerConfig{Protocol:"tcp", Timeout: 60*time.Duration}
srv2, _ := NewServer("locahost", 9000, &amp;conf)</pre>
<p>这段代码算是不错了，大多数情况下，我们可能就止步于此了。但是，对于有洁癖的有追求的程序员来说，他们能看到其中有一点不好的是，<code>Config</code> 并不是必需的，所以，你需要判断是否是 <code>nil</code> 或是 Empty &#8211; <code> Config{}</code>这让我们的代码感觉还是有点不是很干净。</p>
<h4>Builder模式</h4>
<p>如果你是一个Java程序员，熟悉设计模式的一定会很自然地使用上Builder模式。比如如下的代码：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="java">User user = new User.Builder()
  .name("Hao Chen")
  .email("haoel@hotmail.com")
  .nickname("左耳朵")
  .build();</pre>
<p>仿照上面这个模式，我们可以把上面代码改写成如下的代码（注：下面的代码没有考虑出错处理，其中关于出错处理的更多内容，请参看《<a title="GO 编程模式：错误处理" href="https://coolshell.cn/articles/21140.html" target="_blank" rel="noopener">Go 编程模式：出错处理</a>》）：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">//使用一个builder类来做包装
type ServerBuilder struct {
  Server
}

func (sb *ServerBuilder) Create(addr string, port int) *ServerBuilder {
  sb.Server.Addr = addr
  sb.Server.Port = port
  //其它代码设置其它成员的默认值
  return sb
}

func (sb *ServerBuilder) WithProtocol(protocol string) *ServerBuilder {
  sb.Server.Protocol = protocol 
  return sb
}

func (sb *ServerBuilder) WithMaxConn( maxconn int) *ServerBuilder {
  sb.Server.MaxConns = maxconn
  return sb
}

func (sb *ServerBuilder) WithTimeOut( timeout time.Duration) *ServerBuilder {
  sb.Server.Timeout = timeout
  return sb
}

func (sb *ServerBuilder) WithTLS( tls *tls.Config) *ServerBuilder {
  sb.Server.TLS = tls
  return sb
}

func (sb *ServerBuilder) Build() (Server) {
  return  sb.Server
}
</pre>
<p>于是就可以以如下的方式来使用了</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">sb := ServerBuilder{}
server, err := sb.Create("127.0.0.1", 8080).
  WithProtocol("udp").
  WithMaxConn(1024).
  WithTimeOut(30*time.Second).
  Build()</pre>
<p>上面这样的方式也很清楚，不需要额外的Config类，使用链式的函数调用的方式来构造一个对象，只需要多加一个Builder类，这个Builder类似乎有点多余，我们似乎可以直接在<code>Server</code> 上进行这样的 Builder 构造，的确是这样的。但是在处理错误的时候可能就有点麻烦（需要为Server结构增加一个error 成员，破坏了Server结构体的“纯洁”），不如一个包装类更好一些。</p>
<p>如果我们想省掉这个包装的结构体，那么就轮到我们的Functional Options上场了，函数式编程。</p>
<h4>Functional Options</h4>
<p>首先，我们先定义一个函数类型：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">type Option func(*Server)</pre>
<p>然后，我们可以使用函数式的方式定义一组如下的函数：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func Protocol(p string) Option {
    return func(s *Server) {
        s.Protocol = p
    }
}
func Timeout(timeout time.Duration) Option {
    return func(s *Server) {
        s.Timeout = timeout
    }
}
func MaxConns(maxconns int) Option {
    return func(s *Server) {
        s.MaxConns = maxconns
    }
}
func TLS(tls *tls.Config) Option {
    return func(s *Server) {
        s.TLS = tls
    }
}</pre>
<p>上面这组代码传入一个参数，然后返回一个函数，返回的这个函数会设置自己的 <code>Server</code> 参数。例如：</p>
<ul>
<li>当我们调用其中的一个函数用 <code>MaxConns(30)</code> 时</li>
<li>其返回值是一个 <code>func(s* Server) { s.MaxConns = 30 }</code> 的函数。</li>
</ul>
<p>这个叫高阶函数。在数学上，就好像这样的数学定义，计算长方形面积的公式为： <code>rect(width, height) = width * height;</code> 这个函数需要两个参数，我们包装一下，就可以变成计算正方形面积的公式：<code>square(width) = rect(width, width)</code> 也就是说，<code>squre(width)</code>返回了另外一个函数，这个函数就是<code>rect(w,h)</code> 只不过他的两个参数是一样的。即：<code>f(x)  = g(x, x)</code></p>
<p>好了，现在我们再定一个 <code>NewServer()</code>的函数，其中，有一个可变参数 <code>options</code> 其可以传出多个上面上的函数，然后使用一个for-loop来设置我们的 <code>Server</code> 对象。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func NewServer(addr string, port int, options ...func(*Server)) (*Server, error) {

  srv := Server{
    Addr:     addr,
    Port:     port,
    Protocol: "tcp",
    Timeout:  30 * time.Second,
    MaxConns: 1000,
    TLS:      nil,
  }
  for _, option := range options {
    option(&amp;srv)
  }
  //...
  return &amp;srv, nil
}</pre>
<p>于是，我们在创建 <code>Server</code> 对象的时候，我们就可以这样来了。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">s1, _ := NewServer("localhost", 1024)
s2, _ := NewServer("localhost", 2048, Protocol("udp"))
s3, _ := NewServer("0.0.0.0", 8080, Timeout(300*time.Second), MaxConns(1000))</pre>
<p>怎么样，是不是高度的整洁和优雅？不但解决了使用 <code>Config</code> 对象方式 的需要有一个config参数，但在不需要的时候，是放 <code>nil</code> 还是放 <code>Config{}</code>的选择困难，也不需要引用一个Builder的控制对象，直接使用函数式编程的试，在代码阅读上也很优雅。</p>
<p>所以，以后，大家在要玩类似的代码时，强烈推荐使用Functional Options这种方式，这种方式至少带来了如下的好处：</p>
<ul>
<li>直觉式的编程</li>
<li>高度的可配置化</li>
<li>很容易维护和扩展</li>
<li>自文档</li>
<li>对于新来的人很容易上手</li>
<li>没有什么令人困惑的事（是nil 还是空）</li>
</ul>
<h4>参考文档</h4>
<ul>
<li><b>“Self referential functions and design” by Rob Pike<br />
</b><a href="http://commandcenter.blogspot.com.au/2014/01/self-referential-functions-and-design.html">http://commandcenter.blogspot.com.au/2014/01/self-referential-functions-and-design.html</a></li>
</ul>
<p>(全文完)<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/21164.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.map_.reduce-150x150.png" alt="Go编程模式：Map-Reduce" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21164.html" class="wp_rp_title">Go编程模式：Map-Reduce</a></li><li ><a href="https://coolshell.cn/articles/17929.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2017/06/go-hardhat-150x150.png" alt="Go编程模式：修饰器" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17929.html" class="wp_rp_title">Go编程模式：修饰器</a></li><li ><a href="https://coolshell.cn/articles/21615.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2021/09/go-generics-150x150.png" alt="Go编程模式 ： 泛型编程" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21615.html" class="wp_rp_title">Go编程模式 ： 泛型编程</a></li><li ><a href="https://coolshell.cn/articles/21263.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.k8s-150x150.png" alt="Go 编程模式：k8s Visitor 模式" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21263.html" class="wp_rp_title">Go 编程模式：k8s Visitor 模式</a></li><li ><a href="https://coolshell.cn/articles/21228.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.line_.-150x150.png" alt="Go编程模式：Pipeline" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21228.html" class="wp_rp_title">Go编程模式：Pipeline</a></li><li ><a href="https://coolshell.cn/articles/21214.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.pair_-150x150.png" alt="Go编程模式：委托和反转控制" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21214.html" class="wp_rp_title">Go编程模式：委托和反转控制</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/21146.html">Go 编程模式：Functional Options</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/21146.html/feed</wfw:commentRss>
			<slash:comments>15</slash:comments>
		
		
			</item>
		<item>
		<title>Go 编程模式：错误处理</title>
		<link>https://coolshell.cn/articles/21140.html</link>
					<comments>https://coolshell.cn/articles/21140.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Tue, 22 Dec 2020 10:19:30 +0000</pubDate>
				<category><![CDATA[Go 语言]]></category>
		<category><![CDATA[程序设计]]></category>
		<category><![CDATA[编程语言]]></category>
		<category><![CDATA[Error]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[golang]]></category>
		<guid isPermaLink="false">https://coolshell.cn/?p=21140</guid>

					<description><![CDATA[<p>错误处理一直以一是编程必需要面对的问题，错误处理如果做的好的话，代码的稳定性会很好。不同的语言有不同的出现处理的方式。Go语言也一样，在本篇文章中，我们来讨论一...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/21140.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/21140.html">Go 编程模式：错误处理</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><img decoding="async" loading="lazy" class="alignright size-medium wp-image-21143" src="https://coolshell.cn/wp-content/uploads/2020/12/err-check-300x186.jpg" alt="" width="300" height="186" srcset="https://coolshell.cn/wp-content/uploads/2020/12/err-check-300x186.jpg 300w, https://coolshell.cn/wp-content/uploads/2020/12/err-check-768x477.jpg 768w, https://coolshell.cn/wp-content/uploads/2020/12/err-check-435x270.jpg 435w, https://coolshell.cn/wp-content/uploads/2020/12/err-check.jpg 770w" sizes="(max-width: 300px) 100vw, 300px" />错误处理一直以一是编程必需要面对的问题，错误处理如果做的好的话，代码的稳定性会很好。不同的语言有不同的出现处理的方式。Go语言也一样，在本篇文章中，我们来讨论一下Go语言的出错出处，尤其是那令人抓狂的 <code>if err != nil</code> 。</p>
<p>在正式讨论Go代码里满屏的 <code>if err != nil</code> 怎么办这个事之前，我想先说一说编程中的错误处理。这样可以让大家在更高的层面理解编程中的错误处理。</p>
<section class="post-series"><h3 class="post-series-title">本文是全系列中第2 / 10篇：<a href="https://coolshell.cn/articles/series/go%e7%bc%96%e7%a8%8b%e6%a8%a1%e5%bc%8f">Go编程模式</a></h3><ul class="post-series-list"><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21128.html">Go编程模式：切片，接口，时间和性能</a></span></li><li class="post-series-item-current post-series-item"><span class="post-series-item-title">Go 编程模式：错误处理</span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21146.html">Go 编程模式：Functional Options</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21214.html">Go编程模式：委托和反转控制</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21164.html">Go编程模式：Map-Reduce</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21179.html">Go 编程模式：Go Generation</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/17929.html">Go编程模式：修饰器</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21228.html">Go编程模式：Pipeline</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21263.html">Go 编程模式：k8s Visitor 模式</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21615.html">Go编程模式 ： 泛型编程</a></span></li></ul><nav class="post-series-nav"><span class="post-series-nav-prev">&laquo; <a href="https://coolshell.cn/articles/21128.html" rel="prev" title="Go编程模式：切片，接口，时间和性能">上一篇文章</a></span><span class="post-series-nav-next"><a href="https://coolshell.cn/articles/21146.html" rel="next" title="Go 编程模式：Functional Options">下一篇文章</a> &raquo;</span></nav></section>
<h4>C语言的错误检查</h4>
<p>首先，我们知道，处理错误最直接的方式是通过错误码，这也是传统的方式，在过程式语言中通常都是用这样的方式处理错误的。比如 C 语言，基本上来说，其通过函数的返回值标识是否有错，然后通过全局的 <code>errno</code> 变量并配合一个 <code>errstr</code> 的数组来告诉你为什么出错。</p>
<p>为什么是这样的设计？道理很简单，除了可以共用一些错误，更重要的是这其实是一种妥协。比如：<code>read()</code>, <code>write()</code>, <code>open()</code> 这些函数的返回值其实是返回有业务逻辑的值。也就是说，这些函数的返回值有两种语义，一种是成功的值，比如 <code>open()</code> 返回的文件句柄指针 <code>FILE*</code> ，或是错误 <code>NULL</code>。这样会导致调用者并不知道是什么原因出错了，需要去检查 <code>errno</code> 来获得出错的原因，从而可以正确地处理错误。</p>
<p>一般而言，这样的错误处理方式在大多数情况下是没什么问题的。但是也有例外的情况，我们来看一下下面这个 C 语言的函数：</p>
<p><span id="more-21140"></span></p>
<pre class="EnlighterJSRAW" data-enlighter-language="c">int atoi(const char *str)</pre>
<p>这个函数是把一个字符串转成整型。但是问题来了，如果一个要传的字符串是非法的（不是数字的格式），如 &#8220;ABC&#8221; 或者整型溢出了，那么这个函数应该返回什么呢？出错返回，返回什么数都不合理，因为这会和正常的结果混淆在一起。比如，返回 <code>0</code>，那么会和正常的对 “0” 字符的返回值完全混淆在一起。这样就无法判断出错的情况。你可能会说，是不是要检查一下 <code>errno</code>，按道理说应该是要去检查的，但是，我们在 C99 的规格说明书中可以看到这样的描述——</p>
<blockquote><p>7.20.1The functions atof, atoi, atol, and atoll need not affect the value of the integer expression errno on an error. If the value of the result cannot be represented, the behavior is undeﬁned.</p></blockquote>
<p>像<code>atoi()</code>, <code>atof()</code>, <code>atol()</code> 或是 <code>atoll()</code> 这样的函数是不会设置 <code>errno</code>的，而且，还说了，如果结果无法计算的话，行为是undefined。所以，后来，libc 又给出了一个新的函数<code>strtol()</code>，这个函数在出错的时会设置全局变量 <code>errno</code> ：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="c">long val = strtol(in_str, &amp;endptr, 10);  //10的意思是10进制

//如果无法转换
if (endptr == str) {
    fprintf(stderr, "No digits were found\n");
    exit(EXIT_FAILURE);
}

//如果整型溢出了
if ((errno == ERANGE &amp;&amp; (val == LONG_MAX || val == LONG_MIN)) {
    fprintf(stderr, "ERROR: number out of range for LONG\n");
    exit(EXIT_FAILURE);
 }

//如果是其它错误
if (errno != 0 &amp;&amp; val == 0) {
    perror("strtol");
    exit(EXIT_FAILURE);
}
</pre>
<p>虽然，<code>strtol()</code> 函数解决了 <code>atoi()</code> 函数的问题，但是我们还是能感觉到不是很舒服和自然。</p>
<p>因为，这种用 返回值 + errno 的错误检查方式会有一些问题:</p>
<ul>
<li>程序员一不小心就会忘记返回值的检查，从而造成代码的 Bug；</li>
<li>函数接口非常不纯洁，正常值和错误值混淆在一起，导致语义有问题。</li>
</ul>
<p>所以，后来，有一些类库就开始区分这样的事情。比如，Windows 的系统调用开始使用 <code>HRESULT</code> 的返回来统一错误的返回值，这样可以明确函数调用时的返回值是成功还是错误。但这样一来，函数的 input 和 output 只能通过函数的参数来完成，于是出现了所谓的 入参 和 出参 这样的区别。</p>
<p>然而，这又使得函数接入中参数的语义变得复杂，一些参数是入参，一些参数是出参，函数接口变得复杂了一些。而且，依然没有解决函数的成功或失败可以被人为忽略的问题。</p>
<h4>Java的错误处理</h4>
<p>Java语言使用 <code>try-catch-finally</code> 通过使用异常的方式来处理错误，其实，这比起C语言的错处理进了一大步，使用抛异常和抓异常的方式可以让我们的代码有这样的一些好处：</p>
<ul>
<li>函数接口在 input（参数）和 output（返回值）以及错误处理的语义是比较清楚的。</li>
<li>正常逻辑的代码可以与错误处理和资源清理的代码分开，提高了代码的可读性。</li>
<li>异常不能被忽略（如果要忽略也需要 catch 住，这是显式忽略）。</li>
<li>在面向对象的语言中（如 Java），异常是个对象，所以，可以实现多态式的 catch。</li>
<li>与状态返回码相比，异常捕捉有一个显著的好处是，函数可以嵌套调用，或是链式调用。比如：
<ul>
<li><code>int x = add(a, div(b,c));</code></li>
<li><code>Pizza p = PizzaBuilder().SetSize(sz).SetPrice(p)...;</code></li>
</ul>
</li>
</ul>
<h4>Go语言的错误处理</h4>
<p>Go 语言的函数支持多返回值，所以，可以在返回接口把业务语义（业务返回值）和控制语义（出错返回值）区分开来。Go 语言的很多函数都会返回 result, err 两个值，于是:</p>
<ul>
<li>参数上基本上就是入参，而返回接口把结果和错误分离，这样使得函数的接口语义清晰；</li>
<li>而且，Go 语言中的错误参数如果要忽略，需要显式地忽略，用 _ 这样的变量来忽略；</li>
<li>另外，因为返回的 <code>error</code> 是个接口（其中只有一个方法 <code>Error()</code>，返回一个 <code>string</code> ），所以你可以扩展自定义的错误处理。</li>
</ul>
<p>另外，如果一个函数返回了多个不同类型的 <code>error</code>，你也可以使用下面这样的方式：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">if err != nil {
  switch err.(type) {
    case *json.SyntaxError:
      ...
    case *ZeroDivisionError:
      ...
    case *NullPointerError:
      ...
    default:
      ...
  }
}</pre>
<p>我们可以看到，Go语言的错误处理的的方式，本质上是返回值检查，但是他也兼顾了异常的一些好处 &#8211; 对错误的扩展。</p>
<h4>资源清理</h4>
<p>出错后是需要做资源清理的，不同的编程语言有不同的资源清理的编程模式：</p>
<ul>
<li>C语言 &#8211; 使用的是 <code>goto fail;</code> 的方式到一个集中的地方进行清理（有篇有意思的文章可以看一下《<a title="由苹果的低级Bug想到的" href="https://coolshell.cn/articles/11112.html" target="_blank" rel="noopener">由苹果的低级BUG想到的</a>》）</li>
<li>C++语言- 一般来说使用 <a href="https://en.wikipedia.org/wiki/Resource_acquisition_is_initialization" target="_blank" rel="noopener">RAII模式</a>，通过面向对象的代理模式，把需要清理的资源交给一个代理类，然后在析构函数来解决。</li>
<li>Java语言 &#8211; 可以在finally 语句块里进行清理。</li>
<li>Go语言 &#8211; 使用 <code>defer</code> 关键词进行清理。</li>
</ul>
<p>下面是一个Go语言的资源清理的示例：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func Close(c io.Closer) {
  err := c.Close()
  if err != nil {
    log.Fatal(err)
  }
}

func main() {
  r, err := Open("a")
  if err != nil {
    log.Fatalf("error opening 'a'\n")
  }
  defer Close(r) // 使用defer关键字在函数退出时关闭文件。

  r, err = Open("b")
  if err != nil {
    log.Fatalf("error opening 'b'\n")
  }
  defer Close(r) // 使用defer关键字在函数退出时关闭文件。
}</pre>
<h4>Error Check  Hell</h4>
<p>好了，说到 Go 语言的 <code>if err !=nil</code> 的代码了，这样的代码的确是能让人写到吐。那么有没有什么好的方式呢，有的。我们先看如下的一个令人崩溃的代码。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func parse(r io.Reader) (*Point, error) {

    var p Point

    if err := binary.Read(r, binary.BigEndian, &amp;p.Longitude); err != nil {
        return nil, err
    }
    if err := binary.Read(r, binary.BigEndian, &amp;p.Latitude); err != nil {
        return nil, err
    }
    if err := binary.Read(r, binary.BigEndian, &amp;p.Distance); err != nil {
        return nil, err
    }
    if err := binary.Read(r, binary.BigEndian, &amp;p.ElevationGain); err != nil {
        return nil, err
    }
    if err := binary.Read(r, binary.BigEndian, &amp;p.ElevationLoss); err != nil {
        return nil, err
    }
}</pre>
<p>要解决这个事，我们可以用函数式编程的方式，如下代码示例：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func parse(r io.Reader) (*Point, error) {
    var p Point
    var err error
    read := func(data interface{}) {
        if err != nil {
            return
        }
        err = binary.Read(r, binary.BigEndian, data)
    }

    read(&amp;p.Longitude)
    read(&amp;p.Latitude)
    read(&amp;p.Distance)
    read(&amp;p.ElevationGain)
    read(&amp;p.ElevationLoss)

    if err != nil {
        return &amp;p, err
    }
    return &amp;p, nil
}</pre>
<p>上面的代码我们可以看到，我们通过使用Closure 的方式把相同的代码给抽出来重新定义一个函数，这样大量的  <code>if err!=nil</code> 处理的很干净了。但是会带来一个问题，那就是有一个 <code>err</code> 变量和一个内部的函数，感觉不是很干净。</p>
<p>那么，我们还能不能搞得更干净一点呢，我们从Go 语言的 <code>bufio.Scanner()</code>中似乎可以学习到一些东西：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">scanner := bufio.NewScanner(input)

for scanner.Scan() {
    token := scanner.Text()
    // process token
}

if err := scanner.Err(); err != nil {
    // process the error
}</pre>
<p>上面的代码我们可以看到，<code>scanner</code>在操作底层的I/O的时候，那个for-loop中没有任何的 <code>if err !=nil</code> 的情况，退出循环后有一个 <code>scanner.Err()</code> 的检查。看来使用了结构体的方式。模仿它，我们可以把我们的代码重构成下面这样：</p>
<p>首先，定义一个结构体和一个成员函数</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">type Reader struct {
    r   io.Reader
    err error
}

func (r *Reader) read(data interface{}) {
    if r.err == nil {
        r.err = binary.Read(r.r, binary.BigEndian, data)
    }
}</pre>
<p>然后，我们的代码就可以变成下面这样：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">func parse(input io.Reader) (*Point, error) {
    var p Point
    r := Reader{r: input}

    r.read(&amp;p.Longitude)
    r.read(&amp;p.Latitude)
    r.read(&amp;p.Distance)
    r.read(&amp;p.ElevationGain)
    r.read(&amp;p.ElevationLoss)

    if r.err != nil {
        return nil, r.err
    }

    return &amp;p, nil
}</pre>
<p>有了上面这个技术，我们的“<a href="https://martinfowler.com/bliki/FluentInterface.html" target="_blank" rel="noopener">流式接口 Fluent Interface</a>”，也就很容易处理了。如下所示：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">package main

import (
  "bytes"
  "encoding/binary"
  "fmt"
)

// 长度不够，少一个Weight
var b = []byte {0x48, 0x61, 0x6f, 0x20, 0x43, 0x68, 0x65, 0x6e, 0x00, 0x00, 0x2c} 
var r = bytes.NewReader(b)

type Person struct {
  Name [10]byte
  Age uint8
  Weight uint8
  err error
}
func (p *Person) read(data interface{}) {
  if p.err == nil {
    p.err = binary.Read(r, binary.BigEndian, data)
  }
}

func (p *Person) ReadName() *Person {
  p.read(&amp;p.Name) 
  return p
}
func (p *Person) ReadAge() *Person {
  p.read(&amp;p.Age) 
  return p
}
func (p *Person) ReadWeight() *Person {
  p.read(&amp;p.Weight) 
  return p
}
func (p *Person) Print() *Person {
  if p.err == nil {
    fmt.Printf("Name=%s, Age=%d, Weight=%d\n",p.Name, p.Age, p.Weight)
  }
  return p
}

func main() {   
  p := Person{}
  p.ReadName().ReadAge().ReadWeight().Print()
  fmt.Println(p.err)  // EOF 错误
}
</pre>
<p>相信你应该看懂这个技巧了，但是，其使用场景也就只能在对于同一个业务对象的不断操作下可以简化错误处理，对于多个业务对象的话，还是得需要各种 <code>if err != nil</code>的方式。</p>
<h4>包装错误</h4>
<p>最后，多说一句，我们需要包装一下错误，而不是干巴巴地把<code>err</code>给返回到上层，我们需要把一些执行的上下文加入。</p>
<p>通常来说，我们会使用 <code>fmt.Errorf()</code>来完成这个事，比如：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">if err != nil {
   return fmt.Errorf("something failed: %v", err)
}</pre>
<p>另外，在Go语言的开发者中，更为普遍的做法是将错误包装在另一个错误中，同时保留原始内容：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">type authorizationError struct {
    operation string
    err error   // original error
}

func (e *authorizationError) Error() string {
    return fmt.Sprintf("authorization failed during %s: %v", e.operation, e.err)
}</pre>
<p>当然，更好的方式是通过一种标准的访问方法，这样，我们最好使用一个接口，比如 <code>causer</code>接口中实现 <code>Cause()</code> 方法来暴露原始错误，以供进一步检查：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">type causer interface {
    Cause() error
}

func (e *authorizationError) Cause() error {
    return e.err
}
</pre>
<p>&nbsp;</p>
<p>这里有个好消息是，这样的代码不必再写了，有一个第三方的错误库（<a href="https://github.com/pkg/errors" target="_blank" rel="noopener">github.com/pkg/errors</a>），对于这个库，我无论到哪都能看到他的存在，所以，这个基本上来说就是事实上的标准了。代码示例如下：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">import "github.com/pkg/errors"

//错误包装
if err != nil {
    return errors.Wrap(err, "read failed")
}

// Cause接口
switch err := errors.Cause(err).(type) {
case *MyError:
    // handle specifically
default:
    // unknown error
}</pre>
<h4>参考文章</h4>
<ul>
<li><b>Golang Error Handling lesson by Rob Pike<br />
</b><a href="http://jxck.hatenablog.com/entry/golang-error-handling-lesson-by-rob-pike">http://jxck.hatenablog.com/entry/golang-error-handling-lesson-by-rob-pike</a></li>
<li><b>Errors are values<br />
</b><a href="https://blog.golang.org/errors-are-values">https://blog.golang.org/errors-are-values</a></li>
</ul>
<p>（全文完）<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/21615.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2021/09/go-generics-150x150.png" alt="Go编程模式 ： 泛型编程" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21615.html" class="wp_rp_title">Go编程模式 ： 泛型编程</a></li><li ><a href="https://coolshell.cn/articles/21263.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.k8s-150x150.png" alt="Go 编程模式：k8s Visitor 模式" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21263.html" class="wp_rp_title">Go 编程模式：k8s Visitor 模式</a></li><li ><a href="https://coolshell.cn/articles/21228.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.line_.-150x150.png" alt="Go编程模式：Pipeline" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21228.html" class="wp_rp_title">Go编程模式：Pipeline</a></li><li ><a href="https://coolshell.cn/articles/21214.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.pair_-150x150.png" alt="Go编程模式：委托和反转控制" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21214.html" class="wp_rp_title">Go编程模式：委托和反转控制</a></li><li ><a href="https://coolshell.cn/articles/21179.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.generate-150x150.png" alt="Go 编程模式：Go Generation" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21179.html" class="wp_rp_title">Go 编程模式：Go Generation</a></li><li ><a href="https://coolshell.cn/articles/21164.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.map_.reduce-150x150.png" alt="Go编程模式：Map-Reduce" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21164.html" class="wp_rp_title">Go编程模式：Map-Reduce</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/21140.html">Go 编程模式：错误处理</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/21140.html/feed</wfw:commentRss>
			<slash:comments>24</slash:comments>
		
		
			</item>
		<item>
		<title>Go编程模式：切片，接口，时间和性能</title>
		<link>https://coolshell.cn/articles/21128.html</link>
					<comments>https://coolshell.cn/articles/21128.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Fri, 18 Dec 2020 07:36:30 +0000</pubDate>
				<category><![CDATA[Go 语言]]></category>
		<category><![CDATA[程序设计]]></category>
		<category><![CDATA[编程语言]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[golang]]></category>
		<guid isPermaLink="false">https://coolshell.cn/?p=21128</guid>

					<description><![CDATA[<p>在本篇文章中，我会对Go语言编程模式的一些基本技术和要点，这样可以让你更容易掌握Go语言编程。其中，主要包括，数组切片的一些小坑，还有接口编程，以及时间和程序运...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/21128.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/21128.html">Go编程模式：切片，接口，时间和性能</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><img decoding="async" loading="lazy" class="alignright size-medium wp-image-21234" src="https://coolshell.cn/wp-content/uploads/2020/12/go.slices-300x169.png" alt="" width="300" height="169" srcset="https://coolshell.cn/wp-content/uploads/2020/12/go.slices-300x169.png 300w, https://coolshell.cn/wp-content/uploads/2020/12/go.slices-1024x578.png 1024w, https://coolshell.cn/wp-content/uploads/2020/12/go.slices-768x434.png 768w, https://coolshell.cn/wp-content/uploads/2020/12/go.slices-1536x867.png 1536w, https://coolshell.cn/wp-content/uploads/2020/12/go.slices-478x270.png 478w, https://coolshell.cn/wp-content/uploads/2020/12/go.slices.png 1576w" sizes="(max-width: 300px) 100vw, 300px" /></p>
<p>在本篇文章中，我会对Go语言编程模式的一些基本技术和要点，这样可以让你更容易掌握Go语言编程。其中，主要包括，数组切片的一些小坑，还有接口编程，以及时间和程序运行性能相关的话题。</p>
<section class="post-series"><h3 class="post-series-title">本文是全系列中第1 / 10篇：<a href="https://coolshell.cn/articles/series/go%e7%bc%96%e7%a8%8b%e6%a8%a1%e5%bc%8f">Go编程模式</a></h3><ul class="post-series-list"><li class="post-series-item-current post-series-item"><span class="post-series-item-title">Go编程模式：切片，接口，时间和性能</span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21140.html">Go 编程模式：错误处理</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21146.html">Go 编程模式：Functional Options</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21214.html">Go编程模式：委托和反转控制</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21164.html">Go编程模式：Map-Reduce</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21179.html">Go 编程模式：Go Generation</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/17929.html">Go编程模式：修饰器</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21228.html">Go编程模式：Pipeline</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21263.html">Go 编程模式：k8s Visitor 模式</a></span></li><li class="post-series-item"><span class="post-series-item-title"><a href="https://coolshell.cn/articles/21615.html">Go编程模式 ： 泛型编程</a></span></li></ul><nav class="post-series-nav"><span class="post-series-nav-next"><a href="https://coolshell.cn/articles/21140.html" rel="next" title="Go 编程模式：错误处理">下一篇文章</a> &raquo;</span></nav></section>
<h4>Slice</h4>
<p>首先，我们先来讨论一下Slice，中文翻译叫“切片”，这个东西在Go语言中不是数组，而是一个结构体，其定义如下：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">type slice struct {
    array unsafe.Pointer //指向存放数据的数组指针
    len   int            //长度有多大
    cap   int            //容量有多大
}</pre>
<p>用图示来看，一个空的slice的表现如下：</p>
<p><img decoding="async" loading="lazy" class="aligncenter wp-image-21129 size-medium" src="https://coolshell.cn/wp-content/uploads/2020/12/slice1-300x190.png" alt="" width="300" height="190" srcset="https://coolshell.cn/wp-content/uploads/2020/12/slice1-300x190.png 300w, https://coolshell.cn/wp-content/uploads/2020/12/slice1-427x270.png 427w, https://coolshell.cn/wp-content/uploads/2020/12/slice1.png 466w" sizes="(max-width: 300px) 100vw, 300px" /> 熟悉C/C++的同学一定会知道，在结构体里用数组指针的问题——数据会发生共享！下面我们来看一下slice的一些操作</p>
<p><span id="more-21128"></span></p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">foo = make([]int, 5)
foo[3] = 42
foo[4] = 100

bar  := foo[1:4]
bar[1] = 99</pre>
<p>对于上面这段代码。</p>
<ul>
<li>首先先创建一个foo的slice，其中的长度和容量都是5</li>
<li>然后开始对foo所指向的数组中的索引为3和4的元素进行赋值</li>
<li>然后，对foo做切片后赋值给bar，再修改bar[1]</li>
</ul>
<p><img decoding="async" loading="lazy" class="aligncenter size-full wp-image-21130" src="https://coolshell.cn/wp-content/uploads/2020/12/slice2.png" alt="" width="818" height="362" srcset="https://coolshell.cn/wp-content/uploads/2020/12/slice2.png 818w, https://coolshell.cn/wp-content/uploads/2020/12/slice2-300x133.png 300w, https://coolshell.cn/wp-content/uploads/2020/12/slice2-768x340.png 768w, https://coolshell.cn/wp-content/uploads/2020/12/slice2-604x267.png 604w" sizes="(max-width: 818px) 100vw, 818px" /></p>
<p>通过上图我们可以看到，因为foo和bar的内存是共享的，所以，foo和bar的对数组内容的修改都会影响到对方。</p>
<p>接下来，我们再来看一个数据操作 <code>append()</code> 的示例</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">a := make([]int, 32)
b := a[1:16]
a = append(a, 1)
a[2] = 42</pre>
<p>上面这段代码中，把 <code>a[1:16]</code> 的切片赋给到了 <code>b</code> ，此时，<code>a</code> 和 <code>b</code> 的内存空间是共享的，然后，对 <code>a</code>做了一个 <code>append()</code>的操作，这个操作会让 <code>a</code> 重新分享内存，导致 <code>a</code> 和 <code>b</code> 不再共享，如下图所示：</p>
<p><img decoding="async" loading="lazy" class="aligncenter size-large wp-image-21326" src="https://coolshell.cn/wp-content/uploads/2020/12/go.slices.append-1024x513.png" alt="" width="640" height="321" srcset="https://coolshell.cn/wp-content/uploads/2020/12/go.slices.append-1024x513.png 1024w, https://coolshell.cn/wp-content/uploads/2020/12/go.slices.append-300x150.png 300w, https://coolshell.cn/wp-content/uploads/2020/12/go.slices.append-768x385.png 768w, https://coolshell.cn/wp-content/uploads/2020/12/go.slices.append-539x270.png 539w, https://coolshell.cn/wp-content/uploads/2020/12/go.slices.append.png 1454w" sizes="(max-width: 640px) 100vw, 640px" /></p>
<p>从上图我们可以看以看到 <code>append()</code>操作让 <code>a</code> 的容量变成了64，而长度是33。这里，需要重点注意一下——<strong><code>append()</code>这个函数在 <code>cap</code> 不够用的时候就会重新分配内存以扩大容量，而如果够用的时候不不会重新分享内存！</strong></p>
<p>我们再看来看一个例子：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func main() {
    path := []byte("AAAA/BBBBBBBBB")
    sepIndex := bytes.IndexByte(path,'/’)

    dir1 := path[:sepIndex]
    dir2 := path[sepIndex+1:]

    fmt.Println("dir1 =&gt;",string(dir1)) //prints: dir1 =&gt; AAAA
    fmt.Println("dir2 =&gt;",string(dir2)) //prints: dir2 =&gt; BBBBBBBBB

    dir1 = append(dir1,"suffix"...)

    fmt.Println("dir1 =&gt;",string(dir1)) //prints: dir1 =&gt; AAAAsuffix
    fmt.Println("dir2 =&gt;",string(dir2)) //prints: dir2 =&gt; uffixBBBB
}</pre>
<p>上面这个例子中，<code>dir1</code> 和 <code>dir2</code> 共享内存，虽然 <code>dir1</code> 有一个 <code>append()</code> 操作，但是因为 cap 足够，于是数据扩展到了<code>dir2</code> 的空间。下面是相关的图示（注意上图中 <code>dir1</code> 和 <code>dir2</code> 结构体中的 <code>cap</code> 和 <code>len</code> 的变化）</p>
<p><img decoding="async" loading="lazy" class="aligncenter size-large wp-image-21328" src="https://coolshell.cn/wp-content/uploads/2020/12/slice4-1024x740.png" alt="" width="640" height="463" srcset="https://coolshell.cn/wp-content/uploads/2020/12/slice4-1024x740.png 1024w, https://coolshell.cn/wp-content/uploads/2020/12/slice4-300x217.png 300w, https://coolshell.cn/wp-content/uploads/2020/12/slice4-768x555.png 768w, https://coolshell.cn/wp-content/uploads/2020/12/slice4-374x270.png 374w, https://coolshell.cn/wp-content/uploads/2020/12/slice4.png 1481w" sizes="(max-width: 640px) 100vw, 640px" /></p>
<p>如果要解决这个问题，我们只需要修改一行代码。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">dir1 := path[:sepIndex]
</pre>
<p>修改为</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">dir1 := path[:sepIndex:sepIndex]
</pre>
<p>新的代码使用了 Full Slice Expression，其最后一个参数叫“Limited Capacity”，于是，后续的 <code>append()</code> 操作将会导致重新分配内存。</p>
<h4>深度比较</h4>
<p>当我们复杂一个对象时，这个对象可以是内建数据类型，数组，结构体，map……我们在复制结构体的时候，当我们需要比较两个结构体中的数据是否相同时，我们需要使用深度比较，而不是只是简单地做浅度比较。这里需要使用到反射 <code>reflect.DeepEqual()</code> ，下面是几个示例</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">import (
    "fmt"
    "reflect"
)

func main() {

    v1 := data{}
    v2 := data{}
    fmt.Println("v1 == v2:",reflect.DeepEqual(v1,v2))
    //prints: v1 == v2: true

    m1 := map[string]string{"one": "a","two": "b"}
    m2 := map[string]string{"two": "b", "one": "a"}
    fmt.Println("m1 == m2:",reflect.DeepEqual(m1, m2))
    //prints: m1 == m2: true

    s1 := []int{1, 2, 3}
    s2 := []int{1, 2, 3}
    fmt.Println("s1 == s2:",reflect.DeepEqual(s1, s2))
    //prints: s1 == s2: true
}</pre>
<h4>接口编程</h4>
<p>下面，我们来看段代码，其中是两个方法，它们都是要输出一个结构体，其中一个使用一个函数，另一个使用一个“成员函数”。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func PrintPerson(p *Person) {
    fmt.Printf("Name=%s, Sexual=%s, Age=%d\n",
  p.Name, p.Sexual, p.Age)
}

func (p *Person) Print() {
    fmt.Printf("Name=%s, Sexual=%s, Age=%d\n",
  p.Name, p.Sexual, p.Age)
}

func main() {
    var p = Person{
        Name: "Hao Chen",
        Sexual: "Male",
        Age: 44,
    }

    PrintPerson(&amp;p)
    p.Print()
}</pre>
<p>你更喜欢哪种方式呢？在 Go 语言中，使用“成员函数”的方式叫“Receiver”，这种方式是一种封装，因为 <code>PrintPerson()</code>本来就是和 <code>Person</code>强耦合的，所以，理应放在一起。更重要的是，这种方式可以进行接口编程，对于接口编程来说，也就是一种抽象，主要是用在“多态”，这个技术，在《<a href="https://coolshell.cn/articles/8460.html#%E6%8E%A5%E5%8F%A3%E5%92%8C%E5%A4%9A%E6%80%81" target="_blank" rel="noopener">Go语言简介（上）：接口与多态</a>》中已经讲过。在这里，我想讲另一个Go语言接口的编程模式。</p>
<p>首先，我们来看一下，有下面这段代码：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">type Country struct {
    Name string
}

type City struct {
    Name string
}

type Printable interface {
    PrintStr()
}
func (c Country) PrintStr() {
    fmt.Println(c.Name)
}
func (c City) PrintStr() {
    fmt.Println(c.Name)
}

c1 := Country {"China"}
c2 := City {"Beijing"}
c1.PrintStr()
c2.PrintStr()</pre>
<p>其中，我们可以看到，其使用了一个 <code>Printable</code> 的接口，而 <code>Country</code> 和 <code>City</code> 都实现了接口方法 <code>PrintStr()</code> 而把自己输出。然而，这些代码都是一样的。能不能省掉呢？</p>
<p>我们可以使用“结构体嵌入”的方式来完成这个事，如下的代码所示，</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">type WithName struct {
    Name string
}

type Country struct {
    WithName
}

type City struct {
    WithName
}

type Printable interface {
    PrintStr()
}

func (w WithName) PrintStr() {
    fmt.Println(w.Name)
}

c1 := Country {WithName{ "China"}}
c2 := City { WithName{"Beijing"}}
c1.PrintStr()
c2.PrintStr()</pre>
<p>引入一个叫 <code>WithName</code>的结构体，然而，所带来的问题就是，在初始化的时候，变得有点乱。那么，我们有没有更好的方法？下面是另外一个解。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">type Country struct {
    Name string
}

type City struct {
    Name string
}

type Stringable interface {
    ToString() string
}
func (c Country) ToString() string {
    return "Country = " + c.Name
}
func (c City) ToString() string{
    return "City = " + c.Name
}

func PrintStr(p Stringable) {
    fmt.Println(p.ToString())
}

d1 := Country {"USA"}
d2 := City{"Los Angeles"}
PrintStr(d1)
PrintStr(d2)</pre>
<p>上面这段代码，我们可以看到——<strong>我们使用了一个叫<code>Stringable</code> 的接口，我们用这个接口把“业务类型” <code>Country</code> 和 <code>City</code> 和“控制逻辑” <code>Print()</code> 给解耦了。</strong>于是，只要实现了<code>Stringable</code> 接口，都可以传给 <code>PrintStr()</code> 来使用。</p>
<p>这种编程模式在Go 的标准库有很多的示例，最著名的就是 <code>io.Read</code> 和 <code>ioutil.ReadAll</code> 的玩法，其中 <code>io.Read</code> 是一个接口，你需要实现他的一个 <code>Read(p []byte) (n int, err error)</code> 接口方法，只要满足这个规模，就可以被 <code>ioutil.ReadAll</code>这个方法所使用。<strong>这就是面向对象编程方法的黄金法则——“Program to an interface not an implementation”</strong></p>
<h4>接口完整性检查</h4>
<p>另外，我们可以看到，Go语言的编程器并没有严格检查一个对象是否实现了某接口所有的接口方法，如下面这个示例：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">type Shape interface {
    Sides() int
    Area() int
}
type Square struct {
    len int
}
func (s* Square) Sides() int {
    return 4
}
func main() {
    s := Square{len: 5}
    fmt.Printf("%d\n",s.Sides())
}</pre>
<p>我们可以看到 <code>Square</code> 并没有实现 <code>Shape</code> 接口的所有方法，程序虽然可以跑通，但是这样编程的方式并不严谨，如果我们需要强制实现接口的所有方法，那么我们应该怎么办呢？</p>
<p>在Go语言编程圈里有一个比较标准的作法：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">var _ Shape = (*Square)(nil)</pre>
<p>声明一个 <code>_</code> 变量（没人用），其会把一个 <code>nil</code> 的空指针，从 <code>Square</code> 转成 <code>Shape</code>，这样，如果没有实现完相关的接口方法，编译器就会报错：</p>
<blockquote><p>cannot use (*Square)(nil) (type *Square) as type Shape in assignment: *Square does not implement Shape (missing Area method)</p></blockquote>
<p>这样就做到了个强验证的方法。</p>
<h4>时间</h4>
<p>对于时间来说，这应该是编程中比较复杂的问题了，相信我，时间是一种非常复杂的事（比如《<a title="你确信你了解时间吗？" href="https://coolshell.cn/articles/5075.html" target="_blank" rel="noopener">你确信你了解时间吗？</a>》、《<a title="关于闰秒" href="https://coolshell.cn/articles/7804.html">关于闰秒</a>》等文章）。而且，时间有时区、格式、精度等等问题，其复杂度不是一般人能处理的。所以，一定要重用已有的时间处理，而不是自己干。</p>
<p>在 Go 语言中，你一定要使用 <code>time.Time</code> 和 <code>time.Duration</code> 两个类型：</p>
<ul>
<li>在命令行上，<code>flag</code> 通过 <code>time.ParseDuration</code> 支持了 <code>time.Duration</code></li>
<li>JSon 中的 <code>encoding/json</code> 中也可以把<code>time.Time</code> 编码成 <a href="https://tools.ietf.org/html/rfc3339" target="_blank" rel="noopener">RFC 3339</a> 的格式</li>
<li>数据库使用的 <code>database/sql</code> 也支持把 <code>DATATIME</code> 或 <code>TIMESTAMP</code> 类型转成 <code>time.Time</code></li>
<li>YAML你可以使用 <code>gopkg.in/yaml.v2</code> 也支持 <code>time.Time</code> 、<code>time.Duration</code> 和 <a href="https://tools.ietf.org/html/rfc3339" target="_blank" rel="noopener">RFC 3339</a> 格式</li>
</ul>
<p>如果你要和第三方交互，实在没有办法，也请使用 <a href="https://tools.ietf.org/html/rfc3339" target="_blank" rel="noopener">RFC 3339</a> 的格式。</p>
<p>最后，如果你要做全球化跨时区的应用，你一定要把所有服务器和时间全部使用UTC时间。</p>
<h4>性能提示</h4>
<p>Go 语言是一个高性能的语言，但并不是说这样我们就不用关心性能了，我们还是需要关心的。下面是一个在编程方面和性能相关的提示。</p>
<ul>
<li>如果需要把数字转字符串，使用 <code>strconv.Itoa()</code> 会比 <code>fmt.Sprintf()</code> 要快一倍左右</li>
<li>尽可能地避免把<code>String</code>转成<code>[]Byte</code> 。这个转换会导致性能下降。</li>
<li>如果在for-loop里对某个slice 使用 <code>append()</code>请先把 slice的容量很扩充到位，这样可以避免内存重新分享以及系统自动按2的N次方幂进行扩展但又用不到，从而浪费内存。</li>
<li>使用<code>StringBuffer</code> 或是<code>StringBuild</code> 来拼接字符串，会比使用 <code>+</code> 或 <code>+=</code> 性能高三到四个数量级。</li>
<li>尽可能的使用并发的 go routine，然后使用 <code>sync.WaitGroup</code> 来同步分片操作</li>
<li>避免在热代码中进行内存分配，这样会导致gc很忙。尽可能的使用 <code>sync.Pool</code> 来重用对象。</li>
<li>使用 lock-free的操作，避免使用 mutex，尽可能使用 <code>sync/Atomic</code>包。 （关于无锁编程的相关话题，可参看《<a title="无锁队列的实现" href="https://coolshell.cn/articles/8239.html">无锁队列实现</a>》或《<a title="无锁HashMap的原理与实现" href="https://coolshell.cn/articles/9703.html">无锁Hashmap实现</a>》）</li>
<li>使用 I/O缓冲，I/O是个非常非常慢的操作，使用 <code>bufio.NewWrite()</code> 和 <code>bufio.NewReader()</code> 可以带来更高的性能。</li>
<li>对于在for-loop里的固定的正则表达式，一定要使用 <code>regexp.Compile()</code> 编译正则表达式。性能会得升两个数量级。</li>
<li>如果你需要更高性能的协议，你要考虑使用 <a href="https://github.com/golang/protobuf" target="_blank" rel="noopener">protobuf</a> 或 <a href="https://github.com/tinylib/msgp" target="_blank" rel="noopener">msgp</a> 而不是JSON，因为JSON的序列化和反序列化里使用了反射。</li>
<li>你在使用map的时候，使用整型的key会比字符串的要快，因为整型比较比字符串比较要快。</li>
</ul>
<h4>参考文档</h4>
<p>还有很多不错的技巧，下面的这些参考文档可以让你写出更好的Go的代码，必读！</p>
<ul>
<li><b>Effective</b> <b>Go<br />
</b><a href="https://golang.org/doc/effective_go.html">https://golang.org/doc/effective_go.html</a></li>
<li><b>Uber</b> <b>Go</b> <b>Style<br />
</b><a href="https://github.com/uber-go/guide/blob/master/style.md">https://github.com/uber-go/guide/blob/master/style.md</a></li>
<li><b>50 Shades of Go: Traps, Gotchas, and Common Mistakes for New Golang Devs<br />
</b><a href="http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/">http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/</a></li>
<li><b>Go</b> <b>Advice<br />
</b><a href="https://github.com/cristaloleg/go-advice">https://github.com/cristaloleg/go-advice</a><b></b></li>
<li><b>Practical Go Benchmarks<br />
</b><a href="https://www.instana.com/blog/practical-golang-benchmarks/">https://www.instana.com/blog/practical-golang-benchmarks/</a><b></b></li>
<li><b>Benchmarks of Go serialization methods<br />
</b><a href="https://github.com/alecthomas/go_serialization_benchmarks">https://github.com/alecthomas/go_serialization_benchmarks</a><b></b></li>
<li><b>Debugging</b> <b>performance</b> <b>issues</b> <b>in</b> <b>Go</b> <b>programs<br />
</b><a href="https://github.com/golang/go/wiki/Performance">https://github.com/golang/go/wiki/Performance</a><b></b></li>
<li><b>Go</b> <b>code</b> <b>refactoring:</b> <b>the</b> <b>23x</b> <b>performance</b> <b>hunt<br />
</b><a href="https://medium.com/@val_deleplace/go-code-refactoring-the-23x-performance-hunt-156746b522f7">https://medium.com/@val_deleplace/go-code-refactoring-the-23x-performance-hunt-156746b522f7</a></li>
</ul>
<p>（全文完）<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/21615.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2021/09/go-generics-150x150.png" alt="Go编程模式 ： 泛型编程" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21615.html" class="wp_rp_title">Go编程模式 ： 泛型编程</a></li><li ><a href="https://coolshell.cn/articles/21263.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.k8s-150x150.png" alt="Go 编程模式：k8s Visitor 模式" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21263.html" class="wp_rp_title">Go 编程模式：k8s Visitor 模式</a></li><li ><a href="https://coolshell.cn/articles/21228.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.line_.-150x150.png" alt="Go编程模式：Pipeline" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21228.html" class="wp_rp_title">Go编程模式：Pipeline</a></li><li ><a href="https://coolshell.cn/articles/21214.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.pair_-150x150.png" alt="Go编程模式：委托和反转控制" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21214.html" class="wp_rp_title">Go编程模式：委托和反转控制</a></li><li ><a href="https://coolshell.cn/articles/21179.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.generate-150x150.png" alt="Go 编程模式：Go Generation" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21179.html" class="wp_rp_title">Go 编程模式：Go Generation</a></li><li ><a href="https://coolshell.cn/articles/21164.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.map_.reduce-150x150.png" alt="Go编程模式：Map-Reduce" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21164.html" class="wp_rp_title">Go编程模式：Map-Reduce</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/21128.html">Go编程模式：切片，接口，时间和性能</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/21128.html/feed</wfw:commentRss>
			<slash:comments>33</slash:comments>
		
		
			</item>
		<item>
		<title>计时攻击 Timing Attacks</title>
		<link>https://coolshell.cn/articles/21003.html</link>
					<comments>https://coolshell.cn/articles/21003.html#comments</comments>
		
		<dc:creator><![CDATA[tanglei.name]]></dc:creator>
		<pubDate>Sun, 05 Jul 2020 05:26:52 +0000</pubDate>
				<category><![CDATA[程序设计]]></category>
		<category><![CDATA[网络安全]]></category>
		<category><![CDATA[HMAC]]></category>
		<guid isPermaLink="false">https://coolshell.cn/?p=21003</guid>

					<description><![CDATA[<p>本文来自读者“程序猿石头”的投稿文章《这 10 行比较字符串相等的代码给我整懵了，不信你也来看看》，原文写的很好，但不够直接了当，信息密度不够高，所以我对原文进...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/21003.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/21003.html">计时攻击 Timing Attacks</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><img decoding="async" loading="lazy" class="alignright wp-image-21015 " src="https://coolshell.cn/wp-content/uploads/2020/06/time-bomb-300x300.png" alt="" width="240" height="240" srcset="https://coolshell.cn/wp-content/uploads/2020/06/time-bomb-300x300.png 300w, https://coolshell.cn/wp-content/uploads/2020/06/time-bomb-150x150.png 150w, https://coolshell.cn/wp-content/uploads/2020/06/time-bomb-200x200.png 200w, https://coolshell.cn/wp-content/uploads/2020/06/time-bomb-270x270.png 270w, https://coolshell.cn/wp-content/uploads/2020/06/time-bomb.png 512w" sizes="(max-width: 240px) 100vw, 240px" />本文来自读者“程序猿石头”的投稿文章《<a href="http://mp.weixin.qq.com/s?__biz=MzI3OTUzMzcwNw==&amp;mid=100002290&amp;idx=1&amp;sn=8829db16a065f485b257fba0c691d94f&amp;chksm=6b4708165c30810096133f36523c8c781ce5333d851c31905d6cc49dd9b756a3f08141fbc9e8#rd" target="_blank" rel="noopener noreferrer">这 10 行比较字符串相等的代码给我整懵了，不信你也来看看</a>》，原文写的很好，但不够直接了当，信息密度不够高，所以我对原文进行大量的删减、裁剪、改写和添加，主要删除了一些没有信息的段落，主要加入了如何实施计时攻击相关的其它内容，让这篇文章中的知识更系统一些，并且还指出了其它的一些问题。所以，我把标题也改成了《计时攻击 Timing Attacks》。</p>
<h4>另类的字符串比较</h4>
<p>在 Java 的 Play Framework 里有<a href="https://github.com/playframework/play1/blob/b01eb02eb8df2e94cac2793c028dd9c4c5a57b31/framework/src/play/mvc/CookieDataCodec.java#L82" target="_blank" rel="noopener noreferrer">一段代码</a>用来验证cookie(session)中的数据是否合法（包含签名的验证）的代码，如下所示：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="java">boolean safeEqual(String a, String b) {
   if (a.length() != b.length()) {
       return false;
   }
   int equal = 0;
   for (int i = 0; i &lt; a.length(); i++) {
       equal |= a.charAt(i) ^ b.charAt(i);
   }
   return equal == 0;
}</pre>
<p>相信刚看到这段源码的人会感觉挺奇怪的，这个函数的功能是比较两个字符串是否相等，如果要判断两个字符串是否相等，正常人的写法应该是下面这个样子的（来自JDK8 的 <code>String.equals()</code>-有删减）：</p>
<p><span id="more-21003"></span></p>
<pre class="EnlighterJSRAW" data-enlighter-language="java" data-enlighter-highlight="9,10">public boolean equals(Object anObject) {
    String anotherString = (String)anObject;
    int n = value.length;
    if (n == anotherString.value.length) {
        char v1[] = value;
        char v2[] = anotherString.value;
        int i = 0;
        while (n-- != 0) {
            if (v1[i] != v2[i]) // &lt;- 遇到第一个不一样的字符时退出
                return false;
            i++;
        }
        return true;
    }
    return false;
}</pre>
<p>我们可以看到，在比较两个字符串是否相等的正常写法是：</p>
<ol>
<li>先看一下两个字符串长度是否相等，如果不等直接返回 false。</li>
<li>如果长度相等，则依次判断每个字符是否相等，如果不等则返回 false。</li>
<li>如果全部相等，则返回 true。一旦遇到不一样的字符时，直接返回false。</li>
</ol>
<p>然而，Play Framework里的代码却不是这样的，尤其是上述的第2点，用到了异或，熟悉位操作的你很容易就能看懂，通过异或操作 <code>1^1=0</code> , <code>1^0=1</code>, <code>0^0=0</code>，来比较每一位，如果每一位都相等的话，两个字符串肯定相等，最后存储累计异或值的变量 <code>equal</code>必定为 0（因为相同的字符必然为偶数），否则为 1。</p>
<p>但是，这种异或的方式不是遇到第一个不一样的字符就返回 false 了，而是要做全量比较，这种比较完全没有效率，这是为什么呢？原因是为了安全。</p>
<h4>计时攻击(Timing Attack)</h4>
<p>计时攻击（<a href="https://en.wikipedia.org/wiki/Timing_attack" target="_blank" rel="noopener noreferrer">Wikipedia</a>）是<a href="https://en.wikipedia.org/wiki/Side-channel_attack" target="_blank" rel="noopener noreferrer">旁道攻击</a>(或称&#8221;侧信道攻击&#8221;， Side Channel Attack， 简称SCA) 的一种，<b>旁通道攻击</b>是指基于从计算机系统的实现中获得的信息的任何攻击 ，而不是基于实现的算法本身的弱点（例如，密码分析和软件错误）。时间信息，功耗，电磁泄漏甚至声音可以提供额外的信息来源，可以加以利用。在很多物理隔绝的环境中（黑盒），往往也能出奇制胜，这类新型攻击的有效性远高于传统的密码分析的数学方法。（注：企图通过社会工程学欺骗或强迫具有合法访问权限的人来破坏密码系统通常不被视为旁道攻击）</p>
<p>计时攻击是最常用的攻击方法。那么，正常的字符串比较是怎么被黑客进行时间攻击的呢？</p>
<p>我们知道，正常的字符串比较，一旦遇到每一个不同的字符就返回失败了，所以，理论上来说，前面只有2个字符相同字符串比较的耗时，要比前面有10个字符相同的比较要短。你会说，这能相差多少呢？可能几微秒吧。但是，我们可以放大这个事。比如，在Web应用时，记录每个请求的返回所需请求时间（一般是毫秒级），如果我们重复50次，我们可以查看平均时间或是p50的时间，以了解哪个字符返回的时间比较长，如果某个我们要尝试的字符串的时间比较长，我们就可以确定地得出这个这字符串的前面一段必然是正确的。（当然，你会说网络请求的燥音太多了，在毫秒级的请求上完全没办判断，这个需要用到统计学来降噪，后面会给出方法）</p>
<p>这个事情，可以用来做HMAC的攻击，所谓HMAC，你可以参看本站的《<a title="HTTP API 认证授权术" href="https://coolshell.cn/articles/19395.html" target="_blank" rel="noopener noreferrer">HTTP API 认证授权术</a>》文章了解更多的细节。简单来说，HMAC，就是客户端向服务端发来一个字符串和其签名字符串（HMAC），然后，服务端的程序用一个私钥来对客户端发来的字符串进行签名得到签名字符串，然后再比较这个签名字符串（所谓签名，也就是使用MD5或SHA这样的哈希算法进行编码，是不可逆的）</p>
<p>写成伪代码大概是这个样子：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="c">bool verify(message, digest) {
    my_digest = HMAC(key, message);
    return my_digest.equals(digest) ;
}</pre>
<p>于是，攻击者在不知道签名算法和私钥的情况下，但是知道API的调用接口时，就可以通过一边穷举签名，一边统计调用时间的方式来非常有效率的破解签名。在这篇文章《<a href="http://www.eggie5.com/45-hmac-timing-attacks" target="_blank" rel="noopener noreferrer">HMAC Timing Attacks</a>》中记录了整个攻击的过程。文章中记载：</p>
<p>如果一个签名有40个长度，如：<code>f5acdffbf0bb39b2cdf59ccc19625015b33f55fe</code> 攻击者，从 <code>0000000000000000000000000000000000000000</code> 开始穷举，下面是穷举第一个字符（从<code>0</code>到<code>f</code>因为这是HMAC算法的取值范围）的时间统计。</p>
<pre>0 0.005450913
1 0.005829198
2 0.004905407
3 0.005286876
4 0.005597611
5 0.004814430
6 0.004969118
7 0.005335884
8 0.004433182
9 0.004440246
a 0.004860263
b 0.004561121
c 0.004463188
d 0.004406799
e 0.004978907
f 0.004887240
</pre>
<p>可以看到，第一次测试通过的计时结果（以秒为单位），而值“ f”与样品的其余部分之间没有较大的变化量，所有结果看起来都非常接近。换句话说，有很多噪声掩盖了信号。因此，有必要进行多个采样（对测试进行缩放）并使用统计工具从噪声中滤除信号。为了将信号与噪声分开，我们必须按任意常数对测试进行缩放。通过实验，作者发现500是一个很好的数字。换句话说：运行测试500次，并记录500个试验中每个试验的结果。然后，通过人的肉眼观察可以可能看到 f 的调用明显比别的要长，但是这种方法很难自动化。</p>
<p>所以，作者给了另一个统计算法，这个算法向服务器分别从 0 到 f 发出16个请求，并记录每个请求的响应时间，并将它们排序为1-16，其中1是最长（最慢）的请求，而16是最短（最快的请求），分别记录 0 &#8211; f 的名次，然后重复上述的过程 500 次。如下所示（仅显示25个样本，字符“ 0”首先被排名7、1、3，然后再次排名3……）：</p>
<pre>{
"0"=&gt;[7, 1, 3, 3, 15, 5, 4, 9, 15, 10, 13, 2, 14, 9, 4, 14, 7, 9, 15, 2, 14, 9, 14, 6, 11...],
"1"=&gt;[13, 4, 7, 11, 0, 4, 0, 2, 14, 11, 6, 7, 2, 2, 14, 11, 8, 10, 5, 13, 11, 7, 4, 9, 3...],
"2"=&gt;[14, 5, 15, 5, 1, 0, 3, 1, 9, 12, 4, 4, 1, 1, 8, 6, 9, 4, 9, 5, 8, 3, 12, 8, 5...],
"3"=&gt;[15, 2, 9, 7, 2, 1, 14, 11, 7, 8, 8, 1, 4, 7, 12, 15, 13, 0, 4, 1, 7, 0, 3, 0, 0...],
"4"=&gt;[12, 10, 14, 15, 8, 9, 10, 12, 10, 4, 1, 13, 15, 15, 3, 1, 6, 8, 2, 6, 15, 4, 0, 3, 2...],
"5"=&gt;[5, 13, 13, 12, 7, 8, 13, 14, 3, 13, 2, 12, 7, 14, 2, 10, 12, 5, 8, 0, 4, 10, 5, 10, 12...]
"6"=&gt;[0, 15, 11, 13, 5, 15, 8, 8, 4, 7, 12, 9, 10, 11, 11, 7, 0, 6, 0, 9, 2, 6, 15, 13, 14...]
"7"=&gt;[1, 9, 0, 10, 6, 6, 2, 4, 12, 9, 5, 10, 5, 10, 7, 2, 4, 14, 6, 7, 13, 11, 6, 12, 4...],
"8"=&gt;[4, 0, 2, 1, 9, 11, 12, 13, 11, 14, 0, 15, 9, 0, 0, 13, 11, 13, 1, 8, 6, 5, 11, 15, 7...],
"9"=&gt;[11, 11, 10, 4, 13, 7, 6, 3, 2, 2, 14, 5, 3, 3, 15, 9, 14, 7, 10, 3, 0, 14, 1, 5, 15...],
"a"=&gt;[8, 3, 6, 14, 10, 2, 7, 5, 1, 3, 3, 0, 0, 6, 10, 12, 15, 12, 12, 15, 9, 13, 13, 11, 9...],
"b"=&gt;[9, 12, 5, 8, 3, 3, 5, 15, 0, 6, 11, 11, 12, 8, 1, 3, 1, 11, 11, 14, 5, 1, 2, 1, 6...],
"c"=&gt;[6, 7, 8, 2, 12, 10, 9, 10, 6, 1, 10, 8, 6, 4, 6, 4, 3, 2, 7, 11, 1, 8, 7, 2, 13...],
"d"=&gt;[2, 14, 4, 0, 14, 12, 11, 0, 8, 0, 15, 3, 8, 12, 5, 0, 10, 1, 3, 4, 12, 12, 8, 14, 8...],
"e"=&gt;[10, 8, 12, 6, 11, 13, 1, 6, 13, 5, 7, 14, 11, 5, 9, 5, 2, 15, 14, 10, 10, 2, 10, 4, 1...],
"f"=&gt;[3, 6, 1, 9, 4, 14, 15, 7, 5, 15, 9, 6, 13, 13, 13, 8, 5, 3, 13, 12, 3, 15, 9, 7, 10...]
}</pre>
<p>然后将每个字符的500个排名进行平均，得出以下示例输出：</p>
<pre>"f", 5.302
"0", 7.17
"6", 7.396
"3", 7.472
"5", 7.562
"a", 7.602
"2", 7.608
"8", 7.626
"9", 7.688
"b", 7.698
"1", 7.704
"e", 7.812
"4", 7.82
"d", 7.826
"7", 7.854
"c", 7.86</pre>
<p>于是，<code>f</code> 就这样脱颖而出了。然后，再对剩余的39个字符重复此算法。</p>
<p><strong>这是一种统计技术，可让我们从噪声中滤出真实的信号</strong>。因此，总共需要调用：16 * 500 * 40 = 320,000个请求，而蛮力穷举需要花费16 ^ 40个请求。</p>
<p>另外，学术界的这篇论文就宣称用这种计时攻击的方法破解了 OpenSSL 0.9.7 的RSA加密算法了。这篇 <a href="http://crypto.stanford.edu/~dabo/papers/ssl-timing.pdf" target="_blank" rel="noopener noreferrer">Remote Timing Attacks are Practical （PDF）</a>论文中指出（我大致翻译下摘要，感兴趣的同学可以通过链接去看原文）：</p>
<blockquote><p>计时攻击往往用于攻击一些性能较弱的计算设备，例如一些智能卡。我们通过实验发现，也能用于攻击普通的软件系统。本文通过实验证明，通过这种计时攻击方式能够攻破一个基于 OpenSSL 的 web 服务器的私钥。结果证明计时攻击用于进行网络攻击在实践中可行的，因此各大安全系统需要抵御这种风险。</p></blockquote>
<p>参考资料：</p>
<ul>
<li>
<section><a href="http://www.cs.sjsu.edu/faculty/stamp/students/article.html">Timing Attacks on RSA: Revealing Your Secrets through the Fourth Dimension</a></section>
</li>
<li>
<section><a href="http://crypto.stanford.edu/~dabo/papers/ssl-timing.pdf">Remote Timing Attacks are Practical</a></section>
</li>
</ul>
<h4>各语言的对应函数</h4>
<p>下面，我们来看看各个语言对计时攻击的对应函数</p>
<p><strong>PHP</strong>: <a href="https://wiki.php.net/rfc/timing_attack" target="_blank" rel="noopener noreferrer">https://wiki.php.net/rfc/timing_attack</a></p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">bool hash_equals ( string $known_string , string $user_string )

boolean password_verify ( string $password , string $hash )</pre>
<p><strong>Java</strong>:  Java 在1.6时是有问题的，其在 1.6.0._17(6u17)才Fix了这个问题（<a href="http://hg.openjdk.java.net/jdk6/jdk6/jdk/rev/562da0baf70b" target="_blank" rel="noopener noreferrer">相关的fix patch</a>），下面是 <a href="https://hg.openjdk.java.net/jdk8u/jdk8u-dev/jdk/file/1832c29655eb/src/share/classes/java/security/MessageDigest.java#l442" target="_blank" rel="noopener noreferrer">JDK8源码</a> &#8211; <code>MessageDigest.isEqual()</code></p>
<pre class="EnlighterJSRAW" data-enlighter-language="java">public static boolean MessageDigest.isEqual(byte[] digesta, byte[] digestb) {
    if (digesta == digestb) return true;
    if (digesta == null || digestb == null) {
        return false;
    }
    if (digesta.length != digestb.length) {
        return false;
    }

    int result = 0;
    // time-constant comparison
    for (int i = 0; i &lt; digesta.length; i++) {
        result |= digesta[i] ^ digestb[i];
    }
    return result == 0;
}</pre>
<p><strong>C/C++</strong>：没有在常用的库中找到相关的函数，还是自己写吧。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="c">int util_cmp_const(const void * a, const void *b, const size_t size) 
{
  const unsigned char *_a = (const unsigned char *) a;
  const unsigned char *_b = (const unsigned char *) b;
  unsigned char result = 0;
  size_t i;

  for (i = 0; i &lt; size; i++) {
    result |= _a[i] ^ _b[i];
  }

  return result; /* returns 0 if equal, nonzero otherwise */
}</pre>
<p><strong>Python</strong> &#8211; 2.7.7+使用 <code>hmac.compare_digest(a, b)</code>，否则，使用如下的Django的代码</p>
<pre class="EnlighterJSRAW" data-enlighter-language="python">#Taken from Django Source Code

def constant_time_compare(val1, val2):
    """
    Returns True if the two strings are equal, False otherwise.

    The time taken is independent of the number of characters that match.

    For the sake of simplicity, this function executes in constant time only
    when the two strings have the same length. It short-circuits when they
    have different lengths.
    """
    if len(val1) != len(val2):
        return False
    result = 0
    for x, y in zip(val1, val2):
        result |= ord(x) ^ ord(y)
    return result == 0</pre>
<p><strong>Go</strong>  &#8211; 使用 <code>crypto/subtle</code> 代码包</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func ConstantTimeByteEq(x, y uint8) int
func ConstantTimeCompare(x, y []byte) int
func ConstantTimeCopy(v int, x, y []byte)
func ConstantTimeEq(x, y int32) int
func ConstantTimeLessOrEq(x, y int) int
func ConstantTimeSelect(v, x, y int) int</pre>
<h4>One More Thing</h4>
<p>在文章结束前，再提一个事。</p>
<p>上面的所有的代码都还有一个问题——他们都要判断字符串的长度是否一致，如果不一致就返回了，所以，通过时间攻击是可以知道字符串的长度的。比如：你的密码长度。理论上来说，字符串的长度也应该属于“隐私数据”（当然，对于签名则不是）。</p>
<p>(全文完)<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/21708.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2022/01/iStock-1175502114-150x150.png" alt="网络数字身份认证术" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21708.html" class="wp_rp_title">网络数字身份认证术</a></li><li ><a href="https://coolshell.cn/articles/21672.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2021/12/bachelor-mechanical-eng-icon@72x-150x150.png" alt="我做系统架构的一些原则" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21672.html" class="wp_rp_title">我做系统架构的一些原则</a></li><li ><a href="https://coolshell.cn/articles/19395.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2019/05/Authorization-360x200-1-150x150.png" alt="HTTP API 认证授权术" width="150" height="150" /></a><a href="https://coolshell.cn/articles/19395.html" class="wp_rp_title">HTTP API 认证授权术</a></li><li ><a href="https://coolshell.cn/articles/3258.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/3.jpg" alt="C++的字符串格式化库" width="150" height="150" /></a><a href="https://coolshell.cn/articles/3258.html" class="wp_rp_title">C++的字符串格式化库</a></li><li ><a href="https://coolshell.cn/articles/3385.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2010/12/rank_scatter1-150x150.png" alt="编程语言流行度" width="150" height="150" /></a><a href="https://coolshell.cn/articles/3385.html" class="wp_rp_title">编程语言流行度</a></li><li ><a href="https://coolshell.cn/articles/12176.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2014/12/011-150x150.jpg" alt="C++ 对象的内存布局" width="150" height="150" /></a><a href="https://coolshell.cn/articles/12176.html" class="wp_rp_title">C++ 对象的内存布局</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/21003.html">计时攻击 Timing Attacks</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/21003.html/feed</wfw:commentRss>
			<slash:comments>46</slash:comments>
		
		
			</item>
		<item>
		<title>与程序员相关的CPU缓存知识</title>
		<link>https://coolshell.cn/articles/20793.html</link>
					<comments>https://coolshell.cn/articles/20793.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Sun, 01 Mar 2020 11:43:41 +0000</pubDate>
				<category><![CDATA[程序设计]]></category>
		<category><![CDATA[编程语言]]></category>
		<category><![CDATA[cache]]></category>
		<category><![CDATA[CPU]]></category>
		<category><![CDATA[性能调优]]></category>
		<guid isPermaLink="false">https://coolshell.cn/?p=20793</guid>

					<description><![CDATA[<p>好久没有写一些微观方面的文章了，今天写一篇关于CPU Cache相关的文章，这篇文章比较长，主要分成这么几个部分：基础知识、缓存的命中、缓存的一致性、相关的代码...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/20793.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/20793.html">与程序员相关的CPU缓存知识</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><img decoding="async" loading="lazy" class="alignright size-medium wp-image-20817" src="https://coolshell.cn/wp-content/uploads/2020/03/cpu_512x512-300x300.png" alt="" width="300" height="300" srcset="https://coolshell.cn/wp-content/uploads/2020/03/cpu_512x512-300x300.png 300w, https://coolshell.cn/wp-content/uploads/2020/03/cpu_512x512-150x150.png 150w, https://coolshell.cn/wp-content/uploads/2020/03/cpu_512x512-200x200.png 200w, https://coolshell.cn/wp-content/uploads/2020/03/cpu_512x512-270x270.png 270w, https://coolshell.cn/wp-content/uploads/2020/03/cpu_512x512.png 512w" sizes="(max-width: 300px) 100vw, 300px" />好久没有写一些微观方面的文章了，今天写一篇关于CPU Cache相关的文章，这篇文章比较长，主要分成这么几个部分：基础知识、缓存的命中、缓存的一致性、相关的代码示例和延伸阅读。其中会讲述一些多核 CPU 的系统架构以及其原理，包括对程序性能上的影响，以及在进行并发编程的时候需要注意到的一些问题。这篇文章我会尽量地写简单和通俗易懂一些，主要是讲清楚相关的原理和问题，而对于一些细节和延伸阅读我会在文章最后会给出相关的资源。</p>
<p>因为无论你写什么样的代码都会交给CPU来执行，所以，如果你想写出性能比较高的代码，这篇文章中提到的技术还是值得认真学习的。另外，千万别觉得这些东西没用，这些东西非常有用，十多年前就是这些知识在性能调优上帮了我的很多大忙，从而跟很多人拉开了差距……</p>
<h4>基础知识</h4>
<p>首先，我们都知道现在的CPU多核技术，都会有几级缓存，老的CPU会有两级内存（L1和L2），新的CPU会有三级内存（L1，L2，L3 ），如下图所示：</p>
<p><img decoding="async" loading="lazy" class="aligncenter size-full wp-image-20794" src="https://coolshell.cn/wp-content/uploads/2020/02/cache.architecture.png" alt="" width="729" height="371" srcset="https://coolshell.cn/wp-content/uploads/2020/02/cache.architecture.png 729w, https://coolshell.cn/wp-content/uploads/2020/02/cache.architecture-300x153.png 300w, https://coolshell.cn/wp-content/uploads/2020/02/cache.architecture-531x270.png 531w" sizes="(max-width: 729px) 100vw, 729px" /><span id="more-20793"></span></p>
<p>其中：</p>
<ul>
<li>L1缓存分成两种，一种是指令缓存，一种是数据缓存。L2缓存和L3缓存不分指令和数据。</li>
<li>L1和L2缓存在每一个CPU核中，L3则是所有CPU核心共享的内存。</li>
<li>L1、L2、L3的越离CPU近就越小，速度也越快，越离CPU远，速度也越慢。</li>
</ul>
<p>再往后面就是内存，内存的后面就是硬盘。我们来看一些他们的速度：</p>
<ul class="">
<li>L1 的存取速度：<strong class="hd jp">4 个CPU时钟周期</strong></li>
<li>L2 的存取速度： <strong class="hd jp">11 个CPU时钟周期</strong></li>
<li>L3 的存取速度：<strong class="hd jp">39 个CPU时钟周期</strong></li>
<li>RAM内存的存取速度<strong class="hd jp">：107 个CPU时钟周期</strong></li>
</ul>
<p>我们可以看到，L1的速度是RAM的27倍，但是L1/L2的大小基本上也就是KB级别的，L3会是MB级别的。例如：<a href="https://en.wikichip.org/wiki/intel/core_i7/i7-8700k" target="_blank" rel="noopener noreferrer">Intel Core i7-8700K</a> ，是一个6核的CPU，每核上的L1是64KB（数据和指令各32KB），L2 是 256K，L3有2MB（我的苹果电脑是<a href="https://en.wikichip.org/wiki/intel/core_i9/i9-8950hk" target="_blank" rel="noopener noreferrer"> Intel Core i9-8950HK</a>，和Core i7-8700K的Cache大小一样）。</p>
<p>我们的数据就从内存向上，先到L3，再到L2，再到L1，最后到寄存器进行CPU计算。为什么会设计成三层？这里有下面几个方面的考虑：</p>
<ul>
<li>一个方面是物理速度，如果要更大的容量就需要更多的晶体管，除了芯片的体积会变大，更重要的是大量的晶体管会导致速度下降，因为访问速度和要访问的晶体管所在的位置成反比，也就是当信号路径变长时，通信速度会变慢。这部分是物理问题。</li>
<li>另外一个问题是，多核技术中，数据的状态需要在多个CPU中进行同步，并且，我们可以看到，cache和RAM的速度差距太大，所以，多级不同尺寸的缓存有利于提高整体的性能。</li>
</ul>
<p>这个世界永远是平衡的，一面变得有多光鲜，另一面也会变得有多黑暗。建立这么多级的缓存，一定就会引入其它的问题，这里有两个比较重要的问题，</p>
<ul>
<li>一个是比较简单的缓存的命中率的问题。</li>
<li>另一个是比较复杂的缓存更新的一致性问题。</li>
</ul>
<p>尤其是第二个问题，在多核技术下，这就很像分布式的系统了，要对多个地方进行更新。</p>
<h4>缓存的命中</h4>
<p>在说明这两个问题之前。我们需要要解一个术语 Cache Line。缓存基本上来说就是把后面的数据加载到离自己近的地方，对于CPU来说，它是不会一个字节一个字节的加载的，因为这非常没有效率，一般来说都是要一块一块的加载的，对于这样的一块一块的数据单位，术语叫“Cache Line”，一般来说，一个主流的CPU的Cache Line 是 64 Bytes（也有的CPU用32Bytes和128Bytes），64Bytes也就是16个32位的整型，这就是CPU从内存中捞数据上来的最小数据单位。</p>
<p>比如：Cache Line是最小单位（64Bytes），所以先把Cache分布多个Cache Line，比如：L1有32KB，那么，32KB/64B = 512 个 Cache Line。</p>
<p>一方面，缓存需要把内存里的数据放到放进来，英文叫 CPU Associativity。Cache的数据放置的策略决定了内存中的数据块会拷贝到CPU Cache中的哪个位置上，因为Cache的大小远远小于内存，所以，需要有一种地址关联的算法，能够让内存中的数据可以被映射到Cache中来。这个有点像内存地址从逻辑地址向物理地址映射的方法，但不完全一样。</p>
<p>基本上来说，我们会有如下的一些方法。</p>
<ul>
<li>一种方法是，任何一个内存地址的数据可以被缓存在任何一个Cache Line里，这种方法是最灵活的，但是，如果我们要知道一个内存是否存在于Cache中，我们就需要进行O(n)复杂度的Cache遍历，这是很没有效率的。</li>
<li>另一种方法，为了降低缓存搜索算法，我们需要使用像Hash Table这样的数据结构，最简单的hash table就是做“求模运算”，比如：我们的L1 Cache有512个Cache Line，那么，公式：<code>（内存地址 mod 512）* 64</code> 就可以直接找到所在的Cache地址的偏移了。但是，这样的方式需要我们的程序对内存地址的访问要非常地平均，不然冲突就会非常严重。这成了一种非常理想的情况了。</li>
<li>为了避免上述的两种方案的问题，于是就要容忍一定的hash冲突，也就出现了 N-Way 关联。也就是把连续的N个Cache Line绑成一组，然后，先把找到相关的组，然后再在这个组内找到相关的Cache Line。这叫 Set Associativity。如下图所示。</li>
</ul>
<p><img decoding="async" loading="lazy" class="aligncenter size-full wp-image-20806" src="https://coolshell.cn/wp-content/uploads/2020/02/cache-associative-fill-both.png" alt="" width="546" height="271" srcset="https://coolshell.cn/wp-content/uploads/2020/02/cache-associative-fill-both.png 546w, https://coolshell.cn/wp-content/uploads/2020/02/cache-associative-fill-both-300x149.png 300w, https://coolshell.cn/wp-content/uploads/2020/02/cache-associative-fill-both-544x270.png 544w" sizes="(max-width: 546px) 100vw, 546px" /></p>
<p>对于 N-Way 组关联，可能有点不好理解，这里个例子，并多说一些细节（不然后面的代码你会不能理解），Intel 大多数处理器的L1 Cache都是32KB，8-Way 组相联，Cache Line 是64 Bytes。这意味着，</p>
<ul>
<li>32KB的可以分成，32KB / 64 = 512 条 Cache Line。</li>
<li>因为有8 Way，于是会每一Way 有 512 / 8 = 64 条 Cache Line。</li>
<li>于是每一路就有 64 x 64 = 4096 Byts 的内存。</li>
</ul>
<p>为了方便索引内存地址，</p>
<ul>
<li><strong>Tag</strong>：每条 Cache Line 前都会有一个独立分配的 24 bits来存的 tag，其就是内存地址的前24bits</li>
<li><strong>Index</strong>：内存地址后续的6个bits则是在这一Way的是Cache Line 索引，2^6 = 64 刚好可以索引64条Cache Line</li>
<li><strong>Offset</strong>：再往后的6bits用于表示在Cache Line 里的偏移量</li>
</ul>
<p>如下图所示：（图片来自《<a href="https://manybutfinite.com/post/intel-cpu-caches/" target="_blank" rel="noopener noreferrer">Cache: a place for concealment and safekeeping</a>》）</p>
<p>当拿到一个内存地址的时候，先拿出中间的 6bits 来，找到是哪组。</p>
<p style="text-align: center;"><img decoding="async" loading="lazy" class="aligncenter size-full wp-image-20809" src="https://coolshell.cn/wp-content/uploads/2020/03/L1CacheExample.png" alt="" width="687" height="461" srcset="https://coolshell.cn/wp-content/uploads/2020/03/L1CacheExample.png 687w, https://coolshell.cn/wp-content/uploads/2020/03/L1CacheExample-300x201.png 300w, https://coolshell.cn/wp-content/uploads/2020/03/L1CacheExample-402x270.png 402w" sizes="(max-width: 687px) 100vw, 687px" /></p>
<p>然后，在这一个8组的cache line中，再进行O(n) n=8 的遍历，主是要匹配前24bits的tag。如果匹配中了，就算命中，如果没有匹配到，那就是cache miss，如果是读操作，就需要进向后面的缓存进行访问了。L2/L3同样是这样的算法。而淘汰算法有两种，一种是随机一种是LRU。现在一般都是以LRU的算法（通过增加一个访问计数器来实现）</p>
<p><img decoding="async" loading="lazy" class="aligncenter size-full wp-image-20840" src="https://coolshell.cn/wp-content/uploads/2020/03/selectingCacheLine.png" alt="" width="681" height="283" srcset="https://coolshell.cn/wp-content/uploads/2020/03/selectingCacheLine.png 681w, https://coolshell.cn/wp-content/uploads/2020/03/selectingCacheLine-300x125.png 300w, https://coolshell.cn/wp-content/uploads/2020/03/selectingCacheLine-604x251.png 604w" sizes="(max-width: 681px) 100vw, 681px" /></p>
<p>这也意味着：</p>
<ul>
<li>L1 Cache 可映射 36bits 的内存地址，一共 2^36 = 64GB的内存</li>
<li>当CPU要访问一个内存的时候，通过这个内存中间的6bits 定位是哪个set，通过前 24bits 定位相应的Cache Line。</li>
<li>就像一个hash Table的数据结构一样，先是O(1)的索引，然后进入冲突搜索。</li>
<li>因为中间的 6bits 决定了一个同一个set，所以，对于一段连续的内存来说，每隔4096的内存会被放在同一个组内，导致缓存冲突。</li>
</ul>
<p>此外，当有数据没有命中缓存的时候，CPU就会以最小为Cache Line的单元向内存更新数据。当然，CPU并不一定只是更新64Bytes，因为访问主存实在是太慢了，所以，一般都会多更新一些。好的CPU会有一些预测的技术，如果找到一种pattern的话，就会预先加载更多的内存，包括指令也可以预加载。这叫 Prefetching 技术 （参看，Wikipedia 的 <a href="https://en.wikipedia.org/wiki/Cache_prefetching" target="_blank" rel="noopener noreferrer">Cache Prefetching</a> 和 <a href="http://compas.cs.stonybrook.edu/~nhonarmand/courses/sp16/cse502/slides/13-prefetch.pdf" target="_blank" rel="noopener noreferrer">纽约州立大学的 Memory Prefetching</a>）。比如，你在for-loop访问一个连续的数组，你的步长是一个固定的数，内存就可以做到prefetching。（注：指令也是以预加载的方式执行，参看本站的《<a href="https://coolshell.cn/articles/7886.html" target="_blank" rel="noopener noreferrer">代码执行的效率</a>》中的第三个示例）</p>
<p>了解这些细节，会有利于我们知道在什么情况下有可以导致缓存的失效。</p>
<h4>缓存的一致性</h4>
<p>对于主流的CPU来说，缓存的写操作基本上是两种策略（参看本站《<a href="https://coolshell.cn/articles/17416.html" target="_blank" rel="noopener noreferrer">缓存更新的套路</a>》），</p>
<ul>
<li>一种是Write Back，写操作只要在cache上，然后再flush到内存上。</li>
<li>一种是Write Through，写操作同时写到cache和内存上。</li>
</ul>
<p>为了提高写的性能，一般来说，主流的CPU（如：Intel Core i7/i9）采用的是Write Back的策略，因为直接写内存实在是太慢了。</p>
<p>好了，现在问题来了，如果有一个数据 x 在 CPU 第0核的缓存上被更新了，那么其它CPU核上对于这个数据 x 的值也要被更新，这就是缓存一致性的问题。（当然，对于我们上层的程序我们不用关心CPU多个核的缓存是怎么同步的，这对上层的代码来说都是透明的）</p>
<p>一般来说，在CPU硬件上，会有两种方法来解决这个问题。</p>
<ul>
<li><strong>Directory 协议</strong>。这种方法的典型实现是要设计一个集中式控制器，它是主存储器控制器的一部分。其中有一个目录存储在主存储器中，其中包含有关各种本地缓存内容的全局状态信息。当单个CPU Cache 发出读写请求时，这个集中式控制器会检查并发出必要的命令，以在主存和CPU Cache之间或在CPU Cache自身之间进行数据同步和传输。</li>
<li><strong>Snoopy 协议</strong>。这种协议更像是一种数据通知的总线型的技术。CPU Cache通过这个协议可以识别其它Cache上的数据状态。如果有数据共享的话，可以通过广播机制将共享数据的状态通知给其它CPU Cache。这个协议要求每个CPU Cache 都可以<strong class="hu je"><em class="io">“</em>窥探<em class="io">”</em></strong>数据事件的通知并做出相应的反应。如下图所示，有一个Snoopy Bus的总线。</li>
</ul>
<p><strong><img decoding="async" loading="lazy" class="aligncenter wp-image-20797" style="font-weight: 400;" src="https://coolshell.cn/wp-content/uploads/2020/02/The-cache-coherence-problem-Initially-processors-0-and-1-both-read-location-x.png" alt="" width="400" height="217" srcset="https://coolshell.cn/wp-content/uploads/2020/02/The-cache-coherence-problem-Initially-processors-0-and-1-both-read-location-x.png 850w, https://coolshell.cn/wp-content/uploads/2020/02/The-cache-coherence-problem-Initially-processors-0-and-1-both-read-location-x-300x163.png 300w, https://coolshell.cn/wp-content/uploads/2020/02/The-cache-coherence-problem-Initially-processors-0-and-1-both-read-location-x-768x417.png 768w, https://coolshell.cn/wp-content/uploads/2020/02/The-cache-coherence-problem-Initially-processors-0-and-1-both-read-location-x-498x270.png 498w" sizes="(max-width: 400px) 100vw, 400px" /></strong></p>
<p>因为Directory协议是一个中心式的，会有性能瓶颈，而且会增加整体设计的复杂度。而Snoopy协议更像是微服务+消息通讯，所以，现在基本都是使用Snoopy的总线的设计。</p>
<p>这里，我想多写一些细节，因为这种微观的东西，让人不自然地就会跟分布式系统关联起来，在分布式系统中我们一般用Paxos/Raft这样的分布式一致性的算法。而在CPU的微观世界里，则不必使用这样的算法，原因是因为CPU的多个核的硬件不必考虑网络会断会延迟的问题。所以，CPU的多核心缓存间的同步的核心就是要管理好数据的状态就好了。</p>
<p>这里介绍几个状态协议，先从最简单的开始，MESI协议，这个协议跟那个著名的足球运动员梅西没什么关系，其主要表示缓存数据有四个状态：Modified（已修改）, Exclusive（独占的）,Shared（共享的），Invalid（无效的）。</p>
<p>这些状态的状态机如下所示（有点复杂，你可以先不看，这个图就是想告诉你状态控制有多复杂）：</p>
<p><img decoding="async" loading="lazy" class="aligncenter size-full wp-image-20804" src="https://coolshell.cn/wp-content/uploads/2020/02/MESI.png" alt="" width="420" height="406" srcset="https://coolshell.cn/wp-content/uploads/2020/02/MESI.png 420w, https://coolshell.cn/wp-content/uploads/2020/02/MESI-300x290.png 300w, https://coolshell.cn/wp-content/uploads/2020/02/MESI-279x270.png 279w" sizes="(max-width: 420px) 100vw, 420px" /></p>
<p>下面是个示例（如果你想看一下动画演示的话，这里有一个网页（<a href="https://www.scss.tcd.ie/Jeremy.Jones/VivioJS/caches/MESIHelp.htm" target="_blank" rel="noopener noreferrer">MESI Interactive Animations</a>），你可以进行交互操作，这个动画演示中使用的Write Through算法）：</p>
<table>
<thead>
<tr>
<th>当前操作</th>
<th>CPU0</th>
<th>CPU1</th>
<th>Memory</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>1) CPU0 read(x)</td>
<td> x=1 (E)</td>
<td></td>
<td>x=1</td>
<td>只有一个CPU有 x 变量，<br />
所以，状态是 Exclusive</td>
</tr>
<tr>
<td>2) CPU1 read(x)</td>
<td> x=1 (S)</td>
<td>x=1(S)</td>
<td>x=1</td>
<td>有两个CPU都读取 x 变量，<br />
所以状态变成 Shared</td>
</tr>
<tr>
<td>3) CPU0 write(x,9)</td>
<td> x=<span style="color: #ff0000;">9</span> (M)</td>
<td>x=1(I)</td>
<td>x=1</td>
<td>变量改变，在CPU0中状态<br />
变成 Modified，在CPU1中<br />
状态变成 Invalid</td>
</tr>
<tr>
<td>4) 变量 x 写回内存</td>
<td> x=9 (M)</td>
<td>X=1(I)</td>
<td>x=9</td>
<td>目前的状态不变</td>
</tr>
<tr>
<td>5) CPU1  read(x)</td>
<td> x=9 (S)</td>
<td>x=9(S)</td>
<td>x=9</td>
<td>变量同步到所有的Cache中，<br />
状态回到Shared</td>
</tr>
</tbody>
</table>
<p>&nbsp;</p>
<p>MESI 这种协议在数据更新后，会标记其它共享的CPU缓存的数据拷贝为Invalid状态，然后当其它CPU再次read的时候，就会出现 cache miss 的问题，此时再从内存中更新数据。从内存中更新数据意味着20倍速度的降低。我们能不能直接从我隔壁的CPU缓存中更新？是的，这就可以增加很多速度了，但是状态控制也就变麻烦了。还需要多来一个状态：Owner(宿主)，用于标记，我是更新数据的源。于是，出现了 <a href="https://en.wikipedia.org/wiki/MOESI_protocol" target="_blank" rel="noopener noreferrer">MOESI 协议</a></p>
<p>MOESI协议的状态机和演示示例我就不贴了（有兴趣可以上<a href="https://inst.eecs.berkeley.edu/~cs61c/su18/disc/11/Disc11Sol.pdf" target="_blank" rel="noopener">Berkeley上看看相关的课件</a>），<strong>我们只需要理解MOESI协议允许 CPU Cache 间同步数据，于是也降低了对内存的操作</strong>，性能是非常大的提升，但是控制逻辑也非常复杂。</p>
<p>顺便说一下，与 MOESI 协议类似的一个协议是 <a href="https://en.wikipedia.org/wiki/MESIF_protocol" target="_blank" rel="noopener noreferrer">MESIF</a>，其中的 F 是 Forward，同样是把更新过的数据转发给别的 CPU Cache 但是，MOESI 中的 Owner 状态 和MESIF 中的 Forward 状态有一个非常大的不一样—— <strong>Owner状态下的数据是dirty的，还没有写回内存，Forward状态下的数据是clean的，可以丢弃而不用另行通知</strong>。</p>
<p>需要说明的是，AMD用MOESI，Intel用MESIF。所以，F 状态主要是针对 CPU L3 Cache 设计的（前面我们说过，L3是所有CPU核心共享的）。（相关的比较可以参看<a href="https://stackoverflow.com/a/49989985" target="_blank" rel="noopener noreferrer">StackOverlow上这个问题的答案</a>）</p>
<h4>程序性能</h4>
<p>了解了我们上面的这些东西后，我们来看一下对于程序的影响。</p>
<h5>示例一</h5>
<p>首先，假设我们有一个64M长的数组，设想一下下面的两个循环：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="cpp">const int LEN = 64*1024*1024;
int *arr = new int[LEN];

for (int i = 0; i &lt; LEN; i += 2) arr[i] *= i;

for (int i = 0; i &lt; LEN; i += 8) arr[i] *= i;</pre>
<p>按我们的想法来看，第二个循环要比第一个循环少4倍的计算量，其应该也是要快4倍的。但实际跑下来并不是，<strong>在我的机器上，第一个循环需要127毫秒，第二个循环则需要121毫秒，相差无几</strong>。这里最主要的原因就是 Cache Line，因为CPU会以一个Cache Line 64Bytes最小时单位加载，也就是16个32bits的整型，所以，无论你步长是2还是8，都差不多。而后面的乘法其实是不耗CPU时间的。</p>
<h5>示例二</h5>
<p>我们再来看一个与缓存命中率有关的代码，我们以一定的步长<code>increment</code> 来访问一个连续的数组。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="cpp">for (int i = 0; i &lt; 10000000; i++) {
    for (int j = 0; j &lt; size; j += increment) {
        memory[j] += j;
    }
}</pre>
<p>我们测试一下，在下表中， 表头是步长，也就是每次跳多少个整数，而纵向是这个数组可以跳几次（你可以理解为要几条Cache Line），于是表中的任何一项代表了这个数组有多少，而且步长是多少。比如：横轴是 512，纵轴是4，意思是，这个数组有 <code>4*512 = 2048</code> 个长度，访问时按512步长访问，也就是访问其中的这几项：<code>[0, 512, 1024, 1536]</code> 这四项。</p>
<p>表中同的项是，是循环1000万次的时间，单位是“微秒”（除以1000后是毫秒）</p>
<pre>| count |   1    |   16  |  512  | 1024  |
------------------------------------------
|     1 |  17539 | 16726 | 15143 | 14477 |
|     2 |  15420 | 14648 | 13552 | 13343 |
|     3 |  14716 | 14463 | 15086 | 17509 |
|     4 |  18976 | 18829 | 18961 | 21645 |
|     5 |  23693 | 23436 | 74349 | 29796 |
|     6 |  23264 | 23707 | 27005 | 44103 |
|     7 |  28574 | 28979 | 33169 | 58759 |
|     8 |  33155 | 34405 | 39339 | 65182 |
|     9 |  37088 | 37788 | 49863 |<span style="color: #cc0000;"><strong>156745</strong></span> |
|    10 |  41543 | 42103 | 58533 |<span style="color: #cc0000;"><strong>215278</strong></span> |
|    11 |  47638 | 50329 | 66620 |<span style="color: #cc0000;"><strong>335603</strong></span> |
|    12 |  49759 | 51228 | 75087 |<span style="color: #cc0000;"><strong>305075</strong></span> |
|    13 |  53938 | 53924 | 77790 |<span style="color: #cc0000;"><strong>366879</strong></span> |
|    14 |  58422 | 59565 | 90501 |<span style="color: #cc0000;"><strong>466368</strong></span> |
|    15 |  62161 | 64129 | 90814 |<span style="color: #cc0000;"><strong>525780</strong></span> |
|    16 |  67061 | 66663 | 98734 |<span style="color: #cc0000;"><strong>440558</strong></span> |
|    17 |  71132 | 69753 |<span style="color: #cc0000;"><strong>171203</strong></span> |<span style="color: #cc0000;"><strong>506631</strong></span> |
|    18 |  74102 | 73130 |<span style="color: #cc0000;"><strong>293947</strong></span> |<span style="color: #cc0000;"><strong>550920</strong></span> |
</pre>
<p>我们可以看到，从 <code>[9，1024]</code> 以后，时间显著上升。包括 <code>[17，512]</code> 和 <code>[18,512]</code> 也显著上升。这是因为，我机器的 L1 Cache 是 32KB, 8 Way 的，前面说过，8 Way的有64组，每组8个Cache Line，当for-loop步长超过1024个整型，也就是正好 4096 Bytes时，也就是导致内存地址的变化是变化在高位的24bits上，而低位的12bits变化不大，尤其是中间6bits没有变化，导致全部命中同一组set，导致大量的cache 冲突，导致性能下降，时间上升。而 [16, 512]也是一样的，其中的几步开始导致L1 Cache开始冲突失效。</p>
<h5>示例三</h5>
<p>接下来，我们再来看个示例。下面是一个二维数组的两种遍历方式，一个逐行遍历，一个是逐列遍历，这两种方式在理论上来说，寻址和计算量都是一样的，执行时间应该也是一样的。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="cpp">const int row = 1024;
const int col = 512
int matrix[row][col];

//逐行遍历
int sum_row=0;
for(int _r=0; _r&lt;row; _r++) {
    for(int _c=0; _c&lt;col; _c++){
        sum_row += matrix[_r][_c];
    }
}

//逐列遍历
int sum_col=0;
for(int _c=0; _c&lt;col; _c++) {
    for(int _r=0; _r&lt;row; _r++){
        sum_col += matrix[_r][_c];
    }
}</pre>
<p>然而，并不是，在我的机器上，得到下面的结果。</p>
<ul>
<li>逐行遍历：0.081ms</li>
<li>逐列遍历：1.069ms</li>
</ul>
<p>执行时间有十几倍的差距。其中的原因，就是逐列遍历对于CPU Cache 的运作方式并不友好，所以，付出巨大的代价。</p>
<h5>示例四</h5>
<p>接下来，我们来看一下多核下的性能问题，参看如下的代码。两个线程在操作一个数组的两个不同的元素（无需加锁），线程循环1000万次，做加法操作。在下面的代码中，我高亮了一行，就是<code>p2</code>指针，要么是<code>p[1]</code>，或是 <code>p[30]</code>，理论上来说，无论访问哪两个数组元素，都应该是一样的执行时间。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="cpp" data-enlighter-highlight="9">void fn (int* data) {
    for(int i = 0; i &lt; 10*1024*1024; ++i)
        *data += rand();
}

int p[32];

int *p1 = &amp;p[0];
int *p2 = &amp;p[1]; // int *p2 = &amp;p[30];

thread t1(fn, p1);
thread t2(fn, p2);</pre>
<p>然而，并不是，在我的机器上执行下来的结果是：</p>
<ul>
<li>对于 <code>p[0]</code> 和 <code>p[1]</code> ：560ms</li>
<li>对于 <code>p[0]</code> 和 <code>p[30]</code>：104ms</li>
</ul>
<p>这是因为 <code>p[0]</code> 和 <code>p[1]</code> 在同一条 Cache Line 上，而 <code>p[0]</code> 和 <code>p[30]</code> 则不可能在同一条Cache Line 上 ，CPU的缓存最小的更新单位是Cache Line，所以，<strong>这导致虽然两个线程在写不同的数据，但是因为这两个数据在同一条Cache Line上，就会导致缓存需要不断进在两个CPU的L1/L2中进行同步，从而导致了5倍的时间差异</strong>。</p>
<h5>示例五</h5>
<p>接下来，我们再来看一下另外一段代码：我们想统计一下一个数组中的奇数个数，但是这个数组太大了，我们希望可以用多线程来完成这个统计。下面的代码中，<strong>我们为每一个线程传入一个 id ，然后通过这个 id 来完成对应数组段的统计任务。这样可以加快整个处理速度</strong>。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="cpp">int total_size = 16 * 1024 * 1024; //数组长度
int* test_data = new test_data[total_size]; //数组
int nthread = 6; //线程数（因为我的机器是6核的）
int result[nthread]; //收集结果的数组

void thread_func (int id) {
    result[id] = 0;
    int chunk_size = total_size / nthread + 1;
    int start = id * chunk_size;
    int end = min(start + chunk_size, total_size);

    for ( int i = start; i &lt; end; ++i ) {
        if (test_data[i] % 2 != 0 ) ++result[id];
    }
}</pre>
<p>然而，在执行过程中，<strong>你会发现，6个线程居然跑不过1个线程</strong>。因为根据上面的例子你知道 <code>result[]</code> 这个数组中的数据在一个Cache Line中，所以，所有的线程都会对这个 Cache Line 进行写操作，导致所有的线程都在不断地重新同步 <code>result[]</code> 所在的 Cache Line，所以，导致 6 个线程还跑不过一个线程的结果。这叫 <strong>False Sharing</strong>。</p>
<p>优化也很简单，使用一个线程内的变量。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="cpp">void thread_func (int id) {
    result[id] = 0;
    int chunk_size = total_size / nthread + 1;
    int start = id * chunk_size;
    int end = min(start + chunk_size, total_size);

    int c = 0; //使用临时变量，没有cache line的同步了
    for ( int i = start; i &lt; end; ++i ) {
        if (test_data[i] % 2 != 0 ) ++c;
    }
    result[id] = c;
}</pre>
<p>我们把两个程序分别在 1 到 32 个线程上跑一下，得出的结果画一张图如下所示（横轴是线程数，纵轴是完成统的时间，单位是微秒）：</p>
<p><img decoding="async" loading="lazy" class="aligncenter size-large wp-image-20813" src="https://coolshell.cn/wp-content/uploads/2020/03/false.sharing-1024x643.png" alt="" width="640" height="402" srcset="https://coolshell.cn/wp-content/uploads/2020/03/false.sharing-1024x643.png 1024w, https://coolshell.cn/wp-content/uploads/2020/03/false.sharing-300x188.png 300w, https://coolshell.cn/wp-content/uploads/2020/03/false.sharing-768x482.png 768w, https://coolshell.cn/wp-content/uploads/2020/03/false.sharing-430x270.png 430w, https://coolshell.cn/wp-content/uploads/2020/03/false.sharing.png 1320w" sizes="(max-width: 640px) 100vw, 640px" /></p>
<p>上图中，我们可以看到，灰色的曲线就是第一种方法，橙色的就是第二种（用局部变量的）方法。当只有一个线程的时候，两个方法相当，基本没有什么差别，但是在线程数增加的时候的时候，你会发现，第二种方法的性能提高的非常快。直到到达6个线程的时候，开始变得稳定（前面说过，我的CPU是6核的）。而第一种方法无论加多少线程也没有办法超过第二种方法。因为第一种方法不是CPU Cache 友好的。也就是说，第二种方法，<strong>只要我的CPU核数足够多，就可以做到线性的性能扩展，让每一个CPU核都跑起来，而第一种则不能</strong>。</p>
<p>篇幅问题，示例就写到这里，相关的代码参看<a href="https://github.com/haoel/cpu-cache" target="_blank" rel="noopener noreferrer">我的Github相关仓库</a>。</p>
<h4>延伸阅读</h4>
<ul>
<li>Wikipedia : <a href="https://en.wikipedia.org/wiki/CPU_cache" target="_blank" rel="noopener noreferrer">CPU Cache </a></li>
<li>经典文章：<a href="http://igoro.com/archive/gallery-of-processor-cache-effects/" target="_blank" rel="noopener noreferrer">Gallery of Processor Cache Effects</a> （这篇文章中的测试已经有点过时了，但是这篇文章中所说的那些东西还是非常适用的）</li>
<li>Effective C++作者 Scott Meyers 的演讲 CPU Caches and Why You Care （<a href="https://www.youtube.com/watch?v=WDIkqP4JbkE" target="_blank" rel="noopener noreferrer">Youtube</a>，<a href="https://www.aristeia.com/TalkNotes/codedive-CPUCachesHandouts.pdf" target="_blank" rel="noopener noreferrer">PPT</a>）</li>
<li>美国私立大学Swarthmore的教材 <a href="https://www.cs.swarthmore.edu/~kwebb/cs31/f18/memhierarchy/caching.html" target="_blank" rel="noopener noreferrer">Cache Architecture and Design</a></li>
<li>经典文章：<a href="https://people.freebsd.org/~lstewart/articles/cpumemory.pdf" target="_blank" rel="noopener noreferrer">What Every Programmer Should Know About Memory</a> （这篇文章非常经典，但是开篇太晦涩了，居然告诉你晶体管内的构造，第三章和第六章是重点）</li>
<li>Nonblocking Algorithms and Scalable Multicore Programming （<a href="https://queue.acm.org/detail.cfm?id=2492433" target="_blank" rel="noopener noreferrer">英文版</a>，<a href="https://www.oschina.net/translate/nonblocking-algorithms-and-scalable-multicore-programming" target="_blank" rel="noopener noreferrer">中文版</a>）</li>
<li>Github上的一个代码库 <a href="https://github.com/Kobzol/hardware-effects" target="_blank" rel="noopener noreferrer">hardware-effects</a> 里面有受CPU影响的程序的演示</li>
<li>Optimizing for instruction caches （<a href="https://www.eetimes.com/optimizing-for-instruction-caches-part-1/" target="_blank" rel="noopener noreferrer">Part 1</a>，<a href="https://www.eetimes.com/optimizing-for-instruction-caches-part-2/" target="_blank" rel="noopener noreferrer">Part 2</a>， <a href="https://www.eetimes.com/optimizing-for-instruction-caches-part-3/">Part 3</a>）</li>
<li>经典数据：<a href="https://gist.github.com/jboner/2841832" target="_blank" rel="noopener noreferrer">Latency Numbers Every Programmer Should Know</a></li>
<li>关于Java的可以看一下这篇<a href="https://dzone.com/articles/optimizing-memory-access-with-cpu-cache" target="_blank" rel="noopener noreferrer">Optimizing Memory Access With CPU Cache</a> 或是 <a href="https://www.stardog.com/blog/writing-cache-friendly-code/" target="_blank" rel="noopener noreferrer">Writing cache-friendly code</a></li>
</ul>
<p>总之，这个CPU Cache的调优技术不是什么新鲜的东西，只要Google就能找到有很多很多文章……</p>
<p>（全文完）<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/10249.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2013/07/image6-150x150.png" alt="7个示例科普CPU Cache" width="150" height="150" /></a><a href="https://coolshell.cn/articles/10249.html" class="wp_rp_title">7个示例科普CPU Cache</a></li><li ><a href="https://coolshell.cn/articles/17416.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2016/07/cache-150x150.png" alt="缓存更新的套路" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17416.html" class="wp_rp_title">缓存更新的套路</a></li><li ><a href="https://coolshell.cn/articles/2039.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/10.jpg" alt="CPU的性价比" width="150" height="150" /></a><a href="https://coolshell.cn/articles/2039.html" class="wp_rp_title">CPU的性价比</a></li><li ><a href="https://coolshell.cn/articles/360.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/16.jpg" alt="Linux C 编程一站式学习" width="150" height="150" /></a><a href="https://coolshell.cn/articles/360.html" class="wp_rp_title">Linux C 编程一站式学习</a></li><li ><a href="https://coolshell.cn/articles/6441.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/18.jpg" alt="Javascript 面向对象编程" width="150" height="150" /></a><a href="https://coolshell.cn/articles/6441.html" class="wp_rp_title">Javascript 面向对象编程</a></li><li ><a href="https://coolshell.cn/articles/2589.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2010/07/get_more_web_traffic-150x150.jpg" alt="十个免费的Web压力测试工具" width="150" height="150" /></a><a href="https://coolshell.cn/articles/2589.html" class="wp_rp_title">十个免费的Web压力测试工具</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/20793.html">与程序员相关的CPU缓存知识</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/20793.html/feed</wfw:commentRss>
			<slash:comments>107</slash:comments>
		
		
			</item>
		<item>
		<title>HTTP的前世今生</title>
		<link>https://coolshell.cn/articles/19840.html</link>
					<comments>https://coolshell.cn/articles/19840.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Tue, 01 Oct 2019 11:21:10 +0000</pubDate>
				<category><![CDATA[技术读物]]></category>
		<category><![CDATA[程序设计]]></category>
		<category><![CDATA[系统架构]]></category>
		<category><![CDATA[网络安全]]></category>
		<category><![CDATA[HTTP]]></category>
		<category><![CDATA[QUIC]]></category>
		<category><![CDATA[TCP]]></category>
		<category><![CDATA[TLS]]></category>
		<category><![CDATA[UDP]]></category>
		<guid isPermaLink="false">https://coolshell.cn/?p=19840</guid>

					<description><![CDATA[<p>HTTP (Hypertext transfer protocol) 翻译成中文是超文本传输协议，是互联网上重要的一个协议，由欧洲核子研究委员会CERN的英国工...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/19840.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/19840.html">HTTP的前世今生</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><img decoding="async" loading="lazy" class="alignright" src="https://coolshell.cn/wp-content/uploads/2019/10/HTTP-770x513-300x200.jpg" alt="" width="300" height="200" />HTTP (Hypertext transfer protocol) 翻译成中文是超文本传输协议，是互联网上重要的一个协议，由欧洲核子研究委员会CERN的英国工程师 <a title="" href="https://en.wikipedia.org/wiki/Tim_Berners-Lee">Tim Berners-Lee</a> v发明的，同时，他也是WWW的发明人，最初的主要是用于传递通过HTML封装过的数据。在1991年发布了HTTP 0.9版，在1996年发布1.0版，1997年是1.1版，1.1版也是到今天为止传输最广泛的版本（初始<a class="external text" href="https://tools.ietf.org/html/rfc2068" rel="nofollow">RFC 2068</a> 在1997年发布， 然后在1999年被 <a class="external text" href="https://tools.ietf.org/html/rfc2616" rel="nofollow">RFC 2616</a> 取代，再在2014年被 <a class="external text" href="https://tools.ietf.org/html/rfc7230" rel="nofollow">RFC 7230</a> /<a class="external text" href="https://tools.ietf.org/html/rfc7231" rel="nofollow">7231</a>/<a class="external text" href="https://tools.ietf.org/html/rfc7232" rel="nofollow">7232</a>/<a class="external text" href="https://tools.ietf.org/html/rfc7233" rel="nofollow">7233</a>/<a class="external text" href="https://tools.ietf.org/html/rfc7234" rel="nofollow">7234</a>/<a class="external text" href="https://tools.ietf.org/html/rfc7235" rel="nofollow">7235</a>取代），2015年发布了2.0版，其极大的优化了HTTP/1.1的性能和安全性，而2018年发布的3.0版，继续优化HTTP/2，激进地使用UDP取代TCP协议，目前，HTTP/3 在2019年9月26日 被 Chrome，Firefox，和Cloudflare支持，所以我想写下这篇文章，简单地说一下HTTP的前世今生，让大家学到一些知识，并希望可以在推动一下HTTP标准协议的发展。</p>
<h4>HTTP 0.9 / 1.0</h4>
<p>0.9和1.0这两个版本，就是最传统的 request &#8211; response的模式了，HTTP 0.9版本的协议简单到极点，请求时，不支持请求头，只支持 <code>GET</code> 方法，没了。HTTP 1.0 扩展了0.9版，其中主要增加了几个变化：</p>
<p><span id="more-19840"></span></p>
<ul>
<li>在请求中加入了HTTP版本号，如：<code>GET /coolshell/index.html HTTP/1.0</code></li>
<li>HTTP 开始有 header了，不管是request还是response 都有header了。</li>
<li>增加了HTTP Status Code 标识相关的状态码。</li>
<li>还有 <code>Content-Type</code> 可以传输其它的文件了。</li>
</ul>
<p>我们可以看到，HTTP 1.0 开始让这个协议变得很文明了，一种工程文明。因为：</p>
<ul>
<li>一个协议有没有版本管理，是一个工程化的象征。</li>
<li>header是协议可以说是把元数据和业务数据解耦，也可以说是控制逻辑和业务逻辑的分离。</li>
<li>Status Code 的出现可以让请求双方以及第三方的监控或管理程序有了统一的认识。最关键是还是控制错误和业务错误的分离。</li>
</ul>
<p>（注：国内很多公司HTTP无论对错只返回200，这种把HTTP Status Code 全部抹掉完全是一种工程界的倒退）</p>
<p>但是，HTTP1.0性能上有一个很大的问题，那就是每请求一个资源都要新建一个TCP链接，而且是串行请求，所以，就算网络变快了，打开网页的速度也还是很慢。所以，HTTP 1.0 应该是一个必需要淘汰的协议了。</p>
<h4> HTTP/1.1</h4>
<p>HTTP/1.1 主要解决了HTTP 1.0的网络性能的问题，以及增加了一些新的东西：</p>
<ul>
<li>可以设置 <code>keepalive</code> 来让HTTP重用TCP链接，重用TCP链接可以省了每次请求都要在广域网上进行的TCP的三次握手的巨大开销。这是所谓的“<strong>HTTP 长链接</strong>” 或是 “<strong>请求响应式的HTTP 持久链接</strong>”。英文叫 HTTP Persistent connection.</li>
<li>然后支持pipeline网络传输，只要第一个请求发出去了，不必等其回来，就可以发第二个请求出去，可以减少整体的响应时间。（注：非幂等的POST 方法或是有依赖的请求是不能被pipeline化的）</li>
<li>支持 Chunked Responses ，也就是说，在Response的时候，不必说明 <code>Content-Length</code> 这样，客户端就不能断连接，直到收到服务端的EOF标识。这种技术又叫 “<strong>服务端Push模型</strong>”，或是 “<strong>服务端Push式的HTTP 持久链接</strong>”</li>
<li>还增加了 cache control 机制。</li>
<li>协议头注增加了 Language, Encoding, Type 等等头，让客户端可以跟服务器端进行更多的协商。</li>
<li>还正式加入了一个很重要的头—— <code><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Host" target="_blank" rel="noopener noreferrer">HOST</a></code>这样的话，服务器就知道你要请求哪个网站了。因为可以有多个域名解析到同一个IP上，要区分用户是请求的哪个域名，就需要在HTTP的协议中加入域名的信息，而不是被DNS转换过的IP信息。</li>
<li>正式加入了 <code>OPTIONS</code> 方法，其主要用于 <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS" target="_blank" rel="noopener noreferrer">CORS &#8211; Cross Origin Resource Sharing</a> 应用。</li>
</ul>
<p>HTTP/1.1应该分成两个时代，一个是2014年前，一个是2014年后，因为2014年HTTP/1.1有了一组RFC（<a class="external text" href="https://tools.ietf.org/html/rfc7230" rel="nofollow">7230</a> /<a class="external text" href="https://tools.ietf.org/html/rfc7231" rel="nofollow">7231</a>/<a class="external text" href="https://tools.ietf.org/html/rfc7232" rel="nofollow">7232</a>/<a class="external text" href="https://tools.ietf.org/html/rfc7233" rel="nofollow">7233</a>/<a class="external text" href="https://tools.ietf.org/html/rfc7234" rel="nofollow">7234</a>/<a class="external text" href="https://tools.ietf.org/html/rfc7235" rel="nofollow">7235</a>），这组RFC又叫“HTTP/2 预览版”。其中影响HTTP发展的是两个大的需求：</p>
<ul>
<li>一个需要是加大了HTTP的安全性，这样就可以让HTTP应用得广泛，比如，使用TLS协议。</li>
<li>另一个是让HTTP可以支持更多的应用，在HTTP/1.1 下，HTTP已经支持四种网络协议：
<ul>
<li>传统的短链接。</li>
<li>可重用TCP的的长链接模型。</li>
<li>服务端push的模型。</li>
<li>WebSocket模型。</li>
</ul>
</li>
</ul>
<p>自从2005年以来，整个世界的应用API越来多，这些都造就了整个世界在推动HTTP的前进，我们可以看到，<strong>自2014的HTTP/1.1 以来，这个世界基本的应用协议的标准基本上都是向HTTP看齐了，也许2014年前，还有一些专用的RPC协议，但是2014年以后，HTTP协议的增强，让我们实在找不出什么理由不向标准靠拢，还要重新发明轮子了。</strong></p>
<h4>HTTP/2</h4>
<p>虽然 HTTP/1.1 已经开始变成应用层通讯协议的一等公民了，但是还是有性能问题，虽然HTTP/1.1 可以重用TCP链接，但是请求还是一个一个串行发的，需要保证其顺序。然而，大量的网页请求中都是些资源类的东西，这些东西占了整个HTTP请求中最多的传输数据量。所以，理论上来说，如果能够并行这些请求，那就会增加更大的网络吞吐和性能。</p>
<p>另外，HTTP/1.1传输数据时，是以文本的方式，借助耗CPU的zip压缩的方式减少网络带宽，但是耗了前端和后端的CPU。这也是为什么很多RPC协议诟病HTTP的一个原因，就是数据传输的成本比较大。</p>
<p>其实，在2010年时，Google 就在搞一个实验型的协议，这个协议叫<a href="https://en.wikipedia.org/wiki/SPDY">SPDY</a>，这个协议成为了HTTP/2的基础（也可以说成HTTP/2就是SPDY的复刻）。HTTP/2基本上解决了之前的这些性能问题，其和HTTP/1.1最主要的不同是：</p>
<ul>
<li>HTTP/2是一个二进制协议，增加了数据传输的效率。</li>
<li>HTTP/2是可以在一个TCP链接中并发请求多个HTTP请求，移除了HTTP/1.1中的串行请求。</li>
<li>HTTP/2会压缩头，如果你同时发出多个请求，他们的头是一样的或是相似的，那么，协议会帮你消除重复的部分。这就是所谓的HPACK算法（参看<a class="external mw-magiclink-rfc" href="https://tools.ietf.org/html/rfc7541" target="_blank" rel="nofollow noopener noreferrer">RFC 7541</a> 附录A）</li>
<li>HTTP/2允许服务端在客户端放cache，又叫服务端push，也就是说，你没有请求的东西，我服务端可以先送给你放在你的本地缓存中。比如，你请求X，我服务端知道X依赖于Y，虽然你没有的请求Y，但我把把Y跟着X的请求一起返回客户端。</li>
</ul>
<p>对于这些性能上的改善，在Medium上有篇文章你可看一下相关的细节说明和测试“<a href="https://medium.com/@factoryhr/http-2-the-difference-between-http-1-1-benefits-and-how-to-use-it-38094fa0e95b" target="_blank" rel="noopener noreferrer">HTTP/2: the difference between HTTP/1.1, benefits and how to use it</a>”</p>
<p>当然，还需要注意到的是HTTP/2的协议复杂度比之前所有的HTTP协议的复杂度都上升了许多许多，其内部还有很多看不见的东西，比如其需要维护一个“优先级树”来用于来做一些资源和请求的调度和控制。如此复杂的协议，自然会产生一些不同的声音，或是降低协议的可维护和可扩展性。所以也有一些争议。尽管如此，HTTP/2还是很快地被世界所采用。</p>
<p>HTTP/2 是2015年推出的，其发布后，Google 宣布移除对SPDY的支持，拥抱标准的 HTTP/2。过了一年后，就有8.7%的网站开启了HTTP/2，根据 <a href="https://w3techs.com/technologies/details/ce-http2/all/all" target="_blank" rel="noopener noreferrer">这份报告</a> ，截止至本文发布时（2019年10月1日 ）， 在全世界范围内已经有41%的网站开启了HTTP/2。</p>
<p>HTTP/2的官方组织在 Github 上维护了一份<a href="https://github.com/http2/http2-spec/wiki/Implementations" target="_blank" rel="noopener noreferrer">各种语言对HTTP/2的实现列表</a>，大家可以去看看。</p>
<p>我们可以看到，HTTP/2 在性能上对HTTP有质的提高，所以，HTTP/2 被采用的也很快，所以，<strong>如果你在你的公司内负责架构的话，HTTP/2是你一个非常重要的需要推动的一个事，除了因为性能上的问题，推动标准落地也是架构师的主要职责，因为，你企业内部的架构越标准，你可以使用到开源软件，或是开发方式就会越有效率，跟随着工业界的标准的发展，你的企业会非常自然的享受到标准所带来的红利。</strong></p>
<h4>HTTP/3</h4>
<p>然而，这个世界没有完美的解决方案，HTTP/2也不例外，其主要的问题是：若干个HTTP的请求在复用一个TCP的连接，底层的TCP协议是不知道上层有多少个HTTP的请求的，所以，一旦发生丢包，造成的问题就是所有的HTTP请求都必需等待这个丢了的包被重传回来，哪怕丢的那个包不是我这个HTTP请求的。因为TCP底层是没有这个知识了。</p>
<p>这个问题又叫<a href="https://en.wikipedia.org/wiki/Head-of-line_blocking" target="_blank" rel="noopener noreferrer">Head-of-Line Blocking</a>问题，这也是一个比较经典的流量调度的问题。这个问题最早主要的发生的交换机上。下图来自Wikipedia。</p>
<p><img decoding="async" loading="lazy" class="aligncenter" src="https://coolshell.cn/wp-content/uploads/2019/10/HOL_blocking.png" alt="" width="423" height="220" /></p>
<p>图中，左边的是输入队列，其中的1，2，3，4表示四个队列，四个队列中的1，2，3，4要去的右边的output的端口号。此时，第一个队列和第三个队列都要写右边的第四个端口，然后，一个时刻只能处理一个包，所以，一个队列只能在那等另一个队列写完后。然后，其此时的3号或1号端口是空闲的，而队列中的要去1和3号端号的数据，被第四号端口给block住了。这就是所谓的HOL blocking问题。</p>
<p>HTTP/1.1中的pipeline中如果有一个请求block了，那么队列后请求也统统被block住了；HTTP/2 多请求复用一个TCP连接，一旦发生丢包，就会block住所有的HTTP请求。这样的问题很讨厌。好像基本无解了。</p>
<p>是的TCP是无解了，但是UDP是有解的 ！<strong>于是HTTP/3破天荒地把HTTP底层的TCP协议改成了UDP！</strong></p>
<p>然后又是Google 家的协议进入了标准 &#8211; QUIC （Quick UDP Internet Connections）。接下来是QUIC协议的几个重要的特性，为了讲清楚这些特性，我需要带着问题来讲（注：下面的网络知识，如果你看不懂的话，你需要学习一下《<a href="https://book.douban.com/subject/1088054/" target="_blank" rel="noopener noreferrer">TCP/IP详解</a>》一书（在我写blog的这15年里，这本书推荐了无数次了），或是看一下本站的《<a href="https://coolshell.cn/articles/11564.html">TCP的那些事</a>》。）：</p>
<ul>
<li>首先是上面的Head-of-Line blocking问题，在UDP的世界中，这个就没了。这个应该比较好理解，因为UDP不管顺序，不管丢包（当然，QUIC的一个任务是要像TCP的一个稳定，所以QUIC有自己的丢包重传的机制）</li>
<li>TCP是一个无私的协议，也就是说，如果网络上出现拥塞，大家都会丢包，于是大家都会进入拥塞控制的算法中，这个算法会让所有人都“冷静”下来，然后进入一个“慢启动”的过程，包括在TCP连接建立时，这个慢启动也在，所以导致TCP性能迸发地比较慢。QUIC基于UDP，使用更为激进的方式。同时，QUIC有一套自己的丢包重传和拥塞控制的协，一开始QUIC是重新实现一TCP 的 CUBIC算法，但是随着BBR算法的成熟（BBR也在借鉴CUBIC算法的数学模型），QUIC也可以使用BBR算法。这里，多说几句，<strong>从模型来说，以前的TCP的拥塞控制算法玩的是数学模型，而新型的TCP拥塞控制算法是以BBR为代表的测量模型</strong>，理论上来说，后者会更好，但QUIC的团队在一开始觉得BBR不如CUBIC的算法好，所以没有用。现在的BBR 2.x借鉴了CUBIC数学模型让拥塞控制更公平。这里有文章大家可以一读“<a href="https://medium.com/google-cloud/tcp-bbr-magic-dust-for-network-performance-57a5f1ccf437" target="_blank" rel="noopener noreferrer">TCP BBR : Magic dust for network performance.</a>”</li>
<li>接下来，现在要建立一个HTTPS的连接，先是TCP的三次握手，然后是TLS的三次握手，要整出六次网络交互，一个链接才建好，虽说HTTP/1.1和HTTP/2的连接复用解决这个问题，但是基于UDP后，UDP也得要实现这个事。于是QUIC直接把TCP的和TLS的合并成了三次握手（对此，在HTTP/2的时候，是否默认开启TLS业内是有争议的，反对派说，TLS在一些情况下是不需要的，比如企业内网的时候，而支持派则说，TLS的那些开销，什么也不算了）。</li>
</ul>
<table>
<tbody>
<tr>
<td><img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2019/10/http-request-over-tcp-tls@2x-292x300.png" alt="" width="292" height="300" /></td>
<td><img decoding="async" loading="lazy" class="" src="https://coolshell.cn/wp-content/uploads/2019/10/http-request-over-quic@2x-300x215.png" alt="" width="312" height="227" /></td>
</tr>
</tbody>
</table>
<p>&nbsp;</p>
<p>所以，QUIC是一个在UDP之上的伪TCP +TLS +HTTP/2的多路复用的协议。</p>
<p>但是对于UDP还是有一些挑战的，这个挑战主要来自互联网上的各种网络设备，这些设备根本不知道是什么QUIC，他们看QUIC就只能看到的就是UDP，所以，在一些情况下，UDP就是有问题的，</p>
<ul>
<li>比如在NAT的环境下，如果是TCP的话，NAT路由或是代理服务器，可以通过记录TCP的四元组（源地址、源端口，目标地址，目标端口）来做连接映射的，然而，在UDP的情况下不行了。于是，QUIC引入了个叫connection id的不透明的ID来标识一个链接，用这种业务ID很爽的一个事是，如果你从你的3G/4G的网络切到WiFi网络（或是反过来），你的链接不会断，因为我们用的是connection id，而不是四元组。</li>
</ul>
<ul>
<li>然而就算引用了connection id，也还是会有问题 ，比如一些不够“聪明”的等价路由交换机，这些交换机会通过四元组来做hash把你的请求的IP转到后端的实际的服务器上，然而，他们不懂connection id，只懂四元组，这么导致属于同一个connection id但是四元组不同的网络包就转到了不同的服务器上，这就是导致数据不能传到同一台服务器上，数据不完整，链接只能断了。所以，你需要更聪明的算法（可以参看 Facebook 的 <a href="https://github.com/facebookincubator/katran" target="_blank" rel="noopener noreferrer">Katran</a> 开源项目 ）</li>
</ul>
<p>好了，就算搞定上面的东西，还有一些业务层的事没解，这个事就是 HTTP/2的头压缩算法 HPACK，HPACK需要维护一个动态的字典表来分析请求的头中哪些是重复的，HPACK的这个数据结构需要在encoder和decoder端同步这个东西。在TCP上，这种同步是透明的，然而在UDP上这个事不好干了。所以，这个事也必需要重新设计了，基于QUIC的QPACK就出来了，利用两个附加的QUIC steam，一个用来发送这个字典表的更新给对方，另一个用来ack对方发过来的update。</p>
<p>目前看下来，HTTP/3目前看上去没有太多的协议业务逻辑上的东西，更多是HTTP/2 + QUIC协议。但，HTTP/3 因为动到了底层协议，所以，在普及方面上可能会比 HTTP/2要慢的多的多。但是，可以看到QUIC协议的强大，细思及恐，QUIC这个协议真对TCP是个威胁，如果QUIC成熟了，TCP是不是会有可能成为历史呢？</p>
<p>未来十年，让我们看看UDP是否能够逆袭TCP……</p>
<p>(全文完)<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/7490.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2012/06/f1-150x150.jpg" alt="性能调优攻略" width="150" height="150" /></a><a href="https://coolshell.cn/articles/7490.html" class="wp_rp_title">性能调优攻略</a></li><li ><a href="https://coolshell.cn/articles/22263.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2022/07/wall_clock-300x167-1-150x150.jpeg" alt="从一次经历谈 TIME_WAIT 的那些事" width="150" height="150" /></a><a href="https://coolshell.cn/articles/22263.html" class="wp_rp_title">从一次经历谈 TIME_WAIT 的那些事</a></li><li ><a href="https://coolshell.cn/articles/22173.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2022/02/http_method-150x150.png" alt="“一把梭：REST API 全用 POST”" width="150" height="150" /></a><a href="https://coolshell.cn/articles/22173.html" class="wp_rp_title">“一把梭：REST API 全用 POST”</a></li><li ><a href="https://coolshell.cn/articles/21708.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2022/01/iStock-1175502114-150x150.png" alt="网络数字身份认证术" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21708.html" class="wp_rp_title">网络数字身份认证术</a></li><li ><a href="https://coolshell.cn/articles/18094.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2017/08/enable-https-banner-150x150.png" alt="如何免费的让网站启用HTTPS" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18094.html" class="wp_rp_title">如何免费的让网站启用HTTPS</a></li><li ><a href="https://coolshell.cn/articles/11609.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2014/05/xin_2001040422167711230318-150x150.jpg" alt="TCP 的那些事儿（下）" width="150" height="150" /></a><a href="https://coolshell.cn/articles/11609.html" class="wp_rp_title">TCP 的那些事儿（下）</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/19840.html">HTTP的前世今生</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/19840.html/feed</wfw:commentRss>
			<slash:comments>77</slash:comments>
		
		
			</item>
	</channel>
</rss>
