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
1
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.
Respuesta:Println. fmt.Println escribe sus argumentos y un salto de línea.
2
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.
3
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.
Los errores
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)
Go es estricto a propósito: las variables o importaciones sin usar suelen ser restos de código o errores, así que no compila hasta que las quites.
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)
4
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.
Una solución posible
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.
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
5
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.
Una solución posible
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.
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.
6
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.
7
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.
Los errores
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())
En Go un tipo cumple una interfaz automáticamente si tiene sus métodos con el mismo nombre y firma; no hace falta declararlo.
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
8
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.
9
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.
10
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}.
¿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.
11
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.
Una solución posible
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.
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.
12
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.
13
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.
Los errores
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 {
Antes de confiar en una prueba, compruébala rompiendo a propósito la función: si la prueba no falla, no está comprobando nada.
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 {
14
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".
15
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.
* 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.
¿Nos dejas medir las visitas?
Con tu permiso usamos Google Analytics, que guarda cookies, para saber cuántas personas visitan la web y qué páginas les resultan útiles. No usamos cookies de publicidad y tus archivos nunca salen de tu navegador. Puedes cambiar de opinión cuando quieras en «Configurar cookies», al pie de la página. Más información.