詳解go如何優(yōu)雅的使用接口與繼承
引言
Go語(yǔ)言中的接口和嵌套結(jié)構(gòu)體是兩種重要的代碼設(shè)計(jì)方式。接口定義了一組方法簽名,使得不同的類型能夠以相同的方式進(jìn)行交互。而嵌套結(jié)構(gòu)體則像面向?qū)ο缶幊讨械睦^承,允許基于已有類型定義新的類型,并自動(dòng)繼承其字段和方法。對(duì)于推薦的Go編程習(xí)慣是優(yōu)先使用接口和組合,而避免過多的嵌套,以降低代碼耦合,增強(qiáng)可維護(hù)性
Go接口的使用
在Go語(yǔ)言中,接口(interface)是定義一組方法的集合,任何對(duì)象只要實(shí)現(xiàn)了這些方法就是這個(gè)接口的實(shí)現(xiàn)類
- 定義接口:接口通過關(guān)鍵字interface來定義,其后跟隨一對(duì)花括號(hào),包含一系列方法的聲明
- 實(shí)現(xiàn)接口:在Go語(yǔ)言中,接口的實(shí)現(xiàn)是隱式的,只要一個(gè)類型包含了接口中所需要的所有方法,那么這個(gè)類型就算實(shí)現(xiàn)了這個(gè)接口
- 使用接口變量:接口變量可以存儲(chǔ)所有實(shí)現(xiàn)了該接口的實(shí)例
- 空接口:空接口interface{},可以接收任何類型的值,因此可以用來實(shí)現(xiàn)通用函數(shù)
代碼示例:
package main import ( "fmt" ) // 加法接口 type Adder interface { Add(a, b int) int } // 實(shí)現(xiàn)加法的類 type Calc struct{} // 實(shí)現(xiàn)Adder接口的Add方法 func (c Calc) Add(a, b int) int { return a + b } func main() { var a Adder c := Calc{} a = c result := a.Add(1, 2) fmt.Println(result) }
接口核心要點(diǎn)
- 在 Go 語(yǔ)言中,如果一個(gè)接口定義了多個(gè)方法,而某個(gè)結(jié)構(gòu)體只實(shí)現(xiàn)了這個(gè)接口的部分方法,那么這個(gè)結(jié)構(gòu)體不算是這個(gè)接口的實(shí)現(xiàn)者
- Go語(yǔ)言的接口是靜態(tài)的,一旦定義就不能再添加或刪除方法,對(duì)于需要?jiǎng)討B(tài)改變的需求可能無(wú)法滿足
- 接口未初始化時(shí),其值為nil,調(diào)用其方法會(huì)引發(fā)panic。接口值可以被認(rèn)為是包含兩個(gè)部分的元組(tuple),一個(gè)具體類型和該類型的值。當(dāng)我們聲明了一個(gè)接口值卻沒有進(jìn)行初始化(也就是沒有賦值),那么這個(gè)接口的類型和值都是nil。在Go中,調(diào)用值為nil的函數(shù)是非法的,所以如果嘗試在這樣的接口值上調(diào)用方法,那么程序?qū)⒂|發(fā)運(yùn)行時(shí)錯(cuò)誤
type MyInterface interface { MyMethod() } func main() { var mi MyInterface // 聲明一個(gè)接口,但未進(jìn)行初始化 mi.MyMethod() // 運(yùn)行時(shí)錯(cuò)誤:在nil的接口值上調(diào)用方法 }
如果接口的值為nil,那么我們可以通過類型斷言來判斷接口的動(dòng)態(tài)類型是否為nil,從而避免panic
type MyInterface interface { MyMethod() } func main() { var mi MyInterface // 聲明一個(gè)接口,但未進(jìn)行初始化 if mi != nil { mi.MyMethod() } }
讓接口值包含了一個(gè)具體的類型,但是該類型的值為nil,那么是完全合法的,因?yàn)轭愋托畔⑷匀淮嬖?/p>
type MyInterface interface { MyMethod() } type MyType struct{} func (mt *MyType) MyMethod() { if mt == nil { fmt.Println("nil receiver value") } } func main() { var mt *MyType //聲明并初始化為nil值 var mi MyInterface = mt //將nil值賦給接口 mi.MyMethod() // "nil receiver value" }
- 空接口(interface{}) 和 類型斷言: Go語(yǔ)言中,空接口(interface{})可以表示任何類型,是Go語(yǔ)言中的一種“萬(wàn)能”類型。由于空接口可以表示任何類型,因此我們?nèi)绻淮_定關(guān)于接口值具體類型,就需要使用類型斷言來判斷接口值的類型
var i interface{} = "a string" s := i.(string) // 類型斷言 fmt.Println(s) s, ok := i.(string) if ok { fmt.Println(s) } else { // 當(dāng)類型斷言失敗時(shí),可以做一些別的操作 fmt.Println("Not a string") }
每個(gè)類型實(shí)現(xiàn)的接口并不需要在該類型中顯式聲明。這可能導(dǎo)致開發(fā)人員在不經(jīng)意間“實(shí)現(xiàn)”了一個(gè)接口,然后當(dāng)該接口發(fā)生更改時(shí),損壞現(xiàn)有的代碼??偟膩碚f,Go的這種接口實(shí)現(xiàn)方式提供了很大的靈活性,但也可能帶來隱藏的陷阱
Go語(yǔ)言的接口中并沒有標(biāo)識(shí)符,如果一個(gè)類型實(shí)現(xiàn)了多個(gè)接口,并且這些接口中有同名的方法,可能會(huì)造成某些問題
Go的接口是隱式實(shí)現(xiàn)的,只要類型實(shí)現(xiàn)的方法滿足接口定義,那么就算實(shí)現(xiàn)了該接口,不需要顯式地聲明這一點(diǎn)
type InterfaceA interface { DoSomething() } type InterfaceB interface { DoSomething() }
type MyType struct{} func (m MyType) DoSomething() { fmt.Println("Doing something") }
在這個(gè)例子中,MyType鐘都實(shí)現(xiàn)了InterfaceA和InterfaceB,即使它們都有一個(gè)同名的方法DoSomething
然而,如果接口有同名但簽名不同的方法,那么該類型就不能同時(shí)實(shí)現(xiàn)這兩個(gè)接口
type InterfaceA interface { DoSomething(int) } type InterfaceB interface { DoSomething(string) }
在這種情況下,你不能讓一個(gè)類型同時(shí)實(shí)現(xiàn)這兩個(gè)接口,因?yàn)闊o(wú)法確保一個(gè)方法同時(shí)滿足兩個(gè)簽名。要解決這個(gè)問題,你可以讓你的類型實(shí)現(xiàn)一個(gè)方法,該方法接受一個(gè)空接口參數(shù)(可以接受任何類型),然后在方法內(nèi)部檢查并處理不同類型的參數(shù):
type MyType struct{} func (m MyType) DoSomething(value interface{}) { switch v := value.(type) { case int: fmt.Printf("Doing something with int: %d\n", v) case string: fmt.Printf("Doing something with string: %s\n", v) default: fmt.Printf("Don't know how to do something with %T\n", v) } }
這種解決辦法并不完美,它會(huì)使代碼變得復(fù)雜且難以理解,因此盡可能地避免在不同的接口中使用同名但簽名不同的方法。在設(shè)計(jì)接口時(shí),應(yīng)盡量保證接口的方法是唯一的,并且清晰地表達(dá)了其行為
Go繼承的使用
Go語(yǔ)言的繼承是通過字段嵌套的方式實(shí)現(xiàn)的,所謂嵌套,是指一個(gè)結(jié)構(gòu)體作為另一個(gè)結(jié)構(gòu)體的字段。內(nèi)嵌結(jié)構(gòu)體的所有字段和方法都成為了外部結(jié)構(gòu)體的成員
- 定義被嵌套的結(jié)構(gòu)體:被繼承的結(jié)構(gòu)體需要先定義,包含一系列字段和方法
- 定義繼承結(jié)構(gòu)體:在新定義的結(jié)構(gòu)體中,包含被繼承的結(jié)構(gòu)體作為匿名字段
- 使用繼承:被繼承的結(jié)構(gòu)體的所有字段和方法都可以被新的結(jié)構(gòu)體使用,就好像它自己擁有的一樣
代碼示例:
// 定義Animal類型 type Animal struct { Name string Age int } // 創(chuàng)建一個(gè)Animal的方法,打印動(dòng)物叫的聲音 func (a *Animal) Sound() { fmt.Println("I am an animal, I don't have a specific sound.") } // 創(chuàng)建Dog類型,包含Animal類型,這就是Go中的嵌套,類似于其他語(yǔ)言中的繼承 type Dog struct { Animal // 嵌入Animal } // 為Dog類型創(chuàng)建一個(gè)Sound方法,打印狗叫的聲音 func (d *Dog) Sound() { fmt.Println("Woof woof!") }
Dog類型中也定義了一個(gè)Sound方法,這樣,當(dāng)你調(diào)用Dog類型的Sound方法時(shí),其實(shí)是調(diào)用的Dog自身定義的Sound,而不是Animal中的Sound。這就實(shí)現(xiàn)了方法的覆蓋(override),也和其他面向?qū)ο笳Z(yǔ)言中的繼承效果類似
在很多面向?qū)ο蟮恼Z(yǔ)言中,如Java,都有"super"關(guān)鍵字可以用來調(diào)用父類的方法或?qū)傩?。但是Go語(yǔ)言并沒有"super"關(guān)鍵字。如果需要調(diào)用被嵌套結(jié)構(gòu)體的同名方法,需要顯式地指定結(jié)構(gòu)體的類型
type B struct{} func (b B) Print() { fmt.Println("B") } type A struct { B } func (a A) Print() { fmt.Println("A") a.B.Print() // 顯示調(diào)用被嵌套結(jié)構(gòu)體(相當(dāng)于父類)的同名方法 }
在Go語(yǔ)言中不推薦使用大量的嵌套,因?yàn)檫@會(huì)使代碼結(jié)構(gòu)變得復(fù)雜,不易維護(hù)和閱讀。當(dāng)需要代碼復(fù)用時(shí),更傾向于使用接口(interface)和組合。這樣可以減少代碼之間的耦合,保持代碼的簡(jiǎn)潔和清晰。
推薦使用接口和組合而不是嵌套
假設(shè)我們現(xiàn)在要設(shè)計(jì)一些不同類型的動(dòng)物,并讓它們都能叫。有的是貓,有的是狗。如果我們使用傳統(tǒng)的嵌套結(jié)構(gòu)體,代碼可能會(huì)這樣寫:
type Animal struct { Name string } func (a *Animal) Sound() { fmt.Println(a.Name + " makes a sound.") } type Dog struct { Animal } type Cat struct { Animal }
然后,如果我們需要增加如“讓動(dòng)物跑”的功能,但貓和狗跑的方式是不同的,就會(huì)顯得很麻煩,因?yàn)槲覀冃枰贒og和Cat結(jié)構(gòu)體中單獨(dú)去實(shí)現(xiàn)。
相反,如果我們使用接口和組合,代碼可以設(shè)計(jì)得更優(yōu)雅些:
type Animal interface { Sound() // 所有的動(dòng)物都會(huì)叫 Run() // 所有的動(dòng)物都會(huì)跑 } type BasicAnimal struct{ Name string } func (a *BasicAnimal) Sound() { fmt.Println(a.Name + " makes a sound.") } type Dog struct { BasicAnimal // 使用組合而不是嵌套 } func (d *Dog) Run() { fmt.Println("Dog runs happily.") } type Cat struct { BasicAnimal // 使用組合而不是嵌套 } func (c *Cat) Run() { fmt.Println("Cat runs gracefully.") }
在這個(gè)例子中,動(dòng)物都實(shí)現(xiàn)了Animal接口。我們定義的Dog和Cat都使用了組合,它們都有一個(gè)BasicAnimal字段。這樣就實(shí)現(xiàn)了代碼的復(fù)用:不用復(fù)寫Sound()方法。
此外,當(dāng)跑的行為對(duì)于不同動(dòng)物有不同的實(shí)現(xiàn)時(shí),我們就在相應(yīng)的結(jié)構(gòu)體中分別實(shí)現(xiàn)Run()方法,從而體現(xiàn)出各自的個(gè)
總結(jié)一下,使用接口和組合的好處:
- 將公共的行為定義在一起,避免代碼重復(fù)。
- 保持代碼解耦,各個(gè)結(jié)構(gòu)體只負(fù)責(zé)自己的行為。
- 易于擴(kuò)展,當(dāng)你需要增加新的動(dòng)物,并增加新的行為時(shí),無(wú)需修改已有的代碼,只需新實(shí)現(xiàn)相應(yīng)的接口方法即可。
以上就是詳解go如何優(yōu)雅的使用接口與繼承的詳細(xì)內(nèi)容,更多關(guān)于go使用接口與繼承的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go 微服務(wù)開發(fā)框架DMicro設(shè)計(jì)思路詳解
這篇文章主要為大家介紹了Go 微服務(wù)開發(fā)框架DMicro設(shè)計(jì)思路詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10go協(xié)程池實(shí)現(xiàn)原理小結(jié)
本文主要介紹了go協(xié)程池實(shí)現(xiàn)原理,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2025-04-04Go語(yǔ)言通過chan進(jìn)行數(shù)據(jù)傳遞的方法詳解
這篇文章主要為大家詳細(xì)介紹了Go語(yǔ)言如何通過chan進(jìn)行數(shù)據(jù)傳遞的功能,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起了解一下2023-06-06Golang實(shí)現(xiàn)自己的orm框架實(shí)例探索
這篇文章主要為大家介紹了Golang實(shí)現(xiàn)自己的orm框架實(shí)例探索,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01golang xorm及time.Time自定義解決json日期格式的問題
這篇文章主要介紹了golang xorm及time.Time自定義解決json日期格式的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-12-12golang?run時(shí)報(bào)undefined錯(cuò)誤的解決
這篇文章主要介紹了golang?run時(shí)報(bào)undefined錯(cuò)誤的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-03-03