Question

Iterate Through Struct Fields in Go with Reflection

gogo-reflect

Question

Given this Go struct:

type Example struct {
    aNumber uint32
    aString string
}

r := &Example{
    aNumber: (2 << 31) - 1,
    aString: "....",
}

I can iterate over selected field values by manually building a slice:

for _, value := range []interface{}{r.aNumber, r.aString} {
    // Use value
}

Is there a more versatile way to create this slice without listing every field individually? Alternatively, what is the proper way to iterate through all fields of a struct using Go's reflect package? For example, after calling reflect.ValueOf(*r).Field(0), what should I do next?

Short Answer

Go does not provide a normal for range operation for struct fields. If you need generic field-by-field processing, use the reflect package. Reflection lets you count fields and retrieve each value at runtime, but it has important limitations, especially for unexported fields. When a collection is meant to be iterated, a slice or map is usually a better data model than a struct.

Concept

A Go struct is a fixed record: each field has a name, type, and position known at compile time.

type User struct {
    Name  string
    Age   int
    Admin bool
}

Unlike a slice, array, or map, a struct is not a collection that supports range. Its fields can have unrelated types, so this is not valid:

// Invalid: structs cannot be ranged over.
for _, value := range user {
    fmt.Println(value)
}

Use reflection when code does not know the exact struct type until runtime. Reflection can inspect a struct's metadata and values, which is useful for generic tools such as serializers, validators, loggers, and configuration loaders.

However, reflection should not be the default solution. If your program's main task is to process a variable number of similar items, model those items as a slice or map. A struct is best when fields have distinct meanings, such as Name, Age, and Email.

Mental Model

Think of a struct as a paper form with labeled boxes:

  • Name is one labeled box.
  • Age is another labeled box.
  • Each box can hold a different kind of value.

Normally, Go expects you to refer to a box by its label: user.Name.

Reflection is like examining the form with a scanner. The scanner can discover how many boxes exist, read their labels, and inspect their contents. This is flexible, but less direct and less type-safe than using the labels yourself.

A slice is different: it is a row of similar boxes numbered 0, 1, 2, and so on. That is why slices naturally support iteration.

Take Quiz

Syntax and Examples

To inspect a struct through a pointer, first get the value the pointer refers to with Elem().

package main

import (
    "fmt"
    "reflect"
)

type Example struct {
    Number uint32
    Text   string
}

func main() {
    r := &Example{
        Number: (2 << 31) - 1,
        Text:   "Hello",
    }

    value := reflect.ValueOf(r).Elem()
    typ := value.Type()

    for i := 0; i < value.NumField(); i++ {
        fieldInfo := typ.Field(i)
        fieldValue := value.Field(i)

        fmt.Printf("%s = %v\n", fieldInfo.Name, fieldValue.Interface())
    }
}

Output:

Number = 2147483647
Text = Hello

Important pieces:

  • reflect.ValueOf(r) creates a reflection value for r, which is a *Example pointer.
  • .Elem() follows that pointer and returns the underlying Example struct value.
  • NumField() returns the number of declared struct fields.
  • Field(i) retrieves the value at position i.
  • Type().Field(i) retrieves metadata, including the field name and struct tags.
  • Interface() converts the reflected value to any (formerly written as interface{}).

The example uses exported fields (Number, Text). A field name starting with an uppercase letter is exported in Go.

Step by Step Execution

Consider this code:

person := &Person{Name: "Mina", Age: 28}
value := reflect.ValueOf(person).Elem()
typ := value.Type()

for i := 0; i < value.NumField(); i++ {
    fmt.Println(typ.Field(i).Name, value.Field(i).Interface())
}

Assume the type is:

type Person struct {
    Name string
    Age  int
}

Execution proceeds as follows:

  1. person holds a pointer to a Person value.
  2. reflect.ValueOf(person) represents the pointer, not the struct itself.
  3. .Elem() follows the pointer, producing a reflected Person struct.
  4. value.NumField() returns 2 because Person has Name and Age.
  5. On the first loop iteration, i is 0:
    • typ.Field(0).Name is "Name".
    • value.Field(0).Interface() is the string "Mina".
  6. On the second iteration, i is 1:
    • typ.Field(1).Name is "Age".
    • value.Field(1).Interface() is the integer 28.
  7. The loop ends after index 1.

The output is:

Name Mina
Age 28

Real World Use Cases

Reflection-based struct iteration is useful when the same logic must work for many different struct types.

  • Validation: Inspect fields with tags such as validate:"required" and reject missing values.
  • JSON or database mapping: Libraries inspect field names and tags to decide which data to encode or decode.
  • Logging and debugging: Print a record's field names and values without writing custom logging for every type.
  • Configuration loading: Match environment variables or configuration keys to struct fields.
  • Command-line parsing: Populate fields from command-line flags based on tags.

For application-specific logic, direct field access is clearer:

if user.Name == "" {
    return errors.New("name is required")
}

Reflection is more appropriate when writing reusable infrastructure that must support many structs.

Take Quiz

Real Codebase Usage

In real Go projects, reflection is commonly isolated in a small utility, library, or framework boundary. Business logic usually remains explicit and type-safe.

Check the kind before treating a value as a struct

A reusable helper should avoid panics when called with the wrong value:

func PrintFields(input any) {
    value := reflect.ValueOf(input)

    if value.Kind() == reflect.Ptr {
        if value.IsNil() {
            return
        }
        value = value.Elem()
    }

    if value.Kind() != reflect.Struct {
        return
    }

    typ := value.Type()
    for i := 0; i < value.NumField(); i++ {
        field := value.Field(i)
        if !field.CanInterface() {
            continue
        }
        fmt.Printf("%s=%v\n", typ.Field(i).Name, field.Interface())
    }
}

