This page looks best with JavaScript enabled

Go first class functions

 ·  🎃 kr0m

In Go, the fact that functions are first-class citizens means that it is possible to:

  • Assign to variables: Functions can be assigned to variables.
  • Pass as arguments: Functions can be passed as arguments to other functions.
  • Return functions: Functions can be returned from other functions.
  • Store in data structures: Functions can be stored in data structures like slices, maps, etc.

This article is quite extensive, so I have organized it as follows:


If you are new to the world of Go, I recommend the following previous articles:


Anonymous functions:

In this example, we create an anonymous function and assign it to the variable a. This can be particularly useful in some unit tests where it is necessary to “mock” the output of some functions.

package main

import (
    "fmt"
)

func main() {
    a := func() {
        fmt.Println("Hello world first class function")
    }

    a()
    fmt.Printf("%T\n", a)
}
Hello world first class function
func()

Custom function types:

It is possible to define a new function type and then create a variable of this type. This prevents programming errors as the variable will have to comply with the type or it cannot be assigned. It reminds me a bit of the filters imposed by interfaces.

package main

import (
    "fmt"
)

// Custom function type definition
type add func(a int, b int) int

func main() {
    // variable of defined custom type
    var a add = func(a int, b int) int {
        return a + b
    }

    s := a(5, 6)
    fmt.Println("Sum", s)
}
Sum 11

Higher-order functions:

A higher-order function is one that takes one or more functions as input and/or returns a function.

In this example, we see how the simple() function accepts a function as an input parameter. This function will be assigned to the variable f, allowing us to use that variable as a function. On the other hand, in the main() function, we define an anonymous function that we assign to the variable f and then call the simple(f) function.

package main

import (
    "fmt"
)

func simple(f func(a, b int) int) {
    fmt.Println(f(60, 7))
}

func main() {
    f := func(a, b int) int {
        return a + b
    }
    simple(f)
}
67

Returning a function is very similar to allowing functions as input parameters. In the simple() function, we specify the input parameters and return type of the function we will return. Inside simple(), we create an anonymous function and assign it to a variable, and finally, we return that variable. In the main() function, we only need to assign the return value of simple() to a variable and then use it as a function.

package main

import (
	"fmt"
)

func simple() func(a, b int) int {
    f := func(a, b int) int {
        return a + b
    }
    return f
}

func main() {
    f := simple()
    fmt.Println(f(60, 7))
}
67

Closures:

In Go, a closure is a function that references variables outside its own scope. These external variables are captured by the function, which means that the function remembers the environment in which it was created, even after that environment has ended. A closure is useful when you need a function that maintains its own internal state across multiple invocations.

In this example, we can see that the anonymous function assigned to the variable c uses the variable t, which was defined outside the scope of the function. By returning the variable c, we are returning the function plus a pointer to the variable t.

func appendStr() func(string) string {
    t := "Hello"
    c := func(b string) string {
        t = t + " " + b
        return t
    }
    return c
}

You can see a complete example here.

package main

import (
    "fmt"
)

func appendStr() func(string) string {
    t := "Hello"
    c := func(b string) string {
        t = t + " " + b
        return t
    }
    return c
}

func main() {
    a := appendStr()
    fmt.Println(a("World"))
}
Hello World

Changes made to that variable will be remembered by the anonymous function, meaning t will not always be “Hello.” As its value is altered, it will change. In this example, we make a second call and see that the changes to the variable t accumulate.

package main

import (
    "fmt"
)

func appendStr() func(string) string {
    t := "Hello"
    c := func(b string) string {
        t = t + " " + b
        return t
    }
    return c
}

func main() {
    a := appendStr()
    fmt.Println(a("World"))
    fmt.Println(a("Gophers"))
}
Hello World
Hello World Gophers

Practical example:

In this example, we have a function called filter() that expects a slice of students and a function to check if a student is valid as input parameters. It also returns a slice of students with the valid students. In the main() function, we generate the slice of students and the function to check if a student is valid, and finally, we call the filter() function with those parameters.

package main

import (
    "fmt"
)

type student struct {
    firstName string
    lastName  string
    grade     string
    country   string
}

func filter(students []student, checkStudent func(student) bool) []student {
    var validStudents []student
    for _, student := range students {
        if checkStudent(student) == true {
            validStudents = append(validStudents, student)
        }
    }
    return validStudents
}

func main() {
    student1 := student{
        firstName: "Naveen",
        lastName:  "Ramanathan",
        grade:     "A",
        country:   "India",
    }

    student2 := student{
        firstName: "Samuel",
        lastName:  "Johnson",
        grade:     "B",
        country:   "USA",
    }

    studentSlice := []student{student1, student2}

    checkStudent := func(student student) bool {
        if student.grade == "B" {
            return true
        }
        return false
    }

    filteredStudents := filter(studentSlice, checkStudent)
    fmt.Println(filteredStudents)
}
[{Samuel Johnson B USA}]
If you liked the article, you can treat me to a RedBull here