理解go语言包导入路径的含义

九路 等级 537 0 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 语言的包导入。

1. Go 程序构建过程

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

一个非 main 包在编译后会对应生成一个.a 文件,该文件可以理解为是 Go 包的目标文件(实则是通过 pack 工具($GOROOT/pkg/tool/darwin_amd64/pack)对 .o 文件打包后形成的 .a)。默认情况下在编译过程中 .a 文件生成在临时目录下,除非使用 go install 安装到 $GOPATH/pkg 下(Go 1.11 版本之前),否则你看不到 .a 文件。如果是构建可执行程序,那么 .a 文件会在构建可执行程序的链接阶段起使用。

标准库包的源码文件在$GOROOT/src 下面,而对应的 .a 文件存放在$GOROOT/pkg/darwin_amd64 下(以 MacOS 上为例;如果是 linux,则是 linux_amd64):

// Go 1.13
$tree -FL 1 $GOROOT/pkg/darwin_amd64 
├── archive/
├── bufio.a
├── bytes.a
├── compress/
├── container/
├── context.a
├── crypto/
├── crypto.a
├── database/
├── debug/
├── encoding/
├── encoding.a
├── errors.a
├── expvar.a
├── flag.a
├── fmt.a
├── go/
├── hash/
├── hash.a
... ...

“求甚解”的读者可能会提出这样的一个问题:构建 Go 程序时,编译器会重新编译依赖包的源文件还是直接链接包的.a 文件呢?我们通过一个实验来给大家答案。Go 1.10 版本引入了 build cache,为了避免 build cache 给实验过程和分析带来的复杂性,我们使用 Go 1.9.7 版本(Go 1.10 之前的版本均可)来进行这个实验。

我们建立如下实验环境的目录结构:

$GOPATH/src/github.com/bigwhite/effective-go-book $tree -F chapter3-demo1
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")
}

执行下面命令:

$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,执行该文件:

$GOPATH/src/github.com/bigwhite/effective-go-book/chapter3-demo1 $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

这样的实验结果告诉我们:在使用第三方包的时候,当第三方包源码存在且对应的 .a 已安装的情况下,编译器链接的仍是根据第三方包最新源码编译出的 .a 文件,而不是之前已经安装到$GOPATH/pkg/darwin_amd64 下面的目标文件。

那么是否可以只链接依赖包的已安装的 .a 文件,而不用第三方包源码呢?我们临时删除掉 pkg1 目录,但保留之前已经 install 到 $GOPATH/pkg/darwin_amd64 下面的 pkg1.a 文件。我们再来编译一下 app1:

$go build github.com/bigwhite/effective-go-book/chapter3-demo1/cmd/app1
cmd/app1/main.go:4:2: cannot find package "github.com/bigwhite/effective-go-book/chapter3-demo1/pkg/pkg1" in any of:
    /Users/tonybai/.bin/go1.9.7/src/github.com/bigwhite/effective-go-book/chapter3-demo1/pkg/pkg1 (from $GOROOT)
    /Users/tonybai/Go/src/github.com/bigwhite/effective-go-book/chapter3-demo1/pkg/pkg1 (from $GOPATH)

我们看到 Go 编译器报错! Go 编译器还是尝试去找 pkg1 包的源码,而不是直接链接已经安装了的 pkg1.a。

下面我们让 Go 编译器输出详细信息,我们来看看为什么 Go 编译器会选择链接根据第三方包最新源码编译出的.a 文件,而不是之前已经安装到 $GOPATH/pkg/darwin_amd64 下面的目标文件。我们在编译 app1 时给 go build 命令传入 -x -v 命令行选项:

