Skip to main content

Command Palette

Search for a command to run...

Paquetes Importantes de Go: context, io, fmt, sync y reflect

Updated
10 min read

En los posts anteriores exploramos los fundamentos de Go: structs, interfaces, métodos, collections y manejo de errores. Hoy vamos a profundizar en los paquetes más importantes de la biblioteca estándar de Go. Estos paquetes son fundamentales para escribir código Go profesional y entenderlos te ayudará a aprovechar al máximo el lenguaje.

Go viene con una biblioteca estándar muy completa y bien diseñada. A diferencia de Java, donde necesitas muchas librerías externas, Go te proporciona herramientas poderosas desde el inicio.

context: Cancelación y Timeouts

El paquete context es uno de los más importantes en Go. Proporciona un mecanismo para cancelación, timeouts y valores que se propagan a través de llamadas de función y goroutines.

¿Por qué es Importante?

En aplicaciones concurrentes y distribuidas, necesitas:

  • Cancelar operaciones que ya no son necesarias

  • Establecer timeouts para evitar que operaciones bloqueen indefinidamente

  • Pasar valores (como request IDs) a través de múltiples capas

Context con Timeout

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

// Simular operación que puede tardar
done := make(chan bool)
go func() {
    time.Sleep(3 * time.Second) // Tarda más que el timeout
    done <- true
}()

select {
case <-done:
    fmt.Println("Operation completed")
case <-ctx.Done():
    fmt.Printf("Operation cancelled: %v\n", ctx.Err())
    // Output: Operation cancelled: context deadline exceeded
}

Context con Cancelación Manual

ctx, cancel := context.WithCancel(context.Background())

go func() {
    time.Sleep(1 * time.Second)
    cancel() // Cancelar después de 1 segundo
}()

<-ctx.Done()
fmt.Printf("Context cancelled: %v\n", ctx.Err())

Context con Valores

// Agregar valores al context
ctx := context.WithValue(context.Background(), "userID", 123)
ctx = context.WithValue(ctx, "requestID", "req-456")

// Recuperar valores
userID := ctx.Value("userID").(int)
requestID := ctx.Value("requestID").(string)

⚠️ Advertencia: Usa valores en context solo para datos de request-scope (como request IDs, user IDs). No uses context como un contenedor de parámetros general.

Uso en HTTP y Database

// En HTTP handlers
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context() // Obtener context del request

    // Pasar context a operaciones de base de datos
    user, err := h.repo.GetUser(ctx, userID)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    // ...
}

// En operaciones de base de datos
func (r *Repository) GetUser(ctx context.Context, id int) (*User, error) {
    // El context permite cancelación y timeout
    query := "SELECT * FROM users WHERE id = ?"
    row := r.db.QueryRowContext(ctx, query, id)
    // ...
}

io: Operaciones de Entrada/Salida

El paquete io proporciona interfaces y funciones para trabajar con I/O de forma genérica. Es similar a java.io pero más simple y poderoso gracias a las interfaces de Go.

Interfaces Principales

// Reader - para leer datos
type Reader interface {
    Read(p []byte) (n int, err error)
}

// Writer - para escribir datos
type Writer interface {
    Write(p []byte) (n int, err error)
}

// Closer - para cerrar recursos
type Closer interface {
    Close() error
}

Leer de un Archivo

file, err := os.Open("data.txt")
if err != nil {
    return err
}
defer file.Close()

// Leer todo el contenido
data, err := io.ReadAll(file)
if err != nil {
    return err
}
fmt.Printf("Read %d bytes\n", len(data))

Escribir a un Writer

// Escribir string directamente
io.WriteString(os.Stdout, "Hello from io.WriteString!\n")

// Escribir a cualquier Writer
var buf bytes.Buffer
io.WriteString(&buf, "Hello")

Copiar entre Reader y Writer

// Copiar de un Reader a un Writer
file, _ := os.Open("source.txt")
defer file.Close()

written, err := io.Copy(os.Stdout, file)
if err != nil {
    return err
}
fmt.Printf("Copied %d bytes\n", written)

Ventaja de las Interfaces

La belleza de io.Reader y io.Writer es que funcionan con cualquier tipo que implemente estas interfaces:

  • Archivos (os.File)

  • Sockets de red (net.Conn)

  • Buffers en memoria (bytes.Buffer)

  • Strings (strings.Reader)

  • Cualquier tipo personalizado que implemente las interfaces

// Esta función funciona con CUALQUIER Reader
func processData(r io.Reader) error {
    data, err := io.ReadAll(r)
    // Procesar data...
    return err
}

// Puedes pasar diferentes tipos:
processData(file)           // Archivo
processData(conn)           // Conexión de red
processData(&buf)           // Buffer
processData(strings.NewReader("data")) // String

fmt: Formateo e Impresión

El paquete fmt es similar a System.out.println y String.format en Java, pero más poderoso y flexible.

Printf: Formateo con Especificadores

name := "John"
age := 30
balance := 1234.56

fmt.Printf("Name: %s, Age: %d, Balance: $%.2f\n", name, age, balance)
// Output: Name: John, Age: 30, Balance: $1234.56

Especificadores comunes:

  • %s - string

  • %d - entero decimal

  • %f - float (.2f para 2 decimales)

  • %v - valor en formato por defecto

  • %+v - struct con nombres de campos

  • %#v - representación Go-syntax

  • %T - tipo del valor

Sprintf: Formateo a String

message := fmt.Sprintf("User %s is %d years old", name, age)
// message = "User John is 30 years old"

Fprintf: Escribir a un Writer

// Escribir a stderr
fmt.Fprintf(os.Stderr, "Error: %v\n", err)

// Escribir a un archivo
file, _ := os.Create("output.txt")
fmt.Fprintf(file, "Data: %s\n", data)

Scanf: Leer Entrada Formateada

var name string
var age int
fmt.Scanf("%s %d", &name, &age)

Nota: fmt.Scanf es útil para programas simples, pero para entrada interactiva compleja, considera usar bufio.Scanner.

Implementar Stringer

Si tu tipo implementa la interfaz Stringer, fmt la usará automáticamente:

type Person struct {
    Name string
    Age  int
}

func (p Person) String() string {
    return fmt.Sprintf("Person{Name: %s, Age: %d}", p.Name, p.Age)
}

p := Person{Name: "John", Age: 30}
fmt.Println(p) // Output: Person{Name: John, Age: 30}

sync: Sincronización y Concurrencia

El paquete sync proporciona primitivas de sincronización para código concurrente. Es similar a java.util.concurrent pero más simple y directo.

Mutex: Exclusión Mutua

var mu sync.Mutex
counter := 0

increment := func() {
    mu.Lock()
    defer mu.Unlock() // Siempre usar defer para unlock
    counter++
}

// Múltiples goroutines pueden llamar increment() de forma segura
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        increment()
    }()
}
wg.Wait()
fmt.Printf("Counter: %d\n", counter) // 10

Regla de oro: Siempre usa defer mu.Unlock() para evitar deadlocks.

RWMutex: Lectura/Escritura

RWMutex permite múltiples lectores simultáneos o un escritor exclusivo:

var rwmu sync.RWMutex
data := make(map[string]int)

// Escritura (exclusiva)
func write(key string, value int) {
    rwmu.Lock()
    defer rwmu.Unlock()
    data[key] = value
}

// Lectura (múltiples lectores simultáneos permitidos)
func read(key string) int {
    rwmu.RLock()
    defer rwmu.RUnlock()
    return data[key]
}

Cuándo usar RWMutex: Cuando tienes muchos lectores y pocos escritores. Si tienes muchos escritores, Mutex regular puede ser más eficiente.

WaitGroup: Esperar Múltiples Goroutines

var wg sync.WaitGroup

for i := 0; i < 5; i++ {
    wg.Add(1) // Incrementar contador
    go func(id int) {
        defer wg.Done() // Decrementar cuando termine
        time.Sleep(100 * time.Millisecond)
        fmt.Printf("Goroutine %d completed\n", id)
    }(i)
}

wg.Wait() // Esperar a que todas terminen
fmt.Println("All goroutines completed")

Patrón común: Add() antes de lanzar la goroutine, Done() al finalizar (usando defer).

Once: Ejecutar Solo Una Vez

var once sync.Once
initialize := func() {
    fmt.Println("Initializing (should only see this once)")
}

// Llamar múltiples veces
once.Do(initialize)
once.Do(initialize)
once.Do(initialize)
// Output: Initializing (should only see this once)

Útil para inicialización lazy thread-safe.

Otras Primitivas de sync

  • sync.Pool: Pool de objetos reutilizables para reducir allocations

  • sync.Cond: Condition variables para señalización entre goroutines

  • sync.Map: Map thread-safe (usa solo cuando realmente lo necesites)

reflect: Reflexión en Tiempo de Ejecución

El paquete reflect permite inspeccionar tipos y valores en tiempo de ejecución. Es similar a java.lang.reflect pero más limitado y con mejor performance.

⚠️ Advertencia

Usa reflect con moderación. Es poderoso pero:

  • Más lento que código directo

  • Menos type-safe

  • Más difícil de entender y mantener

Úsalo solo cuando realmente lo necesites (serialización, frameworks, etc.).

Inspeccionar Tipos

var x int = 42
t := reflect.TypeOf(x)
fmt.Printf("Type: %s\n", t) // int

v := reflect.ValueOf(x)
fmt.Printf("Value: %v\n", v.Int()) // 42

Inspeccionar Structs

type Person struct {
    Name string
    Age  int
}

p := Person{Name: "John", Age: 30}
pType := reflect.TypeOf(p)
pValue := reflect.ValueOf(p)

fmt.Printf("Struct type: %s\n", pType)
fmt.Printf("Number of fields: %d\n", pType.NumField())

for i := 0; i < pType.NumField(); i++ {
    field := pType.Field(i)
    value := pValue.Field(i)
    fmt.Printf("  Field %d: %s (%s) = %v\n", 
        i, field.Name, field.Type, value.Interface())
}

Modificar Valores (Requiere Puntero)

p := &Person{Name: "Jane", Age: 25}
pValue := reflect.ValueOf(p).Elem() // Elem() obtiene el valor apuntado
ageField := pValue.FieldByName("Age")

if ageField.CanSet() {
    ageField.SetInt(26)
    fmt.Printf("Modified age: %d\n", p.Age) // 26
}

Casos de Uso Comunes

  • Serialización/Deserialización: JSON, XML, etc.

  • Frameworks: ORMs, validators

  • Testing: Comparación de valores complejos

  • Code generation: Herramientas que generan código

Ejemplo Práctico: Combinando Múltiples Paquetes

Vamos a construir un DataProcessor que combina varios paquetes:

type DataProcessor struct {
    mu     sync.RWMutex           // sync: sincronización
    data   map[string]interface{} // Datos
    ctx    context.Context        // context: cancelación
    cancel context.CancelFunc
}

func NewDataProcessor() *DataProcessor {
    ctx, cancel := context.WithCancel(context.Background())
    return &DataProcessor{
        data:   make(map[string]interface{}),
        ctx:    ctx,
        cancel: cancel,
    }
}

func (dp *DataProcessor) Set(key string, value interface{}) {
    dp.mu.Lock()
    defer dp.mu.Unlock()
    dp.data[key] = value
}

func (dp *DataProcessor) Get(key string) (interface{}, bool) {
    dp.mu.RLock()
    defer dp.mu.RUnlock()
    value, exists := dp.data[key]
    return value, exists
}

func (dp *DataProcessor) ProcessWithTimeout(timeout time.Duration) error {
    ctx, cancel := context.WithTimeout(dp.ctx, timeout)
    defer cancel()

    done := make(chan error)
    go func() {
        time.Sleep(2 * time.Second)
        done <- nil
    }()

    select {
    case err := <-done:
        return err
    case <-ctx.Done():
        return ctx.Err()
    }
}

func (dp *DataProcessor) Stop() {
    dp.cancel()
}

Este ejemplo muestra:

  • sync.RWMutex: Para acceso thread-safe

  • context: Para cancelación y timeouts

  • Channels: Para comunicación entre goroutines

Comparación: Java vs Go

