4 min read
On this page

Basic Types & Variables

Go's type system is simple and explicit. There are no implicit conversions, no union types, and no type coercion surprises. What you declare is what you get. This chapter covers the building blocks: primitive types, variables, constants, and the collection types you will use every day.

Primitive Types

Numeric Types

var i int       // Platform-dependent: 64-bit on 64-bit systems
var i8 int8     // -128 to 127
var i32 int32   // -2 billion to 2 billion
var i64 int64   // Very large range
var u uint      // Unsigned, platform-dependent
var f64 float64 // Double precision (the default for float literals)

In practice, use int for most integer work and float64 for floating-point. Use sized types (int32, int64) when you need a specific size for binary protocols or serialization.

Strings, Bytes & Runes

var s string    // UTF-8 encoded, immutable
var b byte      // Alias for uint8
var r rune      // Alias for int32, represents a Unicode code point

Strings in Go are immutable sequences of bytes. A byte is a single byte. A rune is a Unicode code point, which can be 1-4 bytes in UTF-8.

s := "Hello, 世界"
fmt.Println(len(s))          // 13 (bytes, not characters)
fmt.Println(len([]rune(s)))  // 9 (rune count)

for i, r := range s {
    fmt.Printf("byte %d: %c (U+%04X)\n", i, r, r)
}
byte 0: H (U+0048)
byte 1: e (U+0065)
byte 2: l (U+006C)
byte 3: l (U+006C)
byte 4: o (U+006F)
byte 5: , (U+002C)
byte 6:   (U+0020)
byte 7: 世 (U+4E16)
byte 10: 界 (U+754C)

Notice the byte index jumps from 7 to 10 -- the Chinese characters are 3 bytes each in UTF-8.

Bool

var b bool  // false (zero value)
done := true

No truthy/falsy values. if 1 does not compile. Only bool expressions work in conditions.

Variable Declaration

Short Declaration (:=)

Inside functions, := declares and initializes a variable with an inferred type.

name := "Alice"          // string
count := 42              // int
ratio := 3.14            // float64
active := true           // bool
items := []string{}      // []string

var Declaration

Use var at the package level, when you want the zero value, or when you need a specific type.

var name string              // "" (zero value)
var count int                // 0
var ratio float32 = 3.14    // Explicit type (float32, not float64)

// Grouped declaration
var (
    host    = "localhost"
    port    = 8080
    timeout = 30 * time.Second
)

Zero Values

Every type in Go has a zero value. Variables are never uninitialized.

var i int          // 0
var f float64      // 0.0
var s string       // ""
var b bool         // false
var p *int         // nil
var sl []int       // nil
var m map[string]int // nil

Type Conversion

Go never converts types implicitly. You must be explicit about every conversion.

var i int = 42
var f float64 = float64(i)    // Must convert explicitly
var u uint = uint(f)          // Must convert explicitly

// This does not compile:
// var f float64 = i          // cannot use i (type int) as type float64

String conversions follow specific rules:

// int to string: does NOT give you "65"
s := string(65)          // "A" (treats 65 as a rune)

// For number-to-string conversion, use strconv
s := strconv.Itoa(65)   // "65"
n, err := strconv.Atoi("65")  // 65, nil

// Byte slice to string and back
bs := []byte("hello")
s := string(bs)

Constants & iota

Constants are declared with const and must be determinable at compile time.

const pi = 3.14159
const maxRetries = 3
const greeting = "hello"

iota for Enumerations

iota is a constant generator that starts at 0 and increments for each constant in a group. It is Go's answer to enums.

type Role int

const (
    RoleGuest  Role = iota  // 0
    RoleUser                // 1
    RoleAdmin               // 2
)

iota supports expressions: 1 << (10 * (iota + 1)) gives you KB, MB, GB, TB constants.

Arrays

Arrays have a fixed size, determined at compile time. They are value types -- assigning an array copies all its elements.

var a [5]int                    // [0, 0, 0, 0, 0]
b := [3]string{"a", "b", "c"}  // Fixed size 3
c := [...]int{1, 2, 3, 4}      // Size inferred from elements: [4]int

Arrays are rarely used directly. Their fixed size makes them inflexible. You will use slices instead.

Slices

Slices are dynamic, reference-backed views over arrays. They are what you use for ordered collections in Go.

// Create slices
s := []int{1, 2, 3}               // Literal
s := make([]int, 5)                // Length 5, all zeros
s := make([]int, 0, 100)          // Length 0, capacity 100

// Append
s = append(s, 4, 5, 6)
s = append(s, otherSlice...)       // Append another slice

// Slicing
sub := s[1:3]   // Elements at index 1, 2 (not 3)
sub := s[:3]    // First 3 elements
sub := s[2:]    // From index 2 to end

// Length and capacity
fmt.Println(len(s))  // Number of elements
fmt.Println(cap(s))  // Underlying array capacity

Slices Are References

A slice is a header containing a pointer to an underlying array, a length, and a capacity. Assigning a slice does not copy the data.

original := []int{1, 2, 3, 4, 5}
subset := original[1:3]  // [2, 3] -- shares underlying array

subset[0] = 99
fmt.Println(original)  // [1, 99, 3, 4, 5] -- original is modified

To get an independent copy, use the copy built-in or the slices.Clone function (Go 1.21+):

src := []int{1, 2, 3}
dst := make([]int, len(src))
copy(dst, src)
// Or: dst := slices.Clone(src)

Nil Slices vs Empty Slices

A nil slice (var s []int) and an empty slice (s := []int{}) both have len(s) == 0 and both work with append, range, and len. Prefer nil slices unless you specifically need non-nil (e.g., JSON serialization where null vs [] matters).

Maps

Maps are unordered key-value collections. Keys must be comparable types (no slices or maps as keys).

// Create maps
m := map[string]int{
    "alice": 32,
    "bob":   28,
}
m := make(map[string]int)       // Empty map, ready to use

// Read, write, delete
m["charlie"] = 35
age := m["alice"]                // 32
delete(m, "bob")

// Check existence with the comma-ok idiom
age, ok := m["dave"]
if !ok {
    fmt.Println("dave not found")
}

A nil map can be read from (returns zero values) but writing to a nil map panics:

var m map[string]int
fmt.Println(m["key"])  // 0 (zero value, no panic)
m["key"] = 1           // PANIC: assignment to entry in nil map

Always initialize maps with make or a literal before writing.

make vs new

These two built-in functions serve different purposes and are a common source of confusion.

// make: initializes slices, maps, and channels
s := make([]int, 10)          // Slice with length 10
m := make(map[string]int)     // Empty, initialized map
ch := make(chan int, 5)        // Buffered channel

// new: allocates memory and returns a pointer
p := new(int)                  // *int, pointing to 0
fmt.Println(*p)                // 0

In practice, make is used constantly (for slices, maps, and channels). new is rarely used because struct literals with & are more idiomatic:

// Prefer this:
cfg := &Config{Port: 8080}

// Over this:
cfg := new(Config)
cfg.Port = 8080

Common Pitfalls

  • Assuming string indexing gives characters. s[0] gives a byte, not a rune. For Unicode-safe iteration, use range or []rune(s).
  • Forgetting that slice operations share memory. Modifying a sub-slice modifies the original. Use copy or slices.Clone when you need independence.
  • Writing to a nil map. Reading a nil map returns zero values silently. Writing to one panics. Always initialize with make or a literal.
  • Implicit type conversions from other languages. int + float64 does not compile. You must convert one side explicitly.
  • Using iota without a type. Untyped iota constants are just int. Define a named type for your enum to get type safety.
  • Comparing slices with ==. Slices cannot be compared with == (except to nil). Use slices.Equal from Go 1.21+ or write a loop.

Key Takeaways

  • Go has a small set of built-in types: integers, floats, strings, bools, bytes, and runes.
  • Use := for concise declarations inside functions, var for package-level or zero-value declarations.
  • All type conversions are explicit. Go does not coerce types silently.
  • iota generates enumeration constants. Pair it with a named type for type safety.
  • Slices are dynamic and reference-based; arrays are fixed and value-based. Use slices.
  • Maps must be initialized before writing. Use make or a literal.
  • make creates slices, maps, and channels. new allocates a pointer. Prefer struct literals with & over new.