此篇文章將列出Go中所有的類型轉(zhuǎn)換、賦值和值比較規(guī)則。 請注意:在闡述這些規(guī)則的時候,自定義泛型中頻繁使用的類型參數(shù)類型被特意忽略掉了。 也就是說,本文不考慮涉及到自定義泛型的情形。
在Go中,如果一個值v
可以被顯式地轉(zhuǎn)換為類型T
,則此轉(zhuǎn)換可以使用語法形式(T)(v)
來表示。 在大多數(shù)情況下,特別是T
為一個類型名(即一個標識符)時,此形式可簡化為T(v)
。
當我們說一個值x
可以被隱式轉(zhuǎn)換為一個類型T
,這同時也意味著x
可以被顯式轉(zhuǎn)換為類型T
。
如果兩個類型表示著同一個類型,則它們的值可以相互隱式轉(zhuǎn)換為這兩個類型中的任意一個。比如,
byte
和uint8
的任何值可以轉(zhuǎn)換為這兩個類型中的任意一個。rune
和int32
的任何值可以轉(zhuǎn)換為這兩個類型中的任意一個。[]byte
和[]uint8
的任何值可以轉(zhuǎn)換為這兩個類型中的任意一個。此條規(guī)則沒什么可解釋的,無論你是否認為此種情況中發(fā)生了轉(zhuǎn)換。
給定一個非接口值x
和一個非接口類型T
,并假設x
的類型為Tx
,
Tx
和T
的底層類型相同(忽略掉結構體字段標簽),則x
可以被顯式轉(zhuǎn)換為類型T
。Tx
和T
中至少有一個是無名類型并且它們的底層類型相同(考慮結構體字段標簽),則x
可以被隱式轉(zhuǎn)換為類型T
。Tx
和T
的底層類型不同,但是兩者都是無名的指針類型并且它們的基類型的底層類型相同(忽略掉結構體字段標簽),則x
可以(而且只能)被顯式轉(zhuǎn)換為類型T
。 (注意:兩處“忽略掉結構體字段標簽”從Go 1.8開始生效。)
一個例子:
package main
func main() {
// 類型[]int、IntSlice和MySlice共享底層類型:[]int。
type IntSlice []int
type MySlice []int
type Foo = struct{n int `foo`}
type Bar = struct{n int `bar`}
var s = []int{}
var is = IntSlice{}
var ms = MySlice{}
var x map[Bar]Foo
var y map[Foo]Bar
// 這兩行隱式轉(zhuǎn)換編譯不通過。
/*
is = ms
ms = is
*/
// 必須使用顯式轉(zhuǎn)換。
is = IntSlice(ms)
ms = MySlice(is)
x = map[Bar]Foo(y)
y = map[Foo]Bar(x)
// 這些隱式轉(zhuǎn)換是沒問題的。
s = is
is = s
s = ms
ms = s
}
指針相關的轉(zhuǎn)換例子:
package main
func main() {
type MyInt int
type IntPtr *int
type MyIntPtr *MyInt
var pi = new(int) // pi的類型為*int
var ip IntPtr = pi // 沒問題,因為底層類型相同
// 并且pi的類型為無名類型。
// var _ *MyInt = pi // 不能隱式轉(zhuǎn)換
var _ = (*MyInt)(pi) // 顯式轉(zhuǎn)換是沒問題的
// 類型*int的值不能被直接轉(zhuǎn)換為類型MyIntPtr,
// 但是可以間接地轉(zhuǎn)換過去。
/*
var _ MyIntPtr = pi // 不能隱式轉(zhuǎn)換
var _ = MyIntPtr(pi) // 也不能顯式轉(zhuǎn)換
*/
var _ MyIntPtr = (*MyInt)(pi) // 間接隱式轉(zhuǎn)換沒問題
var _ = MyIntPtr((*MyInt)(pi)) // 間接顯式轉(zhuǎn)換沒問題
// 類型IntPtr的值不能被直接轉(zhuǎn)換為類型MyIntPtr,
// 但是可以間接地轉(zhuǎn)換過去。
/*
var _ MyIntPtr = ip // 不能隱式轉(zhuǎn)換
var _ = MyIntPtr(ip) // 也不能顯式轉(zhuǎn)換
*/
// 間接隱式或者顯式轉(zhuǎn)換都是沒問題的。
var _ MyIntPtr = (*MyInt)((*int)(ip)) // ok
var _ = MyIntPtr((*MyInt)((*int)(ip))) // ok
}
給定一個通道值x
,假設它的類型Tx
是一個雙向通道類型,T
也是一個通道類型(無論是雙向的還是單向的)。如果Tx
和T
的元素類型相同并且它們中至少有一個為無名類型,則x
可以被隱式轉(zhuǎn)換為類型T
。
一個例子:
package main
func main() {
type C chan string
type C1 chan<- string
type C2 <-chan string
var ca C
var cb chan string
cb = ca // ok,因為底層類型相同
ca = cb // ok,因為底層類型相同
// 這4行都滿足此第3條轉(zhuǎn)換規(guī)則的條件。
var _, _ chan<- string = ca, cb // ok
var _, _ <-chan string = ca, cb // ok
var _ C1 = cb // ok
var _ C2 = cb // ok
// 類型C的值不能直接轉(zhuǎn)換為類型C1或C2。
/*
var _ = C1(ca) // compile error
var _ = C2(ca) // compile error
*/
// 但是類型C的值可以間接轉(zhuǎn)換為類型C1或C2。
var _ = C1((chan<- string)(ca)) // ok
var _ = C2((<-chan string)(ca)) // ok
var _ C1 = (chan<- string)(ca) // ok
var _ C2 = (<-chan string)(ca) // ok
}
給定一個值x
和一個接口類型I
,如果x
的類型(或者默認類型)為Tx
并且類型Tx
實現(xiàn)了接口類型I
,則x
可以被隱式轉(zhuǎn)換為類型I
。 此轉(zhuǎn)換的結果為一個類型為I
的接口值。此接口值包裹了
x
的一個副本(如果Tx
是一個非接口值);x
的動態(tài)值的一個副本(如果Tx
是一個接口值)。請閱讀接口一文獲取更多詳情和示例。
如果一個類型不確定值可以表示為類型T
的值,則它可以被隱式轉(zhuǎn)換為類型T
。
一個例子:
package main
func main() {
var _ []int = nil
var _ map[string]int = nil
var _ chan string = nil
var _ func()() = nil
var _ *bool = nil
var _ interface{} = nil
var _ int = 123.0
var _ float64 = 123
var _ int32 = 1.23e2
var _ int8 = 1 + 0i
}
(此規(guī)則和上一條規(guī)則有些重疊。)
常量的類型轉(zhuǎn)換結果一般仍然是一個常量。(除了下面第8條規(guī)則中將介紹的字符串轉(zhuǎn)換為字節(jié)切片或者碼點切片的情況。)
給定一個常量值x
和一個類型T
,如果x
可以表示成類型T
的一個值,則x
可以被顯式地轉(zhuǎn)換為類型T
;特別地,如果x
是一個類型不確定值,則它可以被隱式轉(zhuǎn)換為類型T
。
一個例子:
package main
func main() {
const I = 123
const I1, I2 int8 = 0x7F, -0x80
const I3, I4 int8 = I, 0.0
const F = 0.123456789
const F32 float32 = F
const F32b float32 = I
const F64 float64 = F
const F64b = float64(I3) // 這里必須顯式轉(zhuǎn)換
const C1, C2 complex64 = F, I
const I5 = int(C2) // 這里必須顯式轉(zhuǎn)換
}
非常量浮點數(shù)和整數(shù)值可以被顯式轉(zhuǎn)換為任何浮點數(shù)和整數(shù)類型。
非常量復數(shù)值可以被顯式轉(zhuǎn)換為任何復數(shù)類型。
注意,
一個例子:
package main
import "fmt"
func main() {
var a, b = 1.6, -1.6 // 類型均為float64
fmt.Println(int(a), int(b)) // 1 -1
var i, j int16 = 0x7FFF, -0x8000
fmt.Println(int8(i), uint16(j)) // -1 32768
var c1 complex64 = 1 + 2i
var _ = complex128(c1)
}
如果一個值的類型(或者默認類型)為一個整數(shù)類型,則此值可以被當作一個碼點值(rune值)顯式轉(zhuǎn)換為任何字符串類型。
一個字符串可以被顯式轉(zhuǎn)換為一個字節(jié)切片類型,反之亦然。 字節(jié)切片類型是指底層類型為[]byte
的類型。
一個字符串可以被顯式轉(zhuǎn)換為一個碼點切片類型,反之亦然。 碼點切片類型是指底層類型為[]rune
的類型。
請閱讀字符串一文獲取更多詳情和示例。
從Go 1.17開始,一個切片可以被轉(zhuǎn)化為一個相同元素類型的數(shù)組的指針類型。 但是如果數(shù)組的長度大于被轉(zhuǎn)化切片的長度,則將導致恐慌產(chǎn)生。
這里有一個例子。
從Go 1.20開始,一個切片可以被轉(zhuǎn)化為一個相同元素類型的數(shù)組。 但是如果數(shù)組的長度大于被轉(zhuǎn)化切片的長度,則將導致恐慌產(chǎn)生。
這里有一個例子。
非類型安全指針類型是指底層類型為unsafe.Pointer
的類型。
任何類型安全指針類型的值可以被顯式轉(zhuǎn)化為一個非類型安全指針類型,反之亦然。
任何uintptr值可以被顯式轉(zhuǎn)化為一個非類型安全指針類型,反之亦然。
請閱讀非類型安全指針一文獲取詳情和示例。
賦值可以看作是隱式類型轉(zhuǎn)換。 各種隱式轉(zhuǎn)換規(guī)則在上一節(jié)中已經(jīng)列出。
除了這些規(guī)則,賦值語句中的目標值必須為一個可尋址的值、一個映射元素表達式或者一個空標識符。
在一個賦值中,源值被復制給了目標值。精確地說,源值的直接部分被復制給了目標值。
注意:函數(shù)傳參和結果返回其實都是賦值。
Go白皮書提到:
在任何比較中,第一個比較值必須能被賦值給第二個比較值的類型,或者反之。
所以,值比較規(guī)則和賦值規(guī)則非常相似。 換句話說,兩個值是否可以比較取決于其中一個值是否可以隱式轉(zhuǎn)換為另一個值的類型。 很簡單?此規(guī)則描述基本正確,但是存在另外一條優(yōu)先級更高的規(guī)則:
如果一個比較表達式中的兩個比較值均為類型確定值,則它們的類型必須都屬于可比較類型。
按照上面這條規(guī)則,如果一個不可比較類型(肯定是一個非接口類型)實現(xiàn)了一個接口類型,則比較這兩個類型的值是非法的,即使前者的值可以隱式轉(zhuǎn)化為后者。
注意,盡管切片/映射/函數(shù)類型為不可比較類型,但是它們的值可以和類型不確定的預聲明nil
標識符比較。
上述規(guī)則并未覆蓋所有的情況。如果兩個值均為類型不確定值,它們可以比較嗎?這種情況的規(guī)則比較簡單:
兩個類型不確定的數(shù)字值的比較結果服從直覺。
注意,兩個類型不確定的nil值不能相互比較。
任何比較的結果均為一個類型不確定的布爾值。
一些值比較的例子:
package main
// 一些類型為不可比較類型的變量。
var s []int
var m map[int]int
var f func()()
var t struct {x []int}
var a [5]map[int]int
func main() {
// 這些比較編譯不通過。
/*
_ = s == s
_ = m == m
_ = f == f
_ = t == t
_ = a == a
_ = nil == nil
_ = s == interface{}(nil)
_ = m == interface{}(nil)
_ = f == interface{}(nil)
*/
// 這些比較編譯都沒問題。
_ = s == nil
_ = m == nil
_ = f == nil
_ = 123 == interface{}(nil)
_ = true == interface{}(nil)
_ = "abc" == interface{}(nil)
}
假設兩個值可以相互比較,并且它們的類型同為T
。 (如果它們的類型不同,則其中一個可以轉(zhuǎn)換為另一個的類型。這里我們不考慮兩者均為類型不確定值的情形。)
T
是一個布爾類型,則這兩個值只有在它們同為true
或者false
的時候比較結果才為true
。
T
是一個整數(shù)類型,則這兩個值只有在它們在內(nèi)存中的表示完全一致的情況下比較結果才為true
。
T
是一個浮點數(shù)類型, 則這兩個值只要滿足下面任何一種情況,它們的比較結果就為true
:
+Inf
;
-Inf
;
-0.0
或者都為+0.0
。
NaN
并且它們在內(nèi)存中的表示完全一致。
T
是一個復數(shù)類型,則這兩個值只有在它們的實部和虛部均做為浮點數(shù)進行進行比較的結果都為true
的情況下比較結果才為true
。
T
是一個指針類型(類型安全或者非類型安全),則這兩個值只有在它們所表示的地址值相等或者它們都為nil的情況下比較結果才為true
。
T
是一個通道類型,則這兩個值只有在它們引用著相同的底層內(nèi)部通道或者它們都為nil時比較結果才為true
。
T
是一個結構體類型,則它們的相應字段將逐對進行比較。只要有一對字段不相等,這兩個結構體值就不相等。
T
是一個數(shù)組類型,則它們的相應元素將逐對進行比較。只要有一對元素不相等,這兩個結構體值就不相等。
T
是一個接口類型,請參閱兩個接口值是如何進行比較的。
T
是一個字符串類型,請參閱兩個字符串值是如何進行比較的。
請注意,動態(tài)類型均為同一個不可比較類型的兩個接口值的比較將產(chǎn)生一個恐慌。比如下面的例子:
package main
func main() {
type T struct {
a interface{}
b int
}
var x interface{} = []int{}
var y = T{a: x}
var z = [3]T{{a: y}}
// 這三個比較均會產(chǎn)生一個恐慌。
_ = x == x
_ = y == y
_ = z == z
}
更多建議: