为了实现功能的聚合,提出的变量类型,其特点就是,接口变量可以接受多种类型,可以调用多种方法,调用的时候无需管里面是什么,这里体现的就是封装的思想!!!
- 描述1: 在 Go 语言中,接口(interface)是一个自定义类型,接口类型具体描述了一系列方法的集合。 接口类型是一种抽象的类型,它不会暴露出它所代表的对象的内部值的结构和这个对象支持的基础操作的集合,它们只会展示出它们自己的方法。因此接口类型不能将其实例化。
比喻:Go 通过接口实现了鸭子类型(duck-typing):“当看到一只鸟走起来像鸭子、游泳起来像鸭子、 叫起来也像鸭子,那么这只鸟就可以被称为鸭子”。我们并不关心对象是什么类型,到底是不是鸭子,只关心行为。
- 描述2:
在Go语言中接口(interface)是一种类型,一种抽象的类型。interface是一组method的集合,是duck-type programming的一种体现。接口做的事情就像是定义一个协议(规则),只要一台机器有洗衣服和甩干的功能,我就称它为洗衣机。不关心属性(数据),只关心行为(方法)。**为了保护你的Go语言职业生涯,请牢记接口(interface)是一种类型。**
回忆
- 用接口,比用方法,多了什么步骤?方便了什么?
- 从流程图来看
- 从代码实现来比较 可以看出,中间一样,只是前面加了个帽子,在main调用的时候,就将不同类型统一为一个类型,这就是接口
不用接口只用方法 | 用接口(以行为为中心) | 同? | |
---|---|---|---|
类型定义 | 同 | ||
用方法实现~ | 同 | ||
main函数中调用 | 不同 |
package main
import (
"fmt"
)
//定义一个接口
type Sayer interface{
say()
}
//定义接受者类型
type dog struct {
name string
}
type cat struct {
name string
}
//------------------------------------------
// 实现Sayer接口
func (d dog) say() {
fmt.Printf("%s会叫汪汪汪\n", d.name)
}
// 实现Mover接口
func (d cat) say() {
fmt.Printf("%s会叫喵喵喵\n", d.name)
}
//-------------------------------------------
func main() {
var x,y Sayer
x,y =dog{name: "旺财"}, cat{name: "小喵"}
x.say()
y.say()
}
package main
import (
"fmt"
)
//定义接受者类型
type dog struct {
name string
}
type cat struct {
name string
}
//------------------------------------------
// 用方法实现类型
func (d dog) say() {
fmt.Printf("%s会叫汪汪汪\n", d.name)
}
func (d cat) say() {
fmt.Printf("%s会叫喵喵喵\n", d.name)
}
//-------------------------------------------
func main() {
var x = dog{name: "旺财"}
var y = cat{name: "小喵"}
x.say()
y.say()
}
基本语法
为什么需要接口
之前,结构的出现,是为了实现数据的聚合,比如一个人的名字性别身高。放在一起,现在如果要实现功能的聚合,怎么办呢?——接口
:::info 实例
:::
上面的代码中定义了猫和狗,然后它们都会叫,你会发现main函数中明显有重复的代码,如果我们后续再加上猪、青蛙等动物的话,我们的代码还会一直重复下去。那我们能不能把它们当成“能叫的动物”来处理呢? 像类似的例子在我们编程过程中会经常遇到: 比如一个网上商城可能使用支付宝、微信、银联等方式去在线支付,我们能不能把它们当成“支付方式”来处理呢? 比如三角形,四边形,圆形都能计算周长和面积,我们能不能把它们当成“图形”来处理呢? 比如销售、行政、程序员都能计算月薪,我们能不能把他们当成“员工”来处理呢? Go语言中为了解决类似上面的问题,就设计了接口这个概念。接口区别于我们之前所有的具体类型,接口是一种抽象的类型。当你看到一个接口类型的值时,你不知道它是什么,唯一知道的是通过它的方法能做什么。接口的定义
接口其目的就是实现多态,接口变量,主要表现于赋值可多样,方法调用可多样,但只有一个接口,你无需关心接口里面有什么
- 接⼝命名
习惯以 er 结尾,如有写操作的接口叫Writer,有字符串功能的接口叫Stringer等。接口名最好要能突出该接口的类型含义。 - 要求
接口只有方法声明,没有实现,没有数据字段 - 省略
参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。 - 访问
方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
如何实现接口型变量?
实现了接口有什么用呢?
存储的多样性
解释:接口类型变量能够存储所有实现了该接口的变量。 例如上面的示例中,Sayer类型的变量能够存储dog和cat类型的变量。
→→→
值接受者和指针接受者
探讨区别何在?两个点
- 本质上和方法的值类型接受者,和指针类型接受者,的思考方法是一样的
- 值接受者是一个拷贝,是一个副本,不可直接修改函数外的变量;
- 指针接受者,传递的是指针,可以修改函数外的变量
- 什么类型的值可以给接口变量赋值?
- 规则1:
这个接口包含了很多方法,实现了方法的类型(接收者类型),都可以给,该接口变量赋值 - 规则2:
- 接受者类型是值类型T,那么接口可接收 T/ *T(语法糖)
- 接受者类型是指针类型T吗,那么接口只可接收 T(这一点与直接使用方法不同)
- 规则1:
:::info 实例验证规则2
:::
- 前言——我们有一个Mover接口和一个dog结构体。
- 值接收者
方法定义:
实现接口:
从上面的代码中我们可以发现,使用值接收者实现接口之后,不管是dog结构体还是结构体指针dog类型的变量都可以赋值给该接口变量。因为Go语言中有对指针类型变量求值的语法糖,dog指针fugui内部会自动求值fugui。 - 指针接收者
此时实现Mover接口的是dog类型,所以不能给x传入dog类型的值,此时x只能存储dog类型的值。
类型与接口的关系
关系:类型——方法——接口的初始化与实现
一个类型,至少有一个方法去实现,所以核心关系是类型与接口
:::info
:::
一个类型实现多个接口(人为中心)
一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。 例如,狗可以叫,也可以动。我们就分别定义Sayer接口和Mover接口,如下: Mover接口。 dog既可以实现Sayer接口,也可以实现Mover接口。
//定义两个接口
type Sayer interface{
say()
}
type Mover interface{
move()
}
//定义接受者类型
type dog struct {
name string
}
//------------------------------------------
// 实现Sayer接口
func (d dog) say() {
fmt.Printf("%s会叫汪汪汪\n", d.name)
}
// 实现Mover接口
func (d dog) move() {
fmt.Printf("%s会动\n", d.name)
}
//-------------------------------------------
func main() {
var x Sayer
var y Mover
var a = dog{name: "旺财"}
x = a
y = a
x.say()
y.move()
}
package main
import (
"fmt"
)
//定义一个接口
type behavior interface{
say()
move()
}
//定义接受者类型
type dog struct {
name string
}
//------------------------------------------
// 实现Sayer接口
func (d dog) say() {
fmt.Printf("%s会叫汪汪汪\n", d.name)
}
// 实现Mover接口
func (d dog) move() {
fmt.Printf("%s会动\n", d.name)
}
//-------------------------------------------
func main() {
var x,y behavior
var a = dog{name: "旺财"}
x,y =a,a
x.say()
y.move()
}
结果都是:
:::info
:::
多个类型实现一个接口(行为为中心)
Go语言中不同的类型还可以实现同一接口 首先我们定义一个Mover接口,它要求各个方法必须同名! 例如狗可以动,汽车也可以动,可以使用如下代码实现这个关系: 这个时候我们在代码中就可以把狗和汽车当成一个会动的物体来处理了,不再需要关注它们具体是什么,只需要调用它们的move方法就可以了。 结果 :::info note ::: ???并且一个接口的方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。
// WashingMachine 洗衣机
type WashingMachine interface {
wash()
dry()
}
// 甩干器
type dryer struct{}
// 实现WashingMachine接口的dry()方法
func (d dryer) dry() {
fmt.Println("甩一甩")
}
// 海尔洗衣机
type haier struct {
dryer //嵌入甩干器
}
// 实现WashingMachine接口的wash()方法
func (h haier) wash() {
fmt.Println("洗刷刷")
}
:::info
:::
接口实现的多样性
下面例子给了多种实现方法,接口就可以调用其所包含的方法,自动匹配对于的方法
接口变量=实参
+接口变量.方法
- 函数,形参是接口变量,实参可以是各种类型变量
函数内部:接口变量.方法
- 切片,切片类型是
[]接口类型
,实参是接口变量
package main
import "fmt"
//这是接口,只有方法声明,没有实现,没有数据字段
type man_er interface {
Myfunc1()
}
//第一个类型和方法
type student struct{
name string
age int
}
func(s student)Myfunc1(){
fmt.Println(s)
}
//第二个类型和方法,接收者为*T
type teacher struct {
name string
subject string
age int
}
func(t *teacher)Myfunc1(){
t.name="change to justin"
t.subject="change to math"
t.age = 16
fmt.Println(t)
}
//第三个类型和方法
type Luch_number int
func(l *Luch_number)Myfunc1(){
*l = 777
fmt.Println(*l)
}
func main() {
var test man_er
man_1 := student{"Tom", 27}
man_2 := &teacher{"June", "语文",27}
var man_3 Luch_number=666
test=man_1
test.Myfunc1()
test = man_2
test.Myfunc1()
test = &man_3
test.Myfunc1()
}
:::info 第一种实现方法
:::
结果:
:::info 第二种实现方法
:::
输入和结果和上文相同
package main
import "fmt"
//这是接口,只有方法声明,没有实现,没有数据字段
type man_er interface {
Myfunc1()
}
//第一个类型和方法
type student struct{
name string
age int
}
func(s student)Myfunc1(){
fmt.Println(s)
}
//第二个类型和方法,接收者为*T
type teacher struct {
name string
subject string
age int
}
func(t *teacher)Myfunc1(){
t.name="change to justin"
t.subject="change to math"
t.age = 16
fmt.Println(t)
}
//第三个类型和方法
type Luch_number int
func(l *Luch_number)Myfunc1(){
*l = 777
fmt.Println(*l)
}
func testing (test man_er){
test.Myfunc1()
}
func main() {
man_1 := student{"Tom", 27}
man_2 := &teacher{"June", "语文",27}
var man_3 Luch_number=666
testing(man_1)
testing(man_2)
testing(&man_3)
}
:::info 第三种实现
:::
\
可以看到这个类型,可以接受各种类型的方法
package main
import "fmt"
//这是接口,只有方法声明,没有实现,没有数据字段
type man_er interface {
Myfunc1()
}
//第一个类型和方法
type student struct{
name string
age int
}
func(s student)Myfunc1(){
fmt.Println(s)
}
//第二个类型和方法,接收者为*T
type teacher struct {
name string
subject string
age int
}
func(t *teacher)Myfunc1(){
t.name="change to justin"
t.subject="change to math"
t.age = 16
fmt.Println(t)
}
//第三个类型和方法
type Luch_number int
func(l *Luch_number)Myfunc1(){
*l = 777
fmt.Println(*l)
}
func main() {
var man_3 Luch_number=666
x := make([]man_er,3)
x[0]=student{"Tom", 27}
x[1]=&teacher{"June", "语文",27}
x[2]=&man_3
for _,k := range x{
k.Myfunc1()
}
}
总结
代码顺序—三步走
语法 | NOTE/要求 | |
---|---|---|
定义接口类型 | ||
实现接口内声明的方法 | 1. 2. |
1. 实现方法前,要定义方法的接受者receiver 2. 要保证上面每个接口,包含的方法都实现了 3. 接口与类型的关系,至关重要,上文有阐述(可能一个类型,多个方法;或者一个个同名方法,多个类型) |
声明、赋值、实现 接口变量 | 1. 声明接口变量 2. 赋值接口变量 ——什么类型的值可以给接口变量赋值? Answer:这个接口包含了很多方法,实现了方法的类型(接收者类型),都可以给,该接口变量赋值 (简而言之,接口是方法集合嘛,方法的定义里,有接收者,T or *T,是这个接收者类型的变量都可以给该接口赋值) 3. 实现接口变量 下面例子给了多种实现方法,接口就可以调用其所包含的方法,自动匹配对于的方法 1. 接口变量=实参 +接口变量.方法 2. 函数,形参是接口变量,实参可以是各种类型变量 函数内部: 接口变量.方法 3. 切片,切片类型是 []接口类型 ,实参是接口变量4. ……. |
构思
事实上,接口的多样性,也早就写代码时候的构思的复杂性,该怎么样去写一段接口代码呢?——由于接口是功能的聚合,我们就从功能出发
比喻 | 选择什么? | |
---|---|---|
情况 | ||
同一功能不同接收者(以行为为中心) | 狗会叫,车也会鸣笛 | 1. 多个类型,一个接口, 2. 实现接口的方法的方法名,要求相同(我试过不同方法名,错了) |
一个人可以干很多活(以个体为中心) | 一个人可以去跑步,去打球,也可以去读书,去写字 | 1. 一个类型,多个接口 2. 实现接口的方法的,接受者receiver要是那同一个人 |
接口组合
接口的继承
与结构体的匿名字段同理,接口也支持嵌入,新的接口变量也就继承了原本接口的方法
接口的转换
接口A,接口B含有接口A,接口A是B的匿名字段,接口A就是子集,接口B就是超集,那么身为超集的接口A的变量a可以转换为,身为子集的接口B,但是不可从接口B转换成接口A
作用:就是接口变量,去调用方法的约束
应用
空接口
即未经定义的interface,空接口是一种万能类型
空接口(interface{})不包含任何的方法,正因为如此,所有的类型都实现了空接口,因此空接口可以存储任意类型的数值。它有点类似于 C 语言的 void *类型。:::info
实例:::
当函数可以接受任意的对象实例时,我们会将其声明为 interface{},最典型的例子是标准库 fmt 中 PrintXXX 系列的函数,例如::::info 运用
:::
类型查询/类型断言
针对空接口变量,用于查询类型,进行判断,并返回value和bool,接口断言 = interface conversion
- 理解:判断变量的子类型是否为T类型,变量必须为空接口类型
- 语法
value := <font style="color:rgb(0,0,0);">element</font>.(T)
- 如果 element 的子类型不是 T,编译器就会panic
- 一般与switch和type一同使用(type比较特俗且固定的用法),可以让接口与多个子类型进行判断,写的好可以防止panic;
算法题可单独使用
value, ok := <font style="color:rgb(0,0,0);">element</font>.(T)
- 一般与 if 一同使用,先判断 ok == true 再执行语句,可以防止panic,只用来判断一个预期类型(也可以多个,就是写的很冗余,就像下图)
- 类似语法:
和map集合/映射如出一辙,语法:value, ok := map_1[key]
,不过map这里是为了检验其键值对是否存在
:::info 实例
:::
if 实现(一般就一个if,这么多个 if ,简洁性不如switch)
switch实现
OCP原则
面向对象的可复用设计的第一块基石,便是所谓的“开-闭原则”(Open-Close Principle,缩写OCP),对扩展是开放的,对修改是关闭的,go语言不是面向对象的语言,但是也可以模拟实现这个原则
:::info 实例说明
:::
package main
import "fmt"
type Pet interface{
eat()
sleep()
}
type Dog struct{
}
type Cat struct{
}
func(dog Dog)eat(){
fmt.Println("dog eat...")
}
func(cat Cat)eat(){
fmt.Println("cat eat...")
}
func (dog Dog)sleep() {
fmt.Println("dog sleep...")
}
func (cat Cat)sleep() {
fmt.Println("cat sleep")
}
type Person struct {
}
func(person Person)care(pet Pet){
pet.eat()
pet.sleep()
}
type Pig struct {
}
func (pig Pig)eat(){
fmt.Println("pig eat")
}
func(pig Pig)sleep(){
fmt.Println("pig sleep")
}
func main() {
dog := Dog{}
cat := Cat{}
person := Person{}
person.care(dog)
person.care(cat)
pig := Pig{}
person.care(pig)
}
- 其实就是在原有的基础上,再套一个保护壳(定义一个结构体,实现其方法,原接口作为输入值)
- 注意,如果是功能的修改,是要在原接口去增加的
- 思路
- 实例分析
- 情况1
结果: - 增加后
结果
- 情况1
OOP思想的实现和方法(无接口)
golang没有面向对象的概念,没有封装的概念,但可以通过结构体struct和函数绑定来实现OOP的属性和方法等特性,接受者receiver方法
:::info 实例
:::