{%hackmd theme-dark %}
# go-kit stringsvc 筆記
###### tags: `Rails` `microservice` `Golang`
### Introduction
官網的介紹:
> Go kit is a set of packages and best practices, which provide a comprehensive, robust, and trustable way of building microservices for organizations of any size.
看起來是個 microservice framework
---
### Goals
> - Operate in a heterogeneous SOA — expect to interact with mostly non-Go-kit services
> - RPC as the primary messaging pattern
> - Pluggable serialization and transport — not just JSON over HTTP
> - Operate within existing infrastructures — no mandates for specific tools or technologies
### Non goals
> - Supporting messaging patterns other than RPC (for now) — e.g. MPI, pub/sub, CQRS, etc.
> - Re-implementing functionality that can be provided by adapting existing software
> - Having opinions on operational concerns: deployment, configuration, process supervision, orchestration, etc.
---
### Architecture
#### Transport layer
> Go kit supports various transports for serving services using HTTP, gRPC, NATS, AMQP and Thrift.
> Because Go kit services are just focused on implementing business logic and don’t have any knowledge about concrete transports, you can provide multiple transports for a same service.
> For example, a single Go kit service can be exposed by using both HTTP and gRPC.
trasport 是負責對外的通訊層,可選擇 HTTP / gRPC 等等協定
而且 go-kit 的 `service` 只專注於實現商業邏輯的部分,因此可以很間單的為同個 `service` 提供不同的 `transport` 方式給外界使用。
#### Endpoint layer
> Endpoint is the fundamental building block of servers and clients. In Go kit, the primary messaging pattern is RPC. An endpoint represents a single RPC method.
> Each service method in a Go kit service converts to an endpoint to make RPC style communication between servers and clients.
> Each endpoint exposes the service method to outside world by using concrete transports like HTTP or gRPC. A single endpoint can be exposed by using multiple transports.
go-kit 主要使用的是 RPC 的結構,因此 server & client 會以 RPC 的方式來做溝通
每個 `endpoint` 就代表一個 RPC 的方法,也代表 service 中定義的一個方法
`endpoint` 透過 `transport` 來把 `service` 的方法開放給外界使用,當然也是可以有多個 `trasnport` 的
#### Service layer
> The business logic is implemented in Services. Go kit services are modelled as interfaces.
> The business logic in the services contain core business logic, which should not have any knowledge of endpoint or concrete transports like HTTP or gRPC, or encoding and decoding of request and response message types.
> This will encourage you follow a clean architecture for the Go kit based services. Each service method converts as an endpoint by using an adapter and exposed by using concrete transports. Because of the clean architecture, a single Go kit service can be exposed by using multiple transports.
`service` 是負責處理商業邏輯的部分,在 go-kit 這邊是以 interface 的方式實作
`service` 裡面只專注在處理商業邏輯,並不會去處理到任何跟 `transport` / `endpoint` 有關的東西
e.g. request / response message type, encoding / decoding ...
每個 `service` 的方法可以透過 adapter 轉為一個 `endpoint`,再經由 `transport` 開放到外界
因此保持這樣的架構可以讓 `service` 使用多個 `transport`
---
### stringsvc1
以下會用官網範例 stringsvc1 做介紹,大致整理步驟如下:
1. Build business logic service
2. Implement the service
3. Make request & response for RPC endpoints
4. Create endpoints
5. Expose endpoints to transports
#### 1. Build business logic service
第一步當然是來做最核心的商業邏輯的部分拉~
首先建立一個 interface
```go=
// StringService provides operations on strings.
type StringService interface {
Uppercase(string) (string, error)
Count(string) int
}
```
#### 2. Implement the service
再來是實現這個 interface
[go interface](https://pjchender.github.io/2020/06/05/golang-interfaces/)
```go=
// stringService is a concrete implementation of StringService
type stringService struct{}
func (stringService) Uppercase(s string) (string, error) {
if s == "" {
return "", ErrEmpty
}
return strings.ToUpper(s), nil
}
func (stringService) Count(s string) int {
return len(s)
}
// ErrEmpty is returned when an input string is empty.
var ErrEmpty = errors.New("empty string")
```
#### 3. Make request & response for RPC endpoints
```go=
// For each method, we define request and response structs
type uppercaseRequest struct {
S string `json:"s"`
}
type uppercaseResponse struct {
V string `json:"v"`
Err string `json:"err,omitempty"` // errors don't define JSON marshaling
}
type countRequest struct {
S string `json:"s"`
}
type countResponse struct {
V int `json:"v"`
}
```
這邊每個 type 定義了所有的輸入和輸出格式
比較特別的是 `error` 沒有辦法 `JSON-Marshal`,所以用 `string` 代替
[Custom error marshaling as JSON](http://blog.magmalabs.io/2014/11/13/custom-error-marshaling-to-json-in-go.html)
#### 4. Create endpoints
go-kit 提供了 endpoint 的定義,不需要自己額外定義,直接使用即可 :100:
```go=
type Endpoint func(ctx context.Context, request interface{}) (response interface{}, err error)
```
每個 endpoint 就代表了一個 RPC,每個 service interface 有定義的方法都可以被轉成一個 endpoint
`Adapter` 這邊界接 `service` 的一個方法並回傳一個 `endpoint`
所有 `service` 的方法都用 `adapter` 包起來,等等就可以用 `transport` 對外連結拉 :tada:
```go=
// Endpoints are a primary abstraction in go-kit. An endpoint represents a single RPC (method in our service interface)
func makeUppercaseEndpoint(svc StringService) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(uppercaseRequest)
v, err := svc.Uppercase(req.S)
if err != nil {
return uppercaseResponse{v, err.Error()}, nil
}
return uppercaseResponse{v, ""}, nil
}
}
func makeCountEndpoint(svc StringService) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(countRequest)
v := svc.Count(req.S)
return countResponse{v}, nil
}
}
```
#### 5. Expose endpoints to transports
這邊可以看目前 [go-kit 提供的 `transport`](https://github.com/go-kit/kit/tree/master/transport)
範例是使用 JSON over HTTP : `transport/http`
```go=
// Transports expose the service to the network. In this first example we utilize JSON over HTTP.
func main() {
svc := stringService{}
uppercaseHandler := httptransport.NewServer(
makeUppercaseEndpoint(svc),
decodeUppercaseRequest,
encodeResponse,
)
countHandler := httptransport.NewServer(
makeCountEndpoint(svc),
decodeCountRequest,
encodeResponse,
)
http.Handle("/uppercase", uppercaseHandler)
http.Handle("/count", countHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
```
在這邊我們使用了 httptransport 的 `NewServer` 把 `endpoint` 包成 HTTP.handler
再用 `http.Handle` 來做跟外界的連結
`httptransport.NewServer` 需要幾個參數定義如下:
```go=
// NewServer constructs a new server, which implements http.Handler and wraps
// the provided endpoint.
func NewServer(
e endpoint.Endpoint,
dec DecodeRequestFunc,
enc EncodeResponseFunc,
options ...ServerOption,
) *Server {
...
```
最後帶一下 decode & encode 的作法,基本上就是用 json 的格式
```go=
func decodeUppercaseRequest(_ context.Context, r *http.Request) (interface{}, error) {
var request uppercaseRequest
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)
}
```
#### Result
執行看看:
```bash=
$ curl -XPOST -d'{"s":"hello, world"}' localhost:8080/uppercase
{"v":"HELLO, WORLD"}
$ curl -XPOST -d'{"s":"hello, world"}' localhost:8080/count
{"v":12}
```