Free Handbook · Every example compiled & verified

Fundamentals

Variables with var and :=, zero values, Go's basic types, constants and iota, operators, Printf verbs, reading input and strconv conversions.

0 / 134 lessons🔥 0 day streak
ShareXLinkedIn

Module 01 · what you'll be able to do

  • Declare variables with var and := and know which one to use where
  • Predict the zero value of any basic type instead of worrying about uninitialised memory
  • Build enumerations with const and iota
  • Format output precisely with fmt.Printf verbs like %v, %d, %q, %.2f and %T
  • Read input with bufio.Scanner and fmt.Scan, and convert between types with T(v) and strconv
01

Variables: var vs :=

Go has two ways to declare a variable. The long form, var name type = value, works everywhere and lets you leave out either the type or the value. The short declaration name := value declares and assigns in one step, infers the type from the value, and is only allowed inside functions. Most Go code uses := inside functions and var at package level or when you want the zero value.

gomain.go
package main

import "fmt"

var appName = "inventory" // package level: var only

func main() {
	var count int = 3 // explicit type and value
	var price = 9.99  // type inferred: float64
	var ready bool    // no value: gets the zero value, false

	sku := "A-100" // short declaration, type string
	x, y := 10, 20 // several at once
	x, y = y, x    // swap, no temp variable needed

	fmt.Println(appName, count, price, ready, sku)
	fmt.Println(x, y)
}
Outputcompiled & run with real Go
inventory 3 9.99 false A-100
20 10
Your turn

Declare var discount float64 without a value and print it. What does it print, and why?

FormWhere allowedUse it when
var x intAnywhereYou want the zero value, or you declare now and assign later
var x = 5AnywherePackage-level variables
var x int64 = 5AnywhereThe inferred type would be wrong (5 alone would be int)
x := 5Inside functions onlyThe normal case inside a function
Error you will hit

declared and not used

go
package main

import "fmt"

func main() {
	name := "Gopher"
	age := 12
	fmt.Println(name)
}
# command-line-arguments
./main.go:7:2: declared and not used: age
Why the compiler said that

Like unused imports, an unused local variable is a compile error in Go, not a warning. A variable you assign and never read is usually a bug — a typo, or a result you forgot to check. (Unused package-level variables are allowed.)

The fix

Use the variable, delete it, or — if you must keep the assignment for now — assign it to the blank identifier: _ = age.

go
package main

import "fmt"

func main() {
	name := "Gopher"
	age := 12
	fmt.Println(name, age)
}
Error you will hit

no new variables on left side of :=

go
package main

import "fmt"

func main() {
	x := 1
	x := 2
	fmt.Println(x)
}
# command-line-arguments
./main.go:7:4: no new variables on left side of :=
Why the compiler said that

:= means "declare". x already exists in this scope, so the second line declares nothing. With several names on the left, := is allowed as long as at least one is new — that is why n, err := ... twice in a row works when n is new.

The fix

Use plain assignment = to change an existing variable.

go
package main

import "fmt"

func main() {
	x := 1
	x = 2
	fmt.Println(x)
}
02

Zero values and basic types

Go has no uninitialised variables. Every variable you declare without a value gets its type's zero value: 0 for numbers, "" for strings, false for booleans, and nil for pointers, slices, maps, channels, functions and interfaces. Good Go code leans on this — a zero-valued struct is often ready to use.

gomain.go
package main

import "fmt"

func main() {
	var i int
	var f float64
	var s string
	var b bool
	var p *int
	var r rune

	fmt.Printf("%v %v %q %v %v %v\n", i, f, s, b, p, r)
	fmt.Printf("%T %T %T %T %T %T\n", i, f, s, b, p, r)
}
Outputcompiled & run with real Go
0 0 "" false <nil> 0
int float64 string bool *int int32
Your turn

Add var by byte and print its type with %T. byte is an alias — for which type?

