本文聚焦 Go 语言中如何遍历数组/切片中的结构体对象,并获取字段的多种方法。通过实际示例,覆盖从基础遍历到通过反射读取字段的高级应用,帮助你在实际项目中更高效地处理数据结构。
1. 基础:使用 range 遍历结构体数组/切片
range 是 Go 语言遍历集合的核心语法,在遍历结构体切片时会逐项返回索引和值。
在默认写法中,循环变量“v”是结构体元素的一个副本,因此对 v 的修改不会改变原切片中的元素。此特性要牢记,尤其是在需要就地修改字段时。

1.1 range 的工作机制
使用 range 遍历切片或数组时,常见模式是同时获取索引和值,但要注意“值”的复制问题。
对结构体体积较大时,请优先使用下标访问以避免不必要的拷贝,或者通过取地址的方式直接修改原有元素。
package mainimport "fmt"type User struct {ID intName stringAge int
}func main() {users := []User{{ID: 1, Name: "Alice", Age: 30},{ID: 2, Name: "Bob", Age: 25},}// 基本遍历,v 为副本for i, v := range users {fmt.Printf("%d: %s (%d)\n", i, v.Name, v.Age)}
}
1.2 使用下标访问以修改字段
如果需要在遍历过程中修改原切片中的结构体字段,推荐通过下标访问,直接操作切片元素本身。
通过下标访问可以避免结构体副本带来的修改失效问题,适用于需要就地更新的场景。
package mainimport "fmt"type User struct {ID intName stringAge int
}func main() {users := []User{{ID: 1, Name: "Alice", Age: 30},{ID: 2, Name: "Bob", Age: 25},}// 通过下标修改原切片中的元素for i := range users {users[i].Name = strings.ToUpper(users[i].Name)}fmt.Println(users)
}
2. 访问结构体字段的不同方式
字段访问是处理结构体数据的核心能力,下面从直接访问、指针修改、以及反射三种方式展开。
需要区分可寻址性与拷贝成本,以选择最合适的遍历策略。
2.1 直接字段访问(只读时最简单)
在 range 循环中直接访问字段非常简单,适用于只读操作或对小结构体进行频繁访问的场景。
直接读取字段时,请确保变量确实是结构体副本中的字段,否则需要通过下标或指针获取地址后再操作。
package mainimport "fmt"type Product struct {ID intName stringPrice float64
}func main() {products := []Product{{ID: 101, Name: "Widget", Price: 9.99},{ID: 102, Name: "Gadget", Price: 12.50},}for _, p := range products {// 直接访问字段(只读)fmt.Printf("Product: %s - $%.2f\n", p.Name, p.Price)}
}
2.2 通过指针遍历以修改字段
若需要在遍历过程中就地更新字段,推荐通过取切片元素的地址来获得可修改的指针。
通过指针可以避免无意中修改到副本,确保对原数据的直接影响。
package mainimport "fmt"type User struct {ID intName stringAge int
}func main() {users := []User{{ID: 1, Name: "Alice", Age: 30},{ID: 2, Name: "Bob", Age: 25},}// 通过下标获取指针来修改for i := range users {p := &users[i]if p.Age > 28 {p.Name = "Senior " + p.Name}}fmt.Println(users)
}
2.3 使用反射读取字段(动态场景)
反射提供了在运行时动态读取字段名和值的能力,适用于字段集合不确定、或需要通用处理的场景。
在使用反射时,导出字段(字段名首字母大写)在不同包间访问更安全,并且类型转换需要谨慎处理。
package mainimport ("fmt""reflect"
)type Customer struct {ID intName stringEmail stringAge int
}func main() {customers := []Customer{{ID: 1, Name: "Amy", Email: "amy@example.com", Age: 28},{ID: 2, Name: "Dan", Email: "dan@example.org", Age: 35},}for i := range customers {v := reflect.ValueOf(&customers[i]).Elem()t := v.Type()for j := 0; j < v.NumField(); j++ {field := t.Field(j)val := v.Field(j).Interface()fmt.Printf("%s=%v ", field.Name, val)}fmt.Println()}// 获取特定字段的值(通过字段名)if len(customers) > 0 {v := reflect.ValueOf(&customers[0]).Elem()f := v.FieldByName("Name")if f.IsValid() && f.Kind() == reflect.String {fmt.Println("First customer's name:", f.String())}}
}
3. 使用反射获取字段集合(实操示例)
有时你需要一次性提取某个字段的所有值,或对字段进行过滤性处理,反射提供了灵活的路径。
通过 FieldByName 与 Field 的组合,可以在不改变结构体定义的前提下完成动态字段提取,但需要注意性能成本。
3.1 动态获取字段值的集合
下面的示例展示如何从结构体切片中,动态地提取同名字段的所有值,形成一个新的值列表。
package mainimport ("fmt""reflect"
)type Item struct {ID intTitle stringQty int
}func main() {items := []Item{{ID: 1, Title: "Book", Qty: 3},{ID: 2, Title: "Pen", Qty: 10},}// 提取所有 Title 的值titles := make([]string, 0, len(items))for i := range items {v := reflect.ValueOf(items[i])if f := v.FieldByName("Title"); f.IsValid() && f.Kind() == reflect.String {titles = append(titles, f.String())}}fmt.Println("Titles:", titles)
}
3.2 通过字段标签进行筛选和映射
如果结构体字段带有标签(如 json、db 等),你可以结合反射按标签筛选字段并做映射。
这是在处理数据库行映射、JSON 序列化时常见的技术路径。
package mainimport ("fmt""reflect"
)type Row struct {ID int `db:"id" json:"id"`Name string `db:"name" json:"name"`Tag string `db:"tag" json:"tag"`
}func main() {rows := []Row{{ID: 1, Name: "Alpha", Tag: "t1"},{ID: 2, Name: "Beta", Tag: "t2"},}// 通过结构体标签过滤字段名for _, r := range rows {v := reflect.ValueOf(r)t := v.Type()for i := 0; i < v.NumField(); i++ {field := t.Field(i)// 输出字段的 json 标签值(若有)jsonTag := field.Tag.Get("json")if jsonTag != "" {val := v.Field(i).Interface()fmt.Printf("%s=%v ", jsonTag, val)}}fmt.Println()}
}
总结性提示:反射是强大但有成本的工具,频繁的字段读取应优先使用静态类型的直接访问方式,只有在字段集合不确定或需要动态行为时再考虑反射。


