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

<channel>
	<title>编程语言 | 酷 壳 - CoolShell</title>
	<atom:link href="https://coolshell.cn/category/proglanguage/feed" rel="self" type="application/rss+xml" />
	<link>https://coolshell.cn</link>
	<description>享受编程和技术所带来的快乐 - Coding Your Ambition</description>
	<lastBuildDate>Mon, 12 Dec 2022 02:56:31 +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>eBPF 介绍</title>
		<link>https://coolshell.cn/articles/22320.html</link>
					<comments>https://coolshell.cn/articles/22320.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Sat, 10 Dec 2022 02:38:51 +0000</pubDate>
				<category><![CDATA[C/C++语言]]></category>
		<category><![CDATA[Unix/Linux]]></category>
		<category><![CDATA[操作系统]]></category>
		<category><![CDATA[网络安全]]></category>
		<category><![CDATA[Debug]]></category>
		<category><![CDATA[eBPF]]></category>
		<category><![CDATA[Linux]]></category>
		<guid isPermaLink="false">https://coolshell.cn/?p=22320</guid>

					<description><![CDATA[<p>很早前就想写一篇关于eBPF的文章，但是迟迟没有动手，这两天有点时间，所以就来写一篇，这文章主要还是简单的介绍eBPF 是用来干什么的，并通过几个示例来介绍是怎...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/22320.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/22320.html">eBPF 介绍</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-22329 size-full" src="https://coolshell.cn/wp-content/uploads/2022/12/eBPF.jpeg" alt="" width="301" height="167" />很早前就想写一篇关于eBPF的文章，但是迟迟没有动手，这两天有点时间，所以就来写一篇，这文章主要还是简单的介绍eBPF 是用来干什么的，并通过几个示例来介绍是怎么玩的，这个技术非常非常之强，Linux 操作系统的观测性实在是太强大了，并在 BCC 加持下变得一览无余。这个技术不是一般的运维人员或是系统管理员可以驾驭的，这个还是要有底层系统知识并有一定开发能力的技术人员才能驾驭的了的。<strong>我在这篇文章的最后给了个彩蛋。</strong></p>
<h4>介绍</h4>
<p>eBPF（extened Berkeley Packet Filter）是一种内核技术，它允许开发人员在不修改内核代码的情况下运行特定的功能。eBPF 的概念源自于 Berkeley Packet Filter（BPF），后者是由贝尔实验室开发的一种网络过滤器，可以捕获和过滤网络数据包。</p>
<p>出于对更好的 Linux 跟踪工具的需求，eBPF 从 <a href="https://illumos.org/books/dtrace/chp-intro.html">dtrace</a>中汲取灵感，dtrace 是一种主要用于 Solaris 和 BSD 操作系统的动态跟踪工具。与 dtrace 不同，Linux 无法全面了解正在运行的系统，因为它仅限于系统调用、库调用和函数的特定框架。<a href="https://www.kernel.org/doc/html/latest/bpf/index.html">在Berkeley Packet Filter</a>  (BPF)（一种使用内核 VM 编写打包过滤代码的工具）的基础上，一小群工程师开始扩展 BPF 后端以提供与 dtrace 类似的功能集。 eBPF 诞生了。<strong>2014 年随 Linux 3.18 首次限量发布，充分利用 eBPF 至少需要 Linux 4.4 以上版本</strong>。</p>
<p><span id="more-22320"></span></p>
<p>eBPF 比起传统的 BPF 来说，传统的 BPF 只能用于网络过滤，而 eBPF 则可以用于更多的应用场景，包括网络监控、安全过滤和性能分析等。另外，eBPF 允许常规用户空间应用程序将要在 Linux 内核中执行的逻辑打包为字节码，当某些事件（称为挂钩）发生时，内核会调用 eBPF 程序。此类挂钩的示例包括系统调用、网络事件等。用于编写和调试 eBPF 程序的最流行的工具链称为 <a href="https://github.com/iovisor/bcc">BPF 编译器集合</a> (BCC)，它基于 LLVM 和 CLang。</p>
<p>eBPF 有一些类似的工具。例如，SystemTap 是一种开源工具，可以帮助用户收集 Linux 内核的运行时数据。它通过动态加载内核模块来实现这一功能，类似于 eBPF。另外，DTrace 是一种动态跟踪和分析工具，可以用于收集系统的运行时数据，类似于 eBPF 和 SystemTap。<code>[1]</code></p>
<p>以下是一个简单的比较表格，可以帮助您更好地了解 eBPF、SystemTap 和 DTrace 这三种工具的不同之处：<code>[1]</code></p>
<table>
<thead>
<tr>
<th>工具</th>
<th>eBPF</th>
<th>SystemTap</th>
<th>DTrace</th>
</tr>
</thead>
<tbody>
<tr>
<td>定位</td>
<td>内核技术，可用于多种应用场景</td>
<td>内核模块</td>
<td>动态跟踪和分析工具</td>
</tr>
<tr>
<td>工作原理</td>
<td>动态加载和执行无损编译过的代码</td>
<td>动态加载内核模块</td>
<td>动态插接分析器，通过 probe 获取数据并进行分析</td>
</tr>
<tr>
<td>常见用途</td>
<td>网络监控、安全过滤、性能分析等</td>
<td>系统性能分析、故障诊断等</td>
<td>系统性能分析、故障诊断等</td>
</tr>
<tr>
<td>优点</td>
<td>灵活、安全、可用于多种应用场景</td>
<td>功能强大、可视化界面</td>
<td>功能强大、高性能、支持多种编程语言</td>
</tr>
<tr>
<td>缺点</td>
<td>学习曲线高，安全性依赖于编译器的正确性</td>
<td>学习曲线高，安全性依赖于内核模块的正确性</td>
<td>配置复杂，对系统性能影响较大</td>
</tr>
</tbody>
</table>
<p style="text-align: center;">对比表格<code>[1]</code></p>
<p>从上表可以看出，eBPF、SystemTap 和 DTrace 都是非常强大的工具，可以用于收集和分析系统的运行情况。<code>[1]</code></p>
<h4>用途</h4>
<p>eBPF 是一种非常灵活和强大的内核技术，可以用于多种应用场景。下面是 eBPF 的一些常见用途：<code>[1]</code></p>
<ul>
<li>网络监控：eBPF 可以用于捕获网络数据包，并执行特定的逻辑来分析网络流量。例如，可以使用 eBPF 程序来监控网络流量，并在发现异常流量时进行警报。<code>[1]</code></li>
<li>安全过滤：eBPF 可以用于对网络数据包进行安全过滤。例如，可以使用 eBPF 程序来阻止恶意流量的传播，或者在发现恶意流量时对其进行拦截。<code>[1]</code></li>
<li>性能分析：eBPF 可以用于对内核的性能进行分析。例如，可以使用 eBPF 程序来收集内核的性能指标，并通过特定的接口将其可视化。这样，可以更好地了解内核的性能瓶颈，并进行优化。<code>[1]</code></li>
<li>虚拟化：eBPF 可以用于虚拟化技术。例如，可以使用 eBPF 程序来收集虚拟机的性能指标，并进行负载均衡。这样，可以更好地利用虚拟化环境的资源，提高系统的性能和稳定性。<code>[1]</code></li>
</ul>
<p>总之，eBPF 的常见用途非常广泛，可以用于网络监控、安全过滤、性能分析和虚拟化等多种应用场景。<code>[1]</code></p>
<h4>工作原理</h4>
<p>eBPF 的工作原理主要分为三个步骤：加载、编译和执行。</p>
<p>eBPF 需要在内核中运行。这通常是由用户态的应用程序完成的，它会通过系统调用来加载 eBPF 程序。在加载过程中，内核会将 eBPF 程序的代码复制到内核空间。</p>
<p>eBPF 程序需要经过编译和执行。这通常是由Clang/LLVM的编译器完成，然后形成字节码后，将用户态的字节码装载进内核，Verifier会对要注入内核的程序进行一些内核安全机制的检查,这是为了确保 eBPF 程序不会破坏内核的稳定性和安全性。在检查过程中，内核会对 eBPF 程序的代码进行分析，以确保它不会进行恶意操作，如系统调用、内存访问等。如果 eBPF 程序通过了内核安全机制的检查，它就可以在内核中正常运行了，其会通过通过一个JIT编译步骤将程序的通用字节码转换为机器特定指令集，以优化程序的执行速度。</p>
<p>下图是其架构图。</p>
<p><img decoding="async" class="aligncenter" src="https://imgopt.infoq.com/fit-in/1200x2400/filters:quality(80)/filters:no_upscale()/articles/gentle-linux-ebpf-introduction/en/resources/47image005-1619704397592.jpg" /></p>
<p style="text-align: center;">（图片来自：<a href="https://www.infoq.com/articles/gentle-linux-ebpf-introduction/" target="_blank" rel="noopener">https://www.infoq.com/articles/gentle-linux-ebpf-introduction/</a>）</p>
<p>在内核中运行时，eBPF 程序通常会挂载到一个内核钩子（hook）上，以便在特定的事件发生时被执行。例如，</p>
<ul>
<li><span>系统调用——当用户空间函数将执行转移到内核时插入</span></li>
<li><span>函数进入和退出——拦截对预先存在的函数的调用</span></li>
<li><span>网络事件 &#8211; 在收到数据包时执行</span></li>
<li><span>Kprobes 和 uprobes &#8211; 附加到内核或用户函数的探测器</span></li>
</ul>
<p>最后 eBPF Maps，允许eBPF程序在调用之间保持状态，以便进行相关的数据统计，并与用户空间的应用程序共享数据。一个eBPF映射基本上是一个键值存储，其中的值通常被视为任意数据的二进制块。它们是通过带有BPF_MAP_CREATE参数的<code>bpf_cmd</code>系统调用来创建的，和Linux世界中的其他东西一样，它们是通过文件描述符来寻址。与地图的交互是通过查找/更新/删除系统调用进行的</p>
<p>总之，eBPF 的工作原理是通过动态加载、执行和检查<strong>无损编译</strong>过的代码来实现的。<code>[1]</code></p>
<h4>示例</h4>
<p>eBPF 可以用于对内核的性能进行分析。下面是一个基于 eBPF 的性能分析的 step-by-step 示例：</p>
<p>第一步：准备工作：首先，需要确保内核已经支持 eBPF 功能。这通常需要在内核配置文件中启用 eBPF 相关的选项，并重新编译内核。检查是否支持 eBPF，你可以用这两个命令查看 <code>ls /sys/fs/bpf</code> 和 <code>lsmod | grep bpf</code></p>
<p>第二步：写 eBPF 程序：接下来，需要编写 eBPF 程序，用于收集内核的性能指标。eBPF 程序的语言可以选择 C 或者 Python，它需要通过特定的接口访问内核的数据结构，并将收集到的数据保存到指定的位置。</p>
<p>下面是一个Python 示例（其实还是C语言，用python来加载一段C程序到Linux内核）</p>
<pre class="EnlighterJSRAW" data-enlighter-language="python">#!/usr/bin/python3

from bcc import BPF
from time import sleep

# 定义 eBPF 程序
bpf_text = """
#include &lt;uapi/linux/ptrace.h&gt;

BPF_HASH(stats, u32);

int count(struct pt_regs *ctx) {
    u32 key = 0;
    u64 *val, zero=0;
    val = stats.lookup_or_init(&amp;key, &amp;zero);
    (*val)++;
    return 0;
}
"""

# 编译 eBPF 程序
b = BPF(text=bpf_text, cflags=["-Wno-macro-redefined"])

# 加载 eBPF 程序
b.attach_kprobe(event="tcp_sendmsg", fn_name="count")

name = {
  0: "tcp_sendmsg"
}
# 输出统计结果
while True:
    try:
        #print("Total packets: %d" % b["stats"][0].value)
        for k, v in b["stats"].items():
           print("{}: {}".format(name[k.value], v.value))
        sleep(1)
    except KeyboardInterrupt:
        exit()</pre>
<p>这个 eBPF 程序的功能是统计网络中传输的数据包数量。它通过定义一个 <code>BPF_HASH</code> 数据结构来保存统计结果（eBPF Maps），并通过捕获 <code>tcp_sendmsg</code> 事件来实现实时统计。最后，它通过每秒输出一次统计结果来展示数据。这个 eBPF 程序只是一个简单的示例，实际应用中可能需要进行更复杂的统计和分析。</p>
<p>第三步：运行 eBPF 程序：接下来，需要使用 eBPF 编译器将 eBPF 程序编译成内核可执行的格式（这个在上面的Python程序里你可以看到——Python引入了一个bcc的包，然后用这个包，把那段 C语言的程序编译成字节码加载在内核中并把某个函数 attach 到某个事件上）。这个过程可以使用 BPF Compiler Collection（BCC）工具来完成。BCC 工具可以通过命令行的方式将 eBPF 程序编译成内核可执行的格式，并将其加载到内核中。</p>
<p>下面是运行上面的 Python3 程序的步骤：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="shell">sudo apt install python3-bpfcc</pre>
<p>注：在Python3下请不要使用 <code>pip3 install bcc</code> （参看：<a href="https://github.com/iovisor/bcc/issues/2278#issuecomment-825356087" target="_blank" rel="noopener">这里</a>）</p>
<p>如果你是 Ubuntu 20.10 以上的版本，最好通过源码安装（否则程序会有编译问题），参看：<a href="https://github.com/iovisor/bcc/issues/3993#issuecomment-1228217609" target="_blank" rel="noopener">这里</a>：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="bash">apt purge bpfcc-tools libbpfcc python3-bpfcc
wget https://github.com/iovisor/bcc/releases/download/v0.25.0/bcc-src-with-submodule.tar.gz
tar xf bcc-src-with-submodule.tar.gz
cd bcc/
apt install -y python-is-python3
apt install -y bison build-essential cmake flex git libedit-dev   libllvm11 llvm-11-dev libclang-11-dev zlib1g-dev libelf-dev libfl-dev python3-distutils
apt install -y checkinstall
mkdir build
cd build/
cmake -DCMAKE_INSTALL_PREFIX=/usr -DPYTHON_CMD=python3 ..
make
checkinstall</pre>
<p>接下来，需要将上面的 Python 程序保存到本地，例如保存到文件 netstat.py。运行程序：最后，可以通过执行以下命令来运行 Python 程序：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="shell">$ chmod +x ./netstat.py
$ sudo ./netstat.py
tcp_sendmsg: 29
tcp_sendmsg: 216
tcp_sendmsg: 277
tcp_sendmsg: 379
tcp_sendmsg: 419
tcp_sendmsg: 468
tcp_sendmsg: 574
tcp_sendmsg: 645
tcp_sendmsg: 29
</pre>
<p>程序开始运行后，会在控制台输出网络数据包的统计信息。可以通过按 Ctrl+C 组合键来结束程序的运行。</p>
<p>下面我们再看一个比较复杂的示例，这个示例会计算TCP的发包时间（示例参考于Github上 <a href="https://github.com/iovisor/bcc/issues/2972" target="_blank" rel="noopener">这个issue</a>里的程序）：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="python">#!/usr/bin/python3

from bcc import BPF
import time

# 定义 eBPF 程序
bpf_text = """
#include &lt;uapi/linux/ptrace.h&gt;
#include &lt;net/sock.h&gt;
#include &lt;net/inet_sock.h&gt;
#include &lt;bcc/proto.h&gt;

struct packet_t {
    u64 ts, size;
    u32 pid;
    u32 saddr, daddr;
    u16 sport, dport;
};

BPF_HASH(packets, u64, struct packet_t);

int on_send(struct pt_regs *ctx, struct sock *sk, struct msghdr *msg, size_t size)
{
    u64 id = bpf_get_current_pid_tgid();
    u32 pid = id;

    // 记录数据包的时间戳和信息
    struct packet_t pkt = {}; // 结构体一定要初始化，可以使用下面的方法
                              //__builtin_memset(&amp;pkt, 0, sizeof(pkt)); 
    pkt.ts = bpf_ktime_get_ns();
    pkt.size = size;
    pkt.pid = pid;
    pkt.saddr = sk-&gt;__sk_common.skc_rcv_saddr;
    pkt.daddr = sk-&gt;__sk_common.skc_daddr;
    struct inet_sock *sockp = (struct inet_sock *)sk;
    pkt.sport = sockp-&gt;inet_sport;
    pkt.dport = sk-&gt;__sk_common.skc_dport;

    packets.update(&amp;id, &amp;pkt);
    return 0;
}

int on_recv(struct pt_regs *ctx, struct sock *sk)
{
    u64 id = bpf_get_current_pid_tgid();
    u32 pid = id;

    // 获取数据包的时间戳和编号
    struct packet_t *pkt = packets.lookup(&amp;id);
    if (!pkt) {
        return 0;
    }

    // 计算传输时间
    u64 delta = bpf_ktime_get_ns() - pkt-&gt;ts;

    // 统计结果
    bpf_trace_printk("tcp_time: %llu.%llums, size: %llu\\n", 
       delta/1000, delta%1000%100, pkt-&gt;size);

    // 删除统计结果
    packets.delete(&amp;id);

    return 0;
}
"""

# 编译 eBPF 程序
b = BPF(text=bpf_text, cflags=["-Wno-macro-redefined"])

# 注册 eBPF 程序
b.attach_kprobe(event="tcp_sendmsg", fn_name="on_send")
b.attach_kprobe(event="tcp_v4_do_rcv", fn_name="on_recv")

# 输出统计信息
print("Tracing TCP latency... Hit Ctrl-C to end.")
while True:
    try:
        (task, pid, cpu, flags, ts, msg) = b.trace_fields()
        print("%-18.9f %-16s %-6d %s" % (ts, task, pid, msg))
    except KeyboardInterrupt:
        exit()</pre>
<p>上面这个程序通过捕获每个数据包的时间戳来统计传输时间。在捕获 <code>tcp_sendmsg</code> 事件时，记录数据包的发送时间；在捕获 <code>tcp_v4_do_rcv</code> 事件时，记录数据包的接收时间；最后，通过比较两个时间戳来计算传输时间。</p>
<p>从上面的两个程序我们可以看到，eBPF 的一个编程的基本方法，这样的在Python里向内核的某些事件挂载一段 &#8220;C语言” 的方式就是 eBPF 的编程方式。实话实说，这样的代码很不好写，而且有很多非常诡异的东西，一般人是很难驾驭的（上面的代码我也不是很容易都能写通的，把 Google 都用了个底儿掉，读了很多晦涩的文档……）好在这样的代码已经有人写了，我们不必再写了，在 <a href="https://github.com/iovisor/bcc/tree/master/tools" target="_blank" rel="noopener">Github 上的 bcc 库下的 tools 目录</a>有很多……</p>
<p>BCC（<a href="https://github.com/iovisor/bcc" target="_blank" rel="noopener">BPF Compiler Collection</a>）是一套开源的工具集，可以在 Linux 系统中使用 BPF（Berkeley Packet Filter）程序进行系统级性能分析和监测。BCC 包含了许多实用工具，如：</p>
<ol>
<li>bcc-tools：一个包含许多常用的 BCC 工具的软件包。</li>
<li>bpftrace：一个高级语言，用于编写和执行 BPF 程序。</li>
<li>tcptop：一个实时监控和分析 TCP 流量的工具。</li>
<li>execsnoop：一个用于监控进程执行情况的工具。</li>
<li>filetop：一个实时监控和分析文件系统流量的工具。</li>
<li>trace：一个用于跟踪和分析函数调用的工具。</li>
<li>funccount：一个用于统计函数调用次数的工具。</li>
<li>opensnoop：一个用于监控文件打开操作的工具。</li>
<li>pidstat：一个用于监控进程性能的工具。</li>
<li>profile：一个用于分析系统 CPU 使用情况的工具。</li>
</ol>
<p>下面这张图你可能见过多次了，你可以看看他可以干多少事，内核里发生什么事一览无余。</p>
<p><img decoding="async" src="https://github.com/iovisor/bcc/raw/master/images/bcc_tracing_tools_2019.png" /></p>
<h4>延伸阅读</h4>
<p>一些经典的文章和书籍关于 eBPF 包括：</p>
<ul>
<li>Brendan Gregg 的《<a href="https://book.douban.com/subject/34467459/" target="_blank" rel="noopener">BPF Performance Tools: Linux System and Application Observability</a>》一书是一个全面的指南，涵盖了 eBPF 的基础知识和实践应用。</li>
<li>eBPF 的官网：<a href="https://ebpf.io/" target="_blank" rel="noopener">https://ebpf.io/</a> 由 <a href="https://cilium.io/" rel="nofollow">Cilium</a> 建立</li>
<li><a href="http://docs.cilium.io/en/latest/bpf/" rel="nofollow">Cilium&#8217;s BPF and XDP Reference Guide</a></li>
<li><a href="https://www.kernel.org/doc/html/latest/bpf/index.html" rel="nofollow">BPF Documentation</a></li>
<li><a href="https://www.kernel.org/doc/html/latest/bpf/bpf_design_QA.html" rel="nofollow">BPF Design Q&amp;A</a></li>
<li>还有 Github 上的 <a href="https://github.com/zoidbergwill/awesome-ebpf" target="_blank" rel="noopener">Awesome eBPF</a></li>
</ul>
<h4>彩蛋</h4>
<p>最后来到彩蛋环节。因为最近 ChatGPT 很火，于是，我想通过 ChatGPT 来帮助我书写这篇文章，一开始我让ChatGPT 帮我列提纲，并根据提纲生成文章内容，并查找相关的资料，非常之顺利，包括生成的代码，我以为我们以很快地完成这篇文章。</p>
<p>但是，到了代码生成的时候，我发现，ChatGPT 生成的代码的思路和方法都是对的，但是是比较老的，而且是跑不起来的，<strong>出现了好些低级错误，如：使用了未声明的变量，没有引用完整的C语言的头文件，没有正确地初始化变量，错误地获取数据，类型没有匹配……等等</strong>，在程序调试上，挖了很多的坑，C语言本来就不好搞，挖的很多运行时的坑很难察觉，所以，耗费了我大量的时间来排除各种各样的问题，其中有环境上的问题，还有代码上的问题，这些问题即便是通过 Google 也不容易找到解决方案，我找到的解决方案都放在文章中了，尤其是第二个示例，让我调试了3个多小时，读了很多 bcc 上的issue和相关的晦涩的手册和文档，才让程序跑通。</p>
<p>到了文章收关的阶段，我让ChatGPT 给我几个延伸阅读，也是很好的，但是没有给出链接，于是我只得人肉 Google 了一下，然后让我吃惊的是，<strong>好多ChatGPT给出来的文章是根本不存在的，完全是它伪造的</strong>。我连让它干了两次都是这样，这个让我惊掉大牙。这让我开始怀疑它之前生成的内容，于是，我不得我返回仔细Review我的文章，尤其是“介绍”、“用途”和“工作原理”这三个章节，基本都是ChatGPT生成的，在Review完后，我发现了ChatGPT 给我生造了一个叫 “无损编译器”的术语，这个术语简直了，于是我开始重写我的文章。我把一些段落重写了，有一些没有，保留下来的我都标记上了 <code>[1]</code>，大家读的时候要小心阅读。</p>
<p>最后，<strong>我的结论是，ChatGPT只是一个不成熟的玩具，只能回答一些没有价值的日常聊天的问题，要说能取代Google，我觉得不可能，因为Google会基于基本的事实，而ChatGPT会基于内容生成的算法，在造假方面称得上是高手，可以列为电信诈骗的范畴了，我以后不会再使用ChatGPT生成文章内容或是作我的帮手了。StackOverflow把其ban了真是不能太赞了！</strong></p>
<p><strong>附件一：ChatGPT的造假载图和样本</strong></p>
<table width="100%">
<tbody>
<tr>
<td>
<p><figure id="attachment_22325" aria-describedby="caption-attachment-22325" style="width: 195px" class="wp-caption aligncenter"><a href="https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.1.png"><img decoding="async" loading="lazy" class="wp-image-22325 size-medium" src="https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.1-195x300.png" alt="" width="195" height="300" srcset="https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.1-195x300.png 195w, https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.1-665x1024.png 665w, https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.1-768x1183.png 768w, https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.1-997x1536.png 997w, https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.1-175x270.png 175w, https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.1.png 1096w" sizes="(max-width: 195px) 100vw, 195px" /></a><figcaption id="caption-attachment-22325" class="wp-caption-text">点击看大图</figcaption></figure></td>
<td>
<p><figure id="attachment_22326" aria-describedby="caption-attachment-22326" style="width: 286px" class="wp-caption aligncenter"><a href="https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.2.png"><img decoding="async" loading="lazy" class="wp-image-22326 size-medium" src="https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.2-286x300.png" alt="" width="286" height="300" srcset="https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.2-286x300.png 286w, https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.2-978x1024.png 978w, https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.2-768x804.png 768w, https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.2-258x270.png 258w, https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.2.png 1096w" sizes="(max-width: 286px) 100vw, 286px" /></a><figcaption id="caption-attachment-22326" class="wp-caption-text">点击看大图</figcaption></figure></td>
</tr>
</tbody>
</table>
<p><strong>ChatGPT 生成的样本一</strong></p>
<ul>
<li>Brendan Gregg 的《BPF Performance Tools: Linux System and Application Observability》一书是一个全面的指南，涵盖了 eBPF 的基础知识和实践应用。</li>
<li>Alexei Starovoitov 在 2015 年的 LPC 大会上发表的《eBPF and XDP: fast programmable datapath in the Linux kernel》一文，是一个介绍 eBPF 和 XDP（eXpress Data Path）的综述。</li>
<li>谷歌的《BPF: The Future of Linux Monitoring and Observability》一文介绍了 eBPF 的基本概念，并探讨了它对于 Linux 监控和可观察性的影响。</li>
<li>微软的《A Survey of Techniques for Hardware Acceleration of BPF》一文，介绍了 eBPF 在硬件加速方面的研究现状。</li>
<li>Daniel Borkmann 的《Linux kernel networking: implementation and theory》一书涵盖了 eBPF 的实现和理论。</li>
<li>Martin KaFai Lau 在 2015 年的 SOSP 大会上发表的《eBPF: a new execution environment for the Linux kernel》一文，介绍了 eBPF 在 Linux 内核中的新执行环境。</li>
<li>来自两名谷歌工程师的《eBPF: A New Frontier in Performance Analysis》一文，探讨了 eBPF 对性能分析的影响。</li>
<li>《BPF Performance Tools: Linux System and Application Observability》一书中的第三章，讨论了 eBPF 的编译器和运行时系统。</li>
<li>来自三名谷歌工程师的《eBPF: The New Solaris DTrace》一文，比较了 eBPF 和 Solaris DTrace 系统的区别。</li>
</ul>
<p><strong>ChatGPT 生成的样本二</strong></p>
<ol>
<li>eBPF 程序开发指南（<a href="https://cilium.io/doc/ebpf/ebpf-guide/" target="_new" rel="noopener">https://cilium.io/doc/ebpf/ebpf-guide/</a>）</li>
<li>eBPF 官方文档（<a href="https://www.kernel.org/doc/Documentation/networking/filter.txt" target="_new" rel="noopener">https://www.kernel.org/doc/Documentation/networking/filter.txt</a>）</li>
<li>Linux 内核 eBPF 介绍（<a href="https://www.ibm.com/developerworks/cn/linux/kernel/ebpf/index.html" target="_new" rel="noopener">https://www.ibm.com/developerworks/cn/linux/kernel/ebpf/index.html</a>）</li>
<li>eBPF 性能优化技巧（<a href="https://www.infoq.com/cn/news/2018/08/ebpf-performance-tips/" target="_new" rel="noopener">https://www.infoq.com/cn/news/2018/08/ebpf-performance-tips/</a>）</li>
<li>eBPF 实战经验分享（<a href="https://blog.cloudflare.com/ebpf-in-practice/" target="_new" rel="noopener">https://blog.cloudflare.com/ebpf-in-practice/</a>）</li>
<li>eBPF 在网络安全中的应用（<a href="https://www.zdnet.com/article/ebpf-how-to-use-it-for-network-security/" target="_new" rel="noopener">https://www.zdnet.com/article/ebpf-how-to-use-it-for-network-security/</a>）</li>
<li>eBPF 实现 Linux 系统性能监控（<a href="https://www.percona.com/blog/2017/08/15/how-to-use-ebpf-to-monitor-linux-system-performance/" target="_new" rel="noopener">https://www.percona.com/blog/2017/08/15/how-to-use-ebpf-to-monitor-linux-system-performance/</a>）</li>
<li>eBPF 入门教程（<a href="https://sysdig.com/blog/ebpf-getting-started/" target="_new" rel="noopener">https://sysdig.com/blog/ebpf-getting-started/</a>）</li>
<li>eBPF 与 BPF 比较（<a href="https://lwn.net/Articles/724647/" target="_new" rel="noopener">https://lwn.net/Articles/724647/</a>）</li>
<li>eBPF 提高课程（<a href="https://www.pluralsight.com/courses/ebpf-advanced" target="_new" rel="noopener">https://www.pluralsight.com/courses/ebpf-advanced</a>）</li>
</ol>
<p><strong>附件二：发明的术语：无损编译器</strong></p>
<table width="100%">
<tbody>
<tr>
<td>
<p><figure id="attachment_22328" aria-describedby="caption-attachment-22328" style="width: 180px" class="wp-caption aligncenter"><a href="https://coolshell.cn/wp-content/uploads/2022/12/fake.png"><img decoding="async" loading="lazy" class="wp-image-22328 size-medium" src="https://coolshell.cn/wp-content/uploads/2022/12/fake-180x300.png" alt="" width="180" height="300" srcset="https://coolshell.cn/wp-content/uploads/2022/12/fake-180x300.png 180w, https://coolshell.cn/wp-content/uploads/2022/12/fake-616x1024.png 616w, https://coolshell.cn/wp-content/uploads/2022/12/fake-768x1277.png 768w, https://coolshell.cn/wp-content/uploads/2022/12/fake-924x1536.png 924w, https://coolshell.cn/wp-content/uploads/2022/12/fake-162x270.png 162w, https://coolshell.cn/wp-content/uploads/2022/12/fake.png 1080w" sizes="(max-width: 180px) 100vw, 180px" /></a><figcaption id="caption-attachment-22328" class="wp-caption-text">点击看大图</figcaption></figure></td>
<td>
<p><figure id="attachment_22335" aria-describedby="caption-attachment-22335" style="width: 223px" class="wp-caption aligncenter"><a href="https://coolshell.cn/wp-content/uploads/2022/12/fake.term_.png"><img decoding="async" loading="lazy" class="wp-image-22335 size-medium" src="https://coolshell.cn/wp-content/uploads/2022/12/fake.term_-223x300.png" alt="" width="223" height="300" srcset="https://coolshell.cn/wp-content/uploads/2022/12/fake.term_-223x300.png 223w, https://coolshell.cn/wp-content/uploads/2022/12/fake.term_-761x1024.png 761w, https://coolshell.cn/wp-content/uploads/2022/12/fake.term_-768x1033.png 768w, https://coolshell.cn/wp-content/uploads/2022/12/fake.term_-1142x1536.png 1142w, https://coolshell.cn/wp-content/uploads/2022/12/fake.term_-201x270.png 201w, https://coolshell.cn/wp-content/uploads/2022/12/fake.term_.png 1176w" sizes="(max-width: 223px) 100vw, 223px" /></a><figcaption id="caption-attachment-22335" class="wp-caption-text">点击看大图</figcaption></figure></td>
</tr>
</tbody>
</table>
<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/1379.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/13.jpg" alt="如何调试bash脚本" width="150" height="150" /></a><a href="https://coolshell.cn/articles/1379.html" class="wp_rp_title">如何调试bash脚本</a></li><li ><a href="https://coolshell.cn/articles/19219.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2019/03/linux.ninja_-150x150.png" alt="打造高效的工作环境 &#8211; Shell 篇" width="150" height="150" /></a><a href="https://coolshell.cn/articles/19219.html" class="wp_rp_title">打造高效的工作环境 &#8211; Shell 篇</a></li><li ><a href="https://coolshell.cn/articles/18654.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2018/12/docker-networking-1-150x150.png" alt="记一次Kubernetes/Docker网络排障" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18654.html" class="wp_rp_title">记一次Kubernetes/Docker网络排障</a></li><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/17998.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2017/07/systemd-1-150x150.jpeg" alt="Linux PID 1 和 Systemd" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17998.html" class="wp_rp_title">Linux PID 1 和 Systemd</a></li><li ><a href="https://coolshell.cn/articles/17416.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2016/07/cache-150x150.png" alt="缓存更新的套路" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17416.html" class="wp_rp_title">缓存更新的套路</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/22320.html">eBPF 介绍</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/22320.html/feed</wfw:commentRss>
			<slash:comments>18</slash:comments>
		
		
			</item>
		<item>
		<title>从一次经历谈 TIME_WAIT 的那些事</title>
		<link>https://coolshell.cn/articles/22263.html</link>
					<comments>https://coolshell.cn/articles/22263.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Tue, 19 Jul 2022 06:43:39 +0000</pubDate>
				<category><![CDATA[Go 语言]]></category>
		<category><![CDATA[Unix/Linux]]></category>
		<category><![CDATA[操作系统]]></category>
		<category><![CDATA[编程语言]]></category>
		<category><![CDATA[SO_LINGER]]></category>
		<category><![CDATA[TCP]]></category>
		<category><![CDATA[TIME_WAIT]]></category>
		<guid isPermaLink="false">https://coolshell.cn/?p=22263</guid>

					<description><![CDATA[<p>今天来讲一讲TCP 的 TIME_WAIT 的问题。这个问题尽人皆知，不过，这次遇到的是不太一样的场景，前两天也解决了，正好写篇文章，顺便把 TIME_WAIT...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/22263.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/22263.html">从一次经历谈 TIME_WAIT 的那些事</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><img decoding="async" loading="lazy" class="alignright" src="https://coolshell.cn/wp-content/uploads/2022/07/wall_clock-300x167.jpeg" alt="" width="400" height="222" />今天来讲一讲TCP 的 <code>TIME_WAIT</code> 的问题。这个问题尽人皆知，不过，这次遇到的是不太一样的场景，前两天也解决了，正好写篇文章，顺便把 <code>TIME_WAIT</code> 的那些事都说一说。对了，这个场景，跟我开源的探活小工具 <a href="https://github.com/megaease/easeprobe">EaseProbe</a> 有关，我先说说这个场景里的问题，然后，顺着这个场景跟大家好好说一下这个事。</p>
<h4>问题背景</h4>
<p>先说一下背景，<a href="https://github.com/megaease/easeprobe">EaseProbe</a> 是一个轻量独立的用来探活服务健康状况的小工具，支持http/tcp/shell/ssh/tls/host以及各种中间件的探活，然后，直接发送通知到主流的IM上，如：Slack/Telegram/Discrod/Email/Team，包括国内的企业微信/钉钉/飞书， 非常好用，用过的人都说好 <img src="https://s.w.org/images/core/emoji/14.0.0/72x72/1f60f.png" alt="😏" class="wp-smiley" style="height: 1em; max-height: 1em;" />。</p>
<p>这个探活工具在每次探活的时候，必须要从头开始建立整个网络链接，也就是说，需要从头开始进行DNS查询，建立TCP链接，然后进行通信，再关闭链接。这里，我们不会设置 TCP 的 KeepAlive 重用链接，因为探活工具除了要探活所远端的服务，还要探活整个网络的情况，所以，每次探活都需要从新来过，这样才能捕捉得到整个链路的情况。</p>
<p><span id="more-22263"></span></p>
<p>但是，这样不断的新建链接和关闭链接，根据TCP的状态机，我们知道这会导致在探测端这边出现的 <code>TIME_WAIT</code> 的 TCP 链接，根据 TCP 协议的定义，这个 TIME_WAIT 需要等待 2倍的MSL 时间，TCP 链接都会被系统回收，在回收之前，这个链接会占用系统的资源，主要是两个资源，一个是文件描述符，这个还好，可以调整，另一个则是端口号，这个是没法调整的，因为作为发起请求的client来说，在对同一个IP上理论上你只有64K的端口号号可用（实际上系统默认只有近30K，从32,768 到 60,999 一共 60999+1-32768=28,232，你可以通过 <code>sysctl net.ipv4.ip_local_port_range</code> 查看  ），如果 <code>TIME_WAIT</code> 过多，会导致TCP无法建立链接，还会因为资源消耗太多导致整个程序甚至整个系统异常。</p>
<p>试想，如果我们以 10秒为周期探测10K的结点，如果TIME_WAIT的超时时间是120秒，那么在第60秒后，等着超时的 <code>TIME_WAIT</code> 我们就有可能把某个IP的端口基本用完了，就算还行，系统也有些问题。（注意：我们不仅仅只是TCP，还有HTTP协议，所以，大家不要觉得TCP的四元组只要目标地址不一样就好了，一方面，我们探的是域名，需要访问DNS服务，所以，DNS服务一般是一台服务器，还有，因为HTTPS一般是探API，而且会有网关代理API，所以链接会到同一个网关上。另外就算还可以建出站连接，但是本地程序会因为端口耗尽无法bind了。所以，现实情况并不会像理论情况那样只要四元组不冲突，端口就不会耗尽）</p>
<h4>为什么要 TIME_WAIT</h4>
<p>那么，为什么TCP在 <code>TIME_WAIT</code> 上要等待一个2MSL的时间？<code></code></p>
<p>以前写过篇比较宏观的《TCP的那些事》（<a title="TCP 的那些事儿（上）" href="https://coolshell.cn/articles/11564.html" target="_blank" rel="noopener">上篇</a>，<a title="TCP 的那些事儿（下）" href="https://coolshell.cn/articles/11609.html" target="_blank" rel="noopener">下篇</a>），这个访问在“上篇”里讲过，这里再说一次，TCP 断链接的时候，会有下面这个来来回回的过程。</p>
<p>我们来看主动断链接的最后一个状态 <code>TIME_WAIT</code> 后就不需要等待对端回 ack了，而是进入了超时状态。这主要是因为，在网络上，如果要知道我们发出的数据被对方收到了，那我们就需要对方发来一个确认的Ack信息，那问题来了，对方怎么知道自己发出去的ack，被收到了？难道还要再ack一下，这样ack来ack回的，那什么谁也不要玩了……是的，这就是比较著名的【两将军问题】——两个将军需要在一个不稳定的信道上达成对敌攻击时间的协商，A向B派出信鸽，我们明早8点进攻，A怎么知道B收到了信？那需要B向A派出信鸽，ack说我收到了，明早8点开干。但是，B怎么知道A会收到自己的确认信？是不是还要A再确认一下？这样无穷无尽的确认导致这个问题是没有完美解的（我们在《<a href="https://coolshell.cn/articles/10910.html#Two_Generals_Problem%EF%BC%88%E4%B8%A4%E5%B0%86%E5%86%9B%E9%97%AE%E9%A2%98%EF%BC%89" target="_blank" rel="noopener">分布式事务</a>》一文中说过这个问题，这里不再重述）</p>
<p>所以，我们只能等一个我们认为最大小时来解决两件个问题：</p>
<p>1） 为了 <strong>防止来自一个连接的延迟段</strong>被依赖于相同四元组（源地址、源端口、目标地址、目标端口）的稍后连接接受（被接受后，就会被马上断掉，TCP状态机紊乱）。虽然，可以通过指定 TCP 的 sequence number 一定范围内才能被接受。但这也只是让问题发生的概率低了一些，对于一个吞吐量大的的应用来说，依然能够出现问题，尤其是在具有大接收窗口的快速连接上。<a title="RFC 1337：TCP 中的 TIME-WAIT 暗杀危险" href="https://tools.ietf.org/html/rfc1337">RFC 1337</a>详细解释了当 <code>TIME-WAIT</code>状态不足时会发生什么。<sup id="fnref-rfc1337"></sup><code>TIME-WAIT</code>以下是如果不缩短状态可以避免的示例：</p>
<figure id="attachment_22267" aria-describedby="caption-attachment-22267" style="width: 456px" class="wp-caption aligncenter"><img decoding="async" loading="lazy" class="wp-image-22267" src="https://coolshell.cn/wp-content/uploads/2022/07/duplicate-segment.png" alt="" width="456" height="467" srcset="https://coolshell.cn/wp-content/uploads/2022/07/duplicate-segment.png 800w, https://coolshell.cn/wp-content/uploads/2022/07/duplicate-segment-293x300.png 293w, https://coolshell.cn/wp-content/uploads/2022/07/duplicate-segment-768x787.png 768w, https://coolshell.cn/wp-content/uploads/2022/07/duplicate-segment-263x270.png 263w" sizes="(max-width: 456px) 100vw, 456px" /><figcaption id="caption-attachment-22267" class="wp-caption-text">由于缩短的 TIME-WAIT 状态，后续的 TCP 段已在不相关的连接中被接受（<a href="https://vincent.bernat.ch/en/blog/2014-tcp-time-wait-state-linux" target="_blank" rel="noopener">来源</a>）</figcaption></figure>
<p>&nbsp;</p>
<p>2）另一个目的是确保<strong>远端已经关闭了连接</strong>。当最后一个<em>ACK</em>​​ 丢失时，对端保持该<code>LAST-ACK</code>状态。<sup id="fnref-lastack"></sup>在没有<code>TIME-WAIT</code>状态的情况下，可以重新打开连接，而远程端仍然认为先前的连接有效。当它收到一个<em>SYN</em>段（并且序列号匹配）时，它将以<em>RST</em>应答，因为它不期望这样的段。新连接将因错误而中止：</p>
<p>&nbsp;</p>
<figure id="attachment_22268" aria-describedby="caption-attachment-22268" style="width: 559px" class="wp-caption aligncenter"><img decoding="async" loading="lazy" class="wp-image-22268" src="https://coolshell.cn/wp-content/uploads/2022/07/last-ack.png" alt="" width="559" height="375" srcset="https://coolshell.cn/wp-content/uploads/2022/07/last-ack.png 783w, https://coolshell.cn/wp-content/uploads/2022/07/last-ack-300x201.png 300w, https://coolshell.cn/wp-content/uploads/2022/07/last-ack-768x515.png 768w, https://coolshell.cn/wp-content/uploads/2022/07/last-ack-403x270.png 403w" sizes="(max-width: 559px) 100vw, 559px" /><figcaption id="caption-attachment-22268" class="wp-caption-text">如果远端因为最后一个 ACK​​ 丢失而停留在 LAST-ACK 状态，则打开具有相同四元组的新连接将不起作用 （<a href="https://vincent.bernat.ch/en/blog/2014-tcp-time-wait-state-linux" target="_blank" rel="noopener">来源</a>）</figcaption></figure>
<p><code>TIME_WAIT</code> 的这个超时时间的值如下所示：</p>
<ul>
<li>在 macOS 上是15秒， <code>sysctl net.inet.tcp | grep net.inet.tcp.msl</code></li>
<li>在 Linux 上是 60秒 <code>cat /proc/sys/net/ipv4/tcp_fin_timeout</code></li>
</ul>
<h4>解决方案</h4>
<p>要解决这个问题，网上一般会有下面这些解法</p>
<ul>
<li>把这个超时间调小一些，这样就可以把TCP 的端口号回收的快一些。但是也不能太小，如果流量很大的话，TIME_WAIT一样会被耗尽。</li>
<li>设置上 <code>tcp_tw_reuse</code> 。<a title="RFC 1323：高性能 TCP 扩展" href="https://tools.ietf.org/html/rfc1323">RFC 1323</a>提出了一组 TCP 扩展来提高高带宽路径的性能。除其他外，它定义了一个新的 TCP 选项，带有两个四字节<strong>时间戳字段</strong>。第一个是发送选项的 TCP 时间戳的当前值，而第二个是从远程主机接收到的最新时间戳。如果新时间戳严格大于为前一个连接记录的最新时间戳。Linux 将重用该状态下的现有 <code>TIME_WAIT</code> 连接用于<strong>出站的链接</strong>。也就是说，<strong>这个参数对于入站连接是没有任何用图的。</strong></li>
<li>设置上 <code>tcp_tw_recycle</code> 。 这个参数同样依赖于时间戳选项，但会影响进站和出站链接。这个参数会影响NAT环境，也就是一个公司里的所有员工用一个IP地址访问外网的情况。在这种情况下，时间戳条件将禁止在这个公网IP后面的所有设备在一分钟内连接，因为它们不共享相同的时间戳时钟。毫无疑问，禁用此选项要好得多，因为它会导致 <strong>难以检测</strong>和<strong>诊断</strong>问题。（注：从 Linux 4.10 (commit <a title="tcp：为每个连接随机化 tcp 时间戳偏移" href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=95a22caee396cef0bb2ca8fafdd82966a49367bb">95a22caee396</a> ) 开始，Linux 将为每个连接随机化时间戳偏移量，从而使该选项完全失效，无论有无<abbr title="网络地址解读">NAT</abbr>。它已从 Linux 4.12中完全删除）</li>
</ul>
<p>对于服务器来说，上述的三个访问都不能解决服务器的 <code>TIME_WAIT</code> 过多的问题，真正解决问题的就是——<strong>不作死就不会死，也就是说，服务器不要主动断链接，而设置上KeepAlive后，让客户端主动断链接，这样服务端只会有<code>CLOSE_WAIT</code></strong>。</p>
<p>但是对于用于建立出站连接的探活的 EaseProbe来说，设置上 <code>tcp_tw_reuse</code> 就可以重用 <code>TIME_WAIT</code> 了，但是这依然无法解决 <code>TIME_WAIT</code> 过多的问题。</p>
<p>然后，过了几天后，我忽然想起来以前在《UNIX 网络编程》上有看到过一个Socket的参数，叫 <code data-enlighter-language="raw" class="EnlighterJSRAW">&lt;code&gt;SO_LINGER</code></code>，我的编程生涯中从来没有使用过这个设置，这个参数主要是为了延尽关闭来用的，也就是说你应用调用 <code>close()</code>函数时，如果还有数据没有发送完成，则需要等一个延时时间来让数据发完，但是，如果你把延时设置为 0  时，Socket就丢弃数据，并向对方发送一个 <code>RST</code> 来终止连接，因为走的是 RST 包，所以就不会有 <code>TIME_WAIT</code> 了。</p>
<p>这个东西在服务器端永远不要设置，不然，你的客户端就总是看到 TCP 链接错误 “connnection reset by peer”，但是这个参数对于 EaseProbe 的客户来说，简直是太完美了，当EaseProbe 探测完后，直接 reset connection， 即不会有功能上的问题，也不会影响服务器，更不会有烦人的 <code> TIME_WAIT</code> 问题。</p>
<h4>Go 实际操作</h4>
<p>在 Golang的标准库代码里，<code>net.TCPConn</code> 有个方法 <code>SetLinger()</code>可以完成这个事，使用起来也比较简单：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">conn, _ := net.DialTimeout("tcp", t.Host, t.Timeout())

if tcpCon, ok := conn.(*net.TCPConn); ok {
    tcpCon.SetLinger(0)
}</pre>
<p>你需要把一个 <code>net.Conn</code>  转型成 <code>net.TCPConn</code>，然后就可以调用方法了。</p>
<p>但是对于Golang 的标准库中的 HTTP 对象来说，就有点麻烦了，Golang的 http 库把底层的这边连接对象全都包装成私有变量了，你在外面根本获取不到。这篇《<a href="https://iximiuz.com/en/posts/go-net-http-setsockopt-example/" target="_blank" rel="noopener">How to Set Go net/http Socket Options &#8211; setsockopt() example</a> 》中给出了下面的方法：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">dialer := &amp;net.Dialer{
    Control: func(network, address string, conn syscall.RawConn) error {
        var operr error
        if err := conn.Control(func(fd uintptr) {
            operr = syscall.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.TCP_QUICKACK, 1)
        }); err != nil {
            return err
        }
        return operr
    },
}

client := &amp;http.Client{
    Transport: &amp;http.Transport{
        DialContext: dialer.DialContext,
    },
}</pre>
<p>上面这个方法非常的低层，需要直接使用setsocketopt这样的系统调用，我其实，还是想使用 <code>TCPConn.SetLinger(0)</code> 来完成这个事，即然都被封装好了，最好还是别破坏封闭性碰底层的东西。</p>
<p>经过Golang http包的源码阅读和摸索，我使用了下面的方法：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">client := &amp;http.Client{
    Timeout: h.Timeout(),
    Transport: &amp;http.Transport{
      TLSClientConfig:   tls,
      DisableKeepAlives: true,
      DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
        d := net.Dialer{Timeout: h.Timeout()}
        conn, err := d.DialContext(ctx, network, addr)
        if err != nil {
          return nil, err
        }
        tcpConn, ok := conn.(*net.TCPConn)
        if ok {
          tcpConn.SetLinger(0)
          return tcpConn, nil
        }
        return conn, nil
      },
    },
  }</pre>
<p>然后，我找来了全球 T0p 100W的域名，然后在AWS上开了一台服务器，用脚本生成了 TOP 10K 和 20K 的网站来以5s, 10s, 30s, 60s的间隔进行探活，搞到Cloudflare 的 1.1.1.1 DNS 时不时就把我拉黑，最后的测试结果也非常不错，根本 没有 TIME_WAIT 的链接，相关的测试方法、测试数据和测试报告可以参看：<a href="https://github.com/megaease/easeprobe/blob/main/docs/Benchmark.md" target="_blank" rel="noopener">Benchmark Report</a></p>
<h4>总结</h4>
<p>下面是几点总结</p>
<ul>
<li><code>TIME_WAIT</code> 是一个TCP 协议完整性的手段，虽然会有一定的副作用，但是这个设计是非常关键的，最好不要妥协掉。</li>
<li>永远不要使用  <code>tcp_tw_recycle</code> ，这个参数是个巨龙，破坏力极大。</li>
<li>服务器端永远不要使用  <code>SO_LINGER(0)</code>，而且使用 <code>tcp_tw_reuse</code> 对服务端意义不大，因为它只对出站流量有用。</li>
<li>在服务端上最好不要主动断链接，设置好KeepAlive，重用链接，让客户端主动断链接。</li>
<li>在客户端上可以使用 <code>tcp_tw_reuse</code>  和 <code>SO_LINGER(0)</code>。</li>
</ul>
<p>最后强烈推荐阅读这篇文章 &#8211; <a href="https://vincent.bernat.ch/en/blog/2014-tcp-time-wait-state-linux" target="_blank" rel="noopener">Coping with the TCP TIME-WAIT state on busy Linux servers</a></p>
<p>（全文完）<!--



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





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

 

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

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/11564.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2014/05/tin-can-phone-150x150.jpg" alt="TCP 的那些事儿（上）" width="150" height="150" /></a><a href="https://coolshell.cn/articles/11564.html" class="wp_rp_title">TCP 的那些事儿（上）</a></li><li ><a href="https://coolshell.cn/articles/19840.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2019/10/HTTP-770x513-300x200-1-150x150.jpg" alt="HTTP的前世今生" width="150" height="150" /></a><a href="https://coolshell.cn/articles/19840.html" class="wp_rp_title">HTTP的前世今生</a></li><li ><a href="https://coolshell.cn/articles/11609.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2014/05/xin_2001040422167711230318-150x150.jpg" alt="TCP 的那些事儿（下）" width="150" height="150" /></a><a href="https://coolshell.cn/articles/11609.html" class="wp_rp_title">TCP 的那些事儿（下）</a></li><li ><a href="https://coolshell.cn/articles/9859.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2013/06/Alan-Cox-150x150.jpg" alt="Alan Cox：单向链表中prev指针的妙用" width="150" height="150" /></a><a href="https://coolshell.cn/articles/9859.html" class="wp_rp_title">Alan Cox：单向链表中prev指针的妙用</a></li><li ><a href="https://coolshell.cn/articles/7490.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2012/06/f1-150x150.jpg" alt="性能调优攻略" width="150" height="150" /></a><a href="https://coolshell.cn/articles/7490.html" class="wp_rp_title">性能调优攻略</a></li><li ><a href="https://coolshell.cn/articles/1484.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2009/09/tcp1-150x150.jpg" alt="TCP网络关闭的状态变换时序图" width="150" height="150" /></a><a href="https://coolshell.cn/articles/1484.html" class="wp_rp_title">TCP网络关闭的状态变换时序图</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/22263.html">从一次经历谈 TIME_WAIT 的那些事</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/22263.html/feed</wfw:commentRss>
			<slash:comments>31</slash:comments>
		
		
			</item>
		<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" ><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>Rust语言的编程范式</title>
		<link>https://coolshell.cn/articles/20845.html</link>
					<comments>https://coolshell.cn/articles/20845.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Sat, 04 Apr 2020 06:48:23 +0000</pubDate>
				<category><![CDATA[Rust 语言]]></category>
		<category><![CDATA[编程语言]]></category>
		<category><![CDATA[C++]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[Rust]]></category>
		<guid isPermaLink="false">https://coolshell.cn/?p=20845</guid>

					<description><![CDATA[<p>总是有很多很多人来问我对Rust语言怎么看的问题，在各种地方被at，其实，我不是很想表达我的想法。因为在不同的角度，你会看到不同的东西。编程语言这个东西，老实说...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/20845.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/20845.html">Rust语言的编程范式</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 wp-image-20925" src="https://coolshell.cn/wp-content/uploads/2020/03/rust-social-wide-360x200.jpg" alt="" width="360" height="200" />总是有很多很多人来问我对Rust语言怎么看的问题，在各种地方被at，其实，我不是很想表达我的想法。因为在不同的角度，你会看到不同的东西。编程语言这个东西，老实说很难评价，在学术上来说，Lisp就是很好的语言，然而在工程使用的时候，你会发现Lisp没什么人用，而Javascript或是PHP这样在学术很糟糕设计的语言反而成了主流，你觉得C++很反人类，在我看来，C++有很多不错的设计，而且对于了解编程语言和编译器的和原理非常有帮助。<strong>但是C++也很危险，所以，出现在像Java或Go 语言来改善它，Rust本质上也是在改善C++的。他们各自都有各自的长处和优势</strong>。</p>
<p>因为各个语言都有好有不好，因此，我不想用别的语言来说Rust的问题，或是把Rust吹成朵花以打压别的语言，写成这样的文章，是很没有营养的事。<strong>本文主要想通过Rust的语言设计来看看编程中的一些挑战，尤其是Rust重要的一些编程范式，这样反而更有意义一些，因为这样你才可能一通百通</strong>。</p>
<p>这篇文章的篇幅比较长，而且有很多代码，信息量可能会非常大，所以，<strong>在读本文前，你需要有如下的知识准备</strong>：</p>
<ul>
<li>你对C++语言的一些特性和问题比较熟悉。尤其是：指针、引用、右值move、内存对象管理、泛型编程、智能指针……</li>
<li>当然，你还要略懂Rust，不懂也没太大关系，但本文不会是Rust的教程文章，可以参看“<a href="https://doc.rust-lang.org/book/title-page.html" target="_blank" rel="noopener noreferrer">Rust的官方教程</a>”（<a href="https://kaisery.github.io/trpl-zh-cn/" target="_blank" rel="noopener noreferrer">中文版</a>）</li>
</ul>
<p><strong>因为本文太长，所以，我有必要写上 TL;DR ——</strong></p>
<p><span id="more-20845"></span></p>
<p>Java 与 Rust 在改善C/C++上走了完全不同的两条路，他们主要改善的问题就是C/C++ Safety的问题。所谓C/C++编程安全上的问题，主要是：内存的管理、数据在共享中出现的“野指针”、“野引用”的问题。</p>
<ul>
<li>对于这些问题，Java用引用垃圾回收再加上强大的VM字节码技术可以进行各种像反射、字节码修改的黑魔法。</li>
<li>而Rust不玩垃圾回收，也不玩VM，所以，作为静态语言的它，只能在编译器上下工夫。如果要让编译器能够在编译时检查出一些安全问题，那么就需要程序员在编程上与Rust语言有一些约定了，其中最大的一个约定规则就是变量的所有权问题，并且还要在代码上“去糖”，比如让程序员说明一些共享引用的生命周期。</li>
<li>Rust的这些所有权的约定造成了很大的编程上的麻烦，写Rust的程序时，基本上来说，你的程序再也不要想可能轻轻松松能编译通过了。而且，在面对一些场景的代码编写时，如：函数式的闭包，多线程的不变数据的共享，多态……开始变得有些复杂，并会让你有种找不到北的感觉。</li>
<li>Rust的Trait很像Java的接口，通过Trait可以实现C++的拷贝构造、重载操作符、多态等操作……</li>
<li>学习Rust的学习曲线并不平，用Rust写程序，基本上来说，一旦编译通过，代码运行起来是安全的，bug也是很少的。</li>
</ul>
<p><strong>如果你对Rust的概念认识的不完整，你完全写不出程序，那怕就是很简单的一段代码</strong>。<strong>这逼着程序员必需了解所有的概念才能编码。但是，另一方面也表明了这门语言并不适合初学者……</strong></p>
<h4>变量的可变性</h4>
<p>首先，Rust里的变量声明默认是“不可变的”，如果你声明一个变量 <code>let x = 5;</code>  变量 <code>x</code> 是不可变的，也就是说，<code>x = y + 10;</code> 编译器会报错的。如果你要变量的话，你需要使用 <code>mut</code> 关键词，也就是要声明成 <code>let mut x = 5;</code> 表示这是一个可以改变的变量。这个是比较有趣的，因为其它主流语言在声明变量时默认是可变的，而Rust则是要反过来。这可以理解，不可变的通常来说会有更好的稳定性，而可变的会代来不稳定性。所以，Rust应该是想成为更为安全的语言，所以，默认是 immutable 的变量。当然，Rust同样有 <code>const</code> 修饰的常量。于是，Rust可以玩出这么些东西来：</p>
<ul>
<li>常量：<code>const LEN:u32 = 1024;</code> 其中的 <code>LEN</code> 就是一个<code>u32</code> 的整型常量（无符号32位整型），是编译时用到的。</li>
<li>可变的变量： <code>let mut x = 5;</code> 这个就跟其它语言的类似， 在运行时用到。</li>
<li>不可变的变量：<code>let x= 5;</code> 对这种变量，你无论修改它，但是，你可以使用 <code>let x = x + 10;</code> 这样的方式来重新定义一个新的 <code>x</code>。这个在Rust里叫 Shadowing ，第二个 <code>x</code>  把第一个 <code>x</code> 给遮蔽了。</li>
</ul>
<p>不可变的变量对于程序的稳定运行是有帮助的，这是一种编程“契约”，当处理契约为不可变的变量时，程序就可以稳定很多，尤其是多线程的环境下，因为不可变意味着只读不写，其他好处是，与易变对象相比，它们更易于理解和推理，并提供更高的安全性。有了这样的“契约”后，编译器也很容易在编译时查错了。这就是Rust语言的编译器的编译期可以帮你检查很多编程上的问题。</p>
<p>对于标识不可变的变量，在 C/C++中我们用<code>const</code> ，在Java中使用 <code>final</code> ，在 C#中使用 <code>readonly</code> ，Scala用 <code>val</code> ……（在Javascript 和Python这样的动态语言中，原始类型基本都是不可变的，而自定义类型是可变的）。</p>
<p>对于Rust的Shadowing，我个人觉得是比较危险的，在我的职业生涯中，这种使用同名变量（在嵌套的scope环境下）带来的bug还是很不好找的。一般来说，每个变量都应该有他最合适的名字，最好不要重名。</p>
<h4>变量的所有权</h4>
<p>这个是Rust这个语言中比较强调的一个概念。其实，在我们的编程中，很多情况下，都是把一个对象（变量）传递过来传递过去，在传递的过程中，传的是一份复本，还是这个对象本身，也就是所谓的“传值还是传引用”的被程序员问得最多的问题。</p>
<ul>
<li><strong>传递副本（传值）</strong>。把一个对象的复本传到一个函数中，或是放到一个数据结构容器中，可能需要出现复制的操作，这个复制对于一个对象来说，需要深度复制才安全，否则就会出现各种问题。而深度复制就会导致性能问题。</li>
<li><strong>传递对象本身（传引用）</strong>。传引用也就是不需要考虑对象的复制成本，但是需要考虑对象在传递后，会多个变量所引用的问题。比如：我们把一个对象的引用传给一个List或其它的一个函数，这意味着，大家对同一个对象都有控制权，如果有一个人释放了这个对象，那边其它人就遭殃了，所以，一般会采用引用计数的方式来共享一个对象。引用除了共享的问题外，还有作用域的问题，比如：你从一个函数的栈内存中返回一个对象的引用给调用者，调用者就会收到一个被释放了个引用对象（因为函数结束后栈被清了）。</li>
</ul>
<p>这些东西在任何一个编程语言中都是必需要解决的问题，要足够灵活到让程序员可以根据自己的需要来写程序。</p>
<p>在C++中，如果你要传递一个对象，有这么几种方式：</p>
<ul>
<li><strong>引用或指针。</strong>也就是不建复本，完全共享，于是，但是会出现悬挂指针（<a href="https://en.wikipedia.org/wiki/Dangling_pointer" target="_blank" rel="noopener noreferrer">Dangling Pointer</a>）又叫野指针的问题，也就是一个指针或引用指向一块废弃的内存。为了解决这个问题，C++的解决方案是使用 <code>share_ptr</code> 这样的托管类来管理共享时的引用计数。</li>
<li><strong>传递复本</strong>，传递一个拷贝，需要重载对象的“拷贝构造函数”和“赋值构造函数”。</li>
<li><strong>移动Move</strong>。C++中，为了解决一些临时对象的构造的开销，可以使用Move操作，把一个对象的所有权移动到给另外一个对象，这个解决了C++中在传递对象时的会产生很多临时对象来影响性能的情况。</li>
</ul>
<p>C++的这些个“神操作”，可以让你非常灵活地在各种情况下传递对象，但是也提升整体语言的复杂度。而Java直接把C/C++的指针给废了，用了更为安全的引用 ，然后为了解决多个引用共享同一个内存，内置了引用计数和垃圾回收，于是整个复杂度大大降低。对于Java要传对象的复本的话，需要定义一个通过自己构造自己的构造函数，或是通过prototype设计模式的 <code>clone()</code> 方法来进行，如果你要让Java解除引用，需要明显的把引用变量赋成 <code>null</code> 。总之，无论什么语言都需要这对象的传递这个事做好，不然，无法提供相对比较灵活编程方法。</p>
<p>在Rust中，Rust强化了“所有权”的概念，下面是Rust的所有者的三大铁律：</p>
<ol>
<li>Rust 中的每一个值都有一个被称为其 <strong>所有者</strong>（owner）的变量。</li>
<li>值有且只有一个所有者。</li>
<li>当所有者（变量）离开作用域，这个值将被丢弃。</li>
</ol>
<p>这意味着什么？</p>
<p>如果你需要传递一个对象的复本，你需要给这个对象实现 <code>Copy</code> trait ，<strong>trait </strong>怎么翻译我也不知道，你可以认为是一个对象的一些特别的接口（可以用于一些对像操作上的约定，比如：<code>Copy</code> 用于复制（类型于C++的拷贝构造和赋值操作符重载），<code>Display</code> 用于输出（类似于Java的 <code>toString()</code>），还有 <code>Drop</code> 和操作符重载等等，当然，也可以是对象的方法，或是用于多态的接口定义，后面会讲）。</p>
<p>对于内建的整型、布尔型、浮点型、字符型、多元组都被实现了 <code>Copy</code> 所以，在进行传递的时候，会进行<code>memcpy</code> 这样的复制（bit-wise式的浅拷贝）。而对于对象来说，则不行，在Rust的编程范式中，需要使用的是 <code>Clone</code> trait。</p>
<p>于是，<code>Copy</code> 和 <code>Clone</code> 这两个相似而又不一样的概念就出来了，<code>Copy</code> 主要是给内建类型，或是由内建类型全是支持 <code>Copy</code> 的对象，而 <code>Clone</code> 则是给程序员自己复制对象的。嗯，这就是浅拷贝和深拷贝的差别，<code>Copy</code> 告诉编译器，我这个对象可以进行 bit-wise的复制，而 <code>Clone</code> 则是指深度拷贝。</p>
<p>像 <code>String</code> 这样的内部需要在堆上分布内存的数据结构，是没有实现<code>Copy</code> 的（因为内部是一个指针，所以，语义上是深拷贝，浅拷贝会招至各种bug和crash），需要复制的话，必需手动的调用其 <code>clone()</code> 方法，如果不这样的的话，当在进行函数参数传递，或是变量传递的时候，所有权一下就转移了，而之前的变量什么也不是了（这里编译器会帮你做检查有没有使用到所有权被转走的变量）。这个相当于C++的Move语义。</p>
<p>参看下面的示例，你可能对Rust自动转移所有权会有更好的了解（代码中有注释了，我就不多说了）。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="rust">// takes_ownership 取得调用函数传入参数的所有权，因为不返回，所以变量进来了就出不去了
fn takes_ownership(some_string: String) {
    println!("{}", some_string);
} // 这里，some_string 移出作用域并调用 <code data-enlighter-language="raw" class="EnlighterJSRAW">drop</code> 方法。占用的内存被释放

// gives_ownership 将返回值移动给调用它的函数
fn gives_ownership() -&gt; String {
    let some_string = String::from("hello"); // some_string 进入作用域.
    some_string // 返回 some_string 并移出给调用的函数
}

// takes_and_gives_back 将传入字符串并返回该值
fn takes_and_gives_back(mut a_string: String) -&gt; String {
    a_string.push_str(", world");
    a_string  // 返回 a_string 将所有权移出给调用的函数
}

fn main()
{
    // gives_ownership 将返回值移给 s1
    let s1 = gives_ownership();
    // 所有权转给了 takes_ownership 函数, s1 不可用了
    takes_ownership(s1);
    // 如果编译下面的代码，会出现s1不可用的错误
    // println!("s1= {}", s1);
    //                    ^^ value borrowed here after move
    let s2 = String::from("hello");// 声明s2
    // s2 被移动到 takes_and_gives_back 中, 它也将返回值移给 s3。
    // 而 s2 则不可用了。
    let s3 = takes_and_gives_back(s2);
    //如果编译下面的代码，会出现可不可用的错误
    //println!("s2={}, s3={}", s2, s3);
    //                         ^^ value borrowed here after move
    println!("s3={}", s3);
}
</pre>
<p>这样的 Move 的方式，在性能上和安全性上都是非常有效的，而Rust的编译器会帮你检查出使用了所有权被move走的变量的错误。而且，我们还可以从函数栈上返回对象了，如下所示：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="rust">fn new_person() -&gt; Person {
    let person = Person {
        name : String::from("Hao Chen"),
        age : 44,
        sex : Sex::Male,
        email: String::from("haoel@hotmail.com"),
    };
    return person;
}

fn main() {
   let p  = new_person();
}
</pre>
<p>因为对象是Move走的，所以，在函数上 <code>new_person()</code> 上返回的 <code>Person</code> 对象是Move 语言，被Move到了 <code>main()</code> 函数中来，这样就没有性能上的问题了。而在C++中，我们需要把对象的Move函数给写出来才能做到。因为，C++默认是调用拷贝构造函数的，而不是Move的。</p>
<h4>Owner语义带来的复杂度</h4>
<p>Owner + Move 的语义也会带来一些复杂度。首先，如果有一个结构体，我们把其中的成员 Move 掉了，会怎么样。参看如下的代码：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="rust">#[derive(Debug)] // 让结构体可以使用 <code data-enlighter-language="raw" class="EnlighterJSRAW">{:?}</code>的方式输出
struct Person {
    name :String,
    email:String,
}

let _name = p.name; // 把结构体 Person::name Move掉
println!("{} {}", _name, p.email); //其它成员可以正常访问
println!("{:?}", p); //编译出错 "value borrowed here after partial move"
p.name = "Hao Chen".to_string(); // Person::name又有了。
println!("{:?}", p); //可以正常的编译了
</pre>
<p>上面这个示例，我们可以看到，结构体中的成员是可以被Move掉的，Move掉的结构实例会成为一个部分的未初始化的结构，如果需要访问整个结构体的成员，会出现编译问题。但是后面把 Person::name补上后，又可以愉快地工作了。</p>
<p>下面我们再看一个更复杂的示例——这个示例模拟动画渲染的场景，我们需要有两个buffer，一个是正在显示的，另一个是下一帧要显示的。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="rust">struct Buffer {
    buffer : String,
}

struct Render {
    current_buffer : Buffer,
    next_buffer : Buffer,
}
//实现结构体 <code data-enlighter-language="raw" class="EnlighterJSRAW">Render</code> 的方法
impl Render { 
    //实现 update_buffer() 方法，
    //更新buffer，把 next 更新到 current 中，再更新 next
    fn update_buffer(&amp; mut self, buf : String) {
        self.current_buffer = self.next_buffer;
        self.next_buffer = Buffer{ buffer: buf};
    }
}
</pre>
<p>上面这段代码，我们写下来没什么问题，但是 Rust 编译不会让我们编译通过。它会告诉我们如下的错误：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="no-highlight">error[E0507]: cannot move out of <code data-enlighter-language="raw" class="EnlighterJSRAW">self.next_buffer</code> which is behind a mutable reference
--&gt; /.........../xxx.rs:18:31
|
14 | self.current_buffer = self.next_buffer;
|                          ^^^^^^^^^^^^^^^^ move occurs because <code data-enlighter-language="raw" class="EnlighterJSRAW">self.next_buffer</code> has type <code data-enlighter-language="raw" class="EnlighterJSRAW">Buffer</code>,
                                            which does not implement the <code data-enlighter-language="raw" class="EnlighterJSRAW">Copy</code> trait</pre>
<p>编译器会提示你，<code>Buffer</code> 没有 Copy trait 方法。<strong>但是，如果你实现了 Copy 方法后，你又不能享受 Move 带来的性能上快乐了。于是，到这里，你开始进退两难了，完全不知道取舍了</strong>。</p>
<ul>
<li>Rust编译器不让我们在成员方法中把成员Move走，因为 <code>self</code> 引用就不完整了。</li>
<li>Rust要我们实现 <code>Copy</code> Trait，但是我们不想要拷贝，因为我们就是想把 <code>next_buffer</code> move 到 <code>current_buffer</code> 中</li>
</ul>
<p>我们想要同时 Move 两个变量，参数 <code>buf</code> move 到 <code>next_buffer</code> 的同时，还要把 <code>next_buffer</code> 里的东西 move 到 <code>current_buffer</code> 中。 我们需要一个“杂耍”的技能。<br />
<img decoding="async" loading="lazy" class="aligncenter size-full wp-image-20872" src="https://coolshell.cn/wp-content/uploads/2020/03/indy.gif" alt="" width="500" height="264" /></p>
<p>这个需要动用 <code>std::mem::replace(&amp;dest, src)</code> 函数了， 这个函数技把 <code>src</code> 的值 move 到 <code>dest</code> 中，然后把 <code>dest</code> 再返回出来（这其中使用了 unsafe 的一些底层骚操作才能完成）。Anyway，最终是这样实现的：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="rust">use std::mem::replace
fn update_buffer(&amp; mut self, buf : String) { 
  self.current_buffer = replace(&amp;mut self.next_buffer, Buffer{buffer : buf}); 
}</pre>
<p>不知道你觉得这样“杂耍”的代码看上去怎么以样？我觉得可读性下降一个数量级。</p>
<h4>引用（借用）和生命周期</h4>
<p>下面，我们来讲讲引用，因为把对象的所有权 Move 走了的情况，在一些时候肯定不合适，比如，我有一个 <code>compare(s1: Student, s2: Student) -&gt; bool</code> 我想比较两个学生的平均份成绩， 我不想传复本，因为太慢，我也不想把所有权交进去，因为只是想计算其中的数据。这个时候，传引用就是一个比较好的选择，Rust同样支持传引用。只需要把上面的函数声明改成：<code>compare(s1 :&amp;Student, s2 : &amp;Student) -&gt; bool</code> 就可以了，在调用的时候，<code>compare (&amp;s1, &amp;s2);</code>  与C++一致。在Rust中，这也叫“借用”（嗯，Rust发明出来的这些新术语，在语义上感觉让人更容易理解了，当然，也增加了学习的复杂度了）</p>
<h5>引用（借用）</h5>
<p>另外，如果你要修改这个引用对象，就需要使用“可变引用”，如：<code>foo( s : &amp;mut Student)</code> 以及 <code>foo( &amp;mut s);</code>另外，为了避免一些数据竞争需要进行数据同步的事，Rust严格规定了——<strong>在任意时刻，要么只能有一个可变引用，要么只能有多个不可变引用</strong>。</p>
<p>这些严格的规定会导致程序员失去编程的灵活性，不熟悉Rust的程序员可能会在一些编译错误下会很崩溃，但是你的代码的稳定性也会提高，bug率也会降低。</p>
<p>另外，Rust为了解决“野引用”的问题，也就是说，有多个变量引用到一个对象上，还不能使用额外的引用计数来增加程序运行的复杂度。那么，Rust就要管理程序中引用的生命周期了，而且还是要在编译期管理，如果发现有引用的生命周期有问题的，就要报错。比如：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="rust">let r;
{
    let x = 10;
    r = &amp;x;
}
println!("r = {}",r );
</pre>
<p>上面的这段代码，程序员肉眼就能看到 <code>x</code> 的作用域比 <code>r</code>  小，所以导致 <code>r</code> 在 <code>println()</code> 的时候 <code>r</code> 引用的 <code>x</code> 已经没有了。这个代码在C++中可以正常编译而且可以执行，虽然最后可以打出“内嵌作用域”的 <code>x</code> 的值，但其实这个值已经是有问题的了。而在 Rust 语言中，编译器会给出一个编译错误，告诉你，“<code>x</code> dropped here while still borrowed”，这个真是太棒了。</p>
<p>但是这中编译时检查的技术对于目前的编译器来说，只在程序变得稍微复杂一点，编译器的“失效引用”检查就不那么容易了。比如下面这个代码：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="rust">fn order_string(s1 : &amp;str, s2 : &amp;str) -&gt; (&amp;str, &amp;str) {
    if s1.len() &lt; s2.len() {
        return (s1, s2);
    }
    return (s2, s1);
}

let str1 = String::from("long long long long string");
let str2 = "short string";

let (long_str, short_str) = order_string(str1.as_str(), str2);

println!(" long={} nshort={} ", long_str, short_str);
</pre>
<p>我们有两个字符串，<code>str1</code> 和 <code>str2</code> 我们想通过函数 <code>order_string()</code> 把这两个字串符返回成 <code>long_str</code> 和 <code>short_str</code>  这样方便后面的代码进行处理。这是一段很常见的处理代码的示例。然而，你会发现，这段代码编译不过。编译器会告诉你，<code>order_string()</code> 返回的 引用类型 <code>&amp;str</code> 需要一个 lifetime的参数 &#8211; “ expected lifetime parameter”。这是因为Rust编译无法通过观察静态代码分析返回的两个引用返回值，到底是<code>(s1, s2)</code> 还是 <code>(s2, s1)</code> ，因为这是运行时决定的。所以，返回值的两个参数的引用没法确定其生命周期到底是跟 <code>s1</code> 还是跟 <code>s2</code>，这个时候，编译器就不知道了。</p>
<h5>生命周期</h5>
<p>如果你的代码是下面这个样子，编程器可以自己推导出来，函数 <code>foo()</code> 的参数和返回值都是一个引用，他们的生命周期是一样的，所以，也就可以编译通过。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="rust">fn foo (s: &amp;mut String) -&gt; &amp;String {
    s.push_str("coolshell");
    s
}

let mut s = "hello, ".to_string();
println!("{}", foo(&amp;mut s))
</pre>
<p>而对于传入多个引用，返回值可能是任一引用，这个时候编译器就犯糊涂了，因为不知道运行时的事，所以，就需要程序员来标注了。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="rust">fn long_string&lt;'c&gt;(s1 : &amp;'c str, s2 : &amp;'c str) -&gt; (&amp;'c str, &amp;'c str) {
    if s1.len() &gt; s2.len() {
        return (s1, s2);
    }
    return (s2, s1);
}
</pre>
<p>上述的Rust的标注语法，用个单引号加一个任意字符串来标注（<code>'static</code>除外，这是一个关键词，表示生命周期跟整个程序一样长），然后，说明返回的那两个引用的生命周期跟 <code>s1</code> 和 <code>s2</code> 的生命周期相同，这个标注的目的就是把运行时的事变成了编译时的事。于是程序就可以编译通过了。（注：你也不要以为你可以用这个技术乱写生命周期，这只是一种“去语法糖操作”，是帮助编译器理解其中的生命周期，如果违反实际生命周期，编译器也是会拒绝编译的）</p>
<p>这里有两个说明，</p>
<ul>
<li>只要你玩引用，生命周期标识就会来了。</li>
<li>Rust编译器不知道运行时会发生什么事，所以，需要你来标注声明</li>
</ul>
<p>我感觉，你现在开始有点头晕了吧？接下来，我们让你再晕一下。比如：如果你要在结构体中玩引用，那必需要为引用声明生命周期，如下所示：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="rust">// 引用 ref1 和 ref2 的生命周期与结构体一致
struct Test &lt;'life&gt; {
    ref_int : &amp;'life i32,
    ref_str : &amp;'life str,
}
</pre>
<p>其中，生命周期标识 <code>'life</code> 定义在结构体上，被使用于其成员引用上。意思是声明规则——“<strong>结构体的生命周期 &lt;= 成员引用的生命周期</strong>”</p>
<p>然后，如果你要给这个结构实现两个 <code>set</code> 方法，你也得带上 lifetime 标识。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="rust">imp&lt;'life&gt; Test&lt;'life&gt; {
    fn set_string(&amp;mut self, s : &amp;'life str) {
        self.ref_str = s;
    }
    fn set_int(&amp;mut self,  i : &amp;'life i32) {
        self.ref_int = i;
    }
}
</pre>
<p>在上面的这个示例中，生命周期变量 <code>'life</code> 声明在 <code>impl</code> 上，用于结构体和其方法的入参上。 意思是声明规则——“<strong>结构体方法的“引用参数”的生命周期 &gt;= 结构体的生命周期</strong>”</p>
<p>有了这些个生命周期的标识规则后，Rust就可以愉快地检查这些规则说明，并编译代码了。</p>
<h4>闭包与所有权</h4>
<p>这种所有权和引用的严格区分和管理，会影响到很多地方，下面我们来看一下函数闭包中的这些东西的传递。函数闭包又叫Closure，是函数式编程中一个不可或缺的东西，又被称为lambda表达式，基本上所有的高级语言都会支持。在 Rust 语言中，其闭包函数的表示是用两根竖线（| |）中间加传如参数进行定义。如下所示：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="rust">// 定义了一个 x + y 操作的 lambda f(x, y) = x + y;
let plus = |x: i32, y:i32| x + y; 
// 定义另一个lambda g(x) = f(x, 5)
let plus_five = |x| plus(x, 5); 
//输出
println!("plus_five(10)={}", plus_five(10) );</pre>
<h5>函数闭包</h5>
<p>但是一旦加上了上述的所有权这些东西后，问题就会变得复杂开来。参看下面的代码。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="rust">struct Person {
    name : String,
    age : u8,
}

fn main() {
    let p = Person{ name: "Hao Chen".to_string(), age : 44};
    //可以运行，因为 <code data-enlighter-language="raw" class="EnlighterJSRAW">u8</code> 有 Copy Trait
    let age = |p : Person| p.age; 
    // String 没有Copy Trait，所以，这里所有权就 Move 走了
    let name = |p : Person | p.name; 
    println! ("name={}, age={}" , name(p), age(p));
}</pre>
<p>上面的代码无法编译通过，因为Rust编译器发现在调用 <code>name(p)</code> 的时候，<code>p</code> 的所有权被移走了。然后，我们想想，改成引用的版本，如下所示：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="rust">let age = |p : &amp;Person| p.age;
let name = |p : &amp;Person | &amp;p.name;

println! ("name={}, age={}" , name(&amp;p), age(&amp;p));</pre>
<p>你会现在还是无法编译，报错中说：<strong>cannot infer an appropriate lifetime for borrow expression due to conflicting requirements</strong></p>
<pre class="EnlighterJSRAW" data-enlighter-language="null">error[E0495]: cannot infer an appropriate lifetime for borrow expression due to conflicting requirements
  --&gt; src/main.rs:11:31
   |
11 |     let name = |p : &amp;Person | &amp;p.name;
   |                               ^^^^^^^</pre>
<p>然后你开始尝试加 lifetime，用尽各种Rust的骚操作（官方Github上的<a href="https://github.com/rust-lang/rust/issues/58052" target="_blank" rel="noopener noreferrer"> #issue 58052</a>），然后，还是无法让你的程序可以编译通过。最后，上StackOverflow 里寻找帮助，得到下面的正确写法（这个可能跟这个bug有关系：<a href="https://github.com/rust-lang/rust/issues/41078" target="_blank" rel="noopener noreferrer">#issue 41078</a> ）。但是这样的写法，已经让简洁的代码变得面目全非。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="rust" data-enlighter-highlight="7">//下面的声明可以正确译
let name: for&lt;'a&gt; fn(&amp;'a Person) -&gt; &amp;'a String = |p: &amp;Person| &amp;p.name;</pre>
<p>上面的这种lifetime的标识也是很奇葩，通过定义一个函数类型来做相关的标注，但是这个函数类型，需要用到 <code>for&lt;'a&gt;</code> 关键字。你可能会很confuse这个关键字不是用来做循环的吗？嗯，Rust这种重用关键字的作法，我个人觉得带来了很多不必要的复杂度。总之，这样的声明代码，我觉得基本不会有人能想得到的——“去语法糖操作太严重了，绝大多数人绝对hold不住”！</p>
<p>最后，我们再来看另一个问题，下面的代码无法编译通过：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="rust">let s = String::from("coolshell");
let take_str = || s;
println!("{}", s); //ERROR
println!("{}",  take_str()); // OK</pre>
<p>Rust的编译器会告诉你，<code>take_str</code>  把 <code>s</code> 的所有权给拿走了（因为需要作成返回值）。所以，后面的输出语句就用不到了。这里意味着：</p>
<ul>
<li>对于内建的类型，都实现了 <code>Copy</code> 的 trait，那么闭包执行的是 “借用”</li>
<li>对于没有实现 <code>Copy</code> 的trait，在闭包中可以调用其方法，是“借用”，但是不能当成返回值，当成返回值了就是“移动”。</li>
</ul>
<p>虽然有了这些“通常情况下是借用的潜规则”，但是还是不能满足一些情况，所以，还要让程序员可以定义 <code>move</code> 的“明规则”。下面的代码，一个有 move 一个没有move，他们的差别也不一样。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="rust">//-----------借用的情况-----------
let mut num = 5;
{
    let mut add_num = |x: i32| num += x;
    add_num(5);
}
println!("num={}", num); //输出 10