This uses guard clauses to handle pointers, nil pointers, and non-struct inputs early.

Use tags to control generic behavior

Real code often reads tags while iterating:

type Account struct {
    ID       int    `json:"id"`
    Password string `json:"-"`
}

A serializer can inspect the json tag and skip Password. Most projects use established packages such as encoding/json rather than writing this reflection logic themselves.

Prefer a slice for repeated data

If fields are logically a list, store them as one:

type Report struct {
    Scores []int
}

for _, score := range report.Scores {
    fmt.Println(score)
}

This avoids reflection and makes additions or removals of items straightforward.

Common Mistakes

Trying to range over a struct

// Does not compile.
for _, value := range example {
    fmt.Println(value)
}

Only arrays, slices, strings, maps, and channels support range. Use direct access, reflection, or a different data structure.

Forgetting Elem() for a pointer

value := reflect.ValueOf(r)
fmt.Println(value.NumField()) // Panic: value is a pointer, not a struct

Use Elem() after confirming the value is a non-nil pointer:

value := reflect.ValueOf(r).Elem()

Calling Interface() on an unexported field

type Example struct {
    number uint32
}

value := reflect.ValueOf(Example{number: 7})
fmt.Println(value.Field(0).Interface()) // Panic

Reflection protects unexported fields. Before calling Interface(), check CanInterface():

field := value.Field(0)
if field.CanInterface() {
    fmt.Println(field.Interface())
}

For generic reflection code, prefer exported fields. Do not use unsafe merely to bypass visibility rules.

Assuming every value has the same type

field.Interface() returns any, so type-specific operations require a type assertion or type switch:

switch v := field.Interface().(type) {
case string:
    fmt.Println("string length:", len(v))
case int:
    fmt.Println("integer:", v)
}

Using reflection when a slice is the right model

A struct with fields such as Item1, Item2, and Item3 is difficult to extend and iterate. Prefer:

type Order struct {
    Items []string
}

Then use a normal range loop.

Comparisons

ApproachBest forAdvantagesLimitations
Direct field accessKnown fields in normal application codeType-safe, clear, fastMust write each field explicitly
Slice or arrayA sequence of similar valuesNatural range, easy to grow with slicesElements normally share one type
MapValues looked up by dynamic keysNatural range, flexible keysLess structure; map iteration order is not guaranteed
ReflectionGeneric tools that accept many struct typesCan inspect fields and tags at runtimeMore complex, slower, can panic if misused

A struct with an any slice is possible:

values := []any{r.Number, r.Text}

But this loses static type information. It is useful only when mixed values genuinely need uniform handling.

For fixed fields with different meanings, keep a struct. For repeatable values, choose a slice. For reusable metadata-driven tooling, use reflection.

Take Quiz

Cheat Sheet

value := reflect.ValueOf(input)
  • Gets a reflected value.
value = value.Elem()
  • Follows a pointer to its target.
  • Call only when value.Kind() == reflect.Ptr and !value.IsNil().
if value.Kind() != reflect.Struct {
    return
}
  • Ensure the target is a struct before using struct operations.
typ := value.Type()
for i := 0; i < value.NumField(); i++ {
    info := typ.Field(i)
    field := value.Field(i)

    if field.CanInterface() {
        fmt.Println(info.Name, field.Interface())
    }
}

Quick rules:

  • Structs cannot be iterated with range.
  • NumField and Field require a reflected struct value.
  • Type().Field(i) gives field metadata; Value.Field(i) gives its value.
  • Interface() can panic for unexported fields; use CanInterface().
  • Use exported field names for generic reflection-based access.
  • Prefer slices or maps when iteration is central to the data model.

FAQ

Can you use range on a struct in Go?

No. Go's range works with arrays, slices, strings, maps, and channels, but not structs.

How do I iterate over fields of a pointer to a struct?

Use reflect.ValueOf(pointer).Elem() to obtain the pointed-to struct, then loop from 0 to NumField() - 1.

Why does reflect.Value.Interface() panic for a field?

The field is often unexported. Check field.CanInterface() first, or design generic code around exported fields.

What is the difference between Value.Field(i) and Type.Field(i)?

Value.Field(i) returns the field's runtime value. Type.Field(i) returns metadata, such as its name, type, and tags.

Should I use reflection to process ordinary structs?

Usually no. Direct field access is clearer and safer when the struct type is known. Reflection is best for generic infrastructure.

How can I modify a struct field with reflection?

Start with a pointer, call Elem(), then ensure the field's CanSet() method returns true. The new value must have an assignable type.

Is reflection slower than direct field access?

Yes, reflection generally has additional runtime work and less compile-time checking. Use it when its flexibility is needed, not for routine field access.

Take Quiz

Mini Project

Description

Build a small struct inspection utility for debug output. It accepts a struct or a pointer to a struct, prints each exported field name and value, and safely rejects unsupported inputs. This demonstrates the checks production reflection helpers need before accessing fields.

Goal

Create a function that prints the exported fields of any non-nil struct value or pointer to a struct.

Requirements

Accept both a struct value and a pointer to a struct. Return a useful error for nil pointers and non-struct values. Print each exported field as Name: value. Skip fields that cannot safely be converted with Interface(). Use reflect rather than hard-coding a particular struct type.

Take Quiz

Keep learning

Related questions

View more