Exploring Numeric Data Types in Rust and Go

bkataru

Baalateja Kataru

Posted on April 23, 2024

Exploring Numeric Data Types in Rust and Go

Introduction

The comprehensive yet strict static type systems provided by Rust and Go, modern compiled systems programming languages that are closer to the metal than other dynamically typed scripting languages such as Python or JavaScript, allow one to deterministically declare sized variables as well as unsized, architecture dependent variables.

The privilege of a garbage collector (GC) in Go means that there is more flexibility when it comes to manipulating types and values. Rust, being a statically-typed language that has no GC but instead relies on its Ownership model to ensure memory safety, is comparatively more strict at enforcing variable assignments and type conversions when multiple types are involved. The consequence of this difference is that storing the maximum/minimum values of certain numeric type in Rust sometimes requires a variable with a type having more storage capacity (eg. storing the minimum value of the i32 type requires an i64-typed variable)

In this post, we will explore the primitive numeric data types present in both languages while simultaneously drawing a comparison of the similarities and the differences.

Integers

Integers are numeric values without a fractional part. Examples include 24, -2423, 1250682, etc...

Sized Integers

Both Rust and Go have sized integer types: integer data types that are stored with a fixed memory value which in turn determines their ranges when it comes to the possible integer values that can be stored in them.

Unsigned, Sized Integers

Unsigned integers hold non-negative integer values, i.e., values that can only be 0 or positive integers.

Given an unsigned integer type that stores n bits in memory, the minimum and maximum values that can be stored are 0 and (2^n) - 1 respectively.

The various unsigned data types are:

  • Unsigned, 8-bit integer 0 to 255
    • Rust: u8
    • Go: uint8
    • Minimum: 0
    • Maximum: 2^8 - 1 = 255
  • Unsigned, 16-bit integer 0 to 65535
    • Rust: u16
    • Go: uint16
    • Minimum: 0
    • Maximum: 2^16 - 1 = 65535
  • Unsigned, 32-bit integer 0 to 4294967295
    • Rust: u32
    • Go: uint32
    • Minimum: 0
    • Maximum: 2^32 - 1 = 4294967295
  • Unsigned, 64-bit integer 0 to 18446744073709551615
    • Rust: u64
    • Go: uint64
    • Minimum: 0
    • Maximum: 2^64 - 1 = 18446744073709551615
  • Unsigned, 128-bit integer 0 to 340282366920938463463374607431768211455
    • Rust: u128
    • Go: no equivalent primitive type
    • Minimum: 0
    • Maximum: 2^128 - 1 = 340282366920938463463374607431768211455

Signed, Sized Integers

Signed integers can hold both positive and negative integer values, but effectively only have half the storage capacity of their unsigned counterparts as the allocated memory is split between the positive and negative halfs. In terms of bits of memory, this can be seen as the signed integer having one less bit of storage capacity, since the sign bit takes up one extra bit.

Given a signed integer type that stores n bits in memory, the minimum and maximum values that can be stored are -2^(n - 1) and 2^(n - 1) - 1 respectively.

The various signed data types are:

  • Signed, 8-bit integer -128 to 127
    • Rust: i8
    • Go: int8
    • Minimum: -2^7 = -128
    • Maximum: 2^7 - 1 = 127
  • Signed, 16-bit integer -32768 to 32767
    • Rust: i16
    • Go: int16
    • Minimum: -2^15 = -32768
    • Maximum: 2^15 - 1 = 32767
  • Signed, 32-bit integer -2147483648 to 2147483647
    • Rust: i32
    • Go: int32
    • Minimum: -2^31 = -2147483648
    • Maximum: 2^31 - 1 = 2147483647
  • Signed, 64-bit integer -9223372036854775808 to 9223372036854775807
    • Rust: i64
    • Go: int64
    • Minimum: -2^63 = -9223372036854775808
    • Maximum: 2^63 - 1 = 9223372036854775807
  • Signed, 128-bit integer -170141183460469231731687303715884105728 to 170141183460469231731687303715884105727
    • Rust: i128
    • Go: no equivalent primitive type
    • Minimum: -2^127 = -170141183460469231731687303715884105728
    • Maximum: 2^127 - 1 = 170141183460469231731687303715884105727

