概述
Go 语言的程序一般都会组织成若干组文件,每组文件称为一个包。这样每个包的代码都可以作为很小的复用单元,被其它项目引用。
Go 语言的源码复用建立在包(package)基础之上,包通过 package, import, GOPATH 操作完成,使其具有更好的可重用性与可读性,并且易于维护。
包声明
package 声明格式:
// 这行代码指定了某一源文件属于一个包,它应该放在每一个源文件的第一行。
package packagename
package 遵循以下原则:
package是最基本的分发单位和工程管理中依赖关系的体现。- 每个 Go 语言源代码文件开头都必须要有一个
package声明,表示源代码文件所属包。 - 要生成 Go 语言可执行程序,必须要有名为
main的package包,且在该包下必须有且只有一个main函数。 - 同一个路径下只能存在一个
package,一个package可以由多个源代码文件组成。
注意:
所有的 .go 文件,除了空行和注释,都应该在第一行声明自己所属的包。每个包都在一个单独的目录里。不能把多个包放到同一个目录中,也不能把同一个包的文件分拆到多个不同的目录中。这意味着,同一个目录下的所有 .go 文件必须声明同一个包名。
包名惯例
- 使用包所在目录的名字作为包名,这有利于在导入包的时候就能清晰地知道包名。
- 包名使用简洁、清晰且全小写的名字,这有利于开发时频繁输入包名。
注意:
并不需要所有包的名字都与别的包不同,因为导入包时是使用全路径的,所以可以区分同名的不同包。一般情况下,包被导入后会使用你的包名作为默认名字,不过这个导入后的名字可以修改。这个特性在需要导入不同目录的同名包时很有用。
main包
在 Go 语言里,命名为 main 的包具有特殊的含义。Go 语言的编译程序会试图把这种名字的包编译为二进制可执行文件。所有用 Go 语言编译的可执行程序都必须有一个名叫 main 的包。
编译器会把包名为 main 并且存在函数名为 main() 的 .go 文件创建为可执行文件,会使用该文件所在的目录名作为二进制可执行文件的文件名。
示例:
文件路径 $GOPATH/src/geometry/geometry.go
package main // package main 这一行指定该文件属于 main 包
import "fmt" // import "packagename" 语句用于导入一个已存在的包
func main() {
fmt.Println("Geometrical shape properties") // fmt 包,包内含有 Println 方法
}
在以上文件目录($GOPATH/src/geometry)里执行 go build,该命令会在 geometry 文件夹内搜索拥有 main 函数的文件,在这里,它找到了 geometry.go。接下来,它编译并生产一个二进制文件,在 UNIX、Linux 和 Mac OS X 系统上,这个文件命名为 geometry, 而在 Windows 系统上命名为 geometry.exe。执行这个程序会在控制台上显示 “Geometrical shape properties”。
如果以上文件把包名改为 main 之外的某个名字,如 geometry, 编译器就认为这只是一个包,而不是命令。
如下:
// 包含 main 函数的无效的 Go 程序
package geometry
import "fmt"
func main() {
fmt.Println("Geometrical shape properties")
}
自定义包
以下自定义一个计算矩形的面积和对角线的包,文件路径: $GOPATH/src/geometry/rectangle/rectprops.go
// rectprops.go
package rectangle
import "math"
func Area(len, wid float64) float64 {
area := len * wid
return area
}
func Diagonal(len, wid float64) float64 {
diagonal := math.Sqrt((len * len) + (wid * wid))
return diagonal
}
在上面的代码中,创建了两个函数用于计算 Area 和 Diagonal。矩形的面积是长和宽的乘积。矩形的对角线是长与宽平方和的平方根。
导入包
导入包需要使用关键字 import,它会告诉编译器引用该位置的包内的代码。如果需要导入多个包,习惯上是将 import 语句包装在一个导入块中。
导入包的语法格式:
import path
如下:
import (
"fmt"
"strings"
)
编译器会使用 Go 环境变量设置的路径,通过导入的相对路径来查找磁盘上的包。
- 标准库中的包会在安装 Go 的位置找到
- Go 开发者创建的包会在 GOPATH 环境变量指定的目录(即开发者的个人工作空间)里找到
例如,如果 Go 安装在 /usr/local/go, 并且环境变量 GOPATH 设置为 /home/myproject:/home/mylibraries,编译器就会按照下面的顺序查找 net/http 包:
/usr/local/go/src/pkg/net/http // 这就是标准库源代码所在的位置
/home/myproject/src/net/http
/home/mylibraries/src/net/http
一旦编译器找到一个满足 import 语句的包,就停止进一步查找。需要记住的是,编译器会首先查找 Go 的安装目录,然后才会按顺序查找 GOPATH 变量里列出的目录。
1. 远程导入
Go 语言的工具链支持通过 URL 从远程网络源码库获取包,并把包的源代码保存在 GOPATH 指向的路径与 URL 匹配的目录里。 例如:
import "github.com/spf13/viper"
以上 URL 指向 GitHub 上的代码库,使用 Go 工具链获取包并保存在与 URL 匹配的目录里。
2. 命名导入
命名导入是指在 import 语句给出的包路径的左侧定义一个名字,将导入的包命名为新名字。重名的包可以通过命名导入来导入。
例如:
// 重命名导入
package main
import (
"fmt"
myfmt "mylib/fmt"
)
func main() {
fmt.Println("Standard Library")
myfmt.Println("mylib/fmt")
}
注意:
-
如果导入了一个不存在代码里使用的包,Go 编译器会编译失败,并输出一个错误。这样可以有效防止导入未使用的包,避免代码变得臃肿。
-
如果需要导入一个包,但是不需要引用这个包的标识符,可以使用空白标识符_来重命名这个导入。
函数 init
每个包可以包含任意多个 init 函数,这些函数都会在程序执行开始的时候被调用。所有被编译器发现的 init 函数都会安排在 main 函数之前执行。init 函数用在设置包、初始化变量或者其它要在程序运行前优化完成的引导工作。
init 函数不应该有任何返回值类型和参数,在代码中也不能显式地调用它。
init 函数的用法示例:
package postgres
import (
"database/sql"
)
func init() {
sql.Register("postgres", new(PostgresDriver)) // 创建一个postgre 驱动的实例。这里为了展现 init 的作用,没有展现其它定义细节。
}
以上代码示例包含在 PostgreSQL 数据库的驱动里。如果程序导入了这个包,就会调用 init 函数,促使 PostgreSQL 的驱动最终注册到 Go 的 sql 包里,成为一个可用的驱动。
现在可以调用 sql.Open 方法来使用这个驱动了, 如下:
package main
import (
"database/sql"
_ "github.com/goinaction/code/chapter3/dbdriver/postgres" // 使用空白标识符导入包,避免编译错误。
)
func main() {
sql.Open("postgres", "mydb") // 调用 sql 包提供的 Open 方法。该方法能工作的关键在于 postgres 驱动通过自己的 init 函数将自身注册到了 sql 包。
}
包执行顺序
修改 rectprops.go、 geometry.go 文件如下所示:
// rectprops.go
package rectangle
import "math"
import "fmt"
/*
* init function added
*/
func init() {
fmt.Println("rectangle package initialized")
}
func Area(len, wid float64) float64 {
area := len * wid
return area
}
func Diagonal(len, wid float64) float64 {
diagonal := math.Sqrt((len * len) + (wid * wid))
return diagonal
}
// geometry.go
package main
import (
"fmt"
"geometry/rectangle" // 导入自定义包
"log"
)
/*
* 1. 包级别变量
*/
var rectLen, rectWidth float64 = 6, 7
/*
*2. init 函数会检查长和宽是否大于0
*/
func init() {
println("main package initialized")
if rectLen < 0 {
log.Fatal("length is less than zero")
}
if rectWidth < 0 {
log.Fatal("width is less than zero")
}
}
func main() {
fmt.Println("Geometrical shape properties")
fmt.Printf("area of rectangle %.2f\n", rectangle.Area(rectLen, rectWidth))
fmt.Printf("diagonal of the rectangle %.2f ",rectangle.Diagonal(rectLen, rectWidth))
}
以上程序执行后输出:
rectangle package initialized
main package initialized
Geometrical shape properties
area of rectangle 42.00
diagonal of the rectangle 9.22
main 包的初始化顺序如下:
- 首先初始化被导入的包
- 接着初始化包级别(Package Level)的变量
- 紧接着调用 init 函数。包可以有多个 init 函数(在一个文件或分布于多个文件中),它们按照编译器解析它们的顺序进行调用
- 最后调用 main 函数