//-----------Move的情况-----------
let mut num = 5;
{
    // 把 num（5）所有权给 move 到了 add_num 中，
    // 使用其成为闭包中的局部变量。
    let mut add_num = move |x: i32| num += x;
    add_num(5);
    println!("num(move)={}", num); //输出10
}
//因为i32实现了 <code data-enlighter-language="raw" class="EnlighterJSRAW">Copy</code>，所以，这里还可以访问
println!("num(move)={}", num); //输出5</pre>
<p>真是有点头大了，int这样的类型，因为实现了Copy Trait，所以，所有权被移走后，意味着，在内嵌块中的<code>num</code> 和外层的 <code>num</code> 是两个完全不相干的变量。<strong>但是你在读代码的时候，你的大脑可能并不会让你这么想，因为里面的那个num又没有被声明过，应该是外层的</strong>。我个人觉得这是Rust 各种“按下葫芦起了瓢”的现象。</p>
<h5>线程闭包</h5>
<p>通过上面的示例，我们可以看到， <code>move</code> 关键词，可以把闭包外使用到的变量给移动到闭包内，成为闭包内的一个局部变量。这种方式，在多线程的方式下可以让线程运行地更为的安全。参看如下代码：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="rust">let name = "CoolShell".to_string();
let t = thread::spawn(move || {
    println!("Hello, {}", name);
});
println!("wait {:?}", t.join());</pre>
<p>首先，线程 <code>thread::spawn()</code> 里的闭包函数是不能带参数的，因为是闭包，所以可以使用这个可见范围内的变量，但是，问题来了，因为是另一个线程，所以，这代表其和其它线程（如：主线程）开始共享数据了，所以，在Rust下，要求把使用到的变量给 Move 到线程内，这就保证了安全的问题—— <code>name</code> 在线程中永远不会失效，而且不会被别人改了。</p>
<p>你可能会有一些疑问，你会质疑到</p>
<ul>
<li>一方面，这个 <code>name</code> 变量又没有声明成 <code>mut</code> 这意味着不变，没必要使用move语义也是安全的。</li>
<li>另一方面，如果我想把这个 <code>name</code> 传递到多个线程里呢？</li>
</ul>
<p>嗯，是的，但是Rust的线程必需是 move的，不管是不是可变的，不然编译不过去。如果你想把一个变量传到多个线程中，你得创建变量的复本，也就是调用 <code>clone()</code> 方法。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="rust" data-enlighter-highlight="2,7">let name = "CoolShell".to_string();
let name1 = name.clone();
let t1 = thread::spawn(move || {
    println!("Hello, {}", name.clone());
})
let t2 = thread::spawn(move || {
    println!("Hello, {}", name1.clone());
});
println!("wait t1={:?}, t2={:?}", t1.join(), t2.join());</pre>
<p>然后，你说，这种clone的方式成本不是很高？设想，如果我要用多线程对一个很大的数组做统计，这种clone的方式完全吃不消。嗯，是的。这个时候，需要使用另一个技术，智能指针了。</p>
<h4>Rust的智能指针</h4>
<p>如果你看到这里还不晕的话，那么，我的文章还算成功（如果晕的话，请告诉我，我会进行改善）。接下来我们来讲讲Rust的智能指针和多态。</p>
<p>因为有些内存需要分配在Heap（堆）上，而不是Stack（堆）上，Stack上的内存一般是编译时决定的，所以，编译器需要知道你的数组、结构体、枚举等这些数据类型的长度，没有长度是无法编译的，而且长度也不能太大，Stack上的内存大小是有限，太大的内存会有StackOverflow的错误。所以，对于更大的内存或是动态的内存分配需要分配在Heap上。学过C/C++的同学对于这个概念不会陌生。</p>
<p>Rust 作为一个内存安全的语言，这个堆上分配的内存也是需要管理的。在C中，需要程序员自己管理，而在C++中，一般使用 <a href="https://en.wikipedia.org/wiki/Resource_acquisition_is_initialization" target="_blank" rel="noopener noreferrer">RAII 的机制</a>（面向对象的代理模式），一种通过分配在Stack上的对象来管理Heap上的内存的技术。在C++中，这种技术的实现叫“智能指针”（Smart Pointer）。</p>
<p>在C++11中，会有三种智能指针（这三种指针是什么我就不多说了）：</p>
<ul>
<li><code>unique_ptr</code>。独占内存，不共享。在Rust中是：<code>std::boxed::Box</code></li>
<li><code>shared_ptr</code>。以引用计数的方式共享内存。在Rust中是：<code>std::rc::Rc</code></li>
<li><code>weak_ptr</code>。不以引用计数的方式共享内存。在Rust中是：<code>std::rc::Weak</code></li>
</ul>
<p>对于独占的 <code>Box</code> 不多说了，这里重点说一下共享的 <code>Rc</code> 和 <code>Weak</code> ：</p>
<ul>
<li>对于Rust的 Rc 来说，Rc指针内会有一个 <code>strong_count</code> 的引用持计数，一旦引用计数为0后，内存就自动释放了。</li>
<li>需要共享内存的时候，需要调用实例的 <code>clone()</code> 方法。如： <code>let another = rc.clone()</code> 克隆的时候，只会增加引用计数，不会作深度复制（个人觉得Clone的语义在这里被践踏了）</li>
<li>有这种共享的引用计数，就意味着有多线程的问题，所以，如果需要使用线程安全的智能指针，则需要使用<code>std::sync::Arc</code></li>
<li>可以使用 <code>Rc::downgrade(&amp;rc)</code> 后，会变成 Weak 指针，Weak指针增加的是 <code>weak_count</code> 的引用计数，内存释放时不会检查它是否为 0。</li>
</ul>
<p>我们简单的来看个示例：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="rust">use std::rc::Rc;
use std::rc::Weak

