Skip to content

Go方法 ​

阅读提示

  • 适用版本:Go 1.21+(示例在 Go 1.22 下验证)· 前置知识:函数 · 阅读时长:约 10 分钟

方法与函数的区别在于,方法拥有接收者,而函数没有,且只有自定义类型能够拥有方法。先来看一个例子。

go
type IntSlice []int

func (i IntSlice) Get(index int) int {
  return i[index]
}
func (i IntSlice) Set(index, val int) {
  i[index] = val
}

func (i IntSlice) Len() int {
  return len(i)
}

先声明了一个类型IntSlice,其底层类型为[]int,再声明了三个方法Get,Set和Len,方法的长相与函数并无太大的区别,只是多了一小段(i IntSlice) 。i就是接收者,IntSlice就是接收者的类型,接收者就类似于其他语言中的this或self,只不过在 Go 中需要显式的指明。

go
func main() {
   var intSlice IntSlice
   intSlice = []int{1, 2, 3, 4, 5}
   fmt.Println(intSlice.Get(0))
   intSlice.Set(0, 2)
   fmt.Println(intSlice)
   fmt.Println(intSlice.Len())
}

方法的使用就类似于调用一个类的成员方法,先声明,再初始化,再调用。

值接收者 ​

接收者也分两种类型,值接收者和指针接收者,先看一个例子

go
type MyInt int

func (i MyInt) Set(val int) {
   i = MyInt(val) // 修改了,但是不会造成任何影响
}

func main() {
   myInt := MyInt(1)
   myInt.Set(2)
   fmt.Println(myInt)
}

上述代码运行过后,会发现myInt的值依旧是 1,并没有被修改成 2。方法在被调用时,会将接收者的值传入方法中,上例的接收者就是一个值接收者,可以简单的看成一个形参,而修改一个形参的值,并不会对方法外的值造成任何影响,那么如果通过指针调用会如何呢?

go
func main() {
  myInt := MyInt(1)
  (&myInt).Set(2)
  fmt.Println(myInt)
}

遗憾的是,这样的代码依旧不能修改内部的值,为了能够匹配上接收者的类型,Go 会将其解引用,解释为(*(&myInt)).Set(2)。

指针接收者 ​

稍微修改了一下,就能正常修改myInt的值。

go
type MyInt int

func (i *MyInt) Set(val int) {
   *i = MyInt(val)
}

func main() {
   myInt := MyInt(1)
   myInt.Set(2)
   fmt.Println(myInt)
}

现在的接收者就是一个指针接收者,虽然myInt是一个值类型,在通过值类型调用指针接收者的方法时,Go 会将其解释为(&myint).Set(2)。所以方法的接收者为指针时,不管调用者是不是指针,都可以修改内部的值。

函数的参数传递过程中,是值拷贝的,如果传递的是一个整型,那就拷贝这个整型,如果是一个切片,那就拷贝这个切片,但如果是一个指针,就只需要拷贝这个指针,显然传递一个指针比起传递一个切片所消耗的资源更小,接收者也不例外,值接收者和指针接收者也是同样的道理。在大多数情况下,都推荐使用指针接收者,不过两者并不应该混合使用,要么都用,要么就都不用,看下面一个例子。

TIP

需要先了解接口

go
type Animal interface {
   Run()
}

type Dog struct {
}

func (d *Dog) Run() {
   fmt.Println("Run")
}

func main() {
   var an Animal
   an = Dog{}
   // an = &Dog{} 正确方式
   an.Run()
}

这一段代码将会无法通过编译,编译器将会输出如下错误

cannot use Dog{} (value of type Dog) as type Animal in assignment:
  Dog does not implement Animal (Run method has pointer receiver)

翻译过来就是,无法使用Dog{}初始化Animal类型的变量,因为Dog没有实现Animal ,解决办法有两种,一是将指针接收者改为值接收者,二是将Dog{}改为&Dog{},接下来逐个讲解。

