跟我一起来学golang之《面向对象》

335 阅读5分钟

这是我参与8月更文挑战的第15天,活动详情查看:8月更文挑战

我们学习过java的知道什么是面向对象语言,以及面向对象语言的特点,到那时go并不是一个纯面向对象的编程语言。在go中提供了一些方式方法尽量向面向对象靠拢,我们之前学习了结构体,其实结构体就是替换了面向对象的类的概念。

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

定义结构体和方法

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

我们再main.go文件中

package main

import "oop/employee"

func main() {  
    e := employee.Employee {
        FirstName: "Sam",
        LastName: "Adolf",
        TotalLeaves: 30,
        LeavesTaken: 20,
    }
    e.LeavesRemaining()
}

运行结果:

Sam Adolf has 10 leaves remaining

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

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

package main

import "oop/employee"

func main() {  
    var e employee.Employee
    e.LeavesRemaining()
}

运行结果:

has 0 leaves remaining

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

go不支持构造函数。如果某个类型的零值不可用,我们开发者需要提供一个类似构造函数一样的函数来保证其它包可以获取指定对象。

更改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))
}

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

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

修改main.go代码

package main  

import "oop/employee"

func main() {  
    e := employee.New("Sam", "Adolf", 30, 20)
    e.LeavesRemaining()
}

运行结果:

Sam Adolf has 10 leaves remaining

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

组合(Composition )替代了继承(Inheritance)

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

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

通过嵌入结构体实现组成

可以通过将一个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{
        "1",
        "2",
        "3",
    }
    post1 := post{
        "4",
        "5",
        author1,
    }
    post1.details()
}

嵌入结构体的切片

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

type website struct {  
        []post
}
func (w website) contents() {  
    fmt.Println("----------------\n")
    for _, v := range w.posts {
        v.details()
        fmt.Println()
    }
}

运行报错:

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

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

type website struct {  
        posts []post
}

现在让我们修改的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{
        "1",
        "2",
        "3",
    }
    post1 := post{
        "4",
        "5",
        author1,
    }
    post2 := post{
        "6",
        "7",
        author1,
    }
    post3 := post{
        "8",
        "9",
        author1,
    }
    w := website{
        posts: []post{post1, post2, post3},
    }
    w.contents()
}

多态性(Polymorphism)

多态最好理解的就是一句话:父类型的引用指向子类对象。Go中的多态性是在接口的帮助下实现的。我们直接举个例子就明白了:

我们先来创建一个interface,当然也是通过type关键字:

type Mammal interface {
 Say()
}

我们定义了一个Mammal的接口,当中声明了一个Say函数。也就是说只要是拥有这个函数的结构体就可以用这个接口来接收,我们和刚才一样,定义Cat、Dog和Human三个结构体,分别实现各自的Say方法:

type Dog struct{}

type Cat struct{}

type Human struct{}

func (d Dog) Say() {
 fmt.Println("woof")
}

func (c Cat) Say() {
 fmt.Println("meow")
}

func (h Human) Say() {
 fmt.Println("speak")
}

之后,我们尝试使用这个接口来接收各种结构体的对象,然后调用它们的Say方法:

func main() {
 var m Mammal
 m = Dog{}
 m.Say()
 m = Cat{}
 m.Say()
 m = Human{}
 m.Say()
}

这就是多态。其实跟java语言中的概念和含义类似。