2.6 包和文件
在Go语言中包的作用和其他语言中的库或模块作用类似,用于支持模块化、封装、编译隔离和重用。一个包的源代码保存在一个或多个以.go结尾的文件中,它所在目录名的尾部就是包的导入路径,例如,gopl.io/ch1/helloworld包的文件存储在目录$GOPATH/src/gopl.io/ch1/helloworld中。
每一个包给它的声明提供独立的命名空间。例如,在image包中,Decode标识符和unicode/utf16包中的标识符一样,但是关联了不同的函数。为了从包外部引用一个函数,我们必须明确修饰标识符来指明所指的是image.Decode或utf16.Decode。
包让我们可以通过控制变量在包外面的可见性或导出情况来隐藏信息。在Go里,通过一条简单的规则来管理标识符是否对外可见:导出的标识符以大写字母开头。
为了说明基本原理,假设温度转换软件很受欢迎,我们想把它作为新包贡献给Go社区,将要怎么做呢?
我们创建一个叫作gopl.io/ch2/tempconv的包,这是前面例子的变种(这里我们没有照惯例对例子进行顺序编号,目的是让包路径更实际一些)。包自己保存在两个文件里,以展示如何访问一个包里面多个独立文件中的声明。现实中,像这样的小包可能只需要一个文件。
将类型、它们的常量及方法的声明放在tempconv.go中:
将转换函数放在conv.go中:
每一个文件的开头用package声明定义包的名称。当导入包时,它的成员通过诸如tempconv.CToF等方式被引用。如果包级别的名字(像类型和常量)在包的一个文件中声明,就像所有的源代码在同一个文件中一样,它们对于同一个包中的其他文件可见。注意,tempconv.go导入fmt包,但是conv.go没有,因为它本身没有用到fmt包。
因为包级别的常量名字以大写字母开头,所以它们也可以使用修饰过的名称(如tempconv.AbsoluteZeroC)来访问:
为了在某个包里将摄氏温度转换为华氏温度,导入包gopl.io/ch2/tempconv,然后编写下面的代码:
package声明前面紧挨着的文档注释(参考10.7.4节)对整个包进行描述。习惯上,应该在开头用一句话对包进行总结性的描述。每一个包里只有一个文件应该包含该包的文档注释。扩展的文档注释通常放在一个文件中,按惯例名字叫作doc.go。
练习2.1:添加类型、常量和函数到tempconv包中,处理以开尔文为单位(K)的温度值,0K=-273.15℃,变化1K和变化1℃是等价的。
2.6.1 导入
在Go程序里,每一个包通过称为导入路径(import path)的唯一字符串来标识。它们出现在诸如"gopl.io/ch2/tempconv"之类的import声明中。语言的规范没有定义哪些字符串从哪来以及它们的含义,这依赖于工具来解释。当使用go工具(参考第10章)时,一个导入路径标注一个目录,目录中包含构成包的一个或多个Go源文件。除了导入路径之外,每个包还有一个包名,它以短名字的形式(且不必是唯一的)出现在包的声明中。按约定,包名匹配导入路径的最后一段,这样可以方便地预测gopl.io/ch2/tempconv的包名是tempconv。
为了使用gopl.io/ch2/tempconv,必须导入它:
导入声明可以给导入的包绑定一个短名字,用来在整个文件中引用包的内容。上面的import可以使用修饰标识符来引用gopl.io/ch2/tempconv包里的变量名,如tempconv.CToF。默认这个短名字是包名,在本例中是tempconv,但是导入声明可以设定一个可选的名字来避免冲突(参考10.4节)。
cf程序将一个数字型的命令行参数分别转换成摄氏温度和华氏温度:
如果导入一个没有被引用的包,就会触发一个错误。这个检查帮助消除代码演进过程中不再需要的依赖(尽管它在调试过程中会带来一些麻烦),因为注释掉一条诸如log.Print("got here!")之类的代码,可能去除了对于log包唯一的一个引用,导致编译器报错。这种情况下,需要注释掉或者删掉不必要的import。
练习2.2:写一个类似于cf的通用的单位转换程序,从命令行参数或者标准输入(如果没有参数)获取数字,然后将每一个数字转换为以摄氏温度和华氏温度表示的温度,以英寸和米表示的长度单位,以磅和千克表示的重量,等等。
2.6.2 包初始化
包的初始化从初始化包级别的变量开始,这些变量按照声明顺序初始化,在依赖已解析完毕的情况下,根据依赖的顺序进行。
如果包由多个.go文件组成,初始化按照编译器收到文件的顺序进行:go工具会在调用编译器前将.go文件进行排序。
对于包级别的每一个变量,生命周期从其值被初始化开始,但是对于其他一些变量,比如数据表,初始化表达式不是简单地设置它的初始化值。这种情况下,init函数的机制会比较简单。任何文件可以包含任意数量的声明如下的函数:
这个init函数不能被调用和被引用,另一方面,它也是普通的函数。在每一个文件里,当程序启动的时候,init函数按照它们声明的顺序自动执行。
包的初始化按照在程序中导入的顺序来进行,依赖顺序优先,每次初始化一个包。因此,如果包p导入了包q,可以确保q在p之前已完全初始化。初始化过程是自下向上的,main包最后初始化。在这种方式下,在程序的main函数开始执行前,所有的包已初始化完毕。
下面的包定义了一个PopCount函数,它返回一个数字中被置位的个数,即在一个uint64的值中,值为1的位的个数,这称为种群统计。它使用init函数来针对每一个可能的8位值预计算一个结果表pc,这样PopCount只需要将8个快查表的结果相加而不用进行64步的计算。(这个不是最快的统计位算法,只是方便用来说明init函数,用来展示如何预计算一个数值表,它是一种很有用的编程技术。)
注意,init中的range循环只使用索引;值不是必需的,所以没必要包含进来。循环可以重写为下面的形式:
我们将在下一节和10.5节看到init函数的其他用途。
练习2.3:使用循环重写PopCount来代替单个表达式。对比两个版本的效率。(11.4节会展示如何系统性地对比不同实现的性能。)
练习2.4:写一个用于统计位的PopCount,它在其实际参数的64位上执行移位操作,每次判断最右边的位,进而实现统计功能。把它与快查表版本的性能进行对比。
练习2.5:使用x&(x-1)可以清除x最右边的非零位,利用该特点写一个PopCount,然后评价它的性能。