-
Notifications
You must be signed in to change notification settings - Fork 566
Proposal: Function Decorators
This proposal introduces function decorators to XGo — a syntactic mechanism for wrapping functions and methods using higher-order helper functions. XGo decorators are resolved and expanded by the compiler, producing idiomatic Go code with no additional overhead from the decoration mechanism itself.
Many common cross-cutting concerns — retry logic, logging, timing, access control, transaction management — are currently expressed in XGo (and Go) through manual wrapping or code duplication. For example:
func Foo(arg1 ArgT1, arg2 ArgT2, ...) (ret1 RetT1, ret2 RetT2, ...) {
Retry(3, func() {
ret1, ret2, ... = _actualFoo(arg1, arg2, ...)
})
}This boilerplate is error-prone and obscures intent. A decorator syntax lets programmers express the wrapping declaratively, while the compiler performs the expansion transparently.
Decorators are especially valuable in classfile-based frameworks, where a framework author can provide decorator functions that end users apply to their domain methods — without requiring reflection, code generation tools, or runtime overhead.
A decorator is written as @name or @name(args...) immediately before a function or method declaration:
@decoratorName(arg1, arg2, ...)
func Foo(...) (...) { ... }
@decoratorName(arg1, arg2, ...)
func (recv *T) Bar(...) (...) { ... }
Multiple decorators may be stacked. They are applied innermost-first (bottom-up), consistent with Python's convention:
@decoratorA
@decoratorB(x)
func Foo(...) (...) { ... }
This is equivalent to applying @decoratorB(x) first, then @decoratorA around the result.
In XGo, function calls encourage lowercase-initial names as an alternative to uppercase. For example, Foo(arg1, arg2) and foo(arg1, arg2) are equivalent at the call site. This convention applies to decorators as well — @Retry(3) and @retry(3) are interchangeable. The examples in this proposal use lowercase decorator references.
XGo supports two forms of decorator function signature, distinguished by the type of the trailing closure parameter.
The last parameter is a func() closure. The compiler generates a closure that captures all named return variables of the decorated function or method, so the decorator does not need to know the return types.
// Zero-argument decorator
func Log(fn func()) { ... }
// Parameterized decorator
func Timeout(d time.Duration, fn func()) { ... }This form is simple and sufficient when the decorator only needs to control when or whether the function runs, without inspecting its return values.
The last parameter is a func() error closure. The compiler generates a closure that calls the original function or method, assigns its return values via named returns, and returns the last return value if it is of type error, or nil otherwise.
// Decorator that can inspect the error return value
func Retry(times int, fn func() error) { ... }This form allows the decorator to observe whether the wrapped call succeeded and act accordingly — for example, retrying on non-nil error. It does not require reflection.
The func() error closure generated by the compiler:
- Calls
_xgodeco_<FuncName>(or_xgodeco_<MethodName>for methods) and assigns all return values via named returns. - Returns the
error-typed return value if one exists in the decorated function's signature (the last return value of typeerroris used by convention). - Returns
nilif the decorated function has noerrorreturn value (making Form 2 a valid but redundant alternative to Form 1 in that case).
Given:
func Log(fn func()) { ... }
@log
func Foo(arg1 ArgT1, arg2 ArgT2, ...) (ret1 RetT1, ret2 RetT2, ...) {
// original body
}The compiler produces:
func _xgodeco_Foo(arg1 ArgT1, arg2 ArgT2, ...) (ret1 RetT1, ret2 RetT2, ...) {
// original body
}
func Foo(arg1 ArgT1, arg2 ArgT2, ...) (ret1 RetT1, ret2 RetT2, ...) {
Log(func() {
ret1, ret2, ... = _xgodeco_Foo(arg1, arg2, ...)
})
return
}For a method, the transformation is identical in structure — the receiver is preserved on both the renamed original and the wrapper:
func Log(fn func()) { ... }
@log
func (recv *T) Bar(arg1 ArgT1, ...) (ret1 RetT1, ...) {
// original body
}Produces:
func (recv *T) _xgodeco_Bar(arg1 ArgT1, ...) (ret1 RetT1, ...) {
// original body
}
func (recv *T) Bar(arg1 ArgT1, ...) (ret1 RetT1, ...) {
Log(func() {
ret1, ... = recv._xgodeco_Bar(arg1, ...)
})
return
}Key points:
- The original body moves to
_xgodeco_<Name>(function) or_xgodeco_<Name>method on the same receiver type. - The public wrapper uses named returns so the closure can assign them.
- If there are no return values, the closure body is a plain call with no assignment.
Given:
func Retry(times int, fn func() error) { ... }
@retry(3)
func Foo(arg1 ArgT1, arg2 ArgT2, ...) (ret1 RetT1, ret2 RetT2, ..., err error) {
// original body
}The compiler produces:
func _xgodeco_Foo(arg1 ArgT1, arg2 ArgT2, ...) (ret1 RetT1, ret2 RetT2, ..., err error) {
// original body
}
func Foo(arg1 ArgT1, arg2 ArgT2, ...) (ret1 RetT1, ret2 RetT2, ..., err error) {
Retry(3, func() error {
ret1, ret2, ..., err = _xgodeco_Foo(arg1, arg2, ...)
return err
})
return
}If the decorated function has no error return value, the generated closure returns nil:
func Foo(arg1 ArgT1, arg2 ArgT2, ...) (ret1 RetT1, ret2 RetT2, ...) {
Retry(3, func() error {
ret1, ret2, ... = _xgodeco_Foo(arg1, arg2, ...)
return nil
})
return
}The same pattern applies to methods, with the receiver carried through to the renamed method.
Stacked decorators always produce exactly two functions (or two methods): _xgodeco_<Name> and the public <Name>. The compiler selects the expansion form for each decorator independently based on its signature, and nests them bottom-up inline.
@log
@retry(3)
func Foo(arg1 ArgT1) (ret1 RetT1, err error) { ... }Here @retry uses Form 2 and @log uses Form 1. Expands to:
func _xgodeco_Foo(arg1 ArgT1) (ret1 RetT1, err error) { ... }
func Foo(arg1 ArgT1) (ret1 RetT1, err error) {
Log(func() {
Retry(3, func() error {
ret1, err = _xgodeco_Foo(arg1)
return err
})
})
return
}The compiler resolves decorator names using the following lookup order:
The compiler first looks for the decorator function in the same package as the decorated function or method. This covers the common case where decorators are defined alongside the code that uses them.
If the decorated function or method resides inside a classfile, the compiler additionally searches the class framework package that the classfile belongs to. This allows framework authors to provide a library of standard decorators that classfile users can apply without an explicit import.
This resolution order mirrors how classfile method lookup already works for other framework-provided identifiers.
If no matching decorator is found in either scope, a compiler error is reported.
For a decorator @name(a, b, ...) applied to a function or method, the compiler looks for a function named name (case-insensitive first letter, per XGo convention) and determines which form applies based on the type of the trailing parameter:
Form 1 is matched when the last parameter of name is func():
- The leading parameters of
namemust match the types of the arguments in@name(a, b, ...). - The compiler generates a
func()closure capturing all named return variables.
Form 2 is matched when the last parameter of name is func() error:
- The leading parameters of
namemust match the types of the arguments in@name(a, b, ...). - The compiler generates a
func() errorclosure that assigns all return values and returns theerror-typed return value (ornilif none).
If neither form matches, or no function named name is found in the resolution scope, a compiler error is reported.
Retry needs to inspect the error return value to decide whether to retry. It uses Form 2:
// Framework or package definition
func Retry(times int, fn func() error) {
for i := 0; i < times; i++ {
if fn() == nil {
return
}
}
}
// User code — plain function
@retry(3)
func FetchData(url string) (data []byte, err error) {
data, err = http.Get(url)
return
}Compiled output:
func _xgodeco_FetchData(url string) (data []byte, err error) {
data, err = http.Get(url)
return
}
func FetchData(url string) (data []byte, err error) {
Retry(3, func() error {
data, err = _xgodeco_FetchData(url)
return err
})
return
}Log does not need to inspect return values. It uses Form 1:
func Log(fn func()) {
log.Println("entering")
fn()
log.Println("exiting")
}
@log
func Process(input string) (result string) {
result = strings.ToUpper(input)
return
}Compiled output:
func _xgodeco_Process(input string) (result string) {
result = strings.ToUpper(input)
return
}
func Process(input string) (result string) {
Log(func() {
result = _xgodeco_Process(input)
})
return
}Decorators apply to methods in exactly the same way. The receiver is preserved on both the renamed original and the wrapper:
@retry(3)
func (s *Service) Call(req Request) (resp Response, err error) {
resp, err = s.client.Do(req)
return
}Compiled output:
func (s *Service) _xgodeco_Call(req Request) (resp Response, err error) {
resp, err = s.client.Do(req)
return
}
func (s *Service) Call(req Request) (resp Response, err error) {
Retry(3, func() error {
resp, err = s._xgodeco_Call(req)
return err
})
return
}In a classfile-based game engine, the framework package might provide:
// In the framework package (e.g., spx)
func OnMainThread(fn func()) {
mainThreadQueue <- fn
}Users can then write in their classfile:
@onMainThread
func UpdateScore(delta int) {
score += delta
}Without needing to import or qualify OnMainThread explicitly.
A decorator is represented in the XGo AST as a list of DecoratorSpec nodes attached to FuncDecl:
type DecoratorSpec struct {
At token.Pos // position of '@'
Name *Ident // decorator function name
Args []Expr // arguments (may be empty)
}
type FuncDecl struct {
// ... existing fields ...
Decorators []*DecoratorSpec // nil if no decorators; ordered top-to-bottom
}Decorators are stored in source order (top to bottom). The compiler applies them bottom-up during transformation.
The following restrictions apply in this initial proposal:
-
Variadic functions and methods. Decorated functions or methods with variadic parameters (
...T) are supported; the compiler passes the slice directly in the generated closure rather than re-spreading it:// For: func Foo(args ...int) { ... } func Foo(args ...int) { Decorator(func() { _xgodeco_Foo(args...) }) }
- No generic functions or methods. Decorating generic (type-parameterized) functions or methods is not supported in this version and results in a compiler error. Generic decorator support requires further design.
- Decorator arguments must be constants or package-level expressions. Decorator arguments are evaluated at the call site in the generated wrapper. They may not reference local variables of the decorated function.
- No decorator lookup across arbitrary packages. Decorator lookup is limited to the current package and the classfile's framework package. Decorators from other imported packages are not in scope unless explicitly aliased into the current package.
- Dynamic decoration. XGo decorators are a compiler rewrite, not a runtime protocol. There is no reflection-based or dynamic decorator mechanism.
- Class/struct decorators. Decorating type declarations is not in scope.
- Decorator factories returning decorators. Chained decorator factories (a function that returns a decorator function) are not supported in this proposal.
| Aspect | Design |
|---|---|
| Syntax |
@name or @name(args...) before func or method |
| Applies to | Functions and methods (including classfile methods) |
| Application order | Bottom-up (innermost decorator applied first) |
| Expansion | Compiler renames original to _xgodeco_<Name>, generates wrapper |
| Decorator form 1 | Trailing fn func() — closure, no access to return values |
| Decorator form 2 | Trailing fn func() error — closure returning the error return value |
| Resolution scope | Current package, then classfile framework package |
| Generics | Not supported (this version) |
| Runtime overhead | None from decoration mechanism itself |
Function decorators give XGo a clean, expressive way to apply cross-cutting concerns while remaining fully transparent to the Go type system and toolchain. The generated code is idiomatic Go, inspectable, and debuggable without any special tooling.