一、Go 语言源码的组织方式
与许多编程语言一样,Go 语言的源码也是以代码包为基本组织单位的。这些代码包与目录一一对应,子目录对应包的子包。包的目的是为了支持模块化、封装、单独编译和代码重用。
一个代码包中可以包含任意个以.go 为扩展名的源码文件,这些源码文件都需要被声明属于同一个代码包。
每个包都对应一个独立的名字空间,对应一个全局唯一的导入路径。不同包之间可以有同名函数,调用时需要显示通过“包名”来区分。
包还通过控制包内名字的可见性和是否导出来实现封装特性。这允许包的维护者在不影响外部包用户的前提下调整包的内部实现。在Go语言中,一个简单的规则是:如果一个名字是大写字母开头的,那么该名字是导出的(不支持汉字)
所以说,Go 语言源码的组织方式就是以环境变量 GOPATH、工作区、src 目录和代码包为主线的。一般情况下,Go 语言的源码文件都需要被存放在环境变量 GOPATH 包含的某个工作区(目录)中的 src 目录下的某个代码包(目录)中。
二、包的声明和导入
我们通过 package 关键字声明一个包名。包声明语句的主要目的是确定当前包被其它包导入时默认的标识符(也称为包名)。
通过 import 关键字导入一个包。导入语句将导入的包绑定到一个短小的名字,然后通过该短小的名字就可以引用包中导出的全部内容。当然,默认情况下,这个短小的名字就是包名(路径最后一个目录名),但当有名字冲突时,我们也可以给它起一个新的别名(除了解决冲突外,有时候还可以把特别笨重且长的报名简化)。如下所示:
导入声明语句的重要作用是明确指定了当前包和被导入包之间的依赖关系。
import (
"crypto/rand"
mrand "math/rand" // alternative name mrand avoids conflict
)
导入包时,可以一行一个,也可以用圆括号括起来导入多个导入路径,如上代码一样。且后一种形式更为常见。
如果我们导入一个包,但是并没有使用,将会导致“unused import”的编译错误。如果我们只是想利用导入包而产生的副作用:它会计算包级变量的初始化表达式和执行导入包的init初始化函数,那么我们需要“匿名导入”(用下划线_来重命名导入的包)来避开报错。
import _ "image/png" // register PNG decoder
有一种特殊的导入方式写成“import . "XXX"”的形式,这种情况下,“XXX”包中公开的程序实体,被当前源码文件中的代码,视为当前代码包中的程序实体。如果XXX包中有函数Abc(),那么在当前源码文件中,就可以直接使用了,而不必通过XXX.Abc()的方式调用。
下面的示例代码,演示了如果如何“写”一个包,以及如何在你的程序里导入其他的“包”。假如创建的是一个“温度转换”功能的包,路径是 doc.zkbhj.com/example/tempconv
// 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文件中:
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) }
我们已经准备好了一个“包”,可以在需要用到它的地方使用了。我们注意到了包里的方法是“可导出”的,因为手写字母是大写的CToF和FToC。
// Cf converts its numeric argument to Celsius and Fahrenheit.
package main
import (
"fmt"
"os"
"strconv"
"doc.zkbhj.com/example/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))
}
}
我们通过“import doc.zkbhj.com/example/tempconv”这行语句,将写好的tempconv包导入,然后我们就可以通过“包名 . 函数名”来调用包里提供的可导出的方法了。
bingo,就是这么简单,你学会了吗?
三、源文件修改后需要重新编译所有依赖的包
当我们修改了一个源文件,我们必须重新编译该源文件对应的包和所有依赖该包的其他包。
Go语言编译器的编译速度也明显快于其它编译语言。得益于以下三点,总结来看就是得益于包的设计,避免了很多重复的间接依赖。
- 所有导入的包必须在每个文件的开头显式声明,这样的话编译器就没有必要读取和分析整个源文件来判断包的依赖关系。
- 禁止包的环状依赖,因为没有循环依赖,包的依赖关系形成一个有向无环图,每个包可以被独立编译,而且很可能是被并发编译。
- 编译后包的目标文件不仅仅记录包本身的导出信息,目标文件同时还记录了包的依赖关系。
四、关于包声明的几个特殊情况
通常来说,默认的包名就是包导入路径名的最后一段。但也有三种例外情况。
- 包对应一个可执行程序,也就是main包,这时候main包本身的导入路径是无关紧要的。
- 包所在的目录中可能有一些文件名是以_test.go为后缀的Go源文件,并且这些源文件声明的包名也是以_test为后缀名的(以_test为后缀包名的测试外部扩展包都由go test命令独立编译,普通包和测试的外部扩展包是相互独立的)。
- 一些依赖版本号的管理工具会在导入路径后追加版本号信息,例如“gopkg.in/yaml.v2”。这种情况下包的名字并不包含版本号后缀,而是yaml。
好了,关于命名空间和包的导入的相关问题就总结到这,都记住了吗?可以开始下一节了呦!