├── .gitignore ├── README.md ├── SUMMARY.md ├── book.json ├── chapter01 ├── 01.0.md ├── 01.1.md ├── 01.2.md ├── 01.3.md └── 01.4.md ├── chapter02 ├── 02.0.md ├── 02.1.md ├── 02.2.md ├── 02.3.md ├── 02.4.md └── 02.5.md ├── chapter03 ├── 03.0.md ├── 03.1.md └── 03.3.md ├── chapter04 ├── 04.0.md ├── 04.1.md ├── 04.2.md ├── 04.3.md └── 04.4.md ├── chapter05 └── 05.1.md ├── chapter06 ├── 06.0.md ├── 06.1.md └── 06.2.md ├── chapter07 ├── 07.0.md └── 07.1.md ├── chapter08 ├── 08.0.md └── 08.1.md ├── chapter09 ├── 09.0.md ├── 09.1.md ├── 09.2.md ├── 09.3.md ├── 09.4.md ├── 09.5.md └── 09.6.md ├── chapter10 ├── 10.0.md ├── 10.1.md ├── 10.2.md ├── 10.3.md └── 10.4.md ├── chapter13 ├── 13.0.md └── 13.3.md ├── chapter15 └── 15.02.md ├── chapter16 ├── 16.01.md ├── 16.02.md └── 16.03.md └── code ├── install.bat ├── install.sh └── src ├── chapter01 └── io │ ├── 01.txt │ ├── byterwer.go │ ├── main.go │ └── reader.go ├── chapter06 ├── filepath │ └── walk │ │ └── main.go └── os │ └── dirtree │ └── main.go ├── chapter09 ├── benchmark_result.go ├── httptest │ └── server.go └── testing │ ├── example_test.go │ ├── parallel.go │ ├── parallel_test.go │ ├── server_test.go │ ├── t.go │ └── t_test.go ├── chapter10 └── os_exec.go └── util └── util.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 《Go语言标准库》The Golang Standard Library by Example # 2 | 3 | Golang标准库。对于程序员而言,标准库与语言本身同样重要,它好比一个百宝箱,能为各种常见的任务提供完美的解决方案。以示例驱动的方式讲解Golang的标准库。 4 | 5 | 标准库基于最新版本Go。注:目前 Go 标准库文档并没有标识某个 API 基于哪个版本的 Go,将来会加上这部分 [issue](https://github.com/golang/go/issues/5778)。 6 | 7 | 讲解中涉及到特定操作系统时,针对的都是 Linux/amd64。Go 中相关系统调用在 Linux 下,对于同一个系统调用,如果有对应的 `at` 版本,使用的都是 `at` 版本,如 `open` 系统调用使用都是 `openat`。更多信息参考 [Go语言中文网博客中关于系统调用的文章](http://blog.studygolang.com)。 8 | 9 | ## 交流 ## 10 | 11 | 欢迎大家加入QQ群:192706294 《Go语言实现与标准库》交流群 12 | 13 | Go语言构建的 Go语言中文网:[http://studygolang.com](http://studygolang.com) 14 | 15 | ## 阅读 ## 16 | 17 | 为了更方便阅读,Go语言中文网搭建了阅读平台,可以更友好的在线阅读。 18 | 19 | [Go语言中文网——Go语言标准库](http://books.studygolang.com/The-Golang-Standard-Library-by-Example) 20 | 21 | ## 捐赠 ## 22 | 23 | 如果您觉得本书对您有帮助,通过微信或支付宝捐赠作者,金额随意! 24 | 25 | **由于无法从支付方获取支付者信息,请在支付的留言备注功能中附上 Go语言中文网账户的昵称等信息,以便我们记录!** 26 | 27 | ### 通过微信支付捐赠 ### 28 | 29 | ![wxpay.png](http://studygolang.qiniudn.com/img/wxpay.png) 30 | 31 | ### 通过支付宝捐赠 ### 32 | 33 | ![1.png](http://studygolang.qiniudn.com/170605/9d11b43988fbbb42a5da9f970a0f6818.png) 34 | 35 | ## 贡献者 ## 36 | 37 | [hikerell](https://github.com/hikerell) 38 | 39 | ## 反馈 ## 40 | 41 | 由于本人能力有限,书中难免有写的不对之处,且目前所写内容没有经过校正。如果阅读过程中有任何疑问或觉得不对之处,欢迎提出,谢谢! 42 | 43 | ## 版权声明 ## 44 | 45 | 本书所有内容遵循 [CC-BY-SA 3.0协议(署名-相同方式共享)](http://zh.wikipedia.org/wiki/Wikipedia:CC-by-sa-3.0%E5%8D%8F%E8%AE%AE%E6%96%87%E6%9C%AC) 46 | 47 | 1. 常见误解 48 | 49 | 2. 常用手法 50 | 51 | 3. 如何理解,使用 52 | 53 | 4. 为什么接口如此组织 54 | 55 | 5. 和其它语言对比优缺点 56 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | * [简介](README.md) 2 | * [第一章 输入输出 (Input/Output)](chapter01/01.0.md) 3 | - 1.1 [io — 基本的 IO 接口](chapter01/01.1.md) 4 | - 1.2 [ioutil — 方便的 IO 操作函数集](chapter01/01.2.md) 5 | - 1.3 [fmt — 格式化 IO](chapter01/01.3.md) 6 | - 1.4 [bufio — 缓存 IO](chapter01/01.4.md) 7 | * [第二章 文本](chapter02/02.0.md) 8 | - 2.1 [strings — 字符串操作](chapter02/02.1.md) 9 | - 2.2 [bytes — byte slice 便利操作](chapter02/02.2.md) 10 | - 2.3 [strconv — 字符串和基本数据类型之间转换](chapter02/02.3.md) 11 | - 2.4 [regexp — 正则表达式](chapter02/02.4.md) 12 | - 2.5 [unicode — Unicode 码点、UTF-8/16 编码](chapter02/02.5.md) 13 | * [第三章 数据结构与算法](chapter03/03.0.md) 14 | - 3.1 [sort — 排序算法](chapter03/03.1.md) 15 | - 3.2 index/suffixarray — 后缀数组实现子字符串查询 16 | - 3.3 [container — 容器数据类型:heap、list 和 ring](chapter03/03.3.md) 17 | * [第四章 日期与时间](chapter04/04.0.md) 18 | - 4.1 [主要类型概述](chapter04/04.1.md) 19 | - 4.2 [时区](chapter04/04.2.md) 20 | - 4.3 [Time类型详解](chapter04/04.3.md) 21 | - 4.4 [定时器](chapter04/04.4.md) 22 | * 第五章 数学计算 23 | - 5.1 [math — 基本数学函数](chapter05/05.1.md) 24 | - 5.2 math/big — 大数实现 25 | - 5.3 math/cmplx — 复数基本函数操作 26 | - 5.4 math/rand — 伪随机数生成器 27 | * 第六章 [文件系统](chapter06/06.0.md) 28 | - 6.1 [os — 平台无关的操作系统功能实现](chapter06/06.1.md) 29 | - 6.2 [path/filepath — 操作路径](chapter06/06.2.md) 30 | * [第七章 数据持久存储与交换](chapter07/07.0.md) 31 | - 7.1 [database/sql — SQL/SQL-Like 数据库操作接口](chapter07/07.1.md) 32 | - 7.2 encoding/json — json 解析 33 | - 7.3 encoding/xml — xml 解析 34 | - 7.4 encoding/gob — golang 自定义二进制格式 35 | - 7.5 csv — 逗号分隔值文件 36 | * [第八章 数据压缩与归档](chapter08/08.0.md) 37 | - 8.1 [flate - DEFLATE 压缩算法](chapter08/08.1.md) 38 | - 8.2 compress/zlib — gnu zlib 压缩 39 | - 8.3 compress/gzip — 读写 gnu zip 文件 40 | - 8.4 compress/bzip2 — bzip2 压缩 41 | - 8.5 archive/tar — tar 归档访问 42 | - 8.6 archive/zip — zip 归档访问 43 | * 第九章 [测试](chapter09/09.0.md) 44 | - 9.1 [testing - 单元测试](chapter09/09.1.md) 45 | - 9.2 [testing - 基准测试](chapter09/09.2.md) 46 | - 9.3 [testing - 子测试](chapter09/09.3.md) 47 | - 9.4 [testing - 运行并验证示例](chapter09/09.4.md) 48 | - 9.5 [testing - 其他功能](chapter09/09.5.md) 49 | - 9.6 [httptest - HTTP 测试辅助工具](chapter09/09.6.md) 50 | * 第十章 [进程、线程与 goroutine](chapter10/10.0.md) 51 | - 10.1 [创建进程](chapter10/10.1.md) 52 | - 10.2 [进程属性和控制](chapter10/10.2.md) 53 | - 10.3 [线程](chapter10/10.3.md) 54 | - 10.4 [进程间通信](chapter10/10.4.md) 55 | * 第十一章 网络通信与互联网 (Internet) 56 | * 第十二章 email 57 | * 第十三章 [应用构建 与 debug](chapter13/13.0.md) 58 | - 13.1 [flag - 命令行参数解析](chapter13/13.1.md) 59 | - 13.2 [log - 日志记录](chapter13/13.2.md) 60 | - 13.3 [expvar - 公共变量的标准化接口](chapter13/13.3.md) 61 | - 13.4 [runtime/debug - 运行时的调试工具](chapter13/13.4.md) 62 | * 第十四章 运行时特性 63 | * 第十五章 底层库介绍 64 | - 15.1 builtin 65 | - 15.2 [unsafe — 非类型安全操作](chapter15/15.02.md) 66 | * 第十六章 同步 67 | - 16.1 [sync - 处理同步需求](chapter16/16.01.md) 68 | - 16.2 [sync/atomic - 原子操作](chapter16/16.02.md) 69 | - 16.3 [os/signal - 信号](chapter16/16.03.md) 70 | * 第十七章 加解密 71 | -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Go语言标准库", 3 | "description": "通过例子学习标准库", 4 | "author": "徐新华", 5 | "language": "zh-hans", 6 | "root": ".", 7 | "links": { 8 | "sidebar": { 9 | "Go语言中文网": "http://studygolang.com" 10 | } 11 | }, 12 | "plugins": [ 13 | "-lunr", 14 | "-search", 15 | "-livereload", 16 | "highlight", 17 | "search-plus@^0.0.11", 18 | "simple-page-toc@^0.1.1", 19 | "github@^2.0.0", 20 | "github-buttons@2.1.0", 21 | "edit-link@^2.0.2", 22 | "disqus@^0.1.0", 23 | "anchors@^0.7.1", 24 | "include-codeblock@^3.0.2", 25 | "emphasize@^1.1.0", 26 | "mermaid@^0.0.9", 27 | "tbfed-pagefooter@^0.0.1", 28 | "expandable-chapters-small@^0.1.7", 29 | "sectionx@^3.1.0", 30 | "donate@^1.0.2", 31 | "sitemap-general", 32 | "anchor-navigation-ex", 33 | "favicon@^0.0.2", 34 | "todo@^0.1.3", 35 | "ba" 36 | ], 37 | "pluginsConfig": { 38 | "theme-default": { 39 | "showLevel": true 40 | }, 41 | "ba": { 42 | "token": "224c227cd9239761ec770bc8c1fb134c" 43 | }, 44 | "disqus": { 45 | "shortName": "Go语言中文网" 46 | }, 47 | "github": { 48 | "url": "https://github.com/polaris1119/The-Golang-Standard-Library-by-Example" 49 | }, 50 | "github-buttons": { 51 | "repo": "polaris1119/The-Golang-Standard-Library-by-Example", 52 | "types": [ 53 | "star" 54 | ], 55 | "size": "small" 56 | }, 57 | "include-codeblock": { 58 | "template": "ace", 59 | "unindent": true, 60 | "edit": true 61 | }, 62 | "sharing": { 63 | "weibo": true, 64 | "facebook": true, 65 | "twitter": true, 66 | "google": false, 67 | "instapaper": false, 68 | "vk": false, 69 | "all": [ 70 | "facebook", 71 | "google", 72 | "twitter", 73 | "weibo", 74 | "instapaper" 75 | ] 76 | }, 77 | "tbfed-pagefooter": { 78 | "copyright": "Copyright © studygolang.com 2013", 79 | "modify_label": "该文件修订时间:", 80 | "modify_format": "YYYY-MM-DD HH:mm:ss" 81 | }, 82 | "donate": { 83 | "wechat": "http://studygolang.qiniudn.com/img/wxpay.png", 84 | "alipay": "http://studygolang.qiniudn.com/170605/9d11b43988fbbb42a5da9f970a0f6818.png", 85 | "title": "", 86 | "button": "赏", 87 | "alipayText": "支付宝打赏", 88 | "wechatText": "微信打赏" 89 | }, 90 | "simple-page-toc": { 91 | "maxDepth": 3, 92 | "skipFirstH1": true 93 | }, 94 | "edit-link": { 95 | "base": "https://github.com/polaris1119/The-Golang-Standard-Library-by-Example/edit/master", 96 | "label": "编辑该页面" 97 | }, 98 | "sitemap-general": { 99 | "prefix": "http://books.studygolang.com" 100 | }, 101 | "anchor-navigation-ex": { 102 | "isRewritePageTitle": false, 103 | "tocLevel1Icon": "fa fa-hand-o-right", 104 | "tocLevel2Icon": "fa fa-hand-o-right", 105 | "tocLevel3Icon": "fa fa-hand-o-right" 106 | }, 107 | "sectionx": { 108 | "tag": "b" 109 | }, 110 | "favicon": { 111 | "shortcut": "favicon.ico", 112 | "bookmark": "favicon.ico" 113 | } 114 | } 115 | } -------------------------------------------------------------------------------- /chapter01/01.0.md: -------------------------------------------------------------------------------- 1 | # 第一章 输入输出 (Input/Output) # 2 | 3 | 一般的,计算机程序是:输入 (Input) 经过算法处理产生输出 (Output)。各种语言一般都会提供IO库供开发者使用。Go语言也不例外。 4 | 5 | Go 语言中,为了方便开发者使用,将 IO 操作封装在了如下几个包中: 6 | 7 | - [io](http://docs.studygolang.com/pkg/io/) 为 IO 原语(I/O primitives)提供基本的接口 8 | - [io/ioutil](http://docs.studygolang.com/pkg/io/ioutil/) 封装一些实用的 I/O 函数 9 | - [fmt](http://docs.studygolang.com/pkg/fmt/) 实现格式化 I/O,类似 C 语言中的 printf 和 scanf 10 | - [bufio](http://docs.studygolang.com/pkg/bufio/) 实现带缓冲I/O 11 | 12 | 本章会详细介绍这些 IO 包提供的函数、类型和方法,同时通过实例讲解这些包的使用方法。 13 | 14 | # 导航 # 15 | 16 | - [简介](/README.md) 17 | - 下一节:[io — 基本的 IO 接口](01.1.md) 18 | -------------------------------------------------------------------------------- /chapter01/01.2.md: -------------------------------------------------------------------------------- 1 | # 1.2 ioutil — 方便的IO操作函数集 # 2 | 3 | 虽然 io 包提供了不少类型、方法和函数,但有时候使用起来不是那么方便。比如读取一个文件中的所有内容。为此,标准库中提供了一些常用、方便的IO操作函数。 4 | 5 | 说明:这些函数使用都相对简单,一般就不举例子了。 6 | 7 | ## NopCloser 函数 ## 8 | 9 | 有时候我们需要传递一个io.ReadCloser的实例,而我们现在有一个io.Reader的实例,比如:strings.Reader,这个时候NopCloser就派上用场了。它包装一个io.Reader,返回一个io.ReadCloser,而相应的Close方法啥也不做,只是返回nil。 10 | 11 | 比如,在标准库net/http包中的NewRequest,接收一个io.Reader的body,而实际上,Request的Body的类型是io.ReadCloser,因此,代码内部进行了判断,如果传递的io.Reader也实现了io.ReadCloser接口,则转换,否则通过ioutil.NopCloser包装转换一下。相关代码如下: 12 | 13 | rc, ok := body.(io.ReadCloser) 14 | if !ok && body != nil { 15 | rc = ioutil.NopCloser(body) 16 | } 17 | 18 | 如果没有这个函数,我们得自己实现一个。当然,实现起来很简单,读者可以看看NopCloser的实现。 19 | 20 | ## ReadAll 函数 ## 21 | 22 | 很多时候,我们需要一次性读取io.Reader中的数据,通过上一节的讲解,我们知道有很多种实现方式。考虑到读取所有数据的需求比较多,Go提供了ReadAll这个函数,用来从io.Reader中一次读取所有数据。 23 | 24 | func ReadAll(r io.Reader) ([]byte, error) 25 | 26 | 阅读该函数的源码发现,它是通过bytes.Buffer中的ReadFrom来实现读取所有数据的。 27 | 28 | ## ReadDir 函数 ## 29 | 30 | 笔试题:编写程序输出某目录下的所有文件(包括子目录) 31 | 32 | 是否见过这样的笔试题? 33 | 34 | 在Go中如何输出目录下的所有文件呢?首先,我们会想到查os包,看File类型是否提供了相关方法(关于os包,后面会讲解)。 35 | 36 | 其实在ioutil中提供了一个方便的函数:ReadDir,它读取目录并返回排好序的文件和子目录名([]os.FileInfo)。通过这个方法,我们可以很容易的实现“面试题”。 37 | 38 | 下面的例子实现了类似Unix中的tree命令,不过它在windows下也运行的很好哦。 39 | 40 | // 未实现-L参数功能 41 | func main() { 42 | if len(os.Args) > 1 { 43 | Tree(os.Args[1], 1, map[int]bool{1:true}) 44 | } 45 | } 46 | 47 | // 列出dirname目录中的目录树,实现类似Unix中的tree命令 48 | // curHier 当前层级(dirname为第一层) 49 | // hierMap 当前层级的上几层是否需要'|'的映射 50 | func Tree(dirname string, curHier int, hierMap map[int]bool) error { 51 | dirAbs, err := filepath.Abs(dirname) 52 | if err != nil { 53 | return err 54 | } 55 | fileInfos, err := ioutil.ReadDir(dirAbs) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | fileNum := len(fileInfos) 61 | for i, fileInfo := range fileInfos { 62 | for j := 1; j < curHier; j++ { 63 | if hierMap[j] { 64 | fmt.Print("|") 65 | } else { 66 | fmt.Print(" ") 67 | } 68 | fmt.Print(strings.Repeat(" ", 3)) 69 | } 70 | 71 | // map是引用类型,所以新建一个map 72 | tmpMap := map[int]bool{} 73 | for k, v := range hierMap { 74 | tmpMap[k] = v 75 | } 76 | if i+1 == fileNum { 77 | fmt.Print("`") 78 | delete(tmpMap, curHier) 79 | } else { 80 | fmt.Print("|") 81 | tmpMap[curHier] = true 82 | } 83 | fmt.Print("-- ") 84 | fmt.Println(fileInfo.Name()) 85 | if fileInfo.IsDir() { 86 | Tree(filepath.Join(dirAbs, fileInfo.Name()), curHier+1, tmpMap) 87 | } 88 | } 89 | return nil 90 | } 91 | 92 | ## ReadFile 和 WriteFile 函数 ## 93 | 94 | ReadFile 读取整个文件的内容,在上一节我们自己实现了一个函数读取文件整个内容,由于这种需求很常见,因此Go提供了ReadFile函数,方便使用。ReadFile的是实现和ReadAll类似,不过,ReadFile会先判断文件的大小,给bytes.Buffer一个预定义容量,避免额外分配内存。 95 | 96 | WriteFile 函数的签名如下: 97 | 98 | func WriteFile(filename string, data []byte, perm os.FileMode) error 99 | 100 | 它将data写入filename文件中,当文件不存在时会创建一个(文件权限由perm指定);否则会先清空文件内容。对于perm参数,我们一般可以指定为:0666,具体含义os包中讲解。 101 | 102 | **小提示** 103 | 104 | ReadFile 源码中先获取了文件的大小,当大小 < 1e9时,才会用到文件的大小。按源码中注释的说法是FileInfo不会很精确地得到文件大小。 105 | 106 | ## TempDir 和 TempFile 函数 ## 107 | 108 | 操作系统中一般都会提供临时目录,比如linux下的/tmp目录(通过os.TempDir()可以获取到)。有时候,我们自己需要创建临时目录,比如Go工具链源码中(src/cmd/go/build.go),通过TempDir创建一个临时目录,用于存放编译过程的临时文件: 109 | 110 | b.work, err = ioutil.TempDir("", "go-build") 111 | 112 | 第一个参数如果为空,表明在系统默认的临时目录(os.TempDir)中创建临时目录;第二个参数指定临时目录名的前缀,该函数返回临时目录的路径。 113 | 114 | 相应的,TempFile用于创建临时文件。如gofmt命令的源码中创建临时文件: 115 | 116 | f1, err := ioutil.TempFile("", "gofmt") 117 | 118 | 参数和ioutil.TempDir参数含义类似。 119 | 120 | 这里需要**注意**:创建者创建的临时文件和临时目录要负责删除这些临时目录和文件。如删除临时文件: 121 | 122 | defer func() { 123 | f.Close() 124 | os.Remove(f.Name()) 125 | }() 126 | 127 | ## Discard 变量 ## 128 | 129 | Discard 对应的类型(`type devNull int`)实现了io.Writer接口,同时,为了优化io.Copy到Discard,避免不必要的工作,实现了io.ReaderFrom接口。 130 | 131 | devNull 在实现io.Writer接口时,只是简单的返回(标准库文件:src/pkg/io/ioutil.go)。 132 | 133 | func (devNull) Write(p []byte) (int, error) { 134 | return len(p), nil 135 | } 136 | 137 | 而ReadFrom的实现是读取内容到一个buf中,最大也就8192字节,其他的会丢弃(当然,这个也不会读取)。 138 | 139 | # 导航 # 140 | 141 | - [目录](/preface.md) 142 | - 上一节:[io — 基本的IO接口](01.1.md) 143 | - 下一节:[fmt — 格式化IO](01.3.md) 144 | -------------------------------------------------------------------------------- /chapter01/01.3.md: -------------------------------------------------------------------------------- 1 | # 1.3 fmt — 格式化IO # 2 | 3 | fmt 包实现了格式化I/O函数,类似于C的 printf 和 scanf. 格式“占位符”衍生自C,但比C更简单。 4 | 5 | fmt 包的官方文档对Printing和Scanning有很详细的说明。这里就直接引用文档进行说明,同时附上额外的说明或例子,之后再介绍具体的函数使用。 6 | 7 | 以下例子中用到的类型或变量定义: 8 | 9 | type Website struct { 10 | Name string 11 | } 12 | 13 | // 打印结构体时 14 | var site = Website{Name:"studygolang"} 15 | 16 | ## Printing ## 17 | 18 | ### 占位符 ### 19 | 20 | **普通占位符** 21 | 22 | 占位符 说明 举例 输出 23 | %v 相应值的默认格式。 Printf("%v", site),Printf("%+v", site) {studygolang},{Name:studygolang} 24 | 在打印结构体时,“加号”标记(%+v)会添加字段名 25 | %#v 相应值的Go语法表示 Printf("#v", site) main.Website{Name:"studygolang"} 26 | %T 相应值的类型的Go语法表示 Printf("%T", site) main.Website 27 | %% 字面上的百分号,并非值的占位符 Printf("%%") % 28 | 29 | **布尔占位符** 30 | 31 | 占位符 说明 举例 输出 32 | %t 单词 true 或 false。 Printf("%t", true) true 33 | 34 | **整数占位符** 35 | 36 | 占位符 说明 举例 输出 37 | %b 二进制表示 Printf("%b", 5) 101 38 | %c 相应Unicode码点所表示的字符 Printf("%c", 0x4E2D) 中 39 | %d 十进制表示 Printf("%d", 0x12) 18 40 | %o 八进制表示 Printf("%d", 10) 12 41 | %q 单引号围绕的字符字面值,由Go语法安全地转义 Printf("%q", 0x4E2D) '中' 42 | %x 十六进制表示,字母形式为小写 a-f Printf("%x", 13) d 43 | %X 十六进制表示,字母形式为大写 A-F Printf("%x", 13) D 44 | %U Unicode格式:U+1234,等同于 "U+%04X" Printf("%U", 0x4E2D) U+4E2D 45 | 46 | **浮点数和复数的组成部分(实部和虚部)** 47 | 48 | 占位符 说明 举例 输出 49 | %b 无小数部分的,指数为二的幂的科学计数法,与 strconv.FormatFloat 50 | 的 'b' 转换格式一致。例如 -123456p-78 51 | %e 科学计数法,例如 -1234.456e+78 Printf("%e", 10.2) 1.020000e+01 52 | %E 科学计数法,例如 -1234.456E+78 Printf("%e", 10.2) 1.020000E+01 53 | %f 有小数点而无指数,例如 123.456 Printf("%f", 10.2) 10.200000 54 | %g 根据情况选择 %e 或 %f 以产生更紧凑的(无末尾的0)输出 Printf("%g", 10.20) 10.2 55 | %G 根据情况选择 %E 或 %f 以产生更紧凑的(无末尾的0)输出 Printf("%G", 10.20+2i) (10.2+2i) 56 | 57 | **字符串与字节切片** 58 | 59 | 占位符 说明 举例 输出 60 | %s 输出字符串表示(string类型或[]byte) Printf("%s", []byte("Go语言中文网")) Go语言中文网 61 | %q 双引号围绕的字符串,由Go语法安全地转义 Printf("%q", "Go语言中文网") "Go语言中文网" 62 | %x 十六进制,小写字母,每字节两个字符 Printf("%x", "golang") 676f6c616e67 63 | %X 十六进制,大写字母,每字节两个字符 Printf("%X", "golang") 676F6C616E67 64 | 65 | **指针** 66 | 67 | 占位符 说明 举例 输出 68 | %p 十六进制表示,前缀 0x Printf("%p", &site) 0x4f57f0 69 | 70 | 这里没有 'u' 标记。若整数为无符号类型,他们就会被打印成无符号的。类似地,这里也不需要指定操作数的大小(int8,int64)。 71 | 72 | 宽度与精度的控制格式以Unicode码点为单位。(这点与C的 printf 不同,它以字节数为单位)二者或其中之一均可用字符 '*' 表示,此时它们的值会从下一个操作数中获取,该操作数的类型必须为 int。 73 | 74 | 对数值而言,宽度为该数值占用区域的最小宽度;精度为小数点之后的位数。 但对于 %g/%G 而言,精度为所有数字的总数。例如,对于123.45,格式 %6.2f 会打印123.45,而 %.4g 会打印123.5。%e 和 %f 的默认精度为6;但对于 %g 而言,它的默认精度为确定该值所必须的最小位数。 75 | 76 | 对大多数的值而言,宽度为输出的最小字符数,如果必要的话会为已格式化的形式填充空格。对字符串而言,精度为输出的最大字符数,如果必要的话会直接截断。 77 | 78 | **其它标记** 79 | 80 | 占位符 说明 举例 输出 81 | + 总打印数值的正负号;对于%q(%+q)保证只输出ASCII编码的字符。 Printf("%+q", "中文") "\u4e2d\u6587" 82 | - 在右侧而非左侧填充空格(左对齐该区域) 83 | # 备用格式:为八进制添加前导 0(%#o),为十六进制添加前导 0x(%#x)或 Printf("%#U", '中') U+4E2D '中' 84 | 0X(%#X),为 %p(%#p)去掉前导 0x;如果可能的话,%q(%#q)会打印原始 85 | (即反引号围绕的)字符串;如果是可打印字符,%U(%#U)会写出该字符的 86 | Unicode 编码形式(如字符 x 会被打印成 U+0078 'x')。 87 | ' ' (空格)为数值中省略的正负号留出空白(% d); 88 | 以十六进制(% x, % X)打印字符串或切片时,在字节之间用空格隔开 89 | 0 填充前导的0而非空格;对于数字,这会将填充移到正负号之后 90 | 91 | 标记有时会被占位符忽略,所以不要指望它们。例如十进制没有备用格式,因此 %#d 与 %d 的行为相同。 92 | 93 | 对于每一个 Printf 类的函数,都有一个 Print 函数,该函数不接受任何格式化,它等价于对每一个操作数都应用 %v。另一个变参函数 Println 会在操作数之间插入空白,并在末尾追加一个换行符。 94 | 95 | 不考虑占位符的话,如果操作数是接口值,就会使用其内部的具体值,而非接口本身。 因此: 96 | 97 | var i interface{} = 23 98 | fmt.Printf("%v\n", i) 99 | 100 | 会打印 23。 101 | 102 | 若一个操作数实现了 Formatter 接口,该接口就能更好地用于控制格式化。 103 | 104 | 若其格式(它对于 Println 等函数是隐式的 %v)对于字符串是有效的 (%s %q %v %x %X),以下两条规则也适用: 105 | 106 | 1. 若一个操作数实现了 error 接口,Error 方法就能将该对象转换为字符串,随后会根据占位符的需要进行格式化。 107 | 2. 若一个操作数实现了 String() string 方法,该方法能将该对象转换为字符串,随后会根据占位符的需要进行格式化。 108 | 109 | 为避免以下这类递归的情况: 110 | 111 | type X string 112 | func (x X) String() string { return Sprintf("<%s>", x) } 113 | 114 | 需要在递归前转换该值: 115 | 116 | func (x X) String() string { return Sprintf("<%s>", string(x)) } 117 | 118 | **格式化错误** 119 | 120 | 如果给占位符提供了无效的实参(例如将一个字符串提供给 %d),所生成的字符串会包含该问题的描述,如下例所示: 121 | 122 | 类型错误或占位符未知:%!verb(type=value) 123 | Printf("%d", hi): %!d(string=hi) 124 | 实参太多:%!(EXTRA type=value) 125 | Printf("hi", "guys"): hi%!(EXTRA string=guys) 126 | 实参太少: %!verb(MISSING) 127 | Printf("hi%d"): hi %!d(MISSING) 128 | 宽度或精度不是int类型: %!(BADWIDTH) 或 %!(BADPREC) 129 | Printf("%*s", 4.5, "hi"): %!(BADWIDTH)hi 130 | Printf("%.*s", 4.5, "hi"): %!(BADPREC)hi 131 | 所有错误都始于“%!”,有时紧跟着单个字符(占位符),并以小括号括住的描述结尾。 132 | 133 | ## Scanning ## 134 | 135 | 一组类似的函数通过扫描已格式化的文本来产生值。Scan、Scanf 和 Scanln 从 os.Stdin 中读取;Fscan、Fscanf 和 Fscanln 从指定的 io.Reader 中读取; Sscan、Sscanf 和 Sscanln 从实参字符串中读取。Scanln、Fscanln 和 Sscanln 在换行符处停止扫描,且需要条目紧随换行符之后;Scanf、Fscanf 和 Sscanf 需要输入换行符来匹配格式中的换行符;其它函数则将换行符视为空格。 136 | 137 | Scanf、Fscanf 和 Sscanf 根据格式字符串解析实参,类似于 Printf。例如,%x 会将一个整数扫描为十六进制数,而 %v 则会扫描该值的默认表现格式。 138 | 139 | 格式化行为类似于 Printf,但也有如下例外: 140 | 141 | %p 没有实现 142 | %T 没有实现 143 | %e %E %f %F %g %G 都完全等价,且可扫描任何浮点数或复数数值 144 | %s 和 %v 在扫描字符串时会将其中的空格作为分隔符 145 | 标记 # 和 + 没有实现 146 | 147 | 在使用 %v 占位符扫描整数时,可接受友好的进制前缀0(八进制)和0x(十六进制)。 148 | 149 | 宽度被解释为输入的文本(%5s 意为最多从输入中读取5个 rune 来扫描成字符串),而扫描函数则没有精度的语法(没有 %5.2f,只有 %5f)。 150 | 151 | 当以某种格式进行扫描时,无论在格式中还是在输入中,所有非空的连续空白字符 (除换行符外)都等价于单个空格。由于这种限制,格式字符串文本必须匹配输入的文本,如果不匹配,扫描过程就会停止,并返回已扫描的实参数。 152 | 153 | 在所有的扫描参数中,若一个操作数实现了 Scan 方法(即它实现了 Scanner 接口), 该操作数将使用该方法扫描其文本。此外,若已扫描的实参数少于所提供的实参数,就会返回一个错误。 154 | 155 | 所有需要被扫描的实参都必须是基本类型或 Scanner 接口的实现。 156 | 157 | 注意:Fscan 等函数会从输入中多读取一个字符(rune),因此,如果循环调用扫描函数,可能会跳过输入中的某些数据。一般只有在输入的数据中没有空白符时该问题才会出现。若提供给 Fscan 的读取器实现了 ReadRune,就会用该方法读取字符。若此读取器还实现了 UnreadRune 方法,就会用该方法保存字符,而连续的调用将不会丢失数据。若要为没有 ReadRune 和 UnreadRune 方法的读取器加上这些功能,需使用 bufio.NewReader。 158 | 159 | ## Print 序列函数 ## 160 | 161 | 这里说的 Print 序列函数包括:Fprint/Fprintf/Fprintln/Sprint/Sprintf/Sprintln/Print/Printf/Println。之所以将放在一起介绍,是因为它们的使用方式类似、参数意思也类似。 162 | 163 | 一般的,我们将Fprint/Fprintf/Fprintln归为一类;Sprint/Sprintf/Sprintln归为一类;Print/Printf/Println归为另一类。其中,Print/Printf/Println会调用相应的F开头一类函数。如: 164 | 165 | func Print(a ...interface{}) (n int, err error) { 166 | return Fprint(os.Stdout, a...) 167 | } 168 | 169 | Fprint/Fprintf/Fprintln 函数的第一个参数接收一个io.Writer类型,会将内容输出到io.Writer中去。而Print/Printf/Println 函数是将内容输出到标准输出中,因此,直接调用F类函数做这件事,并将os.Stdout作为第一个参数传入。 170 | 171 | Sprint/Sprintf/Sprintln 是格式化内容为string类型,而并不输出到某处,需要格式化字符串并返回时,可以用次组函数。 172 | 173 | 在这三组函数中,`S/F/Printf`函数通过指定的格式输出或格式化内容;`S/F/Print`函数只是使用默认的格式输出或格式化内容;`S/F/Println`函数使用默认的格式输出或格式化内容,同时会在最后加上"换行符"。 174 | 175 | Print 序列函数的最后一个参数都是 `a ...interface{}` 这种不定参数。对于`S/F/Printf`序列,这个不定参数的实参个数应该和`formt`参数的占位符个数一致,否则会出现格式化错误;而对于其他函数,当不定参数的实参个数为多个时,它们之间会直接(对于`S/F/Print`)或通过" "(空格)(对于`S/F/Println`)连接起来(注:对于`S/F/Print`,当两个参数都不是字符串时,会自动添加一个空格,否则不会加。感谢guoshanhe1983 反馈。[官方 effective_go](http://docs.studygolang.com/doc/effective_go.html#Printing) 也有说明)。利用这一点,我们可以做如下事情: 176 | 177 | result1 := fmt.Sprintln("studygolang.com", 2013) 178 | result2 := fmt.Sprint("studygolang.com", 2013) 179 | 180 | result1的值是:`studygolang.com 2013`,result2的值是:`studygolang.com2013`。这起到了连接字符串的作用,而不需要通过strconv.Itoa()转换。 181 | 182 | Print序列函数用的较多,而且也易于使用(可能需要掌握一些常用的占位符用法),接下来我们结合fmt包中几个相关的接口来掌握更多关于Print的内容。 183 | 184 | ## Stringer 接口 ## 185 | 186 | Stringer接口的定义如下: 187 | 188 | type Stringer interface { 189 | String() string 190 | } 191 | 192 | 根据Go语言中实现接口的定义,一个类型只要有 `String() string` 方法,我们就说它实现了Stringer接口。而在本节开始已经说到,如果格式化输出某种类型的值,只要它实现了String()方法,那么会调用String()方法进行处理。 193 | 194 | 我们定义如下struct: 195 | 196 | type Person struct { 197 | Name string 198 | Age int 199 | Sex int 200 | } 201 | 202 | 我们给Person实现String方法,这个时候,我们输出Person的实例: 203 | 204 | p := &Person{"polaris", 28, 0} 205 | fmt.Println(p) 206 | 207 | 输出: 208 | 209 | &{polaris 28 0} 210 | 211 | 接下来,为Person增加String方法。 212 | 213 | func (this *Person) String() string { 214 | buffer := bytes.NewBufferString("This is ") 215 | buffer.WriteString(this.Name + ", ") 216 | if this.Sex == 0 { 217 | buffer.WriteString("He ") 218 | } else { 219 | buffer.WriteString("She ") 220 | } 221 | buffer.WriteString("is ") 222 | buffer.WriteString(strconv.Itoa(this.Age)) 223 | buffer.WriteString(" years old.") 224 | return buffer.String() 225 | } 226 | 227 | 这个时候运行: 228 | 229 | p := &Person{"polaris", 28, 0} 230 | fmt.Println(p) 231 | 232 | 输出变为: 233 | 234 | This is polaris, He is 28 years old 235 | 236 | 可见,Stringer接口和Java中的ToString方法类似。 237 | 238 | ## Formatter 接口 ## 239 | 240 | Formatter接口的定义如下: 241 | 242 | type Formatter interface { 243 | Format(f State, c rune) 244 | } 245 | 246 | 官方文档中关于该接口方法的说明: 247 | 248 | > Formatter 接口由带有定制的格式化器的值所实现。 Format 的实现可调用 Sprintf 或 Fprintf(f) 等函数来生成其输出。 249 | 250 | 也就是说,通过实现Formatter接口可以做到自定义输出格式(自定义占位符)。 251 | 252 | 接着上面的例子,我们为Person增加一个方法: 253 | 254 | func (this *Person) Format(f fmt.State, c rune) { 255 | if c == 'L' { 256 | f.Write([]byte(this.String())) 257 | f.Write([]byte(" Person has three fields.")) 258 | } else { 259 | // 没有此句,会导致 fmt.Printf("%s", p) 啥也不输出 260 | f.Write([]byte(fmt.Sprintln(this.String()))) 261 | } 262 | } 263 | 264 | 这样,Person便实现了Formatter接口。这时再运行: 265 | 266 | p := &Person{"polaris", 28, 0} 267 | fmt.Printf("%L", p) 268 | 269 | 输出为: 270 | 271 | This is polaris, He is 28 years old. Person has three fields. 272 | 273 | 这里需要解释以下几点: 274 | 275 | 1)fmt.State 是一个接口。由于Format方法是被fmt包调用的,它内部会实例化好一个fmt.State接口的实例,我们不需要关心该接口; 276 | 277 | 2)可以实现自定义占位符,同时fmt包中和类型相对应的预定义占位符会无效。因此例子中Format的实现加上了else子句; 278 | 279 | 3)实现了Formatter接口,相应的Stringer接口不起作用。但实现了Formatter接口的类型应该实现Stringer接口,这样方便在Format方法中调用String()方法。就像本例的做法; 280 | 281 | 4)Format方法的第二个参数是占位符中%后的字母(有精度和宽度会被忽略,只保留字母); 282 | 283 | 一般地,我们不需要实现Formatter接口。如果对Formatter接口的实现感兴趣,可以看看标准库math/big包中Int类型的Formatter接口实现。 284 | 285 | **小贴士** 286 | 287 | State接口相关说明: 288 | 289 | type State interface { 290 | // Write is the function to call to emit formatted output to be printed. 291 | // Write 函数用于打印出已格式化的输出。 292 | Write(b []byte) (ret int, err error) 293 | // Width returns the value of the width option and whether it has been set. 294 | // Width 返回宽度选项的值以及它是否已被设置。 295 | Width() (wid int, ok bool) 296 | // Precision returns the value of the precision option and whether it has been set. 297 | // Precision 返回精度选项的值以及它是否已被设置。 298 | Precision() (prec int, ok bool) 299 | 300 | // Flag returns whether the flag c, a character, has been set. 301 | // Flag 返回标记 c(一个字符)是否已被设置。 302 | Flag(c int) bool 303 | } 304 | 305 | fmt包中的print.go文件中的`type pp struct`实现了State接口。由于State接口有Write方法,因此,实现了State接口的类型必然实现了io.Writer接口。 306 | 307 | ## GoStringer 接口 ## 308 | 309 | GoStringer 接口定义如下; 310 | 311 | type GoStringer interface { 312 | GoString() string 313 | } 314 | 315 | 该接口定义了类型的Go语法格式。用于打印(Printf)格式化占位符为%#v的值。 316 | 317 | 用前面的例子演示。执行: 318 | 319 | p := &Person{"polaris", 28, 0} 320 | fmt.Printf("%#v", p) 321 | 322 | 输出: 323 | 324 | &main.Person{Name:"polaris", Age:28, Sex:0} 325 | 326 | 接着为Person增加方法: 327 | 328 | func (this *Person) GoString() string { 329 | return "&Person{Name is "+this.Name+", Age is "+strconv.Itoa(this.Age)+", Sex is "+strconv.Itoa(this.Sex)+"}" 330 | } 331 | 332 | 这个时候再执行 333 | 334 | p := &Person{"polaris", 28, 0} 335 | fmt.Printf("%#v", p) 336 | 337 | 输出: 338 | 339 | &Person{Name is polaris, Age is 28, Sex is 0} 340 | 341 | 一般的,我们不需要实现该接口。 342 | 343 | ## Scan 序列函数 ## 344 | 345 | 该序列函数和 Print 序列函数相对应,包括:Fscan/Fscanf/Fscanln/Sscan/Sscanf/Sscanln/Scan/Scanf/Scanln。 346 | 347 | 一般的,我们将Fscan/Fscanf/Fscanln归为一类;Sscan/Sscanf/Sscanln归为一类;Scan/Scanf/Scanln归为另一类。其中,Scan/Scanf/Scanln会调用相应的F开头一类函数。如: 348 | 349 | func Scan(a ...interface{}) (n int, err error) { 350 | return Fscan(os.Stdin, a...) 351 | } 352 | 353 | Fscan/Fscanf/Fscanln 函数的第一个参数接收一个io.Reader类型,从其读取内容并赋值给相应的实参。而 Scan/Scanf/Scanln 正是从标准输入获取内容,因此,直接调用F类函数做这件事,并将os.Stdin作为第一个参数传入。 354 | 355 | Sscan/Sscanf/Sscanln 则直接从字符串中获取内容。 356 | 357 | 对于Scan/Scanf/Scanln三个函数的区别,我们通过例子来说明,为了方便讲解,我们使用Sscan/Sscanf/Sscanln这组函数。 358 | 359 | 1) Scan/FScan/Sscan 360 | 361 | var ( 362 | name string 363 | age int 364 | ) 365 | n, _ := fmt.Sscan("polaris 28", &name, &age) 366 | // 可以将"polaris 28"中的空格换成"\n"试试 367 | // n, _ := fmt.Sscan("polaris\n28", &name, &age) 368 | fmt.Println(n, name, age) 369 | 370 | 输出为: 371 | 372 | 2 polaris 28 373 | 374 | 不管"polaris 28"是用空格分隔还是"\n"分隔,输出一样。也就是说,`Scan/FScan/Sscan` 这组函数将连续由空格分隔的值存储为连续的实参(换行符也记为空格)。 375 | 376 | 2) Scanf/FScanf/Sscanf 377 | 378 | var ( 379 | name string 380 | age int 381 | ) 382 | n, _ := fmt.Sscanf("polaris 28", "%s%d", &name, &age) 383 | // 可以将"polaris 28"中的空格换成"\n"试试 384 | // n, _ := fmt.Sscanf("polaris\n28", "%s%d", &name, &age) 385 | fmt.Println(n, name, age) 386 | 387 | 输出: 388 | 389 | 2 polaris 28 390 | 391 | 如果将"空格"分隔改为"\n"分隔,则输出为:1 polaris 0。可见,`Scanf/FScanf/Sscanf` 这组函数将连续由空格分隔的值存储为连续的实参, 其格式由 `format` 决定,换行符处停止扫描(Scan)。 392 | 393 | 3) Scanln/FScanln/Sscanln 394 | 395 | var ( 396 | name string 397 | age int 398 | ) 399 | n, _ := fmt.Sscanln("polaris 28", &name, &age) 400 | // 可以将"polaris 28"中的空格换成"\n"试试 401 | // n, _ := fmt.Sscanln("polaris\n28", &name, &age) 402 | fmt.Println(n, name, age) 403 | 404 | 输出: 405 | 406 | 2 polaris 28 407 | 408 | `Scanln/FScanln/Sscanln`表现和上一组一样,遇到"\n"停止(对于Scanln,表示从标准输入获取内容,最后需要回车)。 409 | 410 | 一般地,我们使用 `Scan/Scanf/Scanln` 这组函数。 411 | 412 | **提示** 413 | 414 | 如果你是Windows系统,在使用 `Scanf` 时,有一个地方需要注意。看下面的代码: 415 | 416 | for i := 0; i < 2; i++ { 417 | var name string 418 | fmt.Print("Input Name:") 419 | n, err := fmt.Scanf("%s", &name) 420 | fmt.Println(n, err, name) 421 | } 422 | 423 | 编译、运行(或直接 go run ),输入:polaris回车。控制台内如下: 424 | 425 | Input Name:polaris 426 | 1 polaris 427 | Input Name:0 unexpected newline 428 | 429 | 为什么不是让输入两次?第二次好像有默认值一样。 430 | 431 | 同样的代码在Linux下正常。个人认为这是go在Windows下的一个bug,已经向官方提出:[issue5391](https://code.google.com/p/go/issues/detail?id=5391)。 432 | 433 | 目前的解决方法是:换用Scanln或者改为Scanf("%s\n", &name)。 434 | 435 | ## Scanner 和 ScanState 接口 ## 436 | 437 | 基本上,我们不会去自己实现这两个接口,只需要使用上文中相应的Scan函数就可以了。这里只是简单的介绍一下这两个接口的作用。 438 | 439 | 任何实现了Scan方法的对象都实现了Scanner接口,Scan方法会从输入读取数据并将处理结果存入接收端,接收端必须是有效的指针。Scan方法会被任何Scan、Scanf、Scanln等函数调用,只要对应的参数实现了该方法。Scan方法接收的第一个参数为`ScanState`接口类型。 440 | 441 | ScanState是一个交给用户定制的Scanner接口的参数的接口。Scanner接口可能会进行一次一个字符的扫描或者要求ScanState去探测下一个空白分隔的token。该接口的方法基本上在io包中都有讲解,这里不赘述。 442 | 443 | 在fmt包中,scan.go文件中的 ss 结构实现了 ScanState 接口。 444 | 445 | # 导航 # 446 | 447 | - [目录](/preface.md) 448 | - 上一节:[ioutil — 方便的IO操作函数集](01.2.md) 449 | - 下一节:[bufio — 缓存IO](01.4.md) -------------------------------------------------------------------------------- /chapter01/01.4.md: -------------------------------------------------------------------------------- 1 | # 1.4 bufio — 缓存IO # 2 | 3 | bufio 包实现了缓存IO。它包装了 io.Reader 和 io.Writer 对象,创建了另外的Reader和Writer对象,它们也实现了io.Reader和io.Writer接口,不过它们是有缓存的。该包同时为文本I/O提供了一些便利操作。 4 | 5 | ## 1.4.1 Reader 类型和方法 ## 6 | 7 | bufio.Reader 结构包装了一个 io.Reader 对象,提供缓存功能,同时实现了 io.Reader 接口。 8 | 9 | Reader 结构没有任何导出的字段,结构定义如下: 10 | 11 | type Reader struct { 12 | buf []byte // 缓存 13 | rd io.Reader // 底层的io.Reader 14 | // r:从buf中读走的字节(偏移);w:buf中填充内容的偏移; 15 | // w - r 是buf中可被读的长度(缓存数据的大小),也是Buffered()方法的返回值 16 | r, w int 17 | err error // 读过程中遇到的错误 18 | lastByte int // 最后一次读到的字节(ReadByte/UnreadByte) 19 | lastRuneSize int // 最后一次读到的Rune的大小(ReadRune/UnreadRune) 20 | } 21 | 22 | ### 1.4.1.1 实例化 ### 23 | 24 | bufio 包提供了两个实例化 bufio.Reader 对象的函数:NewReader 和 NewReaderSize。其中,NewReader 函数是调用 NewReaderSize 函数实现的: 25 | 26 | func NewReader(rd io.Reader) *Reader { 27 | // 默认缓存大小:defaultBufSize=4096 28 | return NewReaderSize(rd, defaultBufSize) 29 | } 30 | 31 | 我们看一下NewReaderSize的源码: 32 | 33 | func NewReaderSize(rd io.Reader, size int) *Reader { 34 | // 已经是bufio.Reader类型,且缓存大小不小于 size,则直接返回 35 | b, ok := rd.(*Reader) 36 | if ok && len(b.buf) >= size { 37 | return b 38 | } 39 | // 缓存大小不会小于 minReadBufferSize (16字节) 40 | if size < minReadBufferSize { 41 | size = minReadBufferSize 42 | } 43 | // 构造一个bufio.Reader实例 44 | return &Reader{ 45 | buf: make([]byte, size), 46 | rd: rd, 47 | lastByte: -1, 48 | lastRuneSize: -1, 49 | } 50 | } 51 | 52 | ### 1.4.1.2 ReadSlice、ReadBytes、ReadString 和 ReadLine 方法 ### 53 | 54 | 之所以将这几个方法放在一起,是因为他们有着类似的行为。事实上,后三个方法最终都是调用ReadSlice来实现的。所以,我们先来看看ReadSlice方法。 55 | 56 | **ReadSlice方法签名**如下: 57 | 58 | func (b *Reader) ReadSlice(delim byte) (line []byte, err error) 59 | 60 | ReadSlice从输入中读取,直到遇到第一个界定符(delim)为止,返回一个指向缓存中字节的slice,在下次调用读操作(read)时,这些字节会无效。举例说明: 61 | 62 | reader := bufio.NewReader(strings.NewReader("http://studygolang.com. \nIt is the home of gophers")) 63 | line, _ := reader.ReadSlice('\n') 64 | fmt.Printf("the line:%s\n", line) 65 | // 这里可以换上任意的 bufio 的 Read/Write 操作 66 | n, _ := reader.ReadSlice('\n') 67 | fmt.Printf("the line:%s\n", line) 68 | fmt.Println(string(n)) 69 | 70 | 输出: 71 | 72 | the line:http://studygolang.com. 73 | 74 | the line:It is the home of gophers 75 | It is the home of gophers 76 | 77 | 从结果可以看出,第一次ReadSlice的结果(line),在第二次调用读操作后,内容发生了变化。也就是说,ReadSlice返回的[]byte是指向Reader中的buffer,而不是copy一份返回。正因为ReadSlice返回的数据会被下次的I/O操作重写,因此许多的客户端会选择使用ReadBytes或者ReadString来代替。读者可以将上面代码中的ReadSlice改为ReadBytes或ReadString,看看结果有什么不同。 78 | 79 | 注意,这里的界定符可以是任意的字符,可以将上面代码中的'\n'改为'm'试试。同时,返回的结果是包含界定符本身的,上例中,输出结果有一空行就是'\n'本身。 80 | 81 | 如果ReadSlice在找到界定符之前遇到了error,它就会返回缓存中所有的数据和错误本身(经常是 io.EOF)。如果在找到界定符之前缓存已经满了,ReadSlice会返回bufio.ErrBufferFull错误。当且仅当返回的结果(line)没有以界定符结束的时候,ReadSlice返回err != nil,也就是说,如果ReadSlice返回的结果line不是以界定符delim结尾,那么返回的err也一定不等于nil(可能是bufio.ErrBufferFull或io.EOF)。例子代码: 82 | 83 | reader := bufio.NewReaderSize(strings.NewReader("http://studygolang.com"),16) 84 | line, err := reader.ReadSlice('\n') 85 | fmt.Printf("line:%s\terror:%s\n", line, err) 86 | line, err = reader.ReadSlice('\n') 87 | fmt.Printf("line:%s\terror:%s\n", line, err) 88 | 89 | 输出: 90 | 91 | line:http://studygola error:bufio: buffer full 92 | line:ng.com error:EOF 93 | 94 | **ReadBytes方法签名**如下: 95 | 96 | func (b *Reader) ReadBytes(delim byte) (line []byte, err error) 97 | 98 | 该方法的参数和返回值类型与ReadSlice都一样。 ReadBytes 从输入中读取直到遇到界定符(delim)为止,返回的slice包含了从当前到界定符的内容(包括界定符)。如果ReadBytes在遇到界定符之前就捕获到一个错误,它会返回遇到错误之前已经读取的数据,和这个捕获到的错误(经常是 io.EOF)。跟ReadSlice一样,如果ReadBytes返回的结果line不是以界定符delim结尾,那么返回的err也一定不等于nil(可能是bufio.ErrBufferFull或io.EOF)。 99 | 100 | 从这个说明可以看出,ReadBytes和ReadSlice功能和用法都很像,那他们有什么不同呢? 101 | 102 | 在讲解ReadSlice时说到,它返回的[]byte是指向Reader中的buffer,而不是copy一份返回,也正因为如此,通常我们会使用ReadBytes或ReadString。很显然,ReadBytes返回的[]byte不会是指向Reader中的buffer,通过查看源码可以证实这一点。 103 | 104 | 还是上面的例子,我们将ReadSlice改为ReadBytes: 105 | 106 | reader := bufio.NewReader(strings.NewReader("http://studygolang.com. \nIt is the home of gophers")) 107 | line, _ := reader.ReadBytes('\n') 108 | fmt.Printf("the line:%s\n", line) 109 | // 这里可以换上任意的 bufio 的 Read/Write 操作 110 | n, _ := reader.ReadBytes('\n') 111 | fmt.Printf("the line:%s\n", line) 112 | fmt.Println(string(n)) 113 | 114 | 输出: 115 | 116 | the line:http://studygolang.com. 117 | 118 | the line:http://studygolang.com. 119 | 120 | It is the home of gophers 121 | 122 | **ReadString方法** 123 | 124 | 看一下该方法的源码: 125 | 126 | func (b *Reader) ReadString(delim byte) (line string, err error) { 127 | bytes, err := b.ReadBytes(delim) 128 | return string(bytes), err 129 | } 130 | 131 | 它调用了ReadBytes方法,并将结果的[]byte转为string类型。 132 | 133 | **ReadLine方法签名**如下 134 | 135 | func (b *Reader) ReadLine() (line []byte, isPrefix bool, err error) 136 | 137 | ReadLine是一个底层的原始行读取命令。许多调用者或许会使用ReadBytes('\n')或者ReadString('\n')来代替这个方法。 138 | 139 | ReadLine尝试返回单独的行,不包括行尾的换行符。如果一行大于缓存,isPrefix会被设置为true,同时返回该行的开始部分(等于缓存大小的部分)。该行剩余的部分就会在下次调用的时候返回。当下次调用返回该行剩余部分时,isPrefix将会是false。跟ReadSlice一样,返回的line只是buffer的引用,在下次执行IO操作时,line会无效。可以将ReadSlice中的例子该为ReadLine试试。 140 | 141 | 注意,返回值中,要么line不是nil,要么err非nil,两者不会同时非nil。 142 | 143 | ReadLine返回的文本不会包含行结尾("\r\n"或者"\n")。如果输入中没有行尾标识符,不会返回任何指示或者错误。 144 | 145 | 从上面的讲解中,我们知道,读取一行,通常会选择ReadBytes或ReadString。不过,正常人的思维,应该用ReadLine,只是不明白为啥ReadLine的实现不是通过ReadBytes,然后清除掉行尾的\\n(或\r\n),它现在的实现,用不好会出现意想不到的问题,比如丢数据。个人建议可以这么实现读取一行: 146 | 147 | line, err := reader.ReadBytes('\n') 148 | line = bytes.TrimRight(line, "\r\n") 149 | 150 | 这样既读取了一行,也去掉了行尾结束符(当然,如果你希望不留行尾结束符,则直接用ReadBytes即可)。 151 | 152 | ### 1.4.1.3 Peek 方法 ### 153 | 154 | 从方法的名称可以猜到,该方法只是“窥探”一下Reader中没有读取的n个字节。好比栈数据结构中的取栈顶元素,但不出栈。 155 | 156 | 方法的签名如下: 157 | 158 | func (b *Reader) Peek(n int) ([]byte, error) 159 | 160 | 同上面介绍的ReadSlice一样,返回的[]byte只是buffer中的引用,在下次IO操作后会无效,可见该方法(以及ReadSlice这样的,返回buffer引用的方法)对多goroutine是不安全的,也就是在多并发环境下,不能依赖其结果。 161 | 162 | 我们通过例子来证明一下: 163 | 164 | package main 165 | 166 | import ( 167 | "bufio" 168 | "fmt" 169 | "strings" 170 | "time" 171 | ) 172 | 173 | func main() { 174 | reader := bufio.NewReaderSize(strings.NewReader("http://studygolang.com.\t It is the home of gophers"), 14) 175 | go Peek(reader) 176 | go reader.ReadBytes('\t') 177 | time.Sleep(1e8) 178 | } 179 | 180 | func Peek(reader *bufio.Reader) { 181 | line, _ := reader.Peek(14) 182 | fmt.Printf("%s\n", line) 183 | // time.Sleep(1) 184 | fmt.Printf("%s\n", line) 185 | } 186 | 187 | 输出: 188 | 189 | http://studygo 190 | http://studygo 191 | 192 | 输出结果和预期的一致。然而,这是由于目前的goroutine调度方式导致的结果。如果我们将例子中注释掉的time.Sleep(1)取消注释(这样调度其他goroutine执行),再次运行,得到的结果为: 193 | 194 | http://studygo 195 | ng.com. It is 196 | 197 | 另外,Reader的Peek方法如果返回的[]byte长度小于n,这时返回的err为非nil,用于解释为啥会小于n。如果n大于reader的buffer长度,err会是ErrBufferFull。 198 | 199 | ### 1.4.1.4 其他方法 ### 200 | 201 | Reader的其他方法都是实现了io包中的接口,它们的使用方法在io包中都有介绍,在此不赘述。 202 | 203 | 这些方法包括: 204 | 205 | func (b *Reader) Read(p []byte) (n int, err error) 206 | func (b *Reader) ReadByte() (c byte, err error) 207 | func (b *Reader) ReadRune() (r rune, size int, err error) 208 | func (b *Reader) UnreadByte() error 209 | func (b *Reader) UnreadRune() error 210 | func (b *Reader) WriteTo(w io.Writer) (n int64, err error) 211 | 212 | 你应该知道它们都是哪个接口的方法吧。 213 | 214 | ## 1.4.2 Scanner 类型和方法 ## 215 | 216 | 对于简单的读取一行,在Reader类型中,感觉没有让人特别满意的方法。于是,Go1.1增加了一个类型:Scanner。官方关于**Go1.1**增加该类型的说明如下: 217 | 218 | > 在 bufio 包中有多种方式获取文本输入,ReadBytes、ReadString 和独特的 ReadLine,对于简单的目的这些都有些过于复杂了。在 Go 1.1 中,添加了一个新类型,Scanner,以便更容易的处理如按行读取输入序列或空格分隔的词等,这类简单的任务。它终结了如输入一个很长的有问题的行这样的输入错误,并且提供了简单的默认行为:基于行的输入,每行都剔除分隔标识。这里的代码展示一次输入一行: 219 | 220 | scanner := bufio.NewScanner(os.Stdin) 221 | for scanner.Scan() { 222 | fmt.Println(scanner.Text()) // Println will add back the final '\n' 223 | } 224 | if err := scanner.Err(); err != nil { 225 | fmt.Fprintln(os.Stderr, "reading standard input:", err) 226 | } 227 | 228 | > 输入的行为可以通过一个函数控制,来控制输入的每个部分(参阅 SplitFunc 的文档),但是对于复杂的问题或持续传递错误的,可能还是需要原有接口。 229 | 230 | Scanner 类型和 Reader 类型一样,没有任何导出的字段,同时它也包装了一个 io.Reader 对象,但它没有实现 io.Reader 接口。 231 | 232 | Scanner 的结构定义如下: 233 | 234 | type Scanner struct { 235 | r io.Reader // The reader provided by the client. 236 | split SplitFunc // The function to split the tokens. 237 | maxTokenSize int // Maximum size of a token; modified by tests. 238 | token []byte // Last token returned by split. 239 | buf []byte // Buffer used as argument to split. 240 | start int // First non-processed byte in buf. 241 | end int // End of data in buf. 242 | err error // Sticky error. 243 | } 244 | 245 | 这里 split、maxTokenSize 和 token 需要讲解一下。 246 | 247 | 然而,在讲解之前,需要先讲解 split 字段的类型 SplitFunc。 248 | 249 | ### 1.4.2.1 SplitFunc 类型和实例 ### 250 | 251 | **SplitFunc 类型定义**如下: 252 | 253 | type SplitFunc func(data []byte, atEOF bool) (advance int, token []byte, err error) 254 | 255 | SplitFunc 定义了 用于对输入进行分词的 split 函数的签名。参数 data 是还未处理的数据,atEOF 标识 Reader 是否还有更多数据(是否到了EOF)。返回值 advance 表示从输入中读取的字节数,token 表示下一个结果数据,err 则代表可能的错误。 256 | 257 | 举例说明一下这里的 token 代表的意思: 258 | 259 | 有数据 "studygolang\tpolaris\tgolangchina",通过"\t"进行分词,那么会得到三个token,它们的内容分别是:studygolang、polaris 和 golangchina。而 SplitFunc 的功能是:进行分词,并返回未处理的数据中第一个 token。对于这个数据,就是返回 studygolang。 260 | 261 | 如果 data 中没有一个完整的 token,例如,在扫描行(scanning lines)时没有换行符,SplitFunc 会返回(0,nil,nil)通知 Scanner 读取更多数据到 slice 中,然后在这个更大的 slice 中同样的读取点处,从输入中重试读取。如下面要讲解的 split 函数的源码中有这样的代码: 262 | 263 | // Request more data. 264 | return 0, nil, nil 265 | 266 | 如果 err 非nil,扫描停止,同时该错误会返回。 267 | 268 | 如果参数 data 为空的 slice,除非 atEOF 为 true,否则该函数永远不会被调用。如果 atEOF 为 true,这时 data 可以非空,这时的数据是没有处理的。 269 | 270 | **bufio 包定义的 split 函数,即 SplitFunc 的实例** 271 | 272 | 在 bufio 包中预定义了一些 split 函数,也就是说,在 Scanner 结构中的 split 字段,可以通过这些预定义的 split 赋值,同时 Scanner 类型的 Split 方法也可以接收这些预定义函数作为参数。所以,我们可以说,这些预定义 split 函数都是 SplitFunc 类型的实例。这些函数包括:ScanBytes、ScanRunes、ScanWords 和 ScanLines。(由于都是 SplitFunc 的实例,自然这些函数的签名都和 SplitFunc 一样) 273 | 274 | **ScanBytes** 返回单个字节作为一个 token。 275 | 276 | **ScanRunes** 返回单个 UTF-8 编码的 rune 作为一个 token。返回的 rune 序列(token)和 range string类型 返回的序列是等价的,也就是说,对于无效的 UTF-8 编码会解释为 U+FFFD = "\xef\xbf\xbd"。 277 | 278 | **ScanWords** 返回通过“空格”分词的单词。如:study golang,调用会返回study。注意,这里的“空格”是 `unicode.IsSpace()`,即包括:'\t', '\n', '\v', '\f', '\r', ' ', U+0085 (NEL), U+00A0 (NBSP)。 279 | 280 | **ScanLines** 返回一行文本,不包括行尾的换行符。这里的换行包括了Windows下的"\r\n"和Unix下的"\n"。 281 | 282 | 一般地,我们不会单独使用这些函数,而是提供给 Scanner 实例使用。现在我们回到 Scanner 的 split、maxTokenSize 和 token 字段上来。 283 | 284 | **split 字段**(SplitFunc 类型实例),很显然,代表了当前 Scanner 使用的分词策略,可以使用上面介绍的预定义 SplitFunc 实例赋值,也可以自定义 SplitFunc 实例。(当然,要给 split 字段赋值,必须调用 Scanner 的 Split 方法) 285 | 286 | **maxTokenSize 字段** 表示通过 split 分词后的一个 token 允许的最大长度。在该包中定义了一个常量 MaxScanTokenSize = 64 * 1024,这是允许的最大 token 长度(64k)。 287 | 288 | **token 字段** 上文已经解释了这个是什么意思。 289 | 290 | ### 1.4.2.2 Scanner 的实例化 ### 291 | 292 | Scanner 没有导出任何字段,而它需要有外部的 io.Reader 对象,因此,我们不能直接实例化 Scanner 对象,必须通过 bufio 包提供的实例化函数来实例化。实例化函数签名以及内部实现: 293 | 294 | func NewScanner(r io.Reader) *Scanner { 295 | return &Scanner{ 296 | r: r, 297 | split: ScanLines, 298 | maxTokenSize: MaxScanTokenSize, 299 | buf: make([]byte, 4096), // Plausible starting size; needn't be large. 300 | } 301 | } 302 | 303 | 可见,返回的 Scanner 实例默认的 split 函数是 ScanLines。 304 | 305 | ### 1.4.2.2 Scanner 的方法 ### 306 | 307 | **Split 方法** 前面我们提到过可以通过 Split 方法为 Scanner 实例设置分词行为。由于 Scanner 实例的默认 split 总是 ScanLines,如果我们想要用其他的 split,可以通过 Split 方法做到。 308 | 309 | 比如,我们想要统计一段英文有多少个单词(不排除重复),我们可以这么做: 310 | 311 | const input = "This is The Golang Standard Library.\nWelcome you!" 312 | scanner := bufio.NewScanner(strings.NewReader(input)) 313 | scanner.Split(bufio.ScanWords) 314 | count := 0 315 | for scanner.Scan() { 316 | count++ 317 | } 318 | if err := scanner.Err(); err != nil { 319 | fmt.Fprintln(os.Stderr, "reading input:", err) 320 | } 321 | fmt.Println(count) 322 | 323 | 输出: 324 | 325 | 8 326 | 327 | 我们实例化 Scanner 后,通过调用 scanner.Split(bufio.ScanWords) 来更改 split 函数。注意,我们应该在调用 Scan 方法之前调用 Split 方法。 328 | 329 | **Scan 方法** 该方法好比 iterator 中的 Next 方法,它用于将 Scanner 获取下一个 token,以便 Bytes 和 Text 方法可用。当扫描停止时,它返回false,这时候,要么是到了输入的末尾要么是遇到了一个错误。注意,当 Scan 返回 false 时,通过 Err 方法可以获取第一个遇到的错误(但如果错误是 io.EOF,Err 方法会返回 nil)。 330 | 331 | **Bytes 和 Text 方法** 这两个方法的行为一致,都是返回最近的 token,无非 Bytes 返回的是 []byte,Text 返回的是 string。该方法应该在 Scan 调用后调用,而且,下次调用 Scan 会覆盖这次的 token。比如: 332 | 333 | scanner := bufio.NewScanner(strings.NewReader("http://studygolang.com. \nIt is the home of gophers")) 334 | if scanner.Scan() { 335 | scanner.Scan() 336 | fmt.Printf("%s", scanner.Text()) 337 | } 338 | 339 | 返回的是:It is the home of gophers 而不是 http://studygolang.com. 340 | 341 | **Err 方法** 前面已经提到,通过 Err 方法可以获取第一个遇到的错误(但如果错误是 io.EOF,Err 方法会返回 nil)。 342 | 343 | 下面,我们通过一个完整的示例来演示 Scanner 类型的使用。 344 | 345 | ### 1.4.2.3 Scanner 使用示例 ### 346 | 347 | 我们经常会有这样的需求:读取文件中的数据,一次读取一行。在学习了 Reader 类型,我们可以使用它的 ReadBytes 或 ReadString来实现,甚至使用 ReadLine 来实现。然而,在 Go1.1 中,我们可以使用 Scanner 来做这件事,而且更简单好用。 348 | 349 | file, err := os.Create("scanner.txt") 350 | if err != nil { 351 | panic(err) 352 | } 353 | defer file.Close() 354 | file.WriteString("http://studygolang.com.\nIt is the home of gophers.\nIf you are studying golang, welcome you!") 355 | // 将文件 offset 设置到文件开头 356 | file.Seek(0, os.SEEK_SET) 357 | scanner := bufio.NewScanner(file) 358 | for scanner.Scan() { 359 | fmt.Println(scanner.Text()) 360 | } 361 | 362 | 输出结果: 363 | 364 | http://studygolang.com. 365 | It is the home of gophers. 366 | If you are studying golang, welcome you! 367 | 368 | ## 1.4.3 Writer 类型和方法 ## 369 | 370 | bufio.Writer 结构包装了一个 io.Writer 对象,提供缓存功能,同时实现了 io.Writer 接口。 371 | 372 | Writer 结构没有任何导出的字段,结构定义如下: 373 | 374 | type Writer struct { 375 | err error // 写过程中遇到的错误 376 | buf []byte // 缓存 377 | n int // 当前缓存中的字节数 378 | wr io.Writer // 底层的 io.Writer 对象 379 | } 380 | 381 | 相比 bufio.Reader, bufio.Writer 结构定义简单很多。 382 | 383 | 注意:如果在写数据到 Writer 的时候出现了一个错误,不会再允许有数据被写进来了,并且所有随后的写操作都会返回该错误。 384 | 385 | ### 1.4.3.1 实例化 ### 386 | 387 | 和 Reader 类型一样,bufio 包提供了两个实例化 bufio.Writer 对象的函数:NewWriter 和 NewWriterSize。其中,NewWriter 函数是调用 NewWriterSize 函数实现的: 388 | 389 | func NewWriter(wr io.Writer) *Writer { 390 | // 默认缓存大小:defaultBufSize=4096 391 | return NewWriterSize(wr, defaultBufSize) 392 | } 393 | 394 | 我们看一下 NewWriterSize 的源码: 395 | 396 | func NewWriterSize(wr io.Writer, size int) *Writer { 397 | // 已经是 bufio.Writer 类型,且缓存大小不小于 size,则直接返回 398 | b, ok := wr.(*Writer) 399 | if ok && len(b.buf) >= size { 400 | return b 401 | } 402 | if size <= 0 { 403 | size = defaultBufSize 404 | } 405 | return &Writer{ 406 | buf: make([]byte, size), 407 | wr: w, 408 | } 409 | } 410 | 411 | ### 1.4.3.2 Available 和 Buffered 方法 ### 412 | 413 | Available 方法获取缓存中还未使用的字节数(缓存大小 - 字段 n 的值);Buffered 方法获取写入当前缓存中的字节数(字段 n 的值) 414 | 415 | ### 1.4.3.3 Flush 方法 ### 416 | 417 | 该方法将缓存中的所有数据写入底层的 io.Writer 对象中。使用 bufio.Writer 时,在所有的 Write 操作完成之后,应该调用 Flush 方法使得缓存都写入 io.Writer 对象中。 418 | 419 | ### 1.4.3.4 其他方法 ### 420 | 421 | Writer 类型其他方法是一些实际的写方法: 422 | 423 | // 实现了 io.ReaderFrom 接口 424 | func (b *Writer) ReadFrom(r io.Reader) (n int64, err error) 425 | 426 | // 实现了 io.Writer 接口 427 | func (b *Writer) Write(p []byte) (nn int, err error) 428 | 429 | // 实现了 io.ByteWriter 接口 430 | func (b *Writer) WriteByte(c byte) error 431 | 432 | // io 中没有该方法的接口,它用于写入单个 Unicode 码点,返回写入的字节数(码点占用的字节),内部实现会根据当前 rune 的范围调用 WriteByte 或 WriteString 433 | func (b *Writer) WriteRune(r rune) (size int, err error) 434 | 435 | // 写入字符串,如果返回写入的字节数比 len(s) 小,返回的error会解释原因 436 | func (b *Writer) WriteString(s string) (int, error) 437 | 438 | 这些写方法在缓存满了时会调用 Flush 方法。另外,这些写方法源码开始处,有这样的代码: 439 | 440 | if b.err != nil { 441 | return b.err 442 | } 443 | 444 | 也就是说,只要写的过程中遇到了错误,再次调用写操作会直接返回该错误。 445 | 446 | ## 1.4.4 ReadWriter 类型和实例化 ## 447 | 448 | ReadWriter 结构存储了 bufio.Reader 和 bufio.Writer 类型的指针(内嵌),它实现了 io.ReadWriter 结构。 449 | 450 | type ReadWriter struct { 451 | *Reader 452 | *Writer 453 | } 454 | 455 | ReadWriter 的实例化可以跟普通结构类型一样,也可以通过调用 bufio.NewReadWriter 函数来实现:只是简单的实例化 ReadWriter 456 | 457 | func NewReadWriter(r *Reader, w *Writer) *ReadWriter { 458 | return &ReadWriter{r, w} 459 | } 460 | 461 | # 导航 # 462 | 463 | - [目录](/preface.md) 464 | - 上一节:[fmt — 格式化IO](01.3.md) 465 | - 下一节:[I/O 总结](01.5.md) 466 | -------------------------------------------------------------------------------- /chapter02/02.0.md: -------------------------------------------------------------------------------- 1 | # 第二章 文本 # 2 | 3 | 几乎任何程序都离不开文本(字符串)。Go 中 string 是内置类型,同时它与普通的 slice 类型有着相似的性质,例如,可以进行切片(slice)操作,这使得 Go 中少了一些处理 string 类型的函数,比如没有 substring 这样的函数,然而却能够很方便的进行这样的操作。除此之外,Go 标准库中有几个包专门用于处理文本。 4 | 5 | *strings* 包提供了很多操作字符串的简单函数,通常一般的字符串操作需求都可以在这个包中找到。 6 | 7 | *strconv* 包提供了基本数据类型和字符串之间的转换。在 Go 中,没有隐式类型转换,一般的类型转换可以这么做:int32(i),将 i (比如为 int 类型)转换为 int32,然而,字符串类型和 int、float、bool 等类型之间的转换却没有这么简单。 8 | 9 | 进行复杂的文本处理必然离不开正则表达式。*regexp* 包提供了正则表达式功能,它的语法基于 [RE2](http://code.google.com/p/re2/wiki/Syntax) ,*regexp/syntax* 子包进行正则表达式解析。 10 | 11 | Go 代码使用 UTF-8 编码(且不能带 BOM),同时标识符支持 Unicode 字符。在标准库 *unicode* 包及其子包 utf8、utf16中,提供了对 Unicode 相关编码、解码的支持,同时提供了测试 Unicode 码点(Unicode code points)属性的功能。 12 | 13 | 在开发过程中,可能涉及到字符集的转换,作为补充,本章最后会讲解一个第三方库:mahonia — 纯 Go 语言实现的字符集转换库,以方便需要进行字符集转换的读者。 14 | 15 | # 导航 # 16 | 17 | - [第一章](/chapter01/01.0.md) 18 | - 下一节:[strings — 字符串操作](02.1.md) 19 | -------------------------------------------------------------------------------- /chapter02/02.1.md: -------------------------------------------------------------------------------- 1 | # 2.1 strings — 字符串操作 # 2 | 3 | 字符串常见操作有: 4 | 5 | - 字符串长度; 6 | - 求子串; 7 | - 是否存在某个字符或子串; 8 | - 子串出现的次数(字符串匹配); 9 | - 字符串分割(切分)为[]string; 10 | - 字符串是否有某个前缀或后缀; 11 | - 字符或子串在字符串中首次出现的位置或最后一次出现的位置; 12 | - 通过某个字符串将[]string连接起来; 13 | - 字符串重复几次; 14 | - 字符串中子串替换; 15 | - 大小写转换; 16 | - Trim操作; 17 | - ... 18 | 19 | 前面已经说过,由于string类型可以看成是一种特殊的slice类型,因此获取长度可以用内置的函数len;同时支持 切片 操作,因此,子串获取很容易。 20 | 21 | 其他的字符串常见操作就是我们这小节要介绍的,由于这些操作函数的使用比较简单,只会对某些函数举例说明;但会深入这些函数的内部实现,更好的掌握它们。 22 | 23 | 说明:这里说的字符,值得是 rune 类型,即一个 UTF-8 字符(Unicode 代码点)。 24 | 25 | ## 2.1.1 是否存在某个字符或子串 ## 26 | 27 | 有三个函数做这件事: 28 | 29 | // 子串substr在s中,返回true 30 | func Contains(s, substr string) bool 31 | // chars中任何一个Unicode代码点在s中,返回true 32 | func ContainsAny(s, chars string) bool 33 | // Unicode代码点r在s中,返回true 34 | func ContainsRune(s string, r rune) bool 35 | 36 | 这里对 ContainsAny 函数进行一下说明,看如下例子: 37 | 38 | fmt.Println(strings.ContainsAny("team", "i")) 39 | fmt.Println(strings.ContainsAny("failure", "u & i")) 40 | fmt.Println(strings.ContainsAny("in failure", "s g")) 41 | fmt.Println(strings.ContainsAny("foo", "")) 42 | fmt.Println(strings.ContainsAny("", "")) 43 | 44 | 输出: 45 | 46 | false 47 | true 48 | true 49 | false 50 | false 51 | 52 | 也就是说,第二个参数 chars 中任意一个字符(Unicode Code Point)如果在第一个参数 s 中存在,则返回true。 53 | 54 | 查看这三个函数的源码,发现它们只是调用了相应的Index函数(子串出现的位置),然后和 0 作比较返回true或fale。如,Contains: 55 | 56 | func Contains(s, substr string) bool { 57 | return Index(s, substr) >= 0 58 | } 59 | 60 | 关于Index相关函数的实现,我们后面介绍。 61 | 62 | ## 2.1.2 子串出现次数(字符串匹配) ## 63 | 64 | 在数据结构与算法中,可能会讲解以下字符串匹配算法: 65 | 66 | - 朴素匹配算法 67 | - KMP算法 68 | - Rabin-Karp算法 69 | - Boyer-Moore算法 70 | 71 | 还有其他的算法,这里不一一列举,感兴趣的可以网上搜一下。 72 | 73 | 在Go中,查找子串出现次数即字符串模式匹配,实现的是Rabin-Karp算法。Count 函数的签名如下: 74 | 75 | func Count(s, sep string) int 76 | 77 | 在 Count 的实现中,处理了几种特殊情况,属于字符匹配预处理的一部分。这里要特别说明一下的是当 sep 为空时,Count 的返回值是:utf8.RuneCountInString(s) + 1 78 | 79 | fmt.Println(strings.Count("five", "")) // before & after each rune 80 | 81 | 输出: 82 | 83 | 5 84 | 85 | 关于Rabin-Karp算法的实现,有兴趣的可以看看 Count 的源码。 86 | 87 | 另外,Count 是计算子串在字符串中出现的无重叠的次数,比如: 88 | 89 | fmt.Println(strings.Count("fivevev", "vev")) 90 | 91 | 输出: 92 | 93 | 1 94 | 95 | ## 2.1.3 字符串分割为[]string ## 96 | 97 | 这个需求很常见,倒不一定是为了得到[]string。 98 | 99 | 该包提供了六个三组分割函数:Fields 和 FieldsFunc、Split 和 SplitAfter、SplitN 和 SplitAfterN。 100 | 101 | ### 2.1.3.1 Fields 和 FieldsFunc ### 102 | 103 | 这两个函数的签名如下: 104 | 105 | func Fields(s string) []string 106 | func FieldsFunc(s string, f func(rune) bool) []string 107 | 108 | Fields 用一个或多个连续的空格分隔字符串 s,返回子字符串的数组(slice)。如果字符串 s 只包含空格,则返回空列表([]string的长度为0)。其中,空格的定义是 unicode.IsSpace,之前已经介绍过。 109 | 110 | 由于是用空格分隔,因此结果中不会含有空格或空子字符串,例如: 111 | 112 | fmt.Printf("Fields are: %q", strings.Fields(" foo bar baz ")) 113 | 114 | 输出: 115 | 116 | Fields are: ["foo" "bar" "baz"] 117 | 118 | FieldsFunc 用这样的Unicode代码点 c 进行分隔:满足 f(c) 返回 true。该函数返回[]string。如果字符串 s 中所有的代码点(unicode code points)都满足f(c)或者 s 是空,则 FieldsFunc 返回空slice。 119 | 120 | 也就是说,我们可以通过实现一个回调函数来指定分隔字符串 s 的字符。比如上面的例子,我们通过 FieldsFunc 来实现: 121 | 122 | fmt.Println(strings.FieldsFunc(" foo bar baz ", unicode.IsSpace)) 123 | 124 | 实际上,Fields 函数就是调用 FieldsFunc 实现的: 125 | 126 | func Fields(s string) []string { 127 | return FieldsFunc(s, unicode.IsSpace) 128 | } 129 | 130 | 对于 FieldsFunc 源码留给读者自己阅读。 131 | 132 | ### 2.1.3.2 Split 和 SplitAfter、 SplitN 和 SplitAfterN ### 133 | 134 | 之所以将这四个函数放在一起讲,是因为它们都是通过一个同一个内部函数来实现的。它们的函数签名及其实现: 135 | 136 | func Split(s, sep string) []string { return genSplit(s, sep, 0, -1) } 137 | func SplitAfter(s, sep string) []string { return genSplit(s, sep, len(sep), -1) } 138 | func SplitN(s, sep string, n int) []string { return genSplit(s, sep, 0, n) } 139 | func SplitAfterN(s, sep string, n int) []string { return genSplit(s, sep, len(sep), n) } 140 | 141 | 它们都调用了 genSplit 函数。 142 | 143 | 这四个函数都是通过 sep 进行分割,返回[]string。如果 sep 为空,相当于分成一个个的 UTF-8 字符,如 `Split("abc","")`,得到的是[a b c]。 144 | 145 | Split(s, sep) 和 SplitN(s, sep, -1) 等价;SplitAfter(s, sep) 和 SplitAfterN(s, sep, -1) 等价。 146 | 147 | 那么,Split 和 SplitAfter 有啥区别呢?通过这两句代码的结果就知道它们的区别了: 148 | 149 | fmt.Printf("%q\n", strings.Split("foo,bar,baz", ",")) 150 | fmt.Printf("%q\n", strings.SplitAfter("foo,bar,baz", ",")) 151 | 152 | 输出: 153 | 154 | ["foo" "bar" "baz"] 155 | ["foo," "bar," "baz"] 156 | 157 | 也就是说,Split 会将 s 中的 sep 去掉,而 SplitAfter 会保留 sep。 158 | 159 | 带 N 的方法可以通过最后一个参数 n 控制返回的结果中的 slice 中的元素个数,当 n < 0 时,返回所有的子字符串;当 n == 0 时,返回的结果是 nil;当 n > 0 时,表示返回的 slice 中最多只有 n 个元素,其中,最后一个元素不会分割,比如: 160 | 161 | fmt.Printf("%q\n", strings.SplitN("foo,bar,baz", ",", 2)) 162 | 163 | 输出: 164 | 165 | ["foo" "bar,baz"] 166 | 167 | 另外看一下官方文档提供的例子,注意一下输出结果: 168 | 169 | fmt.Printf("%q\n", strings.Split("a,b,c", ",")) 170 | fmt.Printf("%q\n", strings.Split("a man a plan a canal panama", "a ")) 171 | fmt.Printf("%q\n", strings.Split(" xyz ", "")) 172 | fmt.Printf("%q\n", strings.Split("", "Bernardo O'Higgins")) 173 | 174 | 输出: 175 | 176 | ["a" "b" "c"] 177 | ["" "man " "plan " "canal panama"] 178 | [" " "x" "y" "z" " "] 179 | [""] 180 | 181 | ## 2.1.4 字符串是否有某个前缀或后缀 ## 182 | 183 | 这两个函数比较简单,源码如下: 184 | 185 | // s 中是否以 prefix 开始 186 | func HasPrefix(s, prefix string) bool { 187 | return len(s) >= len(prefix) && s[0:len(prefix)] == prefix 188 | } 189 | // s 中是否以 suffix 结尾 190 | func HasSuffix(s, suffix string) bool { 191 | return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix 192 | } 193 | 194 | ## 2.1.5 字符或子串在字符串中出现的位置 ## 195 | 196 | 有一序列函数与该功能有关: 197 | 198 | // 在 s 中查找 sep 的第一次出现,返回第一次出现的索引 199 | func Index(s, sep string) int 200 | // chars中任何一个Unicode代码点在s中首次出现的位置 201 | func IndexAny(s, chars string) int 202 | // 查找字符 c 在 s 中第一次出现的位置,其中 c 满足 f(c) 返回 true 203 | func IndexFunc(s string, f func(rune) bool) int 204 | // Unicode 代码点 r 在 s 中第一次出现的位置 205 | func IndexRune(s string, r rune) int 206 | 207 | // 有三个对应的查找最后一次出现的位置 208 | func LastIndex(s, sep string) int 209 | func LastIndexAny(s, chars string) int 210 | func LastIndexFunc(s string, f func(rune) bool) int 211 | 212 | 在 2.1.1 小节提到过,Contain 相关的函数内部调用的是响应的 Index 函数。 213 | 214 | 这一序列函数,只举 IndexFunc 的例子: 215 | 216 | fmt.Printf("%d\n", strings.IndexFunc("studygolang", func(c rune) bool { 217 | if c > 'u' { 218 | return true 219 | } 220 | return false 221 | })) 222 | 223 | 输出: 224 | 225 | 4 226 | 227 | 因为 y 的 Unicode 代码点大于 u 的代码点。 228 | 229 | ## 2.1.6 字符串 JOIN 操作 ## 230 | 231 | 将字符串数组(或slice)连接起来可以通过 Join 实现,函数签名如下: 232 | 233 | func Join(a []string, sep string) string 234 | 235 | 假如没有这个库函数,我们自己实现一个,我们会这么实现: 236 | 237 | func Join(str []string, sep string) string { 238 | // 特殊情况应该做处理 239 | if len(str) == 0 { 240 | return "" 241 | } 242 | if len(str) == 1 { 243 | return str[0] 244 | } 245 | buffer := bytes.NewBufferString(str[0]) 246 | for _, s := range str[1:] { 247 | buffer.WriteString(sep) 248 | buffer.WriteString(s) 249 | } 250 | return buffer.String() 251 | } 252 | 253 | 这里,我们使用了 bytes 包的 Buffer 类型,避免大量的字符串连接操作(因为 Go 中字符串是不可变的)。我们再看一下标准库的实现: 254 | 255 | func Join(a []string, sep string) string { 256 | if len(a) == 0 { 257 | return "" 258 | } 259 | if len(a) == 1 { 260 | return a[0] 261 | } 262 | n := len(sep) * (len(a) - 1) 263 | for i := 0; i < len(a); i++ { 264 | n += len(a[i]) 265 | } 266 | 267 | b := make([]byte, n) 268 | bp := copy(b, a[0]) 269 | for _, s := range a[1:] { 270 | bp += copy(b[bp:], sep) 271 | bp += copy(b[bp:], s) 272 | } 273 | return string(b) 274 | } 275 | 276 | 标准库的实现没有用 bytes 包,当然也不会简单的通过 + 号连接字符串。Go 中是不允许循环依赖的,标准库中很多时候会出现代码拷贝,而不是引入某个包。这里 Join 的实现方式挺好,我个人猜测,不直接使用 bytes 包,也是不想依赖 bytes 包(其实 bytes 中的实现也是 copy 方式)。 277 | 278 | 简单使用示例: 279 | 280 | fmt.Println(Join([]string{"name=xxx", "age=xx"}, "&")) 281 | // 输出 name=xxx&age=xx 282 | 283 | ## 2.1.7 字符串重复几次 ## 284 | 285 | 函数签名如下: 286 | 287 | func Repeat(s string, count int) string 288 | 289 | 这个函数使用很简单: 290 | 291 | // 输出 banana 292 | fmt.Println("ba" + strings.Repeat("na", 2)) 293 | 294 | ## 2.1.8 字符串子串替换 ## 295 | 296 | 进行字符串替换时,考虑到性能问题,能不用正则尽量别用,应该用这里的函数。 297 | 298 | 字符串替换的函数签名如下: 299 | 300 | // 用 new 替换 s 中的 old,一共替换 n 个。 301 | // 如果 n < 0,则不限制替换次数,即全部替换 302 | func Replace(s, old, new string, n int) string 303 | 304 | 使用示例: 305 | 306 | fmt.Println(strings.Replace("oink oink oink", "k", "ky", 2)) 307 | fmt.Println(strings.Replace("oink oink oink", "oink", "moo", -1)) 308 | 309 | 输出: 310 | 311 | oinky oinky oink 312 | moo moo moo 313 | 314 | 如果我们希望一次替换多个,比如我们希望替换 `This is HTML` 中的 `<` 和 `>` 为 `<` 和 `>`,可以调用上面的函数两次。但标准库提供了另外的方法进行这种替换。 315 | 316 | ## 2.1.9 Replacer 类型 ## 317 | 318 | 这是一个结构,没有导出任何字段,实例化通过 `func NewReplacer(oldnew ...string) *Replacer` 函数进行,其中不定参数 oldnew 是 old-new 对,即进行多个替换。 319 | 320 | 解决上面说的替换问题: 321 | 322 | r := strings.NewReplacer("<", "<", ">", ">") 323 | fmt.Println(r.Replace("This is HTML!")) 324 | 325 | 另外,Replacer 还提供了另外一个方法: 326 | 327 | func (r *Replacer) WriteString(w io.Writer, s string) (n int, err error) 328 | 329 | 它在替换之后将结果写入 io.Writer 中。 330 | 331 | ## 2.1.10 Reader 类型 ## 332 | 333 | 看到名字就能猜到,这是实现了 `io` 包中的接口。它实现了 io.Reader(Read 方法),io.ReaderAt(ReadAt 方法),io.Seeker(Seek 方法),io.WriterTo(WriteTo 方法),io.ByteReader(ReadByte 方法),io.ByteScanner(ReadByte 和 UnreadByte 方法),io.RuneReader(ReadRune 方法) 和 io.RuneScanner(ReadRune 和 UnreadRune 方法)。 334 | 335 | Reader 结构如下: 336 | 337 | type Reader struct { 338 | s string // Reader 读取的数据来源 339 | i int // current reading index(当前读的索引位置) 340 | prevRune int // index of previous rune; or < 0(前一个读取的 rune 索引位置) 341 | } 342 | 343 | 可见 Reader 结构没有导出任何字段,而是提供一个实例化方法: 344 | 345 | func NewReader(s string) *Reader 346 | 347 | 该方法接收一个字符串,返回的 Reader 实例就是从该参数字符串读数据。在后面学习了 bytes 包之后,可以知道 bytes.NewBufferString 有类似的功能,不过,如果只是为了读取,NewReader 会更高效。 348 | 349 | 其他方法不介绍了,都是之前接口的实现,有兴趣的可以看看源码实现,大部分都是根据 i、prevRune 两个属性来控制。 350 | 351 | # 导航 # 352 | 353 | - [第二章 文本](/chapter02/02.0.md) 354 | - 下一节:[bytes — byte slice 便利操作](02.2.md) -------------------------------------------------------------------------------- /chapter02/02.2.md: -------------------------------------------------------------------------------- 1 | # 2.2 bytes — byte slice 便利操作 # 2 | 3 | 该包定义了一些操作 byte slice 的便利操作。因为字符串可以表示为 []byte,因此,bytes 包定义的函数、方法等和 strings 包很类似,所以讲解时会和 strings 包类似甚至可以直接参考。 4 | 5 | 说明:为了方便,会称呼 []byte 为 字节数组 6 | 7 | ## 2.2.1 是否存在某个子slice 8 | 9 | // 子slice subslice 在 b 中,返回 true 10 | func Contains(b, subslice []byte) bool 11 | 12 | 该函数的内部调用了 bytes.Index 函数(在后面会讲解): 13 | 14 | func Contains(b, subslice []byte) bool { 15 | return Index(b, subslice) != -1 16 | } 17 | 18 | 题外:对比 `strings.Contains` 你会发现,一个判断 `>=0`,一个判断 `!= -1`,可见库不是一个人写的,没有做到一致性。 19 | 20 | ## 2.2.2 []byte 出现次数 ## 21 | 22 | // slice sep 在 s 中出现的次数(无重叠) 23 | func Count(s, sep []byte) int 24 | 25 | 和 strings 实现不同,此包中的 Count 核心代码如下: 26 | 27 | count := 0 28 | c := sep[0] 29 | i := 0 30 | t := s[:len(s)-n+1] 31 | for i < len(t) { 32 | // 判断 sep 第一个字节是否在 t[i:] 中 33 | // 如果在,则比较之后相应的字节 34 | if t[i] != c { 35 | o := IndexByte(t[i:], c) 36 | if o < 0 { 37 | break 38 | } 39 | i += o 40 | } 41 | // 执行到这里表示 sep[0] == t[i] 42 | if n == 1 || Equal(s[i:i+n], sep) { 43 | count++ 44 | i += n 45 | continue 46 | } 47 | i++ 48 | } 49 | 50 | ## 2.2.3 - 7 参见 strings 包对应的部分 ## 51 | 52 | ## 2.2.4 ## 53 | 54 | # 导航 # 55 | 56 | - 上一节:[strings — 字符串操作](02.1.md) 57 | - 下一节:[strconv — 字符串和基本数据类型之间转换](02.3.md) -------------------------------------------------------------------------------- /chapter02/02.3.md: -------------------------------------------------------------------------------- 1 | # 2.3 strconv — 字符串和基本数据类型之间转换 # 2 | 3 | 这里的基本数据类型包括:布尔、整型(包括有/无符号、二进制、八进制、十进制和十六进制)和浮点型等。 4 | 5 | ## 2.3.1 strconv 包转换错误处理 ## 6 | 7 | 介绍具体的转换之前,先看看 *strconv* 中的错误处理。 8 | 9 | 由于将字符串转为其他数据类型可能会出错,*strconv* 包定义了两个 *error* 类型的变量:*ErrRange* 和 *ErrSyntax*。其中,*ErrRange* 表示值超过了类型能表示的最大范围,比如将 "128" 转为 int8 就会返回这个错误;*ErrSyntax* 表示语法错误,比如将 "" 转为 int 类型会返回这个错误。 10 | 11 | 然而,在返回错误的时候,不是直接将上面的变量值返回,而是通过构造一个 *NumError* 类型的 *error* 对象返回。*NumError* 结构的定义如下: 12 | 13 | // A NumError records a failed conversion. 14 | type NumError struct { 15 | Func string // the failing function (ParseBool, ParseInt, ParseUint, ParseFloat) 16 | Num string // the input 17 | Err error // the reason the conversion failed (ErrRange, ErrSyntax) 18 | } 19 | 20 | 可见,该结构记录了转换过程中发生的错误信息。该结构不仅包含了一个 *error* 类型的成员,记录具体的错误信息,而且它自己也实现了 *error* 接口: 21 | 22 | func (e *NumError) Error() string { 23 | return "strconv." + e.Func + ": " + "parsing " + Quote(e.Num) + ": " + e.Err.Error() 24 | } 25 | 26 | 包的实现中,定义了两个便捷函数,用于构造 *NumError* 对象: 27 | 28 | func syntaxError(fn, str string) *NumError { 29 | return &NumError{fn, str, ErrSyntax} 30 | } 31 | 32 | func rangeError(fn, str string) *NumError { 33 | return &NumError{fn, str, ErrRange} 34 | } 35 | 36 | 在遇到 *ErrSyntax* 或 *ErrRange* 错误时,通过上面的函数构造 *NumError* 对象。 37 | 38 | ## 2.3.2 字符串和整型之间的转换 ## 39 | 40 | ### 2.3.2.1 字符串转为整型 ### 41 | 42 | 包括三个函数:ParseInt、ParseUint 和 Atoi,函数原型如下: 43 | 44 | func ParseInt(s string, base int, bitSize int) (i int64, err error) 45 | func ParseUint(s string, base int, bitSize int) (n uint64, err error) 46 | func Atoi(s string) (i int, err error) 47 | 48 | 其中,Atoi 是 ParseInt 的便捷版,内部通过调用 *ParseInt(s, 10, 0)* 来实现的;ParseInt 转为有符号整型;ParseUint 转为无符号整型,着重介绍 ParseInt。 49 | 50 | 参数 *base* 代表字符串按照给定的进制进行解释。一般的,base 的取值为 2~36,如果 base 的值为 0,则会根据字符串的前缀来确定 base 的值:"0x" 表示 16 进制; "0" 表示 8 进制;否则就是 10 进制。 51 | 52 | 参数 *bitSize* 表示的是整数取值范围,或者说整数的具体类型。取值 0、8、16、32 和 64 分别代表 int、int8、int16、int32 和 int64。 53 | 54 | 这里有必要说一下,当 bitSize==0 时的情况。 55 | 56 | Go中,int/uint 类型,不同系统能表示的范围是不一样的,目前的实现是,32 位系统占 4 个字节;64 位系统占 8 个字节。当 bitSize==0 时,应该表示 32 位还是 64 位呢?这里没有利用 *runtime.GOARCH* 之类的方式,而是巧妙的通过如下表达式确定 intSize: 57 | 58 | const intSize = 32 << uint(^uint(0)>>63) 59 | const IntSize = intSize // number of bits in int, uint (32 or 64) 60 | 61 | 主要是 *^uint(0)>>63* 这个表达式。操作符 *^* 在这里是一元操作符 按位取反,而不是 按位异或。更多解释可以参考:[Go位运算:取反和异或](http://studygolang.com/topics/303)。 62 | 63 | 问题:下面的代码 n 和 err 的值分别是什么? 64 | 65 | n, err := strconv.ParseInt("128", 10, 8) 66 | 67 | 在 *ParseInt/ParseUint* 的实现中,如果字符串表示的整数超过了 bitSize 参数能够表示的范围,则会返回 ErrRange,同时会返回 bitSize 能够表示的最大或最小值。因此,这里的 n 是 127。 68 | 69 | 另外,*ParseInt* 返回的是 int64,这是为了能够容纳所有的整型,在实际使用中,可以根据传递的 bitSize,然后将结果转为实际需要的类型。 70 | 71 | 转换的基本原理(以 "128" 转 为 10 进制 int 为例): 72 | 73 | s := "128" 74 | n := 0 75 | for i := 0; i < len(s); i++ { 76 | n *= 10 + s[i] // base 77 | } 78 | 79 | 在循环处理的过程中,会检查数据的有效性和是否越界等。 80 | 81 | ### 2.3.2.2 整型转为字符串 ### 82 | 83 | 实际应用中,我们经常会遇到需要将字符串和整型连接起来,在Java中,可以通过操作符 "+" 做到。不过,在Go语言中,你需要将整型转为字符串类型,然后才能进行连接。这个时候,*strconv* 包中的整型转字符串的相关函数就派上用场了。这些函数签名如下: 84 | 85 | func FormatUint(i uint64, base int) string // 无符号整型转字符串 86 | func FormatInt(i int64, base int) string // 有符号整型转字符串 87 | func Itoa(i int) string 88 | 89 | 其中,*Itoa* 内部直接调用 *FormatInt(i, 10)* 实现的。base 参数可以取 2~36(0-9,a-z)。 90 | 91 | 转换的基本原理(以 10 进制的 127 转 string 为例) : 92 | 93 | const digits = "0123456789abcdefghijklmnopqrstuvwxyz" 94 | u := uint64(127) 95 | var a [65]byte 96 | i := len(a) 97 | b := uint64(base) 98 | for u >= b { 99 | i-- 100 | a[i] = digits[uintptr(u%b)] 101 | u /= b 102 | } 103 | i-- 104 | a[i] = digits[uintptr(u)] 105 | return string(a[1:]) 106 | 107 | 即将整数每一位数字对应到相应的字符,存入字符数组中,最后字符数组转为字符串即为结果。 108 | 109 | 具体实现时,当 base 是 2 的幂次方时,有优化处理(移位和掩码);十进制也做了优化。 110 | 111 | 标准库还提供了另外两个函数:*AppendInt* 和 *AppendUint*,这两个函数不是将整数转为字符串,而是将整数转为字符数组 append 到目标字符数组中。(最终,我们也可以通过返回的 []byte 得到字符串) 112 | 113 | 除了使用上述方法将整数转为字符串外,经常见到有人使用 *fmt* 包来做这件事。如: 114 | 115 | fmt.Sprintf("%d", 127) 116 | 117 | 那么,这两种方式我们该怎么选择呢?我们主要来考察一下性能。 118 | 119 | startTime := time.Now() 120 | for i := 0; i < 10000; i++ { 121 | fmt.Sprintf("%d", i) 122 | } 123 | fmt.Println(time.Now().Sub(startTime)) 124 | 125 | startTime := time.Now() 126 | for i := 0; i < 10000; i++ { 127 | strconv.Itoa(i) 128 | } 129 | fmt.Println(time.Now().Sub(startTime)) 130 | 131 | 我们分别循环转换了10000次。*Sprintf* 的时间是 3.549761ms,而 *Itoa* 的时间是 848.208us,相差 4 倍多。 132 | 133 | *Sprintf* 性能差些可以预见,因为它接收的是 interface,需要进行反射等操作。个人建议使用 *strconv* 包中的方法进行转换。 134 | 135 | 注意:别想着通过 string(65) 这种方式将整数转为字符串,这样实际上得到的会是 ASCCII 值为 65 的字符,即 'A'。 136 | 137 | 思考: 138 | 139 | 给定一个 40 以内的正整数,如何快速判断其是否是 2 的幂次方? 140 | 141 | *提示:在 strconv 包源码 itoa.go 文件中找答案* 142 | 143 | ## 2.3.3 字符串和布尔值之间的转换 ## 144 | 145 | Go中字符串和布尔值之间的转换比较简单,主要有三个函数: 146 | 147 | // 接受 1, t, T, TRUE, true, True, 0, f, F, FALSE, false, False 等字符串; 148 | // 其他形式的字符串会返回错误 149 | func ParseBool(str string) (value bool, err error) 150 | // 直接返回 "true" 或 "false" 151 | func FormatBool(b bool) string 152 | // 将 "true" 或 "false" append 到 dst 中 153 | // 这里用了一个 append 函数对于字符串的特殊形式:append(dst, "true"...) 154 | func AppendBool(dst []byte, b bool) 155 | 156 | ## 2.3.4 字符串和浮点数之间的转换 ## 157 | 158 | 类似的,包含三个函数: 159 | 160 | func ParseFloat(s string, bitSize int) (f float64, err error) 161 | func FormatFloat(f float64, fmt byte, prec, bitSize int) string 162 | func AppendFloat(dst []byte, f float64, fmt byte, prec int, bitSize int) 163 | 164 | 函数的命名和作用跟上面讲解的其他类型一致。 165 | 166 | 关于 *FormatFloat* 的 *fmt* 参数, 在第一章第三节[格式化IO](/chapter01/01.3.md)中有详细介绍。而 *prec* 表示有效数字(对 *fmt='b'* 无效),对于 'e', 'E' 和 'f',有效数字用于小数点之后的位数;对于 'g' 和 'G',则是所有的有效数字。例如: 167 | 168 | strconv.FormatFloat(1223.13252, 'e', 3, 32) // 结果:1.223e+03 169 | strconv.FormatFloat(1223.13252, 'g', 3, 32) // 结果:1.22e+03 170 | 171 | 由于浮点数有精度的问题,精度不一样,ParseFloat 和 FormatFloat 可能达不到互逆的效果。如: 172 | 173 | s := strconv.FormatFloat(1234.5678, 'g', 6, 64) 174 | strconv.ParseFloat(s, 64) 175 | 176 | 另外,fmt='b' 时,得到的字符串是无法通过 *ParseFloat* 还原的。 177 | 178 | 特别地(不区分大小写),+inf/inf,+infinity/infinity,-inf/-infinity 和 nan 通过 ParseFloat 转换分别返回对应的值(在 math 包中定义)。 179 | 180 | 同样的,基于性能的考虑,应该使用 *FormatFloat* 而不是 *fmt.Sprintf*。 181 | 182 | ## 2.3.5 其他导出的函数 ## 183 | 184 | 如果要输出这样一句话:*This is "studygolang.com" website*. 该如何做? 185 | 186 | So easy: 187 | 188 | fmt.Println(`This is "studygolang.com" website`) 189 | 190 | 如果没有 *``* 符号,该怎么做?转义: 191 | 192 | fmt.Println("This is \"studygolang.com\" website") 193 | 194 | 除了这两种方法,*strconv* 包还提供了函数这做件事(Quote 函数)。我们称 "studygolang.com" 这种用双引号引起来的字符串为 Go 语言字面值字符串(Go string literal)。 195 | 196 | 上面的一句话可以这么做: 197 | 198 | fmt.Println("This is", strconv.Quote("studygolang.com"), "website") 199 | 200 | 201 | # 导航 # 202 | 203 | - 上一节:[bytes — byte slice 便利操作](02.2.md) 204 | - 下一节:[regexp — 正则表达式](02.4.md) 205 | -------------------------------------------------------------------------------- /chapter02/02.4.md: -------------------------------------------------------------------------------- 1 | # 2.4 regexp — 正则表达式 2 | 3 | 正则表达式使用单个字符串来描述、匹配一系列符合某个句法规则的字符串。正则表达式为文本处理提供了强大的功能。Go作为一门通用语言,自然提供了对正则表达式的支持。 4 | 5 | `regexp` 包实现了正则表达式搜索。 6 | 7 | 正则表达式采用RE2语法(除了\c、\C),和Perl、Python等语言的正则基本一致。确切地说是兼容 `RE2` 语法。相关资料:[http://code.google.com/p/re2/wiki/Syntax](http://code.google.com/p/re2/wiki/Syntax),[包:regexp/syntax](http://docs.studygolang.com/pkg/regexp/syntax/) 8 | 9 | 注意:`regexp` 包的正则表达式实现保证运行时间随着输入大小线性增长的(即复杂度为O\(n\),其中n为输入的长度),这一点,很多正则表达式的开源实现无法保证,参见:RSC 的 [《Regular Expression Matching Can Be Simple And Fast 10 | \(but is slow in Java, Perl, PHP, Python, Ruby, ...\)》](http://swtch.com/~rsc/regexp/regexp1.html) 11 | 12 | 另外,所有的字符都被视为utf-8编码的码值\(Code Point\)。 13 | 14 | Regexp类型提供了多达16个方法,用于匹配正则表达式并获取匹配的结果。它们的名字满足如下正则表达式: 15 | 16 | ``` 17 | Find(All)?(String)?(Submatch)?(Index)? 18 | ``` 19 | 20 | 未完待续。。。 21 | 22 | [https://github.com/StefanSchroeder/Golang-Regex-Tutorial](https://github.com/StefanSchroeder/Golang-Regex-Tutorial) 23 | 24 | # 导航 25 | 26 | * 上一节:[strconv — 字符串和基本数据类型之间转换](02.3.md) 27 | * 下一节:[unicode — Unicode码点、UTF-8/16编码](02.5.md) 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /chapter02/02.5.md: -------------------------------------------------------------------------------- 1 | # 2.5 unicode — Unicode码点、UTF-8/16编码 # 2 | 3 | 世界中的字符有许许多多,有英文,中文,韩文等。我们强烈需要一个大大的映射表把世界上的字符映射成计算机可以阅读的二进制数字(字节)。 4 | 这样,每个字符都给予一个独一无二的编码,就不会出现写文字的人和阅读文字的人编码不同而出现无法读取的乱码现象了。 5 | 6 | 于是Unicode就出现了,它是一种所有符号的编码映射。最开始的时候,unicode认为使用两个字节,也就是16位就能包含所有的字符了。 7 | 但是非常可惜,两个字节最多只能覆盖65536个字符,这显然是不够的,于是附加了一套字符编码,即unicode4.0,附加的字符用4个字节表示。 8 | 现在为止,大概Unicode可以覆盖100多万个字符了。 9 | 10 | Unicode只是定义了一个字符和一个编码的映射,但是呢,对应的存储却没有制定。 11 | 比如一个编码0x0041代表大写字母A,那么可能有一种存储至少有4个字节,那可能0x00000041来存储代表A。 12 | 这个就是unicode的具体实现。unicode的具体实现有很多种,UTF-8和UTF-16就是其中两种。 13 | 14 | UTF-8表示最少用一个字节就能表示一个字符的编码实现。它采取的方式是对不同的语言使用不同的方法,将unicode编码按照这个方法进行转换。 15 | 我们只要记住最后的结果是英文占一个字节,中文占三个字节。这种编码实现方式也是现在应用最为广泛的方式了。 16 | 17 | UTF-16表示最少用两个字节能表示一个字符的编码实现。同样是对unicode编码进行转换,它的结果是英文占用两个字节,中文占用两个或者四个字节。 18 | 实际上,UTF-16就是最严格实现了unicode4.0。但由于英文是最通用的语言,所以推广程度没有UTF-8那么普及。 19 | 20 | 回到go对unicode包的支持,由于UTF-8的作者Ken Thompson同时也是go语言的创始人,所以说,在字符支持方面,几乎没有语言的理解会高于go了。 21 | go对unicode的支持包含三个包: 22 | 23 | * unicode 24 | * unicode/utf8 25 | * unicode/utf16 26 | 27 | unicode包包含基本的字符判断函数。utf8包主要负责rune和byte之间的转换。utf16包负责rune和uint16数组之间的转换。 28 | 29 | 由于字符的概念有的时候比较模糊,比如字符(小写a)普通显示为a,在重音字符中(grave-accented)中显示为à。 30 | 这时候字符(character)的概念就有点不准确了,因为a和à显然是两个不同的unicode编码,但是却代表同一个字符,所以引入了rune。 31 | 一个rune就代表一个unicode编码,所以上面的a和à是两个不同的rune。 32 | 33 | 这里有个要注意的事情,go语言的所有代码都是UTF8的,所以如果我们在程序中的字符串都是utf8编码的,但是我们的单个字符(单引号扩起来的)却是unicode的。 34 | 35 | ## 2.5.1 unicode包 ## 36 | 37 | unicode包含了对rune的判断。这个包把所有unicode涉及到的编码进行了分类,使用结构 38 | 39 | ```golang 40 | type RangeTable struct { 41 | R16 []Range16 42 | R32 []Range32 43 | LatinOffset int 44 | } 45 | ``` 46 | 来表示这个功能的字符集。这些字符集都集中列表在table.go这个源码里面。 47 | 48 | 比如控制字符集: 49 | 50 | ```golang 51 | var _Pc = &RangeTable{ 52 | R16: []Range16{ 53 | {0x005f, 0x203f, 8160}, 54 | {0x2040, 0x2054, 20}, 55 | {0xfe33, 0xfe34, 1}, 56 | {0xfe4d, 0xfe4f, 1}, 57 | {0xff3f, 0xff3f, 1}, 58 | }, 59 | } 60 | ``` 61 | 62 | 回到包的函数,我们看到有下面这些判断函数: 63 | 64 | ``` 65 | func IsControl(r rune) bool // 是否控制字符 66 | func IsDigit(r rune) bool // 是否阿拉伯数字字符,即1-9 67 | func IsGraphic(r rune) bool // 是否图形字符 68 | func IsLetter(r rune) bool // 是否字母 69 | func IsLower(r rune) bool // 是否小写字符 70 | func IsMark(r rune) bool // 是否符号字符 71 | func IsNumber(r rune) bool // 是否数字字符,比如罗马数字Ⅷ也是数字字符 72 | func IsOneOf(ranges []*RangeTable, r rune) bool // 是否是RangeTable中的一个 73 | func IsPrint(r rune) bool // 是否可打印字符 74 | func IsPunct(r rune) bool // 是否标点符号 75 | func IsSpace(r rune) bool // 是否空格 76 | func IsSymbol(r rune) bool // 是否符号字符 77 | func IsTitle(r rune) bool // 是否title case 78 | func IsUpper(r rune) bool // 是否大写字符 79 | ``` 80 | 81 | 看下面这个例子: 82 | 83 | ```golang 84 | func main() { 85 | single := '\u0015' 86 | fmt.Println(unicode.IsControl(single)) //true 87 | single = '\ufe35' 88 | fmt.Println(unicode.IsControl(single)) // false 89 | 90 | digit := rune('1') 91 | fmt.Println(unicode.IsDigit(digit)) //true 92 | fmt.Println(unicode.IsNumber(digit)) //true 93 | letter := rune('Ⅷ') 94 | fmt.Println(unicode.IsDigit(letter)) //false 95 | fmt.Println(unicode.IsNumber(letter)) //true 96 | } 97 | ``` 98 | 99 | ## 2.5.2 utf8包 ## 100 | 101 | utf8里面的函数就有一些字节和字符的转换。 102 | 103 | 判断是否符合utf8编码的函数: 104 | * func Valid(p []byte) bool 105 | * func ValidRune(r rune) bool 106 | * func ValidString(s string) bool 107 | 108 | 判断rune的长度的函数: 109 | * func RuneLen(r rune) int 110 | 111 | 判断字节串或者字符串的rune数 112 | * func RuneCount(p []byte) int 113 | * func RuneCountInString(s string) (n int) 114 | 115 | 编码和解码rune到byte 116 | * func DecodeRune(p []byte) (r rune, size int) 117 | * func EncodeRune(p []byte, r rune) int 118 | 119 | ## 2.5.3 utf16包 ## 120 | 121 | utf16的包的函数就比较少了。 122 | 123 | 将int16和rune进行转换 124 | * func Decode(s []uint16) []rune 125 | * func Encode(s []rune) []uint16 126 | 127 | unicode有个基本字符平面和增补平面的概念,基本字符平面只有65535个字符,增补平面(有16个)加上去就能表示1114112个字符。 128 | utf16就是严格实现了unicode的这种编码规范。 129 | 130 | 而基本字符和增补平面字符就是一个代理对(Surrogate Pair)。一个代理对可以和一个rune进行转换。 131 | 132 | * func DecodeRune(r1, r2 rune) rune 133 | * func EncodeRune(r rune) (r1, r2 rune) 134 | 135 | # 导航 # 136 | 137 | - 上一节:[strings — 字符串操作](02.1.md) 138 | - 下一节:[strconv — 字符串和基本数据类型之间转换](02.3.md) 139 | -------------------------------------------------------------------------------- /chapter03/03.0.md: -------------------------------------------------------------------------------- 1 | # 第三章 数据结构与算法 # 2 | 3 | 程序设计离不开数据结构和算法。数据结构是数据组织和存储的逻辑形式,以达到方便访问和修改数据的目的。而算法是根据实际输入输出的需求设计的一系列计算过程,被认为是程序的灵魂。设计良好的算法的重要意义正如*Thomas*在《算法导论》中提到“计算机可以做得很快,但不是无限快;存储器可以做到很便宜,但不是免费的。因此,计算时间是一种有限的资源,存储空间也是一种有限的资源。这些有限的资源必须有效地使用,那些时间上和空间上有效的算法可以帮助做到这一点。“ 4 | 5 | 本章内容涵盖了Go标准库中的3个包: 6 | 7 | *sort* 包包含基本的排序方法,支持切片数据排序以及用户自定义数据集合排序 8 | 9 | *index/suffixary* 包实现了后缀数组相关算法以支持许多常见的字符串操作 10 | 11 | *container* 包提供了对heap、list和ring这3种数据结构的底层支持。任何实现了相应接口的数据结构都可以调用该结构的方法。 12 | 13 | # 导航 # 14 | 15 | - [第二章](/chapter02/02.0.md) 16 | - 下一节:[sort - 排序算法](03.1.md) 17 | -------------------------------------------------------------------------------- /chapter03/03.1.md: -------------------------------------------------------------------------------- 1 | # 3.1 sort —— 排序算法 # 2 | 3 | 该包实现了四种基本排序算法:插入排序、归并排序、堆排序和快速排序。 4 | 但是这四种排序方法是不公开的,它们只被用于sort包内部使用。所以在对数据集合排序时不必考虑应当选择哪一种排序方法,只要实现了sort.Interface定义的三个方法:获取数据集合长度的Len()方法、比较两个元素大小的Less()方法和交换两个元素位置的Swap()方法,就可以顺利对数据集合进行排序。sort包会根据实际数据自动选择高效的排序算法。 5 | 除此之外,为了方便对常用数据类型的操作,sort包提供了对[]int切片、[]float64切片和[]string切片完整支持,主要包括: 6 | - 对基本数据类型切片的排序支持 7 | - 基本数据元素查找 8 | - 判断基本数据类型切片是否已经排好序 9 | - 对排好序的数据集合逆序 10 | 11 | ## 3.1.1 数据集合排序 ## 12 | 13 | 前面已经提到过,对数据集合(包括自定义数据类型的集合)排序需要实现sort.Interface接口的三个方法,我们看以下该接口的定义: 14 | 15 | type Interface interface { 16 | // 获取数据集合元素个数 17 | Len() int 18 | // 如果i索引的数据小于j所以的数据,返回true,不会调用 19 | // 下面的Swap(),即数据升序排序。 20 | Less(i, j int) bool 21 | // 交换i和j索引的两个元素的位置 22 | Swap(i, j int) 23 | } 24 | 25 | 数据集合实现了这三个方法后,即可调用该包的Sort()方法进行排序。 26 | Sort()方法定义如下: 27 | 28 | func Sort(data Interface) 29 | 30 | Sort()方法惟一的参数就是待排序的数据集合。 31 | 32 | 该包还提供了一个方法可以判断数据集合是否已经排好顺序,该方法的内部实现依赖于我们自己实现的Len()和Less()方法: 33 | 34 | func IsSorted(data Interface) bool { 35 | n := data.Len() 36 | for i := n - 1; i > 0; i-- { 37 | if data.Less(i, i-1) { 38 | return false 39 | } 40 | } 41 | return true 42 | } 43 | 44 | 下面是一个使用sort包对学生成绩排序的示例: 45 | 46 | ```golang 47 | package main 48 | 49 | import ( 50 | "fmt" 51 | "sort" 52 | ) 53 | 54 | //学生成绩结构体 55 | type StuScore struct { 56 | //姓名 57 | name string 58 | //成绩 59 | score int 60 | } 61 | 62 | type StuScores []StuScore 63 | 64 | //Len() 65 | func (s StuScores) Len() int { 66 | return len(s) 67 | } 68 | 69 | //Less():成绩将有低到高排序 70 | func (s StuScores) Less(i, j int) bool { 71 | return s[i].score < s[j].score 72 | } 73 | 74 | //Swap() 75 | func (s StuScores) Swap(i, j int) { 76 | s[i], s[j] = s[j], s[i] 77 | } 78 | 79 | func main() { 80 | stus := StuScores{ 81 | {"alan", 95}, 82 | {"hikerell", 91}, 83 | {"acmfly", 96}, 84 | {"leao", 90}} 85 | 86 | fmt.Println("Default:") 87 | //原始顺序 88 | for _, v := range stus { 89 | fmt.Println(v.name, ":", v.score) 90 | } 91 | fmt.Println() 92 | //StuScores已经实现了sort.Interface接口 93 | sort.Sort(stus) 94 | 95 | fmt.Println("Sorted:") 96 | //排好序后的结构 97 | for _, v := range stus { 98 | fmt.Println(v.name, ":", v.score) 99 | } 100 | 101 | //判断是否已经排好顺序,将会打印true 102 | fmt.Println("IS Sorted?", sort.IsSorted(stus)) 103 | } 104 | ``` 105 | 程序该示例程序的自定义类型StuScores实现了sort.Interface接口,所以可以将其对象作为sort.Sort()和sort.IsSorted()的参数传入。运行结果: 106 | 107 | ======Default====== 108 | alan : 95 109 | hikerell : 91 110 | acmfly : 96 111 | leao : 90 112 | 113 | ======Sorted======= 114 | leao : 90 115 | hikerell : 91 116 | alan : 95 117 | acmfly : 96 118 | IS Sorted? true 119 | 120 | 该示例实现的是升序排序,如果要得到降序排序结果,其实只要修改Less()函数: 121 | ```golang 122 | //Less():成绩降序排序,只将小于号修改为大于号 123 | func (s StuScores) Less(i, j int) bool { 124 | return s[i].score > s[j].score 125 | } 126 | ``` 127 | 此外,*sort*包提供了Reverse()方法,可以允许将数据按Less()定义的排序方式逆序排序,而不必修改Less()代码。方法定义如下: 128 | 129 | func Reverse(data Interface) Interface 130 | 131 | 我们可以看到Reverse()返回的一个sort.Interface接口类型,整个Reverse()的内部实现比较有趣: 132 | ```golang 133 | //定义了一个reverse结构类型,嵌入Interface接口 134 | type reverse struct { 135 | Interface 136 | } 137 | 138 | //reverse结构类型的Less()方法拥有嵌入的Less()方法相反的行为 139 | //Len()和Swap()方法则会保持嵌入类型的方法行为 140 | func (r reverse) Less(i, j int) bool { 141 | return r.Interface.Less(j, i) 142 | } 143 | 144 | //返回新的实现Interface接口的数据类型 145 | func Reverse(data Interface) Interface { 146 | return &reverse{data} 147 | } 148 | ``` 149 | 了解内部原理后,可以在学生成绩排序示例中使用Reverse()来实现成绩升序排序: 150 | ```golang 151 | sort.Sort(sort.Reverse(stus)) 152 | for _, v := range stus { 153 | fmt.Println(v.name, ":", v.score) 154 | } 155 | ``` 156 | 157 | 最后一个方法:Search() 158 | 159 | func Search(n int, f func(int) bool) int 160 | 161 | 官方文档这样描述该方法: 162 | >Search()方法回使用“二分查找”算法来搜索某指定切片[0:n],并返回能够使f(i)=true的最 163 | >小的i(0<=i<n)值,并且会假定,如果f(i)=true,则f(i+1)=true,即对于切片[0:n], 164 | >i之前的切片元素会使f()函数返回false,i及i之后的元素会使f()函数返回true。但是,当 165 | >在切片中无法找到时f(i)=true的i时(此时切片元素都不能使f()函数返回true),Search() 166 | >方法会返回n。 167 | 168 | Search()函数一个常用的使用方式是搜索元素x是否在已经升序排好的切片s中: 169 | 170 | ```golang 171 | x := 11 172 | s := []int{3, 6, 8, 11, 45} //注意已经升序排序 173 | pos := sort.Search(len(s), func(i int) bool { return s[i] >= x }) 174 | if pos < len(s) && s[pos] == x { 175 | fmt.Println(x, "在s中的位置为:", pos) 176 | } else { 177 | fmt.Println("s不包含元素", x) 178 | } 179 | ``` 180 | 181 | 官方文档还给出了一个猜数字的小程序: 182 | 183 | ```golang 184 | func GuessingGame() { 185 | var s string 186 | fmt.Printf("Pick an integer from 0 to 100.\n") 187 | answer := sort.Search(100, func(i int) bool { 188 | fmt.Printf("Is your number <= %d? ", i) 189 | fmt.Scanf("%s", &s) 190 | return s != "" && s[0] == 'y' 191 | }) 192 | fmt.Printf("Your number is %d.\n", answer) 193 | } 194 | ``` 195 | 196 | ## 3.1.2 *sort*包已经支持的内部数据类型排序 197 | 198 | 前面已经提到,*sort*包原生支持[]int、[]float64和[]string三种内建数据类型切片的排序操作,即不必我们自己实现相关的Len()、Less()和Swap()方法。 199 | 200 | **1. IntSlice类型及[]int排序** 201 | 202 | 由于[]int切片排序内部实现及使用方法与[]float64和[]string类似,所以只详细描述该部分。 203 | 204 | *sort*包定义了一个IntSlice类型,并且实现了sort.Interface接口: 205 | 206 | ```golang 207 | type IntSlice []int 208 | func (p IntSlice) Len() int { return len(p) } 209 | func (p IntSlice) Less(i, j int) bool { return p[i] < p[j] } 210 | func (p IntSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } 211 | //IntSlice类型定义了Sort()方法,包装了sort.Sort()函数 212 | func (p IntSlice) Sort() { Sort(p) } 213 | //IntSlice类型定义了SearchInts()方法,包装了SearchInts()函数 214 | func (p IntSlice) Search(x int) int { return SearchInts(p, x) } 215 | ``` 216 | 并且提供的sort.Ints()方法使用了该IntSlice类型: 217 | ```goalng 218 | func Ints(a []int) { Sort(IntSlice(a)) } 219 | ``` 220 | 221 | 所以,对[]int切片排序是更常使用sort.Ints(),而不是直接使用IntSlice类型: 222 | 223 | ```golang 224 | s := []int{5, 2, 6, 3, 1, 4} // 未排序的切片数据 225 | sort.Ints(s) 226 | fmt.Println(s) //将会输出[1 2 3 4 5 6] 227 | ``` 228 | 如果要使用降序排序,显然要用前面提到的Reverse()方法: 229 | 230 | ```golang 231 | s := []int{5, 2, 6, 3, 1, 4} // 未排序的切片数据 232 | sort.Sort(sort.Reverse(sort.IntSlice(s))) 233 | fmt.Println(s) //将会输出[6 5 4 3 2 1] 234 | ``` 235 | 236 | 如果要查找整数x在切片a中的位置,相对于前面提到的Search()方法,*sort*包提供了SearchInts(): 237 | 238 | ```golang 239 | func SearchInts(a []int, x int) int 240 | ``` 241 | 注意,SearchInts()的使用条件为:**切片a已经升序排序** 242 | 243 | ```golang 244 | s := []int{5, 2, 6, 3, 1, 4} // 未排序的切片数据 245 | sort.Ints(s) //排序后的s为[1 2 3 4 5 6] 246 | fmt.Println(sort.SearchInts(s, 3)) //将会输出2 247 | ``` 248 | 249 | **2. Float64Slice类型及[]float64排序** 250 | 251 | 实现与Ints类似,只看一下其内部实现: 252 | 253 | ```golang 254 | type Float64Slice []float64 255 | 256 | func (p Float64Slice) Len() int { return len(p) } 257 | func (p Float64Slice) Less(i, j int) bool { return p[i] < p[j] || isNaN(p[i]) && !isNaN(p[j]) } 258 | func (p Float64Slice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } 259 | func (p Float64Slice) Sort() { Sort(p) } 260 | func (p Float64Slice) Search(x float64) int { return SearchFloat64s(p, x) } 261 | ``` 262 | 与Sort()、IsSorted()、Search()相对应的三个方法: 263 | 264 | func Float64s(a []float64) 265 | func Float64sAreSorted(a []float64) bool 266 | func SearchFloat64s(a []float64, x float64) int 267 | 268 | 要说明一下的是,在上面Float64Slice类型定义的Less方法中,有一个内部函数isNaN()。 269 | isNaN()与*math*包中IsNaN()实现完全相同,*sort*包之所以不使用math.IsNaN(),完全是基于包依赖性的考虑,应当看到,*sort*包的实现不依赖与其他任何包。 270 | 271 | **3. StringSlice类型及[]string排序** 272 | 273 | 两个string对象之间的大小比较是基于“字典序”的。 274 | 275 | 实现与Ints类似,只看一下其内部实现: 276 | 277 | ```golang 278 | type StringSlice []string 279 | 280 | func (p StringSlice) Len() int { return len(p) } 281 | func (p StringSlice) Less(i, j int) bool { return p[i] < p[j] } 282 | func (p StringSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } 283 | func (p StringSlice) Sort() { Sort(p) } 284 | func (p StringSlice) Search(x string) int { return SearchStrings(p, x) } 285 | ``` 286 | 287 | 与Sort()、IsSorted()、Search()相对应的三个方法: 288 | 289 | func Strings(a []string) 290 | func StringsAreSorted(a []string) bool 291 | func SearchStrings(a []string, x string) int 292 | 293 | # 导航 # 294 | 295 | - [第三章 数据结构与算法](/chapter03/03.0.md) 296 | - 下一节:index/suffixarray — 后缀数组实现子字符串查询 297 | -------------------------------------------------------------------------------- /chapter03/03.3.md: -------------------------------------------------------------------------------- 1 | # 3.3 container — 容器数据类型:heap、list和ring # 2 | 3 | 该包实现了三个复杂的数据结构:堆,链表,环。 4 | 这个包就意味着你使用这三个数据结构的时候不需要再费心从头开始写算法了。 5 | 6 | ## 3.3.1 堆 ## 7 | 8 | 这里的堆使用的数据结构是最小二叉树,即根节点比左边子树和右边子树的所有值都小。 9 | go的堆包只是实现了一个接口,我们看下它的定义: 10 | 11 | ```golang 12 | type Interface interface { 13 | sort.Interface 14 | Push(x interface{}) // add x as element Len() 15 | Pop() interface{} // remove and return element Len() - 1. 16 | } 17 | ``` 18 | 19 | 可以看出,这个堆结构继承自sort.Interface, 回顾下sort.Interface,它需要实现三个方法 20 | 21 | * Len() int 22 | * Less(i, j int) bool 23 | * Swap(i, j int) 24 | 25 | 加上堆接口定义的两个方法 26 | 27 | * Push(x interface{}) 28 | * Pop() interface{} 29 | 30 | 就是说你定义了一个堆,就要实现五个方法,直接拿package doc中的example做例子: 31 | 32 | ```golang 33 | type IntHeap []int 34 | 35 | func (h IntHeap) Len() int { return len(h) } 36 | func (h IntHeap) Less(i, j int) bool { return h[i] < h[j] } 37 | func (h IntHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } 38 | 39 | func (h *IntHeap) Push(x interface{}) { 40 | *h = append(*h, x.(int)) 41 | } 42 | 43 | func (h *IntHeap) Pop() interface{} { 44 | old := *h 45 | n := len(old) 46 | x := old[n-1] 47 | *h = old[0 : n-1] 48 | return x 49 | } 50 | ``` 51 | 那么IntHeap就实现了这个堆结构,我们就可以使用堆的方法来对它进行操作: 52 | 53 | ```golang 54 | h := &IntHeap{2, 1, 5} 55 | heap.Init(h) 56 | heap.Push(h, 3) 57 | heap.Pop(h) 58 | ``` 59 | 60 | 具体说下内部实现,是使用最小堆,索引排序从根节点开始,然后左子树,右子树的顺序方式。 61 | 内部实现了down和up分别表示对堆中的某个元素向上保证最小堆和向上保证最小堆。 62 | 63 | 当往堆中插入一个元素的时候,这个元素插入到最右子树的最后一个节点中,然后调用up向上保证最小堆。 64 | 65 | 当要从堆中推出一个元素的时候,先吧这个元素和右子树最后一个节点兑换,然后弹出最后一个节点,然后对root调用down,向下保证最小堆。 66 | 67 | ## 3.3.2 链表 ## 68 | 69 | 链表就是一个有prev和next指针的数组了。它维护两个type,(注意,这里不是interface) 70 | 71 | ```golang 72 | type Element struct { 73 | next, prev *Element // 上一个元素和下一个元素 74 | list *List // 元素所在链表 75 | Value interface{} // 元素 76 | } 77 | 78 | type List struct { 79 | root Element // 链表的根元素 80 | len int // 链表的长度 81 | } 82 | ``` 83 | 84 | 基本使用是先创建list,然后往list中插入值,list就内部创建一个Element,并内部设置好Element的next,prev等。具体可以看下例子: 85 | 86 | ```golang 87 | // This example demonstrates an integer heap built using the heap interface. 88 | package main 89 | 90 | import ( 91 | "container/list" 92 | "fmt" 93 | ) 94 | 95 | func main() { 96 | list := list.New() 97 | list.PushBack(1) 98 | list.PushBack(2) 99 | 100 | fmt.Printf("len: %v\n", list.Len()); 101 | fmt.Printf("first: %#v\n", list.Front()); 102 | fmt.Printf("second: %#v\n", list.Front().Next()); 103 | } 104 | 105 | output: 106 | len: 2 107 | first: &list.Element{next:(*list.Element)(0x2081be1b0), prev:(*list.Element)(0x2081be150), list:(*list.List)(0x2081be150), Value:1} 108 | second: &list.Element{next:(*list.Element)(0x2081be150), prev:(*list.Element)(0x2081be180), list:(*list.List)(0x2081be150), Value:2} 109 | ``` 110 | 111 | list对应的方法有: 112 | ``` 113 | type Element 114 | func (e *Element) Next() *Element 115 | func (e *Element) Prev() *Element 116 | type List 117 | func New() *List 118 | func (l *List) Back() *Element // 最后一个元素 119 | func (l *List) Front() *Element // 第一个元素 120 | func (l *List) Init() *List // 链表初始化 121 |    func (l *List) InsertAfter(v interface{}, mark *Element) *Element // 在某个元素后插入 122 |    func (l *List) InsertBefore(v interface{}, mark *Element) *Element // 在某个元素前插入 123 | func (l *List) Len() int // 在链表长度 124 | func (l *List) MoveAfter(e, mark *Element) // 把e元素移动到mark之后 125 | func (l *List) MoveBefore(e, mark *Element) // 把e元素移动到mark之前 126 | func (l *List) MoveToBack(e *Element) // 把e元素移动到队列最后 127 | func (l *List) MoveToFront(e *Element) // 把e元素移动到队列最头部 128 | func (l *List) PushBack(v interface{}) *Element // 在队列最后插入元素 129 | func (l *List) PushBackList(other *List) // 在队列最后插入接上新队列 130 | func (l *List) PushFront(v interface{}) *Element // 在队列头部插入元素 131 | func (l *List) PushFrontList(other *List) // 在队列头部插入接上新队列 132 | func (l *List) Remove(e *Element) interface{} // 删除某个元素 133 | ``` 134 | 135 | ## 3.3.3 环 ## 136 | 137 | 环的结构有点特殊,环的尾部就是头部,所以每个元素实际上就可以代表自身的这个环。 138 | 它不需要像list一样保持list和element两个结构,只需要保持一个结构就行。 139 | 140 | ```golang 141 | type Ring struct { 142 | next, prev *Ring 143 | Value interface{} 144 | } 145 | ``` 146 | 147 | 我们初始化环的时候,需要定义好环的大小,然后对环的每个元素进行赋值。环还提供一个Do方法,能便利一遍环,对每个元素执行一个function。 148 | 看下面的例子: 149 | 150 | ```golang 151 | // This example demonstrates an integer heap built using the heap interface. 152 | package main 153 | 154 | import ( 155 | "container/ring" 156 | "fmt" 157 | ) 158 | 159 | func main() { 160 | ring := ring.New(3) 161 | 162 | for i := 1; i <= 3; i++ { 163 | ring.Value = i 164 | ring = ring.Next() 165 | } 166 | 167 | // 计算1+2+3 168 | s := 0 169 | ring.Do(func(p interface{}){ 170 | s += p.(int) 171 | }) 172 | fmt.Println("sum is", s) 173 | } 174 | 175 | output: 176 | sum is 6 177 | ``` 178 | 179 | ring提供的方法有 180 | 181 | ``` 182 | type Ring 183 | func New(n int) *Ring // 初始化环 184 | func (r *Ring) Do(f func(interface{})) // 循环环进行操作 185 | func (r *Ring) Len() int // 环长度 186 | func (r *Ring) Link(s *Ring) *Ring // 连接两个环 187 | func (r *Ring) Move(n int) *Ring // 指针从当前元素开始向后移动或者向前(n可以为负数) 188 | func (r *Ring) Next() *Ring // 当前元素的下个元素 189 | func (r *Ring) Prev() *Ring // 当前元素的上个元素 190 | func (r *Ring) Unlink(n int) *Ring // 从当前元素开始,删除n个元素 191 | ``` 192 | 193 | # 导航 # 194 | 195 | - [第三章 数据结构与算法](/chapter03/03.0.md) 196 | - 上一节:index/suffixarray — 后缀数组实现子字符串查询 197 | - 下一节:container总结 198 | -------------------------------------------------------------------------------- /chapter04/04.0.md: -------------------------------------------------------------------------------- 1 | # 第四章 日期与时间 # 2 | 3 | 实际开发中,经常会遇到日期和时间相关的操作,比如:格式化日期和时间,解析一个日期时间字符串等。Go语言通过标准库 `time` 包处理日期和时间相关的问题。 4 | 5 | 本章只有 `time` 这一个包,为了便于阅读,将拆为如下小结进行讲解: 6 | 7 | - [主要类型概述](04.1.md) 8 | - [时区](04.2.md) 9 | - [Time类型详解](04.3.md) 10 | - [定时器](04.4.md) 11 | 12 | # 导航 # 13 | 14 | - [第三章](/chapter03/03.0.md) 15 | - 下一节:[主要类型概述](04.1.md) 16 | -------------------------------------------------------------------------------- /chapter04/04.1.md: -------------------------------------------------------------------------------- 1 | # 4.1 主要类型概述 # 2 | 3 | time 包提供了时间的显示和计量用的功能。日历的计算采用的是公历。提供的主要类型如下: 4 | 5 | ## Location 6 | 7 | 代表一个地区,并表示该地区所在的时区(可能多个)。`Location` 通常代表地理位置的偏移,比如 CEST 和 CET 表示中欧。下一节将详细讲解 Location。 8 | 9 | ## Time 10 | 11 | 代表一个纳秒精度的时间点,是公历时间。后面会详细介绍。 12 | 13 | ## Duration 14 | 15 | 代表两个时间点之间经过的时间,以纳秒为单位。可表示的最长时间段大约290年,也就是说如果两个时间点相差超过 290 年,会返回 290 年,也就是 minDuration(-1 << 63) 或 maxDuration(1 << 63 - 1)。 16 | 17 | 类型定义:`type Duration int64`。 18 | 19 | 将 `Duration` 类型直接输出时,因为实现了 `fmt.Stringer` 接口,会输出人类友好的可读形式,如:72h3m0.5s。 20 | 21 | ## Timer 和 Ticker 22 | 23 | 这是定时器相关类型。本章最后会讨论定时器。 24 | 25 | ## Weekday 和 Month 26 | 27 | 这两个类型的原始类型都是 int,定义它们,语义更明确,同时,实现 `fmt.Stringer` 接口,方便输出。 28 | 29 | # 导航 # 30 | 31 | - [第四章 日期与时间](04.0.md) 32 | - 下一节:[时区](04.2.md) 33 | -------------------------------------------------------------------------------- /chapter04/04.2.md: -------------------------------------------------------------------------------- 1 | # 4.2 时区 # 2 | 3 | 不同国家(有时甚至是同一个国家内的不同地区)使用不同的时区。对于要输入和输出时间的程序来说,必须对系统所处的时区加以考虑。Go 语言使用 `Location` 来表示地区相关的时区,一个 Location 可能表示多个时区。 4 | 5 | `time` 包提供了 Location 的两个实例:`Local` 和 `UTC`。`Local` 代表当前系统本地时区;`UTC` 代表通用协调时间,也就是零时区。`time` 包默认(为显示提供时区)使用 `UTC` 时区。 6 | 7 | ## Local 是如何做到表示本地时区的? 8 | 9 | 时区信息既浩繁又多变,Unix 系统以标准格式存于文件中,这些文件位于 /usr/share/zoneinfo,而本地时区可以通过 /etc/localtime 获取,这是一个符号链接,指向 /usr/share/zoneinfo 中某一个时区。比如我本地电脑指向的是:/usr/share/zoneinfo/Asia/Shanghai。 10 | 11 | 因此,在初始化 Local 时,通过读取 /etc/localtime 可以获取到系统本地时区。 12 | 13 | 当然,如果设置了环境变量 `TZ`,则会优先使用它。 14 | 15 | 相关代码: 16 | 17 | ``` 18 | tz, ok := syscall.Getenv("TZ") 19 | switch { 20 | case !ok: 21 | z, err := loadZoneFile("", "/etc/localtime") 22 | if err == nil { 23 | localLoc = *z 24 | localLoc.name = "Local" 25 | return 26 | } 27 | case tz != "" && tz != "UTC": 28 | if z, err := loadLocation(tz); err == nil { 29 | localLoc = *z 30 | return 31 | } 32 | } 33 | ``` 34 | ## 获得特定时区的实例 35 | 36 | 函数 `LoadLocation` 可以根据名称获取特定时区的实例。函数声明如下: 37 | 38 | `func LoadLocation(name string) (*Location, error)` 39 | 40 | 如果 name 是""或"UTC",返回UTC;如果 name 是"Local",返回Local;否则 name 应该是IANA时区数据库里有记录的地点名(该数据库记录了地点和对应的时区),如"America/New_York"。 41 | 42 | LoadLocation 函数需要的时区数据库可能不是所有系统都提供,特别是非Unix系统。此时 `LoadLocation` 会查找环境变量 ZONEINFO 指定目录或解压该变量指定的zip文件(如果有该环境变量);然后查找Unix系统的惯例时区数据安装位置,最后查找 `$GOROOT/lib/time/zoneinfo.zip`。 43 | 44 | 可以在 Unix 系统下的 /usr/share/zoneinfo 中找到所有的名称。 45 | 46 | ## 总结 47 | 48 | 通常,我们使用 `time.Local` 即可,偶尔可能会需要使用 `UTC`。在解析时间时,心中一定记得有时区这么回事。当你发现时间出现莫名的情况时,很可能是因为时区的问题,特别是当时间相差 8 小时时。 49 | 50 | # 导航 # 51 | 52 | - 上一节:[主要类型概述](04.1.md) 53 | - 下一节:[Time类型详解](04.3.md) 54 | -------------------------------------------------------------------------------- /chapter04/04.3.md: -------------------------------------------------------------------------------- 1 | # 4.3 Time 类型详解 # 2 | 3 | `Time` 代表一个纳秒精度的时间点。 4 | 5 | 程序中应使用 Time 类型值来保存和传递时间,而不是指针。就是说,表示时间的变量和字段,应为time.Time类型,而不是*time.Time.类型。一个Time类型值可以被多个go程同时使用。时间点可以使用Before、After和Equal方法进行比较。Sub方法让两个时间点相减,生成一个Duration类型值(代表时间段)。Add方法给一个时间点加上一个时间段,生成一个新的Time类型时间点。 6 | 7 | Time 零值代表时间点 January 1, year 1, 00:00:00.000000000 UTC。因为本时间点一般不会出现在使用中,IsZero 方法提供了检验时间是否是显式初始化的一个简单途径。 8 | 9 | 每一个时间都具有一个地点信息(及对应地点的时区信息),当计算时间的表示格式时,如Format、Hour和Year等方法,都会考虑该信息。Local、UTC和In方法返回一个指定时区(但指向同一时间点)的Time。修改地点/时区信息只是会改变其表示;不会修改被表示的时间点,因此也不会影响其计算。 10 | 11 | 通过 == 比较 Time 时,Location 信息也会参与比较,因此 Time 不应该作为 map 的 key。 12 | 13 | ## Time 的内部结构 14 | ``` 15 | type Time struct { 16 | // sec gives the number of seconds elapsed since 17 | // January 1, year 1 00:00:00 UTC. 18 | sec int64 19 | 20 | // nsec specifies a non-negative nanosecond 21 | // offset within the second named by Seconds. 22 | // It must be in the range [0, 999999999]. 23 | nsec int32 24 | 25 | // loc specifies the Location that should be used to 26 | // determine the minute, hour, month, day, and year 27 | // that correspond to this Time. 28 | // Only the zero Time has a nil Location. 29 | // In that case it is interpreted to mean UTC. 30 | loc *Location 31 | } 32 | ``` 33 | 34 | 要讲解 `time.Time` 的内部结构,得先看 `time.Now()` 函数。 35 | 36 | ``` 37 | // Now returns the current local time. 38 | func Now() Time { 39 | sec, nsec := now() 40 | return Time{sec + unixToInternal, nsec, Local} 41 | } 42 | ``` 43 | now() 的具体实现在 `runtime` 包中,以 linux/amd64 为例,在 sys_linux_amd64.s 中的 `time·now`,这是汇编实现的: 44 | 45 | * 调用系统调用 `clock_gettime` 获取时钟值(这是 POSIX 时钟)。其中 clockid_t 时钟类型是 CLOCK_REALTIME,也就是可设定的系统级实时时钟。得到的是 struct timespec 类型。(可以到纳秒) 46 | * 如果 `clock_gettime` 不存在,则使用精度差些的系统调用 `gettimeofday`。得到的是 struct timeval 类型。(最多到微妙) 47 | 48 | *注意:* 这里使用了 Linux 的 vdso 特性,不了解的,可以查阅相关知识。 49 | 50 | 虽然 `timespec` 和 `timeval` 不一样,但结构类似。因为 `now()` 函数返回两个值:sec(秒)和 nsec(纳秒),所以,`time·now` 的实现将这两个结构转为需要的返回值。需要注意的是,Linux 系统调用返回的 sec(秒) 是 Unix 时间戳,也就是从 1970-1-1 算起的。 51 | 52 | 回到 `time.Now()` 的实现,现在我们得到了 sec 和 nsec,从 `Time{sec + unixToInternal, nsec, Local}` 这句可以看出,Time 结构的 sec 并非 Unix 时间戳,实际上,加上的 `unixToInternal` 是 1-1-1 到 1970-1-1 经历的秒数。也就是 `Time` 中的 sec 是从 1-1-1 算起的秒数,而不是 Unix 时间戳。 53 | 54 | `Time` 的最后一个字段表示地点时区信息。本章后面会专门介绍。 55 | 56 | ## 常用函数或方法 57 | 58 | `Time` 相关的函数和方法较多,有些很容易理解,不赘述,查文档即可。 59 | 60 | ### 零值的判断 61 | 62 | 因为 `Time` 的零值是 sec 和 nsec 都是0,表示 1年1月1日。 63 | 64 | Time.IsZero() 函数用于判断 Time 表示的时间是否是 0 值。 65 | 66 | ### 与 Unix 时间戳的转换 67 | 68 | 相关函数或方法: 69 | 70 | * time.Unix(sec, nsec int64) 通过 Unix 时间戳生成 `time.Time` 实例; 71 | * time.Time.Unix() 得到 Unix 时间戳; 72 | * time.Time.UnixNano() 得到 Unix 时间戳的纳秒表示; 73 | 74 | ### 格式化和解析 75 | 76 | 这是实际开发中常用到的。 77 | 78 | * time.Parse 和 time.ParseInLocation 79 | * time.Time.Format 80 | 81 | #### 解析 82 | 83 | 对于解析,要特别注意时区问题,否则很容易出 bug。比如: 84 | 85 | ``` 86 | t, _ := time.Parse("2006-01-02 15:04:05", "2016-06-13 09:14:00") 87 | fmt.Println(time.Now().Sub(t).Hours()) 88 | ``` 89 | `2016-06-13 09:14:00` 这个时间可能是参数传递过来的。这段代码的结果跟预期的不一样。 90 | 91 | 原因是 `time.Now()` 的时区是 `time.Local`,而 `time.Parse` 解析出来的时区却是 time.UTC(可以通过 `Time.Location()` 函数知道是哪个时区)。在中国,它们相差 8 小时。 92 | 93 | 所以,一般的,我们应该总是使用 `time.ParseInLocation` 来解析时间,并给第三个参数传递 `time.Local`。 94 | 95 | #### 为什么是 2006-01-02 15:04:05 96 | 97 | 可能你已经注意到:`2006-01-02 15:04:05` 这个字符串了。没错,这是固定写法,类似于其他语言中 `Y-m-d H:i:s` 等。为什么采用这种形式?又为什么是这个时间点而不是其他时间点? 98 | 99 | * 官方说,使用具体的时间,比使用 `Y-m-d H:i:s` 更容易理解和记忆;这么一说还真是~ 100 | * 而选择这个时间点,也是出于好记的考虑,官方的例子:`Mon Jan 2 15:04:05 MST 2006`,另一种形式 `01/02 03:04:05PM '06 -0700`,对应是 1、2、3、4、5、6、7;常见的格式:`2006-01-02 15:04:05`,很好记:2006年1月2日3点4分5秒~ 101 | 102 | *如果你是 PHPer,喜欢 PHP 的格式,可以试试 [times](https://github.com/polaris1119/times) 这个包。* 103 | 104 | #### 格式化 105 | 106 | 时间格式化输出,使用 `Format` 方法,`layout` 参数和 `Parse` 的一样。`Time.String()` 方法使用了 `2006-01-02 15:04:05.999999999 -0700 MST` 这种 `layout`。 107 | 108 | ### 实现 序列化/反序列化 相关接口 109 | 110 | `Time` 实现了 `encoding` 包中的 `BinaryMarshaler`、`BinaryUnmarshaler`、`TextMarshaler` 和 `TextUnmarshaler` 接口;`encoding/json` 包中的 `Marshaler` 和 `Unmarshaler` 接口。 111 | 112 | 它还实现了 `gob` 包中的 `GobEncoder` 和 `GobDecoder` 接口。 113 | 114 | 对于文本序列化/反序列化,通过 `Parse` 和 `Format` 实现;对于二进制序列化,需要单独实现。 115 | 116 | 对于 json,使用的是 `time.RFC3339Nano` 这种格式。通常程序中不使用这种格式。解决办法是定义自己的类型。如: 117 | 118 | ``` 119 | type OftenTime time.Time 120 | 121 | func (self OftenTime) MarshalJSON() ([]byte, error) { 122 | t := time.Time(self) 123 | if y := t.Year(); y < 0 || y >= 10000 { 124 | return nil, errors.New("Time.MarshalJSON: year outside of range [0,9999]") 125 | } 126 | // 注意 `"2006-01-02 15:04:05"`。因为是 JSON,双引号不能少 127 | return []byte(t.Format(`"2006-01-02 15:04:05"`)), nil 128 | } 129 | ``` 130 | 131 | ### Round 和 Truncate 方法 132 | 133 | 比如,有这么个需求:获取当前时间整点的 `Time` 实例。例如,当前时间是 15:54:23,需要的是 15:00:00。我们可以这么做: 134 | 135 | ``` 136 | t, _ := time.ParseInLocation("2006-01-02 15:04:05", time.Now().Format("2006-01-02 15:00:00"), time.Local) 137 | fmt.Println(t) 138 | ``` 139 | 实际上,`time` 包给我们提供了专门的方法,功能更强大,性能也更好,这就是 `Round` 和 `Trunate`,它们区别,一个是取最接近的,一个是向下取整。 140 | 141 | 使用示例: 142 | 143 | ``` 144 | t, _ := time.ParseInLocation("2006-01-02 15:04:05", "2016-06-13 15:34:39", time.Local) 145 | // 整点(向下取整) 146 | fmt.Println(t.Truncate(1 * time.Hour)) 147 | // 整点(最接近) 148 | fmt.Println(t.Round(1 * time.Hour)) 149 | 150 | // 整分(向下取整) 151 | fmt.Println(t.Truncate(1 * time.Minute)) 152 | // 整分(最接近) 153 | fmt.Println(t.Round(1 * time.Minute)) 154 | 155 | t2, _ := time.ParseInLocation("2006-01-02 15:04:05", t.Format("2006-01-02 15:00:00"), time.Local) 156 | fmt.Println(t2) 157 | ``` 158 | 159 | # 导航 # 160 | 161 | - 上一节:[时区](04.2.md) 162 | - 下一节:[定时器](04.4.md) 163 | -------------------------------------------------------------------------------- /chapter04/04.4.md: -------------------------------------------------------------------------------- 1 | # 4.4 定时器 # 2 | 3 | 定时器是进程规划自己在未来某一时刻接获通知的一种机制。本节介绍两种定时器:`Timer`(到达指定时间触发且只触发一次)和 `Ticker`(间隔特定时间触发)。 4 | 5 | ## Timer 6 | 7 | ### 内部实现源码分析 8 | 9 | `Timer` 类型代表单次时间事件。当 `Timer` 到期时,当时的时间会被发送给 C (channel),除非 `Timer` 是被 `AfterFunc` 函数创建的。 10 | 11 | 注意:`Timer` 的实例必须通过 `NewTimer` 或 `AfterFunc` 获得。 12 | 13 | 类型定义如下: 14 | 15 | ``` 16 | type Timer struct { 17 | C <-chan Time // The channel on which the time is delivered. 18 | r runtimeTimer 19 | } 20 | ``` 21 | C 已经解释了,我们看看 `runtimeTimer`。它定义在 sleep.go 文件中,必须和 `runtime` 包中 `time.go` 文件中的 `timer` 必须保持一致: 22 | 23 | ``` 24 | type timer struct { 25 | i int // heap index 26 | 27 | // Timer wakes up at when, and then at when+period, ... (period > 0 only) 28 | // each time calling f(now, arg) in the timer goroutine, so f must be 29 | // a well-behaved function and not block. 30 | when int64 31 | period int64 32 | f func(interface{}, uintptr) 33 | arg interface{} 34 | seq uintptr 35 | } 36 | ``` 37 | 我们通过 `NewTimer()` 来看这些字段都怎么赋值,是什么用途。 38 | 39 | ``` 40 | // NewTimer creates a new Timer that will send 41 | // the current time on its channel after at least duration d. 42 | func NewTimer(d Duration) *Timer { 43 | c := make(chan Time, 1) 44 | t := &Timer{ 45 | C: c, 46 | r: runtimeTimer{ 47 | when: when(d), 48 | f: sendTime, 49 | arg: c, 50 | }, 51 | } 52 | startTimer(&t.r) 53 | return t 54 | } 55 | ``` 56 | 在 `when` 表示的时间到时,会往 Timer.C 中发送当前时间。`when` 表示的时间是纳秒时间,正常通过 `runtimeNano() + int64(d)` 赋值。跟上一节中讲到的 `now()` 类似,`runtimeNano()` 也在 `runtime` 中实现(`runtime·nanotime`): 57 | 58 | * 调用系统调用 `clock_gettime` 获取时钟值(这是 POSIX 时钟)。其中 clockid_t 时钟类型是 CLOCK_MONOTONIC,也就是不可设定的恒定态时钟。具体的是什么时间,SUSv3 规定始于未予规范的过去某一点,Linux 上,始于系统启动。 59 | * 如果 `clock_gettime` 不存在,则使用精度差些的系统调用 `gettimeofday`。 60 | 61 | `f` 参数的值是 `sendTime`,定时器时间到时,会调用 f,并将 `arg` 和 `seq` 传给 `f`。 62 | 63 | 因为 `Timer` 是一次性的,所以 `period` 保留默认值 0。 64 | 65 | 定时器的具体实现逻辑,都在 `runtime` 中的 `time.go` 中,它的实现,没有采用经典 Unix 间隔定时器 `setitimer` 系统调用,也没有 采用 POSIX 间隔式定时器(相关系统调用:`timer_create`、`timer_settime` 和 `timer_delete`),而是通过四叉树堆(heep)实现的(`runtimeTimer` 结构中的 `i` 字段,表示在堆中的索引)。通过构建一个最小堆,保证最快拿到到期了的定时器执行。定时器的执行,在专门的 `goroutine` 中进行的:`go timerproc()`。有兴趣的同学,可以阅读 `runtime/time.go` 的源码。 66 | 67 | ### Timer 相关函数或方法的使用 68 | 69 | **通过 `time.After` 模拟超时:** 70 | 71 | ``` 72 | c := make(chan int) 73 | 74 | go func() { 75 | // time.Sleep(1 * time.Second) 76 | time.Sleep(3 * time.Second) 77 | <-c 78 | }() 79 | 80 | select { 81 | case c <- 1: 82 | fmt.Println("channel...") 83 | case <-time.After(2 * time.Second): 84 | close(c) 85 | fmt.Println("timeout...") 86 | } 87 | ``` 88 | 89 | **`time.Stop` 停止定时器 或 `time.Reset` 重置定时器** 90 | 91 | ``` 92 | start := time.Now() 93 | timer := time.AfterFunc(2*time.Second, func() { 94 | fmt.Println("after func callback, elaspe:", time.Now().Sub(start)) 95 | }) 96 | 97 | time.Sleep(1 * time.Second) 98 | // time.Sleep(3*time.Second) 99 | // Reset 在 Timer 还未触发时返回 true;触发了或Stop了,返回false 100 | if timer.Reset(3 * time.Second) { 101 | fmt.Println("timer has not trigger!") 102 | } else { 103 | fmt.Println("timer had expired or stop!") 104 | } 105 | 106 | time.Sleep(10 * time.Second) 107 | 108 | // output: 109 | // timer has not trigger! 110 | // after func callback, elaspe: 4.00026461s 111 | ``` 112 | 如果定时器还未触发,`Stop` 会将其移除,并返回 true;否则返回 false;后续再对该 `Timer` 调用 `Stop`,直接返回 false。 113 | 114 | `Reset` 会先调用 `stopTimer` 再调用 `startTimer`,类似于废弃之前的定时器,重新启动一个定时器。返回值和 `Stop` 一样。 115 | 116 | ### Sleep 的内部实现 117 | 118 | 查看 `runtime/time.go` 文件中的 `timeSleep` 可知,`Sleep` 的是通过 `Timer` 实现的,把当前 goroutine 作为 `arg` 参数(`getg()`)。 119 | 120 | ## Ticker 相关函数或方法的使用 121 | 122 | `Ticker` 和 `Timer` 类似,区别是:`Ticker` 中的`runtimeTimer` 字段的 `period` 字段会赋值为 `NewTicker(d Duration)` 中的 `d`,表示每间隔 `d` 纳秒,定时器就会触发一次。 123 | 124 | 除非程序终止前定时器一直需要触发,否则,不需要时应该调用 `Ticker.Stop` 来释放相关资源。 125 | 126 | 如果程序终止前需要定时器一直触发,可以使用更简单方便的 `time.Tick` 函数,因为 `Ticker` 实例隐藏起来了,因此,该函数启动的定时器无法停止。 127 | 128 | ## 定时器的实际应用 129 | 130 | 在实际开发中,定时器用的较多的会是 `Timer`,如模拟超时,而需要类似 `Tiker` 的功能时,可以使用实现了 `cron spec` 的库 [cron](https://github.com/robfig/cron),感兴趣的可以参考文章:[《Go语言版crontab》](http://blog.studygolang.com/2014/02/go_crontab/)。 131 | 132 | # 导航 # 133 | 134 | - 上一节:[Time类型详解](04.3.md) 135 | - 下一节:[Unix 时间相关系统调用](04.5.md) 136 | -------------------------------------------------------------------------------- /chapter05/05.1.md: -------------------------------------------------------------------------------- 1 | # 5.1 math — 基本数学函数 # 2 | 3 | math包实现的就是数学函数计算。 4 | 5 | ## 5.1.1 三角函数 ## 6 | 7 | 正弦函数,反正弦函数,双曲正弦,反双曲正弦 8 | 9 | - func Sin(x float64) float64 10 | - func Asin(x float64) float64 11 | - func Sinh(x float64) float64 12 | - func Asinh(x float64) float64 13 | 14 | 一次性返回sin,cos 15 | 16 | - func Sincos(x float64) (sin, cos float64) 17 | 18 | 余弦函数,反余弦函数,双曲余弦,反双曲余弦 19 | 20 | - func Cos(x float64) float64 21 | - func Acos(x float64) float64 22 | - func Cosh(x float64) float64 23 | - func Acosh(x float64) float64 24 | 25 | 正切函数,反正切函数,双曲正切,反双曲正切 26 | 27 | - func Tan(x float64) float64 28 | - func Atan(x float64) float64 和 func Atan2(y, x float64) float64 29 | - func Tanh(x float64) float64 30 | - func Atanh(x float64) float64 31 | 32 | ## 5.1.2 幂次函数 ## 33 | 34 | - func Cbrt(x float64) float64 //立方根函数 35 | - func Pow(x, y float64) float64 // x的幂函数 36 | - func Pow10(e int) float64 // 10根的幂函数 37 | - func Sqrt(x float64) float64 // 平方根 38 | - func Log(x float64) float64 // 对数函数 39 | - func Log10(x float64) float64 // 10为底的对数函数 40 | - func Log2(x float64) float64 // 2为底的对数函数 41 | - func Log1p(x float64) float64 // log(1 + x) 42 | - func Logb(x float64) float64 // 相当于log2(x)的绝对值 43 | - func Ilogb(x float64) int // 相当于log2(x)的绝对值的整数部分 44 | - func Exp(x float64) float64 // 指数函数 45 | - func Exp2(x float64) float64 // 2为底的指数函数 46 | - func Expm1(x float64) float64 // Exp(x) - 1 47 | 48 | ## 5.1.3 特殊函数 ## 49 | - func Inf(sign int) float64 // 正无穷 50 | - func IsInf(f float64, sign int) bool // 是否正无穷 51 | - func NaN() float64 // 无穷值 52 | - func IsNaN(f float64) (is bool) // 是否是无穷值 53 | - func Hypot(p, q float64) float64 // 计算直角三角形的斜边长 54 | 55 | ## 5.1.4 类型转化函数 ## 56 | - func Float32bits(f float32) uint32 // float32和unit32的转换 57 | - func Float32frombits(b uint32) float32 // uint32和float32的转换 58 | - func Float64bits(f float64) uint64 // float64和uint64的转换 59 | - func Float64frombits(b uint64) float64 // uint64和float64的转换 60 | 61 | ## 5.1.5 其他函数 ## 62 | 63 | - func Abs(x float64) float64 // 绝对值函数 64 | - func Ceil(x float64) float64 // 向上取整 65 | - func Floor(x float64) float64 // 向下取整 66 | - func Mod(x, y float64) float64 // 取模 67 | - func Modf(f float64) (int float64, frac float64) // 分解f,以得到f的整数和小数部分 68 | - func Frexp(f float64) (frac float64, exp int) // 分解f,得到f的位数和指数 69 | - func Max(x, y float64) float64 // 取大值 70 | - func Min(x, y float64) float64 // 取小值 71 | - func Dim(x, y float64) float64 // 复数的维数 72 | - func J0(x float64) float64 // 0阶贝塞尔函数 73 | - func J1(x float64) float64 // 1阶贝塞尔函数 74 | - func Jn(n int, x float64) float64 // n阶贝塞尔函数 75 | - func Y0(x float64) float64 // 第二类贝塞尔函数0阶 76 | - func Y1(x float64) float64 // 第二类贝塞尔函数1阶 77 | - func Yn(n int, x float64) float64 // 第二类贝塞尔函数n阶 78 | - func Erf(x float64) float64 // 误差函数 79 | - func Erfc(x float64) float64 // 余补误差函数 80 | - func Copysign(x, y float64) float64 // 以y的符号返回x值 81 | - func Signbit(x float64) bool // 获取x的符号 82 | - func Gamma(x float64) float64 // 伽玛函数 83 | - func Lgamma(x float64) (lgamma float64, sign int) // 伽玛函数的自然对数 84 | - func Ldexp(frac float64, exp int) float64 // value乘以2的exp次幂 85 | - func Nextafter(x, y float64) (r float64) //返回参数x在参数y方向上可以表示的最接近的数值,若x等于y,则返回x 86 | - func Nextafter32(x, y float32) (r float32) //返回参数x在参数y方向上可以表示的最接近的数值,若x等于y,则返回x 87 | - func Remainder(x, y float64) float64 // 取余运算 88 | - func Trunc(x float64) float64 // 截取函数 89 | 90 | # 导航 # 91 | -------------------------------------------------------------------------------- /chapter06/06.0.md: -------------------------------------------------------------------------------- 1 | # 第六章 文件系统 # 2 | 3 | Go 的标准库提供了很多工具,可以处理文件系统中的文件、构造和解析文件名等。 4 | 5 | 处理文件的第一步是确定要处理的文件的名字。Go 将文件名表示为简单的字符串,提供了 `path`、`filepath` 等库来操作文件名或路径。用 `os` 中 `File` 结构的 `Readdir` 可以列出一个目录中的内容。 6 | 7 | 可以用 `os.Stat` 或 `os.Lstat` 来检查文件的一些特性,如权限、大小等。 8 | 9 | 有时需要创建草稿文件来保存临时数据,或将数据移动到一个永久位置之前需要临时文件存储,`os.TempDir` 可以返回默认的临时目录,用于存放临时文件。关于临时文件,在 `ioutil` 中已经讲解了。 10 | 11 | `os` 包还包含了很多其他文件系统相关的操作,比如创建目录、重命名、移动文件等等。 12 | 13 | 由于本章探讨文件系统相关知识,`os` 包中关于进程相关的知识会在后续章节讲解。 14 | 15 | # 导航 # 16 | 17 | - [第五章](/chapter05/05.0.md) 18 | - 下一节:[os — 平台无关的操作系统功能实现](06.1.md) 19 | -------------------------------------------------------------------------------- /chapter06/06.1.md: -------------------------------------------------------------------------------- 1 | # 6.1 os — 平台无关的操作系统功能实现 # 2 | 3 | `os` 包提供了平台无关的操作系统功能接口。尽管错误处理是 go 风格的,但设计是 Unix 风格的;所以,失败的调用会返回 `error` 而非错误码。通常 `error` 里会包含更多信息。例如,如果使用一个文件名的调用(如Open、Stat)失败了,打印错误时会包含该文件名,错误类型将为`*PathError`,其内部可以解包获得更多信息。 4 | 5 | os包的接口规定实现为在所有操作系统中都是一致的。有一些某个系统特定的功能,需要使用 `syscall` 获取。实际上,`os` 依赖于 `syscall`。在实际编程中,我们应该总是优先使用 `os` 中提供的功能,而不是 `syscall`。 6 | 7 | 下面是一个简单的例子,打开一个文件并从中读取一些数据: 8 | 9 | ``` 10 | file, err := os.Open("file.go") // For read access. 11 | if err != nil { 12 | log.Fatal(err) 13 | } 14 | ``` 15 | 如果打开失败,错误字符串是自解释的,例如: 16 | 17 | `open file.go: no such file or directory` 18 | 19 | 而不像 C 语言,需要额外的函数(或宏)来解释错误码。 20 | 21 | ## 文件 I/O 22 | 23 | 在第一章,我们较全面的介绍了 Go 中的 I/O。本节,我们着重介绍文件相关的 I/O。因为 I/O 操作涉及到系统调用,在讲解时会涉及到 Unix 在这方面的系统调用。 24 | 25 | 在 Unix 系统调用中,所有执行 I/O 操作以文件描述符,一个非负整数(通常是小整数),来指代打开的文件。文件描述符用以表示所有类型的已打开文件,包括管道(pipe)、FIFO、socket、终端、设备和普通文件。这里,我们主要介绍普通文件的 I/O。 26 | 27 | 在 Go 中,文件描述符封装在 `os.File` 结构中,通过 `File.Fd()` 可以获得底层的文件描述符:fd。 28 | 29 | 按照惯例,大多数程序都期望能够使用 3 种标准的文件描述符:0-标准输入;1-标准输出;2-标准错误。`os` 包提供了 3 个 `File` 对象,分别代表这 3 种标准描述符:`Stdin`、`Stdout` 和 `Stderr`,它们对应的文件名分别是:/dev/stdin、/dev/stdout 和 /dev/stderr。注意,这里说的文件名,并不说一定存在的文件名,比如 Windows 下就没有。 30 | 31 | ### 打开一个文件:OpenFile 32 | 33 | `OpenFile` 既能打开一个已经存在的文件,也能创建并打开一个新文件。 34 | 35 | `func OpenFile(name string, flag int, perm FileMode) (*File, error)` 36 | 37 | `OpenFile` 是一个更一般性的文件打开函数,大多数调用者都应用 `Open` 或 `Create` 代替本函数。它会使用指定的选项(如O_RDONLY等)、指定的模式(如0666等)打开指定名称的文件。如果操作成功,返回的文件对象可用于 I/O。如果出错,错误底层类型是 `*PathError`。 38 | 39 | 要打开的文件由参数 `name` 指定,它可以是绝对路径或相对路径(相对于进程当前工作目录),也可以是一个符号链接(会对其进行解引用)。 40 | 41 | 位掩码参数 `flag` 用于指定文件的访问模式,可用的值在 `os` 中定义为常量(以下值并非所有操作系统都可用): 42 | 43 | ``` 44 | const ( 45 | O_RDONLY int = syscall.O_RDONLY // 只读模式打开文件 46 | O_WRONLY int = syscall.O_WRONLY // 只写模式打开文件 47 | O_RDWR int = syscall.O_RDWR // 读写模式打开文件 48 | O_APPEND int = syscall.O_APPEND // 写操作时将数据附加到文件尾部 49 | O_CREATE int = syscall.O_CREAT // 如果不存在将创建一个新文件 50 | O_EXCL int = syscall.O_EXCL // 和O_CREATE配合使用,文件必须不存在 51 | O_SYNC int = syscall.O_SYNC // 打开文件用于同步I/O 52 | O_TRUNC int = syscall.O_TRUNC // 如果可能,打开时清空文件 53 | ) 54 | ``` 55 | 其中,`O_RDONLY`、`O_WRONLY`、`O_RDWR` 应该只指定一个,剩下的通过 `|` 操作符来指定。该函数内部会给 `flags` 加上 `syscall.O_CLOEXEC`,在 fork 子进程时会关闭通过 `OpenFile` 打开的文件,即子进程不会重用该文件描述符。 56 | 57 | *注意:由于历史原因,`O_RDONLY | O_WRONLY` 并非等于 `O_RDWR`,它们的值一般是 0、1、2。* 58 | 59 | 位掩码参数 `perm` 指定了文件的模式和权限位,类型是 `os.FileMode`,文件模式位常量定义在 `os` 中: 60 | 61 | ``` 62 | const ( 63 | // 单字符是被 String 方法用于格式化的属性缩写。 64 | ModeDir FileMode = 1 << (32 - 1 - iota) // d: 目录 65 | ModeAppend // a: 只能写入,且只能写入到末尾 66 | ModeExclusive // l: 用于执行 67 | ModeTemporary // T: 临时文件(非备份文件) 68 | ModeSymlink // L: 符号链接(不是快捷方式文件) 69 | ModeDevice // D: 设备 70 | ModeNamedPipe // p: 命名管道(FIFO) 71 | ModeSocket // S: Unix域socket 72 | ModeSetuid // u: 表示文件具有其创建者用户id权限 73 | ModeSetgid // g: 表示文件具有其创建者组id的权限 74 | ModeCharDevice // c: 字符设备,需已设置ModeDevice 75 | ModeSticky // t: 只有root/创建者能删除/移动文件 76 | 77 | // 覆盖所有类型位(用于通过&获取类型位),对普通文件,所有这些位都不应被设置 78 | ModeType = ModeDir | ModeSymlink | ModeNamedPipe | ModeSocket | ModeDevice 79 | ModePerm FileMode = 0777 // 覆盖所有Unix权限位(用于通过&获取类型位) 80 | ) 81 | ``` 82 | 以上常量在所有操作系统都有相同的含义(可用时),因此文件的信息可以在不同的操作系统之间安全的移植。不是所有的位都能用于所有的系统,唯一共有的是用于表示目录的 `ModeDir` 位。 83 | 84 | 以上这些被定义的位是 `FileMode` 最重要的位。另外 9 个位(权限位)为标准 Unix rwxrwxrwx 权限(所有人都可读、写、运行)。 85 | 86 | `FileMode` 还定义了几个方法,用于判断文件类型的 `IsDir()` 和 `IsRegular()`,用于获取权限的 `Perm()`。 87 | 88 | 返回的 `error`,具体实现是 `*os.PathError`,它会记录具体操作、文件路径和错误原因。 89 | 90 | 另外,在 `OpenFile` 内部会调用 `NewFile`,来得到 `File` 对象。 91 | 92 | **使用方法** 93 | 94 | 打开一个文件,一般通过 `Open` 或 `Create`,我们看这两个函数的实现。 95 | 96 | ``` 97 | func Open(name string) (*File, error) { 98 | return OpenFile(name, O_RDONLY, 0) 99 | } 100 | 101 | func Create(name string) (*File, error) { 102 | return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666) 103 | } 104 | ``` 105 | 106 | ### 读取文件内容:Read 107 | 108 | `func (f *File) Read(b []byte) (n int, err error)` 109 | 110 | `Read` 方法从 `f` 中读取最多 `len(b)` 字节数据并写入 `b`。它返回读取的字节数和可能遇到的任何错误。文件终止标志是读取0个字节且返回值 err 为 `io.EOF`。 111 | 112 | 从方法声明可以知道,`File` 实现了 `io.Reader` 接口。 113 | 114 | `Read` 对应的系统调用是 `read`。 115 | 116 | 对比下 `ReadAt` 方法: 117 | 118 | `func (f *File) ReadAt(b []byte, off int64) (n int, err error)` 119 | 120 | `ReadAt` 从指定的位置(相对于文件开始位置)读取长度为 `len(b)` 个字节数据并写入 `b`。它返回读取的字节数和可能遇到的任何错误。当 n0,`Readdirnames` 函数会返回一个最多 n 个成员的切片。这时,如果 `Readdirnames` 返回一个空切片,它会返回一个非 nil 的错误说明原因。如果到达了目录 `f` 的结尾,返回值 err 会是 `io.EOF`。 471 | 472 | 如果 n<=0,`Readdirnames` 函数返回目录中剩余所有文件对象的名字构成的切片。此时,如果 `Readdirnames` 调用成功(读取所有内容直到结尾),它会返回该切片和 nil 的错误值。如果在到达结尾前遇到错误,会返回之前成功读取的名字构成的切片和该错误。 473 | 474 | `func (f *File) Readdir(n int) (fi []FileInfo, err error)` 475 | 476 | `Readdir` 内部会调用 `Readdirnames`,将得到的 `names` 构造路径,通过 `Lstat` 构造出 `[]FileInfo`。 477 | 478 | 列出某个目录的文件列表示例程序见 [dirtree](/code/src/chapter06/os/dirtree/main.go)。 479 | 480 | # 导航 # 481 | 482 | - [第六章](/chapter06/06.0.md) 483 | - 下一节:[path — 操作路径](06.2.md) 484 | -------------------------------------------------------------------------------- /chapter06/06.2.md: -------------------------------------------------------------------------------- 1 | # 6.2 path/filepath — 兼容操作系统的文件路径操作 # 2 | 3 | `path/filepath` 包涉及到路径操作时,路径分隔符使用 `os.PathSeparator`。不同系统,路径表示方式有所不同,比如 Unix 和 Windows 差别很大。本包能够处理所有的文件路径,不管是什么系统。 4 | 5 | 注意,路径操作函数并不会校验路径是否真实存在。 6 | 7 | ## 解析路径名字符串 8 | 9 | `Dir()` 和 `Base()` 函数将一个路径名字符串分解成目录和文件名两部分。(注意一般情况,这些函数与 Unix 中 dirname 和 basename 命令类似,但如果路径以 `/` 结尾,`Dir` 的行为和 `dirname` 不太一致。) 10 | 11 | ``` 12 | func Dir(path string) string 13 | func Base(path string) string 14 | ``` 15 | `Dir` 返回路径中除去最后一个路径元素的部分,即该路径最后一个元素所在的目录。在使用 `Split` 去掉最后一个元素后,会简化路径并去掉末尾的斜杠。如果路径是空字符串,会返回".";如果路径由1到多个斜杠后跟0到多个非斜杠字符组成,会返回"/";其他任何情况下都不会返回以斜杠结尾的路径。 16 | 17 | `Base` 函数返回路径的最后一个元素。在提取元素前会去掉末尾的斜杠。如果路径是"",会返回".";如果路径是只有一个斜杆构成的,会返回"/"。 18 | 19 | 比如,给定路径名 `/home/polaris/studygolang.go`,`Dir` 返回 `/home/polaris`,而 `Base` 返回 `studygolang.go`。 20 | 21 | 如果给定路径名 `/home/polaris/studygolang/`,`Dir` 返回 `/home/polaris/studygolang`(这与 Unix 中的 dirname 不一致,dirname 会返回 /home/polaris),而 `Base` 返回 `studygolang`。 22 | 23 | 有人提出此问题,见[issue13199](https://github.com/golang/go/issues/13199),不过官方认为这不是问题,如果需要和 `dirname` 一样的功能,应该自己处理,比如在调用 `Dir` 之前,先将末尾的 `/` 去掉。 24 | 25 | 此外,`Ext` 可以获得路径中文件名的扩展名。 26 | 27 | `func Ext(path string) string` 28 | 29 | `Ext` 函数返回 `path` 文件扩展名。扩展名是路径中最后一个从 `.` 开始的部分,包括 `.`。如果该元素没有 `.` 会返回空字符串。 30 | 31 | ## 相对路径和绝对路径 32 | 33 | 某个进程都会有当前工作目录(进程相关章节会详细介绍),一般的相对路径,就是针对进程当前工作目录而言的。当然,可以针对某个目录指定相对路径。 34 | 35 | 绝对路径,在 Unix 中,以 `/` 开始;在 Windows 下以某个盘符开始,比如 `C:\Program Files`。 36 | 37 | `func IsAbs(path string) bool` 38 | 39 | `IsAbs` 返回路径是否是一个绝对路径。而 40 | 41 | `func Abs(path string) (string, error)` 42 | 43 | `Abs` 函数返回 `path` 代表的绝对路径,如果 `path` 不是绝对路径,会加入当前工作目录以使之成为绝对路径。因为硬链接的存在,不能保证返回的绝对路径是唯一指向该地址的绝对路径。在 `os.Getwd` 出错时,`Abs` 会返回该错误,一般不会出错,如果路径名长度超过系统限制,则会报错。 44 | 45 | `func Rel(basepath, targpath string) (string, error)` 46 | 47 | `Rel` 函数返回一个相对路径,将 `basepath` 和该路径用路径分隔符连起来的新路径在词法上等价于 `targpath`。也就是说,`Join(basepath, Rel(basepath, targpath))` 等价于 `targpath`。如果成功执行,返回值总是相对于 `basepath` 的,即使`basepath` 和 `targpath` 没有共享的路径元素。如果两个参数一个是相对路径而另一个是绝对路径,或者 `targpath` 无法表示为相对于`basepath` 的路径,将返回错误。 48 | 49 | ``` 50 | fmt.Println(filepath.Rel("/home/polaris/studygolang", "/home/polaris/studygolang/src/logic/topic.go")) 51 | fmt.Println(filepath.Rel("/home/polaris/studygolang", "/data/studygolang")) 52 | 53 | // Output: 54 | // src/logic/topic.go 55 | // ../../../data/studygolang 56 | ``` 57 | 58 | ## 路径的切分和拼接 59 | 60 | 对于一个常规文件路径,我们可以通过 `Split` 函数得到它的目录路径和文件名: 61 | 62 | `func Split(path string) (dir, file string)` 63 | 64 | `Split` 函数根据最后一个路径分隔符将路径 `path` 分隔为目录和文件名两部分(`dir` 和 `file`)。如果路径中没有路径分隔符,函数返回值 `dir` 为空字符串,`file` 等于 `path`;反之,如果路径中最后一个字符是 `/`,则 `dir` 等于 `path`,`file` 为空字符串。返回值满足 `path == dir+file`。`dir` 非空时,最后一个字符总是 `/`。 65 | 66 | ``` 67 | // dir == /home/polaris/,file == studygolang 68 | filepath.Split("/home/polaris/studygolang") 69 | 70 | // dir == /home/polaris/studygolang/,file == "" 71 | filepath.Split("/home/polaris/studygolang/") 72 | 73 | // dir == "",file == studygolang 74 | filepath.Split("studygolang") 75 | ``` 76 | 相对路径到绝对路径的转变,需要经过路径的拼接。`Join` 用于将多个路径拼接起来,会根据情况添加路径分隔符。 77 | 78 | `func Join(elem ...string) string` 79 | 80 | `Join` 函数可以将任意数量的路径元素放入一个单一路径里,会根据需要添加路径分隔符。结果是经过 `Clean` 的,所有的空字符串元素会被忽略。对于拼接路径的需求,我们应该总是使用 `Join` 函数来处理。 81 | 82 | 有时,我们需要分割 `PATH` 或 `GOPATH` 之类的环境变量(这些路径被特定于`OS` 的列表分隔符连接起来),`filepath.SplitList` 就是这个用途: 83 | 84 | `func SplitList(path string) []string` 85 | 86 | 注意,与 `strings.Split` 函数的不同之处是:对 "",SplitList返回[]string{},而 `strings.Split` 返回 []string{""}。`SplitList` 内部调用的是 `strings.Split`。 87 | 88 | ## 规整化路径 89 | 90 | `func Clean(path string) string` 91 | 92 | `Clean` 函数通过单纯的词法操作返回和 `path` 代表同一地址的最短路径。 93 | 94 | 它会不断的依次应用如下的规则,直到不能再进行任何处理: 95 | 96 | 1. 将连续的多个路径分隔符替换为单个路径分隔符 97 | 2. 剔除每一个 `.` 路径名元素(代表当前目录) 98 | 3. 剔除每一个路径内的 `..` 路径名元素(代表父目录)和它前面的非 `..` 路径名元素 99 | 4. 剔除开始于根路径的 `..` 路径名元素,即将路径开始处的 `/..` 替换为 `/`(假设路径分隔符是 `/`) 100 | 101 | 返回的路径只有其代表一个根地址时才以路径分隔符结尾,如 Unix的 `/` 或Windows的 `C:\`。 102 | 103 | 如果处理的结果是空字符串,Clean会返回 `.`,代表当前路径。 104 | 105 | ## 符号链接指向的路径名 106 | 107 | 在上一节 `os` 包中介绍了 `Readlink`,可以读取符号链接指向的路径名。不过,如果原路径中又包含符号链接,`Readlink` 却不会解析出来。`filepath.EvalSymlinks` 会将所有路径的符号链接都解析出来。除此之外,它返回的路径,是直接可访问的。 108 | 109 | `func EvalSymlinks(path string) (string, error)` 110 | 111 | 如果 `path` 或返回值是相对路径,则是相对于进程当前工作目录。 112 | 113 | `os.Readlink` 和 `filepath.EvalSymlinks` 区别示例程序: 114 | 115 | ``` 116 | // 在当前目录下创建一个 studygolang.txt 文件和一个 symlink 目录,在 symlink 目录下对 studygolang.txt 建一个符号链接 studygolang.txt.2 117 | fmt.Println(filepath.EvalSymlinks("symlink/studygolang.txt.2")) 118 | fmt.Println(os.Readlink("symlink/studygolang.txt.2")) 119 | 120 | // Ouput: 121 | // studygolang.txt 122 | // ../studygolang.txt 123 | ``` 124 | 125 | ## 文件路径匹配 126 | 127 | `func Match(pattern, name string) (matched bool, err error)` 128 | 129 | `Match` 指示 `name` 是否和 shell 的文件模式匹配。模式语法如下: 130 | 131 | ``` 132 | pattern: 133 | { term } 134 | term: 135 | '*' 匹配0或多个非路径分隔符的字符 136 | '?' 匹配1个非路径分隔符的字符 137 | '[' [ '^' ] { character-range } ']' 138 | 字符组(必须非空) 139 | c 匹配字符c(c != '*', '?', '\\', '[') 140 | '\\' c 匹配字符c 141 | character-range: 142 | c 匹配字符c(c != '\\', '-', ']') 143 | '\\' c 匹配字符c 144 | lo '-' hi 匹配区间[lo, hi]内的字符 145 | ``` 146 | 匹配要求 `pattern` 必须和 `name` 全匹配上,不只是子串。在 Windows 下转义字符被禁用。 147 | 148 | `Match` 函数很少使用,搜索了一遍,标准库没有用到这个函数。而 `Glob` 函数在模板标准库中被用到了。 149 | 150 | `func Glob(pattern string) (matches []string, err error)` 151 | 152 | `Glob` 函数返回所有匹配了 模式字符串 `pattern` 的文件列表或者nil(如果没有匹配的文件)。`pattern` 的语法和 `Match` 函数相同。`pattern` 可以描述多层的名字,如 `/usr/*/bin/ed`(假设路径分隔符是 `/`)。 153 | 154 | 注意,`Glob` 会忽略任何文件系统相关的错误,如读目录引发的 I/O 错误。唯一的错误和 `Match` 一样,在 `pattern` 不合法时,返回 `filepath.ErrBadPattern`。返回的结果是根据文件名字典顺序进行了排序的。 155 | 156 | `Glob` 的常见用法,是读取某个目录下所有的文件,比如写单元测试时,读取 `testdata` 目录下所有测试数据: 157 | 158 | `filepath.Glob("testdata/*.input")` 159 | 160 | ## 遍历目录 161 | 162 | 在介绍 `os` 时,讲解了读取目录的方法,并给出了一个遍历目录的示例。在 `filepath` 中,提供了 `Walk` 函数,用于遍历目录树。 163 | 164 | `func Walk(root string, walkFn WalkFunc) error` 165 | 166 | `Walk` 函数会遍历 `root` 指定的目录下的文件树,对每一个该文件树中的目录和文件都会调用 `walkFn`,包括 `root` 自身。所有访问文件/目录时遇到的错误都会传递给 `walkFn` 过滤。文件是按字典顺序遍历的,这让输出更漂亮,但也导致处理非常大的目录时效率会降低。`Walk` 函数不会遍历文件树中的符号链接(快捷方式)文件包含的路径。 167 | 168 | `walkFn` 的类型 `WalkFunc` 的定义如下: 169 | 170 | `type WalkFunc func(path string, info os.FileInfo, err error) error` 171 | 172 | `Walk` 函数对每一个文件/目录都会调用 `WalkFunc` 函数类型值。调用时 `path` 参数会包含 `Walk` 的 `root` 参数作为前缀;就是说,如果 `Walk` 函数的 `root` 为 "dir",该目录下有文件 "a",将会使用 "dir/a" 作为调用 `walkFn` 的参数。`walkFn` 参数被调用时的 `info` 参数是 `path` 指定的地址(文件/目录)的文件信息,类型为 `os.FileInfo`。 173 | 174 | 如果遍历 `path` 指定的文件或目录时出现了问题,传入的参数 `err` 会描述该问题,`WalkFunc` 类型函数可以决定如何去处理该错误(`Walk` 函数将不会深入该目录);如果该函数返回一个错误,`Walk` 函数的执行会中止;只有一个例外,如果 `Walk` 的 `walkFn` 返回值是 `SkipDir`,将会跳过该目录的内容而 `Walk` 函数照常执行处理下一个文件。 175 | 176 | 和 `os` 遍历目录树的示例对应,使用 `Walk` 遍历目录树的示例程序在 [walk](/code/src/chapter06/filepath/walk/main.go),程序简单很多。 177 | 178 | ## Windows 起作用的函数 179 | 180 | `filepath` 中有三个函数:`VolumeName`、`FromSlash` 和 `ToSlash`,针对非 Unix 平台的。 181 | 182 | ## 关于 path 包 183 | 184 | `path` 包提供了对 `/` 分隔的路径的实用操作函数。 185 | 186 | 在 Unix 中,路径的分隔符是 `/`,但 Windows 是 `\`。在使用 `path` 包时,应该总是使用 `/`,不论什么系统。 187 | 188 | `path` 包中提供的函数,`filepath` 都有提供,功能类似,但实现不同。 189 | 190 | 一般应该总是使用 `filepath` 包,而不是 `path` 包。 191 | 192 | # 导航 # 193 | 194 | - 下一节:[os — 平台无关的操作系统功能实现](06.1.md) 195 | - 第七章:[数据持久存储与交换](/chapter07/07.0.md) 196 | -------------------------------------------------------------------------------- /chapter07/07.0.md: -------------------------------------------------------------------------------- 1 | # 第七章 数据持久存储与交换 # 2 | 3 | 现代程序离不开数据存储,现阶段很热的所谓大数据处理、云盘等,更是以存储为依托。有数据存储,自然需要进行数据交换,已达到数据共享等目的。 4 | 5 | 关系型数据库发展了很长一段时间,SQL/SQL-like 已经很成熟,使用也很广泛,Go 语言标准库提供了对 SQL/SQL-like 数据库的操作的标准接口,即 [database/sql](http://docs.studygolang.com/pkg/database/sql) 包。 6 | 7 | 在数据交换方面,有很多成熟的协议可以使用,常用的有:JSON、XML等,似乎 Java 社区更喜欢 XML,而目前似乎使用更多的是 JSON。在交换协议选择方面,考虑的主要这几个方面因素:性能、跨语言(通用性)、传输量等。因此,对于性能要求高的场景,会使用 protobuf、msgpack 之类的协议。由于 JSON 和 XML 使用很广泛,Go 语言提供了解析它们的标准库;同时,为了方便 Go 程序直接数据交换,Go 专门提供了 [gob](http://docs.studygolang.com/pkg/) 这种交换协议。 8 | 9 | # 导航 # 10 | 11 | - [目录](/preface.md) 12 | - 下一节:[database/sql — SQL/SQL-Like 数据库操作接口](07.1.md) 13 | -------------------------------------------------------------------------------- /chapter07/07.1.md: -------------------------------------------------------------------------------- 1 | # 7.1 database/sql — SQL/SQL-Like 数据库操作接口 # 2 | 3 | 这是 Go 提供的操作 SQL/SQL-Like 数据库的通用接口,但 Go 标准库并没有提供具体数据库的实现,需要结合第三方的驱动来使用该接口。本书使用的是 mysql 的驱动:[github.com/go-sql-driver/mysql](https://github.com/go-sql-driver/mysql)。 4 | 5 | *注:该包有一个子包:driver,它定义了一些接口供数据库驱动实现,一般业务代码中使用 database/sql 包即可,尽量避免使用 driver 这个子包。* 6 | 7 | ## 7.1.1 database/sql 是什么? ## 8 | 9 | 很明显,[database/sql](http://docs.studygolang.com/pkg/database/sql) 首先是 Go 标准库提供的一个包,用于和 SQL/SQL-Like 数据库(关系或类似关系数据库)通讯。它提供了和 ODBC、Perl的DBI、Java的JDBC和PHP的PDO 类似的功能。然而,它的设计却不太一样,掌握了它有利于构建健壮、高性能的基于 database 的应用。 10 | 11 | 另一方面,database/sql 提供的是抽象概念,和具体数据库无关,具体的数据库实现,有驱动来做,这样可以很方便的更换数据库。 12 | 13 | 该包提供了一些类型(概括性的),每个类型可能包括一个或多个概念。 14 | 15 | - DB 16 | sql.DB 类型代表了一个数据库。这点和很多其他语言不同,它并不代表一个到数据库的具体连接,而是一个能操作的数据库对象,具体的连接在内部通过连接池来管理,对外不暴露。这点是很多人容易误解的:每一次数据库操作,都产生一个 sql.DB 实例,操作完 Close。 17 | - Results 18 | 定义了三种结果类型:sql.Rows、sql.Row 和 sql.Result,分别用于获取多个多行结果、一行结果和修改数据库影响的行数(或其返回last insert id)。 19 | - Statements 20 | sql.Stmt 代表一个语句,如:DDL、DML等。 21 | - Transactions 22 | sql.Tx 代表带有特定属性的一个事务。 23 | 24 | ## 7.1.2 sql.DB 的使用 ## 25 | 26 | 官方文档关于 DB 的描述: 27 | 28 | > 是一个数据库句柄,代表一个具有零到多个底层连接的连接池,它可以安全的被多个 goroutine 同时使用。 29 | > sql包会自动创建和释放连接;它也会维护一个闲置连接的连接池。如果数据库具有单连接状态的概念,该状态只有在事务中被观察时才可信。一旦调用了BD.Begin,返回的Tx会绑定到单个连接。当调用事务Tx的Commit或Rollback后,该事务使用的连接会归还到DB的闲置连接池中。连接池的大小可以用SetMaxIdleConns方法控制。 30 | 31 | 由于 DB 并非一个实际的到数据库的连接,而且可以被多个 goroutine 并发使用,因此,程序中只需要拥有一个全局的实例即可。所以,经常见到的示例代码: 32 | 33 | db, err := sql.Open("mysql", "root:@tcp(localhost:3306)/test?charset=utf8") 34 | if err != nil { 35 | panic(err) 36 | } 37 | defer db.Close() 38 | 39 | 实际中,`defer db.Close()`可以不调用,官方文档关于 DB.Close 的说明也提到了:Close 用于关闭数据库,释放任何打开的资源。一般不会关闭 DB,因为 DB 句柄通常被多个 goroutine 共享,并长期活跃。当然,如果你确定 DB 只会被使用一次,之后不会使用了,应该调用 Close。 40 | 41 | 所以,实际的 Go 程序,应该在一个go文件中的 init 函数中调用 `sql.Open` 初始化全局的 sql.DB 对象,供程序中所有需要进行数据库操作的地方使用。 42 | 43 | 前面说过,sql.DB 并不是实际的数据库连接,因此,sql.Open 函数并没有进行数据库连接,只有在驱动未注册时才会返回 `err != nil`。 44 | 45 | 例如:`db, err := sql.Open("mysql", "root:@tcp23(localhost233:3306)/test?charset=utf8")`。虽然这里的 dsn 是错误的,但依然 `err == nil`,只有在实际操作数据库(查询、更新等)或调用 `Ping` 时才会报错。 46 | 47 | 关于 Open 函数的参数,第一个是驱动名,为了避免混淆,一般和驱动包名一致,在驱动实现中,会有类似这样的代码: 48 | 49 | func init() { 50 | sql.Register("mysql", &MySQLDriver{}) 51 | } 52 | 53 | 其中 mysql 即是注册的驱动名。由于注册驱动是在 init 函数中进行的,这也就是为什么采用`_ "github.com/go-sql-driver/mysql"` 这种方式引入驱动包。第二个参数是 DSN(数据源名称),这个是和具体驱动相关的,database/sql 包并没有规定,具体书写方式参见驱动文档。 54 | 55 | ### 7.1.2.1 连接池的工作原理 ### 56 | 57 | 获取 DB 对象后,连接池是空的,第一个连接在需要的时候才会创建。可以通过下面的代码验证这一点: 58 | 59 | db, _ := sql.Open("mysql", "root:@tcp(localhost:3306)/test?charset=utf8") 60 | fmt.Println("please exec show processlist") 61 | time.Sleep(10 * time.Second) 62 | fmt.Println("please exec show processlist again") 63 | db.Ping() 64 | time.Sleep(10 * time.Second) 65 | 66 | 在 Ping 执行之前和之后,show processlist 多了一条记录,即多了一个连接,Command 列是 Sleep。 67 | 68 | 连接池的工作方式:当调用一个函数,需要访问数据库时,该函数会请求从连接池中获取一个连接,如果连接池中存在一个空闲连接,它会将该空闲连接给该函数;否则,会打开一个新的连接。当该函数结束时,该连接要么返回给连接池,要么传递个某个需要该连接的对象,知道该对象完成时,连接才会返回给连接池。相关方法的处理说明(假设 sql.DB 的对象是 db): 69 | 70 | - **db.Ping()** 会将连接立马返回给连接池。 71 | - **db.Exec()** 会将连接立马返回给连接池,但是它返回的 Result 对象会引用该连接,所以,之后可能会再次被使用。 72 | - **db.Query()** 会传递连接给 sql.Rows 对象,直到完全遍历了所有的行或 Rows 的 Close 方法被调用了,连接才会返回给连接池。 73 | - **db.QueryRow()** 会传递连接给 sql.Row 对象,当该对象的 Scan 方法被调用时,连接会返回给连接池。 74 | - **db.Begin()** 会传递连接给 sql.Tx 对象,当该对象的 Commit 或 Rollback 方法被调用时,该链接会返回给连接池。 75 | 76 | 从上面的解释可以知道,大部分时候,我们不需要关心连接不释放问题,它们会自动返回给连接池,只有 Query 方法有点特殊,后面讲解如何处理。 77 | 78 | 注意:如果某个连接有问题(broken connection),database/sql 内部会进行[最多10次](http://docs.studygolang.com/src/database/sql/sql.go?s=22080:22097#L824)的重试,从连接池中获取或新开一个连接来服务,因此,你的代码中不需要重试的逻辑。 79 | 80 | ### 7.1.2.2 控制连接池 ### 81 | 82 | Go1.2.1 之前,没法控制连接池,Go1.2.1 之后,提供了两个方法来控制连接池(Go1.2 提供了控制,不过有bug)。 83 | 84 | - **db.SetMaxOpenConns(n int)** 设置连接池中最多保存打开多少个数据库连接。注意,它包括在使用的和空闲的。如果某个方法调用需要一个连接,但连接池中没有空闲的可用,且打开的连接数达到了该方法设置的最大值,该方法调用将堵塞。默认限制是0,表示最大打开数没有限制。 85 | - **db.SetMaxIdleConns(n int)** 设置连接池中能够保持的最大空闲连接的数量。[默认值是2](http://docs.studygolang.com/src/database/sql/sql.go?s=13724:13743#L501) 86 | 87 | 上面的两个设置,可以用程序实际测试。比如通过下面的代码,可以验证 MaxIdleConns 是 2: 88 | 89 | db, _ := sql.Open("mysql", "root:@tcp(localhost:3306)/test?charset=utf8") 90 | 91 | // 去掉注释,可以看看相应的空闲连接是不是变化了 92 | // db.SetMaxIdleConns(3) 93 | 94 | for i := 0; i < 10; i++ { 95 | go func() { 96 | db.Ping() 97 | }() 98 | } 99 | 100 | time.Sleep(20 * time.Second) 101 | 102 | 通过 show processlist 命令,可以看到有两个是 Sleep 的连接。 103 | 104 | 105 | # 导航 # 106 | 107 | - [第七章 数据持久存储与交换](/chapter07/07.0.md) 108 | - 下一节:encoding/json — json 解析 -------------------------------------------------------------------------------- /chapter08/08.0.md: -------------------------------------------------------------------------------- 1 | # 第八章 数据压缩与归档 # 2 | 3 | 在计算机算法中,经常会有以空间换时间的做法;而无损压缩算法以压缩或解压缩数据花费的时间来换取存储空间。除此之外,压缩还有另外一个重要用途,那就是减少网络传输的数据量,进而减少网络传输的时间。 4 | 5 | Go 标准库实现了一些最流行的压缩标准。zlib 和 gzip 提供了 GNU zip 库,bzip2 用于读写 bzip2 格式。这些格式都可以处理数据流而不考虑输入格式,并且提供了接口可以透明地读写压缩文件。除此之外,标准库还提供了 DEFLATE 压缩算法的实现,gzip 和 zlib 可以读取基于 DEFLATE 的文件格式。 6 | 7 | 标准库提供了 LZW 压缩算法(串表压缩算法)的实现,该算法常用的文件格式:GIF 和 PDF。另外,TIFF 文件格式使用和该算法类似,但和目前 LZW 算法版本不兼容。 8 | 9 | 标准库还提供一些包管理归档(archive)格式,将多个文件合并到一个文件,从而将其作为一个单元管理。`archive/tar` 读写 UNIX 磁带归档格式,这是一种老标准,但由于其灵活性,当前仍得到广泛使用。`archive/zip` 根据 zip 格式来处理归档,这种格式因 PC 程序 PKZIP 得以普及,原先在 MS-DOS 和 Windows 下使用,不过由于其 API 的简单性以及这种格式的可移植性,现在也用于其他平台。 10 | 11 | # 导航 # 12 | 13 | - [第七章](/chapter07/07.0.md) 14 | - 下一节:[flate - DEFLATE 压缩算法](08.1.md) 15 | -------------------------------------------------------------------------------- /chapter08/08.1.md: -------------------------------------------------------------------------------- 1 | # flate - DEFLATE 压缩算法 # 2 | 3 | DEFLATE 是同时使用了哈夫曼编码(Huffman Coding)与 LZ77 算法的一个无损数据压缩算法,是一种压缩数据流的算法。任何需要流式压缩的地方都可以用。目前 zip 压缩文件默认使用的就是该算法。 4 | 5 | 关于算法的原理,以及 哈夫曼编码(Huffman Coding)与 LZ77 算法,感兴趣的读者可以查询相关资料,这里推荐 [GZIP压缩原理分析——第五章 Deflate算法详解](http://blog.csdn.net/jison_r_wang/article/details/52071317) 序列文章。 6 | 7 | 8 | ## 使用预设字典提升压缩率 9 | 10 | 11 | 12 | # 导航 # 13 | 14 | - [第八章](/chapter08/08.0.md) 15 | - 下一节:[zlib - GNU zlib 压缩](08.2.md) -------------------------------------------------------------------------------- /chapter09/09.0.md: -------------------------------------------------------------------------------- 1 | # 第九章 测试 # 2 | 3 | Go 语言从开发初期就注意了测试用例的编写。特别是静态语言,由于调试没有动态语言那么方便,所以能最快最方便地编写一个测试用例就显得非常重要了。 4 | 5 | 本章内容涵盖了 Go 标准库中的 2 个包: 6 | 7 | * *testing* 方便进行 Go 包的自动化单元测试、基准测试 8 | * *net/http/httptest* 提供测试 `HTTP` 的工具 9 | 10 | # 导航 # 11 | 12 | - [第八章](/chapter08/08.0.md) 13 | - 下一节:[testing - 单元测试](09.1.md) 14 | -------------------------------------------------------------------------------- /chapter09/09.1.md: -------------------------------------------------------------------------------- 1 | # testing - 单元测试 # 2 | 3 | `testing` 为 Go 语言 package 提供自动化测试的支持。通过 `go test` 命令,能够自动执行如下形式的任何函数: 4 | 5 | func TestXxx(*testing.T) 6 | 7 | 注意:Xxx 可以是任何字母数字字符串,但是第一个字母不能是小些字母。 8 | 9 | 在这些函数中,使用 Error, Fail 或相关方法来发出失败信号。 10 | 11 | 要编写一个新的测试套件,需要创建一个名称以 _test.go 结尾的文件,该文件包含 `TestXxx` 函数,如上所述。 将该文件放在与被测试的包相同的包中。该文件将被排除在正常的程序包之外,但在运行 “go test” 命令时将被包含。 有关详细信息,请运行 “go help test” 和 “go help testflag” 了解。 12 | 13 | 如果有需要,可以调用 `*T` 和 `*B` 的 Skip 方法,跳过该测试或基准测试: 14 | 15 | ```go 16 | func TestTimeConsuming(t *testing.T) { 17 | if testing.Short() { 18 | t.Skip("skipping test in short mode.") 19 | } 20 | ... 21 | } 22 | ``` 23 | 24 | ## 第一个单元测试 ## 25 | 26 | 要测试的代码: 27 | 28 | ```go 29 | func Fib(n int) int { 30 | if n < 2 { 31 | return n 32 | } 33 | return Fib(n-1) + Fib(n-2) 34 | } 35 | ``` 36 | 37 | 测试代码: 38 | 39 | ```go 40 | func TestFib(t *testing.T) { 41 | var ( 42 | in = 7 43 | expected = 13 44 | ) 45 | actual := Fib(in) 46 | if actual != expected { 47 | t.Errorf("Fib(%d) = %d; expected %d", in, actual, expected) 48 | } 49 | } 50 | ``` 51 | 执行 `go test .`,输出: 52 | 53 | ``` 54 | $ go test . 55 | ok chapter09/testing 0.007s 56 | ``` 57 | 58 | 表示测试通过。 59 | 60 | 我们将 `Sum` 函数改为: 61 | 62 | ```go 63 | func Fib(n int) int { 64 | if n < 2 { 65 | return n 66 | } 67 | return Fib(n-1) + Fib(n-1) 68 | } 69 | ``` 70 | 71 | 再执行 `go test .`,输出: 72 | 73 | ``` 74 | $ go test . 75 | --- FAIL: TestSum (0.00s) 76 | t_test.go:16: Fib(10) = 64; expected 13 77 | FAIL 78 | FAIL chapter09/testing 0.009s 79 | ``` 80 | 81 | ## Table-Driven Test ## 82 | 83 | 测试讲究 case 覆盖,按上面的方式,当我们要覆盖更多 case 时,显然通过修改代码的方式很笨拙。这时我们可以采用 Table-Driven 的方式写测试,标准库中有很多测试是使用这种方式写的。 84 | 85 | ```go 86 | func TestFib(t *testing.T) { 87 | var fibTests = []struct { 88 | in int // input 89 | expected int // expected result 90 | }{ 91 | {1, 1}, 92 | {2, 1}, 93 | {3, 2}, 94 | {4, 3}, 95 | {5, 5}, 96 | {6, 8}, 97 | {7, 13}, 98 | } 99 | 100 | for _, tt := range fibTests { 101 | actual := Fib(tt.in) 102 | if actual != tt.expected { 103 | t.Errorf("Fib(%d) = %d; expected %d", tt.in, actual, tt.expected) 104 | } 105 | } 106 | } 107 | ``` 108 | 因为我们使用的是 `t.Errorf`,其中某个 case 失败,并不会终止测试执行。 109 | 110 | ## T 类型 ## 111 | 112 | 单元测试中,传递给测试函数的参数是 `*testing.T` 类型。它用于管理测试状态并支持格式化测试日志。测试日志会在执行测试的过程中不断累积,并在测试完成时转储至标准输出。 113 | 114 | 当一个测试的测试函数返回时,又或者当一个测试函数调用 `FailNow`、 `Fatal`、`Fatalf`、`SkipNow`、`Skip` 或者 `Skipf` 中的任意一个时,该测试即宣告结束。跟 `Parallel` 方法一样,以上提到的这些方法只能在运行测试函数的 goroutine 中调用。 115 | 116 | 至于其他报告方法,比如 `Log` 以及 `Error` 的变种, 则可以在多个 goroutine 中同时进行调用。 117 | 118 | ### 报告方法 ### 119 | 120 | 上面提到的系列包括方法,带 `f` 的是格式化的,格式化语法参考 `fmt` 包。 121 | 122 | T 类型内嵌了 common 类型,common 提供这一系列方法,我们经常会用到的(注意,这里说的测试中断,都是指当前测试函数): 123 | 124 | 1)当我们遇到一个断言错误的时候,标识这个测试失败,会使用到: 125 | 126 | Fail: 测试失败,测试继续,也就是之后的代码依然会执行 127 | FailNow: 测试失败,测试中断 128 | 129 | 在 `FailNow ` 方法实现的内部,是通过调用 `runtime.Goexit()` 来中断测试的。 130 | 131 | 2)当我们遇到一个断言错误,只希望跳过这个错误,但是不希望标识测试失败,会使用到: 132 | 133 | SkipNow: 跳过测试,测试中断 134 | 135 | 在 `SkipNow` 方法实现的内部,是通过调用 `runtime.Goexit()` 来中断测试的。 136 | 137 | 3)当我们只希望打印信息,会用到: 138 | 139 | Log: 输出信息 140 | Logf: 输出格式化的信息 141 | 142 | 注意:默认情况下,单元测试成功时,它们打印的信息不会输出,可以通过加上 `-v` 选项,输出这些信息。但对于基准测试,它们总是会被输出。 143 | 144 | 4)当我们希望跳过这个测试,并且打印出信息,会用到: 145 | 146 | Skip: 相当于 Log + SkipNow 147 | Skipf: 相当于 Logf + SkipNow 148 | 149 | 5)当我们希望断言失败的时候,标识测试失败,并打印出必要的信息,但是测试继续,会用到: 150 | 151 | Error: 相当于 Log + Fail 152 | Errorf: 相当于 Logf + Fail 153 | 154 | 6)当我们希望断言失败的时候,标识测试失败,打印出必要的信息,但中断测试,会用到 155 | 156 | Fatal: 相当于 Log + FailNow 157 | Fatalf: 相当于 Logf + FailNow 158 | 159 | ### Parallel 测试 ### 160 | 161 | 包中的 Parallel 方法用于表示当前测试只会与其他带有 Parallel 方法的测试并行进行测试。 162 | 163 | 下面的例子演示的 Parallel 的使用: 164 | 165 | ```go 166 | var ( 167 | data = make(map[string]string) 168 | locker sync.RWMutex 169 | ) 170 | 171 | func WriteToMap(k, v string) { 172 | locker.Lock() 173 | defer locker.Unlock() 174 | data[k] = v 175 | } 176 | 177 | func ReadFromMap(k string) string { 178 | locker.RLock() 179 | defer locker.RUnlock() 180 | return data[k] 181 | } 182 | ``` 183 | 184 | 测试代码: 185 | 186 | ```go 187 | var pairs = []struct { 188 | k string 189 | v string 190 | }{ 191 | {"polaris", "徐新华"}, 192 | {"studygolang", "Go语言中文网"}, 193 | {"stdlib", "Go语言标准库"}, 194 | {"polaris1", "徐新华1"}, 195 | {"studygolang1", "Go语言中文网1"}, 196 | {"stdlib1", "Go语言标准库1"}, 197 | {"polaris2", "徐新华2"}, 198 | {"studygolang2", "Go语言中文网2"}, 199 | {"stdlib2", "Go语言标准库2"}, 200 | {"polaris3", "徐新华3"}, 201 | {"studygolang3", "Go语言中文网3"}, 202 | {"stdlib3", "Go语言标准库3"}, 203 | {"polaris4", "徐新华4"}, 204 | {"studygolang4", "Go语言中文网4"}, 205 | {"stdlib4", "Go语言标准库4"}, 206 | } 207 | 208 | // 注意 TestWriteToMap 需要在 TestReadFromMap 之前 209 | func TestWriteToMap(t *testing.T) { 210 | t.Parallel() 211 | for _, tt := range pairs { 212 | WriteToMap(tt.k, tt.v) 213 | } 214 | } 215 | 216 | func TestReadFromMap(t *testing.T) { 217 | t.Parallel() 218 | for _, tt := range pairs { 219 | actual := ReadFromMap(tt.k) 220 | if actual != tt.v { 221 | t.Errorf("the value of key(%s) is %s, expected: %s", tt.k, actual, tt.v) 222 | } 223 | } 224 | } 225 | ``` 226 | 试验步骤: 227 | 228 | 1. 注释掉 WriteToMap 和 ReadFromMap 中 locker 保护的代码,同时注释掉测试代码中的 t.Parallel,执行测试,测试通过,即使加上 `-race`,测试依然通过; 229 | 2. 只注释掉 WriteToMap 和 ReadFromMap 中 locker 保护的代码,执行测试,测试失败(如果未失败,加上 `-race` 一定会失败); 230 | 231 | 如果代码能够进行并行测试,在写测试时,尽量加上 Parallel,这样可以测试出一些可能的问题。 232 | 233 | 关于 Parallel 的更多内容,会在 [子测试](chapter09/09.3.md) 中介绍。 234 | 235 | 当你写完一个函数,结构体,main之后,你下一步需要的就是测试了。testing包提供了很简单易用的测试包。 236 | 237 | # 写一个基本的测试用例 # 238 | 239 | 测试文件的文件名需要以_test.go为结尾,测试用例需要以TestXxxx的样式存在。 240 | 241 | 比如我要测试utils包的sql.go中的函数: 242 | 243 | func GetOne(db *sql.DB, query string, args ...interface{}) (map[string][]byte, error) { 244 | 245 | 就需要创建一个sql_test.go 246 | 247 | package utils 248 | 249 | import ( 250 | "database/sql" 251 | _ "fmt" 252 | _ "github.com/go-sql-driver/mysql" 253 | "strconv" 254 | "testing" 255 | ) 256 | 257 | func Test_GetOne(t *testing.T) { 258 | db, err := sql.Open("mysql", "root:123.abc@tcp(192.168.33.10:3306)/test") 259 | defer func() { 260 | db.Close() 261 | }() 262 | if err != nil { 263 | t.Fatal(err) 264 | } 265 | 266 | // 测试empty 267 | car_brand, err := GetOne(db, "select * from user where id = 999999") 268 | if (car_brand != nil) || (err != nil) { 269 | t.Fatal("emtpy测试错误") 270 | } 271 | } 272 | 273 | #testing的测试用例形式# 274 | 275 | 测试用例有四种形式: 276 | TestXxxx(t *testing.T) // 基本测试用例 277 | BenchmarkXxxx(b *testing.B) // 压力测试的测试用例 278 | Example_Xxx() // 测试控制台输出的例子 279 | TestMain(m *testing.M) // 测试Main函数 280 | 281 | 给个Example的例子:(Example需要在最后用注释的方式确认控制台输出和预期是不是一致的) 282 | 283 | func Example_GetScore() { 284 | score := getScore(100, 100, 100, 2.1) 285 | fmt.Println(score) 286 | // Output: 287 | // 31.1 288 | } 289 | 290 | #testing的变量# 291 | 292 | gotest的变量有这些: 293 | 294 | * test.short : 一个快速测试的标记,在测试用例中可以使用testing.Short()来绕开一些测试 295 | * test.outputdir : 输出目录 296 | * test.coverprofile : 测试覆盖率参数,指定输出文件 297 | * test.run : 指定正则来运行某个/某些测试用例 298 | * test.memprofile : 内存分析参数,指定输出文件 299 | * test.memprofilerate : 内存分析参数,内存分析的抽样率 300 | * test.cpuprofile : cpu分析输出参数,为空则不做cpu分析 301 | * test.blockprofile : 阻塞事件的分析参数,指定输出文件 302 | * test.blockprofilerate : 阻塞事件的分析参数,指定抽样频率 303 | * test.timeout : 超时时间 304 | * test.cpu : 指定cpu数量 305 | * test.parallel : 指定运行测试用例的并行数 306 | 307 | #testing包内的结构# 308 | 309 | * B : 压力测试 310 | * BenchmarkResult : 压力测试结果 311 | * Cover : 代码覆盖率相关结构体 312 | * CoverBlock : 代码覆盖率相关结构体 313 | * InternalBenchmark : 内部使用的结构 314 | * InternalExample : 内部使用的结构 315 | * InternalTest : 内部使用的结构 316 | * M : main测试使用的结构 317 | * PB : Parallel benchmarks 并行测试使用结果 318 | * T : 普通测试用例 319 | * TB : 测试用例的接口 320 | 321 | #testing的通用方法# 322 | 323 | T结构内部是继承自common结构,common结构提供集中方法,是我们经常会用到的: 324 | 325 | 当我们遇到一个断言错误的时候,我们就会判断这个测试用例失败,就会使用到: 326 | 327 | Fail : case失败,测试用例继续 328 | FailedNow : case失败,测试用例中断 329 | 330 | 当我们遇到一个断言错误,只希望跳过这个错误,但是不希望标示测试用例失败,会使用到: 331 | 332 | SkipNow : case跳过,测试用例不继续 333 | 334 | 当我们只希望在一个地方打印出信息,我们会用到: 335 | 336 | Log : 输出信息 337 | Logf : 输出有format的信息 338 | 339 | 当我们希望跳过这个用例,并且打印出信息: 340 | 341 | Skip : Log + SkipNow 342 | Skipf : Logf + SkipNow 343 | 344 | 当我们希望断言失败的时候,测试用例失败,打印出必要的信息,但是测试用例继续: 345 | 346 | Error : Log + Fail 347 | Errorf : Logf + Fail 348 | 349 | 当我们希望断言失败的时候,测试用例失败,打印出必要的信息,测试用例中断: 350 | 351 | Fatal : Log + FailNow 352 | Fatalf : Logf + FailNow 353 | 354 | 355 | # 导航 # 356 | 357 | - [第九章](/chapter09/09.0.md) 358 | - 下一节:[testing - 基准测试](09.2.md) 359 | 360 | 361 | -------------------------------------------------------------------------------- /chapter09/09.2.md: -------------------------------------------------------------------------------- 1 | # testing - 基准测试 # 2 | 3 | 在 _test.go 结尾的测试文件中,如下形式的函数: 4 | 5 | func BenchmarkXxx(*testing.B) 6 | 7 | 被认为是基准测试,通过 "go test" 命令,加上 `-bench` flag 来执行。多个基准测试按照顺序运行。 8 | 9 | 基准测试函数样例看起来如下所示: 10 | 11 | ```go 12 | func BenchmarkHello(b *testing.B) { 13 | for i := 0; i < b.N; i++ { 14 | fmt.Sprintf("hello") 15 | } 16 | } 17 | ``` 18 | 19 | 基准函数会运行目标代码 b.N 次。在基准执行期间,会调整 b.N 直到基准测试函数持续足够长的时间。输出 20 | 21 | BenchmarkHello 10000000 282 ns/op 22 | 23 | 意味着循环执行了 10000000 次,每次循环花费 282 纳秒(ns)。 24 | 25 | 如果在运行前基准测试需要一些耗时的配置,则可以先重置定时器: 26 | 27 | ```go 28 | func BenchmarkBigLen(b *testing.B) { 29 | big := NewBig() 30 | b.ResetTimer() 31 | for i := 0; i < b.N; i++ { 32 | big.Len() 33 | } 34 | } 35 | ``` 36 | 如果基准测试需要在并行设置中测试性能,则可以使用 RunParallel 辅助函数; 这样的基准测试一般与 `go test -cpu` 标志一起使用: 37 | 38 | ```go 39 | func BenchmarkTemplateParallel(b *testing.B) { 40 | templ := template.Must(template.New("test").Parse("Hello, {{.}}!")) 41 | b.RunParallel(func(pb *testing.PB) { 42 | // 每个 goroutine 有属于自己的 bytes.Buffer. 43 | var buf bytes.Buffer 44 | for pb.Next() { 45 | // 所有 goroutine 一起,循环一共执行 b.N 次 46 | buf.Reset() 47 | templ.Execute(&buf, "World") 48 | } 49 | }) 50 | } 51 | ``` 52 | 53 | ## 基准测试示例 ## 54 | 55 | 接着上一节的例子,我们对 `Fib` 进行基准测试: 56 | 57 | ```go 58 | func BenchmarkFib10(b *testing.B) { 59 | for n := 0; n < b.N; n++ { 60 | Fib(10) 61 | } 62 | } 63 | ``` 64 | 执行 `go test -bench=.`,输出: 65 | 66 | ``` 67 | $ go test -bench=. 68 | BenchmarkFib10-4 3000000 424 ns/op 69 | PASS 70 | ok chapter09/testing 1.724s 71 | ``` 72 | 这里测试了 `Fib(10)` 的情况,我们可能需要测试更多不同的情况,这时可以改写我们的测试代码: 73 | 74 | ```go 75 | func BenchmarkFib1(b *testing.B) { benchmarkFib(1, b) } 76 | func BenchmarkFib2(b *testing.B) { benchmarkFib(2, b) } 77 | func BenchmarkFib3(b *testing.B) { benchmarkFib(3, b) } 78 | func BenchmarkFib10(b *testing.B) { benchmarkFib(10, b) } 79 | func BenchmarkFib20(b *testing.B) { benchmarkFib(20, b) } 80 | func BenchmarkFib40(b *testing.B) { benchmarkFib(40, b) } 81 | 82 | func benchmarkFib(i int, b *testing.B) { 83 | for n := 0; n < b.N; n++ { 84 | Fib(i) 85 | } 86 | } 87 | ``` 88 | 再次执行 `go test -bench=.`,输出: 89 | 90 | ``` 91 | $ go test -bench=. 92 | BenchmarkFib1-4 1000000000 2.58 ns/op 93 | BenchmarkFib2-4 200000000 7.38 ns/op 94 | BenchmarkFib3-4 100000000 13.0 ns/op 95 | BenchmarkFib10-4 3000000 429 ns/op 96 | BenchmarkFib20-4 30000 54335 ns/op 97 | BenchmarkFib40-4 2 805759850 ns/op 98 | PASS 99 | ok chapter09/testing 15.361s 100 | ``` 101 | 默认情况下,每个基准测试最少运行 1 秒。如果基准测试函数返回时,还不到 1 秒钟,`b.N` 的值会按照序列 1,2,5,10,20,50,... 增加,同时再次运行基准测测试函数。 102 | 103 | 我们注意到 `BenchmarkFib40` 一共才运行 2 次。为了更精确的结果,我们可以通过增加标志 `-benchtime` 来它运行更多次。 104 | 105 | ``` 106 | $ go test -bench=Fib40 -benchtime=20s 107 | BenchmarkFib40-4 30 838675800 ns/op 108 | ``` 109 | 110 | ## B 类型 ## 111 | 112 | B 是传递给基准测试函数的一种类型,它用于管理基准测试的计时行为,并指示应该迭代地运行测试多少次。 113 | 114 | 一个基准测试在它的基准测试函数返回时,又或者在它的基准测试函数调用 `FailNow`、`Fatal`、`Fatalf`、`SkipNow`、`Skip` 或者 `Skipf` 中的任意一个方法时,测试即宣告结束。至于其他报告方法,比如 `Log` 和 `Error` 的变种,则可以在其他 goroutine 中同时进行调用。 115 | 116 | 跟单元测试一样,基准测试会在执行的过程中积累日志,并在测试完毕时将日志转储到标准错误。但跟单元测试不一样的是,为了避免基准测试的结果受到日志打印操作的影响,基准测试总是会把日志打印出来。 117 | 118 | B 类型中的报告方法使用方式和 T 类型是一样的,一般来说,基准测试中也不需要使用,毕竟主要是测性能。这里我们对 B 类型中其他的一些方法进行讲解。 119 | 120 | ### 计时方法 ### 121 | 122 | 有三个方法用于计时: 123 | 124 | 1. StartTimer:开始对测试进行计时。该方法会在基准测试开始时自动被调用,我们也可以在调用 StopTimer 之后恢复计时; 125 | 2. StopTimer:停止对测试进行计时。当你需要执行一些复杂的初始化操作,并且你不想对这些操作进行测量时,就可以使用这个方法来暂时地停止计时。 126 | 3. ResetTimer:对已经逝去的基准测试时间以及内存分配计数器进行清零。对于正在运行中的计时器,这个方法不会产生任何效果。本节开头有使用示例。 127 | 128 | ### 并行执行 ### 129 | 130 | 通过 `RunParallel` 方法以并行的方式执行给定的基准测试。RunParallel 会创建出多个 goroutine,并将 b.N 分配给这些 goroutine 执行,其中 goroutine 数量的默认值为 GOMAXPROCS。用户如果想要增加非CPU受限(non-CPU-bound)基准测试的并行性,那么可以在 RunParallel 之前调用 SetParallelism(如 SetParallelism(2),则 goroutine 数量为 2*GOMAXPROCS)。RunParallel 通常会与 -cpu 标志一同使用。 131 | 132 | `body` 函数将在每个 goroutine 中执行,这个函数需要设置所有 goroutine 本地的状态,并迭代直到 `pb.Next` 返回 false 值为止。因为 `StartTimer`、`StopTime` 和 `ResetTimer` 这三个方法都带有全局作用,所以 body 函数不应该调用这些方法; 除此之外,body 函数也不应该调用 Run 方法。 133 | 134 | 具体的使用示例,在本节开头已经提供! 135 | 136 | ### 内存统计 ### 137 | 138 | `ReportAllocs` 方法用于打开当前基准测试的内存统计功能, 与 `go test` 使用 `-benchmem` 标志类似,但 `ReportAllocs` 只影响那些调用了该函数的基准测试。 139 | 140 | 测试示例: 141 | 142 | ```go 143 | func BenchmarkTmplExucte(b *testing.B) { 144 | b.ReportAllocs() 145 | templ := template.Must(template.New("test").Parse("Hello, {{.}}!")) 146 | b.RunParallel(func(pb *testing.PB) { 147 | // Each goroutine has its own bytes.Buffer. 148 | var buf bytes.Buffer 149 | for pb.Next() { 150 | // The loop body is executed b.N times total across all goroutines. 151 | buf.Reset() 152 | templ.Execute(&buf, "World") 153 | } 154 | }) 155 | } 156 | ``` 157 | 158 | 测试结果类似这样: 159 | 160 | BenchmarkTmplExucte-4 2000000 898 ns/op 368 B/op 9 allocs/op 161 | 162 | ### 基准测试结果 ## 163 | 164 | 对 165 | 166 | BenchmarkTmplExucte-4 2000000 898 ns/op 368 B/op 9 allocs/op 167 | 168 | 中的每一项,你是否都清楚是什么意思呢? 169 | 170 | `testing` 包中的 `BenchmarkResult` 类型能为你提供帮助。它保存了基准测试的结果。 171 | 172 | 它的定义如下: 173 | 174 | ```go 175 | type BenchmarkResult struct { 176 | N int // The number of iterations. 即 b.N 177 | T time.Duration // The total time taken. 基准测试花费的时间 178 | Bytes int64 // Bytes processed in one iteration. 一次迭代处理的字节数,通过 b.SetBytes 设置 179 | MemAllocs uint64 // The total number of memory allocations. 总的分配内存的次数 180 | MemBytes uint64 // The total number of bytes allocated. 总的分配内存的字节数 181 | } 182 | ``` 183 | 该类型还提供了相应的计算每个操作每秒相应指标的方法。示例如下: 184 | 185 | ```go 186 | package main 187 | 188 | import ( 189 | "bytes" 190 | "fmt" 191 | "testing" 192 | "text/template" 193 | ) 194 | 195 | func main() { 196 | benchmarkResult := testing.Benchmark(func(b *testing.B) { 197 | templ := template.Must(template.New("test").Parse("Hello, {{.}}!")) 198 | // RunParallel will create GOMAXPROCS goroutines 199 | // and distribute work among them. 200 | b.RunParallel(func(pb *testing.PB) { 201 | // Each goroutine has its own bytes.Buffer. 202 | var buf bytes.Buffer 203 | for pb.Next() { 204 | // The loop body is executed b.N times total across all goroutines. 205 | buf.Reset() 206 | templ.Execute(&buf, "World") 207 | } 208 | }) 209 | }) 210 | 211 | // fmt.Printf("%8d\t%10d ns/op\t%10d B/op\t%10d allocs/op\n", benchmarkResult.N, benchmarkResult.NsPerOp(), benchmarkResult.AllocedBytesPerOp(), benchmarkResult.AllocsPerOp()) 212 | fmt.Printf("%s\t%s\n", benchmarkResult.String(), benchmarkResult.MemString()) 213 | } 214 | ``` 215 | 216 | # 导航 # 217 | 218 | - 上一节:[testing - 单元测试](09.1.md) 219 | - 下一节:[testing - 子测试](09.3.md) 220 | -------------------------------------------------------------------------------- /chapter09/09.3.md: -------------------------------------------------------------------------------- 1 | # testing - 子测试与子基准测试 # 2 | 3 | 从 Go 1.7 开始,引入了一个新特性:子测试,又叫 命名测试(named tests),它意味着您现在可以拥有嵌套测试,这对于自定义(和过滤)给定测试的示例非常有用。 4 | 5 | T 和 B 的 Run 方法允许定义子单元测试和子基准测试,而不必为每个子测试和子基准定义单独的函数。这使得可以使用 Table-Driven 的基准测试和创建层级测试。它还提供了一种共享通用 `setup` 和 `tear-down` 代码的方法: 6 | 7 | ```go 8 | func TestFoo(t *testing.T) { 9 | // 10 | t.Run("A=1", func(t *testing.T) { ... }) 11 | t.Run("A=2", func(t *testing.T) { ... }) 12 | t.Run("B=1", func(t *testing.T) { ... }) 13 | // 14 | } 15 | ``` 16 | 每个子测试和子基准测试都有一个唯一的名称:顶级测试的名称和传递给 Run 的名称的组合,以斜杠分隔,并具有用于消歧的可选尾随序列号。 17 | 18 | `-run` 和 `-bench` 命令行标志的参数是与测试名称相匹配的非固定的正则表达式。对于具有多个斜杠分隔元素(例如子测试)的测试,该参数本身是斜杠分隔的,其中表达式依次匹配每个名称元素。因为它是非固定的,一个空的表达式匹配任何字符串。例如,使用 "匹配" 表示 "其名称包含": 19 | 20 | ``` 21 | go test -run '' # Run 所有测试。 22 | go test -run Foo # Run 匹配 "Foo" 的顶层测试,例如 "TestFooBar"。 23 | go test -run Foo/A= # 匹配顶层测试 "Foo",运行其匹配 "A=" 的子测试。 24 | go test -run /A=1 # 运行所有匹配 "A=1" 的子测试。 25 | ``` 26 | 子测试也可用于控制并行性。所有的子测试完成后,父测试才会完成。在这个例子中,所有的测试是相互并行运行的,当然也只是彼此之间,不包括定义在其他顶层测试的子测试: 27 | 28 | ```go 29 | func TestGroupedParallel(t *testing.T) { 30 | for _, tc := range tests { 31 | tc := tc // capture range variable 32 | t.Run(tc.Name, func(t *testing.T) { 33 | t.Parallel() 34 | ... 35 | }) 36 | } 37 | } 38 | ``` 39 | 在并行子测试完成之前,Run 方法不会返回,这提供了一种测试后清理的方法: 40 | 41 | ```go 42 | func TestTeardownParallel(t *testing.T) { 43 | // This Run will not return until the parallel tests finish. 44 | t.Run("group", func(t *testing.T) { 45 | t.Run("Test1", parallelTest1) 46 | t.Run("Test2", parallelTest2) 47 | t.Run("Test3", parallelTest3) 48 | }) 49 | // 50 | } 51 | ``` 52 | 53 | # 导航 # 54 | 55 | - 上一节:[testing - 基准测试](09.2.md) 56 | - 下一节:[testing - 运行并验证示例](09.4.md) 57 | -------------------------------------------------------------------------------- /chapter09/09.4.md: -------------------------------------------------------------------------------- 1 | # testing - 运行并验证示例 # 2 | 3 | testing 包除了测试,还提供了运行并验证示例的功能。示例,一方面是文档的效果,是关于某个功能的使用例子;另一方面,可以会被当做测试运行。 4 | 5 | 一个示例的例子如下: 6 | 7 | ```go 8 | func ExampleHello() { 9 | fmt.Println("Hello") 10 | // Output: Hello 11 | } 12 | ``` 13 | 14 | 如果 `Output` 改为:`Output: hello`,运行测试会失败,提示: 15 | 16 | ``` 17 | got: 18 | Hello 19 | want: 20 | hello 21 | ``` 22 | 23 | 一个示例函数以 Example 开头,如果示例函数包含以 "Output" 开头的行注释,在运行测试时,go 会将示例函数的输出和 "Output" 注释中的值做比较,就如上面的例子。 24 | 25 | 有时候,输出顺序可能不确定,比如循环输出 map 的值,那么可以使用 "Unordered output" 开头的注释。 26 | 27 | 如果示例函数没有 "Output" 注释,该示例函数只会被编译而不会被运行。 28 | 29 | ## 命名约定 30 | 31 | Go 语言通过大量的命名约定来简化工具的复杂度,规范代码的风格。对示例函数的命名有如下约定: 32 | 33 | - 包级别的示例函数,直接命名为 `func Example() { ... }` 34 | - 函数 F 的示例,命名为 `func ExampleF() { ... }` 35 | - 类型 T 的示例,命名为 `func ExampleT() { ... }` 36 | - 类型 T 上的 方法 M 的示例,命名为 `func ExampleT_M() { ... }` 37 | 38 | 有时,我们想要给 包/类型/函数/方法 提供多个示例,可以通过在示例函数名称后附加一个不同的后缀来实现,但这种后缀必须以小写字母开头,如: 39 | 40 | ``` 41 | func Example_suffix() { ... } 42 | func ExampleF_suffix() { ... } 43 | func ExampleT_suffix() { ... } 44 | func ExampleT_M_suffix() { ... } 45 | ``` 46 | 通常,示例代码会放在单独的示例文件中,命名为 `example_test.go`。 47 | 48 | # 导航 # 49 | 50 | - 上一节:[testing - 子测试与子基准测试](09.3.md) 51 | - 下一节:[testing - 其他功能](09.5.md) 52 | -------------------------------------------------------------------------------- /chapter09/09.5.md: -------------------------------------------------------------------------------- 1 | # testing - 其他功能 # 2 | 3 | 4 | # 导航 # 5 | 6 | - 上一节:[testing - testing - 运行并验证示例](09.4.md) 7 | - 下一节:[httptest - HTTP 测试辅助工具](09.6.md) 8 | -------------------------------------------------------------------------------- /chapter09/09.6.md: -------------------------------------------------------------------------------- 1 | # httptest - HTTP 测试辅助工具 # 2 | 3 | httptest 包提供了进行 HTTP 测试所需的设施。 4 | 5 | ## 测试 http.Handler ## 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /chapter10/10.0.md: -------------------------------------------------------------------------------- 1 | # 第十章 进程、线程和 goroutine # 2 | 3 | 本章将研究 Go 语言进程、线程和 goroutine,会涉及到操作系统关于进程、线程的知识,同时研究 Go 语言提供的相关标准库 API;goroutine 作为 Go 的一个核心特性,本章会重点介绍。 4 | 5 | 虽然标准库中能操作进程、线程和 goroutine 的API不多,但它们是深入学习、理解 Go 语言必须掌握的知识。本章从操作系统和 Go 源码层面深入探讨它们。 6 | 7 | # 导航 # 8 | 9 | - [第九章](/chapter09/09.0.md) 10 | - 下一节:[创建进程](10.1.md) 11 | -------------------------------------------------------------------------------- /chapter10/10.1.md: -------------------------------------------------------------------------------- 1 | # 10.1 创建进程 # 2 | 3 | `os` 包及其子包 `os/exec` 提供了创建进程的方法。 4 | 5 | 一般的,应该优先使用 `os/exec` 包。因为 `os/exec` 包依赖 `os` 包中关键创建进程的 API,为了便于理解,我们先探讨 `os` 包中和进程相关的部分。 6 | 7 | ## 进程的创建 8 | 9 | 在 Unix 中,创建一个进程,通过系统调用 `fork` 实现(及其一些变种,如 vfork、clone)。在 Go 语言中,Linux 下创建进程使用的系统调用是 `clone`。 10 | 11 | 很多时候,系统调用 `fork`、`execve`、`wait` 和 `exit` 会在一起出现。此处先简要介绍这 4 个系统调用及其典型用法。 12 | 13 | - fork:允许一进程(父进程)创建一新进程(子进程)。具体做法是,新的子进程几近于对父进程的翻版:子进程获得父进程的栈、数据段、堆和执行文本段的拷贝。可将此视为把父进程一分为二。 14 | - exit(status):终止一进程,将进程占用的所有资源(内存、文件描述符等)归还内核,交其进行再次分配。参数 `status` 为一整型变量,表示进程的退出状态。父进程可使用系统调用 `wait()` 来获取该状态。 15 | - wait(&status)目的有二:其一,如果进程尚未调用 `exit()` 终止,那么 `wait` 会挂起父进程直至子进程终止;其二,子进程的终止状态通过 `wait` 的 `status` 参数返回。 16 | - execve(pathname, argv, envp) 加载一个新程序(路径名为 pathname,参数列表为 argv,环境变量列表为 envp)到当前进程的内存。这将丢弃现存的程序文本段,并为新程序重新创建栈、数据段以及堆。通常将这一动作称为执行一个新程序。 17 | 18 | 在 Go 语言中,没有直接提供 `fork` 系统调用的封装,而是将 `fork` 和 `execve` 合二为一,提供了 `syscall.ForkExec`。如果想只调用 `fork`,得自己通过 `syscall.Syscall(syscall.SYS_FORK, 0, 0, 0)` 实现。 19 | 20 | ### Process 及其相关方法 21 | 22 | `os.Process` 存储了通过 `StartProcess` 创建的进程的相关信息。 23 | 24 | ``` 25 | type Process struct { 26 | Pid int 27 | handle uintptr // handle is accessed atomically on Windows 28 | isdone uint32 // process has been successfully waited on, non zero if true 29 | } 30 | ``` 31 | 一般通过 `StartProcess` 创建 `Process` 的实例,函数声明如下: 32 | 33 | `func StartProcess(name string, argv []string, attr *ProcAttr) (*Process, error)` 34 | 35 | 它使用提供的程序名、命令行参数、属性开始一个新进程。`StartProcess` 是一个低级别的接口。`os/exec` 包提供了高级别的接口,一般应该尽量使用 `os/exec` 包。如果出错,错误的类型会是`*PathError`。 36 | 37 | 其中的参数 `attr`,类型是 `ProcAttr` 的指针,用于为 `StartProcess` 创建新进程提供一些属性。定义如下: 38 | 39 | ``` 40 | type ProcAttr struct { 41 | // 如果 Dir 非空,子进程会在创建 Process 实例前先进入该目录。(即设为子进程的当前工作目录) 42 | Dir string 43 | // 如果 Env 非空,它会作为新进程的环境变量。必须采用 Environ 返回值的格式。 44 | // 如果 Env 为 nil,将使用 Environ 函数的返回值。 45 | Env []string 46 | // Files 指定被新进程继承的打开文件对象。 47 | // 前三个绑定为标准输入、标准输出、标准错误输出。 48 | // 依赖底层操作系统的实现可能会支持额外的文件对象。 49 | // nil 相当于在进程开始时关闭的文件对象。 50 | Files []*File 51 | // 操作系统特定的创建属性。 52 | // 注意设置本字段意味着你的程序可能会执行异常甚至在某些操作系统中无法通过编译。这时候可以通过为特定系统设置。 53 | // 看 syscall.SysProcAttr 的定义,可以知道用于控制进程的相关属性。 54 | Sys *syscall.SysProcAttr 55 | } 56 | ``` 57 | 58 | `FindProcess` 可以通过 `pid` 查找一个运行中的进程。该函数返回的 `Process` 对象可以用于获取关于底层操作系统进程的信息。在 Unix 系统中,此函数总是成功,即使 `pid` 对应的进程不存在。 59 | 60 | `func FindProcess(pid int) (*Process, error)` 61 | 62 | `Process` 提供了四个方法:`Kill`、`Signal`、`Wait` 和 `Release`。其中 `Kill` 和 `Signal` 跟信号相关,而 `Kill` 实际上就是调用 `Signal`,发送了 `SIGKILL` 信号,强制进程退出,关于信号,后续章节会专门讲解。 63 | 64 | `Release` 方法用于释放 `Process` 对象相关的资源,以便将来可以被再使用。该方法只有在确定没有调用 `Wait` 时才需要调用。Unix 中,该方法的内部实现只是将 `Process` 的 `pid` 置为 -1。 65 | 66 | 我们重点看看 `Wait` 方法。 67 | 68 | `func (p *Process) Wait() (*ProcessState, error)` 69 | 70 | 在多进程应用程序的设计中,父进程需要知道某个子进程何时改变了状态 —— 子进程终止或因收到信号而停止。`Wait` 方法就是一种用于监控子进程的技术。 71 | 72 | `Wait` 方法阻塞直到进程退出,然后返回一个 `ProcessState` 描述进程的状态和可能的错误。`Wait` 方法会释放绑定到 `Process` 的所有资源。在大多数操作系统中,`Process` 必须是当前进程的子进程,否则会返回错误。 73 | 74 | 看看 `ProcessState` 的内部结构: 75 | 76 | ``` 77 | type ProcessState struct { 78 | pid int // The process's id. 79 | status syscall.WaitStatus // System-dependent status info. 80 | rusage *syscall.Rusage 81 | } 82 | ``` 83 | 84 | `ProcessState` 保存了 `Wait` 函数报告的某个进程的信息。`status` 记录了状态原因,通过 `syscal.WaitStatus` 类型定义的方法可以判断: 85 | 86 | - Exited():是否正常退出,如调用 `os.Exit`; 87 | - Signaled():是否收到未处理信号而终止; 88 | - CoreDump():是否收到未处理信号而终止,同时生成 coredump 文件,如 SIGABRT; 89 | - Stopped():是否因信号而停止(SIGSTOP); 90 | - Continued():是否因收到信号 SIGCONT 而恢复; 91 | 92 | `syscal.WaitStatus` 还提供了其他一些方法,比如获取退出状态、信号、停止信号和中断(Trap)原因。 93 | 94 | 因为 Linux 下 `Wait` 的内部实现使用的是 `wait4` 系统调用,因此,`ProcessState` 中包含了 `rusage`,用于统计进程的各类资源信息。一般情况下,`syscall.Rusage` 中定义的信息都用不到,如果实际中需要使用,可以查阅 Linux 系统调用 `getrusage` 获得相关说明(`getrusage(2)`)。 95 | 96 | `ProcessState` 结构内部字段是私有的,我们可以通过它提供的方法来获得一些基本信息,比如:进程是否退出、Pid、进程是否是正常退出、进程CPU时间、用户时间等等。 97 | 98 | 实现类似 Linux 中 `time` 命令的功能: 99 | 100 | ``` 101 | package main 102 | 103 | import ( 104 | "fmt" 105 | "os" 106 | "os/exec" 107 | "path/filepath" 108 | "time" 109 | ) 110 | 111 | func main() { 112 | if len(os.Args) < 2 { 113 | fmt.Printf("Usage: %s [command]\n", os.Args[0]) 114 | os.Exit(1) 115 | } 116 | 117 | cmdName := os.Args[1] 118 | if filepath.Base(os.Args[1]) == os.Args[1] { 119 | if lp, err := exec.LookPath(os.Args[1]); err != nil { 120 | fmt.Println("look path error:", err) 121 | os.Exit(1) 122 | } else { 123 | cmdName = lp 124 | } 125 | } 126 | 127 | procAttr := &os.ProcAttr{ 128 | Files: []*os.File{os.Stdin, os.Stdout, os.Stderr}, 129 | } 130 | 131 | cwd, err := os.Getwd() 132 | if err != nil { 133 | fmt.Println("look path error:", err) 134 | os.Exit(1) 135 | } 136 | 137 | start := time.Now() 138 | process, err := os.StartProcess(cmdName, []string{cwd}, procAttr) 139 | if err != nil { 140 | fmt.Println("start process error:", err) 141 | os.Exit(2) 142 | } 143 | 144 | processState, err := process.Wait() 145 | if err != nil { 146 | fmt.Println("wait error:", err) 147 | os.Exit(3) 148 | } 149 | 150 | fmt.Println() 151 | fmt.Println("real", time.Now().Sub(start)) 152 | fmt.Println("user", processState.UserTime()) 153 | fmt.Println("system", processState.SystemTime()) 154 | } 155 | 156 | // go build main.go && ./main ls 157 | // Output: 158 | // 159 | // real 4.994739ms 160 | // user 1.177ms 161 | // system 2.279ms 162 | ``` 163 | 164 | ## 运行外部命令 165 | 166 | 通过 `os` 包可以做到运行外部命令,如前面的例子。不过,Go 标准库为我们封装了更好用的包: `os/exec`,运行外部命令,应该优先使用它,它包装了 `os.StartProcess` 函数以便更容易的重定向标准输入和输出,使用管道连接I/O,以及作其它的一些调整。 167 | 168 | ### 查找可执行程序 169 | 170 | `exec.LookPath` 函数在 `PATH` 指定目录中搜索可执行程序,如 `file` 中有 `/`,则只在当前目录搜索。该函数返回完整路径或相对于当前路径的一个相对路径。 171 | 172 | `func LookPath(file string) (string, error)` 173 | 174 | 如果在 `PATH` 中没有找到可执行文件,则返回 `exec.ErrNotFound`。 175 | 176 | ### Cmd 及其相关方法 177 | 178 | `Cmd` 结构代表一个正在准备或者在执行中的外部命令,调用了 `Run`、`Output` 或 `CombinedOutput` 后,`Cmd` 实例不能被重用。 179 | 180 | ``` 181 | type Cmd struct { 182 | // Path 是将要执行的命令路径。 183 | // 该字段不能为空(也是唯一一个不能为空的字段),如为相对路径会相对于 Dir 字段。 184 | // 通过 Command 初始化时,会在需要时调用 LookPath 获得完整的路径。 185 | Path string 186 | 187 | // Args 存放着命令的参数,第一个值是要执行的命令(Args[0]);如果为空切片或者nil,使用 {Path} 运行。 188 | // 一般情况下,Path 和 Args 都应被 Command 函数设定。 189 | Args []string 190 | 191 | // Env 指定进程的环境变量,如为 nil,则使用当前进程的环境变量,即 os.Environ(),一般就是当前系统的环境变量。 192 | Env []string 193 | 194 | // Dir 指定命令的工作目录。如为空字符串,会在调用者的进程当前工作目录下执行。 195 | Dir string 196 | 197 | // Stdin 指定进程的标准输入,如为 nil,进程会从空设备读取(os.DevNull) 198 | // 如果 Stdin 是 *os.File 的实例,进程的标准输入会直接指向这个文件 199 | // 否则,会在一个单独的 goroutine 中从 Stdin 中读数据,然后将数据通过管道传递到该命令中(也就是从 Stdin 读到数据后,写入管道,该命令可以从管道读到这个数据)。在 goroutine 停止数据拷贝之前(停止的原因如遇到EOF或其他错误,或管道的 write 端错误),Wait 方法会一直堵塞。 200 | Stdin io.Reader 201 | 202 | // Stdout 和 Stderr 指定进程的标准输出和标准错误输出。 203 | // 如果任一个为 nil,Run 方法会将对应的文件描述符关联到空设备(os.DevNull) 204 | // 如果两个字段相同,同一时间最多有一个线程可以写入。 205 | Stdout io.Writer 206 | Stderr io.Writer 207 | 208 | // ExtraFiles 指定额外被新进程继承的已打开文件,不包括标准输入、标准输出、标准错误输出。 209 | // 如果本字段非 nil,其中的元素 i 会变成文件描述符 3+i。 210 | // 211 | // BUG: 在OS X 10.6系统中,子进程可能会继承不期望的文件描述符。 212 | // http://golang.org/issue/2603 213 | ExtraFiles []*os.File 214 | 215 | // SysProcAttr 提供可选的、各操作系统特定的 sys 属性。 216 | // Run 方法会将它作为 os.ProcAttr 的 Sys 字段传递给os.StartProcess 函数。 217 | SysProcAttr *syscall.SysProcAttr 218 | 219 | // Process 是底层的,只执行一次的进程。 220 | Process *os.Process 221 | 222 | // ProcessState 包含一个已经存在的进程的信息,只有在调用 Wait 或 Run 后才可用。 223 | ProcessState *os.ProcessState 224 | } 225 | ``` 226 | **Command** 227 | 228 | 一般的,应该通过 `exec.Command` 函数产生 `Cmd` 实例: 229 | 230 | `func Command(name string, arg ...string) *Cmd` 231 | 232 | 该函数返回一个 `*Cmd`,用于使用给出的参数执行 `name` 指定的程序。返回的 `*Cmd` 只设定了 `Path` 和 `Args` 两个字段。 233 | 234 | 如果 `name` 不含路径分隔符,将使用 `LookPath` 获取完整路径;否则直接使用 `name`。参数 `arg` 不应包含命令名。 235 | 236 | 得到 `*Cmd` 实例后,接下来一般有两种写法: 237 | 238 | 1. 调用 `Start()`,接着调用 `Wait()`,然后会阻塞直到命令执行完成; 239 | 2. 调用 `Run()`,它内部会先调用 `Start()`,接着调用 `Wait()`; 240 | 241 | **Start** 242 | 243 | `func (c *Cmd) Start() error` 244 | 245 | 开始执行 `c` 包含的命令,但并不会等待该命令完成即返回。`Wait` 方法会返回命令的退出状态码并在命令执行完后释放相关的资源。内部调用 `os.StartProcess`,执行 `forkExec`。 246 | 247 | **Wait** 248 | 249 | `func (c *Cmd) Wait() error` 250 | 251 | `Wait` 会阻塞直到该命令执行完成,该命令必须是先通过 `Start` 执行。 252 | 253 | 如果命令成功执行,stdin、stdout、stderr 数据传递没有问题,并且返回状态码为 0,方法的返回值为 nil;如果命令没有执行或者执行失败,会返回 `*ExitError` 类型的错误;否则返回的 error 可能是表示 I/O 问题。 254 | 255 | 如果 `c.Stdin` 不是 `*os.File` 类型,`Wait` 会等待,直到数据从 `c.Stdin` 拷贝到进程的标准输入。 256 | 257 | `Wait` 方法会在命令返回后释放相关的资源。 258 | 259 | **Output** 260 | 261 | 除了 `Run()` 是 `Start`+`Wait` 的简便写法,`Output()` 更是 `Run()` 的简便写法,外加获取外部命令的输出。 262 | 263 | `func (c *Cmd) Output() ([]byte, error)` 264 | 265 | 它要求 `c.Stdout` 必须是 `nil`,内部会将 `bytes.Buffer` 赋值给 `c.Stdout`,在 `Run()` 成功返回后,会将 `Buffer` 的结果返回(`stdout.Bytes()`)。 266 | 267 | **CombinedOutput** 268 | 269 | `Output()` 只返回 `Stdout` 的结果,而 `CombinedOutput` 组合 `Stdout` 和 `Stderr` 的输出,即 `Stdout` 和 `Stderr` 都赋值为同一个 `bytes.Buffer`。 270 | 271 | 272 | **StdoutPipe、StderrPipe 和 StdinPipe** 273 | 274 | 除了上面介绍的 `Output` 和 `CombinedOutput` 直接获取命令输出结果外,还可以通过 `StdoutPipe` 返回 `io.ReadCloser` 来获取输出;相应的 `StderrPipe` 得到错误信息;而 `StdinPipe` 则可以往命令写入数据。 275 | 276 | `func (c *Cmd) StdoutPipe() (io.ReadCloser, error)` 277 | 278 | `StdoutPipe` 方法返回一个在命令 `Start` 执行后与命令标准输出关联的管道。`Wait` 方法会在命令结束后会关闭这个管道,所以一般不需要手动关闭该管道。但是在从管道读取完全部数据之前调用 `Wait` 出错了,则必须手动关闭。 279 | 280 | `func (c *Cmd) StderrPipe() (io.ReadCloser, error)` 281 | 282 | `StderrPipe` 方法返回一个在命令 `Start` 执行后与命令标准错误输出关联的管道。`Wait` 方法会在命令结束后会关闭这个管道,一般不需要手动关闭该管道。但是在从管道读取完全部数据之前调用 `Wait` 出错了,则必须手动关闭。 283 | 284 | `func (c *Cmd) StdinPipe() (io.WriteCloser, error)` 285 | 286 | `StdinPipe` 方法返回一个在命令 `Start` 执行后与命令标准输入关联的管道。`Wait` 方法会在命令结束后会关闭这个管道。必要时调用者可以调用 `Close` 方法来强行关闭管道。例如,标准输入已经关闭了,命令执行才完成,这时调用者需要显示关闭管道。 287 | 288 | 因为 `Wait` 之后,会将管道关闭,所以,要使用这些方法,只能使用 `Start`+`Wait` 组合,不能使用 `Run`。 289 | 290 | ### 执行外部命令示例 291 | 292 | 前面讲到,通过 `Cmd` 实例后,有两种方式运行命令。有时候,我们不只是简单的运行命令,还希望能控制命令的输入和输出。通过上面的 API 介绍,控制输入输出有几种方法: 293 | 294 | - 得到 `Cmd ` 实例后,直接给它的字段 `Stdin`、`Stdout` 和 `Stderr` 赋值; 295 | - 通过 `Output` 或 `CombinedOutput` 获得输出; 296 | - 通过带 `Pipe` 后缀的方法获得管道,用于输入或输出; 297 | 298 | #### 直接赋值 `Stdin`、`Stdout` 和 `Stderr` 299 | 300 | ``` 301 | func FillStd(name string, arg ...string) ([]byte, error) { 302 | cmd := exec.Command(name, arg...) 303 | var out = new(bytes.Buffer) 304 | 305 | cmd.Stdout = out 306 | cmd.Stderr = out 307 | 308 | err := cmd.Run() 309 | if err != nil { 310 | return nil, err 311 | } 312 | 313 | return out.Bytes(), nil 314 | } 315 | ``` 316 | 317 | #### 使用 `Output` 318 | 319 | ``` 320 | func UseOutput(name string, arg ...string) ([]byte, error) { 321 | return exec.Command(name, arg...).Output() 322 | } 323 | ``` 324 | 325 | #### 使用 Pipe 326 | 327 | ``` 328 | func UsePipe(name string, arg ...string) ([]byte, error) { 329 | cmd := exec.Command(name, arg...) 330 | stdout, err := cmd.StdoutPipe() 331 | if err != nil { 332 | return nil, err 333 | } 334 | 335 | if err = cmd.Start(); err != nil { 336 | return nil, err 337 | } 338 | 339 | var out = make([]byte, 0, 1024) 340 | for { 341 | tmp := make([]byte, 128) 342 | n, err := stdout.Read(tmp) 343 | out = append(out, tmp[:n]...) 344 | if err != nil { 345 | break 346 | } 347 | } 348 | 349 | if err = cmd.Wait(); err != nil { 350 | return nil, err 351 | } 352 | 353 | return out, nil 354 | } 355 | ``` 356 | 357 | 完整代码见 [os_exec](/code/src/chapter10/os_exec.go)。 358 | 359 | ## 进程终止 360 | 361 | `os.Exit()` 函数会终止当前进程,对应的系统调用不是 `_exit`,而是 `exit_group`。 362 | 363 | `func Exit(code int)` 364 | 365 | `Exit` 让当前进程以给出的状态码 `code` 退出。一般来说,状态码 0 表示成功,非 0 表示出错。进程会立刻终止,defer 的函数不会被执行。 366 | 367 | # 导航 # 368 | 369 | - [第十章](/chapter10/10.0.md) 370 | - 下一节:[进程属性和控制](10.2.md) 371 | 372 | -------------------------------------------------------------------------------- /chapter10/10.2.md: -------------------------------------------------------------------------------- 1 | # 10.2 进程属性和控制 # 2 | 3 | 每个进程都有一些属性,`os` 包提供了一些函数可以获取进程属性。 4 | 5 | ## 进程 ID 6 | 7 | 每个进程都会有一个进程ID,可以通过 `os.Getpid` 获得。同时,每个进程都有创建自己的父进程,通过 `os.Getppid` 获得。 8 | 9 | ## 进程凭证 10 | 11 | Unix 中进程都有一套数字表示的用户 ID(UID) 和组 ID(GID),有时也将这些 ID 称之为进程凭证。Windows 下总是 -1。 12 | 13 | ### 实际用户 ID 和实际组 ID 14 | 15 | 实际用户 ID(real user ID)和实际组 ID(real group ID)确定了进程所属的用户和组。登录 shell 从 `/etc/passwd` 文件读取用户 ID 和组 ID。当创建新进程时(如 shell 执行程序),将从其父进程中继承这些 ID。 16 | 17 | 可通过 `os.Getuid()` 和 `os.Getgid()` 获取当前进程的实际用户 ID 和实际组 ID; 18 | 19 | ### 有效用户 ID 和有效组 ID 20 | 21 | 大多数 Unix 实现中,当进程尝试执行各种操作(即系统调用)时,将结合有效用户 ID、有效组 ID,连同辅助组 ID 一起来确定授予进程的权限。内核还会使用有效用户 ID 来决定一个进程是否能向另一个进程发送信号。 22 | 23 | 有效用户 ID 为 0(root 的用户 ID)的进程拥有超级用户的所有权限。这样的进程又称为特权级进程(privileged process)。某些系统调用只能由特权级进程执行。 24 | 25 | 可通过 `os.Geteuid()` 和 `os.Getegid()` 获取当前进程的有效用户 ID(effective user ID)和有效组 ID(effectvie group ID)。 26 | 27 | 通常,有效用户 ID 及组 ID 与其相应的实际 ID 相等,但有两种方法能够致使二者不同。一是使用相关系统调用;二是执行 set-user-ID 和 set-group-ID 程序。 28 | 29 | ### Set-User-ID 和 Set-Group-ID 程序 30 | 31 | `set-user-ID` 程序会将进程的有效用户 ID 置为可执行文件的用户ID(属主),从而获得常规情况下并不具有的权限。`set-group-ID` 程序对进程有效组 ID 实现类似任务。(有时也将这程序简称为 set-UID 程序和 set-GID 程序。) 32 | 33 | 与其他文件一样,可执行文件的用户 ID 和组 ID 决定了该文件的所有权。在 [6.1 os — 平台无关的操作系统功能实现](chapter06/06.1.md) 中提到过,文件还拥有两个特别的权限位 set-user-ID 位和 set-group-ID 位,可以使用 `os.Chmod` 修改这些权限位(非特权用户进程只能修改其自身文件,而特权用户进程能修改任何文件)。 34 | 35 | 文件设置了 set-user-ID 位后,`ls -l` 显示文件后,会在属主用户执行权限字段上看到字母 s(有执行权限时) 或 S(无执行权限时);相应的 set-group-ID 则是在组用户执行位上看到 s 或 S。 36 | 37 | 当运行 set-user-ID 程序时,内核会将进程的有效用户 ID 设置为可执行文件的用户 ID。set-group-ID 程序对进程有效组 ID 的操作与之类似。通过这种方法修改进程的有效用户 ID 或组 ID,能够使进程(换言之,执行该程序的用户)获得常规情况下所不具有的权限。例如,如果一个可执行文件的属主为 root,且为此程序设置了 set-user-ID 权限位,那么当运行该程序时,进程会取得超级用户权限。 38 | 39 | 也可以利用程序的 set-user-ID 和 set-group-ID 机制,将进程的有效 ID 修改为 root 之外的其他用户。例如,为提供一个受保护文件的访问,可采用如下方案:创建一个具有对该文件访问权限的专有用户(组)ID,然后再创建一个 set-user-ID(set-group-ID)程序,将进程有效用户(组)ID 变更为这个专用 ID。这样,无需拥有超级用户的所有权限,程序就能访问该文件。 40 | 41 | Linux 系统中经常使用的 set-user-ID 程序,如 passwd。 42 | 43 | #### 测试 set-user-ID 程序 44 | 45 | 在 Linux 的某个目录下,用 root 账号创建一个文件: 46 | 47 | `echo "This is my shadow, studygolang." > my_shadow.txt` 48 | 49 | 然后将所有权限都去掉:`chmod 0 my_shadow.txt`。 ls -l 结果类似如下: 50 | 51 | `---------- 1 root root 32 6月 24 17:31 my_shadow.txt` 52 | 53 | 这时,如果非 root 用户是无法查看文件内容的。 54 | 55 | 接着,用 root 账号创建一个 `main.go` 文件,内容如下: 56 | 57 | ``` 58 | package main 59 | 60 | import ( 61 | "fmt" 62 | "io/ioutil" 63 | "log" 64 | "os" 65 | ) 66 | 67 | func main() { 68 | file, err := os.Open("my_shadow.txt") 69 | if err != nil { 70 | log.Fatal(err) 71 | } 72 | defer file.Close() 73 | 74 | data, err := ioutil.ReadAll(file) 75 | if err != nil { 76 | log.Fatal(err) 77 | } 78 | 79 | fmt.Printf("my_shadow:%s\n", data) 80 | } 81 | ``` 82 | 就是简单地读取 `my_shadow` 文件内容。`go build main.go` 后,生成的 `main` 可执行文件,权限是:`-rwxrwxr-x`。 83 | 84 | 这时,切换到非 root 用户,执行 `./main`,会输出: 85 | 86 | `open my_shadow.txt: permission denied` 87 | 88 | 因为这时的 `main` 程序生成的进程有效用户 ID 是当前用户的(非 root)。 89 | 90 | 接着,给 `main` 设置 `set-user-ID` 位:`chmod u+s main`,权限变为 `-rwsrwxr-x`,非 root 下再次执行 `./main`,输出: 91 | 92 | `my_shadow:This is my shadow, studygolang.` 93 | 94 | 因为设置了 `set-user-ID` 位,这时 `main` 程序生成的进程有效用户是 `main` 文件的属主,即 root 的 ID,因此有权限读 `my_shadow.txt`。 95 | 96 | ### 修改进程的凭证 97 | 98 | `os` 包没有提供相应的功能修改进程的凭证,在 `syscall` 包对这些系统调用进行了封装。因为 [https://golang.org/s/go1.4-syscall](https://golang.org/s/go1.4-syscall),用户程序不建议直接使用该包,应该使用 `golang.org/x/sys` 包代替。 99 | 100 | 该包提供了修改进程各种 ID 的系统调用封装,这里不一一介绍。 101 | 102 | 此外,`os` 还提供了获取辅助组 ID 的函数:`os.Getgroups()`。 103 | 104 | ### 操作系统用户 105 | 106 | 包 `os/user` 允许通过名称或 ID 查询用户账号。用户结构定义如下: 107 | 108 | ``` 109 | type User struct { 110 | Uid string // user id 111 | Gid string // primary group id 112 | Username string 113 | Name string 114 | HomeDir string 115 | } 116 | ``` 117 | `User` 代表一个用户帐户。 118 | 119 | 在 POSIX 系统中 Uid 和 Gid 字段分别包含代表 uid 和 gid 的十进制数字。在 Windows 系统中 Uid 和 Gid 包含字符串格式的安全标识符(SID)。在 Plan 9 系统中,Uid、Gid、Username 和 Name 字段是 /dev/user 的内容。 120 | 121 | `Current` 函数可以获取当前用户账号。而 `Lookup` 和 `LookupId` 则分别根据用户名和用户 ID 查询用户。如果对应的用户不存在,则返回 `user.UnknownUserError ` 或 `user.UnknownUserIdError`。 122 | 123 | ``` 124 | package main 125 | 126 | import ( 127 | "fmt" 128 | "os/user" 129 | ) 130 | 131 | func main() { 132 | fmt.Println(user.Current()) 133 | fmt.Println(user.Lookup("xuxinhua")) 134 | fmt.Println(user.LookupId("0")) 135 | } 136 | 137 | // Output: 138 | // &{502 502 xuxinhua /home/xuxinhua} 139 | // &{502 502 xuxinhua /home/xuxinhua} 140 | // &{0 0 root root /root} 141 | ``` 142 | 143 | ## 进程的当前工作目录 144 | 145 | 一个进程的当前工作目录(current working directory)定义了该进程解析相对路径名的起点。新进程的当前工作目录继承自其父进程。 146 | 147 | `func Getwd() (dir string, err error)` 148 | 149 | `Getwd` 返回一个对应当前工作目录的根路径。如果当前目录可以经过多条路径抵达(比如符号链接),`Getwd` 会返回其中一个。对应系统调用:`getcwd`。 150 | 151 | `func Chdir(dir string) error` 152 | 153 | 相应的,`Chdir` 将当前工作目录修改为 `dir` 指定的目录。如果出错,会返回 `*PathError` 类型的错误。对应系统调用 `chdir`。 154 | 155 | 另外,`os.File` 有一个方法:`Chdir`,对应系统调用 `fchidr`(以文件描述符为参数),也可以改变当前工作目录。 156 | 157 | ## 改变进程的根目录 158 | 159 | 每个进程都有一个根目录,该目录是解释绝对路径(即那些以/开始的目录)时的起点。默认情况下,这是文件系统的真是根目录。新进程从其父进程处继承根目录。有时可能需要改变一个进程的根目录(比如 ftp 服务就是一个典型的例子)。系统调用 `chroot` 能改变一个进程的根目录,Go 中对应的封装在 `syscall.Chroot`。 160 | 161 | 除此之外,在 `fork` 子进程时,可以通过给 `syscall.SysProcAttr` 结构的 `Chroot` 字段指定一个路径,来初始化子进程的根目录。 162 | 163 | ## 进程环境列表 164 | 165 | 每个进程都有与其相关的称之为环境列表(environment list)的字符串数组,或简称环境(environment)。其中每个字符串都以 名称=值(name=value)形式定义。因此,环境是“名称-值”的成对集合,可存储任何信息。常将列表中的名称称为环境变量(environment variables)。 166 | 167 | 新进程在创建之时,会继承其父进程的环境副本。这是一种原始的进程间通信方式,却颇为常用。环境(environment)提供了将信息和父进程传递给子进程的方法。创建后,父子进程的环境相互独立,互不影响。 168 | 169 | 环境变量的常见用途之一是在 shell 中,通过在自身环境中放置变量值,shell 就可确保把这些值传递给其所创建的进程,并以此来执行用户命令。 170 | 171 | 在程序中,可以通过 `os.Environ` 获取环境列表: 172 | 173 | `func Environ() []string` 174 | 175 | 返回的 `[]string` 中每个元素是 `key=value` 的形式。 176 | 177 | `func Getenv(key string) string` 178 | 179 | `Getenv` 检索并返回名为 `key` 的环境变量的值。如果不存在该环境变量会返回空字符串。有时候,可能环境变量存在,只是值刚好是空。为了区分这种情况,提供了另外一个函数 `LookupEnv()`: 180 | 181 | `func LookupEnv(key string) (string, bool)` 182 | 183 | 如果变量名存在,第二个参数返回 `true`,否则返回 `false`。 184 | 185 | `func Setenv(key, value string) error` 186 | 187 | `Setenv` 设置名为 `key` 的环境变量,值为 `value`。如果出错会返回该错误。(如果值之前存在,会覆盖) 188 | 189 | `func Unsetenv(key string) error` 190 | 191 | `Unsetenv` 删除名为 `key` 的环境变量。 192 | 193 | `func Clearenv()` 194 | 195 | `Clearenv` 删除所有环境变量。 196 | 197 | ``` 198 | package main 199 | 200 | import ( 201 | "fmt" 202 | "os" 203 | ) 204 | 205 | func main() { 206 | fmt.Println("The num of environ:", len(os.Environ())) 207 | godebug, ok := os.LookupEnv("GODEBUG") 208 | if ok { 209 | fmt.Println("GODEBUG==", godebug) 210 | } else { 211 | fmt.Println("GODEBUG not exists!") 212 | os.Setenv("GODEBUG", "gctrace=1") 213 | fmt.Println("after setenv:", os.Getenv("GODEBUG")) 214 | } 215 | 216 | os.Clearenv() 217 | fmt.Println("clearenv, the num:", len(os.Environ())) 218 | } 219 | 220 | // Output: 221 | // The num of environ: 25 222 | // GODEBUG not exists! 223 | // after setenv: gctrace=1 224 | // clearenv, the num: 0 225 | ``` 226 | 227 | 另外,`ExpandEnv` 和 `Getenv` 功能类似,不过,前者使用变量方式,如: 228 | 229 | os.ExpandEnv("$GODEBUG") 和 os.Getenv("GODEBUG") 是一样的。 230 | 231 | 实际上,`os.ExpandEnv` 调用的是 `os.Expand(s, os.Getenv)`。 232 | 233 | `func Expand(s string, mapping func(string) string) string` 234 | 235 | `Expand` 能够将 ${var} 或 $var 形式的变量,经过 mapping 处理,得到结果。 236 | 237 | # 导航 # 238 | 239 | - 上一节:[创建进程](10.1.md) 240 | - 下一节:[进程间通信](10.3.md) 241 | 242 | -------------------------------------------------------------------------------- /chapter10/10.3.md: -------------------------------------------------------------------------------- 1 | # 10.3 线程 # 2 | 3 | 与进程类似,线程是允许应用程序并发执行多个任务的一种机制。一个进程可以包含多个线程。同一个程序中的所有线程均会独立执行相同程序,且共享同一份全局内存区域。 4 | 5 | 同一进程中的多个线程可以并发执行。在多处理器环境下,多个线程可以同时并行。如果一个线程因等待 I/O 操作而遭阻塞,那么其他线程依然可以继续运行。 6 | 7 | 在 Linux 中,通过系统调用 `clone()` 来实现线程的。从前面的介绍,我们知道,该系统调用也可以用来创建进程。实际上,从内核的角度来说,它并没有线程这个概念。Linux 把所有的线程都当作进程来实现。内核并没有准备特别的调度算法或是定义特别的数据结构来表征线程。相反,线程仅仅被视为一个使用某些共享资源的进程。所以,在内核中,它看起来就是一个普通的进程(只是该进程和其他一些进程共享某些资源,如地址空间)。 8 | 9 | 在 Go 中,通过 `clone()` 系统调用来创建线程,其中的 `clone_flags` 为: 10 | 11 | ``` 12 | cloneFlags = _CLONE_VM | /* share memory */ 13 | _CLONE_FS | /* share cwd, etc */ 14 | _CLONE_FILES | /* share fd table */ 15 | _CLONE_SIGHAND | /* share sig handler table */ 16 | _CLONE_THREAD /* revisit - okay for now */ 17 | ``` 18 | 19 | 也就是说,父子俩共享了地址空间(_CLONE_VM)、文件系统资源(_CLONE_FS)、文件描述符(_CLONE_FILES)和信号处理程序(_CLONE_SIGHAND)。而 `_CLONE_THREAD` 则会将父子进程放入相同的线程组。这样一来,新建的进程和父进程都叫做线程。 20 | 21 | 22 | 23 | 24 | # 导航 # 25 | 26 | - 上一节:[进程属性和控制](10.2.md) 27 | - 下一节:[进程间通信](10.4.md) 28 | 29 | -------------------------------------------------------------------------------- /chapter10/10.4.md: -------------------------------------------------------------------------------- 1 | # 10.3 进程间通信 # 2 | 3 | 进程之间用来相互通讯和同步 4 | 5 | # 导航 # 6 | 7 | - 上一节:[创建进程](10.1.md) 8 | - 下一节:[进程间通信](10.3.md) 9 | 10 | -------------------------------------------------------------------------------- /chapter13/13.0.md: -------------------------------------------------------------------------------- 1 | # 第十三章 应用构建 与 debug # 2 | 3 | 本章我们来研究应用程序构建相关的标准库,包括用于命令行参数解析的 flag 包,简单的日志记录包 log(以及 syslog),公共变量标准接口 expvar 包,以及运行时的调试工具 runtime/debug 包。 4 | 5 | # 导航 # 6 | 7 | - [第十二章](/chapter12/12.0.md) 8 | - 下一节:[flag - 命令行参数解析](13.1.md) 9 | -------------------------------------------------------------------------------- /chapter13/13.3.md: -------------------------------------------------------------------------------- 1 | # 13.3 expvar - 公共变量的标准化接口 # 2 | 3 | expvar 挺简单的,然而,它也是很有用的。但不幸的是,貌似了解它的人不多。来自 godoc.org 的数据表明,没有多少人知道这个包。截止目前(2017-6-18),该包被公开的项目 import 2207 次,相比较而言,连 image 包都被 import 3491 次之多。 4 | 5 | 如果你看到了这里,希望以后你的项目中能使用上 expvar 这个包。 6 | 7 | ## 包简介 ## 8 | 9 | 包 expvar 为公共变量提供了一个标准化的接口,如服务器中的操作计数器。它以 JSON 格式通过 `/debug/vars` 接口以 HTTP 的方式公开这些公共变量。 10 | 11 | 设置或修改这些公共变量的操作是原子的。 12 | 13 | 除了为程序增加 HTTP handler,此包还注册以下变量: 14 | 15 | cmdline os.Args 16 | memstats runtime.Memstats 17 | 18 | 导入该包有时只是为注册其 HTTP handler 和上述变量。 要以这种方式使用,请将此包通过如下形式引入到程序中: 19 | 20 | import _ "expvar" 21 | 22 | ## 例子 ## 23 | 24 | 在继续介绍此包的详细信息之前,我们演示使用 expvar 包可以做什么。以下代码创建一个在监听 8080 端口的 HTTP 服务器。每个请求到达hander() 后,在向访问者发送响应消息之前增加计数器。 25 | 26 | package main 27 | 28 | import ( 29 | "expvar" 30 | "fmt" 31 | "net/http" 32 | ) 33 | 34 | var visits = expvar.NewInt("visits") 35 | 36 | func handler(w http.ResponseWriter, r *http.Request) { 37 | visits.Add(1) 38 | fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:]) 39 | } 40 | 41 | func main() { 42 | http.HandleFunc("/", handler) 43 | http.ListenAndServe(":8080", nil) 44 | } 45 | 46 | 导入 expvar 包后,它将为 `http.DefaultServeMux` 上的 PATH `/debug/vars` 注册一个处理函数。此处理程序返回已在 expvar 包中注册的所有公共变量。运行代码并访问 `http://localhost:8080/debug/vars`,您将看到如下所示的内容(输出被截断以增加可读性): 47 | 48 | { 49 | "cmdline": [ 50 | "/var/folders/qv/2jztyc09357ddtxn_bvgh8j00000gn/T/go-build146580631/command-line-arguments/_obj/exe/test" 51 | ], 52 | "memstats": { 53 | "Alloc": 414432, 54 | "TotalAlloc": 414432, 55 | "Sys": 3084288, 56 | "Lookups": 13, 57 | "Mallocs": 5111, 58 | "Frees": 147, 59 | "HeapAlloc": 414432, 60 | "HeapSys": 1703936, 61 | "HeapIdle": 835584, 62 | "HeapInuse": 868352, 63 | "HeapReleased": 0, 64 | "HeapObjects": 4964, 65 | "StackInuse": 393216, 66 | "StackSys": 393216, 67 | "MSpanInuse": 15504, 68 | "MSpanSys": 16384, 69 | "MCacheInuse": 4800, 70 | "MCacheSys": 16384, 71 | "BuckHashSys": 2426, 72 | "GCSys": 137216, 73 | "OtherSys": 814726, 74 | "NextGC": 4473924, 75 | "LastGC": 0, 76 | "PauseTotalNs": 0, 77 | "PauseNs": [ 78 | 0, 79 | 0, 80 | ], 81 | "PauseEnd": [ 82 | 0, 83 | 0 84 | ], 85 | "GCCPUFraction": 0, 86 | "EnableGC": true, 87 | "DebugGC": false, 88 | "BySize": [ 89 | { 90 | "Size": 16640, 91 | "Mallocs": 0, 92 | "Frees": 0 93 | }, 94 | { 95 | "Size": 17664, 96 | "Mallocs": 0, 97 | "Frees": 0 98 | } 99 | ] 100 | }, 101 | "visits": 0 102 | } 103 | 104 | 信息真不少。这是因为默认情况下该包注册了 `os.Args` 和 `runtime.Memstats` 两个指标。因为我们还没有访问到增加 visits 的路径,所以它的值仍然为 0。现在通过访问 `http:// localhost:8080/golang` 来增加计数器,然后返回。计数器不再为 0。 105 | 106 | ## expvar.Publish 函数 ## 107 | 108 | expvar 包相当小且容易理解。它主要由两个部分组成。第一个是函数 `expvar.Publish(name string,v expvar.Var)`。该函数可用于在未导出的全局注册表中注册具有特定名称(name)的 v。以下代码段显示了具体实现。接下来的 3 个代码段是从 expvar 包的源代码中截取的。 109 | 110 | 先看下全局注册表: 111 | 112 | var ( 113 | mutex sync.RWMutex 114 | vars = make(map[string]Var) 115 | varKeys []string // sorted 116 | ) 117 | 118 | 全局注册表实际就是一个 map:vars。 119 | 120 | Publish 函数的实现: 121 | 122 | // Publish declares a named exported variable. This should be called from a 123 | // package's init function when it creates its Vars. If the name is already 124 | // registered then this will log.Panic. 125 | func Publish(name string, v Var) { 126 | mutex.Lock() 127 | defer mutex.Unlock() 128 | 129 | // Check if name has been taken already. If so, panic. 130 | if _, existing := vars[name]; existing { 131 | log.Panicln("Reuse of exported var name:", name) 132 | } 133 | 134 | // vars is the global registry. It is defined somewhere else in the 135 | // expvar package like this: 136 | // 137 | // vars = make(map[string]Var) 138 | vars[name] = v 139 | // 一方面,该包中所有公共变量,放在 vars 中,同时,通过 varKeys 保存了所有变量名,并且按字母序排序,即实现了一个有序的、线程安全的 map 140 | varKeys = append(varKeys, name) 141 | sort.Strings(varKeys) 142 | } 143 | 144 | expvar 包内置的两个公共变量就是通过 Publish 注册的: 145 | 146 | Publish("cmdline", Func(cmdline)) 147 | Publish("memstats", Func(memstats)) 148 | 149 | ## expvar.Var 接口 ## 150 | 151 | 另一个重要的组成部分是 `expvar.Var` 接口。 这个接口只有一个方法: 152 | 153 | // Var is an abstract type for all exported variables. 154 | type Var interface { 155 | // String returns a valid JSON value for the variable. 156 | // Types with String methods that do not return valid JSON 157 | // (such as time.Time) must not be used as a Var. 158 | String() string 159 | } 160 | 161 | 所以你可以在有 String() string 方法的所有类型上调用 Publish() 函数,但需要注意的是,这里的 String() 要求返回的是一个有效的 JSON 字符串。 162 | 163 | ## expvar.Int 类型 ## 164 | 165 | expvar 包提供了其他几个类型,它们实现了 expvar.Var 接口。其中一个是 expvar.Int,我们已经在演示代码中通过 expvar.NewInt("visits") 方式使用它了,它会创建一个新的 expvar.Int,并使用 expvar.Publish 注册它,然后返回一个指向新创建的 expvar.Int 的指针。 166 | 167 | func NewInt(name string) *Int { 168 | v := new(Int) 169 | Publish(name, v) 170 | return v 171 | } 172 | 173 | expvar.Int 包装一个 int64,并有两个函数 Add(delta int64) 和 Set(value int64),它们以线程安全的方式(通过 `atomic` 包实现)修改包装的 int64。另外通过 `Value() int64` 函数获得包装的 int64。 174 | 175 | type Int struct { 176 | i int64 177 | } 178 | 179 | ## 其他类型 ## 180 | 181 | 除了 expvar.Int,该包还提供了一些实现 expvar.Var 接口的其他类型: 182 | 183 | * [expvar.Float](http://docs.studygolang.com/pkg/expvar/#Float) 184 | * [expvar.String](http://docs.studygolang.com/pkg/expvar/#String) 185 | * [expvar.Map](http://docs.studygolang.com/pkg/expvar/#Map) 186 | * [expvar.Func](http://docs.studygolang.com/pkg/expvar/#Func) 187 | 188 | 前两个类型包装了 float64 和 string。后两种类型需要稍微解释下。 189 | 190 | `expvar.Map` 类型可用于使公共变量出现在某个名称空间下。可以这样用: 191 | 192 | var stats = expvar.NewMap("http") 193 | var requests, requestsFailed expvar.Int 194 | 195 | func init() { 196 | stats.Set("req_succ", &requests) 197 | stats.Set("req_failed", &requestsFailed) 198 | } 199 | 200 | 这段代码使用名称空间 http 注册了两个指标 req_succ 和 req_failed。它将显示在 JSON 响应中,如下所示: 201 | 202 | { 203 | "http": { 204 | "req_succ": 18, 205 | "req_failed": 21 206 | } 207 | } 208 | 209 | 当要注册某个函数的执行结果到某个公共变量时,您可以使用 `expvar.Func`。假设您希望计算应用程序的正常运行时间,每次有人访问 `http://localhost:8080/debug/vars` 时,都必须重新计算此值。 210 | 211 | var start = time.Now() 212 | 213 | func calculateUptime() interface { 214 | return time.Since(start).String() 215 | } 216 | 217 | expvar.Publish("uptime", expvar.Func(calculateUptime)) 218 | 219 | 实际上,内置的两个指标 `cmdline` 和 `memstats` 就是通过这种方式注册的。注意,函数签名有如下要求:没有参数,返回 interface{} 220 | 221 | type Func func() interface{} 222 | 223 | ## 关于 Handler 函数 ## 224 | 225 | 本文开始时提到,可以简单的导入 expvar 包,然后使用其副作用,导出路径 `/debug/vars`。然而,如果我们使用了一些框架,并非使用 `http.DefaultServeMux`,而是框架自己定义的 `Mux`,这时直接导入使用副作用可能不会生效。我们可以按照使用的框架,定义自己的路径,比如也叫 `/debug/vars`,然后,这对应的处理程序中,按如下的两种方式处理: 226 | 227 | 1)将处理直接交给 `expvar.Handler`,比如: 228 | 229 | handler := expvar.Handler() 230 | handler.ServeHTTP(w, req) 231 | 232 | 2)自己遍历 expvar 中的公共变量,构造输出,甚至可以过滤 expvar 默认提供的 cmdline 和 memstats,我们看下 expvarHandler 的源码就明白了:(通过 expvar.Do 函数来遍历的) 233 | 234 | func expvarHandler(w http.ResponseWriter, r *http.Request) { 235 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 236 | fmt.Fprintf(w, "{\n") 237 | first := true 238 | Do(func(kv KeyValue) { 239 | if !first { 240 | fmt.Fprintf(w, ",\n") 241 | } 242 | first = false 243 | fmt.Fprintf(w, "%q: %s", kv.Key, kv.Value) 244 | }) 245 | fmt.Fprintf(w, "\n}\n") 246 | } 247 | 248 | [Go 语言中文网](https://github.com/studygolang/studygolang/blob/V3.0/src/http/controller/admin/metrics.go#L42) 因为使用了 Echo 框架,使用第 1 种方式来处理的。 249 | 250 | ## 定义自己的 expvar.Var 类型 ## 251 | 252 | expvar 包提供了 int、float 和 string 这三种基本数据类型的 expvar.Var 实现,以及 Func 和 Map。有时,我们自己有一个复杂的类型,想要实现 expvar.Var 接口,怎么做呢? 253 | 254 | 从上面的介绍,应该很容易实现吧,如果您遇到了具体的需求,可以试试。 255 | 256 | ## 总结 ## 257 | 258 | 综上所述,通过 expvar 包,可以非常容易的展示应用程序指标。建议您在您的每个应用程序中使用它来展示一些指示应用程序运行状况的指标,通过它和其他的一些工具来监控应用程序。 259 | 260 | # 导航 # 261 | 262 | - 上一节:[log - 日志记录](10.2.md) 263 | - 下一节:[runtime/debug - 运行时的调试工具](10.4.md) 264 | 265 | -------------------------------------------------------------------------------- /chapter15/15.02.md: -------------------------------------------------------------------------------- 1 | # 15.2 — 非类型安全操作 # 2 | 3 | *unsafe*库徘徊在“类型安全”边缘,由于它们绕过了Golang的内存安全原则,一般被认为使用该库是不安全的。但是,在许多情况下,*unsafe*库的作用又是不可替代的,灵活地使用它们可以实现对内存的直接读写操作。在*reflect*库、*syscall*库以及其他许多需要操作内存的开源项目中都有对它的引用。 4 | 5 | *unsafe*库源码极少,只有两个类型的定义和三个方法的声明。 6 | 7 | ## Arbitrary 类型 ## 8 | 9 | 官方导出这个类型只是出于完善文档的考虑,在其他的库和任何项目中都没有使用价值,除非程序员故意使用它。 10 | 11 | ## Pointer 类型 ## 12 | 13 | 这个类型比较重要,它是实现定位欲读写的内存的基础。官方文档对该类型有四个重要描述: 14 | 15 | - (1)任何类型的指针都可以被转化为Pointer 16 | - (2)Pointer可以被转化为任何类型的指针 17 | - (3)uintptr可以被转化为Pointer 18 | - (4)Pointer可以被转化为uintptr 19 | 20 | 举例来说,该类型可以这样使用: 21 | 22 | func main() { 23 | i := 100 24 | fmt.Println(i) // 100 25 | p := (*int)unsafe.Pointer(&i) 26 | fmt.Println(*p) // 100 27 | *p = 0 28 | fmt.Println(i) // 0 29 | fmt.Println(*p) // 0 30 | } 31 | 32 | ## Sizeof 函数 ## 33 | 34 | 该函数的定义如下: 35 | 36 | func Sizeof(v ArbitraryType) uintptr 37 | 38 | Sizeof函数返回变量v占用的内存空间的字节数,该字节数不是按照变量v实际占用的内存计算,而是按照v的“top level”内存计算。比如,在64位系统中,如果变量v是int类型,会返回16,因为v的“top level”内存就是它的值使用的内存;如果变量v是string类型,会返回16,因为v的“top level”内存不是存放着实际的字符串,而是该字符串的地址;如果变量v是slice类型,会返回24,这是因为slice的描述符就占了24个字节。 39 | 40 | ## Offsetof 函数 ## 41 | 42 | 该函数的定义如下: 43 | 44 | func Offsetof(v ArbitraryType) uintptr 45 | 46 | 该函数返回由v所指示的某结构体中的字段在该结构体中的位置偏移字节数,注意,v的表达方式必须是“struct.filed”形式。 47 | 举例说明,在64为系统中运行以下代码: 48 | 49 | type Datas struct{ 50 | c0 byte 51 | c1 int 52 | c2 string 53 | c3 int 54 | } 55 | func main(){ 56 | var d Datas 57 | fmt.Println(unsafe.Offset(d.c0)) // 0 58 | fmt.Println(unsafe.Offset(d.c1)) // 8 59 | fmt.Println(unsafe.Offset(d.c2)) // 16 60 | fmt.Println(unsafe.Offset(d.c3)) // 32 61 | } 62 | 63 | 如果知道的结构体的起始地址和字段的偏移值,就可以直接读写内存: 64 | 65 | d.c3 = 13 66 | p := unsafe.Pointer(&d) 67 | offset := unsafe.Offsetof(d.c3) 68 | q := (*int)(unsafe.Pointer(uintptr(p) + offset)) 69 | fmt.Println(*q) // 13 70 | *p = 1013 71 | fmt.Println(d.c3) // 1013 72 | 73 | 74 | 75 | # 导航 # 76 | 77 | - [目录](/preface.md) 78 | - 上一节:buildin 79 | - 下一节:暂未确定 80 | -------------------------------------------------------------------------------- /chapter16/16.01.md: -------------------------------------------------------------------------------- 1 | # sync - 处理同步需求 # 2 | 3 | golang是一门语言级别支持并发的程序语言。golang中使用go语句来开启一个新的协程。 4 | goroutine是非常轻量的,除了给它分配栈空间,它所占用的内存空间是微乎其微的。 5 | 6 | 但当多个goroutine同时进行处理的时候,就会遇到比如同时抢占一个资源,某个goroutine等待另一个goroutine处理完某一个步骤之后才能继续的需求。 7 | 在golang的官方文档上,作者明确指出,golang并不希望依靠共享内存的方式进行进程的协同操作。而是希望通过管道channel的方式进行。 8 | 当然,golang也提供了共享内存,锁,等机制进行协同操作的包。sync包就是为了这个目的而出现的。 9 | 10 | ## 锁 ## 11 | 12 | sync包中定义了Locker结构来代表锁。 13 | 14 | ```golang 15 | type Locker interface { 16 | Lock() 17 | Unlock() 18 | } 19 | ``` 20 | 并且创造了两个结构来实现Locker接口:Mutex 和 RWMutex。 21 | 22 | Mutex就是互斥锁,互斥锁代表着当数据被加锁了之后,除了加锁的程序,其他程序不能对数据进行读操作和写操作。 23 | 这个当然能解决并发程序对资源的操作。但是,效率上是个问题。当加锁后,其他程序要读取操作数据,就只能进行等待了。 24 | 这个时候就需要使用读写锁。 25 | 26 | 读写锁分为读锁和写锁,读数据的时候上读锁,写数据的时候上写锁。有写锁的时候,数据不可读不可写。有读锁的时候,数据可读,不可写。 27 | 互斥锁就不举例子,读写锁可以看下面的例子: 28 | 29 | ```golang 30 | package main 31 | 32 | import ( 33 | "sync" 34 | "time" 35 | ) 36 | 37 | var m *sync.RWMutex 38 | var val = 0 39 | 40 | func main() { 41 | m = new(sync.RWMutex) 42 | go read(1) 43 | go write(2) 44 | go read(3) 45 | time.Sleep(5 * time.Second) 46 | } 47 | 48 | func read(i int) { 49 | m.RLock() 50 | time.Sleep(1 * time.Second) 51 | println("val: ", val) 52 | time.Sleep(1 * time.Second) 53 | m.RUnlock() 54 | } 55 | 56 | func write(i int) { 57 | m.Lock() 58 | val = 10 59 | time.Sleep(1 * time.Second) 60 | m.Unlock() 61 | } 62 | 63 | 返回: 64 | val: 0 65 | val: 10 66 | 67 | ``` 68 | 但是如果我们把read中的RLock和RUnlock两个函数给注释了,就返回了: 69 | ```golang 70 | val: 10 71 | val: 10 72 | ``` 73 | 这个就是由于读的时候没有加读锁,在准备读取val的时候,val被write函数进行修改了。 74 | 75 | ## 临时对象池 ## 76 | 77 | 当多个goroutine都需要创建同一个对象的时候,如果goroutine过多,可能导致对象的创建数目剧增。 78 | 而对象又是占用内存的,进而导致的就是内存回收的GC压力徒增。造成“并发大-占用内存大-GC缓慢-处理并发能力降低-并发更大”这样的恶性循环。 79 | 在这个时候,我们非常迫切需要有一个对象池,每个goroutine不再自己单独创建对象,而是从对象池中获取出一个对象(如果池中已经有的话)。 80 | 这就是sync.Pool出现的目的了。 81 | 82 | sync.Pool的使用非常简单,提供两个方法:Get和Put 和一个初始化回调函数New。 83 | 84 | 看下面这个例子(取自[gomemcache](https://github.com/bradfitz/gomemcache/blob/master/memcache/selector.go)): 85 | ```golang 86 | // keyBufPool returns []byte buffers for use by PickServer's call to 87 | // crc32.ChecksumIEEE to avoid allocations. (but doesn't avoid the 88 | // copies, which at least are bounded in size and small) 89 | var keyBufPool = sync.Pool{ 90 | New: func() interface{} { 91 | b := make([]byte, 256) 92 | return &b 93 | }, 94 | } 95 | 96 | func (ss *ServerList) PickServer(key string) (net.Addr, error) { 97 | ss.mu.RLock() 98 | defer ss.mu.RUnlock() 99 | if len(ss.addrs) == 0 { 100 | return nil, ErrNoServers 101 | } 102 | if len(ss.addrs) == 1 { 103 | return ss.addrs[0], nil 104 | } 105 | bufp := keyBufPool.Get().(*[]byte) 106 | n := copy(*bufp, key) 107 | cs := crc32.ChecksumIEEE((*bufp)[:n]) 108 | keyBufPool.Put(bufp) 109 | 110 | return ss.addrs[cs%uint32(len(ss.addrs))], nil 111 | } 112 | ``` 113 | 114 | 这是实际项目中的一个例子,这里使用keyBufPool的目的是为了让crc32.ChecksumIEEE所使用的[]bytes数组可以重复使用,减少GC的压力。 115 | 116 | 但是这里可能会有一个问题,我们没有看到Pool的手动回收函数。 117 | 那么是不是就意味着,如果我们的并发量不断增加,这个Pool的体积会不断变大,或者一直维持在很大的范围内呢? 118 | 119 | 答案是不会的,sync.Pool的回收是有的,它是在系统自动GC的时候,触发pool.go中的poolCleanup函数。 120 | 121 | ```golang 122 | func poolCleanup() { 123 | for i, p := range allPools { 124 | allPools[i] = nil 125 | for i := 0; i < int(p.localSize); i++ { 126 | l := indexLocal(p.local, i) 127 | l.private = nil 128 | for j := range l.shared { 129 | l.shared[j] = nil 130 | } 131 | l.shared = nil 132 | } 133 | p.local = nil 134 | p.localSize = 0 135 | } 136 | allPools = []*Pool{} 137 | } 138 | ``` 139 | 140 | 这个函数会把Pool中所有goroutine创建的对象都进行销毁。 141 | 142 | 那这里另外一个问题也凸显出来了,很可能我上一步刚往pool中PUT一个对象之后,下一步GC触发,导致pool的GET函数获取不到PUT进去的对象。 143 | 这个时候,GET函数就会调用New函数,临时创建出一个对象,并存放到pool中。 144 | 145 | 根据以上结论,sync.Pool其实不适合用来做持久保存的对象池(比如连接池)。它更适合用来做临时对象池,目的是为了降低GC的压力。 146 | 147 | 连接池性能测试 148 | 149 | ```golang 150 | package main 151 | 152 | import ( 153 | "sync" 154 | "testing" 155 | ) 156 | 157 | var bytePool = sync.Pool{ 158 | New: newPool, 159 | } 160 | 161 | func newPool() interface{} { 162 | b := make([]byte, 1024) 163 | return &b 164 | } 165 | func BenchmarkAlloc(b *testing.B) { 166 | for i := 0; i < b.N; i++ { 167 | obj := make([]byte, 1024) 168 | _ = obj 169 | } 170 | } 171 | 172 | func BenchmarkPool(b *testing.B) { 173 | for i := 0; i < b.N; i++ { 174 | obj := bytePool.Get().(*[]byte) 175 | _ = obj 176 | bytePool.Put(obj) 177 | } 178 | } 179 | ``` 180 | 181 | 文件目录下执行 `go test -bench . ` 182 | 183 | ``` 184 | E:\MyGo\sync>go test -bench . 185 | testing: warning: no tests to run 186 | PASS 187 | BenchmarkAlloc-4 50000000 39.3 ns/op 188 | BenchmarkPool-4 50000000 25.4 ns/op 189 | ok _/E_/MyGo/sync 3.345s 190 | ``` 191 | 192 | 通过性能测试可以清楚地看到,使用连接池消耗的CPU时间远远小于每次手动分配内存。 193 | 194 | ## Once ## 195 | 196 | 有的时候,我们多个goroutine都要过一个操作,但是这个操作我只希望被执行一次,这个时候Once就上场了。比如下面的例子: 197 | 198 | ```golang 199 | package main 200 | 201 | import ( 202 | "fmt" 203 | "sync" 204 | "time" 205 | ) 206 | 207 | func main() { 208 | var once sync.Once 209 | onceBody := func() { 210 | fmt.Println("Only once") 211 | } 212 | for i := 0; i < 10; i++ { 213 | go func() { 214 | once.Do(onceBody) 215 | }() 216 | } 217 | time.Sleep(3e9) 218 | } 219 | 220 | ``` 221 | 只会打出一次"Only once"。 222 | 223 | ## WaitGroup 和 Cond ## 224 | 225 | 一个goroutine需要等待一批goroutine执行完毕以后才继续执行,那么这种多线程等待的问题就可以使用WaitGroup了。 226 | 227 | ```golang 228 | package main 229 | 230 | import ( 231 | "fmt" 232 | "sync" 233 | ) 234 | 235 | func main() { 236 | wp := new(sync.WaitGroup) 237 | wp.Add(10); 238 | 239 | for i := 0; i < 10; i++ { 240 | go func() { 241 | fmt.Println("done ", i) 242 | wp.Done() 243 | }() 244 | } 245 | 246 | wp.Wait() 247 | fmt.Println("wait end") 248 | } 249 | ``` 250 | 251 | 还有个sync.Cond是用来控制某个条件下,goroutine进入等待时期,等待信号到来,然后重新启动。比如: 252 | 253 | ```golang 254 | package main 255 | 256 | import ( 257 | "fmt" 258 | "sync" 259 | "time" 260 | ) 261 | 262 | func main() { 263 | locker := new(sync.Mutex) 264 | cond := sync.NewCond(locker) 265 | done := false 266 | 267 | cond.L.Lock() 268 | 269 | go func() { 270 | time.Sleep(2e9) 271 | done = true 272 | cond.Signal() 273 | }() 274 | 275 | if (!done) { 276 | cond.Wait() 277 | } 278 | 279 | fmt.Println("now done is ", done); 280 | } 281 | ``` 282 | 这里当主goroutine进入cond.Wait的时候,就会进入等待,当从goroutine发出信号之后,主goroutine才会继续往下面走。 283 | 284 | sync.Cond还有一个BroadCast方法,用来通知唤醒所有等待的gouroutine。 285 | ```golang 286 | 287 | package main 288 | 289 | import ( 290 | "fmt" 291 | "sync" 292 | "time" 293 | ) 294 | 295 | var locker = new(sync.Mutex) 296 | var cond = sync.NewCond(locker) 297 | 298 | func test(x int) { 299 | 300 | cond.L.Lock() // 获取锁 301 | cond.Wait() // 等待通知 暂时阻塞 302 | fmt.Println(x) 303 | time.Sleep(time.Second * 1) 304 | cond.L.Unlock() // 释放锁,不释放的话将只会有一次输出 305 | } 306 | func main() { 307 | for i := 0; i < 40; i++ { 308 | go test(i) 309 | } 310 | fmt.Println("start all") 311 | cond.Broadcast() // 下发广播给所有等待的goroutine 312 | time.Sleep(time.Second * 60) 313 | } 314 | 315 | ``` 316 | 主gouroutine开启后,可以创建多个从gouroutine,从gouroutine获取锁后,进入cond.Wait状态,当主gouroutine执行完任务后,通过BroadCast广播信号。 317 | 处于cond.Wait状态的所有gouroutine收到信号后将全部被唤醒并往下执行。需要注意的是,从gouroutine执行完任务后,需要通过cond.L.Unlock释放锁, 否则其它被唤醒的gouroutine将没法继续执行。 318 | 通过查看cond.Wait 的源码就明白为什么需要需要释放锁了 319 | ```golang 320 | func (c *Cond) Wait() { 321 | c.checker.check() 322 | if raceenabled { 323 | raceDisable() 324 | } 325 | atomic.AddUint32(&c.waiters, 1) 326 | if raceenabled { 327 | raceEnable() 328 | } 329 | c.L.Unlock() 330 | runtime_Syncsemacquire(&c.sema) 331 | c.L.Lock() 332 | } 333 | ``` 334 | Cond.Wait会自动释放锁等待信号的到来,当信号到来后,第一个获取到信号的Wait将继续往下执行并从新上锁,如果不释放锁, 其它收到信号的gouroutine将阻塞无法继续执行。 335 | 由于各个Wait收到信号的时间是不确定的,因此每次的输出顺序也都是随机的。 336 | # 导航 # 337 | 338 | - [目录](/preface.md) 339 | - 上一节:buildin 340 | - 下一节:暂未确定 341 | -------------------------------------------------------------------------------- /chapter16/16.02.md: -------------------------------------------------------------------------------- 1 | # sync/atomic - 原子操作 # 2 | 3 | 对于并发操作而言,原子操作是个非常现实的问题。典型的就是i++的问题。 4 | 当两个CPU同时对内存中的i进行读取,然后把加一之后的值放入内存中,可能两次i++的结果,这个i只增加了一次。 5 | 如何保证多CPU对同一块内存的操作是原子的。 6 | golang中sync/atomic就是做这个使用的。 7 | 8 | 具体的原子操作在不同的操作系统中实现是不同的。比如在Intel的CPU架构机器上,主要是使用总线锁的方式实现的。 9 | 大致的意思就是当一个CPU需要操作一个内存块的时候,向总线发送一个LOCK信号,所有CPU收到这个信号后就不对这个内存块进行操作了。 10 | 等待操作的CPU执行完操作后,发送UNLOCK信号,才结束。 11 | 在AMD的CPU架构机器上就是使用MESI一致性协议的方式来保证原子操作。 12 | 所以我们在看atomic源码的时候,我们看到它针对不同的操作系统有不同汇编语言文件。 13 | 14 | 如果我们善用原子操作,它会比锁更为高效。 15 | 16 | ## CAS ## 17 | 18 | 原子操作中最经典的CAS(compare-and-swap)在atomic包中是Compare开头的函数。 19 | 20 | - func CompareAndSwapInt32(addr \*int32, old, new int32) (swapped bool) 21 | - func CompareAndSwapInt64(addr \*int64, old, new int64) (swapped bool) 22 | - func CompareAndSwapPointer(addr \*unsafe.Pointer, old, new unsafe.Pointer) (swapped bool) 23 | - func CompareAndSwapUint32(addr \*uint32, old, new uint32) (swapped bool) 24 | - func CompareAndSwapUint64(addr \*uint64, old, new uint64) (swapped bool) 25 | - func CompareAndSwapUintptr(addr \*uintptr, old, new uintptr) (swapped bool) 26 | 27 | CAS的意思是判断内存中的某个值是否等于old值,如果是的话,则赋new值给这块内存。CAS是一个方法,并不局限在CPU原子操作中。 28 | CAS比互斥锁乐观,但是也就代表CAS是有赋值不成功的时候,调用CAS的那一方就需要处理赋值不成功的后续行为了。 29 | 30 | 这一系列的函数需要比较后再进行交换,也有不需要进行比较就进行交换的原子操作。 31 | 32 | - func SwapInt32(addr \*int32, new int32) (old int32) 33 | - func SwapInt64(addr \*int64, new int64) (old int64) 34 | - func SwapPointer(addr \*unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer) 35 | - func SwapUint32(addr \*uint32, new uint32) (old uint32) 36 | - func SwapUint64(addr \*uint64, new uint64) (old uint64) 37 | - func SwapUintptr(addr \*uintptr, new uintptr) (old uintptr) 38 | 39 | ## 增加或减少 ## 40 | 41 | 对一个数值进行增加或者减少的行为也需要保证是原子的,它对应于atomic包的函数就是 42 | 43 | - func AddInt32(addr \*int32, delta int32) (new int32) 44 | - func AddInt64(addr \*int64, delta int64) (new int64) 45 | - func AddUint32(addr \*uint32, delta uint32) (new uint32) 46 | - func AddUint64(addr \*uint64, delta uint64) (new uint64) 47 | - func AddUintptr(addr \*uintptr, delta uintptr) (new uintptr) 48 | 49 | ## 读取或写入 ## 50 | 51 | 当我们要读取一个变量的时候,很有可能这个变量正在被写入,这个时候,我们就很有可能读取到写到一半的数据。 52 | 所以读取操作是需要一个原子行为的。在atomic包中就是Load开头的函数群。 53 | 54 | - func LoadInt32(addr \*int32) (val int32) 55 | - func LoadInt64(addr \*int64) (val int64) 56 | - func LoadPointer(addr \*unsafe.Pointer) (val unsafe.Pointer) 57 | - func LoadUint32(addr \*uint32) (val uint32) 58 | - func LoadUint64(addr \*uint64) (val uint64) 59 | - func LoadUintptr(addr \*uintptr) (val uintptr) 60 | 61 | 好了,读取我们是完成了原子性,那写入呢?也是同样的,如果有多个CPU往内存中一个数据块写入数据的时候,可能导致这个写入的数据不完整。 62 | 在atomic包对应的是Store开头的函数群。 63 | 64 | - func StoreInt32(addr \*int32, val int32) 65 | - func StoreInt64(addr \*int64, val int64) 66 | - func StorePointer(addr \*unsafe.Pointer, val unsafe.Pointer) 67 | - func StoreUint32(addr \*uint32, val uint32) 68 | - func StoreUint64(addr \*uint64, val uint64) 69 | - func StoreUintptr(addr \*uintptr, val uintptr) 70 | 71 | # 导航 # 72 | 73 | - [目录](/preface.md) 74 | - 上一节:[sync - 处理同步需求](chapter16/16.01.md) 75 | - 下一节:[os/signal - 信号](chapter16/16.03.md) 76 | -------------------------------------------------------------------------------- /chapter16/16.03.md: -------------------------------------------------------------------------------- 1 | # os/signal - 信号 # 2 | 3 | ## 基本概念 4 | 5 | 信号是事件发生时对进程的通知机制。有时也称之为软件中断。信号与硬件中断的相似之处在于打断了程序执行的正常流程,大多数情况下,无法预测信号到达的精确时间。 6 | 7 | 因为一个具有合适权限的进程可以向另一个进程发送信号,这可以称为进程间的一种同步技术。当然,进程也可以向自身发送信号。然而,发往进程的诸多信号,通常都是源于内核。引发内核为进程产生信号的各类事件如下。 8 | 9 | * 硬件发生异常,即硬件检测到一个错误条件并通知内核,随即再由内核发送相应信号给相关进程。比如执行一条异常的机器语言指令(除0,引用无法访问的内存区域)。 10 | * 用户键入了能够产生信号的终端特殊字符。如中断字符(通常是 Control-C)、暂停字符(通常是 Control-Z)。 11 | * 发生了软件事件。如调整了终端窗口大小,定时器到期等。 12 | 13 | 针对每个信号,都定义了一个唯一的(小)整数,从 1 开始顺序展开。系统会用相应常量表示。Linux 中,1-31 为标准信号;32-64 为实时信号(通过 `kill -l` 可以查看)。 14 | 15 | 信号达到后,进程视具体信号执行如下默认操作之一。 16 | 17 | * 忽略信号,也就是内核将信号丢弃,信号对进程不产生任何影响。 18 | * 终止(杀死)进程。 19 | * 产生 coredump 文件,同时进程终止。 20 | * 暂停(Stop)进程的执行。 21 | * 恢复进程执行。 22 | 23 | 当然,对于有些信号,程序是可以改变默认行为的,这也就是 `os/signal` 包的用途。 24 | 25 | 兼容性问题:信号的概念来自于 Unix-like 系统。Windows 下只支持 os.SIGINT 信号。 26 | 27 | ## Go 对信号的处理 28 | 29 | 程序无法捕获信号 SIGKILL 和 SIGSTOP (终止和暂停进程),因此 `os/signal` 包对这两个信号无效。 30 | 31 | ### Go 程序对信号的默认行为 32 | 33 | Go 语言实现了自己的运行时,因此,对信号的默认处理方式和普通的 C 程序不太一样。 34 | 35 | * SIGBUS(总线错误), SIGFPE(算术错误)和 SIGSEGV(段错误)称为同步信号,它们在程序执行错误时触发,而不是通过 `os.Process.Kill` 之类的触发。通常,Go 程序会将这类信号转为 run-time panic。 36 | * SIGHUP(挂起), SIGINT(中断)或 SIGTERM(终止)默认会使得程序退出。 37 | * SIGQUIT, SIGILL, SIGTRAP, SIGABRT, SIGSTKFLT, SIGEMT 或 SIGSYS 默认会使得程序退出,同时生成 stack dump。 38 | * SIGTSTP, SIGTTIN 或 SIGTTOU,这是 shell 使用的,作业控制的信号,执行系统默认的行为。 39 | * SIGPROF(性能分析定时器,记录 CPU 时间,包括用户态和内核态), Go 运行时使用该信号实现 `runtime.CPUProfile`。 40 | * 其他信号,Go 捕获了,但没有做任何处理。 41 | 42 | 信号可以被忽略或通过掩码阻塞(屏蔽字 mask)。忽略信号通过 signal.Ignore,没有导出 API 可以直接修改阻塞掩码,虽然 Go 内部有实现 sigprocmask 等。Go 中的信号被 runtime 控制,在使用时和 C 是不太一样的。 43 | 44 | ### 改变信号的默认行为 45 | 46 | 这就是 `os/signal` 包的功能。 47 | 48 | `Notify` 改变信号处理,可以改变信号的默认行为;`Ignore` 可以忽略信号;`Reset` 重置信号为默认行为;`Stop` 则停止接收信号,但并没有重置为默认行为。 49 | 50 | ### SIGPIPE 51 | 52 | 文档中对这个信号单独进行了说明。如果 Go 程序往一个 broken pipe 写数据,内核会产生一个 SIGPIPE 信号。 53 | 54 | 如果 Go 程序没有为 SIGPIPE 信号调用 Notify,对于标准输出或标准错误(文件描述符1或2),该信号会使得程序退出;但其他文件描述符对该信号是啥也不做,当然 write 会返回错误 EPIPE。 55 | 56 | 如果 Go 程序为 SIGPIPE 调用了 Notify,不论什么文件描述符,SIGPIPE 信号都会传递给 Notify channel,当然 write 依然会返回 EPIPE。 57 | 58 | 也就是说,默认情况下,Go 的命令行程序跟传统的 Unix 命令行程序行为一致;但当往一个关闭的网络连接写数据时,传统 Unix 程序会 crash,但 Go 程序不会。 59 | 60 | ### cgo 注意事项 61 | 62 | 如果非 Go 代码使用信号相关功能,需要仔细阅读掌握 `os/signal` 包中相关文档:Go programs that use cgo or SWIG 和 Non-Go programs that call Go code 63 | 64 | ## signal 中 API 详解 65 | 66 | ### Ignore 函数 67 | 68 | `func Ignore(sig ...os.Signal)` 69 | 70 | 忽略一个、多个或全部(不提供任何信号)信号。如果程序接收到了被忽略的信号,则什么也不做。对一个信号,如果先调用 `Notify`,再调用 `Ignore`,`Notify` 的效果会被取消;如果先调用 `Ignore`,在调用 `Notify`,接着调用 `Reset/Stop` 的话,会回到 Ingore 的效果。注意,如果 Notify 作用于多个 chan,则 Stop 需要对每个 chan 都调用才能起到该作用。 71 | 72 | ### Notify 函数 73 | 74 | `func Notify(c chan<- os.Signal, sig ...os.Signal)` 75 | 76 | 类似于绑定信号处理程序。将输入信号转发到 chan c。如果没有列出要传递的信号,会将所有输入信号传递到c;否则只传递列出的输入信号。 77 | 78 | channel c 缓存如何决定?因为 `signal` 包不会为了向c发送信息而阻塞(就是说如果发送时 c 阻塞了,signal包会直接放弃):调用者应该保证 c 有足够的缓存空间可以跟上期望的信号频率。对使用单一信号用于通知的channel,缓存为1就足够了。 79 | 80 | 相关源码: 81 | 82 | // src/os/signal/signal.go process 函数 83 | for c, h := range handlers.m { 84 | if h.want(n) { 85 | // send but do not block for it 86 | select { 87 | case c <- sig: 88 | default: // 保证不会阻塞,直接丢弃 89 | } 90 | } 91 | } 92 | 93 | 可以使用同一 channel 多次调用 `Notify`:每一次都会扩展该 channel 接收的信号集。唯一从信号集去除信号的方法是调用 `Stop`。可以使用同一信号和不同 channel 多次调用 `Notify`:每一个 channel 都会独立接收到该信号的一个拷贝。 94 | 95 | ### Stop 函数 96 | 97 | `func Stop(c chan<- os.Signal)` 98 | 99 | 让 signal 包停止向 c 转发信号。它会取消之前使用 c 调用的所有`Notify` 的效果。当 `Stop` 返回后,会保证 c 不再接收到任何信号。 100 | 101 | ### Reset 函数 102 | 103 | `func Reset(sig ...os.Signal)` 104 | 105 | 取消之前使用 `Notify` 对信号产生的效果;如果没有参数,则所有信号处理都被重置。 106 | 107 | ### 使用示例 108 | 109 | 注:syscall 包中定义了所有的信号常量 110 | 111 | package main 112 | 113 | import ( 114 | "fmt" 115 | "os" 116 | "os/signal" 117 | "syscall" 118 | ) 119 | 120 | var firstSigusr1 = true 121 | 122 | func main() { 123 | // 忽略 Control-C (SIGINT) 124 | // os.Interrupt 和 syscall.SIGINT 是同义词 125 | signal.Ignore(os.Interrupt) 126 | 127 | c1 := make(chan os.Signal, 2) 128 | // Notify SIGHUP 129 | signal.Notify(c1, syscall.SIGHUP) 130 | // Notify SIGUSR1 131 | signal.Notify(c1, syscall.SIGUSR1) 132 | go func() { 133 | for { 134 | switch <-c1 { 135 | case syscall.SIGHUP: 136 | fmt.Println("sighup, reset sighup") 137 | signal.Reset(syscall.SIGHUP) 138 | case syscall.SIGUSR1: 139 | if firstSigusr1 { 140 | fmt.Println("first usr1, notify interrupt which had ignore!") 141 | c2 := make(chan os.Signal, 1) 142 | // Notify Interrupt 143 | signal.Notify(c2, os.Interrupt) 144 | go handlerInterrupt(c2) 145 | } 146 | } 147 | } 148 | }() 149 | 150 | select {} 151 | } 152 | 153 | func handlerInterrupt(c <-chan os.Signal) { 154 | for { 155 | switch <-c { 156 | case os.Interrupt: 157 | fmt.Println("signal interrupt") 158 | } 159 | } 160 | } 161 | 162 | 编译后运行,先后给该进程发送如下信号:SIGINT、SIGUSR1、SIGINT、SIGHUP、SIGHUP,看输出是不是和你预期的一样。 163 | 164 | ### 关于信号的额外说明 165 | 166 | 1. 查看 Go 中 Linux/amd64 信号的实现,发现大量使用的是 rt 相关系统调用,这是支持实时信号处理的 API。 167 | 2. C 语言中信号处理涉及到可重入函数和异步信号安全函数问题;Go 中不存在此问题。 168 | 3. Unix 和信号处理相关的很多系统调用,Go 都隐藏起来了,Go 中对信号的处理,`signal` 包中的函数基本就能搞定。 169 | 170 | # 导航 # 171 | 172 | - [目录](/preface.md) 173 | - 上一节:[sync/atomic - 原子操作](chapter16/16.02.md) 174 | - 下一节:暂未确定 175 | -------------------------------------------------------------------------------- /code/install.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | setlocal 4 | 5 | if exist install.bat goto ok 6 | echo install.bat must be run from its folder 7 | goto end 8 | 9 | :ok 10 | 11 | set OLDGOPATH=%GOPATH% 12 | set GOPATH=%~dp0;%~dp0..\thirdparty 13 | 14 | gofmt -w src 15 | 16 | go install chapter01/io 17 | 18 | set GOPATH=%OLDGOPATH% 19 | 20 | :end 21 | echo finished -------------------------------------------------------------------------------- /code/install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | if [ ! -f install.sh ]; then 6 | echo 'install must be run within its container folder' 1>&2 7 | exit 1 8 | fi 9 | 10 | CURDIR=`pwd` 11 | OLDGOPATH="$GOPATH" 12 | export GOPATH="$CURDIR" 13 | 14 | gofmt -w src 15 | 16 | go install ./... 17 | 18 | export GOPATH="$OLDGOPATH" 19 | export PATH="$OLDPATH" 20 | 21 | echo 'finished' 22 | 23 | -------------------------------------------------------------------------------- /code/src/chapter01/io/01.txt: -------------------------------------------------------------------------------- 1 | from file -------------------------------------------------------------------------------- /code/src/chapter01/io/byterwer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | func ByteRWerExample() { 10 | FOREND: 11 | for { 12 | fmt.Println("请输入要通过WriteByte写入的一个ASCII字符(b:返回上级;q:退出):") 13 | var ch byte 14 | fmt.Scanf("%c\n", &ch) 15 | switch ch { 16 | case 'b': 17 | fmt.Println("返回上级菜单!") 18 | break FOREND 19 | case 'q': 20 | fmt.Println("程序退出!") 21 | os.Exit(0) 22 | default: 23 | buffer := new(bytes.Buffer) 24 | err := buffer.WriteByte(ch) 25 | if err == nil { 26 | fmt.Println("写入一个字节成功!准备读取该字节……") 27 | newCh, _ := buffer.ReadByte() 28 | fmt.Printf("读取的字节:%c\n", newCh) 29 | } else { 30 | fmt.Println("写入错误") 31 | } 32 | } 33 | 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /code/src/chapter01/io/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "util" 6 | ) 7 | 8 | func main() { 9 | util.Welcome() 10 | MainMenu() 11 | } 12 | 13 | func MainMenu() { 14 | MAINFOR: 15 | for { 16 | fmt.Println("") 17 | fmt.Println("*******请选择示例:*********") 18 | fmt.Println("1 表示 io.Reader 示例") 19 | fmt.Println("2 表示 io.ByteReader/ByteWriter 示例") 20 | fmt.Println("q 退出") 21 | fmt.Println("***********************************") 22 | 23 | var ch string 24 | fmt.Scanln(&ch) 25 | 26 | switch ch { 27 | case "1": 28 | ReaderExample() 29 | case "2": 30 | ByteRWerExample() 31 | case "q": 32 | fmt.Println("程序退出!") 33 | break MAINFOR 34 | default: 35 | fmt.Println("输入错误!") 36 | continue 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /code/src/chapter01/io/reader.go: -------------------------------------------------------------------------------- 1 | // io.Reader 接口示例 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "io" 7 | "os" 8 | "strings" 9 | "util" 10 | ) 11 | 12 | func ReaderExample() { 13 | FOREND: 14 | for { 15 | readerMenu() 16 | 17 | var ch string 18 | fmt.Scanln(&ch) 19 | var ( 20 | data []byte 21 | err error 22 | ) 23 | switch strings.ToLower(ch) { 24 | case "1": 25 | fmt.Println("请输入不多于9个字符,以回车结束:") 26 | data, err = ReadFrom(os.Stdin, 11) 27 | case "2": 28 | file, err := os.Open(util.GetProjectRoot() + "/src/chapter01/io/01.txt") 29 | if err != nil { 30 | fmt.Println("打开文件 01.txt 错误:", err) 31 | continue 32 | } 33 | data, err = ReadFrom(file, 9) 34 | file.Close() 35 | case "3": 36 | data, err = ReadFrom(strings.NewReader("from string"), 12) 37 | case "4": 38 | fmt.Println("暂未实现!") 39 | case "b": 40 | fmt.Println("返回上级菜单!") 41 | break FOREND 42 | case "q": 43 | fmt.Println("程序退出!") 44 | os.Exit(0) 45 | default: 46 | fmt.Println("输入错误!") 47 | continue 48 | } 49 | 50 | if err != nil { 51 | fmt.Println("数据读取失败,可以试试从其他输入源读取!") 52 | } else { 53 | fmt.Printf("读取到的数据是:%s\n", data) 54 | } 55 | } 56 | } 57 | 58 | func ReadFrom(reader io.Reader, num int) ([]byte, error) { 59 | p := make([]byte, num) 60 | n, err := reader.Read(p) 61 | if n > 0 { 62 | return p[:n], nil 63 | } 64 | return p, err 65 | } 66 | 67 | func readerMenu() { 68 | fmt.Println("") 69 | fmt.Println("*******从不同来源读取数据*********") 70 | fmt.Println("*******请选择数据源,请输入:*********") 71 | fmt.Println("1 表示 标准输入") 72 | fmt.Println("2 表示 普通文件") 73 | fmt.Println("3 表示 从字符串") 74 | fmt.Println("4 表示 从网络") 75 | fmt.Println("b 返回上级菜单") 76 | fmt.Println("q 退出") 77 | fmt.Println("***********************************") 78 | } 79 | -------------------------------------------------------------------------------- /code/src/chapter06/filepath/walk/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | func main() { 10 | filepath.Walk("../../..", func(path string, info os.FileInfo, err error) error { 11 | if info.IsDir() { 12 | return nil 13 | } 14 | 15 | fmt.Println("file:", info.Name(), "in directory:", path) 16 | 17 | return nil 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /code/src/chapter06/os/dirtree/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | func main() { 11 | ReadAndOutputDir("../../..", 3) 12 | } 13 | 14 | func ReadAndOutputDir(rootPath string, deep int) { 15 | file, err := os.Open(rootPath) 16 | if err != nil { 17 | fmt.Println("error:", err) 18 | return 19 | } 20 | defer file.Close() 21 | 22 | for { 23 | fileInfos, err := file.Readdir(100) 24 | if err != nil { 25 | if err == io.EOF { 26 | break 27 | } 28 | fmt.Println("readdir error:", err) 29 | return 30 | } 31 | 32 | if len(fileInfos) == 0 { 33 | break 34 | } 35 | 36 | for _, fileInfo := range fileInfos { 37 | if fileInfo.IsDir() { 38 | if deep > 0 { 39 | ReadAndOutputDir(filepath.Join(rootPath, string(os.PathSeparator), fileInfo.Name()), deep-1) 40 | } 41 | } else { 42 | fmt.Println("file:", fileInfo.Name(), "in directory:", rootPath) 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /code/src/chapter09/benchmark_result.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | "text/template" 8 | ) 9 | 10 | func main() { 11 | benchmarkResult := testing.Benchmark(func(b *testing.B) { 12 | templ := template.Must(template.New("test").Parse("Hello, {{.}}!")) 13 | // RunParallel will create GOMAXPROCS goroutines 14 | // and distribute work among them. 15 | b.RunParallel(func(pb *testing.PB) { 16 | // Each goroutine has its own bytes.Buffer. 17 | var buf bytes.Buffer 18 | for pb.Next() { 19 | // The loop body is executed b.N times total across all goroutines. 20 | buf.Reset() 21 | templ.Execute(&buf, "World") 22 | } 23 | }) 24 | }) 25 | 26 | // fmt.Printf("%8d\t%10d ns/op\t%10d B/op\t%10d allocs/op\n", benchmarkResult.N, benchmarkResult.NsPerOp(), benchmarkResult.AllocedBytesPerOp(), benchmarkResult.AllocsPerOp()) 27 | fmt.Printf("%s\t%s\n", benchmarkResult.String(), benchmarkResult.MemString()) 28 | } 29 | -------------------------------------------------------------------------------- /code/src/chapter09/httptest/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | ) 7 | 8 | func main() { 9 | http.Handle("/httptest", new(MyHandler)) 10 | 11 | log.Fatal(http.ListenAndServe(":8080", nil)) 12 | } 13 | 14 | type MyHandler func(http.ResponseWriter, *http.Request) 15 | 16 | func (self MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 17 | self(w, r) 18 | } 19 | -------------------------------------------------------------------------------- /code/src/chapter09/testing/example_test.go: -------------------------------------------------------------------------------- 1 | package testing_test 2 | 3 | import ( 4 | . "chapter09/testing" 5 | "fmt" 6 | ) 7 | 8 | func ExampleFib() { 9 | fmt.Println(Fib(7)) 10 | // Output: 13 11 | } 12 | -------------------------------------------------------------------------------- /code/src/chapter09/testing/parallel.go: -------------------------------------------------------------------------------- 1 | package testing 2 | 3 | import "sync" 4 | 5 | var ( 6 | data = make(map[string]string) 7 | locker sync.RWMutex 8 | ) 9 | 10 | func WriteToMap(k, v string) { 11 | locker.Lock() 12 | defer locker.Unlock() 13 | data[k] = v 14 | } 15 | 16 | func ReadFromMap(k string) string { 17 | locker.RLock() 18 | defer locker.RUnlock() 19 | return data[k] 20 | } 21 | -------------------------------------------------------------------------------- /code/src/chapter09/testing/parallel_test.go: -------------------------------------------------------------------------------- 1 | package testing_test 2 | 3 | import ( 4 | "bytes" 5 | . "chapter09/testing" 6 | "html/template" 7 | "testing" 8 | ) 9 | 10 | var pairs = []struct { 11 | k string 12 | v string 13 | }{ 14 | {"polaris", "徐新华"}, 15 | {"studygolang", "Go语言中文网"}, 16 | {"stdlib", "Go语言标准库"}, 17 | {"polaris1", "徐新华1"}, 18 | {"studygolang1", "Go语言中文网1"}, 19 | {"stdlib1", "Go语言标准库1"}, 20 | {"polaris2", "徐新华2"}, 21 | {"studygolang2", "Go语言中文网2"}, 22 | {"stdlib2", "Go语言标准库2"}, 23 | {"polaris3", "徐新华3"}, 24 | {"studygolang3", "Go语言中文网3"}, 25 | {"stdlib3", "Go语言标准库3"}, 26 | {"polaris4", "徐新华4"}, 27 | {"studygolang4", "Go语言中文网4"}, 28 | {"stdlib4", "Go语言标准库4"}, 29 | } 30 | 31 | // 注意 TestWriteToMap 需要在 TestReadFromMap 之前 32 | func TestWriteToMap(t *testing.T) { 33 | t.Parallel() 34 | for _, tt := range pairs { 35 | WriteToMap(tt.k, tt.v) 36 | } 37 | } 38 | 39 | func TestReadFromMap(t *testing.T) { 40 | t.Parallel() 41 | for _, tt := range pairs { 42 | actual := ReadFromMap(tt.k) 43 | if actual != tt.v { 44 | t.Errorf("the value of key(%s) is %s, expected: %s", tt.k, actual, tt.v) 45 | } 46 | } 47 | } 48 | 49 | func BenchmarkTemplateParallel(b *testing.B) { 50 | templ := template.Must(template.New("test").Parse("Hello, {{.}}!")) 51 | b.RunParallel(func(pb *testing.PB) { 52 | var buf bytes.Buffer 53 | for pb.Next() { 54 | buf.Reset() 55 | templ.Execute(&buf, "World") 56 | } 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /code/src/chapter09/testing/server_test.go: -------------------------------------------------------------------------------- 1 | package testing 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestTime(t *testing.T) { 10 | testCases := []struct { 11 | gmt string 12 | loc string 13 | want string 14 | }{ 15 | {"12:31", "Europe/Zuri", "13:31"}, 16 | {"12:31", "America/New_York", "7:31"}, 17 | {"08:08", "Australia/Sydney", "18:08"}, 18 | } 19 | for _, tc := range testCases { 20 | t.Run(fmt.Sprintf("%s in %s", tc.gmt, tc.loc), func(t *testing.T) { 21 | t.Parallel() 22 | loc, err := time.LoadLocation(tc.loc) 23 | if err != nil { 24 | t.Fatal("could not load location") 25 | } 26 | gmt, _ := time.Parse("15:04", tc.gmt) 27 | if got := gmt.In(loc).Format("15:04"); got != tc.want { 28 | t.Errorf("got %s; want %s", got, tc.want) 29 | } 30 | }) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /code/src/chapter09/testing/t.go: -------------------------------------------------------------------------------- 1 | package testing 2 | 3 | func Fib(n int) int { 4 | if n < 2 { 5 | return n 6 | } 7 | return Fib(n-1) + Fib(n-2) 8 | } 9 | -------------------------------------------------------------------------------- /code/src/chapter09/testing/t_test.go: -------------------------------------------------------------------------------- 1 | package testing_test 2 | 3 | import ( 4 | "bytes" 5 | "html/template" 6 | "testing" 7 | "time" 8 | 9 | . "chapter09/testing" 10 | ) 11 | 12 | // Table-Driven Test 13 | func TestFib_TableDrivenParallel(t *testing.T) { 14 | var fibTests = []struct { 15 | name string 16 | in int // input 17 | expected int // expected result 18 | }{ 19 | {"1的Fib", 1, 1}, 20 | {"2的Fib", 2, 1}, 21 | {"3的Fib", 3, 2}, 22 | {"4的Fib", 4, 3}, 23 | {"5的Fib", 5, 5}, 24 | {"6的Fib", 6, 8}, 25 | {"7的Fib", 7, 13}, 26 | } 27 | 28 | for _, tt := range fibTests { 29 | tt := tt 30 | t.Run(tt.name, func(t *testing.T) { 31 | t.Log("time:", time.Now()) 32 | t.Parallel() 33 | time.Sleep(3 * time.Second) 34 | actual := Fib(tt.in) 35 | if actual != tt.expected { 36 | t.Errorf("Fib(%d) = %d; expected %d", tt.in, actual, tt.expected) 37 | } 38 | }) 39 | } 40 | 41 | for _, tt := range fibTests { 42 | t.Log("time:", time.Now()) 43 | actual := Fib(tt.in) 44 | if actual != tt.expected { 45 | t.Errorf("Fib(%d) = %d; expected %d", tt.in, actual, tt.expected) 46 | } 47 | } 48 | 49 | defer func() { 50 | t.Log("time:", time.Now()) 51 | }() 52 | } 53 | 54 | func TestFib(t *testing.T) { 55 | var ( 56 | in = 7 // input 57 | expected = 13 // expected result 58 | ) 59 | actual := Fib(in) 60 | if actual != expected { 61 | t.Errorf("Fib(%d) = %d; expected %d", in, actual, expected) 62 | } 63 | } 64 | 65 | // Table-Driven Test 66 | func TestFib_TableDriven(t *testing.T) { 67 | var fibTests = []struct { 68 | in int // input 69 | expected int // expected result 70 | }{ 71 | {1, 1}, 72 | {2, 1}, 73 | {3, 2}, 74 | {4, 3}, 75 | {5, 5}, 76 | {6, 8}, 77 | {7, 13}, 78 | } 79 | 80 | for _, tt := range fibTests { 81 | actual := Fib(tt.in) 82 | if actual != tt.expected { 83 | t.Errorf("Fib(%d) = %d; expected %d", tt.in, actual, tt.expected) 84 | } 85 | } 86 | } 87 | 88 | func BenchmarkFib1(b *testing.B) { benchmarkFib(1, b) } 89 | func BenchmarkFib2(b *testing.B) { benchmarkFib(2, b) } 90 | func BenchmarkFib3(b *testing.B) { benchmarkFib(3, b) } 91 | func BenchmarkFib10(b *testing.B) { benchmarkFib(10, b) } 92 | func BenchmarkFib20(b *testing.B) { benchmarkFib(20, b) } 93 | func BenchmarkFib40(b *testing.B) { benchmarkFib(40, b) } 94 | 95 | func benchmarkFib(i int, b *testing.B) { 96 | for n := 0; n < b.N; n++ { 97 | Fib(i) 98 | } 99 | } 100 | 101 | func BenchmarkTmplExucte(b *testing.B) { 102 | b.ReportAllocs() 103 | 104 | templ := template.Must(template.New("test").Parse("Hello, {{.}}!")) 105 | b.RunParallel(func(pb *testing.PB) { 106 | // Each goroutine has its own bytes.Buffer. 107 | var buf bytes.Buffer 108 | for pb.Next() { 109 | // The loop body is executed b.N times total across all goroutines. 110 | buf.Reset() 111 | templ.Execute(&buf, "World") 112 | } 113 | }) 114 | } 115 | -------------------------------------------------------------------------------- /code/src/chapter10/os_exec.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/exec" 9 | ) 10 | 11 | func main() { 12 | argNum := len(os.Args) 13 | if argNum < 2 { 14 | log.Printf("Usage:%s command\n", os.Args[0]) 15 | os.Exit(1) 16 | } 17 | 18 | arg := []string{} 19 | if argNum > 2 { 20 | arg = os.Args[2:] 21 | } 22 | 23 | mainOutput(UsePipe(os.Args[1], arg...)) 24 | } 25 | 26 | func mainOutput(out []byte, err error) { 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | 31 | fmt.Printf("The output of command %q is\n%s\n", os.Args[1], out) 32 | } 33 | 34 | // 直接给 Cmd.Stdout 赋值 35 | func FillStd(name string, arg ...string) ([]byte, error) { 36 | cmd := exec.Command(name, arg...) 37 | var out = new(bytes.Buffer) 38 | 39 | cmd.Stdout = out 40 | cmd.Stderr = out 41 | 42 | err := cmd.Run() 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | return out.Bytes(), nil 48 | } 49 | 50 | func UseOutput(name string, arg ...string) ([]byte, error) { 51 | return exec.Command(name, arg...).Output() 52 | } 53 | 54 | // 使用 Pipe 55 | func UsePipe(name string, arg ...string) ([]byte, error) { 56 | cmd := exec.Command(name, arg...) 57 | stdout, err := cmd.StdoutPipe() 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | if err = cmd.Start(); err != nil { 63 | return nil, err 64 | } 65 | 66 | var out = make([]byte, 0, 1024) 67 | for { 68 | tmp := make([]byte, 128) 69 | n, err := stdout.Read(tmp) 70 | out = append(out, tmp[:n]...) 71 | if err != nil { 72 | break 73 | } 74 | } 75 | 76 | if err = cmd.Wait(); err != nil { 77 | return nil, err 78 | } 79 | 80 | return out, nil 81 | } 82 | -------------------------------------------------------------------------------- /code/src/util/util.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The StudyGolang Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | // http://studygolang.com 5 | // Author:polaris studygolang@gmail.com 6 | 7 | package util 8 | 9 | import ( 10 | "fmt" 11 | "os" 12 | "path" 13 | "path/filepath" 14 | "strings" 15 | ) 16 | 17 | // 获得项目根目录 18 | func GetProjectRoot() string { 19 | binDir, err := executableDir() 20 | if err != nil { 21 | return "" 22 | } 23 | return path.Dir(binDir) 24 | } 25 | 26 | // 获得可执行程序所在目录 27 | func executableDir() (string, error) { 28 | pathAbs, err := filepath.Abs(os.Args[0]) 29 | if err != nil { 30 | return "", err 31 | } 32 | return filepath.Dir(pathAbs), nil 33 | } 34 | 35 | func Welcome() { 36 | fmt.Println("***********************************") 37 | fmt.Println("*******欢迎来到Go语言中文网*******") 38 | fmt.Println("***********************************") 39 | } 40 | 41 | // strings.Index的UTF-8版本 42 | // 即 Utf8Index("Go语言中文网", "学习") 返回 4,而不是strings.Index的 8 43 | func Utf8Index(str, substr string) int { 44 | asciiPos := strings.Index(str, substr) 45 | if asciiPos == -1 || asciiPos == 0 { 46 | return asciiPos 47 | } 48 | pos := 0 49 | totalSize := 0 50 | reader := strings.NewReader(str) 51 | for _, size, err := reader.ReadRune(); err == nil; _, size, err = reader.ReadRune() { 52 | totalSize += size 53 | pos++ 54 | // 匹配到 55 | if totalSize == asciiPos { 56 | return pos 57 | } 58 | } 59 | return pos 60 | } 61 | --------------------------------------------------------------------------------