這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
類型轉換在程式設計中都是不可避免的問題。當然有一些語言將這個過程給模糊了,大多數時候開發人員並不需要去關注這方面的問題。但是golang中的類型匹配是很嚴格的,不同的類型之間通常需要手動轉換,編譯器不會代你去做這個事。我之所以說通常需要手動轉換,是因為interface類型作為一個特例,會有不同的處理方式。
golang中的所有類型都有自己的預設值,對此我做了個測試。
$GOPATH/src
----typeassert_test
--------main.go
main.go的代碼如下:
package mainimport ("fmt")type myStruct struct {name booluserid 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可以賦值給哪些類型變數,那麼就可以和哪些類型變數做相等比較。官方對此有明確的說明: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 = 5var myFloat float64 = 0fmt.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 = 12345var 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 int32func main() {var uid int32 = 12345var 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 = 0x12345678var 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 = 0x12345678var 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 Speakerperson = wlperson.Say("Hello World!")person = jlperson.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