Go语言之面向对象编程(三)


Golang也拥有面向对象编程的封装、继承、多态特性。

一、封装

封装就是将抽象出的字段和对字段的操作封装在一起,数据被保护在内部,程序的其它包只有通过被授权的方法才能对字段进行操作。那么如何来实现封装呢?

  • 将结构体、首字母小写(这样就是私有变量,只能在本包使用)
  • 在结构体所在的包中提供一个工厂模式的函数,首字母大写,这时一个对外公开的函数

比如,对私有属性赋值,可以提供一个SetAttr属性工厂函数,在内部可以进行业务验证,进行属性赋值;获取属性的值,可以提供一个GetAttr属性工厂函数。

案例:员工包括姓名、年龄、薪资,其中薪资、年龄对外是隐私的,年龄大于等于18岁。

分析:薪资、年龄通过工厂函数进行赋值,并且年龄赋值时进行判断。

  • model/person.go
package model

import "fmt"

type person struct {
    Name   string
    age    int
    salary int
}

// 结构体时小写变量名,提供工厂方法
func CreatePerson(name string) *person {

    return &person{
        Name: name,
    }
}

// 提供年龄赋值工厂方法
func (p *person) SetAge(age int) {
    if age >= 18 {
        p.age = age
    } else {
        fmt.Println("年龄输入不合法")
    }
}

// 提供获取年龄的方法
func (p *person) GetAge() int {
    return p.age
}

// 提供薪资赋值工厂方法
func (p *person) SetSalary(salary int) {
    p.salary = salary
}

// 提供获取薪资的方法
func (p *person) GetSalary() int {
    return p.salary
}
  • main/main.go
package main

import (
    "fmt"
    "go_tutorial/day12/example/02/model"
)

func main() {

    // 创建一个person的结构体变量
    p := model.CreatePerson("ariry")
    p.SetAge(30)
    p.SetSalary(5000)
    fmt.Println("姓名:", p.Name, "年龄:", p.GetAge(), "薪资:", p.GetSalary())

}

可以看到person结构体、字段age、salary都是私有的,通过提供工厂方法进行创建、赋值、获取值操作。

二、继承

(一)基础

1、快速上手

   继承主要解决的就时代码复用问题,不至于出现过多的代码冗余问题。当多个结构体存在相同的属性、方法时,可以从这些结构体中抽象出一个拥有共同属性、方法的结构体。其余结构体可以通过匿名结构体的方式来继承这个结构体即可。

案例:假如学生分为中学生、大学生,这些学生都有姓名、年龄、考试成绩,平时需要进行各种考试。不过大学生平日里空闲时间比较多,所以去图书馆看书。所以这里面可以抽象出:

  • 属性有姓名、年龄、成绩
  • 方法有考试
  • 另外大学生额外的方法有看书

package main

import "fmt"

type Student struct {
    Name  string
    Age   int
    Score float64
}

func (student *Student) Examing() {
    fmt.Print("姓名:", student.Name, "年龄:", student.Age, "考试成绩:", student.Score)
}

type MiddleStudent struct {
    Student // 学生匿名结构体
}

type UgStudent struct {
    Student // 学生匿名结构体
}

// 大学生特有读书方法
func (ugs *UgStudent) ReadBook() {
    fmt.Print(ugs.Name, "正在读书...")
}

func main() {
    // MiddleStudent继承Student,所以可以通过mt.Student.Name来进行属性的调用,
    // 同时也可以mt.Name,如果在本结构体中没找到该属性,会去匿名结构体中查找
    mt := MiddleStudent{}
    mt.Student.Name = "中学生" //mt.Name = "中学生"
    mt.Student.Age = 15     //mt.Age = 15
    mt.Student.Score = 85.5 //mt.Score = 85.5
    mt.Examing()

    // 调用大学生特有的方法
    ugs := UgStudent{}
    ugs.Name = "大学生"
    ugs.Age = 25
    ugs.Score = 95.5
    ugs.ReadBook()

}

 2、深入理解

  • 结构体可以使用匿名结构体中的所有字段和方法,包括首字母小写的结构体、属性、方法等私有变量
  • 匿名结构体字段访问可简化(mt.Student.Name可简化为mt.Name)
  • 当结构体于匿名结构体有相同字段和方法时,采用就近原则,如若希望访问匿名结构体字段需要指明匿名结构体
  • 如若结构体有两个或者多个匿名结构体时,并且匿名结构体都拥有相同字段和方法,但是结构体本身无同名字段和方法时,需指明匿名结构体,否则编译报错
package main

import "fmt"

type animal struct {
    name  string
    hobby string
}

func (a *animal) showHobby() {
    fmt.Println("hobby:", a.hobby)
}

type cat struct {
    animal
    name string
}

