Go語言入門——interface

一、Go如何定義interface

Go經過type聲明一個接口,形如編程

type geometry interface {
    area() float64
    perim() float64
}
複製代碼

和聲明一個結構體同樣,接口也是經過type聲明。安全

type後面是接口名稱,緊挨着是關鍵字interface。bash

接口裏面定義的area()和perim是接口geometry的方法。框架

有了接口,那應該如何實現接口呢?編程語言

type rect struct {
    width, height float64
}

func (r rect) area() float64 {
    return r*width*height
}

func (r rect) perim() float64 {
    return 2*r*width + 2*r*height
}
複製代碼

上面就是rect實現接口geometry的代碼。不一樣於Java這些語言,有顯式的關鍵字如implement表示實現某個接口。ui

和Java接口的契約精神有些不一樣的是,Go裏面的接口實現更像是組合的概念。spa

這裏要提一個」鴨子類型「的概念。鴨子類型是動態編程語言的一種對象推斷策略,它更關注對象能如何被使用,而不是對象的類型自己。即一個東西若是長得像鴨子,會像鴨子同樣嘎嘎叫、走路、游泳,那麼咱們就能夠推斷這個小東西就是鴨子。指針

類比上面的代碼,rect就是長得像鴨子geometry的,能夠像geometry同樣的area()行爲,也能夠像geometry同樣的perim(),rect知足了geometry定義的一切行爲,因此咱們推斷rect就是實現了接口geometry的。code

這樣,咱們不用再去寫implement xxx這樣的代碼了。由原來一個類的粒度細化到類裏面方法的粒度了。對象

順便提一句,以前在作Java開發的時候,因爲是協同開發,都是用統一的框架,加上面向接口編程的思想深刻人心,以致於成爲這樣的一種條件反射:在寫一個service的時候,第一反應是新建一個接口,而後定義接口中方法,以後再是編寫實現類,絕大多數狀況,都是隻會用到這一個實現類,將來很長時間都沒有看到這個接口的其餘實現類。這種爲了實現接口而編寫接口,有時候在中小型項目中讓代碼顯得很死板。

二、如何斷定是不是某個interface的實現

上面咱們介紹了Go是如何定義一個接口並」實現「接口的。上面代碼只有一個rect結構體,若是有多個呢

type rect struct {
    width, height float64
}

func (r rect) area() float64 {
    return r*width*height
}

func (r rect) perim() float64 {
    return 2*r*width + 2*r*height
}

type circle struct {
    radius float64
}

func (c circle) area() float64 {
    return math.Pi * c.radius * c.radius
}
func (c circle) perim() float64 {
    return 2 * math.Pi * c.radius
}
複製代碼

對於這種狀況,咱們總不能一個個肉眼比對,看看rect、circle是否實現了geometry中定義的全部方法吧

Go能夠經過類型斷言來斷定。

func main() {
	r := rect{width: 3, height: 4}
	c := circle{radius: 5}

	measure(r)
	measure(c)
    
	var g geometry
	g = circle{radius:10}
	switch t := g.(type) {
	case circle:
		fmt.Println("circle type", t)
	case rect:
		fmt.Println("rect type", t)
	}
}

複製代碼

執行結果爲

{3 4}
12
14
{5}
78.53981633974483
31.41592653589793
circle type {10}
複製代碼

能夠看出,Go能夠推斷g是實現了geometry接口的circle。

類型斷言的語法爲

<目標類型的值>,<布爾參數> := <表達式>.( 目標類型 ) // 安全類型斷言

<目標類型的值> := <表達式>.( 目標類型 )&emsp;&emsp;//非安全類型斷言
複製代碼

上面的寫法是switch語法,即第二種。第一種舉例以下

var g geometry
if f, ok := g.(circle); ok {
		fmt.Println("circle type", f)
}
複製代碼

三、值接收仍是指針接收

在Go中一個方法,咱們能夠定義一個方法是用某個struct的值來接收仍是指針接收,形如

type rect struct {
	width, height int
}

func (r *rect) area() int {
	return r.width * r.height
}

func (r rect) perim() int {
	return 2*r.width + 2*r.height
}

func main() {
	r := rect{width: 10, height: 20}
	fmt.Println("area:", r.area())
	fmt.Println("perim:", r.perim())

	rp := &r
	fmt.Println("area:", rp.area())
	fmt.Println("perim:", rp.perim())
}
複製代碼

這裏定義結構體rect,同時定義兩個方法area()和perim()。在這兩個方法左邊定義的即爲方法的接收者,其中area()由rect的指針類型接收,perim()則由rect值類型接收。

