关于golang:Go语言入门系列九写这些就是为了搞懂怎么用接口

8次阅读

共计 7450 个字符,预计需要花费 19 分钟才能阅读完成。

【Go 语言入门系列】后面的文章:

  • 【Go 语言入门系列】(六)再探函数
  • 【Go 语言入门系列】(七)如何应用 Go 的办法?
  • 【Go 语言入门系列】(八)Go 语言是不是面向对象语言?

1. 引入例子

如果你应用过 Java 等面向对象语言,那么必定对接口这个概念并不生疏。简略地来说,接口就是标准,如果你的类实现了接口,那么该类就必须具备接口所要求的所有性能、行为。接口中通常定义的都是办法。

就像玩具工厂要生产玩具,生产前必定要先拿到一个生产标准,该标准要求了玩具的色彩、尺寸和性能,工人就依照这个标准来生产玩具,如果有一项要求没实现,那就是不合格的玩具。

如果你之前还没用过面向对象语言,那也没关系,因为 Go 的接口和 Java 的接口有区别。间接看上面一个实例代码,来感触什么是 Go 的接口,前面也围绕该例代码来介绍。

package main

import "fmt"

type people struct {
    name string
    age int
}

type student struct {
    people //"继承"people
    subject string
    school string
}

type programmer struct {
    people //"继承"people
    language string
    company string
}

type human interface { // 定义 human 接口
    say()
    eat()}

type adult interface { // 定义 adult 接口
    say()
    eat()
    drink()
    work()}

type teenager interface { // 定义 teenager 接口
    say()
    eat()
    learn()}

func (p people) say() { //people 实现 say()办法
    fmt.Printf("我是 %s,往年 %d。\n", p.name, p.age)
}

func (p people) eat() { //people 实现 eat()办法
    fmt.Printf("我是 %s,在吃饭。\n", p.name)
}

func (s student) learn() { //student 实现 learn()办法
    fmt.Printf("我在 %s 学习 %s。\n", s.school, s.subject)
}

func (s student) eat() { //student 重写 eat()办法
    fmt.Printf("我是 %s,在 %s 学校食堂吃饭。\n", s.name, s.school)
}

func (pr programmer) work() { //programmer 实现 work()办法
    fmt.Printf("我在 %s 用 %s 工作。\n", pr.company, pr.language)
}

func (pr programmer) drink() {//programmer 实现 drink()办法
    fmt.Printf("我是成年人了,能大口喝酒。\n")
}

func (pr programmer) eat() { //programmer 重写 eat()办法
    fmt.Printf("我是 %s,在 %s 公司餐厅吃饭。\n", pr.name, pr.company)
}


func main() {xiaoguan := people{"行小观", 20}
    zhangsan := student{people{"张三", 20}, "数学", "河汉大学"}
    lisi := programmer{people{"李四", 21},"Go", "火星有限公司"}

    var h human
    h = xiaoguan
    h.say()
    h.eat()
    fmt.Println("------------")
    var a adult
    a = lisi
    a.say()
    a.eat()
    a.work()
    fmt.Println("------------")
    var t teenager
    t = zhangsan
    t.say()
    t.eat()
    t.learn()}

运行:

我是行小观,往年 20。我是行小观,在吃饭。------------
我是李四,往年 21。我是李四,在火星有限公司公司餐厅吃饭。我在火星有限公司用 Go 工作。------------
我是张三,往年 20。我是张三,在河汉大学学校食堂吃饭。我在河汉大学学习数学。

这段代码比拟长,你能够间接复制粘贴运行一下,上面好好地解释一下。

2. 接口的申明

上例中,咱们申明了三个接口humanadultteenager

type human interface { // 定义 human 接口
    say()
    eat()}

type adult interface { // 定义 adult 接口
    say()
    eat()
    drink()
    work()}

type teenager interface { // 定义 teenager 接口
    say()
    eat()
    learn()}

例子摆在这里了,能够很容易总结出它的特点。

  1. 接口 interface 和构造体 strcut 的申明相似:
type interface_name interface {}
  1. 接口外部定义了一组办法的签名。何为办法的签名?即办法的办法名、参数列表、返回值列表(没有接收者)。
type interface_name interface {
    办法签名 1
    办法签名 2
    ...
}

3. 如何实现接口?

先说一下上例代码的具体内容。

有三个接口别离是:

  1. human接口:有 say()eat() 办法签名。
  2. adult接口:有 say()eat()drink()work() 办法签名。
  3. teenager接口:有 say()eat()learn() 办法签名。

有三个构造体别离是:

  1. people构造体:有 say()eat() 办法。
  2. student构造体:有匿名字段 people,所以能够说student“继承”了people。有learn() 办法,并“重写”了 eat() 办法。
  3. programmer构造体:有匿名字段 people,所以能够说programmer“继承”了people。有work()drink() 办法,并“重写”了 eat() 办法。

后面说过,接口就是标准,要想实现接口就必须恪守并具备接口所要求的所有。当初好好看看下面三个构造体和三个接口之间的关系:

people构造体有 human 接口要求的 say()eat() 办法。

student构造体有 teenager 接口要求的 say()eat()learn() 办法。

programmer构造体有 adult 接口要求的 say()eat()drink()work() 办法。

尽管 studentprogrammer都重写了 say() 办法,即外部实现和接收者不同,但这没关系,因为接口中只是一组办法签名(不论外部实现和接收者)。

所以咱们当初能够说:people实现了 human 接口,student实现了 humanteenager 接口,programmer实现了 humanadult 接口。

是不是感觉很奇妙?不须要像 Java 一样应用 implements 关键字来显式地实现接口,只有类型实现了接口中定义的所有办法签名,就能够说该类型实现了该接口。(后面都是用构造体举例,构造体就是一个类型)。

换句话说:接口负责指定一个类型应该具备的办法,该类型负责决定这些办法如何实现

在 Go 中,实现接口能够这样了解:programmer谈话像 adult、吃饭像adult、喝酒像adult、工作像adult,所以programmeradult

4. 接口值

接口也是值,这就意味着接口能像值一样进行传递,并能够作为函数的参数和返回值。

4.1. 接口变量存值

func main() {xiaoguan := people{"行小观", 20}
    zhangsan := student{people{"张三", 20}, "数学", "河汉大学"}
    lisi := programmer{people{"李四", 21},"Go", "火星有限公司"}
    
    var h human // 定义 human 类型变量
    h = xiaoguan

    var a adult // 定义 adult 类型变量
    a = lisi

    var t teenager // 定义 teenager 类型变量
    t = zhangsan
}

如果定义了一个接口类型变量,那么该变量中能够存储实现了该接口的任意类型值:

func main() {
    // 这三个人都实现了 human 接口
    xiaoguan := people{"行小观", 20}
    zhangsan := student{people{"张三", 20}, "数学", "河汉大学"}
    lisi := programmer{people{"李四", 21},"Go", "火星有限公司"}
    
    var h human // 定义 human 类型变量
    // 所以 h 变量能够存这三个人
    h = xiaoguan
    h = zhangsan
    h = lisi
}

不能存储未实现该 interface 接口的类型值:

func main() {xiaoguan := people{"行小观", 20} // 实现 human 接口
    zhangsan := student{people{"张三", 20}, "数学", "河汉大学"} // 实现 teenager 接口
    lisi := programmer{people{"李四", 21},"Go", "火星有限公司"} // 实现 adult 接口
    
    var a adult // 定义 adult 类型变量
    // 但 zhangsan 没实现 adult 接口
    a = zhangsan // 所以 a 不能存 zhangsan,会报错
}

否则会相似这样报错:

cannot use zhangsan (type student) as type adult in assignment:
student does not implement adult (missing drink method)

也能够定义接口类型切片:

func main() {var sli = make([]human, 3)
    sli[0] = xiaoguan
    sli[1] = zhangsan
    sli[2] = lisi

    for _, v := range sli {v.say()
    }
}

4.2. 空接口

所谓空接口,即定义了零个办法签名的接口。

空接口能够用来保留任何类型的值,因为空接口中定义了零个办法签名,这就相当于每个类型都会实现实现空接口。

空接口长这样:

interface {}

下例代码展现了空接口能够保留任何类型的值:

package main

import "fmt"

type people struct {
    name string
    age int
}

func main() {xiaoguan := people{"行小观", 20}
    var ept interface{} // 定义一个空接口变量
    ept = 10 // 能够存整数
    ept = xiaoguan // 能够存构造体
    ept = make([]int, 3) // 能够存切片
}

4.3. 接口值作为函数参数或返回值

看下例:

package main

import "fmt"

type sayer interface {// 接口
    say()}

func foo(a sayer) { // 函数的参数是接口值
    a.say()}

type people struct { // 构造体类型
    name string
    age int
}

func (p people) say() { //people 实现了接口 sayer
    fmt.Printf("我是 %s,往年 %d 岁。", p.name, p.age)
}

type MyInt int //MyInt 类型

func (m MyInt) say() { //MyInt 实现了接口 sayer
    fmt.Printf("我是 %d。\n", m)
}

func main() {xiaoguan := people{"行小观", 20}
    foo(xiaoguan) // 构造体类型作为参数
    
    i := MyInt(5)
    foo(i) //MyInt 类型作为参数
}

运行:

我是行小观,往年 20 岁。我是 5。

因为 peopleMyInt都实现了 sayer 接口,所以它们都能作为 foo 函数的参数。

5. 类型断言

上一大节说过,interface 类型变量中能够存储实现了该 interface 接口的任意类型值。

那么给你一个接口类型的变量,你怎么晓得该变量中存储的是什么类型的值呢?这时就须要应用类型断言了。类型断言是这样应用的:

t := var_interface.(val_type)

var_interface:一个接口类型的变量。

val_type:该变量中存储的值的类型。

你可能会问:我的目标就是要晓得接口变量中存储的值的类型,你这里还让我提供值的类型?

留神:这是 类型断言,你得有个假如(猜)才行,而后去验证猜对得对不对。

如果正确,则会返回该值,你能够用 t 去接管;如果不正确,则会报panic

话说多了容易迷糊,间接看代码。还是用本章一开始举的那个例子:

func main() {zhangsan := student{people{"张三", 20}, "数学", "河汉大学"}

    var x interface{} = zhangsan // x 接口变量中存了一个 student 类型构造体
    var y interface{} = "HelloWorld" // y 接口变量中存了一个 string 类型的字符串
    /* 当初假如你不晓得 x、y 中存的是什么类型的值 */
    // 当初应用类型断言去验证
    
    //a := x.(people) // 报 panic
    //fmt.Println(a)
    //panic: interface conversion: interface {} is main.student, not main.people
    
    a := x.(student)
    fmt.Println(a) // 打印{{张三 20} 数学 河汉大学}

    b := y.(string)
    fmt.Println(b) // 打印 HelloWorld
}

第一次,咱们断言 x 中存储的变量是 people 类型,但实际上是 student 类型,所以报 panic。

第二次,咱们断言 x 中存储的变量是 student 类型,断言对了,所以会把 x 的值赋给a

第三次,咱们断言 y 中存储的变量是 string 类型,也断言对了。

有时候咱们并不需要值,只想晓得接口变量中是否存储了某类型的值,类型断言能够返回两个值:

t, ok := var_interface.(val_type)

ok是个布尔值,如果断言对了,为 true;如果断言错了,为 false 且不报 panic,但t 会被置为“零值”。

// 断言谬误
value, ok := x.(people)
fmt.Println(value, ok) // 打印{0} false

// 断言正确
_, ok := y.(string)
fmt.Println(ok) //true

6. 类型抉择

类型断言其实就是在猜接口变量中存储的值的类型。

因为咱们并不确定该接口变量中存储的是什么类型的值,所以必定会思考足够多的状况:当是 int 类型的值时,采取这种操作,当是 string 类型的值时,采取那种操作等。这时你可能会采纳 if...else... 来实现:

func main() {xiaoguan := people{"行小观", 20}

    var x interface{} = 12

    if value, ok := x.(string); ok { // x 的值是 string 类型
        fmt.Printf("%s 是个字符串。开心", value)
    } else if value, ok := x.(int); ok { // x 的值是 int 类型
        value *= 2
        fmt.Printf("翻倍了,%d 是个整数。哈哈", value)
    } else if value, ok := x.(people); ok { // x 的值是 people 类型
        fmt.Println("这是个构造体。", value)
    }
}

这样显得有点啰嗦,应用 switch...case... 会更加简洁。

switch value := x.(type) {
    case string:
        fmt.Printf("%s 是个字符串。开心", value)
    case int:
           value *= 2
           fmt.Printf("翻倍了,%d 是个整数。哈哈", value)
    case human:
        fmt.Println("这是个构造体。", value)
    default:
        fmt.Printf("后面的 case 都没猜对,x 是 %T 类型", value)
        fmt.Println("x 的值为", value)
}

这就是类型抉择,看起来和一般的 switch 语句类似,但不同的是 case 是类型而不是值。

当接口变量 x 中存储的值和某个 case 的类型匹配,便执行该 case。如果所有 case 都不匹配,则执行 default,并且此时 value 的类型和值会和 x 中存储的值雷同。

7.“继承”接口

这里的“继承”并不是面向对象的继承,只是借用该词表白意思。

咱们曾经在【Go 语言入门系列】(八)Go 语言是不是面向对象语言?一文中应用构造体时曾经体验了 匿名字段(嵌入字段)的益处,这样能够复用许多代码,比方字段和办法。如果你对通过匿名字段“继承”失去的字段和办法不称心,还能够“重写”它们。

对于接口来说,也能够通过“继承”来复用代码,实际上就是把一个接口当做匿名字段嵌入另一个接口中。上面是一个实例:

package main

import "fmt"

type animal struct { // 构造体 animal
    name string
    age int
}

type dog struct { // 构造体 dog
    animal //“继承”animal
    address string
}

type runner interface { //runner 接口
    run()}

type watcher interface { //watcher 接口
    runner //“继承”runner 接口
    watch()}

func (a animal) run() { //animal 实现 runner 接口
    fmt.Printf("%s 会跑 \n", a.name)
}

func (d dog) watch()  { //dog 实现 watcher 接口
    fmt.Printf("%s 在 %s 看门 \n", d.name, d.address)
}

func main() {a := animal{"小动物", 12}
    d := dog{animal{"哮天犬", 13}, "天庭"}
    a.run()
    d.run() // 哮天犬能够调用“继承”失去的接口中的办法
    d.watch()}

运行:

小动物会跑
哮天犬会跑
哮天犬在天庭看门

作者简介

【作者】:行小观

【公众号】:行人观学

【简介】:一个面向学习的账号,用乏味的语言写系列文章。包含 Java、Go、数据结构和算法、计算机根底等相干文章。


本文章属于系列文章「Go 语言入门系列」,本系列从 Go 语言根底开始介绍,适宜从零开始的初学者。


欢送关注,咱们一起踏上编程的行程。

如有谬误,还请斧正。

正文完
 0