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 a new operator to pass error between calls. #64493

Closed
4 tasks
jellyterra opened this issue Dec 1, 2023 · 4 comments
Closed
4 tasks

proposal: Go 2: Add a new operator to pass error between calls. #64493

jellyterra opened this issue Dec 1, 2023 · 4 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

@jellyterra
Copy link

jellyterra commented Dec 1, 2023

Go Programming Experience

Experienced

Other Languages Experience

Go, C, Scala, Kotlin

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?

No.
It just simplifies the syntax of passing error and avoid most if err != nil { return 0, nil, error } statements which don't really handle the errors.

Is this about generics?

No.

Proposal

It is really a pain when write codes about analysis intensive tasks (such as compilers) in current Go version. The codes around passing errors take much!

Add a new operator or use existing token to simplify if err != nil { return ...(zero values)..., err } to reduce the codes around error checking, passing and handling.

Language Spec Changes

A new operator.
This operator allows hiding received error (from callee) in user code and passing the error to the caller if the error is not nil.

Informal Change

Old style:

func PerfectSubroutine() (int, int) {
	...

	return resultA, resultB
}

func Subroutine() (int, int, error) {
	..., ... := PerfectSubroutine()

	...

	..., err := ...()
	if err != nil {
		return 0, 0, err // It is a pain!
	}

	return ..., ..., nil
}

func Foo() (int, int, error) {
	a, b, err := Subroutine()

	if err != nil {
		return 0, 0, err // It is a pain!
	}

	...

	c, d, err := Subroutine()

	if err != nil {
		return 0, 0, err // It is a pain!
	}

	...

	e, f, err := Subroutine()

	if err != nil {
		return 0, 0, err // It is a pain!
	}

	// ..., err := Subroutine()
	// ..., err := Subroutine()
	// ..., err := Subroutine()

	...

	return resultA, resultB, nil
}


func main() {
	...

	a, b, err := Foo()
	switch err := err.(type) {
	case nil:
	case AError:
		...
	case BError:
		...
	default:
		...
	}

	...
}

New style:

func PerfectSubroutine() (int, int) {
	...

	// The operator is denied when the last return value type is not error.

	// ILLEGAL: the caller's last return value type is not error.
	// ..., ... := Subroutine()!!!

	return resultA, resultB
}

func Subroutine() (int, int, error) {

	// ILLEGAL: the callee's last return value type is not error.
	// ..., ... := PerfectSubroutine()!!!

	..., ... := PerfectSubroutine()

	...

	... := ...()!!!
	// if err != nil {
	// 	return 0, 0, err
	// }

	return ..., ..., nil
}

func Foo() (int, int, error) {

	// NOTE: Here "err" refers to an error interface instance managed by the compiler and hidden from the user code.

	a, b, /* , err */ := Subroutine()!!!
	// if err != nil {
	// 	return 0, 0, err
	// }

	...

	c, d /* , err */ := Subroutine()!!!
	// if err ...

	...

	e, f /* , err */ := Subroutine()!!!
	// if err ...

	// ... := Subroutine()!!!
	// ... := Subroutine()!!!
	// ... := Subroutine()!!!

	...

	return resultA, resultB, nil
}

func main() {
	...

	a, b, err := Foo()
	switch err := err.(type) {
	case nil:
	case AError:
		...
	case BError:
		...
	default:
		...
	}

	...
}

Is this change backward compatible?

Yes.

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

  • Simplify error passing at the language level.
  • The change improves the quality of codes.
    Passing the occurred error through the last return value (and is type of error) with an operator.

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

Easier.
Reducing the monotonous "if err != nil``" blocks that don't really handle but pass errors makes the main logic clear.

Cost Description

  • No runtime change.
  • Choose and add this operator to lexer/scanner.
  • Add new nodes about it to the AST.

Changes to Go ToolChain

Go compiler frontend, Gopls.

Performance Costs

