<?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>Go | 酷 壳 - CoolShell</title>
	<atom:link href="https://coolshell.cn/tag/go/feed" rel="self" type="application/rss+xml" />
	<link>https://coolshell.cn</link>
	<description>享受编程和技术所带来的快乐 - Coding Your Ambition</description>
	<lastBuildDate>Fri, 06 May 2022 02:17:19 +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>Go编程模式 ： 泛型编程</title>
		<link>https://coolshell.cn/articles/21615.html</link>
					<comments>https://coolshell.cn/articles/21615.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Sat, 04 Sep 2021 05:44:02 +0000</pubDate>
				<category><![CDATA[Go 语言]]></category>
		<category><![CDATA[编程语言]]></category>
		<category><![CDATA[generics]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[golang]]></category>
		<category><![CDATA[MapReduce]]></category>
		<category><![CDATA[泛型]]></category>
		<guid isPermaLink="false">https://coolshell.cn/?p=21615</guid>

					<description><![CDATA[<p>Go语言的1.17版本发布了，其中开始正式支持泛型了。虽然还有一些限制（比如，不能把泛型函数export），但是，可以体验了。我的这个《Go编程模式》的系列终于...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/21615.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/21615.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 wp-image-21627 " src="https://coolshell.cn/wp-content/uploads/2021/09/go-generics-1024x512.png" alt="" width="406" height="203" srcset="https://coolshell.cn/wp-content/uploads/2021/09/go-generics-1024x512.png 1024w, https://coolshell.cn/wp-content/uploads/2021/09/go-generics-300x150.png 300w, https://coolshell.cn/wp-content/uploads/2021/09/go-generics-768x384.png 768w, https://coolshell.cn/wp-content/uploads/2021/09/go-generics-540x270.png 540w, https://coolshell.cn/wp-content/uploads/2021/09/go-generics.png 1200w" sizes="(max-width: 406px) 100vw, 406px" />Go语言的1.17版本发布了，其中开始正式支持泛型了。虽然还有一些限制（比如，不能把泛型函数export），但是，可以体验了。我的这个《Go编程模式》的系列终于有了真正的泛型编程了，再也不需要使用反射或是go generation这些难用的技术了。周末的时候，我把Go 1.17下载下来，然后，体验了一下泛型编程，还是很不错的。下面，就让我们来看一下Go的泛型编程。（注：不过，如果你对泛型编程的重要性还不是很了解的话，你可以先看一下之前的这篇文章《<a title="Go 编程模式：Go Generation" href="https://coolshell.cn/articles/21179.html" target="_blank" rel="noopener">Go编程模式：Go Generation</a>》，然后再读一下《<a title="Go编程模式：Map-Reduce" href="https://coolshell.cn/articles/21164.html" target="_blank" rel="noopener">Go编程模式：MapReduce</a>》）</p>
<section class="post-series"><h3 class="post-series-title">本文是全系列中第10 / 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"><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-current post-series-item"><span class="post-series-item-title">Go编程模式 ： 泛型编程</span></li></ul><nav class="post-series-nav"><span class="post-series-nav-prev">&laquo; <a href="https://coolshell.cn/articles/21263.html" rel="prev" title="Go 编程模式：k8s Visitor 模式">上一篇文章</a></span></nav></section>
<h4>初探</h4>
<p>我们先来看一个简单的示例：</p>
<p><span id="more-21615"></span></p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">package main

import "fmt"

func print[T any] (arr []T) {
  for _, v := range arr {
    fmt.Print(v)
    fmt.Print(" ")
  }
  fmt.Println("")
}

func main() {
  strs := []string{"Hello", "World",  "Generics"}
  decs := []float64{3.14, 1.14, 1.618, 2.718 }
  nums := []int{2,4,6,8}

  print(strs)
  print(decs)
  print(nums)
}</pre>
<p>上面这个例子中，有一个 <code>print()</code> 函数，这个函数就是想输出数组的值，如果没有泛型的话，这个函数需要写出 <code>int</code> 版，<code>float</code>版，<code>string</code> 版，以及我们的自定义类型（<code>struct</code>）的版本。现在好了，有了泛型的支持后，我们可以使用 <code>[T any]</code> 这样的方式来声明一个泛型类型（有点像C++的 <code>typename T</code>），然后面都使用 <code>T</code> 来声明变量就好。</p>
<p>上面这个示例中，我们泛型的 <code>print()</code> 支持了三种类型的适配—— <code>int</code>型，<code>float64</code>型，和 <code>string</code>型。要让这段程序跑起来需要在编译行上加上 <code>-gcflags=-G=3</code>编译参数（这个编译参数会在1.18版上成为默认参数），如下所示：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="shell">$ go run -gcflags=-G=3 ./main.go</pre>
<p>有了个操作以后，我们就可以写一些标准的算法了，比如，一个查找的算法</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func find[T comparable] (arr []T, elem T) int {
  for i, v := range arr {
    if  v == elem {
      return i
    }
  }
  return -1
}</pre>
<p>我们注意到，我们没有使用 <code>[T any]</code>的形式，而是使用 <code>[T comparable]</code>的形式，<code>comparable</code>是一个接口类型，其约束了我们的类型需要支持 <code>==</code> 的操作， 不然就会有类型不对的编译错误。上面的这个 <code>find()</code> 函数同样可以使用于 <code>int</code>, <code>float64</code>或是<code>string</code>类型。</p>
<p>从上面的这两个小程序来看，Go语言的泛型已基本可用了，只不过，还有三个问题：</p>
<ul>
<li>一个是 <code>fmt.Printf()</code>中的泛型类型是 <code>%v</code> 还不够好，不能像c++ <code>iostream</code>重载 <code>&gt;&gt;</code> 来获得程序自定义的输出。</li>
<li>另外一个是，go不支持操作符重载，所以，你也很难在泛型算法中使用“泛型操作符”如：<code>==</code> 等</li>
<li>最后一个是，上面的 <code>find()</code> 算法依赖于“数组”，对于hash-table、tree、graph、link等数据结构还要重写。也就是说，没有一个像C++ STL那样的一个泛型迭代器（这其中的一部分工作当然也需要通过重载操作符（如：<code>++</code> 来实现）</li>
</ul>
<p>不过，这个已经很好了，让我们来看一下，可以干哪些事了。</p>
<h4>数据结构</h4>
<h5>Stack 栈</h5>
<p>编程支持泛型最大的优势就是可以实现类型无关的数据结构了。下面，我们用Slices这个结构体来实现一个Stack的数结构。</p>
<p>首先，我们可以定义一个泛型的Stack</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">type stack [T any] []T</pre>
<p>看上去很简单，还是 <code>[T any]</code> ，然后 <code>[]T</code> 就是一个数组，接下来就是实现这个数据结构的各种方法了。下面的代码实现了 <code>push()</code> ，<code>pop()</code>，<code>top()</code>，<code>len()</code>，<code>print()</code>这几个方法，这几个方法和 C++的STL中的 Stack很类似。（注：目前Go的泛型函数不支持 export，所以只能使用第一个字符是小写的函数名）</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func (s *stack[T]) push(elem T) {
  *s = append(*s, elem)
}

func (s *stack[T]) pop() {
  if len(*s) &gt; 0 {
    *s = (*s)[:len(*s)-1]
  } 
}
func (s *stack[T]) top() *T{
  if len(*s) &gt; 0 {
    return &amp;(*s)[len(*s)-1]
  } 
  return nil
}

func (s *stack[T]) len() int{
  return len(*s)
}

func (s *stack[T]) print() {
  for _, elem := range *s {
    fmt.Print(elem)
    fmt.Print(" ")
  }
  fmt.Println("")
}</pre>
<p>上面的这个例子还是比较简单的，不过在实现的过程中，对于一个如果栈为空，那么 <code>top()</code>要么返回<code>error</code>要么返回空值，在这个地方卡了一下。因为，之前，我们返回的“空”值，要么是 int 的<code>0</code>，要么是 string 的 <code>“”</code>，然而在泛型的<code>T</code>下，这个值就不容易搞了。也就是说，除了类型泛型后，还需要有一些“值的泛型”（注：在C++中，如果你要用一个空栈进行 <code>top()</code> 操作，你会得到一个 segmentation fault），所以，这里我们返回的是一个指针，这样可以判断一下指针是否为空。</p>
<p>下面是如何使用这个stack的代码。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func main() {

  ss := stack[string]{}
  ss.push("Hello")
  ss.push("Hao")
  ss.push("Chen")
  ss.print()
  fmt.Printf("stack top is - %v\n", *(ss.top()))
  ss.pop()
  ss.pop()
  ss.print()

  
  ns := stack[int]{}
  ns.push(10)
  ns.push(20)
  ns.print()
  ns.pop()
  ns.print()
  *ns.top() += 1
  ns.print()
  ns.pop()
  fmt.Printf("stack top is - %v\n", ns.top())

}</pre>
<p>&nbsp;</p>
<h5>LinkList 双向链表</h5>
<p>下面我们再来看一个双向链表的实现。下面这个实现中实现了 这几个方法：</p>
<ul>
<li><code>add()</code> &#8211; 从头插入一个数据结点</li>
<li><code>push()</code> &#8211; 从尾插入一个数据结点</li>
<li><code>del()</code> &#8211; 删除一个结点（因为需要比较，所以使用了 <code>compareable</code> 的泛型）</li>
<li><code>print()</code> &#8211; 从头遍历一个链表，并输出值。</li>
</ul>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">type node[T comparable] struct {
  data T
  prev *node[T]
  next *node[T]
}

type list[T comparable] struct {
  head, tail *node[T]
  len int
}

func (l *list[T]) isEmpty() bool {
  return l.head == nil &amp;&amp; l.tail == nil
}

func (l *list[T]) add(data T) {
  n := &amp;node[T] {
    data : data,
    prev : nil,
    next : l.head,
  }
  if l.isEmpty() {
    l.head = n
    l.tail = n
  }
  l.head.prev = n
  l.head = n
}

func (l *list[T]) push(data T) { 
  n := &amp;node[T] {
    data : data,
    prev : l.tail,
    next : nil,
  }
  if l.isEmpty() {
    l.head = n
    l.tail = n
  }
  l.tail.next = n
  l.tail = n
}

func (l *list[T]) del(data T) { 
  for p := l.head; p != nil; p = p.next {
    if data == p.data {
      
      if p == l.head {
        l.head = p.next
      }
      if p == l.tail {
        l.tail = p.prev
      }
      if p.prev != nil {
        p.prev.next = p.next
      }
      if p.next != nil {
        p.next.prev = p.prev
      }
      return 
    }
  } 
}

func (l *list[T]) print() {
  if l.isEmpty() {
    fmt.Println("the link list is empty.")
    return 
  }
  for p := l.head; p != nil; p = p.next {
    fmt.Printf("[%v] -&gt; ", p.data)
  }
  fmt.Println("nil")
}</pre>
<p>上面这个代码都是一些比较常规的链表操作，学过链表数据结构的同学应该都不陌生，使用的代码也不难，如下所示，都很简单，看代码就好了。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func main(){
  var l = list[int]{}
  l.add(1)
  l.add(2)
  l.push(3)
  l.push(4)
  l.add(5)
  l.print() //[5] -&gt; [2] -&gt; [1] -&gt; [3] -&gt; [4] -&gt; nil
  l.del(5)
  l.del(1)
  l.del(4)
  l.print() //[2] -&gt; [3] -&gt; nil
  
}</pre>
<h4>函数式范型</h4>
<p>接下来，我们就要来看一下我们函数式编程的三大件 <code>map()</code> 、 <code>reduce()</code> 和 <code>filter()</code> 在之前的《<a title="Go编程模式：Map-Reduce" href="https://coolshell.cn/articles/21164.html" target="_blank" rel="noopener">Go编程模式：Map-Reduce</a>》文章中，我们可以看到要实现这样的泛型，需要用到反射，代码复杂到完全读不懂。下面来看一下真正的泛型版本。</p>
<h5>泛型Map</h5>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func gMap[T1 any, T2 any] (arr []T1, f func(T1) T2) []T2 {
  result := make([]T2, len(arr))
  for i, elem := range arr {
    result[i] = f(elem)
  }
  return result
}</pre>
<p>在上面的这个 map函数中我使用了两个类型 &#8211; <code>T1</code> 和 <code>T2</code> ，</p>
<ul>
<li><code>T1</code> &#8211; 是需要处理数据的类型</li>
<li><code>T2</code> &#8211; 是处理后的数据类型</li>
</ul>
<p><code>T1</code> 和 <code>T2</code> 可以一样，也可以不一样。</p>
<p>我们还有一个函数参数 &#8211;  <code>func(T1) T2</code> 意味着，进入的是 <code>T1</code> 类型的，出来的是 <code>T2</code> 类型的。</p>
<p>然后，整个函数返回的是一个 <code>[]T2</code></p>
<p>好的，我们来看一下怎么使用这个map函数：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">nums := []int {0,1,2,3,4,5,6,7,8,9}
squares := gMap(nums, func (elem int) int {
  return elem * elem
})
print(squares)  //0 1 4 9 16 25 36 49 64 81 

strs := []string{"Hao", "Chen", "MegaEase"}
upstrs := gMap(strs, func(s string) string  {
  return strings.ToUpper(s)
})
print(upstrs) // HAO CHEN MEGAEASE 


dict := []string{"零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖"}
strs =  gMap(nums, func (elem int) string  {
  return  dict[elem]
})
print(strs) // 零 壹 贰 叁 肆 伍 陆 柒 捌 玖</pre>
<h5>泛型 Reduce</h5>
<p>接下来，我们再来看一下我们的Reduce函数，reduce函数是把一堆数据合成一个。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func gReduce[T1 any, T2 any] (arr []T1, init T2, f func(T2, T1) T2) T2 {
  result := init
  for _, elem := range arr {
    result = f(result, elem)
  }
  return result
}</pre>
<p>函数实现起来很简单，但是感觉不是很优雅。</p>
<ul>
<li>也是有两个类型 <code>T1</code> 和 <code>T2</code>，前者是输出数据的类型，后者是佃出数据的类型。</li>
<li>因为要合成一个数据，所以需要有这个数据的初始值 <code>init</code>，是 <code>T2</code> 类型</li>
<li>而自定义函数 <code>func(T2, T1) T2</code>，会把这个init值传给用户，然后用户处理完后再返回出来。</li>
</ul>
<p>下面是一个使用上的示例——求一个数组的和</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">nums := []int {0,1,2,3,4,5,6,7,8,9}
sum := gReduce(nums, 0, func (result, elem int) int  {
    return result + elem
})
fmt.Printf("Sum = %d \n", sum)</pre>
<h5>泛型 filter</h5>
<p>filter函数主要是用来做过滤的，把数据中一些符合条件（filter in）或是不符合条件（filter out）的数据过滤出来，下面是相关的代码示例</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func gFilter[T any] (arr []T, in bool, f func(T) bool) []T {
  result := []T{}
  for _, elem := range arr {
    choose := f(elem)
    if (in &amp;&amp; choose) || (!in &amp;&amp; !choose) {
      result = append(result, elem)
    }
  }
  return result
}

func gFilterIn[T any] (arr []T, f func(T) bool) []T {
  return gFilter(arr, true, f)
}

func gFilterOut[T any] (arr []T, f func(T) bool) []T {
  return gFilter(arr, false, f)
}</pre>
<p>其中，用户需要提从一个 <code>bool</code> 的函数，我们会把数据传给用户，然后用户只需要告诉我行还是不行，于是我们就会返回一个过滤好的数组给用户。</p>
<p>比如，我们想把数组中所有的奇数过滤出来</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">nums := []int {0,1,2,3,4,5,6,7,8,9}
odds := gFilterIn(nums, func (elem int) bool  {
    return elem % 2 == 1
})
print(odds)</pre>
<h4>业务示例</h4>
<p>正如《<a title="Go编程模式：Map-Reduce" href="https://coolshell.cn/articles/21164.html" target="_blank" rel="noopener">Go编程模式：Map-Reduce</a>》中的那个业务示例，我们在这里再做一遍。</p>
<p>首先，我们先声明一个员工对象和相关的数据</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">type Employee struct {
  Name     string
  Age      int
  Vacation int
  Salary   float32
}

var employees = []Employee{
  {"Hao", 44, 0, 8000.5},
  {"Bob", 34, 10, 5000.5},
  {"Alice", 23, 5, 9000.0},
  {"Jack", 26, 0, 4000.0},
  {"Tom", 48, 9, 7500.75},
  {"Marry", 29, 0, 6000.0},
  {"Mike", 32, 8, 4000.3},
}</pre>
<p>然后，我们想统一下所有员工的薪水，我们就可以使用前面的reduce函数</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">total_pay := gReduce(employees, 0.0, func(result float32, e Employee) float32 {
  return result + e.Salary
})
fmt.Printf("Total Salary: %0.2f\n", total_pay) // Total Salary: 43502.05</pre>
<p>我们函数这个 <code>gReduce</code> 函数有点啰嗦，还需要传一个初始值，在用户自己的函数中，还要关心 <code>result</code> 我们还是来定义一个更好的版本。</p>
<p>一般来说，我们用 reduce 函数大多时候基本上是统计求和或是数个数，所以，是不是我们可以定义的更为直接一些？比如下面的这个 <code>CountIf()</code>，就比上面的 Reduce 干净了很多。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func gCountIf[T any](arr []T, f func(T) bool) int {
  cnt := 0
  for _, elem := range arr {
    if f(elem) {
      cnt += 1
    }
  }
  return cnt;
}</pre>
<p>我们做求和，我们也可以写一个Sum的泛型。</p>
<ul>
<li>处理 <code>T</code> 类型的数据，返回 <code>U</code>类型的结果</li>
<li>然后，用户只需要给我一个需要统计的 <code>T</code> 的 <code>U</code> 类型的数据就可以了。</li>
</ul>
<p>代码如下所示：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">type Sumable interface {
  type int, int8, int16, int32, int64,
        uint, uint8, uint16, uint32, uint64,
        float32, float64
}

