Saturday, March 1, 2025
Building a Web Server in Go Using the net/http Package
Posted by

Introduction
Welcome to this tutorial on building a web server in Go using the net/http
package. Go, also known as Golang, is a statically typed, compiled language designed at Google by Robert Griesemer, Rob Pike, and Ken Thompson. Known for its simplicity, efficiency, and powerful standard library, Go is an excellent choice for building web servers. The net/http
package is part of Go's standard library and provides HTTP client and server implementations.
In this blog, we will cover:
- Setting up a basic HTTP server.
- Creating routes to handle different endpoints.
- Responding to HTTP requests with different HTTP methods.
- Serving static files.
- Handling errors gracefully.
Setting Up Your Go Environment
Before we dive into writing the code, ensure you have Go installed on your system. You can download it from the official Go website.
Once you have Go installed, you can create a new directory for your project:
mkdir go-web-server
cd go-web-server
Initialize a new Go module:
go mod init go-web-server
Creating a Basic HTTP Server
Let's start by creating a simple HTTP server that listens on a specific port and responds to all requests with "Hello, World!".
Create a new Go file, main.go
, and add the following code:
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
})
fmt.Println("Starting server at port 8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
fmt.Println(err)
}
}
Explanation
- http.HandleFunc: This function registers the handler function for the given pattern. In this case, the pattern is
"\/"
, which means the root URL of the server. - http.ResponseWriter: This interface is used to construct an HTTP response. It allows us to write a response back to the client.
- http.Request: This struct represents the HTTP request received by the server.
- http.ListenAndServe: This function starts an HTTP server that listens on the specified address and port. In this case, it listens on port 8080. The second argument is a handler instance, which is
nil
here to use the default handler.
To run your server, execute the following command:
go run main.go
Open your browser and navigate to http://localhost:8080
. You should see "Hello, World!" displayed in your browser.
Creating Routes
Next, let's create multiple routes to handle different endpoints. We will add a new route /about
that responds with "About Page".
Modify your main.go
file as follows:
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
})
http.HandleFunc("/about", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Welcome to the About Page")
})
fmt.Println("Starting server at port 8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
fmt.Println(err)
}
}
In the code above, we have added a new route /about
by calling http.HandleFunc
again with the pattern "/about"
.
Run your server again:
go run main.go
Navigate to http://localhost:8080
to see "Hello, World!" and to http://localhost:8080/about
to see "Welcome to the About Page".
Handling HTTP Methods
HTTP methods determine the action to be performed on a resource identified by a URL. Common HTTP methods include GET, POST, PUT, DELETE, etc. In this section, we will handle GET and POST methods.
Modify your main.go
to handle different HTTP methods:
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
fmt.Fprintf(w, "This is the root page handling GET requests")
} else if r.Method == http.MethodPost {
fmt.Fprintf(w, "This is the root page handling POST requests")
} else {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
})
http.HandleFunc("/about", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "About Page")
})
fmt.Println("Starting server at port 8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
fmt.Println(err)
}
}
Explanation
- r.Method: This field contains the HTTP method of the request (e.g., GET, POST).
- http.MethodGet, http.MethodPost: These constants are used for HTTP methods.
- http.Error: This function sends an HTTP response with a specified status code and error message.
To test the POST request, you can use a tool like Postman or curl
:
Using curl
:
curl -X POST http://localhost:8080/
You should see "This is the root page handling POST requests" in the terminal where the server is running.
Serving Static Files
To serve static files like HTML, CSS, and JavaScript files, you can use the http.FileServer
.
Add a new folder public
and a file index.html
inside it:
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Web Server Example</title>
</head>
<body>
<h1>Welcome to Our Web Server</h1>
<p>This is a static HTML file served by Go.</p>
</body>
</html>
Modify your main.go
to serve static files:
package main
import (
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "public/index.html")
})
http.HandleFunc("/about", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "About Page")
})
fmt.Println("Starting server at port 8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
fmt.Println(err)
}
}
Explanation
- http.ServeFile: This function serves the specified file to the client. In the example, it serves
public/index.html
.
Run your server and navigate to http://localhost:8080
to see the static HTML page.
Serving Multiple Static Files
To serve multiple static files (HTML, CSS, JavaScript), you can use http.FileServer
with http.StripPrefix
.
Modify your main.go
to serve all files from the public
directory:
package main
import (
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "public/index.html")
})
http.HandleFunc("/about", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "About Page")
})
http.Handle("/public/", http.StripPrefix("/public/", http.FileServer(http.Dir("public"))))
fmt.Println("Starting server at port 8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
fmt.Println(err)
}
}
Explanation
- http.Handle: This function registers a handler for the given pattern. It’s similar to
http.HandleFunc
but allows you to associate a handler with a pattern directly. - http.StripPrefix: This function removes the provided prefix from the request URL before routing it to the handler. This is useful when serving files from a specific directory.
- http.Dir: This function returns a handler that serves HTTP requests with the contents of the specified directory.
Now, any file placed in the public
directory can be accessed by appending its path to http://localhost:8080/public/
.
Handling JSON Responses
Often, web servers need to respond with JSON data instead of plain text. We can use the encoding/json
package to encode data into JSON format.
Add a new route /api
that responds with JSON data:
package main
import (
"encoding/json"
"fmt"
"net/http"
)
type Response struct {
Message string `json:"message"`
Status string `json:"status"`
}
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "public/index.html")
})
http.HandleFunc("/about", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "About Page")
})
http.Handle("/public/", http.StripPrefix("/public/", http.FileServer(http.Dir("public"))))
http.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
response := Response{Message: "API Response", Status: "Success"}
json.NewEncoder(w).Encode(response)
})
fmt.Println("Starting server at port 8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
fmt.Println(err)
}
}
Explanation
- Response struct: This struct defines the structure of the JSON response.
- json.NewEncoder(w).Encode(response): This line encodes the
response
struct into JSON and writes it to the response writerw
.
Navigate to http://localhost:8080/api
to see the JSON response.
Handling Query Parameters
Query parameters are key-value pairs appended to the URL after a question mark (?
). You can access these parameters using the r.URL.Query()
method.
Add a new route /search
that handles query parameters:
package main
import (
"encoding/json"
"fmt"
"net/http"
)
type Response struct {
Message string `json:"message"`
Status string `json:"status"`
}
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "public/index.html")
})
http.HandleFunc("/about", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "About Page")
})
http.Handle("/public/", http.StripPrefix("/public/", http.FileServer(http.Dir("public"))))
http.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
response := Response{Message: "API Response", Status: "Success"}
json.NewEncoder(w).Encode(response)
})
http.HandleFunc("/search", func(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
keyword := query.Get("keyword")
if keyword == "" {
http.Error(w, "No keyword provided", http.StatusBadRequest)
return
}
response := Response{Message: fmt.Sprintf("Searching for: %s", keyword), Status: "Success"}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
})
fmt.Println("Starting server at port 8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
fmt.Println(err)
}
}
Explanation
- r.URL.Query(): This method returns a map of URL query parameters.
- query.Get("keyword"): This method retrieves the value of the
keyword
parameter.
Navigate to http://localhost:8080/search?keyword=golang
to see the JSON response with the search keyword.
Middleware in Go HTTP Server
Middleware in a web server is a function that modifies the request or response or both before or after the request is processed by the handler. Middleware can be used for logging, authentication, session management, etc.
Let’s add a logging middleware to log the request method and URL:
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"time"
)
type Response struct {
Message string `json:"message"`
Status string `json:"status"`
}
func loggingMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("Method: %s\tURL: %s\tDuration: %v\n", r.Method, r.URL.Path, time.Since(start))
}
}
func main() {
http.HandleFunc("/", loggingMiddleware(func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "public/index.html")
}))
http.HandleFunc("/about", loggingMiddleware(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "About Page")
}))
http.Handle("/public/", loggingMiddleware(http.StripPrefix("/public/", http.FileServer(http.Dir("public"))))
http.HandleFunc("/api", loggingMiddleware(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
response := Response{Message: "API Response", Status: "Success"}
json.NewEncoder(w).Encode(response)
}))
http.HandleFunc("/search", loggingMiddleware(func(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
keyword := query.Get("keyword")
if keyword == "" {
http.Error(w, "No keyword provided", http.StatusBadRequest)
return
}
response := Response{Message: fmt.Sprintf("Searching for: %s", keyword), Status: "Success"}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}))
fmt.Println("Starting server at port 8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
fmt.Println(err)
}
}
Explanation
- loggingMiddleware: This function wraps the handler function and adds logging before and after the handler is executed.
Error Handling
Proper error handling is crucial for building reliable web servers. Let's add basic error handling to our routes.
Modify the main.go
file to include error handling:
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"time"
)
type Response struct {
Message string `json:"message"`
Status string `json:"status"`
}
func loggingMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("Method: %s\tURL: %s\tDuration: %v\n", r.Method, r.URL.Path, time.Since(start))
}
}
func main() {
http.HandleFunc("/", loggingMiddleware(func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "public/index.html")
}))
http.HandleFunc("/about", loggingMiddleware(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "About Page")
}))
http.Handle("/public/", loggingMiddleware(http.StripPrefix("/public/", http.FileServer(http.Dir("public")))))
http.HandleFunc("/api", loggingMiddleware(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
response := Response{Message: "API Response", Status: "Success"}
json.NewEncoder(w).Encode(response)
}))
http.HandleFunc("/search", loggingMiddleware(func(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
keyword := query.Get("keyword")
if keyword == "" {
http.Error(w, "No keyword provided", http.StatusBadRequest)
return
}
response := Response{Message: fmt.Sprintf("Searching for: %s", keyword), Status: "Success"}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}))
http.HandleFunc("/data", loggingMiddleware(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
data := map[string]string{
"name": "Go Web Server",
"desc": "A simple web server in Go",
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(data)
}))
fmt.Println("Starting server at port 8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
fmt.Println(err)
}
}
Explanation
- r.Method != http.MethodGet: This condition checks if the request method is not GET and responds with an error if true.
- w.Header().Set("Content-Type", "application/json"): This line sets the
Content-Type
header toapplication/json
, indicating that the response is in JSON format.
Navigate to http://localhost:8080/data
to see the JSON response.
Conclusion
In this tutorial, we learned how to build a basic web server in Go using the net/http
package. We covered setting up routes, handling different HTTP methods, serving static files, responding with JSON, and adding middleware for logging and error handling.
Building a web server in Go is straightforward and powerful, thanks to its simple and efficient standard library. This tutorial provided a foundation for creating more complex web applications with Go. Feel free to extend this server by adding more features and functionalities as you become more comfortable with the basics.
References
Further Reading
Feel free to explore the official Go documentation and these resources to deepen your understanding of web programming in Go.