Skip to content

Instantly share code, notes, and snippets.

@boseabhishek
Last active April 5, 2024 16:42
Show Gist options
  • Save boseabhishek/1f7953a233ac57b11c4d343dc3173cb5 to your computer and use it in GitHub Desktop.
Save boseabhishek/1f7953a233ac57b11c4d343dc3173cb5 to your computer and use it in GitHub Desktop.
Wait for a task to finish with timeout
// usage:
// go run waitfor.go <num>
// where, num can be 1 or 2 or 3 or 4.
package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"time"
)
// ----- IMPORTANT -----
// Go routines in funcs:
// waitForWithTimeoutReturnsChannel & waitForWithCtxWithTimeoutReturnsChannel
// are used bc they return channels(w/ data inside) from other func someTimeConsumingOp
// instead of data directly...
type someresp struct {
status bool
}
// waitForWithTimeoutReturnsChannel waits for a specific task for a timeout duration.
// No context used.
// note: the duration must be a bit more than teh expected time the task might take.
func waitForWithTimeoutReturnsChannel(timeout time.Duration) (<-chan *someresp, <-chan error) {
resC := make(chan *someresp)
errC := make(chan error, 1)
go func() {
defer close(resC)
defer close(errC)
s, err := someTimeConsumingOp()
if err != nil {
errC <- err
}
select {
case resC <- &someresp{status: s}:
case <-time.After(timeout):
err = fmt.Errorf("timeout occurred")
errC <- err
}
}()
return resC, errC
}
// waitForWithCtxWithTimeoutReturnsChannel waits for a specific task till the context is "Done".
// note: the invoker must program the context accordingly.
func waitForWithCtxWithTimeoutReturnsChannel(ctx context.Context) (<-chan *someresp, <-chan error) {
resC := make(chan *someresp)
errC := make(chan error, 1)
go func() {
defer close(resC)
defer close(errC)
s, err := someTimeConsumingOp()
select {
case <-ctx.Done():
if ctx.Err() == context.DeadlineExceeded {
errC <- fmt.Errorf("canceled due to timeout: %v", ctx.Err())
} else {
errC <- fmt.Errorf("canceled: %v", ctx.Err())
}
default:
if err != nil {
errC <- err
} else {
resC <- &someresp{status: s}
}
}
}()
return resC, errC
}
// waitForWithTimeout waits for a timeout.
// note: the invoker must pass teh right timeout.
func waitForWithTimeout(timeout time.Duration) (*someresp, error) {
// acheive the timeout by creating a context w/ timeout.
// or, use time.After(timeout) as waitForWithTimeoutReturnsChannel instead.
// Both timeout and context with timeout can be used to enforce time limits for operations,
// context with timeout provides additional features such as cancellation propagation and cleanup,
// making it more suitable for managing deadlines and timeouts in concurrent programs.
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
s, err := someTimeConsumingOp()
select {
case <-ctx.Done():
if ctx.Err() == context.DeadlineExceeded {
return nil, fmt.Errorf("canceled due to timeout: %v", ctx.Err())
} else {
return nil, fmt.Errorf("canceled: %v", ctx.Err())
}
default:
if err != nil {
return nil, err
} else {
return &someresp{status: s}, nil
}
}
}
type Todo struct {
UserID int `json:"userId"`
ID int `json:"id"`
Title string `json:"title"`
Body string `json:"body"`
}
// waitForHttpGetCallWithTimeout shows how a GET http call can be managed and handled via timeouts.
func waitForHttpGetCallWithTimeout(url string, timeout time.Duration) (*Todo, error) {
// creating a new context with a timeout
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
// adds the ctx with timeout into req
req = req.WithContext(ctx)
resp, err := http.DefaultClient.Do(req)
if err != nil {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
return nil, err
}
defer resp.Body.Close()
data := new(Todo)
err = json.NewDecoder(resp.Body).Decode(data)
if err != nil {
return nil, fmt.Errorf("error decoding response body: %v", err)
}
return data, nil
}
func main() {
if len(os.Args) == 1 {
fmt.Println("oops, say 1, 2 or 3!")
return
}
option := os.Args[1]
switch option {
case "1":
// with timeout and receive channels
resC, errC := waitForWithTimeoutReturnsChannel(10 * time.Second)
select {
case result := <-resC:
fmt.Println("result:", result)
case err := <-errC:
fmt.Println("error:", err)
}
case "2":
// with context and receive channels
// create context w/ cancel
// w/ cancel, ctx gets done once the ops is complete
// ctx, cancel := context.WithCancel(context.Background())
// create context w/ timeout
// w/ timeout, ctx gets done once the time is complete.
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
resC, errC := waitForWithCtxWithTimeoutReturnsChannel(ctx)
select {
case result := <-resC:
fmt.Println("result:", result)
case err := <-errC:
fmt.Println("error:", err)
}
case "3":
res, err := waitForWithTimeout(10 * time.Second)
fmt.Println("result:", res, "error:", err)
case "4":
res, err := waitForHttpGetCallWithTimeout("https://jsonplaceholder.typicode.com/todos/1", 10*time.Second)
fmt.Println("result:", res, "error:", err)
}
}
// calling a REST API
// or waiting for something to happen
// or a task to get to a desired state
// (adjust someTimeConsumingOp() signature accordingly)
func someTimeConsumingOp() (bool, error) {
time.Sleep(5 * time.Second) // some actual op as defined above
return true, nil
}
@boseabhishek
Copy link
Author

Closing channels

In Go, it's generally not necessary to manually close channels unless you have a specific reason for doing so. Channels are automatically closed when the sender finishes sending values - the channel will be automatically closed when the function returns or when the goroutine sending on the channel completes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment