Go 的“面向对象”初探:指针、方法与接收者

前言

欢迎来到本系列的第五课!如果你有其他编程语言(如 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 语言提供了两个核心的指针操作符:&*

  1. 取地址 (&):使用 & 操作符可以获取一个变量的内存地址。

  2. 指针类型 (*T):一个指向类型 T 的指针,其类型为 *T。例如,指向 int 变量的指针类型是 *int

  3. 解引用 (*):使用 * 操作符可以获取指针所指向地址上存储的

代码研习

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 为什么需要指针?
  1. 允许在函数内部修改外部变量:这是指针最主要的用途。我们知道 Go 的函数参数是值传递,函数内部对参数的修改不影响外部。但如果传递的是一个指针,我们就可以通过这个指针修改原始变量。

  2. 提升性能:对于非常大的数据结构,传递指针可以避免整个数据结构的值拷贝,从而提高程序效率。

第二章:方法 (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 方法内部的 uuser 的一个副本,地址都不同。对副本的修改,自然影响不到正本。

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)。敬请期待!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值