在结构体中遍历字符串字段

12

我想要迭代结构体的字符串字段,以便进行一些清理/验证(使用 strings.TrimSpacestrings.Trim 等)。

现在我的代码有一个混乱的 switch-case,不太可扩展,而且这不是我的应用程序的热点(一个 web 表单),因此在这里利用 reflect 似乎是一个不错的选择。

然而,我对如何实现这个目标有些困惑,反射文档对我来说有点难以理解(我已经查看了一些其他的验证包,但它们太重量级了,而且我已经在使用 gorilla/schema 进行非编组部分):

  • 迭代结构体
  • 对于每个字符串类型的字段,使用 strings 包中所需的任何操作(例如: field = strings.TrimSpace(field)
  • 如果存在 field.Tag.Get("max"),我们将使用该值(strconv.Atoi,然后 unicode.RuneCountInString)
  • 提供一个与错误接口类型兼容的错误切片

type FormError []string         

type Listing struct {
        Title string `max:"50"`
        Location string `max:"100"`
        Description string `max:"10000"`
        ExpiryDate time.Time
        RenderedDesc template.HTML
        Contact string `max:"255"`
    }

    // Iterate over our struct, fix whitespace/formatting where possible
    // and return errors encountered
    func (l *Listing) Validate() error {

       typ := l.Elem().Type()

       var invalid FormError
       for i = 0; i < typ.NumField(); i++ {
           // Iterate over fields
           // For StructFields of type string, field = strings.TrimSpace(field)
           // if field.Tag.Get("max") != "" {
           //     check max length/convert to int/utf8.RuneCountInString
                  if max length exceeded, invalid = append(invalid, "errormsg")
       }

       if len(invalid) > 0 {
           return invalid
       } 

       return nil
   }


   func (f FormError) Error() string {
       var fullError string
       for _, v := range f {
           fullError =+ v + "\n"
       }
       return "Errors were encountered during form processing: " + fullError
   }

提前致谢。

3个回答

17
你需要的主要是 reflect.Value 上的方法,包括 NumFields() int 和 Field(int)。你唯一缺少的是字符串检查和 SetString 方法。请保留 HTML 标签。
package main

import "fmt"
import "reflect"
import "strings"

type MyStruct struct {
    A,B,C string
    I int
    D string
    J int
}

func main() {
    ms := MyStruct{"Green ", " Eggs", " and ", 2, " Ham      ", 15}
    // Print it out now so we can see the difference
    fmt.Printf("%s%s%s%d%s%d\n", ms.A, ms.B, ms.C, ms.I, ms.D, ms.J)

    // We need a pointer so that we can set the value via reflection
    msValuePtr := reflect.ValueOf(&ms)
    msValue := msValuePtr.Elem()

    for i := 0; i < msValue.NumField(); i++ {
        field := msValue.Field(i)

        // Ignore fields that don't have the same type as a string
        if field.Type() != reflect.TypeOf("") {
            continue
        }

        str := field.Interface().(string)
        str = strings.TrimSpace(str)
        field.SetString(str)
    }
    fmt.Printf("%s%s%s%d%s%d\n", ms.A, ms.B, ms.C, ms.I, ms.D, ms.J)
}

(Playground链接)

这里有两个注意点:

  1. 你需要一个指针来修改变量的值。如果你只有一个值,你需要返回修改后的结果。

  2. 试图修改未公开字段通常会导致反射引发 panic。如果你计划修改未公开字段,请确保在包内部进行此操作。

这段代码非常灵活,你可以使用 switch 语句或类型 switch(根据 field.Interface() 返回的值)来实现不同类型的行为。

编辑:关于标签的行为,你似乎已经弄清楚了。一旦你有了 field,并检查它是一个字符串,你可以直接使用 field.Tag.Get("max") 并从那里解析。

编辑2:我在标签上犯了一个小错误。标签是结构体的反射类型的一部分,因此要获取它们,你可以使用(这有点冗长)msValue.Type().Field(i).Tag.Get("max")

(带有可用的 Tag get 的评论中发布的代码的Playground版本)。


太棒了,你帮了我很多。我的所有字段都已导出(结构体也反映了我的数据库模式),但是Validate与Listing在同一个包中,所以应该没问题。我仍然遇到的唯一问题是使用field.Tag.Get("max") - 如果不是字段,我应该在哪个方法上调用Tag?http://play.golang.org/p/yMRLFCW4vt - elithrar
1
我刚刚进行了编辑。标签是结构体本身的reflect.Type的一部分,因此您必须从msValue.Type()重新获取字段,然后从相应的字段获取标签。 - Linear
太好了 - 现在这个(http://play.golang.org/p/Uks300ZsS3)运行得很好。我按照Tyson的答案声明了`listType:= reflect.TypeOf(*l),以提供访问字段Tag`的快捷方式。再次感谢您的帮助! - elithrar

5

我来晚了,但是既然我已经处理了,这里有一个解决方案:

type FormError []*string

type Listing struct {
    Title        string `max:"50"`
    Location     string `max:"100"`
    Description  string `max:"10000"`
    ExpiryDate   time.Time
    RenderedDesc template.HTML
    Contact      string `max:"255"`
}

// Iterate over our struct, fix whitespace/formatting where possible
// and return errors encountered
func (l *Listing) Validate() error {
    listingType := reflect.TypeOf(*l)
    listingValue := reflect.ValueOf(l)
    listingElem := listingValue.Elem()

    var invalid FormError = []*string{}
    // Iterate over fields
    for i := 0; i < listingElem.NumField(); i++ {
        fieldValue := listingElem.Field(i)
        // For StructFields of type string, field = strings.TrimSpace(field)
        if fieldValue.Type().Name() == "string" {
            newFieldValue := strings.TrimSpace(fieldValue.Interface().(string))
            fieldValue.SetString(newFieldValue)

            fieldType := listingType.Field(i)
            maxLengthStr := fieldType.Tag.Get("max")
            if maxLengthStr != "" {
                maxLength, err := strconv.Atoi(maxLengthStr)
                if err != nil {
                    panic("Field 'max' must be an integer")
                }
                //     check max length/convert to int/utf8.RuneCountInString
                if utf8.RuneCountInString(newFieldValue) > maxLength {
                    //     if max length exceeded, invalid = append(invalid, "errormsg")
                    invalidMessage := `"`+fieldType.Name+`" is too long (max allowed: `+maxLengthStr+`)`
                    invalid = append(invalid, &invalidMessage)
                }
            }
        }
    }

    if len(invalid) > 0 {
        return invalid
    }

    return nil
}

func (f FormError) Error() string {
    var fullError string
    for _, v := range f {
        fullError = *v + "\n"
    }
    return "Errors were encountered during form processing: " + fullError
}

我看到你询问如何处理标签。反射有两个组成部分:类型和值。标签与类型相关联,因此您必须单独获取它而不是字段:listingType := reflect.TypeOf(*l)。然后,您可以从中获取索引字段和标签。


感谢您提供的代码和关于反射中类型与值的澄清。我很好奇为什么我们要使用指向切片(*[]string)的指针作为自定义错误类型的基础?我认为在这里复制不是一个主要问题。我还假设 reflect.TypeOf(*l) 确保我们获取 l 的底层类型? - elithrar
1
[]*string 不是指向切片的指针,而是一个字符串指针的切片。但是,我没有真正使用指针的好理由——这只是我的习惯。是的,你需要解引用 l 来获取结构体的类型。如果不这样做,你会得到指针的类型。 :-) - Tyson

1

我不知道这是否是一个好方法,但我像这样使用它。

https://play.golang.org/p/aQ_hG2BYmMD

你可以将结构体的地址发送给这个函数。 对不起,我的英语不是很好。
trimStruct(&someStruct)

func trimStruct(v interface{}) {
    bytes, err := json.Marshal(v)
    if err != nil {
        fmt.Println("[trimStruct] Marshal Error :", err)
    }
    var mapSI map[string]interface{}
    if err := json.Unmarshal(bytes, &mapSI); err != nil {
        fmt.Println("[trimStruct] Unmarshal to byte Error :", err)
    }
    mapSI = trimMapStringInterface(mapSI).(map[string]interface{})
    bytes2, err := json.Marshal(mapSI)
    if err != nil {
        fmt.Println("[trimStruct] Marshal Error :", err)
    }
    if err := json.Unmarshal(bytes2, v); err != nil {
        fmt.Println("[trimStruct] Unmarshal to b Error :", err)
    }
}

func trimMapStringInterface(data interface{}) interface{} {
    if values, valid := data.([]interface{}); valid {
        for i := range values {
            data.([]interface{})[i] = trimMapStringInterface(values[i])
        }
    } else if values, valid := data.(map[string]interface{}); valid {
        for k, v := range values {
            data.(map[string]interface{})[k] = trimMapStringInterface(v)
        }
    } else if value, valid := data.(string); valid {
        data = strings.TrimSpace(value)
    }
    return data
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接