GoProgrammingSenior Go Developer

When does modifying a value through **reflect.Value** fail to persist changes outside the reflection call, despite appearing to succeed within it?

Pass interviews with Hintsage AI assistant

Answer to the question

History

The reflect package was introduced to provide runtime type introspection while maintaining Go's static type safety. Early implementations allowed dangerous modifications that could corrupt memory or violate type constraints. To prevent this, the Go team implemented strict addressability rules. A reflect.Value tracks whether its underlying value is addressable—meaning it refers to actual memory that can be modified. This distinction exists to prevent modifications to transient copies, constants, or unexported fields, ensuring that reflection cannot circumvent Go's compile-time safety guarantees.

Problem

When you pass a value (not a pointer) to reflect.ValueOf, Go creates a copy of that value on the stack. The resulting reflect.Value points to this ephemeral copy, making it unaddressable. If you attempt to modify this value using SetInt, SetString, or similar methods, they silently succeed if you forget to check CanSet(), but since they only modify the stack copy, the original variable remains unchanged. This creates a silent logic error where the program appears to execute correctly but produces no actual side effects.

Solution

Always pass a pointer to the value you intend to modify, then use Elem() to obtain the addressable value. Before any modification, verify Value.CanSet() returns true. If working with structs, ensure you are setting exported fields (capitalized), as unexported fields are never settable from outside the package. For maps and slices accessed via reflection, remember that while the container itself might need addressability, individual elements accessed via Index() or MapIndex() follow the same rules regarding addressability.

Code Example

package main import ( "fmt" "reflect" ) func main() { x := 42 // Wrong: passes copy, modification doesn't persist v := reflect.ValueOf(x) if v.CanSet() { v.SetInt(100) // This will never execute } // Correct: passes pointer and uses Elem() ptr := reflect.ValueOf(&x).Elem() if ptr.CanSet() { ptr.SetInt(100) // Modifies original x } fmt.Println(x) // Output: 100 }

Situation from life

Detailed Example

We developed a dynamic configuration system for a high-frequency trading gateway. The system needed to update specific parameters (like rate limits and threshold values) in a running service without restart. A ReloadConfig function used reflection to iterate over struct fields and apply new values from a JSON patch.

Problem Description

The initial implementation passed the global config struct by value to a helper function applyUpdate(cfg Config, fieldName string, newValue int). Inside, it used reflect.ValueOf(cfg) to locate the field and update it. Unit tests passed because they checked the return value of the reflection call, but integration tests showed the global config remained stale. The reflection appeared to work—SetInt returned no error—but only because we cast the Value to a settable type incorrectly, actually creating a new copy within the reflection machinery.

Different Solutions Considered

Solution 1: Pointer Passing with Mutex

Change the signature to accept a pointer applyUpdate(cfg *Config, ...) and use reflect.ValueOf(cfg).Elem() to obtain an addressable reflect.Value. This approach requires wrapping updates in a sync.RWMutex to ensure thread safety during concurrent access.

  • Pros: Direct memory modification, efficient, race-detector friendly, idiomatic Go.
  • Cons: Requires careful locking to prevent contention during high-frequency updates.

Solution 2: Immutable Replacement

Keep pass-by-value semantics but return the modified struct. Use atomic.Value to perform an atomic swap of the global pointer, ensuring readers always see a consistent configuration state.

  • Pros: Lock-free reads, simpler concurrency model, no risk of partial updates.
  • Cons: Higher memory churn for large configs, complex to implement with nested structs requiring deep copying.

Solution 3: Unsafe Addressability Bypass

Use unsafe.Pointer to forcibly make the unaddressable Value settable by manipulating internal reflect.Value flags. This bypasses the runtime's safety checks entirely.

  • Pros: Works without changing function signatures.
  • Cons: Violates Go's memory model, breaks with compiler optimizations, causes crashes in newer Go versions due to stricter write barriers.

Chosen Solution and Result

We selected Solution 1 because it maintained type safety without the memory overhead of Solution 2. We refactored to pass *Config, added explicit CanSet() checks that logged errors when false, and protected the global state with an sync.RWMutex to prevent race conditions.

The reflection updates now persisted correctly across the application. The system successfully handled 50,000 dynamic config updates per second without increasing garbage collection pressure or latency spikes.

What candidates often miss

Why does reflect.ValueOf return a different pointer address for the same integer when passed by value versus by pointer?

When passing by value, ValueOf receives a copy of the integer allocated on the stack or in a register. The internal pointer of the reflect.Value tracks this ephemeral copy's address. When passing a pointer, ValueOf tracks the original variable's heap or stack location. This distinction determines whether CanSet() returns true, as only the latter represents mutable memory that outlives the reflection call.

How does the Addr() method differ from Elem(), and why does Addr panic on unexported struct fields?

Elem() dereferences a pointer Value, returning the value it points to. Addr() returns a Value representing a pointer to the value, but only if the value is addressable. Addr enforces package boundary protection: if you obtain a Value by accessing an unexported struct field using FieldByName, calling Addr panics to prevent escaping references to encapsulated data. This maintains Go's visibility rules even through reflection.

Why might Value.CanInterface() return false even when CanSet() returns true, and how does this relate to method receivers?

CanInterface returns false if the Value was obtained via unexported fields or represents a method value that cannot be safely converted to interface{} without exposing internal implementation details. Even if a Value is settable and exported, CanInterface protects against interface conversion that would allow type assertion to bypass package boundaries. This is crucial when reflecting on method receivers: a Value representing a bound method value may be settable in context but not interface-convertible because it contains internal closure state that must remain hidden.