Golang 第二章 6.包和文件

  • By v2ray节点

  • 2024-08-18 18:22:51

  • 评论

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 中:

// tempconv 包执行摄氏度和华氏度的转换。
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 中的转换函数:

package tempconv
// CToF 将摄氏温度转换为华氏温度。
func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }
// FToC 将华氏温度转换为摄氏度。
func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }

每个文件都以一个包声明开始,该声明定义了包的名称。当包被导入时,可以通过诸如 tempconv.CToF 等方式引用其成员。包级别的名称,例如在包的一个文件中声明的类型和常量,对包的所有其他文件都是可见的,就像所有源代码都在一个文件中一样。需要注意的是,tempconv.go 文件导入了 fmt 包,而 conv.go 文件没有导入,因为它没有使用 fmt 包中的任何内容。

由于包级别的常量名称以大写字母开头,因此它们也可以通过诸如 tempconv.AbsoluteZeroC 之类的限定名称来访问。

fmt.Printf("Brrrr! %v\n", tempconv.AbsoluteZeroC) // "Brrrr! -273.15°C"

要在导入 gopl.io/ch2/temp-conv 的包中将摄氏温度转换为华氏温度,我们可以编写以下代码:

fmt.Println(tempconv.CToF(tempconv.BoilingC)) // "212°F"

紧接在包声明之前的文档注释(第十章 .7.4 节)记录了整个包的文档。按照惯例,它应该以一个总结性的句子开头,如示例所示。每个包中只有一个文件应该包含包的文档注释。通常,将详细的文档注释放在一个单独的文件中,该文件通常命名为 doc.go

练习 2.1:为 tempconv 添加类型、常量和函数,以处理开尔文温标中的温度,其中零开尔文等于 -273.15°C,且 1K 的温度差异与 1°C 的温度差异大小相同。


6.1.Imports导入

在 Go 程序中,每个包都由一个称为导入路径的唯一字符串标识。这些字符串出现在导入声明中,如 "gopl.io/ch2/tempconv"。语言规范并未定义这些字符串的来源或含义;工具负责解释它们。在使用 go 工具时(第 十 章),导入路径表示一个目录,该目录包含一个或多个 Go 源文件,这些文件共同组成包。

除了导入路径,每个包还有一个包名称,这是在包声明中出现的简短(且不一定唯一)的名称。按照惯例,包名与其导入路径的最后一个段相匹配,这使得预测 gopl.io/ch2/tempconv 的包名为 tempconv 变得容易。

要使用 gopl.io/ch2/tempconv,我们必须导入它:

gopl.io/ch2/cf
// Cf 将其数字参数转换为摄氏度和华氏度。
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 程序将一个数值命令行参数转换为摄氏度和华氏度的值:

$ 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 程序,该程序从命令行参数中读取数字,如果没有参数,则从标准输入读取,并将每个数字转换为诸如摄氏度和华氏度的温度单位、英尺和米的长度单位、磅和千克的重量单位等。



6.2.包初始化

包的初始化从初始化包级别的变量开始,按照它们声明的顺序进行,除非存在依赖关系,此时会优先解决依赖关系:

var a = b + c // a initialized third, to 3
var b = f()   // b initialized second, to 2, by calling f
var c = 1     // c initialized first, to 1
func f() int  { return c + 1 }

如果包有多个 .go 文件,这些文件会按照它们传递给编译器的顺序进行初始化;go 工具在调用编译器之前会按名称对 .go 文件进行排序。

每个在包级别声明的变量在初始化时会使用其初始化表达式的值(如果有的话),但对于某些变量,如数据表,初始化表达式可能不是设置初始值的最简单方法。在这种情况下,init 函数机制可能会更简单。任何文件都可以包含任意数量的 init 函数,其声明如下:

func init() { /* ... */ }

这些 init 函数不能被调用或引用,但除此之外它们是普通的函数。在每个文件内,init 函数会在程序启动时自动执行,按照声明的顺序进行。

每次初始化一个包时,按照程序中的导入顺序进行,先初始化依赖项,因此包 p 导入包 q 时,可以确保在 p 的初始化开始之前,包 q 已经完全初始化。初始化过程从底部向上进行;main 包是最后被初始化的。这样,所有包在应用程序的 main 函数开始之前都会被完全初始化。

下面的包定义了一个函数 PopCount,它返回 uint64 值中设置位的数量,即值为 1 的位,这称为其“人口计数”。它使用一个 init 函数预先计算一个结果表 pc,用于每个可能的 8 位值,这样 PopCount 函数就不需要执行 64 步,而只需返回八个表查找的总和。(这绝对不是计算位数最快的算法,但它方便用于说明 init 函数,以及展示如何预先计算值表,这通常是一种有用的编程技术。)

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))])
}

请注意,init 中的 range 循环仅使用索引;该值是不必要的,因此不需要包含。该循环也可以写成

for i, _ := range pc {

我们将在下一节和第十章 .5 节中看到 init 函数的其他用途。

练习 2.3:将 PopCount 重写为使用循环而不是单个表达式。比较这两个版本的性能。(第十一章.4 节展示了如何系统地比较不同实现的性能。)

练习 2.4:编写一个版本的 PopCount,通过将其参数在 64 位位置上逐位移位来计数每一位,每次测试最右边的位。将其性能与表查找版本进行比较。

练习 2.5:表达式 x & (x - 1) 清除 x 的最右边的非零位。编写一个使用这一事实来计数位的 PopCount 版本,并评估其性能。

v2ray节点购买