Spring boot like framework for [golang]. It has 3 major Components
pastebin
clone// PbService provides storage capabilities
type PbService interface {
Create(content string, ctx context.Context) (string, error)
Delete(key string, ctx context.Context) (string, error)
Get(key string, ctx context.Context) (string, error)
}
This struct is used to group together the functionalities of pastebin service
type pbService struct {
memory map[uuid.UUID]string
}
// NewPbService make a new PbService
func NewPbService() PbService {
return pbService{
memory: make(map[uuid.UUID]string),
}
}
In [golang] we do not have a key word to define that this structs implements a specific interface like the implements
in Java.
They way we enforce contracts is by implementing all the methods of the contract interface in our case here its the PbService
interface.
Since our NewPbService
method returns the type of PbService
the go compiler will ensure that NewPbService
confirms to the PbService
interface.
//Create: Here we store the content and return a uuid
func (s pbService) Create(ctx context.Context, content string) (string, error) {
id := uuid.New()
s.memory[id] = content
return id.String(), nil
}
//Delete: Here we use the key to find and delete the content stored
func (s pbService) Delete(ctx context.Context, key string) (string, error) {
id, err := uuid.Parse(key)
if err != nil {
return "", errors.New("Invalid Uuid Format")
}
delete(s.memory, id)
return "ok", nil
}
//Get: Here we use the key to find and return the content stored
func (s pbService) Get(ctx context.Context, key string) (string, error) {
id, err := uuid.Parse(key)
if err != nil {
return "", errors.New("Invalid Uuid Format")
}
content, exists := s.memory[id]
if exists {
return content, nil
}
return "", errors.New("Invalid Uuid")
}
In Go kit, the primary messaging pattern is RPC.
So, every method in our interface will be modeled as a remote procedure call. For each method, we define request and response structs, capturing all of the input and output parameters respectively.
type createPbRequest struct {
content string `json:"content"`
}
type createPbResponse struct {
key string `json:"key"`
Err string `json:"err,omitempty"` // errors don't JSON-marshal, so we use a string
}
type deletePbRequest struct {
key string `json:"key"`
}
type deletePbResponse struct {
status string `json:"status"`
Err string `json:"err,omitempty"` // errors don't JSON-marshal, so we use a string
}
type getPbRequest struct {
key string `json:"key"`
}
type getPbResponse struct {
content string `json:"content"`
Err string `json:"err,omitempty"` // errors don't JSON-marshal, so we use a string
}
An endpoint represents a single RPC, which is a single method in our service.
func createPbEndpoint(svc PbService) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(createPbRequest)
key, err := svc.Create(ctx, req.Content)
if err != nil {
return createPbResponse{key, err.Error()}, nil
}
return createPbResponse{key, ""}, nil
}
}
func deletePbEndpoint(svc PbService) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(deletePbRequest)
status, err := svc.Delete(ctx, req.Key)
if err != nil {
return deletePbResponse{status, err.Error()}, nil
}
return deletePbResponse{status, ""}, nil
}
}
func getPbEndpoint(svc PbService) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(getPbRequest)
content, err := svc.Get(ctx, req.Key)
if err != nil {
return getPbResponse{content, err.Error()}, nil
}
return getPbResponse{content, ""}, nil
}
}
Since this trivial example used JSON over HTTP we would have to decode the JSON to structs that our service can understand
func decodeCreatePbRequest(_ context.Context, r *http.Request) (interface{}, error) {
var request createPbRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
return nil, err
}
return request, nil
}
func decodeDeletePbRequest(_ context.Context, r *http.Request) (interface{}, error) {
var request deletePbRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
return nil, err
}
return request, nil
}
func decodeGetPbRequest(_ context.Context, r *http.Request) (interface{}, error) {
var request getPbRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
return nil, err
}
return request, nil
}
This method would accept an interface
type and convert it JSON, this allows it to accept createPbResponse
,deletePbResponse
,getPbResponse
as an interface{}
and encode it as json using the annotations in the struct definition.
func encodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error {
return json.NewEncoder(w).Encode(response)
}
import (
"context"
"encoding/json"
"errors"
"log"
"net/http"
"github.com/go-kit/kit/endpoint"
"github.com/google/uuid"
httptransport "github.com/go-kit/kit/transport/http"
)
func main() {
svc := NewPbService()
createPbHandler := httptransport.NewServer(
createPbEndpoint(svc),
decodeCreatePbRequest,
encodeResponse,
)
deletePbHandler := httptransport.NewServer(
deletePbEndpoint(svc),
decodeDeletePbRequest,
encodeResponse,
)
getPbHandler := httptransport.NewServer(
getPbEndpoint(svc),
decodeGetPbRequest,
encodeResponse,
)
http.Handle("/create", createPbHandler)
http.Handle("/delete", deletePbHandler)
http.Handle("/get", getPbHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
At this point the main.go
has a lot of code so lets move to different files so that we have separation of concerns.
service.go
package main
import (
"context"
"errors"
"github.com/google/uuid"
)
// PbService provides storage capabilities
type PbService interface {
Create(ctx context.Context, content string) (string, error)
Delete(ctx context.Context, key string) (string, error)
Get(ctx context.Context, key string) (string, error)
}
type pbService struct {
memory map[uuid.UUID]string
}
// NewPbService make a new PbService
func NewPbService() PbService {
return pbService{
memory: make(map[uuid.UUID]string),
}
}
//Create: Here we store the content and return a uuid
func (s pbService) Create(ctx context.Context, content string) (string, error) {
id := uuid.New()
s.memory[id] = content
return id.String(), nil
}
//Get: Here we use the key to find and return the content stored
func (s pbService) Get(ctx context.Context, key string) (string, error) {
id, err := uuid.Parse(key)
if err != nil {
return "", errors.New("Invalid Uuid Format")
}
content, exists := s.memory[id]
if exists {
return content, nil
}
return "", errors.New("Invalid Uuid")
}
//Delete: Here we use the key to find and delete the content stored
func (s pbService) Delete(ctx context.Context, key string) (string, error) {
id, err := uuid.Parse(key)
if err != nil {
return "", errors.New("Invalid Uuid Format")
}
delete(s.memory, id)
return "ok", nil
}
transport.go
package main
import (
"context"
"encoding/json"
"net/http"
"github.com/go-kit/kit/endpoint"
)
type createPbRequest struct {
Content string `json:"content"`
}
type createPbResponse struct {
Key string `json:"key"`
Err string `json:"err,omitempty"` // errors don't JSON-marshal, so we use a string
}
type getPbRequest struct {
Key string `json:"key"`
}
type getPbResponse struct {
Content string `json:"content"`
Err string `json:"err,omitempty"` // errors don't JSON-marshal, so we use a string
}
type deletePbRequest struct {
Key string `json:"key"`
}
type deletePbResponse struct {
Status string `json:"status"`
Err string `json:"err,omitempty"` // errors don't JSON-marshal, so we use a string
}
func createPbEndpoint(svc PbService) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(createPbRequest)
key, err := svc.Create(ctx, req.Content)
if err != nil {
return createPbResponse{key, err.Error()}, nil
}
return createPbResponse{key, ""}, nil
}
}
func deletePbEndpoint(svc PbService) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(deletePbRequest)
status, err := svc.Delete(ctx, req.Key)
if err != nil {
return deletePbResponse{status, err.Error()}, nil
}
return deletePbResponse{status, ""}, nil
}
}
func getPbEndpoint(svc PbService) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(getPbRequest)
content, err := svc.Get(ctx, req.Key)
if err != nil {
return getPbResponse{content, err.Error()}, nil
}
return getPbResponse{content, ""}, nil
}
}
func decodeCreatePbRequest(_ context.Context, r *http.Request) (interface{}, error) {
var request createPbRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
return nil, err
}
return request, nil
}
func decodeGetPbRequest(_ context.Context, r *http.Request) (interface{}, error) {
var request getPbRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
return nil, err
}
return request, nil
}
func decodeDeletePbRequest(_ context.Context, r *http.Request) (interface{}, error) {
var request deletePbRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
return nil, err
}
return request, nil
}
main.go
func main() {
svc := NewPbService()
createPbHandler := httptransport.NewServer(
createPbEndpoint(svc),
decodeCreatePbRequest,
encodeResponse,
)
deletePbHandler := httptransport.NewServer(
deletePbEndpoint(svc),
decodeDeletePbRequest,
encodeResponse,
)
getPbHandler := httptransport.NewServer(
getPbEndpoint(svc),
decodeGetPbRequest,
encodeResponse,
)
http.Handle("/create", createPbHandler)
http.Handle("/delete", deletePbHandler)
http.Handle("/get", getPbHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
All applications need to log information, this can be enabled by adding a logging middleware that we create in a file called logging.go
Middleware in go-kit work on Endpoint
The interface definition is type Middleware func(Endpoint) Endpoint
, which means it is a function that takes in an endpoint and returns an endpoint
We can create the loggingMiddleware
so that it adheres to the PbService
by implementing the Create
Delete
Get
methods.
type loggingMiddleware struct {
logger log.Logger
next PbService
}
func (m loggingMiddleware) Create(ctx context.Context, content string) (output string, err error) {
// This defered function would be invoked just before the retuen statement
defer func(begin time.Time) {
m.logger.Log(
"method", "CreatePb",
"input", content,
"output", output,
"err", err,
"took", time.Since(begin),
)
}(time.Now())
output, err = m.next.Create(ctx, content)
return
}
func (m loggingMiddleware) Delete(ctx context.Context, key string) (output string, err error) {
defer func(begin time.Time) {
m.logger.Log(
"method", "DeletePb",
"input", key,
"output", output,
"err", err,
"took", time.Since(begin),
)
}(time.Now())
output, err = m.next.Delete(ctx, key)
return
}
func (m loggingMiddleware) Get(ctx context.Context, key string) (output string, err error) {
defer func(begin time.Time) {
m.logger.Log(
"method", "GetPb",
"input", key,
"output", output,
"err", err,
"took", time.Since(begin),
)
}(time.Now())
output, err = m.next.Get(ctx, key)
return
}
In order to wire the middleware in all we have to do is link it up with the service that we have defined
package main
import (
"context"
"encoding/json"
"net/http"
"os"
"github.com/go-kit/kit/log"
httptransport "github.com/go-kit/kit/transport/http"
)
func main() {
// Use the global logger
logger := log.NewLogfmtLogger(os.Stderr)
var svc PbService
svc = NewPbService()
// Wire the middleware and thats it
svc = loggingMiddleware{logger, svc}
createPbHandler := httptransport.NewServer(
createPbEndpoint(svc),
decodeCreatePbRequest,
encodeResponse,
)
deletePbHandler := httptransport.NewServer(
deletePbEndpoint(svc),
decodeDeletePbRequest,
encodeResponse,
)
getPbHandler := httptransport.NewServer(
getPbEndpoint(svc),
decodeGetPbRequest,
encodeResponse,
)
http.Handle("/create", createPbHandler)
http.Handle("/delete", deletePbHandler)
http.Handle("/get", getPbHandler)
logger.Log("msg", "HTTP", "addr", ":8080")
logger.Log("err", http.ListenAndServe(":8080", nil))
}
func encodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error {
return json.NewEncoder(w).Encode(response)
}
$ curl localhost:8080/create -XPOST -d '{"content":"THIS IS SPARTA"}'
{"key":"c449250a-d74c-4d23-acbb-6785b0bd822a"}
$ curl localhost:8080/get -XPOST -d '{"key":"c449250a-d74c-4d23-acbb-6785b0bd822a"}'
{"content":"THIS IS SPARTA"}
$ curl localhost:8I00/delete -XPOST -d '{"key":"c449250a-d74c-4d23-acbb-6785b0bd822a"}'
{"status":"ok"}
$ curl localhost:8080/get -XPOST -d '{"key":"c449250a-d74c-4d23-acbb-6785b0bd822a"}'
{"content":"","err":"Invalid Uuid"}
$ ./pastebin-II
msg=HTTP addr=:8080
method=CreatePb input="THIS IS SPARTA" output=c449250a-d74c-4d23-acbb-6785b0bd822a err=null took=67.92µs
method=GetPb input=c449250a-d74c-4d23-acbb-6785b0bd822a output="THIS IS SPARTA" err=null took=1.675µs
method=DeletePb input=c449250a-d74c-4d23-acbb-6785b0bd822a output=ok err=null took=1.45µs
method=GetPb input=c449250a-d74c-4d23-acbb-6785b0bd822a output= err="Invalid Uuid" took=803ns