En Go que las funciones sean de primera clase significa que es posible:
- Asignación a variables: Las funciones pueden ser asignadas a variables.
- Pasar como argumentos: Las funciones pueden ser pasadas como argumentos a otras funciones.
- Retorno de funciones: Las funciones pueden ser retornadas desde otras funciones.
- Almacenamiento en estructuras: Las funciones pueden ser almacenadas en estructuras de datos como slices, mapas, etc.
Este artÃculo es bastante extenso asà que lo he organizado de la siguiente manera:
- Anonymous functions.
- Tipos de funciones personalizados.
- Higher-order functions.
- Closures.
- Ejemplo práctico.
Si eres nuevo en el mundo de Go te recomiendo los siguientes artÃculos anteriores:
- Go basics
- Go functions
- Go unit tests
- Go pointers
- Go interfaces
- Go structs
- Go generics
- Go errors
- Go routines
- Go strings, chars y runas
- Go defer
Anonymous functions:
En este ejemplo creamos una función anónima y la asignamos a la variable a. Esto puede resultar realmente útil en algunos tests unitarios donde es necesario “mockear” la salida de algunas funciones.
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()
Tipos de funciones personalizados:
Es posible definir un tipo nuevo de función, para luego crear una variable de este tipo, esto evita errores de programación ya que la variable tendrá que cumplir con el tipo o no podrá ser asignada, me recuerda un poco a los filtros impuestos por las 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:
Una función de orden superior es aquella que tiene una o mas funciones como entrada y/o valor de retorno.
En este ejemplo vemos como la función simple() acepta una función como parámetro de entrada, esta función será asignada a la variable f pudiendo utilizar dicha variable como función. Por otro lado en la función main() definimos una función anónima que asignamos a la variable f para acto seguido llamar a la función simple(f).
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
Retornar una función es muy similar a permitir funciones como parámetros de entrada, en la función simple() se indica los parámetros de entrada y retorno de la función que retornaremos, dentro de simple() creamos una función anónima y la asignamos a una variable, finalmente retornamos dicha variable. En la función main() tan solo debemos asignar el valor de retorno de simple() a una variable y luego utilizarla como función.
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:
En Go, un closure es una función que referencia variables fuera de su propio alcance, estas variables externas son capturadas por la función, lo que significa que la función recuerda el entorno en el que fue creada, incluso después de que el entorno haya terminado. Un closure es útil cuando necesitas una función que mantenga su propio estado interno a lo largo de múltiples invocaciones.
En este ejemplo podemos ver que la función anónima asignada a la variable c hace uso de la variable t que fué definida fuera del alcance de la función, al retornar la variable c estamos retornando la función mas un puntero a la variable t.
func appendStr() func(string) string {
t := "Hello"
c := func(b string) string {
t = t + " " + b
return t
}
return c
}
Podemos ver un ejemplo entero aquÃ.
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
Los cambios que se hagan sobre esa variable serán recordados por la función anónima, es decir t no valdrá siempre “Hello”, conforme se vaya alterando su valor, irá cambiando. En este ejemplo hacemos una segunda llamada y vemos que los cambios sobre la variable t se van acumulando.
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
Ejemplo práctico:
En este ejemplo tenemos una función lamada filter() que espera como parámetros de entrada un slice de alumnos y una función con la que comprobar si un alumno es válido, además retornará un slice de alumnos con los alumnos aptos.
En la función main() generamos el slice de alumnos y la función con la que comprobar si un alumno es válido para finalmente llamar a la función filter() con dichos parámetros.
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}]