Backend Go

10 Ejercicios Básicos de Go para Principiantes

Domina los fundamentos de Go con 10 ejercicios prácticos: desde Hola Mundo hasta estructuras de datos. Código comentado y listo para ejecutar.

14 min
Go Golang Backend Principiantes

Go (o Golang) es uno de los lenguajes de backend de más rápido crecimiento: compila a binario nativo, tiene concurrencia integrada y una sintaxis sorprendentemente limpia. El problema es que muchos tutoriales te lanzan directamente a goroutines y channels sin consolidar los fundamentos.

En este artículo vas a practicar 10 ejercicios progresivos que cubren las bases reales del lenguaje: entrada/salida, tipos, control de flujo, funciones, recursión, slices y structs. Cada ejercicio incluye el código completo con comentarios explicativos para que entiendas por qué funciona, no solo qué hace.

Si ya completaste estos ejercicios y quieres dar el siguiente paso, revisa los proyectos de desarrollo backend a medida donde aplico Go en entornos de producción.


¿Por qué aprender Go con ejercicios prácticos?

Go fue diseñado en Google para resolver problemas reales de ingeniería: compilación rápida, rendimiento cercano a C y gestión de dependencias simple. Hoy es el lenguaje detrás de Docker, Kubernetes, Terraform y decenas de APIs de alto tráfico.

Aprender Go con ejercicios progresivos te da tres ventajas concretas:

  • Memoria muscular: escribir código a mano fija mejor los patrones sintácticos que solo leerlos.
  • Errores tempranos: Go tiene un compilador estricto que te enseña buenas prácticas desde el día uno (variables no usadas = error de compilación).
  • Confianza incremental: pasar de fmt.Println a un struct con métodos en 10 pasos genera confianza real.

Nota: Para ejecutar cualquiera de estos ejercicios necesitas Go instalado. Verifica con go version en tu terminal. Si no lo tienes, descárgalo desde go.dev.


10 ejercicios básicos de Go resueltos

Ejercicio 1: Hola Mundo y saludo personalizado

El punto de partida de cualquier lenguaje. Este ejercicio muestra dos cosas fundamentales de Go: la función fmt.Println para imprimir y dos formas distintas de leer entrada del usuario.

¿Qué aprenderás?

  • Estructura mínima de un programa Go (package main, func main)
  • fmt.Scanln para leer una palabra
  • bufio.NewReader para leer líneas completas con espacios
package main

import (
	"bufio"
	"fmt"
	"os"
	"strings"
)

func main() {
	// Imprimir hola mundo
	fmt.Println("Hola, mundo")

	// Método 1: fmt.Scanln (funciona bien para una sola palabra)
	var name string
	fmt.Print("Ingrese su nombre: ")
	_, err := fmt.Scanln(&name)
	if err != nil {
		fmt.Println("Error al obtener nombre.")
	}
	fmt.Printf("Hola estimado %s\n", name)

	// Método 2: bufio.Reader (recomendado para líneas con espacios)
	reader := bufio.NewReader(os.Stdin)
	fmt.Print("Ingrese su nombre: ")
	name, err = reader.ReadString('\n')
	if err != nil {
		fmt.Println("Error al obtener nombre.")
	}

	// TrimSpace elimina espacios y saltos de línea en los extremos
	name = strings.TrimSpace(name)
	fmt.Printf("Hola estimado %s\n", name)
}

Tip: Prefiere bufio.NewReader cuando el usuario pueda ingresar texto con espacios (nombres compuestos, frases). fmt.Scanln se detiene en el primer espacio.


Ejercicio 2: Suma de dos números con entrada del usuario

Este ejercicio introduce la conversión de tipos (strconv.Atoi) y la extracción de funciones, un hábito clave en Go para mantener el código limpio.

¿Qué aprenderás?

  • Convertir string a int con strconv.Atoi
  • Devolver múltiples valores desde una función (patrón value, error)
  • Manejo explícito de errores con fmt.Errorf
package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

