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