Skip to content

Proposal: Command Syntax Support for `defer`

xushiwei edited this page Mar 13, 2026 · 2 revisions

Summary

Extend defer in xgo to accept a command-style invocation — arguments separated by spaces without enclosing parentheses — in addition to the existing function-call syntax.

// existing (still valid)
defer fn(arg1, arg2)
 
// proposed
defer fn arg1, arg2

Motivation

xgo already allows top-level statements such as go and ordinary function calls to be written in a command style, inspired by shell and scripting languages. defer is the one common control keyword left out of that ergonomic improvement.

Allowing command syntax for defer provides three concrete benefits:

  1. Consistency. Users who adopt the command style for regular calls no longer need to switch mental models when writing a deferred call.
  2. Reduced noise. The extra pair of parentheses adds punctuation without adding information, especially for single-argument defers like defer close f or defer log "done".
  3. Scripting friendliness. xgo is frequently used in script-like programs where brevity matters; bringing defer in line with that style lowers friction.

Specification

Syntax

The grammar rule for a DeferStmt is extended from:

DeferStmt = "defer" Expression .

to:

DeferStmt     = "defer" CallExpr
              | "defer" CommandExpr .
 
CommandExpr   = FunctionRef { "," Argument } .
FunctionRef   = QualifiedIdent | PrimaryExpr .
Argument      = Expression .

In prose: after the defer keyword the parser may encounter either

  • an expression that is already a call (fn(...)) — the existing path; or
  • an identifier (or selector expression) followed by zero or more comma-separated argument expressions without an opening parenthesis — the new command path.

The two forms are unambiguous at the token level: if the token immediately following the function reference is (, the existing call-expression path is taken; otherwise the parser attempts the command path.

Desugaring

A command-style defer is desugared to the equivalent parenthesised form at parse time (or as an explicit AST transform), so that no changes are required downstream in the type-checker, IR, or code generator.

defer fn arg1, arg2, arg3
  ──►  defer fn(arg1, arg2, arg3)

Variadic functions

Variadic calls are handled naturally because the transform is purely syntactic:

defer fmt.Println "hello", "world"
  ──►  defer fmt.Println("hello", "world")

Method expressions

Selector expressions work as the function reference:

defer db.Close
  ──►  defer db.Close()          // zero-argument command call
 
defer w.Write buf
  ──►  defer w.Write(buf)

Zero-argument form

defer fn with no arguments is already valid Go (a function value used as an expression is not a call). The command syntax does not change that rule — a bare defer fn remains a deferred call with no arguments only if fn is a no-argument function; otherwise it is a compile error, identical to defer fn().


Examples

// 1. Close a file
f, _ := os.Open("data.txt")
defer f.Close
 
// 2. Unlock a mutex
defer mu.Unlock
 
// 3. Log with arguments
defer log.Printf "request done: %s", reqID
 
// 4. Custom cleanup helper
defer cleanup db, cache, logger
 
// 5. Anonymous function — command syntax does not apply; use existing form
defer func() {
    fmt.Println("bye")
}()

What Is NOT Covered

Scenario Status
Anonymous func literals as the callee Not supported in command syntax; the parenthesised form must be used.
Spread operator (fn args...) Out of scope for this proposal.
Chained calls (a.b.c arg) a.b.c is a valid selector expression, so this works as written.
defer inside a select or switch arm Behaves identically to the existing form; no special handling needed.

Disambiguation Rules

The parser applies the following precedence to avoid ambiguity:

  1. If the token after the function reference is (, parse as a standard call expression.
  2. If the token is , or a newline / } / ;, parse as a command expression (the arguments following , are collected greedily until end-of-statement).
  3. If neither applies (e.g., a binary operator follows), fall back to treating the entire expression as a standard call expression, which may produce a compile error if it is not a call.

Backwards Compatibility

All existing defer statements using parenthesised call syntax remain valid and unchanged. This proposal is purely additive.


Implementation Notes

  • The transform should happen in the parser (or an early AST rewrite pass) so that the rest of the compiler pipeline is unaffected.
  • Error messages from the type-checker will naturally reference the desugared form; a follow-up pass may map error positions back to the original source range for cleaner diagnostics.
  • The xgo formatter (xgofmt) should round-trip command-style defers without converting them to parenthesised form, preserving the author's intent. A --normalize flag could optionally canonicalize to one style.

Open Questions

  1. Should xgofmt prefer one style over the other? A project-level config option (similar to gofmt's -s) could allow teams to enforce a consistent style.
  2. Interaction with go generate / AST tooling — third-party tools that inspect xgo ASTs need to handle the new CommandDeferStmt node (or its desugared equivalent).
  3. Should other control keywords (go, return) adopt an analogous command syntax in a single unified proposal, or is defer best treated on its own?

Clone this wiki locally