📁 Subir archivos en Go: implementación segura, validación y almacenamiento
Subir archivos parece una tarea sencilla hasta que la aplicación empieza a recibir documentos grandes, imágenes dañadas, nombres de archivos extraños o intentos de subir contenido que nunca debería llegar al servidor.
En aplicaciones internas suele bastar con guardar un archivo en una carpeta. En producción la historia cambia rápidamente: límites de tamaño, validación del tipo de archivo, colisiones de nombres, permisos, almacenamiento en la nube y ataques mediante archivos maliciosos pasan a formar parte del día a día.
Go facilita mucho este tipo de tareas gracias al paquete net/http, que ya incorpora soporte para formularios multipart/form-data. A partir de ahí la diferencia entre una implementación básica y una robusta depende principalmente de las validaciones que se agreguen.
Cómo funciona una subida de archivos
Cuando un navegador envía un formulario con archivos utiliza el tipo de contenido:
multipart/form-data
Cada campo del formulario viaja separado por un boundary. Go procesa automáticamente este formato mediante:
r.ParseMultipartForm()
o directamente utilizando:
r.FormFile()
El archivo recibido implementa la interfaz io.Reader, por lo que puede copiarse fácilmente a cualquier destino usando io.Copy.
Estructura del proyecto
upload-demo/
│
├── main.go
├── uploads/
└── go.mod
La carpeta uploads almacenará los archivos recibidos.
Servidor HTTP
package main
import (
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
)
const (
maxUploadSize = 10 << 20 // 10 MB
uploadPath = "./uploads"
)
func main() {
if err := os.MkdirAll(uploadPath, os.ModePerm); err != nil {
log.Fatal(err)
}
http.HandleFunc("/", formHandler)
http.HandleFunc("/upload", uploadHandler)
fmt.Println("Servidor iniciado en :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Este servidor crea automáticamente la carpeta donde se almacenarán los archivos si todavía no existe.
Formulario HTML
func formHandler(w http.ResponseWriter, r *http.Request) {
html := `
<html>
<body>
<form enctype="multipart/form-data"
action="/upload"
method="POST">
<input type="file" name="file">
<button>Subir</button>
</form>
</body>
</html>
`
w.Write([]byte(html))
}
El atributo que nunca debe olvidarse es:
enctype="multipart/form-data"
Sin él, el navegador no enviará correctamente el archivo.
Recibiendo el archivo
func uploadHandler(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxUploadSize)
err := r.ParseMultipartForm(maxUploadSize)
if err != nil {
http.Error(w, "Archivo demasiado grande", http.StatusBadRequest)
return
}
file, header, err := r.FormFile("file")
if err != nil {
http.Error(w, "No se recibió ningún archivo", http.StatusBadRequest)
return
}
defer file.Close()
dst, err := os.Create(filepath.Join(uploadPath, header.Filename))
if err != nil {
http.Error(w, err.Error(), 500)
return
}
defer dst.Close()
if _, err := io.Copy(dst, file); err != nil {
http.Error(w, err.Error(), 500)
return
}
fmt.Fprintf(w, "Archivo %s subido correctamente", header.Filename)
}
Con esto ya es posible subir archivos desde el navegador.
Aunque funciona, todavía tiene varios problemas que aparecen rápidamente en producción.
Limitar el tamaño del archivo
Uno de los errores más habituales consiste en aceptar cualquier tamaño de archivo.
Basta con que un cliente envíe varios archivos de cientos de megabytes para consumir memoria o espacio en disco.
La protección más sencilla es:
r.Body = http.MaxBytesReader(
w,
r.Body,
10<<20,
)
Ese límite se aplica antes de procesar el formulario.
No conviene confiar únicamente en validaciones del lado del navegador, ya que cualquier cliente HTTP puede ignorarlas.
Validar el tipo de archivo
Muchos sistemas verifican únicamente la extensión.
foto.jpg
Eso no significa que realmente sea una imagen JPEG.
Go permite inspeccionar los primeros bytes del archivo:
buffer := make([]byte, 512)
_, err = file.Read(buffer)
if err != nil {
return
}
contentType := http.DetectContentType(buffer)
fmt.Println(contentType)
El resultado puede ser:
image/jpeg
o
application/pdf
Después de la lectura hay que volver al inicio:
file.Seek(0, io.SeekStart)
Una validación típica sería:
allowed := map[string]bool{
"image/jpeg": true,
"image/png": true,
}
if !allowed[contentType] {
http.Error(w, "Formato no permitido", 400)
return
}
Esta comprobación evita muchos intentos de subir contenido inesperado.
Evitar sobrescribir archivos
Guardar usando el nombre original suele provocar conflictos.
Dos usuarios pueden subir:
foto.png
El segundo sobrescribirá al primero.
Una alternativa sencilla consiste en generar un nombre único.
filename := fmt.Sprintf(
"%d%s",
time.Now().UnixNano(),
filepath.Ext(header.Filename),
)
También es habitual utilizar UUID.
d81c92b6-acde-4cb8.png
De esta forma el nombre almacenado deja de depender del cliente.
Sanitizar nombres de archivos
Nunca debe confiarse en el nombre recibido.
Ejemplos como estos son frecuentes:
../../config.env
o
C:\Windows\system32
La forma más sencilla de evitar problemas es ignorar completamente el nombre original y generar uno nuevo.
Si por motivos de negocio debe conservarse, es recomendable almacenarlo únicamente en la base de datos como información descriptiva.
Subiendo varios archivos
Go también permite recibir múltiples archivos.
HTML:
<input
type="file"
name="files"
multiple
>
Servidor:
files := r.MultipartForm.File["files"]
for _, header := range files {
file, err := header.Open()
if err != nil {
continue
}
dst, _ := os.Create(filepath.Join(
uploadPath,
header.Filename,
))
io.Copy(dst, file)
dst.Close()
file.Close()
}
Cuando se manejan decenas de archivos resulta recomendable limitar también la cantidad permitida.
Guardar directamente en Amazon S3
En muchos proyectos el servidor nunca almacena archivos localmente.
El flujo habitual es:
Cliente
│
▼
Servidor Go
│
▼
Amazon S3
Una vez obtenido el archivo basta con enviarlo mediante el SDK oficial.
uploader.Upload(ctx, &s3.PutObjectInput{
Bucket: aws.String("mi-bucket"),
Key: aws.String(filename),
Body: file,
})
Esta estrategia evita llenar el disco del servidor y simplifica el escalado horizontal.
Streaming para archivos grandes
Un error bastante frecuente consiste en cargar archivos completos en memoria.
Con archivos de varios gigabytes el consumo puede dispararse rápidamente.
Una ventaja de io.Copy es que realiza la copia utilizando un buffer interno.
io.Copy(dst, file)
No necesita cargar el archivo completo en memoria antes de escribirlo.
Cuando el almacenamiento también soporta streaming (como S3), el consumo de memoria permanece prácticamente constante.
Respuestas JSON
En APIs REST normalmente no se devuelve texto plano.
type UploadResponse struct {
File string `json:"file"`
Size int64 `json:"size"`
}
Respuesta:
response := UploadResponse{
File: filename,
Size: header.Size,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
Respuesta obtenida:
{
"file": "174981922831923.png",
"size": 928112
}
Organización del código
Cuando el proyecto crece resulta conveniente separar responsabilidades.
internal/
handlers/
upload.go
services/
storage.go
validators/
file.go
middleware/
El handler debería limitarse a recibir la petición HTTP.
Las validaciones, almacenamiento y lógica de negocio conviene mantenerlas desacopladas para facilitar pruebas y mantenimiento.
Consideraciones de seguridad
Hay varias medidas que suelen olvidarse durante las primeras implementaciones:
- Validar el tipo MIME además de la extensión.
- Limitar el tamaño máximo permitido.
- Generar nombres únicos.
- No confiar en el nombre enviado por el cliente.
- Evitar permisos de escritura innecesarios.
- Analizar archivos con antivirus cuando el negocio lo requiera.
- Mantener los archivos fuera del directorio público del servidor si no deben ser accesibles directamente.
- Registrar quién subió cada archivo y cuándo.
Un detalle que suele pasar desapercibido es el almacenamiento de documentos privados.
Guardar archivos dentro de la carpeta pública del servidor web permite que cualquier persona conozca la URL y los descargue sin autenticación. En sistemas donde se manejan contratos, documentos médicos o información financiera, normalmente se almacenan fuera del directorio público y se entregan mediante un endpoint que verifica permisos antes de iniciar la descarga.
Buenas prácticas
- Generar nombres únicos en lugar de utilizar el nombre original.
- Validar tanto el tamaño como el tipo real del archivo.
- Limitar el número de archivos que pueden enviarse en una sola petición.
- Utilizar
http.MaxBytesReaderpara proteger el servidor frente a cargas excesivas. - Usar
io.Copypara aprovechar el streaming y evitar consumir memoria innecesariamente. - Mantener la lógica de almacenamiento separada de los handlers HTTP.
- Almacenar archivos en servicios como Amazon S3 o sistemas equivalentes cuando la aplicación deba escalar horizontalmente.
- Registrar eventos de subida y errores para facilitar el diagnóstico en producción.