Top 50 Go (Golang) Interview Questions & Answers (2026)
About Go (Golang)
Top 50 Go (Golang) interview questions covering goroutines, channels, interfaces, concurrency patterns, and idiomatic Go programming. Companies hiring for Go (Golang) roles test this knowledge at every stage — from first-round screens that check the basics to final-round conversations about real-world trade-offs.
What to Expect in a Go (Golang) Interview
Expect a mix of conceptual and practical Go (Golang) questions: clear definitions and core-concept checks for junior roles, hands-on scenario questions for mid-level roles, and architecture or trade-off discussions for senior roles. Interviewers usually move from foundational topics toward the kind of problems you'd actually face on the job.
How to Use This Guide
Work through the Go (Golang) questions in order — Beginner, then Intermediate, then Advanced — so each concept builds on the last. Every question has its own page; bookmark the ones that trip you up and revisit them the day before your interview.
Curated by Tech Baithak Editorial Team · Last updated: June 2026
Core concepts every Go (Golang) developer must know.
01
What is Go and who created it?
Go (also called Golang) is a statically typed, compiled programming language designed at Google by Robert Griesemer, Rob Pike, and Ken Thompson, first announced in 2009 and reaching version 1.0 in 2012. It was designed to address shortcomings in C++ and Java — specifically slow compile times, verbose syntax, and poor concurrency support. Go compiles to a single self-contained binary with no external dependencies, making deployment extremely simple. It is widely used at Google, Uber, Docker, and Kubernetes, particularly for backend services and cloud infrastructure.
02
What is a goroutine in Go?
A goroutine is a lightweight, concurrently executing function managed by the Go runtime. You start one by prefixing a function call with the go keyword: go myFunc(). Unlike OS threads, goroutines start with a tiny stack (about 2 KB) that grows dynamically, so you can launch thousands or even millions of them without exhausting memory. The Go scheduler multiplexes goroutines onto OS threads automatically. Goroutines are the fundamental unit of concurrency in Go and are central to idiomatic Go design.
03
What is a channel in Go?
A channel is a typed conduit for communication between goroutines, following Go's philosophy: "Do not communicate by sharing memory; instead, share memory by communicating." Create a channel with ch := make(chan int). Send a value with ch <- 42 and receive with val := <-ch. By default, channels are unbuffered — a send blocks until a receiver is ready, and vice versa. Buffered channels (make(chan int, 10)) allow up to N sends without a corresponding receive, decoupling the sender and receiver.
04
What is the defer statement in Go?
The defer statement schedules a function call to be executed just before the surrounding function returns, regardless of whether it returns normally or panics. Deferred calls are stacked in LIFO (last-in, first-out) order — the last defer in a function runs first. It is the idiomatic Go way to handle cleanup: defer file.Close(), defer mutex.Unlock(), defer rows.Close(). Because deferred calls run even when a panic occurs (combined with recover), they are also used for cleanup during error recovery.
05
How are packages and imports used in Go?
Go code is organized into packages — every .go file begins with a package declaration. The main package is special: it defines an executable program with its main() entry point. Other packages are libraries. Import external packages with the import block: import ("fmt" "net/http"). Only identifiers starting with an uppercase letter are exported (public). The Go toolchain enforces that every imported package is actually used — unused imports cause a compile error. Package management is handled by Go modules (go.mod).
06
What is a struct in Go?
A struct is Go's primary way to group related data together into a composite type. Declare one with type Person struct { Name string; Age int } and instantiate it with p := Person{Name: "Alice", Age: 30}. Structs can have methods attached to them, making them the closest Go equivalent to classes. Unlike class-based OOP, Go uses struct embedding for composition rather than inheritance. Struct fields are exported (accessible outside the package) if they start with an uppercase letter.
07
What is an interface in Go?
An interface in Go defines a set of method signatures that a type must implement. Unlike Java or C#, Go uses implicit interface satisfaction — a type implements an interface simply by having all the required methods, with no explicit implements keyword. For example, any type with a String() string method implements the built-in fmt.Stringer interface. This design allows loose coupling between packages and enables powerful patterns like dependency injection and mocking in tests without any framework.
08
How do pointers work in Go?
A pointer in Go holds the memory address of a value. Get the address of a variable with the & operator: p := &x. Dereference a pointer (access the value it points to) with the * operator: *p = 42. Go has no pointer arithmetic — you cannot increment or manipulate pointer addresses as in C. Passing a pointer to a function allows the function to modify the original value rather than a copy. Go's garbage collector tracks pointer reachability, so unlike C, you never manually free pointer memory.
09
How does error handling work in Go?
Go handles errors by returning them as values rather than using exceptions. Functions that can fail return an error as their last return value: f, err := os.Open("file.txt"). The caller checks if err != nil { ... } immediately. The built-in error interface requires only one method: Error() string. This explicit approach makes error paths visible in the code and forces the programmer to consciously decide how to handle each failure. Custom error types can carry additional context beyond a simple message.
10
What is the difference between a slice and an array in Go?
An array in Go has a fixed length that is part of its type: [5]int and [3]int are different types. Arrays are value types — assigning an array copies all its elements. A slice is a dynamic, flexible view over an underlying array described by a pointer, a length, and a capacity: []int. Slices are reference types — multiple slices can share the same backing array. Append to a slice with append(s, elem), which grows the backing array automatically when capacity is exceeded. In practice, slices are used almost exclusively; raw arrays are rare.
11
How do maps work in Go?
A map in Go is a built-in hash map (dictionary) type: m := make(map[string]int). Set values with m["key"] = 1 and read with val := m["key"]. To distinguish between a missing key and a zero value, use the two-value form: val, ok := m["key"] — ok is false if the key is absent. Delete a key with delete(m, "key"). Maps are not safe for concurrent use — use a sync.RWMutex or sync.Map when multiple goroutines access a map simultaneously.
12
How does the range loop work in Go?
The range keyword iterates over arrays, slices, maps, strings, and channels. For slices and arrays it yields the index and value: for i, v := range slice { ... }. For maps it yields the key and value. For strings it yields the byte position and the Unicode code point (rune). For channels it receives values until the channel is closed. Discard either variable with the blank identifier: for _, v := range slice or for i := range slice. Range is idiomatic Go for any iteration over built-in collection types.
13
What are multiple return values in Go and why are they useful?
Go functions can return multiple values natively: func divide(a, b float64) (float64, error). This is the idiomatic way to return both a result and an error without needing out-parameters, wrapper types, or exceptions. It makes error handling explicit at each call site. Named return values are also supported: func stats() (min, max int), which can improve documentation clarity. Multiple returns are also used in map lookups (val, ok := m[key]) and type assertions (v, ok := x.(Type)).
14
What is the blank identifier (_) in Go?
The blank identifier _ is a write-only variable that discards any value assigned to it. It signals to the compiler and readers: "I am intentionally ignoring this value." Common uses include discarding unwanted return values (_, err := fmt.Println("hi")), ignoring a loop variable (for _, v := range items), and importing a package solely for its side effects (import _ "database/sql/driver"). Go's strict "no unused variables" rule would cause a compile error for a named variable that is never read, so _ is the sanctioned escape hatch.
15
How do constants and iota work in Go?
Constants in Go are declared with const and must have a value known at compile time: const Pi = 3.14159. The iota identifier is a special counter used in const blocks that starts at 0 and increments by 1 for each constant in the block. It enables elegant enumerations: const (Sunday = iota; Monday; Tuesday ...). Combined with expressions, iota can create bitflag constants (1 << iota) or skip values. Each new const block resets iota to zero.
16
What is the init function in Go?
The init() function is a special function that runs automatically before main() — it is called by the Go runtime after all variable initializations in the package are complete. A package can have multiple init() functions across multiple files; they all run in the order the files are presented to the compiler. Common uses include registering drivers (sql.Register), validating configuration, setting up global state, and initializing caches. You cannot call init() explicitly — it is only invoked by the runtime.
17
What is the difference between go run and go build?
go run file.go compiles and immediately executes the specified Go file(s) without saving a binary to disk — useful for quick scripts and experimentation. go build compiles the package and writes a binary executable to disk (the current directory by default, named after the module or directory). The binary can then be deployed and executed independently with no Go toolchain required on the target machine. go install is similar to go build but also moves the binary into $GOPATH/bin (or $HOME/go/bin), making it available system-wide.
18
What is the difference between GOPATH and Go modules?
GOPATH was the original Go workspace model where all Go code had to live under a single directory tree ($GOPATH/src/github.com/user/project), making dependency management fragile and requiring all developers to use the same directory structure. Go modules (introduced in Go 1.11, default since 1.16) replaced GOPATH with a project-local go.mod file that declares the module path, Go version, and all versioned dependencies. With modules you can work in any directory, and each project has its own isolated, reproducible dependency graph locked by go.sum.
19
What are zero values in Go?
In Go, every variable is automatically initialized to its zero value if no explicit value is given — there is no concept of an "uninitialized" variable. Zero values by type: int → 0, float64 → 0.0, bool → false, string → "" (empty string), pointers/maps/slices/channels/functions → nil, structs → each field set to its own zero value. This guarantee eliminates entire classes of bugs caused by reading uninitialized memory and is a deliberate, principled design decision in Go.
20
What is a type declaration in Go?
A type declaration creates a new named type based on an existing type: type Celsius float64. The new type has the same underlying representation but is a distinct type — you cannot accidentally assign a Fahrenheit to a Celsius without an explicit conversion, preventing unit confusion bugs. You can attach methods to named types: func (c Celsius) ToFahrenheit() Fahrenheit { return Fahrenheit(c*9/5 + 32) }. This is how Go achieves type-safe domain modeling without heavyweight class hierarchies.
Practical knowledge for developers with hands-on experience.
01
What is the context package and why is it important?
The context package provides a standard way to carry deadlines, cancellation signals, and request-scoped values across API boundaries and goroutines. A context.Context is passed as the first argument to functions that do I/O or long-running work: func FetchUser(ctx context.Context, id int) (*User, error). When a request times out or is cancelled upstream, the context is cancelled, and well-behaved functions propagate that cancellation immediately, freeing resources. Without context, a cancelled HTTP request would still cause downstream goroutines to keep running, wasting CPU and connections.
02
What is sync.Mutex and when do you use it?
sync.Mutex is a mutual exclusion lock that protects shared data from concurrent access by multiple goroutines. Call mu.Lock() before accessing shared state and mu.Unlock() after — typically with defer mu.Unlock() immediately after locking to ensure it releases even if the function panics. Use sync.RWMutex when reads are far more frequent than writes — RLock()/RUnlock() allows multiple concurrent readers while still providing exclusive write access. The race detector (go test -race) catches Mutex misuse in tests.
03
What is sync.WaitGroup in Go?
sync.WaitGroup is used to wait for a collection of goroutines to finish. Call wg.Add(n) before launching goroutines to set the counter, wg.Done() (typically via defer wg.Done()) inside each goroutine when it completes, and wg.Wait() in the coordinating goroutine to block until the counter reaches zero. A common pattern: for _, item := range items { wg.Add(1); go func(i Item) { defer wg.Done(); process(i) }(item) }; wg.Wait(). Always call Add before launching the goroutine to avoid a race between launching and waiting.
04
What is sync.Once in Go?
sync.Once guarantees that a function is executed exactly once, no matter how many goroutines call it concurrently. Its Do(f func()) method runs f only on the first call — subsequent calls do nothing and block until the first invocation completes. This makes it the idiomatic way to implement lazy singleton initialization: var once sync.Once; var instance *DB; func GetDB() *DB { once.Do(func() { instance = connect() }); return instance }. It is safe without any additional locking.
05
What is the select statement in Go?
The select statement lets a goroutine wait on multiple channel operations simultaneously, executing whichever case is ready first. If multiple cases are ready, one is chosen at random — providing fair scheduling. If no case is ready and there is a default clause, the select executes the default immediately (non-blocking select). Without a default, select blocks until at least one case can proceed. Common uses: implementing timeouts (case <-time.After(5*time.Second)), combining a data channel with a done/cancel channel, and fan-in patterns.
06
What are buffered channels and when are they useful?
A buffered channel has an internal queue of a fixed capacity: ch := make(chan int, 10). A send to a buffered channel only blocks when the buffer is full, and a receive only blocks when the buffer is empty. This decouples the sender and receiver, allowing bursts of messages to be queued without requiring a receiver to be immediately ready. Common use cases: worker pools where jobs are queued for a fixed number of workers, rate limiting, and semaphore patterns. The built-in len(ch) returns the current number of queued items and cap(ch) returns the capacity.
07
What is a closure in Go?
A closure is a function value that captures and carries references to variables from its enclosing scope. In Go, functions are first-class values — they can be assigned to variables, passed as arguments, and returned from functions. A closure "closes over" the variables it references: func counter() func() int { n := 0; return func() int { n++; return n } }. Each call to counter() creates a new n with its own closure. A common pitfall in loops: all goroutines in a loop may share the same loop variable unless it is passed as an argument or a local copy is made.
08
What is struct embedding in Go?
Struct embedding is Go's mechanism for composition — embedding one struct inside another to promote its methods and fields without explicit delegation. Declare an embedded field without a name: type Employee struct { Person; Department string }. Now Employee automatically has all of Person's methods and fields accessible directly: emp.Name instead of emp.Person.Name. The embedded type's methods are promoted to the outer type's method set, which is how Go achieves interface satisfaction through composition. The embedded field can still be accessed explicitly by its type name.
09
What are type assertions and type switches in Go?
A type assertion extracts the concrete value from an interface: s, ok := val.(string). If ok is false, the assertion failed and s is the zero value — the two-value form prevents a panic. A type switch dispatches over multiple possible concrete types in one statement: switch v := x.(type) { case string: ...; case int: ...; default: ... }. Type switches are the idiomatic way to handle heterogeneous data stored as interface{} or any, common when processing JSON, ASTs, or plugin systems.
10
What is reflection in Go and when should you use it?
The reflect package lets you inspect and manipulate values, types, and struct tags at runtime. reflect.TypeOf(x) returns the type and reflect.ValueOf(x) returns a reflected value. Reflection is used internally by encoding packages (encoding/json, encoding/xml) to inspect struct field names and tags like json:"name". It is also used for ORM libraries and dependency injection frameworks. However, reflection is slow, bypasses static type checking, and makes code harder to read — prefer generics or interface-based designs and use reflection only when the type is genuinely unknown at compile time.
11
What is table-driven testing in Go?
Table-driven testing is the idiomatic Go pattern for writing tests. You define a slice of structs, each representing one test case with inputs and expected outputs, then loop over them calling t.Run(tc.name, func(t *testing.T) { ... }) for each. This makes it trivial to add new test cases without duplicating test logic and clearly documents what each case tests. The testing package is built into Go's standard library — no framework needed. Run tests with go test ./... and get verbose output with -v. Table-driven tests also work seamlessly with the race detector.
12
What does the -race flag do in go test?
go test -race runs your tests with Go's built-in race detector enabled. The race detector instruments memory accesses at compile time and dynamically detects when two goroutines concurrently access the same memory location without synchronization, printing a detailed report with goroutine stack traces. It adds roughly 5–10x runtime overhead and 2x memory usage, so it is not used in production but is invaluable in CI pipelines. Always run -race on concurrent code — it catches subtle data races that would otherwise cause intermittent, hard-to-reproduce bugs in production.
13
What is go generate in Go?
go generate scans Go source files for special comments of the form //go:generate command args and runs those commands when you execute go generate ./.... It is not part of the regular build — you run it manually and commit the generated output. Common uses include generating mock implementations from interfaces (e.g., mockgen), generating string methods for enum types (e.g., stringer), generating protobuf Go code from .proto files, and generating boilerplate from templates. The generated files are real Go source that the compiler then uses normally.
14
What are build tags in Go?
Build tags (also called build constraints) conditionally include or exclude a file from compilation. The modern syntax (Go 1.17+) uses a comment at the top of the file: //go:build linux && amd64. Boolean operators (&&, ||, !) and parentheses are supported. Common uses: OS-specific implementations (//go:build windows), architecture-specific code, including integration test files only with a specific tag (//go:build integration), and excluding files that depend on unavailable native libraries. Run go build -tags integration ./... to include tagged files.
15
How do io.Reader and io.Writer work in Go?
io.Reader and io.Writer are two of Go's most important interfaces. Reader has one method: Read(p []byte) (n int, err error). Writer has: Write(p []byte) (n int, err error). The power lies in composability — any type implementing Reader (files, HTTP response bodies, network connections, buffers, gzip decompressors) can be passed to any function that accepts io.Reader. The standard library's io.Copy, bufio.Scanner, json.NewDecoder, and many others all work with these interfaces. This design lets you build pipelines without knowing the concrete source or destination.
16
How do you build an HTTP server in Go using net/http?
Go's standard library net/http package makes building HTTP servers straightforward. Register handlers with http.HandleFunc("/path", handler) where a handler is func(w http.ResponseWriter, r *http.Request). Start the server with http.ListenAndServe(":8080", nil). For more control, create a custom http.ServeMux (router) and an http.Server struct with configurable timeouts (always set ReadTimeout, WriteTimeout, IdleTimeout to prevent resource exhaustion). For production services, most teams use a lightweight router like chi or gorilla/mux on top of the standard server.
17
How does JSON encoding and decoding work in Go?
The encoding/json package serializes Go values to JSON and back. json.Marshal(v) encodes a Go value to a []byte of JSON. json.Unmarshal(data, &v) decodes JSON bytes into a Go value. Struct field names are used as JSON keys by default; customize them with struct tags: json:"user_name,omitempty". For streaming large JSON (HTTP bodies, files), prefer json.NewDecoder(r.Body).Decode(&v) and json.NewEncoder(w).Encode(v) which work with io.Reader/io.Writer and avoid loading the full payload into memory.
18
What is a goroutine leak and how do you detect it?
A goroutine leak occurs when a goroutine is started but never terminates — usually because it is blocked waiting on a channel or lock that will never be signalled. Over time, leaked goroutines accumulate, consuming memory and potentially causing the program to run out of resources. Common causes: sending to an unbuffered channel with no receiver, forgetting to cancel a context passed to a goroutine, and HTTP server handlers that never time out. Detect leaks using the goleak library in tests, runtime.NumGoroutine() for monitoring, or pprof's goroutine profile (/debug/pprof/goroutine) in a running server.
19
What is panic and recover in Go?
panic stops normal execution and begins unwinding the stack, running deferred functions as it goes. If a panic reaches the top of the goroutine's stack without being recovered, the program crashes with a stack trace. recover() can only be called inside a deferred function — it intercepts the panic and returns the panic value, allowing the program to continue. HTTP servers commonly use this pattern in middleware: defer func() { if r := recover(); r != nil { log.Error(r); w.WriteHeader(500) } }(). Use panic only for truly unrecoverable programmer errors, not for expected runtime errors (use the error return for those).
20
What is fmt.Errorf with %w and how do errors.Is / errors.As work?
Go 1.13 introduced error wrapping. fmt.Errorf("loading user: %w", err) wraps an existing error in a new one with additional context while preserving the original error in the chain. errors.Is(err, target) checks whether any error in the chain matches target (using == equality or the error's Is method) — useful for checking sentinel errors like sql.ErrNoRows. errors.As(err, &target) finds the first error in the chain that can be assigned to target's type, enabling access to fields on custom error types. Together they replace the fragile string-matching approach to error inspection.
Deep expertise questions for senior and lead roles.
01
Explain the Go scheduler and the GMP model.
Go's runtime uses a GMP model — G (goroutine), M (OS thread / machine), P (logical processor / scheduler context). Each P has a local run queue of goroutines and is bound to one M at a time. The number of Ps is set by GOMAXPROCS (defaults to the number of CPU cores), limiting true parallelism. The scheduler is cooperative/preemptive: goroutines yield at function calls and since Go 1.14 can be asynchronously preempted via signals, preventing a CPU-bound loop from monopolizing a thread. When a goroutine blocks on a syscall, its M detaches from the P so the P can pick up another M and continue running other goroutines — this is how Go achieves high concurrency even with blocking I/O.
02
How does Go's garbage collector work?
Go uses a concurrent tri-color mark-and-sweep garbage collector. It divides objects into three sets: white (not yet seen), grey (reachable but children not yet scanned), and black (reachable with all children scanned). The GC starts with roots (globals, stack variables), marks them grey, then iteratively moves grey objects to black while colouring their children grey, until no grey objects remain. White objects at the end are unreachable and swept. The mark phase runs concurrently with the program (using write barriers to maintain correctness), minimizing stop-the-world pauses to sub-millisecond levels — critical for latency-sensitive services. Tune GC pressure with the GOGC environment variable.
03
What is escape analysis in Go?
Escape analysis is a compile-time analysis that determines whether a variable can safely live on the goroutine's stack or must be heap-allocated. Stack allocation is vastly cheaper — it requires no GC, no synchronization, and is cache-friendly. If a variable's address is taken and passed to a function that stores it beyond the current stack frame, or if it is returned from a function, it "escapes" to the heap. Run go build -gcflags="-m" to see the compiler's escape decisions. Writing allocation-free hot paths (avoiding unnecessary heap escapes) is a key Go performance optimization. Common escapes: interface conversions, closures capturing variables, and values returned as pointers.
04
What are generics in Go and how do they work?
Generics (type parameters) were introduced in Go 1.18 and allow writing functions and types that work with any type satisfying a given constraint. Syntax: func Map[T, U any](s []T, f func(T) U) []U. Constraints are interfaces that restrict which types are accepted — the built-in comparable allows ==, and the golang.org/x/exp/constraints package provides Integer, Ordered, etc. You can also write inline union constraints: [T int | float64]. Generics are instantiated at compile time, producing specialized code per type (monomorphization), with some inlining to control binary size. They eliminate many uses of interface{} and unsafe type assertions.
05
How do you profile a Go program with pprof?
The net/http/pprof package exposes profiling endpoints when imported: import _ "net/http/pprof". Access /debug/pprof/ in a browser or use go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30 for a 30-second CPU profile. Use go tool pprof http://.../heap for memory allocations. The interactive pprof shell lets you run top, list funcName, and web (generates a flame graph in the browser). For benchmarks, add -cpuprofile and -memprofile flags. Key metrics: CPU time by function, heap in-use bytes, goroutine count, and allocation rates. Profiling is essential before any optimization — measure first, then fix.
06
What is the unsafe package and when is it appropriate to use it?
The unsafe package lets you bypass Go's type system and memory safety guarantees. unsafe.Pointer can be converted to and from any pointer type or uintptr, enabling pointer arithmetic. unsafe.Sizeof, unsafe.Offsetof, and unsafe.Alignof expose low-level memory layout information. Legitimate uses include: high-performance zero-copy conversion between []byte and string, calling C code via CGo, implementing lock-free data structures, and interoperating with binary protocols. It is rarely needed in application code and must be used with extreme care — the GC does not track uintptr values, so they can become dangling references if the GC moves objects (not currently an issue in Go, but a correctness concern for future GC designs).
07
How does CGo work in Go?
CGo enables Go programs to call C code (and vice versa) by embedding C declarations in a special comment block immediately followed by import "C". Go types and C types are bridged via the pseudo-package C: C.int, C.CString, C.free. The Go compiler generates the necessary glue code and links against the C library. However, CGo has significant costs: it disables cross-compilation (the target machine needs a C toolchain), calls across the Go-C boundary are ~4x slower than pure Go calls (context switching between the Go scheduler and OS threads), and the binary becomes larger and harder to deploy. Prefer pure Go alternatives and use CGo only when wrapping an existing C library with no Go equivalent.
08
What is the Go plugin system?
Go supports building shared libraries as plugins using go build -buildmode=plugin, producing a .so file. The main program loads it at runtime with plugin.Open("myplugin.so"), then looks up exported symbols with p.Lookup("MyFunc") and type-asserts to the expected function or variable type. This allows dynamically loading functionality without recompiling the main binary — useful for extensible applications and driver models. Significant limitations: plugins only work on Linux and macOS, both the plugin and host must be compiled with the same Go toolchain version and module paths, and there is no way to unload a plugin. In practice, many teams prefer interface-based extension points or GRPC-based plugin systems (like HashiCorp's go-plugin) for better portability.
09
How does cross-compilation work in Go?
Go supports cross-compilation natively by setting two environment variables: GOOS (target operating system: linux, windows, darwin, android, etc.) and GOARCH (target architecture: amd64, arm64, 386, etc.) before running go build. Example: GOOS=linux GOARCH=arm64 go build -o myapp-linux-arm64 ./cmd/myapp. No separate cross-compiler toolchain is needed for pure Go code — the standard library includes implementations for all supported platforms. CGo breaks this: cross-compiling with CGo requires a C cross-compiler for the target. Go supports over 40 OS/architecture combinations, making it one of the best languages for building multi-platform binaries in CI/CD pipelines.
10
What is the Go module proxy and how does workspace mode work?
The Go module proxy (GOPROXY, defaulting to proxy.golang.org) is a caching HTTP server that serves module versions. When you run go get, Go fetches modules from the proxy rather than directly from version control, providing immutability guarantees (modules cannot be deleted once published), faster downloads, and a checksum database (sum.golang.org) that cryptographically verifies module contents against a transparency log. Go workspace mode (introduced in Go 1.18 via go work init) allows you to work across multiple modules simultaneously — changes in a local dependency module are immediately visible without publishing a new version. A go.work file lists the local module directories to use, and the workspace takes precedence over the module proxy for those modules.