Unsized Integers

The unsized integer type is used when the size of the integer is not known or not important. The actual size is determined based on the architecture of the underlying machine running the program, which is true for both Rust and Go programs.

If the system is a 32-bit architecture, the appropriate 32-bit integer type (u32/i32 in Rust, uint32/int32 in Go) is used.

If the system is a 64-bit architecture, however, the appropriate 64-bit integer type (u64/i64 in Rust, uint64/int64 in Go) is used.

  • Unsigned, unsized integer 0 to 4294967295 or 0 to 18446744073709551615

    • Rust: usize
    • Go: uint
    • Minimum: 0
    • Maximum: 2^32 - 1 = 4294967295 or 2^64 - 1 = 18446744073709551615
  • Signed, unsized integer -2147483648 to 2147483647 or -9223372036854775808 to 9223372036854775807

    • Rust: isize
    • Go: int
    • Minimum: -2^31 = -2147483648 or -2^63 = -9223372036854775808
    • Maximum: 2^31 - 1 = 2147483647 or 2^63 - 1 = 9223372036854775807

Floating Points

Floating point numbers are used to represent real numbers, i.e., numbers with a fractional component. Examples include 3.1415926, 500.672492, and -28402.75927. All floating point types are signed by default, and there is no architecture-dependent unsized variant either.

  • 32-bit floating point -3.4028235e+38 to 3.4028235e+38
    • Rust: f32
    • Go: float32
    • Minimum: -3.4028235e+38
    • Smallest, Positive value: 1.1754944e-38 in Rust, 1e-45 in Go
    • Maximum: 3.4028235e+38
  • 64-bit floating point -1.7976931348623157e+308 to 1.7976931348623157e+308
    • Rust: f64
    • Go: float64
    • Minimum: -1.7976931348623157e+308
    • Smallest, Positive value: 2.2250738585072014e-308 in Rust, 5e-324 in Go
    • Maximum: 1.7976931348623157e+308

Implementations

Here are Rust and Go programs that try to compute the minimum and maximum values of each numeric type described above and test them against the actual values which are declared as constants in each language's respective standard libraries.

Rust

use ibig::{ibig, ubig, IBig, UBig};

