Saltar al contenido

Funciones

En los ejercicios de las últimas lecciones completaste funciones que ya tenían su primera línea escrita. Al terminar esta lección vas a poder escribirlas enteras: con parámetros, con uno o varios resultados, con resultados con nombre y con una cantidad variable de valores. También vas a usar min y max, y vas a saber leer los errores que aparecen cuando falta un return o cuando los valores no calzan con la función.

Declarar una función

La caja de la Papelería Central calcula muchas veces lo mismo: el precio de un producto por la cantidad que lleva el cliente. En lugar de repetir la cuenta, la escribe una vez, en una función:

main.go
package main

import "fmt"

func precioTotal(precio int, cantidad int) int {
	return precio * cantidad
}

func main() {
	fmt.Println("3 cuadernos:", precioTotal(1500, 3))
	fmt.Println("2 reglas:", precioTotal(800, 2))
}

Salida:

3 cuadernos: 4500
2 reglas: 1600

La primera línea de la función es su firma: dice cómo se llama, qué recibe y qué devuelve.

  • func empieza la declaración, y precioTotal es el nombre.
  • Entre paréntesis van los parámetros: precio int y cantidad int. Cada uno tiene un nombre y, después, su tipo, como en var precio int.
  • Después de la ) va el tipo del resultado: int.

El cuerpo va entre { y }. return precio * cantidad calcula el total, termina la función y devuelve ese valor.

precioTotal(1500, 3) llama a la función. Los valores entre paréntesis son los argumentos, y llegan a los parámetros en orden: precio vale 1500 y cantidad vale 3. La llamada vale lo que devuelve la función, así que en la primera línea de main se imprime 4500. La segunda llamada usa otros valores y da 1600: la cuenta está escrita una sola vez.

Una función se declara fuera de las demás, al mismo nivel que main, y el orden no importa: precioTotal podría ir después de main. main también es una función, sin parámetros ni resultado. Los parámetros son variables de la función: existen solo dentro de su cuerpo, entre sus llaves, así que main no puede usar precio.

Cuando varios parámetros seguidos son del mismo tipo, puedes escribir el tipo una sola vez: func precioTotal(precio, cantidad int) int es la misma firma.

Los argumentos calzan con los parámetros

Cada parámetro recibe un valor. Si en la llamada falta uno, el programa no compila:

main.go
package main

import "fmt"

func precioTotal(precio int, cantidad int) int {
	return precio * cantidad
}

func main() {
	fmt.Println(precioTotal(1500))
}

Salida:

./main.go:10:26: not enough arguments in call to precioTotal
	have (number)
	want (int, int)

Lee el mensaje por partes:

  • ./main.go:10:26 apunta a la llamada, en la línea 10.
  • not enough arguments in call to precioTotal significa “no hay suficientes argumentos en la llamada a precioTotal”.
  • have (number) (“tienes”) dice lo que le pasaste: un valor, un número. number es como Go llama a un número escrito tal cual, que todavía no tiene tipo.
  • want (int, int) (“quiere”) dice lo que espera la función: dos int.

Go no completa los que faltan: no hay valores por defecto. Pasa un valor por cada parámetro, en el orden de la firma: precioTotal(1500, 3). Si pasas de más, el mensaje empieza con too many arguments, con las mismas líneas have y want. Los tipos también tienen que calzar: con precioTotal("1500", 3), Go responde cannot use "1500", porque un texto no sirve donde va un int.

Tampoco puede haber dos funciones con el mismo nombre, aunque reciban parámetros distintos: Go responde precioTotal redeclared in this block (“declarada de nuevo”). Si hacen cosas distintas, dales nombres distintos.

Si vienes de JavaScript, Python o Java…

En JavaScript puedes llamar a una función con menos argumentos (los que faltan valen undefined), y en Python un parámetro puede tener un valor por defecto, como cantidad=1. Go no tiene ninguna de las dos cosas: cada llamada pasa todos los valores.

En Java, dos métodos pueden llamarse igual si reciben parámetros distintos (sobrecarga). En Go no: cada función tiene un nombre propio.

Consejo

Antes de una función, escribe un comentario que empiece con su nombre y diga qué devuelve: // precioTotal devuelve lo que se paga por…. Es la convención de Go para documentar el código (Go Doc Comments), y es la forma de los comentarios de los ejercicios.

Funciones sin resultado

Una función también puede hacer algo sin devolver nada, como imprimir una línea del ticket:

main.go
package main

import "fmt"

func imprimirLinea(producto string, precio int) {
	fmt.Printf("%s: %d\n", producto, precio)
}

func main() {
	imprimirLinea("cuaderno", 1500)
	imprimirLinea("regla", 800)
}

Salida:

cuaderno: 1500
regla: 800

imprimirLinea no tiene tipo de resultado después de la ). No necesita return: termina al llegar a su } (para terminarla antes, existe el return solo, como ves en la sección siguiente). Se llama sola en su línea, como fmt.Println. Como no devuelve nada, su llamada no se puede guardar en una variable: con linea := imprimirLinea("cuaderno", 1500), Go responde que la llamada no tiene valor ((no value) used as value).

return termina la función

return puede ir en cualquier lugar del cuerpo, y la función termina en cuanto se ejecuta. Es el estado del stock de la lección Decisiones con if, ahora como función:

main.go
package main

import "fmt"

func estadoStock(stock int) string {
	if stock == 0 {
		return "Agotado"
	}
	if stock < 5 {
		return "Pocas unidades"
	}
	return "Disponible"
}

func main() {
	fmt.Println(estadoStock(0))
	fmt.Println(estadoStock(3))
	fmt.Println(estadoStock(20))
}

Salida:

Agotado
Pocas unidades
Disponible

Con stock en 0, el primer if se cumple y return "Agotado" termina la función: el segundo if ni siquiera se revisa. Con 3, el primer if no se cumple, el segundo sí, y la función devuelve Pocas unidades. Con 20, ninguno se cumple y se llega al último return.

Como cada if termina en un return, no hace falta else: lo que sigue a un if solo se ejecuta si ese if no se cumplió.

return también termina la función desde dentro de un for o un switch, con el bucle incluido. En la lección Decisiones con switch, la papelería atendía de 12 a 16 y se cortaba la luz. Dentro de una función, el case del corte no necesita una etiqueta:

main.go
package main

import "fmt"

func horasAtendidas(corte int) int {
	horas := 0
	for hora := 12; hora <= 16; hora++ {
		switch {
		case hora == corte:
			return horas
		case hora == 13:
			// Almuerzo: esta hora no se atiende.
		default:
			horas++
		}
	}
	return horas
}

func main() {
	fmt.Println("Corte a las 14:", horasAtendidas(14))
	fmt.Println("Sin corte:", horasAtendidas(0))
}

Salida:

Corte a las 14: 1
Sin corte: 4

Con el corte a las 14, el case hora == corte ejecuta return horas: la función termina ahí, con el switch y el for, y devuelve 1, la hora de las 12. Sin corte, el for termina solo y el último return devuelve 4.

En una función sin resultado, return va solo, sin ningún valor, y la termina en ese punto:

main.go
package main

import "fmt"

func imprimirStock(producto string, stock int) {
	if stock == 0 {
		fmt.Println(producto, "agotado")
		return
	}
	fmt.Println(producto, stock, "unidades")
}

func main() {
	imprimirStock("cuaderno", 24)
	imprimirStock("regla", 0)
}

Salida:

cuaderno 24 unidades
regla agotado

Con stock en 0, imprimirStock imprime regla agotado y el return la termina: la última línea no se ejecuta. Un return con un valor, como return 0, no compilaría aquí, porque la función no declara un tipo de resultado.

Consejo

Resuelve primero los casos especiales, cada uno con su return, y deja el caso normal al final, sin sangría extra, como en estadoStock. Así el camino principal de la función se lee de arriba abajo, sin else anidados. Es una práctica habitual en Go (Go Code Review Comments).

Falta un return

Una función con resultado tiene que terminar siempre en un return. Esta versión de estadoStock cubre todos los casos con sus condiciones, pero no compila:

main.go
package main

import "fmt"

func estadoStock(stock int) string {
	if stock == 0 {
		return "Agotado"
	} else if stock < 5 {
		return "Pocas unidades"
	} else if stock >= 5 {
		return "Disponible"
	}
}

func main() {
	fmt.Println(estadoStock(3))
}

Salida:

./main.go:13:1: missing return

./main.go:13:1 apunta a la } que cierra la función, en la línea 13, y missing return significa “falta un return”. Go revisa la forma del código, no los valores: no sabe que stock >= 5 se cumple siempre que las otras dos condiciones no se cumplen. Para el compilador, el último else if puede no cumplirse, y entonces la función llegaría a su } sin devolver nada.

Para corregirlo, cambia el último else if stock >= 5 por else, o borra ese if y deja return "Disponible" al final, como en el primer ejemplo. Un if con else en el que las dos ramas terminan en return cuenta como un final; un if o un else if solos, no.

Varios resultados

La papelería guarda los lápices en cajas de 12. Una función puede devolver las cajas llenas y los lápices que sobran, los dos a la vez:

main.go
package main

import "fmt"

func cajasYSueltos(lapices int) (int, int) {
	return lapices / 12, lapices % 12
}

func main() {
	cajas, sueltos := cajasYSueltos(30)
	fmt.Println("Cajas:", cajas)
	fmt.Println("Sueltos:", sueltos)
}

Salida:

Cajas: 2
Sueltos: 6

Cuando una función devuelve varios valores, sus tipos van entre paréntesis, separados por comas: (int, int). El return lleva los valores en ese mismo orden, también separados por comas: primero 30 / 12, que da 2, y después 30 % 12, que da 6.

Para recibirlos, escribes una variable por cada valor, como hiciste con strings.Cut en la lección Variables y valores cero: cajas recibe el primero y sueltos, el segundo. Si solo necesitas uno, pon _ en el lugar del otro: cajas, _ := cajasYSueltos(30).

Si recibes los valores en una sola variable, el programa no compila:

main.go
package main

import "fmt"

func cajasYSueltos(lapices int) (int, int) {
	return lapices / 12, lapices % 12
}

func main() {
	cajas := cajasYSueltos(30)
	fmt.Println(cajas)
}

Salida:

./main.go:10:11: assignment mismatch: 1 variable but cajasYSueltos returns 2 values

assignment mismatch significa “la asignación no calza”: 1 variable but cajasYSueltos returns 2 values, hay una variable a la izquierda, pero cajasYSueltos devuelve dos valores. Escribe una variable por cada valor: cajas, sueltos :=, o cajas, _ := si no usas los sueltos.

Por la misma razón, no puedes poner la llamada donde va un solo valor, como en fmt.Println("Cajas:", cajasYSueltos(30)). Go responde con un mensaje que empieza con multiple-value y termina con in single-value context (“varios valores en un lugar de un solo valor”). Recibe antes los dos valores en sus variables y usa esas variables.

Si vienes de JavaScript, Python o Java…

En Python, return cajas, sueltos devuelve una tupla, y en JavaScript lo habitual es devolver un array o un objeto. Los dos se pueden guardar enteros en una sola variable. En Go no: los valores no forman un valor nuevo, y se reciben siempre por separado, uno por variable. En Java, una función devuelve un solo valor; para devolver dos, hace falta una clase o un record.

Resultados con nombre

Los resultados también pueden tener nombre, como los parámetros:

main.go
package main

import "fmt"

func cajasYSueltos(lapices int) (cajas, sueltos int) {
	cajas = lapices / 12
	sueltos = lapices % 12
	return
}

func main() {
	cajas, sueltos := cajasYSueltos(30)
	fmt.Println("Cajas:", cajas, "Sueltos:", sueltos)
}

Salida:

Cajas: 2 Sueltos: 6

(cajas, sueltos int) declara dos variables, cajas y sueltos, que existen desde el principio de la función con su valor cero: 0. El cuerpo les da su valor con =, porque ya existen. Con :=, Go respondería no new variables on left side of :=.

Un return solo, sin valores, devuelve lo que valen esas variables en ese momento: 2 y 6. También puedes escribir los valores, como return lapices / 12, lapices % 12, y la función devuelve esos. Con o sin valores, la función tiene que terminar en un return: si lo borras, Go responde missing return, aunque los resultados tengan nombre.

Los nombres documentan qué devuelve cada valor. Por eso los usa la biblioteca estándar. Esta es la firma de strings.Cut, tal como aparece en su documentación:

func Cut(s, sep string) (before, after string, found bool)

Recibe dos string (s, el texto, y sep, el separador) y devuelve tres valores con nombre: lo que está antes del separador (before), lo que está después (after) y un bool (found, “encontrado”).

Consejo

Pon nombre a los resultados cuando aclaran algo, por ejemplo si la función devuelve dos valores del mismo tipo, como cajas y sueltos. El return solo, sin valores, está bien en una función de pocas líneas; en una más larga, escribe los valores en el return, para que se vea qué devuelve (Go Code Review Comments).

Funciones variádicas

Un pedido puede tener un producto o muchos. Una función variádica recibe una cantidad variable de valores:

main.go
package main

import "fmt"

func totalPedido(precios ...int) int {
	total := 0
	for _, precio := range precios {
		total += precio
	}
	return total
}

func main() {
	fmt.Println(totalPedido(1500, 800, 2300))
	fmt.Println(totalPedido(990))
	fmt.Println(totalPedido())
}

Salida:

4600
990
0

Los tres puntos de precios ...int dicen que el parámetro recibe cualquier cantidad de int, desde ninguno. La primera llamada pasa tres precios; la segunda, uno, y la tercera, ninguno.

