Skip to content

Reflection and unsafe: the compiler errors

Level: 301 · anyone whose first unsafe.Pointer or reflect.Value did not compile — and whose second one compiled and panicked

One line: The compiler refuses the pointer conversions that skip unsafe.Pointer, a reflect.Value used as if it were the value it describes, and unsafe.Sizeof on something that is not an expression; everything about which Kind a Value holds is known only at run time, so those mistakes are panics, recorded here with their exact messages.

cannot convert &x (value of type int) to type float64

Reading an int's bytes as a float64 by converting the pointer directly.

Verified output of reflection_err_pointer_convert_sh.sh — regenerated by tools/run_examples.py, never hand-typed.

$ go build bits.go
# command-line-arguments
./bits.go:7:24: cannot convert &x (value of type *int) to type *float64
exit status 1

The mistake — Go has no cast: a conversion between pointer types is allowed only when the underlying base types are identical (Conversions ↗), and int and float64 are not.

The fix — decide which you meant. A new value: float64(x). The same bytes read as another type: pattern 1 of unsafe.Pointer reinterprets memory, or better math.Float64frombits(uint64(x)), which says so.

asValue := float64(x)                             // 1 -> 1.0
asBits := *(*float64)(unsafe.Pointer(&x))         // the bytes of 1 read as a float64: 5e-324

cannot indirect p (variable of type unsafe.Pointer)

Dereferencing an unsafe.Pointer, and converting an int to one.

Verified output of reflection_err_indirect_unsafe_pointer_sh.sh — regenerated by tools/run_examples.py, never hand-typed.

$ go build deref.go
# command-line-arguments
./deref.go:11:15: invalid operation: cannot indirect p (variable of type unsafe.Pointer)
./deref.go:12:22: cannot convert total (variable of type int) to type unsafe.Pointer
exit status 1

The mistake — unsafe.Pointer points at memory of no particular type, so *p has no type to be; and unsafe.Pointer(total) fails because an int is not an address — only a pointer or a uintptr converts to Pointer.

The fix — convert to a typed pointer before dereferencing; and if you really hold an address as an integer, it has to be a uintptr, which is the round trip that go vet flags.

p := unsafe.Pointer(&total)
fmt.Println(*(*int)(p))

reflect.Value is not a function

Calling a method found by name with parentheses, as if MethodByName returned a Go function.

Verified output of reflection_err_call_non_function_sh.sh — regenerated by tools/run_examples.py, never hand-typed.

$ go build method.go
# command-line-arguments
./method.go:14:11: invalid operation: cannot call reflect.ValueOf(inv).MethodByName("Total") (value of struct type reflect.Value): reflect.Value is not a function
exit status 1

The mistake — MethodByName returns a reflect.Value of Kind Func; a Value is a struct, and the call syntax needs a function type.

The fix — Call takes and returns []reflect.Value; or assert the Value to a function type first.

results := reflect.ValueOf(inv).MethodByName("Total").Call(nil)
fmt.Println(results[0].Float())
total := reflect.ValueOf(inv).MethodByName("Total").Interface().(func() float64)
fmt.Println(total())

cannot use quantity (variable of struct type reflect.Value) as int value

Assigning a reflect.Value to an int, and comparing one with 3.

Verified output of reflection_err_value_is_not_the_value_sh.sh — regenerated by tools/run_examples.py, never hand-typed.

$ go build value.go
# command-line-arguments
./value.go:10:14: cannot use quantity (variable of struct type reflect.Value) as int value in variable declaration
./value.go:11:29: cannot convert 3 (untyped int constant) to type struct{typ_ *abi.Type; ptr unsafe.Pointer; reflect.flag}
exit status 1

The mistake — the Value describes the int; it is not the int. The second message shows what it is: a struct of a type pointer, a data pointer and a flag word.

The fix — read the value out with the method for its Kind, or go back through an interface value with Interface() and a type assertion, or with Go 1.25's TypeAssert.

n := int(quantity.Int())
n, ok := reflect.TypeAssert[int](quantity)

use of untyped nil in argument to unsafe.Sizeof

Asking for the size of nil, of a type name, and of a call that returns nothing.

Verified output of reflection_err_sizeof_sh.sh — regenerated by tools/run_examples.py, never hand-typed.

$ go build sizes.go
# command-line-arguments
./sizes.go:11:28: use of untyped nil in argument to unsafe.Sizeof
./sizes.go:12:28: int64 (type) is not an expression
./sizes.go:13:28: audit() (no value) used as value
exit status 1

The mistake — Sizeof takes an expression of any type (Package unsafe ↗); an untyped nil has no type, int64 is a type rather than an expression, and audit() has no value.

The fix — give it a typed value: a zero of the type, or a typed nil pointer.

fmt.Println(unsafe.Sizeof(int64(0)))    // 8
fmt.Println(unsafe.Sizeof((*int)(nil))) // 8: the size of a pointer
fmt.Println(unsafe.Sizeof(audit))       // 8: a func value is a pointer

invalid argument: packet (variable of type []byte) is not a pointer

Passing a slice to unsafe.Slice, which wants a pointer to the first element and a length.

