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

Go Starlark 的 Thread 局部数据与脚本全局变量有什么区别

来源:17golang原创

时间:2026-10-31 00:04:36 110浏览 收藏

在 Go 中嵌入 go.starlark.net/starlark 时,Thread.Local、Thread.SetLocal 和 Starlark 脚本里的全局变量都能“保存一个值”,但它们不是同一种变量。前者属于 Go 侧的线程状态,后者属于 Starlark 模块里的名称绑定。

官方文档:https://pkg.go.dev/go.starlark.net/starlark

最实用的判断方法是:只有宿主 Go 回调需要读取的请求上下文、租户标识或 tracing 信息,才放进 Thread.Local;需要被脚本表达式直接读写的配置,放进脚本全局变量或显式参数。不要把 Thread.Local 当成脚本的隐形全局变量。

先把两种状态放回各自的边界

Thread 是 Starlark 求值过程携带的状态对象,文档把它描述为包含调用栈和线程局部存储的 Starlark thread state。Local 与 SetLocal 通过字符串键读写 Go 侧的局部值;脚本全局变量则存在模块环境中,由脚本赋值,并可作为执行结果的一部分返回给宿主。

Go Starlark Thread 局部数据、脚本全局变量与 predeclared 环境的静态关系图
图1:Thread 局部数据与脚本全局变量的作用域关系,重点看三组边界分别由谁拥有;这不是运行截图。

可以先用三个问题区分它们:

  • 这个值是否只给 Go 写的 builtin 或回调使用?是的话优先考虑 Thread.Local。
  • 这个值是否需要在 Starlark 代码里通过名称访问或重新赋值?是的话它属于脚本环境。
  • 这个值是执行前注入的能力,还是执行后由脚本产生的结果?前者通常放入 predeclared,后者从返回的 globals 中读取。

Thread 局部数据是宿主回调的上下文

Thread.SetLocal(key, value) 把任意 Go 值挂到指定的 Thread 上,Thread.Local(key) 再按键取回。这个值不会因为名字叫得像脚本变量,就自动出现在 Starlark 的名称空间中。脚本要使用它,必须经过宿主提供的 builtin、回调或其他显式桥接。

还有一个经常被忽略的时机约束:官方文档明确说明,SetLocal 不应在执行开始后调用。因此可以在创建线程并准备执行环境时完成初始化;执行过程中让回调只读,或者把需要变化的状态放在更明确的宿主对象里。

func newThread() *starlark.Thread {
    // 执行开始前设置宿主上下文,避免在脚本运行中改变线程局部值。
    thread := &starlark.Thread{Name: "config-eval"}
    thread.SetLocal("request_id", "req-42")
    thread.SetLocal("tenant", "demo")
    return thread
}

func readContext(thread *starlark.Thread, _ *starlark.Builtin,
    _ starlark.Tuple, _ []starlark.Tuple) (starlark.Value, error) {
    // builtin 通过 Thread 指针读取 Go 侧上下文,脚本本身看不到 Local 存储。
    requestID, ok := thread.Local("request_id").(string)
    if !ok {
        // 缺少上下文时返回可识别的 Starlark 错误,不把 nil 泄漏给脚本。
        return nil, fmt.Errorf("request_id is missing from thread local")
    }
    return starlark.String(requestID), nil
}

这个例子里,request_id 是宿主上下文,不是脚本配置。回调可以把它转成 starlark.String 返回,脚本才能看到结果;如果没有这个桥接,脚本中直接写 request_id 仍然会被当成未定义名称。

脚本全局变量是模块环境中的名称绑定

Starlark 文件里的顶层赋值属于该文件的模块环境。例如下面的 mode 和 build_name 都是脚本侧的名字,它们服务于脚本表达式和函数,而不是服务于某个 Go 回调的隐形上下文。

mode = "safe"

def build_name(prefix):
    # mode 是脚本全局变量,函数通过名称读取同一模块中的绑定。
    return prefix + "-" + mode

宿主调用 ExecFile 或 ExecFileOptions 后,可以从返回的 StringDict 读取模块全局变量。它与传入的 predeclared 不是一回事:

  • predeclared 是执行前提供给脚本的预声明名称,例如宿主注册的 builtin。
  • 模块 globals 是脚本执行环境中的结果,包含脚本顶层产生的绑定。
  • Thread.Local 不属于上述名称字典,不会因为键名相同就自动覆盖 globals。

因此,脚本全局变量适合表达“这个模块的配置或函数共享什么”,而 Thread.Local 适合表达“这次求值由宿主携带什么上下文”。两者都叫状态,但所有权和可见范围完全不同。

