參考文章:https://time.geekbang.org/column/article/18035編程
package main import "fmt" // 示例1。 // AnimalCategory 表明動物分類學中的基本分類法。 type AnimalCategory struct { kingdom string // 界。 phylum string // 門。 class string // 綱。 order string // 目。 family string // 科。 genus string // 屬。 species string // 種。 } //這裏綁定,這個String方法不須要任何參數聲明,但須要有一個string類型的結果聲明。我在調用fmt.Printf函數時,使用佔位符%s和category值自己就能夠打印出後者的字符串表示形式,而無需顯式地調用它的String方法。 func (ac AnimalCategory) String() string { return fmt.Sprintf("%s%s%s%s%s%s%s", ac.kingdom, ac.phylum, ac.class, ac.order, ac.family, ac.genus, ac.species) } // 示例2。 type Animal struct { scientificName string // 學名。 AnimalCategory // 動物基本分類。這是一個嵌入字段 } // 該方法會"屏蔽"掉嵌入字段中的同名方法。 // func (a Animal) String() string { // return fmt.Sprintf("%s (category: %s)", //Sprintf輸出格式化的字符串 // a.scientificName, a.AnimalCategory) // } // 示例3。 type Cat struct { name string Animal } // 該方法會"屏蔽"掉嵌入字段中的同名方法。 // func (cat Cat) String() string { // return fmt.Sprintf("%s (category: %s, name: %q)", // cat.scientificName, cat.Animal.AnimalCategory, cat.name) // } func main() { // 示例1。species字段指定了字符串值"cat",fmt.Printf函數會本身去尋找它。此時的打印內容會是The animal category: cat。 category := AnimalCategory{species: "cat"} fmt.Printf("The animal category: %s\n", category) // 示例2。 animal := Animal{ scientificName: "American Shorthair", AnimalCategory: category, } fmt.Printf("The animal: %s\n", animal) // 示例3。 cat := Cat{ name: "little pig", Animal: animal, } fmt.Printf("The cat: %s\n", cat) }
go run demo29.go The animal category: cat The animal: cat The cat: cat
由於: cat的string()方法: func (cat Cat) String() string 和Animal的string方法: func (a Animal) String() string 都被註釋掉了,執行腳本的時候,當示例2和示例3調用fmt.Printf()方法,就只能使用func (ac AnimalCategory) String() string 方法打印7個字段了。若是不註釋,後邊的string方法會覆蓋前面的string方法,打印結果發生變化。dom
package main import "fmt" // 示例1。 // AnimalCategory 表明動物分類學中的基本分類法。 type AnimalCategory struct { kingdom string // 界。 phylum string // 門。 class string // 綱。 order string // 目。 family string // 科。 genus string // 屬。 species string // 種。 } //這裏綁定,這個String方法不須要任何參數聲明,但須要有一個string類型的結果聲明。我在調用fmt.Printf函數時,使用佔位符%s和category值自己就能夠打印出後者的字符串表示形式,而無需顯式地調用它的String方法。 func (ac AnimalCategory) String() string { return fmt.Sprintf("%s%s%s%s%s%s%s", ac.kingdom, ac.phylum, ac.class, ac.order, ac.family, ac.genus, ac.species) } // 示例2。 type Animal struct { scientificName string // 學名。 AnimalCategory // 動物基本分類。這是一個嵌入字段 } // 該方法會"屏蔽"掉嵌入字段中的同名方法。 func (a Animal) String() string { return fmt.Sprintf("%s (category: %s)", //Sprintf輸出格式化的字符串 a.scientificName, a.AnimalCategory) } // 示例3。 type Cat struct { name string Animal } // 該方法會"屏蔽"掉嵌入字段中的同名方法。 func (cat Cat) String() string { return fmt.Sprintf("%s (category: %s, name: %q)", cat.scientificName, cat.Animal.AnimalCategory, cat.name) } func main() { // 示例1。species字段指定了字符串值"cat",fmt.Printf函數會本身去尋找它。此時的打印內容會是The animal category: cat。 category := AnimalCategory{species: "cat"} fmt.Printf("The animal category: %s\n", category) // 示例2。 //使用fmt.Printf函數和%s佔位符試圖打印animal的字符串表示形式,至關於調用animal的String方法。 //雖然咱們尚未爲Animal類型編寫String方法,但這樣作是沒問題的。 //由於在這裏,嵌入字段AnimalCategory的String方法會被當作animal的方法調用。 //那若是我也爲Animal類型編寫一個String方法呢?這裏會調用哪個呢? //animal的String方法會被調用。這時,咱們說,嵌入字段AnimalCategory的String方法被「屏蔽」了。注意,只要名稱相同,不管這兩個方法的簽名是否一致,被嵌入類型的方法都會「屏蔽」掉嵌入字段的同名方法。 animal := Animal{ scientificName: "American Shorthair", AnimalCategory: category, } fmt.Printf("The animal: %s\n", animal) // 示例3。 cat := Cat{ name: "little pig", Animal: animal, } //當咱們調用Cat類型值的String方法時,若是該類型確有String方法,那麼嵌入字段Animal和AnimalCategory的String方法都會被「屏蔽」。 fmt.Printf("The cat: %s\n", cat) }
daixuandeMacBook-Pro:q0 daixuan$ go run demo29.go The animal category: cat The animal: American Shorthair (category: cat) The cat: American Shorthair (category: cat, name: "little pig")
一個數據類型關聯的全部方法,共同組成了該類型的方法集合。同一個方法集合中的方法不能出現重名。而且,若是它們所屬的是一個結構體類型,那麼它們的名稱與該類型中任何字段的名稱也不能重複。ide
咱們能夠把結構體類型中的一個字段看做是它的一個屬性或者一項數據,再把隸屬於它的一個方法看做是附加在其中數據之上的一個能力或者一項操做。將屬性及其能力(或者說數據及其操做)封裝在一塊兒,是面向對象編程函數
字段聲明AnimalCategory表明了Animal類型的一個嵌入字段。Go 語言規範規定,若是一個字段的聲明中只有字段的類型名而沒有字段的名稱,那麼它就是一個嵌入字段,也能夠被稱爲匿名字段。咱們能夠經過此類型變量的名稱後跟「.」,再後跟嵌入字段類型的方式引用到該字段。也就是說,嵌入字段的類型既是類型也是名稱。優化
說到引用結構體的嵌入字段,Animal類型有個方法叫Category,它是這麼寫的:3d
func (a Animal) Category() string { return a.AnimalCategory.String()}
Category方法的接收者類型是Animal,接收者名稱是a。在該方法中,我經過表達式a.AnimalCategory選擇到了a的這個嵌入字段,而後又選擇了該字段的String方法並調用了它。指針
順便提一下,在某個表明變量的標識符的右邊加「.」,再加上字段名或方法名的表達式被稱爲選擇表達式,它用來表示選擇了該變量的某個字段或者方法。code
實際上,把一個結構體類型嵌入到另外一個結構體類型中的意義不止如此。嵌入字段的方法集合會被無條件地合併進被嵌入類型的方法集合中。例以下面這種:對象
animal := Animal{ scientificName: "American Shorthair", AnimalCategory: category, } fmt.Printf("The animal: %s\n", animal)
我聲明瞭一個Animal類型的變量animal並對它進行初始化。我把字符串值"American Shorthair"賦給它的字段scientificName,並把前面聲明過的變量category賦給它的嵌入字段AnimalCategory。blog
我在後面使用fmt.Printf函數和%s佔位符試圖打印animal的字符串表示形式,至關於調用animal的String方法。雖然咱們尚未爲Animal類型編寫String方法,但這樣作是沒問題的。由於在這裏,嵌入字段AnimalCategory的String方法會被當作animal的方法調用。
答案是,animal的String方法會被調用。這時,咱們說,嵌入字段AnimalCategory的String方法被「屏蔽」了。注意,只要名稱相同,不管這兩個方法的簽名是否一致,被嵌入類型的方法都會「屏蔽」掉嵌入字段的同名方法。
因爲咱們一樣能夠像訪問被嵌入類型的字段那樣,直接訪問嵌入字段的字段,因此若是這兩個結構體類型裏存在同名的字段,那麼嵌入字段中的那個字段必定會被「屏蔽」。這與咱們在前面講過的,可重名變量之間可能存在的「屏蔽」現象很類似
由於嵌入字段的字段和方法均可以「嫁接」到被嵌入類型上,因此即便在兩個同名的成員一個是字段,另外一個是方法的狀況下,這種「屏蔽」現象依然會存在。
不過,即便被屏蔽了,咱們仍然能夠經過鏈式的選擇表達式,選擇到嵌入字段的字段或方法,就像我在Category方法中所作的那樣。這種「屏蔽」其實還帶來了一些好處。咱們看看下面這個Animal類型的String方法的實現:
func (a Animal) String() string { return fmt.Sprintf("%s (category: %s)", a.scientificName, a.AnimalCategory) }
咱們把對嵌入字段的String方法的調用結果融入到了Animal類型的同名方法的結果中。這種將同名方法的結果逐層「包裝」的手法是很常見和有用的,也算是一種慣用法了。
結構體類型中的嵌入字段
最後,我還要提一下多層嵌入的問題。也就是說,嵌入字段自己也有嵌入字段的狀況。請看我聲明的Cat類型:
type Cat struct { name string Animal } func (cat Cat) String() string { return fmt.Sprintf("%s (category: %s, name: %q)", cat.scientificName, cat.Animal.AnimalCategory, cat.name) }
結構體類型Cat中有一個嵌入字段Animal,而Animal類型還有一個嵌入字段AnimalCategory。在這種狀況下,「屏蔽」現象會以嵌入的層級爲依據,嵌入層級越深的字段或方法越可能被「屏蔽」。
例如,當咱們調用Cat類型值的String方法時,若是該類型確有String方法,那麼嵌入字段Animal和AnimalCategory的String方法都會被「屏蔽」。
若是該類型沒有String方法,那麼嵌入字段Animal的String方法會被調用,而它的嵌入字段AnimalCategory的String方法仍然會被屏蔽。
只有當Cat類型和Animal類型都沒有String方法的時候,AnimalCategory的String方法菜會被調用。
最後的最後,若是處於同一個層級的多個嵌入字段擁有同名的字段或方法,那麼從被嵌入類型的值那裏,選擇此名稱的時候就會引起一個編譯錯誤,由於編譯器沒法肯定被選擇的成員究竟是哪個。
Go 語言中根本沒有繼承的概念,它所作的是經過嵌入字段的方式實現了類型之間的組合
向對象編程中的繼承,實際上是經過犧牲必定的代碼簡潔性來換取可擴展性,並且這種可擴展性是經過侵入的方式來實現的。
類型之間的組合採用的是非聲明的方式,咱們不須要顯式地聲明某個類型實現了某個接口,或者一個類型繼承了另外一個類型。
同時,類型組合也是非侵入式的,它不會破壞類型的封裝或加劇類型之間的耦合。
咱們要作的只是把類型當作字段嵌入進來,而後不勞而獲地使用嵌入字段所擁有的一切。若是嵌入字段有哪裏不合心意,咱們還能夠用「包裝」或「屏蔽」的方式去調整和優化。
另外,類型間的組合也是靈活的,咱們老是能夠經過嵌入字段的方式把一個類型的屬性和能力「嫁接」給另外一個類型。
這時候,被嵌入類型也就天然而然地實現了嵌入字段所實現的接口。再者,組合要比繼承更加簡潔和清晰,Go 語言能夠垂手可得地經過嵌入多個字段來實現功能強大的類型,卻不會有多重繼承那樣複雜的層次結構和可觀的管理成本。
接口類型之間也能夠組合。在 Go 語言中,接口類型之間的組合甚至更加常見,咱們經常以此來擴展接口定義的行爲或者標記接口的特徵。與此有關的內容我在下一篇文章中再講
方法的接收者類型必須是某個自定義的數據類型,並且不能是接口類型或接口的指針類型。所謂的值方法,就是接收者類型是非指針的自定義數據類型的方法。
好比,咱們在前面爲AnimalCategory、Animal以及Cat類型聲明的那些方法都是值方法。就拿Cat來講,它的String方法的接收者類型就是Cat,一個非指針類型。那什麼叫指針類型呢?請看這個方法:
func (cat *Cat) SetName(name string) { cat.name = name }
方法SetName的接收者類型是Cat。Cat左邊再加個表明的就是Cat類型的指針類型。
這時,Cat能夠被叫作*Cat的基本類型。你能夠認爲這種指針類型的值表示的是指向某個基本類型值的指針。
們能夠經過把取值操做符*放在這樣一個指針值的左邊來組成一個取值表達式,以獲取該指針值指向的基本類型值,也能夠經過把取址操做符&放在一個可尋址的基本類型值的左邊來組成一個取址表達式,以獲取該基本類型值的指針值。
所謂的指針方法,就是接收者類型是上述指針類型的方法。
值方法的接收者是該方法所屬的那個類型值的一個副本。咱們在該方法內對該副本的修改通常都不會體如今原值上,除非這個類型自己是某個引用類型(好比切片或字典)的別名類型。而指針方法的接收者,是該方法所屬的那個基本類型值的指針值的一個副本。咱們在這樣的方法內對該副本指向的值進行修改,卻必定會體如今原值上。
一個自定義數據類型的方法集合中僅會包含它的全部值方法,而該類型的指針類型的方法集合卻囊括了前者的全部方法,包括全部值方法和全部指針方法。嚴格來說,咱們在這樣的基本類型的值上只能調用到它的值方法。可是,Go 語言會適時地爲咱們進行自動地轉譯,使得咱們在這樣的值上也能調用到它的指針方法。好比,在Cat類型的變量cat之上,之因此咱們能夠經過cat.SetName("monster")修改貓的名字,是由於 Go 語言把它自動轉譯爲了(&cat).SetName("monster"),即:先取cat的指針值,而後在該指針值上調用SetName方法。
在後邊你會了解到,一個類型的方法集合中有哪些方法與它能實現哪些接口類型是息息相關的。若是一個基本類型和它的指針類型的方法集合是不一樣的,那麼它們具體實現的接口類型的數量就也會有差別,除非這兩個數量都是零。好比,一個指針類型實現了某某接口類型,但它的基本類型卻不必定可以做爲該接口的實現類型。
package main import "fmt" type Cat struct { name string // 名字。 scientificName string // 學名。 category string // 動物學基本分類。 } func New(name, scientificName, category string) Cat { return Cat{ name: name, scientificName: scientificName, category: category, } } func (cat *Cat) SetName(name string) { cat.name = name } func (cat Cat) SetNameOfCopy(name string) { cat.name = name } func (cat Cat) Name() string { return cat.name } func (cat Cat) ScientificName() string { return cat.scientificName } func (cat Cat) Category() string { return cat.category } func (cat Cat) String() string { return fmt.Sprintf("%s (category: %s, name: %q)", cat.scientificName, cat.category, cat.name) } func main() { cat := New("little pig", "American Shorthair", "cat") cat.SetName("monster") // (&cat).SetName("monster") fmt.Printf("The cat: %s\n", cat) cat.SetNameOfCopy("little pig") fmt.Printf("The cat: %s\n", cat) type Pet interface { SetName(name string) Name() string Category() string ScientificName() string } _, ok := interface{}(cat).(Pet) fmt.Printf("Cat implements interface Pet: %v\n", ok) _, ok = interface{}(&cat).(Pet) fmt.Printf("*Cat implements interface Pet: %v\n", ok) }
go run demo30.go The cat: American Shorthair (category: cat, name: "monster") The cat: American Shorthair (category: cat, name: "monster") Cat implements interface Pet: false *Cat implements interface Pet: true
結構體類型的嵌入字段比較容易讓 Go 語言新手們迷惑,因此我在本篇文章着重解釋了它的編寫方法、基本的特性和規則以及更深層次的含義。在理解告終構體類型及其方法的組成方式和構造套路以後,這些知識應該是你重點掌握的。
嵌入字段是其聲明中只有類型而沒有名稱的字段,它能夠以一種很天然的方式爲被嵌入的類型帶來新的屬性和能力。在通常狀況下,咱們用簡單的選擇表達式就能夠直接引用到它們的字段和方法。
不過,咱們須要當心可能產生「屏蔽」現象的地方,尤爲是當存在多個嵌入字段或者多層嵌入的時候。「屏蔽」現象可能會讓你的實際引用與你的預期不符
另外,你必定要梳理清楚值方法和指針方法的不一樣之處,包括這兩種方法各自能作什麼、不能作什麼以及會影響到其所屬類型的哪些方面。這涉及值的修改、方法集合和接口實現。
最後,再次強調,嵌入字段是實現類型間組合的一種方式,這與繼承沒有半點兒關係。Go 語言雖然支持面向對象編程,可是根本就沒有「繼承」這個概念。