Go Variables, Types and Zero Values

AI-assistedDrafted with AI assistance and reviewed before publishing; examples are version-pinned. Verify against official docs when in doubt.

Go 1.24

Go is statically typed and has no implicit conversion: int and int64 are different types and you must convert explicitly. That one rule removes a whole category of bugs, and it also means you have to pick the right type up front.

var versus :=

var count int              // declared, zero value 0
var name string = "ann"    // declared and initialised
count = 1                  // assignment

age := 30                  // short declaration, functions only

:= means “declare and assign”, with the type inferred from the right side. Three rules:

  1. Function scope only. At package level you must use var or const.
  2. At least one variable on the left must be newly declared, otherwise it is a compile error.
  3. The inferred type is fixed. age = "30" does not compile.
x := 1
x := 2      // compile error: no new variables on left side of :=
x = 2       // fine

The second rule is what makes n, err := f() work at the start of a function and n, err = f() work later in the same scope — the second call does not introduce a new variable.

Zero values: there is no “uninitialised”

Every variable has a defined initial value. There is no undefined state to reason about:

Type Zero value
numeric (int, float64, …) 0
string ""
bool false
pointer, slice, map, channel, interface, func nil
struct each field set to its zero value
var s string
var n int
var p *int
var m map[string]int

fmt.Println(s == "", n, p == nil, m == nil)   // true 0 true true

Usable zero values are a design goal, not an accident. This struct works without a constructor:

type Counter struct {
	total int
}

var c Counter       // ready to use
c.total++

The exceptions to remember: a nil map is readable but not writable, while a nil slice accepts append directly. Both are covered in the slices and maps chapters.

Basic types

var (
	i   int     = 42
	i8  int8    = 127      // -128 .. 127
	i64 int64   = 1 << 40
	f   float64 = 3.14
	b   bool    = true
	s   string  = "hello"
	by  byte    = 'a'      // byte is an alias for uint8
	r   rune    = 'é'      // rune is an alias for int32: one Unicode code point
)

int is platform-sized: 64 bits on any machine you are likely to deploy to, but not guaranteed. Use fixed-width types (int64, uint32) for anything serialised, hashed or compared across platforms, otherwise results can differ between architectures.

byte and rune are aliases, not distinct types, so []byte and []uint8 are the same type. Prefer the alias that communicates intent.

Constants and iota

const Pi = 3.14159
const AppName = "hello"

const (
	StatusPending = iota   // 0
	StatusRunning          // 1
	StatusDone             // 2
)

iota restarts at 0 in each const block and increments per line. Skip a value with _:

const (
	_  = iota                // discard 0
	KB = 1 << (10 * iota)    // 1 << 10 = 1024
	MB                       // 1 << 20
	GB                       // 1 << 30
)

Constants are untyped unless you say otherwise, so a single definition adapts to context:

const Max = 100
var a int = Max        // ok
var b int64 = Max      // also ok

That flexibility disappears the moment you write const Max int64 = 100 — now var a int = Max fails. Keep constants untyped unless you need the constraint.

Conversion is always explicit

var i int = 42
var f float64 = float64(i)
var u uint = uint(i)

fmt.Println(int(3.9))   // 3 — truncation, not rounding

Strings and numbers need the standard library; a cast will not do:

import "strconv"

s := strconv.Itoa(42)          // "42"
n, err := strconv.Atoi("42")   // 42, nil
f, _ := strconv.ParseFloat("3.14", 64)

Remember that string(65) is not "65" — it converts the code point 65 to the character "A":

fmt.Println(string(rune(65)))   // A
fmt.Println(strconv.Itoa(65))   // 65

Since Go 1.15, go vet flags string(int) conversions for exactly this reason.

string, byte and rune

A Go string is an immutable byte sequence, conventionally UTF-8:

s := "héllo"
fmt.Println(len(s))                      // 6 — bytes, not characters
fmt.Println(utf8.RuneCountInString(s))   // 5 — characters

for i, r := range s {
	fmt.Println(i, r)   // 0 104 / 1 233 / 3 108 ...
}

range over a string decodes UTF-8 for you: i is the byte offset of the character, r is the rune. That is why the index jumps from 1 to 3 after a two-byte character, and why indexing s[1] gives you the first byte of a character rather than the character.

Work with runes when you mean characters:

chars := []rune("你好")
fmt.Println(len(chars))        // 2
fmt.Println(string(chars[0]))  // 你

Converting between string and []byte copies:

b := []byte("hello")
s := string(b)

For concatenation, + is fine for a handful of pieces. Inside a loop use strings.Builder, which grows a single buffer instead of allocating a new string per iteration:

var sb strings.Builder
for _, w := range []string{"a", "b", "c"} {
	sb.WriteString(w)
}
fmt.Println(sb.String())   // abc

Common mistakes

1. Mixing int and int64

mismatched types int and int64. Convert explicitly; do not paper over it with int(...) everywhere.

2. Using string(n) to format a number

You get a code point. Use strconv.Itoa or fmt.Sprintf.

3. Treating len(s) as a character count

It is a byte count. Use utf8.RuneCountInString or len([]rune(s)) when characters are what you mean.

4. Redeclaring with := in the same scope

Compile error. Use = for assignment.

5. Building a string with + inside a loop

Quadratic allocations. Use strings.Builder.

6. Reading the zero value as “unset”

0 from a config parser is indistinguishable from a deliberate 0. Use a pointer or a separate set flag when that matters.

Summary

  • := inside functions, var / const at package level; := needs at least one new variable.
  • Every value has a zero value, so structs are usually usable as-is; nil maps are read-only.
  • Use fixed-width types across platforms and serialisation boundaries; convert explicitly.
  • strconv for number/string conversion; const plus iota for enumerations.
  • Strings are bytes: range or []rune for characters, strings.Builder for accumulation.

Next: control flow — one loop keyword, no parentheses, and a switch that breaks by default.