golang: 型別轉換和型別斷言
型別轉換在程式設計中都是不可避免的問題。當然有一些語言將這個過程給模糊了,大多數時候開發者並不需要去關注這方面的問題。但是golang中的型別匹配是很嚴格的,不同的型別之間通常需要手動轉換,編譯器不會代你去做這個事。我之所以說通常需要手動轉換,是因為interface型別作為一個特例,會有不同的處理方式。
golang中的所有型別都有自己的預設值,對此我做了個測試。
$GOPATH/src
----typeassert_test
--------main.go
main.go的程式碼如下:
package mainimport ( "fmt") type myStruct struct { namebool userid int64 }var structZero myStructvar intZero intvar int32Zero int32var int64Zero int64var uintZero uintvar uint8Zero uint8var uint32Zero uint32var uint64Zero uint64var byteZero bytevar boolZero boolvar float32Zero float32var float64Zero float64var stringZero stringvar funcZero func(int) intvar byteArrayZero [5]bytevar boolArrayZero [5]boolvar byteSliceZero []bytevar boolSliceZero []boolvar mapZero map[string]boolvar interfaceZero interface{}var chanZero chan intvar pointerZero *intfunc main() { fmt.Println("structZero: ", structZero) fmt.Println("intZero: ", intZero) fmt.Println("int32Zero: ", int32Zero) fmt.Println("int64Zero: ", int64Zero) fmt.Println("uintZero: ", uintZero) fmt.Println("uint8Zero: ", uint8Zero) fmt.Println("uint32Zero: ", uint32Zero) fmt.Println("uint64Zero: ", uint64Zero) fmt.Println("byteZero: ", byteZero) fmt.Println("boolZero: ", boolZero) fmt.Println("float32Zero: ", float32Zero) fmt.Println("float64Zero: ", float64Zero) fmt.Println("stringZero: ", stringZero) fmt.Println("funcZero: ", funcZero) fmt.Println("funcZero == nil?", funcZero == nil) fmt.Println("byteArrayZero: ", byteArrayZero) fmt.Println("boolArrayZero: ", boolArrayZero) fmt.Println("byteSliceZero: ", byteSliceZero) fmt.Println("byteSliceZero's len?", len(byteSliceZero)) fmt.Println("byteSliceZero's cap?", cap(byteSliceZero)) fmt.Println("byteSliceZero == nil?", byteSliceZero == nil) fmt.Println("boolSliceZero: ", boolSliceZero) fmt.Println("mapZero: ", mapZero) fmt.Println("mapZero's len?", len(mapZero)) fmt.Println("mapZero == nil?", mapZero == nil) fmt.Println("interfaceZero: ", interfaceZero) fmt.Println("interfaceZero == nil?", interfaceZero == nil) fmt.Println("chanZero: ", chanZero) fmt.Println("chanZero == nil?", chanZero == nil) fmt.Println("pointerZero: ", pointerZero) fmt.Println("pointerZero == nil?", pointerZero == nil) }
$ cd $GOPATH/src/typeassert_test$ go build$ ./typeassert_test
您可以清楚的瞭解到各種型別的預設值。如bool的預設值是false,string的預設值是空串,byte的預設值是0,陣列的預設就是這個陣列成員型別的預設值所組成的陣列等等。然而您或許會發現在上面的例子中:map、interface、pointer、slice、func、chan的預設值和nil是相等的。關於nil可以和什麼樣的型別做相等比較,您只需要知道nil可以賦值給哪些型別變數,那麼就可以和哪些型別變數做相等比較。官方對此有明確的說明: ofollow,noindex">http://pkg.golang.org/pkg/builtin/#Type ,也可以看我的另一篇文章: golang: 詳解interface和nil 。所以現在您應該知道nil只能賦值給指標、channel、func、interface、map或slice型別的變數。如果您用int型別的變數跟nil做相等比較,panic會找上您。
對於字面量的值,編譯器會有一個隱式轉換。看下面的例子:
package mainimport ("fmt") func main() {var myInt int32= 5 var myFloat float64 = 0 fmt.Println(myInt) fmt.Println(myFloat) }
對於myInt變數,它儲存的就是int32型別的5;對於myFloat變數,它儲存的是int64型別的0。或許您可能會寫出這樣的程式碼,但確實不是必須這麼做的:
package mainimport ("fmt") func main() {var myInt int32= int32(5)var myFloat float64 = float64(0) fmt.Println(myInt) fmt.Println(myFloat) }
在C中,大多數型別轉換都是可以隱式進行的,比如:
#include <stdio.h>int main(int argc, char **argv){ int uid= 12345; long gid = uid; printf("uid=%d, gid=%d\n", uid, gid); return 0; }
但是在golang中,您不能這麼做。有個類似的例子:
package mainimport ("fmt") func main() {var uid int32 = 12345 var gid int64 = int64(uid) fmt.Printf("uid=%d, gid=%d\n", uid, gid) }
很顯然,將uid賦值給gid之前,需要將uid強制轉換成int64型別,否則會panic。golang中的型別區分靜態型別和底層型別。您可以用type關鍵字定義自己的型別,這樣做的好處是可以語義化自己的程式碼,方便理解和閱讀。
package mainimport ("fmt") type MyInt32 int32 func main() {var uid int32= 12345 var gid MyInt32 = MyInt32(uid) fmt.Printf("uid=%d, gid=%d\n", uid, gid) }
在上面的程式碼中,定義了一個新的型別MyInt32。對於型別MyInt32來說,MyInt32是它的靜態型別,int32是它的底層型別。即使兩個型別的底層型別相同,在相互賦值時還是需要強制型別轉換的。可以用reflect包中的Kind方法來獲取相應型別的底層型別。
對於型別轉換的截斷問題,為了問題的簡單化,這裡只考慮具有相同底層型別之間的型別轉換。小型別(這裡指儲存空間)向大型別轉換時,通常都是安全的。下面是一個大型別向小型別轉換的示例:
package mainimport ("fmt") func main() {var gid int32 = 0x12345678 var uid int8= int8(gid) fmt.Printf("uid=%#x, gid=%#x\n", uid, gid) }
在上面的程式碼中,gid為int32型別,也即佔4個位元組空間(在記憶體中佔有4個儲存單元),因此這4個儲存單元的值分別是:0x12, 0x34, 0x56, 0x78。但事實不總是如此,這跟cpu架構有關。在記憶體中的儲存方式分為兩種:大端序和小端序。大端序的儲存方式是高位位元組儲存在低地址上;小端序的儲存方式是高位位元組儲存在高地址上。本人的機器是按小端序來儲存的,所以gid在我的記憶體上的儲存序列是這樣的:0x78, 0x56, 0x34, 0x12。如果您的機器是按大端序來儲存,則gid的儲存序列剛好反過來:0x12, 0x34, 0x56, 0x78。對於強制轉換後的uid,肯定是產生了截斷行為。因為uid只佔1個位元組,轉換後的結果必然會丟棄掉多餘的3個位元組。截斷的規則是:保留低地址上的資料,丟棄多餘的高地址上的資料。來看下測試結果:
$ cd $GOPATH/src/typeassert_test$ go build$ ./typeassert_testuid=0x78, gid=0x12345678
如果您的輸出結果是:
uid=0x12, gid=0x12345678
那麼請不要驚訝,因為您的機器是屬於大端序儲存。
其實很容易根據上面所說的知識來判斷是屬於大端序或小端序:
package mainimport ( "fmt") func IsBigEndian() bool {var i int32 = 0x12345678 var b byte= byte(i)if b == 0x12 {return true }return false}func main() {if IsBigEndian() { fmt.Println("大端序") } else { fmt.Println("小端序") } }
$ cd $GOPATH/src/typeassert_test$ go build$ ./typeassert_test小端序
介面的轉換遵循以下規則:
-
普通型別向介面型別的轉換是隱式的。
-
介面型別向普通型別轉換需要型別斷言。
普通型別向介面型別轉換的例子隨處可見,例如:
package mainimport ("fmt")func main() { var val interface{} = "hello" fmt.Println(val) val = []byte{'a', 'b', 'c'} fmt.Println(val) }
正如您所預料的,"hello"作為string型別儲存在interface{}型別的變數val中,[]byte{'a', 'b', 'c'}作為slice儲存在interface{}型別的變數val中。這個過程是隱式的,是編譯期確定的。
介面型別向普通型別轉換有兩種方式:Comma-ok斷言和switch測試。任何實現了介面I的型別都可以賦值給這個介面型別變數。由於interface{}包含了0個方法,所以任何型別都實現了interface{}介面,這就是為什麼可以將任意型別值賦值給interface{}型別的變數,包括nil。還有一個要注意的就是介面的實現問題,*T包含了定義在T和*T上的所有方法,而T只包含定義在T上的方法。我們來看一個例子:
package mainimport ( "fmt")// 演講者介面type Speaker interface {// 說 Say(string)// 聽 Listen(string) string // 打斷、插嘴 Interrupt(string) }// 王蘭講師type WangLan struct { msg string} func (this *WangLan) Say(msg string) { fmt.Printf("王蘭說:%s\n", msg) } func (this *WangLan) Listen(msg string) string {this.msg = msgreturn msg } func (this *WangLan) Interrupt(msg string) {this.Say(msg) }// 江婁講師type JiangLou struct { msg string} func (this *JiangLou) Say(msg string) { fmt.Printf("江婁說:%s\n", msg) } func (this *JiangLou) Listen(msg string) string {this.msg = msgreturn msg } func (this *JiangLou) Interrupt(msg string) {this.Say(msg) }func main() { wl := &WangLan{} jl := &JiangLou{}var person Speaker person = wl person.Say("Hello World!") person = jl person.Say("Good Luck!") }
Speaker介面有兩個實現WangLan型別和JiangLou型別。但是具體到例項來說,變數wl和變數jl只有是對應例項的指標型別才真正能被Speaker介面變數所持有。這是因為WangLan型別和JiangLou型別所有對Speaker介面的實現都是在*T上。這就是上例中person能夠持有wl和jl的原因。
想象一下java的泛型(很可惜golang不支援泛型),java在支援泛型之前需要手動裝箱和拆箱。由於golang能將不同的型別存入到介面型別的變數中,使得問題變得更加複雜。所以有時候我們不得不面臨這樣一個問題:我們究竟往介面存入的是什麼樣的型別?有沒有辦法反向查詢?答案是肯定的。
Comma-ok斷言的語法是:value, ok := element.(T)。element必須是介面型別的變數,T是普通型別。如果斷言失敗,ok為false,否則ok為true並且value為變數的值。來看個例子:
package mainimport ( "fmt") type Html []interface{}func main() { html := make(Html, 5) html[0] = "div" html[1] = "span" html[2] = []byte("script") html[3] = "style" html[4] = "head" for index, element := range html {if value, ok := element.(string); ok { fmt.Printf("html[%d] is a string and its value is %s\n", index, value) } else if value, ok := element.([]byte); ok { fmt.Printf("html[%d] is a []byte and its value is %s\n", index, string(value)) } } }
其實Comma-ok斷言還支援另一種簡化使用的方式:value := element.(T)。但這種方式不建議使用,因為一旦element.(T)斷言失敗,則會產生執行時錯誤。如:
package mainimport ( "fmt") func main() {var val interface{} = "good" fmt.Println(val.(string))// fmt.Println(val.(int))}
以上的程式碼中被註釋的那一行會執行時錯誤。這是因為val實際儲存的是string型別,因此斷言失敗。
還有一種轉換方式是switch測試。既然稱之為switch測試,也就是說這種轉換方式只能出現在switch語句中。可以很輕鬆的將剛才用Comma-ok斷言的例子換成由switch測試來實現:
package mainimport ( "fmt" )type Html []interface{}func main() {html := make(Html, 5) html[0] = "div" html[1] = "span" html[2] = []byte("script") html[3] = "style" html[4] = "head" for index, element := range html { switch value := element.(type) { case string: fmt.Printf("html[%d] is a string and its value is %s\n", index, value) case []byte: fmt.Printf("html[%d] is a []byte and its value is %s\n", index, string(value)) case int: fmt.Printf("invalid type\n") default: fmt.Printf("unknown type\n") } } }
$ cd $GOPATH/src/typeassert_test$ go build$ ./typeassert_test