Saltar al contenido principal
70 min de lectura1 de agosto de 2026
GoBackendGoroutinesHTTPLenguaje

Lo que aprenderás

  • Escribir programas Go con variables y tipos
  • Crear funciones y manejar errores como valores
  • Modelar datos con structs e interfaces
  • Usar goroutines y channels para concurrencia
  • Construir una API REST con net/http
Tutorial70 min

Go: cero a servidor web

Go es el lenguaje creado por Google para construir sistemas rápidos y fáciles de mantener. En este tutorial pasarás de cero absoluto a escribir tu primer servidor web con API REST, pasando por variables, funciones, structs y goroutines. Solo necesitas conceptos previos de programación.


Qué es Go

Go (también llamado Golang) nació en 2009 dentro de Google, creado por Robert Griesemer, Rob Pike y Ken Thompson para resolver un problema concreto: los programas en C++ tardaban mucho en compilar y eran difíciles de leer. Go se diseñó para que escribir software de servidor fuera simple, rápido y seguro. Es compilado: go build genera un binario nativo sin máquina virtual, ideal para microservicios, CLIs y sistemas de red.

En Go no existen while ni do-while: solo for. Pocas palabras reservadas y una sola forma de hacer cada cosa. Otra clave son las goroutines: funciones concurrentes ligerísimas que se lanzan con la palabra go y que el runtime reparte entre pocos hilos reales. Por eso Docker o Kubernetes, escritos en Go, manejan enorme tráfico con pocos recursos.

Instalar y primer programa

En macOS se instala con Homebrew; en Linux con el gestor de paquetes (por ejemplo apt install golang) o desde go.dev; en Windows hay instalador MSI oficial.

# Instalar Go en macOS
brew install go

# Verificar la versión instalada
go version

Crea una carpeta y un archivo hola.go. Todo archivo ejecutable empieza con package main y una función main:

package main

import "fmt"

func main() {
    fmt.Println("Hola, Go!")
}

go run compila y ejecuta en un solo paso. Para distribuir, go build genera un binario listo para copiar a cualquier servidor:

go run hola.go
# Salida: Hola, Go!

go build hola.go
./hola
El binario de Go es estáticamente enlazado: sin dependencias del sistema, copias un archivo y funciona. Las imágenes Docker resultantes son minúsculas.

Módulos y go.mod

Desde la versión 1.11, Go gestiona dependencias con módulos: un conjunto de paquetes con un archivo go.mod que declara el nombre del proyecto y sus dependencias, como package.json en Node:

# Inicializar un módulo en la carpeta actual
go mod init ejemplo/hola

// go.mod generado automáticamente
module ejemplo/hola

go 1.23

El nombre del módulo suele ser la ruta del repositorio (por ejemplo github.com/tu-usuario/mi-api), para que los paquetes se importen entre sí usando esa ruta. Para añadir una dependencia usa go get, y antes de cada commit ejecuta go mod tidy, que elimina dependencias sin usar y descarga las que faltan:

go get github.com/gorilla/mux   # añade la dependencia
go mod tidy                    # limpia y descarga lo necesario

go.mod registra las versiones exactas y go.sum los hashes de integridad para builds reproducibles. Con módulos, go get actualiza go.mod automáticamente; ya no se instala nada dentro del GOPATH clásico.

Variables y tipos

Go es fuertemente tipado pero con inferencia. Se declara con var o con el operador corto :=, que declara e infiere el tipo en una línea. Dentro de funciones usa :=; usa var a nivel de paquete o para declarar sin inicializar:

package main

import "fmt"

const Pi = 3.1416 // constante: no se puede reasignar

func main() {
    var edad int = 30    // declaración explícita
    var nombre string    // valor cero: ""
    nombre = "Ana"

    altura := 1.75       // declaración corta: infiere float64

    fmt.Println(nombre, edad, altura, Pi)
}

Tipos básicos

Los tipos básicos incluyen bool, int (entero dependiente de la plataforma), float64, byte (alias de uint8), rune (carácter Unicode), slices []string y mapas map[string]int.

Go no tiene herencia ni clases: tiene structs con métodos e interfaces implícitas. Tampoco tiene null: usa valores cero por defecto (0, vacío, false) y nil para punteros, interfaces y funciones.

Control de flujo

El if no necesita paréntesis, pero las llaves son obligatorias, y permite declarar una variable en la propia condición. Solo existe for (clásico, estilo while e infinito), y for range itera devolviendo índice y valor. El switch no necesita break y funciona sin expresión:

if nota := 7; nota >= 5 {
    fmt.Println("Aprobado")
} else if nota >= 3 {
    fmt.Println("Suspenso")
} else {
    fmt.Println("Muy bajo")
}

for i := 1; i <= 5; i++ {
    fmt.Println(i)
}

n := 10
for n > 0 {
    n--
}

frutas := []string{"manzana", "pera", "uva"}
for _, fruta := range frutas {
    fmt.Println(fruta)
}
func diaEnPalabras(dia int) string {
    switch dia {
    case 1:
        return "Lunes"
    case 2, 3, 4, 5:
        return "Entre semana"
    case 6, 7:
        return "Fin de semana"
    default:
        return "Desconocido"
    }
}

Funciones y errores

Las funciones se declaran con func y pueden devolver varios valores. La convención es devolver el resultado junto con un error:

package main

import (
    "errors"
    "fmt"
)

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

func main() {
    resultado, err := dividir(10, 2)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println("Resultado:", resultado)
}
Los errores en Go son valores, no excepciones: no hay try/catch. Cada función que puede fallar devuelve un error y el llamador decide qué hacer. Parece verboso, pero garantiza que ningún error se ignore en silencio.

