首页 >后端开发 >Golang >Golang 中的面向对象编程 (OOP) 简介

Golang 中的面向对象编程 (OOP) 简介

Linda Hamilton
Linda Hamilton原创
2024-12-23 12:43:11376浏览

当我们谈论编程时,我们通常指的是编写一堆修改某些数据并与某些数据交互的函数。面向对象编程(OOP)是一种编程模型,它专注于包含数据并附加一些相关功能的“对象”。面向对象编程有四大支柱:继承、封装、多态性和抽象。在本博客中,我们将通过示例了解如何在 Golang 中实现它们中的每一个。推荐一些有关 OOP 的基本概念,但如果没有,我将简要介绍所有四个支柱的含义。

Introduction to Object Oriented Programming (OOP) in Golang

类、对象和方法

面向对象编程的核心思想可以概括为以下几点:

  • 您定义“类”,它们是数据和可以调用该数据的函数的集合。
  • 这些特定的函数称为该特定类的“方法”。
  • 类的实际实例称为“对象”。

让我们看一些 Golang 中的代码来理解这三个概念:

package main

import "fmt"

type Batman struct {
    actor string
    year int
}

func (b Batman) SayImBatman() {
    fmt.Printf("I'm %s and I'm Batman from year %d\n", b.actor, b.year)
}

func main() {
    b1 := Batman{actor: "Michael Keaton", year: 1989}
    b2 := Batman{actor: "Christian Bale", year: 2005}

    b1.SayImBatman()
    b2.SayImBatman()
}

在Golang中,类只不过是我们定义的类型。这些类型不一定必须是结构体,但通常都是结构体,因为在 OOP 中我们使用数据集合,这些数据可以是任何类型(字符串、整数等)。

类是对象的蓝图。每当实例化一个类时,就会形成一个对象。在此示例中,b1 和 b2 是 Batman 类的对象。

SayImBatman 函数可以在该类的任何对象上调用。由于它与 Batman 类相关,因此不将其称为常规函数,而是将其称为类的方法

我认为这应该足以让您清楚 OOP 的基础知识,以便您继续进行下一部分,我们将在其中介绍 OOP 的四大支柱。

遗产

继承引入了 OOP 中 parentchild 类的概念。子类是从父类派生的类,并继承其所有方法和属性(数据)。让我们看一些代码来帮助我们理解这一点:

package main

import "fmt"

type Hero struct {
    team string
}

type Batman struct {
    Hero
    name string
}

type Ironman struct {
    Hero
    power int
}

func (h Hero) SayTeam() {
    fmt.Println("My Team is", h.team)
}

func (b Batman) SayImBatman() {
    fmt.Printf("I'm %s and I'm Batman\n", b.name)
}

func (i Ironman) SayPowerLevel() {
    fmt.Printf("I'm Ironman and my powerlevel is %d\n", i.power)
}

func main() {
    b1 := Batman{Hero{team: "Justice League"}, "Christian Bale"}
    i1 := Ironman{Hero{team: "Avengers"}, 23}

    b1.SayImBatman()
    b1.SayTeam()

    i1.SayPowerLevel()
    i1.SayTeam()
}

在此示例中,蝙蝠侠和钢铁侠是 Hero 父类的子类。他们可以访问其父类的属性(即 team)及其方法(即 SayTeam)。正如您在声明 b1 和 i1 实例时所看到的那样,我们指定了父类属性以及它们各自类的特定属性。它们都能够调用父类中定义的 SayTeam 方法。但它们也有各自独特的属性和方法。

Golang 使用组合(在结构体中使用结构体)来实现继承。它不像其他 OOP 语言(例如 C 或 Java)那样具有内置的基于类的继承。

封装

封装是隐藏对象内部属性并且不允许直接修改它们的原理。相反,它依赖于提供获取和更新这些属性的方法。让我们看一个例子来更好地理解这一点:

package main

import "fmt"

type Batman struct {
    actor string
    year int
}

func (b Batman) SayImBatman() {
    fmt.Printf("I'm %s and I'm Batman from year %d\n", b.actor, b.year)
}

func main() {
    b1 := Batman{actor: "Michael Keaton", year: 1989}
    b2 := Batman{actor: "Christian Bale", year: 2005}

    b1.SayImBatman()
    b2.SayImBatman()
}

