Polyglot
Languages Go types
Go § types

Types

Go is statically typed with type inference at the binding site (x := 5 infers int). The type system divides into basic types (numerics, bool, string), composite types (array, slice, map, struct, channel), named types (declared with type), and interface types. Type conversions are always explicit; Go does not perform implicit numeric conversions, even between integer widths. The combination — small set of basic types, value semantics with explicit pointers, structural interfaces, garbage-collected heap allocation — is the substance of Go’s type model.

Basic types

// Integers (signed):
int8        // -128 to 127
int16       // -32768 to 32767
int32       // -2_147_483_648 to 2_147_483_647 (alias: rune)
int64       // -9_223_372_036_854_775_808 to ...
int         // platform-dependent: 32 or 64 bits

// Integers (unsigned):
uint8       // 0 to 255 (alias: byte)
uint16
uint32
uint64
uint        // platform-dependent
uintptr     // unsigned integer wide enough to hold any pointer

// Floating point:
float32
float64     // the conventional default

// Complex:
complex64
complex128

// Boolean:
bool        // true or false

// String:
string      // immutable byte sequence (treated as UTF-8 by convention)

Two type aliases:

  • byte = uint8 — used for raw byte data.
  • rune = int32 — used for Unicode code points.

The conventional defaults:

  • int — for general integer arithmetic.
  • int64 — for explicit 64-bit needs.
  • float64 — for floating point (the default; float32 is rare).
  • string — for text.

Numeric literals

42                                               // decimal
0o755                                            // octal (Go 1.13+)
0755                                             // legacy octal
0xff                                             // hex
0b1010                                           // binary (Go 1.13+)

3.14                                             // float
6.022e23                                         // scientific
0x1.fp10                                         // hex floating point

42i                                              // imaginary
2 + 3i                                           // complex literal

1_000_000                                        // underscore separators (Go 1.13+)

A literal has an untyped form until used in a context that requires a type:

const Pi = 3.14159                               // untyped float
var x float32 = Pi                               // becomes float32
var y float64 = Pi                               // becomes float64

The mechanism admits substantial flexibility for constants without explicit type annotations.

String literals

Two forms:

"hello"                                          // interpreted; admits escapes
"line one\nline two"                             // \n, \t, \", \\

`raw string`                                     // raw; no escapes
`first line
second line`                                     // multi-line
`quotes are "literal" in raw strings`
`backslashes \ are literal too`

The raw-string form is conventional for regular expressions, multi-line code samples, and JSON literals.

A string is immutable and byte-indexed:

s := "héllo"                                     // 6 bytes (é is 2 bytes in UTF-8)
fmt.Println(len(s))                              // 6
fmt.Println(s[0])                                // 104 (the byte value of 'h')

For Unicode-aware iteration, for i, r := range s produces the rune sequence:

for i, r := range s {
    fmt.Printf("byte %d: rune %c\n", i, r)
}

Treated in Strings.

Type conversion

Conversions are always explicit:

x := 10                                          // int
var y float64 = float64(x)                       // explicit conversion
var z int32 = int32(x)                           // explicit conversion

// String / byte slice / rune slice conversions:
s := "hello"
b := []byte(s)                                   // string → []byte
r := []rune(s)                                   // string → []rune
s2 := string(b)                                  // []byte → string

// Integer to string is NOT a numeric conversion:
n := 65
s := string(n)                                   // "A" (the rune for 65); often surprising

// For "65" → "65", use strconv.Itoa or fmt.Sprintf:
s := strconv.Itoa(n)                             // "65"
s := fmt.Sprintf("%d", n)                         // "65"

Implicit conversions are not admitted, even between compatible types:

var a int = 5
var b int64 = a                                  // ERROR: cannot use int as int64
var b int64 = int64(a)                           // OK

The strictness eliminates substantial classes of bugs that C-style conversions admit; the conventional response is more explicit, more verbose code.

Composite types

Four principal composite types:

Arrays

Fixed-size; the length is part of the type:

var arr [5]int                                   // [0 0 0 0 0]
arr := [5]int{1, 2, 3, 4, 5}
arr := [...]int{1, 2, 3}                         // length inferred (3)

fmt.Println(len(arr))                            // 5
fmt.Println(arr[2])                              // index access

// Two arrays of different sizes have different types:
var a [3]int
var b [4]int
// a = b                                         // ERROR: type mismatch

Arrays are value types — assignment and parameter passing copy the entire array. For dynamic-sized data, slices are the conventional choice.

Slices

Dynamic-length, backed by an underlying array:

var s []int                                      // nil slice; len 0, cap 0
s := []int{1, 2, 3}                              // literal
s := make([]int, 5)                              // [0 0 0 0 0]; length 5, cap 5
s := make([]int, 5, 10)                          // length 5, cap 10

fmt.Println(len(s))                              // 5
fmt.Println(cap(s))                              // 10

s = append(s, 6)                                 // grows the slice

// Slicing:
s2 := s[1:3]                                     // elements 1, 2 (exclusive of 3)
s3 := s[:3]                                      // first 3 elements
s4 := s[2:]                                      // from index 2 to end

Slices are reference-like — multiple slices may share the same underlying array; modifying through one affects the others. Treated in Data structures.

Maps

Key-value associative containers:

var m map[string]int                             // nil map; cannot write to
m := make(map[string]int)                        // empty, ready
m := map[string]int{
    "alice": 30,
    "bob":   25,
}

m["charlie"] = 40                                // insert/update
v := m["alice"]                                  // get; returns zero value if missing
v, ok := m["dave"]                               // two-value form; ok is false if missing
delete(m, "alice")                               // remove
fmt.Println(len(m))

for k, v := range m {                            // iteration (order is randomised)
    fmt.Println(k, v)
}

The conventional defence against missing keys is the two-value form v, ok := m[key].

Structs

Aggregate types with named fields:

type Point struct {
    X, Y float64
}

p := Point{X: 1.0, Y: 2.0}
p := Point{1.0, 2.0}                             // positional; less robust

fmt.Println(p.X, p.Y)
p.X = 10.0                                       // mutate field

// Anonymous struct:
config := struct {
    Host string
    Port int
}{
    Host: "localhost",
    Port: 8080,
}

Structs are value types — assignment copies all fields. Treated in Data structures.

Named types

The type keyword introduces a new type:

type UserID int                                  // distinct from int
type Celsius float64
type Inches float64

func freezing() Celsius {
    return 0.0
}

c := Celsius(20)
i := Inches(50)
// c = i                                         // ERROR: type mismatch
c = Celsius(i)                                   // OK; explicit conversion

A named type is distinct from its underlying type; the compiler enforces the distinction. The mechanism admits substantial type safety for semantically distinct values.

Type aliases

The type X = Y form (since Go 1.9) admits an alias — the same type under a new name:

type ByteSlice = []byte                          // alias; same as []byte
type Reader = io.Reader                          // alias; same interface

Aliases are conventionally used during code refactoring; for substantial type safety, the type X Y form (without =) is the conventional choice.

Pointers

Pointers admit referring to a value’s memory location:

x := 10
p := &x                                          // pointer to x
fmt.Println(*p)                                  // 10 (dereference)
*p = 20                                          // modify through the pointer
fmt.Println(x)                                   // 20

// nil pointers:
var q *int                                       // q == nil
// *q                                            // PANIC: nil dereference

Go does not admit pointer arithmetic; pointers may be assigned and dereferenced but not incremented or decremented:

p := &x
// p++                                           // ERROR: cannot increment pointer

The mechanism eliminates a substantial class of memory-safety bugs that C admits. Treated in Pointers and references.

Interface types

An interface specifies a set of method signatures:

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Stringer interface {
    String() string
}

A type implements an interface implicitly — by having all the required methods. There is no implements keyword:

type Cat struct{ name string }

func (c Cat) String() string {
    return "Cat: " + c.name
}

var s Stringer = Cat{name: "Whiskers"}            // Cat implements Stringer

The interface mechanism is structural: any type with the required methods satisfies the interface. Treated in Methods and interfaces.

The empty interface interface{} (or any since Go 1.18) admits any type:

var x interface{} = 42
var y any = "hello"                              // any is an alias for interface{}

Channel types

Channels admit communication between goroutines:

ch := make(chan int)                             // bidirectional channel
ch := make(chan int, 10)                         // buffered, capacity 10

ch <- 5                                          // send
v := <-ch                                        // receive

var sender chan<- int = ch                       // send-only
var receiver <-chan int = ch                     // receive-only

Treated in Concurrency.

Function types

Functions are first-class values; their type is func(...) ...:

var f func(int) int = func(x int) int { return x * 2 }

type IntOp func(int) int                         // named function type

func compose(f, g IntOp) IntOp {
    return func(x int) int {
        return g(f(x))
    }
}

Treated in Functions.

Constants

The const keyword admits compile-time constants:

const Pi = 3.14159
const MaxRetries = 3
const Greeting = "hello"

const (                                           // grouped constants
    Sunday = iota                                 // 0
    Monday                                        // 1
    Tuesday                                       // 2
    Wednesday
    Thursday
    Friday
    Saturday
)

The iota is a special compile-time identifier — it starts at 0 in each const block and increments with each ConstSpec.

const (
    KB = 1 << (10 * (iota + 1))                  // 1 << 10
    MB                                            // 1 << 20
    GB                                            // 1 << 30
    TB                                            // 1 << 40
)

Constants admit substantial compile-time computation but only on basic types and other constants.

The zero value

Every type has a zero value — the value of an uninitialised variable:

TypeZero value
Numeric0
Booleanfalse
String""
Pointer, slice, map, channel, func, interfacenil
StructAll-fields-zero
ArrayAll-elements-zero

The mechanism eliminates the C-style “uninitialised variable” pitfall:

var s []int                                      // nil slice; len(s) == 0
var m map[string]int                             // nil map; reading is OK, writing panics
var p *int                                       // nil pointer

if s == nil {
    // handle nil slice
}

// nil slices are USABLE:
s = append(s, 1)                                 // OK; produces a slice of length 1
fmt.Println(len(s))                              // OK; 1

The zero-value design admits many “default” behaviours without explicit initialisation.

Type assertions

A type assertion extracts a concrete type from an interface:

var i interface{} = "hello"

s := i.(string)                                  // s = "hello"; panics if i is not a string

s, ok := i.(string)                              // two-value form; ok is false on mismatch
if ok {
    fmt.Println(s)
}

For multi-type discrimination, type switches are conventional:

switch v := i.(type) {
case string:
    fmt.Println("string:", v)
case int:
    fmt.Println("int:", v)
case []byte:
    fmt.Println("bytes:", v)
default:
    fmt.Println("unknown:", v)
}

Treated in Switch and pattern dispatch.

Generics (Go 1.18+)

Type parameters admit generic functions and types:

func Max[T constraints.Ordered](a, b T) T {
    if a > b {
        return a
    }
    return b
}

type Stack[T any] struct {
    items []T
}

x := Max(3, 5)                                   // T inferred as int
y := Max[float64](3.0, 5.0)                      // explicit

Treated in Generics.

A note on the conventional discipline

The contemporary Go type advice:

  • Use int, float64, string, bool as the conventional defaults.
  • Use named types for semantically distinct values (UserID int).
  • Use slices, not arrays for dynamic data.
  • Use maps for key-value lookup.
  • Use struct types for grouping related data.
  • Trust the zero value — many types are usable without initialisation.
  • Use explicit conversions — Go is strict.
  • Use the two-value form (v, ok := m[k]) for safe map access.

The combination — small set of basic types, value semantics, explicit conversions, structural interfaces, zero-value initialisation, garbage-collected composite types — is the substance of Go’s type system. The discipline produces clear, explicit code with substantial compile-time guarantees.