【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语言基础开始介绍,适合从零开始的初学者。


欢迎关注,我们一起踏上编程的行程。

如有错误,还请指正。

【Go语言入门系列】(九)写这些就是为了搞懂怎么用接口的更多相关文章

  1. 【Go语言入门系列】Go语言工作目录介绍及命令工具的使用

    [Go语言入门系列]前面的文章: [保姆级教程]手把手教你进行Go语言环境安装及相关VSCode配置 [Go语言入门系列](八)Go语言是不是面向对象语言? [Go语言入门系列](九)写这些就是为了搞 ...

  2. Go语言入门系列(五)之指针和结构体的使用

    Go语言入门系列前面的文章: Go语言入门系列(二)之基础语法总结 Go语言入门系列(三)之数组和切片 Go语言入门系列(四)之map的使用 1. 指针 如果你使用过C或C++,那你肯定对指针这个概念 ...

  3. Go语言入门系列(六)之再探函数

    Go语言入门系列前面的文章: Go语言入门系列(三)之数组和切片 Go语言入门系列(四)之map的使用 Go语言入门系列(五)之指针和结构体的使用 在Go语言入门系列(二)之基础语法总结这篇文章中已经 ...

  4. 【Go语言入门系列】(七)如何使用Go的方法?

    [Go语言入门系列]前面的文章: [Go语言入门系列](四)之map的使用 [Go语言入门系列](五)之指针和结构体的使用 [Go语言入门系列](六)之再探函数 本文介绍Go语言的方法的使用. 1. ...

  5. Go语言入门系列(四)之map的使用

    本系列前面的文章: Go语言入门系列(一)之Go的安装和使用 Go语言入门系列(二)之基础语法总结 Go语言入门系列(三)之数组和切片 1. 声明 map是一种映射,可以将键(key)映射到值(val ...

  6. 【Go语言入门系列】(八)Go语言是不是面向对象语言?

    [Go语言入门系列]前面的文章: [Go语言入门系列](五)指针和结构体的使用 [Go语言入门系列](六)再探函数 [Go语言入门系列](七)如何使用Go的方法? 1. Go是面向对象的语言吗? 在[ ...

  7. 新手入门HTML5开发,你必须先搞懂这6个问题

    凭借着跨平台,实时更新,无需安装,易于分发等众多优点,HTML5受到越来越多企业的青睐.而凭借着入门相对简单的优势,很多人编程初学者都选择学习HTML5.但对于初学者来说,学习HTML5之前,会有很多 ...

  8. Go语言入门系列2 基本语法

    get download and install packages and dependencies install = compile and install packages and depend ...

  9. Go语言入门系列1:安装,How to Write Go Code

    https://golang.org/doc/code.html src contains Go source files, pkg contains package objects, and bin ...

随机推荐

  1. 微信小程序后台springboot+mybatis+mysql“采坑”集锦

    "采坑"错误集锦 1.service层 错误描述:2019-04-14 22:09:52.027 ERROR 8416 --- [nio-8082-exec-5] o.a.c.c. ...

  2. WebMvcConfigurerAdapter在2.x向上过时问题

    在spring boot2.x向上,书写配置类时集成的WebMvcConfigurerAdapter会显示此类已经过时. 解决:不继承WebMvcConfigurerAdapter类,该实现WebMv ...

  3. 12、Decorator 装饰器 模式 装饰起来美美哒 结构型设计模式

    1.Decorator模式 装饰模式又名包装(Wrapper)模式.装饰模式以对客户端透明的方式扩展对象的功能,是继承关系的一个替代方案. 装饰器模式(Decorator Pattern)允许向一个现 ...

  4. java 模拟斗地主发牌洗牌

    一 模拟斗地主洗牌发牌 1.案例需求 按照斗地主的规则,完成洗牌发牌的动作. 具体规则: 1. 组装54张扑克牌 2. 将54张牌顺序打乱 3. 三个玩家参与游戏,三人交替摸牌,每人17张牌,最后三张 ...

  5. C#LeetCode刷题之#453-最小移动次数使数组元素相等(Minimum Moves to Equal Array Elements)

    问题 该文章的最新版本已迁移至个人博客[比特飞],单击链接 https://www.byteflying.com/archives/3877 访问. 给定一个长度为 n 的非空整数数组,找到让数组所有 ...

  6. LeetCode406 queue-reconstruction-by-height详解

    题目详情 假设有打乱顺序的一群人站成一个队列. 每个人由一个整数对(h, k)表示,其中h是这个人的身高,k是排在这个人前面且身高大于或等于h的人数. 编写一个算法来重建这个队列. 注意: 总人数少于 ...

  7. 阿里P8架构师大话设计模式,体会乐与怒的程序人生中值得回味一幕

    本书特色 本书有两个特色,第一特色是重视过程.看了太多的计算机编程类的图书,大多数书籍都是集中在讲授优秀的解决方案或者一个完美的程序样例,但对这些解决方案和程序的演变过程却重视不够,好书之所以好,就是 ...

  8. OpenCV Error - Core.hpp header must be compiled as C++

    在XCode 里编译OpenCV的时候,经常报如题类似的错误. 简单解决办法: 把 *.m 文件重命名为 *.mm 即可

  9. 手动SQL注入总结

    1.基于报错与union的注入 注意:union联合查询注入一般要配合其他注入使用 A.判断是否存在注入,注入是字符型还是数字型,有没过滤了关键字,可否绕过 a.如何判断是否存在注入 一般有一下几种 ...

  10. 微服务技术栈:API网关中心,落地实现方案

    本文源码:GitHub·点这里 || GitEE·点这里 一.服务网关简介 1.外观模式 客户端与各个业务子系统的通信必须通过一个统一的外观对象进行,外观模式提供一个高层次的接口,使得子系统更易于使用 ...