← BackGo30 Q

Go

30 questions | entry to principal | answers written to be said out loud, not read

entry

Why did Google build Go, and what problem was it actually solving?

Go came out of frustration with slow C++ builds, sprawling dependency graphs, and how hard it was to onboard engineers onto huge codebases at Google. The designers wanted the safety and garbage collection of a managed language but the fast compilation and simplicity of C. So Go leans hard into a small language spec, fast builds, and built-in concurrency instead of giving you 5 competing paradigms to choose between.

Can you rely on the order you get back when you range over a Go map?

No, and that's intentional. Go randomizes map iteration order on purpose specifically so nobody writes code that accidentally depends on it. If I need a deterministic order I pull the keys into a slice, sort that slice, and then iterate the slice instead of ranging over the map directly.

What is a struct in Go, and how do you think about it compared to a class?

A struct is just a typed collection of fields, a plain data container with no inheritance and no constructor built into the language. I attach behavior to it by defining methods on the type separately, and I initialize it with a plain literal or a small constructor function like NewUser if there's setup logic. It's deliberately less ceremony than a class in an object oriented language.

What does Go do instead of forcing you to initialize every variable?

Every type in Go has a zero value, so a variable is usable the moment it's declared even if you never assign to it. An int zero value is 0, a bool is false, a pointer or slice or map or channel is nil, and a struct's zero value is just each field set to its own zero value. I lean on this a lot, for example a freshly declared struct is immediately safe to read fields from without a null check.

How do interfaces work in Go, and how is that different from Java or C#?

A Go interface is just a set of method signatures, and a type satisfies it automatically the moment it implements those methods, there's no implements keyword and no explicit declaration required. That's structural typing rather than nominal typing, so I can define a small interface after the fact and any existing type that happens to have the right methods already satisfies it. It's why Go interfaces tend to be small, often just 1 or 2 methods, because you're describing behavior you need rather than declaring a formal contract up front.

Go doesn't have exceptions for normal error handling. Why did they design it that way?

In Go an error is just an ordinary value that implements the error interface, and functions that can fail return it as an explicit extra return value instead of throwing. The philosophy is that error handling should be visible in the code path rather than jumping through an invisible control flow, so I see if err != nil right where the failure can actually happen. It reads as more verbose, but I actually like that I can't accidentally swallow an error the way you can with an empty catch block.

What is a goroutine, and how is it different from an OS thread?

A goroutine is a function running concurrently, managed by the Go runtime instead of the operating system, and starting one is as cheap as writing go before a function call. It starts with a tiny stack, only a few kilobytes, that grows and shrinks as needed, so I can comfortably spin up thousands or even millions of them, which isn't realistic with OS threads that cost megabytes each. The runtime multiplexes many goroutines onto a much smaller number of actual OS threads.

Why do people say Go's standard library is batteries included for backend work?

net/http alone gives you a production capable HTTP server and client, routing, and TLS without pulling in a single external dependency, and that's genuinely what a lot of real services ship on directly. Beyond that, encoding/json, database/sql, context, and testing cover most of what a typical backend service needs day to day. I still reach for a router library or an ORM sometimes for convenience, but it's a real choice rather than a requirement, which is different from ecosystems where you basically can't start without picking a framework first.

junior

What's the difference between an array and a slice, and where does the len versus cap distinction bite people?

An array has a fixed size baked into its type, so a 5 element array and a 10 element array are actually different types, and arrays copy by value. A slice is a small header with a pointer, a length, and a capacity that points into some underlying array, and it's what I actually use day to day. The gotcha is that appending to a slice can silently share the same backing array with another slice until it grows past capacity, so 2 slices can alias each other's data in ways that surprise people who assume slicing is a full copy.

How does Go represent strings, and what's the difference between a byte and a rune?

A Go string is just an immutable slice of bytes, and by convention that byte sequence holds UTF-8 encoded text. A byte is a single 8 bit value, while a rune is an int32 alias representing one Unicode code point, which can take up several bytes in UTF-8. If I index a string directly I get bytes, but if I range over it Go decodes it rune by rune, which matters the moment you touch non-ASCII text.

When do you use a pointer receiver versus a value receiver on a method?

I use a pointer receiver when the method needs to mutate the receiver, or when the struct is large enough that copying it on every call would be wasteful. I use a value receiver for small immutable-feeling types where copying is cheap and there's no need to modify the original. The practical rule I follow is to stay consistent across all methods on a type, because mixing the two gets confusing about whether you're working with a copy or the original.

