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

Go fastjson 怎样复用Parser减少临时对象

来源:17golang原创

时间:2026-10-31 11:04:21 385浏览 收藏

在循环或高频请求中反复解析 JSON 时,fastjson 的关键优化不是把所有状态做成全局变量,而是让同一个调用方复用局部 Parser,并在下一次 Parse 前结束旧 Value 的使用。并发场景再用 ParserPool 借出独占的 Parser,就能减少解析器本身的临时分配,同时保持请求之间的边界清楚。

官方地址:https://pkg.go.dev/github.com/valyala/fastjson

要点速览
  • 同一份 JSON 需要读取多个字段时,先 Parser.Parse 一次,再对返回的 Value 调用多个 Get* 方法。
  • Parser 返回的 Value 可能引用 Parser 的内部数据;下一次 Parse 前不要继续保存旧 Value。
  • 并发请求不能共享一个可变 Parser;用 ParserPool 借用、使用、归还,每次只让一个请求持有。

先判断:便捷函数还是一次 Parse 多次读取

fastjson.GetString、GetInt 等顶层函数适合只取一个字段的短代码。若同一份 JSON 要读取多个互不相邻的字段,继续逐个调用顶层函数会重复解析输入。此时应把输入交给 Parser 一次,再从同一个 Value 读取字段:

package main

import (
    "fmt"
    "log"

    "github.com/valyala/fastjson"
)

func readPayload(data []byte) (string, int, error) {
    var parser fastjson.Parser

    // 一次解析整份 JSON,后续字段读取共享同一个 Value。
    value, err := parser.ParseBytes(data)
    if err != nil {
        // 解析失败时立即返回,避免继续读取无效 Value。
        return "", 0, err
    }

    name := value.GetStringBytes("user", "name")
    count := value.GetInt("items", "count")
    return string(name), count, nil
}

func main() {
    name, count, err := readPayload([]byte(`{"user":{"name":"Lin"},"items":{"count":3}}`))
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("%s %d\n", name, count)
}

这里的优化点是“解析一次、读取多次”,而不是把 Parser 做成共享全局对象。Parser 是可变状态,局部变量最容易表达独占关系;需要并发处理时,再把这个模式放进池中。

fastjson 单字段便捷函数重复解析与 Parser 一次解析多次读取的结构对比图
图1:结构说明图,对比单字段便捷函数与一次 Parse 后多次 Get 的静态关系,不是运行截图。

复用局部 Parser 时,Value 的生命周期要收紧

官方文档提示,Parser 递归返回的部分对象不能跨过下一次 Parse 继续使用。实际代码中可以把“解析、读取、转换为业务值”放在同一个函数里,让 Value 不逃逸到下一轮循环:

func collectNames(inputs [][]byte) ([]string, error) {
    var parser fastjson.Parser
    names := make([]string, 0, len(inputs))

    for _, data := range inputs {
        // Parse 会更新 Parser 的内部状态,本轮只在这里使用 value。
        value, err := parser.ParseBytes(data)
        if err != nil {
            return nil, err
        }

        // 转成独立的 Go string,再进入下一次 Parse。
        names = append(names, value.GetStringBytes("user", "name")...)
    }
    return names, nil
}

这个例子刻意没有把 *fastjson.Value 保存到切片,也没有把它交给 goroutine。若需要把字符串或字节交给异步任务,应先确认返回数据是否仍依赖 Parser 的内部存储;不确定时,复制成独立的业务数据,再进入异步边界。

复用 Parser 的前提是“上一轮的 Value 已经完成消费”,不是“Parser 可以永久保存所有历史结果”。只要下一轮 Parse 可能覆盖内部状态,就把 Value 当作当前轮次的借用视图。

并发请求用 ParserPool,而不是共享一个 Parser

服务端通常有多个请求同时解析 JSON。一个全局 Parser 会让请求之间共享可变状态,既难以保证数据正确,也会制造竞态。ParserPool 的职责是提供可复用的 Parser;借出后由当前请求独占,处理完成后归还:

var parserPool fastjson.ParserPool

func parseRequest(data []byte) (int, error) {
    parser := parserPool.Get()
    defer parserPool.Put(parser)

    // 当前请求独占 parser;返回前完成所有 Value 读取。
    value, err := parser.ParseBytes(data)
    if err != nil {
        return 0, err
    }

    total := value.GetInt("items", "count")
    return total, nil
}

defer 能覆盖成功和错误两条路径,但不能替你处理外部引用:如果把 value 或它返回的视图交给异步任务,函数返回后 Parser 可能已经被归还并再次使用。正确做法是复制需要跨越函数边界的数据,或者把消费动作放在归还之前。

fastjson ParserPool、请求独占、Value读取与下一次Parse前释放引用的关系图
图2:边界说明图,展示 ParserPool 的借用与归还,以及 Value 不应跨越下一次 Parse 的生命周期关系,不是并发运行证据。

哪些改造会让复用失去意义

写法问题更合适的边界
每个字段都调用顶层 Get*同一份输入被重复解析一次 Parse 后读取多个字段
所有 goroutine 共用一个 Parser可变解析状态互相覆盖ParserPool 每次借出一个独占实例
保存 Value 到下一轮循环下一次 Parse 后引用可能失效当前轮次完成读取并转成独立数据
把 Value 的视图交给异步任务Parser 归还后底层数据可能变化复制后再跨线程或队列传递

还要控制输入大小。官方说明指出,解析过程可能按输入规模占用内存,因此复用 Parser 不能替代请求体上限、超大 JSON 拒绝策略和错误处理。复用解决的是重复分配压力,不是无限放大输入容量。

落地前的判断清单

  • 同一份 JSON 是否需要读取多个字段?如果只取一个字段,顶层便捷函数可能更直观。
  • Parser 是否只被一个调用方持有?并发场景是否通过 ParserPool 借用而不是共享?
  • 下一次 Parse 前,旧 Value 和其视图是否已经全部消费?
  • 跨函数、goroutine 或队列传递的数据是否已经复制到独立存储?
  • 是否设置了输入大小上限,并把解析错误作为正常分支处理?

常见问题

复用 Parser 会不会让所有结果都被覆盖?

只有仍依赖 Parser 内部状态的 Value 或视图才有生命周期约束。需要长期保存的字段应先转换或复制成独立的 Go 数据。

ParserPool 是否意味着可以把 Parser 放进全局并发使用?

不是。池可以是全局的,但每次 Get 到的 Parser 只能由当前调用方独占,Parse、读取和 Put 之间不能被多个 goroutine 同时操作。

复用 Parser 后还需要关注请求体大小吗?

需要。Parser 复用只减少反复创建解析器的分配,不能消除大输入本身带来的内存占用,应配合输入上限和解析错误处理。

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