Here is a variant of the topological sorting algorithm discussed in The Art of Computer Programming, vol. 1 (Fundamental Algorithms), sect. 2.2.3 implemented in Go, that implements the required features:

package graph import "os/exec" // a user of this package shall initialize each node with the command to be // run and a set of other nodes that can only be executed when this node has // been finished. It is assumed that no cycles exist. type Node struct { exec.Cmd incoming int // populated by Execute() Outgoing []Node } type Graph struct { Nodes []Node } // execute a graph func (g *Graph) Execute() { var dispatcher = make(chan *Node) for _, n := range g.Nodes { for _, nn := range n.Outgoing { nn.incoming++ } } for _, n := range g.Nodes { if n.incoming == 0 { go n.run(dispatcher) } } // each node will spawn one task for _ = range g.Nodes { n := <-dispatcher for _, nn := range n.Outgoing { nn.incoming-- if nn.incoming == 0 { go n.run(dispatcher) } } } } // run the function corresponding to a node and notify when done func (n *Node) run(dispatcher chan<- *Node) { n.Cmd.Run() dispatcher <- n }

Instead of a set of undone tasks, this algorithm directly spawns workers and collects them using a channel.