skip to content

Why does reflect.Value.CanSet return false for an addressable but unexported struct field?

level: middleimportance: should knowfreq 40%

answer

  1. CanSet is two conditions, not one
  2. addressable yet still refused
  3. a read-only flag rides along
  4. it spreads to everything reached beneath
  5. PkgPath is non-empty for unexported fields

basics

~20 s

Because reflect refuses to let outside code break a package's encapsulation. A Value reached through an unexported field is flagged read-only: CanAddr stays true, CanSet is false, and both Set and Interface on it panic.

solid answer

~40 s

`CanSet` is the conjunction of two conditions: the Value must be addressable, **and** it must not have been obtained through an unexported struct field. An unexported field of an addressable struct satisfies the first and fails the second, so `CanAddr()` is true while `CanSet()` is false. Reflect propagates a read-only flag onto that Value and onto everything reached through it, so a whole subtree becomes unwritable. Calling `SetString` on it panics with a message about using a value obtained using an unexported field, and `Interface()` panics as well — `CanInterface` reports that in advance. You can still read simple kinds with `Int`, `String` or `Len`. A struct walk should therefore skip fields where `CanSet` is false, or test the field descriptor's export status, rather than assuming every field of a settable struct is writable.

code

go · 16 lines
go
type Config struct {
	Port   int
	secret string
}

var cfg Config
v := reflect.ValueOf(&cfg).Elem()

port := v.FieldByName("Port")
fmt.Println(port.CanAddr(), port.CanSet()) // true true

secret := v.FieldByName("secret")
fmt.Println(secret.CanAddr(), secret.CanSet()) // true false

// panics: reflect.Value.SetString using value obtained using unexported field
secret.SetString("x")

go deeper

for a junior

Remember that reflect will not write a field whose name starts with a lower-case letter, and that testing CanSet before calling a setter is how you avoid the panic.

for a middle

Explain that CanSet requires both addressability and an exported path, that a read-only flag propagates to everything reached through an unexported field, and that reading is still permitted while Interface is not.

for a senior

Show judgment about where the check belongs: validate addressability once at the API boundary, and use CanSet or the field's export status per field, so a skipped write is never mistaken for a successful one.

for a principal

Own the design consequence: a type whose fields must be populated reflectively has an implicit contract with every such tool, so exported shape versus setter methods is a boundary decision, not a style preference.

## Two independent conditions The documentation for `reflect.Value.CanSet` states the rule precisely: a Value can be changed only if it is **addressable** and was **not obtained by the use of unexported struct fields**. Those are separate gates, and confusing them is the most common source of surprise here. - Addressability answers "does reflect know where this lives?" - The export check answers "is this package's code allowed to touch it from outside?" So for an unexported field of a struct you reached through a pointer: ``` v := reflect.ValueOf(&cfg).Elem() f := v.FieldByName("secret") f.CanAddr() // true - reflect knows the address f.CanSet() // false - the field is unexported ``` ## What reflect does internally Every `reflect.Value` carries a small set of flags alongside its type and data pointer. Two matter here: an *addressable* flag and a *read-only* flag. `Field` and `FieldByName` set the read-only flag whenever the field's name is not exported. That flag then travels: if the unexported field is itself a struct, every field you reach inside it is read-only too, and so is every element of an unexported slice field. Reflection cannot be used to walk around encapsulation one hop at a time. ## What is still allowed, and what panics Read-only does not mean opaque. You may inspect a read-only Value freely: - `Kind()`, `Type()`, `Len()`, `NumField()` all work - typed readers such as `Int()`, `String()`, `Bool()`, `Float()` work - `IsZero()` and `IsNil()` work What is blocked is anything that would let the value escape into normal, unrestricted Go code or be mutated: - `Set`, `SetInt`, `SetString` and the other setters panic with `reflect: reflect.Value.SetString using value obtained using unexported field` - `Interface()` panics for the same reason; `CanInterface()` reports it in advance without panicking - `Addr()` still works if the value is addressable, but the pointer Value it produces is read-only too, so you cannot launder access through it The asymmetry is deliberate: reading a private field through reflection can only leak information the process already holds, while writing one or handing it out as an `any` would let arbitrary third-party code corrupt a package's invariants. ## Detecting it before you panic There are two clean ways to avoid the panic, and they answer slightly different questions. 1. **Ask the Value:** `if !f.CanSet() { continue }`. This is the belt-and-braces check, because it also covers the unaddressable case. 2. **Ask the type:** the field descriptor returned by `Type.Field(i)` has a non-empty `PkgPath` for unexported fields, and reports `IsExported() == false`. This is the right check when you are deciding what a type's writable surface *is*, independent of any particular Value — for example when validating a target type up front. A loader that populates a settings struct should skip unexported fields quietly and, ideally, report at the boundary if a field it was configured to fill is not settable, so a typo does not turn into a setting that stays at its default. ## The trap the check itself creates Guarding with `if !f.CanSet() { continue }` is correct, but it merges two very different causes into one silent branch. If the caller handed the loader a struct by value, *every* field fails `CanSet` and the loop skips everything without a word. That is why the export check and the addressability check are worth separating in production code: validate addressability once, loudly, at the entry point; then use `CanSet` (or the field's export status) inside the loop for the genuinely per-field decision. ## Package boundaries do not help One last misconception: the read-only flag is not relative to the calling package. Even code inside the same package that declares the type gets a read-only Value from an unexported field, because `reflect` has no idea who its caller is. Reflection's export rule is a property of the field name, not of the call site.

  • Can you read an unexported field through reflect, or is it blocked entirely?
    You can read it. `Kind`, `Len`, and the typed readers such as `Int` and `String` all work on a read-only Value. What is blocked is mutation and `Interface()`, which would hand the value to ordinary Go code with no restrictions; `CanInterface` tells you in advance whether that call would panic.
  • Does code inside the package that declares the type get a settable Value for its own unexported field?
    No. The read-only flag is set by `Field`/`FieldByName` from the field's name alone, and reflect has no notion of who called it. Same-package code sees exactly the same restriction, which is why packages that need reflective writes export the fields or provide setter methods.
  • How would you skip unexported fields when walking a struct type rather than a value?
    Use the field descriptor from `Type.Field(i)`: `IsExported()` reports false, and equivalently `PkgPath` is non-empty, for an unexported field. That works before you have any Value in hand, which makes it the right check when validating a target type up front rather than mid-walk.

saying these in an interview costs you the question

  • Says reflect can write private fields since it bypasses the compiler
  • Treats CanAddr and CanSet as the same check
  • Expects Set on an unexported field to be a silent no-op
  • Thinks same-package code gets settable unexported fields
  • Calls Interface on a read-only Value and is surprised by the panic