Tutorial: Go Microservice
This tutorial takes you from an empty directory to a fully protected Go microservice with a nolapse coverage gate in GitHub Actions. It covers every command and shows the expected output at each step.
Time to complete: 30–45 minutes.
Part 1 — Create the Go Service
Section titled “Part 1 — Create the Go Service”1.1 Initialise the module
Section titled “1.1 Initialise the module”mkdir hello-svc && cd hello-svcgit initgo mod init github.com/example/hello-svc1.2 Write the service
Section titled “1.2 Write the service”Create handler.go:
package main
import ( "encoding/json" "net/http")
type HealthResponse struct { Status string `json:"status"`}
func healthHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(HealthResponse{Status: "ok"})}
type GreetResponse struct { Message string `json:"message"`}
func greetHandler(w http.ResponseWriter, r *http.Request) { name := r.URL.Query().Get("name") if name == "" { name = "world" } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(GreetResponse{Message: "Hello, " + name + "!"})}
func main() { http.HandleFunc("/health", healthHandler) http.HandleFunc("/greet", greetHandler) http.ListenAndServe(":8080", nil)}1.3 Write the tests
Section titled “1.3 Write the tests”Create handler_test.go:
package main
import ( "encoding/json" "net/http" "net/http/httptest" "testing")
func TestHealthHandler(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/health", nil) w := httptest.NewRecorder() healthHandler(w, req)
if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d", w.Code) } var resp HealthResponse json.NewDecoder(w.Body).Decode(&resp) if resp.Status != "ok" { t.Errorf("expected status ok, got %q", resp.Status) }}
func TestGreetHandler_default(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/greet", nil) w := httptest.NewRecorder() greetHandler(w, req)
var resp GreetResponse json.NewDecoder(w.Body).Decode(&resp) if resp.Message != "Hello, world!" { t.Errorf("unexpected message: %q", resp.Message) }}
func TestGreetHandler_named(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/greet?name=Alice", nil) w := httptest.NewRecorder() greetHandler(w, req)
var resp GreetResponse json.NewDecoder(w.Body).Decode(&resp) if resp.Message != "Hello, Alice!" { t.Errorf("unexpected message: %q", resp.Message) }}1.4 Run the tests
Section titled “1.4 Run the tests”go test -cover ./...Expected output:
ok github.com/example/hello-svc 0.003s coverage: 85.7% of statementsPart 2 — Add nolapse
Section titled “Part 2 — Add nolapse”2.1 Install the CLI
Section titled “2.1 Install the CLI”go install github.com/nolapse/nolapse-cli/nolapse-cli/cmd/nolapse@latestnolapse version# nolapse v0.1.02.2 Initialise the baseline
Section titled “2.2 Initialise the baseline”nolapse init --repo .Expected output:
measuring coverage...coverage: 85.71%baseline written to .audit/coverage/baseline.mdnolapse.yaml createdInspect the baseline:
cat .audit/coverage/baseline.mdcoverage: 85.71%timestamp: 2026-03-18T09:00:00Zcommit: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b22.3 Commit the baseline
Section titled “2.3 Commit the baseline”git add .audit/coverage/baseline.md nolapse.yaml handler.go handler_test.go go.modgit commit -m "chore: add nolapse coverage baseline"2.4 Run a coverage check
Section titled “2.4 Run a coverage check”nolapse run --repo .Expected output (nothing has changed, so delta is 0):
file baseline coverage PR coverage delta outcome. 85.71% 85.71% +0.00% pass
outcome: pass delta: +0.00 coverage: 85.71% baseline: 85.71%warn_threshold: 0.5 fail_threshold: 1.0Exit code is 0 — pass.
Part 3 — Simulate a Regression
Section titled “Part 3 — Simulate a Regression”3.1 Delete the named-greeting test
Section titled “3.1 Delete the named-greeting test”Open handler_test.go and remove TestGreetHandler_named entirely. Save the file.
3.2 Run nolapse again
Section titled “3.2 Run nolapse again”nolapse run --repo .Expected output:
file baseline coverage PR coverage delta outcome. 85.71% 71.43% -14.28% fail
outcome: fail delta: -14.28 coverage: 71.43% baseline: 85.71%warn_threshold: 0.5 fail_threshold: 1.0Coverage delta: -14.28% (threshold: 1.0%) FAILExit code is 1. nolapse detected the regression.
3.3 Fix the regression
Section titled “3.3 Fix the regression”Add TestGreetHandler_named back to handler_test.go. Re-run:
nolapse run --repo .file baseline coverage PR coverage delta outcome. 85.71% 85.71% +0.00% passExit code is 0 again.
Part 4 — Add the GitHub Action
Section titled “Part 4 — Add the GitHub Action”4.1 Create the workflow file
Section titled “4.1 Create the workflow file”Create .github/workflows/coverage.yml:
name: Coverage check
on: pull_request:
jobs: coverage: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0
- name: Set up Go uses: actions/setup-go@v5 with: go-version: "1.22"
- name: Run nolapse uses: nolapse/nolapse-action@v1 with: repo-token: ${{ secrets.GITHUB_TOKEN }} warn-threshold: "0.5" fail-threshold: "1.0"4.2 Commit and push
Section titled “4.2 Commit and push”git add .github/workflows/coverage.ymlgit commit -m "ci: add nolapse coverage gate"git push -u origin main4.3 Open a pull request
Section titled “4.3 Open a pull request”Create a feature branch, make a change, and open a PR. The Coverage check job will run automatically. If coverage drops more than 1 percentage point, the check fails and blocks the merge.
4.4 Enable branch protection (optional)
Section titled “4.4 Enable branch protection (optional)”Go to Settings → Branches on your GitHub repository. Add a branch protection rule for main, enable Require status checks to pass before merging, and add Coverage check / coverage as a required check. From this point on, no PR can merge unless nolapse passes.
What You Built
Section titled “What You Built”- A Go HTTP service with handler tests covering two endpoints
- A nolapse baseline committed to git
- A local verification loop:
nolapse run --repo . - A GitHub Actions workflow that fails PRs with coverage regressions
- Branch protection enforcing the check
Next Steps
Section titled “Next Steps”- Raise Your Coverage Threshold — tighten the threshold once the team is comfortable with the gate
- Monorepo Setup — extend nolapse to cover multiple Go services
- Add a Coverage Badge — show coverage status in the README