【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()}
例子摆在这里了,可以很容易总结出它的特点。
- 接口
interface
和结构体
strcut
的声明类似:
type interface_name interface {}
- 接口内部定义了一组方法的签名。何为方法的签名?即方法的方法名、参数列表、返回值列表(没有接收者)。
type interface_name interface {方法签名1方法签名2...}
3. 如何实现接口?
先说一下上例代码的具体内容。
有三个接口分别是:
-
human
接口:有
say()
、
eat()
方法签名。
-
adult
接口:有
say()
、
eat()
、
drink()
、
work()
方法签名。
-
teenager
接口:有
say()
、
eat()
、
learn()
方法签名。
有三个结构体分别是:
-
people
结构体:有
say()
、
eat()
方法。
-
student
结构体:有匿名字段
people
,所以可以说
student
“继承”了
people
。有
learn()
方法,并“重写”了
eat()
方法。
-
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语言基础开始介绍,适合从零开始的初学者。
欢迎关注,我们一起踏上编程的行程。
如有错误,还请指正。