package main

import "fmt"

type Hero struct {
    team string
}

type Batman struct {
    Hero
    name string
}

type Ironman struct {
    Hero
    power int
}

func (h Hero) SayTeam() {
    fmt.Println("My Team is", h.team)
}

func (b Batman) SayImBatman() {
    fmt.Printf("I'm %s and I'm Batman\n", b.name)
}

func (i Ironman) SayPowerLevel() {
    fmt.Printf("I'm Ironman and my powerlevel is %d\n", i.power)
}

func main() {
    b1 := Batman{Hero{team: "Justice League"}, "Christian Bale"}
    i1 := Ironman{Hero{team: "Avengers"}, 23}

    b1.SayImBatman()
    b1.SayTeam()

    i1.SayPowerLevel()
    i1.SayTeam()
}

在Golang中,从包中导出的属性和方法以大写字母开头。当我们在utils包中定义小写的actor和year时,我们确保它们不能被直接修改。相反,就像您在 main.go 文件中看到的那样,您需要使用导出的方法(以大写字母开头) - GetActor、SetActor 等来获取和修改它们。

这就是封装的全部内容 - 确保防止数据意外更改,并提供与数据安全交互的方法。

您会注意到不同的一件事是,在 Batman 类的所有方法中,我们使用指针接收器 *Batman 而不是像前面示例中那样使用值接收器 Batman。这是因为我们希望能够在 Set 方法中修改原始结构。在 Golang 中,最佳实践是,如果某些方法需要指针接收器,则让所有方法都使用指针接收器以保持一致性。这就是为什么 Get 方法也使用指针接收器,即使它们没有修改原始结构。

此外,还要注意的一件事是,仅仅因为我们使用的是指针接收器,我们就不必这样做:(&b1).GetActor。在 Golang 中,带有指针参数的函数必须采用指针,但带有指针接收器的方法可以采用值或指针作为接收器。

TL;DR:Golang 自动将 b1.GetActor 转换为 (&b1).GetActor,因为 GetActor 方法有一个指针接收器,但如果 GetActor 是一个普通函数,它不会将 GetActor(b1) 转换为 GetActor(&b1)一个指针参数。

多态性和抽象

OOP 的接下来两个支柱可以合并在一起,因为它们的代码示例看起来非常相似。多态性是指两个不同类的两个不同对象可以被视为同一公共超类的对象的编程实践。这意味着您可以在两个不同的对象上调用相同的函数,就好像它们是同一类的对象一样。这应该开始让您感受到所涉及的接口的味道:)

让我们看一些代码来更好地理解这一点:

package main

import "fmt"

type Batman struct {
    actor string
    year int
}

func (b Batman) SayImBatman() {
    fmt.Printf("I'm %s and I'm Batman from year %d\n", b.actor, b.year)
}

func main() {
    b1 := Batman{actor: "Michael Keaton", year: 1989}
    b2 := Batman{actor: "Christian Bale", year: 2005}

    b1.SayImBatman()
    b2.SayImBatman()
}

在此示例中,StartFight 函数可以传递 b1 和 i1 对象,即使它们彼此没有任何关系。尝试理解这与继承有何不同,在继承中,子类可以访问父类的方法。在此示例中,没有子类和父类(也没有共享方法)。相反,两个不同的对象被函数视为相同:这称为多态性。

现在,这也可以被视为抽象的一个例子。顾名思义,抽象是一种隐藏实现细节,而只是提供为您处理事务的函数的编程实践。在这个例子中,你不需要关心单个英雄的方法是如何配置的。当您想使用任何英雄的战斗功能时,您可以随时继续使用开始战斗功能。这样,实现细节对用户来说是隐藏的,只暴露基本的细节。

现在回到多态,有两个更常见的例子,那就是方法重写和重载。

方法重写

方法重写是指子类定义自己的父类中定义的方法的实现。现在使用此实现来代替原始父类的实现。让我们看看之前用于继承的代码,看看它在方法重写后的样子:

package main

import "fmt"

type Hero struct {
    team string
}

type Batman struct {
    Hero
    name string
}

type Ironman struct {
    Hero
    power int
}

func (h Hero) SayTeam() {
    fmt.Println("My Team is", h.team)
}