func gSum[T any, U Sumable](arr []T, f func(T) U) U {
  var sum U
  for _, elem := range arr {
    sum += f(elem)
  }
  return sum
}</pre>
<p>上面的代码我们动用了一个叫 Sumable 的接口，其限定了 U 类型，只能是 Sumable里的那些类型，也就是整型或浮点型，这个支持可以让我们的泛型代码更健壮一些。</p>
<p>于是，我们就可以完成下面的事了。</p>
<p><strong>1）统计年龄大于40岁的员工数</strong></p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">old := gCountIf(employees, func (e Employee) bool  {
    return e.Age &gt; 40
})
fmt.Printf("old people(&gt;40): %d\n", old) 
// ld people(&gt;40): 2</pre>
<p><strong>2）统计薪水超过 6000元的员工数</strong></p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">high_pay := gCountIf(employees, func(e Employee) bool {
  return e.Salary &gt;= 6000
})
fmt.Printf("High Salary people(&gt;6k): %d\n", high_pay) 
//High Salary people(&gt;6k): 4</pre>
<p><strong>3）统计年龄小于30岁的员工的薪水</strong></p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">younger_pay := gSum(employees, func(e Employee) float32 {
  if e.Age &lt; 30 {
      return e.Salary
  } 
  return 0
})
fmt.Printf("Total Salary of Young People: %0.2f\n", younger_pay)
//Total Salary of Young People: 19000.00</pre>
<p><strong>4）统计全员的休假天数</strong></p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">total_vacation := gSum(employees, func(e Employee) int {
  return e.Vacation
})
fmt.Printf("Total Vacation: %d day(s)\n", total_vacation)
//Total Vacation: 32 day(s)</pre>
<p><strong>5）把没有休假的员工过滤出来</strong></p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">no_vacation := gFilterIn(employees, func(e Employee) bool {
  return e.Vacation == 0
})
print(no_vacation)
//{Hao 44 0 8000.5} {Jack 26 0 4000} {Marry 29 0 6000}</pre>
<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/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/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/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/21615.html">Go编程模式 ： 泛型编程</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/21615.html/feed</wfw:commentRss>
			<slash:comments>14</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>Go语言、Docker 和新技术</title>
		<link>https://coolshell.cn/articles/18190.html</link>
					<comments>https://coolshell.cn/articles/18190.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Mon, 30 Oct 2017 01:24:20 +0000</pubDate>
				<category><![CDATA[Go 语言]]></category>
		<category><![CDATA[编程语言]]></category>
		<category><![CDATA[Docker]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[golang]]></category>
		<guid isPermaLink="false">https://coolshell.cn/?p=18190</guid>

					<description><![CDATA[<p>上个月，作为 Go 语言的三位创始人之一，Unix 老牌黑客罗勃·派克（Rob Pike）在新文章“Go: Ten years and climbing”中，回...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/18190.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/18190.html">Go语言、Docker 和新技术</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-sup_wechat_big " src="https://coolshell.cn/wp-content/uploads/2017/10/golang.docker-360x200.png" alt="" width="360" height="200" />上个月，作为 Go 语言的三位创始人之一，Unix 老牌黑客罗勃·派克（Rob Pike）在新文章“<a href="https://commandcenter.blogspot.com/2017/09/go-ten-years-and-climbing.html" target="_blank" rel="noopener noreferrer">Go: Ten years and climbing</a>”中，回顾了一下 Go 语言的发展过程。其中提到，Go 语言这十年的迅猛发展大到连他们自己都没有想到，并且还成为了云计算领域中新一代的开发语言。还提到了，中国程序员对 Go 语言的热爱完全超出了他们的想象，甚至他们都不敢相信是真的。</p>
<p>这让我想起，我在 2015 年 5 月份拜访 Docker 公司在湾区的总部时，Docker 负责人也和我表达了相似的感叹：他们完全没有想到居然中国有那么多人喜欢 Docker，而且还有这么多人在为 Docker 做贡献，这让他们感到非常意外。此外，还跟我说，中国是除了美国本土之外的另一个如此喜欢 Docker 技术的国家，在其它国家都没有看到。</p>
<p>的确如他们所说，Go 语言和 Docker 这两种技术已经成为新一代的云计算技术，而且可以看到其发展态势非常迅猛。而中国也成为了像美国一样在强力推动这两种技术的国家。这的确是一件让人感到非常高兴的事，因为中国在跟随时代潮流这件事上已经做得非常不错了。</p>
<p>然而，从 2014-2015 年我在阿里推动 Docker 和 Go 语言的痛苦和失败过程中，以及这许多年来，有很多很多人问我是否要学 Go 语言，是否要学 Docker，Go 和 Docker 是否能用在生产线上，这些问题看来，对于 Go 语言和 Docker 这两种技术，在国内的技术圈中有相当大的一部分人和群体还在执观望或是不信任的态度。</p>
<p>所以，我想写这篇文章，从两个方面来论述一下我的观点和看法。</p>
<ul class="list-paddingleft-2">
<li>一个方面，为什么 Go 语言和 Docker 会是新一代的云计算技术。</li>
<li>另一个方面，作为技术人员，我们如何识别什么样的新技术会是未来的趋势。</li>
</ul>
<p>这两个问题是相辅相成的，所以我会把这两个问题揉在一起谈。</p>
<p><span id="more-18190"></span></p>
<p>虽然 Go 语言是在 2009 年底开源的，但我是从 2012 年才开始接触和学习 Go 语言的。我只花了一个周末两天的时间就学完了，而且在这两天，我还很快地写出了一个能工作很好的网页爬虫程序，以及一个简单的高并发文件处理服务，用于提取前面抓取的网页的关键内容。这两个程序都很简单，总共才写了不到 500 行代码。</p>
<p>我当时对 Go 语言有几点体会。</p>
<p><strong>第一，语言简单，上手快。</strong>Go 语言的语法特性简直是太简单了，简单到你几乎玩不出什么花招，直来直去的，学习曲线很低，上手非常快。</p>
<p><strong>第二，并行和异步编程几乎无痛点。</strong>Go 语言的 Goroutine 和 Channel 这两个神器简直就是并发和异步编程的巨大福音。像 C、C++、Java、Python 和 JavaScript 这些语言的并发和异步方式太控制就比较复杂了，而且容易出错，而 Go 解决这个问题非常地优雅和流畅。这对于编程多年受尽并发和异步折磨的我来说，完全就是让我眼前一亮的感觉。</p>
<p><img decoding="async" loading="lazy" class="aligncenter " src="https://coolshell.cn/wp-content/uploads/2017/10/golang.01.png" alt="" width="664" height="403" /></p>
<p style="text-align: center;">（图片来自 Medium：<a href="https://medium.com/@kevalpatel2106/why-should-you-learn-go-f607681fad65" target="_blank" rel="noopener noreferrer">Why should you learn Go?</a>）</p>
<p><strong>第三，Go 语言的 lib 库麻雀虽小五脏俱全。</strong>Go 语言的 lib 库中基本上有绝大多数常用的库，虽然有些库还不是很好，但我觉得不是问题，因为我相信在未来的发展中会把这些问题解决掉。</p>
<p><strong>第四，C 语言的理念和 Python 的姿态。</strong>C 语言的理念是信任程序员，保持语言的小巧，不屏蔽底层且底层友好，关注语言的执行效率和性能。而 Python 的姿态是用尽量少的代码完成尽量多的事。于是我能够感觉到，Go 语言想要把 C 和 Python 统一起来，这是多棒的一件事啊。</p>
<p><img decoding="async" loading="lazy" class="aligncenter " src="https://coolshell.cn/wp-content/uploads/2017/10/golang.02.png" alt="" width="662" height="400" /></p>
<p style="text-align: center;">（图片来自 Medium：<a href="https://medium.com/@kevalpatel2106/why-should-you-learn-go-f607681fad65" target="_blank" rel="noopener noreferrer">Why should you learn Go?</a>）</p>
<p>所以，即便 Go 语言存在诸多的问题，比如垃圾回收、异常处理、泛型编程等，但相较于上面这几个优势，我认为这些问题都是些小问题。于是就毫不犹豫地入坑了。</p>
<p>当然，一个技术能不能发展起来，关键还要看三点。</p>
<ul class="list-paddingleft-2">
<li><strong>有没有一个比较好的社区。</strong>像 C、C++、Java、Python 和 JavaScript 的生态圈都是非常丰富和火爆的。尤其是有很多商业机构参与的社区那就更为人气爆棚了，比如 Linux 的社区。</li>
<li><strong>有没有一个工业化的标准。</strong>像 C、C++、Java 都是有标准化组织的。尤其是 Java，其在架构上还搞出了像 J2EE 这样的企业级标准。</li>
<li><strong>有没有一个或多个杀手级应用。</strong>C、C++ 和 Java 的杀手级应用不用多说了，就算是对于 PHP 这样还不能算是一个好的编程语言来说，因为是 Linux 时代的第一个杀手级解决方案 LAMP 中的关键技术，所以，也发展起来了。</li>
</ul>
<p>上述的这三点是非常关键的，新的技术只需要占到其中一到两点就已经很不错了，何况有的技术，比如 Java，是三点全占到了，所以，Java 的发展是如此好。当然，除了上面这三点重要的，还有一些其它的影响因素，比如：</p>
<ul class="list-paddingleft-2">
<li><strong>学习曲线是否低，上手是否快。</strong>这点非常重要，C++ 在这点上越做越不好了。</li>
<li><strong>有没有一个不错的提高开发效率的开发框架。</strong>如：Java 的 Spring 框架，C++ 的 STL 等。</li>
<li><strong>是否有一个或多个巨型的技术公司作为后盾。</strong>如：Java 和 Linux 后面的 IBM、Sun……</li>
<li><strong>有没有解决软件开发中的痛点。</strong>如：Java 解决了 C 和 C++ 的内存管理问题。</li>
</ul>
<p>用这些标尺来量一下 Go 语言，我们可以清楚地看到：</p>
<ul class="list-paddingleft-2">
<li>Go 语言容易上手；</li>
<li>Go 语言解决了并发编程和写底层应用开发效率的痛点；</li>
<li>Go 语言有 Google 这个世界一流的技术公司在后面；</li>
<li>Go 语言的杀手级应用是 Docker，而 Docker 的生态圈在这几年完全爆棚了。</li>
</ul>
<p>所以，Go 语言的未来是不可限量的。当然，我个人觉得，Go 可能会吞食很多 C、C++、Java 的项目。不过，Go 语言所吞食主要的项目应该是中间层的项目，既不是非常底层也不会是业务层。</p>
<p>也就是说，Go 语言不会吞食底层到 C 和 C++ 那个级别的，也不会吞食到高层如 Java 业务层的项目。Go 语言能吞食的一定是 PaaS 上的项目，比如一些消息缓存中间件、服务发现、服务代理、控制系统、Agent、日志收集等等，没有复杂的业务场景，也到不了特别底层（如操作系统）的中间平台层的软件项目或工具。而 C 和 C++ 会被打到更底层，Java 会被打到更上层的业务层。这是我的一个判断。</p>
<p>好了，我们再用上面的标尺来量一下 Go 语言的杀手级应用 Docker，你会发现基本是一样的。</p>
<ul class="list-paddingleft-2">
<li>Docker 上手很容易。</li>
<li>Docker 解决了运维中的环境问题以及服务调度的痛点。</li>
<li>Docker 的生态圈中有大公司在后面助力。比如 Google。</li>
<li>Docker 产出了工业界标准 OCI。</li>
<li>Docker 的社区和生态圈已经出现像 Java 和 Linux 那样的态势。</li>
<li>……</li>
</ul>
<p>所以，早在 3、4 年前我就觉得 Docker 一定会是未来的技术。虽然当时的坑儿还很多，但是，相对于这些大的因素来说，那些小坑儿都不是问题。只是需要一些时间，这些小坑儿在未来 5-10 年就可以完全被填平了。</p>
<p>同样，我们可以看到 Kubernetes 作为服务和容器调度的关键技术一定会是最后的赢家。这点我在去年初就能够很明显地感觉到了。</p>
<p>关于 Docker 我还想多说几句，这是云计算中 PaaS 的关键技术，虽然，这世上在出现 Docker 之前，几乎所有的要玩公有 PaaS 的公司和产品都玩不起来，比如：Google 的 GAE，国内的各种 XAE，如淘宝的 TAE，新浪的 SAE 等。但我还是想说，<strong>PaaS 是一个被世界或是被产业界严重低估的平台。</strong></p>
<p><strong>PaaS 层是承上启下的关键技术，任何一个不重视 PaaS 的公司，其技术架构都不可能让这家公司成长为一个大型的公司</strong>。因为 PaaS 层的技术主要能解决下面这些问题。</p>
<ul class="list-paddingleft-2">
<li><strong>软件生产线的问题。</strong>持续集成和持续发布，以及 DevOps 中的技术必需通过 PaaS。</li>
<li><strong>分布式服务化的问题。</strong>分布式服务化的服务高可用、服务编排、服务调度、服务发现、服务路由，以及分布式服务化的支撑技术完全是 PaaS 的菜。</li>
<li><strong>提高服务的可用性 SLA。</strong>提高服务可用性 SLA 所需要的分布式、高可用的技术架构和运维工具，也是 PaaS 层提供的。</li>
<li><strong>软件能力的复用。</strong>软件工程中的核心就是软件能力的复用，这一点也完美地体现在 PaaS 平台的技术上。</li>
</ul>
<p>老实说，这些问题的关键程度已经到了能判断一家依托技术的公司的研发能力是否靠谱的程度。没有这些技术，依托技术拓展业务的公司几乎没有可能发展得规模很大。</p>
<p>在后面，我会在“<a href="https://time.geekbang.org/" target="_blank" rel="noopener noreferrer">极客时间</a>”<a href="https://time.geekbang.org/column/intro/48" target="_blank" rel="noopener noreferrer">我的付费专栏</a>里另外写几篇文章详细地讲一下分布式服务化和 PaaS 平台的重要程度。</p>
<p>最后，我还要说一下，为什么要早一点地进入这些新技术，而不是等待这些技术成熟了后再进入。原因有这么几个。</p>
<blockquote><p>技术的发展过程非常重要。我进入 Go 和 Docker 的技术不能算早，但也不算晚，从 2012 年学习 Go，到 2013 年学习 Docker 到今天，我清楚地看到了这两种技术的生态圈发展过程。让我收获最大的并不是这些技术本身，而是一个技术的变迁和行业的发展。</p></blockquote>
<p>从中，我看到了非常具体的各种思潮和思路，这些东西比起 Go 和 Docker 来说更有价值。因为，这不但让我重新思考我已掌握的技术以及如何更好地解决已有的问题，而且还让我看到了未来。我不但有了技术优势，而且这些知识还让我的技术生涯多了很多的可能性。</p>
<blockquote><p>这些关键新技术，可以让你拿到技术的先机。这些对一个需要技术领导力的个人或公司来说都是非常重要的。</p></blockquote>
<p>一个公司或是个人能够占有技术先机，就会比其它公司或个人有更大的影响力。一旦未来行业需求引爆，那么这个公司或是个人的影响力就会形成一个比较大的护城河，并可以快速地产生经济利益。</p>
<p>近期，在与中国移动、中国电信以及一些股份制银行进行交流的过程中，我已看到通讯行业、金融行业对于 PaaS 平台的理解已经超过了互联网公司，而我近 3 年来在这些技术上的研究让我也从中受益非浅。</p>
<p>所以，Go 语和 Docker 作为 PaaS 平台的关键技术前途是无限的，我很庆幸赶上了这个浪潮，也很庆幸在 3 年前我就看到了这个趋势，现在我也在用这些技术开发相关的技术产品，助力于为高速成长的公司提供这些关键技术。</p>
<p>&nbsp;</p>
<p><strong>最后注明一下：</strong></p>
<p><strong>这篇文章于上周发布于<a href="https://time.geekbang.org/column/intro/48" target="_blank" rel="noopener noreferrer">“极客时间”的我的付费专栏</a>中。极客时间中的付费是我受Geekbang邀请写的一个付费专栏，因为过去10多年给企业有过很多内训，过去2年又给好多企业做过一些咨询工作，所以，我会把一些商业化的内容写在极客时间里，当然，也会有一些我的新文章。关于这个事，我后面我专门开一篇文章说一下。（大家可以到 Apple的App Store上搜极客时间，Android版本等到12月初吧）<br />
</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/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/18190.html">Go语言、Docker 和新技术</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/18190.html/feed</wfw:commentRss>
			<slash:comments>70</slash:comments>
		
		
			</item>
		<item>
		<title>Go编程模式：修饰器</title>
		<link>https://coolshell.cn/articles/17929.html</link>
					<comments>https://coolshell.cn/articles/17929.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Thu, 01 Jun 2017 08:48:15 +0000</pubDate>
				<category><![CDATA[Go 语言]]></category>
		<category><![CDATA[程序设计]]></category>
		<category><![CDATA[编程语言]]></category>
		<category><![CDATA[functional]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[golang]]></category>
		<category><![CDATA[Programming]]></category>
		<category><![CDATA[函数式]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=17929</guid>

					<description><![CDATA[<p>之前写过一篇《Python修饰器的函数式编程》，这种模式很容易的可以把一些函数装配到另外一些函数上，可以让你的代码更为的简单，也可以让一些“小功能型”的代码复用...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/17929.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/17929.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-full wp-image-17945" src="https://coolshell.cn/wp-content/uploads/2017/06/go-hardhat.png" alt="" width="200" height="193" />之前写过一篇《<a href="https://coolshell.cn/articles/11265.html" target="_blank" rel="noopener noreferrer">Python修饰器的函数式编程</a>》，这种模式很容易的可以把一些函数装配到另外一些函数上，可以让你的代码更为的简单，也可以让一些“小功能型”的代码复用性更高，让代码中的函数可以像乐高玩具那样自由地拼装。所以，一直以来，我对修饰器decoration这种编程模式情有独钟，这里写一篇Go语言相关的文章。</p>
<section class="post-series"><h3 class="post-series-title">本文是全系列中第7 / 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-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/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/21179.html" rel="prev" title="Go 编程模式：Go Generation">上一篇文章</a></span><span class="post-series-nav-next"><a href="https://coolshell.cn/articles/21228.html" rel="next" title="Go编程模式：Pipeline">下一篇文章</a> &raquo;</span></nav></section>
<p>看过<a href="https://coolshell.cn/articles/11265.html" target="_blank" rel="noopener noreferrer">Python修饰器</a>那篇文章的同学，一定知道这是一种函数式编程的玩法——用一个高阶函数来包装一下。多唠叨一句，关于函数式编程，可以参看我之前写过一篇文章《<a href="https://coolshell.cn/articles/10822.html" target="_blank" rel="noopener noreferrer">函数式编程</a>》，这篇文章主要是，想通过从过程式编程的思维方式过渡到函数式编程的思维方式，从而带动更多的人玩函数式编程，所以，如果你想了解一下函数式编程，那么可以移步先阅读一下。所以，Go语言的修饰器编程模式，其实也就是函数式编程的模式。</p>
<p>不过，要提醒注意的是，Go 语言的“糖”不多，而且又是强类型的静态无虚拟机的语言，所以，无法做到像 Java 和 Python 那样的优雅的修饰器的代码。当然，也许是我才才疏学浅，如果你知道有更多的写法，请你一定告诉我。先谢过了。<br />
<span id="more-17929"></span></p>
<h4>简单示例</h4>
<p>我们先来看一个示例：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">package main

import "fmt"

func decorator(f func(s string)) func(s string) {

    return func(s string) {
        fmt.Println("Started")
        f(s)
        fmt.Println("Done")
    }
}

func Hello(s string) {
    fmt.Println(s)
}

func main() {
        decorator(Hello)("Hello, World!")
}</pre>
<p>我们可以看到，我们动用了一个高阶函数 <code>decorator()</code>，在调用的时候，先把 <code>Hello()</code> 函数传进去，然后其返回一个匿名函数，这个匿名函数中除了运行了自己的代码，也调用了被传入的 <code>Hello()</code> 函数。</p>
<p>这个玩法和 Python 的异曲同工，只不过，有些遗憾的是，Go 并不支持像 Python 那样的 <code>@decorator</code> 语法糖。所以，在调用上有些难看。当然，如果你要想让代码容易读一些，你可以这样：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">hello := decorator(Hello)
hello("Hello")</pre>
<p>我们再来看一个和计算运行时间的例子：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang" data-enlighter-highlight="16-26">package main

import (
  "fmt"
  "reflect"
  "runtime"
  "time"
)

type SumFunc func(int64, int64) int64

func getFunctionName(i interface{}) string {
  return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
}

func timedSumFunc(f SumFunc) SumFunc {
  return func(start, end int64) int64 {

    defer func(t time.Time) {
      fmt.Printf("--- Time Elapsed (%s): %v ---\n", 
          getFunctionName(f), time.Since(t))
    }(time.Now())

    return f(start, end)
  }
}

func Sum1(start, end int64) int64 {
  var sum int64
  sum = 0
  if start &gt; end {
    start, end = end, start
  }
  for i := start; i &lt;= end; i++ {
    sum += i
  }
  return sum
}

func Sum2(start, end int64) int64 {
  if start &gt; end {
    start, end = end, start
  }
  return (end - start + 1) * (end + start) / 2
}

func main() {

  sum1 := timedSumFunc(Sum1)
  sum2 := timedSumFunc(Sum2)

  fmt.Printf("%d, %d\n", sum1(-10000, 10000000), sum2(-10000, 10000000))
}</pre>
<p>关于上面的代码，有几个事说明一下：</p>
<p>1）有两个 Sum 函数，<code>Sum1()</code> 函数就是简单的做个循环，<code>Sum2()</code> 函数动用了数据公式。（注意：start 和 end 有可能有负数的情况）</p>
<p>2）代码中使用了 Go 语言的反射机器来获取函数名。</p>
<p>3）修饰器函数是 <code>timedSumFunc()</code></p>
<p>运行后输出：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="shell">$ go run time.sum.go
--- Time Elapsed (main.Sum1): 3.557469ms ---
--- Time Elapsed (main.Sum2): 291ns ---
49999954995000, 49999954995000
</pre>
<h4>HTTP 相关的一个示例</h4>
<p>我们再来看一个处理 HTTP 请求的相关的例子。</p>
<p>先看一个简单的 HTTP Server 的代码。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang" data-enlighter-highlight="10-16,24">package main

import (
    "fmt"
    "log"
    "net/http"
    "strings"
)

func WithServerHeader(h http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        log.Println("---&gt;WithServerHeader()")
        w.Header().Set("Server", "HelloServer v0.0.1")
        h(w, r)
    }
}

func hello(w http.ResponseWriter, r *http.Request) {
    log.Printf("Recieved Request %s from %s\n", r.URL.Path, r.RemoteAddr)
    fmt.Fprintf(w, "Hello, World! "+r.URL.Path)
}

func main() {
    http.HandleFunc("/v1/hello", WithServerHeader(hello))
    err := http.ListenAndServe(":8080", nil)
    if err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}</pre>
<p>上面代码中使用到了修饰模式，<code>WithServerHeader()</code> 函数就是一个 Decorator，其传入一个 <code>http.HandlerFunc</code>，然后返回一个改写的版本。上面的例子还是比较简单，用 <code>WithServerHeader()</code> 就可以加入一个 Response 的 Header。</p>
<p>于是，这样的函数我们可以写出好些个。如下所示，有写 HTTP 响应头的，有写认证 Cookie 的，有检查认证Cookie的，有打日志的……</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang" data-enlighter-highlight="60-62">package main

import (
    "fmt"
    "log"
    "net/http"
    "strings"
)

func WithServerHeader(h http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        log.Println("---&gt;WithServerHeader()")
        w.Header().Set("Server", "HelloServer v0.0.1")
        h(w, r)
    }
}

func WithAuthCookie(h http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        log.Println("---&gt;WithAuthCookie()")
        cookie := &amp;http.Cookie{Name: "Auth", Value: "Pass", Path: "/"}
        http.SetCookie(w, cookie)
        h(w, r)
    }
}

func WithBasicAuth(h http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        log.Println("---&gt;WithBasicAuth()")
        cookie, err := r.Cookie("Auth")
        if err != nil || cookie.Value != "Pass" {
            w.WriteHeader(http.StatusForbidden)
            return
        }
        h(w, r)
    }
}

func WithDebugLog(h http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        log.Println("---&gt;WithDebugLog")
        r.ParseForm()
        log.Println(r.Form)
        log.Println("path", r.URL.Path)
        log.Println("scheme", r.URL.Scheme)
        log.Println(r.Form["url_long"])
        for k, v := range r.Form {
            log.Println("key:", k)
            log.Println("val:", strings.Join(v, ""))
        }
        h(w, r)
    }
}
func hello(w http.ResponseWriter, r *http.Request) {
    log.Printf("Recieved Request %s from %s\n", r.URL.Path, r.RemoteAddr)
    fmt.Fprintf(w, "Hello, World! "+r.URL.Path)
}

func main() {
    http.HandleFunc("/v1/hello", WithServerHeader(WithAuthCookie(hello)))
    http.HandleFunc("/v2/hello", WithServerHeader(WithBasicAuth(hello)))
    http.HandleFunc("/v3/hello", WithServerHeader(WithBasicAuth(WithDebugLog(hello))))
    err := http.ListenAndServe(":8080", nil)
    if err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}</pre>
<h4>多个修饰器的 Pipeline</h4>
<p>在使用上，需要对函数一层层的套起来，看上去好像不是很好看，如果需要 decorator 比较多的话，代码会比较难看了。嗯，我们可以重构一下。</p>
<p>重构时，我们需要先写一个工具函数——用来遍历并调用各个 decorator：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">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="golang">http.HandleFunc("/v4/hello", Handler(hello,
                WithServerHeader, WithBasicAuth, WithDebugLog))</pre>
