- 原文地址:Part 18: Interfaces - I
- 原文作者:Naveen R
- 譯者:咔嘰咔嘰 轉載請註明出處。
什麼是介面
物件導向世界中介面的定義是“介面定義物件的行為”。它只指定物件應該做什麼。實現此行為(實現細節)的方法取決於物件。
在 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 行個用單個方法CalculateSalary() int
宣告瞭SalaryCalculator
介面型別。
我們公司有兩種員工,永久Permanent
和合同Contract
由第一行的結構定義。Permanent
僱員的工資是基本工資和工資的總和,而對於Contract
僱員來說,只需要基本的工資支付,分別在 23 和 28 行相應的CalculateSalary
方法中表示。通過宣告此方法,Permanent
和Contract
現在都實現了SalaryCalculator
介面。
第 36 行中宣告的totalExpense
函式體現了使用介面的美妙之處。此方法使用SalaryCalculator
介面的切片[] SalaryCalculator
作為引數。在第 49 行中,我們將一個包含Permanent
和Contract
型別的切片傳遞給totalExpense
函式。在第 39 行,totalExpense
函式通過呼叫相應型別的CalculateSalary
方法來計算費用。
這樣做的最大好處是totalExpense
可以擴充套件到任何新員工型別,而無需更改任何程式碼。假如 說公司增加了一種具有不同薪資結構的新型員工Freelancer
自由職業者。這個Freelancer
可以在 slice 引數中傳遞給totalExpense
,totalExpense
函式甚至沒有一行程式碼更改。這個方法將做它應該做的事情,而Freelancer
也將實現SalaryCalculator
介面:)。
程式輸出,Total Expense Per Month $14050
介面的內部表示
介面在內部可以被認為是由元組(type, value)
表示。 type
是介面的基礎具體型別,value
儲存具體型別的值。
讓我們寫一段程式碼加深理解,
package main
import (
"fmt"
)
type Tester interface {
Test()
}
type MyFloat float64
func (m MyFloat) Test() {
fmt.Println(m)
}
func describe(t Tester) {
fmt.Printf("Interface type %T value %v\n", t, t)
}
func main() {
var t Tester
f := MyFloat(89.7)
t = f
describe(t)
t.Test()
}
複製程式碼
Tester
介面有一個方法Test()
,MyFloat
型別實現該介面。在 24 行中,我們將MyFloat
型別的變數f
賦給 Tester 型別的t
。現在t
的具體型別是MyFloat
,t
的值是 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{})
函式將空介面作為引數,因此可以傳遞任何型別。
我們將string
,int
和struct
分別為 13,15 和 21 行傳遞給describe
函式,這個程式列印,
Type = string, value = Hello World
Type = int, value = 55
Type = struct { name string }, value = {Naveen R}
複製程式碼
型別斷言
型別斷言用於獲取介面的基礎值。
i.(T)
是用於獲取具體型別為T
的i
介面的基礎值的語法。
程式碼勝過言語。讓我們寫一個型別斷言。
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
函式,該函式嘗試從中提取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
的零值並且程式將不會發生混亂。
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 的零值。該程式將列印,
56 true
0 false
複製程式碼
型別 Switch
型別switch
用於將介面的具體型別與各種case
語句中指定的多種型別進行比較。它類似於switch case
。唯一的區別是case
指定型別而不是正常switch
中的值。
type switch
的語法類似於Type
斷言。將型別斷言的語法i.(T)
的型別T
替換為型別switch
的關鍵字type
就行了。讓我們看看下面的程式如何工作。
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)
指定了型別switch
。每個case
語句都將i
的具體型別與特定型別進行比較。如果匹配,則列印相應的語句。該程式輸出,
I am a string and my value is Naveen
I am an int and my value is 77
Unknown type
複製程式碼
第 20 行的89.98
是float64
型別,它不匹配任何case
,因此在最後一行列印未知型別。
還可以將型別與介面進行比較。如果我們有一個型別,並且該型別實現了一個介面,則可以將該型別與它實現的介面進行比較。
來寫一段程式碼讓理解更清晰,
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
與Describer
介面型別進行比較。 p
實現了Describer
,因此滿足了這種情況,當程式執行findType(p)
時,呼叫了Describe()
方法。
列印,
unknown type
Naveen R is 25 years old
複製程式碼
介面的第一部分就結束了。我們將在第二部分中繼續討論介面。