一、方法

1.方法是什么

你可以为某个类型声明它的专属函数,如为 A 类型声明函数 f

函数 f 就附加在了类型 A 上,使用 A.f() 来调用

这时 f 就称为 A 的 「方法」

类型 A 就称为方法 f 的 「接收者」

方法就是一种带「接收者」参数的函数

2.为什么要使用方法

方法使我们能在Go中运用面向对象的思想来编程(就像C++中的类一样)

3.如何使用方法

3.1 方法的声明

首先复习一下函数的声明格式

1
2
3
4
func 函数名 (参数列表) (返回值列表){
// 函数体
return 返回值列表
}

方法无疑就是多了个接收者参数,位置在 func 和函数名之间

1
2
3
4
func (接收者) 函数名 (参数列表) (返回值列表){
// 函数体
return 返回值列表
}

注意:

  1. 你不能在 A 包内为 B 包内的一个类型定义方法,也就是说方法和接收者必须在同一个包里

  2. 内建类型无法定义方法,因为这些类型不是你定义的,你也不知道它是在哪个包里定义的

例子:声明一个 point 类型,用于表示一个二维坐标的点,再为它声明一个方法 dis ,用于获取该点到原点的距离。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"fmt"
"math"
)

//声明 Point 类型
type Point struct {
X, Y float64
}

//声明 dis 方法
func (a Point) dis() float64 {
return math.Sqrt(a.X*a.X + a.Y*a.Y)
}

func main() {
a := Point{3, 4}
fmt.Println(a.dis()) //输出 5
}

3.2 值接收者与指针接收者

如为 A 类型声明方法 f,那么接收者就是值,每次传入的都是拷贝的一个副本,方法内操作的是原变量的副本

如为 A 类型的指针声明方法 f,那么接收者就是指针,每次传入的都是一个指针,方法内操作的是原变量

现在新声明一个方法 add,实现对坐标的加法功能

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
package main

import (
"fmt"
"math"
)

//声明 Point 类型
type Point struct {
X, Y float64
}

//声明 dis 方法
func (a Point) dis() float64 {
return math.Sqrt(a.X*a.X + a.Y*a.Y)
}

//声明 add 方法,新增加法功能
func (a *Point) add(n, m float64) {
a.X += n
a.Y += m
}

func main() {
a := Point{3, 4}
fmt.Println(a) //输出 {3 4}
fmt.Println(a.dis()) //输出 5
(&a).add(2, 8)
fmt.Println(a) //输出 {5 12}
fmt.Println(a.dis()) //输出 13
}

3.3 指针重定向

刚才为 Point 的指针声明了方法 add,但实际上使用值也可以成功调用,Go可以为你自动转换(重定向)

1
2
3
4
5
6
7
8
func main() {
a := Point{3, 4}
fmt.Println(a) //输出 {3 4}
fmt.Println(a.dis()) //输出 5
a.add(2, 8) //与 (&a).add(2, 8) 等价,没有问题
fmt.Println(a) //输出 {5 12}
fmt.Println(a.dis()) //输出 13
}

也就是说,如果方法的接收者为指针,传入时无论是值或者指针都会被自动转换为指针

对立情况也是如此,如果方法的接收者为值,传入时无论是值或者指针都会被自动转换为值

1
fmt.Println((&a).dis()) //会自动转换,没有问题

二、接口

首先感谢这个视频,教会了我接口

1.为什么要用接口

在讲接口是什么的时候,有必要先弄清楚为什么要用接口

接口同方法一样,也是服务于面向对象思想的

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

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

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

Go语言中为了解决类似上面的问题,就设计了接口这个概念。接口区别于我们之前所有的具体类型,接口是一种抽象的类型。

当你看到一个接口类型的值时,你不知道它是什么,唯一知道的是通过它的方法能做什么。

2.接口是什么

接口是一种数据类型 ,它的作用是 保存 一类符合条件的数据类型

“符合条件”具体地说就是看那些数据类型有没有实现某个接口所规定的方法,凡是实现了这些方法的类型就称实现了这个接口,实现了这个接口,这个接口就能保存那些类型

接口不管你的方法怎么实现的,只要你有这个方法就行。也就是说,你拿到一个接口,只知道它能实现什么(有什么方法),而不知道它具体是怎么完成的

3.接口怎么用

3.1 接口的声明

接口的声明格式如下:

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

}

注意:接口在命名时,一般会在单词后面添加 er,如有写操作的接口叫 Writer,有字符串功能的接口叫 Stringer 等。接口名最好要能突出该接口的类型含义

3.2 接口的基础用法

我们来定义一个 Sayer 接口:

1
2
3
4
// Sayer 接口
type Sayer interface {
say()
}

再定义 dog 和 cat 两个结构体:

1
2
3
type dog struct {}

type cat struct {}

因为 Sayer 接口里只有一个 say 方法,所以我们只需要给 dog 和 cat 分别实现 say 方法就可以实现 Sayer 接口了

