DEV Community

Cover image for How to Use Unsafe in Go Without Killing Your Service
Pavel Sanikovich
Pavel Sanikovich

Posted on

How to Use Unsafe in Go Without Killing Your Service

There’s a moment in every senior Go engineer’s career when you stare at a CPU flamegraph, see a hotspot you can’t optimize anymore, and your brain whispers:

“Maybe… maybe I should try unsafe?”

Then your rational side immediately screams:

“NO! YOU’RE GOING TO SUMMON DEMONS!”

And honestly? Both voices are right.

unsafe is one of the most misunderstood tools in Go. It's neither evil nor magical. It's just… dangerous. Like a chainsaw. If you know exactly what you're doing, you can cut through impossible performance bottlenecks. If not — you can chop off your system’s stability and memory safety.

This article is about how to use unsafe properly in production Go services — where it can help, when it should be avoided, and what patterns actually make sense.


1. Why Unsafe Exists (And Why It's Not Actually “Illegal”)

Many languages have an escape hatch:

  • Rust: unsafe
  • Java: sun.misc.Unsafe
  • C#: pointers
  • C/C++: raw memory

In Go, unsafe lets you:

  • circumvent the type system
  • reinterpret memory
  • manipulate pointers
  • access memory layout directly

This makes it possible to:

  • eliminate allocations
  • reuse buffers
  • perform zero-copy conversions
  • manually pack/unpack data structures
  • implement extremely fast serializers
  • interoperate with C

And that’s the key:

unsafe trades safety for raw, unfiltered performance.


2. The Three Core Tools of Unsafe

Go exposes three extremely powerful weapons:

1. unsafe.Pointer

A pointer to arbitrary memory.
This is the gateway to everything dangerous.

2. uintptr

An integer representation of an address.
Used for pointer arithmetic.

3. Sizeof / Alignof / Offsetof

Used to inspect memory layout of structs.

Everything else is a combination of these primitives.


3. When Unsafe Becomes a Real Production Tool

Let’s be honest: 95% of Go developers should never touch unsafe.

But in high-performance systems, unsafe becomes a viable, even necessary tool.

Unsafe is justified when:

  • performance bottleneck is memory copying
  • you need zero-copy serialization
  • converting []byte <-> string is too expensive
  • reflection code is too slow
  • high-throughput systems require tight memory control
  • marshaling/unmarshaling is killing p95 latency
  • you must interoperate with a C library
  • GC overhead is too high for an object type

Unsafe is never “first choice”.
It’s the last resort that saves the day.


4. The Most Useful Unsafe Pattern: Zero-Copy Conversions

The classic example:

string → []byte without allocation

func StringToBytes(s string) []byte {
    return unsafe.Slice(unsafe.StringData(s), len(s))
}
Enter fullscreen mode Exit fullscreen mode

And the opposite:

[]byte → string without allocation

func BytesToString(b []byte) string {
    return unsafe.String(&b[0], len(b))
}
Enter fullscreen mode Exit fullscreen mode

Why this matters:

  • standard conversion makes a copy (O(n))
  • unsafe version is O(1)
  • zero allocations
  • zero garbage

This is a HUGE win if you process thousands of payloads per second.


5. The Second Killer Pattern: Manual Memory Layout Access

Example: reading a struct header directly.

type Header struct {
    Size   uint32
    Flags  uint16
    TypeId uint16
}

func HeaderFromBytes(buf []byte) *Header {
    return (*Header)(unsafe.Pointer(&buf[0]))
}
Enter fullscreen mode Exit fullscreen mode

That’s:

  • zero-copy
  • instant reinterpretation
  • highly performant

Works great in binary protocols, network frames, and log formats.


6. The Third Killer Pattern: Struct Packing

If you need tight memory packing — unsafe lets you control padding.

Example:

type Packed struct {
    A uint8
    B uint8
    C uint16
}
Enter fullscreen mode Exit fullscreen mode

You can inspect:

unsafe.Sizeof(Packed{})  
unsafe.Alignof(Packed{})
Enter fullscreen mode Exit fullscreen mode

Then rearrange fields to minimize struct size.

In highload systems processing millions of objects, even a 4–8 byte reduction per object matters.


7. Where Unsafe Goes Wrong (And How People Blow Up Services)

Most unsafe disasters fall into five categories.


1. Using zero-copy string <-> []byte on mutable data

If you do:

s := BytesToString(buf)
buf[0] = 'X'
Enter fullscreen mode Exit fullscreen mode

You just mutated the string.
Strings are supposed to be immutable.

Your service is now haunted.


2. Pointer arithmetic without bounds checking

ptr := uintptr(unsafe.Pointer(&arr[0]))
ptr += 1000 // oops
Enter fullscreen mode Exit fullscreen mode

Go will not stop you.
You’re now reading memory you don't own.


3. Breaking GC invariants

Go’s garbage collector expects:

  • object graphs to be valid
  • pointer references to be safe

Unsafe code can create “dangling pointers”, causing:

  • crashes
  • memory corruption
  • random behavior

4. Using unsafe across Go versions

Go does NOT guarantee:

  • struct field alignment
  • padding
  • memory layout consistency
  • internal representation of strings/slices

Using unsafe ties your code to a specific Go version.


5. Mixing unsafe with reflection

Reflection + unsafe = instant footgun.
The type system has no idea what you’re doing.


8. How to Use Unsafe Safely (Yes, This Is Possible)

Here is the safety checklist we use in production.


Rule 1 — Keep unsafe in isolated helper functions

Never put unsafe logic inline.

Always hide it behind a well-named function like:

  • BytesToStringUnsafe
  • ZeroCopyMarshal
  • ParseHeaderFromBytes

This lets you:

  • audit unsafe code
  • minimize exposure
  • add tests
  • wrap with comments
  • prevent accidental misuse

Rule 2 — Document every unsafe function

Every unsafe block must have a comment:

  • why unsafe is needed
  • what invariants must be true
  • what callers must guarantee
  • what memory ownership looks like

Future you will thank you.


Rule 3 — NEVER use unsafe on mutable data without copying

Zero-copy conversions are safe only when data is immutable.


Rule 4 — Benchmark before and after

Unsafe is a surgical tool.
If unsafe doesn’t help, don’t use it.


Rule 5 — Fuzz test everything around unsafe

Unsafe code is extremely sensitive to unexpected inputs.
Fuzz tests help expose edge cases quickly.


Rule 6 — Avoid relying on memory layout unless absolutely required

Prefer:

  • code-generated serializers
  • binary encoding libraries
  • protobuf/flatbuffers

Unsafe struct reinterpretation should be a last resort.


9. Real Example: Unsafe in a Production Go Service

We had an event ingestion service doing:

  • 120k messages/sec
  • each 600–900 bytes
  • heavy JSON unmarshaling
  • constant []byte <-> string conversions

We replaced:

string(b)
Enter fullscreen mode Exit fullscreen mode

with the unsafe version.

Results:

  • CPU usage dropped ~17%
  • Allocations dropped 40%
  • GC pauses reduced noticeably
  • p95 went from 8.2ms → 5.4ms
  • p99 became much more stable

No other optimization delivered as much for so little code change.


10. When Unsafe Is the RIGHT Choice

Unsafe is appropriate when:

  • performance is critical
  • data is immutable
  • pointers won’t outlive the buffer
  • memory ownership is clear
  • you’re working with binary protocols
  • you’ve proven reflection is too slow
  • you need zero-copy transformations

Unsafe is not appropriate when:

  • you don’t understand Go’s memory model deeply
  • the service is not performance-critical
  • correctness matters more than speed
  • the team maintaining the code is junior-heavy
  • you can use a binary format instead

11. Key Takeaways (You MUST Know These)

  • Unsafe is not evil — it’s dangerous.
  • It enables optimizations impossible with pure Go.
  • It bypasses safety guarantees of the language.
  • If used wrong, it destroys stability.
  • If used right, it delivers extreme performance wins.
  • Always isolate unsafe code and document invariants.
  • Benchmark before and after.

Unsafe is a powerful tool — but only if you respect it.


12. If You Want to Go Deeper

These Educative courses shaped how I think about performance and low-level Go behavior:

Worth it if you’re pushing Go systems to their limits.

Top comments (0)