DEV Community

Cover image for Negroni: A Lightweight Middleware Library for Go
Leapcell
Leapcell

Posted on

1 1 1 1 1

Negroni: A Lightweight Middleware Library for Go

Image description

Leapcell: The Best of Serverless Web Hosting

Introduction to the Negroni Library

Negroni is a library that focuses on HTTP middleware. It is small in size and non-intrusive, and it encourages the use of handlers from the standard library net/http. This article will provide a detailed introduction to this library.

I. Why Use Middleware

During the development process, there are some common logical codes, such as statistics, logging, debugging, etc., and these logics may be required in each handler. If these codes are added to each handler one by one, it is not only cumbersome but also prone to errors and omissions.

Take the example of counting the time consumption of a handler. If the code for counting the time consumption is added to each handler, the example is as follows:

package main

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

func index(w http.ResponseWriter, r *http.Request) {
    start := time.Now()
    fmt.Fprintf(w, "leapcell page")
    fmt.Printf("index elasped:%fs", time.Since(start).Seconds())
}

func greeting(w http.ResponseWriter, r *http.Request) {
    start := time.Now()
    name := r.FormValue("name")
    if name == "" {
        name = "world"
    }
    fmt.Fprintf(w, "hello %s", name)
    fmt.Printf("greeting elasped:%fs\n", time.Since(start).Seconds())
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", index)
    mux.HandleFunc("/greeting", greeting)
    http.ListenAndServe(":8000", mux)
}
Enter fullscreen mode Exit fullscreen mode

However, this approach has many disadvantages:

  1. Lack of flexibility: Every time a new handler is added, this part of the code for counting time consumption needs to be added, and these codes have no direct relationship with the actual handler logic.
  2. Prone to omission: It is easy to forget to add such codes when writing handlers, especially when considering all possible return paths, which increases the coding burden.
  3. Not conducive to modification: If there is an error in the statistical code or it needs to be adjusted, all the handlers involved must be modified.
  4. Difficult to add new logic: If other statistical logics need to be added, the code of all handlers also needs to be changed.

By utilizing the closure feature of the Go language, the actual handler code can be encapsulated in a function, and additional logic can be executed in this function. It is shown as follows:

func elasped(h func(w http.ResponseWriter, r *http.Request)) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        path := r.URL.Path
        start := time.Now()
        h(w, r)
        fmt.Printf("path:%s elasped:%fs\n", path, time.Since(start).Seconds())
    }
}

func index(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "leapcell page")
}

func greeting(w http.ResponseWriter, r *http.Request) {
    name := r.FormValue("name")
    if name == "" {
        name = "world"
    }
    fmt.Fprintf(w, "hello %s", name)
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", elasped(index))
    mux.HandleFunc("/greeting", elasped(greeting))
    http.ListenAndServe(":8000", mux)
}
Enter fullscreen mode Exit fullscreen mode

In this example, the additional code that has nothing to do with the handler is placed in the elasped function. When registering the handler function, instead of directly using the original handler function, it is encapsulated with the elasped function. In fact, a function like elasped is a middleware. It encapsulates the original handler function and returns a new handler function, which is convenient for inserting code before and after the actual processing logic, and is beneficial for addition, modification, and maintenance.

II. Quick Start with Negroni

(I) Installation

Execute the following command to install the Negroni library:

$ go get github.com/urfave/negroni
Enter fullscreen mode Exit fullscreen mode

(II) Usage Example

package main

import (
    "fmt"
    "net/http"
    "github.com/urfave/negroni"
)

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello World!")
    })
    n := negroni.Classic()
    n.UseHandler(mux)
    http.ListenAndServe(":8080", n)
}
Enter fullscreen mode Exit fullscreen mode

Negroni is relatively easy to use and can work conveniently with http.Handler. negroni.Classic() provides several commonly used middleware:

  1. negroni.Recovery: It is used to recover from panic that occurs during the program's running process. If a panic occurs in the handler code, this middleware will catch the exception and prevent the program from exiting.
  2. negroni.Logger: It is responsible for recording basic information about requests and responses, implementing the logging function.
  3. negroni.Static: It can provide static file services in the public directory.

By calling n.UseHandler(mux), these middleware are applied to the multiplexer mux. After running the program, enter localhost:3000 in the browser, and you can see the following similar output in the console:

[negroni] 2025-03-22T18:48:53+08:00 | 200 |      10.9966ms | localhost:8080 | GET /
Enter fullscreen mode Exit fullscreen mode

III. In-depth Features of Negroni

(I) negroni.Handler Interface

The negroni.Handler interface provides more flexible control over the execution process of middleware. The interface is defined as follows:

type Handler interface {
    ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc)
}
Enter fullscreen mode Exit fullscreen mode

The signature of the written middleware must be func(http.ResponseWriter, *http.Request, http.HandlerFunc), or implement the negroni.Handler interface. For example:

func RandomMiddleware(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
    if rand.Int31n(100) <= 50 {
        fmt.Fprintf(w, "hello from RandomMiddleware")
    } else {
        next(w, r)
    }
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello World!")
    })
    n := negroni.New()
    n.Use(negroni.HandlerFunc(RandomMiddleware))
    n.UseHandler(mux)
    http.ListenAndServe(":8080", n)
}
Enter fullscreen mode Exit fullscreen mode

The above code implements a random middleware. There is a 50% probability that the response will be directly returned from the RandomMiddleware middleware, and there is a 50% probability that the actual handler function will be executed. After running the program, refresh localhost:8080 in the browser continuously to observe the effect.

In fact, func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) is a convenient way of writing. When calling n.Use, it is encapsulated with negroni.HandlerFunc, and negroni.HandlerFunc implements the negroni.Handler interface. The following is the relevant code:

// src/github.com/urfave/negroni/negroni.go
type HandlerFunc func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc)

func (h HandlerFunc) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
    h(rw, r, next)
}
Enter fullscreen mode Exit fullscreen mode

Similarly, in net/http, func(http.ResponseWriter, *http.Request) is also encapsulated with http.HandlerFunc to implement the http.Handler interface.

(II) negroni.With Method

When there are multiple middleware, it is cumbersome to add them one by one using the n.Use() method. Negroni provides the With() method, which accepts one or more negroni.Handler parameters and returns a new object. The example is as follows:

func Middleware1(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
    fmt.Println("Middleware A begin")
    next(w, r)
    fmt.Println("Middleware A end")
}

func Middleware2(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
    fmt.Println("Middleware B begin")
    next(w, r)
    fmt.Println("Middleware B end")
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello World!")
    })
    n := negroni.New()
    n = n.With(
        negroni.HandlerFunc(Middleware1),
        negroni.HandlerFunc(Middleware2),
    )
    n.UseHandler(mux)
    http.ListenAndServe(":8080", n)
}
Enter fullscreen mode Exit fullscreen mode

(III) Run Method

The Negroni object provides the Run() method, which is used to conveniently run the server program. This method accepts the same address (Addr) parameter as http.ListenAndServe(). The example is as follows:

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello World!")
    })
    n := negroni.New()
    n.UseHandler(mux)
    n.Run(":8080")
}
Enter fullscreen mode Exit fullscreen mode

If the port is not specified, the Run() method will try to use the PORT environment variable. If the PORT environment variable is not set either, the default port :8080 will be used.

(IV) Using as http.Handler

Negroni can be easily used in net/http programs, and the negroni.Negroni object can be directly passed as an http.Handler to the corresponding method. The example is as follows:

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello World!")
    })
    n := negroni.Classic()
    n.UseHandler(mux)
    s := &http.Server{
        Addr:           ":8080",
        Handler:        n,
        ReadTimeout:    10 * time.Second,
        WriteTimeout:   10 * time.Second,
        MaxHeaderBytes: 1 << 20,
    }
    s.ListenAndServe()
}
Enter fullscreen mode Exit fullscreen mode

IV. Built-in Middleware

(I) Static

The negroni.Static middleware can provide file services in the specified directory. The example code is as follows:

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "hello world")
    })
    n := negroni.New()
    n.Use(negroni.NewStatic(http.Dir("./public")))
    n.UseHandler(mux)
    http.ListenAndServe(":8080", n)
}
Enter fullscreen mode Exit fullscreen mode

Create a public directory in the program's running directory and put some files (such as 1.txt, 2.jpg) in it. After the program runs, you can access localhost:8080/1.txt and localhost:8080/2.jpg in the browser to request these files. It should be particularly noted that if the corresponding file cannot be found, the Static middleware will pass the request to the next middleware or handler function. For example, when entering localhost:3000/none - exist.txt in the browser, you will see the response of hello world.

(II) Logger

In the "Quick Start" section, this middleware has been used through negroni.Classic(). It can also be used alone to record request information, and the SetFormat() method can be called to set the log format. The example is as follows:

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "hello world")
    })
    n := negroni.New()
    logger := negroni.NewLogger()
    logger.SetFormat("[{{.Status}} {{.Duration}}] - {{.Request.UserAgent}}")
    n.Use(logger)
    n.UseHandler(mux)
    http.ListenAndServe(":8080", n)
}
Enter fullscreen mode Exit fullscreen mode

The above code sets the log format to [{{.Status}} {{.Duration}}] - {{.Request.UserAgent}}, that is, it records the response status, time consumption, and UserAgent.

V. Third-party Middleware

In addition to the built-in middleware, Negroni also has many third-party middleware. For the complete list, please refer to: https://github.com/urfave/negroni?tab=readme-ov-file#third-party-middleware.

VI. Conclusion

Negroni focuses on middleware functionality and does not have too many cumbersome and rarely used fancy features. Its non-intrusive design feature enables it to work seamlessly with the standard library net/http and other Web libraries (such as gorilla/mux). This feature provides great convenience for developers when building HTTP services. Developers can flexibly select and combine middleware according to actual needs to achieve various functions such as logging, error handling, and request statistics, without the need for large-scale changes to the existing code architecture.

Overall, Negroni is a very valuable library in Go language HTTP service development. For those who pursue efficient, flexible, and easy-to-maintain Web application development, Negroni is undoubtedly an excellent tool worthy of in-depth understanding and widespread application.

Leapcell: The Best of Serverless Web Hosting

Finally, I would like to introduce the best platform for deploying Go services: Leapcell

Image description

🚀 Build with Your Favorite Language

Develop effortlessly in JavaScript, Python, Go, or Rust.

🌍 Deploy Unlimited Projects for Free

Only pay for what you use—no requests, no charges.

⚡ Pay-as-You-Go, No Hidden Costs

No idle fees, just seamless scalability.

Image description

📖 Explore Our Documentation

🔹 Follow us on Twitter: @LeapcellHQ

AWS Q Developer image

Your AI Code Assistant

Generate and update README files, create data-flow diagrams, and keep your project fully documented. Built to handle large projects, Amazon Q Developer works alongside you from idea to production code.

Get started free in your IDE

Top comments (0)