首頁 > 軟體

Go基礎教學系列之Go介面使用詳解

2022-04-16 19:00:04

介面用法簡介

介面(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儲存的內容則如下圖:

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

方法集(Method Set)規則

官方手冊對Method Set的解釋:https://golang.org/ref/spec

範例的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是指標型別的方法只可能存在於指標型別的實體方法集中
  • receiver是值型別的方法既存在於值型別的實體方法集中,也存在於指標型別的方法集中

從實現介面方法的角度上看:

  • 如果某型別實現介面的方法的receiver是(T *Type)型別的,那麼只有指標型別的範例*T才算是實現了這個介面,因為這個方法不在值型別的範例T方法集中
  • 如果某型別實現介面的方法的receiver是(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)的地址

abcabc.go:11:22: cannot call pointer method on myint(3)
abcabc.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()
}

除此之外,型別巢狀時,如果內部型別實現了介面,那麼外部型別也會自動實現介面,因為內部屬性是屬於外部屬性的。

更多關於Go基礎教學系列之Go介面的使用方法請檢視下面的相關連結


IT145.com E-mail:sddin#qq.com