Go – 如何优雅地处理多个错误?

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

Go — handling multiple errors elegantly?

问题

有没有办法清理一下这个(在我看来)看起来很可怕的代码?

aJson, err1 := json.Marshal(a)
bJson, err2 := json.Marshal(b)
cJson, err3 := json.Marshal(c)
dJson, err4 := json.Marshal(d)
eJson, err5 := json.Marshal(e)
fJson, err6 := json.Marshal(f)
gJson, err4 := json.Marshal(g)
if err1 != nil {
return err1
} else if err2 != nil {
return err2
} else if err3 != nil {
return err3
} else if err4 != nil {
return err4
} else if err5 != nil {
return err5
} else if err5 != nil {
return err5
} else if err6 != nil {
return err6
}

具体来说,我指的是错误处理。能够一次性处理所有的错误会很好。

英文:

Is there a way to clean up this (IMO) horrific-looking code?

	aJson, err1 := json.Marshal(a)
bJson, err2 := json.Marshal(b)
cJson, err3 := json.Marshal(c)
dJson, err4 := json.Marshal(d)
eJson, err5 := json.Marshal(e)
fJson, err6 := json.Marshal(f)
gJson, err4 := json.Marshal(g)
if err1 != nil {
	return err1
} else if err2 != nil {
	return err2
} else if err3 != nil {
	return err3
} else if err4 != nil {
	return err4
} else if err5 != nil {
	return err5
} else if err5 != nil {
	return err5
} else if err6 != nil {
	return err6
} 

Specifically, I'm talking about the error handling. It would be nice to be able to handle all the errors in one go.

答案1

得分: 23

var err error
f := func(dest *D, src S) bool {
*dest, err = json.Marshal(src)
return err == nil
} // EDIT: removed ()

f(&aJson, a) &&
f(&bJson, b) &&
f(&cJson, c) &&
f(&dJson, d) &&
f(&eJson, e) &&
f(&fJson, f) &&
f(&gJson, g)
return err

英文:
var err error
f := func(dest *D, src S) bool {
	*dest, err = json.Marshal(src)
	return err == nil
} // EDIT: removed ()

f(&aJson, a) &&
	f(&bJson, b) &&
	f(&cJson, c) &&
	f(&dJson, d) &&
	f(&eJson, e) &&
	f(&fJson, f) &&
	f(&gJson, g)
return err

答案2

得分: 14

将结果放入一个切片中,而不是变量,将初始值放入另一个切片中进行迭代,并在迭代过程中如果有错误则返回。

var result [][]byte
for _, item := range []interface{}{a, b, c, d, e, f, g} {
	res, err := json.Marshal(item)
	if err != nil {
		return err
	}
	result = append(result, res)
}

甚至可以重用一个数组,而不是使用两个切片。

var values, err = [...]interface{}{a, b, c, d, e, f, g}, error(nil)
for i, item := range values {
	if values[i], err = json.Marshal(item); err != nil {
		return err
	}
}

当然,这将需要类型断言来使用结果。

英文:

Put the result in a slice instead of variables, put the intial values in another slice to iterate and return during the iteration if there's an error.

var result [][]byte
for _, item := range []interface{}{a, b, c, d, e, f, g} {
	res, err := json.Marshal(item)
	if err != nil {
		return err
	}
	result = append(result, res)
}

You could even reuse an array instead of having two slices.

var values, err = [...]interface{}{a, b, c, d, e, f, g}, error(nil)
for i, item := range values {
	if values[i], err = json.Marshal(item); err != nil {
		return err
	}
}

Of course, this'll require a type assertion to use the results.

答案3

得分: 7

定义一个函数。

func marshalMany(vals ...interface{}) ([][]byte, error) {
	out := make([][]byte, 0, len(vals))
	for i := range vals {
		b, err := json.Marshal(vals[i])
		if err != nil {
			return nil, err
		}
		out = append(out, b)
	}
	return out, nil
}

你没有提到你希望如何处理错误。一个失败就全部失败?第一个失败?收集成功还是丢弃?

英文:

define a function.

func marshalMany(vals ...interface{}) ([][]byte, error) {
	out := make([][]byte, 0, len(vals))
	for i := range vals {
		b, err := json.Marshal(vals[i])
		if err != nil {
			return nil, err
		}
		out = append(out, b)
	}
	return out, nil
}

you didn't say anything about how you'd like your error handling to work. Fail one, fail all? First to fail? Collect successes or toss them?

答案4

得分: 5

我相信这里的其他答案对于你的具体问题是正确的,但更一般地说,panic可以用来简化错误处理,同时仍然是一个行为良好的库(即不会跨包边界panic)。

考虑以下代码:

func mustMarshal(v interface{}) []byte {
    bs, err := json.Marshal(v)
    if err != nil {
        panic(err)
    }
    return bs
}

func encodeAll() (err error) {
    defer func() {
        if r := recover(); r != nil {
            var ok bool
            if err, ok = r.(error); ok {
                return
            }
            panic(r)
        }
    }()

    ea := mustMarshal(a)
    eb := mustMarshal(b)
    ec := mustMarshal(c)

    return nil
}

这段代码使用mustMarshal在序列化值时出现问题时会panic。但是encodeAll函数会从panic中恢复,并将其作为普通的错误值返回。在这种情况下,客户端不会暴露给panic

但是需要注意的是:在所有地方都使用这种方法并不符合惯用法。它也可能更糟,因为它不适合处理每个单独的错误,而是更多地将每个错误视为相同。但是当需要处理大量错误时,它有其用途。例如,我在一个Web应用程序中使用这种方法,其中顶级处理程序可以捕获不同类型的错误,并根据错误的类型适当地将其显示给用户(或记录到日志文件)。

当需要处理大量错误时,它可以使代码更简洁,但代价是失去了Go的惯用法和对每个错误的特殊处理。另一个缺点是它可能阻止本应该panic的东西实际上发生panic。(但这可以通过使用自己的错误类型来轻松解决。)

英文:

I believe the other answers here are correct for your specific problem, but more generally, panic can be used to shorten error handling while still being a well-behaving library. (i.e., not panicing across package boundaries.)

Consider:

func mustMarshal(v interface{}) []byte {
	bs, err := json.Marshal(v)
	if err != nil {
		panic(err)
	}
	return bs
}

func encodeAll() (err error) {
	defer func() {
		if r := recover(); r != nil {
			var ok bool
			if err, ok = r.(error); ok {
				return
			}
			panic(r)
		}
	}()
	
	ea := mustMarshal(a) 	
	eb := mustMarshal(b)
	ec := mustMarshal(c)
	
	return nil
}

This code uses mustMarshal to panic whenever there is a problem marshaling a value. But the encodeAll function will recover from the panic and return it as a normal error value. The client in this case is never exposed to the panic.

But this comes with a warning: using this approach everywhere is not idiomatic. It can also be worse since it doesn't lend itself well to handling each individual error specially, but more or less treating each error the same. But it has its uses when there are tons of errors to handle. As an example, I use this kind of approach in a web application, where a top-level handler can catch different kinds of errors and display them appropriately to the user (or a log file) depending on the kind of error.

It makes for terser code when there is a lot of error handling, but at the loss of idiomatic Go and handling each error specially. Another down-side is that it could prevent something that should panic from actually panicing. (But this can be trivially solved by using your own error type.)

答案5

得分: 2

你可以创建一个可重用的方法来处理多个错误,这个实现只会显示最后一个错误,但你可以通过修改以下代码来返回每个错误消息的组合:

func hasError(errs ...error) error {
	for i, _ := range errs {
		if errs[i] != nil {
			return errs[i]
		}
	}
	return nil
}

aJson, err := json.Marshal(a)
bJson, err1 := json.Marshal(b)
cJson, err2 := json.Marshal(c)

if error := hasError(err, err1, err2); error != nil {
	return error
}
英文:

You can create a reusable method to handle multiple errors, this implementation will only show the last error but you could return every error msg combined by modifying the following code:

func hasError(errs ...error) error {
	for i, _ := range errs {
		if errs[i] != nil {
			return errs[i]
		}
	}
	return nil
}

aJson, err := json.Marshal(a)
bJson, err1 := json.Marshal(b)
cJson, err2 := json.Marshal(c)

if error := hasError(err, err1, err2); error != nil {
	return error
}

答案6

得分: 2

你可以使用Hashicorp的go-multierror

var merr error

如果 err := step1(); err != nil {
    merr = multierror.Append(merr, err)
}
如果 err := step2(); err != nil {
    merr = multierror.Append(merr, err)
}

返回 merr
英文:

You can use go-multierror by Hashicorp.

var merr error

if err := step1(); err != nil {
    merr = multierror.Append(merr, err)
}
if err := step2(); err != nil {
    merr = multierror.Append(merr, err)
}

return merr

答案7

得分: 0

另一个角度是,我们是否真的应该问“如何”处理令人讨厌的冗长性。这个建议在很大程度上取决于上下文,所以要小心。

为了决定处理json.Marshal错误是否值得,我们可以检查其实现,看看何时返回错误。为了将错误返回给调用者并保持代码简洁,json.Marshal 在内部使用panicrecover,类似于异常的方式。它定义了一个内部辅助方法,当调用时,会使用给定的错误值引发panic。通过查看每个调用此函数的地方,我们了解到json.Marshal在以下情况下会出错:

  • 在实现了json.Marshalerencoding.TextMarshaler的类型的值/字段上调用MarshalJSONMarshalText会返回错误,换句话说,自定义编组方法失败;
  • 输入是/包含循环(自引用)结构;
  • 输入是/包含不受支持类型(complexchanfunc)的值;
  • 输入是/包含浮点数,其值为NaNInfinity(这些在规范中不允许,参见第2.4节);
  • 输入是/包含json.Number字符串,其表示不正确的数字(例如,"foo"而不是"123")。

现在,编组数据的常见场景是创建API响应。在这种情况下,由于服务器自身生成数据,您将百分之百拥有满足编组器约束和有效值的数据类型。在使用用户提供的输入的情况下,数据应该事先进行验证,因此它仍然不应该导致编组器出现问题。此外,我们可以看到,除了自定义编组器错误之外,所有其他错误都是在运行时发生的,因为Go的类型系统无法自行强制执行所需的条件。在给出所有这些观点之后,问题来了:鉴于我们对数据类型和值的控制,我们是否需要处理json.Marshal的错误呢?

可能不需要。对于像

type Person struct {
    Name string
    Age  int
}

这样的类型,现在很明显json.Marshal不会失败。当类型看起来像

type Foo struct {
    Data any
}

any是Go 1.18中interface{}的新别名)时,情况就变得棘手了,因为在编译时无法保证Foo.Data将保存一个有效类型的值,但我仍然认为,如果Foo被序列化为响应,Foo.Data也将是可序列化的。InfinityNaN浮点数仍然是一个问题,但是,鉴于JSON标准的限制,如果要序列化这两个特殊值,无论如何都不能使用JSON数字,因此您将不得不寻找另一种解决方案,这意味着您最终将避免错误。

总之,我的观点是您可能可以这样做:

aJson, _ := json.Marshal(a)
bJson, _ := json.Marshal(b)
cJson, _ := json.Marshal(c)
dJson, _ := json.Marshal(d)
eJson, _ := json.Marshal(e)
fJson, _ := json.Marshal(f)
gJson, _ := json.Marshal(g)

并且可以正常运行。如果您想要严谨一些,可以使用一个辅助函数,例如:

func must[T any](v T, err error) T {
    if err != nil {
        panic(err)
    }
    return v
}

(注意Go 1.18泛型的使用),然后执行以下操作:

aJson := must(json.Marshal(a))
bJson := must(json.Marshal(b))
cJson := must(json.Marshal(c))
dJson := must(json.Marshal(d))
eJson := must(json.Marshal(e))
fJson := must(json.Marshal(f))
gJson := must(json.Marshal(g))

当您有类似HTTP服务器的情况时,每个请求都包装在一个从panic中恢复并向客户端响应状态500的中间件中。这也是您关心这些意外错误的地方——当您根本不希望程序/服务崩溃时。对于一次性脚本,您可能希望停止操作并转储堆栈跟踪。

如果您对将来如何更改类型不确定,不信任您的测试,数据可能不完全受您控制,代码库太大以至于无法跟踪数据或任何其他导致对数据正确性产生不确定性的原因,最好处理错误。注意您所处的上下文!


附:在实际中忽略错误通常是可取的。例如,bytes.Bufferstrings.Builder上的Write*方法从不返回错误;fmt.Fprintf在格式字符串有效且写入器不返回错误的情况下也不返回错误;bufio.Writer在底层写入器不返回错误的情况下也不返回错误。您会发现一些类型实现了返回错误的方法的接口,但实际上并不返回任何错误。在这些情况下,如果您知道具体类型,处理错误会显得冗长和多余。您更喜欢哪种方式:

var sb strings.Builder
if _, err := sb.WriteString("hello "); err != nil {
    return err
}
if _, err := sb.WriteString("world!"); err != nil {
    return err
}

还是

var sb strings.Builder
sb.WriteString("hello ")
sb.WriteString("world!")

(当然,忽略它可以是一个WriteString调用)?

给定的示例将写入内存缓冲区,除非机器内存不足,否则您无法在Go中处理的错误永远不会失败。您的代码中将出现其他类似的情况——盲目处理错误几乎没有任何价值!谨慎为上——如果实现更改并返回错误,您可能会陷入麻烦。标准库或经过良好验证的包是可以省略错误检查的好选择,如果可能的话。

英文:

Another perspective on this is, instead of asking "how" to handle the abhorrent verbosity, whether we actually "should". This advice is heavily dependent on context, so be careful.

