@gosfc/astro
| Install | |
|---|---|
npm install @gosfc/astro@0.0.0 |
|
| Latest Version: | 0.0.0 |
| Vue: | 3 |
| License: | MIT |
| Last Updated: | Oct 21, 2026 |
| Links: | GitHub · npm |
gosfc
English | 日本語
An integration layer for using real Go in the <script setup> of Vue Single File Components, and in the frontmatter and <script> of .astro files.
<template>
<div>
Total: {{ total }}
</div>
</template>
<script setup lang="go">
import cart "example.com/app/src/features/cart/pkg"
items := []cart.Item{
{
Price: 100,
Quantity: 2,
},
}
total := cart.Total(items)
</script>
.go files are ordinary Go packages, and imports are ordinary Go imports. Go compilation is handled by goesm, SFCs and templates by the Vue tooling, .astro templates by Astro, builds by Vite, and pages and SSR by Astro. See ARCHITECTURE.md for the design.
Status: PoC. What is not implemented yet is listed in ARCHITECTURE.md, section 11 ("Not yet implemented / open questions").
Usage (Astro)
-
Add goesm and gosfc to your Go module as tools. Their versions are pinned by go.mod / go.sum. gosfc needs goesm v0.0.1-beta.3 or later.
go get -tool github.com/goesm-dev/goesm/cmd/goesm@<version> go get -tool github.com/goesm-dev/gosfc/cmd/gosfc@<version> -
Install the integration from npm (
bun add @gosfc/astro,pnpm add @gosfc/astroornpm install @gosfc/astro). For plain Vite + Vue projects, install@gosfc/viteinstead. -
Add the integration to Astro. When the project has
@astrojs/vueinstalled, it is added if it is not already there. A project with Go only in.astrofiles needs neither Vue nor@astrojs/vue; examples/astro-go is one.// astro.config.mjs import { defineConfig } from "astro/config"; import gosfc from "@gosfc/astro"; export default defineConfig({ integrations: [gosfc()], }); -
Use
<script setup lang="go">in your.vuefiles.--- import Summary from "../features/cart/Summary.vue"; --- <Summary />An
.astrofile can also be written in Go: see Go in .astro files.
To use gosfc with Vite alone, put @gosfc/vite before @vitejs/plugin-vue.
import vue from "@vitejs/plugin-vue";
import gosfc from "@gosfc/vite";
export default { plugins: [gosfc(), vue()] };
Components using lang="ts" or a plain <script setup> keep working alongside them. gosfc only touches components with lang="go".
Writing the Go block
-
Like Vue's
<script setup>, the top level runs once, top to bottom, per component instance. You can writex := ...,var,const,type, andfunc F() {...}. -
Top-level variables, constants, and functions are available in the template. When the template calls a Go function (for example
@click="Increment"), the rendered output follows the Go values. Values and the arguments and results of calls are converted as for goesm's exported functions: strings, slices as arrays, structs as plain objects. -
importtakes Go packages only; the Go frontmatter of an.astrofile can also import JavaScript modules, as described below. In a Vue component, Vue components, TypeScript and JavaScript come in through goesm's//goesm:importdirective, before avar(a component, a class, any value) or a function without a body (a function, called with automatic conversion of its arguments and result). Both are template bindings like the block's other names. Their types may use imported and built-in types, not types declared in the block, since they are declared at package level.<template> <p>{{ formatPrice(12800) }} <Badge label="new" /></p> </template> <script setup lang="go"> import "syscall/js" //goesm:import "./Badge.vue" var Badge js.Value //goesm:import "./format.ts" formatPrice func formatPrice(yen int) string </script> -
Put methods and generic functions in a Go package.
-
To receive props, declare
type Props struct {...}; the block then has apropsvariable of that type. FieldRouteis read from the attributeroute(or the field's json tag name, or its kebab-case form), converted to the field's type: strings, bools and numbers, and structs, slices, pointers (nullis nil) and functions as goesm's ABI converts them. A field'sdefault:"..."tag fills a prop the parent leaves out. When the parent passes new props,propsis replaced and the template reads it again ({{ props.Title }}). The fields are declared as Vue props, so other attributes (class,style, listeners) fall through to the root element as usual.<script setup lang="go"> import "strings" type Props struct { Title string } heading := strings.ToUpper(props.Title) </script>
Reactivity: the vue package
Go variables are invisible to Vue: the template reads them again only after one of its own Go calls (an event handler). State that changes on its own (a timer, a goroutine, another component) goes in a vue.Ref of the package github.com/goesm-dev/gosfc/vue:
<template>
<p>{{ seconds }} s ({{ Label() }})</p>
<input v-model="name" />
<canvas ref="canvas"></canvas>
</template>
<script setup lang="go">
import (
"syscall/js"
"time"
"github.com/goesm-dev/gosfc/vue"
)
seconds := vue.NewRef(0)
name := vue.NewRef("")
canvas := vue.NewRef(js.Null())
emit := vue.Emitter()
func Label() string { return "hello " + name.Get() }
vue.OnMounted(func() {
go func() {
for range time.Tick(time.Second) {
seconds.Set(seconds.Get() + 1)
}
}()
})
vue.Watch(func() int { return seconds.Get() }, func(n int) { emit("tick", n) })
</script>
- A Ref bound in the template reads as its value and is followed whoever sets it, from any package: Refs can live in a Go package and be shared by components. The Go of all the components (and the packages JavaScript imports with
go:) is one goesm program, as the packages of a Go binary are, so a package's variables are one set for the whole app.v-modelandref="..."write it withSet. - A Go function called from the template is re-run when a Ref it read changes.
vue.NewComputedderives a cached value;vue.Watchandvue.WatchEffectrun side effects, and also re-run when the component's props change.vue.OnMounted,OnBeforeUnmount,OnUnmountedandvue.Emitterbelong to the component being set up: call them from the top level of the block.emit("update:modelValue", v)with aModelValueprop makes the component work withv-model.- Outside a goesm build (
go test), Refs and Computeds hold and compute values, and watchers and hooks never run. - gosfc needs goesm v0.0.1-beta.6 or newer: handles inside an
anyhave their methods, several packages build as one program, and a component's function variables (update := func() {...}) do not make its setup async because some other function value of the app blocks.
Go in .astro files
A frontmatter fence that opens with ---go instead of --- holds Go. It follows the rules of a Go block of a .vue file: the top level runs once per render (at build time, or per request with SSR), and its top-level variables, constants and functions are available in the template.
---go
import (
"strconv"
cart "example.com/app/src/features/cart/pkg"
Line "../features/cart/Line.astro"
Summary "../features/cart/Summary.vue"
)
items := []cart.Item{
{Price: 120, Quantity: 3},
{Price: 80, Quantity: 1},
}
total := cart.Total(items)
func Yen(n int) string {
return "¥" + strconv.Itoa(n)
}
---
<Line label="Apples" price={120} quantity={3} />
<p>Total: {Yen(total)} ({items.length} items)</p>
<Summary />
- JavaScript modules (components, styles, npm packages) are imported with Go import syntax.
import Card "./Card.vue"meansimport Card from "./Card.vue", andimport _ "./global.css"is a side-effect import. An import path is JavaScript when it starts with./,../,/or@, or contains:(such asastro:assets); any other path is a Go package. type Props struct {...}receivesAstro.props, with the same field names as the props of a.vueblock (Labelis read fromlabel).- Values reach the template converted for JavaScript (strings, slices as arrays, structs as objects), and the template can call Go functions.
- If the Go code blocks (channels,
time.Sleep, ...), the frontmatter waits for it.
A <script lang="go"> in the template is Go that runs in the browser once per page, like Astro's processed <script>: Astro bundles it, and a component used several times includes it once. The template does not see its top-level names; use syscall/js to work with the DOM.
<button id="counter">0</button>
<script lang="go">
import (
"strconv"
"syscall/js"
)
count := 0
button := js.Global().Get("document").Call("getElementById", "counter")
button.Call("addEventListener", "click", js.FuncOf(func(this js.Value, args []js.Value) any {
count++
button.Set("textContent", strconv.Itoa(count))
return nil
}))
</script>
Limitations:
- A Go frontmatter cannot export anything, so
getStaticPathsand other exports need a TypeScript frontmatter. That frontmatter can still import Go with ago:specifier (next section). - Go sees nothing of the
Astroglobal but the props. ForAstro.url,Astro.cookies, redirects and so on, use a TypeScript frontmatter. - Go import syntax can write a default import or a side-effect import only. To use named exports of a JavaScript module, re-export them as the default export of a small module of your own and import that.
<script lang="go">takes no other attributes (is:inline,define:vars, ...).
Importing Go from JavaScript
A .js, .ts or .astro module inside a Go module can import a Go package directly with a go: specifier. gosfc compiles the package with goesm in the Go module of the importing file, and Vite bundles it like any other module. The API is goesm's JS calling ABI: the exported functions take and return plain JavaScript values (strings, arrays, objects).
---
// src/pages/[slug].astro
import { Slugs } from "go:example.com/app/content";
export function getStaticPaths() {
return Slugs().map((slug) => ({ params: { slug } }));
}
---
TypeScript sees the types of these imports. With goesm v0.0.1-beta.8 or later, gosfc declares each package JavaScript imports with go: as a module, from the declaration file of its exports that goesm writes (goesm emit-ts -dts):
declare module "go:example.com/app/content" {
export function Slugs(): Array<string>;
}
In Astro, @gosfc/astro writes the declarations to .astro/integrations/_gosfc_astro/go-imports.d.ts, which .astro/types.d.ts references, so astro check and editors see them. astro sync, and so astro check, compiles the Go to write them; astro dev and astro build rewrite them when they compile. With Vite alone, @gosfc/vite writes them to gosfc.d.ts in Vite's root, which the include of the project's tsconfig.json must cover; gosfc({ dts: "src/gosfc.d.ts" }) picks another file and gosfc({ dts: false }) none. Until the Go has been compiled once, the file declares every go: import as any.
Benchmark
The same components written with <script setup lang="go"> and with <script setup lang="ts"> (bench/), built with Vite 8 and @vitejs/plugin-vue. The Go side adds @gosfc/vite in front; nothing else differs.
gosfc (lang="go") |
Vue (lang="ts") |
ratio | |
|---|---|---|---|
| Client build time | 196 ms | 130 ms | 1.51x |
| Client JS (minified) | 68.2 KiB | 59.8 KiB | 1.14x |
| Client JS (gzip) | 26.4 KiB | 23.3 KiB | 1.13x |
| SSR render, small component | 13.8 µs | 12.9 µs | 1.07x |
| SSR render, 1,000,000 items | 104.9 ms | 95.8 ms | 1.09x |
- Client build time:
vite buildof a page with a counter and a cart summary. Median of 9 builds, alternating which side builds first, after one warm-up build per side, so the goesm binary and the go command's build cache are warm, as in an edit-and-rebuild loop. - Client JS: every JS file of that build, Vue runtime included. The difference (+8.4 KiB, +3.1 KiB gzip) is goesm's runtime and the code that keeps Go semantics.
- SSR render:
renderToStringwith the production SSR build, both sides rendering the same HTML. Each side and component is measured in its own fresh Node process, 5 times with the order alternating, and the table shows the median. "small component" is a cart summary of 3 items; "1,000,000 items" builds and sums 1,000,000 items in the component's setup, which on both sides is mostly allocation. The small gap there is presumably the code goesm generates to keep Go semantics (for example,rangecopies each struct value); it has not been profiled.
Measured with pnpm bench on 2026-10-04 with the mise.toml versions (Node.js 26.10.0, Go 1.27.1) and goesm 20dbf1d, on a 4 vCPU Intel Xeon 2.80GHz cloud VM. Sizes are exact; timings move between runs on that machine (over 6 runs the ratios ranged from 1.51x to 1.73x for the build, 1.02x to 1.26x for the small component and 1.09x to 1.20x for 1,000,000 items).
Development
Go, Node.js, and pnpm are pinned to the versions in mise.toml with mise.
mise install
pnpm install
pnpm test # go test ./... and tests/*.test.mjs
pnpm bench # bench/run.mjs: prints the Benchmark table, rewrites bench/results-*.svg
cd examples/astro && pnpm build # dist/index.html contains 「合計: 200」, dist/go/index.html is src/pages/go.astro
cd examples/astro && pnpm dev
The browser HMR test uses Chromium at /opt/pw-browsers/chromium (or the CHROMIUM environment variable) and is skipped if it is not found.
Contributing / License
See CONTRIBUTING.md for how to contribute and for the design principles. gosfc is released under the MIT License.
Version History
| Version | Released | License |
|---|---|---|
| 0.0.0 | MIT |
Pre-releases (7)
| Version | Released | License |
|---|---|---|
| 0.0.1-beta.5 | MIT | |
| 0.0.1-beta.4 | MIT | |
| 0.0.1-beta.3 | MIT | |
| 0.0.1-beta.2 | MIT | |
| 0.0.1-beta.1 | MIT | |
| 0.0.1-beta.0 | MIT | |
| 0.0.0-stage |