Programación · n.º 14 entre los lenguajes más usados

Aprende Go desde cero

Sencillo y rápido: servidores, herramientas de la nube y programas concurrentes.

  • 15 lecciones
  • 19 ejercicios interactivos
  • 23 conceptos clave
  • 16,5 % lo usa*

Qué es Go y para qué sirve

Go (o Golang) es un lenguaje creado en Google con una idea: que sea simple de leer y rápido de compilar. Genera un único ejecutable sin dependencias y trae de serie herramientas para la concurrencia. Docker y Kubernetes, por ejemplo, están escritos en Go.

  • Servidores y APIs
  • Herramientas de la nube y DevOps
  • Programas de terminal

Para empezar: Prueba los ejemplos en el Go Playground oficial o instala Go desde go.dev y ejecútalos con go run main.go.

Nivel 1 Básico

Lección 1 · Básico

Tu primer programa

Un programa de Go empieza en la función main del paquete main. Con import traes paquetes de la biblioteca estándar, como fmt, que escribe en la consola.

go run main.go compila y ejecuta en un paso; go build genera el ejecutable.

Ejemplo práctico
package main

import "fmt"

func main() {
	fmt.Println("¡Hola, mundo!")
	fmt.Println(2 + 3) // 5
}

Ejercicio

Completa la función que escribe una línea.

fmt.("Hola")
Ver la solución

Respuesta: Println. fmt.Println escribe sus argumentos y un salto de línea.

Lección 2 · Básico

Variables y tipos

:= declara una variable y deduce su tipo a la vez; var sirve para declararla con un tipo explícito. Los tipos básicos son int, float64, bool y string.

Una variable sin valor toma el «valor cero» de su tipo: 0, false o el texto vacío. Y si declaras una variable y no la usas, Go no compila.

Ejemplo práctico
package main

import "fmt"

func main() {
	nombre := "Ana"
	edad := 30
	var precio float64 = 19.99
	var activo bool // false (valor cero)

	fmt.Printf("%s tiene %d años. Total: %.2f. Activo: %t\n", nombre, edad, precio*3, activo)
}

Ejercicio

¿Qué hace edad := 30?

Ver la solución

Respuesta: Declara edad y deduce que es int. := declara una variable nueva e infiere su tipo a partir del valor.

Lección 3 · Básico

Condiciones, for y switch

Go solo tiene un bucle: for. Con tres partes es un contador clásico, con una condición funciona como un while y con range recorre colecciones.

if y switch no llevan paréntesis, pero las llaves son obligatorias. En switch no hace falta break.

Ejemplo práctico
package main

import "fmt"

func main() {
	for i := 1; i <= 3; i++ {
		fmt.Println("vuelta", i)
	}

	nota := 7
	switch {
	case nota >= 9:
		fmt.Println("Sobresaliente")
	case nota >= 5:
		fmt.Println("Aprobado")
	default:
		fmt.Println("Suspenso")
	}
}

Ejercicio

¿Cuántas palabras clave de bucle tiene Go?

Ver la solución

Respuesta: Una: for. Go solo tiene for, que cubre todos los casos.

Encuentra los errores

Este programa debería mostrar el total y la media de 1, 2 y 3, pero Go se niega a compilarlo. Tiene 2 errores.

Ver la solución
  • Línea 10: Go no permite declarar una variable y no usarla: da el error «declared and not used». Si la media hace falta, hay que mostrarla. Corrección: fmt.Println("Media:", total/3)
  • Línea 11: No se puede sumar un texto y un número con +: Go no convierte tipos automáticamente. Println acepta varios valores separados por comas. Corrección: fmt.Println("Total:", total)

Lección 4 · Básico

Slices y maps

Un slice es una lista de tamaño variable: []int{7, 8, 9}. append añade elementos y devuelve el slice nuevo, por eso se reasigna. len dice cuántos hay.

Un map guarda pares clave–valor. Al leer una clave puedes pedir un segundo valor que indica si existía.

Ejemplo práctico
package main

import "fmt"

func main() {
	notas := []int{7, 8, 9}
	notas = append(notas, 10)
	for i, n := range notas {
		fmt.Println(i, n)
	}

	stock := map[string]int{"teclado": 10, "raton": 0}
	if unidades, ok := stock["webcam"]; !ok {
		fmt.Println("No hay webcams en el catálogo", unidades)
	}
}

Ejercicio

Completa para añadir un 6 al slice.

notas = (notas, 6)
Ver la solución

Respuesta: append. append devuelve un slice con el elemento añadido; hay que guardarlo de nuevo en la variable.

Escribe el código

Escribe func contarPalabras(texto string) map[string]int: separa el texto en palabras con strings.Fields y devuelve cuántas veces aparece cada una.

Tab escribe una sangría; para salir del editor con el teclado, pulsa Esc y luego Tab. Ctrl + Intro corrige.

Ver la solución
func contarPalabras(texto string) map[string]int {
	cuenta := make(map[string]int)
	for _, palabra := range strings.Fields(texto) {
		cuenta[palabra]++
	}
	return cuenta
}

Leer una clave que no existe en un mapa de Go da el valor cero del tipo (0 para int), así que cuenta[palabra]++ sirve también para la primera aparición.

Nivel 2 Intermedio

Lección 5 · Intermedio

Funciones y errores

Las funciones pueden devolver varios valores. Por convención, las que pueden fallar devuelven el resultado y un error, que vale nil si todo fue bien.

Go no tiene excepciones para los fallos normales: compruebas el error justo después de cada llamada.

Ejemplo práctico
package main

import (
	"errors"
	"fmt"
)

func dividir(a, b float64) (float64, error) {
	if b == 0 {
		return 0, errors.New("división entre cero")
	}
	return a / b, nil
}

func main() {
	if r, err := dividir(10, 4); err == nil {
		fmt.Println(r) // 2.5
	}
	if _, err := dividir(1, 0); err != nil {
		fmt.Println("Error:", err)
	}
}

Ejercicio

¿Qué valor tiene err cuando la función ha ido bien?

Ver la solución

Respuesta: nil. Un error igual a nil significa que no ha habido ningún problema.

Escribe el código

Escribe func dividir(a, b float64) (float64, error): si b es 0, devuelve 0 y un error creado con errors.New; si no, el resultado y nil.

Tab escribe una sangría; para salir del editor con el teclado, pulsa Esc y luego Tab. Ctrl + Intro corrige.

Ver la solución
func dividir(a, b float64) (float64, error) {
	if b == 0 {
		return 0, errors.New("no se puede dividir entre 0")
	}
	return a / b, nil
}

En Go los errores son un valor más que se devuelve. Quien llama comprueba if err != nil justo después, sin excepciones.

Lección 6 · Intermedio

Structs y métodos

Un struct agrupa campos con nombre. Los métodos se declaran fuera, indicando antes del nombre a qué tipo pertenecen (el receptor).

Si el método modifica el struct, el receptor debe ser un puntero (*Cuenta).

Ejemplo práctico
package main

import "fmt"

type Cuenta struct {
	Titular string
	Saldo   float64
}

func (c *Cuenta) Ingresar(cantidad float64) {
	c.Saldo += cantidad
}

func main() {
	c := Cuenta{Titular: "Ana"}
	c.Ingresar(50)
	fmt.Printf("%s: %.2f\n", c.Titular, c.Saldo) // Ana: 50.00
}

Ejercicio

¿Por qué el receptor de Ingresar es *Cuenta y no Cuenta?

Ver la solución

Respuesta: Para modificar la cuenta original y no una copia. Con un receptor de valor, el método trabajaría sobre una copia y el cambio se perdería.

Lección 7 · Intermedio

Interfaces

Una interfaz es un conjunto de métodos. Cualquier tipo que tenga esos métodos la cumple automáticamente, sin escribir implements.

Así una función puede aceptar «cualquier cosa que sepa calcular su área», sea un círculo o un rectángulo.

Ejemplo práctico
package main

import (
	"fmt"
	"math"
)

type Figura interface {
	Area() float64
}

type Circulo struct{ Radio float64 }
type Rectangulo struct{ Ancho, Alto float64 }

func (c Circulo) Area() float64    { return math.Pi * c.Radio * c.Radio }
func (r Rectangulo) Area() float64 { return r.Ancho * r.Alto }

func main() {
	figuras := []Figura{Circulo{1}, Rectangulo{2, 3}}
	for _, f := range figuras {
		fmt.Printf("%.2f\n", f.Area())
	}
}

Ejercicio

¿Cómo indica un tipo de Go que cumple una interfaz?

Ver la solución

Respuesta: Basta con que tenga los métodos. Las interfaces de Go se cumplen de forma implícita: si tiene los métodos, la cumple.

Encuentra los errores

Cuadrado debería cumplir la interfaz Forma y el programa debería mostrar 9. Tiene 2 errores.

Ver la solución
  • Línea 7: Para cumplir la interfaz, el método se tiene que llamar exactamente Area. En Go, además, la mayúscula inicial lo hace visible desde otros paquetes. Corrección: func (c Cuadrado) Area() float64 {
  • Línea 13: Sin paréntesis no se llama al método: se muestra la propia función (una dirección de memoria), no el área. Corrección: fmt.Println(f.Area())

Nivel 3 Avanzado

Lección 8 · Avanzado

Goroutines y canales

Una goroutine es una función que se ejecuta a la vez que el resto del programa; se lanza poniendo go delante de la llamada. Son muy ligeras: se pueden lanzar miles.

Los canales (chan) comunican goroutines de forma segura: una envía con <- y otra recibe. sync.WaitGroup espera a que terminen todas.

Ejemplo práctico
package main

import (
	"fmt"
	"sync"
)

func cuadrado(n int, resultados chan<- int, wg *sync.WaitGroup) {
	defer wg.Done()
	resultados <- n * n
}

func main() {
	resultados := make(chan int, 3)
	var wg sync.WaitGroup
	for _, n := range []int{2, 3, 4} {
		wg.Add(1)
		go cuadrado(n, resultados, &wg)
	}
	wg.Wait()
	close(resultados)

	suma := 0
	for r := range resultados {
		suma += r
	}
	fmt.Println(suma) // 29
}

Ejercicio

Completa para lanzar la función en una goroutine.

 descargar(url)
Ver la solución

Respuesta: go. Escribir go delante de una llamada la ejecuta en una goroutine nueva.

Lección 9 · Avanzado

Módulos y dependencias

Cada proyecto es un módulo, descrito en el archivo go.mod. go mod init lo crea y go get añade una dependencia.

go mod tidy limpia el archivo: añade lo que falta y quita lo que ya no usas. Las dependencias quedan fijadas con su versión exacta.

Ejemplo práctico
# En la terminal
go mod init ejemplo.com/tienda
go get github.com/google/uuid@latest
go mod tidy

// main.go
package main

import (
	"fmt"

	"github.com/google/uuid"
)

func main() {
	fmt.Println("Pedido", uuid.NewString())
}

Ejercicio

¿Qué archivo describe un módulo de Go y sus dependencias?

Ver la solución

Respuesta: go.mod. go.mod guarda el nombre del módulo, la versión de Go y las dependencias.

Lección 10 · Avanzado

Un servidor web en pocas líneas

La biblioteca estándar incluye net/http, suficiente para crear un servidor web o una API sin instalar nada más.

Cada ruta se asocia a una función que recibe la petición y escribe la respuesta. Desde Go 1.22 los patrones admiten el método y variables, como GET /saludo/{nombre}.

Ejemplo práctico
package main

import (
	"encoding/json"
	"log"
	"net/http"
)

func main() {
	http.HandleFunc("GET /saludo/{nombre}", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]string{"saludo": "Hola, " + r.PathValue("nombre")})
	})
	log.Println("Escuchando en http://localhost:8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

Ejercicio

¿Qué paquete de la biblioteca estándar sirve para crear un servidor web?

Ver la solución

Respuesta: net/http. net/http contiene el servidor y el cliente HTTP de Go.

Lección 11 · Avanzado

Genéricos

Desde Go 1.18, una función puede tener parámetros de tipo entre corchetes: func Maximo[T cmp.Ordered](a, b T) T sirve para enteros, decimales o textos sin escribirla tres veces. La restricción (cmp.Ordered, comparable, any) dice qué operaciones admite T.

Los paquetes slices y maps de la biblioteca estándar ya usan genéricos: slices.Contains, slices.Sort o slices.Index funcionan con cualquier tipo de slice.

Ejemplo práctico
package main

import (
	"cmp"
	"fmt"
	"slices"
)

func Maximo[T cmp.Ordered](a, b T) T {
	if a > b {
		return a
	}
	return b
}

func Filtrar[T any](lista []T, cumple func(T) bool) []T {
	var resultado []T
	for _, v := range lista {
		if cumple(v) {
			resultado = append(resultado, v)
		}
	}
	return resultado
}

func main() {
	fmt.Println(Maximo(3, 7), Maximo("pera", "manzana"))  // 7 pera

	pares := Filtrar([]int{1, 2, 3, 4}, func(n int) bool { return n%2 == 0 })
	fmt.Println(pares)                                     // [2 4]

	nombres := []string{"Luis", "Ana", "Marta"}
	slices.Sort(nombres)
	fmt.Println(nombres, slices.Contains(nombres, "Ana"))  // [Ana Luis Marta] true
}

Escribe el código

Escribe la función genérica func Suma[T int | float64](numeros []T) T: devuelve la suma de todos los elementos del slice.

Tab escribe una sangría; para salir del editor con el teclado, pulsa Esc y luego Tab. Ctrl + Intro corrige.

Ver la solución
func Suma[T int | float64](numeros []T) T {
	var total T
	for _, n := range numeros {
		total += n
	}
	return total
}

var total T crea una variable con el valor cero de T (0 o 0.0). La restricción int | float64 permite usar += porque ambos tipos lo admiten.

Lección 12 · Avanzado

Errores a fondo: envolver, Is y As

Al devolver un error hacia arriba, añádele contexto envolviéndolo con fmt.Errorf("leer config: %w", err). El mensaje queda como una cadena de causas («leer config: open config.json: no such file…») y el error original no se pierde.

errors.Is(err, objetivo) comprueba si en esa cadena está un error concreto, como os.ErrNotExist, y errors.As busca un error de un tipo determinado para leer sus datos. Comparar con == fallaría con errores envueltos.

Ejemplo práctico
package main

import (
	"errors"
	"fmt"
	"os"
)

func leerConfig(ruta string) ([]byte, error) {
	datos, err := os.ReadFile(ruta)
	if err != nil {
		return nil, fmt.Errorf("leer configuración: %w", err)
	}
	return datos, nil
}

func main() {
	_, err := leerConfig("no-existe.json")
	if errors.Is(err, os.ErrNotExist) {
		fmt.Println("Falta el archivo; uso la configuración por defecto")
	}
	fmt.Println(err)
	// leer configuración: open no-existe.json: no such file or directory
}

Ejercicio

Completa el verbo de formato que envuelve el error original para poder comprobarlo después con errors.Is.

return fmt.Errorf("guardar pedido: ", err)
Ver la solución

Respuesta: %w. %w envuelve el error (lo guarda dentro); %v solo copiaría su texto y errors.Is ya no lo encontraría.

Lección 13 · Avanzado

Pruebas con go test

Las pruebas de Go van en archivos que terminan en _test.go, en funciones TestNombre(t *testing.T). go test ./... las encuentra y ejecuta todas; t.Errorf marca un fallo con un mensaje y la prueba continúa.

Lo habitual son las pruebas por tabla: un slice con casos (entrada y resultado esperado) que se recorre en un bucle. Añadir un caso nuevo es añadir una línea.

Ejemplo práctico
// archivo precio.go
package tienda

func ConIva(precio float64) float64 {
	return precio * 1.21
}

// archivo precio_test.go
package tienda

import "testing"

func TestConIva(t *testing.T) {
	casos := []struct {
		entrada, esperado float64
	}{
		{100, 121},
		{0, 0},
		{50, 60.5},
	}
	for _, c := range casos {
		if obtenido := ConIva(c.entrada); obtenido != c.esperado {
			t.Errorf("ConIva(%v) = %v; se esperaba %v", c.entrada, obtenido, c.esperado)
		}
	}
}
// go test ./...  →  ok  tienda  0.002s

Encuentra los errores

Esta prueba nunca se ejecutaría y, si lo hiciera, daría un falso fallo. Tiene 2 errores.

Ver la solución
  • Línea 5: go test solo ejecuta las funciones cuyo nombre empieza por Test (y que reciben *testing.T). Corrección: func TestDoble(t *testing.T) {
  • Línea 9: El doble de 0 es 0: esta comprobación fallaría aunque Doble esté bien. Corrección: if Doble(0) != 0 {

Lección 14 · Avanzado

JSON con encoding/json

json.Marshal convierte un struct en JSON y json.Unmarshal lo rellena a partir de un JSON. Solo se incluyen los campos exportados, los que empiezan por mayúscula; los demás se ignoran en silencio.

Las etiquetas de los campos (json:"nombre") indican el nombre de la clave en el JSON y opciones como omitempty, que omite el campo si está vacío.

Ejemplo práctico
package main

import (
	"encoding/json"
	"fmt"
)

type Pedido struct {
	ID       int      `json:"id"`
	Cliente  string   `json:"cliente"`
	Total    float64  `json:"total"`
	Notas    string   `json:"notas,omitempty"`
	interno  string   // minúscula: no se exporta ni se guarda en el JSON
}

func main() {
	p := Pedido{ID: 7, Cliente: "Ana", Total: 42.5, interno: "x"}
	datos, _ := json.Marshal(p)
	fmt.Println(string(datos))  // {"id":7,"cliente":"Ana","total":42.5}

	var leido Pedido
	err := json.Unmarshal([]byte(`{"id":8,"cliente":"Luis","total":10}`), &leido)
	fmt.Println(leido.Cliente, err)  // Luis <nil>
}

Ejercicio

Un campo del struct se llama precio (en minúscula) y no aparece en el JSON. ¿Por qué?

Ver la solución

Respuesta: Porque solo se convierten los campos exportados, que empiezan por mayúscula. encoding/json solo ve los campos exportados. Hay que llamarlo Precio y, si quieres la clave en minúscula, añadir la etiqueta json:"precio".

Lección 15 · Avanzado

context: cancelar y limitar el tiempo

Un programa que hace peticiones a otros servicios no debería esperar para siempre. context.WithTimeout crea un contexto que se cancela solo pasado un tiempo, y las funciones que lo reciben (como las peticiones HTTP) se detienen al cancelarse.

El contexto se pasa como primer parámetro (ctx context.Context) y siempre se llama a la función cancel que devuelve, normalmente con defer, para liberar recursos.

Ejemplo práctico
package main

import (
	"context"
	"fmt"
	"net/http"
	"time"
)

func consultar(ctx context.Context, url string) (int, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return 0, err
	}
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return 0, err  // incluye «context deadline exceeded» si se pasa de tiempo
	}
	defer resp.Body.Close()
	return resp.StatusCode, nil
}

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()

	codigo, err := consultar(ctx, "https://go.dev")
	fmt.Println(codigo, err)  // 200 <nil> (o un error si tarda más de 2 segundos)
}

