Golang中为什么指针接收器无处不在
Golang中为什么指针接收器无处不在 特别是在这篇文档中
func (e *QueryError) Unwrap() error { return e.Err }
据我理解,指针接收器应该只在需要修改接收器值时使用。 在上面的例子中,它只是简单地返回了接收器值的一个字段值。
我是不是漏掉了什么,或者它可以(并且应该)简化为:
func (e QueryError) Unwrap() error { return e.Err }
?
而当没有疑问时。
例如,在这种情况下:
type QueryError struct {
Query string
Err error
}
func (e *QueryError) Error() string { return e.Query + ": " + e.Err.Error() }
显然不需要指向 QueryError,因为没有修改操作,对吗?
(只是想理解一下)。
那么你可以应用以下原则:
“如果接收者是一个大型结构体或数组,使用指针接收者效率更高。多大算大?可以假设这相当于将所有元素作为参数传递给方法。如果感觉太大,那么对接收者来说也太大了。”
这是一份描述何时使用指针接收器的优秀文档。
golang/go
Go 编程语言。通过在 GitHub 上创建账户来为 golang/go 的开发做出贡献。
简而言之,该文档指出“如有疑问,请使用指针。”
有一种情况使用指针是绝对必要的,但大多数人会忽略它。那就是当你无法复制结构体的状态时。以互斥锁为例,文档明确指出不能复制它们:
互斥锁在首次使用后不得复制。
为什么?因为互斥锁意味着它需要接收所有并发调用,并根据一个参照状态来同步它们。如果你复制了互斥锁及其状态,就无法再进行同步。为了防止这种情况发生,Lock 和 Unlock 方法使用了指针接收器,从而确保你无法在副本上调用它们。
在Go语言中,指针接收器确实无处不在,这不仅仅是出于修改接收器值的需要。你提到的例子涉及错误处理,这里使用指针接收器有几个关键原因:
1. 接口实现的一致性
Unwrap()方法是error接口的一部分。当使用值接收器时,方法操作的是副本,这可能导致接口实现出现问题:
type QueryError struct {
Query string
Err error
}
// 值接收器版本
func (e QueryError) Unwrap() error { return e.Err }
func main() {
var err error = &QueryError{Query: "SELECT *", Err: io.EOF}
// 这里会发生什么?
unwrapped := errors.Unwrap(err)
fmt.Println(unwrapped == io.EOF) // 可能不是预期的结果
}
2. 避免不必要的复制
即使方法不修改接收器,使用指针接收器也能避免结构体的复制开销,特别是对于较大的结构体:
type LargeStruct struct {
data [1024]byte
}
// 值接收器 - 每次调用都会复制1024字节
func (s LargeStruct) GetData() [1024]byte { return s.data }
// 指针接收器 - 只复制指针(8字节)
func (s *LargeStruct) GetDataPtr() [1024]byte { return s.data }
3. 方法集规则
Go语言的方法集规则决定了哪些方法可以被调用:
- 类型
T的方法集包含所有值接收器声明的方法 - 类型
*T的方法集包含所有值接收器和指针接收器声明的方法
这意味着指针类型拥有更完整的方法集:
type MyError struct {
msg string
}
func (e *MyError) Error() string {
return e.msg
}
func main() {
// 这样是OK的
var err1 error = &MyError{msg: "error"}
// 这样会编译错误,因为MyError没有实现error接口
// var err2 error = MyError{msg: "error"}
}
4. 在错误处理中的具体原因
在你的例子中,QueryError很可能实现了error接口:
type QueryError struct {
Query string
Err error
}
func (e *QueryError) Error() string {
return fmt.Sprintf("query %q failed: %v", e.Query, e.Err)
}
func (e *QueryError) Unwrap() error { return e.Err }
如果Error()方法使用指针接收器(通常如此),那么Unwrap()也必须使用指针接收器来保持一致性,否则类型QueryError不会实现包含这两个方法的接口。
5. 实际示例
考虑一个更完整的错误包装示例:
package main
import (
"errors"
"fmt"
)
type QueryError struct {
Query string
Err error
}
func (e *QueryError) Error() string {
return fmt.Sprintf("query %q failed: %v", e.Query, e.Err)
}
func (e *QueryError) Unwrap() error {
return e.Err
}
func main() {
err := &QueryError{
Query: "SELECT * FROM users",
Err: errors.New("connection refused"),
}
// 错误链可以正确解包
unwrapped := errors.Unwrap(err)
fmt.Println(unwrapped) // connection refused
// 使用errors.Is检查错误链
if errors.Is(err, unwrapped) {
fmt.Println("Error matches!") // 这会执行
}
}
总结来说,在Go的标准库和常见实践中,即使方法不需要修改接收器,也倾向于使用指针接收器,主要是为了:
- 保持接口实现的一致性
- 避免结构体复制
- 遵循Go的惯用法
- 确保方法集的完整性
在你的具体例子中,将Unwrap()改为值接收器可能会导致类型系统问题,特别是当QueryError需要作为接口值传递时。

