• 100天精通Golang(基础入门篇)——第21天:Go语言面向对象(OOP)核心概念解析


    在这里插入图片描述


    🌷🍁 博主猫头虎🐅🐾 带您进入 Golang 语言的新世界✨✨🍁
    🦄 博客首页——🐅🐾猫头虎的博客🎐
    🐳 《面试题大全专栏》 🦕 文章图文并茂🦖生动形象🐅简单易学!欢迎大家来踩踩~🌺
    🌊 《IDEA开发秘籍专栏》 🐾 学会IDEA常用操作,工作效率翻倍~💐
    🌊 《100天精通Golang(基础入门篇)》 🐅 学会Golang语言,畅游云原生领域,无厂不可去~💐

    🪁🍁 希望本文能给您带来价值🌸如果有任何不足,欢迎批评指正!🐅🐾🍁🐥


    100天精通Golang(基础入门篇)——第21天:Go语言中的面向对象(OOP)思想

    摘要 📖:

    Go 语言为开发者提供了一种与传统 OOP 语言不同的实现面向对象概念的方式。尽管 Go 不支持类,但它通过结构体、接口、组合等手段模拟了面向对象的主要特性。

    引言 🌟:

    在编程世界中,面向对象编程 (OOP) 是一种非常受欢迎的设计和开发方法。Go 语言,作为一个现代的编程语言,也提供了一套独特的工具和概念来实现 OOP,尽管它不完全遵循传统的 OOP 模型。

    go并不是一个纯面向对象的编程语言。在go中的面向对象,结构体替换了类。

    Go并没有提供类class,但是它提供了结构体struct,方法method,可以在结构体上添加。提供了捆绑数据和方法的行为,这些数据和方法与类类似。

    1.1 定义结构体和方法

    通过以下代码来更好的理解,首先在src目录下创建一个package命名为oop,在oop目录下,再创建一个子目录命名为employee,在该目录下创建一个go文件命名为employee.go。

    目录结构:oop -> employee -> employee.go

    在employee.go文件中保存以下代码:

    package employee
    
    import (  
        "fmt"
    )
    
    type Employee struct {  
        FirstName   string
        LastName    string
        TotalLeaves int
        LeavesTaken int
    }
    
    func (e Employee) LeavesRemaining() {  
        fmt.Printf("%s %s has %d leaves remaining", e.FirstName, e.LastName, (e.TotalLeaves - e.LeavesTaken))
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    然后在oop目录下,创建文件并命名为main.go,并保存以下内容

    package main
    
    import "oop/employee"
    
    func main() {  
        e := employee.Employee {
            FirstName: "Sam",
            LastName: "Adolf",
            TotalLeaves: 30,
            LeavesTaken: 20,
        }
        e.LeavesRemaining()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    运行结果:

    Sam Adolf has 10 leaves remaining 
    
    • 1

    1.2 New()函数替代了构造函数

    我们上面写的程序看起来不错,但是里面有一个微妙的问题。让我们看看当我们用0值定义employee struct时会发生什么。更改main的内容。转到下面的代码,

    package main
    
    import "oop/employee"
    
    func main() {  
        var e employee.Employee
        e.LeavesRemaining()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    运行结果:

    has 0 leaves remaining
    
    • 1

    通过运行结果可以知道,使用Employee的零值创建的变量是不可用的。它没有有效的名、姓,也没有有效的保留细节。在其他的OOP语言中,比如java,这个问题可以通过使用构造函数来解决。使用参数化构造函数可以创建一个有效的对象。

    go不支持构造函数。如果某个类型的零值不可用,则程序员的任务是不导出该类型以防止其他包的访问,并提供一个名为NewT(parameters)的函数,该函数初始化类型T和所需的值。在go中,它是一个命名一个函数的约定,它创建了一个T类型的值给NewT(parameters)。这就像一个构造函数。如果包只定义了一个类型,那么它的一个约定就是将这个函数命名为New(parameters)而不是NewT(parameters)。

    更改employee.go的代码:

    首先修改employee结构体为非导出,并创建一个函数New(),它将创建一个新Employee。代码如下:

    package employee
    
    import (  
        "fmt"
    )
    
    type employee struct {  
        firstName   string
        lastName    string
        totalLeaves int
        leavesTaken int
    }
    
    func New(firstName string, lastName string, totalLeave int, leavesTaken int) employee {  
        e := employee {firstName, lastName, totalLeave, leavesTaken}
        return e
    }
    
    func (e employee) LeavesRemaining() {  
        fmt.Printf("%s %s has %d leaves remaining", e.firstName, e.lastName, (e.totalLeaves - e.leavesTaken))
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    我们在这里做了一些重要的改变。我们已经将Employee struct的起始字母e设置为小写,即我们已经将类型Employee struct更改为type Employee struct。通过这样做,我们成功地导出了employee结构并阻止了其他包的访问。将未导出的结构的所有字段都导出为未导出的方法是很好的做法,除非有特定的需要导出它们。由于我们不需要在包之外的任何地方使用employee struct的字段,所以我们也没有导出所有字段。

    由于employee是未导出的,所以不可能从其他包中创建类型employee的值。因此,我们提供了一个输出的新函数。将所需的参数作为输入并返回新创建的employee。

    这个程序还需要做一些修改,让它能够工作,但是让我们运行这个程序来了解到目前为止变化的效果。如果这个程序运行,它将会失败,有以下编译错误,

    go/src/constructor/main.go:6: undefined: employee.Employee  
    
    • 1

    这是因为我们有未导出的Employee,因此编译器抛出错误,该类型在main中没有定义。完美的。正是我们想要的。现在没有其他的包能够创建一个零值的员工。我们成功地防止了一个无法使用的员工结构价值被创建。现在创建员工的唯一方法是使用新功能。

    修改main.go代码

    package main  
    
    import "oop/employee"
    
    func main() {  
        e := employee.New("Sam", "Adolf", 30, 20)
        e.LeavesRemaining()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    运行结果:

    Sam Adolf has 10 leaves remaining 
    
    • 1

    因此,我们可以明白,虽然Go不支持类,但是结构体可以有效地使用,在使用构造函数的位置,使用New(parameters)的方法即可。

    1.3组成(Composition )替代了继承(Inheritance)

    Go不支持继承,但它支持组合。组合的一般定义是“放在一起”。构图的一个例子就是汽车。汽车是由轮子、发动机和其他各种部件组成的。

    博客文章就是一个完美的组合例子。每个博客都有标题、内容和作者信息。这可以用组合完美地表示出来。

    1.3.1 通过嵌入结构体实现组成

    可以通过将一个struct类型嵌入到另一个结构中实现。

    示例代码:

    package main
    
    import (  
        "fmt"
    )
    
    /*
    我们创建了一个author struct,它包含字段名、lastName和bio。我们还添加了一个方法fullName(),将作者作为接收者类型,这将返回作者的全名。
    */
    type author struct {  
        firstName string
        lastName  string
        bio       string
    }
    
    func (a author) fullName() string {  
        return fmt.Sprintf("%s %s", a.firstName, a.lastName)
    }
    /*
    post struct有字段标题、内容。它还有一个嵌入式匿名字段作者。这个字段表示post struct是由author组成的。现在post struct可以访问作者结构的所有字段和方法。我们还在post struct中添加了details()方法,它打印出作者的标题、内容、全名和bio。
    */
    type post struct {  
        title     string
        content   string
        author
    }
    
    func (p post) details() {  
        fmt.Println("Title: ", p.title)
        fmt.Println("Content: ", p.content)
        fmt.Println("Author: ", p.author.fullName())
        fmt.Println("Bio: ", p.author.bio)
    }
    
    func main() {  
        author1 := author{
            "Naveen",
            "Ramanathan",
            "Golang Enthusiast",
        }
        post1 := post{
            "Inheritance in Go",
            "Go supports composition instead of inheritance",
            author1,
        }
        post1.details()
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    运行结果:

    Title:  Inheritance in Go  
    Content:  Go supports composition instead of inheritance  
    Author:  Naveen Ramanathan  
    Bio:  Golang Enthusiast  
    
    • 1
    • 2
    • 3
    • 4

    嵌入结构体的切片

    在以上程序的main函数下增加以下代码,并运行

    type website struct {  
            []post
    }
    func (w website) contents() {  
        fmt.Println("Contents of Website\n")
        for _, v := range w.posts {
            v.details()
            fmt.Println()
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    运行报错:

    main.go:31:9: syntax error: unexpected [, expecting field name or embedded type 
    
    • 1

    这个错误指向structs []post的嵌入部分。原因是不可能匿名嵌入一片。需要一个字段名。我们来修正这个错误,让编译器通过。

    type website struct {  
            posts []post
    }
    
    • 1
    • 2
    • 3

    现在让我们修改的main函数,为我们的新的website创建几个posts。修改完完整代码如下:

    package main
    
    import (  
        "fmt"
    )
    
    type author struct {  
        firstName string
        lastName  string
        bio       string
    }
    
    func (a author) fullName() string {  
        return fmt.Sprintf("%s %s", a.firstName, a.lastName)
    }
    
    type post struct {  
        title   string
        content string
        author
    }
    func (p post) details() {  
        fmt.Println("Title: ", p.title)
        fmt.Println("Content: ", p.content)
        fmt.Println("Author: ", p.fullName())
        fmt.Println("Bio: ", p.bio)
    }
    
    type website struct {  
     posts []post
    }
    func (w website) contents() {  
        fmt.Println("Contents of Website\n")
        for _, v := range w.posts {
            v.details()
            fmt.Println()
        }
    }
    func main() {  
        author1 := author{
            "Naveen",
            "Ramanathan",
            "Golang Enthusiast",
        }
        post1 := post{
            "Inheritance in Go",
            "Go supports composition instead of inheritance",
            author1,
        }
        post2 := post{
            "Struct instead of Classes in Go",
            "Go does not support classes but methods can be added to structs",
            author1,
        }
        post3 := post{
            "Concurrency",
            "Go is a concurrent language and not a parallel one",
            author1,
        }
        w := website{
            posts: []post{post1, post2, post3},
        }
        w.contents()
    }   
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64

    运行结果:

    Contents of Website
    
    Title:  Inheritance in Go  
    Content:  Go supports composition instead of inheritance  
    Author:  Naveen Ramanathan  
    Bio:  Golang Enthusiast
    
    Title:  Struct instead of Classes in Go  
    Content:  Go does not support classes but methods can be added to structs  
    Author:  Naveen Ramanathan  
    Bio:  Golang Enthusiast
    
    Title:  Concurrency  
    Content:  Go is a concurrent language and not a parallel one  
    Author:  Naveen Ramanathan  
    Bio:  Golang Enthusiast  
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    1.4 多态性(Polymorphism)

    Go中的多态性是在接口的帮助下实现的。正如我们已经讨论过的,接口可以在Go中隐式地实现。如果类型为接口中声明的所有方法提供了定义,则实现一个接口。让我们看看在接口的帮助下如何实现多态。

    任何定义接口所有方法的类型都被称为隐式地实现该接口。

    类型接口的变量可以保存实现接口的任何值。接口的这个属性用于实现Go中的多态性。

    举个例子,一个虚构的组织有两种项目的收入:固定的账单和时间和材料。组织的净收入是由这些项目的收入之和计算出来的。为了保持本教程的简单,我们假设货币是美元,我们不会处理美分。它将使用整数来表示。

    首先我们定义一个接口:Income

    type Income interface {  
        calculate() int
        source() string
    }
    
    • 1
    • 2
    • 3
    • 4

    接下来,定义两个结构体:FixedBilling和TimeAndMaterial

    type FixedBilling struct {  
        projectName string
        biddedAmount int
    }
    
    • 1
    • 2
    • 3
    • 4
    type TimeAndMaterial struct {  
        projectName string
        noOfHours  int
        hourlyRate int
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    下一步是定义这些结构体类型的方法,计算并返回实际收入和收入来源。

    func (fb FixedBilling) calculate() int {  
        return fb.biddedAmount
    }
    
    func (fb FixedBilling) source() string {  
        return fb.projectName
    }
    
    func (tm TimeAndMaterial) calculate() int {  
        return tm.noOfHours * tm.hourlyRate
    }
    
    func (tm TimeAndMaterial) source() string {  
        return tm.projectName
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    接下来,我们来声明一下计算和打印总收入的calculateNetIncome函数。

    func calculateNetIncome(ic []Income) {  
        var netincome int = 0
        for _, income := range ic {
            fmt.Printf("Income From %s = $%d\n", income.source(), income.calculate())
            netincome += income.calculate()
        }
        fmt.Printf("Net income of organisation = $%d", netincome)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    上面的calculateNetIncome函数接受一部分Income接口作为参数。它通过遍历切片和调用calculate()方法来计算总收入。它还通过调用source()方法来显示收入来源。根据收入接口的具体类型,将调用不同的calculate()和source()方法。因此,我们在calculateNetIncome函数中实现了多态。

    在未来,如果组织增加了一种新的收入来源,这个函数仍然可以正确地计算总收入,而没有一行代码更改。

    最后我们写以下主函数:

    func main() {  
        project1 := FixedBilling{projectName: "Project 1", biddedAmount: 5000}
        project2 := FixedBilling{projectName: "Project 2", biddedAmount: 10000}
        project3 := TimeAndMaterial{projectName: "Project 3", noOfHours: 160, hourlyRate: 25}
        incomeStreams := []Income{project1, project2, project3}
        calculateNetIncome(incomeStreams)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    运行结果:

    Income From Project 1 = $5000  
    Income From Project 2 = $10000  
    Income From Project 3 = $4000  
    Net income of organisation = $19000  
    
    • 1
    • 2
    • 3
    • 4

    假设该组织通过广告找到了新的收入来源。让我们看看如何简单地添加新的收入方式和计算总收入,而不用对calculateNetIncome函数做任何更改。由于多态性,这样是可行的。

    首先让我们定义Advertisement类型和calculate()和source()方法。

    type Advertisement struct {  
        adName     string
        CPC        int
        noOfClicks int
    }
    
    func (a Advertisement) calculate() int {  
        return a.CPC * a.noOfClicks
    }
    
    func (a Advertisement) source() string {  
        return a.adName
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    广告类型有三个字段adName, CPC(cost per click)和noof点击数(cost per click)。广告的总收入是CPC和noOfClicks的产品。

    修改主函数:

    func main() {  
        project1 := FixedBilling{projectName: "Project 1", biddedAmount: 5000}
        project2 := FixedBilling{projectName: "Project 2", biddedAmount: 10000}
        project3 := TimeAndMaterial{projectName: "Project 3", noOfHours: 160, hourlyRate: 25}
        bannerAd := Advertisement{adName: "Banner Ad", CPC: 2, noOfClicks: 500}
        popupAd := Advertisement{adName: "Popup Ad", CPC: 5, noOfClicks: 750}
        incomeStreams := []Income{project1, project2, project3, bannerAd, popupAd}
        calculateNetIncome(incomeStreams)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    运行结果:

    Income From Project 1 = $5000  
    Income From Project 2 = $10000  
    Income From Project 3 = $4000  
    Income From Banner Ad = $1000  
    Income From Popup Ad = $3750  
    Net income of organisation = $23750 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    综上,我们没有对calculateNetIncome函数做任何更改,尽管我们添加了新的收入方式。它只是因为多态性而起作用。由于新的Advertisement类型也实现了Income接口,我们可以将它添加到incomeStreams切片中。calculateNetIncome函数也在没有任何更改的情况下工作,因为它可以调用Advertisement类型的calculate()和source()方法。

    结论:

    关键点:

    1. 结构体替代类: Go语言没有类的概念,但结构体可以起到相同的作用。可以在结构体上定义方法,从而模拟类的行为。

    2. 构造函数的替代: Go不支持构造函数。但可以提供一个New()函数,来初始化并返回一个结构体的实例。

    3. 组合替代继承: Go不支持继承。但可以通过嵌入结构体的方式实现组合,从而达到类似继承的效果。

    4. 多态性: 在Go中,多态是通过接口实现的。任何结构体只要实现了接口的所有方法,都被认为实现了该接口。这意味着,可以用接口类型的变量来持有这些结构体的实例,并调用它们的方法。

    这些关键点为Go开发者提供了一种不同于传统OOP语言的方式来实现面向对象的概念。

    补充内容

    1. 接口值与nil:
      当声明一个接口类型的变量并未初始化时,它的值是nil。对于nil的接口值,调用其上的方法会导致运行时错误。因此,需要在调用其方法前检查其是否为nil。

    2. 接口嵌套:
      Go语言也支持接口嵌套。这意味着一个接口可以包含其他接口,从而使得实现该接口的结构体需要实现多个接口中的所有方法。

    3. 类型断言:
      当你持有一个接口值,并想知道其具体的类型或者想将其转为具体的类型,可以使用类型断言。例如,value, ok := interfaceValue.(ConcreteType)

    4. 方法重写与接口:
      当一个结构体嵌套另一个结构体并且这两个结构体都实现了相同的方法时,嵌套结构体的方法会被外部结构体的方法覆盖。这为方法重写提供了一种机制。

    5. 私有与公有:
      Go通过首字母的大小写来决定变量、方法或结构体的可见性。首字母大写的是公有的,可以被其他包访问;首字母小写的是私有的,只能在当前包中访问。

    通过这些补充内容,我们可以更全面地了解Go语言中的面向对象思想。

    对比记忆:

    功能/语言GoJavaC++Python
    不支持(使用结构体代替)支持支持支持
    继承通过组合实现支持单继承支持多继承支持多继承
    接口支持(隐式实现)支持(显式实现)通过纯虚函数实现通过抽象基类实现
    构造函数不直接支持(使用New函数)支持支持支持(__init__
    多态通过接口实现通过接口实现通过虚函数实现通过方法重写实现
    封装通过首字母大小写实现公有/私有使用访问修饰符使用访问修饰符通过下划线前缀实现
    方法重载不支持支持支持不支持
    析构函数不直接支持支持(finalizer)支持(destructor)支持(__del__

    总结 📝:

    Go 语言提供了一套独特的工具和方法来实现面向对象的思想:

    1. 结构体替代类: 在 Go 中,结构体可以起到类似类的作用,提供方法定义。
    2. 构造函数的替代: 尽管 Go 不支持构造函数,但它鼓励使用 New() 函数来初始化和返回结构体实例。
    3. 组合替代继承: Go 不支持继承,但可以通过嵌入结构体的方式实现组合。
    4. 多态性: Go 通过接口实现多态性,任何结构体只要实现了接口的所有方法,都被认为实现了该接口。

    参考资料 📚:

    1. 《100天精通Golang(基础入门篇)》 - 第21天:Go语言中的面向对象(OOP)思想 by 猫头虎博主.
    2. Go 官方文档
    3. 《Go 语言圣经》

    💼 文章整理者:猫头虎博主 🐅
    📅 日期:2023-09-12 📆

    在这里插入图片描述

    结语

    通过今天的学习,您已经踏上了Golang的学习之旅。在未来的日子里,您将探索Golang的各个方面,从基础概念到高级技巧,从实际应用到性能优化。
    学习一门编程语言是一个持续的过程,每一天都是您向Golang的精通迈进的重要一步。我鼓励您坚持每天学习,保持热情和好奇心,解决挑战并享受成功的喜悦。

    在您的学习旅程中,不要忘记参与社区和与其他Golang开发者交流。分享您的见解和经验,向他人学习,并在开源项目或实际应用中展示您的技能。

    如果您在学习过程中遇到困难或有任何问题,不要犹豫向社区和专家寻求帮助。持续学习,勇敢探索,您将在Golang领域取得令人瞩目的成就。

    最后,感谢您的阅读和支持!祝愿您在未来的每一天中都能够成为一名精通Golang的开发者!

    期待听到您在学习过程中的进展和成就。如果您需要进一步的帮助,请随时告诉我。祝您在学习Golang的旅程中取得巨大成功!

    点击下方名片,加入IT技术核心学习团队。一起探索科技的未来,共同成长。

    如果您在学习过程中有任何疑惑,请点击下方名片,带您一对一快速入门 Go语言 的世界 ~

  • 相关阅读:
    react 父组件调用子组件的属性或方法
    基于C#的消息处理的应用程序 - 开源研究系列文章
    美国网站服务器SSL证书介绍
    2023烟台大学计算机考研信息汇总
    GO 语言如何用好变长参数?
    质量评估模型助力风险决策水平提升
    商户订单信息语音通知功能如何实现?
    NFT的中国化改良尝试
    华为认证HCIA H12-811 Datacom数通考试真题题库【带答案刷题必过】【第一部分】
    k8s部署针对外部服务器的prometheus服务
  • 原文地址:https://blog.csdn.net/qq_44866828/article/details/132835632