|
| 1 | +# 3.4 Go 语言中关于包导入必学的 8 个知识点 |
| 2 | + |
| 3 | +## 1. 单行导入与多行导入 |
| 4 | + |
| 5 | +在 Go 语言中,一个包可包含多个 `.go` 文件(这些文件必须得在同一级文件夹中),只要这些 `.go` 文件的头部都使用 `package` 关键字声明了同一个包。 |
| 6 | + |
| 7 | +导入包主要可分为两种方式: |
| 8 | + |
| 9 | +- 单行导入 |
| 10 | + |
| 11 | +``` |
| 12 | +import "fmt" |
| 13 | +import "sync" |
| 14 | +``` |
| 15 | + |
| 16 | +- 多行导入 |
| 17 | + |
| 18 | +``` |
| 19 | +import( |
| 20 | + "fmt" |
| 21 | + "sync" |
| 22 | +) |
| 23 | +``` |
| 24 | + |
| 25 | + |
| 26 | + |
| 27 | +>如你所见,Go 语言中 导入的包,必须得用双引号包含,在这里吐槽一下。 |
| 28 | +
|
| 29 | + |
| 30 | + |
| 31 | +## 2. 使用别名 |
| 32 | + |
| 33 | +在一些场景下,我们可能需要对导入的包进行重新命名,比如 |
| 34 | + |
| 35 | +- 我们导入了两个具有同一包名的包时产生冲突,此时这里为其中一个包定义别名 |
| 36 | + |
| 37 | +```go |
| 38 | +import ( |
| 39 | + "crypto/rand" |
| 40 | + mrand "math/rand" // 将名称替换为mrand避免冲突 |
| 41 | +) |
| 42 | +``` |
| 43 | + |
| 44 | +- 我们导入了一个名字很长的包,为了避免后面都写这么长串的包名,可以这样定义别名 |
| 45 | + |
| 46 | +```go |
| 47 | +import hw "helloworldtestmodule" |
| 48 | +``` |
| 49 | + |
| 50 | +- 防止导入的包名和本地的变量发生冲突,比如 path 这个很常用的变量名和导入的标准包冲突。 |
| 51 | + |
| 52 | +```go |
| 53 | +import pathpkg "path" |
| 54 | +``` |
| 55 | + |
| 56 | + |
| 57 | + |
| 58 | +## 3. 使用点操作 |
| 59 | + |
| 60 | +如里在我们程序内部里频繁使用了一个工具包,比如 fmt,那每次使用它的打印函数打印时,都要 包名+方法名。 |
| 61 | + |
| 62 | +对于这种使用高频的包,可以在导入的时,就把它定义会 "`自己人`"(方法是使用一个 `.` ),自己人的话,不分彼此,它的方法,就是我们的方法。 |
| 63 | + |
| 64 | +从此,我们打印再也不用加 fmt 了。 |
| 65 | + |
| 66 | +``` |
| 67 | +import . "fmt" |
| 68 | +
|
| 69 | +func main() { |
| 70 | + Println("hello, world") |
| 71 | +} |
| 72 | +``` |
| 73 | + |
| 74 | +但这种用法,会有一定的隐患,就是导入的包里可能有函数,会和我们自己的函数发生冲突。 |
| 75 | + |
| 76 | +## 4. 包的初始化 |
| 77 | + |
| 78 | +每个包都允许有一个 `init` 函数,当这个包被导入时,会执行该包的这个 `init` 函数,做一些初始化任务。 |
| 79 | + |
| 80 | +对于 `init ` 函数的执行有两点需要注意 |
| 81 | + |
| 82 | +1. `init` 函数优先于 `main` 函数执行 |
| 83 | + |
| 84 | +2. 在一个包引用链中,包的初始化是深度优先的。比如,有这样一个包引用关系:main→A→B→C,那么初始化顺序为 |
| 85 | + |
| 86 | + ``` |
| 87 | + C.init→B.init→A.init→main |
| 88 | + ``` |
| 89 | + |
| 90 | + |
| 91 | + |
| 92 | +## 5. 包的匿名导入 |
| 93 | + |
| 94 | +当我们导入一个包时,如果这个包没有被使用到,在编译时,是会报错的。 |
| 95 | + |
| 96 | +但是有些情况下,我们导入一个包,只想执行包里的 `init` 函数,来运行一些初始化任务,此时怎么办呢? |
| 97 | + |
| 98 | +可以使用匿名导入,用法如下,其中下划线为空白标识符,并不能被访问 |
| 99 | + |
| 100 | +```go |
| 101 | +// 注册一个PNG decoder |
| 102 | +import _ "image/png" |
| 103 | +``` |
| 104 | + |
| 105 | +由于导入时,会执行 init 函数,所以编译时,仍然会将这个包编译到可执行文件中。 |
| 106 | + |
| 107 | +## 6. 导入的是路径还是包? |
| 108 | + |
| 109 | +当我们使用 import 导入 `testmodule/foo` 时,初学者,经常会问,这个 `foo` 到底是一个包呢,还是只是包所在目录名? |
| 110 | + |
| 111 | +``` |
| 112 | +import "testmodule/foo" |
| 113 | +``` |
| 114 | + |
| 115 | +为了得出这个结论,专门做了个试验(请看「第七点里的代码示例」),最后得出的结论是: |
| 116 | + |
| 117 | +- 导入时,是按照目录导入。导入目录后,可以使用这个目录下的所有包。 |
| 118 | +- 出于习惯,包名和目录名通常会设置成一样,所以会让你有一种你导入的是包的错觉。 |
| 119 | + |
| 120 | +## 7. 相对导入和绝对导入 |
| 121 | + |
| 122 | +据我了解在 Go 1.10 之前,好像是不支持相对导入的,在 Go 1.10 之后才可以。 |
| 123 | + |
| 124 | +**绝对导入**:从 `$GOPATH/src` 或 `$GOROOT` 或者 `$GOPATH/pkg/mod` 目录下搜索包并导入 |
| 125 | + |
| 126 | +**相对导入**:从当前目录中搜索包并开始导入。就像下面这样 |
| 127 | + |
| 128 | +``` |
| 129 | +import ( |
| 130 | + "./module1" |
| 131 | + "../module2" |
| 132 | + "../../module3" |
| 133 | + "../module4/module5" |
| 134 | +) |
| 135 | +``` |
| 136 | + |
| 137 | + |
| 138 | + |
| 139 | +分别举个例子吧 |
| 140 | + |
| 141 | +**一、使用绝对导入** |
| 142 | + |
| 143 | +有如下这样的目录结构(注意确保当前目录在 GOPATH 下) |
| 144 | + |
| 145 | + |
| 146 | + |
| 147 | +其中 main.go 是这样的 |
| 148 | + |
| 149 | +```go |
| 150 | +package main |
| 151 | + |
| 152 | +import ( |
| 153 | + "app/utilset" // 这种使用的就是绝对路径导入 |
| 154 | +) |
| 155 | + |
| 156 | +func main() { |
| 157 | + utils.PrintHello() |
| 158 | +} |
| 159 | +``` |
| 160 | + |
| 161 | +而在 main.go 的同级目录下,还有另外一个文件夹 `utilset` ,为了让你理解 「**第六点:import 导入的是路径而不是包**」,我在 utilset 目录下定义了一个 `hello.go` 文件,这个go文件定义所属包为 `utils`。 |
| 162 | + |
| 163 | +```go |
| 164 | +package utils |
| 165 | + |
| 166 | +import "fmt" |
| 167 | + |
| 168 | +func PrintHello(){ |
| 169 | + fmt.Println("Hello, 我在 utilset 目录下的 utils 包里") |
| 170 | +} |
| 171 | +``` |
| 172 | + |
| 173 | +运行结果如下 |
| 174 | + |
| 175 | + |
| 176 | + |
| 177 | +**二、使用相对导入** |
| 178 | + |
| 179 | +还是上面的代码,将绝对导入改为相对导入后 |
| 180 | + |
| 181 | +将 GOPATH 路径设置回去(请对比上面使用绝对路径的 GOPATH) |
| 182 | + |
| 183 | + |
| 184 | + |
| 185 | +然后再次运行 |
| 186 | + |
| 187 | + |
| 188 | + |
| 189 | + |
| 190 | + |
| 191 | +总结一下,使用相对导入,有两点需要注意 |
| 192 | + |
| 193 | +- 项目不要放在 `$GOPATH/src` 下,否则会报错(比如我修改当前项目目录为GOPATH后,运行就会报错) |
| 194 | + |
| 195 | +  |
| 196 | + |
| 197 | +- Go Modules 不支持相对导入,在你开启 GO111MODULE 后,无法使用相对导入。 |
| 198 | + |
| 199 | +最后,不得不说的是:使用相对导入的方式,项目可读性会大打折扣,不利用开发者理清整个引用关系。 |
| 200 | + |
| 201 | +所以一般更推荐使用绝对引用的方式。使用绝对引用的话,又要谈及优先级了 |
| 202 | + |
| 203 | +## 8. 包导入路径优先级 |
| 204 | + |
| 205 | +前面一节,介绍了三种不同的包依赖管理方案,不同的管理模式,存放包的路径可能都不一样,有的可以将包放在 GOPATH 下,有的可以将包放在 vendor 下,还有些包是内置包放在 GOROOT 下。 |
| 206 | + |
| 207 | +那么问题就来了,如果在这三个不同的路径下,有一个相同包名但是版本不同的包,我们导入的时候,是选择哪个进行导入呢? |
| 208 | + |
| 209 | +这就需要我们搞懂,在 Golang 中包搜索路径优先级是怎样的? |
| 210 | + |
| 211 | +这时候就需要区分,是使用哪种模式进行包的管理的。 |
| 212 | + |
| 213 | +**如果使用 govendor** |
| 214 | + |
| 215 | +当我们导入一个包时,它会: |
| 216 | + |
| 217 | +1. 先从项目根目录的 `vendor ` 目录中查找 |
| 218 | +2. 最后从 `$GOROOT/src` 目录下查找 |
| 219 | +3. 然后从 `$GOPATH/src` 目录下查找 |
| 220 | +4. 都找不到的话,就报错。 |
| 221 | + |
| 222 | +为了验证这个过程,我在创建中创建一个 vendor 目录后,就开启了 vendor 模式了,我在 main.go 中随便导入一个包 pkg,由于这个包是我随便指定的,当然会找不到,找不到就会报错, Golang 会在报错信息中打印中搜索的过程,从这个信息中,就可以看到 Golang 的包查找优先级了。 |
| 223 | + |
| 224 | + |
| 225 | + |
| 226 | +**如果使用 go modules** |
| 227 | + |
| 228 | +你导入的包如果有域名,都会先在 `$GOPATH/pkg/mod` 下查找,找不到就连网去该网站上寻找,找不到或者找到的不是一个包,则报错。 |
| 229 | + |
| 230 | +而如果你导入的包没有域名(比如 "fmt"这种),就只会到 `$GOROOT` 里查找。 |
| 231 | + |
| 232 | +还有一点很重要,当你的项目下有 vendor 目录时,不管你的包有没有域名,都只会在 vendor 目录中想找。 |
| 233 | + |
| 234 | + |
| 235 | + |
| 236 | +通常`vendor` 目录是通过 `go mod vendor` 命令生成的,这个命令会将项目依赖全部打包到你的项目目录下的 verdor 文件夹中。 |
| 237 | + |
| 238 | + |
| 239 | + |
| 240 | +--- |
| 241 | + |
| 242 | + |
| 243 | + |
| 244 | + |
0 commit comments