[譯] part18: golang 介面 1

咔嘰咔嘰發表於2019-04-08

什麼是介面

物件導向世界中介面的定義是“介面定義物件的行為”。它只指定物件應該做什麼。實現此行為(實現細節)的方法取決於物件。

在 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())

}
複製程式碼

Run in playgroud

上述程式的第 8 行建立了一個名為VowelsFinder的介面型別,它有一個方法FindVowels() []rune

下一行建立了MyString型別。

在第 15 行,我們將FindVowels() []rune方法新增到接收者型別MyString中。現在MyString實現了VowelsFinder介面。這與 Java 等其他語言完全不同,其中類必須明確宣告使用implements關鍵字去實現介面,而在 go 中這是是不需要的,如果型別包含介面中宣告的所有方法,那麼就說 go 實現了介面。

在第 28 行中,我們將型別為MyStringname賦值給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)

}
複製程式碼

Run in playground

上面程式的第 7 行個用單個方法CalculateSalary() int宣告瞭SalaryCalculator介面型別。

我們公司有兩種員工,永久Permanent和合同Contract由第一行的結構定義。Permanent僱員的工資是基本工資和工資的總和,而對於Contract僱員來說,只需要基本的工資支付,分別在 23 和 28 行相應的CalculateSalary方法中表示。通過宣告此方法,PermanentContract現在都實現了SalaryCalculator介面。

第 36 行中宣告的totalExpense函式體現了使用介面的美妙之處。此方法使用SalaryCalculator介面的切片[] SalaryCalculator作為引數。在第 49 行中,我們將一個包含PermanentContract型別的切片傳遞給totalExpense函式。在第 39 行,totalExpense函式通過呼叫相應型別的CalculateSalary方法來計算費用。

這樣做的最大好處是totalExpense可以擴充套件到任何新員工型別,而無需更改任何程式碼。假如 說公司增加了一種具有不同薪資結構的新型員工Freelancer自由職業者。這個Freelancer可以在 slice 引數中傳遞給totalExpensetotalExpense函式甚至沒有一行程式碼更改。這個方法將做它應該做的事情,而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()
}
複製程式碼

Run in playground

Tester介面有一個方法Test()MyFloat型別實現該介面。在 24 行中,我們將MyFloat型別的變數f賦給 Tester 型別的t。現在t的具體型別是MyFloatt的值是 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)
}
複製程式碼

Run in playground

在上面程式的第 7 行中,describe(i interface{})函式將空介面作為引數,因此可以傳遞任何型別。

我們將stringintstruct分別為 13,15 和 21 行傳遞給describe函式,這個程式列印,

Type = string, value = Hello World  
Type = int, value = 55  
Type = struct { name string }, value = {Naveen R}  
複製程式碼

型別斷言

型別斷言用於獲取介面的基礎值。

i.(T)是用於獲取具體型別為Ti介面的基礎值的語法。

程式碼勝過言語。讓我們寫一個型別斷言。

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)
}
複製程式碼

Run in playground

在第 12 行,s的具體型別是int。我們在第 8 行中使用語法i.(int)去獲取iint型基礎值。該程式列印 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)
}

複製程式碼

Run in playground

在上面的程式中,我們將具體型別為strings傳遞給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)
}
複製程式碼

Run in playground

當 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)
}
複製程式碼

Run in playground

在上述程式的第 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.98float64型別,它不匹配任何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)
}
複製程式碼

Run in playgroud

在上面的程式中,Person結構實現了Describer介面。在第 19 行中的 case 語句,將vDescriber介面型別進行比較。 p實現了Describer,因此滿足了這種情況,當程式執行findType(p)時,呼叫了Describe()方法。

列印,

unknown type  
Naveen R is 25 years old  
複製程式碼

介面的第一部分就結束了。我們將在第二部分中繼續討論介面。

相關文章