介面用法簡介 介面(interface)是一種類型,用來定義行為(方法)。 但這些行為不會在介面上直接實現,而是需要用戶自定義的方法來實現。所以,在上面的Namer介面類型中的方法 都是沒有實際方法體的,僅僅只是在介面Namer中存放這些方法的簽名( )。 當用戶自定義的類型實現了介面上定義的這些方 ...
介面用法簡介
介面(interface)是一種類型,用來定義行為(方法)。
type Namer interface {
my_method1()
my_method2(para)
my_method3(para) return_type
...
}
但這些行為不會在介面上直接實現,而是需要用戶自定義的方法來實現。所以,在上面的Namer介面類型中的方法my_methodN
都是沒有實際方法體的,僅僅只是在介面Namer中存放這些方法的簽名(簽名 = 函數名+參數(類型)+返回值(類型)
)。
當用戶自定義的類型實現了介面上定義的這些方法,那麼自定義類型的值(也就是實例)可以賦值給介面類型的值(也就是介面實例)。這個賦值過程使得介面實例中保存了用戶自定義類型實例。
例如:
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。
另外,從上面賦值ins1和ins2的賦值語句上看:
ins1 = c
ins2 := s
是否說明介面實例ins就是自定義類型的實例?實際上介面是指針類型(指向什麼見下文)。這個時候,自定義類型的實例c、s稱為具體實例,ins實例是抽象實例,因為ins介面中定義的行為(方法)並沒有具體的行為模式,而c、s中的行為是具體的。
因為介面實例ins也是自定義類型的實例,所以當介面實例中保存了自定義類型的實例後,就可以直接從介面上調用它所保存的實例的方法。例如:
fmt.Println(ins1.Area()) // 輸出19.625
fmt.Println(ins2.Area()) // 輸出10.24
這裡ins1.Area()
調用的是Circle類型上的方法Area(),ins2.Area()
調用的則是Square類型上的方法Area()。這說明Go的介面可以實現面向對象中的多態:可以按需調用名稱相同、功能不同的方法。
介面實例中存的是什麼
前面說了,介面類型是指針類型,但是它到底存放了什麼東西?
介面類型的數據結構是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保存的內容則如下圖:
方法集(Method Set)規則
官方手冊對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)
也就是說:
- 值類型的實例的Method Set只由值類型的receiver
(T Type)
組成
- 指針類型的實例的Method Set由值類型和指針類型的receiver共同組成,即
(T Type)
和(T *Type)
這是什麼意思呢?從receiver的角度去考慮:
receiver 實例的類型
---------------------------
(T Type) T 或 *T
(T *Type) *T
上面的意思是:
- 如果某類型實現介面的方法的receiver是
(T *Type)
類型的,那麼只有指針類型的實例*T
才算是實現了這個介面
- 如果某類型實現介面的方法的receiver是
(T Type)
類型的,那麼值類型的實例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())
}
很經常的,我們會直接使用推斷類型的賦值方式(如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。
介面類型作為參數
將介面類型作為參數很常見。這時,那些實現介面的實例都能作為介面類型參數傳遞給函數/方法。
例如,下麵的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()中來,至於每個東西怎麼輸出,那就要看具體情況。
介面類型的嵌套
介面可以嵌套,嵌套的內部介面將屬於外部介面,內部介面的方法也將屬於外部介面。
例如,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()
}
除此之外,類型嵌套時,如果內部類型實現了介面,那麼外部類型也會自動實現介面,因為內部屬性是屬於外部屬性的。