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

Go jsonnull 出错时怎么查空值分支

来源:17golang原创

时间:2026-09-13 10:25:17 287浏览 收藏

Go 里遇到 jsonnull 出错,先别把所有“空”都改成 len(v) == 0。接口入参至少有四种状态:字段没有传、传了 JSON null、传了空数组 [],以及传了有内容的数组。它们在 PATCH 更新中分别可能代表“保持原值”“清空字段”“设置为空列表”和“替换为新列表”。

要点速览
  • 标准库会把 nil slice 编码成 null,已初始化的空 slice 编码成 []
  • nullable 包装类型要分开记录是否出现过、是否为 null,以及实际 Value。
  • 排错顺序应是原始 JSON → UnmarshalJSON → 业务分支 → MarshalJSON,不能只看最后的响应。

下面用一个可表达三态的泛型包装器说明排查方法。项目使用第三方 jsonnull 时,字段名可能叫 PresentSetValid,判断原则相同。

先用四组输入确认到底是哪一种空

最小复现不要只写一条 {"tags":null}。把字段缺失、显式 null、空数组和非空数组放在同一张表里,先观察包装器的状态变化。

输入字段是否出现数组值PATCH 含义
{}未读取保持原值
{"tags":null}无效值清空字段
{"tags":[]}长度为 0设置为空列表
{"tags":["go"]}长度为 1替换为新列表

如果四组输入最后都走到同一个分支,问题通常不在 JSON 文本,而在包装类型丢失了“字段是否出现”的信息。

Go jsonnull 将字段缺失、null、空数组和有值数组映射到 Present、Valid 与长度状态的示意图
图1:Go jsonnull 四种输入状态的原创技术示意图,null 与空数组分别进入不同分支。

检查标准库的 null 与空数组边界

encoding/json 对 slice 的编码很容易造成误判:nil slice 输出 JSON null,非 nil 但长度为零的 slice 输出 []。反序列化时,JSON null 对 slice 会得到 nil;对字符串、数字、布尔等非指针类型通常不会因为 null 自动报错,而是保留原值,这也是“看起来没生效”的常见来源。

package main

import (
	"encoding/json"
	"fmt"
)

func main() {
	var nilTags []string
	emptyTags := make([]string, 0)

	// 这里故意比较 nil slice 与已初始化空 slice 的 JSON 结果。
	for _, tags := range [][]string{nilTags, emptyTags, {"go"}} {
		data, err := json.Marshal(tags)
		if err != nil {
			panic(err) // 示例中直接终止,业务代码应返回或记录错误。
		}
		fmt.Println(string(data))
	}
}
null
[]
["go"]

排查时把 fmt.Printf("%#v nil=%v len=%d\n", tags, tags == nil, len(tags)) 临时放在解码后。len(tags) == 0 只能说明没有元素,不能说明输入是 null、[] 还是根本没有传。

让 jsonnull 保留 Present、Valid 和 Value

如果业务需要区分“未传”和“显式清空”,包装器至少需要两个布尔状态。下面的示例把 Present=false 留给字段缺失,把 Present=true, Valid=false 留给 JSON null,把空数组作为有效但长度为零的值。

package patch

import (
	"bytes"
	"encoding/json"
)

type Nullable[T any] struct {
	Value   T
	Valid   bool // true 表示输入不是 JSON null,空数组也属于有效值。
	Present bool // true 表示字段在 JSON 中出现过,区分字段缺失。
}

func (n *Nullable[T]) UnmarshalJSON(data []byte) error {
	n.Present = true
	if bytes.Equal(bytes.TrimSpace(data), []byte("null")) {
		var zero T
		n.Value = zero // 显式 null 清掉旧值,避免复用对象时残留。
		n.Valid = false
		return nil
	}

	var value T
	if err := json.Unmarshal(data, &value); err != nil {
		return err // 类型不匹配时交给调用方处理,不能静默当成空值。
	}
	n.Value = value
	n.Valid = true
	return nil
}

type Update struct {
	Tags Nullable[[]string] `json:"tags"`
}

这里最容易漏掉的是对象复用:同一个 Update 变量先解码有值数组、再解码 null 时,如果 UnmarshalJSON 没有把 Value 重置,旧数组可能继续参与业务判断。第三方 jsonnull 出错时,优先打开它的 UnmarshalJSON 实现,确认是否处理了这两个边界。

业务分支不要把 null 和 [] 合并

PATCH 逻辑应先判断字段是否出现,再判断它是否为 null,最后才读取数组长度。顺序反过来就会把“未传”误当成“清空”,或者把“设置空列表”误当成“没有修改”。

func applyTags(current []string, in Nullable[[]string]) []string {
	if !in.Present {
		return current // 字段缺失:调用方没有表达修改意图。
	}
	if !in.Valid {
		return nil // 显式 null:按接口约定清空当前值。
	}
	return in.Value // [] 与非空数组都属于明确的新值。
}

如果接口约定“null 和 [] 都表示清空”,可以在协议层明确合并,但不要让它由 len 或 nil 判断偶然决定。调试日志最好同时记录 PresentValidlen(Value),这样一眼就能知道是输入语义还是业务规则导致了结果。

Go jsonnull 从原始 JSON 经 UnmarshalJSON、Present 和 Valid 判断进入 PATCH 结果分支的示意图
图2:从 UnmarshalJSON 到 PATCH 决策的原创分支示意图,先区分是否出现再处理值。

最后检查 MarshalJSON 与 omitempty

入参分支修好后,还要看响应是否再次把差异抹掉。包装器若实现 MarshalJSON,应规定无效状态输出 null,有效空 slice 输出 [];若结构体字段使用 omitempty,要单独确认它是想隐藏字段,还是必须返回显式 null。不要用响应中的“字段不存在”反推请求一定没有传值。

建议用四组表驱动断言回归:输入 JSON、Present、Valid、Value 是否为 nil、再次编码的 JSON。尤其要加入“同一对象连续解码有值数组再解码 null”的案例,它能抓住残留旧值这一类只在长连接或对象复用场景出现的错误。

相关问题

为什么 len(slice) == 0 不能判断字段缺失?

因为 nil slice、空 slice 和缺失字段都可能得到长度零,但只有包装器的 Present 能记录字段是否在 JSON 中出现。

JSON null 解码到普通字符串为什么没有报错?

encoding/json 对部分非指针基础类型会保留原值;如果业务必须拒绝 null,应在包装器或请求校验层显式检查。

第三方 jsonnull 应该先查哪段代码?

先查 UnmarshalJSON 是否识别 null、是否重置 Value,再查业务层是否先判断 Present,最后查 MarshalJSONomitempty 是否改变了输出。

把空值拆成“缺失、null、空数组、有值数组”四种可观察状态,jsonnull 的大多数分支错误就能从猜测变成可复现、可断言的问题。

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