Golang面向对象思想
简介
Golang 仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它 OOP 语言不一样;
没有封装、继承、多态这些概念,但同样通过别的方式实现这些特性;
封装:通过方法实现;
继承:通过匿名字段实现;
多态:通过接口实现;
- 抽象
把一类事物的共有的属性(字段)和行为(方法)提取出来,形成一个物理模型(结构体)。这种研究问题的方法称为抽象
封装
封装(encapsulation)就是把抽象出的字段和对字段的操作封装在一起,数据被保护在内部,程序的其它包只有通过被授权的操作(方法),才能对字段进行操作
- 封装的理解和好处
- 隐藏实现细节
- 提可以对数据进行验证,保证安全合理(Age)
- 如何体现封装
- 对结构体中的属性进行封装
- 通过方法,包 实现封装
- 封装的实现步骤
- 将结构体、字段(属性)的首字母小写(不能导出了,其它包不能使用,类似 private)
- 给结构体所在包提供一个工厂模式的函数,首字母大写。类似一个构造函数
- 提供一个首字母大写的 Set 方法(类似其它语言的 public),用于对属性判断并赋值
func (var 结构体类型名) SetXxx(参数列表) (返回值列表) {
//加入数据验证的业务逻辑
var.字段 = 参数
}
- 提供一个首字母大写的 Get 方法(类似其它语言的 public),用于获取属性的值
func (var 结构体类型名) GetXxx() {
return var.age;
}
- 特别说明:
在 Golang 开发中并没有特别强调封装,这点并不像 Java. 所以提醒学过 java 的朋友,不用总是用 java 的语法特性来看待 Golang, Golang 本身对面向对象的特性做了简化的.
继承
继承可以解决代码复用,让我们的编程更加靠近人类思维。
当多个结构体存在相同的属性(字段)和方法时,可以从这些结构体中抽象出结构体,在该结构体中定义这些相同的属性和方法。其它的结构体不需要重新定义这些属性(字段)和方法,只需嵌套一个定义相同属性和方法的匿名结构体即可。
在 Golang 中,如果一个 struct 嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现了继承特性。
- 嵌套匿名结构体的基本语法
type Goods struct {
Name string
Price int
}
type Book struct {
Goods
//这里就是嵌套匿名结构体 Goods
Writer string
}
- 继承给编程带来的便利
- 代码的复用性提高了
- 代码的扩展性和维护性提高了
- 深入细节
- 结构体可以使用嵌套匿名结构体所有的字段和方法,即:首字母大写或者小写的字段、方法,都可以使用。
- 匿名结构体字段访问可以简化
type A struct { Name string age int } func (a *A) SayOk() { fmt.Println("A SayOk", a.Name) } func (a *A) hello() { fmt.Println("A hello", a.Name) } type B struct { A } func main() { var b B <!-- b.A.Name = "tom" b.A.age = 19 b.A.SayOk() b.A.hello() --> //上面的写法可以简化 b.Name = "smith" b.age = 20 b.SayOk() b.hello() } 对上面的代码小结 1. 当我们直接通过 b 访问字段或方法时,其执行流程如下比如 b.Name 2. 编译器会先看 b 对应的类型有没有 Name, 如果有,则直接调用 B 类型的 Name 字段 3. 如果没有就去看 B 中嵌入的匿名结构体 A 有没有声明 Name 字段,如果有就调用,如果没有继续查找..如果都找不到就报错.
- 当结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访问原则访问,如希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分
type A struct {
Name string
age int
}
func (a *A) SayOk() {
fmt.Println("A SayOk", a.Name)
}
func (a *A) hello() {
fmt.Println("A hello", a.Name)
}
type B struct {
A
Name string
}
func (b *B) SayOk() {
fmt.Println("B SayOk", b.Name)
}
func main() {
var b B
b.Name = "jack" // ok
b.A.Name = "scott"
b.age = 100 //ok
b.SayOk() // B SayOk jack
b.A.SayOk() // A SayOk scott
b.hello() // A hello "scott"
}
- 结构体嵌入两个(或多个)匿名结构体,如两个匿名结构体有相同的字段和方法(同时结构体本身没有同名的字段和方法),在访问时,就必须明确指定匿名结构体名字,否则编译报错。
type A struct {
Name string
age int
}
type B struct {
Name string
Score float64
}
type C struct {
A
B
}
func main() {
var c C
//如果c 没有Name字段,而A 和 B有Name, 这时就必须通过指定匿名结构体名字来区分
//所以 c.Name 就会包编译错误, 这个规则对方法也是一样的!
//c.Name = "tom" error
c.A.Name = "tom"
}
- 如果一个 struct 嵌套了一个有名结构体,这种模式就是组合,如果是组合关系,那么在访问组合的结构体的字段或方法时,必须带上结构体的名字
type A struct {
Name string
age int
}
type B struct {
a A //有名结构体
}
func main() {
//如果B 中是一个有名结构体,则访问有名结构体的字段时,就必须带上有名结构体的名字
//比如 b.a.Name
var b B
b.a.Name = "jack"
}
- 嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值
type Goods struct {
Name string
Price float64
}
type Brand struct {
Name string
Address string
}
type TV struct {
Goods
Brand
}
type TV2 struct {
*Goods
*Brand
}
type Monster struct {
Name string
Age int
}
type E struct {
Monster
int
n int
}
func main() {
//类型一
//方式一
tv := TV{ Goods{"电视机001", 5000.99}, Brand{"海尔", "山东"}, }
//方式二
tv2 := TV{
Goods{
Price : 5000.99,
Name : "电视机002",
},
Brand{
Name : "夏普",
Address :"北京",
},
}
//类型二
//方式一
tv3 := TV2{ &Goods{"电视机003", 7000.99}, &Brand{"创维", "河南"}, }
//方式二
tv4 := TV2{
&Goods{
Name : "电视机004",
Price : 9000.99,
},
&Brand{
Name : "长虹",
Address : "四川",
},
}
}
- 匿名字段时基本数据类型的使用
type Monster struct {
Name string
Age int
}
type A struct {
Monster
int
n int
}
func main() {
var e E
e.Name = "狐狸精"
e.Age = 300
e.int = 20
e.n = 40
}
说明
1. 如果一个结构体有 int 类型的匿名字段,就不能第二个。
2. 如果需要有多个 int 的字段,则必须给 int 字段指定名字
- 多重继承
假如一个 struct 嵌套了多个匿名结构体,那么该结构体可以直接访问嵌套的匿名结构体的字段和方法,从而实现了多重继承。
type Goods struct {
Name string
Price float64
}
type Brand struct {
Name string
Address string
}
type TV struct {
Goods
Brand
}
func main() {
//演示访问Goods的Name
fmt.Println(tv.Goods.Name)
fmt.Println(tv.Price)
}
说明:
1. 如嵌入的匿名结构体有相同的字段名或者方法名,则在访问时,需要通过匿名结构体类型名来区分。
2. 为了保证代码的简洁性,建议大家尽量不使用多重继承
接口
interface 类型可以定义一组方法,但是这些不需要实现。并且 interface 不能包含任何变量。到某个自定义类型(比如结构体 Phone)要使用的时候,在根据具体情况把这些方法写出来(实现)。
- 声明
type 接口名 interface {
method1(参数列表)返回値列表
method2(参数列表)返回値列表
}
1. 接口里的所有方法都没有方法体,即接口的方法都是没有实现的方法。接口体现了程序设计的多态和高内聚低偶合的思想。
2. Golang 中的接口,不需要显式的实现。只要一个变量,含有接口类型中的所有方法,那么这个变量就实现这个接口。因此,Golang 中没有 implement 这样的关键字
- 注意事项和细节
- 接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量(实例)
- 接口中所有的方法都没有方法体,即都是没有实现的方法。
- 在 Golang 中,一个自定义类型需要将某个接口的所有方法都实现,我们说这个自定义类型实现了该接口。
- 一个自定义类型只有实现了某个接口,才能将该自定义类型的实例(变量)赋给接口类型
- 只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型。
- 一个自定义类型可以实现多个接口
- Golang 接口中不能有任何变量
- 一个接口(比如 A 接口)可以继承多个别的接口(比如 B,C 接口),这时如果要实现 A 接口,也必须将 B,C 接口的方法也全部实现。
- interface 类型默认是一个指针(引用类型),如果没有对 interface 初始化就使用,那么会输出 nil
- 空接口 interface{} 没有任何方法,所以所有类型都实现了空接口, 即我们可以把任何一个变量赋给空接口。
- 实现接口 vs 继承
- 实现接口可以看作是对 继承的一种补充
- 接口和继承解决的解决的问题不同
继承的价值主要在于:解决代码的复用性和可维护性。
接口的价值主要在于:设计,设计好各种规范(方法),让其它自定义类型去实现这些方法。 - 接口比继承更加灵活
接口比继承更加灵活,继承是满足 is - a 的关系,而接口只需满足 like - a 的关系。 - 接口在一定程度上实现代码解耦
多态
变量(实例)具有多种形态。面向对象的第三大特征,在 Go 语言,多态特征是通过接口实现的。可以按照统一的接口来调用不同的实现。这时接口变量就呈现不同的形态。
- 两种形式
- 多态参数
type Usb interface {
//声明了两个没有实现的方法
Start()
Stop()
}
type Phone struct {
}
func (p Phone) Start() {
fmt.Println("手机开始工作。。。")
}
func (p Phone) Stop() {
fmt.Println("手机停止工作。。。")
}
type Camera struct {
}
func (c Camera) Start() {
fmt.Println("相机开始工作~~~。。。")
}
func (c Camera) Stop() {
fmt.Println("相机停止工作。。。")
}
type Computer struct {
}
func (c Computer) Working(usb Usb) {
usb.Start()
usb.Stop()
}
func main() {
computer := Computer{}
phone := Phone{}
camera := Camera{}
computer.Working(phone)
computer.Working(camera) //
}
- 多态数组
type Usb interface {
Start()
Stop()
}
type Phone struct {
name string
}
func (p Phone) Start() {
fmt.Println("手机开始工作。。。")
}
func (p Phone) Stop() {
fmt.Println("手机停止工作。。。")
}
type Camera struct {
name string
}
func (c Camera) Start() {
fmt.Println("相机开始工作。。。")
}
func (c Camera) Stop() {
fmt.Println("相机停止工作。。。")
}
func main() {
var usbArr [3]Usb
usbArr[0] = Phone{"vivo"}
usbArr[1] = Phone{"小米"}
usbArr[2] = Camera{"尼康"}
fmt.Println(usbArr)
}
类型断言
类型断言,由于接口是一般类型,不知道具体类型,如果要转成具体类型,就需要使用类型断言
//类型断言的其它案例
var x interface{}
var b2 float32 = 1.1
x = b2 //空接口,可以接收任意类型
// x=>float32 [使用类型断言]
y := x.(float32)
fmt.Printf("y 的类型是 %T 值是=%v", y, y)
在进行类型断言时,如果类型不匹配,就会报 panic, 因此进行类型断言时,要确保原来的空接口指向的就是断言的类型.
//在进行断言时,带上检测机制,如果成功就 ok,否则也不要报 panic
//类型断言(带检测的)
var x interface{}
var b2 float32 = 2.1
x = b2 //空接口,可以接收任意类型
// x=>float32 [使用类型断言]
//类型断言(带检测的)
if y, ok := x.(float32); ok {
fmt.Println("convert success")
fmt.Printf("y 的类型是 %T 值是=%v", y, y)
} else {
fmt.Println("convert fail")
}
fmt.Println("继续执行...")