《Go语言精进之路》读书笔记 | 理解Go语言的包导入

Posted COCOgsta

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《Go语言精进之路》读书笔记 | 理解Go语言的包导入相关的知识,希望对你有一定的参考价值。

书籍来源:《Go语言精进之路:从新手到高手的编程思想、方法和技巧》

一边学习一边整理读书笔记,并与大家分享,侵权即删,谢谢支持!

附上汇总贴:《Go语言精进之路》读书笔记 | 汇总_COCOgsta的博客-CSDN博客


一个Go程序就是由一些包链接在一起构建而成的。Go语言的这种以包为基本构建单元的构建模型使依赖分析变得十分简单。Go编译速度快的原因具体体现在以下三方面。

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

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

已编译的Go包对应的目标文件(file_name.o或package_name.a)中不仅记录了该包本身的导出符号信息,还记录了其所依赖包的导出符号信息。这样,Go编译器在编译某包P时,针对P依赖的每个包导入(比如导入包Q),只需读取一个目标文件即可。

Go语言中包的定义和使用十分简单。通过package关键字声明Go源文件所属的包:

// xx.go
package a
...
复制代码

使用import关键字导入依赖的标准库包或第三方包:

package main

import (
    "fmt"    // 标准库包导入
    "a/b/c"  // 第三方包导入
)

func main() 
    c.Func1()
    fmt.Println("Hello, Go!")

复制代码

16.1 Go程序构建过程

和主流静态编译型语言一样,Go程序的构建简单来讲也是由编译(compile)和链接(link)两个阶段组成的。

一个非main包在编译后会对应生成一个.a文件,该文件可以理解为Go包的目标文件,该目标文件实际上是通过pack工具对.o文件打包后形成的。如果是构建可执行程序,那么.a文件会在构建可执行程序的链接阶段起使用。

标准库包的源码文件在GOROOT/src下面,而对应的.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文件呢?

在使用第三方包的时候,在第三方包源码存在且对应的.a已安装的情况下,编译器链接的仍是根据第三方包最新源码编译出的.a文件(存放在临时目录下的包的.a文件),而不是之前已经安装到$GOPATH/pkg/darwin_amd64下的目标文件。

默认情况下对于标准库中的包,编译器直接链接的是$GOROOT/pkg/darwin_amd64下的.a文件。

那么如何让编译器能够“感知”到标准库中的最新更新呢?有两种方法。

1)删除$GOROOT/pkg/darwin_amd64下的.a文件,然后重新执行go install xxx。

2)使用go build的-a命令行选项。

go build -a可以让编译器将Go源文件(比如例子中的main.go)的所有直接和间接的依赖包(包括标准库)都重新编译一遍,并将最新的.a作为链接器的输入。

16.2 究竟是路径名还是包名

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

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

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

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

未来的Go版本将只有module-aware模式,即只在module缓存的目录下搜索包的源码。

搜索路径的第二部分就是位于每个包源码文件头部的包导入路径。

源文件头部的包导入语句import后面的部分就是一个路径,路径的最后一个分段也不是包名。

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

当包名与包导入路径中的最后一个目录名不同时,最好用下面的语法将包名显式放入包导入语句,这种惯用法让代码可读性更好。

// app2/main.go

package main

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

func main() 
    mypkg2.Func1()

复制代码

16.3 包名冲突问题

同一个包名在不同的项目、不同的仓库下可能都会存在。解决冲突的方法还是用为包导入路径下的包显式指定包名的方法:

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语言精进之路》读书笔记 | 理解Go语言的包导入的主要内容,如果未能解决你的问题,请参考以下文章

《Go语言精进之路》读书笔记 | 理解Go语言代码块与作用域

《Go语言精进之路》读书笔记 | 理解Go语言表达式的求值顺序

《Go语言精进之路》读书笔记 | 汇总

《Go语言精进之路》读书笔记 | 选择适当的Go语言版本

《Go语言精进之路》读书笔记 | 了解Go语言的诞生与演进

《Go语言精进之路》读书笔记 | 使用Go语言原生编码思维来写Go代码