Building Generic Service Container in Go [PART 1]

Walking through the new world of generics in Go.

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()
}
Package: firasdarwish/gencon

See also

comments powered by Disqus