10

假设我们要实现以下计算:

outval / err = f3(f3(f1(inval))

其中f1, f2,中的每一个f3都可能因错误而失败,此时我们停止计算并设置err为失败函数返回的错误。(当然嵌套可以任意长)

在像 C++/JAVA/C# 这样的语言中,它可以很容易地通过使用f1,f2f3抛出异常并将计算封闭在 try-catch 块中来完成,而在像 Haskell 这样的语言中,我们可以使用 monads 来代替。

现在我正在尝试在 GO 中实现它,我能想到的唯一方法是明显的 if-else 阶梯,它相当冗长。如果我们不能嵌套调用,我没有问题,但在我看来,在代码中的每一行之后添加错误检查看起来很难看,并且会破坏流程。我想知道是否有更好的方法。

编辑:根据peterSO的评论进行编辑
下面是具体的例子和简单的实现

package main

import "fmt"

func f1(in int) (out int, err error) {
    return in + 1, err
}

func f2(in int) (out int, err error) {
    return in + 2, err
}

func f3(in int) (out int, err error) {
    return in + 3, err
}

func calc(in int) (out int, err error) {
    var temp1, temp2 int
    temp1, err = f1(in)
    if err != nil {
        return temp1, err
    }
    temp2, err = f2(temp1)
    if err != nil {
        return temp2, err
    }
    return f3(temp2)
}

func main() {
     inval := 0
     outval, err := calc3(inval)
     fmt.Println(inval, outval, err)
}

我想说明的是,函数 calc 可能在可能失败的库函数的帮助下进行一些计算,语义是如果任何调用失败,则 calc 会将错误传播给调用者(类似于不处理异常)。在我看来, calc 的代码很难看。

对于所有库函数具有完全相同签名的这种特殊情况,我们可以使代码更好(我使用来自http://golang.org/doc/articles/wiki/#tmp_269的想法)

func saferun(f func (int) (int, error)) func (int, error) (int, error) {
    return func (in int, err error) (int, error) {
        if err != nil {
            return in, err
        }
        return f(in)
    }
} 

然后我们可以将 calc 重新定义为

func calc(in int) (out int, err error) {
    return saferun(f3)(saferun(f2)(f1(in)))
}

或作为

func calc(in int) (out int, err error) {
    sf2 := saferun(f2)
    sf3 := saferun(f3)
    return sf3(sf2(f1(in)))
}

但是如果没有泛型支持,我不确定如何将这种方法用于任何库函数集。

4

6 回答 6

7

如果您真的希望能够做到这一点,您可以使用 compose 功能。

func compose(fs ...func(Value) (OutVal, error)) func(Value) (OutVal, error) {
  return func(val Value) OutVal, Error {
    sVal := val
    var err error
    for _, f := range fs {
      sval, err = f(val)
      if err != nil {
        // bail here and return the val
        return nil, err
      }
    }
    return sval, nil
  }
}

outVal, err := compose(f1, f2)(inVal)

大多数时候,尽管您可能希望比这更直接,因为其他人在遇到您的代码时可能很难理解它。

于 2012-06-10T18:49:11.910 回答
7

首先,您习惯的 try-catch 样式的扩展版本,显然是从 jimt 的答案和 PeterSO 的答案中借用的。

package main

import "fmt"

// Some dummy library functions with different signatures.
// Per idiomatic Go, they return error values if they have a problem.
func f1(in string) (out int, err error) {
    return len(in), err
}

func f2(in int) (out int, err error) {
    return in + 1, err
}

func f3(in int) (out float64, err error) {
    return float64(in) + .5, err
}

func main() {
    inval := "one"

    // calc3 three is the function you want to call that does a computation
    // involving f1, f2, and f3 and returns any error that crops up.
    outval, err := calc3(inval)

    fmt.Println("inval: ", inval)
    fmt.Println("outval:", outval)
    fmt.Println("err:   ", err)
}

func calc3(in string) (out float64, err error) {
    // Ignore the following big comment and the deferred function for a moment,
    // skip to the comment on the return statement, the last line of calc3...
    defer func() {
        // After viewing what the fXp function do, this function can make
        // sense.  As a deferred function it runs whenever calc3 returns--
        // whether a panic has happened or not.
        //
        // It first calls recover.  If no panic has happened, recover returns
        // nil and calc3 is allowed to return normally.
        //
        // Otherwise it does a type assertion (the value.(type) syntax)
        // to make sure that x is of type error and to get the actual error
        // value.
        //
        // It does a tricky thing then. The deferred function, being a
        // function literal, is a closure.  Specifically, it has access to
        // calc3's return value "err" and can force calc3 to return an error.
        // A line simply saying  "err = xErr" would be enough, but we can
        // do better by annotating the error (something specific from f1,
        // f2, or f3) with the context in which it occurred (calc3).
        // It allows calc3 to return then, with this descriptive error.
        //
        // If x is somehow non-nil and yet not an error value that we are
        // expecting, we re-panic with this value, effectively passing it on
        // to allow a higer level function to catch it.
        if x := recover(); x != nil {
            if xErr, ok := x.(error); ok {
                err = fmt.Errorf("calc3 error: %v", xErr)
                return
            }
            panic(x)
        }
    }()
    // ... this is the way you want to write your code, without "breaking
    // the flow."
    return f3p(f2p(f1p(in))), nil
}

// So, notice that we wrote the computation in calc3 not with the original
// fX functions, but with fXp functions.  These are wrappers that catch
// any error and panic, removing the error from the function signature.
// Yes, you must write a wrapper for each library function you want to call.
// It's pretty easy though:
func f1p(in string) int {
    v, err := f1(in)
    if err != nil {
        panic(err)
    }
    return v
}

func f2p(in int) int {
    v, err := f2(in)
    if err != nil {
        panic(err)
    }
    return v
}

func f3p(in int) float64 {
    v, err := f3(in)
    if err != nil {
        panic(err)
    }
    return v
}
// Now that you've seen the wrappers that panic rather than returning errors,
// go back and look at the big comment in the deferred function in calc3.

因此,您可能会抗议您要求更轻松,而事实并非如此。整体上没有参数,但是如果库函数都返回错误值并且你想在没有错误值的情况下链接函数调用,可用的解决方案是包装库函数,并且包装器非常薄且易于编写。唯一困难的部分是延迟函数,但它是一种您可以学习和重用的模式,而且它只有几行代码。

我不想过多地宣传这个解决方案,因为它不是经常使用的。这是一个有效的模式,并且确实有一些合适的用例。

正如 jimt 所提到的,错误处理是一个大主题。“在 Go 中进行错误处理的好方法是什么?” 对于 SO 来说,这将是一个很好的问题,除了它不符合“整本书”标准的问题。我可以想象一本关于 Go 错误处理主题的整本书。

相反,我将提供我的一般观察,如果你只是开始使用错误值而不是试图让它们消失,一段时间后你就会开始理解这样做的好处。当你第一次在现实世界的程序中编写它时,看起来像我们在这里使用的玩具示例中的 if 语句的详细阶梯可能仍然看起来像 if 语句的详细阶梯。但是,当您实际上需要处理这些错误时,您会回到代码并突然将其视为存根,所有这些都在等待您用真正的错误处理代码充实。您可以看到要做什么,因为导致错误的代码就在那里。您可以防止用户看到晦涩的低级错误消息,而是显示一些有意义的内容。作为程序员,你会被提示做正确的事情,而不是接受默认的事情。

要获得更全面的答案,一个很好的资源是文章错误处理和 Go。如果您搜索Go-Nuts 消息,那里也会对此事进行长时间的讨论。标准库中的函数相互调用相当多,(惊喜)因此标准库的源代码包含许多处理错误的示例。这些都是很好的例子,因为代码是由 Go 作者编写的,他们正在推广这种使用错误值的编程风格。

于 2012-06-11T02:00:57.800 回答
6

错误与异常之间的讨论是一个漫长而乏味的讨论。因此,我不会深入探讨。

您的问题的最简单答案涉及 Go 的内置deferpanicrecover函数,如本博客文章中所述。它们可以提供类似于异常的行为。

package main

import "fmt"

func main() {
    defer func() {
        // This recovers from a panic if one occurred. 
        if x := recover(); x != nil {
            fmt.Printf("%v\n", x)
        }
    }()

    value := f(f(f(1)))
    fmt.Printf("%d\n", value)
}

func f(i int) int {
    value := i*i + 1

    // something goes wrong, panic instead of returning an error.
    panic("ohnoes")

    return value
}
于 2012-06-10T09:24:13.143 回答
0

找到了关于这个主题的 go-nuts 的邮件线程。添加它以供参考。

于 2012-06-13T07:34:56.113 回答
-1

没有一个具体的例子,你正在向风车倾斜。例如,根据您的定义, fn 函数返回一个值和任何错误。fn 函数是不能更改签名的包函数。使用您的示例,

package main

import "fmt"

func f1(in int) (out int, err error) {
    return in + 1, err
}

func f2(in int) (out int, err error) {
    return in + 2, err
}

func f3(in int) (out int, err error) {
    return in + 3, err
}

func main() {
    inval := 0
    outval, err := f3(f2(f1(inval)))
    fmt.Println(inval, outval, err)
}

你将如何让你的示例编译和运行?

于 2012-06-10T13:06:28.170 回答
-1

太糟糕了,这个已经关闭了......这个:

value := f(f(f(1)))

不是链接的例子,而是嵌套的例子。链接应该类似于:

c.funA().funB().funC()

这是一个工作示例

于 2013-06-21T19:53:59.337 回答