Go Basics #2 Variables, Types, Constants

3 min read

In #1 Getting started and your first program you set up the environment — now it’s time for the language itself. Go’s basic types, ways to declare variables, and constants.

Basic types #

Go is a statically typed language. The type of every variable is fixed at compile time.

KindType
Integerint, int8, int16, int32, int64, uint, uint8 (= byte), …
Floating pointfloat32, float64
Complexcomplex64, complex128
Booleanbool
Stringstring
Rune (Unicode code point)rune (= int32)

The ones you’ll use most often are int, string, bool, and float64. Use the fixed-width types (int32, int64, etc.) only when you need an exact size.

The size of int #

int is either 32-bit or 64-bit depending on the platform. On a 64-bit OS it’s typically the same as int64. When the exact size matters, spell out int32 / int64.

Variable declarations — two ways #

1) var — explicit declaration #

var
var name string = "Curtis"
var age int = 30
var ok bool = true

The most explicit form. You write the type.

1.5) var — type inference #

var + inference
var name = "Curtis"   // inferred as string
var age = 30          // inferred as int

If the type can be inferred from the initial value, you can omit the type annotation.

2) := — short declaration #

The most common form inside functions.

:=
name := "Curtis"
age := 30
ok := true

Almost the same meaning as var x = ..., but it can only be used inside functions. Inside functions, := is almost always the standard convention for new variables.

:= doesn’t work outside functions #

Outside functions
package main

var globalName = "Curtis"   // OK
// globalName2 := "other"      ✗ := doesn't work outside functions

func main() {
	localName := "local"        // OK
}

Multiple variables at once #

Multiple variables
var a, b, c int = 1, 2, 3
var x, y = 1, "hi"   // mixed types OK

i, j := 10, 20
i, j = j, i           // swap

Patterns like swap feel natural with :=.

var block #

var block
var (
	name string = "Curtis"
	age  int    = 30
	ok   bool   = true
)

For grouping multiple var declarations together. You’ll often see this shape with package-level variables.

Zero values #

If you declare a variable without an initial value, Go initializes it with the zero value.

zero values
var i int        // 0
var f float64    // 0.0
var b bool       // false
var s string     // "" (empty string)
var p *int       // nil (zero value of a pointer)

A key difference from null/undefined in other languages — in Go every variable always holds a meaningful value. There’s no “declared but not initialized” state.

Type conversion — explicit #

Go almost never converts types automatically. You need an explicit conversion between different types.

Explicit conversion
i := 42
f := float64(i)      // int → float64
ui := uint(f)         // float64 → uint

// i + f          ✗ mismatched types
i + int(f)        // OK

It feels restrictive at first, but it’s a design choice that prevents accidental loss of precision.

String ↔ number #

For these you have dedicated conversion functions.

Strings and numbers
import "strconv"

s := strconv.Itoa(42)              // int → string ("42")
n, err := strconv.Atoi("42")       // string → int (can return an error)

f, err := strconv.ParseFloat("3.14", 64)

We cover error returns in detail in #4 Functions, multiple return, error type.

Constants — const #

If a value is fixed at compile time and never changes, use const.

const basics
const Pi = 3.14159
const MaxRetries = 3
const AppName = "MyApp"

Similar to var, but — since they’re compile-time constants — they don’t change at runtime, and values like the result of a function call can’t be assigned to a const.

const limitations
const Now = time.Now()   // ✗ time.Now() is a runtime call

Typed const vs untyped const #

typed vs untyped const
const Pi = 3.14159              // untyped — type is determined by context
const PiTyped float32 = 3.14159 // typed — fixed to float32

var f float64 = Pi        // OK — Pi is untyped
var f32 float32 = Pi      // OK — Pi is untyped
var f32 float32 = PiTyped // OK
var f64 float64 = PiTyped // ✗ float32 → float64 needs explicit conversion

Untyped consts are more flexible. Unless you have a specific reason, leaving consts untyped is the norm.

iota — auto-incrementing const #

