Go中的面向对象:结构体而非类
本文是《Go系列教程》的第二十四篇文章。
Go是面向对象的吗?
Go并不是一个纯面向对象语言。这里有一段从Go的FAQ中摘录来的,询问Go是不是面向对象语言的答案。
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).
在接下来的章节中,我们将讨论如何使用Go来实现面向对象编程的概念。这其中有很多是和其他面向对象编程语言如Java是很不一样的。
结构体而非类
Go并不支持类,但是它支持结构体。也可以向结构体中添加方法。这样的话,利用结构体我们就可以把数据的操作和数据绑定在一起,这就很像Java中的类。为了更好地理解它,我们来写一个例子。
为了理解为何结构体可以有效第替代类,我们先创建一个自定义的包。在~/Documents/下创建一个子目录,并命名为oop。同时我们初始化一个名为oop的Go模块,在oop目录下输入以下命令即可创建一个名为oop的模块。
go mod init oop
在oop里面再创建一个employee子目录。在employee子目录下,创建一个employee.go文件。目录的结构如下:
├── Documents
│ └── oop
│ ├── employee
│ │ └── employee.go
│ └── go.mod
然后我们在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\n", e.FirstName, e.LastName, (e.TotalLeaves - e.LeavesTaken))
}
另外,在oop目录下,新建一个文件main.go,此时目录结构如下:
├── Documents
│ └── oop
│ ├── employee
│ │ └── employee.go
│ ├── go.mod
│ └── main.go
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()函数而不是构造函数
在上面,我们写的程序看样子没问题了,但是还有一点小问题。我们来看一下,当我们定义了一个零值的employee结构体时,都发生了什么。用下面这些代码,替换main.go中的代码。
package main
import "oop/employee"
func main() {
var e employee.Employee
e.LeavesRemaining()
}
上面程序的唯一变化,就是我们创建了一个零值的Employee。程序的输出如下:
has 0 leaves remaining
正如你看到的,Employee的零值变量毫无用处。它根本没有有效的字段。在面向对象的编程语言如java中,此问题可以使用构造函数解决。可以使用一个参数化的构造函数创建一个有效的对象。
Go不支持构造函数。如果某个类型的零值没用的话,编程人员就不应该输出此类型,以防止在其他包中访问。同时还应该提供一个名为NewT(parameters)函数负责初始化类型T。
Go的传统是如果你要创建一个函数,此函数用于完成类型T的初始化的话,你就把它命名为NewT(parameters)。这其实就和构造函数的作用一样。如果某个包中只有一个类型,我们只需要把函数的名字定义为New(parameters)而不必定义为NewT(parameters)。
我们把上面的程序修改下,第一步,就是不输出Employee结构体,并创建一个New()函数,此函数会创建一个新的Employee。使用下面的代码替换employee.go中的原有代码。
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\n", e.firstName, e.lastName, (e.totalLeaves - e.leavesTaken))
}
我们做了几点重要的修改。我们把Employee的起始字母变成小写,把type Employee struct 改变为 type employee struct。这样做之后,我们就可以不输出employee结构体,并防止在其他包中访问。
其他包中访问employee。除非有特定的需要要输出它们,否则的话,把一个非输出的结构体的所有字段设置为非输出是最好的选择。因为我们不需要在employee包的外面访问employee结构体的字段,所以我们把所有的字段都设置为非输出。
现在既然employee是非输出的,那就不可能在其他包中创建Employee类型的值。因此,我们提供了一个导出函数New,此函数可以接收参数,并返回一个新创建的employee。
运行此程序,编译器会报:
# oop
./main.go:6:8: undefined: employee.Employee
这是因为employee包中的employee是非导出的,它不能在main函数中访问。因此,编译器就扔了一个错。这正是我们希望的。这样的话,其他包就不能创建零值的employee。
我们已经成功地阻止了创建没用的employee结构体变量。创建employee的唯一方式就是使用New函数。
使用下面的代码替换main.go中的内容:
package main
import "oop/employee"
func main() {
e := employee.New("Sam", "Adolf", 30, 20)
e.LeavesRemaining()
}
上面程序的唯一变化之处,就是我们通过把必需的参数传递给New函数来完成employee的创建。
修改后的文件内容如下。
employee.go
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\n", e.firstName, e.lastName, (e.totalLeaves - e.leavesTaken))
}
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)可以用来替换类的构造函数。
感谢您的阅读,请留下您珍贵的反馈和评论。Have a good Day!
备注
本文系翻译之作原文博客地址
网友评论