<p>这样的代码是不是更易读了一些？pipeline 的功能也就出来了。</p>
<h4>泛型的修饰器</h4>
<p>不过，对于 Go 的修饰器模式，还有一个小问题 —— 好像无法做到泛型，就像上面那个计算时间的函数一样，其代码耦合了需要被修饰的函数的接口类型，无法做到非常通用，如果这个事解决不了，那么，这个修饰器模式还是有点不好用的。</p>
<p>因为 Go 语言不像 Python 和 Java，Python是动态语言，而 Java 有语言虚拟机，所以他们可以干好些比较变态的事，然而 Go 语言是一个静态的语言，这意味着其类型需要在编译时就要搞定，否则无法编译。不过，Go 语言支持的最大的泛型是 <code>interface{}</code> 还有比较简单的 reflection 机制，在上面做做文章，应该还是可以搞定的。</p>
<p>废话不说，下面是我用 reflection 机制写的一个比较通用的修饰器（为了便于阅读，我删除了出错判断代码）</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang" data-enlighter-highlight="7,10">func Decorator(decoPtr, fn interface{}) (err error) {
    var decoratedFunc, targetFunc reflect.Value

    decoratedFunc = reflect.ValueOf(decoPtr).Elem()
    targetFunc = reflect.ValueOf(fn)

    v := reflect.MakeFunc(targetFunc.Type(),
            func(in []reflect.Value) (out []reflect.Value) {
                fmt.Println("before")
                out = targetFunc.Call(in)
                fmt.Println("after")
                return
            })

    decoratedFunc.Set(v)
    return
}</pre>
<p>上面的代码动用了 <code>reflect.MakeFunc()</code> 函数制出了一个新的函数其中的 <code>targetFunc.Call(in)</code> 调用了被修饰的函数。关于 Go 语言的反射机制，推荐官方文章 —— 《<a href="https://blog.golang.org/laws-of-reflection" target="_blank" rel="noopener noreferrer">The Laws of Reflection</a>》，在这里我不多说了。</p>
<p>上面这个 <code>Decorator()</code> 需要两个参数，</p>
<ul>
<li>第一个是出参 <code>decoPtr</code> ，就是完成修饰后的函数</li>
<li>第二个是入参 <code>fn</code> ，就是需要修饰的函数</li>
</ul>
<p>这样写是不是有些二？的确是的。不过，这是我个人在 Go 语言里所能写出来的最好的的代码了。如果你知道更多优雅的，请你一定告诉我！</p>
<p>好的，让我们来看一下使用效果。首先假设我们有两个需要修饰的函数：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func foo(a, b, c int) int {
    fmt.Printf("%d, %d, %d \n", a, b, c)
    return a + b + c
}

