Golang 面向对象编程
*目录
00 Golang语言面向对象编程说明
01 字段、属性
02 方法
03 面向对象编程
04 工厂模式
05 面向对象编程思想*
00 Golang语言面向对象编程说明
- Golang 也支持面向对象编程(OOP),但是和传统的面向对象编程有区别,并不是纯粹的面向对象语言。所以我们说Golang支持面向对象编程特性是比较准确的。
- Golang没有类(class),Go语言的结构体(struct)和其它编程语言的类(class)有同等的地位,你可以理解Golang是基于struct来实现OOP特性的。
- Golang面向对象编程非常简洁,去掉了传统OOP语言的继承、方法重载、构造函数和析构函数、隐藏的this指针等等
- Golang仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它OOP语言不一样,比如继承 :Golang没有extends关键字,继承是通过匿名字段来实现。
- Golang面向对象(OOP)很优雅,OOP本身就是语言类型系统(type system)的一部分,通过接口(interface)关联,耦合性低,也非常灵活。也就是说在Golang 中面向接口编程是非常重要的特性。
结构体和结构体变量(实例)的区别和联系
- 结构体是自定义的数据类型,代表一类事物
- 结构体变量(实例)是具体的,实际的,代表一个具体变量
如何声明结构体
type 结构体名称 struct {
field1 type
field2 type
...
}
定义Dog结构体
type Dog struct{
Name string
Age int64
Color string
}
01 字段、属性
- 从概念或叫法上看:结构体字段=属性= field
- 字段是结构体的一个组成部分,一般是基本数据类型、数组,也可是引用类型
注意事项和细节说明
- 字段声明语法同变量,示例:字段名字段类型
- 字段的类型可以为:基本类型、数组或引用类型
在创建一个结构体变量后,如果没有给字段赋值,都对应一个零值(默认值),规则同前面讲的一样:
- 布尔类型是 false ,数值是0 ,字符串是""
- 数组类型的默认值和它的元素类型相关,比如score [3]int 则为[0,0, 0]
指针,slice,和map 的零值都是nil ,即还没有分配空间
- 使用slice一定make :p1.slice1=make([]int,10)
- 使用map一定make :p1.map1=make(map[string]string)
- 不同结构体变量的字段是独立,互不影响,结构体是值类型。
创建结构体变量和访问结构体字段
//方式1 直接
var dd Dog
//TODO 逐一赋值
//方式2 {}
var dd Dog = Person{"小花",4,"red"}
//方式3 &
var dd *Dog = new(Dog)
(*dd).Name = "小花" //可以
dd.Name = "小花" //可以
//底层会对dd.Name = "小花"进行处理
//(会给dd加上取值运算(*dd).Name = "小花"
//方式4 &{}
var dd *Dog = &Dog{"小花",4,"red"}
var dd *Dog = &Dog{}
(*dd).Name = "小花" //可以
dd.Name = "小花" //可以
说明:
1)
第3种和第4种方式返回的是结构体指针。
2)
结构体指针访问字段的标准方式应该是:(*结构体指针).字段名,
比如(*person).Name = "tom"3)但go做了一个简化,
也支持结构体指针.字段名,
比如 person.Name = "tom"。
3)
go编译器底层对person.Name做了转(*person).Name.
结构体使用注意事项和细节
- 结构体的所有字段在内存中是连续的
- 结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段(名字、个数和类型)
- 结构体进行type重新定义(相当于取别名),Golang认为是新的数据类型,但是相互间可以强转
- struct 的每个字段上,可以写上一个tag,该tag 可以通过反射机制获取,常见的使用场景就是序列化和反序列化。
02 方法
Golang中的方法是作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是struct。
方法的声明和调用
type A struct {
Num int
}
func (a A) test(){
fmt.Println(a.Num)
}
说明
- func(a A) test(书表示A结构体有一方法,方法名为test
- (aA)体现 test方法是和A类型绑定的
//例子
package main
import(
"fmt"
)
type Person struct {
Name string
}
//给Person类型绑定一方法
func (P Person) test({
fmt.Println("test((name=",p.Name)
func main(){
var p Personp.Name = "tom”
p.test()//调用方法
}
//1) test方法和Person类型绑定
//2) test方法只能通过 Person类型的变量来调用,而不能直接调用,也不能使用其它类型变量来调用
//3) func (p Person) test()....p表示哪个Person变量调用,这个p就是它的副本,这点和函数传参常相似。
//4) p这个名字,有程序员指定,不是固定,比如修改成person也是可以
方法的调用和传参机制原理
- 在通过一个变量去调用方法时,其调用机制和函数一样
- 不一样的地方是,变量调用方法时,该变量本身也会作为一个参数传递到方法(如果变量是值类型,则进行值拷贝,如果变量是引用类型,则进行地址拷贝)
方法的注意事项和细节
- 结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式
- 如程序员希望在方法中,修改结构体变量的值,可以通过结构体指针的方式来处理
- Golang中的方法作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是struct,比如int , float32等都可以有方法
- 方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问,方法首字母大写,可以在本包和其它包访问。
- 如果一个类型实现了String()这个方法,那么fimt.PrintIn默认会调用这个变量的String()进行输出
类型绑定中值拷贝与地址拷贝
- 不管调用形式如何,真正决定是值拷贝还是地址拷贝,看这个方法是和哪个类型绑定.
- 如果是和值类型,比如(p Person),则是值拷贝,如果和指针类型,比如是(p*Person)则是地址拷贝。
- 地址拷贝可以对绑定类型中的属性进行修改
03 面向对象编程
步骤
- 声明(定义)结构体,确定结构体名
- 编写结构体的字段
- 编写结构体的方法
创建结构体变量时指定字段值
//方式1
var stu1 =Stu{”小明”,19}
var stu3 = Stu{
Name : "jack",
Age : 20,
}
//方式2
//返回结构体的指针类型
var stu5 *stu = &stu{"小王",29}
var stu7 =&stu{
Name :“小李”,
Age :49,
}
04 工厂模式
Golang 的结构体没有构造函数,通常可以使用工厂模式来解决这个问题。
结构体的Student 的首字母S是大写的,如果我们想在其它包创建Student的实例(比如main包)引入model包后,就可以直接创建Student结构体的变量(实例)。但是问题来了,如果首字母是小写的,比如是type student struct ,就不不行了。怎么办?工厂模式来解决。
使用工厂模式实现跨包创建结构体实例(变量)
type student struct{
Name string
score float64
)
//因为student结构体首字母是小写,因此是只能在model使用
//我们通过工厂模式来解决
fune Newstudent(n string, s float64) *student {
return &student{
Name : n,
Score : s,
}
05 面向对象编程思想
抽象
我们在前面去定义一个结构体时候,实际上就是把一类事物的共有的属性(字段)和行为(方法)提取出来,形成一个物理模型(结构体)。这种研究问题的方法称为抽象。
封装
Golang仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它OOP语言不一样。
封装(encapsulation)就是把抽象出的字段和对字段的操作封装在一起,数据被保护在内部,程序的其它包只有通过被授权的操作(方法),才能对字段进行操作。
封装的理解和好处
- 隐藏实现细节
- 提可以对数据进行验证,保证安全合理(Age)
如何体现封装
- 对结构体中的属性进行封装
- 通过方法,包实现封装
封装的实现步骤
- 将结构体、字段(属性)的首字母小写(不能导出了,其它包不能使用,类似private)
- 给结构体所在包提供一个工厂模式的函数,首字母大写。类似一个构造函数
- 提供一个首字母大写的Set方法(类似其它语言的public),用于对属性判断并赋值
- 提供一个首字母大写的Get方法(类似其它语言的public),用于获取属性的
特别说明:
在Golang开发中并没有特别强调封装,这点并不像Java, Golang本身对面向对象的特性做了简化的
type person struct {
Name string
age int //其它包不能直接访问.
}
//写一个工厂模式的函数,相当于构造函数
func NewPerson(name string) *person {
return &person{
Name : name,
}
}
//为了访问age,编写一对setXxx的方法和GetXxx的方法
func (p*person) setAge(age int) {
if age >0 && age <150 {
p.age = age
}else{
fmt.Print1n("年龄范围不正确..")
}
}
func (p*person) GetAge() int {
return p.age
}
继承
继承可以解决代码复用,让我们的编程更加靠近人类思维。
当多个结构体存在相同的属性(字段)和方法时,可以从这些结构体中抽象出结构体,在该结构体中定义这些相同的属性和方法。
嵌套匿名结构体的基本语法
type Goods struct {
Name stringPrice int
}
type Book struct {
Goods //这里就是嵌套匿名结构体Goods
Writer string
}
继承的深入讨论
- 结构体可以使用嵌套匿名结构体所有的字段和方法,即:首字母大写或者小写的字段、方法,都可以使用。
- 匿名结构体字段访问可以简化。
b.A.Name = "tom"
b.A.age= 19
b.A.Sayok()
b.A.hello()
//上面的写法可以简化
b.Name = "smith"
b.age = 20
b.sayok()
b.hello()
- 当结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访问原则访问,如希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分
b.A.age= 19
- 结构体嵌入两个(或多个)匿名结构体,如两个匿名结构体有相同的字段和方法(同时结构体本身没有同名的字段和方法),在访问时,就必须明确指定匿名结构体名字,否则编译报错。
- 如果一个struct 嵌套了一个有名结构体,这种模式就是组合,如果是组合关系,那么在访问组合的结构体的字段或方法时,必须带上结构体的名字。
type D struct i
aA //有名结构体
}
- 嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值
type Goods struct {
Name string
Price float64
}
type Brand struct {
Name string
Address string
}
type TV struct {
Goods
Brand
}
type TV2 struct {
*Goods
*Brand
}
//嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值
tv := TV{Goods{"电视机001",900.99}, Brand{"海尔","山东"},}
tv2 := TV{
Goods{
Price : 88.99,
Name :"电视机ee2"
},
Brand{
Name :"夏普",
Address :"北京",
},
}
tv3 := TV2{ &Goodsf{"电视机03",7000.99},&Brand{"创维","河南"}}
tv4 := TV2{
&Goods{
Name :"电视机4",
Price : 9eee.99,
}
&Brand{
Name :"长虹",
Address :"四川",
},
}
多重继承
如一个struct嵌套了多个匿名结构体,那么该结构体可以直接访问嵌套的匿名结构体的字段和方法,从而实现了多重继承。
为了保证代码的简洁性,建议尽量不使用多重继承
接口
在Golang中多态特性主要是通过接口来体现的
interface类型可以定义一组方法,但是这些不需要实现。并且 interface不能包含任何变量。到某个自定义类型要使用的时候,在根据具体情况把这些方法写出来(实现)。
type Usb interface {
//声明了两个没有实现的方法
Start()
Stop()
}
- 接口里的所有方法都没有方法体,即接口的方法都是没有实现的方法。接口体现了程序设计的多态和高内聚低偶合的思想。
- Golang 中的接口,不需要显式的实现。只要一个变量,含有接口类型中的所有方法,那么这个变量就实现这个接口。因此,Golang 中没有implement这样的关键字
注意事项和细节
- 接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量(实例)
- 接口中所有的方法都没有方法体,即都是没有实现的方法。
- 在Golang 中,一个自定义类型需要将某个接口的所有方法都实现,我们说这个自定义类型实现了该接口。
- 一个自定义类型只有实现了某个接口,才能将该自定义类型的实例(变量)赋给接口类型
- 只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型。
- 一个自定义类型可以实现多个接口
- Golang 接口中不能有任何变量
- 一个接口(比如A接口)可以继承多个别的接口(比如B,C接口),这时如果要实现A接口,也必须将B,C接口的方法也全部实现。
- interface类型默认是一个指针(引用类型),如果没有对interface初始化就使用,那么会输出nil
- 空接口 interface}没有任何方法,所以所有类型都实现了空接口,即我们可以把任何一个变量赋给空接口。
继承和接口的区别
- 当A结构体继承了B结构体,那么A结构就自动的继承了B结构体的字段和方法,并且可以直接使用
- 当A结构体需要扩展功能,同时不希望去破坏继承关系,则可以去实现某个接口即可,因此我们可以认为:实现接口是对继承机制的补充.
实现接口可以看作是对继承的一种补充
接口和继承解决的解决的问题不同
- 继承的价值主要在于:解决代码的复用性和可维护性。
- 接口的价值主要在于:设计,设计好各种规范(方法),让其它自定义类型去实现这些方法。
- 接口比继承更加灵活
接口比继承更加灵活,
- 继承是满足is - a 的关系
- 接口只需满足like - a的关系
- 接口在一定程度上实现代码解耦
多态
变量(实例)具有多种形态。面向对象的第三大特征,在Go语言,多态特征是通过接口实现的。可以按照统一的接口来调用不同的实现。这时接口变量就呈现不同的形态。
- 多态参数
- 多态数组
类型断言
类型断言,由于接口是一般类型,不知道具体类型,如果要转成具体类型,就需要使用类型断言
//类型断言
var x interface{}
var b2 float32 =1.1
x=b2 //空接口,可以接收任意类型
//x=>float32[使用类型断言]
y := x.(float32)
fmt.Printf("y的类型是灯值是=%v", y, y)
在进行类型断言时,如果类型不匹配,就会报 panic,因此进行类型断言时,要确保原来的空接口指向的就是断言的类型.
//类型断言(带检测的)
var x interface{}
var b2 float32 =2.1
x = b2//空接口,可以接收任意类型
//类型断言(带检测的)
if y, ok := x.(float32);ok {
fmt.Println(" convert success")
fmt.Printf("y的类型是%T值是=%v",y, y)
}else {
fmt.Print1n(" convert fai1")
}
fmt.Print1n("继续执行...")