Skip to content

简单示例

stringsvc 教程

https://github.com/go-kit/examples

基本原则

1
2
3
$ mkdir stringsvc1
$ go mod init example.com/stringsvc1
$ go get github.com/go-kit/kit

新建 main.go

业务逻辑 service

在 Go kit 中,我们将服务建模为接口

1
2
3
4
5
6
7
// StringService provides operations on strings.
import "context"

type StringService interface {
    Uppercase(string) (string, error)
    Count(string) int
}

接口的实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import (
    "context"
    "errors"
    "strings"
)

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 input string is empty
var ErrEmpty = errors.New("Empty string")

请求和响应

在 Go kit 中主要的通信方式是 RPC,接口中的每个方法会被建模为一个远程过程调用。
对于每个方法,我们定义请求和响应的结构体,分别涵盖所有的输入和输出参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
type uppercaseRequest struct {
    S string `json:"s"`
}

type uppercaseResponse struct {
    V   string `json:"v"`
    Err string `json:"err,omitempty"` // errors don't JSON-marshal, so we use a string
}

type countRequest struct {
    S string `json:"s"`
}

type countResponse struct {
    V int `json:"v"`
}

端点 Endpoints

端点的定义如下所示 (不需要自己写代码, go-kit 提供了这个定义):

1
type Endpoint func(ctx context.Context, request interface{}) (response interface{}, err error)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import (
    "context"
    "github.com/go-kit/kit/endpoint"
)

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
    }
}

通信方式 Transports

这里使用 JSON 来通信

执行如下命令下载 transport/http:

1
$ go get github.com/go-kit/kit/transport/http
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import (
    "context"
    "encoding/json"
    "log"
    "net/http"

    httptransport "github.com/go-kit/kit/transport/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))
}

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 decodeCountRequest(_ context.Context, r *http.Request) (interface{}, error) {
    var request countRequest
    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)
}

stringsvc1

1
$ go run main.go
1
2
3
4
$ curl -XPOST -d'{"s":"hello, world"}' localhost:8080/uppercase
{"v":"HELLO, WORLD"}
$ curl -XPOST -d'{"s":"hello, world"}' localhost:8080/count
{"v":12}

中间件

关注点分离

Place your services into a service.go file with the following functions and types.

service.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
    "errors"
    "strings"
)

// StringService provides operations on strings.
type StringService interface {
    Uppercase(string) (string, error)
    Count(string) int
}

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 input string is empty
var ErrEmpty = errors.New("Empty string")

Place your transports into a transport.go file with the following functions and types.

transport.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package main

import (
    "context"
    "encoding/json"
    "net/http"

    "github.com/go-kit/kit/endpoint"
)

type uppercaseRequest struct {
    S string `json:"s"`
}

type uppercaseResponse struct {
    V   string `json:"v"`
    Err string `json:"err,omitempty"` // errors don't JSON-marshal, so we use a string
}

type countRequest struct {
    S string `json:"s"`
}

type countResponse struct {
    V int `json:"v"`
}

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
    }
}

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 decodeCountRequest(_ context.Context, r *http.Request) (interface{}, error) {
    var request countRequest
    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)
}

Transport logging

A middleware is a function that takes an endpoint and returns an endpoint.

1
type Middleware func(Endpoint) Endpoint

Below you can see how a basic logging middleware could be implemented (you don’t need to copy/paste this code anywhere):

1
2
3
4
5
6
7
8
9
func loggingMiddleware(logger log.Logger) Middleware {
    return func(next endpoint.Endpoint) endpoint.Endpoint {
        return func(ctx context.Context, request interface{}) (interface{}, error) {
            logger.Log("msg", "calling endpoint")
            defer logger.Log("msg", "called endpoint")
            return next(ctx, request)
        }
    }
}

Use the go-kit log package and remove the standard libraries log. You will need to remove log.Fatal from the bottom of the main.go file.

1
2
3
import (
    "github.com/go-kit/kit/log"
)

And wire it into each of our handlers. Note that the next code section will not compile until you follow the Application Logging section, which defines loggingMiddleware.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main

import (
    "net/http"
    "os"

    "github.com/go-kit/kit/endpoint"
    httptransport "github.com/go-kit/kit/transport/http"
    "github.com/go-kit/log"
)

func main() {
    logger := log.NewLogfmtLogger(os.Stderr)

    svc := stringService{}

    var uppercase endpoint.Endpoint
    uppercase = makeUppercaseEndpoint(svc)
    uppercase = loggingMiddleware(log.With(logger, "method", "uppercase"))(uppercase)

    var count endpoint.Endpoint
    count = makeCountEndpoint(svc)
    count = loggingMiddleware(log.With(logger, "method", "count"))(count)

    uppercaseHandler := httptransport.NewServer(
        uppercase,
        decodeUppercaseRequest,
        encodeResponse,
    )

    countHandler := httptransport.NewServer(
        count,
        decodeCountRequest,
        encodeResponse,
    )

    http.Handle("/uppercase", uppercaseHandler)
    http.Handle("/count", countHandler)
    http.ListenAndServe(":8080", nil)
}

Application logging

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package main

import (
    "net/http"
    "os"
    "time"

    httptransport "github.com/go-kit/kit/transport/http"
    "github.com/go-kit/log"
)

type loggingMiddleware struct {
    logger log.Logger
    next   StringService
}

func (mw loggingMiddleware) Uppercase(s string) (output string, err error) {
    defer func(begin time.Time) {
        mw.logger.Log(
            "method", "uppercase",
            "input", s,
            "output", output,
            "err", err,
            "took", time.Since(begin),
        )
    }(time.Now())

    output, err = mw.next.Uppercase(s)
    return
}

func (mw loggingMiddleware) Count(s string) (n int) {
    defer func(begin time.Time) {
        mw.logger.Log(
            "method", "count",
            "input", s,
            "n", n,
            "took", time.Since(begin),
        )
    }(time.Now())

    n = mw.next.Count(s)
    return
}

func main() {
    logger := log.NewLogfmtLogger(os.Stderr)

    var svc StringService
    svc = stringService{}
    svc = loggingMiddleware{logger, svc}

    uppercaseHandler := httptransport.NewServer(
        makeUppercaseEndpoint(svc),
        decodeUppercaseRequest,
        encodeResponse,
    )

    countHandler := httptransport.NewServer(
        makeCountEndpoint(svc),
        decodeCountRequest,
        encodeResponse,
    )

    http.Handle("/uppercase", uppercaseHandler)
    http.Handle("/count", countHandler)
    http.ListenAndServe(":8080", nil)
}

Application instrumentation

In Go kit, instrumentation means using package metrics to record statistics about your service’s runtime behavior.
Counting the number of jobs processed, recording the duration of requests after they’ve finished, and tracking the number of in-flight operations would all be considered instrumentation.

We can use the same middleware pattern that we used for logging.

1
2
3
4
$ go mod download github.com/prometheus/client_golang
$ go mod tidy
$ go mod tidy -go=1.16 && go mod tidy -go=1.17
$ go get github.com/prometheus/client_golang/prometheus/promhttp
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
package main

import (
    "fmt"
    "net/http"
    "os"
    "time"

    "github.com/go-kit/kit/metrics"
    kitprometheus "github.com/go-kit/kit/metrics/prometheus"
    httptransport "github.com/go-kit/kit/transport/http"
    "github.com/go-kit/log"
    stdprometheus "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

type loggingMiddleware struct {
    logger log.Logger
    next   StringService
}

func (mw loggingMiddleware) Uppercase(s string) (output string, err error) {
    defer func(begin time.Time) {
        mw.logger.Log(
            "method", "uppercase",
            "input", s,
            "output", output,
            "err", err,
            "took", time.Since(begin),
        )
    }(time.Now())

    output, err = mw.next.Uppercase(s)
    return
}

func (mw loggingMiddleware) Count(s string) (n int) {
    defer func(begin time.Time) {
        mw.logger.Log(
            "method", "count",
            "input", s,
            "n", n,
            "took", time.Since(begin),
        )
    }(time.Now())

    n = mw.next.Count(s)
    return
}

type instrumentingMiddleware struct {
    requestCount   metrics.Counter
    requestLatency metrics.Histogram
    countResult    metrics.Histogram
    next           StringService
}

func (mw instrumentingMiddleware) Uppercase(s string) (output string, err error) {
    defer func(begin time.Time) {
        lvs := []string{"method", "uppercase", "error", fmt.Sprint(err != nil)}
        mw.requestCount.With(lvs...).Add(1)
        mw.requestLatency.With(lvs...).Observe(time.Since(begin).Seconds())
    }(time.Now())

    output, err = mw.next.Uppercase(s)
    return
}

func (mw instrumentingMiddleware) Count(s string) (n int) {
    defer func(begin time.Time) {
        lvs := []string{"method", "count", "error", "false"}
        mw.requestCount.With(lvs...).Add(1)
        mw.requestLatency.With(lvs...).Observe(time.Since(begin).Seconds())
        mw.countResult.Observe(float64(n))
    }(time.Now())

    n = mw.next.Count(s)
    return
}

func main() {
    logger := log.NewLogfmtLogger(os.Stderr)

    fieldKeys := []string{"method", "error"}
    requestCount := kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
        Namespace: "my_group",
        Subsystem: "string_service",
        Name:      "request_count",
        Help:      "Number of requests received.",
    }, fieldKeys)
    requestLatency := kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
        Namespace: "my_group",
        Subsystem: "string_service",
        Name:      "request_latency_microseconds",
        Help:      "Total duration of requests in microseconds.",
    }, fieldKeys)
    countResult := kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
        Namespace: "my_group",
        Subsystem: "string_service",
        Name:      "count_result",
        Help:      "The result of each count method.",
    }, []string{}) // no fields here

    var svc StringService
    svc = stringService{}
    svc = loggingMiddleware{logger, svc}
    svc = instrumentingMiddleware{requestCount, requestLatency, countResult, svc}

    uppercaseHandler := httptransport.NewServer(
        makeUppercaseEndpoint(svc),
        decodeUppercaseRequest,
        encodeResponse,
    )

    countHandler := httptransport.NewServer(
        makeCountEndpoint(svc),
        decodeCountRequest,
        encodeResponse,
    )

    http.Handle("/uppercase", uppercaseHandler)
    http.Handle("/count", countHandler)
    http.Handle("/metrics", promhttp.Handler())
    logger.Log("msg", "HTTP", "addr", ":8080")
    logger.Log("err", http.ListenAndServe(":8080", nil))
}

stringsvc2

The complete service so far is stringsvc2.

运行服务:

1
$ go run *.go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$ curl -XPOST -d'{"s":"hello, world"}' localhost:8080/uppercase
{"v":"HELLO, WORLD"}
$ curl -XPOST -d'{"s":"hello, world"}' localhost:8080/count
{"v":12}
$ curl localhost:8080/metrics
...
go_info{version="go1.17.1"} 1
...
# HELP promhttp_metric_handler_requests_total Total number of scrapes by HTTP status code.
# TYPE promhttp_metric_handler_requests_total counter
promhttp_metric_handler_requests_total{code="200"} 2
promhttp_metric_handler_requests_total{code="500"} 0
promhttp_metric_handler_requests_total{code="503"} 0
...

服务的 log:

1
2
3
msg=HTTP addr=:8080
method=uppercase input="hello, world" output="HELLO, WORLD" err=null took=5.6µs
method=count input="hello, world" n=12 took=5.7µs