Golang使用包(package)这种语法元素来组织源码,所有语法可见性均定义在package这个级别,与Java 、python等语言相比,这算不上什么创新,但与C传统的include相比,则是显得“先进”了许多。

Golang中包的定义和使用看起来十分简单:

通过package关键字定义包:

package xxx

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

 import "a/b/c"import "fmt"c.Func1()fmt.Println("Hello, World")

很多Golang初学者看到上面代码,都会想当然的将import后面的"c"、"fmt"当成包名,将其与c.Func1()和 fmt.Println()中的c和fmt认作为同一个语法元素:包名。但在深入Golang后,很多人便会发现事实上并非如此。比如在使用实时分布式消息平台nsq提供的go client api时:

我们导入的路径如下:

 import “github.com/bitly/go-nsq”

但在使用其提供的export functions时,却用nsq做前缀包名:

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

人们不禁要问:import后面路径中的最后一个元素到底代表的是啥? 是包名还是仅仅是一个路径?我们一起通过试验来理解一下。  实验环境:darwin_amd64 , go 1.4。

初始试验环境目录结构如下:

GOPATH = /Users/tony/Test/Go/pkgtest/
pkgtest/
    pkg/
    src/
       libproj1/
           foo/
              foo1.go
       app1/
           main.go

一、编译时使用的是包源码还是.a

我们知道一个非main包在编译后会生成一个.a文件(在临时目录下生成,除非使用go install安装到$GOROOT或$GOPATH下,否则你看不到.a),用于后续可执行程序链接使用。

比如Go标准库中的包对应的源码部分路径在:$GOROOT/src,而标准库中包编译后的.a文件路径在$GOROOT/pkg/darwin_amd64下。一个奇怪的问题在我脑袋中升腾起来,编译时,编译器到底用的是.a还是源码

我们先以用户自定义的package为例做个小实验。

$GOPATH/src/
    libproj1/foo/
            – foo1.go
    app1/
            – main.go

//foo1.go

package fooimport "fmt"func Foo1() {fmt.Println("Foo1")
}

// main.go

package mainimport ("libproj1/foo"
)func main() {foo.Foo1()
}

执行go install libproj1/foo,Go编译器编译foo包,并将foo.a安装到$GOPATH/pkg/darwin_amd64/libproj1下。
编译app1:go build app1,在app1目录下生成app1*可执行文件,执行app1,我们得到一个初始预期结果:

$./app1
Foo1

现在我们无法看出使用的到底是foo的源码还是foo.a,因为目前它们的输出都是一致的。我们修改一下foo1.go的代码://foo1.go

package fooimport "fmt"func Foo1() {fmt.Println("Foo1 – modified")
}

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

$./app1
Foo1 – modified

实际测试结果告诉我们:(1)在使用第三方包的时候,当源码和.a均已安装的情况下,编译器链接的是源码

那么是否可以只链接.a,不用第三方包源码呢?我们临时删除掉libproj1目录,但保留之前install的libproj1/foo.a文件。

我们再次尝试编译app1,得到如下错误:

$go build app1
main.go:5:2: cannot find package "libproj1/foo" in any of:/Users/tony/.Bin/go14/src/libproj1/foo (from $GOROOT)/Users/tony/Test/Go/pkgtest/src/libproj1/foo (from $GOPATH)

编译器还是去找源码,而不是.a,因此我们要依赖第三方包,就必须搞到第三方包的源码,这也是Golang包管理的一个特点。

其实通过编译器的详细输出我们也可得出上面结论。我们在编译app1时给编译器传入-x -v选项:

