Go 中实现“面向对象”
在 Go 中实现“面向对象”的核心是两个概念的结合:
- 结构体 (Struct): 用来封装数据,类似于 PHP 类中的属性。
- 方法 (Method): 本质上是一个函数,但它与一个特定的类型(我们称之为接收者 Receiver)绑定。类似于 PHP 类中的方法。
我们先定义一个数据结构:
// 一个表示用户的 Struct
type User struct {FirstName stringLastName stringAge int
}
现在,我们想创建一个函数来获取用户的全名。
如果是用我们之前学的普通函数,我们会这样做:
// 普通函数,把 User 对象作为参数传入
func GetFullName(user User) string {return user.FirstName + " " + user.LastName
}// 调用时:
// u := User{FirstName: "John", LastName: "Doe"}
// fullName := GetFullName(u)
这种写法完全没问题,但它没有体现出 GetFullName
这个行为和 User
这个数据类型之间的强关联。
定义“方法”
而“方法”的写法,通过增加一个“接收者”,把这种关联变得非常明确。
// (u User) 就是接收者,它让这个函数成为了 User 类型的一个方法
// |
// v
func (u User) FullName() string {return u.FirstName + " " + u.LastName
}
我们来分解一下这个语法:
func
:还是那个我们熟悉的函数关键字。(u User)
: 这是最关键的新增部分!它被称为接收者 (Receiver)。User
: 表示这个方法是绑定在User
这个类型上的。u
: 是接收者变量的名字,类似于 PHP 中的$this
。它代表调用这个方法的具体实例。你可以给它取任何名字(比如this
、self
),但 Go 社区的惯例是使用该类型的单字母或双字母缩写(如u
forUser
)。
FullName()
: 方法名和参数列表,和普通函数一样。string
: 返回值类型。
如何调用这个方法?
现在,调用方式变得和你熟悉的面向对象语法一模一样了:
func main() {// 创建一个 User 实例user := User{FirstName: "Jane",LastName: "Doe",Age: 30,}// 使用 . 语法调用绑定在 user 上的方法fullName := user.FullName()fmt.Println(fullName) // 输出: Jane Doe
}
通过这种方式,我们把数据(User
结构体)和操作数据的行为(FullName
方法)清晰地绑定在了一起,让代码的结构变得更加内聚和易于理解。
现在轮到你了。请你模仿 FullName
方法的格式,为 User
类型再添加一个新方法,名为 CanVote()
,它判断用户的年龄(Age
)是否大于等于 18 岁,并返回一个布尔值 (bool
)。
你的直觉完全正确!不能。
你说“只读吧”,这正是问题的核心。
为什么不行?
这要回到我们之前提到的 Go 的一个基本规则:Go 默认总是“按值传递” (pass-by-value)。
当我们这样定义方法时:func (u User) HaveBirthday()
,接收者 u User
的意思是:
“当调用这个方法时,请把那个 User 实例完整地复制一份,然后把这份副本命名为
u
,交给方法使用。”
所以,在 HaveBirthday
方法内部,u.Age = u.Age + 1
这行代码修改的只是那个副本的年龄,而原始的 user
变量毫发无损。
你的尝试非常棒,已经完全掌握了方法的结构!
这里只需要做两个微小的调整,就能完全符合要求了:
- 返回值类型: 方法需要返回一个布尔值(
true
或false
),所以返回值类型应该是bool
,而不是int
。 - 判断逻辑: 投票的条件通常是“大于等于18岁”,所以我们应该用
>=
操作符。
修正后的版本是这样的:
func (u User) CanVote() bool {return u.Age >= 18
}
你已经成功创建了一个读取结构体数据的方法。我们来看看它的实际效果:
func main() {user1 := User{"Jane", "Doe", 30}user2 := User{"Peter", "Pan", 17}fmt.Printf("%s 能投票吗: %t\n", user1.FullName(), user1.CanVote())fmt.Printf("%s 能投票吗: %t\n", user2.FullName(), user2.CanVote())
}
// 输出:
// Jane Doe 能投票吗: true
// Peter Pan 能投票吗: false
这引出了我们关于方法的最后一个、也是最重要的概念。到目前为止,我们的方法都只是读取数据。那如果我们想要一个修改数据的方法呢?
比如说,我们想创建一个 HaveBirthday
方法,用来给用户增加一岁。
你觉得下面这段代码,能成功地改变用户的年龄吗?为什么?
// 这个方法能成功地改变用户的年龄吗?
func (u User) HaveBirthday() {u.Age = u.Age + 1
}
解决方案:指针接收者 (Pointer Receiver)
那么,如何才能修改原始的数据呢?答案就是使用指针。
我们需要把方法的接收者从一个 User
值,改成一个指向 User
的指针 *User
。
语法上,只需要加一个 *
号:
// 注意接收者类型的变化:User -> *User
func (u *User) HaveBirthday() {u.Age = u.Age + 1
}
现在,接收者 u *User
的意思是:
“当调用这个方法时,请把那个 User 实例的内存地址(也就是一个指针)交给我,我把它命名为
u
。”
因为方法现在拿到了原始数据的“地址”,它就可以顺着地址找到那个唯一的、原始的 User
实例,并直接修改它的内容。
我们来看一下完整的、可以成功修改年龄的代码:
func main() {user := User{FirstName: "Jane",LastName: "Doe",Age: 30,}fmt.Println("生日前的年龄:", user.Age) // 输出: 30// 调用带有指针接收者的方法// Go 很聪明,你不需要写 (&user).HaveBirthday()// 直接写 user.HaveBirthday() 即可,Go 会自动帮你转换user.HaveBirthday()fmt.Println("生日后的年龄:", user.Age) // 输出: 31
}
成功了!
何时使用值接收者 vs. 指针接收者?
这是一个简单的经验法则:
- 如果你的方法不需要修改原始数据,只是读取它(像
FullName
或CanVote
),那么使用值接收者(u User)
。 - 如果你的方法需要修改原始数据(像
HaveBirthday
),那么必须使用指针接收者(u *User)
。
在实际开发中,为了保持一致性,如果一个类型中有一个方法使用了指针接收者,那么该类型的其他所有方法通常也会统一使用指针接收者。
至此,你已经学完了 Go 函数和方法的所有核心概念。从基础语法,到函数式编程,再到 Go 风格的面向对象编程,你已经建立了一个非常完整和坚实的知识体系。恭喜你!