Files
build-web-application-with-…/zh-tw/02.6.md
2019-02-26 01:40:54 +08:00

14 KiB
Raw Blame History

2.6 interface

interface

Go語言裡面設計最精妙的應該算interface它讓物件導向內容組織實現非常的方便當你看完這一章你就會被interface的巧妙設計所折服。

什麼是interface

簡單的說interface是一組method簽名的組合我們透過interface來定義物件的一組行為。

我們前面一章最後一個例子中Student和Employee都能SayHi雖然他們的內部實現不一樣但是那不重要重要的是他們都能say hi

讓我們來繼續做更多的擴充套件Student和Employee實現另一個方法Sing然後Student實現方法BorrowMoney而Employee實現SpendSalary。

這樣Student實現了三個方法SayHi、Sing、BorrowMoney而Employee實現了SayHi、Sing、SpendSalary。

上面這些方法的組合稱為interface(被物件Student和Employee實現)。例如Student和Employee都實現了interfaceSayHi和Sing也就是這兩個物件是該interface型別。而Employee沒有實現這個interfaceSayHi、Sing和BorrowMoney因為Employee沒有實現BorrowMoney這個方法。

interface型別

interface型別定義了一組方法如果某個物件實現了某個介面的所有方法則此物件就實現了此介面。詳細的語法參考下面這個例子


type Human struct {
	name string
	age int
	phone string
}

type Student struct {
	Human //匿名欄位Human
	school string
	loan float32
}

type Employee struct {
	Human //匿名欄位Human
	company string
	money float32
}

//Human物件實現Sayhi方法
func (h *Human) SayHi() {
	fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}

// Human物件實現Sing方法
func (h *Human) Sing(lyrics string) {
	fmt.Println("La la, la la la, la la la la la...", lyrics)
}

//Human物件實現Guzzle方法
func (h *Human) Guzzle(beerStein string) {
	fmt.Println("Guzzle Guzzle Guzzle...", beerStein)
}

// Employee過載Human的Sayhi方法
func (e *Employee) SayHi() {
	fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
		e.company, e.phone) //此句可以分成多行
}

//Student實現BorrowMoney方法
func (s *Student) BorrowMoney(amount float32) {
	s.loan += amount // (again and again and...)
}

//Employee實現SpendSalary方法
func (e *Employee) SpendSalary(amount float32) {
	e.money -= amount // More vodka please!!! Get me through the day!
}

// 定義interface
type Men interface {
	SayHi()
	Sing(lyrics string)
	Guzzle(beerStein string)
}

type YoungChap interface {
	SayHi()
	Sing(song string)
	BorrowMoney(amount float32)
}

type ElderlyGent interface {
	SayHi()
	Sing(song string)
	SpendSalary(amount float32)
}

透過上面的程式碼我們可以知道interface可以被任意的物件實現。我們看到上面的Men interface被Human、Student和Employee實現。同理一個物件可以實現任意多個interface例如上面的Student實現了Men和YoungChap兩個interface。

最後任意的型別都實現了空interface(我們這樣定義interface{})也就是包含0個method的interface。

interface值

那麼interface裡面到底能存什麼值呢如果我們定義了一個interface的變數那麼這個變數裡面可以存實現這個interface的任意型別的物件。例如上面例子中我們定義了一個Men interface型別的變數m那麼m裡面可以存Human、Student或者Employee值。

因為m能夠持有這三種類型的物件所以我們可以定義一個包含Men型別元素的slice這個slice可以被賦予實現了Men介面的任意結構的物件這個和我們傳統意義上面的slice有所不同。

讓我們來看一下下面這個例子:


package main

import "fmt"

type Human struct {
	name string
	age int
	phone string
}

type Student struct {
	Human //匿名欄位
	school string
	loan float32
}

type Employee struct {
	Human //匿名欄位
	company string
	money float32
}

//Human實現SayHi方法
func (h Human) SayHi() {
	fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}

//Human實現Sing方法
func (h Human) Sing(lyrics string) {
	fmt.Println("La la la la...", lyrics)
}

//Employee過載Human的SayHi方法
func (e Employee) SayHi() {
	fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
		e.company, e.phone)
	}

// Interface Men被Human,Student和Employee實現
// 因為這三個型別都實現了這兩個方法
type Men interface {
	SayHi()
	Sing(lyrics string)
}

