news 2026/9/11 12:38:56

Gin 运行时如何实现 json.Core 并替换 json.API 而不用构建标签

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Gin 运行时如何实现 json.Core 并替换 json.API 而不用构建标签

Gin 运行时如何实现 json.Core 并替换 json.API 而不用构建标签

【免费下载链接】ginGin is a high-performance HTTP web framework written in Go. It provides a Martini-like API but with significantly better performance—up to 40 times faster—thanks to httprouter. Gin is designed for building REST APIs, web applications, and microservices.项目地址: https://gitcode.com/GitHub_Trending/gi/gin

如果你的 Gin 项目需要定制 JSON 序列化逻辑——例如换用特定库、开启特定配置(HTML 转义、Map 键排序、RawMessage 校验)或自定义某类类型的时间格式——传统做法是加构建标签(jsonitergo_jsonsonic)在编译期切换 codec。另一条文档明确给出的路径是运行时替换:自定义一个实现json.Core接口的结构体,在引擎启动前把它赋值给json.API全局变量,整个过程不需要任何构建标签,也不需要修改 gin 源码。

先看 Gin 的 JSON codec 是怎么组织的

Gin 把 JSON codec 抽象在 codec/json 包中,核心是一个接口和一个全局变量:

// codec/json/api.go // API the json codec in use. var API Core // Core the api for json codec. type Core interface { Marshal(v any) ([]byte, error) Unmarshal(data []byte, v any) error MarshalIndent(v any, prefix, indent string) ([]byte, error) NewEncoder(writer io.Writer) Encoder NewDecoder(reader io.Reader) Decoder }

编译期切换由四个文件上的构建标签完成:

  • codec/json/json.go:构建标签为!jsoniter && !go_json && !(sonic && (linux || windows || darwin)),即默认走标准库encoding/json,其init()执行API = jsonApi{}
  • codec/json/go_json.go:标签go_json,使用github.com/goccy/go-json
  • codec/json/jsoniter.go:标签jsoniter,使用github.com/json-iterator/go
  • codec/json/sonic.go:标签sonic && (linux || windows || darwin),使用github.com/bytedance/sonic

四条路径最终都落在同一个动作上:在init()里给API赋值。运行时替换就是跳过构建标签这一层,直接改API指向。

需要实现的三个接口

自定义结构体必须实现 Core 的五个方法;它返回的EncoderDecoder也要满足 Gin 定义(codec/json/api.go):

type Encoder interface { SetEscapeHTML(on bool) Encode(v any) error } type Decoder interface { UseNumber() DisallowUnknownFields() Decode(v any) error }

有两个调用点会约束你的实现:

  • render/json.go 中PureJSON的渲染会调用encoder.SetEscapeHTML(false),所以自定义Encoder必须提供该方法;
  • binding/json.go 的decodeJSON只在包级变量binding.EnableDecoderUseNumberbinding.EnableDecoderDisallowUnknownFieldstrue时才调用decoder.UseNumber()/decoder.DisallowUnknownFields(),因此自定义Decoder也要实现这两个方法,不实现就没有对应行为。

实现自定义 codec 并替换 json.API

以下是 docs/doc.md “Custom json codec at runtime” 一节给出的完整示例。它用json-iterator/go的配置(EscapeHTMLSortMapKeysValidateJsonRawMessage,经Froze()冻结后使用)承载序列化逻辑,前置条件是你的模块引入了该依赖:

package main import ( "io" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/codec/json" jsoniter "github.com/json-iterator/go" ) var customConfig = jsoniter.Config{ EscapeHTML: true, SortMapKeys: true, ValidateJsonRawMessage: true, }.Froze() // implement api.JsonApi type customJsonApi struct { } func (j customJsonApi) Marshal(v any) ([]byte, error) { return customConfig.Marshal(v) } func (j customJsonApi) Unmarshal(data []byte, v any) error { return customConfig.Unmarshal(data, v) } func (j customJsonApi) MarshalIndent(v any, prefix, indent string) ([]byte, error) { return customConfig.MarshalIndent(v, prefix, indent) } func (j customJsonApi) NewEncoder(writer io.Writer) json.Encoder { return customConfig.NewEncoder(writer) } func (j customJsonApi) NewDecoder(reader io.Reader) json.Decoder { return customConfig.NewDecoder(reader) } func main() { //Replace the default json api json.API = customJsonApi{} //Start your gin engine router := gin.Default() router.Run(":8080") }

注意赋值的位置:文档要求json.API = customJsonApi{}必须发生在引擎启动之前(“Before your engine starts”),即先替换、再gin.Default()router.Run(":8080")

替换后生效的范围覆盖 Gin 内部所有 JSON 出入口:

  • 请求绑定:binding/json.go 的decodeJSON通过json.API.NewDecoder(r)解析请求体,之后走校验;
  • 表单中的 JSON 字段:binding/form_mapping.go 用json.API.Unmarshal解码;
  • 响应渲染:render/json.go 中JSONIndentedJSONSecureJSONJsonpJSONAsciiJSONPureJSON分别走json.API.Marshal/MarshalIndent/NewEncoder
  • 错误序列化:errors.go 中Error.JSON()json.API.Marshal(msg.JSON())生成错误对象。

也就是说,一处赋值后,绑定与渲染两侧都换成你的逻辑,不存在只改一半的情况。

验证替换是否生效

仓库中的 binding/json_test.go 的TestCustomJsonCodec展示了完整的验证模式,可以照搬到自己的项目里作为测试:

func TestCustomJsonCodec(t *testing.T) { // Restore json encoding configuration after testing oldMarshal := json.API defer func() { json.API = oldMarshal }() // Custom json api json.API = customJsonApi{} // test decode json obj := customReq{} err := jsonBinding{}.BindBody([]byte(`{"time_empty":null,"time_struct": "2001-12-05 10:01:02.345","time_nil":null,"time_pointer":"2002-12-05 10:01:02.345"}`), &obj) require.NoError(t, err) assert.Equal(t, zeroTime, obj.TimeEmpty) assert.Equal(t, time.Date(2001, 12, 5, 10, 1, 2, 345000000, time.Local), obj.TimeStruct) assert.Nil(t, obj.TimeNil) assert.Equal(t, time.Date(2002, 12, 5, 10, 1, 2, 345000000, time.Local), *obj.TimePointer) // test encode json w := httptest.NewRecorder() err2 := (render.PureJSON{Data: obj}).Render(w) require.NoError(t, err2) assert.JSONEq(t, "{\"time_empty\":null,\"time_struct\":\"2001-12-05 10:01:02.345\",\"time_nil\":null,\"time_pointer\":\"2002-12-05 10:01:02.345\"}\n", w.Body.String()) assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) }

该测试的判定标准(来自文档中的测试代码,属示例结果):

  1. 解码侧:jsonBinding{}.BindBody能按自定义逻辑解析请求体,字段值与预期一致;
  2. 编码侧:render.PureJSON{}.Render(w)输出的响应体与预期 JSON 相等(assert.JSONEq),且Content-Typeapplication/json; charset=utf-8

测试文件里还给出了一个更有说服力的定制例子:通过customConfig.RegisterExtension注册TimeEx/TimePointerEx扩展(binding/json_test.go),让time.Time"2006-01-02 15:04:05.000"本地时区格式序列化/反序列化、零值输出为null。如果你的需求正是这类类型级定制,这个扩展写法可以直接参考。

另外注意测试开头保存并defer恢复原json.API的写法:json.API是包级全局变量,在测试中改动它会影响同包其他测试,验证完务必还原。

限制与边界

  • 赋值时机是硬约束:文档要求替换发生在引擎启动之前,先跑起来再改json.API的行为没有文档支撑;
  • 自定义DecoderUseNumber/DisallowUnknownFields只有在 binding 的两个全局开关置为true时才被调用,实现里不要假设它们一定被触发;
  • 运行时替换只影响codec/json这一个包提供的 codec 路径;如果你本来就是用构建标签(jsonitergo_jsonsonic)编译的,运行时赋值会把编译期选定的实现覆盖掉,反之两者指向同一变量、后赋值者生效。

【免费下载链接】ginGin is a high-performance HTTP web framework written in Go. It provides a Martini-like API but with significantly better performance—up to 40 times faster—thanks to httprouter. Gin is designed for building REST APIs, web applications, and microservices.项目地址: https://gitcode.com/GitHub_Trending/gi/gin

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 12:37:54

Claudian 使用指南:把 AI 编程助手请进 Obsidian 笔记库

Claudian 使用指南:把 AI 编程助手请进 Obsidian 笔记库 【免费下载链接】claudian An Obsidian plugin that embeds Claude Code/Codex as an AI collaborator in your vault 项目地址: https://gitcode.com/GitHub_Trending/cl/claudian Claudian 是一个 O…

作者头像 李华
网站建设 2026/9/11 12:36:45

Git小技巧:一个本地仓库同时推送到多个远程仓库

去年我把维护了大半年的一个开源小工具放在GitHub上,后来公司内部也想把代码镜像到自建的GitLab里作为备份。一开始我的做法很笨:先推GitHub,再切到GitLab的地址推一次,忘了切换就推错地方。后来花了一晚上研究Git的remote工作机制…

作者头像 李华
网站建设 2026/9/11 12:35:46

Python面向对象一文精通:类、继承、多态与实战避坑指南

Python面向对象一文精通先直接说结论:Python的面向对象(OOP)不是一套需要背下来的语法,而是一种组织代码的思维方式。我见过太多初学者卡在“类、对象、继承”这些抽象名词上,其实换个角度理解——你只是想把数据和操作…

作者头像 李华
网站建设 2026/9/11 12:33:54

从功率训练到智能骑行台:迈金生态设备配置与顽鹿竞技实战指南

如果你最近两年开始认真骑公路车,或者是铁三爱好者,那大概率绕不开两个词:功率训练和虚拟骑行。我自己折腾设备这两年,感触最深的是,国内这套骑行智能硬件生态里,迈金科技几乎把从入门到进阶的坑都补得差不…

作者头像 李华
网站建设 2026/9/11 12:33:11

Flink SQL + Kafka 实时统计实战:从建表、窗口聚合到踩坑排查

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华