func bar(a, b string) string {
    fmt.Printf("%s, %s \n", a, b)
    return a + b
}</pre>
<p>然后，我们可以这样做：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">type MyFoo func(int, int, int) int
var myfoo MyFoo
Decorator(&amp;myfoo, foo)
myfoo(1, 2, 3)
</pre>
<p>你会发现，使用 <code>Decorator()</code> 时，还需要先声明一个函数签名，感觉好傻啊。一点都不泛型，不是吗？</p>
<p>嗯。如果你不想声明函数签名，那么你也可以这样</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">mybar := bar
Decorator(&amp;mybar, bar)
mybar("hello,", "world!")</pre>
<p>好吧，看上去不是那么的漂亮，但是 it works。看样子 Go 语言目前本身的特性无法做成像 Java 或 Python 那样，对此，我们只能多求 Go 语言多放糖了！</p>
<p>Again， 如果你有更好的写法，请你一定要告诉我。</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/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><li ><a href="https://coolshell.cn/articles/11265.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2014/03/snake-hat-new-year-schedule-800x960-150x150.jpg" alt="Python修饰器的函数式编程" width="150" height="150" /></a><a href="https://coolshell.cn/articles/11265.html" class="wp_rp_title">Python修饰器的函数式编程</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></ul></div></div>The post <a href="https://coolshell.cn/articles/17929.html">Go编程模式：修饰器</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/17929.html/feed</wfw:commentRss>
			<slash:comments>27</slash:comments>
		
		
			</item>
		<item>
		<title>Go 语言简介（下）— 特性</title>
		<link>https://coolshell.cn/articles/8489.html</link>
					<comments>https://coolshell.cn/articles/8489.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Wed, 07 Nov 2012 00:17:20 +0000</pubDate>
				<category><![CDATA[Go 语言]]></category>
		<category><![CDATA[程序设计]]></category>
		<category><![CDATA[编程语言]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[golang]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=8489</guid>

					<description><![CDATA[<p>希望你看到这篇文章的时候还是在公交车和地铁上正在上下班的时间，我希望我的这篇文章可以让你利用这段时间了解一门语言。当然，希望你不会因为看我的文章而错过站。呵呵。...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/8489.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/8489.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>希望你看到这篇文章的时候还是在公交车和地铁上正在上下班的时间，我希望我的这篇文章可以让你利用这段时间了解一门语言。当然，希望你不会因为看我的文章而错过站。呵呵。</p>
<p>如果你还不了解Go语言的语法，还请你移步先看一下上篇——《<strong><a title="Go语言简介（上）：语法" href="https://coolshell.cn/articles/8460.html" target="_blank" rel="noopener noreferrer">Go语言简介（上）：语法</a></strong>》</p>
<p><img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2012/11/google-go-language.jpg" alt="" title="google-go-language" width="450" height="272" class="aligncenter size-full wp-image-8531" srcset="https://coolshell.cn/wp-content/uploads/2012/11/google-go-language.jpg 450w, https://coolshell.cn/wp-content/uploads/2012/11/google-go-language-300x181.jpg 300w, https://coolshell.cn/wp-content/uploads/2012/11/google-go-language-447x270.jpg 447w" sizes="(max-width: 450px) 100vw, 450px" /></p>
<h4>goroutine</h4>
<p>GoRoutine主要是使用go关键字来调用函数，你还可以使用匿名函数，如下所示：</p>
<p><span id="more-8489"></span></p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang" data-enlighter-highlight="9,11">package main
import &quot;fmt&quot;

func f(msg string) {
    fmt.Println(msg)
}

func main(){
    go f(&quot;goroutine&quot;)

    go func(msg string) {
        fmt.Println(msg)
    }(&quot;going&quot;)
}</pre>
<p>我们再来看一个示例，下面的代码中包括很多内容，包括时间处理，随机数处理，还有goroutine的代码。如果你熟悉C语言，你应该会很容易理解下面的代码。</p>
<p>你可以简单的把go关键字调用的函数想像成pthread_create。下面的代码使用for循环创建了3个线程，每个线程使用一个随机的Sleep时间，然后在routine()函数中会输出一些线程执行的时间信息。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">
package main

import &quot;fmt&quot;
import &quot;time&quot;
import &quot;math/rand&quot;

func routine(name string, delay time.Duration) {

    t0 := time.Now()
    fmt.Println(name, &quot; start at &quot;, t0)

    time.Sleep(delay)

    t1 := time.Now()
    fmt.Println(name, &quot; end at &quot;, t1)

    fmt.Println(name, &quot; lasted &quot;, t1.Sub(t0))
}

func main() {

    //生成随机种子
    rand.Seed(time.Now().Unix())

    var name string
    for i:=0; i&lt;3; i++{
        name = fmt.Sprintf(&quot;go_%02d&quot;, i) //生成ID
        //生成随机等待时间，从0-4秒
        go routine(name, time.Duration(rand.Intn(5)) * time.Second)
    }

    //让主进程停住，不然主进程退了，goroutine也就退了
    var input string
    fmt.Scanln(&amp;input)
    fmt.Println(&quot;done&quot;)
}
</pre>
<p>运行的结果可能是：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="shell">
go_00  start at  2012-11-04 19:46:35.8974894 +0800 +0800
go_01  start at  2012-11-04 19:46:35.8974894 +0800 +0800
go_02  start at  2012-11-04 19:46:35.8974894 +0800 +0800
go_01  end at  2012-11-04 19:46:36.8975894 +0800 +0800
go_01  lasted  1.0001s
go_02  end at  2012-11-04 19:46:38.8987895 +0800 +0800
go_02  lasted  3.0013001s
go_00  end at  2012-11-04 19:46:39.8978894 +0800 +0800
go_00  lasted  4.0004s
</pre>
<h4>goroutine的并发安全性</h4>
<p>关于goroutine，我试了一下，无论是Windows还是Linux，基本上来说是用操作系统的线程来实现的。不过，goroutine有个特性，也就是说，<strong>如果一个goroutine没有被阻塞，那么别的goroutine就不会得到执行</strong>。这并不是真正的并发，如果你要真正的并发，你需要在你的main函数的第一行加上下面的这段代码：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">import &quot;runtime&quot;
...
runtime.GOMAXPROCS(4)</pre>
<p>还是让我们来看一个有并发安全性问题的示例（注意：我使用了C的方式来写这段Go的程序）</p>
<p>这是一个经常出现在教科书里卖票的例子，我启了5个goroutine来卖票，卖票的函数sell_tickets很简单，就是随机的sleep一下，然后对全局变量total_tickets作减一操作。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">
package main

import &quot;fmt&quot;
import &quot;time&quot;
import &quot;math/rand&quot;
import &quot;runtime&quot;

var total_tickets int32 = 10;

func sell_tickets(i int){
    for{
        if total_tickets &gt; 0 { //如果有票就卖
            time.Sleep( time.Duration(rand.Intn(5)) * time.Millisecond)
            total_tickets-- //卖一张票
            fmt.Println(&quot;id:&quot;, i, &quot;  ticket:&quot;, total_tickets)
        }else{
            break
        }
    }
}

func main() {
    runtime.GOMAXPROCS(4) //我的电脑是4核处理器，所以我设置了4
    rand.Seed(time.Now().Unix()) //生成随机种子

    for i := 0; i &lt; 5; i++ { //并发5个goroutine来卖票
         go sell_tickets(i)
    }
    //等待线程执行完
    var input string
    fmt.Scanln(&amp;input)
    fmt.Println(total_tickets, &quot;done&quot;) //退出时打印还有多少票
}</pre>
<p>这个程序毋庸置疑有并发安全性问题，所以执行起来你会看到下面的结果：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="shell">
$go run sell_tickets.go
id: 0   ticket: 9  
id: 0   ticket: 8  
id: 4   ticket: 7  
id: 1   ticket: 6  
id: 3   ticket: 5  
id: 0   ticket: 4  
id: 3   ticket: 3  
id: 2   ticket: 2  
id: 0   ticket: 1  
id: 3   ticket: 0  
id: 1   ticket: -1  
id: 4   ticket: -2  
id: 2   ticket: -3  
id: 0   ticket: -4  
-4 done</pre>
<p>可见，我们需要使用上锁，我们可以使用互斥量来解决这个问题。下面的代码，我只列出了修改过的内容：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang" data-enlighter-highlight="5,9,13,19"> package main
import &quot;fmt&quot;
import &quot;time&quot;
import &quot;math/rand&quot;
import &quot;sync&quot;
import &quot;runtime&quot;

var total_tickets int32 = 10;
var mutex = &amp;sync.Mutex{} //可简写成：var mutex sync.Mutex

func sell_tickets(i int){
    for total_tickets&gt;0 {
        mutex.Lock()
        if total_tickets &gt; 0 {
            time.Sleep( time.Duration(rand.Intn(5)) * time.Millisecond)
            total_tickets--
            fmt.Println(i, total_tickets)
        }
        mutex.Unlock()
    }
}
.......
......
</pre>
<h4>原子操作</h4>
<p>说到并发就需要说说原子操作，相信大家还记得我写的那篇《<a title="无锁队列的实现" href="https://coolshell.cn/articles/8239.html" target="_blank" rel="noopener noreferrer">无锁队列的实现</a>》一文，里面说到了一些CAS &#8211; CompareAndSwap的操作。Go语言也支持。你可以看一下相当的文档</p>
<p>我在这里就举一个很简单的示例：下面的程序有10个goroutine，每个会对cnt变量累加20次，所以，最后的cnt应该是200。如果没有atomic的原子操作，那么cnt将有可能得到一个小于200的数。</p>
<p>下面使用了atomic操作，所以是安全的。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang" data-enlighter-highlight="5,13,18">package main

import &quot;fmt&quot;
import &quot;time&quot;
import &quot;sync/atomic&quot;

func main() {
    var cnt uint32 = 0
    for i := 0; i &lt; 10; i++ {
        go func() {
            for i:=0; i&lt;20; i++ {
                time.Sleep(time.Millisecond)
                atomic.AddUint32(&amp;cnt, 1)
            }
        }()
    }
    time.Sleep(time.Second)//等一秒钟等goroutine完成
    cntFinal := atomic.LoadUint32(&amp;cnt)//取数据
    fmt.Println(&quot;cnt:&quot;, cntFinal)
}</pre>
<p>这样的函数还有很多，参看<a href="http://golang.org/pkg/sync/atomic/" target="_blank" rel="noopener noreferrer">go的atomic包文档</a>（被墙）</p>
<h4>Channel 信道</h4>
<p>Channal是什么？Channal就是用来通信的，就像Unix下的管道一样，在Go中是这样使用Channel的。</p>
<p>下面的程序演示了一个goroutine和主程序通信的例程。这个程序足够简单了。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang" data-enlighter-highlight="7,10">
package main

import &quot;fmt&quot;

func main() {
    //创建一个string类型的channel
    channel := make(chan string)

    //创建一个goroutine向channel里发一个字符串
    go func() { channel &lt;- &quot;hello&quot; }()

    msg := &lt;- channel
    fmt.Println(msg)
}[</pre>
<p><strong>指定channel的buffer</strong></p>
<p>指定buffer的大小很简单，看下面的程序：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang" data-enlighter-highlight="5">package main
import &quot;fmt&quot;

func main() {
    channel := make(chan string, 2)

    go func() {
        channel &lt;- &quot;hello&quot;
        channel &lt;- &quot;World&quot;
    }()

    msg1 := &lt;-channel
    msg2 := &lt;-channel
    fmt.Println(msg1, msg2)
}</pre>
<p><strong>Channel的阻塞</strong></p>
<p>注意，channel默认上是阻塞的，也就是说，如果Channel满了，就阻塞写，如果Channel空了，就阻塞读。于是，我们就可以使用这种特性来同步我们的发送和接收端。</p>
<p>下面这个例程说明了这一点，代码有点乱，不过我觉得不难理解。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">package main

import &quot;fmt&quot;
import &quot;time&quot;

func main() {

    channel := make(chan string) //注意: buffer为1

    go func() {
        channel &lt;- &quot;hello&quot;
        fmt.Println(&quot;write \&quot;hello\&quot; done!&quot;)

        channel &lt;- &quot;World&quot; //Reader在Sleep，这里在阻塞
        fmt.Println(&quot;write \&quot;World\&quot; done!&quot;)

        fmt.Println(&quot;Write go sleep...&quot;)
        time.Sleep(3*time.Second)
        channel &lt;- &quot;channel&quot;
        fmt.Println(&quot;write \&quot;channel\&quot; done!&quot;)
    }()

    time.Sleep(2*time.Second)
    fmt.Println(&quot;Reader Wake up...&quot;)

    msg := &lt;-channel
    fmt.Println(&quot;Reader: &quot;, msg)

    msg = &lt;-channel
    fmt.Println(&quot;Reader: &quot;, msg)

    msg = &lt;-channel //Writer在Sleep，这里在阻塞
    fmt.Println(&quot;Reader: &quot;, msg)
}</pre>
<p>上面的代码输出的结果如下：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="shell">
Reader Wake up...
Reader:  hello
write &quot;hello&quot; done!
write &quot;World&quot; done!
Write go sleep...
Reader:  World
write &quot;channel&quot; done!
Reader:  channel
</pre>
<p><strong>Channel阻塞的这个特性还有一个好处是，可以让我们的goroutine在运行的一开始就阻塞在从某个channel领任务，这样就可以作成一个类似于线程池一样的东西。关于这个程序我就不写了。我相信你可以自己实现的。</strong></p>
<p><strong>多个Channel的select</strong></p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">package main
import &quot;time&quot;
import &quot;fmt&quot;

func main() {
    //创建两个channel - c1 c2
    c1 := make(chan string)
    c2 := make(chan string)

    //创建两个goruntine来分别向这两个channel发送数据
    go func() {
        time.Sleep(time.Second * 1)
        c1 &lt;- &quot;Hello&quot;
    }()
    go func() {
        time.Sleep(time.Second * 1)
        c2 &lt;- &quot;World&quot;
    }()

    //使用select来侦听两个channel
    for i := 0; i &lt; 2; i++ {
        select {
        case msg1 := &lt;-c1:
            fmt.Println(&quot;received&quot;, msg1)
        case msg2 := &lt;-c2:
            fmt.Println(&quot;received&quot;, msg2)
        }
    }
}</pre>
<p>注意：上面的select是阻塞的，所以，才搞出ugly的for i &lt;2这种东西<strong>。<br />
</strong></p>
<p><strong>Channel select阻塞的Timeout</strong></p>
<p>解决上述那个for循环的问题，一般有两种方法：一种是阻塞但有timeout，一种是无阻塞。我们来看看如果给select设置上timeout的。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang" data-enlighter-highlight="8">
    for {
        timeout_cnt := 0
        select {
        case msg1 := &lt;-c1:
            fmt.Println(&quot;msg1 received&quot;, msg1)
        case msg2 := &lt;-c2:
            fmt.Println(&quot;msg2 received&quot;, msg2)
        case  &lt;-time.After(time.Second * 30)：
            fmt.Println(&quot;Time Out&quot;)
            timout_cnt++
        }
        if time_cnt &gt; 3 {
            break
        }
    }
</pre>
<p>上面代码中高亮的代码主要是用来让select返回的，注意 case中的time.After事件。</p>
<p><strong>Channel的无阻塞</strong></p>
<p>好，我们再来看看无阻塞的channel，其实也很简单，就是在select中加入default，如下所示：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang" data-enlighter-highlight="8">
    for {
        select {
        case msg1 := &lt;-c1:
            fmt.Println(&quot;received&quot;, msg1)
        case msg2 := &lt;-c2:
            fmt.Println(&quot;received&quot;, msg2)
        default: //default会导致无阻塞
            fmt.Println(&quot;nothing received!&quot;)
            time.Sleep(time.Second)
        }
    }
</pre>
<p><strong>Channel的关闭</strong></p>
<p>关闭Channel可以通知对方内容发送完了，不用再等了。参看下面的例程：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang" data-enlighter-highlight="19,27">package main

import &quot;fmt&quot;
import &quot;time&quot;
import &quot;math/rand&quot;

func main() {

    channel := make(chan string)
    rand.Seed(time.Now().Unix())

    //向channel发送随机个数的message
    go func () {
        cnt := rand.Intn(10)
        fmt.Println(&quot;message cnt :&quot;, cnt)
        for i:=0; i&lt;cnt; i++{
            channel &lt;- fmt.Sprintf(&quot;message-%2d&quot;, i)
        }
        close(channel) //关闭Channel
    }()

    var more bool = true
    var msg string
    for more {
        select{
        //channel会返回两个值，一个是内容，一个是还有没有内容
        case msg, more = &lt;- channel:
            if more {
                fmt.Println(msg)
            }else{
                fmt.Println(&quot;channel closed!&quot;)
            }
        }
    }
}</pre>
<h4>定时器</h4>
<p>Go语言中可以使用time.NewTimer或time.NewTicker来设置一个定时器，这个定时器会绑定在你的当前channel中，通过channel的阻塞通知机器来通知你的程序。</p>
<p>下面是一个timer的示例。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang" data-enlighter-highlight="9">package main

import &quot;time&quot;
import &quot;fmt&quot;

func main() {
    timer := time.NewTimer(2*time.Second)

    &lt;- timer.C
    fmt.Println(&quot;timer expired!&quot;)
}</pre>
<p>上面的例程看起来像一个Sleep，是的，不过Timer是可以Stop的。你需要注意Timer只通知一次。如果你要像C中的Timer能持续通知的话，你需要使用Ticker。下面是Ticker的例程：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang" data-enlighter-highlight="9">package main

import &quot;time&quot;
import &quot;fmt&quot;

func main() {
    ticker := time.NewTicker(time.Second)

    for t := range ticker.C {
        fmt.Println(&quot;Tick at&quot;, t)
    }
}</pre>
<p>上面的这个ticker会让你程序进入死循环，我们应该放其放在一个goroutine中。下面这个程序结合了timer和ticker</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">package main

import &quot;time&quot;
import &quot;fmt&quot;

func main() {

    ticker := time.NewTicker(time.Second)

    go func () {
        for t := range ticker.C {
            fmt.Println(t)
        }
    }()

    //设置一个timer，10钞后停掉ticker
    timer := time.NewTimer(10*time.Second)
    &lt;- timer.C

    ticker.Stop()
    fmt.Println(&quot;timer expired!&quot;)
}</pre>
<h4>Socket编程</h4>
<p>下面是我尝试的一个Echo Server的Socket代码，感觉还是挺简单的。</p>
<p><strong>Server端</strong></p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang" data-enlighter-highlight="12,19,24,33,36"> 
package main

import (
    &quot;net&quot;
    &quot;fmt&quot;
    &quot;io&quot;
)

const RECV_BUF_LEN = 1024

func main() {
    listener, err := net.Listen(&quot;tcp&quot;, &quot;0.0.0.0:6666&quot;)//侦听在6666端口
    if err != nil {
        panic(&quot;error listening:&quot;+err.Error())
    }
    fmt.Println(&quot;Starting the server&quot;)

    for {
        conn, err := listener.Accept() //接受连接
        if err != nil {
            panic(&quot;Error accept:&quot;+err.Error())
        }
        fmt.Println(&quot;Accepted the Connection :&quot;, conn.RemoteAddr())
        go EchoServer(conn)
    }
}

func EchoServer(conn net.Conn) {
    buf := make([]byte, RECV_BUF_LEN)
    defer conn.Close()

    for {
        n, err := conn.Read(buf);
        switch err {
            case nil:
                conn.Write( buf[0:n] )
            case io.EOF:
                fmt.Printf(&quot;Warning: End of data: %s \n&quot;, err);
                return
            default:
                fmt.Printf(&quot;Error: Reading data : %s \n&quot;, err);
                return
        }
     }
}
</pre>
<p><strong>Client端</strong></p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang" data-enlighter-highlight="12,23,31">
package main

import (
    &quot;fmt&quot;
    &quot;time&quot;
    &quot;net&quot;
)

const RECV_BUF_LEN = 1024

func main() {
    conn,err := net.Dial(&quot;tcp&quot;, &quot;127.0.0.1:6666&quot;)
    if err != nil {
        panic(err.Error())
    }
    defer conn.Close()

    buf := make([]byte, RECV_BUF_LEN)

    for i := 0; i &lt; 5; i++ {
        //准备要发送的字符串
        msg := fmt.Sprintf(&quot;Hello World, %03d&quot;, i)
        n, err := conn.Write([]byte(msg))
        if err != nil {
            println(&quot;Write Buffer Error:&quot;, err.Error())
            break
        }
        fmt.Println(msg)

        //从服务器端收字符串
        n, err = conn.Read(buf)
        if err !=nil {
            println(&quot;Read Buffer Error:&quot;, err.Error())
            break
        }
        fmt.Println(string(buf[0:n]))

        //等一秒钟
        time.Sleep(time.Second)
    }
}
</pre>
<h4>系统调用</h4>
<p>Go语言那么C，所以，一定会有一些系统调用。Go语言主要是通过两个包完成的。一个是<a href="http://golang.org/pkg/os/" target="_blank" rel="noopener noreferrer">os包</a>，一个是<a href="http://golang.org/pkg/syscall/" target="_blank" rel="noopener noreferrer">syscall包</a>。（注意，链接被墙）</p>
<p>这两个包里提供都是Unix-Like的系统调用，</p>
<ul>
<li>syscall里提供了什么Chroot/Chmod/Chmod/Chdir&#8230;，Getenv/Getgid/Getpid/Getgroups/Getpid/Getppid&#8230;，还有很多如Inotify/Ptrace/Epoll/Socket/&#8230;的系统调用。</li>
</ul>
<ul>
<li>os包里提供的东西不多，主要是一个跨平台的调用。它有三个子包，Exec（运行别的命令）, Signal（捕捉信号）和User（通过uid查name之类的）</li>
</ul>
<p>syscall包的东西我不举例了，大家可以看看《Unix高级环境编程》一书。</p>
<p>os里的取几个例：</p>
<p><strong>环境变量</strong></p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">package main

import &quot;os&quot;
import &quot;strings&quot;


func main() {
    os.Setenv(&quot;WEB&quot;, &quot;https://coolshell.cn&quot;) //设置环境变量
    println(os.Getenv(&quot;WEB&quot;)) //读出来

    for _, env := range os.Environ() { //穷举环境变量
        e := strings.Split(env, &quot;=&quot;)
        println(e[0], &quot;=&quot;, e[1])
    }
}
</pre>
<h4>执行命令行</h4>
<p>下面是一个比较简单的示例</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">
package main
import &quot;os/exec&quot;
import &quot;fmt&quot;
func main() {
    cmd := exec.Command(&quot;ping&quot;, &quot;127.0.0.1&quot;)
    out, err := cmd.Output()
    if err!=nil {
        println(&quot;Command Error!&quot;, err.Error())
        return
    }
    fmt.Println(string(out))
}</pre>
<p>正规一点的用来处理标准输入和输出的示例如下：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">package main

import (
    &quot;strings&quot;
    &quot;bytes&quot;
    &quot;fmt&quot;
    &quot;log&quot;
    &quot;os/exec&quot;
)

func main() {
    cmd := exec.Command(&quot;tr&quot;, &quot;a-z&quot;, &quot;A-Z&quot;)
    cmd.Stdin = strings.NewReader(&quot;some input&quot;)
    var out bytes.Buffer
    cmd.Stdout = &amp;out
    err := cmd.Run()
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf(&quot;in all caps: %q\n&quot;, out.String())
}</pre>
<h4>命令行参数</h4>
<p>Go语言中处理命令行参数很简单：(使用os的Args就可以了)</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang" data-enlighter-highlight="2">func main() {
    args := os.Args
    fmt.Println(args) //带执行文件的
    fmt.Println(args[1:]) //不带执行文件的
}</pre>
<p>在Windows下，如果运行结果如下：</p>
<p><code>C:\Projects\Go>go run args.go aaa bbb ccc ddd<br />
[C:\Users\haoel\AppData\Local\Temp\go-build742679827\command-line-arguments&#95;<br />
obj\a.out.exe aaa bbb ccc ddd]<br />
[aaa bbb ccc ddd]</code></p>
<p>那么，如果我们要搞出一些像 mysql -uRoot -hLocalhost -pPwd 或是像 cc -O3 -Wall -o a a.c 这样的命令行参数我们怎么办？Go提供了一个package叫flag可以容易地做到这一点</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang" data-enlighter-highlight="13">
package main
import &quot;flag&quot;
import &quot;fmt&quot;

func main() {

    //第一个参数是“参数名”，第二个是“默认值”，第三个是“说明”。返回的是指针
    host := flag.String(&quot;host&quot;, &quot;coolshell.cn&quot;, &quot;a host name &quot;)
    port := flag.Int(&quot;port&quot;, 80, &quot;a port number&quot;)
    debug := flag.Bool(&quot;d&quot;, false, &quot;enable/disable debug mode&quot;)

    //正式开始Parse命令行参数
    flag.Parse()

    fmt.Println(&quot;host:&quot;, *host)
    fmt.Println(&quot;port:&quot;, *port)
    fmt.Println(&quot;debug:&quot;, *debug)
}</pre>
<p>执行起来会是这个样子：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="shell">
#如果没有指定参数名，则使用默认值
$ go run flagtest.go
host: coolshell.cn
port: 80
debug: false

#指定了参数名后的情况
$ go run flagtest.go -host=localhost -port=22 -d
host: localhost
port: 22
debug: true

#用法出错了（如：使用了不支持的参数，参数没有=）
$ go build flagtest.go
$ ./flagtest -debug -host localhost -port=22
flag provided but not defined: -debug
Usage of flagtest:
  -d=false: enable/disable debug mode
  -host=&quot;coolshell.cn&quot;: a host name
  -port=80: a port number
exit status 2
</pre>
<p>感觉还是挺不错的吧。</p>
<h4>一个简单的HTTP Server</h4>
<p>代码胜过千言万语。呵呵。这个小程序让我又找回以前用C写CGI的时光了。（Go的官方文档是《<strong><a href="http://golang.org/doc/articles/wiki/" target="_blank" rel="noopener noreferrer">Writing Web Applications</a></strong>》）</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">package main

import (
    &quot;fmt&quot;
    &quot;net/http&quot;
    &quot;io/ioutil&quot;
    &quot;path/filepath&quot;
)

const http_root = &quot;/home/haoel/coolshell.cn/&quot;

func main() {
    http.HandleFunc(&quot;/&quot;, rootHandler)
    http.HandleFunc(&quot;/view/&quot;, viewHandler)
    http.HandleFunc(&quot;/html/&quot;, htmlHandler)

    http.ListenAndServe(&quot;:8080&quot;, nil)
}

//读取一些HTTP的头
func rootHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, &quot;rootHandler: %s\n&quot;, r.URL.Path)
    fmt.Fprintf(w, &quot;URL: %s\n&quot;, r.URL)
    fmt.Fprintf(w, &quot;Method: %s\n&quot;, r.Method)
    fmt.Fprintf(w, &quot;RequestURI: %s\n&quot;, r.RequestURI )
    fmt.Fprintf(w, &quot;Proto: %s\n&quot;, r.Proto)
    fmt.Fprintf(w, &quot;HOST: %s\n&quot;, r.Host) 
}

//特别的URL处理
func viewHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, &quot;viewHandler: %s&quot;, r.URL.Path)
}

//一个静态网页的服务示例。（在http_root的html目录下）
func htmlHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Printf(&quot;htmlHandler: %s\n&quot;, r.URL.Path)
    
    filename := http_root + r.URL.Path
    fileext := filepath.Ext(filename)

    content, err := ioutil.ReadFile(filename)
    if err != nil {
        fmt.Printf(&quot;   404 Not Found!\n&quot;)
        w.WriteHeader(http.StatusNotFound)
        return
    }
    
    var contype string
    switch fileext {
        case &quot;.html&quot;, &quot;htm&quot;:
            contype = &quot;text/html&quot;
        case &quot;.css&quot;:
            contype = &quot;text/css&quot;
        case &quot;.js&quot;:
            contype = &quot;application/javascript&quot;
        case &quot;.png&quot;:
            contype = &quot;image/png&quot;
        case &quot;.jpg&quot;, &quot;.jpeg&quot;:
            contype = &quot;image/jpeg&quot;
        case &quot;.gif&quot;:
            contype = &quot;image/gif&quot;
        default: 
            contype = &quot;text/plain&quot;
    }
    fmt.Printf(&quot;ext %s, ct = %s\n&quot;, fileext, contype)
    
    w.Header().Set(&quot;Content-Type&quot;, contype)
    fmt.Fprintf(w, &quot;%s&quot;, content)
    
}</pre>
<p>Go的功能库有很多，大家自己慢慢看吧。<strong>我再吐个槽——Go的文档真不好读。例子太少了</strong>。</p>
<p>先说这么多吧。这是我周末两天学Go语言学到的东西，写得太仓促了，而且还有一些东西理解不到位，还大家请指正！</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/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/8489.html">Go 语言简介（下）— 特性</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/8489.html/feed</wfw:commentRss>
			<slash:comments>95</slash:comments>
		
		
			</item>
		<item>
		<title>Go 语言简介（上）— 语法</title>
		<link>https://coolshell.cn/articles/8460.html</link>
					<comments>https://coolshell.cn/articles/8460.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Tue, 06 Nov 2012 00:27:48 +0000</pubDate>
				<category><![CDATA[Go 语言]]></category>
		<category><![CDATA[程序设计]]></category>
		<category><![CDATA[编程语言]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[golang]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=8460</guid>

					<description><![CDATA[<p>周末天气不好，只能宅在家里，于是就顺便看了一下Go语言，觉得比较有意思，所以写篇文章介绍一下。我想写一篇你可以在乘坐地铁或公交车上下班时就可以初步了解一门语言的...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/8460.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/8460.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>周末天气不好，只能宅在家里，于是就顺便看了一下Go语言，觉得比较有意思，所以写篇文章介绍一下。<strong>我想写一篇你可以在乘坐地铁或公交车上下班时就可以初步了解一门语言的文章</strong>。所以，下面的文章主要是以代码和注释为主。只需要你对C语言，Unix，Python有一点基础，我相信你会在30分钟左右读完并对Go语言有一些初步了解的。</p>
<p><img decoding="async" loading="lazy" class="aligncenter size-full wp-image-8485" title="go" src="https://coolshell.cn/wp-content/uploads/2012/11/go2.jpg" alt="" width="435" height="255" srcset="https://coolshell.cn/wp-content/uploads/2012/11/go2.jpg 435w, https://coolshell.cn/wp-content/uploads/2012/11/go2-300x176.jpg 300w" sizes="(max-width: 435px) 100vw, 435px" /></p>
<h4>Hello World</h4>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">
//文件名：hello.go
package main //声明本文件的package名

import &quot;fmt&quot; //import语言的fmt库——用于输出

func main() {
    fmt.Println(&quot;hello world&quot;)
}</pre>
<p><span id="more-8460"></span></p>
<h4>运行</h4>
<p>你可以有两种运行方式，</p>
<pre class="EnlighterJSRAW" data-enlighter-language="shell">
#解释执行（实际是编译成a.out再执行）
$go run hello.go
hello world

#编译执行
$go build hello.go

$ls
hello hello.go

$./hello
hello world</pre>
<h4>自己的package</h4>
<p>你可以使用GOPATH环境变量，或是使用相对路径来import你自己的package。</p>
<p>Go的规约是这样的：</p>
<p style="padding-left: 30px;">1）<strong>在import中，你可以使用相对路径，如 ./或 ../ 来引用你的package</strong></p>
<p style="padding-left: 30px;">2）<strong>如果没有使用相对路径，那么，go会去找$GOPATH/src/目录。</strong></p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">
//使用相对路径
import &quot;./haoel&quot;  //import当前目录里haoel子目录里的所有的go文件
</pre>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">
//使用GOPATH路径
import &quot;haoel&quot;  //import 环境变量 $GOPATH/src/haoel子目录里的所有的go文件
</pre>
<h4>fmt输出格式</h4>
<p>fmt包和libc里的那堆使用printf， scanf，fprintf，fscanf 很相似。下面的东西对于C程序员不会陌生。</p>
<p>注意：Println不支持，Printf才支持%式的输出：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">
package main

import &quot;fmt&quot;
import &quot;math&quot;

func main() {
    fmt.Println(&quot;hello world&quot;)

    fmt.Printf(&quot;%t\n&quot;, 1==2)
    fmt.Printf(&quot;二进制：%b\n&quot;, 255)
    fmt.Printf(&quot;八进制：%o\n&quot;, 255)
    fmt.Printf(&quot;十六进制：%X\n&quot;, 255)
    fmt.Printf(&quot;十进制：%d\n&quot;, 255)
    fmt.Printf(&quot;浮点数：%f\n&quot;, math.Pi)
    fmt.Printf(&quot;字符串：%s\n&quot;, &quot;hello world&quot;)
}</pre>
<p>当然，也可以使用如\n\t\r这样的和C语言一样的控制字符</p>
<h4>变量和常量</h4>
<p>变量的声明很像 javascript，使用 var关键字。注意：<strong>go是静态类型的语言</strong>，下面是代码：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">
//声明初始化一个变量
var  x int = 100
var str string = &quot;hello world&quot;&lt;/pre&gt;
//声明初始化多个变量
var  i, j, k int = 1, 2, 3

//不用指明类型，通过初始化值来推导
var b = true //bool型
</pre>
<p>还有一种定义变量的方式（这让我想到了Pascal语言，但完全不一样）</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">
x := 100 //等价于 var x int = 100;
</pre>
<p>常量很简单，使用const关键字：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">
const s string = &quot;hello world&quot;
const pi float32 = 3.1415926
</pre>
<h4>数组</h4>
<p>直接看代码（注意其中的for语句，和C很相似吧，就是没有括号了）</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">
func main() {
    var a [5]int
    fmt.Println(&quot;array a:&quot;, a)

    a[1] = 10
    a[3] = 30
    fmt.Println(&quot;assign:&quot;, a)

    fmt.Println(&quot;len:&quot;, len(a))

    b := [5]int{1, 2, 3, 4, 5}
    fmt.Println(&quot;init:&quot;, b)

    var c [2][3]int
    for i := 0; i &lt; 2; i++ {
        for j := 0; j &lt; 3; j++ {
            c[i][j] = i + j
        }
    }
    fmt.Println(&quot;2d: &quot;, c)
}
</pre>
<p>运行结果：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="shell">

array a: [0 0 0 0 0]
assign: [0 10 0 30 0]
len: 5
init: [1 2 3 4 5]
2d:  [[0 1 2] [1 2 3]]

</pre>
<h4>数组的切片操作</h4>
<p>这个很Python了。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">

a := [5]int{1, 2, 3, 4, 5}

b := a[2:4] // a[2] 和 a[3]，但不包括a[4]
fmt.Println(b)

b = a[:4] // 从 a[0]到a[4]，但不包括a[4]
fmt.Println(b)

b = a[2:] // 从 a[2]到a[4]，且包括a[2]
fmt.Println(b)

</pre>
<p><strong>但是，我们要记住，Golang的切片是共享内存的，也就是说，没有数据的复制，只是记录从哪切到哪的信息。</strong></p>
<h4>分支循环语句</h4>
<p><strong>if语句</strong></p>
<p>注意：if 语句没有圆括号，而必需要有花括号</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">
//if 语句
if x % 2 == 0 {
    //...
}
//if - else
if x % 2 == 0 {
    //偶数...
} else {
    //奇数...
}

//多分支
if num &lt; 0 {
    //负数
} else if num == 0 {
    //零
} else {
    //正数
}
</pre>
<p><strong>switch 语句</strong></p>
<p>注意：switch语句没有break，还可以使用逗号case多个值</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">
switch i {
    case 1:
        fmt.Println(&quot;one&quot;)
    case 2:
        fmt.Println(&quot;two&quot;)
    case 3:
        fmt.Println(&quot;three&quot;)
    case 4,5,6:
        fmt.Println(&quot;four, five, six&quot;)
    default:
        fmt.Println(&quot;invalid value!&quot;)
}
</pre>
<p><strong>for 语句</strong></p>
<p>前面你已见过了，下面再来看看for的三种形式：（注意：Go语言中没有while）</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">
//经典的for语句 init; condition; post
for i := 0; i&lt;10; i++{
     fmt.Println(i)
}

//精简的for语句 condition
i := 1
for i&lt;10 {
    fmt.Println(i)
    i++
}

//死循环的for语句 相当于for(;;)
i :=1
for {
    if i&gt;10 {
        break
    }
    i++
}
</pre>
<h4>关于分号</h4>
<p>从上面的代码我们可以看到代码里没有分号。其实，<strong>和C一样，Go的正式的语法使用分号来终止语句。和C不同的是，这些分号由词法分析器在扫描源代码过程中使用简单的规则自动插入分号，因此输入源代码多数时候就不需要分号了</strong>。</p>
<p>规则是这样的：如果在一个新行前方的最后一个标记是一个标识符（包括像<code>int</code>和<code>float64</code>这样的单词）、一个基本的如数值这样的文字、或以下标记中的一个时，会自动插入分号：</p>
<pre>break continue fallthrough return ++ -- ) }</pre>
<p>通常Go程序仅在<code>for</code>循环语句中使用分号，以此来分开初始化器、条件和增量单元。如果你在一行中写多个语句，也需要用分号分开。</p>
<p><strong>注意</strong>：<strong>无论任何时候，你都不应该将一个控制结构（(<code>if</code>、<code>for</code>、<code>switch</code>或<code>select</code>）的左大括号放在下一行。如果这样做，将会在大括号的前方插入一个分号，这可能导致出现不想要的结果</strong>。</p>
<h4>map</h4>
<p>map在别的语言里可能叫哈希表或叫dict，下面是和map的相关操作的代码，代码很容易懂</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">
func main(){
    m := make(map[string]int) //使用make创建一个空的map

    m[&quot;one&quot;] = 1
    m[&quot;two&quot;] = 2
    m[&quot;three&quot;] = 3

    fmt.Println(m) //输出 map[three:3 two:2 one:1] (顺序在运行时可能不一样)
    fmt.Println(len(m)) //输出 3

    v := m[&quot;two&quot;] //从map里取值
    fmt.Println(v) // 输出 2

    delete(m, &quot;two&quot;)
    fmt.Println(m) //输出 map[three:3 one:1]

    m1 := map[string]int{&quot;one&quot;: 1, &quot;two&quot;: 2, &quot;three&quot;: 3}
    fmt.Println(m1) //输出 map[two:2 three:3 one:1] (顺序在运行时可能不一样)

    for key, val := range m1{
        fmt.Printf(&quot;%s =&gt; %d \n&quot;, key, val)
        /*输出：(顺序在运行时可能不一样)
            three =&gt; 3
            one =&gt; 1
            two =&gt; 2*/
    }
}
</pre>
<h4>指针</h4>
<p>Go语言一样有指针，看代码</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">

var i int = 1
var pInt *int = &amp;i
//输出：i=1     pInt=0xf8400371b0       *pInt=1
fmt.Printf(&quot;i=%d\tpInt=%p\t*pInt=%d\n&quot;, i, pInt, *pInt)

*pInt = 2
//输出：i=2     pInt=0xf8400371b0       *pInt=2
fmt.Printf(&quot;i=%d\tpInt=%p\t*pInt=%d\n&quot;, i, pInt, *pInt)

i = 3
//输出：i=3     pInt=0xf8400371b0       *pInt=3
fmt.Printf(&quot;i=%d\tpInt=%p\t*pInt=%d\n&quot;, i, pInt, *pInt)

</pre>
<p>Go具有两个分配内存的机制，分别是内建的函数new和make。他们所做的事不同，所应用到的类型也不同，这可能引起混淆，但规则却很简单。</p>
<h4><strong>内存分配 </strong></h4>
<p><strong>new</strong> 是一个分配内存的内建函数，但不同于其他语言中同名的new所作的工作，<strong>它只是将内存清零，而不是初始化内存</strong>。new(T)为一个类型为T的新项目分配了值为零的存储空间并返回其地址，也就是一个类型为*T的值。用Go的术语来说，就是<strong>它返回了一个指向新分配的类型为T的零值的指针</strong>。</p>
<p><code><strong>make</strong>(T, </code><em>args</em><code>)</code>函数的目的与<code>new(T)</code>不同。它仅用于创建切片、map和chan（消息管道），并返回类型<code>T</code>（不是<code>*T</code>）的一个<strong>被初始化了的</strong>（不是<strong>零</strong>）实例。这种差别的出现是由于这三种类型实质上是对在使用前必须进行初始化的数据结构的引用。例如，切片是一个具有三项内容的描述符，包括指向数据（在一个数组内部）的指针、长度以及容量，在这三项内容被初始化之前，切片值为<code>nil</code>。对于切片、映射和信道，<code>make</code>初始化了其内部的数据结构并准备了将要使用的值。如：</p>
<p>下面的代码分配了一个整型数组，长度为10，容量为100，并返回前10个数组的切片</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">make([]int, 10, 100)</pre>
<p>以下示例说明了<code>new</code>和<code>make</code>的不同。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">
var p *[]int = new([]int)   // 为切片结构分配内存；*p == nil；很少使用
var v  []int = make([]int, 10) // 切片v现在是对一个新的有10个整数的数组的引用

// 不必要地使问题复杂化：
var p *[]int = new([]int)
fmt.Println(p) //输出：&amp;[]
*p = make([]int, 10, 10)
fmt.Println(p) //输出：&amp;[0 0 0 0 0 0 0 0 0 0]
fmt.Println((*p)[2]) //输出： 0

// 习惯用法:
v := make([]int, 10)
fmt.Println(v) //输出：[0 0 0 0 0 0 0 0 0 0]
</pre>
<h4>函数</h4>
<p>老实说，我对Go语言这种反过来声明变量类型和函数返回值的做法有点不满（保持和C一样的不可以吗? 呵呵）</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">
package main
import &quot;fmt&quot;

func max(a int, b int) int { //注意参数和返回值是怎么声明的

    if a &gt; b {
        return a
    }
    return b
}

func main(){
    fmt.Println(max(4, 5))
}

</pre>
<p><strong>函数返回多个值</strong></p>
<p>Go中很多Package 都会返回两个值，一个是正常值，一个是错误，如下所示：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">
package main
import &quot;fmt&quot;

func main(){
    v, e := multi_ret(&quot;one&quot;)
    fmt.Println(v,e) //输出 1 true

    v, e = multi_ret(&quot;four&quot;)
    fmt.Println(v,e) //输出 0 false

    //通常的用法(注意分号后有e)
    if v, e = multi_ret(&quot;four&quot;); e {
        // 正常返回
    }else{
        // 出错返回
    }
}

func multi_ret(key string) (int, bool){
    m := map[string]int{&quot;one&quot;: 1, &quot;two&quot;: 2, &quot;three&quot;: 3}

    var err bool
    var val int

    val, err = m[key]

    return val, err
}
</pre>
<p><strong>函数不定参数</strong></p>
<p>例子很清楚了，我就不多说了</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">
func sum(nums ...int) {
    fmt.Print(nums, &quot; &quot;)  //输出如 [1, 2, 3] 之类的数组
    total := 0
    for _, num := range nums { //要的是值而不是下标
        total += num
    }
    fmt.Println(total)
}
func main() {
    sum(1, 2)
    sum(1, 2, 3)

    //传数组
    nums := []int{1, 2, 3, 4}
    sum(nums...)
}</pre>
<p><strong>函数闭包</strong></p>
<p>nextNum这个函数返回了一个匿名函数，这个匿名函数记住了nextNum中i+j的值，并改变了i,j的值，于是形成了一个闭包的用法</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">
func nextNum() func() int {
    i,j := 1,1
    return func() int {
        var tmp = i+j
        i, j = j, tmp
        return tmp
    }
}
//main函数中是对nextNum的调用，其主要是打出下一个斐波拉契数
func main(){
    nextNumFunc := nextNum()
    for i:=0; i&lt;10; i++ {
        fmt.Println(nextNumFunc())
    }
}
</pre>
<p><strong>函数的递归</strong></p>
<p>和c基本是一样的</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">
func fact(n int) int {
    if n == 0 {
        return 1
    }
    return n * fact(n-1)
}

func main() {
    fmt.Println(fact(7))
}</pre>
<h4>结构体</h4>
<p>Go的结构体和C的基本上一样，不过在初始化时有些不一样，Go支持带名字的初始化。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">
type Person struct {
    name string
    age  int
    email string
}

func main() {
    //初始化
    person := Person{&quot;Tom&quot;, 30, &quot;tom@gmail.com&quot;}
    person = Person{name:&quot;Tom&quot;, age: 30, email:&quot;tom@gmail.com&quot;}

    fmt.Println(person) //输出 {Tom 30 tom@gmail.com}

    pPerson := &amp;person

    fmt.Println(pPerson) //输出 &amp;{Tom 30 tom@gmail.com}

    pPerson.age = 40
    person.name = &quot;Jerry&quot;
    fmt.Println(person) //输出 {Jerry 40 tom@gmail.com}
}
</pre>
<h4>结构体方法</h4>
<p>不多说了，看代码吧。</p>
<p>注意：Go语言中没有public, protected, private的关键字，所以，<strong>如果你想让一个方法可以被别的包访问的话，你需要把这个方法的第一个字母大写。这是一种约定</strong>。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">
type rect struct {
    width, height int
}

func (r *rect) area() int { //求面积
    return r.width * r.height
}

func (r *rect) perimeter() int{ //求周长
    return 2*(r.width + r.height)
}

func main() {
    r := rect{width: 10, height: 15}

    fmt.Println(&quot;面积: &quot;, r.area())
    fmt.Println(&quot;周长: &quot;, r.perimeter())

    rp := &amp;r
    fmt.Println(&quot;面积: &quot;, rp.area())
    fmt.Println(&quot;周长: &quot;, rp.perimeter())
}
</pre>
<h4>接口和多态</h4>
<p>接口意味着多态，下面是一个经典的例子，不用多说了，自己看代码吧。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">
//---------- 接 口 --------//
type shape interface {
    area() float64 //计算面积
    perimeter() float64 //计算周长
}

//--------- 长方形 ----------//
type rect struct {
    width, height float64
}

func (r *rect) area() float64 { //面积
    return r.width * r.height
}

func (r *rect) perimeter() float64 { //周长
    return 2*(r.width + r.height)
}

//----------- 圆  形 ----------//
type circle struct {
    radius float64
}

func (c *circle) area() float64 { //面积
    return math.Pi * c.radius * c.radius
}

func (c *circle) perimeter() float64 { //周长
    return 2 * math.Pi * c.radius
}

// ----------- 接口的使用 -----------//
func interface_test() {
    r := rect {width:2.9, height:4.8}
    c := circle {radius:4.3}

    s := []shape{&amp;r, &amp;c} //通过指针实现

    for _, sh := range s {
        fmt.Println(sh)
        fmt.Println(sh.area())
        fmt.Println(sh.perimeter())
    }
}
</pre>
<h4>错误处理 &#8211; Error接口</h4>
<p>函数错误返回可能是C/C++时最让人纠结的东西的，Go的多值返回可以让我们更容易的返回错误，其可以在返回一个常规的返回值之外，还能轻易地返回一个详细的错误描述。通常情况下，错误的类型是error，它有一个内建的接口。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">type error interface {
    Error() string
}</pre>
<p>还是看个示例吧：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">package main

import &quot;fmt&quot;
import &quot;errors&quot;

//自定义的出错结构
type myError struct {
    arg  int
    errMsg string
}
//实现Error接口
func (e *myError) Error() string {
    return fmt.Sprintf(&quot;%d - %s&quot;, e.arg, e.errMsg)
}

//两种出错
func error_test(arg int) (int, error) {
    if arg &lt; 0  {
         return -1, errors.New(&quot;Bad Arguments - negtive!&quot;)
     }else if arg &gt;256 {
        return -1, &amp;myError{arg, &quot;Bad Arguments - too large!&quot;}
    }
    return arg*arg, nil
}

//相关的测试
func main() {
    for _, i := range []int{-1, 4, 1000} {
        if r, e := error_test(i); e != nil {
            fmt.Println(&quot;failed:&quot;, e)
        } else {
            fmt.Println(&quot;success:&quot;, r)
        }
    }
}</pre>
<p>程序运行后输出：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="shell">
failed: Bad Arguments - negtive!
success: 16
failed: 1000 - Bad Arguments - too large!
</pre>
<h4>错误处理 &#8211; Defer</h4>
<p>下面的程序对于每一个熟悉C语言的人来说都不陌生（有资源泄露的问题），C++使用RAII来解决这种问题。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">
func CopyFile(dstName, srcName string) (written int64, err error) {
    src, err := os.Open(srcName)
    if err != nil {
        return
    }

    dst, err := os.Create(dstName)
    if err != nil {
        return
    }

    written, err = io.Copy(dst, src)
    dst.Close()
    src.Close()
    return
}</pre>
<p>Go语言引入了Defer来确保那些被打开的文件能被关闭。如下所示：（这种解决方式还是比较优雅的）</p>
<p>&lt;</p>
<p>pre class=&#8221;EnlighterJSRAW&#8221; data-enlighter-language=&#8221;golang&#8221; data-enlighter-highlight=&#8221;6,12&#8243;><br />
func CopyFile(dstName, srcName string) (written int64, err error) {<br />
    src, err := os.Open(srcName)<br />
    if err != nil {<br />
        return<br />
    }<br />
    defer src.Close()</p>
<pre><code>dst, err := os.Create(dstName)
if err != nil {
    return
}
defer dst.Close()

return io.Copy(dst, src)
</code></pre>
<p>}[/c]</p>
<p>Go的defer语句预设一个函数调用（延期的函数），该调用在函数执行defer返回时立刻运行。该方法显得不同常规，但却是处理上述情况很有效，无论函数怎样返回，都必须进行资源释放。</p>
<p>我们再来看一个defer函数的示例：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">
for i := 0; i &lt; 5; i++ {
    defer fmt.Printf(&quot;%d &quot;, i)
}</pre>
<p>被延期的函数以后进先出（LIFO）的顺行执行，因此以上代码在返回时将打印4 3 2 1 0。</p>
<p>总之，我个人觉得defer的函数行为有点怪异，我现在还没有完全搞清楚。</p>
<h4>错误处理 &#8211; Panic/Recover</h4>
<p>对于不可恢复的错误，Go提供了一个内建的panic函数，它将创建一个运行时错误并使程序停止（相当暴力）。该函数接收一个任意类型（往往是字符串）作为程序死亡时要打印的东西。当编译器在函数的结尾处检查到一个panic时，就会停止进行常规的return语句检查。</p>
<p>下面的仅仅是一个示例。实际的库函数应避免panic。如果问题可以容忍，最好是让事情继续下去而不是终止整个程序。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">
var user = os.Getenv(&quot;USER&quot;)

func init() {
    if user == &quot;&quot; {
        panic(&quot;no value for $USER&quot;)
    }
}</pre>
<p>当panic被调用时，它将立即停止当前函数的执行并开始逐级解开函数堆栈，同时运行所有被defer的函数。如果这种解开达到堆栈的顶端，程序就死亡了。但是，也可以使用内建的recover函数来重新获得Go程的控制权并恢复正常的执行。 对recover的调用会通知解开堆栈并返回传递到panic的参量。由于仅在解开期间运行的代码处在被defer的函数之内，recover仅在被延期的函数内部才是有用的。</p>
<p>你可以简单地理解为recover就是用来捕捉Painc的，防止程序一下子就挂掉了。</p>
<p>下面是一个例程，很简单了，不解释了</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">
func g(i int) {
    if i&gt;1 {
        fmt.Println(&quot;Panic!&quot;)
        panic(fmt.Sprintf(&quot;%v&quot;, i))
    }

}

func f() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println(&quot;Recovered in f&quot;, r)
        }
    }()

    for i := 0; i &lt; 4; i++ {
        fmt.Println(&quot;Calling g with &quot;, i)
        g(i)
        fmt.Println(&quot;Returned normally from g.&quot;)
     }
}

