-
Notifications
You must be signed in to change notification settings - Fork 566
Proposal: Command Syntax Support for `defer`
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, arg2xgo 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:
- Consistency. Users who adopt the command style for regular calls no longer need to switch mental models when writing a deferred call.
-
Reduced noise. The extra pair of parentheses adds punctuation without adding information, especially for single-argument defers like
defer close fordefer log "done". -
Scripting friendliness. xgo is frequently used in script-like programs where brevity matters; bringing
deferin line with that style lowers friction.
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.
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 calls are handled naturally because the transform is purely syntactic:
defer fmt.Println "hello", "world"
──► defer fmt.Println("hello", "world")
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)
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().
// 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")
}()| 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. |
The parser applies the following precedence to avoid ambiguity:
- If the token after the function reference is
(, parse as a standard call expression. - If the token is
,or a newline /}/;, parse as a command expression (the arguments following,are collected greedily until end-of-statement). - 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.
All existing defer statements using parenthesised call syntax remain valid and unchanged. This proposal is purely additive.
- 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--normalizeflag could optionally canonicalize to one style.
-
Should
xgofmtprefer one style over the other? A project-level config option (similar togofmt's-s) could allow teams to enforce a consistent style. -
Interaction with
go generate/ AST tooling — third-party tools that inspect xgo ASTs need to handle the newCommandDeferStmtnode (or its desugared equivalent). - Should other control keywords (
go,return) adopt an analogous command syntax in a single unified proposal, or isdeferbest treated on its own?