Paquete GoEquivalente JavaDiferencia Clave
contextCompletableFuture, ExecutorServiceMás integrado, más simple
iojava.ioInterfaces más pequeñas y composables
fmtSystem.out, String.formatMás flexible, mejor integrado
syncjava.util.concurrentMás simple, menos overhead
reflectjava.lang.reflectMás limitado pero más rápido

Mejores Prácticas

1. Siempre Pasa Context en Operaciones Largas

// ✅ Bueno
func (r *Repository) GetUser(ctx context.Context, id int) (*User, error) {
    return r.db.QueryRowContext(ctx, "SELECT ...", id)
}

// ❌ Evitar (sin cancelación posible)
func (r *Repository) GetUser(id int) (*User, error) {
    return r.db.QueryRow("SELECT ...", id)
}

2. Usa Interfaces de io en Lugar de Tipos Concretos

// ✅ Bueno - acepta cualquier Reader
func processData(r io.Reader) error {
    data, err := io.ReadAll(r)
    // ...
}

// ❌ Menos flexible
func processData(file *os.File) error {
    // Solo acepta archivos
}

3. Siempre Usa defer con Mutex

// ✅ Bueno
mu.Lock()
defer mu.Unlock()
// código...

// ❌ Peligroso - fácil olvidar unlock
mu.Lock()
// código...
mu.Unlock()

4. Evita reflect a Menos que Sea Necesario

// ✅ Preferir código directo
func getAge(p Person) int {
    return p.Age
}

// ❌ Evitar reflect si no es necesario
func getAge(p Person) int {
    v := reflect.ValueOf(p)
    return int(v.FieldByName("Age").Int())
}

5. Usa fmt.Sprintf para Construir Mensajes de Error

// ✅ Bueno
return fmt.Errorf("failed to process user %d: %w", userID, err)

// ❌ Menos informativo
return errors.New("failed to process user")

Errores Comunes

❌ Error 1: Olvidar Cancelar Context

// ❌ MAL - leak de recursos
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
// Olvidar cancel()

// ✅ BIEN - siempre cancelar
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

❌ Error 2: No Usar defer con Mutex

// ❌ MAL - puede causar deadlock
mu.Lock()
if condition {
    return // Olvidó unlock!
}
mu.Unlock()

// ✅ BIEN
mu.Lock()
defer mu.Unlock()
if condition {
    return // Unlock automático
}

❌ Error 3: Modificar reflect.Value sin CanSet()

// ❌ MAL - panic
p := Person{Name: "John"}
v := reflect.ValueOf(p)
v.FieldByName("Age").SetInt(30) // Panic!

// ✅ BIEN - usar puntero
p := &Person{Name: "John"}
v := reflect.ValueOf(p).Elem()
if v.FieldByName("Age").CanSet() {
    v.FieldByName("Age").SetInt(30)
}

Conclusiones

Los paquetes estándar de Go son poderosos y bien diseñados:

context: Esencial para cancelación y timeouts en código concurrente

io: Interfaces simples y composables para I/O genérico

fmt: Formateo flexible e integrado con el lenguaje

sync: Primitivas de sincronización simples y eficientes

reflect: Poderoso pero úsalo con moderación

Conceptos clave a recordar:

  • Siempre pasa context en operaciones que pueden cancelarse

  • Usa interfaces de io para código más flexible

  • defer con mutex es tu amigo

  • reflect es útil pero costoso - úsalo solo cuando sea necesario

  • La biblioteca estándar de Go es muy completa - aprende a usarla bien

Dominar estos paquetes te permitirá escribir código Go profesional, eficiente y mantenible. Son la base sobre la cual se construyen aplicaciones Go reales.

Próximos Pasos

En el siguiente post comenzaremos a explorar la concurrencia en Go: goroutines, channels, select, y más. Este es uno de los aspectos más poderosos y distintivos de Go, y entenderlo bien es crucial para alcanzar nivel Senior.


¿Tienes preguntas sobre estos paquetes estándar de Go? Déjame saber en los comentarios. Y si quieres ver el código completo de estos ejemplos, puedes encontrarlo en mi repositorio go-mastery-lab.

Enjoy!

José Díaz

+51 939965148

More from this blog

JoeDayz

53 posts

Community Guy | Java Champion | AWS Architect | Software Architect

Paquetes Importantes de Go: context, io, fmt, sync y reflect