Go doesn't have inheritance. How do you get code reuse between types instead?

Go uses composition through struct embedding instead of classical inheritance. If I embed one struct or interface inside another, the outer type automatically promotes the inner type's fields and methods, so from the outside it looks similar to inheritance. But there's no is-a relationship or polymorphic dispatch under the hood, it's really the compiler generating forwarding methods, which keeps relationships explicit instead of hiding a deep class hierarchy.

What is the empty interface, and how does 'any' relate to it?

interface{} is the interface with 0 methods, so literally every type in Go satisfies it, which makes it the closest thing Go has to a truly generic holds-anything value. any is just a type alias for interface{} added for readability once generics landed. I try to avoid it in normal code because you lose compile time type safety and need a type assertion or type switch to do anything useful with the value, so I mostly use it at real boundaries like decoding JSON into an unknown shape.

How do you get a concrete type back out of an interface value?

A type assertion checks whether the underlying value stored in the interface is a specific concrete type, and the comma ok form lets me check safely instead of panicking on a mismatch. When I need to branch across several possible types I use a type switch instead, which reads a lot cleaner than a chain of if-else assertions. I reach for a type switch anytime I'm handling more than 2 possible underlying types.

What does wrapping an error with %w actually do, and why bother?

fmt.Errorf with a %w verb wraps the original error inside a new one while preserving a reference to it, so I get a readable message with added context without throwing away the original error. That preserved chain is what lets errors.Is and errors.As walk back through the wrapped errors later. If I use %v instead of %w, I get the same string, but the underlying error is gone and callers can't unwrap it anymore.

What's the difference between errors.Is and errors.As?

errors.Is checks whether a specific sentinel error value appears anywhere in the wrapped error chain, so I use it for something like checking against a known not-found error. errors.As instead checks whether some error in the chain matches a given error type, and if it does it assigns that specific error into the target variable so I can pull fields off it, like a custom validation error with a field name. I think of Is as identity comparison and As as a type match that also walks the chain.

How does defer work, and when exactly are its arguments evaluated?

defer schedules a function call to run when the surrounding function returns, and multiple deferred calls run last in first out, so the most recently deferred call runs first. The important gotcha is that the arguments to a deferred call are evaluated immediately at the defer statement, not when it actually runs later, only the call itself is delayed. I use it constantly for cleanup like closing a file or unlocking a mutex right next to where the resource was acquired.

What does the select statement do with channels?

select lets a goroutine wait on multiple channel operations at once and proceeds with whichever one becomes ready first, kind of like a switch statement for channels. If several cases are ready at the same time it picks between them at random, which keeps things fair. I use it constantly for patterns like putting a timeout on a channel read, or fanning in results from several worker channels into one loop.

When do you reach for sync.WaitGroup versus sync.Mutex?

WaitGroup is for waiting until a group of goroutines finishes, I call Add before launching them, each goroutine calls Done when it's finished, usually deferred, and the main goroutine calls Wait to block until the count hits 0. Mutex does a completely different job, it protects a shared piece of state from being read and written by multiple goroutines at once, so I Lock before touching the shared data and Unlock right after, usually with defer. I think of WaitGroup as coordinating completion and Mutex as guarding access.

How do table-driven tests work in Go, and how is benchmarking different?

A table-driven test defines a slice of struct cases, each with an input and an expected output, then loops over them calling t.Run for each one so every case shows up as its own named subtest in the output. It keeps adding a new case as simple as adding a line to the table instead of writing a whole new test function. Benchmarking is a separate concern using testing.B and go test -bench, where the runtime calls the code repeatedly and reports time or allocations per operation, which I use specifically when I'm trying to prove a performance change actually helped.

senior

What problem do generics solve in Go, and when do you actually reach for them?

Before generics, writing a function that worked across multiple types meant either duplicating code per type or falling back to interface{} and losing type safety along with runtime type assertions. Generics let me write one function or type parameterized over a type parameter with constraints, so a Max function or a generic Stack works for any comparable or ordered type and the compiler still catches type errors. I only reach for them when I'm genuinely duplicating logic across types, not as a default style, because concrete types are usually more readable.

When is it appropriate to use panic and recover instead of returning an error?

I treat panic as reserved for truly unrecoverable programmer errors, like an invariant that should be impossible to violate, or startup failures where the program genuinely cannot continue. It's not meant as a general error handling mechanism the way exceptions are in other languages. recover only works inside a deferred function and lets you stop a panic from unwinding the whole goroutine, which I mostly use at the boundary of something like an HTTP handler so a single bad request doesn't crash the whole server.

