Go kit is a popular Go microservices framework. I've found it quite interesting but lacking clear and detailed examples that a learner can follow. The official hello-world tutorial stringsvc really confused me when I tried to implement stringsvc3. After some struggles, I realized that all this example wanted to do was to "simulate" an API gateway, which was kinda impractical in the real world. Therefore, I combined stringsvc3 with apigateway to create a more practical microservices application. I hope this article may help you if you want to write a useful demo with Go kit.
In this article, I will focus on the implementation of API gateway rather than basic concepts in Go kit such as endpoint, transport or service.
API Gateway Based On Service Discovery
Service discovery is an essential component of a microservices architecture. In a nutshell, we don't need to bother choosing which instance of a service to use because the service discovery system will do this under the hood.
In this example, I will use Consul to implement a very simple client-side service discovery system.
Service Registration
Firstly, download the source code of stringsvc3, remove the file proxying.go
and delete the relevant code in main.go
.
// We don't need this anymore.
// svc = proxyingMiddleware(context.Background(), *proxy, logger)(svc)
Then, we can register the stringsvc using Go kit's sd package.
package main
import (
....
"github.com/go-kit/kit/sd/consul"
"github.com/hashicorp/consul/api"
)
func main() {
...
// Build consul client and register services.
// Specify the information of an instance.
asr := api.AgentServiceRegistration{
// Every service instance must have an unique ID.
ID: fmt.Sprintf("%v%v/%v", host, listen, prefix),
Name: serviceName,
// These two values are the location of an instance.
Address: host,
Port: port,
}
consulConfig := api.DefaultConfig()
// We can get the address of consul server from environment variale or a config file.
if len(consulServer) > 0 {
consulConfig.Address = consulServer
}
consulClient, err := api.NewClient(consulConfig)
if err != nil {
logger.Log("err", err)
os.Exit(1)
}
sdClient := consul.NewClient(consulClient)
registar := consul.NewRegistrar(sdClient, &asr, logger)
registar.Register()
// According to the official doc of Go kit,
// it's important to call registar.Deregister() before the program exits.
defer registar.Deregister()
...
}
It's pretty simple to register services in Consul, the service registry. You can do some additional configurations like some tags for the service. Further reading: Go consul API's Godoc.
I am gonna deploy this demo using Docker Compose later so right now I want to build a Docker image of stringsvc (I have converted this app into a Go modules project in order to manage dependencies more conveniently).
FROM golang:latest as builder
ENV GO111MODULE=on
ENV GOPROXY=https://goproxy.cn,direct
RUN mkdir /app
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o out
FROM alpine:latest
RUN mkdir /app
WORKDIR /app
COPY --from=builder /app/out .
CMD ["./out"]
Service Discovery Client - API Gateway
Create a new Go modules project called stringclient
that will act as the service discovery client i.e. the API gateway. In main.go
file, we first need to build a consul instancer that yields instances for a service, which has been registered in Consul:
// Build instancer.
consulConfig := api.DefaultConfig()
if len(consulServer) > 0 {
consulConfig.Address = consulServer
}
consulClient, err := api.NewClient(consulConfig)
if err != nil {
logger.Log("err", err)
os.Exit(1)
}
client := consul.NewClient(consulClient)
instancer := consul.NewInstancer(client, logger, serviceName, []string{}, true)
I think the last two parameters of consul.NewInstancer
might be confusing (at least I was confused in the very first place). The 4th parameter, tags
, indicates only those services with these tags will be returned. This parameter is useful only when you tag your services when you register them. Otherwise, just pass an empty slice of string. As for the last parameter passingOnly
, only instances where both the service and any proxy are healthy will be returned if it is true
.
At last, we just need to create some endpoints for stringsvc and run the client.
// uppercase endpoint
// Create an endpointer that subscribes to the instancer.
uppercaseEndpointer := sd.NewEndpointer(instancer, serviceFactoryBuilder(uppercasePath, "POST", encodeRequest, decodeResponseFuncBuilder(uppercaseResponse{})), logger)
// Use round-robin load balancing.
// Set retry policy.
uppercaseEndpoint := lb.Retry(3, 3*time.Second, lb.NewRoundRobin(uppercaseEndpointer))
http.Handle(uppercasePath, httptransport.NewServer(
uppercaseEndpoint,
decodeRequestFuncBuilder(uppercaseRequest{}),
encodeResponse,
))
// count endpoint
countEndPointer := sd.NewEndpointer(instancer, serviceFactoryBuilder(countPath, "POST", encodeRequest, decodeResponseFuncBuilder(countResponse{})), logger)
countEndPoint := lb.Retry(3, 3*time.Second, lb.NewRoundRobin(countEndPointer))
http.Handle(countPath, httptransport.NewServer(
countEndPoint,
decodeRequestFuncBuilder(countRequest{}),
encodeResponse,
))
logger.Log("err", http.ListenAndServe(":8080", nil))
In the above code, sd.NewEndpointer
creates an endpointer that subscribes to the instancer. The instancer can retrieve healthy instances from Consul. What the endpointer does is to fetching an instance from the "instance pool" created by the instancer and use a factory function to convert it to a client endpoint. In this case, I define a factory builder function to construct factory function based on some parameters like relative path, HTTP method, etc.
func serviceFactoryBuilder(path string, method string, enc httptransport.EncodeRequestFunc, dec httptransport.DecodeResponseFunc) sd.Factory {
// instance (host:port) is the location of an instance.
return func(instance string) (e endpoint.Endpoint, closer io.Closer, err error) {
httpPrefix := "http://"
if !strings.HasPrefix(instance, httpPrefix) {
instance = httpPrefix + instance
}
tgt, err := url.Parse(instance)
if err != nil {
return nil, nil, err
}
tgt.Path = path
return httptransport.NewClient(method, tgt, enc, dec).Endpoint(), nil, nil
}
}
Of course, build the client program into a Docker image using the same Dockerfile in the last section.
Deployment And Test
Like I said before, I would use Docker Compose to deploy this demo.
version: "3.7"
services:
consul:
image: consul
command: agent -server -bootstrap -ui -client=0.0.0.0
ports:
- 8500:8500
- 8600:8600/udp
networks:
- gokit
stringsvc1:
image: stringsvc
depends_on:
- consul
ports:
- 8001
networks:
- gokit
stringsvc2:
image: stringsvc
depends_on:
- consul
ports:
- 8002
networks:
- gokit
stringsvc3:
image: stringsvc
depends_on:
- consul
ports:
- 8003
networks:
- gokit
stringclient:
image: stringclient
depends_on:
- consul
ports:
- 8080:8080
networks:
- gokit
networks:
gokit:
I created 3 instances of stringsvc called stringsvc1
, stringsvc2
and stringsvc3
respectively. I also created a client that will be the API gateway. After everything is ready, we can test the client:
$ curl -d '{"s": "foo"}' http://localhost:8080/stringsvc/uppercase
{"v":"FOO"}
$ curl -d '{"s": "foo"}' http://localhost:8080/stringsvc/count
{"v":"3"}
Great! It works! There is still one more thing I would like to show you before ending this article:
$ for s in foo bar baz ; do curl -d"{\"s\":\"$s\"}" localhost:8080/stringsvc/uppercase ; done
{"v":"FOO"}
{"v":"BAR"}
{"v":"BAZ"}
If we have a look at the logs, we will find something like this:
stringsvc2_1 | listen=:8002 caller=logging.go:22 method=uppercase input=foo output=FOO err=null took=629ns
stringsvc3_1 | listen=:8003 caller=logging.go:22 method=uppercase input=bar output=BAR err=null took=967ns
stringsvc1_1 | listen=:8001 caller=logging.go:22 method=uppercase input=baz output=BAZ err=null took=646ns
3 instances of the same services were called one by one and this was because we used the round-robin load balancing policy when we created the endpoint.
uppercaseEndpoint := lb.Retry(3, 3*time.Second, lb.NewRoundRobin(uppercaseEndpointer))
Read the complete source code here.
Top comments (2)
Hello , i tried this sample but when run service's main.go file following errors occured
./main.go:97:8: undefined: loggingMiddleware
./main.go:98:8: undefined: instrumentingMiddleware
./main.go:102:3: undefined: makeUppercaseEndpoint
./main.go:103:3: undefined: decodeUppercaseRequest
./main.go:104:3: undefined: encodeResponse
./main.go:107:3: undefined: makeCountEndpoint
./main.go:108:3: undefined: decodeCountRequest
./main.go:109:3: undefined: encodeResponse
can you please tell me why :)
go run .