GCTT | Go 中的 import 声明

362 阅读9分钟
原文链接: mp.weixin.qq.com

Go 中的程序由各种包组成。通常,包依赖于其它包,这些包内置于标准库或者第三方。包首先需要被导入才能使用包中的导出标识符。这是通过结构体调用 import 声明 来实现的:

1 package main2 3 import (4    "fmt"5    "math"6 )7 8 func main() {9     fmt.Println(math.Exp2(10))  // 102410 }

上面我们有一个 import 导入的例子,其中包含了两行导入声明。每行声明定义了单个包的导入。

命名为 main 的包,是用来创建可执行的二进制文件。程序的执行是从包 main 开始,通过调用包中也叫做  main 的函数开始。

但是,还有其它一些鲜为人知的导入用法,这些用法在各种场景下都很实用:

1 import (2     "math"3     m "math"4     . "math"5     _ "math"6 )

这四个导入格式都有各自不同的行为,在这篇文章中我们将分析这些差异。

导入包只能引用导入包中的导出标识符。导出标识符是以Unicode大写字母开头的

https://golang.org/ref/spec#Exported_identifiers。

基础

Import 声明剖析

ImportDeclaration = "import" ImportSpecImportSpec        = [ "." | "_" | Identifier ] ImportPath

Identifier 是将在限定标识符中使用的任何有效标识符。ImportPath 是一个字符串(原始或解释字符串,译注:例如 \n 和  "\n" 的区别,原始字符串或回车)让我们看一些例子:

1 import . "fmt"2 import _ "io"3 import log "github.com/sirupsen/logrus"4 import m "math"

合并 Import 声明

导入两个或者更多的包可以有两种写法。一个是,我们可以写多个 import 声明:

1 import "io"2 import "bufio"

或者,我们可以将多个 import 声明合并(将多个导入放在一条导入声明中):

1 import (2     "io"3     "bufio"4 )

第二种导入方式在导入很多个包的时候非常实用,然后多次重复的用 import 关键字导入包会降低可读性。如果你不使用自动导入之类的工具,例如:https://github.com/bradfitz/goimports,这种方式还可以减少按键次数。

(短)导入路径

导入规范中使用的字符串文字(每个导入声明包含一个或多个导入规范)告诉导入哪个包。这个字符串称为导入路径。根据语言规范,它取决于如何解释导入路径(字符串)的实现方式,但在现实运用中它的路径相对包的第三方库目录或 go env GOPATH / src 目录(更多内容参考  GOPATH )。

内置的包导入使用 “math” 或  “fmt” 等短导入路径。

.go 文件剖析

每个.go 文件的结构是相同的。首先是 package 语句,可选地在其前面加上通常是描述包的作用的注释。然后零个或多个导入声明。接着包含零个或多个顶级声明。

1 // description...2 package main // package clause3 4 // zero or more import declarations5 import (6     "fmt"7     "strings"8 )9 10 import "strconv"11 12 // top-level declarations13 14 func main() {15     fmt.Println(strings.Repeat(strconv.FormatInt(15, 16 16), 5))17 }

强制组织 (Enforced organisation) 不允许引入不必要的混乱,这简化了解析和基本的代码库跳转(导入声明不能放在  package 子句之前,也不能与顶级声明交错,所以它总是很容易找到)。

导入作用域

导入的作用域是文件块级别。这意味着它可以从整个文件中访问,但不能在整个包中被访问:

1 // github.com/mlowicki/a/main.go2 package main3 4 import "fmt"5 6 func main() {7    fmt.Println(a)8 }9 10 // github.com/mlowicki/a/foo.go11 package main12 13 var a int = 114 15 func hi() {16    fmt.Println("Hi!")17 }

上述代码无法被成功编译:

go build// github.com/mlowicki/a./foo.go:6:2: undefined: fmt更多的关于作用域的内容参考之前发表的文章:Scopes in Go

导入的类型

自定义包名按照约定,导入路径的最后一个部分同时也是导入包的包名。当然,我们也可以不遵循这个约定:

1 // github.com/mlowicki/main.go2 package main3 4 import (5     "fmt"6     "github.com/mlowicki/b"7 )8 9 func main() {10     fmt.Println(c.B)11 }12 13 // github.com/mlowicki/b/b.go14 package c15 16 var B = "b"

这个输出很明显是 b 。当然尽可能的遵循这些约定是更好的 很多工具也是依赖这个约定。如果自定义包名在导入的时候没有特别的指定,则使用来自包子句的名称来引用导入包的导出标识符:

1 package main2 import "fmt"3 func main() {4     fmt.Println("Hi!")5 }

也可以自定义一个包名称进行导入:

1 // github.com/mlowicki/b/b.go2 package b3 4 var B = "b"5 6 // github.com/mlowicki/main.go (依据原文含义,译者添加)7 package main8 9 import (10     "fmt"11     c "github.com/mlowicki/b"12 )13 14 func main() {15     fmt.Println(c.B)16 }

这个输出结果和之前一样。如果我们的包具有与其它包相同的接口(导出的标识符),则这种导入形式非常有用。一个这样的例子是 https://github.com/sirupsen/logrus,它有一个与  log 兼容的 API

1 import log "github.com/sirupsen/logrus"

如果我们只使用内置日志包中的 API ,那么用导入  log 替换这样的导入不需要对源代码进行任何更改。它也有点短(但仍然有意义)所以可能会节省一些按键次数。

导入所有的导出标识符

例如:

import m "math"import "fmt"

可以使用指定的包的别名 (m.Exp) 或者导入的包名  (fmt.Prinln) 实现引用导出标识符。还有另一个方式不用通过限定标识符就可以访问导出标识符:

1 package main2 3 import (4     "fmt"5     . "math"6 )7 8 func main() {9     fmt.Println(Exp2(6))  // 6410 }

什么时候这种用法有用呢?在测试中。假设我们有一个包 b 导入包  a。现在我们想给包a 添加测试。如果测试也在包 a 中,并且测试也会导入包  b (因为到时需要在那实现一些东西),那么我们将最终将会变成循环依赖,这是禁止的。绕过它的一种方法是将测试放入单独的包中,如 a_tests。然后我们需要导入包 a并使用限定标识符引用每个导出的标识符。为了让我们的实现的更轻松,我们可以用点来导入包 a

1 import . "a"

然后引用包 a中的导出标识符就不需要带上包名(就像测试是在同一个包中一样,但是那些非导出的标识符是不能访问的)

如果导入的包中存在至少一个同名的导出标识符,则无法使用点作为包名导入两个包:

1 // github.com/mlowicki/c2 package c3 4 var V = "c"5 // github.com/mlowkci/b6 package b7 8 var V = "b"9 10 // github.com/mlowicki/a11 package main12 13 import (14     "fmt"15     . "github.com/mlowicki/b"16     . "github.com/mlowicki/c"17 )1819 func main() {20     fmt.Println(V)21 }

go run main.go// command-line-arguments./main.go:6:2: V redeclared during import "github.com/mlowicki/c"previous declaration during import "github.com/mlowicki/b" ./main.go:6:2: imported and not used: "github.com/mlowicki/c"

使用空标识符

如果导入了包但是不使用,Golang的编译器将无法编译通过。

1 package main2 3 import "fmt"4 5 func main() {}

使用点导入,其中所有导出的标识符都直接添加到导入文件块中,在编译时也会出现失败。唯一的绕过方式是使用空白标识符。需要知道init函数是什么,以便理解为什么我们需要导入空白标识符。参考之前 init的介绍文章 https://medium.com/golangspec/init-functions-in-go-eac191b3860a 我鼓励从上到下阅读这篇文章,但本质上,像如下的导入方式:

1 import _ "math"

不需要在导入文件中使用包math,但是无论如何都将执行导入包中的  init函数(包和它的依赖关系将被初始化)。如果我们只对导入包完成的初始化工作感兴趣,但我们不引用任何的导出标识符,那么就很有用。

如果一个包被导入没有被使用或者没有使用空标识符,那将编译失败

循环导入

Go 规范明确禁止循环导入  - 当包间接导入自身时。最明显的情况是包 a 导入包 b 然后包b 中也导入包 a

1 // github.com/mlowicki/a/main.go2 package a3 4 import "github.com/mlowicki/b"5 6 var A = b.B7 8 // github.com/mlowicki/b/main.go9 package b1011 import "github.com/mlowicki/a"12 13 var B = a.A

尝试构建这两个包中的任何一个都会导致错误:

go buildcan't load package: import cycle not allowedpackage github.com/mlowicki/aimports github.com/mlowicki/bimports github.com/mlowicki/a

当然,比如 a -> b -> c -> d -> a 这种情况更加的复杂 (x -> y 指包 x 导入包 y)。

包也是不能导入自己的:

1 package main2 3 import (4     "fmt"5     "github.com/mlowicki/a"6 )78 var A = "a"910 func main() {11     fmt.Println(a.A)12 }

编译上述代码将会提示错误:can’t load package: import cycle not allowed

(完)

via: https://medium.com/golangspec/import-declarations-in-go-8de0fd3ae8ff


作者:Michał Łowicki译者:iloghyr校对:Unknwon

本文由 GCTT 原创编译,Go语言中文网 荣誉推出