接口(interface)是一種類型,用來定義行爲(方法)。golang
type Namer interface { my_method1() my_method2(para) my_method3(para) return_type ... }
但這些行爲不會在接口上直接實現,而是須要用戶自定義的方法來實現。因此,在上面的Namer接口類型中的方法my_methodN
都是沒有實際方法體的,僅僅只是在接口Namer中存放這些方法的簽名(簽名 = 函數名+參數(類型)+返回值(類型)
)。c#
當用戶自定義的類型實現了接口上定義的這些方法,那麼自定義類型的值(也就是實例)能夠賦值給接口類型的值(也就是接口實例)。這個賦值過程使得接口實例中保存了用戶自定義類型實例。數據結構
例如:函數
package main import ( "fmt" ) // Shaper 接口類型 type Shaper interface { Area() float64 } // Circle struct類型 type Circle struct { radius float64 } // Circle類型實現Shaper中的方法Area() func (c *Circle) Area() float64 { return 3.14 * c.radius * c.radius } // Square struct類型 type Square struct { length float64 } // Square類型實現Shaper中的方法Area() func (s *Square) Area() float64 { return s.length * s.length } func main() { // Circle類型的指針類型實例 c := new(Circle) c.radius = 2.5 // Square類型的值類型實例 s := Square{3.2} // Sharpe接口實例ins1,它自身是指針類型的 var ins1 Shaper // 將Circle實例c賦值給接口實例ins1 // 那麼ins1中就保存了實例c ins1 = c fmt.Println(ins1) // 使用類型推斷將Square實例s賦值給接口實例 ins2 := s fmt.Println(ins2) }
上面將輸出:指針
&{2.5} {3.2}
從上面輸出結果中能夠看出,兩個接口實例ins1和ins2被分別賦值後,分別保存了指針類型的Circle實例c和值類型的Square實例s。code
另外,從上面賦值ins1和ins2的賦值語句上看:對象
ins1 = c ins2 := s
是否說明接口實例ins就是自定義類型的實例?實際上接口是指針類型(指向什麼見下文)。這個時候,自定義類型的實例c、s稱爲具體實例,ins實例是抽象實例,由於ins接口中定義的行爲(方法)並無具體的行爲模式,而c、s中的行爲是具體的。blog
由於接口實例ins也是自定義類型的實例,因此當接口實例中保存了自定義類型的實例後,就能夠直接從接口上調用它所保存的實例的方法。例如:接口
fmt.Println(ins1.Area()) // 輸出19.625 fmt.Println(ins2.Area()) // 輸出10.24
這裏ins1.Area()
調用的是Circle類型上的方法Area(),ins2.Area()
調用的則是Square類型上的方法Area()。這說明Go的接口能夠實現面向對象中的多態:能夠按需調用名稱相同、功能不一樣的方法。get
前面說了,接口類型是指針類型,可是它到底存放了什麼東西?
接口類型的數據結構是2個指針,佔用2個機器字長。
當將類型實例c
賦值給接口實例ins1
後,用println()
函數輸出ins1和c,比較它們的地址:
println(ins1) println(c)
輸出結果:
(0x4ceb00,0xc042068058) 0xc042068058
從結果中能夠看出,接口實例中包含了兩個地址,其中第二個地址和類型實例c的地址是徹底相同的。而第二個地址c
是Circle的指針類型實例,因此ins中的第二個值也是指針。
ins中的第一個是指針是什麼?它所指向的是一個內部表結構iTable,這個Table中包含兩部分:第一部分是實例c的類型信息,也就是*Circle
,第二部分是這個類型(Circle)的方法集,也就是Circle類型的全部方法(此示例中Circle只定義了一個方法Area())。
因此,如圖所示:
注意,上圖中的實例c是指針,是指針類型的Circle實例。
對於值類型的Square實例s
,ins2保存的內容則以下圖:
實際上接口實例中保存的內容,在反射(reflect)中體現的淋漓盡致,reflect全部的一切都離不開接口實例保存的內容。
官方手冊對Method Set的解釋:https://golang.org/ref/spec#Method_sets
實例的method set決定了它所實現的接口,以及經過receiver能夠調用的方法。
方法集是類型的方法集合,對於非接口類型,每一個類型都分兩個Method Set:值類型實例是一個Method Set,指針類型的實例是另外一個Method Set。兩個Method Set由不一樣receiver類型的方法組成:
實例的類型 receiver -------------------------------------- 值類型:T (T Type) 指針類型:*T (T Type)或(T *Type)
也就是說:
(T Type)
組成(T Type)
和(T *Type)
這是什麼意思呢?從receiver的角度去考慮:
receiver 實例的類型 --------------------------- (T Type) T 或 *T (T *Type) *T
上面的意思是:
從實現接口方法的角度上看:
(T *Type)
類型的,那麼只有指針類型的實例*T
纔算是實現了這個接口,由於這個方法不在值類型的實例T
方法集中(T Type)
類型的,那麼值類型的實例T
和指針類型的實例*T
都算實現了這個接口,由於這個方法既在值類型的實例T
方法集中,也在指針類型的實例*T
方法集中舉個例子。接口方法Area(),自定義類型Circle有一個receiver類型爲(c *Circle)
的Area()方法時,說明實現了接口的方法,但只有Circle實例的類型爲指針類型時,這個實例纔算是實現了接口,才能賦值給接口實例,才能看成一個接口參數。以下:
package main import "fmt" // Shaper 接口類型 type Shaper interface { Area() float64 } // Circle struct類型 type Circle struct { radius float64 } // Circle類型實現Shaper中的方法Area() // receiver類型爲指針類型 func (c *Circle) Area() float64 { return 3.14 * c.radius * c.radius } func main() { // 聲明2個接口實例 var ins1, ins2 Shaper // Circle的指針類型實例 c1 := new(Circle) c1.radius = 2.5 ins1 = c1 fmt.Println(ins1.Area()) // Circle的值類型實例 c2 := Circle{3.0} // 下面的將報錯 ins2 = c2 fmt.Println(ins2.Area()) }
報錯結果:
cannot use c2 (type Circle) as type Shaper in assignment: Circle does not implement Shaper (Area method has pointer receiver)
它的意思是,Circle值類型的實例c2沒有實現Share接口的Area()方法,它的Area()方法是指針類型的receiver。換句話說,值類型的c2實例的Method Set中沒有receiver類型爲指針的Area()方法。
因此,上面應該改爲:
ins2 = &c2
再聲明一個方法,它的receiver是值類型的。下面的代碼一切正常。
type Square struct{ length float64 } // 實現方法Area(),receiver爲值類型 func (s Square) Area() float64{ return s.length * s.length } func main() { var ins3,ins4 Shaper // 值類型的Square實例s1 s1 := Square{3.0} ins3 = s1 fmt.Println(ins3.Area()) // 指針類型的Square實例s2 s2 := new(Square) s2.length=4.0 ins4 = s2 fmt.Println(ins4.Area()) }
因此,從struct類型定義的方法的角度去看,若是這個類型的方法有指針類型的receiver方法,則只能使用指針類型的實例賦值給接口變量,纔算是實現了接口。若是這個類型的方法全是值類型的receiver方法,則能夠隨意使用值類型或指針類型的實例賦值給接口變量。下面這兩個對應關係,對於理解頗有幫助:
實例的類型 receiver -------------------------------------- 值類型:T (T Type) 指針類型:*T (T Type)或(T *Type) receiver 實例的類型 --------------------------- (T Type) T 或 *T (T *Type) *T
很常常的,咱們會直接使用推斷類型的賦值方式(如ins2 := c2
)將實例賦值給一個變量,咱們覺得這個變量是接口的實例,但實際上並不必定。正如上面值類型的c2賦值給ins2,這個ins2將是從c2數據結構拷貝而來的另外一個副本數據結構,並不是接口實例,但這時經過ins2也能調用Area()方法:
c2 = Circle{3.2} ins2 := c2 fmt.Println(ins2.Area()) // 正常執行
之因此能調用,是由於Circle類型中有Area()方法,但這不是經過接口去調用的。
因此,在使用接口的時候,應當儘可能使用var先聲明接口類型的實例,再將類型的實例賦值給接口實例(如var ins1,ins2 Shaper
),或者使用ins1 := Shaper(c1)
的方式。這樣,若是賦值給接口實例的類型實例沒有實現該接口,將會報錯。
可是,爲何要限制指針類型的receiver只能是指針類型的實例的Method Set呢?
看下圖,假如指針類型的receiver能夠組成值類型實例的Method Set,那麼接口實例的第二個指針就必須找到值類型的實例的地址。但實際上,並不是全部值類型的實例都能獲取到它們的地址。
哪些值類型的實例找不到地址?最多見的是那些簡單數據類型的別名類型,若是匿名生成它們的實例,它們的地址就會被Go完全隱藏,外界找不到這個實例的地址。
例如:
package main import "fmt" type myint int func (m *myint) add() myint { return *m + 1 } func main() { fmt.Println(myint(3).add()) }
如下是報錯信息:找不到myint(3)的地址
abc\abc.go:11:22: cannot call pointer method on myint(3) abc\abc.go:11:22: cannot take the address of myint(3)
這裏的myint(3)
是匿名的myint實例,它的底層是簡單數據類型int,myint(3)
的地址會被完全隱藏,只會提供它的值對象3。
對於普通方法,不管是值類型仍是指針類型的實例,都能正常調用,且調用時拷貝的內容都由receiver的類型決定。
func (T Type) method1 // 值類型receiver func (T *Type) method2 // 指針類型receiver
指針類型的receiver決定了不管是值類型仍是指針類型的實例,都拷貝實例的指針。值類型的receiver決定了不管是值類型仍是指針類型的實例,都拷貝實例自己。
因此,對於person數據結構:
type person struct {} p1 := person{} // 值類型的實例 p2 := new(person) // 指針類型的實例
p1.method1()
和p2.method1()
都是拷貝整個person實例,只不過Go對待p2.method1()
時多一個"步驟":將其解除引用。因此p2.method1()
等價於(*p2).method1()
。
p1.method2()
和p2.method2()
都拷貝person實例的指針,只不過Go對待p1.method2()
時多一個"步驟":建立一個額外的引用。因此,p1.method2()
等價於(&p1).method2()
。
而類型實現接口方法時,method set規則決定了類型實例是否實現了接口。
receiver 實例的類型 --------------------------- (T Type) T 或 *T (T *Type) *T
對於接口abc、接口方法method1()、method2()和結構person:
type abc interface { method1 method2 } type person struct {} func (T person) method1 // 值類型receiver func (T *person) method2 // 指針類型receiver p1 := abc(person) // 接口變量保存值類型實例 p2 := abc(&person) // 接口變量保存指針類型實例
p2.method1()
、p2.method2()
以及p1.method1()
都是容許的,都會經過接口實例去調用具體person實例的方法。
但p1.method2()
是錯誤的,由於method2()的receiver是指針類型的,致使p1沒有實現接口abc的method2()方法。
將接口類型做爲參數很常見。這時,那些實現接口的實例都能做爲接口類型參數傳遞給函數/方法。
例如,下面的myArea()函數的參數是n Shaper
,是接口類型。
package main import ( "fmt" ) // Shaper 接口類型 type Shaper interface { Area() float64 } // Circle struct類型 type Circle struct { radius float64 } // Circle類型實現Shaper中的方法Area() func (c *Circle) Area() float64 { return 3.14 * c.radius * c.radius } func main() { // Circle的指針類型實例 c1 := new(Circle) c1.radius = 2.5 myArea(c1) } func myArea(n Shaper) { fmt.Println(n.Area()) }
上面myArea(c1)
是將c1做爲接口類型參數傳遞給n,而後調用c1.Area()
,由於實現了接口方法,因此調用的是Circle的Area()。
若是實現接口方法的receiver是指針類型的,但倒是值類型的實例,將無法做爲接口參數傳遞給函數,緣由前面已經解釋過了,這種類型的實例沒有實現接口。
以接口做爲方法或函數的參數,將使得一切都變得靈活且通用,只要是實現了接口的類型實例,均可以去調用它。
用的很是多的fmt.Println()
,它的參數也是接口,並且是變長的接口參數:
$ go doc fmt Println func Println(a ...interface{}) (n int, err error)
每個參數都會放進一個名爲a的Slice中,Slice中的元素是接口類型,並且是空接口,這使得無需實現任何方法,任何東西均可以丟到fmt.Println()中來,至於每一個東西怎麼輸出,那就要看具體狀況:由類型的實現的String()方法決定。
接口能夠嵌套,嵌套的內部接口將屬於外部接口,內部接口的方法也將屬於外部接口。
例如,File接口內部嵌套了ReadWrite接口和Lock接口。
type ReadWrite interface { Read(b Buffer) bool Write(b Buffer) bool } type Lock interface { Lock() Unlock() } type File interface { ReadWrite Lock Close() }
除此以外,類型嵌套時,若是內部類型實現了接口,那麼外部類型也會自動實現接口,由於內部屬性是屬於外部屬性的。