Skip to content
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

Closed
sergeyprokhorenko opened this issue Nov 11, 2021 · 122 comments
Closed

proposal: Go 2: error map or log for error handling #49535

sergeyprokhorenko opened this issue Nov 11, 2021 · 122 comments
Labels
error-handling Language & library change proposals that are about error handling. FrozenDueToAge LanguageChange Suggested changes to the Go language Proposal v2 An incompatible library change
Milestone

Comments

@sergeyprokhorenko
Copy link

sergeyprokhorenko commented Nov 11, 2021

New better proposal #50280 is ready

I propose the interrelated changes to the Go language:

  1. 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 the error_ or error. 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.

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

  1. 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:

  1. Very concise and obvious notation even for a novice Go programmer
  2. 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
  3. Simple implementation
  4. Doesn't affect compilation time and performance
  5. Explicit and composite error naming in the calling function
  6. Аrbitrary and easy error naming in the function in which the error occurred (including dynamic name generation)
  7. Ability to send errors along the chain of function calls
  8. The compiler can catch unhandled explicitly specified errors
  9. 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)
  10. 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
  11. 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!")
}

questionnaire.xlsx
questionnaire.txt

@mvdan
Copy link
Member

mvdan commented Nov 11, 2021

Please note that you should fill https://github.com/golang/proposal/blob/master/go2-language-changes.md when proposing a language change.

@mvdan mvdan added the WaitingForInfo Issue is not actionable because of missing required information, which needs to be provided. label Nov 11, 2021
@sergeyprokhorenko
Copy link
Author

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

@ianlancetaylor
Copy link
Contributor

Please attach the questions and answers as plain text, not as an xlsx file. Thanks.

@ianlancetaylor
Copy link
Contributor

I'm sorry, I don't understand the benefit of this. What do we gain?

@ianlancetaylor ianlancetaylor changed the title Error map for error handling proposal: Go 2: error map for error handling Nov 12, 2021
@ianlancetaylor ianlancetaylor added error-handling Language & library change proposals that are about error handling. v2 An incompatible library change LanguageChange Suggested changes to the Go language Proposal labels Nov 12, 2021
@gopherbot gopherbot added this to the Proposal milestone Nov 12, 2021
@sergeyprokhorenko
Copy link
Author

Please attach the questions and answers as plain text, not as an xlsx file. Thanks.

Hello Ian

I attached the required questions and answers as plain text to the issue

@seankhliao
Copy link
Member

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

@sergeyprokhorenko
Copy link
Author

sergeyprokhorenko commented Nov 13, 2021

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.
I will answer your question as soon as possible.

@sergeyprokhorenko
Copy link
Author

I'm sorry, I don't understand the benefit of this. What do we gain?

Hello Ian

I inserted the required benefits of the proposal into the issue

@sergeyprokhorenko
Copy link
Author

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

I inserted the required examples of code before/after into the issue

@sergeyprokhorenko
Copy link
Author

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!

@ianlancetaylor
Copy link
Contributor

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 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 main and the function capitalize have 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 warning mean? What does escape mean? What does @capitalize mean? None of this is obvious.

@seankhliao
Copy link
Member

additional questions:
where does this return values?
how does this handle error wrapping?
how do you compare against multiple different errors?
how will this integrate with existing libraries/code that returns error values?

@sergeyprokhorenko
Copy link
Author

additional questions: where does this return values? how does this handle error wrapping? how do you compare against multiple different errors? how will this integrate with existing libraries/code that returns error values?

The answers are in the proposal:

  • There is no mention of returning error values (last returned parameter) in the proposal because there is no return of error values in the proposal, but instead there is an error map through which error codes are passed from the called function to the caller function
  • As to error wrapping, the ability to send errors along the chain of function calls is provided: a caller function can insert error codes from the called function inside its own error codes or replace them with its own error codes
  • As to multiple different errors, If the corresponding function is used several times in the same scope, then all different types of errors will appear in the error map each time when function is used, and the mentioned switch operator in the caller function is convenient to compare against multiple different errors (or the mentioned desision table in difficult cases)
  • No integration is envisaged with the existing overengeneered technology, which has been rightly criticized for many years, and should be deprecated. But for the transition period my proposal can be used in parallel with existing error handling methods

@sergeyprokhorenko
Copy link
Author

sergeyprokhorenko commented Nov 14, 2021

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 capitalize? All I see is a reference to @capitalize. Does that somehow call the function? What argument does it pass?

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.