func main() {
	mike := Student{Human{"Mike", 25, "222-222-XXX"}, "MIT", 0.00}
	paul := Student{Human{"Paul", 26, "111-222-XXX"}, "Harvard", 100}
	sam := Employee{Human{"Sam", 36, "444-222-XXX"}, "Golang Inc.", 1000}
	tom := Employee{Human{"Tom", 37, "222-444-XXX"}, "Things Ltd.", 5000}

	//定義Men型別的變數i
	var i Men

	//i能儲存Student
	i = mike
	fmt.Println("This is Mike, a Student:")
	i.SayHi()
	i.Sing("November rain")

	//i也能儲存Employee
	i = tom
	fmt.Println("This is tom, an Employee:")
	i.SayHi()
	i.Sing("Born to be wild")

	//定義了slice Men
	fmt.Println("Let's use a slice of Men and see what happens")
	x := make([]Men, 3)
	//這三個都是不同型別的元素但是他們實現了interface同一個介面
	x[0], x[1], x[2] = paul, sam, mike

	for _, value := range x{
		value.SayHi()
	}
}

透過上面的程式碼你會發現interface就是一組抽象方法的集合它必須由其他非interface型別實現而不能自我實現 Go透過interface實現了duck-typing:即"當看到一隻鳥走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,那麼這隻鳥就可以被稱為鴨子"。

空interface

