Advanced Acknowledgments¶
Acknowledgments (ACKs) in Socket.IO enable implementing request-response patterns over bidirectional communication. This guide covers advanced acknowledgment usage.
Table of Contents¶
Fundamental Concepts¶
What are Acknowledgments?¶
Acknowledgments are callbacks that allow the sender to receive a response from the receiver:
// Client emits and waits for response
client.EmitWithAck("get-data", func(response ...interface{}) {
fmt.Println("Server responded:", response[0])
}, "param1", "param2")
Acknowledgment Direction¶
ACKs can flow in both directions:
Client → Server → Client:
// Client requests data
client.EmitWithAck("get-user", func(response ...interface{}) {
user := response[0].(map[string]interface{})
fmt.Println("User:", user["name"])
}, "user-123")
Server → Client → Server:
// Server requests confirmation, client responds
client.On("confirm-action", func(data ...interface{}) {
action := data[0].(string)
// Last argument is the ACK callback
if ack, ok := data[len(data)-1].(func(...interface{})); ok {
// Confirm the action
ack(map[string]interface{}{
"confirmed": true,
"timestamp": time.Now().Unix(),
})
}
})
Usage Patterns¶
Pattern: Simple Request-Response¶
Basic request with response:
client.EmitWithAck("get-data", func(response ...interface{}) {
if response == nil {
fmt.Println("Timeout: no response received")
return
}
data := response[0]
fmt.Printf("Data received: %v\n", data)
}, "data-id-123")
Pattern: Data Validation¶
Send data and validate it was processed correctly:
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 waiting for confirmation")
return
}
resp := response[0].(map[string]interface{})
if resp["success"].(bool) {
fmt.Println("Data saved successfully")
} else {
result = fmt.Errorf("error: %s", resp["error"])
}
}, data)
<-done
return result
}
// Usage
err := saveData(client, map[string]interface{}{
"name": "Juan",
"age": 30,
})
if err != nil {
fmt.Println("Error:", err)
}
Pattern: Multiple Parallel Requests¶
Execute multiple requests in parallel and wait for all responses:
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
}
// Usage
users := fetchMultipleUsers(client, []string{"user-1", "user-2", "user-3"})
fmt.Printf("Fetched %d users\n", len(users))
Pattern: Request with Retry¶
Retry the request if it fails or times out:
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("Attempt %d failed: %v. Retrying...\n", attempt+1, err)
time.Sleep(time.Second * time.Duration(attempt+1)) // Exponential backoff
} else {
return nil, fmt.Errorf("failed after %d attempts: %v", maxRetries+1, err)
}
}
}
return nil, fmt.Errorf("failed after %d attempts", maxRetries+1)
}
// Usage
result, err := requestWithRetry(client, "get-data", 3, "param1", "param2")
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Result:", result)
}
Pattern: Request Queue¶
Priority request queue:
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)
// Sort by priority (highest first)
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()
// Execute request
rq.client.EmitWithAck(req.Event, req.Callback, req.Data...)
// Small delay between requests
time.Sleep(50 * time.Millisecond)
}
}
// Usage
queue := NewRequestQueue(client)
// High priority request
queue.Add(&Request{
Event: "urgent-data",
Data: []interface{}{"id-1"},
Priority: 10,
Callback: func(response ...interface{}) {
fmt.Println("Urgent response:", response[0])
},
})
// Normal priority request
queue.Add(&Request{
Event: "normal-data",
Data: []interface{}{"id-2"},
Priority: 5,
Callback: func(response ...interface{}) {
fmt.Println("Normal response:", response[0])
},
})
Timeout Handling¶
Global Timeout¶
Configure global timeout for all acknowledgments:
client := socketio.New("ws://localhost:3000", socketio.Options{
AckTimeout: 10 * time.Second, // 10 second timeout
})
client.EmitWithAck("slow-operation", func(response ...interface{}) {
if response == nil {
fmt.Println("⏱️ Operation took longer than 10 seconds")
return
}
fmt.Println("Completed:", response[0])
}, "data")
Per-Request Timeout¶
Implement custom timeout per 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 after %v", timeout)
}
}
// Usage with short timeout
result, err := emitWithCustomTimeout(client, "quick-op", 2*time.Second, "data")
if err != nil {
fmt.Println("Error:", err)
}
// Usage with long timeout
result, err = emitWithCustomTimeout(client, "slow-op", 30*time.Second, "data")
if err != nil {
fmt.Println("Error:", err)
}
Timeout Detection¶
Distinguish between timeout and server error:
client.EmitWithAck("operation", func(response ...interface{}) {
if response == nil {
// Timeout: server did not respond
fmt.Println("⏱️ Timeout: server did not respond in time")
handleTimeout()
return
}
// Response received, check if it's an error
if resp, ok := response[0].(map[string]interface{}); ok {
if errorMsg, hasError := resp["error"]; hasError {
// Server error
fmt.Printf("❌ Server error: %s\n", errorMsg)
handleServerError(errorMsg.(string))
return
}
// Success
fmt.Println("✅ Operation successful:", resp["data"])
handleSuccess(resp["data"])
}
}, "data")
Error Handling¶
Pattern: Standard Error Response¶
Define standard format for error responses:
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: no response from server")
}
if len(response) == 0 {
return nil, fmt.Errorf("empty response")
}
respMap, ok := response[0].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("invalid response format")
}
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
}
// Usage
client.EmitWithAck("operation", func(response ...interface{}) {
resp, err := handleAckResponse(response...)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Data:", resp.Data)
}, "params")
Pattern: Graceful Degradation¶
Handle failures gracefully with fallback values:
func getUserDataWithDefault(client *socketio.Socket, userId string) map[string]interface{} {
defaultUser := map[string]interface{}{
"id": userId,
"name": "Unknown User",
"status": "offline",
}
resultChan := make(chan map[string]interface{}, 1)
client.EmitWithAck("get-user", func(response ...interface{}) {
if response == nil {
// Timeout: use default values
resultChan <- defaultUser
return
}
if user, ok := response[0].(map[string]interface{}); ok {
resultChan <- user
} else {
resultChan <- defaultUser
}
}, userId)
// Wait with additional timeout
select {
case user := <-resultChan:
return user
case <-time.After(6 * time.Second):
// Additional timeout: return default values
return defaultUser
}
}
// Usage
user := getUserDataWithDefault(client, "user-123")
fmt.Printf("User: %s (%s)\n", user["name"], user["status"])
Advanced Patterns¶
Pattern: Promise-like API¶
Wrap acknowledgments in a Promise-like API:
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
}
// Promise-style usage
NewPromise(client, "get-data", "id-123").
Then(func(data interface{}) {
fmt.Println("Data:", data)
}).
Catch(func(err error) {
fmt.Println("Error:", err)
})
// Await-style usage
data, err := NewPromise(client, "get-data", "id-123").Await()
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Data:", data)
}
Pattern: Batch Requests¶
Send multiple requests in a single 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 or no responses")
}
return results, nil
}
// Usage
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)
}
}
Pattern: Streaming with Acknowledgments¶
Implement data streaming with incremental confirmations:
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 on chunk %d", i)
return
}
resp := response[0].(map[string]interface{})
if resp["success"].(bool) {
done <- nil
} else {
done <- fmt.Errorf("error on 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 sent\n", i+1, len(chunks))
}
return nil
}
// Usage
data, _ := os.ReadFile("large-file.bin")
chunks := splitIntoChunks(data, 1024*64) // 64KB chunks
if err := streamDataWithAcks(client, chunks); err != nil {
fmt.Println("Streaming error:", err)
} else {
fmt.Println("File sent completely")
}
Best Practices¶
1. Always Check for nil¶
// ✅ Good
client.EmitWithAck("event", func(response ...interface{}) {
if response == nil {
fmt.Println("Timeout")
return
}
// Process response
}, data)
// ❌ Bad
client.EmitWithAck("event", func(response ...interface{}) {
data := response[0] // Panic if response == nil!
}, data)
2. Use Safe Type Assertions¶
// ✅ Good
if user, ok := response[0].(map[string]interface{}); ok {
fmt.Println(user["name"])
} else {
fmt.Println("Unexpected format")
}
// ❌ Bad
user := response[0].(map[string]interface{}) // Panic if wrong type!
3. Configure Appropriate Timeouts¶
// ✅ Good: timeout based on operation type
fastOps := socketio.Options{AckTimeout: 2 * time.Second}
slowOps := socketio.Options{AckTimeout: 30 * time.Second}
// ❌ Bad: same timeout for everything
opts := socketio.Options{AckTimeout: 5 * time.Second} // May be too short or too long
4. Document Response Format¶
// ✅ Good: document expected response
// Expected response: {success: bool, data: User, error: string}
client.EmitWithAck("get-user", func(response ...interface{}) {
// ...
}, userId)