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 , .



Source: https://habr.com/ru/post/472758/


All Articles