Go 是面向对象的吗?
Go不是一种纯面向对象的编程语言。这段摘自 Go FAQs 回答了 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 )相比,它们中的一些在实现上有很大的不同。
Structs 代替类
Go 不提供 classes ,但提供 structs。Methods 可以添加到结构体上。这提供了将数据和方法捆绑在一起的行为,类似于类。
让我们马上开始一个例子。
我们将在此示例中创建一个自定义包,因为它有助于更好地理解结构如何能够有效地替代类。
在 ~/Documents/ 创建一个子文件夹,并将其命名为 oop
。
让我们初始化一个名为 oop 的 go module。在 oop 目录下输入以下命令来创建一个名为 oop 的 go mod。
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", e.FirstName, e.LastName, (e.TotalLeaves - e.LeavesTaken))
}
在上面的程序中,第一行指定该文件属于 employee
package。第 7 行声明 Employee 结构。第 14 行的 Employee 结构中添加了一个名为 LeavesRemaining
的方法。 这将计算并显示员工剩余的休假数。现在我们有了一个结构体和一个方法,该方法对捆绑在一起的结构体进行操作,类似于类。
在 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()
}
我们在第 3 行中导入 employee
包。Employee
结构的 LeavesRemaining()
方法在main()
函数第 12 行调用。
此程序无法在 playground 上运行,因为它有自定义的 package。如果你在本地运行这个程序,在 oop
路径下终端输入 go install oop
,程序将输出,
Sam Adolf has 10 leaves remaining
如果你不清楚如何运行此程序,请访问 https://golangbot.com/hello-world-gomod/ 了解更多。
New() 函数代替构造函数
我们上面写的程序看起来不错,但它有一个小问题。让我们看看当我们定义零值的employee 结构时会发生什么。将 main.go 的内容更改为以下代码,
package main
import "oop/employee"
func main() {
var e employee.Employee
e.LeavesRemaining()
}
我们所做的唯一改变是在第6行创建零值 Employee
。该程序将输出,
has 0 leaves remaining
如你所见,使用 Employee
的零值创建的变量不可用。它没有有效的名字,姓氏,也没有有效的休假详情。
在像 Java 这样的其他 OOP 语言中,这个问题可以通过使用构造函数来解决。可以使用参数化构造函数创建有效对象。
Go 不支持构造函数。如果类型的零值不可用,则程序员的工作是取消导出类型以防止从其他 package 访问,并提供 NewT(parameters)
的函数,该函数使用所需的值来初始化类型 T
。 在 Go 中,给一个函数命名是一种惯例,它给 NewT(parameters)
创建一个 T
类型的值。这将像一个构造函数。如果 package 只定义了一种类型,那么 Go 的惯例就是将此函数命名为 New(parameters)
而不是 NewT(parameters)
。
让我们对我们编写的程序进行修改,这样每次创建一个员工时,它都是可用的。
第一步是取消 Employee
结构的导出,并创建一个 New()
函数,该函数将创建一个新的 Em``ployee
。替换 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", e.firstName, e.lastName, (e.totalLeaves - e.leavesTaken))
}
我们在这方面做了一些重要的改变。我们把 Employee struct 的开头字母 e
改成了小写,也就是说我们把类型 Employee struct 改成了 type employee struct
。这样做我们成功地取消了 employee 结构的导出,并阻止了来自其他包的访问。将未导出结构的所有字段也设置为未导出,这是一个很好的实践,除非需要导出它们。由于我们不需要 employee 结构的字段在包之外的任何地方,所以我们也取消了所有字段的导出。
我们在 LeavesRemaining()
方法中相应地更改了字段名。
由于 employee
没有导出,所以不可能从其他包创建 Employee
类型的值。因此,我们在第一行中提供了一个导出的 New
函数。它接受所需的参数作为输入,并返回一个新创建的 employee。
这个程序仍然需要进行一些更改才能正常工作,但是让我们运行这个程序来获取目前为止更改的效果。如果运行此程序,它将失败,并出现以下编译错误
go/src/constructor/main.go:6: undefined: employee.Employee
这是因为我们有未导出的 Employee
,因此编译器会抛出一个错误,该类型在 main.go
中没有定义。这正是我们想要的。现在没有其他包能够创建零值 employee
。我们成功地阻止了创建不可用的 employee struct 值。现在创建 employee 的惟一方法是使用 New
函数。
main.go
代码修改如下
package main
import "oop/employee"
func main() {
e := employee.New("Sam", "Adolf", 30, 20)
e.LeavesRemaining()
}
对该文件的唯一更改在第 6 行, 通过将所需的参数传递给 New
函数,我们创建了一个新员工。
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", 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 不支持类,但是可以有效地使用 struct 来代替类,并且可以使用 New(parameters)
的方法来代替构造函数。