In a previous article, we covered the basics of the Go language
Go basics
. This time, we will delve deeper into Go’s functions.
-
Function declaration.
-
Multiple return values.
-
Functions as types.
-
Recursivity.
-
Methods.
-
Functions with variable input.
If you are new to the world of Go, I recommend the following previous articles:
Function declaration:
Inside the parentheses, the types of the input variables are indicated.
package main
import "fmt"
func main() {
repeat("AlfaExploit", 5)
}
func repeat(word string, reps int) {
for i := 0; i < reps; i++ {
fmt.Println(word)
}
}
If the function returns a value, the type of the returned value will be indicated outside the parentheses:
package main
import "fmt"
func addition(a, b int) int {
return a + b
}
func main() {
result := addition(3, 5)
fmt.Println(result)
}
Multiple return values:
It is similar to returning an array of values.
package main
import "fmt"
func divisionAndRemainder(dividend, divisor int) (int, int) {
quotient := dividend / divisor
remainder := dividend % divisor
return quotient, remainder
}
func main() {
quotient, remainder := divisionAndRemainder(10, 3)
fmt.Println(quotient, remainder)
}
Functions as types:
It’s about using a variable as a function, in this example, the variable operation
behaves as if it were the multiplication
function.
package main
import "fmt"
type Operation func(int, int) int
func multiplication(a, b int) int {
return a * b
}
func main() {
var operation Operation = multiplication
result := operation(2, 3)
fmt.Println(result)
}
Recursivity:
Recursion can consume more resources than non-recursive solutions, but it simplifies the code.
A basic example would be calculating the factorial of a given number.
package main
import "fmt"
func factorial(n int) int {
if n <= 1 {
return 1
}
return n * factorial(n-1)
}
func main() {
result := factorial(3)
fmt.Println(result)
}
As an example, let’s assume n is 3, so the code of the factorial function that calls itself would be equivalent to calculating:
n * factorial(n-1)
factorial(3): 3 * factorial(3-1) -> 3 * factorial(2)
factorial(2): 2 * factorial(2-1) -> 2 * factorial(1)
factorial(1): 1
Therefore, we can see that:
factorial(3): 3 * factorial(2)
factorial(2): 2 * factorial(1)
factorial(1): 1
So, substituting, we get:
factorial(3): 3 * factorial(2)
factorial(2): 2 * 1
And finally:
factorial(3): 3 * 2 * 1
Another really useful example is when we need to list a directory tree:
package main
import (
"fmt"
"io/ioutil"
"log"
"path/filepath"
)
func scanDirectory(path string) error {
fmt.Println(path)
files, err := ioutil.ReadDir(path)
if err != nil {
fmt.Printf("Returning error from scanDirectory(\"%s\") call\n", path)
return err
}
for _, file := range files {
filePath := filepath.Join(path, file.Name())
if file.IsDir() {
err := scanDirectory(filePath)
if err != nil {
fmt.Printf("Returning error from scanDirectory(\"%s\") call\n", path)
return err
}
} else {
fmt.Println(filePath)
}
}
return nil
}
func main() {
err := scanDirectory("my_huge_directory")
if err != nil {
log.Fatal(err)
}
}
Methods:
Go is not an object-oriented language in the traditional sense, but it supports encapsulation, interfaces, and methods. In this section, we’ll focus on the latter to understand how to link methods to types.
Methods offer several advantages over using functions:
- Methods are logically linked to types.
- Method names can be reused across different types.
In this example, we’ll link a structure type to the area()
method. In this method, we obtain a copy of the object itself, which Go refers to as a receiver
; in some programming languages, this is known as this
. The idiomatic way to name the receiver is to use the first letter of the object type.
package main
import "fmt"
type rectangle struct {
Base int
Height int
}
func (r rectangle) area() int {
return r.Base * r.Height
}
func main() {
// Assign structure variable values
rect := rectangle{Base: 3, Height: 4}
// rect structure moreover has area method available
// Call structure method
area := rect.area()
fmt.Println(area)
}
Methods don’t have to be limited to structures; they can also be linked to primitive types. To do this, you need to create an alias for the type, in this case, myInt
, and assign it as the type of the method’s receiver. In this example, we’ve also defined the method’s input parameter and return value to be of the same type. Let’s see the following example.
package main
import "fmt"
type myInt int
func (a myInt) add(b myInt) myInt {
return a + b
}
func main() {
num1 := myInt(5)
num2 := myInt(10)
sum := num1.add(num2)
fmt.Println("Sum is: ", sum)
}
Functions with variable input:
A variadic function is one that has a variable number of input parameters. The only requirements are that all variadic variables are of the same type and that it is the last one in the list of arguments.
These variables receive the arguments separated by commas and convert them into an array of elements. We can see its functionality in the following example.
package main
import "fmt"
func addition(nums ...int) int {
fmt.Println(nums)
result := 0
for _, num := range nums {
result += num
}
return result
}
func main() {
total := addition(1, 2, 3)
fmt.Println(total)
total = addition(1, 2, 3, 4)
fmt.Println(total)
}
If, on the other hand, we have a slice and want to pass it to the function, we must indicate it so that the function does not perform a double conversion from array to array.
package main
import "fmt"
func addition(nums ...int) int {
fmt.Println(nums)
result := 0
for _, num := range nums {
result += num
}
return result
}
func main() {
testSlice := []int{1, 2, 3}
total := addition(testSlice...)
fmt.Println(total)
testSlice = []int{1, 2, 3, 4}
total = addition(testSlice...)
fmt.Println(total)
}