$go build -x -v github.com/bigwhite/effective-go-book/chapter3-demo1/cmd/app1
WORK=/var/folders/cz/sbj5kg2d3m3c6j650z0qfm800000gn/T/go-build878870664
github.com/bigwhite/effective-go-book/chapter3-demo1/pkg/pkg1
mkdir -p $WORK/github.com/bigwhite/effective-go-book/chapter3-demo1/pkg/pkg1/_obj/
mkdir -p $WORK/github.com/bigwhite/effective-go-book/chapter3-demo1/pkg/
cd /Users/tonybai/Go/src/github.com/bigwhite/effective-go-book/chapter3-demo1/pkg/pkg1
/Users/tonybai/.bin/go1.9.7/pkg/tool/darwin_amd64/compile -o $WORK/github.com/bigwhite/effective-go-book/chapter3-demo1/pkg/pkg1.a -trimpath $WORK -goversion go1.9.7 -p github.com/bigwhite/effective-go-book/chapter3-demo1/pkg/pkg1 -complete -buildid 5508f4ff15d0000af68a19c84d5200be6b52aac0 -D _/Users/tonybai/Go/src/github.com/bigwhite/effective-go-book/chapter3-demo1/pkg/pkg1 -I $WORK -pack ./pkg1.go
github.com/bigwhite/effective-go-book/chapter3-demo1/cmd/app1
mkdir -p $WORK/github.com/bigwhite/effective-go-book/chapter3-demo1/cmd/app1/_obj/
mkdir -p $WORK/github.com/bigwhite/effective-go-book/chapter3-demo1/cmd/app1/_obj/exe/
cd /Users/tonybai/Go/src/github.com/bigwhite/effective-go-book/chapter3-demo1/cmd/app1
/Users/tonybai/.bin/go1.9.7/pkg/tool/darwin_amd64/compile -o $WORK/github.com/bigwhite/effective-go-book/chapter3-demo1/cmd/app1.a -trimpath $WORK -goversion go1.9.7 -p main -complete -buildid d116bd4b4731d2f7eac18df2368f87eee7bc7977 -D _/Users/tonybai/Go/src/github.com/bigwhite/effective-go-book/chapter3-demo1/cmd/app1 -I $WORK -I /Users/tonybai/Go/pkg/darwin_amd64 -pack ./main.go
cd .
/Users/tonybai/.bin/go1.9.7/pkg/tool/darwin_amd64/link -o $WORK/github.com/bigwhite/effective-go-book/chapter3-demo1/cmd/app1/_obj/exe/a.out -L $WORK -L /Users/tonybai/Go/pkg/darwin_amd64 -extld=clang -buildmode=exe -buildid=d116bd4b4731d2f7eac18df2368f87eee7bc7977 $WORK/github.com/bigwhite/effective-go-book/chapter3-demo1/cmd/app1.a
mv $WORK/github.com/bigwhite/effective-go-book/chapter3-demo1/cmd/app1/_obj/exe/a.out app1

我们看到 app1 的构建过程大致分为几步:

  • 建立临时工作路径,命名为 WORK,以后的编译、链接均以$WORK 为当前工作目录;
  • 编译 app1 的依赖包 pkg1,将目标文件打包后放入 $WORK/github.com/bigwhite/effective-go-book/chapter3-demo1/pkg/pkg1.a;
  • 编译 app1 的 main 包,将目标文件打包后放入 $WORK/github.com/bigwhite/effective-go-book/chapter3-demo1/cmd/app1.a ;
  • 链接器将 app1.a、pkg1.a 链接成 $WORK/github.com/bigwhite/effective-go-book/chapter3-demo1/cmd/app1/_obj/exe/a.out ;
  • 最后,将 a.out 改名为 app1。这个 app1 是在执行 go build 命令的目录中。

我们细致看看链接器进行目标文件链接所执行的命令:

/Users/jiulu/.bin/go1.9.7/pkg/tool/darwin_amd64/link -o $WORK/github.com/bigwhite/effective-go-book/chapter3-demo1/cmd/app1/_obj/exe/a.out -L $WORK -L /Users/tonybai/Go/pkg/darwin_amd64 -extld=clang -buildmode=exe -buildid=d116bd4b4731d2f7eac18df2368f87eee7bc7977 $WORK/github.com/bigwhite/effective-go-book/chapter3-demo1/cmd/app1.a

