En la integración de sistemas, es común tener que consultar grandes volúmenes de información a servicios externos que tienen límites estrictos o, simplemente, servicios de terceros donde no tenemos la posibilidad de solicitar un escalamiento.
En los últimos años, he tenido la "oportunidad" de desarrollar soluciones para migraciones de datos con volúmenes entre 1.000 y 300.000 registros, con datos que dependen de otros sistemas, en su mayoría legacy. Si a eso le sumamos que muchos de estos no podían ser escalados para soportar consultas muy grandes porque colapsaban, la situación se volvía un dolor de cabeza.
Y dado que no era viable una solución trivial como hacer llamadas individuales para cada elemento porque eso no es eficiente, es costoso y poco sostenible , necesitaba una forma simple y reutilizable de dividir la carga de trabajo o, dicho de otra manera, dividir el problema. A continuación, expongo un caso real:
El caso
Tenemos que consultar 500.000 ítems para enriquecer la data que migraremos a los nuevos sistemas. A esta data la llamaremos "Producto". En este caso, solo necesitamos obtener información desde un servicio para guardar un nuevo campo llamado codigo_viejo.
Problemas
- No podemos ir a buscar estos ítems de uno en uno.
- El servicio permite búsquedas de hasta un máximo de 100 códigos por request.
- El servicio lanza un RateLimit cuando se hacen muchas peticiones en paralelo.
Diseño de la solución
Creación de una utilidad genérica, ya que este problema se había repetido en varios proyectos.
Decidí crear una función genérica en Go que permitiera dividir un slice de cualquier tipo T
en sub-slices de un tamaño fijo. La idea es que esta utilidad fuera reutilizable en distintos contextos, sin importar el tipo de dato con el que se trabaje ni el uso que se le quiera dar después.
func ChunkSlice[T any](data []T, chunkSize int) [][]T {
if chunkSize <= 0 {
return nil
}
n := len(data)
chunks := make([][]T, 0, (n+chunkSize-1)/chunkSize)
for i := 0; i < n; i += chunkSize {
end := i + chunkSize
if end > n {
end = n
}
chunks = append(chunks, data[i:end])
}
return chunks
}
Ejemplo de uso
Supongamos que tienes 500.000 códigos y necesitas procesarlos en bloques de 100, porque es lo máximo que tu servicio externo soporta.
//Devuelve una []string con los ID que necesitamos consultar.
itemCodes := getItemsFilesFromCsv()
chunks := utils.ChunkSlice(itemCodes, 100)
oldToNewCodes := make(map[string]string)
for _, chunk := range chunks {
resp, err := service.GetCodes(chunk)
if err != nil {
//Aquí podríamos agregar lógica de reintento o almacenar los errores para manejarlos más tarde.
log.Warn("Error trying to get codes: %s", err.Error())
continue
}
for _, v := range resp {
if v.OldCode != "" {
oldToNewCodes[v.OldCode] = v.NewCode
}
}
}
SaveItems(itemCodes, oldToNewCodes)
En este punto podrías pensar que SaveItems
recibirá demasiada data y que es posible que falle al intentar insertar todo en nuestra base de datos. Dentro de esta función podrías nuevamente dividir la carga usando utils.ChunkSlice(itemCodes, "tamaño ajustado a la capacidad de tu servicio o base de datos")
. En otra ocasión mostraré un ejemplo de esto, enfocado en PostgreSQL y manejo eficiente de conexiones y consultas.
Hablemos un poco de ChunkSlice
:
-
Genérica: Gracias a
T any
, sirve para[]string
,[]int
,[]PurchaseOrders
, etc. -
Segura: Maneja el caso en que
chunkSize <= 0
, devolviendonil
. - Eficiente: Preasigna capacidad para evitar reasignaciones innecesarias.
chunks := make([][]T, 0, (n+chunkSize-1)/chunkSize)
Cuando usas make([][]T, 0, capacidad)
, estás diciendo:
“Define un slice con largo 0 pero con espacio reservado para X elementos”.
Esto evita que Go tenga que crecer el slice dinámicamente con cada append
, lo que implicaría:
- Copiar datos a una nueva ubicación en memoria.
- Reasignar espacio interno varias veces.
Conclusión
Pequeñas utilidades como esta pueden parecer triviales, pero en entornos reales marcan la diferencia entre un sistema confiable y uno que colapsa bajo carga, dejándote muchos dolores de cabeza. Además, escribir herramientas genéricas mejora la mantenibilidad del código y fomenta la reutilización. En equipos grandes esto puede ser beneficioso no solo por ahorrar tiempo, sino porque a mi parecer es más fácil que todos conozcan los utils más usados en el lugar de trabajo.
Puedes descargar el repositorio aquí: Git
Estoy preparando una versión de este artículo donde mostraré casos reales en los que he utilizado este utilitario con goroutines para hacer peticiones a servicios, así como algunos ejemplos mapeando datos para insertar en una base de datos. Donde aparecen nuevos desafíos al utilizar concurrencia en Go.
Top comments (0)