TodlyCodly
5 min readOct 6, 2021

Photo by Aedrian on Unsplash

Locker — Microservice in Golang

My learning of Golang brought me from very beginning where I was trying to reinvent every wheel at every step to current phase when I try to use standardized approach to common problems. In this article I wanted to share some insight about Go-kit which helps to build “Clean Code” Microservices, transport agnostic, extensible and team work friendly.

Notice

This is not a tutorial or step by step guide, but summary of my findings after completing DIY project.

All of work here is based on example project and C# client I developed during this learning:

mes1234/LockerClient (github.com)

mes1234/golock: Lightweight Secrects Store (github.com)

Go-kit

Go kit — A toolkit for microservices

is a framework to build Clean Architecture services in Go. It helps to keep stuff organized and decoupled. Basically it gives you a lot of out box. There is widely used concept of Middleware's (like in good ASP.NET). Onion organization of code is really great. When middleware is setup once it is transparent and sometimes you can forget that it is there.

Example — Authorization of user

func AuthorizationMiddleware() endpoint.Middleware {
return func(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {

data := ctx.Value(gokitjwt.JWTClaimsContextKey).(*jwt.StandardClaims)
genericRequest := request.(adapters.ClientAssigner)

clientId, _ := uuid.Parse(data.Id)

repository := persistance.NewClientRepository()

client := adapters.Client{
ClientId: clientId,
}

repository.Retrieve(&client)

request = genericRequest.AssignClient(client.ClientId)

return next(ctx, request)
}
}
}

What happened here is as follows:

Expect Middleware and return Middleware so another one can be added

return func(next endpoint.Endpoint) endpoint.Endpoint

This middleware expects that context contains JWT token (attaching token to context is one of things go-kit can also do for you :))

data := ctx.Value(gokitjwt.JWTClaimsContextKey).(*jwt.StandardClaims)clientId, _ := uuid.Parse(data.Id)

Then lets check if this user is defined in our system. Here I am using Repository pattern so there is no need to really difference if it is memory or real DB.

client := adapters.Client{
ClientId: clientId,
}

repository.Retrieve(&client)

And here goes beauty of Golang. By using type assertion and casting to adapters.ClientAssigner interface, ClientId can be automatically added to every request.

genericRequest := request.(adapters.ClientAssigner)
request = genericRequest.AssignClient(client.ClientId)
return next(ctx, request)

And rest of logic is sure that there is always ClientId :)

Endpoint routing

Routing in go-kit is also clean and precise

// MakeEndpoint Prepare endpoint for access service
func MakeEndpoint(svc service.AccessService, endpoint string) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {

switch endpoint {
case "addlocker":
return svc.NewLocker(ctx, request.(adapters.AddLockerRequest))
case "additem":
return svc.Add(ctx, request.(adapters.AddItemRequest))
case "removeitem":
return svc.Remove(ctx, request.(adapters.RemoveItemRequest))
case "getitem":
return svc.Get(ctx, request.(adapters.GetItemRequest))
default:
panic("wrong endpoint name")
}
}
}

In entry point of service we need to explicitly define routes:

addLockerEndpoint := endpoints.MakeEndpoint(svc, "addlocker")
addItemEndpoint := endpoints.MakeEndpoint(svc, "additem")
getItemEndpoint := endpoints.MakeEndpoint(svc, "getitem")
removeItemEndpoint := endpoints.MakeEndpoint(svc, "removeitem")

Then building pipeline gets quite simple:

// Attach  Authorization

addLockerEndpoint = middlewares.AuthorizationMiddleware(logger)(addLockerEndpoint)
addItemEndpoint = middlewares.AuthorizationMiddleware(logger)(addItemEndpoint)
getItemEndpoint = middlewares.AuthorizationMiddleware(logger)(getItemEndpoint)
removeItemEndpoint = middlewares.AuthorizationMiddleware(logger)(removeItemEndpoint)

// Attach Authentication

addLockerEndpoint = gokitjwt.NewParser(auth.Keys, jwt.SigningMethodHS256, gokitjwt.StandardClaimsFactory)(addLockerEndpoint)
addItemEndpoint = gokitjwt.NewParser(auth.Keys, jwt.SigningMethodHS256, gokitjwt.StandardClaimsFactory)(addItemEndpoint)
getItemEndpoint = gokitjwt.NewParser(auth.Keys, jwt.SigningMethodHS256, gokitjwt.StandardClaimsFactory)(getItemEndpoint)
removeItemEndpoint = gokitjwt.NewParser(auth.Keys, jwt.SigningMethodHS256, gokitjwt.StandardClaimsFactory)(removeItemEndpoint)

How to write logic

To take full advantage of “Clean Code” approach I would suggest to keep internal logic basing on Domain objects not on incoming messages.

Even if there will be only one way to transport data (in my case REST) would suggest to keep it decoupled:

Domain internal Request:

// Domain internal add item request
type AddItemRequest struct {
ClientId uuid.UUID
LockerId uuid.UUID // Identification of locker to insert into
SecretId string // Identification of secret to get
Content []byte // Content which shall be injected
}

Rest representation:

// Http accepted representation of add item request
type AddItemHttpInboundDto struct {
LockerId string `json:"lockerid"`
SecretId string `json:"secretid"`
Content string `json:"content"`
}

Mappings:

// Decode Http inbound message to domain accepted message
func DecodeHttpAddItemRequest(_ context.Context, r *http.Request) (interface{}, error) {
var requestHttp AddItemHttpInboundDto
if err := json.NewDecoder(r.Body).Decode(&requestHttp); err != nil {
return nil, err
}
lockerId, err := uuid.Parse(requestHttp.LockerId)
if err != nil {
return nil, err
}
secretid := requestHttp.SecretId

content, err := base64.StdEncoding.DecodeString(requestHttp.Content)
if err != nil {
return nil, err
}

request := adapters.AddItemRequest{
LockerId: lockerId,
SecretId: secretid,
Content: content,
}

return request, nil
}

It might be tempting to take shortcut and use AddItemHttpInboundDto in service logic, but I found it to be a bad idea.

Any change in service logic was quickly causing REST request to fail. With those two decoupled I was always sure that REST call will be decoded, and only thing I needed to do was to correct mappings.

Go-kit downsides

Go-kit is not perfect and that's OK.

I can say that two things are quite significant:

  • Interface{}
  • Huge func main()

Interface{}

Go-kit base a lot of feature on type assertion and boxing/unboxing capability of golang interface{}. I think there is a little bit too much of it. I believe that interface{} is a game changer, it allows to merge static with dynamic language. But I found myself too often in situation that runtime errors where complex to debug.

Huge func main()

In Go-kit main() is like place to rule the world. But it would be nice to be able to register middlewares and connect them into pipelines and let framework figure it out. But it might be ASP.NET talk.

Go-kit good stuff

As I mentioned multiple times, mine simple project was using REST but I am almost sure, that adding gRPC support is a manner of few hours of work. Go-kit helps you to keep logic and transport decoupled all the time.

Metrics/JWT/Authentication/DI — all of stuff you just do, but it is really always the same, go kit has some ready to use packages.

Logic is where it should be — I cannot express how happy I was after few days of break when I got back to my Locker project and instantly knew where I should put logic, where to change REST etc. Go-kit is your friend if you want to keep stuff where it should be and don’t mess structure down the road :)

TodlyCodly
TodlyCodly

Written by TodlyCodly

C# developer, who once was Pythonista and dreams of being Golang guy.

No responses yet