func (b Batman) SayImBatman() {
    fmt.Printf("I'm %s and I'm Batman\n", b.name)
}

func (i Ironman) SayPowerLevel() {
    fmt.Printf("I'm Ironman and my powerlevel is %d\n", i.power)
}

func main() {
    b1 := Batman{Hero{team: "Justice League"}, "Christian Bale"}
    i1 := Ironman{Hero{team: "Avengers"}, 23}

    b1.SayImBatman()
    b1.SayTeam()

    i1.SayPowerLevel()
    i1.SayTeam()
}

该程序的输出是:

//oops-in-go/utils/utils.go

package utils

type Batman struct {
    actor string
    year int
}

func (b *Batman) GetActor() string {
    return b.actor
}

func (b *Batman) GetYear() int {
    return b.year
}

func (b *Batman) SetActor(actor string) {
    b.actor = actor
}

func (b *Batman) SetYear(year int) {
    b.year = year
}

Batman 类的对象现在使用自己的 SayTeam 方法,而不是父 Hero 类的方法。由于 Ironman 类没有自己的 SayTeam 方法,因此它的对象仍然使用其父类的方法。这就是方法重写的意思,子类“重写”父类中定义的方法。

方法重载

这是指同一个函数能够接受多个不同的参数。这些参数的数量或类型可能不同。 Golang 提供了两种方法来实现这一点:通过可变参数函数,另一种通过接口。

让我们看一下两者的代码,这将帮助您更好地理解:

使用可变参数函数

// oops-in-go/main.go

package main

import (
    "fmt"
    "oops-in-go/utils"
)

func main() {
    b1 := utils.Batman{}
    b1.SetActor("Michael Keaton")
    b1.SetYear(1989)
    fmt.Printf("I'm %s and I'm Batman from year %d\n", b1.GetActor(), b1.GetYear())

    b1.SetActor("Christian Bale")
    b1.SetYear(2005)
    fmt.Printf("I'm %s and I'm Batman from year %d\n", b1.GetActor(), b1.GetYear())
}

在这里,您可以使用任意数量个参数“重载”listMembers函数。

使用接口

package main

import "fmt"

type Hero interface {
    Fight()
}

type Batman struct {
    weapon string
}

type Ironman struct {
    weapon string
}

func (b Batman) Fight() {
    fmt.Printf("Batman hits with a %s\n", b.weapon)
}

func (i Ironman) Fight() {
    fmt.Printf("Ironman hits with a %s\n", i.weapon)
}

func StartFight(h Hero) {
    fmt.Println("Fight has started.")
    h.Fight()
}

func main() {
    b1 := Batman{"Batarang"}
    i1 := Ironman{"Repulsor rays"}

    StartFight(b1)
    StartFight(i1)
}

该程序的输出是:

package main

import "fmt"

type Hero struct {
    team string
}

type Batman struct {
    Hero
    name string
}

type Ironman struct {
    Hero
    power int
}

func (h Hero) SayTeam() {
    fmt.Println("My Team is", h.team)
}

func (b Batman) SayImBatman() {
    fmt.Printf("I'm %s and I'm Batman\n", b.name)
}

func (i Ironman) SayPowerLevel() {
    fmt.Printf("I'm Ironman and my powerlevel is %d\n", i.power)
}

func (b Batman) SayTeam() {
    fmt.Printf("I'm Batman and my team is %s\n", b.team)
}

func main() {
    b1 := Batman{Hero{team: "Justice League"}, "Christian Bale"}
    i1 := Ironman{Hero{team: "Avengers"}, 23}

    b1.SayImBatman()
    b1.SayTeam()

    i1.SayPowerLevel()
    i1.SayTeam()
}

这里我们“重载”了 saySomething 方法来接受不同类型的参数。我们采用一个空接口作为参数,它可以是任何类型,然后使用 switch case 检查其类型并相应地打印输出。

结论

我很清楚这是一篇很长的文章,如果您坚持读到最后,我想让您知道我真的很高兴:) 我真诚地希望您学到很多有关面向对象编程的新知识以及如何在 Golang 中实现它。我在我的网站上撰写有关不同技术概念的博客,如果您有兴趣学习新事物,我建议您订阅我的时事通讯。

以上是Golang 中的面向对象编程 (OOP) 简介的详细内容。更多信息请关注PHP中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn