#Building a Web Server in Go with net/http
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.
Related Articles
For a deeper dive into web applications in Go, check out these related tutorials:
- Understanding Go Mutexes - Learn more about concurrent access patterns
- Building REST APIs in Go - Take your web server to the next level with proper API design
- Authenticating your REST API with JWTs - Secure your endpoints with authentication
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.
Continue Learning
An Introduction to Go Closures - Tutorial
Learn how closures work in Go with simple, practical examples. Understand lexical scoping and how closures capture and maintain their own state.
Creating a RESTful API With Golang
this tutorial demonstrates how you can create your own simple RESTful JSON api using Go(Lang)
How To Consume Data From A REST HTTP API With Go
Learn how to consume RESTful APIs in Go (Golang) with this step-by-step tutorial. Perfect for developers looking to master HTTP requests and JSON parsing in Go.
Accepting Interfaces and Returning Structs
In this article, we are going to discuss the benefits of accepting interfaces in your code and returning structs.