Back
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.
Start practising Go
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
•
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+)
•
[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
•
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
•
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
•
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.
Start practising Go
All about Go practice
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