本周ARTS
上周身体不适,休息了一周,下周补上
本周ARTS打卡内容:
- Algorithm 来源 LeetCode22
- Review 分享 用Go kit写微服务
- Tip 分享 cookie、 sessionStorage 、localStorage之间的区别和使用
- 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
7G(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
40func 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
10package 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
26type 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
68package 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
55package 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文本。对于statusRequest
与getRequest
,由于其不会往服务端发送内容,所以为空。对于validateRequest
,我们将传递一个日期来判断是否合法,所以其包含日期字段。响应也是同样的。
在第二部分,我们编写“解码器”来处理请求,告诉服务该怎么处理请求,怎么转换成正确的结构。get
和status
是空的,因为它们已完成。
最后,我们解码响应输出,即给定一个对象,返回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
90package 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
42package 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
47package 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
11curl 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 | <script type="text/javascript"> |
sessionStorage - 针对一个 session 的数据存储,当用户关闭浏览器窗口后,数据会被删除。
创建并访问一个 sessionStorage:
1 | <script type="text/javascript"> |
- 下面的例子对用户在当前 session 中访问页面的次数进行计数:
1 | <script type="text/javascript"> |
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包中,可以看下以上的链接。