This commit is contained in:
Xargin 2017-12-11 19:12:51 +08:00
commit a2a5cd9d9d
13 changed files with 26 additions and 26 deletions

View File

@ -79,7 +79,7 @@
* [并发的循环](ch8/ch8-05.md) * [并发的循环](ch8/ch8-05.md)
* [示例: 并发的Web爬虫](ch8/ch8-06.md) * [示例: 并发的Web爬虫](ch8/ch8-06.md)
* [基于select的多路复用](ch8/ch8-07.md) * [基于select的多路复用](ch8/ch8-07.md)
* [示例: 并发的字典遍历](ch8/ch8-08.md)  * [示例: 并发的目录遍历](ch8/ch8-08.md)
* [并发的退出](ch8/ch8-09.md) * [并发的退出](ch8/ch8-09.md)
* [示例: 聊天服务](ch8/ch8-10.md) * [示例: 聊天服务](ch8/ch8-10.md)
* [基于共享变量的并发](ch9/ch9.md) * [基于共享变量的并发](ch9/ch9.md)
@ -87,7 +87,7 @@
* [sync.Mutex互斥锁](ch9/ch9-02.md) * [sync.Mutex互斥锁](ch9/ch9-02.md)
* [sync.RWMutex读写锁](ch9/ch9-03.md) * [sync.RWMutex读写锁](ch9/ch9-03.md)
* [内存同步](ch9/ch9-04.md) * [内存同步](ch9/ch9-04.md)
* [sync.Once初始化](ch9/ch9-05.md)  * [sync.Once惰性初始化](ch9/ch9-05.md)
* [竞争条件检测](ch9/ch9-06.md) * [竞争条件检测](ch9/ch9-06.md)
* [示例: 并发的非阻塞缓存](ch9/ch9-07.md) * [示例: 并发的非阻塞缓存](ch9/ch9-07.md)
* [Goroutines和线程](ch9/ch9-08.md) * [Goroutines和线程](ch9/ch9-08.md)

View File

@ -4,7 +4,7 @@
当创建一个包一般要用短小的包名但也不能太短导致难以理解。标准库中最常用的包有bufio、bytes、flag、fmt、http、io、json、os、sort、sync和time等包。 当创建一个包一般要用短小的包名但也不能太短导致难以理解。标准库中最常用的包有bufio、bytes、flag、fmt、http、io、json、os、sort、sync和time等包。
它们的名字都简洁明了。例如不要将一个类似imageutil或ioutilis的通用包命名为util虽然它看起来很短小。要尽量避免包名使用可能被经常用于局部变量的名字这样可能导致用户重命名导入包例如前面看到的path包。 尽可能让命名有描述性且无歧义。例如类似imageutil或ioutilis的工具包命名已经足够简洁了就无须再命名为util了。要尽量避免包名使用可能被经常用于局部变量的名字这样可能导致用户重命名导入包例如前面看到的path包。
包名一般采用单数的形式。标准库的bytes、errors和strings使用了复数形式这是为了避免和预定义的类型冲突同样还有go/types是为了避免和type关键字冲突。 包名一般采用单数的形式。标准库的bytes、errors和strings使用了复数形式这是为了避免和预定义的类型冲突同样还有go/types是为了避免和type关键字冲突。

View File

