Appearance
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)
}接收者命名通常用类型名的首字母小写,比如 Host 用 h,Server 用 s。这不是强制规则,但代码风格上推荐这样做。
二、值接收者
值接收者操作的是原对象的副本,方法内部修改不会影响原对象:
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 是指针接收者方法,只有 *Task 有 Do,Task 本身没有。这种错误在编译期就能发现,排查时看接口定义和类型的方法列表,确认方法接收者类型。
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 + ")"
}