1安装 Gox
方式一:npm 安装(推荐)
@goxjs/goxjs 包内置 Windows / Linux / macOS × x64 / arm64(windows-arm64 除外共 5 个平台)的预编译二进制,无需 Go 环境:
npm i -g @goxjs/goxjs # 安装后得到 goxjs 命令
goxjs # 进入 REPL
goxjs app.js # 运行脚本
npx goxjs app.js # 或者不全局安装,直接跑
方式二:从源码构建
环境要求 Go 1.26+:
git clone https://github.com/14752222/Gox.git
cd Gox
go build # Windows 下生成 Gox.exe,类 Unix 下生成 Gox
./Gox app.js # 之后用 ./Gox 替代教程中的 goxjs
教程统一用 goxjs 演示命令。如果你是源码构建,请自行替换成 ./Gox(Windows 为 Gox.exe),用法完全相同。
2REPL 与运行脚本
交互式 REPL
不带参数启动就是交互式环境,适合随手试验:
$ goxjs
Gox REPL (ES6 subset, no var)
Type :exit to quit, :help for help
> let x = 10
> let y = 20
> x + y
30
> [1, 2, 3].map(v => v * 2)
[2, 4, 6]
REPL 命令::help 查看帮助、:clear 重置环境、:exit 退出。
运行脚本
goxjs example.js
脚本执行完毕后会回显最后一个顶层表达式的值(undefined 除外),并等待所有定时器与异步回调执行完再退出:
// hello.js
let name = "Gox"
console.log(`Hello, ${name}!`)
setTimeout(() => console.log("tick"), 10)
"bye"
$ goxjs hello.js
Hello, Gox!
bye
tick
3语言基础
Gox 实现 ES6+ 语言子集。声明变量用 let / const —— 不支持 var(刻意的设计取舍,见常见问题)。
// 变量与常量
let x = 10
const PI = 3.14
// 箭头函数、闭包
const add = (a, b) => a + b
const counter = () => { let n = 0; return () => ++n }
let next = counter()
// class
class Point {
constructor(x, y) { this.x = x; this.y = y }
norm() { return Math.hypot(this.x, this.y) }
}
console.log(new Point(3, 4).norm()) // → 5
// 解构、剩余参数、默认参数、展开
let [a, b, ...rest] = [1, 2, 3, 4]
function greet(name = "world", ...tags) { return `hi ${name} ${tags}` }
let merged = [...[1, 2], ...[3, 4]] // [1, 2, 3, 4]
let { host, port } = { host: "127.0.0.1", port: 8080 }
// 模板字符串、for...of、try/catch
for (const v of [1, 2, 3]) console.log(`v = ${v}`)
try {
throw new Error("boom")
} catch (e) {
console.log(e.message) // → boom
}
// 可选链与空值合并
let cfg = { db: { host: "localhost" } }
let host2 = cfg?.db?.host ?? "127.0.0.1"
JSX 语法(<text>...</text>)也是语言子集的一部分,编译期会被降级为 h(tag, props, ...children) 调用,详见 GUI 桌面应用 一节。
4ES 模块
脚本通过 import / export 组织,相对路径以当前文件为基准解析。lib.js:
export const PI = 3.14
export function double(x) { return x * 2 }
main.js:
import { PI, double } from "./lib.js"
setTimeout(() => console.log("tick"), 10)
console.log(double(PI))
$ goxjs main.js
6.28
tick
也支持默认导出与动态 import():
import sq, { PI } from "./math_utils.js" // 默认导出 + 命名导出
import("./math_utils.js").then(m => console.log(m.PI)) // 动态加载
5异步与事件循环
async / await 与 Promise 开箱即用。内置工具 delay(ms, value) 返回一个在 ms 毫秒后 resolve 为 value 的 Promise,写示例和测试都很方便:
async function main() {
let v = await delay(10, "timer done")
console.log(v)
}
main()
Promise { <pending> } ← 顶层回显:main() 调用的返回值
timer done ← 定时器到期后,await 继续执行
定时器家族:
| API | 说明 |
|---|---|
setTimeout(fn, ms) | 延迟执行,返回定时器 id;clearTimeout(id) 取消 |
setInterval(fn, ms) | 周期执行;clearInterval(id) 取消 |
setStrictTimeout 等 | 严格定时器变体,精度可控,适合对时间敏感的场景 |
requestIdleCallback(fn) | 空闲时机回调,不阻塞关键路径 |
脚本不会在有未完成的定时器或异步回调时立刻退出 —— 事件循环会驱动它们全部执行完,这也是上面 main() 能打印结果的原因。
6内置对象速览
全局环境内置了现代 JS 的常用对象,无需 import:
| 类别 | 对象 |
|---|---|
| 基础 | Object Array String Number Boolean Symbol BigInt Math JSON RegExp |
| 集合 | Map Set WeakMap WeakSet |
| 元编程 | Proxy Reflect Iterator |
| 异步 | Promise |
| 二进制 | ArrayBuffer DataView 与 TypedArray 家族 |
| 弱引用 | WeakRef FinalizationRegistry |
| 错误 | 完整错误类型族(TypeError / RangeError / SyntaxError / ReferenceError …) |
| 日期时间 | Temporal —— 取代 Date 的现代日期时间 API |
console.log([1, 2, 3].filter(v => v > 1).reduce((a, b) => a + b)) // → 5
console.log(Math.hypot(3, 4)) // → 5
console.log(JSON.stringify({ ok: true }, null, 2))
let m = new Map([["a", 1]])
m.set("b", 2)
console.log([...m.keys()]) // → [a, b]
7文件与系统(fs / path / process)
fs、path、process 是全局对象,风格与 Node.js 对齐,无需 import。每个 fs API 都有同步(xxxSync)与回调式异步两套:
// 同步风格
fs.writeFileSync("hello.txt", "hello gox")
console.log(fs.readFileSync("hello.txt", "utf-8")) // → hello gox
fs.appendFileSync("hello.txt", "!")
console.log(fs.existsSync("hello.txt")) // → true
let st = fs.statSync("hello.txt")
console.log(st.size, st.isFile(), st.isDirectory()) // 10 true false
for (const f of fs.readdirSync(".")) console.log(f)
fs.mkdirSync("logs", { recursive: true })
// 回调式异步(错误优先,Node 语义)
fs.readFile("hello.txt", "utf-8", (err, data) => {
if (err) throw err
console.log(data)
})
// 也支持 Promise 化的 await 用法
const text = await fs.readFile("hello.txt", "utf-8")
| 模块 | 常用 API |
|---|---|
fs |
readFileSync/readFile、writeFileSync/writeFile、appendFileSync、readBytesSync(返回字节数组)、existsSync、statSync/stat、readdirSync/readdir、mkdirSync/mkdir、renameSync、copyFileSync、rmSync、unlinkSync/unlink、rmdirSync |
path |
join、basename、dirname、extname、resolve、isAbsolute、sep、delimiter |
process |
argv(命令行参数)、env(环境变量)、cwd()、chdir()、exit(code)、platform(windows/linux/darwin)、pid |
let cfgPath = path.join(process.cwd(), "app.json")
console.log(path.extname("archive.tar.gz")) // → .gz
console.log(process.platform, process.pid)
console.log(process.argv) // [goxjs, app.js, ...]
fs.readFileSync(path) 默认按 UTF-8 返回字符串;传 'base64' 则返回 base64 编码;需要原始字节时用 fs.readBytesSync(path)。
8网络请求与 HTTP 服务
fetch:全局 HTTP 客户端
const r = await fetch("https://httpbin.org/get?name=zed")
console.log(r.status, r.ok) // 200 true
console.log(await r.text())
// POST 请求
const r2 = await fetch("https://httpbin.org/post", {
method: "POST",
body: JSON.stringify({ hello: "gox" }),
headers: { "Content-Type": "application/json" }
})
const data = await r2.json()
http.createServer:HTTP 服务端
http 同样是全局对象,支持 get / request 客户端方法与 createServer 服务端:
// server.js
const server = http.createServer(function (req, res) {
if (req.path === "/json") {
res.writeHead(200, { "Content-Type": "application/json" })
res.end(JSON.stringify({ msg: "hi " + req.query.name, method: req.method }))
} else {
res.end("hello world")
}
})
server.listen(8080, function () {
console.log("listening on http://127.0.0.1:8080")
})
$ goxjs server.js
listening on http://127.0.0.1:8080
# 另开一个终端验证
$ curl "http://127.0.0.1:8080/json?name=zed"
{"msg":"hi zed","method":"GET"}
- 请求对象
req:method、url、path、query(解析后的查询参数)、headers、body - 响应对象
res:writeHead(status, headers)、setHeader/removeHeader/getHeader、write、end(body)、statusCode server.listen(0, cb)传 0 表示随机端口,实际端口写入server.port;server.close()关闭- 处理函数里可以
await或用setTimeout延迟end,服务器会挂起任务保活事件循环直到响应完成
网络回调跑在 Go 的 goroutine 上,捕获后被调度回 VM 单线程执行 —— 你的 JS 代码永远单线程,无需加锁。
9响应式编程
Gox 内置两套响应式原语,均为全局函数。
obs:GetX 风格(Dart GetX 语义)
let count = obs(0)
ever(count, v => console.log("count =", v)) // 订阅时立即以当前值回调一次
count.value = 1
count.value = 2
count.value = 2 // 值未变化,不触发通知
count = 0
count = 1
count = 2
| API | 说明 |
|---|---|
obs(value) | 创建可观察值,读写走 .value |
computed(fn) | 由其他 obs 派生的计算值,依赖变化时自动重算 |
ever(obs, fn) | 持续订阅,每次变化都回调 |
once(obs, fn) | 只在下一次变化时回调一次 |
signals:SolidJS 风格(gx/solid 模块)
import { createSignal, createEffect, createMemo } from "gx/solid"
const [count, setCount] = createSignal(0)
createEffect(() => console.log("count is", count())) // 立即执行一次
setCount(5) // → count is 5
const doubled = createMemo(() => count() * 2)
setCount(10)
console.log(doubled()) // → 20
obs 是全局函数可直接用;createSignal 等需要从 "gx/solid" 导入 —— 这是 Gox 内置模块(优先于文件系统解析),也是下一节 GUI 响应式更新的基石。
10GUI 桌面应用
gx/gfx 模块 + JSX 语法提供声明式 UI:渲染器是纯 Go 软件光栅化(无 cgo、无动态库依赖),flex 风格布局,脏矩形局部重绘。一个 16 行的计数器:
// counter.js
import { createSignal } from "gx/solid"
import { h, render } from "gx/gfx"
const [count, setCount] = createSignal(0)
render(
<window title="Counter" width={400} height={300}>
<column gap={8} padding={16}>
<text font={20}>{() => `count: ${count()}`}</text>
<button onClick={() => setCount(c => c + 1)}>加一</button>
</column>
</window>
)
goxjs counter.js # 直接运行,弹出 400x300 窗口
它是怎么工作的
- JSX 在编译期降级为
h(tag, props, ...children)调用 —— 没有虚拟 DOM diff,属性和文本节点直接与信号关联;h因此必须 import - 属性或文本传函数(如
{() => `count: ${count()}`})即为响应式绑定:信号更新 → 依赖该信号的节点标脏 → 脏矩形合并后只重绘受影响区域 - 点击按钮 →
setCount更新信号 → 上述链路自动完成,无需手写刷新 - 输入类组件是受控的:显示只看
value,编辑只派发onInput/onChange,所以别忘在回调里把值写回 signal
内置元素与属性
共 23 个脚本可写的内置元素(select-popup / menu-item 等由 Go 侧构造的内部标签不计),
按用途分七类。下面是速查表,每个组件的完整参数与示例见
组件参考。
| 类别 | 元素 |
|---|---|
| 布局容器 | <column>、<row>、<scroll>、<separator>、<spacer> |
| 表单控件 | <button>、<checkbox>、<radio>、<switch>、<input>、<textarea>、<select>、<slider> |
| 内容展示 | <text>(wrap / ellipsis)、<image>、<progress> |
| 反馈与弹层 | <dialog>、<toast>,以及 gx/dialog 的原生 alert / confirm / openFile |
| 导航与菜单 | <menubar>、<menu>、<menuitem>(全局 shortcut)、openContextMenu(x, y, items) |
| 媒体与自绘 | <canvas>(onDraw(ctx) + 7 个绘制原语) |
| 窗口 | <window>(仅作 render() 的根元素,可多次调用开多窗口) |
常用属性:
| 属性 | 说明 |
|---|---|
width / height / margin | 整数像素;不写则由内容决定 |
gap / padding / flexGrow | 间距、内边距、主轴富余分配 |
alignItems / justifyContent | 交叉轴 / 主轴对齐 |
background / border / color / font | 样式;颜色支持 alpha(#rrggbbaa / rgba()) |
value / checked / open | 受控组件的状态,全部由 JS 的 signal 驱动 |
disabled | 沿祖先链继承;子树不响应事件也不参与焦点 |
zIndex / position / escapeClipping | 层叠、绝对定位、逃逸父盒裁剪 |
transition / opacity | 过渡动画(可动属性:width/height/left/top/opacity) |
事件回调:onClick、onMouseMove、onWheel、
onContextMenu、onKeyDown / onKeyUp、
onFocus / onBlur、onInput / onChange、
onClose、onDraw、onResize(窗口级:挂布局根,载荷 {width, height})。
事件沿祖先链"找第一个处理器"即停,不冒泡;其中 onClick 回调没有参数。
所有输入类组件都不存自己的状态 —— value 决定显示什么,操作只派发
onInput / onChange。忘记把值写回 signal,输入框就会"打字没反应"、
滑块会"弹回原位"。这与 DOM 受控组件一致。
窗口后端:Windows(纯 syscall win32)与 Linux(X11,Wayland 下走 XWayland)。macOS 的 GUI 后端尚未实现 —— CLI 脚本不受影响。 输入法(IME)与原生对话框目前仅 Windows 后端提供,其余平台会安全降级。
完整示例见仓库 testdata/ 目录:计数器 counter_demo.js、
表单 form_demo.js、菜单 menu_demo.js、
画布 canvas_demo.js、滑动条 slider_demo.js、
多窗口 multiwindow_demo.js 等 20 余个,每个都可直接
goxjs testdata/xxx_demo.js 运行。
11打包成独立可执行文件
jsbuild(仓库 packager 目录)把入口脚本及其相对 import 的模块嵌入一个生成的 Go 工程,编译成自带完整运行时的单文件程序 —— 目标机器不需要装 Gox、Go 或任何运行时:
go run ./packager app.js -o app.exe # CLI 应用
go run ./packager counter.js --gui -o counter.exe # GUI 应用
go run ./packager app.js --gui --target linux/amd64 # 纯 Go 交叉编译
| 选项 | 说明 |
|---|---|
-o, --out <path> | 输出文件路径(默认:<输入文件名>.exe) |
--name <name> | 应用名,用于错误信息显示,默认取输入文件名 |
--windowed | 窗口模式:不显示控制台窗口(仅 Windows) |
--gui | GUI 应用:窗口消息泵事件循环(配合 gx/gfx render) |
--target <os>/<arch> | 交叉编译目标:windows / linux / darwin × amd64 / arm64 / 386 |
-v, --verbose | 显示构建过程输出 |
整个运行时是纯 Go(无 cgo),所以 --target linux/amd64 在 Windows 上也能直接编,不需要目标机的交叉工具链。
各平台的分发注意事项(Windows 图标与签名、Linux 打包格式、macOS .app bundle)见 docs/desktop-distribution.md。
12常见问题
为什么不支持 var?
Gox 刻意只实现 ES6+ 子集:let / const 具备块级作用域,语义更清晰,省去了 var 提升等历史包袱。REPL 启动时的提示语 "ES6 subset, no var" 说的就是这件事。
和 Node.js / Bun / Deno 是什么关系?
定位不同。Gox 的价值在于从零走通完整编译管线(lexer → parser → compiler → 字节码 VM)并提供可用的语言与宿主能力,适合写脚本工具、CLI、小型桌面程序,以及学习运行时原理。它不追求替代生产环境的 Node 生态 —— 没有 npm 生态兼容,也没有 JIT。
macOS 能用吗?
CLI 完全可用:npm i -g @goxjs/goxjs 的预编译二进制包含 darwin-amd64 与 darwin-arm64。GUI 窗口后端尚未实现 macOS 支持。
怎么调试 / 参与开发?
go test ./... # 运行全部测试
go run ./dbgtool # 词法调试器:打印 Token 流
go run ./test/bench # 性能剖析基准(fib、函数调用、对象操作)
想新增标准库 API 或深入理解回调桥与内存管理,阅读 JavaScript Runtime API 实现教程。