*os.File es la implementación fundamental de los descriptores de archivos del sistema operativo en Go. No es solo un objeto que representa un archivo en el disco; es una pieza clave de la polimorfia en el lenguaje porque implementa múltiples interfaces críticas: io.Reader, io.Writer, io.Closer e io.Seeker. Gracias a que satisface estas interfaces, puedes pasar un archivo directamente a un json.NewDecoder o a un io.Copy sin que estas funciones necesiten saber si están interactuando con un disco, un socket de red o un buffer de memoria. Esta abstracción es lo que permite que el ecosistema de I/O sea tan coherente: el mismo código que procesa un archivo puede procesar la entrada estándar (os.Stdin) de forma transparente. Sin embargo, esta flexibilidad exige entender que el archivo mantiene un estado interno (el puntero de lectura/escritura) que puede llevarte a errores lógicos si no lo gestionas con cuidado.
package main
import (
"encoding/json"
"fmt"
"io"
"os"
)
type Config struct {
AppName string `json:"app_name"`
Version float64 `json:"version"`
}
func main() {
// Creamos un archivo temporal para el ejemplo.
// os.CreateTemp devuelve un *os.File que implementa io.ReadWriteSeeker.
tmpFile, err := os.CreateTemp("", "config-*.json")
if err != nil {
panic(err)
}
// Nos aseguramos de limpiar el archivo al terminar.
defer os.Remove(tmpFile.Name())
// Cerramos el descriptor de archivo para liberar recursos del SO.
defer tmpFile.Close()
// 1. Escribir datos usando la interfaz io.Writer (vía json.Encoder)
config := Config{AppName: "ServiceAlpha", Version: 2.1}
encoder := json.NewEncoder(tmpFile)
if err := encoder.Encode(config); err != nil {
panic(err)
}
// IMPORTANTE: Después de escribir, el puntero de lectura está al final del archivo.
// Usamos la interfaz io.Seeker para moverlo al inicio.
if _, err := tmpFile.Seek(0, 0); err != nil {
panic(err)
}
// 2. Leer datos usando la interfaz io.Reader (vía json.Decoder)
var loadedConfig Config
decoder := json.NewDecoder(tmpFile)
if err := decoder.Decode(&loadedConfig); err != nil {
panic(err)
}
fmt.Printf("Configuración cargada: %+v\n", loadedConfig)
// 3. Usar os.Stdout (que es un *os.File) como io.Writer
// io.Copy lee desde el archivo y escribe en la salida estándar.
fmt.Print("Copia de seguridad en stdout: ")
tmpFile.Seek(0, 0) // Resetear puntero de nuevo para la copia
if _, err := io.Copy(os.Stdout, tmpFile); err != nil {
panic(err)
}
// 4. Permisos de archivo (Unix style)
// Cambiamos permisos a solo lectura (0400).
// Ten en cuenta que en Windows esto tiene un soporte muy limitado.
if err := tmpFile.Chmod(0400); err != nil {
panic(err)
}
fmt.Println("\nPermisos actualizados correctamente.")
}
Desglose del código
En el ejemplo, tmpFile es un puntero a os.File. Cuando ejecutamos json.NewEncoder(tmpFile), el constructor de json no pide un archivo, pide un io.Writer. Como *os.File implementa Write, el compilador lo acepta sin problemas.
Un punto crítico es la llamada a tmpFile.Seek(0, 0). Esto es posible porque *os.File implementa io.Seeker. Cuando escribimos el JSON, el “cursor” interno del archivo avanza hasta el final. Si intentáramos llamar a json.NewDecoder(tmpFile).Decode(...) inmediatamente después de escribir, obtendríamos un error de “EOF” (End of File) porque el lector empezaría a buscar datos donde ya no hay nada.
Observa la línea io.Copy(os.Stdout, tmpFile). Aquí estamos pasando os.Stdout como el destino. os.Stdout es un *os.File global predefinido que representa la salida estándar del proceso. Como satisface io.Writer, io.Copy puede volcar los bytes del archivo directamente a la consola.
Finalmente, tmpFile.Chmod(0400) interactúa con los permisos del sistema de archivos. Si estás en un entorno Linux o macOS, esto restringirá drásticamente quién puede leer ese archivo. Si estás en Windows, notarás que el comportamiento es distinto; Go intenta mapear estos permisos, pero la semántica de archivos de Windows no es idéntica a la de Unix, especialmente en lo que respecta a los bits de ejecución y grupos.
El error frecuente
El error más común al trabajar con *os.File es olvidar que el archivo mantiene un estado de posición. Si intentas realizar una operación de lectura justo después de una de escritura sin resetear el puntero, tu programa fallará silenciosamente o devolverá io.EOF.
f, _ := os.Create("test.txt")
f.WriteString("Hola mundo")
// Error: El puntero está al final de "Hola mundo"
var data string
err := json.NewDecoder(f).Decode(&data)
// err será io.EOF, no porque el archivo esté vacío, sino porque el puntero no está al inicio.
Para evitarlo, siempre que necesites reusar un archivo que acabas de modificar, debes usar f.Seek(0, 0) para volver al principio.
N° 114