AI智能
改变未来

【Go语言入门系列】(九)写这些就是为了搞懂怎么用接口

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

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

1. 引入例子

如果你使用过Java等面向对象语言,那么肯定对接口这个概念并不陌生。简单地来说,接口就是规范,如果你的类实现了接口,那么该类就必须具有接口所要求的一切功能、行为。接口中通常定义的都是方法。

就像玩具工厂要生产玩具,生产前肯定要先拿到一个生产规范,该规范要求了玩具的颜色、尺寸和功能,工人就按照这个规范来生产玩具,如果有一项要求没完成,那就是不合格的玩具。

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

package mainimport "fmt"type people struct {name stringage int}type student struct {people //"继承"peoplesubject stringschool string}type programmer struct {people //"继承"peoplelanguage stringcompany 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 humanh = xiaoguanh.say()h.eat()fmt.Println("------------")var a adulta = lisia.say()a.eat()a.work()fmt.Println("------------")var t teenagert = zhangsant.say()t.eat()t.learn()}

运行:

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

这段代码比较长,你可以直接复制粘贴运行一下,下面好好地解释一下。

2. 接口的声明

上例中,我们声明了三个接口

human

adult

teenager

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

方法。

虽然

student

programmer

都重写了

say()

方法,即内部实现和接收者不同,但这没关系,因为接口中只是一组方法签名(不管内部实现和接收者)。

所以我们现在可以说:

people

实现了

human

接口,

student

实现了

human

teenager

接口,

programmer

实现了

human

adult

接口。

是不是感觉很巧妙?不需要像Java一样使用

implements

关键字来显式地实现接口,只要类型实现了接口中定义的所有方法签名,就可以说该类型实现了该接口。(前面都是用结构体举例,结构体就是一个类型)。

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

在Go中,实现接口可以这样理解:

programmer

说话像

adult

、吃饭像

adult

、喝酒像

adult

、工作像

adult

,所以

programmer

adult

4. 接口值

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

4.1. 接口变量存值

func main() {xiaoguan := people{"行小观", 20}zhangsan := student{people{"张三", 20}, "数学", "银河大学"}lisi := programmer{people{"李四", 21},"Go", "火星有限公司"}var h human //定义human类型变量h = xiaoguanvar a adult //定义adult类型变量a = lisivar 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 = xiaoguanh = zhangsanh = 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] = xiaoguansli[1] = zhangsansli[2] = lisifor _, v := range sli {v.say()}}

4.2. 空接口

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

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

空接口长这样:

interface {}

下例代码展示了空接口可以保存任何类型的值:

package mainimport "fmt"type people struct {name stringage int}func main() {xiaoguan := people{"行小观", 20}var ept interface{} //定义一个空接口变量ept = 10 //可以存整数ept = xiaoguan //可以存结构体ept = make([]int, 3) //可以存切片}

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

看下例:

package mainimport "fmt"type sayer interface {//接口say()}func foo(a sayer) { //函数的参数是接口值a.say()}type people struct { //结构体类型name stringage int}func (p people) say() { //people实现了接口sayerfmt.Printf("我是%s,今年%d岁。", p.name, p.age)}type MyInt int //MyInt类型func (m MyInt) say() { //MyInt实现了接口sayerfmt.Printf("我是%d。\\n", m)}func main() {xiaoguan := people{"行小观", 20}foo(xiaoguan) //结构体类型作为参数i := MyInt(5)foo(i) //MyInt类型作为参数}

运行:

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

由于

people

MyInt

都实现了

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.peoplea := 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{} = 12if value, ok := x.(string); ok { //x的值是string类型fmt.Printf("%s是个字符串。开心", value)} else if value, ok := x.(int); ok { //x的值是int类型value *= 2fmt.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 *= 2fmt.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 mainimport "fmt"type animal struct { //结构体animalname stringage int}type dog struct { //结构体doganimal //“继承”animaladdress 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) 打赏
未经允许不得转载:爱站程序员基地 » 【Go语言入门系列】(九)写这些就是为了搞懂怎么用接口