使用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 } } } }
注意事项
- 未导出字段:反射方案会自动跳过结构体的未导出字段(首字母小写),因为Go反射无法访问这些字段。
- JSON标签支持:两种方案都会优先使用结构体字段的
json标签作为JSON字段名,兼容已有标签的情况。 - 嵌套结构:两种方案都支持递归处理嵌套的结构体、切片、map,无需额外配置。
内容的提问来源于stack exchange,提问作者Renoa
相关产品推荐
相关产品推荐