// leerNum encapsula la lectura y conversión de un número entero.
// Devuelve el entero y un error si la entrada es inválida.
func leerNum(reader *bufio.Reader, msg string) (int, error) {
	fmt.Print(msg)
	numString, err := reader.ReadString('\n')
	if err != nil {
		return 0, fmt.Errorf("error al momento de obtener el número")
	}

	numString = strings.TrimSpace(numString)
	num, err := strconv.Atoi(numString)
	if err != nil {
		return 0, fmt.Errorf("el número ingresado es inválido")
	}

	return num, nil
}

func main() {
	reader := bufio.NewReader(os.Stdin)

	num1, err := leerNum(reader, "Ingrese el num 1: ")
	if err != nil {
		fmt.Printf("Num 1 - %s\n", err)
		return
	}

	num2, err := leerNum(reader, "Ingrese el num 2: ")
	if err != nil {
		fmt.Printf("Num 2 - %s\n", err)
		return
	}

	sum := num1 + num2
	fmt.Printf("La suma de %d y %d es %d\n", num1, num2, sum)
}

Patrón clave de Go: devolver (valor, error) en lugar de lanzar excepciones. Obliga a quien llama a decidir qué hacer con el error, lo que produce código más robusto.


Ejercicio 3: Mayor de tres números

Aquí practicamos condicionales y también introducimos los slices, la estructura de datos más usada en Go. Se presentan dos enfoques: el explícito con if/else y el genérico con range.

¿Qué aprenderás?

  • Condicionales con &&
  • Slices ([]int) e iteración con range
  • strings.Join para construir texto desde una lista
package main

import (
	"fmt"
	"strconv"
	"strings"
)

func main() {
	// Enfoque 1: tres variables con if/else
	num1, num2, num3 := 1, 4, 6

	if num1 > num2 && num1 > num3 {
		fmt.Printf("El mayor es %d\n", num1)
	} else if num2 > num3 && num2 > num1 {
		fmt.Printf("El mayor es %d\n", num2)
	} else {
		fmt.Printf("El mayor es %d\n", num3)
	}

	// Enfoque 2: slice de N números con range (más escalable)
	numbers := []int{1, 2, 4, 23, 5, 6, 7}
	mayor := numbers[0]

	for _, num := range numbers {
		if num > mayor {
			mayor = num
		}
	}

	// Construir lista de los demás números para el mensaje
	otros := []string{}
	for _, num := range numbers {
		if num != mayor {
			otros = append(otros, strconv.Itoa(num))
		}
	}

	fmt.Printf(
		"El número %d es mayor que: %s\n",
		mayor,
		strings.Join(otros, ", "),
	)
}

Nota: el Enfoque 2 escala a cualquier cantidad de números sin cambiar la lógica, lo que lo hace más mantenible.


Ejercicio 4: Factorial con bucle y recursión

El factorial es el ejercicio clásico para entender la recursión. Go la soporta sin problema, pero también es ideal para ver cómo un bucle for resuelve el mismo problema de forma iterativa.

¿Qué aprenderás?

  • Bucle for con range sobre un entero (Go 1.22+)
  • Definir y llamar funciones recursivas
  • Cuándo preferir iteración vs recursión
package main

import "fmt"

// recursion calcula el factorial de n de forma recursiva.
// Caso base: factorial de 0 o 1 es 1.
func recursion(num int) int {
	if num == 1 || num == 0 {
		return 1
	}
	return num * recursion(num-1)
}

func main() {
	numberFac := 4

	// Enfoque iterativo con bucle for
	mul := 1
	for num := range numberFac + 1 {
		if num != 0 {
			mul = mul * num
		}
	}

	fmt.Printf("Factorial de %d (bucle):     %d\n", numberFac, mul)
	fmt.Printf("Factorial de %d (recursión): %d\n", numberFac, recursion(numberFac))
}

¿Cuándo usar recursión en Go? Para árboles, grafos o problemas con estructura naturalmente recursiva. Para cálculos simples como el factorial, el bucle es más eficiente porque evita el overhead de llamadas en la pila de ejecución.


Ejercicio 5: Invertir una cadena

Invertir un string parece trivial hasta que te das cuenta de que en Go un string es una secuencia de bytes, no de caracteres. Para manejar correctamente tildes y caracteres Unicode usamos []rune.

¿Qué aprenderás?

  • Diferencia entre string, []byte y []rune
  • Recursión con slices de runas
  • Conversión stringrune
package main

import "fmt"

// revertText recorre el slice de runas de atrás hacia adelante usando recursión.
func revertText(arr []rune, lenArr int) string {
	if lenArr == 0 {
		return ""
	}
	// Toma el último carácter y concatena con el resultado del resto
	return string(arr[lenArr-1]) + revertText(arr, lenArr-1)
}

func main() {
	myText := "Hola amigo"

	// Convertir a []rune para manejar correctamente tildes y Unicode
	runas := []rune(myText)

	fmt.Println(revertText(runas, len(runas)))
	// Output: ogima aloH
}

Regla de oro: siempre usa []rune cuando iteres sobre un string que puede contener caracteres no-ASCII (español, emojis, etc.). Iterar sobre string directamente te da bytes, no caracteres.


Ejercicio 6: Contar vocales en un texto

Un ejercicio que combina iteración sobre strings con una verificación simple. Vemos dos formas de hacerlo: lista manual y strings.Contains.

¿Qué aprenderás?

  • Iteración sobre un string con índices
  • strings.ToLower para normalizar
  • strings.Contains como alternativa elegante a if a == "a" || a == "e"...
package main

import (
	"fmt"
	"strings"
)

func main() {
	text := "abcdaewAOiiII"
	totalCount := 0

	for idx := range text {
		// Normalizar a minúsculas para cubrir A, E, I, O, U también
		char := strings.ToLower(string(text[idx]))

		// strings.Contains verifica si "aeiou" contiene el carácter actual
		if strings.Contains("aeiou", char) {
			totalCount++
		}
	}

	fmt.Printf("El total de vocales en \"%s\" es: %d\n", text, totalCount)
	// Output: El total de vocales en "abcdaewAOiiII" es: 7
}

Alternativa con función propia: si necesitas lógica más compleja (vocales con tilde: á, é, í…) conviene crear una función esVocal(r rune) bool que incluya el mapa completo de vocales acentuadas.


Ejercicio 7: Tabla de multiplicar

Simple pero esencial para consolidar el bucle for en Go y el formateo con fmt.Printf.

¿Qué aprenderás?

  • fmt.Scanln para leer un entero directamente
  • Bucle for con índice clásico
  • Formateo de tabla con Printf
package main

import "fmt"

func main() {
	var num int
	fmt.Print("Ingrese un número: ")
	fmt.Scanln(&num)

	fmt.Printf("\n--- Tabla de multiplicar del %d ---\n", num)
	for i := 1; i <= 10; i++ {
		fmt.Printf("%d x %2d = %d\n", num, i, num*i)
	}
}

Salida esperada para num = 3:

--- Tabla de multiplicar del 3 ---
3 x  1 = 3
3 x  2 = 6
3 x  3 = 9
...
3 x 10 = 30

Ejercicio 8: Calculadora de áreas geométricas

Introducimos switch/case y el paquete math para cálculos geométricos. El switch de Go no necesita break explícito (no hay fall-through por defecto).

¿Qué aprenderás?

  • switch sin break explícito
  • Importar y usar math.Pi y math.Pow
  • Variables float64 para operaciones decimales
package main

import (
	"fmt"
	"math"
)

func main() {
	var opcion int
	fmt.Println("Seleccione la figura:")
	fmt.Println("1. Círculo")
	fmt.Println("2. Rectángulo")
	fmt.Println("3. Triángulo")
	fmt.Print("Opción: ")
	fmt.Scanln(&opcion)

	switch opcion {
	case 1:
		var radio float64
		fmt.Print("Ingrese el radio: ")
		fmt.Scanln(&radio)
		// Fórmula: π * r²
		area := math.Pi * math.Pow(radio, 2)
		fmt.Printf("Área del círculo: %.2f\n", area)

	case 2:
		var base, altura float64
		fmt.Print("Ingrese la base: ")
		fmt.Scanln(&base)
		fmt.Print("Ingrese la altura: ")
		fmt.Scanln(&altura)
		area := base * altura
		fmt.Printf("Área del rectángulo: %.2f\n", area)

	case 3:
		var base, altura float64
		fmt.Print("Ingrese la base: ")
		fmt.Scanln(&base)
		fmt.Print("Ingrese la altura: ")
		fmt.Scanln(&altura)
		// Fórmula: (base * altura) / 2
		area := (base * altura) / 2
		fmt.Printf("Área del triángulo: %.2f\n", area)

	default:
		fmt.Println("Opción inválida")
	}
}

