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: add trap on direct assignment with select block #66161

Open
2 of 4 tasks
bjorndm opened this issue Mar 7, 2024 · 14 comments
Open
2 of 4 tasks

proposal: Go 2: add trap on direct assignment with select block #66161

bjorndm opened this issue Mar 7, 2024 · 14 comments
Labels
error-handling Language & library change proposals that are about error handling. LanguageChange Proposal Proposal-FinalCommentPeriod v2 A language change or incompatible library change
Milestone

Comments

@bjorndm
Copy link

bjorndm commented Mar 7, 2024

Go Programming Experience

Experienced

Other Languages Experience

C, Ruby, Pascal, Basic, Java, Shell, ...

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?

Not that I know of. It is somewhat inspired by the trap statement in certain shells.

Does this affect error handling?

Yes, although it is a language feature of general use. It differs because it is explicit, and can be used not only for error handling, but also for reducing boiler plate.

Is this about generics?

No.

Proposal

The select block is extended to allow a single variable after select and before the block. This installs a trap on the variable that is triggered when any value is assigned to the variable. In the body of such a select block case statements can be used to check the value of the variable. The contents of the select block are executed as if a switch statement with the body of the select statement had been injected just after the statement that caused the variable to be assigned to.

The trap remains in effect for as long as the variable is in scope. The select trap must be defined at the same block level as the variable, so, unlike a defer statement, it is not allowed to do so conditionally in an if or switch block. The trap is not appended for indirect assignments through a function or pointer. the trap is only injected for direct assignments or for assignments to members. For example err=, or in case of a struct err.Foo=. This makes the question if a variable has a trap or not decidable at compile time.

Example:

func CanFail(name string) error {
var err error
select err { // this installs an assignment trap on the err variable
      case err != nil:
          return fmt.Errorf("CanFail: %w", err) 
}

fin, err := os.Open(name)
// Here, the compiler injects the body of the select block, but changes select with switch, so, if err != nil, it will be returned.  
buf, err := io.ReadAll(fin)
// Here, the compiler injects the body of the select block again, but changes select with switch. So, if err != nil, it will be returned.  

return nil
}

Language Spec Changes

No response

Informal Change

No response

Is this change backward compatible?

Yes. In stead of select, a new keyword like trap could also be introduced, but using select is strictly backwards compatible.

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

This is an orthogonal new feature that can be used not only for error handling, but also to reduce repetitive boilerplate in other cases.

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

Somewhat harder as it is a new feature, but it would also simplify error handling and help reduce boilerplate.

Cost Description

No response

Changes to Go ToolChain

All of them.

Performance Costs

Compile time: extra injection work on trapped variables. Run time cost: none.

Prototype

The compiler would simply inject the code block substituting select for switch. That should be quite easy to implement.

@bjorndm bjorndm added LanguageChange Proposal v2 A language change or incompatible library change labels Mar 7, 2024
@gopherbot gopherbot added this to the Proposal milestone Mar 7, 2024
@mainjzb
Copy link

mainjzb commented Mar 7, 2024

#60720

Similar, except for introduction of new keyword

@bjorndm
Copy link
Author

bjorndm commented Mar 7, 2024

Well the main difference is that this doesn't call a function but injects a switch block in predictable locations.

Also this is intended to be purely at compile time only.

@randall77
Copy link
Contributor

This makes the question if a variable has a trap or not decidable at compile time.

What about assignments through pointers, and additionally in other functions?

p := &err
if ... { *p = ... }
maybeAssignToArg(p)

@ianlancetaylor ianlancetaylor added the error-handling Language & library change proposals that are about error handling. label Mar 7, 2024
@ianlancetaylor
Copy link
Contributor

See also #56258.

@bjorndm
Copy link
Author

bjorndm commented Mar 7, 2024

Assignments in other functions would not have the trap appended. Assignments through a pointer would but only if the variable named in the trap is a pointer. The trap block is injected only if there is a = or := statement. So @randall77 in your examples no trap block is injected unless if there also is a select block for p. The assignments are though the p variable not through the err variable. Like this this proposal remains a pure compile time text expansion that is easy to understand.

