ARTS-WEEK6

2019-04-29

本周ARTS

上周身体不适,休息了一周,下周补上

本周ARTS打卡内容:

  1. Algorithm 来源 LeetCode22
  2. Review 分享 用Go kit写微服务
  3. Tip 分享 cookie、 sessionStorage 、localStorage之间的区别和使用
  4. Share 分享 golang的container/heap包详解

Algorithm

LeetCode的22题,产生合法的括号字符串:

https://leetcode.com/problems/generate-parentheses/

比如给定 n=3 ,产生:

1
2
3
4
5
6
7
[
"((()))",
"(()())",
"(())()",
"()(())",
"()()()"
]

可以用递归的方法考虑:G(n)表示长度为n产生的字符串组,所以:

1
2
3
4
5
6
7
G(n) = “(”+G(0)+")"×G(n-1-0) +
“(”+G(1)+")"×G(n-1-1) +
“(”+G(2)+")"×G(n-1-3) +
...
“(”+G(i)+")"×G(n-1-i) +
...
“(”+G(n-1)+")"×G(0)

代码如下:

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
func generateParenthesis(n int) []string {
if n == 0 {
return []string{""}
}
var results = []string{}
for i := 0 ; i < n ; i++ {
substr1 := generateParenthesis(i)
substr2 := generateParenthesis(n-i-1)
for k, sub := range substr1 {
substr1[k] = "(" + sub + ")"
}

for _, sub := range stingsMutiStrings(substr1, substr2) {
if len(sub) != 0 {
results = append(results, sub)
}
}
}
return results
}

func stingsMutiStrings(str1 ,str2 []string) []string{
if len(str1) == 0 && len(str2) == 0 {
return nil
}
if len(str1) == 0 {
return str2
}
if len(str2) == 0 {
return str1
}
result := []string{}
for _, substr1 := range str1 {
for _, substr2 := range str2 {
sub := substr1 + substr2
result = append(result, sub)
}
}
return result
}

Review

本周内容:How to write a microservice in Go with Go kit

https://dev.to/napolux/how-to-write-a-microservice-in-go-with-go-kit-a66

首先,作者指出了学习使用Go kit的当前情况,样例很好,但Go kit的文档太生硬,市面上教程太少,所以写了这个例子,通过做来学习。

做什么

我们的微服务由一些后端接口:

  • GET /status 返回微服务是否处在运行状态
  • GET /get返回今天的日期
  • POST /validate接收一个dd/mm/yyyy格式的日期字符串,通过正则判断其是否合法

源码在:
https://github.com/napolux/go-kit-microservice-example-tutorial-99999

napodate微服务

在$GOPATH目录下新建一个napodate目录,作为我们微服务的目录。在其中新建一个service.go的文件,然后在其中增加接口:

1
2
3
4
5
6
7
8
9
10
package napodate

import "context"

// Service provides some "date capabilities" to your application
type Service interface {
Status(ctx context.Context) (string, error)
Get(ctx context.Context) (string, error)
Validate(ctx context.Context, date string) (bool, error)
}

这里我们定义了我们服务的“蓝图”: 在Go kit中,我们需要把服务抽象为接口。如上,我们需要三个端点来实现这个接口。

为什么要使用context包呢,请阅读https://blog.golang.org/context:

在Google,我们开发了context包,使一个请求中的请求值,取消,超时传递到所有协程变的更容易

因为我们的微服务从开始的时候就处理并行请求,所以每个请求一个context是必须的。

实现我们的服务

前面我们定义了接口,但其是没有实现的,所以让我们来实现它:

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
type dateService struct{}

// NewService makes a new Service.
func NewService() Service {
return dateService{}
}

// Status only tell us that our service is ok!
func (dateService) Status(ctx context.Context) (string, error) {
return "ok", nil
}

// Get will return today's date
func (dateService) Get(ctx context.Context) (string, error) {
now := time.Now()
return now.Format("02/01/2006"), nil
}

// Validate will check if the date today's date
func (dateService) Validate(ctx context.Context, date string) (bool, error) {
_, err := time.Parse("02/01/2006", date)
if err != nil {
return false, err
}
return true, nil
}

通过新定义的dataService(空结构),我们将服务的方法聚集在一起,并隐藏具体的方法实现。

NewService()是我们对象的构造函数。我们通过调用它来获取服务实例。

编写单元测试

使用NewService()来编写我们的测试用例。创建service_test.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
66
67
68
package napodate

import (
"context"
"testing"
"time"
)

func TestStatus(t *testing.T) {
srv, ctx := setup()

s, err := srv.Status(ctx)
if err != nil {
t.Errorf("Error: %s", err)
}

// testing status
ok := s == "ok"
if !ok {
t.Errorf("expected service to be ok")
}
}

func TestGet(t *testing.T) {
srv, ctx := setup()
d, err := srv.Get(ctx)
if err != nil {
t.Errorf("Error: %s", err)
}

time := time.Now()
today := time.Format("02/01/2006")

// testing today's date
ok := today == d
if !ok {
t.Errorf("expected dates to be equal")
}
}

func TestValidate(t *testing.T) {
srv, ctx := setup()
b, err := srv.Validate(ctx, "31/12/2019")
if err != nil {
t.Errorf("Error: %s", err)
}

// testing that the date is valid
if !b {
t.Errorf("date should be valid")
}

// testing an invalid date
b, err = srv.Validate(ctx, "31/31/2019")
if b {
t.Errorf("date should be invalid")
}

// testing a USA date date
b, err = srv.Validate(ctx, "12/31/2019")
if b {
t.Errorf("USA date should be invalid")
}
}

func setup() (srv Service, ctx context.Context) {
return NewService(), context.Background()
}

以上的测试代码是为了读者容易看到,但请你遵守Subtests, for a more up-to-date syntax

传输

我们的服务通过HTTP暴露。现在我们来设计HTTP请求和响应。在service.go的同级目录新建一个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
package napodate

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

// In the first part of the file we are mapping requests and responses to their JSON payload.
type getRequest struct{}

type getResponse struct {
Date string `json:"date"`
Err string `json:"err,omitempty"`
}

type validateRequest struct {
Date string `json:"date"`
}

type validateResponse struct {
Valid bool `json:"valid"`
Err string `json:"err,omitempty"`
}

type statusRequest struct{}

type statusResponse struct {
Status string `json:"status"`
}

// In the second part we will write "decoders" for our incoming requests
func decodeGetRequest(ctx context.Context, r *http.Request) (interface{}, error) {
var req getRequest
return req, nil
}

func decodeValidateRequest(ctx context.Context, r *http.Request) (interface{}, error) {
var req validateRequest
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
return nil, err
}
return req, nil
}

func decodeStatusRequest(ctx context.Context, r *http.Request) (interface{}, error) {
var req statusRequest
return req, nil
}

// Last but not least, we have the encoder for the response output
func encodeResponse(ctx context.Context, w http.ResponseWriter, response interface{}) error {
return json.NewEncoder(w).Encode(response)
}

可以在作者博客中找到微服务源码,地址。代码很少,但是其中包含了不少注释。

在这个文件的第一部分,我们将请求和响应映射为JSON文本。对于statusRequestgetRequest,由于其不会往服务端发送内容,所以为空。对于validateRequest,我们将传递一个日期来判断是否合法,所以其包含日期字段。响应也是同样的。

在第二部分,我们编写“解码器”来处理请求,告诉服务该怎么处理请求,怎么转换成正确的结构。getstatus是空的,因为它们已完成。

最后,我们解码响应输出,即给定一个对象,返回json文本。

端点

新建一个endpoint.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
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
package napodate

import (
"context"
"errors"

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

// Endpoints are exposed
type Endpoints struct {
GetEndpoint endpoint.Endpoint
StatusEndpoint endpoint.Endpoint
ValidateEndpoint endpoint.Endpoint
}

// MakeGetEndpoint returns the response from our service "get"
func MakeGetEndpoint(srv Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
_ = request.(getRequest) // we really just need the request, we don't use any value from it
d, err := srv.Get(ctx)
if err != nil {
return getResponse{d, err.Error()}, nil
}
return getResponse{d, ""}, nil
}
}

// MakeStatusEndpoint returns the response from our service "status"
func MakeStatusEndpoint(srv Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
_ = request.(statusRequest) // we really just need the request, we don't use any value from it
s, err := srv.Status(ctx)
if err != nil {
return statusResponse{s}, err
}
return statusResponse{s}, nil
}
}

// MakeValidateEndpoint returns the response from our service "validate"
func MakeValidateEndpoint(srv Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(validateRequest)
b, err := srv.Validate(ctx, req.Date)
if err != nil {
return validateResponse{b, err.Error()}, nil
}
return validateResponse{b, ""}, nil
}
}

// Get endpoint mapping
func (e Endpoints) Get(ctx context.Context) (string, error) {
req := getRequest{}
resp, err := e.GetEndpoint(ctx, req)
if err != nil {
return "", err
}
getResp := resp.(getResponse)
if getResp.Err != "" {
return "", errors.New(getResp.Err)
}
return getResp.Date, nil
}

// Status endpoint mapping
func (e Endpoints) Status(ctx context.Context) (string, error) {
req := statusRequest{}
resp, err := e.StatusEndpoint(ctx, req)
if err != nil {
return "", err
}
statusResp := resp.(statusResponse)
return statusResp.Status, nil
}

// Validate endpoint mapping
func (e Endpoints) Validate(ctx context.Context, date string) (bool, error) {
req := validateRequest{Date: date}
resp, err := e.ValidateEndpoint(ctx, req)
if err != nil {
return false, err
}
validateResp := resp.(validateResponse)
if validateResp.Err != "" {
return false, errors.New(validateResp.Err)
}
return validateResp.Valid, nil
}

为了把我们的服务方法Get()Status()Validate()暴露为端点,我们需要编写函数来处理输入请求,调用响应的服务方法,根据响应报文将构建返回合适的对象。

这些方法是上面的那些make函数。它们将接收服务作为参数,然后使用断言强制转换请求类型,再使用转换后的类型来调用服务的方法。

HTTP服务

对于微服务,我们需要HTTP服务。Go相当擅长于此,但是我还是选择https://github.com/gorilla/mux作为路由,因为它看起来简单直接。

再新建一个server.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
package napodate

import (
"context"
"net/http"

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

// NewHTTPServer is a good little server
func NewHTTPServer(ctx context.Context, endpoints Endpoints) http.Handler {
r := mux.NewRouter()
r.Use(commonMiddleware) // @see https://stackoverflow.com/a/51456342

r.Methods("GET").Path("/status").Handler(httptransport.NewServer(
endpoints.StatusEndpoint,
decodeStatusRequest,
encodeResponse,
))

r.Methods("GET").Path("/get").Handler(httptransport.NewServer(
endpoints.GetEndpoint,
decodeGetRequest,
encodeResponse,
))

r.Methods("POST").Path("/validate").Handler(httptransport.NewServer(
endpoints.ValidateEndpoint,
decodeValidateRequest,
encodeResponse,
))

return r
}

func commonMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
next.ServeHTTP(w, r)
})
}

