日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

Golang中interface的基本用法詳解_Golang

作者:rubys_ ? 更新時間: 2023-02-10 編程語言

Go 中接口也是一個使用得非常頻繁的特性,好的軟件設計往往離不開接口的使用,比如依賴倒置原則(通過抽象出接口,分離了具體實現與實際使用的耦合)。 今天,就讓我們來了解一下 Go 中接口的一些基本用法。

概述

Go 中的接口跟我們常見的編程語言中的接口不太一樣,go 里面實現接口是不需要使用?implements?關鍵字顯式聲明的, go 的接口為我們提供了難以置信的一系列的靈活性和抽象性。接口有兩個特點:

  • 接口本質是一種自定義類型。(跟 Java 中的接口不一樣)
  • 接口是一種特殊的自定義類型,其中沒有數據成員,只有方法(也可以為空)。

go 中的接口定義方式如下:

type?Flyable?interface?{
????Fly()?string
}

接口是完全抽象的,不能將其實例化。但是我們創建變量的時候可以將其類型聲明為接口類型:

var?a?Flyable

然后,對于接口類型變量,我們可以把任何實現了接口所有方法的類型變量賦值給它,這個過程不需要顯式聲明。 例如,假如?Bird?實現了?Fly?方法,那么下面的賦值就是合法的:

//?Bird?實現了?Flyable?的所有方法
var?a?Flyable?=?Bird{}

go 實現接口不需要顯式聲明。

由此我們引出 go 接口的最重要的特性是:

  • 只要某個類型實現了接口的所有方法,那么我們就說該類型實現了此接口。該類型的值可以賦給該接口的值。

  • 因為 interface{} 沒有任何方法,所以任何類型的值都可以賦值給它(類似 Java 中的 Object)

基本使用

Java 中的 interface(接口)

先看看其他語言中的 interface 是怎么使用的。

我們知道,很多編程語言里面都有?interface?這個關鍵字,表示的是接口,應該也用過,比如 Java 里面的:

//?定義一個?Flyable?接口
interface?Flyable?{
????public?void?fly();
}

//?定義一個名為?Bird?的類,顯式實現了?Flyable?接口
class?Bird??implements?Flyable?{
????public?void?fly()?{
????????System.out.println("Bird?fly.");
????}
}

class?Test?{
????//?fly?方法接收一個實現了?Flyable?接口的類
????public?static?void?fly(Flyable?flyable)?{
????????flyable.fly();
????}

????public?static?void?main(String[]?args)?{
????????Bird?b?=?new?Bird();
????????//?b?實現了?Flyable?接口,所以可以作為?fly?的參數
????????fly(b);
????}
}

在這個例子中,我們定義了一個?Flyable?接口,然后定義了一個實現了?Flyable?接口的?Bird?類, 最后,定義了一個測試的類,這個類的?fly?方法接收一個?Flyable?接口類型的參數, 因為?Bird?類實現了?Flyable?接口,所以可以將?b?作為參數傳遞給?fly?方法。

這個例子就是 Java 中?interface?的典型用法,如果一個類要實現一個接口,我們必須顯式地通過?implements?關鍵字來聲明。 然后使用的時候,對于需要某一接口類型的參數的方法,我們可以傳遞實現了那個接口的對象進去。

Java 中類實現接口必須顯式通過?implements?關鍵字聲明。

go 中的 interface(接口)

go 里面也有?interface?這個關鍵字,但是 go 與其他語言不太一樣。 go 里面結構體與接口之間不需要顯式地通過?implements?關鍵字來聲明的,在 go 中,只要一個結構體實現了?interface?的所有方法,我們就可以將這個結構體當做這個?interface?類型,比如下面這個例子:

package?main

import?"fmt"

//?定義一個?Flyable?接口
type?Flyable?interface?{
?Fly()?string
}

//?Bird?結構體沒有顯式聲明實現了?Flyable?接口(沒有?implements?關鍵字)
//?但是?Bird?定義了?Fly()?方法,
//?所以可以作為下面?fly?函數的參數使用。
type?Bird?struct?{
}

func?(b?Bird)?Fly()?string?{
?return?"bird?fly."
}

//?只要實現了?Flyable?的所有方法,
//?就可以作為?output?的參數。
func?fly(f?Flyable)?{
?fmt.Println(f.Fly())
}

func?main()?{
?var?b?=?Bird{}
?//?在?go?看來,b?實現了?Fly?接口,
?//?因為?Bird?里面實現了?Fly?接口的所有方法。
?fly(b)
}

在上面這個例子中,Person?結構體實現了?Stringer?接口的所有方法,所以在需要?Stringer?接口的地方,都可以用?Person?的實例作為參數。

Go 中結構體實現接口不用通過?implements?關鍵字聲明。(實際上,Go 也沒有這個關鍵字)

go interface 的優勢

go 接口的這種實現方式,有點類似于動態類型的語言,比如 Python,但是相比 Python,go 在編譯期間就可以發現一些明顯的錯誤。

比如像 Python 中下面這種代碼,如果傳遞的?coder?沒有?say_hello?方法,這種錯誤只有運行時才能發現:

