github.com/botify-labs/v8go is Botify's fork of v8go, the Go binding to the
V8 JavaScript engine. It is a snapshot of
tommie/v8go (commit in deps/tommie_sha), itself a fork of
rogchap/v8go, with V8 15.4.80.20, renamed to the module path
github.com/botify-labs/v8go, plus the Botify additions below. It replaces the older Botify fork of
rogchap.com/v8go (V8 9.0): see MIGRATION.md.
go get github.com/botify-labs/v8go@v0.15.4V8 and v8go's C++ bridge ship prebuilt in the deps/<os>_<arch> modules and link statically.
Consumers compile no C++ for v8go.
| Linux, macOS | Windows | |
|---|---|---|
| Go | the version in go.mod |
≥ 1.27 |
| C compiler | any (system gcc or clang), no flags, no CGO_CXXFLAGS |
MSVC-target clang (LLVM ≥ 21), with MSVC Build Tools and the Windows SDK |
| Environment | nothing | CC="clang -fuse-ld=lld", CXX="clang++ -fuse-ld=lld", clang on PATH |
| C runtime | system libc | static MSVC CRT (/MT); MinGW is not supported |
Windows notes: -fuse-ld=lld must be in CC (or passed with -ldflags=-extldflags=-fuse-ld=lld)
for Go to detect LLD, otherwise it passes flags only GNU ld accepts. Go splits CC on spaces, so
clang must be found on PATH rather than given as a full path. Every other cgo dependency of the
binary must be MSVC-compatible too: see CGO-DEPENDENCIES.md.
linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, windows/amd64.
- Prebuilt C++ bridge: the
.ccfiles only compile with-tags v8go_source; consumers linkdeps/<os>_<arch>/libv8go.a(v8go.libon Windows), checked against the sources by CI. Isolate.Cleanup/Context.Cleanup: release the values and scripts a long-lived isolate or context tracks, and run V8's pending GC tasks (memory reducer). Cleanup never runs JavaScript or Go callbacks, and does nothing when called with JavaScript on the stack.SetDefaultLocale: pins the default locale of V8's ICU, process-wide, so thatIntland the locale-sensitive methods don't depend on the host's locale.- Faster JS→Go calls: indexed value tracking, the context found without a call into Go, one allocation per call up to 4 arguments, plus guards for callbacks of closed contexts and terminated nested scripts.
- C++ runtime isolation on Linux: V8's copy of Chromium's libc++abi/libc++ ABI layer is renamed
(
.v8crsuffix) in the Linux archives, so binaries also linking g++/libstdc++ C++ work, fully static included. - No allocator shim: PartitionAlloc's malloc replacement is removed from V8's archives; the
process keeps the system
malloc.
- MIGRATION.md: migrating from the old fork (
rogchap.com/v8go, V8 9.0): toolchains, glibc, memory, behaviour and API changes (in French). - CGO-DEPENDENCIES.md: other cgo libraries next to V8 (static linking, MSVC rebuilds, linux/arm64 archives), and checks for new ones (in French).
- BOTIFY.md: how the fork works: prebuilt bridges, pinned
deps/*modules, release procedure, V8 upgrades, benchmarks (in French). - CHANGELOG.md: Botify releases, then tommie/v8go's history.
- Go reference: https://pkg.go.dev/github.com/botify-labs/v8go
import v8 "github.com/botify-labs/v8go"ctx := v8.NewContext() // creates a new V8 context with a new Isolate aka VM
ctx.RunScript("const add = (a, b) => a + b", "math.js") // executes a script on the global context
ctx.RunScript("const result = add(3, 4)", "main.js") // any functions previously added to the context can be called
val, _ := ctx.RunScript("result", "value.js") // return a value in JavaScript back to Go
fmt.Printf("addition result: %s", val)iso := v8.NewIsolate() // creates a new JavaScript VM
ctx1 := v8.NewContext(iso) // new context within the VM
ctx1.RunScript("const multiply = (a, b) => a * b", "math.js")
ctx2 := v8.NewContext(iso) // another context on the same VM
if _, err := ctx2.RunScript("multiply(3, 4)", "main.js"); err != nil {
// this will error as multiply is not defined in this context
}iso := v8.NewIsolate() // create a new VM
// a template that represents a JS function
printfn := v8.NewFunctionTemplate(iso, func(info *v8.FunctionCallbackInfo) *v8.Value {
fmt.Printf("%v", info.Args()) // when the JS function is called this Go callback will execute
return nil // you can return a value back to the JS caller if required
})
global := v8.NewObjectTemplate(iso) // a template that represents a JS Object
global.Set("print", printfn) // sets the "print" property of the Object to our function
ctx := v8.NewContext(iso, global) // new Context with the global Object set to our object template
ctx.RunScript("print('foo')", "print.js") // will execute the Go callback with a single argunent 'foo'ctx := v8.NewContext() // new context with a default VM
obj := ctx.Global() // get the global object from the context
obj.Set("version", "v1.0.0") // set the property "version" on the object
val, _ := ctx.RunScript("version", "version.js") // global object will have the property set within the JS VM
fmt.Printf("version: %s", val)
if obj.Has("version") { // check if a property exists on the object
obj.Delete("version") // remove the property from the object
}val, err := ctx.RunScript(src, filename)
if err != nil {
e := err.(*v8.JSError) // JavaScript errors will be returned as the JSError struct
fmt.Println(e.Message) // the message of the exception thrown
fmt.Println(e.Location) // the filename, line number and the column where the error occurred
fmt.Println(e.StackTrace) // the full stack trace of the error, if available
fmt.Printf("javascript error: %v", e) // will format the standard error message
fmt.Printf("javascript stack trace: %+v", e) // will format the full error stack trace
}For scripts that are large or are repeatedly run in different contexts, it is beneficial to compile the script once and used the cached data from that compilation to avoid recompiling every time you want to run it.
source := "const multiply = (a, b) => a * b"
iso1 := v8.NewIsolate() // creates a new JavaScript VM
ctx1 := v8.NewContext(iso1) // new context within the VM
script1, _ := iso1.CompileUnboundScript(source, "math.js", v8.CompileOptions{}) // compile script to get cached data
val, _ := script1.Run(ctx1)
cachedData := script1.CreateCodeCache()
iso2 := v8.NewIsolate() // create a new JavaScript VM
ctx2 := v8.NewContext(iso2) // new context within the VM
script2, _ := iso2.CompileUnboundScript(source, "math.js", v8.CompileOptions{CachedData: cachedData}) // compile script in new isolate with cached data
val, _ = script2.Run(ctx2)vals := make(chan *v8.Value, 1)
errs := make(chan error, 1)
go func() {
val, err := ctx.RunScript(script, "forever.js") // exec a long running script
if err != nil {
errs <- err
return
}
vals <- val
}()
select {
case val := <- vals:
// success
case err := <- errs:
// javascript error
case <- time.After(200 * time.Millisecond):
vm := ctx.Isolate() // get the Isolate from the context
vm.TerminateExecution() // terminate the execution
err := <- errs // will get a termination error back from the running script
}V8 supports setting a hard limit on Javascript memory usage.
To do so, add a call to WithResourceConstraints to the NewIsolate invocation.
If the limit is hit, v8go terminates the running script, like TerminateExecution above, instead of letting V8 end the process.
The error matches v8.ErrHeapLimitReached. The isolate can be used again, with its initial limit restored, but the state the interrupted script left behind is undefined: recycling the isolate is recommended.
vm := v8.NewIsolate(v8.WithResourceConstraints(8*1024*1024, 16*1024*1024))
ctx := v8.NewContext(vm)
val, err = ctx.RunScript(`
const data = [];
for (let i = 0; i < 1000 * 1000; i++) {
data.push("large data chunk ".repeat(1000));
}
data.length;
`, "memory-test.js")
// errors.Is(err, v8.ErrHeapLimitReached) is true.V8 only samples the OS thread that started profiling.
CPUProfiler.Do keeps the profiled function on that thread.
When using StartProfiling and StopProfiling directly, call runtime.LockOSThread first, and execute JavaScript on the same goroutine.
Otherwise, samples are silently missing from the profile.
func createProfile() {
iso := v8.NewIsolate()
ctx := v8.NewContext(iso)
cpuProfiler := v8.NewCPUProfiler(iso)
cpuProfile := cpuProfiler.Do("my-profile", func() {
ctx.RunScript(profileScript, "script.js") // this script is defined in cpuprofiler_test.go
val, _ := ctx.Global().Get("start")
fn, _ := val.AsFunction()
fn.Call(ctx.Global())
})
printTree("", cpuProfile.GetTopDownRoot()) // helper function to print the profile
}
func printTree(nest string, node *v8.CPUProfileNode) {
fmt.Printf("%s%s %s:%d:%d\n", nest, node.GetFunctionName(), node.GetScriptResourceName(), node.GetLineNumber(), node.GetColumnNumber())
count := node.GetChildrenCount()
if count == 0 {
return
}
nest = fmt.Sprintf("%s ", nest)
for i := 0; i < count; i++ {
printTree(nest, node.GetChild(i))
}
}
// Output
// (root) :0:0
// (program) :0:0
// start script.js:23:15
// foo script.js:15:13
// delay script.js:12:15
// loop script.js:1:14
// bar script.js:13:13
// delay script.js:12:15
// loop script.js:1:14
// baz script.js:14:13
// delay script.js:12:15
// loop script.js:1:14
// (garbage collector) :0:0Contributors working on v8go itself build its C++ sources instead of the prebuilt bridge. V8 is built
with Chromium's hardened libc++, so v8go must be compiled against the same headers
(deps/include_libcxx/), with Clang 21 or newer. -nostdinc++ isn't allowed in #cgo directives,
so it goes in CGO_CXXFLAGS:
CC=clang-21 CXX=clang++-21 CGO_CXXFLAGS=-nostdinc++ go test -tags v8go_source ./...These variables apply to every cgo package of the build. On Windows:
CC="clang -fuse-ld=lld" CXX="clang++ -fuse-ld=lld" CGO_CXXFLAGS=-nostdinc++.
The Docker dev image (tools/docker/dev.sh '<command>') has the toolchains (Go, clang 21, gcc,
benchstat, LLVM binutils). After a change to the C++ sources, the prebuilt bridges must be rebuilt
and the deps/* modules re-pinned: see BOTIFY.md. Source mode has limits (no C++
runtime isolation), also listed there.
go test -c -tags 'leakcheck v8go_source' builds the tests with the
Leak Sanitizer (Linux; botify-ci runs it). Run
the resulting ./v8go.test; separate compile and run steps give line numbers in backtraces.
Go has go fmt; the *.h and *.cc files follow clang-format with the Chromium style
(.clang-format, go generate). clang-format rewrites the //go:build v8go_source line of the
.cc files: see the pitfall in BOTIFY.md before formatting.
Release builds of V8 have no debug information. To get C++ backtraces with line numbers, build V8
with debug info and checks (deps/build.py --debug, see tommie/v8go), build the test binary with
go test -c -ldflags=-compressdwarf=false, and run it under a debugger. Output printed with C
printf is buffered: fflush(stdout) after printing.
v8go was created by Roger Chapman (rogchap/v8go) and is maintained upstream in tommie/v8go; this fork tracks tommie/v8go. Upstream's project goal stands: a high quality, idiomatic Go binding to the V8 C++ API. License: LICENSE (BSD 3-Clause).
V8 Gopher image based on original artwork from the amazing Renee French.
