Skip to main content
Go beginner Lesson 4 of 25

Data Types in Go

A complete guide to Go's built-in types: integers, floats, strings, booleans, runes, bytes, and explicit type conversions.

Integer Types

Go provides signed and unsigned integers at multiple fixed sizes. Having explicit sizes matters when working with binary protocols, file formats, or external systems that specify exactly how wide a field should be. For everyday use, int is the right default — it matches the native word size of the platform and is what the standard library uses for counts and indices.

var a int8   = 127          // -128 to 127
var b int16  = 32767        // -32768 to 32767
var c int32  = 2147483647
var d int64  = 9223372036854775807
var e int    = 42           // platform-width (32 or 64 bit) — use this by default

var u uint   = 42           // unsigned, platform-width
var u8 uint8 = 255          // 0 to 255 (also called byte)

Which to use? Default to int for general integers. Use specific sizes when interfacing with binary protocols, files, or external systems that specify widths.

Go supports readable numeric literals with underscores as separators and alternative bases, which helps when writing values that align with hardware or protocol specifications:

// Underscores improve readability for large numbers
population := 8_000_000_000
hexColor   := 0xFF5733    // hexadecimal
octal      := 0o755       // octal (Unix file permissions)
binary     := 0b1010_1100 // binary

Floating-Point Types

Go has two floating-point types. The difference is precision: float64 gives you about 15 significant decimal digits, while float32 gives about 7. Loss of precision in float32 can cause subtle bugs in calculations, so float64 is the right default for almost everything.

var f32 float32 = 3.14          // ~7 decimal digits of precision
var f64 float64 = 3.141592653589793  // ~15 decimal digits — use this by default

float64 is the default and almost always the right choice. Use float32 only when memory is constrained and precision can be sacrificed (e.g., graphics or large numerical arrays).

import "math"

fmt.Println(math.MaxFloat64)  // 1.7976931348623157e+308
fmt.Println(math.Pi)          // 3.141592653589793
fmt.Println(math.Sqrt(2))     // 1.4142135623730951

// Special float values — useful for detecting edge cases in calculations
posInf := math.Inf(1)
negInf := math.Inf(-1)
notNum := math.NaN()
fmt.Println(math.IsNaN(notNum)) // true

Boolean

The bool type holds exactly two values: true and false. Unlike C or Python, Go does not treat integers as booleans — if 1 {} is a compile error. This strictness prevents a common class of bugs where a non-zero value is accidentally treated as truthy in a context where you actually meant to check a specific condition.

var flag bool = true
active := false

// Must be a boolean expression — integers don't work as conditions
if flag {
    fmt.Println("active")
}

// Logical operators produce booleans
fmt.Println(true && false)  // false — AND: both must be true
fmt.Println(true || false)  // true  — OR: at least one must be true
fmt.Println(!true)          // false — NOT: inverts the value

Strings

Strings in Go are immutable sequences of bytes encoded in UTF-8. Immutability means you can safely pass strings between goroutines and share them without copying. UTF-8 encoding means Go programs handle international text correctly by default, but it also means that a string’s byte length and its character count can differ — an important distinction covered in depth in the Strings tutorial.

s := "Hello, 世界"

fmt.Println(len(s))           // 13 — byte count, not character count
fmt.Println(s[0])             // 72 — byte value of 'H'
fmt.Println(string(s[0]))     // "H"

// Raw string literals preserve backslashes and newlines literally
raw := `C:\Users\alice\go\bin`
multiline := `line one
line two
line three`

Strings support comparison operators (==, <, >) which compare lexicographically.

Rune and Byte

The byte and rune types exist to clearly distinguish between raw binary data and Unicode text. When you’re working with network protocols or file formats, you think in bytes. When you’re processing human-readable text, you think in runes (characters). Using the right type makes your intent explicit and prevents subtle bugs with multi-byte characters.

// byte is an alias for uint8 — represents raw binary or ASCII data
var b byte = 'A'
fmt.Println(b)         // 65 — the ASCII value

// rune is an alias for int32 — represents one Unicode code point
var r rune = ''
fmt.Println(r)         // 19990 (Unicode code point)
fmt.Println(string(r)) // 世

// range iterates a string by rune, not by byte — handles multi-byte correctly
s := "Hello, 世界"
for i, ch := range s {
    fmt.Printf("index %d: %c (U+%04X)\n", i, ch, ch)
}
// index 0: H (U+0048)
// index 7: 世 (U+4E16)  ← byte index 7, not character index 7
// index 10: 界 (U+754C)

// Convert string to rune slice when you need to work with individual characters
runes := []rune(s)
fmt.Println(len(runes)) // 9 — 9 characters, not 13 bytes

Complex Numbers