Ejercicio

¿Por qué se escribe defer cancel() después de context.WithTimeout?

Ver la solución

Respuesta: Para liberar los recursos del contexto al salir de la función, aunque termine antes del tiempo límite. Si no se llama a cancel, el temporizador y lo asociado al contexto siguen vivos hasta que vence el plazo. go vet avisa si te olvidas.

¿Has terminado? Sigue practicando

Repasa los conceptos en el diccionario, practica con estas herramientas o prueba otro lenguaje: las ideas que has aprendido (variables, condiciones, funciones, objetos…) te servirán en cualquiera.

  • Calculadoras informáticas

    Calculadora de subredes IPv4

    Red, difusión, rango de hosts, máscara y wildcard de cualquier IP/CIDR, con división en subredes y VLSM.

  • Texto y programación

    Base64, URL y JSON

    Codifica y decodifica Base64 y URL, y valida o formatea JSON con la línea del error.

  • Texto y programación

    Conversor de fecha Unix y zonas horarias

    Pasa un timestamp Unix a fecha y al revés, y compara la hora en varias zonas horarias.

Fuentes consultadas

* Porcentaje de personas que dijeron haber usado Go en la Stack Overflow Developer Survey 2026: Technology (pregunta sobre lenguajes de programación, de scripts y de marcado; todas las respuestas). El progreso de las lecciones se guarda solo en este navegador.