什么是接口
在面向對象語言中,接口一般被定義為 :接口定義了一個對象的行為。它僅僅指定了一個對象應該做什么。具體怎么做(實現細節)是由對象決定的。
在 Go 中,一個接口定義為若干方法的簽名。當一個類型定義了所有接口里的方法時,就說這個類型實現了這個接口。這和 OOP 很像。接口指定了一個類型應該包含什么方法,而該類型決定怎么實現這些方法。
比如 WashingMachine
可以作為一個接口,並提供兩個函數 Cleaning()
和 Drying()
。任何提供了 Cleaning()
和 Drying()
方法定義的類型就可以說它實現了 WashingMachine
接口。
聲明和實現接口
讓我們通過一個程序看一下如何聲明和實現一個接口
package main import ( "fmt" ) //interface definition type VowelsFinder interface { FindVowels() []rune } type MyString string //MyString implements VowelsFinder func (ms MyString) FindVowels() []rune { var vowels []rune for _, rune := range ms { if rune == 'a' || rune == 'e' || rune == 'i' || rune == 'o' || rune == 'u' { vowels = append(vowels, rune) } } return vowels } func main() { name := MyString("Sam Anderson") var v VowelsFinder v = name // possible since MyString implements VowelsFinder fmt.Printf("Vowels are %c", v.FindVowels()) }
程序的第8行創建了一個接口類型名為 VowelsFinder
,它有一個方法 FindVowels() []rune
。
在下一行 MyString
類型被創建。
在第15行我們添加了一個方法 FindVowels() []rune
給接受者類型 MyString
。現在可以說 MyString
實現了 VowelsFinder
接口。這和其他語言大大的不同,比如在Java中,一個類必須用 implements
關鍵字顯式的標明實現了一個接口。這在Go中是不需要的,在Go中,如果一個類型包含了一個接口聲明的所有方法,那么這個類型就隱式地實現了這個接口。
在第28行,我們將 MyString
類型的變量 name
賦值給 VowelsFinder
類型的變量 v
。這是合法的,因為 MyString
實現了 VowelsFinder
。在下一行,v.FindVowels()
在 MyString
上調用 FindVowels
方法打印在 Sam Anderson
中所有的元音。程序的輸出為:Vowels are [a e o]
。
恭喜!你已經創建並實現了你的第一個接口。
接口的實際用途
上面的程序告訴我們怎么創建和實現接口,但是沒有展示接口的實際用途。在上面的程序中,我們可以使用 name.FindVowels()
替代 v.FindVowels()
,程序照樣可以工作,那么我們為什么還要使用接口呢?
現在讓我們來看接口的一個實際用途。
我們將編寫一個簡單的程序,根據員工的個人工資計算公司的總支出。為了簡潔起見,我們假設所有費用都是美元。
package main import ( "fmt" ) type SalaryCalculator interface { CalculateSalary() int } type Permanent struct { empId int basicpay int pf int } type Contract struct { empId int basicpay int } //salary of permanent employee is sum of basic pay and pf func (p Permanent) CalculateSalary() int { return p.basicpay + p.pf } //salary of contract employee is the basic pay alone func (c Contract) CalculateSalary() int { return c.basicpay } /* total expense is calculated by iterating though the SalaryCalculator slice and summing the salaries of the individual employees */ func totalExpense(s []SalaryCalculator) { expense := 0 for _, v := range s { expense = expense + v.CalculateSalary() } fmt.Printf("Total Expense Per Month $%d", expense) } func main() { pemp1 := Permanent{1, 5000, 20} pemp2 := Permanent{2, 6000, 30} cemp1 := Contract{3, 3000} employees := []SalaryCalculator{pemp1, pemp2, cemp1} totalExpense(employees) }
上面程序地第7行,定義了一個 SalaryCalculator
接口類型,該接口只聲明了一個方法:CalculateSalary() int
。
在公司中我們有兩種類型的員工:Permanent
和 Contract
(第 11 和 17 行)。永久性員工的工資是 basicpay
和 pf
的總和,而合同員工的工資只是基本工資 basicpay
。這分別在第23行和第28行的方法 CalculateSalary
中表示。通過聲明這個方法,Permanent
和 Contract
都實現了 SalaryCalculator
接口。
第36行中, totalExpense
方法的聲明展示了使用接口的好處。這個方法接收一個 SalaryCalculator
接口的切片 []SalaryCalculator
作為參數。在第49行,我們將一個包含了 Permanent
和 Contract
類型的切片給方法 totalExpense
。totalExpense
方法出通過調用各自類型的 CalculateSalary
方法來計算總支。這是在第 39行完成的。
最大的優點是可以將 totalExpense
擴展到任何新的員工類型,而不必修改任何代碼。假設該公司引入了第三種員工類型 Freelancer
和不同的計算工資的方法。那么 Freelancer
可以被包含在傳遞給 totalExpense
的切片參數中,而不需要修改任何 totalExpense
方法的接口。這個方法會做自己應該做的事情,因為 Freelancer
同樣實現了 SalaryCalculator
接口:)
程序的輸出為:Total Expense Per Month $14050
。
接口的內部表示
可以把接口想象成這樣一個元組 (type, value)
。type
是接口包含的具體類型,value
是接口包含的具體的值。
讓我們寫一個程序來理解這一點。
package main import ( "fmt" ) type Test interface { Tester() } type MyFloat float64 func (m MyFloat) Tester() { fmt.Println(m) } func describe(t Test) { fmt.Printf("Interface type %T value %v\n", t, t) } func main() { var t Test f := MyFloat(89.7) t = f describe(t) t.Tester() }
Test
接口提供了一個方法 Tester()
,MyFloat
類型實現了這個接口。在第 24 行,我們將 MyFloat
類型的變量 f
賦值給 Test
類型的變量 t
。現在 t
的具體類型是 MyFloat
而它的值是 89.7
。在第17行, describe
函數打印接口的值和具體類型。程序的輸出為:
Interface type main.MyFloat value 89.7 89.7
空接口
一個沒有聲明任何方法的接口稱為空接口。空接口表示為 interface{}
。因為空接口沒有方法,因此所有類都都實現了空接口。
package main import ( "fmt" ) func describe(i interface{}) { fmt.Printf("Type = %T, value = %v\n", i, i) } func main() { s := "Hello World" describe(s) i := 55 describe(i) strt := struct { name string }{ name: "Naveen R", } describe(strt) }
上面程序的第7行,describe(i interface{})
函數接受一個空接口作為參數,因此任何值都可以傳遞給它。
在第13行,15行,21行,我們傳遞 string,int 和結構體給 describe
。程序的輸出結果為:
Type = string, value = Hello World Type = int, value = 55 Type = struct { name string }, value = {Naveen R}
類型斷言
類型斷言(type assertion)用來提取接口的實際類型的值。
i.(T)是用來獲取接口 i
的實際類型 T
的值的語法。
一個程序勝過千言萬語:),讓我們寫一個類型斷言。
package main import ( "fmt" ) func assert(i interface{}) { s := i.(int) //get the underlying int value from i fmt.Println(s) } func main() { var s interface{} = 56 assert(s) }
第12行,s
的實際類型是 int
。在第8 行我們使用 i.(int)
來獲取 i
的 int
值。程序的輸出為:56
。
如果實際類型不是 int
,那么上面的程序會發生什么?讓我們找出來:
package main import ( "fmt" ) func assert(i interface{}) { s := i.(int) fmt.Println(s) } func main() { var s interface{} = "Steven Paul" assert(s) }
在上面的程序中,我們將實際類型為 string
的變量 s
傳遞給 assert
函數,assert
函數嘗試從其中提取出一個 int
值。該程序會觸發 panic:panic: interface conversion: interface {} is string, not int
。
為了解決以上問題,我們可以使用下面的語法:
v, ok := i.(T)
如果 i
的具體類型是 T
,則 v
將具有 i
的實際值,ok
為 true
。
如果 i
的具體類型不是 T
,則 ok
將為 false
, v
將具有 T
的 0 值,程序不會觸發 panic。
package main import ( "fmt" ) func assert(i interface{}) { v, ok := i.(int) fmt.Println(v, ok) } func main() { var s interface{} = 56 assert(s) var i interface{} = "Steven Paul" assert(i) }
當 Steven Paul
傳遞給 assert
函數,ok
將是 false
因為 i
的實際類型不是 int
並且 v
的值將是 0(int
的 0 值)。程序將輸出:
56 true 0 false
Type Switch
類型分支(type switch)用來將一個接口的具體類型與多個 case 語句指定的類型進行比較。這很像普通的 switch 語句。唯一不同的是 type switch 中 case 指定的是類型,而普通的 switch 語句中 case 指定的是值。
type switch 的語法與類型斷言和很相似。在類型斷言 i.(T)
中,將類型 T
替換為關鍵字 type
就變成了 type switch。讓我們通過下面的程序看看它是如何工作的。
package main import ( "fmt" ) func findType(i interface{}) { switch i.(type) { case string: fmt.Printf("I am a string and my value is %s\n", i.(string)) case int: fmt.Printf("I am an int and my value is %d\n", i.(int)) default: fmt.Printf("Unknown type\n") } } func main() { findType("Naveen") findType(77) findType(89.98) }
上面的程序中,第8行,switch i.(type)
是一個 type switch。每一個 case
語句都將 i
的實際類型和 case 指定的類型相比較。如果一個 case 匹配,則打印相應的語句。程序的輸出為:
I am a string and my value is Naveen I am an int and my value is 77 Unknown type
在第20行,89.98
是 flaot64
類型,並不匹配任何一個 case
。因此在會后一行打印:Unknown type
。
也可以將類型和接口進行比較。如果我們有一個類型,並且該類型實現了一個接口,那么這個類型可以和它實現的接口進行比較。
讓我們寫一個程序來更清楚地了解這一點。
package main import "fmt" type Describer interface { Describe() } type Person struct { name string age int } func (p Person) Describe() { fmt.Printf("%s is %d years old", p.name, p.age) } func findType(i interface{}) { switch v := i.(type) { case Describer: v.Describe() default: fmt.Printf("unknown type\n") } } func main() { findType("Naveen") p := Person{ name: "Naveen R", age: 25, } findType(p) }
在上面的程序中,Person
結構體實現了 Describer
接口。在第 19 行的 case 語句,v
和 Describe
接口進行比較。由於 p
實現了 Describer
接口因此這個 case
匹配成功,在第32行 findType(p)
中 Person
的 Describe()
方法被調用。
程序的輸出為:
unknown type Naveen R is 25 years old