Compile time cost changes slightly. Almost no runtime cost.

Prototype

Write a new implementation of "go/ast.Expr" for this operator, which wraps "go/ast.CallExpr".

Possible implementations to translate the node:

  • Generate/transform to "go/ast.BranchExpr" with "go/ast.ReturnStmt" inside.
    ... := Callee()!!! => ..., err_or_else := Callee(); if err_or_else != nil { return ..., err }
  • Translate the node in native way.
@jellyterra jellyterra added LanguageChange Proposal v2 A language change or incompatible library change labels Dec 1, 2023
@gopherbot gopherbot added this to the Proposal milestone Dec 1, 2023
@gophun
Copy link

gophun commented Dec 1, 2023

This is just the try proposal (#32437) with !!! instead of try().

@seankhliao seankhliao added the error-handling Language & library change proposals that are about error handling. label Dec 1, 2023
@adonovan
Copy link
Member

adonovan commented Dec 6, 2023

This seems like a duplicate of an earlier proposal that was explored in detail, and is thus a likely decline. Leaving open for four weeks of final comments.

@asyslinux
Copy link

asyslinux commented Dec 21, 2023

I suggest this. But if the developers of the Go language think about it, I think it could be quite possible to implement it. I have an idea how to additionally simplify error handling in Go, but without add new keywords and obey backward compatibility in Go also for mixed use of old and new style, only need create a special type like swap err type to special type for compiler.

In addition to err != nil, it is better to also have the possibility of err.(failure) and scope(success). The D programming language has scope guard functions for this purposes.

For example, defer in Go already performs a role similar to scope(exit) in the D programming language.
defer f.Close() in Go is the same as scope(exit) f.Close() in D

Go lacks scope(failure) and scope(success) or their analogues, but if you do it in Go it will return the error value to the upper scope. It would be much more convenient and beautiful than trying to do an analogue of try.

This is we have now:

package main

import (
        "log"
        "os"
)

func work(n string) error {
        f, err := os.Open(n)
        if err != nil {
                log.Fatal(err)
                return err
        }
        defer f.Close()

        _, err = f.Stat()
        if err != nil {
                log.Fatal(err)
                return err
        }
        return nil
}

func main() {
        n := "/tmp/myfile.txt"
        err := work(n)
        if err != nil {
                log.Fatal(err)
        }
}

You have been trying for years now to come up with a way to simplify error handling down to 1 line, this is the most realistic solution. Automatic error forwarding to the upper scope. And so that explicit error handling remains in the Go language. It would be nice if Go could do something like this:

package main

import (
        "log"
        "os"
)

func work(n string) error {
        f, err.(failure) := os.Open(n) // If err.(failure) != nil, then automatically returns err
        defer f.Close() // Not executed after err.(failure) != nil
        _, err.(failure) = f.Stat() // If err.(failure) != nil, then automatically returns err
        return nil // Not executed after err.(failure) != nil
}

func swork(n string) (string, error) {
        f, err.(failure) := os.Open(n) // If err.(failure) != nil then automatically returns err and string value as "" value. For another types also default values, empty structs etc...
        defer f.Close() // Not executed after err.(failure) != nil
        _, err.(failure) = f.Stat() // If err.(failure) != nil then automatically returns err and string value as "" value. For another types also default values, empty structs etc...
        return "", nil // Not executed after err.(failure) != nil
}

func main() {

        n := "/tmp/myfile.txt"

        //err.(failure) := work(n) // failure in this case automatically generates panic, because this is main scope
        err := work(n) // classic error handling
        if err != nil {
                log.Fatal(err)
        }

        //s, err.(failure) := swork(n) // failure in this case automatically generates panic, because this is main scope
        s, err := swork(n) // classic error handling
        if err != nil {
                log.Fatal(err)
        }
        fmt.Println(s)

}

@findleyr
Copy link
Contributor

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

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

8 participants