Go Advanced
A Complete 5-Chapter Course
Table of Contents
- Generics: Type Parameters and Constraints
- Error Wrapping: errors.Is, errors.As, and %w
- context.Context
- Testing: the testing Package and go test
- JSON and encoding/json
Generics: Type Parameters and Constraints
Chapter 5 (Fundamentals) showed that sum(numbers ...int) only works for int โ a separate function would be needed for float64, despite the logic being identical. Generics, added to Go relatively recently, solve exactly this: one function definition, usable with multiple types, fully checked at compile time. Unlike Intermediate Chapter 2's interfaces (which work via shared methods), generics work via shared capability โ "any type that supports +", for example.
The Problem Without Generics
These two functions are identical except for one type. Before generics, this duplication was simply accepted, or worked around with any (Intermediate Chapter 2) at the cost of losing type safety and needing manual type assertions.
A Generic Function with a Type Parameter
[T int | float64] declares T as a type parameter, constrained to either int or float64 โ T stands in for whichever concrete type is actually used at the call site. var total T uses Chapter 2's zero-value behaviour generically: it's 0 when T is int, 0.0 when T is float64, decided automatically. Go infers T from the argument โ there's no need to write Sum[int](...) explicitly in normal use.
Named Constraints
Inline constraints like int | float64 get repetitive across several generic functions, so they're usually pulled out into a named constraint interface instead โ an interface listing allowed underlying types rather than methods, reusable by name across the whole package.
comparable, satisfied by any type that supports == and != โ commonly used for generic functions that need to check for a specific value inside a collection, covered in this chapter's challenges.
A Generic Type โ Not Just a Generic Function
Structs (Intermediate Chapter 1) can be generic too: Stack[T any] declares a stack that works with whatever single type T is chosen at creation time โ Stack[int]{} here. Every method automatically carries the same T, so Push and Pop stay perfectly type-safe for whichever type was chosen, with no any-style runtime checking required anywhere inside.
any (Intermediate Chapter 2) gives up type information entirely โ the compiler can't help at all. Generics keep full compile-time type checking; Stack[int] and Stack[string] are still fully separate, type-safe usages, just generated from one shared definition.
| Approach | Type safety | Code reuse |
|---|---|---|
| Separate function per type | Full | None โ duplicated logic |
| any / interface{} | None at compile time | Full, but unsafe |
| Generics โ func F[T Constraint](...) | Full | Full |
Coding Challenges
Write a generic function Max[T int | float64](a, b T) T that returns the larger of two values. Call it once with two ints and once with two float64s, printing both results.
๐ View solutionWrite a generic function Contains[T comparable](items []T, target T) bool that returns true if target appears anywhere in items. Test it once with a []string and once with a []int.
๐ View solutionUsing the Stack[T any] type from this chapter, create a Stack[string], push 3 names onto it, then pop and print all 3 (which should come back out in reverse order).
๐ View solutionChapter 1 Quick Reference (Advanced)
- func F[T Constraint](x T) T { } โ a generic function with type parameter T
- [T int | float64] โ inline constraint: T must be one of these listed types
- type Name interface { typeA | typeB } โ a reusable, named constraint
- comparable โ built-in constraint for types supporting == and !=
- any โ the loosest constraint; equivalent to no constraint at all
- type Name[T any] struct { } โ a generic type; methods carry the same T automatically
- Generics โ any โ full compile-time type checking is preserved throughout
- Next chapter: error wrapping โ errors.Is, errors.As, and fmt.Errorf("%w", err)
Error Wrapping: errors.Is, errors.As, and %w
Fundamentals Chapter 5 introduced error and nil. In real programs, an error often passes through several layers of function calls before reaching code that decides what to do about it โ and each layer usually wants to add context ("failed while loading config" on top of "file not found"). Error wrapping is how Go adds that context without throwing away the original error underneath.
A Sentinel Error โ A Known, Comparable Error Value
A sentinel error โ a specific, named error value declared once at package level โ lets calling code check for that exact error later. This is the foundation everything else in this chapter builds on: a known error value that can be compared against.
Wrapping an Error with fmt.Errorf and %w
%w (used only with fmt.Errorf) wraps the original error inside a new one, attaching extra context ("loadItem: ") while keeping the original โ ErrNotFound โ retrievable from inside the new error. This differs from %v or %s, which would only produce a plain string with no way back to the original error value.
errors.Is โ Checking for a Specific Error Through Any Number of Wraps
err != ErrNotFound (a direct comparison) would actually return true here, since err is now the wrapped version, not the original โ direct comparison breaks once wrapping is involved. errors.Is(err, ErrNotFound) unwraps as many layers as necessary, checking whether ErrNotFound is anywhere inside the chain. This is the correct, wrap-aware way to ask "is this ultimately that specific error?"
fmt.Errorf("...: %w", err) even once, it is a brand-new error value โ == against the original sentinel will always be false. errors.Is exists specifically to make this comparison correctly regardless of how many wrapping layers exist.
Custom Error Types
Any type with an Error() string method automatically satisfies Go's built-in error interface (Intermediate Chapter 2's structural typing again, applied to a single specific method). A custom error type like ValidationError can carry structured data โ Field, Message โ rather than just a flat message string.
errors.As โ Extracting a Custom Error Type
errors.As checks whether an error (possibly wrapped) matches a specific custom error type, and if so, assigns it into valErr so its specific fields (Field, Message) become accessible โ something a plain error interface value alone never allows. errors.Is answers "is this that exact value?"; errors.As answers "is this that type, and if so, give it back to me properly typed."
| Tool | Question it answers |
|---|---|
| fmt.Errorf("...: %w", err) | Add context while preserving the original error |
| errors.Is(err, sentinel) | "Is this ultimately THIS specific error value?" |
| errors.As(err, &target) | "Is this (or something it wraps) of THIS type? Give it to me." |
| err == sentinel | Unsafe once wrapping is involved โ avoid |
Coding Challenges
Declare var ErrEmptyName = errors.New("name cannot be empty"). Write a function greet(name string) error that returns this sentinel (wrapped with fmt.Errorf and extra context "greet: %w") if name is "". Use errors.Is to check the result and print an appropriate message.
๐ View solutionDefine a custom error type RangeError with fields Min and Max ints, and an Error() string method describing the valid range. Write a function checkAge(age int) error returning a *RangeError if age is outside 0-120. Use errors.As to extract it and print Min/Max.
๐ View solutionWrite three functions that call each other: layerOne calls layerTwo, layerTwo calls layerThree, and layerThree returns a sentinel error ErrFailed. Each layer wraps the error with its own name using fmt.Errorf("...: %w", err). Print the final error from layerOne, then confirm errors.Is(err, ErrFailed) is still true despite 3 layers of wrapping.
๐ View solutionChapter 2 Quick Reference
- var ErrX = errors.New("...") โ a sentinel error, a known comparable value
- fmt.Errorf("context: %w", err) โ wraps err, adding context, keeping it unwrappable
- errors.Is(err, sentinel) โ true if sentinel appears anywhere in err's wrap chain
- errors.As(err, &target) โ extracts a specific custom error TYPE from the chain
- type X struct {} + func (e *X) Error() string โ defines a custom error type
- Never use == on a wrapped error โ it will not match, even when logically "the same"
- Next chapter: context.Context โ cancellation, timeouts, and request-scoped values
context.Context
Intermediate Chapter 3 showed goroutines running independently, with no automatic way for main() to know when they're done short of a channel or WaitGroup. context.Context solves a related but different problem: how does code running several layers deep โ goroutines, function calls, network requests โ find out that it should stop, because a timeout passed or the caller gave up? There's no real JavaScript equivalent to reach for here; the closest conceptual cousin is an AbortController, but Go's version is used far more pervasively.
The Basic Pattern โ Pass ctx as the First Parameter
By strong convention, ctx context.Context is always the first parameter of any function that supports cancellation โ never buried among other arguments. ctx.Done() returns a channel (Intermediate Chapter 3) that something closes the moment cancellation happens; select here waits on whichever of two channels is ready first, a new construct specifically for working with multiple channels at once.
context.WithTimeout โ Cancel Automatically After a Duration
context.Background() creates a brand-new, empty starting context โ the root every other context derives from. context.WithTimeout(parent, duration) wraps it with a deadline: after that duration, ctx.Done() closes automatically, and ctx.Err() reports context deadline exceeded. doWork here finishes via the timeout branch instead of its normal 2-second wait, since the 1-second deadline fires first.
WithTimeout (and its cousin WithCancel) both return a cancel function that must be called to release the context's internal resources, whether or not the timeout ever actually triggers. defer cancel() immediately after creating the context is the standard, safe habit โ skipping it can leak resources in long-running programs.
context.WithCancel โ Manual Cancellation
WithCancel hands back a cancel function with no automatic timer attached โ cancellation happens only when something explicitly calls it, from anywhere that has a reference to that function. This is the pattern used when one part of a program needs to tell every dependent goroutine to stop, on demand, rather than after a fixed duration.
Passing Request-Scoped Values
context.WithValue attaches a single key/value pair to a context, retrievable anywhere downstream via ctx.Value(key) โ most commonly used for request-scoped data in a web server (a user ID, a trace ID) that many unrelated layers of code might need without explicitly threading it through every function signature.
WithValue as a way to avoid adding parameters to a function โ resist this. It's intended specifically for cross-cutting, request-scoped data (tracing IDs, auth info), not as a general substitute for normal arguments, which stay clearer and more type-safe as actual parameters.
| Tool | Purpose |
|---|---|
| context.Background() | The root context everything else derives from |
| context.WithTimeout(parent, d) | Auto-cancels after duration d; always defer cancel() |
| context.WithCancel(parent) | Manual cancellation via the returned cancel function |
| context.WithValue(parent, key, val) | Attaches request-scoped data, retrieved with ctx.Value(key) |
| ctx.Done() / ctx.Err() | Channel that closes on cancellation; reports why |
Coding Challenges
Write a function countTo(ctx context.Context, n int) that counts from 1 to n, printing each number with a 300ms pause between them, but stops early and prints "Stopped early" if ctx is cancelled. Call it with a context.WithTimeout of 1 second and n = 10, so it stops before reaching 10.
๐ View solutionUsing context.WithCancel, start a goroutine that prints "Working..." every 200ms until cancelled. In main, let it run for about 1 second, then call cancel() and confirm (via a short Sleep and a printed message) that the goroutine stopped.
๐ View solutionDefine a contextKey type and a requestIDKey constant. Use context.WithValue to attach a request ID string to a context, then write a function logRequest(ctx context.Context) that reads it back with ctx.Value and prints it.
๐ View solutionChapter 3 Quick Reference
- ctx context.Context โ always the first parameter of a cancellation-aware function
- context.Background() โ the root context to start from
- context.WithTimeout(parent, duration) โ auto-cancels after a fixed time; always defer cancel()
- context.WithCancel(parent) โ manual cancellation via the returned function
- context.WithValue(parent, key, value) โ attaches request-scoped data
- ctx.Done() โ a channel that closes when the context is cancelled or times out
- ctx.Err() โ explains why: context.Canceled or context.DeadlineExceeded
- Next chapter: testing โ the testing package, table-driven tests, and go test
Testing: the testing Package and go test
Every chapter so far has tested code by reading fmt.Println output by eye โ fine for a 5-line example, unworkable for a real program with dozens of functions. Go's standard library includes a testing package, and the go command includes a built-in test runner, go test โ together, the standard way Go code gets verified automatically.
The File Naming Convention
A test file must be named *_test.go (here, math_test.go, testing code in math.go) โ this naming is how Go's tooling recognises test code and excludes it from a normal build. Each test function's name must start with Test and take exactly one parameter, *testing.T.
Running Tests with go test
go test automatically finds and runs every Test... function in *_test.go files, with no manual registration needed anywhere. -v shows each individual test's name and result, rather than just a single pass/fail summary line.
t.Errorf vs t.Fatalf
t.Errorf records a failure but lets the rest of the test function keep running โ useful when several independent checks are worth reporting together. t.Fatalf records a failure AND stops that test function immediately, appropriate when a later check would be meaningless or panic without the earlier one passing first (here, examining result wouldn't make sense if err turned out to be unexpectedly nil from a successful division).
Table-Driven Tests โ The Idiomatic Go Pattern
Rather than writing a separate test function per case, a table-driven test declares a slice of anonymous structs (Intermediate Chapter 1's struct knowledge, combined with Fundamentals Chapter 6's range) โ each one a distinct input/expected-output pair โ and loops over them with one shared assertion. Adding a new test case becomes a one-line addition to the table, rather than an entirely new function.
Subtests with t.Run
t.Run("name", func(t *testing.T) { ... }) wraps each table entry as its own named subtest โ go test -v then reports each case individually (TestAdd/2+3, TestAdd/-1+1, ...) instead of one combined pass/fail for the whole table, making it immediately clear which specific input failed.
go test -run TestAdd runs only tests whose name matches the given pattern โ useful for focusing on one failing test without re-running an entire, possibly slow, test suite.
| Concept | Convention |
|---|---|
| Test file name | name_test.go |
| Test function name | func TestXxx(t *testing.T) |
| Record failure, continue | t.Errorf(...) |
| Record failure, stop this test | t.Fatalf(...) |
| Run all tests | go test ./... |
| Verbose output | go test -v ./... |
Coding Challenges
Write a function IsEven(n int) bool, then write a test file with TestIsEven covering at least 3 cases (an even number, an odd number, and zero) using t.Errorf for any mismatch.
๐ View solutionRewrite Challenge 1's test as a table-driven test: a slice of struct { input int; want bool } with at least 5 cases, looped over with a single shared assertion.
๐ View solutionWrite a function SafeDivide(a, b float64) (float64, error) returning an error for division by zero. Write a table-driven test using t.Run for named subtests, covering a normal division and a divide-by-zero case, checking both the result/error appropriately for each.
๐ View solutionChapter 4 Quick Reference
- name_test.go โ required file naming for Go to recognise test code
- func TestXxx(t *testing.T) โ required test function signature
- t.Errorf(...) โ records a failure, test function keeps running
- t.Fatalf(...) โ records a failure, stops the test function immediately
- Table-driven tests โ a slice of struct cases looped with one shared assertion
- t.Run("name", func(t *testing.T) {...}) โ named subtests, reported individually
- go test ./... / go test -v ./... โ run all tests, verbosely
- This completes Go Advanced (Course 3) as currently planned. A possible Chapter 5 would cover JSON and encoding/json for working with APIs.
JSON and encoding/json
Fundamentals Chapter 10 used response.json() in JavaScript without much ceremony โ JSON parses directly into a plain object there, since JavaScript objects and JSON already share the same shape. Go's static typing (Fundamentals Chapter 2) means converting to and from JSON needs an explicit step: marshaling (struct โ JSON) and unmarshaling (JSON โ struct), both handled by the standard library's encoding/json package.
Marshaling โ Struct to JSON
json.Marshal returns the JSON as a []byte slice, plus an error โ the same Fundamentals Chapter 5 pattern used throughout the language. string(data) converts those raw bytes into a printable string. Note that Name and Age appear capitalised in the JSON output by default โ only capitalised (exported) struct fields are visible to encoding/json at all.
Struct Tags โ Controlling the JSON Output
The backtick-delimited text after each field is a struct tag โ metadata read by encoding/json at runtime. json:"name" renames the field for JSON purposes (lowercase, matching typical API conventions); omitempty drops the field entirely from the output when it holds its zero value (Fundamentals Chapter 2) โ here, Email disappears since it was never set.
data, _ := json.Marshal(p) uses the blank identifier (Fundamentals Chapter 4) to explicitly throw away the error return value โ acceptable in a quick example, but real code should always check it properly, the same way err != nil is checked everywhere else in Go.
Unmarshaling โ JSON to Struct
json.Unmarshal goes the other direction: raw JSON bytes into a struct. The destination is passed as a pointer (&p) โ exactly Intermediate Chapter 1's pointer-receiver lesson applied here: without the &, Unmarshal would only be able to fill in a throwaway copy, and p back in the caller would remain empty.
json.Unmarshal(jsonData, p) (no &) compiles and runs without panicking in many cases, but p never actually gets populated โ Unmarshal needs a pointer specifically so it can write through to the real variable, the same plain-vs-pointer-receiver distinction from Intermediate Chapter 1.
Fetching JSON from a Real API
This is the complete realistic pattern: http.Get fetches the response, io.ReadAll reads its full body into bytes, json.Unmarshal parses those bytes into a struct. defer resp.Body.Close() (Intermediate Chapter 3's defer) guarantees the response body is closed once the function returns, success or failure โ the direct Go equivalent of JavaScript Fundamentals Chapter 10's fetch + response.json() pair.
| JavaScript | Go |
|---|---|
| JSON.stringify(obj) | json.Marshal(value) |
| JSON.parse(text) | json.Unmarshal(bytes, &target) |
| No equivalent โ keys match property names | `json:"key"` struct tag controls the JSON key name |
| await response.json() | io.ReadAll(resp.Body) + json.Unmarshal(body, &target) |
Coding Challenges
Define a struct Product with fields Name (string), Price (float64), and InStock (bool), with json tags using lowercase key names. Create one, marshal it with json.Marshal, and print the resulting JSON string.
๐ View solutionGiven a raw JSON string `{"name":"Laptop","price":999.99,"inStock":true}`, define a matching struct with appropriate json tags, unmarshal it, and print each field.
๐ View solutionWrite a function fetchUser(id int) (User, error) that fetches from https://jsonplaceholder.typicode.com/users/{id}, unmarshals into a User struct with at least Name and Email fields (with json tags), properly closing the response body with defer. Call it and print the result, handling any error.
๐ View solutionChapter 5 Quick Reference
- json.Marshal(value) โ struct/value โ JSON []byte, plus an error
- json.Unmarshal(bytes, &target) โ JSON []byte โ struct; target MUST be a pointer
- Only capitalised (exported) struct fields are visible to encoding/json
- `json:"key"` โ struct tag controlling the JSON field name
- `json:"key,omitempty"` โ omits the field from output if it's the zero value
- http.Get(url) + io.ReadAll(resp.Body) + json.Unmarshal โ the full fetch-and-parse pattern
- defer resp.Body.Close() โ always close a response body once done with it
- This completes Go Advanced (Course 3) and the Go course overall as currently planned across all 3 courses.