最后,main.go文件

我们有端点,有HTTP服务,现在我们只需要把这些包起来。这就是main.go文件。我们新建一个文件夹cmd:

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
package main

import (
"context"
"flag"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"

"napodate"
)

func main() {
var (
httpAddr = flag.String("http", ":8080", "http listen address")
)
flag.Parse()
ctx := context.Background()
// our napodate service
srv := napodate.NewService()
errChan := make(chan error)

go func() {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
errChan <- fmt.Errorf("%s", <-c)
}()

// mapping endpoints
endpoints := napodate.Endpoints{
GetEndpoint: napodate.MakeGetEndpoint(srv),
StatusEndpoint: napodate.MakeStatusEndpoint(srv),
ValidateEndpoint: napodate.MakeValidateEndpoint(srv),
}

// HTTP transport
go func() {
log.Println("napodate is listening on port:", *httpAddr)
handler := napodate.NewHTTPServer(ctx, endpoints)
errChan <- http.ListenAndServe(*httpAddr, handler)
}()

log.Fatalln(<-errChan)
}

使用flag来让监听端口可配置,服务默认的端口是8080,我们可以通过flag指定任意端口。

接下来,我们创建了一个上下文,获取到我们的服务,error管道也同时创建出来。

然后我们创建了两个协程,一个用来接收CTRL+C来停止服务,另一个接收请求。

