← Volver al listado de tecnologías

JSON y HTTP

Por: Artiko
gojsonhttpapiservidor

JSON y HTTP

Go incluye soporte completo para JSON y HTTP en su biblioteca estandar. No necesitas frameworks externos para construir APIs robustas.

encoding/json

Marshal: struct a JSON

package main

import (
    "encoding/json"
    "fmt"
)

type Usuario struct {
    ID     int    `json:"id"`
    Nombre string `json:"nombre"`
    Email  string `json:"email"`
    Edad   int    `json:"edad,omitempty"` // Omite si es valor cero
    clave  string // Campo no exportado: ignorado por json
}

func main() {
    u := Usuario{
        ID:     1,
        Nombre: "Ana",
        Email:  "[email protected]",
    }

    // Marshal compacto
    datos, err := json.Marshal(u)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println(string(datos))
    // {"id":1,"nombre":"Ana","email":"[email protected]"}

    // MarshalIndent para formato legible
    bonito, _ := json.MarshalIndent(u, "", "  ")
    fmt.Println(string(bonito))
}

Struct tags JSON

TagEfecto
`json:"nombre"`Usa “nombre” como clave JSON
`json:"nombre,omitempty"`Omite si el valor es cero
`json:"-"`Ignora el campo siempre
`json:",string"`Codifica numero/bool como string
type Config struct {
    Host     string `json:"host"`
    Puerto   int    `json:"puerto,string"`   // "puerto": "8080"
    Debug    bool   `json:"debug,omitempty"`
    Internal string `json:"-"`               // Nunca se serializa
}

Unmarshal: JSON a struct

package main

import (
    "encoding/json"
    "fmt"
)

type Producto struct {
    Nombre string  `json:"nombre"`
    Precio float64 `json:"precio"`
    Stock  int     `json:"stock"`
}

func main() {
    jsonStr := `{"nombre":"Laptop","precio":999.99,"stock":50}`

    var p Producto
    err := json.Unmarshal([]byte(jsonStr), &p)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Printf("%+v\n", p)
    // {Nombre:Laptop Precio:999.99 Stock:50}
}

JSON dinamico con map

Cuando no conoces la estructura de antemano:

var datos map[string]any
json.Unmarshal([]byte(`{"nombre":"Go","version":1.22}`), &datos)
fmt.Println(datos["nombre"])  // Go
fmt.Println(datos["version"]) // 1.22 (float64)

json.NewEncoder y json.NewDecoder

Para trabajar con streams (archivos, HTTP bodies, conexiones):

// Encoder: escribe JSON a un Writer
enc := json.NewEncoder(os.Stdout)
enc.Encode(Evento{Tipo: "info", Mensaje: "inicio"})

// Decoder: lee JSON de un Reader
dec := json.NewDecoder(strings.NewReader(jsonStr))
var e Evento
dec.Decode(&e)

json.RawMessage

Retrasa el parsing de parte del JSON.

package main

import (
    "encoding/json"
    "fmt"
)

type Respuesta struct {
    Tipo string          `json:"tipo"`
    Data json.RawMessage `json:"data"` // Se parsea despues
}

type Usuario struct {
    Nombre string `json:"nombre"`
}

type Error struct {
    Codigo  int    `json:"codigo"`
    Detalle string `json:"detalle"`
}

func main() {
    jsonStr := `{"tipo":"usuario","data":{"nombre":"Ana"}}`

    var resp Respuesta
    json.Unmarshal([]byte(jsonStr), &resp)

    switch resp.Tipo {
    case "usuario":
        var u Usuario
        json.Unmarshal(resp.Data, &u)
        fmt.Println("Usuario:", u.Nombre)
    case "error":
        var e Error
        json.Unmarshal(resp.Data, &e)
        fmt.Printf("Error %d: %s\n", e.Codigo, e.Detalle)
    }
}

net/http: Servidor HTTP

Servidor basico

package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "Hola desde Go!")
    })

    http.HandleFunc("/salud", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        fmt.Fprintln(w, `{"status":"ok"}`)
    })

    fmt.Println("Servidor en :8080")
    http.ListenAndServe(":8080", nil) // nil usa DefaultServeMux
}

http.Handler interface

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

Cualquier tipo que implemente ServeHTTP es un Handler.

package main

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

type APIHandler struct{}

func (h *APIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]any{
        "hora":   time.Now().Format(time.RFC3339),
        "status": "activo",
    })
}

func main() {
    http.Handle("/api", &APIHandler{})
    http.ListenAndServe(":8080", nil)
}

ServeMux mejorado (Go 1.22+)

Go 1.22 mejoro el router con soporte para metodos HTTP y wildcards.

package main

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

type Tarea struct {
    ID     string `json:"id"`
    Titulo string `json:"titulo"`
}

