语言参考
gotsx 的方言是一门借 TSX 语法的静态语言, 不是 TypeScript 的子集实现——它没有 React 运行时, 类型系统限定在 Go 能表示的集合里。子集由类型系统定义: 每个表达式都能推出一个静态类型, 且落在允许集合里, 就能编译; 否则是带位置的编译错误。
几个语义约定
number 是 float64 · 可选的原始类型用零值表示缺席("" / 0 / false) · 字符串在 Go 侧按 rune 处理 · || 和 ?? 对字符串以空为假 · 跑在服务端的代码里不能出现 DOM / async / Node, 只跑在浏览器的可以。语法表
73 项支持, 13 项明确不支持(会报错)。这张表本身是一个岛: 过滤在浏览器里完成。
86 / 86
| 类别 | 语法 | 说明 | 状态 |
|---|---|---|---|
| 模块 | import X from "./a" | 默认导入; 路径可省 .tsx / .server.tsx / .client.tsx | 支持 |
| 模块 | import { a, b as c } from "./a" | 命名导入 | 支持 |
| 模块 | import type { T } from "./a" | 类型导入; 客户端对 host:* 只允许 import type(action 除外) | 支持 |
| 模块 | import { useState, useEffect, useMemo } from "gotsx" | hooks | 支持 |
| 模块 | import { createStore, seed } from "gotsx" | store: 多个岛共享的状态, 由页面每请求注入初值(见 Hooks 行) | 支持 |
| 模块 | import type { Node, PageProps, LayoutProps, Meta, Flash } from "gotsx" | 框架类型 | 支持 |
| 模块 | import { x } from "host:name" | 服务端组件里的宿主模块: 编译成直接 Go 调用 | 支持 |
| 模块 | import { toggle } from "host:name" (island) → await toggle(id) | 类型化 action: Registry[...].Actions 里的 Go 方法; 调用是同源 POST, 类型 Promise<T> 来自 Go 签名; 失败抛出带 .status / .fields 的 Error | 支持 |
| 模块 | export function meta(props?: PageProps): Meta | 页面元数据(title / description / canonical / image / noIndex), 每请求算一次, 每层布局通过 props.meta 拿到 | 支持 |
| 模块 | props.session / props.flash / props.csrf | PageProps: 签名会话的键值(只读)、一次性 flash 消息、经典表单用的 CSRF token | 支持 |
| 模块 | export default function / export function | 组件(大写)或普通函数 | 支持 |
| 模块 | export const data: T[] = [...] | 模块级 const → Go 包级变量; 不允许 let | 支持 |
| 模块 | export interface / export type | 类型导出(所有类型自动可被 import type) | 支持 |
| 模块 | pages/a/[id].server.tsx / pages/docs/[...slug].server.tsx | 文件路由: params.id; catch-all 的 params.slug = "x/y/z"; 更具体的路由优先 | 支持 |
| 模块 | pages/**/_layout.server.tsx / _404 / _error | 嵌套布局(LayoutProps = PageProps + meta + children, 外层包内层); _404 → gen.NotFound, _error(ErrorProps)→ gen.ErrorPage | 支持 |
| 模块 | import { Suspense } from "gotsx" | 流式边界(仅服务端): <Suspense fallback={…}> 随外壳先发 fallback, children 在自己的 goroutine 里渲染后流式填入 | 支持 |
| 模块 | import * as ns from | 命名空间导入 | 不支持 |
| 语句 | const x = ... / let x = ... | 单声明; 类型可标注可推断 | 支持 |
| 语句 | const { a, b = 1 } = obj / const [x, setX] = ... | 解构 + 默认值(原始类型, 零值语义) | 支持 |
| 语句 | if / else if / else | 条件按 JS 真值语义(空串、0 为假) | 支持 |
| 语句 | for (const x of xs) | for-of 遍历数组 | 支持 |
| 语句 | for (let i = 0; i < n; i++) / while (cond) / break / continue | 经典循环; Go 侧是真正的 for 语句(continue 会执行更新) | 支持 |
| 语句 | switch (x) { case a: case b: … break; default: … } | 保留 JS 的贯穿语义(翻译成 Go fallthrough); switch (true) 也可以 | 支持 |
| 语句 | return / throw | throw 在 Go 侧是 panic, 请求层 recover | 支持 |
| 语句 | try / catch / finally | 客户端完整; Go 侧只执行 try 和 finally 体 | 支持 |
| 语句 | function f() {} / async function f() {} | 嵌套函数; async 只进 JS 后端 | 支持 |
| 语句 | do … while / for … in / labeled break | 改写成 while, 或 for-of 遍历 Object.keys(obj) | 不支持 |
| 语句 | const a = 1, b = 2 | 一条语句多个声明 | 不支持 |
| 表达式 | number / string / template string / true / null / undefined | number 是 float64 | 支持 |
| 表达式 | [a, ...b] / { a, b: 1, ...c } | 数组和对象字面量(对象展开只在客户端) | 支持 |
| 表达式 | a.b / a?.b / a[i] / a?.[i] | 成员、可选链、下标 | 支持 |
| 表达式 | f(x) / f?.(x) / useState<T>(x) | 调用、可选调用、显式类型参数 | 支持 |
| 表达式 | ! - + typeof | 一元 | 支持 |
| 表达式 | + - * / % === !== < > <= >= | 算术、严格相等、比较 | 支持 |
| 表达式 | && || ?? | 逻辑; 原始类型的 ?? 与 || 等价(零值 = 缺席); 缺席的对象(find 没找到、可选字段)为假且 === undefined | 支持 |
| 表达式 | a ? b : c | 三元(节点或值) | 支持 |
| 表达式 | (a, b) => expr / x => { ... } / async () => ... | 箭头函数; 参数类型可从上下文推断 | 支持 |
| 表达式 | x = v / += -= *= /= %= / x++ x-- ++x --x | 给变量、字段、Record 键、数组下标赋值; ++/-- 只对 number | 支持 |
| 表达式 | x as T / x! | 类型断言、非空断言 | 支持 |
| 表达式 | await x | 只在客户端代码里 | 支持 |
| 表达式 | == != | 只允许 === / !== | 不支持 |
| 表达式 | new / class / this / function expression | 没有类和原型 | 不支持 |
| 表达式 | /pattern/gimsu | 正则字面量, RE2 子集编译期校验(无 lookaround / 反向引用); re.test, s.match/replace/replaceAll/split/search | 支持 |
| 表达式 | delete m.key / delete m[key] / Object.hasOwn(m, key) | Record 键: 缺席的键两端都读出零值; hasOwn 判断存在 | 支持 |
| 表达式 | in / instanceof / void | 不支持 | |
| JSX | <div class="x" id={v} disabled>…</div> / <br /> | 元素、属性字符串/表达式/布尔简写、自闭合 | 支持 |
| JSX | <></> | fragment | 支持 |
| JSX | onClick={fn} / onInput={(e) => ...} | 事件; 处理器参数类型 any; 服务端不生成 | 支持 |
| JSX | aria-* / data-* / role | 布尔值渲染成 "true" / "false" | 支持 |
| JSX | {cond && <x/>} / {a ? <x/> : <y/>} / {list.map(...)} | 条件与列表; 读到 signal 的才是响应式的 | 支持 |
| JSX | {list.map((x) => <li key={x.id}>…</li>)} | keyed 列表: 客户端按 key 复用 / 移动 / 销毁 DOM, 输入框、焦点、行内 effect 在重排后保留; 不写 key 就整块重建 | 支持 |
| JSX | <Comp prop={v}>children</Comp> | 组件调用; children 是 Node | 支持 |
| JSX | {/* comment */} | 支持 | |
| JSX | <div {...props} /> | 属性展开 | 不支持 |
| JSX | passing children to an island | 岛的 props 走 JSON 进 HTML 属性, 放不下 Node; 共享组件和服务端组件的 children 正常 | 不支持 |
| JSX | dangerouslySetInnerHTML | 没有注入 HTML 的口子; 用宿主返回 token 自己渲染 | 不支持 |
| 类型 | string number boolean void any undefined null | 支持 | |
| 类型 | T[] / Array<T> / Record<string, T> | 数组、映射 | 支持 |
| 类型 | { a: string; b?: number; f(x: T): R } | 对象类型; 可选原始类型 = 零值语义 | 支持 |
| 类型 | interface A extends B, C { … } / type alias | extends 复制基类型的字段(同名字段覆盖) | 支持 |
| 类型 | "a" | "b" / T | undefined | 字面量联合 → string; 可选 | 支持 |
| 类型 | (x: T) => R / Promise<T> | 函数类型; Promise<T> 视为 T | 支持 |
| 类型 | custom generics / tuple / intersection / keyof / typeof / enum / class | 不支持 | |
| Hooks | const [x, setX] = useState(init) | 服务端 = 初值; 客户端 = signal | 支持 |
| Hooks | setX(v) / setX(prev => ...) | 支持 | |
| Hooks | const y = x * 2 | 依赖 signal 的 const 自动是 memo, 不需要 useMemo | 支持 |
| Hooks | useMemo(() => ...) / useEffect(() => ...) | useEffect 只进客户端, 依赖自动追踪 | 支持 |
| Hooks | export const cart = createStore<T>(init) | client 模块的模块级 const(app/stores/cart.client.tsx): 一个 store —— 浏览器里每个字段一个 signal, 服务端是本请求注入的值; init 里缺的字段是零值; 状态必须能 JSON 化(没有函数和 Node) | 支持 |
| Hooks | cart.count / const { count, items } = cart | 读取就是 signal(细粒度绑定、memo、effect); 只在 client 模块; set 之外只读 | 支持 |
| Hooks | cart.set((s) => { s.items.push(x); s.count += 1; }) / cart.set(value) | 在事件处理、effect 和普通函数里调用, 不能在渲染中: s 是写时复制的 draft —— 只有改过的字段通知订阅者, keyed 列表里没动的行保持身份; 传整个值则整体替换 | 支持 |
| Hooks | seed(cart, value) | 在页面或布局默认组件的函数体里、JSX 之前: 本请求的所有岛用这个值渲染, 浏览器里的 store 也从它开始(首屏不闪, 不用再请求) | 支持 |
| Hooks | useRef / useContext / useReducer | 不支持 | |
| 内建 | console.log / JSON.stringify / Math.max min floor ceil round abs sqrt random | 两端都有 | 支持 |
| 内建 | String() Number() Boolean() parseInt parseFloat isNaN encodeURIComponent | 支持 | |
| 内建 | fetch setTimeout document window location history navigator localStorage | 只在客户端, 类型 any | 支持 |
| 内建 | Object.keys / Object.values | 键按字典序(与 Go map 一致, 保证 hydrate 稳定) | 支持 |
| 内建 | Math.pow sign trunc | 支持 | |
| 内建 | redirect(url, status?) / notFound() | 只在服务端页面: 中断渲染, 回 3xx / 404 页(也可以写 return redirect(…)) | 支持 |
| 内建 | Date.now() / Date.parse(iso) / isoDate(ms) | 两端都是毫秒; 用 fmtDate / isoDate 格式化 | 支持 |
| 内建 | Array.from / new Date(...) / Object.assign | 没有构造函数; 日期用数字 + isoDate | 不支持 |
| 数组 | length map filter find findIndex some every includes indexOf lastIndexOf join slice concat forEach | find 没找到给零值, 为假且 === undefined | 支持 |
| 数组 | sort reduce reverse flat at | sort/reduce 与 Go 后端同语义(拷贝, 不改原数组) | 支持 |
| 数组 | push pop shift unshift splice | 原地修改, 作用于变量 / 字段 / 下标(Go 侧取地址); 不能用在 useState 的数组上——用 setXs([...xs, x]) | 支持 |
| 字符串 | length toUpperCase toLowerCase trim includes startsWith endsWith split slice replace replaceAll repeat indexOf charAt | Go 侧按 rune 处理 | 支持 |
| 字符串 | padStart padEnd trimStart trimEnd lastIndexOf at localeCompare toString / number toFixed toString | 按 rune 处理; localeCompare 两端都按码点比较(不做本地化排序) | 支持 |
| 字符串 | matchAll / named groups / sticky regex | 不在 RE2 子集里 | 不支持 |
响应性规则
客户端的更新模型由编译器决定, 没有 hooks 调用顺序这回事
const [n, setN] = useState(0)— n 是 signal; 读 n 的地方编译成n()。const double = n * 2— 初始化表达式读到 signal 的 const 自动是 memo。- JSX 里读到 signal/memo 的文本、属性、条件、列表各自绑一个 effect; 没读到的就是静态的, 服务端也不留标记。
list.map(cb)的响应性只看 list, 不看回调内部; 条件和三元只看条件。- 组件的 props 不是响应式的(和 Solid 一样, 传 signal 的值只在创建时读一次); 需要响应式的内容放进 children 或条件块。
服务端 / 客户端 / 共享
*.server.tsx: 只编成 Go, 能 importhost:*, 永不进浏览器。*.client.tsx: 编成 Go(SSR 单趟)和 JS; 只能import type宿主类型; default 导出是岛。- 无后缀: 共享组件, 两端都编, 不能碰宿主也不能碰 DOM——纯渲染函数。
- 边界由编译器强制: 客户端 import 服务端组件、服务端出现 await, 都是编译错误。