GO语言学习笔记-接口1

原文

接口是什么?

在面向对象的世界中,接口的意思是:“接口定义了对象的行为”。它只表明对象应该能做什么,而具体怎么做则由对象内部实现。

Go语言中,接口是方法的集合,若某个类型实现了接口中定义的所有方法,则可以说这个类型实现了这个接口。用OOP的形式来说就是: 接口表明了类型应该有哪些方法,而类型则决定如何实现那些方法

比如,WashingMachine这个接口中可以定义Cleaning()Drying()方法,任何类型实现了这2个方法都可以说是实现了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行我们给MyString类型添加了FindVowels() []rune方法,现在我们可以说MyString实现了接口VowelsFinder。这里和Java这种需要使用关键字implements显式声明实现某接口不同,GO中并不需要这个关键字,而是如果某个类型全部定义了接口中的方法,则隐式的实现了这个接口。

第28行,我们将MyString类型的变量name转换成了VowelsFinder接口类型,这是可以的因为MyString实现了VowelsFinder接口。接下来通过v.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方法。

公司中有2种雇员,PermanentContract。Permanent的工资由basicpaypf构成,而Contract仅有basicpay。各自的CalculateSalary方法在23、28行实现。Permanent和Contract都实现了SalaryCalculator接口。

36行定义的totalExpense方法是一种十分漂亮的接口使用案例,这个方法参数是一个由SalaryCalculator接口类型组成的切片。49行我们把合同工和临时工这2种类型数据组成了一个切片传递给了totalExpense方法,而totalExpense方法通过调用各自的CalculateSalary函数来计算相应的花费。

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类型的变量fTest类型并赋值给t。现在具体类型是MyFloat并且值为89.7describe函数输出了接口的真实类型和值,输出如下:

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{})函数使用一个空接口作为参数,所以可以接受任何类型数据。我们分别传递intstringstruct类型数据到函数中,输出如下:

Type = string, value = Hello World  
Type = int, value = 55  
Type = struct { name string }, value = {Naveen R}  

类型断言

类型断言被用于提取接口的隐含值(underlying value)。

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)
}

s的真实类型是int所以我们在第8行使用语法i.(int)来获取i的隐藏整数值,程序输出为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函数,并且尝试从中获取一个整数的值,程序将报错panic: interface conversion: interface {} is string, not int

解决这个问题可以使用语法

v, ok := i.(T)

如果i的类型是T,那么v将得到i的隐含值并且ok为true。否则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类型的零值是0。(roy注:把assert函数中的int改成string则将返回空字符串,零值意思应该是默认初始值。)

56 true  
0 false

类型判断

(roy注:原文是Type Switch,内容就是使用switch语法判断某接口的类型。)

Type Switch用来比较某个接口的具体类型是否在多个指定的case分支中,和switch-case语法类似,区别就是case后的类型不再是普通的值。

语法上和类型断言也很像,在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的实际类型和后面指定的类型。如果有匹配的则进行相应的输出。执行结果如下:

I am a string and my value is Naveen  
I am an int and my value is 77  
Unknown type  

88.98是float64类型不匹配任何一项,所以最后输出Unknow type

对某类型和接口进行比较也是可以的,如果我们定义了某个类型并且这个实现了某个接口,那么就可以对这2者进行比较:

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行,变量vDescriber接口类型进行比较。p实现了Describer所以满足第一个case判断,Describe函数也被调用。输出如下:

unknown type  
Naveen R is 25 years old

这篇文章至此结束,剩下的部分将在第二篇文章中讨论。