Architecture-Dependent Types

Understand Go's architecture-dependent integer types int and uint, how their size changes across 32-bit and 64-bit systems, and how to write portable code that avoids common pitfalls

Not all integer types in Go have a fixed size. The language provides int and uint — two types whose bit width changes depending on the CPU architecture the program is compiled for. They are not broken or unpredictable. They are designed to match the native word size of the underlying processor, giving the compiler room to generate efficient machine code.

This section covers what these types are, why they exist, how to inspect their size at runtime, and where their flexibility turns into a risk you need to manage.

What Are Architecture-Dependent Types?

Go's specification defines int and uint as implementation-specific integer types. They are guaranteed to be at least 32 bits wide, but on a 64-bit platform they become 64 bits. The same principle applies to uintptr, an unsigned integer large enough to hold a pointer value.

In practice, the Go compiler (gc) makes int and uint 32 bits on a 32-bit system and 64 bits on a 64-bit system. This decision is not something you control with a build flag — it follows the target architecture of the compilation.

The key point for a beginner is: you cannot assume int is always 64 bits just because your development machine is 64-bit. If you cross-compile for a 32-bit ARM device, int becomes 32 bits. That shift can break code if it depends on a specific width.

How to Determine the Size at Runtime

You can check the actual width of int and uint using the unsafe.Sizeof function or by inspecting the type through reflection. The size is expressed in bytes.

package main
import (
    "fmt"
    "unsafe"
)
func main() {
    var i int
    var u uint
    fmt.Printf("Size of int: %d bytes (%d bits)\n", unsafe.Sizeof(i), unsafe.Sizeof(i)*8)
    fmt.Printf("Size of uint: %d bytes (%d bits)\n", unsafe.Sizeof(u), unsafe.Sizeof(u)*8)
}

When you run this program on a typical modern machine, you will see that both types occupy 8 bytes (64 bits). On a 32-bit system the output drops to 4 bytes (32 bits).

The program itself is architecture-agnostic. It adapts to wherever it runs. That is exactly the mindset you need when writing code with int and uint — let the size vary, and only pin it down when you absolutely must.

The Go Playground runs on a 64-bit server:

If you test the example on the Go Playground, you will always see a 64-bit result. To observe a 32-bit size, compile and run the program on a 32-bit machine or use GOARCH=386 go run main.go on a 64-bit host.

Why Go Has Architecture-Dependent Integer Types

A CPU processes data most naturally in chunks equal to its word size. On a 64-bit processor, loading, adding, and storing 64-bit values is a single operation. Using a smaller fixed-width type might save memory, but for general-purpose counters, loop variables, and indices, matching the word size often leads to faster code.

Go's designers wanted a default integer type that programmers can reach for without having to think about whether the target CPU is 32-bit or 64-bit. That type is int. It gives you a reasonable range on any platform and lets the compiler pick the most efficient instruction sequence.

At the same time, Go provides explicitly sized integers (int32, int64, etc.) for situations where the bit width really matters — file formats, network protocols, and cross-platform data structures.

Using int and uint Safely

The safest way to use architecture-dependent types is to keep them inside your program's memory and avoid writing them directly to disk or over a network. When you use int for slice indexing, loop counters, or length values from the len built-in, you are following idiomatic Go. These operations do not need a fixed bit width.

// Idiomatic usage — no assumption about exact bit width.
func sum(values []int) int {
    total := 0
    for _, v := range values {
        total += v
    }
    return total
}

The return type of len is int. Slice indices are int. The range operator works with int. All of this is designed so that you can write natural code without constant casts.

Loop counters are where int shines:

The vast majority of integer variables in a Go program — loop counters, array indices, local accumulators — should simply be int. The platform-dependent size is a feature here, not a bug.

When Architecture Dependence Becomes a Problem

The trouble starts the moment a value escapes your process in binary form. If you write an int directly into a file using binary.Write without specifying a fixed-width type, you get a number of bytes that depends on the platform. The same program compiled for 64-bit and 32-bit will produce different binary output, which breaks compatibility.

// This is fragile — the output size depends on the platform.
var count int = 42
err := binary.Write(file, binary.LittleEndian, count)

On a 64-bit system that writes 8 bytes. On a 32-bit system it writes 4 bytes. A reader on the other side expecting a fixed format will misinterpret the data.

Silent data corruption across architectures:

Writing int to persistent storage or network sockets without converting to a fixed-size type can cause silent, hard-to-detect data corruption when the program is compiled for a different architecture. Always use int32 or int64 for binary I/O.

The fix is to explicitly cast to a fixed-width type before serialization:

var count int32 = 42 // Guaranteed 4 bytes everywhere.
err := binary.Write(file, binary.LittleEndian, count)

The uintptr Type

uintptr is another architecture-dependent type, defined as an unsigned integer large enough to hold the bits of a pointer. Its size changes between 32-bit and 64-bit platforms just like int and uint.

uintptr is not a general-purpose integer. It exists for low-level operations inside the unsafe package — pointer arithmetic, memory address manipulation, and interaction with the garbage collector. Normal application code should not use uintptr directly unless it is performing the specific pattern of converting a uintptr back to a unsafe.Pointer in a single expression, as required by the runtime.

// This pattern is valid (within unsafe code) because the conversion
// back to unsafe.Pointer happens in the same expression.
p := unsafe.Pointer(&x)
addr := uintptr(p)
// ... arithmetic on addr ...
// Next line must immediately convert back to unsafe.Pointer.

Storing a uintptr in a variable and using it later is dangerous because the garbage collector can move objects, invalidating the stored address.

uintptr is not a safe reference to memory:

A uintptr does not prevent the garbage collector from reclaiming or moving the object it points to. Never store a uintptr across function calls or goroutine boundaries unless you have proven it remains reachable by other means.

Common Misconceptions

A common belief is that int is always 64 bits on a 64-bit OS. Go compiles to the target architecture, not the host OS. If you set GOARCH=386 on a 64-bit Linux machine, int becomes 32 bits in the compiled binary. The resulting executable runs on 32-bit kernels or inside 32-bit compatibility layers, and int will behave as 32-bit.

Another mistake is assuming that int and int64 are interchangeable on a 64-bit platform. While they have the same size, they are distinct types in Go's type system. You cannot pass an int to a function expecting int64 without an explicit conversion. This is true even when the underlying size is identical.

func takeInt64(val int64) { /* ... */ }
var x int = 10
// takeInt64(x) // compile error: cannot use x (type int) as type int64
takeInt64(int64(x)) // explicit conversion required

This type distinction is intentional. It forces you to acknowledge when you are crossing from the architecture-dependent world into the fixed-width world.

Practical Decision Framework

When you choose between int and a fixed-width integer, ask yourself two questions.

First: will this value ever be written to a file, sent over a network, or shared between processes that might run on different architectures? If yes, use a fixed-size type like int32 or int64.

Second: is the value constrained by language semantics, such as a slice index or the result of len, which is guaranteed to be int? If yes, using int is not just idiomatic — it avoids a cascade of explicit type conversions.

For everything else — local counters, intermediate calculations, temporary accumulators — int is the natural choice.

Architecture-Dependent Type Behavior on Different Platforms

The following comparison shows how the same Go source behaves when compiled for 32-bit and 64-bit architectures. The code is identical; only the build target changes.

// Compiled with: GOARCH=amd64 go build main.go
package main
import (
    "fmt"
    "unsafe"
)
func main() {
    var i int
    fmt.Printf("int size: %d bytes, max value: %d\n",
        unsafe.Sizeof(i), ^uint(0))
}

Output on a 64-bit target:

int size: 8 bytes, max value: 18446744073709551615

The expression ^uint(0) gives the maximum value for uint on the current platform — 2^64-1 on 64-bit, 2^32-1 on 32-bit. This single line of code reveals exactly how much range you are working with.

Cross-Compilation and Testing for Portability

You can verify that your code does not accidentally rely on a specific int size by cross-compiling and running it under a different GOARCH. The go test command supports the same build constraints.

GOARCH=386 go build ./...   # Check compilation on 32-bit
GOARCH=386 go test ./...    # Run tests in 32-bit emulation

If your test suite passes on both GOARCH=amd64 and GOARCH=386, you have reasonable evidence that the code does not depend on the word size — or that the dependency is intentional and handled.

GOARCH does not emulate hardware:

Running GOARCH=386 go test on a 64-bit machine uses the 32-bit memory layout and type sizes, but the kernel still runs the binary in compatibility mode. It does not simulate all hardware differences like address space layout, but it catches most int-size bugs.

Summary

Architecture-dependent types are not a hazard to be avoided; they are a tool with a specific scope. int and uint make everyday Go code clean and efficient by matching the CPU's natural word size. The risk surfaces only at the boundary of your process — when data leaves memory and enters storage, a wire, or another architecture's binary format.

The single most important habit is to separate the two worlds. Inside your program, stay with int. At the edges, convert to int32 or int64. If you keep that boundary crisp, architecture dependence becomes a feature you never have to fight.