//声明两个未初始化的指针变量
let weak : Weak; 
let strong : Rc;
{
    let five = Rc::new(5); //局部变量
    strong = five.clone(); //进行强引用
    weak = Rc::downgrade(&amp;five); //对局部变量进行弱引用
}
//此时，five已析构，所以 Rc::strong_count(&amp;strong)=1， Rc::weak_count(&amp;strong)=1
//如果调用 drop(strong)，那个整个内存就释放了
//drop(strong);

//如果要访问弱引用的值，需要把弱引用 upgrade 成强引用，才能安全的使用
match  weak_five.upgrade() {
    Some(r) =&gt; println!("{}", r),
    None =&gt; println!("None"),
} 
</pre>
<p>上面这个示例比较简单，其中主要展示了，指针共享的东西。因为指针是共享的，所以，对于强引用来说，最后的那个人把引用给释放了，是安全的。但是对于弱引用来说，这就是一个坑了，你们强引用的人有Ownership，但是我们弱引用没有，你们把内存释放了，我怎么知道？</p>
<p>于是，<strong>在弱引用需要使用内存的时候需要“升级”成强引用 ，但是这个升级可能会不成功，因为内存可能已经被别人清空了</strong>。所以，这个操作会返回一个 <code>Option</code> 的枚举值，<code>Option::Some(T)</code> 表示成功了，而 <code>Option::None</code> 则表示失改了。你会说，这么麻烦，我们为什么还要 <code>Weak</code> ? 这是因为强引用的 <code>Rc</code> 会有循环引用的问题……（学过C++的都应该知道）</p>
<p>另外，如果你要修改 <code>Rc</code> 里的值，Rust 会给你两个方法，一个是 <code>get_mut()</code>，一个是 <code>make_mut()</code> ，这两个方法都有副作用或是限制。</p>
<p><code>get_mut()</code> 需要做一个“唯一引用”的检查，也就是没有任何的共享才能修改</p>
<pre class="EnlighterJSRAW" data-enlighter-language="rust">//修改引用的变量 - get_mut 会返回一个Option对象
//但是需要注意，仅当（只有一个强引用 &amp;&amp; 没有弱引用）为真才能修改
if let Some(val) = Rc::get_mut(&amp;mut strong) {
    *val = 555;
}</pre>
<p><code>make_mut()</code> 则是会把当前的引用给clone出来，再也不共享了， 是一份全新的。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="rust">//此处可以修改，但是是以 clone 的方式，也就是让strong这个指针独立出来了。
*Rc::make_mut(&amp;mut strong) = 555;
</pre>
<p>如果不这样做，就会出现很多内存不安全的情况。<strong>这些小细节一定要注意，不然你的代码怎么运作的你会一脸蒙逼的</strong>。</p>
<p>嗯，如果你想更快乐地使用智能指针，这里还有个选择 &#8211; <code>Cell</code> 和 <code>RefCell</code>，它们弥补了 Rust 所有权机制在灵活性上和某些场景下的不足。他们提供了 <code>set()</code>/<code>get()</code> 以及 <code>borrow()</code>/<code>borrow_mut()</code> 的方法，让你的程序更灵活，而不会被限制得死死的。参看下面的示例。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="rust">use std::cell::Cell;
use std::cell::RefCell

