In the Java/Scala ecosystem, there are a lot of frameworks that help in building API-based solutions. Examples include the Netflix OSS stack and the Twitter Finangle. Go kit (https://gokit.io/) is a collection of packages that together give a slightly opinionated framework for quickly building a service-oriented architecture.
Go kit enforces separation of concerns through a decorator design pattern. It is organized in three main layers (with some sub-layers):
- Transport layer
- Endpoint layer
- Service layer
This is shown in the following diagram:
The transport layer defines the bindings and implements protocol specifics of various transports such as HTTP and gRPC (Google RPC).
The innermost service layer is where the business logic is implemented in a transport-agnostic fashion. Reminiscent of the Java world, one defines an interface for the service and provides an implementation. This provides another layer of decoupling and ensures that contract and implementation are not muddled together and are clearly maintained separately. One can write service middleware to provide cross-cutting functionality, such as logging, analytics, instrumentation, and so on.
The middle endpoint layer is somewhat equivalent to the controller in the MVC pattern. It is the place where the service layer is hooked up, and the safety and anti-fragile logic is implemented.
Let's take an example of a service that counts vowels in a string. Let's start with our service implementation:
// CountVowels counts vowels in strings. type VowelsService interface { Count(context.Context, string) int } // VowelsService is a concrete implementation of VowelsService type VowelsServiceImpl struct{} var vowels = map[rune]bool{ 'a': true, 'e': true, 'i': true, 'o': true, 'u': true, } func (VowelsServiceImpl) Count(_ context.Context, s string) int { count:= 0 for _, c:= range s { if _, ok:= vowels[c]; ok { count++ } } return count }
The service has an interface that essentially takes a string and returns the number of vowels in it. The implementation uses a lookup dictionary and counts vowels in the string.
Next, we define the input and output response formats for our service:
// For each method, we define request and response structs type countVowelsRequest struct { Input string `json:"input"` } type countVowelsResponse struct { Result int `json:"result"` }
Using this, we can now define Endpoint:
// An endpoint represents a single RPC in the service interface func makeEndpoint(svc VowelsService) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { req:= request.(countVowelsRequest) result:= svc.Count(ctx, req.Input) return countVowelsResponse{result}, nil } }
Till now, we have not defined how the data will reach or be available from the endpoint.
Finally, we hook up the endpoint using a transport of our choice. In the following example, we use HTTP as the transport:
func main() { svc:= VowelsServiceImpl{} countHandler:= httptransport.NewServer( makeEndpoint(svc), decodecountVowelsRequest, encodeResponse, ) http.Handle("/count", countHandler) log.Fatal(http.ListenAndServe(":8080", nil)) } func decodecountVowelsRequest(_ context.Context, r *http.Request) (interface{}, error) { var request countVowelsRequest if err:= json.NewDecoder(r.Body).Decode(&request); err != nil { return nil, err } return request, nil } func encodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error { return json.NewEncoder(w).Encode(response) }
It uses JSON for the serialization format and uses the standard net/http package to define a HTTP server.
The preceding simple example was intended to give a minimal working example of Go kit. There are many more rich constructs such as middleware; for more details, please refer to https://gokit.io/.