写在前面
在上一篇文章《Golang入门学习之结构体(struct)》当中,我们学习了Golang当中结构体(struct)的知识点,接下来我们将学习Golang当中的方法(method)。
方法的定义
在Golang当中,方法是作用在接收者(receiver)上的一个函数,接收者是某种类型的变量。因此,方法是一种特殊的函数。这里的接收者可以(几乎,接收者类型不能是一个接口类型或指针类型)任何类型,不仅仅是结构体类型,也就意味着,几乎任何类型都可以方法,甚至是函数类型,或者是int、bool等的别名类型。
我们可以这样理解:一个类型(比如说是结构体)加上它的方法就等价于面向对象语言当中的一个类。
方法的定义格式
func (recv receiver_type) methodName(parameter_list) (return_value_list) { ... }
复制代码
recv
就像是面向对象语言中的 this
或 self
,但是 Golang 中并没有这两个关键字。随个人喜好,你可以使用 this
或 self
作为 receiver 的名字。
注意点
在 Golang 中,类型的代码和绑定在它上面的方法的代码可以不放置在一起,它们可以存在在不同的源文件,唯一的要求是:它们必须是同一个包的。请看下面这个例子:
我们在src/go_code/method/model/immortal.go
当中定义了一个修仙者类型
package model
// 修仙者
type immortal struct {
name string
age int
gender string
}
复制代码
然后,我们在src/go_code/method/model/immortal_method.go
当中定义immortal
类型的方法
package model
// 工厂函数
func NewImmortal(age int, name, gender string) *immortal {
return &immortal{
name: name,
age: age,
gender: gender,
}
}
// Getter
func (recv *immortal) GetName() string {
return recv.name
}
...
复制代码
再然后,我们再main包当中使用它
package main
import (
"fmt"
"go_code/method/model"
)
func main() {
i := model.NewImmortal(18,"韩立","男")
name :=i.GetName()
fmt.Println(name)
}
复制代码
输出:
韩立
复制代码
函数与方法的区别
函数和方法都是一段可复用的代码段。他们的区别在于函数是面向过程的,方法是面向对象。从调用上来看,函数通过函数名进行调用,而方法则通过与实例关联的变量进行调用。
// 函数调用
println("Hello World")
// 方法调用
immortal := model.NewImmortal(18,"韩立","男")
immortal.GetName()
复制代码
再看Golang当中,方法由接收者类型、方法名、形参列表、返回值列表和方法体五部分构成,并且接收者必须有一个显式的名字,这个名字必须在方法中被使用。而且,接收者类型(receiver_type)必须在和方法同样的包中被声明。
Golang中方法的其他特性
在Golang当中,接收者类型关联的方法不写在类型结构里面(面向对象语言Java的方法是在类当中进行定义的)。因此,在Golang当中方法与接收者类型的耦合更加地宽松,也就是说,数据(字段)与其对应的行为是相互独立。
接收者类型可以是一个值而不是类型的指针吗?
接收者类型可以是一个值而不是类型的指针吗?答案是可以的。但是,基于性能方面的考虑,我并不建议大家这样做。因为接收者是作为值传递给对应的方法的,这相当于将实例的值拷贝传递给方法,这并不是一件划算的买卖。请看下面的例子,接收者完全可以是实例的值。
// 修仙者等级
type Level struct {
level string
levelValue int
}
// 获取等级描述
func (recv Level) GetLevel() string{
return recv.level
}
func main{
level := model.Level{"练气九层",9200}
fmt.Println(level.GetLevel())
}
复制代码
输出:
练气九层
复制代码
注意:
指针方法和值方法都可以在指针或非指针上被调用。如下面程序所示,类型 Level
在值上有一个方法 GetLevel()
,在指针上有一个方法 SetLevel()
,但是可以看到两个方法都可以在两种类型的变量上被调用。
package model
// 修仙者等级
type Level struct {
level string
levelValue int
}
func NewLevel(level string, levelValue int) Level {
return Level{
level: level,
levelValue: levelValue,
}
}
// 获取等级描述
func (recv Level) Level() string{
return recv.level
}
func (recv *Level) SetLevel(level string) {
recv.level = level
}
复制代码
package main
import (
"fmt"
"go_code/method/model"
)
func main() {
level := model.NewLevel("练气九层",9200)
levelPointer := & level
fmt.Println("晋级之前:",level.Level())
levelPointer.SetLevel("炼气大圆满")
fmt.Println("晋级之后:",level.Level())
}
复制代码
输出:
晋级之前: 练气九层
晋级之后: 炼气大圆满
复制代码
方法和未导出字段
在上面的例子当中,level
类型的字段对包外部而言是不可见的(可以理解为面向对象语言当中的private
属性)。因此如果在main
包当中直接通过选择器进行访问的话,将会报错。这是,我们可以通过面向对象语言一个众所周知的技术来完成:提供 getter 和 setter 方法。在Golang当中,对于 setter 方法使用 Set 前缀,对于 getter 方法只使用成员名。
关于并发访问对象
对象的字段(属性)不应该由 2 个或 2 个以上的不同线程在同一时间去改变。如果在程序发生这种情况,为了安全并发访问,可以使用包 sync
中的方法(比如加个互斥锁)。但是这并不是一个推荐的选项(之后我们将会学习通过 goroutines 和 channels 去探索一种新的方式)。请看下面的例子
src/go_code/method/model/level_lock.go
package model
import "sync"
// 修仙者等级
type levelLock struct {
Lock sync.Mutex
level string
levelValue int
}
func NewLevelLock(level string, levelValue int) *levelLock {
return &levelLock{
level: level,
levelValue: levelValue,
}
}
func (recv *levelLock) SetLevel(level string) {
recv.level = level
}
复制代码
src/go_code/struct/main/level_lock.go
package main
import "go_code/method/model"
func main() {
level := model.NewLevelLock("练气九层",9200)
// 获取锁
level.Lock.Lock()
//修改值
level.SetLevel("练气圆满")
// 释放锁
defer level.Lock.Unlock()
}
复制代码
内嵌类型的方法和继承
当一个匿名类型被内嵌在结构体中时,匿名类型的可见方法也同样被内嵌,这在效果上等同于外层类型 继承 了这些方法:将父类型放在子类型中来实现亚型。这个机制提供了一种简单的方式来模拟经典面向对象语言中的子类和继承相关的效果。因为一个结构体可以嵌入多个匿名类型,所以实际上我们可以有一个简单版本的多重继承
。
在model包当中定义一个immortal2
类型,并让其内嵌一个匿名类型level
src/go_code/method/model/anonymous_type.go
:
package model
// 修仙者
type immortal2 struct {
name string
age int
gender string
Level
lingGen
}
func NewImmortal2(age int, name, gender string,levelName string,levelValue int,lingGenNames...string) *immortal2 {
return &immortal2{
name: name,
age: age,
gender: gender,
Level: Level{levelName,levelValue},
lingGen: lingGen{linGenNames: lingGenNames},
}
}
复制代码
src/go_code/method/model/level.go
:
package model
// 修仙者等级
type Level struct {
level string
levelValue int
}
func NewLevel(level string, levelValue int) Level {
return Level{
level: level,
levelValue: levelValue,
}
}
// 获取等级描述
func (recv Level) Level() string{
return recv.level
}
func (recv *Level) SetLevel(level string) {
recv.level = level
}
func (recv *Level) LevelName() string{
return recv.level
}
复制代码
src/go_code/method/model/lingen.go
:
package model
// 修士的灵根
type lingGen struct {
linGenNames[] string
}
func NewLinggen(name ...string) *lingGen {
return &lingGen{linGenNames: name}
}
func (recv *lingGen) LingGenNames() []string {
return recv.linGenNames
}
复制代码
在main包当中导入并使用
package main
import (
"fmt"
"go_code/method/model"
)
func main() {
im := model.NewImmortal2(18,"韩立","男",
"练气九层",9200,"木灵根","水灵根","火灵根","土灵根")
im.SetLevel("练气大圆满")
fmt.Println("境界:",im.LevelName())
fmt.Println("灵根:",im.LingGenNames())
}
复制代码
输出:
境界: 练气大圆满
灵根: [木灵根 水灵根 火灵根 土灵根]
复制代码
Go 的类型和方法和其他面向对象语言对比
在如 C++、Java、C# 和 Python这样的面向对象语言中,方法在类的上下文中被定义和继承:在一个对象上调用方法时,运行时会检测类以及它的超类中是否有此方法的定义,如果没有会导致异常发生。
在 Golang 中,这样的继承层次是完全没必要的:如果方法在此类型定义了,就可以调用它,和其他类型上是否存在这个方法没有关系。在这个意义上,Golang具有更大的灵活性。
Golang不需要一个显式的类定义,如同 Java和C++等那样,相反地,“类”是通过提供一组作用于一个共同类型的方法集加类型本身来隐式定义的。类型可以是结构体或者任何用户自定义类型。
总结
在Golang中,类=类型+与之关联的方法集。
在 Golang 中,代码复用通过组合和委托实现,多态通过接口的使用来实现:有时这也叫 组件编程(Component Programming)。
相比于类继承,Go 的接口(后面将会详细讲解)提供了更强大、却更简单的多态行为。
写在后面
关于Golang中方法的学习就写到这了。本文当中涉及到的例子可以点击此处下载。如果我的学习笔记能够给你带来帮助,还请多多点赞鼓励。文章如有错漏之处还请各位小伙伴帮忙斧正。