【Go】面向对象 - 匿名字段、接口(转载)

文章目录
一、匿名字段
1. 什么是匿名字段
2. 同名字段的情况
3. 所有的内置类型和自定义类型都可以作为匿名字段去使用
4. 指针类型匿名字段
5. 匿名字段与面向对象
二、接口
1. 接口是一种类型
2. 为什么要使用接口
3. 接口的定义
4. 实现接口的条件
5. 接口类型变量
6. 值接收者和指针接收者实现接口的区别
(1)值接收者实现接口
(2)指针接收者实现接口
7. 类型与接口的关系
(1)一个类型实现多个接口
(2)多个类型实现同一接口
(3)接口嵌套
8. 空接口
(1)空接口的定义
(2)空接口的应用
9. 什么时候需要写接口?
三、参考链接
一、匿名字段
1. 什么是匿名字段
go 支持 只提供类型而不写字段名 的方式,也就是 匿名字段 ,也称为嵌入字段。

package main

import "fmt"

//go支持只提供类型而不写字段名的方式,也就是匿名字段,也称为嵌入字段

//人-结构体
type Person struct {
    name string
    sex  string
    age  int
}

type Student struct {
    Person
    id   int
    addr string
}

func main() {
    // 初始化
    s1 := Student{Person{"5lmh", "man", 20}, 1, "bj"}
    fmt.Println(s1)

    s2 := Student{Person: Person{"5lmh", "man", 20}}
    fmt.Println(s2)

    s3 := Student{Person: Person{name: "5lmh"}}
    fmt.Println(s3)
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
输出结果:

{{5lmh man 20} 1 bj}
{{5lmh man 20} 0 }
{{5lmh  0} 0 }
1
2
3
2. 同名字段的情况
package main

import "fmt"

//人
type Person struct {
    name string
    sex  string
    age  int
}

type Student struct {
    Person
    id   int
    addr string
    //同名字段
    name string
}

func main() {
    var s Student
    // 给自己字段赋值了
    s.name = "5lmh"
    fmt.Println(s)

    // 若给父类同名字段赋值,如下
    s.Person.name = "枯藤"
    fmt.Println(s)
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
输出结果:

{{  0} 0  5lmh}
{{枯藤  0} 0  5lmh}
1
2
3. 所有的内置类型和自定义类型都可以作为匿名字段去使用
package main

import "fmt"

//人
type Person struct {
    name string
    sex  string
    age  int
}

// 自定义类型
type mystr string

// 学生
type Student struct {
    Person
    int
    mystr
}

func main() {
    s1 := Student{Person{"5lmh", "man", 18}, 1, "bj"}
    fmt.Println(s1)
}

 
输出结果:

{{5lmh man 18} 1 bj}
1
4. 指针类型匿名字段
package main

import "fmt"

//人
type Person struct {
    name string
    sex  string
    age  int
}

// 学生
type Student struct {
    *Person
    id   int
    addr string
}

func main() {
    s1 := Student{&Person{"5lmh", "man", 18}, 1, "bj"}
    fmt.Println(s1)
    fmt.Println(s1.name)
    fmt.Println(s1.Person.name)
}

 
输出结果:

{0xc0000c0450 1 bj}
5lmh
5lmh
1
2
3
5. 匿名字段与面向对象
结构体的匿名字段如果是结构体类型,那么该匿名字段就像是一个对父结构体有继承权的子结构体。

通过匿名字段,可 获得和继承类似的复用能力 。依据编译器查找次序,只需在外层定义同名方法,就可以实现 “override”。详细讲解请参考我的另一篇文章:【Go】Go语言中的方法

二、接口
接口(interface)定义了一个对象的行为规范,只定义规范 不实现,由具体的对象来实现规范的细节。

1. 接口是一种类型
在Go语言中接口(interface)是一种类型,一种抽象的类型。

interface 是一组 method 的集合,是 duck-type programming 的一种体现。(duck-type:“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子”。在鸭子类型中,关注点在于对象的行为,能作什么;而不是关注对象所属的类型。)接口做的事情就像是定义一个协议(规则),只要一台机器有洗衣服和甩干的功能,我就称它为洗衣机。不关心属性(数据),只关心行为(方法)。

为了保护你的Go语言职业生涯,请牢记接口(interface)是一种类型。

2. 为什么要使用接口
实例:

package main

import "fmt"

type Cat struct{}

func (c Cat) Say() string { return "喵喵喵" }

type Dog struct{}

func (d Dog) Say() string { return "汪汪汪" }

func main() {
    c := Cat{}
    fmt.Println("猫:", c.Say())
    d := Dog{}
    fmt.Println("狗:", d.Say())
}

 
输出结果:

猫: 喵喵喵
狗: 汪汪汪
1
2
上面的代码中定义了猫和狗,然后它们都会叫,你会发现 main 函数中明显有重复的代码,如果我们后续再加上猪、青蛙等动物的话,我们的代码还会一直重复下去。那我们能不能把它们当成“能叫的动物”来处理呢?

像类似的例子在我们编程过程中会经常遇到:

比如一个网上商城可能使用支付宝、微信、银联等方式去在线支付,我们能不能把它们当成“支付方式”来处理呢?

比如三角形,四边形,圆形都能计算周长和面积,我们能不能把它们当成“图形”来处理呢?

比如销售、行政、程序员都能计算月薪,我们能不能把他们当成“员工”来处理呢?

Go语言中为了解决类似上面的问题,就设计了接口这个概念。接口区别于我们之前所有的具体类型,接口是一种抽象的类型。当你看到一个接口类型的值时,你不知道它是什么,唯一知道的是通过它的方法能做什么。

3. 接口的定义
Go语言提倡 面向接口 编程。

接口是一个或多个方法的签名的集合。
任何类型的方法集中只要拥有该接口对应的 全部方法 签名,就表示它 “实现” 了该接口,无须在该类型上显式声明实现了哪个接口。这称为Structural Typing。
所谓对应方法,是指有相同名称、参数列表 (不包括参数名) 以及返回值。
当然,该类型还可以有其他方法。
注意:

接口只有 方法声明 ,没有实现,没有数据字段。
接口可以匿名嵌入其他接口,或嵌入到结构中。
对象赋值给接口时,会发生 拷贝 ,而接口内部存储的是指向这个复制品的指针,既无法修改复制品的状态,也无法获取指针。
只有当接口存储的类型和对象都为nil时,接口才等于nil。
接口调用不会做 receiver 的自动转换。
接口同样支持匿名字段方法。
接口也可实现类似OOP中的多态。
空接口可以作为任何类型数据的容器。
一个类型可实现多个接口。
接口命名习惯以 er 结尾。
接口定义的语法:

每个接口由数个方法组成,接口的定义格式如下:

 type 接口类型名 interface{
     方法名1( 参数列表1 ) 返回值列表1
     方法名2( 参数列表2 ) 返回值列表2
     …
 }
1
2
3
4
5
其中:

接口名:使用 type 将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫Writer,有字符串功能的接口叫Stringer等。接口名最好要能突出该接口的类型含义。
方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。
举个例子:

type writer interface{
    Write([]byte) error
}
1
2
3
当你看到这个接口类型的值时,你不知道它是什么,唯一知道的就是可以通过它的Write方法来做一些事情。

4. 实现接口的条件
一个对象 只要实现了接口中的 全部方法 ,那么就实现了这个接口。换句话说,接口就是一个需要实现的方法列表。

我们来定义一个 Sayer 接口:

// Sayer 接口
type Sayer interface {
    Say()
}
1
2
3
4
定义dog和cat两个结构体:

type Cat struct{}
type Dog struct {}
1
2
因为Sayer接口里只有一个 say 方法,所以我们只需要给 dog 和 cat 分别实现 say 方法就可以实现 Sayer 接口了。

// cat实现了Sayer接口
func (c Cat) Say() {
    fmt.Println("喵喵喵")
}

// dog实现了Sayer接口
func (d Dog) Say() {
    fmt.Println("汪汪汪")
}
1
2
3
4
5
6
7
8
9
接口的实现就是这么简单,只要实现了接口中的所有方法,就实现了这个接口。

5. 接口类型变量
那实现了接口有什么用呢?

Important:接口类型变量能够存储所有实现了该接口的实例。

例如上面的示例中,Sayer 类型的变量能够存储 dog 和 cat 类型的变量。

func main() {
    var x Sayer // 声明一个Sayer类型的变量x
    a := cat{}  // 实例化一个cat
    b := dog{}  // 实例化一个dog
    x = a       // 可以把cat实例直接赋值给x
    x.say()     // 喵喵喵
    x = b       // 可以把dog实例直接赋值给x
    x.say()     // 汪汪汪
}
1
2
3
4
5
6
7
8
9
可以将实现了接口的结构体理解为该接口类型的一个实例。

完整代码:

package main

import "fmt"

//“说话者”接口,包含一个声明的方法Say()
type Sayer interface {
    Say()
}

//定义Cat结构体并在其上实现方法Say(),那么Cat实现了接口Sayer
type Cat struct{}

func (c Cat) Say() {
    fmt.Println("喵喵喵")
}

//定义Dog结构体并在其上实现方法Say(),那么Dog实现了接口Sayer
type Dog struct{}

func (d Dog) Say() {
    fmt.Println("汪汪汪")
}

func main() {
    var x Sayer
    a := Cat{} // 实例化一个Cat
    b := Dog{} // 实例化一个Dog

    x = a   // 可以把Cat实例直接赋值给x
    x.Say() // 喵喵喵
    x = b   // 可以把Dog实例直接赋值给x
    x.Say() // 汪汪汪

}

1
 
输出结果:

喵喵喵
汪汪汪
1
2
或者:

package main

import "fmt"

//“说话者”接口,包含一个声明的方法Say()
type Sayer interface {
    Say()
}

//定义Cat结构体并在其上实现方法Say(),那么Cat实现了接口Sayer
type Cat struct{}

func (c Cat) Say() {
    fmt.Println("喵喵喵")
}

//定义Dog结构体并在其上实现方法Say(),那么Dog实现了接口Sayer
type Dog struct{}

func (d Dog) Say() {
    fmt.Println("汪汪汪")
}

func main() {
    var x Sayer
    x = Cat{}   //接口类型变量X可以存储所有实现了该接口的类型的变量
    x.Say()
    x = Dog{}
    x.Say()

}

 
输出结果:

喵喵喵
汪汪汪
1
2
6. 值接收者和指针接收者实现接口的区别
使用值接收者实现接口和使用指针接收者实现接口有什么区别呢?接下来我们通过一个例子看一下其中的区别。

我们有一个Mover接口和一个dog结构体。

type Mover interface {
    move()
}

type dog struct {}
1
2
3
4
5
(1)值接收者实现接口
值接收者是:在结构体 dog 上实现接口中的方法 move() 时,接收者是值类型的(不是指针类型)。

func (d dog) move() {  //dog类型实现了接口Mover
    fmt.Println("狗会动")
}
1
2
3
此时,dog 类型 实现了接口 Mover,接口类型 Mover 的变量就可以接收 dog 类型的变量了,同时,接口类型 Mover 的变量还可以接收 *dog类型的变量:

func main() {
    var x Mover
    var wangcai = dog{} // 旺财是dog类型
    x = wangcai         // x可以接收dog类型
    var fugui = &dog{}  // 富贵是*dog类型
    x = fugui           // x可以接收*dog类型
    x.move()
}
1
2
3
4
5
6
7
8
完整代码:

package main

import "fmt"

type Mover interface {
    move()
}

type dog struct{}

func (d dog) move() {   //dog类型实现了接口Mover
    fmt.Println("狗会动")
}

func main() {
    var x Mover
    var wangcai = dog{} // 旺财是dog类型
    x = wangcai         // x可以接收dog类型
    x.move()
    var fugui = &dog{} // 富贵是*dog类型
    x = fugui          // x可以接收*dog类型
    x.move()
}

 
输出结果:

狗会动
狗会动
1
2
从上面的代码中我们可以发现,使用值接收者实现接口之后,不管是 dog 结构体还是结构体指针 *dog 类型的变量都可以赋值给该接口变量。
因为Go语言中有 对指针类型变量求值 的语法糖,dog 指针 fugui 内部会自动求值 *fugui 。

(2)指针接收者实现接口
同样的代码我们再来测试一下使用指针接收者有什么区别:

func (d *dog) move() {   //*dog类型实现了接口Mover
    fmt.Println("狗会动")
}
func main() {
    var x Mover
    var wangcai = dog{} // 旺财是dog类型
    x = wangcai         // x不可以接收dog类型,会报错cannot use wangcai (type dog) as type Mover in assignment
    var fugui = &dog{}  // 富贵是*dog类型
    x = fugui           // x可以接收*dog类型
}
1
2
3
4
5
6
7
8
9
10
完整代码:

package main

import "fmt"

type Mover interface {
    move()
}

type dog struct{}

func (d *dog) move() {    //*dog类型实现了接口Mover
    fmt.Println("狗会动")
}

func main() {
    var x Mover
    //var wangcai = dog{} // 旺财是dog类型
    //x = wangcai         // x不可以接收dog类型

    var fugui = &dog{} // 富贵是*dog类型
    x = fugui          // x可以接收*dog类型
    x.move()
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
输出结果:

狗会动
1
此时 *dog类型 实现了 Mover 接口,所以不能给 x 传入 dog 类型的 wangcai,此时 x 只能存储 *dog 类型的值。

下面的代码是一个比较好的面试题:请问下面的代码是否能通过编译?

package main

import "fmt"

type People interface {
    Speak(string) string
}

type Student struct{}

func (stu *Student) Speak(think string) (talk string) {
    if think == "sb" {
        talk = "你是个大帅比"
    } else {
        talk = "您好"
    }
    return
}

func main() {
    var peo People = Student{}
    think := "bitch"
    fmt.Println(peo.Speak(think)) //peo是Student类型的,而实现接口方法Speak的是*Student类型,所以会出错
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
报错:

# hello
.\main.go:21:6: cannot use Student{} (type Student) as type People in assignment:
    Student does not implement People (Speak method has pointer receiver)
1
2
3
这样改就好了:

package main

import "fmt"

type People interface {
    Speak(string) string
}

type Student struct{}

func (stu *Student) Speak(think string) (talk string) {
    if think == "sb" {
        talk = "你是个大帅比"
    } else {
        talk = "您好"
    }
    return
}

func main() {
    var peo People = &Student{} //改成*Stduent类型
    think := "bitch"
    fmt.Println(peo.Speak(think))
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
输出结果:

您好
1
7. 类型与接口的关系
(1)一个类型实现多个接口
一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。 例如,狗可以叫,也可以动。我们就分别定义 Sayer 接口和 Mover 接口,如下:

// Sayer 接口
type Sayer interface {
    say()
}

// Mover 接口
type Mover interface {
    move()
}
1
2
3
4
5
6
7
8
9
类型 dog 既可以实现 Sayer 接口,也可以实现 Mover 接口:

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)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
完整代码:

package main

import "fmt"

// Sayer 接口
type Sayer interface {
    say()
}

// Mover 接口
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()
}

 
输出结果:

旺财会叫汪汪汪
旺财会动
1
2
(2)多个类型实现同一接口
Go语言中不同的类型还可以实现同一接口。

首先我们定义一个 Mover 接口,它要求必须有一个 move 方法。

// Mover 接口
type Mover interface {
    move()
}
1
2
3
4
例如狗可以动,汽车也可以动,可以使用如下代码实现这个关系:

type dog struct {
    name string
}

type car struct {
    brand string
}

// dog类型实现Mover接口
func (d dog) move() {
    fmt.Printf("%s会跑\n", d.name)
}

// car类型实现Mover接口
func (c car) move() {
    fmt.Printf("%s速度70迈\n", c.brand)
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
这个时候我们在代码中就可以把狗和汽车当成一个会动的物体来处理了,不再需要关注它们具体是什么,只需要调用它们的 move 方法就可以了。

func main() {
    var x Mover
    var a = dog{name: "旺财"}
    var b = car{brand: "保时捷"}
    x = a
    x.move()
    x = b
    x.move()
}
1
2
3
4
5
6
7
8
9
完整代码:

package main

import "fmt"

// Mover 接口
type Mover interface {
    move()
}

type dog struct {
    name string
}

type car struct {
    brand string
}

// dog类型实现Mover接口
func (d dog) move() {
    fmt.Printf("%s会跑\n", d.name)
}

// car类型实现Mover接口
func (c car) move() {
    fmt.Printf("%s速度70迈\n", c.brand)
}

func main() {
    var x Mover
    var a = dog{name: "旺财"}
    var b = car{brand: "保时捷"}
    x = a
    x.move()
    x = b
    x.move()
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
输出结果:

旺财会跑
保时捷速度70迈
1
2
并且一个接口的方法,不一定需要由一个类型完全实现。

可以通过在类型中嵌入其他类型或者结构体来 “继承” 其对接口方法的实现,从而只实现剩下的方法,就能完全实现接口。

下例中,dryer 结构体实现了接口的 dry() 方法;haier 结构体由于内嵌了 dryer 结构体,所以 “继承” 了它对 dry() 方法的实现,然后 haier 自己实现了 wash() 方法。这样,haier 结构体把接口的两个方法都实现了,就实现了接口。

而 dryer 结构体由于只实现了dry() 方法,所以没能实现接口。所以如果你把var x WashingMachine = haier{}改成var x WashingMachine = dryer{}就会报错:cannot use dryer{} (type dryer) as type WashingMachine in assignment: dryer does not implement WashingMachine (missing wash method)(提示你 dryer 结构体缺失了 wash 方法的实现)

package main

import "fmt"

// WashingMachine 洗衣机
type WashingMachine interface {
    wash()
    dry()
}

type dryer struct{} // 甩干器
func (d dryer) dry() { // 实现WashingMachine接口的dry()方法
    fmt.Println("甩一甩")
}

type haier struct { // 海尔洗衣机
    dryer // 匿名字段,嵌入甩干器
}
func (h haier) wash() { // 实现WashingMachine接口的wash()方法
    fmt.Println("洗刷刷")
}

func main() {
    var x WashingMachine = haier{}
    x.wash()
    x.dry()
}

 
输出结果:

洗刷刷
甩一甩
————————————————
版权声明:本文为CSDN博主「想变厉害的大白菜」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_44211968/article/details/122992407

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值