let x = Cell::new(1);
let y = &amp;x; //引用（借用）
let z = &amp;x; //引用（借用）
x.set(2); // 可以进行修改，x，y，z全都改了
y.set(3);
z.set(4);
println!("x={} y={} z={}", x.get(), y.get(), z.get());

let x = RefCell::new(vec![1,2,3,4]);
{
    println!("{:?}", *x.borrow())
}

{
    let mut my_ref = x.borrow_mut();
    my_ref.push(1);
}
println!("{:?}", *x.borrow());</pre>
<p>通过上面的示例你可以看到你可以比较方便地更为正常的使用智能指针了。然而，需要注意的是 <code>Cell</code> 和 <code>RefCell</code> 不是线程安全的。在多线程下，需要使用Mutex进行互斥。</p>
<h4>线程与智能指针</h4>
<p>现在，我们回来来解决前面那还没有解决的问题，就是——我想在多个线程中共享一个只读的数据，比如：一个很大的数组，我开多个线程进行并行统计。我们肯定不能对这个大数组进行clone，但也不能把这个大数组move到一个线程中。根据上述的智能指针的逻辑，我们可以通过智指指针来完成这个事，下面是一个例程：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="rust" data-enlighter-highlight="4,5,12,13,27">const TOTAL_SIZE:usize = 100 * 1000; //数组长度
const NTHREAD:usize = 6; //线程数