fn main() {
    {
        println!("- unsigned, unsized integer -");
        let min: usize = 0;
        let max: usize = usize::MAX;
        println!("usize min: {min}");
        println!("usize max: {max}");
    }

    {
        println!("- unsigned, 8-bit integer -");
        // computations
        let min: u8 = 0;
        let max: u16 = 2_u16.pow(8) - 1;
        // tests
        assert_eq!(min, u8::MIN);
        assert_eq!(max, u8::MAX.into());
        // values
        println!("u8 min: {min}");
        println!("u8 max: {max}");
    }

    {
        println!("- unsigned, 16-bit integer -");
        // computations
        let min: u16 = 0;
        let max: u32 = 2_u32.pow(16) - 1;
        // tests
        assert_eq!(min, u16::MIN);
        assert_eq!(max, u16::MAX.into());
        // values
        println!("u16 min: {min}");
        println!("u16 max: {max}");
    }

    {
        println!("- unsigned, 32-bit integer -");
        // computations
        let min: u32 = 0;
        let max: u64 = 2_u64.pow(32) - 1;
        // tests
        assert_eq!(min, u32::MIN);
        assert_eq!(max, u32::MAX.into());
        // values
        println!("u32 min: {min}");
        println!("u32 max: {max}");
    }

    {
        println!("- unsigned, 64-bit integer -");
        // computations
        let min: u64 = 0;
        let max: u128 = 2_u128.pow(64) - 1;
        // tests
        assert_eq!(min, u64::MIN);
        assert_eq!(max, u64::MAX.into());
        // values
        println!("u64 min: {min}");
        println!("u64 max: {max}");
    }

    {
        println!("- unsigned, 128-bit integer -");
        // computations
        let min: u128 = 0;
        let max: UBig = ubig!(2).pow(128) - 1;
        // tests
        assert_eq!(min, u128::MIN);
        assert_eq!(max, u128::MAX.into());
        // values
        println!("u128 min: {min}");
        println!("u128 max: {max}");
    }

    println!();

    {
        println!("- signed, unsized integer -");
        let min: isize = isize::MIN;
        let max: isize = isize::MAX;
        println!("isize min: {min}");
        println!("isize max: {max}");
    }

    {
        println!("- signed, 8-bit integer -");
        // computations
        let min: i16 = -(2_i16.pow(7));
        let max: u8 = 2_u8.pow(7) - 1;
        // tests
        assert_eq!(min, i8::MIN.into());
        assert_eq!(max, i8::MAX.try_into().unwrap());
        // values
        println!("i8 min: {min}");
        println!("i8 max: {max}");
    }

    {
        println!("- signed, 16-bit integer -");
        // computations
        let min: i32 = -(2_i32.pow(15));
        let max: u16 = 2_u16.pow(15) - 1;
        // tests
        assert_eq!(min, i16::MIN.into());
        assert_eq!(max, i16::MAX.try_into().unwrap());
        // values
        println!("i16 min: {min}");
        println!("i16 max: {max}");
    }

    {
        println!("- signed, 32-bit integer -");
        // computations
        let min: i64 = -(2_i64.pow(31));
        let max: u32 = 2_u32.pow(31) - 1;
        // tests
        assert_eq!(min, i32::MIN.into());
        assert_eq!(max, i32::MAX.try_into().unwrap());
        // values
        println!("i32 min: {min}");
        println!("i32 max: {max}");
    }

    {
        println!("- signed, 64-bit integer -");
        // computations
        let min: i128 = -(2_i128.pow(63));
        let max: u128 = 2_u128.pow(63) - 1;
        // tests
        assert_eq!(min, i64::MIN.into());
        assert_eq!(max, i64::MAX.try_into().unwrap());
        // values
        println!("i64 min: {min}");
        println!("i64 max: {max}");
    }

    {
        println!("- signed, 128-bit integer -");
        // computations
        let min: IBig = -ibig!(2).pow(127);
        let max: u128 = 2_u128.pow(127) - 1;
        // tests
        assert_eq!(min, i128::MIN.into());
        assert_eq!(max, i128::MAX.try_into().unwrap());
        // values
        println!("i128 min: {min}");
        println!("i128 max: {max}");
    }

    println!();

    {
        println!("- 32-bit floating point -");
        let min: f32 = f32::MIN;
        let smallest: f32 = f32::MIN_POSITIVE;
        let max: f32 = f32::MAX;
        println!("f32 min: {min}");
        println!("f32 smallest: {smallest}");
        println!("f32 max: {max}");
    }

    {
        println!("- 64-bit floating point -");
        let min: f64 = f64::MIN;
        let smallest: f64 = f64::MIN_POSITIVE;
        let max: f64 = f64::MAX;
        println!("f64 min: {min}");
        println!("f64 smallest: {smallest}");
        println!("f64 max: {max}");
    }
}
Enter fullscreen mode Exit fullscreen mode

Running this via cargo run produces:

- unsigned, unsized integer -
usize min: 0
usize max: 18446744073709551615
- unsigned, 8-bit integer -
u8 min: 0
u8 max: 255
- unsigned, 16-bit integer -
u16 min: 0
u16 max: 65535
- unsigned, 32-bit integer -
u32 min: 0
u32 max: 4294967295
- unsigned, 64-bit integer -
u64 min: 0
u64 max: 18446744073709551615
- unsigned, 128-bit integer -
u128 min: 0
u128 max: 340282366920938463463374607431768211455

- signed, unsized integer -
isize min: -9223372036854775808
isize max: 9223372036854775807
- signed, 8-bit integer -
i8 min: -128
i8 max: 127
- signed, 16-bit integer -
i16 min: -32768
i16 max: 32767
- signed, 32-bit integer -
i32 min: -2147483648
i32 max: 2147483647
- signed, 64-bit integer -
i64 min: -9223372036854775808
i64 max: 9223372036854775807
- signed, 128-bit integer -
i128 min: -170141183460469231731687303715884105728
i128 max: 170141183460469231731687303715884105727