@sergeyprokhorenko
Copy link
Author

It seems that both the caller main and the function capitalize have to know about the string "no name provided". That seems harder to use, not easier.

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.

@DeedleFake
Copy link

DeedleFake commented Nov 14, 2021

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 errno meaning 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.

@sergeyprokhorenko
Copy link
Author

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 warning mean? What does escape mean? What does @capitalize mean? None of this is obvious.

This is plain English, without intricate use of interfaces, and term warning is always used in this way: https://i.stack.imgur.com/z5Fim.png
If you don't like the term escape, please suggest exit, error or anything else.

warning("error_type") is only shortening of the operator @function_name["error_type"] = true

escape("error_type") is only shortening of the operators @function_name["error_type"] = true; return

@capitalize[] means error map of the function capitalize(). This is the only thing a programmer will have to remember.

@sergeyprokhorenko
Copy link
Author

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 errno meaning 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.

No, you don't understand this proposal correctly, because:

  • Error map is not global (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 state
  • There will be two new predefined functions (with and without instantaneous exiting the function where the error occured)
  • There are initially no errors in error map, which values become true later. The error map is empty initially. The new predefined functions will add errors into the error map
  • There could be many booleans for every function
  • It does not lose a lot of compile-time safety due to the usage of string keyed data everywhere, because The compiler can catch unhandled explicitly specified errors

@DeedleFake
Copy link

DeedleFake commented Nov 15, 2021

Error map is not global (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 state

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.

There could be many booleans for every function

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.

It does not lose a lot of compile-time safety due to the usage of string keyed data everywhere, because The compiler can catch unhandled explicitly specified errors

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?

@ianlancetaylor
Copy link
Contributor

No. I have demonstrated what the simplified example from the ]source](https://www.digitalocean.com/community/tutorials/handling-errors-in-go) 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.

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 main calls capitalize, passing the string "sammy".

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 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?

@ianlancetaylor
Copy link
Contributor

This is plain English, without intricate use of interfaces, and term warning is always used in this way:

With respect, the term "warning" means many different things in different uses in programming. It is not always used in any one way.

@vatine
Copy link

vatine commented Nov 16, 2021

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 false value in the map, a len(<errormap>) may suffice to say "no error occurred". In many cases, you have a multi-layered error handling approach, where stage 1 is "there was an error", and stage 2 is "appropriate error-specific handling". This may simplify stage 2, but certainly does not simplify stage 1.

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 errors.Is or errors.As. What is does do, however, is making it impossible to return more detailed information about what failed.

It is also not at all obvious how this error propagation model works in self-recursive situations. Is there a single @function map? Is there one that spans each self-call boundary? If the latter, how do you distinguish between the "communicates with a function I call" and "communicates with my caller"?

@sergeyprokhorenko
Copy link
Author

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

@vatine
Copy link

vatine commented Dec 8, 2021

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

@sergeyprokhorenko
Copy link
Author

If you propose further divergence from how Go works today...

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.

@ianlancetaylor
Copy link
Contributor

Operating systems can do this. How it should be implemented in Go, the compiler and runtime developers have to come up with.

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.

@vatine
Copy link

vatine commented Dec 9, 2021

@sergeyprokhorenko

If you propose further divergence from how Go works today...

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.

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.

@sergeyprokhorenko
Copy link
Author

sergeyprokhorenko commented Dec 9, 2021

Operating systems can do this. How it should be implemented in Go, the compiler and runtime developers have to come up with.

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.

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

@deltamualpha
Copy link

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?

@seebs
Copy link
Contributor

seebs commented Dec 9, 2021

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.

@sergeyprokhorenko
Copy link
Author

@sergeyprokhorenko

If you propose further divergence from how Go works today...

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.

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.

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

@sergeyprokhorenko
Copy link
Author

function can be "stopped" (what does that even mean?)

@deltamualpha

stop(pointer_to_function_X) outside or inside the function_X means the same as return inside the function_X

@sergeyprokhorenko
Copy link
Author

sergeyprokhorenko commented Dec 9, 2021

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.

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

@sergeyprokhorenko
Copy link
Author

sergeyprokhorenko commented Dec 11, 2021

The error log with predeclared identifier ErrorLog can be similar to a table in database, containing the following fields:

  • package name (like regular topics in Apache Kafka)
  • password hash for the package
  • source file name
  • pointer to the instance of the function/method that caused the error
  • timestamp
  • the name of the function/method that caused the error
  • error code / error type
  • additional info
  • etc.

The error types dictionary with a predeclared identifier ErrorType may contain the following fields:

  • error code / error type (it's a key field)
  • severity level
  • error description
  • URL of manual
  • etc.

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)

@sergeyprokhorenko sergeyprokhorenko changed the title proposal: Go 2: error map for error handling proposal: Go 2: error map or log for error handling Dec 11, 2021
@sergeyprokhorenko
Copy link
Author

sergeyprokhorenko commented Dec 11, 2021

Operating systems can do this. How it should be implemented in Go, the compiler and runtime developers have to come up with.

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.

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

The pointer to the function or method can be also get from the go statement when running the goroutine:

go(pointer_to_function_X_variable) X() // the pointer to the new function instance is assigned to the variable
// something else
stop(pointer_to_function_X_variable) // the pointer to desired function instance is extracted from the variable

Additional statements may be helpful:

pause(pointer_to_function_X_variable)
continue(pointer_to_function_X_variable)
recompute(pointer_to_function_X_variable, mode) // input data and control parameters must be
// updated beforehand, for example, for safe mode or for recovery mode
next(pointer_to_function_X_variable) // like in Python's generator
wait(pointer_to_function_X_variable)

The parameter can be a list of several pointers, separated by commas.

@seebs
Copy link
Contributor

seebs commented Dec 11, 2021

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.

var x sync.Mutex

func StopMeBeforeISleepAgain() {
    for {
        time.Sleep(1 * time.Duration)
        func() {
            defer x.Unlock()
            x.Lock()
            fmt.Printf("%*s", 1000000, "")
        }()
    }
}

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.

@sergeyprokhorenko
Copy link
Author

sergeyprokhorenko commented Dec 11, 2021

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.

Local error map @functionname with its area of visibility and global error log are alternatives. They cannot be combined. Error log with pointer to the instance of the function/method that caused the error is a better choice for functional programming

@seebs
Copy link
Contributor

seebs commented Dec 11, 2021

So how do we know which call of a function @functionname refers 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?

y1 := foo(x1)
y2 := foo(x2)
if len(@foo) != 0 {...}

Is @foo here 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 @foo refers 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.

@sergeyprokhorenko
Copy link
Author

What about the defer?

@seebs stop is needed to stop zombie goroutines or goroutines that have become useless or even dangerous. Any tool can be used in the wrong way, but this does not mean that the tool is not needed. stop continues with defer, just like return continues with defer. Therefore, the resources of the stopped goroutine will be released.

@seebs
Copy link
Contributor

seebs commented Dec 11, 2021

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:

// #1:
m.Lock()
defer m.Unlock()
// #2:
defer m.Unlock()
m.Lock()

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.

@sergeyprokhorenko
Copy link
Author

sergeyprokhorenko commented Dec 11, 2021

So how do we know which call of a function @functionname refers 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?

See in the description of this proposal for error map:

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.

The @functionname refers to both instances.

But the pointer to "each function instance" is a feature of alternative (better) design: error log

@sergeyprokhorenko
Copy link
Author

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.

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.

@theckman
Copy link
Contributor

No, sometimes operation system have to externally stop a goroutine with all your application, but it's too late and rude.

I don't understand what this means. I've never seen a goroutine be killed off by the OS.

@sergeyprokhorenko
Copy link
Author

sergeyprokhorenko commented Dec 11, 2021

No, sometimes operation system have to externally stop a goroutine with all your application, but it's too late and rude.

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.

@seebs
Copy link
Contributor

seebs commented Dec 11, 2021

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.

@sergeyprokhorenko
Copy link
Author

sergeyprokhorenko commented Dec 12, 2021

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

@sergeyprokhorenko
Copy link
Author

sergeyprokhorenko commented Dec 15, 2021

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.

@ianlancetaylor
Copy link
Contributor

@sergeyprokhorenko Just to be clear, please open your new proposal in a new issue, and close this one, when you're ready. Thanks.

@sergeyprokhorenko
Copy link
Author

New better proposal #50280 is ready

@golang golang locked and limited conversation to collaborators Dec 20, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
error-handling Language & library change proposals that are about error handling. FrozenDueToAge LanguageChange Suggested changes to the Go language Proposal v2 An incompatible library change
Projects
None yet
Development

No branches or pull requests