Golang面向对象编程(三)
接口(interface)
基本介绍
基本介绍
- 在Go中,接口(interface)是一种用于定义方法集合的抽象类型,接口中定义了一组方法的签名,而不包含方法的实现细节,其他类型可以通过实现接口中定义的方法来满足接口的要求。
- 接口的实现是隐式的,类型不需要显式声明它实现了某个接口,只要类型提供了接口中定义的所有方法,就被视为实现了该接口。
- 接口提供了一种灵活的方式来定义和使用抽象的行为,通过接口可以实现与类型无关的编程,提高代码的灵活性和可扩展性。
接口的定义方式
接口的定义方式
Go中接口的定义方式如下:
相关说明:
- 接口中定义的都是方法的签名,没有方法体,也不能有任何变量。
- 接口本身不能创建实例,但可以指向一个实现了该接口的自定义类型的变量。
- 一个自定义类型要实现某个接口,就必须将接口中定义的所有方法都实现。
- 只要是自定义类型就可以实现接口,而不仅仅是结构体类型
例如,下面的代码中定义了一个Usb接口,并让自定义类型Phone和Camera实现了该接口。如下:
package main
import "fmt"
// 定义接口
type Usb interface {
Start()
Stop()
}
type Phone struct{}
// Phone实现Usb接口的所有方法
func (p Phone) Start() {
fmt.Println("phone start working...")
}
func (p Phone) Stop() {
fmt.Println("phone stop working...")
}
type Camera struct{}
// Camera实现Usb接口的所有方法
func (c Camera) Start() {
fmt.Println("camera start working...")
}
func (c Camera) Stop() {
fmt.Println("camera stop working...")
}
type Computer struct{}
func (c Computer) Working(usb Usb) { // 参数是Usb接口类型,可以接收任何实现了Usb接口的类型变量
usb.Start()
usb.Stop()
}
func main() {
computer := Computer{}
phone := Phone{}
camera := Camera{}
computer.Working(phone)
computer.Working(camera)
}
代码解释:
- 在现实生活中,手机和相机都可以通过USB接口与电脑传输数据,因此将Computer的Working方法的参数设置为Usb接口类型,此时Working方法就能接收任何实现了Usb接口的自定义类型变量。
- 由于Phone和Camera都实现了Usb接口,因此在调用Computer的Working方法时,如果传入的是Phone类型的变量,那么在Working方法内部调用的就是Phone对应的Start和Stop方法,如果传入的是Camera类型的变量,则调用的是Camera的Start和Stop方法。
程序的运行结果如下:
注意: 类型在实现接口中的定义的方法时,要么全部使用值接收者绑定,要么全部使用指针接收者绑定,不能在一个类型上混合使用值接收者和指针接收者来实现同一个接口。
接口的大小
接口类型本质是包含两个指针字段的数据结构:
- 动态类型指针:该指针指向接口变量所持有的数据值的类型信息,可以用于在运行时进行类型断言和动态方法调用。如果接口变量为空(nil),则动态类型指针也为空(nil)。
- 动态值指针:该指针指向接口变量所持有的数据值,具体的类型由动态类型指针指示。如果接口变量为空(nil),那么动态值指针也为空(nil)。
示意图如下:
因此对于64位系统来说,接口类型变量的大小为固定的16字节,其中8个字节用于存储动态类型指针,另外8个字节用于存储动态值指针。如下:
package main
import (
"fmt"
"unsafe"
)
type Usb interface {
Start()
Stop()
}
func main() {
var usb Usb
fmt.Printf("usb value = %v\n", usb) // usb value = <nil>
fmt.Printf("usb type = %T\n", usb) // usb type = <nil>
fmt.Printf("usb size = %d\n", unsafe.Sizeof(usb)) // usb size = 16
}
接口继承
接口继承
在定义接口时,一个接口可以继承多个其他接口,此时如果一个自定义类型要实现该接口,除了需要实现该接口中定义的方法外,还需要实现该接口继承的其他接口中定义的方法。如下:
package main
import "fmt"
type AInterface interface {
test1()
}
type BInterface interface {
test2()
}
type CInterface interface {
AInterface // 继承接口A
BInterface // 继承接口B
test3()
}
type Student struct{}
// Student实现A接口及其继承的B接口和C接口的所有方法
func (stu Student) test1() {
fmt.Println("test1...")
}
func (stu Student) test2() {
fmt.Println("test2...")
}
func (stu Student) test3() {
fmt.Println("test3...")
}
func main() {
var c CInterface = Student{}
c.test1() // test1...
c.test2() // test2...
c.test3() // test3...
}
注意: 只有实现了接口的类型,才能将其实例赋值给相应接口类型的变量,否则会产生报错。
空接口
空接口
空接口interface{}中没有定义任何方法,因此所有的类型都实现了空接口,可以将任意类型的变量赋值给空接口。如下:
package main
import "fmt"
type Student struct {
Name string
Age int
Gender string
}
func PrintValue(value interface{}) { // 参数是空接口类型,可以接收任意类型变量
fmt.Printf("value = %v\n", value)
}
func main() {
PrintValue(10) // value = 10
PrintValue(true) // value = true
PrintValue(Student{"Alice", 12, "女"}) // value = {Alice 12 女}
PrintValue(1.2) // value = 1.2
PrintValue("Hello World") // value = Hello World
}
自定义类型序列排序
自定义类型排序
Go标准库的sort包中提供了Sort函数,可用于对自定义类型序列进行排序。该函数的签名如下:
func Sort(data Interface)
Sort函数接收一个Interface类型的参数,并对其进行原地排序。Interface在sort包中是一个接口类型,接口中定义了三个方法,因此要使用Sort函数进行排序,需要先让自定义类型序列实现Interface接口。Interface接口的定义如下:
type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}
相关说明:
- Len方法:返回待排序序列中元素的个数,Sort底层排序时会通过Len方法获取待排序的元素个数。
- Less方法:确定排序的规则,如果返回true,表示索引为i的元素应该排在索引为j的元素之前,如果返回false,表示索引为i的元素应该排在索引为j的元素之后,Sort底层排序时会通过Less方法判断元素的位置是否需要交换。
- Swap方法:交换序列中索引为i和j的元素,Sort底层排序时会通过调用Swap方法实现元素位置的交换,以达到排序的目的。
使用案例如下:
package main
import (
"fmt"
"sort"
)
type Student struct {
Name string
Score float64
}
type Students []Student
// Students实现sort.Interface接口的所有方法
func (stus Students) Len() int {
return len(stus)
}
func (stus Students) Less(i int, j int) bool {
return stus[i].Score < stus[j].Score
}
func (stus Students) Swap(i int, j int) {
stus[i], stus[j] = stus[j], stus[i]
}
func main() {
var stus = Students{
{"张三", 89},
{"李四", 67},
{"王五", 75},
{"赵六", 99},
{"田七", 58},
{"周八", 92},
}
fmt.Printf("stus = %v\n", stus) // stus = [{张三 89} {李四 67} {王五 75} {赵六 99} {田七 58} {周八 92}]
sort.Sort(stus)
fmt.Printf("stus = %v\n", stus) // stus = [{田七 58} {李四 67} {王五 75} {张三 89} {周八 92} {赵六 99}]
}
说明一下:
- 代码中使用Sort函数对自定义类型Student切片进行排序,因此需要让Student切片实现Interface接口中的Len、Less和Swap方法。
- 为了让Student切片实现方法,首先通过type关键字将Student切片定义为自定义类型Students。
- Students的Len方法返回切片中元素的个数,Less方法中按照Student的成绩进行排序,当索引为i的Student的成绩小于索引为j的Student的成绩时返回true,表示按Student的成绩排升序,Swap方法完成切片中索引i和j位置的元素交换。
- 在Go中,
i, j = j, i
是一种特殊的赋值写法,可以避免在交换两个变量的值时使用额外的临时变量。
接口与继承
接口与继承
接口与继承的关系如下:
- 继承的价值主要在于解决代码的复用问题,使代码更具可维护性和可扩展性。而接口可以作为代码的契约,其明确规定了一组方法的输入、输出和预期行为,可以提高代码的可读性和可维护性,同时可以促进团队协作和代码的理解。
- 接口比继承更加灵活,继承是 is - a 的关系,比如Student能够继承Person,是因为Student与Person本就是 is - a 的关系。而接口只需满足 like - a 的关系,比如在BirdAble接口中定义一个Fly方法,那么实现了Fly方法的任意类型变量都可以赋值给BirdAble接口(这与鸭子理论中关注对象的行为而不是类型的思想一致)。
此外,接口可以看作是对继承的一种补充,当某个结构体需要扩展功能,同时不希望破坏已有的继承关系,这时可以定义出一个接口对其进行实现。如下:
上图说明:
- 座机和手机都可以看作是一种通信设备,所以可以继承通信设备的属性和行为。相机和监控都可以看作是一种拍摄设备,所以可以继承拍摄设备的属性和行为。
- 当需要让手机和相机都支持通过USB接口与电脑进行数据传输时,不能直接将这个功能添加到通信设备和拍摄设备当中,因为并不是所有的通信设备和拍摄设备都支持该功能,因此定义出一个Usb接口规范,让手机和相机各自根据规范对Usb接口进行实现。
- 虽然不定义接口直接让手机和相机实现对应的方法也能达到目的,但这样做会使得手机和相机各自实现的方法五花八门,不利于代码的维护和统一管理,这也正是引入接口的目的和意义所在。
多态
基本介绍
基本介绍
- 多态是面向对象编程中的一个重要概念,其允许使用相同的接口来处理不同的对象类型,实现代码的灵活性和可扩展性。多态使得我们可以在不修改现有代码的情况下,通过添加新的类或子类来扩展程序的功能。
- 多态是建立在继承关系上的,子类继承父类的属性和方法后,可以对继承自父类的方法进行重写(覆盖),以实现自己特定的行为。在多态中,通过使用父类的引用变量来引用子类的对象,这样就能在运行时根据实际对象类型来确定调用哪个类的方法。
多态的实现
多态的实现
- Go中的多态是通过接口(interface)来实现的,Go中的接口机制使得不同的类型可以实现相同的接口,然后通过统一的接口来进行调用和处理,达到多态的效果。
- 如果一个自定义类型实现了某个接口,那么该自定义类型的变量就可以赋值给对应的接口变量,这样就能在运行时根据接口变量实际指向的对象类型来确定调用哪个类型的方法。
- 例如在前面的USB接口案例中,Usb接口变量既可以接收Phone类型变量,也能能够接收Camera类型变量,在通过Usb接口变量调用Start和Stop方法时,会根据Usb接口变量实际指向的对象类型来调用对应类型的Start和Stop方法,这就体现出多态的特点。
类型断言
基本介绍
基本介绍
- 类型断言(Type Assertion)是一种在编程语言中用于检查接口值的实际类型的机制。在Go中,类型断言用于判断接口值是否实现了特定的接口或是否是某个具体类型,并且允许我们将接口值转换为其底层类型。
类型断言的方式
不带检测的类型断言
通过接口变量.(类型)
的方式可以将接口变量转换为指定类型,如果接口变量底层是所指定的类型,则断言成功并返回类型转换后的值,否则断言失败并触发panic异常。如下:
package main
import "fmt"
func TypeAssert(value interface{}) {
num := value.(int) // 不带检测的类型断言
fmt.Printf("num = %d\n", num)
}
func main() {
TypeAssert(10) // num = 10
// TypeAssert(10.1) // 类型断言失败,抛出panic
}
带检测的类型断言
当类型断言失败时如果不希望触发panic异常,可以对接口变量.(类型)
的第二个返回值进行接收,其表示本次类型断言是否成功,如果类型断言成功,则第一个返回值是类型转换后的值,如果类型断言失败,则第一个返回值是所指定类型的零值。如下:
package main
import "fmt"
type Usb interface {
Start()
Stop()
}
type Phone struct{}
// Phone实现Usb接口的所有方法
func (p Phone) Start() {
fmt.Println("phone start working...")
}
func (p Phone) Stop() {
fmt.Println("phone stop working...")
}
func (p Phone) Prompt() {
fmt.Println("phone get a prompt information...")
}
type Camera struct{}
// Camera实现Usb接口的所有方法
func (c Camera) Start() {
fmt.Println("camera start working...")
}
func (c Camera) Stop() {
fmt.Println("camera stop working...")
}
type Computer struct{}
func (c Computer) Working(usb Usb) { // 参数是Usb接口类型,可以接收任何实现了Usb接口的类型变量
usb.Start()
if phone, ok := usb.(Phone); ok { // 带检测的类型断言
phone.Prompt()
}
usb.Stop()
}
func main() {
computer := Computer{}
phone := Phone{}
camera := Camera{}
computer.Working(phone)
computer.Working(camera)
}
上述代码在Working方法中通过类型断言对Phone类型进行了特殊处理,运行程序后可以看到,在调用Computer的Working方法时,如果传入的是Phone类型变量,除了会调用其Start和Stop方法外,还会调用Phone的Prompt方法。如下:
参数类型识别
参数类型识别
将类型断言与switch语句相结合(Type Switch),可以将接口变量转换为具体类型,并根据不同的类型指向相应的代码逻辑。如下:
package main
import "fmt"
type Student struct {
Name string
Age int
Gender string
}
func TypeJudge(values ...interface{}) { // 参数是空接口类型的可变参数,可以接收任意个数的任意类型参数
for i := 0; i < len(values); i++ {
switch values[i].(type) {
case int:
fmt.Printf("第%d个参数: value = %v, type = int\n", i+1, values[i])
case float64:
fmt.Printf("第%d个参数: value = %v, type = float64\n", i+1, values[i])
case bool:
fmt.Printf("第%d个参数: value = %v, type = bool\n", i+1, values[i])
case Student:
fmt.Printf("第%d个参数: value = %v, type = Student\n", i+1, values[i])
case *Student:
fmt.Printf("第%d个参数: value = %v, type = *Student\n", i+1, values[i])
default:
fmt.Printf("第%d个参数: value = %v, type unknown\n", i+1, values[i])
}
}
}
func main() {
var stu = Student{"Alice", 12, "女"}
TypeJudge(10, 12.2, true, stu, &stu, "Hello World")
}
程序运行结果如下:
接口断言
接口断言
通过接口变量.(接口类型)
的方式,可以判断接口变量底层指向的类型是否实现了所指定的接口类型。如下:
package main
import "fmt"
type Usb interface {
Start()
Stop()
}
type Phone struct{}
// Phone实现Usb接口的所有方法
func (p Phone) Start() {
fmt.Println("phone start working...")
}
func (p Phone) Stop() {
fmt.Println("phone stop working...")
}
type Camera struct{}
// Camera只实现Usb接口的Start方法
func (c Camera) Start() {
fmt.Println("camera start working...")
}
func InterfaceAssert(value interface{}) {
if _, ok := value.(Usb); ok { // 接口断言
fmt.Printf("%T implemented the Usb interface...\n", value)
} else {
fmt.Printf("%T does not implemented the Usb interface...\n", value)
}
}
func main() {
phone := Phone{}
camera := Camera{}
InterfaceAssert(phone) // main.Phone implemented the Usb interface...
InterfaceAssert(camera) // main.Camera does not implemented the Usb interface...
}
注意:
- 只有接口变量才能进行类型断言和接口断言,不能直接通过
具体类型变量.(接口类型)
的方式判断某个类型是否实现了所指定的接口类型,这时应该通过interface{}(具体类型变量).(接口类型)
的方式先将具体类型变量转换为空接口类型,然后再进行接口断言。 - 接口断言与类型断言类似,
接口变量.(接口类型)
的第二个返回值表示本次接口断言是否成功,如果接口断言成功,则第一个返回值是转换为指定接口类型后的值,如果接口断言失败,则第一个值是所指定接口类型的零值,如果不对第二个返回值进行接收,那么断言失败时会触发panic异常。