{
  "$type": "site.standard.document",
  "canonicalUrl": "https://rednafi.com/go/retry-function/",
  "description": "Build retry logic in Go without reflection using generics. Implement exponential backoff and configurable retry strategies with type safety.",
  "path": "/go/retry-function/",
  "publishedAt": "2024-02-04T00:00:00.000Z",
  "site": "at://did:plc:fgtm2c26vfcj74rfmeggbyqj/site.standard.publication/3mnl6f7ob462z",
  "tags": [
    "Go",
    "TIL",
    "Error Handling"
  ],
  "textContent": "I used to reach for reflection whenever I needed a Retry function in Go. It's fun to\nwrite, but gets messy quite quickly.\n\nHere's a rudimentary Retry function that does the following:\n\n- It takes in another function that accepts arbitrary arguments.\n- Then tries to execute the wrapped function.\n- If the wrapped function returns an error after execution, Retry attempts to run the\n  underlying function n times with some backoff.\n\nThe following implementation leverages the reflect module to achieve the above goals.\nWe're intentionally avoiding complex retry logic for brevity:\n\nThe Retry function uses reflection to call a function passed as any. It takes the\nfunction's arguments as a []any slice, allowing us to run functions with varied\nsignatures. Using reflect.ValueOf(fn).Call(argVals), it dynamically invokes the target\nfunction after converting arguments from any to reflect.Value.\n\nThe retry logic runs up to maxRetry times with exponential backoff. The delay starts at\nstartBackoff, doubles after each failure, and caps at maxBackoff. If the last return\nvalue is an error and retries remain, it waits and tries again. Otherwise, it gives up.\n\nYou can wrap a dummy function that always returns an error to see how Retry works:\n\nRunning it will give you the following output:\n\nThis isn't too terrible for reflection-heavy code. But now that Go has generics, I wanted to\nsee if I could avoid the metaprogramming. While reflection is powerful, it's prone to\nruntime panics and the compiler can't type-check dynamic code.\n\nTurns out, there's a way to write the same functionality with generics if you don't mind\ntrading off some flexibility for shorter and more type-safe code. Here's how:\n\nFunctionally, the generic implementation works the same way as the previous one. However, it\nhas a few limitations:\n\n- The generic Retry assumes the wrapped function returns (result, error). This fits Go's\n  common idiom, but the reflection version could handle varied return patterns.\n\n- The reflection-based Retry wraps any function via the empty interface. The generic\n  version requires a matching signature, so you need a thin wrapper to adapt it.\n\nHere's how you'd use the generic Retry function:\n\nRunning it will give you the same output as before.\n\nNotice how someFunc uses a closure to capture a and b rather than accepting them as\narguments. This adaptation is necessary for type safety. I don't mind it if it means\navoiding reflection - plus the generic version is slightly faster.\n\nAfter this entry went live, [Anton Zhiyanov pointed out on Twitter] that there's a\nclosure-based approach that's even simpler and eliminates the need for generics. The\nimplementation looks like this:\n\nNow calling Retry is easier since the closure signature is static - you don't need to\nadapt the call when the wrapped function's signature changes:\n\nThe runtime behavior of this version is the same as the ones before.\n\nFin!\n\n\n\n\n[anton zhiyanov pointed out on twitter]:\n    https://twitter.com/ohmypy/status/1754105508863393835",
  "title": "Retry function in Go"
}