var tareas = map[string]Tarea{
    "1": {ID: "1", Titulo: "Aprender Go"},
    "2": {ID: "2", Titulo: "Construir API"},
}

func main() {
    mux := http.NewServeMux()

    // Metodo + patron
    mux.HandleFunc("GET /tareas", listarTareas)
    mux.HandleFunc("POST /tareas", crearTarea)

    // Wildcard con {nombre}
    mux.HandleFunc("GET /tareas/{id}", obtenerTarea)
    mux.HandleFunc("DELETE /tareas/{id}", eliminarTarea)

    fmt.Println("API en :8080")
    http.ListenAndServe(":8080", mux)
}

func listarTareas(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    lista := make([]Tarea, 0, len(tareas))
    for _, t := range tareas {
        lista = append(lista, t)
    }
    json.NewEncoder(w).Encode(lista)
}

func obtenerTarea(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id") // Go 1.22+
    t, ok := tareas[id]
    if !ok {
        http.Error(w, "tarea no encontrada", http.StatusNotFound)
        return
    }
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(t)
}

func crearTarea(w http.ResponseWriter, r *http.Request) {
    var t Tarea
    if err := json.NewDecoder(r.Body).Decode(&t); err != nil {
        http.Error(w, "json invalido", http.StatusBadRequest)
        return
    }
    tareas[t.ID] = t
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(t)
}

func eliminarTarea(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    delete(tareas, id)
    w.WriteHeader(http.StatusNoContent)
}

http.Request: leer datos del cliente

func handler(w http.ResponseWriter, r *http.Request) {
    // Metodo HTTP
    fmt.Println(r.Method) // GET, POST, etc

    // Query params: /buscar?q=golang&page=1
    q := r.URL.Query().Get("q")
    page := r.URL.Query().Get("page")

    // Headers
    auth := r.Header.Get("Authorization")
    contentType := r.Header.Get("Content-Type")

    // Body (para POST/PUT)
    defer r.Body.Close()
    var datos map[string]any
    json.NewDecoder(r.Body).Decode(&datos)

    // Path value (Go 1.22+)
    id := r.PathValue("id")

    _ = q; _ = page; _ = auth; _ = contentType; _ = id
}

http.ResponseWriter: enviar respuesta

func responder(w http.ResponseWriter, r *http.Request) {
    // Headers (antes de WriteHeader)
    w.Header().Set("Content-Type", "application/json")
    w.Header().Set("X-Custom", "valor")

    // Status code
    w.WriteHeader(http.StatusCreated) // 201

    // Body
    json.NewEncoder(w).Encode(map[string]string{
        "mensaje": "creado exitosamente",
    })
}
FuncionUso
http.Error(w, msg, code)Respuesta de error rapida
http.NotFound(w, r)Responde 404
http.Redirect(w, r, url, code)Redireccion
http.ServeFile(w, r, path)Sirve archivo estatico

http.Client: hacer requests

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "time"
)

func main() {
    // Cliente con timeout
    client := &http.Client{Timeout: 10 * time.Second}

    // GET
    resp, err := client.Get("https://httpbin.org/get")
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    defer resp.Body.Close()

    body, _ := io.ReadAll(resp.Body)
    fmt.Println("Status:", resp.StatusCode)
    fmt.Println(string(body))

    // POST con JSON
    payload := map[string]string{"nombre": "Go"}
    jsonBody, _ := json.Marshal(payload)

    resp2, err := client.Post(
        "https://httpbin.org/post",
        "application/json",
        bytes.NewReader(jsonBody),
    )
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    defer resp2.Body.Close()
    fmt.Println("POST Status:", resp2.StatusCode)
}

Request personalizado

Para requests con headers custom, usa http.NewRequest + client.Do:

client := &http.Client{Timeout: 5 * time.Second}
req, err := http.NewRequest("GET", "https://api.ejemplo.com/datos", nil)
if err != nil {
    fmt.Println("Error:", err)
    return
}
req.Header.Set("Authorization", "Bearer mi-token")
req.Header.Set("Accept", "application/json")

resp, err := client.Do(req)
if err != nil {
    fmt.Println("Error:", err)
    return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))

Middleware

Un middleware envuelve un handler para agregar funcionalidad transversal.

func logging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        inicio := time.Now()
        next.ServeHTTP(w, r)
        log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(inicio))
    })
}

func cors(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Access-Control-Allow-Origin", "*")
        if r.Method == "OPTIONS" {
            w.WriteHeader(http.StatusOK)
            return
        }
        next.ServeHTTP(w, r)
    })
}

// Encadenar: handler := logging(cors(mux))

Helper para respuestas JSON

func jsonResponse(w http.ResponseWriter, status int, data any) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(data)
}

func jsonError(w http.ResponseWriter, status int, mensaje string) {
    jsonResponse(w, status, map[string]string{"error": mensaje})
}

← Cap 19: I/O, OS y Archivos | Cap 21 →