Go x/image/font/sfnt Buffer 为什么不适合多协程同时复用
我第一次把 golang.org/x/image/font/sfnt 接进并发字体查询服务时,直觉上把 Font 和 Buffer 都当成了“解析后的字体对象”。结果是:单协程下看不出问题,一旦多个 goroutine 共用一个 *sfnt.Buffer,返回的指标和字形数据就出现了不稳定的交叉覆盖。
官方地址:https://pkg.go.dev/golang.org/x/image/font/sfnt
结论很明确:同一个*sfnt.Buffer不适合被多个协程同时复用;真正可以共享的是同一个*sfnt.Font。让每次 Font 方法调用拿到不同的 Buffer,或者传入nil使用一次性临时 Buffer。
先把 Font 和 Buffer 看成两种不同职责
sfnt 是 TTF、OTF 等 SFNT 字体的低层解码器。Font 保存了字体数据的解析结果,而 Buffer 是给多次 Font 方法调用复用的临时工作区,目的是减少重复分配。它不是字体本身,也不是一个可以被并发请求共享的“上下文”。
官方文档把这两个边界说得很清楚:Font 的方法可以并发调用,但每次调用必须使用不同的 *Buffer,也可以传入 nil。同一个 Buffer 可以在不同调用之间复用,也可以服务不同的 Font,但前一个调用已经结束、后一个调用还没有开始,才是安全的时间关系。

共享 Font 没问题,共享 Buffer 会把临时状态混在一起
下面这种写法的问题不在于多个 goroutine 读取同一个 Font,而在于它们同时修改同一个 Buffer。Buffer 内部字段没有作为公共并发接口暴露出来,但 Font 的方法会把它当作可写的复用空间;两个调用一旦交错,谁最后写入了临时空间,谁就可能改变另一方正在使用的数据。
package main
import (
"fmt"
"golang.org/x/image/font/sfnt"
)
var sharedBuffer sfnt.Buffer
func unsafeQuery(f *sfnt.Font, r rune) (sfnt.GlyphIndex, error) {
// 错误示例:全局 Buffer 会被多个并发调用同时当作临时工作区。
return f.GlyphIndex(&sharedBuffer, r)
}
func safeQuery(f *sfnt.Font, r rune) (sfnt.GlyphIndex, error) {
var local sfnt.Buffer
// 每个调用拥有自己的临时区,Font 本身仍然可以由多个调用共享。
glyph, err := f.GlyphIndex(&local, r)
if err != nil {
// 保留库返回的错误,让调用方区分解析失败与缺少字形。
return 0, fmt.Errorf("查询字形 %q:%w", r, err)
}
return glyph, nil
}
上面的 unsafeQuery 还没有启动 goroutine,但只要把同一个 Buffer 放进共享服务结构体,问题就会变成竞态。更稳妥的原则是:Font 可以长期共享,Buffer 只在一次调用的边界内被借用。
不需要复用时,传 nil 反而更简单
如果一次请求只查询一两个字符,或者当前瓶颈根本不在临时分配,直接传 nil 是合法的。文档说明 nil 表示这次调用使用一个临时 Buffer;它避免了调用方维护共享池,也不会把 Buffer 的生命周期泄漏到业务对象中。
func glyphIndexWithoutReuse(f *sfnt.Font, r rune) (sfnt.GlyphIndex, error) {
// nil 表示只为本次调用使用临时 Buffer,适合低频或简单查询。
glyph, err := f.GlyphIndex(nil, r)
if err != nil {
// ErrNotFound 也要交给上层决定是回退字体还是返回业务错误。
return 0, err
}
return glyph, nil
}
这也是我后来采用的取舍:先用 nil 把并发所有权写对,再在确认分配成本确实影响吞吐时引入 sync.Pool。不要为了“看起来高性能”一开始就把可复用对象升级成全局共享状态。
LoadGlyph 返回的 Segments 不能越过 Buffer 的复用边界
LoadGlyph 是更容易踩坑的一组 API。它返回字形的矢量线段;当传入非 nil Buffer 时,官方文档明确说明:Buffer 一旦被再次使用,返回的 Segments 就失效。也就是说,问题不止是“调用期间不能共享”,还包括“调用结束后不能把依赖临时 Buffer 的结果继续保存”。