In order to decide whether handling the json.Marshal error is worth it, we can inspect its implementation to see when errors are returned. In order to return errors to the caller and preserve code terseness, json.Marshal uses panic and recover internally in a manner akin to exceptions. It defines an internal helper method which, when called, panics with the given error value. By looking at each call of this function, we learn that json.Marshal errors in the given scenarios:

  • calling MarshalJSON or MarshalText on a value/field of a type which implements json.Marshaler or encoding.TextMarshaler returns an error—in other words, a custom marshaling method fails;
  • the input is/contains a cyclic (self-referencing) structure;
  • the input is/contains a value of an unsupported type (complex, chan, func);
  • the input is/contains a floating-point number which is NaN or Infinity (these are not allowed by the spec, see section 2.4);
  • the input is/contains a json.Number string that is an incorrect number representation (for example, "foo" instead of "123").

Now, a usual scenario for marshaling data is creating an API response, for example. In that case, you will 100% have data types that satisfy all of the marshaler's constraints and valid values, given that the server itself generates them. In the situation user-provided input is used, the data should be validated anyway beforehand, so it should still not cause issues with the marshaler. Furthermore, we can see that, apart from the custom marshaler errors, all the other errors occur at runtime because Go's type system cannot enforce the required conditions by itself. With all these points given, here comes the question: given our control over the data types and values, do we need to handle json.Marshal's error at all?

Probably no. For a type like

type Person struct {
    Name string
    Age  int
}

it is now obvious that json.Marshal cannot fail. It is trickier when the type looks like

type Foo struct {
    Data any
}

(any is a new Go 1.18 alias for interface{}) because there is no compile-time guarantee that Foo.Data will hold a value of a valid type—but I'd still argue that if Foo is meant to be serialized as a response, Foo.Data will also be serializable. Infinity or NaN floats remain an issue, but, given the JSON standard limitation, if you want to serialize these two special values you cannot use JSON numbers anyway, so you'll have to look for another solution, which means that you'll end up avoiding the error anyway.

To conclude, my point is that you can probably do:

aJson, _ := json.Marshal(a)
bJson, _ := json.Marshal(b)
cJson, _ := json.Marshal(c)
dJson, _ := json.Marshal(d)
eJson, _ := json.Marshal(e)
fJson, _ := json.Marshal(f)
gJson, _ := json.Marshal(g)

and live fine with it. If you want to be pedantic, you can use a helper such as:

func must[T any](v T, err error) T {
    if err != nil {
        panic(err)
    }
    return v
}

(note the Go 1.18 generics usage) and do

aJson := must(json.Marshal(a))
bJson := must(json.Marshal(b))
cJson := must(json.Marshal(c))
dJson := must(json.Marshal(d))
eJson := must(json.Marshal(e))
fJson := must(json.Marshal(f))
gJson := must(json.Marshal(g))

This will work nice when you have something like an HTTP server, where each request is wrapped in a middleware that recovers from panics and responds to the client with status 500. It's also where you would care about these unexpected errors—when you don't want the program/service to crash at all. For one-time scripts you'll probably want to have the operation halted and a stack trace dumped.

If you're unsure of how your types will be changed in the future, you don't trust your tests, data may not be in your full control, the codebase is too big to trace the data or whatever other reason which causes uncertainty over the correctness of your data, it is better to handle the error. Pay attention to the context you're in!


P.S.: Pragmatically ignoring errors should be generally sought after. For example, the Write* methods on bytes.Buffer, strings.Builder never return errors; fmt.Fprintf, with a valid format string and a writer that doesn't return errors, also returns no errors; bufio.Writer aswell doesn't, if the underlying writer doesn't return. You will find some types implement interfaces with methods that return errors but don't actually return any. In these cases, if you know the concrete type, handling errors is unnecessarily verbose and redundant. What do you prefer,

var sb strings.Builder
if _, err := sb.WriteString("hello "); err != nil {
    return err
}
if _, err := sb.WriteString("world!"); err != nil {
    return err
}

or

var sb strings.Builder
sb.WriteString("hello ")
sb.WriteString("world!")

(of course, ignoring that it could be a single WriteString call)?

The given examples write to an in-memory buffer, which unless the machine is out of memory, an error which you cannot handle in Go, cannot ever fail. Other such situations will surface in your code—blindly handling errors adds little to no value! Caution is key—if an implementation changes and does return errors, you may be in trouble. Standard library or well-established packages are good candidates for eliding error checking, if possible.

huangapple
  • 本文由 发表于 2013年3月14日 06:06:40
  • 转载请务必保留本文链接:https://go.coder-hub.com/15397419.html
匿名

发表评论

匿名网友

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

确定