Understanding Functions in Golang

Getting familiar with functions and how to implement and when to use them.

Pizza by Louis Hansel on Unsplash

What are functions or how do we use them and how it is different from methods?

xPhoto by Sigmund on Unsplash

In brief, functions are like simple tasks for which you give input to do something and you get the desired output. We follow functions in our daily lives like cooking food, delivering a letter, or booking a flight. We follow step by step process to complete the overall process. Coming to the context of Go, functions are expressed like this:

Function representation of Go

Representation of a function in Go

As you can see above implementing a function in go is easy and clean. In this, our functionality is to find out the names based on their rank that could be related to marks for say. The parameters need to be defined first and then the type of the parameter should be accordingly to that. As Go is a statically typed language that restricts you from using loose data types. Below is a simple example of how you can use functions in Go: https://go.dev/play/pz/znF6bqobkOQ

Methods are a bit more special than functions as they operate on the addition of parameters in the func keyword to specify the receiver of the method. I will explain this in more depth later.

Declaration in functions

In Go, we can declare functions using the func keyword where we can pass its parameters and return the type of function. Also, if we declare functions with capital letters they will become public and we can export them to different packages. Similarly, with a small case declaration, we can use that function in that particular package. Hence, encapsulation………..
Let’s see that with an example:

Examples of private and public functions in Go

Examples of private and public functions in Go

Most programming languages have functions that accept multiple parameters but Go functions can also return multiple return types that give flexibility to a function to get desired output. Functions are reusable and it becomes easy to understand when it splits further. When functions are declared and if we initialize and declare one value inside, it becomes block-scoped to that function. It cannot be used outside the body of that function.

func celsiusToFahrenheit(c float64) float64 {
 f := (c * 9.0 / 5.0) + 32.0
 fmt.Println(celsius)
 return f
}

func main() {
 celsius := 33.2
 fahrenheit := celsiusToFahrenheit(celsius)
 fmt.Println(fahrenheit, "F°")
}

Hence, we are now familiar with the function declaration, well there’s more in the crux table which is functional programming. Basically, in brief, it says that a function should be pure with no state mutation and side effects. We will get to know more about functional programming in Go in the upcoming blog series. Let’s get to know more about functions.

What are first-class functions?

First-class functions are like when functions are treated as variables or could be passed as an argument, or parameter, or we could also return multiple functions. Let’s get to know more about how we can use functions in different ways:

Multiple return values

In multiple return values, we can pass individual or multiple parameters and in return, we can multiply return values. It’s just simple as that nothing to put on. Let’s get familiar with an example:

func multiplyValues(x int) (int, int) {
    return x * 3, x * 2
}
multiplyValues(3) // 9, 6

Assigning functions to variables

Functions that are assigned via variables could be updated easily when we assign a different function to them. Let’s see an example

func areaOfSqaure() float64 {
 return 45.4 * 45.4
}

func invalidArea() float64 {
 return 0
}

func main() {
 result := areaOfSqaure
 fmt.Println("Result:", result()) // Result: 2061.16
 result = invalidArea
 fmt.Println("Result:", result()) // Result: 0
}

Higher-Order functions

Functions that either accept a function as a type or return a function. We can achieve code reusability with complex code readability while using HOF in Go. Let’s see with an example:

func main() {
 result := areaOfSquare()
 displayResult(5, 5, result)
}

func areaOfSquare() func(int, int) int {
 return func(x, y int) int {
  return x * y
 }
}

func displayResult(x, y int, result func(int, int) int) {
 fmt.Printf("Area of square: %d", result(x, y))
}

Here, we have implemented two functions one with areaOfSqaure and displayResult. In the first function, we are passing a function return type as a function of type int and in the second function, we are passing a function as assigned with a variable result in function params.

Passing functions to other functions

In Go, the function can also be returned to another function as a return type. Let’s get familiar with an example:

func functionReturnFunction() func() int {
 i := 0
 return func() int {
  i++
  return i * i
 }
}

func main() {
 i := functionReturnFunction()
 j := functionReturnFunction() fmt.Println("i1:", i())  // i1: 1
 fmt.Println("i2:", i())  // i2: 4
 fmt.Println("j1:", j())  // j1: 1
 fmt.Println("j2:", j())  // j2: 4
 fmt.Println("i3:", i())  // i3: 9
}

As you can see above in functionReturnFunction we have initialized and declared variable “i” and also declared an anonymous function that can form closures. An anonymous function, also known as function literal in Go, means functions without a name. Function literals are closures because they keep references to the variables in the surrounding scope even after the function is successfully executed. So, here we can see that we have initialized and declared functionReturnFunction via variables like i and j. We see that when we are invoking both functions in different sequences the count of “i” will be maintained when we are invoking a function with the variable “i” and the same happens when we are invoking with the variable “j”. In conclusion, each function execution creates its own variable scope to maintain its functionality.

Passing functions with pointer parameters

Functions with pointer parameters are a pretty straightforward approach to using pointers in functions. In parameter, you need to define the type of parameter with a pointer and when invoking the function, you just need to pass ampersand to the function invocation. Let’s see this in an example:

func functionWithPointers(value *float64) float64 {
 return *value * *value
}

func main() {
 x := 88.4
 fmt.Println(functionWithPointers(&x)) //7814.560000000001 x = 80
 fmt.Println(functionWithPointers(&x)) //6400
}

As pointers are stored in the memory address with the ampersand operator we can receive the memory address value as we can see in functionWithPointers the value which parameters take is dereferencing the value by putting * in front of the value.

Functions with return type pointer

In passing functions with pointer parameters as we saw that we can pass pointer types and with that, we can also pass the pointer types to the return values of the function. Let’s see with an example:

func functionReturnPointers(v float64) *float64 {
 x := v * v
 return &x
}

func main() {
 result := functionReturnPointers(20.5)
 fmt.Println("Dereference result:", *result) 
 // Dereference result: 420.25
 fmt.Println("Without dereference result:", result)
 // Without dereference result: 0xc000018030
}

Hence, we get the desired result with the use of dereferencing the same as what we did above when we are invoking functionWithPointers we also first dereference the values, and when invoking with a memory address, we got the desired result. Suppose, if didn’t use dereference before the result variable, we get the memory address of that variable.

Well, there’s more to another type of function which is a method that accepts the receiver as a special parameter. I will discuss the same in upcoming blogs in detail and how it makes our code much more easier while implementing structs. Stay tuned…..

#WeMakeDevs #hashnode