Golang教程:接口


什么是接口

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

在 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 類型的切片給方法 totalExpensetotalExpense 方法出通過調用各自類型的 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  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM