Go 經(jīng)典入門系列 18:接口(一)
歡迎來到 Golang 系列教程[1]的第 18 個(gè)教程。接口共有兩個(gè)教程,這是我們接口的第一個(gè)教程。
什么是接口?
在面向?qū)ο蟮念I(lǐng)域里,接口一般這樣定義:接口定義一個(gè)對象的行為。接口只指定了對象應(yīng)該做什么,至于如何實(shí)現(xiàn)這個(gè)行為(即實(shí)現(xiàn)細(xì)節(jié)),則由對象本身去確定。
在 Go 語言中,接口就是方法簽名(Method Signature)的集合。當(dāng)一個(gè)類型定義了接口中的所有方法,我們稱它實(shí)現(xiàn)了該接口。這與面向?qū)ο缶幊蹋∣OP)的說法很類似。接口指定了一個(gè)類型應(yīng)該具有的方法,并由該類型決定如何實(shí)現(xiàn)這些方法。
例如,WashingMachine 是一個(gè)含有 Cleaning() 和 Drying() 兩個(gè)方法的接口。任何定義了 Cleaning() 和 Drying() 的類型,都稱它實(shí)現(xiàn)了 WashingMachine 接口。
接口的聲明與實(shí)現(xiàn)
讓我們編寫代碼,創(chuàng)建一個(gè)接口并且實(shí)現(xiàn)它。
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())
}
在線運(yùn)行程序[2]
在上面程序的第 8 行,創(chuàng)建了一個(gè)名為 VowelsFinder 的接口,該接口有一個(gè) FindVowels() []rune 的方法。
在接下來的一行,我們創(chuàng)建了一個(gè) MyString 類型。
在第 15 行,我們給接受者類型(Receiver Type) MyString 添加了方法 FindVowels() []rune。現(xiàn)在,我們稱 MyString 實(shí)現(xiàn)了 VowelsFinder 接口。這就和其他語言(如 Java)很不同,其他一些語言要求一個(gè)類使用 implement 關(guān)鍵字,來顯式地聲明該類實(shí)現(xiàn)了接口。而在 Go 中,并不需要這樣。如果一個(gè)類型包含了接口中聲明的所有方法,那么它就隱式地實(shí)現(xiàn)了 Go 接口。
在第 28 行,v 的類型為 VowelsFinder,name 的類型為 MyString,我們把 name 賦值給了 v。由于 MyString 實(shí)現(xiàn)了 VowelFinder,因此這是合法的。在下一行,v.FindVowels() 調(diào)用了 MyString 類型的 FindVowels 方法,打印字符串 Sam Anderson 里所有的元音。該程序輸出 Vowels are [a e o]。
祝賀!你已經(jīng)創(chuàng)建并實(shí)現(xiàn)了你的第一個(gè)接口。
接口的實(shí)際用途
前面的例子教我們創(chuàng)建并實(shí)現(xiàn)了接口,但還沒有告訴我們接口的實(shí)際用途。在上面的程序里,如果我們使用 name.FindVowels(),而不是 v.FindVowels(),程序依然能夠照常運(yùn)行,但接口并沒有體現(xiàn)出實(shí)際價(jià)值。
因此,我們現(xiàn)在討論一下接口的實(shí)際應(yīng)用場景。
我們編寫一個(gè)簡單程序,根據(jù)公司員工的個(gè)人薪資,計(jì)算公司的總支出。為了簡單起見,我們假定支出的單位都是美元。
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)
}
在線運(yùn)行程序[3]
上面程序的第 7 行聲明了一個(gè) SalaryCalculator 接口類型,它只有一個(gè)方法 CalculateSalary() int。
在公司里,我們有兩類員工,即第 11 行和第 17 行定義的結(jié)構(gòu)體:Permanent 和 Contract。長期員工(Permanent)的薪資是 basicpay 與 pf 相加之和,而合同員工(Contract)只有基本工資 basicpay。在第 23 行和第 28 行中,方法 CalculateSalary 分別實(shí)現(xiàn)了以上關(guān)系。由于 Permanent 和 Contract 都聲明了該方法,因此它們都實(shí)現(xiàn)了 SalaryCalculator 接口。
第 36 行聲明的 totalExpense 方法體現(xiàn)出了接口的妙用。該方法接收一個(gè) SalaryCalculator 接口的切片([]SalaryCalculator)作為參數(shù)。在第 49 行,我們向 totalExpense 方法傳遞了一個(gè)包含 Permanent 和 Contact 類型的切片。在第 39 行中,通過調(diào)用不同類型對應(yīng)的 CalculateSalary 方法,totalExpense 可以計(jì)算得到支出。
這樣做最大的優(yōu)點(diǎn)是:totalExpense 可以擴(kuò)展新的員工類型,而不需要修改任何代碼。假如公司增加了一種新的員工類型 Freelancer,它有著不同的薪資結(jié)構(gòu)。Freelancer只需傳遞到 totalExpense 的切片參數(shù)中,無需 totalExpense 方法本身進(jìn)行修改。只要 Freelancer 也實(shí)現(xiàn)了 SalaryCalculator 接口,totalExpense 就能夠?qū)崿F(xiàn)其功能。
該程序輸出 Total Expense Per Month $14050。
接口的內(nèi)部表示
我們可以把接口看作內(nèi)部的一個(gè)元組 (type, value)。type 是接口底層的具體類型(Concrete Type),而 value 是具體類型的值。
我們編寫一個(gè)程序來更好地理解它。
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()
}
在線運(yùn)行程序[4]
Test 接口只有一個(gè)方法 Tester(),而 MyFloat 類型實(shí)現(xiàn)了該接口。在第 24 行,我們把變量 f(MyFloat 類型)賦值給了 t(Test 類型)?,F(xiàn)在 t 的具體類型為 MyFloat,而 t 的值為 89.7。第 17 行的 describe 函數(shù)打印出了接口的具體類型和值。該程序輸出:
Interface?type?main.MyFloat?value?89.7
89.7
空接口
沒有包含方法的接口稱為空接口。空接口表示為 interface{}。由于空接口沒有方法,因此所有類型都實(shí)現(xiàn)了空接口。
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)
}
在線運(yùn)行程序[5]
在上面的程序的第 7 行,describe(i interface{}) 函數(shù)接收空接口作為參數(shù),因此,可以給這個(gè)函數(shù)傳遞任何類型。
在第 13 行、第 15 行和第 21 行,我們分別給 describe 函數(shù)傳遞了 string、int 和 struct。該程序打?。?/p>
Type?=?string,?value?=?Hello?World
Type?=?int,?value?=?55
Type?=?struct?{?name?string?},?value?=?{Naveen?R}
類型斷言
類型斷言用于提取接口的底層值(Underlying Value)。
在語法 i.(T) 中,接口 i 的具體類型是 T,該語法用于獲得接口的底層值。
一段代碼勝過千言。下面編寫個(gè)關(guān)于類型斷言的程序。
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)
}
在線運(yùn)行程序[6]
在第 12 行,s 的具體類型是 int。在第 8 行,我們使用了語法 i.(int) 來提取 i 的底層 int 值。該程序會打印 56。
在上面程序中,如果具體類型不是 int,會發(fā)生什么呢?接下來看看。
package?main
import?(
????"fmt"
)
func?assert(i?interface{})?{
????s?:=?i.(int)
????fmt.Println(s)
}
func?main()?{
????var?s?interface{}?=?"Steven?Paul"
????assert(s)
}
在線運(yùn)行程序[7]
在上面程序中,我們把具體類型為 string 的 s 傳遞給了 assert 函數(shù),試圖從它提取出 int 值。該程序會報(bào)錯(cuò):panic: interface conversion: interface {} is string, not int.。
要解決該問題,我們可以使用以下語法:
v,?ok?:=?i.(T)
如果 i 的具體類型是 T,那么 v 賦值為 i 的底層值,而 ok 賦值為 true。
如果 i 的具體類型不是 T,那么 ok 賦值為 false,v 賦值為 T 類型的零值,此時(shí)程序不會報(bào)錯(cuò)。
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)
}
在線運(yùn)行程序[8]
當(dāng)給 assert 函數(shù)傳遞 Steven Paul 時(shí),由于 i 的具體類型不是 int,ok 賦值為 false,而 v 賦值為 0(int 的零值)。該程序打印:
56?true
0?false
類型選擇(Type Switch)
類型選擇用于將接口的具體類型與很多 case 語句所指定的類型進(jìn)行比較。它與一般的 switch 語句類似。唯一的區(qū)別在于類型選擇指定的是類型,而一般的 switch 指定的是值。
類型選擇的語法類似于類型斷言。類型斷言的語法是 i.(T),而對于類型選擇,類型 T 由關(guān)鍵字 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)
}
在線運(yùn)行程序[9]
在上述程序的第 8 行,switch i.(type) 表示一個(gè)類型選擇。每個(gè) case 語句都把 i 的具體類型和一個(gè)指定類型進(jìn)行了比較。如果 case 匹配成功,會打印出相應(yīng)的語句。該程序輸出:
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 上匹配成功,因此最后一行打印了 Unknown type。
還可以將一個(gè)類型和接口相比較。如果一個(gè)類型實(shí)現(xiàn)了接口,那么該類型與其實(shí)現(xiàn)的接口就可以互相比較。
為了闡明這一點(diǎn),下面寫一個(gè)程序。
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)
}
在線運(yùn)行程序[10]
在上面程序中,結(jié)構(gòu)體 Person 實(shí)現(xiàn)了 Describer 接口。在第 19 行的 case 語句中,v 與接口類型 Describer 進(jìn)行了比較。p 實(shí)現(xiàn)了 Describer,因此滿足了該 case 語句,于是當(dāng)程序運(yùn)行到第 32 行的 findType(p) 時(shí),程序調(diào)用了 Describe() 方法。
該程序輸出:
unknown?type
Naveen?R?is?25?years?old
接口(一)的內(nèi)容到此結(jié)束。在接口(二)中我們還會繼續(xù)討論接口。祝您愉快!
上一教程 - 方法
下一教程 - 接口 - II[11]
via: https://golangbot.com/interfaces-part-1/
作者:Nick Coghlan[12]譯者:Noluye[13]校對:polaris1119[14]
本文由 GCTT[15] 原創(chuàng)編譯,Go 中文網(wǎng)[16] 榮譽(yù)推出
參考資料
Golang 系列教程: https://studygolang.com/subject/2
[2]在線運(yùn)行程序: https://play.golang.org/p/F-T3S_wNNB
[3]在線運(yùn)行程序: https://play.golang.org/p/5t6GgQ2TSU
[4]在線運(yùn)行程序: https://play.golang.org/p/Q40Omtewlh
[5]在線運(yùn)行程序: https://play.golang.org/p/Fm5KescoJb
[6]在線運(yùn)行程序: https://play.golang.org/p/YstKXEeSBL
[7]在線運(yùn)行程序: https://play.golang.org/p/88KflSceHK
[8]在線運(yùn)行程序: https://play.golang.org/p/0sB-KlVw8A
[9]在線運(yùn)行程序: https://play.golang.org/p/XYPDwOvoCh
[10]在線運(yùn)行程序: https://play.golang.org/p/o6aHzIz4wC
[11]接口 - II: https://studygolang.com/articles/12325
[12]Nick Coghlan: https://golangbot.com/about/
[13]Noluye: https://github.com/Noluye
[14]polaris1119: https://github.com/polaris1119
[15]GCTT: https://github.com/studygolang/GCTT
[16]Go 中文網(wǎng): https://studygolang.com/
推薦閱讀
