本节读书笔记对应原书第十章。
我们通过将一组相关的特性放到同一个包中,便于理解和后期更新,每个包更新的同时保持和程序中其他包的相对对立性,每个包都可以被其他的不同项目使用。
这么多的包,我们给他们定义不同的名字,用于区分和标识访问。
每个包还通过控制包内名字的可见性和是否导出来实现封装的特性,限制包内变量的可见性(有点类似于Java的private
),用户必须通过特定的函数访问和更新变量(类似于Java通过getter/setter
访问私有变量),保证内部变量的安全性。
包声明和导入声明
每个go语言源文件的开头都必须有包声明语句,包声明语句的主要目的就是确定当前包被其他包导入时默认的标识符(也称为包名)。每个包是由一个全局唯一的字符串所标识的导入路径定位。出现在import
语句中的导入路径是字符串。
我们来举一个小例子说明一下,什么是包声明语句,如何导入包。
package main
import(
"fmt"
"math/rand"
)
func main(){
fmt.Println(rand.Int())
}
math/rand
包下的每个源文件第一行都是package rand
包声明语句,当我们导入了这个包之后,就可以使用rand.Int
类似的方式访问包成员。我们通过import
可以导入我们需要使用的包。
默认包名是包导入路径名的最后一段,因此即使两个包的导入路径不同,它们的包名可能是相同的。那这样的情况怎么办呢?比如math/rand
和crypto/rand
包,那么导入声明必须至少为一个同名包指定一个新的包名以避免冲突,也就是导入包的重命名。
import(
"crypto/rand"
mrand"math/rand"
)
不要担心,导入包的重命名只会影响当前文件,其他的文件如果也导入了math/rand
包,可以用导入包原本默认的名字或者重命名为另一个完全不同的名字。
当然了,导入包的重命名不仅可以解决名字冲突,如果我们导入的包名很笨重,那么可以通过重命名起一个简短的名称。
包的匿名导入
如果只是导入一个包而并不使用导入的包,将会导致一个编译错误unused import
。首先,解决第一个可能存在的疑虑:为什么导入包却不使用,那导入有啥用?如果我们只想执行导入包的init
初始化函数或者计算包级变量的初始化表达式,那么我们就需要导入包了,但实际上可能并不会调用其中的方法/函数或使用变量。
哈哈,有点跑题了,第二个问题,那要如何解决编译报错的问题呢?只需要用下划线重命名导入的包即可,如下所示。
import _"image/png"
像这样的方式,我们就称为包的匿名导入。虽然这里重点并不是介绍png
的相关使用,但还是说一下,一些图片的编解码器会支持一定的格式,如果没有编解码器,那么就没有办法正确识别和解码某一格式的图像,而png
格式的图像通过注册驱动,让解码器检测识别到自己,最后玩转编解码工作。这个注册驱动就是在每个格式包的init
初始化函数中调用的。
再举一个使用包的匿名导入的例子,比如数据库包database/sql
,用户可能会使用不同的数据库驱动,那么就需要执行对应驱动的init
函数,一般init
函数会做一些注册工作。
import(
"database/sql"
_"github.com/lib/pq" //支持Postgres
—"github.com/go-sql-driver/mysql"//支持MySQL
)
db,err=sql.Open("postgres",dbname)
db,err=sql.Open("mysql",dbname)
db,err=sql.Open("sqlite3",dbname) //会返回错误,因为没有导入sqllite的驱动
题外话:为什么go语言的编译速度如此之快?
这主要得益于三个语言特性。
- 所有导入的包必须再每个文件的开头显式声明,这样的话编译器就没有必要读取和分析整个源文件来判断包的依赖关系;
- 禁止包的环状依赖,每个包可以被独立编译,而且很可能是被并发编译;
- 编译后包的目标文件不仅仅记录包本身的导出信息,目标文件同时还记录包的依赖关系
所以,编译器只需要读取每个直接导入包的目标文件,而不需要遍历所有依赖的文件。