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

歡迎訪問 生活随笔!

生活随笔

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

windows

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

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

目錄

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

? ? ? ??類型方法

? ? ? ??組合和方法集

? ? ? ??函數(shù)類型


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

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

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

?

?

命名類型和未命名類型

命名類型( Named Type )

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

?

未命名類型 ( Unamed Type )

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

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 (底層類型)。底層類型的規(guī)則如下:

(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) 兩個未命名類型相同的條件是它們的類型聲明宇面量的結(jié)構(gòu)相同,井且內(nèi)部元素的類型相同。

(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() }

?

?

類型強制轉(zhuǎn)換

由于 Go 是強類型的語言,如果不滿足自動轉(zhuǎn)換的條件,則必須進行強制類型轉(zhuǎn)換。任意兩個不相干的類型如果進行強制轉(zhuǎn)換,則必須符合一定的規(guī)則。強制類型的語法格式: var a T?= (T) (b),使用括號將類型和要轉(zhuǎn)換的變量或表達式的值括起來。非常量類型的變量 x 可以強制轉(zhuǎn)化并傳遞給類型 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 都是復數(shù)類型。

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

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

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) 數(shù)值類型和 string 類型之間的相互轉(zhuǎn)換可能造成值部分丟失;其他的轉(zhuǎn)換僅是類型的轉(zhuǎn)換,不會造成值的改變。string 和數(shù)字之間的轉(zhuǎn)換可使用標準庫?strconv。

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

?

自定義類型

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

?

自定義 struct 類型

struct 類型是?Go 語言自定義類型的普遍的形式,是 Go 語言類型擴展的基石,也是 Go 語言面向?qū)ο蟪休d的基礎。struct 初始化:

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

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

?

結(jié)構(gòu)字段的特點

結(jié)構(gòu)的字段可以是任意的類型,基本類型、接口類型、指針類型、函數(shù)類型都可以作為 struct 的字段。結(jié)構(gòu)字段的類型名必須唯一 ,struct 字段類型可以是普通類型,也可以是指針。另外,結(jié)構(gòu)支持內(nèi)嵌自身的指針,這也是實現(xiàn)樹形和鏈表等復雜數(shù)據(jù)結(jié)構(gòu)的基礎。例如:

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

?

匿名字段

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

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

?

?

類型

為類型增加方法是 Go 語言實現(xiàn)面向?qū)ο缶幊痰幕A,在介紹類型方法之前先介紹自定義類型。方法能給用戶定義的類型添加新的行為。方法實際上也是函數(shù),只是在聲明時,在關(guān)鍵字 func 和方法名之間增加了一個參數(shù)。

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

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

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

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

//類型方法接收者是值類型 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 }這個函數(shù)和上面的方法等價 func SliceInt_Sum(s SliceInt) int{sum := 0for _, i := range s {sum += i}return sum }

類型方法有如下特點 :

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

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

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

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

?

?

類型的本質(zhì)

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

?

內(nèi)置類型

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

?

引用類型

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

?

結(jié)構(gòu)類型

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

?

?

法調(diào)用

一般調(diào)用

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

TypeinstanceName 是類型實例名或指向?qū)嵗闹羔樧兞棵?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 的靜態(tài)類型是 TM 是類型 T 的一個方法,x.T 被稱為方法值(method value)。 x.M 是一個函數(shù)類型變量,可以賦值給其他變量,并像普通的函數(shù)名一樣使用。例如:

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

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

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

方法表達式相當于提供一種語法將類型方法調(diào)用顯式地轉(zhuǎn)換為函數(shù)調(diào)用,接收者( 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),方法表達式可以看作函數(shù)名,只不過這個函數(shù)的首個參數(shù)是接收者的實例或指針。T.Get 的函數(shù)簽名是 func (t T)int,(*T).Set 的函數(shù)簽名是 func(t?*Ti?int)。注意:這里的 T.Get 不能寫成(*T).Get,(*T).Set 也不能寫成 T.Set,在方法表達式中編譯器不會做自動轉(zhuǎn)換。例如:

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 的方法底層是基于函數(shù)實現(xiàn)的,只是語法格式不同,本質(zhì)是一樣的。

