结构体 vs 类

Golang是面向对象编程语言么?

Golang不是一个纯粹的面向对象编程语言。下面这段话摘录自Golang的FAQs,这段话解释了Golang是否是一种面向对象编程语言。

Yes and no. Although Go has types and methods and allows an object-oriented style of programming, there is no type hierarchy. The concept of “interface” in Go provides a different approach that we believe is easy to use and in some ways more general. There are also ways to embed types in other types to provide something analogous—but not identical—to subclassing. Moreover, methods in Go are more general than in C++ or Java: they can be defined for any sort of data, even built-in types such as plain, “unboxed” integers. They are not restricted to structs (classes). Also, the lack of a type hierarchy makes “objects” in Go feel much more lightweight than in languages such as C++ or Java.

在接下来的几节我们会讨论如何在Golang中实现面向对象编程。从前面的摘录也可以看出来,Golang中面向对象编程跟Java等其他编程语言不一样。

基于结构体而不是基于类

Golang并没有提供类(class),但是Golang提供了结构体,我们可以给结构体绑定方法。这其实就提供了一种类似class的机制,class本质上也将数据和方法绑定起来。

下面我们实现一个例子来理解下。

在Golang工作目录下面创建一个文件夹命名为oop。在oop文件夹下创建一个子文件夹employee,在employee文件夹下创建文件employee.go

文件目录结构如下:

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

这里第一行代码指明本文件属于包employee。接着声明了一个结构体类型Employee,然后给该结构体绑定了一个方法LeavesRemaining。在该方法内部我们访问了结构体变量e的数据,这其实跟类多少有点相像。

下面我们在oop文件夹下创建main.go文件,用来测试前面我们实现的结构体和方法。

现在整个文件目录结构如下:

workspacepath
    src
        oop
            employee
                employee.go
            main.go

main.go中输入如下代码:

package main

import "oop/employee"

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

这里我们导入了包employee,并且定义了一个结构体变量e,然后调用了前面定义的方法LeavesRemaining

通过如下命令执行:

go install oop
workspacepath/bin/oop

输出如下:

Sam Adolf has 10 leaves remaining

New()函数 vs 构造函数

前面的程序虽然能正常执行,但是有个小问题。比如我们将main.go的代码修改为如下所示,仅仅声明一个Employee类型的结构体变量但是不初始化:

package main

import "oop/employee"

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

这时的执行结果如下,注意has前面有两个空格:

  has 0 leaves remaining

由于我们仅声明了一下变量e,并未进行人工初始化,因此Golang会自动初始化为默认零值,这显然会存在隐患。在Java等其他编程语言中,我们可以通过构造函数来创建一个合法的对象,然后再在这个合法的对象上面调用方法,但是Golang并不支持构造函数。

虽然Golang中没有构造函数,但是我们可以实现一个普通函数来模拟构造函数。我们可以将前面导出的结构体类型Employee改为employee,即设置为不导出,这样就只能在本文件内部访问该结构体类型,然后创建一个导出的普通函数来构造一个结构体变量并返回。

Golang中的惯例是使用函数名New(parameters)或者NewT(parameters)来模拟一个构造函数。如果一个包中仅定义了一个类型,那么使用New(parameters),否则使用NewT(parameters)来加以区分。

下面我们将前面的程序升级下来体会下这种用法。

package employee

import "fmt"

type employee struct {
    firstName string
    lastName string
    totalLeaves int
    leavesTaken int
}

func New(firstName string, lastName string, totalLeaves int, leavesTaken int) employee {
    e := employee {firstName, lastName, totalLeaves, leavesTaken}
    return e
}

func (e employee) LeavesRemaining() {
    fmt.Printf("%s %s has %d leaves remaining", e.firstName, e.lastName, e.totalLeaves - e.leavesTaken)
}

这里我们首先将类型Employee改为employee,即设置为不导出,这样就无法在包外再访问该结构体变量,注意我们同时将结构体类型的各个属性也改为了小写,即设置为不导出(这在Golang中是一种很良好的编程模式,除非有特殊需求,否则不应该打破这种模式)。然后我们定义了一个模拟构造函数New,由于employee是非导出类型,因此在包外仅能通过New函数来构造一个合法的employee结构体类型的变量,这也就避免了前面可能会出现不合法Employee的问题。

下面我们升级下main.go的代码来测试下:

package main

import "oop/employee"

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

这里我们给New函数传入了合法参数,来创建了一个结构体变量。该程序输出如下:

Sam Adolf has 10 leaves remaining

因此,虽然Golang不支持类,但是我们可以通过结构体来模拟类,通过New或者NewT来模拟构造函数,这样来实现面向对象编程。

Last updated