Go October 2, 2024 Aditya Rawas

Go Data Types: A Comprehensive Guide for Developers

One of the fundamental aspects of any programming language is its type system. In Go (Golang), data types are designed to be simple, explicit, and efficient. Understanding Go’s type system is essential for writing reliable, well-optimized applications.

This guide covers every primary data type in Go — from primitives to composite types like slices, maps, and interfaces — with examples and best practices.


Primitive Types

Boolean (bool)

Represents true or false. Used in control flow — if statements, loops, conditions.

var isGoAwesome bool = true

Go does not allow implicit conversions between numeric and boolean types. if 1 { } is a compile error — you must write if count > 0 { }.


String (string)

An immutable sequence of bytes encoded in UTF-8. Defined with double quotes.

var greeting string = "Hello, Go!"
greeting := "Hello, " + "World!" // concatenation with +

For multi-line strings, use backtick raw string literals:

query := `SELECT *
FROM users
WHERE active = true`

Integer Types

Go provides multiple integer types with specific bit widths.

Signed integers:

TypeSizeRange
int32 or 64-bit (platform)Platform-dependent
int88-bit-128 to 127
int1616-bit-32,768 to 32,767
int3232-bit-2,147,483,648 to 2,147,483,647
int6464-bit-9.2 × 10¹⁸ to 9.2 × 10¹⁸

Unsigned integers:

TypeSizeRange
uintPlatform0 to max
uint88-bit0 to 255
uint1616-bit0 to 65,535
uint3232-bit0 to 4,294,967,295
uint6464-bit0 to 18.4 × 10¹⁸

Use int for general-purpose integers. Only reach for int8/int16 when memory optimization is critical (e.g., large arrays of small numbers).


Byte and Rune

  • byte is an alias for uint8. Used for raw binary data or ASCII characters.
  • rune is an alias for int32. Represents a Unicode code point — use it when working with text that may contain non-ASCII characters.
var letter byte = 'A'       // ASCII
var emoji rune  = '🚀'      // Unicode code point U+1F680

Floating-Point Types

  • float32: 32-bit single precision
  • float64: 64-bit double precision (default for float literals)
var pi float64 = 3.141592653589793
var e  float32 = 2.71828

When in doubt, use float64 — it’s the default and provides higher precision.


Complex Number Types

Go has built-in support for complex numbers:

var c complex128 = complex(3.0, 4.0) // 3 + 4i
fmt.Println(real(c)) // 3
fmt.Println(imag(c)) // 4

Rarely used in web development, but valuable for scientific computing.


Composite Types

Arrays

An array is a fixed-length sequence of elements of the same type. The length is part of the type — [3]int and [5]int are different types.

var scores [3]int = [3]int{95, 87, 72}
// or using short declaration:
names := [3]string{"Alice", "Bob", "Charlie"}

fmt.Println(names[0]) // Alice
fmt.Println(len(names)) // 3

Use [...] to let the compiler infer the length:

primes := [...]int{2, 3, 5, 7, 11}

Arrays in Go are values, not references. Assigning an array to another variable copies all elements. For large arrays, this is expensive — use slices instead.


Slices

Slices are the most important composite type in Go. A slice is a dynamically-sized, flexible view into an underlying array.

// Create a slice literal
fruits := []string{"apple", "banana", "cherry"}

// Append elements
fruits = append(fruits, "date")

// Slice a slice (half-open range)
fmt.Println(fruits[1:3]) // ["banana", "cherry"]

// Length and capacity
fmt.Println(len(fruits)) // 4
fmt.Println(cap(fruits)) // varies (Go manages this internally)

make() for Pre-allocated Slices

When you know the size upfront, use make to avoid repeated re-allocations:

// make([]Type, length, capacity)
nums := make([]int, 0, 100) // empty slice, capacity 100

Iterating Over a Slice

for i, v := range fruits {
    fmt.Printf("Index %d: %s\n", i, v)
}

Slice vs Array: When to Use Which

ArraySlice
LengthFixed at compile timeDynamic
Passed to functionsBy value (copy)By reference (to underlying array)
Use caseFixed-size buffers, matricesGeneral-purpose sequences

Default to slices. Use arrays only when you need a fixed-size data structure or when embedding in a struct.


Maps

A map is an unordered collection of key-value pairs. Keys must be comparable types (string, int, bool — but not slices or maps).

// Map literal
scores := map[string]int{
    "Alice": 95,
    "Bob":   87,
}

// Reading a value
fmt.Println(scores["Alice"]) // 95