Verified output of reflection_err_slice_needs_pointer_sh.sh — regenerated by tools/run_examples.py, never hand-typed.

$ go build view.go
# command-line-arguments
./view.go:10:23: invalid argument: packet (variable of type []byte) is not a pointer
exit status 1

The mistake — unsafe.Slice(ptr, len) builds a slice from a pointer; a slice already is one, and re-slicing it is ordinary Go.

The fix — packet[:2]; or, when the point is the pointer, unsafe.SliceData(packet) is the *byte that unsafe.Slice and unsafe.String take.

head := packet[:2]
text := unsafe.String(unsafe.SliceData(packet), 2)

The panics the compiler cannot see

A reflect.Value is one static type, whatever Kind it holds, so Field on an int, Int on a string, Elem on a non-pointer and an index past NumField all compile. Each is a panic from package reflect, and the last one — reflect.TypeOf(nil) returns a nil Type, and a method on a nil interface is a nil-pointer dereference — comes from the runtime. reflect.ValueOf(nil) is different: it returns the zero Value, whose Kind is Invalid and whose Type panics.

Verified output of reflection_err_runtime_panics_go.go — regenerated by tools/run_examples.py, never hand-typed.

Field(0) on an int               panic: reflect: call of reflect.Value.Field on int Value
Field(2) on a 2-field struct     panic: reflect: Field index out of range
Int() on a string                panic: reflect: call of reflect.Value.Int on string Value
NumField() on an int Type        panic: reflect: NumField of non-struct type int
Elem() on an int                 panic: reflect: call of reflect.Value.Elem on int Value
Elem() on an int Type            panic: reflect: Elem of invalid type int
Call() on an int                 panic: reflect: call of reflect.Value.Call on int Value
Type() on the zero Value         panic: reflect: call of reflect.Value.Type on zero Value
Kind() on the zero Value         no panic: Kind is invalid
TypeOf(nil) is nil, so .Kind()   panic: runtime error: invalid memory address or nil pointer dereference
reflection_err_runtime_panics_go.go

reflection_err_runtime_panics_go.go in full — pasted here by tools/run_examples.py from the file CI runs.

// The compiler checks that a reflect.Value is a reflect.Value; what Kind it
// holds is known only at run time, so a method called on the wrong Kind, or
// a Field index past the end, is a panic. Each one here is recovered and
// printed. Every message is from package reflect itself, except the last.
//
//  go build reflection_err_runtime_panics_go.go && ./reflection_err_runtime_panics_go
package main

import (
    "fmt"
    "reflect"
)

type Invoice struct {
    Number string
    Amount float64
}

func main() {
    count := 3
    inv := Invoice{Number: "INV-7", Amount: 12.5}
    try("Field(0) on an int", func() { reflect.ValueOf(count).Field(0) })
    try("Field(2) on a 2-field struct", func() { reflect.ValueOf(inv).Field(2) })
    try("Int() on a string", func() { reflect.ValueOf(inv.Number).Int() })
    try("NumField() on an int Type", func() { reflect.TypeOf(count).NumField() })
    try("Elem() on an int", func() { reflect.ValueOf(count).Elem() })
    try("Elem() on an int Type", func() { reflect.TypeOf(count).Elem() })
    try("Call() on an int", func() { reflect.ValueOf(count).Call(nil) })
    try("Type() on the zero Value", func() { reflect.ValueOf(nil).Type() })
    try("Kind() on the zero Value", func() {
        fmt.Printf("%-32s no panic: Kind is %s\n", "Kind() on the zero Value", reflect.ValueOf(nil).Kind())
    })
    try("TypeOf(nil) is nil, so .Kind()", func() { reflect.TypeOf(nil).Kind() })
}

// try runs f and prints what it panicked with.
func try(what string, f func()) {
    defer func() {
        if r := recover(); r != nil {
            fmt.Printf("%-32s panic: %v\n", what, r)
        }
    }()
    f()
}

The mistake — trusting the Kind of a Value that came from an any. The messages name the method and the Kind (reflect: call of reflect.Value.Field on int Value), which is the compiler's error message arriving late.

The fix — check Kind() (and IsValid() for a value that may be nil) before a Kind-specific method, and return an error; reflect cannot know what you expected, but your function can. The settability panics — using unaddressable value, using value obtained using unexported field, value of type string is not assignable to type int — are on reflect sets only addressable values.

v := reflect.ValueOf(x)
if !v.IsValid() || v.Kind() != reflect.Struct {
    return fmt.Errorf("want a struct, got %v", v.Kind())
}

A malformed struct tag is not a compiler error

json:name, json: "name" and json:"a",xml:"b" are all valid Go: a tag is a string literal. reflect.StructTag.Get returns "" for the key, encoding/json falls back to the field name, and nothing fails. go vet's structtag analyzer is the only check, on the vet page.

In other languages

  • Rust turns most of this page into compile errors: a raw pointer cast between unrelated types is as with a size check in transmute, a private field is unreachable at compile time rather than at run time, and there is no Value to call the wrong method on — What unsafe turns off ↗ lists what stays checked inside unsafe.
  • C compiles the first entry without a word: *(double *)&x is a cast, and reading it is undefined behaviour the compiler may assume never happens.

Sources