一、Package
Go语言中的包(Package)就像其它语言的库(Library)或模块(Module)一样,支持模块化,封装性,可重用性,单独编译等特点。包的源码是由数个.go文件组成,这些文件所在的目录名是import路径的最后一个词,例如github.com/sunface/corego包的所有文件都存储在$GOPATH/src/github.com/sunface/corego底下。
每个包都有独立的命名空间。例如,在image包中的Decode和unicode/utf16中的Decode是完全不同的函数。如果要引用第三方库的函数,我们要使用package.Func的形式,例如image.Decode和utf16.Decode。
包也允许我们自己控制包内变量、函数的可见性。在Go语言中,变量、函数等的导出只取决于一个因素:名字首字母的大小写。
想象一下,如果我们的温度转换软件开始流行了,然后希望贡献给开源社区,应该怎么做?
首先让我们创建一个包github.com/sunface/temconv,在1.3节的例子基础上做一些变化。这个包中包含了两个文件,演示了怎么样把数据声明和数据访问分开,在现实项目中,这个包实际只需要一个文件。
temconv.go包含了类型声明,常量,类型的method:
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
// 将Celsius温度转换为Fahrenheit
func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }
// 将Fahrenheit温度转换为Celsius.
func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }
每个.go文件的第一行都是package tempconv的声明,表示该文件属于哪个包。当包被导入后,可以这样调用它的成员:tempconv.CToF等等。包级别的变量,例如类型、常量等,对同一个包内的所有文件都是可见的,就好像所有代码定义在同一个文件中一样。注意这里temconv.go导入了fmt.但是conv.go没有,因为它没有用到fmt的任何成员。
上面代码中包级别的const变量,都是大写字母开头的,因此可以在temconv包的外部使用,tempconv.AbsoluteZeroC:
fmt.Println(tempconv.CToF(tempconv.BoilingC)) // "212°F"
我们会选择包内的某个.go文件进行包级别的注释,注释写在该文件的package声明前(见之前的conv.go)。一般来说,这里的注释是对文件进行概述的。一个包只有一个文件需要包级别的注释。这些注释一般会放在doc.go文件中,后续可以通过go doc tempconv来查看包注释。
二、包导入(import)
在Go程序中,每一个包都是通过一个唯一的字符串来标示的,被称为导入路径,这些包是在import声明中统一导入的。Go语言规范不会对导入的包名进行任何约定,这些是由Go的工具来完成解析的。当使用go的工具 (tool)时,一个导入路径代表了一个文件夹,该文件夹内包含了组成包的.go文件。
在import声明中,每个包都有自己的导入包名,按照惯例,这个包名是导入路径的最后一个词,例如github.com/sunface/tempconv的导入包名是temconv:
package main
import (
"fmt"
"os"
"strconv"
"github.com/sunface/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。还可以使用别名机制避免包名冲突:import (
"github.com/sunface/tempconv"
temp "github.com/sunfei/tempconv"
)
调用github.com/sunfei/tempconv:temp.CToF;调用github.com/sunface/tempconv:tempconv.CToF。上面这个程序将单独的数字命令行参数转换成Celsius和Fahrenhit的值。
$ go build github.com/sunface/corego/ch1.4/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
如果导入一个包后不去使用,那么就会报编译错误,编译器的这个检查可以帮助消除不需要的包引用,虽然在debug期间可能会比较蛋疼,例如注释掉log.Print("hello")可能会消除程序对log包的引用,这个时候编译器就会报错。还好,我们可以使用golang.org/x/tools/cmd/goimports工具,它会自动插入和移除包引用,大多数ide都支持配置去使用goimports。
三、包的初始化
包的初始化时会按照声明的顺序初始化包级别的变量,除非变量间有依赖顺序:
var a = b + c // a 第三个初始化 3
var b = f() // b 第二个初始化,调用了f
var c = 1 // c 第一个初始化
func f() int { return c + 1 }
如果某个包有多个.go文件,那这些文件会按照提交给编译器的顺序来初始化,在唤醒编译器前,go tool会通过文件名对.go文件进行排序。任何文件都可以包含任何数目的init函数:
func init() { /* ... */ }
这种init函数不能被调用也不能被引用,在每个文件内,init函数都会在程序刚启动的时候自动运行,文件内每个init函数会按照声明的顺序依次执行。每个包只会被初始化一次,首先是初始化依赖包,如果包p导入q,可以肯定的是:q在p初始化之前肯定会完全初始化。因为依赖包会先初始化,所以程序的初始化是自底向上的,main包肯定是最后一个初始化(包的组织类似一个树形结构,main是根节点)。这样在main函数开始时,所有的包都会初始化完毕!
文章所有权:Golang隐修会 联系人:孙飞,CTO@188.com!
有疑问加站长微信联系(非本文作者)