A couple of months ago, Google released Service Weaver - a Go framework for developing so-called moduliths (modular monoliths). There is no exact definition of a modulith, but in my view, it is supposed to bridge the gap between monoliths and microservices. In simpler terms, a modulith allows the developer to develop and deploy an application as if it were a monolith and still be able to distribute it across a cluster of microservices at the push of a button.
On paper, Service Weaver seems to be able to do just that - it introduces the concept of components that interact with but know very little about one another. A plain Go interface represents a component contract:
type Reverser interface {
Reverse(context.Context, string) (string, error)
}
Each component has one or more concrete implementations too:
//go:generate go run github.com/ServiceWeaver/weaver/cmd/weaver@latest generate
type reverser struct {
weaver.Implements[Reverser]
}
// Reverse is a concrete implementation of the Reverser interface
func (r *reverser) Reverse(ctx context.Context, s string) (string, error) {
// concrete implementation
}
The interesting part here is the embedding of weaver.Implements[Reverser]
into reverser
. Although it defies the core tenet of Go interfaces (interfaces and their implementations don’t need to know about one another), it serves an important purpose in Service Weaver. This is a marker for SW’s code generator to go and generate a whole bunch of type-safe Go code that does three things:
- Create a local stub for
Reverser
for when we run the application as a single monolith. Essentially, it “binds”Reverser
toreverser
and forwards all method calls to its concrete implementation. - Create remote clients and servers for when (or if) we decide to split components into microservices. Thus, calling
Reverser's
Reverse(...)
method turns into a whole chain of serializing the input, executing a network call, deserializing it on the other end, performing the desired action, and returning a response the entire way back. If that sounds a lot like CORBA, Erlang’s actor model, or Java’s Enterprise Beans (EJBs), that’s because it’s kind of what it is (or maybe not). In any case, that’s not what I am interested in right now. What interests me the most is p3. - Initialize and inject component dependencies (concrete, or network proxies, depending on how we deploy).
A-ha! So SW is a network proxy code generator and a dependency injection (DI) container (sort of like Spring, but for Go). But how does that actually work?
Suppose our Reverser
component wants to capitalize string inputs before returning them to the caller. For that, it will need a Capitalizer
:
type Capitalizer interface {
Capitalize(context.Context, string) (string, error)
}
To make use of the Capitalizer
, our Reverser
implementation needs to use another SW marker, called weaver.Ref
:
type reverser struct {
weaver.Implements[Reverser]
cap weaver.Ref[Capitalizer]
}
Assuming that all the initialization is going through through SW, our Reverser
can now make use of the Capitalizer
implementation:
// Reverse is a concrete implementation of the Reverser interface
func (r *reverser) Reverse(ctx context.Context, s string) (string, error) {
// concrete implementation
return cap.get().Capitalize(ctx, reversedStr)
}
Again, the concrete implementation of Capitalizer
will change depending on whether we run our application as a monolith or as a distributed cluster of microservices, but our code will remain the same.
In a production scenario, Reverser
and Capitalizer
will be replaced by more practical examples - Storage
(for DB operations), Mailer
, various clients for 3rd-party API calls, or other business logic components.
It is important to note that this way of injecting components into one another is uncommon and not preferred in the Go community. As a Java Spring developer, I am used to using automated dependency injection, so I can see how the weaver.Ref
part can become a thing of its own (even without the network proxy part), but that’s not what the majority of the Go community will agree with. Still, it is a matter of time to see where the future direction of SW will take, whether it will become a popular framework, and whether it will inspire a new wave of business application frameworks.
Have something to say? Join the discussion below 👇
Want to explore instead? Fly with the time capsule 🛸
You may also find these interesting
Interfaces Are Not Meant for That
It’s time to ask ourselves how much abstraction in our Go code really makes sense.
Python is Easy. Go is Simple. Simple != Easy.
Python and Go have distinct qualities that can complement each other.
Focus on the Happy Path With Step Functions
A simple pattern that will help you reduce error handling, while keeping your Go code simple and idiomatic.