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.
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 versionCrea 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
./holaMó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.23El 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 necesariogo.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)
}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)
}
}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"))
}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.
/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.