在使用 go 這樣的強類型語言時,我們常常會遇到類型轉換的問題。比如 int 類型轉 int64,interface{} 轉 struct ,對一種類型取指針、解指針等等。今天在這篇文章中我們就來梳理一下,我們在 go 的日常使用中常碰到的幾個類型轉換場景。
go存在4種類型轉換分別為:斷言、強制、顯式、隱式。
通常說的類型轉換是指斷言,強制在日常不會使用到、顯示是基本的類型轉換、隱式使用到但是不會注意到。斷言、強制、顯式三類在go語法描述中均有說明,隱式是在日常使用過程中總結出來。
一、Assert 斷言
斷言通過判斷變量是否可以轉換成某一個類型
1、斷言(assert)
語法文檔:https://golang.google.cn/ref/spec#Type_assertions
expression必須是接口類型,且自身類型與Type類型相符。
expression.(Type)的返回值一般為兩個:value和ok,匹配成功ok為true,value有值,匹配失敗ok為false,value無值;也可以直接接受value一個返回值,不過失敗則直接panic:
func main() {
var a interface{} = 100
if aa, ok := a.(int); ok {
fmt.Println(aa)
}
}
一個簡單的斷言表達式:
var s = x.(T)
如果x不是nil,且x可以轉換成T類型,就會斷言成功,返回T類型的變量s。如果T不是接口類型,則要求x的類型就是T,如果T是一個接口,要求x實現了T接口。
如果斷言類型成立,則表達式返回值就是T類型的x,如果斷言失敗就會觸發panic。
上述表所示再斷言失敗就會panic,go提供了另外一種帶返回是否成立的斷言語法:
s, ok := x.(T)
該方法和第一種差不多一樣,但是ok會返回是否斷言成功不會出現panic,ok就表示是否是成功了。
2、類型switch
go語法種還提供了另外一種類型switch的斷言方法。
語法文檔:https://golang.google.cn/ref/spec#Type_switches
x斷言成了type類型,type類型具體值就是switch case的值,如果x成功斷言成了某個case類型,就可以執行那個case,此時i := x.(type)返回的i就是那個類型的變量了,可以直接當作case類型使用。
func main() {
var t interface{} = 100
switch i := t.(type) {
case float32:
fmt.Printf("i的類型%T i的值%v\n", i, i)
case float64:
fmt.Printf("i的類型%T i的值%v\n", i, i)
case int:
fmt.Printf("i的類型%T i的值%v\n", i, i)
case bool:
fmt.Printf("i的類型%T i的值%v\n", i, i)
case string:
fmt.Printf("i的類型%T i的值%v\n", i, i)
default:
fmt.Println("其他類型")
}
}
二、強制類型轉換
強制類型轉換通過修改變量類型
該方法不常見,主要用於unsafe包和接口類型檢測,需要懂得go變量的知識。
1、unsafe
語法文檔:
https://golang.google.cn/ref/spec#Package_unsafe/
https://golang.org/ref/spec#Package_unsafe
本文檔僅大概說明一下,具體研究請求查找相關資料。
var f float64 bits = *(*uint64)(unsafe.Pointer(&f)) type ptr unsafe.Pointer bits = *(*uint64)(ptr(&f)) var p ptr = nil
float64就強制轉換成uint64類型,float的地址就是一個值但是類型是float64,然后創建了一個uint64類型變量,地址值也是float64的地址值,兩個變量值相同類型不同,強制轉換了類型。
unsafe強制轉換是指針的底層操作了,用c的朋友就很熟悉這樣的指針類型轉換,利用內存對齊才能保證轉換可靠,例如int和uint存在符號位差別,利用unsafe轉換后值可能不同,但是在內存存儲二進制一模一樣。
2、接口類型檢測
例如下列代碼:
var _ Context = (*ContextBase)(nil)
nil的類型是nil地址值為0,利用強制類型轉換成了*ContextBase,返回的變量就是類型為*ContextBase地址值為0,然后Context=xx賦值如果xx實現了Context接口就沒事,如果沒有實現在編譯時期就會報錯,實現編譯期間檢測接口是否實現。
三、顯示類型轉換
語法文檔:https://golang.google.cn/ref/spec#Conversions
一個顯式轉換的表達式T(x) ,其中T是一種類型並且x是可轉換為類型的表達式T,例如:uint(666)。
在以下任何一種情況下,變量x都可以轉換成T類型:
- x可以分配成T類型。
- 忽略struct標簽x的類型和T具有相同的基礎類型。
- 忽略struct標記x的類型和T是未定義類型的指針類型,並且它們的指針基類型具有相同的基礎類型。
- x的類型和T都是整數或浮點類型。
- x的類型和T都是復數類型。
- x的類型是整數或[]byte或[]rune,並且T是字符串類型。
- x的類型是字符串,T類型是[]byte或[]rune。
例如下列代碼利用了規則進行轉換,規則實現可以參考reflect.Value.Convert方法邏輯:
int64(222)
[]byte("ssss")
type A int
A(2)
下面是demo
int(time.Now().Weekday()) //星期轉int int(time.Now().Month()) //月份轉int var a float64 a = 3.1 b := int(a) //float64轉int var a int a = 1 b := int64(a) //int轉int64 //這種類型轉換主要在切換同一基礎類型不同精度范圍時使用,比如我們要將 int 型轉為 int64 類型時。
四、隱式類型轉換
隱式類型轉換日常使用並不會感覺到,但是運行中確實出現了類型轉換,以下列出了三種。
1、JSON
Golang中大多數數據類型都可以轉化為有效的JSON文本,除了channel通道、complex復數、func函數等。
Golang指針可進行隱式轉換,表面進行的是指針序列化,內部會針對指針進行取值操作,實際還是針對所指的對象進行序列化。
2、組合間的重新斷言類型
type Reader interface {
Read(p []byte) (n int, err error)
}
type ReadCloser interface {
Reader
Close() error
}
var rc ReaderClose
r := rc
ReaderClose接口組合了Reader接口,但是r=rc的賦值時還是類型轉換了,go使用系統內置的函數執行了類型轉換。以前遇到過類似接口組合類型的變量賦值,然后使用pprof和bench測試發現了這一細節,在接口類型轉移時浪費了一些性能。
3、相同類型間賦值
type Handler func()
func NewHandler() Handler {
return func() {}
}
雖然type定義了Handler類型,但是Handler和func()是兩種實際類型,類型不會相等,使用反射和斷言均會出現兩種類型不同。
兩者類型不同驗證代碼:
package main
import (
"fmt"
"reflect"
)
type Handler func()
func a() Handler {
return func() {}
}
func main() {
var i interface{} = main
_, ok := i.(func())
fmt.Println(ok)
_, ok = i.(Handler)
fmt.Println(ok)
fmt.Println(reflect.TypeOf(main) == reflect.TypeOf((*Handler)(nil)).Elem())
}
// true
// false
// false