func (c *cat) showHobby() {
    fmt.Println("hobby:", c.hobby)
}

type tigger struct {
    animal
    cat
}

func main() {
    /*
        1、结构体可以使用匿名结构体中的所有字段和方法,包括首字母小写的结构体、属性、方法等私有变量
        2、匿名结构体字段访问可简化
        3、当结构体于匿名结构体有相同字段和方法时,采用就近原则,如若希望访问匿名结构体字段需要指明匿名结构体
    */
    // 声明一个cat结构体变量
    var c cat
    c.name = "猫"      // 就近原则,使用的name时cat结构体中的name,而非animal中的name
    c.hobby = "吃鱼..." // 匿名结构体可简写,查找顺序:本结构体-->各个匿名结构体-->如若没有报错
    c.showHobby()

    /*
        4、如若结构体有两个或者多个匿名结构体时,并且匿名结构体都拥有相同字段和方法,但是结构体本身无同名字段和方法时,需指明匿名结构体,否则编译报错
    */
    // 声明一个tigger结构体
    t := tigger{}
    t.animal.name = "老虎..."    // 需要指明具体的匿名结构体
    t.animal.hobby = "吃小动物..." // 需要指明具体的匿名结构体
    t.animal.showHobby()

}

(二)进阶

1、组合

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

package main

type Student struct {
    Name string
    Age  int
}

type MiddleStudent struct {
    s Student // 有名结构体,组合关系
}

func main() {
    var ms MiddleStudent
    // 必须带上有名结构体的名字s
    ms.s.Name = "Alice"
    ms.s.Age = 25
}

2、匿名结构体初始化值

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

package main

import "fmt"

type Student struct {
    Name string
    Age  int
}

type MiddleStudent struct {
    Student
}

type UgStudent struct {
    *Student
}

func main() {
    // 直接给匿名结构体初始化赋值
    ms := MiddleStudent{
        Student{Name: "kity", Age: 21},
    }

    ugs := UgStudent{
        &Student{Name: "jack", Age: 25},
    }

    fmt.Print(ms)
    fmt.Print(*ugs.Student)

}

3、基本数据类型的匿名字段

结构体的匿名字段时基本数据类型,又该如何访问呢?

package main

import "fmt"

type Student struct {
    username string
    age      int
    int      // 匿名字段(基本数据类型)
}

func main() {
    // 匿名字段
    s := Student{}
    s.username = "peter"
    s.age = 20
    s.int = 85
    fmt.Println(s)
}

注意:如果一个结构体有int类型的匿名字段,就不能有第二个;如果需要有多个int的字段,则必须给int字段指定名称。

4、多继承

一个结构体嵌套了多个匿名结构体,那么该结构体可以直接访问嵌套的匿名结构体的字段和方法,从而实现多继承。

package main

import "fmt"

type A struct {
    x1 string
    x2 int
}

type B struct {
    x1 float64
}

type C struct {
    A
    B
}

func main() {
    c := C{}
    // 必须指明匿名结构体的
    c.A.x1 = "abc"

    fmt.Println(c)
}

如果结构体嵌套多个匿名结构体,并且拥有相同的字段,而本结构体又没有该字段,则进行属性操作时必须指明匿名结构体。

三、多态

 多态的特性主要是通过接口来体现的,所以需要先了解清楚接口相关知识。

(一)接口基础

 接口(interface)类型就是定义一组方法,但是这些方法都不需要实现。并且interface不能包含任何任何变量,如果某个变量要使用interface时,再去实现具体的方法。

基本语法:

type 接口名称 interface {
  
    method1(参数列表) 返回值列表
    method2(参数列表)返回值列表
    ...   
   
}

接口中的所有方法都是没有方法体,即都是没有实现的方法。只要一个变量事项了接口类型中的所有方法,那么这个变量就实现了这个接口。

案例:

  我们电脑上有USB接口,这个接口可以插入多种设备,比如相机、手机、硬盘等。如果插入的的是硬盘那么就会读取硬盘空间的操作,如果是手机,那么就会执行手机对应的操作等。

package main

import "fmt"

// 定义一个接口
type Usb interface {
    // 声明两个未实现的方法
    start()
    stop()
}

// 声明一个Phone的结构体
type Phone struct {
}

// Phone结构体变量接口实现方法
func (phone Phone) start() {
    fmt.Println("手机开始工作...")
}

func (phone Phone) stop() {
    fmt.Println("手机结束工作...")
}

// 声明一个Camera的结构体
type Camera struct {
}

// Camera结构体变量接口实现方法
func (camera Camera) start() {
    fmt.Println("相机开始工作...")
}

func (camera Camera) stop() {
    fmt.Println("相机结束工作...")
}

// 声明一个Computer结构体
type Computer struct {
}

