如何在 Go 中遍历结构体时排除空字段(零值或 nil 字段)

本文介绍如何使用反射动态获取结构体中非空(非零值、非 nil)字段的名称,适用于表单解析、api 参数过滤等场景。

在 Go 中,结构体字段默认具有其类型的零值(如 string 为 "",int 为 0,指针/切片/映射为 nil)。当从用户输入(如 HTTP 表单或 JSON)初始化结构体后,我们常需仅处理“真正被设置”的字段——即排除所有零值或 nil 的字段。这无法通过简单的字段名遍历实现,而需结合 reflect 包对每个字段值进行语义化判空。

以下是一个健壮、可复用的判空函数,支持常见类型:

func isEmpty(v reflect.Value) bool {
    switch v.Kind() {
    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:
        return v.Uint() == 0
    case reflect.String:
        return v.String() == ""
    case reflect.Bool:
        return !v.Bool()
    case reflect.Ptr, reflect.Slice, reflect.Map, reflect.Interface, reflect.Chan, reflect.Func:
        return v.IsNil()
    case reflect.Struct:
        // 可选:递归判断结构体是否全为零值(需谨慎,可能影响性能)
        // 此处按“非空结构体”视为非零(因无通用 DeepZero 判断),如需严格判断请另行实现
        return false
    case reflect.Float32, reflect.Float64:
        return v.Float() == 0.0
    default:
        return false
    }
}

配合结构体遍历,即可精准提取非空字段名:

package main

import (
    "fmt"
    "reflect"
)

type Users struct {
    Name     string
    Password string
    Age      int
    Token    *string
}

func isEmpty(v reflect.Value) bool {
    switch v.Kind() {
    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:
        return v.Uint() == 0
    case reflect.String:
        return v.String() == ""
    case reflect.Bool:
        return !v.Bool()
    case reflect.Ptr, reflect.Slice, reflect.Map, reflect.Interface, reflect.Chan, reflect.Func:
        return v.IsNil()
    case reflect.Float32, reflect.Float64:
        return v.Float() == 0.0
    }
    return false
}

func getNonEmptyFieldNames(v interface{}) []string {
    val := reflect.ValueOf(v)
    if val.Kind() == reflect.Ptr {
        val = val.Elem()
    }
    if val.Kind() != reflect.Struct {
        panic("getNonEmptyFieldNames: input must be a struct or *struct")
    }

    var names []string
    typ := val.Type()
    for i := 0; i < val.NumField(); i++ {
        field := val.Field(i)
        if !isEmpty(field) {
            names = append(names, typ.Field(i).Name)
        }
    }
    return names
}

func main() {
    token := "abc123"
    u := Users{
        Name:     "Robert",  // non-empty
        Password: "",        // empty string → excluded
        Age:      0,         // zero int → excluded
        Token:    &token,    // non-nil pointer → included
    }

    fields := getNonEmptyFieldNames(u)
    fmt.Println(fields) // Output: [Name Token]
}

注意事项

  • 该方案基于反射,不适用于未导出(小写开头)字段,因 reflect 无法访问私有字段;
  • 对嵌套结构体(struct 类型字段),上述 isEmpty 默认返回 false(即视作“非空”),如需深度判空,需递归调用并结合 reflect.DeepEqual(v.Interface(), reflect.Zero(v.Type()).Interface()),但性能开销显著;
  • 若结构体含指针字段(如 *string),IsNil() 能正确识别未初始化状态;但若指针指向零值(如 ptr := new(string); *ptr = ""),则 *ptr 为空字符串,此时需额外逻辑判断解引用后的值;
  • 生产环境高频调用时,建议缓存 reflect.Type 和字段信息,避免重复反射开销。

总结:通过自定义 isEmpty 辅助函数 + reflect 遍历,可安全、准确地筛选出结构体中真正携带有效数据的字段名,是构建灵活数据处理管道的关键技巧。