Back to blog

Saturday, March 1, 2025

Building a Web Server in Go Using the net/http Package

cover

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 writer w.

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 to application/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.