Replacing Signals With Small Interfaces
I ported the non-visual half of a Qt utility to Go and immediately missed signals and slots. They had been carrying progress, log messages, and completion notifications between objects. My first replacement was a channel for every event. It worked and looked like a telephone exchange designed by an enthusiastic octopus.
The simpler answer for synchronous notifications was a small interface:
type Progress interface {
Step(done, total int)
}
func scan(files []string, progress Progress) os.Error {
for i, name := range files {
if err := scanFile(name); err != nil {
return err
}
progress.Step(i+1, len(files))
}
return nil
}
The terminal implementation prints a line. A test implementation records calls. A future graphical shell can arrange delivery to its UI thread. The scanner knows only the behavior it needs.
This differs from a Qt signal in an important way: the call is ordinary and synchronous. The scanner waits while Step runs. That makes control flow and failure behavior obvious, but a slow observer slows the operation. If isolation is required, an adapter can implement Progress by sending values to a channel consumed elsewhere.
I prefer putting that asynchronous decision in the adapter rather than in the core interface. A channel everywhere turns every notification into a lifetime and shutdown problem. An interface everywhere can accidentally block. Neither mechanism removes design; they merely make different design visible.
For optional progress I use a no-op implementation rather than scattering nil checks through the scanner. It is one tiny type with one empty method, and it keeps the hot path unsurprising.
Completion remains a return, not another notification. The caller already has a direct control path, and duplicating it creates two accounts of whether work finished.
The Go compiler decides satisfaction structurally. My terminal reporter does not inherit from a framework base or declare that it implements Progress; matching Step is enough. That made extracting this contract from existing code much easier than I expected.
This is written against an early 2011 weekly snapshot, including the os.Error return convention. APIs will move. The lesson I am keeping is narrower: use a direct small interface for required behavior, then add a channel at the boundary where asynchronous delivery is actually needed. Not every callback needs its own postal service.