gopl-zh.github.com/print.html
github-actions[bot] c12f1dbf53 deploy: ac5c565db6
2022-08-24 14:36:47 +00:00

10419 lines
846 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE HTML>
<html lang="zh" class="sidebar-visible no-js light">
<head>
<!-- Book generated using mdBook -->
<meta charset="UTF-8">
<title>Go语言圣经</title>
<meta name="robots" content="noindex" />
<!-- 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.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</a></li><li class="chapter-item expanded "><a href="ch4/ch4-06.html"><strong aria-hidden="true">4.6.</strong> 文本和HTML模板</a></li></ol></li><li class="chapter-item expanded "><a href="ch5/ch5.html"><strong aria-hidden="true">5.</strong> 函数</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="ch5/ch5-01.html"><strong aria-hidden="true">5.1.</strong> 函数声明</a></li><li class="chapter-item expanded "><a href="ch5/ch5-02.html"><strong aria-hidden="true">5.2.</strong> 递归</a></li><li class="chapter-item expanded "><a href="ch5/ch5-03.html"><strong aria-hidden="true">5.3.</strong> 多返回值</a></li><li class="chapter-item expanded "><a href="ch5/ch5-04.html"><strong aria-hidden="true">5.4.</strong> 错误</a></li><li class="chapter-item expanded "><a href="ch5/ch5-05.html"><strong aria-hidden="true">5.5.</strong> 函数值</a></li><li class="chapter-item expanded "><a href="ch5/ch5-06.html"><strong aria-hidden="true">5.6.</strong> 匿名函数</a></li><li class="chapter-item expanded "><a href="ch5/ch5-07.html"><strong aria-hidden="true">5.7.</strong> 可变参数</a></li><li class="chapter-item expanded "><a href="ch5/ch5-08.html"><strong aria-hidden="true">5.8.</strong> Deferred函数</a></li><li class="chapter-item expanded "><a href="ch5/ch5-09.html"><strong aria-hidden="true">5.9.</strong> Panic异常</a></li><li class="chapter-item expanded "><a href="ch5/ch5-10.html"><strong aria-hidden="true">5.10.</strong> Recover捕获异常</a></li></ol></li><li class="chapter-item expanded "><a href="ch6/ch6.html"><strong aria-hidden="true">6.</strong> 方法</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="ch6/ch6-01.html"><strong aria-hidden="true">6.1.</strong> 方法声明</a></li><li class="chapter-item expanded "><a href="ch6/ch6-02.html"><strong aria-hidden="true">6.2.</strong> 基于指针对象的方法</a></li><li class="chapter-item expanded "><a href="ch6/ch6-03.html"><strong aria-hidden="true">6.3.</strong> 通过嵌入结构体来扩展类型</a></li><li class="chapter-item expanded "><a href="ch6/ch6-04.html"><strong aria-hidden="true">6.4.</strong> 方法值和方法表达式</a></li><li class="chapter-item expanded "><a href="ch6/ch6-05.html"><strong aria-hidden="true">6.5.</strong> 示例: Bit数组</a></li><li class="chapter-item expanded "><a href="ch6/ch6-06.html"><strong aria-hidden="true">6.6.</strong> 封装</a></li></ol></li><li class="chapter-item expanded "><a href="ch7/ch7.html"><strong aria-hidden="true">7.</strong> 接口</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="ch7/ch7-01.html"><strong aria-hidden="true">7.1.</strong> 接口是合约</a></li><li class="chapter-item expanded "><a href="ch7/ch7-02.html"><strong aria-hidden="true">7.2.</strong> 接口类型</a></li><li class="chapter-item expanded "><a href="ch7/ch7-03.html"><strong aria-hidden="true">7.3.</strong> 实现接口的条件</a></li><li class="chapter-item expanded "><a href="ch7/ch7-04.html"><strong aria-hidden="true">7.4.</strong> flag.Value接口</a></li><li class="chapter-item expanded "><a href="ch7/ch7-05.html"><strong aria-hidden="true">7.5.</strong> 接口值</a></li><li class="chapter-item expanded "><a href="ch7/ch7-06.html"><strong aria-hidden="true">7.6.</strong> sort.Interface接口</a></li><li class="chapter-item expanded "><a href="ch7/ch7-07.html"><strong aria-hidden="true">7.7.</strong> http.Handler接口</a></li><li class="chapter-item expanded "><a href="ch7/ch7-08.html"><strong aria-hidden="true">7.8.</strong> error接口</a></li><li class="chapter-item expanded "><a href="ch7/ch7-09.html"><strong aria-hidden="true">7.9.</strong> 示例: 表达式求值</a></li><li class="chapter-item expanded "><a href="ch7/ch7-10.html"><strong aria-hidden="true">7.10.</strong> 类型断言</a></li><li class="chapter-item expanded "><a href="ch7/ch7-11.html"><strong aria-hidden="true">7.11.</strong> 基于类型断言识别错误类型</a></li><li class="chapter-item expanded "><a href="ch7/ch7-12.html"><strong aria-hidden="true">7.12.</strong> 通过类型断言查询接口</a></li><li class="chapter-item expanded "><a href="ch7/ch7-13.html"><strong aria-hidden="true">7.13.</strong> 类型分支</a></li><li class="chapter-item expanded "><a href="ch7/ch7-14.html"><strong aria-hidden="true">7.14.</strong> 示例: 基于标记的XML解码</a></li><li class="chapter-item expanded "><a href="ch7/ch7-15.html"><strong aria-hidden="true">7.15.</strong> 补充几点</a></li></ol></li><li class="chapter-item expanded "><a href="ch8/ch8.html"><strong aria-hidden="true">8.</strong> Goroutines和Channels</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="ch8/ch8-01.html"><strong aria-hidden="true">8.1.</strong> Goroutines</a></li><li class="chapter-item expanded "><a href="ch8/ch8-02.html"><strong aria-hidden="true">8.2.</strong> 示例: 并发的Clock服务</a></li><li class="chapter-item expanded "><a href="ch8/ch8-03.html"><strong aria-hidden="true">8.3.</strong> 示例: 并发的Echo服务</a></li><li class="chapter-item expanded "><a href="ch8/ch8-04.html"><strong aria-hidden="true">8.4.</strong> Channels</a></li><li class="chapter-item expanded "><a href="ch8/ch8-05.html"><strong aria-hidden="true">8.5.</strong> 并发的循环</a></li><li class="chapter-item expanded "><a href="ch8/ch8-06.html"><strong aria-hidden="true">8.6.</strong> 示例: 并发的Web爬虫</a></li><li class="chapter-item expanded "><a href="ch8/ch8-07.html"><strong aria-hidden="true">8.7.</strong> 基于select的多路复用</a></li><li class="chapter-item expanded "><a href="ch8/ch8-09.html"><strong aria-hidden="true">8.8.</strong> 并发的退出</a></li><li class="chapter-item expanded "><a href="ch8/ch8-10.html"><strong aria-hidden="true">8.9.</strong> 示例: 聊天服务</a></li></ol></li><li class="chapter-item expanded "><a href="ch9/ch9.html"><strong aria-hidden="true">9.</strong> 基于共享变量的并发</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="ch9/ch9-01.html"><strong aria-hidden="true">9.1.</strong> 竞争条件</a></li><li class="chapter-item expanded "><a href="ch9/ch9-02.html"><strong aria-hidden="true">9.2.</strong> sync.Mutex互斥锁</a></li><li class="chapter-item expanded "><a href="ch9/ch9-03.html"><strong aria-hidden="true">9.3.</strong> sync.RWMutex读写锁</a></li><li class="chapter-item expanded "><a href="ch9/ch9-04.html"><strong aria-hidden="true">9.4.</strong> 内存同步</a></li><li class="chapter-item expanded "><a href="ch9/ch9-06.html"><strong aria-hidden="true">9.5.</strong> 竞争条件检测</a></li><li class="chapter-item expanded "><a href="ch9/ch9-07.html"><strong aria-hidden="true">9.6.</strong> 示例: 并发的非阻塞缓存</a></li><li class="chapter-item expanded "><a href="ch9/ch9-08.html"><strong aria-hidden="true">9.7.</strong> Goroutines和线程</a></li></ol></li><li class="chapter-item expanded "><a href="ch10/ch10.html"><strong aria-hidden="true">10.</strong> 包和工具</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="ch10/ch10-01.html"><strong aria-hidden="true">10.1.</strong> 包简介</a></li><li class="chapter-item expanded "><a href="ch10/ch10-02.html"><strong aria-hidden="true">10.2.</strong> 导入路径</a></li><li class="chapter-item expanded "><a href="ch10/ch10-03.html"><strong aria-hidden="true">10.3.</strong> 包声明</a></li><li class="chapter-item expanded "><a href="ch10/ch10-04.html"><strong aria-hidden="true">10.4.</strong> 导入声明</a></li><li class="chapter-item expanded "><a href="ch10/ch10-05.html"><strong aria-hidden="true">10.5.</strong> 包的匿名导入</a></li><li class="chapter-item expanded "><a href="ch10/ch10-06.html"><strong aria-hidden="true">10.6.</strong> 包和命名</a></li><li class="chapter-item expanded "><a href="ch10/ch10-07.html"><strong aria-hidden="true">10.7.</strong> 工具</a></li></ol></li><li class="chapter-item expanded "><a href="ch11/ch11.html"><strong aria-hidden="true">11.</strong> 测试</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="ch11/ch11-01.html"><strong aria-hidden="true">11.1.</strong> go test</a></li><li class="chapter-item expanded "><a href="ch11/ch11-02.html"><strong aria-hidden="true">11.2.</strong> 测试函数</a></li><li class="chapter-item expanded "><a href="ch11/ch11-03.html"><strong aria-hidden="true">11.3.</strong> 测试覆盖率</a></li><li class="chapter-item expanded "><a href="ch11/ch11-04.html"><strong aria-hidden="true">11.4.</strong> 基准测试</a></li><li class="chapter-item expanded "><a href="ch11/ch11-05.html"><strong aria-hidden="true">11.5.</strong> 剖析</a></li><li class="chapter-item expanded "><a href="ch11/ch11-06.html"><strong aria-hidden="true">11.6.</strong> 示例函数</a></li></ol></li><li class="chapter-item expanded "><a href="ch12/ch12.html"><strong aria-hidden="true">12.</strong> 反射</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="ch12/ch12-01.html"><strong aria-hidden="true">12.1.</strong> 为何需要反射?</a></li><li class="chapter-item expanded "><a href="ch12/ch12-02.html"><strong aria-hidden="true">12.2.</strong> reflect.Type和reflect.Value</a></li><li class="chapter-item expanded "><a href="ch12/ch12-03.html"><strong aria-hidden="true">12.3.</strong> Display递归打印</a></li><li class="chapter-item expanded "><a href="ch12/ch12-04.html"><strong aria-hidden="true">12.4.</strong> 示例: 编码S表达式</a></li><li class="chapter-item expanded "><a href="ch12/ch12-05.html"><strong aria-hidden="true">12.5.</strong> 通过reflect.Value修改值</a></li><li class="chapter-item expanded "><a href="ch12/ch12-06.html"><strong aria-hidden="true">12.6.</strong> 示例: 解码S表达式</a></li><li class="chapter-item expanded "><a href="ch12/ch12-08.html"><strong aria-hidden="true">12.7.</strong> 显示一个类型的方法集</a></li><li class="chapter-item expanded "><a href="ch12/ch12-09.html"><strong aria-hidden="true">12.8.</strong> 几点忠告</a></li></ol></li><li class="chapter-item expanded "><a href="ch13/ch13.html"><strong aria-hidden="true">13.</strong> 底层编程</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="ch13/ch13-01.html"><strong aria-hidden="true">13.1.</strong> unsafe.Sizeof, Alignof 和 Offsetof</a></li><li class="chapter-item expanded "><a href="ch13/ch13-02.html"><strong aria-hidden="true">13.2.</strong> unsafe.Pointer</a></li><li class="chapter-item expanded "><a href="ch13/ch13-03.html"><strong aria-hidden="true">13.3.</strong> 示例: 深度相等判断</a></li><li class="chapter-item expanded "><a href="ch13/ch13-04.html"><strong aria-hidden="true">13.4.</strong> 通过cgo调用C代码</a></li><li class="chapter-item expanded "><a href="ch13/ch13-05.html"><strong aria-hidden="true">13.5.</strong> 几点忠告</a></li></ol></li><li class="chapter-item expanded "><a href="appendix/appendix.html"><strong aria-hidden="true">14.</strong> 附录</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="appendix/appendix-a-errata.html"><strong aria-hidden="true">14.1.</strong> 附录A原文勘误</a></li><li class="chapter-item expanded "><a href="appendix/appendix-b-author.html"><strong aria-hidden="true">14.2.</strong> 附录B作者译者</a></li><li class="chapter-item expanded "><a href="appendix/appendix-c-cpoyright.html"><strong aria-hidden="true">14.3.</strong> 附录C译文授权</a></li><li class="chapter-item expanded "><a href="appendix/appendix-d-translations.html"><strong aria-hidden="true">14.4.</strong> 附录D其它语言</a></li></ol></li></ol> </div>
<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>
</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>
<h1 id="go语言圣经中文版"><a class="header" href="#go语言圣经中文版">Go语言圣经中文版</a></h1>
<p>Go语言圣经 <a href="http://gopl.io">《The Go Programming Language》</a> 中文版本仅供学习交流之用。对于希望学习CGO、Go汇编语言等高级用法的同学我们推荐<a href="https://github.com/chai2010/advanced-go-programming-book">《Go语言高级编程》</a>开源图书。如果希望深入学习Go语言语法树结构可以参考<a href="https://github.com/chai2010/go-ast-book">《Go语法树入门——开启自制编程语言和编译器之旅》</a>。如果想从头实现一个玩具Go语言可以参考<a href="https://github.com/chai2010/ugo-compiler-book">《从头实现µGo语言》</a></p>
<p><img src="cover.jpg" alt="" /></p>
<ul>
<li>在线阅读:<a href="https://gopl-zh.github.io">https://gopl-zh.github.io</a></li>
<li>在线阅读:<a href="https://golang-china.github.io/gopl-zh">https://golang-china.github.io/gopl-zh</a></li>
<li>项目主页:<a href="https://github.com/gopl-zh">https://github.com/gopl-zh</a></li>
<li>项目主页(旧)<a href="http://github.com/golang-china/gopl-zh">http://github.com/golang-china/gopl-zh</a></li>
<li>原版官网:<a href="http://gopl.io">http://gopl.io</a></li>
</ul>
<p>译者信息:</p>
<ul>
<li>译者柴树杉Github <a href="https://github.com/chai2010">@chai2010</a>Twitter <a href="https://twitter.com/chaishushan">@chaishushan</a></li>
<li>译者Xargin, <a href="https://github.com/cch123">https://github.com/cch123</a></li>
<li>译者CrazySssst, <a href="https://github.com/CrazySssst">https://github.com/CrazySssst</a></li>
<li>译者foreversmart, <a href="https://github.com/foreversmart">https://github.com/foreversmart</a> <a href="mailto:njutree@gmail.com">njutree@gmail.com</a></li>
</ul>
<p>Go 语言中国:</p>
<ul>
<li>Go 语言中国:<a href="https://github.com/golang-china">https://github.com/golang-china</a></li>
<li>Go 语言中国论坛:<a href="https://github.com/golang-china/main.go/discussions">https://github.com/golang-china/main.go/discussions</a></li>
</ul>
<div style="break-before: page; page-break-before: always;"></div><h1 id="前言"><a class="header" href="#前言">前言</a></h1>
<h2 id="go语言起源"><a class="header" href="#go语言起源">Go语言起源</a></h2>
<p>编程语言的演化跟生物物种的演化类似,一个成功的编程语言的后代一般都会继承它们祖先的优点;当然有时多种语言杂合也可能会产生令人惊讶的特性;还有一些激进的新特性可能并没有先例。通过观察这些影响,我们可以学到为什么一门语言是这样子的,它已经适应了怎样的环境。</p>
<p>下图展示了有哪些早期的编程语言对Go语言的设计产生了重要影响。</p>
<p><img src="../images/ch0-01.png" alt="" /></p>
<p>Go语言有时候被描述为“类C语言”或者是“21世纪的C语言”。Go从C语言继承了相似的表达式语法、控制流结构、基础数据类型、调用参数传值、指针等很多思想还有C语言一直所看中的编译后机器码的运行效率以及和现有操作系统的无缝适配。</p>
<p>但是在Go语言的家族树中还有其它的祖先。其中一个有影响力的分支来自<a href="https://en.wikipedia.org/wiki/Niklaus_Wirth">Niklaus Wirth</a>所设计的<code>Pascal</code>语言。然后<code>Modula-2</code>语言激发了包的概念。然后<code>Oberon</code>语言摒弃了模块接口文件和模块实现文件之间的区别。第二代的<code>Oberon-2</code>语言直接影响了包的导入和声明的语法,还有<code>Oberon</code>语言的面向对象特性所提供的方法的声明语法等。</p>
<p>Go语言的另一支祖先带来了Go语言区别其他语言的重要特性灵感来自于贝尔实验室的<a href="https://en.wikipedia.org/wiki/Tony_Hoare">Tony Hoare</a>于1978年发表的鲜为外界所知的关于并发研究的基础文献 <em>顺序通信进程</em> <em>communicating sequential processes</em> ,缩写为<code>CSP</code>。在<code>CSP</code>中,程序是一组中间没有共享状态的平行运行的处理过程,它们之间使用管道进行通信和控制同步。不过<a href="https://en.wikipedia.org/wiki/Tony_Hoare">Tony Hoare</a><code>CSP</code>只是一个用于描述并发性基本概念的描述语言,并不是一个可以编写可执行程序的通用编程语言。</p>
<p>接下来Rob Pike和其他人开始不断尝试将<a href="https://en.wikipedia.org/wiki/Communicating_sequential_processes">CSP</a>引入实际的编程语言中。他们第一次尝试引入<a href="https://en.wikipedia.org/wiki/Communicating_sequential_processes">CSP</a>特性的编程语言叫<a href="http://doc.cat-v.org/bell_labs/squeak/">Squeak</a>(老鼠间交流的语言),是一个提供鼠标和键盘事件处理的编程语言,它的管道是静态创建的。然后是改进版的<a href="http://doc.cat-v.org/bell_labs/squeak/">Newsqueak</a>语言,提供了类似<code>C</code>语言语句和表达式的语法和类似<code>Pascal</code>语言的推导语法。<code>Newsqueak</code>是一个带垃圾回收的纯函数式语言,它再次针对键盘、鼠标和窗口事件管理。但是在<code>Newsqueak</code>语言中管道是动态创建的,属于第一类值,可以保存到变量中。</p>
<p><code>Plan9</code>操作系统中,这些优秀的想法被吸收到了一个叫<code>Alef</code>的编程语言中。<code>Alef</code>试图将<code>Newsqueak</code>语言改造为系统编程语言,但是因为缺少垃圾回收机制而导致并发编程很痛苦。(译注:在<code>Alef</code>之后还有一个叫<code>Limbo</code>的编程语言Go语言从其中借鉴了很多特性。 具体请参考Pike的讲稿http://talks.golang.org/2012/concurrency.slide#9 </p>
<p>Go语言的其他的一些特性零散地来自于其他一些编程语言比如<code>iota</code>语法是从<code>APL</code>语言借鉴,词法作用域与嵌套函数来自于<code>Scheme</code>语言和其他很多语言。当然我们也可以从Go中发现很多创新的设计。比如Go语言的切片为动态数组提供了有效的随机存取的性能这可能会让人联想到链表的底层的共享机制。还有Go语言新发明的<code>defer</code>语句。</p>
<h2 id="go语言项目"><a class="header" href="#go语言项目">Go语言项目</a></h2>
<p>所有的编程语言都反映了语言设计者对编程哲学的反思通常包括之前的语言所暴露的一些不足地方的改进。Go项目是在Google公司维护超级复杂的几个软件系统遇到的一些问题的反思但是这类问题绝不是Google公司所特有的</p>
<p>正如<a href="http://genius.cat-v.org/rob-pike/">Rob Pike</a>所说,“软件的复杂性是乘法级相关的”,通过增加一个部分的复杂性来修复问题通常将慢慢地增加其他部分的复杂性。通过增加功能、选项和配置是修复问题的最快的途径,但是这很容易让人忘记简洁的内涵,即从长远来看,简洁依然是好软件的关键因素。</p>
<p>简洁的设计需要在工作开始的时候舍弃不必要的想法,并且在软件的生命周期内严格区别好的改变和坏的改变。通过足够的努力,一个好的改变可以在不破坏原有完整概念的前提下保持自适应,正如<a href="http://www.cs.unc.edu/%7Ebrooks/">Fred Brooks</a>所说的“概念完整性”;而一个坏的改变则不能达到这个效果,它们仅仅是通过肤浅的和简单的妥协来破坏原有设计的一致性。只有通过简洁的设计,才能让一个系统保持稳定、安全和持续的进化。</p>
<p>Go项目包括编程语言本身附带了相关的工具和标准库最后但并非代表不重要的是关于简洁编程哲学的宣言。就事后诸葛的角度来看Go语言的这些地方都做的还不错拥有自动垃圾回收、一个包系统、函数作为一等公民、词法作用域、系统调用接口、只读的UTF8字符串等。但是Go语言本身只有很少的特性也不太可能添加太多的特性。例如它没有隐式的数值转换没有构造函数和析构函数没有运算符重载没有默认参数也没有继承没有泛型没有异常没有宏没有函数修饰更没有线程局部存储。但是语言本身是成熟和稳定的而且承诺保证向后兼容用之前的Go语言编写程序可以用新版本的Go语言编译器和标准库直接构建而不需要修改代码。</p>
<p>Go语言有足够的类型系统以避免动态语言中那些粗心的类型错误但是Go语言的类型系统相比传统的强类型语言又要简洁很多。虽然有时候这会导致一个“无类型”的抽象类型概念但是Go语言程序员并不需要像<code>C++</code><code>Haskell</code>程序员那样纠结于具体类型的安全属性。在实践中Go语言简洁的类型系统给程序员带来了更多的安全性和更好的运行时性能。</p>
<p>Go语言鼓励当代计算机系统设计的原则特别是局部的重要性。它的内置数据类型和大多数的准库数据结构都经过精心设计而避免显式的初始化或隐式的构造函数因为很少的内存分配和内存初始化代码被隐藏在库代码中了。Go语言的聚合类型结构体和数组可以直接操作它们的元素只需要更少的存储空间、更少的内存写操作而且指针操作比其他间接操作的语言也更有效率。由于现代计算机是一个并行的机器Go语言提供了基于<code>CSP</code>的并发特性支持。Go语言的动态栈使得轻量级线程<code>goroutine</code>的初始栈可以很小,因此,创建一个<code>goroutine</code>的代价很小,创建百万级的<code>goroutine</code>完全是可行的。</p>
<p>Go语言的标准库通常被称为语言自带的电池提供了清晰的构建模块和公共接口包含I/O操作、文本处理、图像、密码学、网络和分布式应用程序等并支持许多标准化的文件格式和编解码协议。库和工具使用了大量的约定来减少额外的配置和解释从而最终简化程序的逻辑而且每个Go程序结构都是如此的相似因此Go程序也很容易学习。使用Go语言自带工具构建Go语言项目只需要使用文件名和标识符名称一个偶尔的特殊注释来确定所有的库、可执行文件、测试、基准测试、例子、以及特定于平台的变量、项目的文档等Go语言源代码本身就包含了构建规范。</p>
<h2 id="本书的组织"><a class="header" href="#本书的组织">本书的组织</a></h2>
<p>我们假设你已经有一种或多种其他编程语言的使用经历不管是类似C、C++或Java的编译型语言还是类似Python、Ruby、JavaScript的脚本语言因此我们不会像对完全的编程语言初学者那样解释所有的细节。因为Go语言的变量、常量、表达式、控制流和函数等基本语法也是类似的。</p>
<p>第一章包含了本教程的基本结构通过十几个程序介绍了用Go语言如何实现类似读写文件、文本格式化、创建图像、网络客户端和服务器通讯等日常工作。</p>
<p>第二章描述了Go语言程序的基本元素结构、变量、新类型定义、包和文件、以及作用域等概念。第三章讨论了数字、布尔值、字符串和常量并演示了如何显示和处理Unicode字符。第四章描述了复合类型从简单的数组、字典、切片到动态列表。第五章涵盖了函数并讨论了错误处理、panic和recover还有defer语句。</p>
<p>第一章到第五章是基础部分主流命令式编程语言这部分都类似。个别之处Go语言有自己特色的语法和风格但是大多数程序员能很快适应。其余章节是Go语言特有的方法、接口、并发、包、测试和反射等语言特性。</p>
<p>Go语言的面向对象机制与一般语言不同。它没有类层次结构甚至可以说没有类仅仅通过组合而不是继承简单的对象来构建复杂的对象。方法不仅可以定义在结构体上而且可以定义在任何用户自定义的类型上并且具体类型和抽象类型接口之间的关系是隐式的所以很多类型的设计者可能并不知道该类型到底实现了哪些接口。方法在第六章讨论接口在第七章讨论。</p>
<p>第八章讨论了基于顺序通信进程CSP概念的并发编程使用goroutines和channels处理并发编程。第九章则讨论了传统的基于共享变量的并发编程。</p>
<p>第十章描述了包机制和包的组织结构。这一章还展示了如何有效地利用Go自带的工具使用单个命令完成编译、测试、基准测试、代码格式化、文档以及其他诸多任务。</p>
<p>第十一章讨论了单元测试Go语言的工具和标准库中集成了轻量级的测试功能避免了强大但复杂的测试框架。测试库提供了一些基本构件必要时可以用来构建复杂的测试构件。</p>
<p>第十二章讨论了反射一种程序在运行期间审视自己的能力。反射是一个强大的编程工具不过要谨慎地使用这一章利用反射机制实现一些重要的Go语言库函数展示了反射的强大用法。第十三章解释了底层编程的细节在必要时可以使用unsafe包绕过Go语言安全的类型系统。</p>
<p>每一章都有一些练习题你可以用来测试你对Go的理解你也可以探讨书中这些例子的扩展和替代。</p>
<p>书中所有的代码都可以从 http://gopl.io 上的Git仓库下载。go get命令根据每个例子的导入路径智能地获取、构建并安装。只需要选择一个目录作为工作空间然后将GOPATH环境变量设置为该路径。</p>
<p>必要时Go语言工具会创建目录。例如</p>
<pre><code>$ export GOPATH=$HOME/gobook # 选择工作目录
$ go get gopl.io/ch1/helloworld # 获取/编译/安装
$ $GOPATH/bin/helloworld # 运行程序
Hello, 世界 # 这是中文
</code></pre>
<p>运行这些例子需要安装Go1.5以上的版本。</p>
<pre><code>$ go version
go version go1.5 linux/amd64
</code></pre>
<p>如果使用其他的操作系统,请参考 https://golang.org/doc/install 提供的说明安装。</p>
<h2 id="更多的信息"><a class="header" href="#更多的信息">更多的信息</a></h2>
<p>最佳的帮助信息来自Go语言的官方网站https://golang.org 它提供了完善的参考文档包括编程语言规范和标准库等诸多权威的帮助信息。同时也包含了如何编写更地道的Go程序的基本教程还有各种各样的在线文本资源和视频资源它们是本书最有价值的补充。Go语言的官方博客 https://blog.golang.org 会不定期发布一些Go语言最好的实践文章包括当前语言的发展状态、未来的计划、会议报告和Go语言相关的各种会议的主题等信息译注 http://talks.golang.org/ 包含了官方收录的各种报告的讲稿)。</p>
<p>在线访问的一个有价值的地方是可以从web页面运行Go语言的程序而纸质书则没有这么便利了。这个功能由来自 https://play.golang.org 的 Go Playground 提供,并且可以方便地嵌入到其他页面中,例如 https://golang.org 的主页,或 godoc 提供的文档页面中。</p>
<p>Playground可以简单的通过执行一个小程序来测试对语法、语义和对程序库的理解类似其他很多语言提供的REPL即时运行的工具。同时它可以生成对应的url非常适合共享Go语言代码片段汇报bug或提供反馈意见等。</p>
<p>基于 Playground 构建的 Go Tourhttps://tour.golang.org 是一个系列的Go语言入门教程它包含了诸多基本概念和结构相关的并可在线运行的互动小程序。</p>
<p>当然Playground 和 Tour 也有一些限制它们只能导入标准库而且因为安全的原因对一些网络库做了限制。如果要在编译和运行时需要访问互联网对于一些更复杂的实验你可能需要在自己的电脑上构建并运行程序。幸运的是下载Go语言的过程很简单从 https://golang.org 下载安装包应该不超过几分钟译注感谢伟大的长城让大陆的Gopher们都学会了自己打洞的基本生活技能下载时间可能会因为洞的大小等因素从几分钟到几天或更久然后就可以在自己电脑上编写和运行Go程序了。</p>
<p>Go语言是一个开源项目你可以在 https://golang.org/pkg 阅读标准库中任意函数和类型的实现代码,和下载安装包的代码完全一致。这样,你可以知道很多函数是如何工作的, 通过挖掘找出一些答案的细节或者仅仅是出于欣赏专业级Go代码。</p>
<h2 id="致谢"><a class="header" href="#致谢">致谢</a></h2>
<p><a href="http://genius.cat-v.org/rob-pike/">Rob Pike</a><a href="http://research.swtch.com/">Russ Cox</a>以及很多其他Go团队的核心成员多次仔细阅读了本书的手稿他们对本书的组织结构和表述用词等给出了很多宝贵的建议。在准备日文版翻译的时候Yoshiki Shibata更是仔细地审阅了本书的每个部分及时发现了诸多英文和代码的错误。我们非常感谢本书的每一位审阅者并感谢对本书给出了重要的建议的Brian Goetz、Corey Kosak、Arnold Robbins、Josh Bleecher Snyder和Peter Weinberger等人。</p>
<p>我们还感谢Sameer Ajmani、Ittai Balaban、David Crawshaw、Billy Donohue、Jonathan Feinberg、Andrew Gerrand、Robert Griesemer、John Linderman、Minux Ma译注中国人Go团队成员。、Bryan Mills、Bala Natarajan、Cosmos Nicolaou、Paul Staniforth、Nigel Tao译注好像是陶哲轩的兄弟以及Howard Trickey给出的许多有价值的建议。我们还要感谢David Brailsford和Raph Levien关于类型设置的建议。</p>
<p>我们从来自Addison-Wesley的编辑Greg Doench收到了很多帮助从最开始就得到了越来越多的帮助。来自AW生产团队的John Fuller、Dayna Isley、Julie Nahil、Chuti Prasertsith到Barbara Wood感谢你们的热心帮助。</p>
<p><a href="https://github.com/adonovan">Alan Donovan</a>特别感谢Sameer Ajmani、Chris Demetriou、Walt Drummond和Google公司的Reid Tatge允许他有充裕的时间去写本书感谢Stephen Donovan的建议和始终如一的鼓励以及他的妻子Leila Kazemi并没有让他为了家庭琐事而分心并热情坚定地支持这个项目。</p>
<p><a href="http://www.cs.princeton.edu/%7Ebwk/">Brian Kernighan</a>特别感谢朋友和同事对他的耐心和宽容让他慢慢地梳理本书的写作思路。同时感谢他的妻子Meg和其他很多朋友对他写作事业的支持。</p>
<p>2015年 10月 于 纽约</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="第1章-入门"><a class="header" href="#第1章-入门">第1章 入门</a></h1>
<p>本章介绍Go语言的基础组件。本章提供了足够的信息和示例程序希望可以帮你尽快入门写出有用的程序。本章和之后章节的示例程序都针对你可能遇到的现实案例。先了解几个Go程序涉及的主题从简单的文件处理、图像处理到互联网客户端和服务端并发。当然第一章不会解释细枝末节但用这些程序来学习一门新语言还是很有效的。</p>
<p>学习一门新语言时会有一种自然的倾向按照自己熟悉的语言的套路写新语言程序。学习Go语言的过程中请警惕这种想法尽量别这么做。我们会演示怎么写好Go语言程序所以请使用本书的代码作为你自己写程序时的指南。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="11-hello-world"><a class="header" href="#11-hello-world">1.1. Hello, World</a></h2>
<p>我们以现已成为传统的“hello world”案例来开始吧这个例子首次出现于 1978 年出版的 C 语言圣经 <a href="http://s3-us-west-2.amazonaws.com/belllabs-microsite-dritchie/cbook/index.html">《The C Programming Language》</a>(译注:本书作者之一 Brian W. Kernighan 也是《The C Programming Language》一书的作者。C 语言是直接影响 Go 语言设计的语言之一。这个例子体现了 Go 语言一些核心理念。</p>
<p><u><i>gopl.io/ch1/helloworld</i></u></p>
<pre><code class="language-go">package main
import &quot;fmt&quot;
func main() {
fmt.Println(&quot;Hello, 世界&quot;)
}
</code></pre>
<p>Go 是一门编译型语言Go 语言的工具链将源代码及其依赖转换成计算机的机器指令译注静态编译。Go 语言提供的工具都通过一个单独的命令 <code>go</code> 调用,<code>go</code> 命令有一系列子命令。最简单的一个子命令就是 <code>run</code>。这个命令编译一个或多个以。<code>.go</code> 结尾的源文件,链接库文件,并运行最终生成的可执行文件。(本书使用$表示命令行提示符。)</p>
<pre><code class="language-bash">$ go run helloworld.go
</code></pre>
<p>毫无意外,这个命令会输出:</p>
<pre><code class="language-text">Hello, 世界
</code></pre>
<p>Go 语言原生支持 Unicode它可以处理全世界任何语言的文本。</p>
<p>如果不只是一次性实验,你肯定希望能够编译这个程序,保存编译结果以备将来之用。可以用 <code>build</code> 子命令:</p>
<pre><code class="language-shell">$ go build helloworld.go
</code></pre>
<p>这个命令生成一个名为 <code>helloworld</code> 的可执行的二进制文件译注Windows 系统下生成的可执行文件是 <code>helloworld.exe</code>,增加了 <code>.exe</code> 后缀名),之后你可以随时运行它(译注:在 Windows 系统下在命令行直接输入 <code>helloworld.exe</code> 命令运行),不需任何处理(译注:因为静态编译,所以不用担心在系统库更新的时候冲突,幸福感满满)。</p>
<pre><code>$ ./helloworld
Hello, 世界
</code></pre>
<p>本书中所有示例代码上都有一行标记,利用这些标记可以从 <a href="http://gopl.io">gopl.io</a> 网站上本书源码仓库里获取代码:</p>
<pre><code class="language-text">gopl.io/ch1/helloworld
</code></pre>
<p>执行 <code>go get gopl.io/ch1/helloworld</code> 命令,就会从网上获取代码,并放到对应目录中(需要先安装 Git 或 Hg 之类的版本管理工具,并将对应的命令添加到 <code>PATH</code> 环境变量中。序言已经提及,需要先设置好 <code>GOPATH</code> 环境变量,下载的代码会放在 <code>$GOPATH/src/gopl.io/ch1/helloworld</code> 目录)。<a href="ch1/../ch2/ch2-06.html">2.6</a><a href="ch1/../ch10/ch10-07.html">10.7 节</a>有这方面更详细的介绍。</p>
<p>来讨论下程序本身。Go 语言的代码通过<strong></strong>package组织包类似于其它语言里的库libraries或者模块modules。一个包由位于单个目录下的一个或多个 <code>.go</code> 源代码文件组成,目录定义包的作用。每个源文件都以一条 <code>package</code> 声明语句开始,这个例子里就是 <code>package main</code>表示该文件属于哪个包紧跟着一系列导入import的包之后是存储在这个文件里的程序语句。</p>
<p>Go 的标准库提供了 100 多个包,以支持常见功能,如输入、输出、排序以及文本处理。比如 <code>fmt</code> 包,就含有格式化输出、接收输入的函数。<code>Println</code> 是其中一个基础函数,可以打印以空格间隔的一个或多个值,并在最后添加一个换行符,从而输出一整行。</p>
<p><code>main</code> 包比较特殊。它定义了一个独立可执行的程序,而不是一个库。在 <code>main</code> 里的 <code>main</code> <em>函数</em>也很特殊它是整个程序执行时的入口译注C 系语言差不多都这样)。<code>main</code> 函数所做的事情就是程序做的。当然了,<code>main</code> 函数一般调用其它包里的函数完成很多工作(如:<code>fmt.Println</code>)。</p>
<p>必须告诉编译器源文件需要哪些包,这就是跟随在 <code>package</code> 声明后面的 <code>import</code> 声明扮演的角色。<code>hello world</code> 例子只用到了一个包,大多数程序需要导入多个包。</p>
<p>必须恰当导入需要的包缺少了必要的包或者导入了不需要的包程序都无法编译通过。这项严格要求避免了程序开发过程中引入未使用的包译注Go 语言编译过程没有警告信息,争议特性之一)。</p>
<p><code>import</code> 声明必须跟在文件的 <code>package</code> 声明之后。随后,则是组成程序的函数、变量、常量、类型的声明语句(分别由关键字 <code>func</code><code>var</code><code>const</code><code>type</code> 定义)。这些内容的声明顺序并不重要(译注:最好还是定一下规范)。这个例子的程序已经尽可能短了,只声明了一个函数,其中只调用了一个其他函数。为了节省篇幅,有些时候示例程序会省略 <code>package</code><code>import</code> 声明,但是,这些声明在源代码里有,并且必须得有才能编译。</p>
<p>一个函数的声明由 <code>func</code> 关键字、函数名、参数列表、返回值列表(这个例子里的 <code>main</code> 函数参数列表和返回值都是空的)以及包含在大括号里的函数体组成。第五章进一步考察函数。</p>
<p>Go 语言不需要在语句或者声明的末尾添加分号,除非一行上有多条语句。实际上,编译器会主动把特定符号后的换行符转换为分号,因此换行符添加的位置会影响 Go 代码的正确解析(译注:比如行末是标识符、整数、浮点数、虚数、字符或字符串文字、关键字 <code>break</code><code>continue</code><code>fallthrough</code><code>return</code> 中的一个、运算符和分隔符 <code>++</code><code>--</code><code>)</code><code>]</code><code>}</code> 中的一个)。举个例子,函数的左括号 <code>{</code> 必须和 <code>func</code> 函数声明在同一行上,且位于末尾,不能独占一行,而在表达式 <code>x+y</code> 中,可在 <code>+</code> 后换行,不能在 <code>+</code> 前换行(译注:以+结尾的话不会被插入分号分隔符,但是以 x 结尾的话则会被分号分隔符,从而导致编译错误)。</p>
<p>Go 语言在代码格式上采取了很强硬的态度。<code>gofmt</code>工具把代码格式化为标准格式译注这个格式化工具没有任何可以调整代码格式的参数Go 语言就是这么任性),并且 <code>go</code> 工具中的 <code>fmt</code> 子命令会对指定包否则默认为当前目录中所有。go 源文件应用 <code>gofmt</code> 命令。本书中的所有代码都被 gofmt 过。你也应该养成格式化自己的代码的习惯。以法令方式规定标准的代码格式可以避免无尽的无意义的琐碎争执(译注:也导致了 Go 语言的 TIOBE 排名较低,因为缺少撕逼的话题)。更重要的是,这样可以做多种自动源码转换,如果放任 Go 语言代码格式,这些转换就不大可能了。</p>
<p>很多文本编辑器都可以配置为保存文件时自动执行 <code>gofmt</code>,这样你的源代码总会被恰当地格式化。还有个相关的工具:<code>goimports</code>,可以根据代码需要,自动地添加或删除 <code>import</code> 声明。这个工具并没有包含在标准的分发包中,可以用下面的命令安装:</p>
<pre><code class="language-shell">$ go get golang.org/x/tools/cmd/goimports
</code></pre>
<p>对于大多数用户来说,下载、编译包、运行测试用例、察看 Go 语言的文档等等常用功能都可以用 go 的工具完成。<a href="ch1/../ch10/ch10-07.html">10.7 节</a>详细介绍这些知识。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="12-命令行参数"><a class="header" href="#12-命令行参数">1.2. 命令行参数</a></h2>
<p>大多数的程序都是处理输入,产生输出;这也正是“计算”的定义。但是,程序如何获取要处理的输入数据呢?一些程序生成自己的数据,但通常情况下,输入来自于程序外部:文件、网络连接、其它程序的输出、敲键盘的用户、命令行参数或其它类似输入源。下面几个例子会讨论其中几个输入源,首先是命令行参数。</p>
<p><code>os</code> 包以跨平台的方式,提供了一些与操作系统交互的函数和变量。程序的命令行参数可从 <code>os</code> 包的 <code>Args</code> 变量获取;<code>os</code> 包外部使用 <code>os.Args</code> 访问该变量。</p>
<p><code>os.Args</code> 变量是一个字符串string<em>切片</em>slice译注slice 和 Python 语言中的切片类似,是一个简版的动态数组),切片是 Go 语言的基础概念,稍后详细介绍。现在先把切片 <code>s</code> 当作数组元素序列,序列的长度动态变化,用 <code>s[i]</code> 访问单个元素,用 <code>s[m:n]</code> 获取子序列(译注:和 Python 里的语法差不多)。序列的元素数目为 <code>len(s)</code>。和大多数编程语言类似区间索引时Go 语言里也采用左闭右开形式,即,区间包括第一个索引元素,不包括最后一个,因为这样可以简化逻辑。(译注:比如 <code>a=[1,2,3,4,5]</code>, <code>a[0:3]=[1,2,3]</code>,不包含最后一个元素)。比如 <code>s[m:n]</code> 这个切片,<code>0≤m≤n≤len(s)</code>,包含 <code>n-m</code> 个元素。</p>
<p><code>os.Args</code> 的第一个元素:<code>os.Args[0]</code>,是命令本身的名字;其它的元素则是程序启动时传给它的参数。<code>s[m:n]</code> 形式的切片表达式,产生从第 <code>m</code> 个元素到第 <code>n-1</code> 个元素的切片,下个例子用到的元素包含在 <code>os.Args[1:len(os.Args)]</code> 切片中。如果省略切片表达式的 <code>m</code><code>n</code>,会默认传入 <code>0</code><code>len(s)</code>,因此前面的切片可以简写成 <code>os.Args[1:]</code></p>
<p>下面是 Unix 里 <code>echo</code> 命令的一份实现,<code>echo</code> 把它的命令行参数打印成一行。程序导入了两个包,用括号把它们括起来写成列表形式,而没有分开写成独立的 <code>import</code> 声明。两种形式都合法,列表形式习惯上用得多。包导入顺序并不重要;<code>gofmt</code> 工具格式化时按照字母顺序对包名排序。(示例有多个版本时,我们会对示例编号,这样可以明确当前正在讨论的是哪个。)</p>
<p><u><i>gopl.io/ch1/echo1</i></u></p>
<pre><code class="language-go">// Echo1 prints its command-line arguments.
package main
import (
&quot;fmt&quot;
&quot;os&quot;
)
func main() {
var s, sep string
for i := 1; i &lt; len(os.Args); i++ {
s += sep + os.Args[i]
sep = &quot; &quot;
}
fmt.Println(s)
}
</code></pre>
<p>注释语句以 <code>//</code> 开头。对于程序员来说,<code>//</code> 之后到行末之间所有的内容都是注释,被编译器忽略。按照惯例,我们在每个包的包声明前添加注释;对于 <code>main package</code>,注释包含一句或几句话,从整体角度对程序做个描述。</p>
<p><code>var</code> 声明定义了两个 <code>string</code> 类型的变量 <code>s</code><code>sep</code>。变量会在声明时直接初始化。如果变量没有显式初始化,则被隐式地赋予其类型的 <em>零值</em>zero value数值类型是 <code>0</code>,字符串类型是空字符串 <code>&quot;&quot;</code>。这个例子里,声明把 <code>s</code><code>sep</code> 隐式地初始化成空字符串。第 2 章再来详细地讲解变量和声明。</p>
<p>对数值类型Go 语言提供了常规的数值和逻辑运算符。而对 <code>string</code> 类型,<code>+</code> 运算符连接字符串(译注:和 C++ 或者 JavaScript 是一样的)。所以表达式:<code>sep + os.Args[i]</code> 表示连接字符串 <code>sep</code><code>os.Args</code>。程序中使用的语句:<code>s+=sep+os.Args[i]</code> 是一条 <em>赋值语句</em>,将 <code>s</code> 的旧值跟 <code>sep</code><code>os.Args[i]</code> 连接后赋值回 <code>s</code>,等价于:<code>s=s+sep+os.Args[i]</code></p>
<p>运算符 <code>+=</code> 是赋值运算符assignment operator每种数值运算符或逻辑运算符<code>+</code><code>*</code>,都有对应的赋值运算符。</p>
<p><code>echo</code> 程序可以每循环一次输出一个参数,这个版本却是不断地把新文本追加到末尾来构造字符串。字符串 <code>s</code> 开始为空,即值为 <code>&quot;&quot;</code>每次循环会添加一些文本第一次迭代之后还会再插入一个空格因此循环结束时每个参数中间都有一个空格。这是一种二次加工quadratic process当参数数量庞大时开销很大但是对于 <code>echo</code>,这种情形不大可能出现。本章会介绍 <code>echo</code> 的若干改进版,下一章解决低效问题。</p>
<p>循环索引变量 <code>i</code><code>for</code> 循环的第一部分中定义。符号 <code>:=</code><em>短变量声明</em>short variable declaration的一部分这是定义一个或多个变量并根据它们的初始值为这些变量赋予适当类型的语句。下一章有这方面更多说明。</p>
<p>自增语句 <code>i++</code><code>i</code><code>1</code>;这和 <code>i+=1</code> 以及 <code>i=i+1</code> 都是等价的。对应的还有 <code>i--</code><code>i</code><code>1</code>。它们是语句,而不像 C 系的其它语言那样是表达式。所以 <code>j=i++</code> 非法,而且 <code>++</code><code>--</code> 都只能放在变量名后面,因此 <code>--i</code> 也非法。</p>
<p>Go 语言只有 <code>for</code> 循环这一种循环语句。<code>for</code> 循环有多种形式,其中一种如下所示:</p>
<pre><code class="language-go">for initialization; condition; post {
// zero or more statements
}
</code></pre>
<p><code>for</code> 循环三个部分不需括号包围。大括号强制要求,左大括号必须和 <em><code>post</code></em> 语句在同一行。</p>
<p><em><code>initialization</code></em> 语句是可选的,在循环开始前执行。<em><code>initalization</code></em> 如果存在,必须是一条 <em>简单语句</em>simple statement短变量声明、自增语句、赋值语句或函数调用。<code>condition</code> 是一个布尔表达式boolean expression其值在每次循环迭代开始时计算。如果为 <code>true</code> 则执行循环体语句。<code>post</code> 语句在循环体执行结束后执行,之后再次对 <code>condition</code> 求值。<code>condition</code> 值为 <code>false</code> 时,循环结束。</p>
<p>for 循环的这三个部分每个都可以省略,如果省略 <code>initialization</code><code>post</code>,分号也可以省略:</p>
<pre><code class="language-go">// a traditional &quot;while&quot; loop
for condition {
// ...
}
</code></pre>
<p>如果连 <code>condition</code> 也省略了,像下面这样:</p>
<pre><code class="language-go">// a traditional infinite loop
for {
// ...
}
</code></pre>
<p>这就变成一个无限循环,尽管如此,还可以用其他方式终止循环,如一条 <code>break</code><code>return</code> 语句。</p>
<p><code>for</code> 循环的另一种形式在某种数据类型的区间range上遍历如字符串或切片。<code>echo</code> 的第二版本展示了这种形式:</p>
<p><u><i>gopl.io/ch1/echo2</i></u></p>
<pre><code class="language-go">// Echo2 prints its command-line arguments.
package main
import (
&quot;fmt&quot;
&quot;os&quot;
)
func main() {
s, sep := &quot;&quot;, &quot;&quot;
for _, arg := range os.Args[1:] {
s += sep + arg
sep = &quot; &quot;
}
fmt.Println(s)
}
</code></pre>
<p>每次循环迭代,<code>range</code> 产生一对值;索引以及在该索引处的元素值。这个例子不需要索引,但 <code>range</code> 的语法要求,要处理元素,必须处理索引。一种思路是把索引赋值给一个临时变量(如 <code>temp</code>)然后忽略它的值,但 Go 语言不允许使用无用的局部变量local variables因为这会导致编译错误。</p>
<p>Go 语言中这种情况的解决方法是用 <em>空标识符</em>blank identifier<code>_</code>(也就是下划线)。空标识符可用于在任何语法需要变量名但程序逻辑不需要的时候(如:在循环里)丢弃不需要的循环索引,并保留元素值。大多数的 Go 程序员都会像上面这样使用 <code>range</code><code>_</code><code>echo</code> 程序,因为隐式地而非显式地索引 <code>os.Args</code>,容易写对。</p>
<p><code>echo</code> 的这个版本使用一条短变量声明来声明并初始化 <code>s</code><code>seps</code>,也可以将这两个变量分开声明,声明一个变量有好几种方式,下面这些都等价:</p>
<pre><code class="language-go">s := &quot;&quot;
var s string
var s = &quot;&quot;
var s string = &quot;&quot;
</code></pre>
<p>用哪种不用哪种,为什么呢?第一种形式,是一条短变量声明,最简洁,但只能用在函数内部,而不能用于包变量。第二种形式依赖于字符串的默认初始化零值机制,被初始化为 <code>&quot;&quot;</code>。第三种形式用得很少,除非同时声明多个变量。第四种形式显式地标明变量的类型,当变量类型与初值类型相同时,类型冗余,但如果两者类型不同,变量类型就必须了。实践中一般使用前两种形式中的某个,初始值重要的话就显式地指定变量的类型,否则使用隐式初始化。</p>
<p>如前文所述,每次循环迭代字符串 <code>s</code> 的内容都会更新。<code>+=</code> 连接原字符串、空格和下个参数,产生新字符串,并把它赋值给 <code>s</code><code>s</code> 原来的内容已经不再使用,将在适当时机对它进行垃圾回收。</p>
<p>如果连接涉及的数据量很大,这种方式代价高昂。一种简单且高效的解决方案是使用 <code>strings</code> 包的 <code>Join</code> 函数:</p>
<p><u><i>gopl.io/ch1/echo3</i></u></p>
<pre><code class="language-go">func main() {
fmt.Println(strings.Join(os.Args[1:], &quot; &quot;))
}
</code></pre>
<p>最后,如果不关心输出格式,只想看看输出值,或许只是为了调试,可以用 <code>Println</code> 为我们格式化输出。</p>
<pre><code class="language-go">fmt.Println(os.Args[1:])
</code></pre>
<p>这条语句的输出结果跟 <code>strings.Join</code> 得到的结果很像,只是被放到了一对方括号里。切片都会被打印成这种格式。</p>
<hr />
<p><strong>练习 1.1</strong> 修改 <code>echo</code> 程序,使其能够打印 <code>os.Args[0]</code>,即被执行命令本身的名字。</p>
<p><strong>练习 1.2</strong> 修改 <code>echo</code> 程序,使其打印每个参数的索引和值,每个一行。</p>
<p><strong>练习 1.3</strong> 做实验测量潜在低效的版本和使用了 <code>strings.Join</code> 的版本的运行时间差异。(<a href="ch1/./ch1-06.html">1.6 节</a>讲解了部分 <code>time</code> 包,<a href="ch1/../ch11/ch11-04.html">11.4 节</a>展示了如何写标准测试程序,以得到系统性的性能评测。)</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="13-查找重复的行"><a class="header" href="#13-查找重复的行">1.3. 查找重复的行</a></h2>
<p>对文件做拷贝、打印、搜索、排序、统计或类似事情的程序都有一个差不多的程序结构:一个处理输入的循环,在每个元素上执行计算处理,在处理的同时或最后产生输出。我们会展示一个名为 <code>dup</code> 的程序的三个版本;灵感来自于 Unix 的 <code>uniq</code> 命令,其寻找相邻的重复行。该程序使用的结构和包是个参考范例,可以方便地修改。</p>
<p><code>dup</code> 的第一个版本打印标准输入中多次出现的行,以重复次数开头。该程序将引入 <code>if</code> 语句,<code>map</code> 数据类型以及 <code>bufio</code> 包。</p>
<p><u><i>gopl.io/ch1/dup1</i></u></p>
<pre><code class="language-go">// Dup1 prints the text of each line that appears more than
// once in the standard input, preceded by its count.
package main
import (
&quot;bufio&quot;
&quot;fmt&quot;
&quot;os&quot;
)
func main() {
counts := make(map[string]int)
input := bufio.NewScanner(os.Stdin)
for input.Scan() {
counts[input.Text()]++
}
// NOTE: ignoring potential errors from input.Err()
for line, n := range counts {
if n &gt; 1 {
fmt.Printf(&quot;%d\t%s\n&quot;, n, line)
}
}
}
</code></pre>
<p>正如 <code>for</code> 循环一样,<code>if</code> 语句条件两边也不加括号,但是主体部分需要加。<code>if</code> 语句的 <code>else</code> 部分是可选的,在 <code>if</code> 的条件为 <code>false</code> 时执行。</p>
<p><strong>map</strong> 存储了键/值key/value的集合对集合元素提供常数时间的存、取或测试操作。键可以是任意类型只要其值能用 <code>==</code> 运算符比较,最常见的例子是字符串;值则可以是任意类型。这个例子中的键是字符串,值是整数。内置函数 <code>make</code> 创建空 <code>map</code>此外它还有别的作用。4.3 节讨论 <code>map</code></p>
<p>(译注:从功能和实现上说,<code>Go</code><code>map</code> 类似于 <code>Java</code> 语言中的 <code>HashMap</code>Python 语言中的 <code>dict</code><code>Lua</code> 语言中的 <code>table</code>,通常使用 <code>hash</code> 实现。遗憾的是,对于该词的翻译并不统一,数学界术语为<em>映射</em>,而计算机界众说纷纭莫衷一是。为了防止对读者造成误解,保留不译。)</p>
<p>每次 <code>dup</code> 读取一行输入,该行被当做键存入 <code>map</code>,其对应的值递增。<code>counts[input.Text()]++</code> 语句等价下面两句:</p>
<pre><code class="language-go">line := input.Text()
counts[line] = counts[line] + 1
</code></pre>
<p><code>map</code> 中不含某个键时不用担心,首次读到新行时,等号右边的表达式 <code>counts[line]</code> 的值将被计算为其类型的零值,对于 <code>int</code><code>0</code></p>
<p>为了打印结果,我们使用了基于 <code>range</code> 的循环,并在 <code>counts</code> 这个 <code>map</code> 上迭代。跟之前类似,每次迭代得到两个结果,键和其在 <code>map</code> 中对应的值。<code>map</code> 的迭代顺序并不确定,从实践来看,该顺序随机,每次运行都会变化。这种设计是有意为之的,因为能防止程序依赖特定遍历顺序,而这是无法保证的。(译注:具体可以参见这里<a href="https://stackoverflow.com/questions/11853396/google-go-lang-assignment-order">https://stackoverflow.com/questions/11853396/google-go-lang-assignment-order</a></p>
<p>继续来看 <code>bufio</code> 包,它使处理输入和输出方便又高效。<code>Scanner</code> 类型是该包最有用的特性之一,它读取输入并将其拆成行或单词;通常是处理行形式的输入最简单的方法。</p>
<p>程序使用短变量声明创建 <code>bufio.Scanner</code> 类型的变量 <code>input</code></p>
<pre><code class="language-go">input := bufio.NewScanner(os.Stdin)
</code></pre>
<p>该变量从程序的标准输入中读取内容。每次调用 <code>input.Scan()</code>,即读入下一行,并移除行末的换行符;读取的内容可以调用 <code>input.Text()</code> 得到。<code>Scan</code> 函数在读到一行时返回 <code>true</code>,不再有输入时返回 <code>false</code></p>
<p>类似于 C 或其它语言里的 <code>printf</code> 函数,<code>fmt.Printf</code> 函数对一些表达式产生格式化输出。该函数的首个参数是个格式字符串指定后续参数被如何格式化。各个参数的格式取决于“转换字符”conversion character形式为百分号后跟一个字母。举个例子<code>%d</code> 表示以十进制形式打印一个整型操作数,而 <code>%s</code> 则表示把字符串型操作数的值展开。</p>
<p><code>Printf</code> 有一大堆这种转换Go程序员称之为<em>动词verb</em>。下面的表格虽然远不是完整的规范,但展示了可用的很多特性:</p>
<pre><code class="language-text">%d 十进制整数
%x, %o, %b 十六进制,八进制,二进制整数。
%f, %g, %e 浮点数: 3.141593 3.141592653589793 3.141593e+00
%t 布尔true或false
%c 字符rune (Unicode码点)
%s 字符串
%q 带双引号的字符串&quot;abc&quot;或带单引号的字符'c'
%v 变量的自然形式natural format
%T 变量的类型
%% 字面上的百分号标志(无操作数)
</code></pre>
<p><code>dup1</code> 的格式字符串中还含有制表符<code>\t</code>和换行符<code>\n</code>。字符串字面上可能含有这些代表不可见字符的<strong>转义字符escape sequences</strong>。默认情况下,<code>Printf</code> 不会换行。按照惯例,以字母 <code>f</code> 结尾的格式化函数,如 <code>log.Printf</code><code>fmt.Errorf</code>,都采用 <code>fmt.Printf</code> 的格式化准则。而以 <code>ln</code> 结尾的格式化函数,则遵循 <code>Println</code> 的方式,以跟 <code>%v</code> 差不多的方式格式化参数,并在最后添加一个换行符。(译注:后缀 <code>f</code><code>format</code><code>ln</code><code>line</code>。)</p>
<p>很多程序要么从标准输入中读取数据,如上面的例子所示,要么从一系列具名文件中读取数据。<code>dup</code> 程序的下个版本读取标准输入或是使用 <code>os.Open</code> 打开各个具名文件,并操作它们。</p>
<p><u><i>gopl.io/ch1/dup2</i></u></p>
<pre><code class="language-go">// Dup2 prints the count and text of lines that appear more than once
// in the input. It reads from stdin or from a list of named files.
package main
import (
&quot;bufio&quot;
&quot;fmt&quot;
&quot;os&quot;
)
func main() {
counts := make(map[string]int)
files := os.Args[1:]
if len(files) == 0 {
countLines(os.Stdin, counts)
} else {
for _, arg := range files {
f, err := os.Open(arg)
if err != nil {
fmt.Fprintf(os.Stderr, &quot;dup2: %v\n&quot;, err)
continue
}
countLines(f, counts)
f.Close()
}
}
for line, n := range counts {
if n &gt; 1 {
fmt.Printf(&quot;%d\t%s\n&quot;, n, line)
}
}
}
func countLines(f *os.File, counts map[string]int) {
input := bufio.NewScanner(f)
for input.Scan() {
counts[input.Text()]++
}
// NOTE: ignoring potential errors from input.Err()
}
</code></pre>
<p><code>os.Open</code> 函数返回两个值。第一个值是被打开的文件(<code>*os.File</code>),其后被 <code>Scanner</code> 读取。</p>
<p><code>os.Open</code> 返回的第二个值是内置 <code>error</code> 类型的值。如果 <code>err</code> 等于内置值<code>nil</code>(译注:相当于其它语言里的 <code>NULL</code>),那么文件被成功打开。读取文件,直到文件结束,然后调用 <code>Close</code> 关闭该文件,并释放占用的所有资源。相反的话,如果 <code>err</code> 的值不是 <code>nil</code>,说明打开文件时出错了。这种情况下,错误值描述了所遇到的问题。我们的错误处理非常简单,只是使用 <code>Fprintf</code> 与表示任意类型默认格式值的动词 <code>%v</code>,向标准错误流打印一条信息,然后 <code>dup</code> 继续处理下一个文件;<code>continue</code> 语句直接跳到 <code>for</code> 循环的下个迭代开始执行。</p>
<p>为了使示例代码保持合理的大小,本书开始的一些示例有意简化了错误处理,显而易见的是,应该检查 <code>os.Open</code> 返回的错误值,然而,使用 <code>input.Scan</code> 读取文件过程中不大可能出现错误因此我们忽略了错误处理。我们会在跳过错误检查的地方做说明。5.4 节中深入介绍错误处理。</p>
<p>注意 <code>countLines</code> 函数在其声明前被调用。函数和包级别的变量package-level entities可以任意顺序声明并不影响其被调用。译注最好还是遵循一定的规范</p>
<p><code>map</code> 是一个由 <code>make</code> 函数创建的数据结构的引用。<code>map</code> 作为参数传递给某函数时该函数接收这个引用的一份拷贝copy或译为副本被调用函数对 <code>map</code> 底层数据结构的任何修改,调用者函数都可以通过持有的 <code>map</code> 引用看到。在我们的例子中,<code>countLines</code> 函数向 <code>counts</code> 插入的值,也会被 <code>main</code> 函数看到。(译注:类似于 C++ 里的引用传递,实际上指针是另一个指针了,但内部存的值指向同一块内存)</p>
<p><code>dup</code> 的前两个版本以&quot;流”模式读取输入,并根据需要拆分成多个行。理论上,这些程序可以处理任意数量的输入数据。还有另一个方法,就是一口气把全部输入数据读到内存中,一次分割为多行,然后处理它们。下面这个版本,<code>dup3</code>,就是这么操作的。这个例子引入了 <code>ReadFile</code> 函数(来自于<code>io/ioutil</code>包),其读取指定文件的全部内容,<code>strings.Split</code> 函数把字符串分割成子串的切片。(<code>Split</code> 的作用与前文提到的 <code>strings.Join</code> 相反。)</p>
<p>我们略微简化了 <code>dup3</code>。首先,由于 <code>ReadFile</code> 函数需要文件名作为参数,因此只读指定文件,不读标准输入。其次,由于行计数代码只在一处用到,故将其移回 <code>main</code> 函数。</p>
<p><u><i>gopl.io/ch1/dup3</i></u></p>
<pre><code class="language-go">package main
import (
&quot;fmt&quot;
&quot;io/ioutil&quot;
&quot;os&quot;
&quot;strings&quot;
)
func main() {
counts := make(map[string]int)
for _, filename := range os.Args[1:] {
data, err := ioutil.ReadFile(filename)
if err != nil {
fmt.Fprintf(os.Stderr, &quot;dup3: %v\n&quot;, err)
continue
}
for _, line := range strings.Split(string(data), &quot;\n&quot;) {
counts[line]++
}
}
for line, n := range counts {
if n &gt; 1 {
fmt.Printf(&quot;%d\t%s\n&quot;, n, line)
}
}
}
</code></pre>
<p><code>ReadFile</code> 函数返回一个字节切片byte slice必须把它转换为 <code>string</code>,才能用 <code>strings.Split</code> 分割。我们会在3.5.4 节详细讲解字符串和字节切片。</p>
<p>实现上,<code>bufio.Scanner</code><code>ioutil.ReadFile</code><code>ioutil.WriteFile</code> 都使用 <code>*os.File</code><code>Read</code><code>Write</code> 方法但是大多数程序员很少需要直接调用那些低级lower-level函数。高级higher-level函数<code>bufio</code><code>io/ioutil</code> 包中所提供的那些,用起来要容易点。</p>
<hr />
<p><strong>练习 1.4</strong> 修改 <code>dup2</code>,出现重复的行时打印文件名称。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="14-gif动画"><a class="header" href="#14-gif动画">1.4. GIF动画</a></h2>
<p>下面的程序会演示Go语言标准库里的image这个package的用法我们会用这个包来生成一系列的bit-mapped图然后将这些图片编码为一个GIF动画。我们生成的图形名字叫利萨如图形Lissajous figures这种效果是在1960年代的老电影里出现的一种视觉特效。它们是协振子在两个纬度上振动所产生的曲线比如两个sin正弦波分别在x轴和y轴输入会产生的曲线。图1.1是这样的一个例子:</p>
<p><img src="ch1/../images/ch1-01.png" alt="" /></p>
<p>译注要看这个程序的结果需要将标准输出重定向到一个GIF图像文件使用 <code>./lissajous &gt; output.gif</code> 命令。下面是GIF图像动画效果</p>
<p><img src="ch1/../images/ch1-01.gif" alt="" /></p>
<p>这段代码里我们用了一些新的结构包括const声明struct结构体类型复合声明。和我们举的其它的例子不太一样这一个例子包含了浮点数运算。这些概念我们只在这里简单地说明一下之后的章节会更详细地讲解。</p>
<p><u><i>gopl.io/ch1/lissajous</i></u></p>
<pre><code class="language-go">// Lissajous generates GIF animations of random Lissajous figures.
package main
import (
&quot;image&quot;
&quot;image/color&quot;
&quot;image/gif&quot;
&quot;io&quot;
&quot;math&quot;
&quot;math/rand&quot;
&quot;os&quot;
&quot;time&quot;
)
var palette = []color.Color{color.White, color.Black}
const (
whiteIndex = 0 // first color in palette
blackIndex = 1 // next color in palette
)
func main() {
// The sequence of images is deterministic unless we seed
// the pseudo-random number generator using the current time.
// Thanks to Randall McPherson for pointing out the omission.
rand.Seed(time.Now().UTC().UnixNano())
lissajous(os.Stdout)
}
func lissajous(out io.Writer) {
const (
cycles = 5 // number of complete x oscillator revolutions
res = 0.001 // angular resolution
size = 100 // image canvas covers [-size..+size]
nframes = 64 // number of animation frames
delay = 8 // delay between frames in 10ms units
)
freq := rand.Float64() * 3.0 // relative frequency of y oscillator
anim := gif.GIF{LoopCount: nframes}
phase := 0.0 // phase difference
for i := 0; i &lt; nframes; i++ {
rect := image.Rect(0, 0, 2*size+1, 2*size+1)
img := image.NewPaletted(rect, palette)
for t := 0.0; t &lt; cycles*2*math.Pi; t += res {
x := math.Sin(t)
y := math.Sin(t*freq + phase)
img.SetColorIndex(size+int(x*size+0.5), size+int(y*size+0.5),
blackIndex)
}
phase += 0.1
anim.Delay = append(anim.Delay, delay)
anim.Image = append(anim.Image, img)
}
gif.EncodeAll(out, &amp;anim) // NOTE: ignoring encoding errors
}
</code></pre>
<p>当我们import了一个包路径包含有多个单词的package时比如image/colorimage和color两个单词通常我们只需要用最后那个单词表示这个包就可以。所以当我们写color.White时这个变量指向的是image/color包里的变量同理gif.GIF是属于image/gif包里的变量。</p>
<p>这个程序里的常量声明给出了一系列的常量值常量是指在程序编译后运行时始终都不会变化的值比如圈数、帧数、延迟值。常量声明和变量声明一般都会出现在包级别所以这些常量在整个包中都是可以共享的或者你也可以把常量声明定义在函数体内部那么这种常量就只能在函数体内用。目前常量声明的值必须是一个数字值、字符串或者一个固定的boolean值。</p>
<p>[]color.Color{...}和gif.GIF{...}这两个表达式就是我们说的复合声明4.2和4.4.1节有说明。这是实例化Go语言里的复合类型的一种写法。这里的前者生成的是一个slice切片后者生成的是一个struct结构体。</p>
<p>gif.GIF是一个struct类型参考4.4节。struct是一组值或者叫字段的集合不同的类型集合在一个struct可以让我们以一个统一的单元进行处理。anim是一个gif.GIF类型的struct变量。这种写法会生成一个struct变量并且其内部变量LoopCount字段会被设置为nframes而其它的字段会被设置为各自类型默认的零值。struct内部的变量可以以一个点.来进行访问就像在最后两个赋值语句中显式地更新了anim这个struct的Delay和Image字段。</p>
<p>lissajous函数内部有两层嵌套的for循环。外层循环会循环64次每一次都会生成一个单独的动画帧。它生成了一个包含两种颜色的201*201大小的图片白色和黑色。所有像素点都会被默认设置为其零值也就是调色板palette里的第0个值这里我们设置的是白色。每次外层循环都会生成一张新图片并将一些像素设置为黑色。其结果会append到之前结果之后。这里我们用到了append(参考4.2.1)内置函数将结果append到anim中的帧列表末尾并设置一个默认的80ms的延迟值。循环结束后所有的延迟值被编码进了GIF图片中并将结果写入到输出流。out这个变量是io.Writer类型这个类型支持把输出结果写到很多目标很快我们就可以看到例子。</p>
<p>内层循环设置两个偏振值。x轴偏振使用sin函数。y轴偏振也是正弦波但其相对x轴的偏振是一个0-3的随机值初始偏振值是一个零值随着动画的每一帧逐渐增加。循环会一直跑到x轴完成五次完整的循环。每一步它都会调用SetColorIndex来为(x,y)点来染黑色。</p>
<p>main函数调用lissajous函数用它来向标准输出流打印信息所以下面这个命令会像图1.1中产生一个GIF动画。</p>
<pre><code>$ go build gopl.io/ch1/lissajous
$ ./lissajous &gt;out.gif
</code></pre>
<p><strong>练习 1.5</strong> 修改前面的Lissajous程序里的调色板由黑色改为绿色。我们可以用<code>color.RGBA{0xRR, 0xGG, 0xBB, 0xff}</code>来得到<code>#RRGGBB</code>这个色值,三个十六进制的字符串分别代表红、绿、蓝像素。</p>
<p><strong>练习 1.6</strong> 修改Lissajous程序修改其调色板来生成更丰富的颜色然后修改SetColorIndex的第三个参数看看显示结果吧。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="15-获取url"><a class="header" href="#15-获取url">1.5. 获取URL</a></h2>
<p>对于很多现代应用来说访问互联网上的信息和访问本地文件系统一样重要。Go语言在net这个强大package的帮助下提供了一系列的package来做这件事情使用这些包可以更简单地用网络收发信息还可以建立更底层的网络连接编写服务器程序。在这些情景下Go语言原生的并发特性在第八章中会介绍显得尤其好用。</p>
<p>为了最简单地展示基于HTTP获取信息的方式下面给出一个示例程序fetch这个程序将获取对应的url并将其源文本打印出来这个例子的灵感来源于curl工具译注unix下的一个用来发http请求的工具具体可以man curl。当然curl提供的功能更为复杂丰富这里只编写最简单的样例。这个样例之后还会多次被用到。</p>
<p><u><i>gopl.io/ch1/fetch</i></u></p>
<pre><code class="language-go">// Fetch prints the content found at a URL.
package main
import (
&quot;fmt&quot;
&quot;io/ioutil&quot;
&quot;net/http&quot;
&quot;os&quot;
)
func main() {
for _, url := range os.Args[1:] {
resp, err := http.Get(url)
if err != nil {
fmt.Fprintf(os.Stderr, &quot;fetch: %v\n&quot;, err)
os.Exit(1)
}
b, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
fmt.Fprintf(os.Stderr, &quot;fetch: reading %s: %v\n&quot;, url, err)
os.Exit(1)
}
fmt.Printf(&quot;%s&quot;, b)
}
}
</code></pre>
<p>这个程序从两个package中导入了函数net/http和io/ioutil包http.Get函数是创建HTTP请求的函数如果获取过程没有出错那么会在resp这个结构体中得到访问的请求结果。resp的Body字段包括一个可读的服务器响应流。ioutil.ReadAll函数从response中读取到全部内容将其结果保存在变量b中。resp.Body.Close关闭resp的Body流防止资源泄露Printf函数会将结果b写出到标准输出流中。</p>
<pre><code>$ go build gopl.io/ch1/fetch
$ ./fetch http://gopl.io
&lt;html&gt;
&lt;head&gt;
&lt;title&gt;The Go Programming Language&lt;/title&gt;title&gt;
...
</code></pre>
<p>HTTP请求如果失败了的话会得到下面这样的结果</p>
<pre><code>$ ./fetch http://bad.gopl.io
fetch: Get http://bad.gopl.io: dial tcp: lookup bad.gopl.io: no such host
</code></pre>
<p>译注在大天朝的网络环境下很容易重现这种错误下面是Windows下运行得到的错误信息</p>
<pre><code>$ go run main.go http://gopl.io
fetch: Get http://gopl.io: dial tcp: lookup gopl.io: getaddrinfow: No such host is known.
</code></pre>
<p>无论哪种失败原因我们的程序都用了os.Exit函数来终止进程并且返回一个status错误码其值为1。</p>
<p><strong>练习 1.7</strong> 函数调用io.Copy(dst, src)会从src中读取内容并将读到的结果写入到dst中使用这个函数替代掉例子中的ioutil.ReadAll来拷贝响应结构体到os.Stdout避免申请一个缓冲区例子中的b来存储。记得处理io.Copy返回结果中的错误。</p>
<p><strong>练习 1.8</strong> 修改fetch这个范例如果输入的url参数没有 <code>http://</code> 前缀的话为这个url加上该前缀。你可能会用到strings.HasPrefix这个函数。</p>
<p><strong>练习 1.9</strong> 修改fetch打印出HTTP协议的状态码可以从resp.Status变量得到该状态码。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="16-并发获取多个url"><a class="header" href="#16-并发获取多个url">1.6. 并发获取多个URL</a></h2>
<p>Go语言最有意思并且最新奇的特性就是对并发编程的支持。并发编程是一个大话题在第八章和第九章中会专门讲到。这里我们只浅尝辄止地来体验一下Go语言里的goroutine和channel。</p>
<p>下面的例子fetchall和前面小节的fetch程序所要做的工作基本一致fetchall的特别之处在于它会同时去获取所有的URL所以这个程序的总执行时间不会超过执行时间最长的那一个任务前面的fetch程序执行时间则是所有任务执行时间之和。fetchall程序只会打印获取的内容大小和经过的时间不会像之前那样打印获取的内容。</p>
<p><u><i>gopl.io/ch1/fetchall</i></u></p>
<pre><code class="language-go">// Fetchall fetches URLs in parallel and reports their times and sizes.
package main
import (
&quot;fmt&quot;
&quot;io&quot;
&quot;io/ioutil&quot;
&quot;net/http&quot;
&quot;os&quot;
&quot;time&quot;
)
func main() {
start := time.Now()
ch := make(chan string)
for _, url := range os.Args[1:] {
go fetch(url, ch) // start a goroutine
}
for range os.Args[1:] {
fmt.Println(&lt;-ch) // receive from channel ch
}
fmt.Printf(&quot;%.2fs elapsed\n&quot;, time.Since(start).Seconds())
}
func fetch(url string, ch chan&lt;- string) {
start := time.Now()
resp, err := http.Get(url)
if err != nil {
ch &lt;- fmt.Sprint(err) // send to channel ch
return
}
nbytes, err := io.Copy(ioutil.Discard, resp.Body)
resp.Body.Close() // don't leak resources
if err != nil {
ch &lt;- fmt.Sprintf(&quot;while reading %s: %v&quot;, url, err)
return
}
secs := time.Since(start).Seconds()
ch &lt;- fmt.Sprintf(&quot;%.2fs %7d %s&quot;, secs, nbytes, url)
}
</code></pre>
<p>下面使用fetchall来请求几个地址</p>
<pre><code>$ go build gopl.io/ch1/fetchall
$ ./fetchall https://golang.org http://gopl.io https://godoc.org
0.14s 6852 https://godoc.org
0.16s 7261 https://golang.org
0.48s 2475 http://gopl.io
0.48s elapsed
</code></pre>
<p>goroutine是一种函数的并发执行方式而channel是用来在goroutine之间进行参数传递。main函数本身也运行在一个goroutine中而go function则表示创建一个新的goroutine并在这个新的goroutine中执行这个函数。</p>
<p>main函数中用make函数创建了一个传递string类型参数的channel对每一个命令行参数我们都用go这个关键字来创建一个goroutine并且让函数在这个goroutine异步执行http.Get方法。这个程序里的io.Copy会把响应的Body内容拷贝到ioutil.Discard输出流中译注可以把这个变量看作一个垃圾桶可以向里面写一些不需要的数据因为我们需要这个方法返回的字节数但是又不想要其内容。每当请求返回内容时fetch函数都会往ch这个channel里写入一个字符串由main函数里的第二个for循环来处理并打印channel里的这个字符串。</p>
<p>当一个goroutine尝试在一个channel上做send或者receive操作时这个goroutine会阻塞在调用处直到另一个goroutine从这个channel里接收或者写入值这样两个goroutine才会继续执行channel操作之后的逻辑。在这个例子中每一个fetch函数在执行时都会往channel里发送一个值ch &lt;- expression主函数负责接收这些值&lt;-ch。这个程序中我们用main函数来完整地处理/接收所有fetch函数传回的字符串可以避免因为有两个goroutine同时完成而使得其输出交错在一起的危险。</p>
<p><strong>练习 1.10</strong> 找一个数据量比较大的网站用本小节中的程序调研网站的缓存策略对每个URL执行两遍请求查看两次时间是否有较大的差别并且每次获取到的响应内容是否一致修改本节中的程序将响应结果输出到文件以便于进行对比。</p>
<p><strong>练习 1.11</strong> 在fetchall中尝试使用长一些的参数列表比如使用在alexa.com的上百万网站里排名靠前的。如果一个网站没有回应程序将采取怎样的行为Section8.9 描述了在这种情况下的应对机制)。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="17-web服务"><a class="header" href="#17-web服务">1.7. Web服务</a></h2>
<p>Go语言的内置库使得写一个类似fetch的web服务器变得异常地简单。在本节中我们会展示一个微型服务器这个服务器的功能是返回当前用户正在访问的URL。比如用户访问的是 http://localhost:8000/hello 那么响应是URL.Path = &quot;hello&quot;</p>
<p><u><i>gopl.io/ch1/server1</i></u></p>
<pre><code class="language-go">// Server1 is a minimal &quot;echo&quot; server.
package main
import (
&quot;fmt&quot;
&quot;log&quot;
&quot;net/http&quot;
)
func main() {
http.HandleFunc(&quot;/&quot;, handler) // each request calls handler
log.Fatal(http.ListenAndServe(&quot;localhost:8000&quot;, nil))
}
// handler echoes the Path component of the request URL r.
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, &quot;URL.Path = %q\n&quot;, r.URL.Path)
}
</code></pre>
<p>我们只用了八九行代码就实现了一个Web服务程序这都是多亏了标准库里的方法已经帮我们完成了大量工作。main函数将所有发送到/路径下的请求和handler函数关联起来/开头的请求其实就是所有发送到当前站点上的请求服务监听8000端口。发送到这个服务的“请求”是一个http.Request类型的对象这个对象中包含了请求中的一系列相关字段其中就包括我们需要的URL。当请求到达服务器时这个请求会被传给handler函数来处理这个函数会将/hello这个路径从请求的URL中解析出来然后把其发送到响应中这里我们用的是标准输出流的fmt.Fprintf。Web服务会在第7.7节中做更详细的阐述。</p>
<p>让我们在后台运行这个服务程序。如果你的操作系统是Mac OS X或者Linux那么在运行命令的末尾加上一个&amp;符号即可让程序简单地跑在后台windows下可以在另外一个命令行窗口去运行这个程序。</p>
<pre><code>$ go run src/gopl.io/ch1/server1/main.go &amp;
</code></pre>
<p>现在可以通过命令行来发送客户端请求了:</p>
<pre><code>$ go build gopl.io/ch1/fetch
$ ./fetch http://localhost:8000
URL.Path = &quot;/&quot;
$ ./fetch http://localhost:8000/help
URL.Path = &quot;/help&quot;
</code></pre>
<p>还可以直接在浏览器里访问这个URL然后得到返回结果如图1.2</p>
<p><img src="ch1/../images/ch1-02.png" alt="" /></p>
<p>在这个服务的基础上叠加特性是很容易的。一种比较实用的修改是为访问的url添加某种状态。比如下面这个版本输出了同样的内容但是会对请求的次数进行计算对URL的请求结果会包含各种URL被访问的总次数直接对/count这个URL的访问要除外。</p>
<p><u><i>gopl.io/ch1/server2</i></u></p>
<pre><code class="language-go">// Server2 is a minimal &quot;echo&quot; and counter server.
package main
import (
&quot;fmt&quot;
&quot;log&quot;
&quot;net/http&quot;
&quot;sync&quot;
)
var mu sync.Mutex
var count int
func main() {
http.HandleFunc(&quot;/&quot;, handler)
http.HandleFunc(&quot;/count&quot;, counter)
log.Fatal(http.ListenAndServe(&quot;localhost:8000&quot;, nil))
}
// handler echoes the Path component of the requested URL.
func handler(w http.ResponseWriter, r *http.Request) {
mu.Lock()
count++
mu.Unlock()
fmt.Fprintf(w, &quot;URL.Path = %q\n&quot;, r.URL.Path)
}
// counter echoes the number of calls so far.
func counter(w http.ResponseWriter, r *http.Request) {
mu.Lock()
fmt.Fprintf(w, &quot;Count %d\n&quot;, count)
mu.Unlock()
}
</code></pre>
<p>这个服务器有两个请求处理函数根据请求的url不同会调用不同的函数对/count这个url的请求会调用到counter这个函数其它的url都会调用默认的处理函数。如果你的请求pattern是以/结尾那么所有以该url为前缀的url都会被这条规则匹配。在这些代码的背后服务器每一次接收请求处理时都会另起一个goroutine这样服务器就可以同一时间处理多个请求。然而在并发情况下假如真的有两个请求同一时刻去更新count那么这个值可能并不会被正确地增加这个程序可能会引发一个严重的bug竞态条件参见9.1。为了避免这个问题我们必须保证每次修改变量的最多只能有一个goroutine这也就是代码里的mu.Lock()和mu.Unlock()调用将修改count的所有行为包在中间的目的。第九章中我们会进一步讲解共享变量。</p>
<p>下面是一个更为丰富的例子handler函数会把请求的http头和请求的form数据都打印出来这样可以使检查和调试这个服务更为方便</p>
<p><u><i>gopl.io/ch1/server3</i></u></p>
<pre><code class="language-go">// handler echoes the HTTP request.
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, &quot;%s %s %s\n&quot;, r.Method, r.URL, r.Proto)
for k, v := range r.Header {
fmt.Fprintf(w, &quot;Header[%q] = %q\n&quot;, k, v)
}
fmt.Fprintf(w, &quot;Host = %q\n&quot;, r.Host)
fmt.Fprintf(w, &quot;RemoteAddr = %q\n&quot;, r.RemoteAddr)
if err := r.ParseForm(); err != nil {
log.Print(err)
}
for k, v := range r.Form {
fmt.Fprintf(w, &quot;Form[%q] = %q\n&quot;, k, v)
}
}
</code></pre>
<p>我们用http.Request这个struct里的字段来输出下面这样的内容</p>
<pre><code>GET /?q=query HTTP/1.1
Header[&quot;Accept-Encoding&quot;] = [&quot;gzip, deflate, sdch&quot;]
Header[&quot;Accept-Language&quot;] = [&quot;en-US,en;q=0.8&quot;]
Header[&quot;Connection&quot;] = [&quot;keep-alive&quot;]
Header[&quot;Accept&quot;] = [&quot;text/html,application/xhtml+xml,application/xml;...&quot;]
Header[&quot;User-Agent&quot;] = [&quot;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5)...&quot;]
Host = &quot;localhost:8000&quot;
RemoteAddr = &quot;127.0.0.1:59911&quot;
Form[&quot;q&quot;] = [&quot;query&quot;]
</code></pre>
<p>可以看到这里的ParseForm被嵌套在了if语句中。Go语言允许这样的一个简单的语句结果作为局部的变量声明出现在if语句的最前面这一点对错误处理很有用处。我们还可以像下面这样写当然看起来就长了一些</p>
<pre><code class="language-go">err := r.ParseForm()
if err != nil {
log.Print(err)
}
</code></pre>
<p>用if和ParseForm结合可以让代码更加简单并且可以限制err这个变量的作用域这么做是很不错的。我们会在2.7节中讲解作用域。</p>
<p>在这些程序中我们看到了很多不同的类型被输出到标准输出流中。比如前面的fetch程序把HTTP的响应数据拷贝到了os.Stdoutlissajous程序里我们输出的是一个文件。fetchall程序则完全忽略到了HTTP的响应Body只是计算了一下响应Body的大小这个程序中把响应Body拷贝到了ioutil.Discard。在本节的web服务器程序中则是用fmt.Fprintf直接写到了http.ResponseWriter中。</p>
<p>尽管三种具体的实现流程并不太一样他们都实现一个共同的接口即当它们被调用需要一个标准流输出时都可以满足。这个接口叫作io.Writer在7.1节中会详细讨论。</p>
<p>Go语言的接口机制会在第7章中讲解为了在这里简单说明接口能做什么让我们简单地将这里的web服务器和之前写的lissajous函数结合起来这样GIF动画可以被写到HTTP的客户端而不是之前的标准输出流。只要在web服务器的代码里加入下面这几行。</p>
<pre><code class="language-Go">handler := func(w http.ResponseWriter, r *http.Request) {
lissajous(w)
}
http.HandleFunc(&quot;/&quot;, handler)
</code></pre>
<p>或者另一种等价形式:</p>
<pre><code class="language-Go">http.HandleFunc(&quot;/&quot;, func(w http.ResponseWriter, r *http.Request) {
lissajous(w)
})
</code></pre>
<p>HandleFunc函数的第二个参数是一个函数的字面值也就是一个在使用时定义的匿名函数。这些内容我们会在5.6节中讲解。</p>
<p>做完这些修改之后,在浏览器里访问 http://localhost:8000 。每次你载入这个页面都可以看到一个像图1.3那样的动画。</p>
<p><img src="ch1/../images/ch1-03.png" alt="" /></p>
<p><strong>练习 1.12</strong> 修改Lissajour服务从URL读取变量比如你可以访问 http://localhost:8000/?cycles=20 这个URL这样访问可以将程序里的cycles默认的5修改为20。字符串转换为数字可以调用strconv.Atoi函数。你可以在godoc里查看strconv.Atoi的详细说明。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="18-本章要点"><a class="header" href="#18-本章要点">1.8. 本章要点</a></h2>
<p>本章对Go语言做了一些介绍Go语言很多方面在有限的篇幅中无法覆盖到。本节会把没有讲到的内容也做一些简单的介绍这样读者在读到完整的内容之前可以有个简单的印象。</p>
<p><strong>控制流:</strong> 在本章我们只介绍了if控制和for但是没有提到switch多路选择。这里是一个简单的switch的例子</p>
<pre><code class="language-go">switch coinflip() {
case &quot;heads&quot;:
heads++
case &quot;tails&quot;:
tails++
default:
fmt.Println(&quot;landed on edge!&quot;)
}
</code></pre>
<p>在翻转硬币的时候例子里的coinflip函数返回几种不同的结果每一个case都会对应一个返回结果这里需要注意Go语言并不需要显式地在每一个case后写break语言默认执行完case后的逻辑语句会自动退出。当然了如果你想要相邻的几个case都执行同一逻辑的话需要自己显式地写上一个fallthrough语句来覆盖这种默认行为。不过fallthrough语句在一般的程序中很少用到。</p>
<p>Go语言里的switch还可以不带操作对象译注switch不带操作对象时默认用true值代替然后将每个case的表达式和true值进行比较可以直接罗列多种条件像其它语言里面的多个if else一样下面是一个例子</p>
<pre><code class="language-go">func Signum(x int) int {
switch {
case x &gt; 0:
return +1
default:
return 0
case x &lt; 0:
return -1
}
}
</code></pre>
<p>这种形式叫做无tag switch(tagless switch)这和switch true是等价的。</p>
<p>像for和if控制语句一样switch也可以紧跟一个简短的变量声明一个自增表达式、赋值语句或者一个函数调用译注比其它语言丰富</p>
<p>break和continue语句会改变控制流。和其它语言中的break和continue一样break会中断当前的循环并开始执行循环之后的内容而continue会跳过当前循环并开始执行下一次循环。这两个语句除了可以控制for循环还可以用来控制switch和select语句之后会讲到在1.3节中我们看到continue会跳过内层的循环如果我们想跳过的是更外层的循环的话我们可以在相应的位置加上label这样break和continue就可以根据我们的想法来continue和break任意循环。这看起来甚至有点像goto语句的作用了。当然一般程序员也不会用到这种操作。这两种行为更多地被用到机器生成的代码中。</p>
<p><strong>命名类型:</strong> 类型声明使得我们可以很方便地给一个特殊类型一个名字。因为struct类型声明通常非常地长所以我们总要给这种struct取一个名字。本章中就有这样一个例子二维点类型</p>
<pre><code class="language-go">type Point struct {
X, Y int
}
var p Point
</code></pre>
<p>类型声明和命名类型会在第二章中介绍。</p>
<p><strong>指针:</strong> Go语言提供了指针。指针是一种直接存储了变量的内存地址的数据类型。在其它语言中比如C语言指针操作是完全不受约束的。在另外一些语言中指针一般被处理为“引用”除了到处传递这些指针之外并不能对这些指针做太多事情。Go语言在这两种范围中取了一种平衡。指针是可见的内存地址&amp;操作符可以返回一个变量的内存地址,并且*操作符可以获取指针指向的变量内容但是在Go语言里没有指针运算也就是不能像c语言里可以对指针进行加或减操作。我们会在2.3.2中进行详细介绍。</p>
<p><strong>方法和接口:</strong> 方法是和命名类型关联的一类函数。Go语言里比较特殊的是方法可以被关联到任意一种命名类型。在第六章我们会详细地讲方法。接口是一种抽象类型这种类型可以让我们以同样的方式来处理不同的固有类型不用关心它们的具体实现而只需要关注它们提供的方法。第七章中会详细说明这些内容。</p>
<p><strong>packages</strong> Go语言提供了一些很好用的package并且这些package是可以扩展的。Go语言社区已经创造并且分享了很多很多。所以Go语言编程大多数情况下就是用已有的package来写我们自己的代码。通过这本书我们会讲解一些重要的标准库内的package但是还是有很多限于篇幅没有去说明因为我们没法在这样的厚度的书里去做一部代码大全。</p>
<p>在你开始写一个新程序之前,最好先去检查一下是不是已经有了现成的库可以帮助你更高效地完成这件事情。你可以在 https://golang.org/pkg 和 https://godoc.org 中找到标准库和社区写的package。godoc这个工具可以让你直接在本地命令行阅读标准库的文档。比如下面这个例子。</p>
<pre><code>$ go doc http.ListenAndServe
package http // import &quot;net/http&quot;
func ListenAndServe(addr string, handler Handler) error
ListenAndServe listens on the TCP network address addr and then
calls Serve with handler to handle requests on incoming connections.
...
</code></pre>
<p><strong>注释:</strong> 我们之前已经提到过了在源文件的开头写的注释是这个源文件的文档。在每一个函数之前写一个说明函数行为的注释也是一个好习惯。这些惯例很重要因为这些内容会被像godoc这样的工具检测到并且在执行命令时显示这些注释。具体可以参考10.7.4。</p>
<p>多行注释可以用 <code>/* ... */</code> 来包裹,和其它大多数语言一样。在文件一开头的注释一般都是这种形式,或者一大段的解释性的注释文字也会被这符号包住,来避免每一行都需要加//。在注释中//和/*是没什么意义的,所以不要在注释中再嵌入注释。</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="第2章-程序结构"><a class="header" href="#第2章-程序结构">第2章 程序结构</a></h1>
<p>Go语言和其他编程语言一样一个大的程序是由很多小的基础构件组成的。变量保存值简单的加法和减法运算被组合成较复杂的表达式。基础类型被聚合为数组或结构体等更复杂的数据结构。然后使用if和for之类的控制语句来组织和控制表达式的执行流程。然后多个语句被组织到一个个函数中以便代码的隔离和复用。函数以源文件和包的方式被组织。</p>
<p>我们已经在前面章节的例子中看到了很多例子。在本章中我们将深入讨论Go程序基础结构方面的一些细节。每个示例程序都是刻意写的简单这样我们可以减少复杂的算法或数据结构等不相关的问题带来的干扰从而可以专注于Go语言本身的学习。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="21-命名"><a class="header" href="#21-命名">2.1. 命名</a></h2>
<p>Go语言中的函数名、变量名、常量名、类型名、语句标号和包名等所有的命名都遵循一个简单的命名规则一个名字必须以一个字母Unicode字母或下划线开头后面可以跟任意数量的字母、数字或下划线。大写字母和小写字母是不同的heapSort和Heapsort是两个不同的名字。</p>
<p>Go语言中类似if和switch的关键字有25个关键字不能用于自定义名字只能在特定语法结构中使用。</p>
<pre><code>break default func interface select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var
</code></pre>
<p>此外还有大约30多个预定义的名字比如int和true等主要对应内建的常量、类型和函数。</p>
<pre><code>内建常量: true false iota nil
内建类型: int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
float32 float64 complex128 complex64
bool byte rune string error
内建函数: make len cap new append copy close delete
complex real imag
panic recover
</code></pre>
<p>这些内部预先定义的名字并不是关键字,你可以在定义中重新使用它们。在一些特殊的场景中重新定义它们也是有意义的,但是也要注意避免过度而引起语义混乱。</p>
<p>如果一个名字是在函数内部定义那么它就只在函数内部有效。如果是在函数外部定义那么将在当前包的所有文件中都可以访问。名字的开头字母的大小写决定了名字在包外的可见性。如果一个名字是大写字母开头的译注必须是在函数外部定义的包级名字包级函数名本身也是包级名字那么它将是导出的也就是说可以被外部的包访问例如fmt包的Printf函数就是导出的可以在fmt包外部访问。包本身的名字一般总是用小写字母。</p>
<p>名字的长度没有逻辑限制但是Go语言的风格是尽量使用短小的名字对于局部变量尤其是这样你会经常看到i之类的短名字而不是冗长的theLoopIndex命名。通常来说如果一个名字的作用域比较大生命周期也比较长那么用长的名字将会更有意义。</p>
<p>在习惯上Go语言程序员推荐使用 <strong>驼峰式</strong> 命名当名字由几个单词组成时优先使用大小写分隔而不是优先用下划线分隔。因此在标准库有QuoteRuneToASCII和parseRequestLine这样的函数命名但是一般不会用quote_rune_to_ASCII和parse_request_line这样的命名。而像ASCII和HTML这样的缩略词则避免使用大小写混合的写法它们可能被称为htmlEscape、HTMLEscape或escapeHTML但不会是escapeHtml。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="22-声明"><a class="header" href="#22-声明">2.2. 声明</a></h2>
<p>声明语句定义了程序的各种实体对象以及部分或全部的属性。Go语言主要有四种类型的声明语句var、const、type和func分别对应变量、常量、类型和函数实体对象的声明。这一章我们重点讨论变量和类型的声明第三章将讨论常量的声明第五章将讨论函数的声明。</p>
<p>一个Go语言编写的程序对应一个或多个以.go为文件后缀名的源文件。每个源文件中以包的声明语句开始说明该源文件是属于哪个包。包声明语句之后是import语句导入依赖的其它包然后是包一级的类型、变量、常量、函数的声明语句包一级的各种类型的声明语句的顺序无关紧要译注函数内部的名字则必须先声明之后才能使用。例如下面的例子中声明了一个常量、一个函数和两个变量</p>
<p><u><i>gopl.io/ch2/boiling</i></u></p>
<pre><code class="language-Go">// Boiling prints the boiling point of water.
package main
import &quot;fmt&quot;
const boilingF = 212.0
func main() {
var f = boilingF
var c = (f - 32) * 5 / 9
fmt.Printf(&quot;boiling point = %g°F or %g°C\n&quot;, f, c)
// Output:
// boiling point = 212°F or 100°C
}
</code></pre>
<p>其中常量boilingF是在包一级范围声明语句声明的然后f和c两个变量是在main函数内部声明的声明语句声明的。在包一级声明语句声明的名字可在整个包对应的每个源文件中访问而不是仅仅在其声明语句所在的源文件中访问。相比之下局部声明的名字就只能在函数内部很小的范围被访问。</p>
<p>一个函数的声明由一个函数名字、参数列表由函数的调用者提供参数变量的具体值、一个可选的返回值列表和包含函数定义的函数体组成。如果函数没有返回值那么返回值列表是省略的。执行函数从函数的第一个语句开始依次顺序执行直到遇到return返回语句如果没有返回语句则是执行到函数末尾然后返回到函数调用者。</p>
<p>我们已经看到过很多函数声明和函数调用的例子了在第五章将深入讨论函数的相关细节这里只简单解释下。下面的fToC函数封装了温度转换的处理逻辑这样它只需要被定义一次就可以在多个地方多次被使用。在这个例子中main函数就调用了两次fToC函数分别使用在局部定义的两个常量作为调用函数的参数。</p>
<p><u><i>gopl.io/ch2/ftoc</i></u></p>
<pre><code class="language-Go">// Ftoc prints two Fahrenheit-to-Celsius conversions.
package main
import &quot;fmt&quot;
func main() {
const freezingF, boilingF = 32.0, 212.0
fmt.Printf(&quot;%g°F = %g°C\n&quot;, freezingF, fToC(freezingF)) // &quot;32°F = 0°C&quot;
fmt.Printf(&quot;%g°F = %g°C\n&quot;, boilingF, fToC(boilingF)) // &quot;212°F = 100°C&quot;
}
func fToC(f float64) float64 {
return (f - 32) * 5 / 9
}
</code></pre>
<div style="break-before: page; page-break-before: always;"></div><h2 id="23-变量"><a class="header" href="#23-变量">2.3. 变量</a></h2>
<p>var声明语句可以创建一个特定类型的变量然后给变量附加一个名字并且设置变量的初始值。变量声明的一般语法如下</p>
<pre><code class="language-Go">var 变量名字 类型 = 表达式
</code></pre>
<p>其中“<em>类型</em>”或“<em>= 表达式</em>”两个部分可以省略其中的一个。如果省略的是类型信息,那么将根据初始化表达式来推导变量的类型信息。如果初始化表达式被省略,那么将用零值初始化该变量。 数值类型变量对应的零值是0布尔类型变量对应的零值是false字符串类型对应的零值是空字符串接口或引用类型包括slice、指针、map、chan和函数变量对应的零值是nil。数组或结构体等聚合类型对应的零值是每个元素或字段都是对应该类型的零值。</p>
<p>零值初始化机制可以确保每个声明的变量总是有一个良好定义的值因此在Go语言中不存在未初始化的变量。这个特性可以简化很多代码而且可以在没有增加额外工作的前提下确保边界条件下的合理行为。例如</p>
<pre><code class="language-Go">var s string
fmt.Println(s) // &quot;&quot;
</code></pre>
<p>这段代码将打印一个空字符串而不是导致错误或产生不可预知的行为。Go语言程序员应该让一些聚合类型的零值也具有意义这样可以保证不管任何类型的变量总是有一个合理有效的零值状态。</p>
<p>也可以在一个声明语句中同时声明一组变量,或用一组初始化表达式声明并初始化一组变量。如果省略每个变量的类型,将可以声明多个类型不同的变量(类型由初始化表达式推导):</p>
<pre><code class="language-Go">var i, j, k int // int, int, int
var b, f, s = true, 2.3, &quot;four&quot; // bool, float64, string
</code></pre>
<p>初始化表达式可以是字面量或任意的表达式。在包级别声明的变量会在main入口函数执行前完成初始化§2.6.2),局部变量将在声明语句被执行到的时候完成初始化。</p>
<p>一组变量也可以通过调用一个函数,由函数返回的多个返回值初始化:</p>
<pre><code class="language-Go">var f, err = os.Open(name) // os.Open returns a file and an error
</code></pre>
<h3 id="231-简短变量声明"><a class="header" href="#231-简短变量声明">2.3.1. 简短变量声明</a></h3>
<p>在函数内部,有一种称为简短变量声明语句的形式可用于声明和初始化局部变量。它以“名字 := 表达式”形式声明变量变量的类型根据表达式来自动推导。下面是lissajous函数中的三个简短变量声明语句§1.4</p>
<pre><code class="language-Go">anim := gif.GIF{LoopCount: nframes}
freq := rand.Float64() * 3.0
t := 0.0
</code></pre>
<p>因为简洁和灵活的特点简短变量声明被广泛用于大部分的局部变量的声明和初始化。var形式的声明语句往往是用于需要显式指定变量类型的地方或者因为变量稍后会被重新赋值而初始值无关紧要的地方。</p>
<pre><code class="language-Go">i := 100 // an int
var boiling float64 = 100 // a float64
var names []string
var err error
var p Point
</code></pre>
<p>和var形式声明语句一样简短变量声明语句也可以用来声明和初始化一组变量</p>
<pre><code class="language-Go">i, j := 0, 1
</code></pre>
<p>但是这种同时声明多个变量的方式应该限制只在可以提高代码可读性的地方使用比如for语句的循环的初始化语句部分。</p>
<p>请记住“:=”是一个变量声明语句,而“=”是一个变量赋值操作。也不要混淆多个变量的声明和元组的多重赋值§2.4.1),后者是将右边各个表达式的值赋值给左边对应位置的各个变量:</p>
<pre><code class="language-Go">i, j = j, i // 交换 i 和 j 的值
</code></pre>
<p>和普通var形式的变量声明语句一样简短变量声明语句也可以用函数的返回值来声明和初始化变量像下面的os.Open函数调用将返回两个值</p>
<pre><code class="language-Go">f, err := os.Open(name)
if err != nil {
return err
}
// ...use f...
f.Close()
</code></pre>
<p>这里有一个比较微妙的地方简短变量声明左边的变量可能并不是全部都是刚刚声明的。如果有一些已经在相同的词法域声明过了§2.7),那么简短变量声明语句对这些已经声明过的变量就只有赋值行为了。</p>
<p>在下面的代码中第一个语句声明了in和err两个变量。在第二个语句只声明了out一个变量然后对已经声明的err进行了赋值操作。</p>
<pre><code class="language-Go">in, err := os.Open(infile)
// ...
out, err := os.Create(outfile)
</code></pre>
<p>简短变量声明语句中必须至少要声明一个新的变量,下面的代码将不能编译通过:</p>
<pre><code class="language-Go">f, err := os.Open(infile)
// ...
f, err := os.Create(outfile) // compile error: no new variables
</code></pre>
<p>解决的方法是第二个简短变量声明语句改用普通的多重赋值语句。</p>
<p>简短变量声明语句只有对已经在同级词法域声明过的变量才和赋值操作语句等价,如果变量是在外部词法域声明的,那么简短变量声明语句将会在当前词法域重新声明一个新的变量。我们在本章后面将会看到类似的例子。</p>
<h3 id="232-指针"><a class="header" href="#232-指针">2.3.2. 指针</a></h3>
<p>一个变量对应一个保存了变量对应类型值的内存空间。普通变量在声明语句创建时被绑定到一个变量名比如叫x的变量但是还有很多变量始终以表达式方式引入例如x[i]或x.f变量。所有这些表达式一般都是读取一个变量的值除非它们是出现在赋值语句的左边这种时候是给对应变量赋予一个新的值。</p>
<p>一个指针的值是另一个变量的地址。一个指针对应变量在内存中的存储位置。并不是每一个值都会有一个内存地址,但是对于每一个变量必然有对应的内存地址。通过指针,我们可以直接读或更新对应变量的值,而不需要知道该变量的名字(如果变量有名字的话)。</p>
<p>如果用“var x int”声明语句声明一个x变量那么&amp;x表达式取x变量的内存地址将产生一个指向该整数变量的指针指针对应的数据类型是<code>*int</code>指针被称之为“指向int类型的指针”。如果指针名字为p那么可以说“p指针指向变量x”或者说“p指针保存了x变量的内存地址”。同时<code>*p</code>表达式对应p指针指向的变量的值。一般<code>*p</code>表达式读取指针指向的变量的值这里为int类型的值同时因为<code>*p</code>对应一个变量,所以该表达式也可以出现在赋值语句的左边,表示更新指针所指向的变量的值。</p>
<pre><code class="language-Go">x := 1
p := &amp;x // p, of type *int, points to x
fmt.Println(*p) // &quot;1&quot;
*p = 2 // equivalent to x = 2
fmt.Println(x) // &quot;2&quot;
</code></pre>
<p>对于聚合类型每个成员——比如结构体的每个字段、或者是数组的每个元素——也都是对应一个变量,因此可以被取地址。</p>
<p>变量有时候被称为可寻址的值。即使变量由表达式临时生成,那么表达式也必须能接受<code>&amp;</code>取地址操作。</p>
<p>任何类型的指针的零值都是nil。如果p指向某个有效变量那么<code>p != nil</code>测试为真。指针之间也是可以进行相等测试的只有当它们指向同一个变量或全部是nil时才相等。</p>
<pre><code class="language-Go">var x, y int
fmt.Println(&amp;x == &amp;x, &amp;x == &amp;y, &amp;x == nil) // &quot;true false false&quot;
</code></pre>
<p>在Go语言中返回函数中局部变量的地址也是安全的。例如下面的代码调用f函数时创建局部变量v在局部变量地址被返回之后依然有效因为指针p依然引用这个变量。</p>
<pre><code class="language-Go">var p = f()
func f() *int {
v := 1
return &amp;v
}
</code></pre>
<p>每次调用f函数都将返回不同的结果</p>
<pre><code class="language-Go">fmt.Println(f() == f()) // &quot;false&quot;
</code></pre>
<p>因为指针包含了一个变量的地址因此如果将指针作为参数调用函数那将可以在函数中通过该指针来更新变量的值。例如下面这个例子就是通过指针来更新变量的值然后返回更新后的值可用在一个表达式中译注这是对C语言中<code>++v</code>操作的模拟这里只是为了说明指针的用法incr函数模拟的做法并不推荐</p>
<pre><code class="language-Go">func incr(p *int) int {
*p++ // 非常重要只是增加p指向的变量的值并不改变p指针
return *p
}
v := 1
incr(&amp;v) // side effect: v is now 2
fmt.Println(incr(&amp;v)) // &quot;3&quot; (and v is 3)
</code></pre>
<p>每次我们对一个变量取地址,或者复制指针,我们都是为原变量创建了新的别名。例如,<code>*p</code>就是变量v的别名。指针特别有价值的地方在于我们可以不用名字而访问一个变量但是这是一把双刃剑要找到一个变量的所有访问者并不容易我们必须知道变量全部的别名译注这是Go语言的垃圾回收器所做的工作。不仅仅是指针会创建别名很多其他引用类型也会创建别名例如slice、map和chan甚至结构体、数组和接口都会创建所引用变量的别名。</p>
<p>指针是实现标准库中flag包的关键技术它使用命令行参数来设置对应变量的值而这些对应命令行标志参数的变量可能会零散分布在整个程序中。为了说明这一点在早些的echo版本中就包含了两个可选的命令行参数<code>-n</code>用于忽略行尾的换行符,<code>-s sep</code>用于指定分隔字符默认是空格。下面这是第四个版本对应包路径为gopl.io/ch2/echo4。</p>
<p><u><i>gopl.io/ch2/echo4</i></u></p>
<pre><code class="language-Go">// Echo4 prints its command-line arguments.
package main
import (
&quot;flag&quot;
&quot;fmt&quot;
&quot;strings&quot;
)
var n = flag.Bool(&quot;n&quot;, false, &quot;omit trailing newline&quot;)
var sep = flag.String(&quot;s&quot;, &quot; &quot;, &quot;separator&quot;)
func main() {
flag.Parse()
fmt.Print(strings.Join(flag.Args(), *sep))
if !*n {
fmt.Println()
}
}
</code></pre>
<p>调用flag.Bool函数会创建一个新的对应布尔型标志参数的变量。它有三个属性第一个是命令行标志参数的名字“n”然后是该标志参数的默认值这里是false最后是该标志参数对应的描述信息。如果用户在命令行输入了一个无效的标志参数或者输入<code>-h</code><code>-help</code>参数那么将打印所有标志参数的名字、默认值和描述信息。类似的调用flag.String函数将创建一个对应字符串类型的标志参数变量同样包含命令行标志参数对应的参数名、默认值、和描述信息。程序中的<code>sep</code><code>n</code>变量分别是指向对应命令行标志参数变量的指针,因此必须用<code>*sep</code><code>*n</code>形式的指针语法间接引用它们。</p>
<p>当程序运行时必须在使用标志参数对应的变量之前先调用flag.Parse函数用于更新每个标志参数对应变量的值之前是默认值。对于非标志参数的普通命令行参数可以通过调用flag.Args()函数来访问返回值对应一个字符串类型的slice。如果在flag.Parse函数解析命令行参数时遇到错误默认将打印相关的提示信息然后调用os.Exit(2)终止程序。</p>
<p>让我们运行一些echo测试用例</p>
<pre><code>$ go build gopl.io/ch2/echo4
$ ./echo4 a bc def
a bc def
$ ./echo4 -s / a bc def
a/bc/def
$ ./echo4 -n a bc def
a bc def$
$ ./echo4 -help
Usage of ./echo4:
-n omit trailing newline
-s string
separator (default &quot; &quot;)
</code></pre>
<h3 id="233-new函数"><a class="header" href="#233-new函数">2.3.3. new函数</a></h3>
<p>另一个创建变量的方法是调用内建的new函数。表达式new(T)将创建一个T类型的匿名变量初始化为T类型的零值然后返回变量地址返回的指针类型为<code>*T</code></p>
<pre><code class="language-Go">p := new(int) // p, *int 类型, 指向匿名的 int 变量
fmt.Println(*p) // &quot;0&quot;
*p = 2 // 设置 int 匿名变量的值为 2
fmt.Println(*p) // &quot;2&quot;
</code></pre>
<p>用new创建变量和普通变量声明语句方式创建变量没有什么区别除了不需要声明一个临时变量的名字外我们还可以在表达式中使用new(T)。换言之new函数类似是一种语法糖而不是一个新的基础概念。</p>
<p>下面的两个newInt函数有着相同的行为</p>
<pre><code class="language-Go">func newInt() *int {
return new(int)
}
func newInt() *int {
var dummy int
return &amp;dummy
}
</code></pre>
<p>每次调用new函数都是返回一个新的变量的地址因此下面两个地址是不同的</p>
<pre><code class="language-Go">p := new(int)
q := new(int)
fmt.Println(p == q) // &quot;false&quot;
</code></pre>
<p>当然也可能有特殊情况如果两个类型都是空的也就是说类型的大小是0例如<code>struct{}</code><code>[0]int</code>有可能有相同的地址依赖具体的语言实现译注请谨慎使用大小为0的类型因为如果类型的大小为0的话可能导致Go语言的自动垃圾回收器有不同的行为具体请查看<code>runtime.SetFinalizer</code>函数相关文档)。</p>
<p>new函数使用通常相对比较少因为对于结构体来说直接用字面量语法创建新变量的方法会更灵活§4.4.1)。</p>
<p>由于new只是一个预定义的函数它并不是一个关键字因此我们可以将new名字重新定义为别的类型。例如下面的例子</p>
<pre><code class="language-Go">func delta(old, new int) int { return new - old }
</code></pre>
<p>由于new被定义为int类型的变量名因此在delta函数内部是无法使用内置的new函数的。</p>
<h3 id="234-变量的生命周期"><a class="header" href="#234-变量的生命周期">2.3.4. 变量的生命周期</a></h3>
<p>变量的生命周期指的是在程序运行期间变量有效存在的时间段。对于在包一级声明的变量来说,它们的生命周期和整个程序的运行周期是一致的。而相比之下,局部变量的生命周期则是动态的:每次从创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收。函数的参数变量和返回值变量都是局部变量。它们在函数每次被调用的时候创建。</p>
<p>例如下面是从1.4节的Lissajous程序摘录的代码片段</p>
<pre><code class="language-Go">for t := 0.0; t &lt; cycles*2*math.Pi; t += res {
x := math.Sin(t)
y := math.Sin(t*freq + phase)
img.SetColorIndex(size+int(x*size+0.5), size+int(y*size+0.5),
blackIndex)
}
</code></pre>
<p>译注:函数的右小括弧也可以另起一行缩进,同时为了防止编译器在行尾自动插入分号而导致的编译错误,可以在末尾的参数变量后面显式插入逗号。像下面这样:</p>
<pre><code class="language-Go">for t := 0.0; t &lt; cycles*2*math.Pi; t += res {
x := math.Sin(t)
y := math.Sin(t*freq + phase)
img.SetColorIndex(
size+int(x*size+0.5), size+int(y*size+0.5),
blackIndex, // 最后插入的逗号不会导致编译错误这是Go编译器的一个特性
) // 小括弧另起一行缩进,和大括弧的风格保存一致
}
</code></pre>
<p>在每次循环的开始会创建临时变量t然后在每次循环迭代中创建临时变量x和y。</p>
<p>那么Go语言的自动垃圾收集器是如何知道一个变量是何时可以被回收的呢这里我们可以避开完整的技术细节基本的实现思路是从每个包级的变量和每个当前运行函数的每一个局部变量开始通过指针或引用的访问路径遍历是否可以找到该变量。如果不存在这样的访问路径那么说明该变量是不可达的也就是说它是否存在并不会影响程序后续的计算结果。</p>
<p>因为一个变量的有效周期只取决于是否可达,因此一个循环迭代内部的局部变量的生命周期可能超出其局部作用域。同时,局部变量可能在函数返回之后依然存在。</p>
<p>编译器会自动选择在栈上还是在堆上分配局部变量的存储空间但可能令人惊讶的是这个选择并不是由用var还是new声明变量的方式决定的。</p>
<pre><code class="language-Go">var global *int
func f() {
var x int
x = 1
global = &amp;x
}
func g() {
y := new(int)
*y = 1
}
</code></pre>
<p>f函数里的x变量必须在堆上分配因为它在函数退出后依然可以通过包一级的global变量找到虽然它是在函数内部定义的用Go语言的术语说这个x局部变量从函数f中逃逸了。相反当g函数返回时变量<code>*y</code>将是不可达的,也就是说可以马上被回收的。因此,<code>*y</code>并没有从函数g中逃逸编译器可以选择在栈上分配<code>*y</code>的存储空间译注也可以选择在堆上分配然后由Go语言的GC回收这个变量的内存空间虽然这里用的是new方式。其实在任何时候你并不需为了编写正确的代码而要考虑变量的逃逸行为要记住的是逃逸的变量需要额外分配内存同时对性能的优化可能会产生细微的影响。</p>
<p>Go语言的自动垃圾收集器对编写正确的代码是一个巨大的帮助但也并不是说你完全不用考虑内存了。你虽然不需要显式地分配和释放内存但是要编写高效的程序你依然需要了解变量的生命周期。例如如果将指向短生命周期对象的指针保存到具有长生命周期的对象中特别是保存到全局变量时会阻止对短生命周期对象的垃圾回收从而可能影响程序的性能</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="24-赋值"><a class="header" href="#24-赋值">2.4. 赋值</a></h2>
<p>使用赋值语句可以更新一个变量的值,最简单的赋值语句是将要被赋值的变量放在=的左边,新值的表达式放在=的右边。</p>
<pre><code class="language-Go">x = 1 // 命名变量的赋值
*p = true // 通过指针间接赋值
person.name = &quot;bob&quot; // 结构体字段赋值
count[x] = count[x] * scale // 数组、slice或map的元素赋值
</code></pre>
<p>特定的二元算术运算符和赋值语句的复合操作有一个简洁形式,例如上面最后的语句可以重写为:</p>
<pre><code class="language-Go">count[x] *= scale
</code></pre>
<p>这样可以省去对变量表达式的重复计算。</p>
<p>数值变量也可以支持<code>++</code>递增和<code>--</code>递减语句(译注:自增和自减是语句,而不是表达式,因此<code>x = i++</code>之类的表达式是错误的):</p>
<pre><code class="language-Go">v := 1
v++ // 等价方式 v = v + 1v 变成 2
v-- // 等价方式 v = v - 1v 变成 1
</code></pre>
<h3 id="241-元组赋值"><a class="header" href="#241-元组赋值">2.4.1. 元组赋值</a></h3>
<p>元组赋值是另一种形式的赋值语句,它允许同时更新多个变量的值。在赋值之前,赋值语句右边的所有表达式将会先进行求值,然后再统一更新左边对应变量的值。这对于处理有些同时出现在元组赋值语句左右两边的变量很有帮助,例如我们可以这样交换两个变量的值:</p>
<pre><code class="language-Go">x, y = y, x
a[i], a[j] = a[j], a[i]
</code></pre>
<p>或者是计算两个整数值的的最大公约数GCD译注GCD不是那个敏感字而是greatest common divisor的缩写欧几里德的GCD是最早的非平凡算法</p>
<pre><code class="language-Go">func gcd(x, y int) int {
for y != 0 {
x, y = y, x%y
}
return x
}
</code></pre>
<p>或者是计算斐波纳契数列Fibonacci的第N个数</p>
<pre><code class="language-Go">func fib(n int) int {
x, y := 0, 1
for i := 0; i &lt; n; i++ {
x, y = y, x+y
}
return x
}
</code></pre>
<p>元组赋值也可以使一系列琐碎赋值更加紧凑(译注: 特别是在for循环的初始化部分</p>
<pre><code class="language-Go">i, j, k = 2, 3, 5
</code></pre>
<p>但如果表达式太复杂的话,应该尽量避免过度使用元组赋值;因为每个变量单独赋值语句的写法可读性会更好。</p>
<p>有些表达式会产生多个值,比如调用一个有多个返回值的函数。当这样一个函数调用出现在元组赋值右边的表达式中时(译注:右边不能再有其它表达式),左边变量的数目必须和右边一致。</p>
<pre><code class="language-Go">f, err = os.Open(&quot;foo.txt&quot;) // function call returns two values
</code></pre>
<p>通常这类函数会用额外的返回值来表达某种错误类型例如os.Open是用额外的返回值返回一个error类型的错误还有一些是用来返回布尔值通常被称为ok。在稍后我们将看到的三个操作都是类似的用法。如果map查找§4.3、类型断言§7.10或通道接收§8.4.2)出现在赋值语句的右边,它们都可能会产生两个结果,有一个额外的布尔结果表示操作是否成功:</p>
<pre><code class="language-Go">v, ok = m[key] // map lookup
v, ok = x.(T) // type assertion
v, ok = &lt;-ch // channel receive
</code></pre>
<p>译注map查找§4.3、类型断言§7.10或通道接收§8.4.2出现在赋值语句的右边时并不一定是产生两个结果也可能只产生一个结果。对于只产生一个结果的情形map查找失败时会返回零值类型断言失败时会发生运行时panic异常通道接收失败时会返回零值阻塞不算是失败。例如下面的例子</p>
<pre><code class="language-Go">v = m[key] // map查找失败时返回零值
v = x.(T) // type断言失败时panic异常
v = &lt;-ch // 管道接收,失败时返回零值(阻塞不算是失败)
_, ok = m[key] // map返回2个值
_, ok = mm[&quot;&quot;], false // map返回1个值
_ = mm[&quot;&quot;] // map返回1个值
</code></pre>
<p>和变量声明一样,我们可以用下划线空白标识符<code>_</code>来丢弃不需要的值。</p>
<pre><code class="language-Go">_, err = io.Copy(dst, src) // 丢弃字节数
_, ok = x.(T) // 只检测类型,忽略具体值
</code></pre>
<h3 id="242-可赋值性"><a class="header" href="#242-可赋值性">2.4.2. 可赋值性</a></h3>
<p>赋值语句是显式的赋值形式但是程序中还有很多地方会发生隐式的赋值行为函数调用会隐式地将调用参数的值赋值给函数的参数变量一个返回语句会隐式地将返回操作的值赋值给结果变量一个复合类型的字面量§4.2)也会产生赋值行为。例如下面的语句:</p>
<pre><code class="language-Go">medals := []string{&quot;gold&quot;, &quot;silver&quot;, &quot;bronze&quot;}
</code></pre>
<p>隐式地对slice的每个元素进行赋值操作类似这样写的行为</p>
<pre><code class="language-Go">medals[0] = &quot;gold&quot;
medals[1] = &quot;silver&quot;
medals[2] = &quot;bronze&quot;
</code></pre>
<p>map和chan的元素虽然不是普通的变量但是也有类似的隐式赋值行为。</p>
<p>不管是隐式还是显式地赋值,在赋值语句左边的变量和右边最终的求到的值必须有相同的数据类型。更直白地说,只有右边的值对于左边的变量是可赋值的,赋值语句才是允许的。</p>
<p>可赋值性的规则对于不同类型有着不同要求对每个新类型特殊的地方我们会专门解释。对于目前我们已经讨论过的类型它的规则是简单的类型必须完全匹配nil可以赋值给任何指针或引用类型的变量。常量§3.6)则有更灵活的赋值规则,因为这样可以避免不必要的显式的类型转换。</p>
<p>对于两个值是否可以用<code>==</code><code>!=</code>进行相等比较的能力也和可赋值能力有关系:对于任何类型的值的相等比较,第二个值必须是对第一个值类型对应的变量是可赋值的,反之亦然。和前面一样,我们会对每个新类型比较特殊的地方做专门的解释。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="25-类型"><a class="header" href="#25-类型">2.5. 类型</a></h2>
<p>变量或表达式的类型定义了对应存储值的属性特征例如数值在内存的存储大小或者是元素的bit个数它们在内部是如何表达的是否支持一些操作符以及它们自己关联的方法集等。</p>
<p>在任何程序中都会存在一些变量有着相同的内部结构但是却表示完全不同的概念。例如一个int类型的变量可以用来表示一个循环的迭代索引、或者一个时间戳、或者一个文件描述符、或者一个月份一个float64类型的变量可以用来表示每秒移动几米的速度、或者是不同温度单位下的温度一个字符串可以用来表示一个密码或者一个颜色的名称。</p>
<p>一个类型声明语句创建了一个新的类型名称,和现有类型具有相同的底层结构。新命名的类型提供了一个方法,用来分隔不同概念的类型,这样即使它们底层类型相同也是不兼容的。</p>
<pre><code class="language-Go">type 类型名字 底层类型
</code></pre>
<p>类型声明语句一般出现在包一级,因此如果新创建的类型名字的首字符大写,则在包外部也可以使用。</p>
<p>译注对于中文汉字Unicode标志都作为小写字母处理因此中文的命名默认不能导出不过国内的用户针对该问题提出了不同的看法根据RobPike的回复在Go2中有可能会将中日韩等字符当作大写字母处理。下面是RobPik在 <a href="https://github.com/golang/go/issues/5763">Issue763</a> 的回复:</p>
<blockquote>
<p>A solution that's been kicking around for a while:</p>
<p>For Go 2 (can't do it before then): Change the definition to “lower case letters and _ are package-local; all else is exported”. Then with non-cased languages, such as Japanese, we can write 日本语 for an exported name and _日本语 for a local name. This rule has no effect, relative to the Go 1 rule, with cased languages. They behave exactly the same.</p>
</blockquote>
<p>为了说明类型声明,我们将不同温度单位分别定义为不同的类型:</p>
<p><u><i>gopl.io/ch2/tempconv0</i></u></p>
<pre><code class="language-Go">// Package tempconv performs Celsius and Fahrenheit temperature computations.
package tempconv
import &quot;fmt&quot;
type Celsius float64 // 摄氏温度
type Fahrenheit float64 // 华氏温度
const (
AbsoluteZeroC Celsius = -273.15 // 绝对零度
FreezingC Celsius = 0 // 结冰点温度
BoilingC Celsius = 100 // 沸水温度
)
func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }
func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }
</code></pre>
<p>我们在这个包声明了两种类型Celsius和Fahrenheit分别对应不同的温度单位。它们虽然有着相同的底层类型float64但是它们是不同的数据类型因此它们不可以被相互比较或混在一个表达式运算。刻意区分类型可以避免一些像无意中使用不同单位的温度混合计算导致的错误因此需要一个类似Celsius(t)或Fahrenheit(t)形式的显式转型操作才能将float64转为对应的类型。Celsius(t)和Fahrenheit(t)是类型转换操作它们并不是函数调用。类型转换不会改变值本身但是会使它们的语义发生变化。另一方面CToF和FToC两个函数则是对不同温度单位下的温度进行换算它们会返回不同的值。</p>
<p>对于每一个类型T都有一个对应的类型转换操作T(x)用于将x转为T类型译注如果T是指针类型可能会需要用小括弧包装T比如<code>(*int)(0)</code>。只有当两个类型的底层基础类型相同时才允许这种转型操作或者是两者都是指向相同底层结构的指针类型这些转换只改变类型而不会影响值本身。如果x是可以赋值给T类型的值那么x必然也可以被转为T类型但是一般没有这个必要。</p>
<p>数值类型之间的转型也是允许的并且在字符串和一些特定类型的slice之间也是可以转换的在下一章我们会看到这样的例子。这类转换可能改变值的表现。例如将一个浮点数转为整数将丢弃小数部分将一个字符串转为<code>[]byte</code>类型的slice将拷贝一个字符串数据的副本。在任何情况下运行时不会发生转换失败的错误译注: 错误只会发生在编译阶段)。</p>
<p>底层数据类型决定了内部结构和表达方式也决定是否可以像底层类型一样对内置运算符的支持。这意味着Celsius和Fahrenheit类型的算术运算行为和底层的float64类型是一样的正如我们所期望的那样。</p>
<pre><code class="language-Go">fmt.Printf(&quot;%g\n&quot;, BoilingC-FreezingC) // &quot;100&quot; °C
boilingF := CToF(BoilingC)
fmt.Printf(&quot;%g\n&quot;, boilingF-CToF(FreezingC)) // &quot;180&quot; °F
fmt.Printf(&quot;%g\n&quot;, boilingF-FreezingC) // compile error: type mismatch
</code></pre>
<p>比较运算符<code>==</code><code>&lt;</code>也可以用来比较一个命名类型的变量和另一个有相同类型的变量,或有着相同底层类型的未命名类型的值之间做比较。但是如果两个值有着不同的类型,则不能直接进行比较:</p>
<pre><code class="language-Go">var c Celsius
var f Fahrenheit
fmt.Println(c == 0) // &quot;true&quot;
fmt.Println(f &gt;= 0) // &quot;true&quot;
fmt.Println(c == f) // compile error: type mismatch
fmt.Println(c == Celsius(f)) // &quot;true&quot;!
</code></pre>
<p>注意最后那个语句。尽管看起来像函数调用但是Celsius(f)是类型转换操作它并不会改变值仅仅是改变值的类型而已。测试为真的原因是因为c和f都是零值。</p>
<p>一个命名的类型可以提供书写方便特别是可以避免一遍又一遍地书写复杂类型译注例如用匿名的结构体定义变量。虽然对于像float64这种简单的底层类型没有简洁很多但是如果是复杂的类型将会简洁很多特别是我们即将讨论的结构体类型。</p>
<p>命名类型还可以为该类型的值定义新的行为。这些行为表示为一组关联到该类型的函数集合,我们称为类型的方法集。我们将在第六章中讨论方法的细节,这里只说些简单用法。</p>
<p>下面的声明语句Celsius类型的参数c出现在了函数名的前面表示声明的是Celsius类型的一个名叫String的方法该方法返回该类型对象c带着°C温度单位的字符串</p>
<pre><code class="language-Go">func (c Celsius) String() string { return fmt.Sprintf(&quot;%g°C&quot;, c) }
</code></pre>
<p>许多类型都会定义一个String方法因为当使用fmt包的打印方法时将会优先使用该类型对应的String方法返回的结果打印我们将在7.1节讲述。</p>
<pre><code class="language-Go">c := FToC(212.0)
fmt.Println(c.String()) // &quot;100°C&quot;
fmt.Printf(&quot;%v\n&quot;, c) // &quot;100°C&quot;; no need to call String explicitly
fmt.Printf(&quot;%s\n&quot;, c) // &quot;100°C&quot;
fmt.Println(c) // &quot;100°C&quot;
fmt.Printf(&quot;%g\n&quot;, c) // &quot;100&quot;; does not call String
fmt.Println(float64(c)) // &quot;100&quot;; does not call String
</code></pre>
<div style="break-before: page; page-break-before: always;"></div><h2 id="26-包和文件"><a class="header" href="#26-包和文件">2.6. 包和文件</a></h2>
<p>Go语言中的包和其他语言的库或模块的概念类似目的都是为了支持模块化、封装、单独编译和代码重用。一个包的源代码保存在一个或多个以.go为文件后缀名的源文件中通常一个包所在目录路径的后缀是包的导入路径例如包gopl.io/ch1/helloworld对应的目录路径是$GOPATH/src/gopl.io/ch1/helloworld。</p>
<p>每个包都对应一个独立的名字空间。例如在image包中的Decode函数和在unicode/utf16包中的 Decode函数是不同的。要在外部引用该函数必须显式使用image.Decode或utf16.Decode形式访问。</p>
<p>包还可以让我们通过控制哪些名字是外部可见的来隐藏内部实现信息。在Go语言中一个简单的规则是如果一个名字是大写字母开头的那么该名字是导出的译注因为汉字不区分大小写因此汉字开头的名字是没有导出的</p>
<p>为了演示包基本的用法先假设我们的温度转换软件已经很流行我们希望到Go语言社区也能使用这个包。我们该如何做呢</p>
<p>让我们创建一个名为gopl.io/ch2/tempconv的包这是前面例子的一个改进版本。这里我们没有按照惯例按顺序对例子进行编号因此包路径看起来更像一个真实的包包代码存储在两个源文件中用来演示如何在一个源文件声明然后在其他的源文件访问虽然在现实中这样小的包一般只需要一个文件。</p>
<p>我们把变量的声明、对应的常量还有方法都放到tempconv.go源文件中</p>
<p><u><i>gopl.io/ch2/tempconv</i></u></p>
<pre><code class="language-Go">// Package tempconv performs Celsius and Fahrenheit conversions.
package tempconv
import &quot;fmt&quot;
type Celsius float64
type Fahrenheit float64
const (
AbsoluteZeroC Celsius = -273.15
FreezingC Celsius = 0
BoilingC Celsius = 100
)
func (c Celsius) String() string { return fmt.Sprintf(&quot;%g°C&quot;, c) }
func (f Fahrenheit) String() string { return fmt.Sprintf(&quot;%g°F&quot;, f) }
</code></pre>
<p>转换函数则放在另一个conv.go源文件中</p>
<pre><code class="language-Go">package tempconv
// CToF converts a Celsius temperature to Fahrenheit.
func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }
// FToC converts a Fahrenheit temperature to Celsius.
func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }
</code></pre>
<p>每个源文件都是以包的声明语句开始用来指明包的名字。当包被导入的时候包内的成员将通过类似tempconv.CToF的形式访问。而包级别的名字例如在一个文件声明的类型和常量在同一个包的其他源文件也是可以直接访问的就好像所有代码都在一个文件一样。要注意的是tempconv.go源文件导入了fmt包但是conv.go源文件并没有因为这个源文件中的代码并没有用到fmt包。</p>
<p>因为包级别的常量名都是以大写字母开头它们可以像tempconv.AbsoluteZeroC这样被外部代码访问</p>
<pre><code class="language-Go">fmt.Printf(&quot;Brrrr! %v\n&quot;, tempconv.AbsoluteZeroC) // &quot;Brrrr! -273.15°C&quot;
</code></pre>
<p>要将摄氏温度转换为华氏温度需要先用import语句导入gopl.io/ch2/tempconv包然后就可以使用下面的代码进行转换了</p>
<pre><code class="language-Go">fmt.Println(tempconv.CToF(tempconv.BoilingC)) // &quot;212°F&quot;
</code></pre>
<p>在每个源文件的包声明前紧跟着的注释是包注释§10.7.4。通常包注释的第一句应该先是包的功能概要说明。一个包通常只有一个源文件有包注释译注如果有多个包注释目前的文档工具会根据源文件名的先后顺序将它们链接为一个包注释。如果包注释很大通常会放到一个独立的doc.go文件中。</p>
<p><strong>练习 2.1</strong> 向tempconv包添加类型、常量和函数用来处理Kelvin绝对温度的转换Kelvin 绝对零度是273.15°CKelvin绝对温度1K和摄氏度1°C的单位间隔是一样的。</p>
<h3 id="261-导入包"><a class="header" href="#261-导入包">2.6.1. 导入包</a></h3>
<p>在Go语言程序中每个包都有一个全局唯一的导入路径。导入语句中类似&quot;gopl.io/ch2/tempconv&quot;的字符串对应包的导入路径。Go语言的规范并没有定义这些字符串的具体含义或包来自哪里它们是由构建工具来解释的。当使用Go语言自带的go工具箱时第十章一个导入路径代表一个目录中的一个或多个Go源文件。</p>
<p>除了包的导入路径每个包还有一个包名包名一般是短小的名字并不要求包名是唯一的包名在包的声明处指定。按照惯例一个包的名字和包的导入路径的最后一个字段相同例如gopl.io/ch2/tempconv包的名字一般是tempconv。</p>
<p>要使用gopl.io/ch2/tempconv包需要先导入</p>
<p><u><i>gopl.io/ch2/cf</i></u></p>
<pre><code class="language-Go">// Cf converts its numeric argument to Celsius and Fahrenheit.
package main
import (
&quot;fmt&quot;
&quot;os&quot;
&quot;strconv&quot;
&quot;gopl.io/ch2/tempconv&quot;
)
func main() {
for _, arg := range os.Args[1:] {
t, err := strconv.ParseFloat(arg, 64)
if err != nil {
fmt.Fprintf(os.Stderr, &quot;cf: %v\n&quot;, err)
os.Exit(1)
}
f := tempconv.Fahrenheit(t)
c := tempconv.Celsius(t)
fmt.Printf(&quot;%s = %s, %s = %s\n&quot;,
f, tempconv.FToC(f), c, tempconv.CToF(c))
}
}
</code></pre>
<p>导入语句将导入的包绑定到一个短小的名字然后通过该短小的名字就可以引用包中导出的全部内容。上面的导入声明将允许我们以tempconv.CToF的形式来访问gopl.io/ch2/tempconv包中的内容。在默认情况下导入的包绑定到tempconv名字译注指包声明语句指定的名字但是我们也可以绑定到另一个名称以避免名字冲突§10.4)。</p>
<p>cf程序将命令行输入的一个温度在Celsius和Fahrenheit温度单位之间转换</p>
<pre><code>$ go build gopl.io/ch2/cf
$ ./cf 32
32°F = 0°C, 32°C = 89.6°F
$ ./cf 212
212°F = 100°C, 212°C = 413.6°F
$ ./cf -40
-40°F = -40°C, -40°C = -40°F
</code></pre>
<p>如果导入了一个包但是又没有使用该包将被当作一个编译错误处理。这种强制规则可以有效减少不必要的依赖虽然在调试期间可能会让人讨厌因为删除一个类似log.Print(&quot;got here!&quot;)的打印语句可能导致需要同时删除log包导入声明否则编译器将会发出一个错误。在这种情况下我们需要将不必要的导入删除或注释掉。</p>
<p>不过有更好的解决方案我们可以使用golang.org/x/tools/cmd/goimports导入工具它可以根据需要自动添加或删除导入的包许多编辑器都可以集成goimports工具然后在保存文件的时候自动运行。类似的还有gofmt工具可以用来格式化Go源文件。</p>
<p><strong>练习 2.2</strong> 写一个通用的单位转换程序用类似cf程序的方式从命令行读取参数如果缺省的话则是从标准输入读取参数然后做类似Celsius和Fahrenheit的单位转换长度单位可以对应英尺和米重量单位可以对应磅和公斤等。</p>
<h3 id="262-包的初始化"><a class="header" href="#262-包的初始化">2.6.2. 包的初始化</a></h3>
<p>包的初始化首先是解决包级变量的依赖顺序,然后按照包级变量声明出现的顺序依次初始化:</p>
<pre><code class="language-Go">var a = b + c // a 第三个初始化, 为 3
var b = f() // b 第二个初始化, 为 2, 通过调用 f (依赖c)
var c = 1 // c 第一个初始化, 为 1
func f() int { return c + 1 }
</code></pre>
<p>如果包中含有多个.go源文件它们将按照发给编译器的顺序进行初始化Go语言的构建工具首先会将.go文件根据文件名排序然后依次调用编译器编译。</p>
<p>对于在包级别声明的变量如果有初始化表达式则用表达式初始化还有一些没有初始化表达式的例如某些表格数据初始化并不是一个简单的赋值过程。在这种情况下我们可以用一个特殊的init初始化函数来简化初始化工作。每个文件都可以包含多个init初始化函数</p>
<pre><code class="language-Go">func init() { /* ... */ }
</code></pre>
<p>这样的init初始化函数除了不能被调用或引用外其他行为和普通函数类似。在每个文件中的init初始化函数在程序开始执行时按照它们声明的顺序被自动调用。</p>
<p>每个包在解决依赖的前提下以导入声明的顺序初始化每个包只会被初始化一次。因此如果一个p包导入了q包那么在p包初始化的时候可以认为q包必然已经初始化过了。初始化工作是自下而上进行的main包最后被初始化。以这种方式可以确保在main函数执行之前所有依赖的包都已经完成初始化工作了。</p>
<p>下面的代码定义了一个PopCount函数用于返回一个数字中含二进制1bit的个数。它使用init初始化函数来生成辅助表格pcpc表格用于处理每个8bit宽度的数字含二进制的1bit的bit个数这样的话在处理64bit宽度的数字时就没有必要循环64次只需要8次查表就可以了。这并不是最快的统计1bit数目的算法但是它可以方便演示init函数的用法并且演示了如何预生成辅助表格这是编程中常用的技术</p>
<p><u><i>gopl.io/ch2/popcount</i></u></p>
<pre><code class="language-Go">package popcount
// pc[i] is the population count of i.
var pc [256]byte
func init() {
for i := range pc {
pc[i] = pc[i/2] + byte(i&amp;1)
}
}
// PopCount returns the population count (number of set bits) of x.
func PopCount(x uint64) int {
return int(pc[byte(x&gt;&gt;(0*8))] +
pc[byte(x&gt;&gt;(1*8))] +
pc[byte(x&gt;&gt;(2*8))] +
pc[byte(x&gt;&gt;(3*8))] +
pc[byte(x&gt;&gt;(4*8))] +
pc[byte(x&gt;&gt;(5*8))] +
pc[byte(x&gt;&gt;(6*8))] +
pc[byte(x&gt;&gt;(7*8))])
}
</code></pre>
<p>译注对于pc这类需要复杂处理的初始化可以通过将初始化逻辑包装为一个匿名函数处理像下面这样</p>
<pre><code class="language-Go">// pc[i] is the population count of i.
var pc [256]byte = func() (pc [256]byte) {
for i := range pc {
pc[i] = pc[i/2] + byte(i&amp;1)
}
return
}()
</code></pre>
<p>要注意的是在init函数中range循环只使用了索引省略了没有用到的值部分。循环也可以这样写</p>
<pre><code class="language-Go">for i, _ := range pc {
</code></pre>
<p>我们在下一节和10.5节还将看到其它使用init函数的地方。</p>
<p><strong>练习 2.3</strong> 重写PopCount函数用一个循环代替单一的表达式。比较两个版本的性能。11.4节将展示如何系统地比较两个不同实现的性能。)</p>
<p><strong>练习 2.4</strong> 用移位算法重写PopCount函数每次测试最右边的1bit然后统计总数。比较和查表算法的性能差异。</p>
<p><strong>练习 2.5</strong> 表达式<code>x&amp;(x-1)</code>用于将x的最低的一个非零的bit位清零。使用这个算法重写PopCount函数然后比较性能。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="27-作用域"><a class="header" href="#27-作用域">2.7. 作用域</a></h2>
<p>一个声明语句将程序中的实体和一个名字关联,比如一个函数或一个变量。声明语句的作用域是指源代码中可以有效使用这个名字的范围。</p>
<p>不要将作用域和生命周期混为一谈。声明语句的作用域对应的是一个源代码的文本区域;它是一个编译时的属性。一个变量的生命周期是指程序运行时变量存在的有效时间段,在此时间区域内它可以被程序的其他部分引用;是一个运行时的概念。</p>
<p>句法块是由花括弧所包含的一系列语句就像函数体或循环体花括弧包裹的内容一样。句法块内部声明的名字是无法被外部块访问的。这个块决定了内部声明的名字的作用域范围。我们可以把块block的概念推广到包括其他声明的群组这些声明在代码中并未显式地使用花括号包裹起来我们称之为词法块。对全局的源代码来说存在一个整体的词法块称为全局词法块对于每个包每个for、if和switch语句也都有对应词法块每个switch或select的分支也有独立的词法块当然也包括显式书写的词法块花括弧包含的语句</p>
<p>声明语句对应的词法域决定了作用域范围的大小。对于内置的类型、函数和常量比如int、len和true等是在全局作用域的因此可以在整个程序中直接使用。任何在函数外部也就是包级语法域声明的名字可以在同一个包的任何源文件中访问的。对于导入的包例如tempconv导入的fmt包则是对应源文件级的作用域因此只能在当前的文件中访问导入的fmt包当前包的其它源文件无法访问在当前源文件导入的包。还有许多声明语句比如tempconv.CToF函数中的变量c则是局部作用域的它只能在函数内部甚至只能是局部的某些部分访问。</p>
<p>控制流标号就是break、continue或goto语句后面跟着的那种标号则是函数级的作用域。</p>
<p>一个程序可能包含多个同名的声明只要它们在不同的词法域就没有关系。例如你可以声明一个局部变量和包级的变量同名。或者是像2.3.3节的例子那样你可以将一个函数参数的名字声明为new虽然内置的new是全局作用域的。但是物极必反如果滥用不同词法域可重名的特性的话可能导致程序很难阅读。</p>
<p>当编译器遇到一个名字引用时,它会对其定义进行查找,查找过程从最内层的词法域向全局的作用域进行。如果查找失败,则报告“未声明的名字”这样的错误。如果该名字在内部和外部的块分别声明过,则内部块的声明首先被找到。在这种情况下,内部声明屏蔽了外部同名的声明,让外部的声明的名字无法被访问:</p>
<pre><code class="language-Go">func f() {}
var g = &quot;g&quot;
func main() {
f := &quot;f&quot;
fmt.Println(f) // &quot;f&quot;; local var f shadows package-level func f
fmt.Println(g) // &quot;g&quot;; package-level var
fmt.Println(h) // compile error: undefined: h
}
</code></pre>
<p>在函数中词法域可以深度嵌套因此内部的一个声明可能屏蔽外部的声明。还有许多语法块是if或for等控制流语句构造的。下面的代码有三个不同的变量x因为它们是定义在不同的词法域这个例子只是为了演示作用域规则但不是好的编程风格</p>
<pre><code class="language-Go">func main() {
x := &quot;hello!&quot;
for i := 0; i &lt; len(x); i++ {
x := x[i]
if x != '!' {
x := x + 'A' - 'a'
fmt.Printf(&quot;%c&quot;, x) // &quot;HELLO&quot; (one letter per iteration)
}
}
}
</code></pre>
<p><code>x[i]</code><code>x + 'A' - 'a'</code>声明语句的初始化的表达式中都引用了外部作用域声明的x变量稍后我们会解释这个。注意后面的表达式与unicode.ToUpper并不等价。</p>
<p>正如上面例子所示并不是所有的词法域都显式地对应到由花括弧包含的语句还有一些隐含的规则。上面的for语句创建了两个词法域花括弧包含的是显式的部分是for的循环体部分词法域另外一个隐式的部分则是循环的初始化部分比如用于迭代变量i的初始化。隐式的词法域部分的作用域还包含条件测试部分和循环后的迭代部分<code>i++</code>),当然也包含循环体词法域。</p>
<p>下面的例子同样有三个不同的x变量每个声明在不同的词法域一个在函数体词法域一个在for隐式的初始化词法域一个在for循环体词法域只有两个块是显式创建的</p>
<pre><code class="language-Go">func main() {
x := &quot;hello&quot;
for _, x := range x {
x := x + 'A' - 'a'
fmt.Printf(&quot;%c&quot;, x) // &quot;HELLO&quot; (one letter per iteration)
}
}
</code></pre>
<p>和for循环类似if和switch语句也会在条件部分创建隐式词法域还有它们对应的执行体词法域。下面的if-else测试链演示了x和y的有效作用域范围</p>
<pre><code class="language-Go">if x := f(); x == 0 {
fmt.Println(x)
} else if y := g(x); x == y {
fmt.Println(x, y)
} else {
fmt.Println(x, y)
}
fmt.Println(x, y) // compile error: x and y are not visible here
</code></pre>
<p>第二个if语句嵌套在第一个内部因此第一个if语句条件初始化词法域声明的变量在第二个if中也可以访问。switch语句的每个分支也有类似的词法域规则条件部分为一个隐式词法域然后是每个分支的词法域。</p>
<p>在包级别,声明的顺序并不会影响作用域范围,因此一个先声明的可以引用它自身或者是引用后面的一个声明,这可以让我们定义一些相互嵌套或递归的类型或函数。但是如果一个变量或常量递归引用了自身,则会产生编译错误。</p>
<p>在这个程序中:</p>
<pre><code class="language-Go">if f, err := os.Open(fname); err != nil { // compile error: unused: f
return err
}
f.ReadByte() // compile error: undefined f
f.Close() // compile error: undefined f
</code></pre>
<p>变量f的作用域只在if语句内因此后面的语句将无法引入它这将导致编译错误。你可能会收到一个局部变量f没有声明的错误提示具体错误信息依赖编译器的实现。</p>
<p>通常需要在if之前声明变量这样可以确保后面的语句依然可以访问变量</p>
<pre><code class="language-Go">f, err := os.Open(fname)
if err != nil {
return err
}
f.ReadByte()
f.Close()
</code></pre>
<p>你可能会考虑通过将ReadByte和Close移动到if的else块来解决这个问题</p>
<pre><code class="language-Go">if f, err := os.Open(fname); err != nil {
return err
} else {
// f and err are visible here too
f.ReadByte()
f.Close()
}
</code></pre>
<p>但这不是Go语言推荐的做法Go语言的习惯是在if中处理错误然后直接返回这样可以确保正常执行的语句不需要代码缩进。</p>
<p>要特别注意短变量声明语句的作用域范围考虑下面的程序它的目的是获取当前的工作目录然后保存到一个包级的变量中。这本来可以通过直接调用os.Getwd完成但是将这个从主逻辑中分离出来可能会更好特别是在需要处理错误的时候。函数log.Fatalf用于打印日志信息然后调用os.Exit(1)终止程序。</p>
<pre><code class="language-Go">var cwd string
func init() {
cwd, err := os.Getwd() // compile error: unused: cwd
if err != nil {
log.Fatalf(&quot;os.Getwd failed: %v&quot;, err)
}
}
</code></pre>
<p>虽然cwd在外部已经声明过但是<code>:=</code>语句还是将cwd和err重新声明为新的局部变量。因为内部声明的cwd将屏蔽外部的声明因此上面的代码并不会正确更新包级声明的cwd变量。</p>
<p>由于当前的编译器会检测到局部声明的cwd并没有使用然后报告这可能是一个错误但是这种检测并不可靠。因为一些小的代码变更例如增加一个局部cwd的打印语句就可能导致这种检测失效。</p>
<pre><code class="language-Go">var cwd string
func init() {
cwd, err := os.Getwd() // NOTE: wrong!
if err != nil {
log.Fatalf(&quot;os.Getwd failed: %v&quot;, err)
}
log.Printf(&quot;Working directory = %s&quot;, cwd)
}
</code></pre>
<p>全局的cwd变量依然是没有被正确初始化的而且看似正常的日志输出更是让这个BUG更加隐晦。</p>
<p>有许多方式可以避免出现类似潜在的问题。最直接的方法是通过单独声明err变量来避免使用<code>:=</code>的简短声明方式:</p>
<pre><code class="language-Go">var cwd string
func init() {
var err error
cwd, err = os.Getwd()
if err != nil {
log.Fatalf(&quot;os.Getwd failed: %v&quot;, err)
}
}
</code></pre>
<p>我们已经看到包、文件、声明和语句如何来表达一个程序结构。在下面的两个章节,我们将探讨数据的结构。</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="第3章-基础数据类型"><a class="header" href="#第3章-基础数据类型">第3章 基础数据类型</a></h1>
<p>虽然从底层而言所有的数据都是由比特组成但计算机一般操作的是固定大小的数如整数、浮点数、比特数组、内存地址等。进一步将这些数组织在一起就可表达更多的对象例如数据包、像素点、诗歌甚至其他任何对象。Go语言提供了丰富的数据组织形式这依赖于Go语言内置的数据类型。这些内置的数据类型兼顾了硬件的特性和表达复杂数据结构的便捷性。</p>
<p>Go语言将数据类型分为四类基础类型、复合类型、引用类型和接口类型。本章介绍基础类型包括数字、字符串和布尔型。复合数据类型——数组§4.1和结构体§4.2——是通过组合简单类型来表达更加复杂的数据结构。引用类型包括指针§2.3.2、切片§4.2)、字典§4.3、函数§5、通道§8虽然数据种类很多但它们都是对程序中一个变量或状态的间接引用。这意味着对任一引用类型数据的修改都会影响所有该引用的拷贝。我们将在第7章介绍接口类型。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="31-整型"><a class="header" href="#31-整型">3.1. 整型</a></h2>
<p>Go语言的数值类型包括几种不同大小的整数、浮点数和复数。每种数值类型都决定了对应的大小范围和是否支持正负符号。让我们先从整数类型开始介绍。</p>
<p>Go语言同时提供了有符号和无符号类型的整数运算。这里有int8、int16、int32和int64四种截然不同大小的有符号整数类型分别对应8、16、32、64bit大小的有符号整数与此对应的是uint8、uint16、uint32和uint64四种无符号整数类型。</p>
<p>这里还有两种一般对应特定CPU平台机器字大小的有符号和无符号整数int和uint其中int是应用最广泛的数值类型。这两种类型都有同样的大小32或64bit但是我们不能对此做任何的假设因为不同的编译器即使在相同的硬件平台上可能产生不同的大小。</p>
<p>Unicode字符rune类型是和int32等价的类型通常用于表示一个Unicode码点。这两个名称可以互换使用。同样byte也是uint8类型的等价类型byte类型一般用于强调数值是一个原始的数据而不是一个小的整数。</p>
<p>最后还有一种无符号的整数类型uintptr没有指定具体的bit大小但是足以容纳指针。uintptr类型只有在底层编程时才需要特别是Go语言和C语言函数库或操作系统接口相交互的地方。我们将在第十三章的unsafe包相关部分看到类似的例子。</p>
<p>不管它们的具体大小int、uint和uintptr是不同类型的兄弟类型。其中int和int32也是不同的类型即使int的大小也是32bit在需要将int当作int32类型的地方需要一个显式的类型转换操作反之亦然。</p>
<p>其中有符号整数采用2的补码形式表示也就是最高bit位用来表示符号位一个n-bit的有符号数的值域是从$-2^{n-1}$到$2^{n-1}-1$。无符号整数的所有bit位都用于表示非负数值域是0到$2^n-1$。例如int8类型整数的值域是从-128到127而uint8类型整数的值域是从0到255。</p>
<p>下面是Go语言中关于算术运算、逻辑运算和比较运算的二元运算符它们按照优先级递减的顺序排列</p>
<pre><code>* / % &lt;&lt; &gt;&gt; &amp; &amp;^
+ - | ^
== != &lt; &lt;= &gt; &gt;=
&amp;&amp;
||
</code></pre>
<p>二元运算符有五种优先级。在同一个优先级,使用左优先结合规则,但是使用括号可以明确优先顺序,使用括号也可以用于提升优先级,例如<code>mask &amp; (1 &lt;&lt; 28)</code></p>
<p>对于上表中前两行的运算符,例如+运算符还有一个与赋值相结合的对应运算符+=,可以用于简化赋值语句。</p>
<p>算术运算符<code>+</code><code>-</code><code>*</code><code>/</code>可以适用于整数、浮点数和复数,但是取模运算符%仅用于整数间的运算。对于不同编程语言,%取模运算的行为可能并不相同。在Go语言中%取模运算符的符号和被取模数的符号总是一致的,因此<code>-5%3</code><code>-5%-3</code>结果都是-2。除法运算符<code>/</code>的行为则依赖于操作数是否全为整数,比如<code>5.0/4.0</code>的结果是1.25但是5/4的结果是1因为整数除法会向着0方向截断余数。</p>
<p>一个算术运算的结果不管是有符号或者是无符号的如果需要更多的bit位才能正确表示的话就说明计算结果是溢出了。超出的高位的bit位部分将被丢弃。如果原始的数值是有符号类型而且最左边的bit位是1的话那么最终结果可能是负的例如int8的例子</p>
<pre><code class="language-Go">var u uint8 = 255
fmt.Println(u, u+1, u*u) // &quot;255 0 1&quot;
var i int8 = 127
fmt.Println(i, i+1, i*i) // &quot;127 -128 1&quot;
</code></pre>
<p>两个相同的整数类型可以使用下面的二元比较运算符进行比较;比较表达式的结果是布尔类型。</p>
<pre><code>== 等于
!= 不等于
&lt; 小于
&lt;= 小于等于
&gt; 大于
&gt;= 大于等于
</code></pre>
<p>事实上,布尔型、数字类型和字符串等基本类型都是可比较的,也就是说两个相同类型的值可以用==和!=进行比较。此外,整数、浮点数和字符串可以根据比较结果排序。许多其它类型的值可能是不可比较的,因此也就可能是不可排序的。对于我们遇到的每种类型,我们需要保证规则的一致性。</p>
<p>这里是一元的加法和减法运算符:</p>
<pre><code>+ 一元加法(无效果)
- 负数
</code></pre>
<p>对于整数,+x是0+x的简写-x则是0-x的简写对于浮点数和复数+x就是x-x则是x 的负数。</p>
<p>Go语言还提供了以下的bit位操作运算符前面4个操作运算符并不区分是有符号还是无符号数</p>
<pre><code>&amp; 位运算 AND
| 位运算 OR
^ 位运算 XOR
&amp;^ 位清空AND NOT
&lt;&lt; 左移
&gt;&gt; 右移
</code></pre>
<p>位操作运算符<code>^</code>作为二元运算符时是按位异或XOR当用作一元运算符时表示按位取反也就是说它返回一个每个bit位都取反的数。位操作运算符<code>&amp;^</code>用于按位置零AND NOT如果对应y中bit位为1的话表达式<code>z = x &amp;^ y</code>结果z的对应的bit位为0否则z对应的bit位等于x相应的bit位的值。</p>
<p>下面的代码演示了如何使用位操作解释uint8类型值的8个独立的bit位。它使用了Printf函数的%b参数打印二进制格式的数字其中%08b中08表示打印至少8个字符宽度不足的前缀部分用0填充。</p>
<pre><code class="language-Go">var x uint8 = 1&lt;&lt;1 | 1&lt;&lt;5
var y uint8 = 1&lt;&lt;1 | 1&lt;&lt;2
fmt.Printf(&quot;%08b\n&quot;, x) // &quot;00100010&quot;, the set {1, 5}
fmt.Printf(&quot;%08b\n&quot;, y) // &quot;00000110&quot;, the set {1, 2}
fmt.Printf(&quot;%08b\n&quot;, x&amp;y) // &quot;00000010&quot;, the intersection {1}
fmt.Printf(&quot;%08b\n&quot;, x|y) // &quot;00100110&quot;, the union {1, 2, 5}
fmt.Printf(&quot;%08b\n&quot;, x^y) // &quot;00100100&quot;, the symmetric difference {2, 5}
fmt.Printf(&quot;%08b\n&quot;, x&amp;^y) // &quot;00100000&quot;, the difference {5}
for i := uint(0); i &lt; 8; i++ {
if x&amp;(1&lt;&lt;i) != 0 { // membership test
fmt.Println(i) // &quot;1&quot;, &quot;5&quot;
}
}
fmt.Printf(&quot;%08b\n&quot;, x&lt;&lt;1) // &quot;01000100&quot;, the set {2, 6}
fmt.Printf(&quot;%08b\n&quot;, x&gt;&gt;1) // &quot;00010001&quot;, the set {0, 4}
</code></pre>
<p>6.5节给出了一个可以远大于一个字节的整数集的实现。)</p>
<p><code>x&lt;&lt;n</code><code>x&gt;&gt;n</code>移位运算中决定了移位操作的bit数部分必须是无符号数被操作的x可以是有符号数或无符号数。算术上一个<code>x&lt;&lt;n</code>左移运算等价于乘以$2^n$,一个<code>x&gt;&gt;n</code>右移运算等价于除以$2^n$。</p>
<p>左移运算用零填充右边空缺的bit位无符号数的右移运算也是用0填充左边空缺的bit位但是有符号数的右移运算会用符号位的值填充左边空缺的bit位。因为这个原因最好用无符号运算这样你可以将整数完全当作一个bit位模式处理。</p>
<p>尽管Go语言提供了无符号数的运算但即使数值本身不可能出现负数我们还是倾向于使用有符号的int类型就像数组的长度那样虽然使用uint无符号类型似乎是一个更合理的选择。事实上内置的len函数返回一个有符号的int我们可以像下面例子那样处理逆序循环。</p>
<pre><code class="language-Go">medals := []string{&quot;gold&quot;, &quot;silver&quot;, &quot;bronze&quot;}
for i := len(medals) - 1; i &gt;= 0; i-- {
fmt.Println(medals[i]) // &quot;bronze&quot;, &quot;silver&quot;, &quot;gold&quot;
}
</code></pre>
<p>另一个选择对于上面的例子来说将是灾难性的。如果len函数返回一个无符号数那么i也将是无符号的uint类型然后条件<code>i &gt;= 0</code>则永远为真。在三次迭代之后,也就是<code>i == 0</code>i--语句将不会产生-1而是变成一个uint类型的最大值可能是$2^64-1$然后medals[i]表达式运行时将发生panic异常§5.9也就是试图访问一个slice范围以外的元素。</p>
<p>出于这个原因无符号数往往只有在位运算或其它特殊的运算场景才会使用就像bit集合、分析二进制文件格式或者是哈希和加密操作等。它们通常并不用于仅仅是表达非负数量的场合。</p>
<p>一般来说,需要一个显式的转换将一个值从一种类型转化为另一种类型,并且算术和逻辑运算的二元操作中必须是相同的类型。虽然这偶尔会导致需要很长的表达式,但是它消除了所有和类型相关的问题,而且也使得程序容易理解。</p>
<p>在很多场景,会遇到类似下面代码的常见的错误:</p>
<pre><code class="language-Go">var apples int32 = 1
var oranges int16 = 2
var compote int = apples + oranges // compile error
</code></pre>
<p>当尝试编译这三个语句时,将产生一个错误信息:</p>
<pre><code>invalid operation: apples + oranges (mismatched types int32 and int16)
</code></pre>
<p>这种类型不匹配的问题可以有几种不同的方法修复,最常见方法是将它们都显式转型为一个常见类型:</p>
<pre><code class="language-Go">var compote = int(apples) + int(oranges)
</code></pre>
<p>如2.5节所述对于每种类型T如果转换允许的话类型转换操作T(x)将x转换为T类型。许多整数之间的相互转换并不会改变数值它们只是告诉编译器如何解释这个值。但是对于将一个大尺寸的整数类型转为一个小尺寸的整数类型或者是将一个浮点数转为整数可能会改变数值或丢失精度</p>
<pre><code class="language-Go">f := 3.141 // a float64
i := int(f)
fmt.Println(f, i) // &quot;3.141 3&quot;
f = 1.99
fmt.Println(int(f)) // &quot;1&quot;
</code></pre>
<p>浮点数到整数的转换将丢失任何小数部分,然后向数轴零方向截断。你应该避免对可能会超出目标类型表示范围的数值做类型转换,因为截断的行为可能依赖于具体的实现:</p>
<pre><code class="language-Go">f := 1e100 // a float64
i := int(f) // 结果依赖于具体实现
</code></pre>
<p>任何大小的整数字面值都可以用以0开始的八进制格式书写例如0666或用以0x或0X开头的十六进制格式书写例如0xdeadbeef。十六进制数字可以用大写或小写字母。如今八进制数据通常用于POSIX操作系统上的文件访问权限标志十六进制数字则更强调数字值的bit位模式。</p>
<p>当使用fmt包打印一个数值时我们可以用%d、%o或%x参数控制输出的进制格式就像下面的例子</p>
<pre><code class="language-Go">o := 0666
fmt.Printf(&quot;%d %[1]o %#[1]o\n&quot;, o) // &quot;438 666 0666&quot;
x := int64(0xdeadbeef)
fmt.Printf(&quot;%d %[1]x %#[1]x %#[1]X\n&quot;, x)
// Output:
// 3735928559 deadbeef 0xdeadbeef 0XDEADBEEF
</code></pre>
<p>请注意fmt的两个使用技巧。通常Printf格式化字符串包含多个%参数时将会包含对应相同数量的额外操作数,但是%之后的<code>[1]</code>副词告诉Printf函数再次使用第一个操作数。第二%后的<code>#</code>副词告诉Printf在用%o、%x或%X输出时生成0、0x或0X前缀。</p>
<p>字符面值通过一对单引号直接包含对应字符。最简单的例子是ASCII中类似'a'写法的字符面值但是我们也可以通过转义的数值来表示任意的Unicode码点对应的字符马上将会看到这样的例子。</p>
<p>字符使用<code>%c</code>参数打印,或者是用<code>%q</code>参数打印带单引号的字符:</p>
<pre><code class="language-Go">ascii := 'a'
unicode := '国'
newline := '\n'
fmt.Printf(&quot;%d %[1]c %[1]q\n&quot;, ascii) // &quot;97 a 'a'&quot;
fmt.Printf(&quot;%d %[1]c %[1]q\n&quot;, unicode) // &quot;22269 国 '国'&quot;
fmt.Printf(&quot;%d %[1]q\n&quot;, newline) // &quot;10 '\n'&quot;
</code></pre>
<div style="break-before: page; page-break-before: always;"></div><h2 id="32-浮点数"><a class="header" href="#32-浮点数">3.2. 浮点数</a></h2>
<p>Go语言提供了两种精度的浮点数float32和float64。它们的算术规范由IEEE754浮点数国际标准定义该浮点数规范被所有现代的CPU支持。</p>
<p>这些浮点数类型的取值范围可以从很微小到很巨大。浮点数的范围极限值可以在math包找到。常量math.MaxFloat32表示float32能表示的最大数值大约是 3.4e38对应的math.MaxFloat64常量大约是1.8e308。它们分别能表示的最小值近似为1.4e-45和4.9e-324。</p>
<p>一个float32类型的浮点数可以提供大约6个十进制数的精度而float64则可以提供约15个十进制数的精度通常应该优先使用float64类型因为float32类型的累计计算误差很容易扩散并且float32能精确表示的正整数并不是很大译注因为float32的有效bit位只有23个其它的bit位用于指数和符号当整数大于23bit能表达的范围时float32的表示将出现误差</p>
<pre><code class="language-Go">var f float32 = 16777216 // 1 &lt;&lt; 24
fmt.Println(f == f+1) // &quot;true&quot;!
</code></pre>
<p>浮点数的字面值可以直接写小数部分,像这样:</p>
<pre><code class="language-Go">const e = 2.71828 // (approximately)
</code></pre>
<p>小数点前面或后面的数字都可能被省略(例如.707或1.。很小或很大的数最好用科学计数法书写通过e或E来指定指数部分</p>
<pre><code class="language-Go">const Avogadro = 6.02214129e23 // 阿伏伽德罗常数
const Planck = 6.62606957e-34 // 普朗克常数
</code></pre>
<p>用Printf函数的%g参数打印浮点数将采用更紧凑的表示形式打印并提供足够的精度但是对应表格的数据使用%e带指数或%f的形式打印可能更合适。所有的这三个打印形式都可以指定打印的宽度和控制打印精度。</p>
<pre><code class="language-Go">for x := 0; x &lt; 8; x++ {
fmt.Printf(&quot;x = %d e^x = %8.3f\n&quot;, x, math.Exp(float64(x)))
}
</code></pre>
<p>上面代码打印e的幂打印精度是小数点后三个小数精度和8个字符宽度</p>
<pre><code>x = 0 e^x = 1.000
x = 1 e^x = 2.718
x = 2 e^x = 7.389
x = 3 e^x = 20.086
x = 4 e^x = 54.598
x = 5 e^x = 148.413
x = 6 e^x = 403.429
x = 7 e^x = 1096.633
</code></pre>
<p>math包中除了提供大量常用的数学函数外还提供了IEEE754浮点数标准中定义的特殊值的创建和测试正无穷大和负无穷大分别用于表示太大溢出的数字和除零的结果还有NaN非数一般用于表示无效的除法操作结果0/0或Sqrt(-1).</p>
<pre><code class="language-Go">var z float64
fmt.Println(z, -z, 1/z, -1/z, z/z) // &quot;0 -0 +Inf -Inf NaN&quot;
</code></pre>
<p>函数math.IsNaN用于测试一个数是否是非数NaNmath.NaN则返回非数对应的值。虽然可以用math.NaN来表示一个非法的结果但是测试一个结果是否是非数NaN则是充满风险的因为NaN和任何数都是不相等的译注在浮点数中NaN、正无穷大和负无穷大都不是唯一的每个都有非常多种的bit模式表示</p>
<pre><code class="language-Go">nan := math.NaN()
fmt.Println(nan == nan, nan &lt; nan, nan &gt; nan) // &quot;false false false&quot;
</code></pre>
<p>如果一个函数返回的浮点数结果可能失败,最好的做法是用单独的标志报告失败,像这样:</p>
<pre><code class="language-Go">func compute() (value float64, ok bool) {
// ...
if failed {
return 0, false
}
return result, true
}
</code></pre>
<p>接下来的程序演示了通过浮点计算生成的图形。它是带有两个参数的z = f(x, y)函数的三维形式使用了可缩放矢量图形SVG格式输出SVG是一个用于矢量线绘制的XML标准。图3.1显示了sin(r)/r函数的输出图形其中r是<code>sqrt(x*x+y*y)</code></p>
<p><img src="ch3/../images/ch3-01.png" alt="" /></p>
<p><u><i>gopl.io/ch3/surface</i></u></p>
<pre><code class="language-Go">// Surface computes an SVG rendering of a 3-D surface function.
package main
import (
&quot;fmt&quot;
&quot;math&quot;
)
const (
width, height = 600, 320 // canvas size in pixels
cells = 100 // number of grid cells
xyrange = 30.0 // axis ranges (-xyrange..+xyrange)
xyscale = width / 2 / xyrange // pixels per x or y unit
zscale = height * 0.4 // pixels per z unit
angle = math.Pi / 6 // angle of x, y axes (=30°)
)
var sin30, cos30 = math.Sin(angle), math.Cos(angle) // sin(30°), cos(30°)
func main() {
fmt.Printf(&quot;&lt;svg xmlns='http://www.w3.org/2000/svg' &quot;+
&quot;style='stroke: grey; fill: white; stroke-width: 0.7' &quot;+
&quot;width='%d' height='%d'&gt;&quot;, width, height)
for i := 0; i &lt; cells; i++ {
for j := 0; j &lt; cells; j++ {
ax, ay := corner(i+1, j)
bx, by := corner(i, j)
cx, cy := corner(i, j+1)
dx, dy := corner(i+1, j+1)
fmt.Printf(&quot;&lt;polygon points='%g,%g %g,%g %g,%g %g,%g'/&gt;\n&quot;,
ax, ay, bx, by, cx, cy, dx, dy)
}
}
fmt.Println(&quot;&lt;/svg&gt;&quot;)
}
func corner(i, j int) (float64, float64) {
// Find point (x,y) at corner of cell (i,j).
x := xyrange * (float64(i)/cells - 0.5)
y := xyrange * (float64(j)/cells - 0.5)
// Compute surface height z.
z := f(x, y)
// Project (x,y,z) isometrically onto 2-D SVG canvas (sx,sy).
sx := width/2 + (x-y)*cos30*xyscale
sy := height/2 + (x+y)*sin30*xyscale - z*zscale
return sx, sy
}
func f(x, y float64) float64 {
r := math.Hypot(x, y) // distance from (0,0)
return math.Sin(r) / r
}
</code></pre>
<p>要注意的是corner函数返回了两个结果分别对应每个网格顶点的坐标参数。</p>
<p>要解释这个程序是如何工作的需要一些基本的几何学知识但是我们可以跳过几何学原理因为程序的重点是演示浮点数运算。程序的本质是三个不同的坐标系中映射关系如图3.2所示。第一个是100x100的二维网格对应整数坐标(i,j),从远处的(0,0)位置开始。我们从远处向前面绘制,因此远处先绘制的多边形有可能被前面后绘制的多边形覆盖。</p>
<p>第二个坐标系是一个三维的网格浮点坐标(x,y,z)其中x和y是i和j的线性函数通过平移转换为网格单元的中心然后用xyrange系数缩放。高度z是函数f(x,y)的值。</p>
<p>第三个坐标系是一个二维的画布,起点(0,0)在左上角。画布中点的坐标用(sx,sy)表示。我们使用等角投影将三维点(x,y,z)投影到二维的画布中。</p>
<p><img src="ch3/../images/ch3-02.png" alt="" /></p>
<p>画布中从远处到右边的点对应较大的x值和较大的y值。并且画布中x和y值越大则对应的z值越小。x和y的垂直和水平缩放系数来自30度角的正弦和余弦值。z的缩放系数0.4,是一个任意选择的参数。</p>
<p>对于二维网格中的每一个网格单元main函数计算单元的四个顶点在画布中对应多边形ABCD的顶点其中B对应(i,j)顶点位置A、C和D是其它相邻的顶点然后输出SVG的绘制指令。</p>
<p><strong>练习 3.1</strong> 如果f函数返回的是无限制的float64值那么SVG文件可能输出无效的<polygon>多边形元素虽然许多SVG渲染器会妥善处理这类问题。修改程序跳过无效的多边形。</p>
<p><strong>练习 3.2</strong> 试验math包中其他函数的渲染图形。你是否能输出一个egg box、moguls或a saddle图案?</p>
<p><strong>练习 3.3</strong> 根据高度给每个多边形上色,那样峰值部将是红色(#ff0000谷部将是蓝色#0000ff</p>
<p><strong>练习 3.4</strong> 参考1.7节Lissajous例子的函数构造一个web服务器用于计算函数曲面然后返回SVG数据给客户端。服务器必须设置Content-Type头部</p>
<pre><code class="language-Go">w.Header().Set(&quot;Content-Type&quot;, &quot;image/svg+xml&quot;)
</code></pre>
<p>这一步在Lissajous例子中不是必须的因为服务器使用标准的PNG图像格式可以根据前面的512个字节自动输出对应的头部。允许客户端通过HTTP请求参数设置高度、宽度和颜色等参数。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="33-复数"><a class="header" href="#33-复数">3.3. 复数</a></h2>
<p>Go语言提供了两种精度的复数类型complex64和complex128分别对应float32和float64两种浮点数精度。内置的complex函数用于构建复数内建的real和imag函数分别返回复数的实部和虚部</p>
<pre><code class="language-Go">var x complex128 = complex(1, 2) // 1+2i
var y complex128 = complex(3, 4) // 3+4i
fmt.Println(x*y) // &quot;(-5+10i)&quot;
fmt.Println(real(x*y)) // &quot;-5&quot;
fmt.Println(imag(x*y)) // &quot;10&quot;
</code></pre>
<p>如果一个浮点数面值或一个十进制整数面值后面跟着一个i例如3.141592i或2i它将构成一个复数的虚部复数的实部是0</p>
<pre><code class="language-Go">fmt.Println(1i * 1i) // &quot;(-1+0i)&quot;, i^2 = -1
</code></pre>
<p>在常量算术规则下一个复数常量可以加到另一个普通数值常量整数或浮点数、实部或虚部我们可以用自然的方式书写复数就像1+2i或与之等价的写法2i+1。上面x和y的声明语句还可以简化</p>
<pre><code class="language-Go">x := 1 + 2i
y := 3 + 4i
</code></pre>
<p>复数也可以用==和!=进行相等比较。只有两个复数的实部和虚部都相等的时候它们才是相等的(译注:浮点数的相等比较是危险的,需要特别小心处理精度问题)。</p>
<p>math/cmplx包提供了复数处理的许多函数例如求复数的平方根函数和求幂函数。</p>
<pre><code class="language-Go">fmt.Println(cmplx.Sqrt(-1)) // &quot;(0+1i)&quot;
</code></pre>
<p>下面的程序使用complex128复数算法来生成一个Mandelbrot图像。</p>
<p><u><i>gopl.io/ch3/mandelbrot</i></u></p>
<pre><code class="language-Go">// Mandelbrot emits a PNG image of the Mandelbrot fractal.
package main
import (
&quot;image&quot;
&quot;image/color&quot;
&quot;image/png&quot;
&quot;math/cmplx&quot;
&quot;os&quot;
)
func main() {
const (
xmin, ymin, xmax, ymax = -2, -2, +2, +2
width, height = 1024, 1024
)
img := image.NewRGBA(image.Rect(0, 0, width, height))
for py := 0; py &lt; height; py++ {
y := float64(py)/height*(ymax-ymin) + ymin
for px := 0; px &lt; width; px++ {
x := float64(px)/width*(xmax-xmin) + xmin
z := complex(x, y)
// Image point (px, py) represents complex value z.
img.Set(px, py, mandelbrot(z))
}
}
png.Encode(os.Stdout, img) // NOTE: ignoring errors
}
func mandelbrot(z complex128) color.Color {
const iterations = 200
const contrast = 15
var v complex128
for n := uint8(0); n &lt; iterations; n++ {
v = v*v + z
if cmplx.Abs(v) &gt; 2 {
return color.Gray{255 - contrast*n}
}
}
return color.Black
}
</code></pre>
<p>用于遍历1024x1024图像每个点的两个嵌套的循环对应-2到+2区间的复数平面。程序反复测试每个点对应复数值平方值加一个增量值对应的点是否超出半径为2的圆。如果超过了通过根据预设置的逃逸迭代次数对应的灰度颜色来代替。如果不是那么该点属于Mandelbrot集合使用黑色颜色标记。最终程序将生成的PNG格式分形图像输出到标准输出如图3.3所示。</p>
<p><img src="ch3/../images/ch3-03.png" alt="" /></p>
<p><strong>练习 3.5</strong> 实现一个彩色的Mandelbrot图像使用image.NewRGBA创建图像使用color.RGBA或color.YCbCr生成颜色。</p>
<p><strong>练习 3.6</strong> 升采样技术可以降低每个像素对计算颜色值和平均值的影响。简单的方法是将每个像素分成四个子像素,实现它。</p>
<p><strong>练习 3.7</strong> 另一个生成分形图像的方式是使用牛顿法来求解一个复数方程,例如$z^4-1=0$。每个起点到四个根的迭代次数对应阴影的灰度。方程根对应的点用颜色表示。</p>
<p><strong>练习 3.8</strong> 通过提高精度来生成更多级别的分形。使用四种不同精度类型的数字实现相同的分形complex64、complex128、big.Float和big.Rat。后面两种类型在math/big包声明。Float是有指定限精度的浮点数Rat是无限精度的有理数。它们间的性能和内存使用对比如何当渲染图可见时缩放的级别是多少</p>
<p><strong>练习 3.9</strong> 编写一个web服务器用于给客户端生成分形的图像。运行客户端通过HTTP参数指定x、y和zoom参数。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="34-布尔型"><a class="header" href="#34-布尔型">3.4. 布尔型</a></h2>
<p>一个布尔类型的值只有两种true和false。if和for语句的条件部分都是布尔类型的值并且==和&lt;等比较操作也会产生布尔型的值。一元操作符<code>!</code>对应逻辑非操作,因此<code>!true</code>的值为<code>false</code>,更罗嗦的说法是<code>(!true==false)==true</code>虽然表达方式不一样不过我们一般会采用简洁的布尔表达式就像用x来表示<code>x==true</code></p>
<p>布尔值可以和&amp;&amp;AND和||OR操作符结合并且有短路行为如果运算符左边值已经可以确定整个布尔表达式的值那么运算符右边的值将不再被求值因此下面的表达式总是安全的</p>
<pre><code class="language-Go">s != &quot;&quot; &amp;&amp; s[0] == 'x'
</code></pre>
<p>其中s[0]操作如果应用于空字符串将会导致panic异常。</p>
<p>因为<code>&amp;&amp;</code>的优先级比<code>||</code>高(助记:<code>&amp;&amp;</code>对应逻辑乘法,<code>||</code>对应逻辑加法,乘法比加法优先级要高),下面形式的布尔表达式是不需要加小括弧的:</p>
<pre><code class="language-Go">if 'a' &lt;= c &amp;&amp; c &lt;= 'z' ||
'A' &lt;= c &amp;&amp; c &lt;= 'Z' ||
'0' &lt;= c &amp;&amp; c &lt;= '9' {
// ...ASCII letter or digit...
}
</code></pre>
<p>布尔值并不会隐式转换为数字值0或1反之亦然。必须使用一个显式的if语句辅助转换</p>
<pre><code class="language-Go">i := 0
if b {
i = 1
}
</code></pre>
<p>如果需要经常做类似的转换,包装成一个函数会更方便:</p>
<pre><code class="language-Go">// btoi returns 1 if b is true and 0 if false.
func btoi(b bool) int {
if b {
return 1
}
return 0
}
</code></pre>
<p>数字到布尔型的逆转换则非常简单,不过为了保持对称,我们也可以包装一个函数:</p>
<pre><code class="language-Go">// itob reports whether i is non-zero.
func itob(i int) bool { return i != 0 }
</code></pre>
<div style="break-before: page; page-break-before: always;"></div><h2 id="35-字符串"><a class="header" href="#35-字符串">3.5. 字符串</a></h2>
<p>一个字符串是一个不可改变的字节序列。字符串可以包含任意的数据包括byte值0但是通常是用来包含人类可读的文本。文本字符串通常被解释为采用UTF8编码的Unicode码点rune序列我们稍后会详细讨论这个问题。</p>
<p>内置的len函数可以返回一个字符串中的字节数目不是rune字符数目索引操作s[i]返回第i个字节的字节值i必须满足0 ≤ i&lt; len(s)条件约束。</p>
<pre><code class="language-Go">s := &quot;hello, world&quot;
fmt.Println(len(s)) // &quot;12&quot;
fmt.Println(s[0], s[7]) // &quot;104 119&quot; ('h' and 'w')
</code></pre>
<p>如果试图访问超出字符串索引范围的字节将会导致panic异常</p>
<pre><code class="language-Go">c := s[len(s)] // panic: index out of range
</code></pre>
<p>第i个字节并不一定是字符串的第i个字符因为对于非ASCII字符的UTF8编码会要两个或多个字节。我们先简单说下字符的工作方式。</p>
<p>子字符串操作s[i:j]基于原始的s字符串的第i个字节开始到第j个字节并不包含j本身生成一个新字符串。生成的新字符串将包含j-i个字节。</p>
<pre><code class="language-Go">fmt.Println(s[0:5]) // &quot;hello&quot;
</code></pre>
<p>同样如果索引超出字符串范围或者j小于i的话将导致panic异常。</p>
<p>不管i还是j都可能被忽略当它们被忽略时将采用0作为开始位置采用len(s)作为结束的位置。</p>
<pre><code class="language-Go">fmt.Println(s[:5]) // &quot;hello&quot;
fmt.Println(s[7:]) // &quot;world&quot;
fmt.Println(s[:]) // &quot;hello, world&quot;
</code></pre>
<p>其中+操作符将两个字符串连接构造一个新字符串:</p>
<pre><code class="language-Go">fmt.Println(&quot;goodbye&quot; + s[5:]) // &quot;goodbye, world&quot;
</code></pre>
<p>字符串可以用==和&lt;进行比较;比较通过逐个字节比较完成的,因此比较的结果是字符串自然编码的顺序。</p>
<p>字符串的值是不可变的:一个字符串包含的字节序列永远不会被改变,当然我们也可以给一个字符串变量分配一个新字符串值。可以像下面这样将一个字符串追加到另一个字符串:</p>
<pre><code class="language-Go">s := &quot;left foot&quot;
t := s
s += &quot;, right foot&quot;
</code></pre>
<p>这并不会导致原始的字符串值被改变但是变量s将因为+=语句持有一个新的字符串值但是t依然是包含原先的字符串值。</p>
<pre><code class="language-Go">fmt.Println(s) // &quot;left foot, right foot&quot;
fmt.Println(t) // &quot;left foot&quot;
</code></pre>
<p>因为字符串是不可修改的,因此尝试修改字符串内部数据的操作也是被禁止的:</p>
<pre><code class="language-Go">s[0] = 'L' // compile error: cannot assign to s[0]
</code></pre>
<p>不变性意味着如果两个字符串共享相同的底层数据的话也是安全的这使得复制任何长度的字符串代价是低廉的。同样一个字符串s和对应的子字符串切片s[7:]的操作也可以安全地共享相同的内存,因此字符串切片操作代价也是低廉的。在这两种情况下都没有必要分配新的内存。 图3.4演示了一个字符串和两个子串共享相同的底层数据。</p>
<h3 id="351-字符串面值"><a class="header" href="#351-字符串面值">3.5.1. 字符串面值</a></h3>
<p>字符串值也可以用字符串面值方式编写,只要将一系列字节序列包含在双引号内即可:</p>
<pre><code>&quot;Hello, 世界&quot;
</code></pre>
<p><img src="ch3/../images/ch3-04.png" alt="" /></p>
<p>因为Go语言源文件总是用UTF8编码并且Go语言的文本字符串也以UTF8编码的方式处理因此我们可以将Unicode码点也写到字符串面值中。</p>
<p>在一个双引号包含的字符串面值中,可以用以反斜杠<code>\</code>开头的转义序列插入任意的数据。下面的换行、回车和制表符等是常见的ASCII控制代码的转义方式</p>
<pre><code>\a 响铃
\b 退格
\f 换页
\n 换行
\r 回车
\t 制表符
\v 垂直制表符
\' 单引号(只用在 '\'' 形式的rune符号面值中
\&quot; 双引号(只用在 &quot;...&quot; 形式的字符串面值中)
\\ 反斜杠
</code></pre>
<p>可以通过十六进制或八进制转义在字符串面值中包含任意的字节。一个十六进制的转义形式是<code>\xhh</code>其中两个h表示十六进制数字大写或小写都可以。一个八进制转义形式是<code>\ooo</code>包含三个八进制的o数字0到7但是不能超过<code>\377</code>译注对应一个字节的范围十进制为255。每一个单一的字节表达一个特定的值。稍后我们将看到如何将一个Unicode码点写到字符串面值中。</p>
<p>一个原生的字符串面值形式是`...`,使用反引号代替双引号。在原生的字符串面值中,没有转义操作;全部的内容都是字面的意思,包含退格和换行,因此一个程序中的原生字符串面值可能跨越多行(译注:在原生字符串面值内部是无法直接写`字符的,可以用八进制或十六进制转义或+&quot;`&quot;连接字符串常量完成。唯一的特殊处理是会删除回车以保证在所有平台上的值都是一样的包括那些把回车也放入文本文件的系统译注Windows系统会把回车和换行一起放入文本文件中</p>
<p>原生字符串面值用于编写正则表达式会很方便因为正则表达式往往会包含很多反斜杠。原生字符串面值同时被广泛应用于HTML模板、JSON面值、命令行提示信息以及那些需要扩展到多行的场景。</p>
<pre><code class="language-Go">const GoUsage = `Go is a tool for managing Go source code.
Usage:
go command [arguments]
...`
</code></pre>
<h3 id="352-unicode"><a class="header" href="#352-unicode">3.5.2. Unicode</a></h3>
<p>在很久以前世界还是比较简单的起码计算机世界就只有一个ASCII字符集美国信息交换标准代码。ASCII更准确地说是美国的ASCII使用7bit来表示128个字符包含英文字母的大小写、数字、各种标点符号和设备控制符。对于早期的计算机程序来说这些就足够了但是这也导致了世界上很多其他地区的用户无法直接使用自己的符号系统。随着互联网的发展混合多种语言的数据变得很常见译注比如本身的英文原文或中文翻译都包含了ASCII、中文、日文等多种语言字符。如何有效处理这些包含了各种语言的丰富多样的文本数据呢</p>
<p>答案就是使用Unicode http://unicode.org 它收集了这个世界上所有的符号系统包括重音符号和其它变音符号制表符和回车符还有很多神秘的符号每个符号都分配一个唯一的Unicode码点Unicode码点对应Go语言中的rune整数类型译注rune是int32等价类型</p>
<p>在第八版本的Unicode标准里收集了超过120,000个字符涵盖超过100多种语言。这些在计算机程序和数据中是如何体现的呢通用的表示一个Unicode码点的数据类型是int32也就是Go语言中rune对应的类型它的同义词rune符文正是这个意思。</p>
<p>我们可以将一个符文序列表示为一个int32序列。这种编码方式叫UTF-32或UCS-4每个Unicode码点都使用同样大小的32bit来表示。这种方式比较简单统一但是它会浪费很多存储空间因为大多数计算机可读的文本是ASCII字符本来每个ASCII字符只需要8bit或1字节就能表示。而且即使是常用的字符也远少于65,536个也就是说用16bit编码方式就能表达常用字符。但是还有其它更好的编码方法吗</p>
<h3 id="353-utf-8"><a class="header" href="#353-utf-8">3.5.3. UTF-8</a></h3>
<p>UTF8是一个将Unicode码点编码为字节序列的变长编码。UTF8编码是由Go语言之父Ken Thompson和Rob Pike共同发明的现在已经是Unicode的标准。UTF8编码使用1到4个字节来表示每个Unicode码点ASCII部分字符只使用1个字节常用字符部分使用2或3个字节表示。每个符号编码后第一个字节的高端bit位用于表示编码总共有多少个字节。如果第一个字节的高端bit为0则表示对应7bit的ASCII字符ASCII字符每个字符依然是一个字节和传统的ASCII编码兼容。如果第一个字节的高端bit是110则说明需要2个字节后续的每个高端bit都以10开头。更大的Unicode码点也是采用类似的策略处理。</p>
<pre><code>0xxxxxxx runes 0-127 (ASCII)
110xxxxx 10xxxxxx 128-2047 (values &lt;128 unused)
1110xxxx 10xxxxxx 10xxxxxx 2048-65535 (values &lt;2048 unused)
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 65536-0x10ffff (other values unused)
</code></pre>
<p>变长的编码无法直接通过索引来访问第n个字符但是UTF8编码获得了很多额外的优点。首先UTF8编码比较紧凑完全兼容ASCII码并且可以自动同步它可以通过向前回朔最多3个字节就能确定当前字符编码的开始字节的位置。它也是一个前缀编码所以当从左向右解码时不会有任何歧义也并不需要向前查看译注像GBK之类的编码如果不知道起点位置则可能会出现歧义。没有任何字符的编码是其它字符编码的子串或是其它编码序列的字串因此搜索一个字符时只要搜索它的字节编码序列即可不用担心前后的上下文会对搜索结果产生干扰。同时UTF8编码的顺序和Unicode码点的顺序一致因此可以直接排序UTF8编码序列。同时因为没有嵌入的NUL(0)字节可以很好地兼容那些使用NUL作为字符串结尾的编程语言。</p>
<p>Go语言的源文件采用UTF8编码并且Go语言处理UTF8编码的文本也很出色。unicode包提供了诸多处理rune字符相关功能的函数比如区分字母和数字或者是字母的大写和小写转换等unicode/utf8包则提供了用于rune字符序列的UTF8编码和解码的功能。</p>
<p>有很多Unicode字符很难直接从键盘输入并且还有很多字符有着相似的结构有一些甚至是不可见的字符译注中文和日文就有很多相似但不同的字。Go语言字符串面值中的Unicode转义字符让我们可以通过Unicode码点输入特殊的字符。有两种形式<code>\uhhhh</code>对应16bit的码点值<code>\Uhhhhhhhh</code>对应32bit的码点值其中h是一个十六进制数字一般很少需要使用32bit的形式。每一个对应码点的UTF8编码。例如下面的字母串面值都表示相同的值</p>
<pre><code>&quot;世界&quot;
&quot;\xe4\xb8\x96\xe7\x95\x8c&quot;
&quot;\u4e16\u754c&quot;
&quot;\U00004e16\U0000754c&quot;
</code></pre>
<p>上面三个转义序列都为第一个字符串提供替代写法,但是它们的值都是相同的。</p>
<p>Unicode转义也可以使用在rune字符中。下面三个字符是等价的</p>
<pre><code>'世' '\u4e16' '\U00004e16'
</code></pre>
<p>对于小于256的码点值可以写在一个十六进制转义字节中例如<code>\x41</code>对应字符'A',但是对于更大的码点则必须使用<code>\u</code><code>\U</code>转义形式。因此,<code>\xe4\xb8\x96</code>并不是一个合法的rune字符虽然这三个字节对应一个有效的UTF8编码的码点。</p>
<p>得益于UTF8编码优良的设计诸多字符串操作都不需要解码操作。我们可以不用解码直接测试一个字符串是否是另一个字符串的前缀</p>
<pre><code class="language-Go">func HasPrefix(s, prefix string) bool {
return len(s) &gt;= len(prefix) &amp;&amp; s[:len(prefix)] == prefix
}
</code></pre>
<p>或者是后缀测试:</p>
<pre><code class="language-Go">func HasSuffix(s, suffix string) bool {
return len(s) &gt;= len(suffix) &amp;&amp; s[len(s)-len(suffix):] == suffix
}
</code></pre>
<p>或者是包含子串测试:</p>
<pre><code class="language-Go">func Contains(s, substr string) bool {
for i := 0; i &lt; len(s); i++ {
if HasPrefix(s[i:], substr) {
return true
}
}
return false
}
</code></pre>
<p>对于UTF8编码后文本的处理和原始的字节处理逻辑是一样的。但是对应很多其它编码则并不是这样的。上面的函数都来自strings字符串处理包真实的代码包含了一个用哈希技术优化的Contains 实现。)</p>
<p>另一方面如果我们真的关心每个Unicode字符我们可以使用其它处理方式。考虑前面的第一个例子中的字符串它混合了中西两种字符。图3.5展示了它的内存表示形式。字符串包含13个字节以UTF8形式编码但是只对应9个Unicode字符</p>
<pre><code class="language-Go">import &quot;unicode/utf8&quot;
s := &quot;Hello, 世界&quot;
fmt.Println(len(s)) // &quot;13&quot;
fmt.Println(utf8.RuneCountInString(s)) // &quot;9&quot;
</code></pre>
<p>为了处理这些真实的字符我们需要一个UTF8解码器。unicode/utf8包提供了该功能我们可以这样使用</p>
<pre><code class="language-Go">for i := 0; i &lt; len(s); {
r, size := utf8.DecodeRuneInString(s[i:])
fmt.Printf(&quot;%d\t%c\n&quot;, i, r)
i += size
}
</code></pre>
<p>每一次调用DecodeRuneInString函数都返回一个r和长度r对应字符本身长度对应r采用UTF8编码后的编码字节数目。长度可以用于更新第i个字符在字符串中的字节索引位置。但是这种编码方式是笨拙的我们需要更简洁的语法。幸运的是Go语言的range循环在处理字符串的时候会自动隐式解码UTF8字符串。下面的循环运行如图3.5所示需要注意的是对于非ASCII索引更新的步长将超过1个字节。</p>
<p><img src="ch3/../images/ch3-05.png" alt="" /></p>
<pre><code class="language-Go">for i, r := range &quot;Hello, 世界&quot; {
fmt.Printf(&quot;%d\t%q\t%d\n&quot;, i, r, r)
}
</code></pre>
<p>我们可以使用一个简单的循环来统计字符串中字符的数目,像这样:</p>
<pre><code class="language-Go">n := 0
for _, _ = range s {
n++
}
</code></pre>
<p>像其它形式的循环那样,我们也可以忽略不需要的变量:</p>
<pre><code class="language-Go">n := 0
for range s {
n++
}
</code></pre>
<p>或者我们可以直接调用utf8.RuneCountInString(s)函数。</p>
<p>正如我们前面提到的文本字符串采用UTF8编码只是一种惯例但是对于循环的真正字符串并不是一个惯例这是正确的。如果用于循环的字符串只是一个普通的二进制数据或者是含有错误编码的UTF8数据将会发生什么呢</p>
<p>每一个UTF8字符解码不管是显式地调用utf8.DecodeRuneInString解码或是在range循环中隐式地解码如果遇到一个错误的UTF8编码输入将生成一个特别的Unicode字符<code>\uFFFD</code>,在印刷中这个符号通常是一个黑色六角或钻石形状,里面包含一个白色的问号&quot;?&quot;。当程序遇到这样的一个字符通常是一个危险信号说明输入并不是一个完美没有错误的UTF8字符串。</p>
<p>UTF8字符串作为交换格式是非常方便的但是在程序内部采用rune序列可能更方便因为rune大小一致支持数组索引和方便切割。</p>
<p>将[]rune类型转换应用到UTF8编码的字符串将返回字符串编码的Unicode码点序列</p>
<pre><code class="language-Go">// &quot;program&quot; in Japanese katakana
s := &quot;プログラム&quot;
fmt.Printf(&quot;% x\n&quot;, s) // &quot;e3 83 97 e3 83 ad e3 82 b0 e3 83 a9 e3 83 a0&quot;
r := []rune(s)
fmt.Printf(&quot;%x\n&quot;, r) // &quot;[30d7 30ed 30b0 30e9 30e0]&quot;
</code></pre>
<p>在第一个Printf中的<code>% x</code>参数用于在每个十六进制数字前插入一个空格。)</p>
<p>如果是将一个[]rune类型的Unicode字符slice或数组转为string则对它们进行UTF8编码</p>
<pre><code class="language-Go">fmt.Println(string(r)) // &quot;プログラム&quot;
</code></pre>
<p>将一个整数转型为字符串意思是生成以只包含对应Unicode码点字符的UTF8字符串</p>
<pre><code class="language-Go">fmt.Println(string(65)) // &quot;A&quot;, not &quot;65&quot;
fmt.Println(string(0x4eac)) // &quot;&quot;
</code></pre>
<p>如果对应码点的字符是无效的,则用<code>\uFFFD</code>无效字符作为替换:</p>
<pre><code class="language-Go">fmt.Println(string(1234567)) // &quot;?&quot;
</code></pre>
<h3 id="354-字符串和byte切片"><a class="header" href="#354-字符串和byte切片">3.5.4. 字符串和Byte切片</a></h3>
<p>标准库中有四个包对字符串处理尤为重要bytes、strings、strconv和unicode包。strings包提供了许多如字符串的查询、替换、比较、截断、拆分和合并等功能。</p>
<p>bytes包也提供了很多类似功能的函数但是针对和字符串有着相同结构的[]byte类型。因为字符串是只读的因此逐步构建字符串会导致很多分配和复制。在这种情况下使用bytes.Buffer类型将会更有效稍后我们将展示。</p>
<p>strconv包提供了布尔型、整型数、浮点数和对应字符串的相互转换还提供了双引号转义相关的转换。</p>
<p>unicode包提供了IsDigit、IsLetter、IsUpper和IsLower等类似功能它们用于给字符分类。每个函数有一个单一的rune类型的参数然后返回一个布尔值。而像ToUpper和ToLower之类的转换函数将用于rune字符的大小写转换。所有的这些函数都是遵循Unicode标准定义的字母、数字等分类规范。strings包也有类似的函数它们是ToUpper和ToLower将原始字符串的每个字符都做相应的转换然后返回新的字符串。</p>
<p>下面例子的basename函数灵感源于Unix shell的同名工具。在我们实现的版本中basename(s)将看起来像是系统路径的前缀删除,同时将看似文件类型的后缀名部分删除:</p>
<pre><code class="language-Go">fmt.Println(basename(&quot;a/b/c.go&quot;)) // &quot;c&quot;
fmt.Println(basename(&quot;c.d.go&quot;)) // &quot;c.d&quot;
fmt.Println(basename(&quot;abc&quot;)) // &quot;abc&quot;
</code></pre>
<p>第一个版本并没有使用任何库,全部手工硬编码实现:</p>
<p><u><i>gopl.io/ch3/basename1</i></u></p>
<pre><code class="language-Go">// basename removes directory components and a .suffix.
// e.g., a =&gt; a, a.go =&gt; a, a/b/c.go =&gt; c, a/b.c.go =&gt; b.c
func basename(s string) string {
// Discard last '/' and everything before.
for i := len(s) - 1; i &gt;= 0; i-- {
if s[i] == '/' {
s = s[i+1:]
break
}
}
// Preserve everything before last '.'.
for i := len(s) - 1; i &gt;= 0; i-- {
if s[i] == '.' {
s = s[:i]
break
}
}
return s
}
</code></pre>
<p>这个简化版本使用了strings.LastIndex库函数</p>
<p><u><i>gopl.io/ch3/basename2</i></u></p>
<pre><code class="language-Go">func basename(s string) string {
slash := strings.LastIndex(s, &quot;/&quot;) // -1 if &quot;/&quot; not found
s = s[slash+1:]
if dot := strings.LastIndex(s, &quot;.&quot;); dot &gt;= 0 {
s = s[:dot]
}
return s
}
</code></pre>
<p>path和path/filepath包提供了关于文件路径名更一般的函数操作。使用斜杠分隔路径可以在任何操作系统上工作。斜杠本身不应该用于文件名但是在其他一些领域可能会用于文件名例如URL路径组件。相比之下path/filepath包则使用操作系统本身的路径规则例如POSIX系统使用/foo/bar而Microsoft Windows使用<code>c:\foo\bar</code>等。</p>
<p>让我们继续另一个字符串的例子。函数的功能是将一个表示整数值的字符串每隔三个字符插入一个逗号分隔符例如“12345”处理后成为“12,345”。这个版本只适用于整数类型支持浮点数类型的留作练习。</p>
<p><u><i>gopl.io/ch3/comma</i></u></p>
<pre><code class="language-Go">// comma inserts commas in a non-negative decimal integer string.
func comma(s string) string {
n := len(s)
if n &lt;= 3 {
return s
}
return comma(s[:n-3]) + &quot;,&quot; + s[n-3:]
}
</code></pre>
<p>输入comma函数的参数是一个字符串。如果输入字符串的长度小于或等于3的话则不需要插入逗号分隔符。否则comma函数将在最后三个字符前的位置将字符串切割为两个子串并插入逗号分隔符然后通过递归调用自身来得出前面的子串。</p>
<p>一个字符串是包含只读字节的数组一旦创建是不可变的。相比之下一个字节slice的元素则可以自由地修改。</p>
<p>字符串和字节slice之间可以相互转换</p>
<pre><code class="language-Go">s := &quot;abc&quot;
b := []byte(s)
s2 := string(b)
</code></pre>
<p>从概念上讲,一个[]byte(s)转换是分配了一个新的字节数组用于保存字符串数据的拷贝然后引用这个底层的字节数组。编译器的优化可以避免在一些场景下分配和复制字符串数据但总的来说需要确保在变量b被修改的情况下原始的s字符串也不会改变。将一个字节slice转换到字符串的string(b)操作则是构造一个字符串拷贝以确保s2字符串是只读的。</p>
<p>为了避免转换中不必要的内存分配bytes包和strings同时提供了许多实用函数。下面是strings包中的六个函数</p>
<pre><code class="language-Go">func Contains(s, substr string) bool
func Count(s, sep string) int
func Fields(s string) []string
func HasPrefix(s, prefix string) bool
func Index(s, sep string) int
func Join(a []string, sep string) string
</code></pre>
<p>bytes包中也对应的六个函数</p>
<pre><code class="language-Go">func Contains(b, subslice []byte) bool
func Count(s, sep []byte) int
func Fields(s []byte) [][]byte
func HasPrefix(s, prefix []byte) bool
func Index(s, sep []byte) int
func Join(s [][]byte, sep []byte) []byte
</code></pre>
<p>它们之间唯一的区别是字符串类型参数被替换成了字节slice类型的参数。</p>
<p>bytes包还提供了Buffer类型用于字节slice的缓存。一个Buffer开始是空的但是随着string、byte或[]byte等类型数据的写入可以动态增长一个bytes.Buffer变量并不需要初始化因为零值也是有效的</p>
<p><u><i>gopl.io/ch3/printints</i></u></p>
<pre><code class="language-Go">// intsToString is like fmt.Sprint(values) but adds commas.
func intsToString(values []int) string {
var buf bytes.Buffer
buf.WriteByte('[')
for i, v := range values {
if i &gt; 0 {
buf.WriteString(&quot;, &quot;)
}
fmt.Fprintf(&amp;buf, &quot;%d&quot;, v)
}
buf.WriteByte(']')
return buf.String()
}
func main() {
fmt.Println(intsToString([]int{1, 2, 3})) // &quot;[1, 2, 3]&quot;
}
</code></pre>
<p>当向bytes.Buffer添加任意字符的UTF8编码时最好使用bytes.Buffer的WriteRune方法但是WriteByte方法对于写入类似'['和']'等ASCII字符则会更加有效。</p>
<p>bytes.Buffer类型有着很多实用的功能我们在第七章讨论接口时将会涉及到我们将看看如何将它用作一个I/O的输入和输出对象例如当做Fprintf的io.Writer输出对象或者当作io.Reader类型的输入源对象。</p>
<p><strong>练习 3.10</strong> 编写一个非递归版本的comma函数使用bytes.Buffer代替字符串链接操作。</p>
<p><strong>练习 3.11</strong> 完善comma函数以支持浮点数处理和一个可选的正负号的处理。</p>
<p><strong>练习 3.12</strong> 编写一个函数,判断两个字符串是否是相互打乱的,也就是说它们有着相同的字符,但是对应不同的顺序。</p>
<h3 id="355-字符串和数字的转换"><a class="header" href="#355-字符串和数字的转换">3.5.5. 字符串和数字的转换</a></h3>
<p>除了字符串、字符、字节之间的转换字符串和数值之间的转换也比较常见。由strconv包提供这类转换功能。</p>
<p>将一个整数转为字符串一种方法是用fmt.Sprintf返回一个格式化的字符串另一个方法是用strconv.Itoa(“整数到ASCII”)</p>
<pre><code class="language-Go">x := 123
y := fmt.Sprintf(&quot;%d&quot;, x)
fmt.Println(y, strconv.Itoa(x)) // &quot;123 123&quot;
</code></pre>
<p>FormatInt和FormatUint函数可以用不同的进制来格式化数字</p>
<pre><code class="language-Go">fmt.Println(strconv.FormatInt(int64(x), 2)) // &quot;1111011&quot;
</code></pre>
<p>fmt.Printf函数的%b、%d、%o和%x等参数提供功能往往比strconv包的Format函数方便很多特别是在需要包含有附加额外信息的时候</p>
<pre><code class="language-Go">s := fmt.Sprintf(&quot;x=%b&quot;, x) // &quot;x=1111011&quot;
</code></pre>
<p>如果要将一个字符串解析为整数可以使用strconv包的Atoi或ParseInt函数还有用于解析无符号整数的ParseUint函数</p>
<pre><code class="language-Go">x, err := strconv.Atoi(&quot;123&quot;) // x is an int
y, err := strconv.ParseInt(&quot;123&quot;, 10, 64) // base 10, up to 64 bits
</code></pre>
<p>ParseInt函数的第三个参数是用于指定整型数的大小例如16表示int160则表示int。在任何情况下返回的结果y总是int64类型你可以通过强制类型转换将它转为更小的整数类型。</p>
<p>有时候也会使用fmt.Scanf来解析输入的字符串和数字特别是当字符串和数字混合在一行的时候它可以灵活处理不完整或不规则的输入。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="36-常量"><a class="header" href="#36-常量">3.6. 常量</a></h2>
<p>常量表达式的值在编译期计算而不是在运行期。每种常量的潜在类型都是基础类型boolean、string或数字。</p>
<p>一个常量的声明语句定义了常量的名字,和变量的声明语法类似,常量的值不可修改,这样可以防止在运行期被意外或恶意的修改。例如,常量比变量更适合用于表达像π之类的数学常数,因为它们的值不会发生变化:</p>
<pre><code class="language-Go">const pi = 3.14159 // approximately; math.Pi is a better approximation
</code></pre>
<p>和变量声明一样,可以批量声明多个常量;这比较适合声明一组相关的常量:</p>
<pre><code class="language-Go">const (
e = 2.71828182845904523536028747135266249775724709369995957496696763
pi = 3.14159265358979323846264338327950288419716939937510582097494459
)
</code></pre>
<p>所有常量的运算都可以在编译期完成,这样可以减少运行时的工作,也方便其他编译优化。当操作数是常量时,一些运行时的错误也可以在编译时被发现,例如整数除零、字符串索引越界、任何导致无效浮点数的操作等。</p>
<p>常量间的所有算术运算、逻辑运算和比较运算的结果也是常量对常量的类型转换操作或以下函数调用都是返回常量结果len、cap、real、imag、complex和unsafe.Sizeof§13.1)。</p>
<p>因为它们的值是在编译期就确定的,因此常量可以是构成类型的一部分,例如用于指定数组类型的长度:</p>
<pre><code class="language-Go">const IPv4Len = 4
// parseIPv4 parses an IPv4 address (d.d.d.d).
func parseIPv4(s string) IP {
var p [IPv4Len]byte
// ...
}
</code></pre>
<p>一个常量的声明也可以包含一个类型和一个值但是如果没有显式指明类型那么将从右边的表达式推断类型。在下面的代码中time.Duration是一个命名类型底层类型是int64time.Minute是对应类型的常量。下面声明的两个常量都是time.Duration类型可以通过%T参数打印类型信息</p>
<pre><code class="language-Go">const noDelay time.Duration = 0
const timeout = 5 * time.Minute
fmt.Printf(&quot;%T %[1]v\n&quot;, noDelay) // &quot;time.Duration 0&quot;
fmt.Printf(&quot;%T %[1]v\n&quot;, timeout) // &quot;time.Duration 5m0s&quot;
fmt.Printf(&quot;%T %[1]v\n&quot;, time.Minute) // &quot;time.Duration 1m0s&quot;
</code></pre>
<p>如果是批量声明的常量,除了第一个外其它的常量右边的初始化表达式都可以省略,如果省略初始化表达式则表示使用前面常量的初始化表达式写法,对应的常量类型也一样的。例如:</p>
<pre><code class="language-Go">const (
a = 1
b
c = 2
d
)
fmt.Println(a, b, c, d) // &quot;1 1 2 2&quot;
</code></pre>
<p>如果只是简单地复制右边的常量表达式其实并没有太实用的价值。但是它可以带来其它的特性那就是iota常量生成器语法。</p>
<h3 id="361-iota-常量生成器"><a class="header" href="#361-iota-常量生成器">3.6.1. iota 常量生成器</a></h3>
<p>常量声明可以使用iota常量生成器初始化它用于生成一组以相似规则初始化的常量但是不用每行都写一遍初始化表达式。在一个const声明语句中在第一个声明的常量所在的行iota将会被置为0然后在每一个有常量声明的行加一。</p>
<p>下面是来自time包的例子它首先定义了一个Weekday命名类型然后为一周的每天定义了一个常量从周日0开始。在其它编程语言中这种类型一般被称为枚举类型。</p>
<pre><code class="language-Go">type Weekday int
const (
Sunday Weekday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)
</code></pre>
<p>周日将对应0周一为1如此等等。</p>
<p>我们也可以在复杂的常量表达式中使用iota下面是来自net包的例子用于给一个无符号整数的最低5bit的每个bit指定一个名字</p>
<pre><code class="language-Go">type Flags uint
const (
FlagUp Flags = 1 &lt;&lt; iota // is up
FlagBroadcast // supports broadcast access capability
FlagLoopback // is a loopback interface
FlagPointToPoint // belongs to a point-to-point link
FlagMulticast // supports multicast access capability
)
</code></pre>
<p>随着iota的递增每个常量对应表达式1 &lt;&lt; iota是连续的2的幂分别对应一个bit位置。使用这些常量可以用于测试、设置或清除对应的bit位的值</p>
<p><u><i>gopl.io/ch3/netflag</i></u></p>
<pre><code class="language-Go">func IsUp(v Flags) bool { return v&amp;FlagUp == FlagUp }
func TurnDown(v *Flags) { *v &amp;^= FlagUp }
func SetBroadcast(v *Flags) { *v |= FlagBroadcast }
func IsCast(v Flags) bool { return v&amp;(FlagBroadcast|FlagMulticast) != 0 }
func main() {
var v Flags = FlagMulticast | FlagUp
fmt.Printf(&quot;%b %t\n&quot;, v, IsUp(v)) // &quot;10001 true&quot;
TurnDown(&amp;v)
fmt.Printf(&quot;%b %t\n&quot;, v, IsUp(v)) // &quot;10000 false&quot;
SetBroadcast(&amp;v)
fmt.Printf(&quot;%b %t\n&quot;, v, IsUp(v)) // &quot;10010 false&quot;
fmt.Printf(&quot;%b %t\n&quot;, v, IsCast(v)) // &quot;10010 true&quot;
}
</code></pre>
<p>下面是一个更复杂的例子每个常量都是1024的幂</p>
<pre><code class="language-Go">const (
_ = 1 &lt;&lt; (10 * iota)
KiB // 1024
MiB // 1048576
GiB // 1073741824
TiB // 1099511627776 (exceeds 1 &lt;&lt; 32)
PiB // 1125899906842624
EiB // 1152921504606846976
ZiB // 1180591620717411303424 (exceeds 1 &lt;&lt; 64)
YiB // 1208925819614629174706176
)
</code></pre>
<p>不过iota常量生成规则也有其局限性。例如它并不能用于产生1000的幂KB、MB等因为Go语言并没有计算幂的运算符。</p>
<p><strong>练习 3.13</strong> 编写KB、MB的常量声明然后扩展到YB。</p>
<h3 id="362-无类型常量"><a class="header" href="#362-无类型常量">3.6.2. 无类型常量</a></h3>
<p>Go语言的常量有个不同寻常之处。虽然一个常量可以有任意一个确定的基础类型例如int或float64或者是类似time.Duration这样命名的基础类型但是许多常量并没有一个明确的基础类型。编译器为这些没有明确基础类型的数字常量提供比基础类型更高精度的算术运算你可以认为至少有256bit的运算精度。这里有六种未明确类型的常量类型分别是无类型的布尔型、无类型的整数、无类型的字符、无类型的浮点数、无类型的复数、无类型的字符串。</p>
<p>通过延迟明确常量的具体类型无类型的常量不仅可以提供更高的运算精度而且可以直接用于更多的表达式而不需要显式的类型转换。例如例子中的ZiB和YiB的值已经超出任何Go语言中整数类型能表达的范围但是它们依然是合法的常量而且像下面的常量表达式依然有效译注YiB/ZiB是在编译期计算出来的并且结果常量是1024是Go语言int变量能有效表示的</p>
<pre><code class="language-Go">fmt.Println(YiB/ZiB) // &quot;1024&quot;
</code></pre>
<p>另一个例子math.Pi无类型的浮点数常量可以直接用于任意需要浮点数或复数的地方</p>
<pre><code class="language-Go">var x float32 = math.Pi
var y float64 = math.Pi
var z complex128 = math.Pi
</code></pre>
<p>如果math.Pi被确定为特定类型比如float64那么结果精度可能会不一样同时对于需要float32或complex128类型值的地方则会强制需要一个明确的类型转换</p>
<pre><code class="language-Go">const Pi64 float64 = math.Pi
var x float32 = float32(Pi64)
var y float64 = Pi64
var z complex128 = complex128(Pi64)
</code></pre>
<p>对于常量面值不同的写法可能会对应不同的类型。例如0、0.0、0i和<code>\u0000</code>虽然有着相同的常量值但是它们分别对应无类型的整数、无类型的浮点数、无类型的复数和无类型的字符等不同的常量类型。同样true和false也是无类型的布尔类型字符串面值常量是无类型的字符串类型。</p>
<p>前面说过除法运算符/会根据操作数的类型生成对应类型的结果。因此,不同写法的常量除法表达式可能对应不同的结果:</p>
<pre><code class="language-Go">var f float64 = 212
fmt.Println((f - 32) * 5 / 9) // &quot;100&quot;; (f - 32) * 5 is a float64
fmt.Println(5 / 9 * (f - 32)) // &quot;0&quot;; 5/9 is an untyped integer, 0
fmt.Println(5.0 / 9.0 * (f - 32)) // &quot;100&quot;; 5.0/9.0 is an untyped float
</code></pre>
<p>只有常量可以是无类型的。当一个无类型的常量被赋值给一个变量的时候,就像下面的第一行语句,或者出现在有明确类型的变量声明的右边,如下面的其余三行语句,无类型的常量将会被隐式转换为对应的类型,如果转换合法的话。</p>
<pre><code class="language-Go">var f float64 = 3 + 0i // untyped complex -&gt; float64
f = 2 // untyped integer -&gt; float64
f = 1e123 // untyped floating-point -&gt; float64
f = 'a' // untyped rune -&gt; float64
</code></pre>
<p>上面的语句相当于:</p>
<pre><code class="language-Go">var f float64 = float64(3 + 0i)
f = float64(2)
f = float64(1e123)
f = float64('a')
</code></pre>
<p>无论是隐式或显式转换,将一种类型转换为另一种类型都要求目标可以表示原始值。对于浮点数和复数,可能会有舍入处理:</p>
<pre><code class="language-Go">const (
deadbeef = 0xdeadbeef // untyped int with value 3735928559
a = uint32(deadbeef) // uint32 with value 3735928559
b = float32(deadbeef) // float32 with value 3735928576 (rounded up)
c = float64(deadbeef) // float64 with value 3735928559 (exact)
d = int32(deadbeef) // compile error: constant overflows int32
e = float64(1e309) // compile error: constant overflows float64
f = uint(-1) // compile error: constant underflows uint
)
</code></pre>
<p>对于一个没有显式类型的变量声明(包括简短变量声明),常量的形式将隐式决定变量的默认类型,就像下面的例子:</p>
<pre><code class="language-Go">i := 0 // untyped integer; implicit int(0)
r := '\000' // untyped rune; implicit rune('\000')
f := 0.0 // untyped floating-point; implicit float64(0.0)
c := 0i // untyped complex; implicit complex128(0i)
</code></pre>
<p>注意有一点不同无类型整数常量转换为int它的内存大小是不确定的但是无类型浮点数和复数常量则转换为内存大小明确的float64和complex128。
如果不知道浮点数类型的内存大小是很难写出正确的数值算法的因此Go语言不存在整型类似的不确定内存大小的浮点数和复数类型。</p>
<p>如果要给变量一个不同的类型,我们必须显式地将无类型的常量转化为所需的类型,或给声明的变量指定明确的类型,像下面例子这样:</p>
<pre><code class="language-Go">var i = int8(0)
var i int8 = 0
</code></pre>
<p>当尝试将这些无类型的常量转为一个接口值时见第7章这些默认类型将显得尤为重要因为要靠它们明确接口对应的动态类型。</p>
<pre><code class="language-Go">fmt.Printf(&quot;%T\n&quot;, 0) // &quot;int&quot;
fmt.Printf(&quot;%T\n&quot;, 0.0) // &quot;float64&quot;
fmt.Printf(&quot;%T\n&quot;, 0i) // &quot;complex128&quot;
fmt.Printf(&quot;%T\n&quot;, '\000') // &quot;int32&quot; (rune)
</code></pre>
<p>现在我们已经讲述了Go语言中全部的基础数据类型。下一步将演示如何用基础数据类型组合成数组或结构体等复杂数据类型然后构建用于解决实际编程问题的数据结构这将是第四章的讨论主题。</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="第4章-复合数据类型"><a class="header" href="#第4章-复合数据类型">第4章 复合数据类型</a></h1>
<p>在第三章我们讨论了基本数据类型它们可以用于构建程序中数据的结构是Go语言世界的原子。在本章我们将讨论复合数据类型它是以不同的方式组合基本类型而构造出来的复合数据类型。我们主要讨论四种类型——数组、slice、map和结构体——同时在本章的最后我们将演示如何使用结构体来解码和编码到对应JSON格式的数据并且通过结合使用模板来生成HTML页面。</p>
<p>数组和结构体是聚合类型它们的值由许多元素或成员字段的值组成。数组是由同构的元素组成——每个数组元素都是完全相同的类型——结构体则是由异构的元素组成的。数组和结构体都是有固定内存大小的数据结构。相比之下slice和map则是动态的数据结构它们将根据需要动态增长。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="41-数组"><a class="header" href="#41-数组">4.1. 数组</a></h2>
<p>数组是一个由固定长度的特定类型元素组成的序列一个数组可以由零个或多个元素组成。因为数组的长度是固定的因此在Go语言中很少直接使用数组。和数组对应的类型是Slice切片它是可以增长和收缩的动态序列slice功能也更灵活但是要理解slice工作原理的话需要先理解数组。</p>
<p>数组的每个元素可以通过索引下标来访问索引下标的范围是从0开始到数组长度减1的位置。内置的len函数将返回数组中元素的个数。</p>
<pre><code class="language-Go">var a [3]int // array of 3 integers
fmt.Println(a[0]) // print the first element
fmt.Println(a[len(a)-1]) // print the last element, a[2]
// Print the indices and elements.
for i, v := range a {
fmt.Printf(&quot;%d %d\n&quot;, i, v)
}
// Print the elements only.
for _, v := range a {
fmt.Printf(&quot;%d\n&quot;, v)
}
</code></pre>
<p>默认情况下数组的每个元素都被初始化为元素类型对应的零值对于数字类型来说就是0。我们也可以使用数组字面值语法用一组值来初始化数组</p>
<pre><code class="language-Go">var q [3]int = [3]int{1, 2, 3}
var r [3]int = [3]int{1, 2}
fmt.Println(r[2]) // &quot;0&quot;
</code></pre>
<p>在数组字面值中,如果在数组的长度位置出现的是“...”省略号则表示数组的长度是根据初始化值的个数来计算。因此上面q数组的定义可以简化为</p>
<pre><code class="language-Go">q := [...]int{1, 2, 3}
fmt.Printf(&quot;%T\n&quot;, q) // &quot;[3]int&quot;
</code></pre>
<p>数组的长度是数组类型的一个组成部分,因此[3]int和[4]int是两种不同的数组类型。数组的长度必须是常量表达式因为数组的长度需要在编译阶段确定。</p>
<pre><code class="language-Go">q := [3]int{1, 2, 3}
q = [4]int{1, 2, 3, 4} // compile error: cannot assign [4]int to [3]int
</code></pre>
<p>我们将会发现数组、slice、map和结构体字面值的写法都很相似。上面的形式是直接提供顺序初始化值序列但是也可以指定一个索引和对应值列表的方式初始化就像下面这样</p>
<pre><code class="language-Go">type Currency int
const (
USD Currency = iota // 美元
EUR // 欧元
GBP // 英镑
RMB // 人民币
)
symbol := [...]string{USD: &quot;$&quot;, EUR: &quot;&quot;, GBP: &quot;&quot;, RMB: &quot;&quot;}
fmt.Println(RMB, symbol[RMB]) // &quot;3 ¥&quot;
</code></pre>
<p>在这种形式的数组字面值形式中,初始化索引的顺序是无关紧要的,而且没用到的索引可以省略,和前面提到的规则一样,未指定初始值的元素将用零值初始化。例如,</p>
<pre><code class="language-Go">r := [...]int{99: -1}
</code></pre>
<p>定义了一个含有100个元素的数组r最后一个元素被初始化为-1其它元素都是用0初始化。</p>
<p>如果一个数组的元素类型是可以相互比较的,那么数组类型也是可以相互比较的,这时候我们可以直接通过==比较运算符来比较两个数组,只有当两个数组的所有元素都是相等的时候数组才是相等的。不相等比较运算符!=遵循同样的规则。</p>
<pre><code class="language-Go">a := [2]int{1, 2}
b := [...]int{1, 2}
c := [2]int{1, 3}
fmt.Println(a == b, a == c, b == c) // &quot;true false false&quot;
d := [3]int{1, 2}
fmt.Println(a == d) // compile error: cannot compare [2]int == [3]int
</code></pre>
<p>作为一个真实的例子crypto/sha256包的Sum256函数对一个任意的字节slice类型的数据生成一个对应的消息摘要。消息摘要有256bit大小因此对应[32]byte数组类型。如果两个消息摘要是相同的那么可以认为两个消息本身也是相同译注理论上有HASH码碰撞的情况但是实际应用可以基本忽略如果消息摘要不同那么消息本身必然也是不同的。下面的例子用SHA256算法分别生成“x”和“X”两个信息的摘要</p>
<p><u><i>gopl.io/ch4/sha256</i></u></p>
<pre><code class="language-Go">import &quot;crypto/sha256&quot;
func main() {
c1 := sha256.Sum256([]byte(&quot;x&quot;))
c2 := sha256.Sum256([]byte(&quot;X&quot;))
fmt.Printf(&quot;%x\n%x\n%t\n%T\n&quot;, c1, c2, c1 == c2, c1)
// Output:
// 2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881
// 4b68ab3847feda7d6c62c1fbcbeebfa35eab7351ed5e78f4ddadea5df64b8015
// false
// [32]uint8
}
</code></pre>
<p>上面例子中两个消息虽然只有一个字符的差异但是生成的消息摘要则几乎有一半的bit位是不相同的。需要注意Printf函数的%x副词参数它用于指定以十六进制的格式打印数组或slice全部的元素%t副词参数是用于打印布尔型数据%T副词参数是用于显示一个值对应的数据类型。</p>
<p>当调用一个函数的时候函数的每个调用参数将会被赋值给函数内部的参数变量所以函数参数变量接收的是一个复制的副本并不是原始调用的变量。因为函数参数传递的机制导致传递大的数组类型将是低效的并且对数组参数的任何的修改都是发生在复制的数组上并不能直接修改调用时原始的数组变量。在这个方面Go语言对待数组的方式和其它很多编程语言不同其它编程语言可能会隐式地将数组作为引用或指针对象传入被调用的函数。</p>
<p>当然,我们可以显式地传入一个数组指针,那样的话函数通过指针对数组的任何修改都可以直接反馈到调用者。下面的函数用于给[32]byte类型的数组清零</p>
<pre><code class="language-Go">func zero(ptr *[32]byte) {
for i := range ptr {
ptr[i] = 0
}
}
</code></pre>
<p>其实数组字面值[32]byte{}就可以生成一个32字节的数组。而且每个数组的元素都是零值初始化也就是0。因此我们可以将上面的zero函数写的更简洁一点</p>
<pre><code class="language-Go">func zero(ptr *[32]byte) {
*ptr = [32]byte{}
}
</code></pre>
<p>虽然通过指针来传递数组参数是高效的而且也允许在函数内部修改数组的值但是数组依然是僵化的类型因为数组的类型包含了僵化的长度信息。上面的zero函数并不能接收指向[16]byte类型数组的指针而且也没有任何添加或删除数组元素的方法。由于这些原因除了像SHA256这类需要处理特定大小数组的特例外数组依然很少用作函数参数相反我们一般使用slice来替代数组。</p>
<p><strong>练习 4.1</strong> 编写一个函数计算两个SHA256哈希码中不同bit的数目。参考2.6.2节的PopCount函数。)</p>
<p><strong>练习 4.2</strong> 编写一个程序默认情况下打印标准输入的SHA256编码并支持通过命令行flag定制输出SHA384或SHA512哈希算法。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="42-slice"><a class="header" href="#42-slice">4.2. Slice</a></h2>
<p>Slice切片代表变长的序列序列中每个元素都有相同的类型。一个slice类型一般写作[]T其中T代表slice中元素的类型slice的语法和数组很像只是没有固定长度而已。</p>
<p>数组和slice之间有着紧密的联系。一个slice是一个轻量级的数据结构提供了访问数组子序列或者全部元素的功能而且slice的底层确实引用一个数组对象。一个slice由三个部分构成指针、长度和容量。指针指向第一个slice元素对应的底层数组元素的地址要注意的是slice的第一个元素并不一定就是数组的第一个元素。长度对应slice中元素的数目长度不能超过容量容量一般是从slice的开始位置到底层数据的结尾位置。内置的len和cap函数分别返回slice的长度和容量。</p>
<p>多个slice之间可以共享底层的数据并且引用的数组部分区间可能重叠。图4.1显示了表示一年中每个月份名字的字符串数组还有重叠引用了该数组的两个slice。数组这样定义</p>
<pre><code class="language-Go">months := [...]string{1: &quot;January&quot;, /* ... */, 12: &quot;December&quot;}
</code></pre>
<p>因此一月份是months[1]十二月份是months[12]。通常数组的第一个元素从索引0开始但是月份一般是从1开始的因此我们声明数组时直接跳过第0个元素第0个元素会被自动初始化为空字符串。</p>
<p>slice的切片操作s[i:j]其中0 ≤ i≤ j≤ cap(s)用于创建一个新的slice引用s的从第i个元素开始到第j-1个元素的子序列。新的slice将只有j-i个元素。如果i位置的索引被省略的话将使用0代替如果j位置的索引被省略的话将使用len(s)代替。因此months[1:13]切片操作将引用全部有效的月份和months[1:]操作等价months[:]切片操作则是引用整个数组。让我们分别定义表示第二季度和北方夏天月份的slice它们有重叠部分</p>
<p><img src="ch4/../images/ch4-01.png" alt="" /></p>
<pre><code class="language-Go">Q2 := months[4:7]
summer := months[6:9]
fmt.Println(Q2) // [&quot;April&quot; &quot;May&quot; &quot;June&quot;]
fmt.Println(summer) // [&quot;June&quot; &quot;July&quot; &quot;August&quot;]
</code></pre>
<p>两个slice都包含了六月份下面的代码是一个包含相同月份的测试性能较低</p>
<pre><code class="language-Go">for _, s := range summer {
for _, q := range Q2 {
if s == q {
fmt.Printf(&quot;%s appears in both\n&quot;, s)
}
}
}
</code></pre>
<p>如果切片操作超出cap(s)的上限将导致一个panic异常但是超出len(s)则是意味着扩展了slice因为新slice的长度会变大</p>
<pre><code class="language-Go">fmt.Println(summer[:20]) // panic: out of range
endlessSummer := summer[:5] // extend a slice (within capacity)
fmt.Println(endlessSummer) // &quot;[June July August September October]&quot;
</code></pre>
<p>另外,字符串的切片操作和[]byte字节类型切片的切片操作是类似的。都写作x[m:n]并且都是返回一个原始字节序列的子序列底层都是共享之前的底层数组因此这种操作都是常量时间复杂度。x[m:n]切片操作对于字符串则生成一个新字符串如果x是[]byte的话则生成一个新的[]byte。</p>
<p>因为slice值包含指向第一个slice元素的指针因此向函数传递slice将允许在函数内部修改底层数组的元素。换句话说复制一个slice只是对底层的数组创建了一个新的slice别名§2.3.2。下面的reverse函数在原内存空间将[]int类型的slice反转而且它可以用于任意长度的slice。</p>
<p><u><i>gopl.io/ch4/rev</i></u></p>
<pre><code class="language-Go">// reverse reverses a slice of ints in place.
func reverse(s []int) {
for i, j := 0, len(s)-1; i &lt; j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
}
</code></pre>
<p>这里我们反转数组的应用:</p>
<pre><code class="language-Go">a := [...]int{0, 1, 2, 3, 4, 5}
reverse(a[:])
fmt.Println(a) // &quot;[5 4 3 2 1 0]&quot;
</code></pre>
<p>一种将slice元素循环向左旋转n个元素的方法是三次调用reverse反转函数第一次是反转开头的n个元素然后是反转剩下的元素最后是反转整个slice的元素。如果是向右循环旋转则将第三个函数调用移到第一个调用位置就可以了。</p>
<pre><code class="language-Go">s := []int{0, 1, 2, 3, 4, 5}
// Rotate s left by two positions.
reverse(s[:2])
reverse(s[2:])
reverse(s)
fmt.Println(s) // &quot;[2 3 4 5 0 1]&quot;
</code></pre>
<p>要注意的是slice类型的变量s和数组类型的变量a的初始化语法的差异。slice和数组的字面值语法很类似它们都是用花括弧包含一系列的初始化元素但是对于slice并没有指明序列的长度。这会隐式地创建一个合适大小的数组然后slice的指针指向底层的数组。就像数组字面值一样slice的字面值也可以按顺序指定初始化值序列或者是通过索引和元素值指定或者用两种风格的混合语法初始化。</p>
<p>和数组不同的是slice之间不能比较因此我们不能使用==操作符来判断两个slice是否含有全部相等元素。不过标准库提供了高度优化的bytes.Equal函数来判断两个字节型slice是否相等[]byte但是对于其他类型的slice我们必须自己展开每个元素进行比较</p>
<pre><code class="language-Go">func equal(x, y []string) bool {
if len(x) != len(y) {
return false
}
for i := range x {
if x[i] != y[i] {
return false
}
}
return true
}
</code></pre>
<p>上面关于两个slice的深度相等测试运行的时间并不比支持==操作的数组或字符串更多但是为何slice不直接支持比较运算符呢这方面有两个原因。第一个原因一个slice的元素是间接引用的一个slice甚至可以包含自身译注当slice声明为[]interface{}时slice的元素可以是自身。虽然有很多办法处理这种情形但是没有一个是简单有效的。</p>
<p>第二个原因因为slice的元素是间接引用的一个固定的slice值译注指slice本身的值不是元素的值在不同的时刻可能包含不同的元素因为底层数组的元素可能会被修改。而例如Go语言中map的key只做简单的浅拷贝它要求key在整个生命周期内保持不变性译注例如slice扩容就会导致其本身的值/地址变化。而用深度相等判断的话显然在map的key这种场合不合适。对于像指针或chan之类的引用类型==相等测试可以判断两个是否是引用相同的对象。一个针对slice的浅相等测试的==操作符可能是有一定用处的也能临时解决map类型的key问题但是slice和数组不同的相等测试行为会让人困惑。因此安全的做法是直接禁止slice之间的比较操作。</p>
<p>slice唯一合法的比较操作是和nil比较例如</p>
<pre><code class="language-Go">if summer == nil { /* ... */ }
</code></pre>
<p>一个零值的slice等于nil。一个nil值的slice并没有底层数组。一个nil值的slice的长度和容量都是0但是也有非nil值的slice的长度和容量也是0的例如[]int{}或make([]int, 3)[3:]。与任意类型的nil值一样我们可以用[]int(nil)类型转换表达式来生成一个对应类型slice的nil值。</p>
<pre><code class="language-Go">var s []int // len(s) == 0, s == nil
s = nil // len(s) == 0, s == nil
s = []int(nil) // len(s) == 0, s == nil
s = []int{} // len(s) == 0, s != nil
</code></pre>
<p>如果你需要测试一个slice是否是空的使用len(s) == 0来判断而不应该用s == nil来判断。除了和nil相等比较外一个nil值的slice的行为和其它任意0长度的slice一样例如reverse(nil)也是安全的。除了文档已经明确说明的地方所有的Go语言函数应该以相同的方式对待nil值的slice和0长度的slice。</p>
<p>内置的make函数创建一个指定元素类型、长度和容量的slice。容量部分可以省略在这种情况下容量将等于长度。</p>
<pre><code class="language-Go">make([]T, len)
make([]T, len, cap) // same as make([]T, cap)[:len]
</code></pre>
<p>在底层make创建了一个匿名的数组变量然后返回一个slice只有通过返回的slice才能引用底层匿名的数组变量。在第一种语句中slice是整个数组的view。在第二个语句中slice只引用了底层数组的前len个元素但是容量将包含整个的数组。额外的元素是留给未来的增长用的。</p>
<h3 id="421-append函数"><a class="header" href="#421-append函数">4.2.1. append函数</a></h3>
<p>内置的append函数用于向slice追加元素</p>
<pre><code class="language-Go">var runes []rune
for _, r := range &quot;Hello, 世界&quot; {
runes = append(runes, r)
}
fmt.Printf(&quot;%q\n&quot;, runes) // &quot;['H' 'e' 'l' 'l' 'o' ',' ' ' '世' '界']&quot;
</code></pre>
<p>在循环中使用append函数构建一个由九个rune字符构成的slice当然对应这个特殊的问题我们可以通过Go语言内置的[]rune(&quot;Hello, 世界&quot;)转换操作完成。</p>
<p>append函数对于理解slice底层是如何工作的非常重要所以让我们仔细查看究竟是发生了什么。下面是第一个版本的appendInt函数专门用于处理[]int类型的slice</p>
<p><u><i>gopl.io/ch4/append</i></u></p>
<pre><code class="language-Go">func appendInt(x []int, y int) []int {
var z []int
zlen := len(x) + 1
if zlen &lt;= cap(x) {
// There is room to grow. Extend the slice.
z = x[:zlen]
} else {
// There is insufficient space. Allocate a new array.
// Grow by doubling, for amortized linear complexity.
zcap := zlen
if zcap &lt; 2*len(x) {
zcap = 2 * len(x)
}
z = make([]int, zlen, zcap)
copy(z, x) // a built-in function; see text
}
z[len(x)] = y
return z
}
</code></pre>
<p>每次调用appendInt函数必须先检测slice底层数组是否有足够的容量来保存新添加的元素。如果有足够空间的话直接扩展slice依然在原有的底层数组之上将新添加的y元素复制到新扩展的空间并返回slice。因此输入的x和输出的z共享相同的底层数组。</p>
<p>如果没有足够的增长空间的话appendInt函数则会先分配一个足够大的slice用于保存新的结果先将输入的x复制到新的空间然后添加y元素。结果z和输入的x引用的将是不同的底层数组。</p>
<p>虽然通过循环复制元素更直接不过内置的copy函数可以方便地将一个slice复制另一个相同类型的slice。copy函数的第一个参数是要复制的目标slice第二个参数是源slice目标和源的位置顺序和<code>dst = src</code>赋值语句是一致的。两个slice可以共享同一个底层数组甚至有重叠也没有问题。copy函数将返回成功复制的元素的个数我们这里没有用到等于两个slice中较小的长度所以我们不用担心覆盖会超出目标slice的范围。</p>
<p>为了提高内存使用效率新分配的数组一般略大于保存x和y所需要的最低大小。通过在每次扩展数组时直接将长度翻倍从而避免了多次内存分配也确保了添加单个元素操作的平均时间是一个常数时间。这个程序演示了效果</p>
<pre><code class="language-Go">func main() {
var x, y []int
for i := 0; i &lt; 10; i++ {
y = appendInt(x, i)
fmt.Printf(&quot;%d cap=%d\t%v\n&quot;, i, cap(y), y)
x = y
}
}
</code></pre>
<p>每一次容量的变化都会导致重新分配内存和copy操作</p>
<pre><code>0 cap=1 [0]
1 cap=2 [0 1]
2 cap=4 [0 1 2]
3 cap=4 [0 1 2 3]
4 cap=8 [0 1 2 3 4]
5 cap=8 [0 1 2 3 4 5]
6 cap=8 [0 1 2 3 4 5 6]
7 cap=8 [0 1 2 3 4 5 6 7]
8 cap=16 [0 1 2 3 4 5 6 7 8]
9 cap=16 [0 1 2 3 4 5 6 7 8 9]
</code></pre>
<p>让我们仔细查看i=3次的迭代。当时x包含了[0 1 2]三个元素但是容量是4因此可以简单将新的元素添加到末尾不需要新的内存分配。然后新的y的长度和容量都是4并且和x引用着相同的底层数组如图4.2所示。</p>
<p><img src="ch4/../images/ch4-02.png" alt="" /></p>
<p>在下一次迭代时i=4现在没有新的空余的空间了因此appendInt函数分配一个容量为8的底层数组将x的4个元素[0 1 2 3]复制到新空间的开头然后添加新的元素i新元素的值是4。新的y的长度是5容量是8后面有3个空闲的位置三次迭代都不需要分配新的空间。当前迭代中y和x是对应不同底层数组的view。这次操作如图4.3所示。</p>
<p><img src="ch4/../images/ch4-03.png" alt="" /></p>
<p>内置的append函数可能使用比appendInt更复杂的内存扩展策略。因此通常我们并不知道append调用是否导致了内存的重新分配因此我们也不能确认新的slice和原始的slice是否引用的是相同的底层数组空间。同样我们不能确认在原先的slice上的操作是否会影响到新的slice。因此通常是将append返回的结果直接赋值给输入的slice变量</p>
<pre><code class="language-Go">runes = append(runes, r)
</code></pre>
<p>更新slice变量不仅对调用append函数是必要的实际上对应任何可能导致长度、容量或底层数组变化的操作都是必要的。要正确地使用slice需要记住尽管底层数组的元素是间接访问的但是slice对应结构体本身的指针、长度和容量部分是直接访问的。要更新这些信息需要像上面例子那样一个显式的赋值操作。从这个角度看slice并不是一个纯粹的引用类型它实际上是一个类似下面结构体的聚合类型</p>
<pre><code class="language-Go">type IntSlice struct {
ptr *int
len, cap int
}
</code></pre>
<p>我们的appendInt函数每次只能向slice追加一个元素但是内置的append函数则可以追加多个元素甚至追加一个slice。</p>
<pre><code class="language-Go">var x []int
x = append(x, 1)
x = append(x, 2, 3)
x = append(x, 4, 5, 6)
x = append(x, x...) // append the slice x
fmt.Println(x) // &quot;[1 2 3 4 5 6 1 2 3 4 5 6]&quot;
</code></pre>
<p>通过下面的小修改我们可以达到append函数类似的功能。其中在appendInt函数参数中的最后的“...”省略号表示接收变长的参数为slice。我们将在5.7节详细解释这个特性。</p>
<pre><code class="language-Go">func appendInt(x []int, y ...int) []int {
var z []int
zlen := len(x) + len(y)
// ...expand z to at least zlen...
copy(z[len(x):], y)
return z
}
</code></pre>
<p>为了避免重复,和前面相同的代码并没有显示。</p>
<h3 id="422-slice内存技巧"><a class="header" href="#422-slice内存技巧">4.2.2. Slice内存技巧</a></h3>
<p>让我们看看更多的例子比如旋转slice、反转slice或在slice原有内存空间修改元素。给定一个字符串列表下面的nonempty函数将在原有slice内存空间之上返回不包含空字符串的列表</p>
<p><u><i>gopl.io/ch4/nonempty</i></u></p>
<pre><code class="language-Go">// Nonempty is an example of an in-place slice algorithm.
package main
import &quot;fmt&quot;
// nonempty returns a slice holding only the non-empty strings.
// The underlying array is modified during the call.
func nonempty(strings []string) []string {
i := 0
for _, s := range strings {
if s != &quot;&quot; {
strings[i] = s
i++
}
}
return strings[:i]
}
</code></pre>
<p>比较微妙的地方是输入的slice和输出的slice共享一个底层数组。这可以避免分配另一个数组不过原来的数据将可能会被覆盖正如下面两个打印语句看到的那样</p>
<pre><code class="language-Go">data := []string{&quot;one&quot;, &quot;&quot;, &quot;three&quot;}
fmt.Printf(&quot;%q\n&quot;, nonempty(data)) // `[&quot;one&quot; &quot;three&quot;]`
fmt.Printf(&quot;%q\n&quot;, data) // `[&quot;one&quot; &quot;three&quot; &quot;three&quot;]`
</code></pre>
<p>因此我们通常会这样使用nonempty函数<code>data = nonempty(data)</code></p>
<p>nonempty函数也可以使用append函数实现</p>
<pre><code class="language-Go">func nonempty2(strings []string) []string {
out := strings[:0] // zero-length slice of original
for _, s := range strings {
if s != &quot;&quot; {
out = append(out, s)
}
}
return out
}
</code></pre>
<p>无论如何实现以这种方式重用一个slice一般都要求最多为每个输入值产生一个输出值事实上很多这类算法都是用来过滤或合并序列中相邻的元素。这种slice用法是比较复杂的技巧虽然使用到了slice的一些技巧但是对于某些场合是比较清晰和有效的。</p>
<p>一个slice可以用来模拟一个stack。最初给定的空slice对应一个空的stack然后可以使用append函数将新的值压入stack</p>
<pre><code class="language-Go">stack = append(stack, v) // push v
</code></pre>
<p>stack的顶部位置对应slice的最后一个元素</p>
<pre><code class="language-Go">top := stack[len(stack)-1] // top of stack
</code></pre>
<p>通过收缩stack可以弹出栈顶的元素</p>
<pre><code class="language-Go">stack = stack[:len(stack)-1] // pop
</code></pre>
<p>要删除slice中间的某个元素并保存原有的元素顺序可以通过内置的copy函数将后面的子slice向前依次移动一位完成</p>
<pre><code class="language-Go">func remove(slice []int, i int) []int {
copy(slice[i:], slice[i+1:])
return slice[:len(slice)-1]
}
func main() {
s := []int{5, 6, 7, 8, 9}
fmt.Println(remove(s, 2)) // &quot;[5 6 8 9]&quot;
}
</code></pre>
<p>如果删除元素后不用保持原来顺序的话,我们可以简单的用最后一个元素覆盖被删除的元素:</p>
<pre><code class="language-Go">func remove(slice []int, i int) []int {
slice[i] = slice[len(slice)-1]
return slice[:len(slice)-1]
}
func main() {
s := []int{5, 6, 7, 8, 9}
fmt.Println(remove(s, 2)) // &quot;[5 6 9 8]
}
</code></pre>
<p><strong>练习 4.3</strong> 重写reverse函数使用数组指针代替slice。</p>
<p><strong>练习 4.4</strong> 编写一个rotate函数通过一次循环完成旋转。</p>
<p><strong>练习 4.5</strong> 写一个函数在原地完成消除[]string中相邻重复的字符串的操作。</p>
<p><strong>练习 4.6</strong> 编写一个函数原地将一个UTF-8编码的[]byte类型的slice中相邻的空格参考unicode.IsSpace替换成一个空格返回</p>
<p><strong>练习 4.7</strong> 修改reverse函数用于原地反转UTF-8编码的[]byte。是否可以不用分配额外的内存</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="43-map"><a class="header" href="#43-map">4.3. Map</a></h2>
<p>哈希表是一种巧妙并且实用的数据结构。它是一个无序的key/value对的集合其中所有的key都是不同的然后通过给定的key可以在常数时间复杂度内检索、更新或删除对应的value。</p>
<p>在Go语言中一个map就是一个哈希表的引用map类型可以写为map[K]V其中K和V分别对应key和value。map中所有的key都有相同的类型所有的value也有着相同的类型但是key和value之间可以是不同的数据类型。其中K对应的key必须是支持==比较运算符的数据类型所以map可以通过测试key是否相等来判断是否已经存在。虽然浮点数类型也是支持相等运算符比较的但是将浮点数用做key类型则是一个坏的想法正如第三章提到的最坏的情况是可能出现的NaN和任何浮点数都不相等。对于V对应的value数据类型则没有任何的限制。</p>
<p>内置的make函数可以创建一个map</p>
<pre><code class="language-Go">ages := make(map[string]int) // mapping from strings to ints
</code></pre>
<p>我们也可以用map字面值的语法创建map同时还可以指定一些最初的key/value</p>
<pre><code class="language-Go">ages := map[string]int{
&quot;alice&quot;: 31,
&quot;charlie&quot;: 34,
}
</code></pre>
<p>这相当于</p>
<pre><code class="language-Go">ages := make(map[string]int)
ages[&quot;alice&quot;] = 31
ages[&quot;charlie&quot;] = 34
</code></pre>
<p>因此另一种创建空的map的表达式是<code>map[string]int{}</code></p>
<p>Map中的元素通过key对应的下标语法访问</p>
<pre><code class="language-Go">ages[&quot;alice&quot;] = 32
fmt.Println(ages[&quot;alice&quot;]) // &quot;32&quot;
</code></pre>
<p>使用内置的delete函数可以删除元素</p>
<pre><code class="language-Go">delete(ages, &quot;alice&quot;) // remove element ages[&quot;alice&quot;]
</code></pre>
<p>所有这些操作是安全的即使这些元素不在map中也没有关系如果一个查找失败将返回value类型对应的零值例如即使map中不存在“bob”下面的代码也可以正常工作因为ages[&quot;bob&quot;]失败时将返回0。</p>
<pre><code class="language-Go">ages[&quot;bob&quot;] = ages[&quot;bob&quot;] + 1 // happy birthday!
</code></pre>
<p>而且<code>x += y</code><code>x++</code>等简短赋值语法也可以用在map上所以上面的代码可以改写成</p>
<pre><code class="language-Go">ages[&quot;bob&quot;] += 1
</code></pre>
<p>更简单的写法</p>
<pre><code class="language-Go">ages[&quot;bob&quot;]++
</code></pre>
<p>但是map中的元素并不是一个变量因此我们不能对map的元素进行取址操作</p>
<pre><code class="language-Go">_ = &amp;ages[&quot;bob&quot;] // compile error: cannot take address of map element
</code></pre>
<p>禁止对map元素取址的原因是map可能随着元素数量的增长而重新分配更大的内存空间从而可能导致之前的地址无效。</p>
<p>要想遍历map中全部的key/value对的话可以使用range风格的for循环实现和之前的slice遍历语法类似。下面的迭代语句将在每次迭代时设置name和age变量它们对应下一个键/值对:</p>
<pre><code class="language-Go">for name, age := range ages {
fmt.Printf(&quot;%s\t%d\n&quot;, name, age)
}
</code></pre>
<p>Map的迭代顺序是不确定的并且不同的哈希函数实现可能导致不同的遍历顺序。在实践中遍历的顺序是随机的每一次遍历的顺序都不相同。这是故意的每次都使用随机的遍历顺序可以强制要求程序不会依赖具体的哈希函数实现。如果要按顺序遍历key/value对我们必须显式地对key进行排序可以使用sort包的Strings函数对字符串slice进行排序。下面是常见的处理方式</p>
<pre><code class="language-Go">import &quot;sort&quot;
var names []string
for name := range ages {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
fmt.Printf(&quot;%s\t%d\n&quot;, name, ages[name])
}
</code></pre>
<p>因为我们一开始就知道names的最终大小因此给slice分配一个合适的大小将会更有效。下面的代码创建了一个空的slice但是slice的容量刚好可以放下map中全部的key</p>
<pre><code class="language-Go">names := make([]string, 0, len(ages))
</code></pre>
<p>在上面的第一个range循环中我们只关心map中的key所以我们忽略了第二个循环变量。在第二个循环中我们只关心names中的名字所以我们使用“_”空白标识符来忽略第一个循环变量也就是迭代slice时的索引。</p>
<p>map类型的零值是nil也就是没有引用任何哈希表。</p>
<pre><code class="language-Go">var ages map[string]int
fmt.Println(ages == nil) // &quot;true&quot;
fmt.Println(len(ages) == 0) // &quot;true&quot;
</code></pre>
<p>map上的大部分操作包括查找、删除、len和range循环都可以安全工作在nil值的map上它们的行为和一个空的map类似。但是向一个nil值的map存入元素将导致一个panic异常</p>
<pre><code class="language-Go">ages[&quot;carol&quot;] = 21 // panic: assignment to entry in nil map
</code></pre>
<p>在向map存数据前必须先创建map。</p>
<p>通过key作为索引下标来访问map将产生一个value。如果key在map中是存在的那么将得到与key对应的value如果key不存在那么将得到value对应类型的零值正如我们前面看到的ages[&quot;bob&quot;]那样。这个规则很实用但是有时候可能需要知道对应的元素是否真的是在map之中。例如如果元素类型是一个数字你可能需要区分一个已经存在的0和不存在而返回零值的0可以像下面这样测试</p>
<pre><code class="language-Go">age, ok := ages[&quot;bob&quot;]
if !ok { /* &quot;bob&quot; is not a key in this map; age == 0. */ }
</code></pre>
<p>你会经常看到将这两个结合起来使用,像这样:</p>
<pre><code class="language-Go">if age, ok := ages[&quot;bob&quot;]; !ok { /* ... */ }
</code></pre>
<p>在这种场景下map的下标语法将产生两个值第二个是一个布尔值用于报告元素是否真的存在。布尔变量一般命名为ok特别适合马上用于if条件判断部分。</p>
<p>和slice一样map之间也不能进行相等比较唯一的例外是和nil进行比较。要判断两个map是否包含相同的key和value我们必须通过一个循环实现</p>
<pre><code class="language-Go">func equal(x, y map[string]int) bool {
if len(x) != len(y) {
return false
}
for k, xv := range x {
if yv, ok := y[k]; !ok || yv != xv {
return false
}
}
return true
}
</code></pre>
<p>从例子中可以看到如何用!ok来区分元素不存在与元素存在但为0的。我们不能简单地用xv != y[k]判断那样会导致在判断下面两个map时产生错误的结果</p>
<pre><code class="language-Go">// True if equal is written incorrectly.
equal(map[string]int{&quot;A&quot;: 0}, map[string]int{&quot;B&quot;: 42})
</code></pre>
<p>Go语言中并没有提供一个set类型但是map中的key也是不相同的可以用map实现类似set的功能。为了说明这一点下面的dedup程序读取多行输入但是只打印第一次出现的行。它是1.3节中出现的dup程序的变体。dedup程序通过map来表示所有的输入行所对应的set集合以确保已经在集合存在的行不会被重复打印。</p>
<p><u><i>gopl.io/ch4/dedup</i></u></p>
<pre><code class="language-Go">func main() {
seen := make(map[string]bool) // a set of strings
input := bufio.NewScanner(os.Stdin)
for input.Scan() {
line := input.Text()
if !seen[line] {
seen[line] = true
fmt.Println(line)
}
}
if err := input.Err(); err != nil {
fmt.Fprintf(os.Stderr, &quot;dedup: %v\n&quot;, err)
os.Exit(1)
}
}
</code></pre>
<p>Go程序员将这种忽略value的map当作一个字符串集合并非所有<code>map[string]bool</code>类型value都是无关紧要的有一些则可能会同时包含true和false的值。</p>
<p>有时候我们需要一个map或set的key是slice类型但是map的key必须是可比较的类型但是slice并不满足这个条件。不过我们可以通过两个步骤绕过这个限制。第一步定义一个辅助函数k将slice转为map对应的string类型的key确保只有x和y相等时k(x) == k(y)才成立。然后创建一个key为string类型的map在每次对map操作时先用k辅助函数将slice转化为string类型。</p>
<p>下面的例子演示了如何使用map来记录提交相同的字符串列表的次数。它使用了fmt.Sprintf函数将字符串列表转换为一个字符串以用于map的key通过%q参数忠实地记录每个字符串元素的信息</p>
<pre><code class="language-Go">var m = make(map[string]int)
func k(list []string) string { return fmt.Sprintf(&quot;%q&quot;, list) }
func Add(list []string) { m[k(list)]++ }
func Count(list []string) int { return m[k(list)] }
</code></pre>
<p>使用同样的技术可以处理任何不可比较的key类型而不仅仅是slice类型。这种技术对于想使用自定义key比较函数的时候也很有用例如在比较字符串的时候忽略大小写。同时辅助函数k(x)也不一定是字符串类型,它可以返回任何可比较的类型,例如整数、数组或结构体等。</p>
<p>这是map的另一个例子下面的程序用于统计输入中每个Unicode码点出现的次数。虽然Unicode全部码点的数量巨大但是出现在特定文档中的字符种类并没有多少使用map可以用比较自然的方式来跟踪那些出现过的字符的次数。</p>
<p><u><i>gopl.io/ch4/charcount</i></u></p>
<pre><code class="language-Go">// Charcount computes counts of Unicode characters.
package main
import (
&quot;bufio&quot;
&quot;fmt&quot;
&quot;io&quot;
&quot;os&quot;
&quot;unicode&quot;
&quot;unicode/utf8&quot;
)
func main() {
counts := make(map[rune]int) // counts of Unicode characters
var utflen [utf8.UTFMax + 1]int // count of lengths of UTF-8 encodings
invalid := 0 // count of invalid UTF-8 characters
in := bufio.NewReader(os.Stdin)
for {
r, n, err := in.ReadRune() // returns rune, nbytes, error
if err == io.EOF {
break
}
if err != nil {
fmt.Fprintf(os.Stderr, &quot;charcount: %v\n&quot;, err)
os.Exit(1)
}
if r == unicode.ReplacementChar &amp;&amp; n == 1 {
invalid++
continue
}
counts[r]++
utflen[n]++
}
fmt.Printf(&quot;rune\tcount\n&quot;)
for c, n := range counts {
fmt.Printf(&quot;%q\t%d\n&quot;, c, n)
}
fmt.Print(&quot;\nlen\tcount\n&quot;)
for i, n := range utflen {
if i &gt; 0 {
fmt.Printf(&quot;%d\t%d\n&quot;, i, n)
}
}
if invalid &gt; 0 {
fmt.Printf(&quot;\n%d invalid UTF-8 characters\n&quot;, invalid)
}
}
</code></pre>
<p>ReadRune方法执行UTF-8解码并返回三个值解码的rune字符的值字符UTF-8编码后的长度和一个错误值。我们可预期的错误值只有对应文件结尾的io.EOF。如果输入的是无效的UTF-8编码的字符返回的将是unicode.ReplacementChar表示无效字符并且编码长度是1。</p>
<p>charcount程序同时打印不同UTF-8编码长度的字符数目。对此map并不是一个合适的数据结构因为UTF-8编码的长度总是从1到utf8.UTFMax最大是4个字节使用数组将更有效。</p>
<p>作为一个实验我们用charcount程序对英文版原稿的字符进行了统计。虽然大部分是英语但是也有一些非ASCII字符。下面是排名前10的非ASCII字符</p>
<p><img src="ch4/../images/ch4-xx-01.png" alt="" /></p>
<p>下面是不同UTF-8编码长度的字符的数目</p>
<pre><code>len count
1 765391
2 60
3 70
4 0
</code></pre>
<p>Map的value类型也可以是一个聚合类型比如是一个map或slice。在下面的代码中图graph的key类型是一个字符串value类型map[string]bool代表一个字符串集合。从概念上讲graph将一个字符串类型的key映射到一组相关的字符串集合它们指向新的graph的key。</p>
<p><u><i>gopl.io/ch4/graph</i></u></p>
<pre><code class="language-Go">var graph = make(map[string]map[string]bool)
func addEdge(from, to string) {
edges := graph[from]
if edges == nil {
edges = make(map[string]bool)
graph[from] = edges
}
edges[to] = true
}
func hasEdge(from, to string) bool {
return graph[from][to]
}
</code></pre>
<p>其中addEdge函数惰性初始化map是一个惯用方式也就是说在每个值首次作为key时才初始化。hasEdge函数显示了如何让map的零值也能正常工作即使from到to的边不存在graph[from][to]依然可以返回一个有意义的结果。</p>
<p><strong>练习 4.8</strong> 修改charcount程序使用unicode.IsLetter等相关的函数统计字母、数字等Unicode中不同的字符类别。</p>
<p><strong>练习 4.9</strong> 编写一个程序wordfreq程序报告输入文本中每个单词出现的频率。在第一次调用Scan前先调用input.Split(bufio.ScanWords)函数,这样可以按单词而不是按行输入。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="44-结构体"><a class="header" href="#44-结构体">4.4. 结构体</a></h2>
<p>结构体是一种聚合的数据类型,是由零个或多个任意类型的值聚合成的实体。每个值称为结构体的成员。用结构体的经典案例是处理公司的员工信息,每个员工信息包含一个唯一的员工编号、员工的名字、家庭住址、出生日期、工作岗位、薪资、上级领导等等。所有的这些信息都需要绑定到一个实体中,可以作为一个整体单元被复制,作为函数的参数或返回值,或者是被存储到数组中,等等。</p>
<p>下面两个语句声明了一个叫Employee的命名的结构体类型并且声明了一个Employee类型的变量dilbert</p>
<pre><code class="language-Go">type Employee struct {
ID int
Name string
Address string
DoB time.Time
Position string
Salary int
ManagerID int
}
var dilbert Employee
</code></pre>
<p>dilbert结构体变量的成员可以通过点操作符访问比如dilbert.Name和dilbert.DoB。因为dilbert是一个变量它所有的成员也同样是变量我们可以直接对每个成员赋值</p>
<pre><code class="language-Go">dilbert.Salary -= 5000 // demoted, for writing too few lines of code
</code></pre>
<p>或者是对成员取地址,然后通过指针访问:</p>
<pre><code class="language-Go">position := &amp;dilbert.Position
*position = &quot;Senior &quot; + *position // promoted, for outsourcing to Elbonia
</code></pre>
<p>点操作符也可以和指向结构体的指针一起工作:</p>
<pre><code class="language-Go">var employeeOfTheMonth *Employee = &amp;dilbert
employeeOfTheMonth.Position += &quot; (proactive team player)&quot;
</code></pre>
<p>相当于下面语句</p>
<pre><code class="language-Go">(*employeeOfTheMonth).Position += &quot; (proactive team player)&quot;
</code></pre>
<p>下面的EmployeeByID函数将根据给定的员工ID返回对应的员工信息结构体的指针。我们可以使用点操作符来访问它里面的成员</p>
<pre><code class="language-Go">func EmployeeByID(id int) *Employee { /* ... */ }
fmt.Println(EmployeeByID(dilbert.ManagerID).Position) // &quot;Pointy-haired boss&quot;
id := dilbert.ID
EmployeeByID(id).Salary = 0 // fired for... no real reason
</code></pre>
<p>后面的语句通过EmployeeByID返回的结构体指针更新了Employee结构体的成员。如果将EmployeeByID函数的返回值从<code>*Employee</code>指针类型改为Employee值类型那么更新语句将不能编译通过因为在赋值语句的左边并不确定是一个变量译注调用函数返回的是值并不是一个可取地址的变量</p>
<p>通常一行对应一个结构体成员成员的名字在前类型在后不过如果相邻的成员类型如果相同的话可以被合并到一行就像下面的Name和Address成员那样</p>
<pre><code class="language-Go">type Employee struct {
ID int
Name, Address string
DoB time.Time
Position string
Salary int
ManagerID int
}
</code></pre>
<p>结构体成员的输入顺序也有重要的意义。我们也可以将Position成员合并因为也是字符串类型或者是交换Name和Address出现的先后顺序那样的话就是定义了不同的结构体类型。通常我们只是将相关的成员写到一起。</p>
<p>如果结构体成员名字是以大写字母开头的那么该成员就是导出的这是Go语言导出规则决定的。一个结构体可能同时包含导出和未导出的成员。</p>
<p>结构体类型往往是冗长的因为它的每个成员可能都会占一行。虽然我们每次都可以重写整个结构体成员但是重复会令人厌烦。因此完整的结构体写法通常只在类型声明语句的地方出现就像Employee类型声明语句那样。</p>
<p>一个命名为S的结构体类型将不能再包含S类型的成员因为一个聚合的值不能包含它自身。该限制同样适用于数组。但是S类型的结构体可以包含<code>*S</code>指针类型的成员,这可以让我们创建递归的数据结构,比如链表和树结构等。在下面的代码中,我们使用一个二叉树来实现一个插入排序:</p>
<p><u><i>gopl.io/ch4/treesort</i></u></p>
<pre><code class="language-Go">type tree struct {
value int
left, right *tree
}
// Sort sorts values in place.
func Sort(values []int) {
var root *tree
for _, v := range values {
root = add(root, v)
}
appendValues(values[:0], root)
}
// appendValues appends the elements of t to values in order
// and returns the resulting slice.
func appendValues(values []int, t *tree) []int {
if t != nil {
values = appendValues(values, t.left)
values = append(values, t.value)
values = appendValues(values, t.right)
}
return values
}
func add(t *tree, value int) *tree {
if t == nil {
// Equivalent to return &amp;tree{value: value}.
t = new(tree)
t.value = value
return t
}
if value &lt; t.value {
t.left = add(t.left, value)
} else {
t.right = add(t.right, value)
}
return t
}
</code></pre>
<p>结构体类型的零值是每个成员都是零值。通常会将零值作为最合理的默认值。例如对于bytes.Buffer类型结构体初始值就是一个随时可用的空缓存还有在第9章将会讲到的sync.Mutex的零值也是有效的未锁定状态。有时候这种零值可用的特性是自然获得的但是也有些类型需要一些额外的工作。</p>
<p>如果结构体没有任何成员的话就是空结构体写作struct{}。它的大小为0也不包含任何信息但是有时候依然是有价值的。有些Go语言程序员用map来模拟set数据结构时用它来代替map中布尔类型的value只是强调key的重要性但是因为节约的空间有限而且语法比较复杂所以我们通常会避免这样的用法。</p>
<pre><code class="language-Go">seen := make(map[string]struct{}) // set of strings
// ...
if _, ok := seen[s]; !ok {
seen[s] = struct{}{}
// ...first time seeing s...
}
</code></pre>
<h3 id="441-结构体字面值"><a class="header" href="#441-结构体字面值">4.4.1. 结构体字面值</a></h3>
<p>结构体值也可以用结构体字面值表示,结构体字面值可以指定每个成员的值。</p>
<pre><code class="language-Go">type Point struct{ X, Y int }
p := Point{1, 2}
</code></pre>
<p>这里有两种形式的结构体字面值语法上面的是第一种写法要求以结构体成员定义的顺序为每个结构体成员指定一个字面值。它要求写代码和读代码的人要记住结构体的每个成员的类型和顺序不过结构体成员有细微的调整就可能导致上述代码不能编译。因此上述的语法一般只在定义结构体的包内部使用或者是在较小的结构体中使用这些结构体的成员排列比较规则比如image.Point{x, y}或color.RGBA{red, green, blue, alpha}。</p>
<p>其实更常用的是第二种写法以成员名字和相应的值来初始化可以包含部分或全部的成员如1.4节的Lissajous程序的写法</p>
<pre><code class="language-Go">anim := gif.GIF{LoopCount: nframes}
</code></pre>
<p>在这种形式的结构体字面值写法中,如果成员被忽略的话将默认用零值。因为提供了成员的名字,所以成员出现的顺序并不重要。</p>
<p>两种不同形式的写法不能混合使用。而且,你不能企图在外部包中用第一种顺序赋值的技巧来偷偷地初始化结构体中未导出的成员。</p>
<pre><code class="language-Go">package p
type T struct{ a, b int } // a and b are not exported
package q
import &quot;p&quot;
var _ = p.T{a: 1, b: 2} // compile error: can't reference a, b
var _ = p.T{1, 2} // compile error: can't reference a, b
</code></pre>
<p>虽然上面最后一行代码的编译错误信息中并没有显式提到未导出的成员,但是这样企图隐式使用未导出成员的行为也是不允许的。</p>
<p>结构体可以作为函数的参数和返回值。例如这个Scale函数将Point类型的值缩放后返回</p>
<pre><code class="language-Go">func Scale(p Point, factor int) Point {
return Point{p.X * factor, p.Y * factor}
}
fmt.Println(Scale(Point{1, 2}, 5)) // &quot;{5 10}&quot;
</code></pre>
<p>如果考虑效率的话,较大的结构体通常会用指针的方式传入和返回,</p>
<pre><code class="language-Go">func Bonus(e *Employee, percent int) int {
return e.Salary * percent / 100
}
</code></pre>
<p>如果要在函数内部修改结构体成员的话用指针传入是必须的因为在Go语言中所有的函数参数都是值拷贝传入的函数参数将不再是函数调用时的原始变量。</p>
<pre><code class="language-Go">func AwardAnnualRaise(e *Employee) {
e.Salary = e.Salary * 105 / 100
}
</code></pre>
<p>因为结构体通常通过指针处理,可以用下面的写法来创建并初始化一个结构体变量,并返回结构体的地址:</p>
<pre><code class="language-Go">pp := &amp;Point{1, 2}
</code></pre>
<p>它和下面的语句是等价的</p>
<pre><code class="language-Go">pp := new(Point)
*pp = Point{1, 2}
</code></pre>
<p>不过&amp;Point{1, 2}写法可以直接在表达式中使用,比如一个函数调用。</p>
<h3 id="442-结构体比较"><a class="header" href="#442-结构体比较">4.4.2. 结构体比较</a></h3>
<p>如果结构体的全部成员都是可以比较的,那么结构体也是可以比较的,那样的话两个结构体将可以使用==或!=运算符进行比较。相等比较运算符==将比较两个结构体的每个成员,因此下面两个比较的表达式是等价的:</p>
<pre><code class="language-Go">type Point struct{ X, Y int }
p := Point{1, 2}
q := Point{2, 1}
fmt.Println(p.X == q.X &amp;&amp; p.Y == q.Y) // &quot;false&quot;
fmt.Println(p == q) // &quot;false&quot;
</code></pre>
<p>可比较的结构体类型和其他可比较的类型一样可以用于map的key类型。</p>
<pre><code class="language-Go">type address struct {
hostname string
port int
}
hits := make(map[address]int)
hits[address{&quot;golang.org&quot;, 443}]++
</code></pre>
<h3 id="443-结构体嵌入和匿名成员"><a class="header" href="#443-结构体嵌入和匿名成员">4.4.3. 结构体嵌入和匿名成员</a></h3>
<p>在本节中我们将看到如何使用Go语言提供的不同寻常的结构体嵌入机制让一个命名的结构体包含另一个结构体类型的匿名成员这样就可以通过简单的点运算符x.f来访问匿名成员链中嵌套的x.d.e.f成员。</p>
<p>考虑一个二维的绘图程序,提供了一个各种图形的库,例如矩形、椭圆形、星形和轮形等几何形状。这里是其中两个的定义:</p>
<pre><code class="language-Go">type Circle struct {
X, Y, Radius int
}
type Wheel struct {
X, Y, Radius, Spokes int
}
</code></pre>
<p>一个Circle代表的圆形类型包含了标准圆心的X和Y坐标信息和一个Radius表示的半径信息。一个Wheel轮形除了包含Circle类型所有的全部成员外还增加了Spokes表示径向辐条的数量。我们可以这样创建一个wheel变量</p>
<pre><code class="language-Go">var w Wheel
w.X = 8
w.Y = 8
w.Radius = 5
w.Spokes = 20
</code></pre>
<p>随着库中几何形状数量的增多,我们一定会注意到它们之间的相似和重复之处,所以我们可能为了便于维护而将相同的属性独立出来:</p>
<pre><code class="language-Go">type Point struct {
X, Y int
}
type Circle struct {
Center Point
Radius int
}
type Wheel struct {
Circle Circle
Spokes int
}
</code></pre>
<p>这样改动之后结构体类型变的清晰了,但是这种修改同时也导致了访问每个成员变得繁琐:</p>
<pre><code class="language-Go">var w Wheel
w.Circle.Center.X = 8
w.Circle.Center.Y = 8
w.Circle.Radius = 5
w.Spokes = 20
</code></pre>
<p>Go语言有一个特性让我们只声明一个成员对应的数据类型而不指名成员的名字这类成员就叫匿名成员。匿名成员的数据类型必须是命名的类型或指向一个命名的类型的指针。下面的代码中Circle和Wheel各自都有一个匿名成员。我们可以说Point类型被嵌入到了Circle结构体同时Circle类型被嵌入到了Wheel结构体。</p>
<pre><code class="language-Go">type Circle struct {
Point
Radius int
}
type Wheel struct {
Circle
Spokes int
}
</code></pre>
<p>得益于匿名嵌入的特性,我们可以直接访问叶子属性而不需要给出完整的路径:</p>
<pre><code class="language-Go">var w Wheel
w.X = 8 // equivalent to w.Circle.Point.X = 8
w.Y = 8 // equivalent to w.Circle.Point.Y = 8
w.Radius = 5 // equivalent to w.Circle.Radius = 5
w.Spokes = 20
</code></pre>
<p>在右边的注释中给出的显式形式访问这些叶子成员的语法依然有效因此匿名成员并不是真的无法访问了。其中匿名成员Circle和Point都有自己的名字——就是命名的类型名字——但是这些名字在点操作符中是可选的。我们在访问子成员的时候可以忽略任何匿名成员部分。</p>
<p>不幸的是,结构体字面值并没有简短表示匿名成员的语法, 因此下面的语句都不能编译通过:</p>
<pre><code class="language-Go">w = Wheel{8, 8, 5, 20} // compile error: unknown fields
w = Wheel{X: 8, Y: 8, Radius: 5, Spokes: 20} // compile error: unknown fields
</code></pre>
<p>结构体字面值必须遵循形状类型声明时的结构,所以我们只能用下面的两种语法,它们彼此是等价的:</p>
<p><u><i>gopl.io/ch4/embed</i></u></p>
<pre><code class="language-Go">w = Wheel{Circle{Point{8, 8}, 5}, 20}
w = Wheel{
Circle: Circle{
Point: Point{X: 8, Y: 8},
Radius: 5,
},
Spokes: 20, // NOTE: trailing comma necessary here (and at Radius)
}
fmt.Printf(&quot;%#v\n&quot;, w)
// Output:
// Wheel{Circle:Circle{Point:Point{X:8, Y:8}, Radius:5}, Spokes:20}
w.X = 42
fmt.Printf(&quot;%#v\n&quot;, w)
// Output:
// Wheel{Circle:Circle{Point:Point{X:42, Y:8}, Radius:5}, Spokes:20}
</code></pre>
<p>需要注意的是Printf函数中%v参数包含的#副词它表示用和Go语言类似的语法打印值。对于结构体类型来说将包含每个成员的名字。</p>
<p>因为匿名成员也有一个隐式的名字因此不能同时包含两个类型相同的匿名成员这会导致名字冲突。同时因为成员的名字是由其类型隐式地决定的所以匿名成员也有可见性的规则约束。在上面的例子中Point和Circle匿名成员都是导出的。即使它们不导出比如改成小写字母开头的point和circle我们依然可以用简短形式访问匿名成员嵌套的成员</p>
<pre><code class="language-Go">w.X = 8 // equivalent to w.circle.point.X = 8
</code></pre>
<p>但是在包外部因为circle和point没有导出不能访问它们的成员因此简短的匿名成员访问语法也是禁止的。</p>
<p>到目前为止,我们看到匿名成员特性只是对访问嵌套成员的点运算符提供了简短的语法糖。稍后,我们将会看到匿名成员并不要求是结构体类型;其实任何命名的类型都可以作为结构体的匿名成员。但是为什么要嵌入一个没有任何子成员类型的匿名成员类型呢?</p>
<p>答案是匿名类型的方法集。简短的点运算符语法可以用于选择匿名成员嵌套的成员也可以用于访问它们的方法。实际上外层的结构体不仅仅是获得了匿名成员类型的所有成员而且也获得了该类型导出的全部的方法。这个机制可以用于将一些有简单行为的对象组合成有复杂行为的对象。组合是Go语言中面向对象编程的核心我们将在6.3节中专门讨论。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="45-json"><a class="header" href="#45-json">4.5. JSON</a></h2>
<p>JavaScript对象表示法JSON是一种用于发送和接收结构化信息的标准协议。在类似的协议中JSON并不是唯一的一个标准协议。 XML§7.14、ASN.1和Google的Protocol Buffers都是类似的协议并且有各自的特色但是由于简洁性、可读性和流行程度等原因JSON是应用最广泛的一个。</p>
<p>Go语言对于这些标准格式的编码和解码都有良好的支持由标准库中的encoding/json、encoding/xml、encoding/asn1等包提供支持译注Protocol Buffers的支持由 github.com/golang/protobuf 包提供并且这类包都有着相似的API接口。本节我们将对重要的encoding/json包的用法做个概述。</p>
<p>JSON是对JavaScript中各种类型的值——字符串、数字、布尔值和对象——Unicode本文编码。它可以用有效可读的方式表示第三章的基础数据类型和本章的数组、slice、结构体和map等聚合数据类型。</p>
<p>基本的JSON类型有数字十进制或科学记数法、布尔值true或false、字符串其中字符串是以双引号包含的Unicode字符序列支持和Go语言类似的反斜杠转义特性不过JSON使用的是<code>\Uhhhh</code>转义数字来表示一个UTF-16编码译注UTF-16和UTF-8一样是一种变长的编码有些Unicode码点较大的字符需要用4个字节表示而且UTF-16还有大端和小端的问题而不是Go语言的rune类型。</p>
<p>这些基础类型可以通过JSON的数组和对象类型进行递归组合。一个JSON数组是一个有序的值序列写在一个方括号中并以逗号分隔一个JSON数组可以用于编码Go语言的数组和slice。一个JSON对象是一个字符串到值的映射写成一系列的name:value对形式用花括号包含并以逗号分隔JSON的对象类型可以用于编码Go语言的map类型key类型是字符串和结构体。例如</p>
<pre><code>boolean true
number -273.15
string &quot;She said \&quot;Hello, BF\&quot;&quot;
array [&quot;gold&quot;, &quot;silver&quot;, &quot;bronze&quot;]
object {&quot;year&quot;: 1980,
&quot;event&quot;: &quot;archery&quot;,
&quot;medals&quot;: [&quot;gold&quot;, &quot;silver&quot;, &quot;bronze&quot;]}
</code></pre>
<p>考虑一个应用程序该程序负责收集各种电影评论并提供反馈功能。它的Movie数据类型和一个典型的表示电影的值列表如下所示。在结构体声明中Year和Color成员后面的字符串面值是结构体成员Tag我们稍后会解释它的作用。</p>
<p><u><i>gopl.io/ch4/movie</i></u></p>
<pre><code class="language-Go">type Movie struct {
Title string
Year int `json:&quot;released&quot;`
Color bool `json:&quot;color,omitempty&quot;`
Actors []string
}
var movies = []Movie{
{Title: &quot;Casablanca&quot;, Year: 1942, Color: false,
Actors: []string{&quot;Humphrey Bogart&quot;, &quot;Ingrid Bergman&quot;}},
{Title: &quot;Cool Hand Luke&quot;, Year: 1967, Color: true,
Actors: []string{&quot;Paul Newman&quot;}},
{Title: &quot;Bullitt&quot;, Year: 1968, Color: true,
Actors: []string{&quot;Steve McQueen&quot;, &quot;Jacqueline Bisset&quot;}},
// ...
}
</code></pre>
<p>这样的数据结构特别适合JSON格式并且在两者之间相互转换也很容易。将一个Go语言中类似movies的结构体slice转为JSON的过程叫编组marshaling。编组通过调用json.Marshal函数完成</p>
<pre><code class="language-Go">data, err := json.Marshal(movies)
if err != nil {
log.Fatalf(&quot;JSON marshaling failed: %s&quot;, err)
}
fmt.Printf(&quot;%s\n&quot;, data)
</code></pre>
<p>Marshal函数返回一个编码后的字节slice包含很长的字符串并且没有空白缩进我们将它折行以便于显示</p>
<pre><code>[{&quot;Title&quot;:&quot;Casablanca&quot;,&quot;released&quot;:1942,&quot;Actors&quot;:[&quot;Humphrey Bogart&quot;,&quot;Ingr
id Bergman&quot;]},{&quot;Title&quot;:&quot;Cool Hand Luke&quot;,&quot;released&quot;:1967,&quot;color&quot;:true,&quot;Ac
tors&quot;:[&quot;Paul Newman&quot;]},{&quot;Title&quot;:&quot;Bullitt&quot;,&quot;released&quot;:1968,&quot;color&quot;:true,&quot;
Actors&quot;:[&quot;Steve McQueen&quot;,&quot;Jacqueline Bisset&quot;]}]
</code></pre>
<p>这种紧凑的表示形式虽然包含了全部的信息但是很难阅读。为了生成便于阅读的格式另一个json.MarshalIndent函数将产生整齐缩进的输出。该函数有两个额外的字符串参数用于表示每一行输出的前缀和每一个层级的缩进</p>
<pre><code class="language-Go">data, err := json.MarshalIndent(movies, &quot;&quot;, &quot; &quot;)
if err != nil {
log.Fatalf(&quot;JSON marshaling failed: %s&quot;, err)
}
fmt.Printf(&quot;%s\n&quot;, data)
</code></pre>
<p>上面的代码将产生这样的输出(译注:在最后一个成员或元素后面并没有逗号分隔符):</p>
<pre><code class="language-Json">[
{
&quot;Title&quot;: &quot;Casablanca&quot;,
&quot;released&quot;: 1942,
&quot;Actors&quot;: [
&quot;Humphrey Bogart&quot;,
&quot;Ingrid Bergman&quot;
]
},
{
&quot;Title&quot;: &quot;Cool Hand Luke&quot;,
&quot;released&quot;: 1967,
&quot;color&quot;: true,
&quot;Actors&quot;: [
&quot;Paul Newman&quot;
]
},
{
&quot;Title&quot;: &quot;Bullitt&quot;,
&quot;released&quot;: 1968,
&quot;color&quot;: true,
&quot;Actors&quot;: [
&quot;Steve McQueen&quot;,
&quot;Jacqueline Bisset&quot;
]
}
]
</code></pre>
<p>在编码时默认使用Go语言结构体的成员名字作为JSON的对象通过reflect反射技术我们将在12.6节讨论)。只有导出的结构体成员才会被编码,这也就是我们为什么选择用大写字母开头的成员名称。</p>
<p>细心的读者可能已经注意到其中Year名字的成员在编码后变成了released还有Color成员编码后变成了小写字母开头的color。这是因为结构体成员Tag所导致的。一个结构体成员Tag是和在编译阶段关联到该成员的元信息字符串</p>
<pre><code>Year int `json:&quot;released&quot;`
Color bool `json:&quot;color,omitempty&quot;`
</code></pre>
<p>结构体的成员Tag可以是任意的字符串面值但是通常是一系列用空格分隔的key:&quot;value&quot;键值对序列因为值中含有双引号字符因此成员Tag一般用原生字符串面值的形式书写。json开头键名对应的值用于控制encoding/json包的编码和解码的行为并且encoding/...下面其它的包也遵循这个约定。成员Tag中json对应值的第一部分用于指定JSON对象的名字比如将Go语言中的TotalCount成员对应到JSON中的total_count对象。Color成员的Tag还带了一个额外的omitempty选项表示当Go语言结构体成员为空或零值时不生成该JSON对象这里false为零值。果然Casablanca是一个黑白电影并没有输出Color成员。</p>
<p>编码的逆操作是解码对应将JSON数据解码为Go语言的数据结构Go语言中一般叫unmarshaling通过json.Unmarshal函数完成。下面的代码将JSON格式的电影数据解码为一个结构体slice结构体中只有Title成员。通过定义合适的Go语言数据结构我们可以选择性地解码JSON中感兴趣的成员。当Unmarshal函数调用返回slice将被只含有Title信息的值填充其它JSON成员将被忽略。</p>
<pre><code class="language-Go">var titles []struct{ Title string }
if err := json.Unmarshal(data, &amp;titles); err != nil {
log.Fatalf(&quot;JSON unmarshaling failed: %s&quot;, err)
}
fmt.Println(titles) // &quot;[{Casablanca} {Cool Hand Luke} {Bullitt}]&quot;
</code></pre>
<p>许多web服务都提供JSON接口通过HTTP接口发送JSON格式请求并返回JSON格式的信息。为了说明这一点我们通过Github的issue查询服务来演示类似的用法。首先我们要定义合适的类型和常量</p>
<p><u><i>gopl.io/ch4/github</i></u></p>
<pre><code class="language-Go">// Package github provides a Go API for the GitHub issue tracker.
// See https://developer.github.com/v3/search/#search-issues.
package github
import &quot;time&quot;
const IssuesURL = &quot;https://api.github.com/search/issues&quot;
type IssuesSearchResult struct {
TotalCount int `json:&quot;total_count&quot;`
Items []*Issue
}
type Issue struct {
Number int
HTMLURL string `json:&quot;html_url&quot;`
Title string
State string
User *User
CreatedAt time.Time `json:&quot;created_at&quot;`
Body string // in Markdown format
}
type User struct {
Login string
HTMLURL string `json:&quot;html_url&quot;`
}
</code></pre>
<p>和前面一样即使对应的JSON对象名是小写字母每个结构体的成员名也是声明为大写字母开头的。因为有些JSON成员名字和Go结构体成员名字并不相同因此需要Go语言结构体成员Tag来指定对应的JSON名字。同样在解码的时候也需要做同样的处理GitHub服务返回的信息比我们定义的要多很多。</p>
<p>SearchIssues函数发出一个HTTP请求然后解码返回的JSON格式的结果。因为用户提供的查询条件可能包含类似<code>?</code><code>&amp;</code>之类的特殊字符为了避免对URL造成冲突我们用url.QueryEscape来对查询中的特殊字符进行转义操作。</p>
<p><u><i>gopl.io/ch4/github</i></u></p>
<pre><code class="language-Go">package github
import (
&quot;encoding/json&quot;
&quot;fmt&quot;
&quot;net/http&quot;
&quot;net/url&quot;
&quot;strings&quot;
)
// SearchIssues queries the GitHub issue tracker.
func SearchIssues(terms []string) (*IssuesSearchResult, error) {
q := url.QueryEscape(strings.Join(terms, &quot; &quot;))
resp, err := http.Get(IssuesURL + &quot;?q=&quot; + q)
if err != nil {
return nil, err
}
// We must close resp.Body on all execution paths.
// (Chapter 5 presents 'defer', which makes this simpler.)
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
return nil, fmt.Errorf(&quot;search query failed: %s&quot;, resp.Status)
}
var result IssuesSearchResult
if err := json.NewDecoder(resp.Body).Decode(&amp;result); err != nil {
resp.Body.Close()
return nil, err
}
resp.Body.Close()
return &amp;result, nil
}
</code></pre>
<p>在早些的例子中我们使用了json.Unmarshal函数来将JSON格式的字符串解码为字节slice。但是这个例子中我们使用了基于流式的解码器json.Decoder它可以从一个输入流解码JSON数据尽管这不是必须的。如您所料还有一个针对输出流的json.Encoder编码对象。</p>
<p>我们调用Decode方法来填充变量。这里有多种方法可以格式化结构。下面是最简单的一种以一个固定宽度打印每个issue但是在下一节我们将看到如何利用模板来输出复杂的格式。</p>
<p><u><i>gopl.io/ch4/issues</i></u></p>
<pre><code class="language-Go">// Issues prints a table of GitHub issues matching the search terms.
package main
import (
&quot;fmt&quot;
&quot;log&quot;
&quot;os&quot;
&quot;gopl.io/ch4/github&quot;
)
func main() {
result, err := github.SearchIssues(os.Args[1:])
if err != nil {
log.Fatal(err)
}
fmt.Printf(&quot;%d issues:\n&quot;, result.TotalCount)
for _, item := range result.Items {
fmt.Printf(&quot;#%-5d %9.9s %.55s\n&quot;,
item.Number, item.User.Login, item.Title)
}
}
</code></pre>
<p>通过命令行参数指定检索条件。下面的命令是查询Go语言项目中和JSON解码相关的问题还有查询返回的结果</p>
<pre><code>$ go build gopl.io/ch4/issues
$ ./issues repo:golang/go is:open json decoder
13 issues:
#5680 eaigner encoding/json: set key converter on en/decoder
#6050 gopherbot encoding/json: provide tokenizer
#8658 gopherbot encoding/json: use bufio
#8462 kortschak encoding/json: UnmarshalText confuses json.Unmarshal
#5901 rsc encoding/json: allow override type marshaling
#9812 klauspost encoding/json: string tag not symmetric
#7872 extempora encoding/json: Encoder internally buffers full output
#9650 cespare encoding/json: Decoding gives errPhase when unmarshalin
#6716 gopherbot encoding/json: include field name in unmarshal error me
#6901 lukescott encoding/json, encoding/xml: option to treat unknown fi
#6384 joeshaw encoding/json: encode precise floating point integers u
#6647 btracey x/tools/cmd/godoc: display type kind of each named type
#4237 gjemiller encoding/base64: URLEncoding padding is optional
</code></pre>
<p>GitHub的Web服务接口 https://developer.github.com/v3/ 包含了更多的特性。</p>
<p><strong>练习 4.10</strong> 修改issues程序根据问题的时间进行分类比如不到一个月的、不到一年的、超过一年。</p>
<p><strong>练习 4.11</strong> 编写一个工具允许用户在命令行创建、读取、更新和关闭GitHub上的issue当必要的时候自动打开用户默认的编辑器用于输入文本信息。</p>
<p><strong>练习 4.12</strong> 流行的web漫画服务xkcd也提供了JSON接口。例如一个 https://xkcd.com/571/info.0.json 请求将返回一个很多人喜爱的571编号的详细描述。下载每个链接只下载一次然后创建一个离线索引。编写一个xkcd工具使用这些离线索引打印和命令行输入的检索词相匹配的漫画的URL。</p>
<p><strong>练习 4.13</strong> 使用开放电影数据库的JSON服务接口允许你检索和下载 https://omdbapi.com/ 上电影的名字和对应的海报图像。编写一个poster工具通过命令行输入的电影名字下载对应的海报。</p>
<div style="break-before: page; page-break-before: always;"></div><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="ch4/../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="ch4/../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="ch4/../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>
<div style="break-before: page; page-break-before: always;"></div><h1 id="第5章-函数"><a class="header" href="#第5章-函数">第5章 函数</a></h1>
<p>函数可以让我们将一个语句序列打包为一个单元,然后可以从程序中其它地方多次调用。函数的机制可以让我们将一个大的工作分解为小的任务,这样的小任务可以让不同程序员在不同时间、不同地方独立完成。一个函数同时对用户隐藏了其实现细节。由于这些因素,对于任何编程语言来说,函数都是一个至关重要的部分。</p>
<p>我们已经见过许多函数了。现在让我们多花一点时间来彻底地讨论函数特性。本章的运行示例是一个网络蜘蛛也就是web搜索引擎中负责抓取网页部分的组件它们根据抓取网页中的链接继续抓取链接指向的页面。一个网络蜘蛛的例子给我们足够的机会去探索递归函数、匿名函数、错误处理和函数其它的很多特性。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="51-函数声明"><a class="header" href="#51-函数声明">5.1. 函数声明</a></h2>
<p>函数声明包括函数名、形式参数列表、返回值列表(可省略)以及函数体。</p>
<pre><code class="language-Go">func name(parameter-list) (result-list) {
body
}
</code></pre>
<p>形式参数列表描述了函数的参数名以及参数类型。这些参数作为局部变量其值由参数调用者提供。返回值列表描述了函数返回值的变量名以及类型。如果函数返回一个无名变量或者没有返回值返回值列表的括号是可以省略的。如果一个函数声明不包括返回值列表那么函数体执行完毕后不会返回任何值。在hypot函数中</p>
<pre><code class="language-Go">func hypot(x, y float64) float64 {
return math.Sqrt(x*x + y*y)
}
fmt.Println(hypot(3,4)) // &quot;5&quot;
</code></pre>
<p>x和y是形参名3和4是调用时的传入的实参函数返回了一个float64类型的值。
返回值也可以像形式参数一样被命名。在这种情况下,每个返回值被声明成一个局部变量,并根据该返回值的类型,将其初始化为该类型的零值。
如果一个函数在声明时,包含返回值列表,该函数必须以 return语句结尾除非函数明显无法运行到结尾处。例如函数在结尾时调用了panic异常或函数中存在无限循环。</p>
<p>正如hypot一样如果一组形参或返回值有相同的类型我们不必为每个形参都写出参数类型。下面2个声明是等价的</p>
<pre><code class="language-Go">func f(i, j, k int, s, t string) { /* ... */ }
func f(i int, j int, k int, s string, t string) { /* ... */ }
</code></pre>
<p>下面我们给出4种方法声明拥有2个int型参数和1个int型返回值的函数.blank identifier(译者注即下文的_符号)可以强调某个参数未被使用。</p>
<pre><code class="language-Go">func add(x int, y int) int {return x + y}
func sub(x, y int) (z int) { z = x - y; return}
func first(x int, _ int) int { return x }
func zero(int, int) int { return 0 }
fmt.Printf(&quot;%T\n&quot;, add) // &quot;func(int, int) int&quot;
fmt.Printf(&quot;%T\n&quot;, sub) // &quot;func(int, int) int&quot;
fmt.Printf(&quot;%T\n&quot;, first) // &quot;func(int, int) int&quot;
fmt.Printf(&quot;%T\n&quot;, zero) // &quot;func(int, int) int&quot;
</code></pre>
<p>函数的类型被称为函数的签名。如果两个函数形式参数列表和返回值列表中的变量类型一一对应,那么这两个函数被认为有相同的类型或签名。形参和返回值的变量名不影响函数签名,也不影响它们是否可以以省略参数类型的形式表示。</p>
<p>每一次函数调用都必须按照声明顺序为所有参数提供实参参数值。在函数调用时Go语言没有默认参数值也没有任何方法可以通过参数名指定形参因此形参和返回值的变量名对于函数调用者而言没有意义。</p>
<p>在函数体中,函数的形参作为局部变量,被初始化为调用者提供的值。函数的形参和有名返回值作为函数最外层的局部变量,被存储在相同的词法块中。</p>
<p>实参通过值的方式传递因此函数的形参是实参的拷贝。对形参进行修改不会影响实参。但是如果实参包括引用类型如指针slice(切片)、map、function、channel等类型实参可能会由于函数的间接引用被修改。</p>
<p>你可能会偶尔遇到没有函数体的函数声明这表示该函数不是以Go实现的。这样的声明定义了函数签名。</p>
<pre><code class="language-Go">package math
func Sin(x float64) float //implemented in assembly language
</code></pre>
<div style="break-before: page; page-break-before: always;"></div><h2 id="52-递归"><a class="header" href="#52-递归">5.2. 递归</a></h2>
<p>函数可以是递归的这意味着函数可以直接或间接的调用自身。对许多问题而言递归是一种强有力的技术例如处理递归的数据结构。在4.4节我们通过遍历二叉树来实现简单的插入排序在本章节我们再次使用它来处理HTML文件。</p>
<p>下文的示例代码使用了非标准包 golang.org/x/net/html 解析HTML。golang.org/x/... 目录下存储了一些由Go团队设计、维护对网络编程、国际化文件处理、移动平台、图像处理、加密解密、开发者工具提供支持的扩展包。未将这些扩展包加入到标准库原因有二一是部分包仍在开发中二是对大多数Go语言的开发者而言扩展包提供的功能很少被使用。</p>
<p>例子中调用golang.org/x/net/html的部分api如下所示。html.Parse函数读入一组bytes解析后返回html.Node类型的HTML页面树状结构根节点。HTML拥有很多类型的结点如text文本、commnets注释类型在下面的例子中我们 只关注&lt; name key='value' &gt;形式的结点。</p>
<p><u><i>golang.org/x/net/html</i></u></p>
<pre><code class="language-Go">package html
type Node struct {
Type NodeType
Data string
Attr []Attribute
FirstChild, NextSibling *Node
}
type NodeType int32
const (
ErrorNode NodeType = iota
TextNode
DocumentNode
ElementNode
CommentNode
DoctypeNode
)
type Attribute struct {
Key, Val string
}
func Parse(r io.Reader) (*Node, error)
</code></pre>
<p>main函数解析HTML标准输入通过递归函数visit获得links链接并打印出这些links</p>
<p><u><i>gopl.io/ch5/findlinks1</i></u></p>
<pre><code class="language-Go">// Findlinks1 prints the links in an HTML document read from standard input.
package main
import (
&quot;fmt&quot;
&quot;os&quot;
&quot;golang.org/x/net/html&quot;
)
func main() {
doc, err := html.Parse(os.Stdin)
if err != nil {
fmt.Fprintf(os.Stderr, &quot;findlinks1: %v\n&quot;, err)
os.Exit(1)
}
for _, link := range visit(nil, doc) {
fmt.Println(link)
}
}
</code></pre>
<p>visit函数遍历HTML的节点树从每一个anchor元素的href属性获得link,将这些links存入字符串数组中并返回这个字符串数组。</p>
<pre><code class="language-Go">// visit appends to links each link found in n and returns the result.
func visit(links []string, n *html.Node) []string {
if n.Type == html.ElementNode &amp;&amp; n.Data == &quot;a&quot; {
for _, a := range n.Attr {
if a.Key == &quot;href&quot; {
links = append(links, a.Val)
}
}
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
links = visit(links, c)
}
return links
}
</code></pre>
<p>为了遍历结点n的所有后代结点每次遇到n的孩子结点时visit递归的调用自身。这些孩子结点存放在FirstChild链表中。</p>
<p>让我们以Go的主页golang.org作为目标运行findlinks。我们以fetch1.5章的输出作为findlinks的输入。下面的输出做了简化处理。</p>
<pre><code>$ go build gopl.io/ch1/fetch
$ go build gopl.io/ch5/findlinks1
$ ./fetch https://golang.org | ./findlinks1
#
/doc/
/pkg/
/help/
/blog/
http://play.golang.org/
//tour.golang.org/
https://golang.org/dl/
//blog.golang.org/
/LICENSE
/doc/tos.html
http://www.google.com/intl/en/policies/privacy/
</code></pre>
<p>注意在页面中出现的链接格式,在之后我们会介绍如何将这些链接,根据根路径( https://golang.org 生成可以直接访问的url。</p>
<p>在函数outline中我们通过递归的方式遍历整个HTML结点树并输出树的结构。在outline内部每遇到一个HTML元素标签就将其入栈并输出。</p>
<p><u><i>gopl.io/ch5/outline</i></u></p>
<pre><code class="language-Go">func main() {
doc, err := html.Parse(os.Stdin)
if err != nil {
fmt.Fprintf(os.Stderr, &quot;outline: %v\n&quot;, err)
os.Exit(1)
}
outline(nil, doc)
}
func outline(stack []string, n *html.Node) {
if n.Type == html.ElementNode {
stack = append(stack, n.Data) // push tag
fmt.Println(stack)
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
outline(stack, c)
}
}
</code></pre>
<p>有一点值得注意outline有入栈操作但没有相对应的出栈操作。当outline调用自身时被调用者接收的是stack的拷贝。被调用者对stack的元素追加操作修改的是stack的拷贝其可能会修改slice底层的数组甚至是申请一块新的内存空间进行扩容但这个过程并不会修改调用方的stack。因此当函数返回时调用方的stack与其调用自身之前完全一致。</p>
<p>下面是 https://golang.org 页面的简要结构:</p>
<pre><code>$ go build gopl.io/ch5/outline
$ ./fetch https://golang.org | ./outline
[html]
[html head]
[html head meta]
[html head title]
[html head link]
[html body]
[html body div]
[html body div]
[html body div div]
[html body div div form]
[html body div div form div]
[html body div div form div a]
...
</code></pre>
<p>正如你在上面实验中所见大部分HTML页面只需几层递归就能被处理但仍然有些页面需要深层次的递归。</p>
<p>大部分编程语言使用固定大小的函数调用栈常见的大小从64KB到2MB不等。固定大小栈会限制递归的深度当你用递归处理大量数据时需要避免栈溢出除此之外还会导致安全性问题。与此相反Go语言使用可变栈栈的大小按需增加初始时很小。这使得我们使用递归时不必考虑溢出和安全问题。</p>
<p><strong>练习 5.1</strong> 修改findlinks代码中遍历n.FirstChild链表的部分将循环调用visit改成递归调用。</p>
<p><strong>练习 5.2</strong> 编写函数记录在HTML树中出现的同名元素的次数。</p>
<p><strong>练习 5.3</strong> 编写函数输出所有text结点的内容。注意不要访问<code>&lt;script&gt;</code><code>&lt;style&gt;</code>元素,因为这些元素对浏览者是不可见的。</p>
<p><strong>练习 5.4</strong> 扩展visit函数使其能够处理其他类型的结点如images、scripts和style sheets。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="53-多返回值"><a class="header" href="#53-多返回值">5.3. 多返回值</a></h2>
<p>在Go中一个函数可以返回多个值。我们已经在之前例子中看到许多标准库中的函数返回2个值一个是期望得到的返回值另一个是函数出错时的错误信息。下面的例子会展示如何编写多返回值的函数。</p>
<p>下面的程序是findlinks的改进版本。修改后的findlinks可以自己发起HTTP请求这样我们就不必再运行fetch。因为HTTP请求和解析操作可能会失败因此findlinks声明了2个返回值链接列表和错误信息。一般而言HTML的解析器可以处理HTML页面的错误结点构造出HTML页面结构所以解析HTML很少失败。这意味着如果findlinks函数失败了很可能是由于I/O的错误导致的。</p>
<p><u><i>gopl.io/ch5/findlinks2</i></u></p>
<pre><code class="language-Go">func main() {
for _, url := range os.Args[1:] {
links, err := findLinks(url)
if err != nil {
fmt.Fprintf(os.Stderr, &quot;findlinks2: %v\n&quot;, err)
continue
}
for _, link := range links {
fmt.Println(link)
}
}
}
// findLinks performs an HTTP GET request for url, parses the
// response as HTML, and extracts and returns the links.
func findLinks(url string) ([]string, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
return nil, fmt.Errorf(&quot;getting %s: %s&quot;, url, resp.Status)
}
doc, err := html.Parse(resp.Body)
resp.Body.Close()
if err != nil {
return nil, fmt.Errorf(&quot;parsing %s as HTML: %v&quot;, url, err)
}
return visit(nil, doc), nil
}
</code></pre>
<p>在findlinks中有4处return语句每一处return都返回了一组值。前三处return将http和html包中的错误信息传递给findlinks的调用者。第一处return直接返回错误信息其他两处通过fmt.Errorf§7.8输出详细的错误信息。如果findlinks成功结束最后的return语句将一组解析获得的连接返回给用户。</p>
<p>在findlinks中我们必须确保resp.Body被关闭释放网络资源。虽然Go的垃圾回收机制会回收不被使用的内存但是这不包括操作系统层面的资源比如打开的文件、网络连接。因此我们必须显式的释放这些资源。</p>
<p>调用多返回值函数时,返回给调用者的是一组值,调用者必须显式的将这些值分配给变量:</p>
<pre><code class="language-Go">links, err := findLinks(url)
</code></pre>
<p>如果某个值不被使用可以将其分配给blank identifier:</p>
<pre><code class="language-Go">links, _ := findLinks(url) // errors ignored
</code></pre>
<p>一个函数内部可以将另一个有多返回值的函数调用作为返回值下面的例子展示了与findLinks有相同功能的函数两者的区别在于下面的例子先输出参数</p>
<pre><code class="language-Go">func findLinksLog(url string) ([]string, error) {
log.Printf(&quot;findLinks %s&quot;, url)
return findLinks(url)
}
</code></pre>
<p>当你调用接受多参数的函数时可以将一个返回多参数的函数调用作为该函数的参数。虽然这很少出现在实际生产代码中但这个特性在debug时很方便我们只需要一条语句就可以输出所有的返回值。下面的代码是等价的</p>
<pre><code class="language-Go">log.Println(findLinks(url))
links, err := findLinks(url)
log.Println(links, err)
</code></pre>
<p>准确的变量名可以传达函数返回值的含义。尤其在返回值的类型都相同时,就像下面这样:</p>
<pre><code class="language-Go">func Size(rect image.Rectangle) (width, height int)
func Split(path string) (dir, file string)
func HourMinSec(t time.Time) (hour, minute, second int)
</code></pre>
<p>虽然良好的命名很重要但你也不必为每一个返回值都取一个适当的名字。比如按照惯例函数的最后一个bool类型的返回值表示函数是否运行成功error类型的返回值代表函数的错误信息对于这些类似的惯例我们不必思考合适的命名它们都无需解释。</p>
<p>如果一个函数所有的返回值都有显式的变量名那么该函数的return语句可以省略操作数。这称之为bare return。</p>
<pre><code class="language-Go">// CountWordsAndImages does an HTTP GET request for the HTML
// document url and returns the number of words and images in it.
func CountWordsAndImages(url string) (words, images int, err error) {
resp, err := http.Get(url)
if err != nil {
return
}
doc, err := html.Parse(resp.Body)
resp.Body.Close()
if err != nil {
err = fmt.Errorf(&quot;parsing HTML: %s&quot;, err)
return
}
words, images = countWordsAndImages(doc)
return
}
func countWordsAndImages(n *html.Node) (words, images int) { /* ... */ }
</code></pre>
<p>按照返回值列表的次序返回所有的返回值在上面的例子中每一个return语句等价于</p>
<pre><code class="language-Go">return words, images, err
</code></pre>
<p>当一个函数有多处return语句以及许多返回值时bare return 可以减少代码的重复但是使得代码难以被理解。举个例子如果你没有仔细的审查代码很难发现前2处return等价于 return 0,0,errGo会将返回值 words和images在函数体的开始处根据它们的类型将其初始化为0最后一处return等价于 return words, image, nil。基于以上原因不宜过度使用bare return。</p>
<p><strong>练习 5.5</strong> 实现countWordsAndImages。参考练习4.9如何分词)</p>
<p><strong>练习 5.6</strong> 修改gopl.io/ch3/surface§3.2中的corner函数将返回值命名并使用bare return。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="54-错误"><a class="header" href="#54-错误">5.4. 错误</a></h2>
<p>在Go中有一部分函数总是能成功的运行。比如strings.Contains和strconv.FormatBool函数对各种可能的输入都做了良好的处理使得运行时几乎不会失败除非遇到灾难性的、不可预料的情况比如运行时的内存溢出。导致这种错误的原因很复杂难以处理从错误中恢复的可能性也很低。</p>
<p>还有一部分函数只要输入的参数满足一定条件也能保证运行成功。比如time.Date函数该函数将年月日等参数构造成time.Time对象除非最后一个参数时区是nil。这种情况下会引发panic异常。panic是来自被调用函数的信号表示发生了某个已知的bug。一个良好的程序永远不应该发生panic异常。</p>
<p>对于大部分函数而言永远无法确保能否成功运行。这是因为错误的原因超出了程序员的控制。举个例子任何进行I/O操作的函数都会面临出现错误的可能只有没有经验的程序员才会相信读写操作不会失败即使是简单的读写。因此当本该可信的操作出乎意料的失败后我们必须弄清楚导致失败的原因。</p>
<p>在Go的错误处理中错误是软件包API和应用程序用户界面的一个重要组成部分程序运行失败仅被认为是几个预期的结果之一。</p>
<p>对于那些将运行失败看作是预期结果的函数它们会返回一个额外的返回值通常是最后一个来传递错误信息。如果导致失败的原因只有一个额外的返回值可以是一个布尔值通常被命名为ok。比如cache.Lookup失败的唯一原因是key不存在那么代码可以按照下面的方式组织</p>
<pre><code class="language-Go">value, ok := cache.Lookup(key)
if !ok {
// ...cache[key] does not exist…
}
</code></pre>
<p>通常导致失败的原因不止一种尤其是对I/O操作而言用户需要了解更多的错误信息。因此额外的返回值不再是简单的布尔类型而是error类型。</p>
<p>内置的error是接口类型。我们将在第七章了解接口类型的含义以及它对错误处理的影响。现在我们只需要明白error类型可能是nil或者non-nil。nil意味着函数运行成功non-nil表示失败。对于non-nil的error类型我们可以通过调用error的Error函数或者输出函数获得字符串类型的错误信息。</p>
<pre><code class="language-Go">fmt.Println(err)
fmt.Printf(&quot;%v&quot;, err)
</code></pre>
<p>通常当函数返回non-nil的error时其他的返回值是未定义的undefined这些未定义的返回值应该被忽略。然而有少部分函数在发生错误时仍然会返回一些有用的返回值。比如当读取文件发生错误时Read函数会返回可以读取的字节数以及错误信息。对于这种情况正确的处理方式应该是先处理这些不完整的数据再处理错误。因此对函数的返回值要有清晰的说明以便于其他人使用。</p>
<p>在Go中函数运行失败时会返回错误信息这些错误信息被认为是一种预期的值而非异常exception这使得Go有别于那些将函数运行失败看作是异常的语言。虽然Go有各种异常机制但这些机制仅被使用在处理那些未被预料到的错误即bug而不是那些在健壮程序中应该被避免的程序错误。对于Go的异常机制我们将在5.9介绍。</p>
<p>Go这样设计的原因是由于对于某个应该在控制流程中处理的错误而言将这个错误以异常的形式抛出会混乱对错误的描述这通常会导致一些糟糕的后果。当某个程序错误被当作异常处理后这个错误会将堆栈跟踪信息返回给终端用户这些信息复杂且无用无法帮助定位错误。</p>
<p>正因此Go使用控制流机制如if和return处理错误这使得编码人员能更多的关注错误处理。</p>
<h3 id="541-错误处理策略"><a class="header" href="#541-错误处理策略">5.4.1. 错误处理策略</a></h3>
<p>当一次函数调用返回错误时,调用者应该选择合适的方式处理错误。根据情况的不同,有很多处理方式,让我们来看看常用的五种方式。</p>
<p>首先也是最常用的方式是传播错误。这意味着函数中某个子程序的失败会变成该函数的失败。下面我们以5.3节的findLinks函数作为例子。如果findLinks对http.Get的调用失败findLinks会直接将这个HTTP错误返回给调用者</p>
<pre><code class="language-Go">resp, err := http.Get(url)
if err != nil{
return nil, err
}
</code></pre>
<p>当对html.Parse的调用失败时findLinks不会直接返回html.Parse的错误因为缺少两条重要信息1、发生错误时的解析器html parser2、发生错误的url。因此findLinks构造了一个新的错误信息既包含了这两项也包括了底层的解析出错的信息。</p>
<pre><code class="language-Go">doc, err := html.Parse(resp.Body)
resp.Body.Close()
if err != nil {
return nil, fmt.Errorf(&quot;parsing %s as HTML: %v&quot;, url,err)
}
</code></pre>
<p>fmt.Errorf函数使用fmt.Sprintf格式化错误信息并返回。我们使用该函数添加额外的前缀上下文信息到原始错误信息。当错误最终由main函数处理时错误信息应提供清晰的从原因到后果的因果链就像美国宇航局事故调查时做的那样</p>
<pre><code>genesis: crashed: no parachute: G-switch failed: bad relay orientation
</code></pre>
<p>由于错误信息经常是以链式组合在一起的所以错误信息中应避免大写和换行符。最终的错误信息可能很长我们可以通过类似grep的工具处理错误信息译者注grep是一种文本搜索工具</p>
<p>编写错误信息时,我们要确保错误信息对问题细节的描述是详尽的。尤其是要注意错误信息表达的一致性,即相同的函数或同包内的同一组函数返回的错误在构成和处理方式上是相似的。</p>
<p>以os包为例os包确保文件操作如os.Open、Read、Write、Close返回的每个错误的描述不仅仅包含错误的原因如无权限文件目录不存在也包含文件名这样调用者在构造新的错误信息时无需再添加这些信息。</p>
<p>一般而言被调用函数f(x)会将调用信息和参数信息作为发生错误时的上下文放在错误信息中并返回给调用者调用者需要添加一些错误信息中不包含的信息比如添加url到html.Parse返回的错误中。</p>
<p>让我们来看看处理错误的第二种策略。如果错误的发生是偶然性的,或由不可预知的问题导致的。一个明智的选择是重新尝试失败的操作。在重试时,我们需要限制重试的时间间隔或重试的次数,防止无限制的重试。</p>
<p><u><i>gopl.io/ch5/wait</i></u></p>
<pre><code class="language-Go">// WaitForServer attempts to contact the server of a URL.
// It tries for one minute using exponential back-off.
// It reports an error if all attempts fail.
func WaitForServer(url string) error {
const timeout = 1 * time.Minute
deadline := time.Now().Add(timeout)
for tries := 0; time.Now().Before(deadline); tries++ {
_, err := http.Head(url)
if err == nil {
return nil // success
}
log.Printf(&quot;server not responding (%s);retrying…&quot;, err)
time.Sleep(time.Second &lt;&lt; uint(tries)) // exponential back-off
}
return fmt.Errorf(&quot;server %s failed to respond after %s&quot;, url, timeout)
}
</code></pre>
<p>如果错误发生后程序无法继续运行我们就可以采用第三种策略输出错误信息并结束程序。需要注意的是这种策略只应在main中执行。对库函数而言应仅向上传播错误除非该错误意味着程序内部包含不一致性即遇到了bug才能在库函数中结束程序。</p>
<pre><code class="language-Go">// (In function main.)
if err := WaitForServer(url); err != nil {
fmt.Fprintf(os.Stderr, &quot;Site is down: %v\n&quot;, err)
os.Exit(1)
}
</code></pre>
<p>调用log.Fatalf可以更简洁的代码达到与上文相同的效果。log中的所有函数都默认会在错误信息之前输出时间信息。</p>
<pre><code class="language-Go">if err := WaitForServer(url); err != nil {
log.Fatalf(&quot;Site is down: %v\n&quot;, err)
}
</code></pre>
<p>长时间运行的服务器常采用默认的时间格式,而交互式工具很少采用包含如此多信息的格式。</p>
<pre><code>2006/01/02 15:04:05 Site is down: no such domain:
bad.gopl.io
</code></pre>
<p>我们可以设置log的前缀信息屏蔽时间信息一般而言前缀信息会被设置成命令名。</p>
<pre><code class="language-Go">log.SetPrefix(&quot;wait: &quot;)
log.SetFlags(0)
</code></pre>
<p>第四种策略有时我们只需要输出错误信息就足够了不需要中断程序的运行。我们可以通过log包提供函数</p>
<pre><code class="language-Go">if err := Ping(); err != nil {
log.Printf(&quot;ping failed: %v; networking disabled&quot;,err)
}
</code></pre>
<p>或者标准错误流输出错误信息。</p>
<pre><code class="language-Go">if err := Ping(); err != nil {
fmt.Fprintf(os.Stderr, &quot;ping failed: %v; networking disabled\n&quot;, err)
}
</code></pre>
<p>log包中的所有函数会为没有换行符的字符串增加换行符。</p>
<p>第五种,也是最后一种策略:我们可以直接忽略掉错误。</p>
<pre><code class="language-Go">dir, err := ioutil.TempDir(&quot;&quot;, &quot;scratch&quot;)
if err != nil {
return fmt.Errorf(&quot;failed to create temp dir: %v&quot;,err)
}
// ...use temp dir…
os.RemoveAll(dir) // ignore errors; $TMPDIR is cleaned periodically
</code></pre>
<p>尽管os.RemoveAll会失败但上面的例子并没有做错误处理。这是因为操作系统会定期的清理临时目录。正因如此虽然程序没有处理错误但程序的逻辑不会因此受到影响。我们应该在每次函数调用后都养成考虑错误处理的习惯当你决定忽略某个错误时你应该清晰地写下你的意图。</p>
<p>在Go中错误处理有一套独特的编码风格。检查某个子函数是否失败后我们通常将处理失败的逻辑代码放在处理成功的代码之前。如果某个错误会导致函数返回那么成功时的逻辑代码不应放在else语句块中而应直接放在函数体中。Go中大部分函数的代码结构几乎相同首先是一系列的初始检查防止错误发生之后是函数的实际逻辑。</p>
<h3 id="542-文件结尾错误eof"><a class="header" href="#542-文件结尾错误eof">5.4.2. 文件结尾错误EOF</a></h3>
<p>函数经常会返回多种错误这对终端用户来说可能会很有趣但对程序而言这使得情况变得复杂。很多时候程序必须根据错误类型作出不同的响应。让我们考虑这样一个例子从文件中读取n个字节。如果n等于文件的长度读取过程的任何错误都表示失败。如果n小于文件的长度调用者会重复的读取固定大小的数据直到文件结束。这会导致调用者必须分别处理由文件结束引起的各种错误。基于这样的原因io包保证任何由文件结束引起的读取失败都返回同一个错误——io.EOF该错误在io包中定义</p>
<pre><code class="language-Go">package io
import &quot;errors&quot;
// EOF is the error returned by Read when no more input is available.
var EOF = errors.New(&quot;EOF&quot;)
</code></pre>
<p>调用者只需通过简单的比较就可以检测出这个错误。下面的例子展示了如何从标准输入中读取字符以及判断文件结束。4.3的chartcount程序展示了更加复杂的代码</p>
<pre><code class="language-Go">in := bufio.NewReader(os.Stdin)
for {
r, _, err := in.ReadRune()
if err == io.EOF {
break // finished reading
}
if err != nil {
return fmt.Errorf(&quot;read failed:%v&quot;, err)
}
// ...use r…
}
</code></pre>
<p>因为文件结束这种错误不需要更多的描述所以io.EOF有固定的错误信息——“EOF”。对于其他错误我们可能需要在错误信息中描述错误的类型和数量这使得我们不能像io.EOF一样采用固定的错误信息。在7.11节中,我们会提出更系统的方法区分某些固定的错误值。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="55-函数值"><a class="header" href="#55-函数值">5.5. 函数值</a></h2>
<p>在Go中函数被看作第一类值first-class values函数像其他值一样拥有类型可以被赋值给其他变量传递给函数从函数返回。对函数值function value的调用类似函数调用。例子如下</p>
<pre><code class="language-Go"> func square(n int) int { return n * n }
func negative(n int) int { return -n }
func product(m, n int) int { return m * n }
f := square
fmt.Println(f(3)) // &quot;9&quot;
f = negative
fmt.Println(f(3)) // &quot;-3&quot;
fmt.Printf(&quot;%T\n&quot;, f) // &quot;func(int) int&quot;
f = product // compile error: can't assign func(int, int) int to func(int) int
</code></pre>
<p>函数类型的零值是nil。调用值为nil的函数值会引起panic错误</p>
<pre><code class="language-Go"> var f func(int) int
f(3) // 此处f的值为nil, 会引起panic错误
</code></pre>
<p>函数值可以与nil比较</p>
<pre><code class="language-Go"> var f func(int) int
if f != nil {
f(3)
}
</code></pre>
<p>但是函数值之间是不可比较的也不能用函数值作为map的key。</p>
<p>函数值使得我们不仅仅可以通过数据来参数化函数亦可通过行为。标准库中包含许多这样的例子。下面的代码展示了如何使用这个技巧。strings.Map对字符串中的每个字符调用add1函数并将每个add1函数的返回值组成一个新的字符串返回给调用者。</p>
<pre><code class="language-Go"> func add1(r rune) rune { return r + 1 }
fmt.Println(strings.Map(add1, &quot;HAL-9000&quot;)) // &quot;IBM.:111&quot;
fmt.Println(strings.Map(add1, &quot;VMS&quot;)) // &quot;WNT&quot;
fmt.Println(strings.Map(add1, &quot;Admix&quot;)) // &quot;Benjy&quot;
</code></pre>
<p>5.2节的findLinks函数使用了辅助函数visit遍历和操作了HTML页面的所有结点。使用函数值我们可以将遍历结点的逻辑和操作结点的逻辑分离使得我们可以复用遍历的逻辑从而对结点进行不同的操作。</p>
<p><u><i>gopl.io/ch5/outline2</i></u></p>
<pre><code class="language-Go">// forEachNode针对每个结点x都会调用pre(x)和post(x)。
// pre和post都是可选的。
// 遍历孩子结点之前pre被调用
// 遍历孩子结点之后post被调用
func forEachNode(n *html.Node, pre, post func(n *html.Node)) {
if pre != nil {
pre(n)
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
forEachNode(c, pre, post)
}
if post != nil {
post(n)
}
}
</code></pre>
<p>该函数接收2个函数作为参数分别在结点的孩子被访问前和访问后调用。这样的设计给调用者更大的灵活性。举个例子现在我们有startElement和endElement两个函数用于输出HTML元素的开始标签和结束标签<code>&lt;b&gt;...&lt;/b&gt;</code></p>
<pre><code class="language-Go">var depth int
func startElement(n *html.Node) {
if n.Type == html.ElementNode {
fmt.Printf(&quot;%*s&lt;%s&gt;\n&quot;, depth*2, &quot;&quot;, n.Data)
depth++
}
}
func endElement(n *html.Node) {
if n.Type == html.ElementNode {
depth--
fmt.Printf(&quot;%*s&lt;/%s&gt;\n&quot;, depth*2, &quot;&quot;, n.Data)
}
}
</code></pre>
<p>上面的代码利用fmt.Printf的一个小技巧控制输出的缩进。<code>%*s</code>中的<code>*</code>会在字符串之前填充一些空格。在例子中,每次输出会先填充<code>depth*2</code>数量的空格,再输出&quot;&quot;最后再输出HTML标签。</p>
<p>如果我们像下面这样调用forEachNode</p>
<pre><code class="language-Go">forEachNode(doc, startElement, endElement)
</code></pre>
<p>与之前的outline程序相比我们得到了更加详细的页面结构</p>
<pre><code>$ go build gopl.io/ch5/outline2
$ ./outline2 http://gopl.io
&lt;html&gt;
&lt;head&gt;
&lt;meta&gt;
&lt;/meta&gt;
&lt;title&gt;
&lt;/title&gt;
&lt;style&gt;
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;table&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a&gt;
&lt;img&gt;
&lt;/img&gt;
...
</code></pre>
<p><strong>练习 5.7</strong> 完善startElement和endElement函数使其成为通用的HTML输出器。要求输出注释结点文本结点以及每个元素的属性&lt; a href='...'&gt;)。使用简略格式输出没有孩子结点的元素(即用<code>&lt;img/&gt;</code>代替<code>&lt;img&gt;&lt;/img&gt;</code>。编写测试验证程序输出的格式正确。详见11章</p>
<p><strong>练习 5.8</strong> 修改pre和post函数使其返回布尔类型的返回值。返回false时中止forEachNoded的遍历。使用修改后的代码编写ElementByID函数根据用户输入的id查找第一个拥有该id元素的HTML元素查找成功后停止遍历。</p>
<pre><code class="language-Go">func ElementByID(doc *html.Node, id string) *html.Node
</code></pre>
<p><strong>练习 5.9</strong> 编写函数expand将s中的&quot;foo&quot;替换为f(&quot;foo&quot;)的返回值。</p>
<pre><code class="language-Go">func expand(s string, f func(string) string) string
</code></pre>
<div style="break-before: page; page-break-before: always;"></div><h2 id="56-匿名函数"><a class="header" href="#56-匿名函数">5.6. 匿名函数</a></h2>
<p>拥有函数名的函数只能在包级语法块中被声明通过函数字面量function literal我们可绕过这一限制在任何表达式中表示一个函数值。函数字面量的语法和函数声明相似区别在于func关键字后没有函数名。函数值字面量是一种表达式它的值被称为匿名函数anonymous function</p>
<p>函数字面量允许我们在使用函数时再定义它。通过这种技巧我们可以改写之前对strings.Map的调用</p>
<pre><code class="language-Go">strings.Map(func(r rune) rune { return r + 1 }, &quot;HAL-9000&quot;)
</code></pre>
<p>更为重要的是通过这种方式定义的函数可以访问完整的词法环境lexical environment这意味着在函数中定义的内部函数可以引用该函数的变量如下例所示</p>
<p><u><i>gopl.io/ch5/squares</i></u></p>
<pre><code class="language-Go">// squares返回一个匿名函数。
// 该匿名函数每次被调用时都会返回下一个数的平方。
func squares() func() int {
var x int
return func() int {
x++
return x * x
}
}
func main() {
f := squares()
fmt.Println(f()) // &quot;1&quot;
fmt.Println(f()) // &quot;4&quot;
fmt.Println(f()) // &quot;9&quot;
fmt.Println(f()) // &quot;16&quot;
}
</code></pre>
<p>函数squares返回另一个类型为 func() int 的函数。对squares的一次调用会生成一个局部变量x并返回一个匿名函数。每次调用匿名函数时该函数都会先使x的值加1再返回x的平方。第二次调用squares时会生成第二个x变量并返回一个新的匿名函数。新匿名函数操作的是第二个x变量。</p>
<p>squares的例子证明函数值不仅仅是一串代码还记录了状态。在squares中定义的匿名内部函数可以访问和更新squares中的局部变量这意味着匿名函数和squares中存在变量引用。这就是函数值属于引用类型和函数值不可比较的原因。Go使用闭包closures技术实现函数值Go程序员也把函数值叫做闭包。</p>
<p>通过这个例子我们看到变量的生命周期不由它的作用域决定squares返回后变量x仍然隐式的存在于f中。</p>
<p>接下来,我们讨论一个有点学术性的例子,考虑这样一个问题:给定一些计算机课程,每个课程都有前置课程,只有完成了前置课程才可以开始当前课程的学习;我们的目标是选择出一组课程,这组课程必须确保按顺序学习时,能全部被完成。每个课程的前置课程如下:</p>
<p><u><i>gopl.io/ch5/toposort</i></u></p>
<pre><code class="language-Go">// prereqs记录了每个课程的前置课程
var prereqs = map[string][]string{
&quot;algorithms&quot;: {&quot;data structures&quot;},
&quot;calculus&quot;: {&quot;linear algebra&quot;},
&quot;compilers&quot;: {
&quot;data structures&quot;,
&quot;formal languages&quot;,
&quot;computer organization&quot;,
},
&quot;data structures&quot;: {&quot;discrete math&quot;},
&quot;databases&quot;: {&quot;data structures&quot;},
&quot;discrete math&quot;: {&quot;intro to programming&quot;},
&quot;formal languages&quot;: {&quot;discrete math&quot;},
&quot;networks&quot;: {&quot;operating systems&quot;},
&quot;operating systems&quot;: {&quot;data structures&quot;, &quot;computer organization&quot;},
&quot;programming languages&quot;: {&quot;data structures&quot;, &quot;computer organization&quot;},
}
</code></pre>
<p>这类问题被称作拓扑排序。从概念上说,前置条件可以构成有向图。图中的顶点表示课程,边表示课程间的依赖关系。显然,图中应该无环,这也就是说从某点出发的边,最终不会回到该点。下面的代码用深度优先搜索了整张图,获得了符合要求的课程序列。</p>
<pre><code class="language-Go">func main() {
for i, course := range topoSort(prereqs) {
fmt.Printf(&quot;%d:\t%s\n&quot;, i+1, course)
}
}
func topoSort(m map[string][]string) []string {
var order []string
seen := make(map[string]bool)
var visitAll func(items []string)
visitAll = func(items []string) {
for _, item := range items {
if !seen[item] {
seen[item] = true
visitAll(m[item])
order = append(order, item)
}
}
}
var keys []string
for key := range m {
keys = append(keys, key)
}
sort.Strings(keys)
visitAll(keys)
return order
}
</code></pre>
<p>当匿名函数需要被递归调用时,我们必须首先声明一个变量(在上面的例子中,我们首先声明了 visitAll再将匿名函数赋值给这个变量。如果不分成两步函数字面量无法与visitAll绑定我们也无法递归调用该匿名函数。</p>
<pre><code class="language-Go">visitAll := func(items []string) {
// ...
visitAll(m[item]) // compile error: undefined: visitAll
// ...
}
</code></pre>
<p>在toposort程序的输出如下所示它的输出顺序是大多人想看到的固定顺序输出但是这需要我们多花点心思才能做到。哈希表prepreqs的value是遍历顺序固定的切片而不再试遍历顺序随机的map所以我们对prereqs的key值进行排序保证每次运行toposort程序都以相同的遍历顺序遍历prereqs。</p>
<pre><code>1: intro to programming
2: discrete math
3: data structures
4: algorithms
5: linear algebra
6: calculus
7: formal languages
8: computer organization
9: compilers
10: databases
11: operating systems
12: networks
13: programming languages
</code></pre>
<p>让我们回到findLinks这个例子。我们将代码移动到了links包下将函数重命名为Extract在第八章我们会再次用到这个函数。新的匿名函数被引入用于替换原来的visit函数。该匿名函数负责将新连接添加到切片中。在Extract中使用forEachNode遍历HTML页面由于Extract只需要在遍历结点前操作结点所以forEachNode的post参数被传入nil。</p>
<p><u><i>gopl.io/ch5/links</i></u></p>
<pre><code class="language-Go">// Package links provides a link-extraction function.
package links
import (
&quot;fmt&quot;
&quot;net/http&quot;
&quot;golang.org/x/net/html&quot;
)
// Extract makes an HTTP GET request to the specified URL, parses
// the response as HTML, and returns the links in the HTML document.
func Extract(url string) ([]string, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
return nil, fmt.Errorf(&quot;getting %s: %s&quot;, url, resp.Status)
}
doc, err := html.Parse(resp.Body)
resp.Body.Close()
if err != nil {
return nil, fmt.Errorf(&quot;parsing %s as HTML: %v&quot;, url, err)
}
var links []string
visitNode := func(n *html.Node) {
if n.Type == html.ElementNode &amp;&amp; n.Data == &quot;a&quot; {
for _, a := range n.Attr {
if a.Key != &quot;href&quot; {
continue
}
link, err := resp.Request.URL.Parse(a.Val)
if err != nil {
continue // ignore bad URLs
}
links = append(links, link.String())
}
}
}
forEachNode(doc, visitNode, nil)
return links, nil
}
</code></pre>
<p>上面的代码对之前的版本做了改进现在links中存储的不是href属性的原始值而是通过resp.Request.URL解析后的值。解析后这些连接以绝对路径的形式存在可以直接被http.Get访问。</p>
<p>网页抓取的核心问题就是如何遍历图。在topoSort的例子中已经展示了深度优先遍历在网页抓取中我们会展示如何用广度优先遍历图。在第8章我们会介绍如何将深度优先和广度优先结合使用。</p>
<p>下面的函数实现了广度优先算法。调用者需要输入一个初始的待访问列表和一个函数f。待访问列表中的每个元素被定义为string类型。广度优先算法会为每个元素调用一次f。每次f执行完毕后会返回一组待访问元素。这些元素会被加入到待访问列表中。当待访问列表中的所有元素都被访问后breadthFirst函数运行结束。为了避免同一个元素被访问两次代码中维护了一个map。</p>
<p><u><i>gopl.io/ch5/findlinks3</i></u></p>
<pre><code class="language-Go">// breadthFirst calls f for each item in the worklist.
// Any items returned by f are added to the worklist.
// f is called at most once for each item.
func breadthFirst(f func(item string) []string, worklist []string) {
seen := make(map[string]bool)
for len(worklist) &gt; 0 {
items := worklist
worklist = nil
for _, item := range items {
if !seen[item] {
seen[item] = true
worklist = append(worklist, f(item)...)
}
}
}
}
</code></pre>
<p>就像我们在章节3解释的那样append的参数“f(item)...”会将f返回的一组元素一个个添加到worklist中。</p>
<p>在我们网页抓取器中元素的类型是url。crawl函数会将URL输出提取其中的新链接并将这些新链接返回。我们会将crawl作为参数传递给breadthFirst。</p>
<pre><code class="language-go">func crawl(url string) []string {
fmt.Println(url)
list, err := links.Extract(url)
if err != nil {
log.Print(err)
}
return list
}
</code></pre>
<p>为了使抓取器开始运行我们用命令行输入的参数作为初始的待访问url。</p>
<pre><code class="language-Go">func main() {
// Crawl the web breadth-first,
// starting from the command-line arguments.
breadthFirst(crawl, os.Args[1:])
}
</code></pre>
<p>让我们从 https://golang.org 开始,下面是程序的输出结果:</p>
<pre><code>$ go build gopl.io/ch5/findlinks3
$ ./findlinks3 https://golang.org
https://golang.org/
https://golang.org/doc/
https://golang.org/pkg/
https://golang.org/project/
https://code.google.com/p/go-tour/
https://golang.org/doc/code.html
https://www.youtube.com/watch?v=XCsL89YtqCs
http://research.swtch.com/gotour
</code></pre>
<p>当所有发现的链接都已经被访问或电脑的内存耗尽时,程序运行结束。</p>
<p><strong>练习5.10</strong> 重写topoSort函数用map代替切片并移除对key的排序代码。验证结果的正确性结果不唯一</p>
<p><strong>练习5.11</strong> 现在线性代数的老师把微积分设为了前置课程。完善topSort使其能检测有向图中的环。</p>
<p><strong>练习5.12</strong> gopl.io/ch5/outline25.5节的startElement和endElement共用了全局变量depth将它们修改为匿名函数使其共享outline中的局部变量。</p>
<p><strong>练习5.13</strong> 修改crawl使其能保存发现的页面必要时可以创建目录来保存这些页面。只保存来自原始域名下的页面。假设初始页面在golang.org下就不要保存vimeo.com下的页面。</p>
<p><strong>练习5.14</strong> 使用breadthFirst遍历其他数据结构。比如topoSort例子中的课程依赖关系有向图、个人计算机的文件层次结构你所在城市的公交或地铁线路无向图</p>
<h3 id="561-警告捕获迭代变量"><a class="header" href="#561-警告捕获迭代变量">5.6.1. 警告:捕获迭代变量</a></h3>
<p>本节将介绍Go词法作用域的一个陷阱。请务必仔细的阅读弄清楚发生问题的原因。即使是经验丰富的程序员也会在这个问题上犯错误。</p>
<p>考虑这样一个问题你被要求首先创建一些目录再将目录删除。在下面的例子中我们用函数值来完成删除操作。下面的示例代码需要引入os包。为了使代码简单我们忽略了所有的异常处理。</p>
<pre><code class="language-Go">var rmdirs []func()
for _, d := range tempDirs() {
dir := d // NOTE: necessary!
os.MkdirAll(dir, 0755) // creates parent directories too
rmdirs = append(rmdirs, func() {
os.RemoveAll(dir)
})
}
// ...do some work…
for _, rmdir := range rmdirs {
rmdir() // clean up
}
</code></pre>
<p>你可能会感到困惑为什么要在循环体中用循环变量d赋值一个新的局部变量而不是像下面的代码一样直接使用循环变量dir。需要注意下面的代码是错误的。</p>
<pre><code class="language-go">var rmdirs []func()
for _, dir := range tempDirs() {
os.MkdirAll(dir, 0755)
rmdirs = append(rmdirs, func() {
os.RemoveAll(dir) // NOTE: incorrect!
})
}
</code></pre>
<p>问题的原因在于循环变量的作用域。在上面的程序中for循环语句引入了新的词法块循环变量dir在这个词法块中被声明。在该循环中生成的所有函数值都共享相同的循环变量。需要注意函数值中记录的是循环变量的内存地址而不是循环变量某一时刻的值。以dir为例后续的迭代会不断更新dir的值当删除操作执行时for循环已完成dir中存储的值等于最后一次迭代的值。这意味着每次对os.RemoveAll的调用删除的都是相同的目录。</p>
<p>通常为了解决这个问题我们会引入一个与循环变量同名的局部变量作为循环变量的副本。比如下面的变量dir虽然这看起来很奇怪但却很有用。</p>
<pre><code class="language-Go">for _, dir := range tempDirs() {
dir := dir // declares inner dir, initialized to outer dir
// ...
}
</code></pre>
<p>这个问题不仅存在基于range的循环在下面的例子中对循环变量i的使用也存在同样的问题</p>
<pre><code class="language-Go">var rmdirs []func()
dirs := tempDirs()
for i := 0; i &lt; len(dirs); i++ {
os.MkdirAll(dirs[i], 0755) // OK
rmdirs = append(rmdirs, func() {
os.RemoveAll(dirs[i]) // NOTE: incorrect!
})
}
</code></pre>
<p>如果你使用go语句第八章或者defer语句5.8节会经常遇到此类问题。这不是go或defer本身导致的而是因为它们都会等待循环结束后再执行函数值。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="57-可变参数"><a class="header" href="#57-可变参数">5.7. 可变参数</a></h2>
<p>参数数量可变的函数称为可变参数函数。典型的例子就是fmt.Printf和类似函数。Printf首先接收一个必备的参数之后接收任意个数的后续参数。</p>
<p>在声明可变参数函数时,需要在参数列表的最后一个参数类型之前加上省略符号“...”,这表示该函数会接收任意数量的该类型参数。</p>
<p><u><i>gopl.io/ch5/sum</i></u></p>
<pre><code class="language-Go">func sum(vals ...int) int {
total := 0
for _, val := range vals {
total += val
}
return total
}
</code></pre>
<p>sum函数返回任意个int型参数的和。在函数体中vals被看作是类型为[] int的切片。sum可以接收任意数量的int型参数</p>
<pre><code class="language-Go">fmt.Println(sum()) // &quot;0&quot;
fmt.Println(sum(3)) // &quot;3&quot;
fmt.Println(sum(1, 2, 3, 4)) // &quot;10&quot;
</code></pre>
<p>在上面的代码中调用者隐式的创建一个数组并将原始参数复制到数组中再把数组的一个切片作为参数传给被调用函数。如果原始参数已经是切片类型我们该如何传递给sum只需在最后一个参数后加上省略符。下面的代码功能与上个例子中最后一条语句相同。</p>
<pre><code class="language-Go">values := []int{1, 2, 3, 4}
fmt.Println(sum(values...)) // &quot;10&quot;
</code></pre>
<p>虽然在可变参数函数内部,...int 型参数的行为看起来很像切片类型,但实际上,可变参数函数和以切片作为参数的函数是不同的。</p>
<pre><code class="language-Go">func f(...int) {}
func g([]int) {}
fmt.Printf(&quot;%T\n&quot;, f) // &quot;func(...int)&quot;
fmt.Printf(&quot;%T\n&quot;, g) // &quot;func([]int)&quot;
</code></pre>
<p>可变参数函数经常被用于格式化字符串。下面的errorf函数构造了一个以行号开头的经过格式化的错误信息。函数名的后缀f是一种通用的命名规范代表该可变参数函数可以接收Printf风格的格式化字符串。</p>
<pre><code class="language-Go">func errorf(linenum int, format string, args ...interface{}) {
fmt.Fprintf(os.Stderr, &quot;Line %d: &quot;, linenum)
fmt.Fprintf(os.Stderr, format, args...)
fmt.Fprintln(os.Stderr)
}
linenum, name := 12, &quot;count&quot;
errorf(linenum, &quot;undefined: %s&quot;, name) // &quot;Line 12: undefined: count&quot;
</code></pre>
<p>interface{}表示函数的最后一个参数可以接收任意类型我们会在第7章详细介绍。</p>
<p><strong>练习5.15</strong> 编写类似sum的可变参数函数max和min。考虑不传参时max和min该如何处理再编写至少接收1个参数的版本。</p>
<p>**练习5.16**编写多参数版本的strings.Join。</p>
<p>**练习5.17**编写多参数版本的ElementsByTagName函数接收一个HTML结点树以及任意数量的标签名返回与这些标签名匹配的所有元素。下面给出了2个例子</p>
<pre><code class="language-Go">func ElementsByTagName(doc *html.Node, name...string) []*html.Node
images := ElementsByTagName(doc, &quot;img&quot;)
headings := ElementsByTagName(doc, &quot;h1&quot;, &quot;h2&quot;, &quot;h3&quot;, &quot;h4&quot;)
</code></pre>
<div style="break-before: page; page-break-before: always;"></div><h2 id="58-deferred函数"><a class="header" href="#58-deferred函数">5.8. Deferred函数</a></h2>
<p>在findLinks的例子中我们用http.Get的输出作为html.Parse的输入。只有url的内容的确是HTML格式的html.Parse才可以正常工作但实际上url指向的内容很丰富可能是图片纯文本或是其他。将这些格式的内容传递给html.parse会产生不良后果。</p>
<p>下面的例子获取HTML页面并输出页面的标题。title函数会检查服务器返回的Content-Type字段如果发现页面不是HTML将终止函数运行返回错误。</p>
<p><u><i>gopl.io/ch5/title1</i></u></p>
<pre><code class="language-Go">func title(url string) error {
resp, err := http.Get(url)
if err != nil {
return err
}
// Check Content-Type is HTML (e.g., &quot;text/html;charset=utf-8&quot;).
ct := resp.Header.Get(&quot;Content-Type&quot;)
if ct != &quot;text/html&quot; &amp;&amp; !strings.HasPrefix(ct,&quot;text/html;&quot;) {
resp.Body.Close()
return fmt.Errorf(&quot;%s has type %s, not text/html&quot;,url, ct)
}
doc, err := html.Parse(resp.Body)
resp.Body.Close()
if err != nil {
return fmt.Errorf(&quot;parsing %s as HTML: %v&quot;, url,err)
}
visitNode := func(n *html.Node) {
if n.Type == html.ElementNode &amp;&amp; n.Data == &quot;title&quot;&amp;&amp;n.FirstChild != nil {
fmt.Println(n.FirstChild.Data)
}
}
forEachNode(doc, visitNode, nil)
return nil
}
</code></pre>
<p>下面展示了运行效果:</p>
<pre><code>$ go build gopl.io/ch5/title1
$ ./title1 http://gopl.io
The Go Programming Language
$ ./title1 https://golang.org/doc/effective_go.html
Effective Go - The Go Programming Language
$ ./title1 https://golang.org/doc/gopher/frontpage.png
title1: https://golang.org/doc/gopher/frontpage.png has type image/png, not text/html
</code></pre>
<p>resp.Body.close调用了多次这是为了确保title在所有执行路径下即使函数运行失败都关闭了网络连接。随着函数变得复杂需要处理的错误也变多维护清理逻辑变得越来越困难。而Go语言独有的defer机制可以让事情变得简单。</p>
<p>你只需要在调用普通函数或方法前加上关键字defer就完成了defer所需要的语法。当执行到该条语句时函数和参数表达式得到计算但直到包含该defer语句的函数执行完毕时defer后的函数才会被执行不论包含defer语句的函数是通过return正常结束还是由于panic导致的异常结束。你可以在一个函数中执行多条defer语句它们的执行顺序与声明顺序相反。</p>
<p>defer语句经常被用于处理成对的操作如打开、关闭、连接、断开连接、加锁、释放锁。通过defer机制不论函数逻辑多复杂都能保证在任何执行路径下资源被释放。释放资源的defer应该直接跟在请求资源的语句后。在下面的代码中一条defer语句替代了之前的所有resp.Body.Close</p>
<p><u><i>gopl.io/ch5/title2</i></u></p>
<pre><code class="language-Go">func title(url string) error {
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
ct := resp.Header.Get(&quot;Content-Type&quot;)
if ct != &quot;text/html&quot; &amp;&amp; !strings.HasPrefix(ct,&quot;text/html;&quot;) {
return fmt.Errorf(&quot;%s has type %s, not text/html&quot;,url, ct)
}
doc, err := html.Parse(resp.Body)
if err != nil {
return fmt.Errorf(&quot;parsing %s as HTML: %v&quot;, url,err)
}
// ...print doc's title element…
return nil
}
</code></pre>
<p>在处理其他资源时也可以采用defer机制比如对文件的操作</p>
<p><u><i>io/ioutil</i></u></p>
<pre><code class="language-Go">package ioutil
func ReadFile(filename string) ([]byte, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
return ReadAll(f)
}
</code></pre>
<p>或是处理互斥锁9.2章)</p>
<pre><code class="language-Go">var mu sync.Mutex
var m = make(map[string]int)
func lookup(key string) int {
mu.Lock()
defer mu.Unlock()
return m[key]
}
</code></pre>
<p>调试复杂程序时defer机制也常被用于记录何时进入和退出函数。下例中的bigSlowOperation函数直接调用trace记录函数的被调情况。bigSlowOperation被调时trace会返回一个函数值该函数值会在bigSlowOperation退出时被调用。通过这种方式 我们可以只通过一条语句控制函数的入口和所有的出口甚至可以记录函数的运行时间如例子中的start。需要注意一点不要忘记defer语句后的圆括号否则本该在进入时执行的操作会在退出时执行而本该在退出时执行的永远不会被执行。</p>
<p><u><i>gopl.io/ch5/trace</i></u></p>
<pre><code class="language-Go">func bigSlowOperation() {
defer trace(&quot;bigSlowOperation&quot;)() // don't forget the extra parentheses
// ...lots of work…
time.Sleep(10 * time.Second) // simulate slow operation by sleeping
}
func trace(msg string) func() {
start := time.Now()
log.Printf(&quot;enter %s&quot;, msg)
return func() {
log.Printf(&quot;exit %s (%s)&quot;, msg,time.Since(start))
}
}
</code></pre>
<p>每一次bigSlowOperation被调用程序都会记录函数的进入退出持续时间。我们用time.Sleep模拟一个耗时的操作</p>
<pre><code>$ go build gopl.io/ch5/trace
$ ./trace
2015/11/18 09:53:26 enter bigSlowOperation
2015/11/18 09:53:36 exit bigSlowOperation (10.000589217s)
</code></pre>
<p>我们知道defer语句中的函数会在return语句更新返回值变量后再执行又因为在函数中定义的匿名函数可以访问该函数包括返回值变量在内的所有变量所以对匿名函数采用defer机制可以使其观察函数的返回值。</p>
<p>以double函数为例</p>
<pre><code class="language-Go">func double(x int) int {
return x + x
}
</code></pre>
<p>我们只需要首先命名double的返回值再增加defer语句我们就可以在double每次被调用时输出参数以及返回值。</p>
<pre><code class="language-Go">func double(x int) (result int) {
defer func() { fmt.Printf(&quot;double(%d) = %d\n&quot;, x,result) }()
return x + x
}
_ = double(4)
// Output:
// &quot;double(4) = 8&quot;
</code></pre>
<p>可能double函数过于简单看不出这个小技巧的作用但对于有许多return语句的函数而言这个技巧很有用。</p>
<p>被延迟执行的匿名函数甚至可以修改函数返回给调用者的返回值:</p>
<pre><code class="language-Go">func triple(x int) (result int) {
defer func() { result += x }()
return double(x)
}
fmt.Println(triple(4)) // &quot;12&quot;
</code></pre>
<p>在循环体中的defer语句需要特别注意因为只有在函数执行完毕后这些被延迟的函数才会执行。下面的代码会导致系统的文件描述符耗尽因为在所有文件都被处理之前没有文件会被关闭。</p>
<pre><code class="language-Go">for _, filename := range filenames {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close() // NOTE: risky; could run out of file descriptors
// ...process f…
}
</code></pre>
<p>一种解决方法是将循环体中的defer语句移至另外一个函数。在每次循环时调用这个函数。</p>
<pre><code class="language-Go">for _, filename := range filenames {
if err := doFile(filename); err != nil {
return err
}
}
func doFile(filename string) error {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close()
// ...process f…
}
</code></pre>
<p>下面的代码是fetch1.5节的改进版我们将http响应信息写入本地文件而不是从标准输出流输出。我们通过path.Base提出url路径的最后一段作为文件名。</p>
<p><u><i>gopl.io/ch5/fetch</i></u></p>
<pre><code class="language-Go">// Fetch downloads the URL and returns the
// name and length of the local file.
func fetch(url string) (filename string, n int64, err error) {
resp, err := http.Get(url)
if err != nil {
return &quot;&quot;, 0, err
}
defer resp.Body.Close()
local := path.Base(resp.Request.URL.Path)
if local == &quot;/&quot; {
local = &quot;index.html&quot;
}
f, err := os.Create(local)
if err != nil {
return &quot;&quot;, 0, err
}
n, err = io.Copy(f, resp.Body)
// Close file, but prefer error from Copy, if any.
if closeErr := f.Close(); err == nil {
err = closeErr
}
return local, n, err
}
</code></pre>
<p>对resp.Body.Close延迟调用我们已经见过了在此不做解释。上例中通过os.Create打开文件进行写入在关闭文件时我们没有对f.close采用defer机制因为这会产生一些微妙的错误。许多文件系统尤其是NFS写入文件时发生的错误会被延迟到文件关闭时反馈。如果没有检查文件关闭时的反馈信息可能会导致数据丢失而我们还误以为写入操作成功。如果io.Copy和f.close都失败了我们倾向于将io.Copy的错误信息反馈给调用者因为它先于f.close发生更有可能接近问题的本质。</p>
<p>**练习5.18**不修改fetch的行为重写fetch函数要求使用defer机制关闭文件。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="59-panic异常"><a class="header" href="#59-panic异常">5.9. Panic异常</a></h2>
<p>Go的类型系统会在编译时捕获很多错误但有些错误只能在运行时检查如数组访问越界、空指针引用等。这些运行时错误会引起panic异常。</p>
<p>一般而言当panic异常发生时程序会中断运行并立即执行在该goroutine可以先理解成线程在第8章会详细介绍中被延迟的函数defer 机制。随后程序崩溃并输出日志信息。日志信息包括panic value和函数调用的堆栈跟踪信息。panic value通常是某种错误信息。对于每个goroutine日志信息中都会有与之相对的发生panic时的函数调用堆栈跟踪信息。通常我们不需要再次运行程序去定位问题日志信息已经提供了足够的诊断依据。因此在我们填写问题报告时一般会将panic异常和日志信息一并记录。</p>
<p>不是所有的panic异常都来自运行时直接调用内置的panic函数也会引发panic异常panic函数接受任何值作为参数。当某些不应该发生的场景发生时我们就应该调用panic。比如当程序到达了某条逻辑上不可能到达的路径</p>
<pre><code class="language-Go">switch s := suit(drawCard()); s {
case &quot;Spades&quot;: // ...
case &quot;Hearts&quot;: // ...
case &quot;Diamonds&quot;: // ...
case &quot;Clubs&quot;: // ...
default:
panic(fmt.Sprintf(&quot;invalid suit %q&quot;, s)) // Joker?
}
</code></pre>
<p>断言函数必须满足的前置条件是明智的做法,但这很容易被滥用。除非你能提供更多的错误信息,或者能更快速的发现错误,否则不需要使用断言,编译器在运行时会帮你检查代码。</p>
<pre><code class="language-Go">func Reset(x *Buffer) {
if x == nil {
panic(&quot;x is nil&quot;) // unnecessary!
}
x.elements = nil
}
</code></pre>
<p>虽然Go的panic机制类似于其他语言的异常但panic的适用场景有一些不同。由于panic会引起程序的崩溃因此panic一般用于严重错误如程序内部的逻辑不一致。勤奋的程序员认为任何崩溃都表明代码中存在漏洞所以对于大部分漏洞我们应该使用Go提供的错误机制而不是panic尽量避免程序的崩溃。在健壮的程序中任何可以预料到的错误如不正确的输入、错误的配置或是失败的I/O操作都应该被优雅的处理最好的处理方式就是使用Go的错误机制。</p>
<p>考虑regexp.Compile函数该函数将正则表达式编译成有效的可匹配格式。当输入的正则表达式不合法时该函数会返回一个错误。当调用者明确的知道正确的输入不会引起函数错误时要求调用者检查这个错误是不必要和累赘的。我们应该假设函数的输入一直合法就如前面的断言一样当调用者输入了不应该出现的输入时触发panic异常。</p>
<p>在程序源码中大多数正则表达式是字符串字面值string literals因此regexp包提供了包装函数regexp.MustCompile检查输入的合法性。</p>
<pre><code class="language-Go">package regexp
func Compile(expr string) (*Regexp, error) { /* ... */ }
func MustCompile(expr string) *Regexp {
re, err := Compile(expr)
if err != nil {
panic(err)
}
return re
}
</code></pre>
<p>包装函数使得调用者可以便捷的用一个编译后的正则表达式为包级别的变量赋值:</p>
<pre><code class="language-Go">var httpSchemeRE = regexp.MustCompile(`^https?:`) //&quot;http:&quot; or &quot;https:&quot;
</code></pre>
<p>显然MustCompile不能接收不合法的输入。函数名中的Must前缀是一种针对此类函数的命名约定比如template.Must4.6节)</p>
<pre><code class="language-Go">func main() {
f(3)
}
func f(x int) {
fmt.Printf(&quot;f(%d)\n&quot;, x+0/x) // panics if x == 0
defer fmt.Printf(&quot;defer %d\n&quot;, x)
f(x - 1)
}
</code></pre>
<p>上例中的运行输出如下:</p>
<pre><code>f(3)
f(2)
f(1)
defer 1
defer 2
defer 3
</code></pre>
<p>当f(0)被调用时发生panic异常之前被延迟执行的3个fmt.Printf被调用。程序中断执行后panic信息和堆栈信息会被输出下面是简化的输出</p>
<pre><code>panic: runtime error: integer divide by zero
main.f(0)
src/gopl.io/ch5/defer1/defer.go:14
main.f(1)
src/gopl.io/ch5/defer1/defer.go:16
main.f(2)
src/gopl.io/ch5/defer1/defer.go:16
main.f(3)
src/gopl.io/ch5/defer1/defer.go:16
main.main()
src/gopl.io/ch5/defer1/defer.go:10
</code></pre>
<p>我们在下一节将看到如何使程序从panic异常中恢复阻止程序的崩溃。</p>
<p>为了方便诊断问题runtime包允许程序员输出堆栈信息。在下面的例子中我们通过在main函数中延迟调用printStack输出堆栈信息。</p>
<p><u><i>gopl.io/ch5/defer2</i></u></p>
<pre><code class="language-Go">func main() {
defer printStack()
f(3)
}
func printStack() {
var buf [4096]byte
n := runtime.Stack(buf[:], false)
os.Stdout.Write(buf[:n])
}
</code></pre>
<p>printStack的简化输出如下下面只是printStack的输出不包括panic的日志信息</p>
<pre><code>goroutine 1 [running]:
main.printStack()
src/gopl.io/ch5/defer2/defer.go:20
main.f(0)
src/gopl.io/ch5/defer2/defer.go:27
main.f(1)
src/gopl.io/ch5/defer2/defer.go:29
main.f(2)
src/gopl.io/ch5/defer2/defer.go:29
main.f(3)
src/gopl.io/ch5/defer2/defer.go:29
main.main()
src/gopl.io/ch5/defer2/defer.go:15
</code></pre>
<p>将panic机制类比其他语言异常机制的读者可能会惊讶runtime.Stack为何能输出已经被释放函数的信息在Go的panic机制中延迟函数的调用在释放堆栈信息之前。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="510-recover捕获异常"><a class="header" href="#510-recover捕获异常">5.10. Recover捕获异常</a></h2>
<p>通常来说不应该对panic异常做任何处理但有时也许我们可以从异常中恢复至少我们可以在程序崩溃前做一些操作。举个例子当web服务器遇到不可预料的严重问题时在崩溃前应该将所有的连接关闭如果不做任何处理会使得客户端一直处于等待状态。如果web服务器还在开发阶段服务器甚至可以将异常信息反馈到客户端帮助调试。</p>
<p>如果在deferred函数中调用了内置函数recover并且定义该defer语句的函数发生了panic异常recover会使程序从panic中恢复并返回panic value。导致panic异常的函数不会继续运行但能正常返回。在未发生panic时调用recoverrecover会返回nil。</p>
<p>让我们以语言解析器为例说明recover的使用场景。考虑到语言解析器的复杂性即使某个语言解析器目前工作正常也无法肯定它没有漏洞。因此当某个异常出现时我们不会选择让解析器崩溃而是会将panic异常当作普通的解析错误并附加额外信息提醒用户报告此错误。</p>
<pre><code class="language-Go">func Parse(input string) (s *Syntax, err error) {
defer func() {
if p := recover(); p != nil {
err = fmt.Errorf(&quot;internal error: %v&quot;, p)
}
}()
// ...parser...
}
</code></pre>
<p>deferred函数帮助Parse从panic中恢复。在deferred函数内部panic value被附加到错误信息中并用err变量接收错误信息返回给调用者。我们也可以通过调用runtime.Stack往错误信息中添加完整的堆栈调用信息。</p>
<p>不加区分的恢复所有的panic异常不是可取的做法因为在panic之后无法保证包级变量的状态仍然和我们预期一致。比如对数据结构的一次重要更新没有被完整完成、文件或者网络连接没有被关闭、获得的锁没有被释放。此外如果写日志时产生的panic被不加区分的恢复可能会导致漏洞被忽略。</p>
<p>虽然把对panic的处理都集中在一个包下有助于简化对复杂和不可以预料问题的处理但作为被广泛遵守的规范你不应该试图去恢复其他包引起的panic。公有的API应该将函数的运行失败作为error返回而不是panic。同样的你也不应该恢复一个由他人开发的函数引起的panic比如说调用者传入的回调函数因为你无法确保这样做是安全的。</p>
<p>有时我们很难完全遵循规范举个例子net/http包中提供了一个web服务器将收到的请求分发给用户提供的处理函数。很显然我们不能因为某个处理函数引发的panic异常杀掉整个进程web服务器遇到处理函数导致的panic时会调用recover输出堆栈信息继续运行。这样的做法在实践中很便捷但也会引起资源泄漏或是因为recover操作导致其他问题。</p>
<p>基于以上原因安全的做法是有选择性的recover。换句话说只恢复应该被恢复的panic异常此外这些异常所占的比例应该尽可能的低。为了标识某个panic是否应该被恢复我们可以将panic value设置成特殊类型。在recover时对panic value进行检查如果发现panic value是特殊类型就将这个panic作为error处理如果不是则按照正常的panic进行处理在下面的例子中我们会看到这种方式</p>
<p>下面的例子是title函数的变形如果HTML页面包含多个<code>&lt;title&gt;</code>该函数会给调用者返回一个错误error。在soleTitle内部处理时如果检测到有多个<code>&lt;title&gt;</code>会调用panic阻止函数继续递归并将特殊类型bailout作为panic的参数。</p>
<pre><code class="language-Go">// soleTitle returns the text of the first non-empty title element
// in doc, and an error if there was not exactly one.
func soleTitle(doc *html.Node) (title string, err error) {
type bailout struct{}
defer func() {
switch p := recover(); p {
case nil: // no panic
case bailout{}: // &quot;expected&quot; panic
err = fmt.Errorf(&quot;multiple title elements&quot;)
default:
panic(p) // unexpected panic; carry on panicking
}
}()
// Bail out of recursion if we find more than one nonempty title.
forEachNode(doc, func(n *html.Node) {
if n.Type == html.ElementNode &amp;&amp; n.Data == &quot;title&quot; &amp;&amp;
n.FirstChild != nil {
if title != &quot;&quot; {
panic(bailout{}) // multiple titleelements
}
title = n.FirstChild.Data
}
}, nil)
if title == &quot;&quot; {
return &quot;&quot;, fmt.Errorf(&quot;no title element&quot;)
}
return title, nil
}
</code></pre>
<p>在上例中deferred函数调用recover并检查panic value。当panic value是bailout{}类型时deferred函数生成一个error返回给调用者。当panic value是其他non-nil值时表示发生了未知的panic异常deferred函数将调用panic函数并将当前的panic value作为参数传入此时等同于recover没有做任何操作。请注意在例子中对可预期的错误采用了panic这违反了之前的建议我们在此只是想向读者演示这种机制。</p>
<p>有些情况下我们无法恢复。某些致命错误会导致Go在运行时终止程序如内存不足。</p>
<p><strong>练习5.19</strong> 使用panic和recover编写一个不包含return语句但能返回一个非零值的函数。</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="第6章-方法"><a class="header" href="#第6章-方法">第6章 方法</a></h1>
<p>从90年代早期开始面向对象编程OOP就成为了称霸工程界和教育界的编程范式所以之后几乎所有大规模被应用的语言都包含了对OOP的支持go语言也不例外。</p>
<p>尽管没有被大众所接受的明确的OOP的定义从我们的理解来讲一个对象其实也就是一个简单的值或者一个变量在这个对象中会包含一些方法而一个方法则是一个一个和特殊类型关联的函数。一个面向对象的程序会用方法来表达其属性和对应的操作这样使用这个对象的用户就不需要直接去操作对象而是借助方法来做这些事情。</p>
<p>在早些的章节中我们已经使用了标准库提供的一些方法比如time.Duration这个类型的Seconds方法</p>
<pre><code class="language-Go">const day = 24 * time.Hour
fmt.Println(day.Seconds()) // &quot;86400&quot;
</code></pre>
<p>并且在2.5节中我们定义了一个自己的方法Celsius类型的String方法:</p>
<pre><code class="language-Go">func (c Celsius) String() string { return fmt.Sprintf(&quot;%g°C&quot;, c) }
</code></pre>
<p>在本章中OOP编程的第一方面我们会向你展示如何有效地定义和使用方法。我们会覆盖到OOP编程的两个关键点封装和组合。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="61-方法声明"><a class="header" href="#61-方法声明">6.1. 方法声明</a></h2>
<p>在函数声明时,在其名字之前放上一个变量,即是一个方法。这个附加的参数会将该函数附加到这种类型上,即相当于为这种类型定义了一个独占的方法。</p>
<p>下面来写我们第一个方法的例子这个例子在package geometry下</p>
<p><u><i>gopl.io/ch6/geometry</i></u></p>
<pre><code class="language-go">package geometry
import &quot;math&quot;
type Point struct{ X, Y float64 }
// traditional function
func Distance(p, q Point) float64 {
return math.Hypot(q.X-p.X, q.Y-p.Y)
}
// same thing, but as a method of the Point type
func (p Point) Distance(q Point) float64 {
return math.Hypot(q.X-p.X, q.Y-p.Y)
}
</code></pre>
<p>上面的代码里那个附加的参数p叫做方法的接收器receiver早期的面向对象语言留下的遗产将调用一个方法称为“向一个对象发送消息”。</p>
<p>在Go语言中我们并不会像其它语言那样用this或者self作为接收器我们可以任意的选择接收器的名字。由于接收器的名字经常会被使用到所以保持其在方法间传递时的一致性和简短性是不错的主意。这里的建议是可以使用其类型的第一个字母比如这里使用了Point的首字母p。</p>
<p>在方法调用过程中,接收器参数一般会在方法名之前出现。这和方法声明是一样的,都是接收器参数在方法名字之前。下面是例子:</p>
<pre><code class="language-Go">p := Point{1, 2}
q := Point{4, 6}
fmt.Println(Distance(p, q)) // &quot;5&quot;, function call
fmt.Println(p.Distance(q)) // &quot;5&quot;, method call
</code></pre>
<p>可以看到上面的两个函数调用都是Distance但是却没有发生冲突。第一个Distance的调用实际上用的是包级别的函数geometry.Distance而第二个则是使用刚刚声明的Point调用的是Point类下声明的Point.Distance方法。</p>
<p>这种p.Distance的表达式叫做选择器因为他会选择合适的对应p这个对象的Distance方法来执行。选择器也会被用来选择一个struct类型的字段比如p.X。由于方法和字段都是在同一命名空间所以如果我们在这里声明一个X方法的话编译器会报错因为在调用p.X时会有歧义译注这里确实挺奇怪的</p>
<p>因为每种类型都有其方法的命名空间我们在用Distance这个名字的时候不同的Distance调用指向了不同类型里的Distance方法。让我们来定义一个Path类型这个Path代表一个线段的集合并且也给这个Path定义一个叫Distance的方法。</p>
<pre><code class="language-Go">// A Path is a journey connecting the points with straight lines.
type Path []Point
// Distance returns the distance traveled along the path.
func (path Path) Distance() float64 {
sum := 0.0
for i := range path {
if i &gt; 0 {
sum += path[i-1].Distance(path[i])
}
}
return sum
}
</code></pre>
<p>Path是一个命名的slice类型而不是Point那样的struct类型然而我们依然可以为它定义方法。在能够给任意类型定义方法这一点上Go和很多其它的面向对象的语言不太一样。因此在Go语言里我们为一些简单的数值、字符串、slice、map来定义一些附加行为很方便。我们可以给同一个包内的任意命名类型定义方法只要这个命名类型的底层类型译注这个例子里底层类型是指[]Point这个slicePath就是命名类型不是指针或者interface。</p>
<p>两个Distance方法有不同的类型。他们两个方法之间没有任何关系尽管Path的Distance方法会在内部调用Point.Distance方法来计算每个连接邻接点的线段的长度。</p>
<p>让我们来调用一个新方法,计算三角形的周长:</p>
<pre><code class="language-Go">perim := Path{
{1, 1},
{5, 1},
{5, 4},
{1, 1},
}
fmt.Println(perim.Distance()) // &quot;12&quot;
</code></pre>
<p>在上面两个对Distance名字的方法的调用中编译器会根据方法的名字以及接收器来决定具体调用的是哪一个函数。第一个例子中path[i-1]数组中的类型是Point因此Point.Distance这个方法被调用在第二个例子中perim的类型是Path因此Distance调用的是Path.Distance。</p>
<p>对于一个给定的类型其内部的方法都必须有唯一的方法名但是不同的类型却可以有同样的方法名比如我们这里Point和Path就都有Distance这个名字的方法所以我们没有必要非在方法名之前加类型名来消除歧义比如PathDistance。这里我们已经看到了方法比之函数的一些好处方法名可以简短。当我们在包外调用的时候这种好处就会被放大因为我们可以使用这个短名字而可以省略掉包的名字下面是例子</p>
<pre><code class="language-Go">import &quot;gopl.io/ch6/geometry&quot;
perim := geometry.Path{{1, 1}, {5, 1}, {5, 4}, {1, 1}}
fmt.Println(geometry.PathDistance(perim)) // &quot;12&quot;, standalone function
fmt.Println(perim.Distance()) // &quot;12&quot;, method of geometry.Path
</code></pre>
<p><strong>译注:</strong> 如果我们要用方法去计算perim的distance还需要去写全geometry的包名和其函数名但是因为Path这个类型定义了一个可以直接用的Distance方法所以我们可以直接写perim.Distance()。相当于可以少打很多字作者应该是这个意思。因为在Go里包外调用函数需要带上包名还是挺麻烦的。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="62-基于指针对象的方法"><a class="header" href="#62-基于指针对象的方法">6.2. 基于指针对象的方法</a></h2>
<p>当调用一个函数时,会对其每一个参数值进行拷贝,如果一个函数需要更新一个变量,或者函数的其中一个参数实在太大我们希望能够避免进行这种默认的拷贝,这种情况下我们就需要用到指针了。对应到我们这里用来更新接收器的对象的方法,当这个接受者变量本身比较大时,我们就可以用其指针而不是对象来声明方法,如下:</p>
<pre><code class="language-go">func (p *Point) ScaleBy(factor float64) {
p.X *= factor
p.Y *= factor
}
</code></pre>
<p>这个方法的名字是<code>(*Point).ScaleBy</code>。这里的括号是必须的;没有括号的话这个表达式可能会被理解为<code>*(Point.ScaleBy)</code></p>
<p>在现实的程序里一般会约定如果Point这个类有一个指针作为接收器的方法那么所有Point的方法都必须有一个指针接收器即使是那些并不需要这个指针接收器的函数。我们在这里打破了这个约定只是为了展示一下两种方法的异同而已。</p>
<p>只有类型Point和指向他们的指针<code>(*Point)</code>,才可能是出现在接收器声明里的两种接收器。此外,为了避免歧义,在声明方法时,如果一个类型名本身是一个指针的话,是不允许其出现在接收器中的,比如下面这个例子:</p>
<pre><code class="language-go">type P *int
func (P) f() { /* ... */ } // compile error: invalid receiver type
</code></pre>
<p>想要调用指针类型方法<code>(*Point).ScaleBy</code>只要提供一个Point类型的指针即可像下面这样。</p>
<pre><code class="language-go">r := &amp;Point{1, 2}
r.ScaleBy(2)
fmt.Println(*r) // &quot;{2, 4}&quot;
</code></pre>
<p>或者这样:</p>
<pre><code class="language-go">p := Point{1, 2}
pptr := &amp;p
pptr.ScaleBy(2)
fmt.Println(p) // &quot;{2, 4}&quot;
</code></pre>
<p>或者这样:</p>
<pre><code class="language-go">p := Point{1, 2}
(&amp;p).ScaleBy(2)
fmt.Println(p) // &quot;{2, 4}&quot;
</code></pre>
<p>不过后面两种方法有些笨拙。幸运的是go语言本身在这种地方会帮到我们。如果接收器p是一个Point类型的变量并且其方法需要一个Point指针作为接收器我们可以用下面这种简短的写法</p>
<pre><code class="language-go">p.ScaleBy(2)
</code></pre>
<p>编译器会隐式地帮我们用&amp;p去调用ScaleBy这个方法。这种简写方法只适用于“变量”包括struct里的字段比如p.X以及array和slice内的元素比如perim[0]。我们不能通过一个无法取到地址的接收器来调用指针方法,比如临时变量的内存地址就无法获取得到:</p>
<pre><code class="language-go">Point{1, 2}.ScaleBy(2) // compile error: can't take address of Point literal
</code></pre>
<p>但是我们可以用一个<code>*Point</code>这样的接收器来调用Point的方法因为我们可以通过地址来找到这个变量只要用解引用符号<code>*</code>来取到该变量即可。编译器在这里也会给我们隐式地插入<code>*</code>这个操作符,所以下面这两种写法等价的:</p>
<pre><code class="language-Go">pptr.Distance(q)
(*pptr).Distance(q)
</code></pre>
<p>这里的几个例子可能让你有些困惑,所以我们总结一下:在每一个合法的方法调用表达式中,也就是下面三种情况里的任意一种情况都是可以的:</p>
<p>要么接收器的实际参数和其形式参数是相同的类型比如两者都是类型T或者都是类型<code>*T</code></p>
<pre><code class="language-go">Point{1, 2}.Distance(q) // Point
pptr.ScaleBy(2) // *Point
</code></pre>
<p>或者接收器实参是类型T但接收器形参是类型<code>*T</code>,这种情况下编译器会隐式地为我们取变量的地址:</p>
<pre><code class="language-go">p.ScaleBy(2) // implicit (&amp;p)
</code></pre>
<p>或者接收器实参是类型<code>*T</code>形参是类型T。编译器会隐式地为我们解引用取到指针指向的实际变量</p>
<pre><code class="language-go">pptr.Distance(q) // implicit (*pptr)
</code></pre>
<p>如果命名类型T译注用type xxx定义的类型的所有方法都是用T类型自己来做接收器而不是<code>*T</code>那么拷贝这种类型的实例就是安全的调用他的任何一个方法也就会产生一个值的拷贝。比如time.Duration的这个类型在调用其方法时就会被全部拷贝一份包括在作为参数传入函数的时候。但是如果一个方法使用指针作为接收器你需要避免对其进行拷贝因为这样可能会破坏掉该类型内部的不变性。比如你对bytes.Buffer对象进行了拷贝那么可能会引起原始对象和拷贝对象只是别名而已实际上它们指向的对象是一样的。紧接着对拷贝后的变量进行修改可能会有让你有意外的结果。</p>
<p><strong>译注:</strong> 作者这里说的比较绕,其实有两点:</p>
<ol>
<li>不管你的method的receiver是指针类型还是非指针类型都是可以通过指针/非指针类型进行调用的,编译器会帮你做类型转换。</li>
<li>在声明一个method的receiver该是指针还是非指针类型时你需要考虑两方面的因素第一方面是这个对象本身是不是特别大如果声明为非指针变量时调用会产生一次拷贝第二方面是如果你用指针类型作为receiver那么你一定要注意这种指针类型指向的始终是一块内存地址就算你对其进行了拷贝。熟悉C或者C++的人这里应该很快能明白。</li>
</ol>
<h3 id="621-nil也是一个合法的接收器类型"><a class="header" href="#621-nil也是一个合法的接收器类型">6.2.1. Nil也是一个合法的接收器类型</a></h3>
<p>就像一些函数允许nil指针作为参数一样方法理论上也可以用nil指针作为其接收器尤其当nil对于对象来说是合法的零值时比如map或者slice。在下面的简单int链表的例子里nil代表的是空链表</p>
<pre><code class="language-go">// An IntList is a linked list of integers.
// A nil *IntList represents the empty list.
type IntList struct {
Value int
Tail *IntList
}
// Sum returns the sum of the list elements.
func (list *IntList) Sum() int {
if list == nil {
return 0
}
return list.Value + list.Tail.Sum()
}
</code></pre>
<p>当你定义一个允许nil作为接收器值的方法的类型时在类型前面的注释中指出nil变量代表的意义是很有必要的就像我们上面例子里做的这样。</p>
<p>下面是net/url包里Values类型定义的一部分。</p>
<p><u><i>net/url</i></u></p>
<pre><code class="language-go">package url
// Values maps a string key to a list of values.
type Values map[string][]string
// Get returns the first value associated with the given key,
// or &quot;&quot; if there are none.
func (v Values) Get(key string) string {
if vs := v[key]; len(vs) &gt; 0 {
return vs[0]
}
return &quot;&quot;
}
// Add adds the value to key.
// It appends to any existing values associated with key.
func (v Values) Add(key, value string) {
v[key] = append(v[key], value)
}
</code></pre>
<p>这个定义向外部暴露了一个map的命名类型并且提供了一些能够简单操作这个map的方法。这个map的value字段是一个string的slice所以这个Values是一个多维map。客户端使用这个变量的时候可以使用map固有的一些操作make切片m[key]等等),也可以使用这里提供的操作方法,或者两者并用,都是可以的:</p>
<p><u><i>gopl.io/ch6/urlvalues</i></u></p>
<pre><code class="language-go">m := url.Values{&quot;lang&quot;: {&quot;en&quot;}} // direct construction
m.Add(&quot;item&quot;, &quot;1&quot;)
m.Add(&quot;item&quot;, &quot;2&quot;)
fmt.Println(m.Get(&quot;lang&quot;)) // &quot;en&quot;
fmt.Println(m.Get(&quot;q&quot;)) // &quot;&quot;
fmt.Println(m.Get(&quot;item&quot;)) // &quot;1&quot; (first value)
fmt.Println(m[&quot;item&quot;]) // &quot;[1 2]&quot; (direct map access)
m = nil
fmt.Println(m.Get(&quot;item&quot;)) // &quot;&quot;
m.Add(&quot;item&quot;, &quot;3&quot;) // panic: assignment to entry in nil map
</code></pre>
<p>对Get的最后一次调用中nil接收器的行为即是一个空map的行为。我们可以等价地将这个操作写成Value(nil).Get(&quot;item&quot;)但是如果你直接写nil.Get(&quot;item&quot;)的话是无法通过编译的因为nil的字面量编译器无法判断其准确类型。所以相比之下最后的那行m.Add的调用就会产生一个panic因为他尝试更新一个空map。</p>
<p>由于url.Values是一个map类型并且间接引用了其key/value对因此url.Values.Add对这个map里的元素做任何的更新、删除操作对调用方都是可见的。实际上就像在普通函数中一样虽然可以通过引用来操作内部值但在方法想要修改引用本身时是不会影响原始值的比如把他置换为nil或者让这个引用指向了其它的对象调用方都不会受影响。译注因为传入的是存储了内存地址的变量你改变这个变量本身是影响不了原始的变量的想想C语言是差不多的</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="63-通过嵌入结构体来扩展类型"><a class="header" href="#63-通过嵌入结构体来扩展类型">6.3. 通过嵌入结构体来扩展类型</a></h2>
<p>来看看ColoredPoint这个类型</p>
<p><u><i>gopl.io/ch6/coloredpoint</i></u></p>
<pre><code class="language-go">import &quot;image/color&quot;
type Point struct{ X, Y float64 }
type ColoredPoint struct {
Point
Color color.RGBA
}
</code></pre>
<p>我们完全可以将ColoredPoint定义为一个有三个字段的struct但是我们却将Point这个类型嵌入到ColoredPoint来提供X和Y这两个字段。像我们在4.4节中看到的那样内嵌可以使我们在定义ColoredPoint时得到一种句法上的简写形式并使其包含Point类型所具有的一切字段然后再定义一些自己的。如果我们想要的话我们可以直接认为通过嵌入的字段就是ColoredPoint自身的字段而完全不需要在调用时指出Point比如下面这样。</p>
<pre><code class="language-go">var cp ColoredPoint
cp.X = 1
fmt.Println(cp.Point.X) // &quot;1&quot;
cp.Point.Y = 2
fmt.Println(cp.Y) // &quot;2&quot;
</code></pre>
<p>对于Point中的方法我们也有类似的用法我们可以把ColoredPoint类型当作接收器来调用Point里的方法即使ColoredPoint里没有声明这些方法</p>
<pre><code class="language-go">red := color.RGBA{255, 0, 0, 255}
blue := color.RGBA{0, 0, 255, 255}
var p = ColoredPoint{Point{1, 1}, red}
var q = ColoredPoint{Point{5, 4}, blue}
fmt.Println(p.Distance(q.Point)) // &quot;5&quot;
p.ScaleBy(2)
q.ScaleBy(2)
fmt.Println(p.Distance(q.Point)) // &quot;10&quot;
</code></pre>
<p>Point类的方法也被引入了ColoredPoint。用这种方式内嵌可以使我们定义字段特别多的复杂类型我们可以将字段先按小类型分组然后定义小类型的方法之后再把它们组合起来。</p>
<p>读者如果对基于类来实现面向对象的语言比较熟悉的话可能会倾向于将Point看作一个基类而ColoredPoint看作其子类或者继承类或者将ColoredPoint看作&quot;is a&quot; Point类型。但这是错误的理解。请注意上面例子中对Distance方法的调用。Distance有一个参数是Point类型但q并不是一个Point类所以尽管q有着Point这个内嵌类型我们也必须要显式地选择它。尝试直接传q的话你会看到下面这样的错误</p>
<pre><code class="language-go">p.Distance(q) // compile error: cannot use q (ColoredPoint) as Point
</code></pre>
<p>一个ColoredPoint并不是一个Point但他&quot;has a&quot;Point并且它有从Point类里引入的Distance和ScaleBy方法。如果你喜欢从实现的角度来考虑问题内嵌字段会指导编译器去生成额外的包装方法来委托已经声明好的方法和下面的形式是等价的</p>
<pre><code class="language-go">func (p ColoredPoint) Distance(q Point) float64 {
return p.Point.Distance(q)
}
func (p *ColoredPoint) ScaleBy(factor float64) {
p.Point.ScaleBy(factor)
}
</code></pre>
<p>当Point.Distance被第一个包装方法调用时它的接收器值是p.Point而不是p当然了在Point类的方法里你是访问不到ColoredPoint的任何字段的。</p>
<p>在类型中内嵌的匿名字段也可能是一个命名类型的指针这种情况下字段和方法会被间接地引入到当前的类型中译注访问需要通过该指针指向的对象去取。添加这一层间接关系让我们可以共享通用的结构并动态地改变对象之间的关系。下面这个ColoredPoint的声明内嵌了一个*Point的指针。</p>
<pre><code class="language-go">type ColoredPoint struct {
*Point
Color color.RGBA
}
p := ColoredPoint{&amp;Point{1, 1}, red}
q := ColoredPoint{&amp;Point{5, 4}, blue}
fmt.Println(p.Distance(*q.Point)) // &quot;5&quot;
q.Point = p.Point // p and q now share the same Point
p.ScaleBy(2)
fmt.Println(*p.Point, *q.Point) // &quot;{2 2} {2 2}&quot;
</code></pre>
<p>一个struct类型也可能会有多个匿名字段。我们将ColoredPoint定义为下面这样</p>
<pre><code class="language-go">type ColoredPoint struct {
Point
color.RGBA
}
</code></pre>
<p>然后这种类型的值便会拥有Point和RGBA类型的所有方法以及直接定义在ColoredPoint中的方法。当编译器解析一个选择器到方法时比如p.ScaleBy它会首先去找直接定义在这个类型里的ScaleBy方法然后找被ColoredPoint的内嵌字段们引入的方法然后去找Point和RGBA的内嵌字段引入的方法然后一直递归向下找。如果选择器有二义性的话编译器会报错比如你在同一级里有两个同名的方法。</p>
<p>方法只能在命名类型像Point或者指向类型的指针上定义但是多亏了内嵌有些时候我们给匿名struct类型来定义方法也有了手段。</p>
<p>下面是一个小trick。这个例子展示了简单的cache其使用两个包级别的变量来实现一个mutex互斥量§9.2和它所操作的cache</p>
<pre><code class="language-go">var (
mu sync.Mutex // guards mapping
mapping = make(map[string]string)
)
func Lookup(key string) string {
mu.Lock()
v := mapping[key]
mu.Unlock()
return v
}
</code></pre>
<p>下面这个版本在功能上是一致的但将两个包级别的变量放在了cache这个struct一组内</p>
<pre><code class="language-go">var cache = struct {
sync.Mutex
mapping map[string]string
}{
mapping: make(map[string]string),
}
func Lookup(key string) string {
cache.Lock()
v := cache.mapping[key]
cache.Unlock()
return v
}
</code></pre>
<p>我们给新的变量起了一个更具表达性的名字cache。因为sync.Mutex字段也被嵌入到了这个struct里其Lock和Unlock方法也就都被引入到了这个匿名结构中了这让我们能够以一个简单明了的语法来对其进行加锁解锁操作。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="64-方法值和方法表达式"><a class="header" href="#64-方法值和方法表达式">6.4. 方法值和方法表达式</a></h2>
<p>我们经常选择一个方法并且在同一个表达式里执行比如常见的p.Distance()形式实际上将其分成两步来执行也是可能的。p.Distance叫作“选择器”选择器会返回一个方法“值”-&gt;一个将方法Point.Distance绑定到特定接收器变量的函数。这个函数可以不通过指定其接收器即可被调用即调用时不需要指定接收器译注因为已经在前文中指定过了只要传入函数的参数即可</p>
<pre><code class="language-go">p := Point{1, 2}
q := Point{4, 6}
distanceFromP := p.Distance // method value
fmt.Println(distanceFromP(q)) // &quot;5&quot;
var origin Point // {0, 0}
fmt.Println(distanceFromP(origin)) // &quot;2.23606797749979&quot;, sqrt(5)
scaleP := p.ScaleBy // method value
scaleP(2) // p becomes (2, 4)
scaleP(3) // then (6, 12)
scaleP(10) // then (60, 120)
</code></pre>
<p>在一个包的API需要一个函数值、且调用方希望操作的是某一个绑定了对象的方法的话方法“值”会非常实用``=_=`真是绕。举例来说下面例子中的time.AfterFunc这个函数的功能是在指定的延迟时间之后来执行一个译注另外的函数。且这个函数操作的是一个Rocket对象r</p>
<pre><code class="language-go">type Rocket struct { /* ... */ }
func (r *Rocket) Launch() { /* ... */ }
r := new(Rocket)
time.AfterFunc(10 * time.Second, func() { r.Launch() })
</code></pre>
<p>直接用方法“值”传入AfterFunc的话可以更为简短</p>
<pre><code class="language-go">time.AfterFunc(10 * time.Second, r.Launch)
</code></pre>
<p>译注:省掉了上面那个例子里的匿名函数。</p>
<p>和方法“值”相关的还有方法表达式。当调用一个方法时与调用一个普通的函数相比我们必须要用选择器p.Distance语法来指定方法的接收器。</p>
<p>当T是一个类型时方法表达式可能会写作<code>T.f</code>或者<code>(*T).f</code>,会返回一个函数“值”,这种函数会将其第一个参数用作接收器,所以可以用通常(译注:不写选择器)的方式来对其进行调用:</p>
<pre><code class="language-go">p := Point{1, 2}
q := Point{4, 6}
distance := Point.Distance // method expression
fmt.Println(distance(p, q)) // &quot;5&quot;
fmt.Printf(&quot;%T\n&quot;, distance) // &quot;func(Point, Point) float64&quot;
scale := (*Point).ScaleBy
scale(&amp;p, 2)
fmt.Println(p) // &quot;{2 4}&quot;
fmt.Printf(&quot;%T\n&quot;, scale) // &quot;func(*Point, float64)&quot;
// 译注这个Distance实际上是指定了Point对象为接收器的一个方法func (p Point) Distance()
// 但通过Point.Distance得到的函数需要比实际的Distance方法多一个参数
// 即其需要用第一个额外参数指定接收器后面排列Distance方法的参数。
// 看起来本书中函数和方法的区别是指有没有接收器,而不像其他语言那样是指有没有返回值。
</code></pre>
<p>当你根据一个变量来决定调用同一个类型的哪个函数时方法表达式就显得很有用了。你可以根据选择来调用接收器各不相同的方法。下面的例子变量op代表Point类型的addition或者subtraction方法Path.TranslateBy方法会为其Path数组中的每一个Point来调用对应的方法</p>
<pre><code class="language-go">type Point struct{ X, Y float64 }
func (p Point) Add(q Point) Point { return Point{p.X + q.X, p.Y + q.Y} }
func (p Point) Sub(q Point) Point { return Point{p.X - q.X, p.Y - q.Y} }
type Path []Point
func (path Path) TranslateBy(offset Point, add bool) {
var op func(p, q Point) Point
if add {
op = Point.Add
} else {
op = Point.Sub
}
for i := range path {
// Call either path[i].Add(offset) or path[i].Sub(offset).
path[i] = op(path[i], offset)
}
}
</code></pre>
<div style="break-before: page; page-break-before: always;"></div><h2 id="65-示例-bit数组"><a class="header" href="#65-示例-bit数组">6.5. 示例: Bit数组</a></h2>
<p>Go语言里的集合一般会用map[T]bool这种形式来表示T代表元素类型。集合用map类型来表示虽然非常灵活但我们可以以一种更好的形式来表示它。例如在数据流分析领域集合元素通常是一个非负整数集合会包含很多元素并且集合会经常进行并集、交集操作这种情况下bit数组会比map表现更加理想。译注这里再补充一个例子比如我们执行一个http下载任务把文件按照16kb一块划分为很多块需要有一个全局变量来标识哪些块下载完成了这种时候也需要用到bit数组。</p>
<p>一个bit数组通常会用一个无符号数或者称之为“字”的slice来表示每一个元素的每一位都表示集合里的一个值。当集合的第i位被设置时我们才说这个集合包含元素i。下面的这个程序展示了一个简单的bit数组类型并且实现了三个函数来对这个bit数组来进行操作</p>
<p><u><i>gopl.io/ch6/intset</i></u></p>
<pre><code class="language-go">// An IntSet is a set of small non-negative integers.
// Its zero value represents the empty set.
type IntSet struct {
words []uint64
}
// Has reports whether the set contains the non-negative value x.
func (s *IntSet) Has(x int) bool {
word, bit := x/64, uint(x%64)
return word &lt; len(s.words) &amp;&amp; s.words[word]&amp;(1&lt;&lt;bit) != 0
}
// Add adds the non-negative value x to the set.
func (s *IntSet) Add(x int) {
word, bit := x/64, uint(x%64)
for word &gt;= len(s.words) {
s.words = append(s.words, 0)
}
s.words[word] |= 1 &lt;&lt; bit
}
// UnionWith sets s to the union of s and t.
func (s *IntSet) UnionWith(t *IntSet) {
for i, tword := range t.words {
if i &lt; len(s.words) {
s.words[i] |= tword
} else {
s.words = append(s.words, tword)
}
}
}
</code></pre>
<p>因为每一个字都有64个二进制位所以为了定位x的bit位我们用了x/64的商作为字的下标并且用x%64得到的值作为这个字内的bit的所在位置。UnionWith这个方法里用到了bit位的“或”逻辑操作符号|来一次完成64个元素的或计算。在练习6.5中我们还会有程序用到这个64位字的例子。</p>
<p>当前这个实现还缺少了很多必要的特性我们把其中一些作为练习题列在本小节之后。但是有一个方法如果缺失的话我们的bit数组可能会比较难混将IntSet作为一个字符串来打印。这里我们来实现它让我们来给上面的例子添加一个String方法类似2.5节中做的那样:</p>
<pre><code class="language-go">// String returns the set as a string of the form &quot;{1 2 3}&quot;.
func (s *IntSet) String() string {
var buf bytes.Buffer
buf.WriteByte('{')
for i, word := range s.words {
if word == 0 {
continue
}
for j := 0; j &lt; 64; j++ {
if word&amp;(1&lt;&lt;uint(j)) != 0 {
if buf.Len() &gt; len(&quot;{&quot;) {
buf.WriteByte(' ')
}
fmt.Fprintf(&amp;buf, &quot;%d&quot;, 64*i+j)
}
}
}
buf.WriteByte('}')
return buf.String()
}
</code></pre>
<p>这里留意一下String方法是不是和3.5.4节中的intsToString方法很相似bytes.Buffer在String方法里经常这么用。当你为一个复杂的类型定义了一个String方法时fmt包就会特殊对待这种类型的值这样可以让这些类型在打印的时候看起来更加友好而不是直接打印其原始的值。fmt会直接调用用户定义的String方法。这种机制依赖于接口和类型断言在第7章中我们会详细介绍。</p>
<p>现在我们就可以在实战中直接用上面定义好的IntSet了</p>
<pre><code class="language-go">var x, y IntSet
x.Add(1)
x.Add(144)
x.Add(9)
fmt.Println(x.String()) // &quot;{1 9 144}&quot;
y.Add(9)
y.Add(42)
fmt.Println(y.String()) // &quot;{9 42}&quot;
x.UnionWith(&amp;y)
fmt.Println(x.String()) // &quot;{1 9 42 144}&quot;
fmt.Println(x.Has(9), x.Has(123)) // &quot;true false&quot;
</code></pre>
<p>这里要注意我们声明的String和Has两个方法都是以指针类型<code>*IntSet</code>来作为接收器的但实际上对于这两个类型来说把接收器声明为指针类型也没什么必要。不过另外两个函数就不是这样了因为另外两个函数操作的是s.words对象如果你不把接收器声明为指针对象那么实际操作的是拷贝对象而不是原来的那个对象。因此因为我们的String方法定义在IntSet指针上所以当我们的变量是IntSet类型而不是IntSet指针时可能会有下面这样让人意外的情况</p>
<pre><code class="language-go">fmt.Println(&amp;x) // &quot;{1 9 42 144}&quot;
fmt.Println(x.String()) // &quot;{1 9 42 144}&quot;
fmt.Println(x) // &quot;{[4398046511618 0 65536]}&quot;
</code></pre>
<p>在第一个Println中我们打印一个<code>*IntSet</code>的指针这个类型的指针确实有自定义的String方法。第二Println我们直接调用了x变量的String()方法这种情况下编译器会隐式地在x前插入&amp;操作符这样相当于我们还是调用的IntSet指针的String方法。在第三个Println中因为IntSet类型没有String方法所以Println方法会直接以原始的方式理解并打印。所以在这种情况下&amp;符号是不能忘的。在我们这种场景下你把String方法绑定到IntSet对象上而不是IntSet指针上可能会更合适一些不过这也需要具体问题具体分析。</p>
<p><strong>练习6.1:</strong> 为bit数组实现下面这些方法</p>
<pre><code class="language-go">func (*IntSet) Len() int // return the number of elements
func (*IntSet) Remove(x int) // remove x from the set
func (*IntSet) Clear() // remove all elements from the set
func (*IntSet) Copy() *IntSet // return a copy of the set
</code></pre>
<p><strong>练习 6.2</strong> 定义一个变参方法(*IntSet).AddAll(...int)这个方法可以添加一组IntSet比如s.AddAll(1,2,3)。</p>
<p><strong>练习 6.3</strong> (*IntSet).UnionWith会用<code>|</code>操作符计算两个集合的并集我们再为IntSet实现另外的几个函数IntersectWith交集元素在A集合B集合均出现DifferenceWith差集元素出现在A集合未出现在B集合SymmetricDifference并差集元素出现在A但没有出现在B或者出现在B没有出现在A</p>
<p>***练习6.4: ** 实现一个Elems方法返回集合中的所有元素用于做一些range之类的遍历操作。</p>
<p><strong>练习 6.5</strong> 我们这章定义的IntSet里的每个字都是用的uint64类型但是64位的数值可能在32位的平台上不高效。修改程序使其使用uint类型这种类型对于32位平台来说更合适。当然了这里我们可以不用简单粗暴地除64可以定义一个常量来决定是用32还是64这里你可能会用到平台的自动判断的一个智能表达式32 &lt;&lt; (^uint(0) &gt;&gt; 63)</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="66-封装"><a class="header" href="#66-封装">6.6. 封装</a></h2>
<p>一个对象的变量或者方法如果对调用方是不可见的话,一般就被定义为“封装”。封装有时候也被叫做信息隐藏,同时也是面向对象编程最关键的一个方面。</p>
<p>Go语言只有一种控制可见性的手段大写首字母的标识符会从定义它们的包中被导出小写字母的则不会。这种限制包内成员的方式同样适用于struct或者一个类型的方法。因而如果我们想要封装一个对象我们必须将其定义为一个struct。</p>
<p>这也就是前面的小节中IntSet被定义为struct类型的原因尽管它只有一个字段</p>
<pre><code class="language-go">type IntSet struct {
words []uint64
}
</code></pre>
<p>当然我们也可以把IntSet定义为一个slice类型但这样我们就需要把代码中所有方法里用到的s.words用<code>*s</code>替换掉了:</p>
<pre><code class="language-go">type IntSet []uint64
</code></pre>
<p>尽管这个版本的IntSet在本质上是一样的但它也允许其它包中可以直接读取并编辑这个slice。换句话说相对于<code>*s</code>这个表达式会出现在所有的包中s.words只需要在定义IntSet的包中出现译注所以还是推荐后者吧的意思</p>
<p>这种基于名字的手段使得在语言中最小的封装单元是package而不是像其它语言一样的类型。一个struct类型的字段对同一个包的所有代码都有可见性无论你的代码是写在一个函数还是一个方法里。</p>
<p>封装提供了三方面的优点。首先,因为调用方不能直接修改对象的变量值,其只需要关注少量的语句并且只要弄懂少量变量的可能的值即可。</p>
<p>第二隐藏实现的细节可以防止调用方依赖那些可能变化的具体实现这样使设计包的程序员在不破坏对外的api情况下能得到更大的自由。</p>
<p>把bytes.Buffer这个类型作为例子来考虑。这个类型在做短字符串叠加的时候很常用所以在设计的时候可以做一些预先的优化比如提前预留一部分空间来避免反复的内存分配。又因为Buffer是一个struct类型这些额外的空间可以用附加的字节数组来保存且放在一个小写字母开头的字段中。这样在外部的调用方只能看到性能的提升但并不会得到这个附加变量。Buffer和其增长算法我们列在这里为了简洁性稍微做了一些精简</p>
<pre><code class="language-go">type Buffer struct {
buf []byte
initial [64]byte
/* ... */
}
// Grow expands the buffer's capacity, if necessary,
// to guarantee space for another n bytes. [...]
func (b *Buffer) Grow(n int) {
if b.buf == nil {
b.buf = b.initial[:0] // use preallocated space initially
}
if len(b.buf)+n &gt; cap(b.buf) {
buf := make([]byte, b.Len(), 2*cap(b.buf) + n)
copy(buf, b.buf)
b.buf = buf
}
}
</code></pre>
<p>封装的第三个优点也是最重要的优点是阻止了外部调用方对对象内部的值任意地进行修改。因为对象内部变量只可以被同一个包内的函数修改所以包的作者可以让这些函数确保对象内部的一些值的不变性。比如下面的Counter类型允许调用方来增加counter变量的值并且允许将这个值reset为0但是不允许随便设置这个值译注因为压根就访问不到</p>
<pre><code class="language-go">type Counter struct { n int }
func (c *Counter) N() int { return c.n }
func (c *Counter) Increment() { c.n++ }
func (c *Counter) Reset() { c.n = 0 }
</code></pre>
<p>只用来访问或修改内部变量的函数被称为setter或者getter例子如下比如log包里的Logger类型对应的一些函数。在命名一个getter方法时我们通常会省略掉前面的Get前缀。这种简洁上的偏好也可以推广到各种类型的前缀比如FetchFind或者Lookup。</p>
<pre><code class="language-go">package log
type Logger struct {
flags int
prefix string
// ...
}
func (l *Logger) Flags() int
func (l *Logger) SetFlags(flag int)
func (l *Logger) Prefix() string
func (l *Logger) SetPrefix(prefix string)
</code></pre>
<p>Go的编码风格不禁止直接导出字段。当然一旦进行了导出就没有办法在保证API兼容的情况下去除对其的导出所以在一开始的选择一定要经过深思熟虑并且要考虑到包内部的一些不变量的保证未来可能的变化以及调用方的代码质量是否会因为包的一点修改而变差。</p>
<p>封装并不总是理想的。
虽然封装在有些情况是必要的但有时候我们也需要暴露一些内部内容比如time.Duration将其表现暴露为一个int64数字的纳秒使得我们可以用一般的数值操作来对时间进行对比甚至可以定义这种类型的常量</p>
<pre><code class="language-go">const day = 24 * time.Hour
fmt.Println(day.Seconds()) // &quot;86400&quot;
</code></pre>
<p>另一个例子将IntSet和本章开头的geometry.Path进行对比。Path被定义为一个slice类型这允许其调用slice的字面方法来对其内部的points用range进行迭代遍历在这一点上IntSet是没有办法让你这么做的。</p>
<p>这两种类型决定性的不同geometry.Path的本质是一个坐标点的序列不多也不少我们可以预见到之后也并不会给他增加额外的字段所以在geometry包中将Path暴露为一个slice。相比之下IntSet仅仅是在这里用了一个[]uint64的slice。这个类型还可以用[]uint类型来表示或者我们甚至可以用其它完全不同的占用更小内存空间的东西来表示这个集合所以我们可能还会需要额外的字段来在这个类型中记录元素的个数。也正是因为这些原因我们让IntSet对调用方不透明。</p>
<p>在这章中我们学到了如何将方法与命名类型进行组合并且知道了如何调用这些方法。尽管方法对于OOP编程来说至关重要但他们只是OOP编程里的半边天。为了完成OOP我们还需要接口。Go里的接口会在下一章中介绍。</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="第7章-接口"><a class="header" href="#第7章-接口">第7章 接口</a></h1>
<p>接口类型是对其它类型行为的抽象和概括;因为接口类型不会和特定的实现细节绑定在一起,通过这种抽象的方式我们可以让我们的函数更加灵活和更具有适应能力。</p>
<p>很多面向对象的语言都有相似的接口概念但Go语言中接口类型的独特之处在于它是满足隐式实现的。也就是说我们没有必要对于给定的具体类型定义所有满足的接口类型简单地拥有一些必需的方法就足够了。这种设计可以让你创建一个新的接口类型满足已经存在的具体类型却不会去改变这些类型的定义当我们使用的类型来自于不受我们控制的包时这种设计尤其有用。</p>
<p>在本章我们会开始看到接口类型和值的一些基本技巧。顺着这种方式我们将学习几个来自标准库的重要接口。很多Go程序中都尽可能多的去使用标准库中的接口。最后我们会在§7.10看到类型断言的知识§7.13)看到类型开关的使用并且学到他们是怎样让不同的类型的概括成为可能。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="71-接口约定"><a class="header" href="#71-接口约定">7.1. 接口约定</a></h2>
<p>目前为止,我们看到的类型都是具体的类型。一个具体的类型可以准确的描述它所代表的值,并且展示出对类型本身的一些操作方式:就像数字类型的算术操作,切片类型的取下标、添加元素和范围获取操作。具体的类型还可以通过它的内置方法提供额外的行为操作。总的来说,当你拿到一个具体的类型时你就知道它的本身是什么和你可以用它来做什么。</p>
<p>在Go语言中还存在着另外一种类型接口类型。接口类型是一种抽象的类型。它不会暴露出它所代表的对象的内部值的结构和这个对象支持的基础操作的集合它们只会表现出它们自己的方法。也就是说当你有看到一个接口类型的值时你不知道它是什么唯一知道的就是可以通过它的方法来做什么。</p>
<p>在本书中我们一直使用两个相似的函数来进行字符串的格式化fmt.Printf它会把结果写到标准输出和fmt.Sprintf它会把结果以字符串的形式返回。得益于使用接口我们不必可悲的因为返回结果在使用方式上的一些浅显不同就必需把格式化这个最困难的过程复制一份。实际上这两个函数都使用了另一个函数fmt.Fprintf来进行封装。fmt.Fprintf这个函数对它的计算结果会被怎么使用是完全不知道的。</p>
<pre><code class="language-go">package fmt
func Fprintf(w io.Writer, format string, args ...interface{}) (int, error)
func Printf(format string, args ...interface{}) (int, error) {
return Fprintf(os.Stdout, format, args...)
}
func Sprintf(format string, args ...interface{}) string {
var buf bytes.Buffer
Fprintf(&amp;buf, format, args...)
return buf.String()
}
</code></pre>
<p>Fprintf的前缀F表示文件File也表明格式化输出结果应该被写入第一个参数提供的文件中。在Printf函数中的第一个参数os.Stdout是<code>*os.File</code>类型在Sprintf函数中的第一个参数&amp;buf是一个指向可以写入字节的内存缓冲区然而它
并不是一个文件类型尽管它在某种意义上和文件类型相似。</p>
<p>即使Fprintf函数中的第一个参数也不是一个文件类型。它是io.Writer类型这是一个接口类型定义如下</p>
<pre><code class="language-go">package io
// Writer is the interface that wraps the basic Write method.
type Writer interface {
// Write writes len(p) bytes from p to the underlying data stream.
// It returns the number of bytes written from p (0 &lt;= n &lt;= len(p))
// and any error encountered that caused the write to stop early.
// Write must return a non-nil error if it returns n &lt; len(p).
// Write must not modify the slice data, even temporarily.
//
// Implementations must not retain p.
Write(p []byte) (n int, err error)
}
</code></pre>
<p>io.Writer类型定义了函数Fprintf和这个函数调用者之间的约定。一方面这个约定需要调用者提供具体类型的值就像<code>*os.File</code><code>*bytes.Buffer</code>这些类型都有一个特定签名和行为的Write的函数。另一方面这个约定保证了Fprintf接受任何满足io.Writer接口的值都可以工作。Fprintf函数可能没有假定写入的是一个文件或是一段内存而是写入一个可以调用Write函数的值。</p>
<p>因为fmt.Fprintf函数没有对具体操作的值做任何假设而是仅仅通过io.Writer接口的约定来保证行为所以第一个参数可以安全地传入一个只需要满足io.Writer接口的任意具体类型的值。一个类型可以自由地被另一个满足相同接口的类型替换被称作可替换性LSP里氏替换。这是一个面向对象的特征。</p>
<p>让我们通过一个新的类型来进行校验,下面<code>*ByteCounter</code>类型里的Write方法仅仅在丢弃写向它的字节前统计它们的长度。在这个+=赋值语句中让len(p)的类型和<code>*c</code>的类型匹配的转换是必须的。)</p>
<p><u><i>gopl.io/ch7/bytecounter</i></u></p>
<pre><code class="language-go">type ByteCounter int
func (c *ByteCounter) Write(p []byte) (int, error) {
*c += ByteCounter(len(p)) // convert int to ByteCounter
return len(p), nil
}
</code></pre>
<p>因为*ByteCounter满足io.Writer的约定我们可以把它传入Fprintf函数中Fprintf函数执行字符串格式化的过程不会去关注ByteCounter正确的累加结果的长度。</p>
<pre><code class="language-go">var c ByteCounter
c.Write([]byte(&quot;hello&quot;))
fmt.Println(c) // &quot;5&quot;, = len(&quot;hello&quot;)
c = 0 // reset the counter
var name = &quot;Dolly&quot;
fmt.Fprintf(&amp;c, &quot;hello, %s&quot;, name)
fmt.Println(c) // &quot;12&quot;, = len(&quot;hello, Dolly&quot;)
</code></pre>
<p>除了io.Writer这个接口类型还有另一个对fmt包很重要的接口类型。Fprintf和Fprintln函数向类型提供了一种控制它们值输出的途径。在2.5节中我们为Celsius类型提供了一个String方法以便于可以打印成这样&quot;100°C&quot; 在6.5节中我们给*IntSet添加一个String方法这样集合可以用传统的符号来进行表示就像&quot;{1 2 3}&quot;。给一个类型定义String方法可以让它满足最广泛使用之一的接口类型fmt.Stringer</p>
<pre><code class="language-go">package fmt
// The String method is used to print values passed
// as an operand to any format that accepts a string
// or to an unformatted printer such as Print.
type Stringer interface {
String() string
}
</code></pre>
<p>我们会在7.10节解释fmt包怎么发现哪些值是满足这个接口类型的。</p>
<p><strong>练习 7.1</strong> 使用来自ByteCounter的思路实现一个针对单词和行数的计数器。你会发现bufio.ScanWords非常的有用。</p>
<p><strong>练习 7.2</strong> 写一个带有如下函数签名的函数CountingWriter传入一个io.Writer接口类型返回一个把原来的Writer封装在里面的新的Writer类型和一个表示新的写入字节数的int64类型指针。</p>
<pre><code class="language-go">func CountingWriter(w io.Writer) (io.Writer, *int64)
</code></pre>
<p><strong>练习 7.3</strong> 为在gopl.io/ch4/treesort§4.4)中的*tree类型实现一个String方法去展示tree类型的值序列。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="72-接口类型"><a class="header" href="#72-接口类型">7.2. 接口类型</a></h2>
<p>接口类型具体描述了一系列方法的集合,一个实现了这些方法的具体类型是这个接口类型的实例。</p>
<p>io.Writer类型是用得最广泛的接口之一因为它提供了所有类型的写入bytes的抽象包括文件类型内存缓冲区网络链接HTTP客户端压缩工具哈希等等。io包中定义了很多其它有用的接口类型。Reader可以代表任意可以读取bytes的类型Closer可以是任意可以关闭的值例如一个文件或是网络链接。到现在你可能注意到了很多Go语言中单方法接口的命名习惯</p>
<pre><code class="language-go">package io
type Reader interface {
Read(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
</code></pre>
<p>再往下看,我们发现有些新的接口类型通过组合已有的接口来定义。下面是两个例子:</p>
<pre><code class="language-go">type ReadWriter interface {
Reader
Writer
}
type ReadWriteCloser interface {
Reader
Writer
Closer
}
</code></pre>
<p>上面用到的语法和结构内嵌相似我们可以用这种方式以一个简写命名一个接口而不用声明它所有的方法。这种方式称为接口内嵌。尽管略失简洁我们可以像下面这样不使用内嵌来声明io.ReadWriter接口。</p>
<pre><code class="language-go">type ReadWriter interface {
Read(p []byte) (n int, err error)
Write(p []byte) (n int, err error)
}
</code></pre>
<p>或者甚至使用一种混合的风格:</p>
<pre><code class="language-go">type ReadWriter interface {
Read(p []byte) (n int, err error)
Writer
}
</code></pre>
<p>上面3种定义方式都是一样的效果。方法顺序的变化也没有影响唯一重要的就是这个集合里面的方法。</p>
<p><strong>练习 7.4</strong> strings.NewReader函数通过读取一个string参数返回一个满足io.Reader接口类型的值和其它值。实现一个简单版本的NewReader用它来构造一个接收字符串输入的HTML解析器§5.2</p>
<p><strong>练习 7.5</strong> io包里面的LimitReader函数接收一个io.Reader接口类型的r和字节数n并且返回另一个从r中读取字节但是当读完n个字节后就表示读到文件结束的Reader。实现这个LimitReader函数</p>
<pre><code class="language-go">func LimitReader(r io.Reader, n int64) io.Reader
</code></pre>
<div style="break-before: page; page-break-before: always;"></div><h2 id="73-实现接口的条件"><a class="header" href="#73-实现接口的条件">7.3. 实现接口的条件</a></h2>
<p>一个类型如果拥有一个接口需要的所有方法,那么这个类型就实现了这个接口。例如,<code>*os.File</code>类型实现了io.ReaderWriterCloser和ReadWriter接口。<code>*bytes.Buffer</code>实现了ReaderWriter和ReadWriter这些接口但是它没有实现Closer接口因为它不具有Close方法。Go的程序员经常会简要的把一个具体的类型描述成一个特定的接口类型。举个例子<code>*bytes.Buffer</code>是io.Writer<code>*os.Files</code>是io.ReadWriter。</p>
<p>接口指定的规则非常简单:表达一个类型属于某个接口只要这个类型实现这个接口。所以:</p>
<pre><code class="language-go">var w io.Writer
w = os.Stdout // OK: *os.File has Write method
w = new(bytes.Buffer) // OK: *bytes.Buffer has Write method
w = time.Second // compile error: time.Duration lacks Write method
var rwc io.ReadWriteCloser
rwc = os.Stdout // OK: *os.File has Read, Write, Close methods
rwc = new(bytes.Buffer) // compile error: *bytes.Buffer lacks Close method
</code></pre>
<p>这个规则甚至适用于等式右边本身也是一个接口类型</p>
<pre><code class="language-go">w = rwc // OK: io.ReadWriteCloser has Write method
rwc = w // compile error: io.Writer lacks Close method
</code></pre>
<p>因为ReadWriter和ReadWriteCloser包含有Writer的方法所以任何实现了ReadWriter和ReadWriteCloser的类型必定也实现了Writer接口</p>
<p>在进一步学习前必须先解释一个类型持有一个方法的表示当中的细节。回想在6.2章中对于每一个命名过的具体类型T它的一些方法的接收者是类型T本身然而另一些则是一个<code>*T</code>的指针。还记得在T类型的参数上调用一个<code>*T</code>的方法是合法的只要这个参数是一个变量编译器隐式的获取了它的地址。但这仅仅是一个语法糖T类型的值不拥有所有<code>*T</code>指针的方法,这样它就可能只实现了更少的接口。</p>
<p>举个例子可能会更清晰一点。在第6.5章中IntSet类型的String方法的接收者是一个指针类型所以我们不能在一个不能寻址的IntSet值上调用这个方法</p>
<pre><code class="language-go">type IntSet struct { /* ... */ }
func (*IntSet) String() string
var _ = IntSet{}.String() // compile error: String requires *IntSet receiver
</code></pre>
<p>但是我们可以在一个IntSet变量上调用这个方法</p>
<pre><code class="language-go">var s IntSet
var _ = s.String() // OK: s is a variable and &amp;s has a String method
</code></pre>
<p>然而,由于只有<code>*IntSet</code>类型有String方法所以也只有<code>*IntSet</code>类型实现了fmt.Stringer接口</p>
<pre><code class="language-go">var _ fmt.Stringer = &amp;s // OK
var _ fmt.Stringer = s // compile error: IntSet lacks String method
</code></pre>
<p>12.8章包含了一个打印出任意值的所有方法的程序然后可以使用godoc -analysis=type tool(§10.7.4)展示每个类型的方法和具体类型和接口之间的关系</p>
<p>就像信封封装和隐藏起信件来一样,接口类型封装和隐藏具体类型和它的值。即使具体类型有其它的方法,也只有接口类型暴露出来的方法会被调用到:</p>
<pre><code class="language-go">os.Stdout.Write([]byte(&quot;hello&quot;)) // OK: *os.File has Write method
os.Stdout.Close() // OK: *os.File has Close method
var w io.Writer
w = os.Stdout
w.Write([]byte(&quot;hello&quot;)) // OK: io.Writer has Write method
w.Close() // compile error: io.Writer lacks Close method
</code></pre>
<p>一个有更多方法的接口类型比如io.ReadWriter和少一些方法的接口类型例如io.Reader进行对比更多方法的接口类型会告诉我们更多关于它的值持有的信息并且对实现它的类型要求更加严格。那么关于interface{}类型,它没有任何方法,请讲出哪些具体的类型实现了它?</p>
<p>这看上去好像没有用但实际上interface{}被称为空接口类型是不可或缺的。因为空接口类型对实现它的类型没有要求,所以我们可以将任意一个值赋给空接口类型。</p>
<pre><code class="language-go">var any interface{}
any = true
any = 12.34
any = &quot;hello&quot;
any = map[string]int{&quot;one&quot;: 1}
any = new(bytes.Buffer)
</code></pre>
<p>尽管不是很明显从本书最早的例子中我们就已经在使用空接口类型。它允许像fmt.Println或者5.7章中的errorf函数接受任何类型的参数。</p>
<p>对于创建的一个interface{}值持有一个booleanfloatstringmappointer或者任意其它的类型我们当然不能直接对它持有的值做操作因为interface{}没有任何方法。我们会在7.10章中学到一种用类型断言来获取interface{}中值的方法。</p>
<p>因为接口与实现只依赖于判断两个类型的方法,所以没有必要定义一个具体类型和它实现的接口之间的关系。也就是说,有意地在文档里说明或者程序上断言这种关系偶尔是有用的,但程序上不强制这么做。下面的定义在编译期断言一个<code>*bytes.Buffer</code>的值实现了io.Writer接口类型:</p>
<pre><code class="language-go">// *bytes.Buffer must satisfy io.Writer
var w io.Writer = new(bytes.Buffer)
</code></pre>
<p>因为任意<code>*bytes.Buffer</code>的值甚至包括nil通过<code>(*bytes.Buffer)(nil)</code>进行显示的转换都实现了这个接口所以我们不必分配一个新的变量。并且因为我们绝不会引用变量w我们可以使用空标识符来进行代替。总的看这些变化可以让我们得到一个更朴素的版本</p>
<pre><code class="language-go">// *bytes.Buffer must satisfy io.Writer
var _ io.Writer = (*bytes.Buffer)(nil)
</code></pre>
<p>非空的接口类型比如io.Writer经常被指针类型实现尤其当一个或多个接口方法像Write方法那样隐式的给接收者带来变化的时候。一个结构体的指针是非常常见的承载方法的类型。</p>
<p>但是并不意味着只有指针类型满足接口类型甚至连一些有设置方法的接口类型也可能会被Go语言中其它的引用类型实现。我们已经看过slice类型的方法geometry.Path§6.1和map类型的方法url.Values§6.2.1后面还会看到函数类型的方法的例子http.HandlerFunc§7.7。甚至基本的类型也可能会实现一些接口就如我们在7.4章中看到的time.Duration类型实现了fmt.Stringer接口。</p>
<p>一个具体的类型可能实现了很多不相关的接口。考虑在一个组织出售数字文化产品比如音乐,电影和书籍的程序中可能定义了下列的具体类型:</p>
<pre><code>Album
Book
Movie
Magazine
Podcast
TVEpisode
Track
</code></pre>
<p>我们可以把每个抽象的特点用接口来表示。一些特性对于所有的这些文化产品都是共通的,例如标题,创作日期和作者列表。</p>
<pre><code class="language-go">type Artifact interface {
Title() string
Creators() []string
Created() time.Time
}
</code></pre>
<p>其它的一些特性只对特定类型的文化产品才有。和文字排版特性相关的只有books和magazines还有只有movies和TV剧集和屏幕分辨率相关。</p>
<pre><code class="language-go">type Text interface {
Pages() int
Words() int
PageSize() int
}
type Audio interface {
Stream() (io.ReadCloser, error)
RunningTime() time.Duration
Format() string // e.g., &quot;MP3&quot;, &quot;WAV&quot;
}
type Video interface {
Stream() (io.ReadCloser, error)
RunningTime() time.Duration
Format() string // e.g., &quot;MP4&quot;, &quot;WMV&quot;
Resolution() (x, y int)
}
</code></pre>
<p>这些接口不止是一种有用的方式来分组相关的具体类型和表示他们之间的共同特点。我们后面可能会发现其它的分组。举例如果我们发现我们需要以同样的方式处理Audio和Video我们可以定义一个Streamer接口来代表它们之间相同的部分而不必对已经存在的类型做改变。</p>
<pre><code class="language-go">type Streamer interface {
Stream() (io.ReadCloser, error)
RunningTime() time.Duration
Format() string
}
</code></pre>
<p>每一个具体类型的组基于它们相同的行为可以表示成一个接口类型。不像基于类的语言他们一个类实现的接口集合需要进行显式的定义在Go语言中我们可以在需要的时候定义一个新的抽象或者特定特点的组而不需要修改具体类型的定义。当具体的类型来自不同的作者时这种方式会特别有用。当然也确实没有必要在具体的类型中指出这些共性。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="74-flagvalue接口"><a class="header" href="#74-flagvalue接口">7.4. flag.Value接口</a></h2>
<p>在本章我们会学到另一个标准的接口类型flag.Value是怎么帮助命令行标记定义新的符号的。思考下面这个会休眠特定时间的程序</p>
<p><u><i>gopl.io/ch7/sleep</i></u></p>
<pre><code class="language-go">var period = flag.Duration(&quot;period&quot;, 1*time.Second, &quot;sleep period&quot;)
func main() {
flag.Parse()
fmt.Printf(&quot;Sleeping for %v...&quot;, *period)
time.Sleep(*period)
fmt.Println()
}
</code></pre>
<p>在它休眠前它会打印出休眠的时间周期。fmt包调用time.Duration的String方法打印这个时间周期是以用户友好的注解方式而不是一个纳秒数字</p>
<pre><code>$ go build gopl.io/ch7/sleep
$ ./sleep
Sleeping for 1s...
</code></pre>
<p>默认情况下,休眠周期是一秒,但是可以通过 -period 这个命令行标记来控制。flag.Duration函数创建一个time.Duration类型的标记变量并且允许用户通过多种用户友好的方式来设置这个变量的大小这种方式还包括和String方法相同的符号排版形式。这种对称设计使得用户交互良好。</p>
<pre><code>$ ./sleep -period 50ms
Sleeping for 50ms...
$ ./sleep -period 2m30s
Sleeping for 2m30s...
$ ./sleep -period 1.5h
Sleeping for 1h30m0s...
$ ./sleep -period &quot;1 day&quot;
invalid value &quot;1 day&quot; for flag -period: time: invalid duration 1 day
</code></pre>
<p>因为时间周期标记值非常的有用所以这个特性被构建到了flag包中但是我们为我们自己的数据类型定义新的标记符号是简单容易的。我们只需要定义一个实现flag.Value接口的类型如下</p>
<pre><code class="language-go">package flag
// Value is the interface to the value stored in a flag.
type Value interface {
String() string
Set(string) error
}
</code></pre>
<p>String方法格式化标记的值用在命令行帮助消息中这样每一个flag.Value也是一个fmt.Stringer。Set方法解析它的字符串参数并且更新标记变量的值。实际上Set方法和String是两个相反的操作所以最好的办法就是对他们使用相同的注解方式。</p>
<p>让我们定义一个允许通过摄氏度或者华氏温度变换的形式指定温度的celsiusFlag类型。注意celsiusFlag内嵌了一个Celsius类型§2.5因此不用实现本身就已经有String方法了。为了实现flag.Value我们只需要定义Set方法</p>
<p><u><i>gopl.io/ch7/tempconv</i></u></p>
<pre><code class="language-go">// *celsiusFlag satisfies the flag.Value interface.
type celsiusFlag struct{ Celsius }
func (f *celsiusFlag) Set(s string) error {
var unit string
var value float64
fmt.Sscanf(s, &quot;%f%s&quot;, &amp;value, &amp;unit) // no error check needed
switch unit {
case &quot;C&quot;, &quot;°C&quot;:
f.Celsius = Celsius(value)
return nil
case &quot;F&quot;, &quot;°F&quot;:
f.Celsius = FToC(Fahrenheit(value))
return nil
}
return fmt.Errorf(&quot;invalid temperature %q&quot;, s)
}
</code></pre>
<p>调用fmt.Sscanf函数从输入s中解析一个浮点数value和一个字符串unit。虽然通常必须检查Sscanf的错误返回但是在这个例子中我们不需要。因为如果有错误发生就没有switch case会匹配到。</p>
<p>下面的CelsiusFlag函数将所有逻辑都封装在一起。它返回一个内嵌在celsiusFlag变量f中的Celsius指针给调用者。Celsius字段是一个会通过Set方法在标记处理的过程中更新的变量。调用Var方法将标记加入应用的命令行标记集合中有异常复杂命令行接口的全局变量flag.CommandLine.Programs可能有几个这个类型的变量。调用Var方法将一个<code>*celsiusFlag</code>参数赋值给一个flag.Value参数导致编译器去检查<code>*celsiusFlag</code>是否有必须的方法。</p>
<pre><code class="language-go">// CelsiusFlag defines a Celsius flag with the specified name,
// default value, and usage, and returns the address of the flag variable.
// The flag argument must have a quantity and a unit, e.g., &quot;100C&quot;.
func CelsiusFlag(name string, value Celsius, usage string) *Celsius {
f := celsiusFlag{value}
flag.CommandLine.Var(&amp;f, name, usage)
return &amp;f.Celsius
}
</code></pre>
<p>现在我们可以开始在我们的程序中使用新的标记:</p>
<p><u><i>gopl.io/ch7/tempflag</i></u></p>
<pre><code class="language-go">var temp = tempconv.CelsiusFlag(&quot;temp&quot;, 20.0, &quot;the temperature&quot;)
func main() {
flag.Parse()
fmt.Println(*temp)
}
</code></pre>
<p>下面是典型的场景:</p>
<pre><code>$ go build gopl.io/ch7/tempflag
$ ./tempflag
20°C
$ ./tempflag -temp -18C
-18°C
$ ./tempflag -temp 212°F
100°C
$ ./tempflag -temp 273.15K
invalid value &quot;273.15K&quot; for flag -temp: invalid temperature &quot;273.15K&quot;
Usage of ./tempflag:
-temp value
the temperature (default 20°C)
$ ./tempflag -help
Usage of ./tempflag:
-temp value
the temperature (default 20°C)
</code></pre>
<p><strong>练习 7.6</strong> 对tempFlag加入支持开尔文温度。</p>
<p><strong>练习 7.7</strong> 解释为什么帮助信息在它的默认值是20.0没有包含°C的情况下输出了°C。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="75--接口值"><a class="header" href="#75--接口值">7.5. 接口值</a></h2>
<p>概念上讲一个接口的值接口值由两个部分组成一个具体的类型和那个类型的值。它们被称为接口的动态类型和动态值。对于像Go语言这种静态类型的语言类型是编译期的概念因此一个类型不是一个值。在我们的概念模型中一些提供每个类型信息的值被称为类型描述符比如类型的名称和方法。在一个接口值中类型部分代表与之相关类型的描述符。</p>
<p>下面4个语句中变量w得到了3个不同的值。开始和最后的值是相同的</p>
<pre><code class="language-go">var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil
</code></pre>
<p>让我们进一步观察在每一个语句后的w变量的值和动态行为。第一个语句定义了变量w:</p>
<pre><code class="language-go">var w io.Writer
</code></pre>
<p>在Go语言中变量总是被一个定义明确的值初始化即使接口类型也不例外。对于一个接口的零值就是它的类型和值的部分都是nil图7.1)。</p>
<p><img src="ch7/../images/ch7-01.png" alt="" /></p>
<p>一个接口值基于它的动态类型被描述为空或非空所以这是一个空的接口值。你可以通过使用w==nil或者w!=nil来判断接口值是否为空。调用一个空接口值上的任意方法都会产生panic:</p>
<pre><code class="language-go">w.Write([]byte(&quot;hello&quot;)) // panic: nil pointer dereference
</code></pre>
<p>第二个语句将一个<code>*os.File</code>类型的值赋给变量w:</p>
<pre><code class="language-go">w = os.Stdout
</code></pre>
<p>这个赋值过程调用了一个具体类型到接口类型的隐式转换这和显式的使用io.Writer(os.Stdout)是等价的。这类转换不管是显式的还是隐式的,都会刻画出操作到的类型和值。这个接口值的动态类型被设为<code>*os.File</code>指针的类型描述符它的动态值持有os.Stdout的拷贝这是一个代表处理标准输出的os.File类型变量的指针图7.2)。</p>
<p><img src="ch7/../images/ch7-02.png" alt="" /></p>
<p>调用一个包含<code>*os.File</code>类型指针的接口值的Write方法使得<code>(*os.File).Write</code>方法被调用。这个调用输出“hello”。</p>
<pre><code class="language-go">w.Write([]byte(&quot;hello&quot;)) // &quot;hello&quot;
</code></pre>
<p>通常在编译期我们不知道接口值的动态类型是什么所以一个接口上的调用必须使用动态分配。因为不是直接进行调用所以编译器必须把代码生成在类型描述符的方法Write上然后间接调用那个地址。这个调用的接收者是一个接口动态值的拷贝os.Stdout。效果和下面这个直接调用一样</p>
<pre><code class="language-go">os.Stdout.Write([]byte(&quot;hello&quot;)) // &quot;hello&quot;
</code></pre>
<p>第三个语句给接口值赋了一个*bytes.Buffer类型的值</p>
<pre><code class="language-go">w = new(bytes.Buffer)
</code></pre>
<p>现在动态类型是*bytes.Buffer并且动态值是一个指向新分配的缓冲区的指针图7.3)。</p>
<p><img src="ch7/../images/ch7-03.png" alt="" /></p>
<p>Write方法的调用也使用了和之前一样的机制</p>
<pre><code class="language-go">w.Write([]byte(&quot;hello&quot;)) // writes &quot;hello&quot; to the bytes.Buffers
</code></pre>
<p>这次类型描述符是*bytes.Buffer所以调用了(*bytes.Buffer).Write方法并且接收者是该缓冲区的地址。这个调用把字符串“hello”添加到缓冲区中。</p>
<p>最后第四个语句将nil赋给了接口值</p>
<pre><code class="language-go">w = nil
</code></pre>
<p>这个重置将它所有的部分都设为nil值把变量w恢复到和它之前定义时相同的状态在图7.1中可以看到。</p>
<p>一个接口值可以持有任意大的动态值。例如表示时间实例的time.Time类型这个类型有几个对外不公开的字段。我们从它上面创建一个接口值</p>
<pre><code class="language-go">var x interface{} = time.Now()
</code></pre>
<p>结果可能和图7.4相似。从概念上讲,不论接口值多大,动态值总是可以容下它。(这只是一个概念上的模型;具体的实现可能会非常不同)</p>
<p><img src="ch7/../images/ch7-04.png" alt="" /></p>
<p>接口值可以使用==和!来进行比较。两个接口值相等仅当它们都是nil值或者它们的动态类型相同并且动态值也根据这个动态类型的==操作相等。因为接口值是可比较的所以它们可以用在map的键或者作为switch语句的操作数。</p>
<p>然而如果两个接口值的动态类型相同但是这个动态类型是不可比较的比如切片将它们进行比较就会失败并且panic:</p>
<pre><code class="language-go">var x interface{} = []int{1, 2, 3}
fmt.Println(x == x) // panic: comparing uncomparable type []int
</code></pre>
<p>考虑到这点接口类型是非常与众不同的。其它类型要么是安全的可比较类型如基本类型和指针要么是完全不可比较的类型如切片映射类型和函数但是在比较接口值或者包含了接口值的聚合类型时我们必须要意识到潜在的panic。同样的风险也存在于使用接口作为map的键或者switch的操作数。只能比较你非常确定它们的动态值是可比较类型的接口值。</p>
<p>当我们处理错误或者调试的过程中得知接口值的动态类型是非常有帮助的。所以我们使用fmt包的%T动作:</p>
<pre><code class="language-go">var w io.Writer
fmt.Printf(&quot;%T\n&quot;, w) // &quot;&lt;nil&gt;&quot;
w = os.Stdout
fmt.Printf(&quot;%T\n&quot;, w) // &quot;*os.File&quot;
w = new(bytes.Buffer)
fmt.Printf(&quot;%T\n&quot;, w) // &quot;*bytes.Buffer&quot;
</code></pre>
<p>在fmt包内部使用反射来获取接口动态类型的名称。我们会在第12章中学到反射相关的知识。</p>
<h3 id="751--警告一个包含nil指针的接口不是nil接口"><a class="header" href="#751--警告一个包含nil指针的接口不是nil接口">7.5.1. 警告一个包含nil指针的接口不是nil接口</a></h3>
<p>一个不包含任何值的nil接口值和一个刚好包含nil指针的接口值是不同的。这个细微区别产生了一个容易绊倒每个Go程序员的陷阱。</p>
<p>思考下面的程序。当debug变量设置为true时main函数会将f函数的输出收集到一个bytes.Buffer类型中。</p>
<pre><code class="language-go">const debug = true
func main() {
var buf *bytes.Buffer
if debug {
buf = new(bytes.Buffer) // enable collection of output
}
f(buf) // NOTE: subtly incorrect!
if debug {
// ...use buf...
}
}
// If out is non-nil, output will be written to it.
func f(out io.Writer) {
// ...do something...
if out != nil {
out.Write([]byte(&quot;done!\n&quot;))
}
}
</code></pre>
<p>我们可能会预计当把变量debug设置为false时可以禁止对输出的收集但是实际上在out.Write方法调用时程序发生了panic</p>
<pre><code class="language-go">if out != nil {
out.Write([]byte(&quot;done!\n&quot;)) // panic: nil pointer dereference
}
</code></pre>
<p>当main函数调用函数f时它给f函数的out参数赋了一个*bytes.Buffer的空指针所以out的动态值是nil。然而它的动态类型是*bytes.Buffer意思就是out变量是一个包含空指针值的非空接口如图7.5所以防御性检查out!=nil的结果依然是true。</p>
<p><img src="ch7/../images/ch7-05.png" alt="" /></p>
<p>动态分配机制依然决定(*bytes.Buffer).Write的方法会被调用但是这次的接收者的值是nil。对于一些如*os.File的类型nil是一个有效的接收者§6.2.1),但是*bytes.Buffer类型不在这些种类中。这个方法会被调用但是当它尝试去获取缓冲区时会发生panic。</p>
<p>问题在于尽管一个nil的*bytes.Buffer指针有实现这个接口的方法它也不满足这个接口具体的行为上的要求。特别是这个调用违反了(*bytes.Buffer).Write方法的接收者非空的隐含先觉条件所以将nil指针赋给这个接口是错误的。解决方案就是将main函数中的变量buf的类型改为io.Writer因此可以避免一开始就将一个不完整的值赋值给这个接口</p>
<pre><code class="language-go">var buf io.Writer
if debug {
buf = new(bytes.Buffer) // enable collection of output
}
f(buf) // OK
</code></pre>
<p>现在我们已经把接口值的技巧都讲完了让我们来看更多的一些在Go标准库中的重要接口类型。在下面的三章中我们会看到接口类型是怎样用在排序web服务错误处理中的。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="76-sortinterface接口"><a class="header" href="#76-sortinterface接口">7.6. sort.Interface接口</a></h2>
<p>排序操作和字符串格式化一样是很多程序经常使用的操作。尽管一个最短的快排程序只要15行就可以搞定但是一个健壮的实现需要更多的代码并且我们不希望每次我们需要的时候都重写或者拷贝这些代码。</p>
<p>幸运的是sort包内置的提供了根据一些排序函数来对任何序列排序的功能。它的设计非常独到。在很多语言中排序算法都是和序列数据类型关联同时排序函数和具体类型元素关联。相比之下Go语言的sort.Sort函数不会对具体的序列和它的元素做任何假设。相反它使用了一个接口类型sort.Interface来指定通用的排序算法和可能被排序到的序列类型之间的约定。这个接口的实现由序列的具体表示和它希望排序的元素决定序列的表示经常是一个切片。</p>
<p>一个内置的排序算法需要知道三个东西序列的长度表示两个元素比较的结果一种交换两个元素的方式这就是sort.Interface的三个方法</p>
<pre><code class="language-go">package sort
type Interface interface {
Len() int
Less(i, j int) bool // i, j are indices of sequence elements
Swap(i, j int)
}
</code></pre>
<p>为了对序列进行排序我们需要定义一个实现了这三个方法的类型然后对这个类型的一个实例应用sort.Sort函数。思考对一个字符串切片进行排序这可能是最简单的例子了。下面是这个新的类型StringSlice和它的LenLess和Swap方法</p>
<pre><code class="language-go">type StringSlice []string
func (p StringSlice) Len() int { return len(p) }
func (p StringSlice) Less(i, j int) bool { return p[i] &lt; p[j] }
func (p StringSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
</code></pre>
<p>现在我们可以通过像下面这样将一个切片转换为一个StringSlice类型来进行排序</p>
<pre><code class="language-go">sort.Sort(StringSlice(names))
</code></pre>
<p>这个转换得到一个相同长度容量和基于names数组的切片值并且这个切片值的类型有三个排序需要的方法。</p>
<p>对字符串切片的排序是很常用的需要所以sort包提供了StringSlice类型也提供了Strings函数能让上面这些调用简化成sort.Strings(names)。</p>
<p>这里用到的技术很容易适用到其它排序序列中例如我们可以忽略大小写或者含有的特殊字符。本书使用Go程序对索引词和页码进行排序也用到了这个技术对罗马数字做了额外逻辑处理。对于更复杂的排序我们使用相同的方法但是会用更复杂的数据结构和更复杂地实现sort.Interface的方法。</p>
<p>我们会运行上面的例子来对一个表格中的音乐播放列表进行排序。每个track都是单独的一行每一列都是这个track的属性像艺术家标题和运行时间。想象一个图形用户界面来呈现这个表格并且点击一个属性的顶部会使这个列表按照这个属性进行排序再一次点击相同属性的顶部会进行逆向排序。让我们看下每个点击会发生什么响应。</p>
<p>下面的变量tracks包含了一个播放列表。One of the authors apologizes for the other authors musical tastes.每个元素都不是Track本身而是指向它的指针。尽管我们在下面的代码中直接存储Tracks也可以工作sort函数会交换很多对元素所以如果每个元素都是指针而不是Track类型会更快指针是一个机器字码长度而Track类型可能是八个或更多。</p>
<p><u><i>gopl.io/ch7/sorting</i></u></p>
<pre><code class="language-go">type Track struct {
Title string
Artist string
Album string
Year int
Length time.Duration
}
var tracks = []*Track{
{&quot;Go&quot;, &quot;Delilah&quot;, &quot;From the Roots Up&quot;, 2012, length(&quot;3m38s&quot;)},
{&quot;Go&quot;, &quot;Moby&quot;, &quot;Moby&quot;, 1992, length(&quot;3m37s&quot;)},
{&quot;Go Ahead&quot;, &quot;Alicia Keys&quot;, &quot;As I Am&quot;, 2007, length(&quot;4m36s&quot;)},
{&quot;Ready 2 Go&quot;, &quot;Martin Solveig&quot;, &quot;Smash&quot;, 2011, length(&quot;4m24s&quot;)},
}
func length(s string) time.Duration {
d, err := time.ParseDuration(s)
if err != nil {
panic(s)
}
return d
}
</code></pre>
<p>printTracks函数将播放列表打印成一个表格。一个图形化的展示可能会更好点但是这个小程序使用text/tabwriter包来生成一个列整齐对齐和隔开的表格像下面展示的这样。注意到<code>*tabwriter.Writer</code>是满足io.Writer接口的。它会收集每一片写向它的数据它的Flush方法会格式化整个表格并且将它写向os.Stdout标准输出</p>
<pre><code class="language-go">func printTracks(tracks []*Track) {
const format = &quot;%v\t%v\t%v\t%v\t%v\t\n&quot;
tw := new(tabwriter.Writer).Init(os.Stdout, 0, 8, 2, ' ', 0)
fmt.Fprintf(tw, format, &quot;Title&quot;, &quot;Artist&quot;, &quot;Album&quot;, &quot;Year&quot;, &quot;Length&quot;)
fmt.Fprintf(tw, format, &quot;-----&quot;, &quot;------&quot;, &quot;-----&quot;, &quot;----&quot;, &quot;------&quot;)
for _, t := range tracks {
fmt.Fprintf(tw, format, t.Title, t.Artist, t.Album, t.Year, t.Length)
}
tw.Flush() // calculate column widths and print table
}
</code></pre>
<p>为了能按照Artist字段对播放列表进行排序我们会像对StringSlice那样定义一个新的带有必须的LenLess和Swap方法的切片类型。</p>
<pre><code class="language-go">type byArtist []*Track
func (x byArtist) Len() int { return len(x) }
func (x byArtist) Less(i, j int) bool { return x[i].Artist &lt; x[j].Artist }
func (x byArtist) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
</code></pre>
<p>为了调用通用的排序程序我们必须先将tracks转换为新的byArtist类型它定义了具体的排序</p>
<pre><code class="language-go">sort.Sort(byArtist(tracks))
</code></pre>
<p>在按照artist对这个切片进行排序后printTrack的输出如下</p>
<pre><code>Title Artist Album Year Length
----- ------ ----- ---- ------
Go Ahead Alicia Keys As I Am 2007 4m36s
Go Delilah From the Roots Up 2012 3m38s
Ready 2 Go Martin Solveig Smash 2011 4m24s
Go Moby Moby 1992 3m37s
</code></pre>
<p>如果用户第二次请求“按照artist排序”我们会对tracks进行逆向排序。然而我们不需要定义一个有颠倒Less方法的新类型byReverseArtist因为sort包中提供了Reverse函数将排序顺序转换成逆序。</p>
<pre><code class="language-go">sort.Sort(sort.Reverse(byArtist(tracks)))
</code></pre>
<p>在按照artist对这个切片进行逆向排序后printTrack的输出如下</p>
<pre><code>Title Artist Album Year Length
----- ------ ----- ---- ------
Go Moby Moby 1992 3m37s
Ready 2 Go Martin Solveig Smash 2011 4m24s
Go Delilah From the Roots Up 2012 3m38s
Go Ahead Alicia Keys As I Am 2007 4m36s
</code></pre>
<p>sort.Reverse函数值得进行更近一步的学习因为它使用了§6.3章中的组合这是一个重要的思路。sort包定义了一个不公开的struct类型reverse它嵌入了一个sort.Interface。reverse的Less方法调用了内嵌的sort.Interface值的Less方法但是通过交换索引的方式使排序结果变成逆序。</p>
<pre><code class="language-go">package sort
type reverse struct{ Interface } // that is, sort.Interface
func (r reverse) Less(i, j int) bool { return r.Interface.Less(j, i) }
func Reverse(data Interface) Interface { return reverse{data} }
</code></pre>
<p>reverse的另外两个方法Len和Swap隐式地由原有内嵌的sort.Interface提供。因为reverse是一个不公开的类型所以导出函数Reverse返回一个包含原有sort.Interface值的reverse类型实例。</p>
<p>为了可以按照不同的列进行排序我们必须定义一个新的类型例如byYear</p>
<pre><code class="language-go">type byYear []*Track
func (x byYear) Len() int { return len(x) }
func (x byYear) Less(i, j int) bool { return x[i].Year &lt; x[j].Year }
func (x byYear) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
</code></pre>
<p>在使用sort.Sort(byYear(tracks))按照年对tracks进行排序后printTrack展示了一个按时间先后顺序的列表</p>
<pre><code>Title Artist Album Year Length
----- ------ ----- ---- ------
Go Moby Moby 1992 3m37s
Go Ahead Alicia Keys As I Am 2007 4m36s
Ready 2 Go Martin Solveig Smash 2011 4m24s
Go Delilah From the Roots Up 2012 3m38s
</code></pre>
<p>对于我们需要的每个切片元素类型和每个排序函数我们需要定义一个新的sort.Interface实现。如你所见Len和Swap方法对于所有的切片类型都有相同的定义。下个例子具体的类型customSort会将一个切片和函数结合使我们只需要写比较函数就可以定义一个新的排序。顺便说下实现了sort.Interface的具体类型不一定是切片类型customSort是一个结构体类型。</p>
<pre><code class="language-go">type customSort struct {
t []*Track
less func(x, y *Track) bool
}
func (x customSort) Len() int { return len(x.t) }
func (x customSort) Less(i, j int) bool { return x.less(x.t[i], x.t[j]) }
func (x customSort) Swap(i, j int) { x.t[i], x.t[j] = x.t[j], x.t[i] }
</code></pre>
<p>让我们定义一个多层的排序函数它主要的排序键是标题第二个键是年第三个键是运行时间Length。下面是该排序的调用其中这个排序使用了匿名排序函数</p>
<pre><code class="language-go">sort.Sort(customSort{tracks, func(x, y *Track) bool {
if x.Title != y.Title {
return x.Title &lt; y.Title
}
if x.Year != y.Year {
return x.Year &lt; y.Year
}
if x.Length != y.Length {
return x.Length &lt; y.Length
}
return false
}})
</code></pre>
<p>这下面是排序的结果。注意到两个标题是“Go”的track按照标题排序是相同的顺序但是在按照year排序上更久的那个track优先。</p>
<pre><code>Title Artist Album Year Length
----- ------ ----- ---- ------
Go Moby Moby 1992 3m37s
Go Delilah From the Roots Up 2012 3m38s
Go Ahead Alicia Keys As I Am 2007 4m36s
Ready 2 Go Martin Solveig Smash 2011 4m24s
</code></pre>
<p>尽管对长度为n的序列排序需要 O(n log n)次比较操作检查一个序列是否已经有序至少需要n-1次比较。sort包中的IsSorted函数帮我们做这样的检查。像sort.Sort一样它也使用sort.Interface对这个序列和它的排序函数进行抽象但是它从不会调用Swap方法这段代码示范了IntsAreSorted和Ints函数在IntSlice类型上的使用</p>
<pre><code class="language-go">values := []int{3, 1, 4, 1}
fmt.Println(sort.IntsAreSorted(values)) // &quot;false&quot;
sort.Ints(values)
fmt.Println(values) // &quot;[1 1 3 4]&quot;
fmt.Println(sort.IntsAreSorted(values)) // &quot;true&quot;
sort.Sort(sort.Reverse(sort.IntSlice(values)))
fmt.Println(values) // &quot;[4 3 1 1]&quot;
fmt.Println(sort.IntsAreSorted(values)) // &quot;false&quot;
</code></pre>
<p>为了使用方便sort包为[]int、[]string和[]float64的正常排序提供了特定版本的函数和类型。对于其他类型例如[]int64或者[]uint尽管路径也很简单还是依赖我们自己实现。</p>
<p><strong>练习 7.8</strong> 很多图形界面提供了一个有状态的多重排序表格插件主要的排序键是最近一次点击过列头的列第二个排序键是第二最近点击过列头的列等等。定义一个sort.Interface的实现用在这样的表格中。比较这个实现方式和重复使用sort.Stable来排序的方式。</p>
<p><strong>练习 7.9</strong> 使用html/template包§4.6替代printTracks将tracks展示成一个HTML表格。将这个解决方案用在前一个练习中让每次点击一个列的头部产生一个HTTP请求来排序这个表格。</p>
<p><strong>练习 7.10</strong> sort.Interface类型也可以适用在其它地方。编写一个IsPalindrome(s sort.Interface) bool函数表明序列s是否是回文序列换句话说反向排序不会改变这个序列。假设如果!s.Less(i, j) &amp;&amp; !s.Less(j, i)则索引i和j上的元素相等。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="77-httphandler接口"><a class="header" href="#77-httphandler接口">7.7. http.Handler接口</a></h2>
<p>在第一章中我们粗略的了解了怎么用net/http包去实现网络客户端§1.5和服务器§1.7。在这个小节中我们会对那些基于http.Handler接口的服务器API做更进一步的学习</p>
<p><u><i>net/http</i></u></p>
<pre><code class="language-go">package http
type Handler interface {
ServeHTTP(w ResponseWriter, r *Request)
}
func ListenAndServe(address string, h Handler) error
</code></pre>
<p>ListenAndServe函数需要一个例如“localhost:8000”的服务器地址和一个所有请求都可以分派的Handler接口实例。它会一直运行直到这个服务因为一个错误而失败或者启动失败它的返回值一定是一个非空的错误。</p>
<p>想象一个电子商务网站为了销售将数据库中物品的价格映射成美元。下面这个程序可能是能想到的最简单的实现了。它将库存清单模型化为一个命名为database的map类型我们给这个类型一个ServeHttp方法这样它可以满足http.Handler接口。这个handler会遍历整个map并输出物品信息。</p>
<p><u><i>gopl.io/ch7/http1</i></u></p>
<pre><code class="language-go">func main() {
db := database{&quot;shoes&quot;: 50, &quot;socks&quot;: 5}
log.Fatal(http.ListenAndServe(&quot;localhost:8000&quot;, db))
}
type dollars float32
func (d dollars) String() string { return fmt.Sprintf(&quot;$%.2f&quot;, d) }
type database map[string]dollars
func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
for item, price := range db {
fmt.Fprintf(w, &quot;%s: %s\n&quot;, item, price)
}
}
</code></pre>
<p>如果我们启动这个服务,</p>
<pre><code>$ go build gopl.io/ch7/http1
$ ./http1 &amp;
</code></pre>
<p>然后用1.5节中的获取程序如果你更喜欢可以使用web浏览器来连接服务器我们得到下面的输出</p>
<pre><code>$ go build gopl.io/ch1/fetch
$ ./fetch http://localhost:8000
shoes: $50.00
socks: $5.00
</code></pre>
<p>目前为止这个服务器不考虑URL只能为每个请求列出它全部的库存清单。更真实的服务器会定义多个不同的URL每一个都会触发一个不同的行为。让我们使用/list来调用已经存在的这个行为并且增加另一个/price调用表明单个货品的价格像这样/price?item=socks来指定一个请求参数。</p>
<p><u><i>gopl.io/ch7/http2</i></u></p>
<pre><code class="language-go">func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
switch req.URL.Path {
case &quot;/list&quot;:
for item, price := range db {
fmt.Fprintf(w, &quot;%s: %s\n&quot;, item, price)
}
case &quot;/price&quot;:
item := req.URL.Query().Get(&quot;item&quot;)
price, ok := db[item]
if !ok {
w.WriteHeader(http.StatusNotFound) // 404
fmt.Fprintf(w, &quot;no such item: %q\n&quot;, item)
return
}
fmt.Fprintf(w, &quot;%s\n&quot;, price)
default:
w.WriteHeader(http.StatusNotFound) // 404
fmt.Fprintf(w, &quot;no such page: %s\n&quot;, req.URL)
}
}
</code></pre>
<p>现在handler基于URL的路径部分req.URL.Path来决定执行什么逻辑。如果这个handler不能识别这个路径它会通过调用w.WriteHeader(http.StatusNotFound)返回客户端一个HTTP错误这个检查应该在向w写入任何值前完成。顺便提一下http.ResponseWriter是另一个接口。它在io.Writer上增加了发送HTTP相应头的方法。等效地我们可以使用实用的http.Error函数</p>
<pre><code class="language-go">msg := fmt.Sprintf(&quot;no such page: %s\n&quot;, req.URL)
http.Error(w, msg, http.StatusNotFound) // 404
</code></pre>
<p>/price的case会调用URL的Query方法来将HTTP请求参数解析为一个map或者更准确地说一个net/url包中url.Values(§6.2.1)类型的多重映射。然后找到第一个item参数并查找它的价格。如果这个货品没有找到会返回一个错误。</p>
<p>这里是一个和新服务器会话的例子:</p>
<pre><code>$ go build gopl.io/ch7/http2
$ go build gopl.io/ch1/fetch
$ ./http2 &amp;
$ ./fetch http://localhost:8000/list
shoes: $50.00
socks: $5.00
$ ./fetch http://localhost:8000/price?item=socks
$5.00
$ ./fetch http://localhost:8000/price?item=shoes
$50.00
$ ./fetch http://localhost:8000/price?item=hat
no such item: &quot;hat&quot;
$ ./fetch http://localhost:8000/help
no such page: /help
</code></pre>
<p>显然我们可以继续向ServeHTTP方法中添加case但在一个实际的应用中将每个case中的逻辑定义到一个分开的方法或函数中会很实用。此外相近的URL可能需要相似的逻辑例如几个图片文件可能有形如/images/*.png的URL。因为这些原因net/http包提供了一个请求多路器ServeMux来简化URL和handlers的联系。一个ServeMux将一批http.Handler聚集到一个单一的http.Handler中。再一次我们可以看到满足同一接口的不同类型是可替换的web服务器将请求指派给任意的http.Handler
而不需要考虑它后面的具体类型。</p>
<p>对于更复杂的应用一些ServeMux可以通过组合来处理更加错综复杂的路由需求。Go语言目前没有一个权威的web框架就像Ruby语言有Rails和python有Django。这并不是说这样的框架不存在而是Go语言标准库中的构建模块就已经非常灵活以至于这些框架都是不必要的。此外尽管在一个项目早期使用框架是非常方便的但是它们带来额外的复杂度会使长期的维护更加困难。</p>
<p>在下面的程序中我们创建一个ServeMux并且使用它将URL和相应处理/list和/price操作的handler联系起来这些操作逻辑都已经被分到不同的方法中。然后我们在调用ListenAndServe函数中使用ServeMux为主要的handler。</p>
<p><u><i>gopl.io/ch7/http3</i></u></p>
<pre><code class="language-go">func main() {
db := database{&quot;shoes&quot;: 50, &quot;socks&quot;: 5}
mux := http.NewServeMux()
mux.Handle(&quot;/list&quot;, http.HandlerFunc(db.list))
mux.Handle(&quot;/price&quot;, http.HandlerFunc(db.price))
log.Fatal(http.ListenAndServe(&quot;localhost:8000&quot;, mux))
}
type database map[string]dollars
func (db database) list(w http.ResponseWriter, req *http.Request) {
for item, price := range db {
fmt.Fprintf(w, &quot;%s: %s\n&quot;, item, price)
}
}
func (db database) price(w http.ResponseWriter, req *http.Request) {
item := req.URL.Query().Get(&quot;item&quot;)
price, ok := db[item]
if !ok {
w.WriteHeader(http.StatusNotFound) // 404
fmt.Fprintf(w, &quot;no such item: %q\n&quot;, item)
return
}
fmt.Fprintf(w, &quot;%s\n&quot;, price)
}
</code></pre>
<p>让我们关注这两个注册到handlers上的调用。第一个db.list是一个方法值§6.4),它是下面这个类型的值。</p>
<pre><code class="language-go">func(w http.ResponseWriter, req *http.Request)
</code></pre>
<p>也就是说db.list的调用会援引一个接收者是db的database.list方法。所以db.list是一个实现了handler类似行为的函数但是因为它没有方法理解该方法没有它自己的方法所以它不满足http.Handler接口并且不能直接传给mux.Handle。</p>
<p>语句http.HandlerFunc(db.list)是一个转换而非一个函数调用因为http.HandlerFunc是一个类型。它有如下的定义</p>
<p><u><i>net/http</i></u></p>
<pre><code class="language-go">package http
type HandlerFunc func(w ResponseWriter, r *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
</code></pre>
<p>HandlerFunc显示了在Go语言接口机制中一些不同寻常的特点。这是一个实现了接口http.Handler的方法的函数类型。ServeHTTP方法的行为是调用了它的函数本身。因此HandlerFunc是一个让函数值满足一个接口的适配器这里函数和这个接口仅有的方法有相同的函数签名。实际上这个技巧让一个单一的类型例如database以多种方式满足http.Handler接口一种通过它的list方法一种通过它的price方法等等。</p>
<p>因为handler通过这种方式注册非常普遍ServeMux有一个方便的HandleFunc方法它帮我们简化handler注册代码成这样</p>
<p><u><i>gopl.io/ch7/http3a</i></u></p>
<pre><code class="language-go">mux.HandleFunc(&quot;/list&quot;, db.list)
mux.HandleFunc(&quot;/price&quot;, db.price)
</code></pre>
<p>从上面的代码很容易看出应该怎么构建一个程序由两个不同的web服务器监听不同的端口并且定义不同的URL将它们指派到不同的handler。我们只要构建另外一个ServeMux并且再调用一次ListenAndServe可能并行的。但是在大多数程序中一个web服务器就足够了。此外在一个应用程序的多个文件中定义HTTP handler也是非常典型的如果它们必须全部都显式地注册到这个应用的ServeMux实例上会比较麻烦。</p>
<p>所以为了方便net/http包提供了一个全局的ServeMux实例DefaultServerMux和包级别的http.Handle和http.HandleFunc函数。现在为了使用DefaultServeMux作为服务器的主handler我们不需要将它传给ListenAndServe函数nil值就可以工作。</p>
<p>然后服务器的主函数可以简化成:</p>
<p><u><i>gopl.io/ch7/http4</i></u></p>
<pre><code class="language-go">func main() {
db := database{&quot;shoes&quot;: 50, &quot;socks&quot;: 5}
http.HandleFunc(&quot;/list&quot;, db.list)
http.HandleFunc(&quot;/price&quot;, db.price)
log.Fatal(http.ListenAndServe(&quot;localhost:8000&quot;, nil))
}
</code></pre>
<p>最后一个重要的提示就像我们在1.7节中提到的web服务器在一个新的协程中调用每一个handler所以当handler获取其它协程或者这个handler本身的其它请求也可以访问到变量时一定要使用预防措施比如锁机制。我们后面的两章中将讲到并发相关的知识。</p>
<p><strong>练习 7.11</strong> 增加额外的handler让客户端可以创建读取更新和删除数据库记录。例如一个形如 <code>/update?item=socks&amp;price=6</code> 的请求会更新库存清单里一个货品的价格并且当这个货品不存在或价格无效时返回一个错误值。(注意:这个修改会引入变量同时更新的问题)</p>
<p><strong>练习 7.12</strong> 修改/list的handler让它把输出打印成一个HTML的表格而不是文本。html/template包§4.6)可能会对你有帮助。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="78-error接口"><a class="header" href="#78-error接口">7.8. error接口</a></h2>
<p>从本书的开始我们就已经创建和使用过神秘的预定义error类型而且没有解释它究竟是什么。实际上它就是interface类型这个类型有一个返回错误信息的单一方法</p>
<pre><code class="language-go">type error interface {
Error() string
}
</code></pre>
<p>创建一个error最简单的方法就是调用errors.New函数它会根据传入的错误信息返回一个新的error。整个errors包仅只有4行</p>
<pre><code class="language-go">package errors
func New(text string) error { return &amp;errorString{text} }
type errorString struct { text string }
func (e *errorString) Error() string { return e.text }
</code></pre>
<p>承载errorString的类型是一个结构体而非一个字符串这是为了保护它表示的错误避免粗心或有意的更新。并且因为是指针类型<code>*errorString</code>满足error接口而非errorString类型所以每个New函数的调用都分配了一个独特的和其他错误不相同的实例。我们也不想要重要的error例如io.EOF和一个刚好有相同错误消息的error比较后相等。</p>
<pre><code class="language-go">fmt.Println(errors.New(&quot;EOF&quot;) == errors.New(&quot;EOF&quot;)) // &quot;false&quot;
</code></pre>
<p>调用errors.New函数是非常稀少的因为有一个方便的封装函数fmt.Errorf它还会处理字符串格式化。我们曾多次在第5章中用到它。</p>
<pre><code class="language-go">package fmt
import &quot;errors&quot;
func Errorf(format string, args ...interface{}) error {
return errors.New(Sprintf(format, args...))
}
</code></pre>
<p>虽然<code>*errorString</code>可能是最简单的错误类型但远非只有它一个。例如syscall包提供了Go语言底层系统调用API。在多个平台上它定义一个实现error接口的数字类型Errno并且在Unix平台上Errno的Error方法会从一个字符串表中查找错误消息如下面展示的这样</p>
<pre><code class="language-go">package syscall
type Errno uintptr // operating system error code
var errors = [...]string{
1: &quot;operation not permitted&quot;, // EPERM
2: &quot;no such file or directory&quot;, // ENOENT
3: &quot;no such process&quot;, // ESRCH
// ...
}
func (e Errno) Error() string {
if 0 &lt;= int(e) &amp;&amp; int(e) &lt; len(errors) {
return errors[e]
}
return fmt.Sprintf(&quot;errno %d&quot;, e)
}
</code></pre>
<p>下面的语句创建了一个持有Errno值为2的接口值表示POSIX ENOENT状况</p>
<pre><code class="language-go">var err error = syscall.Errno(2)
fmt.Println(err.Error()) // &quot;no such file or directory&quot;
fmt.Println(err) // &quot;no such file or directory&quot;
</code></pre>
<p>err的值图形化的呈现在图7.6中。</p>
<p><img src="ch7/../images/ch7-06.png" alt="" /></p>
<p>Errno是一个系统调用错误的高效表示方式它通过一个有限的集合进行描述并且它满足标准的错误接口。我们会在第7.11节了解到其它满足这个接口的类型。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="79-示例-表达式求值"><a class="header" href="#79-示例-表达式求值">7.9. 示例: 表达式求值</a></h2>
<p>在本节中我们会构建一个简单算术表达式的求值器。我们将使用一个接口Expr来表示Go语言中任意的表达式。现在这个接口不需要有方法但是我们后面会为它增加一些。</p>
<pre><code class="language-go">// An Expr is an arithmetic expression.
type Expr interface{}
</code></pre>
<p>我们的表达式语言包括浮点数符号(小数点);二元操作符+-* 和/;一元操作符-x和+x调用pow(x,y)sin(x)和sqrt(x)的函数例如x和pi的变量当然也有括号和标准的优先级运算符。所有的值都是float64类型。这下面是一些表达式的例子</p>
<pre><code class="language-go">sqrt(A / pi)
pow(x, 3) + pow(y, 3)
(F - 32) * 5 / 9
</code></pre>
<p>下面的五个具体类型表示了具体的表达式类型。Var类型表示对一个变量的引用。我们很快会知道为什么它可以被输出。literal类型表示一个浮点型常量。unary和binary类型表示有一到两个运算对象的运算符表达式这些操作数可以是任意的Expr类型。call类型表示对一个函数的调用我们限制它的fn字段只能是powsin或者sqrt。</p>
<p><u><i>gopl.io/ch7/eval</i></u></p>
<pre><code class="language-go">// A Var identifies a variable, e.g., x.
type Var string
// A literal is a numeric constant, e.g., 3.141.
type literal float64
// A unary represents a unary operator expression, e.g., -x.
type unary struct {
op rune // one of '+', '-'
x Expr
}
// A binary represents a binary operator expression, e.g., x+y.
type binary struct {
op rune // one of '+', '-', '*', '/'
x, y Expr
}
// A call represents a function call expression, e.g., sin(x).
type call struct {
fn string // one of &quot;pow&quot;, &quot;sin&quot;, &quot;sqrt&quot;
args []Expr
}
</code></pre>
<p>为了计算一个包含变量的表达式我们需要一个environment变量将变量的名字映射成对应的值</p>
<pre><code class="language-go">type Env map[Var]float64
</code></pre>
<p>我们也需要每个表达式去定义一个Eval方法这个方法会根据给定的environment变量返回表达式的值。因为每个表达式都必须提供这个方法我们将它加入到Expr接口中。这个包只会对外公开ExprEnv和Var类型。调用方不需要获取其它的表达式类型就可以使用这个求值器。</p>
<pre><code class="language-go">type Expr interface {
// Eval returns the value of this Expr in the environment env.
Eval(env Env) float64
}
</code></pre>
<p>下面给大家展示一个具体的Eval方法。Var类型的这个方法对一个environment变量进行查找如果这个变量没有在environment中定义过这个方法会返回一个零值literal类型的这个方法简单的返回它真实的值。</p>
<pre><code class="language-go">func (v Var) Eval(env Env) float64 {
return env[v]
}
func (l literal) Eval(_ Env) float64 {
return float64(l)
}
</code></pre>
<p>unary和binary的Eval方法会递归的计算它的运算对象然后将运算符op作用到它们上。我们不将被零或无穷数除作为一个错误因为它们都会产生一个固定的结果——无限。最后call的这个方法会计算对于powsin或者sqrt函数的参数值然后调用对应在math包中的函数。</p>
<pre><code class="language-go">func (u unary) Eval(env Env) float64 {
switch u.op {
case '+':
return +u.x.Eval(env)
case '-':
return -u.x.Eval(env)
}
panic(fmt.Sprintf(&quot;unsupported unary operator: %q&quot;, u.op))
}
func (b binary) Eval(env Env) float64 {
switch b.op {
case '+':
return b.x.Eval(env) + b.y.Eval(env)
case '-':
return b.x.Eval(env) - b.y.Eval(env)
case '*':
return b.x.Eval(env) * b.y.Eval(env)
case '/':
return b.x.Eval(env) / b.y.Eval(env)
}
panic(fmt.Sprintf(&quot;unsupported binary operator: %q&quot;, b.op))
}
func (c call) Eval(env Env) float64 {
switch c.fn {
case &quot;pow&quot;:
return math.Pow(c.args[0].Eval(env), c.args[1].Eval(env))
case &quot;sin&quot;:
return math.Sin(c.args[0].Eval(env))
case &quot;sqrt&quot;:
return math.Sqrt(c.args[0].Eval(env))
}
panic(fmt.Sprintf(&quot;unsupported function call: %s&quot;, c.fn))
}
</code></pre>
<p>一些方法会失败。例如一个call表达式可能有未知的函数或者错误的参数个数。用一个无效的运算符如!或者&lt;去构建一个unary或者binary表达式也是可能会发生的尽管下面提到的Parse函数不会这样做。这些错误会让Eval方法panic。其它的错误像计算一个没有在environment变量中出现过的Var只会让Eval方法返回一个错误的结果。所有的这些错误都可以通过在计算前检查Expr来发现。这是我们接下来要讲的Check方法的工作但是让我们先测试Eval方法。</p>
<p>下面的TestEval函数是对evaluator的一个测试。它使用了我们会在第11章讲解的testing包但是现在知道调用t.Errof会报告一个错误就足够了。这个函数循环遍历一个表格中的输入这个表格中定义了三个表达式和针对每个表达式不同的环境变量。第一个表达式根据给定圆的面积A计算它的半径第二个表达式通过两个变量x和y计算两个立方体的体积之和第三个表达式将华氏温度F转换成摄氏度。</p>
<pre><code class="language-go">func TestEval(t *testing.T) {
tests := []struct {
expr string
env Env
want string
}{
{&quot;sqrt(A / pi)&quot;, Env{&quot;A&quot;: 87616, &quot;pi&quot;: math.Pi}, &quot;167&quot;},
{&quot;pow(x, 3) + pow(y, 3)&quot;, Env{&quot;x&quot;: 12, &quot;y&quot;: 1}, &quot;1729&quot;},
{&quot;pow(x, 3) + pow(y, 3)&quot;, Env{&quot;x&quot;: 9, &quot;y&quot;: 10}, &quot;1729&quot;},
{&quot;5 / 9 * (F - 32)&quot;, Env{&quot;F&quot;: -40}, &quot;-40&quot;},
{&quot;5 / 9 * (F - 32)&quot;, Env{&quot;F&quot;: 32}, &quot;0&quot;},
{&quot;5 / 9 * (F - 32)&quot;, Env{&quot;F&quot;: 212}, &quot;100&quot;},
}
var prevExpr string
for _, test := range tests {
// Print expr only when it changes.
if test.expr != prevExpr {
fmt.Printf(&quot;\n%s\n&quot;, test.expr)
prevExpr = test.expr
}
expr, err := Parse(test.expr)
if err != nil {
t.Error(err) // parse error
continue
}
got := fmt.Sprintf(&quot;%.6g&quot;, expr.Eval(test.env))
fmt.Printf(&quot;\t%v =&gt; %s\n&quot;, test.env, got)
if got != test.want {
t.Errorf(&quot;%s.Eval() in %v = %q, want %q\n&quot;,
test.expr, test.env, got, test.want)
}
}
}
</code></pre>
<p>对于表格中的每一条记录这个测试会解析它的表达式然后在环境变量中计算它输出结果。这里我们没有空间来展示Parse函数但是如果你使用go get下载这个包你就可以看到这个函数。</p>
<p>go test(§11.1) 命令会运行一个包的测试用例:</p>
<pre><code>$ go test -v gopl.io/ch7/eval
</code></pre>
<p>这个-v标识可以让我们看到测试用例打印的输出正常情况下像这样一个成功的测试用例会阻止打印结果的输出。这里是测试用例里fmt.Printf语句的输出</p>
<pre><code>sqrt(A / pi)
map[A:87616 pi:3.141592653589793] =&gt; 167
pow(x, 3) + pow(y, 3)
map[x:12 y:1] =&gt; 1729
map[x:9 y:10] =&gt; 1729
5 / 9 * (F - 32)
map[F:-40] =&gt; -40
map[F:32] =&gt; 0
map[F:212] =&gt; 100
</code></pre>
<p>幸运的是目前为止所有的输入都是适合的格式,但是我们的运气不可能一直都有。甚至在解释型语言中,为了静态错误检查语法是非常常见的;静态错误就是不用运行程序就可以检测出来的错误。通过将静态检查和动态的部分分开,我们可以快速的检查错误并且对于多次检查只执行一次而不是每次表达式计算的时候都进行检查。</p>
<p>让我们往Expr接口中增加另一个方法。Check方法对一个表达式语义树检查出静态错误。我们马上会说明它的vars参数。</p>
<pre><code class="language-go">type Expr interface {
Eval(env Env) float64
// Check reports errors in this Expr and adds its Vars to the set.
Check(vars map[Var]bool) error
}
</code></pre>
<p>具体的Check方法展示在下面。literal和Var类型的计算不可能失败所以这些类型的Check方法会返回一个nil值。对于unary和binary的Check方法会首先检查操作符是否有效然后递归的检查运算单元。相似地对于call的这个方法首先检查调用的函数是否已知并且有没有正确个数的参数然后递归的检查每一个参数。</p>
<pre><code class="language-go">func (v Var) Check(vars map[Var]bool) error {
vars[v] = true
return nil
}
func (literal) Check(vars map[Var]bool) error {
return nil
}
func (u unary) Check(vars map[Var]bool) error {
if !strings.ContainsRune(&quot;+-&quot;, u.op) {
return fmt.Errorf(&quot;unexpected unary op %q&quot;, u.op)
}
return u.x.Check(vars)
}
func (b binary) Check(vars map[Var]bool) error {
if !strings.ContainsRune(&quot;+-*/&quot;, b.op) {
return fmt.Errorf(&quot;unexpected binary op %q&quot;, b.op)
}
if err := b.x.Check(vars); err != nil {
return err
}
return b.y.Check(vars)
}
func (c call) Check(vars map[Var]bool) error {
arity, ok := numParams[c.fn]
if !ok {
return fmt.Errorf(&quot;unknown function %q&quot;, c.fn)
}
if len(c.args) != arity {
return fmt.Errorf(&quot;call to %s has %d args, want %d&quot;,
c.fn, len(c.args), arity)
}
for _, arg := range c.args {
if err := arg.Check(vars); err != nil {
return err
}
}
return nil
}
var numParams = map[string]int{&quot;pow&quot;: 2, &quot;sin&quot;: 1, &quot;sqrt&quot;: 1}
</code></pre>
<p>我们在两个组中有选择地列出有问题的输入和它们得出的错误。Parse函数这里没有出现会报出一个语法错误和Check函数会报出语义错误。</p>
<pre><code>x % 2 unexpected '%'
math.Pi unexpected '.'
!true unexpected '!'
&quot;hello&quot; unexpected '&quot;'
log(10) unknown function &quot;log&quot;
sqrt(1, 2) call to sqrt has 2 args, want 1
</code></pre>
<p>Check方法的参数是一个Var类型的集合这个集合聚集从表达式中找到的变量名。为了保证成功的计算这些变量中的每一个都必须出现在环境变量中。从逻辑上讲这个集合就是调用Check方法返回的结果但是因为这个方法是递归调用的所以对于Check方法填充结果到一个作为参数传入的集合中会更加的方便。调用方在初始调用时必须提供一个空的集合。</p>
<p>在第3.2节中我们绘制了一个在编译期才确定的函数f(x,y)。现在我们可以解析检查和计算在字符串中的表达式我们可以构建一个在运行时从客户端接收表达式的web应用并且它会绘制这个函数的表示的曲面。我们可以使用集合vars来检查表达式是否是一个只有两个变量x和y的函数——实际上是3个因为我们为了方便会提供半径大小r。并且我们会在计算前使用Check方法拒绝有格式问题的表达式这样我们就不会在下面函数的40000个计算过程100x100个栅格每一个有4个角重复这些检查。</p>
<p>这个ParseAndCheck函数混合了解析和检查步骤的过程</p>
<p><u><i>gopl.io/ch7/surface</i></u></p>
<pre><code class="language-go">import &quot;gopl.io/ch7/eval&quot;
func parseAndCheck(s string) (eval.Expr, error) {
if s == &quot;&quot; {
return nil, fmt.Errorf(&quot;empty expression&quot;)
}
expr, err := eval.Parse(s)
if err != nil {
return nil, err
}
vars := make(map[eval.Var]bool)
if err := expr.Check(vars); err != nil {
return nil, err
}
for v := range vars {
if v != &quot;x&quot; &amp;&amp; v != &quot;y&quot; &amp;&amp; v != &quot;r&quot; {
return nil, fmt.Errorf(&quot;undefined variable: %s&quot;, v)
}
}
return expr, nil
}
</code></pre>
<p>为了编写这个web应用所有我们需要做的就是下面这个plot函数这个函数有和http.HandlerFunc相似的签名</p>
<pre><code class="language-go">func plot(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
expr, err := parseAndCheck(r.Form.Get(&quot;expr&quot;))
if err != nil {
http.Error(w, &quot;bad expr: &quot;+err.Error(), http.StatusBadRequest)
return
}
w.Header().Set(&quot;Content-Type&quot;, &quot;image/svg+xml&quot;)
surface(w, func(x, y float64) float64 {
r := math.Hypot(x, y) // distance from (0,0)
return expr.Eval(eval.Env{&quot;x&quot;: x, &quot;y&quot;: y, &quot;r&quot;: r})
})
}
</code></pre>
<p><img src="ch7/../images/ch7-07.png" alt="" /></p>
<p>这个plot函数解析和检查在HTTP请求中指定的表达式并且用它来创建一个两个变量的匿名函数。这个匿名函数和来自原来surface-plotting程序中的固定函数f有相同的签名但是它计算一个用户提供的表达式。环境变量中定义了xy和半径r。最后plot调用surface函数它就是gopl.io/ch3/surface中的主要函数修改后它可以接受plot中的函数和输出io.Writer作为参数而不是使用固定的函数f和os.Stdout。图7.7中显示了通过程序产生的3个曲面。</p>
<p><strong>练习 7.13</strong> 为Expr增加一个String方法来打印美观的语法树。当再一次解析的时候检查它的结果是否生成相同的语法树。</p>
<p><strong>练习 7.14</strong> 定义一个新的满足Expr接口的具体类型并且提供一个新的操作例如对它运算单元中的最小值的计算。因为Parse函数不会创建这个新类型的实例为了使用它你可能需要直接构造一个语法树或者继承parser接口</p>
<p><strong>练习 7.15</strong> 编写一个从标准输入中读取一个单一表达式的程序,用户及时地提供对于任意变量的值,然后在结果环境变量中计算表达式的值。优雅的处理所有遇到的错误。</p>
<p><strong>练习 7.16</strong> 编写一个基于web的计算器程序。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="710-类型断言"><a class="header" href="#710-类型断言">7.10. 类型断言</a></h2>
<p>类型断言是一个使用在接口值上的操作。语法上它看起来像x.(T)被称为断言类型这里x表示一个接口的类型和T表示一个类型。一个类型断言检查它操作对象的动态类型是否和断言的类型匹配。</p>
<p>这里有两种可能。第一种如果断言的类型T是一个具体类型然后类型断言检查x的动态类型是否和T相同。如果这个检查成功了类型断言的结果是x的动态值当然它的类型是T。换句话说具体类型的类型断言从它的操作对象中获得具体的值。如果检查失败接下来这个操作会抛出panic。例如</p>
<pre><code class="language-go">var w io.Writer
w = os.Stdout
f := w.(*os.File) // success: f == os.Stdout
c := w.(*bytes.Buffer) // panic: interface holds *os.File, not *bytes.Buffer
</code></pre>
<p>第二种如果相反地断言的类型T是一个接口类型然后类型断言检查是否x的动态类型满足T。如果这个检查成功了动态值没有获取到这个结果仍然是一个有相同动态类型和值部分的接口值但是结果为类型T。换句话说对一个接口类型的类型断言改变了类型的表述方式改变了可以获取的方法集合通常更大但是它保留了接口值内部的动态类型和值的部分。</p>
<p>在下面的第一个类型断言后w和rw都持有os.Stdout因此它们都有一个动态类型<code>*os.File</code>但是变量w是一个io.Writer类型只对外公开了文件的Write方法而rw变量还公开了它的Read方法。</p>
<pre><code class="language-go">var w io.Writer
w = os.Stdout
rw := w.(io.ReadWriter) // success: *os.File has both Read and Write
w = new(ByteCounter)
rw = w.(io.ReadWriter) // panic: *ByteCounter has no Read method
</code></pre>
<p>如果断言操作的对象是一个nil接口值那么不论被断言的类型是什么这个类型断言都会失败。我们几乎不需要对一个更少限制性的接口类型更少的方法集合做断言因为它表现的就像是赋值操作一样除了对于nil接口值的情况。</p>
<pre><code class="language-go">w = rw // io.ReadWriter is assignable to io.Writer
w = rw.(io.Writer) // fails only if rw == nil
</code></pre>
<p>经常地对一个接口值的动态类型我们是不确定的并且我们更愿意去检验它是否是一些特定的类型。如果类型断言出现在一个预期有两个结果的赋值操作中例如如下的定义这个操作不会在失败的时候发生panic但是替代地返回一个额外的第二个结果这个结果是一个标识成功与否的布尔值</p>
<pre><code class="language-go">var w io.Writer = os.Stdout
f, ok := w.(*os.File) // success: ok, f == os.Stdout
b, ok := w.(*bytes.Buffer) // failure: !ok, b == nil
</code></pre>
<p>第二个结果通常赋值给一个命名为ok的变量。如果这个操作失败了那么ok就是false值第一个结果等于被断言类型的零值在这个例子中就是一个nil的<code>*bytes.Buffer</code>类型。</p>
<p>这个ok结果经常立即用于决定程序下面做什么。if语句的扩展格式让这个变的很简洁</p>
<pre><code class="language-go">if f, ok := w.(*os.File); ok {
// ...use f...
}
</code></pre>
<p>当类型断言的操作对象是一个变量你有时会看见原来的变量名重用而不是声明一个新的本地变量名这个重用的变量原来的值会被覆盖理解其实是声明了一个同名的新的本地变量外层原来的w不会被改变如下面这样</p>
<pre><code class="language-go">if w, ok := w.(*os.File); ok {
// ...use w...
}
</code></pre>
<div style="break-before: page; page-break-before: always;"></div><h2 id="711-基于类型断言区别错误类型"><a class="header" href="#711-基于类型断言区别错误类型">7.11. 基于类型断言区别错误类型</a></h2>
<p>思考在os包中文件操作返回的错误集合。I/O可以因为任何数量的原因失败但是有三种经常的错误必须进行不同的处理文件已经存在对于创建操作找不到文件对于读取操作和权限拒绝。os包中提供了三个帮助函数来对给定的错误值表示的失败进行分类</p>
<pre><code class="language-go">package os
func IsExist(err error) bool
func IsNotExist(err error) bool
func IsPermission(err error) bool
</code></pre>
<p>对这些判断的一个缺乏经验的实现可能会去检查错误消息是否包含了特定的子字符串,</p>
<pre><code class="language-go">func IsNotExist(err error) bool {
// NOTE: not robust!
return strings.Contains(err.Error(), &quot;file does not exist&quot;)
}
</code></pre>
<p>但是处理I/O错误的逻辑可能一个和另一个平台非常的不同所以这种方案并不健壮并且对相同的失败可能会报出各种不同的错误消息。在测试的过程中通过检查错误消息的子字符串来保证特定的函数以期望的方式失败是非常有用的但对于线上的代码是不够的。</p>
<p>一个更可靠的方式是使用一个专门的类型来描述结构化的错误。os包中定义了一个PathError类型来描述在文件路径操作中涉及到的失败像Open或者Delete操作并且定义了一个叫LinkError的变体来描述涉及到两个文件路径的操作像Symlink和Rename。这下面是os.PathError</p>
<pre><code class="language-go">package os
// PathError records an error and the operation and file path that caused it.
type PathError struct {
Op string
Path string
Err error
}
func (e *PathError) Error() string {
return e.Op + &quot; &quot; + e.Path + &quot;: &quot; + e.Err.Error()
}
</code></pre>
<p>大多数调用方都不知道PathError并且通过调用错误本身的Error方法来统一处理所有的错误。尽管PathError的Error方法简单地把这些字段连接起来生成错误消息PathError的结构保护了内部的错误组件。调用方需要使用类型断言来检测错误的具体类型以便将一种失败和另一种区分开具体的类型可以比字符串提供更多的细节。</p>
<pre><code class="language-go">_, err := os.Open(&quot;/no/such/file&quot;)
fmt.Println(err) // &quot;open /no/such/file: No such file or directory&quot;
fmt.Printf(&quot;%#v\n&quot;, err)
// Output:
// &amp;os.PathError{Op:&quot;open&quot;, Path:&quot;/no/such/file&quot;, Err:0x2}
</code></pre>
<p>这就是三个帮助函数是怎么工作的。例如下面展示的IsNotExist它会报出是否一个错误和syscall.ENOENT§7.8或者和有名的错误os.ErrNotExist相等可以在§5.4.2中找到io.EOF或者是一个<code>*PathError</code>它内部的错误是syscall.ENOENT和os.ErrNotExist其中之一。</p>
<pre><code class="language-go">import (
&quot;errors&quot;
&quot;syscall&quot;
)
var ErrNotExist = errors.New(&quot;file does not exist&quot;)
// IsNotExist returns a boolean indicating whether the error is known to
// report that a file or directory does not exist. It is satisfied by
// ErrNotExist as well as some syscall errors.
func IsNotExist(err error) bool {
if pe, ok := err.(*PathError); ok {
err = pe.Err
}
return err == syscall.ENOENT || err == ErrNotExist
}
</code></pre>
<p>下面这里是它的实际使用:</p>
<pre><code class="language-go">_, err := os.Open(&quot;/no/such/file&quot;)
fmt.Println(os.IsNotExist(err)) // &quot;true&quot;
</code></pre>
<p>如果错误消息结合成一个更大的字符串当然PathError的结构就不再为人所知例如通过一个对fmt.Errorf函数的调用。区别错误通常必须在失败操作后错误传回调用者前进行。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="712-通过类型断言询问行为"><a class="header" href="#712-通过类型断言询问行为">7.12. 通过类型断言询问行为</a></h2>
<p>下面这段逻辑和net/http包中web服务器负责写入HTTP头字段例如&quot;Content-type:text/html&quot;的部分相似。io.Writer接口类型的变量w代表HTTP响应写入它的字节最终被发送到某个人的web浏览器上。</p>
<pre><code class="language-go">func writeHeader(w io.Writer, contentType string) error {
if _, err := w.Write([]byte(&quot;Content-Type: &quot;)); err != nil {
return err
}
if _, err := w.Write([]byte(contentType)); err != nil {
return err
}
// ...
}
</code></pre>
<p>因为Write方法需要传入一个byte切片而我们希望写入的值是一个字符串所以我们需要使用[]byte(...)进行转换。这个转换分配内存并且做一个拷贝但是这个拷贝在转换后几乎立马就被丢弃掉。让我们假装这是一个web服务器的核心部分并且我们的性能分析表示这个内存分配使服务器的速度变慢。这里我们可以避免掉内存分配么</p>
<p>这个io.Writer接口告诉我们关于w持有的具体类型的唯一东西就是可以向它写入字节切片。如果我们回顾net/http包中的内幕我们知道在这个程序中的w变量持有的动态类型也有一个允许字符串高效写入的WriteString方法这个方法会避免去分配一个临时的拷贝。这可能像在黑夜中射击一样但是许多满足io.Writer接口的重要类型同时也有WriteString方法包括<code>*bytes.Buffer</code><code>*os.File</code><code>*bufio.Writer</code>。)</p>
<p>我们不能对任意io.Writer类型的变量w假设它也拥有WriteString方法。但是我们可以定义一个只有这个方法的新接口并且使用类型断言来检测是否w的动态类型满足这个新接口。</p>
<pre><code class="language-go">// writeString writes s to w.
// If w has a WriteString method, it is invoked instead of w.Write.
func writeString(w io.Writer, s string) (n int, err error) {
type stringWriter interface {
WriteString(string) (n int, err error)
}
if sw, ok := w.(stringWriter); ok {
return sw.WriteString(s) // avoid a copy
}
return w.Write([]byte(s)) // allocate temporary copy
}
func writeHeader(w io.Writer, contentType string) error {
if _, err := writeString(w, &quot;Content-Type: &quot;); err != nil {
return err
}
if _, err := writeString(w, contentType); err != nil {
return err
}
// ...
}
</code></pre>
<p>为了避免重复定义我们将这个检查移入到一个实用工具函数writeString中但是它太有用了以致于标准库将它作为io.WriteString函数提供。这是向一个io.Writer接口写入字符串的推荐方法。</p>
<p>这个例子的神奇之处在于没有定义了WriteString方法的标准接口也没有指定它是一个所需行为的标准接口。一个具体类型只会通过它的方法决定它是否满足stringWriter接口而不是任何它和这个接口类型所表达的关系。它的意思就是上面的技术依赖于一个假设这个假设就是如果一个类型满足下面的这个接口然后WriteString(s)方法就必须和Write([]byte(s))有相同的效果。</p>
<pre><code class="language-go">interface {
io.Writer
WriteString(s string) (n int, err error)
}
</code></pre>
<p>尽管io.WriteString实施了这个假设但是调用它的函数极少可能会去实施类似的假设。定义一个特定类型的方法隐式地获取了对特定行为的协约。对于Go语言的新手特别是那些来自有强类型语言使用背景的新手可能会发现它缺乏显式的意图令人感到混乱但是在实战的过程中这几乎不是一个问题。除了空接口interface{},接口类型很少意外巧合地被实现。</p>
<p>上面的writeString函数使用一个类型断言来获知一个普遍接口类型的值是否满足一个更加具体的接口类型并且如果满足它会使用这个更具体接口的行为。这个技术可以被很好的使用不论这个被询问的接口是一个标准如io.ReadWriter或者用户定义的如stringWriter接口。</p>
<p>这也是fmt.Fprintf函数怎么从其它所有值中区分满足error或者fmt.Stringer接口的值。在fmt.Fprintf内部有一个将单个操作对象转换成一个字符串的步骤像下面这样</p>
<pre><code class="language-go">package fmt
func formatOneValue(x interface{}) string {
if err, ok := x.(error); ok {
return err.Error()
}
if str, ok := x.(Stringer); ok {
return str.String()
}
// ...all other types...
}
</code></pre>
<p>如果x满足这两个接口类型中的一个具体满足的接口决定对值的格式化方式。如果都不满足默认的case或多或少会统一地使用反射来处理所有的其它类型我们可以在第12章知道具体是怎么实现的。</p>
<p>再一次的它假设任何有String方法的类型都满足fmt.Stringer中约定的行为这个行为会返回一个适合打印的字符串。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="713-类型分支"><a class="header" href="#713-类型分支">7.13. 类型分支</a></h2>
<p>接口被以两种不同的方式使用。在第一个方式中以io.Readerio.Writerfmt.Stringersort.Interfacehttp.Handler和error为典型一个接口的方法表达了实现这个接口的具体类型间的相似性但是隐藏了代码的细节和这些具体类型本身的操作。重点在于方法上而不是具体的类型上。</p>
<p>第二个方式是利用一个接口值可以持有各种具体类型值的能力将这个接口认为是这些类型的联合。类型断言用来动态地区别这些类型使得对每一种情况都不一样。在这个方式中重点在于具体的类型满足这个接口而不在于接口的方法如果它确实有一些的话并且没有任何的信息隐藏。我们将以这种方式使用的接口描述为discriminated unions可辨识联合</p>
<p>如果你熟悉面向对象编程你可能会将这两种方式当作是subtype polymorphism子类型多态和 ad hoc polymorphism非参数多态但是你不需要去记住这些术语。对于本章剩下的部分我们将会呈现一些第二种方式的例子。</p>
<p>和其它那些语言一样Go语言查询一个SQL数据库的API会干净地将查询中固定的部分和变化的部分分开。一个调用的例子可能看起来像这样</p>
<pre><code class="language-go">import &quot;database/sql&quot;
func listTracks(db sql.DB, artist string, minYear, maxYear int) {
result, err := db.Exec(
&quot;SELECT * FROM tracks WHERE artist = ? AND ? &lt;= year AND year &lt;= ?&quot;,
artist, minYear, maxYear)
// ...
}
</code></pre>
<p>Exec方法使用SQL字面量替换在查询字符串中的每个'?'SQL字面量表示相应参数的值它有可能是一个布尔值一个数字一个字符串或者nil空值。用这种方式构造查询可以帮助避免SQL注入攻击这种攻击就是对手可以通过利用输入内容中不正确的引号来控制查询语句。在Exec函数内部我们可能会找到像下面这样的一个函数它会将每一个参数值转换成它的SQL字面量符号。</p>
<pre><code class="language-go">func sqlQuote(x interface{}) string {
if x == nil {
return &quot;NULL&quot;
} else if _, ok := x.(int); ok {
return fmt.Sprintf(&quot;%d&quot;, x)
} else if _, ok := x.(uint); ok {
return fmt.Sprintf(&quot;%d&quot;, x)
} else if b, ok := x.(bool); ok {
if b {
return &quot;TRUE&quot;
}
return &quot;FALSE&quot;
} else if s, ok := x.(string); ok {
return sqlQuoteString(s) // (not shown)
} else {
panic(fmt.Sprintf(&quot;unexpected type %T: %v&quot;, x, x))
}
}
</code></pre>
<p>switch语句可以简化if-else链如果这个if-else链对一连串值做相等测试。一个相似的type switch类型分支可以简化类型断言的if-else链。</p>
<p>在最简单的形式中一个类型分支像普通的switch语句一样它的运算对象是x.(type)——它使用了关键词字面量type——并且每个case有一到多个类型。一个类型分支基于这个接口值的动态类型使一个多路分支有效。这个nil的case和if x == nil匹配并且这个default的case和如果其它case都不匹配的情况匹配。一个对sqlQuote的类型分支可能会有这些case</p>
<pre><code class="language-go">switch x.(type) {
case nil: // ...
case int, uint: // ...
case bool: // ...
case string: // ...
default: // ...
}
</code></pre>
<p>§1.8中的普通switch语句一样每一个case会被顺序的进行考虑并且当一个匹配找到时这个case中的内容会被执行。当一个或多个case类型是接口时case的顺序就会变得很重要因为可能会有两个case同时匹配的情况。default case相对其它case的位置是无所谓的。它不会允许落空发生。</p>
<p>注意到在原来的函数中对于bool和string情况的逻辑需要通过类型断言访问提取的值。因为这个做法很典型类型分支语句有一个扩展的形式它可以将提取的值绑定到一个在每个case范围内都有效的新变量。</p>
<pre><code class="language-go">switch x := x.(type) { /* ... */ }
</code></pre>
<p>这里我们已经将新的变量也命名为x和类型断言一样重用变量名是很常见的。和一个switch语句相似地一个类型分支隐式的创建了一个词法块因此新变量x的定义不会和外面块中的x变量冲突。每一个case也会隐式的创建一个单独的词法块。</p>
<p>使用类型分支的扩展形式来重写sqlQuote函数会让这个函数更加的清晰</p>
<pre><code class="language-go">func sqlQuote(x interface{}) string {
switch x := x.(type) {
case nil:
return &quot;NULL&quot;
case int, uint:
return fmt.Sprintf(&quot;%d&quot;, x) // x has type interface{} here.
case bool:
if x {
return &quot;TRUE&quot;
}
return &quot;FALSE&quot;
case string:
return sqlQuoteString(x) // (not shown)
default:
panic(fmt.Sprintf(&quot;unexpected type %T: %v&quot;, x, x))
}
}
</code></pre>
<p>在这个版本的函数中在每个单一类型的case内部变量x和这个case的类型相同。例如变量x在bool的case中是bool类型和string的case中是string类型。在所有其它的情况中变量x是switch运算对象的类型接口在这个例子中运算对象是一个interface{}。当多个case需要相同的操作时比如int和uint的情况类型分支可以很容易的合并这些情况。</p>
<p>尽管sqlQuote接受一个任意类型的参数但是这个函数只会在它的参数匹配类型分支中的一个case时运行到结束其它情况的它会panic出“unexpected type”消息。虽然x的类型是interface{}但是我们把它认为是一个intuintboolstring和nil值的discriminated union可识别联合</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="714-示例-基于标记的xml解码"><a class="header" href="#714-示例-基于标记的xml解码">7.14. 示例: 基于标记的XML解码</a></h2>
<p>第4.5章节展示了如何使用encoding/json包中的Marshal和Unmarshal函数来将JSON文档转换成Go语言的数据结构。encoding/xml包提供了一个相似的API。当我们想构造一个文档树的表示时使用encoding/xml包会很方便但是对于很多程序并不是必须的。encoding/xml包也提供了一个更低层的基于标记的API用于XML解码。在基于标记的样式中解析器消费输入并产生一个标记流四个主要的标记类型StartElementEndElementCharData和Comment每一个都是encoding/xml包中的具体类型。每一个对(*xml.Decoder).Token的调用都返回一个标记。</p>
<p>这里显示的是和这个API相关的部分</p>
<p><u><i>encoding/xml</i></u></p>
<pre><code class="language-go">package xml
type Name struct {
Local string // e.g., &quot;Title&quot; or &quot;id&quot;
}
type Attr struct { // e.g., name=&quot;value&quot;
Name Name
Value string
}
// A Token includes StartElement, EndElement, CharData,
// and Comment, plus a few esoteric types (not shown).
type Token interface{}
type StartElement struct { // e.g., &lt;name&gt;
Name Name
Attr []Attr
}
type EndElement struct { Name Name } // e.g., &lt;/name&gt;
type CharData []byte // e.g., &lt;p&gt;CharData&lt;/p&gt;
type Comment []byte // e.g., &lt;!-- Comment --&gt;
type Decoder struct{ /* ... */ }
func NewDecoder(io.Reader) *Decoder
func (*Decoder) Token() (Token, error) // returns next Token in sequence
</code></pre>
<p>这个没有方法的Token接口也是一个可识别联合的例子。传统的接口如io.Reader的目的是隐藏满足它的具体类型的细节这样就可以创造出新的实现在这个实现中每个具体类型都被统一地对待。相反满足可识别联合的具体类型的集合被设计为确定和暴露而不是隐藏。可识别联合的类型几乎没有方法操作它们的函数使用一个类型分支的case集合来进行表述这个case集合中每一个case都有不同的逻辑。</p>
<p>下面的xmlselect程序获取和打印在一个XML文档树中确定的元素下找到的文本。使用上面的API它可以在输入上一次完成它的工作而从来不要实例化这个文档树。</p>
<p><u><i>gopl.io/ch7/xmlselect</i></u></p>
<pre><code class="language-go">// Xmlselect prints the text of selected elements of an XML document.
package main
import (
&quot;encoding/xml&quot;
&quot;fmt&quot;
&quot;io&quot;
&quot;os&quot;
&quot;strings&quot;
)
func main() {
dec := xml.NewDecoder(os.Stdin)
var stack []string // stack of element names
for {
tok, err := dec.Token()
if err == io.EOF {
break
} else if err != nil {
fmt.Fprintf(os.Stderr, &quot;xmlselect: %v\n&quot;, err)
os.Exit(1)
}
switch tok := tok.(type) {
case xml.StartElement:
stack = append(stack, tok.Name.Local) // push
case xml.EndElement:
stack = stack[:len(stack)-1] // pop
case xml.CharData:
if containsAll(stack, os.Args[1:]) {
fmt.Printf(&quot;%s: %s\n&quot;, strings.Join(stack, &quot; &quot;), tok)
}
}
}
}
// containsAll reports whether x contains the elements of y, in order.
func containsAll(x, y []string) bool {
for len(y) &lt;= len(x) {
if len(y) == 0 {
return true
}
if x[0] == y[0] {
y = y[1:]
}
x = x[1:]
}
return false
}
</code></pre>
<p>main函数中的循环每遇到一个StartElement时它把这个元素的名称压到一个栈里并且每次遇到EndElement时它将名称从这个栈中推出。这个API保证了StartElement和EndElement的序列可以被完全的匹配甚至在一个糟糕的文档格式中。注释会被忽略。当xmlselect遇到一个CharData时只有当栈中有序地包含所有通过命令行参数传入的元素名称时它才会输出相应的文本。</p>
<p>下面的命令打印出任意出现在两层div元素下的h2元素的文本。它的输入是XML的说明文档并且它自己就是XML文档格式的。</p>
<pre><code>$ go build gopl.io/ch1/fetch
$ ./fetch http://www.w3.org/TR/2006/REC-xml11-20060816 |
./xmlselect div div h2
html body div div h2: 1 Introduction
html body div div h2: 2 Documents
html body div div h2: 3 Logical Structures
html body div div h2: 4 Physical Structures
html body div div h2: 5 Conformance
html body div div h2: 6 Notation
html body div div h2: A References
html body div div h2: B Definitions for Character Normalization
...
</code></pre>
<p><strong>练习 7.17</strong> 扩展xmlselect程序以便让元素不仅可以通过名称选择也可以通过它们CSS风格的属性进行选择。例如一个像这样</p>
<pre><code class="language-html">&lt;div id=&quot;page&quot; class=&quot;wide&quot;&gt;
</code></pre>
<p>的元素可以通过匹配id或者class同时还有它的名称来进行选择。</p>
<p><strong>练习 7.18</strong> 使用基于标记的解码API编写一个可以读取任意XML文档并构造这个文档所代表的通用节点树的程序。节点有两种类型CharData节点表示文本字符串和 Element节点表示被命名的元素和它们的属性。每一个元素节点有一个子节点的切片。</p>
<p>你可能发现下面的定义会对你有帮助。</p>
<pre><code class="language-go">import &quot;encoding/xml&quot;
type Node interface{} // CharData or *Element
type CharData string
type Element struct {
Type xml.Name
Attr []xml.Attr
Children []Node
}
</code></pre>
<div style="break-before: page; page-break-before: always;"></div><h2 id="715-一些建议"><a class="header" href="#715-一些建议">7.15. 一些建议</a></h2>
<p>当设计一个新的包时新手Go程序员总是先创建一套接口然后再定义一些满足它们的具体类型。这种方式的结果就是有很多的接口它们中的每一个仅只有一个实现。不要再这么做了。这种接口是不必要的抽象它们也有一个运行时损耗。你可以使用导出机制§6.6)来限制一个类型的方法或一个结构体的字段是否在包外可见。接口只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要。</p>
<p>当一个接口只被一个单一的具体类型实现时有一个例外,就是由于它的依赖,这个具体类型不能和这个接口存在在一个相同的包中。这种情况下,一个接口是解耦这两个包的一个好方式。</p>
<p>因为在Go语言中只有当两个或更多的类型实现一个接口时才使用接口它们必定会从任意特定的实现细节中抽象出来。结果就是有更少和更简单方法的更小的接口经常和io.Writer或 fmt.Stringer一样只有一个。当新的类型出现时小的接口更容易满足。对于接口设计的一个好的标准就是 ask only for what you need只考虑你需要的东西</p>
<p>我们完成了对方法和接口的学习过程。Go语言对面向对象风格的编程支持良好但这并不意味着你只能使用这一风格。不是任何事物都需要被当做一个对象独立的函数有它们自己的用处未封装的数据类型也是这样。观察一下在本书前五章的例子中像input.Scan这样的方法被调用不超过二十次与之相反的是普遍调用的函数如fmt.Printf。</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="第8章-goroutines和channels"><a class="header" href="#第8章-goroutines和channels">第8章 Goroutines和Channels</a></h1>
<p>并发程序指同时进行多个任务的程序随着硬件的发展并发程序变得越来越重要。Web服务器会一次处理成千上万的请求。平板电脑和手机app在渲染用户画面同时还会后台执行各种计算任务和网络请求。即使是传统的批处理问题——读取数据、计算、写输出现在也会用并发来隐藏掉I/O的操作延迟以充分利用现代计算机设备的多个核心。计算机的性能每年都在以非线性的速度增长。</p>
<p>Go语言中的并发程序可以用两种手段来实现。本章讲解goroutine和channel其支持“顺序通信进程”communicating sequential processes或被简称为CSP。CSP是一种现代的并发编程模型在这种编程模型中值会在不同的运行实例goroutine中传递尽管大多数情况下仍然是被限制在单一实例中。第9章覆盖更为传统的并发模型多线程共享内存如果你在其它的主流语言中写过并发程序的话可能会更熟悉一些。第9章也会深入介绍一些并发程序带来的风险和陷阱。</p>
<p>尽管Go对并发的支持是众多强力特性之一但跟踪调试并发程序还是很困难在线性程序中形成的直觉往往还会使我们误入歧途。如果这是读者第一次接触并发推荐稍微多花一些时间来思考这两个章节中的样例。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="81-goroutines"><a class="header" href="#81-goroutines">8.1. Goroutines</a></h2>
<p>在Go语言中每一个并发的执行单元叫作一个goroutine。设想这里的一个程序有两个函数一个函数做计算另一个输出结果假设两个函数没有相互之间的调用关系。一个线性的程序会先调用其中的一个函数然后再调用另一个。如果程序中包含多个goroutine对两个函数的调用则可能发生在同一时刻。马上就会看到这样的一个程序。</p>
<p>如果你使用过操作系统或者其它语言提供的线程那么你可以简单地把goroutine类比作一个线程这样你就可以写出一些正确的程序了。goroutine和线程的本质区别会在9.8节中讲。</p>
<p>当一个程序启动时其主函数即在一个单独的goroutine中运行我们叫它main goroutine。新的goroutine会用go语句来创建。在语法上go语句是一个普通的函数或方法调用前加上关键字go。go语句会使其语句中的函数在一个新创建的goroutine中运行。而go语句本身会迅速地完成。</p>
<pre><code class="language-go">f() // call f(); wait for it to return
go f() // create a new goroutine that calls f(); don't wait
</code></pre>
<p>下面的例子main goroutine将计算菲波那契数列的第45个元素值。由于计算函数使用低效的递归所以会运行相当长时间在此期间我们想让用户看到一个可见的标识来表明程序依然在正常运行所以来做一个动画的小图标</p>
<p><u><i>gopl.io/ch8/spinner</i></u></p>
<pre><code class="language-go">func main() {
go spinner(100 * time.Millisecond)
const n = 45
fibN := fib(n) // slow
fmt.Printf(&quot;\rFibonacci(%d) = %d\n&quot;, n, fibN)
}
func spinner(delay time.Duration) {
for {
for _, r := range `-\|/` {
fmt.Printf(&quot;\r%c&quot;, r)
time.Sleep(delay)
}
}
}
func fib(x int) int {
if x &lt; 2 {
return x
}
return fib(x-1) + fib(x-2)
}
</code></pre>
<p>动画显示了几秒之后fib(45)的调用成功地返回,并且打印结果:</p>
<pre><code>Fibonacci(45) = 1134903170
</code></pre>
<p>然后主函数返回。主函数返回时所有的goroutine都会被直接打断程序退出。除了从主函数退出或者直接终止程序之外没有其它的编程方法能够让一个goroutine来打断另一个的执行但是之后可以看到一种方式来实现这个目的通过goroutine之间的通信来让一个goroutine请求其它的goroutine并让被请求的goroutine自行结束执行。</p>
<p>留意一下这里的两个独立的单元是如何进行组合的spinning和菲波那契的计算。分别在独立的函数中但两个函数会同时执行。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="82-示例-并发的clock服务"><a class="header" href="#82-示例-并发的clock服务">8.2. 示例: 并发的Clock服务</a></h2>
<p>网络编程是并发大显身手的一个领域由于服务器是最典型的需要同时处理很多连接的程序这些连接一般来自于彼此独立的客户端。在本小节中我们会讲解go语言的net包这个包提供编写一个网络客户端或者服务器程序的基本组件无论两者间通信是使用TCP、UDP或者Unix domain sockets。在第一章中我们使用过的net/http包里的方法也算是net包的一部分。</p>
<p>我们的第一个例子是一个顺序执行的时钟服务器,它会每隔一秒钟将当前时间写到客户端:</p>
<p><u><i>gopl.io/ch8/clock1</i></u></p>
<pre><code class="language-go">// Clock1 is a TCP server that periodically writes the time.
package main
import (
&quot;io&quot;
&quot;log&quot;
&quot;net&quot;
&quot;time&quot;
)
func main() {
listener, err := net.Listen(&quot;tcp&quot;, &quot;localhost:8000&quot;)
if err != nil {
log.Fatal(err)
}
for {
conn, err := listener.Accept()
if err != nil {
log.Print(err) // e.g., connection aborted
continue
}
handleConn(conn) // handle one connection at a time
}
}
func handleConn(c net.Conn) {
defer c.Close()
for {
_, err := io.WriteString(c, time.Now().Format(&quot;15:04:05\n&quot;))
if err != nil {
return // e.g., client disconnected
}
time.Sleep(1 * time.Second)
}
}
</code></pre>
<p>Listen函数创建了一个net.Listener的对象这个对象会监听一个网络端口上到来的连接在这个例子里我们用的是TCP的localhost:8000端口。listener对象的Accept方法会直接阻塞直到一个新的连接被创建然后会返回一个net.Conn对象来表示这个连接。</p>
<p>handleConn函数会处理一个完整的客户端连接。在一个for死循环中用time.Now()获取当前时刻然后写到客户端。由于net.Conn实现了io.Writer接口我们可以直接向其写入内容。这个死循环会一直执行直到写入失败。最可能的原因是客户端主动断开连接。这种情况下handleConn函数会用defer调用关闭服务器侧的连接然后返回到主函数继续等待下一个连接请求。</p>
<p>time.Time.Format方法提供了一种格式化日期和时间信息的方式。它的参数是一个格式化模板标识如何来格式化时间而这个格式化模板限定为Mon Jan 2 03:04:05PM 2006 UTC-0700。有8个部分周几、月份、一个月的第几天……。可以以任意的形式来组合前面这个模板出现在模板中的部分会作为参考来对时间格式进行输出。在上面的例子中我们只用到了小时、分钟和秒。time包里定义了很多标准时间格式比如time.RFC1123。在进行格式化的逆向操作time.Parse时也会用到同样的策略。译注这是go语言和其它语言相比比较奇葩的一个地方。你需要记住格式化字符串是1月2日下午3点4分5秒零六年UTC-0700而不像其它语言那样Y-m-d H:i:s一样当然了这里可以用1234567的方式来记忆倒是也不麻烦。</p>
<p>为了连接例子里的服务器我们需要一个客户端程序比如netcat这个工具nc命令这个工具可以用来执行网络连接操作。</p>
<pre><code>$ go build gopl.io/ch8/clock1
$ ./clock1 &amp;
$ nc localhost 8000
13:58:54
13:58:55
13:58:56
13:58:57
^C
</code></pre>
<p>客户端将服务器发来的时间显示了出来我们用Control+C来中断客户端的执行在Unix系统上你会看到^C这样的响应。如果你的系统没有装nc这个工具你可以用telnet来实现同样的效果或者也可以用我们下面的这个用go写的简单的telnet程序用net.Dial就可以简单地创建一个TCP连接</p>
<p><u><i>gopl.io/ch8/netcat1</i></u></p>
<pre><code class="language-go">// Netcat1 is a read-only TCP client.
package main
import (
&quot;io&quot;
&quot;log&quot;
&quot;net&quot;
&quot;os&quot;
)
func main() {
conn, err := net.Dial(&quot;tcp&quot;, &quot;localhost:8000&quot;)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
mustCopy(os.Stdout, conn)
}
func mustCopy(dst io.Writer, src io.Reader) {
if _, err := io.Copy(dst, src); err != nil {
log.Fatal(err)
}
}
</code></pre>
<p>这个程序会从连接中读取数据并将读到的内容写到标准输出中直到遇到end of file的条件或者发生错误。mustCopy这个函数我们在本节的几个例子中都会用到。让我们同时运行两个客户端来进行一个测试这里可以开两个终端窗口下面左边的是其中的一个的输出右边的是另一个的输出</p>
<pre><code>$ go build gopl.io/ch8/netcat1
$ ./netcat1
13:58:54 $ ./netcat1
13:58:55
13:58:56
^C
13:58:57
13:58:58
13:58:59
^C
$ killall clock1
</code></pre>
<p>killall命令是一个Unix命令行工具可以用给定的进程名来杀掉所有名字匹配的进程。</p>
<p>第二个客户端必须等待第一个客户端完成工作这样服务端才能继续向后执行因为我们这里的服务器程序同一时间只能处理一个客户端连接。我们这里对服务端程序做一点小改动使其支持并发在handleConn函数调用的地方增加go关键字让每一次handleConn的调用都进入一个独立的goroutine。</p>
<p><u><i>gopl.io/ch8/clock2</i></u></p>
<pre><code class="language-go">for {
conn, err := listener.Accept()
if err != nil {
log.Print(err) // e.g., connection aborted
continue
}
go handleConn(conn) // handle connections concurrently
}
</code></pre>
<p>现在多个客户端可以同时接收到时间了:</p>
<pre><code>$ go build gopl.io/ch8/clock2
$ ./clock2 &amp;
$ go build gopl.io/ch8/netcat1
$ ./netcat1
14:02:54 $ ./netcat1
14:02:55 14:02:55
14:02:56 14:02:56
14:02:57 ^C
14:02:58
14:02:59 $ ./netcat1
14:03:00 14:03:00
14:03:01 14:03:01
^C 14:03:02
^C
$ killall clock2
</code></pre>
<p><strong>练习 8.1</strong> 修改clock2来支持传入参数作为端口号然后写一个clockwall的程序这个程序可以同时与多个clock服务器通信从多个服务器中读取时间并且在一个表格中一次显示所有服务器传回的结果类似于你在某些办公室里看到的时钟墙。如果你有地理学上分布式的服务器可以用的话让这些服务器跑在不同的机器上面或者在同一台机器上跑多个不同的实例这些实例监听不同的端口假装自己在不同的时区。像下面这样</p>
<pre><code>$ TZ=US/Eastern ./clock2 -port 8010 &amp;
$ TZ=Asia/Tokyo ./clock2 -port 8020 &amp;
$ TZ=Europe/London ./clock2 -port 8030 &amp;
$ clockwall NewYork=localhost:8010 Tokyo=localhost:8020 London=localhost:8030
</code></pre>
<p><strong>练习 8.2</strong> 实现一个并发FTP服务器。服务器应该解析客户端发来的一些命令比如cd命令来切换目录ls来列出目录内文件get和send来传输文件close来关闭连接。你可以用标准的ftp命令来作为客户端或者也可以自己实现一个。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="83-示例-并发的echo服务"><a class="header" href="#83-示例-并发的echo服务">8.3. 示例: 并发的Echo服务</a></h2>
<p>clock服务器每一个连接都会起一个goroutine。在本节中我们会创建一个echo服务器这个服务在每个连接中会有多个goroutine。大多数echo服务仅仅会返回他们读取到的内容就像下面这个简单的handleConn函数所做的一样</p>
<pre><code class="language-go">func handleConn(c net.Conn) {
io.Copy(c, c) // NOTE: ignoring errors
c.Close()
}
</code></pre>
<p>一个更有意思的echo服务应该模拟一个实际的echo的“回响”并且一开始要用大写HELLO来表示“声音很大”之后经过一小段延迟返回一个有所缓和的Hello然后一个全小写字母的hello表示声音渐渐变小直至消失像下面这个版本的handleConn(译注:笑看作者脑洞大开)</p>
<p><u><i>gopl.io/ch8/reverb1</i></u></p>
<pre><code class="language-go">func echo(c net.Conn, shout string, delay time.Duration) {
fmt.Fprintln(c, &quot;\t&quot;, strings.ToUpper(shout))
time.Sleep(delay)
fmt.Fprintln(c, &quot;\t&quot;, shout)
time.Sleep(delay)
fmt.Fprintln(c, &quot;\t&quot;, strings.ToLower(shout))
}
func handleConn(c net.Conn) {
input := bufio.NewScanner(c)
for input.Scan() {
echo(c, input.Text(), 1*time.Second)
}
// NOTE: ignoring potential errors from input.Err()
c.Close()
}
</code></pre>
<p>我们需要升级我们的客户端程序,这样它就可以发送终端的输入到服务器,并把服务端的返回输出到终端上,这使我们有了使用并发的另一个好机会:</p>
<p><u><i>gopl.io/ch8/netcat2</i></u></p>
<pre><code class="language-go">func main() {
conn, err := net.Dial(&quot;tcp&quot;, &quot;localhost:8000&quot;)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
go mustCopy(os.Stdout, conn)
mustCopy(conn, os.Stdin)
}
</code></pre>
<p>当main goroutine从标准输入流中读取内容并将其发送给服务器时另一个goroutine会读取并打印服务端的响应。当main goroutine碰到输入终止时例如用户在终端中按了Control-D(^D)在windows上是Control-Z这时程序就会被终止尽管其它goroutine中还有进行中的任务。在8.4.1中引入了channels后我们会明白如何让程序等待两边都结束。</p>
<p>下面这个会话中,客户端的输入是左对齐的,服务端的响应会用缩进来区别显示。
客户端会向服务器“喊三次话”:</p>
<pre><code>$ go build gopl.io/ch8/reverb1
$ ./reverb1 &amp;
$ go build gopl.io/ch8/netcat2
$ ./netcat2
Hello?
HELLO?
Hello?
hello?
Is there anybody there?
IS THERE ANYBODY THERE?
Yooo-hooo!
Is there anybody there?
is there anybody there?
YOOO-HOOO!
Yooo-hooo!
yooo-hooo!
^D
$ killall reverb1
</code></pre>
<p>注意客户端的第三次shout在前一个shout处理完成之前一直没有被处理这貌似看起来不是特别“现实”。真实世界里的回响应该是会由三次shout的回声组合而成的。为了模拟真实世界的回响我们需要更多的goroutine来做这件事情。这样我们就再一次地需要go这个关键词了这次我们用它来调用echo</p>
<p><u><i>gopl.io/ch8/reverb2</i></u></p>
<pre><code class="language-go">func handleConn(c net.Conn) {
input := bufio.NewScanner(c)
for input.Scan() {
go echo(c, input.Text(), 1*time.Second)
}
// NOTE: ignoring potential errors from input.Err()
c.Close()
}
</code></pre>
<p>go后跟的函数的参数会在go语句自身执行时被求值因此input.Text()会在main goroutine中被求值。
现在回响是并发并且会按时间来覆盖掉其它响应了:</p>
<pre><code>$ go build gopl.io/ch8/reverb2
$ ./reverb2 &amp;
$ ./netcat2
Is there anybody there?
IS THERE ANYBODY THERE?
Yooo-hooo!
Is there anybody there?
YOOO-HOOO!
is there anybody there?
Yooo-hooo!
yooo-hooo!
^D
$ killall reverb2
</code></pre>
<p>让服务使用并发不只是处理多个客户端的请求甚至在处理单个连接时也可能会用到就像我们上面的两个go关键词的用法。然而在我们使用go关键词的同时需要慎重地考虑net.Conn中的方法在并发地调用时是否安全事实上对于大多数类型来说也确实不安全。我们会在下一章中详细地探讨并发安全性。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="84-channels"><a class="header" href="#84-channels">8.4. Channels</a></h2>
<p>如果说goroutine是Go语言程序的并发体的话那么channels则是它们之间的通信机制。一个channel是一个通信机制它可以让一个goroutine通过它给另一个goroutine发送值信息。每个channel都有一个特殊的类型也就是channels可发送数据的类型。一个可以发送int类型数据的channel一般写为chan int。</p>
<p>使用内置的make函数我们可以创建一个channel</p>
<pre><code class="language-Go">ch := make(chan int) // ch has type 'chan int'
</code></pre>
<p>和map类似channel也对应一个make创建的底层数据结构的引用。当我们复制一个channel或用于函数参数传递时我们只是拷贝了一个channel引用因此调用者和被调用者将引用同一个channel对象。和其它的引用类型一样channel的零值也是nil。</p>
<p>两个相同类型的channel可以使用==运算符比较。如果两个channel引用的是相同的对象那么比较的结果为真。一个channel也可以和nil进行比较。</p>
<p>一个channel有发送和接受两个主要操作都是通信行为。一个发送语句将一个值从一个goroutine通过channel发送到另一个执行接收操作的goroutine。发送和接收两个操作都使用<code>&lt;-</code>运算符。在发送语句中,<code>&lt;-</code>运算符分割channel和要发送的值。在接收语句中<code>&lt;-</code>运算符写在channel对象之前。一个不使用接收结果的接收操作也是合法的。</p>
<pre><code class="language-Go">ch &lt;- x // a send statement
x = &lt;-ch // a receive expression in an assignment statement
&lt;-ch // a receive statement; result is discarded
</code></pre>
<p>Channel还支持close操作用于关闭channel随后对基于该channel的任何发送操作都将导致panic异常。对一个已经被close过的channel进行接收操作依然可以接受到之前已经成功发送的数据如果channel中已经没有数据的话将产生一个零值的数据。</p>
<p>使用内置的close函数就可以关闭一个channel</p>
<pre><code class="language-Go">close(ch)
</code></pre>
<p>以最简单方式调用make函数创建的是一个无缓存的channel但是我们也可以指定第二个整型参数对应channel的容量。如果channel的容量大于零那么该channel就是带缓存的channel。</p>
<pre><code class="language-Go">ch = make(chan int) // unbuffered channel
ch = make(chan int, 0) // unbuffered channel
ch = make(chan int, 3) // buffered channel with capacity 3
</code></pre>
<p>我们将先讨论无缓存的channel然后在8.4.4节讨论带缓存的channel。</p>
<h3 id="841-不带缓存的channels"><a class="header" href="#841-不带缓存的channels">8.4.1. 不带缓存的Channels</a></h3>
<p>一个基于无缓存Channels的发送操作将导致发送者goroutine阻塞直到另一个goroutine在相同的Channels上执行接收操作当发送的值通过Channels成功传输之后两个goroutine可以继续执行后面的语句。反之如果接收操作先发生那么接收者goroutine也将阻塞直到有另一个goroutine在相同的Channels上执行发送操作。</p>
<p>基于无缓存Channels的发送和接收操作将导致两个goroutine做一次同步操作。因为这个原因无缓存Channels有时候也被称为同步Channels。当通过一个无缓存Channels发送数据时接收者收到数据发生在再次唤醒发送者goroutine之前译注<em>happens before</em>这是Go语言并发内存模型的一个关键术语</p>
<p>在讨论并发编程时当我们说x事件在y事件之前发生<em>happens before</em>我们并不是说x事件在时间上比y时间更早我们要表达的意思是要保证在此之前的事件都已经完成了例如在此之前的更新某些变量的操作已经完成你可以放心依赖这些已完成的事件了。</p>
<p>当我们说x事件既不是在y事件之前发生也不是在y事件之后发生我们就说x事件和y事件是并发的。这并不是意味着x事件和y事件就一定是同时发生的我们只是不能确定这两个事件发生的先后顺序。在下一章中我们将看到当两个goroutine并发访问了相同的变量时我们有必要保证某些事件的执行顺序以避免出现某些并发问题。</p>
<p>在8.3节的客户端程序它在主goroutine中译注就是执行main函数的goroutine将标准输入复制到server因此当客户端程序关闭标准输入时后台goroutine可能依然在工作。我们需要让主goroutine等待后台goroutine完成工作后再退出我们使用了一个channel来同步两个goroutine</p>
<p><u><i>gopl.io/ch8/netcat3</i></u></p>
<pre><code class="language-Go">func main() {
conn, err := net.Dial(&quot;tcp&quot;, &quot;localhost:8000&quot;)
if err != nil {
log.Fatal(err)
}
done := make(chan struct{})
go func() {
io.Copy(os.Stdout, conn) // NOTE: ignoring errors
log.Println(&quot;done&quot;)
done &lt;- struct{}{} // signal the main goroutine
}()
mustCopy(conn, os.Stdin)
conn.Close()
&lt;-done // wait for background goroutine to finish
}
</code></pre>
<p>当用户关闭了标准输入主goroutine中的mustCopy函数调用将返回然后调用conn.Close()关闭读和写方向的网络连接。关闭网络连接中的写方向的连接将导致server程序收到一个文件end-of-file结束的信号。关闭网络连接中读方向的连接将导致后台goroutine的io.Copy函数调用返回一个“read from closed connection”“从关闭的连接读”类似的错误因此我们临时移除了错误日志语句在练习8.3将会提供一个更好的解决方案。需要注意的是go语句调用了一个函数字面量这是Go语言中启动goroutine常用的形式。</p>
<p>在后台goroutine返回之前它先打印一个日志信息然后向done对应的channel发送一个值。主goroutine在退出前先等待从done对应的channel接收一个值。因此总是可以在程序退出前正确输出“done”消息。</p>
<p>基于channels发送消息有两个重要方面。首先每个消息都有一个值但是有时候通讯的事实和发生的时刻也同样重要。当我们更希望强调通讯发生的时刻时我们将它称为<strong>消息事件</strong>。有些消息事件并不携带额外的信息它仅仅是用作两个goroutine之间的同步这时候我们可以用<code>struct{}</code>空结构体作为channels元素的类型虽然也可以使用bool或int类型实现同样的功能<code>done &lt;- 1</code>语句也比<code>done &lt;- struct{}{}</code>更短。</p>
<p><strong>练习 8.3</strong> 在netcat3例子中conn虽然是一个interface类型的值但是其底层真实类型是<code>*net.TCPConn</code>代表一个TCP连接。一个TCP连接有读和写两个部分可以使用CloseRead和CloseWrite方法分别关闭它们。修改netcat3的主goroutine代码只关闭网络连接中写的部分这样的话后台goroutine可以在标准输入被关闭后继续打印从reverb1服务器传回的数据。要在reverb2服务器也完成同样的功能是比较困难的参考<strong>练习 8.4</strong>。)</p>
<h3 id="842-串联的channelspipeline"><a class="header" href="#842-串联的channelspipeline">8.4.2. 串联的ChannelsPipeline</a></h3>
<p>Channels也可以用于将多个goroutine连接在一起一个Channel的输出作为下一个Channel的输入。这种串联的Channels就是所谓的管道pipeline。下面的程序用两个channels将三个goroutine串联起来如图8.1所示。</p>
<p><img src="ch8/../images/ch8-01.png" alt="" /></p>
<p>第一个goroutine是一个计数器用于生成0、1、2、……形式的整数序列然后通过channel将该整数序列发送给第二个goroutine第二个goroutine是一个求平方的程序对收到的每个整数求平方然后将平方后的结果通过第二个channel发送给第三个goroutine第三个goroutine是一个打印程序打印收到的每个整数。为了保持例子清晰我们有意选择了非常简单的函数当然三个goroutine的计算很简单在现实中确实没有必要为如此简单的运算构建三个goroutine。</p>
<p><u><i>gopl.io/ch8/pipeline1</i></u></p>
<pre><code class="language-Go">func main() {
naturals := make(chan int)
squares := make(chan int)
// Counter
go func() {
for x := 0; ; x++ {
naturals &lt;- x
}
}()
// Squarer
go func() {
for {
x := &lt;-naturals
squares &lt;- x * x
}
}()
// Printer (in main goroutine)
for {
fmt.Println(&lt;-squares)
}
}
</code></pre>
<p>如您所料上面的程序将生成0、1、4、9、……形式的无穷数列。像这样的串联Channels的管道Pipelines可以用在需要长时间运行的服务中每个长时间运行的goroutine可能会包含一个死循环在不同goroutine的死循环内部使用串联的Channels来通信。但是如果我们希望通过Channels只发送有限的数列该如何处理呢</p>
<p>如果发送者知道没有更多的值需要发送到channel的话那么让接收者也能及时知道没有多余的值可接收将是有用的因为接收者可以停止不必要的接收等待。这可以通过内置的close函数来关闭channel实现</p>
<pre><code class="language-Go">close(naturals)
</code></pre>
<p>当一个channel被关闭后再向该channel发送数据将导致panic异常。当一个被关闭的channel中已经发送的数据都被成功接收后后续的接收操作将不再阻塞它们会立即返回一个零值。关闭上面例子中的naturals变量对应的channel并不能终止循环它依然会收到一个永无休止的零值序列然后将它们发送给打印者goroutine。</p>
<p>没有办法直接测试一个channel是否被关闭但是接收操作有一个变体形式它多接收一个结果多接收的第二个结果是一个布尔值okture表示成功从channels接收到值false表示channels已经被关闭并且里面没有值可接收。使用这个特性我们可以修改squarer函数中的循环代码当naturals对应的channel被关闭并没有值可接收时跳出循环并且也关闭squares对应的channel.</p>
<pre><code class="language-Go">// Squarer
go func() {
for {
x, ok := &lt;-naturals
if !ok {
break // channel was closed and drained
}
squares &lt;- x * x
}
close(squares)
}()
</code></pre>
<p>因为上面的语法是笨拙的而且这种处理模式很常见因此Go语言的range循环可直接在channels上面迭代。使用range循环是上面处理模式的简洁语法它依次从channel接收数据当channel被关闭并且没有值可接收时跳出循环。</p>
<p>在下面的改进中我们的计数器goroutine只生成100个含数字的序列然后关闭naturals对应的channel这将导致计算平方数的squarer对应的goroutine可以正常终止循环并关闭squares对应的channel。在一个更复杂的程序中可以通过defer语句关闭对应的channel。最后主goroutine也可以正常终止循环并退出程序。</p>
<p><u><i>gopl.io/ch8/pipeline2</i></u></p>
<pre><code class="language-Go">func main() {
naturals := make(chan int)
squares := make(chan int)
// Counter
go func() {
for x := 0; x &lt; 100; x++ {
naturals &lt;- x
}
close(naturals)
}()
// Squarer
go func() {
for x := range naturals {
squares &lt;- x * x
}
close(squares)
}()
// Printer (in main goroutine)
for x := range squares {
fmt.Println(x)
}
}
</code></pre>
<p>其实你并不需要关闭每一个channel。只有当需要告诉接收者goroutine所有的数据已经全部发送时才需要关闭channel。不管一个channel是否被关闭当它没有被引用时将会被Go语言的垃圾自动回收器回收。不要将关闭一个打开文件的操作和关闭一个channel操作混淆。对于每个打开的文件都需要在不使用的时候调用对应的Close方法来关闭文件。</p>
<p>试图重复关闭一个channel将导致panic异常试图关闭一个nil值的channel也将导致panic异常。关闭一个channels还会触发一个广播机制我们将在8.9节讨论。</p>
<h3 id="843-单方向的channel"><a class="header" href="#843-单方向的channel">8.4.3. 单方向的Channel</a></h3>
<p>随着程序的增长人们习惯于将大的函数拆分为小的函数。我们前面的例子中使用了三个goroutine然后用两个channels来连接它们它们都是main函数的局部变量。将三个goroutine拆分为以下三个函数是自然的想法</p>
<pre><code class="language-Go">func counter(out chan int)
func squarer(out, in chan int)
func printer(in chan int)
</code></pre>
<p>其中计算平方的squarer函数在两个串联Channels的中间因此拥有两个channel类型的参数一个用于输入一个用于输出。两个channel都拥有相同的类型但是它们的使用方式相反一个只用于接收另一个只用于发送。参数的名字in和out已经明确表示了这个意图但是并无法保证squarer函数向一个in参数对应的channel发送数据或者从一个out参数对应的channel接收数据。</p>
<p>这种场景是典型的。当一个channel作为一个函数参数时它一般总是被专门用于只发送或者只接收。</p>
<p>为了表明这种意图并防止被滥用Go语言的类型系统提供了单方向的channel类型分别用于只发送或只接收的channel。类型<code>chan&lt;- int</code>表示一个只发送int的channel只能发送不能接收。相反类型<code>&lt;-chan int</code>表示一个只接收int的channel只能接收不能发送。箭头<code>&lt;-</code>和关键字chan的相对位置表明了channel的方向。这种限制将在编译期检测。</p>
<p>因为关闭操作只用于断言不再向channel发送新的数据所以只有在发送者所在的goroutine才会调用close函数因此对一个只接收的channel调用close将是一个编译错误。</p>
<p>这是改进的版本这一次参数使用了单方向channel类型</p>
<p><u><i>gopl.io/ch8/pipeline3</i></u></p>
<pre><code class="language-Go">func counter(out chan&lt;- int) {
for x := 0; x &lt; 100; x++ {
out &lt;- x
}
close(out)
}
func squarer(out chan&lt;- int, in &lt;-chan int) {
for v := range in {
out &lt;- v * v
}
close(out)
}
func printer(in &lt;-chan int) {
for v := range in {
fmt.Println(v)
}
}
func main() {
naturals := make(chan int)
squares := make(chan int)
go counter(naturals)
go squarer(squares, naturals)
printer(squares)
}
</code></pre>
<p>调用counternaturalsnaturals的类型将隐式地从chan int转换成chan&lt;- int。调用printer(squares)也会导致相似的隐式转换,这一次是转换为<code>&lt;-chan int</code>类型只接收型的channel。任何双向channel向单向channel变量的赋值操作都将导致该隐式转换。这里并没有反向转换的语法也就是不能将一个类似<code>chan&lt;- int</code>类型的单向型的channel转换为<code>chan int</code>类型的双向型的channel。</p>
<h3 id="844-带缓存的channels"><a class="header" href="#844-带缓存的channels">8.4.4. 带缓存的Channels</a></h3>
<p>带缓存的Channel内部持有一个元素队列。队列的最大容量是在调用make函数创建channel时通过第二个参数指定的。下面的语句创建了一个可以持有三个字符串元素的带缓存Channel。图8.2是ch变量对应的channel的图形表示形式。</p>
<pre><code class="language-Go">ch = make(chan string, 3)
</code></pre>
<p><img src="ch8/../images/ch8-02.png" alt="" /></p>
<p>向缓存Channel的发送操作就是向内部缓存队列的尾部插入元素接收操作则是从队列的头部删除元素。如果内部缓存队列是满的那么发送操作将阻塞直到因另一个goroutine执行接收操作而释放了新的队列空间。相反如果channel是空的接收操作将阻塞直到有另一个goroutine执行发送操作而向队列插入元素。</p>
<p>我们可以在无阻塞的情况下连续向新创建的channel发送三个值</p>
<pre><code class="language-Go">ch &lt;- &quot;A&quot;
ch &lt;- &quot;B&quot;
ch &lt;- &quot;C&quot;
</code></pre>
<p>此刻channel的内部缓存队列将是满的图8.3),如果有第四个发送操作将发生阻塞。</p>
<p><img src="ch8/../images/ch8-03.png" alt="" /></p>
<p>如果我们接收一个值,</p>
<pre><code class="language-Go">fmt.Println(&lt;-ch) // &quot;A&quot;
</code></pre>
<p>那么channel的缓存队列将不是满的也不是空的图8.4因此对该channel执行的发送或接收操作都不会发生阻塞。通过这种方式channel的缓存队列解耦了接收和发送的goroutine。</p>
<p><img src="ch8/../images/ch8-04.png" alt="" /></p>
<p>在某些特殊情况下程序可能需要知道channel内部缓存的容量可以用内置的cap函数获取</p>
<pre><code class="language-Go">fmt.Println(cap(ch)) // &quot;3&quot;
</code></pre>
<p>同样对于内置的len函数如果传入的是channel那么将返回channel内部缓存队列中有效元素的个数。因为在并发程序中该信息会随着接收操作而失效但是它对某些故障诊断和性能优化会有帮助。</p>
<pre><code class="language-Go">fmt.Println(len(ch)) // &quot;2&quot;
</code></pre>
<p>在继续执行两次接收操作后channel内部的缓存队列将又成为空的如果有第四个接收操作将发生阻塞</p>
<pre><code class="language-Go">fmt.Println(&lt;-ch) // &quot;B&quot;
fmt.Println(&lt;-ch) // &quot;C&quot;
</code></pre>
<p>在这个例子中发送和接收操作都发生在同一个goroutine中但是在真实的程序中它们一般由不同的goroutine执行。Go语言新手有时候会将一个带缓存的channel当作同一个goroutine中的队列使用虽然语法看似简单但实际上这是一个错误。Channel和goroutine的调度器机制是紧密相连的如果没有其他goroutine从channel接收发送者——或许是整个程序——将会面临永远阻塞的风险。如果你只是需要一个简单的队列使用slice就可以了。</p>
<p>下面的例子展示了一个使用了带缓存channel的应用。它并发地向三个镜像站点发出请求三个镜像站点分散在不同的地理位置。它们分别将收到的响应发送到带缓存channel最后接收者只接收第一个收到的响应也就是最快的那个响应。因此mirroredQuery函数可能在另外两个响应慢的镜像站点响应之前就返回了结果。顺便说一下多个goroutines并发地向同一个channel发送数据或从同一个channel接收数据都是常见的用法。</p>
<pre><code class="language-Go">func mirroredQuery() string {
responses := make(chan string, 3)
go func() { responses &lt;- request(&quot;asia.gopl.io&quot;) }()
go func() { responses &lt;- request(&quot;europe.gopl.io&quot;) }()
go func() { responses &lt;- request(&quot;americas.gopl.io&quot;) }()
return &lt;-responses // return the quickest response
}
func request(hostname string) (response string) { /* ... */ }
</code></pre>
<p>如果我们使用了无缓存的channel那么两个慢的goroutines将会因为没有人接收而被永远卡住。这种情况称为goroutines泄漏这将是一个BUG。和垃圾变量不同泄漏的goroutines并不会被自动回收因此确保每个不再需要的goroutine能正常退出是重要的。</p>
<p>关于无缓存或带缓存channels之间的选择或者是带缓存channels的容量大小的选择都可能影响程序的正确性。无缓存channel更强地保证了每个发送操作与相应的同步接收操作但是对于带缓存channel这些操作是解耦的。同样即使我们知道将要发送到一个channel的信息的数量上限创建一个对应容量大小的带缓存channel也是不现实的因为这要求在执行任何接收操作之前缓存所有已经发送的值。如果未能分配足够的缓存将导致程序死锁。</p>
<p>Channel的缓存也可能影响程序的性能。想象一家蛋糕店有三个厨师一个烘焙一个上糖衣还有一个将每个蛋糕传递到它下一个厨师的生产线。在狭小的厨房空间环境每个厨师在完成蛋糕后必须等待下一个厨师已经准备好接受它这类似于在一个无缓存的channel上进行沟通。</p>
<p>如果在每个厨师之间有一个放置一个蛋糕的额外空间那么每个厨师就可以将一个完成的蛋糕临时放在那里而马上进入下一个蛋糕的制作中这类似于将channel的缓存队列的容量设置为1。只要每个厨师的平均工作效率相近那么其中大部分的传输工作将是迅速的个体之间细小的效率差异将在交接过程中弥补。如果厨师之间有更大的额外空间——也是就更大容量的缓存队列——将可以在不停止生产线的前提下消除更大的效率波动例如一个厨师可以短暂地休息然后再加快赶上进度而不影响其他人。</p>
<p>另一方面,如果生产线的前期阶段一直快于后续阶段,那么它们之间的缓存在大部分时间都将是满的。相反,如果后续阶段比前期阶段更快,那么它们之间的缓存在大部分时间都将是空的。对于这类场景,额外的缓存并没有带来任何好处。</p>
<p>生产线的隐喻对于理解channels和goroutines的工作机制是很有帮助的。例如如果第二阶段是需要精心制作的复杂操作一个厨师可能无法跟上第一个厨师的进度或者是无法满足第三阶段厨师的需求。要解决这个问题我们可以再雇佣另一个厨师来帮助完成第二阶段的工作他执行相同的任务但是独立工作。这类似于基于相同的channels创建另一个独立的goroutine。</p>
<p>我们没有太多的空间展示全部细节但是gopl.io/ch8/cake包模拟了这个蛋糕店可以通过不同的参数调整。它还对上面提到的几种场景提供对应的基准测试§11.4</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="85-并发的循环"><a class="header" href="#85-并发的循环">8.5. 并发的循环</a></h2>
<p>本节中我们会探索一些用来在并行时循环迭代的常见并发模型。我们会探究从全尺寸图片生成一些缩略图的问题。gopl.io/ch8/thumbnail包提供了ImageFile函数来帮我们拉伸图片。我们不会说明这个函数的实现只需要从gopl.io下载它。</p>
<p><u><i>gopl.io/ch8/thumbnail</i></u></p>
<pre><code class="language-go">package thumbnail
// ImageFile reads an image from infile and writes
// a thumbnail-size version of it in the same directory.
// It returns the generated file name, e.g., &quot;foo.thumb.jpg&quot;.
func ImageFile(infile string) (string, error)
</code></pre>
<p>下面的程序会循环迭代一些图片文件名,并为每一张图片生成一个缩略图:</p>
<p><u><i>gopl.io/ch8/thumbnail</i></u></p>
<pre><code class="language-go">// makeThumbnails makes thumbnails of the specified files.
func makeThumbnails(filenames []string) {
for _, f := range filenames {
if _, err := thumbnail.ImageFile(f); err != nil {
log.Println(err)
}
}
}
</code></pre>
<p>显然我们处理文件的顺序无关紧要因为每一个图片的拉伸操作和其它图片的处理操作都是彼此独立的。像这种子问题都是完全彼此独立的问题被叫做易并行问题译注embarrassingly parallel直译的话更像是尴尬并行。易并行问题是最容易被实现成并行的一类问题废话并且最能够享受到并发带来的好处能够随着并行的规模线性地扩展。</p>
<p>下面让我们并行地执行这些操作从而将文件IO的延迟隐藏掉并用上多核cpu的计算能力来拉伸图像。我们的第一个并发程序只是使用了一个go关键字。这里我们先忽略掉错误之后再进行处理。</p>
<pre><code class="language-go">// NOTE: incorrect!
func makeThumbnails2(filenames []string) {
for _, f := range filenames {
go thumbnail.ImageFile(f) // NOTE: ignoring errors
}
}
</code></pre>
<p>这个版本运行的实在有点太快实际上由于它比最早的版本使用的时间要短得多即使当文件名的slice中只包含有一个元素。这就有点奇怪了如果程序没有并发执行的话那为什么一个并发的版本还是要快呢答案其实是makeThumbnails在它还没有完成工作之前就已经返回了。它启动了所有的goroutine每一个文件名对应一个但没有等待它们一直到执行完毕。</p>
<p>没有什么直接的办法能够等待goroutine完成但是我们可以改变goroutine里的代码让其能够将完成情况报告给外部的goroutine知晓使用的方式是向一个共享的channel中发送事件。因为我们已经确切地知道有len(filenames)个内部goroutine所以外部的goroutine只需要在返回之前对这些事件计数。</p>
<pre><code class="language-go">// makeThumbnails3 makes thumbnails of the specified files in parallel.
func makeThumbnails3(filenames []string) {
ch := make(chan struct{})
for _, f := range filenames {
go func(f string) {
thumbnail.ImageFile(f) // NOTE: ignoring errors
ch &lt;- struct{}{}
}(f)
}
// Wait for goroutines to complete.
for range filenames {
&lt;-ch
}
}
</code></pre>
<p>注意我们将f的值作为一个显式的变量传给了函数而不是在循环的闭包中声明</p>
<pre><code class="language-go">for _, f := range filenames {
go func() {
thumbnail.ImageFile(f) // NOTE: incorrect!
// ...
}()
}
</code></pre>
<p>回忆一下之前在5.6.1节中匿名函数中的循环变量快照问题。上面这个单独的变量f是被所有的匿名函数值所共享且会被连续的循环迭代所更新的。当新的goroutine开始执行字面函数时for循环可能已经更新了f并且开始了另一轮的迭代或者更有可能的已经结束了整个循环所以当这些goroutine开始读取f的值时它们所看到的值已经是slice的最后一个元素了。显式地添加这个参数我们能够确保使用的f是当go语句执行时的“当前”那个f。</p>
<p>如果我们想要从每一个worker goroutine往主goroutine中返回值时该怎么办呢当我们调用thumbnail.ImageFile创建文件失败的时候它会返回一个错误。下一个版本的makeThumbnails会返回其在做拉伸操作时接收到的第一个错误</p>
<pre><code class="language-go">// makeThumbnails4 makes thumbnails for the specified files in parallel.
// It returns an error if any step failed.
func makeThumbnails4(filenames []string) error {
errors := make(chan error)
for _, f := range filenames {
go func(f string) {
_, err := thumbnail.ImageFile(f)
errors &lt;- err
}(f)
}
for range filenames {
if err := &lt;-errors; err != nil {
return err // NOTE: incorrect: goroutine leak!
}
}
return nil
}
</code></pre>
<p>这个程序有一个微妙的bug。当它遇到第一个非nil的error时会直接将error返回到调用方使得没有一个goroutine去排空errors channel。这样剩下的worker goroutine在向这个channel中发送值时都会永远地阻塞下去并且永远都不会退出。这种情况叫做goroutine泄露§8.4.4可能会导致整个程序卡住或者跑出out of memory的错误。</p>
<p>最简单的解决办法就是用一个具有合适大小的buffered channel这样这些worker goroutine向channel中发送错误时就不会被阻塞。一个可选的解决办法是创建一个另外的goroutine当main goroutine返回第一个错误的同时去排空channel。</p>
<p>下一个版本的makeThumbnails使用了一个buffered channel来返回生成的图片文件的名字附带生成时的错误。</p>
<pre><code class="language-go">// makeThumbnails5 makes thumbnails for the specified files in parallel.
// It returns the generated file names in an arbitrary order,
// or an error if any step failed.
func makeThumbnails5(filenames []string) (thumbfiles []string, err error) {
type item struct {
thumbfile string
err error
}
ch := make(chan item, len(filenames))
for _, f := range filenames {
go func(f string) {
var it item
it.thumbfile, it.err = thumbnail.ImageFile(f)
ch &lt;- it
}(f)
}
for range filenames {
it := &lt;-ch
if it.err != nil {
return nil, it.err
}
thumbfiles = append(thumbfiles, it.thumbfile)
}
return thumbfiles, nil
}
</code></pre>
<p>我们最后一个版本的makeThumbnails返回了新文件们的大小总计数bytes。和前面的版本都不一样的一点是我们在这个版本里没有把文件名放在slice里而是通过一个string的channel传过来所以我们无法对循环的次数进行预测。</p>
<p>为了知道最后一个goroutine什么时候结束最后一个结束并不一定是最后一个开始我们需要一个递增的计数器在每一个goroutine启动时加一在goroutine退出时减一。这需要一种特殊的计数器这个计数器需要在多个goroutine操作时做到安全并且提供在其减为零之前一直等待的一种方法。这种计数类型被称为sync.WaitGroup下面的代码就用到了这种方法</p>
<pre><code class="language-go">// makeThumbnails6 makes thumbnails for each file received from the channel.
// It returns the number of bytes occupied by the files it creates.
func makeThumbnails6(filenames &lt;-chan string) int64 {
sizes := make(chan int64)
var wg sync.WaitGroup // number of working goroutines
for f := range filenames {
wg.Add(1)
// worker
go func(f string) {
defer wg.Done()
thumb, err := thumbnail.ImageFile(f)
if err != nil {
log.Println(err)
return
}
info, _ := os.Stat(thumb) // OK to ignore error
sizes &lt;- info.Size()
}(f)
}
// closer
go func() {
wg.Wait()
close(sizes)
}()
var total int64
for size := range sizes {
total += size
}
return total
}
</code></pre>
<p>注意Add和Done方法的不对称。Add是为计数器加一必须在worker goroutine开始之前调用而不是在goroutine中否则的话我们没办法确定Add是在&quot;closer&quot; goroutine调用Wait之前被调用。并且Add还有一个参数但Done却没有任何参数其实它和Add(-1)是等价的。我们使用defer来确保计数器即使是在出错的情况下依然能够正确地被减掉。上面的程序代码结构是当我们使用并发循环但又不知道迭代次数时很通常而且很地道的写法。</p>
<p>sizes channel携带了每一个文件的大小到main goroutine在main goroutine中使用了range loop来计算总和。观察一下我们是怎样创建一个closer goroutine并让其在所有worker goroutine们结束之后再关闭sizes channel的。两步操作wait和close必须是基于sizes的循环的并发。考虑一下另一种方案如果等待操作被放在了main goroutine中在循环之前这样的话就永远都不会结束了如果在循环之后那么又变成了不可达的部分因为没有任何东西去关闭这个channel这个循环就永远都不会终止。</p>
<p>图8.5 表明了makethumbnails6函数中事件的序列。纵列表示goroutine。窄线段代表sleep粗线段代表活动。斜线箭头代表用来同步两个goroutine的事件。时间向下流动。注意main goroutine是如何大部分的时间被唤醒执行其range循环等待worker发送值或者closer来关闭channel的。</p>
<p><img src="ch8/../images/ch8-05.png" alt="" /></p>
<p><strong>练习 8.4</strong> 修改reverb2服务器在每一个连接中使用sync.WaitGroup来计数活跃的echo goroutine。当计数减为零时关闭TCP连接的写入像练习8.3中一样。验证一下你的修改版netcat3客户端会一直等待所有的并发“喊叫”完成即使是在标准输入流已经关闭的情况下。</p>
<p><strong>练习 8.5</strong> 使用一个已有的CPU绑定的顺序程序比如在3.3节中我们写的Mandelbrot程序或者3.2节中的3-D surface计算程序并将他们的主循环改为并发形式使用channel来进行通信。在多核计算机上这个程序得到了多少速度上的改进使用多少个goroutine是最合适的呢</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="86-示例-并发的web爬虫"><a class="header" href="#86-示例-并发的web爬虫">8.6. 示例: 并发的Web爬虫</a></h2>
<p>在5.6节中我们做了一个简单的web爬虫用bfs(广度优先)算法来抓取整个网站。在本节中我们会让这个爬虫并行化这样每一个彼此独立的抓取命令可以并行进行IO最大化利用网络资源。crawl函数和gopl.io/ch5/findlinks3中的是一样的。</p>
<p><u><i>gopl.io/ch8/crawl1</i></u></p>
<pre><code class="language-go">func crawl(url string) []string {
fmt.Println(url)
list, err := links.Extract(url)
if err != nil {
log.Print(err)
}
return list
}
</code></pre>
<p>主函数和5.6节中的breadthFirst(广度优先)类似。像之前一样一个worklist是一个记录了需要处理的元素的队列每一个元素都是一个需要抓取的URL列表不过这一次我们用channel代替slice来做这个队列。每一个对crawl的调用都会在他们自己的goroutine中进行并且会把他们抓到的链接发送回worklist。</p>
<pre><code class="language-go">func main() {
worklist := make(chan []string)
// Start with the command-line arguments.
go func() { worklist &lt;- os.Args[1:] }()
// Crawl the web concurrently.
seen := make(map[string]bool)
for list := range worklist {
for _, link := range list {
if !seen[link] {
seen[link] = true
go func(link string) {
worklist &lt;- crawl(link)
}(link)
}
}
}
}
</code></pre>
<p>注意这里的crawl所在的goroutine会将link作为一个显式的参数传入来避免“循环变量快照”的问题在5.6.1中有讲解。另外注意这里将命令行参数传入worklist也是在一个另外的goroutine中进行的这是为了避免channel两端的main goroutine与crawler goroutine都尝试向对方发送内容却没有一端接收内容时发生死锁。当然这里我们也可以用buffered channel来解决问题这里不再赘述。</p>
<p>现在爬虫可以高并发地运行起来并且可以产生一大坨的URL了不过还是会有俩问题。一个问题是在运行一段时间后可能会出现在log的错误信息里的</p>
<pre><code>$ go build gopl.io/ch8/crawl1
$ ./crawl1 http://gopl.io/
http://gopl.io/
https://golang.org/help/
https://golang.org/doc/
https://golang.org/blog/
...
2015/07/15 18:22:12 Get ...: dial tcp: lookup blog.golang.org: no such host
2015/07/15 18:22:12 Get ...: dial tcp 23.21.222.120:443: socket: too many open files
...
</code></pre>
<p>最初的错误信息是一个让人莫名的DNS查找失败即使这个域名是完全可靠的。而随后的错误信息揭示了原因这个程序一次性创建了太多网络连接超过了每一个进程的打开文件数限制既而导致了在调用net.Dial像DNS查找失败这样的问题。</p>
<p>这个程序实在是太他妈并行了。无穷无尽地并行化并不是什么好事情因为不管怎么说你的系统总是会有一些个限制因素比如CPU核心数会限制你的计算负载比如你的硬盘转轴和磁头数限制了你的本地磁盘IO操作频率比如你的网络带宽限制了你的下载速度上限或者是你的一个web服务的服务容量上限等等。为了解决这个问题我们可以限制并发程序所使用的资源来使之适应自己的运行环境。对于我们的例子来说最简单的方法就是限制对links.Extract在同一时间最多不会有超过n次调用这里的n一般小于文件描述符的上限值比如20。这和一个夜店里限制客人数目是一个道理只有当有客人离开时才会允许新的客人进入店内。</p>
<p>我们可以用一个有容量限制的buffered channel来控制并发这类似于操作系统里的计数信号量概念。从概念上讲channel里的n个空槽代表n个可以处理内容的token通行证从channel里接收一个值会释放其中的一个token并且生成一个新的空槽位。这样保证了在没有接收介入时最多有n个发送操作。这里可能我们拿channel里填充的槽来做token更直观一些不过还是这样吧。由于channel里的元素类型并不重要我们用一个零值的struct{}来作为其元素。</p>
<p>让我们重写crawl函数将对links.Extract的调用操作用获取、释放token的操作包裹起来来确保同一时间对其只有20个调用。信号量数量和其能操作的IO资源数量应保持接近。</p>
<p><u><i>gopl.io/ch8/crawl2</i></u></p>
<pre><code class="language-go">// tokens is a counting semaphore used to
// enforce a limit of 20 concurrent requests.
var tokens = make(chan struct{}, 20)
func crawl(url string) []string {
fmt.Println(url)
tokens &lt;- struct{}{} // acquire a token
list, err := links.Extract(url)
&lt;-tokens // release the token
if err != nil {
log.Print(err)
}
return list
}
</code></pre>
<p>第二个问题是这个程序永远都不会终止即使它已经爬到了所有初始链接衍生出的链接。当然除非你慎重地选择了合适的初始化URL或者已经实现了练习8.6中的深度限制你应该还没有意识到这个问题。为了使这个程序能够终止我们需要在worklist为空或者没有crawl的goroutine在运行时退出主循环。</p>
<pre><code class="language-go">func main() {
worklist := make(chan []string)
var n int // number of pending sends to worklist
// Start with the command-line arguments.
n++
go func() { worklist &lt;- os.Args[1:] }()
// Crawl the web concurrently.
seen := make(map[string]bool)
for ; n &gt; 0; n-- {
list := &lt;-worklist
for _, link := range list {
if !seen[link] {
seen[link] = true
n++
go func(link string) {
worklist &lt;- crawl(link)
}(link)
}
}
}
}
</code></pre>
<p>这个版本中计数器n对worklist的发送操作数量进行了限制。每一次我们发现有元素需要被发送到worklist时我们都会对n进行++操作在向worklist中发送初始的命令行参数之前我们也进行过一次++操作。这里的操作++是在每启动一个crawler的goroutine之前。主循环会在n减为0时终止这时候说明没活可干了。</p>
<p>现在这个并发爬虫会比5.6节中的深度优先搜索版快上20倍而且不会出什么错并且在其完成任务时也会正确地终止。</p>
<p>下面的程序是避免过度并发的另一种思路。这个版本使用了原来的crawl函数但没有使用计数信号量取而代之用了20个常驻的crawler goroutine这样来保证最多20个HTTP请求在并发。</p>
<pre><code class="language-go">func main() {
worklist := make(chan []string) // lists of URLs, may have duplicates
unseenLinks := make(chan string) // de-duplicated URLs
// Add command-line arguments to worklist.
go func() { worklist &lt;- os.Args[1:] }()
// Create 20 crawler goroutines to fetch each unseen link.
for i := 0; i &lt; 20; i++ {
go func() {
for link := range unseenLinks {
foundLinks := crawl(link)
go func() { worklist &lt;- foundLinks }()
}
}()
}
// The main goroutine de-duplicates worklist items
// and sends the unseen ones to the crawlers.
seen := make(map[string]bool)
for list := range worklist {
for _, link := range list {
if !seen[link] {
seen[link] = true
unseenLinks &lt;- link
}
}
}
}
</code></pre>
<p>所有的爬虫goroutine现在都是被同一个channel - unseenLinks喂饱的了。主goroutine负责拆分它从worklist里拿到的元素然后把没有抓过的经由unseenLinks channel发送给一个爬虫的goroutine。</p>
<p>seen这个map被限定在main goroutine中也就是说这个map只能在main goroutine中进行访问。类似于其它的信息隐藏方式这样的约束可以让我们从一定程度上保证程序的正确性。例如内部变量不能够在函数外部被访问到变量§2.3.4)在没有发生变量逃逸(译注:局部变量被全局变量引用地址导致变量被分配在堆上)的情况下是无法在函数外部访问的;一个对象的封装字段无法被该对象的方法以外的方法访问到。在所有的情况下,信息隐藏都可以帮助我们约束我们的程序,使其不发生意料之外的情况。</p>
<p>crawl函数爬到的链接在一个专有的goroutine中被发送到worklist中来避免死锁。为了节省篇幅这个例子的终止问题我们先不进行详细阐述了。</p>
<p><strong>练习 8.6</strong> 为并发爬虫增加深度限制。也就是说如果用户设置了depth=3那么只有从首页跳转三次以内能够跳到的页面才能被抓取到。</p>
<p><strong>练习 8.7</strong> 完成一个并发程序来创建一个线上网站的本地镜像把该站点的所有可达的页面都抓取到本地硬盘。为了省事我们这里可以只取出现在该域下的所有页面比如golang.org开头译注外链的应该就不算了。当然了出现在页面里的链接你也需要进行一些处理使其能够在你的镜像站点上进行跳转而不是指向原始的链接。</p>
<p><strong>译注:</strong>
拓展阅读 <a href="http://marcio.io/2015/07/handling-1-million-requests-per-minute-with-golang/">Handling 1 Million Requests per Minute with Go</a></p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="87-基于select的多路复用"><a class="header" href="#87-基于select的多路复用">8.7. 基于select的多路复用</a></h2>
<p>下面的程序会进行火箭发射的倒计时。time.Tick函数返回一个channel程序会周期性地像一个节拍器一样向这个channel发送事件。每一个事件的值是一个时间戳不过更有意思的是其传送方式。</p>
<p><u><i>gopl.io/ch8/countdown1</i></u></p>
<pre><code class="language-go">func main() {
fmt.Println(&quot;Commencing countdown.&quot;)
tick := time.Tick(1 * time.Second)
for countdown := 10; countdown &gt; 0; countdown-- {
fmt.Println(countdown)
&lt;-tick
}
launch()
}
</code></pre>
<p>现在我们让这个程序支持在倒计时中用户按下return键时直接中断发射流程。首先我们启动一个goroutine这个goroutine会尝试从标准输入中读入一个单独的byte并且如果成功了会向名为abort的channel发送一个值。</p>
<p><u><i>gopl.io/ch8/countdown2</i></u></p>
<pre><code class="language-go">abort := make(chan struct{})
go func() {
os.Stdin.Read(make([]byte, 1)) // read a single byte
abort &lt;- struct{}{}
}()
</code></pre>
<p>现在每一次计数循环的迭代都需要等待两个channel中的其中一个返回事件了当一切正常时的ticker channel就像NASA jorgon的&quot;nominal&quot;译注这梗估计我们是不懂了或者异常时返回的abort事件。我们无法做到从每一个channel中接收信息如果我们这么做的话如果第一个channel中没有事件发过来那么程序就会立刻被阻塞这样我们就无法收到第二个channel中发过来的事件。这时候我们需要多路复用multiplex这些操作了为了能够多路复用我们使用了select语句。</p>
<pre><code class="language-go">select {
case &lt;-ch1:
// ...
case x := &lt;-ch2:
// ...use x...
case ch3 &lt;- y:
// ...
default:
// ...
}
</code></pre>
<p>上面是select语句的一般形式。和switch语句稍微有点相似也会有几个case和最后的default选择分支。每一个case代表一个通信操作在某个channel上进行发送或者接收并且会包含一些语句组成的一个语句块。一个接收表达式可能只包含接收表达式自身译注不把接收到的值赋值给变量什么的就像上面的第一个case或者包含在一个简短的变量声明中像第二个case里一样第二种形式让你能够引用接收到的值。</p>
<p>select会等待case中有能够执行的case时去执行。当条件满足时select才会去通信并执行case之后的语句这时候其它通信是不会执行的。一个没有任何case的select语句写作select{},会永远地等待下去。</p>
<p>让我们回到我们的火箭发射程序。time.After函数会立即返回一个channel并起一个新的goroutine在经过特定的时间后向该channel发送一个独立的值。下面的select语句会一直等待直到两个事件中的一个到达无论是abort事件或者一个10秒经过的事件。如果10秒经过了还没有abort事件进入那么火箭就会发射。</p>
<pre><code class="language-go">func main() {
// ...create abort channel...
fmt.Println(&quot;Commencing countdown. Press return to abort.&quot;)
select {
case &lt;-time.After(10 * time.Second):
// Do nothing.
case &lt;-abort:
fmt.Println(&quot;Launch aborted!&quot;)
return
}
launch()
}
</code></pre>
<p>下面这个例子更微妙。ch这个channel的buffer大小是1所以会交替的为空或为满所以只有一个case可以进行下去无论i是奇数或者偶数它都会打印0 2 4 6 8。</p>
<pre><code class="language-go">ch := make(chan int, 1)
for i := 0; i &lt; 10; i++ {
select {
case x := &lt;-ch:
fmt.Println(x) // &quot;0&quot; &quot;2&quot; &quot;4&quot; &quot;6&quot; &quot;8&quot;
case ch &lt;- i:
}
}
</code></pre>
<p>如果多个case同时就绪时select会随机地选择一个执行这样来保证每一个channel都有平等的被select的机会。增加前一个例子的buffer大小会使其输出变得不确定因为当buffer既不为满也不为空时select语句的执行情况就像是抛硬币的行为一样是随机的。</p>
<p>下面让我们的发射程序打印倒计时。这里的select语句会使每次循环迭代等待一秒来执行退出操作。</p>
<p><u><i>gopl.io/ch8/countdown3</i></u></p>
<pre><code class="language-go">func main() {
// ...create abort channel...
fmt.Println(&quot;Commencing countdown. Press return to abort.&quot;)
tick := time.Tick(1 * time.Second)
for countdown := 10; countdown &gt; 0; countdown-- {
fmt.Println(countdown)
select {
case &lt;-tick:
// Do nothing.
case &lt;-abort:
fmt.Println(&quot;Launch aborted!&quot;)
return
}
}
launch()
}
</code></pre>
<p>time.Tick函数表现得好像它创建了一个在循环中调用time.Sleep的goroutine每次被唤醒时发送一个事件。当countdown函数返回时它会停止从tick中接收事件但是ticker这个goroutine还依然存活继续徒劳地尝试向channel中发送值然而这时候已经没有其它的goroutine会从该channel中接收值了——这被称为goroutine泄露§8.4.4)。</p>
<p>Tick函数挺方便但是只有当程序整个生命周期都需要这个时间时我们使用它才比较合适。否则的话我们应该使用下面的这种模式</p>
<pre><code class="language-go">ticker := time.NewTicker(1 * time.Second)
&lt;-ticker.C // receive from the ticker's channel
ticker.Stop() // cause the ticker's goroutine to terminate
</code></pre>
<p>有时候我们希望能够从channel中发送或者接收值并避免因为发送或者接收导致的阻塞尤其是当channel没有准备好写或者读时。select语句就可以实现这样的功能。select会有一个default来设置当其它的操作都不能够马上被处理时程序需要执行哪些逻辑。</p>
<p>下面的select语句会在abort channel中有值时从其中接收值无值时什么都不做。这是一个非阻塞的接收操作反复地做这样的操作叫做“轮询channel”。</p>
<pre><code class="language-go">select {
case &lt;-abort:
fmt.Printf(&quot;Launch aborted!\n&quot;)
return
default:
// do nothing
}
</code></pre>
<p>channel的零值是nil。也许会让你觉得比较奇怪nil的channel有时候也是有一些用处的。因为对一个nil的channel发送和接收操作会永远阻塞在select语句中操作nil的channel永远都不会被select到。</p>
<p>这使得我们可以用nil来激活或者禁用case来达成处理其它输入或输出事件时超时和取消的逻辑。我们会在下一节中看到一个例子。</p>
<p><strong>练习 8.8</strong> 使用select来改造8.3节中的echo服务器为其增加超时这样服务器可以在客户端10秒中没有任何喊话时自动断开连接。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="89-并发的退出"><a class="header" href="#89-并发的退出">8.9. 并发的退出</a></h2>
<p>有时候我们需要通知goroutine停止它正在干的事情比如一个正在执行计算的web服务然而它的客户端已经断开了和服务端的连接。</p>
<p>Go语言并没有提供在一个goroutine中终止另一个goroutine的方法由于这样会导致goroutine之间的共享变量落在未定义的状态上。在8.7节中的rocket launch程序中我们往名字叫abort的channel里发送了一个简单的值在countdown的goroutine中会把这个值理解为自己的退出信号。但是如果我们想要退出两个或者任意多个goroutine怎么办呢</p>
<p>一种可能的手段是向abort的channel里发送和goroutine数目一样多的事件来退出它们。如果这些goroutine中已经有一些自己退出了那么会导致我们的channel里的事件数比goroutine还多这样导致我们的发送直接被阻塞。另一方面如果这些goroutine又生成了其它的goroutine我们的channel里的数目又太少了所以有些goroutine可能会无法接收到退出消息。一般情况下我们是很难知道在某一个时刻具体有多少个goroutine在运行着的。另外当一个goroutine从abort channel中接收到一个值的时候他会消费掉这个值这样其它的goroutine就没法看到这条信息。为了能够达到我们退出goroutine的目的我们需要更靠谱的策略来通过一个channel把消息广播出去这样goroutine们能够看到这条事件消息并且在事件完成之后可以知道这件事已经发生过了。</p>
<p>回忆一下我们关闭了一个channel并且被消费掉了所有已发送的值操作channel之后的代码可以立即被执行并且会产生零值。我们可以将这个机制扩展一下来作为我们的广播机制不要向channel发送值而是用关闭一个channel来进行广播。</p>
<p>只要一些小修改我们就可以把退出逻辑加入到前一节的du程序。首先我们创建一个退出的channel不需要向这个channel发送任何值但其所在的闭包内要写明程序需要退出。我们同时还定义了一个工具函数cancelled这个函数在被调用的时候会轮询退出状态。</p>
<p><u><i>gopl.io/ch8/du4</i></u></p>
<pre><code class="language-go">var done = make(chan struct{})
func cancelled() bool {
select {
case &lt;-done:
return true
default:
return false
}
}
</code></pre>
<p>下面我们创建一个从标准输入流中读取内容的goroutine这是一个比较典型的连接到终端的程序。每当有输入被读到比如用户按了回车键这个goroutine就会把取消消息通过关闭done的channel广播出去。</p>
<pre><code class="language-go">// Cancel traversal when input is detected.
go func() {
os.Stdin.Read(make([]byte, 1)) // read a single byte
close(done)
}()
</code></pre>
<p>现在我们需要使我们的goroutine来对取消进行响应。在main goroutine中我们添加了select的第三个case语句尝试从done channel中接收内容。如果这个case被满足的话在select到的时候即会返回但在结束之前我们需要把fileSizes channel中的内容“排”空在channel被关闭之前舍弃掉所有值。这样可以保证对walkDir的调用不要被向fileSizes发送信息阻塞住可以正确地完成。</p>
<pre><code class="language-go">for {
select {
case &lt;-done:
// Drain fileSizes to allow existing goroutines to finish.
for range fileSizes {
// Do nothing.
}
return
case size, ok := &lt;-fileSizes:
// ...
}
}
</code></pre>
<p>walkDir这个goroutine一启动就会轮询取消状态如果取消状态被设置的话会直接返回并且不做额外的事情。这样我们将所有在取消事件之后创建的goroutine改变为无操作。</p>
<pre><code class="language-go">func walkDir(dir string, n *sync.WaitGroup, fileSizes chan&lt;- int64) {
defer n.Done()
if cancelled() {
return
}
for _, entry := range dirents(dir) {
// ...
}
}
</code></pre>
<p>在walkDir函数的循环中我们对取消状态进行轮询可以带来明显的益处可以避免在取消事件发生时还去创建goroutine。取消本身是有一些代价的想要快速的响应需要对程序逻辑进行侵入式的修改。确保在取消发生之后不要有代价太大的操作可能会需要修改你代码里的很多地方但是在一些重要的地方去检查取消事件也确实能带来很大的好处。</p>
<p>对这个程序的一个简单的性能分析可以揭示瓶颈在dirents函数中获取一个信号量。下面的select可以让这种操作可以被取消并且可以将取消时的延迟从几百毫秒降低到几十毫秒。</p>
<pre><code class="language-go">func dirents(dir string) []os.FileInfo {
select {
case sema &lt;- struct{}{}: // acquire token
case &lt;-done:
return nil // cancelled
}
defer func() { &lt;-sema }() // release token
// ...read directory...
}
</code></pre>
<p>现在当取消发生时所有后台的goroutine都会迅速停止并且主函数会返回。当然当主函数返回时一个程序会退出而我们又无法在主函数退出的时候确认其已经释放了所有的资源译注因为程序都退出了你的代码都没法执行了。这里有一个方便的窍门我们可以一用取代掉直接从主函数返回我们调用一个panic然后runtime会把每一个goroutine的栈dump下来。如果main goroutine是唯一一个剩下的goroutine的话他会清理掉自己的一切资源。但是如果还有其它的goroutine没有退出他们可能没办法被正确地取消掉也有可能被取消但是取消操作会很花时间所以这里的一个调研还是很有必要的。我们用panic来获取到足够的信息来验证我们上面的判断看看最终到底是什么样的情况。</p>
<p><strong>练习 8.10</strong> HTTP请求可能会因http.Request结构体中Cancel channel的关闭而取消。修改8.6节中的web crawler来支持取消http请求。提示http.Get并没有提供方便地定制一个请求的方法。你可以用http.NewRequest来取而代之设置它的Cancel字段然后用http.DefaultClient.Do(req)来进行这个http请求。</p>
<p><strong>练习 8.11</strong> 紧接着8.4.4中的mirroredQuery流程实现一个并发请求url的fetch的变种。当第一个请求返回时直接取消其它的请求。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="810-示例-聊天服务"><a class="header" href="#810-示例-聊天服务">8.10. 示例: 聊天服务</a></h2>
<p>我们用一个聊天服务器来终结本章节的内容这个程序可以让一些用户通过服务器向其它所有用户广播文本消息。这个程序中有四种goroutine。main和broadcaster各自是一个goroutine实例每一个客户端的连接都会有一个handleConn和clientWriter的goroutine。broadcaster是select用法的不错的样例因为它需要处理三种不同类型的消息。</p>
<p>下面演示的main goroutine的工作是listen和accept(译注:网络编程里的概念)从客户端过来的连接。对每一个连接程序都会建立一个新的handleConn的goroutine就像我们在本章开头的并发的echo服务器里所做的那样。</p>
<p><u><i>gopl.io/ch8/chat</i></u></p>
<pre><code class="language-go">func main() {
listener, err := net.Listen(&quot;tcp&quot;, &quot;localhost:8000&quot;)
if err != nil {
log.Fatal(err)
}
go broadcaster()
for {
conn, err := listener.Accept()
if err != nil {
log.Print(err)
continue
}
go handleConn(conn)
}
}
</code></pre>
<p>然后是broadcaster的goroutine。他的内部变量clients会记录当前建立连接的客户端集合。其记录的内容是每一个客户端的消息发出channel的“资格”信息。</p>
<pre><code class="language-go">type client chan&lt;- string // an outgoing message channel
var (
entering = make(chan client)
leaving = make(chan client)
messages = make(chan string) // all incoming client messages
)
func broadcaster() {
clients := make(map[client]bool) // all connected clients
for {
select {
case msg := &lt;-messages:
// Broadcast incoming message to all
// clients' outgoing message channels.
for cli := range clients {
cli &lt;- msg
}
case cli := &lt;-entering:
clients[cli] = true
case cli := &lt;-leaving:
delete(clients, cli)
close(cli)
}
}
}
</code></pre>
<p>broadcaster监听来自全局的entering和leaving的channel来获知客户端的到来和离开事件。当其接收到其中的一个事件时会更新clients集合当该事件是离开行为时它会关闭客户端的消息发送channel。broadcaster也会监听全局的消息channel所有的客户端都会向这个channel中发送消息。当broadcaster接收到什么消息时就会将其广播至所有连接到服务端的客户端。</p>
<p>现在让我们看看每一个客户端的goroutine。handleConn函数会为它的客户端创建一个消息发送channel并通过entering channel来通知客户端的到来。然后它会读取客户端发来的每一行文本并通过全局的消息channel来将这些文本发送出去并为每条消息带上发送者的前缀来标明消息身份。当客户端发送完毕后handleConn会通过leaving这个channel来通知客户端的离开并关闭连接。</p>
<pre><code class="language-go">func handleConn(conn net.Conn) {
ch := make(chan string) // outgoing client messages
go clientWriter(conn, ch)
who := conn.RemoteAddr().String()
ch &lt;- &quot;You are &quot; + who
messages &lt;- who + &quot; has arrived&quot;
entering &lt;- ch
input := bufio.NewScanner(conn)
for input.Scan() {
messages &lt;- who + &quot;: &quot; + input.Text()
}
// NOTE: ignoring potential errors from input.Err()
leaving &lt;- ch
messages &lt;- who + &quot; has left&quot;
conn.Close()
}
func clientWriter(conn net.Conn, ch &lt;-chan string) {
for msg := range ch {
fmt.Fprintln(conn, msg) // NOTE: ignoring network errors
}
}
</code></pre>
<p>另外handleConn为每一个客户端创建了一个clientWriter的goroutine用来接收向客户端发送消息的channel中的广播消息并将它们写入到客户端的网络连接。客户端的读取循环会在broadcaster接收到leaving通知并关闭了channel后终止。</p>
<p>下面演示的是当服务器有两个活动的客户端连接并且在两个窗口中运行的情况使用netcat来聊天</p>
<pre><code>$ go build gopl.io/ch8/chat
$ go build gopl.io/ch8/netcat3
$ ./chat &amp;
$ ./netcat3
You are 127.0.0.1:64208 $ ./netcat3
127.0.0.1:64211 has arrived You are 127.0.0.1:64211
Hi!
127.0.0.1:64208: Hi! 127.0.0.1:64208: Hi!
Hi yourself.
127.0.0.1:64211: Hi yourself. 127.0.0.1:64211: Hi yourself.
^C
127.0.0.1:64208 has left
$ ./netcat3
You are 127.0.0.1:64216 127.0.0.1:64216 has arrived
Welcome.
127.0.0.1:64211: Welcome. 127.0.0.1:64211: Welcome.
^C
127.0.0.1:64211 has left”
</code></pre>
<p>当与n个客户端保持聊天session时这个程序会有2n+2个并发的goroutine然而这个程序却并不需要显式的锁§9.2。clients这个map被限制在了一个独立的goroutine中broadcaster所以它不能被并发地访问。多个goroutine共享的变量只有这些channel和net.Conn的实例两个东西都是并发安全的。我们会在下一章中更多地讲解约束并发安全以及goroutine中共享变量的含义。</p>
<p><strong>练习 8.12</strong> 使broadcaster能够在每个新的客户端到来时通知它当前的客户端集合。这需要你在clients集合中以及entering和leaving的channel中记录客户端的名字。</p>
<p><strong>练习 8.13</strong> 使聊天服务器能够断开空闲的客户端连接比如最近五分钟之后没有发送任何消息的那些客户端。提示可以在其它goroutine中调用conn.Close()来解除Read调用就像input.Scanner()所做的那样。</p>
<p><strong>练习 8.14</strong> 修改聊天服务器的网络协议这样每一个客户端就可以在entering时提供他们的名字。将消息前缀由之前的网络地址改为这个名字。</p>
<p><strong>练习 8.15</strong> 如果一个客户端没有及时地读取数据可能会导致所有的客户端被阻塞。修改broadcaster来跳过一条消息而不是等待这个客户端一直到其准备好读写。或者为每一个客户端的消息发送channel建立缓冲区这样大部分的消息便不会被丢掉broadcaster应该用一个非阻塞的send向这个channel中发消息。</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="第9章-基于共享变量的并发"><a class="header" href="#第9章-基于共享变量的并发">第9章 基于共享变量的并发</a></h1>
<p>前一章我们介绍了一些使用goroutine和channel这样直接而自然的方式来实现并发的方法。然而这样做我们实际上回避了在写并发代码时必须处理的一些重要而且细微的问题。</p>
<p>在本章中我们会细致地了解并发机制。尤其是在多goroutine之间的共享变量并发问题的分析手段以及解决这些问题的基本模式。最后我们会解释goroutine和操作系统线程之间的技术上的一些区别。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="91-竞争条件"><a class="header" href="#91-竞争条件">9.1. 竞争条件</a></h2>
<p>在一个线性就是说只有一个goroutine的的程序中程序的执行顺序只由程序的逻辑来决定。例如我们有一段语句序列第一个在第二个之前废话以此类推。在有两个或更多goroutine的程序中每一个goroutine内的语句也是按照既定的顺序去执行的但是一般情况下我们没法去知道分别位于两个goroutine的事件x和y的执行顺序x是在y之前还是之后还是同时发生是没法判断的。当我们没有办法自信地确认一个事件是在另一个事件的前面或者后面发生的话就说明x和y这两个事件是并发的。</p>
<p>考虑一下,一个函数在线性程序中可以正确地工作。如果在并发的情况下,这个函数依然可以正确地工作的话,那么我们就说这个函数是并发安全的,并发安全的函数不需要额外的同步工作。我们可以把这个概念概括为一个特定类型的一些方法和操作函数,对于某个类型来说,如果其所有可访问的方法和操作都是并发安全的话,那么该类型便是并发安全的。</p>
<p>在一个程序中有非并发安全的类型的情况下我们依然可以使这个程序并发安全。确实并发安全的类型是例外而不是规则所以只有当文档中明确地说明了其是并发安全的情况下你才可以并发地去访问它。我们会避免并发访问大多数的类型无论是将变量局限在单一的一个goroutine内还是用互斥条件维持更高级别的不变性都是为了这个目的。我们会在本章中说明这些术语。</p>
<p>相反包级别的导出函数一般情况下都是并发安全的。由于package级的变量没法被限制在单一的gorouine所以修改这些变量“必须”使用互斥条件。</p>
<p>一个函数在并发调用时没法工作的原因太多了比如死锁deadlock、活锁livelock和饿死resource starvation。我们没有空去讨论所有的问题这里我们只聚焦在竞争条件上。</p>
<p>竞争条件指的是程序在多个goroutine交叉执行操作时没有给出正确的结果。竞争条件是很恶劣的一种场景因为这种问题会一直潜伏在你的程序里然后在非常少见的时候蹦出来或许只是会在很大的负载时才会发生又或许是会在使用了某一个编译器、某一种平台或者某一种架构的时候才会出现。这些使得竞争条件带来的问题非常难以复现而且难以分析诊断。</p>
<p>传统上经常用经济损失来为竞争条件做比喻,所以我们来看一个简单的银行账户程序。</p>
<pre><code class="language-go">// Package bank implements a bank with only one account.
package bank
var balance int
func Deposit(amount int) { balance = balance + amount }
func Balance() int { return balance }
</code></pre>
<p>(当然我们也可以把Deposit存款函数写成balance += amount这种形式也是等价的不过长一些的形式解释起来更方便一些。)</p>
<p>对于这个简单的程序而言我们一眼就能看出以任意顺序调用函数Deposit和Balance都会得到正确的结果。也就是说Balance函数会给出之前的所有存入的额度之和。然而当我们并发地而不是顺序地调用这些函数的话Balance就再也没办法保证结果正确了。考虑一下下面的两个goroutine其代表了一个银行联合账户的两笔交易</p>
<pre><code class="language-go">// Alice:
go func() {
bank.Deposit(200) // A1
fmt.Println(&quot;=&quot;, bank.Balance()) // A2
}()
// Bob:
go bank.Deposit(100) // B
</code></pre>
<p>Alice存了$200然后检查她的余额同时Bob存了$100。因为A1和A2是和B并发执行的我们没法预测他们发生的先后顺序。直观地来看的话我们会认为其执行顺序只有三种可能性“Alice先”“Bob先”以及“Alice/Bob/Alice”交错执行。下面的表格会展示经过每一步骤后balance变量的值。引号里的字符串表示余额单。</p>
<pre><code>Alice first Bob first Alice/Bob/Alice
0 0 0
A1 200 B 100 A1 200
A2 &quot;= 200&quot; A1 300 B 300
B 300 A2 &quot;= 300&quot; A2 &quot;= 300&quot;
</code></pre>
<p>所有情况下最终的余额都是$300。唯一的变数是Alice的余额单是否包含了Bob交易不过无论怎么着客户都不会在意。</p>
<p>但是事实是上面的直觉推断是错误的。第四种可能的结果是事实存在的这种情况下Bob的存款会在Alice存款操作中间在余额被读到balance + amount之后在余额被更新之前balance = ...这样会导致Bob的交易丢失。而这是因为Alice的存款操作A1实际上是两个操作的一个序列读取然后写可以称之为A1r和A1w。下面是交叉时产生的问题</p>
<pre><code>Data race
0
A1r 0 ... = balance + amount
B 100
A1w 200 balance = ...
A2 &quot;= 200&quot;
</code></pre>
<p>在A1r之后balance + amount会被计算为200所以这是A1w会写入的值并不受其它存款操作的干预。最终的余额是$200。银行的账户上的资产比Bob实际的资产多了$100。译注因为丢失了Bob的存款操作所以其实是说Bob的钱丢了。</p>
<p>这个程序包含了一个特定的竞争条件叫作数据竞争。无论任何时候只要有两个goroutine并发访问同一变量且至少其中的一个是写操作的时候就会发生数据竞争。</p>
<p>如果数据竞争的对象是一个比一个机器字译注32位机器上一个字=4个字节更大的类型时事情就变得更麻烦了比如interfacestring或者slice类型都是如此。下面的代码会并发地更新两个不同长度的slice</p>
<pre><code class="language-go">var x []int
go func() { x = make([]int, 10) }()
go func() { x = make([]int, 1000000) }()
x[999999] = 1 // NOTE: undefined behavior; memory corruption possible!
</code></pre>
<p>最后一个语句中的x的值是未定义的其可能是nil或者也可能是一个长度为10的slice也可能是一个长度为1,000,000的slice。但是回忆一下slice的三个组成部分指针pointer、长度length和容量capacity。如果指针是从第一个make调用来而长度从第二个make来x就变成了一个混合体一个自称长度为1,000,000但实际上内部只有10个元素的slice。这样导致的结果是存储999,999元素的位置会碰撞一个遥远的内存位置这种情况下难以对值进行预测而且debug也会变成噩梦。这种语义雷区被称为未定义行为对C程序员来说应该很熟悉幸运的是在Go语言里造成的麻烦要比C里小得多。</p>
<p>尽管并发程序的概念让我们知道并发并不是简单的语句交叉执行。我们将会在9.4节中看到数据竞争可能会有奇怪的结果。许多程序员甚至一些非常聪明的人也还是会偶尔提出一些理由来允许数据竞争比如“互斥条件代价太高”“这个逻辑只是用来做logging”“我不介意丢失一些消息”等等。因为在他们的编译器或者平台上很少遇到问题可能给了他们错误的信心。一个好的经验法则是根本就没有什么所谓的良性数据竞争。所以我们一定要避免数据竞争那么在我们的程序中要如何做到呢</p>
<p>我们来重复一下数据竞争的定义因为实在太重要了数据竞争会在两个以上的goroutine并发访问相同的变量且至少其中一个为写操作时发生。根据上述定义有三种方式可以避免数据竞争</p>
<p>第一种方法是不要去写变量。考虑一下下面的map会被“懒”填充也就是说在每个key被第一次请求到的时候才会去填值。如果Icon是被顺序调用的话这个程序会工作很正常但如果Icon被并发调用那么对于这个map来说就会存在数据竞争。</p>
<pre><code class="language-go">var icons = make(map[string]image.Image)
func loadIcon(name string) image.Image
// NOTE: not concurrency-safe!
func Icon(name string) image.Image {
icon, ok := icons[name]
if !ok {
icon = loadIcon(name)
icons[name] = icon
}
return icon
}
</code></pre>
<p>反之如果我们在创建goroutine之前的初始化阶段就初始化了map中的所有条目并且再也不去修改它们那么任意数量的goroutine并发访问Icon都是安全的因为每一个goroutine都只是去读取而已。</p>
<pre><code class="language-go">var icons = map[string]image.Image{
&quot;spades.png&quot;: loadIcon(&quot;spades.png&quot;),
&quot;hearts.png&quot;: loadIcon(&quot;hearts.png&quot;),
&quot;diamonds.png&quot;: loadIcon(&quot;diamonds.png&quot;),
&quot;clubs.png&quot;: loadIcon(&quot;clubs.png&quot;),
}
// Concurrency-safe.
func Icon(name string) image.Image { return icons[name] }
</code></pre>
<p>上面的例子里icons变量在包初始化阶段就已经被赋值了包的初始化是在程序main函数开始执行之前就完成了的。只要初始化完成了icons就再也不会被修改。数据结构如果从不被修改或是不变量则是并发安全的无需进行同步。不过显然如果update操作是必要的我们就没法用这种方法比如说银行账户。</p>
<p>第二种避免数据竞争的方法是避免从多个goroutine访问变量。这也是前一章中大多数程序所采用的方法。例如前面的并发web爬虫§8.6的main goroutine是唯一一个能够访问seen map的goroutine而聊天服务器§8.10中的broadcaster goroutine是唯一一个能够访问clients map的goroutine。这些变量都被限定在了一个单独的goroutine中。</p>
<p>由于其它的goroutine不能够直接访问变量它们只能使用一个channel来发送请求给指定的goroutine来查询更新变量。这也就是Go的口头禅“不要使用共享数据来通信使用通信来共享数据”。一个提供对一个指定的变量通过channel来请求的goroutine叫做这个变量的monitor监控goroutine。例如broadcaster goroutine会监控clients map的全部访问。</p>
<p>下面是一个重写了的银行的例子这个例子中balance变量被限制在了monitor goroutine中名为teller</p>
<p><u><i>gopl.io/ch9/bank1</i></u></p>
<pre><code class="language-go">// Package bank provides a concurrency-safe bank with one account.
package bank
var deposits = make(chan int) // send amount to deposit
var balances = make(chan int) // receive balance
func Deposit(amount int) { deposits &lt;- amount }
func Balance() int { return &lt;-balances }
func teller() {
var balance int // balance is confined to teller goroutine
for {
select {
case amount := &lt;-deposits:
balance += amount
case balances &lt;- balance:
}
}
}
func init() {
go teller() // start the monitor goroutine
}
</code></pre>
<p>即使当一个变量无法在其整个生命周期内被绑定到一个独立的goroutine绑定依然是并发问题的一个解决方案。例如在一条流水线上的goroutine之间共享变量是很普遍的行为在这两者间会通过channel来传输地址信息。如果流水线的每一个阶段都能够避免在将变量传送到下一阶段后再去访问它那么对这个变量的所有访问就是线性的。其效果是变量会被绑定到流水线的一个阶段传送完之后被绑定到下一个以此类推。这种规则有时被称为串行绑定。</p>
<p>下面的例子中Cakes会被严格地顺序访问先是baker gorouine然后是icer gorouine</p>
<pre><code class="language-go">type Cake struct{ state string }
func baker(cooked chan&lt;- *Cake) {
for {
cake := new(Cake)
cake.state = &quot;cooked&quot;
cooked &lt;- cake // baker never touches this cake again
}
}
func icer(iced chan&lt;- *Cake, cooked &lt;-chan *Cake) {
for cake := range cooked {
cake.state = &quot;iced&quot;
iced &lt;- cake // icer never touches this cake again
}
}
</code></pre>
<p>第三种避免数据竞争的方法是允许很多goroutine去访问变量但是在同一个时刻最多只有一个goroutine在访问。这种方式被称为“互斥”在下一节来讨论这个主题。</p>
<p><strong>练习 9.1</strong> 给gopl.io/ch9/bank1程序添加一个Withdraw(amount int)取款函数。其返回结果应该要表明事务是成功了还是因为没有足够资金失败了。这条消息会被发送给monitor的goroutine且消息需要包含取款的额度和一个新的channel这个新channel会被monitor goroutine来把boolean结果发回给Withdraw。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="92-syncmutex互斥锁"><a class="header" href="#92-syncmutex互斥锁">9.2. sync.Mutex互斥锁</a></h2>
<p>在8.6节中我们使用了一个buffered channel作为一个计数信号量来保证最多只有20个goroutine会同时执行HTTP请求。同理我们可以用一个容量只有1的channel来保证最多只有一个goroutine在同一时刻访问一个共享变量。一个只能为1和0的信号量叫做二元信号量binary semaphore</p>
<p><u><i>gopl.io/ch9/bank2</i></u></p>
<pre><code class="language-go">var (
sema = make(chan struct{}, 1) // a binary semaphore guarding balance
balance int
)
func Deposit(amount int) {
sema &lt;- struct{}{} // acquire token
balance = balance + amount
&lt;-sema // release token
}
func Balance() int {
sema &lt;- struct{}{} // acquire token
b := balance
&lt;-sema // release token
return b
}
</code></pre>
<p>这种互斥很实用而且被sync包里的Mutex类型直接支持。它的Lock方法能够获取到token(这里叫锁)并且Unlock方法会释放这个token</p>
<p><u><i>gopl.io/ch9/bank3</i></u></p>
<pre><code class="language-go">import &quot;sync&quot;
var (
mu sync.Mutex // guards balance
balance int
)
func Deposit(amount int) {
mu.Lock()
balance = balance + amount
mu.Unlock()
}
func Balance() int {
mu.Lock()
b := balance
mu.Unlock()
return b
}
</code></pre>
<p>每次一个goroutine访问bank变量时这里只有balance余额变量它都会调用mutex的Lock方法来获取一个互斥锁。如果其它的goroutine已经获得了这个锁的话这个操作会被阻塞直到其它goroutine调用了Unlock使该锁变回可用状态。mutex会保护共享变量。惯例来说被mutex所保护的变量是在mutex变量声明之后立刻声明的。如果你的做法和惯例不符确保在文档里对你的做法进行说明。</p>
<p>在Lock和Unlock之间的代码段中的内容goroutine可以随便读取或者修改这个代码段叫做临界区。锁的持有者在其他goroutine获取该锁之前需要调用Unlock。goroutine在结束后释放锁是必要的无论以哪条路径通过函数都需要释放即使是在错误路径中也要记得释放。</p>
<p>上面的bank程序例证了一种通用的并发模式。一系列的导出函数封装了一个或多个变量那么访问这些变量唯一的方式就是通过这些函数来做或者方法对于一个对象的变量来说。每一个函数在一开始就获取互斥锁并在最后释放锁从而保证共享变量不会被并发访问。这种函数、互斥锁和变量的编排叫作监控monitor这种老式单词的monitor是受“monitor goroutine”的术语启发而来的。两种用法都是一个代理人保证变量被顺序访问</p>
<p>由于在存款和查询余额函数中的临界区代码这么短——只有一行没有分支调用——在代码最后去调用Unlock就显得更为直截了当。在更复杂的临界区的应用中尤其是必须要尽早处理错误并返回的情况下就很难去靠人判断对Lock和Unlock的调用是在所有路径中都能够严格配对的了。Go语言里的defer简直就是这种情况下的救星我们用defer来调用Unlock临界区会隐式地延伸到函数作用域的最后这样我们就从“总要记得在函数返回之后或者发生错误返回时要记得调用一次Unlock”这种状态中获得了解放。Go会自动帮我们完成这些事情。</p>
<pre><code class="language-go">func Balance() int {
mu.Lock()
defer mu.Unlock()
return balance
}
</code></pre>
<p>上面的例子里Unlock会在return语句读取完balance的值之后执行所以Balance函数是并发安全的。这带来的另一点好处是我们再也不需要一个本地变量b了。</p>
<p>此外一个deferred Unlock即使在临界区发生panic时依然会执行这对于用recover§5.10来恢复的程序来说是很重要的。defer调用只会比显式地调用Unlock成本高那么一点点不过却在很大程度上保证了代码的整洁性。大多数情况下对于并发程序来说代码的整洁性比过度的优化更重要。如果可能的话尽量使用defer来将临界区扩展到函数的结束。</p>
<p>考虑一下下面的Withdraw函数。成功的时候它会正确地减掉余额并返回true。但如果银行记录资金对交易来说不足那么取款就会恢复余额并返回false。</p>
<pre><code class="language-go">// NOTE: not atomic!
func Withdraw(amount int) bool {
Deposit(-amount)
if Balance() &lt; 0 {
Deposit(amount)
return false // insufficient funds
}
return true
}
</code></pre>
<p>函数终于给出了正确的结果但是还有一点讨厌的副作用。当过多的取款操作同时执行时balance可能会瞬时被减到0以下。这可能会引起一个并发的取款被不合逻辑地拒绝。所以如果Bob尝试买一辆sports car时Alice可能就没办法为她的早咖啡付款了。这里的问题是取款不是一个原子操作它包含了三个步骤每一步都需要去获取并释放互斥锁但任何一次锁都不会锁上整个取款流程。</p>
<p>理想情况下,取款应该只在整个操作中获得一次互斥锁。下面这样的尝试是错误的:</p>
<pre><code class="language-go">// NOTE: incorrect!
func Withdraw(amount int) bool {
mu.Lock()
defer mu.Unlock()
Deposit(-amount)
if Balance() &lt; 0 {
Deposit(amount)
return false // insufficient funds
}
return true
}
</code></pre>
<p>上面这个例子中Deposit会调用mu.Lock()第二次去获取互斥锁但因为mutex已经锁上了而无法被重入译注go里没有重入锁关于重入锁的概念请参考java——也就是说没法对一个已经锁上的mutex来再次上锁——这会导致程序死锁没法继续执行下去Withdraw会永远阻塞下去。</p>
<p>关于Go的mutex不能重入这一点我们有很充分的理由。mutex的目的是确保共享变量在程序执行时的关键点上能够保证不变性。不变性的一层含义是“没有goroutine访问共享变量”但实际上这里对于mutex保护的变量来说不变性还包含更深层含义当一个goroutine获得了一个互斥锁时它能断定被互斥锁保护的变量正处于不变状态译注即没有其他代码块正在读写共享变量在其获取并保持锁期间可能会去更新共享变量这样不变性只是短暂地被破坏然而当其释放锁之后锁必须保证共享变量重获不变性并且多个goroutine按顺序访问共享变量。尽管一个可以重入的mutex也可以保证没有其它的goroutine在访问共享变量但它不具备不变性更深层含义。译注<a href="https://stackoverflow.com/questions/14670979/recursive-locking-in-go/14671462#14671462">更详细的解释</a>Russ Cox认为可重入锁是bug的温床是一个失败的设计</p>
<p>一个通用的解决方案是将一个函数分离为多个函数比如我们把Deposit分离成两个一个不导出的函数deposit这个函数假设锁总是会被保持并去做实际的操作另一个是导出的函数Deposit这个函数会调用deposit但在调用前会先去获取锁。同理我们可以将Withdraw也表示成这种形式</p>
<pre><code class="language-go">func Withdraw(amount int) bool {
mu.Lock()
defer mu.Unlock()
deposit(-amount)
if balance &lt; 0 {
deposit(amount)
return false // insufficient funds
}
return true
}
func Deposit(amount int) {
mu.Lock()
defer mu.Unlock()
deposit(amount)
}
func Balance() int {
mu.Lock()
defer mu.Unlock()
return balance
}
// This function requires that the lock be held.
func deposit(amount int) { balance += amount }
</code></pre>
<p>当然这里的存款deposit函数很小实际上取款Withdraw函数不需要理会对它的调用尽管如此这里的表达还是表明了规则。</p>
<p>封装§6.6用限制一个程序中的意外交互的方式可以使我们获得数据结构的不变性。因为某种原因封装还帮我们获得了并发的不变性。当你使用mutex时确保mutex和其保护的变量没有被导出在go里也就是小写且不要被大写字母开头的函数访问啦无论这些变量是包级的变量还是一个struct的字段。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="93-syncrwmutex读写锁"><a class="header" href="#93-syncrwmutex读写锁">9.3. sync.RWMutex读写锁</a></h2>
<p>在100刀的存款消失时不做记录多少还是会让我们有一些恐慌Bob写了一个程序每秒运行几百次来检查他的银行余额。他会在家在工作中甚至会在他的手机上来运行这个程序。银行注意到这些陡增的流量使得存款和取款有了延时因为所有的余额查询请求是顺序执行的这样会互斥地获得锁并且会暂时阻止其它的goroutine运行。</p>
<p>由于Balance函数只需要读取变量的状态所以我们同时让多个Balance调用并发运行事实上是安全的只要在运行的时候没有存款或者取款操作就行。在这种场景下我们需要一种特殊类型的锁其允许多个只读操作并行执行但写操作会完全互斥。这种锁叫作“多读单写”锁multiple readers, single writer lockGo语言提供的这样的锁是sync.RWMutex</p>
<pre><code class="language-go">var mu sync.RWMutex
var balance int
func Balance() int {
mu.RLock() // readers lock
defer mu.RUnlock()
return balance
}
</code></pre>
<p>Balance函数现在调用了RLock和RUnlock方法来获取和释放一个读取或者共享锁。Deposit函数没有变化会调用mu.Lock和mu.Unlock方法来获取和释放一个写或互斥锁。</p>
<p>在这次修改后Bob的余额查询请求就可以彼此并行地执行并且会很快地完成了。锁在更多的时间范围可用并且存款请求也能够及时地被响应了。</p>
<p>RLock只能在临界区共享变量没有任何写入操作时可用。一般来说我们不应该假设逻辑上的只读函数/方法也不会去更新某一些变量。比如一个方法功能是访问一个变量,但它也有可能会同时去给一个内部的计数器+1译注可能是记录这个方法的访问次数啥的或者去更新缓存——使即时的调用能够更快。如果有疑惑的话请使用互斥锁。</p>
<p>RWMutex只有当获得锁的大部分goroutine都是读操作而锁在竞争条件下也就是说goroutine们必须等待才能获取到锁的时候RWMutex才是最能带来好处的。RWMutex需要更复杂的内部记录所以会让它比一般的无竞争锁的mutex慢一些。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="94-内存同步"><a class="header" href="#94-内存同步">9.4. 内存同步</a></h2>
<p>你可能比较纠结为什么Balance方法需要用到互斥条件无论是基于channel还是基于互斥量。毕竟和存款不一样它只由一个简单的操作组成所以不会碰到其它goroutine在其执行“期间”执行其它逻辑的风险。这里使用mutex有两方面考虑。第一Balance不会在其它操作比如Withdraw“中间”执行。第二更重要的是“同步”不仅仅是一堆goroutine执行顺序的问题同样也会涉及到内存的问题。</p>
<p>在现代计算机中可能会有一堆处理器每一个都会有其本地缓存local cache。为了效率对内存的写入一般会在每一个处理器中缓冲并在必要时一起flush到主存。这种情况下这些数据可能会以与当初goroutine写入顺序不同的顺序被提交到主存。像channel通信或者互斥量操作这样的原语会使处理器将其聚集的写入flush并commit这样goroutine在某个时间点上的执行结果才能被其它处理器上运行的goroutine得到。</p>
<p>考虑一下下面代码片段的可能输出:</p>
<pre><code class="language-go">var x, y int
go func() {
x = 1 // A1
fmt.Print(&quot;y:&quot;, y, &quot; &quot;) // A2
}()
go func() {
y = 1 // B1
fmt.Print(&quot;x:&quot;, x, &quot; &quot;) // B2
}()
</code></pre>
<p>因为两个goroutine是并发执行并且访问共享变量时也没有互斥会有数据竞争所以程序的运行结果没法预测的话也请不要惊讶。我们可能希望它能够打印出下面这四种结果中的一种相当于几种不同的交错执行时的情况</p>
<pre><code>y:0 x:1
x:0 y:1
x:1 y:1
y:1 x:1
</code></pre>
<p>第四行可以被解释为执行顺序A1,B1,A2,B2或者B1,A1,A2,B2的执行结果。然而实际运行时还是有些情况让我们有点惊讶</p>
<pre><code>x:0 y:0
y:0 x:0
</code></pre>
<p>根据所使用的编译器CPU或者其它很多影响因子这两种情况也是有可能发生的。那么这两种情况要怎么解释呢</p>
<p>在一个独立的goroutine中每一个语句的执行顺序是可以被保证的也就是说goroutine内顺序是连贯的。但是在不使用channel且不使用mutex这样的显式同步操作时我们就没法保证事件在不同的goroutine中看到的执行顺序是一致的了。尽管goroutine A中一定需要观察到x=1执行成功之后才会去读取y但它没法确保自己观察得到goroutine B中对y的写入所以A还可能会打印出y的一个旧版的值。</p>
<p>尽管去理解并发的一种尝试是去将其运行理解为不同goroutine语句的交错执行但看看上面的例子这已经不是现代的编译器和cpu的工作方式了。因为赋值和打印指向不同的变量编译器可能会断定两条语句的顺序不会影响执行结果并且会交换两个语句的执行顺序。如果两个goroutine在不同的CPU上执行每一个核心有自己的缓存这样一个goroutine的写入对于其它goroutine的Print在主存同步之前就是不可见的了。</p>
<p>所有并发的问题都可以用一致的、简单的既定的模式来规避。所以可能的话将变量限定在goroutine内部如果是多个goroutine都需要访问的变量使用互斥条件来访问。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="96-竞争条件检测"><a class="header" href="#96-竞争条件检测">9.6. 竞争条件检测</a></h2>
<p>即使我们小心到不能再小心但在并发程序中犯错还是太容易了。幸运的是Go的runtime和工具链为我们装备了一个复杂但好用的动态分析工具竞争检查器the race detector</p>
<p>只要在go buildgo run或者go test命令后面加上-race的flag就会使编译器创建一个你的应用的“修改”版或者一个附带了能够记录所有运行期对共享变量访问工具的test并且会记录下每一个读或者写共享变量的goroutine的身份信息。另外修改版的程序会记录下所有的同步事件比如go语句channel操作以及对<code>(*sync.Mutex).Lock</code><code>(*sync.WaitGroup).Wait</code>等等的调用。完整的同步事件集合是在The Go Memory Model文档中有说明该文档是和语言文档放在一起的。译注https://golang.org/ref/mem </p>
<p>竞争检查器会检查这些事件会寻找在哪一个goroutine中出现了这样的case例如其读或者写了一个共享变量这个共享变量是被另一个goroutine在没有进行干预同步操作便直接写入的。这种情况也就表明了是对一个共享变量的并发访问即数据竞争。这个工具会打印一份报告内容包含变量身份读取和写入的goroutine中活跃的函数的调用栈。这些信息在定位问题时通常很有用。9.7节中会有一个竞争检查器的实战样例。</p>
<p>竞争检查器会报告所有的已经发生的数据竞争。然而,它只能检测到运行时的竞争条件;并不能证明之后不会发生数据竞争。所以为了使结果尽量正确,请保证你的测试并发地覆盖到了你的包。</p>
<p>由于需要额外的记录因此构建时加了竞争检测的程序跑起来会慢一些且需要更大的内存即使是这样这些代价对于很多生产环境的程序工作来说还是可以接受的。对于一些偶发的竞争条件来说让竞争检查器来干活可以节省无数日夜的debugging。译注多少服务端C和C++程序员为此竞折腰。)</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="97-示例-并发的非阻塞缓存"><a class="header" href="#97-示例-并发的非阻塞缓存">9.7. 示例: 并发的非阻塞缓存</a></h2>
<p>本节中我们会做一个无阻塞的缓存这种工具可以帮助我们来解决现实世界中并发程序出现但没有现成的库可以解决的问题。这个问题叫作缓存memoizing函数译注Memoization的定义 memoization 一词是Donald Michie 根据拉丁语memorandum杜撰的一个词。相应的动词、过去分词、ing形式有memoiz、memoized、memoizing也就是说我们需要缓存函数的返回结果这样在对函数进行调用的时候我们就只需要一次计算之后只要返回计算的结果就可以了。我们的解决方案会是并发安全且会避免对整个缓存加锁而导致所有操作都去争一个锁的设计。</p>
<p>我们将使用下面的httpGetBody函数作为我们需要缓存的函数的一个样例。这个函数会去进行HTTP GET请求并且获取http响应body。对这个函数的调用本身开销是比较大的所以我们尽量避免在不必要的时候反复调用。</p>
<pre><code class="language-go">func httpGetBody(url string) (interface{}, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return ioutil.ReadAll(resp.Body)
}
</code></pre>
<p>最后一行稍微隐藏了一些细节。ReadAll会返回两个结果一个[]byte数组和一个错误不过这两个对象可以被赋值给httpGetBody的返回声明里的interface{}和error类型所以我们也就可以这样返回结果并且不需要额外的工作了。我们在httpGetBody中选用这种返回类型是为了使其可以与缓存匹配。</p>
<p>下面是我们要设计的cache的第一个“草稿”</p>
<p><u><i>gopl.io/ch9/memo1</i></u></p>
<pre><code class="language-go">// Package memo provides a concurrency-unsafe
// memoization of a function of type Func.
package memo
// A Memo caches the results of calling a Func.
type Memo struct {
f Func
cache map[string]result
}
// Func is the type of the function to memoize.
type Func func(key string) (interface{}, error)
type result struct {
value interface{}
err error
}
func New(f Func) *Memo {
return &amp;Memo{f: f, cache: make(map[string]result)}
}
// NOTE: not concurrency-safe!
func (memo *Memo) Get(key string) (interface{}, error) {
res, ok := memo.cache[key]
if !ok {
res.value, res.err = memo.f(key)
memo.cache[key] = res
}
return res.value, res.err
}
</code></pre>
<p>Memo实例会记录需要缓存的函数f类型为Func以及缓存内容里面是一个string到result映射的map。每一个result都是简单的函数返回的值对儿——一个值和一个错误值。继续下去我们会展示一些Memo的变种不过所有的例子都会遵循上面的这些方面。</p>
<p>下面是一个使用Memo的例子。对于流入的URL的每一个元素我们都会调用Get并打印调用延时以及其返回的数据大小的log</p>
<pre><code class="language-go">m := memo.New(httpGetBody)
for url := range incomingURLs() {
start := time.Now()
value, err := m.Get(url)
if err != nil {
log.Print(err)
}
fmt.Printf(&quot;%s, %s, %d bytes\n&quot;,
url, time.Since(start), len(value.([]byte)))
}
</code></pre>
<p>我们可以使用测试包第11章的主题来系统地鉴定缓存的效果。从下面的测试输出我们可以看到URL流包含了一些重复的情况尽管我们第一次对每一个URL的<code>(*Memo).Get</code>的调用都会花上几百毫秒但第二次就只需要花1毫秒就可以返回完整的数据了。</p>
<pre><code>$ go test -v gopl.io/ch9/memo1
=== RUN Test
https://golang.org, 175.026418ms, 7537 bytes
https://godoc.org, 172.686825ms, 6878 bytes
https://play.golang.org, 115.762377ms, 5767 bytes
http://gopl.io, 749.887242ms, 2856 bytes
https://golang.org, 721ns, 7537 bytes
https://godoc.org, 152ns, 6878 bytes
https://play.golang.org, 205ns, 5767 bytes
http://gopl.io, 326ns, 2856 bytes
--- PASS: Test (1.21s)
PASS
ok gopl.io/ch9/memo1 1.257s
</code></pre>
<p>这个测试是顺序地去做所有的调用的。</p>
<p>由于这种彼此独立的HTTP请求可以很好地并发我们可以把这个测试改成并发形式。可以使用sync.WaitGroup来等待所有的请求都完成之后再返回。</p>
<pre><code class="language-go">m := memo.New(httpGetBody)
var n sync.WaitGroup
for url := range incomingURLs() {
n.Add(1)
go func(url string) {
start := time.Now()
value, err := m.Get(url)
if err != nil {
log.Print(err)
}
fmt.Printf(&quot;%s, %s, %d bytes\n&quot;,
url, time.Since(start), len(value.([]byte)))
n.Done()
}(url)
}
n.Wait()
</code></pre>
<p>这次测试跑起来更快了然而不幸的是貌似这个测试不是每次都能够正常工作。我们注意到有一些意料之外的cache miss缓存未命中或者命中了缓存但却返回了错误的值或者甚至会直接崩溃。</p>
<p>但更糟糕的是有时候这个程序还是能正确的运行也就是最让人崩溃的偶发bug所以我们甚至可能都不会意识到这个程序有bug。但是我们可以使用-race这个flag来运行程序竞争检测器§9.6)会打印像下面这样的报告:</p>
<pre><code>$ go test -run=TestConcurrent -race -v gopl.io/ch9/memo1
=== RUN TestConcurrent
...
WARNING: DATA RACE
Write by goroutine 36:
runtime.mapassign1()
~/go/src/runtime/hashmap.go:411 +0x0
gopl.io/ch9/memo1.(*Memo).Get()
~/gobook2/src/gopl.io/ch9/memo1/memo.go:32 +0x205
...
Previous write by goroutine 35:
runtime.mapassign1()
~/go/src/runtime/hashmap.go:411 +0x0
gopl.io/ch9/memo1.(*Memo).Get()
~/gobook2/src/gopl.io/ch9/memo1/memo.go:32 +0x205
...
Found 1 data race(s)
FAIL gopl.io/ch9/memo1 2.393s
</code></pre>
<p>memo.go的32行出现了两次说明有两个goroutine在没有同步干预的情况下更新了cache map。这表明Get不是并发安全的存在数据竞争。</p>
<pre><code class="language-go">28 func (memo *Memo) Get(key string) (interface{}, error) {
29 res, ok := memo.cache(key)
30 if !ok {
31 res.value, res.err = memo.f(key)
32 memo.cache[key] = res
33 }
34 return res.value, res.err
35 }
</code></pre>
<p>最简单的使cache并发安全的方式是使用基于监控的同步。只要给Memo加上一个mutex在Get的一开始获取互斥锁return的时候释放锁就可以让cache的操作发生在临界区内了</p>
<p><u><i>gopl.io/ch9/memo2</i></u></p>
<pre><code class="language-go">type Memo struct {
f Func
mu sync.Mutex // guards cache
cache map[string]result
}
// Get is concurrency-safe.
func (memo *Memo) Get(key string) (value interface{}, err error) {
memo.mu.Lock()
res, ok := memo.cache[key]
if !ok {
res.value, res.err = memo.f(key)
memo.cache[key] = res
}
memo.mu.Unlock()
return res.value, res.err
}
</code></pre>
<p>测试依然并发进行但这回竞争检查器“沉默”了。不幸的是对于Memo的这一点改变使我们完全丧失了并发的性能优点。每次对f的调用期间都会持有锁Get将本来可以并行运行的I/O操作串行化了。我们本章的目的是完成一个无锁缓存而不是现在这样的将所有请求串行化的函数的缓存。</p>
<p>下一个Get的实现调用Get的goroutine会两次获取锁查找阶段获取一次如果查找没有返回任何内容那么进入更新阶段会再次获取。在这两次获取锁的中间阶段其它goroutine可以随意使用cache。</p>
<p><u><i>gopl.io/ch9/memo3</i></u></p>
<pre><code class="language-go">func (memo *Memo) Get(key string) (value interface{}, err error) {
memo.mu.Lock()
res, ok := memo.cache[key]
memo.mu.Unlock()
if !ok {
res.value, res.err = memo.f(key)
// Between the two critical sections, several goroutines
// may race to compute f(key) and update the map.
memo.mu.Lock()
memo.cache[key] = res
memo.mu.Unlock()
}
return res.value, res.err
}
</code></pre>
<p>这些修改使性能再次得到了提升但有一些URL被获取了两次。这种情况在两个以上的goroutine同一时刻调用Get来请求同样的URL时会发生。多个goroutine一起查询cache发现没有值然后一起调用f这个慢不拉叽的函数。在得到结果后也都会去更新map。其中一个获得的结果会覆盖掉另一个的结果。</p>
<p>理想情况下是应该避免掉多余的工作的。而这种“避免”工作一般被称为duplicate suppression重复抑制/避免。下面版本的Memo每一个map元素都是指向一个条目的指针。每一个条目包含对函数f调用结果的内容缓存。与之前不同的是这次entry还包含了一个叫ready的channel。在条目的结果被设置之后这个channel就会被关闭以向其它goroutine广播§8.9)去读取该条目内的结果是安全的了。</p>
<p><u><i>gopl.io/ch9/memo4</i></u></p>
<pre><code class="language-go">type entry struct {
res result
ready chan struct{} // closed when res is ready
}
func New(f Func) *Memo {
return &amp;Memo{f: f, cache: make(map[string]*entry)}
}
type Memo struct {
f Func
mu sync.Mutex // guards cache
cache map[string]*entry
}
func (memo *Memo) Get(key string) (value interface{}, err error) {
memo.mu.Lock()
e := memo.cache[key]
if e == nil {
// This is the first request for this key.
// This goroutine becomes responsible for computing
// the value and broadcasting the ready condition.
e = &amp;entry{ready: make(chan struct{})}
memo.cache[key] = e
memo.mu.Unlock()
e.res.value, e.res.err = memo.f(key)
close(e.ready) // broadcast ready condition
} else {
// This is a repeat request for this key.
memo.mu.Unlock()
&lt;-e.ready // wait for ready condition
}
return e.res.value, e.res.err
}
</code></pre>
<p>现在Get函数包括下面这些步骤了获取互斥锁来保护共享变量cache map查询map中是否存在指定条目如果没有找到那么分配空间插入一个新条目释放互斥锁。如果存在条目的话且其值没有写入完成也就是有其它的goroutine在调用f这个慢函数goroutine必须等待值ready之后才能读到条目的结果。而想知道是否ready的话可以直接从ready channel中读取由于这个读取操作在channel关闭之前一直是阻塞。</p>
<p>如果没有条目的话需要向map中插入一个没有准备好的条目当前正在调用的goroutine就需要负责调用慢函数、更新条目以及向其它所有goroutine广播条目已经ready可读的消息了。</p>
<p>条目中的e.res.value和e.res.err变量是在多个goroutine之间共享的。创建条目的goroutine同时也会设置条目的值其它goroutine在收到&quot;ready&quot;的广播消息之后立刻会去读取条目的值。尽管会被多个goroutine同时访问但却并不需要互斥锁。ready channel的关闭一定会发生在其它goroutine接收到广播事件之前因此第一个goroutine对这些变量的写操作是一定发生在这些读操作之前的。不会发生数据竞争。</p>
<p>这样并发、不重复、无阻塞的cache就完成了。</p>
<p>上面这样Memo的实现使用了一个互斥量来保护多个goroutine调用Get时的共享map变量。不妨把这种设计和前面提到的把map变量限制在一个单独的monitor goroutine的方案做一些对比后者在调用Get时需要发消息。</p>
<p>Func、result和entry的声明和之前保持一致</p>
<pre><code class="language-go">// Func is the type of the function to memoize.
type Func func(key string) (interface{}, error)
// A result is the result of calling a Func.
type result struct {
value interface{}
err error
}
type entry struct {
res result
ready chan struct{} // closed when res is ready
}
</code></pre>
<p>然而Memo类型现在包含了一个叫做requests的channelGet的调用方用这个channel来和monitor goroutine来通信。requests channel中的元素类型是request。Get的调用方会把这个结构中的两组key都填充好实际上用这两个变量来对函数进行缓存的。另一个叫response的channel会被拿来发送响应结果。这个channel只会传回一个单独的值。</p>
<p><u><i>gopl.io/ch9/memo5</i></u></p>
<pre><code class="language-go">// A request is a message requesting that the Func be applied to key.
type request struct {
key string
response chan&lt;- result // the client wants a single result
}
type Memo struct{ requests chan request }
// New returns a memoization of f. Clients must subsequently call Close.
func New(f Func) *Memo {
memo := &amp;Memo{requests: make(chan request)}
go memo.server(f)
return memo
}
func (memo *Memo) Get(key string) (interface{}, error) {
response := make(chan result)
memo.requests &lt;- request{key, response}
res := &lt;-response
return res.value, res.err
}
func (memo *Memo) Close() { close(memo.requests) }
</code></pre>
<p>上面的Get方法会创建一个response channel把它放进request结构中然后发送给monitor goroutine然后马上又会接收它。</p>
<p>cache变量被限制在了monitor goroutine ``(*Memo).server`中下面会看到。monitor会在循环中一直读取请求直到request channel被Close方法关闭。每一个请求都会去查询cache如果没有找到条目的话那么就会创建/插入一个新的条目。</p>
<pre><code class="language-go">func (memo *Memo) server(f Func) {
cache := make(map[string]*entry)
for req := range memo.requests {
e := cache[req.key]
if e == nil {
// This is the first request for this key.
e = &amp;entry{ready: make(chan struct{})}
cache[req.key] = e
go e.call(f, req.key) // call f(key)
}
go e.deliver(req.response)
}
}
func (e *entry) call(f Func, key string) {
// Evaluate the function.
e.res.value, e.res.err = f(key)
// Broadcast the ready condition.
close(e.ready)
}
func (e *entry) deliver(response chan&lt;- result) {
// Wait for the ready condition.
&lt;-e.ready
// Send the result to the client.
response &lt;- e.res
}
</code></pre>
<p>和基于互斥量的版本类似第一个对某个key的请求需要负责去调用函数f并传入这个key将结果存在条目里并关闭ready channel来广播条目的ready消息。使用<code>(*entry).call</code>来完成上述工作。</p>
<p>紧接着对同一个key的请求会发现map中已经有了存在的条目然后会等待结果变为ready并将结果从response发送给客户端的goroutien。上述工作是用<code>(*entry).deliver</code>来完成的。对call和deliver方法的调用必须让它们在自己的goroutine中进行以确保monitor goroutines不会因此而被阻塞住而没法处理新的请求。</p>
<p>这个例子说明我们无论用上锁,还是通信来建立并发程序都是可行的。</p>
<p>上面的两种方案并不好说特定情境下哪种更好不过了解他们还是有价值的。有时候从一种方式切换到另一种可以使你的代码更为简洁。译注不是说好的golang推崇通信并发么。</p>
<p><strong>练习 9.3</strong> 扩展Func类型和<code>(*Memo).Get</code>方法支持调用方提供一个可选的done channel使其具备通过该channel来取消整个操作的能力§8.9。一个被取消了的Func的调用结果不应该被缓存。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="98-goroutines和线程"><a class="header" href="#98-goroutines和线程">9.8. Goroutines和线程</a></h2>
<p>在上一章中我们说goroutine和操作系统的线程区别可以先忽略。尽管两者的区别实际上只是一个量的区别但量变会引起质变的道理同样适用于goroutine和线程。现在正是我们来区分开两者的最佳时机。</p>
<h3 id="981-动态栈"><a class="header" href="#981-动态栈">9.8.1. 动态栈</a></h3>
<p>每一个OS线程都有一个固定大小的内存块一般会是2MB来做栈这个栈会用来存储当前正在被调用或挂起指在调用其它函数时的函数的内部变量。这个固定大小的栈同时很大又很小。因为2MB的栈对于一个小小的goroutine来说是很大的内存浪费比如对于我们用到的一个只是用来WaitGroup之后关闭channel的goroutine来说。而对于go程序来说同时创建成百上千个goroutine是非常普遍的如果每一个goroutine都需要这么大的栈的话那这么多的goroutine就不太可能了。除去大小的问题之外固定大小的栈对于更复杂或者更深层次的递归函数调用来说显然是不够的。修改固定的大小可以提升空间的利用率允许创建更多的线程并且可以允许更深的递归调用不过这两者是没法同时兼备的。</p>
<p>相反一个goroutine会以一个很小的栈开始其生命周期一般只需要2KB。一个goroutine的栈和操作系统线程一样会保存其活跃或挂起的函数调用的本地变量但是和OS线程不太一样的是一个goroutine的栈大小并不是固定的栈的大小会根据需要动态地伸缩。而goroutine的栈的最大值有1GB比传统的固定大小的线程栈要大得多尽管一般情况下大多goroutine都不需要这么大的栈。</p>
<p>** 练习 9.4:** 创建一个流水线程序支持用channel连接任意数量的goroutine在跑爆内存之前可以创建多少流水线阶段一个变量通过整个流水线需要用多久这个练习题翻译不是很确定</p>
<h3 id="982-goroutine调度"><a class="header" href="#982-goroutine调度">9.8.2. Goroutine调度</a></h3>
<p>OS线程会被操作系统内核调度。每几毫秒一个硬件计时器会中断处理器这会调用一个叫作scheduler的内核函数。这个函数会挂起当前执行的线程并将它的寄存器内容保存到内存中检查线程列表并决定下一次哪个线程可以被运行并从内存中恢复该线程的寄存器信息然后恢复执行该线程的现场并开始执行线程。因为操作系统线程是被内核所调度所以从一个线程向另一个“移动”需要完整的上下文切换也就是说保存一个用户线程的状态到内存恢复另一个线程的到寄存器然后更新调度器的数据结构。这几步操作很慢因为其局部性很差需要几次内存访问并且会增加运行的cpu周期。</p>
<p>Go的运行时包含了其自己的调度器这个调度器使用了一些技术手段比如m:n调度因为其会在n个操作系统线程上多工调度m个goroutine。Go调度器的工作和内核的调度是相似的但是这个调度器只关注单独的Go程序中的goroutine译注按程序独立</p>
<p>和操作系统的线程调度不同的是Go调度器并不是用一个硬件定时器而是被Go语言“建筑”本身进行调度的。例如当一个goroutine调用了time.Sleep或者被channel调用或者mutex操作阻塞时调度器会使其进入休眠并开始执行另一个goroutine直到时机到了再去唤醒第一个goroutine。因为这种调度方式不需要进入内核的上下文所以重新调度一个goroutine比调度一个线程代价要低得多。</p>
<p>** 练习 9.5: ** 写一个有两个goroutine的程序两个goroutine会向两个无buffer channel反复地发送ping-pong消息。这样的程序每秒可以支持多少次通信</p>
<h3 id="983-gomaxprocs"><a class="header" href="#983-gomaxprocs">9.8.3. GOMAXPROCS</a></h3>
<p>Go的调度器使用了一个叫做GOMAXPROCS的变量来决定会有多少个操作系统的线程同时执行Go的代码。其默认的值是运行机器上的CPU的核心数所以在一个有8个核心的机器上时调度器一次会在8个OS线程上去调度GO代码。GOMAXPROCS是前面说的m:n调度中的n。在休眠中的或者在通信中被阻塞的goroutine是不需要一个对应的线程来做调度的。在I/O中或系统调用中或调用非Go语言函数时是需要一个对应的操作系统线程的但是GOMAXPROCS并不需要将这几种情况计算在内。</p>
<p>你可以用GOMAXPROCS的环境变量来显式地控制这个参数或者也可以在运行时用runtime.GOMAXPROCS函数来修改它。我们在下面的小程序中会看到GOMAXPROCS的效果这个程序会无限打印0和1。</p>
<pre><code class="language-go">for {
go fmt.Print(0)
fmt.Print(1)
}
$ GOMAXPROCS=1 go run hacker-cliché.go
111111111111111111110000000000000000000011111...
$ GOMAXPROCS=2 go run hacker-cliché.go
010101010101010101011001100101011010010100110...
</code></pre>
<p>在第一次执行时最多同时只能有一个goroutine被执行。初始情况下只有main goroutine被执行所以会打印很多1。过了一段时间后GO调度器会将其置为休眠并唤醒另一个goroutine这时候就开始打印很多0了在打印的时候goroutine是被调度到操作系统线程上的。在第二次执行时我们使用了两个操作系统线程所以两个goroutine可以一起被执行以同样的频率交替打印0和1。我们必须强调的是goroutine的调度是受很多因子影响的而runtime也是在不断地发展演进的所以这里的你实际得到的结果可能会因为版本的不同而与我们运行的结果有所不同。</p>
<p>** 练习9.6:** 测试一下计算密集型的并发程序练习8.5那样的会被GOMAXPROCS怎样影响到。在你的电脑上最佳的值是多少你的电脑CPU有多少个核心</p>
<h3 id="984-goroutine没有id号"><a class="header" href="#984-goroutine没有id号">9.8.4. Goroutine没有ID号</a></h3>
<p>在大多数支持多线程的操作系统和程序语言中当前的线程都有一个独特的身份id并且这个身份信息可以以一个普通值的形式被很容易地获取到典型的可以是一个integer或者指针值。这种情况下我们做一个抽象化的thread-local storage线程本地存储多线程编程中不希望其它线程访问的内容就很容易只需要以线程的id作为key的一个map就可以解决问题每一个线程以其id就能从中获取到值且和其它线程互不冲突。</p>
<p>goroutine没有可以被程序员获取到的身份id的概念。这一点是设计上故意而为之由于thread-local storage总是会被滥用。比如说一个web server是用一种支持tls的语言实现的而非常普遍的是很多函数会去寻找HTTP请求的信息这代表它们就是去其存储层这个存储层有可能是tls查找的。这就像是那些过分依赖全局变量的程序一样会导致一种非健康的“距离外行为”在这种行为下一个函数的行为可能并不仅由自己的参数所决定而是由其所运行在的线程所决定。因此如果线程本身的身份会改变——比如一些worker线程之类的——那么函数的行为就会变得神秘莫测。</p>
<p>Go鼓励更为简单的模式这种模式下参数译注外部显式参数和内部显式参数。tls 中的内容算是&quot;外部&quot;隐式参数)对函数的影响都是显式的。这样不仅使程序变得更易读,而且会让我们自由地向一些给定的函数分配子任务时不用担心其身份信息影响行为。</p>
<p>你现在应该已经明白了写一个Go程序所需要的所有语言特性信息。在后面两章节中我们会回顾一些之前的实例和工具支持我们写出更大规模的程序如何将一个工程组织成一系列的包如何获取构建测试性能测试剖析写文档并且将这些包分享出去。</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="第10章-包和工具"><a class="header" href="#第10章-包和工具">第10章 包和工具</a></h1>
<p>现在随便一个小程序的实现都可能包含超过10000个函数。然而作者一般只需要考虑其中很小的一部分和做很少的设计因为绝大部分代码都是由他人编写的它们通过类似包或模块的方式被重用。</p>
<p>Go语言有超过100个的标准包译注可以用<code>go list std | wc -l</code>命令查看标准包的具体数目标准库为大多数的程序提供了必要的基础构件。在Go的社区有很多成熟的包被设计、共享、重用和改进目前互联网上已经发布了非常多的Go语言开源包它们可以通过 http://godoc.org 检索。在本章,我们将演示如何使用已有的包和创建新的包。</p>
<p>Go还自带了工具箱里面有很多用来简化工作区和包管理的小工具。在本书开始的时候我们已经见识过如何使用工具箱自带的工具来下载、构建和运行我们的演示程序了。在本章我们将看看这些工具的基本设计理论和尝试更多的功能例如打印工作区中包的文档和查询相关的元数据等。在下一章我们将探讨testing包的单元测试用法。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="101-包简介"><a class="header" href="#101-包简介">10.1. 包简介</a></h2>
<p>任何包系统设计的目的都是为了简化大型程序的设计和维护工作,通过将一组相关的特性放进一个独立的单元以便于理解和更新,在每个单元更新的同时保持和程序中其它单元的相对独立性。这种模块化的特性允许每个包可以被其它的不同项目共享和重用,在项目范围内、甚至全球范围统一的分发和复用。</p>
<p>每个包一般都定义了一个不同的名字空间用于它内部的每个标识符的访问。每个名字空间关联到一个特定的包,让我们给类型、函数等选择简短明了的名字,这样可以在使用它们的时候减少和其它部分名字的冲突。</p>
<p>每个包还通过控制包内名字的可见性和是否导出来实现封装特性。通过限制包成员的可见性并隐藏包API的具体实现将允许包的维护者在不影响外部包用户的前提下调整包的内部实现。通过限制包内变量的可见性还可以强制用户通过某些特定函数来访问和更新内部变量这样可以保证内部变量的一致性和并发时的互斥约束。</p>
<p>当我们修改了一个源文件我们必须重新编译该源文件对应的包和所有依赖该包的其他包。即使是从头构建Go语言编译器的编译速度也明显快于其它编译语言。Go语言的闪电般的编译速度主要得益于三个语言特性。第一点所有导入的包必须在每个文件的开头显式声明这样的话编译器就没有必要读取和分析整个源文件来判断包的依赖关系。第二点禁止包的环状依赖因为没有循环依赖包的依赖关系形成一个有向无环图每个包可以被独立编译而且很可能是被并发编译。第三点编译后包的目标文件不仅仅记录包本身的导出信息目标文件同时还记录了包的依赖关系。因此在编译一个包的时候编译器只需要读取每个直接导入包的目标文件而不需要遍历所有依赖的的文件译注很多都是重复的间接依赖</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="102-导入路径"><a class="header" href="#102-导入路径">10.2. 导入路径</a></h2>
<p>每个包是由一个全局唯一的字符串所标识的导入路径定位。出现在import语句中的导入路径也是字符串。</p>
<pre><code class="language-Go">import (
&quot;fmt&quot;
&quot;math/rand&quot;
&quot;encoding/json&quot;
&quot;golang.org/x/net/html&quot;
&quot;github.com/go-sql-driver/mysql&quot;
)
</code></pre>
<p>就像我们在2.6.1节提到过的Go语言的规范并没有指明包的导入路径字符串的具体含义导入路径的具体含义是由构建工具来解释的。在本章我们将深入讨论Go语言工具箱的功能包括大家经常使用的构建测试等功能。当然也有第三方扩展的工具箱存在。例如Google公司内部的Go语言码农他们就使用内部的多语言构建系统译注Google公司使用的是类似<a href="http://bazel.io">Bazel</a>的构建系统支持多种编程语言目前该构件系统还不能完整支持Windows环境用不同的规则来处理包名字和定位包用不同的规则来处理单元测试等等因为这样可以更紧密适配他们内部环境。</p>
<p>如果你计划分享或发布包那么导入路径最好是全球唯一的。为了避免冲突所有非标准库包的导入路径建议以所在组织的互联网域名为前缀而且这样也有利于包的检索。例如上面的import语句导入了Go团队维护的HTML解析器和一个流行的第三方维护的MySQL驱动。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="103-包声明"><a class="header" href="#103-包声明">10.3. 包声明</a></h2>
<p>在每个Go语言源文件的开头都必须有包声明语句。包声明语句的主要目的是确定当前包被其它包导入时默认的标识符也称为包名</p>
<p>例如math/rand包的每个源文件的开头都包含<code>package rand</code>包声明语句所以当你导入这个包你就可以用rand.Int、rand.Float64类似的方式访问包的成员。</p>
<pre><code class="language-Go">package main
import (
&quot;fmt&quot;
&quot;math/rand&quot;
)
func main() {
fmt.Println(rand.Int())
}
</code></pre>
<p>通常来说默认的包名就是包导入路径名的最后一段因此即使两个包的导入路径不同它们依然可能有一个相同的包名。例如math/rand包和crypto/rand包的包名都是rand。稍后我们将看到如何同时导入两个有相同包名的包。</p>
<p>关于默认包名一般采用导入路径名的最后一段的约定也有三种例外情况。第一个例外包对应一个可执行程序也就是main包这时候main包本身的导入路径是无关紧要的。名字为main的包是给go build§10.7.3)构建命令一个信息,这个包编译完之后必须调用连接器生成一个可执行程序。</p>
<p>第二个例外,包所在的目录中可能有一些文件名是以<code>_test.go</code>为后缀的Go源文件译注前面必须有其它的字符因为以<code>_</code><code>.</code>开头的源文件会被构建工具忽略),并且这些源文件声明的包名也是以<code>_test</code>为后缀名的。这种目录可以包含两种包:一种是普通包,另一种则是测试的外部扩展包。所有以<code>_test</code>为后缀包名的测试外部扩展包都由go test命令独立编译普通包和测试的外部扩展包是相互独立的。测试的外部扩展包一般用来避免测试代码中的循环导入依赖具体细节我们将在11.2.4节中介绍。</p>
<p>第三个例外一些依赖版本号的管理工具会在导入路径后追加版本号信息例如“gopkg.in/yaml.v2”。这种情况下包的名字并不包含版本号后缀而是yaml。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="104-导入声明"><a class="header" href="#104-导入声明">10.4. 导入声明</a></h2>
<p>可以在一个Go语言源文件包声明语句之后其它非导入声明语句之前包含零到多个导入包声明语句。每个导入声明可以单独指定一个导入路径也可以通过圆括号同时导入多个导入路径。下面两个导入形式是等价的但是第二种形式更为常见。</p>
<pre><code class="language-Go">import &quot;fmt&quot;
import &quot;os&quot;
import (
&quot;fmt&quot;
&quot;os&quot;
)
</code></pre>
<p>导入的包之间可以通过添加空行来分组通常将来自不同组织的包独自分组。包的导入顺序无关紧要但是在每个分组中一般会根据字符串顺序排列。gofmt和goimports工具都可以将不同分组导入的包独立排序。</p>
<pre><code class="language-Go">import (
&quot;fmt&quot;
&quot;html/template&quot;
&quot;os&quot;
&quot;golang.org/x/net/html&quot;
&quot;golang.org/x/net/ipv4&quot;
)
</code></pre>
<p>如果我们想同时导入两个有着名字相同的包例如math/rand包和crypto/rand包那么导入声明必须至少为一个同名包指定一个新的包名以避免冲突。这叫做导入包的重命名。</p>
<pre><code class="language-Go">import (
&quot;crypto/rand&quot;
mrand &quot;math/rand&quot; // alternative name mrand avoids conflict
)
</code></pre>
<p>导入包的重命名只影响当前的源文件。其它的源文件如果导入了相同的包,可以用导入包原本默认的名字或重命名为另一个完全不同的名字。</p>
<p>导入包重命名是一个有用的特性它不仅仅只是为了解决名字冲突。如果导入的一个包名很笨重特别是在一些自动生成的代码中这时候用一个简短名称会更方便。选择用简短名称重命名导入包时候最好统一以避免包名混乱。选择另一个包名称还可以帮助避免和本地普通变量名产生冲突。例如如果文件中已经有了一个名为path的变量那么我们可以将“path”标准包重命名为pathpkg。</p>
<p>每个导入声明语句都明确指定了当前包和被导入包之间的依赖关系。如果遇到包循环导入的情况Go语言的构建工具将报告错误。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="105-包的匿名导入"><a class="header" href="#105-包的匿名导入">10.5. 包的匿名导入</a></h2>
<p>如果只是导入一个包而并不使用导入的包将会导致一个编译错误。但是有时候我们只是想利用导入包而产生的副作用它会计算包级变量的初始化表达式和执行导入包的init初始化函数§2.6.2。这时候我们需要抑制“unused import”编译错误我们可以用下划线<code>_</code>来重命名导入的包。像往常一样,下划线<code>_</code>为空白标识符,并不能被访问。</p>
<pre><code class="language-Go">import _ &quot;image/png&quot; // register PNG decoder
</code></pre>
<p>这个被称为包的匿名导入。它通常是用来实现一个编译时机制然后通过在main主程序入口选择性地导入附加的包。首先让我们看看如何使用该特性然后再看看它是如何工作的。</p>
<p>标准库的image图像包包含了一个<code>Decode</code>函数,用于从<code>io.Reader</code>接口读取数据并解码图像它调用底层注册的图像解码器来完成任务然后返回image.Image类型的图像。使用<code>image.Decode</code>很容易编写一个图像格式的转换工具,读取一种格式的图像,然后编码为另一种图像格式:</p>
<p><u><i>gopl.io/ch10/jpeg</i></u></p>
<pre><code class="language-Go">// The jpeg command reads a PNG image from the standard input
// and writes it as a JPEG image to the standard output.
package main
import (
&quot;fmt&quot;
&quot;image&quot;
&quot;image/jpeg&quot;
_ &quot;image/png&quot; // register PNG decoder
&quot;io&quot;
&quot;os&quot;
)
func main() {
if err := toJPEG(os.Stdin, os.Stdout); err != nil {
fmt.Fprintf(os.Stderr, &quot;jpeg: %v\n&quot;, err)
os.Exit(1)
}
}
func toJPEG(in io.Reader, out io.Writer) error {
img, kind, err := image.Decode(in)
if err != nil {
return err
}
fmt.Fprintln(os.Stderr, &quot;Input format =&quot;, kind)
return jpeg.Encode(out, img, &amp;jpeg.Options{Quality: 95})
}
</code></pre>
<p>如果我们将<code>gopl.io/ch3/mandelbrot</code>§3.3的输出导入到这个程序的标准输入它将解码输入的PNG格式图像然后转换为JPEG格式的图像输出图3.3)。</p>
<pre><code>$ go build gopl.io/ch3/mandelbrot
$ go build gopl.io/ch10/jpeg
$ ./mandelbrot | ./jpeg &gt;mandelbrot.jpg
Input format = png
</code></pre>
<p>要注意image/png包的匿名导入语句。如果没有这一行语句程序依然可以编译和运行但是它将不能正确识别和解码PNG格式的图像</p>
<pre><code>$ go build gopl.io/ch10/jpeg
$ ./mandelbrot | ./jpeg &gt;mandelbrot.jpg
jpeg: image: unknown format
</code></pre>
<p>下面的代码演示了它的工作机制。标准库还提供了GIF、PNG和JPEG等格式图像的解码器用户也可以提供自己的解码器但是为了保持程序体积较小很多解码器并没有被全部包含除非是明确需要支持的格式。image.Decode函数在解码时会依次查询支持的格式列表。每个格式驱动列表的每个入口指定了四件事情格式的名称一个用于描述这种图像数据开头部分模式的字符串用于解码器检测识别一个Decode函数用于完成解码图像工作一个DecodeConfig函数用于解码图像的大小和颜色空间的信息。每个驱动入口是通过调用image.RegisterFormat函数注册一般是在每个格式包的init初始化函数中调用例如image/png包是这样注册的</p>
<pre><code class="language-Go">package png // image/png
func Decode(r io.Reader) (image.Image, error)
func DecodeConfig(r io.Reader) (image.Config, error)
func init() {
const pngHeader = &quot;\x89PNG\r\n\x1a\n&quot;
image.RegisterFormat(&quot;png&quot;, pngHeader, Decode, DecodeConfig)
}
</code></pre>
<p>最终的效果是主程序只需要匿名导入特定图像驱动包就可以用image.Decode解码对应格式的图像了。</p>
<p>数据库包database/sql也是采用了类似的技术让用户可以根据自己需要选择导入必要的数据库驱动。例如</p>
<pre><code class="language-Go">import (
&quot;database/sql&quot;
_ &quot;github.com/lib/pq&quot; // enable support for Postgres
_ &quot;github.com/go-sql-driver/mysql&quot; // enable support for MySQL
)
db, err = sql.Open(&quot;postgres&quot;, dbname) // OK
db, err = sql.Open(&quot;mysql&quot;, dbname) // OK
db, err = sql.Open(&quot;sqlite3&quot;, dbname) // returns error: unknown driver &quot;sqlite3&quot;
</code></pre>
<p><strong>练习 10.1</strong> 扩展jpeg程序以支持任意图像格式之间的相互转换使用image.Decode检测支持的格式类型然后通过flag命令行标志参数选择输出的格式。</p>
<p><strong>练习 10.2</strong> 设计一个通用的压缩文件读取框架用来读取ZIParchive/zip和POSIX tararchive/tar格式压缩的文档。使用类似上面的注册技术来扩展支持不同的压缩格式然后根据需要通过匿名导入选择导入要支持的压缩格式的驱动包。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="106-包和命名"><a class="header" href="#106-包和命名">10.6. 包和命名</a></h2>
<p>在本节中我们将提供一些关于Go语言独特的包和成员命名的约定。</p>
<p>当创建一个包一般要用短小的包名但也不能太短导致难以理解。标准库中最常用的包有bufio、bytes、flag、fmt、http、io、json、os、sort、sync和time等包。</p>
<p>尽可能让命名有描述性且无歧义。例如类似imageutil或ioutilis的工具包命名已经足够简洁了就无须再命名为util了。要尽量避免包名使用可能被经常用于局部变量的名字这样可能导致用户重命名导入包例如前面看到的path包。</p>
<p>包名一般采用单数的形式。标准库的bytes、errors和strings使用了复数形式这是为了避免和预定义的类型冲突同样还有go/types是为了避免和type关键字冲突。</p>
<p>要避免包名有其它的含义。例如2.5节中我们的温度转换包最初使用了temp包名虽然并没有持续多久。但这是一个糟糕的尝试因为temp几乎是临时变量的同义词。然后我们有一段时间使用了temperature作为包名显然名字并没有表达包的真实用途。最后我们改成了和strconv标准包类似的tempconv包名这个名字比之前的就好多了。</p>
<p>现在让我们看看如何命名包的成员。由于是通过包的导入名字引入包里面的成员例如fmt.Println同时包含了包名和成员名信息。因此我们一般并不需要关注Println的具体内容因为fmt包名已经包含了这个信息。当设计一个包的时候需要考虑包名和成员名两个部分如何很好地配合。下面有一些例子</p>
<pre><code>bytes.Equal flag.Int http.Get json.Marshal
</code></pre>
<p>我们可以看到一些常用的命名模式。strings包提供了和字符串相关的诸多操作</p>
<pre><code class="language-Go">package strings
func Index(needle, haystack string) int
type Replacer struct{ /* ... */ }
func NewReplacer(oldnew ...string) *Replacer
type Reader struct{ /* ... */ }
func NewReader(s string) *Reader
</code></pre>
<p>包名strings并没有出现在任何成员名字中。因为用户会这样引用这些成员strings.Index、strings.Replacer等。</p>
<p>其它一些包可能只描述了单一的数据类型例如html/template和math/rand等只暴露一个主要的数据结构和与它相关的方法还有一个以New命名的函数用于创建实例。</p>
<pre><code class="language-Go">package rand // &quot;math/rand&quot;
type Rand struct{ /* ... */ }
func New(source Source) *Rand
</code></pre>
<p>这可能导致一些名字重复例如template.Template或rand.Rand这就是这些种类的包名往往特别短的原因之一。</p>
<p>在另一个极端还有像net/http包那样含有非常多的名字和种类不多的数据类型因为它们都是要执行一个复杂的复合任务。尽管有将近二十种类型和更多的函数但是包中最重要的成员名字却是简单明了的Get、Post、Handle、Error、Client、Server等。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="107-工具"><a class="header" href="#107-工具">10.7. 工具</a></h2>
<p>本章剩下的部分将讨论Go语言工具箱的具体功能包括如何下载、格式化、构建、测试和安装Go语言编写的程序。</p>
<p>Go语言的工具箱集合了一系列功能的命令集。它可以看作是一个包管理器类似于Linux中的apt和rpm工具用于包的查询、计算包的依赖关系、从远程版本控制系统下载它们等任务。它也是一个构建系统计算文件的依赖关系然后调用编译器、汇编器和链接器构建程序虽然它故意被设计成没有标准的make命令那么复杂。它也是一个单元测试和基准测试的驱动程序我们将在第11章讨论测试话题。</p>
<p>Go语言工具箱的命令有着类似“瑞士军刀”的风格带着一打的子命令有一些我们经常用到例如get、run、build和fmt等。你可以运行go或go help命令查看内置的帮助文档为了查询方便我们列出了最常用的命令</p>
<pre><code>$ go
...
build compile packages and dependencies
clean remove object files
doc show documentation for package or symbol
env print Go environment information
fmt run gofmt on package sources
get download and install packages and dependencies
install compile and install packages and dependencies
list list packages
run compile and run Go program
test test packages
version print Go version
vet run go tool vet on packages
Use &quot;go help [command]&quot; for more information about a command.
...
</code></pre>
<p>为了达到零配置的设计目标Go语言的工具箱很多地方都依赖各种约定。例如根据给定的源文件的名称Go语言的工具可以找到源文件对应的包因为每个目录只包含了单一的包并且包的导入路径和工作区的目录结构是对应的。给定一个包的导入路径Go语言的工具可以找到与之对应的存储着实体文件的目录。它还可以根据导入路径找到存储代码的仓库的远程服务器URL。</p>
<h3 id="1071-工作区结构"><a class="header" href="#1071-工作区结构">10.7.1. 工作区结构</a></h3>
<p>对于大多数的Go语言用户只需要配置一个名叫GOPATH的环境变量用来指定当前工作目录即可。当需要切换到不同工作区的时候只要更新GOPATH就可以了。例如我们在编写本书时将GOPATH设置为<code>$HOME/gobook</code></p>
<pre><code>$ export GOPATH=$HOME/gobook
$ go get gopl.io/...
</code></pre>
<p>当你用前面介绍的命令下载本书全部的例子源码之后,你的当前工作区的目录结构应该是这样的:</p>
<pre><code>GOPATH/
src/
gopl.io/
.git/
ch1/
helloworld/
main.go
dup/
main.go
...
golang.org/x/net/
.git/
html/
parse.go
node.go
...
bin/
helloworld
dup
pkg/
darwin_amd64/
...
</code></pre>
<p>GOPATH对应的工作区目录有三个子目录。其中src子目录用于存储源代码。每个包被保存在与$GOPATH/src的相对路径为包导入路径的子目录中例如gopl.io/ch1/helloworld相对应的路径目录。我们看到一个GOPATH工作区的src目录中可能有多个独立的版本控制系统例如gopl.io和golang.org分别对应不同的Git仓库。其中pkg子目录用于保存编译后的包的目标文件bin子目录用于保存编译后的可执行程序例如helloworld可执行程序。</p>
<p>第二个环境变量GOROOT用来指定Go的安装目录还有它自带的标准库包的位置。GOROOT的目录结构和GOPATH类似因此存放fmt包的源代码对应目录应该为$GOROOT/src/fmt。用户一般不需要设置GOROOT默认情况下Go语言安装工具会将其设置为安装的目录路径。</p>
<p>其中<code>go env</code>命令用于查看Go语言工具涉及的所有环境变量的值包括未设置环境变量的默认值。GOOS环境变量用于指定目标操作系统例如android、linux、darwin或windowsGOARCH环境变量用于指定处理器的类型例如amd64、386或arm等。虽然GOPATH环境变量是唯一必须要设置的但是其它环境变量也会偶尔用到。</p>
<pre><code>$ go env
GOPATH=&quot;/home/gopher/gobook&quot;
GOROOT=&quot;/usr/local/go&quot;
GOARCH=&quot;amd64&quot;
GOOS=&quot;darwin&quot;
...
</code></pre>
<h3 id="1072-下载包"><a class="header" href="#1072-下载包">10.7.2. 下载包</a></h3>
<p>使用Go语言工具箱的go命令不仅可以根据包导入路径找到本地工作区的包甚至可以从互联网上找到和更新包。</p>
<p>使用命令<code>go get</code>可以下载一个单一的包或者用<code>...</code>下载整个子目录里面的每个包。Go语言工具箱的go命令同时计算并下载所依赖的每个包这也是前一个例子中golang.org/x/net/html自动出现在本地工作区目录的原因。</p>
<p>一旦<code>go get</code>命令下载了包然后就是安装包或包对应的可执行的程序。我们将在下一节再关注它的细节现在只是展示整个下载过程是如何的简单。第一个命令是获取golint工具它用于检测Go源代码的编程风格是否有问题。第二个命令是用golint命令对2.6.2节的gopl.io/ch2/popcount包代码进行编码风格检查。它友好地报告了忘记了包的文档</p>
<pre><code>$ go get github.com/golang/lint/golint
$ $GOPATH/bin/golint gopl.io/ch2/popcount
src/gopl.io/ch2/popcount/main.go:1:1:
package comment should be of the form &quot;Package popcount ...&quot;
</code></pre>
<p><code>go get</code>命令支持当前流行的托管网站GitHub、Bitbucket和Launchpad可以直接向它们的版本控制系统请求代码。对于其它的网站你可能需要指定版本控制系统的具体路径和协议例如 Git或Mercurial。运行<code>go help importpath</code>获取相关的信息。</p>
<p><code>go get</code>命令获取的代码是真实的本地存储仓库而不仅仅只是复制源文件因此你依然可以使用版本管理工具比较本地代码的变更或者切换到其它的版本。例如golang.org/x/net包目录对应一个Git仓库</p>
<pre><code>$ cd $GOPATH/src/golang.org/x/net
$ git remote -v
origin https://go.googlesource.com/net (fetch)
origin https://go.googlesource.com/net (push)
</code></pre>
<p>需要注意的是导入路径含有的网站域名和本地Git仓库对应远程服务地址并不相同真实的Git地址是go.googlesource.com。这其实是Go语言工具的一个特性可以让包用一个自定义的导入路径但是真实的代码却是由更通用的服务提供例如googlesource.com或github.com。因为页面 https://golang.org/x/net/html 包含了如下的元数据它告诉Go语言的工具当前包真实的Git仓库托管地址</p>
<pre><code>$ go build gopl.io/ch1/fetch
$ ./fetch https://golang.org/x/net/html | grep go-import
&lt;meta name=&quot;go-import&quot;
content=&quot;golang.org/x/net git https://go.googlesource.com/net&quot;&gt;
</code></pre>
<p>如果指定<code>-u</code>命令行标志参数,<code>go get</code>命令将确保所有的包和依赖的包的版本都是最新的,然后重新编译和安装它们。如果不包含该标志参数的话,而且如果包已经在本地存在,那么代码将不会被自动更新。</p>
<p><code>go get -u</code>命令只是简单地保证每个包是最新版本如果是第一次下载包则是比较方便的但是对于发布程序则可能是不合适的因为本地程序可能需要对依赖的包做精确的版本依赖管理。通常的解决方案是使用vendor的目录用于存储依赖包的固定版本的源代码对本地依赖的包的版本更新也是谨慎和持续可控的。在Go1.5之前一般需要修改包的导入路径所以复制后golang.org/x/net/html导入路径可能会变为gopl.io/vendor/golang.org/x/net/html。最新的Go语言命令已经支持vendor特性但限于篇幅这里并不讨论vendor的具体细节。不过可以通过<code>go help gopath</code>命令查看Vendor的帮助文档。</p>
<p>(译注墙内用户在上面这些命令的基础上还需要学习用翻墙来go get。)</p>
<p><strong>练习 10.3:</strong> 从 http://gopl.io/ch1/helloworld?go-get=1 获取内容,查看本书的代码的真实托管的网址(<code>go get</code>请求HTML页面时包含了<code>go-get</code>参数,以区别普通的浏览器请求)。</p>
<h3 id="1073-构建包"><a class="header" href="#1073-构建包">10.7.3. 构建包</a></h3>
<p><code>go build</code>命令编译命令行参数指定的每个包。如果包是一个库则忽略输出结果这可以用于检测包是可以正确编译的。如果包的名字是main<code>go build</code>将调用链接器在当前目录创建一个可执行程序;以导入路径的最后一段作为可执行程序的名字。</p>
<p>由于每个目录只包含一个包因此每个对应可执行程序或者叫Unix术语中的命令的包会要求放到一个独立的目录中。这些目录有时候会放在名叫cmd目录的子目录下面例如用于提供Go文档服务的golang.org/x/tools/cmd/godoc命令就是放在cmd子目录§10.7.4)。</p>
<p>每个包可以由它们的导入路径指定,就像前面看到的那样,或者用一个相对目录的路径名指定,相对路径必须以<code>.</code><code>..</code>开头。如果没有指定参数,那么默认指定为当前目录对应的包。下面的命令用于构建同一个包,虽然它们的写法各不相同:</p>
<pre><code>$ cd $GOPATH/src/gopl.io/ch1/helloworld
$ go build
</code></pre>
<p>或者:</p>
<pre><code>$ cd anywhere
$ go build gopl.io/ch1/helloworld
</code></pre>
<p>或者:</p>
<pre><code>$ cd $GOPATH
$ go build ./src/gopl.io/ch1/helloworld
</code></pre>
<p>但不能这样:</p>
<pre><code>$ cd $GOPATH
$ go build src/gopl.io/ch1/helloworld
Error: cannot find package &quot;src/gopl.io/ch1/helloworld&quot;.
</code></pre>
<p>也可以指定包的源文件列表这一般只用于构建一些小程序或做一些临时性的实验。如果是main包将会以第一个Go源文件的基础文件名作为最终的可执行程序的名字。</p>
<pre><code>$ cat quoteargs.go
package main
import (
&quot;fmt&quot;
&quot;os&quot;
)
func main() {
fmt.Printf(&quot;%q\n&quot;, os.Args[1:])
}
$ go build quoteargs.go
$ ./quoteargs one &quot;two three&quot; four\ five
[&quot;one&quot; &quot;two three&quot; &quot;four five&quot;]
</code></pre>
<p>特别是对于这类一次性运行的程序,我们希望尽快的构建并运行它。<code>go run</code>命令实际上是结合了构建和运行的两个步骤:</p>
<pre><code>$ go run quoteargs.go one &quot;two three&quot; four\ five
[&quot;one&quot; &quot;two three&quot; &quot;four five&quot;]
</code></pre>
<p>(译注其实也可以偷懒直接go run <code>*.go</code>)</p>
<p>第一行的参数列表中,第一个不是以<code>.go</code>结尾的将作为可执行程序的参数运行。</p>
<p>默认情况下,<code>go build</code>命令构建指定的包和它依赖的包,然后丢弃除了最后的可执行文件之外所有的中间编译结果。依赖分析和编译过程虽然都是很快的,但是随着项目增加到几十个包和成千上万行代码,依赖关系分析和编译时间的消耗将变的可观,有时候可能需要几秒种,即使这些依赖项没有改变。</p>
<p><code>go install</code>命令和<code>go build</code>命令很相似,但是它会保存每个包的编译成果,而不是将它们都丢弃。被编译的包会被保存到$GOPATH/pkg目录下目录路径和 src目录路径对应可执行程序被保存到$GOPATH/bin目录。很多用户会将$GOPATH/bin添加到可执行程序的搜索列表中。还有<code>go install</code>命令和<code>go build</code>命令都不会重新编译没有发生变化的包,这可以使后续构建更快捷。为了方便编译依赖的包,<code>go build -i</code>命令将安装每个目标所依赖的包。</p>
<p>因为编译对应不同的操作系统平台和CPU架构<code>go install</code>命令会将编译结果安装到GOOS和GOARCH对应的目录。例如在Mac系统golang.org/x/net/html包将被安装到$GOPATH/pkg/darwin_amd64目录下的golang.org/x/net/html.a文件。</p>
<p>针对不同操作系统或CPU的交叉构建也是很简单的。只需要设置好目标对应的GOOS和GOARCH然后运行构建命令即可。下面交叉编译的程序将输出它在编译时的操作系统和CPU类型</p>
<p><u><i>gopl.io/ch10/cross</i></u></p>
<pre><code class="language-Go">func main() {
fmt.Println(runtime.GOOS, runtime.GOARCH)
}
</code></pre>
<p>下面以64位和32位环境分别编译和执行</p>
<pre><code>$ go build gopl.io/ch10/cross
$ ./cross
darwin amd64
$ GOARCH=386 go build gopl.io/ch10/cross
$ ./cross
darwin 386
</code></pre>
<p>有些包可能需要针对不同平台和处理器类型使用不同版本的代码文件以便于处理底层的可移植性问题或为一些特定代码提供优化。如果一个文件名包含了一个操作系统或处理器类型名字例如net_linux.go或asm_amd64.sGo语言的构建工具将只在对应的平台编译这些文件。还有一个特别的构建注释参数可以提供更多的构建过程控制。例如文件中可能包含下面的注释</p>
<pre><code class="language-Go">// +build linux darwin
</code></pre>
<p>在包声明和包注释的前面,该构建注释参数告诉<code>go build</code>只在编译程序对应的目标操作系统是Linux或Mac OS X时才编译这个文件。下面的构建注释则表示不编译这个文件</p>
<pre><code class="language-Go">// +build ignore
</code></pre>
<p>更多细节可以参考go/build包的构建约束部分的文档。</p>
<pre><code>$ go doc go/build
</code></pre>
<h3 id="1074-包文档"><a class="header" href="#1074-包文档">10.7.4. 包文档</a></h3>
<p>Go语言的编码风格鼓励为每个包提供良好的文档。包中每个导出的成员和包声明前都应该包含目的和用法说明的注释。</p>
<p>Go语言中的文档注释一般是完整的句子第一行通常是摘要说明以被注释者的名字开头。注释中函数的参数或其它的标识符并不需要额外的引号或其它标记注明。例如下面是fmt.Fprintf的文档注释。</p>
<pre><code class="language-Go">// Fprintf formats according to a format specifier and writes to w.
// It returns the number of bytes written and any write error encountered.
func Fprintf(w io.Writer, format string, a ...interface{}) (int, error)
</code></pre>
<p>Fprintf函数格式化的细节在fmt包文档中描述。如果注释后紧跟着包声明语句那注释对应整个包的文档。包文档对应的注释只能有一个译注其实可以有多个它们会组合成一个包文档注释包注释可以出现在任何一个源文件中。如果包的注释内容比较长一般会放到一个独立的源文件中fmt包注释就有300行之多。这个专门用于保存包文档的源文件通常叫doc.go。</p>
<p>好的文档并不需要面面俱到文档本身应该是简洁但不可忽略的。事实上Go语言的风格更喜欢简洁的文档并且文档也是需要像代码一样维护的。对于一组声明语句可以用一个精炼的句子描述如果是显而易见的功能则并不需要注释。</p>
<p>在本书中,只要空间允许,我们之前很多包声明都包含了注释文档,但你可以从标准库中发现很多更好的例子。有两个工具可以帮到你。</p>
<p>首先是<code>go doc</code>命令,该命令打印其后所指定的实体的声明与文档注释,该实体可能是一个包:</p>
<pre><code>$ go doc time
package time // import &quot;time&quot;
Package time provides functionality for measuring and displaying time.
const Nanosecond Duration = 1 ...
func After(d Duration) &lt;-chan Time
func Sleep(d Duration)
func Since(t Time) Duration
func Now() Time
type Duration int64
type Time struct { ... }
...many more...
</code></pre>
<p>或者是某个具体的包成员:</p>
<pre><code>$ go doc time.Since
func Since(t Time) Duration
Since returns the time elapsed since t.
It is shorthand for time.Now().Sub(t).
</code></pre>
<p>或者是一个方法:</p>
<pre><code>$ go doc time.Duration.Seconds
func (d Duration) Seconds() float64
Seconds returns the duration as a floating-point number of seconds.
</code></pre>
<p>该命令并不需要输入完整的包导入路径或正确的大小写。下面的命令将打印encoding/json包的<code>(*json.Decoder).Decode</code>方法的文档:</p>
<pre><code>$ go doc json.decode
func (dec *Decoder) Decode(v interface{}) error
Decode reads the next JSON-encoded value from its input and stores
it in the value pointed to by v.
</code></pre>
<p>第二个工具名字也叫godoc它提供可以相互交叉引用的HTML页面但是包含和<code>go doc</code>命令相同以及更多的信息。图10.1演示了time包的文档11.6节将看到godoc演示可以交互的示例程序。godoc的在线服务 https://godoc.org ,包含了成千上万的开源包的检索工具。</p>
<p><img src="ch10/../images/ch10-01.png" alt="" /></p>
<p>你也可以在自己的工作区目录运行godoc服务。运行下面的命令然后在浏览器查看 http://localhost:8000/pkg 页面:</p>
<pre><code>$ godoc -http :8000
</code></pre>
<p>其中<code>-analysis=type</code><code>-analysis=pointer</code>命令行标志参数用于打开文档和代码中关于静态分析的结果。</p>
<h3 id="1075-内部包"><a class="header" href="#1075-内部包">10.7.5. 内部包</a></h3>
<p>在Go语言程序中包是最重要的封装机制。没有导出的标识符只在同一个包内部可以访问而导出的标识符则是面向全宇宙都是可见的。</p>
<p>有时候,一个中间的状态可能也是有用的,标识符对于一小部分信任的包是可见的,但并不是对所有调用者都可见。例如,当我们计划将一个大的包拆分为很多小的更容易维护的子包,但是我们并不想将内部的子包结构也完全暴露出去。同时,我们可能还希望在内部子包之间共享一些通用的处理包,或者我们只是想实验一个新包的还并不稳定的接口,暂时只暴露给一些受限制的用户使用。</p>
<p>为了满足这些需求Go语言的构建工具对包含internal名字的路径段的包导入路径做了特殊处理。这种包叫internal包一个internal包只能被和internal目录有同一个父目录的包所导入。例如net/http/internal/chunked内部包只能被net/http/httputil或net/http包导入但是不能被net/url包导入。不过net/url包却可以导入net/http/httputil包。</p>
<pre><code>net/http
net/http/internal/chunked
net/http/httputil
net/url
</code></pre>
<h3 id="1076-查询包"><a class="header" href="#1076-查询包">10.7.6. 查询包</a></h3>
<p><code>go list</code>命令可以查询可用包的信息。其最简单的形式,可以测试包是否在工作区并打印它的导入路径:</p>
<pre><code>$ go list github.com/go-sql-driver/mysql
github.com/go-sql-driver/mysql
</code></pre>
<p><code>go list</code>命令的参数还可以用<code>&quot;...&quot;</code>表示匹配任意的包的导入路径。我们可以用它来列出工作区中的所有包:</p>
<pre><code>$ go list ...
archive/tar
archive/zip
bufio
bytes
cmd/addr2line
cmd/api
...many more...
</code></pre>
<p>或者是特定子目录下的所有包:</p>
<pre><code>$ go list gopl.io/ch3/...
gopl.io/ch3/basename1
gopl.io/ch3/basename2
gopl.io/ch3/comma
gopl.io/ch3/mandelbrot
gopl.io/ch3/netflag
gopl.io/ch3/printints
gopl.io/ch3/surface
</code></pre>
<p>或者是和某个主题相关的所有包:</p>
<pre><code>$ go list ...xml...
encoding/xml
gopl.io/ch7/xmlselect
</code></pre>
<p><code>go list</code>命令还可以获取每个包完整的元信息,而不仅仅只是导入路径,这些元信息可以以不同格式提供给用户。其中<code>-json</code>命令行参数表示用JSON格式打印每个包的元信息。</p>
<pre><code>$ go list -json hash
{
&quot;Dir&quot;: &quot;/home/gopher/go/src/hash&quot;,
&quot;ImportPath&quot;: &quot;hash&quot;,
&quot;Name&quot;: &quot;hash&quot;,
&quot;Doc&quot;: &quot;Package hash provides interfaces for hash functions.&quot;,
&quot;Target&quot;: &quot;/home/gopher/go/pkg/darwin_amd64/hash.a&quot;,
&quot;Goroot&quot;: true,
&quot;Standard&quot;: true,
&quot;Root&quot;: &quot;/home/gopher/go&quot;,
&quot;GoFiles&quot;: [
&quot;hash.go&quot;
],
&quot;Imports&quot;: [
&quot;io&quot;
],
&quot;Deps&quot;: [
&quot;errors&quot;,
&quot;io&quot;,
&quot;runtime&quot;,
&quot;sync&quot;,
&quot;sync/atomic&quot;,
&quot;unsafe&quot;
]
}
</code></pre>
<p>命令行参数<code>-f</code>则允许用户使用text/template包§4.6的模板语言定义输出文本的格式。下面的命令将打印strconv包的依赖的包然后用join模板函数将结果链接为一行连接时每个结果之间用一个空格分隔</p>
<pre><code>$ go list -f '{{join .Deps &quot; &quot;}}' strconv
errors math runtime unicode/utf8 unsafe
</code></pre>
<p>译注上面的命令在Windows的命令行运行会遇到<code>template: main:1: unclosed action</code>的错误。产生这个错误的原因是因为命令行对命令中的<code>&quot; &quot;</code>参数进行了转义处理。可以按照下面的方法解决转义字符串的问题:</p>
<pre><code>$ go list -f &quot;{{join .Deps \&quot; \&quot;}}&quot; strconv
</code></pre>
<p>下面的命令打印compress子目录下所有包的导入包列表</p>
<pre><code>$ go list -f '{{.ImportPath}} -&gt; {{join .Imports &quot; &quot;}}' compress/...
compress/bzip2 -&gt; bufio io sort
compress/flate -&gt; bufio fmt io math sort strconv
compress/gzip -&gt; bufio compress/flate errors fmt hash hash/crc32 io time
compress/lzw -&gt; bufio errors fmt io
compress/zlib -&gt; bufio compress/flate errors fmt hash hash/adler32 io
</code></pre>
<p>译注Windows下有同样有问题要避免转义字符串的干扰</p>
<pre><code>$ go list -f &quot;{{.ImportPath}} -&gt; {{join .Imports \&quot; \&quot;}}&quot; compress/...
</code></pre>
<p><code>go list</code>命令对于一次性的交互式查询或自动化构建或测试脚本都很有帮助。我们将在11.2.4节中再次使用它。每个子命令的更多信息,包括可设置的字段和意义,可以用<code>go help list</code>命令查看。</p>
<p>在本章我们解释了Go语言工具中除了测试命令之外的所有重要的子命令。在下一章我们将看到如何用<code>go test</code>命令去运行Go语言程序中的测试代码。</p>
<p><strong>练习 10.4</strong> 创建一个工具,根据命令行指定的参数,报告工作区所有依赖包指定的其它包集合。提示:你需要运行<code>go list</code>命令两次一次用于初始化包一次用于所有包。你可能需要用encoding/json§4.5包来分析输出的JSON格式的信息。</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="第11章-测试"><a class="header" href="#第11章-测试">第11章 测试</a></h1>
<p>Maurice Wilkes第一个存储程序计算机EDSAC的设计者1949年他在实验室爬楼梯时有一个顿悟。在《计算机先驱回忆录》Memoirs of a Computer Pioneer他回忆到“忽然间有一种醍醐灌顶的感觉我整个后半生的美好时光都将在寻找程序BUG中度过了”。肯定从那之后的大部分正常的码农都会同情Wilkes过分悲观的想法虽然也许会有人困惑于他对软件开发的难度的天真看法。</p>
<p>现在的程序已经远比Wilkes时代的更大也更复杂也有许多技术可以让软件的复杂性可得到控制。其中有两种技术在实践中证明是比较有效的。第一种是代码在被正式部署前需要进行代码评审。第二种则是测试也就是本章的讨论主题。</p>
<p>我们说测试的时候一般是指自动化测试,也就是写一些小的程序用来检测被测试代码(产品代码)的行为和预期的一样,这些通常都是精心设计的执行某些特定的功能或者是通过随机性的输入待验证边界的处理。</p>
<p>软件测试是一个巨大的领域。测试的任务可能已经占据了一些程序员的部分时间和另一些程序员的全部时间。和软件测试技术相关的图书或博客文章有成千上万之多。对于每一种主流的编程语言,都会有一打的用于测试的软件包,同时也有大量的测试相关的理论,而且每种都吸引了大量技术先驱和追随者。这些都足以说服那些想要编写有效测试的程序员重新学习一套全新的技能。</p>
<p>Go语言的测试技术是相对低级的。它依赖一个go test测试命令和一组按照约定方式编写的测试函数测试命令可以运行这些测试函数。编写相对轻量级的纯测试代码是有效的而且它很容易延伸到基准测试和示例文档。</p>
<p>在实践中编写测试代码和编写程序本身并没有多大区别。我们编写的每一个函数也是针对每个具体的任务。我们必须小心处理边界条件思考合适的数据结构推断合适的输入应该产生什么样的结果输出。编写测试代码和编写普通的Go代码过程是类似的它并不需要学习新的符号、规则和工具。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="111-go-test"><a class="header" href="#111-go-test">11.1. go test</a></h2>
<p>go test命令是一个按照一定的约定和组织来测试代码的程序。在包目录内所有以<code>_test.go</code>为后缀名的源文件在执行go build时不会被构建成包的一部分它们是go test测试的一部分。</p>
<p><code>*_test.go</code>文件中有三种类型的函数测试函数、基准测试benchmark函数、示例函数。一个测试函数是以Test为函数名前缀的函数用于测试程序的一些逻辑行为是否正确go test命令会调用这些测试函数并报告测试结果是PASS或FAIL。基准测试函数是以Benchmark为函数名前缀的函数它们用于衡量一些函数的性能go test命令会多次运行基准测试函数以计算一个平均的执行时间。示例函数是以Example为函数名前缀的函数提供一个由编译器保证正确性的示例文档。我们将在11.2节讨论测试函数的所有细节并在11.4节讨论基准测试函数的细节然后在11.6节讨论示例函数的细节。</p>
<p>go test命令会遍历所有的<code>*_test.go</code>文件中符合上述命名规则的函数生成一个临时的main包用于调用相应的测试函数接着构建并运行、报告测试结果最后清理测试中生成的临时文件。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="112-测试函数"><a class="header" href="#112-测试函数">11.2. 测试函数</a></h2>
<p>每个测试函数必须导入testing包。测试函数有如下的签名</p>
<pre><code class="language-Go">func TestName(t *testing.T) {
// ...
}
</code></pre>
<p>测试函数的名字必须以Test开头可选的后缀名必须以大写字母开头</p>
<pre><code class="language-Go">func TestSin(t *testing.T) { /* ... */ }
func TestCos(t *testing.T) { /* ... */ }
func TestLog(t *testing.T) { /* ... */ }
</code></pre>
<p>其中t参数用于报告测试失败和附加的日志信息。让我们定义一个实例包gopl.io/ch11/word1其中只有一个函数IsPalindrome用于检查一个字符串是否从前向后和从后向前读都是一样的。下面这个实现对于一个字符串是否是回文字符串前后重复测试了两次我们稍后会再讨论这个问题。</p>
<p><u><i>gopl.io/ch11/word1</i></u></p>
<pre><code class="language-Go">// Package word provides utilities for word games.
package word
// IsPalindrome reports whether s reads the same forward and backward.
// (Our first attempt.)
func IsPalindrome(s string) bool {
for i := range s {
if s[i] != s[len(s)-1-i] {
return false
}
}
return true
}
</code></pre>
<p>在相同的目录下word_test.go测试文件中包含了TestPalindrome和TestNonPalindrome两个测试函数。每一个都是测试IsPalindrome是否给出正确的结果并使用t.Error报告失败信息</p>
<pre><code class="language-Go">package word
import &quot;testing&quot;
func TestPalindrome(t *testing.T) {
if !IsPalindrome(&quot;detartrated&quot;) {
t.Error(`IsPalindrome(&quot;detartrated&quot;) = false`)
}
if !IsPalindrome(&quot;kayak&quot;) {
t.Error(`IsPalindrome(&quot;kayak&quot;) = false`)
}
}
func TestNonPalindrome(t *testing.T) {
if IsPalindrome(&quot;palindrome&quot;) {
t.Error(`IsPalindrome(&quot;palindrome&quot;) = true`)
}
}
</code></pre>
<p><code>go test</code>命令如果没有参数指定包那么将默认采用当前目录对应的包(和<code>go build</code>命令一样)。我们可以用下面的命令构建和运行测试。</p>
<pre><code>$ cd $GOPATH/src/gopl.io/ch11/word1
$ go test
ok gopl.io/ch11/word1 0.008s
</code></pre>
<p>结果还比较满意,我们运行了这个程序, 不过没有提前退出是因为还没有遇到BUG报告。不过一个法国名为“Noelle Eve Elleon”的用户会抱怨IsPalindrome函数不能识别“été”。另外一个来自美国中部用户的抱怨则是不能识别“A man, a plan, a canal: Panama.”。执行特殊和小的BUG报告为我们提供了新的更自然的测试用例。</p>
<pre><code class="language-Go">func TestFrenchPalindrome(t *testing.T) {
if !IsPalindrome(&quot;été&quot;) {
t.Error(`IsPalindrome(&quot;été&quot;) = false`)
}
}
func TestCanalPalindrome(t *testing.T) {
input := &quot;A man, a plan, a canal: Panama&quot;
if !IsPalindrome(input) {
t.Errorf(`IsPalindrome(%q) = false`, input)
}
}
</code></pre>
<p>为了避免两次输入较长的字符串我们使用了提供了有类似Printf格式化功能的 Errorf函数来汇报错误结果。</p>
<p>当添加了这两个测试用例之后,<code>go test</code>返回了测试失败的信息。</p>
<pre><code>$ go test
--- FAIL: TestFrenchPalindrome (0.00s)
word_test.go:28: IsPalindrome(&quot;été&quot;) = false
--- FAIL: TestCanalPalindrome (0.00s)
word_test.go:35: IsPalindrome(&quot;A man, a plan, a canal: Panama&quot;) = false
FAIL
FAIL gopl.io/ch11/word1 0.014s
</code></pre>
<p>先编写测试用例并观察到测试用例触发了和用户报告的错误相同的描述是一个好的测试习惯。只有这样,我们才能定位我们要真正解决的问题。</p>
<p>先写测试用例的另外的好处是,运行测试通常会比手工描述报告的处理更快,这让我们可以进行快速地迭代。如果测试集有很多运行缓慢的测试,我们可以通过只选择运行某些特定的测试来加快测试速度。</p>
<p>参数<code>-v</code>可用于打印每个测试函数的名字和运行时间:</p>
<pre><code>$ go test -v
=== RUN TestPalindrome
--- PASS: TestPalindrome (0.00s)
=== RUN TestNonPalindrome
--- PASS: TestNonPalindrome (0.00s)
=== RUN TestFrenchPalindrome
--- FAIL: TestFrenchPalindrome (0.00s)
word_test.go:28: IsPalindrome(&quot;été&quot;) = false
=== RUN TestCanalPalindrome
--- FAIL: TestCanalPalindrome (0.00s)
word_test.go:35: IsPalindrome(&quot;A man, a plan, a canal: Panama&quot;) = false
FAIL
exit status 1
FAIL gopl.io/ch11/word1 0.017s
</code></pre>
<p>参数<code>-run</code>对应一个正则表达式,只有测试函数名被它正确匹配的测试函数才会被<code>go test</code>测试命令运行:</p>
<pre><code>$ go test -v -run=&quot;French|Canal&quot;
=== RUN TestFrenchPalindrome
--- FAIL: TestFrenchPalindrome (0.00s)
word_test.go:28: IsPalindrome(&quot;été&quot;) = false
=== RUN TestCanalPalindrome
--- FAIL: TestCanalPalindrome (0.00s)
word_test.go:35: IsPalindrome(&quot;A man, a plan, a canal: Panama&quot;) = false
FAIL
exit status 1
FAIL gopl.io/ch11/word1 0.014s
</code></pre>
<p>当然,一旦我们已经修复了失败的测试用例,在我们提交代码更新之前,我们应该以不带参数的<code>go test</code>命令运行全部的测试用例,以确保修复失败测试的同时没有引入新的问题。</p>
<p>我们现在的任务就是修复这些错误。简要分析后发现第一个BUG的原因是我们采用了 byte而不是rune序列所以像“été”中的é等非ASCII字符不能正确处理。第二个BUG是因为没有忽略空格和字母的大小写导致的。</p>
<p>针对上述两个BUG我们仔细重写了函数</p>
<p><u><i>gopl.io/ch11/word2</i></u></p>
<pre><code class="language-Go">// Package word provides utilities for word games.
package word
import &quot;unicode&quot;
// IsPalindrome reports whether s reads the same forward and backward.
// Letter case is ignored, as are non-letters.
func IsPalindrome(s string) bool {
var letters []rune
for _, r := range s {
if unicode.IsLetter(r) {
letters = append(letters, unicode.ToLower(r))
}
}
for i := range letters {
if letters[i] != letters[len(letters)-1-i] {
return false
}
}
return true
}
</code></pre>
<p>同时我们也将之前的所有测试数据合并到了一个测试中的表格中。</p>
<pre><code class="language-Go">func TestIsPalindrome(t *testing.T) {
var tests = []struct {
input string
want bool
}{
{&quot;&quot;, true},
{&quot;a&quot;, true},
{&quot;aa&quot;, true},
{&quot;ab&quot;, false},
{&quot;kayak&quot;, true},
{&quot;detartrated&quot;, true},
{&quot;A man, a plan, a canal: Panama&quot;, true},
{&quot;Evil I did dwell; lewd did I live.&quot;, true},
{&quot;Able was I ere I saw Elba&quot;, true},
{&quot;été&quot;, true},
{&quot;Et se resservir, ivresse reste.&quot;, true},
{&quot;palindrome&quot;, false}, // non-palindrome
{&quot;desserts&quot;, false}, // semi-palindrome
}
for _, test := range tests {
if got := IsPalindrome(test.input); got != test.want {
t.Errorf(&quot;IsPalindrome(%q) = %v&quot;, test.input, got)
}
}
}
</code></pre>
<p>现在我们的新测试都通过了:</p>
<pre><code>$ go test gopl.io/ch11/word2
ok gopl.io/ch11/word2 0.015s
</code></pre>
<p>这种表格驱动的测试在Go语言中很常见。我们可以很容易地向表格添加新的测试数据并且后面的测试逻辑也没有冗余这样我们可以有更多的精力去完善错误信息。</p>
<p>失败测试的输出并不包括调用t.Errorf时刻的堆栈调用信息。和其他编程语言或测试框架的assert断言不同t.Errorf调用也没有引起panic异常或停止测试的执行。即使表格中前面的数据导致了测试的失败表格后面的测试数据依然会运行测试因此在一个测试中我们可能了解多个失败的信息。</p>
<p>如果我们真的需要停止测试或许是因为初始化失败或可能是早先的错误导致了后续错误等原因我们可以使用t.Fatal或t.Fatalf停止当前测试函数。它们必须在和测试函数同一个goroutine内调用。</p>
<p>测试失败的信息一般的形式是“f(x) = y, want z”其中f(x)解释了失败的操作和对应的输入y是实际的运行结果z是期望的正确的结果。就像前面检查回文字符串的例子实际的函数用于f(x)部分。显示x是表格驱动型测试中比较重要的部分因为同一个断言可能对应不同的表格项执行多次。要避免无用和冗余的信息。在测试类似IsPalindrome返回布尔类型的函数时可以忽略并没有额外信息的z部分。如果x、y或z是y的长度输出一个相关部分的简明总结即可。测试的作者应该要努力帮助程序员诊断测试失败的原因。</p>
<p><strong>练习 11.1:</strong> 为4.3节中的charcount程序编写测试。</p>
<p><strong>练习 11.2:</strong>§6.5的IntSet编写一组测试用于检查每个操作后的行为和基于内置map的集合等价后面练习11.7将会用到。</p>
<h3 id="1121-随机测试"><a class="header" href="#1121-随机测试">11.2.1. 随机测试</a></h3>
<p>表格驱动的测试便于构造基于精心挑选的测试数据的测试用例。另一种测试思路是随机测试,也就是通过构造更广泛的随机输入来测试探索函数的行为。</p>
<p>那么对于一个随机的输入,我们如何能知道希望的输出结果呢?这里有两种处理策略。第一个是编写另一个对照函数,使用简单和清晰的算法,虽然效率较低但是行为和要测试的函数是一致的,然后针对相同的随机输入检查两者的输出结果。第二种是生成的随机输入的数据遵循特定的模式,这样我们就可以知道期望的输出的模式。</p>
<p>下面的例子使用的是第二种方法randomPalindrome函数用于随机生成回文字符串。</p>
<pre><code class="language-Go">import &quot;math/rand&quot;
// randomPalindrome returns a palindrome whose length and contents
// are derived from the pseudo-random number generator rng.
func randomPalindrome(rng *rand.Rand) string {
n := rng.Intn(25) // random length up to 24
runes := make([]rune, n)
for i := 0; i &lt; (n+1)/2; i++ {
r := rune(rng.Intn(0x1000)) // random rune up to '\u0999'
runes[i] = r
runes[n-1-i] = r
}
return string(runes)
}
func TestRandomPalindromes(t *testing.T) {
// Initialize a pseudo-random number generator.
seed := time.Now().UTC().UnixNano()
t.Logf(&quot;Random seed: %d&quot;, seed)
rng := rand.New(rand.NewSource(seed))
for i := 0; i &lt; 1000; i++ {
p := randomPalindrome(rng)
if !IsPalindrome(p) {
t.Errorf(&quot;IsPalindrome(%q) = false&quot;, p)
}
}
}
</code></pre>
<p>虽然随机测试会有不确定因素但是它也是至关重要的我们可以从失败测试的日志获取足够的信息。在我们的例子中输入IsPalindrome的p参数将告诉我们真实的数据但是对于函数将接受更复杂的输入不需要保存所有的输入只要日志中简单地记录随机数种子即可像上面的方式。有了这些随机数初始化种子我们可以很容易修改测试代码以重现失败的随机测试。</p>
<p>通过使用当前时间作为随机种子,在整个过程中的每次运行测试命令时都将探索新的随机数据。如果你使用的是定期运行的自动化测试集成系统,随机测试将特别有价值。</p>
<p><strong>练习 11.3:</strong> TestRandomPalindromes测试函数只测试了回文字符串。编写新的随机测试生成器用于测试随机生成的非回文字符串。</p>
<p><strong>练习 11.4:</strong> 修改randomPalindrome函数以探索IsPalindrome是否对标点和空格做了正确处理。</p>
<p>译者注:<strong>拓展阅读</strong>感兴趣的读者可以再了解一下go-fuzz</p>
<h3 id="1122-测试一个命令"><a class="header" href="#1122-测试一个命令">11.2.2. 测试一个命令</a></h3>
<p>对于测试包<code>go test</code>是一个有用的工具,但是稍加努力我们也可以用它来测试可执行程序。如果一个包的名字是 main那么在构建时会生成一个可执行程序不过main包可以作为一个包被测试器代码导入。</p>
<p>让我们为2.3.2节的echo程序编写一个测试。我们先将程序拆分为两个函数echo函数完成真正的工作main函数用于处理命令行输入参数和echo可能返回的错误。</p>
<p><u><i>gopl.io/ch11/echo</i></u></p>
<pre><code class="language-Go">// Echo prints its command-line arguments.
package main
import (
&quot;flag&quot;
&quot;fmt&quot;
&quot;io&quot;
&quot;os&quot;
&quot;strings&quot;
)
var (
n = flag.Bool(&quot;n&quot;, false, &quot;omit trailing newline&quot;)
s = flag.String(&quot;s&quot;, &quot; &quot;, &quot;separator&quot;)
)
var out io.Writer = os.Stdout // modified during testing
func main() {
flag.Parse()
if err := echo(!*n, *s, flag.Args()); err != nil {
fmt.Fprintf(os.Stderr, &quot;echo: %v\n&quot;, err)
os.Exit(1)
}
}
func echo(newline bool, sep string, args []string) error {
fmt.Fprint(out, strings.Join(args, sep))
if newline {
fmt.Fprintln(out)
}
return nil
}
</code></pre>
<p>在测试中我们可以用各种参数和标志调用echo函数然后检测它的输出是否正确我们通过增加参数来减少echo函数对全局变量的依赖。我们还增加了一个全局名为out的变量来替代直接使用os.Stdout这样测试代码可以根据需要将out修改为不同的对象以便于检查。下面就是echo_test.go文件中的测试代码</p>
<pre><code class="language-Go">package main
import (
&quot;bytes&quot;
&quot;fmt&quot;
&quot;testing&quot;
)
func TestEcho(t *testing.T) {
var tests = []struct {
newline bool
sep string
args []string
want string
}{
{true, &quot;&quot;, []string{}, &quot;\n&quot;},
{false, &quot;&quot;, []string{}, &quot;&quot;},
{true, &quot;\t&quot;, []string{&quot;one&quot;, &quot;two&quot;, &quot;three&quot;}, &quot;one\ttwo\tthree\n&quot;},
{true, &quot;,&quot;, []string{&quot;a&quot;, &quot;b&quot;, &quot;c&quot;}, &quot;a,b,c\n&quot;},
{false, &quot;:&quot;, []string{&quot;1&quot;, &quot;2&quot;, &quot;3&quot;}, &quot;1:2:3&quot;},
}
for _, test := range tests {
descr := fmt.Sprintf(&quot;echo(%v, %q, %q)&quot;,
test.newline, test.sep, test.args)
out = new(bytes.Buffer) // captured output
if err := echo(test.newline, test.sep, test.args); err != nil {
t.Errorf(&quot;%s failed: %v&quot;, descr, err)
continue
}
got := out.(*bytes.Buffer).String()
if got != test.want {
t.Errorf(&quot;%s = %q, want %q&quot;, descr, got, test.want)
}
}
}
</code></pre>
<p>要注意的是测试代码和产品代码在同一个包。虽然是main包也有对应的main入口函数但是在测试的时候main包只是TestEcho测试函数导入的一个普通包里面main函数并没有被导出而是被忽略的。</p>
<p>通过将测试放到表格中,我们很容易添加新的测试用例。让我通过增加下面的测试用例来看看失败的情况是怎么样的:</p>
<pre><code class="language-Go">{true, &quot;,&quot;, []string{&quot;a&quot;, &quot;b&quot;, &quot;c&quot;}, &quot;a b c\n&quot;}, // NOTE: wrong expectation!
</code></pre>
<p><code>go test</code>输出如下:</p>
<pre><code>$ go test gopl.io/ch11/echo
--- FAIL: TestEcho (0.00s)
echo_test.go:31: echo(true, &quot;,&quot;, [&quot;a&quot; &quot;b&quot; &quot;c&quot;]) = &quot;a,b,c&quot;, want &quot;a b c\n&quot;
FAIL
FAIL gopl.io/ch11/echo 0.006s
</code></pre>
<p>错误信息描述了尝试的操作使用Go类似语法实际的结果和期望的结果。通过这样的错误信息你可以在检视代码之前就很容易定位错误的原因。</p>
<p>要注意的是在测试代码中并没有调用log.Fatal或os.Exit因为调用这类函数会导致程序提前退出调用这些函数的特权应该放在main函数中。如果真的有意外的事情导致函数发生panic异常测试驱动应该尝试用recover捕获异常然后将当前测试当作失败处理。如果是可预期的错误例如非法的用户输入、找不到文件或配置文件不当等应该通过返回一个非空的error的方式处理。幸运的是上面的意外只是一个插曲我们的echo示例是比较简单的也没有需要返回非空error的情况。</p>
<h3 id="1123-白盒测试"><a class="header" href="#1123-白盒测试">11.2.3. 白盒测试</a></h3>
<p>一种测试分类的方法是基于测试者是否需要了解被测试对象的内部工作原理。黑盒测试只需要测试包公开的文档和API行为内部实现对测试代码是透明的。相反白盒测试有访问包内部函数和数据结构的权限因此可以做到一些普通客户端无法实现的测试。例如一个白盒测试可以在每个操作之后检测不变量的数据类型。白盒测试只是一个传统的名称其实称为clear box测试会更准确。</p>
<p>黑盒和白盒这两种测试方法是互补的。黑盒测试一般更健壮随着软件实现的完善测试代码很少需要更新。它们可以帮助测试者了解真实客户的需求也可以帮助发现API设计的一些不足之处。相反白盒测试则可以对内部一些棘手的实现提供更多的测试覆盖。</p>
<p>我们已经看到两种测试的例子。TestIsPalindrome测试仅仅使用导出的IsPalindrome函数因此这是一个黑盒测试。TestEcho测试则调用了内部的echo函数并且更新了内部的out包级变量这两个都是未导出的因此这是白盒测试。</p>
<p>当我们准备TestEcho测试的时候我们修改了echo函数使用包级的out变量作为输出对象因此测试代码可以用另一个实现代替标准输出这样可以方便对比echo输出的数据。使用类似的技术我们可以将产品代码的其他部分也替换为一个容易测试的伪对象。使用伪对象的好处是我们可以方便配置容易预测更可靠也更容易观察。同时也可以避免一些不良的副作用例如更新生产数据库或信用卡消费行为。</p>
<p>下面的代码演示了为用户提供网络存储的web服务中的配额检测逻辑。当用户使用了超过90%的存储配额之后将发送提醒邮件。译注一般在实现业务机器监控包括磁盘、cpu、网络等的时候需要类似的到达阈值=&gt;触发报警的逻辑,所以是很实用的案例。)</p>
<p><u><i>gopl.io/ch11/storage1</i></u></p>
<pre><code class="language-Go">package storage
import (
&quot;fmt&quot;
&quot;log&quot;
&quot;net/smtp&quot;
)
func bytesInUse(username string) int64 { return 0 /* ... */ }
// Email sender configuration.
// NOTE: never put passwords in source code!
const sender = &quot;notifications@example.com&quot;
const password = &quot;correcthorsebatterystaple&quot;
const hostname = &quot;smtp.example.com&quot;
const template = `Warning: you are using %d bytes of storage,
%d%% of your quota.`
func CheckQuota(username string) {
used := bytesInUse(username)
const quota = 1000000000 // 1GB
percent := 100 * used / quota
if percent &lt; 90 {
return // OK
}
msg := fmt.Sprintf(template, used, percent)
auth := smtp.PlainAuth(&quot;&quot;, sender, password, hostname)
err := smtp.SendMail(hostname+&quot;:587&quot;, auth, sender,
[]string{username}, []byte(msg))
if err != nil {
log.Printf(&quot;smtp.SendMail(%s) failed: %s&quot;, username, err)
}
}
</code></pre>
<p>我们想测试这段代码但是我们并不希望发送真实的邮件。因此我们将邮件处理逻辑放到一个私有的notifyUser函数中。</p>
<p><u><i>gopl.io/ch11/storage2</i></u></p>
<pre><code class="language-Go">var notifyUser = func(username, msg string) {
auth := smtp.PlainAuth(&quot;&quot;, sender, password, hostname)
err := smtp.SendMail(hostname+&quot;:587&quot;, auth, sender,
[]string{username}, []byte(msg))
if err != nil {
log.Printf(&quot;smtp.SendEmail(%s) failed: %s&quot;, username, err)
}
}
func CheckQuota(username string) {
used := bytesInUse(username)
const quota = 1000000000 // 1GB
percent := 100 * used / quota
if percent &lt; 90 {
return // OK
}
msg := fmt.Sprintf(template, used, percent)
notifyUser(username, msg)
}
</code></pre>
<p>现在我们可以在测试中用伪邮件发送函数替代真实的邮件发送函数。它只是简单记录要通知的用户和邮件的内容。</p>
<pre><code class="language-Go">package storage
import (
&quot;strings&quot;
&quot;testing&quot;
)
func TestCheckQuotaNotifiesUser(t *testing.T) {
var notifiedUser, notifiedMsg string
notifyUser = func(user, msg string) {
notifiedUser, notifiedMsg = user, msg
}
// ...simulate a 980MB-used condition...
const user = &quot;joe@example.org&quot;
CheckQuota(user)
if notifiedUser == &quot;&quot; &amp;&amp; notifiedMsg == &quot;&quot; {
t.Fatalf(&quot;notifyUser not called&quot;)
}
if notifiedUser != user {
t.Errorf(&quot;wrong user (%s) notified, want %s&quot;,
notifiedUser, user)
}
const wantSubstring = &quot;98% of your quota&quot;
if !strings.Contains(notifiedMsg, wantSubstring) {
t.Errorf(&quot;unexpected notification message &lt;&lt;%s&gt;&gt;, &quot;+
&quot;want substring %q&quot;, notifiedMsg, wantSubstring)
}
}
</code></pre>
<p>这里有一个问题当测试函数返回后CheckQuota将不能正常工作因为notifyUsers依然使用的是测试函数的伪发送邮件函数当更新全局对象的时候总会有这种风险。 我们必须修改测试代码恢复notifyUsers原先的状态以便后续其他的测试没有影响要确保所有的执行路径后都能恢复包括测试失败或panic异常的情形。在这种情况下我们建议使用defer语句来延后执行处理恢复的代码。</p>
<pre><code class="language-Go">func TestCheckQuotaNotifiesUser(t *testing.T) {
// Save and restore original notifyUser.
saved := notifyUser
defer func() { notifyUser = saved }()
// Install the test's fake notifyUser.
var notifiedUser, notifiedMsg string
notifyUser = func(user, msg string) {
notifiedUser, notifiedMsg = user, msg
}
// ...rest of test...
}
</code></pre>
<p>这种处理模式可以用来暂时保存和恢复所有的全局变量,包括命令行标志参数、调试选项和优化参数;安装和移除导致生产代码产生一些调试信息的钩子函数;还有有些诱导生产代码进入某些重要状态的改变,比如超时、错误,甚至是一些刻意制造的并发行为等因素。</p>
<p>以这种方式使用全局变量是安全的因为go test命令并不会同时并发地执行多个测试。</p>
<h3 id="1124-外部测试包"><a class="header" href="#1124-外部测试包">11.2.4. 外部测试包</a></h3>
<p>考虑下这两个包net/url包提供了URL解析的功能net/http包提供了web服务和HTTP客户端的功能。如我们所料上层的net/http包依赖下层的net/url包。然后net/url包中的一个测试是演示不同URL和HTTP客户端的交互行为。也就是说一个下层包的测试代码导入了上层的包。</p>
<p><img src="ch11/../images/ch11-01.png" alt="" /></p>
<p>这样的行为在net/url包的测试代码中会导致包的循环依赖正如图11.1中向上箭头所示同时正如我们在10.1节所讲的Go语言规范是禁止包的循环依赖的。</p>
<p>不过我们可以通过外部测试包的方式解决循环依赖的问题也就是在net/url包所在的目录声明一个独立的url_test测试包。其中包名的<code>_test</code>后缀告诉go test工具它应该建立一个额外的包来运行测试。我们将这个外部测试包的导入路径视作是net/url_test会更容易理解但实际上它并不能被其他任何包导入。</p>
<p>因为外部测试包是一个独立的包,所以能够导入那些<code>依赖待测代码本身</code>的其他辅助包包内的测试代码就无法做到这点。在设计层面外部测试包是在所有它依赖的包的上层正如图11.2所示。</p>
<p><img src="ch11/../images/ch11-02.png" alt="" /></p>
<p>通过避免循环的导入依赖,外部测试包可以更灵活地编写测试,特别是集成测试(需要测试多个组件之间的交互),可以像普通应用程序那样自由地导入其他包。</p>
<p>我们可以用go list命令查看包对应目录中哪些Go源文件是产品代码哪些是包内测试还有哪些是外部测试包。我们以fmt包作为一个例子GoFiles表示产品代码对应的Go源文件列表也就是go build命令要编译的部分。</p>
<pre><code>$ go list -f={{.GoFiles}} fmt
[doc.go format.go print.go scan.go]
</code></pre>
<p>TestGoFiles表示的是fmt包内部测试代码以_test.go为后缀文件名不过只在测试时被构建</p>
<pre><code>$ go list -f={{.TestGoFiles}} fmt
[export_test.go]
</code></pre>
<p>包的测试代码通常都在这些文件中不过fmt包并非如此稍后我们再解释export_test.go文件的作用。</p>
<p>XTestGoFiles表示的是属于外部测试包的测试代码也就是fmt_test包因此它们必须先导入fmt包。同样这些文件也只是在测试时被构建运行</p>
<pre><code>$ go list -f={{.XTestGoFiles}} fmt
[fmt_test.go scan_test.go stringer_test.go]
</code></pre>
<p>有时候外部测试包也需要访问被测试包内部的代码例如在一个为了避免循环导入而被独立到外部测试包的白盒测试。在这种情况下我们可以通过一些技巧解决我们在包内的一个_test.go文件中导出一个内部的实现给外部测试包。因为这些代码只有在测试时才需要因此一般会放在export_test.go文件中。</p>
<p>例如fmt包的fmt.Scanf函数需要unicode.IsSpace函数提供的功能。但是为了避免太多的依赖fmt包并没有导入包含巨大表格数据的unicode包相反fmt包有一个叫isSpace内部的简易实现。</p>
<p>为了确保fmt.isSpace和unicode.IsSpace函数的行为保持一致fmt包谨慎地包含了一个测试。一个在外部测试包内的白盒测试是无法直接访问到isSpace内部函数的因此fmt通过一个后门导出了isSpace函数。export_test.go文件就是专门用于外部测试包的后门。</p>
<pre><code class="language-Go">package fmt
var IsSpace = isSpace
</code></pre>
<p>这个测试文件并没有定义测试代码它只是通过fmt.IsSpace简单导出了内部的isSpace函数提供给外部测试包使用。这个技巧可以广泛用于位于外部测试包的白盒测试。</p>
<h3 id="1125-编写有效的测试"><a class="header" href="#1125-编写有效的测试">11.2.5. 编写有效的测试</a></h3>
<p>许多Go语言新人会惊异于Go语言极简的测试框架。很多其它语言的测试框架都提供了识别测试函数的机制通常使用反射或元数据通过设置一些“setup”和“teardown”的钩子函数来执行测试用例运行的初始化和之后的清理操作同时测试工具箱还提供了很多类似assert断言、值比较函数、格式化输出错误信息和停止一个失败的测试等辅助函数通常使用异常机制。虽然这些机制可以使得测试非常简洁但是测试输出的日志却会像火星文一般难以理解。此外虽然测试最终也会输出PASS或FAIL的报告但是它们提供的信息格式却非常不利于代码维护者快速定位问题因为失败信息的具体含义非常隐晦比如“assert: 0 == 1”或成页的海量跟踪日志。</p>
<p>Go语言的测试风格则形成鲜明对比。它期望测试者自己完成大部分的工作定义函数避免重复就像普通编程那样。编写测试并不是一个机械的填空过程一个测试也有自己的接口尽管它的维护者也是测试仅有的一个用户。一个好的测试不应该引发其他无关的错误信息它只要清晰简洁地描述问题的症状即可有时候可能还需要一些上下文信息。在理想情况下维护者可以在不看代码的情况下就能根据错误信息定位错误产生的原因。一个好的测试不应该在遇到一点小错误时就立刻退出测试它应该尝试报告更多的相关的错误信息因为我们可能从多个失败测试的模式中发现错误产生的规律。</p>
<p>下面的断言函数比较两个值,然后生成一个通用的错误信息,并停止程序。它很好用也确实有效,但是当测试失败的时候,打印的错误信息却几乎是没有价值的。它并没有为快速解决问题提供一个很好的入口。</p>
<pre><code class="language-Go">import (
&quot;fmt&quot;
&quot;strings&quot;
&quot;testing&quot;
)
// A poor assertion function.
func assertEqual(x, y int) {
if x != y {
panic(fmt.Sprintf(&quot;%d != %d&quot;, x, y))
}
}
func TestSplit(t *testing.T) {
words := strings.Split(&quot;a:b:c&quot;, &quot;:&quot;)
assertEqual(len(words), 3)
// ...
}
</code></pre>
<p>从这个意义上说,断言函数犯了过早抽象的错误:仅仅测试两个整数是否相同,而没能根据上下文提供更有意义的错误信息。我们可以根据具体的错误打印一个更有价值的错误信息,就像下面例子那样。只有在测试中出现重复模式时才采用抽象。</p>
<pre><code class="language-Go">func TestSplit(t *testing.T) {
s, sep := &quot;a:b:c&quot;, &quot;:&quot;
words := strings.Split(s, sep)
if got, want := len(words), 3; got != want {
t.Errorf(&quot;Split(%q, %q) returned %d words, want %d&quot;,
s, sep, got, want)
}
// ...
}
</code></pre>
<p>现在的测试不仅报告了调用的具体函数、它的输入和结果的意义并且打印的真实返回的值和期望返回的值并且即使断言失败依然会继续尝试运行更多的测试。一旦我们写了这样结构的测试下一步自然不是用更多的if语句来扩展测试用例我们可以用像IsPalindrome的表驱动测试那样来准备更多的s和sep测试用例。</p>
<p>前面的例子并不需要额外的辅助函数如果有可以使测试代码更简单的方法我们也乐意接受。我们将在13.3节看到一个类似reflect.DeepEqual辅助函数。一个好的测试的关键是首先实现你期望的具体行为然后才是考虑简化测试代码、避免重复。如果直接从抽象、通用的测试库着手很难取得良好结果。</p>
<p><strong>练习11.5:</strong> 用表格驱动的技术扩展TestSplit测试并打印期望的输出结果。</p>
<h3 id="1126-避免脆弱的测试"><a class="header" href="#1126-避免脆弱的测试">11.2.6. 避免脆弱的测试</a></h3>
<p>如果一个应用程序对于新出现的但有效的输入经常失败说明程序容易出bug不够稳健同样如果一个测试仅仅对程序做了微小变化就失败则称为脆弱。就像一个不够稳健的程序会挫败它的用户一样一个脆弱的测试同样会激怒它的维护者。最脆弱的测试代码会在程序没有任何变化的时候产生不同的结果时好时坏处理它们会耗费大量的时间但是并不会得到任何好处。</p>
<p>当一个测试函数会产生一个复杂的输出如一个很长的字符串、一个精心设计的数据结构或一个文件时,人们很容易想预先写下一系列固定的用于对比的标杆数据。但是随着项目的发展,有些输出可能会发生变化,尽管很可能是一个改进的实现导致的。而且不仅仅是输出部分,函数复杂的输入部分可能也跟着变化了,因此测试使用的输入也就不再有效了。</p>
<p>避免脆弱测试代码的方法是只检测你真正关心的属性。保持测试代码的简洁和内部结构的稳定。特别是对断言部分要有所选择。不要对字符串进行全字匹配,而是针对那些在项目的发展中是比较稳定不变的子串。很多时候值得花力气来编写一个从复杂输出中提取用于断言的必要信息的函数,虽然这可能会带来很多前期的工作,但是它可以帮助迅速及时修复因为项目演化而导致的不合逻辑的失败测试。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="113-测试覆盖率"><a class="header" href="#113-测试覆盖率">11.3. 测试覆盖率</a></h2>
<p>就其性质而言测试不可能是完整的。计算机科学家Edsger Dijkstra曾说过“测试能证明缺陷存在而无法证明没有缺陷。”再多的测试也不能证明一个程序没有BUG。在最好的情况下测试可以增强我们的信心代码在很多重要场景下是可以正常工作的。</p>
<p>对待测程序执行的测试的程度称为测试的覆盖率。测试覆盖率并不能量化——即使最简单的程序的动态也是难以精确测量的——但是有启发式方法来帮助我们编写有效的测试代码。</p>
<p>这些启发式方法中,语句的覆盖率是最简单和最广泛使用的。语句的覆盖率是指在测试中至少被运行一次的代码占总代码数的比例。在本节中,我们使用<code>go test</code>命令中集成的测试覆盖率工具,来度量下面代码的测试覆盖率,帮助我们识别测试和我们期望间的差距。</p>
<p>下面的代码是一个表格驱动的测试,用于测试第七章的表达式求值程序:</p>
<p><u><i>gopl.io/ch7/eval</i></u></p>
<pre><code class="language-Go">func TestCoverage(t *testing.T) {
var tests = []struct {
input string
env Env
want string // expected error from Parse/Check or result from Eval
}{
{&quot;x % 2&quot;, nil, &quot;unexpected '%'&quot;},
{&quot;!true&quot;, nil, &quot;unexpected '!'&quot;},
{&quot;log(10)&quot;, nil, `unknown function &quot;log&quot;`},
{&quot;sqrt(1, 2)&quot;, nil, &quot;call to sqrt has 2 args, want 1&quot;},
{&quot;sqrt(A / pi)&quot;, Env{&quot;A&quot;: 87616, &quot;pi&quot;: math.Pi}, &quot;167&quot;},
{&quot;pow(x, 3) + pow(y, 3)&quot;, Env{&quot;x&quot;: 9, &quot;y&quot;: 10}, &quot;1729&quot;},
{&quot;5 / 9 * (F - 32)&quot;, Env{&quot;F&quot;: -40}, &quot;-40&quot;},
}
for _, test := range tests {
expr, err := Parse(test.input)
if err == nil {
err = expr.Check(map[Var]bool{})
}
if err != nil {
if err.Error() != test.want {
t.Errorf(&quot;%s: got %q, want %q&quot;, test.input, err, test.want)
}
continue
}
got := fmt.Sprintf(&quot;%.6g&quot;, expr.Eval(test.env))
if got != test.want {
t.Errorf(&quot;%s: %v =&gt; %s, want %s&quot;,
test.input, test.env, got, test.want)
}
}
}
</code></pre>
<p>首先,我们要确保所有的测试都正常通过:</p>
<pre><code>$ go test -v -run=Coverage gopl.io/ch7/eval
=== RUN TestCoverage
--- PASS: TestCoverage (0.00s)
PASS
ok gopl.io/ch7/eval 0.011s
</code></pre>
<p>下面这个命令可以显示测试覆盖率工具的使用用法:</p>
<pre><code>$ go tool cover
Usage of 'go tool cover':
Given a coverage profile produced by 'go test':
go test -coverprofile=c.out
Open a web browser displaying annotated source code:
go tool cover -html=c.out
...
</code></pre>
<p><code>go tool</code>命令运行Go工具链的底层可执行程序。这些底层可执行程序放在$GOROOT/pkg/tool/${GOOS}_${GOARCH}目录。因为有<code>go build</code>命令的原因,我们很少直接调用这些底层工具。</p>
<p>现在我们可以用<code>-coverprofile</code>标志参数重新运行测试:</p>
<pre><code>$ go test -run=Coverage -coverprofile=c.out gopl.io/ch7/eval
ok gopl.io/ch7/eval 0.032s coverage: 68.5% of statements
</code></pre>
<p>这个标志参数通过在测试代码中插入生成钩子来统计覆盖率数据。也就是说在运行每个测试前它将待测代码拷贝一份并做修改在每个词法块都会设置一个布尔标志变量。当被修改后的被测试代码运行退出时将统计日志数据写入c.out文件并打印一部分执行的语句的一个总结。如果你需要的是摘要使用<code>go test -cover</code>。)</p>
<p>如果使用了<code>-covermode=count</code>标志参数,那么将在每个代码块插入一个计数器而不是布尔标志量。在统计结果中记录了每个块的执行次数,这可以用于衡量哪些是被频繁执行的热点代码。</p>
<p>为了收集数据我们运行了测试覆盖率工具打印了测试日志生成一个HTML报告然后在浏览器中打开图11.3)。</p>
<pre><code>$ go tool cover -html=c.out
</code></pre>
<p><img src="ch11/../images/ch11-03.png" alt="" /></p>
<p>绿色的代码块被测试覆盖到了红色的则表示没有被覆盖到。为了清晰起见我们将背景红色文本的背景设置成了阴影效果。我们可以马上发现unary操作的Eval方法并没有被执行到。如果我们针对这部分未被覆盖的代码添加下面的测试用例然后重新运行上面的命令那么我们将会看到那个红色部分的代码也变成绿色了</p>
<pre><code>{&quot;-x * -x&quot;, eval.Env{&quot;x&quot;: 2}, &quot;4&quot;}
</code></pre>
<p>不过两个panic语句依然是红色的。这是没有问题的因为这两个语句并不会被执行到。</p>
<p>实现100%的测试覆盖率听起来很美但是在具体实践中通常是不可行的也不是值得推荐的做法。因为那只能说明代码被执行过而已并不意味着代码就是没有BUG的因为对于逻辑复杂的语句需要针对不同的输入执行多次。有一些语句例如上面的panic语句则永远都不会被执行到。另外还有一些隐晦的错误在现实中很少遇到也很难编写对应的测试代码。测试从本质上来说是一个比较务实的工作编写测试代码和编写应用代码的成本对比是需要考虑的。测试覆盖率工具可以帮助我们快速识别测试薄弱的地方但是设计好的测试用例和编写应用代码一样需要严密的思考。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="114-基准测试"><a class="header" href="#114-基准测试">11.4. 基准测试</a></h2>
<p>基准测试是测量一个程序在固定工作负载下的性能。在Go语言中基准测试函数和普通测试函数写法类似但是以Benchmark为前缀名并且带有一个<code>*testing.B</code>类型的参数;<code>*testing.B</code>参数除了提供和<code>*testing.T</code>类似的方法还有额外一些和性能测量相关的方法。它还提供了一个整数N用于指定操作执行的循环次数。</p>
<p>下面是IsPalindrome函数的基准测试其中循环将执行N次。</p>
<pre><code class="language-Go">import &quot;testing&quot;
func BenchmarkIsPalindrome(b *testing.B) {
for i := 0; i &lt; b.N; i++ {
IsPalindrome(&quot;A man, a plan, a canal: Panama&quot;)
}
}
</code></pre>
<p>我们用下面的命令运行基准测试。和普通测试不同的是,默认情况下不运行任何基准测试。我们需要通过<code>-bench</code>命令行标志参数手工指定要运行的基准测试函数。该参数是一个正则表达式,用于匹配要执行的基准测试函数的名字,默认值是空的。其中“.”模式将可以匹配所有基准测试函数,但因为这里只有一个基准测试函数,因此和<code>-bench=IsPalindrome</code>参数是等价的效果。</p>
<pre><code>$ cd $GOPATH/src/gopl.io/ch11/word2
$ go test -bench=.
PASS
BenchmarkIsPalindrome-8 1000000 1035 ns/op
ok gopl.io/ch11/word2 2.179s
</code></pre>
<p>结果中基准测试名的数字后缀部分这里是8表示运行时对应的GOMAXPROCS的值这对于一些与并发相关的基准测试是重要的信息。</p>
<p>报告显示每次调用IsPalindrome函数花费1.035微秒是执行1,000,000次的平均时间。因为基准测试驱动器开始时并不知道每个基准测试函数运行所花的时间它会尝试在真正运行基准测试前先尝试用较小的N运行测试来估算基准测试函数所需要的时间然后推断一个较大的时间保证稳定的测量结果。</p>
<p>循环在基准测试函数内实现而不是放在基准测试框架内实现这样可以让每个基准测试函数有机会在循环启动前执行初始化代码这样并不会显著影响每次迭代的平均运行时间。如果还是担心初始化代码部分对测量时间带来干扰那么可以通过testing.B参数提供的方法来临时关闭或重置计时器不过这些一般很少会用到。</p>
<p>现在我们有了一个基准测试和普通测试我们可以很容易测试改进程序运行速度的想法。也许最明显的优化是在IsPalindrome函数中第二个循环的停止检查这样可以避免每个比较都做两次</p>
<pre><code class="language-Go">n := len(letters)/2
for i := 0; i &lt; n; i++ {
if letters[i] != letters[len(letters)-1-i] {
return false
}
}
return true
</code></pre>
<p>不过很多情况下一个显而易见的优化未必能带来预期的效果。这个改进在基准测试中只带来了4%的性能提升。</p>
<pre><code>$ go test -bench=.
PASS
BenchmarkIsPalindrome-8 1000000 992 ns/op
ok gopl.io/ch11/word2 2.093s
</code></pre>
<p>另一个改进想法是在开始为每个字符预先分配一个足够大的数组这样就可以避免在append调用时可能会导致内存的多次重新分配。声明一个letters数组变量并指定合适的大小像下面这样</p>
<pre><code class="language-Go">letters := make([]rune, 0, len(s))
for _, r := range s {
if unicode.IsLetter(r) {
letters = append(letters, unicode.ToLower(r))
}
}
</code></pre>
<p>这个改进提升性能约35%报告结果是基于2,000,000次迭代的平均运行时间统计。</p>
<pre><code>$ go test -bench=.
PASS
BenchmarkIsPalindrome-8 2000000 697 ns/op
ok gopl.io/ch11/word2 1.468s
</code></pre>
<p>如这个例子所示,快的程序往往是伴随着较少的内存分配。<code>-benchmem</code>命令行标志参数将在报告中包含内存的分配数据统计。我们可以比较优化前后内存的分配情况:</p>
<pre><code>$ go test -bench=. -benchmem
PASS
BenchmarkIsPalindrome 1000000 1026 ns/op 304 B/op 4 allocs/op
</code></pre>
<p>这是优化之后的结果:</p>
<pre><code>$ go test -bench=. -benchmem
PASS
BenchmarkIsPalindrome 2000000 807 ns/op 128 B/op 1 allocs/op
</code></pre>
<p>用一次内存分配代替多次的内存分配节省了75%的分配调用次数和减少近一半的内存需求。</p>
<p>这个基准测试告诉了我们某个具体操作所需的绝对时间但我们往往想知道的是两个不同的操作的时间对比。例如如果一个函数需要1ms处理1,000个元素那么处理10000或1百万将需要多少时间呢这样的比较揭示了渐近增长函数的运行时间。另一个例子I/O缓存该设置为多大呢基准测试可以帮助我们选择在性能达标情况下所需的最小内存。第三个例子对于一个确定的工作哪种算法更好基准测试可以评估两种不同算法对于相同的输入在不同的场景和负载下的优缺点。</p>
<p>比较型的基准测试就是普通程序代码。它们通常是单参数的函数,由几个不同数量级的基准测试函数调用,就像这样:</p>
<pre><code class="language-Go">func benchmark(b *testing.B, size int) { /* ... */ }
func Benchmark10(b *testing.B) { benchmark(b, 10) }
func Benchmark100(b *testing.B) { benchmark(b, 100) }
func Benchmark1000(b *testing.B) { benchmark(b, 1000) }
</code></pre>
<p>通过函数参数来指定输入的大小但是参数变量对于每个具体的基准测试都是固定的。要避免直接修改b.N来控制输入的大小。除非你将它作为一个固定大小的迭代计算输入否则基准测试的结果将毫无意义。</p>
<p>比较型的基准测试反映出的模式在程序设计阶段是很有帮助的,但是即使程序完工了也应当保留基准测试代码。因为随着项目的发展,或者是输入的增加,或者是部署到新的操作系统或不同的处理器,我们可以再次用基准测试来帮助我们改进设计。</p>
<p><strong>练习 11.6:</strong> 为2.6.2节的练习2.4和练习2.5的PopCount函数编写基准测试。看看基于表格算法在不同情况下对提升性能会有多大帮助。</p>
<p><strong>练习 11.7:</strong><code>*IntSet</code>§6.5的Add、UnionWith和其他方法编写基准测试使用大量随机输入。你可以让这些方法跑多快选择字的大小对于性能的影响如何IntSet和基于内建map的实现相比有多快</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="115-剖析"><a class="header" href="#115-剖析">11.5. 剖析</a></h2>
<p>基准测试Benchmark对于衡量特定操作的性能是有帮助的但是当我们试图让程序跑的更快的时候我们通常并不知道从哪里开始优化。每个码农都应该知道Donald Knuth在1974年的“Structured Programming with go to Statements”上所说的格言。虽然经常被解读为不重视性能的意思但是从原文我们可以看到不同的含义</p>
<blockquote>
<p>毫无疑问对效率的片面追求会导致各种滥用。程序员会浪费大量的时间在非关键程序的速度上实际上这些尝试提升效率的行为反倒可能产生很大的负面影响特别是当调试和维护的时候。我们不应该过度纠结于细节的优化应该说约97%的场景:过早的优化是万恶之源。</p>
<p>当然我们也不应该放弃对那关键3%的优化。一个好的程序员不会因为这个比例小就裹足不前,他们会明智地观察和识别哪些是关键的代码;但是仅当关键代码已经被确认的前提下才会进行优化。对于很多程序员来说,判断哪部分是关键的性能瓶颈,是很容易犯经验上的错误的,因此一般应该借助测量工具来证明。</p>
</blockquote>
<p>当我们想仔细观察我们程序的运行速度的时候,最好的方法是性能剖析。剖析技术是基于程序执行期间一些自动抽样,然后在收尾时进行推断;最后产生的统计结果就称为剖析数据。</p>
<p>Go语言支持多种类型的剖析性能分析每一种关注不同的方面但它们都涉及到每个采样记录的感兴趣的一系列事件消息每个事件都包含函数调用时函数调用堆栈的信息。内建的<code>go test</code>工具对几种分析方式都提供了支持。</p>
<p>CPU剖析数据标识了最耗CPU时间的函数。在每个CPU上运行的线程在每隔几毫秒都会遇到操作系统的中断事件每次中断时都会记录一个剖析数据然后恢复正常的运行。</p>
<p>堆剖析则标识了最耗内存的语句。剖析库会记录调用内部内存分配的操作平均每512KB的内存申请会触发一个剖析数据。</p>
<p>阻塞剖析则记录阻塞goroutine最久的操作例如系统调用、管道发送和接收还有获取锁等。每当goroutine被这些操作阻塞时剖析库都会记录相应的事件。</p>
<p>只需要开启下面其中一个标志参数就可以生成各种分析文件。当同时使用多个标志参数时需要当心,因为一项分析操作可能会影响其他项的分析结果。</p>
<pre><code>$ go test -cpuprofile=cpu.out
$ go test -blockprofile=block.out
$ go test -memprofile=mem.out
</code></pre>
<p>对于一些非测试程序也很容易进行剖析具体的实现方式与程序是短时间运行的小工具还是长时间运行的服务会有很大不同。剖析对于长期运行的程序尤其有用因此可以通过调用Go的runtime API来启用运行时剖析。</p>
<p>一旦我们已经收集到了用于分析的采样数据我们就可以使用pprof来分析这些数据。这是Go工具箱自带的一个工具但并不是一个日常工具它对应<code>go tool pprof</code>命令。该命令有许多特性和选项,但是最基本的是两个参数:生成这个概要文件的可执行程序和对应的剖析数据。</p>
<p>为了提高分析效率和减少空间分析日志本身并不包含函数的名字它只包含函数对应的地址。也就是说pprof需要对应的可执行程序来解读剖析数据。虽然<code>go test</code>通常在测试完成后就丢弃临时用的测试程序但是在启用分析的时候会将测试程序保存为foo.test文件其中foo部分对应待测包的名字。</p>
<p>下面的命令演示了如何收集并展示一个CPU分析文件。我们选择<code>net/http</code>包的一个基准测试为例。通常最好是对业务关键代码的部分设计专门的基准测试。因为简单的基准测试几乎没法代表业务场景,因此我们用-run=NONE参数禁止那些简单测试。</p>
<pre><code>$ go test -run=NONE -bench=ClientServerParallelTLS64 \
-cpuprofile=cpu.log net/http
PASS
BenchmarkClientServerParallelTLS64-8 1000
3141325 ns/op 143010 B/op 1747 allocs/op
ok net/http 3.395s
$ go tool pprof -text -nodecount=10 ./http.test cpu.log
2570ms of 3590ms total (71.59%)
Dropped 129 nodes (cum &lt;= 17.95ms)
Showing top 10 nodes out of 166 (cum &gt;= 60ms)
flat flat% sum% cum cum%
1730ms 48.19% 48.19% 1750ms 48.75% crypto/elliptic.p256ReduceDegree
230ms 6.41% 54.60% 250ms 6.96% crypto/elliptic.p256Diff
120ms 3.34% 57.94% 120ms 3.34% math/big.addMulVVW
110ms 3.06% 61.00% 110ms 3.06% syscall.Syscall
90ms 2.51% 63.51% 1130ms 31.48% crypto/elliptic.p256Square
70ms 1.95% 65.46% 120ms 3.34% runtime.scanobject
60ms 1.67% 67.13% 830ms 23.12% crypto/elliptic.p256Mul
60ms 1.67% 68.80% 190ms 5.29% math/big.nat.montgomery
50ms 1.39% 70.19% 50ms 1.39% crypto/elliptic.p256ReduceCarry
50ms 1.39% 71.59% 60ms 1.67% crypto/elliptic.p256Sum
</code></pre>
<p>参数<code>-text</code>用于指定输出格式在这里每行是一个函数根据使用CPU的时间长短来排序。其中<code>-nodecount=10</code>参数限制了只输出前10行的结果。对于严重的性能问题这个文本格式基本可以帮助查明原因了。</p>
<p>这个概要文件告诉我们HTTPS基准测试中<code>crypto/elliptic.p256ReduceDegree</code>函数占用了将近一半的CPU资源对性能占很大比重。相比之下如果一个概要文件中主要是runtime包的内存分配的函数那么减少内存消耗可能是一个值得尝试的优化策略。</p>
<p>对于一些更微妙的问题你可能需要使用pprof的图形显示功能。这个需要安装GraphViz工具可以从 http://www.graphviz.org 下载。参数<code>-web</code>用于生成函数的有向图标注有CPU的使用和最热点的函数等信息。</p>
<p>这一节我们只是简单看了下Go语言的数据分析工具。如果想了解更多可以阅读Go官方博客的“Profiling Go Programs”一文。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="116-示例函数"><a class="header" href="#116-示例函数">11.6. 示例函数</a></h2>
<p>第三种被<code>go test</code>特别对待的函数是示例函数以Example为函数名开头。示例函数没有函数参数和返回值。下面是IsPalindrome函数对应的示例函数</p>
<pre><code class="language-Go">func ExampleIsPalindrome() {
fmt.Println(IsPalindrome(&quot;A man, a plan, a canal: Panama&quot;))
fmt.Println(IsPalindrome(&quot;palindrome&quot;))
// Output:
// true
// false
}
</code></pre>
<p>示例函数有三个用处。最主要的一个是作为文档一个包的例子可以更简洁直观的方式来演示函数的用法比文字描述更直接易懂特别是作为一个提醒或快速参考时。一个示例函数也可以方便展示属于同一个接口的几种类型或函数之间的关系所有的文档都必须关联到一个地方就像一个类型或函数声明都统一到包一样。同时示例函数和注释并不一样示例函数是真实的Go代码需要接受编译器的编译时检查这样可以保证源代码更新时示例代码不会脱节。</p>
<p>根据示例函数的后缀名部分godoc这个web文档服务器会将示例函数关联到某个具体函数或包本身因此ExampleIsPalindrome示例函数将是IsPalindrome函数文档的一部分Example示例函数将是包文档的一部分。</p>
<p>示例函数的第二个用处是,在<code>go test</code>执行测试的时候也会运行示例函数测试。如果示例函数内含有类似上面例子中的<code>// Output:</code>格式的注释,那么测试工具会执行这个示例函数,然后检查示例函数的标准输出与注释是否匹配。</p>
<p>示例函数的第三个目的提供一个真实的演练场。 http://golang.org 就是由godoc提供的文档服务它使用了Go Playground让用户可以在浏览器中在线编辑和运行每个示例函数就像图11.4所示的那样。这通常是学习函数使用或Go语言特性最快捷的方式。</p>
<p><img src="ch11/../images/ch11-04.png" alt="" /></p>
<p>本书最后的两章是讨论reflect和unsafe包一般的Go程序员很少使用它们事实上也很少需要用到。因此如果你还没有写过任何真实的Go程序的话现在可以先去写些代码了。</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="第12章-反射"><a class="header" href="#第12章-反射">第12章 反射</a></h1>
<p>Go语言提供了一种机制能够在运行时更新变量和检查它们的值、调用它们的方法和它们支持的内在操作而不需要在编译时就知道这些变量的具体类型。这种机制被称为反射。反射也可以让我们将类型本身作为第一类的值类型处理。</p>
<p>在本章我们将探讨Go语言的反射特性看看它可以给语言增加哪些表达力以及在两个至关重要的API是如何使用反射机制的一个是fmt包提供的字符串格式化功能另一个是类似encoding/json和encoding/xml提供的针对特定协议的编解码功能。对于我们在4.6节中看到过的text/template和html/template包它们的实现也是依赖反射技术的。然后反射是一个复杂的内省技术不应该随意使用因此尽管上面这些包内部都是用反射技术实现的但是它们自己的API都没有公开反射相关的接口。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="121-为何需要反射"><a class="header" href="#121-为何需要反射">12.1. 为何需要反射?</a></h2>
<p>有时候我们需要编写一个函数能够处理一类并不满足普通公共接口的类型的值,也可能是因为它们并没有确定的表示方式,或者是在我们设计该函数的时候这些类型可能还不存在。</p>
<p>一个大家熟悉的例子是fmt.Fprintf函数提供的字符串格式化处理逻辑它可以用来对任意类型的值格式化并打印甚至支持用户自定义的类型。让我们也来尝试实现一个类似功能的函数。为了简单起见我们的函数只接收一个参数然后返回和fmt.Sprint类似的格式化后的字符串。我们实现的函数名也叫Sprint。</p>
<p>我们首先用switch类型分支来测试输入参数是否实现了String方法如果是的话就调用该方法。然后继续增加类型测试分支检查这个值的动态类型是否是string、int、bool等基础类型并在每种情况下执行相应的格式化操作。</p>
<pre><code class="language-Go">func Sprint(x interface{}) string {
type stringer interface {
String() string
}
switch x := x.(type) {
case stringer:
return x.String()
case string:
return x
case int:
return strconv.Itoa(x)
// ...similar cases for int16, uint32, and so on...
case bool:
if x {
return &quot;true&quot;
}
return &quot;false&quot;
default:
// array, chan, func, map, pointer, slice, struct
return &quot;???&quot;
}
}
</code></pre>
<p>但是我们如何处理其它类似[]float64、map[string][]string等类型呢我们当然可以添加更多的测试分支但是这些组合类型的数目基本是无穷的。还有如何处理类似url.Values这样的具名类型呢即使类型分支可以识别出底层的基础类型是map[string][]string但是它并不匹配url.Values类型因为它们是两种不同的类型而且switch类型分支也不可能包含每个类似url.Values的类型这会导致对这些库的依赖。</p>
<p>没有办法来检查未知类型的表示方式,我们被卡住了。这就是我们需要反射的原因。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="122-reflecttype-和-reflectvalue"><a class="header" href="#122-reflecttype-和-reflectvalue">12.2. reflect.Type 和 reflect.Value</a></h2>
<p>反射是由 reflect 包提供的。它定义了两个重要的类型Type 和 Value。一个 Type 表示一个Go类型。它是一个接口有许多方法来区分类型以及检查它们的组成部分例如一个结构体的成员或一个函数的参数等。唯一能反映 reflect.Type 实现的是接口的类型描述信息§7.5),也正是这个实体标识了接口值的动态类型。</p>
<p>函数 reflect.TypeOf 接受任意的 interface{} 类型,并以 reflect.Type 形式返回其动态类型:</p>
<pre><code class="language-Go">t := reflect.TypeOf(3) // a reflect.Type
fmt.Println(t.String()) // &quot;int&quot;
fmt.Println(t) // &quot;int&quot;
</code></pre>
<p>其中 TypeOf(3) 调用将值 3 传给 interface{} 参数。回到 7.5节 的将一个具体的值转为接口类型会有一个隐式的接口转换操作,它会创建一个包含两个信息的接口值:操作数的动态类型(这里是 int和它的动态的值这里是 3</p>
<p>因为 reflect.TypeOf 返回的是一个动态类型的接口值,它总是返回具体的类型。因此,下面的代码将打印 &quot;*os.File&quot; 而不是 &quot;io.Writer&quot;。稍后,我们将看到能够表达接口类型的 reflect.Type。</p>
<pre><code class="language-Go">var w io.Writer = os.Stdout
fmt.Println(reflect.TypeOf(w)) // &quot;*os.File&quot;
</code></pre>
<p>要注意的是 reflect.Type 接口是满足 fmt.Stringer 接口的。因为打印一个接口的动态类型对于调试和日志是有帮助的, fmt.Printf 提供了一个缩写 %T 参数,内部使用 reflect.TypeOf 来输出:</p>
<pre><code class="language-Go">fmt.Printf(&quot;%T\n&quot;, 3) // &quot;int&quot;
</code></pre>
<p>reflect 包中另一个重要的类型是 Value。一个 reflect.Value 可以装载任意类型的值。函数 reflect.ValueOf 接受任意的 interface{} 类型,并返回一个装载着其动态值的 reflect.Value。和 reflect.TypeOf 类似reflect.ValueOf 返回的结果也是具体的类型,但是 reflect.Value 也可以持有一个接口值。</p>
<pre><code class="language-Go">v := reflect.ValueOf(3) // a reflect.Value
fmt.Println(v) // &quot;3&quot;
fmt.Printf(&quot;%v\n&quot;, v) // &quot;3&quot;
fmt.Println(v.String()) // NOTE: &quot;&lt;int Value&gt;&quot;
</code></pre>
<p>和 reflect.Type 类似reflect.Value 也满足 fmt.Stringer 接口,但是除非 Value 持有的是字符串,否则 String 方法只返回其类型。而使用 fmt 包的 %v 标志参数会对 reflect.Values 特殊处理。</p>
<p>对 Value 调用 Type 方法将返回具体类型所对应的 reflect.Type</p>
<pre><code class="language-Go">t := v.Type() // a reflect.Type
fmt.Println(t.String()) // &quot;int&quot;
</code></pre>
<p>reflect.ValueOf 的逆操作是 reflect.Value.Interface 方法。它返回一个 interface{} 类型,装载着与 reflect.Value 相同的具体值:</p>
<pre><code class="language-Go">v := reflect.ValueOf(3) // a reflect.Value
x := v.Interface() // an interface{}
i := x.(int) // an int
fmt.Printf(&quot;%d\n&quot;, i) // &quot;3&quot;
</code></pre>
<p>reflect.Value 和 interface{} 都能装载任意的值。所不同的是,一个空的接口隐藏了值内部的表示方式和所有方法,因此只有我们知道具体的动态类型才能使用类型断言来访问内部的值(就像上面那样),内部值我们没法访问。相比之下,一个 Value 则有很多方法来检查其内容,无论它的具体类型是什么。让我们再次尝试实现我们的格式化函数 format.Any。</p>
<p>我们使用 reflect.Value 的 Kind 方法来替代之前的类型 switch。虽然还是有无穷多的类型但是它们的 kinds 类型却是有限的Bool、String 和 所有数字类型的基础类型Array 和 Struct 对应的聚合类型Chan、Func、Ptr、Slice 和 Map 对应的引用类型interface 类型;还有表示空值的 Invalid 类型。(空的 reflect.Value 的 kind 即为 Invalid。</p>
<p><u><i>gopl.io/ch12/format</i></u></p>
<pre><code class="language-Go">package format
import (
&quot;reflect&quot;
&quot;strconv&quot;
)
// Any formats any value as a string.
func Any(value interface{}) string {
return formatAtom(reflect.ValueOf(value))
}
// formatAtom formats a value without inspecting its internal structure.
func formatAtom(v reflect.Value) string {
switch v.Kind() {
case reflect.Invalid:
return &quot;invalid&quot;
case reflect.Int, reflect.Int8, reflect.Int16,
reflect.Int32, reflect.Int64:
return strconv.FormatInt(v.Int(), 10)
case reflect.Uint, reflect.Uint8, reflect.Uint16,
reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return strconv.FormatUint(v.Uint(), 10)
// ...floating-point and complex cases omitted for brevity...
case reflect.Bool:
return strconv.FormatBool(v.Bool())
case reflect.String:
return strconv.Quote(v.String())
case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Slice, reflect.Map:
return v.Type().String() + &quot; 0x&quot; +
strconv.FormatUint(uint64(v.Pointer()), 16)
default: // reflect.Array, reflect.Struct, reflect.Interface
return v.Type().String() + &quot; value&quot;
}
}
</code></pre>
<p>到目前为止,我们的函数将每个值视作一个不可分割没有内部结构的物品,因此它叫 formatAtom。对于聚合类型结构体和数组和接口只是打印值的类型对于引用类型channels、functions、pointers、slices 和 maps打印类型和十六进制的引用地址。虽然还不够理想但是依然是一个重大的进步并且 Kind 只关心底层表示format.Any 也支持具名类型。例如:</p>
<pre><code class="language-Go">var x int64 = 1
var d time.Duration = 1 * time.Nanosecond
fmt.Println(format.Any(x)) // &quot;1&quot;
fmt.Println(format.Any(d)) // &quot;1&quot;
fmt.Println(format.Any([]int64{x})) // &quot;[]int64 0x8202b87b0&quot;
fmt.Println(format.Any([]time.Duration{d})) // &quot;[]time.Duration 0x8202b87e0&quot;
</code></pre>
<div style="break-before: page; page-break-before: always;"></div><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>
<div style="break-before: page; page-break-before: always;"></div><h2 id="124-示例-编码为s表达式"><a class="header" href="#124-示例-编码为s表达式">12.4. 示例: 编码为S表达式</a></h2>
<p>Display是一个用于显示结构化数据的调试工具但是它并不能将任意的Go语言对象编码为通用消息然后用于进程间通信。</p>
<p>正如我们在4.5节中中看到的Go语言的标准库支持了包括JSON、XML和ASN.1等多种编码格式。还有另一种依然被广泛使用的格式是S表达式格式采用Lisp语言的语法。但是和其他编码格式不同的是Go语言自带的标准库并不支持S表达式主要是因为它没有一个公认的标准规范。</p>
<p>在本节中我们将定义一个包用于将任意的Go语言对象编码为S表达式格式它支持以下结构</p>
<pre><code>42 integer
&quot;hello&quot; string带有Go风格的引号
foo symbol未用引号括起来的名字
(1 2 3) list 括号包起来的0个或多个元素
</code></pre>
<p>布尔型习惯上使用t符号表示true空列表或nil符号表示false但是为了简单起见我们暂时忽略布尔类型。同时忽略的还有chan管道和函数因为通过反射并无法知道它们的确切状态。我们忽略的还有浮点数、复数和interface。支持它们是练习12.3的任务。</p>
<p>我们将Go语言的类型编码为S表达式的方法如下。整数和字符串以显而易见的方式编码。空值编码为nil符号。数组和slice被编码为列表。</p>
<p>结构体被编码为成员对象的列表每个成员对象对应一个有两个元素的子列表子列表的第一个元素是成员的名字第二个元素是成员的值。Map被编码为键值对的列表。传统上S表达式使用点状符号列表(key . value)结构来表示key/value对而不是用一个含双元素的列表不过为了简单我们忽略了点状符号列表。</p>
<p>编码是由一个encode递归函数完成如下所示。它的结构本质上和前面的Display函数类似</p>
<p><u><i>gopl.io/ch12/sexpr</i></u></p>
<pre><code class="language-Go">func encode(buf *bytes.Buffer, v reflect.Value) error {
switch v.Kind() {
case reflect.Invalid:
buf.WriteString(&quot;nil&quot;)
case reflect.Int, reflect.Int8, reflect.Int16,
reflect.Int32, reflect.Int64:
fmt.Fprintf(buf, &quot;%d&quot;, v.Int())
case reflect.Uint, reflect.Uint8, reflect.Uint16,
reflect.Uint32, reflect.Uint64, reflect.Uintptr:
fmt.Fprintf(buf, &quot;%d&quot;, v.Uint())
case reflect.String:
fmt.Fprintf(buf, &quot;%q&quot;, v.String())
case reflect.Ptr:
return encode(buf, v.Elem())
case reflect.Array, reflect.Slice: // (value ...)
buf.WriteByte('(')
for i := 0; i &lt; v.Len(); i++ {
if i &gt; 0 {
buf.WriteByte(' ')
}
if err := encode(buf, v.Index(i)); err != nil {
return err
}
}
buf.WriteByte(')')
case reflect.Struct: // ((name value) ...)
buf.WriteByte('(')
for i := 0; i &lt; v.NumField(); i++ {
if i &gt; 0 {
buf.WriteByte(' ')
}
fmt.Fprintf(buf, &quot;(%s &quot;, v.Type().Field(i).Name)
if err := encode(buf, v.Field(i)); err != nil {
return err
}
buf.WriteByte(')')
}
buf.WriteByte(')')
case reflect.Map: // ((key value) ...)
buf.WriteByte('(')
for i, key := range v.MapKeys() {
if i &gt; 0 {
buf.WriteByte(' ')
}
buf.WriteByte('(')
if err := encode(buf, key); err != nil {
return err
}
buf.WriteByte(' ')
if err := encode(buf, v.MapIndex(key)); err != nil {
return err
}
buf.WriteByte(')')
}
buf.WriteByte(')')
default: // float, complex, bool, chan, func, interface
return fmt.Errorf(&quot;unsupported type: %s&quot;, v.Type())
}
return nil
}
</code></pre>
<p>Marshal函数是对encode的包装以保持和encoding/...下其它包有着相似的API</p>
<pre><code class="language-Go">// Marshal encodes a Go value in S-expression form.
func Marshal(v interface{}) ([]byte, error) {
var buf bytes.Buffer
if err := encode(&amp;buf, reflect.ValueOf(v)); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
</code></pre>
<p>下面是Marshal对12.3节的strangelove变量编码后的结果</p>
<pre><code>((Title &quot;Dr. Strangelove&quot;) (Subtitle &quot;How I Learned to Stop Worrying and Lo
ve the Bomb&quot;) (Year 1964) (Actor ((&quot;Grp. Capt. Lionel Mandrake&quot; &quot;Peter Sell
ers&quot;) (&quot;Pres. Merkin Muffley&quot; &quot;Peter Sellers&quot;) (&quot;Gen. Buck Turgidson&quot; &quot;Geor
ge C. Scott&quot;) (&quot;Brig. Gen. Jack D. Ripper&quot; &quot;Sterling Hayden&quot;) (&quot;Maj. T.J. \
&quot;King\&quot; Kong&quot; &quot;Slim Pickens&quot;) (&quot;Dr. Strangelove&quot; &quot;Peter Sellers&quot;))) (Oscars
(&quot;Best Actor (Nomin.)&quot; &quot;Best Adapted Screenplay (Nomin.)&quot; &quot;Best Director (N
omin.)&quot; &quot;Best Picture (Nomin.)&quot;)) (Sequel nil))
</code></pre>
<p>整个输出编码为一行中以减少输出的大小但是也很难阅读。下面是对S表达式手动格式化的结果。编写一个S表达式的美化格式化函数将作为一个具有挑战性的练习任务不过 http://gopl.io 也提供了一个简单的版本。</p>
<pre><code>((Title &quot;Dr. Strangelove&quot;)
(Subtitle &quot;How I Learned to Stop Worrying and Love the Bomb&quot;)
(Year 1964)
(Actor ((&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;)
(&quot;Maj. T.J. \&quot;King\&quot; Kong&quot; &quot;Slim Pickens&quot;)
(&quot;Dr. Strangelove&quot; &quot;Peter Sellers&quot;)))
(Oscars (&quot;Best Actor (Nomin.)&quot;
&quot;Best Adapted Screenplay (Nomin.)&quot;
&quot;Best Director (Nomin.)&quot;
&quot;Best Picture (Nomin.)&quot;))
(Sequel nil))
</code></pre>
<p>和fmt.Print、json.Marshal、Display函数类似sexpr.Marshal函数处理带环的数据结构也会陷入死循环。</p>
<p>在12.6节中我们将给出S表达式解码器的实现步骤但是在那之前我们还需要先了解如何通过反射技术来更新程序的变量。</p>
<p><strong>练习 12.3</strong> 实现encode函数缺少的分支。将布尔类型编码为t和nil浮点数编码为Go语言的格式复数1+2i编码为#C(1.0 2.0)格式。接口编码为类型名和值对,例如(&quot;[]int&quot; (1 2 3)但是这个形式可能会造成歧义reflect.Type.String方法对于不同的类型可能返回相同的结果。</p>
<p><strong>练习 12.4</strong> 修改encode函数以上面的格式化形式输出S表达式。</p>
<p><strong>练习 12.5</strong> 修改encode函数用JSON格式代替S表达式格式。然后使用标准库提供的json.Unmarshal解码器来验证函数是正确的。</p>
<p><strong>练习 12.6</strong> 修改encode作为一个优化忽略对是零值对象的编码。</p>
<p><strong>练习 12.7</strong> 创建一个基于流式的API用于S表达式的解码和json.Decoder(§4.5)函数功能类似。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="125-通过reflectvalue修改值"><a class="header" href="#125-通过reflectvalue修改值">12.5. 通过reflect.Value修改值</a></h2>
<p>到目前为止,反射还只是程序中变量的另一种读取方式。然而,在本节中我们将重点讨论如何通过反射机制来修改变量。</p>
<p>回想一下Go语言中类似x、x.f[1]和*p形式的表达式都可以表示变量但是其它如x + 1和f(2)则不是变量。一个变量就是一个可寻址的内存空间,里面存储了一个值,并且存储的值可以通过内存地址来更新。</p>
<p>对于reflect.Values也有类似的区别。有一些reflect.Values是可取地址的其它一些则不可以。考虑以下的声明语句</p>
<pre><code class="language-Go">x := 2 // value type variable?
a := reflect.ValueOf(2) // 2 int no
b := reflect.ValueOf(x) // 2 int no
c := reflect.ValueOf(&amp;x) // &amp;x *int no
d := c.Elem() // 2 int yes (x)
</code></pre>
<p>其中a对应的变量不可取地址。因为a中的值仅仅是整数2的拷贝副本。b中的值也同样不可取地址。c中的值还是不可取地址它只是一个指针<code>&amp;x</code>的拷贝。实际上所有通过reflect.ValueOf(x)返回的reflect.Value都是不可取地址的。但是对于d它是c的解引用方式生成的指向另一个变量因此是可取地址的。我们可以通过调用reflect.ValueOf(&amp;x).Elem()来获取任意变量x对应的可取地址的Value。</p>
<p>我们可以通过调用reflect.Value的CanAddr方法来判断其是否可以被取地址</p>
<pre><code class="language-Go">fmt.Println(a.CanAddr()) // &quot;false&quot;
fmt.Println(b.CanAddr()) // &quot;false&quot;
fmt.Println(c.CanAddr()) // &quot;false&quot;
fmt.Println(d.CanAddr()) // &quot;true&quot;
</code></pre>
<p>每当我们通过指针间接地获取的reflect.Value都是可取地址的即使开始的是一个不可取地址的Value。在反射机制中所有关于是否支持取地址的规则都是类似的。例如slice的索引表达式e[i]将隐式地包含一个指针它就是可取地址的即使开始的e表达式不支持也没有关系。以此类推reflect.ValueOf(e).Index(i)对应的值也是可取地址的即使原始的reflect.ValueOf(e)不支持也没有关系。</p>
<p>要从变量对应的可取地址的reflect.Value来访问变量需要三个步骤。第一步是调用Addr()方法它返回一个Value里面保存了指向变量的指针。然后是在Value上调用Interface()方法也就是返回一个interface{}里面包含指向变量的指针。最后如果我们知道变量的类型我们可以使用类型的断言机制将得到的interface{}类型的接口强制转为普通的类型指针。这样我们就可以通过这个普通指针来更新变量了:</p>
<pre><code class="language-Go">x := 2
d := reflect.ValueOf(&amp;x).Elem() // d refers to the variable x
px := d.Addr().Interface().(*int) // px := &amp;x
*px = 3 // x = 3
fmt.Println(x) // &quot;3&quot;
</code></pre>
<p>或者不使用指针而是通过调用可取地址的reflect.Value的reflect.Value.Set方法来更新对应的值</p>
<pre><code class="language-Go">d.Set(reflect.ValueOf(4))
fmt.Println(x) // &quot;4&quot;
</code></pre>
<p>Set方法将在运行时执行和编译时进行类似的可赋值性约束的检查。以上代码变量和值都是int类型但是如果变量是int64类型那么程序将抛出一个panic异常所以关键问题是要确保改类型的变量可以接受对应的值</p>
<pre><code class="language-Go">d.Set(reflect.ValueOf(int64(5))) // panic: int64 is not assignable to int
</code></pre>
<p>同样对一个不可取地址的reflect.Value调用Set方法也会导致panic异常</p>
<pre><code class="language-Go">x := 2
b := reflect.ValueOf(x)
b.Set(reflect.ValueOf(3)) // panic: Set using unaddressable value
</code></pre>
<p>这里有很多用于基本数据类型的Set方法SetInt、SetUint、SetString和SetFloat等。</p>
<pre><code class="language-Go">d := reflect.ValueOf(&amp;x).Elem()
d.SetInt(3)
fmt.Println(x) // &quot;3&quot;
</code></pre>
<p>从某种程度上说这些Set方法总是尽可能地完成任务。以SetInt为例只要变量是某种类型的有符号整数就可以工作即使是一些命名的类型、甚至只要底层数据类型是有符号整数就可以而且如果对于变量类型值太大的话会被自动截断。但需要谨慎的是对于一个引用interface{}类型的reflect.Value调用SetInt会导致panic异常即使那个interface{}变量对于整数类型也不行。</p>
<pre><code class="language-Go">x := 1
rx := reflect.ValueOf(&amp;x).Elem()
rx.SetInt(2) // OK, x = 2
rx.Set(reflect.ValueOf(3)) // OK, x = 3
rx.SetString(&quot;hello&quot;) // panic: string is not assignable to int
rx.Set(reflect.ValueOf(&quot;hello&quot;)) // panic: string is not assignable to int
var y interface{}
ry := reflect.ValueOf(&amp;y).Elem()
ry.SetInt(2) // panic: SetInt called on interface Value
ry.Set(reflect.ValueOf(3)) // OK, y = int(3)
ry.SetString(&quot;hello&quot;) // panic: SetString called on interface Value
ry.Set(reflect.ValueOf(&quot;hello&quot;)) // OK, y = &quot;hello&quot;
</code></pre>
<p>当我们用Display显示os.Stdout结构时我们发现反射可以越过Go语言的导出规则的限制读取结构体中未导出的成员比如在类Unix系统上os.File结构体中的fd int成员。然而利用反射机制并不能修改这些未导出的成员</p>
<pre><code class="language-Go">stdout := reflect.ValueOf(os.Stdout).Elem() // *os.Stdout, an os.File var
fmt.Println(stdout.Type()) // &quot;os.File&quot;
fd := stdout.FieldByName(&quot;fd&quot;)
fmt.Println(fd.Int()) // &quot;1&quot;
fd.SetInt(2) // panic: unexported field
</code></pre>
<p>一个可取地址的reflect.Value会记录一个结构体成员是否是未导出成员如果是的话则拒绝修改操作。因此CanAddr方法并不能正确反映一个变量是否是可以被修改的。另一个相关的方法CanSet是用于检查对应的reflect.Value是否是可取地址并可被修改的</p>
<pre><code class="language-Go">fmt.Println(fd.CanAddr(), fd.CanSet()) // &quot;true false&quot;
</code></pre>
<div style="break-before: page; page-break-before: always;"></div><h2 id="126-示例-解码s表达式"><a class="header" href="#126-示例-解码s表达式">12.6. 示例: 解码S表达式</a></h2>
<p>标准库中encoding/...下每个包中提供的Marshal编码函数都有一个对应的Unmarshal函数用于解码。例如我们在4.5节中看到的要将包含JSON编码格式的字节slice数据解码为我们自己的Movie类型§12.3),我们可以这样做:</p>
<pre><code class="language-Go">data := []byte{/* ... */}
var movie Movie
err := json.Unmarshal(data, &amp;movie)
</code></pre>
<p>Unmarshal函数使用了反射机制类修改movie变量的每个成员根据输入的内容为Movie成员创建对应的map、结构体和slice。</p>
<p>现在让我们为S表达式编码实现一个简易的Unmarshal类似于前面的json.Unmarshal标准库函数对应我们之前实现的sexpr.Marshal函数的逆操作。我们必须提醒一下一个健壮的和通用的实现通常需要比例子更多的代码为了便于演示我们采用了精简的实现。我们只支持S表达式有限的子集同时处理错误的方式也比较粗暴代码的目的是为了演示反射的用法而不是构造一个实用的S表达式的解码器。</p>
<p>词法分析器lexer使用了标准库中的text/scanner包将输入流的字节数据解析为一个个类似注释、标识符、字符串面值和数字面值之类的标记。输入扫描器scanner的Scan方法将提前扫描和返回下一个记号对于rune类型。大多数记号比如“(”对应一个单一rune可表示的Unicode字符但是text/scanner也可以用小的负数表示记号标识符、字符串等由多个字符组成的记号。调用Scan方法将返回这些记号的类型接着调用TokenText方法将返回记号对应的文本内容。</p>
<p>因为每个解析器可能需要多次使用当前的记号但是Scan会一直向前扫描所以我们包装了一个lexer扫描器辅助类型用于跟踪最近由Scan方法返回的记号。</p>
<p><u><i>gopl.io/ch12/sexpr</i></u></p>
<pre><code class="language-Go">type lexer struct {
scan scanner.Scanner
token rune // the current token
}
func (lex *lexer) next() { lex.token = lex.scan.Scan() }
func (lex *lexer) text() string { return lex.scan.TokenText() }
func (lex *lexer) consume(want rune) {
if lex.token != want { // NOTE: Not an example of good error handling.
panic(fmt.Sprintf(&quot;got %q, want %q&quot;, lex.text(), want))
}
lex.next()
}
</code></pre>
<p>现在让我们转到语法解析器。它主要包含两个功能。第一个是read函数用于读取S表达式的当前标记然后根据S表达式的当前标记更新可取地址的reflect.Value对应的变量v。</p>
<pre><code class="language-Go">func read(lex *lexer, v reflect.Value) {
switch lex.token {
case scanner.Ident:
// The only valid identifiers are
// &quot;nil&quot; and struct field names.
if lex.text() == &quot;nil&quot; {
v.Set(reflect.Zero(v.Type()))
lex.next()
return
}
case scanner.String:
s, _ := strconv.Unquote(lex.text()) // NOTE: ignoring errors
v.SetString(s)
lex.next()
return
case scanner.Int:
i, _ := strconv.Atoi(lex.text()) // NOTE: ignoring errors
v.SetInt(int64(i))
lex.next()
return
case '(':
lex.next()
readList(lex, v)
lex.next() // consume ')'
return
}
panic(fmt.Sprintf(&quot;unexpected token %q&quot;, lex.text()))
}
</code></pre>
<p>我们的S表达式使用标识符区分两个不同类型结构体成员名和nil值的指针。read函数值处理nil类型的标识符。当遇到scanner.Ident为“nil”时使用reflect.Zero函数将变量v设置为零值。而其它任何类型的标识符我们都作为错误处理。后面的readList函数将处理结构体的成员名。</p>
<p>一个“(”标记对应一个列表的开始。第二个函数readList将一个列表解码到一个聚合类型中map、结构体、slice或数组具体类型依赖于传入待填充变量的类型。每次遇到这种情况循环继续解析每个元素直到遇到于开始标记匹配的结束标记“)”endList函数用于检测结束标记。</p>
<p>最有趣的部分是递归。最简单的是对数组类型的处理。直到遇到“)”结束标记我们使用Index函数来获取数组每个元素的地址然后递归调用read函数处理。和其它错误类似如果输入数据导致解码器的引用超出了数组的范围解码器将抛出panic异常。slice也采用类似方法解析不同的是我们将为每个元素创建新的变量然后将元素添加到slice的末尾。</p>
<p>在循环处理结构体和map每个元素时必须解码一个(key value)格式的对应子列表。对于结构体key部分对于成员的名字。和数组类似我们使用FieldByName找到结构体对应成员的变量然后递归调用read函数处理。对于mapkey可能是任意类型对元素的处理方式和slice类似我们创建一个新的变量然后递归填充它最后将新解析到的key/value对添加到map。</p>
<pre><code class="language-Go">func readList(lex *lexer, v reflect.Value) {
switch v.Kind() {
case reflect.Array: // (item ...)
for i := 0; !endList(lex); i++ {
read(lex, v.Index(i))
}
case reflect.Slice: // (item ...)
for !endList(lex) {
item := reflect.New(v.Type().Elem()).Elem()
read(lex, item)
v.Set(reflect.Append(v, item))
}
case reflect.Struct: // ((name value) ...)
for !endList(lex) {
lex.consume('(')
if lex.token != scanner.Ident {
panic(fmt.Sprintf(&quot;got token %q, want field name&quot;, lex.text()))
}
name := lex.text()
lex.next()
read(lex, v.FieldByName(name))
lex.consume(')')
}
case reflect.Map: // ((key value) ...)
v.Set(reflect.MakeMap(v.Type()))
for !endList(lex) {
lex.consume('(')
key := reflect.New(v.Type().Key()).Elem()
read(lex, key)
value := reflect.New(v.Type().Elem()).Elem()
read(lex, value)
v.SetMapIndex(key, value)
lex.consume(')')
}
default:
panic(fmt.Sprintf(&quot;cannot decode list into %v&quot;, v.Type()))
}
}
func endList(lex *lexer) bool {
switch lex.token {
case scanner.EOF:
panic(&quot;end of file&quot;)
case ')':
return true
}
return false
}
</code></pre>
<p>最后我们将解析器包装为导出的Unmarshal解码函数隐藏了一些初始化和清理等边缘处理。内部解析器以panic的方式抛出错误但是Unmarshal函数通过在defer语句调用recover函数来捕获内部panic§5.10然后返回一个对panic对应的错误信息。</p>
<pre><code class="language-Go">// Unmarshal parses S-expression data and populates the variable
// whose address is in the non-nil pointer out.
func Unmarshal(data []byte, out interface{}) (err error) {
lex := &amp;lexer{scan: scanner.Scanner{Mode: scanner.GoTokens}}
lex.scan.Init(bytes.NewReader(data))
lex.next() // get the first token
defer func() {
// NOTE: this is not an example of ideal error handling.
if x := recover(); x != nil {
err = fmt.Errorf(&quot;error at %s: %v&quot;, lex.scan.Position, x)
}
}()
read(lex, reflect.ValueOf(out).Elem())
return nil
}
</code></pre>
<p>生产实现不应该对任何输入问题都用panic形式报告而且应该报告一些错误相关的信息例如出现错误输入的行号和位置等。尽管如此我们希望通过这个例子来展示类似encoding/json等包底层代码的实现思路以及如何使用反射机制来填充数据结构。</p>
<p><strong>练习 12.8</strong> sexpr.Unmarshal函数和json.Unmarshal一样都要求在解码前输入完整的字节slice。定义一个和json.Decoder类似的sexpr.Decoder类型支持从一个io.Reader流解码。修改sexpr.Unmarshal函数使用这个新的类型实现。</p>
<p><strong>练习 12.9</strong> 编写一个基于标记的API用于解码S表达式参考xml.Decoder7.14的风格。你将需要五种类型的标记Symbol、String、Int、StartList和EndList。</p>
<p><strong>练习 12.10</strong> 扩展sexpr.Unmarshal函数支持布尔型、浮点数和interface类型的解码使用 <strong>练习 12.3</strong> 的方案。提示要解码接口你需要将name映射到每个支持类型的reflect.Type。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="128-显示一个类型的方法集"><a class="header" href="#128-显示一个类型的方法集">12.8. 显示一个类型的方法集</a></h2>
<p>我们的最后一个例子是使用reflect.Type来打印任意值的类型和枚举它的方法</p>
<p><u><i>gopl.io/ch12/methods</i></u></p>
<pre><code class="language-Go">// Print prints the method set of the value x.
func Print(x interface{}) {
v := reflect.ValueOf(x)
t := v.Type()
fmt.Printf(&quot;type %s\n&quot;, t)
for i := 0; i &lt; v.NumMethod(); i++ {
methType := v.Method(i).Type()
fmt.Printf(&quot;func (%s) %s%s\n&quot;, t, t.Method(i).Name,
strings.TrimPrefix(methType.String(), &quot;func&quot;))
}
}
</code></pre>
<p>reflect.Type和reflect.Value都提供了一个Method方法。每次t.Method(i)调用将一个reflect.Method的实例对应一个用于描述一个方法的名称和类型的结构体。每次v.Method(i)方法调用都返回一个reflect.Value以表示对应的值§6.4也就是一个方法是绑到它的接收者的。使用reflect.Value.Call方法我们这里没有演示将可以调用一个Func类型的Value但是这个例子中只用到了它的类型。</p>
<p>这是属于time.Duration和<code>*strings.Replacer</code>两个类型的方法:</p>
<pre><code class="language-Go">methods.Print(time.Hour)
// Output:
// type time.Duration
// func (time.Duration) Hours() float64
// func (time.Duration) Minutes() float64
// func (time.Duration) Nanoseconds() int64
// func (time.Duration) Seconds() float64
// func (time.Duration) String() string
methods.Print(new(strings.Replacer))
// Output:
// type *strings.Replacer
// func (*strings.Replacer) Replace(string) string
// func (*strings.Replacer) WriteString(io.Writer, string) (int, error)
</code></pre>
<div style="break-before: page; page-break-before: always;"></div><h2 id="129-几点忠告"><a class="header" href="#129-几点忠告">12.9. 几点忠告</a></h2>
<p>虽然反射提供的API远多于我们讲到的我们前面的例子主要是给出了一个方向通过反射可以实现哪些功能。反射是一个强大并富有表达力的工具但是它应该被小心地使用原因有三。</p>
<p>第一个原因是基于反射的代码是比较脆弱的。对于每一个会导致编译器报告类型错误的问题在反射中都有与之相对应的误用问题不同的是编译器会在构建时马上报告错误而反射则是在真正运行到的时候才会抛出panic异常可能是写完代码很久之后了而且程序也可能运行了很长的时间。</p>
<p>以前面的readList函数§12.6为例为了从输入读取字符串并填充int类型的变量而调用的reflect.Value.SetString方法可能导致panic异常。绝大多数使用反射的程序都有类似的风险需要非常小心地检查每个reflect.Value的对应值的类型、是否可取地址还有是否可以被修改等。</p>
<p>避免这种因反射而导致的脆弱性的问题的最好方法是将所有的反射相关的使用控制在包的内部如果可能的话避免在包的API中直接暴露reflect.Value类型这样可以限制一些非法输入。如果无法做到这一点在每个有风险的操作前指向额外的类型检查。以标准库中的代码为例当fmt.Printf收到一个非法的操作数时它并不会抛出panic异常而是打印相关的错误信息。程序虽然还有BUG但是会更加容易诊断。</p>
<pre><code class="language-Go">fmt.Printf(&quot;%d %s\n&quot;, &quot;hello&quot;, 42) // &quot;%!d(string=hello) %!s(int=42)&quot;
</code></pre>
<p>反射同样降低了程序的安全性,还影响了自动化重构和分析工具的准确性,因为它们无法识别运行时才能确认的类型信息。</p>
<p>避免使用反射的第二个原因是即使对应类型提供了相同文档但是反射的操作不能做静态类型检查而且大量反射的代码通常难以理解。总是需要小心翼翼地为每个导出的类型和其它接受interface{}或reflect.Value类型参数的函数维护说明文档。</p>
<p>第三个原因,基于反射的代码通常比正常的代码运行速度慢一到两个数量级。对于一个典型的项目,大部分函数的性能和程序的整体性能关系不大,所以当反射能使程序更加清晰的时候可以考虑使用。测试是一个特别适合使用反射的场景,因为每个测试的数据集都很小。但是对于性能关键路径的函数,最好避免使用反射。</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="第13章-底层编程"><a class="header" href="#第13章-底层编程">第13章 底层编程</a></h1>
<p>Go语言的设计包含了诸多安全策略限制了可能导致程序运行出错的用法。编译时类型检查可以发现大多数类型不匹配的操作例如两个字符串做减法的错误。字符串、map、slice和chan等所有的内置类型都有严格的类型转换规则。</p>
<p>对于无法静态检测到的错误,例如数组访问越界或使用空指针,运行时动态检测可以保证程序在遇到问题的时候立即终止并打印相关的错误信息。自动内存管理(垃圾内存自动回收)可以消除大部分野指针和内存泄漏相关的问题。</p>
<p>Go语言的实现刻意隐藏了很多底层细节。我们无法知道一个结构体真实的内存布局也无法获取一个运行时函数对应的机器码也无法知道当前的goroutine是运行在哪个操作系统线程之上。事实上Go语言的调度器会自己决定是否需要将某个goroutine从一个操作系统线程转移到另一个操作系统线程。一个指向变量的指针也并没有展示变量真实的地址。因为垃圾回收器可能会根据需要移动变量的内存位置当然变量对应的地址也会被自动更新。</p>
<p>总的来说Go语言的这些特性使得Go程序相比较低级的C语言来说更容易预测和理解程序也不容易崩溃。通过隐藏底层的实现细节也使得Go语言编写的程序具有高度的可移植性因为语言的语义在很大程度上是独立于任何编译器实现、操作系统和CPU系统结构的当然也不是完全绝对独立例如int等类型就依赖于CPU机器字的大小某些表达式求值的具体顺序还有编译器实现的一些额外的限制等</p>
<p>有时候我们可能会放弃使用部分语言特性而优先选择具有更好性能的方法例如需要与其他语言编写的库进行互操作或者用纯Go语言无法实现的某些函数。</p>
<p>在本章我们将展示如何使用unsafe包来摆脱Go语言规则带来的限制讲述如何创建C语言函数库的绑定以及如何进行系统调用。</p>
<p>本章提供的方法不应该轻易使用译注属于黑魔法虽然功能很强大但是也容易误伤到自己。如果没有处理好细节它们可能导致各种不可预测的并且隐晦的错误甚至连有经验的C语言程序员也无法理解这些错误。使用unsafe包的同时也放弃了Go语言保证与未来版本的兼容性的承诺因为它必然会有意无意中使用很多非公开的实现细节而这些实现的细节在未来的Go语言中很可能会被改变。</p>
<p>要注意的是unsafe包是一个采用特殊方式实现的包。虽然它可以和普通包一样的导入和使用但它实际上是由编译器实现的。它提供了一些访问语言内部特性的方法特别是内存布局相关的细节。将这些特性封装到一个独立的包中是为在极少数情况下需要使用的时候同时引起人们的注意译注因为看包的名字就知道使用unsafe包是不安全的。此外有一些环境因为安全的因素可能限制这个包的使用。</p>
<p>不过unsafe包被广泛地用于比较低级的包例如runtime、os、syscall还有net包等因为它们需要和操作系统密切配合但是对于普通的程序一般是不需要使用unsafe包的。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="131-unsafesizeof-alignof-和-offsetof"><a class="header" href="#131-unsafesizeof-alignof-和-offsetof">13.1. unsafe.Sizeof, Alignof 和 Offsetof</a></h2>
<p>unsafe.Sizeof函数返回操作数在内存中的字节大小参数可以是任意类型的表达式但是它并不会对表达式进行求值。一个Sizeof函数调用是一个对应uintptr类型的常量表达式因此返回的结果可以用作数组类型的长度大小或者用作计算其他的常量。</p>
<pre><code class="language-Go">import &quot;unsafe&quot;
fmt.Println(unsafe.Sizeof(float64(0))) // &quot;8&quot;
</code></pre>
<p>Sizeof函数返回的大小只包括数据结构中固定的部分例如字符串对应结构体中的指针和字符串长度部分但是并不包含指针指向的字符串的内容。Go语言中非聚合类型通常有一个固定的大小尽管在不同工具链下生成的实际大小可能会有所不同。考虑到可移植性引用类型或包含引用类型的大小在32位平台上是4个字节在64位平台上是8个字节。</p>
<p>计算机在加载和保存数据时如果内存地址合理地对齐的将会更有效率。例如2字节大小的int16类型的变量地址应该是偶数一个4字节大小的rune类型变量的地址应该是4的倍数一个8字节大小的float64、uint64或64-bit指针类型变量的地址应该是8字节对齐的。但是对于再大的地址对齐倍数则是不需要的即使是complex128等较大的数据类型最多也只是8字节对齐。</p>
<p>由于地址对齐这个因素一个聚合类型结构体或数组的大小至少是所有字段或元素大小的总和或者更大因为可能存在内存空洞。内存空洞是编译器自动添加的没有被使用的内存空间用于保证后面每个字段或元素的地址相对于结构或数组的开始地址能够合理地对齐译注内存空洞可能会存在一些随机数据可能会对用unsafe包直接操作内存的处理产生影响</p>
<table><thead><tr><th>类型</th><th>大小</th></tr></thead><tbody>
<tr><td><code>bool</code></td><td>1个字节</td></tr>
<tr><td><code>intN, uintN, floatN, complexN</code></td><td>N/8个字节例如float64是8个字节</td></tr>
<tr><td><code>int, uint, uintptr</code></td><td>1个机器字</td></tr>
<tr><td><code>*T</code></td><td>1个机器字</td></tr>
<tr><td><code>string</code></td><td>2个机器字data、len</td></tr>
<tr><td><code>[]T</code></td><td>3个机器字data、len、cap</td></tr>
<tr><td><code>map</code></td><td>1个机器字</td></tr>
<tr><td><code>func</code></td><td>1个机器字</td></tr>
<tr><td><code>chan</code></td><td>1个机器字</td></tr>
<tr><td><code>interface</code></td><td>2个机器字type、value</td></tr>
</tbody></table>
<p>Go语言的规范并没有要求一个字段的声明顺序和内存中的顺序是一致的所以理论上一个编译器可以随意地重新排列每个字段的内存位置虽然在写作本书的时候编译器还没有这么做。下面的三个结构体虽然有着相同的字段但是第一种写法比另外的两个需要多50%的内存。</p>
<pre><code class="language-Go"> // 64-bit 32-bit
struct{ bool; float64; int16 } // 3 words 4words
struct{ float64; int16; bool } // 2 words 3words
struct{ bool; int16; float64 } // 2 words 3words
</code></pre>
<p>关于内存地址对齐算法的细节超出了本书的范围也不是每一个结构体都需要担心这个问题不过有效的包装可以使数据结构更加紧凑译注未来的Go语言编译器应该会默认优化结构体的顺序当然应该也能够指定具体的内存布局相同讨论请参考 <a href="https://github.com/golang/go/issues/10014">Issue10014</a> ),内存使用率和性能都可能会受益。</p>
<p><code>unsafe.Alignof</code> 函数返回对应参数的类型需要对齐的倍数。和 Sizeof 类似, Alignof 也是返回一个常量表达式对应一个常量。通常情况下布尔和数字类型需要对齐到它们本身的大小最多8个字节其它的类型对齐到机器字大小。</p>
<p><code>unsafe.Offsetof</code> 函数的参数必须是一个字段 <code>x.f</code>,然后返回 <code>f</code> 字段相对于 <code>x</code> 起始地址的偏移量,包括可能的空洞。</p>
<p>图 13.1 显示了一个结构体变量 x 以及其在32位和64位机器上的典型的内存。灰色区域是空洞。</p>
<pre><code class="language-Go">var x struct {
a bool
b int16
c []int
}
</code></pre>
<p>下面显示了对x和它的三个字段调用unsafe包相关函数的计算结果</p>
<p><img src="ch13/../images/ch13-01.png" alt="" /></p>
<p>32位系统</p>
<pre><code>Sizeof(x) = 16 Alignof(x) = 4
Sizeof(x.a) = 1 Alignof(x.a) = 1 Offsetof(x.a) = 0
Sizeof(x.b) = 2 Alignof(x.b) = 2 Offsetof(x.b) = 2
Sizeof(x.c) = 12 Alignof(x.c) = 4 Offsetof(x.c) = 4
</code></pre>
<p>64位系统</p>
<pre><code>Sizeof(x) = 32 Alignof(x) = 8
Sizeof(x.a) = 1 Alignof(x.a) = 1 Offsetof(x.a) = 0
Sizeof(x.b) = 2 Alignof(x.b) = 2 Offsetof(x.b) = 2
Sizeof(x.c) = 24 Alignof(x.c) = 8 Offsetof(x.c) = 8
</code></pre>
<p>虽然这几个函数在不安全的unsafe包但是这几个函数调用并不是真的不安全特别在需要优化内存空间时它们返回的结果对于理解原生的内存布局很有帮助。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="132-unsafepointer"><a class="header" href="#132-unsafepointer">13.2. unsafe.Pointer</a></h2>
<p>大多数指针类型会写成<code>*T</code>表示是“一个指向T类型变量的指针”。unsafe.Pointer是特别定义的一种指针类型译注类似C语言中的<code>void*</code>类型的指针),它可以包含任意类型变量的地址。当然,我们不可以直接通过<code>*p</code>来获取unsafe.Pointer指针指向的真实变量的值因为我们并不知道变量的具体类型。和普通指针一样unsafe.Pointer指针也是可以比较的并且支持和nil常量比较判断是否为空指针。</p>
<p>一个普通的<code>*T</code>类型指针可以被转化为unsafe.Pointer类型指针并且一个unsafe.Pointer类型指针也可以被转回普通的指针被转回普通的指针类型并不需要和原始的<code>*T</code>类型相同。通过将<code>*float64</code>类型指针转化为<code>*uint64</code>类型指针,我们可以查看一个浮点数变量的位模式。</p>
<pre><code class="language-Go">package math
func Float64bits(f float64) uint64 { return *(*uint64)(unsafe.Pointer(&amp;f)) }
fmt.Printf(&quot;%#016x\n&quot;, Float64bits(1.0)) // &quot;0x3ff0000000000000&quot;
</code></pre>
<p>通过转为新类型指针,我们可以更新浮点数的位模式。通过位模式操作浮点数是可以的,但是更重要的意义是指针转换语法让我们可以在不破坏类型系统的前提下向内存写入任意的值。</p>
<p>一个unsafe.Pointer指针也可以被转化为uintptr类型然后保存到指针型数值变量中译注这只是和当前指针相同的一个数字值并不是一个指针然后用以做必要的指针数值运算。第三章内容uintptr是一个无符号的整型数足以保存一个地址这种转换虽然也是可逆的但是将uintptr转为unsafe.Pointer指针可能会破坏类型系统因为并不是所有的数字都是有效的内存地址。</p>
<p>许多将unsafe.Pointer指针转为原生数字然后再转回为unsafe.Pointer类型指针的操作也是不安全的。比如下面的例子需要将变量x的地址加上b字段地址偏移量转化为<code>*int16</code>类型指针然后通过该指针更新x.b</p>
<p><u><i>gopl.io/ch13/unsafeptr</i></u></p>
<pre><code class="language-Go">var x struct {
a bool
b int16
c []int
}
// 和 pb := &amp;x.b 等价
pb := (*int16)(unsafe.Pointer(
uintptr(unsafe.Pointer(&amp;x)) + unsafe.Offsetof(x.b)))
*pb = 42
fmt.Println(x.b) // &quot;42&quot;
</code></pre>
<p>上面的写法尽管很繁琐但在这里并不是一件坏事因为这些功能应该很谨慎地使用。不要试图引入一个uintptr类型的临时变量因为它可能会破坏代码的安全性译注这是真正可以体会unsafe包为何不安全的例子。下面段代码是错误的</p>
<pre><code class="language-Go">// NOTE: subtly incorrect!
tmp := uintptr(unsafe.Pointer(&amp;x)) + unsafe.Offsetof(x.b)
pb := (*int16)(unsafe.Pointer(tmp))
*pb = 42
</code></pre>
<p>产生错误的原因很微妙。有时候垃圾回收器会移动一些变量以降低内存碎片等问题。这类垃圾回收器被称为移动GC。当一个变量被移动所有的保存该变量旧地址的指针必须同时被更新为变量移动后的新地址。从垃圾收集器的视角来看一个unsafe.Pointer是一个指向变量的指针因此当变量被移动时对应的指针也必须被更新但是uintptr类型的临时变量只是一个普通的数字所以其值不应该被改变。上面错误的代码因为引入一个非指针的临时变量tmp导致垃圾收集器无法正确识别这个是一个指向变量x的指针。当第二个语句执行时变量x可能已经被转移这时候临时变量tmp也就不再是现在的<code>&amp;x.b</code>地址。第三个向之前无效地址空间的赋值语句将彻底摧毁整个程序!</p>
<p>还有很多类似原因导致的错误。例如这条语句:</p>
<pre><code class="language-Go">pT := uintptr(unsafe.Pointer(new(T))) // 提示: 错误!
</code></pre>
<p>这里并没有指针引用<code>new</code>新创建的变量因此该语句执行完成之后垃圾收集器有权马上回收其内存空间所以返回的pT将是无效的地址。</p>
<p>虽然目前的Go语言实现还没有使用移动GC译注未来可能实现但这不该是编写错误代码侥幸的理由当前的Go语言实现已经有移动变量的场景。在5.2节我们提到goroutine的栈是根据需要动态增长的。当发生栈动态增长的时候原来栈中的所有变量可能需要被移动到新的更大的栈中所以我们并不能确保变量的地址在整个使用周期内是不变的。</p>
<p>在编写本文时还没有清晰的原则来指引Go程序员什么样的unsafe.Pointer和uintptr的转换是不安全的参考 <a href="https://github.com/golang/go/issues/7192">Issue7192</a> . 译注: 该问题已经关闭因此我们强烈建议按照最坏的方式处理。将所有包含变量地址的uintptr类型变量当作BUG处理同时减少不必要的unsafe.Pointer类型到uintptr类型的转换。在第一个例子中有三个转换——字段偏移量到uintptr的转换和转回unsafe.Pointer类型的操作——所有的转换全在一个表达式完成。</p>
<p>当调用一个库函数并且返回的是uintptr类型地址时译注普通方法实现的函数尽量不要返回该类型。下面例子是reflect包的函数reflect包和unsafe包一样都是采用特殊技术实现的编译器可能给它们开了后门比如下面反射包中的相关函数返回的结果应该立即转换为unsafe.Pointer以确保指针指向的是相同的变量。</p>
<pre><code class="language-Go">package reflect
func (Value) Pointer() uintptr
func (Value) UnsafeAddr() uintptr
func (Value) InterfaceData() [2]uintptr // (index 1)
</code></pre>
<div style="break-before: page; page-break-before: always;"></div><h2 id="133-示例-深度相等判断"><a class="header" href="#133-示例-深度相等判断">13.3. 示例: 深度相等判断</a></h2>
<p>来自reflect包的DeepEqual函数可以对两个值进行深度相等判断。DeepEqual函数使用内建的==比较操作符对基础类型进行相等判断,对于复合类型则递归该变量的每个基础类型然后做类似的比较判断。因为它可以工作在任意的类型上,甚至对于一些不支持==操作运算符的类型也可以工作因此在一些测试代码中广泛地使用该函数。比如下面的代码是用DeepEqual函数比较两个字符串slice是否相等。</p>
<pre><code class="language-Go">func TestSplit(t *testing.T) {
got := strings.Split(&quot;a:b:c&quot;, &quot;:&quot;)
want := []string{&quot;a&quot;, &quot;b&quot;, &quot;c&quot;};
if !reflect.DeepEqual(got, want) { /* ... */ }
}
</code></pre>
<p>尽管DeepEqual函数很方便而且可以支持任意的数据类型但是它也有不足之处。例如它将一个nil值的map和非nil值但是空的map视作不相等同样nil值的slice 和非nil但是空的slice也视作不相等。</p>
<pre><code class="language-Go">var a, b []string = nil, []string{}
fmt.Println(reflect.DeepEqual(a, b)) // &quot;false&quot;
var c, d map[string]int = nil, make(map[string]int)
fmt.Println(reflect.DeepEqual(c, d)) // &quot;false&quot;
</code></pre>
<p>我们希望在这里实现一个自己的Equal函数用于比较类型的值。和DeepEqual函数类似的地方是它也是基于slice和map的每个元素进行递归比较不同之处是它将nil值的slicemap类似和非nil值但是空的slice视作相等的值。基础部分的比较可以基于reflect包完成和12.3章的Display函数的实现方法类似。同样我们也定义了一个内部函数equal用于内部的递归比较。读者目前不用关心seen参数的具体含义。对于每一对需要比较的x和yequal函数首先检测它们是否都有效或都无效然后检测它们是否是相同的类型。剩下的部分是一个巨大的switch分支用于相同基础类型的元素比较。因为页面空间的限制我们省略了一些相似的分支。</p>
<p><u><i>gopl.io/ch13/equal</i></u></p>
<pre><code class="language-Go">func equal(x, y reflect.Value, seen map[comparison]bool) bool {
if !x.IsValid() || !y.IsValid() {
return x.IsValid() == y.IsValid()
}
if x.Type() != y.Type() {
return false
}
// ...cycle check omitted (shown later)...
switch x.Kind() {
case reflect.Bool:
return x.Bool() == y.Bool()
case reflect.String:
return x.String() == y.String()
// ...numeric cases omitted for brevity...
case reflect.Chan, reflect.UnsafePointer, reflect.Func:
return x.Pointer() == y.Pointer()
case reflect.Ptr, reflect.Interface:
return equal(x.Elem(), y.Elem(), seen)
case reflect.Array, reflect.Slice:
if x.Len() != y.Len() {
return false
}
for i := 0; i &lt; x.Len(); i++ {
if !equal(x.Index(i), y.Index(i), seen) {
return false
}
}
return true
// ...struct and map cases omitted for brevity...
}
panic(&quot;unreachable&quot;)
}
</code></pre>
<p>和前面的建议一样我们并不公开reflect包相关的接口所以导出的函数需要在内部自己将变量转为reflect.Value类型。</p>
<pre><code class="language-Go">// Equal reports whether x and y are deeply equal.
func Equal(x, y interface{}) bool {
seen := make(map[comparison]bool)
return equal(reflect.ValueOf(x), reflect.ValueOf(y), seen)
}
type comparison struct {
x, y unsafe.Pointer
t reflect.Type
}
</code></pre>
<p>为了确保算法对于有环的数据结构也能正常退出我们必须记录每次已经比较的变量从而避免进入第二次的比较。Equal函数分配了一组用于比较的结构体包含每对比较对象的地址unsafe.Pointer形式保存和类型。我们要记录类型的原因是有些不同的变量可能对应相同的地址。例如如果x和y都是数组类型那么x和x[0]将对应相同的地址y和y[0]也是对应相同的地址这可以用于区分x与y之间的比较或x[0]与y[0]之间的比较是否进行过了。</p>
<pre><code class="language-Go">// cycle check
if x.CanAddr() &amp;&amp; y.CanAddr() {
xptr := unsafe.Pointer(x.UnsafeAddr())
yptr := unsafe.Pointer(y.UnsafeAddr())
if xptr == yptr {
return true // identical references
}
c := comparison{xptr, yptr, x.Type()}
if seen[c] {
return true // already seen
}
seen[c] = true
}
</code></pre>
<p>这是Equal函数用法的例子:</p>
<pre><code class="language-Go">fmt.Println(Equal([]int{1, 2, 3}, []int{1, 2, 3})) // &quot;true&quot;
fmt.Println(Equal([]string{&quot;foo&quot;}, []string{&quot;bar&quot;})) // &quot;false&quot;
fmt.Println(Equal([]string(nil), []string{})) // &quot;true&quot;
fmt.Println(Equal(map[string]int(nil), map[string]int{})) // &quot;true&quot;
</code></pre>
<p>Equal函数甚至可以处理类似12.3章中导致Display陷入死循环的带有环的数据。</p>
<pre><code class="language-Go">// Circular linked lists a -&gt; b -&gt; a and c -&gt; c.
type link struct {
value string
tail *link
}
a, b, c := &amp;link{value: &quot;a&quot;}, &amp;link{value: &quot;b&quot;}, &amp;link{value: &quot;c&quot;}
a.tail, b.tail, c.tail = b, a, c
fmt.Println(Equal(a, a)) // &quot;true&quot;
fmt.Println(Equal(b, b)) // &quot;true&quot;
fmt.Println(Equal(c, c)) // &quot;true&quot;
fmt.Println(Equal(a, b)) // &quot;false&quot;
fmt.Println(Equal(a, c)) // &quot;false&quot;
</code></pre>
<p><strong>练习 13.1</strong> 定义一个深比较函数,对于十亿以内的数字比较,忽略类型差异。</p>
<p><strong>练习 13.2</strong> 编写一个函数,报告其参数是否为循环数据结构。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="134-通过cgo调用c代码"><a class="header" href="#134-通过cgo调用c代码">13.4. 通过cgo调用C代码</a></h2>
<p>Go程序可能会遇到要访问C语言的某些硬件驱动函数的场景或者是从一个C++语言实现的嵌入式数据库查询记录的场景或者是使用Fortran语言实现的一些线性代数库的场景。C语言作为一个通用语言很多库会选择提供一个C兼容的API然后用其他不同的编程语言实现译者Go语言需要也应该拥抱这些巨大的代码遗产</p>
<p>在本节中我们将构建一个简易的数据压缩程序使用了一个Go语言自带的叫cgo的用于支援C语言函数调用的工具。这类工具一般被称为 <em>foreign-function interfaces</em> 简称ffi并且在类似工具中cgo也不是唯一的。SWIG<a href="http://swig.org">http://swig.org</a>是另一个类似的且被广泛使用的工具SWIG提供了很多复杂特性以支援C++的特性但SWIG并不是我们要讨论的主题。</p>
<p>在标准库的<code>compress/...</code>子包有很多流行的压缩算法的编码和解码实现包括流行的LZW压缩算法Unix的compress命令用的算法和DEFLATE压缩算法GNU gzip命令用的算法。这些包的API的细节虽然有些差异但是它们都提供了针对 io.Writer类型输出的压缩接口和提供了针对io.Reader类型输入的解压缩接口。例如</p>
<pre><code class="language-Go">package gzip // compress/gzip
func NewWriter(w io.Writer) io.WriteCloser
func NewReader(r io.Reader) (io.ReadCloser, error)
</code></pre>
<p>bzip2压缩算法是基于优雅的Burrows-Wheeler变换算法运行速度比gzip要慢但是可以提供更高的压缩比。标准库的compress/bzip2包目前还没有提供bzip2压缩算法的实现。完全从头开始实现一个压缩算法是一件繁琐的工作而且 http://bzip.org 已经有现成的libbzip2的开源实现不仅文档齐全而且性能又好。</p>
<p>如果是比较小的C语言库我们完全可以用纯Go语言重新实现一遍。如果我们对性能也没有特殊要求的话我们还可以用os/exec包的方法将C编写的应用程序作为一个子进程运行。只有当你需要使用复杂而且性能更高的底层C接口时就是使用cgo的场景了译注用os/exec包调用子进程的方法会导致程序运行时依赖那个应用程序。下面我们将通过一个例子讲述cgo的具体用法。</p>
<p>译注本章采用的代码都是最新的。因为之前已经出版的书中包含的代码只能在Go1.5之前使用。从Go1.6开始Go语言已经明确规定了哪些Go语言指针可以直接传入C语言函数。新代码重点是增加了bz2alloc和bz2free的两个函数用于bz_stream对象空间的申请和释放操作。下面是新代码中增加的注释说明这个问题</p>
<pre><code class="language-Go">// The version of this program that appeared in the first and second
// printings did not comply with the proposed rules for passing
// pointers between Go and C, described here:
// https://github.com/golang/proposal/blob/master/design/12416-cgo-pointers.md
//
// The rules forbid a C function like bz2compress from storing 'in'
// and 'out' (pointers to variables allocated by Go) into the Go
// variable 's', even temporarily.
//
// The version below, which appears in the third printing, has been
// corrected. To comply with the rules, the bz_stream variable must
// be allocated by C code. We have introduced two C functions,
// bz2alloc and bz2free, to allocate and free instances of the
// bz_stream type. Also, we have changed bz2compress so that before
// it returns, it clears the fields of the bz_stream that contain
// pointers to Go variables.
</code></pre>
<p>要使用libbzip2我们需要先构建一个bz_stream结构体用于保持输入和输出缓存。然后有三个函数BZ2_bzCompressInit用于初始化缓存BZ2_bzCompress用于将输入缓存的数据压缩到输出缓存BZ2_bzCompressEnd用于释放不需要的缓存。目前不要担心包的具体结构这个例子的目的就是演示各个部分如何组合在一起的。</p>
<p>我们可以在Go代码中直接调用BZ2_bzCompressInit和BZ2_bzCompressEnd但是对于BZ2_bzCompress我们将定义一个C语言的包装函数用它完成真正的工作。下面是C代码对应一个独立的文件。</p>
<p><u><i>gopl.io/ch13/bzip</i></u></p>
<pre><code class="language-C">/* This file is gopl.io/ch13/bzip/bzip2.c, */
/* a simple wrapper for libbzip2 suitable for cgo. */
#include &lt;bzlib.h&gt;
int bz2compress(bz_stream *s, int action,
char *in, unsigned *inlen, char *out, unsigned *outlen) {
s-&gt;next_in = in;
s-&gt;avail_in = *inlen;
s-&gt;next_out = out;
s-&gt;avail_out = *outlen;
int r = BZ2_bzCompress(s, action);
*inlen -= s-&gt;avail_in;
*outlen -= s-&gt;avail_out;
s-&gt;next_in = s-&gt;next_out = NULL;
return r;
}
</code></pre>
<p>现在让我们转到Go语言部分第一部分如下所示。其中<code>import &quot;C&quot;</code>的语句是比较特别的。其实并没有一个叫C的包但是这行语句会让Go编译程序在编译之前先运行cgo工具。</p>
<pre><code class="language-Go">// Package bzip provides a writer that uses bzip2 compression (bzip.org).
package bzip
/*
#cgo CFLAGS: -I/usr/include
#cgo LDFLAGS: -L/usr/lib -lbz2
#include &lt;bzlib.h&gt;
#include &lt;stdlib.h&gt;
bz_stream* bz2alloc() { return calloc(1, sizeof(bz_stream)); }
int bz2compress(bz_stream *s, int action,
char *in, unsigned *inlen, char *out, unsigned *outlen);
void bz2free(bz_stream* s) { free(s); }
*/
import &quot;C&quot;
import (
&quot;io&quot;
&quot;unsafe&quot;
)
type writer struct {
w io.Writer // underlying output stream
stream *C.bz_stream
outbuf [64 * 1024]byte
}
// NewWriter returns a writer for bzip2-compressed streams.
func NewWriter(out io.Writer) io.WriteCloser {
const blockSize = 9
const verbosity = 0
const workFactor = 30
w := &amp;writer{w: out, stream: C.bz2alloc()}
C.BZ2_bzCompressInit(w.stream, blockSize, verbosity, workFactor)
return w
}
</code></pre>
<p>在预处理过程中cgo工具生成一个临时包用于包含所有在Go语言中访问的C语言的函数或类型。例如C.bz_stream和C.BZ2_bzCompressInit。cgo工具通过以某种特殊的方式调用本地的C编译器来发现在Go源文件导入声明前的注释中包含的C头文件中的内容译注<code>import &quot;C&quot;</code>语句前紧挨着的注释是对应cgo的特殊语法对应必要的构建参数选项和C语言代码</p>
<p>在cgo注释中还可以包含#cgo指令用于给C语言工具链指定特殊的参数。例如CFLAGS和LDFLAGS分别对应传给C语言编译器的编译参数和链接器参数使它们可以从特定目录找到bzlib.h头文件和libbz2.a库文件。这个例子假设你已经在/usr目录成功安装了bzip2库。如果bzip2库是安装在不同的位置你需要更新这些参数译注这里有一个从纯C代码生成的cgo绑定不依赖bzip2静态库和操作系统的具体环境具体请访问 https://github.com/chai2010/bzip2 )。</p>
<p>NewWriter函数通过调用C语言的BZ2_bzCompressInit函数来初始化stream中的缓存。在writer结构中还包括了另一个buffer用于输出缓存。</p>
<p>下面是Write方法的实现返回成功压缩数据的大小主体是一个循环中调用C语言的bz2compress函数实现的。从代码可以看到Go程序可以访问C语言的bz_stream、char和uint类型还可以访问bz2compress等函数甚至可以访问C语言中像BZ_RUN那样的宏定义全部都是以C.x语法访问。其中C.uint类型和Go语言的uint类型并不相同即使它们具有相同的大小也是不同的类型。</p>
<pre><code class="language-Go">func (w *writer) Write(data []byte) (int, error) {
if w.stream == nil {
panic(&quot;closed&quot;)
}
var total int // uncompressed bytes written
for len(data) &gt; 0 {
inlen, outlen := C.uint(len(data)), C.uint(cap(w.outbuf))
C.bz2compress(w.stream, C.BZ_RUN,
(*C.char)(unsafe.Pointer(&amp;data[0])), &amp;inlen,
(*C.char)(unsafe.Pointer(&amp;w.outbuf)), &amp;outlen)
total += int(inlen)
data = data[inlen:]
if _, err := w.w.Write(w.outbuf[:outlen]); err != nil {
return total, err
}
}
return total, nil
}
</code></pre>
<p>在循环的每次迭代中向bz2compress传入数据的地址和剩余部分的长度还有输出缓存w.outbuf的地址和容量。这两个长度信息通过它们的地址传入而不是值传入因为bz2compress函数可能会根据已经压缩的数据和压缩后数据的大小来更新这两个值。每个块压缩后的数据被写入到底层的io.Writer。</p>
<p>Close方法和Write方法有着类似的结构通过一个循环将剩余的压缩数据刷新到输出缓存。</p>
<pre><code class="language-Go">// Close flushes the compressed data and closes the stream.
// It does not close the underlying io.Writer.
func (w *writer) Close() error {
if w.stream == nil {
panic(&quot;closed&quot;)
}
defer func() {
C.BZ2_bzCompressEnd(w.stream)
C.bz2free(w.stream)
w.stream = nil
}()
for {
inlen, outlen := C.uint(0), C.uint(cap(w.outbuf))
r := C.bz2compress(w.stream, C.BZ_FINISH, nil, &amp;inlen,
(*C.char)(unsafe.Pointer(&amp;w.outbuf)), &amp;outlen)
if _, err := w.w.Write(w.outbuf[:outlen]); err != nil {
return err
}
if r == C.BZ_STREAM_END {
return nil
}
}
}
</code></pre>
<p>压缩完成后Close方法用了defer函数确保函数退出前调用C.BZ2_bzCompressEnd和C.bz2free释放相关的C语言运行时资源。此刻w.stream指针将不再有效我们将它设置为nil以保证安全然后在每个方法中增加了nil检测以防止用户在关闭后依然错误使用相关方法。</p>
<p>上面的实现中不仅仅写是非并发安全的甚至并发调用Close和Write方法也可能导致程序的的崩溃。修复这个问题是练习13.3的内容。</p>
<p>下面的bzipper程序使用我们自己包实现的bzip2压缩命令。它的行为和许多Unix系统的bzip2命令类似。</p>
<p><u><i>gopl.io/ch13/bzipper</i></u></p>
<pre><code class="language-Go">// Bzipper reads input, bzip2-compresses it, and writes it out.
package main
import (
&quot;io&quot;
&quot;log&quot;
&quot;os&quot;
&quot;gopl.io/ch13/bzip&quot;
)
func main() {
w := bzip.NewWriter(os.Stdout)
if _, err := io.Copy(w, os.Stdin); err != nil {
log.Fatalf(&quot;bzipper: %v\n&quot;, err)
}
if err := w.Close(); err != nil {
log.Fatalf(&quot;bzipper: close: %v\n&quot;, err)
}
}
</code></pre>
<p>在上面的场景中我们使用bzipper压缩了/usr/share/dict/words系统自带的词典从938,848字节压缩到335,405字节。大约是原始数据大小的三分之一。然后使用系统自带的bunzip2命令进行解压。压缩前后文件的SHA256哈希码是相同了这也说明了我们的压缩工具是正确的。如果你的系统没有sha256sum命令那么请先按照练习4.2实现一个类似的工具)</p>
<pre><code>$ go build gopl.io/ch13/bzipper
$ wc -c &lt; /usr/share/dict/words
938848
$ sha256sum &lt; /usr/share/dict/words
126a4ef38493313edc50b86f90dfdaf7c59ec6c948451eac228f2f3a8ab1a6ed -
$ ./bzipper &lt; /usr/share/dict/words | wc -c
335405
$ ./bzipper &lt; /usr/share/dict/words | bunzip2 | sha256sum
126a4ef38493313edc50b86f90dfdaf7c59ec6c948451eac228f2f3a8ab1a6ed -
</code></pre>
<p>我们演示了如何将一个C语言库链接到Go语言程序。相反将Go编译为静态库然后链接到C程序或者将Go程序编译为动态库然后在C程序中动态加载也都是可行的译注在Go1.5中Windows系统的Go语言实现并不支持生成C语言动态库或静态库的特性。不过好消息是目前已经有人在尝试解决这个问题具体请访问 <a href="https://github.com/golang/go/issues/11058">Issue11058</a> 。这里我们只展示的cgo很小的一些方面更多的关于内存管理、指针、回调函数、中断信号处理、字符串、errno处理、终结器以及goroutines和系统线程的关系等有很多细节可以讨论。特别是如何将Go语言的指针传入C函数的规则也是异常复杂的译注简单来说要传入C函数的Go指针指向的数据本身不能包含指针或其他引用类型并且C函数在返回后不能继续持有Go指针并且在C函数返回之前Go指针是被锁定的不能导致对应指针数据被移动或栈的调整部分的原因在13.2节有讨论到但是在Go1.5中还没有被明确译注Go1.6将会明确cgo中的指针使用规则。如果要进一步阅读可以从 https://golang.org/cmd/cgo 开始。</p>
<p><strong>练习 13.3</strong> 使用sync.Mutex以保证bzip2.writer在多个goroutines中被并发调用是安全的。</p>
<p><strong>练习 13.4</strong> 因为C库依赖的限制。 使用os/exec包启动/bin/bzip2命令作为一个子进程提供一个纯Go的bzip.NewWriter的替代实现译注虽然是纯Go实现但是运行时将依赖/bin/bzip2命令其他操作系统可能无法运行</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="135-几点忠告"><a class="header" href="#135-几点忠告">13.5. 几点忠告</a></h2>
<p>我们在前一章结尾的时候我们警告要谨慎使用reflect包。那些警告同样适用于本章的unsafe包。</p>
<p>高级语言使得程序员不用再关心真正运行程序的指令细节,同时也不再需要关注许多如内存布局之类的实现细节。因为高级语言这个绝缘的抽象层,我们可以编写安全健壮的,并且可以运行在不同操作系统上的具有高度可移植性的程序。</p>
<p>但是unsafe包它让程序员可以透过这个绝缘的抽象层直接使用一些必要的功能虽然可能是为了获得更好的性能。但是代价就是牺牲了可移植性和程序安全因此使用unsafe包是一个危险的行为。我们对何时以及如何使用unsafe包的建议和我们在11.5节提到的Knuth对过早优化的建议类似。大多数Go程序员可能永远不会需要直接使用unsafe包。当然也永远都会有一些需要使用unsafe包实现会更简单的场景。如果确实认为使用unsafe包是最理想的方式那么应该尽可能将它限制在较小的范围这样其它代码就可以忽略unsafe的影响。</p>
<p>现在赶紧将最后两章抛入脑后吧。编写一些实实在在的应用是真理。请远离reflect和unsafe包除非你确实需要它们。</p>
<p>最后用Go快乐地编程。我们希望你能像我们一样喜欢Go语言。</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="附录"><a class="header" href="#附录">附录</a></h1>
<p>英文原版并没有包含附录部分,只有一个索引部分。中文版增加附录部分主要用于收录一些和本书相关的内容,比如英文原版的勘误(有些读者可能会对照中文和英文原阅读)、英文作者和中文译者、译文授权等内容。以后还可能会考虑增加一些习题解答相关的内容。</p>
<p>需要特别说明的是中文版附录并没有包含英文原版的索引信息。因为英文原版的索引信息主要是记录每个索引所在的英文页面位置而中文版是以GitBook方式组织的html网页形式将英文页面位置转为章节位置可能会更合理不过这个会涉及到繁琐的手工操作。如果大家有更好的建议请告知我们。</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="附录aa-hrefhttpwwwgoplioerratahtml原文勘误a"><a class="header" href="#附录aa-hrefhttpwwwgoplioerratahtml原文勘误a">附录A<a href="http://www.gopl.io/errata.html">原文勘误</a></a></h2>
<p><strong>p.9, ¶2:</strong> for &quot;can compared&quot;, read &quot;can be compared&quot;. (Thanks to Antonio Macías Ojeda, 2015-10-22. Corrected in the second printing.)</p>
<p><strong>p.13:</strong> As printed, the <code>gopl.io/ch1/lissajous</code> program is deterministic, not random. We've added the statement below to the downloadable program so that it prints a pseudo-random image each time it is run. (Thanks to Randall McPherson, 2015-10-19.)</p>
<p><code>rand.Seed(time.Now().UTC().UnixNano())</code></p>
<p><strong>p.15, ¶2:</strong> For &quot;inner loop&quot;, read &quot;outer loop&quot;. (Thanks to Ralph Corderoy, 2015-11-28. Corrected in the third printing.)</p>
<p><strong>p.19, ¶2:</strong> For &quot;Go's libraries makes&quot;, read &quot;Go's library makes&quot;. (Thanks to Victor Farazdagi, 2015-11-30. Corrected in the third printing.)</p>
<p><strong>p.40, ¶4:</strong> For &quot;value of the underlying type&quot;, read &quot;value of an unnamed type with the same underlying type&quot;. (Thanks to Carlos Romero Brox, 2015-12-19.)</p>
<p><strong>p.40, ¶1:</strong> The paragraph should end with a period, not a comma. (Thanks to Victor Farazdagi, 2015-11-30. Corrected in the third printing.)</p>
<p><strong>p.43, ¶3:</strong> Import declarations are explained in §10.4, not §10.3. (Thanks to Peter Jurgensen, 2015-11-21. Corrected in the third printing.)</p>
<p><strong>p.48:</strong> <code>f.ReadByte()</code> serves as an example of a reference to f, but <code>*os.File</code> has no such method. For &quot;ReadByte&quot;, read &quot;Stat&quot;, four times. (Thanks to Peter Olsen, 2016-01-06. Corrected in the third printing.)</p>
<p><strong>p.52, ¶2:</strong> for &quot;an synonym&quot;, read &quot;a synonym&quot;, twice. (Corrected in the second printing.)</p>
<p><strong>p.52, ¶9:</strong> for &quot;The integer arithmetic operators&quot;, read &quot;The arithmetic operators&quot;. (Thanks to Yoshiki Shibata, 2015-12-20.)</p>
<p><strong>p.68:</strong> the table of UTF-8 encodings is missing a bit from each first byte. The corrected table is shown below. (Thanks to Akshay Kumar, 2015-11-02. Corrected in the second printing.)</p>
<pre><code>0xxxxxxx runes 0127 (ASCII)
110xxxxx 10xxxxxx 1282047 (values &lt;128 unused)
1110xxxx 10xxxxxx 10xxxxxx 204865535 (values &lt;2048 unused)
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 655360x10ffff (other values unused)
</code></pre>
<p><strong>p.73, ¶1:</strong> For &quot;a exercise&quot;, read &quot;an exercise&quot;. (Thanks to vrajmohan, 2015-12-28.)</p>
<p><strong>p.74:</strong> the comment in <code>gopl.io/ch3/printints</code> should say <code>fmt.Sprint</code>, not <code>fmt.Sprintf</code>. (Corrected in the second printing.)</p>
<p><strong>p.75, ¶4:</strong> for &quot;%u&quot;, read &quot;%o&quot;. (Thanks to William Hannish, 2015-12-21.)</p>
<p><strong>p.76:</strong> the comment <code>// &quot;time.Duration 5m0s</code> should have a closing double-quotation mark. (Corrected in the second printing.)</p>
<p><strong>p.79, ¶4:</strong> &quot;When an untyped constant is assigned to a variable, as in the first statement below, or
appears on the right-hand side of a variable declaration with an explicit type, as in the other three statements, ...&quot; has it backwards: the <i>first</i> statement is a declaration; the other three are assignments. (Thanks to Yoshiki Shibata, 2015-11-09. Corrected in the third printing.)</p>
<p><strong>p.112:</strong> Exercise 4.11 calls for a &quot;CRUD&quot; (create, read, update, delete) tool for GitHub Issues. Since GitHub does not currently allow Issues to be deleted, for &quot;delete&quot;, read &quot;close&quot;. (Thanks to Yoshiki Shibata, 2016-01-18.)</p>
<p><strong>p.115:</strong> The anchor element in <code>gopl.io/ch4/issueshtml</code>'s template is missing a closing <code>&lt;/a&gt;</code> tag. (Thanks to Taj Khattra, 2016-01-19.)</p>
<p><strong>p.132, code display following ¶3:</strong> the final comment should read: <code>// compile error: can't assign func(int, int) int to func(int) int</code> (Thanks to Toni Suter, 2015-11-21. Corrected in the third printing.)</p>
<p><strong>p.160, ¶4:</strong> <code>For Get(&quot;item&quot;))</code>, read <code>Get(&quot;item&quot;)</code>. (Thanks to Yoshiki Shibata, 2016-02-01.)</p>
<p><strong>p.166, ¶2:</strong> for &quot;way&quot;, read &quot;a way&quot;. (Corrected in the third printing.)</p>
<p><strong>p.200, TestEval function:</strong> the format string in the final call to t.Errorf should format test.env with %v, not %s. (Thanks to Mitsuteru Sawa, 2015-12-07. Corrected in the third printing.)</p>
<p><strong>p.222, Exercise 8.1:</strong> The port numbers for <code>London</code> and <code>Tokyo</code> should be swapped in the final command to match the earlier commands. (Thanks to Kiyoshi Kamishima, 2016-01-08.)</p>
<p><strong>p.272, ¶3:</strong> for &quot;the request body&quot;, read &quot;the response body&quot;. (Thanks to <a href="https://github.com/cch123">曹春晖</a>, 2016-01-19.)</p>
<p><strong>p.288, code display following ¶4:</strong> In the import declaration, for <code>&quot;database/mysql&quot;</code>, read <code>&quot;database/sql&quot;</code>. (Thanks to Jose Colon Rodriguez, 2016-01-09.)</p>
<p><strong>p.347, Exercise 12.8:</strong> for &quot;like json.Marshal&quot;, read &quot;like json.Unmarshal&quot;. (Thanks to <a href="https://github.com/chai2010">chai2010</a>, 2016-01-01.)</p>
<p><strong>p.362:</strong> the <code>gopl.io/ch13/bzip</code> program does not comply with the <a href="https://github.com/golang/proposal/blob/master/design/12416-cgo-pointers.md">proposed rules for passing pointers between Go and C code</a> because the C function <code>bz2compress</code> temporarily stores a Go pointer (in) into the Go heap (the <code>bz_stream</code> variable). The <code>bz_stream</code> variable should be allocated, and explicitly freed after the call to <code>BZ2_bzCompressEnd</code>, by C functions. (Thanks to Joe Tsai, 2015-11-18. Corrected in the third printing.)</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="附录b作者译者"><a class="header" href="#附录b作者译者">附录B作者/译者</a></h2>
<h3 id="英文作者"><a class="header" href="#英文作者">英文作者</a></h3>
<ul>
<li><strong><a href="https://github.com/adonovan">Alan A. A. Donovan</a></strong> is a member of <a href="https://golang.org/">Googles Go</a> team in New York. He holds computer science degrees from Cambridge and MIT and has been programming in industry since 1996. Since 2005, he has worked at Google on infrastructure projects and was the co-designer of its proprietary build system, <a href="http://bazel.io/">Blaze</a>. He has built many libraries and tools for static analysis of Go programs, including <a href="https://godoc.org/golang.org/x/tools/oracle">oracle</a>, <a href="https://godoc.org/golang.org/x/tools/cmd/godoc"><code>godoc -analysis</code></a>, eg, and <a href="https://godoc.org/golang.org/x/tools/cmd/gorename">gorename</a>.</li>
<li><strong><a href="http://www.cs.princeton.edu/%7Ebwk/">Brian W. Kernighan</a></strong> is a professor in the Computer Science Department at Princeton University. He was a member of technical staff in the Computing Science Research Center at <a href="http://www.cs.bell-labs.com/">Bell Labs</a> from 1969 until 2000, where he worked on languages and tools for <a href="http://doc.cat-v.org/unix/">Unix</a>. He is the co-author of several books, including <a href="http://s3-us-west-2.amazonaws.com/belllabs-microsite-dritchie/cbook/index.html">The C Programming Language, Second Edition (Prentice Hall, 1988)</a>, and <a href="https://en.wikipedia.org/wiki/The_Practice_of_Programming">The Practice of Programming (Addison-Wesley, 1999)</a>.</li>
</ul>
<hr />
<h3 id="中文译者"><a class="header" href="#中文译者">中文译者</a></h3>
<table><thead><tr><th>中文译者</th><th>章节</th></tr></thead><tbody>
<tr><td><code>chai2010 &lt;chaishushan@gmail.com&gt;</code></td><td>前言/第2 ~ 4章/第10 ~ 13章</td></tr>
<tr><td><code>Xargin &lt;cao1988228@163.com&gt;</code></td><td>第1章/第6章/第8 ~ 9章</td></tr>
<tr><td><code>CrazySssst</code></td><td>第5章</td></tr>
<tr><td><code>foreversmart &lt;njutree@gmail.com&gt;</code></td><td>第7章</td></tr>
</tbody></table>
<div style="break-before: page; page-break-before: always;"></div><h2 id="附录c译文授权"><a class="header" href="#附录c译文授权">附录C译文授权</a></h2>
<p>除特别注明外,本站内容均采用<a href="http://creativecommons.org/licenses/by/3.0/">知识共享-署名(CC-BY) 3.0协议</a>授权,代码遵循<a href="http://golang.org/LICENSE">Go项目的BSD协议</a>授权。</p>
<p><a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/4.0/"><img alt="Creative Commons License" style="border-width:0" src="appendix/../images/by-nc-sa-4.0-88x31.png"></img></a></p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="附录d其它语言"><a class="header" href="#附录d其它语言">附录D其它语言</a></h2>
<p>下表是 <a href="http://www.gopl.io/">The Go Programming Language</a> 其它语言版本:</p>
<table><thead><tr><th>语言</th><th>链接</th><th>时间</th><th>译者</th><th>ISBN</th></tr></thead><tbody>
<tr><td>中文</td><td><a href="http://golang-china.github.io/gopl-zh/" title="《Go语言圣经》">《Go语言圣经》</a></td><td>2016/2/1</td><td><a href="https://github.com/chai2010">chai2010</a>, <a href="https://github.com/cch123">Xargin</a>, <a href="https://github.com/CrazySssst">CrazySssst</a>, <a href="https://github.com/foreversmart">foreversmart</a></td><td>?</td></tr>
<tr><td>韩语</td><td><a href="http://www.acornpub.co.kr/">Acorn Publishing (Korea)</a></td><td>2016</td><td>Seung Lee</td><td>9788960778320</td></tr>
<tr><td>俄语</td><td><a href="http://www.williamspublishing.com/">Williams Publishing (Russia)</a></td><td>2016</td><td>?</td><td>9785845920515</td></tr>
<tr><td>波兰语</td><td><a href="http://helion.pl/">Helion (Poland)</a></td><td>2016</td><td>?</td><td>?</td></tr>
<tr><td>日语</td><td><a href="http://www.maruzen.co.jp/corp/en/services/publishing.html">Maruzen Publishing (Japan)</a></td><td>2017</td><td>Yoshiki Shibata</td><td>9784621300251</td></tr>
<tr><td>葡萄牙语</td><td><a href="http://novatec.com.br/">Novatec Editora (Brazil)</a></td><td>2017</td><td>?</td><td>?</td></tr>
<tr><td>中文简体</td><td><a href="http://www.pearsonapac.com/">Pearson Education Asia</a></td><td>2017</td><td>?</td><td>?</td></tr>
<tr><td>中文繁体</td><td><a href="http://www.gotop.com.tw/">Gotop Information (Taiwan)</a></td><td>2017</td><td>?</td><td>?</td></tr>
</tbody></table>
<!-- 公众号 -->
<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 -->
<div style="clear: both"></div>
</nav>
</div>
</div>
<nav class="nav-wide-wrapper" aria-label="Page navigation">
</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 = "print.md"
</script>
<!-- Custom JS scripts -->
<script type="text/javascript" src="js/custom.js"></script>
<script type="text/javascript" src="js/bigPicture.js"></script>
<script type="text/javascript">
window.addEventListener('load', function() {
window.setTimeout(window.print, 100);
});
</script>
</body>
</html>