理解Golang包导入

参考文章:http://www.cnblogs.com/sevenyuan/p/4548748.html


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后面路径中的最后一个元素到底代表的是啥? 是包名还是仅仅是一个路径?

我们一起通过试验来理解一下。  实验环境:go version go1.7.5 windows/amd64

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

GOPATH=E:\project\go\pkgtest

E:\project\go\pkgtest>tree /F
文件夹 PATH 列表
卷序列号为 0001-5C61
E:.
├─pkg
└─src
    ├─app1
    │      main.go
    │
    └─libproj1
        └─foo
                foo1.go

1、编译时使用的是包源码还是.a文件

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

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

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

GOPATH=E:\project\go\pkgtest

E:\project\go\pkgtest>tree /F
文件夹 PATH 列表
卷序列号为 0001-5C61
E:.
├─pkg
└─src
    ├─app1
    │      main.go
    │
    └─libproj1
        └─foo
                foo1.go


//foo1.go

package foo


import "fmt"


func Foo1() {
fmt.Println("foo.Foo1()")
}


//main.go

package main


import "libproj1/foo"


func main() {
foo.Foo1()
}


执行go install libproj1/foo,Go编译器编译foo包,并将foo.a安装到$GOPATH/pkg/windows_amd64/libproj1下。

编译app1:go build app1,在当前目录下生成app1.exe可执行文件,执行app1,我们得到一个初始预期结果:

C:\Users\Administrator>app1
foo.Foo1()


现在我们无法看出在生成可执行程序时使用的到底是foo的源码还是foo.a库文件。我们修改一下foo1.go的代码:

//foo1.go

package foo


import "fmt"


func Foo1() {
fmt.Println("foo.Foo1() -modified")
}


再重新编译app1,并执行:

C:\Users\Administrator>go build app1


C:\Users\Administrator>app1
foo.Foo1() -modified


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

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

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

C:\Users\Administrator>go build app1
E:\project\go\pkgtest\src\app1\main.go:3:8: cannot find package "libproj1/foo" in any of:
        C:\GO\src\libproj1\foo (from $GOROOT)
        E:\project\go\pkgtest\src\libproj1\foo (from $GOPATH)

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

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

Go标准库中的包也是这样么?对于标准库,比如fmt而言,编译时,到底使用的时$GOROOT/src下源码还是$GOROOT/pkg下已经编译好的.a呢?

我们不妨也来试试,一个最简单的hello world例子:

//hello.go

package main


import "fmt"


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

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

E:\project>go build .
hello.go:3:8: cannot find package "fmt" in any of:
        C:\GO\src\fmt (from $GOROOT)
        E:\project\go\pkgtest\src\fmt (from $GOPATH)

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


2、import中的是路径还是包名

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

本次实验目录结构:

GOPATH=E:\project\go\pkgtest

E:\project\go\pkgtest>tree /F
文件夹 PATH 列表
卷序列号为 0001-5C61
E:.
└─src
    ├─app2
    │      main.go
    │
    └─libproj2
        └─foo
                foo1.go

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

//foo1.go

package bar


import "fmt"


func Bar() {
fmt.Println("bar.Bar()")
}


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

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

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

//main.go

package main


import "libproj2/bar"


func main() {
bar.Bar()
}


编译app2:

C:\Users\Administrator>go build app2
E:\project\go\pkgtest\src\app2\main.go:3:8: cannot find package "libproj2/bar" in any of:
        C:\GO\src\libproj2\bar (from $GOROOT)
        E:\project\go\pkgtest\src\libproj2\bar (from $GOPATH)


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

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

//main.go

package main


import "libproj2/foo"


func main() {
bar.Bar()
}

再编译执行:

C:\Users\Administrator>go build app2


C:\Users\Administrator>app2
bar.Bar()


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

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

import "libproj1/foo"  //这里的foo指的是路径

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

再类比一下标准库包fmt。

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

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


3、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:m指代的是lib/math路径下唯一的那个包

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

GOPATH=E:\project\go\pkgtest

E:\project\go\pkgtest>tree /F
文件夹 PATH 列表
卷序列号为 0001-5C61
E:.
└─src
    └─test
            hello.go
            world.go

//hello.go

package hello


import "fmt"


func Hello() {
fmt.Println("hello.Hello()")
}

//world.go

package world


import "fmt"


func World() {
fmt.Println("world.World()")
}

从上面的代码可以看出,当前test文件夹有两个包文件,分别是hello和world,下面进行编译:

E:\project\go\pkgtest>go build test
can't load package: package test: found packages hello (hello.go) and world (world.go) in E:\project\go\pkgtest\src\test


E:\project\go\pkgtest>

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


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

GOPATH=E:\project\go\pkgtest

E:\project\go\pkgtest>tree /F
文件夹 PATH 列表
卷序列号为 0001-5C61
E:.
└─src
    ├─main
    │      main.go
    │
    └─test
            hello.go

//hello.go

package hello


import "fmt"


func Hello() {
fmt.Println("hello.Hello()")
}

//main.go

package main


import m "test"


func main() {
m.Hello()
}

下面进行编译main包:

E:\project\go\pkgtest>go build main


E:\project\go\pkgtest>main
hello.Hello()

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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

历史五千年

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值