Skip to content

Instantly share code, notes, and snippets.

@perj
Created March 14, 2025 07:23
Show Gist options
  • Select an option

  • Save perj/dc68227694a800a6c305775b9111742a to your computer and use it in GitHub Desktop.

Select an option

Save perj/dc68227694a800a6c305775b9111742a to your computer and use it in GitHub Desktop.

Proposal: return? Keyword for Conditional Returns in Go

Abstract

This proposal introduces the return? keyword in Go, which allows concise early returns based on conditional assignments. It enables direct assignment to return values and a streamlined syntax for handling common error propagation patterns.

Background

Go's error handling is explicit and encourages checking return values. However, common patterns, such as propagating errors, often lead to verbose and repetitive code. Consider the typical error-checking pattern:

b, err := json.Marshal(v)
if err != nil {
    return nil, fmt.Errorf("failed to marshal JSON: %w", err)
}

While explicit, this pattern introduces redundancy. This proposal introduces return? to simplify such cases while maintaining clarity.

Proposal

Part 1: return? for Conditional Returns

A new keyword, return?, is introduced for conditional returns. This keyword may be assigned one or more values, at most the number of the function’s return values. If fewer values are assigned, they correspond to the rightmost return values. Most commonly, a single value will be assigned, matching the single rightmost return value.

After a return? assignment, the function returns immediately if any assigned value is non-zero.

Example:

func example() ([]byte, error) {
    ...
    b, return? := json.Marshal(v)
    ...
}

This is equivalent to:

func example() ([]byte, error) {
    ...
    b, _tmp0 := json.Marshal(v)
    if _tmp0 != nil {
        return nil, _tmp0
    }
    ...
}

Any non-named return values not assigned to by return? use their zero-value. If the function has named return values, those that are not overwritten by return? retain their existing values, but any assigned to will have the zero value from this point in the case the function does not return.

Part 1 by itself solves the naked err return case, but decorated errors require a wrapping function.

func wraperr(err error, msg string) error {
    if err == nil {
        return nil
    }
    return fmt.Errorf("%s: %w", msg, err)
}

func example() ([]byte, error) {
    ...
    b, err := json.Marshal(nil)
    return? = wraperr(err, "failed to marshal JSON")
    ...
}

Part 2: ? Operator for Conditional Evaluation

To further simplify conditional returns, a unary ? prefix operator is introduced. It is only valid within expressions assigned to return? and extracts a value for evaluation before executing the rest of the statement. If the extracted value is equal to the zero value, the rest of the statement is skipped.

Example:

b, err := json.Marshal(nil)
return? = fmt.Errorf("failed to marshal JSON: %w", ?err)

This translates to the following, given the same example function signature as in part 1:

b, err := json.Marshal(nil)
if _tmp1 := err; _tmp1 != nil {
    _tmp2 := fmt.Errorf("failed to marshal JSON: %w", _tmp1)
    if _tmp2 != nil {
        return nil, _tmp2
    }
}

To avoid ambiguous evaluation order, the ? operator can only appear once in the right-hand side of a return? assignment. To keep code as readable as possible, it is only valid when return? is alone as the left-hand side of the assignment.

Invalid Use Cases of ?

To prevent ambiguity and unintended behavior, the following usages of ? are invalid:

// Invalid: `?` cannot be used outside of a `return?` assignment
x := ?err

// Invalid: Multiple `?` operators in a single assignment
return? = fmt.Errorf("error 1: %w, error 2: %w", ?err1, ?err2)

// Invalid: `return?` is not the only thing assigned to.
x, return? := y, ?err

A valid alternative to the second invalid example would be

// Valid: Returns if errors.Join returns non-nil.
return? = ?errors.Join(err1, err2)

Changes to the Go Language Specification

To incorporate return? and the ? operator, the following modifications to the Go specification would be necessary:

  1. Expressions:

    • Introduce the ? unary operator, restricted to a single expression on the right-hand side of a return? assignment.
    • Define ?expr as an expression that extracts the value before the assignment statement and skips the statement if the value is equal to the zero value of its type.
  2. Statements:

    • Modify the assignment and variable declaration statement rules to allow return? as a valid left-hand side operand.
    • Enforce that return? is used only in functions and that it can only be used in the left-hand side of assignments.
  3. Return Behavior:

    • Specify that return? immediately exits the function at the end of the statement when assigned non-zero values.
    • Ensure compatibility with both named and unnamed return values, where unassigned named returns retain their previous state.

Rationale

The return? keyword and ? operator offer concise, readable error handling without introducing excessive complexity. By focusing on common patterns, they enhance maintainability while preserving Go’s explicit return semantics.

Implementation Considerations

  1. Compiler Changes: return? and ? require changes to Go’s parser and type checker.
  2. Backward Compatibility: No existing Go code is affected.
  3. Tooling Impact: Minimal impact on tooling, as return? is syntactic sugar rather than a structural change.

Conclusion

This proposal introduces a lightweight, intuitive approach to conditional returns, improving Go’s ergonomics while preserving clarity and simplicity.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment