Go语言接口

什么是接口?

在 Go 中,接口是一组方法签名。当一个类型为接口中的所有方法提供定义时,就说它实现了该接口。

它与 OOP 世界非常相似。接口指定类型应该具有哪些方法,类型决定如何实现这些方法。

例如*,WashingMachine* 可以是具[方法签名 Clean( ) 和 Drying() 的接口。任何为 Clean() 和 Drying() 方法提供定义的类型都被称为实现 WashingMachine 接口。

声明和实现接口

让我们直接深入研究创建接口并实现它的程序。

go 复制代码
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 playground

上面程序创建了一个名为 VowelsFinder的接口类型,它有一个方法FindVowels() []rune

在下一行中,MyString创建了一个类型。

在15 行中。我们将FindVowels() []rune方法添加到接收者MyString类型中。

就可以说现在MyString实现该接口VowelsFinder

这与 Java 等其他语言有很大不同,在 Java 中,类必须使用关键字显式声明它实现了接口implements

这在 Go 中是不需要的,如果类型包含接口中声明的所有方法,则 Go 接口会隐式实现。

在第 28 行中,我们将MyString类型分配给 v 。这是可能的,因为MyString实现了接口。 所以可以调用 v.FindVowels()

该程序输出

Vowels are [a e o]  

恭喜!您已经创建并实现了您的第一个接口。

接口的实际使用

上面的例子教会了我们如何创建和实现接口,但并没有真正展示接口的实际用途。相反v.FindVowels(),如果我们name.FindVowels()在上面的程序中使用,它也会起作用,并且接口将没有用处。

现在让我们看一下接口的实际使用。

我们将编写一个简单的程序,根据员工的个人工资计算公司的总费用。为简洁起见,我们假设所有费用均以美元为单位。

go 复制代码
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)

}

Run in playground

上述程序,声明一个接口SalaryCalculator里面有一个方法 CalculateSalary()

我们公司有两种员工,用PermanentContract的结构体来定义。 正式员工的工资是基本工资和公积金之和,而合同员工的工资只是基本工资。

两个结构体分别都实现了该接口。

第 36 行中声明的totalExpense函数表达了接口的美感。此方法将 []SalaryCalculator作为参数传入方法中。

在第 59 行中,我们将一个包含两者的类型切片传递给函数。该函数通过调用相应类型的方法来计算费用。

程序输出

Total Expense Per Month $14050  

这样做的最大优点是可以扩展到任何新员工类型,而无需更改任何代码。假设公司增加了一种具有不同薪酬结构的新型员工。这可以在 slice 参数中传递,甚至不需要对函数进行任何代码更改。此方法将执行它应该执行的操作,也将实现接口

让我们修改这个程序并添加新Freelancer员工。自由职业者的工资是每小时工资和总工作时间的乘积。

go 复制代码
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)

}

Run in playground

我们在第 1 行添加了Freelancer结构体。并在第 22 行声明该方法CalculateSalary

由于Freelancerstruct 也实现了该接口,因此该方法中不需要更改其他代码。我们在该方法中添加了几个Freelancer员工。该程序打印,

Total Expense Per Month $32450  

接口内部表示

(type, value)接口可以被认为是由元组在内部表示的。type是接口的基础具体类型,value是保存具体类型的值。

让我们编写一个程序来更好地理解。

go 复制代码
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()
}

Run in playground

Worker 接口有一种方法Work()Person结构类型实现该接口。

在27行中,我们将类型为Person的变量赋值给 Worker的具体类型,它包含一个name字段。

该程序输出

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

我们将在接下来的章节中详细讨论如何提取接口的潜在价值。

空接口

**具有零个方法的接口称为空接口。它表示为interface{}。**由于空接口有零个方法,因此所有类型都实现空接口。

go 复制代码
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{})函数采用一个空接口作为参数,因此可以传递任何类型。

我们将string,intstructdescribe传递。该程序打印,

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

类型断言

类型断言用于提取接口的底层值。

**i.(T)**是用于获取i具体类型为T 的接口的基础值的语法。

一个程序抵得上一千个字😀。让我们为类型断言编写一个。

