封装

介绍

封装(encapsulation)就是把抽象出的字段与对字段的操作封装在一起,数据被保护在内部,程序的其他包只有通过被授权的操作(方法),才能对字段进行操作。

举例:对电视机的操作就是典型的封装

理解与优点

  • 隐藏实现的细节
  • 对数据进行验证,保证安全合理

如何体现封装

  • 对结构体中的属性进行封装
  • 通过方法,包,实现封装

封装的实现步骤

  1. 将结构体,字段(属性)的首字母小写(不能导出了,其他包不能使用,类似private)
  2. 给结构体所在的包提供一个工厂模式的函数,首字母大写。类似一个构造函数
  3. 提供一个首字母大写的Set方法(类似与其它语言的public),用于对属性判断并赋值

func(var 结构体类型名) SetXxx(参数列表) (返回值列表){ //加入数据验证的业务逻辑 var.字段=参数 }

  1. 提供一个首字母大写的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()
}

继承带来的便利性

  • 代码的复用性提高了
  • 代码的扩张性与维护性提高了

深入理解继承

  1. 结构体可以使用嵌套匿名结构体所有的字段与方法,即:首字母大写或者小写的字段,方法,都可以使用。

  2. 匿名结构体字段访问可以简化。

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中也没有找到则会继续查找,直到未找到,报错。

  1. 当结构体和匿名结构体有相同方法或者字段时,编译器采用就近原则。

  2. 结构体嵌入两个或多个匿名结构体,如两个匿名结构体有相同的字段和方法(同时结构体本身没有同名的字段与方法)在访问时,就必须指定匿名结构体的名字,否则会编译报错。

  3. 如果一个struct嵌套了一个有名结构体,这种模式就是组合,如果是组合关系,那么在访问组合的结构体字段或者方法时,必须带上结构体的名字

type D struct{
    a A //有名结构体
}

//如果D中有一个有名的结构体,则访问有名结构体的字段时候,必须带上有名结构体的名字

var d D
d.a.Name="jack"

  1. 嵌套匿名结构体后,也可以在创建结构体变量时,直接指定匿名结构体字段的值。

多重继承

如果一个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不包含任何变量。到某个自定义类型要使用的时候,再根据具体的情况把这些方法写出来(实现)。

说明:

  1. 接口里的所有方法都没有方法体,即接口的方法都是没有实现的方法。接口体现了程序设计的 ==多态和高内聚低耦合== 的思想。
  2. golang的接口,==不需要显示的实现==。只要一个变量,含有接口类型的所有方法,那么这个变量就实现了这个接口。

使用场景

对于初学者来说,理解接口的概念不算太难,难的是不知道如何使用接口。下面我例举几个应用场景:

  1. 中国要制造轰炸机,武装直升机,专家只需要把飞机所需要的功能与规格定下来,然后让别人具体实现就行。

  2. 一个项目经理,管理三个程序员,开发一个软件,为了控制与管理软件,项目经理可以定义一些接口,然后让程序员具体实现就可以。

注意事项

  1. 接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量
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()
}

  1. 接口中所有的方法都没有方法体,即都是没有实现的方法

  2. 在golang中,一个自定义类型需要实现接口的所有方法,我们才可以说这个自定义类型实现了该接口。

  3. 一个自定义类型只有实现了某个接口,才能将自定义类型的实例赋给接口类型

  4. 只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型

type integer int

func (i integer) Say(){
    fmt.Println("integer say i =", i)
}

var i integer =10
var b AInterface=i
b.Say()

  1. 一个自定义类型可以实现多个接口

  2. golang接口中不可以有任何变量

type AInterface interface{
    Name string
    Test01()
    Test02()
}
  1. 一个接口(比如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()
}


  1. interface类型默认是一个指针(引用类型),如果没有interface初始化就使用,那么会输出nil

  2. 空接口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接口的多态特征。

接口体现多态的两种形式

  1. 多态参数

  2. 多态数组

类型断言

先看一段代码:

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(...)
}