首页 / Go 语言入门教程 / 结构体方法

Go 语言入门教程

结构体方法

本教程共 80 篇 · 第 39 篇 · 更新于 2026-07-27 · 约 8 分钟阅读

GoGo 入门教程结构体方法接收者方法集指针接收者

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 就是调用这个方法的那个实例。

Note

Go 的方法本质上就是第一个参数是接收者的函数。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  变了!
}
Tip

Go 有个语法糖:用值调用指针方法时,c.Increment() 会自动转成 (&c).Increment()。所以你不用手动写 (&c).Increment()

什么时候用指针接收者

三条判断标准:

  1. 方法要修改接收者的值:必须用指针接收者
  2. 结构体很大:用指针接收者避免每次拷贝
  3. 保持一致性:如果一个类型有指针接收者的方法,其他方法也尽量用指针接收者
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 嵌入了 AnimalAnimalSpeak 方法自动提升到 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 的
}

小结

方法这块记住几点:

  • 值接收者改不了原值,指针接收者能改
  • 需要修改原值、结构体大、保持一致性 → 用指针接收者
  • 值的方法集只有值方法,指针的方法集两者都有
  • 嵌入类型的方法会提升,可以覆盖

下一章讲结构体的内存布局,搞懂字段排列和内存对齐。