Go基础11-理解Go语言的包导入

17 篇文章 0 订阅

Go语言是使用包(package)作为基本单元来组织源码的,可以说一个Go程序就是由一些包链接在一起构建而成的。虽然与Java、Python等语言相比这算不上什么创新,但与祖辈C语言的头文件包含机制相比则是“先进”了许多。

编译速度快是这种“先进性”的一个突出表现,即便每次编译都是从零开始。Go语言的这种以包为基本构建单元的构建模型使依赖分析变得十分简单,避免了C语言那种通过头文件分析依赖的巨大开销。

Go编译速度快的原因具体体现在以下三方面。

● Go要求每个源文件在开头处显式地列出所有依赖的包导入,这样Go编译器不必读取和处理整个文件就可以确定其依赖的包列表。

● Go要求包之间不能存在循环依赖,这样一个包的依赖关系便形成了一张有向无环图。由于无环,包可以被单独编译,也可以并行编译。

● 已编译的Go包对应的目标文件(file_name.o或package_name.a)中不仅记录了该包本身的导出符号信息,还记录了其所依赖包的导出符号信息。

这样,Go编译器在编译某包P时,针对P依赖的每个包导入(比如导入包Q),只需读取一个目标文件即可(比如:Q包编译成的目标文件中已经包含Q包的依赖包的导出信息),而无须再读取其他文件中的信息。
Go语言中包的定义和使用十分简单。通过package关键字声明Go源文件所属的包:

// xx.go
package a
...
上述源码表示文件xx.go是包a的一部分。
使用import关键字导入依赖的标准库包或第三方包:
package main
	import (
	"fmt" // 标准库包导入
	"a/b/c" // 第三方包导入
)
func main() {
c.Func1()
fmt.Println("Hello, Go!")
}

很多Gopher看到上面的代码会想当然地将import后面的“c”“fmt”与c.Func1()和fmt.Println()中的c和fmt认作同一个语法元素:包名。但在深入学习Go语言后,大家会发现事实并非如此。

比如在使用实时分布式消息框架nsq提供的官方client包时,我们的包导入是写成这样的:

import "github.com/nsqio/go-nsq"

但在使用该包提供的导出函数时,我们使用的不是go-nsq.xx而是

nsq.xxx:
q, _ := nsq.NewConsumer("write_test", "ch", config)

很多Gopher在学习Go包导入时或多或少有些疑惑:import后面路径中的最后一个分段到底代表的是什么?是包名还是一个路径?

在本条中我就和大家一起来深入探究和理解一下Go语言的包导入。

Go程序构建过程

我们先来简单了解一下Go程序的构建过程,作为后续理解Go包导入的前导知识。和主流静态编译型语言一样,Go程序的构建简单来讲也是由**编译(compile)和链接(link)**两个阶段组成的。

一个非main包在编译后会对应生成一个.a文件,该文件可以理解为Go包的目标文件,
该目标文件实际上是通过pack工具( G O R O O T / p k g / t o o l / d a r w i n a m d 64 / p a c k )对 . o 文件打包后形成的。默认情况下,在编译过程中 . a 文件生成在临时目录下,除非使用 g o i n s t a l l 安装到 GOROOT/pkg/tool/darwin_amd64/pack)对.o文件打 包后形成的。默认情况下,在编译过程中.a文件生成在临时目录下,除非使用go install 安装到 GOROOT/pkg/tool/darwinamd64/pack)对.o文件打包后形成的。默认情况下,在编译过程中.a文件生成在临时目录下,除非使用goinstall安装到GOPATH/pkg下(Go 1.11版本之前),否则你看不到.a文件。如果是构建可执行程序,那么.a文件会在构建可执行程序的链接阶段起使用。

标准库包的源码文件在 G O R O O T / s r c 下面,而对应的 . a 文件存放在 GOROOT/src下面,而对应的.a文件存放在 GOROOT/src下面,而对应的.a文件存放在GOROOT/pkg/
darwin_amd64下(以macOS为例;如果是Linux系统,则是linux_amd64):