Inside a const group, you can produce values that auto-increment. Similar in spirit to enums in other languages.

iota basics
const (
	Red   = iota   // 0
	Green          // 1
	Blue           // 2
)

iota starts at 0 within a const block and increments by 1 with each line. Automatically, without you having to repeat the same expression.

iota patterns #

Various iota patterns
// skipping values
const (
	A = iota    // 0
	B           // 1
	C           // 2
	_           // 3 (skipped)
	E           // 4
)

// bit flags
const (
	Read    = 1 << iota   // 1 (1 << 0)
	Write                  // 2 (1 << 1)
	Execute                // 4 (1 << 2)
)

// units
const (
	_        = iota          // ignore (iota = 0)
	KB       = 1 << (10 * iota)   // 1024
	MB                              // 1024 * 1024
	GB                              // 1024^3
)

These patterns show up often in library code. Bit flags in particular are very common.

Named types — making types meaningful #

Instead of using a primitive type as is, you can create a type with its own name.

named type
type UserID string
type Email string

func sendMessage(id UserID, to Email) {
	// ...
}

var u UserID = "u1"
var e Email = "me@example.com"

sendMessage(u, e)        // OK
// sendMessage(e, u)      ✗ UserID and Email are different types

Both are strings, but UserID and Email are not interchangeable. By baking the meaning into the type, you can prevent incorrect calls at compile time. Similar in effect to TypeScript’s branded types (TS Advanced #5).

Basic output / input — fmt #

The fmt functions you’ll use most.

Common fmt functions
fmt.Println("automatic newline at the end of the line")
fmt.Print("no newline at the end")

fmt.Printf("Name: %s, Age: %d\n", name, age)

s := fmt.Sprintf("Result: %d", 42)   // build a string

Common format verbs:

verbmeaning
%vdefault representation (any type)
%+vstruct including field names
%ddecimal integer
%sstring
%qquoted string
%tbool
%ffloating point
%Ttype name
Format example
fmt.Printf("%d %s %t\n", 42, "hi", true)        // 42 hi true
fmt.Printf("%v %T\n", 3.14, 3.14)                // 3.14 float64
fmt.Printf("%q\n", "hello")                       // "hello"

Two faces of strings #

Strings in Go are an immutable byte sequence. Indexing returns a byte.

byte vs rune
s := "안녕"
fmt.Println(len(s))      // 6 — number of UTF-8 bytes
fmt.Println(s[0])        // 236 — first byte (a number)

// Iterate by character (rune)
for i, r := range s {
	fmt.Printf("%d %c\n", i, r)
}
// 0 안
// 3 녕

for range iterates rune by rune (Unicode code points). When dealing with Korean or emoji, work in runes rather than byte indices.

Common pitfalls #

1) Mixing up := and = #

New variable vs reassignment
x := 10        // new variable (declare + assign)
x = 20         // reassign
y := 30        // new variable
// y := 40      ✗ y already declared (but with multiple targets, it's OK if at least one is new)

In a multi-target statement, as long as at least one variable is new, := works.

Multi-target := behavior
x := 10
x, y := 20, 30   // x reassigned, y is new — OK

2) Integer division truncates #

int / int = int
result := 10 / 3        // 3 (decimal truncated)
result2 := 10.0 / 3.0   // 3.333...
result3 := float64(10) / 3   // 3.333...

There’s no automatic conversion as in some other languages. If you want a floating-point result, convert explicitly.

Wrapping up #

What this post covered:

  • Static typing — every variable’s type is fixed at compile time
  • Two declaration styles: var and :=
  • Inside functions, := is almost the standard
  • Zero values — every variable gets a meaningful initial value automatically
  • Almost no implicit conversion — explicit conversion required
  • Use strconv for string ↔ number
  • const and untyped vs typed consts
  • iota for auto-incrementing values, bit flags, and unit definitions
  • Use named types to make types meaningful
  • Strings are byte sequences; iterate runes with for range

In the next post (#3 Control flow) we cover if/for/switch and the modern patterns inside them. Go has no while; for handles all looping.

X