当前位置:首页 > 文章列表 > Golang > Go问答 > Go Goja 执行 Promise 后为什么异步任务没有自动推进

Go Goja 执行 Promise 后为什么异步任务没有自动推进

来源:17golang原创 2026-10-13 15:29:00 0浏览 收藏

我第一次把 Goja 嵌进 Go 服务时,遇到过一个很容易误判的现象:Promise.resolve().then(...) 里的回调能在脚本执行结束时出现,但把相邻逻辑换成 setTimeout(...) 后,任务却像“没有自动推进”。原因不是 Promise 失效,而是 Goja Runtime 里的 job queue 和浏览器、Node.js 的宿主事件循环不是同一个层次。

官方地址:https://github.com/dop251/goja

事件循环实现:https://github.com/dop251/goja_nodejs

只需要 Promise 反应任务时,直接使用 Goja Runtime 即可;需要 setTimeout、setInterval 或持续接收外部任务时,必须由宿主提供事件循环,并把同一个 Runtime 的访问收敛到它负责的执行边界。

先看结论:Promise 回调和定时器不是一回事

解释 goja_nodejs/eventloop 的 Runtime、EventLoop、timer、jobChan、RunOnLoop 与 Stop 边界。
图2:说明 EventLoop 如何承接 Goja Runtime 与定时器任务,帮助定位启动、线程和停止边界;这是原创结构图。
任务类型由谁提供Goja 单独运行时的边界需要的处理
Promise 反应任务JavaScript Promise 与 Runtime job queue顶层脚本调用结束时可以排空已排队的 job让脚本调用正常返回,避免中途被中断
setTimeout/setInterval宿主环境Goja 核心不自带浏览器或 Node.js 的定时器注入定时器并启动事件循环
Go 回调提交的外部任务宿主 Go 程序不会凭空进入 Runtime通过事件循环提供的调度入口提交

Goja 官方 README 也把 setTimeout 和 setInterval 视为宿主需要提供的能力。它们不是 ECMAScript 标准本身,因此“执行了 Promise”并不意味着已经拥有一套会等待计时器、网络或文件事件的循环。

Goja Runtime、Promise job queue、RunString 与宿主定时器能力的静态关系图
图1:结构图区分 Goja 内部 Promise 反应任务与宿主定时器能力,不是运行截图或执行证据。

我排查时先看 RunString 的返回边界

Goja 的 Runtime 会维护 Promise 相关的 job queue。顶层 JavaScript 调用正常离开 Runtime 时,内部会循环取出并执行当前排队的 job;因此,一个已经排入队列的 Promise 回调有机会在 RunString 返回前处理完。

下面的代码只演示“Promise 回调属于 Runtime 自己能处理的任务”。它没有把结果包装成定时器,也没有假设存在浏览器全局对象:

package main

import (
	"fmt"

	"github.com/dop251/goja"
)

func main() {
	vm := goja.New()
	vm.Set("report", func(call goja.FunctionCall) goja.Value {
		// Go 回调只记录脚本传来的消息,不把 Runtime 带出当前调用。
		fmt.Println(call.Argument(0).String())
		return goja.Undefined()
	})

	_, err := vm.RunString(`
		// Promise 反应任务进入 Goja 的内部 job queue。
		Promise.resolve("Promise 已排队").then(function (message) {
			report(message);
		});
	`)
	if err != nil {
		// 语法或脚本异常仍然要在 Go 层明确处理。
		panic(err)
	}
}

这里的关键不是把 Promise 当成 Go 协程,而是理解调用边界:脚本顶层执行结束后,Goja 有机会清空 Promise job。这个机制只覆盖已经存在的 JavaScript job,不会为不存在的 setTimeout 自动创建计时器。

出现 setTimeout 时,问题已经移到宿主层

如果脚本里直接写下面的代码,而宿主没有先注入 setTimeout,排查方向就不该继续停留在 Promise 上:

// 这段脚本依赖宿主注入 setTimeout,而不是 Goja 核心自动提供。
setTimeout(function () {
	// 计时器回调只有在宿主事件循环继续运行时才有机会执行。
	console.log("timer fired");
}, 0);

即使宿主自己实现了一个 setTimeout 函数,也还要解决两个问题:计时器到期后怎样把回调送回 Runtime,以及谁负责让这个等待过程持续存在。只注册函数、不运行循环,仍然会得到“任务没有自动推进”的现场。

接入 goja_nodejs/eventloop 后,谁负责继续推进

官方的 goja_nodejs 仓库提供了面向 Goja 的 Node.js 兼容模块,其中 eventloop 会创建 Runtime,注册 setTimeout、setInterval、setImmediate 等宿主能力,并把到期任务送入循环处理。最小的宿主边界可以这样组织:

package main

import (
	"github.com/dop251/goja"
	"github.com/dop251/goja_nodejs/eventloop"
)

func main() {
	loop := eventloop.NewEventLoop()
	// Run 会执行脚本,并持续处理循环中尚未完成的延迟任务。
	loop.Run(func(vm *goja.Runtime) {
		_, err := vm.RunString(`
			// Promise job 与 timer 都由同一个事件循环承接。
			Promise.resolve().then(function () { console.log("promise job"); });
			setTimeout(function () { console.log("timer job"); }, 0);
		`)
		if err != nil {
			// 脚本初始化失败时,把错误留在宿主启动边界处理。
			panic(err)
		}
	})
}

示例中的 Run 适合“一次脚本执行,等到延迟任务耗尽后返回”的场景。如果服务需要长期运行,则使用 Start 让循环在后台工作,把外部任务通过 RunOnLoop 投递进去,并在服务退出时调用 Stop 或 Terminate。这几个动作要有清晰的生命周期归属,否则很容易在服务已经停止后继续向 Runtime 提交工作。

把 Runtime 限定在事件循环所属的 goroutine

Goja 官方 README 明确说明,一个 goja.Runtime 不能被多个 goroutine 同时使用。事件循环的价值不只是“等待定时器”,还把脚本执行、Promise job、定时器回调和宿主投递统一到一个可控的执行边界。

func submit(loop *eventloop.EventLoop, message string) bool {
	return loop.RunOnLoop(func(vm *goja.Runtime) {
		// 外部 goroutine 不直接调用 vm,而是把工作交给 loop 所属的执行边界。
		vm.Set("message", message)
		_, err := vm.RunString(`
			// 进入 Runtime 后再创建 Promise,避免跨 goroutine 触碰脚本对象。
			Promise.resolve(message).then(function (value) {
				console.log(value);
			});
		`)
		if err != nil {
			// 生产代码应把错误写入宿主日志或错误通道。
			fmt.Println(err)
		}
	})
}

这类包装函数的重点是返回 bool:事件循环已被 Terminate 后,投递可能失败,调用方不能再把提交成功当成事实。真实服务还要配合关闭顺序,先停止新任务进入,再等待已经进入循环的工作收尾。

四种“没有推进”的现场,应该分别处理

  1. Promise 回调有输出,setTimeout 没有输出:先检查宿主是否注入了 setTimeout,再检查事件循环是否仍在运行;不要继续修改 Promise 链。
  2. 两种回调都没有输出:检查 RunString 是否提前返回错误,或者宿主是否在回调产生前就中断了 Runtime。
  3. 使用 Start 后偶尔没有任务:检查是否在 Start 之前就投递,是否在初始化完成前调用了 Stop,以及是否误用了另一个 Runtime。
  4. 外部 goroutine 直接调用 vm.RunString:把调用改为 RunOnLoop 或等价的单线程队列,不要用“偶尔能跑通”证明这种访问是安全的。

我现在会把排查顺序固定成“任务类型—宿主能力—循环生命周期—Runtime 所属线程”。这样可以把“Promise 没有自动推进”拆成可观察的四个边界,而不是盲目增加等待时间。

几个容易混淆的问题

Promise.then 一定会在 RunString 返回前执行吗?

对正常结束的顶层调用,Goja 会在离开 Runtime 时处理已排队的 Promise job;但脚本若被中断、抛出未处理错误,或宿主在异常路径提前结束,就不能把它当成业务层的完成通知。

Goja 为什么没有内置 setTimeout?

setTimeout 依赖操作系统计时器和宿主调度,它不是 ECMAScript 标准的一部分。Goja 把这部分留给宿主,宿主可以自己注入,也可以使用 goja_nodejs/eventloop 这样的配套实现。

使用 eventloop 后还能把 Runtime 传给其它 goroutine 吗?

不建议。把数据通过 RunOnLoop 投递到循环内部,在回调中读写 Runtime;不要跨线程保存或操作由该 Runtime 创建的 JavaScript Value。

结论:先找缺失的宿主边界

Go Goja 执行 Promise 后,异步任务没有“自动推进”,多数时候是把两类任务混在了一起:Promise 反应任务属于 Runtime 的 job queue,而定时器和持续等待属于宿主事件循环。直接使用 Goja 时,先确认顶层脚本能正常返回;需要 setTimeout 或外部任务时,接入 event loop,并把 Runtime、任务投递和停止顺序放进同一个生命周期设计里。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go Goja 怎么在脚本环境提供 Go 回调函数Go Goja 怎么在脚本环境提供 Go 回调函数
上一篇
Go Goja 怎么在脚本环境提供 Go 回调函数
Go Goja 脚本被 Interrupt 后再次执行怎么办
下一篇
Go Goja 脚本被 Interrupt 后再次执行怎么办
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    460次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    521次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    539次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    487次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    307次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码