func main() {
    f()
    fmt.Println(&quot;Returned normally from f.&quot;)
}</pre>
<p>运行结果如下：（我们可以看到Painc后的for循环就没有往下执行了，但是main的程序还在往下走）</p>
<pre class="EnlighterJSRAW" data-enlighter-language="shell">
Calling g with  0
Returned normally from g.
Calling g with  1
Returned normally from g.
Calling g with  2
Panic!
Recovered in f 2
Returned normally from f.
</pre>
<p>你习惯这种编程方式吗？我觉得有点诡异。呵呵。</p>
<p>好了，上面是是一Go语言相关的编程语法的介绍，我没有事无巨细，只是让你了解一下Go语言是长什么样的。<strong>当然，这还没完，请期待下篇——Go语言的特性</strong>。<!--



<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/8460.html">Go 语言简介（上）— 语法</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/8460.html/feed</wfw:commentRss>
			<slash:comments>135</slash:comments>
		
		
			</item>
		<item>
		<title>少即是极多</title>
		<link>https://coolshell.cn/articles/7771.html</link>
					<comments>https://coolshell.cn/articles/7771.html#comments</comments>
		
		<dc:creator><![CDATA[Tim Shen]]></dc:creator>
		<pubDate>Thu, 05 Jul 2012 00:12:25 +0000</pubDate>
				<category><![CDATA[编程语言]]></category>
		<category><![CDATA[C++]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Unix]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=7771</guid>

					<description><![CDATA[<p>【感谢网友 @innocentim (Twitter) 投稿】 这是一篇翻译练习。力图保留原意。若有不准确处，求速速指出。猛击此处（墙）看原文。作者为Rob P...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/7771.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/7771.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>【<span style="color: #cc0000;"><strong>感谢网友 <a href="https://twitter.com/#!/innocentim" target="_blank">@innocentim</a></strong> (Twitter)<strong> 投稿</strong></span>】</p>
<p>这是一篇翻译练习。力图保留原意。若有不准确处，求速速指出。<a href="http://commandcenter.blogspot.com/2012/06/less-is-exponentially-more.html" target="_blank">猛击此处</a>（墙）看原文。作者为Rob Pike，贝尔实验室来的大牛，现在就职于Google。他主导了Go语言的创建工作。下面是正文——</p>
<p style="text-align: center; font-size: 9pt;"><span style="color: #999999;">——————————————正文分隔线——————————————</span></p>
<p><img decoding="async" loading="lazy" class="alignright size-full wp-image-7818" title="Less is More" src="https://coolshell.cn/wp-content/uploads/2012/06/Less-is-More-Box-ShopTab-300x282.jpg" alt="" width="300" height="282" srcset="https://coolshell.cn/wp-content/uploads/2012/06/Less-is-More-Box-ShopTab-300x282.jpg 300w, https://coolshell.cn/wp-content/uploads/2012/06/Less-is-More-Box-ShopTab-300x282-287x270.jpg 287w" sizes="(max-width: 300px) 100vw, 300px" />这是我在2012年6月的Go SF上演讲的文本。</p>
<p>这是一个个人演讲。 我承认，虽然面前的团队让Go诞生并延续，但是我的观点并不代表任何其他Go语言小组成员的意见。 我也想感谢Go SF的组织者提供这个和你们交流的机会。</p>
<p>几星期前我被问起:“你在推出Go的过程中遇到的最大的惊奇是什么？&#8221;我立即意识到了答案: 虽然我们希望C++程序员意识到Go是个较好的选择，但是令人意外的是，大多数Go程序员来自Python和Ruby这样的动态语言，而很少有来自C++的。</p>
<p>我们——Ken，Robert和我——是C++程序员(译者: Ken也用C++？)，当时在为解决我们所写的这类软件产生的问题设计一个新的语言。 这似乎有点自相矛盾，因为别的C++程序员根本不关心这些问题，更不会去设计一个语言。</p>
<p>我今天想说的是关于那些激发我们创造Go的事情，和为什么它本不应令我们如此惊讶。 我保证这些内容更多与Go相关而不是C++，所以即使你不很了解C++你也能跟得上。</p>
<p>回答可以这样归结: 你认为&#8221;少即是多&#8221;呢，还是&#8221;少就是少&#8221;？</p>
<p>这里有个比喻，将以真实故事的形式给出。 贝尔实验室中心原来发放3位数号码: 物理研究是111，计算科学研究是127，如此这般。 1980年代早期，一个便笺飞过来说&#8221;鉴于你们对研究的理解有所加深，将为你们的号码多加上一位，以便更好地体现你们的工作&#8221;。 所以我们中心的号码变成了1127。 Ron Hardin半当真地开玩笑说如果我们真的理解我们的世界更好一点的话，我们将丢掉一位数字，将127变成27。 当然主管没听到这个笑话(这也不是我们希望的)，但是我想这里面有点值得思考的东西。 少即是多。 你理解得越好，你将变得越简洁。</p>
<p><span id="more-7771"></span></p>
<p>先记住这句话。</p>
<p>回到2007年9月，我在做一个庞大的Google C++项目的细微但核心的部分。 开发必须交互进行，但是我这部分在我们的Google编译集群上要编译45分钟。 同时，有个消息传过来说一群在C++社区的Google员工将开一场讲座，介绍即将到来的C++0x(现在称为C++11)。</p>
<p>在那场持续一小时的讲座中，我们听说了诸如计划中的35个新特性的说法——事实上还有更多，但是那场讲座只说有35个。 有些特性当然是细微的，但是讲座中谈到的至少是足够重要的。 提到的特性中，有些十分微妙并难以理解，比如右值引用(rvalue references); 有些特别符合C++范儿，比如可变参数模板(variadic templates); 还有些十分疯狂，比如用户定义的字面量(user-defined literals)。</p>
<p>那时候我问了自己一个问题: C++社区真的觉得C++错在没有足够多的特性么？ 显然，从Ron Hardin的笑话的角度看，简化语言将比添加新特性取得更好的效果。 当然，对C++来说这很不靠谱，但是先记住这点。</p>
<p>在这场讲座的几个月之前我做了一场讲座(你可以通过<a href="http://video.google.com/videoplay？docid=810232012617965344" target="_blank">YouTube</a>看到)，讲的是一个我1980年代做的一个玩具并发编程语言。 这个语言叫<a href="ftp://cs.bell-labs.com/cm/cs/who/rsc/thread/newsqueak.pdf" target="_blank">Newsqueak</a>，而且显然地，它成为了Go的前身。</p>
<p>在我在Google工作的过程中，我发现我丢掉了Newsqueak中的一些点子。 现在我将重新思考它们，所以我才做了那场讲座。 我相信它们会让服务器端编程变得更容易，而且Google能真正从中获益。</p>
<p>我真的尝试将这些点子加入到C++中，可惜失败了。 我实在难以将一组并发操作融入到C++的控制流程中去——当真融进去的话，它们将变得十分丑陋，从而难以看到优越性。 另外，C++将它变得十分臃肿(虽然我从来没真正发现C++苗条过)。 所以我放弃了这个想法。</p>
<p>但是C++0x的讲座使我再次思考。 一件事十分困扰我——我相信也困扰着Ken和Robert——C++的新内存模型居然新增了原子类型。 为这个不堪重负的类型系统加上这么个细致精巧到极致类型机制十分的不靠谱，不是么？ 将语言和今日的硬件绑在一起似乎有点目光短浅并且不明智，因为硬件过几年就有大变。</p>
<p>那场C++0x讲座结束之后，我们回到办公室。 我开始了另一个编译(译者笑)，转过转过我的椅子，面对Robert，然后开始问一些尖锐的问题。 在编译完成之前，我们拉拢了Ken，并决定做些什么。 我们再也不想写C++了，并且我们——尤其是我——在写Google代码时，想让并发拿来就用。 同时我们也想解决&#8221;大系统编程&#8221;的问题，容后细说。</p>
<p>我们在白板上写下一组我们需要的东西——迫切需要的那种。 我们规划出大体的轮廓，忽略了语法细节和语义。</p>
<p>我仍然有一条碉堡了的那周的邮件线索。 这是一些摘录:</p>
<blockquote><p><strong>Robert</strong>: 起点: C，修补一些显而易见的瑕疵，去除繁杂的东西。 新增一些特性。</p>
<p><strong>Rob</strong>: 命名为&#8217;go&#8217;。 你可以为这个名字编造各种理由，但是它确实拥有很多好的特性。 它短小，易于打出。 工具么: goc，gol，goa。 如果有个交互式调试器/解释器，可以直接叫&#8217;go&#8217;。 代码后缀是。go。</p>
<p><strong>Robert</strong>: 空接口: interface {}。 将被所有接口实现(译者: 原文如此)，并且可以取代void*。</p></blockquote>
<p>我们并没有立即全部设计出来。 比如我们花了一年多才设计出了数组(array)和切片(slice)。 不过相当一部分重要的设计在最初的几天中浮现。</p>
<p>注意到Robert说C是起点，并非C++。 对于这点我不是很确定，不过我相信他说的是C，因为Ken在场(译者笑)。 但是最后我们并没有从C开始，这倒是真的。 我们从最初的草稿开始，仅仅从其它语言中借鉴琐碎的东西，比如运算符，各种括号和一些常见的关键字。(当然我们也借鉴了我们所知道的语言中的思想。)不管怎么说，我们破而后立，从头做起，以此来响应C++。 我们并非想做一个更好的C++，甚至不是一个更好的C。 它仅仅是一个对我们所关心的软件来说更好的语言。</p>
<p>最后，我们得到了既不同于C也不同于C++的东西，甚至比许多人意识到的还要不同。 我列了一个对于C和C++的Go的重要的简化的列表:</p>
<ul>
<li>常规的语法(不需要一个符号表来辅助解析)</li>
<li>GC机制(仅仅是GC)</li>
<li>没有头文件</li>
<li>显式依赖关系</li>
<li>没有循环依赖</li>
<li>数字常量仅仅是数字(译者: 没有类型)</li>
<li>int和int32不是同种类型</li>
<li>字母大小写将确定可见性</li>
<li>任何类型都可以有方法(没有类)</li>
<li>没有子类型继承(没有子类)</li>
<li>包级别的初始化和良好定义的初始化顺序</li>
<li>同一个包的文件一起编译</li>
<li>包级别的全局定义可以以任意顺序进行</li>
<li>没有算术类型转换(常量可以弥补)</li>
<li>接口是隐式实现的(没有&#8221;implements&#8221;声明)</li>
<li>嵌入的结构体(没有类型提升和子类)</li>
<li>方法像函数一样定义(不必定义在特殊的地方)</li>
<li>方法就是函数</li>
<li>接口就是方法(没有数据)</li>
<li>方法仅仅靠名字匹配(不是靠类型)</li>
<li>没有构造函数和析构函数</li>
<li>后置增量/减量运算符仅仅是语句，而不是表达式</li>
<li>没有前置增量/减量运算符</li>
<li>赋值号是语句，不是表达式</li>
<li>表达式求值顺序在赋值和函数调用时确定(没有所谓的&#8221;sequence point&#8221;)</li>
<li>没有指针算术</li>
<li>内存总是初始化为0</li>
<li>对本地变量取地址是合法的</li>
<li>方法中没有叫this的指针</li>
<li>分段式栈</li>
<li>没有常量或其它类型的注记</li>
<li>没有模板</li>
<li>没有异常</li>
<li>内建字符串，切片和映射(map)</li>
<li>数组边界检查</li>
</ul>
<p>并且，我相信通过这一系列的简化，Go将比C或C++更具有表现力。 少即是多。</p>
<p>但是我们没法一下子把所有部分都做出来。 我们需要构建最基础的部分，比如说类型系统的表示，能良好应用于实际的语法，和一些无法形容的但能让库更容易相互操作的东西。</p>
<p>我们同样增加了C或C++中没有的东西，比如切片和映射，组合字面量(？)，文件顶层的表达式(这虽是件大事，但是几乎不为人知)，反射机制，GC等等。 自然，还有并发。</p>
<p>一个显眼的缺少的东西是类型的继承。 请允许我粗暴地对待它一分钟。</p>
<p>早先构建Go的时候有人跟我说，他无法想象用一门没有泛型的语言工作。 正如我在别处说明的那样，我觉得这是个很诡异的言论。</p>
<p>公平起见，他用自己的话说可能是他真的很喜欢C++中STL的那些容器。 以辩论为目的的话，我们来正面看看他的言论。</p>
<p>他说的意味着: 他发现写一个容器，比如以int为元素类型的链表，或字符串映射是一种不能忍的重负。 我发现这是个很诡异的言论，因为我几乎没把时间花在那些个问题上，即使我在用没有泛型的语言。</p>
<p>但是，更重要的是，他说的那些表示<em>类型系统</em>将会解除这种负担。 <em>类型系统</em>。 不是多态函数，或语言级原语，或其它类型的辅助手段(helpers)，而仅仅是<em>类型系统</em>。</p>
<p>这就是粘住我的那个细节。</p>
<p>从C++或Java来Go的程序员怀念和类型系统在一起的日子，特别是带继承和子类的那部分。 也许我在类型系统方面是粗暴了些，但是我绝不觉得那套玩意非常具有表现力。</p>
<p>我已故的朋友Alain Fournier一次告诉我说他认为学术工作的最底层是分类学。 然后信不信由你，类型继承正是分类学。 你必须决定哪个萝卜扔哪个坑里，每个类型的父类型，A是否继承B或者B是否继承A。 一个可排序的数组是一个带有sort方法的数组呢，还是一个长得像数组的排序器呢？ 如果你觉得类型系统能解决所有设计上的问题，你必须做出这个无意义的选择。</p>
<p>我相信对编程来说那是个荒诞的思路。 真正的重点不在于事物之间的继承关系，而在于它们能提供些什么。</p>
<p>因此，接口这个概念进入了Go。 但是它们都是主要部分——真正的Go之道——的一部分。</p>
<p>如果C++和Java注重类型继承和类型系统的分类学，那末Go就注重组合。</p>
<p>Doug Mcilroy，Unix管道的最终发明人，在1964年(!)写道:</p>
<blockquote><p>我们应该有一些机制能将程序耦合(串)起来，像花园软管那样——当我们需要另一种方式传送数据时，拧紧另外一段即可。 I/O也可以这么做。</p></blockquote>
<p>这也是Go所提倡的道路。 Go吸收这个观点，然后把它推进得十分远。 这是一门关于(功能上的)组合和(调用上的)耦合的语言。</p>
<p>一个显然的例子是接口是组合各部分的途径。 关键是，那些部分是什么并不重要，如果某类型实现了M方法我就可以把这个方法填到接口里去。</p>
<p>另一个重要的例子是如何让并发性提供给我们不同的独立计算部分的组合。</p>
<p>并且还有一种不同寻常(但十分简单)的类型组合形式: 嵌入。</p>
<p>————————————————————————</p>
<p>我想提一个和之前不太相关的Go设计: Go被设计为大型团队用来写大型程序的语言。</p>
<p>这里有个概念是&#8221;大型编程&#8221;，并且不知何故C++和Java主宰了这个领域。 我相信这只是因为其历史巧合，或者是工业上的巧合。 但是被广泛接纳的观点是他们和面向对象设计有关。</p>
<p>我压根不相信这点。 大型软件需要确定的方法，但是更重要的是它需要强依赖性管理，干净的接口抽象和优越的文档工具。 C++没一点做得好的(虽然Java明显要好很多)。</p>
<p>我们还不知道Go语言能做到何种程度，因为现在还没有足够的软件是用Go写的。 但是我非常有信心于Go将会成为一个优越的大型编程语言。 时间会说明一切的。</p>
<p>————————————————————————</p>
<p>现在，回到我们演讲开始提的那个问题:</p>
<p>为什么Go，作为从头被设计为符合C++使用者习惯的语言，没有吸引很多C++程序员？</p>
<p>严肃点说，我觉得是因为Go和C++在哲学方面有着巨大的不同。</p>
<p>C++是将所有东西提到你指尖上(译者: 即多范式)。 我在C++11的FAQ上找到了这段引用:</p>
<blockquote><p>C++能优雅地，灵活地，零损耗地(相比于手工操纵代码)表达抽象的能力大幅提升了。</p></blockquote>
<p>Go并非这种&#8221;围绕式&#8221;的。 你并不需要所有的东西都内建好。 你不需要对每个执行细节进行精细的控制。 比如，你不需要RAII，但你拥有一个垃圾回收器，也意味着你不需要执行释放内存的操作。</p>
<p>你得到的是一组非常强有力但易于理解，易于用来构建积木的功能，这些积木可以用来组合出一个你需要的问题的解法。 这并不意味着它能像别的一些语言创造的解法一样快速，复杂，或带来思想上的激励，但是它总能保证易于书写，易于阅读，易于理解，易于维护，而且可能更安全。</p>
<p>从另一个角度说，这当然算作过度简化:</p>
<p>Python和Ruby程序员转到Go，因为他们不需要牺牲表达能力，却获得了性能的提升，并且能好好玩并发系统了。</p>
<p>C++程序员<em>并没有</em>转到Go是因为他们好不容易获得了对程序的精细控制，并且不想牺牲它们的任何一部分。 对他们而言，写软件不仅包括把事情做完，而且包括用特定的方式完成。</p>
<p>关键是，在将来，Go的成功将会颠覆他们的世界观。</p>
<p>并且从一开始我们就应该意识到这点。 对于C++11的新特性很兴奋的人们并不关心一个拥有如此少特性的语言。 即使最后他提供了如此多。</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/18360.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2018/05/300x262-150x150.jpg" alt="程序员练级攻略（2018)  与我的专栏" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_title">程序员练级攻略（2018)  与我的专栏</a></li><li ><a href="https://coolshell.cn/articles/8088.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/7.jpg" alt="对技术的态度" width="150" height="150" /></a><a href="https://coolshell.cn/articles/8088.html" class="wp_rp_title">对技术的态度</a></li><li ><a href="https://coolshell.cn/articles/7965.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2012/07/fork01jpg-150x150.jpg" alt="一个fork的面试题" width="150" height="150" /></a><a href="https://coolshell.cn/articles/7965.html" class="wp_rp_title">一个fork的面试题</a></li><li ><a href="https://coolshell.cn/articles/4990.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2011/07/programmer-150x150.png" alt="程序员技术练级攻略" width="150" height="150" /></a><a href="https://coolshell.cn/articles/4990.html" class="wp_rp_title">程序员技术练级攻略</a></li><li ><a href="https://coolshell.cn/articles/4102.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/25.jpg" alt="如何学好C语言" width="150" height="150" /></a><a href="https://coolshell.cn/articles/4102.html" class="wp_rp_title">如何学好C语言</a></li><li ><a href="https://coolshell.cn/articles/1761.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2009/11/spell_it_with_e-150x150.jpg" alt="Go语言源码的一个改动" width="150" height="150" /></a><a href="https://coolshell.cn/articles/1761.html" class="wp_rp_title">Go语言源码的一个改动</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/7771.html">少即是极多</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/7771.html/feed</wfw:commentRss>
			<slash:comments>50</slash:comments>
		
		
			</item>
	</channel>
</rss>
