标签 import 下的文章

Go包导入与Java的差别

闲暇时翻阅了近期下载到的电子书《Go in Practice》 ,看到1.2.4 Package Management一节中的代码Demo,感觉作者对Go package导入的说法似乎不够精确:“Packages are imported by their name”(后续的说明将解释不精确的原因)。联想到前几天遇到的一个Java包导入的问题,让我隐约地感觉Java程序员很容易将两种语言的Package import机制搞混淆,于是打算在这里将Golang和Java的Package import机制做一个对比,对于Java转型到Golang的程序员将大有裨益:)。这里的重点在于与Java的对比,关于Golang的Package Import的细节可以参考我之前写过的一篇文章《理解Golang包导入》

我们先来看两个功能等价的代码。

//TestDate.java
import java.util.*;
import java.text.DateFormat;

public class TestDate {
        public static void main(String []args){
                Date d = new Date();
                String s = DateFormat.getDateInstance().format(d);
                System.out.println(s);
        }
}

//testdate.go
package main

import (
    "fmt"
    "time"
)

func main() {
    t := time.Now()
    fmt.Println(t.Format("2006-01-02"))
}

两个程序在Run时,都输出下面内容:

2016-9-13

我们看到Golang和Java都是用import关键字来进行包导入的:

import java.util.Date;

Date d = new Date();

vs.

import "time"

t := time.Now()

咋看起来,Java在package import后似乎使用起来更Easy,使用包内的类和方法时,前面无需再附着Package name,即Date d,而不是java.util.Date d。而Go在导入”time”后,引用包中方法时依然要附着着包名,比如time.Now()。但实质上两种语言在import package的机制上是有很大不同的。

1、机制

虽然都使用import,但import关键字后面的字符串所代表的含义有不同。

Java import导入的是类而不是包,import后面的字符串表示的是按需导入Java Package下面的类,比如import java.util.*; 或导入Package下某个类,比如import java.util.Date。而Go import关键字后面的字符串是包名吗?很多初学者会认为这个就是Go包名,实则不然,Go import后面的字符串实际上是一个包导入路径,这也是Java用”xxx.yyy.zzz”形式而Golang使用”xxx/yyy/zzz”形式的原因。我们用个简单的例子就能证明这一点。我们知道Golang会在\$GOROOT/src + \$GOPATH/src下面导入xxx/yyy/zzz路径下的包,我们在import “fmt”时,实际上导入的是\$GOROOT/src/fmt目录下的包,只是恰好这个下面的包的名字是fmt罢了。如果我们将\$GOROOT/src/fmt目录改名为fmt1,结果会是如何呢?

$go build helloworld.go
helloworld.go:3:8: cannot find package "fmt" in any of:
           /Users/tony/.bin/go17/src/fmt (from $GOROOT)
           /Users/tony/Test/GoToolsProjects/src/fmt (from $GOPATH)

helloworld.go是一个helloworld go源码。

之所以出错是因为在\$GOROOT/src下已经没有fmt这个目录了,所以下面代码中的两个fmt含义是不同的(这也解释了Go in practice中关于包导入的说法的不精确的原因):

package main

import "fmt"  ---- 这里的fmt指的是$GOROOT/src下的名为"fmt"的目录名

func main() {
    fmt.Println("Hello, World") --- 这里的fmt是真正的包名"fmt"
}

从上面我们可以看出Go的包名和包的源文件所在的路径的名字并没有必须一致的要求,这也是为什么在Go源码使用包时一定是用packagename.XX形式,而不是packagename.subpackagename.XX的形式了。比如导入”net/http”后,我们在源码中使用的是http.xxx,而不是net.http.xxx,因为net/http只是一个路径,并不是一个嵌套的包名。

之所以看起来导入路径的终段目录名与包名一致,只是因为这是Go官方的建议:Go的导入路径的最后一段目录名(xxx/yyy/zzz中的zzz)与该目录(zzz)下面源文件中的Go Package名字相同。

下面是一个非标准库的包名与导入路径终段名完全不一致的例子:

//github.com/pkgtest/pkg1/foo.go
package foo

import "fmt"

func Foo() {
    fmt.Println("Foo in pkg1")
}

//testfoo.go
package main

import (
    "github.com/pkgtest/pkg1"
)

func main() {
    foo.Foo() //输出:Foo in pkg1
}

可以看出testfoo.go导入的是”github.com/pkgtest/pkg1″这个路径,但这个路径下的包名却是foo。

Java语言中的包实际以.jar为单位,.jar内部实际上也是以路径组织.class文件的,比如:foo.jar这个jar包中有一个package名为:com.tonybai.foo,foo包中包含类Foo、Bar,那实际上foo.jar内部的目录格式将是:

foo.jar
    - com/
        - tonybai/
            - foo/
                - Foo.class
                - Bar.class

但对于Java包的使用者,这些都是透明的。

2、重名

