#Building a Web Server in Go with net/http

Elliot Forbes Elliot Forbes · May 27, 2026 · 5 min read

Building a web server is one of the best first projects when learning a new language — it touches routing, request handling, and the standard library all at once.

In Go, the net/http package makes this surprisingly straightforward. If you’ve used Node’s Express or Python’s Flask, you’ll notice familiar patterns.

We’ll build up from a minimal handler, add a mutex-guarded counter, serve static files, and finally secure the server with HTTPS.

Prerequisites

  • You will need Go 1.21 or later installed on your development machine.

Creating a Basic Web Server

We’ll start with a server that echoes back the URL path of each request — a clean baseline to build on.

package main

import (
    "fmt"
    "html"
    "log"
    "net/http"
)

func main() {

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
    })

    http.HandleFunc("/hi", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hi")
    })

    log.Fatal(http.ListenAndServe(":8081", nil))

}

We define two handlers with http.HandleFunc. Each handler matches a URL pattern and writes a response using fmt.Fprintf.

http.ListenAndServe starts the server on port 8081 and blocks. Wrapping it in log.Fatal ensures any startup error is logged and the process exits.

Running Our Server

Run the server with go run main.go, then open http://localhost:8081/world in your browser.

You should see the URL path echoed back — /world — confirming the handler is working correctly.

Adding a bit of Complexity

HTTP handlers in Go run concurrently, so shared state needs protection. Here we increment a counter on each request, guarded by a sync.Mutex.

Note: If you want to learn more about mutexes, check out the Go Mutex Tutorial.

package main

import (
    "fmt"
    "log"
    "net/http"
    "strconv"
    "sync"
)

var counter int
var mutex = &sync.Mutex{}

func echoString(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "hello")
}

func incrementCounter(w http.ResponseWriter, r *http.Request) {
    mutex.Lock()
    counter++
    fmt.Fprintf(w, strconv.Itoa(counter))
    mutex.Unlock()
}

func main() {
    http.HandleFunc("/", echoString)

    http.HandleFunc("/increment", incrementCounter)

    http.HandleFunc("/hi", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hi")
    })

    log.Fatal(http.ListenAndServe(":8081", nil))

}

Without the mutex, two simultaneous requests could read and write counter at the same time, producing incorrect results.

Navigate to http://localhost:8081/increment and refresh a few times — the count should increase by one on each request.

Serving Static Files

Create a static/ folder in your project and add an index.html file inside it.

<html>
  <head>
    <title>Hello World</title>
  </head>
  <body>
    <h2>Hello World!</h2>
  </body>
</html>

http.ServeFile maps an incoming request URL to a file path on disk.

package main

import (
    "fmt"
    "log"
    "net/http"
)

func main() {

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        http.ServeFile(w, r, r.URL.Path[1:])
    })

    http.HandleFunc("/hi", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hi")
    })

    log.Fatal(http.ListenAndServe(":8081", nil))

}

Note: The pattern r.URL.Path[1:] strips the leading slash to form a relative path. Be careful with user-supplied paths — validate or sanitise them to avoid serving unintended files outside your project directory.

Checking it Works

Run the server and navigate to http://localhost:8081/index.html — you should see your HTML file rendered in the browser.

Serving Content from a Directory

As your project grows, keeping HTML files alongside main.go becomes unwieldy. Moving them into a static/ directory is cleaner.

- main.go
- static/
- - index.html
- - styles/
- - - style.css
- - images/
- - - image1.png

http.FileServer paired with http.Dir serves all files under a directory automatically, including subdirectories for CSS and images.

package main

import (
    "log"
    "net/http"
)

func main() {

    http.Handle("/", http.FileServer(http.Dir("./static")))

    log.Fatal(http.ListenAndServe(":8081", nil))
}

We switch from HandleFunc to Handle here because http.FileServer returns an http.Handler value rather than a plain function.

Checking it Works

Run go run main.go and navigate to http://localhost:8081 — you should see the index.html from your static/ directory.

Serving Content over HTTPS

To serve over HTTPS, swap ListenAndServe for ListenAndServeTLS, passing paths to your certificate and key files.

package main

import (
    "log"
    "net/http"
)

func main() {

    http.Handle("/", http.FileServer(http.Dir("./static")))

    log.Fatal(http.ListenAndServeTLS(":443", "server.crt", "server.key", nil))
}

Generating Keys

For local development you can generate self-signed certificates with openssl.

$ openssl genrsa -out server.key 2048
$ openssl ecparam -genkey -name secp384r1 -out server.key
$ openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650

Navigate to https://localhost:443 — your browser will warn about the self-signed cert, which is expected in development.

Frequently Asked Questions

What is the net/http package in Go?

net/http is Go’s built-in package for building HTTP clients and servers. It provides http.HandleFunc, http.ListenAndServe, http.FileServer, and everything else you need to run a web server without any third-party dependencies.

How do I handle different routes in a Go web server?

Use http.HandleFunc to register a handler function for each URL pattern. The default ServeMux matches patterns by prefix, so a handler registered at / will catch any route not matched by a more specific pattern.

Why do I need a mutex in my HTTP handlers?

Go’s HTTP server handles each request in its own goroutine. If multiple handlers access shared state (like a counter or a map), you need a sync.Mutex to prevent race conditions. See the Go Mutex Tutorial for a full explanation.

How do I serve static files in Go without a framework?

Use http.FileServer(http.Dir("./static")) to serve all files under a directory. For individual files, use http.ServeFile. Both are part of the standard library — no framework needed.

What is the difference between http.Handle and http.HandleFunc?

http.HandleFunc takes a plain func(http.ResponseWriter, *http.Request). http.Handle takes an http.Handler interface — anything with a ServeHTTP method. Use Handle when working with values like http.FileServer that already implement the interface.

How do I add HTTPS to a Go web server?

Replace http.ListenAndServe with http.ListenAndServeTLS, passing the paths to your TLS certificate and key files. For production, use a certificate from Let’s Encrypt rather than a self-signed cert.

Conclusion

In this tutorial we built a Go web server from scratch using net/http — starting with simple handlers, adding concurrent state management, serving static files, and enabling HTTPS.

The standard library gets you surprisingly far without any third-party dependencies.

For a deeper dive into web applications in Go, check out these related tutorials:

Ready to Go Further?

This tutorial is part of the Go Learning Path — a curated route from beginner to advanced Go developer, covering web servers, REST APIs, testing, and more.