$go build -x -v app1
WORK=/var/folders/2h/xr2tmnxx6qxc4w4w13m01fsh0000gn/T/go-build797811168
libproj1/foo
mkdir -p $WORK/libproj1/foo/_obj/
mkdir -p $WORK/libproj1/
cd /Users/tony/Test/Go/pkgtest/src/libproj1/foo
/Users/tony/.Bin/go14/pkg/tool/darwin_amd64/6g -o $WORK/libproj1/foo.a -trimpath $WORK -p libproj1/foo -complete -D _/Users/tony/Test/Go/pkgtest/src/libproj1/foo -I $WORK -pack ./foo1.go ./foo2.go
app1
mkdir -p $WORK/app1/_obj/
mkdir -p $WORK/app1/_obj/exe/
cd /Users/tony/Test/Go/pkgtest/src/app1
/Users/tony/.Bin/go14/pkg/tool/darwin_amd64/6g -o $WORK/app1.a -trimpath $WORK -p app1 -complete -D _/Users/tony/Test/Go/pkgtest/src/app1 -I $WORK -I /Users/tony/Test/Go/pkgtest/pkg/darwin_amd64 -pack ./main.go
cd .
/Users/tony/.Bin/go14/pkg/tool/darwin_amd64/6l -o $WORK/app1/_obj/exe/a.out -L $WORK -L /Users/tony/Test/Go/pkgtest/pkg/darwin_amd64 -extld=clang $WORK/app1.a
mv $WORK/app1/_obj/exe/a.out app1

可以看到编译器6g首先在临时路径下编译出依赖包foo.a,放在$WORK/libproj1下。但我们在最后6l链接器的执行语句中并未显式看到app1链接的是$WORK/libproj1下的foo.a。但是从6l链接器的-L参数来看:-L $WORK -L /Users/tony/Test/Go/pkgtest/pkg/darwin_amd64,我们发现$WORK目录放在了前面,我们猜测6l首先搜索到的是$WORK下面的libproj1/foo.a。

为了验证我们的推论,我们按照编译器输出,按顺序手动执行了一遍如上命令,但在最后执行6l命令时,去掉了-L $WORK:

/Users/tony/.Bin/go14/pkg/tool/darwin_amd64/6l -o $WORK/app1/_obj/exe/a.out -L /Users/tony/Test/Go/pkgtest/pkg/darwin_amd64 -extld=clang $WORK/app1.a

这样做的结果是:

$./app1
Foo1

编译器链接了$GOPATH/pkg下的foo.a。(2)到这里我们明白了所谓的使用第三方包源码,实际上是链接了以该最新源码编译的临时目录下的.a文件而已。

Go标准库中的包也是这样么?对于标准库,比如fmt而言,编译时,到底使用的是$GOROOT/src下源码还是$GOROOT/pkg下已经编译好的.a呢?我们不妨也来试试,一个最简单的hello world例子:
//main.go

import "fmt"func main() {fmt.Println("Hello, World")
}

我们先将$GOROOT/src/fmt目录rename 为fmtbak,看看go compiler有何反应?
go build -x -v ./

$go build -x -v ./
WORK=/var/folders/2h/xr2tmnxx6qxc4w4w13m01fsh0000gn/T/go-build957202426
main.go:4:8: cannot find package "fmt" in any of:/Users/tony/.Bin/go14/src/fmt (from $GOROOT)/Users/tony/Test/Go/pkgtest/src/fmt (from $GOPATH)

找不到fmt包了。显然标准库在编译时也是必须要源码的。不过与自定义包不同的是,即便你修改了fmt包的源码(未重新编译GO安装包),用户源码编译时,也不会尝试重新编译fmt包的,依旧只是在链接时链接已经编译好的fmt.a。通过下面的gc输出可以验证这点:

$go build -x -v ./
WORK=/var/folders/2h/xr2tmnxx6qxc4w4w13m01fsh0000gn/T/go-build773440756
app1
mkdir -p $WORK/app1/_obj/
mkdir -p $WORK/app1/_obj/exe/
cd /Users/tony/Test/Go/pkgtest/src/app1
/Users/tony/.Bin/go14/pkg/tool/darwin_amd64/6g -o $WORK/app1.a -trimpath $WORK -p app1 -complete -D _/Users/tony/Test/Go/pkgtest/src/app1 -I $WORK -pack ./main.go
cd .
/Users/tony/.Bin/go14/pkg/tool/darwin_amd64/6l -o $WORK/app1/_obj/exe/a.out -L $WORK -extld=clang $WORK/app1.a
mv $WORK/app1/_obj/exe/a.out app1

可以看出,编译器的确并未尝试编译标准库中的fmt源码。

二、目录名还是包名?

从第一节的实验中,我们得知了编译器在编译过程中依赖的是包源码的路径,这为后续的实验打下了基础。下面我们再来看看,Go语言中import后面路径中最后的一个元素到底是包名还是路径名?

本次实验目录结构:

$GOPATH
    src/
       libproj2/
             foo/
               foo1.go
       app2/
             main.go