Diferencia con otros lenguajes: en Go el switch hace comparación exacta de valor y no tiene fall-through automático. Si necesitas fall-through explícito, usa la palabra clave fallthrough.


Ejercicio 9: Conversor de temperaturas

Este ejercicio consolida el uso de funciones con parámetros y valores de retorno, más el switch nuevamente para manejar opciones del menú.

¿Qué aprenderás?

  • Funciones con parámetros float64
  • Fórmulas matemáticas en funciones separadas
  • Organización del código por responsabilidad
package main

import "fmt"

// celsiusToFahrenheit convierte grados Celsius a Fahrenheit.
func celsiusToFahrenheit(c float64) float64 {
	return (c * 9 / 5) + 32
}

// fahrenheitToCelsius convierte grados Fahrenheit a Celsius.
func fahrenheitToCelsius(f float64) float64 {
	return (f - 32) * 5 / 9
}

func main() {
	var opcion int
	fmt.Println("Conversor de temperaturas:")
	fmt.Println("1. Celsius → Fahrenheit")
	fmt.Println("2. Fahrenheit → Celsius")
	fmt.Print("Opción: ")
	fmt.Scanln(&opcion)

	switch opcion {
	case 1:
		var c float64
		fmt.Print("Ingrese grados Celsius: ")
		fmt.Scanln(&c)
		fmt.Printf("%.2f °C = %.2f °F\n", c, celsiusToFahrenheit(c))

	case 2:
		var f float64
		fmt.Print("Ingrese grados Fahrenheit: ")
		fmt.Scanln(&f)
		fmt.Printf("%.2f °F = %.2f °C\n", f, fahrenheitToCelsius(f))

	default:
		fmt.Println("Opción inválida")
	}
}

Ejercicio 10: Lista de tareas pendientes (CLI)

El ejercicio más completo de la serie. Introduce structs, slices de structs y funciones que transforman el estado. Es una mini-arquitectura que refleja cómo se organiza el código Go real.

¿Qué aprenderás?

  • Definir y usar struct con campos tipados
  • Modificar slices pasándolos a funciones que devuelven el slice actualizado
  • Separar responsabilidades en funciones: crear, listar, marcar
package main

import "fmt"

// TTodo representa una tarea con ID, nombre, descripción y estado.
type TTodo struct {
	TodoID     int
	TodoName   string
	TodoDesc   string
	TodoStatus bool
}

// newTodo agrega una nueva tarea al slice con ID autoincremental.
func newTodo(todos []TTodo, nueva TTodo) []TTodo {
	latestIdx := 1
	if len(todos) > 0 {
		lastItem := todos[len(todos)-1]
		if lastItem.TodoID > 0 {
			latestIdx = lastItem.TodoID + 1
		}
	}
	nueva.TodoID = latestIdx
	return append(todos, nueva)
}

// listTodos imprime todas las tareas con su estado actual.
func listTodos(todos []TTodo) {
	fmt.Println("===== Lista de tareas =====")
	for idx, item := range todos {
		statusStr := "pendiente"
		if item.TodoStatus {
			statusStr = "✓ completado"
		}
		fmt.Printf(
			"[%d] ID:%d | %s | %s | %s\n",
			idx+1, item.TodoID, item.TodoName, item.TodoDesc, statusStr,
		)
	}
	fmt.Println("===========================")
}

// markCompleted busca la tarea por ID y la marca como completada.
func markCompleted(todos []TTodo, taskID int) []TTodo {
	for idx, item := range todos {
		if item.TodoID == taskID {
			todos[idx].TodoStatus = true
			break
		}
	}
	return todos
}