@ -9,7 +9,7 @@ UTF8是一个将Unicode码点编码为字节序列的变长编码。UTF8编码
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 65536-0x10ffff (other values unused) 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 65536-0x10ffff (other values unused)
``` ```
变长的编码无法直接通过索引来访问第n个字符但是UTF8编码获得了很多额外的优点。首先UTF8编码比较紧凑完全兼容ASCII码并且可以自动同步它可以通过向前回朔最多2个字节就能确定当前字符编码的开始字节的位置。它也是一个前缀编码所以当从左向右解码时不会有任何歧义也并不需要向前查看译注像GBK之类的编码如果不知道起点位置则可能会出现歧义。没有任何字符的编码是其它字符编码的子串或是其它编码序列的字串因此搜索一个字符时只要搜索它的字节编码序列即可不用担心前后的上下文会对搜索结果产生干扰。同时UTF8编码的顺序和Unicode码点的顺序一致因此可以直接排序UTF8编码序列。同时因为没有嵌入的NUL(0)字节可以很好地兼容那些使用NUL作为字符串结尾的编程语言。 变长的编码无法直接通过索引来访问第n个字符但是UTF8编码获得了很多额外的优点。首先UTF8编码比较紧凑完全兼容ASCII码并且可以自动同步它可以通过向前回朔最多3个字节就能确定当前字符编码的开始字节的位置。它也是一个前缀编码所以当从左向右解码时不会有任何歧义也并不需要向前查看译注像GBK之类的编码如果不知道起点位置则可能会出现歧义。没有任何字符的编码是其它字符编码的子串或是其它编码序列的字串因此搜索一个字符时只要搜索它的字节编码序列即可不用担心前后的上下文会对搜索结果产生干扰。同时UTF8编码的顺序和Unicode码点的顺序一致因此可以直接排序UTF8编码序列。同时因为没有嵌入的NUL(0)字节可以很好地兼容那些使用NUL作为字符串结尾的编程语言。
Go语言的源文件采用UTF8编码并且Go语言处理UTF8编码的文本也很出色。unicode包提供了诸多处理rune字符相关功能的函数比如区分字母和数字或者是字母的大写和小写转换等unicode/utf8包则提供了用于rune字符序列的UTF8编码和解码的功能。 Go语言的源文件采用UTF8编码并且Go语言处理UTF8编码的文本也很出色。unicode包提供了诸多处理rune字符相关功能的函数比如区分字母和数字或者是字母的大写和小写转换等unicode/utf8包则提供了用于rune字符序列的UTF8编码和解码的功能。

View File

@ -51,7 +51,7 @@ linenum, name := 12, "count"
errorf(linenum, "undefined: %s", name) // "Line 12: undefined: count" errorf(linenum, "undefined: %s", name) // "Line 12: undefined: count"
``` ```
interfac{}表示函数的最后一个参数可以接收任意类型我们会在第7章详细介绍。 interface{}表示函数的最后一个参数可以接收任意类型我们会在第7章详细介绍。
**练习5.15** 编写类似sum的可变参数函数max和min。考虑不传参时max和min该如何处理再编写至少接收1个参数的版本。 **练习5.15** 编写类似sum的可变参数函数max和min。考虑不传参时max和min该如何处理再编写至少接收1个参数的版本。

View File

@ -46,7 +46,7 @@ title: https://golang.org/doc/gopher/frontpage.png has type image/png, not text/
resp.Body.close调用了多次这是为了确保title在所有执行路径下即使函数运行失败都关闭了网络连接。随着函数变得复杂需要处理的错误也变多维护清理逻辑变得越来越困难。而Go语言独有的defer机制可以让事情变得简单。 resp.Body.close调用了多次这是为了确保title在所有执行路径下即使函数运行失败都关闭了网络连接。随着函数变得复杂需要处理的错误也变多维护清理逻辑变得越来越困难。而Go语言独有的defer机制可以让事情变得简单。
你只需要在调用普通函数或方法前加上关键字defer就完成了defer所需要的语法。当defer语句被执行时跟在defer后面的函数会被延迟执行。直到包含该defer语句的函数执行完毕时defer后的函数才会被执行不论包含defer语句的函数是通过return正常结束还是由于panic导致的异常结束。你可以在一个函数中执行多条defer语句它们的执行顺序与声明顺序相反。 你只需要在调用普通函数或方法前加上关键字defer就完成了defer所需要的语法。当执行到该条语句时,函数和参数表达式得到计算,但直到包含该defer语句的函数执行完毕时defer后的函数才会被执行不论包含defer语句的函数是通过return正常结束还是由于panic导致的异常结束。你可以在一个函数中执行多条defer语句它们的执行顺序与声明顺序相反。
defer语句经常被用于处理成对的操作如打开、关闭、连接、断开连接、加锁、释放锁。通过defer机制不论函数逻辑多复杂都能保证在任何执行路径下资源被释放。释放资源的defer应该直接跟在请求资源的语句后。在下面的代码中一条defer语句替代了之前的所有resp.Body.Close defer语句经常被用于处理成对的操作如打开、关闭、连接、断开连接、加锁、释放锁。通过defer机制不论函数逻辑多复杂都能保证在任何执行路径下资源被释放。释放资源的defer应该直接跟在请求资源的语句后。在下面的代码中一条defer语句替代了之前的所有resp.Body.Close

View File

@ -48,11 +48,11 @@ switch语句可以简化if-else链如果这个if-else链对一连串值做相
```go ```go
switch x.(type) { switch x.(type) {
case nil: // ... case nil: // ...
case int, uint: // ... case int, uint: // ...
case bool: // ... case bool: // ...
case string: // ... case string: // ...
default: // ... default: // ...
} }
``` ```

View File

@ -29,7 +29,7 @@ func main() {
} }
``` ```
当用户关闭了标准输入主goroutine中的mustCopy函数调用将返回然后调用conn.Close()关闭读和写方向的网络连接。关闭网络连接中的写方向的连接将导致server程序收到一个文件end-of-file结束的信号。关闭网络连接中读方向的连接将导致后台goroutine的io.Copy函数调用返回一个“read from closed connection”“从关闭的连接读”类似的错误因此我们临时移除了错误日志语句在练习8.3将会提供一个更好的解决方案。需要注意的是go语句调用了一个函数字面量这Go语言中启动goroutine常用的形式。 当用户关闭了标准输入主goroutine中的mustCopy函数调用将返回然后调用conn.Close()关闭读和写方向的网络连接。关闭网络连接中的写方向的连接将导致server程序收到一个文件end-of-file结束的信号。关闭网络连接中读方向的连接将导致后台goroutine的io.Copy函数调用返回一个“read from closed connection”“从关闭的连接读”类似的错误因此我们临时移除了错误日志语句在练习8.3将会提供一个更好的解决方案。需要注意的是go语句调用了一个函数字面量Go语言中启动goroutine常用的形式。
在后台goroutine返回之前它先打印一个日志信息然后向done对应的channel发送一个值。主goroutine在退出前先等待从done对应的channel接收一个值。因此总是可以在程序退出前正确输出“done”消息。 在后台goroutine返回之前它先打印一个日志信息然后向done对应的channel发送一个值。主goroutine在退出前先等待从done对应的channel接收一个值。因此总是可以在程序退出前正确输出“done”消息。

View File

@ -51,7 +51,7 @@ fmt.Println(<-ch) // "B"
fmt.Println(<-ch) // "C" fmt.Println(<-ch) // "C"
``` ```
在这个例子中发送和接收操作都发生在同一个goroutine中但是在真实的程序中它们一般由不同的goroutine执行。Go语言新手有时候会将一个带缓存的channel当作同一个goroutine中的队列使用虽然语法看似简单但实际上这是一个错误。Channel和goroutine的调度器机制是紧密相连的一个发送操作——或许是整个程序——可能会永远阻塞。如果你只是需要一个简单的队列使用slice就可以了。 在这个例子中发送和接收操作都发生在同一个goroutine中但是在真实的程序中它们一般由不同的goroutine执行。Go语言新手有时候会将一个带缓存的channel当作同一个goroutine中的队列使用虽然语法看似简单但实际上这是一个错误。Channel和goroutine的调度器机制是紧密相连的如果没有其他goroutine从channel接收发送者——或许是整个程序——将会面临永远阻塞的风险。如果你只是需要一个简单的队列使用slice就可以了。
下面的例子展示了一个使用了带缓存channel的应用。它并发地向三个镜像站点发出请求三个镜像站点分散在不同的地理位置。它们分别将收到的响应发送到带缓存channel最后接收者只接收第一个收到的响应也就是最快的那个响应。因此mirroredQuery函数可能在另外两个响应慢的镜像站点响应之前就返回了结果。顺便说一下多个goroutines并发地向同一个channel发送数据或从同一个channel接收数据都是常见的用法。 下面的例子展示了一个使用了带缓存channel的应用。它并发地向三个镜像站点发出请求三个镜像站点分散在不同的地理位置。它们分别将收到的响应发送到带缓存channel最后接收者只接收第一个收到的响应也就是最快的那个响应。因此mirroredQuery函数可能在另外两个响应慢的镜像站点响应之前就返回了结果。顺便说一下多个goroutines并发地向同一个channel发送数据或从同一个channel接收数据都是常见的用法。

View File

