Skip to main content

Golang engineer technical fundamentals interview questions

11 questions on technical fundamentals for golang engineer candidates. Expect prompts such as “What is the difference between a goroutine and an OS thread?” and “How do Go modules work and what is the role of go.sum?”, each with a worked answer outline and the follow-ups interviewers push on.

Questions

Showing 1 to 11 of 11 technical fundamentals questions.

As asked

What is the difference between a goroutine and an OS thread in terms of memory, creation cost, and how they are scheduled? How many goroutines can a typical Go program run concurrently?

Sample answer outline

A strong answer covers that goroutines start with a small growable stack (2 KB versus 8 MB default for OS threads), are cheaper to create (microseconds versus milliseconds), and are scheduled cooperatively/preemptively by the Go runtime rather than the OS kernel. The scheduler multiplexes many goroutines onto a small number of OS threads. A typical program can run hundreds of thousands to millions of goroutines depending on their stack size and available memory.

Expect these follow-ups

  • What limits how many goroutines you can practically create in a single process?
  • How does goroutine stack growth work when a goroutine needs more than its initial allocation?
goroutinesthreadsschedulerfundamentalsruntime

As asked

Explain the Go module system. What does go.mod contain, what is go.sum for, how does minimum version selection work, and how do you handle a dependency that has no module support?

Sample answer outline

A strong answer explains that go.mod declares the module path, Go version, and required dependencies with their minimum versions. Minimum version selection picks the lowest version that satisfies all constraints, making builds reproducible. go.sum contains cryptographic hashes of every dependency version ever used, preventing supply chain tampering. For a module-unaware dependency, use the require directive with a pseudo-version or the replace directive to point to a local copy.

Expect these follow-ups

  • What does go mod tidy do and when should you run it?
  • How do you audit your dependencies for known CVEs?
go-modulesdependency-managementgo-sumfundamentalstooling

As asked

Go's concurrency motto is 'do not communicate by sharing memory; share memory by communicating'. When does that advice apply, and when is a mutex the right tool despite the motto?

Sample answer outline

A strong answer explains that channels are natural for transferring ownership of data between goroutines and for signaling events (pipeline, fan-out, done channel). Mutexes are simpler and more efficient for protecting shared state that is accessed by many goroutines that all hold a reference (cache, counter, map). Using a channel to protect a shared map adds goroutine overhead and complexity without benefit. The candidate should mention that the Go standard library uses mutexes internally for most data structures.

Expect these follow-ups

  • Can you construct an example where using channels instead of a mutex creates a deadlock?
  • What is the share-memory-by-communicating approach inefficient for?
channelsmutexconcurrencygo-idiomsfundamentals

As asked

How do you decide where to define interfaces in Go, and why do experienced Go teams prefer small consumer-owned interfaces?

Sample answer outline

In Go, interfaces are satisfied implicitly, so the consumer can define the behaviour it needs without forcing producers into a shared abstraction. Small interfaces such as io.Reader or a one-method Store make tests and substitutions easy while keeping production types concrete. Defining large provider-owned interfaces too early creates brittle mocks and hides useful methods behind an artificial surface. The candidate should connect this to package boundaries and dependency direction. A strong answer also says when no interface is needed because a concrete type is clearer.

Expect these follow-ups

  • When is a provider-owned interface justified?
  • How do generics change this decision, if at all?
  • What is the smell in an interface with ten methods?
interfacesapi-designtesting

As asked

Go deliberately randomizes map iteration order. Why does this design choice exist, what problems does it prevent, and what are the practical implications for testing code that iterates over a map?

Sample answer outline

A strong answer explains that randomized iteration order prevents developers from accidentally depending on insertion order (which was consistent in older Go versions and caused real bugs when the implementation changed). In tests, this means assertions on map-derived output like JSON or logs must be order-independent: sort slices before comparing, use require.ElementsMatch, or use maps.Keys from Go 1.21 and sort them. The candidate should mention that the randomization is seeded per-process, so tests may pass locally but fail on different runs.

Expect these follow-ups

  • How do you write a test that covers all branches in code that iterates over a map?
  • What is the time complexity of map lookup, insert, and delete in Go?
mapstestingfundamentalsgo-runtimeiteration

As asked

Explain Go build tags (constraints). How do you use them to include or exclude files for different operating systems, architectures, or custom feature flags? What changed in the syntax in Go 1.17?

Sample answer outline

A strong answer explains that build tags are comments at the top of a file (//go:build tag expression in 1.17+ replacing the old // +build syntax) that tell the compiler whether to include the file. The expression supports &&, ||, and ! operators. GOOS and GOARCH can also be embedded in file names (e.g., foo_linux.go). Custom tags are passed with -tags flag at build time and are common for integration tests, cgo-free builds, or feature toggles.

Expect these follow-ups

  • How do you use build tags to exclude a file from test builds but include it in production builds?
  • What is the difference between a build tag and a build constraint file name suffix?
build-tagstoolingcross-compilationfundamentalsgo-build

As asked

Why are io.Reader and io.Writer central to Go's standard library design? Give examples of types that satisfy these interfaces and explain how composition of io.Reader decorators works.

Sample answer outline

A strong answer explains that io.Reader and io.Writer abstract over any source or destination of bytes, enabling composition without coupling to concrete types. Concrete implementations include os.File, bytes.Buffer, strings.Reader, net.Conn, and gzip.Writer. Decoration works by wrapping: a bufio.Reader wraps any io.Reader to add buffering; a gzip.Reader wraps an io.Reader to decompress on the fly. This allows building pipelines like a gzip-compressed file upload to a database without loading it all into memory.

Expect these follow-ups

  • What is io.ReadCloser and when do you need to call Close on a reader?
  • How would you implement an io.Reader that counts the bytes read through it?
iointerfacesstdlibcompositionfundamentals

As asked

Explain type assertions and type switches in Go. When would you use each, what happens if a type assertion fails, and how do you write a safe assertion that does not panic?

Sample answer outline

A strong answer explains that a type assertion x.(T) extracts the concrete value of T from interface x, panicking if the dynamic type is not T. The two-value form v, ok := x.(T) avoids the panic. A type switch is idiomatic when checking multiple possible types in a single block and provides cleaner code than chained if-else assertions. Common use cases are error type extraction and handling heterogeneous message types.

Expect these follow-ups

  • Can you use a type assertion on a non-interface variable?
  • What is the performance cost of a type assertion versus a direct method call?
type-assertiontype-switchinterfacesfundamentalsgo-idioms

As asked

Explain what init() functions do in Go, how many can exist in a single file, in what order they run across packages, and what are the pitfalls of overusing them.

Sample answer outline

A strong answer explains that init() runs after all variable initializations in the package, before main(). Multiple init functions can exist in one file and run in declaration order. Across packages, the import graph determines order: dependencies initialize before dependents. Pitfalls include hard-to-test side effects, circular init dependencies, and init functions that register global state making code hard to test in isolation. The candidate should prefer explicit initialization over init where possible.

Expect these follow-ups

  • How does a blank import (import _ 'pkg') trigger an init function?
  • What is the execution order if a package is imported multiple times through different paths?
initpackagefundamentalsglobal-statetesting

As asked

Strings in Go are immutable sequences of bytes, not runes. What are the performance implications of string concatenation in a loop, how does strings.Builder address this, and how does indexing a string differ from ranging over it?

Sample answer outline

A strong answer explains that each string concatenation with + allocates a new backing array, making naive concatenation in a loop O(N^2) in total allocations. strings.Builder uses an internal byte slice and grows geometrically, making it O(N). Indexing a string with s[i] gives the i-th byte, not the i-th Unicode character; ranging over a string iterates runes (decoded UTF-8 code points). This distinction matters for handling multi-byte characters like emoji or CJK text.

Expect these follow-ups

  • How do you convert between string and []byte without an allocation?
  • What is the difference between len(s) and utf8.RuneCountInString(s)?
stringsimmutabilityperformanceunicodefundamentals

As asked

Every type in Go has a zero value. List the zero values for the common types, give an example of a useful zero value in the standard library, and explain how the useful zero value principle should influence how you design your own types.

Sample answer outline

A strong answer lists: int/float64 are 0, bool is false, string is empty, pointer/slice/map/chan/func are nil. A useful zero value example is sync.Mutex (zero value is unlocked, usable without initialization), bytes.Buffer (zero value is empty, ready to use with Write), and sync.WaitGroup. Good API design means the zero value is either safe to use immediately or safe to detect and reject. A type that requires a constructor call before use is harder to work with and more likely to be misused.

Expect these follow-ups

  • How do you express a required field in a struct without using a pointer?
  • Why is a nil map readable but not writable, and what is the idiomatic way to handle this?
zero-valuesapi-designgo-idiomsfundamentalstypes