共计 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. 接口的申明
上例中,咱们申明了三个接口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 = 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。
因为 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.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 语言根底开始介绍,适宜从零开始的初学者。
欢送关注,咱们一起踏上编程的行程。
如有谬误,还请斧正。