Writing a service container in Go



I've been working on a pretty large API recently, lots of routes, services, handlers etc. The first thing I noticed is my main.go file started to become excuciatingly bloated with 'bootstrapping' code.

In order to avoid setting services as globals, I was (rightly) using structs to share services around handlers. Take this example:

main.go

package main

func main() {  
    r := gin.Default()

    userRepo := models.NewUserRepo(
        drivers.DataStore().C("users"),
    )

    userHandler := handlers.NewUserHandler(userRepo)

    r.GET("/api/v1/users", userHandler.FindAll)

    r.Run(":8080")
}

user_handler.go

type UserHandler struct {  
    userRepo *models.UserRepo
}

func NewUserHandler(userRepo *models.UserRepo) *UserHandler {  
    return &UserHandler{
        userRepo,
    }
}

func (userHandler *UserHandler) FindAll(c *gin.Context) {  
    users, err := userHandler.userRepo.FindAll()
    if err != nil {
        c.JSON(404, nil)
        return
    }
    c.JSON(200, users)
    return
}

This worked perfectly well, however, as you can see in my main.go, I'm doing a fair bit of bootstrapping, and that was for just one handler, and one repository.

So I started playing around with the idea of a container in go. I couldn't find any other libraries I really liked the feel of. So I came up with this tiny section of code.

import(  
    "sync"
) 

type Container struct{  
    mux sync.RWMutex
    m map[string]interface{}
}

// Add service
func (c *Container) Add(name string, object interface{}) {  
    if c.m == nil {
        c.m = make(map[string]interface{})
    }    
    c.mux.Lock()
    c.m[name] = object
    c.mux.Unlock()
}

// Remove service
func (c *Container) Remove(name string) Container {  
    c.mux.Lock()
    delete(c.m, name)
    c.mux.Unlock()
}

// Get a service
func (c *Container) Get(name string) (object interface{}, bool) {  
    c.mux.RLock()
    object, ok = c.m[name]
    c.mux.RUnlock()
    return object, ok
}

Notice each function uses a mutex lock to avoid issues when using the container concurrently.

Now I can do something like...

func GetContainer() Container {  
    c := new(container.Container)
    c.Add("user.handler", handlers.UserHandler)
    return c 
}

Now in main.go

func main() {  
    container := container.GetContainer()

    userHandler, ok := container.Get("user.handler")

    if !ok {
        log.Fatal("Service not found")    
    }

    r.GET(
       "/api/v1/users", 
       userHandler.(*handlers.UserHandler).FindAll(),
    )
}

Props to /u/itsmontoya for the sync stuff

Now I have all the bootstrapping neatly packaged away. Coming from a PHP background, I guess I had pimple in mind when I started thinking of the syntax.

I started abstracting this into its own library, so here's the repo, feel free to contribute.