1
2
3
4
5
6
7
8
9
// dog实现了Sayer接口
func (d dog) say() {
fmt.Println("汪汪汪")
}

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

接口类型变量能够存储所有实现了该接口的实例, 例如上面的示例中,Sayer 类型的变量能够存储 dog 和 cat 类型的变量

1
2
3
4
5
6
7
8
9
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() // 汪汪汪
}

3.3 结合函数的用法

先假设我们摸一只猫的时候,猫会喵喵喵地叫,摸一条狗的时候,它会汪汪汪地叫

现在我们写一个”摸“函数,要求来什么都会摸一下,并且不管是猫还是狗都可以传入

这时我们就可以用接口来作为参数

1
2
3
4
5
6
7
8
9
func touch(x Sayer) {
x.say()
}
func main() {
a := cat{} // 实例化一个cat
b := dog{} // 实例化一个dog
touch(a) //喵喵喵
touch(b) //汪汪汪
}

这便是结合函数的用法

3.4 值接收者和指针接收者实现接口的区别

先来说值接收者实现接口

在上面的例子中,无论是猫还是狗的 say 方法用的都是值接收者

拿狗举例子,我们分别尝试把 dog 类型和 *dog 类型赋给一个 Sayer 接口

第一条(dog类型)我们取名为 旺财,第二条(*dog类型)我们取名为 富贵

1
2
3
4
5
6
7
8
func main() {
var x Sayer
var wangcai = dog{} // 旺财是dog类型
x = wangcai // x可以接收dog类型
var fugui = &dog{} // 富贵是*dog类型
x = fugui // x可以接收*dog类型
x.say()
}

从上面的代码中我们可以发现,使用值接收者实现接口之后,不管是 值(dog) 还是 指针(*dog) 类型的变量都可以赋值给该接口变量,如果是指针的话会先自动求它的值

指针接收者实现接口则不同:

如果我们把狗的 say 方法改为指针接收者,则会有错误:

1
2
3
func (d *dog) say() {   //接收者改为了指针类型
fmt.Println("汪汪汪")
}
1
2
3
4
5
6
7
8
9
func main() {
var x Sayer
var wangcai = dog{} // 旺财是dog类型
x = wangcai //错误:x不能接收dog类型
x.say()
var fugui = &dog{} // 富贵是*dog类型
x = fugui // x可以接收*dog类型
x.say()
}

此时实现 Sayer 接口的是 *dog 类型,所以不能给 x 传入 dog 类型的 wangcai,此时 x 只能存储 *dog 类型的值

3.5 接口嵌套

接口与接口间可以通过嵌套创造出新的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Sayer 接口
type Sayer interface {
say()
}

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

// 接口嵌套
type animal interface {
Sayer
Mover
}

嵌套得到的接口的使用与普通接口一样,只是一种偷懒的方法,相当于把其他接口的内容复制过来

3.6 空接口

空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口

这不就很好玩了吗?空接口可以存储任意类型的变量!

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
// 定义一个空接口x
var x interface{}
s := "Hello World"
x = s
fmt.Printf("type:%T value:%v\n", x, x)
i := 100
x = i
fmt.Printf("type:%T value:%v\n", x, x)
b := true
x = b
fmt.Printf("type:%T value:%v\n", x, x)
}

空接口有两个主要应用:

作为函数的参数

使用空接口实现可以接收任意类型的函数参数

1
2
3
4
// 空接口作为函数参数
func show(a interface{}) {
fmt.Printf("type:%T value:%v\n", a, a)
}

作为map的值

使用空接口实现可以保存任意值的字典

1
2
3
4
5
6
// 空接口作为map值
var studentInfo = make(map[string]interface{})
studentInfo["name"] = "小明"
studentInfo["age"] = 18
studentInfo["married"] = false
fmt.Println(studentInfo)

3.7 类型断言

想判断接口中保存的类型可以用类型断言(其实就是猜测)来完成,格式为:

1
x.(T)

其中 T 表示断言 x 可能是的类型

该语法返回两个参数,第一个参数是 x 转化为 T 类型后的变量,第二个值是一个布尔值,若为 true 则表示断言(猜测)成功,为 false 则表示断言(猜测)失败。

1
2
3
4
5
6
7
8
9
10
func main() {
var x interface{}
x = "Hello 沙河"
v, ok := x.(string)
if ok {
fmt.Println(v)
} else {
fmt.Println("类型断言失败")
}
}

如果要断言多次就需要写多个 if 判断,这个时候我们可以使用 switch 语句来实现:

1
2
3
4
5
6
7
8
9
10
11
12
func justifyType(x interface{}) {
switch v := x.(type) {
case string:
fmt.Printf("x is a string,value is %v\n", v)
case int:
fmt.Printf("x is a int is %v\n", v)
case bool:
fmt.Printf("x is a bool is %v\n", v)
default:
fmt.Println("unsupport type!")
}
}