quickjs-go
English | 简体中文
Go bindings to QuickJS: a fast, small, and embeddable ES2020 JavaScript interpreter.
⚠️ This project is not ready for production use yet. Use at your own risk. APIs may change without notice.
Features
- Evaluate script
- Compile script into bytecode and Eval from bytecode
- Operate JavaScript values and objects in Go
- Bind Go function to JavaScript async/sync function
- Simple exception throwing and catching
- Marshal/Unmarshal Go values to/from JavaScript values
- Full TypedArray support (Int8Array, Uint8Array, Float32Array, etc.)
- Create JavaScript Classes from Go with ClassBuilder
- Create JavaScript Modules from Go with ModuleBuilder
- Cross-platform: Compile vendored quickjs-ng sources directly via cgo. ubuntu-latest, windows-latest, and macos-latest are covered and currently passing in Test CI.
Benchmarks
The benchmark results below are generated by the benchcmp tool. To regenerate them, run cd benchcmp && go run . and update this file.
Default cd benchcmp && go run . output:
Factorial Calculation
Computing factorial(10) 1,000,000 times
| Iteration | quickjs-go(cgo / QuickJS-ng) | GOJA(pure Go) | ModerncQuickJS(ccgo / QuickJS) | QJS(Wasm / wazero) | V8go(cgo / V8 JIT) | | --- | --- | --- | --- | --- | --- | | 1 | 213.363ms | 712.622ms | 879.695ms | 626.162ms | 37.025ms | | 2 | 309.421ms | 728.404ms | 878.465ms | 626.668ms | 22.883ms | | 3 | 208.848ms | 697.569ms | 913.305ms | 629.525ms | 22.682ms | | 4 | 217.621ms | 672.554ms | 951.755ms | 627.514ms | 22.803ms | | 5 | 317.197ms | 695.230ms | 964.052ms | 627.884ms | 23.183ms | | Average | 253.290ms | 701.276ms | 917.455ms | 627.551ms | 25.715ms | | Total | 1.266s | 3.506s | 4.587s | 3.138s | 128.576ms | | Speed vs quickjs-go(cgo / QuickJS-ng) | 1.00x | 0.36x | 0.28x | 0.40x | 9.85x |
AreWeFastYet V8-V7
| Metric | quickjs-go(cgo / QuickJS-ng) | GOJA(pure Go) | ModerncQuickJS(ccgo / QuickJS) | QJS(Wasm / wazero) | V8go(cgo / V8 JIT) | | --- | --- | --- | --- | --- | --- | | Richards | 1425 | 453 | 259 | 470 | 51086 | | DeltaBlue | 1398 | 461 | 285 | 561 | 191967 | | Crypto | 1021 | 287 | 304 | 382 | 97807 | | RayTrace | 1628 | 588 | 519 | 760 | 208751 | | EarleyBoyer | 3216 | 1220 | 774 | 1163 | 152728 | | RegExp | 459 | 415 | 199 | 211 | 21456 | | Splay | 3989 | 2049 | 1324 | 2331 | 68909 | | NavierStokes | 1800 | 450 | 529 | 625 | 67832 | | Score (version 7) | 1560 | 599 | 434 | 638 | 86266 | | Duration (seconds) | 35.108s | 61.477s | 70.983s | 57.522s | 20.052s | | Score vs quickjs-go(cgo / QuickJS-ng) | 1.00x | 0.38x | 0.28x | 0.41x | 55.30x | | Speed vs quickjs-go(cgo / QuickJS-ng) | 1.00x | 0.57x | 0.49x | 0.61x | 1.75x |
For benchmark commands and output details, see benchcmp/README.md.
Benchmarks run on Apple M4, 32GB RAM, macOS.
Upstream Sync
This project has migrated its upstream runtime source from bellard/quickjs to quickjs-ng/quickjs.
The primary reason for this migration is long-term maintainability, not changes to the quickjs-go Go API. Compared with the previous upstream, quickjs-ng is a better long-term fit for a binding project in terms of community collaboration, release cadence, cross-platform support, and test/CI coverage. For a Go binding that vendors engine sources and continuously builds/tests via cgo across Linux, macOS, and Windows, these engineering capabilities directly reduce maintenance overhead and improve upgrade efficiency.
This migration only switches the upstream implementation of the vendored JavaScript engine. The public quickjs-go Go API design and usage remain unchanged, and existing Go-side integration code does not need adjustment.
The runtime sources under deps/quickjs are no longer updated via git submodule. They are synchronized from quickjs-ng GitHub releases.
- Current sync metadata is recorded in deps/quickjs-release.env.
- Manual update: scripts/sync_quickjs_ng_release.sh.
- GitHub Actions checks for new releases daily via .github/workflows/sync_quickjs_ng_release.yml and automatically opens a PR.
- Before opening a PR, the automation runs
go test ./...to validate the current repository state.
Guidelines
Lifecycle, Memory, and Concurrency
- QuickJS is not thread-safe; a Runtime and its Contexts must be created, used, and closed by the same serialized owner goroutine.
- This library enforces owner-goroutine checks by default for APIs that touch QuickJS (non-owner calls are rejected with fail-closed behavior); you can explicitly disable this with
WithOwnerGoroutineCheck(false), but it is an unsafe escape hatch and should be used only when your own scheduler strictly serializes all QuickJS access externally. If that guarantee is broken, cross-goroutine calls can race QuickJS internals and may cause memory corruption. - If you want to additionally verify that a Runtime stays fixed to the same OS thread, enable
WithStrictOSThread(true); this option only enables validation and does not bind the thread automatically. If you enable this mode, callruntime.LockOSThread()yourself in the owner goroutine before creating the Runtime. Runtime.NewContext()keeps the default host bootstrap behavior (std/os+ globalsetTimeout/clearTimeout). If you need finer control, useRuntime.NewBareContext()and applyBootstrapStdOS/BootstrapTimersmanually, or useRuntime.NewContextWithOptions(...)withDefaultBootstrap,MinimalBootstrap, andNoBootstrap.- In
Runtime.NewContextWithOptions(...), enablingWithBootstrapTimers(true)implicitly enables std/os registration, because timer injection importssetTimeout/clearTimeoutfrom theosmodule. - Call
value.Free()for*Valueobjects you create or receive. Runtime and Context objects must be cleaned up viaClose(). Value.Free()is fail-closed when its context pointer is no longer valid, avoiding unsafe cgo calls after close.- Value boundary APIs are fail-closed under foreign-context or closed/orphan context inputs:
Set/SetIdxbecome no-ops, andCall/Execute/CallConstructor/deprecatedContext.Invokereturnnil. Marshal/Unmarshalare fail-closed on invalid context/value inputs: they return controlled errors for nil/closed contexts, nil source values, or cross-context values, and do not pass invalid refs into C.- After
Context.Close(), boundary queries are fail-closed:Runtime()returnsnil,HasException()returnsfalse,Exception()returnsnil,Loop()becomes a no-op, andSchedule()returnsfalse. - Bridge mapping and handle-store reads are fail-closed under corrupted entries (wrong types), preventing panic-based crashes.
- Class instance opaque data is resolved via runtime-scoped object identity (
contextID+handleID), so finalizer andValue.GetGoObject()use deterministic ownership lookup. - Go callbacks returning
nilfrom function/method/getter/setter proxies are normalized to JavaScriptundefined. - Module exports are validated during module initialization: export values must be non-
nil, context-live, and belong to the initializing context. - Use
runtime.SetFinalizer()cautiously as it may interfere with QuickJS GC. - For concurrency or isolation, use a thread pool pattern with pre-initialized runtimes, or separate Runtime/Context instances by task/user.
- Reuse Runtime and Context objects when possible, avoid frequent Go/JS value conversion, and consider bytecode compilation for frequently executed scripts.
Error Handling
- Use
Value.IsException()orContext.HasException()to check for exceptions - Use
Context.Exception()to get the exception as a Go error - Always call
defer value.Free()for returned values to prevent memory leaks - Check
Context.HasException()after operations that might throw
Usage
import "github.com/buke/quickjs-go"
Context Bootstrap Modes
rt := quickjs.NewRuntime()
defer rt.Close()
// Default (compatible with previous behavior)
ctxDefault := rt.NewContext()
defer ctxDefault.Close()
// Bare context (no std/os registration, no timer injection)
ctxBare := rt.NewBareContext()
defer ctxBare.Close()
// Manual bootstrap for bare context
quickjs.BootstrapStdOS(ctxBare)
quickjs.BootstrapTimers(ctxBare)
// Option-based bootstrap
ctxMinimal := rt.NewContextWithOptions(quickjs.MinimalBootstrap())
defer ctxMinimal.Close()
Run a script
package main
import (
"fmt"
"runtime"
"github.com/buke/quickjs-go"
)
func main() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
// Create a new runtime
rt := quickjs.NewRuntime()
defer rt.Close()
// Create a new context
ctx := rt.NewContext()
defer ctx.Close()
ret := ctx.Eval("'Hello ' + 'QuickJS!'")
defer ret.Free()
if ret.IsException() {
err := ctx.Exception()
println(err.Error())
return
}
fmt.Println(ret.ToString())
}
Get/Set Javascript Object
package main
import (
"fmt"
"github.com/buke/quickjs-go"
)
func main() {
// Create a new runtime
rt := quickjs.NewRuntime()
defer rt.Close()
// Create a new context
ctx := rt.NewContext()
defer ctx.Close()
test := ctx.NewObject()
test.Set("A", ctx.NewString("String A"))
test.Set("B", ctx.NewString("String B"))
test.Set("C", ctx.NewString("String C"))
ctx.Globals().Set("test", test)
ret := ctx.Eval(Object.keys(test).map(key => test[key]).join(" "))
defer ret.Free()
if ret.IsException() {
err := ctx.Exception()
fmt.Println("Error:", err.Error())
return
}
fmt.Println(ret.ToString())
}
Bind Go Function to Javascript async/sync function
package main
import (
"fmt"
"time"
"github.com/buke/quickjs-go"
)
func main() {
// Create a new runtime
rt := quickjs.NewRuntime()
defer rt.Close()
// Create a new context
ctx := rt.NewContext()
defer ctx.Close()
// Create a new object
test := ctx.NewObject()
// bind properties to the object
test.Set("A", ctx.NewString("String A"))
test.Set("B", ctx.NewInt32(0))
test.Set("C", ctx.NewBool(false))
// bind go function to js object (sync)
test.Set("hello", ctx.NewFunction(func(ctx quickjs.Context, this quickjs.Value, args []quickjs.Value) quickjs.Value {
return ctx.NewString("Hello " + args[0].ToString())
}))
// bind "test" object to global object
ctx.Globals().Set("test", test)
// call js function by js (sync)
jsRet := ctx.Eval(test.hello("Javascript!"))
defer jsRet.Free()
if jsRet.IsException() {
err := ctx.Exception()
fmt.Println("Error:", err.Error())
return
}
fmt.Println(jsRet.ToString())
// call js function by go (sync)
goRet := test.Call("hello", ctx.NewString("Golang!"))
defer goRet.Free()
if goRet.IsException() {
err := ctx.Exception()
fmt.Println("Error:", err.Error())
return
}
fmt.Println(goRet.ToString())
// --- Sync Promise example (immediate resolve/reject, no goroutine) ---
ctx.Globals().Set("syncPromise", ctx.NewFunction(func(ctx quickjs.Context, this quickjs.Value, args []quickjs.Value) quickjs.Value {
return ctx.NewPromise(func(resolve, reject func(*quickjs.Value)) {
// You can resolve the Promise synchronously:
msg := ctx.NewString("Hello from sync Promise")
defer msg.Free()
resolve(msg)
// Or reject synchronously instead (example):
// errVal := ctx.NewString("sync error")
// defer errVal.Free()
// reject(errVal)
})
}))
syncPromise := ctx.Eval(syncPromise())
defer syncPromise.Free()
syncRet := ctx.Await(syncPromise)
defer syncRet.Free()
if syncRet.IsException() {
err := ctx.Exception()
fmt.Println("Error:", err.Error())
return
}
fmt.Println(syncRet.ToString())
// --- Async binding example using Promise + scheduler ---
ctx.Globals().Set("testAsync", ctx.NewFunction(func(ctx quickjs.Context, this quickjs.Value, args []quickjs.Value) quickjs.Value {
return ctx.NewPromise(func(resolve, reject func(*quickjs.Value)) {
// Heavy or blocking work can happen in a goroutine.
go func() {
time.Sleep(10 * time.Millisecond)
// But QuickJS/Context APIs must be used back on
// the Context's own thread via ctx.Schedule.
ctx.Schedule(func(inner *quickjs.Context) {
value := inner.NewString("Hello Async Function!")
defer value.Free()
resolve(value)
})
}()
})
}))
// From JS side, this is just a normal Promise-returning function.
promiseResult := ctx.Eval(testAsync())
defer promiseResult.Free()
// ctx.Await drives the QuickJS pending-job queue and the
// context-level scheduler, so you usually don't need ctx.Loop()
// if you only care about the Promise result.
asyncRet := ctx.Await(promiseResult)
defer asyncRet.Free()
if asyncRet.IsException() {
err := ctx.Exception()
fmt.Println("Error:", err.Error())
return
}
fmt.Println(asyncRet.ToString())
// Output:
// Hello Javascript!
// Hello Golang!
// Hello from sync Promise
// Hello Async Function!
}
// NOTE:
// - The caller owns thread affinity. Bind the owner goroutine with runtime.LockOSThread()
// yourself if the Runtime must stay on one OS thread.
// - WithOwnerGoroutineCheck(false) is unsafe and should be used only when your
// code guarantees serialized QuickJS access externally.
// - Goroutines must NOT call QuickJS/Context APIs directly.
// - Always schedule back to the Context thread via ctx.Schedule
// before creating values or resolving/rejecting Promises.
// - ctx.Await will internally drive pending jobs and the
// scheduler until the Promise settles.
Error Handling
package main
import (
"fmt"
"errors"
"github.com/buke/quickjs-go"
)
func main() {
// Create a new runtime
rt := quickjs.NewRuntime()
defer rt.Close()
// Create a new context
ctx := rt.NewContext()
defer ctx.Close()
ctx.Globals().SetFunction("A", func(ctx quickjs.Context, this quickjs.Value, args []quickjs.Value) quickjs.Value {
// raise error
return ctx.ThrowError(errors.New("expected error"))
})
result := ctx.Eval("A()")
defer result.Free()
if result.IsException() {
actual := ctx.Exception()
fmt.Println(actual.Error())
}
}
TypedArray Support
QuickJS-Go provides support for JavaScript TypedArrays, enabling binary data processing between Go and JavaScript.
Creating TypedArrays from Go
package main
import (
"fmt"
"github.com/buke/quickjs-go"
)
func main() {
rt := quickjs.NewRuntime()
defer rt.Close()
ctx := rt.NewContext()
defer ctx.Close()
// Create various TypedArrays from Go slices
int8Data := []int8{-128, -1, 0, 1, 127}
int8Array := ctx.NewInt8Array(int8Data)
uint8Data := []uint8{0, 128, 255}
uint8Array := ctx.NewUint8Array(uint8Data)
float32Data := []float32{-3.14, 0.0, 2.718, 100.5}
float32Array := ctx.NewFloat32Array(float32Data)
int64Data := []int64{-9223372036854775808, 0, 9223372036854775807}
bigInt64Array := ctx.NewBigInt64Array(int64Data)
// Set TypedArrays as global variables
ctx.Globals().Set("int8Array", int8Array)
ctx.Globals().Set("uint8Array", uint8Array)
ctx.Globals().Set("float32Array", float32Array)
ctx.Globals().Set("bigInt64Array", bigInt64Array)
// Use in JavaScript
result := ctx.Eval(
// Check types
const results = {
int8Type: int8Array instanceof Int8Array,
uint8Type: uint8Array instanceof Uint8Array,
float32Type: float32Array instanceof Float32Array,
bigInt64Type: bigInt64Array instanceof BigInt64Array,
// Calculate sum of float32 array
float32Sum: float32Array.reduce((sum, val) => sum + val, 0)
};
results;
)
defer result.Free()
if result.IsException() {
err := ctx.Exception()
fmt.Println("Error:", err.Error())
return
}
fmt.Println("Results:", result.JSONStringify())
}
Converting JavaScript TypedArrays to Go
package main
import (
"fmt"
"github.com/buke/quickjs-go"
)
func main() {
rt := quickjs.NewRuntime()
defer rt.Close()
ctx := rt.NewContext()
defer ctx.Close()
// Create TypedArrays in JavaScript
jsTypedArrays := ctx.Eval(
({
int8: new Int8Array([-128, -1, 0, 1, 127]),
uint16: new Uint16Array([0, 32768, 65535]),
float64: new Float64Array([Math.PI, Math.E, 42.5]),
bigUint64: new BigUint64Array([0n, 18446744073709551615n])
})
)
defer jsTypedArrays.Free()
if jsTypedArrays.IsException() {
err := ctx.Exception()
fmt.Println("Error:", err.Error())
return
}
// Convert to Go slices
int8Array := jsTypedArrays.Get("int8")
defer int8Array.Free()
if int8Array.IsInt8Array() {
goInt8Slice, err := int8Array.ToInt8Array()
if err == nil {
fmt.Printf("Int8Array: %v\n", goInt8Slice)
}
}
uint16Array := jsTypedArrays.Get("uint16")
defer uint16Array.Free()
if uint16Array.IsUint16Array() {
goUint16Slice, err := uint16Array.ToUint16Array()
if err == nil {
fmt.Printf("Uint16Array: %v\n", goUint16Slice)
}
}
float64Array := jsTypedArrays.Get("float64")
defer float64Array.Free()
if float64Array.IsFloat64Array() {
goFloat64Slice, err := float64Array.ToFloat64Array()
if err == nil {
fmt.Printf("Float64Array: %v\n", goFloat64Slice)
}
}
bigUint64Array := jsTypedArrays.Get("bigUint64")
defer bigUint64Array.Free()
if bigUint64Array.IsBigUint64Array() {
goBigUint64Slice, err := bigUint64Array.ToBigUint64Array()
if err == nil {
fmt.Printf("BigUint64Array: %v\n", goBigUint64Slice)
}
}
}
TypedArray Types Support
| Go Type | JavaScript TypedArray | Context Method | Value Method |
|------------|----------------------|-------------------------|---------------------|
| []int8 | Int8Array | ctx.NewInt8Array() | val.ToInt8Array() |
| []uint8 | Uint8Array | ctx.NewUint8Array() | val.ToUint8Array() |
| []uint8 | Uint8ClampedArray | ctx.NewUint8ClampedArray() | val.ToUint8Array() |
| []int16 | Int16Array | ctx.NewInt16Array() | val.ToInt16Array() |
| []uint16 | Uint16Array | ctx.NewUint16Array() | val.ToUint16Array() |
| []int32 | Int32Array | ctx.NewInt32Array() | val.ToInt32Array() |
| []uint32 | Uint32Array | ctx.NewUint32Array() | val.ToUint32Array() |
| []float32 | Float32Array | ctx.NewFloat32Array() | val.ToFloat32Array() |
| []float64 | Float64Array | ctx.NewFloat64Array() | val.ToFloat64Array() |
| []int64 | BigInt64Array | ctx.NewBigInt64Array() | val.ToBigInt64Array() |
| []uint64 | BigUint64Array | ctx.NewBigUint64Array() | val.ToBigUint64Array() |
| []byte | ArrayBuffer | ctx.NewArrayBuffer() | val.ToByteArray() |
TypedArray Detection
package main
import (
"fmt"
"github.com/buke/quickjs-go"
)
func main() {
rt := quickjs.NewRuntime()
defer rt.Close()
ctx := rt.NewContext()
defer ctx.Close()
// Create various arrays
regularArray := ctx.Eval([1, 2, 3])
defer regularArray.Free()
int32Array := ctx.NewInt32Array([]int32{1, 2, 3})
float64Array := ctx.NewFloat64Array([]float64{1.1, 2.2, 3.3})
// Set arrays as global variables to be referenced by globals
ctx.Globals().Set("int32Array", int32Array)
ctx.Globals().Set("float64Array", float64Array)
// Detect array types
fmt.Printf("Regular array IsArray: %v\n", regularArray.IsArray())
fmt.Printf("Regular array IsTypedArray: %v\n", regularArray.IsTypedArray())
fmt.Printf("Int32Array IsTypedArray: %v\n", int32Array.IsTypedArray())
fmt.Printf("Int32Array IsInt32Array: %v\n", int32Array.IsInt32Array())
fmt.Printf("Int32Array IsFloat64Array: %v\n", int32Array.IsFloat64Array())
fmt.Printf("Float64Array IsTypedArray: %v\n", float64Array.IsTypedArray())
fmt.Printf("Float64Array IsFloat64Array: %v\n", float64Array.IsFloat64Array())
fmt.Printf("Float64Array IsInt32Array: %v\n", float64Array.IsInt32Array())
}
Binary Data Processing Example
package main
import (
"fmt"
"github.com/buke/quickjs-go"
)
func main() {
rt := quickjs.NewRuntime()
defer rt.Close()
ctx := rt.NewContext()
defer ctx.Close()
// Process image-like data (simulate RGB pixels)
imageData := []uint8{
255, 0, 0, // Red pixel
0, 255, 0, // Green pixel
0, 0, 255, // Blue pixel
255, 255, 0, // Yellow pixel
}
// Send to JavaScript as Uint8Array
imageArray := ctx.NewUint8Array(imageData)
ctx.Globals().Set("imageData", imageArray)
// Process in JavaScript
result := ctx.Eval(
// Convert RGB to grayscale
const grayscale = new Uint8Array(imageData.length / 3);
for (let i = 0; i < imageData.length; i += 3) {
const r = imageData[i];
const g = imageData[i + 1];
const b = imageData[i + 2];
grayscale[i / 3] = Math.round(0.299 r + 0.587 g + 0.114 * b);
}
grayscale;
)
defer result.Free()
if result.IsException() {
err := ctx.Exception()
fmt.Println("Error:", err.Error())
return
}
// Convert back to Go
if result.IsUint8Array() {
grayscaleData, err := result.ToUint8Array()
if err == nil {
fmt.Printf("Original RGB: %v\n", imageData)
fmt.Printf("Grayscale: %v\n", grayscaleData)
}
}
}
Marshal/Unmarshal Go Values
QuickJS-Go provides conversion between Go and JavaScript values through the Marshal and Unmarshal methods.
Basic Types
package main
import (
"fmt"
"github.com/buke/quickjs-go"
)
func main() {
rt := quickjs.NewRuntime()
defer rt.Close()
ctx := rt.NewContext()
defer ctx.Close()
// Marshal Go values to JavaScript
data := map[string]interface{}{
"name": "John Doe",
"age": 30,
"active": true,
"scores": []int{85, 92, 78},
"address": map[string]string{
"city": "New York",
"country": "USA",
},
// TypedArray will be automatically created for typed slices
"floatData": []float32{1.1, 2.2, 3.3},
"intData": []int32{100, 200, 300},
"byteData": []byte{0x48, 0x65, 0x6C, 0x6C, 0x6F}, // "Hello" in bytes
}
jsVal, err := ctx.Marshal(data)
if err != nil {
panic(err)
}
defer jsVal.Free()
// Use the marshaled value in JavaScript
ctx.Globals().Set("user", jsVal)
result := ctx.Eval(
const info = user.name + " is " + user.age + " years old";
const floatArrayType = user.floatData instanceof Float32Array;
const intArrayType = user.intData instanceof Int32Array;
const byteArrayType = user.byteData instanceof ArrayBuffer;
({
info: info,
floatArrayType: floatArrayType,
intArrayType: intArrayType,
byteArrayType: byteArrayType,
byteString: new TextDecoder().decode(user.byteData)
});
)
defer result.Free()
if result.IsException() {
err := ctx.Exception()
fmt.Println("Error:", err.Error())
return
}
fmt.Println("Result:", result.JSONStringify())
// Unmarshal JavaScript values back to Go
var userData map[string]interface{}
err = ctx.Unmarshal(jsVal, &userData)
if err != nil {
panic(err)
}
fmt.Printf("Unmarshaled: %+v\n", userData)
}
Struct Marshaling with Tags
package main
import (
"fmt"
"time"
"github.com/buke/quickjs-go"
)
type User struct {
ID int64 js:"id"
Name string js:"name"
Email string json:"email_address"
CreatedAt time.Time js:"created_at"
IsActive bool js:"is_active"
Tags []string js:"tags"
// TypedArray fields
Scores []float32 js:"scores" // Will become Float32Array
Data []int32 js:"data" // Will become Int32Array
Binary []byte js:"binary" // Will become ArrayBuffer
// unexported fields are ignored
password string
// Fields with "-" tag are skipped
Secret string js:"-"
}
func main() {
rt := quickjs.NewRuntime()
defer rt.Close()
ctx := rt.NewContext()
defer ctx.Close()
user := User{
ID: 123,
Name: "Alice",
Email: "[email protected]",
CreatedAt: time.Now(),
IsActive: true,
Tags: []string{"admin", "user"},
Scores: []float32{95.5, 87.2, 92.0},
Data: []int32{1000, 2000, 3000},
Binary: []byte{0x41, 0x42, 0x43}, // "ABC"
password: "secret123",
Secret: "top-secret",
}
// Marshal struct to JavaScript
jsVal, err := ctx.Marshal(user)
if err != nil {
panic(err)
}
defer jsVal.Free()
// Check TypedArray types in JavaScript
ctx.Globals().Set("user", jsVal)
result := ctx.Eval(
({
scoresType: user.scores instanceof Float32Array,
dataType: user.data instanceof Int32Array,
binaryType: user.binary instanceof ArrayBuffer,
binaryString: new TextDecoder().decode(user.binary),
avgScore: user.scores.reduce((sum, score) => sum + score) / user.scores.length
});
)
defer result.Free()
if result.IsException() {
err := ctx.Exception()
fmt.Println("Error:", err.Error())
return
}
// Modify in JavaScript
modifyResult := ctx.Eval(
user.name = "Alice Smith";
user.tags.push("moderator");
// Modify TypedArray data
user.scores[0] = 98.5;
user;
)
defer modifyResult.Free()
if modifyResult.IsException() {
err := ctx.Exception()
fmt.Println("Error:", err.Error())
return
}
// Unmarshal back to Go struct
var updatedUser User
err = ctx.Unmarshal(modifyResult, &updatedUser)
if err != nil {
panic(err)
}
fmt.Printf("Updated user: %+v\n", updatedUser)
// Note: password and Secret fields remain unchanged (not serialized)
}
Custom Marshal/Unmarshal
package main
import (
"fmt"
"strings"
"github.com/buke/quickjs-go"
)
type CustomType struct {
Value string
}
// Implement Marshaler interface
func (c CustomType) MarshalJS(ctx quickjs.Context) (quickjs.Value, error) {
return ctx.NewString("custom:" + c.Value), nil
}
// Implement Unmarshaler interface
func (c CustomType) UnmarshalJS(ctx quickjs.Context, val *quickjs.Value) error {
if val.IsString() {
str := val.ToString()
if strings.HasPrefix(str, "custom:") {
c.Value = str[7:] // Remove "custom:" prefix
} else {
c.Value = str
}
}
return nil
}
func main() {
rt := quickjs.NewRuntime()
defer rt.Close()
ctx := rt.NewContext()
defer ctx.Close()
// Marshal custom type
custom := CustomType{Value: "hello"}
jsVal, err := ctx.Marshal(custom)
if err != nil {
panic(err)
}
defer jsVal.Free()
fmt.Println("Marshaled:", jsVal.ToString()) // Output: custom:hello
// Unmarshal back
var result CustomType
err = ctx.Unmarshal(jsVal, &result)
if err != nil {
panic(err)
}
fmt.Printf("Unmarshaled: %+v\n", result) // Output: {Value:hello}
}
Type Mappings
Go to JavaScript:
bool→ JavaScript booleanint,int8,int16,int32→ JavaScript number (32-bit)int64→ JavaScript number (64-bit)uint,uint8,uint16,uint32→ JavaScript number (32-bit unsigned)uint64→ JavaScript BigIntfloat32,float64→ JavaScript numberstring→ JavaScript string[]byte→ JavaScript ArrayBuffer[]int8→ JavaScript Int8Array[]uint8→ JavaScript Uint8Array[]int16→ JavaScript Int16Array[]uint16→ JavaScript Uint16Array[]int32→ JavaScript Int32Array[]uint32→ JavaScript Uint32Array[]float32→ JavaScript Float32Array[]float64→ JavaScript Float64Array[]int64→ JavaScript BigInt64Array[]uint64→ JavaScript BigUint64Arrayslice/array→ JavaScript Array (for non-typed arrays)map→ JavaScript Objectstruct→ JavaScript Objectpointer→ recursively marshal pointed value (nil becomes null)
- JavaScript null/undefined → Go nil pointer or zero value
- JavaScript boolean → Go bool
- JavaScript number → Go numeric types (with appropriate conversion)
- JavaScript BigInt → Go
uint64/int64/*big.Int - JavaScript string → Go string
- JavaScript Array → Go slice/array
- JavaScript Object → Go map/struct
- JavaScript ArrayBuffer → Go
[]byte - JavaScript Int8Array → Go
[]int8 - JavaScript Uint8Array/Uint8ClampedArray → Go
[]uint8 - JavaScript Int16Array → Go
[]int16 - JavaScript Uint16Array → Go
[]uint16 - JavaScript Int32Array → Go
[]int32 - JavaScript Uint32Array → Go
[]uint32 - JavaScript Float32Array → Go
[]float32 - JavaScript Float64Array → Go
[]float64 - JavaScript BigInt64Array → Go
[]int64 - JavaScript BigUint64Array → Go
[]uint64
interface{}, the following types are used:
nilfor null/undefinedboolfor booleanint64for integer numbersfloat64for floating-point numbersstringfor string[]interface{}for Arraymap[string]interface{}for Object*big.Intfor BigInt[]bytefor ArrayBuffer
Create JavaScript Modules from Go with ModuleBuilder
The ModuleBuilder API allows you to create JavaScript modules from Go code, making Go functions, values, and objects available for standard ES6 import syntax in JavaScript applications.
For new code, prefer declarative ValueSpec exports (ExportValue / ExportLiteral).
Export(name, *Value) is still supported as a context-bound convenience API for values that are already created in the same Context.
Basic Module Creation
package main
import (
"fmt"
"github.com/buke/quickjs-go"
)
func main() {
rt := quickjs.NewRuntime()
defer rt.Close()
ctx := rt.NewContext()
defer ctx.Close()
// Create a math module with Go functions and values
addFunc := ctx.NewFunction(func(ctx quickjs.Context, this quickjs.Value, args []quickjs.Value) quickjs.Value {
if len(args) >= 2 {
return ctx.NewFloat64(args[0].ToFloat64() + args[1].ToFloat64())
}
return ctx.NewFloat64(0)
})
defer addFunc.Free()
// Build the module using fluent API
module := quickjs.NewModuleBuilder("math").
Export("PI", ctx.NewFloat64(3.14159)).
Export("add", addFunc).
Export("version", ctx.NewString("1.0.0")).
Export("default", ctx.NewString("Math Module"))
err := module.Build(ctx)
if err != nil {
panic(err)
}
// Use the module in JavaScript with standard ES6 import
result := ctx.Eval(
(async function() {
// Named imports
const { PI, add, version } = await import('math');
// Use imported functions and values
const sum = add(PI, 1.0);
return { sum, version };
})()
, quickjs.EvalAwait(true))
defer result.Free()
if result.IsException() {
err := ctx.Exception()
panic(err)
}
fmt.Println("Module result:", result.JSONStringify())
// Output: Module result: {"sum":4.14159,"version":"1.0.0"}
}
Advanced Module Features
package main
import (
"fmt"
"github.com/buke/quickjs-go"
)
func main() {
rt := quickjs.NewRuntime()
defer rt.Close()
ctx := rt.NewContext()
defer ctx.Close()
// Create a utilities module with complex objects
config := ctx.NewObject()
config.Set("appName", ctx.NewString("MyApp"))
config.Set("version", ctx.NewString("2.0.0"))
config.Set("debug", ctx.NewBool(true))
greetFunc := ctx.NewFunction(func(ctx quickjs.Context, this quickjs.Value, args []quickjs.Value) quickjs.Value {
name := "World"
if len(args) > 0 {
name = args[0].ToString()
}
return ctx.NewString(fmt.Sprintf("Hello, %s!", name))
})
defer greetFunc.Free()
jsonVal := ctx.ParseJSON({"MAX": 100, "MIN": 1})
defer jsonVal.Free()
// Create module with various export types
module := quickjs.NewModuleBuilder("utils").
Export("config", config). // Object export
Export("greet", greetFunc). // Function export
Export("constants", jsonVal). // JSON export
Export("default", ctx.NewString("Utils Library")) // Default export
err := module.Build(ctx)
if err != nil {
panic(err)
}
// Use mixed imports in JavaScript
result := ctx.Eval(
(async function() {
// Import from utils module
const { greet, config, constants } = await import('utils');
// Combine functionality
const message = greet("JavaScript");
const info = config.appName + " v" + config.version;
const limits = "Max: " + constants.MAX + ", Min: " + constants.MIN;
return { message, info, limits };
})()
, quickjs.EvalAwait(true))
defer result.Free()
if result.IsException() {
err := ctx.Exception()
panic(err)
}
fmt.Println("Advanced module result:", result.JSONStringify())
}
Declarative ValueSpec Exports (Recommended)
package main
import (
"fmt"
"github.com/buke/quickjs-go"
)
func main() {
rt := quickjs.NewRuntime()
defer rt.Close()
ctx := rt.NewContext()
defer ctx.Close()
module := quickjs.NewModuleBuilder("app-config").
ExportLiteral("version", "2.1.0").
ExportLiteral("featureFlags", map[string]bool{"beta": true}).
ExportValue("now", quickjs.FactorySpec{Factory: func(ctx quickjs.Context) (quickjs.Value, error) {
return ctx.NewInt64(1700000000), nil
}}).
ExportValue("meta", quickjs.MarshalSpec{Value: map[string]interface{}{
"name": "quickjs-go",
"typed": true,
}})
if err := module.Build(ctx); err != nil {
panic(err)
}
result := ctx.Eval(
(async function() {
const { version, featureFlags, now, meta } = await import('app-config');
return [version, featureFlags.beta, now > 0, meta.name].join(':');
})()
, quickjs.EvalAwait(true))
defer result.Free()
if result.IsException() {
panic(ctx.Exception())
}
fmt.Println("Declarative module result:", result.ToString())
// Output: Declarative module result: 2.1.0:true:true:quickjs-go
}
Multiple Module Integration
package main
import (
"fmt"
"strings"
"github.com/buke/quickjs-go"
)
func main() {
rt := quickjs.NewRuntime()
defer rt.Close()
ctx := rt.NewContext()
defer ctx.Close()
// Create math module
addFunc := ctx.NewFunction(func(ctx quickjs.Context, this quickjs.Value, args []quickjs.Value) quickjs.Value {
if len(args) >= 2 {
return ctx.NewFloat64(args[0].ToFloat64() + args[1].ToFloat64())
}
return ctx.NewFloat64(0)
})
defer addFunc.Free()
mathModule := quickjs.NewModuleBuilder("math").
Export("add", addFunc).
Export("PI", ctx.NewFloat64(3.14159))
// Create string utilities module
upperFunc := ctx.NewFunction(func(ctx quickjs.Context, this quickjs.Value, args []quickjs.Value) quickjs.Value {
if len(args) > 0 {
return ctx.NewString(strings.ToUpper(args[0].ToString()))
}
return ctx.NewString("")
})
defer upperFunc.Free()
stringModule := quickjs.NewModuleBuilder("strings").
Export("upper", upperFunc)
// Build both modules
err := mathModule.Build(ctx)
if err != nil {
panic(err)
}
err = stringModule.Build(ctx)
if err != nil {
panic(err)
}
// Use multiple modules together
result := ctx.Eval(
(async function() {
// Import from multiple modules
const { add, PI } = await import('math');
const { upper } = await import('strings');
// Combine functionality
const sum = add(PI, 1);
const message = "Result: " + sum.toFixed(2);
const finalMessage = upper(message);
return finalMessage;
})()
, quickjs.EvalAwait(true))
defer result.Free()
if result.IsException() {
err := ctx.Exception()
panic(err)
}
fmt.Println("Multiple modules result:", result.ToString())
// Output: Multiple modules result: RESULT: 4.14
}
ModuleBuilder API Reference
Core Methods:
NewModuleBuilder(name)- Create a new module builder with the specified nameExportValue(name, spec)- Add a declarative export spec (recommended)ExportLiteral(name, value)- Add a literal export without pre-creating JS values (recommended)Export(name, value)- Add a named export from an existing JS value (context-bound convenience API)Build(ctx)- Register the module in the JavaScript context
Create JavaScript Classes from Go with ClassBuilder
The ClassBuilder API allows you to create JavaScript classes from Go code.
For new code, prefer declarative property specs (PropertyValue / PropertyLiteral / StaticPropertyValue / StaticPropertyLiteral).
Property(name, Value) and StaticProperty(name, Value) remain available as context-bound convenience APIs.
Manual Class Creation
Create JavaScript classes manually with control over methods, properties, and accessors:
package main
import (
"fmt"
"math"
"github.com/buke/quickjs-go"
)
type Point struct {
X, Y float64
Name string
}
func (p *Point) Distance() float64 {
return math.Sqrt(p.Xp.X + p.Yp.Y)
}
func (p *Point) Move(dx, dy float64) {
p.X += dx
p.Y += dy
}
func main() {
rt := quickjs.NewRuntime()
defer rt.Close()
ctx := rt.NewContext()
defer ctx.Close()
// Create Point class using ClassBuilder
pointConstructor, _ := quickjs.NewClassBuilder("Point").
Constructor(func(ctx quickjs.Context, instance quickjs.Value, args []*quickjs.Value) (interface{}, error) {
x, y := 0.0, 0.0
name := "Unnamed Point"
if len(args) > 0 { x = args[0].ToFloat64() }
if len(args) > 1 { y = args[1].ToFloat64() }
if len(args) > 2 { name = args[2].ToString() }
// Return Go object for automatic association
return &Point{X: x, Y: y, Name: name}, nil
}).
// Accessors provide getter/setter functionality with custom logic
Accessor("x",
func(ctx quickjs.Context, this quickjs.Value) *quickjs.Value {
point, _ := this.GetGoObject()
return ctx.NewFloat64(point.(*Point).X)
},
func(ctx quickjs.Context, this quickjs.Value, value quickjs.Value) quickjs.Value {
point, _ := this.GetGoObject()
point.(*Point).X = value.ToFloat64()
return ctx.NewUndefined()
}).
Accessor("y",
func(ctx quickjs.Context, this quickjs.Value) *quickjs.Value {
point, _ := this.GetGoObject()
return ctx.NewFloat64(point.(*Point).Y)
},
func(ctx quickjs.Context, this quickjs.Value, value quickjs.Value) quickjs.Value {
point, _ := this.GetGoObject()
point.(*Point).Y = value.ToFloat64()
return ctx.NewUndefined()
}).
// Properties are bound directly to each instance
// Context-bound convenience API: Property(name, *Value)
Property("version", ctx.NewString("1.0.0")).
Property("type", ctx.NewString("Point")).
// Read-only property
Property("readOnly", ctx.NewBool(true), quickjs.PropertyConfigurable).
// Instance methods
Method("distance", func(ctx quickjs.Context, this quickjs.Value, args []quickjs.Value) quickjs.Value {
point, _ := this.GetGoObject()
return ctx.NewFloat64(point.(*Point).Distance())
}).
Method("move", func(ctx quickjs.Context, this quickjs.Value, args []quickjs.Value) quickjs.Value {
point, _ := this.GetGoObject()
dx, dy := 0.0, 0.0
if len(args) > 0 { dx = args[0].ToFloat64() }
if len(args) > 1 { dy = args[1].ToFloat64() }
point.(*Point).Move(dx, dy)
return ctx.NewUndefined()
}).
Method("getName", func(ctx quickjs.Context, this quickjs.Value, args []quickjs.Value) quickjs.Value {
point, _ := this.GetGoObject()
return ctx.NewString(point.(*Point).Name)
}).
// Static method
StaticMethod("origin", func(ctx quickjs.Context, this quickjs.Value, args []quickjs.Value) quickjs.Value {
// Create a new Point at origin
origin := &Point{X: 0, Y: 0, Name: "Origin"}
jsVal, _ := ctx.Marshal(origin)
return jsVal
}).
Build(ctx)
// Register the class
ctx.Globals().Set("Point", pointConstructor)
// Use in JavaScript
result := ctx.Eval(
const p = new Point(3, 4, "My Point");
const dist1 = p.distance();
p.move(1, 1);
const dist2 = p.distance();
// Static method usage
const origin = Point.origin();
({
// Accessor usage
x: p.x,
y: p.y,
// Property usage
version: p.version,
type: p.type,
readOnly: p.readOnly,
hasOwnProperty: p.hasOwnProperty('version'), // true for properties
// Method results
name: p.getName(),
initialDistance: dist1,
finalDistance: dist2,
// Static method result
originDistance: Math.sqrt(origin.x origin.x + origin.y origin.y)
});
)
defer result.Free()
if result.IsException() {
err := ctx.Exception()
fmt.Println("Error:", err.Error())
return
}
fmt.Println("Result:", result.JSONStringify())
// Demonstrate the difference between accessors and properties
propertyTest := ctx.Eval(
const p1 = new Point(1, 1);
const p2 = new Point(2, 2);
// Properties are instance-specific values
const sameVersion = p1.version === p2.version; // true, same static value
// Accessors provide dynamic values from Go object
const differentX = p1.x !== p2.x; // true, different values from Go objects
({ sameVersion, differentX });
)
defer propertyTest.Free()
if propertyTest.IsException() {
err := ctx.Exception()
fmt.Println("Error:", err.Error())
return
}
fmt.Println("Property vs Accessor:", propertyTest.JSONStringify())
}
Declarative Property Specs (Recommended)
package main
import (
"fmt"
"github.com/buke/quickjs-go"
)
type Point struct {
X, Y float64
}
func main() {
rt := quickjs.NewRuntime()
defer rt.Close()
ctx := rt.NewContext()
defer ctx.Close()
pointConstructor, _ := quickjs.NewClassBuilder("Point").
Constructor(func(ctx quickjs.Context, instance quickjs.Value, args []*quickjs.Value) (interface{}, error) {
return &Point{X: 0, Y: 0}, nil
}).
PropertyLiteral("label", "point-instance").
PropertyValue("meta", quickjs.MarshalSpec{Value: map[string]interface{}{"kind": "2d"}}).
StaticPropertyLiteral("kind", "PointClass").
StaticPropertyValue("version", quickjs.FactorySpec{Factory: func(ctx quickjs.Context) (quickjs.Value, error) {
return ctx.NewInt32(1), nil
}}).
Build(ctx)
ctx.Globals().Set("Point", pointConstructor)
result := ctx.Eval(
(() => {
const p = new Point();
return [p.label, p.meta.kind, Point.kind, Point.version].join(':');
})()
)
defer result.Free()
if result.IsException() {
panic(ctx.Exception())
}
fmt.Println("Declarative class result:", result.ToString())
// Output: Declarative class result: point-instance:2d:PointClass:1
}
Automatic Class Creation with Reflection
Automatically generate JavaScript classes from Go structs using reflection. Go struct fields are automatically converted to JavaScript class accessors, providing getter/setter functionality that directly maps to the underlying Go object fields.
``go
package main
import ( "fmt" "github.com/buke/quickjs-go" )
type User struct {
ID int64 js:"id" // Becomes accessor: user.id
Name string js:"name" // Becomes accessor: user.name
Email string json:"email_address" // Becomes accessor: user.email_address
Age int js:"age" // Becomes accessor: user.age
IsActive bool js:"is_active" // Becomes accessor: user.is_active
Scores []float32 js:"scores" // Becomes accessor: user.scores (Float32Array)
private string // Not accessible (unexported)
Secret string js:"-" // Explicitly ignored
}
func (u *User) GetFullInfo() string { return fmt.Sprintf("%s (%s) - Age: %d", u.Name, u.Email, u.Age) }
func (u *User) UpdateEmail(newEmail string) { u.Email = newEmail }
func (u *User) AddScore(score float32) { u.Scores = append(u.Scores, score) }
func main() { rt := quickjs.NewRuntime() defer rt.Close() ctx := rt.NewContext() defer ctx.Close()
// Automatically create User class from struct userConstructor, _ := ctx.BindClass(&User{})
ctx.Globals().Set("User", userConstructor)
// Use with positional arguments
result1 := ctx.Eval(
const user1 = new User(1, "Alice", "[email protected]", 25, true, [95.5, 87.2]);
user1.GetFullInfo();
)
defer result1.Free()
if result1.IsException() {
err := ctx.Exception()
fmt.Println("Error:", err.Error())
return
}
fmt.Println("Positional:", result1.ToString())
// Use with named arguments (object parameter) result2 := ctx.Eval( const user2 = new User({ id: 2, name: "Bob", email_address: "[email protected]", age: 30, is_active: true, scores: [88.0, 92.5, 85.0] }); // Call methods user2.UpdateEmail("[email protected]"); user2.AddScore(95.0); // Access fields via accessors (directly map to Go struct fields) user2.age = 31; // Setter: modifies the Go struct field const newAge = user2.age; // Getter: reads from the Go struct field ({ info: user2.GetFullInfo(), email: user2.email_address, // Accessor getter age: newAge, // Modified via accesso
... (README truncated for length)