Why would you practice functional programming with Go? To put it simply, functional programming makes your code more readable, easier to test, and less complex due to the absence of states and mutable data. If you encounter bugs, you can debug your app quickly, as long as you don’t violate the rules of functional programming. When functions are isolated, you don’t have to deal with hidden state changes that affect the output.
Software engineer and author Eric Elliot defined function programming as follows.
Functional programming is the process of building software by composing pure functions, avoiding shared state, mutable data, and side-effects. Functional programming is declarative rather than imperative, and application state flows through pure functions. Contrast with object-oriented programming, where application state is usually shared and colocated with methods in objects.
I’ll take it a step further: functional programming, like object-oriented and procedural programming, represents a paradigm shift. It imposes a unique way of thinking when it comes to writing code and introduces a whole new set of rules to stick to.
To fully grasp functional programming, you must first understand the following related concepts.
Let’s quickly review.
A pure function always returns the same output if you give it the same input. This property is also referenced as idempotence. Idempotence means that a function should always return the same output, independent of the number of calls.
A pure function can’t have any side effects. In other words, your function cannot interact with external environments.
For example, functional programming considers an API call to be a side effect. Why? Because an API call is considered an external environment that is not under your direct control. An API can have several inconsistencies, such as a timeout or failure, or it may even return an unexpected value. It does not fit the definition of a pure function since we require consistent results every time we call the API.
Other common side effects include:
DateTime
with time.Now()
The basic idea of function composition is straightforward: you combine two pure functions to create a new function. This means the concept of producing the same output for the same input still applies here. Therefore, it’s important to create more advanced functionality starting with simple, pure functions.
The goal of functional programming is to create functions that do not hold a state. Shared states, especially, can introduce side effects or mutability problems in your pure functions, rendering them nonpure.
Not all states are bad, however. Sometimes, a state is necessary to solve a certain software problem. The goal of functional programming is to make the state visible and explicit to eliminate any side effects. A program uses immutable data structures to derive new data from using pure functions. This way, there is no need for mutable data that may cause side effects.
Now that we’ve covered our bases, let’s define some rules to follow when writing functional code in Go.
As I mentioned, functional programming is a paradigm. As such, it’s difficult to define exact rules for this style of programming. It’s also not always possible to follow these rules to a T; sometimes, you really need to rely on a function that holds a state.
However, to follow the functional programming paradigm as closely as possible, I suggest sticking to the following guidelines.
One good “side effect” we often encounter in functional programming is strong modularization. Instead of approaching software engineering from the top-down, functional programming encourages a bottom-up style of programming. Start by defining modules that group similar pure functions that you expect to need in the future. Next, start writing those small, stateless, independent functions to create your first modules.
We are essentially creating black boxes. Later on, we’ll tie together the boxes following the bottom-up approach. This enables you to build a strong base of tests, especially unit tests that verify the correctness of your pure functions.
Once you have trust in your solid base of modules, it’s time to tie together the modules. This step in the development process also involves writing integration tests to ensure proper integration of the two components.
To paint a fuller picture of how functional programming with Go works, let’s explore five basic examples.
This is the simplest example of a pure function. Normally, when you want to update a string, you would do the following.
<code> name := "first name" name := name + " last name" </code>
The above snippet does not adhere to the rules of functional programming because a variable can’t be modified within a function. Therefore, we should rewrite the snippet of code so every value gets its own variable.
The code is much more readable in the snippet below.
<code> firstname := "first" lastname := "last" fullname := firstname + " " + lastname </code>
When looking at the nonfunctional snippet of code, we have to look through the program to determine the latest state of name
to find the resulting value for the name
variable. This requires more effort and time to understand what the function is doing.
As stated earlier, the objective of functional programming is to use immutable data to derive a new immutable data state through pure functions. This can also be applied to arrays in which we create a new array each time we want to update one.
In nonfunctional programming, update an array like this:
<code> names := [3]string{"Tom", "Ben"} // Add Lucas to the array names[2] = "Lucas" </code>
Let’s try this according to the functional programming paradigm.
<code> names := []string{"Tom", "Ben"} allNames := append(names, "Lucas") </code>
The example uses the original names
slice in combination with the append()
function to add extra values to the new array.
This is a somewhat more extreme example of functional programming. Imagine we have a map with a key of type string and a value of type integer. The map holds the number of fruits we still have left at home. However, we just bought apples and want to add it to the list.
<code> fruits := map[string]int{"bananas": 11} // Buy five apples fruits["apples"] = 5 <code>
We can accomplish the same functionality under the functional programming paradigm.
<code> fruits := map[string]int{"bananas": 11} newFruits := map[string]int{"apples": 5} allFruits := make(map[string]int, len(fruits) + len(newFruits)) for k, v := range fruits { allFruits[k] = v } for k, v := range newFruits { allFruits[k] = v } </code>
Since we don’t want to modify the original maps, the code loops through both maps and adds the values to a new map. This way, data remains immutable.
As you can probably tell by the length of the code, however, the performance of this snippet of is much worse than a simple mutable update of the map because we are looping through both maps. This is the exact point at which you trade better code quality for code performance.
Most programmers don’t use higher-order functions often in their code, but it comes in handy to establish currying in functional programming.
Let’s assume we have a simple function that adds two integers. Although this is already a pure function, we want to elaborate on the example to showcase how we can create more advanced functionality through currying.
In this case, we can only accept one parameter. Next, the function returns another function as a closure. Because the function returns a closure, it will memorize the outer scope, which contains the initial input parameter.
<code> func add(x int) func(y int) int { return func(y int) int { return x + y } } </code>
Now let’s try out currying and create more advanced pure functions.
<code> func main() { // Create more variations add10 := add(10) add20 := add(20) // Currying fmt.Println(add10(1)) // 11 fmt.Println(add20(1)) // 21 } </code>
This approach is common in functional programming, although you don’t see it often outside the paradigm.
Recursion is a software pattern that is commonly employed to circumvent the use of loops. Because loops always hold an internal state to know which round they’re at, we can’t use them under the functional programming paradigm.
For example, the below snippet of code tries to calculate the factorial for a number. The factorial is the product of an integer and all the integers below it. So, the factorial of 4 is equal to 24 (= 4 * 3 * 2 * 1).
Normally, you would use a loop for this.
<code> func factorial(fac int) int { result := 1 for ; fac > 0; fac-- { result *= fac } return result } </code>
To accomplish this within the functional programming paradigm, we need to use recursion. In other words, we’ll call the same function over and over until we reach the lowest integer for the factorial.
<code> func calculateFactorial(fac int) int { if fac == 0 { return 1 } return fac * calculateFactorial(fac - 1) } </code>
Let’s sum up what we learned about functional programming:
To learn more about the use cases of pure functions and why they matter, check out this FreeCodeCamp article about the need for pure functions for Redux reducers.
For a good overview of the differences between functional, procedural, and object-oriented programming, or if you want to understand which paradigm fits you best, I recommend reading this insightful Medium post by Lili Ouaknin Felsen.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowuseState
useState
can effectively replace ref
in many scenarios and prevent Nuxt hydration mismatches that can lead to unexpected behavior and errors.
Explore the evolution of list components in React Native, from `ScrollView`, `FlatList`, `SectionList`, to the recent `FlashList`.
Explore the benefits of building your own AI agent from scratch using Langbase, BaseUI, and Open AI, in a demo Next.js project.
Demand for faster UI development is skyrocketing. Explore how to use Shadcn and Framer AI to quickly create UI components.