go
type Dog struct {
}

func (d Dog) Run() { // 改为了值接收者
   fmt.Println("Run")
}

func main() { // 可以正常运行
   var an Animal
   an = Dog{}
   // an = &Dog{} 同样可以
   an.Run()
}

在原来的代码中,Run 方法的接收者是*Dog ,自然而然实现Animal接口的就是Dog指针,而不是Dog结构体,这是两个不同的类型,所以编译器就会认为Dog{}并不是Animal的实现,因此无法赋值给变量an,所以第二种解决办法就是赋值Dog指针给变量an。不过在使用值接收者时,Dog指针依然可以正常赋值给animal,这是因为 Go 会在适当情况下对指针进行解引用,因为通过指针可以找到Dog结构体,但是反过来的话,无法通过Dog结构体找到Dog指针。如果单纯的在结构体中混用值接收者和指针接收者的话无伤大雅,但是和接口一起使用后,就会出现错误,倒不如无论何时要么都用值接收者,要么就都用指针接收者,形成一个良好的规范,也可以减少后续维护的负担。

还有一种情况,就是当值接收者是可寻址的时候,Go 会自动的插入指针运算符来进行调用,例如切片是可寻址,依旧可以通过值接收者来修改其内部值。比如下面这个代码

go
type Slice []int

func (s Slice) Set(i int, v int) {
  s[i] = v
}

func main() {
  s := make(Slice, 1)
  s.Set(0, 1)
  fmt.Println(s)
}

输出

[1]

但这样会引发另一个问题,如果对其添加元素的话,情况就不同了。看下面的例子

type Slice []int

func (s Slice) Set(i int, v int) {
  s[i] = v
}

func (s Slice) Append(a int) {
  s = append(s, a)
}

func main() {
  s := make(Slice, 1, 2)
  s.Set(0, 1)
  s.Append(2)
  fmt.Println(s)
}
[1]

它的输出还是和之前一样,append函数是有返回值的,向切片添加完元素后必须覆盖原切片,尤其是在扩容后,在方法中对值接收者修改并不会产生任何影响,这也就导致了例子中的结果,改成指针接收者就正常了。

go
type Slice []int

func (s *Slice) Set(i int, v int) {
  (*s)[i] = v
}

func (s *Slice) Append(a int) {
  *s = append(*s, a)
}

func main() {
  s := make(Slice, 1, 2)
  s.Set(0, 1)
  s.Append(2)
  fmt.Println(s)
}

输出

[1 2]

完整示例 ​

下面的程序可以直接运行:复制到文件后执行 go run main.go,或粘贴到 Go Playground 运行。

go
package main

import "fmt"

type Counter struct{ n int }

func (c Counter) Value() int { return c.n }
func (c *Counter) Inc()      { c.n++ }

type Valuer interface{ Value() int }

func describe(v Valuer) {
	fmt.Println("value:", v.Value())
}

func main() {
	c := Counter{}
	c.Inc()
	c.Inc()

	describe(c) // 值接收者:Counter 与 *Counter 都可传入
	describe(&c)

	c.Inc()
	describe(c)
	describe(&c)
}

预期输出:

text
value: 2
value: 2
value: 3
value: 3

常见错误与踩坑 ​

  • 值接收者会拷贝接收者。 在值接收者方法里修改字段,调用方看不到任何变化。
  • 接口匹配看方法集。 *T 同时拥有值与指针接收者方法;T 只包含值接收者方法。
  • 指针接收者方法需要可寻址的值。 对 map 元素调用 m[k].Inc() 无法编译。
  • 接收者命名与类型保持一致。 同一类型反复混用 c、p 或两种接收者,会让读者困惑。
  • 嵌入会提升方法。 两个嵌入类型有同名方法时,必须显式限定调用。

更新记录 ​

  • 2026-10-14:新增"阅读提示"、完整可运行示例(含预期输出)、常见错误与踩坑清单,以及本更新记录。