go: proposal: Go 2: error map or log for error handling
New better proposal #50280 is ready
I propose the interrelated changes to the Go language:
-
Each declared or imported function (including method) must automatically and implicitly declare and initialize an error map, as does the operator
@function_name := map[string]bool{}. Alternatively theerror_orerror.prefix can be used before the function name instead of the@prefix, or the names of the function and the error map can be the same. -
The error map must be visible both inside the body of the function and in the scope (that is, in visibility area outside the function body) of the declared or imported function. The scope (that is, visibility area) of the error map is the same as scope (that is, visibility area) of the parameters of function.
But Apache Kafka attracts by the idea of a more flexible, dynamical and centralized management of the areas of visibility (topics) of messages (about errors). See description of the error log
- The content of the error map should be updated and visible instantly, well before the called function returns, so that the calling function can decide in advance whether the called function needs to be interrupted and how to handle errors.
Cases of assigning functions to variables and transferring functions to other functions etc require special research. Instead of a map, we can use another container for error messages, if it turns out to be more convenient: set, slice, stack, etc.
Description of use:
Programmers should use error types as keys in the error map.
Each function can throw several errors of different types and severity, which can then be handled in different ways (with or without exiting the function where the error occured, with or without return of parameters). If an error occurs, then the value of its type in the error map must be true. Therefore, the operator @function_name["error_type"] = true is required in the function body, but it’s preferable that warning("error_type") and escape("error_type") (with escape from erroneous function) play its role.
If the corresponding function is used several times in the same scope (that is, in visibility area), then all different types of errors will appear in the error map each time when function is used.
If, when checking the expression @function_name["error_type"] in an if or switch statement, an error type was used that is not in the error map, then value false will be returned. It is convenient and obvious. A desision table can be used together with an error map for error handling and informing in difficult cases.
Benefits of the proposal:
- Very concise and obvious notation even for a novice Go programmer
- Change is backward compatible with Go 1 (replaces, but can be used in parallel with existing error handling methods). Therefore it can be done before Go 2
- Simple implementation
- Doesn’t affect compilation time and performance
- Explicit and composite error naming in the calling function
- Аrbitrary and easy error naming in the function in which the error occurred (including dynamic name generation)
- Ability to send errors along the chain of function calls
- The compiler can catch unhandled explicitly specified errors
- Each function can throw several errors of different types and severity, which can then be handled in different ways (including with or without instantaneous exiting the function where the error occured, with or without returning parameters)
- If the corresponding function is used multiple times in the same scope (that is, in visibility area), then all different types of errors will be handled correctly
- A desision table can be used together with an error map for error handling and informing in difficult cases
Examples of code before and after proposal
// Before proposal
package main
import (
"errors"
"fmt"
"strings"
)
func capitalize(name string) (string, error) {
if name == "" {
return "", errors.New("no name provided")
}
return strings.ToTitle(name), nil
}
func main() {
_, err := capitalize("")
if err != nil {
fmt.Println("Could not capitalize:", err)
return
}
fmt.Println("Success!")
}
// =================================================================
// After proposal
package main
import ( // "errors" is not imported
"fmt"
"strings"
)
func capitalize(name string) string { // also declares and initializes an error map @capitalize, as does the operator @capitalize := map[string]bool{}
if name == "" {
warning("no name provided") // new keyword. Without escape from erroneous function. Equivalent to @capitalize["no name provided"] = true
//escape("no name provided") // new keyword. With escape from erroneous function
return ""
}
return strings.ToTitle(name)
}
func main() {
if @capitalize["no name provided"] { // explicit error naming in the calling function after proposal
fmt.Println("Could not capitalize: no name provided")
return
}
fmt.Println("Success!")
}
About this issue
- Original URL
- State: closed
- Created 3 years ago
- Reactions: 37
- Comments: 122 (29 by maintainers)
If your goal is to persuade people, then I suggest that you use a different conversational style. Describing my argument as “biased and unreasonable” does not encourage me to continue communicating. If you want the language to adopt this proposal, you need to convince me and other members of the Go team that this language change is a good idea. We don’t need to convince you.
That said, I want to be clear that I don’t think there is any chance that we will adopt this proposal as is for Go. It’s an interesting idea, but as several other people have pointed out on this issue, it’s not a good fit for the language as it exists today.
In the code under discussion, we are calling a method. It’s not “the same function.” A method can have many different implementations. In a large Go program there can be dozens of different
Readmethods. There is no way for the compiler or, in many cases, even the programmer to know whichReadmethod is being called.@sergeyprokhorenko please build the system you’re proposing so that we can experimentally use and evaluate it.
It is obvious that such people have no motivation to improve Go. They don’t see any problem, and they don’t want any change. They are used to the flaws of the language. They do their best to ensure that no improvement comes about. They put forward absurd demands, put dislikes, distort the meaning of the words of the authors of the proposals, etc. I really hope that the developers of the Go language will not listen to them, and in the end the problem of error handling will be solved as successfully as the problem of lack of generics has already been solved. This is my only desire. I am not selling my proposal here. I am sure it deserves attention, as it is not another copy of other proposals with slight variations. But I’ll be glad if any other proposal solves the error handling problem really well.
@dominikh You love freebies too much
@sergeyprokhorenko Replying to https://github.com/golang/go/issues/49535#issuecomment-986277698 : in this code the approach you are proposing does not seem any better than what the language provides today. The code in your proposal seems longer, harder to write, and it is not any safer. There is no way even in principle for the programmer to know the possible set of error conditions.
My little
Capitalizeexample up there prompted me to consider one other question for this setup: would these error-maps be transitive? i.e.EDIT: or does A simply swallow B’s error-map and main doesn’t receive any errors at all? That’s also a possible interaction here! The necessary implication of making things “implicit” or “automatic” or “decided by the compiler” is that you need to be very explicit with the specification so that users of the language can always rely on the choices and semantics of those features. As Nystrom says in an aside in Crafting Interpreters: “[a language specification] must be so explicit that even a Martian or an outright malicious programmer would be forced to implement the correct semantics provided they followed the letter of the spec.”
I think any way you slice it, this proposal turns up all sorts of very unpleasant edge-cases and curious interactions with existing functionality in go.
People accustomed to flaws are more likely to accept dirty hack than fundamental changes
This is perfectly fine code today:
In order to emulate this in your proposal, I’d have to… what, something like this?
I don’t think there’s much more value to be had here from me asking further questions; you seem set on your design. I wish you luck.
@sergeyprokhorenko I am not proposing that Go should adopt something similar to the Common Lisp condition system. Go’s existing error handling system works fine, for me, when I am writing Go code. It has worked fine, for me, writing Go code, for about 11 years. I am saying that if you want to look at a more ambitious error-handling framework, you could do worse than looking at it.
I am trying both understand your proposal, its implications for code, and critiquing it from the basis of my understanding. It is not productive to attack me for not having made an error-handling proposal. I am unlikely to do so, since I am perfectly happy with the way Go handles errors at the moment.
With Go having functions as a first class data type, any proposal for handling errors need to cope with the fact that function values will be passed around as function arguments, stored in variables (and struct members) and possibly passed over channels. The existing error type does this perfectly well.
Also, “imperative” does not necessarily mean “has spooky action at a distance” (I would classify the proposed error maps as being at least “action at a distance” and with all the changes needed to Go to support them safely with goroutines, it probably also falls into “spooky”).
@deltamualpha Do not invent flaws that don’t exist. It’s not beautiful. Just remember about identifier scopes (areas of visibility) in Go. This is what any programmer must know. The compiler obey the scopes regardless my proposal. The function
B()is in the scope of themain()function. Therefore, according to the rule of this proposal, the@Berror map is also visible in themain()function, and you could use it. You did not handle the warning from the functionB()inside the functionA(), therefore the functionA()will not pass those warning to the main() function. It is obvious that a programmer cannot ignore possible errors with impunity. Error transmission along the call chain does not happen automatically. It must be programmed by the programmer. To do this, Go authors can come up with a simplified syntax.No, you can distinguish “errors we have set” and “errors set by a recursive call” easily. Just insert count of calls into string key of error map in the IRI format:
"error:function_name?type='error_type'&count=87657"DeedleFake, read my answers more carefully. In them, every word is decisive (string keys, not just booleans;
explicitly specified errors, that is, constants, not variables).
That sure looks like global state to me as currently proposed.
I know. What I mean is that the only information that can be conveyed via this system about a given error is whether or not it happened. The only possible information is just booleans. That’s not very useful.
No, it can’t. What happens if I pass a string variable to
warning()instead of a constant? How is the compiler going to have any clue what its value is?So, at this point, we can’t really just use
@functionname, because that wouldn’t refer to the specific instance of the function we just called, but to some kind of global table of these functions.The claim that
stop“means the same as return” doesn’t really make sense.This function alternates between sleeping for a second and printing a million spaces. What happens when a caller stops it? Does that cause a lower-level call to Sleep or Printf or x.Lock() to be interrupted? What about the defer? In normal Go, this is completely reliable – the Lock() cannot be interrupted by any means, so we are guaranteed that the function it’s in doesn’t return until after the lock is successfully taken. In this new variety of Go, what happens?
If we are forcing the Lock() to return early, we now have a defer to unwind. We can’t just skip defers, because if the lock had completed we’d need it. We can’t actually unlock, because the lock failed.
What about the Printf? Are we forcing it to abort early if we come in some part of the way through its execution?
This is absolutely, completely, incompatible with writing robust code in Go. The lack of a way to “stop” a goroutine is absolutely a challenge for programmers, but it provides us with guarantees that make it possible to reason about the behavior of code. If you add a way to “stop” a goroutine, any function that can ever be called, even indirectly, by something that uses that facility now has to be completely rearchitected from the ground up to handle “but what if someone tries to stop this goroutine”, and I don’t think that’s possible to do well.
@ianlancetaylor It’s my answer to your https://github.com/golang/go/issues/49535#issuecomment-974994995
In your code
n, err: = r.Read (s), the compiler somehow figures out whichReadmethod to take theerrerror from. Likewise, the compiler must figure out which of the@Readerror maps to handle.Here is my answer to your question on how to handle all other errors:
Half the issue of the “we just won’t support passing function pointers” stance is that it ignores a lot of the current ecosystem. This would include the standard HTTP server. This could cause a fairly large split within the ecosystem, as some are supported by the new error handling and others aren’t.
The current solution is painful, but it still works for everyone. The new solution needs to work for everyone, not just people who follow a certain paradigm.
We also haven’t seen how interfaces would be handled under this system, like in https://github.com/golang/go/issues/49535#issuecomment-974994995.
Please keep on thinking about this. Your passion for this is admirable, however I don’t believe this would be fit-for-purpose in the context of the larger Go ecosystem in its current state.
Maps are typically emulated as
map[T]struct{}rather thanmap[T]bool. Part of it is thatstruct{}uses less memory, but another part is for this very reason oflen(m)correctly returning the number of items which are in the set.@vatine Your inference is wrong. Each concurrently executing copy of the same function has its own error map instance.
No. The programmer (calling function) can examine ALL strings in the error map, for example, using regular expressions or special parser. The strings may contain not just error codes, but rather detailed messages about errors and warnings. In particular, the error message can contain the entire chain of calls, as well as the parameters data types that the method worked with. I have already suggested several formats for error messages (IRI, serialized struct, JSON), where you can pack extremely detailed information about the error or warning and the place, time and circumstances of its occurrence.
By the way, these error and warning messages become available in the calling function long before the main result from the called function returns. And the error map can be instantly available directly where errors need to be handled and in the log. Therefore, the called function can be interrupted when necessary, and emergency measures can be taken. For example, Arian wouldn’t explode.
Yes, I agree. But that is not the problem that concerns me. What concerns me is that the programmer can’t know which strings to look up in the returned error map, because it could be any string used by any
Readmethod, even ones that haven’t been written yet. And if the programmer can’t know which string to look up, then as far as I can tell error maps are no better than what we have today.@sergeyprokhorenko It is your proposal.
I see your proposed mechanism as having all the drawbacks of exceptions, with none of the advantages. It is more cumbersome to use than error return values, to get support for recursive calls, I would need to amend all recursive functions to take an extra “call depth” parameter, so I can distinguish “this is an error from the current instance” from “this is an error from another instance”. The research for how to handle anonymous functions, and functions passed as parameters must definitely be a part of a complete proposal.
The error maps would either (by necessity) need to be global, or be goroutine-global (a storage class type that I don’t think exists in Go at the moment).
From a programmer convenience POV, I think separating error values from other return values is useful. But Go allows be to return both a “useful” value and a return value at the same time, so I have a clear separation between “there was an error” and “the useful return value(s)” (unlike C’s frequent insistence that “-1 means error” (except when it’s another magic guard value)).
If you are looking for inspiration for more ambitious error-handling systems, I am quite fond of Common Lisp’s condition system, which not only allows you to signa an error and handle it, but also allows you to signal an error, remedy it, then continue from where you were (or abort, depending on what makes sense in context).
@DeedleFake, Brainfuck is clearer than your recursion. I will not solve useless charades.
It’s not a question of willingness to accept the changes, so much as there being a lot of things in these changes that are obvious to you but not directly articulated, in ways that make it hard for anyone else to understand what you’re proposing.
It appears that, at a high level, you’re looking to have each function document all the errors it can throw, and allow checking them, but you’re not providing a mechanism I can see for a function to do cleanup if it needs to do cleanup after an error occurs, or for things like “retry once on error”.
So, let me see if I’m understanding this correctly: The proposal is to automatically initialize a global map for every function, and then have a new predefined function that would set string keyed booleans in that map to true in order to indicate that an error happened?
If that’s correct, that seems like a massive straight downgrade to the existing system. It communicates data from function calls via global state in a similar vein to C’s infamous
errnomeaning that it’s effectively useless for recursive functions, or even just regular function calls in any situation involving concurrency, it only allows communication of a single boolean via that state, and it loses a lot of compile-time safety due to the usage of string keyed data everywhere.I am confused as to the intended utility of this proposal.
@vatine Functional programming practitioners should invent features for functional programming. They can do it better.
@sergeyprokhorenko Passing a function as a parameter, or storing it in a data structure is a perfectly normal thing to do, outside “functional programming”. It is something that is already done in the Go standard library. Heck, it’s even done in C (at least in some cases).
Responding to valid critique of your proposal with an equivalent of “that’s functional programming, it does not need to be supported” is not a very productive use of anyone’s time.
An improvement needs to be just that, an improvement. Something as central as “error handling” needs to be sufficiently fleshed out that it is possible to fully reason about the proposal and its implications. It is also pretty important to have one, and exactly one, modality for passing errors around, so any proposal that aims to replace “return error values” with something else also needs to reason about how we can, without semantic changes, transform code using the existing paradigm into code using the proposed new paradigm.
The proposal needs to be worded sufficiently precisely that it is indeed possible to reason about what the proposal means. It (the proposal) then needs to be discussed, to find possible corner cases. After those have been found, a decision needs to be made, either how the proposal can be amended to be well-specified for those corner cases, or (less ideally) abandoned. Part of this is dealing with recursive function calls, function values stored in variables, passed over channels and/or passed as function parameters (or return values).
One of the major reasons you are getting push-back on this proposal is that when corner cases or ambiguities are pointed out, you react with hostility and disparaging of the people highlighting ambiguities or seeming contradictions in what has been presented so far. Part of this is most probably that the non-obvioous and/or ambiguous bits of the proposal is necessarily to what focus is drawn, as that’s the parts that need to be more tightly specified.
It has been given attention. I have not seen any proposal similar to it, so I can vouch for it being distinctive.
The term ‘scope of a function’ is slightly ambiguous. It usually refers to the inside of the function. The scope of its body. I think that @vatine assumed that you meant the inside of the function because if you meant the scope in which the function is visible then you didn’t actually answer @deanveloper’s question. If the error map is scoped to the same scope as the function definition, how does it work with concurrency? For example,
If the first call to
Example()is scheduled first, then both of these will printv < 0. If not, only the first will. This is inherently racy.It is also possible that you meant that the map is scoped to the same scope that a specific call happened in. If so, that fixes the concurrency issues, somewhat, but it makes it impossible to pass data back up from a recursive call without going way out of your way to do so, and doing it from a doubly-recursive call where two functions call each other back and forth recursively would be nightmarish.
This is a complete misunderstanding of what that means. When they say errors are values, they mean that errors should be reasoned about like any other piece of data. It’s a contrast to exception-based systems where it is often considered incorrect to pass any values of an
Exception-based type around in a normal way. What you are proposing is literally the exact opposite of the intent of statement that errors are values.@sergeyprokhorenko:
This would mean that the error map is not visible outside the function. Yet, it needs to be visible outside the function in order to communicate errors to callers. If it is visible outside the function, and there is exactly one per function, we have one of “goroutine-global storage” (that is, accessible to any dynamic extent within a specific goroutine) or “program-global storage”. This sounds like an excellent source of synchronisation problems.
I am not, per se, opposed to another way of handling errors, I just fail to see how this proposal is:
We would also lose the ability to pass errors embedded in structs, for possible passing over a channel. Perhaps not always useful, but quite useful for the occasional “we use channels for program-internal RPCs”.
The compiler will not be able to catch a mistake in the use of
@capitalizeif the call tocapitalizeis made through an interface. When calling through an interface the compiler does not know the actual method being called, and therefore does not know the set of errors that it might set. Calling through interfaces is very common in Go due to wide use of interfaces likeio.Reader.With respect, the term “warning” means many different things in different uses in programming. It is not always used in any one way.
With respect, this statement is not correct. In the link that you cite, this is the code:
In this example, the function
maincallscapitalize, passing the string"sammy".Above, you rewrote this code using this proposal as
This function no longer calls
capitalize.So I am going to repeat my question:
Is there meant to be a call to
capitalize? All I see is a reference to@capitalize. Does that somehow call the function? What argument does it pass?No, you don’t understand this proposal correctly, because:
The error map must be visible both inside the body of the function and in the scope of the declared or imported function). There is no global statetruelater. The error map is empty initially. The new predefined functions will add errors into the error mapThe compiler can catch unhandled explicitly specified errorsIn this code
is there meant to be a call to
capitalize? All I see is a reference to@capitalize. Does that somehow call the function? What argument does it pass?It seems that both the caller
mainand the functioncapitalizehave to know about the string"no name provided". That seems harder to use, not easier.The first benefit you list is “Very concise and obvious notation even for a novice Go programmer” but at least for me the notation is neither concise nor obvious. I’m not saying that the current language is perfect, but I don’t understand why this is better. What does
warningmean? What doesescapemean? What does@capitalizemean? None of this is obvious.This proposal will be replaced with new proposal: error handling based on the pointers to instance of function/method that caused the error
Thanks to all the participants in the discussion. The discussion was very fruitful and helped me find this excellent promising idea for a new proposal.
So how do we know which call of a function
@functionnamerefers to? You say that “each function instance” has a unique error map, but how am I specifying which function instance I’m checking the error map of?Is
@foohere referring to the first or second call? Is the first call still available, or is it definitely the second call? If I call something else that calls foo(), does that also overwrite@foo, or is the idea that the name@foorefers to a same-scope call only?But before you answer any of this: Your design is heavily obscured by all the one-off questions and answers. You would communicate it more clearly if you provided a top-level example of a significant piece of code which used this feature meaningfully, with comments explaining what it does and why it does it that way.
The error log with predeclared identifier ErrorLog can be similar to a table in database, containing the following fields:
The error types dictionary with a predeclared identifier ErrorType may contain the following fields:
Filtering and searching for errors in the error log using the IF statement can be done similarly to the WHERE … AND … AND … clause in SQL by several fields. You can refer to the field value as ErrorLog.field_name, as in SQL
You can check for the occurrence of an error by checking for the presence of a corresponding error message in the error log.
In order to create an error, insert a message into the error log using the simplified syntax: warning(“error_type”) or escape(“error_type”) (with escape from erroneous function)
@ianlancetaylor The interruption (from outside) of the function or method that raised the warning could be expressed in the language like this:
stop(pointer_to_the_function_or_method_that_raised_the_warning)or for interruption of several functions/methods:stop(pointer, pointer, pointer)This is pretty close to the Go language as it exists today, since this avoids the visibility area question entirely.The pointer to the function or method that raised the warning could be extracted from the error message contained in the error map or error log.
@sergeyprokhorenko For the error result to be visible to in some function other than the “currently executing function”, it must by necessity be visible in a different goroutine. At that point, the error map MUST be visible to all goroutines (since Go does not really have any notion of “parent goroutine”, they are essentially all on the same level).
That would then mean that if you have a function F, with an error map @F, and function F being called in two goroutines (not at all unusual for any utility function), there either is NO visibility of the error map @F outside the goroutine where F is executing (we are not able to see the error before F returns), or it is visible in all goroutines.
If you propose further divergence from how Go works today, I urge you to take as long as you need to work out all the language changes you would need, then come back with a more fully fleshed out design proposal.
@ianlancetaylor
I do not think so. This is a biased and unreasonable conclusion.
How could you get several different errors and warnings from the same function and trace them without my proposal?
Okay, not the compiler but the runtime must do this.
@neetle I’m new to Go and I’m not an expert in functional programming at all, so I can hardly come up with a complete solution. But I understand in which direction we need to move. This proposal borrows and develops previous technologies:
The error map should be an equivalent replacement for the last parameter of the function (with an error), while retaining all the available manipulation capabilities of the last parameter of the function. For example, when you pass a pointer to a function, you also implicitly pass a pointer to the last parameter of the function. That is, you will have to implicitly pass a pointer to the error map along with the function pointer. If you pass a pointer to a group of similar functions for different data types, then you must also pass pointers to the error maps of these functions. But the syntax should allow us to refer to the error map of exactly the function that is actually applied to the actual data type.
I’m going to reply to #49535 (comment) but haven’t had time to tackle it yet.
https://github.com/golang/go/issues/49535#issuecomment-978099169
@DeedleFake Yes, when I see the term “scope of a function”, I assume it means the lexical scope of the function body. For the scope that the function itself is visible in, I would use the term “the scope in which the function is visible”.
Writing this example also highlights another possible issue with this proposal. How do anonymous function fit into this proposal?
I think the widespread confusion of people trying to review this proposal might be saying something 😅
How would this operate with multiple goroutines?
Also, a fundamental problem of this proposal seems to be that error handling is now opt-in instead of opt-out. Currently, we need to explicitly ignore errors by doing
val, _ := dangerousFunction(). However, under this proposal, it may be easy to simply not checkif @dangerousFunction[...], not to mention that the string that we need to check in@dangerousFunctionneeds to be documented in order for us to check the error.How would we simply check “did some error happen” instead of “did a specific error happen”?(just noticed the comments that mentionlen(@dangerousFunction)I think
if len(@dangerousFunction) > 0is definitely a step back fromif err != nilIn this case the value of
@capitalize["I accidentally wrote the wrong string here"]isfalse. Therefore the functionfmt.Println("Capitalized name:", name)would printCapitalized name:SAMMY, as if you weren’t wrong.If instead of
name: = capitalize ("sammy")there werename: = capitalize (""), thenCapitalized name:would be printed instead ofCould not capitalize: no name provided.Erroneous keys do not allow correct processing of data errors, but correct data will be processed correctly. Erroneous keys simply turn off data error handling. In this sense, my proposal is safe.
I believe the compiler must try to catch errors like this. This is possible if string keys are constants both in called function and in caller function.
@sergeyprokhorenko
What if we have code that looks approximately like:
This would result in the map having a non-zero length, but no true errors. Thus,
len(@function)is a sometimes-working proxy for checking all the keys. Unless we also want to introduce a boolean map, where we can never, ever assign “false”. Which feels like a language change that is somewhat peculiar.This means no longer being able to access the error status via a simple map lookup, as we no longer have the key. Instead, we’d have to loop over all the keys in the map, then check for (best case) a known error string prefix. This pretty much blows away any convenience wins your proposal has
As far as I understand this mechanism, this basically means we cannot have a situation where the same function has more than one call in the same call chain, and still be able to handle errors, as (at that point) we cannot distinguish between “errors we have set” and “errors set by a recursive call”. WIth the existing returned error values, this is (essentially) trivial.
DeedleFake,
String keys carry data, since not only values can be retrieved from a map, but also string keys (using the range operator). And in any case, you can check for known errors.
String keys can be both variables and constants. But the compiler can only check constants.
I’m sorry, I don’t understand the benefit of this. What do we gain?
@seebs The purpose of error handling is fault tolerance and fault recovery. And one way to ensure this is switch-off of a faulty equipment.
But you gave me the idea that the runtime could free all the resources occupied by the stopped goroutine, if this was not done with
defer.Yes, but that’s the problem – in Go, the defer is safe to execute because you can’t reach it before the lock completes. In your new language, the defer can lead to a double-unlock, because you could have interrupted the lock operation.
A major component of what makes Go an effective language that we get good developer productivity in is that there isn’t anything that lets you externally stop a goroutine, so the goroutine doesn’t need to be written with careful attention to detail with regards to what happens if your code is interrupted.
Consider two possible orderings:
Neither of these is correct in the presence of
stop(). If you use the former, then a stop() which hits between the Lock() and the defer of the Unlock() will exit before the defer is registered, resulting in the cleanup not happening. So you have to use the latter. But if you use the latter, a stop() which hits between the defer of the Unlock and the successful completion of the Lock() will exit after the defer is registered, but before the lock is taken, resulting in the cleanup happening when the lock was not actually taken in the first place.Again: Make a complete example and show how you think it would work, but right now, I think the reason you haven’t done this is that you haven’t got a complete working model, and this is why your answers to how to do specific things tend to be somewhat mutually-exclusive with each other.
The same function can be running many times. How do I distinguish which instance of it I want to stop? Also, who is checking the error map while the function is running? Obviously not its caller, because the caller transferred control to the function when calling it, so it has to be something else. But I think you’re implying that the error map will somehow have distinct pointers. But now we’re to the next question; how do I distinguish which instance of the error map I want?
This doesn’t feel like it fits at all with the way function calls in Go work. This is a novel language design; it’s not like any existing language that I’ve ever encountered, and it implies things about concurrency models and data structures which are wildly different from existing designs. I think it would be more effective to build a working implementation of a thing that does this, and then point people to that existing, usable, implementation and say “it would be nice to have error handling that works like this”, where people can see complete working programs that use the feature.
so…
Pointers to functions like this are pretty rare in go today; I had to look up the syntax.
This implies that any function can be “stopped” (what does that even mean?) at any time, from any other goroutine that has a pointer to it. It’s like exceptions, only… backward?
@sergeyprokhorenko
Correct. The error map is “the divergence from how Go works today”. All implied extra things needed (cross-go-routine function cancellation, selective cross-go-routine data visibility (necessary for the visibility needed for the cancellation), goroutine-local non-function-scoped storage (implied by other statements on how the error map works) are the further divergence.
@seebs Operating systems can do this. How it should be implemented in Go, the compiler and runtime developers have to come up with.
So this error map mechanism is now happening concurrently with the call. And you say the function can be “interrupted when necessary”. How? Go doesn’t have any mechanisms for “interrupting” a call.
The error wrapping needs to be a standard mechanism with standard protocol and standardized error message format rather then bicycle reinvention of every programmer.
@sergeyprokhorenko In the “error is returned” case, the error is literally on the stack. There’s nothing to figure out, it is all part of the calling convention. We know from the method signature what return values we have.
In the case of the implicit error map, there is no obvious way to know which Read method is invoked at compile time. And we’d need to know which Read method is invoked in order to be able to use the correctly named error map (unless all Read methods have error maps named @Read, but that’d spell interesting problems for Readers that wrap other Readers, which is not an unusual thing).
The error map proposal, as it stands, is in need of some serious consideration of edge cases and explanations for how to handle them. Note that none of the edge cases discussed are really that far from “the core” of the language. It may also be good to consider the problem (or problems) you are actually trying to solve. One thing you have highlighted throughout this discussion is “return multiple errors”, but as has been demonstrated, there are at least two ways of accomplishing that today.
With error wrapping, the existing error system also supports tracking an error from origin, back up the call stack.
In your code
n, err: = r.Read (s), the RUNTIME somehow figures out which Read method to take the err error from. Likewise, the RUNTIME must figure out which of the@Readerror maps to handle.You were the first to break the communication: https://github.com/golang/go/issues/49535#issuecomment-986411341
I’m not going to convince anyone. But I think the discussion, arguments and fresh ideas could be useful for this language, which still has few serious inherent flaws. I hope that they will be corrected in the coming years, despite the obstruction created and the language will gain the popularity it deserves. Duct tape will not help, and the authors will have to make significant changes to the language as it exists today.
@deltamualpha Thanks. Your program code is correct.
All errors from
B()are available insideA()through the error map@B. Errors fromB()do not automatically become errors fromA(). I did not think to do it usingwarning()operator, because it takes one error name as a map key, and it adds one error to the error map@A. But I like your sintaxiswarning(@B)for this purpose. I would delete theif len(@B) > 0condition as redundant.And we have to establish rules (and variants) for naming of the errors transferred this way. These rules may include or exclude call chain description in the error name.
And I would also add the ability to explicitly list several errors in the
warning()andescape()operators.You use the word “implicit” a lot in the discussion of this proposal, but one of the noted features of go is that very little is done implicitly; the language eschews many “magical” features or runtime complexity in order to support very obvious coding patterns (the
if err != nilpattern is but one example).It occurs to me, in reading over this thread again, that what you’re asking for here boils down to, in current-code terms, something like…
but that the
mapI’ve added here is instead implicitly present, somehow, on every single function. And all the errors are implicitly defined as well, instead ofconsts a package could export.But Apache Kafka attracts by the idea of a more flexible, dynamical and centralized management of the areas of visibility (topics) of messages (about errors).
@vatine, You have an over-emphasis on functional languages, whereas Go is imperative. And for some reason I didn’t find your proposal of Common Lisp Condition System for Go. If you want error-handling for functional concepts of Go try to invent it yourself. For most Go programmers, this is not a top priority.
@vatine, as the proposal says, “Cases of assigning functions to variables and transferring functions to other functions require special research”. Same for anonymous functions, recursion and some other tricky concepts (except from pure functions) taken from functional languages.
I would suggest treating the anonymous function as part of the function body where the anonymous function is used. But you can suggest another mechanism.
Is this proposal inspired by any prior art in other programming languages? I’m curious if there are influences on the design from other sources.
For example:
My point is that above you said
That is not possible when calling a method of an interface value. When the compiler is compiling package p3 it has no idea what errors might be returned by the
Readmethods in packages p1 and p2. p3 doesn’t import p1 or p2.The point is not that I was unable to express some things that were obvious to me. But I didn’t even think about these things, and community’s questions help me to identify problems and find possible solutions.
But I share the idea approved by many programmers that errors are values, that is, data. I just go further in this direction, and I believe that errors should have data types and data formats. They must be stored, sent and handled as data. Specifically, errors should be detailed enough messages to allow intelligent responses.
Errors should not be references, interfaces, or any other tricky abstractions, but errors should be obvious and complete data at hand. The currently available error handling mechanism is overcomplicated, restrictive and error-prone. It should be depricated.
Of course, primitive C-style global error codes are bad. But passing errors through the last parameter of functions looks like duct tape. Errors must have their own transmission channel, not mixed with the function parameters. It is like pain impulses that propagate along special nerves separately from signals about temperature or touch. I think that for each function we need automatically generated set or (as an ersatz) map of structured error messages, available in the scope of this function. The IRI format is convenient for recording error messages. It allows to store and later add many details of any type. The serialized struct is also usable. The serialized JSON format seems overcomplicated to me, although in some cases it can be used as well.
I do not think so. The convenience of finding an error in the error map depends entirely on how you handle errors received from the called function in the calling function.
You have many options:
@calleR_function["NEW_error"] = @calleD_function["RECEIVED_error"]@calleR_function["RECEIVED_error"] = @calleD_function["RECEIVED_error"]@calleR_function["error:PREVIOUSLY_calleD_function/calleD_function/calleR_function?type='RECEIVED_error'"] = @calleD_function["error:PREVIOUSLY_calleD_function/calleD_function?type='RECEIVED_error'"]. This can be done using a shorthand syntax.Whenever possible, you should choose the simplest option that provides a convenient later search in the error map.
Since Golang does not have sets, programmers have to emulate sets using maps. Don’t write code like that. Never assign false to errors.
This is plain English, without intricate use of interfaces, and term
warningis always used in this way: https://i.stack.imgur.com/z5Fim.png If you don’t like the termescape, please suggestexit,erroror anything else.warning("error_type")is only shortening of the operator@function_name["error_type"] = trueescape("error_type")is only shortening of the operators@function_name["error_type"] = true; return@capitalize[]means error map of the functioncapitalize(). This is the only thing a programmer will have to remember.Please attach the questions and answers as plain text, not as an xlsx file. Thanks.