What is Closure? Concepts and Real-World Applications Explained in Go

In this post, I will explain the concept of closure in Go and how I use it in my real world projects.


Closure is a function that accesses variables that are defined outside of its body. It is a useful feature in functional programming. This feature is also available in Go.

A function that returns a function can be called a closure, but not necessarily. And this is what I had misunderstood for a long time.

The accurate definition of closure is a function that accesses variables defined outside of its body.

One common example is something like this:

func closureFunc() func() {
    i := 0
    return func() {
        i++
        fmt.Println(i)
    }
}

fn := closureFunc()
fn() // 1
fn() // 2
fn() // 3

Closures can be used to achieve a form of polymorphism in Go by allowing you to parameterize behavior.

func prepareFunc(
    ctx context.Context,
    payload Payload,
) func(fn func(context.Context, Payload) error) error {
    return func(fn func(context.Context, Payload) error) error {
        fn(ctx, payload)
    }
}

func someFunc(ctx context.Context, payload Payload) error {
    // do something
    return nil
}

func anotherFunc(ctx context.Context, payload Payload) error {
    // do something
    return nil
}

func main() {
    ctx := context.Background()
    payload := Payload{}
    do := prepareFunc(ctx, payload)
    do(someFunc)
    do(anotherFunc)
}

This example shows that you can prepare a context and payload once and then apply different functions to them. This is a pattern where the returned closure 'remembers' the prepared context and payload

You may think that this is not a closure, but it is indeed. Because the inner anonymous function returned by prepareFunc forms a closure over ctx and payload, retaining access to these variables from the outer scope even after prepareFunc has finished executing.

I use closure when I need to switch between different functions that have the same signature in real world projects. Especially when handling webhook requests because in many cases the webhook requests arrive at the same endpoint and they have the same payload structure. As a result, the handlers for the webhook requests have the same signature, which can lead to the use of closure as a solution.

However, you need to be careful with the use of closure because it can make the code harder to read.

By the way, you might notice that implementing similar functionality using a struct is also possible and often easier to read. For example, you can implement something like this:

struct Counter {
    i int
}

func (c *Counter) Inc() {
    c.i++
    fmt.Println(c.i)
}

c := &Counter{}
c.Inc() // 1
c.Inc() // 2
c.Inc() // 3

This, of course, is also a valid solution. The key difference lies in the control over value access.

When you use closure, the value is not directly accessible from outside. This means that you can only update the value through the execution of the function returned by the closure. This can prevent unintentional direct modification of the captured variables, whereas a struct's fields are directly accessible and modifiable.

So if you need extra safety, closure can be a solution for you. But in most cases, struct implementation will be enough and easier to read.

When to use closure?

So when do I write closure in real world projects? Hardly ever to be honest. In most cases, I use already defined closure functions from the standard library or third party libraries and provide configuration to them.

For example, when building a web server, you probably add one or more middleware to the server. Let's take a look at the following example:

package main

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

    "github.com/gin-gonic/gin"
)

// Logger is a configuration struct for the logger
type Logger struct {
    TimeFormat string
    Prefix     string
}

func (l *Logger) Info(message string) {
    now := time.Now().Format(l.TimeFormat)
    fmt.Printf("%s %s %s\n", now, l.Prefix, message)
}

func LoggingMiddleware(logger *Logger) gin.HandlerFunc {
    // This is the outer function. It takes a Logger as an argument.
    // The logger variable is captured in the closure.
    return func(c *gin.Context) {
        // The inner function is the actual middleware that Gin will call.
        startTime := time.Now()

        c.Next() // Process the request

        latency := time.Since(startTime)
        logger.Info(fmt.Sprintf(
            "Request: %s | Status: %d | Latency: %s",
            c.Request.URL.Path, c.Writer.Status(), latency,
        ))
    }
}

func main() {
    router := gin.Default()

    // Create a pre-configured logger
    logger := &Logger{
        Prefix:     "[my-cool-app]",
        TimeFormat: time.RFC3339,
    }

    // Apply the LoggingMiddleware using the custom logger
    router.Use(LoggingMiddleware(logger))

    router.GET("/ping", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{"message": "pong"})
    })

    router.Run(":8080")
}

In this example, we initialize the Logger struct with time format and prefix configured. Then we pass the logger instance to the LoggingMiddleware function, which is executed every time a request is made to the server.

The LoggingMiddleware function returns a closure that accesses the logger variable which holds the logging configuration. We apply the middleware to the router using router.Use(LoggingMiddleware(logger)). This is a common pattern in Go web frameworks like Gin, Echo, and others.

Conclusion

In this post, I shared my understanding of closure in Go and how I use it in real world projects. I hope this post helps you understand the concept of closure in Go and how to use it effectively.

0
Subscribe to my newsletter

Read articles from Douglas Shuhei Takeuchi directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Douglas Shuhei Takeuchi
Douglas Shuhei Takeuchi

I am Douglas Takeuchi, a Tokyo-based software engineer with a background in fintech and a particular expertise in developing prepaid card systems using Golang. I also have substantial experience in Python and Javascript, making me a versatile and skilled programmer. Born in Tokyo, Japan, in 1998, I now reside in the bustling Tokyo metropolitan area. I hold a bachelor's degree in social science, which has provided me with a well-rounded perspective in my career. Beyond my professional work, I'm a dedicated musician and language enthusiast. I've been an active musician since my university days, and my music has resonated with audiences worldwide, amassing thousands of plays. One of my notable projects is the creation of a web application designed for university students to seek advice and guidance on various aspects of university life and their future career paths, similar to Yahoo Answers. In the world of coding, I feel most comfortable in Golang. I place a strong emphasis on achieving goals as a team, often coming up with collaborative solutions for success. This might involve organizing workshops to delve deeper into new technologies and collectively improve our skills. As a software engineer, I bring creativity, problem-solving skills, and a determination to excel. My commitment to my craft and the "Fake it till you make it" mentality drive my continuous growth and success. "Fake it till you make it" is my guiding principle, encouraging me to step out of my comfort zone, take on challenges, and learn from every experience, ultimately propelling me toward success.