let data : Vec&lt;i32&gt; = (1..(TOTAL_SIZE+1) as i32).collect(); //初始化一个数据从1到n数组
let arc_data = Arc::new(data); //data 的所有权转给了 ar_data
let result  = Arc::new(AtomicU64::new(0)); //收集结果的数组(原子操作)

let mut thread_handlers = vec![]; // 用于收集线程句柄

for i in 0..NTHREAD {
    // clone Arc 准备move到线程中，只增加引用计数，不会深拷贝内部数据
    let test_data = arc_data.clone(); 
    let res = result.clone(); 
    thread_handlers.push( 
        thread::spawn(move || {
            let id = i;
            //找到自己的分区
            let chunk_size = TOTAL_SIZE / NTHREAD + 1;
            let start = id * chunk_size;
            let end = std::cmp::min(start + chunk_size, TOTAL_SIZE);
            //进行求和运算
            let mut sum = 0;
            for  i in start..end  {
                sum += test_data[i];
            }
            //原子操作
            res.fetch_add(sum as u64, Ordering::SeqCst);
            println!("id={}, sum={}", id, sum );
        }
    ));
}
//等所有的线程执行完
for th in thread_handlers {
    th.join().expect("The sender thread panic!!!");
}
//输出结果
println!("result = {}",result.load(Ordering::SeqCst));</pre>
<p>上面的这个例程，是用多线程的方式来并行计算一个大的数组的和，每个线程都会计算自己的那一部分。上面的代码中，</p>
<ul>
<li>需要向每个线程传入一个只读的数组，我们用<code>Arc</code> 智能指针把这个数组包了一层。</li>
<li>需要向每个线程传入一个变量用于数据数据，我们用 <code>Arc&lt;AtomicU64&gt;</code> 包了一层。</li>
<li>注意：<code>Arc</code> 所包的对象是不可变的，所以，如果要可变的，那要么用原子对象，或是用Mutex/Cell对象再包一层。</li>
</ul>
<p>这一些都是为了要解决“线程的Move语义后还要共享问题”。</p>
<h4>多态和运行时识别</h4>
<h5>通过Trait多态</h5>
<p>多态是抽象和解耦的关键，所以，一个高级的语言是必需实现多态的。在C++中，多态是通过虚函数表来实现的（参看《<a href="https://coolshell.cn/articles/12165.html" target="_blank" rel="noopener noreferrer">C++的虚函数表</a>》），Rust也很类似，不过，在编程范式上，更像Java的接口的方式。其通过借用于Erlang的Trait对象的方式来完成。参看下面的代码：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="rust">struct Rectangle {
    width : u32,
    height : u32,
} 

