エラーの内容
Goのreflectパッケージを使って構造体を検査していると、次のエラーが発生します:
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
このエラーは、リフレクションで構造体のフィールドを反復処理し.Interface()を呼び出す際に、フィールド名が小文字で始まっている場合によく発生します。
原因
Goの可視性ルールはコンパイラだけでなく、reflectパッケージにも適用されます。エクスポートされていないフィールド(小文字名)はパッケージプライベートです。外部のコードから直接アクセスすることはできず、reflectを使って無理にアクセスしようとするとランタイムパニックが発生します。
最小限の再現コードは以下の通りです:
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'
}
}
ループが2番目のフィールド(email)に到達した時点でパニックが発生します。小文字のフィールド名がその原因です。
簡単な修正方法:Interface() の前に CanInterface() を確認する
CanInterface()を使うと、呼び出しが成功するかどうかを事前に確認できます。すべての.Interface()呼び出しをこれでラップしましょう:
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
if !field.CanInterface() {
continue // skip unexported fields
}
fmt.Println(field.Interface())
}
これでパニックはなくなります。フィールド名でアクセスする場合も、ネストされた構造体を掘り下げる場合も、同じパターンが有効です。
フィールド名も確認する
スキップしたフィールドのログを残したい場合は、reflect.Typeを通じて各フィールドのPkgPathを確認します:
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は空文字列になります。エクスポートされていないフィールドでは、mainやgithub.com/yourorg/pkgのような定義パッケージのパスが格納されます。シリアライズ処理のデバッグ時に、どのフィールドがスキップされたかを正確に把握したい場合に役立ちます。
根本的な解決策:構造体の設計を見直す
このパニックに頻繁に遭遇する場合、リフレクションが本来すべきでない処理をしているサインです。エクスポートされていないフィールドは、設計上プライベートなものです。パッケージ外からreflectを使ってそれらを読み取ろうとしているなら、回避策を工夫するよりも、構造体のAPIを見直す必要があります。
方法1:必要なフィールドをエクスポートする
フィールドが正当にパブリックインターフェースに属するなら、大文字に変更しましょう:
type User struct {
Name string
Email string // was 'email', now exported
}
データをパブリックAPIに含める場合、これが最もシンプルな解決策です。
方法2:カスタムマーシャラーまたはアクセサを実装する
フィールドをプライベートに保ちつつ、シリアライズや汎用処理をサポートしたい場合は、構造体を開放するのではなく、メソッドを追加しましょう:
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
}
カプセル化を維持したまま、リフレクションも不要になります。
方法3:encoding/json タグを使う
構造体をマーシャルするためにリフレクションループを自作しているなら、それはまさにencoding/jsonが担う役割です。しかもエクスポートされていないフィールドは自動的に無視されます:
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"}
特殊なケース:構造体へのポインタ
値ではなくポインタを渡している場合は、最初に.Elem()を呼び出して参照を外す必要があります。このステップを省略すると、エクスポートされていないフィールドのチェックに到達する前に別のパニックが発生します:
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())
}
}
修正の確認
CanInterface()のガード処理を追加してコードを実行してみましょう。パニックは発生しなくなります。スキップされているフィールドを確認するには、ログ出力を追加します:
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())
}
Userの例で期待される出力:
Name = Alice
[skip] email (unexported)
この動作を維持するためのリグレッションテストを追加したい場合:
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
}
}
}
go test ./...で実行すると、問題なく通過します。
まとめ
- エクスポートされていない構造体フィールドのreflect.Valueに対して
.Interface()を呼び出すとパニックが発生します。 field.CanInterface()でガードして、該当フィールドを安全にスキップしましょう。- フィールドレベルで明示的に制御するには、
reflect.Type経由のft.PkgPath != ""を使用します。 - 根本的な解決策としては、フィールドのエクスポート、アクセサメソッドの追加、または
encoding/jsonの活用を検討し、リフレクションをプライベートな状態から遠ざけましょう。

