Goroutines and the scheduler, channels and select, slices, maps, interfaces, errors, context, sync, generics and testing: the Go facts interviewers probe.
AMCompiled by Aditya Mishra · Technical Lead, BNP Paribas
new(T) returns a *T to a zeroed value. make is only for slices, maps and channels and returns the initialised value, not a pointer.
Strings are immutable byte sequences. len(s) counts bytes; for i, r := range s yields runes with byte offsets.
Unused imports and unused local variables are compile errors.
Slices
s := make([]int, 0, 10) // len 0, cap 10s = append(s, 1, 2, 3) // always keep the resultt := s[1:3] // shares s's array: len 2, cap 9u := s[1:3:3] // full slice expression: cap 2, next append copiesc := slices.Clone(t) // independent copy (Go 1.21)
go
A slice is a header: pointer, length, capacity. Passing it copies the header, not the elements.
append writes in place when len < cap, otherwise allocates a bigger array. Two slices over one array can overwrite each other.
A function that appends must return the slice; the caller’s length does not change otherwise.
var s []int (nil) and []int{} (empty) both have length 0 and both work with append and range; they differ in s == nil and JSON (null vs []).
A small slice of a huge array keeps the whole array alive: clone it.
slices.Sort, slices.Contains, slices.Index, slices.Equal (1.21); slices.Sorted, slices.Collect with iterators (1.23).
Maps
Create with make(map[K]V) or a literal. A nil map reads as empty but panics on write.
v, ok := m[k] tells a missing key from a zero value. delete(m, k) is a no-op if absent.
Iteration order is unspecified and varies: for _, k := range slices.Sorted(maps.Keys(m)) for stable output (1.23).
Elements are not addressable: m[k].Field = v does not compile for struct values.
Keys must be comparable: no slices, maps or funcs as keys.
Not safe for concurrent use: concurrent writes can crash with fatal error: concurrent map writes, which recover cannot catch.
Swiss-table implementation since Go 1.24.
Structs, methods & interfaces
Concept
Rule
Value receiver func (p Point) X()
works on a copy; in the method set of Point and *Point
Pointer receiver func (c *Cache) Put()
can mutate; only in the method set of *Cache
Satisfaction
implicit: a type implements an interface if its method set has the methods
Compile-time check
var _ io.Reader = (*MyReader)(nil)
Embedding
promotes fields and methods; composition, not inheritance, no virtual dispatch
any
alias for interface{} (1.18)
Type assertion
w, ok := r.(io.Writer); without ok a failed assertion panics
Type switch
switch v := x.(type) { case string: ... case nil: ... }
An interface value is a (type, value) pair and is nil only when both are nil. A nil *MyErr returned as error is not a nil error.
Accept interfaces, return concrete types. Define small interfaces in the package that uses them.
Comparing two interfaces that hold the same uncomparable dynamic type (a slice, map or func) panics at run time.
Errors, defer, panic
if err := save(u); err != nil { return fmt.Errorf("save user %d: %w", u.ID, err) // %w keeps the chain, %v flattens it}var nf *NotFoundErrorswitch {case errors.Is(err, sql.ErrNoRows): // value match anywhere in the chaincase errors.As(err, &nf): // type match, fills nf}
go
Errors are values; return them last and handle each once (log or return).
errors.Join(e1, e2) and multiple %w verbs wrap several errors (1.20).
defer runs at function return in LIFO order; arguments are evaluated immediately; deferred closures can modify named results.
recover() works only when called directly by a deferred function in the panicking goroutine. An unrecovered panic in any goroutine kills the process (exit status 2).
panic(nil) raises *runtime.PanicNilError since Go 1.21, so recover returns non-nil.
Goroutines & the scheduler
go f(x) starts a goroutine; x is evaluated now. When main returns, the program exits without waiting for other goroutines.
Starting stack about 2 KB, grows by copying. Hundreds of thousands of goroutines are normal.
G goroutine, M OS thread, P scheduling context with a local run queue. GOMAXPROCS = number of Ps; defaults to CPU count and, since Go 1.25 on Linux, the container CPU limit.
Idle Ps steal work; blocking syscalls hand the P to another thread; network waits park on the netpoller (epoll/kqueue).
Asynchronous preemption since Go 1.14: tight loops no longer starve other goroutines.
Channels & select
Operation
nil channel
open channel
closed channel
ch <- v
blocks forever
blocks until a receiver (unbuffered) or buffer space
panic
<-ch
blocks forever
blocks until a value is available
buffered values, then zero value with ok == false
close(ch)
panic
succeeds
panic
The sender closes; with many senders, a coordinator closes after a WaitGroup wait.
select picks randomly among ready cases; default makes it non-blocking; a nil channel disables a case.
Directional types: chan<- T (send only), <-chan T (receive only).
Timeout: case <-ctx.Done(): or case <-time.After(d):. Since Go 1.23 (module go ≥ 1.23), unreferenced timers are garbage-collected even if never stopped, and timer channels are unbuffered so Reset/Stop never leave a stale value.
sync & atomics
Tool
Use
sync.Mutex / RWMutex
guard shared state; not reentrant; never copy after use
sync.WaitGroup
Add before go, defer Done(), Wait(); wg.Go(f) since 1.25
sync.Once, OnceFunc, OnceValue
lazy one-time init (the last two since 1.21)
sync.Pool
reuse temporary objects to cut allocations; contents can be dropped during GC
sync.Map
append-mostly caches or disjoint keys per goroutine
atomic.Int64, atomic.Bool, atomic.Pointer[T]
lock-free counters and flags (typed since 1.19)
errgroup.Group (golang.org/x/sync)
goroutines that return errors; WithContext, SetLimit
Data race = concurrent access, at least one write, no synchronisation. Find with go test -race ./... (prints WARNING: DATA RACE).
go vet catches copied locks, lost cancel funcs, misused Printf verbs and more.
context
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)defer cancel() // always: releases the timer and the parent linkreq, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
go
First parameter, named ctx; never stored in a struct; never nil (start from context.Background()).
Cancellation flows parent to children only. ctx.Err() is context.Canceled or context.DeadlineExceeded after Done() closes.
WithValue only for request-scoped data (request ID, trace, principal) with an unexported key type.
Generics
func Map[T, U any](s []T, f func(T) U) []U { out := make([]U, 0, len(s)) for _, v := range s { out = append(out, f(v)) } return out}type Number interface{ ~int | ~int64 | ~float64 }func Sum[T Number](xs []T) (t T) { for _, x := range xs { t += x }; return }
go
Constraints: any, comparable, unions with ~ (underlying type), cmp.Ordered (1.21).
Methods cannot have their own type parameters. Generic type aliases arrived in Go 1.24.
Iterators (1.23): iter.Seq[V] is func(yield func(V) bool); for v := range seq works on it.
Memory & performance
Escape analysis decides stack vs heap: go build -gcflags=-m. Returning &local is legal; it just escapes.
Preallocate (make([]T, 0, n)), reuse buffers, use strings.Builder, avoid []byte/string conversions in hot loops.
Profile: import _ "net/http/pprof", then go tool pprof http://host/debug/pprof/profile; heap, goroutine, block and mutex profiles; go tool trace for scheduler latency.
Tooling & testing
Command
Does
go mod init example.com/app, go mod tidy
create module; add missing and drop unused requirements
go get example.com/lib@v1.4.0
add or upgrade a dependency
go build ./..., go run ., go install ./cmd/app
build, run, install
go test ./... -race -cover
tests with race detector and coverage
go test -run 'TestParse/empty' -v
one subtest
go test -bench . -benchmem
benchmarks with allocation counts
go test -fuzz FuzzParse
native fuzzing (1.18)
go vet ./..., gofmt -l .
static checks, formatting
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build
cross-compile a static binary
Modules use minimal version selection: the highest of the required minimums, never “latest”. go.sum pins content hashes.
Major versions 2+ change the import path (example.com/lib/v2).
Tests: TestXxx(t *testing.T) in _test.go; table-driven cases with t.Run; t.Helper(), t.Cleanup(), t.Parallel(); httptest for HTTP; b.Loop() in benchmarks (1.24); testing/synctest for time-dependent concurrent code (1.25).
Gotchas & traps
x := ... inside an if or for block shadows an outer x, often an outer err.
A goroutine blocked on a channel nobody serves leaks forever; give every goroutine an exit path.
for loop with defer inside keeps every resource open until the function returns.
time.Now() comparisons: use t.Equal(u), not ==, because of monotonic clock readings and locations.
JSON only sees exported fields; unexported ones are silently skipped.
http.Get with the default client has no timeout; set http.Client{Timeout: ...} or a context deadline.
Forgetting resp.Body.Close() leaks connections.
os.Exit and log.Fatal skip deferred calls.
Practise with the full question bank on the Go topic page.