Go Learning Roadmap: From Syntax to a Tested HTTP Service

Follow a ten-week, project-led Go plan that turns basic syntax into a tested local task service, with clear weekly gates and recovery rules.

KnowledgeGate Team

Exam prep & CS education

Updated 10 Sep 20266 min read

Go syntax tutorials can make sense while a service you can test, cancel, profile, and explain still feels out of reach. Use one task-api project as the syllabus instead of studying disconnected features. Give it 8 hours a week. The finish line is a tested local HTTP service with evidence, not a claim of production readiness.

Set the ten-week finish line before learning more syntax

Use the same weekly time box throughout the roadmap:

Session

Time

Tuesday

90 minutes

Thursday

90 minutes

Saturday

3 hours

Sunday

2 hours

That is 90 + 90 + 180 + 120 = 480 minutes, or 8 hours. Spend roughly 25 percent, about 2 hours, reading and 75 percent, about 6 hours, typing, testing, or debugging. The C++ staged learning path uses a similar milestone rhythm. Go needs different evidence: one task service must exercise interfaces, cancellation, race detection, profiling, and HTTP lifecycle controls.

The contract is fixed: POST /tasks, GET /tasks/{id}, and PATCH /tasks/{id}; JSON responses; malformed-input rejection; slow-work cancellation; unit and handler tests; go test -race ./...; /healthz; shutdown within 2 seconds of SIGTERM; and a README with run and test commands.

A ten-week Go roadmap from syntax and a task CLI through HTTP, cancellation, race checks, and profiling to a final load check.

Weeks 1 and 2: learn syntax by shipping a small CLI

Learn variables, zero values, if, for, slices, maps, structs, functions, multiple returns, packages, explicit errors, and pointers only for mutation. Also learn encoding/json with os.ReadFile and os.WriteFile because the CLI needs state that survives between runs. Every concept must change the project.

Each command is a separate process, so an in-memory store would start empty on every run and task list could never see what task add created. Back the CLI with a tasks.json file instead: every invocation loads the file, applies one change, and writes the file back. A missing file means an empty store; unreadable JSON is an explicit error. The next ID is the highest stored ID plus one, so numbering stays stable across runs.

task add "Profile Go service" prints created id=1; a separate task list run reloads the file and prints 1 | false | Profile Go service; task complete 1 prints completed id=1; the next list prints 1 | true | Profile Go service. A blank title prints title is required and exits with code 2 without touching the file. Completing ID 99 prints task 99 not found and exits with code 1.

Week 1 ends when go run ./cmd/task builds, task add followed by task list works across two separate runs, and you can explain Task{ID int, Title string, Done bool} and its JSON tags. Week 2 ends with three working commands, cmd/task and internal/tasks packages, and no changes from gofmt.

Weeks 3 and 4: use interfaces to make behaviour testable

Define Store beside its consumer: Create(title string) (Task, error), Get(id int) (Task, error), and Complete(id int) (Task, error). It has three methods because the service needs three behaviours.

Write table-driven tests for four cases: creating "Profile Go service" yields {ID:1, Title:"Profile Go service", Done:false}; creating "" yields ErrTitleRequired; completing ID 99 yields ErrNotFound; and completing ID 1 sets Done:true. The interface now has two implementations: the Week 2 JSON-file store and a new in-memory store for tests. The same service tests therefore run fast and never touch the disk. The software quality guide separates verification, validation, and defect evidence. The gate is named assertions for public behaviour and errors, with go test ./... passing, not a vanity coverage number.

Weeks 5 and 6: turn the same use cases into an HTTP contract

Build thin net/http handlers that decode JSON and call the service. The server is one long-running process, so wire it to the in-memory store: state now lives in the process between requests, while the JSON file remains the CLI's backend. Use the DNS and HTTP guide to revise methods, status codes, headers, and request-response boundaries.

Carry one task through every layer. POST /tasks with {"title":"Profile Go service"} returns 201, Content-Type: application/json, and {"id":1,"title":"Profile Go service","done":false}. GET /tasks/1 returns 200 with that object. PATCH /tasks/1 with {"done":true} returns 200 and {"id":1,"title":"Profile Go service","done":true}. GET /tasks/99 returns 404 with {"error":"task not found"}; malformed JSON returns 400 with {"error":"invalid JSON"}.

Test all five cases with httptest, asserting exact statuses and decoded bodies. Add request-ID and access-log middleware only after those contract tests pass.

Weeks 7 and 8: add cancellation, concurrency safety, and lifecycle controls

Make context.Context the first argument to every store method and pass r.Context() from handlers. Against a 300 ms fake store, a 150 ms request timeout must return 504 with {"error":"request timed out"} without waiting for slow work.

Launch 20 goroutines that create 10 tasks each, expecting 20 × 10 = 200 unique IDs. Run go test -race ./..., protect the in-memory store's map and next-ID counter with sync.RWMutex, then rerun until the race disappears and 200 IDs remain unique.

Add /healthz, returning 200 with {"status":"ok"}. On SIGTERM, stop accepting work, allow at most 2 seconds for in-flight requests, then exit. These are production-minded controls, not proof of production readiness.

Week 9: measure before optimising

Create BenchmarkList1000 with task-0000 through task-0999, exactly 1,000 tasks. Run go test -bench=BenchmarkList1000 -benchmem ./internal/tasks; record observed ns/op, B/op, and allocs/op. Profile the same workload, change one thing based on its actual top function, and rerun. Fill a before-and-after table only with measurements. "No worthwhile improvement" is valid.

Log request_id, method, path, status, and duration_ms. For GET /tasks/1, use request ID req-0001 and status 200, but take duration from the real run.

Week 10: run a bounded service-readiness check

Seed 1,000 tasks, then send 200 requests at concurrency 10: 120 successful GETs, 40 POSTs, 20 PATCHes, and 20 deliberate GET /tasks/99999 misses. The arithmetic is 120 + 40 + 20 + 20 = 200 total; 120 + 40 + 20 = 180 successes; the remaining 20 are intentional 404 responses. Expect zero unexpected 5xx responses and record observed latency without inventing a universal threshold.

Keep evidence for passing tests and race checks, invalid-JSON and missing-ID contracts, 150 ms cancellation against the 300 ms store, /healthz at 200, 2-second shutdown, no committed secret, and README commands for build, run, benchmark, and load check. The service is production-minded. Durable database-backed storage behind the HTTP service, authentication, an observability backend, deployment, and incident response remain.

Recover from missed days without restarting the roadmap

If Tuesday's 90-minute session is missed, add 45 minutes to Thursday and 45 to Sunday. The revised week is 0 + 135 + 180 + 165 = 480 minutes, still 8 hours. If two sessions are missed, protect the tested milestone and drop a stretch task such as middleware polish. Never borrow more than 2 hours from the following week.

At Weeks 2, 4, 6, 8, and 10, take 20 minutes to rebuild cleanly, run all tests, explain the newest concept in three sentences, and note one failing case. Repeat only a failed milestone.

The short version and next step

The short version is one project, 8 hours a week, a demonstrable gate every two weeks, and evidence before optimisation. Explore the Coding & Skills category for broader programming paths. If software hiring assessments are also in your plan, Coding for Placements is an adjacent option, but it does not currently cover Go.