1. 程式人生 > >Go 1.9 新特性 Type Alias 詳解

Go 1.9 新特性 Type Alias 詳解

北京時間2017.08.25,Go1.9正式版釋出了。Go1.9經歷了2個beta,好幾個月,終於定了,釋出了正式版本。Go 1.9包含了很多改變,比如類型別名Type Alias,安全併發Map,並行編譯等,都是很大的改變,今天這篇文章主要介紹類型別名 Type Alias。

安裝go 1.9

很多眾所周知的原因,大家可能無法下載最新的go 1.9 sdk,如果你沒有梯子,可以到我自建的這個映象網站下載,有很多常用的開發軟體,其中就包含最新的go 1.9。映象地址:http://mirrors.flysnow.org/

作用

type alias這個特性的主要目的是用於已經定義的型別,在package之間的移動時的相容。比如我們有一個匯出的型別flysnow.org/lib/T1

,現在要遷移到另外一個package中, 比如flysnow.org/lib2/T1中。

沒有type alias的時候我們這麼做,就會導致其他第三方引用舊的package路徑的程式碼,都要統一修改,不然無法使用。

有了type alias就不一樣了,型別T1的實現我們可以遷移到lib2下,同時我們在原來的lib下定義一個lib2下T1的別名,這樣第三方的引用就可以不用修改,也可以正常使用,只需要相容一段時間,再徹底的去掉舊的package裡的型別相容,這樣就可以漸進式的重構我們的程式碼,而不是一刀切。

//package:flysnow.org/lib
type T1=lib2.T1

type alias vs defintion

我們基於一個型別建立一個新型別,稱之為defintion;基於一個型別建立一個別名,稱之為alias,這就是他們最大的區別。

type MyInt1 int
type MyInt2 = int

第一行程式碼是基於基本型別int建立了新型別MyInt1,第二行是建立的一個int的類型別名MyInt2,注意類型別名的定義是=

var i int =0
var i1 MyInt1 = i //error
var i2 MyInt2 = i
fmt.Println(i1,i2)

仔細看這個示例,第二行把一個int型別的變數i,賦值給MyInt1型別的變數i1會被提示編譯錯誤:型別無法轉換。但是第三行把int

型別的變數i,賦值給MyInt2型別的變數i2就可以,不會提示錯誤。

從這個例子也可以看出來,這兩種定義方式的不同,因為Go是強型別語言,所以型別之間的轉換必須強制轉換,因為intMyInt1是不同的型別,所以這裡會報編譯錯誤。

但是因為MyInt2只是int的一個別名,本質上還是一個int型別,所以可以直接賦值,不會有問題。

型別方法

每個型別都可以通過接受者的方式,新增屬於它自己的方法,我們看下通過type alias的型別是否可以,以及擁有哪些方法。

type MyInt1 int
type MyInt2 = int
func (i MyInt1) m1(){
    fmt.Println("MyInt1.m1")
}
func (i MyInt2) m2(){
    fmt.Println("MyInt2.m2")
}
func main() {
    var i1 MyInt1
    var i2 MyInt2
    i1.m1()
    i2.m2()
}

以上示例程式碼看著是沒有任何問題,但是我們編譯的時候會提示:

i2.m2 undefined (type int has no field or method m2)
cannot define new methods on non-local type int

這裡面有2個錯誤,一個是提示型別int沒有m2這個方法,所以我們不能呼叫,因為MyInt2本質上就是int

第二個錯誤是我們不能為int型別新增新方法,什麼意思呢?因為int是一個非本地型別,所以我們不能為其增加方法。既然這樣,那我們自定義個struct型別試試。

type User struct {
}
type MyUser1 User
type MyUser2 = User
func (i MyUser1) m1(){
    fmt.Println("MyUser1.m1")
}
func (i MyUser2) m2(){
    fmt.Println("MyUser2.m2")
}
//Blog:www.flysnow.org
//Wechat:flysnow_org
func main() {
    var i1 MyUser1
    var i2 MyUser2
    i1.m1()
    i2.m2()
}

換成struct,正常執行。所以本地定義的型別的別名,還是可以為其新增方法的。現在我們接著上面的例子,看一個有趣的現象,我在main函式裡增加如下程式碼:

var i User
    i.m2()

然後執行,發現,可以正常執行。是不是很奇怪,我們並沒有為型別User 定義方法啊,怎麼可以呼叫呢?這就得益於type alias,MyUser2完全等價於User,所以為MyUser2定義方法,等於就為User定義了方法,反之,亦然。

但是對於新定義的型別MyUser1就不行了,因為它完全是個新型別,所以User的方法,MyUser是沒有的。這裡不再舉例,大家自己可以試試。

還有一點需要注意,因為MyUser2完全等價於User,所以User已經有的方法,MyUser2不能再宣告,反之亦然,如果定義了會有如下提示:

./main.go:37:6: User.m1 redeclared in this block
    previous declaration at ./main.go:31:6

其實就是重複宣告的意思,不能再次重複聲明瞭。

介面實現

上面的小結我們可以發現,UserMyUser2是等價的,並且其中一個新增了方法,另外一個也會有。那麼基於此推匯出,一個實現了某個介面,另外一個也會實現。現在驗證一下:

type I interface {
    m2()
}
type User struct {
}
type MyUser2 = User
func (i User) m(){
    fmt.Println("User.m")
}
func (i MyUser2) m2(){
    fmt.Println("MyUser2.m2")
}
func main() {
    var u User
    var u2 MyUser2
    var i1 I =u
    var i2 I =u2
    fmt.Println(i1,i2)
}

定義了一個介面I,從程式碼上看,只有MyUser2實現了它,但是我們程式碼的演示中,發現User也實現了介面I,所以這就驗證了我們的推到是正確的,返回來如果User實現了某個介面,那麼它的type alias也同樣會實現這個介面。

以上講了很多示例都是型別struct的別名,我們看下介面interface的type alias是否也是等價的。

type I interface {
    m()
}
type MyI1 I
type MyI2 = I
type MyInt int
func (i MyInt) m(){
    fmt.Println("MyInt.m")
}

定義了一個介面IMyI1是基於I的新型別;MyI2I的類型別名;MyInt實現了介面I。下面進行測試。

//Blog:www.flysnow.org
//Wechat:flysnow_org
func main() {
    //賦值實現型別MyInt
    var i I = MyInt(0)
    var i1 MyI1 = MyInt(0)
    var i2 MyI2 = MyInt(0)
    //介面間相互賦值
    i = i1
    i = i2
    i1 = i2
    i1 = i
    i2 = i
    i2 = i1
}

以上程式碼執行是正常的,這個是前面講的具體型別(struct,int等)的type alias不一樣,只要實現了介面,就可以相互賦值,管你是新定義的介面MyI1,還是介面的別名MyI2

型別的巢狀

我們都知道type alias的兩個型別是等價的,但是他們在型別巢狀時有些不一樣。

//Blog:www.flysnow.org
//Wechat:flysnow_org
func main() {
    my:=MyStruct{}
    my.T2.m1()
}
type T1 struct {
}
func (t T1) m1(){
    fmt.Println("T1.m1")
}
type T2 = T1
type MyStruct struct {
    T2
}

示例中T2T1的別名,但是我們把T2巢狀在MyStruct中,在呼叫的時候只能通過T2這個名稱呼叫,而不能通過T1,會提示沒這個欄位的。反過來也一樣。

這是因為T1,T2是兩個名稱,雖然他們等價,但他們是具有兩個不同名字的等價型別,所以在型別巢狀的時候,就是兩個欄位。

當然我們可以把T1,T2同時嵌入到MyStrut中,進行分別呼叫。

//Blog:www.flysnow.org
//Wechat:flysnow_org
func main() {
    my:=MyStruct{}
    my.T2.m1()
    my.T1.m1()
}
type MyStruct struct {
    T2
    T1
}

以上也是可以正常執行的,證明這是具有兩個不同名字的,同種型別的欄位。

下面我們做個有趣的實驗,把main方法的程式碼改為如下:

//Blog:www.flysnow.org
//Wechat:flysnow_org
func main() {
    my:=MyStruct{}
    my.m1()
}

猜猜是不是可以正常編譯執行呢?答應可能出乎意料,是不能正常編譯的,提示如下:

./main.go:25:4: ambiguous selector my.m1

其實想想很簡單,不知道該呼叫哪個,太模糊了,匹配不了,不然該用T1m1,還是T2m1。這種結果不限於方法,欄位也也一樣;也不限於type alias,type defintion也是一樣的,只要有重複的方法、欄位,就會有這種提示,因為不知道該選擇哪個。

型別迴圈

type alias的宣告,一定要留意型別迴圈,不要產生了迴圈,一旦產生,就會編譯不通過,那麼什麼是型別迴圈呢。假如type T2 = T1,那麼T1絕對不能直接、或者間接的引用到T2,一旦有,就會型別迴圈。

type T2 = *T2
type T2 = MyStruct
type MyStruct struct {
    T1
    T2
}

以上兩種定義都是型別迴圈,我們自己在使用的過程中,要避免這種定義的出現。

byte and rune

這兩個型別一個是int8的別名,一個是int32的別名,在Go 1.9之前,他們是這麼定義的。

type byte byte

type rune rune

現在Go 1.9有了type alias這個新特性後,他們的定義就變成如下了:

type byte = uint8

type rune = int32

恩,非常很省事和簡潔。

匯出未匯出的型別

type alias還有一個功能,可以匯出一個未被匯出的型別。

package lib
//Blog:www.flysnow.org
//Wechat:flysnow_org
type user struct {
    name string
    Email string
}
func (u user) getName() string {
    return u.name
}
func (u user) GetEmail() string {
    return u.Email
}
//把這個user匯出為User
type User = user

user本身是一個未匯出的型別,不能被其他package訪問,但是我們可以通過type User = user,定義一個User,這樣這個User就可以被其他package訪問了,可以使用user型別匯出的欄位和方法,示例中是Email欄位和GetEmail方法,另外未被匯出name欄位和getName方法是不能被其他package使用的。

小結

type alias的定義,本質上是一樣的型別,只是起了一個別名,源型別怎麼用,別名型別也怎麼用,保留源型別的所有方法、欄位等。

轉自:http://www.infoq.com/cn/news/2017/08/go-1-9-Type-Alias