struct Circle {
    x : u32,
    y : u32,
    radius : u32,
}

trait  IShape  { 
    fn area(&amp;self) -&gt; f32;
    fn to_string(&amp;self) -&gt; String;
}</pre>
<p>我们有两个类，一个是“长方形”，一个是“圆形”， 还有一个 <code>IShape</code> 的trait 对象（原谅我用了Java的命名方式），其中有两个方法：求面积的 <code>area()</code> 和 转字符串的 <code>to_string()</code>。下面相关的实现：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="rust">impl IShape  for Rectangle {
    fn area(&amp;self) -&gt; f32 { (self.height * self.width) as f32 }
    fn to_string(&amp;self) -&gt;String {
         format!("Rectangle -&gt; width={} height={} area={}", 
                  self.width, self.height, self.area())
    }
}

use std::f64::consts::PI;
impl IShape  for Circle  {
    fn area(&amp;self) -&gt; f32 { (self.radius * self.radius) as f32 * PI as f32}
    fn to_string(&amp;self) -&gt; String {
        format!("Circle -&gt; x={}, y={}, area={}", 
                 self.x, self.y, self.area())
    }
}
</pre>
<p>于是，我们就可以有下面的多态的使用方式了（我们使用独占的智能指针类 <code>Box</code>）：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="rust">use std::vec::Vec;

