Go Decorator Function Pattern Tutorial
Decorators are certainly more prominent in other programming languages such as Python and TypeScript, but that’s not to say you can’t use them in Go. In fact, for certain problems, using decorators is the perfect solution as we’ll hopefully be finding out in this tutorial.
Understanding the Decorator Pattern
Decorators essentially allow you to wrap existing functionality and append or prepend your own custom functionality on top.
In Go, functions are deemed as first class objects which essentially means you can pass them around just as you would a variable. Let’s see this in action with a very simple example:
package main
import (
"fmt"
"time"
)
func myFunc() {
fmt.Println("Hello World")
time.Sleep(1 * time.Second)
}
func main() {
fmt.Printf("Type: %T\n", myFunc)
}
So, in this example, we’ve defined a function called myFunc, which simply
prints out Hello World. However, in the body of our main() function, we’ve
called fmt.Printf and we’ve used %T to print out the type of the value we
pass in as our second argument. In this case, we are passing myFunc which will
subsequently print out the following:
$ go run test.go
Type: func()
So, what does this mean for us Go developers then? Well, it highlights the fact that functions can be passed around and used as arguments within other parts of our code base.
Let’s see this in action by extending our codebase a little bit more and adding
a coolFunc() function which takes in a function as its only parameter:
package main
import (
"fmt"
"time"
)
func myFunc() {
fmt.Println("Hello World")
time.Sleep(1 * time.Second)
}
// coolFunc takes in a function
// as a parameter
func coolFunc(a func()) {
// it then immediately calls that functino
a()
}
func main() {
fmt.Printf("Type: %T\n", myFunc)
// here we call our coolFunc function
// passing in myFunc
coolFunc(myFunc)
}
When we attempt to run this, we should see that our new output features our
Hello World string as we’d expect:
$ go run test.go
Type: func()
Hello World
Now, this may strike you as a bit odd at first. Why would you want to do
something like this? It essentially adds a layer of abstraction over your call
to myFunc and complicates the code without really adding much value.
A Simple Decorator
Let’s see how we could use this pattern to add some value to our codebase. We could, if we wanted, add some additional logging around the execution of a particular function just to highlight it’s start and end time.
package main
import (
"fmt"
"time"
)
func myFunc() {
fmt.Println("Hello World")
time.Sleep(1 * time.Second)
}
func coolFunc(a func()) {
fmt.Printf("Starting function execution: %s\n", time.Now())
a()
fmt.Printf("End of function execution: %s\n", time.Now())
}
func main() {
fmt.Printf("Type: %T\n", myFunc)
coolFunc(myFunc)
}
Upon calling this, you should see logs that look a little like this:
$ go run test.go
Type: func()
Starting function execution: 2018-10-21 11:11:25.011873 +0100 BST m=+0.000443306
Hello World
End of function execution: 2018-10-21 11:11:26.015176 +0100 BST m=+1.003743698
As you can see, we’ve been able to effectively wrap my original function without having to alter it’s implementation. We are now able to clearly see when this function was started and when it finished execution and it highlights to us that the function takes just about a second to finish execution.
Real World Examples: HTTP Middleware as Decorators
The decorator pattern is widely used in HTTP middleware. Let’s look at how we can use decorators for real-world web applications. HTTP middleware decorators allow you to compose cross-cutting concerns like authentication, logging, rate limiting, and CORS handling without modifying your endpoint handlers.
If you fancy learning more about writing a simple REST API in Go then I recommend checking out my other article here: Creating a REST API in Go
package main
import (
"fmt"
"log"
"net/http"
)
func homePage(w http.ResponseWriter, r *http.Request) {
fmt.Println("Endpoint Hit: homePage")
fmt.Fprintf(w, "Welcome to the HomePage!")
}
func handleRequests() {
http.HandleFunc("/", homePage)
log.Fatal(http.ListenAndServe(":8081", nil))
}
func main() {
handleRequests()
}
As you can see, nothing particularly complex in our code. We set up a net/http
router that serves a single / endpoint.
Let’s add a really simple authentication decorator function that will check to
see if the Authorized header is set to true on the incoming request.
package main
import (
"fmt"
"log"
"net/http"
)
func isAuthorized(endpoint func(http.ResponseWriter, *http.Request)) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("Checking to see if Authorized header set...")
if val, ok := r.Header["Authorized"]; ok {
fmt.Println(val)
if val[0] == "true" {
fmt.Println("Header is set! We can serve content!")
endpoint(w, r)
}
} else {
fmt.Println("Not Authorized!!")
fmt.Fprintf(w, "Not Authorized!!")
}
})
}
func homePage(w http.ResponseWriter, r *http.Request) {
fmt.Println("Endpoint Hit: homePage")
fmt.Fprintf(w, "Welcome to the HomePage!")
}
func handleRequests() {
http.Handle("/", isAuthorized(homePage))
log.Fatal(http.ListenAndServe(":8081", nil))
}
func main() {
handleRequests()
}
Note: This is absolutely not the right way to handle securing your REST API, I’d recommend looking at using JWT or OAuth2 in order to achieve that goal!
So, let’s break this down and try to understand what’s going on!
We’ve created a new decorator function called isAuthorized() which takes in a
function that matches the same signature as our original homePage function.
This then returns a http.Handler.
Within the body of our isAuthorized() function, we return a new
http.HandlerFunc within which does the job of validating our Authorized
header is set and equals true. Now, this is a drastically simplified version
of OAuth2 authentication/authorization, There’s a few slight discrepancies but
it gives you a general idea as to how it would work.
The key thing to note however, is the fact that we’ve managed to decorate an existing endpoint and add some form of authentication around said endpoint without having to alter the existing implementation of that function.
Now, if we were to add a new endpoint that we wanted to be protected, we could easily do so:
// define our newEndpoint function. Notice how, yet again,
// we don't do any authentication based stuff in the body
// of this function
func newEndpoint(w http.ResponseWriter, r *http.Request) {
fmt.Println("My New Endpoint")
fmt.Fprintf(w, "My second endpoint")
}
func handleRequests() {
http.Handle("/", isAuthorized(homePage))
// register our /new endpoint and decorate our
// function with our isAuthorized Decorator
http.Handle("/new", isAuthorized(newEndpoint))
log.Fatal(http.ListenAndServe(":8081", nil))
}
This highlights the key benefits of the decorator pattern, where wrapping code within our codebase is incredibly simple. We can easily add new authenticated endpoints using this same method.
Type-Safe Decorators with Go Generics (Go 1.18+)
With the introduction of generics in Go 1.18, we can now create type-safe decorators that work with any function signature. This provides better compile-time safety and reduces the need for type assertions. Go 1.26 further enhances this capability by allowing recursive generic types, which means you can now create decorator patterns with types that refer to themselves in their own type parameter list. This enables even more powerful and sophisticated decorator implementations.
package main
import (
"fmt"
"time"
)
// Decorator is a generic higher-order function that adds timing to any function
func TimeDecorator[T any](fn func() T) func() T {
return func() T {
start := time.Now()
result := fn()
duration := time.Since(start)
fmt.Printf("Function took %v to execute\n", duration)
return result
}
}
func expensiveOperation() string {
time.Sleep(100 * time.Millisecond)
return "Operation complete"
}
func main() {
// Wrap the function with timing decorator
timedOp := TimeDecorator(expensiveOperation)
result := timedOp()
fmt.Println(result)
}
This generic approach allows you to create decorators that work with any function signature while maintaining type safety. You can create more sophisticated generic decorators for error handling, retries, caching, and more.
Conclusion
Hopefully, this tutorial helped to demystify the wonders of the decorator and how you can use the decorator pattern within your own Go-based programs. We learned about the benefits of the decorator pattern and how we could use it to wrap existing functionality with new functionality.
In the second part of the tutorial, we looked at a more realistic example as to how you could potentially use this within your own production-level Go systems.
If you enjoyed this tutorial, then please feel free to share the article far an wide, it really helps the site and I’d be very appreciative! If you have any questions and/or comments then please let me know in the comments section down below!
Note - If you want to keep track of when new Go articles are posted to the site, then please feel free to follow me on twitter for all the latest news: @Elliot_F.
Further Reading
If you are looking for more, then you may quite like some of the other articles on this site. Feel free to check out the following articles:
Continue Learning
Creating a RESTful API With Golang
this tutorial demonstrates how you can create your own simple RESTful JSON api using Go(Lang)
Go 1.23 Iterators Tutorial
๐ Welcome Gophers! In this article, we are going to be looking at how you can use GitHub actions to supercharge your Go project setup!
Functional Options Parameter Pattern in Go
In this tutorial, we'll be discussing one of my favorite patterns and how you can use it to supercharge your Go app dev.
Joining Errors With errors.Join in Go
In this tutorial, we'll be looking at how we can join errors together in Go using the errors.Join method!