Dentro de la función, precios guarda todos esos valores, en orden. Es un slice (una lista de valores del mismo tipo), que veremos en la lección “Slices”. Por ahora basta con saber recorrerlo: for _, precio := range precios { repite el bloque una vez por cada valor, y en cada vuelta precio vale el siguiente. Con range, cada vuelta da dos valores, la posición y el valor; el _ descarta la posición. Sin ningún precio, el for no da ninguna vuelta y total se queda en 0.

Una función variádica puede tener otros parámetros antes, pero el de los tres puntos va siempre al final, porque se queda con todos los valores que quedan en la llamada:

func ticket(cliente string, precios ...int)

Con los tres puntos en otro lugar, Go responde can only use ... with final parameter (“solo puedes usar ... en el último parámetro”).

fmt.Println es variádica: por eso recibe cualquier cantidad de valores.

Si vienes de JavaScript, Python o Java…

En JavaScript escribes ...precios y en Python *precios; en Go los tres puntos van antes del tipo: precios ...int.

min y max

Para quedarte con el menor o el mayor de varios valores, Go tiene dos funciones listas, min y max (desde Go 1.21):

main.go
package main

import "fmt"

func main() {
	stock := 8
	pedido := 12
	fmt.Println("Se envían:", min(pedido, stock))
	fmt.Println("El más caro:", max(1500, 800, 2300))

	precio := 1200
	descuento := 1500
	fmt.Println("A pagar:", max(precio-descuento, 0))
}

Salida:

Se envían: 8
El más caro: 2300
A pagar: 0

min devuelve el menor de los valores que recibe, y max, el mayor. El cliente pide 12 cuadernos y quedan 8: se envían min(pedido, stock), que es 8. max(1500, 800, 2300) da 2300.

max(precio-descuento, 0) es una forma corta de poner un piso: 1200 - 1500 da -300, y max se queda con 0, así que el precio a pagar nunca es negativo. Del mismo modo, min(valor, tope) pone un techo.

min y max son funciones integradas (built-in): son parte del lenguaje, así que se usan sin import y sin nombre de paquete delante. Reciben uno o más valores, nunca ninguno, y todos del mismo tipo: números enteros, números con decimales o strings. Con un int y un float64, el programa no compila.

Reciben los valores escritos uno por uno: no puedes pasarles el parámetro de una función variádica entero.

Si vienes de JavaScript, Python o Java…

Python tiene min y max integradas, como Go. En JavaScript son Math.min y Math.max, y en Java, Math.min y Math.max, que reciben exactamente dos valores. Antes de Go 1.21, Go solo tenía math.Min y math.Max, para float64: si ves en código antiguo un if para quedarse con el menor de dos enteros, era por eso.

En los ejercicios: la función completa

Desde esta lección, en los ejercicios escribes la función entera, firma incluida. El starter solo trae un comentario con su nombre, y el enunciado dice la firma exacta: el nombre, los parámetros con sus tipos y el tipo del resultado. Escríbela fuera de main, igual que en el enunciado.

Como antes, al pulsar Ejecutar, una prueba oculta llama a tu función con varios valores. Si la prueba no la encuentra, verás un error como ./check_test.go:16:13: undefined: precioTotal: la prueba (check_test.go) llama a precioTotal y tu código no tiene ninguna función con ese nombre. Si la encuentra, pero sus parámetros o su resultado no son los del enunciado, el error también sale en check_test.go. En los dos casos, lo que hay que corregir es la primera línea de tu función.

En resumen

  • func nombre(parametro tipo) tipo { … } declara una función, fuera de las demás. Cada llamada pasa un valor por parámetro, en orden, y vale lo que la función devuelve con return.
  • return termina la función en cualquier lugar, también dentro de un for o un switch. Una función con resultado tiene que terminar siempre en un return, o Go responde missing return.
  • Una función puede devolver varios valores, con sus tipos entre paréntesis: (int, int). Se reciben con una variable por valor (cajas, sueltos :=), o con _ en el lugar de los que no usas.
  • Los resultados con nombre, como (cajas, sueltos int), son variables que empiezan en su valor cero; un return solo devuelve lo que valen.
  • Un parámetro como precios ...int, siempre el último, recibe cualquier cantidad de valores. min y max (desde Go 1.21) devuelven el menor y el mayor de sus valores.

Ejercicios

Escribe la función precioTotal(precio int, cantidad int) int, que devuelve lo que se paga por cantidad unidades de un producto que cuesta precio.

Una prueba oculta llama a tu función; main no se ejecuta.

  main.go

Para salir del editor con el teclado, pulsa Escape y luego Tab.

package main

import "fmt"

// Escribe aquí la función precioTotal, fuera de main.

func main() {
	fmt.Println("Caja de la Papelería Central")
}
 

¿Ver la solución?

La solución reemplazará tu código en el editor. Antes de verla, prueba con una pista: se aprende más cuando lo resuelves tú.