Go-native abstract service interface design

215 views
Skip to first unread message

grant

unread,
Nov 15, 2023, 7:06:08 AM11/15/23
to golang-nuts
I've stumbled upon a set of interfaces that doesn't feel quite right, but I can't say why exactly.

```
type (
Named interface {
Name() string
}
App interface {
Named
Run() error
Shutdown()
}
// Runnable typically represents a MQTT or HTTP listener
Runnable interface {
Named
Run(ctx context.Context) error
Shutdown(ctx context.Context) error
}
)
```
There are 5-10 app implementations, each of which implements ~3 "Runnables" on top of ~4 builtin "Runnables".

One problem I can see with the above is the ambiguity of `Shutdown` - shouldn't it suffice to cancel the run context? The most analogous stdlib construct is probably the http package's server, whose `ListenAndServe` doesn't take context, but that code probably predates the context package.

What would be an idiomatic way to design these interfaces? Or is their mere existence a no-Go?

grant

unread,
Nov 15, 2023, 1:15:05 PM11/15/23
to golang-nuts
edit: The `App` interface has just one implementation which adds the ~4 builtin "Runnables". It is used to implement 5-10 different services each of which adds its custom "Runnables".

Grant Zvolský

unread,
Nov 16, 2023, 8:40:37 AM11/16/23
to golang-nuts
Upon reflection, I would make the following improvements to bring the interfaces closer to Go's philosophical ideals:

* Remove the App interface because it only has one implementation
* Remove the Named interface because all its usages seem to be replaceable with a package-scoped const
* Split the Runnable interface into Runner and Shutdowner (inspired by Reader/Writer) with the advice not to use Shutdowner if context-based shutdown is sufficient

Redesigned interfaces:
```
type Runner interface {
       Run(ctx context.Context) error
}

// Shutdowner adds a Shutdown method for services whose shutdown procedure needs to be cancellable.
type Shutdowner interface {
       Shutdown(ctx context.Context) error
}
```

Brian Candler

unread,
Nov 17, 2023, 7:05:20 AM11/17/23
to golang-nuts
I think it depends on what your semantic contract is for this interface.

If the caller starts a server with Run(ctx), is it implied that cancelling of ctx should stop the running server? If so, ISTM that there is no need for a separate Shutdown() method.  (And there would be no need for App and Runnable to be different interfaces)

Note that if the caller wanted to be able to signal a shutdown without cancelling the overall context, they could have passed in a child context and cancelled that instead.  Equally, if the server needs to receive an explicit Shutdown call for whatever reason, it can run a goroutine which waits for the ctx to be cancelled and then do whatever it would do if Shutdown() were called.

Indeed, I cannot see what Shutdown(ctx context.Context) would actually do. What is the purpose of the ctx passed in here? What if the ctx passed to Shutdown is different to the one passed to Run?

Another question I have around semantics is: if a server has been shutdown, is it permitted to call Run() on it a second time?  You could allow this, but it depends on whether Run() does all initialization, or it depends on a clean underlying object.

There is also a question as to whether the error result from Shutdown() is required - actually, a server might terminate for reasons other than Shutdown() being called - and to know when shutdown has finished after being requested. In that case, you might want a separate way to signal (a) that the server has terminated, and (b) to return the error value. That could for example be a channel returned by Run(). If you make it a 1-element buffered channel then it doesn't matter whether the value is consumed or not.
Reply all
Reply to author
Forward
0 new messages