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: spec: error handling with block statement #68745

Closed
4 tasks
hellolio opened this issue Aug 6, 2024 · 22 comments
Closed
4 tasks

proposal: spec: error handling with block statement #68745

hellolio opened this issue Aug 6, 2024 · 22 comments
Labels
error-handling Language & library change proposals that are about error handling. LanguageChange Suggested changes to the Go language LanguageChangeReview Discussed by language change review committee Proposal Proposal-FinalCommentPeriod
Milestone

Comments

@hellolio
Copy link

hellolio commented Aug 6, 2024

Go Programming Experience

Intermediate

Other Languages Experience

python,js,rust,java,c++

Related Idea

  • Has this idea, or one like it, been proposed before?
  • Does this affect error handling?
  • Is this about generics?
  • Is this change backward compatible? Breaking the Go 1 compatibility guarantee is a large cost and requires a large benefit

Has this idea, or one like it, been proposed before?

no

Does this affect error handling?

yes,I wish to introduce a new mechanism aimed at enhancing exception handling and improving coding practices.

Is this about generics?

no

Proposal

A new feature needs to be introduced: block statement, like this: block check_error(err error) {... } This is very similar to a function, but the block statement does not need to define a return value (it can return any value without limit), and is modified with the keyword block.

block defines the block statement
runblock executes block statement(or goto)

Here's an example:
before:

func example1() (msg string, err error){
    db, err1 := sql.Open("postgres", d.ConnectInfo)
    if err1 != nil {
        // `check_error` is executed as a block statement, then return will return to the caller outside the `example1()` .
        return "", err1
    }else {
        fmt.Println("everything is ok...")
    }

    file, err2 := os.Open(fileName)
    if err2 != nil {
        // `check_error` is executed as a code block, then return will return to the caller outside the `example1()` .
        return "", err2
    }else {
        fmt.Println("everything is ok...")
    }
    defer file.Close()
}

func example2() (string, error){
    db, err2 := sql.Open("postgres", d.ConnectInfo)
    if err2 != nil {
        // `check_error` is executed as a code block, then return will return to the caller outside the `example1()` .
        return "", err2
    }else {
        fmt.Println("everything is ok...")
    }
}

after:
demo1

block check(err error) {
    if err != nil {
        // `check` is executed as a code block, then return will return to the caller outside the `example1()` .
        return "", err
    }else {
        fmt.Println("everything is ok...")
    }
}

func example1() (string, error){
    db, err1 := sql.Open("postgres", d.ConnectInfo)
    goto check(err1)

    file, err2 := os.Open(fileName); goto check(err2)
    defer file.Close()
}

func example2() (string, error){
    db, err2 := sql.Open("postgres", d.ConnectInfo); goto check(err2)
}

demo2
The return value of the block will be strictly limited

block check_error(err error) (string, error) {
	if err != nil {
		// `check_error` is executed as a code block, then return will return to the caller outside the `example1()` .
		return "", err
	} else {
		fmt.Println("everything is ok...")
	}
}

func example1() (string, error){
    db, err1 := sql.Open("postgres", d.ConnectInfo)
    runblock check_error(err1)

    file, err2 := os.Open(fileName); runblock check_error(err2)
    defer file.Close()
}

func example2() (string, error){
    db, err2 := sql.Open("postgres", d.ConnectInfo); runblock check_error(err2)
}

Not only error handling, it can also be used in other places


block myloop(){
	...
}

block yourloop(){
	...
}

func myfun1() error {
	state := 0

	for {
		switch state {
		case 0:
			fmt.Println("State 0")
			state = 1
			goto myloop()
		case 1:
			fmt.Println("State 1")
			state = 2
			goto yourloop()
		case 2:
			fmt.Println("State 2")
			return
		}
	}
}

func myfun2() error {
	state := 0
	for {
		switch state {
		case 0:
			fmt.Println("State 0")
			state = 1
			goto myloop()
		case 1:
			fmt.Println("State 1")
			state = 2
			goto yourloop()
		case 2:
			fmt.Println("State 2")
			return
		}
	}
}