- 32-bit floating point -
f32 min: -340282350000000000000000000000000000000
f32 smallest: 0.000000000000000000000000000000000000011754944
f32 max: 340282350000000000000000000000000000000
- 64-bit floating point -
f64 min: -179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
f64 smallest: 0.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000022250738585072014
f64 max: 179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
Enter fullscreen mode Exit fullscreen mode

Note that we make use of an external crate ibig to help store and manage big integer computations, especially when trying to compute the minimum and maximum values of the largest data types of each kind of signed and unsigned integer. Make sure to add this dependency to your Cargo.toml so that Rust can pull the crate and its dependencies during cargo run

[dependencies]
ibig = "0.3.6"
Enter fullscreen mode Exit fullscreen mode

Go

package main

import (
    "fmt"
    "math"
    "testing"

    "gotest.tools/v3/assert"
)

func main() {
    var t = &testing.T{}

    func() {
        fmt.Println("- unsigned, unsized integer -")
        var min uint = 0
        var max uint = math.MaxUint
        fmt.Printf("uint min: %d\n", min)
        fmt.Printf("uint max: %d\n", max)
    }()

    func() {
        fmt.Println("- unsigned, 8-bit integer -")
        // computations
        var min uint8 = 0
        var max uint8 = 1<<8 - 1
        // tests
        assert.Assert(t, min == 0)
        assert.Assert(t, max == math.MaxUint8)
        // values
        fmt.Printf("uint8 min: %d\n", min)
        fmt.Printf("uint8 max: %d\n", max)
    }()

    func() {
        fmt.Println("- unsigned, 16-bit integer -")
        // computations
        var min uint16 = 0
        var max uint16 = 1<<16 - 1
        // tests
        assert.Assert(t, min == 0)
        assert.Assert(t, max == math.MaxUint16)
        // values
        fmt.Printf("uint16 min: %d\n", min)
        fmt.Printf("uint16 max: %d\n", max)
    }()

    func() {
        fmt.Println("- unsigned, 32-bit integer -")
        // computations
        var min uint32 = 0
        var max uint32 = 1<<32 - 1
        // tests
        assert.Assert(t, min == 0)
        assert.Assert(t, max == math.MaxUint32)
        // values
        fmt.Printf("uint32 min: %d\n", min)
        fmt.Printf("uint32 max: %d\n", max)
    }()

    func() {
        fmt.Println("- unsigned, 64-bit integer -")
        // computations
        var min uint64 = 0
        var max uint64 = 1<<64 - 1
        // tests
        assert.Assert(t, min == 0)
        assert.Assert(t, max == math.MaxUint64)
        // values
        fmt.Printf("uint64 min: %d\n", min)
        fmt.Printf("uint64 max: %d\n", max)
    }()

    fmt.Println();

    func() {
        fmt.Println("- signed, unsized integer -");
        var min int = math.MinInt
        var max int = math.MaxInt
        fmt.Printf("int min: %d\n", min)
        fmt.Printf("int max: %d\n", max)
    }()

    func() {
        fmt.Println("- signed, 8-bit integer -")
        // computations
        var min int8 = -(1 << 7)
        var max int8 = (1 << 7) - 1
        // tests
        assert.Assert(t, min == math.MinInt8)
        assert.Assert(t, max == math.MaxInt8)
        // values
        fmt.Printf("int8 min: %d\n", min)
        fmt.Printf("int8 max: %d\n", max)
    }()

    func() {
        fmt.Println("- signed, 16-bit integer -")
        // computations
        var min int16 = -(1 << 15)
        var max int16 = (1 << 15) - 1
        // tests
        assert.Assert(t, min == math.MinInt16)
        assert.Assert(t, max == math.MaxInt16)
        // values
        fmt.Printf("int16 min: %d\n", min)
        fmt.Printf("int16 max: %d\n", max)
    }()

    func() {
        fmt.Println("- signed, 32-bit integer -")
        // computations
        var min int32 = -(1 << 31)
        var max int32 = (1 << 31) - 1
        // tests
        assert.Assert(t, min == math.MinInt32)
        assert.Assert(t, max == math.MaxInt32)
        // values
        fmt.Printf("int32 min: %d\n", min)
        fmt.Printf("int32 max: %d\n", max)
    }()

    func() {
        fmt.Println("- signed, 64-bit integer -")
        // computations
        var min int64 = -(1 << 63)
        var max int64 = (1 << 63) - 1
        // tests
        assert.Assert(t, min == math.MinInt64)
        assert.Assert(t, max == math.MaxInt64)
        // values
        fmt.Printf("int64 min: %d\n", min)
        fmt.Printf("int64 max: %d\n", max)
    }()

    fmt.Println();

    func() {
        fmt.Println("- 32-bit floating point -")
        var smallest float32 = math.SmallestNonzeroFloat32
        var max float32 = math.MaxFloat32
        fmt.Printf("float32 min: %g\n", -max)
        fmt.Printf("float32 smallest: %g\n", smallest)
        fmt.Printf("float32 max: %g\n", max)
    }()

    func() {
        fmt.Println("- 64-bit floating point -")
        var smallest float64 = math.SmallestNonzeroFloat64
        var max float64 = math.MaxFloat64
        fmt.Printf("float64 min: %g\n", -max)
        fmt.Printf("float64 smallest: %g\n", smallest)
        fmt.Printf("float64 max: %g\n", max)
    }()

}
Enter fullscreen mode Exit fullscreen mode

