日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

      歡迎訪問 生活随笔!

      生活随笔

      當前位置: 首頁 > 运维知识 > windows >内容正文

      windows

      Go 语言学习笔记(三):类型系统

      發布時間:2024/2/28 windows 26 豆豆
      生活随笔 收集整理的這篇文章主要介紹了 Go 语言学习笔记(三):类型系统 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

      目錄

      ? ? ? ??命名類型和未命名類型

      ? ? ? ??類型方法

      ? ? ? ??組合和方法集

      ? ? ? ??函數類型


      類型系統對于一門語言來說至關重要,特別是靜態編程語言,類型系統能夠在編譯階段發現大部分程序錯誤。Go 語言是一種靜態類型的編程語言。這意味著,編譯器需要在編譯時知曉程序里每個值的類型。如果提前知道類型信息,編譯器就可以確保程序合理地使用值。這有助于減少潛在的內存異常和 bug,并且使編譯器有機會對代碼進行一些性能優化,提高執行效率。

      值的類型給編譯器提供兩部分信息:第一部分,需要分配多少內存給這個值(即值的規模);第二部分,這段內存表示什么。對于許多內置類型的情況來說,規模和表示是類型名的一部分。例如:int64 類型的值需要 8 字節(64 位),表示一個整數值; float32 類型的值需要 4 字節(32 位)。類型是高級語言實現抽象編程的基礎,學好類型系統對于掌握一門語言來說至關重要。Go 語言的類型系統可以分為命名類型、非命名類型、底層類型、動態類型和靜態類型等。

      Go 語言從設計之初就本著?"大道至簡"?的理念,所以 Go 語言的類型系統設計得非常精煉,拋棄了大部分傳統面向對象語言的類的概念,取而代之的是結構( struct )。結構在內存分布上看起來和 C 語言的 struct 沒有區別,簡單干凈,沒有像 C++ 那樣為了實現多態和多繼承而額外添加虛擬函數指針。這種簡單的設計實際上蘊藏著一種哲學:把語言的特性設計得盡可能正交,相互之間不要關聯,對多態的支持交給接口去處理,類型的存儲盡量簡單、平坦、直接。

      ?

      ?

      命名類型和未命名類型

      命名類型( Named Type )

      類型可以通過標識符來表示,這種類型稱為命名類型。Go 語言的基本類型中有 20 個預聲明簡單類型都是命名類型,Go 語言還有一種命名類型——用戶自定義類型。Go 語言允許用戶定義類型。當用戶聲明一個新類型時,這個聲明就給編譯器提供了一個框架,告知必要的內存大小和表示信息。聲明后的類型與內置類型的運作方式類似。 Go 語言里聲明用戶定義的類型有兩種方法。最常用的方法是使用關鍵字 struct,它可以讓用戶創建一個結構類型。

      ?

      未命名類型 ( Unamed Type )

      一個類型由預聲明類型、關鍵字和操作符組合而成,這個類型稱為未命名類型。未命名類型又稱為類型字面量(Type Literal)。Go 語言的基本類型中的復合類型:數組(array)、切片(slice)、字典(map)、通道(channel)、指針(pointer)、函數字面量(function)、結構(struct)和接口(interface)都屬于類型字面量,也都是未命名類型。所以 *int、[]int、[2]int、map[k]v 都是未命名類型。注意:用 type 聲明的結構和接口是命名類型。

      package mainimport "fmt"type Person struct {name stringage int }func main() {// 這里的struct是未命名類型a := struct {name stringage int}{"asd", 18}fmt.Printf("%T\n", a)fmt.Printf("%v\n", a)b := Person{"tom", 21}fmt.Printf("%T\n", b)fmt.Printf("%v\n", a) }

      ?

      底層類型

      所有?"類型"?都有一個 underlying type (底層類型)。底層類型的規則如下:

      (1) 預聲明類型(Pre-declared types)和類型字面量(type literals)的底層類型是它們自身。

      (2) 自定義類型 type newtype oldtype 中 newtype 的底層類型是逐層遞歸向下查找的,直到查到的 oldtype 是預聲明類型(Pre-declared types)或類型字面量(type literals)為止。例如:

      package mainimport "fmt"type asd struct {asdint int }type qwe struct {qweint int }func main() {var a asdvar b qwefmt.Printf("%T\n", a)fmt.Printf("%T\n", b) }

      ?

      ?

      類型相同和類型賦值

      類型相同

      Go 是強類型的語言,編譯器在編譯時會進行嚴格的類型校驗。兩個命名類型是否相同,參考如下:

      (1) 兩個命名類型相同的條件是兩個類型聲明的語句完全相同。

      (2) 命名類型和未命名類型永遠不相同。

      (3) 兩個未命名類型相同的條件是它們的類型聲明宇面量的結構相同,井且內部元素的類型相同。

      (4) 通過類型別名語句聲明的兩個類型相同。

      ?

      類型可直接賦值

      不同類型的變量之間一般是不能直接相互賦值的,除非滿足一定的條件。下面探討類型可賦值的條件。類型為 T1?的變量 a 可以賦值給類型為 T2 的變量 b,稱為類型 T1 可以賦值給類型 T2,即 var b T2 = a。a 可以賦值給變量 b 必須要滿足以下條件中的一個:

      (1) T1?和 T2 的類型相同。

      (2)?T1?和 T2 具有相同的底層類型,并且 T1?和 T2 里面至少有一個是未命名類型。

      (3)?T2是接口類型, T1 是具體類型, T1 的方法集是 T2 方法集的超集

      (4) T1?和 T2 都是通道類型,它們擁有相同的元素類型,并且 T1?和 T2 中至少有一個是未命名類型。

      (5) a 是預聲明標識符 nilT2 是 pointer、funcition、slice、map、channel、interface 類型中的一個。

      (6) a 是一個字面常量值,可以用來表示類型 T 的值。

      package mainimport "fmt"type Map map[string]stringfunc (m Map) Print() {for _, key := range m {fmt.Println(key)} }type iMap Map //只要底層類型是slice、map等支持range的類型字面量,新類型仍然可以使用range迭代func (m iMap) Print() {for _, key := range m {fmt.Println(key)} }type slice []int func (s slice) Print() {for _, v := range s {fmt.Println(v)} }func main() {mp := make(map[string]string, 10)mp["hi"] = "tata"// mp 與 ma 有相同的底層類型map[string]string,并且 mp 是未命名類型// 所以 mp 可以直接賦值給 mavar ma Map = mp// im 與 ma 雖然有相同的底層類型 map[string]string,但是它們中沒有一個是未命名類型// 所以不能賦值var im iMap = mama.Print()im.Print()var i interface {Print()} = mai.Print()s1 := []int{1,2,3}var s2 slices2 = s1s2.Print() }

      ?

      ?

      類型強制轉換

      由于 Go 是強類型的語言,如果不滿足自動轉換的條件,則必須進行強制類型轉換。任意兩個不相干的類型如果進行強制轉換,則必須符合一定的規則。強制類型的語法格式: var a T?= (T) (b),使用括號將類型和要轉換的變量或表達式的值括起來。非常量類型的變量 x 可以強制轉化并傳遞給類型 T,需要滿足如下任一條件:

      (1) x 可以直接賦值給 T 類型變量。

      (2) x 的類型和 T 具有相同的底層類型。

      package mainimport "fmt"type Map map[string]stringfunc (m Map) Print() {for _, key := range m {fmt.Println(key)} }type iMap Map //只要底層類型是slice、map等支持range的類型字面量,新類型仍然可以使用range迭代func (m iMap) Print() {for _, key := range m {fmt.Println(key)} }type slice []intfunc (s slice) Print() {for _, v := range s {fmt.Println(v)} }func main() {mp := make(map[string]string, 10)mp["hi"] = "tata"var ma Map = mpvar im iMap = (iMap)(ma)ma.Print()im.Print() }

      (3) x 的類型和 T 都是未命名的指針類型,并且指針指向的類型具有相同的底層類型。

      (4) x 的類型和 T 都是整型,或者都是浮點型 。

      (5) x 的類型和 T 都是復數類型。

      (6) x 是整數值或?[]byte 類型的值,T 是 string 類型。

      (7) x 是一個字符串, T 是?[]byte 或?[]rune。字符串和字節切片轉換如下:

      s := "hello, 世界!" var a []byte a = []byte(s)var b string b = string(a)var c []rune c = []rune(s)fmt.Printf("%T\n", a) ?// []uint8 byte 是 int8 的別名 fmt.Printf("%T\n", b) ?// string fmt.Printf("%T\n", c) ?// []int32 rune 是 int32 的別名

      注意:

      (1) 數值類型和 string 類型之間的相互轉換可能造成值部分丟失;其他的轉換僅是類型的轉換,不會造成值的改變。string 和數字之間的轉換可使用標準庫?strconv。

      (2)?Go 語言沒有語言機制支持指針和 interger 之間的直接轉換可以使用標準庫中的 unsafe 包進行處理。

      ?

      自定義類型

      前面介紹命名類型時提到了自定義類型。用戶自定義類型使用關鍵字 type,其語法格式是 type?newtype oldtype。oldtype 可以是自定義類型、預聲明類型、未命名類型中的任意一種。newtype 是新類型的標識符,與 oldtype 具有相同的底層類型,并且都繼承了底層類型的操作集合(這里的操作不是方法,比如底層類型是 map?支持 range?迭代訪問,則新類型也可以使用 range 迭代訪問 )。除此之外,newtype 和 oldtype 是兩個完全不同的類型,newtype 不會繼承 oldtype 的方法。無論 oldtype 是什么類型,使用 type 聲明的新類型都是一種命名類型,也就是說,自定義類型都是命名類型。

      ?

      自定義 struct 類型

      struct 類型是?Go 語言自定義類型的普遍的形式,是 Go 語言類型擴展的基石,也是 Go 語言面向對象承載的基礎。struct 初始化:

      type Person struct {name stringage int }三種初始化方法: (1) a:= Person{"asd" , 18} (2) b:= Person{"asd",18,} (3) c := Person{"asd",18} 這不是一種推薦的方法,一旦結構增加字段,則不得不修改順序初始化語句。指定字段名進行初始化。 (1) a:= Person{name: "asd", age: 18} (2) b:= Person{name: "asd",age: 18,} (3) c := Person{name: "asd",age: 18} 這種方法,就算結構增加了字段,也不用修改初始化語句。使用new創建內置函數,字段默認初始化為其類型的零值,返回值是指向結構的指針。 p := new(Person)

      當聲明變量時,這個變量對應的值總是會被初始化。這個值要么用指定的值初始化,要么用零值(即變量類型的默認值)做初始化。對數值類型來說,零值是 0;對字符串來說,零值是空字符串;對布爾類型,零值是 false。

      ?

      結構字段的特點

      結構的字段可以是任意的類型,基本類型、接口類型、指針類型、函數類型都可以作為 struct 的字段。結構字段的類型名必須唯一 ,struct 字段類型可以是普通類型,也可以是指針。另外,結構支持內嵌自身的指針,這也是實現樹形和鏈表等復雜數據結構的基礎。例如:

      //標準庫 container/list type Element struct {//指向自身類型的指針next, prev *Elementlist *ListValue interface{} }

      ?

      匿名字段

      在定義 struct 的過程中,如果字段只給出字段類型,沒有給出字段名,則稱這樣的字段為?"匿名字段"。被匿名嵌入的字段必須是命名類型或命名類型的指針,類型字面量不能作為匿名字段使用。匿名字段的字段名默認就是類型名,如果匿名字段是指針類型,則默認的字段名就是指針指向的類型名。但一個結構體里面不能同時存在某一類型及其指針類型的匿名字段,原因是二者的字段名相等。如果嵌入的字段來自其他包,則需要加上包名,并且必須是其他包可以導出的類型。示例如下:

      // 標準庫 ${GOROOT}/src/os/type.go內的一個匿名的指針字段 type File struct {*file // os specific }

      ?

      ?

      類型

      為類型增加方法是 Go 語言實現面向對象編程的基礎,在介紹類型方法之前先介紹自定義類型。方法能給用戶定義的類型添加新的行為。方法實際上也是函數,只是在聲明時,在關鍵字 func 和方法名之間增加了一個參數。

      前面介紹了 Go 語言的類型系統和自定義類型,僅使用類型對數據進行抽象和封裝還是不夠的 ,接下來介紹 Go 語言的類型方法。Go 語言的類型方法是一種對類型行為的封裝。Go 語言的方法非常純粹,可以看作特殊類型的函數,其顯式地將對象實例或指針作為函數的第一個參數,并且參數名可以自己指定,而不強制要求一定是 this 或 self。這個對象實例或指針稱為方法的接收者(reciever),換句話說就是關鍵字 func 和函數名之間的參數被稱作接收者,將函數與接收者的類型綁在一起。如果一個函數有接收者,這個函數就被稱為方法。Go 語言里有兩種類型的接收者:值接收者和指針接收者。

      定義方法的語法格式如下:

      //類型方法接收者是值類型 func (t TypeName) MethodName(ParamList) (Returnlist) {//method body }//類型方法接收者是指針 func (t *TypeName) MethodName(ParamList) (Returnlist) {//method body }

      其中,t 是接收者,可以自由指定名稱。TypeName 為命名類型的類型名。MethodName為方法名,是一個自定義標識符。ParamList 和 ReturnList 分別是形參列表和返回值列表。Go 語言的類型方法本質上就是一個函數,沒有使用隱式的指針,這是 Go 的優點,簡單明了。我們可以將類型的方法改寫為常規的函數。示例如下:

      //類型方法接收者是值類型 func TypName MethodName(t TypeName, otherParamList) (Returnlist) {//method body }//類型方法接收者是指針 func TypName MethodName(t *TypeName, otherParamList) (Returnlist) {//method body } 示例 type SliceInt []int func (s SliceInt) Sum() int {sum := 0for _, i := range s {sum += i}return sum }這個函數和上面的方法等價 func SliceInt_Sum(s SliceInt) int{sum := 0for _, i := range s {sum += i}return sum }

      類型方法有如下特點 :

      (1) 可以為命名類型增加方法(除了接口),非命名類型不能自定義方法。比如不能為?[]int 類型增加方法,因為?[]int 是非命名類型。命名接口類型本身就是一個方法的簽名集合,所以不能為其增加具體的實現方法。

      (2) 為類型增加方法有一個限制,就是方法的定義必須和類型的定義在同一個包中。不能再為 int bool?等預聲明類型增加方法,因為它們是命名類型,但它們是 Go 語言內 置的預聲明類型,作用域是全局的,為這些類型新增的方法是在某個包中,所以 Go 編譯器拒絕為 int 增加方法 。

      (3) 方法的命名空間的可見性和變量一樣,大寫開頭的方法可以在包外被訪問,否則只能在包內可見。

      (4) 使用 type 定義的自定義類型是一個新類型,新類型不能調用原有類型的方法,但是底層類型支持的運算可以被新類型繼承。type Map map[string]string,這里的新類型 Map 可以使用底層類型支持的 range 運算。type MyInt int,這里的新類型 MyInt 仍然支持加減乘除運算。

      ?

      ?

      類型的本質

      在聲明一個新類型之后,聲明一個該類型的方法之前,需要先回答一個問題:這個類型的本質是什么。如果給這個類型增加或者刪除某個值,是要創建一個新值,還是要更改當前的值?如果是要創建一個新值,該類型的方法就使用值接收者。如果是要修改當前值,就使用指針接收者。這個答案也會影響程序內部傳遞這個類型的值的方式:是按值做傳遞,還是按指針做傳遞。保持傳遞的一致性很重要。這個背后的原則是,不要只關注某個方法是如何處理這個值,而是要關注這個值的本質是什么。

      ?

      內置類型

      內置類型是由語言提供的一組類型。我們已經見過這些類型,分別是數值類型、字符串類型和布爾類型。這些類型本質上是原始的類型。因此,當對這些值進行增加或者刪除的時候,會創建一個新值。基于這個結論,當把這些類型的值傳遞給方法或者函數時,應該傳遞一個對應值的副本。

      ?

      引用類型

      Go 語言里的引用類型有如下幾個:切片、map、通道、接口和函數類型。當聲明上述類型的變量時,創建的變量被稱作標頭(header)值。從技術細節上說,字符串也是一種引用類型。每個引用類型創建的標頭值是包含一個指向底層數據結構的指針。每個引用類型還包含一組獨特的字段,用于管理底層數據結構。因為標頭值是為復制而設計的,所以永遠不需要共享一個引用類型的值。標頭值里包含一個指針,因此通過復制來傳遞一個引用類型的值的副本,本質上就是在共享底層數據結構。

      ?

      結構類型

      結構類型可以用來描述一組數據值,這組值的本質即可以是原始的,也可以是非原始的。如果決定在某些東西需要刪除或者添加某個結構類型的值時該結構類型的值不應該被更改,那么需要遵守之前提到的內置類型和引用類型的規范。大多數情況下, 結構類型的本質并不是原始的,而是非原始的。這種情況下,對這個類型的值做增加或者刪除的操作應該更改值本身。當需要修改值本身時,在程序中其他地方,需要使用指針來共享這個值。

      ?

      ?

      法調用

      一般調用

      類型方法的一般調用方式:TypeinstanceName.MethodName(ParamList)

      TypeinstanceName 是類型實例名或指向實例的指針變量名。 MethodName 是類型方法名。 ParamList 是方法實參。type T struct{a int }func (t T) Get() int {return t.a }func (t *T) Set(i int) {t.a = i }var t = &T{} t.Set(2) t.Get()

      ?

      方法值(method value)

      變量 x 的靜態類型是 TM 是類型 T 的一個方法,x.T 被稱為方法值(method value)。 x.M 是一個函數類型變量,可以賦值給其他變量,并像普通的函數名一樣使用。例如:

      f := x.M f(參數列表) 等價于 x.M(參數列表)

      方法值(method value)其實是一個帶有閉包的函數變量,其底層實現原理和帶有閉包的匿名函數類似,接收值被隱式地綁定到方法值(method value)的閉包環境中。后續調用不需要再顯式地傳遞接收者。例如:

      type T struct{a int }func (t T) Get() int {return t.a }func (t *T) Set(i int) {t.a = i }func (t *T) Print() {fmt.Printf("%p, %v, %d \n", t, t, t.a) }var t = &T{} f := t.Set f(2) t.Print() ? f(3) t.Print()

      ?

      方法表達式(method expression

      方法表達式相當于提供一種語法將類型方法調用顯式地轉換為函數調用,接收者( receiver?必須顯式地傳遞進去。下面定義一個類型 T,增加兩個方法,方法 Get 的接收者為 T,方法 Set?的接收者類型為 *T。

      type T struct {a int }func (t *T) Set(i int) {t.a = i }func (t T) Get() int {return t.a }func (t *T) Print() {fmt.Printf("%p, %v, %d \n", t, t, t.a) }

      表達式 T.Get 和?(*T).Set 被稱為方法表達式method expression),方法表達式可以看作函數名,只不過這個函數的首個參數是接收者的實例或指針。T.Get 的函數簽名是 func (t T)int,(*T).Set 的函數簽名是 func(t?*Ti?int)。注意:這里的 T.Get 不能寫成(*T).Get,(*T).Set 也不能寫成 T.Set,在方法表達式中編譯器不會做自動轉換。例如:

      t := T{a:1} t.Get(t) (T).Get(t)f1 := T.Get; f1(t) f2 := (T).Get; f2(t)(*T).Set(&t, 1)f3 := (*T).Set; f3(&t, 1)

      通過方法值和方法表達式可以看到:Go 的方法底層是基于函數實現的,只是語法格式不同,本質是一樣的。

      ?

      方法集(method set

      命名類型方法接收者有兩種類型,一個是值類型,另一個是指針類型,這個和函數是一樣的,前者的形參是值類型,后者的形參是指針類型。無論接收者是什么類型,方法和函數的實參傳遞都是值拷貝。如果接收者是值類型,則傳遞的是值的副本;如果接收者是指針類型, 則傳遞的是指針的副本。例如:

      package mainimport "fmt"type Int intfunc (a Int) Max(b Int) Int {if a >= b {return a} else {return b} }func (i *Int) Set(a Int) {*i = a }func (i Int) Print() {fmt.Printf("value = %d\n", i) }func main() {var a Int = 10var b Int = 20c := a.Max(b)c.Print()(&c).Print()a.Set(20)a.Print()(&a).Set(30)a.Print() }

      上面示例定義了一個新類型 Int,新類型的底層類型是 int,Int 雖然不能繼承 int 的方法,但底層類型支持的操作(算術運算和賦值運算〉可以被上層類型繼承,這是 Go 類型系統的一個特點(前文也有提到)

      接收者是 Int 類型的方法集合: func (i Int) Print() func (a Int) Max(b Int) Int接收者是 *Int 類型的方法集合: func (i *Int)Set(a Int)

      為了簡化描述,將接收者為值類型 T 的方法的集合記錄為 S ,將接收者為指針類型?*T?的方法的集合統稱為?*S。從上面的示例可以看出,在直接使用類型實例調用類型的方法時,無論值類型變量還是指針類型變量,都可以調用類型的所有方法,原因是編譯器在編譯期間能夠識別出這種調用關系,做了自動的轉換。比如 a.Set() 使用值類型實例調用指針接收者方法,編譯器會自動將其轉換為(&a).Set()(&a).Print() 使用指針類型實例調用值類型接收者方法,編譯器自動將其轉化為a.Print()

      ?

      值調用和表達式調用的方法集

      具體類型實例變量直接調用其方法時,編譯器會所調用方法進行自動轉換,即使接收者是指針的方法,仍然可以使用值類型變量進行調用。下面討論在以下兩種情況下編譯器是否會進行方法的自動轉換。

      (1) 通過類型字面量顯式地進行值調用和表達式調用,可以看到在這種情況下編譯器不會做自動轉換,會進行嚴格的方法集檢查。例如:

      type Data struct {} func (Data) TestValue() {} func (*Data) TestPointer() {}顯示調用,編譯器不會進行方法集的自動轉換,編譯器會嚴格校驗方法集 (*Data)(&struct{}{}).TestPointer() (*Data)(&struct{}{}).TestValue()(Data)(struct{}{}).TestValue() Data.TestValue(struct{}{})

      ?

      (2) 通過類型變量進行值調用和表達式調用,在這種情況下,使用值調用方式調用時編譯器會進行自動轉換,使用表達式調用方式調用時編譯器不會進行轉換,會進行嚴格的方法集檢查。例如:

      type Data struct {} func (Data) TestValue() {} func (*Data) TestPointer() {}var a Data = struct{}{}// 表達式調用編譯器不會進行自動轉換 Data.TestValue(a) // Data.TestValue(&a) (*Data).TestPointer(&a) // Data.TestPointer(&a) ???// type Data has no method TestPointer// 值調用編譯器會進行自動轉換 f := a.TestValue f()y := (&a).TestValue ????// 編譯器會轉換成 a.TestValue y()g := a.TestPointer ??????// 會轉換成 (&a).TestPointer g()x := (&a).TestPointer x()

      ?

      ?

      組合和法集

      結構類型為 Go 提供了強大的類型擴展,主要體現在兩個方面:第一,struct 可以嵌入任意其他類型的字段;第二,struct 可以嵌套自身的指針類型的字段。這兩個特性決定了 struct 類型有著強大的表達力,幾乎可以表示任意的數據結構。同時,結合結構類型的方法,"數據+方法"?可以靈活地表達程序邏輯 。Go 語言的?struct 和 C 語言的 struct 一樣,內存分配按照字段順序依次開辟連續的存儲空間,沒有插入額外的東西(除字段對齊外),不像 C++ 那樣為了實現多態在對象內存模型里插入了虛擬函數指針,Go 語言的這種設計的優點使數據和邏輯徹底分離,對象內存區只存放數據,干凈簡單;類型的方法也是顯式帶上接收者,沒有像 C++ 一樣使用隱式的 this 指針,這是一種優秀的設計方法。 Go 中的數據就是數據,邏輯就是邏輯, 二者是 "正交"?的,底層實現上沒有相關性,在語言使用層又為開發者提供了統一的數據和邏輯抽象視圖,這種外部統一、內部隔離的面向對象設計是 Go 語言優秀設計的體現。

      ?

      組合

      從前面討論的命名類型的方法可知,使用 type 定義的新類型不會繼承原有類型的方法,有個特例就是命名結構類型,命名結構類型可以嵌套其他的命名類型的段,外層的結構類型是可以調用嵌入字段類型的方法,這種調用既可以是顯式的調用,也可以是隱式的調用。這就是?Go 的?"繼承",準確地說這就是 Go 的?"組合"。因為 Go 語言沒有繼承的語義,結構和字段之間是?"has a" 的關系,而不是?"is a" 的關系;沒有父子的概念,僅僅是整體和局部的概念,所以后續統稱這種嵌套的結構和字段的關系為組合。

      struct 中的組合非常靈活,可以表現為水平的宇段擴展,由于 struct 可以嵌套其他 struct 段,所以組合也可以分層次擴展。struct 類型中的字段稱為?"內嵌字段",內嵌字段的訪問和方法調用遵照的規約接下來進行展開

      ?

      內嵌字段的初始化和訪問

      struct 的字段訪問使用點操作符?".",struct 的字段可以嵌套很多層,只要內嵌的字段是唯一的即可,不需要使用全路徑進行訪問。

      package maintype X struct {a int }type Y struct {Xb int }type Z struct {Yc int }func main() {x := X{a: 1}y := Y{X:x,b:2,}z := Z{Y:y,c:3,}// z.a, z.Y.a, z.Y.X.a 三者是等價的,z.a z.Y.a 是 z.Y.X.a 的簡寫println(z.a, z.Y.a, z.Y.X.a)z = Z{}z.a = 2println(z.a, z.Y.a, z.Y.X.a) }

      在 struct 的多層嵌套中,不同嵌套層次可以有相同的段,此時最好使用完全路徑進行訪問和初始化。在實際數據結構的定義中應該盡量避開相同的段,以免在使用中出現歧義。例如:

      package maintype X struct {a int }type Y struct {Xa int }type Z struct {Ya int }func main() {x := X{a: 1}y := Y{X: x,a: 2,}z := Z{Y: y,a: 3,}println(z.a, z.Y.a, z.Y.X.a)z = Z{}z.a = 4z.Y.a = 5z.Y.X.a = 6println(z.a, z.Y.a, z.Y.X.a) }

      ?

      內嵌字段的方法調用

      struct 類型方法調用也使用點操作符,不同嵌套層次的段可以有相同的方法,外層變量調用內嵌字段的方法時也可以像嵌套字段的訪問一樣使用簡化模式。如果外層字段和內層字段有相同的方法,則使用簡化模式訪問外層的方法會覆蓋內層的方法。即在簡寫模式下,Go 編譯器優先從外向內逐層查找方法,同名方法中外層的方法能夠覆蓋內層的方法。這個特性有點類似于面向對象編程中,子類覆蓋父類的同名方法。示例如下:

      package mainimport "fmt"type X struct {a int }type Y struct {Xb int }type Z struct {Yc int }func (x X) Print(){fmt.Printf("In X, a = %d\n", x.a) }func (x X) XPrint(){fmt.Printf("In X, a = %d\n", x.a) }func (y Y) Print(){fmt.Printf("In Y, b = %d\n", y.b) }func (z Z) Print(){fmt.Printf("In Z, c = %d\n", z.c)z.Y.Print()z.Y.X.Print() }func main(){x := X{a: 1}y := Y{X: x,b: 2,}z := Z{Y: y,c: 3,}z.Print()z.XPrint()z.Y.XPrint() }

      不推薦在多層的 struct 類型中內嵌多個同名的字段;但是不反對 struct 定義和內嵌字段同名方法的用法,因為這提供了一種編程技術,使得 struct 能夠重寫 內嵌字段的方法,提供面向對象編程中子類覆蓋父類的同名方法的功能。

      ?

      組合的方法集

      組合結構的方法集有如下規則:

      (1) 若類型 S 包含匿名字段 T,則 S 的方法集包含 T 的方法集。

      (2) 若類型 S 包含匿名字段?*T,則 S 的方法集包含 T 和 *T 方法集。

      (3) 不管類型 S 中嵌入的匿名字段是 T 還是?*T*S?方法集總是包含 T 和?*T?方法集。

      package maintype X struct {a int }type Y struct {X }type Z struct {*X }func (x X) Get() int {return x.a }func (x *X) Set(i int) {x.a = i }func main() {x := X{a: 1}y := Y{X: x,}println(y.Get())// 此處編譯器做了自動轉換y.Set(2)println(y.Get())// 為了不讓編譯器做自動轉換,使用方法表達式調用方式// Y內嵌字段X,所以type Y的方法集是 Get, type *Y的方法集是Set Get(*Y).Set(&y, 3)// type Y的方法集合并沒有Set方法,所以下一句編譯不能通過// Y.Set(&y, 3)println(y.Get())z := Z{X: &x,}// 按照嵌套字段的方法集規則// Z 內嵌字段 *X,所以type Z和 type *Z方法集都包含類型X定義的方法Get和Setz.Set(z, 4)println(z.Get())(*Z).Set(&z, 5)println(z.Get()) }

      到目前為止還沒有發現方法集有多大的用途,而且通過實踐發現,Go 編譯器會進行自動轉換,看起來不需要太關注方法集,這種認識是錯誤的。編譯器的自動轉換僅適用于直接通過類型實例調用方法時才有效,類型實例傳遞給接口時,編譯器不會進行自動轉換,而是會進行嚴格的方法集校驗。

      Go 函數的調用實參都是值拷貝,方法調用參數傳遞也是一樣的機制,具體類型變量傳遞給接口時也是值拷貝,如果傳遞給接口變量的是值類型,但調用方法的接收者是指針類型,則程序運行時雖然能夠將接收者轉換為指針,但這個指針是副本的指針,并不是我們期望的原變量的指針。所以語言設計者為了杜絕這種非期望的行為,在編譯時做了嚴格的方法集合的檢查,不允許產生這種調用;如果傳遞給接口的變量是指針類型,則接口調用的是值類型的方法,程序運行時能夠自動轉換為值類型這種轉換不會帶來副作用,符合調用者的預期,所以這種轉換是允許的,而且這種情況符合方法集的規約。具體類型傳遞給接口時編譯器會進行嚴格的方法集校驗,掌握了方法集的概念在學習接口時非常有用。

      ?

      ?

      函數類型

      在對 Go 的類型系統做了全面的講解后,本節對函數類型進行全面深入的介紹。首先介紹 "有名函數"?和?"匿名函數"?兩個概念。使用 func FunctionName() 語法格式定義的函數我們稱為?"有名函數",這里所謂的有名是指函數在定義時指定了?"函數名";與之對應的是?"匿名函數",所謂的匿名函數就是在定義時使用 func() 語法格式,沒有指定函數名。通常所說的函數就是指?"有名函數"。函數類型也分兩種,一種是函數字面量類型(未命名類型),另一種是函數命名類型。

      ?

      函數字面量類型

      函數字面量類型的語法表達格式是 func(InputTypeList)OutputTypeList,可以看出 "有名函數" ?"匿名函數" 的類型都屬于函數字面量類型。有名函數的定義相當于初始化一個函數字面量類型后將其賦值給一個函數名變量:"匿名函數" 的定義也是直接初始化一個函數字面量類型,只是沒有綁定到一個具體變量上。從 Go 類型系統的角度來看,"有名函數" ?"匿名函數" 都是函數字面量類型的實例。

      ?

      函數命名類型

      從前面章節知道可以使用 type NewType OldType 語法定義一種新類型,這種類型都是命名類型,同理可以使用該方法定義一種新類型:函數命名類型,簡稱函數類型。例如:

      type NewFuncType FuncLiteral

      ?

      函數簽名

      有了上面的基礎,函數簽名就比較好理解了,所謂?"函數簽名"?就是?"有名函數"?或?"匿名函數"?的字面量類型。所以有名函數和匿名函數的函數簽名可以相同,函數簽名是函數的?"字面量類型",不包括函數名。

      ?

      函數聲明

      Go 語言沒有 C 語言中函數聲明的語義,準確地說,Go 代碼調用 Go 編寫的函數不需要聲明,可以直接調用,但 Go 調用匯編語言編寫的函數還是要使用函數聲明語句,示例如下。

      函數聲明?= 函數名?+ 函數簽名 函數簽名 func (InputTypeList)OutputTypeList函數聲明 func FuncName (InputTypeList)OutputTypeList有名函數定義,函數名是 add add 類型是函數字面量類型 func (int, int) int func add(a, b int) int {return a+b }函數聲明語句,用于 Go 代碼調用匯編代碼 func add(int, int) intadd函數的簽名,實際上就是add的字面量類型 func (int, int) int匿名函數不能獨立存在,常作為函數參數、返回值 匿名函數可以直接顯式初始化 匿名函數的類型也是函數字面量類型 func (int, int) int func (a, b int) int {return a+b }新定義函數類型ADD ADD底層類型是函數字面量類型 func (int, int) int type ADD func (int, int) intadd 和 ADD 的底層類型相同,并且 add 是字面量類型 所以 add 可直接賦值給 ADD 類型的變量 g var g ADD = addfunc main() {f := func(a, b int) int {return a+b}g(1, 2)f(1, 2)// 兩者的函數簽名相同fmt.Printf("%T\n", f)fmt.Printf("%T\n", add) }

      (1) 函數也是一種類型,可以在函數字面量類型的基礎上定義一種命名函數類型。

      (2) 有名函數和匿名函數的函數簽名與命名函數類型的底層類型相同,它們之間可以進行類型轉換。

      (3) 可以為有名函數類型添加方法,這種為一個函數類型添加方法的技法非常有價值,可以方便地為一個函數增加?"攔截" ?"過濾" 等額外功能,這提供了一種裝飾設計模式。

      (4) 為有名函數類型添加方法,使其與接口打通關系,使用接口的地方可以傳遞函數類型的變量,這為函數到接口的轉換開啟了大門。

      總結

      以上是生活随笔為你收集整理的Go 语言学习笔记(三):类型系统的全部內容,希望文章能夠幫你解決所遇到的問題。

      如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。