美文网首页
Go教程第十五篇: 接口-1

Go教程第十五篇: 接口-1

作者: 大风过岗 | 来源:发表于2020-04-29 14:38 被阅读0次

    Go教程第十五篇: 接口-1

    本文是《Go系列教程》的第十五篇文章,本章我们将讲解接口的第一部分。

    什么是接口?

    在Go中,接口就是一组方法签名的集合。若一个类定义了接口中的所有方法,我们就说,它实现了这个接口。这和面向对象编程很类似。接口规定了一个类应该有哪些方法,而类可以决定如何实现这些方法。

    例如,洗衣机可以是一个接口,它有俩个方法签名: Cleaning()和Drying()。任一个类只要定义了Cleaning()和Drying()方法,我们就说它实现了洗衣机接口。

    声明和实现接口

    我们来写段程序,创建一个接口并实现它。

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

    在上面的程序中,我们创建了一个接口类型,名叫VowelsFinder,它有一个方法:FindVowels() []rune。在紧接着的一行,又创建了一个类:MyString。在MyString类中,我们添加了一个方法FindVowels()。这时候,我们就可以说MyString实现了VowelsFinder接口。可以看出,这和其他语言例如Java相差很大,对于java而言,一个类必须要使用implements关键字明确地表明它实现了哪个接口。但是在Go里面不必这样搞,对于Go而言,如果一个类包含了接口中声明的所有方法,那么就认为这个类隐式地实现了此接口。

    在28行 v = name中,我们把MyString类型的变量name赋值给VowelsFinder类型的变量v。这样做也是可以的,因为MyString实现了VowelsFinder接口。在v.FindVowels()这行代码中,我们又在MyString类型上调用了FindVowels方法,同时打印出字符串"Sam Anderson"中的所有元音字母。程序的输出如下:

    Vowels are [a e o]
    
    

    到目前为止,我们已经创建并实现了第一个接口。

    使用接口

    上面的案例教会我们如何创建并实现一个接口,但是它并没有真正地告诉我们应该如何使用接口,相比较于v.FindVowels(),如果我们使用name.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 the 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 through 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{
            empId:    1,
            basicpay: 5000,
            pf:       20,
        }
        pemp2 := Permanent{
            empId:    2,
            basicpay: 6000,
            pf:       30,
        }
        cemp1 := Contract{
            empId:    3,
            basicpay: 3000,
        }
        employees := []SalaryCalculator{pemp1, pemp2, cemp1}
        totalExpense(employees)
    
    }
    
    

    在上面程序的第七行中,我们声明了SalaryCalculator接口,这个接口中有一个方法:CalculateSalary() int。

    我们公司中有俩种员工:正式工permanent和合同工contract。正式工的工资是基础工资basicpay和公积金ProvidentFund的合计。而合同工只有基础工资basicpay。这些都在它们各自的CalculateSalary方法里面实现。permanent和contract都实现了SalaryCalculator接口。

    totalExpense函数充分展示了接口的好处。该方法接收了一个[]SalaryCalculator数组作为参数。在程序的第59行 employees := []SalaryCalculator{pemp1, pemp2, cemp1}中,我们传递了一个包含着Permanant和Contract类型的数组给totalExpense函数。totalExpense函数通过调用每个类型各自的CalculateSalary方法计算出各自的花费。程序的输出如下:

    Total Expense Per Month $14050
    

    这样做的最大好处就是,它可以扩展任何新的员工类型,而无需更改totalExpense方法的任何代码。比如说,公司新增加了一个员工类型Freelancer,这个新类型的员工有着不同的工资结构。这时,我们可以在不改变totalExpense函数代码的情况下,支持新增的员工类型。只要Freelancer实现了SalaryCalculator接口即可。

    我们修改一下这个程序并添加一个新的Freelancer员工类型。Freelancer的工资计算是:每小时的产出率以及总的工作时长。

    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
    }
    
    type Freelancer struct {
        empId       int
        ratePerHour int
        totalHours  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
    }
    
    //salary of freelancer
    func (f Freelancer) CalculateSalary() int {
        return f.ratePerHour * f.totalHours
    }
    
    /*
    total expense is calculated by iterating through 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{
            empId:    1,
            basicpay: 5000,
            pf:       20,
        }
        pemp2 := Permanent{
            empId:    2,
            basicpay: 6000,
            pf:       30,
        }
        cemp1 := Contract{
            empId:    3,
            basicpay: 3000,
        }
        freelancer1 := Freelancer{
            empId:       4,
            ratePerHour: 70,
            totalHours:  120,
        }
        freelancer2 := Freelancer{
            empId:       5,
            ratePerHour: 100,
            totalHours:  100,
        }
        employees := []SalaryCalculator{pemp1, pemp2, cemp1, freelancer1, freelancer2}
        totalExpense(employees)
    
    }
    
    

    我们在程序的22行增加了一个Freelancer结构体,并在第39行声明了CalculateSalary()方法。totalExpense方法不需要做出任何改动,因为Freelancer结构体同样实现了SalaryCalculator接口。我们在main方法中,添加了几个Freelancer员工,程序的输出如下:

    Total Expense Per Month $32450
    
    

    接口的内部表示

    我们可以认为接口的内部实现是由一个二元组表示的。(type,value)。type是接口的具体类型,value是具体类型的值。
    我们来写段程序理解下:

    package main
    
    import (
        "fmt"
    )
    
    type Worker interface {
        Work()
    }
    
    type Person struct {
        name string
    }
    
    func (p Person) Work() {
        fmt.Println(p.name, "is working")
    }
    
    func describe(w Worker) {
        fmt.Printf("Interface type %T value %v\n", w, w)
    }
    
    func main() {
        p := Person{
            name: "Naveen",
        }
        var w Worker = p
        describe(w)
        w.Work()
    }
    
    

    接口Worker有一个名为work的方法,同时结构体Person实现了此接口。在程序的第27行,我们把Persoon类型的变量p赋值给Worker类型的变量w。
    那么,w现在的具体类型就是Person,它有一个name字段的值为“Naveen”。第17行的describe()函数打印出了此接口的值以及具体类型。程序的输出为:

    Interface type main.Person value {Naveen}
    Naveen is working
    

    我们将在接下来的小节中中讨论如何提取接口的底层值。

    空接口

    一个方法也没有的接口,我们称之为空接口。它通过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)
    }
    
    

    在上面程序的第七行中,describe(i interface{})函数可以接收一个空接口作为形参,因此,所有的类型都可以作为参数传递给describe函数。

    我们分别把String、int、struct传递给describe函数,程序的输出如下:

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

    类型断言

    类型断言可用于提取接口的底层值。
    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。我们使用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函数试图从s中提取int值。此时程序会提示信息:
    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)
    }
    
    

    当我们把“Steven Paul”传递给assert函数时,ok的值为false,因为i的具体类型不是int,此时v的值为int的零值,
    即:0。程序的输出如下:

    56 true
    0 false
    
    

    类型匹配

    类型匹配可用于将一个接口的类型和多个不同的类型进行比较。它类似于switch case。区别在于,在switch case中比较的是值,而在这里比较的是类型。

    类型匹配的语法和类型断言的语法比较相近。其语法为: i.(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)
    }
    
    

    在上面的程序中,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
    
    

    由于89.98的数据类型是float64,因此它不符合任何的case,因此,就会打印出“Unkown 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接口,在第9行的case语句中,v和接口类型Describer进行比较。p实现了Describer接口,因此,这个case语句满足条件,故而Describe()方法会被调用。程序输出:

    unknown type
    Naveen R is 25 years old
    

    到此为止,我们已经讲述完了接口的第一部分,下面我们将继续讲解接口的第二部分。

    感谢您的阅读,请留下您珍贵的反馈和评论。Have a good Day!

    备注
    本文系翻译之作原文博客地址

    相关文章

      网友评论

          本文标题:Go教程第十五篇: 接口-1

          本文链接:https://www.haomeiwen.com/subject/kdngwhtx.html