Go語言之接口

接口是一種約定,它是一個抽象的類型,和咱們見到的具體的類型如int、map、slice等不同。具體的類型,咱們能夠知道它是什麼,而且能夠知道能夠用它作什麼;可是接口不同,接口是抽象的,它只有一組接口方法,咱們並不知道它的內部實現,因此咱們不知道接口是什麼,可是咱們知道能夠利用它提供的方法作什麼。數據結構


抽象就是接口的優點,它不用和具體的實現細節綁定在一塊兒,咱們只需定義接口,告訴編碼人員它能夠作什麼,這樣咱們能夠把具體實現分開,編碼就會更加靈活方面,適應能力也會很是強。ide


func main() {
    var b bytes.Buffer
    fmt.Fprint(&b,"Hello World")
    fmt.Println(b.String())
}


以上就是一個使用接口的例子,咱們先看下fmt.Fprint函數的實現。函數


func Fprint(w io.Writer, a ...interface{}) (n int, err error) {
    p := newPrinter()
    p.doPrint(a)
    n, err = w.Write(p.buf)
    p.free()
    return
}


從上面的源代碼中,咱們能夠看到,fmt.Fprint函數的第一個參數是io.Writer這個接口,因此只要實現了這個接口的具體類型均可以做爲參數傳遞給fmt.Fprint函數。而bytes.Buffer偏偏實現了io.Writer接口,因此能夠做爲參數傳遞給fmt.Fprint函數。佈局


內部實現編碼


咱們前面提過接口是用來定義行爲類型的,它是抽象的,這些定義的行爲不是由接口直接實現,而是經過方法由用戶定義的類型實現。若是用戶定義的類型,實現了接口類型聲明的全部方法,那麼這個用戶定義的類型就實現了這個接口,因此這個用戶定義類型的值就能夠賦值給接口類型的值。spa


func main() {
    var b bytes.Buffer
    fmt.Fprint(&b, "Hello World")
    var w io.Writer
    w = &b
    fmt.Println(w)
}


這個例子中,由於bytes.Buffer實現了接口io.Writer,因此咱們能夠經過w = &b賦值,這個賦值的操做會把定義類型的值存入接口類型的值。指針


賦值操做執行後,若是咱們對接口方法執行調用,實際上是調用存儲的用戶定義類型的對應方法,這裏咱們能夠把用戶定義的類型稱之爲實體類型code


咱們能夠定義不少類型,讓它們實現一個接口,那麼這些類型均可以賦值給這個接口。這時候接口方法的調用,其實就是對應實體類型對應方法的調用,這就是多態。orm


func main() {
    var a animal

    var c cat
    a=c
    a.printInfo()
    //使用另一個類型賦值
    var d dog
    a=d
    a.printInfo()
}

type animal interface {
    printInfo()
}

type cat int
type dog int

func (c cat) printInfo(){
    fmt.Println("a cat")
}

func (d dog) printInfo(){
    fmt.Println("a dog")
}


以上例子演示了一個多態。咱們定義了一個接口animal,而後定義地兩種類型catdog實現了接口animal。在使用的時候,分別把類型cat的值c、類型dog的值d賦值給接口animal的值a,而後分別執行aprintInfo方法,能夠看到不一樣的輸出。接口


a cat
a dog


咱們看下接口的值被賦值後,接口值內部的佈局。接口的值是一個兩個字長度的數據結構,第一個字包含一個指向內部表結構的指針,這個內部表裏存儲的有實體類型的信息以及相關聯的方法集;第二個字包含的是一個指向存儲的實體類型值的指針。因此接口的值結構實際上是兩個指針,這也能夠說明接口實際上是一個引用類型。


方法集


咱們都知道,若是要實現一個接口,必須實現這個接口提供的全部方法。可是實現方法的時候,咱們可使用指針接收者實現,也可使用值接收者實現,這二者是有區別的。下面咱們就好好分析下這二者的區別。


func main() {
    var c cat
    //值做爲參數傳遞
    invoke(c)
}
//須要一個animal接口做爲參數
func invoke(a animal){
    a.printInfo()
}

type animal interface {
    printInfo()
}

type cat int

//值接收者實現animal接口
func (c cat) printInfo(){
    fmt.Println("a cat")
}


仍是原來的例子改改,增長一個invoke函數,該函數接收一個animal接口類型的參數,例子中傳遞參數的時候,也是以類型cat的值c傳遞的,運行程序能夠正常執行。如今咱們稍微改造一下,使用類型cat的指針&c做爲參數傳遞。


func main() {
    var c cat
    //指針做爲參數傳遞
    invoke(&c)
}


只修改這一處,其餘保持不變,咱們運行程序,發現也能夠正常執行。經過這個例子咱們能夠得出結論:實體類型以值接收者實現接口的時候,不論是實體類型的值,仍是實體類型值的指針,都實現了該接口。


下面咱們把接收者改成指針試試。


func main() {
    var c cat
    //值做爲參數傳遞
    invoke(c)
}
//須要一個animal接口做爲參數
func invoke(a animal){
    a.printInfo()
}

type animal interface {
    printInfo()
}

type cat int

//指針接收者實現animal接口
func (c *cat) printInfo(){
    fmt.Println("a cat")
}


這個例子中把實現接口的接收者改成指針,可是傳遞參數的時候,咱們仍是按值進行傳遞,點擊運行程序,會出現如下異常提示:


./main.go:10: cannot use c (type cat) as type animal in argument to invoke:
    cat does not implement animal (printInfo method has pointer receiver)


提示中已經很明顯地告訴咱們,說cat沒有實現animal接口,是由於printInfo方法有一個指針接收者,因此cat類型的值c不能做爲接口類型animal傳參使用。下面咱們再稍微修改下,改成以指針做爲參數傳遞。


func main() {
    var c cat
    //指針做爲參數傳遞
    invoke(&c)
}


其餘都不變,只是把之前使用值的參數,改成使用指針做爲參數,咱們再運行程序,就能夠正常運行了。因而可知實體類型以指針接收者實現接口的時候,只有指向這個類型的指針才被認爲實現了該接口。


如今咱們總結下這兩種規則,首先以方法接收者是值仍是指針的角度看。


Methods Receivers

Values

(t T)

T and *T

(t *T)

*T


上面的表格能夠解讀爲:若是是值接收者,實體類型的值和指針均可以實現對應的接口;若是是指針接收者,那麼只有類型的指針可以實現對應的接口。


其次咱們以實體類型是值仍是指針的角度看。


Values

Methods Receivers

T

(t T)

*T

(t T) and (t *T)


上面的表格能夠解讀爲:類型的值只能實現值接收者的接口;指向類型的指針,既能夠實現值接收者的接口,也能夠實現指針接收者的接口。

相關文章
相關標籤/搜索