Go 学习笔记(32)— 类型系统(命名类型、未命名类型、底层类型、类型强制转换、类型别名和新声明类型)
1. 命名類型和未命名類型
1.1 命名類型
類型可以通過標識符來表示,這種類型稱為命名類型( Named Type )。 Go 語言的基本類型中有 20 個預聲明簡單類型都是命名類型, Go 語言還有一種命名類型一一用戶自定義類型。
1.2 未命名類型
一個類型由預聲明類型、關鍵字和操作符組合而成,這個類型稱為未命名類型( Unamed Type )。未命名類型又稱為類型字面量( Type Literal )。
Go 語言的基本類型中的復合類型:數組( array )、切片( slice )、字典( map )、通道( channel )、指針( pointer ) 、函數字面量( function )、結構( struct )和接口( interface )都屬于類型字面量,也都是未命名類型。
所以 *int , []int , [2]int , map[k]v 都是未命名類型。
注意:前面所說的結構和接口是未命名類型,這里的結構和接口沒有使用 type 格式定義,具體見下方示例說明。
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 語言的命名類型和未命名類型總結如下:
- 未命名類型和類型字面量是等價的,我們通常所說的
Go語言基本類型中的復合類型就是類型字面量,所以未命名類型、類型字面量和Go語言基本類型中的復合類型三者等價。 - 通常所說的
Go語言基本類型中的簡單類型就是這 20 個預聲明類型,它們都屬于命名類型。 - 預聲明類型是命名類型的一種,另一類命名類型是自定義類型。
2. 底層類型
所有“類型”都有一個 underlying type (底層類型)。底層類型的規則如下:
- 簡單類型和復合類型的底層類型是它們自身。
- 自定義類型
type newtype oldtype中newtype的底層類型是逐層遞歸向下查找的,直到查到的oldtype是簡單類型或復合類型為止。
例如:
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 的底層類型是不一樣的, 一個是 []T1 ,另一個是 []string 。
底層類型在類型賦值和類型強制轉換時會使用,接下來就介紹這兩個主題。
3. 類型相同和類型賦值
3.1 類型相同
Go 是強類型的語言,編譯器在編譯時會進行嚴格的類型校驗。兩個命名類型是否相同,參考如下:
- 兩個命名類型相同的條件是兩個類型聲明的語句完全相同;
- 命名類型和未命名類型永遠不相同;
- 兩個未命名類型相同的條件是它們的類型聲明字面量的結構相同,井且內部元素的類型相同;
- 通過類型別名語句聲明的兩個類型相同;
Go 1.9 引入了類型別名語法 type T1 = T2 , T1 的類型完全和 T2 一樣。
3.2 類型賦值
不同類型的變量之間一般是不能直接相互賦值的,除非滿足一定的條件。類型為 T1 的變量 a 可以賦值給類型為 T2 的變量 b , 稱為類型 T1 可以賦值給類型 T2 ,偽代碼表述如下:
// a 是類型為T1 的變量,或者a 本身就是一個字面常量或 nil
// 如果如下語句可以執行,則稱之為類型 Tl 可以賦值給類型T2
var b T2 = a
a 可以賦值給變量 b 必須要滿足如下條件中的一個:
T1和T2類型相同;T1和T2具有相同的底層類型,并且T1和T2里面至少有一個是未命名類型;T2是接口類型,T1是具體類型,T1的方法集是T2方法集的超集;T1和T2都是通道類型,它們擁有相同的元素類型,并且T1和T2中至少有一個是未命名類型;T1是預聲明標識符nil,T2是pointer、funcition、slice、map、channel、interface類型中的一個;a是一個字面常量值,可以用來表示類型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,但它們中沒有一個是未命名類型不能賦值, 如下語句不能通過編譯*/// var im iMap = mama.Print()// im.Print()var i interface {Print()} = mai.Print()s1 := []int{1, 2, 3}var s2 slices2 = s1s2.Print()
}
4. 類型強制轉換
由于 Go 是強類型的語言, 如果不滿足自動轉換的條件,則必須進行強制類型轉換。任意兩個不相干的類型如果進行強制轉換,則必須符合一定的規則。
強制類型的語法格式:
var a T = (T) (b)
使用括號將類型和要轉換的變量或表達式的值括起來。
非常量類型的變量 x 可以強制轉化并傳遞給類型 T , 需要滿足如下任一條件:
(1) x 可以直接賦值給 T 類型變量;
(2) x 的類型和 T 具有相同的底層類型;
繼續上一節使用的示例:
/*im 與ma 雖然有相同的底層類型,但是二者中沒有一個是字面量類型,不能直接賦值,可以強制進行類型轉換*/var im iMap = (iMap)(ma)
(3) x 的類型和 T 都是未命名的指針類型,并且指針指向的類型具有相同的底層類型;
(4) x 的類型和 T 都是整型,或者都是浮點型;
(5) x 的類型和 T 都是復數類型;
(6) x 是整數值或 []byte 類型的值, T 是 string 類型;
(7) x 是一個字符串, T 是 []byte 或 []rune ;
字符串和字節切片之間的轉換最常見,示例如下:
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 的別名
}
注意:
-
數值類型和
string類型之間的相互轉換可能造成值部分丟失; 其他的轉換僅是類型的轉換,不會造成值的改變。 -
string和數字之間的轉換可使用標準庫strconv。 -
Go語言沒有語言機制支持指針和interger之間的直接轉換,可以使用標準庫中的unsafe包進行處理。
5. 類型別名和新聲明類型
類型別名與類型定義(新聲明類型)不同之處在于,使用類型別名需要在別名和原類型之間加上賦值符號(=);使用類型別名定義的類型與原類型等價,而使用類型定義出來的類型是一種新的類型。
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)//錯誤參數傳遞,str是字符串類型,不能賦值給b類型變量SayB(str)
}
這段代碼在編譯時會出現如下錯誤:
cannot use str (type string) as type b in argument to SayB
從錯誤信息可知,str 為字符串類型,不能當做 b 類型參數傳入 SayB 函數中。而 str 卻可以當做 a 類型參數傳入到 SayA 函數中。由此可見,使用類型別名定義的類型與原類型一致,而類型定義定義出來的類型,是一種新的類型。
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()}
}
輸出結果:
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// 這里的判等不合法,會引發編譯錯誤。// invalid operation: str == myStr1 (mismatched types string and MyString)// 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 different from type %T.\n", myStr1, str)strs := []string{"E", "F", "G"}var myStrs []MyString// 這里的類型轉換不合法,會引發編譯錯誤。// 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// 這里的賦值不合法,會引發編譯錯誤。// cannot use str (type string) as type MyString2 in assignment// myStr2 = str//myStr1 = myStr2 // 這里的賦值不合法,會引發編譯錯誤。//myStr2 = myStr1 // 這里的賦值不合法,會引發編譯錯誤。}
}
5.4 給類型別名新增方法,會添加到原類型方法集中
給類型別名新增方法后,原類型也能使用這個方法。下邊請看一段示例代碼:
package mainimport ("fmt"
)// 根據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")
}// 函數參數接收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 方法。說明給類型別名新增方法后,原類型也能使用這個方法。從示例中可知,變量 t 可以賦值給 S 類型變量 s,所以類型別名是給原類型取了一個小名,本質上沒有發生任何變化。
類型別名,只能對同一個包中的自定義類型產生作用。舉個例子,Golang SDK 中有很多個包,是不是我們可以使用類型別名,給 SDK 包中的結構體類型新增方法呢?答案是:不行。請牢記一點:類型別名,只能對包內的類型產生作用,對包外的類型采用類型別名,在編譯時將會提示如下信息:
cannot define new methods on non-local type string
參考書籍:
- Go 語言核心編程
- Go 語言圣經
- Go語言快速入門
總結
以上是生活随笔為你收集整理的Go 学习笔记(32)— 类型系统(命名类型、未命名类型、底层类型、类型强制转换、类型别名和新声明类型)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Go 学习笔记(31)— 字符串 str
- 下一篇: 大话西游无脸是谁画的啊?