Go基礎系列:Go介面
介面用法簡介
介面(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的解釋: ofollow,noindex" target="_blank">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() }
除此之外,型別巢狀時,如果內部型別實現了介面,那麼外部型別也會自動實現介面,因為內部屬性是屬於外部屬性的。