我们在链接器的执行语句中并未显式看到 app1 链接的是 $WORK/github.com/bigwhite/effective-go-book/chapter3-demo1/pkg 下 pkg1.a。但是从传给链接器的-L 参数来看:$WORK 这个路径排在了$GOPATH/pkg/darwin_amd64 的前面。这样链接器会优先使用 $WORK 下面的 github.com/bigwhite/effective-go-book/chapter3-demo1/pkg/pkg1.a,而不是 $GOPATH/pkg/darwin_amd64/github.com/bigwhite/effective-go-book/chapter3-demo1/pkg/pkg1.a。

为了验证我们的推论,我们可按照编译器输出,按顺序手工执行了一遍上述命令序列,但在最后执行链接命令时,去掉 -L $WORK 或将 -L $WORK 放到 -L $GOPATH/pkg/darwin_amd64 的后面。考虑到篇幅原因,下面省略了中间的执行过程:

$export WORK=/Users/tonybai/temp
... ...
$/Users/tonybai/.bin/go1.9.7/pkg/tool/darwin_amd64/link -o $WORK/github.com/bigwhite/effective-go-book/chapter3-demo1/cmd/app1/_obj/exe/a.out  -L /Users/tonybai/Go/pkg/darwin_amd64 -extld=clang -buildmode=exe -buildid=d116bd4b4731d2f7eac18df2368f87eee7bc7977 $WORK/github.com/bigwhite/effective-go-book/chapter3-demo1/cmd/app1.a 
... ...

再执行这次手动执行命令序列的成果物:

$./app1
pkg1.Func1 invoked

我们看到这回链接器链接的是 /Users/tonybai/Go/pkg/darwin_amd64 下面的 pkg1.a,输出的是 pkg1 包修改之前的打印信息。到这里我们明白了所谓的使用第三方包源码,实际上是链接了以该最新包源码编译的存放在临时目录下的包的.a 文件而已。

到这里肯定又会有读者会问题:Go 标准库中的包也是这样么?编译时 Go 编译器到底使用的是根据$GOROOT/src 下源码编译出的.a 目标文件,还是 $GOROOT/pkg 下已经随 Go 安装包编译好的.a 文件呢?我们再来做个实验!我们编写一个简单的 Go 文件:

// main.go
package main

import "fmt"

func main() {
        b := true
        s := fmt.Sprintf("%t", b)
        fmt.Println(s)
}

有了前面的经验,我们这次直接将 $GOROOT/src 下面的 fmt 目录改名为 fmtbak,然后编译上面的 main.go 文件:

$go build -x -v main.go
WORK=/var/folders/cz/sbj5kg2d3m3c6j650z0qfm800000gn/T/go-build682636466
main.go:3:8: cannot find package "fmt" in any of:
    /Users/tonybai/.bin/go1.9.7/src/fmt (from $GOROOT)
    /Users/tonybai/Go/src/fmt (from $GOPATH)

我们看到 Go 编译器找不到标准库的 fmt 包了,显然和依赖第三方包一样,依赖标准库包在编译时也是需要所依赖的标准库包的源码的。那如果我们修改了标准库的源码,是否会向上面实验中那样,源码更新直接会体现在最终的可执行文件的输出中呢?这里我们将 fmt 包的部分源码做一下修改(修改前,先把 fmtbak 改回 fmt)。我们对 fmt 目录下面的 format.go 文件做些微调:


// $GOROOT/src/fmt/format.go

// fmt_boolean formats a boolean.
func (f *fmt) fmt_boolean(v bool) {
        if v {
                f.padString("true1") // 我们修改这一行,原代码为:f.padString("true")
        } else {
                f.padString("false")
        }
}

我们构建一下main.go:

 $go build -x -v main.go