@ -131,7 +131,7 @@ $ ./du2 -v $HOME /usr /bin /etc
213201 files 62.7 GB 213201 files 62.7 GB
``` ```
然而这个程序还是会花上很长时间才会结束。无法对walkDir做并行化处理没什么别的原因无非是因为磁盘系统并行限制。下面这个第三个版本的du会对每一个walkDir的调用创建一个新的goroutine。它使用sync.WaitGroup (§8.5)来对仍旧活跃的walkDir调用进行计数另一个goroutine会在计数器减为零的时候将fileSizes这个channel关闭。 然而这个程序还是会花上很长时间才会结束。完全可以并发调用walkDir从而发挥磁盘系统的并行性能。下面这个第三个版本的du会对每一个walkDir的调用创建一个新的goroutine。它使用sync.WaitGroup (§8.5)来对仍旧活跃的walkDir调用进行计数另一个goroutine会在计数器减为零的时候将fileSizes这个channel关闭。
<u><i>gopl.io/ch8/du3</i></u> <u><i>gopl.io/ch8/du3</i></u>
```go ```go

View File

@ -114,7 +114,7 @@ You are 127.0.0.1:64216 127.0.0.1:64216 has arrived
当与n个客户端保持聊天session时这个程序会有2n+2个并发的goroutine然而这个程序却并不需要显式的锁(§9.2)。clients这个map被限制在了一个独立的goroutine中broadcaster所以它不能被并发地访问。多个goroutine共享的变量只有这些channel和net.Conn的实例两个东西都是并发安全的。我们会在下一章中更多地讲解约束并发安全以及goroutine中共享变量的含义。 当与n个客户端保持聊天session时这个程序会有2n+2个并发的goroutine然而这个程序却并不需要显式的锁(§9.2)。clients这个map被限制在了一个独立的goroutine中broadcaster所以它不能被并发地访问。多个goroutine共享的变量只有这些channel和net.Conn的实例两个东西都是并发安全的。我们会在下一章中更多地讲解约束并发安全以及goroutine中共享变量的含义。
**练习 8.12** 使broadcaster能够将arrival事件通知当前所有的客户端。为了达成这个目的,你需要有一个客户端的集合,并且在entering和leaving的channel中记录客户端的名字。 **练习 8.12** 使broadcaster能够将arrival事件通知当前所有的客户端。这需要你在clients集合中以及entering和leaving的channel中记录客户端的名字。
**练习 8.13** 使聊天服务器能够断开空闲的客户端连接比如最近五分钟之后没有发送任何消息的那些客户端。提示可以在其它goroutine中调用conn.Close()来解除Read调用就像input.Scanner()所做的那样。 **练习 8.13** 使聊天服务器能够断开空闲的客户端连接比如最近五分钟之后没有发送任何消息的那些客户端。提示可以在其它goroutine中调用conn.Close()来解除Read调用就像input.Scanner()所做的那样。

View File