In this way, the cumbersome if err !=nil{} can be placed in a commonly defined code block. When this judgment is needed, you only need to call this code block (very similar to calling a function).
I believe that even without this feature, many developers have developed a function similar to check_error for if err !=nil{}.
However, the limitation of this function is that it cannot return to the outside of the function when it needs to return immediately, but only returns to the caller.
But this proposal can solve this problem. The usage is similar to that of a function. It is executed as a code block at the place where it is called. The most important thing is that the return statement will also be parsed as a return at the place where it is called.
And this proposal is different from switch/case: the program is always executed from top to bottom. Even in the place where the code block is called, it is similar to a function, and it does not avoid errors, but checks errors immediately. I know that switch/case is a good proposal, but when I see the code of switch/case, I always feel like tyr/catch. It is not go.
The runblock and block keywords can be replaced with more appropriate

Language Spec Changes

Informal Change

This way, many repetitive error handling codes can be solved with one function, and I believe that many other places can also use this new feature of runblock.
I think this is a good proposal, because I did not deliberately solve the problem of if err != nil {, but added more expressive features to Go, and the new features can be the answer to the problem of if err != nil {
I believe that the new feature of code blocks can be used in more places

Is this change backward compatible?

yes

Orthogonality: How does this change interact or overlap with existing features?

No response

Would this change make Go easier or harder to learn, and why?

No response

Cost Description

No response

Changes to Go ToolChain

No response

Performance Costs

No response

Prototype

No response

@hellolio hellolio added LanguageChange Suggested changes to the Go language Proposal v2 An incompatible library change labels Aug 6, 2024
@gopherbot gopherbot added this to the Proposal milestone Aug 6, 2024
@hellolio hellolio changed the title proposal: Go 2: Code Blocks with External Effects in Go proposal: Go 2: Convert functions into code blocks for execution Aug 6, 2024
@timothy-king
Copy link
Contributor

The return statement in check_error does not type check. That function does not have any return parameters. You will need to write additional information in the syntax to indicate why this function would be allowed.

If it did have return parameters, how would one know when an early return from the closing function is needed as opposed to the err!=nil branch?

@ianlancetaylor ianlancetaylor changed the title proposal: Go 2: Convert functions into code blocks for execution proposal: spec: Convert functions into code blocks for execution Aug 6, 2024
@ianlancetaylor ianlancetaylor added LanguageChangeReview Discussed by language change review committee and removed v2 An incompatible library change labels Aug 6, 2024
@ianlancetaylor ianlancetaylor changed the title proposal: spec: Convert functions into code blocks for execution proposal: spec: convert functions into code blocks for execution Aug 6, 2024
@earthboundkid
Copy link
Contributor

This is become with the ability to go back: #22624

@seankhliao seankhliao added the error-handling Language & library change proposals that are about error handling. label Aug 6, 2024
@hellolio
Copy link
Author

hellolio commented Aug 7, 2024

When facing an error, we have the following situations
1: Ignore it
file, _ := os.Open(fileName)
2: Simple log output or throw directly

if err1 != nil {
fmt.Println("Error:", err)
\\ or panic
}

3: Stop execution immediately and return to the current call

if err1 != nil {
return xxx
}

My proposal aims to solve the third problem
1: Not a problem,
2: Now it can be solved by wrapping a function, no new proposal is needed

@hellolio
Copy link
Author

hellolio commented Aug 7, 2024

The return statement in check_error does not type check. That function does not have any return parameters. You will need to write additional information in the syntax to indicate why this function would be allowed.

If it did have return parameters, how would one know when an early return from the closing function is needed as opposed to the err!=nil branch?

I noticed that there was something wrong with my example, so I modified my proposal. Now instead of converting the function into a code block, I directly define a code block that can be called. You can take a look at
Maybe I should change runblack to goto, like this

func example1() (string, error){
    db, err1 := sql.Open("postgres", d.ConnectInfo)
    goto check_error(err1)

    file, err2 := os.Open(fileName); goto check_error(err2)
    defer file.Close()
}

@adonovan
Copy link
Member

adonovan commented Aug 7, 2024

This proposal is a major change to the syntax (a new keyword, a new function declaration form) and semantics (functions that return values only on some paths, return statements that actually return from the caller function), and it defeats function encapsulation (since you have to know that a function contains non-local returns).

Therefore, this is a likely decline. Leaving open for four weeks for final comments.
— rfindley for the language proposal review group

@hellolio
Copy link
Author

hellolio commented Aug 7, 2024

This proposal is a major change to the syntax (a new keyword, a new function declaration form) and semantics (functions that return values only on some paths, return statements that actually return from the caller function), and it defeats function encapsulation (since you have to know that a function contains non-local returns).

Therefore, this is a likely decline. Leaving open for four weeks for final comments. — rfindley for the language proposal review group

You're right, but so far all similar proposals have implicitly achieved this. On the contrary, I didn't evade the issue; instead, I explicitly proposed this feature. I think it's better to inform developers than not, and block statement should significantly enhance Go's expressiveness.

Of course, if you think Go doesn't need this feature, we can restrict its use to a very narrow scope (definitely not just for exception handling). Everything is open to discussion.

However, my point is: I really dislike the approach of introducing a specialized structure solely for handling exceptions to solve exception-handling problems. I prefer a more natural way, such as introducing a new feature: block statement. block statement isn't born for error handling, but it naturally achieves that.

Furthermore, block statement doesn't impose new learning burdens because Go already uses {} internally in many places (though not explicitly declared). Although not clearly defined, {} in Go is essentially a block statement, and my proposal merely makes {} reusable.

Lastly, what I want to emphasize is that many developers see Go as not very expressive. Therefore, it's good to enhance Go's expressiveness in this regard. of course, I know the goal of go

@hellolio
Copy link
Author

hellolio commented Aug 8, 2024

This proposal is a major change to the syntax (a new keyword, a new function declaration form) and semantics (functions that return values only on some paths, return statements that actually return from the caller function), and it defeats function encapsulation (since you have to know that a function contains non-local returns).

Therefore, this is a likely decline. Leaving open for four weeks for final comments. — rfindley for the language proposal review group

In fact, the core problem of error handling is how to return, right?

@ianlancetaylor
Copy link
Contributor

Go already has blocks, of course. And it already has functions. I'm not aware of anybody asking for the ability to write a block outside of a function, as this proposal suggests. The only additional feature that provides is the ability for a block to return from its calling function. That introduces a new hidden control flow--what looks like a function call can actually return from the calling function. That makes code harder to understand.

In fact, the core problem of error handling is how to return, right?

I don't think so myself. I think the core problem is how to reduce boilerplate code while retaining clarity and simplicity.

@hellolio
Copy link
Author

hellolio commented Aug 8, 2024

Go already has blocks, of course. And it already has functions. I'm not aware of anybody asking for the ability to write a block outside of a function, as this proposal suggests. The only additional feature that provides is the ability for a block to return from its calling function. That introduces a new hidden control flow--what looks like a function call can actually return from the calling function. That makes code harder to understand.

In fact, the core problem of error handling is how to return, right?

I don't think so myself. I think the core problem is how to reduce boilerplate code while retaining clarity and simplicity.

As I mentioned above(#68745 (comment)), reduce boilerplate code is our goal, But when implementing this goal, the only problem we encounter is how can I reduce my code when I need to implement the following code, which is essentially a question of how to return

if err1 != nil {
return xxx
}

To solve the problem of error handling, any proposal cannot escape this question: how to return
Because the core of error handling is to return when encountering processing
Please think carefully about the nature of this question.

@hellolio
Copy link
Author

hellolio commented Aug 8, 2024

Go already has blocks, of course. And it already has functions. I'm not aware of anybody asking for the ability to write a block outside of a function, as this proposal suggests. The only additional feature that provides is the ability for a block to return from its calling function. That introduces a new hidden control flow--what looks like a function call can actually return from the calling function. That makes code harder to understand.

In fact, the core problem of error handling is how to return, right?

I don't think so myself. I think the core problem is how to reduce boilerplate code while retaining clarity and simplicity.

There is always resistance to introducing new features, but I think it is worth it, especially when the goto or runblock keywords clearly tell the caller that this is a block statement rather than a function.

Maybe we can clearly distinguish block statement from functions when declaring them, so that developers can immediately understand their difference from functions.

@Zxilly
Copy link
Member

Zxilly commented Aug 8, 2024

I think it would be much simpler to implement and use this request if you replace it with the concept of macros in C.

previous discussion: #32620

@timothy-king
Copy link
Contributor

timothy-king commented Aug 8, 2024

There are a bunch of loose threads in this proposal. I'll walk through what my reactions were. I think that will show you what I mean.

  1. Is the semantics supposed to be as if you assigned the arguments and then replaced the block into the function, i.e. a macro? This means things like block G() { if !f() { fmt.Println(x); goto err } } are unconditionally legal to write, fmt, f and err are resolved within scope of the calling block. If so, the proposal is kinda half hearted in this direction.
  2. I kinda doubt you intended to go down the macro route. The first draft sorta used func syntax and has types for arguments, and requires creating a block statement. (If I am getting this wrong, the proposal needs more motivating examples for what you are trying to do.)
  3. The examples you have given could just use vanilla Go like scope rules for symbol lookups. I think this is what you intended.
  4. What are the types in return "", err in check_error? Just looking at the syntax in block, this is a tuple (untyped string, error)? It probably should not yet be (string, error). The syntax does not yet have types to do assignability to. We do not want to interpret what untyped constants like 0 as their default type yet. Should it be a float32? uint8? A type T int? A ~int? That depends on the return type of the function the block is run from.
  5. Also there can be multiple different return types within a block as each return statement can have different types that are all assignable to the result types of the calling function.
  6. So to locally disambiguate, gain significant simplifications, and get not too shabby speedups, I would recommend writing down the types for the return statements with each block declaration, e.g. block check_error(err error) (string, error). You do not need to return from the block, but if you do the values are assignable to a value of these types.
  7. Using function like scoping rules and specifying the return types, then each block could then be type checked and applied independently.
  8. You don't really need runblock as a new keyword, just use check_error(err2).
  9. If you have agreed with the above thus far, it is straightforward to implement this as sugar over functions and function calls. You need some mangling for attaching a defer or recover onto the function stack that the block executed in, and to add a boolean parameter to indicate if the no return branch was taken. (This is all pretty similar to the internals of range-over-func.) So block check_error(err error) (string, error) would be syntactic sugar for a func check_error_as_func(frame *internal_stack, err error) (string, err, bool) and a call statement check_error(err2) would be syntactic sugar for:
    if s, e, b := check_error_as_func(runtime.frame(), err2); b { return s, e }
  10. That clarifies kinda all of the currently loose ends. What happens to defer? What about recover? What happens with calling cycles? Do we need to worry about exponential code blow-up during compilation? How does this interaction with function label scope? How about break/continue/goto statements? Handling for constants in returns? etc.
  11. This does loose some flexibility as it requires the calling function to return (string, error) in each instance. Generics + inference would partially alleviate this, but still have an arity problem.
  12. 2cents: This would be a very complicated way to achieve optional control flow changes via an early return. There are simpler error handling proposals that achieve this.

Of course, you may have different ideas for where you would like to take the proposal. I am mostly trying to illustrate what you need to consider.

@ianlancetaylor
Copy link
Contributor

See also #54361.

@hellolio
Copy link
Author

hellolio commented Aug 9, 2024

There are a bunch of loose threads in this proposal. I'll walk through what my reactions were. I think that will show you what I mean.

  1. Is the semantics supposed to be as if you assigned the arguments and then replaced the block into the function, i.e. a macro? This means things like block G() { if !f() { fmt.Println(x); goto err } } are unconditionally legal to write, fmt, f and err are resolved within scope of the calling block. If so, the proposal is kinda half hearted in this direction.
  2. I kinda doubt you intended to go done the macro route. The first draft sorta used func syntax and has types for arguments, and requires creating a block statement. (If I am getting this wrong, the proposal needs more motivating examples for what you are trying to do.)
  3. The examples you have given could just use vanilla Go like scope rules for symbol lookups. I think this is what you intended.
  4. What are the types in return "", err in check_error? Just looking at the syntax in block, this is a tuple (untyped string, error)? It probably should not yet be (string, error). The syntax does not yet have types to do assignability to. We do not want to interpret what untyped constants like 0 as their default type yet. Should it be a float32? uint8? A type T int? A ~int? That depends on the return type of the function the block is run from.
  5. Also there can be multiple different return types within a block as each return statement can have different types that are all assignable to the result types of the calling function.
  6. So to locally disambiguate, gain significant simplifications, and get not too shabby speedups, I would recommend writing down the types for the return statements with each block declaration, e.g. block check_error(err error) (string, error). You do not need to return from the block, but if you do the values are assignable to a value of these types.
  7. Using function like scoping rules and specifying the return types, then each block could then be type checked and applied independently.
  8. You don't really need runblock as a new keyword, just use check_error(err2).
  9. If you have agreed with the above thus far, it is straightforward to implement this as sugar over functions and function calls. You need some mangling for attaching a defer or recover onto the function stack that the block executed in, and to add a boolean parameter to indicate if the no return branch was taken. (This is all pretty similar to the internals of range-over-func.) So block check_error(err error) (string, error) would be syntactic sugar for a func check_error_as_func(frame *internal_stack, err error) (string, err, bool) and a call statement check_error(err2) would be syntactic sugar for:
    if s, e, b := check_error_as_func(runtime.frame(), err2); b { return s, e }
  10. That clarifies kinda all of the currently loose ends. What happens to defer? What about recover? What happens with calling cycles? Do we need to worry about exponential code blow-up during compilation? How does this interaction with function label scope? How about break/continue/goto statements? Handling for constants in returns? etc.
  11. This does loose some flexibility as it requires the calling function to return (string, error) in each instance. Generics + inference would partially alleviate this, but still have an arity problem.
  12. 2cents: This would be a very complicated way to achieve optional control flow changes via an early return. There are simpler error handling proposals that achieve this.

Of course, you may have different ideas for where you would like to take the proposal. I am mostly trying to illustrate what you need to consider.

Thank you for your thoughts. Here is my answer.
1 and 2, No, I don't really want to submit this proposal as a macro
3, You are right, this is the direction of my proposal, but I want to take this opportunity to increase the expressiveness of Go, and naturally solve the problem of error handling
4, I don't quite understand what you mean, the return type of the block must be the same as the return type of the function (caller), otherwise the compiler should report an error,Unless you don't return
That depends on the return type of the function the block is run from>>Yes
5, they should have a one-to-one correspondence, as mentioned in 4, they must be completely consistent
6, your suggestion is very good, I do not reject such a proposal at all, but this will slightly limit the expressiveness of this new feature (maybe this is a good thing), as mentioned before, this can be fully discussed, everything is open
7, yes, that's it
8, I don't want to oppose your suggestion, but as I said at the start, I hope that this block structure can have a clear boundary with the function so that people who check the code will not be dizzy, so if most people think that runblock/goto is not needed, its ok, but I am worrie about the reviewer, this worry is also the only shortcoming of this proposal, But I really like goto check(err)
9, implemented as a syntactic sugar of the function, this is very good, much better than the macro
10, !!!
11. It is always good to be restrained
12. I cannot agree with your point of view. The only thing this proposal adds is the reuse of code blocks, nothing more, because as mentioned above, all proposals that attempt to solve error handling must face the problem of changing control flow, because this is essentially how to return
goto check(err) is more acceptable? runblock>goto, check_error(err)>check(err)
or returnif check(err)?
or returnfrom check(err)?
Below is a more complete

func example() (string, error) {
db, err1 := sql.Open("postgres", d.ConnectInfo)
goto check(err1)
db, err2 := sql.Open("postgres", d.ConnectInfo); goto check(err2)
}

by the way there are many points to discuss, but the reuse of code blocks is the core of this proposal, I don’t want to only solve the problem of error-handling.

@hellolio
Copy link
Author

hellolio commented Aug 9, 2024

block statement

I really understand your restraint, and that's why I love golang, but I also think it's time to give golang a little more expressiveness (currently goto is a bit useless)
If you are worried about block statement being abused, we can limit it to a small scope, but definitely not just error handling, such as not allowing custom block

@hellolio
Copy link
Author

hellolio commented Aug 9, 2024

See also #54361.

Thank you. I read the proposal. Although the two proposals solve different problems, I think our ideas seem to be very similar. See, one feature solves many problems.

@hellolio hellolio closed this as completed Aug 9, 2024
@hellolio
Copy link
Author

hellolio commented Aug 9, 2024

I think it would be much simpler to implement and use this request if you replace it with the concept of macros in C.
previous discussion: #32620

Sorry, I don't think golang needs macro

I thought about your suggestion again and looked at this issue. Maybe I shouldn't reject this implementation,because I like this answer:#32620 (comment),>>
@ianlancetaylor
hygienic macros are not always bad, but I don't want macros to be abused.This is why I don't want to delete the keyword runblock/goto>>
@timothy-king

But I still want to call it a block statement because block statement are easier to understand for people who don’t have experience in other languages.

@hellolio hellolio reopened this Aug 9, 2024
@hellolio
Copy link
Author

hellolio commented Aug 9, 2024

This proposal is a major change to the syntax (a new keyword, a new function declaration form) and semantics (functions that return values only on some paths, return statements that actually return from the caller function), and it defeats function encapsulation (since you have to know that a function contains non-local returns).

Therefore, this is a likely decline. Leaving open for four weeks for final comments. — rfindley for the language proposal review group

functions that return values only on some paths, return statements that actually return from the caller function
goto isn't it?
I think the idea of ​​my proposal is the same as goto, but it slightly expands the scope of use of goto

This is the code that is currently supported

func example() (err error) {
	if err = someFunction(); err != nil {
		goto handleError
	}
	...
	return nil
handleError:
	return err
}

What is the first reaction when people see goto handleError? If he knows that handleError may return at the first time, then his reaction to seeing goto check(err) should be the same. If he doesn't know whether this code will return the function, then he will see the same reaction when seeing goto check(err). You see, golang has such a problem now. Don't cover it up. Of course, you can say that handleError is still inside the function, but it still changes the control flow, because the first code people see is goto handleError, and then handleError. If people can understand where the code will jump to after seeing handleError, then when people see block check(err){...}, they can also understand it through block. My proposal amplifies this problem, but this problem has always existed. Of course, what needs to be considered is whether the cost of amplifying this problem is worth it.

if I don't use goto, but runblock, so that the controller has always been in the hands of the caller. Whether the block returns or not, the program will return to the caller:
If the block has a return, it will return to the outside of the caller function. If there is no return, it will return to the caller. any way, the caller has control, and the block statement is the same as its name, just a reusable code.i think it better than goto

@rsc-infa-sd
Copy link

rsc-infa-sd commented Aug 9, 2024

I like this proposal, but I think try check(err) is better.
like:

func example() (string, error) {
	myfile, err1 := read_file("path")
	try check(err1)
	err2 := read_file("path"); try check(err2)
}

There is a discussion about try, I think this proposal solves some problems of try:
https://swtch.com/try.html#vararg

@hellolio
Copy link
Author

If we use goto check(err) to implement it, it can be seen as an enhancement of the existing goto capability

@hellolio hellolio changed the title proposal: spec: convert functions into code blocks for execution proposal: spec: error handling with block statement Aug 30, 2024
@findleyr
Copy link
Contributor

findleyr commented Sep 4, 2024

No change in consensus, so declined.
— rfindley for the language proposal review group

@findleyr findleyr closed this as not planned Won't fix, can't repro, duplicate, stale Sep 4, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
error-handling Language & library change proposals that are about error handling. LanguageChange Suggested changes to the Go language LanguageChangeReview Discussed by language change review committee Proposal Proposal-FinalCommentPeriod
Projects
None yet
Development

No branches or pull requests

12 participants