golang: 類型轉換和類型斷言

類型轉換在程序設計中都是不可避免的問題。固然有一些語言將這個過程給模糊了,大多數時候開發者並不須要去關注這方面的問題。可是golang中的類型匹配是很嚴格的,不一樣的類型之間一般須要手動轉換,編譯器不會代你去作這個事。我之因此說一般須要手動轉換,是由於interface類型做爲一個特例,會有不一樣的處理方式。
html

golang中的全部類型都有本身的默認值,對此我作了個測試。java

$GOPATH/srcgolang

----typeassert_testshell

--------main.go數組

main.go的代碼以下:安全

package main

import (
	"fmt"
)

type myStruct struct {
	name   bool
	userid int64
}

var structZero myStruct
var intZero int
var int32Zero int32
var int64Zero int64
var uintZero uint
var uint8Zero uint8
var uint32Zero uint32
var uint64Zero uint64
var byteZero byte
var boolZero bool
var float32Zero float32
var float64Zero float64
var stringZero string
var funcZero func(int) int
var byteArrayZero [5]byte
var boolArrayZero [5]bool
var byteSliceZero []byte
var boolSliceZero []bool
var mapZero map[string]bool
var interfaceZero interface{}
var chanZero chan int
var pointerZero *int

func 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 main

import (
	"fmt"
)

func main() {
	var myInt int32     = 5
	var myFloat float64 = 0
	fmt.Println(myInt)
	fmt.Println(myFloat)
}

對於myInt變量,它存儲的就是int32類型的5;對於myFloat變量,它存儲的是int64類型的0。或許您可能會寫出這樣的代碼,但確實不是必須這麼作的:ui

package main

import (
	"fmt"
)

func main() {
	var myInt int32     = int32(5)
	var myFloat float64 = float64(0)
	fmt.Println(myInt)
	fmt.Println(myFloat)
}

在C中,大多數類型轉換都是能夠隱式進行的,好比:
this

#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 main

import (
	"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 main

import (
	"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 main

import (
	"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_test
uid=0x78, gid=0x12345678

若是您的輸出結果是:

uid=0x12, gid=0x12345678

那麼請不要驚訝,由於您的機器是屬於大端序存儲。

其實很容易根據上面所說的知識來判斷是屬於大端序或小端序:

package main

import (
	"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
小端序

接口的轉換遵循如下規則:

  1. 普通類型向接口類型的轉換是隱式的。

  2. 接口類型向普通類型轉換須要類型斷言。

普通類型向接口類型轉換的例子隨處可見,例如:

package main

import (
	"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 main

import (
	"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 = msg
	return 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 = msg
	return 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 main

import (
	"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 main

import (
	"fmt"
)

func main() {
	var val interface{} = "good"
	fmt.Println(val.(string))
	// fmt.Println(val.(int))
}

以上的代碼中被註釋的那一行會運行時錯誤。這是由於val實際存儲的是string類型,所以斷言失敗。

還有一種轉換方式是switch測試。既然稱之爲switch測試,也就是說這種轉換方式只能出如今switch語句中。能夠很輕鬆的將剛纔用Comma-ok斷言的例子換成由switch測試來實現:

package main

import (
	"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

相關文章
相關標籤/搜索