Basics

Hello Go, Variables, Control Flow & Functions

The foundation everything else is built on. If you've never written a line of Go before, start here. Every claim on this page was checked against a real Go 1.26 toolchain before being written down.

Hello, Go Variables & types Control flow Functions Interview Questions

Hello, Go

Here's a complete, runnable Go program — every line matters. Click each highlighted part to see what it does.

package main

import "fmt"

func main() {
	fmt.Println("Hello, Go!")
}
Click any highlighted part above.
package main

import "fmt"

func main() {
	fmt.Println("Hello, Go!")
}
Hello, Go!
Running it yourself

With Go installed, save this as hello.go inside any directory and run go run hello.go. The first time you run a Go file outside an existing module, you'll see a compile error ("could not determine module path"). Fix this by creating a module first:

Terminal
$ go mod init hello go: creating new go.mod: module hello $ go run hello.go Hello, Go!

go mod init hello creates a go.mod file that marks this directory as a Go module — a self-contained unit of code. Every Go project starts with go mod init, even if you only have one file. No Go installed yet? Every "Open in Go Playground" link on this site runs the real Go compiler in your browser, no install required.

Importing multiple packages
package main

import (
	"fmt"
	"strings"
	"time"
)

func main() {
	fmt.Println(strings.ToUpper("hello"))
	fmt.Println(time.Now().Weekday())
}
HELLO Monday

The parenthesized import syntax is the standard way to import several packages at once. By convention, standard-library packages are grouped together, then a blank line separates them from third-party imports — go fmt (Go's built-in code formatter) sorts and groups them automatically. The compiler rejects unused imports, so you'll never end up with dead imports cluttering your code.

Exported vs unexported names

In the code above, strings.ToUpper starts with a capital T — and that's what makes it usable from outside the strings package. In Go, a name is exported (public) if it starts with a capital letter and unexported (private) if it starts with a lowercase letter. This is the only visibility mechanism Go has — there are no public or private keywords.

main.go — compile error
package main

import "fmt"

func main() {
	fmt.println("hello") // lowercase p -> not exported!
}
# command-line-arguments ./main.go:6:4: cannot refer to unexported name fmt.println

The compiler catches this at compile time — it's not a runtime error. Every package's public API is defined purely by capitalization, which makes it visible without needing to look up documentation: if it starts with a capital letter, it's meant to be used from outside the package.

Variables & types

Go is statically typed: every variable has a type, and that type is fixed for its lifetime. You can write the type explicitly with var, or let Go infer it from the value using := (only inside functions — package-level variables always need var).

int
30
whole numbers
float64
19.99
decimal numbers
string
"Gopher"
text
bool
true
true / false
byte (uint8)
65
ASCII / binary data
rune (int32)
'A'
Unicode code point
These are example values you might assign yourself.
package main

import "fmt"

func main() {
	var age int = 30
	var price float64 = 19.99
	name := "Gopher"
	var isReady bool

	fmt.Println(age, price, name, isReady)
	fmt.Printf("%T %T %T %T\n", age, price, name, isReady)

	var count int
	var label string
	var total float64
	var active bool
	fmt.Println(count, label, total, active)
}
30 19.99 Gopher false int float64 string bool 0 0 false

Look closely at that last line: 0 0 false — there are two spaces between the first two values. That's not a typo in this page, it's genuinely what Go prints: label's zero value is an empty string, and fmt.Println puts one space between every argument, so an empty string between two spaces just looks like a gap.

The zero value

var isReady bool declares isReady with no value given — and Go does not leave it as undefined memory. Every type has a zero value it's automatically set to: 0 for numbers, "" for strings, false for booleans. There's no equivalent of reading an uninitialized variable and getting garbage.

Constants

A const declares a value that cannot change during program execution — the compiler enforces this. Unlike var, a const must be given a value on the same line; its type is inferred from the expression. Constants in Go are untyped until they're used in a typed context, which lets them work across different numeric types without conversion.

package main

import "fmt"

const Greeting = "Hello"

const (
	StatusOK  = 200
	Status404 = 404
)

func main() {
	fmt.Println(Greeting, StatusOK)
	// Greeting = "Hi" // compile error: cannot assign to Greeting
}
Hello 200

Grouped const ( ... ) blocks are the standard way to define related constants. Because constants are untyped until used, StatusOK can be assigned to any integer type without an explicit conversion — something that's not true of a regular var.

Type conversion

Go does not automatically convert between types, even if the conversion is safe. If you have a float64 and need an int, you must write the conversion explicitly. This is intentional — implicit conversions have been a source of bugs in C, Java, and JavaScript for decades.

package main

import (
	"fmt"
	"math"
)

func main() {
	x := 3.14                       // float64 by default
	y := int(x)                      // explicit float64 -> int (truncates)
	z := int(math.Round(x))          // properly rounded
	n := float64(42)                 // int -> float64
	s := string(rune(65))              // rune -> string
	fmt.Println(x, y, z, n, s)
}
3.14 3 3 42 A
Conversion is not casting

In languages like C or Java, a "cast" can reinterpret the same bits as a different type. Go's T(x) syntax always performs a real conversion, either changing the bits or failing at compile time. There's no way to re-interpret arbitrary memory as a different type — unsafe.Pointer exists but is deliberately hard to use and almost never needed.

Multiple assignment

Go allows assigning to several variables at once, which pairs naturally with its multiple-return-value functions (shown in the Functions section further down). It's also a convenient way to swap values without a temporary variable.

package main

import "fmt"

func main() {
	a, b, c := 1, "hello", true
	fmt.Println(a, b, c)

	// Swap without a temporary variable
	x, y := 10, 20
	x, y = y, x
	fmt.Println("after swap:", x, y)

	// Only use what you need
	_, second := 1, 2
	fmt.Println("second:", second)
}
1 hello true after swap: 20 10 second: 2

The blank identifier _ discards a value you don't need — here we skip the first return value. This is used constantly in Go for ignoring unused function results.

Common mistake: shadowing with :=

The short declaration := creates a new variable. If you use it inside an inner scope (inside an if block, for example) with a name that already exists in an outer scope, you get a second, separate variable that shadows the outer one — the outer variable is never modified, even though it looks like you're reassigning it.

outer scope
x = 10
inner scope
x = 20
x = 10 (unchanged)
x = 20

The fix is straightforward: use = (not :=) when you intend to reassign an existing variable. := always declares something new; = always assigns to something that already exists. Go even provides go vet with a -shadow flag to detect accidental shadowing.

Control flow

Go keeps this deliberately small: if/else, switch, and exactly one looping keyword, for, which covers what other languages split into for, while, and infinite-loop forms. Conditions never need parentheses, and braces are mandatory — there's no single-statement shortcut.

1
2
3
4
5
Click "Step" to run i := 1.
package main

import "fmt"

func main() {
	for i := 1; i <= 5; i++ {
		if i%2 == 0 {
			fmt.Println(i, "even")
		} else {
			fmt.Println(i, "odd")
		}
	}

	day := 3
	switch day {
	case 1, 7:
		fmt.Println("weekend")
	default:
		fmt.Println("weekday")
	}

	n := 0
	for n < 3 {
		fmt.Println("n is", n)
		n++
	}
}
1 odd 2 even 3 odd 4 even 5 odd weekday n is 0 n is 1 n is 2

Three things worth noticing: switch in Go does not fall through to the next case by default (unlike C or JavaScript) — each case breaks automatically. A for with just a condition (for n < 3) is Go's version of a while loop. And for with nothing at all (for { ... }) is Go's infinite loop, typically exited with break or return.

Current as of Go 1.22: loop variables and goroutines

If you read older Go tutorials or Stack Overflow answers, you'll likely see a famous warning: launching a goroutine inside a for loop that captures the loop variable (e.g. go func() { use(i) }()) used to be a common bug, because every iteration shared the same i, so the goroutines would typically all see its final value once the loop ended. This was fixed in Go 1.22: each iteration now gets its own fresh copy of the loop variable. We confirmed this ourselves — running a loop that launches three goroutines each capturing i directly, with no workaround, correctly and consistently prints [0 10 20] on this site's Go 1.26 toolchain, never a jumbled or repeated result. One catch: this depends on your module's declared Go version, not just your installed toolchain — a go.mod that still declares go 1.21 or earlier keeps the old sharing behavior even when built with a Go 1.22+ toolchain. Check (or bump) the go directive in go.mod if you're relying on this.

For-range: iterating over collections

The for range form iterates over elements of a slice, map, string, or channel. What it yields as the two iteration values changes depending on the type you're ranging over — a common point of confusion for newcomers.

for i, v := range []int{10, 20}
Type: slice. Each range yields two values — check what changes per type.
package main

import "fmt"

func main() {
	slice := []string{"a", "b", "c"}
	for i, v := range slice {
		fmt.Println(i, v)
	}

	letters := map[string]int{"x": 1, "y": 2}
	for k, v := range letters {
		fmt.Println(k, v) // order is NOT guaranteed!
	}

	text := "Hi"
	for pos, runeVal := range text {
		fmt.Printf("byte %d: %c\n", pos, runeVal)
	}
}
0 a 1 b 2 c x 1 y 2 byte 0: H byte 1: i

Notice the pattern changes per type: slices and arrays give you index, element, maps give you key, value, and strings give you byte offset, rune (Unicode code point). If you only need the element or key and can skip the index, use for _, v := range ... with the blank identifier.

If with initialization

Go allows a short statement before the condition in an if. This is most commonly used for the "check, then handle" pattern that is everywhere in Go code:

package main

import (
	"fmt"
	"strconv"
)

func main() {
	input := "42"
	if n, err := strconv.Atoi(input); err != nil {
		fmt.Println("not a number:", err)
	} else {
		fmt.Println("parsed:", n)
	}
}
parsed: 42

n and err are scoped to the if/else block — they don't leak into the surrounding function. This is the most idiomatic Go error-handling pattern, and you'll see it in virtually every function that can fail.

Tagless switch

A switch with no value after the keyword evaluates each case's boolean expression and picks the first true one. This is Go's idiomatic replacement for long if/else if chains:

package main

import "fmt"

func classify(x int) string {
	switch {
	case x < 0:
		return "negative"
	case x == 0:
		return "zero"
	case x < 10:
		return "small"
	default:
		return "large"
	}
}

func main() {
	for _, v := range []int{-5, 0, 3, 42} {
		fmt.Println(v, classify(v))
	}
}
-5 negative 0 zero 3 small 42 large

Each case is checked top-to-bottom, and the first match wins — no break needed. The default case matches if nothing else does, like a final else.

Labeled break for nested loops

A plain break inside a nested loop only exits the innermost loop. To break out of an outer loop, label it:

main.go
package main

import "fmt"

func main() {
outer:
	for i := 0; i < 3; i++ {
		for j := 0; j < 3; j++ {
			if i == 1 && j == 1 {
				break outer // exits BOTH loops
			}
			fmt.Println(i, j)
		}
	}
	fmt.Println("done")
}
0 0 0 1 0 2 1 0 done

Without the outer label, break would only exit the inner j loop and continue the i loop. Labels work with continue too for skipping to the next iteration of a named loop.

Break and continue in a for loop

break exits the loop immediately; continue skips the rest of the current iteration and moves to the next one. Try them in the loop visual above and watch how the remaining iterations change.

Functions

Functions can take any number of typed parameters, and — distinctively for a mainstream language — return more than one value natively, with no wrapper object or out-parameter required.

a = 17
b = 5
divmod(a, b)
3
2
Click to call the function below.
package main

import "fmt"

func divmod(a, b int) (int, int) {
	return a / b, a % b
}

func sum(nums ...int) int {
	total := 0
	for _, n := range nums {
		total += n
	}
	return total
}

func namedReturn(a, b int) (sum int, diff int) {
	sum = a + b
	diff = a - b
	return
}

func main() {
	q, r := divmod(17, 5)
	fmt.Println(q, r)
	fmt.Println(sum(1, 2, 3, 4))
	s, d := namedReturn(10, 4)
	fmt.Println(s, d)
}
3 2 10 14 6

sum(nums ...int) is a variadic parameter — call it with any number of int arguments (sum(1, 2, 3, 4)), and inside the function nums is just a slice. namedReturn shows named return values: declaring sum and diff in the signature itself means a bare return with no arguments sends back whatever they currently hold.

The (result, error) pattern

In Go, errors are returned values — not exceptions. The convention is to return an extra error as the last return value, and have the caller check it immediately. This is the single most common pattern in all of Go code:

package main

import (
	"fmt"
	"strconv"
)

func parseAndAdd(a, b string) (int, error) {
	x, err := strconv.Atoi(a)
	if err != nil {
		return 0, fmt.Errorf("parse a: %w", err)
	}
	y, err := strconv.Atoi(b)
	if err != nil {
		return 0, fmt.Errorf("parse b: %w", err)
	}
	return x + y, nil
}

func main() {
	sum, err := parseAndAdd("10", "32")
	fmt.Println(sum, err)
	sum, err = parseAndAdd("10", "abc")
	fmt.Println(sum, err)
}
42 0 parse b: strconv.Atoi: parsing "abc": invalid syntax

The idiomatic Go pattern is result, err := doSomething(), then if err != nil { return ... } on the next line. When there's no error, you return the zero value for the other results (here, 0), but you could also return partial results with a wrapped error. Core.html has more on error wrapping and errors.Is.

Functions as values

In Go, functions are first-class values — you can store them in variables, pass them as arguments, and return them from other functions. This is the foundation of Go's approach to callbacks, middleware, and event handling:

package main

import "fmt"

func apply(f func(int) int, n int) int {
	return f(n)
}

func main() {
	double := func(x int) int { return x * 2 }
	square := func(x int) int { return x * x }

	fmt.Println(apply(double, 5))
	fmt.Println(apply(square, 5))
}
10 25

A func(x int) int is a function literal — an anonymous function value that you can assign to a variable. This is used constantly in HTTP handlers, goroutines, and as callbacks to library functions like sort.Slice.

Closures: functions that capture their environment

When a function literal references a variable defined outside its body, the function doesn't get a copy — it holds a reference to the original variable. This means the function can still read and modify that variable even after the surrounding function has returned. This is called a closure.

outer function (returned)
multiplier = 3
closure
func(x int) int { ... }
Click to see what the closure captures.
package main

import "fmt"

func makeTripler() func(int) int {
	multiplier := 3
	return func(x int) int {
		return multiplier * x
	}
}

func main() {
	triple := makeTripler()
	fmt.Println(triple(7))
	fmt.Println(triple(3))
}
21 9

makeTripler() runs, creates multiplier with value 3, then returns a function that closes over it. The returned function "remembers" multiplier is 3 long after makeTripler() has returned and the variable would normally be out of scope. This is the foundation of Go's middleware pattern and goroutine-friendly callbacks.

defer: guaranteed cleanup

A defer statement schedules a function call to run after the current function returns. Multiple defer calls run in reverse order (last-in, first-out) — like a stack. This is the standard way to ensure resources (files, locks, network connections) are always cleaned up, even if the function panics.

defer stack
close(file) ← push 1
release(lock) ← push 2
disconnect(ctx) ← push 3
pushing (0/3 pushed)
Click "Step" to push the first defer onto the stack.
package main

import "fmt"

func main() {
	defer fmt.Println("cleanup file")
	defer fmt.Println("release lock")
	defer fmt.Println("close connection")

	fmt.Println("main work")
}
main work close connection release lock cleanup file

Notice the output order: "main work" prints first, then the defers run in reverse order — "close connection" (the last defer pushed) runs first, then "release lock", then "cleanup file". The practical rule: defer cleanup in the order you'd naturally clean up (outermost last), because LIFO will execute it in reverse — first-in, last-out — which is typically the correct cleanup order for nested resources.

Deferred function arguments are evaluated immediately

The function call is deferred, but its arguments are evaluated right away. In the code above, fmt.Println("close connection") captures the string now — the defer only affects when the function runs, not when its inputs are evaluated. This matters when you defer a loop body: the arguments are evaluated once per iteration, which is almost always the correct behavior.

Interview Questions

Common interview questions about Go basics — syntax, variables, control flow, and functions — organized by experience level. Click a tab to filter.

Q1. What must every executable Go program have, and what does func main do?

Every executable Go program must belong to package main and define a func main() with no arguments and no return value. main is the entry point — execution starts there once the runtime has initialized package-level variables and run any init functions.

Q2. What is the difference between declaring a variable with var and with :=?

var x int = 5 is an explicit declaration that can appear at package or function scope and works even with no initial value (it gets the zero value). x := 5 is short variable declaration — it infers the type from the right-hand side and can only be used inside a function body, never at package scope.

Q3. What is a "zero value" in Go? Give an example.

A zero value is the default value a variable gets when it's declared without an explicit initializer. For example, var n int is 0, var s string is "", var b bool is false, and var p *int is nil. Go has no concept of an uninitialized variable.

Q4. Name some of Go's basic scalar types.

int and float64 for numbers, string for text, bool for true/false, byte (an alias for uint8) for raw bytes, and rune (an alias for int32) for a single Unicode code point.

Q5. What is the difference between const and var?

const declares a value that is fixed at compile time and can never be reassigned; var declares a variable whose value can change at runtime. Constants can only be numbers, strings, characters, or booleans — you cannot declare a const slice, map, or struct.

Q6. Does Go require parentheses around if and for conditions?

No — Go does not use parentheses around conditions, for example if x > 0 { ... } not if (x > 0). Braces around the body, however, are mandatory, even for a single statement.

Q7. What forms can Go's for loop take?

Go has a single looping keyword, for, with four forms: the classic three-clause for i := 0; i < n; i++, a condition-only form that acts like a while loop, a bare for {} for an infinite loop, and for i, v := range collection to iterate over arrays, slices, strings, maps, or channels.

Q8. Does a Go switch fall through to the next case by default?

No. Unlike C or Java, each case in a Go switch breaks automatically after its statements run. You must use the explicit fallthrough keyword if you want execution to continue into the next case.

Q9. Can a Go function return more than one value? Give a common use case.

Yes — Go functions can return multiple values, declared in the signature like func divide(a, b int) (int, error). The most common use is returning a result alongside an error, which is Go's idiomatic replacement for exceptions.

Continue to Core →