?

方法集(method set

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

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 的方法,但底層類型支持的操作(算術(shù)運算和賦值運算〉可以被上層類型繼承,這是 Go 類型系統(tǒng)的一個特點(前文也有提到)

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

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

?

值調(diào)用和表達式調(diào)用的方法集

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

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

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

?

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

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

?

?

組合和法集

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

?

組合

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

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

?

內(nèi)嵌字段的初始化和訪問

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

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 的多層嵌套中,不同嵌套層次可以有相同的段,此時最好使用完全路徑進行訪問和初始化。在實際數(shù)據(jù)結(jié)構(gòu)的定義中應該盡量避開相同的段,以免在使用中出現(xiàn)歧義。例如:

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) }

?

內(nèi)嵌字段的方法調(diào)用

struct 類型方法調(diào)用也使用點操作符,不同嵌套層次的段可以有相同的方法,外層變量調(diào)用內(nèi)嵌字段的方法時也可以像嵌套字段的訪問一樣使用簡化模式。如果外層字段和內(nèi)層字段有相同的方法,則使用簡化模式訪問外層的方法會覆蓋內(nèi)層的方法。即在簡寫模式下,Go 編譯器優(yōu)先從外向內(nèi)逐層查找方法,同名方法中外層的方法能夠覆蓋內(nèi)層的方法。這個特性有點類似于面向?qū)ο缶幊讨?#xff0c;子類覆蓋父類的同名方法。示例如下:

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 類型中內(nèi)嵌多個同名的字段;但是不反對 struct 定義和內(nèi)嵌字段同名方法的用法,因為這提供了一種編程技術(shù),使得 struct 能夠重寫 內(nèi)嵌字段的方法,提供面向?qū)ο缶幊讨凶宇惛采w父類的同名方法的功能。

?

組合的方法集

組合結(jié)構(gòu)的方法集有如下規(guī)則:

(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())// 此處編譯器做了自動轉(zhuǎn)換y.Set(2)println(y.Get())// 為了不讓編譯器做自動轉(zhuǎn)換,使用方法表達式調(diào)用方式// Y內(nèi)嵌字段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,}// 按照嵌套字段的方法集規(guī)則// Z 內(nèi)嵌字段 *X,所以type Z和 type *Z方法集都包含類型X定義的方法Get和Setz.Set(z, 4)println(z.Get())(*Z).Set(&z, 5)println(z.Get()) }

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

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

?

?

函數(shù)類型

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

?

函數(shù)字面量類型

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

?

函數(shù)命名類型

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

type NewFuncType FuncLiteral

?

函數(shù)簽名

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

?

函數(shù)聲明

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

函數(shù)聲明?= 函數(shù)名?+ 函數(shù)簽名 函數(shù)簽名 func (InputTypeList)OutputTypeList函數(shù)聲明 func FuncName (InputTypeList)OutputTypeList有名函數(shù)定義,函數(shù)名是 add add 類型是函數(shù)字面量類型 func (int, int) int func add(a, b int) int {return a+b }函數(shù)聲明語句,用于 Go 代碼調(diào)用匯編代碼 func add(int, int) intadd函數(shù)的簽名,實際上就是add的字面量類型 func (int, int) int匿名函數(shù)不能獨立存在,常作為函數(shù)參數(shù)、返回值 匿名函數(shù)可以直接顯式初始化 匿名函數(shù)的類型也是函數(shù)字面量類型 func (int, int) int func (a, b int) int {return a+b }新定義函數(shù)類型ADD ADD底層類型是函數(shù)字面量類型 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)// 兩者的函數(shù)簽名相同fmt.Printf("%T\n", f)fmt.Printf("%T\n", add) }

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

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

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

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

總結(jié)

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

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。