Go 学习笔记(32)— 类型系统(命名类型、未命名类型、底层类型、类型强制转换、类型别名和新声明类型)
1. 命名類型和未命名類型
1.1 命名類型
類型可以通過(guò)標(biāo)識(shí)符來(lái)表示,這種類型稱為命名類型( Named Type )。 Go 語(yǔ)言的基本類型中有 20 個(gè)預(yù)聲明簡(jiǎn)單類型都是命名類型, Go 語(yǔ)言還有一種命名類型一一用戶自定義類型。
1.2 未命名類型
一個(gè)類型由預(yù)聲明類型、關(guān)鍵字和操作符組合而成,這個(gè)類型稱為未命名類型( Unamed Type )。未命名類型又稱為類型字面量( Type Literal )。
Go 語(yǔ)言的基本類型中的復(fù)合類型:數(shù)組( array )、切片( slice )、字典( map )、通道( channel )、指針( pointer ) 、函數(shù)字面量( function )、結(jié)構(gòu)( struct )和接口( interface )都屬于類型字面量,也都是未命名類型。
所以 *int , []int , [2]int , map[k]v 都是未命名類型。
注意:前面所說(shuō)的結(jié)構(gòu)和接口是未命名類型,這里的結(jié)構(gòu)和接口沒(méi)有使用 type 格式定義,具體見(jiàn)下方示例說(shuō)明。
package mainimport "fmt"// Person 使用 type 聲明的是命名類型
type Person struct {name stringage int
}func main() {// 使用 struct 字面量聲明的是未命名類型a := struct {name stringage int}{"Jim", 20}fmt.Printf("%T\n", a) // struct { name string; age int }fmt.Printf("%v\n", a) // {Jim 20}b := Person{"Tom", 22}fmt.Printf("%T\n", b) // main.Personfmt.Printf("%v\n", b) // {Tom 22}}
Go 語(yǔ)言的命名類型和未命名類型總結(jié)如下:
- 未命名類型和類型字面量是等價(jià)的,我們通常所說(shuō)的
Go語(yǔ)言基本類型中的復(fù)合類型就是類型字面量,所以未命名類型、類型字面量和Go語(yǔ)言基本類型中的復(fù)合類型三者等價(jià)。 - 通常所說(shuō)的
Go語(yǔ)言基本類型中的簡(jiǎn)單類型就是這 20 個(gè)預(yù)聲明類型,它們都屬于命名類型。 - 預(yù)聲明類型是命名類型的一種,另一類命名類型是自定義類型。
2. 底層類型
所有“類型”都有一個(gè) underlying type (底層類型)。底層類型的規(guī)則如下:
- 簡(jiǎn)單類型和復(fù)合類型的底層類型是它們自身。
- 自定義類型
type newtype oldtype中newtype的底層類型是逐層遞歸向下查找的,直到查到的oldtype是簡(jiǎn)單類型或復(fù)合類型為止。
例如:
type T1 string
type T2 Tl
type T3 []string
type T4 T3
type T5 []T1
type T6 T5
T1 和 T2 的底層類型都是 string , T3 和 T4 的底層類型都是 []string , T5 和 T6 的底層類型都是 []T1 。特別注意這里的 T6 、 T5 與 T3 、 T4 的底層類型是不一樣的, 一個(gè)是 []T1 ,另一個(gè)是 []string 。
底層類型在類型賦值和類型強(qiáng)制轉(zhuǎn)換時(shí)會(huì)使用,接下來(lái)就介紹這兩個(gè)主題。
3. 類型相同和類型賦值
3.1 類型相同
Go 是強(qiáng)類型的語(yǔ)言,編譯器在編譯時(shí)會(huì)進(jìn)行嚴(yán)格的類型校驗(yàn)。兩個(gè)命名類型是否相同,參考如下:
- 兩個(gè)命名類型相同的條件是兩個(gè)類型聲明的語(yǔ)句完全相同;
- 命名類型和未命名類型永遠(yuǎn)不相同;
- 兩個(gè)未命名類型相同的條件是它們的類型聲明字面量的結(jié)構(gòu)相同,井且內(nèi)部元素的類型相同;
- 通過(guò)類型別名語(yǔ)句聲明的兩個(gè)類型相同;
Go 1.9 引入了類型別名語(yǔ)法 type T1 = T2 , T1 的類型完全和 T2 一樣。
3.2 類型賦值
不同類型的變量之間一般是不能直接相互賦值的,除非滿足一定的條件。類型為 T1 的變量 a 可以賦值給類型為 T2 的變量 b , 稱為類型 T1 可以賦值給類型 T2 ,偽代碼表述如下:
// a 是類型為T1 的變量,或者a 本身就是一個(gè)字面常量或 nil
// 如果如下語(yǔ)句可以執(zhí)行,則稱之為類型 Tl 可以賦值給類型T2
var b T2 = a
a 可以賦值給變量 b 必須要滿足如下條件中的一個(gè):
T1和T2類型相同;T1和T2具有相同的底層類型,并且T1和T2里面至少有一個(gè)是未命名類型;T2是接口類型,T1是具體類型,T1的方法集是T2方法集的超集;T1和T2都是通道類型,它們擁有相同的元素類型,并且T1和T2中至少有一個(gè)是未命名類型;T1是預(yù)聲明標(biāo)識(shí)符nil,T2是pointer、funcition、slice、map、channel、interface類型中的一個(gè);a是一個(gè)字面常量值,可以用來(lái)表示類型T的值;
示例如下:
package mainimport "fmt"type Map map[string]stringfunc (m Map) Print() {for _, v := range m {fmt.Println("v is ", v)}
}type iMap Map// 只要底層類型是slice 、map 等支持range 的類型字面量,新類型仍然可以使用range 迭代
func (m iMap) Print() {for _, v := range m {fmt.Println("v is ", v)}
}type slice []intfunc (s slice) Print() {for _, v := range s {fmt.Println("v is ", v)}
}func main() {mp := make(map[string]string, 10)mp["hi"] = "hello"// mp 與ma 有相同的底層類型map[string]stirng ,并且mp 是未命名類型// 所以mp 可以直接賦值給mavar ma Map = mp/*im 與 ma 雖然有相同的底層類型map[string]stirng,但它們中沒(méi)有一個(gè)是未命名類型不能賦值, 如下語(yǔ)句不能通過(guò)編譯*/// var im iMap = mama.Print()// im.Print()var i interface {Print()} = mai.Print()s1 := []int{1, 2, 3}var s2 slices2 = s1s2.Print()
}
4. 類型強(qiáng)制轉(zhuǎn)換
由于 Go 是強(qiáng)類型的語(yǔ)言, 如果不滿足自動(dòng)轉(zhuǎn)換的條件,則必須進(jìn)行強(qiáng)制類型轉(zhuǎn)換。任意兩個(gè)不相干的類型如果進(jìn)行強(qiáng)制轉(zhuǎn)換,則必須符合一定的規(guī)則。
強(qiáng)制類型的語(yǔ)法格式:
var a T = (T) (b)
使用括號(hào)將類型和要轉(zhuǎn)換的變量或表達(dá)式的值括起來(lái)。
非常量類型的變量 x 可以強(qiáng)制轉(zhuǎn)化并傳遞給類型 T , 需要滿足如下任一條件:
(1) x 可以直接賦值給 T 類型變量;
(2) x 的類型和 T 具有相同的底層類型;
繼續(xù)上一節(jié)使用的示例:
/*im 與ma 雖然有相同的底層類型,但是二者中沒(méi)有一個(gè)是字面量類型,不能直接賦值,可以強(qiáng)制進(jìn)行類型轉(zhuǎn)換*/var im iMap = (iMap)(ma)
(3) x 的類型和 T 都是未命名的指針類型,并且指針指向的類型具有相同的底層類型;
(4) x 的類型和 T 都是整型,或者都是浮點(diǎn)型;
(5) x 的類型和 T 都是復(fù)數(shù)類型;
(6) x 是整數(shù)值或 []byte 類型的值, T 是 string 類型;
(7) x 是一個(gè)字符串, T 是 []byte 或 []rune ;
字符串和字節(jié)切片之間的轉(zhuǎn)換最常見(jiàn),示例如下:
func main() {s := "hello,你好"var a []bytea = []byte(s)var b stringb = string(a)var c []runec = []rune(s)fmt.Printf("%T\n", a) // []uint8 byte 是 int8 的別名fmt.Printf("%T\n", b) // stringfmt.Printf("%T\n", c) // []int32 rune 是 int32 的別名
}
注意:
-
數(shù)值類型和
string類型之間的相互轉(zhuǎn)換可能造成值部分丟失; 其他的轉(zhuǎn)換僅是類型的轉(zhuǎn)換,不會(huì)造成值的改變。 -
string和數(shù)字之間的轉(zhuǎn)換可使用標(biāo)準(zhǔn)庫(kù)strconv。 -
Go語(yǔ)言沒(méi)有語(yǔ)言機(jī)制支持指針和interger之間的直接轉(zhuǎn)換,可以使用標(biāo)準(zhǔn)庫(kù)中的unsafe包進(jìn)行處理。
5. 類型別名和新聲明類型
類型別名與類型定義(新聲明類型)不同之處在于,使用類型別名需要在別名和原類型之間加上賦值符號(hào)(=);使用類型別名定義的類型與原類型等價(jià),而使用類型定義出來(lái)的類型是一種新的類型。
package mainimport ("fmt"
)type a = string
type b stringfunc SayA(str a) {fmt.Println(str)
}func SayB(str b) {fmt.Println(str)
}func main() {var str = "test"SayA(str)//錯(cuò)誤參數(shù)傳遞,str是字符串類型,不能賦值給b類型變量SayB(str)
}
這段代碼在編譯時(shí)會(huì)出現(xiàn)如下錯(cuò)誤:
cannot use str (type string) as type b in argument to SayB
從錯(cuò)誤信息可知,str 為字符串類型,不能當(dāng)做 b 類型參數(shù)傳入 SayB 函數(shù)中。而 str 卻可以當(dāng)做 a 類型參數(shù)傳入到 SayA 函數(shù)中。由此可見(jiàn),使用類型別名定義的類型與原類型一致,而類型定義定義出來(lái)的類型,是一種新的類型。
5.1 類型別名
示例代碼:
package mainimport "fmt"func main() {// 示例1。{type MyString = stringstr := "BCD"myStr1 := MyString(str)myStr2 := MyString("A" + str)fmt.Printf("%T(%q) == %T(%q): %v\n", str, str, myStr1, myStr1, str == myStr1)fmt.Printf("%T(%q) > %T(%q): %v\n", str, str, myStr2, myStr2, str > myStr2)fmt.Printf("Type %T is the same as type %T.\n", myStr1, str)fmt.Println()strs := []string{"E", "F", "G"}myStrs := []MyString(strs)fmt.Printf("A value of type []MyString: %T(%q)\n", myStrs, myStrs)fmt.Printf("Type %T is the same as type %T.\n", myStrs, strs)fmt.Println()}
}
輸出結(jié)果:
string("BCD") == string("BCD"): true
string("BCD") > string("ABCD"): true
Type string is the same as type string.A value of type []MyString: []string(["E" "F" "G"])
Type []string is the same as type []string.
5.2 新聲明類型
示例代碼:
package mainimport "fmt"func main() {{type MyString stringstr := "BCD"myStr1 := MyString(str)myStr2 := MyString("A" + str)_ = myStr2// 這里的判等不合法,會(huì)引發(fā)編譯錯(cuò)誤。// invalid operation: str == myStr1 (mismatched types string and MyString)// fmt.Printf("%T(%q) == %T(%q): %v\n", str, str, myStr1, myStr1, str == myStr1)// 這里的比較不合法,會(huì)引發(fā)編譯錯(cuò)誤。// fmt.Printf("%T(%q) > %T(%q): %v\n", str, str, myStr2, myStr2, str > myStr2)fmt.Printf("Type %T is different from type %T.\n", myStr1, str)strs := []string{"E", "F", "G"}var myStrs []MyString// 這里的類型轉(zhuǎn)換不合法,會(huì)引發(fā)編譯錯(cuò)誤。// cannot convert strs (type []string) to type []MyString// myStrs = []MyString(strs)//fmt.Printf("A value of type []MyString: %T(%q)\n", myStrs, myStrs)fmt.Printf("Type %T is different from type %T.\n", myStrs, strs)fmt.Println()}}
5.3 類型別名和新聲明類型相互賦值
package mainfunc main() {{type MyString1 = stringtype MyString2 stringstr := "BCD"myStr1 := MyString1(str)myStr2 := MyString2(str)myStr1 = MyString1(myStr2)myStr2 = MyString2(myStr1)myStr1 = str// 這里的賦值不合法,會(huì)引發(fā)編譯錯(cuò)誤。// cannot use str (type string) as type MyString2 in assignment// myStr2 = str//myStr1 = myStr2 // 這里的賦值不合法,會(huì)引發(fā)編譯錯(cuò)誤。//myStr2 = myStr1 // 這里的賦值不合法,會(huì)引發(fā)編譯錯(cuò)誤。}
}
5.4 給類型別名新增方法,會(huì)添加到原類型方法集中
給類型別名新增方法后,原類型也能使用這個(gè)方法。下邊請(qǐng)看一段示例代碼:
package mainimport ("fmt"
)// 根據(jù)string類型,定義類型S
type S string
func (r *S) Hi() {fmt.Println("S hi")
}// 定義S的類型別名為T
type T = S
func (r *T) Hello() {fmt.Println("T hello")
}// 函數(shù)參數(shù)接收S類型的指針變量
func exec(obj *S) {obj.Hello()obj.Hi()
}func main() {t := new(T)s := new(S)exec(s)// 將T類型指針變量傳遞給S類型指針變量exec(t)
}
輸出信息是:
T hello
S hi
T hello
S hi
上邊的示例中,S 是原類型,T 是 S 類型別名。在給 T 增加了 Hello 方法后,S 類型的變量也可以使用 Hello 方法。說(shuō)明給類型別名新增方法后,原類型也能使用這個(gè)方法。從示例中可知,變量 t 可以賦值給 S 類型變量 s,所以類型別名是給原類型取了一個(gè)小名,本質(zhì)上沒(méi)有發(fā)生任何變化。
類型別名,只能對(duì)同一個(gè)包中的自定義類型產(chǎn)生作用。舉個(gè)例子,Golang SDK 中有很多個(gè)包,是不是我們可以使用類型別名,給 SDK 包中的結(jié)構(gòu)體類型新增方法呢?答案是:不行。請(qǐng)牢記一點(diǎn):類型別名,只能對(duì)包內(nèi)的類型產(chǎn)生作用,對(duì)包外的類型采用類型別名,在編譯時(shí)將會(huì)提示如下信息:
cannot define new methods on non-local type string
參考書籍:
- Go 語(yǔ)言核心編程
- Go 語(yǔ)言圣經(jīng)
- Go語(yǔ)言快速入門
總結(jié)
以上是生活随笔為你收集整理的Go 学习笔记(32)— 类型系统(命名类型、未命名类型、底层类型、类型强制转换、类型别名和新声明类型)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Go 学习笔记(31)— 字符串 str
- 下一篇: Go 学习笔记(33)— Go 自定义类