Skip to content

Control flow

Use Go control flow inside { … } to decide which markup renders. Conditions, variables, and scopes follow normal Go rules.

If / else

An if block renders its markup when the condition is true. Add else for the alternative; without it, a false condition renders nothing.

gsx
package views

component Inbox(name string, count int) {
	<section>
		<h1>Hi { name }</h1>
		{ if count > 0 {
			<p class="badge">{ count } new</p>
		} else {
			<p>all caught up</p>
		} }
	</section>
}

Renders:

html
<section><h1>Hi World</h1><p class="badge">2 new</p></section>

▶ Open in Playground

Range

A for … range block renders its markup once for each value. The loop variables are in scope inside the block.

gsx
package views

type Item struct {
	Name  string
	Count int
}

component List(items []Item) {
	<ul>
		{ for _, it := range items {
			<li>{ it.Name }: { it.Count }</li>
		} }
	</ul>
}

Renders:

html
<ul><li>alpha: 1</li><li>beta: 2</li></ul>

▶ Open in Playground

Any value supported by Go's range can be used here.

Switch

A switch block renders the matching case, or default when no case matches.

gsx
package views

component Badge(kind string) {
	<span>
		{ switch kind {
		case "warn":
			<b>warning</b>
		case "err":
			<b>error</b>
		default:
			<b>info</b>
		} }
	</span>
}

Renders:

html
<span><b>warning</b></span>

▶ Open in Playground

Cases use normal Go syntax, including comma-separated values.

Text at the start of its own line inside a case body is read as the next arm's label if it looks like one (case <expr>: or default:, colon on the same line) — mid-line text is unaffected, and a line-leading case 1: compiles silently as a new arm, so wrap such text in an interpolation (e.g. {"default: 5"}) or an element to render it literally. A label whose case list spans lines is only recognized when it follows markup, not text — keep arm labels on one line.

Init statements

An if can declare values before its condition. Those values remain in scope through every branch, which is useful for checking a result and its error together.

gsx
package views

func loadUser(id string) (string, error) { return "User:" + id, nil }

component Profile(id string) {
	<div>
		{ if name, err := loadUser(id); err != nil {
			<span class="err">{ err.Error() }</span>
		} else {
			<span>{ name }</span>
		} }
	</div>
}

Renders:

html
<div><span>User:42</span></div>

▶ Open in Playground

The same form works for map lookups, type assertions, and other expressions that return multiple values.

Whitespace in control-flow bodies

Whitespace inside { if }, { for }, and { switch } bodies follows the same whitespace rule as element bodies: whitespace between content is preserved (a run with a newline collapses away; an inline run collapses to one space). Whitespace immediately inside the control-flow braces is ignored, like the interior of { expr }. To keep a separator space next to a conditional, put it in the surrounding markup rather than at the block's inner edge:

gsx
<title>{ if !isProd { {env} - } } {page} - One Learning</title>