空interface(interface{})不包含任何的method正因為如此所有的型別都實現了空interface。空interface對於描述起不到任何的作用(因為它不包含任何的method但是空interface在我們需要儲存任意型別的數值的時候相當有用因為它可以儲存任意型別的數值。它有點類似於C語言的void*型別。


// 定義a為空介面
var a interface{}
var i int = 5
s := "Hello world"
// a可以儲存任意型別的數值
a = i
a = s

一個函式把interface{}作為引數那麼他可以接受任意型別的值作為引數如果一個函式返回interface{},那麼也就可以返回任意型別的值。是不是很有用啊!

interface函式引數

interface的變數可以持有任意實現該interface型別的物件這給我們編寫函式(包括method)提供了一些額外的思考我們是不是可以透過定義interface引數讓函式接受各種型別的引數。

舉個例子fmt.Println是我們常用的一個函式但是你是否注意到它可以接受任意型別的資料。開啟fmt的原始碼檔案你會看到這樣一個定義:


type Stringer interface {
	 String() string
}

也就是說任何實現了String方法的型別都能作為引數被fmt.Println呼叫,讓我們來試一試


package main
import (
	"fmt"
	"strconv"
)

type Human struct {
	name string
	age int
	phone string
}

// 透過這個方法 Human 實現了 fmt.Stringer
func (h Human) String() string {
	return "❰"+h.name+" - "+strconv.Itoa(h.age)+" years -  ✆ " +h.phone+"❱"
}

func main() {
	Bob := Human{"Bob", 39, "000-7777-XXX"}
	fmt.Println("This Human is : ", Bob)
}

現在我們再回顧一下前面的Box示例你會發現Color結構也定義了一個methodString。其實這也是實現了fmt.Stringer這個interface即如果需要某個型別能被fmt套件以特殊的格式輸出你就必須實現Stringer這個介面。如果沒有實現這個介面fmt將以預設的方式輸出。


//實現同樣的功能
fmt.Println("The biggest one is", boxes.BiggestsColor().String())
fmt.Println("The biggest one is", boxes.BiggestsColor())

實現了error介面的物件即實現了Error() string的物件使用fmt輸出時會呼叫Error()方法因此不必再定義String()方法了。

interface變數儲存的型別

我們知道interface的變數裡面可以儲存任意型別的數值(該型別實現了interface)。那麼我們怎麼反向知道這個變數裡面實際儲存了的是哪個型別的物件呢?目前常用的有兩種方法:

  • Comma-ok斷言

    Go語言裡面有一個語法可以直接判斷是否是該型別的變數 value, ok = element.(T)這裡value就是變數的值ok是一個bool型別element是interface變數T是斷言的型別。

    如果element裡面確實儲存了T型別的數值那麼ok返回true否則返回false。

    讓我們透過一個例子來更加深入的理解。


	package main

	import (
		"fmt"
		"strconv"
	)

	type Element interface{}
	type List [] Element

	type Person struct {
		name string
		age int
	}

	//定義了String方法實現了fmt.Stringer
	func (p Person) String() string {
		return "(name: " + p.name + " - age: "+strconv.Itoa(p.age)+ " years)"
	}

	func main() {
		list := make(List, 3)
		list[0] = 1 // an int
		list[1] = "Hello" // a string
		list[2] = Person{"Dennis", 70}

		for index, element := range list {
			if value, ok := element.(int); ok {
				fmt.Printf("list[%d] is an int and its value is %d\n", index, value)
			} else if value, ok := element.(string); ok {
				fmt.Printf("list[%d] is a string and its value is %s\n", index, value)
			} else if value, ok := element.(Person); ok {
				fmt.Printf("list[%d] is a Person and its value is %s\n", index, value)
			} else {
				fmt.Printf("list[%d] is of a different type\n", index)
			}
		}
	}
是不是很簡單啊同時你是否注意到了多個if裡面還記得我前面介紹流程時講過if裡面允許初始化變數。

也許你注意到了我們斷言的型別越多那麼if else也就越多所以才引出了下面要介紹的switch。
  • switch測試

    最好的講解就是程式碼例子,現在讓我們重寫上面的這個實現


	package main

	import (
		"fmt"
		"strconv"
	)

	type Element interface{}
	type List [] Element

	type Person struct {
		name string
		age int
	}

	//列印
	func (p Person) String() string {
		return "(name: " + p.name + " - age: "+strconv.Itoa(p.age)+ " years)"
	}

	func main() {
		list := make(List, 3)
		list[0] = 1 //an int
		list[1] = "Hello" //a string
		list[2] = Person{"Dennis", 70}

		for index, element := range list{
			switch value := element.(type) {
				case int:
					fmt.Printf("list[%d] is an int and its value is %d\n", index, value)
				case string:
					fmt.Printf("list[%d] is a string and its value is %s\n", index, value)
				case Person:
					fmt.Printf("list[%d] is a Person and its value is %s\n", index, value)
				default:
					fmt.Println("list[%d] is of a different type", index)
			}
		}
	}
這裡有一點需要強調的是:`element.(type)`語法不能在switch外的任何邏輯裡面使用如果你要在switch外面判斷一個型別就使用`comma-ok`。

嵌入interface

Go裡面真正吸引人的是它內建的邏輯語法就像我們在學習Struct時學習的匿名欄位多麼的優雅啊那麼相同的邏輯引入到interface裡面那不是更加完美了。如果一個interface1作為interface2的一個嵌入欄位那麼interface2隱式的包含了interface1裡面的method。

我們可以看到原始碼套件container/heap裡面有這樣的一個定義


type Interface interface {
	sort.Interface //嵌入欄位sort.Interface
	Push(x interface{}) //a Push method to push elements into the heap
	Pop() interface{} //a Pop elements that pops elements from the heap
}

我們看到sort.Interface其實就是嵌入欄位把sort.Interface的所有method給隱式的包含進來了。也就是下面三個方法


type Interface interface {
	// Len is the number of elements in the collection.
	Len() int
	// Less returns whether the element with index i should sort
	// before the element with index j.
	Less(i, j int) bool
	// Swap swaps the elements with indexes i and j.
	Swap(i, j int)
}

另一個例子就是io套件下面的 io.ReadWriter 它包含了io套件下面的Reader和Writer兩個interface


// io.ReadWriter
type ReadWriter interface {
	Reader
	Writer
}

反射

Go語言實現了反射所謂反射就是能檢查程式在執行時的狀態。我們一般用到的套件是reflect套件。如何運用reflect套件官方的這篇文章詳細的講解了reflect套件的實現原理laws of reflection

使用reflect一般分成三步下面簡要的講解一下要去反射是一個型別的值(這些值都實現了空interface)首先需要把它轉化成reflect物件(reflect.Type或者reflect.Value根據不同的情況呼叫不同的函式)。這兩種取得方式如下:


t := reflect.TypeOf(i)    //得到型別的元資料,透過t我們能取得型別定義裡面的所有元素
v := reflect.ValueOf(i)   //得到實際的值透過v我們取得儲存在裡面的值還可以去改變值

轉化為reflect物件之後我們就可以進行一些操作了也就是將reflect物件轉化成相應的值例如


tag := t.Elem().Field(0).Tag  //取得定義在struct裡面的標籤
name := v.Elem().Field(0).String()  //取得儲存在第一個欄位裡面的值

取得反射值能返回相應的型別和數值


var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())
fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
fmt.Println("value:", v.Float())

最後,反射的話,那麼反射的欄位必須是可修改的,我們前面學習過傳值和傳參考,這個裡面也是一樣的道理。反射的欄位必須是可讀寫的意思是,如果下面這樣寫,那麼會發生錯誤


var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1)

如果要修改相應的值,必須這樣寫


var x float64 = 3.4
p := reflect.ValueOf(&x)
v := p.Elem()
v.SetFloat(7.1)

上面只是對反射的簡單介紹,更深入的理解還需要自己在程式設計中不斷的實踐。