def?hello_world(coder):
????coder.say_hello()

但如果是 go 的話,下面這種寫法中,如果傳遞給?hello_world?沒有實現?say?接口,那么編譯的時候就會報錯,無法通過編譯:

type?say?interface?{
?say_hello()
}

func?hello_world(coder?say)?{
?coder.say_hello()
}

因此,go 的這種接口實現方式有點像動態類型的語言,在一定程度上給了開發者自由,但是也在語言層面幫開發者做了類型檢查。

go 中不必像靜態類型語言那樣,所有地方都明確寫出類型,go 的編譯器幫我們做了很多工作,讓我們在寫 go 代碼的時候更加的輕松。 interface 也是,我們無需顯式實現接口,只要我們的結構體實現了接口的所有類型,那么它就可以當做那個接口類型使用(duck typing)。

空接口

go 中的?interface{}?表示一個空接口(在比較新版本中也可以使用?any?關鍵字來代替?interface{}),這個接口沒有任何方法。因此可以將任何變量賦值給?interface{}?類型的變量。

這在一些允許不同類型或者不確定類型參數的方法中用得比較廣泛,比如?fmt?里面的?println?等方法。

如何使用 interface{} 類型的參數?

這個可能是大部分人所需要關心的地方,因為這可能在日常開發中經常需要用到。

類型斷言

當實際開發中,我們接收到一個接口類型參數的時候,我們可能會知道它是幾種可能的情況之一了,我們就可以使用類型斷言來判斷?interface{}?變量是否實現了某一個接口:

func?fly(f?interface{})?{
?//?第一個返回值?v?是?f?轉換為接口之前的值,
?//?ok?為?true?表示?f?是?Bird?類型
?if?v,?ok?:=?f.(Flyable);?ok?{
??fmt.Println("bird?"?+?v.Fly())
?}

?//?斷言形式:接口.(類型)
?if?_,?ok?:=?f.(Bird);?ok?{
??fmt.Println("bird?flying...")
?}
}

在實際開發中,我們可以使用?xx.(Type)?這種形式來判斷:

  • interface{}?類型的變量是否是某一個類型
  • interface{}?類型的變量是否實現了某一個接口

如,f.(Flyable)?就是判斷?f?是否實現了?Flyable?接口,f.(Bird)?就是判斷?f?是否是?Bird?類型。

另外一種類型斷言方式

可能我們會覺得上面的那種?if?的判斷方式有點繁瑣,確實如此,但是如果我們不能保證?f?是某一類型的情況下,用上面這種判斷方式是比較安全的。

還有另外一種判斷方式,用在我們確切地知道?f?具體類型的情況:

func?fly2(f?interface{})?{
?fmt.Println("bird?"?+?f.(Flyable).Fly())
}

在這里,我們斷言?f?是?Flyable?類型,然后調用了它的?Fly?方法。

這是一種不安全的調用,如果?f?實際上沒有實現了?Flyable?接口,上面這行代碼會引發?panic。 而相比之下,v, ok := f.(Flyable)?這種方式會返回第二個值讓我們判斷這個斷言是否成立。

switch...case 中判斷接口類型

除了上面的斷言方式,還有另外一種判斷?interface{}?類型的方法,那就是使用?switch...case?語句:

func?str(f?interface{})?string?{
?//?判斷?f?的類型
?switch?f.(type)?{
?case?int:
??//?f?是?int?類型
??return?"int:?"?+?strconv.Itoa(f.(int))
?case?int64:
??//?f?是?int64?類型
??return?"int64:?"?+?strconv.FormatInt(f.(int64),?10)
????case?Flyable:
????????return?"flyable..."
?}
?return?"???"
}

編譯器自動檢測類型是否實現接口

上面我們說過了,在 go 里面,類型不用顯式地聲明實現了某個接口(也不能)。那么問題來了,我們開發的時候, 如果我們就是想讓某一個類型實現某個接口的時候,但是漏實現了一個方法的話,IDE 是沒有辦法知道我們漏了的那個方法的:

type?Flyable?interface?{
?Fly()?string
}

//?沒有實現?Flyable?接口,因為沒有?Fly()?方法
type?Bird?struct?{
}

func?(b?Bird)?Eat()?string?{
?return?"eat."
}

比如這段代碼中,我們本意是要?Bird?也實現?Fly?方法的,但是因為沒有顯式聲明,所以 IDE 沒有辦法知道我們的意圖。 這樣一來,在實際運行的時候,那些我們需要?Flyable?的地方,如果我們傳了?Bird?實例的話,就會報錯了。

一種簡單的解決方法

如果我們明確知道?Bird?將來是要當做?Flyable?參數使用的話,我們可以加一行聲明:

var?_?Flyable?=?Bird{}

這樣一來,因為我們有?Bird?轉?Flyable?類型的操作,所以編譯器就會去幫我們檢查?Bird?是否實現了?Flyable?接口了。 如果?Bird?沒有實現?Flyable?中的所有方法,那么編譯的時候會報錯,這樣一來,這些錯誤就不用等到實際運行的時候才能發現了

