结构体方法
本教程共 80 篇 · 第 39 篇 · 更新于 2026-07-27 · 约 8 分钟阅读
39. 结构体方法
本节目标:学会给结构体定义方法,搞懂值接收者和指针接收者的区别,知道什么时候该用哪个。
什么是方法
方法就是和特定类型关联的函数。普通函数是「谁都能调用」,方法是「只有这个类型才能调用」。
定义方法时,在函数名前面加一个「接收者」参数:
package main
import "fmt"
type Rectangle struct {
Width float64
Height float64
}
// (r Rectangle) 就是接收者
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func main() {
rect := Rectangle{Width: 10, Height: 5}
fmt.Println(rect.Area()) // 50
}
(r Rectangle) 表示这个方法属于 Rectangle 类型,r 就是调用这个方法的那个实例。
NoteGo 的方法本质上就是第一个参数是接收者的函数。
rect.Area()等价于Area(rect),只是写法更自然。
值接收者 vs 指针接收者
这是 Go 方法最关键的概念。
值接收者
接收者是结构体的值拷贝。方法内修改不影响原结构体:
type Counter struct {
Count int
}
func (c Counter) Increment() {
c.Count++ // 改的是副本,不影响原值
}
func main() {
c := Counter{Count: 0}
c.Increment()
fmt.Println(c.Count) // 0 没变!
}
指针接收者
接收者是指针,方法内修改会影响原结构体:
func (c *Counter) Increment() {
c.Count++ // 通过指针修改原值
}
func main() {
c := Counter{Count: 0}
c.Increment()
fmt.Println(c.Count) // 1 变了!
}
TipGo 有个语法糖:用值调用指针方法时,
c.Increment()会自动转成(&c).Increment()。所以你不用手动写(&c).Increment()。
什么时候用指针接收者
三条判断标准:
- 方法要修改接收者的值:必须用指针接收者
- 结构体很大:用指针接收者避免每次拷贝
- 保持一致性:如果一个类型有指针接收者的方法,其他方法也尽量用指针接收者
type User struct {
Name string
Age int
}
// 需要修改:用指针
func (u *User) SetAge(age int) {
u.Age = age
}
// 只读但结构体可能较大:用指针
func (u *User) DisplayName() string {
return u.Name
}
// 小结构体只读:可以用值
func (u User) IsAdult() bool {
return u.Age >= 18
}
Warning最常见的坑是一个类型混用值接收者和指针接收者。虽然能编译,但会导致接口实现出问题(后面讲接口时会详细说)。建议统一用一种风格。
方法集
方法集是一个类型拥有的所有方法的集合。值类型和指针类型的方法集不同:
- 值类型
T的方法集:只包含值接收者的方法 - 指针类型
*T的方法集:包含值接收者和指针接收者的方法
type Animal struct {
Name string
}
func (a Animal) GetName() string {
return a.Name
}
func (a *Animal) SetName(name string) {
a.Name = name
}
func main() {
a := Animal{Name: "Rex"}
// 值可以调值接收者方法
a.GetName() // OK
// 值调指针接收者方法(语法糖自动取地址)
a.SetName("Max") // OK,自动转 (&a).SetName("Max")
// 指针两种都能调
p := &a
p.GetName() // OK
p.SetName("Tom") // OK
}
Note方法集的概念在讲接口时会变得很重要。一个值类型可能不满足某个接口,但它的指针类型满足。后面会展开讲。
方法可以定义在任何类型上
不只是结构体,任何自定义类型都能有方法:
type MyInt int
func (m MyInt) IsEven() bool {
return m%2 == 0
}
func main() {
n := MyInt(4)
fmt.Println(n.IsEven()) // true
}
但不能给基础类型(int、string 等)直接加方法,必须先 type 定义新类型。
嵌入类型的方法提升
上一章讲了字段提升,方法也会提升:
package main
import "fmt"
type Animal struct {
Name string
}
func (a Animal) Speak() string {
return a.Name + " speaks"
}
type Dog struct {
Animal
}
func main() {
d := Dog{Animal{Name: "Rex"}}
fmt.Println(d.Speak()) // Rex speaks
}
Dog 嵌入了 Animal,Animal 的 Speak 方法自动提升到 Dog 上,可以直接调用。
你也可以给 Dog 定义自己的 Speak 来覆盖:
func (d Dog) Speak() string {
return d.Name + " barks"
}
func main() {
d := Dog{Animal{Name: "Rex"}}
fmt.Println(d.Speak()) // Rex barks 用 Dog 自己的
fmt.Println(d.Animal.Speak()) // Rex speaks 显式调用 Animal 的
}
小结
方法这块记住几点:
- 值接收者改不了原值,指针接收者能改
- 需要修改原值、结构体大、保持一致性 → 用指针接收者
- 值的方法集只有值方法,指针的方法集两者都有
- 嵌入类型的方法会提升,可以覆盖
下一章讲结构体的内存布局,搞懂字段排列和内存对齐。