@ -2,7 +2,7 @@
在一个线性(就是说只有一个goroutine的)的程序中,程序的执行顺序只由程序的逻辑来决定。例如,我们有一段语句序列,第一个在第二个之前(废话)以此类推。在有两个或更多goroutine的程序中每一个goroutine内的语句也是按照既定的顺序去执行的但是一般情况下我们没法去知道分别位于两个goroutine的事件x和y的执行顺序x是在y之前还是之后还是同时发生是没法判断的。当我们没有办法自信地确认一个事件是在另一个事件的前面或者后面发生的话就说明x和y这两个事件是并发的。 在一个线性(就是说只有一个goroutine的)的程序中,程序的执行顺序只由程序的逻辑来决定。例如,我们有一段语句序列,第一个在第二个之前(废话)以此类推。在有两个或更多goroutine的程序中每一个goroutine内的语句也是按照既定的顺序去执行的但是一般情况下我们没法去知道分别位于两个goroutine的事件x和y的执行顺序x是在y之前还是之后还是同时发生是没法判断的。当我们没有办法自信地确认一个事件是在另一个事件的前面或者后面发生的话就说明x和y这两个事件是并发的。
考虑一下,一个函数在线性程序中可以正确地工作。如果在并发的情况下,这个函数依然可以正确地工作的话,那么我们就说这个函数是并发安全的,并发安全的函数不需要额外的同步工作。我们可以把这个概念概括为一个特定类型的一些方法和操作函数,对于某个类型来说,如果其所有可访问的方法和操作都是并发安全的话,那么类型便是并发安全的。 考虑一下,一个函数在线性程序中可以正确地工作。如果在并发的情况下,这个函数依然可以正确地工作的话,那么我们就说这个函数是并发安全的,并发安全的函数不需要额外的同步工作。我们可以把这个概念概括为一个特定类型的一些方法和操作函数,对于某个类型来说,如果其所有可访问的方法和操作都是并发安全的话,那么类型便是并发安全的。
在一个程序中有非并发安全的类型的情况下我们依然可以使这个程序并发安全。确实并发安全的类型是例外而不是规则所以只有当文档中明确地说明了其是并发安全的情况下你才可以并发地去访问它。我们会避免并发访问大多数的类型无论是将变量局限在单一的一个goroutine内还是用互斥条件维持更高级别的不变性都是为了这个目的。我们会在本章中说明这些术语。 在一个程序中有非并发安全的类型的情况下我们依然可以使这个程序并发安全。确实并发安全的类型是例外而不是规则所以只有当文档中明确地说明了其是并发安全的情况下你才可以并发地去访问它。我们会避免并发访问大多数的类型无论是将变量局限在单一的一个goroutine内还是用互斥条件维持更高级别的不变性都是为了这个目的。我们会在本章中说明这些术语。
@ -41,10 +41,10 @@ Alice存了$200然后检查她的余额同时Bob存了$100。因为A1和A2
``` ```
Alice first Bob first Alice/Bob/Alice Alice first Bob first Alice/Bob/Alice
0 0 0 0 0 0
A1 200 B 100 A1 200 A1 200 B 100 A1 200
A2 "=200" A1 300 B 300 A2 "= 200" A1 300 B 300
B 300 A2 "=300" A2 "=300" B 300 A2 "= 300" A2 "= 300"
``` ```
所有情况下最终的余额都是$300。唯一的变数是Alice的余额单是否包含了Bob交易不过无论怎么着客户都不会在意。 所有情况下最终的余额都是$300。唯一的变数是Alice的余额单是否包含了Bob交易不过无论怎么着客户都不会在意。
@ -110,7 +110,7 @@ var icons = map[string]image.Image{
func Icon(name string) image.Image { return icons[name] } func Icon(name string) image.Image { return icons[name] }
``` ```
上面的例子里icons变量在包初始化阶段就已经被赋值了包的初始化是在程序main函数开始执行之前就完成了的。只要初始化完成了icons就再也不会修改的或者不变量是本来就并发安全的这种变量不需要进行同步。不过显然我们没法用这种方法因为update操作是必要的操作尤其对于银行账户来说 上面的例子里icons变量在包初始化阶段就已经被赋值了包的初始化是在程序main函数开始执行之前就完成了的。只要初始化完成了icons就再也不会被修改。数据结构如果从不被修改或是不变量则是并发安全的无需进行同步。不过显然如果update操作是必要的我们就没法用这种方法比如说银行账户
第二种避免数据竞争的方法是避免从多个goroutine访问变量。这也是前一章中大多数程序所采用的方法。例如前面的并发web爬虫(§8.6)的main goroutine是唯一一个能够访问seen map的goroutine而聊天服务器(§8.10)中的broadcaster goroutine是唯一一个能够访问clients map的goroutine。这些变量都被限定在了一个单独的goroutine中。 第二种避免数据竞争的方法是避免从多个goroutine访问变量。这也是前一章中大多数程序所采用的方法。例如前面的并发web爬虫(§8.6)的main goroutine是唯一一个能够访问seen map的goroutine而聊天服务器(§8.10)中的broadcaster goroutine是唯一一个能够访问clients map的goroutine。这些变量都被限定在了一个单独的goroutine中。

View File

@ -102,7 +102,7 @@ func Withdraw(amount int) bool {
上面这个例子中Deposit会调用mu.Lock()第二次去获取互斥锁但因为mutex已经锁上了而无法被重入(译注go里没有重入锁关于重入锁的概念请参考java)--也就是说没法对一个已经锁上的mutex来再次上锁--这会导致程序死锁没法继续执行下去Withdraw会永远阻塞下去。 上面这个例子中Deposit会调用mu.Lock()第二次去获取互斥锁但因为mutex已经锁上了而无法被重入(译注go里没有重入锁关于重入锁的概念请参考java)--也就是说没法对一个已经锁上的mutex来再次上锁--这会导致程序死锁没法继续执行下去Withdraw会永远阻塞下去。
关于Go的互斥量不能重入这一点我们有很充分的理由。互斥量的目的是为了确保共享变量在程序执行时的关键点上能够保证不变性。不变性的其中之一是“没有goroutine访问共享变量”但实际上这里对于mutex保护的变量来说不变性还包括其它方面。当一个goroutine获得了一个互斥锁时它会断定这种不变性能够被保持。在其获取并保持锁期间可能会去更新共享变量这样不变性只是短暂地被破坏。然而当其释放锁之后它必须保证不变性已经恢复原样。尽管一个可以重入的mutex也可以保证没有其它的goroutine在访问共享变量但这种方式没法保证这些变量额外的不变性。(译注:这段翻译有点晕) 关于Go的mutex不能重入这一点我们有很充分的理由。mutex的目的是确保共享变量在程序执行时的关键点上能够保证不变性。不变性的其中之一是“没有goroutine访问共享变量”但实际上这里对于mutex保护的变量来说不变性还包括其它方面。当一个goroutine获得了一个互斥锁时它会断定这种不变性能够被保持。在其获取并保持锁期间可能会去更新共享变量这样不变性只是短暂地被破坏。然而当其释放锁之后它必须保证不变性已经恢复原样。尽管一个可以重入的mutex也可以保证没有其它的goroutine在访问共享变量但这种方式没法保证这些变量额外的不变性。(译注:这段翻译有点晕)
一个通用的解决方案是将一个函数分离为多个函数比如我们把Deposit分离成两个一个不导出的函数deposit这个函数假设锁总是会被保持并去做实际的操作另一个是导出的函数Deposit这个函数会调用deposit但在调用前会先去获取锁。同理我们可以将Withdraw也表示成这种形式 一个通用的解决方案是将一个函数分离为多个函数比如我们把Deposit分离成两个一个不导出的函数deposit这个函数假设锁总是会被保持并去做实际的操作另一个是导出的函数Deposit这个函数会调用deposit但在调用前会先去获取锁。同理我们可以将Withdraw也表示成这种形式

View File

@ -1,4 +1,4 @@
## 9.5. sync.Once初始化 ## 9.5. sync.Once惰性初始化
如果初始化成本比较大的话那么将初始化延迟到需要的时候再去做就是一个比较好的选择。如果在程序启动的时候就去做这类初始化的话会增加程序的启动时间并且因为执行的时候可能也并不需要这些变量所以实际上有一些浪费。让我们来看在本章早一些时候的icons变量 如果初始化成本比较大的话那么将初始化延迟到需要的时候再去做就是一个比较好的选择。如果在程序启动的时候就去做这类初始化的话会增加程序的启动时间并且因为执行的时候可能也并不需要这些变量所以实际上有一些浪费。让我们来看在本章早一些时候的icons变量
@ -101,6 +101,6 @@ func Icon(name string) image.Image {
} }
``` ```
每一次对Do(loadIcons)的调用都会锁定mutex并会检查boolean变量。在第一次调用时boolean变量的值是falseDo会调用loadIcons并会将boolean变量设置为true。随后的调用什么都不会做但是mutex同步会保证loadIcons对内存(这里其实就是指icons变量啦)产生的效果能够对所有goroutine可见。用这种方式来使用sync.Once的话我们能够避免在变量被构建完成之前和其它goroutine共享该变量。 每一次对Do(loadIcons)的调用都会锁定mutex并会检查boolean变量(译注Go1.9中会先判断boolean变量是否为1(true)只有不为1才锁定mutex不再需要每次都锁定mutex)。在第一次调用时boolean变量的值是falseDo会调用loadIcons并会将boolean变量设置为true。随后的调用什么都不会做但是mutex同步会保证loadIcons对内存(这里其实就是指icons变量啦)产生的效果能够对所有goroutine可见。用这种方式来使用sync.Once的话我们能够避免在变量被构建完成之前和其它goroutine共享该变量。
**练习 9.2** 重写2.6.2节中的PopCount的例子使用sync.Once只在第一次需要用到的时候进行初始化。(虽然实际上对PopCount这样很小且高度优化的函数进行同步可能代价没法接受) **练习 9.2** 重写2.6.2节中的PopCount的例子使用sync.Once只在第一次需要用到的时候进行初始化。(虽然实际上对PopCount这样很小且高度优化的函数进行同步可能代价没法接受)