前言
欢迎来到本系列的第五课!如果你有其他编程语言(如 Java, Python, C++)的背景,你可能会问:Go 的“类 (class)”在哪里?“继承 (inheritance)”又在哪里?
答案是:Go 语言中没有这些概念。它通过一种更简洁、更灵活的方式来实现面向对象编程的目标,其核心就是**结构体(数据) + 方法(行为)**的组合。
要深刻理解这种模式,我们必须先掌握一个基础但至关重要的概念——指针。它就像一把钥匙,能解锁 Go 语言更深层次的能力,尤其是让我们能够在函数或方法内部修改外部数据。
第一章:指针 (Pointer) —— 内存地址的“遥控器”
1.1 什么是地址与指针?
在 Go 中,每一个变量都存储在内存中的一个特定位置,这个位置就是它的内存地址(Address)。
指针(Pointer)是一个特殊的变量,它不存储普通的数据值(如 10, "hello"),而是专门用来存储另一个变量的内存地址。
-
打个比方:
-
一个变量
var name = "Alice"就像一栋房子,房子里住着 "Alice"。 -
这栋房子的地址是“人民路 101 号”。
-
一个指针就像一张纸条,纸条上写着“人民路 101 号”。通过这张纸条,你就能找到这栋房子。
-
1.2 指针的核心操作
Go 语言提供了两个核心的指针操作符:& 和 *。
-
取地址 (
&):使用&操作符可以获取一个变量的内存地址。 -
指针类型 (
*T):一个指向类型T的指针,其类型为*T。例如,指向int变量的指针类型是*int。 -
解引用 (
*):使用*操作符可以获取指针所指向地址上存储的值。
代码研习:
package main
import "fmt"
func main() {
// 1. 定义一个普通变量 a
a := 10
fmt.Println("变量 a 的值:", a)
fmt.Println("变量 a 的内存地址:", &a)
// 2. 定义一个指针变量 p,用于存储 a 的地址
// p 的类型是 *int (读作 "int a pointer")
var p *int
p = &a // 将 a 的地址赋值给 p
fmt.Println("指针 p 存储的地址:", p)
fmt.Println("指针 p 指向的值:", *p) // 使用 * 解引用,获取地址上的值
// 3. 通过指针修改原始变量的值
*p = 20
fmt.Println("通过指针修改后,变量 a 的值:", a)
}
输出:
变量 a 的值: 10
变量 a 的内存地址: 0x...
指针 p 存储的地址: 0x...
指针 p 指向的值: 10
通过指针修改后,变量 a 的值: 20
1.3 为什么需要指针?
-
允许在函数内部修改外部变量:这是指针最主要的用途。我们知道 Go 的函数参数是值传递,函数内部对参数的修改不影响外部。但如果传递的是一个指针,我们就可以通过这个指针修改原始变量。
-
提升性能:对于非常大的数据结构,传递指针可以避免整个数据结构的值拷贝,从而提高程序效率。
第二章:方法 (Method) —— 绑定到类型的“专属函数”
方法是一个绑定到特定类型的函数。它将**数据(结构体)和行为(函数)**紧密地联系在了一起。
2.1 从函数到方法
在学习方法之前,我们可能会这样写代码:
type User struct {
Name string
}
// 一个普通的函数,接收一个 User 类型的参数
func PrintUserInfo(u User) {
fmt.Println("User name is:", u.Name)
}
这种写法完全正确,但 Go 提供了更符合“面向对象”直觉的方式——方法
2.2 方法的定义与调用
方法的定义与函数非常相似,只是在 func 关键字和函数名之间,增加了一个接收者(Receiver)。
代码研习:
package main
import "fmt"
type User struct {
Name string
Email string
}
// 为 User 类型定义一个 PrintInfo 方法
// (u User) 就是接收者
func (u User) PrintInfo() {
fmt.Printf("Name: %s, Email: %s\n", u.Name, u.Email)
}
func main() {
user := User{Name: "Alice", Email: "alice@example.com"}
// 使用 "对象.方法()" 的语法来调用
user.PrintInfo()
}
接收者 (u User) 的含义是:
-
u: 接收者变量名,在方法内部,可以用它来访问User实例的字段。 -
User: 接收者类型,指明了这个方法是绑定到User这个类型上的。
第三章:接收者 (Receiver) —— 值类型 vs 指针类型的抉择
这是本期最核心、最重要的知识点。方法的接收者可以是值类型,也可以是指针类型。这个选择,决定了方法内部的操作能否影响到原始的结构体实例。
3.1 值接收者 (func (u User) ...)
-
机制:当方法被调用时,接收者会像函数参数一样,进行值传递。方法内部操作的是原始结构体的一个副本。
-
效果:无法在方法内部修改原始结构体的值。
代码研习:
type User struct {
Name string
}
// 使用值接收者,尝试修改 Name
func (u User) ChangeName(newName string) {
fmt.Printf("方法内,u 的地址: %p\n", &u)
u.Name = newName
}
func main() {
user := User{Name: "Alice"}
fmt.Printf("main 中,user 的地址: %p\n", &user)
fmt.Println("调用前:", user.Name)
user.ChangeName("Bob")
fmt.Println("调用后:", user.Name) // Name 并没有被改变
}
输出:
main 中,user 的地址: 0x...
调用前: Alice
方法内,u 的地址: 0x... (与 main 中的地址不同)
调用后: Alice
可以看到,ChangeName 方法内部的 u 是 user 的一个副本,地址都不同。对副本的修改,自然影响不到正本。
3.2 指针接收者 (func (u *User) ...)
-
机制:方法的接收者是一个指向结构体的指针。方法内部操作的是指向原始结构体的引用。
-
效果:可以在方法内部修改原始结构体的值。
代码研习:
type User struct {
Name string
}
// 使用指针接收者
func (u *User) ChangeName(newName string) {
fmt.Printf("方法内,u 指向的地址: %p\n", u)
u.Name = newName
}
func main() {
user := User{Name: "Alice"}
fmt.Printf("main 中,user 的地址: %p\n", &user)
fmt.Println("调用前:", user.Name)
// Go 语言会自动进行转换,(&user).ChangeName("Bob")
user.ChangeName("Bob")
fmt.Println("调用后:", user.Name) // Name 成功被改变
}
输出:
main 中,user 的地址: 0x...
调用前: Alice
方法内,u 指向的地址: 0x... (与 main 中的地址相同)
调用后: Bob
3.3 如何选择?—— 一个简单而明确的规则
那么,到底应该使用值接收者还是指针接收者?请遵循以下准则:
如果你的方法需要修改接收者的状态,或者接收者是大型结构体(为了避免复制开销),那么请使用指针接收者 (*T)。
否则,可以使用值接收者 (T)。
社区最佳实践:保持一致性。如果一个类型中,有任何一个方法使用了指针接收者,那么为了保持一致性,建议该类型的所有方法都使用指针接收者。
总结与下一课预告
在本期中,我们完成了从过程式编程到 Go 特色“面向对象”编程的关键一步:
-
我们学习了指针,掌握了通过内存地址间接访问和修改数据的能力。
-
我们学会了为结构体定义方法,将数据和行为绑定在一起。
-
我们深刻辨析了值接收者和指针接收者的本质区别,并掌握了如何根据**“是否需要修改”**这一核心原则来进行选择。
我们已经学会了如何创建拥有自定义行为的类型。但在一个复杂的系统中,我们如何编写能够处理多种不同类型、但又具有相同行为的通用代码呢?
在下一课中,我们将探索 Go 语言类型系统中最强大、最优雅的特性——接口 (Interface)。敬请期待!
1162

被折叠的 条评论
为什么被折叠?



