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
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是強型別語言,所以型別之間的轉換必須強制轉換,因為int
和MyInt1
是不同的型別,所以這裡會報編譯錯誤。
但是因為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
其實就是重複宣告的意思,不能再次重複聲明瞭。
介面實現
上面的小結我們可以發現,User
和MyUser2
是等價的,並且其中一個新增了方法,另外一個也會有。那麼基於此推匯出,一個實現了某個介面,另外一個也會實現。現在驗證一下:
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") }
定義了一個介面I
,MyI1
是基於I
的新型別;MyI2
是I
的類型別名;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 }
示例中T2
是T1
的別名,但是我們把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
其實想想很簡單,不知道該呼叫哪個,太模糊了,匹配不了,不然該用T1
的m1
,還是T2
的m1
。這種結果不限於方法,欄位也也一樣;也不限於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