Go has built-in complex number types — a relatively rare feature in mainstream languages. They are not commonly used, but they are valuable for scientific computing, signal processing, and certain mathematical algorithms where complex arithmetic would otherwise require a library.

c1 := complex(3, 4)  // 3+4i — complex128 by default
c2 := 1 + 2i

fmt.Println(real(c1))  // 3
fmt.Println(imag(c1))  // 4
fmt.Println(c1 + c2)   // (4+6i)

import "math/cmplx"
fmt.Println(cmplx.Abs(c1))  // 5 (magnitude — Pythagorean theorem: sqrt(3²+4²))

Type Conversions

Go never implicitly converts between types. Every conversion must be written explicitly. This feels verbose at first, but it eliminates an entire category of bugs where data is silently narrowed, widened, or reinterpreted. When you see a conversion in Go code, it is always intentional and visible.

var i int = 42
var f float64 = float64(i)   // int → float64: explicit widening
var u uint = uint(f)          // float64 → uint: explicit, truncates decimal part

// string(n) interprets n as a Unicode code point — this is often not what you want
n := 65
s := string(n)              // "A" — treats 65 as the code point for 'A'
numStr := fmt.Sprintf("%d", n) // "65" — converts the number to its digit representation

// Use strconv for correct numeric ↔ string conversions
import "strconv"

s1 := strconv.Itoa(42)         // "42" — integer to string
n1, err := strconv.Atoi("123") // 123, nil — string to integer
f1, err := strconv.ParseFloat("3.14", 64) // 3.14, nil
b1, err := strconv.ParseBool("true")       // true, nil

// Format a number back to string with control over representation
s2 := strconv.FormatFloat(3.14159, 'f', 2, 64) // "3.14" — 2 decimal places
s3 := strconv.FormatInt(255, 16)                // "ff" — hexadecimal

Type Aliases and Defined Types

Go lets you create new named types based on existing ones. This is a powerful tool for making code self-documenting and for preventing values of different conceptual units from being mixed accidentally. A Meters value and a Feet value are both float64 under the hood, but Go will not let you assign one to the other without an explicit conversion.

// Type alias — completely interchangeable with the original type
type Celsius    = float64
type Fahrenheit = float64

// Defined type — creates a distinct type that requires explicit conversion
type Meters float64
type Feet   float64

func toFeet(m Meters) Feet {
    return Feet(m * 3.28084)
}

dist := Meters(100)
fmt.Println(toFeet(dist)) // 328.084

// Trying to assign Meters to Feet directly is a compile error — catches unit bugs:
// var f Feet = dist  // compile error: cannot use dist (Meters) as Feet

Type Size Reference

TypeSizeRange / Notes
bool1 bytetrue / false
int81 byte-128 to 127
int162 bytes-32,768 to 32,767
int32 / rune4 bytes-2B to 2B
int648 bytes-9.2e18 to 9.2e18
int4 or 8 bytesplatform-dependent
uint8 / byte1 byte0 to 255
float324 bytes~7 significant digits
float648 bytes~15 significant digits
string16 bytes headerimmutable byte sequence
complex648 bytestwo float32 components
complex12816 bytestwo float64 components

Practical Example

This example shows how types and conversions work together in a realistic calculation — parsing string inputs, converting between numeric types, and using a switch on float values to produce a string result.

package main

import (
    "fmt"
    "strconv"
)

func bmi(weightKg, heightM float64) float64 {
    return weightKg / (heightM * heightM)
}

func classify(bmi float64) string {
    switch {
    case bmi < 18.5:
        return "Underweight"
    case bmi < 25.0:
        return "Normal"
    case bmi < 30.0:
        return "Overweight"
    default:
        return "Obese"
    }
}

func main() {
    weightStr := "70"
    heightStr := "1.75"

    // strconv.ParseFloat converts a string to float64 safely
    weight, _ := strconv.ParseFloat(weightStr, 64)
    height, _ := strconv.ParseFloat(heightStr, 64)

    b := bmi(weight, height)
    fmt.Printf("BMI: %.1f%s\n", b, classify(b))
    // BMI: 22.9 — Normal
}

Frequently Asked Questions

What is the difference between int and int64 in Go?
int is platform-dependent — it's 32 bits on 32-bit systems and 64 bits on 64-bit systems. int64 is always 64 bits regardless of platform. Use int for general counting and indexing; use int64 when you need a guaranteed size, such as in serialization or when interfacing with external systems.
What is a rune in Go?
A rune is an alias for int32 and represents a Unicode code point. While a byte holds one byte (ASCII), a rune holds one full Unicode character. Use runes when working with multi-byte characters like emoji or non-Latin scripts.
Does Go do automatic type conversion?
No. Go never implicitly converts between numeric types. You must use explicit conversion: float64(myInt) or int(myFloat). This prevents subtle bugs caused by silent narrowing or widening conversions.