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 校验)或自定义某类类型的时间格式——传统做法是加构建标签(jsoniter、go_json、sonic)在编译期切换 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 的五个方法;它返回的Encoder和Decoder也要满足 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.EnableDecoderUseNumber或binding.EnableDecoderDisallowUnknownFields为true时才调用decoder.UseNumber()/decoder.DisallowUnknownFields(),因此自定义Decoder也要实现这两个方法,不实现就没有对应行为。
实现自定义 codec 并替换 json.API
以下是 docs/doc.md “Custom json codec at runtime” 一节给出的完整示例。它用json-iterator/go的配置(EscapeHTML、SortMapKeys、ValidateJsonRawMessage,经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 中
JSON、IndentedJSON、SecureJSON、JsonpJSON、AsciiJSON、PureJSON分别走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")) }该测试的判定标准(来自文档中的测试代码,属示例结果):
- 解码侧:
jsonBinding{}.BindBody能按自定义逻辑解析请求体,字段值与预期一致; - 编码侧:
render.PureJSON{}.Render(w)输出的响应体与预期 JSON 相等(assert.JSONEq),且Content-Type为application/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的行为没有文档支撑; - 自定义
Decoder的UseNumber/DisallowUnknownFields只有在 binding 的两个全局开关置为true时才被调用,实现里不要假设它们一定被触发; - 运行时替换只影响
codec/json这一个包提供的 codec 路径;如果你本来就是用构建标签(jsoniter、go_json、sonic)编译的,运行时赋值会把编译期选定的实现覆盖掉,反之两者指向同一变量、后赋值者生效。
【免费下载链接】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),仅供参考