Go has several built-in numeric types, “sets of integer or floating-point values.” Some architecture-independent types are
uint8 (8-bit, unsigned integer),
int16 (16-bit, signed integer), and
complex128 (128-bit complex number). Curiously, the Go spec also includes architecture-dependent types:
How many bits do these types require? Unhelpfully, the spec says
uint are the same size, “either 32 or 64 bits.” A
uintptr is “an unsigned integer large enough to store the uninterpreted bits of a pointer value.”
By far the most common of these three types I’ve seen is
int. If you know your program will only use non-negative numbers, you could use
uint instead. The most common time to use
uintptr is when you’re importing
unsafe, which is as safe to use as it sounds.
If these types are architecture-dependent, how can we tell how big an
int is for an architecture we care about? One way is to go through the compiler source code, mapping your target architecture to its corresponding bit size. But, there are at least two solutions better than this:
- Use architecture-independent types. If you need to know exactly how many bits an
intis using in your program, don’t rely on the whims of the compiler — use one of the predefined types (for example,
int64)! This communicates your intent as the author for a variable to have a particular size (the size is in the type’s name!), improving readability and saving mental resources when your code is read later.
- Write a program to tell you. The standard library’s
strconvpackage contains a helper to let you know how large an
strconv.IntSize“is the size in bits of an int or uint value.” You can use it in your program like this:
On 32-bit architectures, this will print
32. On 64-bit architectures, it will print
64. Check it out on the Playground, your computer, or in a 32-bit Docker image (e.g.
const intSize = 32 << (^uint(0) >> 63)// IntSize is the size in bits of an int or uint value.const IntSize = intSize
Let’s break this down into individual bitwise operations to see how it works.
- Unsigned integers are represented with base 2, each bit corresponding to an increasing power of two. For example,
1*2³ + 1*2² + 0*2¹ + 1*2⁰ = 8 + 4 + 0 + 1, or
^operator does a bitwise complement.
^flips bits from
1. For example, with 3 unsigned bits,
^(101) = 010.
uint(0)uses a type conversion to get a
0value of type
>>is the right shift operator. A right shift moves all of the bits to the right, dropping bits off the right and inserting zeros on the left. For example, with 3 unsigned bits,
101 >> 2 = 001.
<<is the left shift operator, which is just like
>>except the bits shift the opposite direction. For example,
101 << 2 = 100.
Here is how all of these operators are used in the
strconv.IntSize expression above:
In other words:
- Start with
^to flip all bits to
- Right shift (
63to only keep a single
1from 64-bit numbers and zero out 32-bit numbers.
- Left shift (
32by whatever the result is.
- This leaves
32on architectures that use 32-bit integer representations and
64for 64-bit architectures.
The future of
int in Go
Arbitrary precision integers can cause problems. For example:
- What if your application is running on a 32-bit machine, but you assume an
inthas 64 bits? Your variable may silently overflow.
- It’s difficult to convert large values into an
int— you have to be careful not to exceed the bounds.
strconv.IntSizerelies on the fact
uintare either 32 bits or 64 bits. How could you modify it to work if an
intwas either 32 or 128 bits?
Do you know any other tricks for learning how large an
intis in Go? Let me know in the comments or on Twitter!