Go > Web Development > REST APIs > Middleware functions

Simple Logging Middleware in Go

This snippet demonstrates how to create a simple logging middleware function in Go for a REST API. This middleware logs each incoming request's method and path to the console. Middleware functions are essential for handling cross-cutting concerns like logging, authentication, and authorization in a structured way.

Concepts Behind Middleware

Middleware functions in Go's `net/http` package are functions that sit between your server's request handling logic and the incoming HTTP requests. They allow you to pre-process requests before they reach your handlers and post-process responses before they are sent back to the client. This provides a powerful mechanism to add functionality to your API without modifying individual handler functions, promoting code reusability and maintainability.

Middleware Function Definition

This code defines the `LoggerMiddleware` function. It takes an `http.Handler` as input (which is the next handler in the chain) and returns a new `http.Handler`. Inside the returned handler function (an anonymous function that implements `http.HandlerFunc`), we log the request's method and URI. Finally, we call `next.ServeHTTP(w, r)` to pass the request to the next handler in the chain.

package main

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

// LoggerMiddleware is a middleware function that logs the request method and URI.
func LoggerMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Log the request details.
		log.Printf("Request: %s %s", r.Method, r.RequestURI)
		
		// Call the next handler in the chain.
		next.ServeHTTP(w, r)
	})
}

Applying the Middleware

In the `main` function, we first define a simple handler that responds with "Hello, World!". Then, we wrap this handler with the `LoggerMiddleware`. This creates a new handler that first executes the logging logic and then calls the original handler. Finally, we register the wrapped handler with the `/` route and start the HTTP server. Note how `http.Handle` is used to register our middleware. `http.ListenAndServe` will use this registered middleware.

func main() {
	// Define a simple handler.
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "Hello, World!")
	})

	// Wrap the handler with the logger middleware.
	loggedHandler := LoggerMiddleware(handler)

	// Register the wrapped handler.
	http.Handle("/", loggedHandler)

	// Start the server.
	log.Fatal(http.ListenAndServe(":8080", nil))
}

Complete Example

This is a complete runnable example. You can copy and paste this into a `main.go` file and run it using `go run main.go`. Accessing `http://localhost:8080` will print "Hello, World!" in the browser and log the request details in the console.

package main

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

// LoggerMiddleware is a middleware function that logs the request method and URI.
func LoggerMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Log the request details.
		log.Printf("Request: %s %s", r.Method, r.RequestURI)
		
		// Call the next handler in the chain.
		next.ServeHTTP(w, r)
	})
}

func main() {
	// Define a simple handler.
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "Hello, World!")
	})

	// Wrap the handler with the logger middleware.
	loggedHandler := LoggerMiddleware(handler)

	// Register the wrapped handler.
	http.Handle("/", loggedHandler)

	// Start the server.
	log.Fatal(http.ListenAndServe(":8080", nil))
}

Real-Life Use Case

Imagine a REST API endpoint that handles user authentication. A middleware could be used to verify the authentication token sent in the request headers before passing the request to the handler. This ensures that only authenticated users can access the endpoint.

Best Practices

  • Chaining Middleware: Middleware functions can be chained together to perform multiple operations on a request. The order of the middleware functions matters as each middleware can modify the request or response before passing it to the next middleware in the chain.
  • Error Handling: Middleware should handle errors gracefully and return appropriate error responses to the client.
  • Context: Use the `context` package to pass request-scoped values between middleware functions and handlers.

Interview Tip

When asked about middleware in an interview, be prepared to explain its purpose, how it works in Go's `net/http` package, and provide examples of common use cases like logging, authentication, and request validation. Also, highlight the benefits of using middleware in terms of code reusability and maintainability.

When to Use Middleware

Use middleware when you have functionalities that need to be applied to multiple endpoints in your API. This avoids code duplication and makes your code more modular and easier to maintain. Common use cases include:

  • Logging
  • Authentication
  • Authorization
  • Request validation
  • Rate limiting
  • CORS handling

Memory Footprint

The memory footprint of middleware is generally small. The primary overhead comes from the function calls and any additional data stored in the request context. Careful consideration should be given to the amount of data stored in the context to avoid unnecessary memory consumption.

Alternatives

Instead of using middleware, you could implement the same logic directly within each handler function. However, this approach leads to code duplication and makes it harder to maintain the code. Another alternative is to use an interceptor pattern (often used in gRPC), but middleware is the standard approach in HTTP.

Pros

  • Code Reusability: Middleware promotes code reusability by allowing you to define common functionalities in a single place and apply them to multiple endpoints.
  • Modularity: Middleware makes your code more modular and easier to maintain.
  • Separation of Concerns: Middleware helps separate concerns by isolating cross-cutting functionalities from the core handler logic.

Cons

  • Complexity: Overuse of middleware can make the request handling flow more complex and harder to understand.
  • Performance Overhead: Each middleware function adds a small amount of overhead to the request processing time. Too much middleware may impact performance.
  • Debugging: Debugging can be difficult if the middleware introduces unexpected behavior.

FAQ

  • What is the purpose of `http.Handler` and `http.HandlerFunc`?

    `http.Handler` is an interface that defines the `ServeHTTP` method, which is used to handle HTTP requests. `http.HandlerFunc` is a type adapter that allows ordinary functions to be used as HTTP handlers.
  • How do I pass data between middleware functions?

    Use the `context` package to store request-scoped values. The `context` can be accessed and modified by middleware functions and handlers.
  • Can I modify the request or response in a middleware function?

    Yes, you can modify the request or response in a middleware function. For example, you can add headers to the response or modify the request body.