Hugo 模板开发指南:Store 数据结构的五种作用域与 hstore 源码解析
在 Hugo 模板中处理带状态的构建逻辑(跨 partial、跨 shortcode 传递中间结果、在渲染钩子中累积计数等)时,Store 是最核心的键值存储机制。本文基于 Hugo 文档中的 Store Scope 章节展开,系统讲解 page、site、global、local、shortcode 五种作用域的创建方式与适用场景,并结合 common/hstore 包源码说明其线程安全实现、"server 重建不重置"的生命周期特性,以及父模板中获取 shortcode 写入值的确定性取值技巧。
作用域总览
决定 Store 数据结构作用域的,是创建它所用的方法或函数。例如,在 Page 对象上调用 Store 方法,创建的就是页面作用域的数据结构。Hugo 提供的五种作用域与创建方式对应关系如下:
| 作用域 | 创建方法/函数 | 生命周期 |
|---|---|---|
| page(页面) | PAGE.Store |
当前页面内,server 重建时保留 |
| site(站点) | SITE.Store |
当前站点内,所有页面共享,server 重建时保留 |
| global(全局) | hugo.Store |
全局共享,server 重建时保留 |
| local(局部) | collections.NewScratch |
仅当前模板执行上下文 |
| shortcode(短代码) | SHORTCODE.Store |
当前 shortcode 实例内 |
各作用域的详细文档位于 methods/page/Store、methods/site/Store、functions/hugo/Store、functions/collections/NewScratch 和 methods/shortcode/Store。其中 SITE.Store、SHORTCODE.Store 与 hugo.Store 均为 0.139.0 版本引入的新能力。
五种作用域的用法示例
local:局部作用域(newScratch)
collections.NewScratch(模板中写作 newScratch,可作函数或方法使用)创建完全私有的临时存储,适合在单个模板内部做局部状态累积:
{{ $s := newScratch }}
{{ $s.Set "greeting" "Hello" }}
{{ $s.Get "greeting" }} → Hello
模板函数注册见 tpl/collections/init.go 中 newScratch 的方法映射;其底层实现非常直接——tpl/collections/collections.go 中 NewScratch 只是委托给 hstore.NewScratch() 返回一个空的结构。
page:页面作用域(PAGE.Store)
{{ .Store.Set "greeting" "Hello" }}
{{ .Store.Get "greeting" }} → Hello
在 hugolib/page__new.go 中可以看到页面 Store 采用 sync.OnceValue 懒初始化(源码注释标注 "Rarely used",说明绝大多数页面并不会触发该分配),hugolib/page__common.go 提供模板可调用的 Store() 入口。页面作用域的典型用途是:在 render hook(_markup 渲染钩子)中写计数值,然后在页面布局的其他位置读取。
site:站点作用域(SITE.Store)
{{ site.Store.Set "greeting" "Hello" }}
{{ site.Store.Get "greeting" }} → Hello
站点作用域在整个站点的构建过程中共享,适合跨页面统计(例如累计所有页面的某种属性)。
global:全局作用域(hugo.Store)
{{ hugo.Store.Set "greeting" "Hello" }}
{{ hugo.Store.Get "greeting" }} → Hello
全局作用域的生命周期最长。注意一个重要的生命周期特性:从 common/hstore/scratch.go 的 StoreProvider 接口注释可以看到,Store 返回的 Scratch 在 server 模式的重建(rebuild)过程中不会被重置。这意味着在 hugo server 开发时,页面/站点/全局 Store 中写入的状态在文件修改触发的增量重建之间得以保留,可用于跨重建缓存临时状态;而 newScratch 创建的局部作用域则随着每次模板执行结束而消失。
shortcode:短代码作用域(SHORTCODE.Store)
{{ .Store.Set "greeting" "Hello" }}
{{ .Store.Get "greeting" }} → Hello
hugolib/shortcode.go 中 ShortcodeWithPage.Store() 同样按需创建 Scratch 实例。需要指出的是:随着 newScratch 函数的引入,以及模板变量初始化后也可以赋值的能力,shortcode 内的 Store 方法已基本过时——官方文档明确建议在新代码中优先使用 newScratch。
Store 数据结构的方法
五种作用域创建出的数据结构共用同一套方法接口(Set、Get、Add、SetInMap、DeleteInMap、GetSortedMapValues、Delete,局部作用域额外提供 Values)。核心示例:
Set:设置给定键的值。
{{ .Store.Set "greeting" "Hello" }}
Get(any):读取给定键的值。
{{ .Store.Set "greeting" "Hello" }}
{{ .Store.Get "greeting" }} → Hello
Add:将给定值累加到已有值上。对单值,Add 接受支持 Go + 运算符的值;若某键第一次 Add 的是数组或切片,后续 Add 都会追加到该列表中。
{{ .Store.Set "greeting" "Hello" }}
{{ .Store.Add "greeting" "Welcome" }}
{{ .Store.Get "greeting" }} → HelloWelcome
{{ .Store.Set "total" 3 }}
{{ .Store.Add "total" 7 }}
{{ .Store.Get "total" }} → 10
{{ .Store.Set "greetings" (slice "Hello") }}
{{ .Store.Add "greetings" (slice "Welcome" "Cheers") }}
{{ .Store.Get "greetings" }} → [Hello Welcome Cheers]
SetInMap:接收 key、mapKey、value 三个参数,在 key 下的 map 中写入 mapKey → value。
{{ .Store.SetInMap "greetings" "english" "Hello" }}
{{ .Store.SetInMap "greetings" "french" "Bonjour" }}
{{ .Store.Get "greetings" }} → map[english:Hello french:Bonjour]
DeleteInMap:从 key 下的 map 中移除 mapKey。
{{ .Store.SetInMap "greetings" "english" "Hello" }}
{{ .Store.SetInMap "greetings" "french" "Bonjour" }}
{{ .Store.DeleteInMap "greetings" "english" }}
{{ .Store.Get "greetings" }} → map[french:Bonjour]
GetSortedMapValues([]any):返回 key 下按 mapKey 排序后的值数组。
{{ .Store.SetInMap "greetings" "english" "Hello" }}
{{ .Store.SetInMap "greetings" "french" "Bonjour" }}
{{ .Store.GetSortedMapValues "greetings" }} → [Hello Bonjour]
Delete:删除给定键。
{{ .Store.Set "greeting" "Hello" }}
{{ .Store.Delete "greeting" }}
Values(map,仅局部作用域安全):返回底层原始 map。文档特别警告:不要对 Page 对象上的 Store 使用该方法,否则会导致并发问题。
源码实现:Scratch 结构与并发安全
Store 的核心实现在 common/hstore/scratch.go。Scratch 的结构极其精简:
// Scratch is a writable context used for stateful build operations
type Scratch struct {
values map[string]any
mu sync.RWMutex
}
几个源码层面的要点:
- 读写锁保护:所有
Set/Add/Delete/SetInMap/DeleteInMap通过mu.Lock()获取写锁,Get/Values/GetSortedMapValues通过mu.RLock()获取读锁。Hugo 构建是并行的(页面按 goroutine 分片渲染),这把RWMutex正是页面级、站点级 Store 在并发渲染下安全共享的前提,也解释了为什么Values返回裸 map 时只对局部作用域安全——页面/站点 Store 的裸 map 一旦被模板持有,就绕过了锁保护。 Add的类型分派:Add用reflect检查已有值的类型,若是Slice/Array走collections.Append追加,否则走common/math包的DoArithmetic执行+运算。这与文档中"支持数字和字符串累加"的行为完全对应。GetSortedMapValues的排序实现:将 map 的键收集为切片后sort.Strings排序,再按序回填为数组——这就是模板中观察到"按 mapKey 排序"的由来。- 模板兼容性返回值:
Set/Add/Delete等方法统一返回空字符串,源码注释写明"have to return something to make it work with the Go templates"——因为 Go template 对多返回值函数的处理方式要求这些方法至少返回一个值,模板表达式{{ .Store.Set "k" "v" }}才不会输出报错。
从 StoreProvider 接口看,任何实现 Store() *Scratch 的对象都能提供这种临时状态容器,页面(hugolib/page__common.go)、shortcode(hugolib/shortcode.go)、站点(hugolib/site.go)乃至 pagesfromdata 的模板上下文(hugolib/pagesfromdata/pagesfromgotmpl.go)都实现了它,这也印证了"谁调用 Store 方法,谁就决定作用域"的设计。
确定性取值:父模板读取 shortcode 写入的值
Store 方法最常被用在 shortcode 模板、由 shortcode 调用的 partial 模板,或 render hook 模板中。这三种场景下,存储的值在 Hugo 渲染完页面内容之前都是不确定的(indeterminate)——因为内容渲染发生在布局模板的后续阶段。
如果父模板需要在内容渲染前访问这些值,可以先把某个渲染结果的返回值赋给一个 noop 变量,强制触发内容渲染:
{{ $noop := .Content }}
{{ .Store.Get "mykey" }}
以下方法同样可以触发内容渲染:ContentWithoutSummary、FuzzyWordCount、Len、Plain、PlainWords、ReadingTime、Summary、Truncated、WordCount。例如:
{{ $noop := .WordCount }}
{{ .Store.Get "mykey" }}
作用域选择建议
结合文档与源码,选择作用域时可以遵循以下原则:
- 只在当前模板/shortcode 内部使用:首选
newScratch。它最轻量(hstore.NewScratch()即分配一个空 map)、无生命周期歧义,且newScratch引入后 shortcode 内Store已被官方视为过时。 - render hook 写、页面布局读:使用
PAGE.Store,配合 noop 技巧保证读取时机。 - 跨页面统计:使用
SITE.Store(0.139.0+),它在整个站点构建内共享。 - 需要跨重建保留的状态:页面/站点/全局 Store 都不会在 server 重建时重置,可在
hugo server开发期作为轻量缓存使用;但需自行处理键的清理(Delete/DeleteInMap)。 - 注意并发:对页面/站点 Store 不要调用
Values获取裸 map,始终通过Get等方法访问,以保持在锁保护之下。
参考
- common/hstore/scratch.go —
Scratch结构与全部方法实现 - common/hstore/scratch_test.go — 行为测试
- tpl/collections/collections.go — 模板侧
NewScratch入口 - hugolib/page__common.go — 页面
Store()方法 - hugolib/shortcode.go — shortcode
Store()方法 - hugolib/site.go — 站点
Store()方法
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00