如何确定我在Go语言中遇到的错误类型?

huangapple go评论86阅读模式
英文:

How do I work out what type of error my error is in go?

问题

我不确定如何准确地表达这个问题,我看到其他人提出了类似的问题,但并没有真正得到答案(这告诉我我问错了问题,但我不确定如何以其他方式解决这个问题)。

我正在尝试学习一些基本的Go语言,但在第一个障碍上遇到了困难。

在我的测试代码中,我正在进行一个基本的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"
    }
}

这显然是一种笨拙的方法,所以我搜索了一下,看看是否有更好的方法来判断引发了什么类型的错误,我找到了以下的Stack Overflow答案:

https://stackoverflow.com/questions/64210407/how-can-i-check-specific-golang-net-http-error-code

> "errors"包有As、Is函数来解包特定的错误类型,而"net"包有一个*DNSError类型。所以:

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

这段代码是有效的,但我完全不知道是如何得出这个结论的。下面是我尝试理解这个问题的方法,我想知道我哪里错了。我大致了解了.Is()和.As()的作用,但我不明白如何确定要提供给这些函数的错误“类型”,而不是猜测或事先知道。

我查看了client.get()的文档这里,文档中写道:
>返回的任何错误都将是*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没有包含其他错误,所以对我来说,这表明它是错误链的底部,也是我想要处理的“真正”错误(或者至少是我想要处理的错误之一)。

问题是,这不是一个可行的方法,我不明白我们应该如何处理这个问题。在我找到初始的Stack Overflow文章之前,我不知道net.DNSError是一个存在的东西,也不知道我的错误可能是那种“类型”。

如果你不知道特定的错误类型存在,并且一个函数调用可能是那种类型,你怎么知道呢?

我对Go语言中的接口和类型有非常有限的了解,我相信这在这里并没有帮助,但对我来说,在拥有一个错误和知道要检查的错误类型之间存在着巨大的差距。我希望这个问题说得通!

英文:

I'm not sure exactly how to phrase this question, and I've seen others ask similar but not really come up with answers (which tells me I'm asking the wrong question, but I'm not sure how else to approach this).

I'm trying to learn some basic Go, and I've come unstuck at the first hurdle.

In my test code, I'm doing a basic http GET to a domain that doesn't exist to trigger a DNS warning. I worked out that err.error() returns a string, so to assert whether it was a DNS error, I used string comparison:

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

This is obviously hacky, so I did some googling to see if there is a better way to work out what kind of error was raised, and I found the following SO answer:

https://stackoverflow.com/questions/64210407/how-can-i-check-specific-golang-net-http-error-code

>Package "errors" has functions As, Is to unwrap specific error types, and package "net" has a *DNSError type. So:

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

This code works, but I have absolutely zero idea how this conclusion was reached. Below is how I'm approaching trying to understand this, and I'd like to know where I'm going wrong. I understand (vaguely) what .Is() and .As() are doing, but what I don't understand is how to work out what error "type" to provide those functions without guessing or prior knowledge.

I looked at the client.get() documentation here which says:
>Any returned error will be of type *url.Error.

some more googling and I found that I need to cast(?) the error to that type to work with it:

urlErr := err.(*url.Error)

the *url.Error contains:

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

so I then look at the net.OpError contained in the url.Error:

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)}

I then do the same thing and "unpack" the net.DNSError contained within net.OpError:

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}

The net.DNSError doesn't "contain" any other errors so to me, this suggests it's the bottom of the chain and the "real" error (or, at least, one I wanted to work with).

Thing is, this is not a viable approach, and I don't understand how we're supposed to approach this. Before the initial SO article I found, I had no idea that net.DNSError is a thing, or that my error could be of that "type".

If you didn't know a particular error type exists, and that a function call could possibly be of that type, how would you know?

I have a very limited understanding of interfaces and types in general in Go, which I'm sure isn't helping here, but to me there seems to be a huge leap between having an error and knowing what kind of error to check it could be. I hope this question makes sense!

答案1

得分: 3

首先,我建议阅读这篇博文: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) {
  // 在这里使用dnsErr
}

**更新:**从概念上讲,你应该知道你可以处理的错误类型:要么处理某个特定的错误,如果你可以处理它,要么将其包装并返回给上层。这在其他语言中处理错误/异常时也是一种常见的做法。所以我的建议是:只处理你知道如何处理的异常。在HTTP请求的情况下,通常是带有状态码的HTTP错误,DNS错误通常返回给调用者函数。


这是关于errors包中函数的一些详细信息和示例。

你可以使用errors包中的errors.Aserrors.Is函数。例如,如果你有一个自定义的错误类型:

type myError struct {
    code int
}

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

var fooErr = &myError{1}

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

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)
    // 我稍后会展示As方法
	if !errors.As(err, &as) {
		return false
	}
	return e.code == as.code
}

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

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
英文:

First, I'd suggest reading this blog post: https://go.dev/blog/go1.13-errors

Short answer to your question: you're correct, to check that a function returns net.DNSError and access its internals you can use errors.As function:

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

Update: conceptually, it's supposed that you know the type of error you can handle: so you either handle some particular error if you can handle it or wrap and return it to upper level. It's a common practice when working with errors/exceptions in other languages too. So my advice: handle only such exceptions that you know how to process. In case of HTTP requests, it's usually http errors with status codes, DNS errors are usually returned to the caller func.


This is some details with examples for functions from errors package.

You can use errors.As and errrors.Is from errors package. For instance, if you have a custom type of error:

type myError struct {
    code int
}

You can check unknown error by reference with errors.Is():

var fooErr = &myError{1}

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

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

or if you want to implement custom logic for comparing your errors (e.g. by code in this example), you can add Is method to your error type:

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
}

Also, Is method can "unwrap" your error from wrapper type, e.g. if you want to create a composition of errors, you may add a new struct with Unwrap method for that or use fmt.Errorf method with %w parameter:

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
}

Or using 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

Another important method is errors.As, it works similar: you either have the exact type of your error to check or you implement As method on your error or error is wrapped by another error:

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

Or with As method:


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
}

And same for wrapping:

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

答案2

得分: 1

当加载网页时,如果页面无法加载,用户会做什么?他们会点击重新加载。如果持续加载失败,用户最终会研究错误信息,因为可能有多种原因:

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

用户对这些错误的反应决定了是放弃、重试还是以不同的方式重试。你的问题始于http.Get的错误(而不是HTTP状态码),但是同样的原则适用。

那么,如果请求失败了,你应该怎么办?如果http.Get是一个更大任务的关键部分,那么整个任务必须失败。如果运行更大任务的成本很高,你可能希望在失败之前重试http.Get(指数退避重试等)。网络错误通常会被记录,因为很少有策略决策可以确定重试是否有效(客户端DNS与服务器504)。

例如,在REST API服务器实现中,身份验证中间件将返回登录问题的错误。在这里,错误类型很重要,以及如何对其做出反应:

  • 身份验证数据库宕机
    • 服务器:记录完整错误;发送电子邮件给管理员等。
    • 客户端:接收简洁的501(服务不可用)
  • 无效的凭据
    • 服务器:记录完整错误
    • 客户端:接收简洁的401(未经授权)

客户端只会收到操作失败的简要摘要,但服务器会记录完整的错误详细信息,并对其做出反应(警报管理员,向用户发送锁定帐户的电子邮件,甚至是成功登录,但来自未识别的设备)。

英文:

What does a user do when loading a web-page and the page fails to load? They hit reload. If it keeps failing, the user eventually studies the error message, as it could be one of many reasons:

  • DNS error (client)
  • 504 gateway error (server)
  • 400 (user: bad request)
  • 404 (user: not found)
  • 401 (user unauthorized)

How the user reacts to these errors determines whether to: give-up; try again; or try again in a different manner. Your question began with the http.Get errors (not http status codes) - but the same principle applies.

So, what should you do if the request fails? If the http.Get is a critical part of a larger task, then the whole task must fail. If the larger task is expensive to run, you may want to retry the http.Get (exponential backup retry etc.) before failing. Network errors are typically logged as there's very little policy decisions one can make to determine if a retry will work or not (client DNS vs. server 504).

In a REST-API server implementation, for example, the authentication middleware will return errors for login issues. Here is where the type of error is important and how one can react to it:

  • authentication-DB is down
    • server: log full error; email admin etc.
    • client: receives terse 501 (service unavailable)
  • invalid credentials
    • server: log full error
    • client: receives terse 401 (unauthorized)

the client is only given a very brief summary that the operation failed, but the server logs the full details of the error and will react to it (alert admin, email user of locked out account or even a successful login, but from an unrecognized device).

huangapple
  • 本文由 发表于 2021年10月14日 17:24:32
  • 转载请务必保留本文链接:https://go.coder-hub.com/69568234.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定