Embed Google's V8 JavaScript engine in Go with a typed API and a packaged
Windows runtime. Applications install with go get and run without requiring
Rust, Visual Studio, a C/C++ compiler, or a separate V8 download.
- Real V8 — execute JavaScript and WebAssembly using the engine from Chrome.
- Go and JavaScript interop — expose Go callbacks to JavaScript and call JavaScript functions from Go.
- Zero-build application setup — the verified Windows amd64 runtime is included in the Go module and extracted automatically.
- Audited behavior — a pinned
rusty_v8oracle runs matching fixtures and benchmarks against the Go implementation. - Broad safe API coverage — isolates, contexts, handle scopes, scripts, callbacks, promises, modules, WebAssembly, snapshots, and Inspector.
The public API follows the safe executable surface of the pinned Rust
v8 crate while preserving V8's explicit
ownership, lifetime, and thread-affinity rules.
gov8 currently supports Windows amd64 only. It uses the same pinned MSVC
V8 artifact as the Rust reference; Windows arm64, MinGW, macOS, and Linux are
not supported.
Applications using gov8 need only:
- Go 1.24 or newer
The current engine is V8 15.2.124.1-rusty, from Rust v8 = 152.2.0.
Add the module, then run your program normally:
go get github.com/maclof/gov8@latest
go run .No Rust, Visual Studio, C compiler, PowerShell setup script, or runtime download is needed by applications. The module contains a gzip-compressed, pinned Windows amd64 shim. On first use it verifies and extracts that DLL to a content-addressed directory below the user's OS cache; later runs verify and reuse the same file. The module adds about 18 MB to a program binary and uses about 46 MB in the per-user cache.
GOV8_SHIM_DLL remains available as a trusted developer override. The file
must be a matching Windows amd64 shim with the module's exact ABI:
$env:GOV8_SHIM_DLL = 'C:\path\to\gov8\build\shim\gov8_shim.dll'
go run .Maintainers who need to rebuild the native shim or run the Rust oracle also need Rust 1.98, Visual Studio with the MSVC C++ x64 build tools, and PowerShell:
git clone https://github.com/maclof/gov8.git
Set-Location gov8
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\setup_windows.ps1
$env:GOV8_SHIM_DLL = (Resolve-Path build\shim\gov8_shim.dll)
go test ./...The setup script downloads or reuses the pinned inputs, verifies their SHA-256
digests, and atomically writes build\shim\gov8_shim.dll.
When intentionally updating the packaged shim after a source change, rebuild it and regenerate the deterministic gzip asset:
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\setup_windows.ps1
go run ./internal/cmd/package-shimAn isolate is the closest V8 equivalent to a standalone VM. A context supplies its global environment, and a scope owns temporary V8 values:
package main
import (
"fmt"
"log"
gov8 "github.com/maclof/gov8"
)
func run() error {
if err := gov8.Initialize(); err != nil {
return err
}
defer gov8.Shutdown()
iso, err := gov8.NewIsolate()
if err != nil {
return err
}
defer iso.Close()
defer gov8.ReleaseIsolateHostState(iso)
ctx, err := iso.NewContext()
if err != nil {
return err
}
defer ctx.Close()
scope, err := iso.NewScope()
if err != nil {
return err
}
defer scope.Close()
script, err := ctx.Compile(scope, `21 * 2`, nil)
if err != nil {
return err
}
defer script.Close()
result, err := script.Run(scope, nil)
if err != nil {
return err
}
n, ok, err := result.IntegerValue(ctx)
if err != nil {
return err
}
if !ok {
return fmt.Errorf("JavaScript result is not an integer")
}
fmt.Println(n) // 42
return nil
}
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}The same program lives in examples/basic/main.go.
Create a V8 function backed by a Go callback, then put it on the context's
global object. This snippet assumes the iso, ctx, and scope from the
previous example:
add, err := iso.NewFunction(scope, ctx,
func(cs *gov8.CallbackScope, args gov8.FunctionCallbackArguments, rv gov8.ReturnValue) {
if args.Length() < 2 {
return // an unset return slot becomes JavaScript undefined
}
a, err := args.Get(0)
if err != nil {
return
}
b, err := args.Get(1)
if err != nil {
return
}
av, aok, err := cs.IntegerValue(a)
if err != nil || !aok {
return
}
bv, bok, err := cs.IntegerValue(b)
if err != nil || !bok {
return
}
_ = rv.SetInt32(int32(av + bv))
}, nil)
if err != nil {
return err
}
global, err := ctx.GlobalObject(scope)
if err != nil {
return err
}
set, err := global.SetByName(scope, ctx, "add", add.Value)
if err != nil {
return err
}
if !set {
return fmt.Errorf("could not define global add")
}
script, err := ctx.Compile(scope, `add(20, 22)`, nil)
if err != nil {
return err
}
defer script.Close()
sum, err := script.Run(scope, nil)
if err != nil {
return err
}
sumText, err := sum.ToString(ctx)
if err != nil {
return err
}
fmt.Println(sumText) // 42Callback arguments, return values, and values created through CallbackScope
are borrowed views valid only during that callback. Do not retain them. To turn
a callback failure into a JavaScript exception, create an error with
cs.NewError and pass it to cs.ThrowException.
Evaluate a function, check its type, and call it with a receiver and arguments:
script, err := ctx.Compile(scope,
`(function (name) { return "Hello, " + name + "!" })`, nil)
if err != nil {
return err
}
defer script.Close()
value, err := script.Run(scope, nil)
if err != nil {
return err
}
fn, ok, err := gov8.AsFunction(value, ctx)
if err != nil {
return err
}
if !ok {
return fmt.Errorf("script did not return a function")
}
receiver, err := scope.Undefined()
if err != nil {
return err
}
name, err := scope.NewString("Go")
if err != nil {
return err
}
greeting, called, err := fn.Call(scope, receiver, name)
if err != nil {
return err
}
if !called {
return fmt.Errorf("JavaScript function threw")
}
text, err := greeting.ToString(ctx) // "Hello, Go!"
if err != nil {
return err
}
fmt.Println(text)Ordinary misuse, lifecycle failures, and uncaught JavaScript exceptions are
returned as Go errors. APIs such as Compile and Run take an optional
*gov8.TryCatch: pass nil when the error is enough, or pass a catcher when
you need the JavaScript exception or message.
tc, err := iso.NewTryCatch()
if err != nil {
return err
}
defer tc.Close()
script, err := ctx.Compile(scope, `throw new Error("boom")`, tc)
if err != nil {
return err
}
defer script.Close()
if _, err := script.Run(scope, tc); err != nil {
caught, catchErr := tc.HasCaught()
if catchErr != nil {
return catchErr
}
if caught {
message, catchErr := tc.ExceptionText(scope, ctx)
if catchErr != nil {
return catchErr
}
fmt.Println(message) // Error: boom
}
}V8's ownership rules are visible in the API:
- Call
Initializeonce before creating isolates, then callShutdownonly after every isolate is closed. NewIsolatelocks the creating goroutine to its OS thread. Use the isolate and close it from that same goroutine; a different isolate can run on its own goroutine.- Close resources in dependency order: scripts and scopes, contexts,
ReleaseIsolateHostState, isolate, then the global platform. - Scopes must nest. Closing a scope invalidates every local
Valuecreated in it. Use persistent handles such asGlobalwhen a value must survive a scope. - Never retain callback-borrowed arguments, return slots, or callback-scope values after the callback returns.
- Check every
error, and check accompanyingokbooleans where an operation can fail without a Go error or JavaScript can throw.
From the repository root:
go test ./... -count=1
go test -race ./... -count=1
go vet ./...
# One quick Go benchmark smoke run.
go test . -run '^$' -bench '^BenchmarkScriptRunPrecompiledWorkload$' -benchtime=1x
# Rust oracle and benchmark smoke run.
Push-Location rust-oracle
cargo test --locked
cargo bench --locked --bench script -- --test
Pop-LocationMatched benchmark reports and their environment metadata are kept in
rust-oracle/bench-results.
The current suite contains 526 normalized cross-language fixture checks: 525 are exact and 1 uses a documented safety normalization. The declaration ledger records 1,698 direct matches, 10 intentional Go-shape differences, no safe executable gaps, and 149 intentionally unexposed raw/borrowed/generic Rust shapes.
This does not mean every Rust ownership or generic type has a literal Go spelling, and it is not a performance-parity claim. The audited safe executable surface has feature and behavioral parity; intentional Go safety shapes and measured performance gaps are tracked openly. The raw CreateParams stack-limit pointer is omitted, and the oracle confirms pinned V8 overwrites it before JavaScript execution.
PARITY.md- feature coverage, behavior notes, and performance evidenceAPI_AUDIT.md- declaration-level API auditrust-oracle/README.md- pinned versions, oracle workflow, and reproducibility details
Contributions should include tests for success, failure, exception, lifetime, and concurrency behavior where applicable, plus matched benchmarks for hot paths.
gov8 is available under the MIT License. The packaged native shim also contains third-party software covered by the notices in THIRD_PARTY_NOTICES.md.