let rect = Box::new( Rectangle { width: 4, height: 6});
let circle = Box::new( Circle { x: 0, y:0, radius: 5});
let mut v : Vec&lt;Box&gt; = Vec::new();
v.push(rect);
v.push(circle);

for i in v.iter() {
   println!("area={}", i.area() );
   println!("{}", i.to_string() );
}</pre>
<h5>向下转型</h5>
<p>但是，在C++中，多态的类型是抽象类型，我们还想把其转成实际的具体类型，在C++中叫运行进实别RTTI，需要使用像 <code>type_id</code> 或是 <code>dynamic_cast</code> 这两个技术。在Rust中，转型是使用 &#8216;<code>as</code>&#8216; 关键字，然而，这是编译时识别，不是运行时。那么，在Rust中是怎么做呢？</p>
<p>嗯，这里需要使用 Rust 的 <code>std::any::Any</code> 这个东西，这个东西就可以使用 <code>downcast_ref</code> 这个东西来进行具体类型的转换。于是我们要对现有的代码进行改造。</p>
<p>首先，先得让 <code>IShape</code> 继承于 <code>Any</code> ，并增加一个 <code>as_any()</code> 的转型接口。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="rust">use std::any::Any;
trait  IShape : Any + 'static  {
    fn as_any(&amp;self) -&gt; &amp;dyn Any; 
    …… …… …… 
}</pre>
<p>然后，在具体类中实现这个接口：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="rust">impl IShape  for Rectangle {
    fn as_any(&amp;self) -&gt; &amp;dyn Any { self }
    …… …… …… 
}
impl IShape  for Circle  {
    fn as_any(&amp;self) -&gt; &amp;dyn Any { self }
    …… …… …… 
}</pre>
<p>于是，我们就可以进行运行时的向下转型了：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="rust" data-enlighter-highlight="5,7">let mut v : Vec&lt;Box&lt;dyn IShape&gt;&gt; = Vec::new();
v.push(rect);
v.push(circle);
for i in v.iter() {
    if let Some(s) = i.as_any().downcast_ref::&lt;Rectangle&gt;() {
        println!("downcast - Rectangle w={}, h={}", s.width, s.height);
    }else if let Some(s) = i.as_any().downcast_ref::&lt;Circle&gt;() {
        println!("downcast - Circle x={}, y={}, r={}", s.x, s.y, s.radius);
    }else{
        println!("invaild type");
    }
}</pre>
<h4>Trait 重载操作符</h4>
<p>操作符重载对进行泛行编程是非常有帮助的，如果所有的对象都可以进行大于，小于，等于这亲的比较操作，那么就可以直接放到一个标准的数组排序的的算法中去了。在Rust中，在 <code>std::ops</code> 下有全载的操作符重载的Trait，在<code>std::cmp</code> 下则是比较操作的操作符。我们下面来看一个示例：</p>
<p>假如我们有一个“员工”对象，我们想要按员工的薪水排序，如果我们想要使用<code>Vec::sort()</code>方法，我们就需要实现这个对象的各种“比较”方法。这些方法在 <code>std::cmp</code> 内—— 其中有四个Trait : <code>Ord</code>、<code>PartialOrd</code> 、<code>Eq</code> 和 <code>PartialEq</code>  。其中，<code>Ord</code> 依赖于 <code>PartialOrd</code> 和 <code>Eq</code> ，而<code>Eq</code> 依赖于 <code>PartialEq</code>，这意味着你需要实现所有的Trait，而<code>Eq</code> 这个Trait 是没有方法的，所以，其实现如下：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="null">use std::cmp::{Ord, PartialOrd, PartialEq, Ordering};

#[derive(Debug)]
struct Employee {
    name : String,
    salary : i32,
}
impl Ord for Employee {
    fn cmp(&amp;self, rhs: &amp;Self) -&gt; Ordering {
        self.salary.cmp(&amp;rhs.salary)
    }
}
impl PartialOrd for Employee {
    fn partial_cmp(&amp;self, rhs: &amp;Self) -&gt; Option&lt;Ordering&gt; {
        Some(self.cmp(rhs))
    }
}
impl Eq for Employee {
}
impl PartialEq for Employee {
    fn eq(&amp;self, rhs: &amp;Self) -&gt; bool {
        self.salary == rhs.salary
    }
}</pre>
<p>于是，我们就可以进行如下的操作了：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="null">let mut v = vec![
    Employee {name : String::from("Bob"),     salary: 2048},
    Employee {name : String::from("Alice"),   salary: 3208},
    Employee {name : String::from("Tom"),     salary: 2359},
    Employee {name : String::from("Jack"),    salary: 4865},
    Employee {name : String::from("Marray"),  salary: 3743},
    Employee {name : String::from("Hao"),     salary: 2964},
    Employee {name : String::from("Chen"),    salary: 4197},
];

//用for-loop找出薪水最多的人
let mut e = &amp;v[0];
for i in 0..v.len() {
    if *e &lt; v[i] { 
        e = &amp;v[i]; 
    }
}
println!("max = {:?}", e);

//使用标准的方法
println!("min = {:?}", v.iter().min().unwrap());
println!("max = {:?}", v.iter().max().unwrap());

//使用标准的排序方法
v.sort();
println!("{:?}", v);</pre>
<h4>小结</h4>
<p>现在我们来小结一下：</p>
<ul>
<li>在Rust的中，最重要的概念就是“不可变”和“所有权”以及“Trait”这三个概念。</li>
<li>在所有权概念上，Rust喜欢move所有权，如果需要借用则需要使用引用。</li>
<li>Move所有权会导致一些编程上的复杂度，尤其是需要同时move两个变量时。</li>
<li>引用（借用）的问题是生命周期的问题，一些时候需要程序员来标注生命周期。</li>
<li>在函数式的闭包和多线程下，这些所有权又出现了各种麻烦事。</li>
<li>使用智能指针可以解决所有权和借用带来的复杂度，但带来其它的问题。</li>
<li>最后介绍了Rust的Trait对象完成多态和函数重载的玩法。</li>
</ul>
<p>Rust是一个比较严格的编程语言，它会严格检查你程序中的：</p>
<ul>
<li>变量是否是可变的</li>
<li>变量的所有权是否被移走了</li>
<li>引用的生命周期是否完整</li>
<li>对象是否需要实现一些Trait</li>
</ul>
<p>这些东西都会导致失去编译的灵活性，并在一些时候需要“去糖”，导致，你在使用Rust会有诸多的不适应，程序编译不过的挫败感也是令人沮丧的。在初学Rust的时候，我想自己写一个单向链表，结果，费尽心力，才得以完成。也就是说，<strong>如果你对Rust的概念认识的不完整，你完全写不出程序，那怕就是很简单的一段代码</strong>。我觉得，这种挺好的，逼着程序员必需了解所有的概念才能编码。但是，另一方面也表明了这门语言并不适合初学者。</p>
<p>没有银弹，任何语言都有些适合的地方和场景。</p>
<p>（全文完）</p>
<p><audio style="display: none;" controls="controls"></audio></p>
<p><audio style="display: none;" controls="controls"></audio></p>
<p><audio style="display: none;" controls="controls"></audio></p>
<p><audio style="display: none;" controls="controls"></audio></p>
<p><audio style="display: none;" controls="controls"></audio></p>
<p><audio style="display: none;" controls="controls"></audio></p>
<p><audio style="display: none;" controls="controls"></audio></p>
<p><audio style="display: none;" controls="controls"></audio></p>
<p><audio style="display: none;" controls="controls"></audio></p>
<p><audio style="display: none;" controls="controls"></audio></p>
<p><audio style="display: none;" controls="controls"></audio></p>
<p><audio style="display: none;" controls="controls"></audio></p>
<p><audio style="display: none;" controls="controls"></audio></p>
<p><audio style="display: none;" controls="controls"></audio></p>
<p><audio style="display: none;" controls="controls"></audio></p>
<p><audio style="display: none;" controls="controls"></audio></p>
<p><audio style="display: none;" controls="controls"></audio></p>
<div id="gtx-trans" style="position: absolute; left: 36px; top: 17975.6px;">
<div class="gtx-trans-icon"></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" ><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/7992.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2012/08/cpp_small-150x150.jpg" alt="C++的坑真的多吗？" width="150" height="150" /></a><a href="https://coolshell.cn/articles/7992.html" class="wp_rp_title">C++的坑真的多吗？</a></li><li ><a href="https://coolshell.cn/articles/5576.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2011/10/00.QuickBasic_PDS_IDE-150x150.png" alt="那些曾伴我走过编程之路的软件" width="150" height="150" /></a><a href="https://coolshell.cn/articles/5576.html" class="wp_rp_title">那些曾伴我走过编程之路的软件</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/4905.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/3.jpg" alt="语言的数据亲和力" width="150" height="150" /></a><a href="https://coolshell.cn/articles/4905.html" class="wp_rp_title">语言的数据亲和力</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/20845.html">Rust语言的编程范式</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/20845.html/feed</wfw:commentRss>
			<slash:comments>107</slash:comments>
		
		
			</item>
		<item>
		<title>与程序员相关的CPU缓存知识</title>
		<link>https://coolshell.cn/articles/20793.html</link>
					<comments>https://coolshell.cn/articles/20793.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Sun, 01 Mar 2020 11:43:41 +0000</pubDate>
				<category><![CDATA[程序设计]]></category>
		<category><![CDATA[编程语言]]></category>
		<category><![CDATA[cache]]></category>
		<category><![CDATA[CPU]]></category>
		<category><![CDATA[性能调优]]></category>
		<guid isPermaLink="false">https://coolshell.cn/?p=20793</guid>

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

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

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

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

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

