在GO中嵌套函数调用

时间:2012-06-10 08:42:12

标签: go

让我们说我们想要实现以下计算:

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

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

在像C ++ / JAVA / C#这样的语言中,可以通过让f1f2f3抛出异常并将计算封装在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)))
}

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

6 个答案:

答案 0 :(得分:7)

如果您真的希望能够这样做,可以使用撰写功能。

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)

大多数情况下,您可能希望比此更直接,因为其他人在遇到代码时可​​能很难理解您的代码。

答案 1 :(得分: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来说这将是一个很好的问题,除了它没有通过"整本书" critereon。我可以想象一下关于Go中错误处理主题的整本书。

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

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

答案 2 :(得分:6)

错误与例外之间的讨论是漫长而乏味的。因此,我不会进入它。

对您的问题的最简单答案是关于Go this blog帖子中讨论的内置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
}

答案 3 :(得分:0)

没有一个具体的例子,你正在风车上倾斜。例如,根据您的定义,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)
}

如何让你的例子编译并运行?

答案 4 :(得分:0)

发现了关于此话题的疯狂邮寄thread。添加它以供参考。

答案 5 :(得分:0)

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

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

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

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

这是一个有效的example