-
Notifications
You must be signed in to change notification settings - Fork 17.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
proposal: Go 2: error map or log for error handling #49535
Comments
Please note that you should fill https://github.com/golang/proposal/blob/master/go2-language-changes.md when proposing a language change. |
Hello Daniel I attached the required filled questionnaire to the issue |
Please attach the questions and answers as plain text, not as an xlsx file. Thanks. |
I'm sorry, I don't understand the benefit of this. What do we gain? |
Hello Ian I attached the required questions and answers as plain text to the issue |
I don't understand what code would look like with this change or how it can be used. Can you please provide examples of before/after |
Hello Sean Please do not edit my proposal, but add comments instead. |
Hello Ian I inserted the required benefits of the proposal into the issue |
Hello Sean I inserted the required examples of code before/after into the issue |
Hello Dave, Sean, Hao Xin, generikvault, Vladimir, Andreas, Dominik and Gabriel The proposal was resently substantially improved and clarified according to your questions. Therefore I hope you assess it again from scratch, change your mind and delete dislikes. Thanks! |
In this code 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!")
} is there meant to be a call to It seems that both the caller 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 |
additional questions: |
The answers are in the proposal:
|
No. I have demonstrated what the simplified example from the source looked like and will look like after the proposal. In this example, there was no call to the capitalize() function to get the capitalized value, since all the attention was paid to passing the error. Accordingly, I do not use the call to the capitalize() function in the program code after the sentence either. |
Of course, the programmer who uses the function that caused the error must know the error codes that this function can pass. This is obvious, it is his/her usual job, and there is nothing hard about it. |
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 I am confused as to the intended utility of this proposal. |
This is plain English, without intricate use of interfaces, and term
|
No, you don't understand this proposal correctly, because:
|
func Example(v int) int {
if v < 0 {
// This is at the bottom of the recursion.
// Can the original caller see it?
// Is it visible all the way up?
// Where is this map allocated?
// How is it scoped?
// What is even going on here?
escape("invalid v")
}
if v == 0 {
return v
}
return v + Example(v - 2)
}
func check() {
if @Example["invalid v"] {
panic("v was invalid") // What was the invalid value? There's no way to know.
}
}
func main() {
v := Example(29)
// Will this work? If so, that is not just global state, but global state
// that's updated on _every_ single function call. How is that going
// to work with anything concurrent? If not, what are the rules? Who
// can see the map? Are there different maps for each call? How are
// they accessed? How are they scoped?
check()
} 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 |
With respect, this statement is not correct. In the link that you cite, this is the code: func capitalize(name string) (string, error) {
if name == "" {
return "", errors.New("no name provided")
}
return strings.ToTitle(name), nil
}
func main() {
name, err := capitalize("sammy")
if err != nil {
fmt.Println("Could not capitalize:", err)
return
}
fmt.Println("Capitalized name:", name)
} In this example, the function Above, you rewrote this code using this proposal as 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!")
} This function no longer calls So I am going to repeat my question: Is there meant to be a call to |
With respect, the term "warning" means many different things in different uses in programming. It is not always used in any one way. |
It seems to me that this proposal does not simplify "find out if an error occurred" (if we can expect people to never set a It does not make it simpler to see if a specific error occurred. In the existing "return an error value", you could compare against package-specific sentinels, or use one of It is also not at all obvious how this error propagation model works in self-recursive situations. Is there a single |
DeedleFake, read my answers more carefully. In them, every word is decisive (string keys, not just booleans; |
@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. |
The Go specification does not say how many instances of the error map should be, and what visibility area the error map has, because the specification say nothing about the error map. |
The question about a concept like "interrupting a call" is not how it would be implemented. The question is how it would be expressed in the language. What would people write to interrupt a call? At this point we are no longer talking about the Go language as it exists today. We are talking about a different language with different capabilities. |
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. |
@ianlancetaylor The interruption (from outside) of the function or method that raised the warning could be expressed in the language like this: 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. |
so... type FuncPointer func()
func A() {
time.Sleep(10 * time.Second)
warning("oops")
time.Sleep(10 * time.Second)
fmt.Println("unreachable because main 'stops' the function once the error map is populated?")
}
func main() {
var a FuncPointer
a = A
go a()
for { // check forever
if len(@a) != 0 {
stop(a)
}
}
} 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? |
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. |
@vatine I am gradually leaning towards the idea that using a global (implicitly declared in the universe block) error log (with pointers to erroneous functions, with limited access and with rich metadata), divided into named topics, like in Apache Kafka, instead of local error maps, could simplify and speed up error handling. And this mimics the Go language as it exists today. |
|
@seebs Each function instance must have a unique pointer and a unique error map. To stop the called function, it must be run in a goroutine, as @deltamualpha depicted above. A pointer to the desired function instance is extracted from the error message or from the error log. |
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) |
The pointer to the function or method can be also get from the go statement when running the goroutine:
Additional statements may be helpful:
The parameter can be a list of several pointers, separated by commas. |
So, at this point, we can't really just use The claim that
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. |
Local error map |
So how do we know which call of a function
Is 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. |
@seebs |
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 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. |
See in the description of this proposal for error map:
The But the pointer to "each function instance" is a feature of alternative (better) design: error log |
No, sometimes operation system have to externally stop a goroutine with all your application, but it's too late and rude. Yes, goroutine needs to be written with careful attention to detail with regards to what happens if the code is interrupted. |
I don't understand what this means. I've never seen a goroutine be killed off by the OS. |
@theckman Goroutine is a part of application. OS can stop the application with all its goroutines, including the erroneos goroutine. Sometimes it's better to sacrifice one erroneos goroutine than the entire application. |
If the entire application dies, we don't have to worry that mutexes are incorrectly locked or double-unlocked. We can't say the same about killing a single goroutine. |
@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 |
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. |
@sergeyprokhorenko Just to be clear, please open your new proposal in a new issue, and close this one, when you're ready. Thanks. |
New better proposal #50280 is ready |
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
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 thatwarning("error_type")
andescape("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 anif
orswitch
statement, an error type was used that is not in the error map, then valuefalse
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:
Examples of code before and after proposal
questionnaire.xlsx
questionnaire.txt
The text was updated successfully, but these errors were encountered: