Acknowledgments Avanzados¶
Los acknowledgments (ACKs) en Socket.IO permiten implementar patrones request-response sobre la comunicación bidireccional. Esta guía cubre uso avanzado de acknowledgments.
Tabla de Contenidos¶
Conceptos Fundamentales¶
¿Qué son los Acknowledgments?¶
Los acknowledgments son callbacks que permiten al emisor recibir una respuesta del receptor:
// Cliente emite y espera respuesta
client.EmitWithAck("get-data", func(response ...interface{}) {
fmt.Println("Servidor respondió:", response[0])
}, "param1", "param2")
Dirección de los Acknowledgments¶
Los ACKs pueden fluir en ambas direcciones:
Cliente → Servidor → Cliente:
// Cliente solicita datos
client.EmitWithAck("get-user", func(response ...interface{}) {
user := response[0].(map[string]interface{})
fmt.Println("Usuario:", user["name"])
}, "user-123")
Servidor → Cliente → Servidor:
// Servidor solicita confirmación, cliente responde
client.On("confirm-action", func(data ...interface{}) {
action := data[0].(string)
// Último argumento es el callback de ACK
if ack, ok := data[len(data)-1].(func(...interface{})); ok {
// Confirmar la acción
ack(map[string]interface{}{
"confirmed": true,
"timestamp": time.Now().Unix(),
})
}
})
Patrones de Uso¶
Patrón: Request-Response Simple¶
Solicitud básica con respuesta:
client.EmitWithAck("get-data", func(response ...interface{}) {
if response == nil {
fmt.Println("Timeout: no se recibió respuesta")
return
}
data := response[0]
fmt.Printf("Datos recibidos: %v\n", data)
}, "data-id-123")
Patrón: Validación de Datos¶
Envía datos y valida que fueron procesados correctamente:
func saveData(client *socketio.Socket, data map[string]interface{}) error {
var result error
done := make(chan struct{})
client.EmitWithAck("save-data", func(response ...interface{}) {
defer close(done)
if response == nil {
result = fmt.Errorf("timeout esperando confirmación")
return
}
resp := response[0].(map[string]interface{})
if resp["success"].(bool) {
fmt.Println("Datos guardados exitosamente")
} else {
result = fmt.Errorf("error: %s", resp["error"])
}
}, data)
<-done
return result
}
// Uso
err := saveData(client, map[string]interface{}{
"name": "Juan",
"age": 30,
})
if err != nil {
fmt.Println("Error:", err)
}
Patrón: Múltiples Requests Paralelos¶
Ejecuta múltiples requests en paralelo y espera todas las respuestas:
func fetchMultipleUsers(client *socketio.Socket, ids []string) map[string]interface{} {
results := make(map[string]interface{})
var mu sync.Mutex
var wg sync.WaitGroup
for _, id := range ids {
wg.Add(1)
go func(userId string) {
defer wg.Done()
client.EmitWithAck("get-user", func(response ...interface{}) {
if response != nil {
mu.Lock()
results[userId] = response[0]
mu.Unlock()
}
}, userId)
}(id)
}
wg.Wait()
return results
}
// Uso
users := fetchMultipleUsers(client, []string{"user-1", "user-2", "user-3"})
fmt.Printf("Obtenidos %d usuarios\n", len(users))
Patrón: Request con Retry¶
Reintenta el request si falla o hace timeout:
func requestWithRetry(client *socketio.Socket, event string, maxRetries int, data ...interface{}) (interface{}, error) {
for attempt := 0; attempt <= maxRetries; attempt++ {
resultChan := make(chan interface{}, 1)
errorChan := make(chan error, 1)
client.EmitWithAck(event, func(response ...interface{}) {
if response == nil {
errorChan <- fmt.Errorf("timeout")
} else {
resultChan <- response[0]
}
}, data...)
select {
case result := <-resultChan:
return result, nil
case err := <-errorChan:
if attempt < maxRetries {
fmt.Printf("Intento %d falló: %v. Reintentando...\n", attempt+1, err)
time.Sleep(time.Second * time.Duration(attempt+1)) // Backoff exponencial
} else {
return nil, fmt.Errorf("falló después de %d intentos: %v", maxRetries+1, err)
}
}
}
return nil, fmt.Errorf("falló después de %d intentos", maxRetries+1)
}
// Uso
result, err := requestWithRetry(client, "get-data", 3, "param1", "param2")
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Resultado:", result)
}
Patrón: Request Queue¶
Cola de requests con prioridad:
type Request struct {
Event string
Data []interface{}
Callback func(...interface{})
Priority int
}
type RequestQueue struct {
client *socketio.Socket
queue []*Request
mu sync.Mutex
running bool
}
func NewRequestQueue(client *socketio.Socket) *RequestQueue {
return &RequestQueue{
client: client,
queue: make([]*Request, 0),
}
}
func (rq *RequestQueue) Add(req *Request) {
rq.mu.Lock()
defer rq.mu.Unlock()
rq.queue = append(rq.queue, req)
// Ordenar por prioridad (mayor primero)
sort.Slice(rq.queue, func(i, j int) bool {
return rq.queue[i].Priority > rq.queue[j].Priority
})
if !rq.running {
go rq.process()
}
}
func (rq *RequestQueue) process() {
rq.running = true
defer func() { rq.running = false }()
for {
rq.mu.Lock()
if len(rq.queue) == 0 {
rq.mu.Unlock()
return
}
req := rq.queue[0]
rq.queue = rq.queue[1:]
rq.mu.Unlock()
// Ejecutar request
rq.client.EmitWithAck(req.Event, req.Callback, req.Data...)
// Pequeño delay entre requests
time.Sleep(50 * time.Millisecond)
}
}
// Uso
queue := NewRequestQueue(client)
// Request de alta prioridad
queue.Add(&Request{
Event: "urgent-data",
Data: []interface{}{"id-1"},
Priority: 10,
Callback: func(response ...interface{}) {
fmt.Println("Respuesta urgente:", response[0])
},
})
// Request de prioridad normal
queue.Add(&Request{
Event: "normal-data",
Data: []interface{}{"id-2"},
Priority: 5,
Callback: func(response ...interface{}) {
fmt.Println("Respuesta normal:", response[0])
},
})
Manejo de Timeouts¶
Timeout Global¶
Configurar timeout global para todos los acknowledgments:
client := socketio.New("ws://localhost:3000", socketio.Options{
AckTimeout: 10 * time.Second, // Timeout de 10 segundos
})
client.EmitWithAck("slow-operation", func(response ...interface{}) {
if response == nil {
fmt.Println("⏱️ Operación tardó más de 10 segundos")
return
}
fmt.Println("Completado:", response[0])
}, "datos")
Timeout por Request¶
Implementar timeout personalizado por request:
func emitWithCustomTimeout(client *socketio.Socket, event string, timeout time.Duration, data ...interface{}) (interface{}, error) {
resultChan := make(chan interface{}, 1)
timeoutChan := time.After(timeout)
client.EmitWithAck(event, func(response ...interface{}) {
if response != nil {
resultChan <- response[0]
}
}, data...)
select {
case result := <-resultChan:
return result, nil
case <-timeoutChan:
return nil, fmt.Errorf("timeout después de %v", timeout)
}
}
// Uso con timeout corto
result, err := emitWithCustomTimeout(client, "quick-op", 2*time.Second, "data")
if err != nil {
fmt.Println("Error:", err)
}
// Uso con timeout largo
result, err = emitWithCustomTimeout(client, "slow-op", 30*time.Second, "data")
if err != nil {
fmt.Println("Error:", err)
}
Detección de Timeouts¶
Distinguir entre timeout y error del servidor:
client.EmitWithAck("operation", func(response ...interface{}) {
if response == nil {
// Timeout: el servidor no respondió
fmt.Println("⏱️ Timeout: el servidor no respondió a tiempo")
handleTimeout()
return
}
// Respuesta recibida, verificar si es error
if resp, ok := response[0].(map[string]interface{}); ok {
if errorMsg, hasError := resp["error"]; hasError {
// Error del servidor
fmt.Printf("❌ Error del servidor: %s\n", errorMsg)
handleServerError(errorMsg.(string))
return
}
// Éxito
fmt.Println("✅ Operación exitosa:", resp["data"])
handleSuccess(resp["data"])
}
}, "datos")
Error Handling¶
Patrón: Error Response Estándar¶
Definir formato estándar para respuestas de error:
type AckResponse struct {
Success bool `json:"success"`
Data interface{} `json:"data,omitempty"`
Error string `json:"error,omitempty"`
Code int `json:"code,omitempty"`
}
func handleAckResponse(response ...interface{}) (*AckResponse, error) {
if response == nil {
return nil, fmt.Errorf("timeout: sin respuesta del servidor")
}
if len(response) == 0 {
return nil, fmt.Errorf("respuesta vacía")
}
respMap, ok := response[0].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("formato de respuesta inválido")
}
ackResp := &AckResponse{}
if success, ok := respMap["success"].(bool); ok {
ackResp.Success = success
}
if data, ok := respMap["data"]; ok {
ackResp.Data = data
}
if errorMsg, ok := respMap["error"].(string); ok {
ackResp.Error = errorMsg
}
if code, ok := respMap["code"].(float64); ok {
ackResp.Code = int(code)
}
if !ackResp.Success && ackResp.Error != "" {
return ackResp, fmt.Errorf("error %d: %s", ackResp.Code, ackResp.Error)
}
return ackResp, nil
}
// Uso
client.EmitWithAck("operation", func(response ...interface{}) {
resp, err := handleAckResponse(response...)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Datos:", resp.Data)
}, "params")
Patrón: Graceful Degradation¶
Manejar fallos de forma elegante con valores por defecto:
func getUserDataWithDefault(client *socketio.Socket, userId string) map[string]interface{} {
defaultUser := map[string]interface{}{
"id": userId,
"name": "Usuario Desconocido",
"status": "offline",
}
resultChan := make(chan map[string]interface{}, 1)
client.EmitWithAck("get-user", func(response ...interface{}) {
if response == nil {
// Timeout: usar valores por defecto
resultChan <- defaultUser
return
}
if user, ok := response[0].(map[string]interface{}); ok {
resultChan <- user
} else {
resultChan <- defaultUser
}
}, userId)
// Esperar con timeout adicional
select {
case user := <-resultChan:
return user
case <-time.After(6 * time.Second):
// Timeout adicional: retornar valores por defecto
return defaultUser
}
}
// Uso
user := getUserDataWithDefault(client, "user-123")
fmt.Printf("Usuario: %s (%s)\n", user["name"], user["status"])
Patrones Avanzados¶
Patrón: Promise-like API¶
Envolver acknowledgments en una API similar a Promises:
type Promise struct {
done chan struct{}
result interface{}
err error
}
func NewPromise(client *socketio.Socket, event string, data ...interface{}) *Promise {
p := &Promise{
done: make(chan struct{}),
}
client.EmitWithAck(event, func(response ...interface{}) {
defer close(p.done)
if response == nil {
p.err = fmt.Errorf("timeout")
return
}
if resp, ok := response[0].(map[string]interface{}); ok {
if errMsg, hasError := resp["error"]; hasError {
p.err = fmt.Errorf("%s", errMsg)
return
}
p.result = resp["data"]
} else {
p.result = response[0]
}
}, data...)
return p
}
func (p *Promise) Then(callback func(interface{})) *Promise {
go func() {
<-p.done
if p.err == nil {
callback(p.result)
}
}()
return p
}
func (p *Promise) Catch(callback func(error)) *Promise {
go func() {
<-p.done
if p.err != nil {
callback(p.err)
}
}()
return p
}
func (p *Promise) Await() (interface{}, error) {
<-p.done
return p.result, p.err
}
// Uso estilo Promise
NewPromise(client, "get-data", "id-123").
Then(func(data interface{}) {
fmt.Println("Datos:", data)
}).
Catch(func(err error) {
fmt.Println("Error:", err)
})
// Uso estilo Await
data, err := NewPromise(client, "get-data", "id-123").Await()
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Datos:", data)
}
Patrón: Batch Requests¶
Enviar múltiples requests en un solo acknowledgment:
type BatchRequest struct {
ID string `json:"id"`
Data interface{} `json:"data"`
}
type BatchResponse struct {
ID string `json:"id"`
Result interface{} `json:"result"`
Error string `json:"error,omitempty"`
}
func batchRequest(client *socketio.Socket, requests []BatchRequest) (map[string]interface{}, error) {
results := make(map[string]interface{})
done := make(chan struct{})
client.EmitWithAck("batch", func(response ...interface{}) {
defer close(done)
if response == nil {
return
}
if responses, ok := response[0].([]interface{}); ok {
for _, r := range responses {
resp := r.(map[string]interface{})
id := resp["id"].(string)
if errMsg, hasError := resp["error"]; hasError && errMsg != "" {
results[id] = fmt.Errorf("%s", errMsg)
} else {
results[id] = resp["result"]
}
}
}
}, requests)
<-done
if len(results) == 0 {
return nil, fmt.Errorf("timeout o sin respuestas")
}
return results, nil
}
// Uso
requests := []BatchRequest{
{ID: "req-1", Data: "user-1"},
{ID: "req-2", Data: "user-2"},
{ID: "req-3", Data: "user-3"},
}
results, err := batchRequest(client, requests)
if err != nil {
fmt.Println("Error:", err)
return
}
for id, result := range results {
if err, isError := result.(error); isError {
fmt.Printf("%s: Error - %v\n", id, err)
} else {
fmt.Printf("%s: Success - %v\n", id, result)
}
}
Patrón: Streaming con Acknowledgments¶
Implementar streaming de datos con confirmaciones incrementales:
func streamDataWithAcks(client *socketio.Socket, chunks [][]byte) error {
for i, chunk := range chunks {
done := make(chan error, 1)
client.EmitBinaryWithAck("stream-chunk", func(response ...interface{}) {
if response == nil {
done <- fmt.Errorf("timeout en chunk %d", i)
return
}
resp := response[0].(map[string]interface{})
if resp["success"].(bool) {
done <- nil
} else {
done <- fmt.Errorf("error en chunk %d: %s", i, resp["error"])
}
}, chunk, map[string]interface{}{
"index": i,
"total": len(chunks),
"last": i == len(chunks)-1,
})
if err := <-done; err != nil {
return err
}
fmt.Printf("Chunk %d/%d enviado\n", i+1, len(chunks))
}
return nil
}
// Uso
data, _ := os.ReadFile("large-file.bin")
chunks := splitIntoChunks(data, 1024*64) // Chunks de 64KB
if err := streamDataWithAcks(client, chunks); err != nil {
fmt.Println("Error en streaming:", err)
} else {
fmt.Println("Archivo enviado completamente")
}
Mejores Prácticas¶
1. Siempre Verificar nil¶
// ✅ Bueno
client.EmitWithAck("event", func(response ...interface{}) {
if response == nil {
fmt.Println("Timeout")
return
}
// Procesar respuesta
}, data)
// ❌ Malo
client.EmitWithAck("event", func(response ...interface{}) {
data := response[0] // Panic si response == nil!
}, data)
2. Usar Type Assertions Seguras¶
// ✅ Bueno
if user, ok := response[0].(map[string]interface{}); ok {
fmt.Println(user["name"])
} else {
fmt.Println("Formato inesperado")
}
// ❌ Malo
user := response[0].(map[string]interface{}) // Panic si tipo incorrecto!
3. Configurar Timeouts Apropiados¶
// ✅ Bueno: timeout según el tipo de operación
fastOps := socketio.Options{AckTimeout: 2 * time.Second}
slowOps := socketio.Options{AckTimeout: 30 * time.Second}
// ❌ Malo: mismo timeout para todo
opts := socketio.Options{AckTimeout: 5 * time.Second} // Puede ser muy corto o muy largo
4. Documentar Formato de Respuestas¶
// ✅ Bueno: documentar qué se espera
// Respuesta esperada: {success: bool, data: User, error: string}
client.EmitWithAck("get-user", func(response ...interface{}) {
// ...
}, userId)