英文:
Nesting function calls in GO
问题
让我们假设我们想要实现以下计算:
outval / err = f3(f3(f1(inval))
其中的每个f1
,f2
,f3
都可能在某个时间点失败,并且在计算停止时将err
设置为失败函数返回的错误。(当然,嵌套可以任意长)
在像C++/JAVA/C#这样的语言中,可以通过让f1
,f2
和f3
抛出异常并将计算包含在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)))
}
但是在没有泛型支持的情况下,我不确定如何在任何一组库函数中使用这种方法。
英文:
Let us say we want to implement following computation:
outval / err = f3(f3(f1(inval))
where each of f1
, f2
, f3
can fail with an error at that time we stop the computation and set err
to error returned by the failing function. (Of course, nesting can be arbitrarily long)
In languages like C++/JAVA/C# it can be easily done by having f1
, f2
and f3
throw an exception and enclosing the computation in a try-catch block, while in languages like Haskell we can use monads instead.
Now I am trying to implement it in GO and the only approach I can think of is obvious if-else ladder which is rather verbose. I don't have issue if we can't nest the calls, but in my opinion adding error check after each line in code looks ugly and it breaks the flow. I would like to know if there is any better way of doing it.
Edit: Editing as per the comment by peterSO
Below is the concrete example and straightforward implementation
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)
}
What I am trying to illustrate is, function calc does some computation possibly with the help of library functions that can fail and semantics is if any call fails calc propagates the error to the caller (similar to not handling the exception). In my opinion, code for calc is ugly.
Between for this particular case where all library functions have exactly same signature, we can make the code better (I am using idea from 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)
}
}
Then we can redefine calc as
func calc(in int) (out int, err error) {
return saferun(f3)(saferun(f2)(f1(in)))
}
or as
func calc(in int) (out int, err error) {
sf2 := saferun(f2)
sf3 := saferun(f3)
return sf3(sf2(f1(in)))
}
But without generics support, I am not sure how I can use this approach for any set of library functions.
答案1
得分: 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 {
// 在这里退出并返回val
return nil, err
}
}
return sval, nil
}
}
outVal, err := compose(f1, f2)(inVal)
不过大多数情况下,你可能希望比这更直接,因为当其他人遇到它时,可能很难理解你的代码。
英文:
If you really want to be able to do this you could use a compose function.
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)
Most of the time though you probably want to be more straightforward than this since it may be difficult for others to understand your code when they encounter it.
答案2
得分: 7
首先,这是一个扩展的try-catch风格的代码,借鉴了jimt和PeterSO的答案。
package main
import "fmt"
// 一些带有不同签名的虚拟库函数。
// 根据惯用的Go语言,如果有问题,它们会返回错误值。
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是你想要调用的函数,它执行涉及f1、f2和f3的计算,并返回任何出现的错误。
outval, err := calc3(inval)
fmt.Println("inval: ", inval)
fmt.Println("outval:", outval)
fmt.Println("err: ", err)
}
func calc3(in string) (out float64, err error) {
// 暂时忽略下面的大注释和延迟函数,
// 跳到calc3的最后一行的返回语句的注释...
defer func() {
// 在查看fXp函数的作用后,这个函数就有了意义。
// 作为一个延迟函数,它在calc3返回时运行——无论是否发生了panic。
//
// 它首先调用recover。如果没有发生panic,recover返回nil,calc3就可以正常返回。
//
// 否则,它进行类型断言(value.(type)语法)
// 确保x是error类型并获取实际的错误值。
//
// 然后它做了一个巧妙的事情。延迟函数作为一个函数字面量,是一个闭包。
// 具体来说,它可以访问calc3的返回值“err”,并强制calc3返回一个错误。
// 一个简单的语句“err = xErr”就足够了,但是我们可以更好地注释错误(来自f1、f2或f3中的某个特定错误),
// 并注明它发生的上下文(calc3)。
// 然后,它允许calc3返回,带有这个描述性错误。
//
// 如果x不为nil,但又不是我们期望的错误值,我们会重新panic这个值,
// 有效地将其传递给更高级别的函数来捕获它。
if x := recover(); x != nil {
if xErr, ok := x.(error); ok {
err = fmt.Errorf("calc3 error: %v", xErr)
return
}
panic(x)
}
}()
// ... 这是你想要编写代码的方式,没有“打断流程”。
return f3p(f2p(f1p(in))), nil
}
// 所以,注意我们在calc3中使用的计算不是原始的fX函数,而是fXp函数。这些是捕获任何错误和panic的包装函数,从函数签名中删除了错误。
// 是的,你必须为你想要调用的每个库函数编写一个包装器。
// 不过,这很容易:
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
}
// 现在你已经看到了那些会panic而不是返回错误的包装器,回到calc3中延迟函数的大注释。
所以,你可能会抗议说你要求更简单,但这并不简单。在整个过程中没有争议,但是如果库函数都返回错误值,并且你想要在不返回错误值的情况下链接函数调用,那么可用的解决方案是包装库函数,而这些包装器非常简单易写。唯一的难点是延迟函数,但这是一个你可以学习和重用的模式,只需要几行代码。
我不想过多推广这个解决方案,因为它并不经常使用。然而,它是一种有效的模式,并且在某些适当的情况下有一些用例。
错误处理是一个大的主题,正如jimt所提到的。“在Go中如何进行良好的错误处理?”是一个很好的问题,除了它不符合“整本书”的标准之外。我可以想象一本关于Go中错误处理的书。
相反,我将提供我的一般观察,即如果你只是开始使用错误值而不是试图让它们消失,过一段时间后你会开始理解这样做的优势。在像我们在这里使用的玩具示例中,看起来像是一堆冗长的if语句的代码,在你第一次在真实世界的程序中编写它时可能仍然看起来像是一堆冗长的if语句。然而,当你真正需要处理这些错误时,你回到代码中,突然看到它们只是等待你用真正的错误处理代码填充的存根。你可以看到应该做什么,因为导致错误的代码就在那里。你可以屏蔽用户看到一个晦涩的低级错误消息,而是显示一些有意义的东西。作为程序员,你被提示去做正确的事情,而不是接受默认的事情。
对于更全面的答案,一个很好的起点是文章Error Handling and Go。如果你搜索Go-Nuts messages,那里也有关于这个问题的长时间讨论。标准库中的函数相互调用得相当频繁(惊讶吧),因此标准库的源代码包含了许多处理错误的示例。这些是很好的示例,可以研究,因为代码是由Go的作者编写的,他们正在推广这种使用错误值的编程风格。
英文:
First, an expanded version of the try-catch style that you are used to, borrowing obviously from jimt's answer and PeterSO's answer.
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.
So, you might protest that you asked for easier and this is not. No argument on the whole, but if the library functions all return error values and you want to chain function calls without the error values, the solution available is to wrap the library functions, and the wrappers are very thin and easy to write. The only other hard part is that deferred function, but it's a pattern that you can learn and reuse and it's only a few lines of code.
I don't want to promote this solution too much because it's not one that is used often. It is a valid pattern though, and does have some use cases where it is appropriate.
Error handling is a big subject, as jimt mentioned. "What are good ways to do error handling in Go?" would be a good question for SO except for the problem that it fails the "whole book" critereon. I can imagine a whole book on the subject of error handling in Go.
Instead, I'll offer my general observation that if you just start working with error values instead of trying to make them disappear, after a while you start understanding advantages of doing this. What looks like a verbose ladder of if statements in a toy example like we used here might still look like a verbose ladder of if statements when you first write it in a real world program. When you actually need to handle those errors though, you come back to the code and suddenly see it as stubs all waiting for you to flesh out with real error handling code. You can see just what to do because the code that caused the error is right there. You can sheild a user from seeing an obscure low level error message and instead show something meaningful. You as the programmer are prompted to do the right thing instead of accepting a default thing.
For more comprehensive answers, one good resource to start with is the article Error Handling and Go. If you search through the Go-Nuts messages there are long discussions on the matter there as well. The functions in the standard library call each other quite a bit, (surprise) and so the source code of the standard library contains many examples of handling errors. These are excelent examples to study since the code is written by Go authors who are promoting this programming style of working with error values.
答案3
得分: 6
The discussion between Errors vs Exceptions is a long and tedious one. I shall therefore not go into it.
The simplest answer to your question concerns Go's built-in defer
, panic
, and recover
functions as discussed in this blog post. They can offer behaviour similar to exceptions.
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
}
英文:
The discussion between Errors vs Exceptions is a long and tedious one. I shall therefore not go into it.
The simplest answer to your question concerns Go's built-in defer
, panic
, and recover
functions as discussed in this blog post. They can offer behaviour similar to exceptions.
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
}
答案4
得分: 0
在go-nuts上找到了关于这个主题的邮件线程。添加它作为参考。
英文:
Found mailing <a href="https://groups.google.com/forum/?fromgroups#!searchin/golang-nuts/generics/golang-nuts/W3tXPy-03JA/8H-foRw-QSgJ">thread</a> on go-nuts for this topic. Adding it for reference.
答案5
得分: -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)
}
你要如何使你的例子编译和运行?
英文:
Without a concrete example, you are tilting at windmills. For example, per your definition, fn functions return a value and any error. The fn functions are package functions whose signature cannot be changed. Using your example,
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)
}
How are you going to get your example to compile and run?
答案6
得分: -1
很遗憾,这个已经关闭了... 这个:
value := f(f(f(1)))
不是链式调用的例子,而是嵌套调用。链式调用应该像这样:
c.funA().funB().funC()
这里有一个可运行的示例。
英文:
Too bad this one is closed already... This:
value := f(f(f(1)))
is not an example of chaining but of nesting. Chaining should look something like:
c.funA().funB().funC()
Here is a working example.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论