go 复制代码
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 值

该程序打印

56

如果上面程序中的具体类型不是 int 会发生什么?好吧,让我们找出答案。

go 复制代码
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

在上面的程序中,我们将s具体类型传递stringassert尝试从中提取 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的零值,并且程序不会死机。

go 复制代码
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,因为 T 的具体类型不是intv其值为 0,即int 的零值。该程序将打印,

56 true  
0 false  

类型开关

类型开关用于将接口的具体类型与各种 case 语句中指定的多种类型进行比较。它类似于switch。唯一的区别是 case 指定类型而不是像普通 switch 中那样指定值。

类型切换的语法类似于类型断言。在类型断言的语法中i.(T)T应替换为switch的关键字type。让我们看看下面的程序是如何工作的。

go 复制代码
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

上面程序,switch i.(type)指定了一个类型开关。每个 case 语句都将 i的具体类型与特定类型进行比较。如果有任何 case 匹配,则打印相应的语句。

该程序输出,

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

第20 行 89.98是float64类型,与任何情况都不匹配,因此Unknown type打印在最后一行。

还可以将类型与接口进行比较。如果我们有一个类型,并且该类型实现了一个接口,则可以将该类型与它实现的接口进行比较。

为了更清楚起见,让我们编写一个程序。

go 复制代码
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 playground

在上面的程序中,Person结构体实现了Describer接口。在第19行的case中是比较Describer接口类型。p实现Describer,因此满足这种情况并Describe()调用方法。

该程序打印

unknown type  
Naveen R is 25 years old  

使用指针接收器与值接收器实现接口

我们在上面讨论的所有示例接口都是使用值接收器实现的。也可以使用指针接收器实现接口。在使用指针接收器实现接口时需要注意一个微妙之处。

让我们使用以下程序来理解这一点。

go 复制代码
package main

import "fmt"

type Describer interface {  
    Describe()
}
type Person struct {  
    name string
    age  int
}

func (p Person) Describe() { //implemented using value receiver  
    fmt.Printf("%s is %d years old\n", p.name, p.age)
}

type Address struct {  
    state   string
    country string
}

func (a *Address) Describe() { //implemented using pointer receiver  
    fmt.Printf("State %s Country %s", a.state, a.country)
}

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

    var d2 Describer
    a := Address{"Washington", "USA"}

    /* compilation error if the following line is
       uncommented
       cannot use a (type Address) as type Describer
       in assignment: Address does not implement
       Describer (Describe method has pointer
       receiver)
    */
    //d2 = a

    d2 = &a //This works since Describer interface
    //is implemented by Address pointer in line 22
    d2.Describe()

}

Run in playground

在上面的程序中,Person结构体使用第 13 行中的值接收器实现Describer接口。

正如我们在讨论方法时已经了解到的那样,具有值接收器的方法同时接受指针和值接收器。对任何值或可以取消引用的值调用值方法是合法的。

p1是一个Person类型的值,在地29行,p1被分配给了d1,然后在30行调用 d1.Describe() 将打印Sam is 25 years old

同样在第 32 行中,d1被分配给&p2,因此第 33 行将打印James is 32 years old

Address结构在第 22 行中的使用指针接收器实现Describer接口。

上面的程序中的把45 行注释取消,我们会得到编译错误main.go:42: Cannot use a (type Address) as typeDescriber in assignment: Address does notimplementDescriber(Describemethodhaspointerreceiver)

这是因为,该Describer接口是使用第 22 行中的地址指针接收器实现的,我们正在尝试分配a值类型,但它尚未实现该Describer接口。这肯定会让您感到惊讶,因为我们之前了解到带有指针接收器的方法将接受指针和值接收器。那为什么代码不在第 45行工作呢?

原因是在任何已经是指针或可以获取地址的东西上调用指针值方法是合法的。存储在接口中的具体值是不可寻址的,因此编译器不可能自动获取,因此编译器不可能自动获取第 45 行中的a地址,因此此代码失败。

第 47 行有效,因为我们将a 的地址分配给d2

程序的其余部分是不言自明的。该程序将打印,

