Go包导入与Java的差别

alex7742 8年前
   <p>闲暇时翻阅了近期下载到的电子书 <a href="/misc/goto?guid=4959715743795012293" rel="nofollow,noindex">《Go in Practice》</a> ,看到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包导入》。</p>    <p>我们先来看两个功能等价的代码。</p>    <pre>  <code class="language-go">//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);          }  }</code></pre>    <p>和</p>    <pre>  <code class="language-go">//testdate.go  package main    import (      "fmt"      "time"  )    func main() {      t := time.Now()      fmt.Println(t.Format("2006-01-02"))  }</code></pre>    <p>两个程序在Run时,都输出下面内容:</p>    <pre>  <code class="language-go">2016-9-13</code></pre>    <p>我们看到Golang和Java都是用import关键字来进行包导入的:</p>    <pre>  <code class="language-go">import java.util.Date;    Date d = new Date();</code></pre>    <p>vs.</p>    <pre>  <code class="language-go">import "time"    t := time.Now()</code></pre>    <p>咋看起来,Java在package import后似乎使用起来更Easy,使用包内的类和方法时,前面无需再附着Package name,即Date d,而不是java.util.Date d。而Go在导入”time”后,引用包中方法时依然要附着着包名,比如time.Now()。但实质上两种语言在import package的机制上是有很大不同的。</p>    <p><strong>1、机制</strong></p>    <p>虽然都使用import,但import关键字后面的字符串所代表的含义有不同。</p>    <p>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,结果会是如何呢?</p>    <pre>  <code class="language-go">$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源码。</code></pre>    <p>之所以出错是因为在\$GOROOT/src下已经没有fmt这个目录了,所以下面代码中的两个fmt含义是不同的(这也解释了Go in practice中关于包导入的说法的不精确的原因):</p>    <pre>  <code class="language-go">package main    import "fmt"  ---- 这里的fmt指的是$GOROOT/src下的名为"fmt"的目录名    func main() {      fmt.Println("Hello, World") --- 这里的fmt是真正的包名"fmt"  }</code></pre>    <p>从上面我们可以看出Go的包名和包的源文件所在的路径的名字并没有必须一致的要求,这也是为什么在Go源码使用包时一定是用packagename.XX形式,而不是packagename.subpackagename.XX的形式了。比如导入”net/http”后,我们在源码中使用的是http.xxx,而不是net.http.xxx,因为net/http只是一个路径,并不是一个嵌套的包名。</p>    <p>之所以看起来导入路径的终段目录名与包名一致,只是因为这是Go官方的建议:Go的导入路径的最后一段目录名(xxx/yyy/zzz中的zzz)与该目录(zzz)下面源文件中的Go Package名字相同。</p>    <p>下面是一个非标准库的包名与导入路径终段名完全不一致的例子:</p>    <pre>  <code class="language-go">//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  }</code></pre>    <p>可以看出testfoo.go导入的是”github.com/pkgtest/pkg1″这个路径,但这个路径下的包名却是foo。</p>    <p>Java语言中的包实际以.jar为单位,.jar内部实际上也是以路径组织.class文件的,比如:foo.jar这个jar包中有一个package名为:com.tonybai.foo,foo包中包含类Foo、Bar,那实际上foo.jar内部的目录格式将是:</p>    <pre>  <code class="language-go">foo.jar      - com/          - tonybai/              - foo/                  - Foo.class                  - Bar.class</code></pre>    <p>但对于Java包的使用者,这些都是透明的。</p>    <p><strong>2、重名</strong></p>    <p>Java中关于包导入(实则是类导入)唯一的约束就是不能有两个类导入后的full name相同,如果存在两个导入类的full name完全相同,Javac在resolve时,要以ClassPath路径的先后顺序为准了,选择最先遇到的那个类。但是在Go中,如果导入的两个路径下的包名相同,那么Go compiler显然是不能允许这种情况的存在的,会给出Error信息。</p>    <p>比如我们在GOPATH下的github.com/pkgtest/pkg1和github.com/pkgtest/pkg2下放置了同名包foo,下面代码将会报错:</p>    <pre>  <code class="language-go">package main    import (      "github.com/pkgtest/pkg1"      "github.com/pkgtest/pkg2"  )    func main() {      foo.Foo()  }</code></pre>    <p>错误信息如下:</p>    <pre>  <code class="language-go">$go run testfoo.go  # command-line-arguments  ./testdate.go:8: foo redeclared as imported package name             previous declaration at ./testfoo.go:7</code></pre>    <p>解决这一问题的方法就是采用package alias:</p>    <pre>  <code class="language-go">package main    import (      a "github.com/pkgtest/pkg1"      b "github.com/pkgtest/pkg2"  )    func main() {      a.Foo()      b.Foo()  }</code></pre>    <p>编译执行上面程序将得到下面结果,而不是Error:</p>    <pre>  <code class="language-go">Foo of foo package in pkg1  Foo in foo package in pkg2</code></pre>    <p>© 2016,bigwhite. 版权所有.</p>    <p> </p>    <p>来自:http://tonybai.com/2016/09/13/package-import-in-golang-vs-in-java/</p>    <p> </p>