實際上,很多開源項目都能看到這種寫法。看起來定義了一個空變量,但是實際上確可以幫我們進行類型檢查。

這種解決方法還有另外一種寫法如下:

var?_?Flyable?=?(*Bird)(nil)

類型轉換與接口斷言

我們知道了,接口斷言可以獲得一個具體類型(也可以是接口)的變量,同時我們也知道了,在 go 里面也有類型轉換這東西, 實際上,接口斷言與類型轉換都是類型轉換,它們的差別只是:

interface{}?只能通過類型斷言來轉換為某一種具體的類型,而一般的類型轉換只是針對普通類型之間的轉換。

//?類型轉換:f?由?float32?轉換為?int
var?f?float32?=?10.8
i?:=?int(f)

//?接口的類型斷言
var?f?interface{}
v,?ok?:=?f.(Flyable)

如果是 interface{},需要使用類型斷言轉換為某一具體類型。

一個類型可以實現多個接口

上文我們說過了,只要一個類型實現了接口中的所有方法,那么那個類型就可以當作是那個接口來使用:

type?Writer?interface?{
????Write(p?[]byte)?(n?int,?err?error)
}

type?Closer?interface?{
????Close()?error
}

type?myFile?struct?{
}

//?實現了?Writer?接口
func?(m?myFile)??Write(p?[]byte)?(n?int,?err?error)?{
?return?0,?nil
}

//?實現了?Closer?接口
func?(m?myFile)?Close()?error?{
?return?nil
}

在上面這個例子中,myFile?實現了?Write?和?Close?方法,而這兩個方法分別是?Writer?和?Closer?接口中的所有方法。 在這種情況下,myFile?的實例既可以作為?Writer?使用,也可以作為?Closer?使用:

func?foo(w?Writer)?{
?w.Write([]byte("foo"))
}

func?bar(c?Closer)?{
?c.Close()
}

func?test()?{
?m?:=?myFile{}
?//?m?可以作為?Writer?接口使用
?foo(m)
?//?m?也可以作為?Closer?接口使用
?bar(m)
}

接口與 nil 不相等

有時候我們會發現,明明傳了一個?nil?給?interface{}?類型的參數,但在我們判斷實參是否與?nil?相等的時候,卻發現并不相等,如下面這個例子:

func?test(i?interface{})?{
?fmt.Println(reflect.TypeOf(i))
?fmt.Println(i?==?nil)
}

func?main()?{
?var?b?*int?=?nil
?test(b)?//?會輸出:*int?false
?test(nil)?//?會輸出:<nil>?true
}

這是因為 go 里面的?interface{}?實際上是包含兩部分的,一部分是?type,一部分是?data,如果我們傳遞的?nil?是某一個類型的?nil, 那么?interface{}?類型的參數實際上接收到的值會包含對應的類型。 但如果我們傳遞的?nil?就是一個普通的?nil,那么?interface{}?類型參數接收到的?type?和?data?都為?nil, 這個時候再與?nil?比較的時候才是相等的。

嵌套的接口

在 go 中,不僅結構體與結構體之間可以嵌套,接口與接口也可以通過嵌套創造出新的接口。

type?Writer?interface?{
????Write(p?[]byte)?(n?int,?err?error)
}

type?Closer?interface?{
????Close()?error
}

//?下面這個接口包含了?Writer?和?Closer?的所有方法
type?WriteCloser?interface?{
????Writer
????Closer
}

WriteCloser?是一個包含了?Writer?和?Closer?兩個接口所有方法的新接口,也就是說,WriteCloser?包含了?Write?和?Close?方法。

這樣的好處是,可以將接口拆分為更小的粒度。比如,對于某些只需要?Close?方法的地方,我們就可以用?Closer?作為參數的類型, 即使參數也實現了?Write?方法,因為我們并不關心除了?Close?以外的其他方法:

func?foo(c?Closer)?{
?//?...
?c.Close()
}

而對于上面的?myFile,因為同時實現了?Writer?接口和?Closer?接口,而?WriteCloser?包含了這兩個接口, 所以實際上?myFile?可以當作?WriteCloser?或者?Writer?或?Closer?類型使用。

總結

  • 接口里面只聲明了方法,沒有數據成員。
  • go 中的接口不需要顯式聲明(也不能)。
  • 只要一個類型實現了接口的所有方法,那么該類型實現了此接口。該類型的值可以賦值給該接口類型。
  • interface{}/any?是空接口,任何類型的值都可以賦值給它。
  • 通過類型斷言我們可以將?interface{}?類型轉換為具體的類型。
  • 我們通過聲明接口類型的?_?變量來讓編譯器幫我們檢查我們的類型是否實現了某一接口。
  • 一個類型可以同時實現多個接口,可以當作多個接口類型來使用。
  • nil?與值為?nil?的?interface{}?實際上不想等,需要注意。
  • go 中的接口可以嵌套,類似結構體的嵌套。

原文鏈接:https://mp.weixin.qq.com/s/XI_dnDGow_tExFrcKSVnPA

欄目分類
最近更新