Skip to Content
Course content

206: Reflection with the reflect Package

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Reflection is the "break glass in case of emergency" tool in Go. Most of the time, you want to avoid it because it bypasses type safety, slows down your code, and makes things harder to read. But there are times—like when you're writing a generic database mapper or a JSON validator—where you simply cannot know the type of the data you're dealing with until the program is actually running.

To understand how the reflect package works, let's build a simple "Required Field Validator." Imagine we want to mark certain fields in a struct with a tag like validate:"required", and then have a single function that can check any struct to see if those fields are empty.

Peeking inside the interface

First, we need a way to pass any struct into our validator. That means our function signature has to take an interface{} (or any in newer Go versions). To actually see what's inside that interface, we use reflect.ValueOf and reflect.TypeOf.

type User struct {
    Name  string `validate:"required"`
    Email string `validate:"required"`
    Age   int
}

func Validate(s any) error {
    v := reflect.ValueOf(s)
    t := reflect.TypeOf(s)
    
    // We want to iterate over the fields of the struct
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        value := v.Field(i)
        
        if field.Tag.Get("validate") == "required" {
            // Check if the value is the zero value for its type
            if value.IsZero() {
                return fmt.Errorf("field %s is required", field.Name)
            }
        }
    }
    return nil
}

Hitting a wall with pointers

Now, if I run this with Validate(User{Name: "Alice"}), it works perfectly. But in a real app, we almost always pass pointers to structs to avoid copying large objects. I tried running it with &User{Name: "Alice"}, and the program immediately panicked: reflect: call of reflect.Value.NumField on ptr.

I realized I made a classic reflection mistake. reflect.ValueOf(s) gives me the value of the interface. If I pass a pointer, v is a pointer, not a struct. You can't call NumField() on a pointer; you have to "dereference" it first to get to the actual struct underneath.

Here is how I fixed it. I need to check if the kind of the value is a pointer, and if so, call .Elem() to get the value the pointer points to.

func Validate(s any) error {
    v := reflect.ValueOf(s)
    
    // Fix: If it's a pointer, get the element it points to
    if v.Kind() == reflect.Ptr {
        v = v.Elem()
    }

    // Now we get the type from the value, ensuring they stay in sync
    t := v.Type()

    if t.Kind() != reflect.Struct {
        return fmt.Errorf("Validate only works on structs")
    }

    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        value := v.Field(i)
        
        if field.Tag.Get("validate") == "required" {
            if value.IsZero() {
                return fmt.Errorf("field %s is required", field.Name)
            }
        }
    }
    return nil
}

Handling different types of "empty"

The value.IsZero() method is incredibly handy here because it doesn't care if the field is a string, an int, or a slice; it just checks if it's the default zero value for that specific type. This is where reflection actually shines—we've written a single loop that handles any data type we throw at it, provided it's wrapped in a struct.

One last thought: notice how I moved t := v.Type() after the pointer check. This is important. If you call reflect.TypeOf on a pointer, you get a pointer type. If you call it on the result of .Elem(), you get the struct type. Keeping them aligned prevents a lot of headaches.




📋 Practical Task

Build a "Secret Masker" Using Reflection

Your task is to create a function called MaskSecrets(obj any). This function should take a pointer to a struct and look for fields tagged with secret:"mask". Instead of returning an error, this function should actually modify the struct, replacing the value of any "masked" string field with "****".

Requirements:

  • The function must handle pointers to structs (use .Elem()).
  • It must check for the secret:"mask" tag.
  • It must only attempt to mask fields that are of type string (use value.Kind() == reflect.String).
  • You must use value.SetString("****") to change the value. (Hint: Remember that for SetString to work, the value must be addressable, which is why we pass a pointer to the struct).

Test Case:

type Config struct {
    APIKey string `secret:"mask"`
    Port   int
    Pass   string `secret:"mask"`
}

cfg := &Config{APIKey: "12345-ABC", Port: 8080, Pass: "super-secret"}
MaskSecrets(cfg)
// Result: cfg.APIKey should be "****" and cfg.Pass should be "****"
Rating
0 0

There are no comments for now.

to be the first to leave a comment.