handler := napodate.NewHTTPServer(ctx, endpoints)这个handler将映射我们的endpoints服务并返回正确的结果。

最后error管道接收到错误信息,服务停止

启动服务

运行以下命令:

1
go run cmd/main.go

通过curl微服务来测试:

1
2
3
4
5
6
7
8
9
10
11
curl http://localhost:8080/get
{"date":"14/04/2019"}

curl http://localhost:8080/status
{"status":"ok"}

curl -XPOST -d '{"date":"32/12/2020"}' http://localhost:8080/validate
{"valid":false,"err":"parsing time \"32/12/2020\": day out of range"}

curl -XPOST -d '{"date":"12/12/2021"}' http://localhost:8080/validate
{"valid":true}

Tip

分享 cookie、 sessionStorage 、localStorage之间的区别和使用

1.cookie:存储在用户本地终端上的数据。有时也用cookies,指某些网站为了辨别用户身份,进行session跟踪而存储在本地终端上的数据,通常经过加密。一般应用最典型的案列就是判断注册用户是否已经登过该网站。

2.HTML5 提供了两种在客户端存储数据的新方法:(http://www.w3school.com.cn/html5/html_5_webstorage.asp)...两者都是仅在客户端(即浏览器)中保存,不参与和服务器的通信;

  • localStorage - 没有时间限制的数据存储,第二天、第二周或下一年之后,数据依然可用。
  • 如何创建和访问 localStorage:
    1
    2
    3
    4
    <script type="text/javascript">
    localStorage.lastname="Smith";
    document.write(localStorage.lastname);
    </script>

下面的例子对用户访问页面的次数进行计数:

1
2
3
4
5
6
7
8
9
<script type="text/javascript">
if (localStorage.pagecount){
localStorage.pagecount=Number(localStorage.pagecount) +1;
}
else{
localStorage.pagecount=1;
}
document.write("Visits "+ localStorage.pagecount + " time(s).");
</script>
  • sessionStorage - 针对一个 session 的数据存储,当用户关闭浏览器窗口后,数据会被删除。

  • 创建并访问一个 sessionStorage:

1
2
3
4
<script type="text/javascript">
sessionStorage.lastname="Smith";
document.write(sessionStorage.lastname);
</script>
  • 下面的例子对用户在当前 session 中访问页面的次数进行计数:
1
2
3
4
5
6
7
8
9
<script type="text/javascript">
if (sessionStorage.pagecount){
sessionStorage.pagecount=Number(sessionStorage.pagecount) +1;
}
else{
sessionStorage.pagecount=1;
}
document.write("Visits "+sessionStorage.pagecount+" time(s) this session.");
</script>
  • sessionStorage 、localStorage 和 cookie 之间的区别
    共同点:都是保存在浏览器端,且同源的。

  • 区别:cookie数据始终在同源的http请求中携带(即使不需要),即cookie在浏览器和服务器间来回传递;cookie数据还有路径(path)的概念,可以限制cookie只属于某个路径下。存储大小限制也不同,cookie数据不能超过4k,同时因为每次http请求都会携带cookie,所以cookie只适合保存很小的数据,如会话标识。

  • 而sessionStorage和localStorage不会自动把数据发给服务器,仅在本地保存。sessionStorage和localStorage 虽然也有存储大小的限制,但比cookie大得多,可以达到5M或更大。

  • 数据有效期不同,sessionStorage:仅在当前浏览器窗口关闭前有效,自然也就不可能持久保持;localStorage:始终有效,窗口或浏览器关闭也一直保存,因此用作持久数据;cookie只在设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭。

  • 作用域不同,sessionStorage不在不同的浏览器窗口中共享,即使是同一个页面;localStorage 在所有同源窗口中都是共享的;cookie也是在所有同源窗口中都是共享的。Web Storage 支持事件通知机制,可以将数据更新的通知发送给监听者。Web Storage 的 api 接口使用更方便。

Share

golang的container/heap包详解: https://ieevee.com/tech/2018/01/29/go-heap.html

在leetcode的刷题过程中,遇到优先队列的问题,go的优先队列在container/heap包中,可以看下以上的链接。