170 companion flashcards · AI-assisted study content · Open the deck →
This deck introduces Go, the statically typed and compiled language developed at Google that's known for its simplicity, strong concurrency support, and fast build times. The cards walk you through the fundamentals of the language, from declaring variables and working with slices, maps, and structs, to understanding pointers, methods, and interfaces. Beyond the basics, you'll also explore the topics that make Go distinctive — goroutines, channels, buffered versus unbuffered communication, the select statement, and Go's idiomatic approach to error handling and packaging code.
It's a good fit whether you're completely new to Go and want a structured way to get started, a developer coming from another language who wants to pick up Go's conventions quickly, or someone preparing for a technical interview or certification. Because Go has a fairly small surface area, the cards focus on the core building blocks you'll use every day rather than niche corners of the standard library, so the time you spend here will pay off across most Go projects.
To get the most out of studying, try writing a small program after each batch of cards — even a few lines using the concept you just reviewed will cement it far better than passive reading. Concurrency in particular (goroutines, channels, and select) is best absorbed in short, repeated sessions rather than a single long cramming block, so space your reviews out and revisit those cards a few times across the week. Finally, when a card covers something like error handling or the defer keyword, pause and predict how a real program would behave before flipping the answer; that active recall is what turns these prompts into lasting familiarity with the language.
Go (often called Golang) is an open-source, statically typed, compiled language developed at Google by Robert Griesemer, Rob Pike, and Ken Thompson. It is designed around simplicity, strong concurrency support, and predictable performance, which is why it has become a common choice for backend services, CLI tools, and infrastructure software. Every Go source file begins with a package declaration, and a program becomes an executable only when it lives in package main and defines func main(). Capitalization controls visibility within this packaging: identifiers starting with an uppercase letter are exported and accessible from other packages, while lowercase identifiers remain private to their package. Beyond ordinary imports, Go supports blank imports import _ "pkg" that run side effects (commonly used to register database drivers), aliased imports such as import f "fmt", and dot imports that bring names into the current namespace. Files can also be conditionally compiled using build constraints like //go:build linux && amd64 or filename suffixes such as _linux.go and _amd64.go.
Go's basic type system is intentionally small. The numeric types span signed and unsigned integers of multiple widths (int, int8...int64, and their unsigned counterparts), floats (float32, float64), and complex numbers (complex64, complex128). The aliases byte for uint8 and rune for int32 handle byte and Unicode code-point work. Every type has a deterministic zero value: numeric types are 0, bool is false, strings are "", and reference-like types (pointers, slices, maps, channels, interfaces, functions) are nil. This makes Go programs easier to reason about because variables always start in a known state. Variables can be declared explicitly with var x int = 10, with inferred types using var y = "hello", or with the short form z := 42 inside functions. For repeated constant values, the iota keyword inside const blocks produces successive integer values, which is the idiomatic way to write enumerations.
Pointers in Go are restricted to safe use: &x takes the address of x, and *p dereferences a pointer to read or write the underlying value. Unlike C, Go disallows pointer arithmetic, so pointers cannot wander outside the bounds of allocated memory. The distinction between new(T) and make(T, args) is also important: new allocates zero-valued memory and returns a *T, while make is reserved for slices, maps, and channels, returning an initialized value (not a pointer). Go programs depend on packages through Go modules. A module is initialized with go mod init module-name, and its dependencies live in go.mod with checksums in go.sum. Running go mod tidy synchronizes those files with the actual imports; go mod vendor copies dependencies into a vendor/ directory for reproducible or air-gapped builds. go.work files, introduced in Go 1.18, let developers work on multiple modules together without replace directives. The retract directive can mark a published version as unsafe to use, and -trimpath removes local filesystem paths from binaries for reproducible production builds.
Slices are the most common composite type in Go. A slice is a flexible, dynamically sized view into an underlying array, and its declaration []int{1, 2, 3} creates one with both length and capacity set. The built-in append adds elements, growing the slice when needed by allocating a new backing array; the runtime typically doubles small slices and grows larger ones by roughly 25% to amortize the cost of repeated reallocation. Two descriptors govern a slice: len(s) reports the number of accessible elements, and cap(s) reports how many elements fit before the next growth. There is a subtle but important distinction between a nil slice (var s []int) and an empty slice (s := []int{} or make([]int, 0)): most operations behave identically, but json.Marshal renders the first as null and the second as []. Assignment between slices shares the underlying array, so changes are visible across both names, while copy(dst, src) duplicates elements into independent storage up to min(len(dst), len(src)). Two-dimensional data is built by composing slices of slices, since Go does not have dynamic multidimensional arrays.
Maps are unordered key-value collections created with make(map[K]V) or with composite literals. Reading a missing key returns the zero value of the value type rather than panicking, but the comma-ok idiom v, ok := m["key"] distinguishes a present-but-zero entry from a missing one. The built-in delete(m, "key") removes an entry (or does nothing if the key is absent), and it is safe to call during iteration. Map elements are not addressable, so &m["key"] is a compile-time error and updates must be done with a plain assignment. Iteration order is intentionally randomized to discourage reliance on it; if deterministic output is required, sort the keys first. Maps are not safe for concurrent use, so access from multiple goroutines requires a mutex or a channel-based protocol.
Structs group related fields under a named type, and Go encourages composition over inheritance through struct embedding. Embedding a type anonymously promotes its fields and methods to the outer struct, so Dog{Animal{Name: "Rex"}, "Labrador"} lets callers write d.Name directly. This is a powerful alternative to classical inheritance and keeps the type hierarchy flat. Arrays, by contrast, have a fixed size baked into their type ([3]int versus [4]int are different types), so slices are overwhelmingly more common in idiomatic Go; a slice can be obtained from an array with the slice expression arr[1:3]. Sorting is handled by the sort package and, since Go 1.21, the slices package: sort.Ints covers built-in numeric types, sort.Slice accepts a custom less function (but is not stable), and sort.SliceStable preserves the relative order of equal elements. The strings package complements these collections with utilities like strings.Builder for efficient concatenation, strings.Repeat for repeated copies, and strings.SplitN for splitting into at most N substrings.
Functions in Go are first-class values: they can be assigned to variables, passed as arguments, returned from other functions, and stored in maps. A variadic parameter func sum(nums ...int) appears inside the function as a []int, and callers can either spread individual arguments or pass a slice with s.... Higher-order functions such as sort.Slice, http.HandlerFunc, and many generic helpers accept function arguments, while anonymous function literals (add := func(a, b int) int { return a + b }) are commonly passed inline to go, defer, and callback APIs. A closure is a function literal that captures variables from its enclosing scope, which is why the classic counter factory returns a function that retains and updates n across calls.
Methods extend a named type by attaching a receiver. The choice between a value receiver (t T) and a pointer receiver (t *T) has concrete consequences: a value receiver operates on a copy and cannot mutate the original, while a pointer receiver modifies the actual value and avoids copying large structs. Because of how method sets are defined, the method set of *T includes both pointer-receiver and value-receiver methods, but the method set of T only includes value-receiver methods. This asymmetry affects interface satisfaction and is the reason receivers should be consistent within a type. A method value such as t.Add binds the method to a specific receiver, producing a closure-like value that can be passed around, while a method expression like (*Time).Add produces a function whose first parameter is the receiver.
Interfaces are Go's primary abstraction mechanism, and unlike Java or C# they are satisfied implicitly. A type satisfies an interface simply by implementing all of its methods; no implements keyword is required. This structural approach encourages small, focused interfaces such as io.Reader and fmt.Stringer. The empty interface interface{} (aliased as any since Go 1.18) is satisfied by every type, making it the universal container, but extracting the concrete value requires a type assertion v, ok := i.(string) or a type switch switch v := i.(type). Compile-time interface satisfaction can be enforced with a blank assignment such as var _ io.Reader = (*MyType)(nil). A subtle pitfall involves typed-nil interfaces: assigning a nil pointer to an interface variable does not produce a nil interface, so callers should check the underlying pointer explicitly. Finally, Go 1.18 added generics, allowing type-parameterized functions and types like func Map[T any, U any](s []T, f func(T) U) []U, which dramatically reduces the need for code generation or interface{}-based containers.
Go deliberately avoids exceptions in favor of explicit error returns. Functions that can fail return an error as their last result, and callers are expected to handle it immediately: result, err := doSomething(); if err != nil { ... }. The error type is itself a built-in interface with a single method, Error() string, so any type implementing that method can serve as an error. Custom errors are typically declared as structs and exposed through a pointer-receiver Error method, while convenience helpers such as errors.New("msg") and fmt.Errorf("wrap: %w", err) cover the common cases. Error wrapping with %w preserves the original cause and enables the inspection helpers errors.Is(err, target), which walks the chain looking for a match, and errors.As(err, &target), which finds the first error assignable to a target type. These patterns let libraries expose rich, structured error information while keeping call sites uncluttered.
Panics represent unrecoverable conditions and propagate up the call stack until a deferred function invokes recover(). Idiomatic Go uses panics sparingly—mostly for true programmer errors or impossible states—rather than for ordinary control flow. A recovered panic can be inspected with if r := recover(); r != nil { ... }, and the program can continue if appropriate. The defer keyword is the partner of recover: it schedules a function call to run after the surrounding function returns, with deferred calls executing in last-in-first-out order. Common uses include closing files, unlocking mutexes, flushing writers, and registering panic-safe cleanup. Because deferred functions still run during a panic propagation, they are the natural place to call recover().
Go's fmt package standardizes formatted output with a set of verbs. %v gives the default representation, %+v includes struct field names, %#v produces a Go-syntax literal, and %T prints the type. Numeric verbs cover integers (%d), floats (%f), strings (%s), and pointer addresses (%p). When a type implements fmt.Stringer—that is, a String() string method—fmt.Println and the %v verb automatically use that method, which is the idiomatic way to give custom types a readable representation. The init function func init() runs automatically before main and is useful for setup that must happen regardless of which entry point runs, such as registering drivers or validating package-level configuration. Package-level variables are initialized in dependency order, then init functions in each file execute in lexical filename order (and, within a file, in declaration order) before main finally begins.
Goroutines are Go's lightweight concurrency primitive. The go keyword starts a function as a goroutine that is multiplexed onto OS threads by the Go runtime, and each starts with only a few kilobytes of stack that grows on demand. Because goroutines are so cheap, programs routinely spawn tens of thousands of them. The runtime uses an M:N scheduler that maps M goroutines onto N OS threads, with logical processors (Ps) holding run queues and engaging in work stealing to keep cores balanced. GOMAXPROCS limits how many OS threads may execute Go code simultaneously and defaults to runtime.NumCPU(); it bounds parallelism, not concurrency. Go distinguishes between concurrency (structuring a program to handle many tasks) and parallelism (executing them simultaneously), and asynchronous preemption since Go 1.14 prevents a tight goroutine loop from starving the scheduler.
Channels are the primary way goroutines communicate safely. A channel is created with make(chan T) for an unbuffered channel or make(chan T, n) for a buffered one. Unbuffered channels synchronize sender and receiver: a send blocks until a receiver is ready and vice versa, so they act as a rendezvous point. Buffered channels allow sends up to capacity without blocking, only blocking when the buffer is full. Channels can be directional—chan<- T for send-only or <-chan T for receive-only—often used in function signatures to document intent. Only the sender should close a channel; receivers detect closure via the comma-ok idiom v, ok := <-ch or by ranging over the channel until it drains. A closed channel always yields the zero value immediately, while a nil channel blocks forever, and that contrast is used to dynamically disable select cases by assigning nil.
The select statement lets a goroutine wait on multiple channel operations at once, running the first case that becomes ready and falling through to default if none are. Combined with channels, select underpins many concurrency patterns: pipelines that chain producer and consumer stages; fan-out/fan-in designs that distribute work across multiple goroutines and merge results; worker pools that read jobs from a shared channel with a fixed number of goroutines; and coordination with context.Context for cancellation and deadlines. sync.WaitGroup waits for a collection of goroutines, sync.Mutex and sync.RWMutex protect shared state, sync.Once ensures one-time initialization, sync.Pool caches temporary objects to reduce allocations, and sync/atomic provides lock-free primitives like CompareAndSwap. The errgroup package from golang.org/x/sync ties multiple goroutines to a shared context and reports the first error, while singleflight collapses duplicate concurrent calls for the same key to prevent cache stampedes. Avoiding goroutine leaks requires cancelling blocked goroutines via context or by ensuring channels are eventually closed; otherwise they sit forever, wasting memory.
Go's I/O model centers on two interfaces: io.Reader with Read(p []byte) (n int, err error) and io.Writer with Write(p []byte) (n int, err error). Files, network connections, buffers, and HTTP bodies all satisfy them, allowing uniform streaming code. io.Copy(dst, src) is the idiomatic way to pump data between any reader and writer using an internal buffer, while io.ReadAll(r) slurps an entire io.Reader into memory. The bufio package adds buffering on top: bufio.NewReader reduces syscall frequency for reads, bufio.NewWriter batches writes that should be flushed before the writer is closed, and bufio.Scanner provides a convenient line-by-line interface. File I/O is similarly straightforward with os.ReadFile and os.WriteFile for whole-file operations, and os.Create combined with file Write methods for streaming writes. Cross-platform path manipulation is handled by path/filepath (OS-aware with filepath.Join and filepath.Separator), while the path package works on slash-separated paths suitable for URLs.
The encoding packages give Go strong support for structured data. encoding/json is the most widely used: json.Marshal converts a value to []byte, json.Unmarshal parses a byte slice into a struct, while json.NewEncoder and json.NewDecoder stream JSON through an io.Writer or io.Reader. Struct tags such as json:"name,omitempty" control field naming and behavior, json:"-" excludes a field, and dec.DisallowUnknownFields() enforces strict decoding. The companion packages encoding/binary, encoding/hex, and encoding/base64 cover fixed-width binary I/O with endianness control, hexadecimal encoding, and base64 variants for safe transport (URL-safe URLEncoding and unpadded RawURLEncoding for tight formats). The strings package provides additional utilities such as strings.Repeat and strings.SplitN, while strings.Builder avoids the O(n²) cost of repeated concatenation.
Go's standard library makes it trivial to stand up an HTTP service. A handler implements http.Handler with ServeHTTP(http.ResponseWriter, *http.Request), while http.HandlerFunc adapts a plain function to that interface. http.NewServeMux builds a request router, and http.ListenAndServe starts the server; for production use, construct an &http.Server with explicit timeouts. Headers must be set on w.Header() before any body is written or w.WriteHeader(code) is called, since they become immutable once the response starts. The client side offers http.Get for simple calls and http.NewRequest combined with client.Do(req) for fully customized requests, with defer resp.Body.Close() and full-body reads being essential to avoid connection leaks. Time handling is provided by time.Time (an instant with nanosecond precision), time.Duration (an int64 alias for nanoseconds, with constants like time.Second and time.Hour), and the reference layout "Mon Jan 2 15:04:05 MST 2006" used by Format and Parse. The context package ties everything together: context.WithTimeout and context.WithCancel create cancellable contexts that propagate deadlines and cancellation through function calls and HTTP requests, allowing long-running goroutines to be shut down cleanly.
Testing is a first-class concern in Go. Test files end in _test.go and import the testing package; tests are functions named TestXxx(t *testing.T) and are run with go test ./.... Failures use t.Error, t.Errorf, t.Fatal, or t.Fatalf, and subtests are created with t.Run. A common idiom is the table-driven test, which iterates over a slice of input/expected pairs and runs each as a subtest, producing clear failure output. The shared interface testing.TB underlies both *testing.T and *testing.B, so helpers can work in tests and benchmarks alike. t.Helper() marks a function as a helper so failure messages report the caller's line, and t.Cleanup(func()) registers teardown scoped to the test and its subtests. Benchmarks follow the same pattern with func BenchmarkX(b *testing.B) running a loop b.N times that the framework adjusts automatically; b.ResetTimer discards setup time, and b.RunParallel measures performance across GOMAXPROCS goroutines.
The Go toolchain is small and focused. go build compiles packages and discards intermediate binaries (or produces one in the current directory for main), while go install places executables in $GOPATH/bin. go run compiles and executes in one step, go test runs the test suite, go fmt (or gofmt) enforces a single canonical code style, and go vet flags suspicious constructs. go mod init, go mod tidy, go mod vendor, and go mod verify manage dependencies and checksums, while go get adds dependencies and (since Go 1.18) go install pkg@version installs CLI tools directly from module paths. For reproducible production binaries, -trimpath strips local paths and the build cache ($GOCACHE/build) speeds up repeated compilations and can be cleared with go clean -cache. Race conditions can be detected at runtime with go test -race or go run -race, which instruments memory accesses with ThreadSanitizer at the cost of higher CPU and memory overhead.
Beyond the surface language, several runtime and interop topics are essential for advanced Go work. The garbage collector is a concurrent, tri-color mark-and-sweep collector with sub-millisecond pauses since Go 1.8, tuned by the GOGC environment variable (default 100). The Go memory model defines the happens-before relationships that synchronization primitives establish—channel sends happen before the corresponding receives complete, and Unlock happens before the next Lock; without such relationships, concurrent behavior is undefined. Two escape hatches exist for low-level code: unsafe.Pointer, a generic pointer type that bypasses Go's type system, and uintptr, an integer large enough to hold a pointer bit pattern that the GC does not track. cgo enables calling C code from Go through the import "C" pseudo-package but adds build complexity and disables some tooling. Finally, the embed package, introduced in Go 1.16, lets files be compiled directly into a binary with //go:embed, making it easy to ship templates, configuration, and static assets without runtime filesystem dependencies. Together, these tools and abstractions give Go a remarkable balance of simplicity, performance, and operational reliability.
go mod init module-name. The go.mod file tracks dependencies and their versions. Use go mod tidy to clean up.fmt verbs:%v — default format%+v — struct with field names%#v — Go syntax representation%T — type of value%d — integer%s — string%f — float%p — pointer addressfunc Map[T any, U any](s []T, f func(T) U) []U {
result := make([]U, len(s))
for i, v := range s {
result[i] = f(v)
}
return result
}a := b makes a and b share the same underlying array — changes to one are visible in the other.copy(dst, src) copies elements into independent backing storage. The copy size is min(len(dst), len(src)).\x (2 hex digits), \u (4 hex digits, Unicode), \U (8 hex digits), \nnn (octal)."\u00e9" → "é"os.ReadFile(name) opens the file, reads it fully, and closes it — returns []byte.io.ReadAll(r) reads fully from any io.Reader — useful for http.Response.Body, strings.Reader, etc. They are otherwise similar in result.http.Post(url, contentType, body) is a convenience wrapper that builds a POST request internally and uses http.DefaultClient.http.NewRequest(method, url, body) returns a request you can customize (headers, context) before sending via client.Do(req).import . "math" brings exported names into the current namespace so they can be used without a prefix: Pi instead of math.Pi. Avoid in production code — it hurts readability and risks collisions.golang.org/x/sync/singleflight collapses multiple concurrent calls for the same key into a single function call — useful for cache stampede prevention:v, err, shared := sf.Do("key", func() (any, error) { return expensive() })Drill this topic
170 flashcards on Go Programming — free, no signup needed to start.
Study Go Programming flashcardsLearnWiki pages are generated with AI assistance from LearnCoachAssist's reviewed study catalog and may contain errors — verify anything critical against your course materials.