// usb会自动根据传递过来的来判断是phone还是camera然后调用对应的结构体变量中实现的方法
func (computer Computer) use(usb Usb) {
    usb.start()
    usb.stop()
}

func main() {
    // 创建结构体变量
    computer := Computer{}

    phone := Phone{}
    camera := Camera{}

    computer.use(phone)
    computer.use(camera)

}

/*
手机开始工作...
手机结束工作...
相机开始工作...
相机结束工作...
*/

(二)深入理解接口

  • 接口本身不能创建实例,但是可以指向一个实现了该接口的自定义数据类型的实例
  • 空接口interface{}没有任何方法,所有类型都实现了空接口,即可以把任何一个类型的变量赋值给空接口
  • 接口中所有的方法都没有方法体,即都是没有实现的方法
  • Golang中,一个自定义类型将接口中所有的方法都实现了,才能说这个自定义类型实现了该接口
  • 只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型
  • 一个自定义类型可以实现多个接口
  • Golang接口中不能存在任何变量
  • 一个接口(A接口)可以继承多个其它接口(B、C接口),此时如果要实现A接口,则必须也实现B、C接口中的全部方法
  • 接口类型是一个指针(引用类型),如果没有对接口初始化就使用,就会输出nil
package main

import "fmt"

// 接口本身不能创建实例,但是可以指向一个实现了该接口的自定义数据类型的实例
type A interface {
    m1()
}

type S1 struct {
}

func (s1 S1) m1() {
    // 实现A接口中的m1方法
}

func main() {
    s1 := S1{}
    var a1 A = s1
    fmt.Println(a1)
}
package main

import "fmt"

// 空接口interface{}没有任何方法,所有类型都实现了空接口,即可以把任何一个类型的变量赋值给空接口
type A interface {
}

func main() {
    var x int
    var i A = x
    fmt.Println(i)
}
package main

import "fmt"

// 一个自定义类型可以实现多个接口
type A interface {
    a1()
}

type B interface {
    b1()
}

type integer int

func (i integer) a1() {

}

func (i integer) b1() {

}

func main() {
    var i integer = 10
    var a A = i
    var b B = i
    fmt.Println(a, b)
}
package main

import "fmt"

// Golang中,一个自定义类型将接口中所有的方法都实现了,才能说这个自定义类型实现了该接口
type Animal interface {
    eat()
}

type Cat struct {
}

/*
这里如果写成这样,就是错误的,因为是*Cat指针类型实现了接口,而非Cat类型实现接口
func (c *Cat) eat() {

}

*/
func (c Cat) eat() {

}

func main() {

    c := Cat{}
    var a Animal = c
    fmt.Println(a)

}

(三)接口最佳实践

1、引入

如果对一个切片中都是int类型的数组或者切片进行排序,想必是一件比较简单的事情,比如可以通过golang中sort包Ints进行排序。

package main

import (
    "fmt"
    "sort"
)

func main() {
    // 声明一个元素是int类型的切片
    var intSlice = []int{2, 1, 10, 5}

    // 对切片进行排序
    sort.Ints(intSlice)

    // 打印排序好的切片
    fmt.Println(intSlice)
}

但是如果实现对结构体根据某个字段进行排序,又当如何呢,如下面:

type UserInfo struct {
    Username string
    Age int
}

将多个用户信息通过年龄进行排序。

2、结构体排序

此时可以通过sort包中Sort方法来实现:

func Sort(data Interface)

Sort方法的参数接收一个接口类型的参数data,只要自定义的数据类型实现了data接口类型的中的方法就可以传入,那么实现哎什么方法呢?

type Interface interface {
    // Len is the number of elements in the collection.
    Len() int

    // Less reports whether the element with index i
    // must sort before the element with index j.
    //
    // If both Less(i, j) and Less(j, i) are false,
    // then the elements at index i and j are considered equal.
    // Sort may place equal elements in any order in the final result,
    // while Stable preserves the original input order of equal elements.
    //
    // Less must describe a transitive ordering:
    //  - if both Less(i, j) and Less(j, k) are true, then Less(i, k) must be true as well.
    //  - if both Less(i, j) and Less(j, k) are false, then Less(i, k) must be false as well.
    //
    // Note that floating-point comparison (the < operator on float32 or float64 values)
    // is not a transitive ordering when not-a-number (NaN) values are involved.
    // See Float64Slice.Less for a correct implementation for floating-point values.
    Less(i, j int) bool

    // Swap swaps the elements with indexes i and j.
    Swap(i, j int)
}

即:Len、less、Swap三个方法即可,所以将UserInfo切片类型实现这三个方法,然后传入到 Sort方法即可。

package main

import (
    "fmt"
    "math/rand"
    "sort"
)

// 声明一个UserInfo的结构体
type UserInfo struct {
    UserName string
    Age      int
}

