英文:
Iterate Over String Fields in Struct
问题
我正在寻找一种方法来迭代结构体的字符串字段,以便进行一些清理/验证操作(使用strings.TrimSpace
、strings.Trim
等函数)。
目前我有一个混乱的switch-case语句,不太可扩展,而且由于这不是我的应用程序的热点部分(一个Web表单),所以在这里利用reflect
似乎是一个不错的选择。
然而,我在如何实现这一点方面遇到了一些困难,而且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"`
}
// 迭代我们的结构体,修复空白/格式,尽可能返回遇到的错误
func (l *Listing) Validate() error {
typ := reflect.TypeOf(*l)
var invalid FormError
for i := 0; i < typ.NumField(); i++ {
// 迭代字段
// 对于类型为字符串的StructField,field = strings.TrimSpace(field)
// if field.Tag.Get("max") != "" {
// 检查最大长度/转换为int/utf8.RuneCountInString
// if 超过最大长度, invalid = append(invalid, "错误消息")
// }
}
if len(invalid) > 0 {
return invalid
}
return nil
}
func (f FormError) Error() string {
var fullError string
for _, v := range f {
fullError += v + "\n"
}
return "在表单处理过程中遇到了错误:" + fullError
}
希望能帮到你。
英文:
I'm looking to iterate over the string fields of a struct so I can do some clean-up/validation (with strings.TrimSpace
, strings.Trim
, etc).
Right now I have a messy switch-case that's not really scalable, and as this isn't in a hot spot of my application (a web form) it seems leveraging reflect
is a good choice here.
I'm at a bit of a roadblock for how to implement this however, and the reflect docs are a little confusing to me (I've been digging through some other validation packages, but they're way too heavyweight + I'm using gorilla/schema for the unmarshalling part already):
-
Iterate over the struct
-
For each field of type string, apply whatever I need to from the
strings
package i.e.field = strings.TrimSpace(field)
-
If there exists a field.Tag.Get("max"), we'll use that value (strconv.Atoi, then unicode.RuneCountInString)
-
Provide an error slice that's also compatible with the error interface type
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 }
Thanks in advance.
答案1
得分: 17
你想要的主要是reflect.Value
上的方法NumFields() int
和Field(int)
。你真正缺少的只是字符串检查和SetString
方法。
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}
// 打印出来以便我们可以看到区别
fmt.Printf("%s%s%s%d%s%d\n", ms.A, ms.B, ms.C, ms.I, ms.D, ms.J)
// 我们需要一个指针,以便可以通过反射设置值
msValuePtr := reflect.ValueOf(&ms)
msValue := msValuePtr.Elem()
for i := 0; i < msValue.NumField(); i++ {
field := msValue.Field(i)
// 忽略类型与字符串不同的字段
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)
}
这里有两个注意事项:
-
你需要一个指向要更改的内容的指针。如果你有一个值,你需要返回修改后的结果。
-
尝试修改未导出的字段通常会导致反射引发 panic。如果你计划修改未导出的字段,请确保在包内部进行此操作。
这段代码相当灵活,如果你需要根据类型的不同而有不同的行为,你可以使用 switch 语句或类型断言(对 field.Interface() 返回的值进行类型判断)。
编辑:关于标签的行为,你似乎已经弄清楚了。一旦你有了 field 并且已经检查过它是一个字符串,你可以直接使用field.Tag.Get("max")
并从中解析。
编辑2:我在标签上犯了一个小错误。标签是结构体的reflect.Type
的一部分,所以要获取它们,你可以使用(这有点冗长)msValue.Type().Field(i).Tag.Get("max")
。
(在评论中发布的代码的Playground 版本,包含一个可工作的标签获取示例)。
英文:
What you want is primarily the methods on reflect.Value called NumFields() int
and Field(int)
. The only thing you're really missing is the string check and SetString
method.
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)
}
There are two caveats here:
-
You need a pointer to what you're going to change. If you have a value, you'll need to return the modified result.
-
Attempts to modify unexported fields generally will cause reflect to panic. If you plan on modifying unexported fields, make sure to do this trick inside the package.
This code is rather flexible, you can use switch statements or type switches (on the value returned by field.Interface()) if you need differing behavior depending on the type.
Edit: As for the tag behavior, you seem to already have that figured out. Once you have field and have checked that it's a string, you can just use field.Tag.Get("max")
and parse it from there.
Edit2: I made a small error on the tag. Tags are part of the reflect.Type of a struct, so to get them you can use (this is a bit long-winded) msValue.Type().Field(i).Tag.Get("max")
(Playground version of the code you posted in the comments with a working Tag get).
答案2
得分: 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"`
}
// 遍历结构体,修复可能的空格/格式问题,并返回遇到的错误
func (l *Listing) Validate() error {
listingType := reflect.TypeOf(*l)
listingValue := reflect.ValueOf(l)
listingElem := listingValue.Elem()
var invalid FormError = []*string{}
// 遍历字段
for i := 0; i < listingElem.NumField(); i++ {
fieldValue := listingElem.Field(i)
// 对于类型为string的StructFields,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")
}
// 检查最大长度/转换为int/utf8.RuneCountInString
if utf8.RuneCountInString(newFieldValue) > maxLength {
// 如果超过最大长度,invalid = append(invalid, "errormsg")
invalidMessage := `"` + fieldType.Name + `"太长了(最大允许长度: ` + 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 "在表单处理过程中遇到了错误:" + fullError
}
我看到你问到了如何处理标签。反射有两个组成部分:类型和值。标签与类型相关联,因此你必须单独获取它,而不是从字段中获取:`listingType := reflect.TypeOf(*l)`。然后你可以从中获取索引字段和标签。
英文:
I got beat to the punch, but since I went to the work, here's a solution:
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
}
I see you asked about how to do the tags. Reflection has two components: a type and a value. The tag is associated with the type, so you have to get it separately than the field: listingType := reflect.TypeOf(*l)
. Then you can get the indexed field and the tag from that.
答案3
得分: 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
}
英文:
I don't know if it's a good way, but I use it like this.
https://play.golang.org/p/aQ_hG2BYmMD
You can send the address of a struct to this function.
Sorry for My English is not very good.
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
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论