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.
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.
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)
}
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.
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)
}
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.