You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用json.Marshal处理Go外部结构体时如何忽略空字段?

解决外部结构体JSON序列化时忽略null字段的通用方案

因为无法修改外部结构体的json标签,我们可以通过反射递归过滤零值字段或者两次序列化清理null的方式实现需求,以下是两种通用方案:

方案一:反射递归过滤(高效通用,支持嵌套)

通过反射遍历结构体的所有字段,递归处理嵌套的结构体、切片、map,自动过滤掉值为零值(包括指针nil、空切片、空字符串等)的字段,最终将处理后的结构序列化为JSON。

实现代码

package utils

import (
	"encoding/json"
	"reflect"
)

// MarshalWithoutNulls 序列化时自动忽略零值/null字段,支持嵌套结构体
func MarshalWithoutNulls(v interface{}) ([]byte, error) {
	processedVal, err := processReflectValue(reflect.ValueOf(v))
	if err != nil {
		return nil, err
	}
	return json.Marshal(processedVal)
}

func processReflectValue(val reflect.Value) (interface{}, error) {
	// 解引用指针
	for val.Kind() == reflect.Ptr {
		if val.IsNil() {
			return nil, nil
		}
		val = val.Elem()
	}

	switch val.Kind() {
	case reflect.Struct:
		resultMap := make(map[string]interface{})
		structType := val.Type()

		for i := 0; i < val.NumField(); i++ {
			field := val.Field(i)
			fieldType := structType.Field(i)

			// 跳过未导出字段(反射无法访问)
			if !fieldType.IsExported() {
				continue
			}

			// 获取JSON字段名:优先用json tag,没有则用结构体字段名
			jsonFieldName := fieldType.Name
			if jsonTag := fieldType.Tag.Get("json"); jsonTag != "" {
				if commaIdx := indexOfComma(jsonTag); commaIdx != -1 {
					jsonFieldName = jsonTag[:commaIdx]
				} else {
					jsonFieldName = jsonTag
				}
			}

			// 递归处理字段值
			processedField, err := processReflectValue(field)
			if err != nil {
				return nil, err
			}

			// 过滤零值/null字段
			if processedField == nil || isZeroValue(reflect.ValueOf(processedField)) {
				continue
			}

			resultMap[jsonFieldName] = processedField
		}
		return resultMap, nil

	case reflect.Slice, reflect.Array:
		var processedSlice []interface{}
		for i := 0; i < val.Len(); i++ {
			elem, err := processReflectValue(val.Index(i))
			if err != nil {
				return nil, err
			}
			processedSlice = append(processedSlice, elem)
		}
		return processedSlice, nil

	case reflect.Map:
		processedMap := make(map[string]interface{})
		iter := val.MapRange()
		for iter.Next() {
			key := iter.Key().String()
			mapVal := iter.Value()

			processedVal, err := processReflectValue(mapVal)
			if err != nil {
				return nil, err
			}

			if processedVal == nil || isZeroValue(reflect.ValueOf(processedVal)) {
				continue
			}

			processedMap[key] = processedVal
		}
		return processedMap, nil

	default:
		// 基础类型直接返回原始值
		return val.Interface(), nil
	}
}

// isZeroValue 判断值是否为对应类型的零值
func isZeroValue(v reflect.Value) bool {
	switch v.Kind() {
	case reflect.Bool:
		return !v.Bool()
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		return v.Int() == 0
	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
		return v.Uint() == 0
	case reflect.Float32, reflect.Float64:
		return v.Float() == 0
	case reflect.String:
		return v.String() == ""
	case reflect.Ptr, reflect.Interface:
		return v.IsNil()
	case reflect.Slice, reflect.Map:
		return v.Len() == 0
	default:
		return false
	}
}

// indexOfComma 查找json tag中逗号的位置
func indexOfComma(s string) int {
	for i := range s {
		if s[i] == ',' {
			return i
		}
	}
	return -1
}

使用示例

import "external/package/providing/external/struct"
import "your/utils/package/path"

func main() {
	result := externalstruct.ExternalStruct{Success: true}
	resultJson, err := utils.MarshalWithoutNulls(result)
	if err != nil {
		// 错误处理
		panic(err)
	}
	println(string(resultJson))
	// 输出:{"Success":true}
}

方案二:两次序列化清理null(简单易实现)

先将结构体序列化为JSON,再解析为map[string]interface{},遍历map删除值为null的条目,最后重新序列化为JSON。该方案代码简单,但需要两次序列化/反序列化,效率略低。

实现代码

package utils

import (
	"encoding/json"
)

// MarshalWithoutNullsSimple 简单版:通过两次序列化清理null字段
func MarshalWithoutNullsSimple(v interface{}) ([]byte, error) {
	// 第一次序列化得到包含null的JSON
	rawData, err := json.Marshal(v)
	if err != nil {
		return nil, err
	}

	// 解析为map
	var dataMap map[string]interface{}
	if err := json.Unmarshal(rawData, &dataMap); err != nil {
		return nil, err
	}

	// 递归清理null和零值
	cleanMap(dataMap)

	// 重新序列化
	return json.Marshal(dataMap)
}

func cleanMap(m map[string]interface{}) {
	for k, v := range m {
		// 删除null值条目
		if v == nil {
			delete(m, k)
			continue
		}

		// 递归清理嵌套map
		if nestedMap, ok := v.(map[string]interface{}); ok {
			cleanMap(nestedMap)
			// 如果嵌套map为空,也删除
			if len(nestedMap) == 0 {
				delete(m, k)
			}
		}

		// 清理切片中的null元素
		if slice, ok := v.([]interface{}); ok {
			var cleanedSlice []interface{}
			for _, elem := range slice {
				if elem != nil {
					if elemMap, ok := elem.(map[string]interface{}); ok {
						cleanMap(elemMap)
						if len(elemMap) > 0 {
							cleanedSlice = append(cleanedSlice, elemMap)
						}
					} else {
						cleanedSlice = append(cleanedSlice, elem)
					}
				}
			}
			if len(cleanedSlice) == 0 {
				delete(m, k)
			} else {
				m[k] = cleanedSlice
			}
		}
	}
}

注意事项

  1. 未导出字段:反射方案会自动跳过结构体的未导出字段(首字母小写),因为Go反射无法访问这些字段。
  2. JSON标签支持:两种方案都会优先使用结构体字段的json标签作为JSON字段名,兼容已有标签的情况。
  3. 嵌套结构:两种方案都支持递归处理嵌套的结构体、切片、map,无需额外配置。

内容的提问来源于stack exchange,提问作者Renoa

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.24 17:18:22