// 声明一个UserInfo的切片
type UserInfoSlice []UserInfo

// 实现interface Len Less Swap
func (uis UserInfoSlice) Len() int {
    // 返回切片的长度
    return len(uis)
}

func (uis UserInfoSlice) Less(i, j int) bool {
    // 按照什么样的顺序进行排序
    return uis[i].Age > uis[j].Age
}

func (uis UserInfoSlice) Swap(i, j int) {
    // 交换,更简单的交换方式 uis[i], uis[j] = uis[j], uis[i]
    temp := uis[i]
    uis[i] = uis[j]
    uis[j] = temp

}

func main() {
    // 随机生成不同年龄的UserInfo切片
    var uis UserInfoSlice
    for i := 0; i < 20; i++ {
        ui := UserInfo{
            UserName: fmt.Sprintf("NO~%d", rand.Intn(50)),
            Age:      rand.Intn(100),
        }

        // 将ui加入到uis中
        uis = append(uis, ui)
    }

    // 排序前顺序
    for _, v := range uis {
        fmt.Println(v)
    }

    // 调用sort.Sort
    sort.Sort(uis)

    // 排序后顺序
    fmt.Println()
    for _, v := range uis {
        fmt.Println(v)
    }

}

(四)接口VS继承

接口于继承之间有什么区别呢?

不同点在于体现的价值不同,继承着重解决代码的复用问题,接口着重解决代码的设计和规范;联系是接口是对继承的一种扩展,当继承这些功能的同时,又希望不破环关系时,可以通过接口的方式来补充功能。

例如:猴子这种动物天性就是会爬树,从父辈那里继承过来的本领,但是小猴子不甘如此,希望会游泳,于是通过后天的不断努力终于学会了。这个例子中小猴子继承的就是父辈会爬树的本领,但是这个个例它还会游泳,如果将游泳加入到父辈哪里显然你不合理。

package main

import "fmt"

// 声明一个Monkey的结构体
type Monkey struct {
    Name string
}

// Monkey结构体天生拥有climbing方法
func (m *Monkey) Climbing() {
    fmt.Printf("%s爬树...", m.Name)
}

// 声明一个LittleMonkey结构体
type LittleMonkey struct {
    Monkey
}

// 声明一个接口
type OtherSkills interface {
    Swimming()
}

// LittleMonkey通过后天不断努力实现了OtherSkills
func (lk *LittleMonkey) Swimming() {
    fmt.Printf("%s学会了游泳...", lk.Name)
}

func main() {
    // 创建一个LittleMonkey的结构体变量
    lk := LittleMonkey{}
    lk.Name = "小猴子"

    // 调用天生的Climbing方法
    lk.Climbing()

    // 通过后天的努力实现了OtherSkills,学会了Swimming
    lk.Swimming()

}

  那么这个例子就体现出了接口是对继承功能的一种扩展,可能有人有疑问了,为什么不将Swimming方法直接写在LittleMonkey结构体变量中呢?实际上要这样讲的话也可以说通,但是写代码还是需要注重规范性和灵活性,使用接口不是显得更好吗?那个特殊个例实现了这个OtherSkills ,就拥有里面的本领。

(五)多态

多态就是通过接口来实现的,统一的接口,不同的表现,此时接口变量体现不同的形态。比如usb Usb接口变量,既可以接收手机变量,又可以接收相机变量,体现了Usb接口的多态特性。

接口体现多态的两种形式:

  • 多态参数
  • 多态数组

1、多态参数

参考之前Usb接口案例,既可以接收手机变量,又可以接收相机变量,体现了Usb接口的多态特性。

2、多态数组

package main

import "fmt"

// 定义一个接口
type Usb interface {
    // 声明两个未实现的方法
    start()
    stop()
}

// 声明一个Phone的结构体
type Phone struct {
    Name string
}

// Phone结构体变量接口实现方法
func (phone Phone) start() {
    fmt.Println("手机开始工作...")
}

func (phone Phone) stop() {
    fmt.Println("手机结束工作...")
}

// 声明一个Camera的结构体
type Camera struct {
    Name string
}

// Camera结构体变量接口实现方法
func (camera Camera) start() {
    fmt.Println("相机开始工作...")
}

func (camera Camera) stop() {
    fmt.Println("相机结束工作...")
}

func main() {
    // 创建Usb结构体数组变量
    var usbArr [3]Usb
    usbArr[0] = Phone{"iphone11"}
    usbArr[1] = Phone{"xiaomi"}
    usbArr[2] = Camera{"Canon"}
    fmt.Println(usbArr)

}

/*
[{iphone11}
{xiaomi}
{Canon}]
*/

可以看到将实现了接口的不同数据类型放到一个数组中,数组本身应该是放一种数据类型的,但是这里就体现了多态特性。