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.

Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.

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.

📋 Quick Reference — net/http

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.

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.