// Go 1.16
$tree -FL 1 $GOROOT/pkg/darwin_amd64
├── archive/
├── bufio.a
├── bytes.a
├── compress/
├── container/
├── context.a
├── crypto/
├── crypto.a
...

“求甚解”的读者可能会提出这样一个问题:构建Go程序时,编译器会重新编译依赖包的源文件还是直接链接包的.a文件呢?下面通过一个实验来给大家答案。

Go 1.10版本引 入了build cache,为了避免build cache给实验过程和分析带来的复杂性,我们使用Go1.9.7版本来进行这个实验。
我们建立实验环境的目录结构如下:

chapter3-demo1
├── cmd/
│ └── app1/
│ └── main.go
└── pkg/
└── pkg1/
└── pkg1.go

由于仅是为了演示,pkg1.go和main.go的源码都很简单:

// cmd/app1/main.go
package main
import (
"github.com/bigwhite/effective-go-book/chapter3-demo1/pkg/pkg1"
)
func main() {
pkg1.Func1()
}
// pkg/pkg1/pkg1.go
package pkg1
import "fmt"
func Func1() {
fmt.Println("pkg1.Func1 invoked")
}

进入chapter3-demo1,执行下面的命令:

$go install github.com/bigwhite/effective-go-book/chapter3-demo1/pkg/pkg1

之后,我们就可以在

$GOPATH/pkg/darwin_amd64/github.com/bigwhite/effective-
go-book/chapter3-demo1/pkg下看到pkg1包对应的目标文件pkg1.a:
$ls $GOPATH/pkg/darwin_amd64/github.com/bigwhite/effective-go-book/chapter3-demo1/pkg
pkg1.a

我们继续在chapter3-demo1路径下编译可执行程序app1:

$go build github.com/bigwhite/effective-go-book/chapter3-demo1/cmd/app1

执行完上述命令后,我们会在chapter3-demo1下看到一个可执行文件app1,执行该文件:

$ls
app1* cmd/ pkg/
$./app1
pkg1.Func1 invoked

这符合我们的预期,但现在我们仍无法知道编译app1使用的到底是pkg1包的源码还是目标文件pkg1.a,因为目前它们的输出都是一致的。

修改一下pkg1.go的代码:

// pkg/pkg1/pkg1.go
package pkg1
import "fmt"
func Func1() {
fmt.Println("pkg1.Func1 invoked - Again")
}

重新编译执行app1,得到如下结果:

$go build github.com/bigwhite/effective-go-book/chapter3-demo1/cmd/app1
$./app1
pkg1.Func1 invoked - Again

究竟是路径名还是包名

通过前面的实验,我们了解到编译器在编译过程中必然要使用的是编译单元(一个包)所依赖的包的源码。

而编译器要找到依赖包的源码文件,就需要知道依赖包的源码路
径。这个路径由两部分组成:基础搜索路径和包导入路径

