37、Go 结构体 - 面向对象之接口
hi,我是温新
什么是接口
面向对象语言中,接口用于定义对象的行为。接口只指定对象应该做什么,实现这种行为的方式由对象决定。
**在 Go 语言中,接口是一组方法签名。**接口指定了类型应该具有的方法,类型决定了如何实现这些方法。当某个类型为接口中的所有方法提供了具体的实现细节时,这个类型就被称为实现了该接口。接口定义了一组方法,若某个对象实现了该接口的所有方法,则此对象就实现了接口。
Go 语言的类型都是隐式实现接口的。任何定义了接口中所有方法的类型都被称为隐式地实现了该接口。
接口定义与实现
定义接口的语法
1、接口声明语法
type 接口名 interface {
方法1([参数列表]) 返回值列表
...
}
参数解释:
接口名:使用 type 将接口定义为自定义的类型名。Go 中,接口命名时一般在单词后面加 er,如 Writer,Stringer;
方法:当方法名首字母大写,且接口名首字母也是大写时,则这个方法可以被接口所在包之外的代码使用;。
2、实现接口的语法
func (变量名 结构体类型) 方法名(参数列表) [返回值] {
}
接口实现
package main
import "fmt"
func main() {
var phone Phone
phone = new(AndroidPhone)
fmt.Printf("%T,%v,%p\n", phone, phone, phone)
phone.call()
ip := IPhone{}
fmt.Printf("%T,%v,%p\n", ip, ip, ip)
ip.call()
}
// 定义一个接口 Phone 接口,并定义一个方法
type Phone interface {
call()
}
// 定义一个安卓机接口
type AndroidPhone struct {
}
// 定义一个苹机果接口
type IPhone struct {
}
// 安卓机实现方法
func (a AndroidPhone) call() {
fmt.Println("我是一个安卓机,可以打电话")
}
// 苹果机实现方法
func (i IPhone) call() {
fmt.Println("我是苹果机,可以打电话")
}
输出结果
*main.AndroidPhone,&{},0xceb418
我是一个安卓机,可以打电话
main.IPhone,{},%!p(main.IPhone={})
我是苹果机,可以打电话
通过基本案例理解接口
package main
import "fmt"
func main() {
// 定义实现接口的对象
duck := Duck{"大鸭"}
person := Person{"王美丽"}
fmt.Println(duck.SayHello())
fmt.Println(person.SayHello())
// 定义接口类型的对象
var i ISayHello
i = duck
fmt.Printf("%T,%v,%p\n", i, i, &i)
fmt.Println(i.SayHello())
i = person
fmt.Printf("%T,%v,%p\n", i, i, &i)
fmt.Println(i.SayHello())
}
// 定义一个接口
type ISayHello interface {
SayHello() string
}
// 定义一个鸭子的结构体
type Duck struct {
name string
}
// 定义一个人的结构体
type Person struct {
name string
}
// 鸭子实现说话的接口
func (d Duck) SayHello() string {
return d.name + "叫:ga ga ga"
}
// 人实现说话的接口
func (p Person) SayHello() string {
return p.name + "说:hello"
}
1、在这个案例中,定义了一个 ISayHello
接口和 Duck
、Person
接口体;
2、人和鸭子都可以发出声音,因此定义一个 ISayHello
接口,然后定义两个方法分别实去实现接口;
3、main
中调用接口。第一种方法是通过接口对象duck := Duck{"大鸭"}
,第二种方法是通过接口类型的对象var i ISayHello
;
3-1、接口对象构造出 duck 和 person 对象,然后通过该对象来调用 SayHello
方法;
3-2、通过接口类型的对象,首先定义一个接口类型的变量var i ISayHello
,然后将实现接口的对象赋值给该变量,然后通过该变量调用方法。
在 Go 语言中,结构体类型 T 不需要显示地声明它实现了接口 I。只要类型 T 实现了接口 I 规定的所有方法,那么它就自动实现了接口 I。
接口实现多态
多态:是指根据类型的具体实现采取不同行为的能力。
下面是一个关于动物形态的案例:假设我们有几种不同类型的动物,比如狗、猫和鸟。首先,我们需要定义一个Animal
接口,用于表示一个动物的共性特征,比如发出声音和移动。
package main
import "fmt"
func main() {
dog := Dog{}
bird := Bird{}
PerformActions(dog)
PerformActions(bird)
}
// Animal 接口定义了动物的行为
type Animal interface {
Sound() string
Move() string
}
// Dog 是狗的结构体
type Dog struct{}
// Sound 返回狗的声音
func (d Dog) Sound() string {
return "汪汪汪"
}
// Move 返回狗的移动方式
func (d Dog) Move() string {
return "跑"
}
// Bird 是鸟的结构体
type Bird struct{}
// Sound 返回鸟的声音
func (b Bird) Sound() string {
return "啾啾啾"
}
// Move 返回鸟的移动方式
func (b Bird) Move() string {
return "飞"
}
// PerformActions 展示动物的多态行为
func PerformActions(animal Animal) {
fmt.Println(animal.Sound())
fmt.Println(animal.Move())
}
输出结果
汪汪汪
跑
啾啾啾
飞
空接口
空接口的定义与声明
什么是空接口
空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口。
空接口的定义
interface{}
空接口的使用
package main
import "fmt"
func main() {
// 定义一个空接口
var i interface{}
// 为空接口赋值
i = 100
fmt.Printf("类型:%T,值%v\n", i, i)
i = "王美丽"
fmt.Printf("类型:%T,值%v\n", i, i)
i = []int{1, 2, 3}
fmt.Printf("类型:%T,值%v\n", i, i)
}
输出结果
类型:int,值100
类型:string,值王美丽
类型:[]int,值[1 2 3]
空接口的应用场景
- 1、在函数中接受任意类型的参数,例如
fmt.Println
函数接受空接口作为参数,因此它可以打印不同类型的数据。 - 2、在数据结构中存储不同类型的值,如通用的键值对容器。
- 3、在反射(reflection)中,可以使用空接口来处理未知类型的数据。
- 3、在实现泛型编程时,空接口可以用于处理通用类型参数。
接口的应用
空接口作为函数参数
使用空接口实现可以接收任意类型的函数参数。
以下是一个示例,演示如何使用空接口作为函数的参数:
package main
import "fmt"
func main() {
printTypeAndValue(100)
printTypeAndValue("hello world")
printTypeAndValue([]int{1, 2, 3})
}
// printTypeAndValue 函数接受一个空接口作为参数,打印其类型和值
func printTypeAndValue(data interface{}) {
fmt.Printf("Type: %T, Value: %v\n", data, data)
}
输出结果
Type: int, Value: 100
Type: string, Value: hello world
Type: []int, Value: [1 2 3]
在上面的示例中,printTypeAndValue
函数接受一个空接口参数 data
,然后使用 %T
和 %v
格式化字符串打印参数的类型和值。
在 main
函数中,我们使用不同类型的数据(整数、字符串和浮点数)调用 printTypeAndValue
函数,它能够正确处理这些不同类型的输入。
空接口作为map的值
使用空接口实现可以保存任意值的字典。
在 Go 中,可以使用空接口作为 map
的值来创建一个具有不同数据类型值的通用 map
。这可以在需要存储不同类型数据的情况下使用。以下是一个示例:
package main
import "fmt"
func main() {
// 创建一个 map,其值为空接口
studentInfo := make(map[string]interface{})
// 向 map 中添加不同类型的值
studentInfo["name"] = "王美丽"
studentInfo["age"] = 19
// map 取值
fmt.Println(studentInfo)
name, nameExists := studentInfo["name"]
if nameExists {
fmt.Println(name)
}
}
类型断言
语法
类型断言,其语法格式:
x.(T)
参数解释:
x
:表示类型为 interface{} 的变量;
T
:表示断言 x 可能是的类型。
该语法返回两个参数,第一个参数是 x 转化为 T 类型后的变量,第二个值是一个布尔值,若为 true 则表示断言成功,为 false 则表示断言失败。
断言案例
package main
import "fmt"
func main() {
// 定义一个空接口
var i interface{}
i = 100
// i = "王美丽"
// 接口值断言为整数
value, ok := i.(int)
if ok {
fmt.Printf("Value: %d (Type: %T)\n", value, value)
} else {
fmt.Println("类型断言失败。i 不是整数。")
}
}
在这个示例中,我们创建了一个空接口 i
并将整数值 100
赋给它。接着,我们使用类型断言 i.(int)
尝试将 i
还原为整数类型。如果类型断言成功(即 ok
变量为 true
),我们就可以访问和操作 value
,它已经成为一个具体的整数。如果类型断言失败,那么程序将输出 "类型断言失败。i 不是整数。"
打开
// i = "王美丽"
注释,因为它是字符串,而判断的时候直接断言成整数,因此它会输出 类型断言失败。
为什么要使用类型断言
- 访问接口的底层值:当您需要访问接口的具体值时,类型断言允许您将接口还原为其底层类型。这对于在不知道接口底层类型的情况下访问数据非常有用。
- 类型安全:类型断言可以在运行时检查接口的实际类型,从而避免在不合适的类型上执行操作,减少了运行时错误的风险。
- 多态性和灵活性:类型断言使得 Go 的接口实现了多态性,允许不同类型的数据共享相同的接口。这对于编写通用的函数或数据结构非常有用。
- 接口值的转换:类型断言允许您将接口值转换为其具体类型,以便在特定上下文中使用。
值接收和指针接收
值接收者实现接口
package main
import "fmt"
func main() {
// 声明 Mover 类型的变量 m
var m Mover
// p1 是 Panda 类型
var p1 = Panda{}
// 将 p1 赋值给 m
m = p1
m.Move()
// p2 是 Panda 指针类型
var p2 = &Panda{}
// 把 P2 赋值给 m
m = p2
m.Move()
}
type Mover interface {
Move()
}
type Panda struct {
}
func (p Panda) Move() {
fmt.Println("走起来")
}
通过案例知道,使用值接收者实现的接口,无论是结构体类型还是对应的结构体指针类型的变量都可以赋值给该接口变量。
指针接收者实现接口
package main
import "fmt"
func main() {
var g GoMove
// 创建一个 Pig 指针变量
var c1 = &Pig{}
// 将 c1 赋值给 g
g = c1
g.run()
}
type GoMove interface {
run()
}
type Pig struct {
}
func (p *Pig) run() {
fmt.Println("跑起来")
}
现在,实现 GoMove
接口的是 *Pig
类型,因此可以把*Pig
类型的变量直接赋值给GoMove
接口变量的变量g
。如果是值类型将报错,如下赋值就会报错:
var c2 = Pig{}
g = c2 // 不能把 c2 赋值给 g
类型与接口的关系
一个类型实现多个接口
一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。如一个动物有很多种功能,可以跑,可以叫。
package main
import "fmt"
func main() {
// 创建一个 Animal 对象
dog := Animals{Name: "Dog"}
// 使用 Walker 接口调用 Walk 方法
var walker Walker
walker = dog
fmt.Println(walker.Walk())
// 使用 Swimmer 接口调用 Swim 方法
var swimmer Swimmer
swimmer = dog
fmt.Println(swimmer.Swim())
}
// 定义一个 Walker 接口
type Walker interface {
Walk() string
}
// 定义一个 Swimmer 接口
type Swimmer interface {
Swim() string
}
// 定义 Animal 结构体,它将实现 Walker 和 Swimmer 接口
type Animals struct {
Name string
}
// 实现 Walker 接口的 Walk 方法
func (a Animals) Walk() string {
return a.Name + " is walking"
}
// 实现 Swimmer 接口的 Swim 方法
func (a Animals) Swim() string {
return a.Name + " is swimming"
}
输出结果
Dog is walking
Dog is swimming
在这个示例中,我们定义了两个接口:Walker
和Swimmer
,每个接口都有一个方法。然后,我们创建了一个Animal
结构体,该结构体实现了Walker
和Swimmer
接口的方法。
在main
函数中,我们创建了一个Animal
对象,然后将它赋给两个不同的接口变量:walker
和swimmer
。通过这两个接口变量,我们可以调用Animal
类型的方法,分别实现了走和游的行为。
多种类型实现同一接口
package main
import "fmt"
func main() {
// 创建不同类型的动物对象
tiger := Tiger{Name: "小王子"}
elephant := Elephant{Name: "花象"}
animals := []Animiler{tiger, elephant}
for _, ani := range animals {
fmt.Println(ani.Speak())
}
}
type Animiler interface {
Speak() string
}
// 定义 Tiger 类型,实现 Animaler 接口
type Tiger struct {
Name string
}
func (t Tiger) Speak() string {
return t.Name + "says 狮子"
}
// 定义 Elephant 类型,实现 Animaler 接口
type Elephant struct {
Name string
}
func (e Elephant) Speak() string {
return e.Name + "says 大象"
}
输出结果
小王子says 狮子
花象says 大象
在这个示例中,我们定义了一个Animal
接口,它有一个Speak
方法。然后,我们创建了两个不同类型的动物:Dog
和Bird
,它们分别实现了Animal
接口的Speak
方法。
在main
函数中,我们创建了一个包含不同类型动物的切片,然后通过循环遍历切片中的动物,并使用Animal
接口来调用Speak
方法。这样,不同类型的动物都可以实现相同的接口,以支持相同的行为。
接口组合
在Go语言中,你可以使用接口组合来创建更复杂的接口,将多个接口组合在一起以定义新的接口。
下面是一个示例,使用接口组合定义了一个更复杂的Animal
接口,包含了Walker
和Swimmer
两个子接口:
package main
import "fmt"
func main() {
// 创建一个 Dog 对象
dog := Dog{"旺财"}
// 使用 Animal 接口来调用 Walk 和 Swim 方法
var animal Animal
animal = dog
fmt.Println(animal.Walk())
fmt.Println(animal.Swim())
}
// 定义 Walker 接口
type Walker interface {
Walk() string
}
// 定义 Swimmer 接口
type Swimmer interface {
Swim() string
}
// 定义 Animal 接口,组合 Walker 和 Swimmer 接口
type Animal interface {
Walker
Swimmer
}
// 定义 Dog 类型,实现 Walker 和 Swimmer 接口
type Dog struct {
Name string
}
func (d Dog) Walk() string {
return d.Name + " is walking"
}
func (d Dog) Swim() string {
return d.Name + " is swimming"
}
输出结果
旺财 is walking
旺财 is swimming
在这个示例中,我们首先定义了两个独立的接口:Walker
和 Swimmer
,分别表示能够行走和游泳的行为。然后,我们定义了一个更复杂的Animal
接口,通过接口组合,它包含了Walker
和 Swimmer
两个子接口。
接着,我们创建了一个Dog
类型,实现了Walker
和 Swimmer
接口的方法。在main
函数中,我们创建了一个Dog
对象,并将其赋给Animal
接口类型的变量animal
。通过Animal
接口,我们可以调用Walker
和Swimmer
接口的方法,实现了组合接口的功能。