Interpolation
Interpolation embeds a Go value into the output using single braces: { expr }. The expression is evaluated at render time; the result is written to the output with the appropriate escaper applied automatically for the context it sits in (HTML text, attribute value, URL, etc.).
Basic interpolation
{ expr } is the core form. It works anywhere a text node can appear — between elements, inside text content, mixed with static text — and accepts any Go expression: a variable, a method call, an arithmetic expression, a type conversion.
package views
component Greeting(name string, count int) {
<p>Hello, { name }! You have { count } messages.</p>
}Renders:
<p>Hello, World! You have 3 messages.</p>The { name } and { count } expressions are evaluated against the component's params. String values are HTML-escaped (angle brackets, ampersands, and quotes are encoded); numeric values (int, float64, etc.) are converted to their decimal string representation without escaping, because digits and a decimal point carry no HTML-special meaning.
Note that { expr } is interpolation — it emits a value. It is not a Go statement block. To run a Go statement that produces no output, use {{ stmt }} (a GoBlock). See Raw Go for details.
Fields & typed values
You can interpolate any Go expression, including field accesses on a struct passed as a param. The type does not need to be a string: any type that satisfies fmt.Stringer is formatted via its String() method, and numeric primitives are formatted directly.
package views
type User struct {
Name string
Age int
}
component Profile(user User) {
<p>{ user.Name } is { user.Age }</p>
}Renders:
<p>Alice is 30</p>{ user.Name } and { user.Age } access fields on the User struct param directly. There is no special accessor syntax — it is ordinary Go field access inside braces.
Functions & (T, error) auto-unwrap
When an expression is a function call returning (T, error), the code generator automatically unwraps the tuple: it assigns the result to a temporary, checks the error, and if the error is non-nil, returns it from the enclosing Render call. No extra syntax is needed.
package views
func lookup(k string) (string, error) { return k, nil }
component Label(key string) {
<span>{ lookup(key) }</span>
}Renders:
<span>hello</span>lookup(key) returns (string, error). The generated code is equivalent to:
_v, _err := lookup(key)
if _err != nil {
return _err
}
// write _v to outputThe caller of Render receives the error and can handle it (log, serve a 500, etc.).
The auto-unwrap is uniform across all expression positions — not limited to text interpolation:
| Position | Example |
|---|---|
| Text interpolation | { f() } |
| Attribute value | title={ f() } |
| Child-component prop value | <Row label={lookup(k)}/> |
| Ordered-attrs pair value | <Card bag={{ "k": f(t) }}/> |
| Pipeline stage | each |> stage whose return is (T, error) |
| Children / slot body | <Wrap>{ f(t) }</Wrap> |
When a child-component prop value returns (T, error), gsx hoists the call to a temporary before building the component literal. When multiple props on the same component call each return a tuple, all are hoisted in source order:
package views
func lookup(k string) (string, error) { return "val-" + k, nil }
component Row(label string) {
<span>{ label }</span>
}
component Page(k string) {
<Row label={lookup(k)}/>
}Renders:
<span>val-item</span>Rules:
- Only
(T, error). Exactly two return values, with the second typederror. Any other multi-value shape —(int, string), three values, etc. — is a compile-time gsx error:only (T, error) is supported. - Automatic — no marker or opt-out. The unwrap is always applied; there is no annotation to add or remove.
?is rejected (a gsx error). A?try-marker suffix (e.g.upper?in a pipeline stage) is reported as not supported, because gsx already auto-unwraps(T, error)values.
Numeric & string contexts
The escaper applied to { expr } depends on where the interpolation appears, not on the type of the value:
- Text content (
<p>{ x }</p>) — HTML-escapes the string form ofx. - Attribute value (
title={ x }) — attribute-escapes the value. - URL attribute (
href={ x },src={ x },action={ x }, and htmx method attrshx-get/hx-post/hx-put/hx-delete/hx-patch) — scheme-sanitizes and escapes. URL attributes are the only ordinaryattr={ x }name-based special case; other attributes, includinghx-on*, are plain attribute text unless written with an explicit embedded-language literal. - Attribute-local JavaScript/CSS (
@click=js`save(@{x})`,style=css`color:@{x}`,style={ css`color:@{x}` }) — escapes each hole for its embedded JavaScript or CSS position. <script>body (@{ x }) — JSON-encodes the Go value to a safe JS literal.<style>body (@{ x }) — CSS value-filters the string.
Numeric values in text context ({ count } where count is int) are formatted as their decimal representation and do not require escaping — no HTML-special characters can appear in a plain integer string. This means numeric interpolation has no overhead from the escaper.
For a complete reference of escaping contexts and opt-out helpers (gsx.Raw, gsx.RawURL, gsx.RawJS, gsx.RawCSS), see Escaping.
Markup or Go in braces
In attribute-value position (name={…}), {…} can hold either a Go expression or markup. gsx resolves the ambiguity positionally — the Babel rule: if the first non-space character after { is < followed by a tag-name character, the content is parsed as markup; otherwise it is a Go expression. So header={ <h1>Title</h1> } is a markup-valued attribute (see Composition — named slots), while disabled={ a < b } is a boolean expression where < is the less-than operator.
In body and text context the ambiguity does not arise: markup is written as bare elements (<span>…</span>), and {…} holds interpolation, a GoBlock ({{ }}), or a control-flow construct ({ if … }, { for … }, { switch … }) — the latter dispatched by keyword, not by <.
For parser corner cases, see the parser/ corpus.