登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

Go Goja 怎么在脚本环境提供 Go 回调函数

来源:17golang原创

时间:2026-10-13 15:28:06 340浏览 收藏

在 Goja 中,可以用 vm.Set("函数名", Go函数) 把宿主回调提供给 JavaScript。参数约定简单时直接注册普通 Go 函数;需要读取参数个数、控制类型规则或主动抛出脚本异常时,使用 func(goja.FunctionCall) goja.Value 原生回调。两种写法都应先完成注册,再运行使用该名称的脚本。

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

普通函数和原生回调怎么选

下面用一个订单标签任务作例子:脚本提供渠道名,Go 负责去掉首尾空白、加上业务前缀并拒绝空名称。这是同步函数调用,不需要浏览器页面,也不需要把 Go 服务变成 HTTP 接口。

两种签名的映射规则可参阅 Goja ToValue 文档。普通函数由 Goja 自动转换参数和返回值;原生签名把参数处理交给回调实现。这里讨论的是 Go 宿主内的 JavaScript 引擎,而不是浏览器插件。

需求适合的写法自己负责什么
明确的字符串入参和结果func(string) (string, error)业务规则、返回错误
严格限制参数个数和类型func(FunctionCall) Value参数检查、值转换、脚本异常

先注册一个最小业务回调

以下完整程序把 makeLabel 注册到运行时。脚本传入渠道名称,再把结果作为表达式返回给 Go。示例是原创用法,未在本文环境执行;按代码逻辑,带空格的输入应得到去除空白后的标签。

package main

import (
    "fmt"
    "strings"

    "github.com/dop251/goja"
)

func makeLabel(channel string) (string, error) {
    // 业务约束留在宿主函数,脚本只负责提交名称。
    channel = strings.TrimSpace(channel)
    if channel == "" {
        return "", fmt.Errorf("渠道名不能为空")
    }
    return "渠道/" + channel, nil
}

func main() {
    vm := goja.New()
    // 注册失败后不要继续运行依赖此函数的脚本。
    if err := vm.Set("makeLabel", makeLabel); err != nil {
        fmt.Println("注册失败:", err)
        return
    }
    value, err := vm.RunString(`
        // 调用宿主回调,并把最后一个表达式交回Go。
        makeLabel("  门店  ");
    `)
    if err != nil {
        fmt.Println("脚本失败:", err)
        return
    }
    fmt.Println(value.String())
}

这里的 error 是函数契约的一部分,而不是要让 JavaScript 接收的第二个数组元素。回调返回非空错误时,脚本会遇到异常;脚本不捕获该异常时,Go 应处理运行脚本返回的错误。因此不要忽略注册和运行这两处错误。

JavaScript调用名称与Go宿主函数注册关系
静态说明图:脚本域中的makeLabel名称对应宿主域中的业务函数;连线表达注册与依赖关系,不是执行截图或运行证据。

用 FunctionCall 明确参数和异常契约

如果不希望数字等输入经过自动转换后被当作渠道名,可以把上述注册代码替换成下面片段。它仍调用同一个 makeLabel 业务函数,但把入口改成 makeLabelStrict:只允许一个原始字符串参数,不接收 String 包装对象。需要引入的包仍与完整程序相同。

// 此片段放在main中,vm创建之后;业务函数沿用上例。
err := vm.Set("makeLabelStrict", func(call goja.FunctionCall) goja.Value {
    if len(call.Arguments) != 1 {
        panic(vm.NewTypeError("需要且仅需要一个渠道名"))
    }
    // Export后的Go类型用于本例的原始字符串入口约束。
    channel, ok := call.Argument(0).Export().(string)
    if !ok {
        panic(vm.NewTypeError("渠道名必须是字符串"))
    }
    label, err := makeLabel(channel)
    if err != nil {
        // 抛出脚本错误对象,而不是使用普通字符串panic。
        panic(vm.NewGoError(err))
    }
    return vm.ToValue(label)
})
if err != nil {
    fmt.Println("注册失败:", err)
    return
}

FunctionCall 携带调用时的 This 和 Arguments。本例不需要接收者,所以只读取参数。入口检查失败时抛出 TypeError,业务规则失败时用 NewGoError 包装;成功结果用当前运行时的 ToValue 转换。不要把任意 Go panic 当成脚本异常约定。

脚本端可以按自己的业务策略捕获错误。下面片段用于上面严格回调注册成功后:捕获只代表已经处理异常,不会自动撤销回调内部发生的外部副作用。

// 失败信息转为业务结果;示例不依赖浏览器console。
var result;
try {
    result = makeLabelStrict(27);
} catch (err) {
    result = "标签未生成:" + err.message;
}
result;
FunctionCall参数检查与脚本错误对象关系
静态说明图:参数域、宿主业务域和脚本值域分开表示,重点看严格入口、业务错误和返回值各自对应的对象;不是顺序流程或实测结果。

宿主环境和并发有哪些边界

Goja 官方仓库说明它不是完整浏览器或 Node.js 环境,计时器等宿主能力需要另外提供。因而注册业务回调不会自动带来 DOM、浏览器网络接口或 Node 模块;脚本如果依赖这些能力,应提供明确适配层,或减少脚本对宿主 API 的依赖,而不是假定存在。

普通函数适合稳定、简单的接口,原生回调适合更明确的参数约束。不要仅凭这两个片段宣布某一种写法性能更好;如果业务函数本身很耗时,改变映射签名也不能消除它的耗时。原生回调同样不会自动变成异步任务。

同一个 Runtime 不应被多个 goroutine 同时操作。回调闭包引用的 Go 状态也要按应用的并发设计管理;不要保存 FunctionCall.Arguments 的引用并在回调返回后继续使用。只暴露完成任务需要的小接口,可减少脚本对宿主对象的依赖,但这不是完整隔离或权限保证。环境与并发限制参阅 Goja 官方 FAQ。

为什么脚本说函数不存在?

先看注册名称是否与脚本一致、注册是否发生在调用之前,以及脚本是不是在另一个 Runtime 中运行。本例两个函数名称不同,替换代码后也要修改调用名称。

普通回调能返回业务错误吗?

可以使用最后一个返回值为 error 的签名;脚本用 try/catch 决定后续逻辑,Go 仍需处理脚本运行返回的错误。

需要读取 this 时怎么办?

使用 FunctionCall.This,并明确要求脚本以什么对象调用。不要把普通全局函数调用的接收者约定误当成某个业务对象。

简而言之,先选接口契约再注册:简单映射用普通函数,严格入口用原生回调。把名称、参数、错误和运行时所属关系说明白,脚本才能稳定使用 Go 提供的能力。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>