Go 面向对象编程篇(二):类的定义、初始化和成员方法
上篇教程学院君简单给大家介绍了 Go 语言的类型系统,关于基础类型、复合类型以及值语义和引用语义我们前面在数据类型篇里面已经基本都介绍过了,接下来,我们就 Go 语言面向对象编程相关的特性展开介绍。
一、类的定义和初始化
Go 语言的面向对象编程与我们之前所熟悉的 PHP、Java 那一套完全不同,没有 class
、extends
、implements
之类的关键字和相应的概念,而是借助结构体来实现类的声明,比如要定义一个学生类,可以这么做:
type Student struct {
id uint
name string
male bool
score float64
}
类名为 Student
,并且包含了 id
、name
、male
、score
四个属性,Go 语言中也不支持构造函数、析构函数,取而代之地,可以通过定义形如 NewXXX
这样的全局函数(首字母大写)作为类的初始化函数:
func NewStudent(id uint, name string, male bool, score float64) *Student {
return &Student{id, name, male, score}
}
在这个函数中,我们通过传入的属性字段对 Student
类进行初始化并返回一个指向该类的指针,除此之外,还可以初始化指定字段:
func NewStudent(id uint, name string, score float64) *Student {
return &Student{id: id, name:name, score:score}
}
在 Go 语言中,未进行显式初始化的变量都会被初始化为该类型的零值,例如
bool
类型的零值为false
,int
类型的零值为 0,string
类型的零值为空字符串,float
类型的零值为0.0
。
然后我们可以在 main()
函数中调用这个 NewStudent
函数对 Student
类进行初始化:
student := NewStudent(1, "学院君", 100)
fmt.Println(student)
上述代码的打印结果如下:
二、定义类的成员方法
值方法
由于 Go 语言不支持 class
这样的代码块,要为 Go 类定义成员方法,需要在 func
和方法名之间声明方法所属的类型(有的地方将其称之为接收者声明),以 Student
类为例,要为其定义获取 name
值的方法,可以这么做:
func (s Student) GetName() string {
return s.name
}
这样一来,我们就可以在初始化 Student
类后,通过 GetName()
方法获取 name
值:
student := NewStudent(1, "学院君", 100)
fmt.Println("Name:", student.GetName())
可以看到,我们通过在函数签名中增加接收者声明的方式定义了函数所归属的类型,这个时候,函数就不再是普通的函数,而是类的成员方法了。
指针方法
在类的成员方法中,可以通过声明的类型变量来访问类的属性和其他方法(Go 语言不支持隐藏的 this
指针,所有的东西都是显式声明)。GetName
是一个只读方法,如果我们要在外部通过 Student
类暴露的方法设置 name
值,可以这么做:
func (s *Student) SetName(name string) {
s.name = name
}
你可能已经注意到,这里的方法声明和前面 GetXXX
方法声明不太一样,Student
类型设置成了指针类型:
s *Student
这是因为 Go 语言面向对象编程不像 PHP、Java 那样支持隐式的 this
指针,所有的东西都是显式声明的,在 GetXXX
方法中,由于不需要对类的成员变量进行修改,所以不需要传入指针,而 SetXXX
方法需要在函数内部修改成员变量的值,并且该修改要作用到该函数作用域以外,所以需要传入指针类型(结构体是值类型,不是引用类型,所以需要显式传入指针)。
我们可以把接收者类型为指针的成员方法叫做指针方法,把接收者类型为非指针的成员方法叫做值方法,二者的区别在于值方法传入的结构体变量是值类型(类型本身为指针类型除外),因此传入函数内部的是外部传入结构体实例的值拷贝,修改不会作用到外部传入的结构体实例。
接下来,我们可以在 main
函数中初始化 Student
类之后,通过 SetName
方法修改 name
值,然后再通过 GetName
将其打印出来:
student := NewStudent(1, "学院君", 100)
student.SetName("学院君小号")
fmt.Println("Name:", student.GetName())
打印结果是:
值方法和指针方法的区别
另外,需要声明的是,在 Go 语言中,当我们将成员方法 SetName
所属的类型声明为指针类型时,严格来说,该方法并不属于 Student
类,而是属于指向 Student
的指针类型,所以,归属于 Student
的成员方法只是 Student
类型下所有可用成员方法的子集,归属于 *Student
的成员方法才是 Student
类完整可用方法的集合。
我们在调用方法时,之所以可以直接在 student
实例上调用 SetName
方法,是因为 Go 语言底层会自动将 student
转化为对应的指针类型 &student
,所以真正调用的代码是 (&student).SetName("学院君小号")
,这一点需要大家知晓。
总结下来,就是一个自定义数据类型的方法集合中仅会包含它的所有「值方法」,而该类型对应的指针类型包含的方法集合才囊括了该类型的所有方法,包括所有「值方法」和「指针方法」,指针方法可以修改所属类型的属性值,而值方法则不能。
Go 版 toString 方法实现
PHP、Java 支持默认调用类的 toString
方法以字符串格式打印类的实例,Go 语言也有类似的机制,只不过这个方法名是 String
,以上面这个 Student
类型为例,我们为其编写 String
方法如下:
func (s Student) String() string {
return fmt.Sprintf("{id: %d, name: %s, male: %t, score: %f}",
s.id, s.name, s.male, s.score)
}
然后我们可以在 main
方法中这样调用来打印 Student
类实例:
student := NewStudent(1, "学院君", 100)
fmt.Println(student)
无需显式调用 String
方法,Go 语言会自动调用该方法来打印,结果如下:
三、小结
我们来简单总结下,在 Go 语言中,有意弱化了传统面向对象编程中的类概念,这也符合 Go 语言的简单设计哲学,基于结构体定义的「类」就是和内置的数据类型一样的普通数据类型而已,内置的数据类型也可以通过 type
关键字转化为可以包含自定义成员方法的「类」。
一个数据类型关联的所有方法,共同组成了该类型的方法集合,和其他支持面向对象编程的语言一样,同一个方法集合中的方法也不能出现重名,并且,如果它们所属的是一个结构体类型,那么它们的名称与该类型中任何字段的名称也不能重复。
(本文完)
学习过程中有任何问题,可以通过下面的评论功能或加入「Go 语言研习社」与学院君讨论:
本系列教程首发在 geekr.dev,你可以点击页面左下角阅读原文链接查看最新更新的教程。