func main() {
	myTodos := []TTodo{}

	myTodos = newTodo(myTodos, TTodo{TodoName: "Aprender structs", TodoDesc: "Estudiar tipos compuestos en Go"})
	myTodos = newTodo(myTodos, TTodo{TodoName: "Practicar slices", TodoDesc: "Ejercicios con append y range"})
	myTodos = newTodo(myTodos, TTodo{TodoName: "Escribir tests", TodoDesc: "Usar el paquete testing"})

	fmt.Println("--- Estado inicial ---")
	listTodos(myTodos)

	myTodos = markCompleted(myTodos, 1)

	fmt.Println("\n--- Después de completar la tarea 1 ---")
	listTodos(myTodos)
}

Salida esperada:

--- Estado inicial ---
===== Lista de tareas =====
[1] ID:1 | Aprender structs | Estudiar tipos compuestos en Go | pendiente
[2] ID:2 | Practicar slices | Ejercicios con append y range | pendiente
[3] ID:3 | Escribir tests | Usar el paquete testing | pendiente
===========================

--- Después de completar la tarea 1 ---
===== Lista de tareas =====
[1] ID:1 | Aprender structs | Estudiar tipos compuestos en Go | ✓ completado
[2] ID:2 | Practicar slices | Ejercicios con append y range | pendiente
[3] ID:3 | Escribir tests | Usar el paquete testing | pendiente
===========================

Conceptos clave aprendidos en estos 10 ejercicios

EjercicioConcepto principalPaquetes utilizados
1 - Hola Mundofmt, bufio, entrada de usuariofmt, bufio, os, strings
2 - SumaFunciones, errores, conversión de tiposstrconv, fmt, bufio
3 - MayorSlices, range, strings.Joinstrconv, strings
4 - FactorialRecursión vs iteraciónfmt
5 - Invertir stringRunas, Unicode, recursiónfmt
6 - Vocalesstrings.Contains, normalizaciónstrings
7 - TablaBucle for clásico, Printffmt
8 - Áreasswitch, math.Pi, math.Powmath, fmt
9 - ConversorFunciones con float64fmt
10 - Tareas CLIStructs, slices de structs, métodosfmt

Mejores prácticas que refuerzan estos ejercicios

  1. Maneja siempre los errores: Go obliga a declarar el error y el compilador advierte si lo ignoras. El patrón valor, err := func() seguido de if err != nil es idiomático y no es opcional.

  2. Nombre descriptivos para funciones: leerNum, markCompleted, revertText son más claros que fn1 o process. Go valora la claridad sobre la brevedad en los nombres.

  3. Evita variables no usadas: Go lanza error de compilación si declaras una variable y no la usas. Es una feature, no un bug: elimina código muerto desde el principio.

  4. Prefiere []rune sobre []byte para texto: cuando trabajes con strings que pueden tener caracteres no-ASCII (español, japonés, emojis), convierte a []rune para no corromper los caracteres al iterar.

  5. Funciones pequeñas con una responsabilidad: los ejercicios 2 y 10 muestran cómo extraer leerNum o newTodo hace el main legible y las funciones fáciles de testear de forma aislada.


Conclusión

Estos 10 ejercicios cubren el núcleo fundamental de Go: tipos, control de flujo, funciones, errores, slices y structs. Con estos bloques puedes construir desde scripts CLI hasta APIs REST básicas.

El siguiente nivel natural es trabajar con goroutines y channels (la concurrencia nativa de Go), interfaces y el paquete net/http para construir servicios web. Pero sin estos fundamentos sólidos, esos temas avanzados se vuelven confusos.

¿Estás construyendo un backend en Go para tu empresa o startup? Cuéntame tu proyecto aquí y te respondo en menos de 24 horas.

¿Tienes un proyecto en mente?

Convierte tu idea en un producto real

Desarrollo web, aplicaciones a medida y consultoría tecnológica para empresas y startups. Cuéntame tu proyecto y te respondo en menos de 24 horas.

Solicitar presupuesto Ver LinkedIn