Skip to content

Types and constants

Go Language › 02 Types and constants

Built-in types

Go's type system is small but complete. Every value has a type, and the compiler enforces type compatibility at compile time.

Booleans

go
var ready bool = true
var done bool = false

A bool is either true or false. The zero value is false.

Numeric types

Go provides signed integers, unsigned integers, floats, and complex numbers at multiple widths.

go
var a int     // platform-dependent: 32 or 64 bits
var b int8    // -128 to 127
var c int16   // -32768 to 32767
var d int32   // -2^31 to 2^31-1
var e int64   // -2^63 to 2^63-1

var u uint    // platform-dependent
var v uint8   // 0 to 255
var w uint16  // 0 to 65535
var x uint32  // 0 to 2^32-1
var y uint64  // 0 to 2^64-1

var p uintptr // unsigned integer large enough to hold any pointer

var f float32 // ~6 decimal digits of precision
var g float64 // ~15 decimal digits of precision

var c1 complex64   // real + imag float32
var c2 complex128  // real + imag float64

The int and uint types are platform-dependent — 32 bits on 32-bit architectures, 64 bits on 64-bit architectures. uintptr is an unsigned integer type large enough to hold any pointer value; it is used primarily in unsafe memory operations and syscall interfaces. Use specific-width types when serialising wire formats or controlling memory layout.

Strings

Strings are immutable byte sequences. The string type has no internal encoding requirement — it can hold arbitrary bytes. By convention, Go source code is UTF-8, and the range loop over a string decodes UTF-8 runes.

go
var s string = "hello"
t := "world"
u := "hello\n" +
    "world"

// len counts bytes, not runes
fmt.Println(len("élite")) // 6 bytes (é is 2 bytes)

Byte and rune aliases

go
type byte = uint8
type rune = int32
  • byte is an alias for uint8. It signals that the value is raw data rather than a numeric quantity.
  • rune is an alias for int32. It holds a single Unicode code point.
go
var b byte = 'a'        // byte literal (ASCII)
var r rune = 'é'        // rune literal (multi-byte)
var r2 rune = '\u2764'  // ❤ — heart emoji code point

Zero values

Every type in Go has a zero value — the default value variables hold when no explicit value is assigned. There is no concept of an uninitialised variable.

go
var i int       // 0
var f float64   // 0
var b bool      // false
var s string    // "" (empty string)
var p *int      // nil
var sl []int    // nil
var m map[int]string // nil
var c chan int  // nil
var iface interface{} // nil
var fn func()   // nil

This eliminates an entire class of bugs common in C and C++ where variables may be used before initialisation. It also means every struct field, array element, and map value is always defined.

go
var arr [3]int      // [0 0 0]
var st struct { x int; y string }  // {0, ""}

Type inference with :=

Go supports short variable declaration with type inference. The compiler deduces the type from the right-hand side expression.

go
x := 42      // int
y := 3.14    // float64
z := "hello" // string

The inferred types follow Go's literal rules:

  • Integer literals default to int.
  • Float literals default to float64.
  • Complex literals default to complex128.
  • Rune literals default to rune (which is int32).
  • String literals default to string.

When you need a specific type, use explicit var:

go
var x int8 = 42           // explicit width
var y float32 = 3.14      // explicit width
var s string = "hello"    // unnecessary but explicit

// Also valid:
x := int8(42)
y := float32(3.14)

The := syntax is only available inside functions. Package-level declarations always use var or const.

go
var version = "1.0"   // package level — OK
// version := "1.0"   // compile error: non-declaration statement outside function body

Multiple variables in one line

go
a, b := 1, "two"           // a is int, b is string
var x, y int = 1, 2         // both int

The := operator can redeclare a variable as long as at least one variable on the left is new:

go
x, err := doSomething()     // x and err are new
y, err := doSomethingElse() // err is reused, y is new — OK

Blank identifier

The blank identifier _ discards values you do not need:

go
_, err := doSomething()     // ignore the first return value

const and iota

Constants in Go are compile-time values — they must be known at compile time, and they can only hold strings, booleans, or numbers.

go
const Pi = 3.14159
const Greeting = "Hello"
const Debug = true

Constants are untyped by default. They get a type only when used in a typed context:

go
const Pi = 3.14159
var x float64 = Pi         // Pi takes type float64 here
var y float32 = float32(Pi) // explicit conversion needed: Pi is not float32

Iota

iota is a predeclared identifier that represents successive integer constants within a const block. It resets to 0 for each new const block and increments by 1 for each line.

go
const (
    StatusOK = iota  // 0
    StatusNotFound   // 1
    StatusError      // 2
)

Bitmask flags with iota

go
const (
    Read  = 1 << iota  // 1
    Write              // 2
    Execute            // 4
)

Skip a value with _

go
const (
    A = iota  // 0
    _         // 1 (discarded)
    B         // 2
)

String mapping pattern

The standard pattern for iota-based enums with string representation is a String() method:

go
type Weekday int

const (
    Sunday Weekday = iota
    Monday
    Tuesday
    Wednesday
    Thursday
    Friday
    Saturday
)

func (d Weekday) String() string {
    return [...]string{
        "Sunday", "Monday", "Tuesday", "Wednesday",
        "Thursday", "Friday", "Saturday",
    }[d]
}

Type conversion

Go requires explicit conversions between different types. There is no implicit numeric promotion — even converting int to int64 requires a cast.

go
var i int = 42
var f float64 = float64(i)  // explicit, not (float64)i like C
var u uint = uint(f)        // explicit

// Compile errors — no implicit conversion:
// var f float64 = i       // cannot use i (type int) as float64
// var i2 int32 = i        // cannot use i (type int) as int32

This explicitness makes it easy to see where precision loss or overflow might occur:

go
var big int64 = 1 << 40
var small int32 = int32(big)  // truncation — compiler lets you, but you see it

For string conversions:

go
s := string(65)           // "A" — rune to string, not integer to string
t := strconv.Itoa(65)     // "65" — integer to string (stdlib)
n, _ := strconv.Atoi("42") // 42 — string to integer

Type assertions

Type assertions extract the concrete value from an interface. They are covered in depth on the Interfaces page, but the basic syntax is worth noting:

go
var x interface{} = "hello"
s := x.(string)                                 // panics if wrong type
s, ok := x.(string)                             // safe — ok is false if wrong type

The two-value form with ok is the idiomatic approach. It avoids panics and handles the failure case cleanly.

Type aliases (type)

Go allows defining new named types from existing ones. A type alias creates a distinct type — not a synonym. Values of the new type cannot be used where the base type is expected without explicit conversion.

go
type MyInt int        // MyInt is a distinct type from int
type UserID int64     // UserID is a distinct type from int64

var a MyInt = 10
var b int = 20

// b = a         // compile error: cannot use a (type MyInt) as type int
b = int(a)        // explicit conversion required

This is one of Go's most powerful features for domain modelling. By creating named types, you make illegal states unrepresentable:

go
type Celsius float64
type Fahrenheit float64

func BoilingPoint() Celsius { return 100.0 }
func Forecast() Fahrenheit  { return 212.0 }

// A Celsius value cannot accidentaly be passed where Fahrenheit is expected
// without explicit conversion.

Type alias (Go 1.9+)

A separate feature, the type alias (using =), creates a true synonym — both types are interchangeable:

go
type MyString = string    // MyString is exactly string, not a new type
var s MyString = "hello"
var t string = s          // OK — they are the same type

Type aliases are primarily used for gradual code refactoring. For most purposes, use distinct types (without =).

Composite types

Go provides several composite types built from its primitives.

Arrays (fixed-size)

go
var arr [3]int            // [0 0 0]
arr2 := [3]int{1, 2, 3}   // [1 2 3]
arr3 := [...]int{1, 2, 3} // compiler counts: [3]int

Arrays are values — assigning an array copies every element. Use slices for references.

Slices (dynamic arrays)

go
var s []int               // nil slice, len 0
s = make([]int, 3, 5)     // len 3, cap 5 — [0 0 0]
s2 := []int{1, 2, 3}      // len 3, cap 3
s3 := append(s2, 4)       // [1 2 3 4]

Slices are the idiomatic Go list type. Every function that takes a list should take a slice.

Maps

go
var m map[string]int       // nil map — cannot write to it
m = make(map[string]int)
m["key"] = 42

m2 := map[string]int{
    "a": 1,
    "b": 2,
}

v, ok := m2["c"]          // 0, false — safe read
delete(m2, "a")           // delete key

Structs

Structs are covered in depth on Structs and methods.

The Intermediate tier uses int, string, bool, and time.Time in its Item struct definition. The Store interface returns slices ([]Item). The configuration is a struct loaded from environment variables. Every one of these types is built on the foundation described here — zero values ensure Item fields are always defined, explicit conversions prevent accidental type confusion in config parsing, and distinct struct types keep domain boundaries clear.


Next: 03 Control flow and functions