A fork of Go with compile-time execution, macros, and hot reload.

forgo is a fork of Go, currently synced to the Go 1.27 release branch, that adds Nim-style compile-time execution and AST macros, plus Rust-style ? error-propagation, a JavaScript-style throw for failing a function out with a new error, a Ruby/Perl-style postfix if for one-line guard clauses, SIMD enabled by default, and Dart/Flutter-style hot reload for a running program.

Install forgo What’s new

Fork of Go 1.27 Sources: .fgo and .go, mixed freely linux · macOS · windows Install: one line


//fgo:comptime functions01

//fgo:macro — AST macros02

? — error propagation03

throw — fail out with a new error04

Postfix if — one-line guard clauses05

comptime/embed and comptime/json06

Non-scalar consts07

forgo run --watch — hot reload08


Installing a prebuilt release

Install

# Linux / macOS
curl -fsSL https://github.com/lsegal/forgo/releases/latest/download/install.sh | sh
# Windows
irm https://github.com/lsegal/forgo/releases/latest/download/install.ps1 | iex

Both scripts install the latest GitHub release to ~/.forgo by default. After installing, point GOROOT at the install directory and add its bin/ to PATH — the script prints the exact commands — then run forgo (e.g. forgo build, forgo run).

Once installed, forgo upgrade (alias forgo update) re-runs the same install script over the current installation. The release tarball also includes forgopls in bin/, for the VS Code extension.

What’s new

Forgo additions, on top of the Go 1.27 toolchain

Files using forgo-specific syntax use the .fgo extension instead of .go. The toolchain treats the two identically everywhere — forgo build, forgo run, forgo test, module resolution — so a package can freely mix both. Each feature below links to its full rules in the docs.

01

//fgo:comptime functions

A function marked //fgo:comptime is ordinary Go: it type-checks, compiles, and can be called at runtime like any other function. When it is invoked directly as the initializer of a const declaration with constant-foldable arguments, the compiler evaluates it during type-checking and folds the result into a real Go constant — usable anywhere the language requires a constant expression.

What’s supported inside a comptime body →

//fgo:comptime
func calculateFactorial(n int) int {
	result := 1
	for i := 1; i <= n; i++ {
		result *= i
	}
	return result
}

// Evaluated entirely by the compiler.
const factFive = calculateFactorial(5)
02

//fgo:macro — AST macros

A function marked //fgo:macro receives the unevaluated syntax tree of each of its call-site arguments and returns a syntax tree that is spliced into the caller’s code in place of the macro call, before type checking runs. Quote captures a template without evaluating it; Splice marks where a bound tree is substituted in.

Quote, Splice, and the v1 limits →

//fgo:macro
func double(x Node) Node {
	return Quote(func() {
		Splice(x) + Splice(x)
	})
}

double(compute()) // expands to: compute() + compute()
03

? — error propagation

expr? evaluates expr, and if it produced a non-nil error, returns immediately from the enclosing function with that error; otherwise it evaluates to the non-error value, so it chains. It requires a function whose last result is a named error.

Chaining, loop headers, and caveats →

func loadConfig(path string) (name string, err error) {
	f := open(path)?      // returns early if open fails
	cfg := parse(f)?.normalize()?
	name = cfg.Name
	return
}
04

throw — fail a function out with a new error

throw EXPR returns immediately from the enclosing function, passing EXPR through as the last (error) result and a zero value for every other result. A bare string literal is sugar for errors.New. It is a contextual keyword, so Go code that uses throw as an identifier is unaffected.

Zero-value rules and the errors import →

func makeThing(s string) (*Thing, error) {
	if s == "" {
		throw "empty"   // = throw errors.New("empty")
	}
	return &Thing{name: s}, nil
}
05

Postfix if — one-line guard clauses

STMT if COND is shorthand for if COND { STMT }. It works as a modifier on any statement kind that doesn’t introduce a new binding into the surrounding scope: an expression statement, a channel send, a plain assignment, return, throw, and break/continue/goto.

What it deliberately excludes →

func check(s string) (n int, err error) {
	throw "empty" if s == ""
	return len(s), nil
}

continue if i%2 == 0
total += i if want(i)
06

comptime/embed and comptime/json

Two stdlib packages the compiler executes natively. comptime/embed reads files and inspects the filesystem at compile time — ReadFile, ReadDir, Exists, and Load, which embeds a whole file tree as an io/fs.FS. comptime/json marshals and unmarshals JSON there.

Full package reference →

import (
	"comptime/embed"
	"comptime/json"
)

const banner = embed.ReadFile("banner.txt")
const content = embed.Load("assets")
const schema = json.Unmarshal[Schema](
	embed.ReadFile("schema.json"))
07

Non-scalar consts

Ordinary Go restricts const to bool, numeric, and string values. forgo also allows a struct or map (an ordered field-name-to-value mapping) and a slice or array (an ordered element list) to be a real constant. Field and index access on one is itself constant, so it composes with everything a scalar const already does.

Current limits →

type Schema struct {
	Name string
	Port int
	Tags []string
}

const schema = json.Unmarshal[Schema](embed.ReadFile("schema.json"))

const name = schema.Name
var proof [schema.Port % 16]byte // an array length, like any const
08

forgo run --watch — hot reload

Edit a function, save, and the running program picks up the new code without restarting. Its goroutines keep running, its heap is untouched, and every package-level variable keeps the value it had — counters keep counting, caches stay warm, open files and connections stay open. This is the same thing Dart and Flutter do, done for an ahead-of-time compiled language.

Watch mode compiles with inlining disabled and runs on linux, macOS, and windows. A change to a function that is already running and never returns does not take effect, and a change to a struct’s layout, a function’s signature, or a package’s initialization is refused rather than applied.

How it works, what needs a restart, and the limits →

$ forgo run --watch ./examples/hotreload
pid 44081 — edit render() in examples/hotreload/main.fgo and save
tick 1
tick 2
...                       # edit render(), save
forgo: reloaded 1 function in 11.9s
GEN1 tick 104 (worker beat 51, 104 in history, up 51s)
GEN1 tick 105

Note the tick count and the uptime: same process, new code.

Editors

forgopls and the VS Code extension

The extension adds .fgo syntax highlighting plus a language client backed by forgopls, a build of gopls that understands ? well enough to type-check it correctly. //fgo:comptime and //fgo:macro are still compiler-only and show as false-positive diagnostics.

Coding agents

AGENTS.md

Using a coding agent on a forgo codebase? Point it at AGENTS.md. It tells the agent when to reach for ?, throw, postfix if, //fgo:comptime, and //fgo:macro instead of plain-Go idioms — including the rule that a forgo codebase should never hand-write return ..., err just to propagate or introduce an error.

Compatibility

Still a Go distribution

The repo root is a Go distribution checkout and builds the way upstream Go does. Plain .go files compile as before, the standard library is Go 1.27’s, and forgo carries its own version — tracked in FORGO_VERSION — independent of the release it is synced against.

Install the toolchain, or read the reference

Install forgo Read the docs GitHub