v8go lets you execute JavaScript from Go using V8, Google's
JavaScript engine. Prebuilt static V8 libraries are shipped for every supported
platform, so go get works out of the box — you should not need to build V8
yourself.
| OS | amd64 | arm64 | cgo C compiler |
|---|---|---|---|
| Linux | ✅ | ✅ | GCC or Clang |
| macOS | ✅ | ✅ | Xcode Clang |
| Windows | ✅ | ✅ | MinGW-w64 only, never MSVC — see Windows |
✅ — a prebuilt static V8 library is committed; nothing is needed at go get time
beyond a working cgo C compiler.
cgo is required, so CGO_ENABLED=0 and cross-compiling without a C toolchain for
the target are not supported.
Windows needs a MinGW-w64 compiler specifically — MSVC does not work, because cgo has never supported it and no version of Go can link MSVC objects.
This repository is a continuation of the original rogchap/v8go, created by Roger Chapman and the v8go contributors. Upstream has been dormant since April 2023. The project was carried forward by Sebastian Döll at katallaxie/v8go, and this repository continues from there.
Most of the code here is upstream's work. It is distributed under the BSD-3-Clause terms in LICENSE, Copyright (c) 2019 Roger Chapman and the v8go contributors.
- Current V8. Tracks recent V8 releases (currently 14.6.202.28)
- Windows support (amd64 and arm64) via the MinGW-w64 toolchain — see Windows.
Value.ArrayBufferViewBytes() []byte— copies the bytes of anyArrayBufferView(typed array orDataView) into a Go-owned slice with a single memcpy, respectingbyteOffset/byteLength.- Abseil isolation. V8's bundled Abseil is rebuilt into the
absl::v8goinline namespace, so it cannot collide at link time with another copy of Abseil in your binary.
import v8 "github.com/hlvs-apps/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 occured
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.Milliseconds):
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
}func createProfile() {
iso := v8.NewIsolate()
ctx := v8.NewContext(iso)
cpuProfiler := v8.NewCPUProfiler(iso)
cpuProfiler.StartProfiling("my-profile")
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())
cpuProfile := cpuProfiler.StopProfiling("my-profile")
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:0Run the benchmarks via make bench.
go vet ./...
go test -bench=. | go tool golang.org/x/perf/cmd/benchstat -
goos: linux
goarch: arm64
pkg: github.com/hlvs-apps/v8go
│ - │
│ sec/op │
Context-8 117.9µ ± ∞ ¹
IsolateInitialization-8 305.1µ ± ∞ ¹
IsolateInitAndRun-8 434.5µ ± ∞ ¹
IsolateCodeCache-8 420.8µ ± ∞ ¹
geomean 284.8µ
¹ need >= 6 samples for confidence interval at level 0.95
│ - │
│ B/op │
Context-8 768.0 ± ∞ ¹
IsolateInitialization-8 152.0 ± ∞ ¹
IsolateInitAndRun-8 921.0 ± ∞ ¹
IsolateCodeCache-8 264.0 ± ∞ ¹
geomean 410.5
¹ need >= 6 samples for confidence interval at level 0.95
│ - │
│ allocs/op │
Context-8 18.00 ± ∞ ¹
IsolateInitialization-8 5.000 ± ∞ ¹
IsolateInitAndRun-8 23.00 ± ∞ ¹
IsolateCodeCache-8 12.00 ± ∞ ¹
geomean 12.55
¹ need >= 6 samples for confidence interval at level 0.95Go Reference & more examples: https://pkg.go.dev/hlvs-apps/v8go
Windows is supported via the MinGW-w64 toolchain, which is what cgo links
with on Windows. A prebuilt static library is included for amd64, so go get
works out of the box — but your build environment must use a MinGW-w64
compiler as cgo's CC.
MSVC is not supported, and cannot be. cgo drives the C compiler with GCC
semantics (GCC-style flags, and it parses the compiler's DWARF output to derive C
type information); cl.exe provides neither. This is a Go limitation, not a
choice made here — see golang/go#20982,
open since 2017. Even Clang targeting MSVC is blocked, because cgo
unconditionally passes the MinGW-only -mthreads
(golang/go#80290).
You do not need a full MSYS2 environment to consume this package — only a
mingw-w64 compiler on PATH. Any of these work:
- MSYS2 —
mingw-w64-x86_64-gcc(amd64) ormingw-w64-clang-aarch64-clang(arm64) - w64devkit — a single zip, no environment
zig cc—CC="zig cc -target x86_64-windows-gnu"; Zig bundles the mingw-w64 headers and CRT
MSYS2 is only required to build V8 itself.
windows/arm64 is supported and ships a prebuilt library like every other
platform. Your cgo compiler must be the CLANGARM64 clang from
MSYS2 (mingw-w64-clang-aarch64-clang); the
mingw-w64-x86_64-gcc used for amd64 is not an aarch64 compiler.
V8 is built for it natively on GitHub's free windows-11-arm runners, also under
CLANGARM64 (build_windows_arm64 in .github/workflows/v8_build.yml).
Cross-compiling from x64 is not an option: mingw-w64 ships no aarch64 GCC, and
the MinGW GN toolchain invokes a bare clang with no --target.
One consequence of CLANGARM64 worth knowing if you touch deps/build.py: its
ar is llvm-ar, not binutils. The two disagree about ar xN. binutils on
Windows matches member names case-insensitively, so V8's runtime.o and the
inspector's Runtime.o resolve as two occurrences of one name; llvm-ar matches
case-sensitively, making them distinct members with one occurrence each.
split_ar() probes with ar --version and adjusts, because getting this wrong
fails only at the very end of an hour-long build.
The V8 static library is built in CI (the build_windows* jobs in
.github/workflows/v8_build.yml) from the patches under
patches/windows/, which are vendored from the
actively-maintained MSYS2 mingw-w64-v8
package and track the same V8 version this project pins — including its arm64
support. deps/build.py applies them (see apply_mingw_patches()) on top of the
gclient-fetched V8 tree when invoked with --os windows. Those patches are
BSD-3-Clause; see patches/windows/LICENSE.
Historical note: Windows support was previously removed upstream in rogchap/v8go#234 and reintroduced here on the MinGW-w64 toolchain.
V8 version: 14.6.202.28 (March 2026)
In order to make v8go usable as a standard Go package, prebuilt static libraries of V8
are included for Linux, macOS and Windows on both amd64 and arm64, so
you should not need to build V8 yourself. Each platform's library lives in its own Go
module under deps/<os>_<arch>/, split into libv8-N.a parts to stay under GitHub's
100 MiB file size limit.
Due to security concerns of binary blobs hiding malicious code, the V8 binary is built via CI ONLY.
To provide a high quality, idiomatic, Go binding to the V8 C++ API.
The API should match the original API as closely as possible, but with an API that Gophers (Go enthusiasts) expect. For example: using multiple return values to return both result and error from a function, rather than throwing an exception.
This project also aims to keep up-to-date with the latest (stable) release of V8.
Aside from data races, Go should be memory-safe and v8go should preserve this property by adding the necessary checks to return an error or panic on these unsupported code paths. Release builds of v8go don't include debugging information for the V8 library since it significantly adds to the binary size, slows down compilation and shouldn't be needed by users of v8go. However, if a v8go bug causes a crash (e.g. during new feature development) then it can be helpful to build V8 with debugging information to get a C++ backtrace with line numbers. The following steps will not only do that, but also enable V8 debug checking, which can help with catching misuse of the V8 API.
- Make sure to clone the projects submodules (ie. the V8's
depot_toolsproject):git submodule update --init --recursive - Build the V8 binary for your OS:
deps/build.py --debug. V8 is a large project, and building the binary can take up to 30 minutes. - Build the executable to debug, using
go buildfor commands orgo test -cfor tests. You may need to add the-ldflags=-compressdwarf=falseoption to disable debug information compression so this information can be read by the debugger (e.g. lldb that comes with Xcode v12.5.1, the latest Xcode released at the time of writing) - Run the executable with a debugger (e.g.
lldb -- ./v8go.test -test.run TestThatIsCrashing,runto start execution then usebtto print a bracktrace after it breaks on a crash), since backtraces printed by Go or V8 don't currently include line number information.
We have the v8_upgrade workflow. The workflow is triggered every day or manually.
If the current v8_hash is different from the latest stable version, the workflow takes care of fetching the latest stable v8 files and copying them into deps/include. The last step of the workflow opens a new PR with the branch name v8_upgrade/<v8-version> with all the changes.
The next steps are:
-
The build is not yet triggered automatically. To trigger it manually, go to the V8 Build Github Action, Select "Run workflow", and select your pushed branch eg.
v8_upgrade/<v8-version>. -
Once built, this opens a PR against your branch for each supported platform — Linux, macOS and Windows on both amd64 and arm64 — adding that platform's static library under
deps/<os>_<arch>/. GitHub's hard file size limit is 100 MiB, so each library is committed as a set of split archive parts (libv8-0.a,libv8-1.a, …) listed in that directory'slibmanifest;deps/build.py(seesplit_ar()) produces them and cgo links the parts. Merge these PRs into your branch. -
Re-pin the
deps/*modules. Eachdeps/<os>_<arch>directory is its own Go module, and the rootgo.modrequires all six at a pseudo-version. Bump those six lines to a commit that contains everydeps/*/go.modtogether with the newly built binaries — normally the commit that merged the last platform PR. Be careful here: thereplace ... => ./deps/...directives hide a wrong pin during local development, because areplaceonly applies whilev8gois the main module. A consumer runninggo get github.com/hlvs-apps/v8go@mainresolves the pseudo-versions for real and fails withinvalid version: missing .../go.mod at revision ...if the pinned commit predates a platform. Verify from outside the repo before releasing:cd $(mktemp -d) && go mod init check go get github.com/hlvs-apps/v8go@main
You are now ready to raise the PR against main with the latest version of V8.
Every platform in the V8 Build workflow compiles through ccache, restored from the GitHub Actions cache. A cold V8 build takes 40 minutes to over two hours per platform; a warm one is typically under ten.
Two details matter if you touch the cache configuration:
- The cache key must not include the V8 version. ccache is content-addressed
— it hashes the preprocessed source together with the compiler, so a cache
carried across a V8 bump cannot produce a stale object; it simply misses on the
files that actually changed. Keying the cache blob by
deps/v8_hashdiscards a mostly-valid cache on every upgrade, which is exactly when you least want a two-hour rebuild. - The size limit must fit a whole V8 build. V8 compiles roughly 1,700 objects,
well over a gigabyte before compression.
hendrikmuhs/ccache-actiondefaults to 500 MB, which silently evicts most of the cache mid-build and produces almost no speedup. Linux and macOS run atmax-size: 2G, Windows atCCACHE_MAXSIZE: 3G.
CCACHE_COMPILERCHECK=content is set on every job, because V8's toolchain is
re-downloaded by the gclient hooks on each run and ccache's default mtime check
would treat every object as a miss.
The build_windows_arm64 job is continue-on-error. Windows arm64 is not yet
wired into go.mod, so its failure cannot break a consumer — but without this a
failing arm64 job skips the commit job and discards every other platform's build.
When using the C/C++ standard library functions for printing (e.g. printf), then the output will be buffered by default.
This can cause some confusion, especially because the test binary (created through go test) does not flush the buffer
at exit (at the time of writing). When standard output is the terminal, then it will use line buffering and flush when
a new line is printed, otherwise (e.g. if the output is redirected to a pipe or file) it will be fully buffered and not even
flush at the end of a line. When the test binary is executed through go test . (e.g. instead of
separately compiled with go test -c and run with ./v8go.test) Go may redirect standard output internally, resulting in
standard output being fully buffered.
A simple way to avoid this problem is to flush the standard output stream after printing with the fflush(stdout); statement.
Not relying on the flushing at exit can also help ensure the output is printed before a crash.
Leak checking is automatically done in CI, but it can be useful to do locally to debug leaks.
Leak checking is done using the Leak Sanitizer which is a part of LLVM. As such, compiling with clang as the C/C++ compiler seems to produce more complete backtraces (unfortunately still only of the system stack at the time of writing).
For instance, on a Debian-based Linux system, you can use sudo apt-get install clang-12 to install a
recent version of clang. Then CC and CXX environment variables are needed to use that compiler. With
that compiler, the tests can be run as follows
CC=clang-12 CXX=clang++-12 go test -c --tags leakcheck && ./v8go.test
The separate compile and link commands are currently needed to get line numbers in the backtrace.
On macOS, leak checking isn't available with the version of clang that comes with Xcode, so a separate
compiler installation is needed. For example, with homebrew, brew install llvm will install a version
of clang with support for this. The ASAN_OPTIONS environment variable will also be needed to run the code
with leak checking enabled, since it isn't enabled by default on macOS. E.g. with the homebrew
installation of llvm, the tests can be run with
CXX=/usr/local/opt/llvm/bin/clang++ CC=/usr/local/opt/llvm/bin/clang go test -c --tags leakcheck -ldflags=-compressdwarf=false
ASAN_OPTIONS=detect_leaks=1 ./v8go.test
The -ldflags=-compressdwarf=false is currently (with clang 13) needed to get line numbers in the backtrace.
Go has go fmt, C has clang-format. Any changes to the v8go.h|cc should be formated with clang-format with the
"Chromium" Coding style. This can be done easily by running the go generate command.
brew install clang-format to install on macOS.
V8 Gopher image based on original artwork from the amazing Renee French.
v8go is distributed under the BSD-3-Clause terms in LICENSE.
This repository also redistributes prebuilt static libraries of V8 under
deps/<os>_<arch>/ and vendors V8's public headers under deps/include/. Those
artifacts carry code from V8 (BSD-3-Clause) and the third-party libraries V8
bundles — Abseil (Apache-2.0), zlib, Highway, simdutf and others. Their notices
are reproduced in THIRD_PARTY_LICENSES.md, and each
deps/<os>_<arch>/ module ships a THIRD_PARTY_NOTICES file generated by
deps/build.py from the exact V8 tree that produced its binary.
The Windows build patches under patches/windows/ come from
the MSYS2 mingw-w64-v8 package and are redistributed under its BSD-3-Clause
license, reproduced at patches/windows/LICENSE.
No MinGW-w64 or GCC code is vendored here or present in the shipped archives. On
Windows, -static links libstdc++/libgcc into your binary from your own
toolchain; those carry the GCC Runtime Library Exception, which permits this
without imposing GPL terms. Nothing in this distribution is copyleft-encumbered.
v8go was created by Roger Chapman and the v8go
contributors at rogchap/v8go, and carried
forward by Sebastian Döll at
katallaxie/v8go. See LICENSE
for the terms this project is distributed under.