WORK=/var/folders/cz/sbj5kg2d3m3c6j650z0qfm800000gn/T/go-build972107899
command-line-arguments
mkdir -p $WORK/command-line-arguments/_obj/
mkdir -p $WORK/command-line-arguments/_obj/exe/
cd /Users/tonybai/temp
/Users/tonybai/.bin/go1.9.7/pkg/tool/darwin_amd64/compile -o $WORK/command-line-arguments.a -trimpath $WORK -goversion go1.9.7 -p main -complete -buildid 374bfe52ceb5beb2748735a34836d6348e6c1e21 -D _/Users/tonybai/temp -I $WORK -pack ./main.go
cd .
/Users/tonybai/.bin/go1.9.7/pkg/tool/darwin_amd64/link -o $WORK/command-line-arguments/_obj/exe/a.out -L $WORK -extld=clang -buildmode=exe -buildid=374bfe52ceb5beb2748735a34836d6348e6c1e21 $WORK/command-line-arguments.a
mv $WORK/command-line-arguments/_obj/exe/a.out main

从输出的详细构建日志来看,编译器并没有去重新编译 format.go,因此编译出来的可执行文件的输出为:

$./main
true

而不是我们期望的"true1"。这说明默认情况下对于标准库中的包,编译器直接链接的是$GOROOT/pkg/darwin_amd64 下的.a 文件。

那么如何让编译器能去”感知“到标准库中的最新更新呢?以 fmt.a 为例,有两种方法:

  • 方法 1:删除$GOROOT/pkg/darwin_amd64 下面的 fmt.a,然后重新执行 go install fmt :
$go install -x -v fmt
WORK=/var/folders/cz/sbj5kg2d3m3c6j650z0qfm800000gn/T/go-build519257256
fmt
mkdir -p $WORK/fmt/_obj/
mkdir -p $WORK/
cd /Users/tonybai/.bin/go1.9.7/src/fmt
/Users/tonybai/.bin/go1.9.7/pkg/tool/darwin_amd64/compile -o $WORK/fmt.a -trimpath $WORK -goversion go1.9.7 -p fmt -std -complete -buildid 784aa2fc38b910a35f11bcd11372fa344f46ebed -D _/Users/tonybai/.bin/go1.9.7/src/fmt -I $WORK -pack ./doc.go ./format.go ./print.go ./scan.go
mkdir -p /Users/tonybai/.bin/go1.9.7/pkg/darwin_amd64/
mv $WORK/fmt.a /Users/tonybai/.bin/go1.9.7/pkg/darwin_amd64/fmt.a
  • 方法 2:使用 go build 的-a 命令行选项: go build -a 可以让编译器将 Go 源文件(比如例子中的 main.go)的所有直接和间接的依赖包(包括标准库)都重新编译一遍,并将最新的 .a 作为链接器的输入。因此,采用 -a 选项进行构建的时间较长,这里仅摘录与 fmt 包相关的日志供参考:
$go build -x -v -a main.go
WORK=/var/folders/cz/sbj5kg2d3m3c6j650z0qfm800000gn/T/go-build094236425
... ...
mkdir -p $WORK/fmt/_obj/
cd /Users/tonybai/.bin/go1.9.7/src/fmt
/Users/tonybai/.bin/go1.9.7/pkg/tool/darwin_amd64/compile -o $WORK/fmt.a -trimpath $WORK -goversion go1.9.7 -p fmt -std -complete -buildid 784aa2fc38b910a35f11bcd11372fa344f46ebed -D _/Users/tonybai/.bin/go1.9.7/src/fmt -I $WORK -pack ./doc.go ./format.go ./print.go ./scan.go
... ...
/Users/tonybai/.bin/go1.9.7/pkg/tool/darwin_amd64/link -o $WORK/command-line-arguments/_obj/exe/a.out -L $WORK -extld=clang -buildmode=exe -buildid=374bfe52ceb5beb2748735a34836d6348e6c1e21 $WORK/command-line-arguments.a
mv $WORK/command-line-arguments/_obj/exe/a.out main

2. 究竟是路径名还是包名

通过前面的实验,我们了解到编译器在编译过程中必然要使用的是编译单元(一个包)所依赖的包的源码。而编译器要找到依赖包的源码文件就需要知道依赖包的源码路径。这个路径由两部分组成:基础搜索路径和包导入路径。

基础搜索路径是一个全局的设置,下面是关于基础搜索路径的规则描述:

所有包(无论标准库包还是第三方包)的源码基础搜索路径都包括 $GOROOT/src;

在上述基础搜索路径的基础上,不同版本的 Go 包含的其他基础搜索路径有不同:

  • Go 1.11版本之前,包的源码基础搜索路径还包括 $GOPATH/src;
  • Go 1.11-Go 1.12 版本,包的源码基础搜索路径有三种模式:
    • 经典 gopath 模式下(GO111MODULE=off):$GOPATH/src ;
    • module-aware 模式下(GO111MODULE=on):$GOPATH/pkg/mod ;
    • auto 模式下(GO111MODULE=auto):在$GOPATH/src 路径下,与 gopath 模式相同;在$GOPATH/src 路径外且包含 go.mod,与 module-aware 模式相同。
  • Go 1.13 版本,包的源码基础搜索路径有两种模式:
  • 经典 gopath 模式下(GO111MODULE=off):$GOPATH/src;
  • module-aware 模式下(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"
)
... ...

我们以 Go 1.11 版本之前的 GOPATH 模式为例,编译器在编译上述 p1 包时,会构建自己的源码搜索路径空间,该空间对应的搜索路径集合包括:

- $GOROOT/src/fmt/
- $GOROOT/src/time/
- $GOROOT/src/github.com/bigwhite/effective-go-book/
- $GOROOT/src/golang.org/x/text/
- $GOROOT/src/a/b/c/
- $GOPATH/src/github.com/bigwhite/effective-go-book/
- $GOPATH/src/golang.org/x/text/
- $GOPATH/src/a/b/c/
- $CWD/e/f/g

最后一个包导入路径"./e/f/g"是一个本地相对路径,它的基础搜索路径是 $CWD,即执行编译命令时的当前工作目录。Go compiler 在编译源码时会使用-D 选项设置当前工作目录,该工作目录与"./e/f/g"的本地相对路径结合,便构成了一个源码搜索路径。

到这里,我们已经给出了前面问题的答案:源文件头部的包导入语句 import 后面的部分就是一个路径,路径的最后一个分段也不是包名。我们再通过一个例子来证明这点。我们在$GOPATH/src/github.com/bigwhite/effective-go-book/chapter3-demo1 下面再建立 cmd/app2 和 pkg/pkg2 两个目录:

$tree -LF 3 chapter3-demo1
chapter3-demo1
├── cmd/
│   └── app2/
│       └── main.go
└── pkg/
    └── pkg2/
        └── pkg2.go

app2/main.go 和 pkg2/pkg2.go 的源码如下:

// pkg2/pkg2.go

package mypkg2

import "fmt"

func Func1() {
        fmt.Println("mypkg2.Func1 invoked")
}

// app2/main.go

package main

import (
        "github.com/bigwhite/effective-go-book/chapter3-demo1/pkg/pkg2"
)

func main() {
        mypkg2.Func1()
}

编译运行 app2:

$go build github.com/bigwhite/effective-go-book/chapter3-demo1/cmd/app2
$./app2
mypkg2.Func1 invoked

我们看到这个例子与 app1 的不同之处在于 app2 的包导入语句中的路径末尾是 pkg2,而在 main 函数中使用的包名却是 mypkg2,这再次印证了包导入语句中的只是一个路径。

不过 Go 语言有一个惯用法,那就是包导入路径的最后一段目录名最好与包名一致,就像 pkg1 那样:

// app1/main.go
package main

import (
        "github.com/bigwhite/effective-go-book/chapter3-demo1/pkg/pkg1"
)

