一篇文章彻底搞懂 Go 语言中的接口(1)

2023-06-12 14:46:27 浏览数 (1)

本篇文章字数:4376 字 写技术文的 leoay, 也是认真的!新号求关注,点赞不迷路!

1

你好,我是 leoay,又好久不见了,好像上篇文章立的 flag 又被打破了, 如果你还没看到,那就前往上篇文章看看我立的 flag(聊聊 Golang 中的切片和数组),然后在心里小声嘲笑我一番。

果然, 同时更新维护两个公众号是一件不太容易的事情,熟悉我的朋友应该知道,我基本上都一值在 leoay 这个号更新,而且是日更,所以技术号有时候就会被忽略掉了,毕竟每天要和大部分人一样要工作,写公众号的时间都是我从自己的休息和娱乐时间里面挤出来的。

虽然很难,但是我一直希望能好好做一个技术公众号,因为目前我的主要工作就是程序员,所以希望写一下我作为技术人是如何看待技术的, 以及我学习技术的一些心得;另一方面,也希望通过自己的分享给其他人一些帮助,不过目前来讲,我在技术方面的分享与积累还很不够,写这些也是给自己敲一个警钟,虽然非技术文章不能断更,但是技术文也不能落下太多。

好了,今天就不说太多废话了,开始聊聊今天的主角,Go 语言中的接口。

2

那么问题来了,什么是一个接口呢?

Go 语言中,接口是一个方法的集合,当一个类型中定义了这个接口中的所有方法时,我们也将这叫做实现了这个接口。

这个很像面向对象编程范式中提到的接口,如 java

接口指定了一个类型中拥有的方法,也决定了这个类型怎么去实现这些方法。

举个例子,洗衣机可以被认为是一个有洗涤和干燥方法的接口,那么我们就可以把任何提供洗涤和干燥方法的类型,就叫做实现了洗衣机接口。

下面我们用代码进行更加详细地说明:

3

声明和实现一个接口

代码语言:javascript复制
package main

import (
 "fmt"
)

//定义一个接口
type WashingMachine interface {
 Cleaning() (string, error)
 Drying() (string, error)
}

type MyWashingMachine string

//MyWashingMachine 实现了 WashingMachine
func (mwm MyWashingMachine) Cleaning() (string, error) {
 //do clean work
 return "Clean Ok!", nil
}

func (mwm MyWashingMachine) Drying() (string, error) {
 //do dry work
 return "Drying Ok!", nil
}

func main() {
 var v WashingMachine
 name := MyWashingMachine("Leoay Home")
 v = name
 resut, err := v.Cleaning()
 fmt.Println("result: ", resut, "err is: ", err)

 resut, err = v.Drying()
 fmt.Println("result: ", resut, "err is: ", err)
}

在上面的代码中我们创建了一个 WashingMachine 接口类型,其中有两个方法,分别是 Cleaning () Drying()

然后,我们定义了一个 MyWashingMachine 类型,接着我们写了两个方法分别是 Cleaning() Drying(), 并将 MyWashingMachine 作为方法的接收类型。

这个时候,我们就可以说 MyWashingMachine 实现了 WashingMachine 接口。这个与 java 有很大不同,在 java 中我们一般使用 implements 这个关键字表示实现了一个接口。

而在 Go 语言中,只需要这个类型包含接口中的所有方法即可。

所以在下面的代码中,我们可以直接用 v 调用 Cleaning() Drying() 这两个方法,因为 WashingMachine 已经实现了 WashingMachine 中的方法。

到了一步,我们就创建了一个接口,怎么样,是不是超级简单。

其实,如果你更深入学习 Go 语言时,你会发现接口在 Go 项目开发中使用的特别频繁,一不留神它就出现在你眼前,不过如果不了解的话,就会感到一头雾水。

4

接口的使用实践

通过上面的例子,我们知道了怎么创建并实现一个接口,但是并没有真正说明白怎么在实际项目中使用。

从上面代码中我们可以发现这一行代码 name := MyWashingMachine("Leoay Home")

那么,这个有什么用呢?

