What is Go's unsafe.Pointer, and which pointer conversions does it make legal?
answer
- a pointer with no element type
- four conversions, not more
- it cannot be dereferenced directly
- one of the four yields a plain integer
- size and layout must match to reinterpret
basics
~20 sunsafe.Pointer is a pointer type with no element type. Any typed pointer converts to it and back out as a different typed pointer, and it converts to and from uintptr. It is Go's only bridge between unrelated pointer types.
solid answer
~40 s`unsafe.Pointer` is declared in the compiler-known `unsafe` package as a pointer to an arbitrary type. Four conversions are permitted: any `*T` to `unsafe.Pointer`, `unsafe.Pointer` to any `*T`, `unsafe.Pointer` to `uintptr`, and `uintptr` back to `unsafe.Pointer`. Chaining the first two reinterprets memory without copying it — `*(*uint64)(unsafe.Pointer(&f))` reads a `float64`'s bits. You cannot dereference an `unsafe.Pointer` directly; convert it to a typed pointer first. Compiling is not the same as being defined: the `unsafe` package documents a small set of valid patterns, and a `*T1`-to-`*T2` conversion is only one of them when `T2` is no larger than `T1` and the two have equivalent memory layout. The third conversion is the dangerous one — a `uintptr` is an ordinary integer that the garbage collector does not treat as a reference.
code
go · 6 linesvar f float64 = 2.5
bits := *(*uint64)(unsafe.Pointer(&f)) // same size, same layout
addr := uintptr(unsafe.Pointer(&f)) // fine to print, not to store
fmt.Printf("%#x at %#x\n", bits, addr)go deeper
Be ready to name the four permitted conversions and to say that unsafe.Pointer has no element type, so it cannot be dereferenced until you convert it to a typed pointer.
Explain why compiling proves nothing here: the unsafe package documents a small set of valid patterns, and reinterpreting one type as another is only defined when the destination is no larger and the layouts are equivalent.
Show that you reach for the safe route first — math.Float64bits, encoding/binary — and that every conversion you keep carries a comment naming the documented pattern it relies on.
Own the consequence at the codebase level: an unsafe import forfeits the Go 1 compatibility promise, so those packages become work every time the toolchain moves and someone must be on the hook for that.
## The package that is not a package `unsafe` is known to the compiler rather than compiled from Go source. Importing it is a visible marker in the import block that this file steps outside the type system, which is exactly why review policies key on that import. `unsafe.Pointer` is documented as a pointer to an arbitrary type — a pointer value with no element type attached. Because it has no element type, you cannot dereference it, do arithmetic on it with `+`, or call a method through it. It exists purely as a conversion hub. ## The four conversions The language permits exactly four moves involving `unsafe.Pointer`: 1. **`*T` to `unsafe.Pointer`** — any typed pointer widens into it. 2. **`unsafe.Pointer` to `*T`** — it narrows back into any typed pointer. 3. **`unsafe.Pointer` to `uintptr`** — the address as a plain integer. 4. **`uintptr` to `unsafe.Pointer`** — an integer reinterpreted as an address. Moves 1 and 2 chained together are the reinterpretation you usually want: `(*uint64)(unsafe.Pointer(&f))` gives you a `*uint64` aimed at the same eight bytes that hold a `float64`, with no copy and no conversion of the value. Without `unsafe.Pointer` that is a compile error, because Go has no implicit conversion between unrelated pointer types. Note that a `uintptr` cannot be converted straight to a `*T`; it must pass through `unsafe.Pointer`. And going the other way, `unsafe.Pointer` is a real pointer as far as the runtime is concerned: it is scanned by the collector and rewritten if a goroutine stack is copied. `uintptr` is not — it is an integer that happens to have once held an address. ## Compiles is not the same as defined The compiler will accept a conversion between any two pointer types once you route it through `unsafe.Pointer`. That is not permission. The `unsafe` package documentation lists a handful of valid patterns and states plainly that other uses are likely to be invalid today or to become invalid in the future. The pattern behind moves 1 and 2 reads: converting a `*T1` to `*T2` is valid provided `T2` is no larger than `T1` and the two share an equivalent memory layout. The size half is easy to check; the layout half is where people get hurt. Two structs with the same field sizes in a different order do not have equivalent layout. A four-byte type reinterpreted as an eight-byte one reads four bytes that belong to something else. Alignment matters too: the address has to satisfy the alignment the destination type requires, or loads through the resulting pointer are undefined and can fault on some architectures. Where the standard library can do the job for you, let it: `math.Float64bits` gives you the bit pattern of a `float64` without any of this, and `encoding/binary` decodes fixed-layout bytes into a struct field by field. ## The uintptr trapdoor Move 3 is legal on its own and is genuinely useful — turning an address into a number so you can print it, log it, or hash it. Move 4 is the one hedged with rules, because between producing the integer and converting it back, the object it referred to may have been freed (nothing reachable points at it any more) or moved (a growing goroutine stack is copied to a bigger allocation and every real pointer into it is rewritten — but a `uintptr` sitting in a variable is not a pointer, so nothing rewrites it). Round-tripping is only defined inside a single expression, which is a separate question in its own right. ## What you give up by importing it Type safety, first: the compiler stops proving that the bytes you are reading mean what your type says they mean. Second, and less often remembered, the Go 1 compatibility promise explicitly does not cover code that depends on the internal properties `unsafe` exposes. A package that reinterprets memory can be broken by a toolchain upgrade in a way ordinary Go code cannot. That is why the usual posture is: reach for the safe standard-library route first, and treat every surviving `unsafe.Pointer` conversion as something that needs a comment naming which documented pattern it is.
- Is converting an unsafe.Pointer to uintptr purely to print the address legal?Yes. Turning a `Pointer` into a `uintptr` is one of the documented valid patterns, and formatting it with `%#x` is a normal use. What is not legal is keeping that number around and converting it back to a `Pointer` later — at that point it is an address the collector never knew about.
- Does unsafe.Pointer let you convert a *int32 into a *int64?It compiles, and it is undefined. Reading through the `*int64` touches eight bytes when the object is only four, so you read whatever happens to sit next to it and may fault on the alignment. The documented rule is that the destination type must be no larger than the source and share an equivalent memory layout.
- Can you dereference an unsafe.Pointer directly?No. It has no element type, so `*p` is a compile error, as is pointer arithmetic with `+`. You must convert it to a typed pointer first, which is precisely the moment the type assertion about those bytes becomes your responsibility instead of the compiler's.
It is a universal adapter rather than a cable: it plugs anything into anything, and takes no responsibility for whether the signal on the other side means what you think.
saying these in an interview costs you the question
- Calls it Go's void* and assumes anything goes
- Thinks a uintptr keeps its object alive
- Says any two pointer types are interchangeable if sizes match
- Believes importing unsafe turns off the garbage collector
- Tries to dereference or do arithmetic on an unsafe.Pointer directly