這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。歡迎來到 [Golang 系列教程](/subject/2)的第 18 個教程。介面共有兩個教程,這是我們介面的第一個教程。 ### 什麼是介面?在物件導向的領域裡,介面一般這樣定義:**介面定義一個對象的行為**。介面只指定了對象應該做什麼,至於如何?這個行為(即實現細節),則由對象本身去確定。 在 Go 語言中,介面就是方法簽名(Method Signature)的集合。當一個類型定義了介面中的所有方法,我們稱它實現了該介面。這與物件導向編程(OOP)的說法很類似。**介面指定了一個類型應該具有的方法,並由該類型決定如何?這些方法**。 例如,`WashingMachine` 是一個含有 `Cleaning()` 和 `Drying()` 兩個方法的介面。任何定義了 `Cleaning()` 和 `Drying()` 的類型,都稱它實現了 `WashingMachine` 介面。 ### 介面的聲明與實現讓我們編寫代碼,建立一個介面並且實現它。```gopackage mainimport ( "fmt")//interface definitiontype VowelsFinder interface { FindVowels() []rune}type MyString string//MyString implements VowelsFinderfunc (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())}``` [線上運行程式](https://play.golang.org/p/F-T3S_wNNB) 在上面程式的第 8 行,建立了一個名為 `VowelsFinder` 的介面,該介面有一個 `FindVowels() []rune` 的方法。 在接下來的一行,我們建立了一個 `MyString` 類型。 **在第 15 行,我們給接受者類型(Receiver Type) `MyString` 添加了方法 `FindVowels() []rune`。現在,我們稱 `MyString` 實現了 `VowelsFinder` 介面。這就和其他語言(如 Java)很不同,其他一些語言要求一個類使用 `implement` 關鍵字,來顯式地聲明該類實現了介面。而在 Go 中,並不需要這樣。如果一個類型包含了介面中聲明的所有方法,那麼它就隱式地實現了 Go 介面**。 在第 28 行,`v` 的類型為 `VowelsFinder`,`name` 的類型為 `MyString`,我們把 `name` 賦值給了 `v`。由於 `MyString` 實現了 `VowelFinder`,因此這是合法的。在下一行,`v.FindVowels()` 調用了 `MyString` 類型的 `FindVowels` 方法,列印字串 `Sam Anderson` 裡所有的母音。該程式輸出 `Vowels are [a e o]`。 祝賀!你已經建立並實現了你的第一個介面。 ### 介面的實際用途前面的例子教我們建立並實現了介面,但還沒有告訴我們介面的實際用途。在上面的程式裡,如果我們使用 `name.FindVowels()`,而不是 `v.FindVowels()`,程式依然能夠照常運行,但介面並沒有體現出實際價值。 因此,我們現在討論一下介面的實際應用情境。 我們編寫一個簡單程式,根據公司員工的個人薪資,計算公司的總支出。為了簡單起見,我們假定支出的單位都是美元。 ```gopackage mainimport ( "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 pffunc (p Permanent) CalculateSalary() int { return p.basicpay + p.pf}//salary of contract employee is the basic pay alonefunc (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)}```[線上運行程式](https://play.golang.org/p/5t6GgQ2TSU) 上面程式的第 7 行聲明了一個 `SalaryCalculator` 介面類型,它只有一個方法 `CalculateSalary() int`。 在公司裡,我們有兩類員工,即第 11 行和第 17 行定義的結構體:`Permanent` 和 `Contract`。長期員工(`Permanent`)的薪資是 `basicpay` 與 `pf` 相加之和,而合約員工(`Contract`)只有基本工資 `basicpay`。在第 23 行和第 28 行中,方法 `CalculateSalary` 分別實現了以上關係。由於 `Permanent` 和 `Contract` 都聲明了該方法,因此它們都實現了 `SalaryCalculator` 介面。 第 36 行聲明的 `totalExpense` 方法體現出了介面的妙用。該方法接收一個 `SalaryCalculator` 介面的切片(`[]SalaryCalculator`)作為參數。在第 49 行,我們向 `totalExpense` 方法傳遞了一個包含 `Permanent` 和 `Contact` 類型的切片。在第 39 行中,通過調用不同類型對應的 `CalculateSalary` 方法,`totalExpense` 可以計算得到支出。 這樣做最大的優點是:`totalExpense` 可以擴充新的員工類型,而不需要修改任何代碼。假如公司增加了一種新的員工類型 `Freelancer`,它有著不同的薪資結構。`Freelancer`只需傳遞到 `totalExpense` 的切片參數中,無需 `totalExpense` 方法本身進行修改。只要 `Freelancer` 也實現了 `SalaryCalculator` 介面,`totalExpense` 就能夠實現其功能。 該程式輸出 `Total Expense Per Month $14050`。 ### 介面的內部表示我們可以把介面看作內部的一個元組 `(type, value)`。 `type` 是介面底層的具體類型(Concrete Type),而 `value` 是具體類型的值。 我們編寫一個程式來更好地理解它。 ```gopackage mainimport ( "fmt")type Test interface { Tester()}type MyFloat float64func (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()}```[線上運行程式](https://play.golang.org/p/Q40Omtewlh) `Test` 介面只有一個方法 `Tester()`,而 `MyFloat` 類型實現了該介面。在第 24 行,我們把變數 `f`(`MyFloat` 類型)賦值給了 `t`(`Test` 類型)。現在 `t` 的具體類型為 `MyFloat`,而 `t` 的值為 `89.7`。第 17 行的 `describe` 函數列印出了介面的具體類型和值。該程式輸出: ```Interface type main.MyFloat value 89.7 89.7 ```### 空介面沒有包含方法的介面稱為空白介面。空介面表示為 `interface{}`。由於空介面沒有方法,因此所有類型都實現了空介面。 ```gopackage mainimport ( "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)}```[線上運行程式](https://play.golang.org/p/Fm5KescoJb) 在上面的程式的第 7 行,`describe(i interface{})` 函數接收空介面作為參數,因此,可以給這個函數傳遞任何類型。 在第 13 行、第 15 行和第 21 行,我們分別給 `describe` 函數傳遞了 `string`、`int` 和 `struct`。該程式列印: ```Type = string, value = Hello World Type = int, value = 55 Type = struct { name string }, value = {Naveen R} ```### 類型斷言類型斷言用於提取介面的底層值(Underlying Value)。 在文法 `i.(T)` 中,介面 `i` 的具體類型是 `T`,該文法用於獲得介面的底層值。 一段代碼勝過千言。下面編寫個關於類型斷言的程式。 ```gopackage mainimport ( "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)}```[線上運行程式](https://play.golang.org/p/YstKXEeSBL) 在第 12 行,`s` 的具體類型是 `int`。在第 8 行,我們使用了文法 `i.(int)` 來提取 `i` 的底層 int 值。該程式會列印 `56`。 在上面程式中,如果具體類型不是 int,會發生什麼呢?接下來看看。 ```gopackage mainimport ( "fmt")func assert(i interface{}) { s := i.(int) fmt.Println(s)}func main() { var s interface{} = "Steven Paul" assert(s)}```[線上運行程式](https://play.golang.org/p/88KflSceHK) 在上面程式中,我們把具體類型為 `string` 的 `s` 傳遞給了 `assert` 函數,試圖從它提取出 int 值。該程式會報錯:`panic: interface conversion: interface {} is string, not int.`。 要解決該問題,我們可以使用以下文法: ```gov, ok := i.(T) ```如果 `i` 的具體類型是 `T`,那麼 `v` 賦值為 `i` 的底層值,而 `ok` 賦值為 `true`。 如果 `i` 的具體類型不是 `T`,那麼 `ok` 賦值為 `false`,`v` 賦值為 `T` 類型的零值,**此時程式不會報錯**。 ```gopackage mainimport ( "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)}```[線上運行程式](https://play.golang.org/p/0sB-KlVw8A) 當給 `assert` 函數傳遞 `Steven Paul` 時,由於 `i` 的具體類型不是 `int`,`ok` 賦值為 `false`,而 `v` 賦值為 0(int 的零值)。該程式列印: ```56 true 0 false ```### 類型選擇(Type Switch)類型選擇用於將介面的具體類型與很多 case 語句所指定的類型進行比較。它與一般的 switch 語句類似。唯一的區別在於類型選擇指定的是類型,而一般的 switch 指定的是值。 類型選擇的文法類似於類型斷言。類型斷言的文法是 `i.(T)`,而對於類型選擇,類型 `T` 由關鍵字 `type` 代替。下面看看程式是如何工作的。 ```gopackage mainimport ( "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)}```[線上運行程式](https://play.golang.org/p/XYPDwOvoCh) 在上述程式的第 8 行,`switch i.(type)` 表示一個類型選擇。每個 case 語句都把 `i` 的具體類型和一個指定類型進行了比較。如果 case 匹配成功,會列印出相應的語句。該程式輸出: ```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`。 **還可以將一個類型和介面相比較。如果一個類型實現了介面,那麼該類型與其實現的介面就可以互相比較**。 為了闡明這一點,下面寫一個程式。 ```gopackage mainimport "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)}```[線上運行程式](https://play.golang.org/p/o6aHzIz4wC) 在上面程式中,結構體 `Person` 實現了 `Describer` 介面。在第 19 行的 case 語句中,`v` 與介面類型 `Describer` 進行了比較。`p` 實現了 `Describer`,因此滿足了該 case 語句,於是當程式運行到第 32 行的 `findType(p)` 時,程式調用了 `Describe()` 方法。 該程式輸出:```unknown type Naveen R is 25 years old ```介面(一)的內容到此結束。在介面(二)中我們還會繼續討論介面。祝您愉快! **上一教程 - [方法](https://studygolang.com/articles/12264)****下一教程 - [介面 - II](https://studygolang.com/articles/12325)**
via: https://golangbot.com/interfaces-part-1/
作者:Nick Coghlan 譯者:Noluye 校對:polaris1119
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
3874 次點擊