func main() {
        pkg1.Func1()
}

pkg1 包导入路径的最后一段目录名为 pkg1,而包名也是 pkg1。也就是说上面代码中出现的两个 pkg1 虽然书写上时一模一样的,但代表的含义是完全不同的:包导入路径上的 pkg1 表示的是一个目录名;而 main 函数体中的 pkg1 则是包名。

关于包导入,Go 语言还有另外一个惯用法:当包名与包导入路径中的最后一个目录名不同时,最好用下面语法将包名显式放入包导入语句。以上面 app2 为例:

// app2/main.go

package main

import (
        mypkg2 "github.com/bigwhite/effective-go-book/chapter3-demo1/pkg/pkg2"
)

func main() {
        mypkg2.Func1()
}

显然,这种惯用法让代码可读性更好。

3. 同一源码文件的依赖包在同一源码搜索路径空间下包名冲突的问题

同一个包名在不同的项目、不同的仓库下可能都会存在。同一个源码文件在其包导入路径构成源码搜索路径空间下很可能存在同名包。比如:我们有另外一个 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 下面的包。就此,同名包冲突问题就轻松解决掉了。

4. 小结

在本节中,我们通过实验对 Go 语言的包导入做了进一步的理解,Gopher 们应牢记以下几点结论:

  • Go 编译器在编译过程中必然要使用的是编译单元(一个包)所依赖的包的源码;
  • Go 源码文件头部的包导入语句中 import 后面的部分是一个路径,路径的最后一个分段是目录名,而不是包名;
  • Go 编译器的包源码搜索路径由基本搜索路径和包导入路径组成。两者结合在一起后,编译器便可确定一个包的所有依赖包的源码路径的集合,这个集合构成了 Go 编译器的源码搜索路径空间;
  • 同一源码文件的依赖包在同一源码搜索路径空间下包名冲突的问题可以由包别名的方式解决。
收藏
评论区

相关推荐