// Adding / updating
scores["Charlie"] = 72

// Deleting
delete(scores, "Bob")

// Check if key exists (comma-ok idiom)
value, ok := scores["Bob"]
if !ok {
    fmt.Println("Bob not found")
}

Creating Maps with make

cache := make(map[string][]string) // map of string → string slice

Never read from a nil map without initializing it. Reading from a nil map returns the zero value. Writing to a nil map panics.

var m map[string]int // nil map
m["key"] = 1         // PANIC: assignment to entry in nil map

m = make(map[string]int) // initialize first
m["key"] = 1             // OK

Iterating Over a Map

for key, value := range scores {
    fmt.Printf("%s: %d\n", key, value)
}

Note: map iteration order is random in Go by design. Never rely on a specific iteration order.


Pointers

A pointer holds the memory address of a value. Go uses pointers explicitly, unlike JavaScript or Python where object references are implicit.

x := 42
p := &x           // p is a pointer to x (type: *int)

fmt.Println(p)    // prints memory address: 0xc0000b4008
fmt.Println(*p)   // dereferences pointer: 42

*p = 100          // modifies x through the pointer
fmt.Println(x)    // 100

When to Use Pointers

Modify a value inside a function:

func double(n *int) {
    *n *= 2
}

x := 5
double(&x)
fmt.Println(x) // 10

Avoid copying large structs:

// Passing a large struct by value copies all fields — expensive
func processLarge(s LargeStruct) { ... }

// Passing a pointer avoids the copy
func processLarge(s *LargeStruct) { ... }

Represent optional values: A nil pointer represents the absence of a value — Go’s equivalent of nullable.

func findUser(id int) *User {
    // return nil if not found
    return nil
}

Go does not have pointer arithmetic like C. You can’t increment a pointer to move through memory — this is intentional for safety.


Interfaces

An interface defines a set of method signatures. Any type that implements all the methods satisfies the interface — implicitly, with no implements keyword needed.

type Shape interface {
    Area() float64
    Perimeter() float64
}

type Rectangle struct {
    Width, Height float64
}

func (r Rectangle) Area() float64      { return r.Width * r.Height }
func (r Rectangle) Perimeter() float64 { return 2 * (r.Width + r.Height) }

type Circle struct {
    Radius float64
}

func (c Circle) Area() float64      { return math.Pi * c.Radius * c.Radius }
func (c Circle) Perimeter() float64 { return 2 * math.Pi * c.Radius }

Both Rectangle and Circle satisfy Shape without any explicit declaration:

func printShape(s Shape) {
    fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter())
}

printShape(Rectangle{Width: 5, Height: 3})
printShape(Circle{Radius: 4})

The Empty Interface

interface{} (or any in modern Go) accepts any type:

func printAnything(v any) {
    fmt.Println(v)
}

printAnything(42)
printAnything("hello")
printAnything([]int{1, 2, 3})

Use it sparingly — it loses type safety.


Type Conversion

Go is strongly typed — you must convert explicitly between types:

var i int = 42
var f float64 = float64(i)   // int → float64
var u uint = uint(f)          // float64 → uint

var s string = strconv.Itoa(i) // int → string (not string(i)!)
var n int
n, _ = strconv.Atoi("123")    // string → int

string(65) gives you "A" (the ASCII character), not "65". Use strconv.Itoa to convert integers to their string representation.


Type Inference

The := short declaration infers the type from the assigned value:

name        := "Golang"    // string
age         := 30           // int
ratio       := 3.14         // float64
isDeveloper := true         // bool

Types remain strict at compile time — inference just removes the verbosity of explicit declarations.


Key Takeaways

  • Use int for general integers, float64 for decimals, string for text.
  • Use rune for Unicode characters, byte for raw binary data.
  • Arrays are fixed-size value types. Slices are dynamic and the default choice for sequences.
  • Maps hold key-value pairs. Always initialize with make or a literal before writing.
  • Pointers enable mutation inside functions and avoid expensive struct copies. Go has no pointer arithmetic.
  • Interfaces are satisfied implicitly — any type with the right methods qualifies. Prefer narrow interfaces.
  • All type conversions must be explicit — Go never performs implicit numeric coercion.

Once you’re comfortable with types, the natural next step is Go structs — how to group these types into custom data models — and creating your first Go module to package and share your code.

Aditya Rawas

Written by

Aditya Rawas

Full-stack engineer writing deep-dives on JavaScript, TypeScript, React, AWS, Docker, and Kubernetes. Passionate about making complex engineering concepts accessible to developers at every level.