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的時候,第一反應是新建一個接口,而後定義接口中方法,以後再是編寫實現類,絕大多數狀況,都是隻會用到這一個實現類,將來很長時間都沒有看到這個接口的其餘實現類。這種爲了實現接口而編寫接口,有時候在中小型項目中讓代碼顯得很死板。
上面咱們介紹了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。
類型斷言的語法爲
<目標類型的值>,<布爾參數> := <表達式>.( 目標類型 ) // 安全類型斷言
<目標類型的值> := <表達式>.( 目標類型 )  //非安全類型斷言
複製代碼
上面的寫法是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的做用和使用就再也不贅述。