godoc 命令和 golang 代码文档管理
介绍 godoc 是 golang 自带的文档查看器,更多的提供部署服务 go doc 和 godoc 在 golang 1.13 被移除了,可以自行安装 golang.org go1.13 godoc(https://links.jianshu.com/go?tohttps%3A%2F%2Fgolang.org%2Fdoc%2Fg
为什么GOPROXY对Golang开发如此重要
为什么GOPROXY对Golang开发如此重要 引言 从Go 1.13开始,Go Module作为Golang中的标准包管理器,在安装时自动启用,并附带一个默认的GOPROXY。 但是对于其他的GOPROXY选项,比如JFrog GoCenter,以及你自己的Go Module包,你需要在公众视野中保持安全,你应该选择什么样的配置? 你怎样才能
【Golang】Goland使用介绍
goland介绍 Goland官方地址:http://www.jetbrains.com/go/(http://www.jetbrains.com/go/) goland安装 下载 Windows下载地址:https://download.jetbrains.com/go/goland2018.2.1.exe(https://download
Go语言开发的利与弊
Go 语言有多火爆?国外如 Google、AWS、Cloudflare、CoreOS 等,国内如七牛、阿里等都已经开始大规模使用 Go 语言开发其云计算相关产品。在 Go 语言的使用过程中,需要注意哪些 Yes 和 But? 最近,我们使用 Go 语言编写了一个 API,Go 语言是一种开源编程语言,2009 年由 Google 推出。在使用 Go 进行开
go 语言资源整理
Awesome GitHub Topic for Go(https://links.jianshu.com/go?tohttps%3A%2F%2Fgithub.com%2Ftopics%2Fgolang) Awesome Go(https://links.jianshu.com/go?tohttps%3A%2F%2F
知乎从Python转为Go,是不是代表Go比Python好?
众所周知,知乎早在几年前就将推荐系统从 Python 转为了 Go。于是乎,一部分人就说 Go 比 Python 好,Go 和 Python 两大社区的相关开发人员为此也争论过不少,似乎,谁也没完全说服谁。 知乎从Python转为Go,是不是代表Go比Python好?我认为,各有优点,谁也取代不了谁,会长期共存! “由 Python 语言转向 Go 语言
部署Go语言项目的 N 种方法
本文以部署 Go Web 程序为例,介绍了在 CentOS7 服务器上部署 Go 语言程序的若干方法。独立部署Go 语言支持跨平台交叉编译,也就是说我们可以在 Windows 或 Mac 平台下编写代码,并且将代码编译成能够在 Linux amd64 服务器上运行的程序。对于简单的项目,通常我们只需要将编译后的二进制文件拷贝到服务器上,然后设置为后台
go的三个运行基本命令的区别,go run ,go build 和 go install
最近在自学go,遇到点基础的问题,通过自己实际操作之后得出结论在实际操作之前,我们需要知道go有三种源码文件:      1,命令源码文件;声明自己属于main包,并且包含main函数的文件,每个项目只能有一个这样的文件,即程序的入口文件      2,库源码文件;不能直接被执行的源码文件      3,测试源码文件本次操作不涉及测试源码文件。go run
go语言开发入门:GO 开发者对 GO 初学者的建议
以促进 India 的 go 编程作为 GopherConIndia 承诺的一部分。我们采访了 40 位 Gophers(一个 Gopher 代表一个 GO 项目或是任何地方的 GO 程序员),得到了他们关于 GO 的意见。如果你正好刚刚开始 go 编程,他们对于我们一些问题的答案可能会对你有非常有用。看看这些。应该做:通读 the Go standard
Linux环境部署go运行环境并启动项目
第一步、搭建Go生产环境1.下载包 https://golang.org/dl/2.解压(有1.14.4版本了,tar zxvf后回有个go文件夹) cd /usr/local/ wget https://dl.google.com/go/go1.13.6.linuxamd64.tar.gz tar xf go1.13.
GO的执行原理以及GO命令
一、Go的源码文件 Go 的源码文件分类: yuanmawenjian1(https://imghelloworld.osscnbeijing.aliyuncs.com/b50e58692d24232e7d6437
go每日一库 [go-rate] 速率限制器
关于我gorate是速率限制器库,基于 Token Bucket(令牌桶)算法实现。 gorate被用在生产中 用于遵守GitHub API速率限制。速率限制可以完成一些特殊的功能需求,包括但不限于服务器端垃圾邮件保护、防止api调用饱和等。 库使用说明 构造限流器我们首先构造一个限流器对象:golimiter : NewLimi
一篇文章彻底弄懂go语言方法的本质
Go 语言不支持经典的面向对象语法元素,比如:类、对象、继承等。但 Go 语言也有方法(method)。和函数相比,Go 语言中的方法在声明形式上仅仅多了一个参数,Go 称之为 receiver 参数。而 receiver 参数正是方法与类型之间的纽带。Go 方法的一般声明形式如下:gofunc (receiver T/T) MethodName(参数列表)
理解go语言包导入路径的含义
Go 语言是使用包(package)作为基本单元来组织源码的,可以说一个 Go 程序就是由一些包链接在一起构建而成的。虽然与 Java、Python 等语言相比这算不上什么创新,但与祖辈 C 语言的头文件包含机制相比则是“先进”了许多。编译速度快是这种”先进性“的一个突出表现,即便是每次编译都是从零开始。Go 语言的这种以包为基本构建单元的构建模型使得依赖分
一篇文章带你弄懂Python异常简介和案例分析
点击上方“Go语言进阶学习”,进行关注回复“Go语言”即可获赠从入门到进阶共10本电子书今日鸡汤似此星辰非昨夜,为谁风露立中宵。大家好,我是Go进阶者,今天给大家分享一些Python基础 (异常),一起来看看吧一、异常简介当Python检测到一个错误时,解释器就无法继续执行了,反而出现了一些错误的提示,这就是所谓的"异常"。 二、案例分析 打开一个不存在的