什么是接口
在面向對象語言中,接口一般被定義為 :接口定義了一個對象的行為。它僅僅指定了一個對象應該做什么。具體怎么做(實現細節)是由對象決定的。
在 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
