Web

By the end of this lesson you'll be able to write a real HTTP server in Go with net/http — handle routes, read query and path parameters, return JSON, and set status codes and headers — using nothing but the standard library.

Part of the free Go course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

1️⃣ Your First Server: Handlers & ListenAndServe

A Go web server is built from two pieces. A handler is a function that takes a http.ResponseWriter (you write the reply into it) and a *http.Request (everything about the incoming call). You map a URL path to a handler with http.HandleFunc , then call http.ListenAndServe(":8080", nil) to start listening. Read this worked example first, then run it.

With the server running, open a second terminal and send it a request. curl is a command-line HTTP client — think of it as a browser you can script. Here's the request and the exact response your server returns:

2️⃣ Reading Query & Path Parameters

Most routes need input from the caller. A query parameter comes after the ? in the URL — read it with r.URL.Query().Get("name") , which returns "" if it's missing. A path parameter is part of the path itself; since Go 1.22 you capture it with a { placeholder } in the pattern and read it with r.PathValue("id") .

Your turn. The program below answers /square?n=5 , but two pieces are missing. Fill in each ___ using the // 👉 hints, then run it and send the request in the comment.

3️⃣ Returning JSON, Status Codes & Headers

Real APIs speak JSON. Define a struct with struct tags like to control the JSON keys, then write it with json.NewEncoder(w).Encode(value) . Two rules matter: set Content-Type to application/json before writing the body, and always check the error that Encode returns so a failure becomes a clean 500 instead of a half-written reply.

Now finish a JSON handler yourself. Three pieces are blanked out — the content type, the encoder package, and the route registration. Fill them in:

4️⃣ Routing with http.ServeMux

Passing nil to ListenAndServe uses a hidden global router. For anything real, create your own with http.NewServeMux() — it keeps every route in one place and is easy to test. Since Go 1.22 you can put the method in the pattern, like "GET /api/ping" , so a handler only runs for the method you intend. Set explicit status codes with w.WriteHeader(http.StatusCreated) — also before the body.

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — net/http

Task

Go Syntax

Register a route

http.HandleFunc("/path", fn)

Handler signature

func(w http.ResponseWriter, r *http.Request)

Start the server

http.ListenAndServe(":8080", mux)

Write text

fmt.Fprintf(w, "Hi %s", name)

Query param

r.URL.Query().Get("name")

Path param

r.PathValue("id")

Set a header

w.Header().Set("Content-Type", "application/json")

Set status code

w.WriteHeader(http.StatusCreated)

Write JSON

json.NewEncoder(w).Encode(data)

New router

mux := http.NewServeMux()

Frequently Asked Questions

Mini-Challenge: A Tiny Status API

No blanks this time — just a brief and an outline. Build a one-route JSON API from scratch: define the struct, register the handler, set the header, encode (and handle the error), and start the server. Run it and check your response against the comment.

🎉 Course Complete — You Built Web Services in Go!

Practice quiz

What is the correct Go HTTP handler signature?

  • func(r *http.Request) http.Response
  • func(req, res)
  • func(w http.ResponseWriter, r *http.Request)
  • func(ctx context.Context)

Answer: func(w http.ResponseWriter, r *http.Request). You write the reply into w; r holds the incoming request.

Which call maps a URL path to a handler function?

  • http.HandleFunc("/path", fn)
  • http.Route("/path", fn)
  • http.Map("/path", fn)
  • http.Get("/path", fn)

Answer: http.HandleFunc("/path", fn). HandleFunc registers a path-to-handler mapping.

Why is http.ListenAndServe usually the last line of main?

  • It returns immediately
  • It only sets up routes
  • It runs in a goroutine automatically
  • It blocks, running the server loop until the process stops

Answer: It blocks, running the server loop until the process stops. ListenAndServe blocks while serving requests.

How do you read a query parameter from /greet?name=Sam?

  • r.PathValue("name")
  • r.URL.Query().Get("name")
  • r.Param("name")
  • r.Query.name

Answer: r.URL.Query().Get("name"). Query().Get returns "" if the key is missing.

Since Go 1.22, how do you read a {id} path parameter?

  • r.PathValue("id")
  • r.URL.Query().Get("id")
  • r.Path("id")

Answer: r.PathValue("id"). Patterns like /users/{id} expose r.PathValue("id").

When must you set the Content-Type header?

  • After encoding the body
  • Anytime before the handler returns
  • Before the first write to the ResponseWriter
  • It is set automatically and cannot change

Answer: Before the first write to the ResponseWriter. Headers are sent on the first write; later changes are ignored.

Which writes a struct to the response as JSON?

  • w.Write(json(value))
  • json.NewEncoder(w).Encode(value)
  • fmt.Fprint(w, value)
  • json.Print(w, value)

Answer: json.NewEncoder(w).Encode(value). NewEncoder(w).Encode streams JSON straight to the response.

What do struct tags like control?

  • Validation rules
  • Database columns
  • The field's Go type
  • The JSON key names for each field

Answer: The JSON key names for each field. Without tags, Go uses the exported Go field names as keys.

How do you send a 201 Created status before the body?

  • w.Status(201)
  • w.WriteHeader(http.StatusCreated)
  • return 201
  • w.Header().Set("Status", "201")

Answer: w.WriteHeader(http.StatusCreated). Call WriteHeader before writing the body; afterwards it is locked in.

Why prefer http.NewServeMux() over passing nil to ListenAndServe?

  • It is faster at runtime
  • nil is deprecated
  • It keeps routes explicit and is easy to test
  • It enables HTTPS automatically

Answer: It keeps routes explicit and is easy to test. A custom mux avoids the hidden global router and centralizes routing.

Continue this course