Sam is 25 years old  
James is 32 years old  
State Washington Country USA  

实现多个接口

一个类型可以实现多个接口。

让我们在下面的程序中看看这是如何完成的。

go 复制代码
package main

import (  
    "fmt"
)

type SalaryCalculator interface {  
    DisplaySalary()
}

type LeaveCalculator interface {  
    CalculateLeavesLeft() int
}

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", 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 s SalaryCalculator = e
    s.DisplaySalary()
    var l LeaveCalculator = e
    fmt.Println("\nLeaves left =", l.CalculateLeavesLeft())
}

Run in playground

上面的程序有两个接口,分别在第 7 行和第 11 行中声明SalaryCalculatorLeaveCalculator接口

第 15 行中定义了Employee结构体,在第 24 行和第 28 行中实现了分别实现了两个接口

在第 41 行中,我们给SalaryCalculator接口类型分配给的e变量,

在第 43 行中,我们将相同的e变量分配给LeaveCalculator类型的变量。这是可能的,因为Employee类型同时实现接口。

该程序输出,

Naveen Ramanathan has salary $5200  
Leaves left = 25  

嵌入接口

虽然 go 不提供继承,但可以通过嵌入其他接口来创建新接口。

让我们看看这是如何完成的。

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", 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()
    fmt.Println("\nLeaves left =", empOp.CalculateLeavesLeft())
}

Run in playground

上面程序第 15 行中的 EmployeeOperations 接口是通过嵌入 SalaryCalculatorLeaveCalculator 接口创建的。

如果任何类型都为SalaryCalculatorLeaveCalculator 接口中存在的方法提供方法定义,则称为实现EmployeeOperations接口。

Employee结构实现接口,因为它分别在第 29 行和第 33 行中分别实现了接口定义。

在第 46 行中,把Employee分配给EmployeeOperations接口,然后通过这去分别调用2个方法

Naveen Ramanathan has salary $5200  
Leaves left = 25  

接口的零值

接口的零值为 nil。nil 接口既有其基础值,也有具体类型为 nil。

go 复制代码
package main

import "fmt"

type Describer interface {  
    Describe()
}

func main() {  
    var d1 Describer
    if d1 == nil {
        fmt.Printf("d1 is nil and has type %T value %v\n", d1, d1)
    }
}

Run in playground

上面程序中,该程序将输出nil

d1 is nil and has type <nil> value <nil>  

如果我们尝试在接口上调用一个方法,程序会崩溃,因为接口既没有底层值也没有具体类型。

go 复制代码
package main

type Describer interface {  
    Describe()
}

func main() {  
    var d1 Describer
    d1.Describe()
}

Run in playground

由于在上面的程序中是,该程序会因运行时错误 panic 而死机:

panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x0 addr=0x0 pc=0x25bb70]                      
                                                                       
goroutine 1 [running]:                                                 
main.main()                                                            
        E:/goproject/structs/main.go:9 +0x10       

接口就是这样。祝你今天开心。

相关推荐
wjs20247 分钟前
CentOS Docker 安装
开发语言
阿熊不会编程34 分钟前
【计网】自定义协议与序列化(一) —— Socket封装于服务器端改写
linux·开发语言·网络·c++·设计模式
小牛itbull35 分钟前
Mono Repository方案与ReactPress的PNPM实践
开发语言·前端·javascript·reactpress
jokerest1231 小时前
pwn——test_your_nc1——测试
开发语言·php
碧海蓝天20221 小时前
接上一主题,C++14中如何设计类似于std::any,使集合在C++中与Python一样支持任意数据?
开发语言·c++·python
周杰伦fans1 小时前
Java与C#
java·开发语言·c#
SAP学习成长之路2 小时前
SAP 零售方案 CAR 系统的介绍与研究
大数据·开发语言·sap·abap·零售·car·bapi
fcopy2 小时前
Golang项目:实现生产者消费者模式
缓存·golang
A_Tai23333332 小时前
MyBatis高级扩展
java·开发语言·mybatis
夏子曦2 小时前
java虚拟机——频繁发生Full GC的原因有哪些?如何避免发生Full GC
java·开发语言