Fix 'reflect.Value.Interface: cannot return value obtained from unexported field' in Go

intermediateπŸ”· Go2026-07-03| Go 1.18+, any OS (Linux, macOS, Windows)

Error Message

reflect.Value.Interface: cannot return value obtained from unexported field or method
#reflect#reflection#unexported#struct#golang

The Error

You're using Go's reflect package to inspect a struct, and you hit this:

panic: reflect.Value.Interface: cannot return value obtained from unexported field or method

goroutine 1 [running]:
reflect.Value.Interface(...)
        /usr/local/go/src/reflect/value.go:1032
main.main()
        /tmp/sandbox/main.go:18 +0x1a5

It usually surfaces when iterating over struct fields with reflection and calling .Interface() β€” but one of those fields starts with a lowercase letter.

Why This Happens

Go's visibility rules don't stop at the compiler β€” the reflect package enforces them too. Unexported fields (lowercase names) are package-private. Outside code can't access them directly, and reflect will panic at runtime if you try.

Here's the minimal reproduction:

package main

import (
    "fmt"
    "reflect"
)

type User struct {
    Name  string // exported
    email string // unexported
}

func main() {
    u := User{Name: "Alice", email: "alice@example.com"}
    v := reflect.ValueOf(u)

    for i := 0; i < v.NumField(); i++ {
        field := v.Field(i)
        fmt.Println(field.Interface()) // panics on 'email'
    }
}

The loop reaches the second field (email) and blows up. That lowercase name is the giveaway.

Quick Fix: Check CanInterface() Before Calling Interface()

CanInterface() tells you whether the call will succeed. Wrap every .Interface() call with it:

for i := 0; i < v.NumField(); i++ {
    field := v.Field(i)
    if !field.CanInterface() {
        continue // skip unexported fields
    }
    fmt.Println(field.Interface())
}

Panic gone. Same pattern works whether you're accessing fields by name or drilling into nested structs.

Check the Field Name Too

Need a log trail of what you're skipping? Check each field's PkgPath through reflect.Type:

t := reflect.TypeOf(u)
v := reflect.ValueOf(u)

for i := 0; i < t.NumField(); i++ {
    ft := t.Field(i)
    fv := v.Field(i)

    if ft.PkgPath != "" {
        fmt.Printf("Skipping unexported field: %s\n", ft.Name)
        continue
    }

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

PkgPath is empty for exported fields. For unexported ones it holds the defining package path β€” something like main or github.com/yourorg/pkg. Useful when you're debugging serialization code and want to know exactly which fields got skipped.

Permanent Fix: Rethink Your Struct Design

Hitting this panic often means reflection is doing work it shouldn't be. Unexported fields are private by design. Using reflect to read them from outside the package is a signal the struct's API needs a rethink β€” not a more clever workaround.

Option 1: Export the fields you need

If a field legitimately belongs in the public interface, capitalize it:

type User struct {
    Name  string
    Email string // was 'email', now exported
}

Cleanest option when the data belongs in your public API.

Option 2: Implement a custom marshaler or accessor

Want to keep the field private but still support serialization or generic processing? Add a method instead of opening the struct up:

type User struct {
    Name  string
    email string
}

func (u User) ToMap() map[string]any {
    return map[string]any{
        "Name":  u.Name,
        "email": u.Email(), // controlled access
    }
}

func (u User) Email() string {
    return u.email
}

Encapsulation stays intact. No reflection needed.

Option 3: Use encoding/json tags

Rolling your own reflection loop to marshal a struct? That's what encoding/json is for β€” and it already silently skips unexported fields:

import "encoding/json"

type User struct {
    Name  string `json:"name"`
    email string // json package silently ignores this
}

data, _ := json.Marshal(User{Name: "Alice", email: "hidden"})
fmt.Println(string(data)) // {"name":"Alice"}

Special Case: Pointer to Struct

Passing a pointer instead of a value? Call .Elem() first to dereference it. Skip that step and you'll hit a different panic before even reaching the unexported field check:

u := &User{Name: "Alice", email: "alice@example.com"}
v := reflect.ValueOf(u)

if v.Kind() == reflect.Ptr {
    v = v.Elem() // dereference the pointer
}

for i := 0; i < v.NumField(); i++ {
    field := v.Field(i)
    if field.CanInterface() {
        fmt.Println(field.Interface())
    }
}

Verify the Fix

Add the CanInterface() guard and run the code. No more panic. To double-check which fields are being skipped, throw in a log line:

for i := 0; i < v.NumField(); i++ {
    ft := t.Field(i)
    fv := v.Field(i)
    if !fv.CanInterface() {
        fmt.Printf("[skip] %s (unexported)\n", ft.Name)
        continue
    }
    fmt.Printf("%s = %v\n", ft.Name, fv.Interface())
}

Expected output for the User example:

Name = Alice
[skip] email (unexported)

Want a regression test to keep it that way?

func TestReflectNoExportedPanic(t *testing.T) {
    u := User{Name: "Alice", email: "alice@example.com"}
    v := reflect.ValueOf(u)
    for i := 0; i < v.NumField(); i++ {
        field := v.Field(i)
        if field.CanInterface() {
            _ = field.Interface() // should not panic
        }
    }
}

Run with go test ./... β€” passes clean.

Summary

  • The panic fires when you call .Interface() on a reflect.Value from an unexported struct field.
  • Guard with field.CanInterface() to skip those fields safely.
  • Use ft.PkgPath != "" via reflect.Type for explicit field-level control.
  • For a lasting fix: export the fields, add accessor methods, or lean on encoding/json β€” and keep reflection out of private state.

Related Error Notes