Lección 9 · Control de flujo y funciones
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:
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.
funcempieza la declaración, yprecioTotales el nombre.- Entre paréntesis van los parámetros:
precio intycantidad int. Cada uno tiene un nombre y, después, su tipo, como envar 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:
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:26apunta a la llamada, en la línea 10.not enough arguments in call to precioTotalsignifica “no hay suficientes argumentos en la llamada aprecioTotal”.have (number)(“tienes”) dice lo que le pasaste: un valor, un número.numberes 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: dosint.
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:
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:
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:
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:
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:
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:
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:
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:
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:
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):
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 conreturn.returntermina la función en cualquier lugar, también dentro de unforo unswitch. Una función con resultado tiene que terminar siempre en unreturn, o Go respondemissing 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; unreturnsolo devuelve lo que valen. - Un parámetro como
precios ...int, siempre el último, recibe cualquier cantidad de valores.minymax(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.
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")
}