int p[32];

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

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

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

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

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



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





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

 

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

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/10249.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2013/07/image6-150x150.png" alt="7个示例科普CPU Cache" width="150" height="150" /></a><a href="https://coolshell.cn/articles/10249.html" class="wp_rp_title">7个示例科普CPU Cache</a></li><li ><a href="https://coolshell.cn/articles/17416.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2016/07/cache-150x150.png" alt="缓存更新的套路" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17416.html" class="wp_rp_title">缓存更新的套路</a></li><li ><a href="https://coolshell.cn/articles/2039.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/10.jpg" alt="CPU的性价比" width="150" height="150" /></a><a href="https://coolshell.cn/articles/2039.html" class="wp_rp_title">CPU的性价比</a></li><li ><a href="https://coolshell.cn/articles/2990.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2010/09/Time-Allocation-while-Programming-150x150.png" alt="编程时间分配图" width="150" height="150" /></a><a href="https://coolshell.cn/articles/2990.html" class="wp_rp_title">编程时间分配图</a></li><li ><a href="https://coolshell.cn/articles/294.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/6.jpg" alt="OSGi和Java企业级运算的未来方向" width="150" height="150" /></a><a href="https://coolshell.cn/articles/294.html" class="wp_rp_title">OSGi和Java企业级运算的未来方向</a></li><li ><a href="https://coolshell.cn/articles/3063.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/0.jpg" alt="40个很不错的CSS技术" width="150" height="150" /></a><a href="https://coolshell.cn/articles/3063.html" class="wp_rp_title">40个很不错的CSS技术</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/20793.html">与程序员相关的CPU缓存知识</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/20793.html/feed</wfw:commentRss>
			<slash:comments>107</slash:comments>
		
		
			</item>
		<item>
		<title>StackOverflow 2019 程序员调查</title>
		<link>https://coolshell.cn/articles/19307.html</link>
					<comments>https://coolshell.cn/articles/19307.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Sun, 21 Apr 2019 04:29:13 +0000</pubDate>
				<category><![CDATA[业界新闻]]></category>
		<category><![CDATA[技术读物]]></category>
		<category><![CDATA[杂项资源]]></category>
		<category><![CDATA[编程语言]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[程序员]]></category>
		<guid isPermaLink="false">https://coolshell.cn/?p=19307</guid>

					<description><![CDATA[<p>前些天，StackOverflow 发布了 2019年的年度程序员调查，这个调查报查有90000名程序员参与，这份调度报告平均花了20分钟，可见，这份报告有很多...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/19307.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/19307.html">StackOverflow 2019 程序员调查</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" class="alignright " src="https://coolshell.cn/wp-content/uploads/2019/04/2019-Dev-Survey-Blog-360x200.png" alt="" />前些天，StackOverflow 发布了 <a href="https://insights.stackoverflow.com/survey/2019" target="_blank" rel="noopener noreferrer">2019年的年度程序员调查</a>，这个调查报查有90000名程序员参与，这份调度报告平均花了20分钟，可见，这份报告有很多的问题，也是很详细的。这份报告有一些地方，让我有了一些思考。</p>
<p>首先，我们先来看一下之份报告的 Key Results：</p>
<ul>
<li>Python 成为了过去一年中成长最快的语言，把Java挤到了第二位，排在后面的是Rust语言。</li>
<li>有半数以上的被访者在是在16岁写下自己的第一行代码。</li>
<li><a href="https://stackoverflow.com/jobs/devops-jobs" target="_blank" rel="noopener noreferrer">DevOps Specialists</a> 和 Site Reliability Engineers 是程序员中最有经验，技术最牛，薪资最好的职位。（这对应于国内的——系统架构师）</li>
<li>在几个头部的程序员大国中，中国的程序员最乐观的，他们相信在今天出生的人会有比他们父母更好的人生。对于欧洲的程序员来说，比较法国和德国的程序员，他们对未来并不太乐观。</li>
<li>对于最影响程序员生产力的事，不同的程序员有不同的想法。</li>
</ul>
<p><span id="more-19307"></span></p>
<h4 id="toc_1">第一部分，Developer Profile</h4>
<p>在第一部分中，我们可以看到，中国程序员参与这个调查的并不多，程序员主要集中在美国、欧洲、印度这三个地方。所以，这份报告更偏国际上一些。这对于我们中国程序员也有很大的帮助，因为一方面可以看到世界发展的趋势，另一方面也可以了解我们和世界有什么不一样。</p>
<p>对于技术职业来说，整个世界的程序员开始趋于全栈和后端，有51.9%的人是全栈，50%的人是后端，32.8%的人是前端……在这些人中，很多程序员都选了多项，中位数是3项，最常见是前端、后端和全栈全选的。然后，接下来是选两项的，选两项目的包括：数据库管理员和系统管理员，DevOps Specialist 和 Site Reliablility Engineer， 学术研究者和科学家，设计师和前端工程师。<img decoding="async" loading="lazy" class="aligncenter wp-image-19308 " src="https://coolshell.cn/wp-content/uploads/2019/04/06-01.Developers.Rols_-1024x259.png" alt="" width="648" height="137" /></p>
<p>从这些数据中我们可以看见：<strong>前后端的界限越来越不明显，设计师和前端的界限也开始模糊。这应该说明，工具和框架的成熟，让后端程序员和设计师也可以进入到前端工程师的领域，或是前端工程师开始进入后端和设计的领域</strong>。总之，复合型人才越来越越成为主流，而前后端也趋于一个相互融合的态势。</p>
<p>在接下来的图表中，我们可以看到有80%以上的人是把编程当成自己的爱好（包括相关的女性）。<img decoding="async" loading="lazy" class="aligncenter " src="https://coolshell.cn/wp-content/uploads/2019/04/06-02.Coding.as_.a.Hobby_.png" alt="" width="410" height="71" /></p>
<p>真是应了那句话——“Programmers who don’t code in their spare time for fun will never become as good as those that do”，是的，如果你对编程没有感到一种快乐，没有在你空闲的时候去以一种的兴趣爱好方式去面对，那么，无论是编程，还是运动，还是去旅游，都不会有太多成效的。</p>
<p>在接下来的编程经验上，有两组如下的数据：</p>
<table>
<thead>
<tr>
<th>学习编程的年限</th>
<th>编程的年限</th>
</tr>
</thead>
<tbody>
<tr>
<td><img decoding="async" loading="lazy" class="aligncenter wp-image-19310 " src="https://coolshell.cn/wp-content/uploads/2019/04/06-03.Years_.Since_.Learning.to_.Code_.png" alt="" width="376" height="272" /></td>
<td><img decoding="async" loading="lazy" class="aligncenter wp-image-19311 " src="https://coolshell.cn/wp-content/uploads/2019/04/06-04.Years_.Coding.Professionally.png" alt="" width="372" height="242" /></td>
</tr>
</tbody>
</table>
<p>我们可以看到无论是学习还是编程，随着时间的拉长，其人数占比越来越少。</p>
<p>下面我们再来看一个年龄图：</p>
<p><img decoding="async" loading="lazy" class="aligncenter wp-image-19312 " src="https://coolshell.cn/wp-content/uploads/2019/04/06-05.Age_-1024x710.png" alt="" width="499" height="270" /></p>
<p>调查报告从20岁开始每隔5年划分一个年龄段，我们不难发现从25-29岁开始每个年龄段都比前一个年龄段人数急剧减少大约30-50%，比如25-29年龄段占总人数27.6%，而30-34则只有19.3%。以此类推，到60岁以上，就只剩1%。可以看出5年是大多数程序员的转型周期。这是合理的，因为5年时间足够一个人积累足够的经验技能为职业转型做准备。</p>
<p>我们也可以看到50岁以上的程序员只有4.2%，大约是参与调查人员的300多人，如果这些人20岁左右参加工作，那么说明他们在1990左右就开始写代码，事实上那个时间点别说是程序员了，连电脑用户都不多。<strong>电脑和互联网真正暴发的时间还是在1995年 &#8211; 2000年之间，不过，那个时间点程序员的总体人数也不多，而行业越来越火才会导致大量的人进入到这个行业中，这个转换过程基本上去需要3-5年，也就是从2000年后才开始有大量的人拥入程序员这个行业，程序员的人数在过去30年间也是呈增涨态势的，所以，我个人认为，所谓的“众多老程序员”的比例会被2005年以后大量拥入程序员行业的年青人所“稀释”。所以，上图的比例不能完全说明程序员是个青春饭</strong>。</p>
<p>但是，我们还是要正视老牌资深的程序员越来越少的这个事实，在这份报告第三部分中说了一些和程序员职业生涯相关的调查，如下：</p>
<ul>
<li><strong>在被问到有多少人对自己的职业满意的时</strong>。有40%的人觉得很满意，而有34.3%的人觉得一般满意，有10%的人说不清，还有15%的人是不满意的。可以看到有不少人是对这个职业生涯是有想法的。</li>
<li><strong>在被问到有多少人想转管理而可以挣得更多时</strong>。有30%的人是说想转的，有51%的人是明确不转的，还有20%的人是说不知道。可见，想转管理的人最多可能会有一半的人。</li>
<li><strong>在被问到有多少人想转管理时</strong>。有1/3的人是明确不想转的，而有1/4的人是明确是想转，而有36%的人则是不说，观望中。可见，的确是有很多想想转管理的。</li>
</ul>
<p><strong>我们可以看到，程序员中并不是所有的人都是可以坚持这么长时间的，这也挺正常的，对很大一部分人来说，对这个职业是有或多或少的不满意的，也有一部分人可能会随着技术的更新被淘汰，还有另外很大一部分人是想转管理的。所以，能够长时间地跟上形势长时间地喜欢写代码，并且对程序员这个的职业长期满意，不想转管理的，的确是为随时年龄的越大也越来越少</strong>。</p>
<p><strong>但我们完全可以看出来，程序员的主力军在20-40岁这个区间，而30岁左右的程序员是年富力强（经验和能力都很好）的黄金时间</strong>。</p>
<p>老程序员在国外似乎不会存在多大的问题，但在国内会有一些问题，所以，对于像我一样喜欢写代码、打算长久做程序员的兄弟，这里分享一些相关的经验。</p>
<ol>
<li><strong>持续高效地学习</strong>。软件行业的新技术层出不穷，旧的技术淘汰很快，所以我们更要多多学习基础技术和原理，那些都是很难改变的，并且基础扎实了后，学习新的技术也才会更快速。其间我们也不要乱学新技术，我们要关注那些有潜力的技术，也就看准了再学（参看酷壳的《<a href="https://coolshell.cn/articles/18190.html">Go语言、Docker和新技术</a>》）。注意，而是跟上大时代已经比较不容易，引领时代的人还是少数，所以，还是要更为高效地学习。</li>
<li><strong>积极面对他人的不解</strong>。 很多时候，总是会有人说：“到了你这个年纪怎么还在做程序员？”，这句话感觉就是对程序员这个职业的一种羞辱，社会的价值观感觉容不下大龄程序员。这个时候，我一般会跟他们解释到，我40来岁了，我觉得自己的状态还很好，工作完成没什么问题，偶尔加班到凌晨也行，新知识和技术我学起来不比年轻人慢，我在这个年纪有的经验比他们都多，而且，我这个年纪还在写代码，说明我真的喜欢这个事，<strong>像我这样的人能够长时间坚持做一个职业的人这个世界已经不多了，你们应该珍惜……</strong></li>
<li><strong>找到自己的定位</strong>。我们需要做好职业规划、财务和心理方面的准备。40岁的程序员，所能竞争的一定是自己的认识和经验，所以，40岁以后如果你还是很喜欢这一行业，你的社会阅历和经历以及对这个社会的理解，可以让你做一些有创新的事，除此之外，你还可以做一个教练、老师、咨询、专家……，用你的经验和能力帮助下一代和一些中小型的公司，这不但是他们的刚需，同时也会让重新焕发的。</li>
</ol>
<h4 id="toc_2">第二部分，技术</h4>
<p>首先，在这部分，主要是了解一些技术，这部分的技术可以给于程序员们一些指导。</p>
<table>
<thead>
<tr>
<th>最流行的语言</th>
<th>最热门的语言</th>
</tr>
</thead>
<tbody>
<tr>
<td><img decoding="async" loading="lazy" class="aligncenter wp-image-19313" src="https://coolshell.cn/wp-content/uploads/2019/04/06-06.Popular.Languages-669x1024.png" alt="" width="377" height="709" /></td>
<td><img decoding="async" loading="lazy" class="aligncenter wp-image-19314" src="https://coolshell.cn/wp-content/uploads/2019/04/06-07.Loved_.Languages-679x1024.png" alt="" width="367" height="743" /></td>
</tr>
</tbody>
</table>
<p>我们可以看到，</p>
<ul>
<li>Javascript/HTML/CSS是很多人都会用到的，后面的是SQL，这个也没什么问题，无论前后端的人，或多或少都会要用到的，这些技术感觉已经成为了基础必会的技术了，就像数中的加减乘除一样。</li>
<li>Python/Java/Shell 是后端开发主流语言的前三强，Python在今年超过了Java。这里让我比较好奇的是居然还有很多人用Shell，这估计跟运维有关，所以，Python的热可能也是通过运维和大数据相关。</li>
<li>流行语言后，第二梯队的是 C# / PHP / C++ / TypeScript / C ，接下来的是： Ruby / Go / Swift / Kotlin /WebAssembly / Rust&#8230; 。但在最被程序员喜欢的编程语言中：Rust / Python / TypeScript / Koltin / WebAssembly / Swift / Go&#8230; 都是排在前几名的。<strong>程序语言每隔一段时间就会整出一些新的语言来，我们一定要明白新出来的东西主要是为了解决什么样的问题，不然很容易迷失。</strong></li>
<li>在后面还有一个编程语言的薪资图，我们可以看到，在上面被提过的这些个编程语言中，<strong>Go语言的薪资是最高的（这可能是因为Go语言写关键的系统级的中件间——因为Go语言正在成为云计算的第一编程语言）</strong>，然后是Scala、Ruby、WebAssembly、Rust、Erlang、Shell、Python、Typescript……</li>
</ul>
<p><strong>通过这些个信息，我们可以看出主流技术、有潜力的技术，传统过气技术，以及相关薪资，对我们在选择编程语言上有一定的启示。</strong></p>
<p>在后面，我们可以看到:</p>
<ul>
<li>在 Web 开发框架上，主流使用还是 jQuery, React.js，Angular.js 为最前面的三个前端开发框架。而被程序员所喜欢的则是 React.js，Vue.js，Express, Spring，程序员非常不喜欢 Drupal，jQuery，Ruby on Rails 和Angular.js……</li>
<li>在其它开发框架/库/工具上，主流是Node.js、.NET、Pandas、Unity 3D、Tensorflow、Ansible、Cordova、Xamarin……而程序员比较喜欢的是.NET、Torch/PyTorch、Flutter、Pandas、Tensorflow、Node.js &#8230;</li>
<li>在操作系统上，主流使用Linux、Windows、Docker、Android、AWS……，而程序员最喜欢的是Linux、Docker、Kubernetes、Raspberry Pi、AWS、MacOS、iOS……</li>
<li>在数据库上，MySQL、PostgreSQL、MSSQL、SQLite、MongoDB、Redis、Elasticsearch是比较主流的，而程序员非常喜欢的是，Redis、PostgreSQL、Elasticsearch、Firebase、MongoDB……，程序员比较讨厌的是 Couchbase、Oracle、Cassandra、MySQL。</li>
</ul>
<p><strong>从这些个图表中，我们可以看到主流和有潜力的技术是什么，我们可以看到 Windows 的技术并没有过时，感觉似乎都有可能会卷土重来，但是，开源的技术来势凶凶，正在吞食整个软件业，不容小觑，Docker/Kubernetes无论是在主流应用上还是被程序员的喜好上都是非常猛的，而云平台的AWS开始成为标准平台技术……</strong></p>
<p>接下来的开发工具中，我们可以看到：</p>
<ul>
<li>Visual Studio Code 成为了最流行的开发工具。让我没有想到的是跟在后面的是 Notepad++（好久没用这个工具了，我得找回来用用了），而IntelliJ、Vim、Sublime Text排以后面。 Eclipse 和 Atom 动力不足，Emacs 开始变得小众了。</li>
<li>程序员主要的开发平台还是Windows占了近1/2， MacOS和Linux随后，各占1/4。</li>
<li>有38%的人使用容器技术做开发，30%的人使用容器做测试，在生产线上使用容器的有26%</li>
</ul>
<p><strong>看样子编程开发工具还是Visual Studio 和 IntelliJ的天下，MacOS/Linux正在抢Windows的开发市场</strong></p>
<p>接下来，StackOverflow给了一个技术圈的图</p>
<p><img decoding="async" loading="lazy" class="aligncenter" src="https://coolshell.cn/wp-content/uploads/2019/04/06-08.Technology.Circle-1024x1024.png" alt="" width="640" height="640" /></p>
<p>从上面这个图中，我们可以看以技术的几圈子：</p>
<ul>
<li><strong>Microsoft圈</strong> &#8211; Windows、.NET、ASP.NET、C#、Azure、SQL Server</li>
<li><strong>Java圈</strong> &#8211; Java、Spring</li>
<li><strong>手机圈</strong> &#8211; Android、 iOS、Kotlin、Swift、Firebase</li>
<li><strong>前端圈</strong> &#8211; Javascript、React.js、Angular.js、PHP</li>
<li><strong>大数据圈</strong> &#8211; Python、TensorFlow、Torch/PyTorch</li>
<li><strong>基础平台圈</strong> &#8211; Linux、Shell、Vim、Docker、Kubernetes、Elasticsearch、Redis……</li>
<li><strong>其它圈子</strong> &#8211; C/C++/汇编圈子、Ruby圈子、Hadoop/Spark圈子、……</li>
</ul>
<p><strong>看到谁的圈子大了吧，圈子大的并不代表技术实力强或是有前途，不过可以代表在那个圈子相关的关联技术，一方面，可以给你一些相关的参考，另一方面，整体可以让你看到全部的目前比较主流的技术。</strong></p>
<h4 id="toc_3">第三部份 工作</h4>
<p>在第三部份工作中，我们可以看到如下的一些数据：</p>
<ul>
<li>有3/4的程序员是全职的，10%左右的程序员是自由职业，6%左右的程序员是失业的，这个比例在北美、印度和欧洲都差不多。</li>
<li>有1/3的人在过去一年内换过工作，1/4的人在过去1-2年间换过工作，1/3的人在2-4年换过工作。</li>
<li>程序员找工作时，影响程序员的几个主要因素是：技术（编程语言、框架和使用的技术）、办公环境和公司文化、灵活的时间和安排、更专业的机会、远程工作……</li>
<li>影响程序员工作的几大因素是：有干扰的工作环境、开会、要干一些和开发无关的事、人手不够、管理不够、工具不够、通勤时间……</li>
<li>对于工程质量，有近70%的人有Code Review，而30%的则没有；有60%多的人有Unit Test，而不到40%的没有……</li>
</ul>
<p><strong>从工作中我们可以看到，程序员还是比较关心技术和公司文化的，换工作也是这个职业很正常的特性，他们并不喜欢被打扰，希望有足够的时间，而对于工程质量还是很有追求的。</strong></p>
<p>最后用一张程序员的“<strong>每周工作时间</strong>” 来结束本文！</p>
<p><img decoding="async" loading="lazy" class="aligncenter " src="https://coolshell.cn/wp-content/uploads/2019/04/07-09.Hours_.Worked.Per_.Week_-1024x640.png" alt="" width="498" height="280" /></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/22298.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2022/10/communication-150x150.png" alt="聊聊团队协同和协同工具" width="150" height="150" /></a><a href="https://coolshell.cn/articles/22298.html" class="wp_rp_title">聊聊团队协同和协同工具</a></li><li ><a href="https://coolshell.cn/articles/22173.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2022/02/http_method-150x150.png" alt="“一把梭：REST API 全用 POST”" width="150" height="150" /></a><a href="https://coolshell.cn/articles/22173.html" class="wp_rp_title">“一把梭：REST API 全用 POST”</a></li><li ><a href="https://coolshell.cn/articles/22157.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2022/02/monitoring-150x150.jpeg" alt="谈谈公司对员工的监控" width="150" height="150" /></a><a href="https://coolshell.cn/articles/22157.html" class="wp_rp_title">谈谈公司对员工的监控</a></li><li ><a href="https://coolshell.cn/articles/21589.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2021/07/knowledge_sharing-300x169-1-150x150.jpeg" alt="如何做一个有质量的技术分享" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21589.html" class="wp_rp_title">如何做一个有质量的技术分享</a></li><li ><a href="https://coolshell.cn/articles/20977.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/08/programmer.01-e1596792460687-150x150.png" alt="程序员如何把控自己的职业" width="150" height="150" /></a><a href="https://coolshell.cn/articles/20977.html" class="wp_rp_title">程序员如何把控自己的职业</a></li><li ><a href="https://coolshell.cn/articles/20765.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/01/remote-150x150.jpg" alt="MegaEase的远程工作文化" width="150" height="150" /></a><a href="https://coolshell.cn/articles/20765.html" class="wp_rp_title">MegaEase的远程工作文化</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/19307.html">StackOverflow 2019 程序员调查</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/19307.html/feed</wfw:commentRss>
			<slash:comments>19</slash:comments>
		
		
			</item>
	</channel>
</rss>
