面向对象的三大特性
封装
介绍
封装(encapsulation)就是把抽象出的字段与对字段的操作封装在一起,数据被保护在内部,程序的其他包只有通过被授权的操作(方法),才能对字段进行操作。
举例:对电视机的操作就是典型的封装
理解与优点
- 隐藏实现的细节
- 对数据进行验证,保证安全合理
如何体现封装
- 对结构体中的属性进行封装
- 通过方法,包,实现封装
封装的实现步骤
- 将结构体,字段(属性)的首字母小写(不能导出了,其他包不能使用,类似private)
- 给结构体所在的包提供一个工厂模式的函数,首字母大写。类似一个构造函数
- 提供一个首字母大写的Set方法(类似与其它语言的public),用于对属性判断并赋值
func(var 结构体类型名) SetXxx(参数列表) (返回值列表){ //加入数据验证的业务逻辑 var.字段=参数 }
- 提供一个首字母大写的get方法(类似于其他语言的public),用于获取属性的值
func (var 结构体类型名) GetXxx() { return var.age; }
快速入门
案例:请大家看一个程序(person.go),不能随便查看人的年龄,工资等隐私,并对输入的年龄进行合理验证。设计model包(person.go)main包(main.go 调用person结构体)
代码实现:
model/person.go
package model
import "fmt"
type person struct{
Name string
age int //其他包不可以访问
sal float64
}
//写一个工厂模式的函数,相当于构造函数
func NewPerson(name string) *person{
return &person{
Name:name,
}
}
//为了访问age和sal我们编写一对SetXxx的方法和GetXxx的方法
func (p *person) SetAge(age int) {
if age>0&&age<150{
p.age=age
} else {
fmt.Println("年龄范围不正确。。")
//给一个默认值
}
}
func (p *person) GetAge() int{
return p.age
}
func (p *person) SetSal(sal float64) {
if sal>=3000&&sal<=30000{
p.sal=sal
} else {
fmt.Println("x薪水范围不正确。。")
}
}
func (p *person) GetSal() float64{
return p.sal
}
main/main.go
package main
import (
"fmt"
"model"
)
func main() {
p:=model.NewPerson("smith")
p.SetAge(18)
p.SetAl(5000)
fmt.Println(p)
fmt.Println(p.Name,"age=",p.GetAge(),"sal=",p.GetSal())
}
继承
继承可以解决代码复用,让我们的编程更加靠近人类思维。
当多个结构体存在相同的属性(字段)和方法时,可以从这些结构体中抽象出结构体(比如大学生,小学生,中学生),在结构体中定义这些相同的属性和方法。
其他的结构体不需要重新定义这些属性(字段)和方法,只需要嵌套一个student匿名结构体即可。
也就是说:在golang中,如果一个struct嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现了继承特性。
基本语法
type Goods struct{
Name string
Price int
}
type Book struct{
Goods //这里就嵌套匿名结构体Goods
Writer string
}
快速入门案例
还是以大学生,小学生,中学生为例
package main
import "fmt"
//编写一个学生考试系统
type Student struct{
Name string
Age int
Score int
}
//将Pupil和Graduate共有的方法也绑定到* Student
func (stu *Student) ShowInfo() {
fmt.Printf("学生名=%v 年龄=%v 成绩=%v\n",stu.Name,stu.Age,stu.Score)
}
func (stu *Student) SetScore(score int){
stu.Score=score
}
//小学生
type Pupil struct{
Student //嵌入Student匿名结构体
}
//显示他的成绩
//这时Pupil结构体特有的方法保留
func (p *Pupil) testing() {
fmt.Println("小学生正在考试。。。")
}
//大学生,研究生
//大学生
type Graduate struct{
Student //嵌入了student匿名结构体
}
//显示他的成绩
//这时Graduate结构体特有的方法,保留
func (p *Graduate) testing() {
fmt.Println("大学生正在考试中")
}
//代码冗余,高中生
func main() {
//当我们对结构体嵌入了匿名结构体使用方法会有变化
pupil:=&Pupil{}
pupil.Student.Name="tom"
pupil.Student.Age=8
pupil.teating()
pupil.Student.SetScore(70)
pupil.Student.ShowInfo()
}
继承带来的便利性
- 代码的复用性提高了
- 代码的扩张性与维护性提高了
深入理解继承
结构体可以使用嵌套匿名结构体所有的字段与方法,即:首字母大写或者小写的字段,方法,都可以使用。
匿名结构体字段访问可以简化。
type A struct{
Name string
age int
}
type B struct{
A
}
var b B
b.A.Name="tom"
//可以简化为
b.Name="smith"
对上面代码的小结—-洋葱理论
1). 当我们直接通过b访问字段或者方法时,其执行流程如下比如b.Name 2). 编译器会首先看b对应的类型有没有Name,如果有,则直接调用B类型的Name字段 3). 如果B没有,才会去寻找A,如果在A中也没有找到则会继续查找,直到未找到,报错。
当结构体和匿名结构体有相同方法或者字段时,编译器采用就近原则。
结构体嵌入两个或多个匿名结构体,如两个匿名结构体有相同的字段和方法(同时结构体本身没有同名的字段与方法)在访问时,就必须指定匿名结构体的名字,否则会编译报错。
如果一个struct嵌套了一个有名结构体,这种模式就是组合,如果是组合关系,那么在访问组合的结构体字段或者方法时,必须带上结构体的名字
type D struct{
a A //有名结构体
}
//如果D中有一个有名的结构体,则访问有名结构体的字段时候,必须带上有名结构体的名字
var d D
d.a.Name="jack"
- 嵌套匿名结构体后,也可以在创建结构体变量时,直接指定匿名结构体字段的值。
多重继承
如果一个struct嵌套了多个匿名结构体,那么该结构体可以直接访问嵌套的匿名结构体的字段和方法,从而实现了多重继承。
接口
package main
import "fmt"
//声明/定义一个接口
type Usb interface{
// 声明两个没有实现的方法
Start()
Stop()
}
typr Phone struct{
}
//让iphone实现Usb接口的方法
func (p Phone) Start() {
fmt.Println("手机开始工作")
}
func (p Phone) Stop() {
fmt.Println("手机停止工作")
}
type Camera struct{
}
//让相机实现USB方法
func (c Camera) Start() {
fmt.Println("相机开始工作")
}
func (c Camera) Stop() {
fmt.Println("相机停止工作")
}
//计算机
type Computer struct{
}
//编写一个working方法,接收一个USB接口类型变量
//只要是实现了USB接口(所谓的实现usb接口,就是指实现了usb接口声明的所有方法)
func (c Computer) Working(usb Usb){
//usb变量会根据传入的实参,来判断到底是phone,还是camera
//通过usb接口来调用start和stop方法
usb.Start()
usb.Stop()
}
func main() {
//先创建结构体变量
computer:=Computer{}
phone:=Phone{}
camera:=Camera{}
//关键点
computer.Working(phone)
computer.Working(camera)
}
接口概念
interface类型可以定义一组方法,但是这些不需要实现。并且interface不包含任何变量。到某个自定义类型要使用的时候,再根据具体的情况把这些方法写出来(实现)。
说明:
- 接口里的所有方法都没有方法体,即接口的方法都是没有实现的方法。接口体现了程序设计的 ==多态和高内聚低耦合== 的思想。
- golang的接口,==不需要显示的实现==。只要一个变量,含有接口类型的所有方法,那么这个变量就实现了这个接口。
使用场景
对于初学者来说,理解接口的概念不算太难,难的是不知道如何使用接口。下面我例举几个应用场景:
中国要制造轰炸机,武装直升机,专家只需要把飞机所需要的功能与规格定下来,然后让别人具体实现就行。
一个项目经理,管理三个程序员,开发一个软件,为了控制与管理软件,项目经理可以定义一些接口,然后让程序员具体实现就可以。
注意事项
- 接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量
package main
import "fmt"
type AInterface interface{
Say()
}
type Stu struct{
Name string
}
func (stu Stu) Say(){
fmt.Println("stu Say()")
}
func main() {
var stu Stu
var a AInterface =stu
a.Say()
}
接口中所有的方法都没有方法体,即都是没有实现的方法
在golang中,一个自定义类型需要实现接口的所有方法,我们才可以说这个自定义类型实现了该接口。
一个自定义类型只有实现了某个接口,才能将自定义类型的实例赋给接口类型
只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型
type integer int
func (i integer) Say(){
fmt.Println("integer say i =", i)
}
var i integer =10
var b AInterface=i
b.Say()
一个自定义类型可以实现多个接口
golang接口中不可以有任何变量
type AInterface interface{
Name string
Test01()
Test02()
}
- 一个接口(比如A接口)可以继承多个别的接口(比如B,C接口),这时候如果要实现A接口,也必须将B,C接口的方法全部实现。
package main
import "fmt"
type BInterface interface{
test01()
}
type CInterface interface {
test02()
}
type AInterface interface{
BInterface
CInterface
test03()
}
type Stu struct{
}
func (stu Stu) test01{
}
func (stu Stu) test02{
}
func (stu Stu) test03{
}
func main (){
var stu Stu
var a AInterface=stu
a.teat01()
}
interface类型默认是一个指针(引用类型),如果没有interface初始化就使用,那么会输出nil
空接口interface{} 没有任何方法,所以所有类型都实现了空接口,即我们可以把任何一个变量赋值给空接口。
接口与继承的对比
package main
import "fmt"
type Monkey struct{
Name string
}
type BirdAble interface{
Flying()
}
type FishAble interface{
Swimming()
}
func (this *Monkey) climbing(){
fmt.Println(this.Name, "生来会爬树")
}
type LittleMonkey struct{
Monkey //继承
}
//让小猴子报一点兴趣班
func (this *LittleMonkey) Flying(){
fmt.Println(this.Name,"通过学习会飞")
}
func (this *LittleMonkey) Swimming(){
fmt.Println(this.Name,"通过学习会游泳")
}
func main () {
monkey:=LittleMonkey{
Monkey{
Name:"悟空",
},
}
monkey.climbing()
monkey.Flying()
monkey.Swimming()
}
代码总结:
1). 当A结构体继承B结构体,那么A自然就继承了B的字段与方法,并且可以直接使用 2). 当A需要扩张功能,同时不许王破坏继承关系,则可以去实现某个接口即可。因此我们认为 ==实现接口是对继承机制的补充==
接口与继承解决的问题不同
继承的价值在于:解决代码复用性与可维护性 接口的价值在于:设计,设计各种好的规范与方法,让其他自定义类型去实现这些方法。
接口一定程度上实现了代码的解耦
多态
变量具有多种形态。面向对象的第三大特征,就go语言,多态特征是通过接口实现的。可以按照统一的接口来调用不同的实现,这时接口变量就呈现不同的形态
在前面接口章节,usb接口案例,usb既可以接收手机变量,又可以接收相机变量,就体现usb接口的多态特征。
接口体现多态的两种形式
多态参数
多态数组
类型断言
先看一段代码:
type Point struct{
x,y int
}
func main() {
var a interface{}
var point Point=Point{1,2}
//point赋值给接口a是可以的
a=point
//如何将a付给一个point变量?
var b Point
b=a // 可以吗? =》error
}
需求:如何将一个接口变量,赋给自定义类型的变量?
因此引申出》》》》类型断言
关于上面代码的解决办法为:
func main() {
var a interface{}
var point Point=Point{1,2}
a=point
var b point
b=a.(Point) //类型断言
fmt.Println(b)
}
b=a.(Point)就是类型断言,表示判断a是否指向Point类型的变量,如果是就转成Point类型并付给b变量,否则报错
基本介绍
类型断言,由于接口是一般类型,不知道具体类型,如果要转成具体类型,就需要使用类型断言,具体如下:
var x interface{}
var b2 float32=1.1
x=b2 //空接口可以接收任意类型
//使用类型断言
y:=x.(float32)
fmt.Printf("y的类型是%T 值是%v", y,y)
在进行类型断言时,如果类型不匹配,就会报panic,因此进行类型断言时,要确保原来的空接口指向的就是断言的类型
断言的检测机制:
var x interface{}
var b2 float32=2.1
x=b2 //空接口,可以接收任意类型
//类型断言(检测)
if y,ok:=x.(float32);ok{
//成功
} else{
//失败
}
fmt.Println("继续执行")
实践
package main
import (
"fmt"
)
type Usb interface{
Start()
Stop()
}
type Phone struct{
name string
}
func (p Phone) Start() {
fmt.Println("手机开始工作。。。")
}
func (p Phone) Stop() {
fmt.Println("手机停止工作。。。")
}
func (p Phone) Call() {
fmt.Println("手机在打电话。。。")
}
type Camera struct{
name string
}
func (c Camera) Start() {
fmt.Println("相机开始工作。。。")
}
func (c Camera) Stop() {
fmt.Println("相机停止工作。。。")
}
type Computer struct{}
func (computer Computer) Working(usb Usb) {
usb.Start()
//如果usb是指向phone结构体变量,则还需要调用Call方法
if phone,ok:=usb.(Phone);ok{
phone.Call()
}
usb.Stop()
}
func main(){
//定义一个Usb接口数组,可以存放Phone和Camera的结构体变量
//这里就体现出多态数组
var usbArr [3]Usb
usbArr[0]=Phone{"vivo"}
usbArr[1]=Phone{"小米"}
usbArr[2]=Phone{"尼康"}
//遍历usbArr
//phone还有一个特有的方法call(),请遍历usb数组,如果是phone变量
//除了调用usb接口声明的方法外,还需要调用phone特有方法call
var computer Computer
for _,v:=range usbArr{
computer.Working(v)
}
}
实践2:写一个函数,循环判断传入的参数类型:
package main
import (
"fmt"
)
func TypeJudge(items... interface{}) {
for index,x:=range items{
switch x.(type) {
case bool:
fmt.Println()
case float32:
fmt.Println()
case int,int32,int64:
fmt.Println()
default:
fmt.Println()
}
}
}
func main() {
TypeJudge(...)
}