【问题标题】:Custom errors in golang and pointer receiversgolang 和指针接收器中的自定义错误
【发布时间】:2018-10-24 07:08:11
【问题描述】:

通过网络和stackoverflow阅读关于值接收器与指针接收器的信息,我理解基本规则是:如果您不打算修改接收器,并且接收器相对较小,则不需要指针。

然后,阅读有关实现error 接口(例如https://blog.golang.org/error-handling-and-go)的信息,我看到Error() 函数的示例都使用指针接收器。

然而,我们并没有修改接收者,而且结构非常小。

我觉得没有指针的代码更好(return &appError{} vs return appError{})。

这些示例使用指针有什么原因吗?

【问题讨论】:

  • 不,没有。也许只是个人风格或一致性。有时您必须使用指针接收器。如果您的案例不属于“必须”部分:选择您喜欢或有意义的任何内容。

标签: pointers go error-handling pass-by-reference


【解决方案1】:

首先,您链接的博客文章appError 不是 error。它是一个包装器,携带示例的实现使用的错误值和其他相关信息,它们没有被公开,也没有appError 也没有*appError用作error 值。

因此,您引用的示例与您的实际问题无关。但是要回答标题中的问题:

一般来说,一致性可能是原因。如果一个类型有很多方法并且有些需要指针接收器(例如因为它们修改了值),通常用指针接收器声明所有方法是有用的,所以类型的method sets和指针类型不会混淆。

关于error 实现的回答:当您使用struct 值来实现error 值时,使用非指针来实现error 接口是很危险的。为什么会这样?

因为error 是一个接口。接口值为comparable。并且通过比较它们包装的值来比较它们。 你会得到不同的比较结果,根据它们里面包含的值/类型!因为如果你在它们中存储指针,如果它们存储相同的指针,错误值将是相等的。如果你在其中存储非指针(结构),如果结构值相等,它们就相等。

详细说明并举例说明:

标准库有一个errors 包。您可以使用errors.New() 函数从string 值创建错误值。如果你看一下它的实现(errors/errors.go),很简单:

// Package errors implements functions to manipulate errors.
package errors

// New returns an error that formats as the given text.
func New(text string) error {
    return &errorString{text}
}

// errorString is a trivial implementation of error.
type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

实现返回一个指向一个非常简单的结构值的指针。这样,如果您创建 2 个具有相同 string 值的错误值,它们将不相等:

e1 := errors.New("hey")
e2 := errors.New("hey")
fmt.Println(e1, e2, e1 == e2)

输出:

hey hey false

这是故意的。

现在如果你要返回一个非指针:

func New(text string) error {
    return errorString{text}
}

type errorString struct {
    s string
}

func (e errorString) Error() string {
    return e.s
}

具有相同 string 的 2 个错误值将相等:

e1 = New("hey")
e2 = New("hey")
fmt.Println(e1, e2, e1 == e2)

输出:

hey hey true

尝试Go Playground 上的示例。

为什么这很重要的一个典型例子:查看存储在变量io.EOF 中的错误值:

var EOF = errors.New("EOF")

预计io.Reader 实现会返回此特定错误值以表示输入结束。因此,您可以和平地比较Reader.Read()io.EOF 返回的错误,以判断是否到达输入结束。您可以确定,如果它们偶尔返回自定义错误,它们将永远不会等于 io.EOF,这是 errors.New() 保证的(因为它返回指向未导出结构值的指针)。

【讨论】:

  • 非常详细的回复。一位同事认为,如果我们使用非指针,我们将无法执行通常的if err != nil - 是吗?
  • @NathanH 不,这不正确。 nil 不是结构的有效值,这是真的,但如果您使用 error 值(可能包装您的非指针结构),error 是一个接口类型,其值可能为 nil (这是接口类型的零值)。但我建议阅读此答案以澄清问题:Hiding nil values, understanding why golang fails here
  • PG。 GOPL (Donovan,Kernighan) 的 196 条提出了同样的观点,但这个答案详细解释了这一点。在每种情况下返回指针或结构,New 创建一个不同的 errorString 实例。就是new返回的值,一个错误接口,就是问题所在。当返回结构时,错误接口的两个实例比较相等,因为它们具有相同的值,即使它们是从 2 个不同的 errorStruts 创建的。当指针被 ret'd 时,错误的两个 ints 是不同的,因为指针指向 errorString 的两个单独的 insts(由每次调用 New 创建)。
  • 我认为“......通常用指针接收器声明所有方法很有用”,可以纠正说“正确性取决于一致性”以及混合同时具有指针和非指针接收器的类型可能会产生不良副作用。这是example
  • @E.Pratt error是一个接口类型,&errorString{}是一个具体类型(*errorString)的值,这个指针类型实现了error接口。返回此值时,将创建一个error 接口值。任何类型实现error的值都可以赋值给error类型的变量,也可以在返回类型为error时返回。
【解决方案2】:

go 中的错误只满足错误接口,即提供.Error() 方法。创建自定义错误,或挖掘 Go 源代码,您会发现错误更多地隐藏在幕后。如果在您的应用程序中填充了一个结构,为了避免在内存中制作副本,将它作为指针传递会更有效。此外,如 Go 编程语言一书中所述:

fmt.Errorf 函数使用 fmt.Sprintf 格式化错误消息并返回新的错误值。我们使用它来构建描述性错误,方法是在原始错误消息中连续添加额外的上下文信息。当错误最终由程序的 main 函数处理时,它应该提供从根本问题到整体故障的清晰因果链,让人联想到 NASA 事故调查:

genesis: crashed: no parachute: G-switch failed: bad relay orientation

由于错误消息经常链接在一起,因此消息字符串不应大写并且应避免换行。产生的错误可能很长,但当被 grep 等工具发现时,它们将是自包含的。

从这里我们可以看到,如果单个“错误类型”包含大量信息,并且在此之上我们将它们“链接”在一起以创建详细消息,那么使用指针将是实现这一目标的最佳方式.

【讨论】:

    【解决方案3】:

    我们可以从错误处理的角度来看待这一点,而不是从错误创建的角度来看。

    错误定义方的故事

    type ErrType1 struct {}
    
    func (e *ErrType1) Error() string {
        return "ErrType1"
    }
    
    type ErrType2 struct {}
    
    func (e ErrType2) Error() string {
        return "ErrType1"
    }
    

    错误处理方的故事

    err :=  someFunc()
    switch err.(type) {
    case *ErrType1
       ...
    case ErrType2, *ErrType2
       ...
    default
       ...
    }
    

    如您所见,如果您在值接收器上实现了错误类型,那么在进行类型断言时,您需要担心这两种情况。

    对于ErrType2&ErrType2{}ErrType2{}都满足接口。

    因为someFunc 返回一个error 接口,你永远不知道它返回的是结构体值还是结构体指针,尤其是当someFunc 不是你写的时候。

    因此,使用指针接收器不会阻止用户将指针作为错误返回。

    话虽如此,所有其他方面,例如 堆栈与堆(内存分配、GC 压力)仍然适用。

    根据您的用例选择实施。

    一般来说,由于我上面演示的原因,我更喜欢指针接收器。我更喜欢友好的 API 而不是性能,有时,当错误类型包含大量信息时,它的性能会更高。

    【讨论】:

      【解决方案4】:

      没有:)

      https://blog.golang.org/error-handling-and-go#TOC_2.

      Go 接口允许使用 error 的代码处理符合错误接口的任何内容

      type error interface {
          Error() string
      }
      

      就像您提到的,如果您不打算修改状态,则几乎没有动力传递指针:

      • 分配到堆
      • GC 压力
      • 可变状态和并发等

      顺便说一句,我个人认为看到这样的例子就是为什么新的 Go 程序员默认偏爱指针接收器的原因。

      【讨论】:

      • 顺便说一下,如果你的结构打算在接口内部使用,那么默认值应该是一个值接收器。指针接收器只有在将指针传递给接口时才能实现接口。值接收器适用于指针或值。 Details
      【解决方案5】:

      go 之旅很好地解释了指针接收器的一般原因:

      https://tour.golang.org/methods/8

      使用指针接收器有两个原因。

      第一个是让方法可以修改其接收者指向的值。

      一般来说,给定类型的所有方法都应该有值或指针接收器,但不能两者兼有。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2014-07-30
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2022-09-22
        • 2016-02-29
        • 2017-07-23
        相关资源
        最近更新 更多