如果业务需要在函数返回后继续保存线段,就在归还 Buffer 前复制一份。sfnt.Segment 是值类型,复制切片即可把结果与临时工作区分开。
func loadGlyphCopy(f *sfnt.Font, glyph sfnt.GlyphIndex, ppem fixed.Int26_6) (sfnt.Segments, error) {
var b sfnt.Buffer
// Segments 只在 b 未被再次使用前可靠,先在当前调用内完成复制。
segments, err := f.LoadGlyph(&b, glyph, ppem, nil)
if err != nil {
// ErrColoredGlyph 等错误不能用空切片伪装成成功结果。
return nil, err
}
copied := append(sfnt.Segments(nil), segments...)
// copied 不再依赖 b,函数返回后可以安全交给异步消费者。
return copied, nil
}
这段示例需要补上 golang.org/x/image/math/fixed 导入。真正的业务代码中,如果只是当场绘制或读取边界,不需要把 Segments 返回出去,就可以在 Buffer 仍由当前调用独占时完成全部工作。
用 sync.Pool 复用 Buffer 时,借还必须包住完整调用
当查询量很大时,可以让 sync.Pool 管理 Buffer。关键不是 Pool 本身能否并发,而是每次 Get 到 Put 之间,Buffer 只能属于当前调用;并且任何依赖它的返回值都必须在 Put 前处理或复制。
var bufferPool = sync.Pool{
New: func() any {
// Pool 只保存临时工作区,不保存 Font 或字体源数据。
return new(sfnt.Buffer)
},
}
func queryMetrics(f *sfnt.Font, ppem fixed.Int26_6, hinting font.Hinting) (font.Metrics, error) {
b := bufferPool.Get().(*sfnt.Buffer)
defer bufferPool.Put(b)
// 借出后到归还前,b 只服务当前调用;Font 仍可被其它协程共享。
metrics, err := f.Metrics(b, ppem, hinting)
if err != nil {
// 先返回错误,defer 仍会把临时 Buffer 放回池中。
return font.Metrics{}, err
}
return metrics, nil
}
这里的 Metrics 返回的是独立的值结构,归还 Buffer 后不会留下像 Segments 那样的切片引用。若换成 LoadGlyph,就必须先复制 Segments,再执行 Put。这正是“接口能不能并发”与“返回值活多久”两个问题的交叉处。
字体源数据也有自己的生命周期
Buffer 的并发问题修好后,还有一个容易被误归类的边界:sfnt.Parse 并不会把传入的字体字节完全复制成一个与源无关的 Font。使用 []byte 解析时,调用方在 Font 仍被使用期间不能修改这段数据;使用 io.ReaderAt 解析时,也不能在 Font 仍使用期间关闭或改变 ReaderAt。
因此一个长期运行的字体服务通常有三层所有权:字体源字节只读并保持有效,Font 作为共享解析对象,Buffer 作为每次调用独占的临时区。把这三层放进同一个可变结构体,并不等于它们拥有相同的并发规则。
最后用一张表做调用前判断
| 对象或返回值 | 能否跨协程共享 | 实践建议 |
|---|---|---|
*sfnt.Font | 可以,Font 方法支持并发 | 保持底层字体数据不被修改 |
*sfnt.Buffer | 不能并发复用 | 每次调用独立实例,或传 nil |
sfnt.Segments | 取决于来源 | 非 nil Buffer 被复用前先复制 |
Font.NumGlyphs 等无 Buffer 方法 | 可以并发调用 | 仍要遵守 Font 源数据生命周期 |
我的最终判断是:如果只是读取字形索引、名称或度量,先选独立 Buffer 或 nil;如果要把线段交给异步流程,先复制再离开调用边界;如果用 sync.Pool,把借还写在同一个函数里,不要把 Buffer 作为服务级共享字段暴露出去。
相关问题
同一个 Buffer 能给不同 Font 轮流使用吗?
可以,但必须是前一次调用已经完成、后一次调用尚未开始的串行复用。不同 Font 不会改变 Buffer 不能并发复用的规则。
所有 Font 方法都需要 Buffer 吗?
不是。官方文档中不接收 Buffer 的方法可以安全并发调用;接收 Buffer 的方法则要为每次并发调用提供不同 Buffer 或传 nil。
LoadGlyph 的结果为什么要复制?
因为非 nil Buffer 被再次使用后,返回的 Segments 会失效。若结果要跨函数、跨 goroutine 或跨缓存保存,就要在归还或再次使用 Buffer 前复制。
Go x/text/width 半角片假名扩宽为什么可能改变字符组合
- 上一篇
- Go x/text/width 半角片假名扩宽为什么可能改变字符组合
- 下一篇
- Go x/image/font/opentype 创建多个 Face 为什么需要分别管理资源
-
- Golang · Go问答 | 1小时前 |
- Go debug/dwarf SeekPC 为什么找不到某些地址
- 471浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go x/image/riff 容器声明长度大于实际输入怎么办
- 209浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go x/image/font/opentype 创建多个 Face 为什么需要分别管理资源
- 358浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go x/text/width 半角片假名扩宽为什么可能改变字符组合
- 439浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go x/text/unicode/norm 分块 NFC 为什么不能独立处理每个小块
- 435浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go x/text/unicode/bidi 段落方向为什么不同于每个字符方向
- 213浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go x/text/transform Reset 为什么不能随意放在分块循环内部
- 348浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go x/text/secure/precis 两个昵称看起来一样为什么规范化后仍不同
- 346浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go x/text/search 语言搜索为什么可能匹配不同长度文本
- 410浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go x/text/secure/bidirule 流式双向文本为什么要等结尾再确认
- 366浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go x/text/runes 转换器链为什么仍需处理输出空间不足
- 404浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go x/text/number MinFraction 与 MaxFraction 为什么要配套设置
- 407浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 727次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 733次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 809次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 770次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 609次使用
-
- 用Nginx反向代理部署go写的网站。
- 2023-01-17 502浏览
-
- GoLand调式动态执行代码
- 2023-01-13 502浏览
-
- Go os.File.ReadAt 为什么到文件末尾仍返回部分数据
- 2026-10-26 501浏览
-
- Go JSONJSON null 解码到指针后状态不符合预期怎么办
- 2026-10-18 501浏览
-
- Go crypto/rand.Text 的长度为什么不是固定字符数
- 2026-10-04 501浏览

