1. 程式人生 > >Go包匯入與Java的差別

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

© 2016, bigwhite. 版權所有.

Related posts: