Proposal: try - built-in error checking function

Summary



A new try



construct is proposed that is designed specifically to eliminate if



-expressions commonly associated with error handling in Go. This is the only change in language. Authors support the use of defer



and standard library functions to enrich or wrap errors. This small extension is suitable for most scenarios, practically without complicating the language.







The try



construct is easy to explain, easy to implement, this functionality is orthogonal to other language constructs and is fully backward compatible. It is also extensible if we want it in the future.







The rest of this document is organized as follows: after a brief introduction, we give a definition of the built-in function and explain its use in practice. The discussion section reviews alternative suggestions and the current design. At the end, conclusions and an implementation plan with examples and a section of questions and answers will be given.







Introduction



At the last Gophercon conference in Denver, members of the Go team (Russ Cox, Marcel van Lohuizen) presented some new ideas on how to reduce the tediousness of manual error handling in Go ( draft design ). Since then we have received a huge amount of feedback.







As Russ Cox explained in his review of the problem , our goal is to make error handling more lightweight by reducing the amount of code devoted specifically to error checking. We also want to make writing error handling code more convenient, increasing the likelihood that developers will still devote time to correcting error handling. At the same time, we want to leave the error handling code clearly visible in the program code.







The ideas discussed in the draft draft are concentrated around the new unary check



statement, which simplifies the explicit verification of the error value obtained from some expression (usually a function call), as well as the declaration of error handlers ( handle



) and a set of rules connecting these two new language constructs.







Most of the feedback we received focused on the details and complexity of the handle



design, and the idea of โ€‹โ€‹a check



operator turned out to be more attractive. In fact, several members of the community took the idea of โ€‹โ€‹a check



operator and expanded it. Here are a few posts most similar to our offer:









The current proposal, although different in detail, was based on these three and, in general, on the feedback received on the draft design proposed last year.







For completeness, we want to note that even more error handling suggestions can be found on this wiki page . It is also worth noting that Liam Breck came with an extensive set of requirements for the error handling mechanism.







Finally, after the publication of this proposal, we learned that Ryan Hileman implemented try



five years ago using the og rewriter tool and successfully used it in real projects. See ( https://news.ycombinator.com/item?id=20101417 ).







Built-in try function



Sentence



We suggest adding a new function-like language element called try



and called with a signature







 func try(expr) (T1, T2, ... Tn)
      
      





where expr



means an expression of an input parameter (usually a function call) that returns n + 1 values โ€‹โ€‹of types T1, T2, ... Tn



and error



for the last value. If expr



is a single value (n = 0), this value must be of type error



and try



does not return a result. Calling try



with an expression that does not return the last value of type error



results in a compilation error.







The try



construct can only be used in a function that returns at least one value, and whose last return value is of type error



. Calling try



in other contexts leads to a compilation error.







Call try



with function f()



as in the example







 x1, x2, โ€ฆ xn = try(f())
      
      





leads to the following code:







 t1, โ€ฆ tn, te := f() // t1, โ€ฆ tn,  ()   if te != nil { err = te //  te    error return //     } x1, โ€ฆ xn = t1, โ€ฆ tn //     //    
      
      





In other words, if the last error



type returned by expr



is nil



, then try



simply returns the first n values, removing the final nil



.







If the last value returned by expr



is not nil



, then:









If try



used in multiple assignments, as in the example above, and a non-zero error (hereinafter not-nil - approx. Per.) Is detected, the assignment (by user variables) is not executed, and none of the variables on the left side of the assignment does not change. That is, try



behaves like a function call: its results are available only if try



returns control to the caller (as opposed to the case with a return from the enclosing function). As a result, if the variables on the left side of the assignment are return parameters, using try



will result in behavior that is different from the typical code that is encountered now. For example, if a,b, err



are named return parameters of an enclosing function, here is this code:







 a, b, err = f() if err != nil { return }
      
      





will always assign values โ€‹โ€‹to the variables a, b



and err



, regardless of whether the call to f()



returned an error or not. Contrary challenge







 a, b = try(f())
      
      





in case of an error, leave a



and b



unchanged. Despite the fact that this is a subtle nuance, we believe that such cases are quite rare. If unconditional assignment behavior is required, you must continue to use if



expressions.







Using



The definition of try



explicitly tells you how to use it: a lot of if



expressions that check for an error return can be replaced with try



. For example:







 f, err := os.Open(filename) if err != nil { return โ€ฆ, err //       }
      
      





can be simplified to







 f := try(os.Open(filename))
      
      





If the calling function does not return an error, try



cannot be used (see the Discussion section). In this case, the error should in any case be processed locally (since there is no error return), and in this case, if



remains the appropriate mechanism for checking for errors.







Generally speaking, our goal is not to replace all possible error checks with a try



. Code that requires different semantics can and should continue to use if



expressions and explicit variables with error values.







Testing and try



In one of our earlier attempts to write a specification (see the design iteration section below), try



was designed to panic when an error occurs when used inside a function without a return error. This allowed using try



in unit tests based on the testing



package of the standard library.







As one of the options, it is possible to use test functions with signatures in the testing



package







 func TestXxx(*testing.T) error func BenchmarkXxx(*testing.B) error
      
      





in order to allow the use of try



in tests. A test function that returns a nonzero error will implicitly call t.Fatal(err)



or b.Fatal(err)



. This is a small library change that avoids the need for different behaviors (return or panic) for try



, depending on the context.







One of the drawbacks of this approach is that t.Fatal



and b.Fatal



will not be able to return the line number on which the test fell. Another disadvantage is that we have to somehow change the subtests too. The solution to this problem is an open question; we do not propose specific changes to the testing



package in this document.







See also # 21111 , which suggests allowing example functions to return an error.







Error processing



The original draft design was largely about language support for wrapping or augmenting errors. The draft proposed a new keyword handle



and a new way to declare error handlers . This new language construct attracted problems like flies due to non-trivial semantics, especially when considering its effect on the flow of execution. In particular, the handle



functionality miserably crossed with the defer



function, which made the new language feature non-orthogonal to everything else.







This proposal reduces the original draft design to its essence. If enrichment or error wrapping is required, there are two approaches: attach to if err != nil { return err}



, or "declare" an error handler inside the defer



expression:







 defer func() { if err != nil { //      -   err = โ€ฆ // /  } }()
      
      





In this example, err



is the name of the return parameter of type error



enclosing function.







In practice, we imagine such helper functions as







 func HandleErrorf(err *error, format string, args ...interface{}) { if *err != nil { *err = fmt.Errorf(format + ": %v", append(args, *err)...) } }
      
      





or something similar. The fmt



package can become a natural place for such helpers (it already provides fmt.Errorf



). Using helpers, the definition of an error handler will in many cases be reduced to a single line. For example, to enrich the error from the copy function, you can write







 defer fmt.HandleErrorf(&err, "copy %s %s", src, dst)
      
      





if fmt.HandleErrorf



implicitly adds error information. Such a construction is fairly easy to read and has the advantage that it can be implemented without adding new elements of the language syntax.







The main drawback of this approach is that the returned error parameter must be named, which potentially leads to a less accurate API (see the FAQ on this topic). We believe that we will get used to it when the appropriate style of writing code is established.







Efficiency defer



An important consideration when using defer



as an error handler is efficiency. The defer



expression is considered slow . We do not want to choose between efficient code and good error handling. Regardless of this proposal, the Go runtime and compiler teams discussed alternative implementation methods and we believe that we can make typical ways of using defer to handle errors comparable in efficiency to the existing โ€œmanualโ€ code. We hope to add a faster implementation of defer



in Go 1.14 (see also ticket CL 171158 , which is the first step in this direction).







Special cases go try(f), defer try(f)





The try



construct looks like a function, and because of this, it is expected that it can be used anywhere where a function call is acceptable. However, if the try



call is used in the go



statement, things get complicated:







 go try(f())
      
      





Here f()



is executed when the go expression is executed in the current goroutine, the results of calling f



are passed as arguments to try



, which starts in the new goroutine. If f



returns a nonzero error, try



is expected to return from the enclosing function; however, there is no function (and there is no return parameter of type error



), because the code is executed in a separate goroutine. Because of this, we propose disabling try



in a go



expression.







Situation with







 defer try(f())
      
      





looks similar, but here the semantics of defer



mean that the execution of try



will be delayed until it returns from the enclosing function. As before, f()



evaluated when defer



, and its results are passed to the deferred try



.







try



checks the error f()



returned only at the very last moment before returning from the enclosing function. Without changing try



behavior, such an error can overwrite another error value that the enclosing function is trying to return. This is confusing at best, and at worst provokes errors. Because of this, we propose that you prohibit calling try



in the defer



statement defer



well. We can always reconsider this decision if there is a reasonable application of such semantics.







Finally, like the rest of the built-in constructs, try



can only be used as a call; it cannot be used as a value function or in a variable assignment expression as in f := try



(just as f := print



and f := new



are forbidden).







Discussion



Design Iterations



The following is a brief discussion of earlier designs that led to the current minimal proposal. We hope that this will shed light on the selected design solutions.







Our first iteration of this sentence was inspired by two ideas from the article โ€œKey Parts of Error Handling,โ€ namely, using the built-in function instead of the operator and the usual Go function to handle errors instead of the new language construct. Unlike that publication, our error handler had a fixed signature func(error) error



to simplify matters. An error handler would be called by the try



function if there was an error before try



would exit the enclosing function. Here is an example:







 handler := func(err error) error { return fmt.Errorf("foo failed: %v", err) //   } f := try(os.Open(filename), handler) //     
      
      





While this approach allowed the definition of effective user-defined error handlers, it also raised many questions that obviously did not have the correct answers: What should happen if nil is passed to the handler? Is it worth try



panic or regard this as a lack of a handler? What if the handler is called with a non-zero error and then returns a null result? Does this mean that the error is "canceled"? Or should an enclosing function return an empty error? There were also doubts that the optional transfer of an error handler would encourage developers to ignore errors instead of correcting them. It would also be easy to do the correct error handling everywhere, but skip one use of try



. Etc.







In the next iteration, the ability to pass a custom error handler was removed in favor of using defer



to wrap errors. This seemed like a better approach because it made error handlers much more noticeable in the source code. This step also eliminated all issues regarding the optional transfer of handler functions, but demanded that the returned parameters with the error



type be named if access was required (we decided that this was normal). Moreover, in an attempt to make try



useful not only within functions that return errors, it was necessary to make the behavior of try



context-sensitive: if try



used at the package level, or if it was called inside a function that does not return an error, try



automatically panicked when an error was detected. (And as a side effect, because of this property, the language construct was called must



instead of try



in that sentence.) The context-sensitive behavior of try



(or must



) seemed natural and also quite useful: it would eliminate many user-defined functions used in expressions initializing package variables. It also opened up the possibility of using try



in unit tests with the testing



package.







However, the context-sensitive behavior of try



was fraught with errors: for example, the behavior of a function using try



could quietly change (panic or not) when adding or removing a return error to the signature of the function. This seemed too dangerous a property. The obvious solution was to split the try



functionality into two separate must



and try



functions, (very similar to how it was suggested in # 31442 ). However, this would require two built-in functions, while only try



directly related to better error handling support.







Therefore, in the current iteration, instead of including the second built-in function, we decided to remove the dual semantics of try



and, therefore, allow its use only in functions that return an error.







Features of the proposed design



This suggestion is quite short and may seem like a step back compared to last year's draft. We believe that the selected solutions are justified:









 info := try(try(os.Open(file)).Stat()) //   try info := try (try os.Open(file)).Stat() //  try   info := try (try (os.Open(file)).Stat()) //  try  
      
      





try



, : try



, .. try



(receiver) .Stat



( os.Open



).







try



, : os.Open(file)



.. try



( , try



os



, , try



try



).







, .. .









conclusions



. , . defer



, .







Go - , . , Go append



. append



, . , . , try



.







, , Go : panic



recover



. error



try



.







, try



, , โ€” โ€” , . Go:









, , . if



-.







Implementation



:









- , . , . .







Robert Griesemer go/types



, () cmd/compile



. , Go 1.14, 1 2019.







, Ian Lance Taylor gccgo



, .







"Go 2, !" , .







1 , , , Go 1.14 .







Examples







CopyFile



:







 func CopyFile(src, dst string) (err error) { defer func() { if err != nil { err = fmt.Errorf("copy %s %s: %v", src, dst, err) } }() r := try(os.Open(src)) defer r.Close() w := try(os.Create(dst)) defer func() { w.Close() if err != nil { os.Remove(dst) //    โ€œtryโ€    } }() try(io.Copy(w, r)) try(w.Close()) return nil }
      
      





, " ", defer



:







 defer fmt.HandleErrorf(&err, "copy %s %s", src, dst)
      
      





( defer



-), defer



, .







printSum









 func printSum(a, b string) error { x := try(strconv.Atoi(a)) y := try(strconv.Atoi(b)) fmt.Println("result:", x + y) return nil }
      
      





:







 func printSum(a, b string) error { fmt.Println( "result:", try(strconv.Atoi(a)) + try(strconv.Atoi(b)), ) return nil }
      
      





main



:







 func localMain() error { hex := try(ioutil.ReadAll(os.Stdin)) data := try(parseHexdump(string(hex))) try(os.Stdout.Write(data)) return nil } func main() { if err := localMain(); err != nil { log.Fatal(err) } }
      
      





- try



, :







 n, err := src.Read(buf) if err == io.EOF { break } try(err)
      
      





Questions and answers



, .







: ?







: check



handle



, . , handle



defer



, handle



.







: try ?







: try



Go . - , . , . , " ". try



, .. .







: try



try?







: , check



, must



do



. try



, . try



check



(, ), - . . must



; try



โ€” . , Rust Swift try



( ). .







: ?



Rust?







: Go ; , Go ( ; - ). , ?



, . , , , (package, interface, if, append, recover, ...), , (struct, var, func, int, len, image, ..). Rust ?



try



โ€” Go, , ( ) . , ?



. , , (, ..) . . , .







: ( error) , defer , go doc. ?







: go doc



, - ( _



) , . , func f() (_ A, _ B, err error)



go doc



func f() (A, B, error)



. , , , . , , . , , , -, (deferred) . Jonathan Geddes try()



.







: defer ?







: defer



. , , defer "" . . CL 171758 , defer 30%.







: ?







: , . , ( , ), . defer



, . defer



- https://golang.org/issue/29934 ( Go 2), .







: , try, error. , ?







: error



( ) , , nil



. try



. ( , . - ).







: Go , try ?







: try



, try



. super return



-, try



Go



. try



. .







: try , . What should I do?







: try



; , . try



( ), . , if



.







: , . try, defer . What should I do?







: , . .







: try



( catch



)?







: try



โ€” ("") , , ( ) . try



; . . "" . , . , try



โ€” . , , throw



try-catch



Go. , (, ), ( ) , . "" try-catch



, . , , . Go . panic



, .








All Articles