TypeWhat it holdsZero value
int, uintWhole numbers; 64 bits on every modern platform0
int8 … int64, uint8 … uint64Fixed-size integers0
float64, float32Floating point (use float64 by default)0
stringImmutable sequence of bytes, usually UTF-8 text""
booltrue or falsefalse
byteAlias for uint8: one byte0
runeAlias for int32: one Unicode code point0
complex128Complex numbers (rarely used)(0+0i)
No implicit conversions — ever
Go will not mix int and int64, or int and float64, in one expression, even though both are numbers. You convert explicitly (lesson 7). It feels strict for a week, then you stop getting silent overflow bugs.
Error you will hit

mismatched types int and float64

go
package main

import "fmt"

func main() {
	count := 3
	price := 2.5
	total := count * price
	fmt.Println(total)
}
# command-line-arguments
./main.go:8:11: invalid operation: count * price (mismatched types int and float64)
Why the compiler said that

count was inferred as int and price as float64. Go never converts one to the other behind your back, because the compiler cannot know whether you want to lose the fraction or not.

The fix

Convert explicitly to the type you want the result in.

go
package main

import "fmt"

func main() {
	count := 3
	price := 2.5
	total := float64(count) * price
	fmt.Println(total)
}
03

Constants and iota

const declares a value fixed at compile time: numbers, strings, booleans. A constant written without a type is untyped — it has arbitrary precision and takes on whatever type its use needs, which is why const big = 1 << 40 can be used as a float64 or an int64. Inside a const ( ... ) block, iota counts up from 0 on each line, and a line with no expression repeats the previous one. That is how Go writes enums.

gomain.go
package main

import "fmt"

type Weekday int

const (
	Sunday Weekday = iota // 0
	Monday                // 1 (repeats "Weekday = iota")
	Tuesday               // 2
)

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

const Pi = 3.14159 // untyped constant

func main() {
	fmt.Println(Sunday, Monday, Tuesday)
	fmt.Println(KB, MB, GB)
	var r float32 = Pi // fits float32 because Pi is untyped
	fmt.Println(r * 2)
}
Outputcompiled & run with real Go
0 1 2
1024 1048576 1073741824
6.28318
Your turn

Add TB after GB and print it. Then add Wednesday to the weekday block.

VisualizeHow iota fills the KB/MB/GB blockStep 1 / 5
const (
_ = iota
KB = 1 << (10 * iota)
MB
GB
)
Line 1

A new const block starts; iota resets to 0.

Variables now

nothing yet

All 5 steps as a table
StepLineWhat happenedVariables now
11A new const block starts; iota resets to 0.
22First line: iota is 0. Assigned to _, the blank identifier, so it is thrown away.iota = 0
33iota is 1, so KB = 1 << 10.iota = 1 KB = 1024
44No expression: Go repeats 1 << (10 * iota) with iota = 2.iota = 2 MB = 1048576
55Repeated again with iota = 3.iota = 3 GB = 1073741824
Error you will hit

constant overflows the type

go
package main

import "fmt"

func main() {
	var b byte = 300
	fmt.Println(b)
}
# command-line-arguments
./main.go:6:15: cannot use 300 (untyped int constant) as byte value in variable declaration (overflows)
Why the compiler said that

A byte holds 0–255. Because constants are checked at compile time, Go catches the overflow before the program ever runs. (A value computed at runtime would silently wrap around instead — constants are the safe case.)

The fix

Use a type big enough for the value, such as int or uint16.

go
package main

import "fmt"

func main() {
	var b uint16 = 300
	fmt.Println(b)
}
04

Operators

Arithmetic, comparison and logical operators look like C, with two things to watch. Integer division truncates toward zero (7 / 2 is 3) and % keeps the sign of the left side. And ++ / -- are statements, not expressions: i++ works on its own line, but j := i++ does not compile.

gomain.go
package main

import "fmt"

func main() {
	fmt.Println(7/2, 7.0/2, 7%3, -7%3)

	i := 10
	i += 5
	i++
	fmt.Println(i)

	age, member := 20, false
	fmt.Println(age >= 18 && !member, age < 18 || member)

	flags := 0b0101
	fmt.Println(flags&0b0100 != 0, flags|0b0010, flags<<1, flags^0b1111)

	s := "go" + "pher"
	fmt.Println(s, s == "gopher", "apple" < "banana")
}
Outputcompiled & run with real Go
3 3.5 1 -1
16
true false
true 7 10 10
gopher true true
Your turn