按照Golang语言习惯,一个go package的所有源文件放在同一个目录下,且该目录名与该包名相同,比如libproj1/foo目录下的package为foo,foo1.go、 foo2.go…共同组成foo package的源文件。但目录名与包名也可以不同,我们就来试试不同的。

我们建立libproj2/foo目录,其中的foo1.go代码如下://foo1.go

package barimport "fmt"func Bar1() {fmt.Println("Bar1")
}

注意:这里package名为bar,与目录名foo完全不同。

接下来就给app2带来了难题:该如何import bar包呢?

我们假设import路径中的最后一个元素是包名,而非路径名。

//app2/main.go

package mainimport ("libproj2/bar"
)func main() {bar.Bar1()
}

编译app2:

$go build -x -v app2
WORK=/var/folders/2h/xr2tmnxx6qxc4w4w13m01fsh0000gn/T/go-build736904327
main.go:5:2: cannot find package "libproj2/bar" in any of:/Users/tony/.Bin/go14/src/libproj2/bar (from $GOROOT)/Users/tony/Test/Go/pkgtest/src/libproj2/bar (from $GOPATH)

编译失败,在两个路径下无法找到对应libproj2/bar包。

我们的假设错了,我们把它改为路径:

//app2/main.go

package mainimport ("libproj2/foo"
)func main() {bar.Bar1()
}

再编译执行:

$go build app2
$app2
Bar1

这回编译顺利通过,执行结果也是OK的。这样我们得到了结论:(3)import后面的最后一个元素应该是路径,就是目录,并非包名

go编译器在这些路径(libproj2/foo)下找bar包。这样看来,go语言的惯例只是一个特例,即恰好目录名与包名一致罢了。也就是说下面例子中的两个foo含义不同:

import "libproj1/foo"func main() {foo.Foo()
}

import中的foo只是一个文件系统的路径罢了。而下面foo.Foo()中的foo则是包名。而这个包是在libproj1/foo目录下的源码中找到的。

再类比一下标准库包fmt。

import "fmt"
fmt.Println("xxx")

这里上下两行中虽然都是“fmt",但同样含义不同,一个是路径 ,对于标准库来说,是$GOROOT/src/fmt这个路径。而第二行中的fmt则是包名。gc会在$GOROOT/src/fmt路径下找到fmt包的源文件。

三、import m "lib/math"

Go language specification中关于import package时列举的一个例子如下:

Import declaration          Local name of Sin

import   "lib/math"         math.Sin
import m "lib/math"         m.Sin
import . "lib/math"         Sin

我们看到import m "lib/math"  m.Sin一行。我们说过lib/math是路径,import语句用m替代lib/math,并在代码中通过m访问math包中的导出函数Sin。

那m到底是包名还是路径呢?既然能通过m访问Sin,那m肯定是包名了,Right!那import m "lib/math"该如何理解呢?

根据上面一、二两节中得出的结论,我们尝试理解一下m:(4)m指代的是lib/math路径下唯一的那个包

一个目录下是否可以存在两个包呢?我们来试试。

我们在libproj1/foo下新增一个go源文件,bar1.go:

package barimport "fmt"func Bar1() {fmt.Println("Bar1")
}

我们重新构建一下这个目录下的包:

$go build libproj1/foo
can't load package: package libproj1/foo: found packages bar1.go (bar) and foo1.go (foo) in /Users/tony/Test/Go/pkgtest/src/libproj1/foo

我们收到了错误提示,编译器在这个路径下发现了两个包,这是不允许的。

我们再作个实验,来验证我们对m含义的解释。

我们建立app3目录,其main.go的源码如下://main.go

package mainimport m "libproj2/foo"func main() {m.Bar1()
}

libproj2/foo路径下的包的包名为bar,按照我们的推论,m指代的就是bar这个包,通过m我们可以访问bar的Bar1导出函数。

编译并执行上面main.go:

$go build app3
$app3
Bar1

执行结果与我们推论完全一致。

附录:6g, 6l文档位置:

6g – $GOROOT/src/cmd/gc/doc.go
6l – $GOROOT/src/cmd/ld/doc.go

© 2015, bigwhite. 版权所有.