如果我们直接使用 name 调用 Cleaning() Drying() 函数时,会出现什么问题呢?

这个时候虽然也能正常输出,但是没有用到接口。

下面,我就用一个实例说明一下接口的使用。

我们将编写一个简单的程序,根据员工的个人工资计算公司的总费用。

代码语言:javascript复制
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
}

//总费用是通过迭代 SalaryCalculator 切片并求和来计算的
func totalExpense(s []SalaryCalculator) {  
    expense := 0
    for _, v := range s {
        expense = expense   v.CalculateSalary()
    }
    fmt.Printf("每个月的总支出 ¥%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() 方法。

在公司里有两种雇员,正式员工和试用期员工,分别用 Permanent Contract 两个结构体表示,正式员工的工资包含基本工资和奖金,试用期员工的工资只有基本工资。

但是我们希望只用一个方法计算员工的工资,所以我们就分别用 PermanentContract 实现了 SalaryCalculator 接口,这样无论员工是哪种类型,都有可以用 CalculateSalary 方法计算薪水了。

然后我们定义了一个总的计算薪水支出的方法 totalExpense, 这个方法将 SalaryCalculator 切片作为参数,然后通过这个切片将所有的员工信息传到方法中去,然后在内部调用 CalculateSalary 方法计算每个员工的薪水并求和。

执行上面的代码我们可以最后的输出结果:

每个月的总支出 ¥14050

这样做的最大优点是 totalExpense 可以扩展到任何新员工类型而无需更改任何代码。

假设公司增加了 Freelancer 一种工资结构不同的新型员工。

Freelancer 可以只在 slice 参数中传递给 totalExpense 而无需对 totalExpense 函数进行任何一行代码更改。

这个方法会做它应该做的事情,Freelancer 也会实现 SalaryCalculator 接口

下面我们就修改这个程序增加一种新的雇员 Freelancer, 其薪资是收入效率和总工作时间的乘积。

代码语言:javascript复制
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
}

//正式员工的工资是基本工资和奖金的总和
func (p Permanent) CalculateSalary() int {  
    return p.basicpay   p.pf
}

//试用期员工的工资是单独的基本工资
func (c Contract) CalculateSalary() int {  
    return c.basicpay
}

//自由职业者的薪资
func (f Freelancer) CalculateSalary() int {  
    return f.ratePerHour * f.totalHours
}

func totalExpense(s []SalaryCalculator) {  
    expense := 0
    for _, v := range s {
        expense = expense   v.CalculateSalary()
    }
    fmt.Printf("每月的总支出 ¥%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)
}

我们添加了一个 Freelancer 结构体。并声明了一个用 Freelancer 实现的CalculateSalary 方法。

新的 totalExpense 方法中不需要更改其他代码, 因为 Freelancer 结构体也实现了该 SalaryCalculator 接口。

然后我们在 main 方法中添加了几个 Freelancer 类型的员工。执行程序后打印,每月的总支出 ¥32450

5

接口内部表示

可以认为接口在内部由 tuple(type, value) 中表示的。type 是接口的底层具体类型, value 保存的具体类型的值。

为了更好地理解,我们写一段代码展示:

代码语言:javascript复制
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 %vn", w, w)
}

func main() {  
    p := Person{
        name: "Naveen",
    }
    var w Worker = p
    describe(w)
    w.Work()
}

从上面的代码我们可以看到,Worker 接口有一个方法 Work(), 而 Person 结构体类型实现了该接口。

main 函数中我们定义了一个 Person 类型的 p, 并将他赋值给 Worker 类型的变量 w, 那么现在 w 的类型就变成了 Person, 而且其包含一个变量 name 值为 Naveen

describe 函数则打印了 Worker 接口的具体类型和值,结果输出:

代码语言:javascript复制
Interface type main.Person value {Naveen}

接下来我们更深入地了解一些怎么获取接口底层的值。

6

空接口

一个没有方法的接口就是空接口, 用 interface{} 表示, 因为空接口中没有方法,所以所有的类型都实现了一个空接口。

代码语言:javascript复制
package main

import (  
    "fmt"
)

func describe(i interface{}) {  
    fmt.Printf("Type = %T, value = %vn", 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 函数中传入字符串、整型和结构体,最后结果输出:

代码语言:javascript复制
Type = string, value = Hello World  
Type = int, value = 55  
Type = struct { name string }, value = {Naveen R}  

7

接口的类型断言

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

i.(T) 用于获取 i 具体类型为 T 的接口的底层值。

代码胜万言,下面我就用代码展示类型断言是怎么用的

代码语言:javascript复制
package main

import (  
    "fmt"
)

func assert(i interface{}) {  
    s := i.(int) //从 i 中获取 int 底层的值
    fmt.Println(s)
}
func main() {  
    var s interface{} = 56
    assert(s)
}

s 的实际类型是 int。我们使用 i.(int) 获取 i 底层的 int 的值

那么如果上面的代码的实际类型不是 int,会出现什么呢?看下面的代码

代码语言:javascript复制
package main

import (  
    "fmt"
)

func assert(i interface{}) {  
    s := i.(int) 
    fmt.Println(s)
}
func main() {  
    var s interface{} = "Steven Paul"
    assert(s)
}

上面的代码中,我们传入了一个字符串到 assert 函数中,想要从中获取一个整型的值,这段代码将会出现 panic,并打印如下信息:

“interface {} is string, not int.”

那么,现在该怎么办呢?怎么才能避免程序的崩溃呢?

其实我们可以这样解决:

因为 i.(T) 会返回一个 error 异常的,只要我们对它进行判断,就可以避免程序崩溃了,

v, ok := i.(T)

如果 i 的实际类型是 T, 那么 v 就是 i 的值,ok 就是 true, 代码正常运行;

如果 i 的实际类型不是 T 的话,v 就返回空, ok 就是 false, 代码也就不会崩溃了。

所以我们对上面的代码进行简单的修改,如下所示:

代码语言:javascript复制
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。所以该程序将输出:

代码语言:javascript复制
56 true  
0 false  

8

类型开关

类型开关用于将接口的实际类型与多种情况下 case 语句中指定的类型进行比较。

类似于 switch case。唯一的区别是 case 指定的是类型而不是正常 switch 中的值。

类型开关的语法类似于类型断言。

i.(T) 类型断言的语法中,类型 T 应替换 type 为类型切换的关键字。

下面看一下代码中是怎么实现的:

代码语言:javascript复制
package main

import (  
    "fmt"
)

func findType(i interface{}) {  
    switch i.(type) {
    case string:
        fmt.Printf("I am a string and my value is %sn", i.(string))
    case int:
        fmt.Printf("I am an int and my value is %dn", i.(int))
    default:
        fmt.Printf("Unknown typen")
    }
}
func main() {  
    findType("Naveen")
    findType(77)
    findType(89.98)
}

上面的代码中, switch i.(type) 指定了一个 a type switch, 每一个 case 语句将 i 的实际类型和指定类型比较。如果任何一个 case 匹配的话, 就打印出相应的语句。

最后程序输出如下:

代码语言:javascript复制
I am a string and my value is Naveen
I am an int and my value is 77
Unknown type

89.98 类型 是 float64,不匹配任何情况,因此最后一行打印 Unknown type

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

为了更清楚,让我们编写一个程序详细说明:

代码语言:javascript复制
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 typen")
    }
}

func main() {  
    findType("Naveen")
    p := Person{
        name: "Naveen R",
        age:  25,
    }
    findType(p)
}

在上面的程序中,Person 结构体实现了 Describer 接口。 然后我们在 findType 函数中使用 case 语句比较类型 v 和比较 Describer 接口类型。

pPerson 类型,因此当我们把 p 传到 findType 中时,v 就是 Describer

所以最后程序输出如下:

代码语言:javascript复制
unknown type
Naveen R is 25 years old

以上就是今天的分享,其实接口本打算用一篇文章写完的,但是由于篇幅较长,所以最后还是决定拆分成两篇文章,第二篇文章先留着改天再写。

我是 leoay, 和你一起每天成长一点点!

0 人点赞