Reflection in Go

By Syncfusion®, Inc. · Last updated

Go reflection with the reflect package: reflect.Type, reflect.Value and Kind, inspecting types and struct fields and tags, reading and setting values, and DeepEqual.

What Is Reflection?

Reflection = a program looking at its own values and types while it runs.

With reflection, code can ask a value:

  • What type are you?
  • What kind of thing are you: a number, a struct, a slice?
  • If you are a struct, what fields do you have, and what are their tags?
  • Can I change you?

Go's reflect package answers these questions.

Simple lesson: reflection lets a program inspect values whose type it did not know when it was written.

A Real-World Example

Think of the school's lost-and-found box.

  • The office does not know in advance what will be dropped in.
  • For each item, they look at it: Is it a bottle? A jacket? A book?
  • For a bag, they open it and list what is inside.
  • Then they write a label for it.

Now the mapping:

  • The unknown item = a value of type any.
  • Looking at it = reflect.TypeOf and reflect.ValueOf.
  • Opening the bag = going through a struct's fields.
  • The label = what your code prints or saves.

Why Reflection Is Used

  • encoding/json uses it to read your struct's fields and tags. See JSON.
  • fmt.Println uses it to print any value.
  • Form and config libraries fill structs from text.
  • Testing tools compare deep values with reflect.DeepEqual.

Reflection is powerful, but use it rarely in your own code.

  • It is slower than normal code.
  • Mistakes show up as panics while running, not as build errors.
  • Interfaces or generics are usually a better choice. See Generics.

What You Will Learn

In this lesson, you will learn:

  1. What reflection is and why it is used.
  2. The reflect package: reflect.Type and reflect.Value.
  3. How to inspect types and struct fields.
  4. How to read values and work with dynamic data.
  5. How to change a value through reflection.

The reflect Package

reflect.Type

reflect.Type = the description of a type. Get it with reflect.TypeOf(x).

reflect.Value

reflect.Value = the value itself, wrapped so you can inspect it. Get it with reflect.ValueOf(x).

Type vs Kind

Kind = the general family of a type: int, string, struct, slice, map, ptr, and so on.

Pick a value and see its Type, Kind and Value.

  • Type is exact: map[string]int.
  • Kind is the family: map.
  • Many different types share one kind. Every struct type has the kind struct.

TypeOf, Kind and ValueOf

package main

import (
	"fmt"
	"reflect"
)

func main() {
	values := []any{42, 3.5, "Asha", true, []int{91, 78}, map[string]int{"maths": 91}}
	for _, v := range values {
		t := reflect.TypeOf(v)
		fmt.Println(t, "| kind:", t.Kind(), "| value:", reflect.ValueOf(v))
	}
}
int | kind: int | value: 42
float64 | kind: float64 | value: 3.5
string | kind: string | value: Asha
bool | kind: bool | value: true
[]int | kind: slice | value: [91 78]
map[string]int | kind: map | value: map[maths:91]

Simple lesson: TypeOf tells you what something is, and ValueOf gives you the value to inspect.

Type Inspection

Inspecting Types

Use Kind() to decide what to do, like a type switch for families. See Interfaces and Polymorphism.

Describe any value by its kind

package main

import (
	"fmt"
	"reflect"
)

func describe(x any) string {
	v := reflect.ValueOf(x)
	switch v.Kind() {
	case reflect.Int, reflect.Int64:
		return fmt.Sprint("a whole number: ", v.Int())
	case reflect.String:
		return fmt.Sprint("text with ", v.Len(), " bytes")
	case reflect.Slice:
		return fmt.Sprint("a list of ", v.Len(), " items")
	case reflect.Map:
		return fmt.Sprint("a map with ", v.Len(), " keys")
	default:
		return "something else: " + v.Kind().String()
	}
}

func main() {
	fmt.Println(describe(7))
	fmt.Println(describe("Green Valley"))
	fmt.Println(describe([]string{"a", "b", "c"}))
	fmt.Println(describe(map[int]bool{1: true}))
	fmt.Println(describe(2.5))
}
a whole number: 7
text with 12 bytes
a list of 3 items
a map with 1 keys
something else: float64
  • v.Int(), v.Len() and similar methods only work for the right kinds.
  • Calling the wrong one panics. That is why we check Kind() first.

Inspecting Struct Fields

Press Play to walk through a struct's fields.

List a struct's fields and tags

  • t.NumField() = how many fields.
  • t.Field(i) = one field's details: Name, Type, Tag.
  • This is exactly how encoding/json finds your JSON names.

Read a custom tag for a form

package main

import (
	"fmt"
	"reflect"
)

type Admission struct {
	StudentName string `label:"Student name" required:"yes"`
	ParentPhone string `label:"Parent phone" required:"yes"`
	Hobby       string `label:"Hobby"`
}

func main() {
	t := reflect.TypeOf(Admission{})
	for i := 0; i < t.NumField(); i++ {
		f := t.Field(i)
		mark := ""
		if f.Tag.Get("required") == "yes" {
			mark = " *"
		}
		fmt.Println(f.Tag.Get("label") + mark)
	}
}
Student name *
Parent phone *
Hobby
  • One loop builds the labels for any form struct.
  • Required fields get a *.

In this lab, keep reflection to exported fields and built-in types.

The runner does not report the names of your own types (t.Name()), their methods, or unexported fields the way the real go tool does.

Simple lesson: Kind() tells you the family, and NumField/Field let you walk through a struct and its tags.

