Keentune

Go curriculum

24 chapters
·
216 concepts
·
free
Everything the adaptive question bank can teach and test in Go, from foundations through advanced practice. Work through it in order, or start practising and let the questions find your level.
New here? Read the Go guide
A free 18-minute primer — the mental model, the mistakes beginners make, and what to practise first.
A. Program structure, declarations and scope
every file opens with package <name>; the directory is the compilation unit, and all files in it share one package
a runnable program is package main with func main(), which takes no arguments and returns nothing
an initial capital letter is the whole visibility rule; there is no public or private keyword
an unused import or unused local variable fails the build; it is not a warning
:= works only inside a function and needs at least one new name on its left
an inner := creates a new variable instead of assigning the outer one, which silently swallows an error
package-level variables initialize in dependency order, not in source order
init() runs after variable initialization and after imported packages, and may be declared several times
_ discards a value, silences an import with only side effects, and asserts an interface at compile time
B. Types, zero values and conversions
every declared variable is immediately usable: 0, "", false, or nil
type Celsius float64 is a new, distinct type that shares float64's underlying type
mixing int and int64 needs an explicit conversion even though both are integers
T(x) converts a concrete value; x.(T) extracts a dynamic type out of an interface
two struct types are identical only when field names, types, order and tags all match
a value is assignable when one of the two types is unnamed and the underlying types are identical
type A = B is one type with two spellings; type A B is a new type that inherits no methods
slices, maps, channels and funcs carry internal pointers; arrays and structs are copied whole
nil is a value only for pointers, slices, maps, channels, funcs and interfaces, never for an int, a string or a struct
C. Constants, iota and untyped values
an untyped constant has no type until it is used, then it adopts the type of its context
constant arithmetic is exact and unbounded; only assignment to a variable can overflow
the defaults are bool, rune, int, float64, complex128 and string
iota counts specification lines inside one const block and starts at 0
omitting the expression repeats the previous one, which is what makes an iota enumeration work
_ consumes one iota value so an enumeration can start at 1
1 << (10 * iota) produces KB, MB and GB constants
only basic types can be constant; you cannot take the address of a constant
D. Operators, numbers and expressions
integer / truncates toward zero, so -7 / 2 is -3
% takes the sign of the dividend, so -7 % 2 is -1
signed integer overflow wraps around silently; it is defined behavior, not a panic
int is 32 or 64 bits by platform, byte is uint8, and rune is int32
a shift count may be any non-negative value; shifting past the width yields 0 rather than undefined behavior
&^ clears the bits set in its right operand, an operator most languages lack
i++ produces no value, so x = i++ and f(i++) do not compile
structs and arrays support ==; slices, maps and funcs may only be compared with nil
&& and || stop as soon as the result is known, which is what makes a nil check before a dereference safe
E. Control flow
one keyword covers the counted loop, the while loop and the infinite loop
if v, err := f(); err != nil scopes both names to the if and its else branches
every case breaks implicitly, and fallthrough must be the last statement in its case
switch { case cond: } replaces an if/else ladder and reads better
one case may hold several comma-separated values, which are tried in order
for i, v := range s copies each element into v, so assigning to v never changes the slice
map range order is deliberately unspecified and varies per run
the index advances by the rune's byte width and the value is a rune, not a byte
for i := range n iterates 0 through n-1 (Go 1.22+)
F. Functions, closures and variadics
a function returns a tuple, and the (value, error) pair is the language's core idiom
named results are pre-declared and zeroed, and a deferred closure can still change them
arguments are always copied; passing a pointer copies the pointer, not the pointee
...T arrives as a slice, and s... forwards an existing slice without copying it
funcs are first class, storable in maps and structs, and comparable only against nil
a closure captures the variable itself, so later writes are visible inside it
since Go 1.22 each iteration gets a fresh loop variable; before that every closure shared one
x.M binds the receiver into a func value; T.M yields a func that takes the receiver first
an iterator is func(yield func(V) bool), and returning false from yield stops it (Go 1.23+)
G. Arrays and slices
[3]int and [4]int are different types, and assigning an array copies every element
a slice value is a pointer, a length and a capacity, so copying a slice copies only that header
len bounds indexing, cap bounds re-slicing and tells you when append must reallocate
append reuses the backing array while capacity allows and silently copies to a new one when it does not
ignoring append's return value drops the appended elements
s[1:3] shares the backing array, so a write through either view is visible in the other
s[a:b:c] caps capacity so a later append cannot overwrite the parent's elements
a nil slice ranges, lens and appends fine; it differs from []T{} only for == nil and JSON output
copy moves min(len(dst), len(src)) elements and is safe on overlapping slices
keeping a two-element sub-slice keeps the whole backing array from being collected
H. Maps
a missing key yields the value type's zero value; the second result is what distinguishes absent from zero
reading or ranging a nil map is legal and empty; writing to one panics
make(map[K]V, n) presizes the table; the hint is a capacity suggestion, not a limit
slices, maps and funcs cannot be keys, while structs and arrays of comparable fields can
deleting during iteration is defined; an entry added during iteration may or may not be produced
m[k].Field = v does not compile for a struct value, and &m[k] is illegal
passing a map to a function shares the same table, unlike passing a struct
map[T]struct{} is the zero-byte set idiom, with _, ok := as the membership test
the runtime detects a concurrent map write and kills the process; it is not merely a data race
I. Strings, bytes and runes
a string is an immutable sequence of bytes, not an array of characters
s[i] has type byte, and one character may occupy several of them
len("héllo") is 6; counting characters needs utf8.RuneCountInString
[]byte(s) and []rune(s) copy the data, which matters inside a loop
decoding malformed bytes produces U+FFFD instead of an error
+= in a loop reallocates every pass; strings.Builder amortizes it
backquoted literals honor no escape sequences and may span lines
< orders by byte value, which is not locale-aware collation
string(65) is "A"; converting an int to its digits requires strconv.Itoa
J. Structs, literals and embedding
a field-keyed literal survives a new field being added; a positional one breaks
assigning or passing a struct copies every field, including arrays inside it
== works when every field is comparable, and panics at runtime only through an interface
an embedded type's fields and methods are promoted, but there is no subtype relationship
the shallowest promoted name wins; two at the same depth are a compile error unless qualified
embedding *T promotes T's methods but panics when the embedded pointer is nil
tags are strings interpreted by reflection, so a misspelled json: key fails silently
struct{}{} occupies zero bytes and signals a value that carries no data
K. Methods and method sets
a method is a function with a receiver, and a value receiver gets its own copy
a value receiver cannot change the caller's value; only a pointer receiver can
*T's method set holds both value and pointer methods, while T's holds only value methods
v.PointerMethod() compiles only when v is addressable, which a map element or a literal is not
if the method has a pointer receiver, T does not satisfy the interface but *T does
a named slice, map or func type can carry methods; only structs is a misconception
you cannot attach a method to a type from another package; define a local named type first
implementing String() changes every %v, and calling %v on the receiver inside it recurses forever
L. Interfaces and type assertions
there is no implements clause; having the methods is the whole requirement
the package that uses the behavior declares the interface, the provider just returns a concrete type
one- and two-method interfaces compose, and io.Reader is the model to copy
an interface variable stores a dynamic type alongside the value
an interface holding a nil *T is non-nil, which is why a concrete error type must not be returned as an error unconditionally
the single-result assertion panics on mismatch; the two-result form reports failure instead
switch v := x.(type) binds v to each case's concrete type, and to the interface type in a multi-type case
any and interface{} are the same type, and neither tells you anything about the value
var _ I = (*T)(nil) fails the build the moment T stops satisfying I
M. Pointers, allocation and escape analysis
pointers can be dereferenced, compared and passed, never offset
p.Field reads through the pointer with no (*p) needed
new(T) returns a zeroed *T; make initializes and returns a slice, map or channel value
&T{...} is idiomatic, and returning that address from a function is safe
the compiler, not a keyword, decides stack or heap; taking an address does not force the heap
the variable escapes to the heap, so Go has no dangling-pointer class of bug
dereferencing nil is a runtime panic that unwinds like any other
unsafe.Pointer bypasses type safety and forfeits the compatibility guarantee
N. defer, panic and recover
deferred calls run last-in-first-out as the function returns
the arguments are captured when the defer executes, not when the call finally runs
deferring inside a loop delays every call to function exit, so file handles pile up
a deferred closure assigning to a named err changes what the caller sees
defers execute while a panic unwinds, which is what makes them the unlock and close mechanism
defer f.Close() discards the error, which loses data on a buffered write
an unrecovered panic runs defers up the stack, prints the trace and exits with status 2
recover() returns nil unless it is called directly by a function deferred during the panic
an unrecovered panic in any goroutine takes down the whole process; a caller cannot recover across the go boundary
O. Errors and error handling
the whole contract is one method, Error() string
errors are returned and inspected like any other value; there is no throw
the idiom is if err != nil right after the call, with the happy path unindented
a package-level var ErrNotFound = errors.New(...) gives callers something stable to compare against
fmt.Errorf("...: %w", err) keeps the chain, while %v flattens it into text and loses it
errors.Is unwraps repeatedly, so wrapping never breaks an existing comparison
errors.As assigns the first matching error in the chain into a typed pointer target
errors.Join combines several errors into one that both Is and As traverse (Go 1.20+)
an error string is lowercase, unpunctuated, and adds context the caller does not already have
comparing err.Error() strings is not an API contract; export a sentinel or a type instead
P. Goroutines and the scheduler
go f(x) evaluates the arguments immediately and runs the call in a new goroutine
concurrency is how the program is structured; parallelism is what the hardware does
a goroutine starts with a small stack that grows on demand, so thousands are routine
when main returns the program exits without waiting for any goroutine
there is no ID and no kill; a goroutine ends only by returning from its function
you signal with a channel or a context and the goroutine must choose to observe it
a goroutine blocked forever on a channel is never collected, and shows up in the goroutine profile
GOMAXPROCS caps how many goroutines run simultaneously, defaulting to the usable CPU count
Q. Channels and select
the send completes only when a receiver takes the value, so both sides synchronize
a buffered send blocks only when the buffer is full, and a receive only when it is empty
operations on a nil channel block permanently, which is how a select case is disabled
receiving from a closed channel returns the zero value immediately and never blocks
the second result is what separates "channel closed" from "the sender sent a zero"
sending on, or closing, an already-closed channel panics, so only the sender closes
closing wakes every receiver at once, which is the shutdown-signal idiom
ranging drains until close, and hangs forever if the producer never closes
chan<- T and <-chan T turn intent into a compile-time restriction at the function boundary
when several cases are ready the runtime picks one at random, which prevents starvation
adding default makes the whole select non-blocking
R. sync, atomics and the memory model
two goroutines touching the same location with at least one write and no ordering between them
visibility is defined by synchronization edges, not by wall-clock order or by "it worked on my machine"
a Mutex guards an invariant over data; every access to that data must take the same mutex
defer mu.Unlock() releases the lock on every return path, including a panic
many readers or one writer, with more bookkeeping than a plain Mutex for short critical sections
copying a struct that embeds a Mutex copies the lock state, which go vet reports
Add must run before the goroutine starts and Done belongs in a defer inside it
Do runs the function exactly once and blocks other callers until that first run finishes
sync/atomic gives lock-free access to a single value, not to an invariant spanning two fields
go test -race reports races that actually occurred, so it proves presence, not absence
S. context
ctx context.Context is the first parameter and is not stored in a struct
Background is the root in main and tests; TODO marks a call site whose plumbing is unfinished
the cancel func returned by WithCancel releases resources and must be deferred, or the context leaks
cancelling a parent cancels every derived context, and never the other way
you select on ctx.Done(), which is closed rather than sent to
context.Canceled versus context.DeadlineExceeded distinguishes a caller giving up from a timeout
WithTimeout takes a duration from now, WithDeadline an absolute instant, and the earlier one wins
Value carries request metadata under an unexported key type, never optional parameters
T. Generics: type parameters and constraints
func F[T any](x T) T declares type parameters that the compiler instantiates per call
a constraint is an interface used as a type set, and such an interface may not be used as a variable type
int | string lists the permitted types directly in the constraint
~int also admits named types whose underlying type is int, which a bare int does not
comparable permits == and !=, and since Go 1.20 accepts any type that supports them
type arguments are usually inferred from the ordinary arguments; explicit instantiation is the fallback
you may only call a method on T if the constraint declares it
var zero T is the only portable way to produce T's zero value
if the code only calls methods, an interface is simpler than a type parameter
U. Modules, versioning and the toolchain
the module path, the go directive and the require list; the module path is the prefix of every import inside it
a v2 or later module carries /v2 in its module path, so two majors can coexist
the build selects the highest version any module requires, not the newest published one
go.sum pins content hashes, and a mismatch stops the build as a supply-chain signal
// indirect marks a requirement that no package in this module imports directly
tidy adds every import that is used and removes every requirement that is not
a replace directive redirects a module locally and is ignored when your module is a dependency
an untagged commit is referenced as v0.0.0-yyyymmddhhmmss-abcdefabcdef
a //go:build line selects a file by OS, architecture or tag, and must precede the package clause
V. Testing, benchmarks and fuzzing
tests live in _test.go files as func TestXxx(t *testing.T) and run under go test
the standard style is if got != want { t.Errorf(...) }, reporting got and want in that order
Errorf records a failure and continues; Fatalf stops that test function immediately
a slice of named cases run through t.Run is the canonical Go test shape
t.Run names each case so -run Test/case can select one
t.Parallel defers the test until the serial phase ends, which changes when shared setup is visible
t.Cleanup registers teardown that runs LIFO after the test and its subtests finish
package foo_test exercises only the exported API and breaks an import cycle
a benchmark repeats the body b.N times, with N chosen by the framework to reach a stable duration
f.Fuzz mutates seed inputs and writes any failing input into testdata as a permanent regression case
W. Runtime, garbage collection and performance
the collector runs alongside the program, trading CPU for short pauses rather than compacting
GOGC sets the heap-growth percentage between collections, and off disables automatic collection
GOMEMLIMIT is a soft cap that makes the GC run more often instead of letting the heap grow (Go 1.19+)
reducing allocations per operation usually beats micro-optimizing the code around them
putting a value in an interface or a ...any argument often forces it onto the heap
runtime.SetFinalizer gives no timing guarantee and must not be used for releasing locks or files
cpu, heap, goroutine, block and mutex profiles each answer a different question
the trace shows scheduling, blocking and latency that a CPU profile cannot explain
X. Standard-library contracts
Read may return n > 0 together with io.EOF, so process the bytes before checking the error
a Write that returns fewer bytes than given must return a non-nil error
io.EOF signals normal termination, and only io.ErrUnexpectedEOF means truncation
a bufio.Writer loses buffered bytes unless Flush is called, and Scanner has a default token size limit
unexported fields are silently skipped, and struct tags control the name, omitempty and omission
decoding into any turns every JSON number into a float64
a time.Time carries a monotonic clock reading, so Sub is immune to wall-clock adjustments
a Duration is an int64 count of nanoseconds; multiply by time.Second rather than passing a bare number
formats are written as the reference time 2006-01-02 15:04:05, not as strftime codes
the caller closes resp.Body, and the default http.Client has no timeout at all
Keentune is not affiliated with or endorsed by the organizations whose documentation informs these maps.
All about Go practice
Also on your phone
All exam, test, and product names and trademarks are the property of their respective owners and are used here for identification and reference only. Keentune is independent study practice — not affiliated with, authorized, or endorsed by any of these organizations.
© 2026 SportaApp LLC