Course outline

Go / Basic: Values and Program Contracts / Lesson 2

Types, Zero Values, and Conversions

Choose representations deliberately and explain what a variable contains before an explicit initializer is supplied.

About 5 min readContent reviewed
On this page

Lesson goals

  • Identify numeric, Boolean, string, and reference-bearing zero values.
  • Distinguish named types from aliases and explicit conversions.
  • Separate byte length, runes, and displayed text.

A declared variable starts with a defined value

Without an explicit initializer, Go gives a variable its typeโ€™s zero value: numeric zero, false, an empty string, or nil for pointers, slices, maps, channels, functions, and interfaces. Arrays and structs recursively contain zero-valued elements or fields. This makes initial state predictable, but does not mean every zero value supports every operation.

A nil slice can be appended to, while assigning an entry in a nil map panics. A nil pointer cannot be dereferenced. Distinguish a useful empty state from a fully initialized resource. A good API either makes its zero value useful or clearly documents the initialization operation required before use.

Inspect zero values without dereferencing nil

The first example prints values and nil comparisons without attempting an invalid operation. %q quotes the empty string, making it visible rather than looking like missing output. The nil slice has length zero even though no backing array was created explicitly. Its nilness and length answer different questions.

Do not use a printed representation as a type-independent equality test. A nil slice and an allocated empty slice can both have length zero, yet differ in nil comparison and some encoding behavior. Decide which distinction belongs to the application contract rather than assuming empty and absent always mean the same thing.

go Observe several zero values
package main

import "fmt"

func main() {
	var count int
	var ready bool
	var label string
	var values []int
	var pointer *int
	fmt.Printf("%d %t %q %d %t %t\n", count, ready, label, len(values), values == nil, pointer == nil)
}
Expected output
0 false "" 0 true true

The program checks nil values safely and makes the empty string explicit with quotes.

Representation determines arithmetic

int has an implementation-dependent size, typically 32 or 64 bits; use explicit-width types when an interface genuinely requires their range. Go does not implicitly mix distinct numeric variable types. A conversion requests a specific representation change and can lose information. Validate application bounds before narrowing external data.

Integer division discards the fractional part toward zero. Converting the quotient afterward does not recover it; convert an operand before division when floating arithmetic is intended. Floating-point calculations are approximate, so formatting two decimals is a display policy rather than proof that every decimal fraction has an exact binary representation.

Convert at the operation that needs it

The next program computes two shares from the same small integer values. The first division produces three before conversion. The second converts total to float64 and therefore divides in the floating domain. Both results are defined independently of int width because the values and intermediate results are small.

Defined types can give domain names to a representation, such as type Celsius float64. Two defined types remain distinct even with the same underlying type, helping prevent accidental mixing. A type alias instead names an existing identical type. Neither a name nor a conversion automatically enforces physical units or business limits.

go Change the arithmetic domain before division
package main

import "fmt"

func main() {
	total, people := 7, 2
	truncated := float64(total / people)
	share := float64(total) / float64(people)
	fmt.Printf("%.2f %.2f\n", truncated, share)
}
Expected output
3.00 3.50

The first result has already lost its fraction; the second keeps one half.

A string is an immutable byte sequence

A Go string is an immutable sequence of bytes; it may contain arbitrary bytes, not only valid UTF-8. len reports bytes. A rune is an alias for int32 and conventionally represents a Unicode code point. Ranging over a string decodes UTF-8 and produces byte indices and runes; malformed sequences require their documented decoding interpretation.

Visible characters can consist of multiple code points, so rune count is not always user-perceived character count. Choose byte processing for protocols and Unicode-aware operations for text requirements. Strings being immutable does not make a shared variable holding a string safe for unsynchronized concurrent assignment; that is a separate synchronization problem.

Common errors and fixes

A floating variable contains a truncated quotient.

Why it happens
Integer division happened before conversion.
How to fix it
Convert an operand before division.
Prevent it next time
Annotate intermediate expression types when reviewing arithmetic.

Writing into an initially nil map panics.

Why it happens
A zero-valued map has no writable initialized map storage.
How to fix it
Initialize it with make or a map literal.
Prevent it next time
Document which zero values are directly usable.

A text limit rejects or truncates unexpected characters.

Why it happens
Byte count was confused with code-point or display width.
How to fix it
Choose the intended text unit and a matching operation.
Prevent it next time
Test non-ASCII and combining-character input.

Check your understanding

1. Explain the difference between var s []int and s := make([]int, 0), including which empty-sequence operations are safe.

Show worked solution

Both have length zero, and ranging over either executes no iterations. The first compares equal to nil; the second does not. append can grow either and its returned slice must be kept. Index zero is invalid for both because length, not nilness or capacity, controls indexing. An API should state whether nilness carries an absence meaning.

2. A protocol accepts a decimal count only from zero through one hundred. Why should parsing and conversion to a smaller type be separate steps?

Show worked solution

Parse with an operation that reports syntax and range errors, then enforce the application interval before narrowing. A conversion alone is not a rejection check and can lose high bits or change representation. Keep the previous valid state until every check succeeds. Tests should include negative input, 100, 101, and a value beyond the parserโ€™s representable range.

Key takeaways

  • Zero values are defined, but operations still have preconditions.
  • Conversions change representations at a specific point.
  • Byte length, code points, and visible text are distinct units.

Official references