第十六章:Go语言面向对象编程
1. GO语言OOP概述
- Go语言不是纯粹的面向对象的语言,准确是描述是,Go语言支持面向对象编程的特性.
- Go语言中没有传统的面向对象编程语言的
class
,而Go语言中的struct
和 其他编程语言中的class
具有同等地位,也就是说Go语言是 基于struct
来实现OOP
的特性的- 面向对象编程在Go语言中的支持设计得很具有特点,Go语言放弃了很多面向对象编程的概念,如
继承
,重载
,构造函数
,析构函数
,隐藏this指针
等- Go语言可以实现面向对象编程的特性
封装
,继承
,多态
- Go语言面向对象编程的支持是
语言类型系统
中天然的组成部分,整个类型系统通过接口串联,灵活度高
面向对象编程OOP的三大基本特征是
封装
继承
和多态
Go语言支持面向对象编程,但是它实现面向对象的方式与其他语言不同,Go语言中没有private
,protected
,public
等对成员属性或者方法进行可见性描述的关键字 , 没有继承
这个字面的概念也不存在多继承
的概念等等,但是Go语言通过自身语言特点实现了对OOP的支持,关于这点我们看到在一些资料中提到了duck typing
的概念,When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck
一只鸟走路像鸭子,游泳像鸭子叫声也像鸭子,那我们就可以称它是鸭子通过对Go语言的学习,一些我们很模糊的概念都会清晰的
2. 封装的实现
封装是将抽象出来的字段或者属性和对字段属性的操作封装在一起,其他程序只能通过授权的方法才能对其进行操作,Go语言在开发中不是很强调封装的特性.无需纠结与其他语言不同的方面.
封装的实现
- 包内成员入定义的变量或者方法或者函数首字母小写,仅在包内可见,包外不可见
- 结构体,字段属性的的首字母小写决定其对外不可见
- 为结构体所在的包提供一个
构造函数
- 提供一些可以设置
私有属性的方法
对外可见- 提供一些可以获取属性的方法对外可见
示例
// 文件结构如下
|__model
|____person.go
|__main.go
person.go
package model
// 这是一`私有`person 类型结构体
// 包外不能直接生生成一个该类型的实例
// person 首字母小写包外不见
type person struct {
// 首字母大写保外可见
Name string
// 首字母小写保外不可见
age int8
Work string
deposit float64
hobbys []string
}
// 构造函数
// 包外可见
func NewPerson(name string, age int8, work string, deposit float64) *person {
return &person{
Name: name,
age: age,
Work: work,
deposit: deposit,
}
}
// 给person类型数据添加各种方法
func (p *person) GetAge() int8 {
return p.age
}
func (p person) GetDeposit() float64 {
return p.deposit
}
func (p *person) SetHobby(h string) {
p.hobbys = append(p.hobbys, h)
}
func (p person) GetHobbys() []string {
return p.hobbys
}
main.go
package main
import (
"GoNote/chapter6/demo18/model"
"fmt"
)
func main(){
// 定义一个结构体变量p1
p1 := model.NewPerson("tom",26,"UI",50000.01)
fmt.Println(p1)
// 只能访问可见属性, 成员属性age和deposit是访问不到的
fmt.Println(p1.Name,p1.Work)
// 通过可见方法访问不可见的属性
fmt.Println(p1.GetAge())
fmt.Println(p1.GetDeposit())
// 调用设置的方法
(*p1).SetHobby("跑步")
p1.SetHobby("爬山")
// 嗲用获取的方法
fmt.Println(p1.GetHobbys())
}
go run main.go
&{tom 26 UI 50000.01 []}
tom UI
26
50000.01
[跑步 爬山]
3. 继承的实现
Go语言的继承是通过组合的方式实现的.
- 结构体中内嵌了只有类型没有名称的结构体的时候我们称之为内嵌了
匿名结构体
- 结构体总内嵌的结构体既有名字也有类型,这种模式我们称为
组合
- Go语言的结构体内嵌特性就是一种组合,使用组合可以快速构建对象的不同属性
package main
import "fmt"
// 定义一个Good结构体
type Good struct {
name string
productDate string
price float64
}
// 定义一个Books结构体
type Books struct {
// 内嵌Good类型的结构体
Good
// Books自由的成员字段
classify string
publishing string
author string
}
// 定义Vendor结构体
type Vendor struct {
VendorName string
retailPrice float64
grade float32
}
// 定义ProgrammingBook 的结构体
type ProgrammingBook struct {
// 构成组合
// 此处类似于多继承
b Books
v Vendor
overview string
chapters []string
}
// 给Good类型绑定一个方法
func (g *Good) setGoodInfo(name string,pd string,price float64) {
g.name = name
g.productDate = pd
g.price = price
}
func (g *Good) GoodDescribe() string {
return fmt.Sprintf("商品名称是%s,价格是%.2f,生产日期是%s\n", g.name, g.price, g.productDate)
}
// 给Books绑定一个BasicInfo的方法
func (b *Books) booksDescribe() string {
return fmt.Sprintf("书的分类是%s,出版商是%s,作者是%s\n", b.classify, b.publishing, b.author)
}
func (p *ProgrammingBook) GetBook() {
//可以调用
ginfo := p.b.GoodDescribe()
binfo := p.b.booksDescribe()
fmt.Printf("商品信息 :%s 书籍信息 :%s 书籍概览 :%s",ginfo,binfo,p.overview)
}
func main() {
var gobook ProgrammingBook
// 结构体变量Gobook 内嵌了 Books类型结构体,Books结构体又和Good是组合的
//不是很严谨的说法:相当于ProgrammingBook 继承 Books ,Books继承了Good , 那么ProgrammingBook也继承Good的属性和方法
// 所以ProgrammingBook的实例 gobook能调用Good的方法
gobook.b.setGoodInfo("Go程序设计语言","2019-10-01",99.90)
// ProgrammingBook结构体内嵌了有名结构体Books,Vendor(组合),相当于继承了他们属性和方法
gobook.b.classify = "计算机|编程"
gobook.b.publishing = "机械工业出版社"
gobook.b.author = "艾伦 A.A.多诺万"
gobook.v.VendorName = "当当网"
gobook.v.retailPrice = 89.0
gobook.v.grade = 9.2
gobook.overview = "号称Go语言界的圣经"
gobook.GetBook()
}
go run main.go
商品信息 :商品名称是Go程序设计语言,价格是99.90,生产日期是2019-10-01
书籍信息 :书的分类是计算机|编程,出版商是机械工业出版社,作者是艾伦 A.A.多诺万
书籍概览 :号称Go语言界的圣经
对结构体组合的补充描述
- 无论匿名的内嵌的匿名结构体还是组合,各个结构体中有相同的结构体字段,那么访问遵循
就近原则
, 就近原则无效的时候,匿名内嵌的必须通过结构体类型访问该字段,组合的必须通过 结构体名(类似别名),去访问- 给不同的类型添加方法时方法名可以相同.前提是接收器不能相同
- 结构体内嵌多个结构体会出现
多继承
的特点- 出现多层级的内嵌结构体如 : A内嵌在B中,B内嵌在C中,C内嵌在D中,D内嵌在E中 ... 无论是想访问结构体属性(字段) 或者是调用方法,只需要正确的指向那个结构体,就可访问成员属性和调用方法
package main
import "fmt"
type AA struct {
name string
AAAddr string
}
type BB struct {
name string
BBAddr string
}
type DD struct {
name string
DDAddr string
}
type XX struct {
AA
name string
XXAddr string
}
type CC struct {
AA
BB
XX
d DD
name string
}
// 给AA结构体类型添加方法demo1
func (a *AA) demo1(){
fmt.Println(a.name)
}
// 给DD结构体类型添加方法demo1
func (d *DD) demo1(){
fmt.Println(d.name)
}
// 给CC结构体类型添加方法demo1
func (c *CC) demo1(){
fmt.Println(c.name)
}
func main(){
ins1 := new(CC)
// 此时访问的是CC结构体自己字段name(就近原则)
ins1.name = "Name-CC"
// CC 结构体内嵌了匿名结构体AA,CC,他们的name在逻辑山处于同一层级
// 要访问AA或者BB中的name 那就必须带上结构类型名
ins1.AA.name = "Name-AA"
// 多层级内嵌匿名结构体,访问方式也是逐层范文过去
ins1.XX.AA.name = "XX(Name-AA)"
// DD是CC有名结构体内嵌,或者叫组合
ins1.d.name = "Name-DD"
// 匿名结构体并且成员字段是唯一的,可以这样直接访问
ins1.BBAddr = "addr of BB"
// 组合类型的 有名内嵌那就必须带上名称
ins1.d.DDAddr = "addr of DD"
// 调用的是自己的方法 CC 结构体绑定的方法
ins1.demo1()
// 调用组合DD的demo1方法
ins1.d.demo1()
// 调用内嵌的匿名结构体AA的demo1方法
ins1.AA.demo1()
ins1.XX.demo1()
}
go run main.go
Name-CC
Name-DD
Name-AA
XX(Name-AA)
4. 多态的实现
多态
是指代码根据类型的具体实现采取不同行为的能力在Go语言中多态的特征是通过接口
interface
来体现的
4.1 接口概述
我们将
接口
看着一种双方约定的协议, 接口的实现者不用在意接口会被怎样使用,接口的调用者不用在关心接口内部的实现细节.Go语言中接口也是一种类型,用来定义行为,也是一种抽象结构.被定义的行为不是由接口直接实现,而是通过方法由用户定义的类型实现,用户定义的类型实现了接口类型声明的一组方法,那么该用户类型就实现了该接口,同时用户定义的类型变量就可以赋给该接口类型变量,该过程会将用户定义的类型变量存入接口类型变量中,接口变量对接口定义方法的调用会执行存入的用户定义类型变量对方法的调用,此时就的调用就是一种多态
Go语言中的接口设计是 非侵入式 的, 其具体变现特征是 : 接口的定义者无需知道接口被哪些类型实现了,而接口的实现者也不需要知道实现了哪些接口,无需指明已经实现了哪些接口,只需要关注自己实现的是什么样的接口即可.编译器会自己识别哪个类型实现哪些接口
侵入式 主要体现是实现接口的类需要很明确的声明自己实现了哪个接口
4.2 声明接口
声明接口的格式如下
- 关键字
type
和interface
- 接口类型名 是自定义的,命名方式和正常的变量名相同,为了严格起见通常会在接口类型名后面追加
er
- 函数名 和 接口类型名 的首字母大小写决定了接口和它定义的方法包外可见性
- 参数列表和返回值列表的定义和普通函数中的参数列表和返回值列表类似
- 接口中的参数列表和放回值列表中的变量名均可以忽略
- 接口中所有的方法都没有方法体
- 接口中不能定义任何变量
package main
type 接口类型名 interface {
函数名1(参数列表1) 返回值列表1
函数名2(参数列表2) 返回值列表2
函数名3(参数列表3) 返回值列表3
函数名4(参数列表4) 返回值列表4
函数名5(参数列表5) 返回值列表5
}
func main(){
}
package main
type Demoer interface {
func1(int,string) (int ,string)
func2(int,int) (int ,error)
func3(s1 ,s2 string) (s3 string ,err error)
func4()
}
func main(){
}
4.3 实现接口
接口的实现由两个基本原则
- 接口的方法和实现接口的类型的方法格式必须完全一致
- 接口中的所有方法都被实现了才能说接口被实现了
package main
import "fmt"
type Doer interface {
Music()
ShowTv()
}
// 实现了接口Doer
type MP4 struct {
}
func (m MP4) Music() {
fmt.Println("play music")
}
func (m MP4) ShowTv() {
fmt.Println("play Tv")
}
func main() {
var m = new(MP4)
m.Music()
m.ShowTv()
}
一个类型可以实现多个接口
一个接口可以被多个类型实现
package main
import "fmt"
// Volume类型 实现CommonFunc接口
type Volume int
type BasicFunc interface {
Start()
Stop()
}
// 接口 CommonFunc 被两种数据类型实现
// 多种数据类型可以实现相同的接口
type CommonFunc interface {
VolumeIncrease()
VolumeReduce()
}
// TV类型结构体实现了CommonFunc和BasicFunc接口
type TV struct {
}
func (v Volume) VolumeIncrease() {
fmt.Println("音量增加")
}
func (v Volume) VolumeReduce() {
fmt.Println("音量减少")
}
func (t TV) Start() {
fmt.Println("开机")
}
func (t TV) Stop() {
fmt.Println("关机")
}
func (t TV) VolumeIncrease() {
fmt.Println("音量增加")
}
func (t TV) VolumeReduce() {
fmt.Println("音量减少")
}
func main() {
}
4.4 接口嵌套
Go语言中,接口与接口之间可以通过嵌套创造出新的接口
嵌套产生的新接口要被实现那么它嵌套关联的接口都被实现才行
简单讲 : X接口 是可以继承多个接口 如A接口 B接口 ,如果想实现X接口,就必须实现A,B接口中的所有方法
package main
// A 接口被Demo类型事项
type A interface {
FuncA()
}
// B 接口被Demo类型实现
type B interface {
FuncB()
}
// x 接口被Demo类型实现
type X interface {
A
B
}
type Demo struct {
}
func (a Demo) FuncA() {
}
func (b Demo) FuncB() {
}
func main() {
}
4.5 类型断言的格式
类型断言的格式如下:
t := i.(T) // t表示转换之后的变量 // i表示接口变量 // T表示转换的目标类型
接口类型可以接受任何的的数据,但是还是不知道到时什么类型,所以需要使用类型断言
package main
import "fmt"
func main(){
// 定义接口类型变量 i
var i interface{}
// 定义浮点类型变量f
var f float64 = 98.90
// 因为i是空接口类型所以可以接受如何变量
i = f
x := i.(float64)
fmt.Printf("x type = %T,value = %0.2f",x,x)
}
go run main.go
x type = float64,value = 98.90
类型断言的如果不匹配就会报panic
这时就需要在类型断言的时候带上检测机制
基础格式如下
t,ok := i.(T) // 和上述的类型断言格式一样 // ok 表示类型匹配是否成功的的标识,ok的类型是bool, 值为true表示成功,false表示失败 ok这个变量名是惯例写法,变量名可以自定义
package main
import "fmt"
func main() {
// 定义接口类型变量 i
var i interface{}
// 定义浮点类型变量f
var f float64 = 98.90
// 因为i是空接口类型所以可以接受如何变量
i = f
x := i.(float64)
fmt.Printf("x type = %T,value = %0.2f\n", x, x)
y ,ok:= i.(float32)
fmt.Printf("ok type = %T,value = %v\n", ok, ok)
if ok{
fmt.Printf("x type = %T,value = %0.2f\n", y, y)
}else{
fmt.Println("类型不符合")
}
}
go run main.go
x type = float64,value = 98.90
ok type = bool,value = false
类型不符合
Go语言中的switch可以做的类型断言
基本格式 :
switch 任意类型变量.(type){ case 类型1: 处理逻辑1 case 类型2: 处理逻辑2 ... default: 处理逻辑n }
package main
import "fmt"
type Demo1 struct {
data map[string]string
}
type Demo2 struct {
}
func PrintType (v interface{}) {
switch v.(type) {
case int :
fmt.Println(v,"is int")
case string :
fmt.Println(v,"is string")
case bool :
fmt.Println(v ,"is bool")
case Demo1:
fmt.Println(v,"is Demo1")
case Demo2 :
fmt.Println(v,"is Demo2")
}
}
func main(){
PrintType(99)
PrintType("golang")
PrintType(true)
demo1 := Demo1{data: map[string]string{"name":"tom"}}
PrintType(demo1)
demo2 := Demo2{}
PrintType(demo2)
}
go run main.go
99 is int
golang is string
true is bool
{map[name:tom]} is Demo1
{} is Demo2
4.6 空接口类型 interface{}
空接口是接口类型的特殊形式, 空接 口没有任何方法,因此任何类型都无须实现空接口,从实现的角度看,任何值都满足这个接口的需求
- 空接口也是一种类型
- 空间接口可以接受任何值
package main
import "fmt"
// 声明一个interface{}类型变量any
var any interface{}
func main() {
// 将int型 数据99 赋值给any
any = 99
fmt.Printf("any type = %T, value = %v\n",any,any)
// 声明一个int型变量i
var i int
// 那么是不是可以将any直接赋值给i呢?
// 实际是不行的,因为any本质上是interface{}类型,而不是int型
// i = any
// 执行之后编译器会抛出
// cannot use any (type interface {}) as type int in assignment: need type assertion
// 所以我们需要用到类型断言,将interface{}类型转换成int类型赋值给i
i = any.(int)
fmt.Printf("i type = %T, value = %v\n",i,i)
any = "golang"
fmt.Printf("any type = %T, value = %v\n",any,any)
any = true
fmt.Printf("any type = %T, value = %v\n",any,any)
}
看一个使用interface{} 的例子
package main
import "fmt"
type Dict struct {
// data 作为map类型可以接受任意类型的键和值
data map[interface{}]interface{}
}
// 增加数据
func (d *Dict) addValue(key, value interface{}) {
d.data[key] = value
}
// 获取数据
func (d *Dict) GetValue(i interface{}) interface{} {
return d.data[i]
}
// 清空
func (d *Dict) DelAll() {
d.data = make(map[interface{}]interface{})
}
func (d *Dict) Visit(callback func(k,v interface{}) bool ) {
if callback == nil{
return
}
for key,value := range d.data{
if ! callback(key,value){
return
}
}
}
// 初始化
func NewDict() *Dict {
d := new(Dict)
d.DelAll()
return d
}
func main() {
d := NewDict()
d.addValue("name", "tom")
d.addValue("male", true)
name := d.GetValue("name")
fmt.Println(name)
// 访问
d.Visit(func(k, v interface{}) bool {
fmt.Println(k,v)
return true
})
}
go run main.go
tom
name tom
male true
4.7 如何实现多态
我们一直说在Go语言中多态是通过接口实现的,可以按照统一的接口,调用不同的实现,此时
接口变量
就呈现不同的形态
实现多态方法1:
把接口当做
参数
在方法或者函数中传递,根据传递进来的实际接口的不同实现,体现同一个接口不同的实现
package main
import (
"fmt"
)
// 定义一个接口USB,有两个方法
type USB interface {
start()
stop()
}
// 定义Phone的类型的结构体
type Phone struct {
name string
}
// Phoen类型实现接口USB
func (p Phone) start() {
fmt.Println(p.name, "start")
}
func (p Phone) stop() {
fmt.Println(p.name, "stop")
}
// 定义Pad类型的结构体
type Pad struct {
name string
}
// Pad实现接口USB
func (p Pad) start() {
fmt.Println(p.name, "start")
}
func (p Pad) stop() {
fmt.Println(p.name, "stop")
}
// 定义machine类型的结构体
type Machine struct{
}
// 给Machine绑定方法Work
// 传入的参数是USB类型,凡是实现了USB的变量都可以是参数
func (m Machine) Work(u USB){
u.start()
u.stop()
}
func main() {
var iphon Phone = Phone{"iphone"}
var ipad Pad = Pad{"ipad"}
// 实现接口的类型变量,可以赋值给接口变量
// 被赋值的接口变量,可以像类型变量一样调用接口方法
// 接口方法被具体实现的不一样,所有调用相同的方法,结果也是不一样的
var usb USB
// 将实例iPhone赋值给接口变量usb
usb = iphon
usb.start()
// 将实例iPad赋值给接口变量usb
usb = ipad
usb.start()
m := Machine{}
// iPhone Iphone 实现USB接口 所以iPhone可以是变量
// 因为USB接口的具体实现的不同,故同样的USB接口呈现了多态
m.Work(iphon)
m.Work(ipad)
}
go run main.go
iphone start
ipad start
iphone start
iphone stop
ipad start
ipad stop
实现多态方法1:
把接口当做数据类型,那么实现该接口的结构体(或者其他类型) 都是符合该接口类型
package main
import (
"fmt"
)
// 定义一个接口USB,有两个方法
type USB interface {
start()
stop()
}
// 定义Phone的类型的结构体
type Phone struct {
name string
}
// Phoen类型实现接口USB
func (p Phone) start() {
fmt.Println(p.name, "start")
}
func (p Phone) stop() {
fmt.Println(p.name, "stop")
}
// 定义Pad类型的结构体
type Pad struct {
name string
}
// Pad实现接口USB
func (p Pad) start() {
fmt.Println(p.name, "start")
}
func (p Pad) stop() {
fmt.Println(p.name, "stop")
}
func main() {
// 定义一个map变量iArr它的键是string类型,值是USB接口类型
// 那么只有实现了USB接口的数据才能符合
var iArr map[string]USB
iArr = make(map[string]USB)
// Phone结构体类型 实现了USB ,所以可以将Phone类型的结构体可以当做值赋给变量IArr
iArr["iPhone X"] = Phone{"iPhonex"}
// ipad结构体类型 实现了USB ,所以可以将ipad类型的结构体可以当做值赋给变量IArr
iArr["IPad2018"] = Pad{"Ipad2018款"}
// 调用方法
iArr["IPad2018"].start()
iArr["IPad2018"].stop()
iArr["P30"] = Phone{"华为P30"}
iArr["Mate20"] = Phone{"Mate 20 X"}
iArr["P30"].stop()
iArr["P30"].start()
}
go run main.go
Ipad2018款 start
Ipad2018款 stop
华为P30 stop
华为P30 start