Go 结构体:struct 与方法
Go 语言没有类,但结构体承担了面向对象编程中“类”的核心职责。结构体将不同类型的数据组合在一起,而方法则是定义在这些数据上的行为。掌握这两者是编写 Go 程序的基础。
1. 定义与初始化结构体
结构体是自定义数据类型的集合。你需要先定义类型,再创建实例。
定义一个名为 User 的结构体,包含 Name(字符串)和 Age(整数)两个字段。
type User struct {
Name string
Age int
}
创建结构体实例有三种常见方式。
第一种,声明变量并使用字段名初始化。这种方式最清晰,字段顺序可以随意。
u1 := User{
Name: "张三",
Age: 25,
}
第二种,按照定义顺序初始化。这种方式要求值的顺序必须与结构体定义完全一致,少写或多写都会报错,容易出错,不推荐在字段多时使用。
u2 := User{"李四", 30}
第三种,使用 new 关键字。这会分配内存并返回指针,但字段的零值会被初始化。
u3 := new(User)
// u3 是 *User 类型
// u3.Name = "", u3.Age = 0
访问和修改字段使用点号 . 操作符。
fmt.Println(u1.Name) // 输出: 张三
u1.Age = 26 // 修改年龄
2. 定义方法
方法是带有接收者的函数。它与普通函数的区别在于,它在 func 关键字和方法名之间增加了一个“参数”,即接收者。
定义一个 Greet 方法,让它属于 User 结构体。接收者放在 func 和方法名之间。
// (u User) 是接收者
func (u User) Greet() {
fmt.Printf("你好,我是 %s\n", u.Name)
}
调用该方法时,使用点号操作符,就像访问字段一样。
u1.Greet() // 输出: 你好,我是 张三
3. 值接收者与指针接收者
这是 Go 方法中最关键的概念。选择哪种接收者,决定了方法能否修改原始数据。
3.1 值接收者
当接收者是结构体本身(如 u User)时,Go 会拷贝一份该结构体的副本。在方法内部对副本的任何修改,都不会影响原始数据。
func (u User) HaveBirthday() {
u.Age++ // 这里只是修改了副本,原始 u1 的 Age 不会变
}
3.2 指针接收者
当接收者是指针(如 u *User)时,方法内部操作的是原始数据的内存地址。所有的修改都会直接影响原始对象。
func (u *User) HaveBirthday() {
u.Age++ // 直接修改了原始对象
}
// 调用
u1.HaveBirthday()
fmt.Println(u1.Age) // 输出 26 (原来是 25)
为了更直观地理解两者的区别,请参考下表:
| 特性 | 值接收者 (u User) |
指针接收者 (u *User) |
|---|---|---|
| 数据操作 | 拷贝一份副本,操作副本 | 直接引用原内存地址 |
| 修改原对象 | 不会改变原始数据 | 会改变原始数据 |
| 适用场景 | 只读操作、小结构体 | 需要修改数据、大结构体 |
| 调用一致性 | 无论变量是值还是指针,都能调用 | 无论变量是值还是指针,都能调用 |
注意:Go 语言允许你直接用值变量调用指针接收者的方法,编译器会自动帮你取地址(&u1.HaveBirthday())。反之亦然,这降低了使用者的心智负担。
4. 嵌套与方法提升
Go 没有继承,但通过“结构体嵌套”可以实现代码复用。
定义一个 Address 结构体,并将其嵌入到 User 中。
type Address struct {
City string
}
type User struct {
Name string
Address // 匿名嵌套
}
初始化并访问嵌套字段。
u := User{
Name: "王五",
Address: Address{
City: "北京",
},
}
fmt.Println(u.City) // 直接访问嵌套体的字段,输出: 北京
定义一个属于 Address 的方法。
func (a Address) ShowLocation() {
fmt.Println("位于:", a.City)
}
调用该方法。因为 User 包含 Address,所以 User 实例可以直接调用 ShowLocation,这被称为“方法提升”。
u.ShowLocation() // 输出: 位于: 北京
如果 User 自己也定义了一个同名方法,那么 User 自己的方法会优先被调用,嵌套体的方法会被“屏蔽”。
5. 接口与结构体的协作
在 Go 中,接口是一组方法签名的集合。只要一个结构体实现了接口中定义的所有方法,我们就说它实现了该接口。这是隐式的,不需要显式声明 implements。
定义一个 Speaker 接口,包含 Speak 方法。
type Speaker interface {
Speak()
}
让 User 实现 Speaker 接口,只需添加对应方法。
func (u User) Speak() {
fmt.Println(u.Name + " 正在说话。")
}
使用接口变量来接收结构体实例。
var s Speaker
s = u1 // u1 是 User 类型
s.Speak()
这种设计允许你编写只依赖行为(接口)而不依赖具体实现(结构体)的代码,极大地降低了代码耦合度。

暂无评论,快来抢沙发吧!