Skip to content

12|方法

结构体存放数据后,还需要把行为绑定上去。Go 用方法给类型附加行为,方法是带接收者的函数。接收者决定了方法操作的是原对象的副本还是原对象本身。值接收者和指针接收者的选择不仅影响方法内部能否修改字段,还影响性能和接口实现。理解方法集的概念,能避免在接口赋值时遇到"实现了方法但类型不匹配"的编译错误。

一、方法定义

方法是带接收者的函数,接收者写在函数名前面:

go
package main

import "fmt"

type Host struct {
	Name string
	IP   string
}

func (h Host) Address() string {
	return h.Name + "(" + h.IP + ")"
}

func main() {
	host := Host{Name: "web01", IP: "192.168.10.11"}
	fmt.Println(host.Address()) // web01(192.168.10.11)
}

接收者命名通常用类型名的首字母小写,比如 HosthServers。这不是强制规则,但代码风格上推荐这样做。

二、值接收者

值接收者操作的是原对象的副本,方法内部修改不会影响原对象:

go
func (h Host) SetName(name string) {
	h.Name = name // 只修改了副本
}

func main() {
	host := Host{Name: "web01"}
	host.SetName("web02")
	fmt.Println(host.Name) // 仍然是 web01
}

值接收者适合只读操作,或者结构体很小、复制开销可以忽略的场景。每次调用值接收者方法时,Go 会复制整个接收者对象。

三、指针接收者

需要修改原对象时用指针接收者:

go
func (h *Host) SetName(name string) {
	h.Name = name // 修改原对象
}

func main() {
	host := Host{Name: "web01"}
	host.SetName("web02")
	fmt.Println(host.Name) // web02
}

注意调用方式:即使 host 是值类型,host.SetName("web02") 也能调用指针接收者方法——Go 会自动取地址。反过来,指针变量也能调用值接收者方法——Go 会自动解引用。

go
host := Host{Name: "web01"}
host.SetName("web02")       // 编译器自动变成 (&host).SetName

ptr := &Host{Name: "web01"}
ptr.Address()               // 编译器自动变成 (*ptr).Address

这个自动转换只对接收者有效,普通函数参数不会自动转换。

四、选择原则

接收者适用场景
值接收者只读操作,不修改字段;结构体很小(几个字段)
指针接收者需要修改字段;结构体较大;方法里要保持一致行为

结构体较大时,值接收者每次调用都会复制整个结构体,性能和内存开销明显。经验上,除非方法明确只读且结构体很小,否则优先用指针接收者。这样后续需要修改字段时,不需要把值接收者改成指针接收者——这种改动会影响所有调用该方法的代码。

五、方法集

类型的方法集决定了它能满足哪些接口。值类型的方法集只包含值接收者方法;指针类型的方法集包含值接收者和指针接收者方法:

go
type Host struct{ Name string }

func (h Host) Read() string  { return h.Name }      // 值接收者
func (h *Host) Write(n string) { h.Name = n }       // 指针接收者

type Reader interface{ Read() string }
type Writer interface{ Write(string) }

func main() {
	var r Reader = Host{}      // 可以,值类型有 Read 方法
	var w Writer = &Host{}     // 可以,指针类型有 Write 方法
	// var w2 Writer = Host{}  // 编译错误:值类型没有 Write 方法
}

这个规则经常被忽略:值类型不满足包含指针接收者方法的接口。如果一个接口需要 Write 方法,只有 *Host 能满足,Host 本身不满足。

六、常见错误

值接收者误以为能修改原对象

go
func (h Host) Rename(name string) {
	h.Name = name
}

func main() {
	h := Host{Name: "a"}
	h.Rename("b")
	fmt.Println(h.Name) // 仍然是 a
}

这是新手最常见的方法错误。如果方法需要修改字段,接收者必须用指针。

方法集不匹配导致接口赋值失败

go
type Worker interface{ Do() }

type Task struct{}
func (t *Task) Do() {}

func main() {
	var w Worker = Task{} // 编译错误:Task 没有 Do 方法(*Task 才有)
}

Do 是指针接收者方法,只有 *TaskDoTask 本身没有。这种错误在编译期就能发现,排查时看接口定义和类型的方法列表,确认方法接收者类型。

nil 指针调用方法

go
var h *Host
fmt.Println(h.Address()) // panic: nil pointer dereference

即使方法内部不修改字段,nil 指针调用值接收者方法也会 panic,因为方法执行前需要先解引用 nil 指针。如果方法需要处理 nil 接收者,必须在方法内部检查:

go
func (h *Host) Address() string {
	if h == nil {
		return "(nil)"
	}
	return h.Name + "(" + h.IP + ")"
}