Java中关于包导入(实则是类导入)唯一的约束就是不能有两个类导入后的full name相同,如果存在两个导入类的full name完全相同,Javac在resolve时,要以ClassPath路径的先后顺序为准了,选择最先遇到的那个类。但是在Go中,如果导入的两个路径下的包名相同,那么Go compiler显然是不能允许这种情况的存在的,会给出Error信息。

比如我们在GOPATH下的github.com/pkgtest/pkg1和github.com/pkgtest/pkg2下放置了同名包foo,下面代码将会报错:

package main

import (
    "github.com/pkgtest/pkg1"
    "github.com/pkgtest/pkg2"
)

func main() {
    foo.Foo()
}

错误信息如下:

$go run testfoo.go
# command-line-arguments
./testdate.go:8: foo redeclared as imported package name
           previous declaration at ./testfoo.go:7

解决这一问题的方法就是采用package alias:

package main

import (
    a "github.com/pkgtest/pkg1"
    b "github.com/pkgtest/pkg2"
)

func main() {
    a.Foo()
    b.Foo()
}

编译执行上面程序将得到下面结果,而不是Error:

Foo of foo package in pkg1
Foo in foo package in pkg2

godep支持Go 1.5 vendor

Go 1.5 vendor/实验特性出炉后,市面上的go第三方包依赖和管理工具显然都无法与之兼容,除了修改代码,别无它法。市场占有率最大的godep做出了表 率,目前其最新版本(go get github.com/tools/godep)已经初步支持了这一实验特性,即在GO15VENDOREXPERIMENT=1时,将使用vendor 目录(而不是Godeps目录)存放copy的第三方包,并在godep go build时不再rewrite GOPATH就可以实现利用vendor下第三方包的构建。下面我们就用例子来验证一下Godep对vendor的支持。

一、升级godep到最新版本

如果要用到go 1.5 vendor,那么godep要升级(go get -u github.com/tools/godep;go build github.com/tools/godep)到当前的最新版本“commit d8799f112f6c8dfe1e56142831bc3bb5c8796a0e”。最新版本兼容老版本的功能,同时提供对go 1.5 vendor支持,两者之间转换的开关就是环境变量:GO15VENDOREXPERIMENT

GO15VENDOREXPERIMENT没有被set时,godep沿用以前的方式;当GO15VENDOREXPERIMENT = 1时,godep将用vendor替代Godeps目录以存放第三方包,同时go save将无法使用-r命令行选项(-r选项用于重写源码中的import path):

$ godep save -r

godep: flag -r is incompatible with the vendoring experiment

二、例子

下面是一个godep的例子(go 1.5 beta3),例子的目录结构如下:

$(GOPATH)/src/tonybai.com/

     ├── app

     │   └── main.go

     └── foolib

         └── foolib.go

//foolib.go

package foo

import "fmt"

func Hello() {

    fmt.Println("Hello from foolib")

}

//main.go

package main

import "tonybai.com/foolib"

func main() {

    foo.Hello()

}

如果GO15VENDOREXPERIMENT没有被set时,godep的各种命令将按之前的方式执行。

$ godep save

$ godep go build

$(GOPATH)/src/tonybai.com/

├── Godeps

│   ├── Godeps.json

│   ├── Readme

│   └── _workspace

│       └── src

│           └── tonybai.com

│               └── foolib

│                   └── foolib.go

├── app*

└── main.go

$./app

Hello from foolib

godep将第三方包放在Godeps/_workspace/src下面。godep go build会rewrite GOPATH以实现使用_workspace下面的第三方包来构建的目的。

如果GO15VENDOREXPERIMENT = 1,那么godep会按照新的方式执行各种命令:

$ godep save

$ godep go build

$(GOPATH)/src/tonybai.com/

├── Godeps

│   ├── Godeps.json

│   └── Readme

├── app*

├── main.go

└── vendor

    └── tonybai.com

        └── foolib

            └── foolib.go

可以看出godep建立vendor目录来存放第三方包,Godeps目录依然保留,但只是存放Godeps.json,以保存些第三方包的meta信息:

//Godeps.json

{

    "ImportPath": "tonybai.com/app",

    "GoVersion": "go1.5beta3",

    "Deps": [

        {

            "ImportPath": "tonybai.com/foolib",

            "Rev": "7f2f94dc589ba9e053ef13b3b01fa327c27bf161"

        }

    ]

}

三、迁移

由于godep前后的两种工作模式并不兼容,因此大量存量的使用godep的repo,如果想使用Go 1.5 vendor,那么在升级到Go 1.5之后需要做一些迁移工作。godep没有提供自动的迁移工具,目前只能手动迁移,godep github主页上给出了手动迁移的命令步骤:

$ unset GO15VENDOREXPERIMENT

$ godep restore

//如果之前使用了godep save -r,那么下面这行命令将自动undo rewritten import。

$ godep save ./…

$ rm -rf Godeps

$ export GO15VENDOREXPERIMENT=1

$ godep save ./…

# You should see your Godeps/_workspace/src files "moved" to vendor/.

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats