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;float32is 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:
| Type | Zero value |
|---|---|
| Numeric | 0 |
| Boolean | false |
| String | "" |
| Pointer, slice, map, channel, func, interface | nil |
| Struct | All-fields-zero |
| Array | All-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,boolas 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.