這樣表示area()和perim()是rect的兩個方法。從代碼咱們能夠看出,該種形式不論是傳入值類型仍是傳入rect的指針,執行都正常返回結果。

area: 200
perim: 60
area: 200
perim: 60
複製代碼

對於r.area()能夠調通的背後Go作了什麼?

此時r是一個值類型,爲了實現調用即便是指針接收類型的area()方法,Go實際是先找到r的地址,而後經過一個指針指向它,即r.area()轉化成了(&r).area(),從而知足了area()方法是指針接收者的約束。

對於rp.perim()能夠調通的背後Go作了什麼?

此時rp是一個指針類型。在調用時,指針被解引用爲值,這樣便符合perim()方法定義的接收者類型的約束。解引用的過程咱們能夠認爲Go把rp.perim()轉化爲於(*rp).perim()。可是注意perim()方法是值接收類型,因此操做的是rect的副本。

因此,綜上,對於普通方法的調用,無論接收者是值類型仍是指針類型,調用者是值類型仍是指針類型,均可以調通。

上面是針對純粹的方法而言的,若是在接口的背景下,狀況是否一致呢?

type geometry interface {
	area() float64
	perim() float64
}

type rect struct {
	width, height float64
}

type circle struct {
	radius float64
}

func (r rect) area() float64 {
	return r.width * r.height
}

func (r rect) perim() float64 {
	return 2*r.width + 2*r.height
}

func (c circle) area() float64 {
	return math.Pi * c.radius * c.radius
}

func (c *circle) perim() float64 {
	return 2 * math.Pi * c.radius
}

func measure(g geometry) {
	fmt.Println(g)
	fmt.Println(g.area())
	fmt.Println(g.perim())
}

func main() {
	r := rect{width: 3, height: 4}
	c := circle{radius: 5}

	measure(r)
	measure(c)
}
複製代碼

這段的代碼與上面的惟一不一樣的地方在於將perim接收者類型由circle改成了*circle類型,致使在運行程序時報錯

# command-line-arguments
main/src/examples/interfaces.go:48:9: cannot use c (type circle) as type geometry in argument to measure:
	circle does not implement geometry (perim method has pointer receiver)
複製代碼

意思是說circle沒有實現geometry接口。

若是反過來

type geometry interface {
	area() float64
	perim() float64
}

type rect struct {
	width, height float64
}

type circle struct {
	radius float64
}

func (r rect) area() float64 {
	return r.width * r.height
}

func (r rect) perim() float64 {
	return 2*r.width + 2*r.height
}

func (c circle) area() float64 {
	return math.Pi * c.radius * c.radius
}

func (c *circle) perim() float64 {
	return 2 * math.Pi * c.radius
}

func measure(g geometry) {
	fmt.Println(g)
	fmt.Println(g.area())
	fmt.Println(g.perim())
}

func main() {
	r := &rects1{width: 3, height: 4}
	c := &circle{radius: 5}

	measure(r)
	measure(c)
}
複製代碼

此時調用一切正常。

因此對比看下來發現,對於值接收者,傳如值或者指針均可以正常調用;對於指針接收者,則只能傳入指針類型,不然會報未實現接口的錯誤。

關於原理,我看了不少說法 說法一

對於指針類型,Go會自動轉換,由於有了指針老是能獲得指針指向的值是什麼,若是是 value 調用,go 將無從得知 value 的原始值是什麼,由於 value 是份拷貝。go 會把指針進行隱式轉換獲得 value,但反過來則不行。

說法二

當實現一個接收者是值類型的方法,就能夠自動生成一個接收者是對應指針類型的方法,由於二者都不會影響接收者。可是,當實現了一個接收者是指針類型的方法,若是此時自動生成一個接收者是值類型的方法,本來指望對接收者的改變(經過指針實現),如今沒法實現,由於值類型會產生一個拷貝,不會真正影響調用者。

可是這兩種說法我以爲仍是沒有真正說到原理上,也多是我沒有理解。

在前面不涉及到接口的單純方法的值接收者和指針接收者,使用值或者指針調用都是能夠的,由於Go會在底層作這個類型轉換。可是在接口這個背景下,若是方法有指針類型接收類型,則只能傳指針類型,我以爲仍是和接口有關。若是你們有本身的理解,歡迎指教。

今天主要介紹了Go語言中的接口的定義和實現以及如何使用,還有一些小知識點好比空interface的做用和使用就再也不贅述。

相關文章
相關標籤/搜索