Running this via go run outputs

- unsigned, unsized integer -
uint min: 0
uint max: 18446744073709551615
- unsigned, 8-bit integer -
uint8 min: 0
uint8 max: 255
- unsigned, 16-bit integer -
uint16 min: 0
uint16 max: 65535
- unsigned, 32-bit integer -
uint32 min: 0
uint32 max: 4294967295
- unsigned, 64-bit integer -
uint64 min: 0
uint64 max: 18446744073709551615

- signed, unsized integer -
int min: -9223372036854775808
int max: 9223372036854775807
- signed, 8-bit integer -
int8 min: -128
int8 max: 127
- signed, 16-bit integer -
int16 min: -32768
int16 max: 32767
- signed, 32-bit integer -
int32 min: -2147483648
int32 max: 2147483647
- signed, 64-bit integer -
int64 min: -9223372036854775808
int64 max: 9223372036854775807

- 32-bit floating point -
float32 min: -3.4028235e+38
float32 smallest: 1e-45
float32 max: 3.4028235e+38
- 64-bit floating point -
float64 min: -1.7976931348623157e+308
float64 smallest: 5e-324
float64 max: 1.7976931348623157e+308
Enter fullscreen mode Exit fullscreen mode

Go does not have a primitive assert statement for asserting expressions like in Rust, but the third party gotest.tools/v3/assert module implements similar behaviour by augmenting the default testing module provided in Go's standard library. Remember to hit go mod tidy to pull this dependency before running this program.

Go also does not have a block construct like in Rust to declare arbitrary scopes in a program, but similar functionality can be achieved in Go by using Immediately Invoked Function Expressions (IIFEs) as seen above.

A Useful Note About Type Inference

Both Go and Rust have type inference capabilities, which allow them to infer the type of a variable when it is not explicitly annotated. Go infers all integer valued variables to be of the unsized type int, which can correspond to int32 or int64 depending on architecture, while Rust infers all such variables to be of type i32. On the contrary, all floating point valued variables in Go as well as Rust are inferred to be of type float64/f64 respectively. The Rust Book justifies this choice over the lower precision but lesser memory f32 type as follows

The default type is f64 because on modern CPUs, it’s roughly the same speed as f32 but is capable of more precision.

-- Data Types (Chapter 3, Section 3.2)

Afterthoughts

This post was mainly for personal reference and exploration because I tend to keep forgetting the intricacies of numeric data types in Rust and Go, especially the maximum and minimum values of each data type. A lack of awareness of these ranges can cause overflow errors in both languages, something the Rust compiler helpfully points out when running in debug mode.

I hope this helps others as well who want a quick reference to numeric data types in Rust and Go and how to compute their maximum and minimum values in both languages. Feel free to leave comments suggesting additions or improvements that you feel are relevant to this discussion.

💖 💪 🙅 🚩
bkataru
Baalateja Kataru

Posted on April 23, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related