Print -7 / 2. Does Go round toward zero or toward negative infinity?

GroupOperatorsNotes
Arithmetic+ - * / %+ also concatenates strings; / truncates for integers
Assignment= := += -= *= /= %=:= declares, = assigns
Incrementi++ i--Statements only; no ++i
Comparison== != < <= > >=Strings compare byte by byte
Logical&& || !Short-circuit: the right side runs only if needed
Bitwise& | ^ &^ << >>&^ is "AND NOT" (bit clear); unary ^x flips all bits
05

Formatting output with fmt.Printf

fmt.Println prints values with spaces between them. fmt.Printf takes a format string with verbs — placeholders starting with % — and does not add a newline, so you end with \n. fmt.Sprintf does the same but returns the string instead of printing it. %v works for any value and is the one you will use most.

gomain.go
package main

import "fmt"

type Point struct {
	X, Y int
}

func main() {
	p := Point{3, 4}
	fmt.Printf("%v | %+v | %#v\n", p, p, p)
	fmt.Printf("%d %5d|%-5d| %05d\n", 42, 42, 42, 42)
	fmt.Printf("%f %.2f %8.3f %e\n", 3.14159, 3.14159, 3.14159, 1234.5)
	fmt.Printf("%s %q %x %c %U\n", "go", "go", 255, 'G', 'G')
	fmt.Printf("%t %T\n", true, 3.0)
	fmt.Printf("%d%%\n", 50)

	label := fmt.Sprintf("%-6s|%6.1f", "total", 99.456)
	fmt.Println(label)
}
Outputcompiled & run with real Go
{3 4} | {X:3 Y:4} | main.Point{X:3, Y:4}
42    42|42   | 00042
3.141590 3.14    3.142 1.234500e+03
go "go" ff G U+0047
true float64
50%
total |  99.5
Your turn

Print a price table row with fmt.Printf("%-10s %8.2f\n", "Coffee", 3.5) and line up three rows.

VerbPrintsExample output
%vAny value, default format{3 4}
%+vStruct with field names{X:3 Y:4}
%#vGo-syntax representationmain.Point{X:3, Y:4}
%TThe typefloat64
%dInteger in base 1042
%5d / %-5d / %05dWidth 5, right / left aligned / zero padded 42
%f / %.2fFloat / float with 2 decimals3.14
%s / %qString / quoted string"go"
%xHexadecimalff
%c / %UCharacter / Unicode code pointG, U+0047
%tBooleantrue
%%A literal percent sign%
Debug with %+v and %T
When a value is not what you expect, fmt.Printf("%+v %T\n", x, x) shows both its contents with field names and its exact type. It answers most "why is this nil / why won't it compile" questions in one line.
06

Reading input: bufio.Scanner and fmt.Scan

For reading a line at a time — the usual case — wrap standard input in a bufio.Scanner. Each call to Scan() reads one line and returns false at end of input; Text() gives the line without its newline. For quick whitespace-separated values, fmt.Scan reads straight into variables through pointers (&x).

gomain.go
package main

import (
	"bufio"
	"fmt"
	"os"
	"strings"
)

func main() {
	scanner := bufio.NewScanner(os.Stdin)
	fmt.Print("Your name: ")
	scanner.Scan()
	name := strings.TrimSpace(scanner.Text())
	fmt.Println("\nHello,", name)

	lines := 0
	for scanner.Scan() { // read the rest until end of input
		lines++
		fmt.Printf("line %d: %s\n", lines, scanner.Text())
	}
}
Outputcompiled & run with real Go
Your name: 
Hello, Ada Lovelace
line 1: first note
line 2: second note

Input fed to the program: Ada Lovelace, first note, second note. In a terminal, end input with Ctrl+D (Ctrl+Z then Enter on Windows).

gomain.go
package main

import "fmt"