Value Inspection

Reading Values

Read struct field values

package main

import (
	"fmt"
	"reflect"
)

type Book struct {
	Title string
	Pages int
	Lent  bool
}

func main() {
	b := Book{"Gitanjali", 103, true}
	v := reflect.ValueOf(b)
	t := v.Type()
	for i := 0; i < v.NumField(); i++ {
		fmt.Printf("%s = %v\n", t.Field(i).Name, v.Field(i).Interface())
	}
}
Title = Gitanjali
Pages = 103
Lent = true
  • v.Field(i) is the field's value, as a reflect.Value.
  • .Interface() turns it back into a normal any you can print.

Changing Values

To change a value, reflect on a pointer, then use Elem().

Set a field by name

package main

import (
	"fmt"
	"reflect"
)

type Student struct {
	Name  string
	Marks int
}

func main() {
	s := Student{"Ravi", 70}
	v := reflect.ValueOf(&s).Elem()

	f := v.FieldByName("Marks")
	fmt.Println(f.CanSet())
	f.SetInt(85)
	v.FieldByName("Name").SetString("Ravi Kumar")
	fmt.Println(s)
}
true
{Ravi Kumar 85}
  • reflect.ValueOf(s) would hold a copy, so it cannot be set.
  • reflect.ValueOf(&s).Elem() reaches the real struct.
  • CanSet() tells you if a change is allowed.
  • The setter must match the kind: SetInt for ints, SetString for strings.

Working with Dynamic Data

Dynamic data = data whose shape is only known while the program runs.

Fill a struct from text pairs

package main

import (
	"fmt"
	"reflect"
	"strconv"
)

type Settings struct {
	School  string
	Periods int
	Debug   bool
}

func fill(target any, data map[string]string) {
	v := reflect.ValueOf(target).Elem()
	for key, text := range data {
		f := v.FieldByName(key)
		if !f.IsValid() || !f.CanSet() {
			continue
		}
		switch f.Kind() {
		case reflect.String:
			f.SetString(text)
		case reflect.Int:
			n, _ := strconv.Atoi(text)
			f.SetInt(int64(n))
		case reflect.Bool:
			f.SetBool(text == "true")
		}
	}
}

func main() {
	var s Settings
	fill(&s, map[string]string{"School": "GVS", "Periods": "8", "Debug": "true", "Colour": "blue"})
	fmt.Printf("%+v\n", s)
}
{School:GVS Periods:8 Debug:true}
  • fill works for any settings struct, without knowing its fields in advance.
  • IsValid() is false for a missing field, like Colour, so it is skipped.
  • Config and form libraries work this way.

Deep Comparison

reflect.DeepEqual

package main

import (
	"fmt"
	"reflect"
)

func main() {
	a := []int{91, 78, 85}
	b := []int{91, 78, 85}
	c := map[string][]int{"8A": {1, 2}}
	d := map[string][]int{"8A": {1, 2}}
	fmt.Println(reflect.DeepEqual(a, b))
	fmt.Println(reflect.DeepEqual(c, d))
	fmt.Println(reflect.DeepEqual(a, []int{91, 78}))
}
true
true
false
  • Slices and maps cannot use ==. See Slices.
  • reflect.DeepEqual compares them element by element. Tests use it a lot.

Easy way to remember

  • What is it? → reflect.TypeOf(x), .Kind()
  • What is in it? → reflect.ValueOf(x), .Field(i), .Len()
  • Change it? → reflect.ValueOf(&x).Elem(), then SetInt / SetString
  • Prefer interfaces or generics when you can.

Simple lesson: ValueOf reads values, ValueOf(&x).Elem() changes them, and DeepEqual compares deeply.

TL;DR

  1. Reflection = inspect types and values while running. Package reflect.
  2. reflect.Type = reflect.TypeOf(x). reflect.Value = reflect.ValueOf(x).
  3. Kind = the family: int, string, slice, map, struct, ptr ...
  4. Struct fields = NumField(), Field(i) → Name, Type, Tag.Get("json").
  5. Read = v.Field(i).Interface(), v.Int(), v.Len().
  6. Change = reflect.ValueOf(&x).Elem(), check CanSet(), then SetInt / SetString / SetBool.
  7. DeepEqual = compare slices, maps and structs deeply.
  8. Use rarely: slower, and errors appear only while running.

Common mistakes

MistakeWrong ideaCorrect ideaWhat goes wrong
reflect.ValueOf(s).Field(0).SetInt(5)Values can always be setReflect on &s, then Elem()reflect: ... using unaddressable value panic.
v.Int() on a stringAny getter worksCheck Kind() firstcall of reflect.Value.Int on string Value panic.
Setting an unexported fieldReflection can do anythingOnly exported fields can be setA panic.
Using reflection for simple jobsIt is more flexibleInterfaces or generics are clearer and fasterSlow, fragile code.
== on two slicesSlices compare like arraysUse reflect.DeepEqualA build error.
Confusing Type and KindThey are the sameKind is the family, Type is exactWrong checks.

Check Yourself

Exercises

What to Remember

  • Reflection inspects types and values while the program runs.
  • TypeOf gives the type, Kind() the family, ValueOf the value.
  • Walk struct fields with NumField and Field(i), and read tags with Tag.Get.
  • To change a value, reflect on a pointer and use Elem().
  • Prefer interfaces and generics. Use reflection only when you must.

Next: Context. You will learn how to cancel work, set deadlines and time limits across goroutines.