Right, let’s talk about integers. You’d think counting would be simple, right? You had it figured out by age three. But here we are, in a language designed by Google engineers, and we have to choose from a whole menu of them. This isn’t over-engineering; it’s a necessary concession to reality. Sometimes you need to save memory, sometimes you need to count to a number so large it would make the national debt blush, and sometimes you need to talk directly to the metal of the machine. Go gives you the tools for all of it.

The workhorse you’ll use 99% of the time is the plain int. Its size is not set in stone; it’s whatever the natural word size is for your target platform. On a modern 64-bit laptop, it’s 64 bits wide. On a 32-bit embedded system, it’s 32 bits. This is the right default because it’s usually the fastest for that particular processor to handle. Don’t fight it. Just use int for pretty much everything: loop counters, array indices, most mathematical operations. It’s your comfortable, reliable jeans-and-t-shirt of types.

The Explicit Sizes: When You Need to Get Specific

But then there are the times you need to be precise. Go provides a full family of explicitly-sized integers: int8, int16, int32, int64 and their unsigned cousins, uint8, uint16, uint32, uint64. You use these primarily in two scenarios:

  1. Interoperability: When you’re reading a binary file format (like a PNG or a WAV header) or talking over a network protocol, the spec will literally say “the next 4 bytes are a signed 32-bit integer.” Using an int32 in your struct is how you tell Go, “I mean it, this is exactly 32 bits.” Using a plain int here would be disastrously wrong and non-portable.
  2. Memory Optimization: If you’re building a massive array that needs to hold millions of values and you know those values will only ever be between 0 and 255, using a uint8 (which is just a byte, literally) instead of an int (which might be 8 bytes) cuts your memory footprint by a factor of 8. That’s a huge win.

Here’s the catch, and it’s a big one: these distinct types are not compatible with each other. You can’t just assign an int16 to an int32 without an explicit conversion. The compiler will stop you, and it’s doing you a favor. This feels annoying for about two weeks until you avoid your first subtle data-corruption bug.

var index int = 1024
var smallIndex int16 = 1024

// This is fine. `int` is a different type but this value fits.
var a int32 = int32(index)

// This is also fine, same reason.
var b int16 = smallIndex

// COMPILER ERROR: cannot use smallIndex (variable of type int16) as int32 value
var c int32 = smallIndex

// This works. We're explicitly telling the compiler we want the conversion.
var d int32 = int32(smallIndex)

// This compiles but is a terrible idea. The value 1024 is too big for an int8!
// It will overflow and you'll get a garbage value: -128.
var e int8 = int8(index)
fmt.Println(e) // Prints -128, not 1024

The Unsigned Question

Then we have the unsigned integers: uint, uintptr, and the explicit uintX types. Their superpower is that they don’t waste a bit on negativity, so they can represent numbers twice as large as their signed counterparts (uint8 range: 0 to 255). Their kryptonite is that they can’t represent negative numbers at all.

This leads to a classic pitfall. What happens when you subtract 1 from a uint that is currently 0? It doesn’t give you -1. It wraps around to the top of its range, becoming a massive positive number. This is a common source of infinite loops.

var u uint = 0
// This loop will run forever. When u is 0, u-1 becomes 18446744073709551615.
for u = 10; u >= 0; u-- {
    fmt.Println(u)
}
// You have been warned.

So, the best practice? Be deeply suspicious of uint. Use it when you have a specific reason, like a bitmask, a hash value, or that interoperability case I mentioned. For quantities that are inherently non-negative (like a length), a strong argument can be made for uint. But for most general purposes, just stick with int. The fact that it can be negative is a useful sanity check against underflow bugs.

The Weird One: uintptr

uintptr is a special beast. It’s an unsigned integer type large enough to store the bit pattern of any pointer. You will almost never use this in application code. It exists for low-level hackery, like interacting with C libraries through the unsafe package, where you need to do arithmetic on actual memory addresses. If you’re not using unsafe.Pointer, you don’t need uintptr. Consider it an interesting footnote for now.