gopl-zh.github.com/ch2/ch2-06.md
2022-08-04 14:58:52 +08:00

198 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

## 2.6. 包和文件
Go语言中的包和其他语言的库或模块的概念类似目的都是为了支持模块化、封装、单独编译和代码重用。一个包的源代码保存在一个或多个以.go为文件后缀名的源文件中通常一个包所在目录路径的后缀是包的导入路径例如包gopl.io/ch1/helloworld对应的目录路径是$GOPATH/src/gopl.io/ch1/helloworld。
每个包都对应一个独立的名字空间。例如在image包中的Decode函数和在unicode/utf16包中的 Decode函数是不同的。要在外部引用该函数必须显式使用image.Decode或utf16.Decode形式访问。
包还可以让我们通过控制哪些名字是外部可见的来隐藏内部实现信息。在Go语言中一个简单的规则是如果一个名字是大写字母开头的那么该名字是导出的译注因为汉字不区分大小写因此汉字开头的名字是没有导出的
为了演示包基本的用法先假设我们的温度转换软件已经很流行我们希望到Go语言社区也能使用这个包。我们该如何做呢
让我们创建一个名为gopl.io/ch2/tempconv的包这是前面例子的一个改进版本。这里我们没有按照惯例按顺序对例子进行编号因此包路径看起来更像一个真实的包包代码存储在两个源文件中用来演示如何在一个源文件声明然后在其他的源文件访问虽然在现实中这样小的包一般只需要一个文件。
我们把变量的声明、对应的常量还有方法都放到tempconv.go源文件中
<u><i>gopl.io/ch2/tempconv</i></u>
```Go
// Package tempconv performs Celsius and Fahrenheit conversions.
package tempconv
import "fmt"
type Celsius float64
type Fahrenheit float64
const (
AbsoluteZeroC Celsius = -273.15
FreezingC Celsius = 0
BoilingC Celsius = 100
)
func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }
func (f Fahrenheit) String() string { return fmt.Sprintf("%g°F", f) }
```
转换函数则放在另一个conv.go源文件中
```Go
package tempconv
// CToF converts a Celsius temperature to Fahrenheit.
func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }
// FToC converts a Fahrenheit temperature to Celsius.
func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }
```
每个源文件都是以包的声明语句开始用来指明包的名字。当包被导入的时候包内的成员将通过类似tempconv.CToF的形式访问。而包级别的名字例如在一个文件声明的类型和常量在同一个包的其他源文件也是可以直接访问的就好像所有代码都在一个文件一样。要注意的是tempconv.go源文件导入了fmt包但是conv.go源文件并没有因为这个源文件中的代码并没有用到fmt包。
因为包级别的常量名都是以大写字母开头它们可以像tempconv.AbsoluteZeroC这样被外部代码访问
```Go
fmt.Printf("Brrrr! %v\n", tempconv.AbsoluteZeroC) // "Brrrr! -273.15°C"
```
要将摄氏温度转换为华氏温度需要先用import语句导入gopl.io/ch2/tempconv包然后就可以使用下面的代码进行转换了
```Go
fmt.Println(tempconv.CToF(tempconv.BoilingC)) // "212°F"
```
在每个源文件的包声明前紧跟着的注释是包注释§10.7.4。通常包注释的第一句应该先是包的功能概要说明。一个包通常只有一个源文件有包注释译注如果有多个包注释目前的文档工具会根据源文件名的先后顺序将它们链接为一个包注释。如果包注释很大通常会放到一个独立的doc.go文件中。
**练习 2.1** 向tempconv包添加类型、常量和函数用来处理Kelvin绝对温度的转换Kelvin 绝对零度是273.15°CKelvin绝对温度1K和摄氏度1°C的单位间隔是一样的。
### 2.6.1. 导入包
在Go语言程序中每个包都有一个全局唯一的导入路径。导入语句中类似"gopl.io/ch2/tempconv"的字符串对应包的导入路径。Go语言的规范并没有定义这些字符串的具体含义或包来自哪里它们是由构建工具来解释的。当使用Go语言自带的go工具箱时第十章一个导入路径代表一个目录中的一个或多个Go源文件。
除了包的导入路径每个包还有一个包名包名一般是短小的名字并不要求包名是唯一的包名在包的声明处指定。按照惯例一个包的名字和包的导入路径的最后一个字段相同例如gopl.io/ch2/tempconv包的名字一般是tempconv。
要使用gopl.io/ch2/tempconv包需要先导入
<u><i>gopl.io/ch2/cf</i></u>
```Go
// Cf converts its numeric argument to Celsius and Fahrenheit.
package main
import (
"fmt"
"os"
"strconv"
"gopl.io/ch2/tempconv"
)
func main() {
for _, arg := range os.Args[1:] {
t, err := strconv.ParseFloat(arg, 64)
if err != nil {
fmt.Fprintf(os.Stderr, "cf: %v\n", err)
os.Exit(1)
}
f := tempconv.Fahrenheit(t)
c := tempconv.Celsius(t)
fmt.Printf("%s = %s, %s = %s\n",
f, tempconv.FToC(f), c, tempconv.CToF(c))
}
}
```
导入语句将导入的包绑定到一个短小的名字然后通过该短小的名字就可以引用包中导出的全部内容。上面的导入声明将允许我们以tempconv.CToF的形式来访问gopl.io/ch2/tempconv包中的内容。在默认情况下导入的包绑定到tempconv名字译注指包声明语句指定的名字但是我们也可以绑定到另一个名称以避免名字冲突§10.4)。
cf程序将命令行输入的一个温度在Celsius和Fahrenheit温度单位之间转换
```
$ go build gopl.io/ch2/cf
$ ./cf 32
32°F = 0°C, 32°C = 89.6°F
$ ./cf 212
212°F = 100°C, 212°C = 413.6°F
$ ./cf -40
-40°F = -40°C, -40°C = -40°F
```
如果导入了一个包但是又没有使用该包将被当作一个编译错误处理。这种强制规则可以有效减少不必要的依赖虽然在调试期间可能会让人讨厌因为删除一个类似log.Print("got here!")的打印语句可能导致需要同时删除log包导入声明否则编译器将会发出一个错误。在这种情况下我们需要将不必要的导入删除或注释掉。
不过有更好的解决方案我们可以使用golang.org/x/tools/cmd/goimports导入工具它可以根据需要自动添加或删除导入的包许多编辑器都可以集成goimports工具然后在保存文件的时候自动运行。类似的还有gofmt工具可以用来格式化Go源文件。
**练习 2.2** 写一个通用的单位转换程序用类似cf程序的方式从命令行读取参数如果缺省的话则是从标准输入读取参数然后做类似Celsius和Fahrenheit的单位转换长度单位可以对应英尺和米重量单位可以对应磅和公斤等。
### 2.6.2. 包的初始化
包的初始化首先是解决包级变量的依赖顺序,然后按照包级变量声明出现的顺序依次初始化:
```Go
var a = b + c // a 第三个初始化, 为 3
var b = f() // b 第二个初始化, 为 2, 通过调用 f (依赖c)
var c = 1 // c 第一个初始化, 为 1
func f() int { return c + 1 }
```
如果包中含有多个.go源文件它们将按照发给编译器的顺序进行初始化Go语言的构建工具首先会将.go文件根据文件名排序然后依次调用编译器编译。
对于在包级别声明的变量如果有初始化表达式则用表达式初始化还有一些没有初始化表达式的例如某些表格数据初始化并不是一个简单的赋值过程。在这种情况下我们可以用一个特殊的init初始化函数来简化初始化工作。每个文件都可以包含多个init初始化函数
```Go
func init() { /* ... */ }
```
这样的init初始化函数除了不能被调用或引用外其他行为和普通函数类似。在每个文件中的init初始化函数在程序开始执行时按照它们声明的顺序被自动调用。
每个包在解决依赖的前提下以导入声明的顺序初始化每个包只会被初始化一次。因此如果一个p包导入了q包那么在p包初始化的时候可以认为q包必然已经初始化过了。初始化工作是自下而上进行的main包最后被初始化。以这种方式可以确保在main函数执行之前所有依赖的包都已经完成初始化工作了。
下面的代码定义了一个PopCount函数用于返回一个数字中含二进制1bit的个数。它使用init初始化函数来生成辅助表格pcpc表格用于处理每个8bit宽度的数字含二进制的1bit的bit个数这样的话在处理64bit宽度的数字时就没有必要循环64次只需要8次查表就可以了。这并不是最快的统计1bit数目的算法但是它可以方便演示init函数的用法并且演示了如何预生成辅助表格这是编程中常用的技术
<u><i>gopl.io/ch2/popcount</i></u>
```Go
package popcount
// pc[i] is the population count of i.
var pc [256]byte
func init() {
for i := range pc {
pc[i] = pc[i/2] + byte(i&1)
}
}
// PopCount returns the population count (number of set bits) of x.
func PopCount(x uint64) int {
return int(pc[byte(x>>(0*8))] +
pc[byte(x>>(1*8))] +
pc[byte(x>>(2*8))] +
pc[byte(x>>(3*8))] +
pc[byte(x>>(4*8))] +
pc[byte(x>>(5*8))] +
pc[byte(x>>(6*8))] +
pc[byte(x>>(7*8))])
}
```
译注对于pc这类需要复杂处理的初始化可以通过将初始化逻辑包装为一个匿名函数处理像下面这样
```Go
// pc[i] is the population count of i.
var pc [256]byte = func() (pc [256]byte) {
for i := range pc {
pc[i] = pc[i/2] + byte(i&1)
}
return
}()
```
要注意的是在init函数中range循环只使用了索引省略了没有用到的值部分。循环也可以这样写
```Go
for i, _ := range pc {
```
我们在下一节和10.5节还将看到其它使用init函数的地方。
**练习 2.3** 重写PopCount函数用一个循环代替单一的表达式。比较两个版本的性能。11.4节将展示如何系统地比较两个不同实现的性能。)
**练习 2.4** 用移位算法重写PopCount函数每次测试最右边的1bit然后统计总数。比较和查表算法的性能差异。
**练习 2.5** 表达式`x&(x-1)`用于将x的最低的一个非零的bit位清零。使用这个算法重写PopCount函数然后比较性能。