Recently, I built a lightweight task scheduler on top of AWS ECS in Go. The application schedules batch jobs with custom arguments and schedules as single containers via AWS ECS, an orchestrator built on top of EC2 and Docker. One of the key requirements of the scheduler is monitoring the status of actively running tasks. For this, I chose to use AWS ECS’s DescribeTasks endpoint, which takes ECS task ID(s) and returns their status, exit code if complete, reason for stopping, etc.

Due to rate limits, I had to batch API calls. Initially, I thought my code would look something like this —

Seems reasonable, right? The scheduler just has to create a monitor, submit tasks via Watch , and poll GetStatus until the target task is complete.

… Sorta. The first problem with GetStatus is the scheduler must know the monitor is batched since it returns a map of all tasks to their status. Exposing implementation details in an API is generally a sign of a leaky abstraction.

Take #2—

Great! Now, GetStatus returns a status given a task. Though just a few extra characters, this is a huge usability improvement since the scheduler no longer needs to batch tasks.

However, there’s still a problem with this code. When should scheduler call GetStatus ? In a loop, of course!

That works, but it’s not elegant and less than ideal for more expensive operations. We can do better.

If you came to Go from JavaScript, Scala, C#, or another language with Promises/Futures, you’re probably thinking “How do those translate to Go?”. Well, Go doesn’t have “futures” per se, but it also doesn’t need them because channels are so powerful. Let’s take a peek!

Boom! This is beautiful, efficient, and way simpler. monitor.Watch(task) returns a channel, which is “await”-able.

Using channels as futures in Go isn’t perfect. First, it’s not possible to make a library around them with helper functions as seen in Bluebird, for example, due to Go’s lack of generics. Also, creating a channel for every function call is not space-efficient, as each channel has its own mutex, buffer, etc., so this isn’t the right decision if you’re dealing with lots of calls in a performance-intensive application. In those cases, you should use a single channel and reader, e.g.

… but that comes with more indirection so try out channels as futures when possible (most use cases) for a readable, straight-forward API!