Go 1.26 practice
Practice questions for the Go 1.26, taken from the sets of the Go course that need no account: a few questions from each part with the answer and the reason, and a quiz on those sets.
start practising
Opens the Go map: one tap, no sign-up.
Show a question's answer to check yours. These questions were written for wspell; no third party's exam is behind them. The quiz runs without an account.
at a glance
- exams
- Go 1.26
- questions
- 4,743 questions
- flashcards
- 2,314 flashcards
- without an account
- 30 of 384 sets
- parts
- 10 parts
- sample questions
- 30 sample questions
Go 1.26
30 sets · 398 questions · 166 flashcards, no account needed, in 10 parts. The page shows 30 sample questions with the answers, a few from each part.
Language
7 sets · 92 questions · 38 flashcards
from the set Slice headers: len, cap and the backing array
An API keeps three records and returns only the first two to an untrusted caller: records := []string{"public", "public2", "SECRET"} shown := records[:2] The caller receives shown. Which statement is true?- It is safe: returning a sub-slice copies the elements into a fresh array
- The caller can recover "SECRET" with shown[:cap(shown)], because reslicing is bounded by capacity
- It is safe: a reslice can never reach past the length it was given
- The caller panics if it reslices past the length, so the data is protected
show the answer
answer B. The caller can recover "SECRET" with shown[:cap(shown)], because reslicing is bounded by capacity
The third record is NOT protected.
shownhas len 2 but cap 3, and a slice expression is bounded by capacity, soshown[:cap(shown)]prints[public public2 SECRET]— verified by running it. Nothing is copied by a slice expression and nothing is cleared, so the caller holds a pointer into the same array. The fix is the full slice expression,records[:2:2], which caps the result at 2 so any further reslice or append cannot see or overwrite the third element;slices.Clone(records[:2])is the other fix and additionally breaks write aliasing. This is the practical reason the three-index form exists, and it applies to []byte returned from parsers just as much as to strings in a list.official docs
from the set Maps: semantics, comma-ok, delete & unordered iteration
m := map[float64]string{} m[math.NaN()] = "a" m[math.NaN()] = "b" fmt.Println(len(m), m[math.NaN()] == "") What is printed?- 2 true
- It panics: NaN is not a valid map key
- 2 false
- 1 false — the second assignment overwrites the first
show the answer
answer A. 2 true
It prints
2 true. float64 is a comparable type, so NaN is a perfectly legal key and nothing panics — but NaN is the one value that is not equal to itself, so each assignment finds no existing key and creates a NEW entry, and no lookup can ever match one again. The map ends up with two entries you can reach only by ranging (or remove with clear);m[math.NaN()]returns the zero value, the empty string. The lesson generalises: avoid float keys wherever NaN can occur, including struct keys that contain a float field, and never treat "the value round-trips" as a property of every comparable key type.official docs
from the set Interfaces: method sets, satisfaction & the empty interface
type T struct{} func (*T) String() string { return "T" } func main() { var s1 fmt.Stringer var a1 any = s1 var p *T var s2 fmt.Stringer = p var a2 any = s2 fmt.Println(a1 == nil, a2 == nil) } What does this print?- true false
- true true
- false false
- false true
show the answer
answer A. true false
It prints
true false. s1 is a nil interface — no dynamic type and no value — and assigning an interface to another interface copies its (type, value) pair, so a1 receives no dynamic type either and is nil. s2 is different: it was assigned a nil *T, so its pair is (*T, nil); copying it into a2 keeps the dynamic type *T, and an interface is nil only when it has NO dynamic type. The Go FAQ states the rule: "An interface value is nil only if the V and T are both unset".true truetreats the nil pointer inside as making the whole value nil; the options with a1 false assume that passing through fmt.Stringer leaves a type behind, but a nil interface has no type to leave.official docs
Generics
2 sets · 15 questions · 21 flashcards
from the set Type parameter declarations on functions
func Largest[T int | float64 | string](vals ...T) T { m := vals[0] for _, v := range vals[1:] { if v > m { m = v } } return m } func main() { fmt.Println(Largest(3, 9, 4), Largest("ant", "zebra")) } What does this program print?- It does not compile: > is not defined for a type parameter whose type set includes string.
- 9 ant
- 9 zebra
- It does not compile: a variadic parameter may not have a type-parameter element type.
show the answer
answer C. 9 zebra
It prints
9 zebra. A variadic parameter is just a[]Tinside the function, so nothing stops T from being a type parameter; inference collects the type from the variadic arguments, giving int for the first call (the untyped constants take their default type) and string for the second.>is available because EVERY type in the type setint | float64 | stringis ordered, and string ordering is byte-wise lexicographic, so "zebra" > "ant". Had the union contained a type without an ordering - a struct or a slice type - the comparison would have been rejected. The real standard-library spelling of this constraint is cmp.Ordered.official docs
from the set Type parameter declarations on functions
func F[T any, U T](a T, b U) { fmt.Println(a, b) } func main() { F[int, int](1, 2) } In Go 1.26, why is this package rejected?- Because U is never used anywhere in the body of the function F.
- Because a type parameter may not be used as another parameter's constraint.
- Because a constraint has to be an interface literal, not a named interface.
- Because F is instantiated with two identical type arguments.
show the answer
answer B. Because a type parameter may not be used as another parameter's constraint.
go1.26.4 reports
cannot use a type parameter as constraint. A constraint is an interface that defines a type set, and a type parameter is a placeholder, not an interface - the spec's own examples listinterface{ P }as illegal for exactly this reason. What a type parameter MAY do is appear inside another parameter's constraint as a type argument or a component:[S ~[]E, E any]and[P Constraint[int]]are both legal and are the standard way to relate two parameters. Instantiating with two identical type arguments is ordinary; an unused type parameter is legal; and a constraint may be a named interface such ascmp.Orderedjust as happily as a literal.official docs
from the set Type parameter declarations on functions
func Describe[P any](v P) string { type L P var l L return fmt.Sprintf("%T %T", v, l) } func main() { fmt.Println(Describe(1)) } In Go 1.26, what does the compiler report for this package?- It fails to build, because a type may not be declared inside a generic function.
- Nothing: it prints `int int`, since L is defined as P and P is int here.
- Nothing: it prints `int main.L`, since L is a distinct defined type.
- It fails to build, because a type parameter may not be the right-hand side of a type declaration.
show the answer
answer D. It fails to build, because a type parameter may not be the right-hand side of a type declaration.
go1.26.4 reports
cannot use a type parameter as RHS in type declaration, and the spec spells the same restriction out withfunc f[P any]() { type L P } // illegal. A defined type needs a single fixed underlying type at declaration time, and a type parameter is a placeholder that stands for a different type per instantiation, so there is nothing for L's underlying type to be. Local type declarations inside a generic function are otherwise fine -type L []intcompiles there - which is why the broad "no types inside a generic function" answer is wrong. Since the package never compiles, neither printed result can occur.official docs
Concurrency
4 sets · 52 questions · 16 flashcards
from the set Goroutines: go statements, argument evaluation & lifetime
A method with a value receiver is started as a goroutine, and the caller then mutates the struct. type counter struct{ n int } func (c counter) report(done chan<- struct{}) { fmt.Println("n =", c.n) close(done) } func main() { done := make(chan struct{}) c := counter{n: 1} go c.report(done) c.n = 99 <-done } What does this program print in Go 1.26, and is the result guaranteed?- n = 99 on some runs, and the race detector flags c.n
- n = 1 or n = 99, depending on which goroutine runs first
- n = 99 on every run, because report starts after the write
- n = 1 on every run, and the race detector stays silent
show the answer
answer D. n = 1 on every run, and the race detector stays silent
Verified on go1.26.4:
n = 1on 10/10 runs, and a-racebuild reports nothing (3 runs).The go statement evaluates the function value
c.reportin the calling goroutine, and the spec (Method values) says of such a value: "The expression x is evaluated and saved during the evaluation of the method value; the saved copy is then used as the receiver in any calls, which may be executed later." With a value receiver, the saved copy is a copy of the whole struct, taken whilec.nis still 1. The goroutine reads only that copy, so there is no shared memory access at all."n = 99 on every run, because report starts after the write" is wrong: when the goroutine starts does not matter, because it never reads main's
c."n = 1 or n = 99, depending on which goroutine runs first" and "n = 99 on some runs, and the race detector flags c.n" both assume the goroutine reads the variable; it reads the copy. They would become true only with a pointer receiver,
func (c *counter) report(...), where just the pointer is saved and the read races withc.n = 99.official docs
from the set Unbuffered channels & synchronous handoff
A monitoring line reports the queue depth while a sender is parked on an unbuffered channel. func main() { ch := make(chan int) go func() { ch <- 7 }() time.Sleep(50 * time.Millisecond) fmt.Println(len(ch), cap(ch)) fmt.Println(<-ch) } What does this print? The two output lines are shown separated by a slash.- 1 0 / 7
- 1 1 / 7
- 0 1 / 7
- 0 0 / 7
show the answer
answer D. 0 0 / 7
Verified on go1.26.4:
0 0then7, 10/10 runs, race detector silent.The spec (Length and capacity) defines
lenon a channel as the "number of elements queued in channel buffer" andcapas the "channel buffer capacity". An unbuffered channel has no buffer, so both are 0 — whether or not a sender happens to be waiting. The waiting sender's value is not queued in the channel; it is handed over directly when the receive happens, which is why the second line is7.The output does not even depend on the sleep: before the sender arrives the answer is also
0 0."1 1 / 7" and "1 0 / 7" both count the parked sender as a queued element; it is not one.
"0 1 / 7" treats an unbuffered channel as a channel of capacity one;
make(chan int)has capacity zero.official docs
from the set select: pseudo-random choice, default & blocking forever
A reviewer asks whether these two receives differ: ch := make(chan int, 2) ch <- 5 ch <- 6 select { case v := <-ch: fmt.Println("select got", v) } v := <-ch fmt.Println("plain got", v) In Go 1.26, what is the difference?- The select form panics if the channel is closed, while the plain receive returns the zero value
- The select form is non-blocking, while the plain receive blocks until a value arrives
- The select form re-evaluates ch on every execution, so it must not be used with a nil channel
- There is none in behaviour — a single-case select with no default blocks exactly like a plain receive
show the answer
answer D. There is none in behaviour — a single-case select with no default blocks exactly like a plain receive
A select with one case and no default has exactly the semantics of that single communication: it blocks until the case can proceed. The compiler even lowers it to the plain operation, so there is no cost and no behavioural difference — the snippet prints
select got 5andplain got 6. Onlydefaultmakes a select non-blocking, so "the select form is non-blocking" is the misconception under test here. Neither form panics on a closed channel: receiving from a closed channel is always legal and yields the zero value with ok == false. And while a select does evaluate its channel operands each time it runs, so does a plain receive, and a nil channel simply blocks forever in both.official docs
Errors
3 sets · 33 questions · 18 flashcards
from the set The error interface, error values & construction
type result struct { n int err error } func main() { var r result fmt.Println(r.err == nil, r.n, r.err) } What is printed?- true 0, then a nil-pointer panic when r.err is printed.
- It does not compile: a struct field of interface type needs an explicit initialiser.
- false 0 <nil>
- true 0 <nil>
show the answer
answer D. true 0 <nil>
It prints
true 0 <nil>. The zero value of any interface type, error included, is nil, so a freshly declared struct starts out meaning 'no error' with no work from you - which is what makesvar r resultand channels of such structs usable straight away. Printing a nil error is safe: fmt checks the interface before calling anything, so there is no method call to dereference and no panic. No initialiser is required - zero values are always well defined in Go - and the field being nil is exactly the state the code then tests withr.err == nil.official docs
from the set Wrapping with %w, multiple %w & Unwrap
var ErrClosed = errors.New("connection closed") func main() { a := fmt.Errorf("send: %w", ErrClosed) b := fmt.Errorf("send: %v", ErrClosed) fmt.Println(a.Error() == b.Error()) fmt.Println(errors.Is(a, ErrClosed), errors.Is(b, ErrClosed)) fmt.Println(errors.Unwrap(a) == ErrClosed, errors.Unwrap(b)) } What is printed (output lines separated by `/`)?- true / true false / true <nil>
- false / true false / true <nil>
- true / true true / true <nil>
- true / true false / false <nil>
show the answer
answer A. true / true false / true <nil>
It prints
true,true false,true <nil>. The two messages are byte-for-byte identical - %w formats its operand exactly like %v - so nothing in the text tells you which one a caller can inspect. The difference is structural: %w records the operand, so the returned error has an Unwrap method that hands ErrClosed back, and errors.Is finds it. %v just interpolates the text, producing an ordinary error that wraps nothing, so errors.Is is false and errors.Unwrap returns nil. This is the single most consequential one-character choice in Go error handling, and it is invisible in logs - which is why an accidental %v shows up as 'errors.Is stopped matching' rather than as a formatting bug.official docs
from the set The error interface, error values & construction
type tempErr struct{ retryIn int } func (e tempErr) Error() string { return fmt.Sprintf("retry in %ds", e.retryIn) } func main() { e := tempErr{retryIn: 3} fmt.Printf("%v|%s|%d\n", e, e, e.retryIn) } What is printed?- retry in 3s|%!s(main.tempErr)|3
- {3}|{3}|3
- retry in 3s|retry in 3s|3
- {3}|retry in 3s|3
show the answer
answer C. retry in 3s|retry in 3s|3
It prints
retry in 3s|retry in 3s|3. fmt gives the error interface priority over its default struct formatting: if an operand implements error, fmt calls Error() and then formats that string according to the verb - and this applies to %v and %s alike, so both fields of the output are the method's result, not{3}. The third verb is given e.retryIn, a plain int, so it prints 3 - the method is irrelevant there. Note that the method has a value receiver, so the value e (not just &e) implements error; with a pointer receiver,fmt.Printf("%v", e)would print{3}instead, because tempErr would no longer be an error.official docs
Stdlib
5 sets · 79 questions · 32 flashcards
from the set fmt — verbs, flags, width & precision
func main() { n := 255 fmt.Printf("%b %o %#o %#x %#X\n", n, n, n, n, n) } What does this print?- 11111111 377 0377 0xff 0XFF
- 11111111 0377 377 0xff 0xFF
- 255 377 0o377 0xff 0XFF
- 11111111 377 0o377 ff FF
show the answer
answer A. 11111111 377 0377 0xff 0XFF
It prints
11111111 377 0377 0xff 0XFF. %b is base 2 with no prefix, %o is base 8 with no prefix, and the sharp flag adds the base prefix that Go source would use for that verb:0for %#o (the old-style octal prefix, not0o),0xfor %#x and0Xfor %#X. The case of thexin the prefix follows the case of the verb, exactly as the hex digits do, so %#X yields0XFF. The option with0377 377has the prefixed and unprefixed octal forms swapped, and the one starting255formats a decimal where %b was asked for. Go does have a0oprefix for octal literals since Go 1.13 and fmt can produce it - but with the verb %O, not with %#o.official docs
from the set strings — search, split, trim, replace & case
func main() { fmt.Println(strings.ToUpper("straße"), strings.ToLower("HÉLLO"), strings.EqualFold("Go", "GO")) } In Go 1.26, what does this print?- STRASSE héllo true
- STRASSE HÉLLO false
- STRAßE héllo false
- STRAßE héllo true
show the answer
answer D. STRAßE héllo true
It prints
STRAßE héllo true. ToUpper and ToLower apply Unicode's SIMPLE case mapping, one rune at a time, so a rune with no single-rune uppercase form is left alone: ß stays ß, because its real uppercase is the two-character SS, a special-casing rule this function deliberately does not implement (golang.org/x/text/cases does). Accented letters do have one-to-one mappings, so É lowercases to é. EqualFold compares under Unicode simple case-folding, which is the correct way to ask 'are these the same word ignoring case' - it is true for Go and GO, so everyfalseanswer is out, and note it is not the same as comparing ToLower of both, though the two agree here. TheSTRASSEanswers expect the full, language-aware case mapping that only x/text provides.official docs
from the set encoding/json — marshalling, struct tags, omitempty & omitzero
type User struct { Name string email string Age int `json:"age"` } func main() { b, err := json.Marshal(User{Name: "ann", email: "a@b.c", Age: 30}) fmt.Println(string(b), err) } What is printed?- {"Name":"ann","email":"a@b.c","age":30} <nil>
- {"Name":"ann","age":30} and a non-nil error
- {"Name":"ann","age":30} <nil>
- {"name":"ann","age":30} <nil>, since an untagged field is lower-cased to match JSON convention.
show the answer
answer C. {"Name":"ann","age":30} <nil>
It prints
{"Name":"ann","age":30} <nil>. Only exported fields are marshalled, and an unexported one is skipped in complete silence - no error, no warning, nothing in the output to hint that a value was dropped. This is the single most common surprise in this package: a field renamed to lower case, or a struct copied from another package with its capitals shaved off, simply stops being serialised. Note also that an untagged exported field keeps its Go name exactly, capital included, so Name appears as "Name" rather than "name"; lower-casing the key is what the tag on Age is doing.official docs
Runtime
2 sets · 18 questions · 21 flashcards
from the set The GMP scheduler: G, M, P, run queues & work stealing
Two goroutines are started one immediately after the other, with no synchronisation between them. What is guaranteed about the order in which their first statements run?
- Nothing is guaranteed; only operations joined by a synchronisation edge are ordered.
- They run in creation order unless one of them blocks, at which point the other overtakes it.
- The goroutine created first runs first, because it is enqueued first.
- With GOMAXPROCS set to 1 the creation order is honoured, and only above 1 does it become unpredictable.
show the answer
answer A. Nothing is guaranteed; only operations joined by a synchronisation edge are ordered.
The Go memory model orders events only through synchronisation - a channel operation, a mutex, a WaitGroup, an atomic with the right ordering. The go statement guarantees only that the new goroutine starts after the go statement itself, and says nothing about it relative to other goroutines. The scheduler's local queues, work stealing and preemption all reshuffle things freely, so answers that promise creation order are asserting an implementation accident that varies run to run. Setting GOMAXPROCS to 1 changes nothing about the guarantee: one P still interleaves goroutines at arbitrary points.
official docs
from the set The GMP scheduler: G, M, P, run queues & work stealing
A program running with GOMAXPROCS set to 1 has ten goroutines blocked receiving from an empty channel while main keeps computing. What is true of the single P?
- It is idle, so the runtime reports a deadlock as soon as all ten goroutines are parked.
- It is unusable, so the runtime spawns ten extra threads, one per blocked goroutine.
- It is held by the first blocked goroutine until that goroutine is woken.
- It is free to run main; a goroutine blocked on a channel does not occupy a P.
show the answer
answer D. It is free to run main; a goroutine blocked on a channel does not occupy a P.
Blocking on a channel parks the goroutine: it is taken off the P and put on the channel's wait queue, and the P immediately picks up the next runnable goroutine - here, main. That is why one P can serve thousands of waiting goroutines. Nothing is held by a parked goroutine, and no thread is created for it; threads are created for goroutines blocked in system calls or cgo calls, not for goroutines parked on runtime-managed structures. The deadlock detector fires only when every goroutine is asleep, and main is still running, so there is no deadlock to report.
official docs
from the set The GMP scheduler: G, M, P, run queues & work stealing
The runtime/metrics documentation says /sched/goroutines/running:goroutines is always less than or equal to /sched/gomaxprocs:threads. What does that bound express?
- Only one goroutine per P may exist, so the two numbers are effectively the same.
- The scheduler caps live goroutines at GOMAXPROCS and queues the rest until a slot frees.
- A goroutine must hold a P to execute, and GOMAXPROCS is the number of Ps.
- Goroutines blocked in system calls are counted as running, which is why the bound is not an equality.
show the answer
answer C. A goroutine must hold a P to execute, and GOMAXPROCS is the number of Ps.
Executing Go code requires a P, and the number of Ps is exactly GOMAXPROCS, so the count of goroutines in the running state cannot exceed it - that is the whole content of the bound. Live goroutines are not capped at all: millions can exist, and a sibling metric, /sched/goroutines:goroutines, counts them. Nor does each P hold one goroutine for its lifetime; a P runs one goroutine at a time and switches constantly. Goroutines blocked in a system call are the opposite of running and have their own metric, /sched/goroutines/not-in-go:goroutines, so counting them as running would break the bound rather than explain it.
official docs
Performance
1 set · 18 questions
from the set Escape analysis: what escapes and why
type I interface{ M() int } type S struct{ n int } func (s S) M() int { return s.n } func viaIface(i I) int { return i.M() } func call() int { var i I = S{5} return viaIface(i) } The analysis says leaking param: i for viaIface. What does it say about the S value in call?- moved to heap: i, because the interface variable holds a pointer to the value.
- Nothing, because escape analysis skips values whose type is an interface.
- S escapes to heap, because assigning to an interface variable always boxes.
- S does not escape, because the call is devirtualized and inlined away.
show the answer
answer D. S does not escape, because the call is devirtualized and inlined away.
With the detailed flag, go1.26.4 prints
inlining call to viaIface, thendevirtualizing i.M to S, theninlining call to S.M, and finallyS{...} does not escape. Escape analysis runs AFTER inlining, so by the time it looks at call there is no interface call left: the compiler proved the dynamic type is S, replaced the dynamic dispatch with a direct call and inlined it. Theleaking param: iline is a statement about viaIface compiled on its own, for callers the compiler cannot see through - it does not bind this call site. So "always boxes" is exactly the misconception this example refutes;moved to heapwould name a variable whose address was taken, which is not what happens; and interfaces are analysed like everything else.official docs
from the set Escape analysis: what escapes and why
type Row struct { ID int Name string } var sink []Row func BenchmarkStructs(b *testing.B) { b.ReportAllocs() for b.Loop() { rows := make([]Row, 0, 100) for i := 0; i < 100; i++ { rows = append(rows, Row{ID: i}) } sink = rows } } The same benchmark written with a []*Row, &Row{ID: i} and a []*Row sink reports 101 allocs/op. What does the version shown report?- 101 allocs/op as well, since each Row is a separate object either way.
- 100 allocs/op, one per append, because the result slice is preallocated.
- 0 allocs/op, because make with a constant capacity is always stack allocated.
- 1 alloc/op, because the hundred Rows live inside one backing array.
show the answer
answer D. 1 alloc/op, because the hundred Rows live inside one backing array.
Measured on go1.26.4, the value version is 1 alloc/op and the pointer version is 101 allocs/op, and both counts are identical across repeated runs. Storing Row VALUES puts all hundred of them inside the slice's single backing array, so exactly one object is allocated - the array itself, which escapes because it is assigned to a package-level variable. Storing *Row allocates the array plus one object per element, hence 1 + 100. append does not allocate per call: the capacity was reserved up front, so no regrowth happens in either version. And the constant capacity does not make it free here, because the slice escapes; the identical loop with the result left local does report 0 allocs/op. This is the whole case for struct-of-values layouts on a hot path: fewer objects to allocate, fewer pointers for the collector to chase. The sink is typed on purpose: with a sink of type any, storing the slice would also box its three-word header, and the two versions would then measure 2 and 102.
official docs
from the set Escape analysis: what escapes and why
func tidy() int { x := 1 defer func() { x = 2 }() return x } What does the compiler report about the deferred func literal and about x?- The literal escapes to heap, and x is moved to heap alongside it.
- The literal does not escape, and no line is printed for x at all.
- The literal does not escape, and x is still moved to heap because it is assigned.
- Nothing is printed: defer bodies are excluded from escape analysis entirely.
show the answer
answer B. The literal does not escape, and no line is printed for x at all.
go1.26.4 prints
./d.go:3:8: func literal does not escapeand nothing about x. An open-coded defer runs before the frame is destroyed, so its closure can live in that frame; the compiler only heap-allocates a deferred literal when the defer sits in a loop or is otherwise not open-coded. Because the literal stays on the stack, the variable it writes to can stay there too - assignment through a closure is not by itself a reason to promote a variable, only assignment through a closure that OUTLIVES the frame is. Defer bodies are certainly analysed: that is where thedoes not escapeline comes from. (The function still returns 1, because the return value is copied before the deferred call runs.)official docs
Testing
2 sets · 37 questions
from the set testing.T basics: Error vs Fatal, Helper, Cleanup, TempDir, Setenv
A test starts a helper goroutine that has to report a problem back to the test. Which testing.T method is documented as safe to call from that goroutine?
- t.SkipNow
- t.Errorf
- t.Parallel
- t.FailNow
show the answer
answer B. t.Errorf
The testing package splits its methods in two. FailNow, Fatal, Fatalf, SkipNow, Skip, Skipf and Parallel must be called only from the goroutine running the test function, because each of them ends the test by stopping that goroutine. The reporting methods - the Log and Error variants - "may be called simultaneously from multiple goroutines", so Errorf is the right way for a helper goroutine to record a problem; the test then fails when it finishes. Verified on go1.26.4: a t.Errorf from a goroutine is logged, the test function runs to its natural end and the test is reported FAIL. The idiomatic shape is to Errorf (or send the problem back over a channel) and then return from the goroutine.
official docs
from the set Table-driven tests & subtests with t.Run
A package has one test function, TestParse, with four subtests. A developer runs `go test -run 'TestParse/unicode' -v`. What does go test -v report?
- TestParse itself and the matching subtest, because a parent has to run for its subtests to be reached.
- Only the subtest whose name matches; the parent is not shown in the output because it was not itself selected.
- All four subtests, because the second element of the pattern only orders them.
- Nothing, because -run matches whole test functions and a subtest is not one.
show the answer
answer A. TestParse itself and the matching subtest, because a parent has to run for its subtests to be reached.
The -run argument is split on unbracketed slashes into a sequence of regular expressions and each element must match the corresponding part of a test's identifier, so the first element selects the parent and the second filters its subtests. The documentation adds that "possible parents of matches are run too", which is a necessity rather than a courtesy: the subtest exists only because the parent's body calls t.Run, so the parent must execute for the subtest to be discovered. Verified on go1.26.4: the verbose output shows the parent running and only the matching subtest beneath it, and the parent is reported with its own PASS or FAIL line. The unmatched subtests are filtered out, and subtests are very much addressable - that is what the slash syntax is for.
official docs
from the set testing.T basics: Error vs Fatal, Helper, Cleanup, TempDir, Setenv
A test calls t.TempDir(), stores the path in a package-level variable and writes a file there. When is that directory removed in Go 1.26?
- When the whole test binary exits, so that a later test in the same package can still read the files left in it.
- When the test and all its subtests complete, so a cleanup registered after the TempDir call still sees it.
- Only if the test passes; a failing test's directory is kept behind for inspection.
- As soon as the test function returns, before any of the test's subtests are resumed.
show the answer
answer B. When the test and all its subtests complete, so a cleanup registered after the TempDir call still sees it.
The directory "is automatically removed when the test and all its subtests complete", which is the same moment Cleanup functions fire - and since TempDir's own removal is itself registered as a cleanup when TempDir is called, any cleanup registered after that call runs first and still sees the directory. Verified on go1.26.4 by stashing the path: a subtest saw the directory, the parent's own cleanup saw it, and a later test in the same package found it gone. So it does not survive to the end of the binary, and it does not disappear at the moment the function returns either, which matters for parallel subtests that keep running after their parent's body has finished. The result of the test is not consulted at all; keeping a failing test's directory is a convenience the standard library does not offer, and a test that wants that must make its own directory.
official docs
Tooling
1 set · 18 questions
from the set Modules: go.mod directives and what each one means
What does the `// indirect` comment on a require line in go.mod mean?
- No package or test in the main module imports that module directly; it is needed to build something else.
- The module is needed only by the main module's own tests, never by any of its non-test code.
- The requirement is informational: minimal version selection skips indirect lines when it computes the build list.
- The go command wrote the line itself and will drop it again on the next build.
show the answer
answer A. No package or test in the main module imports that module directly; it is needed to build something else.
// indirectis a marker the go command maintains: it means no package or test in the main module imports a package from that module. The version is still a real requirement and still participates fully in minimal version selection - the comment changes nothing about selection, it only records why the line is there. Test dependencies of the main module's own packages count as direct, so the marker is not about tests. And the line is not transient: go mod tidy keeps an indirect requirement for as long as some dependency needs it, and only removes it when nothing in the build needs that module any more.official docs
from the set Modules: go.mod directives and what each one means
Go 1.25 added the ignore directive. A module's go.mod contains `ignore ./assets`, and ./assets holds a buildable Go package. What does the directive do?
- It keeps that package out of the module zip that gets published to the proxy.
- It makes the package unimportable: an explicit import of it no longer resolves.
- The directory is skipped when patterns such as ./... expand, but an explicit import of a package there still builds.
- go build skips the directory, but go test and go vet still walk into it and build its packages when given the ./... pattern.
show the answer
answer C. The directory is skipped when patterns such as ./... expand, but an explicit import of a package there still builds.
ignore tells the go command not to walk a directory when it is matching patterns, which is what makes it useful for trees of data, fixtures or vendored non-Go material that would otherwise slow every ./... down or fail to load. Verified on go1.26.4: without the directive
go list ./...prints both the root package and the one in ./assets; withignore ./assetsit prints only the root package - and a main package that imports example.com/ign/assets directly still compiles and runs. So it filters pattern expansion, not importability. It is not a packaging filter either: the module zip is built from the repository contents, and ignore has no say in it. And it applies uniformly to every command that expands patterns, go test included.official docs
from the set Modules: go.mod directives and what each one means
In Go 1.26 a module's go.mod carries the line `go 1.21.0`, the installed toolchain is go1.26.4, and one source file loops with `for i := range 3`. What does go build do?
- It builds, and go vet separately reports the construct through its stdversion analyzer.
- It fails to compile, because ranging over an integer needs go1.22 and the go line fixes the language version for the module's files.
- It builds: the go line only affects module resolution, and the installed compiler is 1.26.
- It fails, because the go command refuses to build any module whose go line is older than the running toolchain until go.mod is raised to match.
show the answer
answer B. It fails to compile, because ranging over an integer needs go1.22 and the go line fixes the language version for the module's files.
The go directive is the module's language version, not just a module-system knob: the compiler is invoked with -lang set from it, so a file in this module may only use features up to Go 1.21. go1.26.4 reports
cannot range over 3 (untyped int constant): requires go1.22 or later (-lang was set to go1.21; check go.mod)and exits 1. Having a 1.26 toolchain installed changes nothing - that is exactly the point of the go line, so a module can be compiled by newer toolchains without silently acquiring newer language semantics. vet's stdversion analyzer reports too-new *standard library* symbols for the effective language version, and it is not what stops this build; the compiler does. And an older go line is the normal case, never an error: a newer toolchain builds older modules happily.official docs
Idioms
3 sets · 36 questions · 20 flashcards
from the set Naming: packages, variables, receivers, interfaces & getters
A type keeps its state in an unexported field named owner and needs an exported accessor for it plus a way to change it. Following Effective Go's Getters section, what should the two methods be called?
- Owner and WithOwner
- GetOwner and PutOwner
- Owner and SetOwner
- GetOwner and SetOwner
show the answer
answer C. Owner and SetOwner
Effective Go is explicit: "If you have a field called owner (lower case, unexported), the getter method should be called Owner (upper case, exported), not GetOwner. The use of upper-case names for export provides the hook to discriminate the field from the method. A setter function, if needed, will likely be called SetOwner." So the accessor drops the Get and the mutator keeps Set. GetOwner and SetOwner is the named misconception imported from Java-style beans - the Get is exactly what Effective Go says not to write, even though the Set is right. GetOwner and PutOwner is wrong twice over: Put is not the Go word for a setter. Owner and WithOwner names the accessor correctly but WithX is the functional-options convention for building a value, not for mutating one; Effective Go names the setter SetOwner.
official docs
from the set Go proverbs — what each one actually means
Which of these lines is NOT on the list of Go proverbs at go-proverbs.github.io, as fetched on 2026-09-25?
- Errors are values.
- A little copying is better than a little dependency.
- Accept interfaces, return structs.
- Clear is better than clever.
show the answer
answer C. Accept interfaces, return structs.
The page, fetched on 2026-09-25, lists 19 proverbs "from @robpike.io's inspiring talk at Gopherfest SV 2015", and adds that "the list may be updated when he next gives the talk". Clear is better than clever, Errors are values and A little copying is better than a little dependency are all on it. Accept interfaces, return structs is a community maxim, not one of the proverbs; the nearest official guidance is Code Review Comments' Interfaces entry: "The implementing package should return concrete (usually pointer or struct) types".
official docs
from the set Naming: packages, variables, receivers, interfaces & getters
A package in Go 1.26 is declared as package my_pkg and exports a constant named MAX_LENGTH. What do go build, go vet and gofmt each do with that code?
- go build rejects the package clause, because a package name must be a single lower-case word
- All three accept it: both names break house convention, and neither is a toolchain error
- go build accepts it, but go vet reports both names as convention violations
- gofmt rewrites MAX_LENGTH to MaxLength and leaves the package name alone
show the answer
answer B. All three accept it: both names break house convention, and neither is a toolchain error
Verified on go1.26.4: the package builds, go vet exits 0 with no diagnostic, and gofmt -l prints nothing. A package clause takes any identifier, and an identifier may contain underscores and upper-case letters, so neither name is ill-formed. The conventions are real - Effective Go says packages are "given lower case, single-word names; there should be no need for underscores or mixedCaps", and Code Review Comments says an unexported constant is maxLength, "not MaxLength or MAX_LENGTH" - but they are conventions carried by review and by third-party linters, not by the Go distribution. Expecting vet to catch them is the common misconception: vet checks correctness traps, not style, and no style linter ships in the Go toolchain. gofmt reformats layout and never renames anything.
official docs
how it works
Each part shows a few of its questions, picked in a fixed order from the sets that need no account, so the page reads the same on every visit. Show an answer to see the correct option and why.
Practising a part runs a quiz on the sets made of that part alone, up to 12 of them; practising the whole exam draws a quiz at random from up to 12 of the exam's sets, the ones that mix parts included, and every start draws again. Both use only the sets that need no account. Each answer is marked on the spot and what you miss comes back. A free account opens the rest and lets you sit the mock exam.
frequently asked questions
Where do the Go 1.26 questions come from?
The 30 sample questions on this page, and the 398 questions behind them, were written for wspell. No third party runs an exam behind them, and wspell is independent of any exam provider.
Do I need an account to practise for the Go 1.26?
Not to start: the quiz on the Go 1.26 runs without an account on 30 of 384 sets, and a practise link starts the course for you in the same tap. A free account keeps your progress, opens the other sets and lets you sit the mock exam.
Exam names and trade marks on this page belong to the organisations that own and run those exams. wspell is independent and is not affiliated with or endorsed by any of them.