func main() {
	var name string
	var a, b int
	n, err := fmt.Scan(&name, &a, &b)
	if err != nil {
		fmt.Println("bad input:", err)
		return
	}
	fmt.Println("read", n, "values")
	fmt.Printf("%s: %d + %d = %d\n", name, a, b, a+b)
}
Outputcompiled & run with real Go
read 3 values
sum: 17 + 25 = 42
Your turn

Feed it sum 17 abc. fmt.Scan returns an error instead of crashing — what does it say?

fmt.Scan stops at spaces
fmt.Scan(&name) reads one whitespace-separated word, so "Ada Lovelace" becomes just "Ada". Use bufio.Scanner when you want whole lines.
07

Type conversion and strconv

Converting between numeric types uses the type name like a function: float64(n), int(f). Converting a float to an int truncates (drops the fraction). Converting between numbers and text is different — that is what the strconv package is for. strconv.Itoa turns an int into its digits, and strconv.Atoi parses digits back, returning an error if the text is not a number.

gomain.go
package main

import (
	"fmt"
	"strconv"
)

func main() {
	f := 9.87
	n := int(f) // truncates
	fmt.Println(n, float64(n)/2)

	s := strconv.Itoa(42)
	fmt.Println(s + "!")

	v, err := strconv.Atoi("123")
	fmt.Println(v+1, err)

	_, err = strconv.Atoi("12a")
	fmt.Println(err)

	price, _ := strconv.ParseFloat("19.99", 64)
	ok, _ := strconv.ParseBool("true")
	fmt.Println(price*2, ok)
	fmt.Println(strconv.FormatInt(255, 2), strconv.Quote("hi"))

	fmt.Println(string(rune(65)), strconv.Itoa(65)) // not the same thing!
}
Outputcompiled & run with real Go
9 4.5
42!
124 <nil>
strconv.Atoi: parsing "12a": invalid syntax
39.98 true
11111111 "hi"
A 65
Your turn

Parse "3.5e2" with strconv.ParseFloat and print the result.

string(65) is "A", not "65"
Converting an integer to string produces the character with that code point, not its digits. go vet warns about string(intValue) for exactly this reason. For digits, always use strconv.Itoa or fmt.Sprint.
Error you will hit

Adding a string and an int

go
package main

import "fmt"

func main() {
	n := 42
	s := "Answer: " + n
	fmt.Println(s)
}
# command-line-arguments
./main.go:7:7: invalid operation: "Answer: " + n (mismatched types untyped string and int)
Why the compiler said that

Go has no automatic conversion to string. + needs both sides to be the same type, and a number is not text.

The fix

Convert the number with strconv.Itoa, or build the string with fmt.Sprintf.

go
package main

import (
	"fmt"
	"strconv"
)

func main() {
	n := 42
	s := "Answer: " + strconv.Itoa(n)
	fmt.Println(s)
}
Short variable declaration
x := value — declares and initialises with an inferred type. Only inside functions.
Zero value
The value a variable has when declared without one: 0, "", false, or nil depending on the type.
Blank identifier
_ — a write-only name that discards a value, e.g. _, err := f().
Untyped constant
A constant with no declared type; it has arbitrary precision and adopts the type its context needs.
iota
A counter inside a const block that starts at 0 and increases by one per line; used to build enums.
rune
Alias for int32; holds one Unicode code point, written as a character literal like 'G'.
Format verb
A % placeholder in a Printf format string, such as %v, %d or %.2f.
Quick check

What does this print? var f float64; var s string; fmt.Printf("%v %q", f, s)

Quick check

Which line compiles?

Frequently asked questions

When should I use var instead of := in Go?
Use := for most variables inside functions. Use var at package level (where := is not allowed), when you want the zero value (var total int), or when the inferred type would be wrong (var x int64 = 5).
Why does Go refuse to compile unused variables?
An unused local variable is almost always a bug or leftover code, so Go makes it a compile error instead of a warning that people ignore. Assign it to _ if you need to keep it temporarily.
How do I convert a string to an int in Go?
Use strconv.Atoi("42"), which returns the int and an error; check the error, because the text might not be a number. For the other direction use strconv.Itoa(42) — not string(42), which gives the character with code point 42.

Finish the Go handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.