基础搜索路径是一个全局的设置,下面是其规则描述。
1)所有包(无论是标准库包还是第三方包)的源码基础搜索路径都包括 G O R O O T / s r c 。 2 )在上述基础搜索路径的基础上,不同版本的 G o 包含的其他基础搜索路径有不同。● G o 1.11 版本之前,包的源码基础搜索路径还包括 GOROOT/ src。 2)在上述基础搜索路径的基础上,不同版本的Go包含的其他基础搜索路径有不同。 ● Go 1.11版本之前,包的源码基础搜索路径还包括 GOROOT/src2)在上述基础搜索路径的基础上,不同版本的Go包含的其他基础搜索路径有不同。Go1.11版本之前,包的源码基础搜索路径还包括GOPATH/src。
● Go 1.11~Go 1.12版本,包的源码基础搜索路径有三种模式:
● 经典gopath模式下(GO111MODULE=off): G O P A T H / s r c 。● m o d u l e − a w a r e 模式下( G O 111 M O D U L E = o n ): GOPATH/src。 ● module-aware模式下(GO111MODULE=on): GOPATH/srcmoduleaware模式下(GO111MODULE=on):GOPATH/pkg/mod。
● auto模式下(GO111MODULE=auto):在 G O P A T H / s r c 路径下,与 g o p a t h 模式相同;在 GOPATH/src路径下,与gopath模式 相同;在 GOPATH/src路径下,与gopath模式相同;在GOPATH/src路径外且包含go.mod,与module-aware模式相同。
● Go 1.13版本,包的源码基础搜索路径有两种模式:
● 经典gopath模式下(GO111MODULE=off): G O P A T H / s r c 。● m o d u l e − a w a r e 模式下( G O 111 M O D U L E = o n / a u t o ): GOPATH/src。 ● module-aware模式下(GO111MODULE=on/auto): GOPATH/srcmoduleaware模式下(GO111MODULE=on/auto):GOPATH/pkg/mod。
● 未来的Go版本将只有module-aware模式,即只在module缓存的目录下搜索包的源码。
而搜索路径的第二部分就是位于每个包源码文件头部的包导入路径。基础搜索路径与
包导入路径结合在一起,Go编译器便可确定一个包的所有依赖包的源码路径的集合,这个
集合构成了Go编译器的源码搜索路径空间。看下面这个例子:

// p1.go
package p1
import (
"fmt"
"time"
"github.com/bigwhite/effective-go-book"
"golang.org/x/text"
"a/b/c"
"./e/f/g"
)
...

包名冲突问题

同一个包名在不同的项目、不同的仓库下可能都会存在。同一个源码文件在其包导入
路径构成源码搜索路径空间下很可能存在同名包。比如:我们有另一个chapter3-demo2,
其下也有名为pkg1的包,导入路径为github.com/bigwhite/effective-go-book/chapter3-
demo2/pkg/pkg1。如果cmd/app3同时导入了chapter3-demo1和chapter3-demo2的pkg1包,
会发生什么呢?

// cmd/app3
package main
import (
"github.com/bigwhite/effective-go-book/chapter3-demo1/pkg/pkg1"
"github.com/bigwhite/effective-go-book/chapter3-demo2/pkg/pkg1"
)
func main() {
pkg1.Func1()
}

编译一下cmd/app3:

$go build github.com/bigwhite/effective-go-book/chapter3-demo1/cmd/app3
# github.com/bigwhite/effective-go-book/chapter3-demo1/cmd/app3
./main.go:5:2: pkg1 redeclared as imported package name
previous declaration at ./main.go:4:2

我们看到的确出现了包名冲突的问题。怎么解决这个问题呢?还是用为包导入路径下
的包显式指定包名的方法:

package main
import (
pkg1 "github.com/bigwhite/effective-go-book/chapter3-demo1/pkg/pkg1"
mypkg1 "github.com/bigwhite/effective-go-book/chapter3-demo2/pkg/pkg1"
)
func main() {
pkg1.Func1()
mypkg1.Func1()
}

上面的pkg1指代的就是chapter3-demo1/pkg/pkg1下面的包,mypkg1则指代的是chapter3-demo1/pkg/pkg1下面的包。就此,包名冲突问题就轻松解决掉了。

我们通过实验进一步理解了Go语言的包导入,Gopher应牢记以下几个结论:

● Go编译器在编译过程中必然要使用的是编译单元(一个包)所依赖的包的源码;

● Go源码文件头部的包导入语句中import后面的部分是一个路径,路径的最后一个分段是目录名,而不是包名;

● Go编译器的包源码搜索路径由基本搜索路径和包导入路径组成,两者结合在一起后,编译器便可确定一个包的所有依赖包的源码路径的集合,这个集合构成了Go编译器的源码搜索路径空间;

● 同一源码文件的依赖包在同一源码搜索路径空间下的包名冲突问题可以由显式指定包名的方式解决。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小海聊智造

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值