【Golang】第八弹----面向对象编程
🔥 个人主页:星云爱编程
🔥 所属专栏:Golang
🌷追光的人,终会万丈光芒
🎉欢迎大家点赞👍评论📝收藏⭐文章
前言:Go语言面向对象编程说明
- Golang也支持面向对象编程(OOP),但是和传统的面向对象编程有区别,并不是纯粹的面向对象语言。所以说Golang支持面向对象编程特性是比较准确的。
- Golang 没有类(class),Go语言的结构体(struct)和其它编程语言的类(class)有同等的地位,你可以理解 Golang 是基于 struct 来实现 OOP 特性的。
- Golang 面向对象编程非常简洁,去掉了传统 OOP 语言的继承、方法重载、构造函数和析构函数、隐藏的 this 指针等等。
- Golang仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它OOP语言不一样,比如继承:Golang没有extends关键子,继承是通过匿名字段来实现。
- Golang面向对象(0OP)很优雅,OOP本身就是语言类型系统(type system)的一部分,通过接口(interface)关联,藕合性低,也非常灵活,在Golang中面向接口编程是非常重要的特性。
1、结构体
1.1基本介绍
- 结构体是自定义的数据类型,代表一类事物;
- 结构体变量(实例)是具体的、实际的,代表一类事物
1.2结构体声明
基本语法:
type 结构体名称 struct { 字段1 类型1 字段2 类型2 ... }
1.3字段/属性
1.3.1介绍
- 结构体字段=属性=field
- 字段是结构体的一个组成部分,一般是基本数据类型、数组,也可以是引用类型
1.3.2注意事项和使用细节
- 字段声明语法同变量,示例:字段名 字段类型
- 字段的类型可以为:基本类型、数组或引用类型
- 在创建一个结构体变量后,如果没有给字段赋值,都对应一个零值(默认值),规则同前面讲的一样:布尔类型是 false,数值是0,字符串是""。数组类型的默认值和它的元素类型相关,比如 score [3]int 则为[0,0, 0]指针,slice,和map的零值都是 nil,即还没有分配空间。
- 不同结构体变量的字段是独立,互不影响,一个结构体变量字段的更改,不影响另外一个。
1.4创建结构体变量和使用结构体字段
方式一:直接声明
var stu1 Stu stu1.name="jack" stu1.age=19
方式二:{}
stu2:=Stu{"tom",18}
方式三:&
var stu3 *Stu=new(Stu) stu3.name="kk" stu3.age=54
方式四:
var stu4 *Stu=&Stu{} stu4.name="xx" stu4.age=22 //方式四也可以直接赋值 var stu5 *Stu=&Stu{"bob",43}
说明:
- 方式三和方式四返回的是结构体指针
- 结构体指针访问字段的标准方式是:(*结构体指针).字段名,例如:
(*stu3).age=21
- Go对结构体指针的使用做了个简化,也支持结构体.字段名,例如方式三中的使用;Go编译器底层对stu3.age做了转化:(*stu3).age
1.5创建结构体变量时指定字段值
Go在创建结构体实例(变量)时,可以直接指定字段的值。有以下几种方式
方式一:创建变量时直接赋值
package main import ( "fmt" ) type Stu struct{ Name string Age int } func main(){ //方式一:在创建变量时直接赋值 var stu1 Stu=Stu{"jack",15} //简写 stu2:=Stu{"tom",13} //在创建结构体变量时,把字段名和字段值写在一起,这种写法,就不依赖字段的定义顺序 var stu3 Stu=Stu{ Name:"张飞", Age:43, } var stu4 Stu=Stu{ Age:23, Name:"李白", } //简写 stu5:=Stu{ Age:98, Name:"王林", } fmt.Println(stu1) fmt.Println(stu2) fmt.Println(stu3) fmt.Println(stu4) fmt.Println(stu5) }
方式二:返回结构体的指针类型
package main import ( "fmt" ) type Stu struct{ Name string Age int } func main(){ //方式二:返回结构体的指针类型 var stu1 *Stu=&Stu{"高启强",33} //简写 stu2:=&Stu{"祁同伟",44} //在创建结构体指针变量时,把字段名和字段值写在一起,这种写法,就不依赖字段的定义顺序 var stu3 *Stu= &Stu{ Age:21, Name:"李达康", } //简写 stu4:=&Stu{ Name:"沙瑞金", Age:43, } fmt.Println(*stu1) fmt.Println(*stu2) fmt.Println(stu3) fmt.Println(stu4) }
1.6结构体内存分配机制
在 Go 语言中, 结构体(struct) 的内存分配机制遵循以下规则:
1.连续内存分配
结构体的字段在内存中是连续分配的。每个字段按照定义的顺序依次存储在内存中。
type Point struct { X int Y int }
内存布局
| X (int) | Y (int) |
2.内存对齐
Go 语言会根据字段的类型进行内存对齐,以提高访问效率。
对齐规则如下:
- 字段的起始地址必须是其类型大小的整数倍。
- 结构体的总大小必须是其最大字段大小的整数倍
type Example struct { A int8 // 1 字节 B int32 // 4 字节 C int64 // 8 字节 }
内存布局:
| A (1) | padding (3) | B (4) | C (8) |
总大小为 16 字节(1 + 3 + 4 + 8)
3.空结构体
空结构体( struct{} )不占用内存空间,大小为 0。
type Empty struct{} fmt.Println(unsafe.Sizeof(Empty{})) // 输出 0
4.嵌套结构体
嵌套结构体的内存分配规则与普通结构体相同,字段按顺序连续存储。
type Inner struct { A int B int } type Outer struct { X int Y Inner }
内存布局:
| X (int) | Y.A (int) | Y.B (int) |
5.指针字段
指针字段占用固定大小(64 位系统为 8 字节,32 位系统为 4 字节),指向实际数据的内存地址。
type PointerExample struct { P *int }
内存布局:
| P (8 字节) |
6.内存分配优化
Go 编译器会对结构体的内存布局进行优化,以减少内存浪费。例如,将较小的字段放在一起,减少填充字节。
type Optimized struct { A int8 B int8 C int32 }
内存布局:
| A (1) | B (1) | padding (2) | C (4) |
总大小为 8 字节(1 + 1 + 2 + 4)。
总结:
- 结构体的字段在内存中连续分配。
- 内存对齐规则确保访问效率。
- 空结构体不占用内存。
- 嵌套结构体和指针字段的内存分配规则与普通结构体相同。
- 编译器会优化内存布局,减少填充字节。
1.7注意事项和使用细节
(1)结构体是用户单独定义的类型,和其他类型进行转化时需要有完全相同的字段(名字、个数和类型);
例如:
package main import ( "fmt" ) type A struct{ Num int } type B struct{ Num int } func main(){ var a A var b B a=A(b)//字段必须完全一样才能转化,而且是强转 fmt.Println(a,b) }
(2)结构体进行type重新定义(想当于取别名),但Go认为是新的数据类型,不过相互之间可以转换
package main import( "fmt" ) type Student struct{ Name string Age int } type Stu Student//取别名 func main(){ var s1 Student var s2 Stu s1=Student(s2)//可以进行转为,不过必须为强转 fmt.Println(s1,s2) }
(3)struct的每一个字段上,可以写上一个tag,该tag可以通过反射机制获取,常见的使用场景就是序列化和反序列化:
package main import ( "fmt" "encoding/json" ) type Person struct{ Name string `json:"name"` Age int `json:"age"` Sex string `json:"sex"` } func main(){ p1:=Person{"子玉",32,"男"} // JSON 序列化 jsonStr, err := json.Marshal(p1) if err!=nil{ fmt.Println("json处理错误:",err) } fmt.Println("jsonStr",string(jsonStr)) }
2、方法
2.1基本介绍
在 Go 语言中, 方法(Method)是与特定类型(通常是结构体)关联的函数。方法通过接收者(receiver)绑定到类型,使得该类型的实例可以调用这些方法。
注意:Go中的方法是作用在指定的数据类型上(即和指定的数据类型绑定),因此自定义类型都可以有方法,不仅仅是struct。
2.2方法的定义
func (接收者 接收者类型) 方法名(参数列表) 返回值类型 { // 方法体 }
例子:
type Rectangle struct { width, height float64 } // 为 Rectangle 定义方法 func (r Rectangle) Area() float64 { return r.width * r.height }
说明:
- func(r Rectangle)Area() float64{ }表示Rectangle结构体有一方法,方法名为Area
- (r Rectangle)体现Area方法是和Rectangle类型绑定的
- Area方法只能通过Rectangle类型的变量来调用,而不能直接调用,也不能使用其他类型变量来调用
- func(r Rectangle)Area() float64{ }...r表示Rectangle变量调用,这个r就是它的副本,这点和函数传参非常相似
- r为形参名,我们可以随意更改,Rectangle为类型名,不能更改。
2.3方法的调用
实例.方法名(参数)
案例:
func main() { r := Rectangle{width: 10, height: 5} fmt.Println("面积:", r.Area()) c := Circle{radius: 5} c.Scale(2) fmt.Println("缩放后的半径:", c.radius) }
2.4方法传参机制
在 Go 语言中, 方法的传参机制 主要与方法的接收者类型有关,分为 值接收者 和 指针接收者 两种方式。
1.值接收者
当方法的接收者是值类型时,方法操作的是接收者的副本,对副本的修改不会影响原始值
例子:
type Rectangle struct { width, height float64 } // 值接收者方法 func (r Rectangle) Scale(factor float64) { r.width *= factor r.height *= factor } func main() { r := Rectangle{width: 10, height: 5} r.Scale(2) fmt.Println(r) // 输出: {10 5},原始值未改变 }
说明:
- 方法操作的是接收者的副本。
- 对副本的修改不会影响原始值。
- 适用于不需要修改原始值的场景。
2.指针接收者
当方法的接收者是指针类型时,方法操作的是接收者的原始值,对值的修改会影响原始值。
例子:
type Rectangle struct { width, height float64 } // 指针接收者方法 func (r *Rectangle) Scale(factor float64) { r.width *= factor r.height *= factor } func main() { r := Rectangle{width: 10, height: 5} r.Scale(2) fmt.Println(r) // 输出: {20 10},原始值被修改 }
说明:
- 方法操作的是接收者的原始值。
- 对值的修改会影响原始值。
- 适用于需要修改原始值的场景。
3.方法传参的底层机制
- 值接收者 :Go 语言会将接收者的值复制一份,传递给方法。
- 指针接收者 :Go 语言会将接收者的地址传递给方法,方法通过指针直接操作原始值。
4.方法的隐式转换
Go 语言会自动处理值接收者和指针接收者之间的调用:
- 如果方法使用值接收者,可以通过值或指针调用。
- 如果方法使用指针接收者,可以通过值或指针调用。
例子:
type Rectangle struct { width, height float64 } func (r Rectangle) Area() float64 { return r.width * r.height } func (r *Rectangle) Scale(factor float64) { r.width *= factor r.height *= factor } func main() { r := Rectangle{width: 10, height: 5} // 值接收者方法可以通过值或指针调用 fmt.Println(r.Area()) fmt.Println((&r).Area()) // 指针接收者方法可以通过值或指针调用 r.Scale(2) (&r).Scale(2) }
2.5方法和函数的区别
(1)调用方式不一样
函数的调用方式:函数名(实参列表)
方法的调用方式:变量.方法名(实参列表)
(2)对于普通函数,接受者为值类型时,不能将指针类型的数据直接传递;
接受者为指针类型时,不能将值类型的数据直接传递;
(3)对于方法(如 struct 的方法),接受者为值类型时,可以直接使用指针类型的变量调用方法;接受者为指针类型时,也可以直接使用值类型的变量调用方法。
package main import ( "fmt" ) type Person2 struct{ Name string } func test01(p Person2){ fmt.Println("test01().name=",p.Name) } func test02(p *Person2){ fmt.Println("test02().name=",p.Name) } func (p Person2)test03(){ p.Name="尤川" fmt.Println("test03().name=",p.Name) } func (p *Person2)test04(){ p.Name="雪儿" fmt.Println("test04().name=",p.Name) } func main(){ p1:=Person2{"蚩梦"} fmt.Println("main().name=",p1.Name) //函数使用 test01(p1)//test01()只能接受值类型 test02(&p1)//test02()只能接受指针类型 //方法使用 p1.test03()//test03()接受者为值类型,传参实质为值拷贝,故不影响实参结果 fmt.Println("main().name=",p1.Name) (&p1).test03()//指针类型也能调用test03()方法,不过传参实质仍为值拷贝,也不影响实参结果 fmt.Println("main().name=",p1.Name) //接受者为指针类型的方法 (&p1).test04()//test04()接受者为指针类型,传参实质为地址拷贝,会影响实参 fmt.Println("main().name=",p1.Name) p1.test04()//test04()也能直接被值类型调用,也能做到影响实参 fmt.Println("main().name=",p1.Name) }
总结:
(1)不管调用形式如何,真正决定是值拷贝还是地址拷贝,要看该方法是和哪个类型绑定;
(2)如果是和值类型,如(p Person),则是值拷贝,如果和指针类型,如(p *Person)则是地址拷贝。
3.面向对象编程三大特性
Go 语言虽然不是纯粹的面向对象编程语言,但它通过一些特性支持面向对象编程(OOP)的核心概念
3.1封装介绍
在 Go 语言中, 封装(Encapsulation)是面向对象编程的核心概念之一,主要通过 结构体(struct) 和 方法(method) 来实现。封装的核心思想是将数据(字段)和行为(方法)绑定在一起,并控制对数据的访问权限。
3.1.1封装的理解和好处
- 隐藏实现细节
- 提可以对数据进行验证,保证安全合理(Age)
3.1.2封装的实现及步骤
Go 语言通过以下方式实现封装:
- 结构体 :用于封装数据。
- 方法 :用于封装行为。
- 访问控制 :通过字段和方法名的首字母大小写控制访问权限。
封装实现步骤:
(1)将结构体、字段(属性)的首字母小写(不能导出了,其它包不能使用,类似private)
(2)给结构体所在包提供一个工厂模式的函数,首字母大写。类似一个构造函数
(3)提供一个首字母大写的set方法(类似其它语言的public),用于对属性判断并赋值
func (var 结构体类型名) SetXxx(参数列表)(返回值列表){ //加入数据验证的业务逻辑 var.字段=参数 }
(4)提供一个首字母大写的Get方法(类似其它语言的public),用于获取属性的值
func (var 结构体类型名)GetXxx() 返回类型{ return var.字段 }
3.1.3访问控制
- 公开字段和方法 :首字母大写的字段和方法可以被外部包访问。
- 私有字段和方法 :首字母小写的字段和方法只能在当前包内访问。
package main import "fmt" // 定义一个结构体 type Person struct { Name string // 公开字段 age int // 私有字段 } // 公开方法 func (p Person) Introduce() { fmt.Printf("大家好,我叫 %s,今年 %d 岁。\n", p.Name, p.age) } // 私有方法 func (p Person) getAge() int { return p.age } func main() { p := Person{Name: "张三", age: 25} p.Introduce() // 可以调用公开方法 // p.getAge() // 无法调用私有方法 }
3.1.4封装的优点
- 数据保护 :通过私有字段和方法,防止外部直接访问和修改数据。
- 代码复用 :将数据和行为封装在一起,便于复用。
- 易于维护 :封装后的代码结构清晰,易于维护和扩展。
3.1.5工厂模式(相当于构造器)
Go的结构体没有构造函数,通常可以使用工厂模式来解决这个问题。
场景一:
student结构体首字母小写,只能在本包使用,现需student结构体在其他包也能使用,则可通过工厂模式来解决
type student struct { Name string Age int Score float64 } func NewStudent(name string, age int, score float64) *student { return &student{ Name: name, Age: age, Score: score, } }
场景二:
若name字段首字母小写,则在其他包不能直接使用,我们可以提供一个方法来解决
func (s *student)GetName()string{ return s.name }
3.1.6封装完整案例
package main import "fmt" // 定义一个银行账户结构体 type Account struct { owner string // 账户所有者(私有字段) balance float64 // 账户余额(私有字段) } // 公开方法:创建账户 func NewAccount(owner string) *Account { return &Account{owner: owner, balance: 0} } // 公开方法:存款 func (a *Account) Deposit(amount float64) { if amount > 0 { a.balance += amount } } // 公开方法:取款 func (a *Account) Withdraw(amount float64) bool { if amount > 0 && a.balance >= amount { a.balance -= amount return true } return false } // 公开方法:获取余额 func (a *Account) Balance() float64 { return a.balance } func main() { account := NewAccount("张三") account.Deposit(1000) fmt.Println("当前余额:", account.Balance()) if account.Withdraw(500) { fmt.Println("取款成功,当前余额:", account.Balance()) } else { fmt.Println("取款失败,余额不足") } }
3.2继承
3.2.1继承介绍
- 继承可以解决代码复用,让我们的编程更加靠近人类思维。
- 当多个结构体存在相同的属性(字段)和方法时,可以从这些结构体中抽象出结构体,在该结构体中定义这些相同的属性和方法。
- 其它的结构体不需要重新定义这些属性和方法,只需嵌套一个匿名结构体即可。
- 在Golang中,如果一个struct嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现了继承特性。
3.2.2嵌套匿名结构体的基本语法
type People struct{ Name string Age int } type Children struct{ People//这里就是嵌套匿名结构体 }
3.2.3继承实现案例
package main import "fmt" // 父结构体 type Animal struct { Name string } // 父结构体的方法 func (a Animal) Speak() { fmt.Println("我是动物,我叫", a.Name) } // 子结构体 type Dog struct { Animal // 嵌入父结构体 Breed string } func main() { d := Dog{ Animal: Animal{Name: "旺财"}, // 初始化父结构体 Breed: "金毛", } d.Speak() // 调用父结构体的方法 fmt.Println("品种:", d.Breed) }
方法重写:
package main import "fmt" type Animal struct { Name string } func (a Animal) Speak() { fmt.Println("我是动物,我叫", a.Name) } type Dog struct { Animal Breed string } // 重写父结构体的方法 func (d Dog) Speak() { fmt.Println("汪汪汪,我是", d.Name) } func main() { d := Dog{ Animal: Animal{Name: "旺财"}, Breed: "金毛", } d.Speak() // 调用子结构体重写的方法 }
3.2.4继承注意事项和使用细节
(1)结构体可以使用嵌套匿名结构体所有的字段和方法,即:首字母大写或者小写的字段、方法,都可以使用。
(2)匿名结构体字段访问可以简化
package main import ( "fmt" ) type A struct{ Name string Age int } func (a *A)testA1(){ fmt.Println("testA1()~") } func (a *A)testA2(){ fmt.Println("testA2()....") } type B struct{ A } func main(){ var b B b.A.Name="张三" b.A.Age=32 b.A.testA1() b.A.testA2() //上面的代码可简化为 b.Name="李四" b.Age=76 b.testA1() b.testA2() }
对上面的代码说明:
- 当我们直接通过b访问字段或方法时,其执行流程如下比如 b.Name
- 编译器会先看b对应的类型有没有 Name.如果有,则直接调用 B类型的 Name 字段
- 如果没有就去看 B 中嵌入的匿名结构体 , 有没有声明 Name 字段,如果有就调用,如果没有继续查找.如果都找不到就报错。
(3)当结构体和匿名结构体有相同的字段或者方法时编译器采用就近访问原则访问,如希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分。
(4)结构体嵌入两个(或多个)匿名结构体,如两个匿名结构体有相同的字段和方法(同时结构体本身没有同名的字段和方法),在访问时,就必须明确指定名结构体名字,否则编译报错。
(5)如果一个struct嵌套了一个有名结构体,这种模式就是组合,如果是组合关系,那么在访问组合的结构体的字段或方式时,必须带上结构体的名字
package main import ( "fmt" ) type A struct{ Name string Age int } type B struct{ Name string Age int } type C struct{ A B } type D struct{ a A//有名结构体 Age int } func main(){ var c C c.A.Name="小三" //结构体嵌入两个(或多个)匿名结构体,如两个匿名结构体有相同的字段和方法(同时结构体本身没有同名的字段和方法), // 在访问时,就必须明确指定名结构体名字,否则编译报错。 fmt.Println(c) //组合关系 var d D d.a.Name="赵四" //如果一个struct嵌套了一个有名结构体,这种模式就是组合, //如果是组合关系,那么在访问组合的结构体的字段或方式时,必须带上结构体的名字 d.Age=21//D结构体有Age属性则可以直接访问 fmt.Println(d) }
(6)嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值。
package main import "fmt" type A struct{ Name string Age int } type C struct{ Name string Age int } type B struct{ A C } func main(){ var b B=B{A{"困了",55},C{"睡觉",555}} fmt.Println(b) }
(7)若结构体的匿名字段是基本数据类型:
- 一个结构体有int类型的匿名字段,就不能有第二个
- 如果需要有多个int的字段,则必须给int字段指定名字
3.2.5多重继承
如果一个struct嵌套了多个匿名结构体,那么该结构体可以直接访问嵌套的匿名结构体的字段和方法,从而实现了多重继承。
多重继承细节:
- 如嵌入的匿名结构体有相同的字段名或者方法名,则在访问时,需要通过匿名结构体类型名来区分。
- 为了保证代码的简洁性,建议大家尽量不使用多重继承
3.3接口
3.3.1接口介绍
接口(interface)类型可以定义一组方法,但是这些不需要实现。并且interface不能包含任何变量。到某个自定义类型(比如结构体Phone)要使用的时候,在根据具体情况把这些方法写出来。
3.3.2基本语法
type 接口名 interface { 方法名1(参数列表) 返回值类型 方法名2(参数列表) 返回值类型 ... }
说明:
- 接口里的所有方法都没有方法体,即接口的方法都是没有实现的方法。接口体现了程序设计的多态和高内聚低偶合的思想。
- Golang中的接口,不需要显式的实现。只要一个变量,含有接口类型中的所有方法,那么这个变量就实现这个接口。因此,Golang中没有implement这样的关键字
案例:
package main import ( "fmt" ) type Usb interface{ run() stop() } type Phone struct{ Usb } func (p Phone)run(){ fmt.Println("手机开始运行...") } func (p Phone)stop(){ fmt.Println("手机停止运行...") } type Computer struct{ Usb } //只要是实现了Usb接口(所谓实现Usb接口,就是指实现了Usb接口声明所有方法) func (c Computer)run(){ fmt.Println("电脑开始运行...") } func (c Computer)stop(){ fmt.Println("电脑停止运行...") } func (p Phone)working(){ p.run() p.stop() } func (c Computer)working(){ c.run() c.stop() } func main(){ var p Phone=Phone{} var c Computer=Computer{} p.working() c.working() }
3.3.3接口注意事项和细节
- 接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量(实例)
- 接口中所有的方法都没有方法体,即都是没有实现的方法。
- 在Golang中,一个自定义类型需要将某个接口的所有方法都实现,我们说这个自定义类型实现了该接口。
- 一个自定义类型只有实现了某个接口,才能将该自定义类型的实例(变量)赋给接口类型。
- 只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型,
- 一个自定义类型可以实现多个接口Golang
- 接口中不能有任何变量
- 一个接口(比如A接口)可以继承多个别的接口(比如B,C接口),这时如果要实现A接口,也必须将B,C接口的方法也全部实现。
- interface类型默认是一个指针(引用类型),如果没有对interface初始化就使用那么会输出nil
- 空接口interface{}没有任何方法,所以所有类型都实现了空接口
对应案例:
package main import "fmt" // 1. 接口不能创建实例,但可以指向实现了该接口的自定义类型的变量 type Speaker interface { Speak() } type Dog struct { Name string } func (d Dog) Speak() { fmt.Println(d.Name, "汪汪汪") } // 2. 接口中的方法没有方法体 type Runner interface { Run() } type Cat struct { Name string } func (c Cat) Speak() { fmt.Println(c.Name, "喵喵喵") } // 3. 自定义类型需要实现接口的所有方法 type Animal interface { Speaker Runner } func (d Dog) Run() { fmt.Println(d.Name, "在跑步") } // 4. 自定义类型可以实现多个接口 type Bird struct { Name string } func (b Bird) Speak() { fmt.Println(b.Name, "叽叽喳喳") } func (b Bird) Run() { fmt.Println(b.Name, "在飞翔") } // 5. 接口中不能有任何变量 type Swimmer interface { Swim() } // 6. 接口可以继承多个接口 type SuperAnimal interface { Animal Swimmer } type Fish struct { Name string } func (f Fish) Speak() { fmt.Println(f.Name, "咕噜咕噜") } func (f Fish) Run() { fmt.Println(f.Name, "在游泳") } func (f Fish) Swim() { fmt.Println(f.Name, "在潜水") } // 7. interface类型默认是一个指针 func checkInterface() { var s Speaker fmt.Println("未初始化的接口变量:", s) // 输出 nil } // 8. 空接口 func describe(i interface{}) { fmt.Printf("类型: %T, 值: %v\n", i, i) } func main() { // 1. 接口不能创建实例,但可以指向实现了该接口的自定义类型的变量 var s Speaker s = Dog{Name: "旺财"} s.Speak() // 2. 接口中的方法没有方法体 var c Cat = Cat{Name: "咪咪"} c.Speak() // 3. 自定义类型需要实现接口的所有方法 var a Animal = Dog{Name: "旺财"} a.Speak() a.Run() // 4. 自定义类型可以实现多个接口 var b Bird = Bird{Name: "小鸟"} b.Speak() b.Run() // 5. 接口中不能有任何变量 // 6. 接口可以继承多个接口 var sa SuperAnimal = Fish{Name: "小鱼"} sa.Speak() sa.Run() sa.Swim() // 7. interface类型默认是一个指针 checkInterface() // 8. 空接口 describe(42) describe("Hello") describe(3.14) }
3.3.4接口最佳实践
实现对Hero结构体切片的排序:sort.Sort(data Interface)
说明:
- Len() 方法 :返回切片的长度。
- Less() 方法 :定义了排序规则,升序或降序。
- Swap() 方法 :交换切片中的两个元素
package main import ( "fmt" "sort" ) // 定义 Hero 结构体 type Hero struct { Name string Age int Score float64 } // 定义 HeroSlice 类型,用于实现 sort.Interface type HeroSlice []Hero // 实现 Len 方法 func (hs HeroSlice) Len() int { return len(hs) } // 实现 Less 方法,按 Score 排序 func (hs HeroSlice) Less(i, j int) bool { return hs[i].Score
3.3.5实现接口VS继承
(1)接口和继承解决的问题不同
- 继承的价值主要在于:解决代码的复用性和可维护性。
- 接口的价值主要在于:设计,设计好各种规范(方法),让其它自定义类型去实现这些方法。
(2)接口比继承更加灵活
- 接口比继承更加灵活,继承是满足 is- a的关系,而接口只需满足 like - a的关系。
(3)接口在一定程度上实现代码解耦
3.4多态
3.4.1多态介绍
变量(实例)具有多种形态。面向对象的第三大特征,在Go语言,多态特征是通过接口实现的。可以按照统一的接口来调用不同的实现。这时接口变量就呈现不同的形态。
3.4.2接口体现多态特性
1.多态参数
在前面的Usb接口案例,Usb usb,即可以接收手机变量,又可以接收电脑变量,就体现了Usb 接口多态
2.多态数组
一个数组可以存储不同类型的对象,只要这些对象都实现了相同的接口,这个数组就是多态数组
案例:
package main import "fmt" // 定义接口 type Speaker interface { Speak() } // 定义 Dog 类型 type Dog struct { Name string } func (d Dog) Speak() { fmt.Println(d.Name, "汪汪汪") } // 定义 Cat 类型 type Cat struct { Name string } func (c Cat) Speak() { fmt.Println(c.Name, "喵喵喵") } // 定义 Bird 类型 type Bird struct { Name string } func (b Bird) Speak() { fmt.Println(b.Name, "叽叽喳喳") } func main() { // 创建多态数组 var speakers []Speaker // 添加不同类型的对象 speakers = append(speakers, Dog{Name: "旺财"}) speakers = append(speakers, Cat{Name: "咪咪"}) speakers = append(speakers, Bird{Name: "小鸟"}) // 遍历多态数组,调用 Speak 方法 for _, speaker := range speakers { speaker.Speak() } }
3.5类型断言
3.5.1基本介绍
Go 语言中, 类型断言 用于从接口中提取具体类型的值。它通常用于判断接口变量中存储的具体类型,并将其转换为该类型
3.5.2基本语法
值, ok := 接口变量.(具体类型)
说明:
- 接口变量 :需要断言的接口变量。
- 具体类型 :希望转换的目标类型。
- 值 :如果断言成功,返回转换后的值。
- ok :布尔值,表示断言是否成功。
案例:
package main import "fmt" func main() { var i interface{} = "Hello" // 类型断言 s, ok := i.(string) if ok { fmt.Println("断言成功:", s) } else { fmt.Println("断言失败") } // 错误的类型断言 f, ok := i.(float64) if ok { fmt.Println("断言成功:", f) } else { fmt.Println("断言失败") } }
3.5.3类型断言的应用
- 处理空接口 :当使用空接口 interface{} 存储任意类型的数据时,需要通过类型断言提取具体类型的值。
- 类型判断 :在需要判断接口变量中存储的具体类型时,使用类型断言。
- 多态处理 :在处理多态数组或接口变量时,可能需要根据具体类型执行不同的逻辑。
案例1:使用 ok 模式进行安全断言
package main import "fmt" func main() { var i interface{} = "Hello" // 安全断言 s, ok := i.(string) if ok { fmt.Println("断言成功:", s) } else { fmt.Println("断言失败") } }
案例2:处理多种类型
说明:在需要处理多种类型时,可以使用 switch 语句结合类型断言,使代码更简洁和易读。
package main import "fmt" func checkType(i interface{}) { switch v := i.(type) { case string: fmt.Println("字符串:", v) case int: fmt.Println("整数:", v) case float64: fmt.Println("浮点数:", v) default: fmt.Println("未知类型") } } func main() { checkType("Hello") checkType(42) checkType(3.14) checkType(true) }
案例3:处理空接口
说明:在处理空接口 interface{} 时,类型断言是提取具体类型值的唯一方式。
package main import "fmt" func main() { var i interface{} = 42 // 处理空接口 if v, ok := i.(int); ok { fmt.Println("整数值:", v) } else { fmt.Println("不是整数") } }
案例4:类型断言与多态结合
在处理空接口 interface{} 时,类型断言是提取具体类型值的唯一方式。
package main import "fmt" type Speaker interface { Speak() } type Dog struct { Name string } func (d Dog) Speak() { fmt.Println(d.Name, "汪汪汪") } type Cat struct { Name string } func (c Cat) Speak() { fmt.Println(c.Name, "喵喵喵") } func main() { var speakers []Speaker = []Speaker{Dog{Name: "旺财"}, Cat{Name: "咪咪"}} for _, speaker := range speakers { if dog, ok := speaker.(Dog); ok { fmt.Println("这是一只狗:", dog.Name) } else if cat, ok := speaker.(Cat); ok { fmt.Println("这是一只猫:", cat.Name) } speaker.Speak() } }
结语
感谢您的耐心阅读,希望这篇博客能够为您带来新的视角和启发。如果您觉得内容有价值,不妨动动手指,给个赞👍,让更多的朋友看到。同时,点击关注🔔,不错过我们的每一次精彩分享。若想随时回顾这些知识点,别忘了收藏⭐,让知识触手可及。您的支持是我们前进的动力,期待与您在下一次分享中相遇!
路漫漫其修远兮,吾将上下而求索。
- 接口比继承更加灵活,继承是满足 is- a的关系,而接口只需满足 like - a的关系。
- Go对结构体指针的使用做了个简化,也支持结构体.字段名,例如方式三中的使用;Go编译器底层对stu3.age做了转化:(*stu3).age