Explain where values are copied, how goroutines are bounded and stopped, and which error or transaction contract callers can rely on. Each answer works through a concrete case and a failure that changes the design.
For: Go developers building services and command-line tools
Guide scope and versions
Covers the language and standard library: shared storage, interface method sets and nil values, bounded concurrency, cancellation and service boundaries.
Question 1 of 8
When can two slices change the same data?
A slice is a descriptor over an underlying array. Copying the slice copies its descriptor, not the elements. Two slices can therefore share storage until an append needs a new backing array.
A parser returns fields[:2] from a larger buffer, and a caller keeps that small slice for a long time. Explain both the mutation and memory-retention risks. A write through either slice is visible through the other while they overlap. Keeping a short slice can also keep the whole backing array reachable.
append may reuse spare capacity or allocate a new array. Code must not rely on which path occurs unless capacity is part of the contract. Clone when the API transfers ownership or needs an independent lifetime.
Go · Worked example
buf := []byte{'a', 'b', 'c', 'd'}
head := buf[:2]
head[0] = 'x'
// buf is now []byte{'x', 'b', 'c', 'd'}
owned := append([]byte(nil), head...)
The first assignment mutates shared storage. The final append copies the selected bytes into an independently owned slice.
Executed with Go 1.24.11 in an isolated Linux container; assertions passed.
Follow-up: When is copying the slice unnecessary?
When sharing is intentional, the backing data is immutable for the shared lifetime, and retaining the allocation is acceptable. State that ownership contract at the API boundary.
Why does a type satisfy an interface without declaring it?
Go interfaces are satisfied implicitly by method sets. This lets a package depend on the behavior it uses, but pointer and value method sets differ, so a seemingly correct implementation can fail to satisfy an interface.
Suppose Store requires Save() error and *FileStore implements Save with a pointer receiver. A *FileStore value can satisfy Store; a FileStore value does not have that pointer-receiver method in its method set. Compile-time assertions make the intended contract explicit.
Keep interfaces near the consumer and small enough to describe the behavior it needs. Avoid exporting a large speculative interface that every implementation must imitate.
An interface value carries a dynamic type and a dynamic value. Assigning a nil *FileStore to Store gives the interface a dynamic type, so the interface itself is not nil. Returning a typed nil error pointer has the same trap: a caller can enter if err != nil even though the pointer inside is nil. Return an untyped nil when there is no error. Whether a method call on a nil receiver panics depends on its implementation, not just interface assignment.
Go · Worked example
type Store interface { Save() error }
type FileStore struct{}
func (*FileStore) Save() error { return nil }
var _ Store = (*FileStore)(nil)
var pointer *FileStore
var store Store = pointer
// pointer == nil is true; store == nil is false
var empty Store
// empty == nil is true
The assertion asks the compiler to check the pointer type against the interface. Changing it to var _ Store = FileStore{} fails because the value method set lacks Save. The additional assignments distinguish an absent interface from an interface containing a typed nil pointer. store has dynamic type *FileStore, so its comparison to nil is false.
Executed with Go 1.24.11 in an isolated Linux container; assertions passed.
Follow-up: Where should a small interface be defined?
Usually beside the code that consumes it, because that package knows the minimal behavior it needs. Define a shared exported interface only when it is a real cross-package contract.
How do you bound concurrent work without an unbounded goroutine queue?
Bound goroutines and queued work at the point tasks enter the system. A fixed worker pool makes concurrency visible; a semaphore acquired only inside each newly spawned goroutine still allows the goroutine count itself to grow without bound.
For an import service, define the maximum workers, queue capacity, and behavior when the queue is full. A bounded queue can block producers, reject work, or shed low-priority work. The choice is part of the service contract, not an implementation detail.
Cancellation must reach workers and producers. A worker that ignores the request context can keep running after its result is no longer wanted. A producer must also be able to stop while blocked sending into a full queue.
Go · Worked example
func send(ctx context.Context, jobs chan<- Job, job Job) error {
select {
case jobs <- job:
return nil
case <-ctx.Done():
return ctx.Err()
}
}
This only shows a cancellation-aware enqueue. The service still needs a bounded channel, a fixed set of workers, and a documented full-queue policy.
Executed with Go 1.24.11 in an isolated Linux container; assertions passed.
Follow-up: Should the queue block or reject when full?
Choose from caller latency and durability requirements. Blocking applies backpressure but can tie up upstream handlers; rejection protects the process and lets callers retry if the operation is safe. A durable job must be persisted before acknowledging acceptance.
How should cancellation flow through a Go request?
Pass the request context to operations that support cancellation, derive deadlines at the boundary that owns the time budget, and call the returned cancel function. Context cancellation is cooperative: code must observe it or use APIs that do.
A handler spends at most two seconds on a database lookup and downstream request. Derive the lookup deadline from the request context, defer cancel, and pass the derived context to QueryContext. If the client disconnects, the parent request context is canceled as well.
A context does not terminate arbitrary CPU work or a library that ignores it. Check ctx.Err() between bounded units of work and make external calls context-aware. Do not store request-specific context in a long-lived struct; pass it to the operation that needs it.
The fixture ran a successful query and rejected a request whose context was already canceled, then checked that returned rows were closed. It does not test cancellation of a query already executing in PostgreSQL. Both the cancel function and returned rows need cleanup on every exit path.
Executed with Go 1.24.11 in an isolated Linux container; assertions passed.
Follow-up: What if a dependency ignores Context?
The caller cannot force-stop it. Use a dependency with cancellation support, isolate the work behind a bounded worker/process boundary, and make late results harmless. A timeout around the caller alone does not prove the underlying operation stopped.
How do you add context to an error without breaking callers?
Wrap an error with %w when callers should still inspect its identity or type, then use errors.Is or errors.As. Use %v when the underlying error should be rendered but deliberately hidden from the public contract.
A repository layer returns a sentinel ErrNotFound. The service adds the record key while preserving inspectability. A handler then maps that domain error to a not-found response. Matching the error string is fragile because wording is not a stable API.
Wrapping exposes an error relationship to callers. Decide which underlying errors are part of the package contract, and avoid leaking implementation-specific driver errors across an abstraction boundary unless that is intentional.
Go · Worked example
if err != nil {
if errors.Is(err, ErrNotFound) {
return fmt.Errorf("load account %q: %w", id, err)
}
return err
}
Inside a function returning an error, this preserves the not-found identity while adding the account key. The caller can still use errors.Is(err, ErrNotFound).
Executed with Go 1.24.11 in an isolated Linux container; assertions passed.
Follow-up: When would you use %v instead of %w?
When you want a diagnostic message but do not want callers to match or depend on the underlying error. That is a contract decision, especially across package or service boundaries.
How do you keep every operation inside a database transaction?
Begin a transaction, perform all related reads and writes through the sql.Tx, then commit once. On every earlier return, roll back. Mixing DB calls into the sequence can run outside the transaction and invalidate the intended atomic boundary.
A checkout decrements inventory and writes an order. Those changes must succeed or fail together. Put both statements on the same transaction and make the transaction-owning function responsible for commit or rollback.
A commit error can be ambiguous: the client may lose the connection after the database commits. Blindly retrying a non-idempotent operation can duplicate effects. Use an operation key or a database constraint to make retries safe, and distinguish a known rollback from an unknown commit outcome.
The deferred rollback is harmless after a successful commit. Every statement uses tx; a production function should return the actual commit error and use idempotent keys where retries can repeat the request.
Executed with Go 1.24.11 in an isolated Linux container; assertions passed.
Follow-up: Should a transaction retry loop repeat every error?
No. Retry only errors known to be transient and only when the entire operation is safe to repeat. Use database-specific serialization/deadlock guidance and an idempotency constraint for externally retried commands.
What does the race detector prove, and what does it miss?
go test -race detects data races exercised by that run. It is valuable evidence, but a clean run does not prove the program has no races because the detector only observes executed paths and schedules.
A cache has concurrent readers and a refresh goroutine. Run realistic parallel tests under the race detector, but also assert the intended ordering, cancellation, and shutdown behavior. A program can be race-free and still deadlock, leak goroutines, or return stale values contrary to its contract.
Fuzzing is useful for input-driven boundaries such as parsers and decoders. Fuzz targets should be fast and deterministic, and a discovered failure becomes a regression input. Keep ordinary unit tests for known cases and invariants.
Shell · Worked example
go test -race ./...
go test -fuzz=FuzzDecode -fuzztime=20s ./internal/codec
These commands ran against the generated codec fixture. The fuzz command completed its published 20-second budget with one worker (GOMAXPROCS=1) in the bounded verifier container. This checks the target and harness, not fuzz coverage or production load. Package and target names must match the repository.
Executed with Go 1.24.11 in an isolated Linux container; assertions passed.
Follow-up: How do you test a goroutine that should stop?
Give it an explicit cancellation or close signal, wait for completion through a WaitGroup or result channel, and assert it exits within a bounded test deadline. Avoid sleeps as the only synchronization mechanism.
How do you decide whether Go code needs optimization?
Start with a representative benchmark or production profile, identify the dominant cost, and change one cause at a time. A CPU profile, heap profile, allocation report, and execution trace answer different questions; none should be replaced by intuition about a short code fragment.
A service has a slow endpoint after adding a cache. Compare a benchmark with realistic input sizes and parallelism, then inspect CPU and allocation profiles. If time is spent waiting on a database, rewriting a loop is unlikely to help. If heap retention dominates, inspect which objects remain reachable rather than only reducing allocation count.
Microbenchmarks can be distorted by compiler optimization, input size, machine load, or an unrealistic hot path. Use repeated measurements and the same Go version and machine conditions when comparing changes.
Shell · Worked example
go test -bench=BenchmarkEncode -benchmem ./internal/codec
go test -run='^$' -bench=BenchmarkEncode -cpuprofile=cpu.out ./internal/codec
These commands ran against a generated deterministic codec fixture and produced benchmark output plus cpu.out. This verifies the tool flow, not representative production performance. Use the generated profile with the matching Go toolchain and benchmark inputs.
Executed with Go 1.24.11 in an isolated Linux container; assertions passed.
Follow-up: Which profile would you inspect for a growing RSS with a stable Go heap?
Compare heap profiles with process and runtime metrics, then inspect native allocations, goroutine stacks, mapped memory, and buffers. RSS includes memory beyond live Go heap objects, so a heap profile alone may not explain it.
These are Hacker News job posts that mention these technologies, not employee earnings or a measure of what a skill adds to pay. The amounts summarize annualized advertised minimums in approximate USD. Groups can overlap and differ in roles, locations and dates.
Approximate USD using ECB daily reference rates as of 2026-10-09. Original currencies and offer ranges remain on the source listings. These figures cover posts tagged Golang, rather than every job in the broader technology family.