The difference with the other proposal is that the trap is installed explicitly and statically on a named variable, not on the _ variable.

@randall77
Copy link
Contributor

So

var err error
select err { ... } // set up a trap
err = ... // this traps
*(&err) = ... // this doesn't trap

?

@bjorndm
Copy link
Author

bjorndm commented Mar 7, 2024

That does trap because err is mentioned explicitly. That is the point of this proposal, everything is explicit! :) Of course doing *(&err) is a bit obtuse.

Edit:
And in case err is a struct err.Field = blah does inject the trap since err is mentioned.

@bjorndm bjorndm changed the title proposal: Go 2: add trap on assignment with select block proposal: Go 2: add trap on direct assignment with select block Mar 7, 2024
@randall77
Copy link
Contributor

That does trap because err is mentioned explicitly.

err is mentioned, but there is never an explicit assignment to it. Perhaps this example is better:

p := &err
*p = ...

For extra fun , add arbitrary other complications between the two statements, like

p := &err
if ... { p = ... something else ... }
*p = ...

The general point is that at any pointer assignment, how do you tell whether it is assigning to a trapped variable or not?
I think the only possible answers are "we have to check it at runtime somehow" or "assignments through pointers don't trap".

@bjorndm
Copy link
Author

bjorndm commented Mar 8, 2024

Then it is "assignments through pointers don't trap". The idea is that the trap is strictly compile time and nominal only, which makes it a lot simpler to implement than, say, a defer statement for which Go has to keep a runtime list.

As I see it it will still be very useful even with these restrictions.

Edit: this is also how it this proposal is different from simply using a defer() statement for error handling as we can already do with the help of generics e.g. this comment: #57645 (comment)

@zephyrtronium
Copy link
Contributor

Is the panic here "trapped" or "untrapped"?

var err error
f := func() { err = io.EOF }
select err {
case err != nil:
    panic("trapped")
}
g := func() { err = io.EOF; panic("untrapped") }
f()
g()

Is the panic here 1 or 2? What gets printed before the panic?

var err error
select err {
case err != nil:
    println(1)
    panic(1)
}
select err {
case err != nil:
    println(2)
    panic(2)
}
err = io.EOF

@bjorndm
Copy link
Author

bjorndm commented Mar 8, 2024

The anonymous func is a separate scope so untrapped. The second trap block overwrites the first one so for the second example it is 2 only.

@ddkwork
Copy link

ddkwork commented Mar 14, 2024

package main

import (
	"reflect"
	"runtime"
	"runtime/debug"
)

func main() {
	c := &Caller{}
	c.call()
}

type Caller struct{}

func (c *Caller) call() {
	Error("this is an error", &c)

	// do not run here
	println(1111)
	println(1111)
	println(1111)
	println(1111)
}

func Error(err any, ptr any) {
	if err == nil {
		return
	}
	debug.PrintStack()
	pc, _, _, _ := runtime.Caller(1)
	fn := runtime.FuncForPC(pc)
	funcName := fn.Name()
	//Is it possible for reflection to dynamically insert a return statement at the next line of code where the parent function calls the sub-function?
	//Also need to assign a value to the return value, not sure of the type
	reflect.Indirect(reflect.ValueOf(ptr)).MethodByName(funcName).Close()
}

@ianlancetaylor
Copy link
Contributor

Based on the discussion above, and the similarity to earlier declined issues, and the emoji votings, this is a likely decline. Leaving open for four weeks for further comments.

@bjorndm
Copy link
Author

bjorndm commented Apr 3, 2024

Thanks for considering this. While this proposal does not seem to be what we need, maybe it will help finding a better idea for error handling by elimination.

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 Proposal Proposal-FinalCommentPeriod v2 A language change or incompatible library change
Projects
None yet
Development

No branches or pull requests

7 participants