詳解Golang中interface接口的原理和使用技巧
一、Go interface 介紹
interface 在 Go 中的重要性說明
interface 接口在 Go 語言里面的地位非常重要,是一個非常重要的數據結構,只要是實際業(yè)務編程,并且想要寫出優(yōu)雅的代碼,那么必然要用上 interface,因此 interface 在 Go 語言里面處于非常核心的地位。
我們都知道,Go 語言和典型的面向對象的語言不太一樣,Go 在語法上是不支持面向對象的類、繼承等相關概念的。但是,并不代表 Go 里面不能實現面向對象的一些行為比如繼承、多態(tài),在 Go 里面,通過 interface 完全可以實現諸如 C++ 里面的繼承 和 多態(tài)的語法效果。
interface 的特性
Go 中的 interface 接口有如下特性:
關于接口的定義和簽名
- 接口是一個或多個方法簽名的集合,接口只有方法聲明,沒有實現,沒有數據字段,只要某個類型擁有該接口的所有方法簽名,那么就相當于實現了該接口,無需顯示聲明了哪個接口,這稱為 Structural Typing。
- interface 接口可以匿名嵌入其他接口中,或嵌入到 struct 結構中
- 接口可以支持匿名字段方法
關于接口賦值
- 只有當接口存儲的類型和對象都為 nil 時,接口才等于 nil
- 一個空的接口可以作為任何類型數據的容器
- 如果兩個接口都擁有相同的方法,那么它們就是等同的,任何實現了他們這個接口的對象之間,都可以相互賦值
- 如果某個 struct 對象實現了某個接口的所有方法,那么可以直接將這個 struct 的實例對象直接賦值給這個接口類型的變量。
關于接口嵌套,Go 里面支持接口嵌套,但是不支持遞歸嵌套
通過接口可以實現面向對象編程中的多態(tài)的效果
interface 接口和 reflect 反射
在 Go 的實現里面,每個 interface 接口變量都有一個對應 pair,這個 pair 中記錄了接口的實際變量的類型和值(value, type)
,其中,value 是實際變量值,type 是實際變量的類型。任何一個 interface{} 類型的變量都包含了2個指針,一個指針指向值的類型,對應 pair 中的 type,這個 type 類型包括靜態(tài)的類型 (static type,比如 int、string...)和具體的類型(concrete type,interface 所指向的具體類型),另外一個指針指向實際的值,對應 pair 中的 value。
interface 及其 pair 的存在,是 Go 語言中實現 reflect 反射的前提,理解了 pair,就更容易理解反射。反射就是用來檢測存儲在接口變量內部(值value;類型concrete type) pair 對的一種機制。
二、Go 里面為啥偏向使用 Interface
Go 里面為啥偏向使用 Interface 呢? 主要原因有如下幾點:
可以實現泛型編程(雖然 Go 在 1.18 之后已經支持泛型了)
在 C++ 等高級語言中使用泛型編程非常的簡單,但是 Go 在 1.18 版本之前,是不支持泛型的,而通過 Go 的接口,可以實現類似的泛型編程,如下是一個參考示例
package sort // A type, typically a collection, that satisfies sort.Interface can be // sorted by the routines in this package. The methods require that the // elements of the collection be enumerated by an integer index. type Interface interface { // Len is the number of elements in the collection. Len() int // Less reports whether the element with // index i should sort before the element with index j. Less(i, j int) bool // Swap swaps the elements with indexes i and j. Swap(i, j int) } ... // Sort sorts data. // It makes one call to data.Len to determine n, and O(n*log(n)) calls to // data.Less and data.Swap. The sort is not guaranteed to be stable. func Sort(data Interface) { // Switch to heapsort if depth of 2*ceil(lg(n+1)) is reached. n := data.Len() maxDepth := 0 for i := n; i > 0; i >>= 1 { maxDepth++ } maxDepth *= 2 quickSort(data, 0, n, maxDepth) }
Sort 函數的形參是一個 interface,包含了三個方法:Len(),Less(i,j int),Swap(i, j int)。使用的時候不管數組的元素類型是什么類型(int, float, string…),只要我們實現了這三個方法就可以使用 Sort 函數,這樣就實現了“泛型編程”。
這種方式,我在閃聊項目里面也有實際應用過,具體案例就是對消息排序。 下面給一個具體示例,代碼能夠說明一切,一看就懂:
type Person struct { Name string Age int } func (p Person) String() string { return fmt.Sprintf("%s: %d", p.Name, p.Age) } // ByAge implements sort.Interface for []Person based on // the Age field. type ByAge []Person //自定義 func (a ByAge) Len() int { return len(a) } func (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age } func main() { people := []Person{ {"Bob", 31}, {"John", 42}, {"Michael", 17}, {"Jenny", 26}, } fmt.Println(people) sort.Sort(ByAge(people)) fmt.Println(people) }
可以隱藏具體的實現
隱藏具體的實現,是說我們提供給外部的一個方法(函數),但是我們是通過 interface 接口的方式提供的,對調用方來說,只能通過 interface 里面的方法來做一些操作,但是內部的具體實現是完全不知道的。
例如我們常用的 context 包,就是這樣設計的,如果熟悉 Context 具體實現的就會很容易理解。詳細代碼如下:
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { c := newCancelCtx(parent) propagateCancel(parent, &c) return &c, func() { c.cancel(true, Canceled) } }
可以看到 WithCancel 函數返回的還是一個 Context interface,但是這個 interface 的具體實現是 cancelCtx struct。
// newCancelCtx returns an initialized cancelCtx. func newCancelCtx(parent Context) cancelCtx { return cancelCtx{ Context: parent, done: make(chan struct{}), } } // A cancelCtx can be canceled. When canceled, it also cancels any children // that implement canceler. type cancelCtx struct { Context //注意一下這個地方 done chan struct{} // closed by the first cancel call. mu sync.Mutex children map[canceler]struct{} // set to nil by the first cancel call err error // set to non-nil by the first cancel call } func (c *cancelCtx) Done() <-chan struct{} { return c.done } func (c *cancelCtx) Err() error { c.mu.Lock() defer c.mu.Unlock() return c.err } func (c *cancelCtx) String() string { return fmt.Sprintf("%v.WithCancel", c.Context) }
盡管內部實現上下面三個函數返回的具體 struct (都實現了 Context interface)不同,但是對于使用者來說是完全無感知的。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) //返回 cancelCtx func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) //返回 timerCtx func WithValue(parent Context, key, val interface{}) Context //返回 valueCtx
可以實現面向對象編程中的多態(tài)用法
interface 只是定義一個或一組方法函數,但是這些方法只有函數簽名,沒有具體的實現,這個 C++ 中的虛函數非常類似。在 Go 里面,如果某個數據類型實現 interface 中定義的那些函數,則稱這些數據類型實現(implement)了這個接口 interface,這是我們常用的 OO 方式,如下是一個簡單的示例
// 定義一個 SimpleLog 接口 type SimpleLog interface { Print() } func TestFunc(x SimpleLog) {} // 定義一個 PrintImpl 結構,用來實現 SimpleLog 接口 type PrintImpl struct {} // PrintImpl 對象實現了SimpleLog 接口的所有方法(本例中是 Print 方法),就說明實現了 SimpleLog 接口 func (p *PrintImpl) Print() { } func main() { var p PrintImpl TestFunc(p) }
空接口可以接受任何類型的參數
空接口比較特殊,它不包含任何方法:interface{} ,在 Go 語言中,所有其它數據類型都實現了空接口,如下:
var v1 interface{} = 1 var v2 interface{} = "abc" var v3 interface{} = struct{ X int }{1}
因此,當我們給 func 定義了一個 interface{} 類型的參數(也就是一個空接口)之后,那么這個參數可以接受任何類型,官方包中最典型的例子就是標準庫 fmt 包中的 Print 和 Fprint 系列的函數。
一個簡單的定義示例方法如下:
Persist(context context.Context, msg interface{}) bool
msg 可以為任何類型,如 pb.MsgInfo or pb.GroupMsgInfo,定義方法的時候可以統(tǒng)一命名模塊,實現的時候,根據不同場景實現不同方法。
三、Go interface 的常見應用和實戰(zhàn)技巧
interface 接口賦值
可以將一個實現接口的對象實例賦值給接口,也可以將另外一個接口賦值給接口。
通過對象實例賦值
將一個對象實例賦值給一個接口之前,要保證該對象實現了接口的所有方法。在 Go 語言中,一個類只需要實現了接口要求的所有函數,我們就說這個類實現了該接口,這個是非侵入式接口的設計模式,非侵入式接口一個很重要的優(yōu)勢就是可以免去面向對象里面那套比較復雜的類的繼承體系。
在 Go 里面,面向對象的那套類的繼承體系就不需要關心了,定義接口的時候,我們只需關心這個接口應該提供哪些方法,當然,按照 Go 的原則,接口的功能要盡可能的保證職責單一。而對應接口的實現,也就是接口的調用方,我們只需要知道這個接口定義了哪些方法,然后我們實現這些方法就可以了,這個也無需提前規(guī)劃,調用方也無需關系是否有其他模塊定義過類似的接口或者實現,只關注自身就行。
考慮如下示例:
type Integer int func (a Integer) Less(b Integer) bool { return a < b } func (a *Integer) Add(b Integer) { *a += b } type LessAdder interface { Less(b Integer) bool Add(b Integer) } var a Integer = 1 var b1 LessAdder = &a //OK var b2 LessAdder = a //not OK
b2 的賦值會報編譯錯誤,為什么呢?因為這個:The method set of any other named type T consists of all methods with receiver type T. The method set of the corresponding pointer type T is the set of all methods with receiver T or T (that is, it also contains the method set of T).
也就是說 *Integer 這個指針類型實現了接口 LessAdder 的所有方法,而 Integer 只實現了 Less 方法,所以不能賦值。
通過接口賦值
var r io.Reader = new(os.File) var rw io.ReadWriter = r //not ok var rw2 io.ReadWriter = new(os.File) var r2 io.Reader = rw2 //ok
因為 r 沒有 Write 方法,所以不能賦值給rw。
interface 接口嵌套
io package 中的一個接口:
// ReadWriter is the interface that groups the basic Read and Write methods. type ReadWriter interface { Reader Writer }
ReadWriter 接口嵌套了 io.Reader 和 io.Writer 兩個接口,實際上,它等同于下面的寫法:
type ReadWriter interface { Read(p []byte) (n int, err error) Write(p []byte) (n int, err error) }
注意,Go 語言中的接口不能遞歸嵌套,如下:
// illegal: Bad cannot embed itself type Bad interface { Bad } // illegal: Bad1 cannot embed itself using Bad2 type Bad1 interface { Bad2 } type Bad2 interface { Bad1 }
interface 強制類型轉換
ret, ok := interface.(type) 斷言
在 Go 語言中,可以通過 interface.(type)
的方式來對一個 interface 進行強制類型轉換,但是如果這個 interface 被轉換為一個不包含指定類型的類型,那么就會出現 panic 。因此,實戰(zhàn)應用中,我們通常都是通過 ret, ok := interface.(type)
這種斷言的方式來優(yōu)雅的進行轉換,這個方法中第一個返回值是對應類型的值,第二個返回值是類型是否正確,只有 ok = true 的情況下,才說明轉換成功,最重要的是,通過這樣的轉換方式可以避免直接轉換如果類型不對的情況下產生 panic。
如下是一個以 string 為類型的示例:
str, ok := value.(string) if ok { fmt.Printf("string value is: %q\n", str) } else { fmt.Printf("value is not a string\n") }
如果類型斷言失敗,則str將依然存在,并且類型為字符串,不過其為零值,即一個空字符串。
switch x.(type) 斷言
查詢接口類型的方式為:
switch x.(type) { // cases : }
示例如下:
var value interface{} // Value provided by caller. switch str := value.(type) { case string: return str //type of str is string case int: return int //type of str is int }
語句switch中的value必須是接口類型,變量str的類型為轉換后的類型。
interface 與 nil 的比較
interface 與 nil 的比較是挺有意思的,例子是最好的說明,如下例子:
package main import ( "fmt" "reflect" ) type State struct{} func testnil1(a, b interface{}) bool { return a == b } func testnil2(a *State, b interface{}) bool { return a == b } func testnil3(a interface{}) bool { return a == nil } func testnil4(a *State) bool { return a == nil } func testnil5(a interface{}) bool { v := reflect.ValueOf(a) return !v.IsValid() || v.IsNil() } func main() { var a *State fmt.Println(testnil1(a, nil)) fmt.Println(testnil2(a, nil)) fmt.Println(testnil3(a)) fmt.Println(testnil4(a)) fmt.Println(testnil5(a)) }
運行后返回的結果如下
false
false
false
true
true
為什么是這個結果?
*因為一個 interface{} 類型的變量包含了2個指針,一個指針指向值的類型,另外一個指針指向實際的值。對一個 interface{} 類型的 nil 變量來說,它的兩個指針都是0;但是 var a State 傳進去后,指向的類型的指針不為0了,因為有類型了, 所以比較為 false。 interface 類型比較, 要是兩個指針都相等,才能相等。
到此這篇關于詳解Golang中interface接口的原理和使用技巧的文章就介紹到這了,更多相關Golang interface接口內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
協(xié)同開發(fā)巧用gitignore中間件避免網絡請求攜帶登錄信息
這篇文章主要為大家介紹了協(xié)同開發(fā)巧用gitignore中間件避免網絡請求攜帶登錄信息2022-06-06Go channel發(fā)送方和接收方如何相互阻塞等待源碼解讀
這篇文章主要為大家介紹了Go channel發(fā)送方和接收方如何相互阻塞等待源碼解讀,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-12-12