Golang中为什么指针接收器无处不在

Golang中为什么指针接收器无处不在 特别是在这篇文档

func (e *QueryError) Unwrap() error { return e.Err }

据我理解,指针接收器应该只在需要修改接收器值时使用。 在上面的例子中,它只是简单地返回了接收器值的一个字段值。

我是不是漏掉了什么,或者它可以(并且应该)简化为:

func (e QueryError) Unwrap() error { return e.Err }

6 回复

好的,好的。

更多关于Golang中为什么指针接收器无处不在的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


而当没有疑问时。

例如,在这种情况下:

type QueryError struct {
    Query string
    Err   error
}

func (e *QueryError) Error() string { return e.Query + ": " + e.Err.Error() }

显然不需要指向 QueryError,因为没有修改操作,对吗?

(只是想理解一下)。

😊 那么你可以应用以下原则: “如果接收者是一个大型结构体或数组,使用指针接收者效率更高。多大算大?可以假设这相当于将所有元素作为参数传递给方法。如果感觉太大,那么对接收者来说也太大了。”

这是一份描述何时使用指针接收器的优秀文档。

GitHub

golang/go

头像

Go 编程语言。通过在 GitHub 上创建账户来为 golang/go 的开发做出贡献。

简而言之,该文档指出“如有疑问,请使用指针。”

有一种情况使用指针是绝对必要的,但大多数人会忽略它。那就是当你无法复制结构体的状态时。以互斥锁为例,文档明确指出不能复制它们:

互斥锁在首次使用后不得复制。

为什么?因为互斥锁意味着它需要接收所有并发调用,并根据一个参照状态来同步它们。如果你复制了互斥锁及其状态,就无法再进行同步。为了防止这种情况发生,LockUnlock 方法使用了指针接收器,从而确保你无法在副本上调用它们。

在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需要作为接口值传递时。

回到顶部