【问题标题】:How do I work out what type of error my error is in go?我如何计算出我的错误是什么类型的错误?
【发布时间】:2021-10-14 09:24:32
【问题描述】:

我不确定如何准确地表达这个问题,而且我看到其他人提出了类似的问题,但并没有真正想出答案(这告诉我我问错了问题,但我不知道还有什么办法来解决这个问题)。

我正在尝试学习一些基本的围棋,但我在第一个障碍中遇到了困难。

在我的测试代码中,我正在对不存在的域执行基本的 http GET 以触发 DNS 警告。我发现 err.error() 返回一个字符串,所以为了判断它是否是 DNS 错误,我使用了字符串比较:

resp, err := http.Get(link)
if err != nil {
    if strings.Contains(err.Error(), "no such host") == true {
        return "no such host"
    }
}

这显然是 hacky,所以我做了一些谷歌搜索,看看是否有更好的方法来确定引发了什么样的错误,我找到了以下 SO 答案:

How can I check specific golang net/http error code?

包“errors”有函数As,是解包特定的错误类型,包“net”有一个*DNSError类型。所以:

var dnsErr *net.DNSError
if errors.As(err, &dnsErr) {
    ...
}

这段代码有效,但我完全不知道这个结论是如何得出的。以下是我试图理解这一点的方法,我想知道我哪里出错了。我(模糊地)了解 .Is() 和 .As() 正在做什么,但我不明白的是如何在没有猜测或先验知识的情况下计算出什么错误“类型”来提供这些功能。

我查看了 client.get() 文档here,上面写着:

任何返回的错误都是 *url.Error 类型。

更多谷歌搜索,我发现我需要将错误转换(?)该类型才能使用它:

urlErr := err.(*url.Error)

*url.Error 包含:

&url.Error{Op:"Get", URL:"http://doesnotexistkjdfhgsdfsdf.com", Err:(*net.OpError)(0xc00021a2d0)}

所以我再看一下url.Error中包含的net.OpError:

netOpError := urlErr.Err.(*net.OpError)
fmt.Printf("Net Op Error contains: %#v\n", netOpError)
---
Net Op Error contains: &net.OpError{Op:"dial", Net:"tcp", Source:net.Addr(nil), Addr:net.Addr(nil), Err:(*net.DNSError)(0xc0001a0040)}

然后我做同样的事情并“解压”net.OpError 中包含的 net.DNSError:

dnsError := netOpError.Err.(*net.DNSError)
fmt.Printf("DNSError contains: %#v\n", dnsError)
---
DNSError contains: &net.DNSError{Err:"dial udp 169.254.169.254:53: connect: no route to host", Name:"doesnotexistkjdfhgsdfsdf.com", Server:"169.254.169.254:53", IsTimeout:false, IsTemporary:true, IsNotFound:false}

net.DNSError 不“包含”任何其他错误,所以对我来说,这表明它是链的底部和“真正的”错误(或者,至少是我想要处理的错误)。

问题是,这不是一个可行的方法,我不明白我们应该如何处理这个问题。在我发现最初的 SO 文章之前,我不知道 net.DNSError 是一个东西,或者我的错误可能是那种“类型”。

如果您不知道存在特定的错误类型,并且函数调用可能属于该类型,您怎么知道?

我对 Go 中的一般接口和类型的理解非常有限,我敢肯定这在这里没有帮助,但对我来说,在发生错误和知道要发生什么样的错误之间似乎有一个巨大的飞跃检查它可能是。我希望这个问题有意义!

【问题讨论】:

  • 你可以查看这个帖子:go.dev/blog/go1.13-errors
  • 如果您不知道 DNSError 存在,那么您为什么要关心处理它呢?你应该关心你所知道的事情,并以通用的方式处理其余的事情。
  • 如果特定的错误类型与 API 的用户相关,那么可能应该记录这些不同的错误类型,以便用户知道要查找和正确处理的类型。这样,错误类型就成为 API 的一部分。
  • “我不明白如何在没有猜测或先验知识的情况下计算出提供这些功能的错误类型” - 这就是文档的用途。当错误类型相关时,文档应说明可能返回的类型。但正如 rustyx 指出的那样,如果您不知道错误可能是什么类型,那么它可能与您的应用程序逻辑无关。

标签: go error-handling go-interface


【解决方案1】:

首先,我建议阅读这篇博文:https://go.dev/blog/go1.13-errors

简短回答您的问题:您是正确的,要检查函数是否返回 net.DNSError 并访问其内部,您可以使用 errors.As 函数:

rsp, err := http.Get(link)
dnsErr := new(net.DNSError)
if errors.As(err, &dnsErr) {
  // use dnsErr here
}

更新:从概念上讲,假设您知道可以处理的错误类型:因此,如果您可以处理某些特定错误,则可以处理它,或者将其包装并返回给上层。在处理其他语言的错误/异常时,这也是一种常见的做法。所以我的建议是:只处理你知道如何处理的异常。对于 HTTP 请求,通常是带有状态码的 http 错误,DNS 错误通常会返回给调用者 func。


这是来自errors 包的函数示例的一些详细信息。

您可以使用errors 包中的errors.Aserrrors.Is。例如,如果您有自定义类型的错误:

type myError struct {
    code int
}

您可以通过errors.Is() 引用来检查未知错误:

var fooErr = &myError{1}

func foo() (int, err) {...}

func main() {
    _, err := foo()
    fmt.Printf("is fooErr? %v\n", errors.Is(err, fooErr))
}

或者如果你想实现自定义逻辑来比较你的错误(例如在这个例子中通过code),你可以将Is方法添加到你的错误类型中:

func (e *myError) Is(err error) bool {
    as := new(myError)
    // I'll show As method later
    if !errors.As(err, &as) {
        return false
    }
    return e.code == as.code
}

func (e *myError) Is(err error) bool {
    as := new(myError)
    if !errors.As(err, &as) {
        return false
    }
    return e.code == as.code
}

另外,Is 方法可以从包装器类型中“解包”您的错误,例如如果你想创建错误组合,你可以添加一个带有Unwrap 方法的新结构,或者使用带有%w 参数的fmt.Errorf 方法:

type errWrap struct {
    origin error
}

func (e *errWrap) Unwrap() error {
    return e.origin
}

func (e *errWrap) Error() string {
    return fmt.Sprintf("wraps error '%s'", e.origin.Error())
}

var fooErr = &myError{1}

func foo() (int, error) {
    return 0, &errWrap{origin: fooErr}
}

func main() {
    _, err := foo()
    fmt.Printf("err == myError? %v\n", err == fooErr) // false
    fmt.Printf("err is fooErr? %v\n", errors.Is(err, &myError{1})) // true
}

或者使用fmt.Errorf:

    err := fmt.Errorf("wraps: %w", fooErr)
    fmt.Printf("err == myError? %v\n", err == fooErr) // false
    fmt.Printf("err is fooErr? %v\n", errors.Is(err, &myError{1})) // true

另一个重要的方法是errors.As,它的工作原理类似:您要么检查错误的确切类型,要么对错误实施As 方法,或者错误被另一个错误包裹:

    err := &myError{1}
    as := new(myError)
    errors.As(err, &as)
    fmt.Printf("code: %v\n", as.code) // code: 1

或者用As方法:


type anotherError struct {
    anotherCode int
}

func (e *anotherError) Error() string {
    return fmt.Sprintf("code: %d", e.anotherCode)
}

func (e *myError) As(target interface{}) bool {
    if out, ok := target.(**anotherError); ok {
        (*out).anotherCode = e.code
        return true
    }
    return false
}

func main() {
    err := &myError{1}
    as := new(anotherError)
    errors.As(err, &as)
    fmt.Printf("code: %v\n", as.anotherCode) // code: 1
}

包装也一样:

err := fmt.Errorf("wraps: %w", &myError{1})
as := new(myError)
errors.As(err, &as)
fmt.Printf("code: %v\n", as.code) // code: 1

【讨论】:

  • 虽然很好地介绍了 Go 中的错误,但我不确定这是否解决了问题。 Dan W 应该如何弄清楚一个特定的库函数可以返回一个 net.DNSError 而无需深入研究大量文档和/或源代码层?
  • @RichardHuxton 这正是我的挫败感/困惑 - http 包的文档没有显示这一点,那么这个结论是如何得出的?这里的所有答案都是“如果你不知道,那么你就不需要知道”或“你必须已经知道”的变体。 Kirill 的帖子很棒,但是 .Is() 和 .As() 都需要先决条件。
【解决方案2】:

当加载网页并且页面加载失败时,用户会怎么做?他们点击重新加载。如果它一直失败,用户最终会研究错误消息,因为它可能是多种原因之一:

  • DNS 错误(客户端)
  • 504网关错误(服务器)
  • 400(用户:错误请求)
  • 404(用户:未找到)
  • 401(用户未授权)

用户对这些错误的反应如何决定是否:放弃;再试一次;或以不同的方式重试。您的问题始于 http.Get 错误(不是 http 状态代码) - 但同样的原则也适用。

那么,如果请求失败了怎么办?如果http.Get 是更大任务的关键部分,那么整个任务肯定会失败。如果较大的任务运行起来很昂贵,您可能需要在失败之前重试http.Getexponential backup 重试等)。网络错误通常会被记录下来,因为几乎没有策略决定可以确定重试是否有效(客户端 DNS 与服务器 504)。

例如,在 REST-API 服务器实现中,身份验证中间件将针对登录问题返回错误。以下是错误类型很重要的地方以及人们如何应对它:

  • 身份验证-DB 已关闭
    • 服务器:日志已满错误;电子邮件管理员等
    • 客户端:接收简洁的501(服务不可用)
  • 无效的凭据
    • 服务器:日志已满错误
    • 客户端:接收简洁的401(未经授权)

客户端仅给出操作失败的非常简短的摘要,但服务器会记录错误的完整详细信息并将对其做出反应(警告管理员、锁定帐户的电子邮件用户甚至成功登录,但来自无法识别的设备)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-10-18
    • 1970-01-01
    • 2015-05-18
    • 2019-10-25
    • 1970-01-01
    • 2022-06-21
    • 1970-01-01
    • 2016-04-15
    相关资源
    最近更新 更多