Go学习笔记06-接口

什么是接口

在Go语言中,接口就是方法签名的集合。当一个类型定义了接口中的所有方法,我们称它实现了该接口。接口指定了一个类型应该具有的方法,并由该类型决定如何实现这些方法。

接口的声明与实现

如下所示,在Go中,如果一个类型包含了接口中声明的所有方法,那么它就隐式地实现了Go接口。

package main

import "fmt"

type VowelsFinder interface {
    FindVowels() []rune
}

type MyString string

func (ms MyString) FindVowels() []rune {
    var vowels []rune
    for _, c := range ms {
        if c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u' {
            vowels = append(vowels, c)
        }
    }
    return vowels
}

func main() {
    name := MyString("Sam Anderson")
    var v VowelsFinder
    v = name
    fmt.Printf("Vowels are %c\n", v.FindVowels()) // Vowels are [a e o]
}

接口的实际用途

通过一个例子来说明接口的实际应用场景:根据公司员工的个人薪资,计算公司的总支出。

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
}

func (p Permanent) CalculateSalary() int {
    return p.basicpay + p.pf
}

func (c Contract) CalculateSalary() int {
    return c.basicpay
}

// 这里体现了接口的妙用,totalExpense 可以扩展新的员工类型,假设公司增加了一种新的员工类型
// Freelancer,它有着不同的薪资结构。Freelancer 只需传递到 totalExpense 的切片参数中,
// 无需 totalExpense 方法本身进行修改。只要 Freelancer 也实现了 SalaryCalculator 接口,
// totalExpense 就能够实现其功能,这样,totalExpense 函数中就实现了多态。
func totalExpense(s []SalaryCalculator) {
    expense := 0
    for _, v := range s {
        expense = expense + v.CalculateSalary()
    }
    fmt.Printf("Total expense per month $%d\n", expense)
}

func main() {
    pemp1 := Permanent{1, 5000, 20}
    pemp2 := Permanent{2, 6000, 30}
    cemp1 := Contract{3, 3000}
    employees := []SalaryCalculator{pemp1, pemp2, cemp1}
    totalExpense(employees) // 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) // Interface type main.MyFloat value 89.7
    t.Test() // 89.7
}

空接口

没有包含方法的接口称为空接口。空接口表示为interface{}。由于空接口没有方法,因此所有类型都实现了空接口。

类型断言

类型断言用于提取接口的底层值。在语法i.(T)中,接口i的具体类型是T,该语法用于获得接口的底层值。举个例子:

package main

import "fmt"

func assert(i interface{}) {
    s := i.(int)
    fmt.Println(s)
}

func main() {
    var s interface{} = 56 // s 的具体类型是 int
    assert(s) // 56
}

在上面的例子中,如果具体类型不是int,assert函数将会报错,所以合理的做法应该是:v, ok := i.(T),这样,如果i的具体类型是T,那么v赋值为i的底层值,而ok赋值为true;如果i的具体类型不是T,那么ok赋值为falsev赋值为T类型的零值。

类型选择(Type Switch)

类型选择用于将接口的具体类型与很多case语句所指定的类型进行比较。它与一般的switch语句类似,唯一的区别在于类型选择指定的是类型,而一般的switch指定的是值

类型选择的语法类似于类型断言。类型断言的语法是i.(T),而对于类型选择,类型T由关键字type代替,举个例子:

package main

import "fmt"

func findType(i interface{}) {
    switch i.(type) {
    case string:
        fmt.Printf("I'm a string and my value is %s\n", i.(string))
    case int:
        fmt.Printf("I'm an int and my value is %d\n", i.(int))
    default:
        fmt.Printf("Unknown type\n")
    }
}

func main() {
    findType("Naveen") // I'm a string and my value is Naveen
    findType(77)       // I'm an int and my value is 77
    findType(89.89)    // 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\n", 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") // Unknown type

    p := Person{
        name: "Naveen R",
        age:  25,
    }
    findType(p) // Naveen R is 25 years old
}

实现接口:指针接收者与值接收者

我们可以使用值接收者或者指针接收者来实现接口,但在使用指针接收者实现接口时,有一些细节需要注意,如下代码:

package main

import "fmt"

type Describer interface {
    Describe()
}

type Person struct {
    name string
    age  int
}

func (p Person) Describe() { // 使用值接收者实现了 Describer 接口
    fmt.Printf("%s is %d years old\n", p.name, p.age)
}

type Address struct {
    state   string
    country string
}

func (a *Address) Describe() { // 使用指针接收者实现了 Describer 接口
    fmt.Printf("State %s Country %s\n", a.state, a.country)
}

func main() {
    var d1 Describer
    p1 := Person{"Sam", 25}
    d1 = p1
    d1.Describe() // Sam is 25 years old
    p2 := Person{"James", 32}
    d1 = &p2
    d1.Describe() // James is 32 years old

    var d2 Describer
    a := Address{"Washington", "USA"}
    
    // 如果取消下面 d2 = a 这行注释会导致编译报错:因为编译器无法自动获取 a 的地址
    // cannot use a (type Address) as type Describer in assignment: Address 
    // does not implement Describer (Describe method has pointer receiver)
    // d2 = a

    d2 = &a
    d2.Describe()
}

总结:对于使用指针接收者的方法,用一个指针或者一个可取得地址的值来调用都是合法的。而对于接口不同,使用值接收者实现接口的对象,可以把它的值或者指向它的指针赋值给接口变量;而使用指针接收者实现接口的对象,则必须把它的指针赋值给接口变量。

实现多个接口

假设我们声明了两个接口:SalaryCalculatorLeaveCalculator,而结构体Employee同时实现了SalaryCalculatorLeaveCalculator接口的方法,那么Employee就实现了SalaryCalculatorLeaveCalculator两个接口。而结构体Employee变量可以赋值给SalaryCalculator类型的接口变量,也可以赋值给LeaveCalculator类型的接口变量。

接口的嵌套

Go语言没有提供继承机制,但可以通过嵌套其它的接口来创建一个新接口。

package main

import "fmt"

type SalaryCalculator interface {
    DisplaySalary()
}

type LeaveCalculator interface {
    CalculateLeavesLeft() int
}

type EmployeeOperations interface {
    SalaryCalculator
    LeaveCalculator
}

type Employee struct {
    firstName   string
    lastName    string
    basicPay    int
    pf          int
    totalLeaves int
    leavesTaken int
}

func (e Employee) DisplaySalary() {
    fmt.Printf("%s %s has salary $%d\n", e.firstName, e.lastName, e.basicPay+e.pf)
}

func (e Employee) CalculateLeavesLeft() int {
    return e.totalLeaves - e.leavesTaken
}

func main() {
    e := Employee{
        firstName:   "Naveen",
        lastName:    "Ramanathan",
        basicPay:    5000,
        pf:          200,
        totalLeaves: 30,
        leavesTaken: 5,
    }
    var empOp EmployeeOperations = e
    empOp.DisplaySalary() // Naveen Ramanathan has salary $5200
    fmt.Printf("Leaves left = %d\n", empOp.CalculateLeavesLeft()) // Leaves left = 25
}

接口的零值

接口的零值是nil。对于值为nil的接口,其底层值和具体类型都为nil,当试图调用它的方法时,程序会产生panic异常。

package main

import "fmt"

type Describer interface {
    Describe()
}

func main() {
    var d Describer
    if d == nil {
        // Output: d is nil and has type <nil> value <nil>
        fmt.Printf("d is nil and has type %T value %v\n", d, d)
    }
}

reference:

https://studygolang.com/articles/12266
https://studygolang.com/articles/12325