宿主回调怎样同时读取两类状态

下面的组合示例把三件事拆开:执行前用 SetLocal 写入请求上下文;用 predeclared 注册一个 builtin;执行后从 globals 读取脚本的顶层变量。代码只展示边界,不把局部存储伪装成脚本全局变量。

Go Starlark 宿主回调同时读取 Thread.Local 与脚本 globals 的静态关系图
图2:宿主回调读取状态的结构关系,Thread.Local 和脚本参数分别承担宿主上下文与脚本数据;这不是运行结果截图。
func execute(src []byte) (starlark.StringDict, error) {
    // 先创建并初始化线程局部上下文,之后再开始脚本求值。
    thread := &starlark.Thread{Name: "config-eval"}
    thread.SetLocal("request_id", "req-42")

    // predeclared 提供脚本可以调用的宿主能力,不等同于模块 globals。
    predeclared := starlark.StringDict{
        "read_request_id": starlark.NewBuiltin("read_request_id", readContext),
    }

    // ExecFile 返回脚本模块环境,顶层 mode 等绑定会出现在 globals 中。
    globals, err := starlark.ExecFile(thread, "config.star", src, predeclared)
    if err != nil {
        // 脚本错误保留原始错误,调用方可据此区分求值失败与宿主配置失败。
        return nil, err
    }

    // 读取脚本产生的绑定;它和 thread.Local("request_id") 属于两个命名空间。
    if value, ok := globals["mode"]; ok {
        _ = value
    }
    return globals, nil
}

这里的 readContext 能从 Thread 取到请求号,但脚本看到的是 builtin 的返回值;脚本中的 mode 则直接从模块全局环境中读取。若要让脚本真正调用它,可以在脚本中写 request = read_request_id(),这一步就是显式桥接。

什么时候该用 Thread.Local,什么时候该用脚本全局变量

判断维度Thread 局部数据脚本全局变量
主要拥有者Go 宿主与 builtinStarlark 模块
脚本直接可见不可见,需显式桥接可以按名称读写
典型内容请求号、租户、追踪对象、宿主回调上下文模式、默认值、脚本函数和模块级配置
初始化时机执行开始前调用 SetLocal脚本顶层赋值或执行前的预声明
跨模块共享取决于是否使用同一个 Thread取决于模块环境与加载策略

如果一个值需要被脚本修改并在执行结果中返回,就不要藏在 Thread.Local 里;如果一个值包含宿主对象、请求级资源或不应该暴露给脚本的实现细节,也不要为了方便把它塞进脚本全局变量。先决定所有权,再决定存储位置。

并发和生命周期上最容易踩的坑

第一,不要把同一个可变模块全局环境当成多个请求的天然隔离区。若每次执行都需要独立配置,应明确创建或管理对应的模块环境,不能只因为每次传入了不同的 Thread 就假定脚本全局变量会自动隔离。

第二,不要把 Thread.Local 当成通用缓存。它的价值是把宿主上下文沿求值过程带到 builtin;缓存、跨请求共享配置和可持久化数据应该放在更清晰的宿主组件中,并自行处理并发安全。

第三,注意 SetLocal 的初始化约束。若执行已经开始才尝试补写局部数据,应改为在执行前准备好线程,或通过显式参数、builtin 参数把变化传入。这样错误会出现在清楚的接口边界,而不是隐藏在脚本执行中。

常见问题

Thread.Local 写入的 key 能直接在脚本里使用吗?

不能。它是 Go 侧线程局部存储。需要脚本使用时,应注册 builtin、把值作为函数参数传入,或在执行环境中显式提供名称。

脚本全局变量和 predeclared 是不是同一个字典?

不是。predeclared 是执行前提供的名称集合;脚本顶层赋值形成模块环境中的 globals。宿主可以从执行返回的 StringDict 读取脚本产生的绑定。

把所有上下文都放进脚本全局变量可以吗?

不建议。请求级上下文、宿主对象和敏感实现细节应留在 Go 侧,通过最小化 builtin 或参数暴露需要的部分。

使用不同 Thread 就一定能隔离脚本全局变量吗?

不能直接这样推断。Thread 局部数据和模块环境是两层状态,是否隔离还取决于模块环境如何创建、复用和加载。

小结

Go Starlark 的 Thread.Local 是沿求值过程传递的宿主局部上下文,脚本全局变量是模块环境里的名称绑定,predeclared 则是执行前注入的脚本能力。把宿主状态、脚本数据和执行输入分别放在对应边界,再通过显式 builtin 或参数连接它们,才能避免状态串扰和生命周期误判。

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