Structs e interfaces

Un struct agrupa campos relacionados. Los métodos se definen fuera, con un receptor que indica sobre qué tipo actúan; por valor no modifican, por puntero sí:

type Usuario struct {
    Nombre string
    Edad   int
    Email  string
}

// Receptor por valor: no modifica el struct
func (u Usuario) Saludar() string {
    return "Hola, soy " + u.Nombre
}

// Receptor por puntero: permite modificar
func (u *Usuario) CumplirAnios() {
    u.Edad++
}

func main() {
    ana := Usuario{Nombre: "Ana", Edad: 21, Email: "ana@email.com"}
    fmt.Println(ana.Saludar())
    ana.CumplirAnios()
    fmt.Println(ana.Edad) // 22
}

Las interfaces son implícitas: un tipo la implementa automáticamente si tiene sus métodos, sin declararlo. Esto desacopla el código: quien consume la interfaz no sabe nada del tipo concreto.

type Describible interface {
    Descripcion() string
}

type Libro struct {
    Titulo  string
    Paginas int
}

// Libro implementa Describible sin declararlo explícitamente
func (l Libro) Descripcion() string {
    return l.Titulo + " (" + fmt.Sprint(l.Paginas) + " páginas)"
}

Goroutines y channels

Una goroutine se lanza anteponiendo go. Para comunicarse se usa un channel (chan) con el operador <-. Los channels sin buffer sincronizan; los buffered tienen capacidad y evitan bloqueos, como en un pool de trabajadores:

func trabajador(id int, trabajos <-chan int, resultados chan<- int) {
    for t := range trabajos {
        fmt.Printf("Trabajador %d procesa %d
", id, t)
        resultados <- t * 2
    }
}

func main() {
    trabajos := make(chan int, 5)
    resultados := make(chan int, 5)

    for i := 1; i <= 3; i++ {
        go trabajador(i, trabajos, resultados)
    }

    for i := 1; i <= 5; i++ {
        trabajos <- i
    }
    close(trabajos)

    for i := 1; i <= 5; i++ {
        fmt.Println("Resultado:", <-resultados)
    }
}
Enviar a un channel cerrado provoca un pánico. Regla de oro: solo el emisor cierra el channel, nunca el receptor.range sobre un channel itera hasta que este se cierra.

Paquetes

Todo código vive en un paquete: un directorio con archivos que comparten la misma declaración. El programa ejecutable es package main; el resto son librerías que se importan por su ruta, derivada del nombre del módulo en go.mod. La visibilidad se controla con la mayúscula inicial: los nombres exportados empiezan en mayúscula, los privados en minúscula. No existen public ni private.

package main

import "fmt"

// Saludo es público: empieza en mayúscula
func Saludo(nombre string) string {
    return "Hola, " + nombre
}

func main() {
    fmt.Println(Saludo("ana"))
}
La biblioteca estándar cubre HTTP, JSON, criptografía, testing y concurrencia. Antes de instalar un paquete externo, revisa si existe en pkg.go.dev dentro de la stdlib.

HTTP con net/http

El paquete net/http es de los más potentes de la stdlib. Cada handler recibe un http.ResponseWriter (donde escribes la respuesta) y un http.Request (con toda la información de la petición). Construyamos una API REST completa con GET y POST, gestionando JSON y protegiendo la lista con un mutex:

package main

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

type Tarea struct {
    ID   int    `json:"id"`
    Text string `json:"text"`
    Done bool   `json:"done"`
}

var (
    tareas = []Tarea{{ID: 1, Text: "Aprender Go", Done: false}}
    nextID = 2
    mutex  sync.Mutex
)

func main() {
    http.HandleFunc("/", home)
    http.HandleFunc("/tareas", tareasHandler)

    fmt.Println("Servidor escuchando en http://localhost:8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

func home(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Bienvenido a la API de tareas en Go")
}

func tareasHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")

    switch r.Method {
    case http.MethodGet:
        json.NewEncoder(w).Encode(tareas)

    case http.MethodPost:
        var nueva Tarea
        if err := json.NewDecoder(r.Body).Decode(&nueva); err != nil {
            http.Error(w, "JSON inválido", http.StatusBadRequest)
            return
        }
        mutex.Lock()
        nueva.ID = nextID
        nextID++
        tareas = append(tareas, nueva)
        mutex.Unlock()

        w.WriteHeader(http.StatusCreated)
        json.NewEncoder(w).Encode(nueva)

    default:
        http.Error(w, "Método no permitido", http.StatusMethodNotAllowed)
    }
}

Las etiquetas de struct (los backticks con json:"id") indican a encoding/json cómo convertir cada campo a JSON; sin ellas se serializarían con el nombre en mayúscula. Prueba:curl http://localhost:8080/tareas para listar y curl -X POST -d '{"text":"Escribir API en Go"}' -H "Content-Type: application/json" http://localhost:8080/tareas para crear.

Para rutas con parámetros dinámicos (como /tareas/:id) se usan librerías como gorilla/mux o chi, compatibles con la misma interfaz de handlers.

Ejercicios


Con esto ya tienes los cimientos de Go: sintaxis limpia, errores como valores, structs e interfaces, concurrencia con goroutines y un servidor HTTP funcional. El siguiente paso es explorar la stdlib (JSON, testing, bases de datos) y librerías como chi o gin para APIs más complejas. Go brilla en servicios que deben ser rápidos, simples de operar y fáciles de mantener a largo plazo.