单值上下文中的多个值

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

Multiple values in single-value context

问题

由于Go语言中的错误处理机制,我经常会遇到返回多个值的函数。到目前为止,我处理这个问题的方式非常混乱,我正在寻找编写更清晰代码的最佳实践。

假设我有以下函数:

type Item struct {
   Value int
   Name string
}

func Get(value int) (Item, error) {
  // 一些代码

  return item, nil
}

如何优雅地将一个新变量赋值给item.Value?在引入错误处理之前,我的函数只返回item,我可以简单地这样做:

val := Get(1).Value

现在我这样做:

item, _ := Get(1)
val := item.Value

难道没有一种直接访问第一个返回变量的方法吗?

英文:

Due to error handling in Go, I often end up with multiple values functions. So far, the way I have managed this has been very messy and I am looking for best practices to write cleaner code.

Let's say I have the following function:

type Item struct {
   Value int
   Name string
}

func Get(value int) (Item, error) {
  // some code

  return item, nil
}

How can I assign a new variable to item.Value elegantly. Before introducing the error handling, my function just returned item and I could simply do this:

val := Get(1).Value

Now I do this:

item, _ := Get(1)
val := item.Value

Isn't there a way to access directly the first returned variable?

答案1

得分: 97

如果一个函数返回多个值,当调用该函数时,你不能引用特定结果值的字段或方法。

如果其中一个值是error,那么它存在的原因是函数可能会失败,你不应该绕过它,因为如果你这样做,你的后续代码可能会失败(例如导致运行时恐慌)。

然而,可能会有一些情况,你知道代码在任何情况下都不会失败。在这些情况下,你可以提供一个辅助函数(或方法),它会丢弃error(或在仍然发生错误时引发运行时恐慌)。
如果你从代码中提供函数的输入值,并且你知道它们是有效的,那么这种情况就可能发生。
templateregexp包是很好的例子:如果你在编译时提供一个有效的模板或正则表达式,你可以确保它们在运行时始终可以被解析而不会出错。因此,template包提供了Must(t *Template, err error) *Template函数,而regexp包提供了MustCompile(str string) *Regexp函数:它们不返回error,因为它们的预期用途是输入保证有效。

示例:

// "text"是一个有效的模板,解析它不会失败
var t = template.Must(template.New("name").Parse("text"))

// `^[a-z]+\[[0-9]+\]$`是一个有效的正则表达式,总是可以编译
var validID = regexp.MustCompile(`^[a-z]+\[[0-9]+\]$`)

回到你的情况

如果你可以确定Get()对于某些输入值不会产生error,你可以创建一个辅助函数Must(),它不会返回error,但如果仍然发生错误,会引发运行时恐慌:

func Must(i Item, err error) Item {
    if err != nil {
        panic(err)
    }
    return i
}

但你不应该在所有情况下都使用这个函数,只有当你确定它会成功时才使用。用法:

val := Must(Get(1)).Value

Go 1.18泛型更新: Go 1.18添加了泛型支持,现在可以编写一个通用的Must()函数:

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

这在github.com/icza/gog中可用,作为gog.Must()(声明:我是作者)。

替代方案/简化

如果将Get()调用合并到辅助函数中,你甚至可以进一步简化它,让我们称之为MustGet

func MustGet(value int) Item {
    i, err := Get(value)
    if err != nil {
        panic(err)
    }
    return i
}

用法:

val := MustGet(1).Value

查看一些有趣/相关的问题:

https://stackoverflow.com/questions/52653779/how-to-parse-multiple-returns-in-golang/52654950#52654950

https://stackoverflow.com/questions/28487036/return-map-like-ok-in-golang-on-normal-functions/28487270#28487270

英文:

In case of a multi-value return function you can't refer to fields or methods of a specific value of the result when calling the function.

And if one of them is an error, it's there for a reason (which is the function might fail) and you should not bypass it because if you do, your subsequent code might also fail miserably (e.g. resulting in runtime panic).

However there might be situations where you know the code will not fail in any circumstances. In these cases you can provide a helper function (or method) which will discard the error (or raise a runtime panic if it still occurs).
This can be the case if you provide the input values for a function from code, and you know they work.
Great examples of this are the template and regexp packages: if you provide a valid template or regexp at compile time, you can be sure they can always be parsed without errors at runtime. For this reason the template package provides the Must(t *Template, err error) *Template function and the regexp package provides the MustCompile(str string) *Regexp function: they don't return errors because their intended use is where the input is guaranteed to be valid.

Examples:

// "text" is a valid template, parsing it will not fail
var t = template.Must(template.New("name").Parse("text"))

// `^[a-z]+\[[0-9]+\]$` is a valid regexp, always compiles
var validID = regexp.MustCompile(`^[a-z]+\[[0-9]+\]$`)

Back to your case

IF you can be certain Get() will not produce error for certain input values, you can create a helper Must() function which would not return the error but raise a runtime panic if it still occurs:

func Must(i Item, err error) Item {
    if err != nil {
        panic(err)
    }
    return i
}

But you should not use this in all cases, just when you're sure it succeeds. Usage:

val := Must(Get(1)).Value

Go 1.18 generics update: Go 1.18 adds generics support, it is now possible to write a generic Must() function:

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

This is available in github.com/icza/gog, as gog.Must() (disclosure: I'm the author).

Alternative / Simplification

You can even simplify it further if you incorporate the Get() call into your helper function, let's call it MustGet:

func MustGet(value int) Item {
    i, err := Get(value)
    if err != nil {
        panic(err)
    }
    return i
}

Usage:

val := MustGet(1).Value

See some interesting / related questions:

https://stackoverflow.com/questions/52653779/how-to-parse-multiple-returns-in-golang/52654950#52654950

https://stackoverflow.com/questions/28487036/return-map-like-ok-in-golang-on-normal-functions/28487270#28487270

答案2

得分: 9

是的,有的。

令人惊讶吧?你可以使用一个简单的mute函数从多个返回值中获取特定的值:

package main

import "fmt"
import "strings"

func μ(a ...interface{}) []interface{} {
    return a
}

type A struct {
    B string
    C func() string
}

func main() {
    a := A{
        B: strings.TrimSpace(μ(E())[1].(string)),
        C: μ(G())[0].(func() string),
    }

    fmt.Printf("%s says %s\n", a.B, a.C())
}

func E() (bool, string) {
    return false, "F"
}

func G() (func() string, bool) {
    return func() string { return "Hello" }, true
}

注意,你可以像从切片/数组中选择值一样选择值的编号,然后获取实际值的类型。

你可以从这篇文章中了解更多关于这方面的科学知识。感谢作者。

英文:

Yes, there is.

Surprising, huh? You can get a specific value from a multiple return using a simple mute function:

package main

import "fmt"
import "strings"

func µ(a ...interface{}) []interface{} {
    return a
}

type A struct {
    B string
    C func()(string)
}

func main() {
    a := A {
        B:strings.TrimSpace(µ(E())[1].(string)),
        C:µ(G())[0].(func()(string)),
    }

    fmt.Printf ("%s says %s\n", a.B, a.C())
}

func E() (bool, string) {
    return false, "F"
}

func G() (func()(string), bool) {
    return func() string { return "Hello" }, true
}

https://play.golang.org/p/IwqmoKwVm-

Notice how you select the value number just like you would from a slice/array and then the type to get the actual value.

You can read more about the science behind that from this article. Credits to the author.

答案3

得分: 7

不,但这是一件好事,因为你应该始终处理错误。

有一些技术可以用来推迟错误处理,可以参考 Rob Pike 的文章 Errors are values

在这篇博文中,他举了一个例子,展示了如何创建一个 errWriter 类型,将错误处理推迟到调用 write 完成之后再处理。

英文:

No, but that is a good thing since you should always handle your errors.

There are techniques that you can employ to defer error handling, see Errors are values by Rob Pike.

> ew := &errWriter{w: fd}
ew.write(p0[a:b])
ew.write(p1[c:d])
ew.write(p2[e:f])
// and so on
if ew.err != nil {
return ew.err
}

In this example from the blog post he illustrates how you could create an errWriter type that defers error handling till you are done calling write.

答案4

得分: 5

不,你不能直接访问第一个值。

我想一个解决方法是返回一个值的数组,而不是"item"和"err",然后只需执行
item, _ := Get(1)[0]
但我不建议这样做。

英文:

No, you cannot directly access the first value.

I suppose a hack for this would be to return an array of values instead of "item" and "err", and then just do
item, _ := Get(1)[0]
but I would not recommend this.

答案5

得分: 3

这是一个示例代码,它定义了一个名为Item的结构体和一个名为Get的函数。代码中还有一个名为items的切片,其中包含了三个Item对象。在main函数中,它调用了Get函数,并通过指针传递了一个error对象。如果Get函数返回的value超出了items切片的索引范围,它会将error对象设置为一个新的错误,并返回一个空的Item对象。否则,它会返回items[value]。

英文:

How about this way?

package main

import (
	"fmt"
	"errors"
)

type Item struct {
	Value int
	Name string
}

var items []Item = []Item{{Value:0, Name:"zero"}, 
						{Value:1, Name:"one"}, 
						{Value:2, Name:"two"}}

func main() {
	var err error
	v := Get(3, &err).Value
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(v)
					
}

func Get(value int, err *error) Item {
	if value > (len(items) - 1) {
		*err = errors.New("error")
		return Item{}
	} else {
		return items[value]
	}
}

答案6

得分: 1

这是一个带有假设检查的通用辅助函数:

func assumeNoError(value interface{}, err error) interface{} {
    if err != nil {
        panic("在没有假设的情况下遇到错误:" + err.Error())
    }
    return value
}

由于它返回的是interface{}类型,通常需要将其转换回函数的返回类型。

例如,原始问题中调用了Get(1),它返回(Item, error)

item := assumeNoError(Get(1)).(Item)

使这种转换成为可能的技巧是:从一个函数调用返回的多个值可以作为多变量参数传递给另一个函数。

> 作为一个特例,如果函数或方法g的返回值的数量与另一个函数或方法f的参数数量相等,并且每个返回值都可以分配给f的参数,则调用f(g(parameters_of_g))将在绑定g的返回值到f的参数后按顺序调用f。

这个答案在很大程度上借鉴了现有答案,但没有提供这种形式的简单通用解决方案。

英文:

Here's a generic helper function with assumption checking:

func assumeNoError(value interface{}, err error) interface{} {
	if err != nil {
		panic("error encountered when none assumed:" + err.Error())
	}
	return value
}

Since this returns as an interface{}, you'll generally need to cast it back to your function's return type.

For example, the OP's example called Get(1), which returns (Item, error).

item := assumeNoError(Get(1)).(Item)

The trick that makes this possible: Multi-values returned from one function call can be passed in as multi-variable arguments to another function.

> As a special case, if the return values of a function or method g are equal in number and individually assignable to the parameters of another function or method f, then the call f(g(parameters_of_g)) will invoke f after binding the return values of g to the parameters of f in order.


This answer borrows heavily from existing answers, but none had provided a simple, generic solution of this form.

huangapple
  • 本文由 发表于 2015年1月30日 08:10:16
  • 转载请务必保留本文链接:https://go.coder-hub.com/28227095.html
匿名

发表评论

匿名网友

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

确定