CHAPTER 06 · CORE LANGUAGE

Basic Types

Every value in Go has a type. The basic types (integers, floats, booleans, strings, and complex numbers) are the atoms you'll combine into everything else. This chapter is a reference tour: ranges, sizes, overflow, and Go's strict conversion rules.

Learning objectives

  • List Go's integer types (signed and unsigned) and their ranges.
  • Know when to prefer int64 vs int.
  • Explain what happens on integer overflow at run time.
  • Pick the right floating-point type and understand its precision limits.
  • Convert between numeric types explicitly and safely.

Integer types

Go's integer family has ten members. Five signed (hold positive and negative values), five unsigned (positive only):

SignedRangeUnsignedRange
int8-128 to 127uint8 / byte0 to 255
int16-32,768 to 32,767uint160 to 65,535
int32 / rune~±2.1 billionuint32~4.3 billion
int64~±9.2 × 1018uint64~1.8 × 1019
int32- or 64-bit (platform)uint32- or 64-bit (platform)

Two aliases worth noticing:

  • byte is an alias for uint8: you'll see it constantly when dealing with bytes of data.
  • rune is an alias for int32: it represents a Unicode code point. Chapter 7 is entirely about runes and strings.
Just use int until you have a reason not to int is the idiomatic default for counters, indexes, and general numeric work. Reach for a sized type like int64 when you need a specific range, performance characteristic, or binary/protocol compatibility. Unsigned types are less common than you'd think, int is fine for "counts of things".

Overflow and wrap-around

At compile time, Go rejects literal values that don't fit:

var b int8 = 300   // ❌ constant 300 overflows int8

At run time, integer arithmetic silently wraps around:

package main

import "fmt"

func main() {
    var x uint8 = 255
    x = x + 1             // wraps to 0
    fmt.Println(x)        // prints 0

    var y int8 = 127
    y = y + 1             // wraps to -128
    fmt.Println(y)        // prints -128
}

Unlike some languages, Go does not panic on overflow. If you need overflow detection, you have to check explicitly. The math package defines constants like math.MaxInt32 and math.MinInt64 to help.

Floating-point types

Go has two floating-point types, both IEEE 754:

  • float32: single precision, ~7 decimal digits.
  • float64: double precision, ~15 digits. The default choice.
var pi float64 = 3.14159265358979
var temp float32 = 98.6

Floating-point arithmetic isn't exact. The classic trap:

package main

import "fmt"

func main() {
    a := 0.1 + 0.2
    fmt.Println(a)          // 0.30000000000000004
    fmt.Println(a == 0.3)   // false
}
!
Never use floats for money Floating-point math can't exactly represent decimal fractions like 0.1. For currency, use integers (cents/pennies) or a decimal library like github.com/shopspring/decimal. This is a lesson every programmer learns once the hard way.

Complex numbers

Go has built-in complex numbers, for when you're doing signal processing, scientific computing, or you just want to feel fancy:

package main

import "fmt"

func main() {
    var z complex128 = 2 + 3i
    fmt.Println(z)           // (2+3i)
    fmt.Println(real(z))     // 2
    fmt.Println(imag(z))     // 3
}

complex64 stores two float32s; complex128 stores two float64s. The built-in functions complex(real, imag), real(), and imag() construct and decompose them.

You probably won't reach for these in typical application code. They're included here for completeness.

Booleans

bool is either true or false. No truthy/falsy values, 0 is not false, "" is not false. Only a bool works in a condition:

package main

import "fmt"

func main() {
    isActive := true
    isExpired := false

    // Only bool-valued expressions are allowed in if/for conditions.
    if isActive && !isExpired {
        fmt.Println("account is active")
    }
}

This strictness is deliberate, it prevents whole categories of bugs (e.g. accidentally treating 0 as "no value"). Chapter 8 covers the &&, ||, and ! operators in detail.

Strings (quick intro)

A string in Go is an immutable sequence of bytes, typically UTF-8 encoded text. You can't modify individual bytes; to "change" a string you build a new one.

package main

import "fmt"

func main() {
    first := "John"
    last  := "Doe"
    full  := first + " " + last     // concatenation with +
    fmt.Println(full)                // John Doe
    fmt.Println(len(full))           // 8 (bytes, not characters)
}

There's a LOT more to strings, indexing returns a byte, ranging gives you runes, and UTF-8 encoding has subtleties that trip up nearly everyone. All of that is the entire next chapter.

Type conversions

Go does no implicit numeric conversions. This is unusual if you're coming from C or JavaScript, even "compatible" numeric types need an explicit cast:

package main

import "fmt"

func main() {
    var a int = 42
    var b float64 = float64(a)     // int → float64
    var c int32 = int32(a)         // int → int32 (may truncate)
    var d int = int(3.9)           // float → int (truncates toward zero: 3)

    fmt.Println(a, b, c, d)
}

The syntax is T(x): the destination type, then the value in parentheses. Like calling a constructor.

!
Converting narrowing is your responsibility int32(1_000_000_000_000) silently discards the high bits. If you're going from a wider type to a narrower one, check bounds yourself.

Converting to/from strings is a whole different kind of operation. You'll use the strconv package:

package main

import (
    "fmt"
    "strconv"
)

func main() {
    s := strconv.Itoa(42)              // int → string: "42"
    fmt.Println(s)

    n, err := strconv.Atoi("42")       // string → int
    if err != nil {
        fmt.Println("parse failed:", err)
        return
    }
    fmt.Println(n + 1)                 // 43
}

Don't confuse string(65) with strconv.Itoa(65)! The first converts a code point to a rune-string (it produces "A"), which is almost never what you want. Use strconv for number ↔ string conversions.

Summary table

CategoryTypesDefaultNotes
Signed intint8, int16, int32, int64, intintSilent wrap-around on overflow
Unsigned intuint8uint64, uint, uintptruintbyte = uint8
Floatfloat32, float64float64IEEE 754; don't use for money
Complexcomplex64, complex128complex128Rarely used
BoolboolboolNo truthy/falsy
StringstringstringImmutable, typically UTF-8
Aliasrune = int32n/aA Unicode code point

Check your understanding

Practice exercises

EXERCISE 1

Overflow playground

Write a program that demonstrates overflow for int8, uint8, and int32. For each type, start at its maximum value and add 1. Print the result.

Bonus: try starting at the minimum value and subtracting 1.

Show a working solution
package main

import (
    "fmt"
    "math"
)

func main() {
    var a int8   = math.MaxInt8
    var b uint8  = math.MaxUint8
    var c int32  = math.MaxInt32

    fmt.Printf("int8  %d + 1 = %d\n",  a, a+1)
    fmt.Printf("uint8 %d + 1 = %d\n", b, b+1)
    fmt.Printf("int32 %d + 1 = %d\n", c, c+1)

    var d int8 = math.MinInt8
    fmt.Printf("int8 %d - 1 = %d\n", d, d-1)   // wraps to 127
}
EXERCISE 2

Safe string → int

Write a program that reads the program's first command-line argument, converts it to an int, doubles it, and prints the result. If the argument is missing or not a number, print a friendly error message and exit with code 1.

Show one possible solution
package main

import (
    "fmt"
    "os"
    "strconv"
)

func main() {
    if len(os.Args) < 2 {
        fmt.Fprintln(os.Stderr, "usage: double N")
        os.Exit(1)
    }
    n, err := strconv.Atoi(os.Args[1])
    if err != nil {
        fmt.Fprintf(os.Stderr, "not a valid integer: %q\n", os.Args[1])
        os.Exit(1)
    }
    fmt.Println(n * 2)
}

Further reading

Types squared away.