Go 基礎系列:Go 接口

接口用法簡介

type Namer interface {
    my_method1()
    my_method2(para)
    my_method3(para) return_type
    ...
}

當用戶自定義的類型實現了接口上定義的這些方法,那麼自定義類型的值 (也就是實例) 可以賦值給接口類型的值(也就是接口實例)。這個賦值過程使得接口實例中保存了用戶自定義類型實例。

例如:

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)
}

上面將輸出:

從上面輸出結果中可以看出,兩個接口實例 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 保存的內容則如下圖:

實際上接口實例中保存的內容,在反射 (reflect) 中體現的淋漓盡致,reflect 所有的一切都離不開接口實例保存的內容。

方法集 (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)

也就是說:

這是什麼意思呢?從 receiver 的角度去考慮:

receiver        實例的類型
---------------------------
(T Type)        T 或 *T
(T *Type)       *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()。

以接口作爲方法或函數的參數,將使得一切都變得靈活且通用,只要是實現了接口的類型實例,都可以去調用它。

用的非常多的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()
}

除此之外,類型嵌套時,如果內部類型實現了接口,那麼外部類型也會自動實現接口,因爲內部屬性是屬於外部屬性的。

原文鏈接:https://www.cnblogs.com/f-ck-need-u/9940845.html

版權聲明:本文爲博主「駿馬金龍」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/MDwP8zSytHiiZbwG4vTgzw