Related posts:

  1. Golang跨平台交叉编译
  2. Golang测试技术
  3. godep的一个“坑”
  4. Go 1.4中值得关注的几个变化
  5. 也谈Go语言编程 – Hello,Go!

深入理解Golang包导入相关推荐

  1. Go包导入与Java的差别

    闲暇时翻阅了近期下载到的电子书<Go in Practice> ,看到1.2.4 Package Management一节中的代码Demo,感觉作者对Go package导入的说法似乎不够 ...

  2. golang 理解包导入

    Golang使用包(package)这种语法元素来组织源码,所有语法可见性均定义在package这个级别,与Java .python等语言相比,这算不上什么创新,但与C传统的include相比,则是显 ...

  3. access导入链接快还是导入表快_Go 语言设计哲学之十三:理解包导入路径的含义...

    Go 语言是使用包(package)作为基本单元来组织源码的, Go 程序就是这些包链接起来而构建的.与C 语言的头文件包含机制相比则是"先进"了许多. 即便是每次编译都是从头开始 ...

  4. 深入理解Golang中的Context包

    context.Context是Go语言中独特的设计,在其他编程语言中我们很少见到类似的概念.context.Context深度支持Golang的高并发. 1. Goroutine和Channel 在 ...

  5. golang import 导入包语法介绍 点 别名 下划线

    package 的导入语法 写 Go 代码的时经常用到 import 这个命令用来导入包,参考如下: import("fmt" ) 然后在代码里面可以通过如下的方式调用: fmt. ...

  6. go get 指定版本_没有VPS搭建govanityurls服务?别急!你依然可以自定义Go包导入路径...

    我们见到的Go包的导入路径常常以github.com.bitbucket.org等代码托管站点的域名为前缀,这样的包导入路径有一个问题,那就是当Go包的托管站点发生变更时(比如从github.om迁移 ...

  7. 深入理解Golang 编程思维和工程实战

    | 导语 Golang 的一些编程思维和思想,以及总结一些常见的优雅编程实战技巧 目录 一 Golang 编程思维 二 Golang 高级编码技巧 1 优雅的实现构造函数编程思想 2 优雅的实现继承编 ...

  8. 深入理解Golang之context

    深入理解Golang之context context是Go并发编程中常用到一种编程模式.本文将从为什么需要context,深入了解context的实现原理,以了解如何使用context. 作者:Tur ...

  9. 【Android】条形码/二维码扫描——ZXing源码分析及相关jar包导入

    转载自:http://blog.csdn.net/u010574567/article/details/51916604 *********************1.源码分析************ ...

最新文章

  1. 2019年末,10 位院士对 AI 的深度把脉(上)
  2. Leetcode | Maximal Rectangle
  3. 《信息检索导论》第三章总结
  4. 动态规划——入门(1)
  5. Bitcoin 中的挖矿算法(3) 挖矿算法代码说明
  6. 【leetcode】Path Sum II
  7. 【2014-11-23】《The Hardware/Software Interface》– Section 11
  8. Mac上使用homebrew安装PostgreSql
  9. 一个计算两个日期间隔的算法
  10. Could NOT find OpenSSL, try to set the path to OpenSSL root folder in the system variable
  11. vue项目之不一样的axios封装(+防抖函数)
  12. centos6.5里用yum简单安装配置lamp
  13. [Oracle 9i] Case Expression and Case Statement in 9i
  14. 华硕win10键盘失灵_win10笔记本键盘失灵
  15. FPGA智能网卡功能剖析
  16. 刘国忠:顺周期股受资金青睐,但能走多远还是未知!
  17. 当你追求女生时,你们聊些什么?
  18. eclipes和idea常用快捷键及缩写大全
  19. Kotlin读书笔记之函数式kotlin
  20. 如何区分MNO和MVNO

热门文章

  1. vue.js 2.0实现的简单分页
  2. 安装phpcms时出现Warning: ob_start(): output handler \'ob_gzhandler\' conflicts with \'zlib
  3. 【转载】数据库范式那些事
  4. jar包使用以及生成
  5. Android横竖屏切换继续播放视频
  6. [转载]根据两点的经纬度求方位角和距离,等
  7. 五种网络管理技巧优化网络办公环境
  8. linux环境下创建MyOS虚拟机
  9. Linux中zip压缩和unzip解压缩
  10. C/C++程序从编译到最终生成可执行文件的过程分析