gopl-zh.github.com/ch12/ch12-03.html

413 lines
40 KiB
HTML
Raw Normal View History

<!DOCTYPE HTML>
<html lang="zh" class="sidebar-visible no-js light">
<head>
<!-- Book generated using mdBook -->
<meta charset="UTF-8">
<title>Display递归打印 - Go语言圣经</title>
<!-- Custom HTML head -->
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<meta name="description" content="&lt;The Go Programming Language&gt;中文版">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="../favicon.svg">
<link rel="shortcut icon" href="../favicon.png">
<link rel="stylesheet" href="../css/variables.css">
<link rel="stylesheet" href="../css/general.css">
<link rel="stylesheet" href="../css/chrome.css">
<link rel="stylesheet" href="../css/print.css" media="print">
<!-- Fonts -->
<link rel="stylesheet" href="../FontAwesome/css/font-awesome.css">
<link rel="stylesheet" href="../fonts/fonts.css">
<!-- Highlight.js Stylesheets -->
<link rel="stylesheet" href="../highlight.css">
<link rel="stylesheet" href="../tomorrow-night.css">
<link rel="stylesheet" href="../ayu-highlight.css">
<!-- Custom theme stylesheets -->
<link rel="stylesheet" href="../style.css">
</head>
<body>
<!-- Provide site root to javascript -->
<script type="text/javascript">
var path_to_root = "../";
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "light";
</script>
<!-- Work around some values being stored in localStorage wrapped in quotes -->
<script type="text/javascript">
try {
var theme = localStorage.getItem('mdbook-theme');
var sidebar = localStorage.getItem('mdbook-sidebar');
if (theme.startsWith('"') && theme.endsWith('"')) {
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
}
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
}
} catch (e) { }
</script>
<!-- Set the theme before any content is loaded, prevents flash -->
<script type="text/javascript">
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; }
var html = document.querySelector('html');
html.classList.remove('no-js')
html.classList.remove('light')
html.classList.add(theme);
html.classList.add('js');
</script>
<!-- Hide / unhide sidebar before it is displayed -->
<script type="text/javascript">
var html = document.querySelector('html');
var sidebar = 'hidden';
if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible';
}
html.classList.remove('sidebar-visible');
html.classList.add("sidebar-" + sidebar);
</script>
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
<div class="sidebar-scrollbox">
<ol class="chapter"><li class="chapter-item expanded affix "><a href="../index.html">Go语言圣经</a></li><li class="chapter-item expanded affix "><a href="../preface-zh.html">译者序</a></li><li class="chapter-item expanded affix "><a href="../preface.html">前言</a></li><li class="chapter-item expanded "><a href="../ch1/ch1.html"><strong aria-hidden="true">1.</strong> 入门</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../ch1/ch1-01.html"><strong aria-hidden="true">1.1.</strong> Hello, World</a></li><li class="chapter-item expanded "><a href="../ch1/ch1-02.html"><strong aria-hidden="true">1.2.</strong> 命令行参数</a></li><li class="chapter-item expanded "><a href="../ch1/ch1-03.html"><strong aria-hidden="true">1.3.</strong> 查找重复的行</a></li><li class="chapter-item expanded "><a href="../ch1/ch1-04.html"><strong aria-hidden="true">1.4.</strong> GIF动画</a></li><li class="chapter-item expanded "><a href="../ch1/ch1-05.html"><strong aria-hidden="true">1.5.</strong> 获取URL</a></li><li class="chapter-item expanded "><a href="../ch1/ch1-06.html"><strong aria-hidden="true">1.6.</strong> 并发获取多个URL</a></li><li class="chapter-item expanded "><a href="../ch1/ch1-07.html"><strong aria-hidden="true">1.7.</strong> Web服务</a></li><li class="chapter-item expanded "><a href="../ch1/ch1-08.html"><strong aria-hidden="true">1.8.</strong> 本章要点</a></li></ol></li><li class="chapter-item expanded "><a href="../ch2/ch2.html"><strong aria-hidden="true">2.</strong> 程序结构</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../ch2/ch2-01.html"><strong aria-hidden="true">2.1.</strong> 命名</a></li><li class="chapter-item expanded "><a href="../ch2/ch2-02.html"><strong aria-hidden="true">2.2.</strong> 声明</a></li><li class="chapter-item expanded "><a href="../ch2/ch2-03.html"><strong aria-hidden="true">2.3.</strong> 变量</a></li><li class="chapter-item expanded "><a href="../ch2/ch2-04.html"><strong aria-hidden="true">2.4.</strong> 赋值</a></li><li class="chapter-item expanded "><a href="../ch2/ch2-05.html"><strong aria-hidden="true">2.5.</strong> 类型</a></li><li class="chapter-item expanded "><a href="../ch2/ch2-06.html"><strong aria-hidden="true">2.6.</strong> 包和文件</a></li><li class="chapter-item expanded "><a href="../ch2/ch2-07.html"><strong aria-hidden="true">2.7.</strong> 作用域</a></li></ol></li><li class="chapter-item expanded "><a href="../ch3/ch3.html"><strong aria-hidden="true">3.</strong> 基础数据类型</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../ch3/ch3-01.html"><strong aria-hidden="true">3.1.</strong> 整型</a></li><li class="chapter-item expanded "><a href="../ch3/ch3-02.html"><strong aria-hidden="true">3.2.</strong> 浮点数</a></li><li class="chapter-item expanded "><a href="../ch3/ch3-03.html"><strong aria-hidden="true">3.3.</strong> 复数</a></li><li class="chapter-item expanded "><a href="../ch3/ch3-04.html"><strong aria-hidden="true">3.4.</strong> 布尔型</a></li><li class="chapter-item expanded "><a href="../ch3/ch3-05.html"><strong aria-hidden="true">3.5.</strong> 字符串</a></li><li class="chapter-item expanded "><a href="../ch3/ch3-06.html"><strong aria-hidden="true">3.6.</strong> 常量</a></li></ol></li><li class="chapter-item expanded "><a href="../ch4/ch4.html"><strong aria-hidden="true">4.</strong> 复合数据类型</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../ch4/ch4-01.html"><strong aria-hidden="true">4.1.</strong> 数组</a></li><li class="chapter-item expanded "><a href="../ch4/ch4-02.html"><strong aria-hidden="true">4.2.</strong> Slice</a></li><li class="chapter-item expanded "><a href="../ch4/ch4-03.html"><strong aria-hidden="true">4.3.</strong> Map</a></li><li class="chapter-item expanded "><a href="../ch4/ch4-04.html"><strong aria-hidden="true">4.4.</strong> 结构体</a></li><li class="chapter-item expanded "><a href="../ch4/ch4-05.html"><strong aria-hidden="true">4.5.</strong> JSON</
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
</nav>
<div id="page-wrapper" class="page-wrapper">
<div class="page">
<div id="menu-bar-hover-placeholder"></div>
<div id="menu-bar" class="menu-bar sticky bordered">
<div class="left-buttons">
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
<i class="fa fa-bars"></i>
</button>
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
<i class="fa fa-paint-brush"></i>
</button>
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
<li role="none"><button role="menuitem" class="theme" id="light">Light (default)</button></li>
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
</ul>
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
<i class="fa fa-search"></i>
</button>
</div>
<h1 class="menu-title">Go语言圣经</h1>
<div class="right-buttons">
<a href="../print.html" title="Print this book" aria-label="Print this book">
<i id="print-button" class="fa fa-print"></i>
</a>
<a href="https://github.com/gopl-zh/gopl-zh.github.com" title="Git repository" aria-label="Git repository">
<i id="git-repository-button" class="fa fa-github"></i>
</a>
<a href="https://github.com/gopl-zh/gopl-zh.github.com/edit/master/./ch12/ch12-03.md" title="Suggest an edit" aria-label="Suggest an edit">
<i id="git-edit-button" class="fa fa-edit"></i>
</a>
</div>
</div>
<div id="search-wrapper" class="hidden">
<form id="searchbar-outer" class="searchbar-outer">
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
</form>
<div id="searchresults-outer" class="searchresults-outer hidden">
<div id="searchresults-header" class="searchresults-header"></div>
<ul id="searchresults">
</ul>
</div>
</div>
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
<script type="text/javascript">
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
});
</script>
<div id="content" class="content">
<!-- Page table of contents -->
<div class="sidetoc"><nav class="pagetoc"></nav></div>
<main>
<!-- 头部 -->
<ul dir="auto">
<li><em>Go语言圣经读书笔记(挖坑中...): <a href="https://github.com/chai2010/gopl-notes-zh">https://github.com/chai2010/gopl-notes-zh</a></em></li>
<li><em>凹语言(Go实现, 面向WASM设计): <a href="https://github.com/wa-lang/wa">https://github.com/wa-lang/wa</a></em></li>
</ul>
<hr>
<h2 id="123-display一个递归的值打印器"><a class="header" href="#123-display一个递归的值打印器">12.3. Display一个递归的值打印器</a></h2>
<p>接下来让我们看看如何改善聚合数据类型的显示。我们并不想完全克隆一个fmt.Sprint函数我们只是构建一个用于调试用的Display函数给定任意一个复杂类型 x打印这个值对应的完整结构同时标记每个元素的发现路径。让我们从一个例子开始。</p>
<pre><code class="language-Go">e, _ := eval.Parse(&quot;sqrt(A / pi)&quot;)
Display(&quot;e&quot;, e)
</code></pre>
<p>在上面的调用中传入Display函数的参数是在7.9节一个表达式求值函数返回的语法树。Display函数的输出如下</p>
<pre><code class="language-Go">Display e (eval.call):
e.fn = &quot;sqrt&quot;
e.args[0].type = eval.binary
e.args[0].value.op = 47
e.args[0].value.x.type = eval.Var
e.args[0].value.x.value = &quot;A&quot;
e.args[0].value.y.type = eval.Var
e.args[0].value.y.value = &quot;pi&quot;
</code></pre>
<p>你应该尽量避免在一个包的API中暴露涉及反射的接口。我们将定义一个未导出的display函数用于递归处理工作导出的是Display函数它只是display函数简单的包装以接受interface{}类型的参数:</p>
<p><u><i>gopl.io/ch12/display</i></u></p>
<pre><code class="language-Go">func Display(name string, x interface{}) {
fmt.Printf(&quot;Display %s (%T):\n&quot;, name, x)
display(name, reflect.ValueOf(x))
}
</code></pre>
<p>在display函数中我们使用了前面定义的打印基础类型——基本类型、函数和chan等——元素值的formatAtom函数但是我们会使用reflect.Value的方法来递归显示复杂类型的每一个成员。在递归下降过程中path字符串从最开始传入的起始值这里是“e”将逐步增长来表示是如何达到当前值例如“e.args[0].value”的。</p>
<p>因为我们不再模拟fmt.Sprint函数我们将直接使用fmt包来简化我们的例子实现。</p>
<pre><code class="language-Go">func display(path string, v reflect.Value) {
switch v.Kind() {
case reflect.Invalid:
fmt.Printf(&quot;%s = invalid\n&quot;, path)
case reflect.Slice, reflect.Array:
for i := 0; i &lt; v.Len(); i++ {
display(fmt.Sprintf(&quot;%s[%d]&quot;, path, i), v.Index(i))
}
case reflect.Struct:
for i := 0; i &lt; v.NumField(); i++ {
fieldPath := fmt.Sprintf(&quot;%s.%s&quot;, path, v.Type().Field(i).Name)
display(fieldPath, v.Field(i))
}
case reflect.Map:
for _, key := range v.MapKeys() {
display(fmt.Sprintf(&quot;%s[%s]&quot;, path,
formatAtom(key)), v.MapIndex(key))
}
case reflect.Ptr:
if v.IsNil() {
fmt.Printf(&quot;%s = nil\n&quot;, path)
} else {
display(fmt.Sprintf(&quot;(*%s)&quot;, path), v.Elem())
}
case reflect.Interface:
if v.IsNil() {
fmt.Printf(&quot;%s = nil\n&quot;, path)
} else {
fmt.Printf(&quot;%s.type = %s\n&quot;, path, v.Elem().Type())
display(path+&quot;.value&quot;, v.Elem())
}
default: // basic types, channels, funcs
fmt.Printf(&quot;%s = %s\n&quot;, path, formatAtom(v))
}
}
</code></pre>
<p>让我们针对不同类型分别讨论。</p>
<p><strong>Slice和数组</strong> 两种的处理逻辑是一样的。Len方法返回slice或数组值中的元素个数Index(i)获得索引i对应的元素返回的也是一个reflect.Value如果索引i超出范围的话将导致panic异常这与数组或slice类型内建的len(a)和a[i]操作类似。display针对序列中的每个元素递归调用自身处理我们通过在递归处理时向path附加“[i]”来表示访问路径。</p>
<p>虽然reflect.Value类型带有很多方法但是只有少数的方法能对任意值都安全调用。例如Index方法只能对Slice、数组或字符串类型的值调用如果对其它类型调用则会导致panic异常。</p>
<p><strong>结构体:</strong> NumField方法报告结构体中成员的数量Field(i)以reflect.Value类型返回第i个成员的值。成员列表也包括通过匿名字段提升上来的成员。为了在path添加“.f”来表示成员路径我们必须获得结构体对应的reflect.Type类型信息然后访问结构体第i个成员的名字。</p>
<p><strong>Maps:</strong> MapKeys方法返回一个reflect.Value类型的slice每一个元素对应map的一个key。和往常一样遍历map时顺序是随机的。MapIndex(key)返回map中key对应的value。我们向path添加“[key]”来表示访问路径。我们这里有一个未完成的工作。其实map的key的类型并不局限于formatAtom能完美处理的类型数组、结构体和接口都可以作为map的key。针对这种类型完善key的显示信息是练习12.1的任务。)</p>
<p><strong>指针:</strong> Elem方法返回指针指向的变量依然是reflect.Value类型。即使指针是nil这个操作也是安全的在这种情况下指针是Invalid类型但是我们可以用IsNil方法来显式地测试一个空指针这样我们可以打印更合适的信息。我们在path前面添加“*”,并用括弧包含以避免歧义。</p>
<p><strong>接口:</strong> 再一次我们使用IsNil方法来测试接口是否是nil如果不是我们可以调用v.Elem()来获取接口对应的动态值,并且打印对应的类型和值。</p>
<p>现在我们的Display函数总算完工了让我们看看它的表现吧。下面的Movie类型是在4.5节的电影类型上演变来的:</p>
<pre><code class="language-Go">type Movie struct {
Title, Subtitle string
Year int
Color bool
Actor map[string]string
Oscars []string
Sequel *string
}
</code></pre>
<p>让我们声明一个该类型的变量然后看看Display函数如何显示它</p>
<pre><code class="language-Go">strangelove := Movie{
Title: &quot;Dr. Strangelove&quot;,
Subtitle: &quot;How I Learned to Stop Worrying and Love the Bomb&quot;,
Year: 1964,
Color: false,
Actor: map[string]string{
&quot;Dr. Strangelove&quot;: &quot;Peter Sellers&quot;,
&quot;Grp. Capt. Lionel Mandrake&quot;: &quot;Peter Sellers&quot;,
&quot;Pres. Merkin Muffley&quot;: &quot;Peter Sellers&quot;,
&quot;Gen. Buck Turgidson&quot;: &quot;George C. Scott&quot;,
&quot;Brig. Gen. Jack D. Ripper&quot;: &quot;Sterling Hayden&quot;,
`Maj. T.J. &quot;King&quot; Kong`: &quot;Slim Pickens&quot;,
},
Oscars: []string{
&quot;Best Actor (Nomin.)&quot;,
&quot;Best Adapted Screenplay (Nomin.)&quot;,
&quot;Best Director (Nomin.)&quot;,
&quot;Best Picture (Nomin.)&quot;,
},
}
</code></pre>
<p>Display(&quot;strangelove&quot;, strangelove)调用将显示strangelove电影对应的中文名是《奇爱博士》</p>
<pre><code class="language-Go">Display strangelove (display.Movie):
strangelove.Title = &quot;Dr. Strangelove&quot;
strangelove.Subtitle = &quot;How I Learned to Stop Worrying and Love the Bomb&quot;
strangelove.Year = 1964
strangelove.Color = false
strangelove.Actor[&quot;Gen. Buck Turgidson&quot;] = &quot;George C. Scott&quot;
strangelove.Actor[&quot;Brig. Gen. Jack D. Ripper&quot;] = &quot;Sterling Hayden&quot;
strangelove.Actor[&quot;Maj. T.J. \&quot;King\&quot; Kong&quot;] = &quot;Slim Pickens&quot;
strangelove.Actor[&quot;Dr. Strangelove&quot;] = &quot;Peter Sellers&quot;
strangelove.Actor[&quot;Grp. Capt. Lionel Mandrake&quot;] = &quot;Peter Sellers&quot;
strangelove.Actor[&quot;Pres. Merkin Muffley&quot;] = &quot;Peter Sellers&quot;
strangelove.Oscars[0] = &quot;Best Actor (Nomin.)&quot;
strangelove.Oscars[1] = &quot;Best Adapted Screenplay (Nomin.)&quot;
strangelove.Oscars[2] = &quot;Best Director (Nomin.)&quot;
strangelove.Oscars[3] = &quot;Best Picture (Nomin.)&quot;
strangelove.Sequel = nil
</code></pre>
<p>我们也可以使用Display函数来显示标准库中类型的内部结构例如<code>*os.File</code>类型:</p>
<pre><code class="language-Go">Display(&quot;os.Stderr&quot;, os.Stderr)
// Output:
// Display os.Stderr (*os.File):
// (*(*os.Stderr).file).fd = 2
// (*(*os.Stderr).file).name = &quot;/dev/stderr&quot;
// (*(*os.Stderr).file).nepipe = 0
</code></pre>
<p>可以看出反射能够访问到结构体中未导出的成员。需要当心的是这个例子的输出在不同操作系统上可能是不同的并且随着标准库的发展也可能导致结果不同。这也是将这些成员定义为私有成员的原因之一我们甚至可以用Display函数来显示reflect.Value 的内部构造(在这里设置为<code>*os.File</code>的类型描述体)。<code>Display(&quot;rV&quot;, reflect.ValueOf(os.Stderr))</code>调用的输出如下,当然不同环境得到的结果可能有差异:</p>
<pre><code class="language-Go">Display rV (reflect.Value):
(*rV.typ).size = 8
(*rV.typ).hash = 871609668
(*rV.typ).align = 8
(*rV.typ).fieldAlign = 8
(*rV.typ).kind = 22
(*(*rV.typ).string) = &quot;*os.File&quot;
(*(*(*rV.typ).uncommonType).methods[0].name) = &quot;Chdir&quot;
(*(*(*(*rV.typ).uncommonType).methods[0].mtyp).string) = &quot;func() error&quot;
(*(*(*(*rV.typ).uncommonType).methods[0].typ).string) = &quot;func(*os.File) error&quot;
...
</code></pre>
<p>观察下面两个例子的区别:</p>
<pre><code class="language-Go">var i interface{} = 3
Display(&quot;i&quot;, i)
// Output:
// Display i (int):
// i = 3
Display(&quot;&amp;i&quot;, &amp;i)
// Output:
// Display &amp;i (*interface {}):
// (*&amp;i).type = int
// (*&amp;i).value = 3
</code></pre>
<p>在第一个例子中Display函数调用reflect.ValueOf(i)它返回一个Int类型的值。正如我们在12.2节中提到的reflect.ValueOf总是返回一个具体类型的 Value因为它是从一个接口值提取的内容。</p>
<p>在第二个例子中Display函数调用的是reflect.ValueOf(&amp;i)它返回一个指向i的指针对应Ptr类型。在switch的Ptr分支中对这个值调用 Elem 方法返回一个Value来表示变量 i 本身对应Interface类型。像这样一个间接获得的Value可能代表任意类型的值包括接口类型。display函数递归调用自身这次它分别打印了这个接口的动态类型和值。</p>
<p>对于目前的实现如果遇到对象图中含有回环Display将会陷入死循环例如下面这个首尾相连的链表</p>
<pre><code class="language-Go">// a struct that points to itself
type Cycle struct{ Value int; Tail *Cycle }
var c Cycle
c = Cycle{42, &amp;c}
Display(&quot;c&quot;, c)
</code></pre>
<p>Display会永远不停地进行深度递归打印</p>
<pre><code class="language-Go">Display c (display.Cycle):
c.Value = 42
(*c.Tail).Value = 42
(*(*c.Tail).Tail).Value = 42
(*(*(*c.Tail).Tail).Tail).Value = 42
...ad infinitum...
</code></pre>
<p>许多Go语言程序都包含了一些循环的数据。让Display支持这类带环的数据结构需要些技巧需要额外记录迄今访问的路径相应会带来成本。通用的解决方案是采用 unsafe 的语言特性我们将在13.3节看到具体的解决方案。</p>
<p>带环的数据结构很少会对fmt.Sprint函数造成问题因为它很少尝试打印完整的数据结构。例如当它遇到一个指针的时候它只是简单地打印指针的数字值。在打印包含自身的slice或map时可能卡住但是这种情况很罕见不值得付出为了处理回环所需的开销。</p>
<p><strong>练习 12.1</strong> 扩展Display函数使它可以显示包含以结构体或数组作为map的key类型的值。</p>
<p><strong>练习 12.2</strong> 增强display函数的稳健性通过记录边界的步数来确保在超出一定限制后放弃递归。在13.3节,我们会看到另一种探测数据结构是否存在环的技术。)</p>
<!-- 公众号 -->
<hr>
<table>
<tr>
<td>
<img width="222px" src="https://chai2010.cn/advanced-go-programming-book/css.png">
</td>
<td>
<img width="222px" src="https://chai2010.cn/advanced-go-programming-book/cch.png">
</td>
</tr>
</table>
<div id="giscus-container"></div>
<footer class="page-footer">
<span>© 2015-2016 | <a href="https://github.com/gopl-zh"> Go语言圣经中文版</a>, 仅学习交流使用</span>
</footer>
</main>
<nav class="nav-wrapper" aria-label="Page navigation">
<!-- Mobile navigation buttons -->
<a rel="prev" href="../ch12/ch12-02.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next" href="../ch12/ch12-04.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
<div style="clear: both"></div>
</nav>
</div>
</div>
<nav class="nav-wide-wrapper" aria-label="Page navigation">
<a rel="prev" href="../ch12/ch12-02.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next" href="../ch12/ch12-04.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
</nav>
</div>
<script type="text/javascript">
window.playground_copyable = true;
</script>
<script src="../elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
<script src="../mark.min.js" type="text/javascript" charset="utf-8"></script>
<script src="../searcher.js" type="text/javascript" charset="utf-8"></script>
<script src="../clipboard.min.js" type="text/javascript" charset="utf-8"></script>
<script src="../highlight.js" type="text/javascript" charset="utf-8"></script>
<script src="../book.js" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript" charset="utf-8">
var pagePath = "ch12/ch12-03.md"
</script>
<!-- Custom JS scripts -->
<script type="text/javascript" src="../js/custom.js"></script>
<script type="text/javascript" src="../js/bigPicture.js"></script>
</body>
</html>