1. 程式人生 > >GO學習筆記——介面(19)

GO學習筆記——介面(19)

什麼是介面?

在面向物件的領域裡,介面一般這樣定義:介面定義一個物件的行為。介面只指定了物件應該做什麼,至於如何實現這個行為(即實現細節),則由物件本身去確定。

在GO語言中,比如說我定義了一個介面download,它內部有一個方法get,任何型別只要定義了這個get方法,就是實現了這個介面。不管型別內部怎麼定義這個get方法,只要定義了,也就是說外部可以呼叫這個方法,這就算實現了這個介面。

這就是典型的 duck typing 的概念,這是很多支援面向物件程式設計的語言都會了解的一個概念。

一隻在漂浮在海上充氣的小黃鴨,只要它長得像鴨子,我們就可以說它就是一隻鴨子。

當然它和真正的鴨子差的很多,但是這就像一個介面,比如我們規定的介面包含下面幾個特點:

  • 嘴巴扁扁長長的
  • 全身黃黃的
  • 有兩個眼睛兩個翅膀的

只要滿足這些特點,那麼它就是一隻鴨子,很顯然,這隻小黃鴨就是一隻鴨子。

總結一下:一個型別,只要定義了介面要求的方法,那麼就是實現了該介面,至於內部怎麼實現的,由型別本身決定。

介面的宣告和定義

package main

import (
	"interface/A"
	"fmt"
)

//定義了一個介面Common,內有一個方法Get
type Common interface {
	Get(key string) string
}

//定義了一個函式,第一個引數是介面
func getValue(c Common,key string) string{
	return c.Get(key)
}

func main() {
	var c Common
	
	a := A.A{Kvs:make(map[string]string)}
	a.Kvs["January"] = "1月"
	c = a
	fmt.Println(getValue(c,"January"))
}

上面我們定義了一個介面,同時定義了一個使用該介面的函式,下面我們定義一個A型別

package A

//定義了一個A型別
type A struct {
	Kvs map[string]string
}

//定義了一個A型別的Get方法
func (a A) Get(key string) string{
	return a.Kvs[key]
}

該型別實現了介面Common所需的Get方法,因此實現了該介面。

執行結果

1月

A型別中有一個map,它定義的Get方法是給一個key返回一個value,接下來再定義一個B型別試試看。

package B

type Mystring string

func (m Mystring) Get(key string) string{
	return key
}

B型別是一個Mystring,它是stirng的別名,它的Get方法是給一個key直接返回key。

func main() {
        var c Common

	//a := A.A{Kvs:make(map[string]string)}
	//a.Kvs["January"] = "1月"
	//fmt.Println(getValue(a,"January"))

	b := B.Mystring("")
	c = b
	fmt.Println(getValue(c,"hello world"))
}

執行結果

hello world

這就起到了介面的效果。A和B型別都實現了Get介面,因此在呼叫介面的函式中,都可以傳A和B型別。

介面的內部表示

介面內部到底是什麼?其實我們可以把介面看成一個(type,value)的元組,它內部有一個type表示這個介面的實際型別,還有一個value表示該實際型別的值。

//定義了一個介面Common,內有一個方法Get
type Common interface {
	Get(key string) string
}

//定義了一個函式,第一個引數是介面
func getValue(c Common,key string) string{
	return c.Get(key)
}

func main() {
	var c Common

	a := A.A{Kvs:make(map[string]string)}
	a.Kvs["January"] = "1月"
	c = a
	fmt.Printf("%T %v\n",c,c)
	//fmt.Println(getValue(c,"January"))
}

執行結果

A.A {map[January:1月]}

%T輸出c的型別,%v輸出c的值,從結果可以看到,c的型別就是A,c的值就是A型別底層的這個map。

型別選擇

語法:interface.(type),該語法和switch語句混用,用於選擇介面底層的型別

//定義了一個介面Common,內有一個方法Get
type Common interface {
	Get(key string) string
}


//做型別選擇
func choose(c Common) {
	switch v := c.(type) {
	case A.A:
		fmt.Println(v.Kvs)
	case B.Mystring:
		fmt.Println(v)
	}
}

func main() {
	var c Common

	a := A.A{Kvs:make(map[string]string)}
	a.Kvs["January"] = "1月"

	b := B.Mystring("pigff")

	choose(a)
	choose(b)
}

執行結果

map[January:1月]
pigff

另外,比較的型別也可以是介面

//定義了一個介面Common,內有一個方法Get
type Common interface {
	Get(key string) string
}


func choose(c Common) {
	switch v := c.(type) {
	case Common:    //與一個介面型別做比較
		fmt.Println(v.Get("January"))
	default:
		fmt.Println(v.Get("January"))
	}
}

func main() {
	a := A.A{Kvs:make(map[string]string)}
	a.Kvs["January"] = "1月"
	
	b := B.Mystring("pigff")

	choose(a)
	choose(b)
}

執行結果

1月
January

型別斷言

語法:interface.(實際型別),該語法用於判斷介面底層型別是不是我們指定的實際型別

//c表示一個空介面,裡面沒有任何方法,因此任何型別都可以滿足該介面
func assert(c interface{}) {
	v := c.(int)
	fmt.Println(v)
}

func main() {
	assert("pigff")
}

