Go 面向对象


面向对象介绍

面向对象和面向过程都是解决问题的一种思路。

  • 面向过程:

    • 是一种以过程为中心的编程思想,实现功能的每一步都是自己实现的。面向过程编程最易被初学者接受,其往往用一长段代码来实现指定功能,尽量忽略面向对象的复杂语法,即面向过程是“强调做什么,而不是以什么形式去做”
    • 开发过程的思路是将数据与方法按照执行的逻辑顺序组织在一起,数据与方法分开考虑,也就是拿数据做操作。
  • 面向对象:

    • 是一种以对象为中心的编程思想,通过指挥对象实现具体的功能,强调“必须通过对象的形式来做事情”
    • 将数据(成员变量)与功能(成员方法)绑定到一起,进行封装,以增强代码的模块化和重用性,这样能够减少重复代码的编写过程,提高开发效率。

Go 中的“面向对象”

  • 在 Go 中没有对象这一说法,因为 Go 是一个面向过程的语言。但是我们又知道面向对象在开发中的便捷性,所以在 Go 中有了结构体这一类型。

  • 在结构体中可以添加属性(成员)和方法(函数)。


匿名字段

什么是继承?

继承是面向对象三大特征之一(封装、继承、多态),可以使得子类具有父类的属性和方法,还可以在子类中重新定义以及追加属性和方法。

虽然在 GO 中是没有继承概念的,但可以通过“匿名字段(也叫匿名组合)”来实现继承的效果。

// 父类
type Person struct {
    name string
    age  int
}

// 子类
type Student struct {
    Person // 匿名字段
    score  int
}

func main() {
    // 顺序初始化
    s1 := Student{Person{"xiaoming", 18}, 97}
    fmt.Printf("%+v\n", s1) // {Person:{name:xiaoming age:18} score:97}
    // 指定成员初始化
    s2 := Student{Person: Person{name: "xiaohua"}, score: 97}
    fmt.Printf("%+v\n", s2) // {Person:{name:xiaohua age:0} score:97}

    // 操作成员
    s1.age = 19 // 子类直接拥有父类的字段
    s2.Person.age = 18

    fmt.Printf("%p\n", &s2.Person) // 0x11004130
    s2.Person = Person{name: "xiaohua2"}
    fmt.Printf("%p\n", &s2.Person) // 0x11004130 仍是同一内存空间
}

若父类和子类存在同名字段,那么操作时采用就近原则:如果能够在自己对象所属的类(结构体)中找到对应的成员,那么直接进行操作;如果找不到就去对应的父类(结构体)中查找。


指针类型匿名字段

结构体(类)中的匿名字段的类型,也可以是指针。

type Person struct {
    name string
    age  int
}

type Student struct {
    *Person // 匿名字段为指针类型
    score   int
}

func main() {
    s1 := Student{&Person{"xiaoming", 18}, 97}
    // 此时Person存的是地址
    fmt.Printf("%+v\n", s1) // {Person:0x11004110 score:97}
    fmt.Println(s1.name, s1.age, s1.score) // xiaoming 18 97

    /*
        var s2 Student
        s2.name = "xiaohua"  // 报错:panic: runtime error: invalid memory address or nil pointer dereference

        原因:
        *Person还没有指向任何的内存地址,那么其默认值为nil,所以对象s2也就无法操作Person中的成员

        解决方案:
        s2 := new(Person)  // new()分配了内存空间并赋值给s2
        s2.name = "xiaohua"
    */
}

多重继承

多重继承指的是一个类可以继承另外一个类,而另外一个类又可以继承别的类,比如 A 类继承 B 类,而 B 类又可以继承 C 类。


    Person
    score int
}

func main() {
    // 对象初始化
    s1 := Student{Person{Object{name: "xiaoming"}, 18}, 97}
    // 成员操作
    s1.Person.Object.name = "xiaoming2"
    s1.name = "xiaoming3"
    fmt.Printf("name=%+v\n", s1) // name={Person:{Object:{name:xiaoming3} age:18} score:97}
}

方法

什么是封装?

封装的概述:

  • 封装是面向对象三大特征之一(封装、继承、多态)。
  • 封装是指将对象的属性和行为进行包装,不需要让外界知道具体的实现细节。

封装的原则:

  • 将类的某些信息隐藏在类内部,不允许外部程序直接访问,而是通过该类提供的方法来实现对隐藏信息的操作和访问。

封装的好处:

  • 通过方法来控制成员变量的操作,提高了代码的安全性。
  • 用方法将代码进行封装,提高了代码的复用性。

而在 GO 中,封装是通过方法来实现的。


方法创建

方法是一类特殊的函数,是绑定在某种类型的变量上的函数,且有一定的约束范围。

其中,变量的类型不仅仅局限于结构体类型,也可以是任意类型,如 int、bool 等的别名类型。

即一个结构体加上绑定在这个类型上的方法,就等价于一个类。

// 函数定义
func 函数名(args) 返回类型

// 方法定义
func (recv recv_type) 函数名(args) 返回类型
  • recv 表示【接收器】,是某种类型(recv_type)的变量。
  • 这个接收器的类型可以是普通变量或者指针变量。
  • 只有指针接收者类型的方法,才能修改这个接收器的原成员值;而非指针接收者,方法修改的只是这个传入的指针接收者的一个拷贝
  • 类型的定义和绑定在它上面的方法的定义可以不放置在同一个文件中,它们可以存在在不同的源文件,唯一的要求是:它们必须在同一个包下面。
import (
    "fmt"
    "strconv"
)

type Student struct {
    name string
    age  int
}

func (s Student) GetInfo() string {
    fmt.Printf("%s在GetInfo方法中的内存地址:%p\n", s.name, &s)
    return "name=" + s.name + ", age=" + strconv.Itoa(s.age)
}

func (s *Student) SetAge(age int) {
    fmt.Printf("%s在SetAge方法中的内存地址:%p\n", s.name, s)
    s.age = age
}

func main() {
    s1 := Student{"xiaoming", 18}
    info1 := s1.GetInfo()
    fmt.Printf("%s在main中的内存地址:%p\n", s1.name, &s1)
    // 对于非指针接收器,它是拷贝原来的数据
    // xiaoming在GetInfo方法中的内存地址:0x11004120
    // xiaoming在main中的内存地址:0x11004110
    fmt.Println("info1: ", info1) // info1: name=xiaoming, age=18

    s2 := &s1
    s2.SetAge(17)
    fmt.Printf("%s在main中的内存地址:%p\n", s2.name, s2)
    // 对于指针接收器,可以直接拿到原数据所在的内存地址,也就是说可以直接修改原来的数值
    // xiaoming在SetAge方法中的内存地址:0x11004110
    // xiaoming在main中的内存地址:0x11004110
    // xiaoming在GetInfo方法中的内存地址:0x11004140
    fmt.Println("s2.info: ", s2.GetInfo())
}

注意:不管是指针接收者还是非指针接收者,他在接受一个对象的时候,会自动将这个对象转换为这个方法所需要的类型。
也就是说,如果现在有一个非指针类型的对象,去调用一个指针接收者的方法,那么这个对象将会自动被【取地址】然后再被调用。
换句话说,方法的调用类型不重要,重要的是方法是怎么【定义】的。


接口

接口是一种规范,一种约定。

在 GO 语言中,任何类型的数据只要实现了一个接口中的所有方法集,那么他就属于这个接口类型。所以,当我们在实现一个接口的时候,需要实现这个接口下的所有方法,否则编译将不能通过。


接口定义

接口定义:

type 接口名 interface {
    方法1(参数) 返回类型
    方法2(参数) 返回类型 
    ...
}

接口使用示例:

// 定义接口
type Action interface {
    Dark()
    Eat()
}

// 再定义多接口
type Look interface {
    Color(color string) string
}

// 定义结构体
type Dog struct {
    name string
}

type Cat struct {
    name string
}

// 定义方法来实现接口(方法名,参数,返回类型,必须和接口中所定义的完全一致)
// 方法中的接收者,就是调用这个方法的对象类型
func (dog Dog) Dark() {
    fmt.Printf("%s汪汪叫\n", dog.name)
}

func (dog Dog) Eat() {
    fmt.Printf("%s喜欢吃狗粮\n", dog.name)
}

func (dog Dog) Color(color string) string {
    return dog.name + "是" + color + "肤色"
}

func (cat Cat) Dark() {
    fmt.Printf("%s喵喵叫\n", cat.name)
}

func (cat Cat) Eat() {
    fmt.Printf("%s喜欢吃猫粮\n", cat.name)
}

func (cat Cat) Color(color string) string {
    return cat.name + "是" + color + "肤色"
}

func main() {
    dog := Dog{"旺财"}
    cat := Cat{"苗苗"}
    // 在调用接口的时候,需要先声明这个接口类型的变量
    var a1 Action // 也可以用自动推导 a1 := dog
    var a2 Action
    var l1 Look
    var l2 Look
    // 然后把实现了这个方法的接收器对象赋值给这个变量
    a1 = dog
    a2 = cat
    l1 = dog
    l2 = cat
    // 随后就可以用接口变量来调用已实现的方法
    a1.Dark()  // 旺财汪汪叫
    a1.Eat()   // 旺财喜欢吃狗粮
    a2.Dark()  // 苗苗喵喵叫
    a2.Eat()   // 苗苗喜欢吃猫粮
    fmt.Println(l1.Color("黄色")) // 旺财是黄色肤色
    fmt.Println(l2.Color("蓝色"))  // 苗苗是蓝色肤色
}

空接口

没有任何方法声明的接口称之为空接口:interface{}

所有的类型都实现了空接口,因此空接口可以存储任意类型的数值。

Go 很多库的源代码都会以空接口作为参数,表示接受任意类型的参数,比如 fmt 包下的 Print 系列函数

func Println(a ...interface{}) (n int, err error)
func Print(a ...interface{}) (n int, err error)

// 自定义函数
func Test(arg ...interface{}) {}  // 表示可以接收任意个数和类型的参数

空接口使用示例:


func main() {
    var any interface{}
    any = 10
    fmt.Println(any)  // 10

    any = "golang"
    fmt.Println(any)  // golang

    any = true
    fmt.Println(any)  // true
}

类型断言

先看一个例子:

func main() {
    var a int = 1
    var i interface{} = a
    var b int = i
}

运行结果:

./prog.go:6:14: cannot use i (variable of type interface{}) as type int in variable declaration:
        need type assertion

编译报错,不能将 interface{} 类型的变量 i 赋值给整型变量 b

结论:可以将任意类型的变量赋值给空接口 interface{} 类型,但是反过来不行。

那么我们怎么反向知道这个变量里面实际保存了的是哪个类型的对象呢?这时就可以用到类型断言。类型断言(Type Assertion)用来检查接口变量值是否实现了某个接口,或者是否等于某个具体类型

// 断言的一般格式
value, ok := x.(T)   // x为接口类型,ok为bool类型
  • 若 T 是具体类型,则类型断言会检查 x 的动态类型是否等于具体类型 T。如果检查成功,类型断言返回的结果是 x 的值。
  • 若 T 是接口类型,则类型断言会检查 x 的动态类型是否满足 T。如果检查成功,返回值是 T 接口类型。
  • 若 T 是空接口,则断言失败。

示例1:类型断言

func main() {
    var x interface{}
    x = 8
    val, ok := x.(int)
    fmt.Printf("val is %d, ok is %t\n", val, ok) // val is 8, ok is true
    val2, ok := x.(string)
    fmt.Printf("val is %s, ok is %t\n", val2, ok) // val is , ok is false
    x = "go"
    val3, ok := x.(int)
    fmt.Printf("val is %d, ok is %t\n", val3, ok) // val is 0, ok is false
}

示例2:类型查询

type Student struct {
    name string
    id   int
}

func main() {
    i := make([]interface{}, 3)
    i[0] = 1                    // int
    i[1] = "hello go"           // string
    i[2] = Student{"mike", 666} // Student

    for index, data := range i {
        switch value := data.(type) {  // 类型查询
        case int:
            fmt.Printf("x[%d] 类型为int, 内容为%d\n", index, value)
        case string:
            fmt.Printf("x[%d] 类型为string, 内容为%s\n", index, value)
        case Student:
            fmt.Printf("x[%d] 类型为Student, 内容为name = %s, id = %d\n", index, value.name, value.id)
        }
    }
}

综合示例

实现计算器的加减法功能

package main

import "fmt"

// 定义父类
type Numbers struct {
    num1 float64
    num2 float64
}

// 定义子类 加法
type Add struct {
    Numbers
}

// 定义子类 减法
type Sub struct {
    Numbers
}

// 定义计算器的接口
type Calculator interface {
    CheckData(args ...interface{}) bool // 数据校验
    OperateData() float64               // 完成计算
}

// 加法类实现接口
func (add *Add) CheckData(args ...interface{}) bool {
    if len(args) != 2 {
        fmt.Println("Error, need two params.")
        return false
    }
    if _, ok := args[0].(float64); !ok {
        fmt.Println("Error, need float param1.")
        return false
    }
    if _, ok := args[1].(float64); !ok {
        fmt.Println("Error, need float param2.")
        return false
    }
    // 赋值num1和num2
    add.num1 = args[0].(float64)
    add.num2 = args[1].(float64)
    // 为什么要用 .(float64) ?
    // 由于args是接口类型,接口类型只有成员方法,没有成员变量,
    // 若要修改某接口的具体成员变量的值,要先通过类型断言将这个接口类型做一层转换,转换为具体实现类型
    return true
}

// 完成加法操作
func (add *Add) OperateData() float64 {
    return add.num1 + add.num2
}

// 减法类实现接口
func (sub *Sub) CheckData(args ...interface{}) bool {  // 空接口表示接收任意类型
    if len(args) != 2 {
        fmt.Println("Error, need two params.")
        return false
    }
    if _, ok := args[0].(float64); !ok {
        fmt.Println("Error, need float param1.")
        return false
    }
    if _, ok := args[1].(float64); !ok {
        fmt.Println("Error, need float param2.")
        return false
    }
    // 赋值num1和num2
    sub.num1 = args[0].(float64)
    sub.num2 = args[1].(float64)
    return true
}

// 完成减法操作
func (sub *Sub) OperateData() float64 {
    return sub.num1 - sub.num2
}

// 定义工厂类
type CalcFactory struct {
}

// 创建加法类对象,返回指针类型
func NewAdd() *Add {
    return new(Add)
}

// 创建减法类对象,返回指针类型
func NewSub() *Sub {
    return new(Sub)
}

// 根据入参opType创建出不同的对象
func (f *CalcFactory) CreateOperation(opType string) Calculator {
    var c Calculator
    switch opType {
    case "+":
        c = NewAdd()
    case "-":
        c = NewSub()
    default:
        panic("Error! this operator doesn't exist.")
    }
    return c
}

func main() {
    factory := new(CalcFactory)
    op := factory.CreateOperation("+")
    op.CheckData(1.12, 3.14)
    fmt.Println("加法结果:", op.OperateData())
    op2 := factory.CreateOperation("-")
    op2.CheckData(7.12, 3.14)
    fmt.Println("减法结果:", op2.OperateData())
}

接口作为函数参数

在函数定义时,若形参为接口类型,那么在函数调用时,实参需为该接口的具体实现。

type Calculer interface {
    Add() int
}

type Operator struct {
    num1 int
    num2 int
}

func (o *Operator) Add() int {
    return o.num1 + o.num2
}

func DoCal(c Calculer) {
    fmt.Println("计算结果为:", c.Add())
}

func main() {
    o := &Operator{1, 4}
    // 如果方法的接口形参是指针类型,那实参只能传接口指针类型
    // 如果接口形参是普通类型,那实参可传接口普通类型或接口指针类型
    DoCal(o)
}

接口嵌套

接口嵌套就是一个接口中又包含了其它接口。此时若要实现外部接口,则需要把内部嵌套接口对应的所有方法也一同实现。

type A interface {
    run1()
}

type B interface {
    run2()
}

// 定义嵌套接口C
type C interface {
    A
    B
    run3()
}

type Runner struct {}
    
// 实现嵌套接口A的方法
func (r Runner ) run1() {
    fmt.Println("run1!!!!")
}

// 实现嵌套接口B的方法
func (r Runner ) run2() {
    fmt.Println("run2!!!!")
}

func (r Runner ) run3() {
    fmt.Println("run3!!!!")
}

func main() {
   var runner C
   runner = new(Runner)  // runner实现了C接口的所有方法
   runner.run1()
   runner.run2()
   runner.run3()
}

posted @ 2023-03-12 00:54  Juno3550  阅读(154)  评论(0编辑  收藏  举报