What's the practical difference between a buffered and an unbuffered channel?

An unbuffered channel has 0 capacity, so a send blocks until a receiver is actually ready, which gives you a synchronization point between 2 goroutines, not just a data pipe. A buffered channel lets sends succeed without a waiting receiver until that buffer fills up, which is more about decoupling producer and consumer speed. I default to unbuffered because it makes handoff timing explicit, and only add a buffer when I have a real reason, like smoothing out bursts.

What is a data race in Go, and how do you actually find one?

A data race happens when 2 goroutines access the same memory at the same time and at least 1 of them is a write, with no synchronization protecting that access, and the result is genuinely undefined behavior, not just a rare bad value. I run tests and even the app itself with the -race flag, which instruments memory accesses and reports the exact 2 goroutines and stack traces involved the moment a race actually occurs during that run. It only catches races that are actually exercised, so I still design code to avoid shared mutable state in the first place rather than relying on the detector to catch everything.

How does Go's module system handle dependency versioning?

Go modules are declared in a go.mod file listing the module path and its required dependencies with specific versions, and go.sum pins the exact cryptographic hashes so builds stay reproducible. Go uses semantic versioning and minimum version selection, picking the lowest version that satisfies every requirement across the whole dependency graph rather than always grabbing the newest. The one sharp edge is that a major version bump past v1, like v2, has to change the import path itself, because Go treats different major versions as genuinely different packages.

staff

What is context.Context for, and how do you use it for cancellation?

context carries a cancellation signal, an optional deadline, and request scoped values down a call chain, so a long request can be cancelled cleanly instead of running to completion after nobody cares about the result anymore. I pass ctx as the first argument through every function on that call path, and functions doing real work select on ctx.Done() alongside their normal work so they can bail out early. A typical case is an HTTP handler using a timeout context so a slow downstream call doesn't hang the request forever.

How would you build a worker pool in Go to process a large batch of jobs?

I create a jobs channel and a results channel, then launch a fixed number of worker goroutines that all range over the same jobs channel, pulling work as they finish the previous item, which naturally load balances across workers. The producer side sends jobs onto the channel and closes it when done, since ranging over a channel exits cleanly once it's closed and drained. I size the worker count based on whether the work is CPU bound, roughly matching cores, or IO bound, where I can run far more concurrent workers than cores.

What does escape analysis have to do with Go's garbage collector?

Escape analysis is the compiler deciding at build time whether a value can live on the stack or whether it escapes, meaning some reference to it outlives the function, like being returned or stored somewhere shared, which forces it onto the heap instead. Stack allocations are basically free and get cleaned up automatically when the function returns, while heap allocations are what the garbage collector actually has to track and eventually reclaim. Go's collector is a concurrent mark and sweep collector that mostly runs alongside the program, so I care about escape analysis mainly when I'm trying to cut allocations in a hot path.

principal

How does the Go scheduler actually run goroutines on top of real OS threads?

Go uses what's called an M:N scheduler, mapping many goroutines onto a smaller set of OS threads through logical processors that GOMAXPROCS controls, roughly 1 per CPU core by default. The scheduler cooperatively switches goroutines at points like channel operations, function calls, and blocking system calls, plus it does preemption so a tight loop can't starve everything else forever. The upshot is I get concurrency without paying full OS thread costs for every context switch.

When would you say Go is the wrong choice for a project?

I'd steer away from Go for heavy data science or machine learning work, since the ecosystem and tooling there is overwhelmingly Python, or for a product that needs rich generic-heavy abstractions and complex domain modeling where something like Rust's type system actually earns its complexity. I'd also think twice for a small script or a quick one-off tool where a scripting language ships faster with less ceremony. Go earns its keep on networked services, CLIs, and infrastructure tooling where simplicity, fast builds, and easy concurrency actually pay off, it's not the best default for everything.

Fast recall

goroutine = lightweight concurrent function | channel = typed pipe between goroutines | slice = pointer, length, and capacity | interface = implicit method contract | defer = runs at function return | panic = unrecoverable runtime failure | recover = stops a panic mid-unwind | context = carries cancellation and deadline | mutex = guards shared state | race detector = flags unsynchronized access | escape analysis = decides stack or heap | go.mod = module and dependency versions | errors.Is = matches a sentinel error | select = waits on multiple channels

BH·Go·github.com/bunlongheng/study