gopl-zh.github.com/ch4/ch4-06.html

358 lines
37 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>文本和HTML模板 - 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/./ch4/ch4-06.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>KusonStack一站式可编程配置技术栈(Go): <a href="https://github.com/KusionStack/kusion">https://github.com/KusionStack/kusion</a></em></li>
<li><em>KCL 配置编程语言(Rust): <a href="https://github.com/KusionStack/KCLVM">https://github.com/KusionStack/KCLVM</a></em></li>
<li><em>凹语言™: <a href="https://github.com/wa-lang/wa">https://github.com/wa-lang/wa</a></em></li>
</ul>
<hr>
<h2 id="46-文本和html模板"><a class="header" href="#46-文本和html模板">4.6. 文本和HTML模板</a></h2>
<p>前面的例子只是最简单的格式化使用Printf是完全足够的。但是有时候会需要复杂的打印格式这时候一般需要将格式化代码分离出来以便更安全地修改。这些功能是由text/template和html/template等模板包提供的它们提供了一个将变量值填充到一个文本或HTML格式的模板的机制。</p>
<p>一个模板是一个字符串或一个文件,里面包含了一个或多个由双花括号包含的<code>{{action}}</code>对象。大部分的字符串只是按字面值打印但是对于actions部分将触发其它的行为。每个actions都包含了一个用模板语言书写的表达式一个action虽然简短但是可以输出复杂的打印值模板语言包含通过选择结构体的成员、调用函数或方法、表达式控制流if-else语句和range循环语句还有其它实例化模板等诸多特性。下面是一个简单的模板字符串</p>
<p><u><i>gopl.io/ch4/issuesreport</i></u></p>
<pre><code class="language-Go">const templ = `{{.TotalCount}} issues:
{{range .Items}}----------------------------------------
Number: {{.Number}}
User: {{.User.Login}}
Title: {{.Title | printf &quot;%.64s&quot;}}
Age: {{.CreatedAt | daysAgo}} days
{{end}}`
</code></pre>
<p>这个模板先打印匹配到的issue总数然后打印每个issue的编号、创建用户、标题还有存在的时间。对于每一个action都有一个当前值的概念对应点操作符写作“.”。当前值“.”最初被初始化为调用模板时的参数在当前例子中对应github.IssuesSearchResult类型的变量。模板中<code>{{.TotalCount}}</code>对应action将展开为结构体中TotalCount成员以默认的方式打印的值。模板中<code>{{range .Items}}</code><code>{{end}}</code>对应一个循环action因此它们之间的内容可能会被展开多次循环每次迭代的当前值对应当前的Items元素的值。</p>
<p>在一个action中<code>|</code>操作符表示将前一个表达式的结果作为后一个函数的输入类似于UNIX中管道的概念。在Title这一行的action中第二个操作是一个printf函数是一个基于fmt.Sprintf实现的内置函数所有模板都可以直接使用。对于Age部分第二个动作是一个叫daysAgo的函数通过time.Since函数将CreatedAt成员转换为过去的时间长度</p>
<pre><code class="language-Go">func daysAgo(t time.Time) int {
return int(time.Since(t).Hours() / 24)
}
</code></pre>
<p>需要注意的是CreatedAt的参数类型是time.Time并不是字符串。以同样的方式我们可以通过定义一些方法来控制字符串的格式化§2.5一个类型同样可以定制自己的JSON编码和解码行为。time.Time类型对应的JSON值是一个标准时间格式的字符串。</p>
<p>生成模板的输出需要两个处理步骤。第一步是要分析模板并转为内部表示然后基于指定的输入执行模板。分析模板部分一般只需要执行一次。下面的代码创建并分析上面定义的模板templ。注意方法调用链的顺序template.New先创建并返回一个模板Funcs方法将daysAgo等自定义函数注册到模板中并返回模板最后调用Parse函数分析模板。</p>
<pre><code class="language-Go">report, err := template.New(&quot;report&quot;).
Funcs(template.FuncMap{&quot;daysAgo&quot;: daysAgo}).
Parse(templ)
if err != nil {
log.Fatal(err)
}
</code></pre>
<p>因为模板通常在编译时就测试好了如果模板解析失败将是一个致命的错误。template.Must辅助函数可以简化这个致命错误的处理它接受一个模板和一个error类型的参数检测error是否为nil如果不是nil则发出panic异常然后返回传入的模板。我们将在5.9节再讨论这个话题。</p>
<p>一旦模板已经创建、注册了daysAgo函数、并通过分析和检测我们就可以使用github.IssuesSearchResult作为输入源、os.Stdout作为输出源来执行模板</p>
<pre><code class="language-Go">var report = template.Must(template.New(&quot;issuelist&quot;).
Funcs(template.FuncMap{&quot;daysAgo&quot;: daysAgo}).
Parse(templ))
func main() {
result, err := github.SearchIssues(os.Args[1:])
if err != nil {
log.Fatal(err)
}
if err := report.Execute(os.Stdout, result); err != nil {
log.Fatal(err)
}
}
</code></pre>
<p>程序输出一个纯文本报告:</p>
<pre><code>$ go build gopl.io/ch4/issuesreport
$ ./issuesreport repo:golang/go is:open json decoder
13 issues:
----------------------------------------
Number: 5680
User: eaigner
Title: encoding/json: set key converter on en/decoder
Age: 750 days
----------------------------------------
Number: 6050
User: gopherbot
Title: encoding/json: provide tokenizer
Age: 695 days
----------------------------------------
...
</code></pre>
<p>现在让我们转到html/template模板包。它使用和text/template包相同的API和模板语言但是增加了一个将字符串自动转义特性这可以避免输入字符串和HTML、JavaScript、CSS或URL语法产生冲突的问题。这个特性还可以避免一些长期存在的安全问题比如通过生成HTML注入攻击通过构造一个含有恶意代码的问题标题这些都可能让模板输出错误的输出从而让他们控制页面。</p>
<p>下面的模板以HTML格式输出issue列表。注意import语句的不同</p>
<p><u><i>gopl.io/ch4/issueshtml</i></u></p>
<pre><code class="language-Go">import &quot;html/template&quot;
var issueList = template.Must(template.New(&quot;issuelist&quot;).Parse(`
&lt;h1&gt;{{.TotalCount}} issues&lt;/h1&gt;
&lt;table&gt;
&lt;tr style='text-align: left'&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;User&lt;/th&gt;
&lt;th&gt;Title&lt;/th&gt;
&lt;/tr&gt;
{{range .Items}}
&lt;tr&gt;
&lt;td&gt;&lt;a href='{{.HTMLURL}}'&gt;{{.Number}}&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;{{.State}}&lt;/td&gt;
&lt;td&gt;&lt;a href='{{.User.HTMLURL}}'&gt;{{.User.Login}}&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href='{{.HTMLURL}}'&gt;{{.Title}}&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
{{end}}
&lt;/table&gt;
`))
</code></pre>
<p>下面的命令将在新的模板上执行一个稍微不同的查询:</p>
<pre><code>$ go build gopl.io/ch4/issueshtml
$ ./issueshtml repo:golang/go commenter:gopherbot json encoder &gt;issues.html
</code></pre>
<p>图4.4显示了在web浏览器中的效果图。每个issue包含到Github对应页面的链接。</p>
<p><img src="../images/ch4-04.png" alt="" /></p>
<p>图4.4中issue没有包含会对HTML格式产生冲突的特殊字符但是我们马上将看到标题中含有<code>&amp;</code><code>&lt;</code>字符的issue。下面的命令选择了两个这样的issue</p>
<pre><code>$ ./issueshtml repo:golang/go 3133 10535 &gt;issues2.html
</code></pre>
<p>图4.5显示了该查询的结果。注意html/template包已经自动将特殊字符转义因此我们依然可以看到正确的字面值。如果我们使用text/template包的话这2个issue将会产生错误其中“&amp;lt;”四个字符将会被当作小于字符“&lt;”处理,同时“&lt;link&gt;”字符串将会被当作一个链接元素处理它们都会导致HTML文档结构的改变从而导致有未知的风险。</p>
<p>我们也可以通过对信任的HTML字符串使用template.HTML类型来抑制这种自动转义的行为。还有很多采用类型命名的字符串类型分别对应信任的JavaScript、CSS和URL。下面的程序演示了两个使用不同类型的相同字符串产生的不同结果A是一个普通字符串B是一个信任的template.HTML字符串类型。</p>
<p><img src="../images/ch4-05.png" alt="" /></p>
<p><u><i>gopl.io/ch4/autoescape</i></u></p>
<pre><code class="language-Go">func main() {
const templ = `&lt;p&gt;A: {{.A}}&lt;/p&gt;&lt;p&gt;B: {{.B}}&lt;/p&gt;`
t := template.Must(template.New(&quot;escape&quot;).Parse(templ))
var data struct {
A string // untrusted plain text
B template.HTML // trusted HTML
}
data.A = &quot;&lt;b&gt;Hello!&lt;/b&gt;&quot;
data.B = &quot;&lt;b&gt;Hello!&lt;/b&gt;&quot;
if err := t.Execute(os.Stdout, data); err != nil {
log.Fatal(err)
}
}
</code></pre>
<p>图4.6显示了出现在浏览器中的模板输出。我们看到A的黑体标记被转义失效了但是B没有。</p>
<p><img src="../images/ch4-06.png" alt="" /></p>
<p>我们这里只讲述了模板系统中最基本的特性。一如既往,如果想了解更多的信息,请自己查看包文档:</p>
<pre><code>$ go doc text/template
$ go doc html/template
</code></pre>
<p><strong>练习 4.14</strong> 创建一个web服务器查询一次GitHub然后生成BUG报告、里程碑和对应的用户信息。</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="../ch4/ch4-05.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="../ch5/ch5.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="../ch4/ch4-05.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="../ch5/ch5.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 = "ch4/ch4-06.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>