Saltar al contenido principal

Go desde cero, parte 4: funciones, retornos múltiples y closures

> serie: aprende go de 0 a 100 | artículo 4 de 19


Las funciones en Go parecen las de cualquier lenguaje hasta que devuelven dos cosas a la vez y no pasa nada raro. Hoy cubrimos la sintaxis, los retornos múltiples, las variádicas, las closures y defer, el mejor amigo de tus recursos abiertos.


Sintaxis: el tipo va detrás

// tipo después del nombre, como en los diagramas UML
func suma(a int, b int) int {
  return a + b
}

// parámetros consecutivos del mismo tipo se agrupan
func suma(a, b int) int {
  return a + b
}

El tipo después del nombre choca los primeros diez minutos y luego se lee mejor, sobre todo en firmas complejas. La justificación oficial está en el blog de Go (Go’s Declaration Syntax). Otra diferencia con Java: no hay sobrecarga de funciones. Un nombre, una firma (FAQ oficial).

Retornos múltiples: el patrón que lo cambia todo

Una función Go puede devolver varios valores sin envoltorios: ni tuplas de Python, ni arrays destructurados de JS, ni clases Result (A Tour of Go, multiple results y Effective Go):

// Dos valores de vuelta: resultado y error.
// Este es el patrón de Go. Grábatelo.
func divide(a, b float64) (float64, error) {
  if b == 0 {
    return 0, errors.New("división entre cero")
  }
  return a / b, nil
}

// En el sitio de la llamada:
resultado, err := divide(10, 3)
if err != nil {
  // gestionar y salir pronto
}

Ese dúo (valor, error) es la columna vertebral del manejo de errores en Go y tiene artículo propio (el 9). De momento quédate con la forma: se devuelve el error como valor normal y se comprueba con un if a la vuelta.

Los retornos también pueden llevar nombre. Úsalo con moderación, en funciones cortas documenta, en largas despista.

Variádicas: los tres puntos

El equivalente al *args de Python o al rest de JS (especificación, function types):

// ...int: cero o más enteros. Dentro es un slice.
func maximo(nums ...int) int {
  m := nums[0]
  for _, n := range nums[1:] {
    if n > m {
      m = n
    }
  }
  return m
}

maximo(3, 7, 2)
valores := []int{3, 7, 2}
maximo(valores...) // un slice se "despliega" con ...

Dentro de la función, nums es un slice normal. Y ojo al detalle de la última línea: … también funciona al revés, desplegando un slice en argumentos individuales. fmt.Println es variádica, por eso traga lo que le eches.

Closures: funciones con mochila

Las funciones son valores de primera clase: se asignan, se pasan y se devuelven. Y cuando una función devuelta captura variables de su entorno, tienes una closure (A Tour of Go, closures):

func contador() func() int {
  n := 0
  return func() int {
    n++ // captura n por referencia
    return n
  }
}

c1 := contador()
c2 := contador()
fmt.Println(c1(), c1(), c1()) // 1 2 3
fmt.Println(c2()) // 1 (estado propio)

Cada llamada a contador() crea un n nuevo e independiente que sobrevive entre llamadas. Si vienes de JS esto es terreno conocido. Si vienes de Java, es lo que las lambdas quisieron ser cuando fueran mayores: aquí la captura es por referencia y sin exigir final.

defer: la limpieza garantizada

defer aplaza una llamada hasta que la función que la contiene retorna, por el camino que sea, incluido un error a mitad (A Tour of Go, defer y Effective Go, defer):

func leeArchivo(ruta string) error {
  f, err := os.Open(ruta)
  if err != nil {
   return err
  }
  defer f.Close() // se ejecuta al salir, pase lo que pase

  // ... trabajar con f tranquilamente ...
  return nil
}

Es el try/finally de Java o el with de Python, pero pegado a la línea que abre el recurso: abres y en la línea siguiente dejas programado el cierre. Imposible olvidarlo 40 líneas después, que es justo el punto fuerte de las funciones en Go con defer.

Detalle clave: los defer apilados se ejecutan en orden LIFO, el último registrado sale primero. Y los argumentos se evalúan en el momento del defer, no al ejecutarse.

Funciones en Go: Diagrama pixelart de tres llamadas defer apilándose en una pila LIFO que se ejecuta en orden inverso 3, 2, 1
[defer apila y desapila: el último en entrar es el primero en salir.]


Reto de la semana: pon a prueba las funciones en Go

  • Escribe divide(a, b float64) (float64, error) y gestiona la división entre cero en el sitio de la llamada.
  • Crea una variádica media(nums …float64) float64 y llámala con argumentos sueltos y con un slice desplegado.
  • Construye una closure acumulador() que sume lo que le pases en cada llamada y demuestra que dos acumuladores no comparten estado.
  • Bonus: apila tres defer con fmt.Println y predice el orden de salida antes de ejecutar.

Funciones en Go: nivel completado.

En el próximo artículo: structs, métodos y por qué Go no necesita clases para vivir bien.

> exit

Retrato pixel art de Jenniffer Cubillos

gracias por leer