Jesús Mendoza

Notas de Go - Funciones, Closure y Defer

Funciones

Las funciones son una pieza de código independiente que puede aceptar 0 o más parámetros y puede devolver 0 o más valores.

Ya hemos visto funciones anteriormente, la función main donde ejecutamos todo nuestro código

func main() {}

Función con parámetros

Como mencionamos anteriormente, las funciones pueden aceptar 0 o más parámetros. Una función que acepta parámetros se puede definir de la siguiente manera:

func add(x int, y int) {
  fmt.Println(x + y)
}

func main() {
  add(1, 2) // 3
}

En el ejemplo anterior vemos como la función add acepta los parámetro x y y y ámbos son de tipo int.

Si queremos retornar el valor de la suma podemos hacerlo de la siguiente manera:

func add(x int, y int) int {
  return x + y
}

func main() {
  result := add(1, 2)
  fmt.Println(result) // 3
}

La ventaja de devolver 1 o más valores es que podemos almacenarlo en variables, hacer comparaciones, etc.

Para devolver multiples valores podemos hacerlo de la siguiente manera:

func add(x int, y int) (int, string) {
  result := x + y
  message := "El resultado es "

  return result, message
}

func main() {
    result, message := add(1, 2)
    string := fmt.Sprintln(message, result)

    fmt.Println(string)
}

En el ejemplo anterior estamos retornando dos valores usando la palabra reservada return y separando ambos valores usando una coma. Puedes retornar cuantos valores desees de tus funciones.

Las funciones no tienen acceso a nada de lo que esté dentro de la función main o dentro de otras funciones, las únicas formas de acceder es pasando dichos valores como argumentos, declarando dichos valores en scope global, es decir, fuera de nuestras funciones o que estemos creando un closure. Por ejemplo:

x := 5

func add(y int) int {
  return x + y
}

func main() {
  y := 2

  fmt.Println(add(y))
}

En el ejemplo anterior nuestra función add no tiene acceso a y así que tenemos que pasarle dicho valor como argumento y tiene acceso a x gracias a que lo definimos en el scope global, es decir, afuera de todas nuestras funciones.

Si nuestra función acepta muchos parametros del mismo tipo, podemos usar la expresión ..., esto solo lo podemos hacer en el último parámetro. Por ejemplo:

func add(args ...int) int {
  total := 0

  for _, value := range args {
    total = total + value
  }

  return total
}

func main() {
  values := []int{1,2,3}
  result1 := add(values...)
  result2 := add(1, 2, 3, 4, 5, 6)

  fmt.Println(result1) // 6
  fmt.Println(result2) // 21
}

En el ejemplo anterior el parámetro args recibirá 0 o más valores, luego usamos un ciclo for para recorrer dichos valores y adicionarlos a la variable total.

Closure

Es posible crear funciones dentro de otras funciones

func main() {
  x := 5

  add := func(y int) int {
    return x + y
  }

  result := add(5)

  fmt.Println(result) // 10
}

En el ejemplo anterior estamos creando una variable que pertenece a la función main y tiene como valor una función. La variable x también pertenece a la función main por lo que tenemos acceso a ella desde dentro de la función almacenada en la variable add.

Recursión

Es cuando una función se llama así misma hasta que cumpla cierta condición. Por ejemplo:

func factorial(x uint) uint {
  if x == 0 {
    return 1
  }

  return x * factorial(x-1)
}

En el ejemplo anterior la función factorial se llama así misma hasta que cumpla la condición de que x == 0. Si no agregamos una condición la función se llamará así misma infinitamente, así que debemos tener cuidado con eso.

Defer

Go tiene una expresión especial que nos permite agendar que una función se ejecute luego de que otra haya terminado. Por ejemplo:

func first() {
  fmt.Println("First function")
}

func second() {
  fmt.Println("Second function")
}

func main() {
  defer second()
  first()

  // First function
  // Second function
}

Como podemos observar en el ejemplo anterior, la llamada a la segunda función está antes que la primera, por lo que debería aparecer primero "Second function", pero como estamos usando la expersión defer, la ejecución de la función second se ejecuta luego de que la función first ha terminado.

Usualmente usamos defer cuando queremos liberar algún recurso que hayamos usado. Por ejemplo:

f, _ := os.Open("file.txt")
defer f.Close()

// rest of our code

Esto tiene varios beneficios. El primero es que nuestro código es más legible ya que podemos ver donde leémos y donde cerramos nuestro archivo en el mismo lugar. Segundo, si tenemos multiples return dentro de un if y else el Close sucederá antes que estos. Tercero, el defer se ejecutará incluso si se ejecuta un panic

Panic & Recover

Panic se encarga de lanzar un error de ejecución y Recover se encarga de manejar ese error. recover para el panic y retorna el valor que se pasó como argumento a panic. Podemos estar tentados a usarlo de la siguiente manera:

func main() {
  panic("Algo malo sucedió")
  error := recover()

  fmt.Println(error)
}

Pero en este caso el recover nunca sucederá ya que el panic para la ejecución de nuestro programa. Para que esto funcione debemos combinarlo con un defer.

func main() {
  defer func() {
    str := recover()

    fmt.Println(str)
  }()

  panic("Algo malo sucedió")
}

En este caso primero ejecutamos la función con defer, esto hará que el programa no se cierre hasta que dicha función retorne, por lo que al ejecutar el panic la función con defer aún no ha sido ejecutada, por lo tanto es capáz de hacer un recover del panic.

En resumen

Hemos observado que las funciones son una parte fundamental de todos nuestros programas, nos permiten encapsular partes de nuestro código y hacer que este sea más modular y legible.

En el próximo post seguiremos hablando sobre los tipos de datos compuestos que faltan (struct, pointers, channels, interfaces, etc).