Finally, Generics are in the house of Go, or are they?
Today I’ll be building a very simple service container using Go’s 1.18 generics as an example
What are service containers anyway?
Service container is a container which stores instances (implementations) of your services (interfaces) so you can retreive/bootstrap them any time you need to.
For this post the said definition is enough, but later on next posts I’ll be extending the service container to full fledged dependency injection solution.
type Welcomer interface {
Welcome(name string) string
}
type englishPerson struct {}
type arabicPerson struct {}
type russianPerson struct {}
func (englishPerson) Welcome(name string) string {
return "Hello " + word
}
func (arabicPerson) Welcome(name string) string {
return "Marhaba habibi " + name
}
func (russianPerson) Welcome(name string) string {
return "Privyet " + name + ", Kak dela ?"
}
In the above example, there’s a pretty simple interface Welcomer
and 3 different structs which implements Welcomer
, now let’s choose one implementation to see how it works:
func main() {
var w Welcomer
w = arabicPerson{}
fmt.Println(w.Welcome("John"))
// prints out: Marhaba habibi John
}
Now let’s start building our generic service container/dependecy injection package!
First of all, a container may contain several interface/struct pairs, so let’s make it a slice.
Okay, but a slice of what?.. Slice of service
type service struct {
inter interface{} // interface
impl interface{} // it's implementation
}
but that’s not generic just yet.
We need to specify the service
’s type:
type service[T comparable] struct {
inter T // interface
impl T // it's implementation
}
T
is the type, and comparable
is it’s constraint.
The service only needs to accept some interface and a struct, but there’s no generic way to accomplish that in Go 1.18, so comparable
is the closest constraint i found.
So now the service
type has some limitations.
comparable
is an interface that is implemented by all comparable types (booleans, numbers, strings, pointers, channels,interfaces
, arrays of comparable types,structs
whose fields are all comparable types). The comparable interface may only be used as a type parameter constraint, not as the type of a variable.
Please note that even though inter
field is an interface
and impl
field a struct
,
I chose to ‘type’ them both as the same type T
, this will make Go compiler accept ONLY values of the same type OR, in our use case, implements T
, so now we can rest assured that Go compiler will yell at us (at compile-time) when the struct (impl
) does NOT implements inter
.
As I said earlier, a container may contains several interface/implementation pairs, so let’s check that too into out package as a global variable:
type service[T comparable] struct {
inter T // interface
impl T // it's implementation
}
var services []service
but this will through an error for not specifying the generic type of services
slice
cannot use generic type service[T comparable] without instantiation
You may think, okay, let’s put that [comparable]
constraint right there.
While it seems logical but unfortunately this, too, will through an error :
var services []service[comparable]
// panic: interface is (or embeds) comparable
but I’ll save talking about these kind of errors for later posts,
for now let’s just use [any]
var services []service[any]
// WORKS OK
Now we need two generic functions Add
and Get
:
// adding a service to our container
func Add [T comparable] (interf, impl T) {
newService := service[any]{
inter: interf,
impl: impl,
}
services = append(services, newService)
}
Notice that here too at line 3 the constraint type is any
while at the function signature is more tightly constrained with comparable
.
And for retrieving a service, let’s build a function which only requires the type of the interface
via type parameters, and then it iterates over services
slice to check which one actually satisfy the requested interface:
// no arguments, only the requested type as type parameter
// and the same type as a return type
func Get [T comparable] () T {
for _, s := range services {
s, ok := s.impl.(T) // type-inference in Go checks whether `s.impl` implements `T`
if ok {
return s
}
}
// if no implementation is found, panic
var requestedTypeInterface T
panic(fmt.Sprintf("NO IMPLEMENTATION FOR %s WAS FOUND", reflect.TypeOf(&requestedTypeInterface).String()))
}
Now let’s do a final example to see how it works, the package’s GitHub link is below the following example, and that’s it for this post, see ya in PART 2.
package main
import "github.com/firasdarwish/gencon"
type englishPerson struct {}
type arabicPerson struct {}
type russianPerson struct {}
type Welcomer interface {
Welcome(name string) string
}
func (englishPerson) Welcome(name string) string {
return "Hello " + word
}
func (arabicPerson) Welcome(name string) string {
return "Marhaba habibi " + name
}
func (russianPerson) Welcome(name string) string {
return "Privyet " + name + ", Kak dela ?"
}
type Runner interface {
RunAwayFromBears()
}
func (englishPerson) RunAwayFromBears() {
return "Running for my life in English"
}
func (arabicPerson) RunAwayFromBears() {
return "Ya weli ya weli ya weli!!"
}
// func (russianPerson) RunAwayFromBears() ....
// In Russia, you don’t run from bear. Bear run from you
// https://www.youtube.com/watch?v=QkrzvRaJvew
func main() {
var r Runner
var w Welcomer
gencon.Add[Welcomer](w, arabicPerson{})
gencon.Add[Runner](r, englishPerson{})
w = gencon.Get[Welcomer]()
w.Welcome("Firas")
r = gencon.Get[Runner]()
r.RunAwayFromBears()
}