- 后端
- 即时通讯
- 社交
- 游戏开发
【免费下载链接】nakama
Scalable open-source game backend server: multiplayer, matchmaking, leaderboards, chat, and social features for games.
导读
本文以 Nakama 仓库中vendor/github.com/dop251/goja/ast包的 API 文档(README.markdown)为核心骨架,系统梳理 goja 这一纯 Go 实现的 ECMAScript 引擎在 Nakama 游戏后端中的 JavaScript 抽象语法树(AST)节点体系:包括 Node/Expression/Statement/Declaration 四大接口、数十种节点结构体、基于file.Idx的源码位置追踪机制,以及它们如何被 goja 的 parser 与 compiler 消费,并最终在 Nakama 的 JS 运行时(server/runtime_javascript.go)中用于解析游戏服务器模块源码。读完本文,你将掌握 goja AST 的完整节点清单、字段语义与位置编码原理,并能读懂 Nakama 加载 JS 模块时的goja.Parse解析链路。
说明:本文中的接口与结构体定义均取自 Nakama 仓库内 goja 库的实际源码与文档,属于该引擎的通用 API;文中另结合 Nakama 服务端代码给出其在游戏后端中的具体应用场景。
一、背景:goja 与 AST 在 Nakama 中的位置
Nakama 是一款可扩展的开源游戏后端服务器,支持用 Lua、TypeScript/JavaScript 编写服务端模块。其中 JavaScript 运行时基于 goja 实现——一个纯 Go 编写、无 cgo 依赖的 ECMAScript 引擎。goja 的解析与编译流程为:
- 词法/语法解析:
parser包将 JS 源码解析为 AST(*ast.Program); - AST 编译:
compiler包将 AST 编译为字节码; - 虚拟机执行:
vm包解释执行字节码。
在 Nakama 中,这一流程被封装在 server/runtime_javascript.go 里——加载 JS 模块时先调用goja.Parse(modName, string(content))生成 AST,再调用goja.Compile(modName, string(content), true)编译,随后将Program与Ast一并放入模块缓存:
modAst, _ := goja.Parse(modName, string(content)) prg, err := goja.Compile(modName, string(content), true) if err != nil { logger.Error("Could not compile JavaScript module", zap.String("module", modName), zap.Error(err)) return nil, err } moduleCache.Add(&RuntimeJSModule{ Name: modName, Path: absEntrypoint, Program: prg, Ast: modAst, })也就是说,Nakama 每次启动加载 JS 模块时,都会先经由ast包定义的节点类型构建一棵完整的语法树,再进行编译与缓存。ast包正是这棵语法树的"骨架定义"。
ast包本身只负责声明节点类型,不含解析逻辑(解析在parser包)、不含求值逻辑(求值在vm包)。其包注释与文档均带有如下警告,读者在自行扩展或依赖这些接口时需要注意:
The parser and AST interfaces are still works-in-progress (particularly where node types are concerned) and may change in the future. (parser 与 AST 接口仍在演进中,尤其是节点类型部分,未来可能发生变化。)
二、AST 的三大核心接口与声明体系
ast包的所有节点都可以归入三棵类型树,全部定义在 vendor/github.com/dop251/goja/ast/node.go 中。
2.1 Node 接口:一切节点的根
type Node interface { Idx0() file.Idx // The index of the first character belonging to the node Idx1() file.Idx // The index of the first character immediately after the node }Node是所有 AST 节点的根基,每个节点都必须实现两个方法:
Idx0():返回节点第一个字符在源文件中的位置索引;Idx1():返回节点最后一个字符之后第一个位置(即开区间终点)的索引。
两者共同定义了节点在源码中占据的字符区间[Idx0, Idx1),这是 AST 节点与源码文本保持可追溯映射的关键。
2.2 Expression / Statement / Declaration 接口
type Expression interface { Node // contains filtered or unexported methods } type Statement interface { Node // contains filtered or unexported methods } type Declaration interface { Node // contains filtered or unexported methods }- Expression(表达式):所有表达式节点实现此接口,例如字面量、二元运算、函数调用、成员访问等;
- Statement(语句):所有语句节点实现此接口,例如
if、for、return等; - Declaration(声明):所有声明节点实现此接口,如
var变量声明。
接口中"filtered or unexported methods"对应源码中用于类型约束的私有标记方法。在 node.go 中,Expression接口实际声明为:
type Expression interface { Node _expressionNode() }私有方法_expressionNode()使接口只能被包内声明的类型实现,外部无法伪造节点类型,从而保证了类型系统的封闭性。Statement、Declaration、Property、Pattern、ClassElement、ConciseBody等接口均采用同样的"私有标记方法"约束模式(见 node.go 与 node.go)。
2.3 Program:AST 的根节点
type Program struct { Body []Statement DeclarationList []Declaration File *file.File }parser.ParseFile的返回值就是*ast.Program,它是整棵语法树的根:
Body:顶层语句序列;DeclarationList:顶层声明列表(如var/let/const与函数声明);File:关联的源文件对象,用于把file.Idx换算成行/列位置。
Program.Idx0()返回Body[0].Idx0()(首个语句的起点),Program.Idx1()返回Body[len(Body)-1].Idx1()(末条语句的终点),见 node.go 与 node.go。
三、源码位置追踪:file.Idx 与 Idx0/Idx1 的实现
AST 节点的位置字段几乎全部使用file.Idx类型。在 vendor/github.com/dop251/goja/file/file.go 中:
// Idx is a compact encoding of a source position within a file set. // It can be converted into a Position for a more convenient, but much // larger, representation. type Idx int type Position struct { Filename string // The filename where the error occurred, if any Line int // The line number, starting at 1 Column int // The column number, starting at 1 (The character count) }Idx是文件集内的紧凑偏移编码,FileSet.Position(idx)可将其换算为Position{Line, Column}。FileSet管理多个File,每个File持有src与base(起始偏移),新增文件时base递推为上一个文件base + len(src) + 1(file.go),从而保证所有文件的Idx全局唯一。文档中Node接口的两条注释也印证了这一设计:
Idx0() file.Idx:The index of the first character belonging to the node(节点首字符索引);Idx1() file.Idx:The index of the first character immediately after the node(节点之后紧邻的字符索引)。
3.1 Idx0 的几种计算策略
从 node.go 的Idx0()实现可以归纳出四类策略:
| 策略 | 代表节点 | 实现 |
|---|---|---|
| 返回自己的关键字/定界符位置 | IfStatement、WhileStatement、FunctionLiteral、ArrayLiteral、ObjectLiteral | return self.If/self.While/self.Function/self.LeftBracket/self.LeftBrace |
| 委托给左操作数/被调用者 | AssignExpression、BinaryExpression、CallExpression、DotExpression | return self.Left.Idx0()/self.Callee.Idx0() |
| 前缀/后缀差异 | UnaryExpression | 后缀时返回Operand.Idx0(),前缀时返回运算符Idx(node.go) |
| 复合节点取首元素 | SequenceExpression、Program | return self.Sequence[0].Idx0() |
3.2 Idx1 的几个关键实现
Idx1()的实现同样值得推敲(node.go):
- 字面量类:如
BooleanLiteral、NumberLiteral、StringLiteral、RegExpLiteral用Idx + len(Literal)计算,NullLiteral固定Idx + 4("null"四个字符),ThisExpression固定Idx + 4、SuperExpression固定Idx + 5; - 括号类:
ArrayLiteral为RightBracket + 1、CallExpression为RightParenthesis + 1、BlockStatement为RightBrace + 1; - 后置运算符:
UnaryExpression在后缀时返回Operand.Idx1() + 2(++/--占两个字符,node.go); - 条件分支:
IfStatement有Alternate时返回Alternate.Idx1(),否则返回Consequent.Idx1();TryStatement依次优先Finally、Catch、Body(node.go、node.go); - 变长声明列表:
VariableStatement、LexicalDeclaration、ForLoopInitializerVarDeclList均取List[len(List)-1].Idx1();VariableDeclaration在列表为空时退化为Var + 3("var"三个字符,node.go)。
正是这套统一的区间计算规则,让 goja 能把每个 AST 节点精确映射回原始源码片段,支持FunctionLiteral.Source之类的源码还原能力。
四、字面量节点(Literal)全解析
字面量是 AST 中最基础的表达式节点。文档列出了五种核心字面量,源码中还存在模板字面量等补充类型。
4.1 BooleanLiteral(布尔字面量)
type BooleanLiteral struct { Idx file.Idx Literal string // 源码文本,如 "true" Value bool // 求值后的布尔值 }同时保留原始文本与解析后的值,Idx1()用Idx + len(Literal)定位。
4.2 NullLiteral(空字面量)
type NullLiteral struct { Idx file.Idx Literal string // 固定为 "null" }无Value字段(null即空值),Idx1()固定返回Idx + 4。
4.3 NumberLiteral(数字字面量)
type NumberLiteral struct { Idx file.Idx Literal string Value interface{} }Value类型为interface{},因为 goja 内部数字可能以int64、float64等不同形态存储,解析阶段统一为接口类型,运行时再按需取值。
4.4 StringLiteral(字符串字面量)
type StringLiteral struct { Idx file.Idx Literal string Value string }Literal是包含引号的原始源码文本,Value是转义处理后的实际字符串值。
4.5 RegExpLiteral(正则字面量)
type RegExpLiteral struct { Idx file.Idx Literal string // 完整的正则源码文本 Pattern string // 模式部分 Flags string // 标志位,如 "gim" Value string }Pattern与Flags从Literal中拆分,Value字段在源码中保留(文档中列出的该类型含Value string字段;node.go 中该字段已移除,仅保留Pattern/Flags,佐证了包注释中"节点类型仍在演进"的警告)。
4.6 ArrayLiteral(数组字面量)
type ArrayLiteral struct { LeftBracket file.Idx RightBracket file.Idx Value []Expression // 元素列表,含 SpreadElement 展开节点 }元素是表达式序列,支持[...spread]展开(SpreadElement内嵌Expression)。Idx0()返回LeftBracket,Idx1()返回RightBracket + 1。
4.7 ObjectLiteral 与 Property(对象字面量)
type ObjectLiteral struct { LeftBrace file.Idx RightBrace file.Idx Value []Property } type Property struct { Key string Kind string Value Expression }对象成员由Property描述:Key为属性名,Kind为属性种类,Value为属性值表达式。文档中Kind是string,源码中已演进为强类型枚举(node.go):
type PropertyKind string const ( PropertyKindValue PropertyKind = "value" // 普通数据属性 PropertyKindGet PropertyKind = "get" // getter PropertyKindSet PropertyKind = "set" // setter PropertyKindMethod PropertyKind = "method" // 方法(ES6 简写方法) )同时在源码中,Property已细分为两个实现类型(node.go):
type PropertyShort struct { Name Identifier Initializer Expression // 例如 { x = 1 } 中的默认值 } type PropertyKeyed struct { Key Expression Kind PropertyKind Value Expression Computed bool // 是否为 [expr] 计算属性名 }4.8 TemplateLiteral 与 TemplateElement(模板字符串)
源码为 ES6 模板字符串补充了节点(node.go):
type TemplateElement struct { Idx file.Idx Literal string Parsed unistring.String // 转义处理后的文本 Valid bool } type TemplateLiteral struct { OpenQuote file.Idx CloseQuote file.Idx Tag Expression // 可选,带标签的模板 Elements []*TemplateElement Expressions []Expression // 插值表达式 ${...} }TemplateElement负责静态文本段,Expressions负责动态插值段,两者按序交错构成完整模板。
五、表达式节点(Expression)分类详解
表达式节点数量最多,可按语义分为若干族。
5.1 标识符与成员访问
type Identifier struct { Name string Idx file.Idx } type DotExpression struct { Left Expression // 对象 Identifier Identifier // 属性名,如 obj.name } type BracketExpression struct { Left Expression // 对象 Member Expression // 下标表达式,如 obj[key] LeftBracket file.Idx RightBracket file.Idx }DotExpression(点访问)与BracketExpression(方括号访问)是属性访问的两种形态。DotExpression.Idx1()委托给Identifier.Idx1(),即Idx + len(Name)。
源码中还包含私有属性访问(node.go)与可选链:
type PrivateDotExpression struct { Left Expression Identifier PrivateIdentifier // #field } type OptionalChain struct { Expression } type Optional struct { Expression }5.2 运算类:Binary / Assign / Unary / Conditional / Sequence
type BinaryExpression struct { Operator token.Token // + - * / && || == < > ... Left Expression Right Expression Comparison bool // 是否为比较运算(==、!=、<、> 等) } type AssignExpression struct { Operator token.Token // = += -= *= ... Left Expression Right Expression } type UnaryExpression struct { Operator token.Token // ! ~ - + typeof void delete Idx file.Idx // If a prefix operation Operand Expression Postfix bool // 是否为后置运算(x++ / x--) } type ConditionalExpression struct { Test Expression // 条件 Consequent Expression // 真分支 Alternate Expression // 假分支 } type SequenceExpression struct { Sequence []Expression // 逗号表达式,如 (a, b, c) }各运算符来自token.Token类型(goja 的 token 包)。BinaryExpression的Comparison标志在编译期可用于区分比较运算与算术运算的求值路径。
5.3 调用与构造:Call / New
type CallExpression struct { Callee Expression // 被调用者,可能是函数、方法链等 LeftParenthesis file.Idx ArgumentList []Expression RightParenthesis file.Idx } type NewExpression struct { New file.Idx Callee Expression LeftParenthesis file.Idx ArgumentList []Expression RightParenthesis file.Idx }两者的Idx1()均为RightParenthesis + 1;NewExpression的Idx1()在无参数列表时回退到Callee.Idx1()(node.go),对应new Foo无括号写法。
5.4 This / Super / MetaProperty / Yield / Await
type ThisExpression struct { Idx file.Idx } type SuperExpression struct { Idx file.Idx } type MetaProperty struct { Meta, Property *Identifier // new.target 等 Idx file.Idx } type YieldExpression struct { Yield file.Idx Argument Expression Delegate bool // 是否为 yield* 委托 } type AwaitExpression struct { Await file.Idx Argument Expression }SuperExpression支持类继承中的super调用,MetaProperty支持new.target,YieldExpression/AwaitExpression对应生成器与 async/await 语法(后两者是文档未列出、源码已支持的类型,再次印证 AST 持续演进)。
5.5 函数与类字面量
type FunctionLiteral struct { Function file.Idx Name *Identifier // 可为 nil(匿名函数) ParameterList *ParameterList Body Statement Source string // 函数体原始源码 DeclarationList []Declaration } type ParameterList struct { Opening file.Idx List []*Identifier Closing file.Idx }FunctionLiteral保存了Source字段用于源码级还原;在 node.go 中,Body类型收紧为*BlockStatement,并新增Async, Generator bool标志位,ParameterList.List也升级为[]*Binding以支持解构与默认参数。
源码补充的箭头函数与类字面量(node.go、node.go):
type ArrowFunctionLiteral struct { Start file.Idx ParameterList *ParameterList Body ConciseBody // 表达式体或块体 Source string DeclarationList []*VariableDeclaration Async bool } type ClassLiteral struct { Class file.Idx RightBrace file.Idx Name *Identifier SuperClass Expression Body []ClassElement Source string }ConciseBody有两个实现:ExpressionBody(x => expr)与BlockStatement(x => { ... })。ClassElement则有FieldDefinition(实例/静态字段)、MethodDefinition(方法/getter/setter)、ClassStaticBlock(静态初始化块)三类实现。
5.6 错误占位与解构模式
type BadExpression struct { From file.Idx To file.Idx }BadExpression用于解析失败时占位,保留失败区间。解构相关(node.go):
type BindingTarget interface { Expression _bindingTarget() } type Pattern interface { BindingTarget _pattern() } type Binding struct { Target BindingTarget Initializer Expression // 默认值 } type ArrayPattern struct { LeftBracket file.Idx RightBracket file.Idx Elements []Expression Rest Expression } type ObjectPattern struct { LeftBrace file.Idx RightBrace file.Idx Properties []Property Rest Expression }Identifier、ArrayPattern、ObjectPattern、BadExpression均可作为绑定目标,支持const {a, b} = obj、const [x, ...rest] = arr等 ES6 解构语法。
六、语句节点(Statement)分类详解
6.1 块与空语句
type BlockStatement struct { LeftBrace file.Idx List []Statement RightBrace file.Idx } type EmptyStatement struct { Semicolon file.Idx // 独立分号 ; }BlockStatement是所有复合语句的"容器";EmptyStatement表示空操作。
6.2 变量声明语句与表达式语句
type VariableStatement struct { Var file.Idx List []Expression // var 声明的绑定列表 } type ExpressionStatement struct { Expression Expression // 如 foo(); a = 1; }在源码中VariableStatement.List已升级为[]*Binding(node.go),并新增LexicalDeclaration(let/const,带Token token.Token区分二者,node.go):
type LexicalDeclaration struct { Idx file.Idx Token token.Token // let 或 const List []*Binding }6.3 控制流:If / Switch / Branch / Labelled
type IfStatement struct { If file.Idx Test Expression // 条件 Consequent Statement // 真分支 Alternate Statement // 假分支,可为 nil } type SwitchStatement struct { Switch file.Idx Discriminant Expression // switch 的判别表达式 Default int // default 子句在 Body 中的下标,-1 表示无 Body []*CaseStatement } type CaseStatement struct { Case file.Idx Test Expression // case 表达式;default 时为 nil Consequent []Statement } type BranchStatement struct { Idx file.Idx Token token.Token // break / continue Label *Identifier // 带标签的 break/continue } type LabelledStatement struct { Label *Identifier Colon file.Idx Statement Statement }SwitchStatement.Default记录default分支在Body中的索引(无则 -1),BranchStatement统一表达break/continue(含标签形式),LabelledStatement支持label: statement语法。
6.4 循环:For / ForIn / ForOf / While / DoWhile
type ForStatement struct { For file.Idx Initializer Expression // 初始化表达式 Update Expression // 步进表达式 Test Expression // 条件 Body Statement } type ForInStatement struct { For file.Idx Into Expression // 迭代目标 Source Expression // 被遍历对象 Body Statement } type WhileStatement struct { While file.Idx Test Expression Body Statement } type DoWhileStatement struct { Do file.Idx Test Expression Body Statement }源码中ForInStatement之外还新增了ForOfStatement(node.go),两者共用ForInto抽象(ForIntoVar、ForDeclaration、ForIntoExpression三个实现,分别对应for (var x in ...)、for (const x of ...)、for (x of ...));ForStatement.Initializer也抽象为ForLoopInitializer接口(表达式 / var 声明列表 / let·const 声明三种实现)。
6.5 跳转与异常:Return / Throw / Try / Catch
type ReturnStatement struct { Return file.Idx Argument Expression // 可为 nil(裸 return) } type ThrowStatement struct { Throw file.Idx Argument Expression } type TryStatement struct { Try file.Idx Body Statement Catch *CatchStatement // 可为 nil Finally Statement // 可为 nil } type CatchStatement struct { Catch file.Idx Parameter *Identifier // 异常变量,如 catch (e) Body Statement }TryStatement的Catch/Finally均为指针,允许try...catch、try...finally、完整try...catch...finally三种组合;ReturnStatement.Idx1()在无参数时固定Return + 6("return"六个字符)。
6.6 其他:With / Debugger / Bad
type WithStatement struct { With file.Idx Object Expression Body Statement } type DebuggerStatement struct { Debugger file.Idx // debugger; } type BadStatement struct { From file.Idx To file.Idx }WithStatement对应非严格模式的with语句;BadStatement与BadExpression一样是解析错误占位。
七、声明节点(Declaration):函数与类
type FunctionDeclaration struct { Function *FunctionLiteral // function foo() {} } type VariableDeclaration struct { Var file.Idx List []*VariableExpression // 单个 var 声明中的多个变量 } type VariableExpression struct { Name string Idx file.Idx Initializer Expression // var x = 1 }FunctionDeclaration把函数字面量包装为声明(区别于作为表达式的FunctionLiteral);VariableDeclaration是var a, b = 1, c;这类多变量声明的单元。源码中VariableDeclaration.List已升级为[]*Binding,并新增ClassDeclaration(node.go):
type ClassDeclaration struct { Class *ClassLiteral // class Foo {} }八、parser 包:AST 的入口
ast包不负责解析,解析由同仓库的parser包完成。ast/README.markdown的姊妹文档 vendor/github.com/dop251/goja/parser/README.markdown 给出了使用范例:
import ( "github.com/dop251/goja/parser" ) filename := "" // A filename is optional src := ` // Sample xyzzy example (function(){ if (3.14159 > 0) { console.log("Hello, World."); return; } var xyzzy = NaN; console.log("Nothing happens."); return xyzzy; })(); ` // Parse some JavaScript, yielding a *ast.Program and/or an ErrorList program, err := parser.ParseFile(nil, filename, src, 0)核心函数签名:
func ParseFile(fileSet *file.FileSet, filename string, src interface{}, mode Mode) (*ast.Program, error)fileSet为nil时解析不带文件集;非nil时先把filename与src加入文件集(这也是Program.File与file.Idx位置换算的基础);filename可选,仅用于错误标注;src可以是字符串、字节切片、bytes.Buffer或io.Reader,必须是 UTF-8 编码;- 返回值
*ast.Program即本文第二节介绍的根节点。
另有便捷函数ParseFunction(parameterList, body string) (*ast.FunctionLiteral, error),把逗号分隔的参数列表与函数体直接解析为ast.FunctionLiteral,适合在运行时动态构造小函数。
在 Nakama 中,goja.Parse(内部封装parser.ParseFile)被用于模块预检与 AST 缓存(见第一节的runtime_javascript.go代码),随后goja.Compile才真正编译执行。这种"先解析成 AST、再编译"的两段式设计,也让 Nakama 在模块加载阶段就能发现语法错误并记录日志("Could not compile JavaScript module")。
九、AST 在 Nakama 中的完整调用链与测试佐证
9.1 运行时调用链
从源码结构可以梳理出 Nakama 中 JS 模块的 AST 相关完整链路:
- Nakama 启动时扫描模块目录(
data/modules/*.js),读取 JS 文件内容(runtime_javascript.go 使用os.ReadFile); - 以模块文件名调用
goja.Parse(modName, content)生成*ast.Program(runtime_javascript.go); - 调用
goja.Compile(modName, content, true)编译为可执行程序(runtime_javascript.go); - 将
Program与Ast一起缓存进RuntimeJSModule(runtime_javascript.go); - 运行时通过
goja.New()创建虚拟机实例执行(如 runtime_javascript.go、runtime_javascript_match_core.go)。
9.2 测试用例佐证
仓库内的测试也直接体现了"AST 先于执行"的编译流程,例如 server/runtime_javascript_test.go:
r := goja.New() p, _ := goja.Compile("test", ` // ... JS 测试代码 ... `) r.RunProgram(p)同样的模式还出现在 runtime_javascript_logger_test.go、runtime_javascript_match_core.go 等处——先Compile(内部含解析阶段)再RunProgram,验证了编译产物(即 AST 的下游)是虚拟机执行的直接输入。
9.3 与其他模块的边界
ast↔parser:parser.ParseFile产出ast.Program;ast↔token:表达式/语句中的运算符、break/continue、let/const等均由token.Token表示;ast↔file:所有位置字段为file.Idx,可经FileSet.Position转为行/列;ast↔compiler/vm:compiler遍历 AST 生成字节码,vm执行。
十、演进提示与实践建议
- 接口不稳定:官方文档与包注释均声明 parser/AST 接口(尤其是节点类型)仍在演进。本文中可明显观察到文档(godocdown 生成)与 node.go 的差异:
Property被PropertyShort/PropertyKeyed取代、ParameterList.List由[]*Identifier变为[]*Binding、RegExpLiteral去掉了Value字段、新增了大量 ES6+ 节点(ClassLiteral、ArrowFunctionLiteral、TemplateLiteral、ForOfStatement、OptionalChain等)。任何基于ast包的二次开发都应锁定 goja 版本并保持更新跟进; - AST 仅用于结构表示:若需对 Nakama 的 JS 模块做静态分析(如遍历函数声明、收集依赖),可在模块缓存命中
Ast字段(server/runtime_javascript.go中RuntimeJSModule.Ast)后自行实现Node遍历器; - 位置信息是排错利器:
file.Idx与FileSet.Position配合,能把编译/运行错误准确定位到文件:行:列,Nakama 的"Could not compile JavaScript module"错误日志正是依赖这一机制。
相关文件索引
- AST 节点类型定义:vendor/github.com/dop251/goja/ast/node.go
- AST 包 API 文档:vendor/github.com/dop251/goja/ast/README.markdown
- 解析器包与示例:vendor/github.com/dop251/goja/parser/README.markdown
- 位置编码与文件集:vendor/github.com/dop251/goja/file/file.go
- Nakama JS 运行时(Parse/Compile/缓存):server/runtime_javascript.go
- Nakama JS 运行时测试:server/runtime_javascript_test.go、server/runtime_javascript_logger_test.go
- 后端
- 即时通讯
- 社交
- 游戏开发
【免费下载链接】nakama
Scalable open-source game backend server: multiplayer, matchmaking, leaderboards, chat, and social features for games.
相关推荐
深入解析Goja:纯Go实现的JavaScript引擎
深入解析Goja:纯Go实现的JavaScript引擎 Goja是一个用纯Go语言实现的ECMAScript 5.1+ JavaScript引擎,专注于标准兼容
编程语言语言运行时只需4步!用PowerShell脚本将Windows 11系统精简50%的终极指南
只需4步!用PowerShell脚本将Windows 11系统精简50%的终极指南 你是否厌倦了Windows 11的臃肿?预装应用占用大量空间,后台服务拖慢系
操作系统深入解析 k6 内置 JavaScript 引擎 sobek 的 parser 包:从源码到 AST 的完整指南
深入解析 k6 内置 JavaScript 引擎 sobek 的 parser 包:从源码到 AST 的完整指南 k6 是一个使用 Go 编写、以 JavaSc
测试开发工具CI/CD
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考