程式會報錯

panic: interface conversion: interface {} is string, not int

這裡assert的函式中,我們規定了介面底層的資料型別是int,但是我們傳進去的型別是string,再列印這個值時就會報錯。

注意:這裡有用到了一個空介面,它可以表示任何型別,所以在GO中想表示任何型別就是用一個空介面來表示的。

。但是實際上該語法還有一個返回值用於判斷介面的底層的型別是不是我們想要的型別type。

//c表示一個空介面,裡面沒有任何方法,因此任何型別都可以滿足該介面
func assert(c interface{}) {
	v,ok := c.(int)	//ok是一個bool型別,用於判斷介面底層型別是不是我們想要的int
	fmt.Println(v,ok)
}

func main() {
	assert("pigff")
        assert(1)
}

執行結果

0 false
1 true

如果c的底層型別就是int,那麼v就是該型別的值,ok就是true

如果c的底層型別不是int,那麼v就是int的零值,ok就是false,程式不會報錯

指標接收者與值接收者

我們在說方法的時候,就討論過方法對於這兩個接收者的不同做法(GO方法),這裡介面對於兩種接收方式也有區別。

之前的例子實現方式都是值接收者,下面來看看指標接收者。之前的例子舉得不好,這裡換一個例子。

type print interface {
	Print()
}

type student struct {
	name string
	age int
}

type teacher struct {
	name string
	age int
}

func (s student) Print(){
	fmt.Println(s.name,s.age)
}

func (t *teacher) Print(){
	fmt.Println(t.name,t.age)
}

func main() {
	var v print
	s := student{"pigff",21}
	t := teacher{"Mike",40}

	v = s
	v.Print()
	v = &s
	v.Print()
	
	//這是非法的
	//v = t
	//v.Print()
	
	v = &t
	v.Print()
}

這邊有一個print介面,student和teacher結構體都實現了該介面,都定義了Print方法,只不過student的Print方法的接收者是一個值接收者,teacher的Print方法的接收者是一個指標接收者。

先來看一下輸出結果

pigff 21
pigff 21
Mike 40

可見,對於值接收者,如果我們傳入給介面的型別是一個值還是指標,它都可以進行方法的呼叫;

而對於指標接收者,它必須是一個指標型別,才可以進行方法的呼叫(註釋那部分如果取消註釋,程式會報如下錯誤)

.\main.go:38:4: cannot use t (type teacher) as type print in assignment:
	teacher does not implement print (Print method has pointer receiver)

我們知道,不管是值接收者還是指標接收者,在方法中,我們傳入的是一個值型別還是指標型別,編譯器都能進行相應的轉換。

但是,在這裡,只有值接收者,不管我們傳入的是指標還是方法都可以,如果傳入的是指標那麼會自動獲取到它的值。

但是指標接收者,則必須傳指標型別才可以。這是為什麼?

因為我們在這裡只知道值,編譯器無法自動獲取值對應的地址。(知道地址可以獲得值,但是知道值並不能自動獲取它的地址)

對於方法來說,如果接收者是指標接收者,那麼如果呼叫者是指標或是可取到地址的值,那麼都可以呼叫成功。

但是這裡,這個值是在介面中的,也就是說,介面中儲存的具體的值(介面中存的是型別和值)並不能取到地址,所以這裡會報錯。

所以,總結一下:

指標接收者只能以指標方式使用,而值接收者兩者皆可

實現多個介面

如果一個型別定義了多個介面的方法,那麼它就是實現了多個介面

type print interface {
	Print()
}

type get interface {
	GetName() string
}

type student struct {
	name string
	age int
}

func (s student) Print(){
	fmt.Println(s.name,s.age)
}


func (s student) GetName() string{
	return s.name
}

func main() {
	var v1 print
	var v2 get
	s := student{"pigff",21}

	v1 = s
	v1.Print()

	v2 = s
	fmt.Println(v2.GetName())
}

如上的student型別實現了兩個介面,執行結果如下

pigff 21
pigff

介面的組合巢狀

像上面的程式碼。我們想有一個介面既要滿足介面print,又要滿足介面get,那麼我們就可以把這兩個介面組合成一個介面,這個新的介面就嵌套了這兩個介面。

type print interface {
	Print()
}

type get interface {
	GetName() string
}

type getAndPrint interface {
	//巢狀兩個介面
	print
	get

	//當然還可以有屬於自己介面的方法,這裡不舉例
	//.....
}

type student struct {
	name string
	age int
}

func (s student) Print(){
	fmt.Println(s.name,s.age)
}


func (s student) GetName() string{
	return s.name
}

func fun(c getAndPrint) {
	c.Print()
	fmt.Println(c.GetName())
}

func main() {
	var v getAndPrint
	s := student{"pigff",21}

	v = s
	fun(v)
}

執行結果

pigff 21
pigff

介面的零值

介面的零值是nil,對應的它內部的型別和值也都是nil

func main() {
	var v getAndPrint
	if v == nil {
		fmt.Printf("%T %v\n",v,v)
	}
}

執行結果

<nil> <nil>

如果一個空的介面想要呼叫它的方法,那麼會報panic。

func main() {
	var v getAndPrint
	v.GetName()
}

執行結果

panic: runtime error: invalid memory address or nil pointer dereference