From 1393330fbd028e867f3cfdbc58ffd20e577ff862 Mon Sep 17 00:00:00 2001 From: Tao <1042911716@qq.com> Date: Sat, 30 Nov 2019 11:42:05 +0800 Subject: [PATCH 001/122] =?UTF-8?q?=E5=A4=84=E7=90=86group=E5=B5=8C?= =?UTF-8?q?=E5=A5=97=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit r1 := r.Group("/v1") _ = r1.Group("/v2") --- day4-group/gee/gee.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/day4-group/gee/gee.go b/day4-group/gee/gee.go index 8c5d374..0d7a5ba 100644 --- a/day4-group/gee/gee.go +++ b/day4-group/gee/gee.go @@ -45,7 +45,7 @@ func (group *RouterGroup) Group(prefix string) *RouterGroup { } func (group *RouterGroup) addRoute(method string, comp string, handler HandlerFunc) { - pattern := group.prefix + comp + pattern := getNestPrefix(group.parent, group.prefix) + comp group.engine.router.addRoute(method, pattern, handler) } @@ -68,3 +68,11 @@ func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { c := newContext(w, req) engine.router.handle(c) } + +func getNestPrefix(group *RouterGroup, p string) string { + p = strings.Join([]string{group.prefix, p}, "") + if group.parent == nil { + return p + } + return getNestPrefix(group.parent, p) +} From 04012562ce4a8be9157fa0e0b73fb56169df87fa Mon Sep 17 00:00:00 2001 From: Tao <1042911716@qq.com> Date: Sat, 30 Nov 2019 11:51:05 +0800 Subject: [PATCH 002/122] add test func --- day4-group/gee/router_test.go | 38 ++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/day4-group/gee/router_test.go b/day4-group/gee/router_test.go index f5d6da3..a4a6611 100644 --- a/day4-group/gee/router_test.go +++ b/day4-group/gee/router_test.go @@ -17,11 +17,20 @@ func newTestRouter() *router { } func TestParsePattern(t *testing.T) { - ok := reflect.DeepEqual(parsePattern("/p/:name"), []string{"p", ":name"}) - ok = ok && reflect.DeepEqual(parsePattern("/p/*"), []string{"p", "*"}) - ok = ok && reflect.DeepEqual(parsePattern("/p/*name/*"), []string{"p", "*name"}) - if !ok { - t.Fatal("test parsePattern failed") + testCases := [][]string{ + parsePattern("/p/:name"), + parsePattern("/p/*"), + parsePattern("/p/*name/*"), + } + wants := [][]string{ + []string{"p", ":name"}, + []string{"p", "*"}, + []string{"p", "*name"}, + } + for index, result := range testCases { + if reflect.DeepEqual(result, wants[index]) { + t.Fatal("test parsePattern failed") + } } } @@ -72,3 +81,22 @@ func TestGetRoutes(t *testing.T) { t.Fatal("the number of routes shoule be 4") } } + +func TestNestingGroup(t *testing.T) { + r := &RouterGroup{ + prefix: "/v1", + middleWares: nil, + engine: nil, + parent: nil, + } + r2 := &RouterGroup{ + prefix: "/v2", + middleWares: nil, + engine: nil, + parent: r, + } + res := getNestPrefix(r2, "/hello") + if res != "/v1/v2/hello" { + t.Fatal("match failed") + } +} From 4db622e2355e36814e963777da2e90a272e9ae85 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Thu, 9 Jan 2020 00:27:57 +0800 Subject: [PATCH 003/122] support group prefix nesting and add Group unit tests --- day1-http-base/base3/gee/gee.go | 2 ++ day2-context/gee/gee.go | 2 ++ day3-router/gee/gee.go | 2 ++ day4-group/gee/gee.go | 23 ++++++++++-------- day4-group/gee/gee_test.go | 32 +++++++++++++++++++++++++ day4-group/gee/router_test.go | 38 ++++-------------------------- day5-middleware/gee/gee.go | 16 ++++++++++--- day5-middleware/gee/gee_test.go | 32 +++++++++++++++++++++++++ day6-template/gee/gee.go | 16 ++++++++++--- day6-template/gee/gee_test.go | 32 +++++++++++++++++++++++++ day7-panic-recover/gee/gee.go | 16 ++++++++++--- day7-panic-recover/gee/gee_test.go | 32 +++++++++++++++++++++++++ 12 files changed, 191 insertions(+), 52 deletions(-) create mode 100644 day4-group/gee/gee_test.go create mode 100644 day5-middleware/gee/gee_test.go create mode 100644 day6-template/gee/gee_test.go create mode 100644 day7-panic-recover/gee/gee_test.go diff --git a/day1-http-base/base3/gee/gee.go b/day1-http-base/base3/gee/gee.go index 8c38b4c..3aefd75 100644 --- a/day1-http-base/base3/gee/gee.go +++ b/day1-http-base/base3/gee/gee.go @@ -2,6 +2,7 @@ package gee import ( "fmt" + "log" "net/http" ) @@ -20,6 +21,7 @@ func New() *Engine { func (engine *Engine) addRoute(method string, pattern string, handler HandlerFunc) { key := method + "-" + pattern + log.Printf("Route %4s - %s", method, pattern) engine.router[key] = handler } diff --git a/day2-context/gee/gee.go b/day2-context/gee/gee.go index ce0d4da..802bb5e 100644 --- a/day2-context/gee/gee.go +++ b/day2-context/gee/gee.go @@ -1,6 +1,7 @@ package gee import ( + "log" "net/http" ) @@ -18,6 +19,7 @@ func New() *Engine { } func (engine *Engine) addRoute(method string, pattern string, handler HandlerFunc) { + log.Printf("Route %4s - %s", method, pattern) engine.router.addRoute(method, pattern, handler) } diff --git a/day3-router/gee/gee.go b/day3-router/gee/gee.go index ce0d4da..802bb5e 100644 --- a/day3-router/gee/gee.go +++ b/day3-router/gee/gee.go @@ -1,6 +1,7 @@ package gee import ( + "log" "net/http" ) @@ -18,6 +19,7 @@ func New() *Engine { } func (engine *Engine) addRoute(method string, pattern string, handler HandlerFunc) { + log.Printf("Route %4s - %s", method, pattern) engine.router.addRoute(method, pattern, handler) } diff --git a/day4-group/gee/gee.go b/day4-group/gee/gee.go index 0d7a5ba..604e5be 100644 --- a/day4-group/gee/gee.go +++ b/day4-group/gee/gee.go @@ -1,6 +1,7 @@ package gee import ( + "log" "net/http" ) @@ -36,7 +37,7 @@ func New() *Engine { func (group *RouterGroup) Group(prefix string) *RouterGroup { engine := group.engine newGroup := &RouterGroup{ - prefix: group.prefix + prefix, + prefix: prefix, parent: group, engine: engine, } @@ -45,10 +46,20 @@ func (group *RouterGroup) Group(prefix string) *RouterGroup { } func (group *RouterGroup) addRoute(method string, comp string, handler HandlerFunc) { - pattern := getNestPrefix(group.parent, group.prefix) + comp + pattern := group.getNestPrefix() + comp + log.Printf("Route %4s - %s", method, pattern) group.engine.router.addRoute(method, pattern, handler) } +// Support group nesting +func (group *RouterGroup) getNestPrefix() string { + p := group.prefix + if group.parent == nil { + return p + } + return group.parent.getNestPrefix() + p +} + // GET defines the method to add GET request func (group *RouterGroup) GET(pattern string, handler HandlerFunc) { group.addRoute("GET", pattern, handler) @@ -68,11 +79,3 @@ func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { c := newContext(w, req) engine.router.handle(c) } - -func getNestPrefix(group *RouterGroup, p string) string { - p = strings.Join([]string{group.prefix, p}, "") - if group.parent == nil { - return p - } - return getNestPrefix(group.parent, p) -} diff --git a/day4-group/gee/gee_test.go b/day4-group/gee/gee_test.go new file mode 100644 index 0000000..8c03dc1 --- /dev/null +++ b/day4-group/gee/gee_test.go @@ -0,0 +1,32 @@ +package gee + +import "testing" + +func TestNestingGroup(t *testing.T) { + v1 := &RouterGroup{ + prefix: "/v1", + } + v2 := &RouterGroup{ + prefix: "/v2", + parent: v1, + } + v3 := &RouterGroup{ + prefix: "/v3", + parent: v2, + } + if v2.getNestPrefix() != "/v1/v2" { + t.Fatal("v2 prefix should be /v1/v2") + } + if v3.getNestPrefix() != "/v1/v2/v3" { + t.Fatal("v3 prefix should be /v1/v2/v3") + } +} + +func TestGroup(t *testing.T) { + r := New() + v1 := r.Group("/v1") + v2 := v1.Group("/v2") + if v2.getNestPrefix() != "/v1/v2" { + t.Fatal("v2 prefix should be /v1/v2") + } +} diff --git a/day4-group/gee/router_test.go b/day4-group/gee/router_test.go index a4a6611..f5d6da3 100644 --- a/day4-group/gee/router_test.go +++ b/day4-group/gee/router_test.go @@ -17,20 +17,11 @@ func newTestRouter() *router { } func TestParsePattern(t *testing.T) { - testCases := [][]string{ - parsePattern("/p/:name"), - parsePattern("/p/*"), - parsePattern("/p/*name/*"), - } - wants := [][]string{ - []string{"p", ":name"}, - []string{"p", "*"}, - []string{"p", "*name"}, - } - for index, result := range testCases { - if reflect.DeepEqual(result, wants[index]) { - t.Fatal("test parsePattern failed") - } + ok := reflect.DeepEqual(parsePattern("/p/:name"), []string{"p", ":name"}) + ok = ok && reflect.DeepEqual(parsePattern("/p/*"), []string{"p", "*"}) + ok = ok && reflect.DeepEqual(parsePattern("/p/*name/*"), []string{"p", "*name"}) + if !ok { + t.Fatal("test parsePattern failed") } } @@ -81,22 +72,3 @@ func TestGetRoutes(t *testing.T) { t.Fatal("the number of routes shoule be 4") } } - -func TestNestingGroup(t *testing.T) { - r := &RouterGroup{ - prefix: "/v1", - middleWares: nil, - engine: nil, - parent: nil, - } - r2 := &RouterGroup{ - prefix: "/v2", - middleWares: nil, - engine: nil, - parent: r, - } - res := getNestPrefix(r2, "/hello") - if res != "/v1/v2/hello" { - t.Fatal("match failed") - } -} diff --git a/day5-middleware/gee/gee.go b/day5-middleware/gee/gee.go index e880d19..7ea237b 100644 --- a/day5-middleware/gee/gee.go +++ b/day5-middleware/gee/gee.go @@ -1,6 +1,7 @@ package gee import ( + "log" "net/http" "strings" ) @@ -37,7 +38,7 @@ func New() *Engine { func (group *RouterGroup) Group(prefix string) *RouterGroup { engine := group.engine newGroup := &RouterGroup{ - prefix: group.prefix + prefix, + prefix: prefix, parent: group, engine: engine, } @@ -51,11 +52,20 @@ func (group *RouterGroup) Use(middlewares ...HandlerFunc) { } func (group *RouterGroup) addRoute(method string, comp string, handler HandlerFunc) { - pattern := group.prefix + comp - + pattern := group.getNestPrefix() + comp + log.Printf("Route %4s - %s", method, pattern) group.engine.router.addRoute(method, pattern, handler) } +// Support group nesting +func (group *RouterGroup) getNestPrefix() string { + p := group.prefix + if group.parent == nil { + return p + } + return group.parent.getNestPrefix() + p +} + // GET defines the method to add GET request func (group *RouterGroup) GET(pattern string, handler HandlerFunc) { group.addRoute("GET", pattern, handler) diff --git a/day5-middleware/gee/gee_test.go b/day5-middleware/gee/gee_test.go new file mode 100644 index 0000000..8c03dc1 --- /dev/null +++ b/day5-middleware/gee/gee_test.go @@ -0,0 +1,32 @@ +package gee + +import "testing" + +func TestNestingGroup(t *testing.T) { + v1 := &RouterGroup{ + prefix: "/v1", + } + v2 := &RouterGroup{ + prefix: "/v2", + parent: v1, + } + v3 := &RouterGroup{ + prefix: "/v3", + parent: v2, + } + if v2.getNestPrefix() != "/v1/v2" { + t.Fatal("v2 prefix should be /v1/v2") + } + if v3.getNestPrefix() != "/v1/v2/v3" { + t.Fatal("v3 prefix should be /v1/v2/v3") + } +} + +func TestGroup(t *testing.T) { + r := New() + v1 := r.Group("/v1") + v2 := v1.Group("/v2") + if v2.getNestPrefix() != "/v1/v2" { + t.Fatal("v2 prefix should be /v1/v2") + } +} diff --git a/day6-template/gee/gee.go b/day6-template/gee/gee.go index 357f6e9..6a56722 100644 --- a/day6-template/gee/gee.go +++ b/day6-template/gee/gee.go @@ -2,6 +2,7 @@ package gee import ( "html/template" + "log" "net/http" "path" "strings" @@ -41,7 +42,7 @@ func New() *Engine { func (group *RouterGroup) Group(prefix string) *RouterGroup { engine := group.engine newGroup := &RouterGroup{ - prefix: group.prefix + prefix, + prefix: prefix, parent: group, engine: engine, } @@ -55,11 +56,20 @@ func (group *RouterGroup) Use(middlewares ...HandlerFunc) { } func (group *RouterGroup) addRoute(method string, comp string, handler HandlerFunc) { - pattern := group.prefix + comp - + pattern := group.getNestPrefix() + comp + log.Printf("Route %4s - %s", method, pattern) group.engine.router.addRoute(method, pattern, handler) } +// Support group nesting +func (group *RouterGroup) getNestPrefix() string { + p := group.prefix + if group.parent == nil { + return p + } + return group.parent.getNestPrefix() + p +} + // GET defines the method to add GET request func (group *RouterGroup) GET(pattern string, handler HandlerFunc) { group.addRoute("GET", pattern, handler) diff --git a/day6-template/gee/gee_test.go b/day6-template/gee/gee_test.go new file mode 100644 index 0000000..8c03dc1 --- /dev/null +++ b/day6-template/gee/gee_test.go @@ -0,0 +1,32 @@ +package gee + +import "testing" + +func TestNestingGroup(t *testing.T) { + v1 := &RouterGroup{ + prefix: "/v1", + } + v2 := &RouterGroup{ + prefix: "/v2", + parent: v1, + } + v3 := &RouterGroup{ + prefix: "/v3", + parent: v2, + } + if v2.getNestPrefix() != "/v1/v2" { + t.Fatal("v2 prefix should be /v1/v2") + } + if v3.getNestPrefix() != "/v1/v2/v3" { + t.Fatal("v3 prefix should be /v1/v2/v3") + } +} + +func TestGroup(t *testing.T) { + r := New() + v1 := r.Group("/v1") + v2 := v1.Group("/v2") + if v2.getNestPrefix() != "/v1/v2" { + t.Fatal("v2 prefix should be /v1/v2") + } +} diff --git a/day7-panic-recover/gee/gee.go b/day7-panic-recover/gee/gee.go index 9eeb356..4dde8de 100644 --- a/day7-panic-recover/gee/gee.go +++ b/day7-panic-recover/gee/gee.go @@ -2,6 +2,7 @@ package gee import ( "html/template" + "log" "net/http" "path" "strings" @@ -48,7 +49,7 @@ func Default() *Engine { func (group *RouterGroup) Group(prefix string) *RouterGroup { engine := group.engine newGroup := &RouterGroup{ - prefix: group.prefix + prefix, + prefix: prefix, parent: group, engine: engine, } @@ -62,11 +63,20 @@ func (group *RouterGroup) Use(middlewares ...HandlerFunc) { } func (group *RouterGroup) addRoute(method string, comp string, handler HandlerFunc) { - pattern := group.prefix + comp - + pattern := group.getNestPrefix() + comp + log.Printf("Route %4s - %s", method, pattern) group.engine.router.addRoute(method, pattern, handler) } +// Support group nesting +func (group *RouterGroup) getNestPrefix() string { + p := group.prefix + if group.parent == nil { + return p + } + return group.parent.getNestPrefix() + p +} + // GET defines the method to add GET request func (group *RouterGroup) GET(pattern string, handler HandlerFunc) { group.addRoute("GET", pattern, handler) diff --git a/day7-panic-recover/gee/gee_test.go b/day7-panic-recover/gee/gee_test.go new file mode 100644 index 0000000..8c03dc1 --- /dev/null +++ b/day7-panic-recover/gee/gee_test.go @@ -0,0 +1,32 @@ +package gee + +import "testing" + +func TestNestingGroup(t *testing.T) { + v1 := &RouterGroup{ + prefix: "/v1", + } + v2 := &RouterGroup{ + prefix: "/v2", + parent: v1, + } + v3 := &RouterGroup{ + prefix: "/v3", + parent: v2, + } + if v2.getNestPrefix() != "/v1/v2" { + t.Fatal("v2 prefix should be /v1/v2") + } + if v3.getNestPrefix() != "/v1/v2/v3" { + t.Fatal("v3 prefix should be /v1/v2/v3") + } +} + +func TestGroup(t *testing.T) { + r := New() + v1 := r.Group("/v1") + v2 := v1.Group("/v2") + if v2.getNestPrefix() != "/v1/v2" { + t.Fatal("v2 prefix should be /v1/v2") + } +} From 64703edcc1e2af90693001fa69dcb33e8eb1ba4f Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Thu, 9 Jan 2020 00:38:50 +0800 Subject: [PATCH 004/122] support group nesting --- day4-group/gee/gee.go | 13 ++----------- day4-group/gee/gee_test.go | 28 ++++++---------------------- day5-middleware/gee/gee.go | 13 ++----------- day5-middleware/gee/gee_test.go | 28 ++++++---------------------- day6-template/gee/gee.go | 13 ++----------- day6-template/gee/gee_test.go | 28 ++++++---------------------- day7-panic-recover/gee/gee.go | 13 ++----------- day7-panic-recover/gee/gee_test.go | 28 ++++++---------------------- 8 files changed, 32 insertions(+), 132 deletions(-) diff --git a/day4-group/gee/gee.go b/day4-group/gee/gee.go index 604e5be..1e5cda3 100644 --- a/day4-group/gee/gee.go +++ b/day4-group/gee/gee.go @@ -37,7 +37,7 @@ func New() *Engine { func (group *RouterGroup) Group(prefix string) *RouterGroup { engine := group.engine newGroup := &RouterGroup{ - prefix: prefix, + prefix: group.prefix + prefix, parent: group, engine: engine, } @@ -46,20 +46,11 @@ func (group *RouterGroup) Group(prefix string) *RouterGroup { } func (group *RouterGroup) addRoute(method string, comp string, handler HandlerFunc) { - pattern := group.getNestPrefix() + comp + pattern := group.prefix + comp log.Printf("Route %4s - %s", method, pattern) group.engine.router.addRoute(method, pattern, handler) } -// Support group nesting -func (group *RouterGroup) getNestPrefix() string { - p := group.prefix - if group.parent == nil { - return p - } - return group.parent.getNestPrefix() + p -} - // GET defines the method to add GET request func (group *RouterGroup) GET(pattern string, handler HandlerFunc) { group.addRoute("GET", pattern, handler) diff --git a/day4-group/gee/gee_test.go b/day4-group/gee/gee_test.go index 8c03dc1..f0c9577 100644 --- a/day4-group/gee/gee_test.go +++ b/day4-group/gee/gee_test.go @@ -2,31 +2,15 @@ package gee import "testing" -func TestNestingGroup(t *testing.T) { - v1 := &RouterGroup{ - prefix: "/v1", - } - v2 := &RouterGroup{ - prefix: "/v2", - parent: v1, - } - v3 := &RouterGroup{ - prefix: "/v3", - parent: v2, - } - if v2.getNestPrefix() != "/v1/v2" { - t.Fatal("v2 prefix should be /v1/v2") - } - if v3.getNestPrefix() != "/v1/v2/v3" { - t.Fatal("v3 prefix should be /v1/v2/v3") - } -} - -func TestGroup(t *testing.T) { +func TestNestedGroup(t *testing.T) { r := New() v1 := r.Group("/v1") v2 := v1.Group("/v2") - if v2.getNestPrefix() != "/v1/v2" { + v3 := v2.Group("/v3") + if v2.prefix != "/v1/v2" { + t.Fatal("v2 prefix should be /v1/v2") + } + if v3.prefix != "/v1/v2/v3" { t.Fatal("v2 prefix should be /v1/v2") } } diff --git a/day5-middleware/gee/gee.go b/day5-middleware/gee/gee.go index 7ea237b..5c237c1 100644 --- a/day5-middleware/gee/gee.go +++ b/day5-middleware/gee/gee.go @@ -38,7 +38,7 @@ func New() *Engine { func (group *RouterGroup) Group(prefix string) *RouterGroup { engine := group.engine newGroup := &RouterGroup{ - prefix: prefix, + prefix: group.prefix + prefix, parent: group, engine: engine, } @@ -52,20 +52,11 @@ func (group *RouterGroup) Use(middlewares ...HandlerFunc) { } func (group *RouterGroup) addRoute(method string, comp string, handler HandlerFunc) { - pattern := group.getNestPrefix() + comp + pattern := group.prefix + comp log.Printf("Route %4s - %s", method, pattern) group.engine.router.addRoute(method, pattern, handler) } -// Support group nesting -func (group *RouterGroup) getNestPrefix() string { - p := group.prefix - if group.parent == nil { - return p - } - return group.parent.getNestPrefix() + p -} - // GET defines the method to add GET request func (group *RouterGroup) GET(pattern string, handler HandlerFunc) { group.addRoute("GET", pattern, handler) diff --git a/day5-middleware/gee/gee_test.go b/day5-middleware/gee/gee_test.go index 8c03dc1..f0c9577 100644 --- a/day5-middleware/gee/gee_test.go +++ b/day5-middleware/gee/gee_test.go @@ -2,31 +2,15 @@ package gee import "testing" -func TestNestingGroup(t *testing.T) { - v1 := &RouterGroup{ - prefix: "/v1", - } - v2 := &RouterGroup{ - prefix: "/v2", - parent: v1, - } - v3 := &RouterGroup{ - prefix: "/v3", - parent: v2, - } - if v2.getNestPrefix() != "/v1/v2" { - t.Fatal("v2 prefix should be /v1/v2") - } - if v3.getNestPrefix() != "/v1/v2/v3" { - t.Fatal("v3 prefix should be /v1/v2/v3") - } -} - -func TestGroup(t *testing.T) { +func TestNestedGroup(t *testing.T) { r := New() v1 := r.Group("/v1") v2 := v1.Group("/v2") - if v2.getNestPrefix() != "/v1/v2" { + v3 := v2.Group("/v3") + if v2.prefix != "/v1/v2" { + t.Fatal("v2 prefix should be /v1/v2") + } + if v3.prefix != "/v1/v2/v3" { t.Fatal("v2 prefix should be /v1/v2") } } diff --git a/day6-template/gee/gee.go b/day6-template/gee/gee.go index 6a56722..d09404c 100644 --- a/day6-template/gee/gee.go +++ b/day6-template/gee/gee.go @@ -42,7 +42,7 @@ func New() *Engine { func (group *RouterGroup) Group(prefix string) *RouterGroup { engine := group.engine newGroup := &RouterGroup{ - prefix: prefix, + prefix: group.prefix + prefix, parent: group, engine: engine, } @@ -56,20 +56,11 @@ func (group *RouterGroup) Use(middlewares ...HandlerFunc) { } func (group *RouterGroup) addRoute(method string, comp string, handler HandlerFunc) { - pattern := group.getNestPrefix() + comp + pattern := group.prefix + comp log.Printf("Route %4s - %s", method, pattern) group.engine.router.addRoute(method, pattern, handler) } -// Support group nesting -func (group *RouterGroup) getNestPrefix() string { - p := group.prefix - if group.parent == nil { - return p - } - return group.parent.getNestPrefix() + p -} - // GET defines the method to add GET request func (group *RouterGroup) GET(pattern string, handler HandlerFunc) { group.addRoute("GET", pattern, handler) diff --git a/day6-template/gee/gee_test.go b/day6-template/gee/gee_test.go index 8c03dc1..f0c9577 100644 --- a/day6-template/gee/gee_test.go +++ b/day6-template/gee/gee_test.go @@ -2,31 +2,15 @@ package gee import "testing" -func TestNestingGroup(t *testing.T) { - v1 := &RouterGroup{ - prefix: "/v1", - } - v2 := &RouterGroup{ - prefix: "/v2", - parent: v1, - } - v3 := &RouterGroup{ - prefix: "/v3", - parent: v2, - } - if v2.getNestPrefix() != "/v1/v2" { - t.Fatal("v2 prefix should be /v1/v2") - } - if v3.getNestPrefix() != "/v1/v2/v3" { - t.Fatal("v3 prefix should be /v1/v2/v3") - } -} - -func TestGroup(t *testing.T) { +func TestNestedGroup(t *testing.T) { r := New() v1 := r.Group("/v1") v2 := v1.Group("/v2") - if v2.getNestPrefix() != "/v1/v2" { + v3 := v2.Group("/v3") + if v2.prefix != "/v1/v2" { + t.Fatal("v2 prefix should be /v1/v2") + } + if v3.prefix != "/v1/v2/v3" { t.Fatal("v2 prefix should be /v1/v2") } } diff --git a/day7-panic-recover/gee/gee.go b/day7-panic-recover/gee/gee.go index 4dde8de..f0dd8fd 100644 --- a/day7-panic-recover/gee/gee.go +++ b/day7-panic-recover/gee/gee.go @@ -49,7 +49,7 @@ func Default() *Engine { func (group *RouterGroup) Group(prefix string) *RouterGroup { engine := group.engine newGroup := &RouterGroup{ - prefix: prefix, + prefix: group.prefix + prefix, parent: group, engine: engine, } @@ -63,20 +63,11 @@ func (group *RouterGroup) Use(middlewares ...HandlerFunc) { } func (group *RouterGroup) addRoute(method string, comp string, handler HandlerFunc) { - pattern := group.getNestPrefix() + comp + pattern := group.prefix + comp log.Printf("Route %4s - %s", method, pattern) group.engine.router.addRoute(method, pattern, handler) } -// Support group nesting -func (group *RouterGroup) getNestPrefix() string { - p := group.prefix - if group.parent == nil { - return p - } - return group.parent.getNestPrefix() + p -} - // GET defines the method to add GET request func (group *RouterGroup) GET(pattern string, handler HandlerFunc) { group.addRoute("GET", pattern, handler) diff --git a/day7-panic-recover/gee/gee_test.go b/day7-panic-recover/gee/gee_test.go index 8c03dc1..f0c9577 100644 --- a/day7-panic-recover/gee/gee_test.go +++ b/day7-panic-recover/gee/gee_test.go @@ -2,31 +2,15 @@ package gee import "testing" -func TestNestingGroup(t *testing.T) { - v1 := &RouterGroup{ - prefix: "/v1", - } - v2 := &RouterGroup{ - prefix: "/v2", - parent: v1, - } - v3 := &RouterGroup{ - prefix: "/v3", - parent: v2, - } - if v2.getNestPrefix() != "/v1/v2" { - t.Fatal("v2 prefix should be /v1/v2") - } - if v3.getNestPrefix() != "/v1/v2/v3" { - t.Fatal("v3 prefix should be /v1/v2/v3") - } -} - -func TestGroup(t *testing.T) { +func TestNestedGroup(t *testing.T) { r := New() v1 := r.Group("/v1") v2 := v1.Group("/v2") - if v2.getNestPrefix() != "/v1/v2" { + v3 := v2.Group("/v3") + if v2.prefix != "/v1/v2" { + t.Fatal("v2 prefix should be /v1/v2") + } + if v3.prefix != "/v1/v2/v3" { t.Fatal("v2 prefix should be /v1/v2") } } From 0c492207eb6e7382e30890e96e076abd49532615 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Thu, 9 Jan 2020 01:04:24 +0800 Subject: [PATCH 005/122] add day7 doc --- README.md | 2 +- day7-panic-recover/main.go | 37 +++-- doc/gee-day2.md | 1 + doc/gee-day4.md | 1 + doc/gee-day7.md | 289 +++++++++++++++++++++++++++++++++++++ doc/gee-day7/go-panic.png | Bin 0 -> 3861 bytes doc/gee.md | 2 +- 7 files changed, 314 insertions(+), 18 deletions(-) create mode 100644 doc/gee-day7.md create mode 100644 doc/gee-day7/go-panic.png diff --git a/README.md b/README.md index 5dee09d..93e00b2 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Gee 的设计与实现参考了Gin,这个教程可以快速入门:[Go Gin简 - [第四天:分组控制(Group)](https://geektutu.com/post/gee-day4.html),[Code - Github](day4-group) - [第五天:中间件(Middleware)](https://geektutu.com/post/gee-day5.html),[Code - Github](day5-middleware) - [第六天:HTML模板(Template)](https://geektutu.com/post/gee-day6.html),[Code - Github](day6-template) -- 第七天:错误恢复(Panic Recover),[Code - Github](day7-panic-recover) +- [第七天:错误恢复(Panic Recover)](https://geektutu.com/post/gee-day7.html),[Code - Github](day7-panic-recover) ## Day 1 - Static Route diff --git a/day7-panic-recover/main.go b/day7-panic-recover/main.go index 5021aa6..0444ea8 100644 --- a/day7-panic-recover/main.go +++ b/day7-panic-recover/main.go @@ -5,26 +5,31 @@ $ curl "http://localhost:9999" Hello Geektutu $ curl "http://localhost:9999/panic" {"message":"Internal Server Error"} +$ curl "http://localhost:9999" +Hello Geektutu >>> log -2019/08/18 17:55:57 [200] / in 4.533µs -2019/08/18 17:55:58 runtime error: index out of range +2020/01/09 01:00:10 Route GET - / +2020/01/09 01:00:10 Route GET - /panic +2020/01/09 01:00:22 [200] / in 25.364µs +2020/01/09 01:00:32 runtime error: index out of range Traceback: - /usr/local/Cellar/go/1.12.5/libexec/src/runtime/panic.go:523 - /usr/local/Cellar/go/1.12.5/libexec/src/runtime/panic.go:44 - /Users/geektutu/7days-golang/day7-panic-recover/main.go:20 - /Users/geektutu/7days-golang/day7-panic-recover/gee/context.go:41 - /Users/geektutu/7days-golang/day7-panic-recover/gee/recovery.go:37 - /Users/geektutu/7days-golang/day7-panic-recover/gee/context.go:41 - /Users/geektutu/7days-golang/day7-panic-recover/gee/logger.go:15 - /Users/geektutu/7days-golang/day7-panic-recover/gee/context.go:41 - /Users/geektutu/7days-golang/day7-panic-recover/gee/router.go:99 - /Users/geektutu/7days-golang/day7-panic-recover/gee/gee.go:129 - /usr/local/Cellar/go/1.12.5/libexec/src/net/http/server.go:2775 - /usr/local/Cellar/go/1.12.5/libexec/src/net/http/server.go:1879 - /usr/local/Cellar/go/1.12.5/libexec/src/runtime/asm_amd64.s:1338 + /usr/local/Cellar/go/1.12.5/libexec/src/runtime/panic.go:523 + /usr/local/Cellar/go/1.12.5/libexec/src/runtime/panic.go:44 + /Users/gzdaijie/Github/blog/geektutu-blog/posts/7days-golang/day7-panic-recover/main.go:47 + /Users/gzdaijie/Github/blog/geektutu-blog/posts/7days-golang/day7-panic-recover/gee/context.go:41 + /Users/gzdaijie/Github/blog/geektutu-blog/posts/7days-golang/day7-panic-recover/gee/recovery.go:37 + /Users/gzdaijie/Github/blog/geektutu-blog/posts/7days-golang/day7-panic-recover/gee/context.go:41 + /Users/gzdaijie/Github/blog/geektutu-blog/posts/7days-golang/day7-panic-recover/gee/logger.go:15 + /Users/gzdaijie/Github/blog/geektutu-blog/posts/7days-golang/day7-panic-recover/gee/context.go:41 + /Users/gzdaijie/Github/blog/geektutu-blog/posts/7days-golang/day7-panic-recover/gee/router.go:99 + /Users/gzdaijie/Github/blog/geektutu-blog/posts/7days-golang/day7-panic-recover/gee/gee.go:130 + /usr/local/Cellar/go/1.12.5/libexec/src/net/http/server.go:2775 + /usr/local/Cellar/go/1.12.5/libexec/src/net/http/server.go:1879 + /usr/local/Cellar/go/1.12.5/libexec/src/runtime/asm_amd64.s:1338 -2019/08/18 17:55:58 [500] /panic in 143.086µs +2020/01/09 01:00:32 [500] /panic in 395.846µs +2020/01/09 01:00:38 [200] / in 6.985µs */ import ( diff --git a/doc/gee-day2.md b/doc/gee-day2.md index e4b5f89..b7cf9d2 100644 --- a/doc/gee-day2.md +++ b/doc/gee-day2.md @@ -180,6 +180,7 @@ func newRouter() *router { } func (r *router) addRoute(method string, pattern string, handler HandlerFunc) { + log.Printf("Route %4s - %s", method, pattern) key := method + "-" + pattern r.handlers[key] = handler } diff --git a/doc/gee-day4.md b/doc/gee-day4.md index c6263f0..f6fb64a 100644 --- a/doc/gee-day4.md +++ b/doc/gee-day4.md @@ -96,6 +96,7 @@ func (group *RouterGroup) Group(prefix string) *RouterGroup { func (group *RouterGroup) addRoute(method string, comp string, handler HandlerFunc) { pattern := group.prefix + comp + log.Printf("Route %4s - %s", method, pattern) group.engine.router.addRoute(method, pattern, handler) } diff --git a/doc/gee-day7.md b/doc/gee-day7.md new file mode 100644 index 0000000..a9efa78 --- /dev/null +++ b/doc/gee-day7.md @@ -0,0 +1,289 @@ +--- +title: Go语言动手写Web框架 - Gee第六天 错误恢复(Panic Recover) +date: 2020-01-09 01:00:00 +description: 7天用 Go语言 从零实现Web框架教程(7 days implement golang web framework from scratch tutorial),用 Go语言/golang 动手写Web框架,从零实现一个Web框架,以 Gin 为原型从零设计一个Web框架。本文介绍了如何为Web框架增加错误处理机制。 +tags: +- Go +categories: +- 从零实现 +keywords: +- Go语言 +- 从零实现Web框架 +- 动手写Web框架 +- Panic +- Recover +image: post/gee-day7/go-panic.png +github: https://github.com/geektutu/7days-golang +--- + +本文是[7天用Go从零实现Web框架Gee教程系列](https://geektutu.com/post/gee.html)的第七篇。 + +- 实现错误处理机制。 + +## panic + +Go 语言中,比较常见的错误处理方法是返回 error,由调用者决定后续如何处理。但是如果是无法恢复的错误,可以手动触发 panic,当然如果在程序运行过程中出现了类似于数组越界的错误,panic 也会被触发。panic 会中止当前执行的程序,退出。 + +下面是主动触发的例子: + +```go +// hello.go +func main() { + fmt.Println("before panic") + panic("crash") + fmt.Println("after panic") +} +``` + +```bash +$ go run hello.go + +before panic +panic: crash + +goroutine 1 [running]: +main.main() + ~/go_demo/hello/hello.go:7 +0x95 +exit status 2 +``` + +下面是数组越界触发的 panic + +```go +// hello.go +func main() { + arr := []int{1, 2, 3} + fmt.Println(arr[4]) +} +``` + +```bash +$ go run hello.go +panic: runtime error: index out of range [4] with length 3 +``` + +## defer + +panic 会导致程序被中止,但是在退出前,会先处理完当前协程上已经defer 的任务,执行完成后再退出。效果类似于 java 语言的 `try...catch`。 + +```go +// hello.go +func main() { + defer func() { + fmt.Println("defer func") + }() + + arr := []int{1, 2, 3} + fmt.Println(arr[4]) +} +``` + +```go +$ go run hello.go +defer func +panic: runtime error: index out of range [4] with length 3 +``` + +可以 defer 多个任务,在同一个函数中 defer 多个任务,会逆序执行。即先执行最后 defer 的任务。 + +在这里,defer 的任务执行完成之后,panic 还会继续被抛出,导致程序非正常结束。 + +## recover + +Go 语言还提供了 recover 函数,可以避免因为 panic 发生而导致整个程序终止,recover 函数只在 defer 中生效。 + +```go +// hello.go +func test_recover() { + defer func() { + fmt.Println("defer func") + if err := recover(); err != nil { + fmt.Println("recover success") + } + }() + + arr := []int{1, 2, 3} + fmt.Println(arr[4]) + fmt.Println("after panic") +} + +func main() { + test_recover() + fmt.Println("after recover") +} +``` + +```go +$ go run hello.go +defer func +recover success +after recover +``` + +我们可以看到,recover 捕获了 panic,程序正常结束。*test_recover()* 中的 *after panic* 没有打印,这是正确的,当 panic 被触发时,控制权就被交给了 defer 。就像在 java 中,`try`代码块中发生了异常,控制权交给了 `catch`,接下来执行 catch 代码块中的代码。而在 *main()* 中打印了 *after recover*,说明程序已经恢复正常,继续往下执行直到结束。 + +## Gee 的错误处理机制 + +对一个 Web 框架而言,错误处理机制是非常必要的。可能是框架本身没有完备的测试,导致在某些情况下出现空指针异常等情况。也有可能用户不正确的参数,触发了某些异常,例如数组越界,空指针等。如果因为这些原因导致系统宕机,必然是不可接受的。 + +我们在[第六天](https://geektutu.com/post/gee-day6.html)实现的框架并没有加入异常处理机制,如果代码中存在会触发 panic 的 BUG,很容易宕掉。 + +例如下面的代码: + +```go +func main() { + r := gee.New() + r.GET("/panic", func(c *gee.Context) { + names := []string{"geektutu"} + c.String(http.StatusOK, names[100]) + }) + r.Run(":9999") +} +``` +在上面的代码中,我们为 gee 注册了路由 `/panic`,而这个路由的处理函数内部存在数组越界 `names[100]`,如果访问 *localhost:9999/panic*,Web 服务就会宕掉。 + +今天,我们将在 gee 中添加一个非常简单的错误处理机制,即在此类错误发生时,向用户返回 *Internal Server Error*,并且在日志中打印必要的错误信息,方便进行错误定位。 + +我们之前实现了中间件机制,错误处理也可以作为一个中间件,增强 gee 框架的能力。 + +新增文件 **gee/recovery.go**,在这个文件中实现中间件 `Recovery`。 + +```go +func Recovery() HandlerFunc { + return func(c *Context) { + defer func() { + if err := recover(); err != nil { + message := fmt.Sprintf("%s", err) + log.Printf("%s\n\n", trace(message)) + c.Fail(http.StatusInternalServerError, "Internal Server Error") + } + }() + + c.Next() + } +} +``` + +`Recovery` 的实现非常简单,使用 defer 挂载上错误恢复的函数,在这个函数中调用 *recover()*,捕获 panic,并且将堆栈信息打印在日志中,向用户返回 *Internal Server Error*。 + +你可能注意到,这里有一个 *trace()* 函数,这个函数是用来获取触发 panic 的堆栈信息,完整代码如下: + +[day7-panic-recover/gee/recovery.go](https://github.com/geektutu/7days-golang/tree/master/day7-panic-recover) + +```go +package gee + +import ( + "fmt" + "log" + "net/http" + "runtime" + "strings" +) + +// print stack trace for debug +func trace(message string) string { + var pcs [32]uintptr + n := runtime.Callers(3, pcs[:]) // skip first 3 caller + + var str strings.Builder + str.WriteString(message + "\nTraceback:") + for _, pc := range pcs[:n] { + fn := runtime.FuncForPC(pc) + file, line := fn.FileLine(pc) + str.WriteString(fmt.Sprintf("\n\t%s:%d", file, line)) + } + return str.String() +} + +func Recovery() HandlerFunc { + return func(c *Context) { + defer func() { + if err := recover(); err != nil { + message := fmt.Sprintf("%s", err) + log.Printf("%s\n\n", trace(message)) + c.Fail(http.StatusInternalServerError, "Internal Server Error") + } + }() + + c.Next() + } +} +``` + +在 *trace()* 中,调用了 `runtime.Callers(3, pcs[:])`,Callers 用来返回调用栈的程序计数器, 第 0 个 Caller 是 Callers 本身,第 1 个是上一层 trace,第 2 个是再上一层的 `defer func`。因此,为了日志简洁一点,我们跳过了前 3 个 Caller。 + +接下来,通过 `runtime.FuncForPC(pc)` 获取对应的函数,在通过 `fn.FileLine(pc)` 获取到调用该函数的文件名和行号,打印在日志中。 + +至此,gee 框架的错误处理机制就完成了。 + +## 使用 Demo + +[day7-panic-recover/main.go](https://github.com/geektutu/7days-golang/tree/master/day7-panic-recover) + +```go +package main + +import ( + "net/http" + + "./gee" +) + +func main() { + r := gee.Default() + r.GET("/", func(c *gee.Context) { + c.String(http.StatusOK, "Hello Geektutu\n") + }) + // index out of range for testing Recovery() + r.GET("/panic", func(c *gee.Context) { + names := []string{"geektutu"} + c.String(http.StatusOK, names[100]) + }) + + r.Run(":9999") +} +``` + +接下来进行测试,先访问主页,访问一个有BUG的 `/panic`,服务正常返回。接下来我们再一次成功访问了主页,说明服务完全运转正常。 + +```bash +$ curl "http://localhost:9999" +Hello Geektutu +$ curl "http://localhost:9999/panic" +{"message":"Internal Server Error"} +$ curl "http://localhost:9999" +Hello Geektutu +``` + +我们可以在后台日志中看到如下内容,引发错误的原因和堆栈信息都被打印了出来,通过日志,我们可以很容易地知道,在*day7-panic-recover/main.go:20* 的地方出现了 `index out of range` 错误。 + +```bash +2020/01/09 01:00:10 Route GET - / +2020/01/09 01:00:10 Route GET - /panic +2020/01/09 01:00:22 [200] / in 25.364µs +2020/01/09 01:00:32 runtime error: index out of range +Traceback: + /usr/local/Cellar/go/1.12.5/libexec/src/runtime/panic.go:523 + /usr/local/Cellar/go/1.12.5/libexec/src/runtime/panic.go:44 + /Users/gzdaijie/Github/blog/geektutu-blog/posts/7days-golang/day7-panic-recover/main.go:47 + /Users/gzdaijie/Github/blog/geektutu-blog/posts/7days-golang/day7-panic-recover/gee/context.go:41 + /Users/gzdaijie/Github/blog/geektutu-blog/posts/7days-golang/day7-panic-recover/gee/recovery.go:37 + /Users/gzdaijie/Github/blog/geektutu-blog/posts/7days-golang/day7-panic-recover/gee/context.go:41 + /Users/gzdaijie/Github/blog/geektutu-blog/posts/7days-golang/day7-panic-recover/gee/logger.go:15 + /Users/gzdaijie/Github/blog/geektutu-blog/posts/7days-golang/day7-panic-recover/gee/context.go:41 + /Users/gzdaijie/Github/blog/geektutu-blog/posts/7days-golang/day7-panic-recover/gee/router.go:99 + /Users/gzdaijie/Github/blog/geektutu-blog/posts/7days-golang/day7-panic-recover/gee/gee.go:130 + /usr/local/Cellar/go/1.12.5/libexec/src/net/http/server.go:2775 + /usr/local/Cellar/go/1.12.5/libexec/src/net/http/server.go:1879 + /usr/local/Cellar/go/1.12.5/libexec/src/runtime/asm_amd64.s:1338 + +2020/01/09 01:00:32 [500] /panic in 395.846µs +2020/01/09 01:00:38 [200] / in 6.985µs +``` + +## 参考 + +- [Package runtime - golang.org](https://golang.org/pkg/runtime/) +- [Is it possible get information about caller function in Golang? - StackOverflow](https://stackoverflow.com/questions/35212985/is-it-possible-get-information-about-caller-function-in-golang) + diff --git a/doc/gee-day7/go-panic.png b/doc/gee-day7/go-panic.png new file mode 100644 index 0000000000000000000000000000000000000000..1682e755f7a399aa51d3ecb2e5a8010a734afbac GIT binary patch literal 3861 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 Xn((PN)Padr00000NkvXXu0mjf*t}Jg literal 0 HcmV?d00001 diff --git a/doc/gee.md b/doc/gee.md index 03f2c79..6db98b0 100644 --- a/doc/gee.md +++ b/doc/gee.md @@ -68,4 +68,4 @@ func handler(w http.ResponseWriter, r *http.Request) { - [第四天:分组控制(Group)](https://geektutu.com/post/gee-day4.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/day4-group) - [第五天:中间件(Middleware)](https://geektutu.com/post/gee-day5.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/day5-middleware) - [第六天:HTML模板(Template)](https://geektutu.com/post/gee-day6.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/day6-template) -- 第七天:错误恢复(Panic Recover),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/day7-panic-recover) \ No newline at end of file +- [第七天:错误恢复(Panic Recover)](https://geektutu.com/post/gee-day7.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/day7-panic-recover) \ No newline at end of file From 944309dffeedc11193cc64e9275d07acd2552afe Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Thu, 9 Jan 2020 01:42:26 +0800 Subject: [PATCH 006/122] modify log --- day7-panic-recover/main.go | 16 ++++++++-------- doc/gee-day7.md | 18 +++++++++--------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/day7-panic-recover/main.go b/day7-panic-recover/main.go index 0444ea8..a342f66 100644 --- a/day7-panic-recover/main.go +++ b/day7-panic-recover/main.go @@ -16,14 +16,14 @@ Hello Geektutu Traceback: /usr/local/Cellar/go/1.12.5/libexec/src/runtime/panic.go:523 /usr/local/Cellar/go/1.12.5/libexec/src/runtime/panic.go:44 - /Users/gzdaijie/Github/blog/geektutu-blog/posts/7days-golang/day7-panic-recover/main.go:47 - /Users/gzdaijie/Github/blog/geektutu-blog/posts/7days-golang/day7-panic-recover/gee/context.go:41 - /Users/gzdaijie/Github/blog/geektutu-blog/posts/7days-golang/day7-panic-recover/gee/recovery.go:37 - /Users/gzdaijie/Github/blog/geektutu-blog/posts/7days-golang/day7-panic-recover/gee/context.go:41 - /Users/gzdaijie/Github/blog/geektutu-blog/posts/7days-golang/day7-panic-recover/gee/logger.go:15 - /Users/gzdaijie/Github/blog/geektutu-blog/posts/7days-golang/day7-panic-recover/gee/context.go:41 - /Users/gzdaijie/Github/blog/geektutu-blog/posts/7days-golang/day7-panic-recover/gee/router.go:99 - /Users/gzdaijie/Github/blog/geektutu-blog/posts/7days-golang/day7-panic-recover/gee/gee.go:130 + /Users/7days-golang/day7-panic-recover/main.go:47 + /Users/7days-golang/day7-panic-recover/gee/context.go:41 + /Users/7days-golang/day7-panic-recover/gee/recovery.go:37 + /Users/7days-golang/day7-panic-recover/gee/context.go:41 + /Users/7days-golang/day7-panic-recover/gee/logger.go:15 + /Users/7days-golang/day7-panic-recover/gee/context.go:41 + /Users/7days-golang/day7-panic-recover/gee/router.go:99 + /Users/7days-golang/day7-panic-recover/gee/gee.go:130 /usr/local/Cellar/go/1.12.5/libexec/src/net/http/server.go:2775 /usr/local/Cellar/go/1.12.5/libexec/src/net/http/server.go:1879 /usr/local/Cellar/go/1.12.5/libexec/src/runtime/asm_amd64.s:1338 diff --git a/doc/gee-day7.md b/doc/gee-day7.md index a9efa78..b2766aa 100644 --- a/doc/gee-day7.md +++ b/doc/gee-day7.md @@ -256,7 +256,7 @@ $ curl "http://localhost:9999" Hello Geektutu ``` -我们可以在后台日志中看到如下内容,引发错误的原因和堆栈信息都被打印了出来,通过日志,我们可以很容易地知道,在*day7-panic-recover/main.go:20* 的地方出现了 `index out of range` 错误。 +我们可以在后台日志中看到如下内容,引发错误的原因和堆栈信息都被打印了出来,通过日志,我们可以很容易地知道,在*day7-panic-recover/main.go:47* 的地方出现了 `index out of range` 错误。 ```bash 2020/01/09 01:00:10 Route GET - / @@ -266,14 +266,14 @@ Hello Geektutu Traceback: /usr/local/Cellar/go/1.12.5/libexec/src/runtime/panic.go:523 /usr/local/Cellar/go/1.12.5/libexec/src/runtime/panic.go:44 - /Users/gzdaijie/Github/blog/geektutu-blog/posts/7days-golang/day7-panic-recover/main.go:47 - /Users/gzdaijie/Github/blog/geektutu-blog/posts/7days-golang/day7-panic-recover/gee/context.go:41 - /Users/gzdaijie/Github/blog/geektutu-blog/posts/7days-golang/day7-panic-recover/gee/recovery.go:37 - /Users/gzdaijie/Github/blog/geektutu-blog/posts/7days-golang/day7-panic-recover/gee/context.go:41 - /Users/gzdaijie/Github/blog/geektutu-blog/posts/7days-golang/day7-panic-recover/gee/logger.go:15 - /Users/gzdaijie/Github/blog/geektutu-blog/posts/7days-golang/day7-panic-recover/gee/context.go:41 - /Users/gzdaijie/Github/blog/geektutu-blog/posts/7days-golang/day7-panic-recover/gee/router.go:99 - /Users/gzdaijie/Github/blog/geektutu-blog/posts/7days-golang/day7-panic-recover/gee/gee.go:130 + /tmp/7days-golang/day7-panic-recover/main.go:47 + /tmp/7days-golang/day7-panic-recover/gee/context.go:41 + /tmp/7days-golang/day7-panic-recover/gee/recovery.go:37 + /tmp/7days-golang/day7-panic-recover/gee/context.go:41 + /tmp/7days-golang/day7-panic-recover/gee/logger.go:15 + /tmp/7days-golang/day7-panic-recover/gee/context.go:41 + /tmp/7days-golang/day7-panic-recover/gee/router.go:99 + /tmp/7days-golang/day7-panic-recover/gee/gee.go:130 /usr/local/Cellar/go/1.12.5/libexec/src/net/http/server.go:2775 /usr/local/Cellar/go/1.12.5/libexec/src/net/http/server.go:1879 /usr/local/Cellar/go/1.12.5/libexec/src/runtime/asm_amd64.s:1338 From fcd8faebdb98aae31b2a7950f5e0508fdf0511ce Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Fri, 10 Jan 2020 00:48:08 +0800 Subject: [PATCH 007/122] fix gee-day7 title --- doc/gee-day7.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/gee-day7.md b/doc/gee-day7.md index b2766aa..e1b1136 100644 --- a/doc/gee-day7.md +++ b/doc/gee-day7.md @@ -1,5 +1,5 @@ --- -title: Go语言动手写Web框架 - Gee第六天 错误恢复(Panic Recover) +title: Go语言动手写Web框架 - Gee第七天 错误恢复(Panic Recover) date: 2020-01-09 01:00:00 description: 7天用 Go语言 从零实现Web框架教程(7 days implement golang web framework from scratch tutorial),用 Go语言/golang 动手写Web框架,从零实现一个Web框架,以 Gin 为原型从零设计一个Web框架。本文介绍了如何为Web框架增加错误处理机制。 tags: From bb58c2b263d8ed38b3322d90d41c823fa56c1757 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Sat, 18 Jan 2020 01:31:09 +0800 Subject: [PATCH 008/122] add go tutorial for gee --- README.md | 3 +++ doc/gee.md | 7 ++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 93e00b2..2da62b5 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ ![Gee](doc/gee/gee.jpg) + +如果是 Go 语言的初学者,推荐先阅读 [Go 语言简明教程](https://geektutu.com/post/quick-golang.html)。 + Gee 的设计与实现参考了Gin,这个教程可以快速入门:[Go Gin简明教程](https://geektutu.com/post/quick-go-gin.html)。 ## [教程目录](https://geektutu.com/post/gee.html) diff --git a/doc/gee.md b/doc/gee.md index 6db98b0..26bf1f2 100644 --- a/doc/gee.md +++ b/doc/gee.md @@ -68,4 +68,9 @@ func handler(w http.ResponseWriter, r *http.Request) { - [第四天:分组控制(Group)](https://geektutu.com/post/gee-day4.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/day4-group) - [第五天:中间件(Middleware)](https://geektutu.com/post/gee-day5.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/day5-middleware) - [第六天:HTML模板(Template)](https://geektutu.com/post/gee-day6.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/day6-template) -- [第七天:错误恢复(Panic Recover)](https://geektutu.com/post/gee-day7.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/day7-panic-recover) \ No newline at end of file +- [第七天:错误恢复(Panic Recover)](https://geektutu.com/post/gee-day7.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/day7-panic-recover) + +## 推荐阅读 + +- [Go 语言简明教程](https://geektutu.com/post/quick-golang.html) +- [Go Gin 简明教程](https://geektutu.com/post/quick-go-gin.html) \ No newline at end of file From 179e70e8359484ff86fd10ff767b58aae967f58e Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Sat, 18 Jan 2020 15:40:29 +0800 Subject: [PATCH 009/122] move gee day1-day7 to gee-web --- README.md | 229 ++---------------- gee-web/README.md | 215 ++++++++++++++++ .../day1-http-base}/base1/main.go | 0 .../day1-http-base}/base2/main.go | 0 .../day1-http-base}/base3/gee/gee.go | 0 .../day1-http-base}/base3/main.go | 0 .../day2-context}/gee/context.go | 0 .../day2-context}/gee/gee.go | 0 .../day2-context}/gee/router.go | 0 .../day2-context}/main.go | 0 .../day3-router}/gee/context.go | 0 .../day3-router}/gee/gee.go | 0 .../day3-router}/gee/router.go | 0 .../day3-router}/gee/router_test.go | 0 .../day3-router}/gee/trie.go | 0 {day3-router => gee-web/day3-router}/main.go | 0 .../day4-group}/gee/context.go | 0 {day4-group => gee-web/day4-group}/gee/gee.go | 0 .../day4-group}/gee/gee_test.go | 0 .../day4-group}/gee/router.go | 0 .../day4-group}/gee/router_test.go | 0 .../day4-group}/gee/trie.go | 0 {day4-group => gee-web/day4-group}/main.go | 0 .../day5-middleware}/gee/context.go | 0 .../day5-middleware}/gee/gee.go | 0 .../day5-middleware}/gee/gee_test.go | 0 .../day5-middleware}/gee/logger.go | 0 .../day5-middleware}/gee/router.go | 0 .../day5-middleware}/gee/router_test.go | 0 .../day5-middleware}/gee/trie.go | 0 .../day5-middleware}/main.go | 0 .../day6-template}/gee/context.go | 0 .../day6-template}/gee/gee.go | 0 .../day6-template}/gee/gee_test.go | 0 .../day6-template}/gee/logger.go | 0 .../day6-template}/gee/router.go | 0 .../day6-template}/gee/router_test.go | 0 .../day6-template}/gee/trie.go | 0 .../day6-template}/main.go | 0 .../day6-template}/static/css/geektutu.css | 0 .../day6-template}/static/file1.txt | 0 .../day6-template}/templates/arr.tmpl | 0 .../day6-template}/templates/css.tmpl | 0 .../day6-template}/templates/custom_func.tmpl | 0 .../day7-panic-recover}/gee/context.go | 0 .../day7-panic-recover}/gee/gee.go | 0 .../day7-panic-recover}/gee/gee_test.go | 0 .../day7-panic-recover}/gee/logger.go | 0 .../day7-panic-recover}/gee/recovery.go | 0 .../day7-panic-recover}/gee/router.go | 0 .../day7-panic-recover}/gee/router_test.go | 0 .../day7-panic-recover}/gee/trie.go | 0 .../day7-panic-recover}/main.go | 0 {doc => gee-web/doc}/gee-day1.md | 8 +- {doc => gee-web/doc}/gee-day2.md | 8 +- {doc => gee-web/doc}/gee-day3.md | 2 +- {doc => gee-web/doc}/gee-day3/trie_eg.jpg | Bin {doc => gee-web/doc}/gee-day3/trie_router.jpg | Bin {doc => gee-web/doc}/gee-day4.md | 2 +- {doc => gee-web/doc}/gee-day4/group.jpg | Bin {doc => gee-web/doc}/gee-day5.md | 8 +- {doc => gee-web/doc}/gee-day5/middleware.jpg | Bin {doc => gee-web/doc}/gee-day6.md | 6 +- {doc => gee-web/doc}/gee-day6/html.png | Bin {doc => gee-web/doc}/gee-day6/static.jpg | Bin {doc => gee-web/doc}/gee-day7.md | 4 +- {doc => gee-web/doc}/gee-day7/go-panic.png | Bin {doc => gee-web/doc}/gee.md | 14 +- {doc => gee-web/doc}/gee/gee.jpg | Bin 69 files changed, 255 insertions(+), 241 deletions(-) create mode 100644 gee-web/README.md rename {day1-http-base => gee-web/day1-http-base}/base1/main.go (100%) rename {day1-http-base => gee-web/day1-http-base}/base2/main.go (100%) rename {day1-http-base => gee-web/day1-http-base}/base3/gee/gee.go (100%) rename {day1-http-base => gee-web/day1-http-base}/base3/main.go (100%) rename {day2-context => gee-web/day2-context}/gee/context.go (100%) rename {day2-context => gee-web/day2-context}/gee/gee.go (100%) rename {day2-context => gee-web/day2-context}/gee/router.go (100%) rename {day2-context => gee-web/day2-context}/main.go (100%) rename {day3-router => gee-web/day3-router}/gee/context.go (100%) rename {day3-router => gee-web/day3-router}/gee/gee.go (100%) rename {day3-router => gee-web/day3-router}/gee/router.go (100%) rename {day3-router => gee-web/day3-router}/gee/router_test.go (100%) rename {day3-router => gee-web/day3-router}/gee/trie.go (100%) rename {day3-router => gee-web/day3-router}/main.go (100%) rename {day4-group => gee-web/day4-group}/gee/context.go (100%) rename {day4-group => gee-web/day4-group}/gee/gee.go (100%) rename {day4-group => gee-web/day4-group}/gee/gee_test.go (100%) rename {day4-group => gee-web/day4-group}/gee/router.go (100%) rename {day4-group => gee-web/day4-group}/gee/router_test.go (100%) rename {day4-group => gee-web/day4-group}/gee/trie.go (100%) rename {day4-group => gee-web/day4-group}/main.go (100%) rename {day5-middleware => gee-web/day5-middleware}/gee/context.go (100%) rename {day5-middleware => gee-web/day5-middleware}/gee/gee.go (100%) rename {day5-middleware => gee-web/day5-middleware}/gee/gee_test.go (100%) rename {day5-middleware => gee-web/day5-middleware}/gee/logger.go (100%) rename {day5-middleware => gee-web/day5-middleware}/gee/router.go (100%) rename {day5-middleware => gee-web/day5-middleware}/gee/router_test.go (100%) rename {day5-middleware => gee-web/day5-middleware}/gee/trie.go (100%) rename {day5-middleware => gee-web/day5-middleware}/main.go (100%) rename {day6-template => gee-web/day6-template}/gee/context.go (100%) rename {day6-template => gee-web/day6-template}/gee/gee.go (100%) rename {day6-template => gee-web/day6-template}/gee/gee_test.go (100%) rename {day6-template => gee-web/day6-template}/gee/logger.go (100%) rename {day6-template => gee-web/day6-template}/gee/router.go (100%) rename {day6-template => gee-web/day6-template}/gee/router_test.go (100%) rename {day6-template => gee-web/day6-template}/gee/trie.go (100%) rename {day6-template => gee-web/day6-template}/main.go (100%) rename {day6-template => gee-web/day6-template}/static/css/geektutu.css (100%) rename {day6-template => gee-web/day6-template}/static/file1.txt (100%) rename {day6-template => gee-web/day6-template}/templates/arr.tmpl (100%) rename {day6-template => gee-web/day6-template}/templates/css.tmpl (100%) rename {day6-template => gee-web/day6-template}/templates/custom_func.tmpl (100%) rename {day7-panic-recover => gee-web/day7-panic-recover}/gee/context.go (100%) rename {day7-panic-recover => gee-web/day7-panic-recover}/gee/gee.go (100%) rename {day7-panic-recover => gee-web/day7-panic-recover}/gee/gee_test.go (100%) rename {day7-panic-recover => gee-web/day7-panic-recover}/gee/logger.go (100%) rename {day7-panic-recover => gee-web/day7-panic-recover}/gee/recovery.go (100%) rename {day7-panic-recover => gee-web/day7-panic-recover}/gee/router.go (100%) rename {day7-panic-recover => gee-web/day7-panic-recover}/gee/router_test.go (100%) rename {day7-panic-recover => gee-web/day7-panic-recover}/gee/trie.go (100%) rename {day7-panic-recover => gee-web/day7-panic-recover}/main.go (100%) rename {doc => gee-web/doc}/gee-day1.md (97%) rename {doc => gee-web/doc}/gee-day2.md (98%) rename {doc => gee-web/doc}/gee-day3.md (99%) rename {doc => gee-web/doc}/gee-day3/trie_eg.jpg (100%) rename {doc => gee-web/doc}/gee-day3/trie_router.jpg (100%) rename {doc => gee-web/doc}/gee-day4.md (99%) rename {doc => gee-web/doc}/gee-day4/group.jpg (100%) rename {doc => gee-web/doc}/gee-day5.md (97%) rename {doc => gee-web/doc}/gee-day5/middleware.jpg (100%) rename {doc => gee-web/doc}/gee-day6.md (98%) rename {doc => gee-web/doc}/gee-day6/html.png (100%) rename {doc => gee-web/doc}/gee-day6/static.jpg (100%) rename {doc => gee-web/doc}/gee-day7.md (98%) rename {doc => gee-web/doc}/gee-day7/go-panic.png (100%) rename {doc => gee-web/doc}/gee.md (93%) rename {doc => gee-web/doc}/gee/gee.jpg (100%) diff --git a/README.md b/README.md index 2da62b5..9f13e1a 100644 --- a/README.md +++ b/README.md @@ -1,222 +1,21 @@ -# 7天用Go从零实现Web框架Gee +# 7天用Go从零实现系列 -![Gee](doc/gee/gee.jpg) +## 序言 +7天能写什么呢?类似 gin 的 web 框架?类似 groupcache 的分布式缓存?或者一个简单的 Python 解释器?希望这个仓库能给你答案。 -如果是 Go 语言的初学者,推荐先阅读 [Go 语言简明教程](https://geektutu.com/post/quick-golang.html)。 +推荐先阅读 **[Go 语言简明教程](https://geektutu.com/post/quick-golang.html)**,一篇文章了解Go的基本语法、并发编程,依赖管理等内容 -Gee 的设计与实现参考了Gin,这个教程可以快速入门:[Go Gin简明教程](https://geektutu.com/post/quick-go-gin.html)。 +## 7天用Go从零实现Web框架Gee -## [教程目录](https://geektutu.com/post/gee.html) +Gee 的设计与实现参考了Gin,[Go Gin简明教程](https://geektutu.com/post/quick-go-gin.html)可以快速入门。 -- [第一天:前置知识(http.Handler接口)](https://geektutu.com/post/gee-day1.html),[Code - Github](day1-http-base) -- [第二天:上下文设计(Context)](https://geektutu.com/post/gee-day2.html),[Code - Github](day2-context) -- [第三天:Tire树路由(Router)](https://geektutu.com/post/gee-day3.html),[Code - Github](day3-router) -- [第四天:分组控制(Group)](https://geektutu.com/post/gee-day4.html),[Code - Github](day4-group) -- [第五天:中间件(Middleware)](https://geektutu.com/post/gee-day5.html),[Code - Github](day5-middleware) -- [第六天:HTML模板(Template)](https://geektutu.com/post/gee-day6.html),[Code - Github](day6-template) -- [第七天:错误恢复(Panic Recover)](https://geektutu.com/post/gee-day7.html),[Code - Github](day7-panic-recover) +#### [教程目录](https://geektutu.com/post/gee.html) - -## Day 1 - Static Route - -```go -func main() { - r := gee.New() - r.GET("/", func(w http.ResponseWriter, req *http.Request) { - fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path) - }) - - r.GET("/hello", func(w http.ResponseWriter, req *http.Request) { - for k, v := range req.Header { - fmt.Fprintf(w, "Header[%q] = %q\n", k, v) - } - }) - - r.Run(":9999") -} -``` - -## Day 2 - Context Design - -```go -func main() { - r := gee.New() - r.GET("/", func(c *gee.Context) { - c.HTML(http.StatusOK, "

Hello Gee

") - }) - r.GET("/hello", func(c *gee.Context) { - // expect /hello?name=geektutu - c.String(http.StatusOK, "hello %s, you're at %s\n", c.Query("name"), c.Path) - }) - - r.POST("/login", func(c *gee.Context) { - c.JSON(http.StatusOK, &map[string]string{ - "username": c.PostForm("username"), - "password": c.PostForm("password"), - }) - }) - - r.Run(":9999") -} -``` - -## Day 3 - Dynamic Route - -```go -func main() { - r := gee.New() - r.GET("/", func(c *gee.Context) { - c.HTML(http.StatusOK, "

Hello Gee

") - }) - - r.GET("/hello", func(c *gee.Context) { - // expect /hello?name=geektutu - c.String(http.StatusOK, "hello %s, you're at %s\n", c.Query("name"), c.Path) - }) - - r.GET("/hello/:name", func(c *gee.Context) { - // expect /hello/geektutu - c.String(http.StatusOK, "hello %s, you're at %s\n", c.Param("name"), c.Path) - }) - - r.GET("/assets/*filepath", func(c *gee.Context) { - c.JSON(http.StatusOK, gee.H{"filepath": c.Param("filepath")}) - }) - - r.Run(":9999") -} -``` - -## Day 4 - Nesting Group Control - -```go -func main() { - r := gee.New() - v1 := r.Group("/v1") - { - v1.GET("/", func(c *gee.Context) { - c.HTML(http.StatusOK, "

Hello Gee

") - }) - - v1.GET("/hello", func(c *gee.Context) { - // expect /hello?name=geektutu - c.String(http.StatusOK, "hello %s, you're at %s\n", c.Query("name"), c.Path) - }) - } - v2 := r.Group("/v2") - { - v2.GET("/hello/:name", func(c *gee.Context) { - // expect /hello/geektutu - c.String(http.StatusOK, "hello %s, you're at %s\n", c.Param("name"), c.Path) - }) - v2.POST("/login", func(c *gee.Context) { - c.JSON(http.StatusOK, &map[string]string{ - "username": c.PostForm("username"), - "password": c.PostForm("password"), - }) - }) - - } - - r.Run(":9999") -} -``` - -## Day 5 - Middleware - -```go -func onlyForV2() gee.HandlerFunc { - return func(c *gee.Context) { - // Start timer - t := time.Now() - // if a server error occurred - c.Fail(500, "Internal Server Error") - // Calculate resolution time - log.Printf("[%d] %s in %v for group v2", c.StatusCode, c.Req.RequestURI, time.Since(t)) - } -} - -func main() { - r := gee.New() - r.Use(gee.Logger()) // global midlleware - r.GET("/", func(c *gee.Context) { - c.HTML(http.StatusOK, "

Hello Gee

") - }) - - v2 := r.Group("/v2") - v2.Use(onlyForV2()) // v2 group middleware - { - v2.GET("/hello/:name", func(c *gee.Context) { - // expect /hello/geektutu - c.String(http.StatusOK, "hello %s, you're at %s\n", c.Param("name"), c.Path) - }) - } - - r.Run(":9999") -} -``` - -## Day 6 - HTML Template - -```go -type student struct { - Name string - Age int8 -} - -func formatAsDate(t time.Time) string { - year, month, day := t.Date() - return fmt.Sprintf("%d-%02d-%02d", year, month, day) -} - -func main() { - r := gee.New() - r.Use(gee.Logger()) - r.SetFuncMap(template.FuncMap{ - "formatAsDate": formatAsDate, - }) - r.LoadHTMLGlob("templates/*") - r.Static("/assets", "./static") - - stu1 := &student{Name: "Geektutu", Age: 20} - stu2 := &student{Name: "Jack", Age: 22} - r.GET("/", func(c *gee.Context) { - c.HTML(http.StatusOK, "css.tmpl", nil) - }) - r.GET("/students", func(c *gee.Context) { - c.HTML(http.StatusOK, "arr.tmpl", gee.H{ - "title": "gee", - "stuArr": [2]*student{stu1, stu2}, - }) - }) - - r.GET("/date", func(c *gee.Context) { - c.HTML(http.StatusOK, "custom_func.tmpl", gee.H{ - "title": "gee", - "now": time.Date(2019, 8, 17, 0, 0, 0, 0, time.UTC), - }) - }) - - r.Run(":9999") -} -``` - -## Day 7 - Panic Recover - -```go -func main() { - r := gee.Default() - r.GET("/", func(c *gee.Context) { - c.String(http.StatusOK, "Hello Geektutu\n") - }) - // index out of range for testing Recovery() - r.GET("/panic", func(c *gee.Context) { - names := []string{"geektutu"} - c.String(http.StatusOK, names[100]) - }) - - r.Run(":9999") -} - -``` \ No newline at end of file +- [第一天:前置知识(http.Handler接口)](https://geektutu.com/post/gee-day1.html),[Code - Github](gee-web/day1-http-base) +- [第二天:上下文设计(Context)](https://geektutu.com/post/gee-day2.html),[Code - Github](gee-web/day2-context) +- [第三天:Tire树路由(Router)](https://geektutu.com/post/gee-day3.html),[Code - Github](gee-web/day3-router) +- [第四天:分组控制(Group)](https://geektutu.com/post/gee-day4.html),[Code - Github](gee-web/day4-group) +- [第五天:中间件(Middleware)](https://geektutu.com/post/gee-day5.html),[Code - Github](gee-web/day5-middleware) +- [第六天:HTML模板(Template)](https://geektutu.com/post/gee-day6.html),[Code - Github](gee-web/day6-template) +- [第七天:错误恢复(Panic Recover)](https://geektutu.com/post/gee-day7.html),[Code - Github](gee-web/day7-panic-recover) \ No newline at end of file diff --git a/gee-web/README.md b/gee-web/README.md new file mode 100644 index 0000000..3361183 --- /dev/null +++ b/gee-web/README.md @@ -0,0 +1,215 @@ +# 7天用Go从零实现Web框架Gee + +![Gee](doc/gee/gee.jpg) + +## 教程目录 + +- [第一天:前置知识(http.Handler接口)](https://geektutu.com/post/gee-day1.html) +- [第二天:上下文设计(Context)](https://geektutu.com/post/gee-day2.html) +- [第三天:Tire树路由(Router)](https://geektutu.com/post/gee-day3.html) +- [第四天:分组控制(Group)](https://geektutu.com/post/gee-day4.html) +- [第五天:中间件(Middleware)](https://geektutu.com/post/gee-day5.html) +- [第六天:HTML模板(Template)](https://geektutu.com/post/gee-day6.html) +- [第七天:错误恢复(Panic Recover)](https://geektutu.com/post/gee-day7.html) + +## Day 1 - Static Route + +```go +func main() { + r := gee.New() + r.GET("/", func(w http.ResponseWriter, req *http.Request) { + fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path) + }) + + r.GET("/hello", func(w http.ResponseWriter, req *http.Request) { + for k, v := range req.Header { + fmt.Fprintf(w, "Header[%q] = %q\n", k, v) + } + }) + + r.Run(":9999") +} +``` + +## Day 2 - Context Design + +```go +func main() { + r := gee.New() + r.GET("/", func(c *gee.Context) { + c.HTML(http.StatusOK, "

Hello Gee

") + }) + r.GET("/hello", func(c *gee.Context) { + // expect /hello?name=geektutu + c.String(http.StatusOK, "hello %s, you're at %s\n", c.Query("name"), c.Path) + }) + + r.POST("/login", func(c *gee.Context) { + c.JSON(http.StatusOK, &map[string]string{ + "username": c.PostForm("username"), + "password": c.PostForm("password"), + }) + }) + + r.Run(":9999") +} +``` + +## Day 3 - Dynamic Route + +```go +func main() { + r := gee.New() + r.GET("/", func(c *gee.Context) { + c.HTML(http.StatusOK, "

Hello Gee

") + }) + + r.GET("/hello", func(c *gee.Context) { + // expect /hello?name=geektutu + c.String(http.StatusOK, "hello %s, you're at %s\n", c.Query("name"), c.Path) + }) + + r.GET("/hello/:name", func(c *gee.Context) { + // expect /hello/geektutu + c.String(http.StatusOK, "hello %s, you're at %s\n", c.Param("name"), c.Path) + }) + + r.GET("/assets/*filepath", func(c *gee.Context) { + c.JSON(http.StatusOK, gee.H{"filepath": c.Param("filepath")}) + }) + + r.Run(":9999") +} +``` + +## Day 4 - Nesting Group Control + +```go +func main() { + r := gee.New() + v1 := r.Group("/v1") + { + v1.GET("/", func(c *gee.Context) { + c.HTML(http.StatusOK, "

Hello Gee

") + }) + + v1.GET("/hello", func(c *gee.Context) { + // expect /hello?name=geektutu + c.String(http.StatusOK, "hello %s, you're at %s\n", c.Query("name"), c.Path) + }) + } + v2 := r.Group("/v2") + { + v2.GET("/hello/:name", func(c *gee.Context) { + // expect /hello/geektutu + c.String(http.StatusOK, "hello %s, you're at %s\n", c.Param("name"), c.Path) + }) + v2.POST("/login", func(c *gee.Context) { + c.JSON(http.StatusOK, &map[string]string{ + "username": c.PostForm("username"), + "password": c.PostForm("password"), + }) + }) + + } + + r.Run(":9999") +} +``` + +## Day 5 - Middleware + +```go +func onlyForV2() gee.HandlerFunc { + return func(c *gee.Context) { + // Start timer + t := time.Now() + // if a server error occurred + c.Fail(500, "Internal Server Error") + // Calculate resolution time + log.Printf("[%d] %s in %v for group v2", c.StatusCode, c.Req.RequestURI, time.Since(t)) + } +} + +func main() { + r := gee.New() + r.Use(gee.Logger()) // global midlleware + r.GET("/", func(c *gee.Context) { + c.HTML(http.StatusOK, "

Hello Gee

") + }) + + v2 := r.Group("/v2") + v2.Use(onlyForV2()) // v2 group middleware + { + v2.GET("/hello/:name", func(c *gee.Context) { + // expect /hello/geektutu + c.String(http.StatusOK, "hello %s, you're at %s\n", c.Param("name"), c.Path) + }) + } + + r.Run(":9999") +} +``` + +## Day 6 - HTML Template + +```go +type student struct { + Name string + Age int8 +} + +func formatAsDate(t time.Time) string { + year, month, day := t.Date() + return fmt.Sprintf("%d-%02d-%02d", year, month, day) +} + +func main() { + r := gee.New() + r.Use(gee.Logger()) + r.SetFuncMap(template.FuncMap{ + "formatAsDate": formatAsDate, + }) + r.LoadHTMLGlob("templates/*") + r.Static("/assets", "./static") + + stu1 := &student{Name: "Geektutu", Age: 20} + stu2 := &student{Name: "Jack", Age: 22} + r.GET("/", func(c *gee.Context) { + c.HTML(http.StatusOK, "css.tmpl", nil) + }) + r.GET("/students", func(c *gee.Context) { + c.HTML(http.StatusOK, "arr.tmpl", gee.H{ + "title": "gee", + "stuArr": [2]*student{stu1, stu2}, + }) + }) + + r.GET("/date", func(c *gee.Context) { + c.HTML(http.StatusOK, "custom_func.tmpl", gee.H{ + "title": "gee", + "now": time.Date(2019, 8, 17, 0, 0, 0, 0, time.UTC), + }) + }) + + r.Run(":9999") +} +``` + +## Day 7 - Panic Recover + +```go +func main() { + r := gee.Default() + r.GET("/", func(c *gee.Context) { + c.String(http.StatusOK, "Hello Geektutu\n") + }) + // index out of range for testing Recovery() + r.GET("/panic", func(c *gee.Context) { + names := []string{"geektutu"} + c.String(http.StatusOK, names[100]) + }) + + r.Run(":9999") +} +``` \ No newline at end of file diff --git a/day1-http-base/base1/main.go b/gee-web/day1-http-base/base1/main.go similarity index 100% rename from day1-http-base/base1/main.go rename to gee-web/day1-http-base/base1/main.go diff --git a/day1-http-base/base2/main.go b/gee-web/day1-http-base/base2/main.go similarity index 100% rename from day1-http-base/base2/main.go rename to gee-web/day1-http-base/base2/main.go diff --git a/day1-http-base/base3/gee/gee.go b/gee-web/day1-http-base/base3/gee/gee.go similarity index 100% rename from day1-http-base/base3/gee/gee.go rename to gee-web/day1-http-base/base3/gee/gee.go diff --git a/day1-http-base/base3/main.go b/gee-web/day1-http-base/base3/main.go similarity index 100% rename from day1-http-base/base3/main.go rename to gee-web/day1-http-base/base3/main.go diff --git a/day2-context/gee/context.go b/gee-web/day2-context/gee/context.go similarity index 100% rename from day2-context/gee/context.go rename to gee-web/day2-context/gee/context.go diff --git a/day2-context/gee/gee.go b/gee-web/day2-context/gee/gee.go similarity index 100% rename from day2-context/gee/gee.go rename to gee-web/day2-context/gee/gee.go diff --git a/day2-context/gee/router.go b/gee-web/day2-context/gee/router.go similarity index 100% rename from day2-context/gee/router.go rename to gee-web/day2-context/gee/router.go diff --git a/day2-context/main.go b/gee-web/day2-context/main.go similarity index 100% rename from day2-context/main.go rename to gee-web/day2-context/main.go diff --git a/day3-router/gee/context.go b/gee-web/day3-router/gee/context.go similarity index 100% rename from day3-router/gee/context.go rename to gee-web/day3-router/gee/context.go diff --git a/day3-router/gee/gee.go b/gee-web/day3-router/gee/gee.go similarity index 100% rename from day3-router/gee/gee.go rename to gee-web/day3-router/gee/gee.go diff --git a/day3-router/gee/router.go b/gee-web/day3-router/gee/router.go similarity index 100% rename from day3-router/gee/router.go rename to gee-web/day3-router/gee/router.go diff --git a/day3-router/gee/router_test.go b/gee-web/day3-router/gee/router_test.go similarity index 100% rename from day3-router/gee/router_test.go rename to gee-web/day3-router/gee/router_test.go diff --git a/day3-router/gee/trie.go b/gee-web/day3-router/gee/trie.go similarity index 100% rename from day3-router/gee/trie.go rename to gee-web/day3-router/gee/trie.go diff --git a/day3-router/main.go b/gee-web/day3-router/main.go similarity index 100% rename from day3-router/main.go rename to gee-web/day3-router/main.go diff --git a/day4-group/gee/context.go b/gee-web/day4-group/gee/context.go similarity index 100% rename from day4-group/gee/context.go rename to gee-web/day4-group/gee/context.go diff --git a/day4-group/gee/gee.go b/gee-web/day4-group/gee/gee.go similarity index 100% rename from day4-group/gee/gee.go rename to gee-web/day4-group/gee/gee.go diff --git a/day4-group/gee/gee_test.go b/gee-web/day4-group/gee/gee_test.go similarity index 100% rename from day4-group/gee/gee_test.go rename to gee-web/day4-group/gee/gee_test.go diff --git a/day4-group/gee/router.go b/gee-web/day4-group/gee/router.go similarity index 100% rename from day4-group/gee/router.go rename to gee-web/day4-group/gee/router.go diff --git a/day4-group/gee/router_test.go b/gee-web/day4-group/gee/router_test.go similarity index 100% rename from day4-group/gee/router_test.go rename to gee-web/day4-group/gee/router_test.go diff --git a/day4-group/gee/trie.go b/gee-web/day4-group/gee/trie.go similarity index 100% rename from day4-group/gee/trie.go rename to gee-web/day4-group/gee/trie.go diff --git a/day4-group/main.go b/gee-web/day4-group/main.go similarity index 100% rename from day4-group/main.go rename to gee-web/day4-group/main.go diff --git a/day5-middleware/gee/context.go b/gee-web/day5-middleware/gee/context.go similarity index 100% rename from day5-middleware/gee/context.go rename to gee-web/day5-middleware/gee/context.go diff --git a/day5-middleware/gee/gee.go b/gee-web/day5-middleware/gee/gee.go similarity index 100% rename from day5-middleware/gee/gee.go rename to gee-web/day5-middleware/gee/gee.go diff --git a/day5-middleware/gee/gee_test.go b/gee-web/day5-middleware/gee/gee_test.go similarity index 100% rename from day5-middleware/gee/gee_test.go rename to gee-web/day5-middleware/gee/gee_test.go diff --git a/day5-middleware/gee/logger.go b/gee-web/day5-middleware/gee/logger.go similarity index 100% rename from day5-middleware/gee/logger.go rename to gee-web/day5-middleware/gee/logger.go diff --git a/day5-middleware/gee/router.go b/gee-web/day5-middleware/gee/router.go similarity index 100% rename from day5-middleware/gee/router.go rename to gee-web/day5-middleware/gee/router.go diff --git a/day5-middleware/gee/router_test.go b/gee-web/day5-middleware/gee/router_test.go similarity index 100% rename from day5-middleware/gee/router_test.go rename to gee-web/day5-middleware/gee/router_test.go diff --git a/day5-middleware/gee/trie.go b/gee-web/day5-middleware/gee/trie.go similarity index 100% rename from day5-middleware/gee/trie.go rename to gee-web/day5-middleware/gee/trie.go diff --git a/day5-middleware/main.go b/gee-web/day5-middleware/main.go similarity index 100% rename from day5-middleware/main.go rename to gee-web/day5-middleware/main.go diff --git a/day6-template/gee/context.go b/gee-web/day6-template/gee/context.go similarity index 100% rename from day6-template/gee/context.go rename to gee-web/day6-template/gee/context.go diff --git a/day6-template/gee/gee.go b/gee-web/day6-template/gee/gee.go similarity index 100% rename from day6-template/gee/gee.go rename to gee-web/day6-template/gee/gee.go diff --git a/day6-template/gee/gee_test.go b/gee-web/day6-template/gee/gee_test.go similarity index 100% rename from day6-template/gee/gee_test.go rename to gee-web/day6-template/gee/gee_test.go diff --git a/day6-template/gee/logger.go b/gee-web/day6-template/gee/logger.go similarity index 100% rename from day6-template/gee/logger.go rename to gee-web/day6-template/gee/logger.go diff --git a/day6-template/gee/router.go b/gee-web/day6-template/gee/router.go similarity index 100% rename from day6-template/gee/router.go rename to gee-web/day6-template/gee/router.go diff --git a/day6-template/gee/router_test.go b/gee-web/day6-template/gee/router_test.go similarity index 100% rename from day6-template/gee/router_test.go rename to gee-web/day6-template/gee/router_test.go diff --git a/day6-template/gee/trie.go b/gee-web/day6-template/gee/trie.go similarity index 100% rename from day6-template/gee/trie.go rename to gee-web/day6-template/gee/trie.go diff --git a/day6-template/main.go b/gee-web/day6-template/main.go similarity index 100% rename from day6-template/main.go rename to gee-web/day6-template/main.go diff --git a/day6-template/static/css/geektutu.css b/gee-web/day6-template/static/css/geektutu.css similarity index 100% rename from day6-template/static/css/geektutu.css rename to gee-web/day6-template/static/css/geektutu.css diff --git a/day6-template/static/file1.txt b/gee-web/day6-template/static/file1.txt similarity index 100% rename from day6-template/static/file1.txt rename to gee-web/day6-template/static/file1.txt diff --git a/day6-template/templates/arr.tmpl b/gee-web/day6-template/templates/arr.tmpl similarity index 100% rename from day6-template/templates/arr.tmpl rename to gee-web/day6-template/templates/arr.tmpl diff --git a/day6-template/templates/css.tmpl b/gee-web/day6-template/templates/css.tmpl similarity index 100% rename from day6-template/templates/css.tmpl rename to gee-web/day6-template/templates/css.tmpl diff --git a/day6-template/templates/custom_func.tmpl b/gee-web/day6-template/templates/custom_func.tmpl similarity index 100% rename from day6-template/templates/custom_func.tmpl rename to gee-web/day6-template/templates/custom_func.tmpl diff --git a/day7-panic-recover/gee/context.go b/gee-web/day7-panic-recover/gee/context.go similarity index 100% rename from day7-panic-recover/gee/context.go rename to gee-web/day7-panic-recover/gee/context.go diff --git a/day7-panic-recover/gee/gee.go b/gee-web/day7-panic-recover/gee/gee.go similarity index 100% rename from day7-panic-recover/gee/gee.go rename to gee-web/day7-panic-recover/gee/gee.go diff --git a/day7-panic-recover/gee/gee_test.go b/gee-web/day7-panic-recover/gee/gee_test.go similarity index 100% rename from day7-panic-recover/gee/gee_test.go rename to gee-web/day7-panic-recover/gee/gee_test.go diff --git a/day7-panic-recover/gee/logger.go b/gee-web/day7-panic-recover/gee/logger.go similarity index 100% rename from day7-panic-recover/gee/logger.go rename to gee-web/day7-panic-recover/gee/logger.go diff --git a/day7-panic-recover/gee/recovery.go b/gee-web/day7-panic-recover/gee/recovery.go similarity index 100% rename from day7-panic-recover/gee/recovery.go rename to gee-web/day7-panic-recover/gee/recovery.go diff --git a/day7-panic-recover/gee/router.go b/gee-web/day7-panic-recover/gee/router.go similarity index 100% rename from day7-panic-recover/gee/router.go rename to gee-web/day7-panic-recover/gee/router.go diff --git a/day7-panic-recover/gee/router_test.go b/gee-web/day7-panic-recover/gee/router_test.go similarity index 100% rename from day7-panic-recover/gee/router_test.go rename to gee-web/day7-panic-recover/gee/router_test.go diff --git a/day7-panic-recover/gee/trie.go b/gee-web/day7-panic-recover/gee/trie.go similarity index 100% rename from day7-panic-recover/gee/trie.go rename to gee-web/day7-panic-recover/gee/trie.go diff --git a/day7-panic-recover/main.go b/gee-web/day7-panic-recover/main.go similarity index 100% rename from day7-panic-recover/main.go rename to gee-web/day7-panic-recover/main.go diff --git a/doc/gee-day1.md b/gee-web/doc/gee-day1.md similarity index 97% rename from doc/gee-day1.md rename to gee-web/doc/gee-day1.md index 14385d2..fd94496 100644 --- a/doc/gee-day1.md +++ b/gee-web/doc/gee-day1.md @@ -24,7 +24,7 @@ github: https://github.com/geektutu/7days-golang Go语言内置了 `net/http`库,封装了HTTP网络编程的基础的接口,我们实现的`Gee` Web 框架便是基于`net/http`的。我们接下来通过一个例子,简单介绍下这个库的使用。 -**[day1-http-base/base1/main.go](https://github.com/geektutu/7days-golang/tree/master/day1-http-base/base1)** +**[day1-http-base/base1/main.go](https://github.com/geektutu/7days-golang/tree/master/gee-web/day1-http-base/base1)** ```go package main @@ -82,7 +82,7 @@ func ListenAndServe(address string, h Handler) error 第二个参数的类型是什么呢?通过查看`net/http`的源码可以发现,`Handler`是一个接口,需要实现方法 _ServeHTTP_ ,也就是说,只要传入任何实现了 _ServerHTTP_ 接口的实例,所有的HTTP请求,就都交给了该实例处理了。马上来试一试吧。 -**[day1-http-base/base2/main.go](https://github.com/geektutu/7days-golang/tree/master/day1-http-base/base2)** +**[day1-http-base/base2/main.go](https://github.com/geektutu/7days-golang/tree/master/gee-web/day1-http-base/base2)** ```go package main @@ -135,7 +135,7 @@ main.go ### main.go -**[day1-http-base/base3/main.go](https://github.com/geektutu/7days-golang/tree/master/day1-http-base/base3)** +**[day1-http-base/base3/main.go](https://github.com/geektutu/7days-golang/tree/master/gee-web/day1-http-base/base3)** ```go package main @@ -167,7 +167,7 @@ func main() { ### gee.go -**[day1-http-base/base3/gee/gee.go](https://github.com/geektutu/7days-golang/tree/master/day1-http-base/base3)** +**[day1-http-base/base3/gee/gee.go](https://github.com/geektutu/7days-golang/tree/master/gee-web/day1-http-base/base3)** ```go package gee diff --git a/doc/gee-day2.md b/gee-web/doc/gee-day2.md similarity index 98% rename from doc/gee-day2.md rename to gee-web/doc/gee-day2.md index b7cf9d2..e47467f 100644 --- a/doc/gee-day2.md +++ b/gee-web/doc/gee-day2.md @@ -25,7 +25,7 @@ github: https://github.com/geektutu/7days-golang 为了展示第二天的成果,我们看一看在使用时的效果。 -[day2-context/main.go](https://github.com/geektutu/7days-golang/tree/master/day2-context) +[day2-context/main.go](https://github.com/geektutu/7days-golang/tree/master/gee-web/day2-context) ```go @@ -90,7 +90,7 @@ c.JSON(http.StatusOK, gee.H{ ### 具体实现 -[day2-context/gee/context.go](https://github.com/geektutu/7days-golang/tree/master/day2-context) +[day2-context/gee/context.go](https://github.com/geektutu/7days-golang/tree/master/gee-web/day2-context) ```go type H map[string]interface{} @@ -168,7 +168,7 @@ func (c *Context) HTML(code int, html string) { 我们将和路由相关的方法和结构提取了出来,放到了一个新的文件中`router.go`,方便我们下一次对 router 的功能进行增强,例如提供动态路由的支持。 router 的 handle 方法作了一个细微的调整,即 handler 的参数,变成了 Context。 -[day2-context/gee/router.go](https://github.com/geektutu/7days-golang/tree/master/day2-context) +[day2-context/gee/router.go](https://github.com/geektutu/7days-golang/tree/master/gee-web/day2-context) ```go type router struct { @@ -197,7 +197,7 @@ func (r *router) handle(c *Context) { ## 框架入口 -[day2-context/gee/gee.go](https://github.com/geektutu/7days-golang/tree/master/day2-context) +[day2-context/gee/gee.go](https://github.com/geektutu/7days-golang/tree/master/gee-web/day2-context) ```go // HandlerFunc defines the request handler used by gee diff --git a/doc/gee-day3.md b/gee-web/doc/gee-day3.md similarity index 99% rename from doc/gee-day3.md rename to gee-web/doc/gee-day3.md index f838554..43407f9 100644 --- a/doc/gee-day3.md +++ b/gee-web/doc/gee-day3.md @@ -52,7 +52,7 @@ HTTP请求的路径恰好是由`/`分隔的多段构成的,因此,每一段 首先我们需要设计树节点上应该存储那些信息。 -**[day3-router/gee/trie.go](https://github.com/geektutu/7days-golang/tree/master/day3-router/gee)** +**[day3-router/gee/trie.go](https://github.com/geektutu/7days-golang/tree/master/gee-web/day3-router/gee)** ```go type node struct { diff --git a/doc/gee-day3/trie_eg.jpg b/gee-web/doc/gee-day3/trie_eg.jpg similarity index 100% rename from doc/gee-day3/trie_eg.jpg rename to gee-web/doc/gee-day3/trie_eg.jpg diff --git a/doc/gee-day3/trie_router.jpg b/gee-web/doc/gee-day3/trie_router.jpg similarity index 100% rename from doc/gee-day3/trie_router.jpg rename to gee-web/doc/gee-day3/trie_router.jpg diff --git a/doc/gee-day4.md b/gee-web/doc/gee-day4.md similarity index 99% rename from doc/gee-day4.md rename to gee-web/doc/gee-day4.md index f6fb64a..b3f6625 100644 --- a/doc/gee-day4.md +++ b/gee-web/doc/gee-day4.md @@ -49,7 +49,7 @@ v1.GET("/", func(c *gee.Context) { 所以,最后的 Group 的定义是这样的: -**[day4-group/gee/gee.go](https://github.com/geektutu/7days-golang/tree/master/day4-group/gee)** +**[day4-group/gee/gee.go](https://github.com/geektutu/7days-golang/tree/master/gee-web/day4-group/gee)** ```go RouterGroup struct { diff --git a/doc/gee-day4/group.jpg b/gee-web/doc/gee-day4/group.jpg similarity index 100% rename from doc/gee-day4/group.jpg rename to gee-web/doc/gee-day4/group.jpg diff --git a/doc/gee-day5.md b/gee-web/doc/gee-day5.md similarity index 97% rename from doc/gee-day5.md rename to gee-web/doc/gee-day5.md index 763e8a7..4ae022e 100644 --- a/doc/gee-day5.md +++ b/gee-web/doc/gee-day5.md @@ -33,7 +33,7 @@ github: https://github.com/geektutu/7days-golang Gee 的中间件的定义与路由映射的 Handler 一致,处理的输入是`Context`对象。插入点是框架接收到请求初始化`Context`对象后,允许用户使用自己定义的中间件做一些额外的处理,例如记录日志等,以及对`Context`进行二次加工。另外通过调用`(*Context).Next()`函数,中间件可等待用户自己定义的 `Handler`处理结束后,再做一些额外的操作,例如计算本次处理所用时间等。即 Gee 的中间件支持用户在请求被处理的前后,做一些额外的操作。举个例子,我们希望最终能够支持如下定义的中间件,`c.Next()`表示等待执行其他的中间件或用户的`Handler`: -****[day4-group/gee/logger.go](https://github.com/geektutu/7days-golang/tree/master/day5-middleware/gee)**** +****[day4-group/gee/logger.go](https://github.com/geektutu/7days-golang/tree/master/gee-web/day5-middleware/gee)**** ```go func Logger() HandlerFunc { @@ -56,7 +56,7 @@ func Logger() HandlerFunc { 为此,我们给`Context`添加了2个参数,定义了`Next`方法: -**[day4-group/gee/context.go](https://github.com/geektutu/7days-golang/tree/master/day5-middleware/gee)** +**[day4-group/gee/context.go](https://github.com/geektutu/7days-golang/tree/master/gee-web/day5-middleware/gee)** ```go type Context struct { @@ -130,7 +130,7 @@ func B(c *Context) { - 定义`Use`函数,将中间件应用到某个 Group 。 -**[day4-group/gee/gee.go](https://github.com/geektutu/7days-golang/tree/master/day5-middleware/gee)** +**[day4-group/gee/gee.go](https://github.com/geektutu/7days-golang/tree/master/gee-web/day5-middleware/gee)** ```go // Use is defined to add middleware to the group @@ -155,7 +155,7 @@ ServeHTTP 函数也有变化,当我们接收到一个具体请求时,要判 - handle 函数中,将从路由匹配得到的 Handler 添加到 `c.handlers`列表中,执行`c.Next()`。 -**[day4-group/gee/router.go](https://github.com/geektutu/7days-golang/tree/master/day5-middleware/gee)** +**[day4-group/gee/router.go](https://github.com/geektutu/7days-golang/tree/master/gee-web/day5-middleware/gee)** ```go func (r *router) handle(c *Context) { diff --git a/doc/gee-day5/middleware.jpg b/gee-web/doc/gee-day5/middleware.jpg similarity index 100% rename from doc/gee-day5/middleware.jpg rename to gee-web/doc/gee-day5/middleware.jpg diff --git a/doc/gee-day6.md b/gee-web/doc/gee-day6.md similarity index 98% rename from doc/gee-day6.md rename to gee-web/doc/gee-day6.md index ad4845b..4ee21df 100644 --- a/doc/gee-day6.md +++ b/gee-web/doc/gee-day6.md @@ -36,7 +36,7 @@ github: https://github.com/geektutu/7days-golang 找到文件后,如何返回这一步,`net/http`库已经实现了。因此,gee 框架要做的,仅仅是解析请求的地址,映射到服务器上文件的真实地址,交给`http.FileServer`处理就好了。 -[day6-template/gee/gee.go](https://github.com/geektutu/7days-golang/tree/master/day6-template/gee) +[day6-template/gee/gee.go](https://github.com/geektutu/7days-golang/tree/master/gee-web/day6-template/gee) ```go // create static handler @@ -103,7 +103,7 @@ func (engine *Engine) LoadHTMLGlob(pattern string) { 接下来,对原来的 `(*Context).HTML()`方法做了些小修改,使之支持根据模板文件名选择模板进行渲染。 -[day6-template/gee/context.go](https://github.com/geektutu/7days-golang/tree/master/day6-template/gee) +[day6-template/gee/context.go](https://github.com/geektutu/7days-golang/tree/master/gee-web/day6-template/gee) ```go func (c *Context) HTML(code int, name string, data interface{}) { @@ -140,7 +140,7 @@ func (c *Context) HTML(code int, name string, data interface{}) { ``` -[day6-template/main.go](https://github.com/geektutu/7days-golang/tree/master/day6-template/gee) +[day6-template/main.go](https://github.com/geektutu/7days-golang/tree/master/gee-web/day6-template/gee) ```go type student struct { diff --git a/doc/gee-day6/html.png b/gee-web/doc/gee-day6/html.png similarity index 100% rename from doc/gee-day6/html.png rename to gee-web/doc/gee-day6/html.png diff --git a/doc/gee-day6/static.jpg b/gee-web/doc/gee-day6/static.jpg similarity index 100% rename from doc/gee-day6/static.jpg rename to gee-web/doc/gee-day6/static.jpg diff --git a/doc/gee-day7.md b/gee-web/doc/gee-day7.md similarity index 98% rename from doc/gee-day7.md rename to gee-web/doc/gee-day7.md index e1b1136..f72a314 100644 --- a/doc/gee-day7.md +++ b/gee-web/doc/gee-day7.md @@ -168,7 +168,7 @@ func Recovery() HandlerFunc { 你可能注意到,这里有一个 *trace()* 函数,这个函数是用来获取触发 panic 的堆栈信息,完整代码如下: -[day7-panic-recover/gee/recovery.go](https://github.com/geektutu/7days-golang/tree/master/day7-panic-recover) +[day7-panic-recover/gee/recovery.go](https://github.com/geektutu/7days-golang/tree/master/gee-web/day7-panic-recover) ```go package gee @@ -219,7 +219,7 @@ func Recovery() HandlerFunc { ## 使用 Demo -[day7-panic-recover/main.go](https://github.com/geektutu/7days-golang/tree/master/day7-panic-recover) +[day7-panic-recover/main.go](https://github.com/geektutu/7days-golang/tree/master/gee-web/day7-panic-recover) ```go package main diff --git a/doc/gee-day7/go-panic.png b/gee-web/doc/gee-day7/go-panic.png similarity index 100% rename from doc/gee-day7/go-panic.png rename to gee-web/doc/gee-day7/go-panic.png diff --git a/doc/gee.md b/gee-web/doc/gee.md similarity index 93% rename from doc/gee.md rename to gee-web/doc/gee.md index 26bf1f2..c215534 100644 --- a/doc/gee.md +++ b/gee-web/doc/gee.md @@ -62,13 +62,13 @@ func handler(w http.ResponseWriter, r *http.Request) { ## 目录 -- [第一天:前置知识(http.Handler接口)](https://geektutu.com/post/gee-day1.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/day1-http-base) -- [第二天:上下文设计(Context)](https://geektutu.com/post/gee-day2.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/day2-context) -- [第三天:Tire树路由(Router)](https://geektutu.com/post/gee-day3.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/day3-router) -- [第四天:分组控制(Group)](https://geektutu.com/post/gee-day4.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/day4-group) -- [第五天:中间件(Middleware)](https://geektutu.com/post/gee-day5.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/day5-middleware) -- [第六天:HTML模板(Template)](https://geektutu.com/post/gee-day6.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/day6-template) -- [第七天:错误恢复(Panic Recover)](https://geektutu.com/post/gee-day7.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/day7-panic-recover) +- [第一天:前置知识(http.Handler接口)](https://geektutu.com/post/gee-day1.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/gee-web/day1-http-base) +- [第二天:上下文设计(Context)](https://geektutu.com/post/gee-day2.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/gee-web/day2-context) +- [第三天:Tire树路由(Router)](https://geektutu.com/post/gee-day3.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/gee-web/day3-router) +- [第四天:分组控制(Group)](https://geektutu.com/post/gee-day4.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/gee-web/day4-group) +- [第五天:中间件(Middleware)](https://geektutu.com/post/gee-day5.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/gee-web/day5-middleware) +- [第六天:HTML模板(Template)](https://geektutu.com/post/gee-day6.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/gee-web/day6-template) +- [第七天:错误恢复(Panic Recover)](https://geektutu.com/post/gee-day7.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/gee-web/day7-panic-recover) ## 推荐阅读 diff --git a/doc/gee/gee.jpg b/gee-web/doc/gee/gee.jpg similarity index 100% rename from doc/gee/gee.jpg rename to gee-web/doc/gee/gee.jpg From 172d0ccfc0415c6082569c431a52725916963de9 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Sat, 18 Jan 2020 15:57:45 +0800 Subject: [PATCH 010/122] remove subdirectory gee from doc --- README.md | 2 -- gee-web/doc/gee-day3.md | 2 +- gee-web/doc/gee-day4.md | 2 +- gee-web/doc/gee-day5.md | 8 ++++---- gee-web/doc/gee-day6.md | 6 +++--- 5 files changed, 9 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 9f13e1a..7d11a7f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ # 7天用Go从零实现系列 -## 序言 - 7天能写什么呢?类似 gin 的 web 框架?类似 groupcache 的分布式缓存?或者一个简单的 Python 解释器?希望这个仓库能给你答案。 推荐先阅读 **[Go 语言简明教程](https://geektutu.com/post/quick-golang.html)**,一篇文章了解Go的基本语法、并发编程,依赖管理等内容 diff --git a/gee-web/doc/gee-day3.md b/gee-web/doc/gee-day3.md index 43407f9..23a0e72 100644 --- a/gee-web/doc/gee-day3.md +++ b/gee-web/doc/gee-day3.md @@ -52,7 +52,7 @@ HTTP请求的路径恰好是由`/`分隔的多段构成的,因此,每一段 首先我们需要设计树节点上应该存储那些信息。 -**[day3-router/gee/trie.go](https://github.com/geektutu/7days-golang/tree/master/gee-web/day3-router/gee)** +**[day3-router/gee/trie.go](https://github.com/geektutu/7days-golang/tree/master/gee-web/day3-router)** ```go type node struct { diff --git a/gee-web/doc/gee-day4.md b/gee-web/doc/gee-day4.md index b3f6625..7f71370 100644 --- a/gee-web/doc/gee-day4.md +++ b/gee-web/doc/gee-day4.md @@ -49,7 +49,7 @@ v1.GET("/", func(c *gee.Context) { 所以,最后的 Group 的定义是这样的: -**[day4-group/gee/gee.go](https://github.com/geektutu/7days-golang/tree/master/gee-web/day4-group/gee)** +**[day4-group/gee/gee.go](https://github.com/geektutu/7days-golang/tree/master/gee-web/day4-group)** ```go RouterGroup struct { diff --git a/gee-web/doc/gee-day5.md b/gee-web/doc/gee-day5.md index 4ae022e..9f5d117 100644 --- a/gee-web/doc/gee-day5.md +++ b/gee-web/doc/gee-day5.md @@ -33,7 +33,7 @@ github: https://github.com/geektutu/7days-golang Gee 的中间件的定义与路由映射的 Handler 一致,处理的输入是`Context`对象。插入点是框架接收到请求初始化`Context`对象后,允许用户使用自己定义的中间件做一些额外的处理,例如记录日志等,以及对`Context`进行二次加工。另外通过调用`(*Context).Next()`函数,中间件可等待用户自己定义的 `Handler`处理结束后,再做一些额外的操作,例如计算本次处理所用时间等。即 Gee 的中间件支持用户在请求被处理的前后,做一些额外的操作。举个例子,我们希望最终能够支持如下定义的中间件,`c.Next()`表示等待执行其他的中间件或用户的`Handler`: -****[day4-group/gee/logger.go](https://github.com/geektutu/7days-golang/tree/master/gee-web/day5-middleware/gee)**** +****[day4-group/gee/logger.go](https://github.com/geektutu/7days-golang/tree/master/gee-web/day5-middleware)**** ```go func Logger() HandlerFunc { @@ -56,7 +56,7 @@ func Logger() HandlerFunc { 为此,我们给`Context`添加了2个参数,定义了`Next`方法: -**[day4-group/gee/context.go](https://github.com/geektutu/7days-golang/tree/master/gee-web/day5-middleware/gee)** +**[day4-group/gee/context.go](https://github.com/geektutu/7days-golang/tree/master/gee-web/day5-middleware)** ```go type Context struct { @@ -130,7 +130,7 @@ func B(c *Context) { - 定义`Use`函数,将中间件应用到某个 Group 。 -**[day4-group/gee/gee.go](https://github.com/geektutu/7days-golang/tree/master/gee-web/day5-middleware/gee)** +**[day4-group/gee/gee.go](https://github.com/geektutu/7days-golang/tree/master/gee-web/day5-middleware)** ```go // Use is defined to add middleware to the group @@ -155,7 +155,7 @@ ServeHTTP 函数也有变化,当我们接收到一个具体请求时,要判 - handle 函数中,将从路由匹配得到的 Handler 添加到 `c.handlers`列表中,执行`c.Next()`。 -**[day4-group/gee/router.go](https://github.com/geektutu/7days-golang/tree/master/gee-web/day5-middleware/gee)** +**[day4-group/gee/router.go](https://github.com/geektutu/7days-golang/tree/master/gee-web/day5-middleware)** ```go func (r *router) handle(c *Context) { diff --git a/gee-web/doc/gee-day6.md b/gee-web/doc/gee-day6.md index 4ee21df..e429faf 100644 --- a/gee-web/doc/gee-day6.md +++ b/gee-web/doc/gee-day6.md @@ -36,7 +36,7 @@ github: https://github.com/geektutu/7days-golang 找到文件后,如何返回这一步,`net/http`库已经实现了。因此,gee 框架要做的,仅仅是解析请求的地址,映射到服务器上文件的真实地址,交给`http.FileServer`处理就好了。 -[day6-template/gee/gee.go](https://github.com/geektutu/7days-golang/tree/master/gee-web/day6-template/gee) +[day6-template/gee/gee.go](https://github.com/geektutu/7days-golang/tree/master/gee-web/day6-template) ```go // create static handler @@ -103,7 +103,7 @@ func (engine *Engine) LoadHTMLGlob(pattern string) { 接下来,对原来的 `(*Context).HTML()`方法做了些小修改,使之支持根据模板文件名选择模板进行渲染。 -[day6-template/gee/context.go](https://github.com/geektutu/7days-golang/tree/master/gee-web/day6-template/gee) +[day6-template/gee/context.go](https://github.com/geektutu/7days-golang/tree/master/gee-web/day6-template) ```go func (c *Context) HTML(code int, name string, data interface{}) { @@ -140,7 +140,7 @@ func (c *Context) HTML(code int, name string, data interface{}) { ``` -[day6-template/main.go](https://github.com/geektutu/7days-golang/tree/master/gee-web/day6-template/gee) +[day6-template/main.go](https://github.com/geektutu/7days-golang/tree/master/gee-web/day6-template) ```go type student struct { From ba48e164a52905ba3c6c871a8b362890d8f80eb0 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Sun, 19 Jan 2020 23:49:42 +0800 Subject: [PATCH 011/122] add distributed cache day1 lru --- gee-cache/day1-lru/geecache/go.mod | 3 + gee-cache/day1-lru/geecache/lru/lru.go | 84 +++++++++++++++++++++ gee-cache/day1-lru/geecache/lru/lru_test.go | 16 ++++ 3 files changed, 103 insertions(+) create mode 100644 gee-cache/day1-lru/geecache/go.mod create mode 100644 gee-cache/day1-lru/geecache/lru/lru.go create mode 100644 gee-cache/day1-lru/geecache/lru/lru_test.go diff --git a/gee-cache/day1-lru/geecache/go.mod b/gee-cache/day1-lru/geecache/go.mod new file mode 100644 index 0000000..f9d454e --- /dev/null +++ b/gee-cache/day1-lru/geecache/go.mod @@ -0,0 +1,3 @@ +module geecache + +go 1.13 diff --git a/gee-cache/day1-lru/geecache/lru/lru.go b/gee-cache/day1-lru/geecache/lru/lru.go new file mode 100644 index 0000000..94f7116 --- /dev/null +++ b/gee-cache/day1-lru/geecache/lru/lru.go @@ -0,0 +1,84 @@ +package lru + +import ( + "container/list" + "unsafe" +) + +// Cache is a LRU cache. It is not safe for concurrent access. +type Cache struct { + maxBytes int + nbytes int + ll *list.List + cache map[string]*list.Element + // optional and executed when an entry is purged. + OnEvicted func(key string, value interface{}) +} + +type entry struct { + key string + value interface{} +} + +// New is the Constructor of Cache +func New(maxBytes int, onEvicted func(string, interface{})) *Cache { + return &Cache{ + maxBytes: maxBytes, + ll: list.New(), + cache: make(map[string]*list.Element), + } +} + +// Add adds a value to the cache. +func (c *Cache) Add(key string, value interface{}) { + if ele, ok := c.cache[key]; ok { + c.ll.MoveToFront(ele) + kv := ele.Value.(*entry) + kv.value = value + return + } + ele := c.ll.PushFront(&entry{key, value}) + c.cache[key] = ele + c.nbytes += len(key) + sizeof(value) + + for c.maxBytes != 0 && c.maxBytes < c.nbytes { + c.RemoveOldest() + } +} + +// Get look ups a key's value +func (c *Cache) Get(key string) (value interface{}, ok bool) { + if ele, ok := c.cache[key]; ok { + c.ll.MoveToFront(ele) + kv := ele.Value.(*entry) + return kv.value, true + } + return +} + +// RemoveOldest removes the oldest item +func (c *Cache) RemoveOldest() { + ele := c.ll.Back() + if ele != nil { + c.ll.Remove(ele) + kv := ele.Value.(*entry) + delete(c.cache, kv.key) + c.nbytes -= len(kv.key) + sizeof(kv.value) + if c.OnEvicted != nil { + c.OnEvicted(kv.key, kv.value) + } + } +} + +// Value is optional interface for the value +// if it's not implemented, use unsafe.Sizeof to count +type Value interface { + Len() int // count how many bytes it takes +} + +func sizeof(value interface{}) int { + if m, ok := value.(Value); ok { + return m.Len() + } + return int(unsafe.Sizeof(value)) +} diff --git a/gee-cache/day1-lru/geecache/lru/lru_test.go b/gee-cache/day1-lru/geecache/lru/lru_test.go new file mode 100644 index 0000000..a820af9 --- /dev/null +++ b/gee-cache/day1-lru/geecache/lru/lru_test.go @@ -0,0 +1,16 @@ +package lru + +import ( + "testing" +) + +func TestGet(t *testing.T) { + lru := New(0, nil) + lru.Add("key1", 1234) + if v, ok := lru.Get("key1"); !ok || v != 1234 { + t.Fatalf("cache hit key1=1234 failed") + } + if _, ok := lru.Get("key2"); ok { + t.Fatalf("cache miss key2 failed") + } +} From 4378b0de1b9ecf27abfda8c03f4e0be99d32a1bc Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Mon, 20 Jan 2020 01:08:56 +0800 Subject: [PATCH 012/122] add test func for lru --- gee-cache/day1-lru/geecache/lru/lru.go | 50 ++++++++++----------- gee-cache/day1-lru/geecache/lru/lru_test.go | 45 +++++++++++++++++-- 2 files changed, 65 insertions(+), 30 deletions(-) diff --git a/gee-cache/day1-lru/geecache/lru/lru.go b/gee-cache/day1-lru/geecache/lru/lru.go index 94f7116..906c9da 100644 --- a/gee-cache/day1-lru/geecache/lru/lru.go +++ b/gee-cache/day1-lru/geecache/lru/lru.go @@ -1,36 +1,40 @@ package lru -import ( - "container/list" - "unsafe" -) +import "container/list" // Cache is a LRU cache. It is not safe for concurrent access. type Cache struct { - maxBytes int - nbytes int + maxBytes int64 + nbytes int64 ll *list.List cache map[string]*list.Element // optional and executed when an entry is purged. - OnEvicted func(key string, value interface{}) + OnEvicted func(key string, value Value) } type entry struct { key string - value interface{} + value Value +} + +// Value is optional interface for the value +// if it's not implemented, use unsafe.Sizeof to count +type Value interface { + Len() int // count how many bytes it takes } // New is the Constructor of Cache -func New(maxBytes int, onEvicted func(string, interface{})) *Cache { +func New(maxBytes int64, onEvicted func(string, Value)) *Cache { return &Cache{ - maxBytes: maxBytes, - ll: list.New(), - cache: make(map[string]*list.Element), + maxBytes: maxBytes, + ll: list.New(), + cache: make(map[string]*list.Element), + OnEvicted: onEvicted, } } // Add adds a value to the cache. -func (c *Cache) Add(key string, value interface{}) { +func (c *Cache) Add(key string, value Value) { if ele, ok := c.cache[key]; ok { c.ll.MoveToFront(ele) kv := ele.Value.(*entry) @@ -39,7 +43,7 @@ func (c *Cache) Add(key string, value interface{}) { } ele := c.ll.PushFront(&entry{key, value}) c.cache[key] = ele - c.nbytes += len(key) + sizeof(value) + c.nbytes += int64(len(key)) + int64(value.Len()) for c.maxBytes != 0 && c.maxBytes < c.nbytes { c.RemoveOldest() @@ -47,7 +51,7 @@ func (c *Cache) Add(key string, value interface{}) { } // Get look ups a key's value -func (c *Cache) Get(key string) (value interface{}, ok bool) { +func (c *Cache) Get(key string) (value Value, ok bool) { if ele, ok := c.cache[key]; ok { c.ll.MoveToFront(ele) kv := ele.Value.(*entry) @@ -63,22 +67,14 @@ func (c *Cache) RemoveOldest() { c.ll.Remove(ele) kv := ele.Value.(*entry) delete(c.cache, kv.key) - c.nbytes -= len(kv.key) + sizeof(kv.value) + c.nbytes -= int64(len(kv.key)) + int64(kv.value.Len()) if c.OnEvicted != nil { c.OnEvicted(kv.key, kv.value) } } } -// Value is optional interface for the value -// if it's not implemented, use unsafe.Sizeof to count -type Value interface { - Len() int // count how many bytes it takes -} - -func sizeof(value interface{}) int { - if m, ok := value.(Value); ok { - return m.Len() - } - return int(unsafe.Sizeof(value)) +// Len the number of cache entries +func (c *Cache) Len() int { + return c.ll.Len() } diff --git a/gee-cache/day1-lru/geecache/lru/lru_test.go b/gee-cache/day1-lru/geecache/lru/lru_test.go index a820af9..7308322 100644 --- a/gee-cache/day1-lru/geecache/lru/lru_test.go +++ b/gee-cache/day1-lru/geecache/lru/lru_test.go @@ -1,16 +1,55 @@ package lru import ( + "reflect" "testing" ) +type String string + +func (d String) Len() int { + return len(d) +} + func TestGet(t *testing.T) { - lru := New(0, nil) - lru.Add("key1", 1234) - if v, ok := lru.Get("key1"); !ok || v != 1234 { + lru := New(int64(0), nil) + lru.Add("key1", String("1234")) + if v, ok := lru.Get("key1"); !ok || string(v.(String)) != "1234" { t.Fatalf("cache hit key1=1234 failed") } if _, ok := lru.Get("key2"); ok { t.Fatalf("cache miss key2 failed") } } + +func TestRemoveoldest(t *testing.T) { + k1, k2, k3 := "key1", "key2", "k3" + v1, v2, v3 := "value1", "value2", "v3" + cap := len(k1 + k2 + v1 + v2) + lru := New(int64(cap), nil) + lru.Add(k1, String(v1)) + lru.Add(k2, String(v2)) + lru.Add(k3, String(v3)) + + if _, ok := lru.Get("key1"); ok || lru.Len() != 2 { + t.Fatalf("Removeoldest key1 failed") + } +} + +func TestOnEvicted(t *testing.T) { + keys := make([]string, 0) + callback := func(key string, value Value) { + keys = append(keys, key) + } + lru := New(int64(10), callback) + lru.Add("key1", String("123456")) + lru.Add("k2", String("k2")) + lru.Add("k3", String("k3")) + lru.Add("k4", String("k4")) + + expect := []string{"key1", "k2"} + + if !reflect.DeepEqual(expect, keys) { + t.Fatalf("Call OnEvicted failed, expect keys equals to %s", expect) + } +} From 2b0d4527669e8aee25da538ba0e79ef4b0abd14b Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Thu, 23 Jan 2020 23:17:29 +0800 Subject: [PATCH 013/122] add go webassembly demo --- README.md | 11 +++++++- demo-wasm/.gitignore | 2 ++ demo-wasm/callback/Makefile | 11 ++++++++ demo-wasm/callback/index.html | 18 +++++++++++++ demo-wasm/callback/main.go | 32 +++++++++++++++++++++++ demo-wasm/hello-world/Makefile | 11 ++++++++ demo-wasm/hello-world/index.html | 12 +++++++++ demo-wasm/hello-world/main.go | 9 +++++++ demo-wasm/manipulate-dom/Makefile | 11 ++++++++ demo-wasm/manipulate-dom/index.html | 18 +++++++++++++ demo-wasm/manipulate-dom/main.go | 34 +++++++++++++++++++++++++ demo-wasm/register-functions/Makefile | 11 ++++++++ demo-wasm/register-functions/index.html | 18 +++++++++++++ demo-wasm/register-functions/main.go | 21 +++++++++++++++ 14 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 demo-wasm/.gitignore create mode 100644 demo-wasm/callback/Makefile create mode 100644 demo-wasm/callback/index.html create mode 100644 demo-wasm/callback/main.go create mode 100644 demo-wasm/hello-world/Makefile create mode 100644 demo-wasm/hello-world/index.html create mode 100644 demo-wasm/hello-world/main.go create mode 100644 demo-wasm/manipulate-dom/Makefile create mode 100644 demo-wasm/manipulate-dom/index.html create mode 100644 demo-wasm/manipulate-dom/main.go create mode 100644 demo-wasm/register-functions/Makefile create mode 100644 demo-wasm/register-functions/index.html create mode 100644 demo-wasm/register-functions/main.go diff --git a/README.md b/README.md index 7d11a7f..6b5b664 100644 --- a/README.md +++ b/README.md @@ -16,4 +16,13 @@ Gee 的设计与实现参考了Gin,[Go Gin简明教程](https://geektutu.com/p - [第四天:分组控制(Group)](https://geektutu.com/post/gee-day4.html),[Code - Github](gee-web/day4-group) - [第五天:中间件(Middleware)](https://geektutu.com/post/gee-day5.html),[Code - Github](gee-web/day5-middleware) - [第六天:HTML模板(Template)](https://geektutu.com/post/gee-day6.html),[Code - Github](gee-web/day6-template) -- [第七天:错误恢复(Panic Recover)](https://geektutu.com/post/gee-day7.html),[Code - Github](gee-web/day7-panic-recover) \ No newline at end of file +- [第七天:错误恢复(Panic Recover)](https://geektutu.com/post/gee-day7.html),[Code - Github](gee-web/day7-panic-recover) + +## WebAssembly Demo + +#### [教程地址](https://geektutu.com/post/quick-go-wasm.html) + +- [1. Hello World](demo-wasm/hello-world) +- [2. 注册函数](demo-wasm/hello-world) +- [3. 操作 DOM](demo-wasm/hello-world) +- [4. 回调函数](demo-wasm/hello-world) \ No newline at end of file diff --git a/demo-wasm/.gitignore b/demo-wasm/.gitignore new file mode 100644 index 0000000..b4d6ef2 --- /dev/null +++ b/demo-wasm/.gitignore @@ -0,0 +1,2 @@ +*.wasm +static \ No newline at end of file diff --git a/demo-wasm/callback/Makefile b/demo-wasm/callback/Makefile new file mode 100644 index 0000000..234573b --- /dev/null +++ b/demo-wasm/callback/Makefile @@ -0,0 +1,11 @@ +all: static/main.wasm static/wasm_exec.js +ifeq (, $(shell which goexec)) + go get -u github.com/shurcooL/goexec +endif + goexec 'http.ListenAndServe(`:9999`, http.FileServer(http.Dir(`.`)))' + +static/wasm_exec.js: + cp "$(shell go env GOROOT)/misc/wasm/wasm_exec.js" static + +static/main.wasm: main.go + GO111MODULE=auto GOOS=js GOARCH=wasm go build -o static/main.wasm . \ No newline at end of file diff --git a/demo-wasm/callback/index.html b/demo-wasm/callback/index.html new file mode 100644 index 0000000..da37fb4 --- /dev/null +++ b/demo-wasm/callback/index.html @@ -0,0 +1,18 @@ + + + + + + + + + + +

+ + + \ No newline at end of file diff --git a/demo-wasm/callback/main.go b/demo-wasm/callback/main.go new file mode 100644 index 0000000..e9c137e --- /dev/null +++ b/demo-wasm/callback/main.go @@ -0,0 +1,32 @@ +// main.go +package main + +import ( + "syscall/js" + "time" +) + +func fib(i int) int { + if i == 0 || i == 1 { + return 1 + } + return fib(i-1) + fib(i-2) +} + +func fibFunc(this js.Value, args []js.Value) interface{} { + callback := args[len(args)-1] + go func() { + time.Sleep(3 * time.Second) + v := fib(args[0].Int()) + callback.Invoke(v) + }() + + js.Global().Get("ans").Set("innerHTML", "Waiting 3s...") + return nil +} + +func main() { + done := make(chan int, 0) + js.Global().Set("fibFunc", js.FuncOf(fibFunc)) + <-done +} diff --git a/demo-wasm/hello-world/Makefile b/demo-wasm/hello-world/Makefile new file mode 100644 index 0000000..234573b --- /dev/null +++ b/demo-wasm/hello-world/Makefile @@ -0,0 +1,11 @@ +all: static/main.wasm static/wasm_exec.js +ifeq (, $(shell which goexec)) + go get -u github.com/shurcooL/goexec +endif + goexec 'http.ListenAndServe(`:9999`, http.FileServer(http.Dir(`.`)))' + +static/wasm_exec.js: + cp "$(shell go env GOROOT)/misc/wasm/wasm_exec.js" static + +static/main.wasm: main.go + GO111MODULE=auto GOOS=js GOARCH=wasm go build -o static/main.wasm . \ No newline at end of file diff --git a/demo-wasm/hello-world/index.html b/demo-wasm/hello-world/index.html new file mode 100644 index 0000000..c75710c --- /dev/null +++ b/demo-wasm/hello-world/index.html @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/demo-wasm/hello-world/main.go b/demo-wasm/hello-world/main.go new file mode 100644 index 0000000..8bcd95b --- /dev/null +++ b/demo-wasm/hello-world/main.go @@ -0,0 +1,9 @@ +// main.go +package main + +import "syscall/js" + +func main() { + alert := js.Global().Get("alert") + alert.Invoke("Hello World!") +} \ No newline at end of file diff --git a/demo-wasm/manipulate-dom/Makefile b/demo-wasm/manipulate-dom/Makefile new file mode 100644 index 0000000..234573b --- /dev/null +++ b/demo-wasm/manipulate-dom/Makefile @@ -0,0 +1,11 @@ +all: static/main.wasm static/wasm_exec.js +ifeq (, $(shell which goexec)) + go get -u github.com/shurcooL/goexec +endif + goexec 'http.ListenAndServe(`:9999`, http.FileServer(http.Dir(`.`)))' + +static/wasm_exec.js: + cp "$(shell go env GOROOT)/misc/wasm/wasm_exec.js" static + +static/main.wasm: main.go + GO111MODULE=auto GOOS=js GOARCH=wasm go build -o static/main.wasm . \ No newline at end of file diff --git a/demo-wasm/manipulate-dom/index.html b/demo-wasm/manipulate-dom/index.html new file mode 100644 index 0000000..60f2e5a --- /dev/null +++ b/demo-wasm/manipulate-dom/index.html @@ -0,0 +1,18 @@ + + + + + + + + + + +

1

+ + + \ No newline at end of file diff --git a/demo-wasm/manipulate-dom/main.go b/demo-wasm/manipulate-dom/main.go new file mode 100644 index 0000000..618cd85 --- /dev/null +++ b/demo-wasm/manipulate-dom/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "strconv" + "syscall/js" +) + +func fib(i int) int { + if i == 0 || i == 1 { + return 1 + } + return fib(i-1) + fib(i-2) +} + +var ( + document = js.Global().Get("document") + numEle = document.Call("getElementById", "num") + ansEle = document.Call("getElementById", "ans") + btnEle = js.Global().Get("btn") +) + +func fibFunc(this js.Value, args []js.Value) interface{} { + v := numEle.Get("value") + if num, err := strconv.Atoi(v.String()); err == nil { + ansEle.Set("innerHTML", js.ValueOf(fib(num))) + } + return nil +} + +func main() { + done := make(chan int, 0) + btnEle.Call("addEventListener", "click", js.FuncOf(fibFunc)) + <-done +} diff --git a/demo-wasm/register-functions/Makefile b/demo-wasm/register-functions/Makefile new file mode 100644 index 0000000..234573b --- /dev/null +++ b/demo-wasm/register-functions/Makefile @@ -0,0 +1,11 @@ +all: static/main.wasm static/wasm_exec.js +ifeq (, $(shell which goexec)) + go get -u github.com/shurcooL/goexec +endif + goexec 'http.ListenAndServe(`:9999`, http.FileServer(http.Dir(`.`)))' + +static/wasm_exec.js: + cp "$(shell go env GOROOT)/misc/wasm/wasm_exec.js" static + +static/main.wasm: main.go + GO111MODULE=auto GOOS=js GOARCH=wasm go build -o static/main.wasm . \ No newline at end of file diff --git a/demo-wasm/register-functions/index.html b/demo-wasm/register-functions/index.html new file mode 100644 index 0000000..9cc865c --- /dev/null +++ b/demo-wasm/register-functions/index.html @@ -0,0 +1,18 @@ + + + + + + + + + + +

1

+ + + \ No newline at end of file diff --git a/demo-wasm/register-functions/main.go b/demo-wasm/register-functions/main.go new file mode 100644 index 0000000..2e22e78 --- /dev/null +++ b/demo-wasm/register-functions/main.go @@ -0,0 +1,21 @@ +// main.go +package main + +import "syscall/js" + +func fib(i int) int { + if i == 0 || i == 1 { + return 1 + } + return fib(i-1) + fib(i-2) +} + +func fibFunc(this js.Value, args []js.Value) interface{} { + return js.ValueOf(fib(args[0].Int())) +} + +func main() { + done := make(chan int, 0) + js.Global().Set("fibFunc", js.FuncOf(fibFunc)) + <-done +} From 6253d09da86f9fb7b6d8acfe0d786f8ff6ec894c Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Sun, 2 Feb 2020 17:17:07 +0800 Subject: [PATCH 014/122] add group & sinks feature for geecache --- .../day2-single-node/geecache/byteview.go | 13 +++ gee-cache/day2-single-node/geecache/cache.go | 35 ++++++++ .../day2-single-node/geecache/geecache.go | 63 +++++++++++++++ .../geecache/geecache_test.go | 37 +++++++++ gee-cache/day2-single-node/geecache/go.mod | 3 + .../day2-single-node/geecache/lru/lru.go | 80 +++++++++++++++++++ .../day2-single-node/geecache/lru/lru_test.go | 55 +++++++++++++ gee-cache/day2-single-node/geecache/sinks.go | 36 +++++++++ 8 files changed, 322 insertions(+) create mode 100644 gee-cache/day2-single-node/geecache/byteview.go create mode 100644 gee-cache/day2-single-node/geecache/cache.go create mode 100644 gee-cache/day2-single-node/geecache/geecache.go create mode 100644 gee-cache/day2-single-node/geecache/geecache_test.go create mode 100644 gee-cache/day2-single-node/geecache/go.mod create mode 100644 gee-cache/day2-single-node/geecache/lru/lru.go create mode 100644 gee-cache/day2-single-node/geecache/lru/lru_test.go create mode 100644 gee-cache/day2-single-node/geecache/sinks.go diff --git a/gee-cache/day2-single-node/geecache/byteview.go b/gee-cache/day2-single-node/geecache/byteview.go new file mode 100644 index 0000000..b67fe3c --- /dev/null +++ b/gee-cache/day2-single-node/geecache/byteview.go @@ -0,0 +1,13 @@ +package geecache + +type ByteView struct { + b []byte +} + +func (v ByteView) Len() int { + return len(v.b) +} + +func (v ByteView) view() { + +} diff --git a/gee-cache/day2-single-node/geecache/cache.go b/gee-cache/day2-single-node/geecache/cache.go new file mode 100644 index 0000000..703d033 --- /dev/null +++ b/gee-cache/day2-single-node/geecache/cache.go @@ -0,0 +1,35 @@ +package geecache + +import ( + "geecache/lru" + "sync" +) + +type cache struct { + mu sync.RWMutex + lru *lru.Cache + cacheBytes int64 +} + +func (c *cache) add(key string, value ByteView) { + c.mu.RLock() + defer c.mu.RUnlock() + if c.lru == nil { + c.lru = lru.New(c.cacheBytes, nil) + } + c.lru.Add(key, value) +} + +func (c *cache) get(key string) (value ByteView, ok bool) { + c.mu.RLock() + defer c.mu.RUnlock() + if c.lru == nil { + return + } + + if v, ok := c.lru.Get(key); ok { + return v.(ByteView), ok + } + + return +} diff --git a/gee-cache/day2-single-node/geecache/geecache.go b/gee-cache/day2-single-node/geecache/geecache.go new file mode 100644 index 0000000..5a3214c --- /dev/null +++ b/gee-cache/day2-single-node/geecache/geecache.go @@ -0,0 +1,63 @@ +package geecache + +import "errors" + +type Group struct { + name string + getter Getter + mainCache cache +} + +type GetterFunc func(key string, dest Sink) error + +func (f GetterFunc) Get(key string, dest Sink) error { + return f(key, dest) +} + +type Getter interface { + Get(key string, dest Sink) error +} + +func NewGroup(name string, cacheBytes int64, getter Getter) *Group { + if getter == nil { + panic("nil Getter") + } + return &Group{ + name: name, + getter: getter, + mainCache: cache{cacheBytes: cacheBytes}, + } +} + +func (g *Group) load(key string, dest Sink) (ByteView, error) { + value, err := g.getLocally(key, dest) + if err != nil { + return value, err + } + + g.populateCache(key, value) + return value, nil +} + +func (g *Group) getLocally(key string, dest Sink) (ByteView, error) { + if err := g.getter.Get(key, dest); err != nil { + return ByteView{}, err + } + return dest.view() +} + +func (g *Group) populateCache(key string, value ByteView) { + g.mainCache.add(key, value) +} + +func (g *Group) Get(key string, dest Sink) error { + if dest == nil { + return errors.New("groupcache: nil dest Sink") + } + if v, ok := g.mainCache.get(key); ok { + return dest.SetBytes(v.b) + } + + _, err := g.load(key, dest) + return err +} diff --git a/gee-cache/day2-single-node/geecache/geecache_test.go b/gee-cache/day2-single-node/geecache/geecache_test.go new file mode 100644 index 0000000..f4e5299 --- /dev/null +++ b/gee-cache/day2-single-node/geecache/geecache_test.go @@ -0,0 +1,37 @@ +package geecache + +import ( + "errors" + "fmt" + "log" + "testing" +) + +var db = map[string]string{ + "Tom": "123", + "Jack": "456", + "Sam": "567", +} + +func TestGet(t *testing.T) { + gee := NewGroup("demo", 2<<10, GetterFunc(func(key string, dest Sink) error { + log.Printf("search key %s", key) + if v, ok := db[key]; ok { + return dest.SetBytes([]byte(v)) + } + return errors.New(fmt.Sprintf("%s not exist", key)) + })) + + var dst []byte + dest := AllocatingByteSliceSink(&dst) + + for k, v := range db { + if err := gee.Get(k, dest); err != nil || string(dst) != v { + t.Fatal("failed to get value of Tom") + } + } + + if err := gee.Get("unknown", dest); err == nil { + t.Fatalf("the value of unknow should be empty, but %s got", string(dst)) + } +} diff --git a/gee-cache/day2-single-node/geecache/go.mod b/gee-cache/day2-single-node/geecache/go.mod new file mode 100644 index 0000000..f9d454e --- /dev/null +++ b/gee-cache/day2-single-node/geecache/go.mod @@ -0,0 +1,3 @@ +module geecache + +go 1.13 diff --git a/gee-cache/day2-single-node/geecache/lru/lru.go b/gee-cache/day2-single-node/geecache/lru/lru.go new file mode 100644 index 0000000..906c9da --- /dev/null +++ b/gee-cache/day2-single-node/geecache/lru/lru.go @@ -0,0 +1,80 @@ +package lru + +import "container/list" + +// Cache is a LRU cache. It is not safe for concurrent access. +type Cache struct { + maxBytes int64 + nbytes int64 + ll *list.List + cache map[string]*list.Element + // optional and executed when an entry is purged. + OnEvicted func(key string, value Value) +} + +type entry struct { + key string + value Value +} + +// Value is optional interface for the value +// if it's not implemented, use unsafe.Sizeof to count +type Value interface { + Len() int // count how many bytes it takes +} + +// New is the Constructor of Cache +func New(maxBytes int64, onEvicted func(string, Value)) *Cache { + return &Cache{ + maxBytes: maxBytes, + ll: list.New(), + cache: make(map[string]*list.Element), + OnEvicted: onEvicted, + } +} + +// Add adds a value to the cache. +func (c *Cache) Add(key string, value Value) { + if ele, ok := c.cache[key]; ok { + c.ll.MoveToFront(ele) + kv := ele.Value.(*entry) + kv.value = value + return + } + ele := c.ll.PushFront(&entry{key, value}) + c.cache[key] = ele + c.nbytes += int64(len(key)) + int64(value.Len()) + + for c.maxBytes != 0 && c.maxBytes < c.nbytes { + c.RemoveOldest() + } +} + +// Get look ups a key's value +func (c *Cache) Get(key string) (value Value, ok bool) { + if ele, ok := c.cache[key]; ok { + c.ll.MoveToFront(ele) + kv := ele.Value.(*entry) + return kv.value, true + } + return +} + +// RemoveOldest removes the oldest item +func (c *Cache) RemoveOldest() { + ele := c.ll.Back() + if ele != nil { + c.ll.Remove(ele) + kv := ele.Value.(*entry) + delete(c.cache, kv.key) + c.nbytes -= int64(len(kv.key)) + int64(kv.value.Len()) + if c.OnEvicted != nil { + c.OnEvicted(kv.key, kv.value) + } + } +} + +// Len the number of cache entries +func (c *Cache) Len() int { + return c.ll.Len() +} diff --git a/gee-cache/day2-single-node/geecache/lru/lru_test.go b/gee-cache/day2-single-node/geecache/lru/lru_test.go new file mode 100644 index 0000000..7308322 --- /dev/null +++ b/gee-cache/day2-single-node/geecache/lru/lru_test.go @@ -0,0 +1,55 @@ +package lru + +import ( + "reflect" + "testing" +) + +type String string + +func (d String) Len() int { + return len(d) +} + +func TestGet(t *testing.T) { + lru := New(int64(0), nil) + lru.Add("key1", String("1234")) + if v, ok := lru.Get("key1"); !ok || string(v.(String)) != "1234" { + t.Fatalf("cache hit key1=1234 failed") + } + if _, ok := lru.Get("key2"); ok { + t.Fatalf("cache miss key2 failed") + } +} + +func TestRemoveoldest(t *testing.T) { + k1, k2, k3 := "key1", "key2", "k3" + v1, v2, v3 := "value1", "value2", "v3" + cap := len(k1 + k2 + v1 + v2) + lru := New(int64(cap), nil) + lru.Add(k1, String(v1)) + lru.Add(k2, String(v2)) + lru.Add(k3, String(v3)) + + if _, ok := lru.Get("key1"); ok || lru.Len() != 2 { + t.Fatalf("Removeoldest key1 failed") + } +} + +func TestOnEvicted(t *testing.T) { + keys := make([]string, 0) + callback := func(key string, value Value) { + keys = append(keys, key) + } + lru := New(int64(10), callback) + lru.Add("key1", String("123456")) + lru.Add("k2", String("k2")) + lru.Add("k3", String("k3")) + lru.Add("k4", String("k4")) + + expect := []string{"key1", "k2"} + + if !reflect.DeepEqual(expect, keys) { + t.Fatalf("Call OnEvicted failed, expect keys equals to %s", expect) + } +} diff --git a/gee-cache/day2-single-node/geecache/sinks.go b/gee-cache/day2-single-node/geecache/sinks.go new file mode 100644 index 0000000..6e528c6 --- /dev/null +++ b/gee-cache/day2-single-node/geecache/sinks.go @@ -0,0 +1,36 @@ +package geecache + +import "errors" + +type Sink interface { + SetBytes(v []byte) error + view() (ByteView, error) +} + +type allocBytesSink struct { + dst *[]byte + v ByteView +} + +func AllocatingByteSliceSink(dst *[]byte) Sink { + return &allocBytesSink{dst: dst} +} + +func cloneBytes(b []byte) []byte { + c := make([]byte, len(b)) + copy(c, b) + return c +} + +func (s *allocBytesSink) SetBytes(b []byte) error { + *s.dst = cloneBytes(b) + s.v.b = b + return nil +} + +func (s *allocBytesSink) view() (ByteView, error) { + if s.v.b == nil { + return ByteView{}, errors.New("byteview not set") + } + return s.v, nil +} From 3091a10a7859ec019e539474bca554b9e10ac43a Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Mon, 3 Feb 2020 00:39:21 +0800 Subject: [PATCH 015/122] remove sinks --- .../day2-single-node/geecache/byteview.go | 10 +++- .../day2-single-node/geecache/geecache.go | 58 +++++++++---------- .../geecache/geecache_test.go | 17 +++--- gee-cache/day2-single-node/geecache/sinks.go | 36 ------------ 4 files changed, 44 insertions(+), 77 deletions(-) delete mode 100644 gee-cache/day2-single-node/geecache/sinks.go diff --git a/gee-cache/day2-single-node/geecache/byteview.go b/gee-cache/day2-single-node/geecache/byteview.go index b67fe3c..a51394f 100644 --- a/gee-cache/day2-single-node/geecache/byteview.go +++ b/gee-cache/day2-single-node/geecache/byteview.go @@ -1,13 +1,21 @@ package geecache +// A ByteView holds an immutable view of bytes. type ByteView struct { b []byte } +// Len returns the view's length func (v ByteView) Len() int { return len(v.b) } -func (v ByteView) view() { +// ByteSlice returns a copy of the data as a byte slice. +func (v ByteView) ByteSlice() []byte { + return cloneBytes(v.b) +} +// String returns the data as a string, making a copy if necessary. +func (v ByteView) String() string { + return string(v.b) } diff --git a/gee-cache/day2-single-node/geecache/geecache.go b/gee-cache/day2-single-node/geecache/geecache.go index 5a3214c..2b51565 100644 --- a/gee-cache/day2-single-node/geecache/geecache.go +++ b/gee-cache/day2-single-node/geecache/geecache.go @@ -1,23 +1,26 @@ package geecache -import "errors" - +// A Group is a cache namespace and associated data loaded spread over type Group struct { name string getter Getter mainCache cache } -type GetterFunc func(key string, dest Sink) error - -func (f GetterFunc) Get(key string, dest Sink) error { - return f(key, dest) +// A Getter loads data for a key. +type Getter interface { + Get(key string) ([]byte, error) } -type Getter interface { - Get(key string, dest Sink) error +// A GetterFunc implements Getter with a function. +type GetterFunc func(key string) ([]byte, error) + +// Get implements Getter interface function +func (f GetterFunc) Get(key string) ([]byte, error) { + return f(key) } +// NewGroup create a new instance of Group func NewGroup(name string, cacheBytes int64, getter Getter) *Group { if getter == nil { panic("nil Getter") @@ -29,35 +32,30 @@ func NewGroup(name string, cacheBytes int64, getter Getter) *Group { } } -func (g *Group) load(key string, dest Sink) (ByteView, error) { - value, err := g.getLocally(key, dest) - if err != nil { - return value, err +// Get value for a key from cache +func (g *Group) Get(key string) (ByteView, error) { + if v, ok := g.mainCache.get(key); ok { + return v, nil } - g.populateCache(key, value) - return value, nil + return g.load(key) +} + +func cloneBytes(b []byte) []byte { + c := make([]byte, len(b)) + copy(c, b) + return c } -func (g *Group) getLocally(key string, dest Sink) (ByteView, error) { - if err := g.getter.Get(key, dest); err != nil { - return ByteView{}, err +func (g *Group) load(key string) (value ByteView, err error) { + bytes, err := g.getter.Get(key) + if err == nil { + value = ByteView{cloneBytes(bytes)} + g.populateCache(key, value) } - return dest.view() + return } func (g *Group) populateCache(key string, value ByteView) { g.mainCache.add(key, value) } - -func (g *Group) Get(key string, dest Sink) error { - if dest == nil { - return errors.New("groupcache: nil dest Sink") - } - if v, ok := g.mainCache.get(key); ok { - return dest.SetBytes(v.b) - } - - _, err := g.load(key, dest) - return err -} diff --git a/gee-cache/day2-single-node/geecache/geecache_test.go b/gee-cache/day2-single-node/geecache/geecache_test.go index f4e5299..64feb90 100644 --- a/gee-cache/day2-single-node/geecache/geecache_test.go +++ b/gee-cache/day2-single-node/geecache/geecache_test.go @@ -1,7 +1,6 @@ package geecache import ( - "errors" "fmt" "log" "testing" @@ -14,24 +13,22 @@ var db = map[string]string{ } func TestGet(t *testing.T) { - gee := NewGroup("demo", 2<<10, GetterFunc(func(key string, dest Sink) error { + gee := NewGroup("demo", 2<<10, GetterFunc(func(key string) ([]byte, error) { log.Printf("search key %s", key) if v, ok := db[key]; ok { - return dest.SetBytes([]byte(v)) + return []byte(v), nil } - return errors.New(fmt.Sprintf("%s not exist", key)) + return nil, fmt.Errorf("%s not exist", key) })) - var dst []byte - dest := AllocatingByteSliceSink(&dst) - for k, v := range db { - if err := gee.Get(k, dest); err != nil || string(dst) != v { + view, err := gee.Get(k) + if err != nil || view.String() != v { t.Fatal("failed to get value of Tom") } } - if err := gee.Get("unknown", dest); err == nil { - t.Fatalf("the value of unknow should be empty, but %s got", string(dst)) + if view, err := gee.Get("unknown"); err == nil { + t.Fatalf("the value of unknow should be empty, but %s got", view) } } diff --git a/gee-cache/day2-single-node/geecache/sinks.go b/gee-cache/day2-single-node/geecache/sinks.go deleted file mode 100644 index 6e528c6..0000000 --- a/gee-cache/day2-single-node/geecache/sinks.go +++ /dev/null @@ -1,36 +0,0 @@ -package geecache - -import "errors" - -type Sink interface { - SetBytes(v []byte) error - view() (ByteView, error) -} - -type allocBytesSink struct { - dst *[]byte - v ByteView -} - -func AllocatingByteSliceSink(dst *[]byte) Sink { - return &allocBytesSink{dst: dst} -} - -func cloneBytes(b []byte) []byte { - c := make([]byte, len(b)) - copy(c, b) - return c -} - -func (s *allocBytesSink) SetBytes(b []byte) error { - *s.dst = cloneBytes(b) - s.v.b = b - return nil -} - -func (s *allocBytesSink) view() (ByteView, error) { - if s.v.b == nil { - return ByteView{}, errors.New("byteview not set") - } - return s.v, nil -} From c822a1ab029a3ee24972f0884a6010f805b675de Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Mon, 3 Feb 2020 00:45:50 +0800 Subject: [PATCH 016/122] fix lru Value comments --- gee-cache/day1-lru/geecache/lru/lru.go | 5 ++--- gee-cache/day2-single-node/geecache/lru/lru.go | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/gee-cache/day1-lru/geecache/lru/lru.go b/gee-cache/day1-lru/geecache/lru/lru.go index 906c9da..81eee43 100644 --- a/gee-cache/day1-lru/geecache/lru/lru.go +++ b/gee-cache/day1-lru/geecache/lru/lru.go @@ -17,10 +17,9 @@ type entry struct { value Value } -// Value is optional interface for the value -// if it's not implemented, use unsafe.Sizeof to count +// Value use Len to count how many bytes it takes type Value interface { - Len() int // count how many bytes it takes + Len() int } // New is the Constructor of Cache diff --git a/gee-cache/day2-single-node/geecache/lru/lru.go b/gee-cache/day2-single-node/geecache/lru/lru.go index 906c9da..81eee43 100644 --- a/gee-cache/day2-single-node/geecache/lru/lru.go +++ b/gee-cache/day2-single-node/geecache/lru/lru.go @@ -17,10 +17,9 @@ type entry struct { value Value } -// Value is optional interface for the value -// if it's not implemented, use unsafe.Sizeof to count +// Value use Len to count how many bytes it takes type Value interface { - Len() int // count how many bytes it takes + Len() int } // New is the Constructor of Cache From cd12513aebeead6d2b3be12ab721dd8384468b74 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Mon, 3 Feb 2020 00:59:05 +0800 Subject: [PATCH 017/122] add geecache day1 & day2 to README --- README.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6b5b664..0aa3323 100644 --- a/README.md +++ b/README.md @@ -18,11 +18,16 @@ Gee 的设计与实现参考了Gin,[Go Gin简明教程](https://geektutu.com/p - [第六天:HTML模板(Template)](https://geektutu.com/post/gee-day6.html),[Code - Github](gee-web/day6-template) - [第七天:错误恢复(Panic Recover)](https://geektutu.com/post/gee-day7.html),[Code - Github](gee-web/day7-panic-recover) +## 7天用Go从零实现分布式缓存GeeCache + +- 第一天:LRU 缓存策略,[Code - Github](gee-cache/day1-lru) +- 第二天:单节点缓存,[Code - Github](gee-cache/day2-single-node) + ## WebAssembly Demo #### [教程地址](https://geektutu.com/post/quick-go-wasm.html) - [1. Hello World](demo-wasm/hello-world) -- [2. 注册函数](demo-wasm/hello-world) -- [3. 操作 DOM](demo-wasm/hello-world) -- [4. 回调函数](demo-wasm/hello-world) \ No newline at end of file +- [2. 注册函数](demo-wasm/register-functions) +- [3. 操作 DOM](demo-wasm/manipulate-dom) +- [4. 回调函数](demo-wasm/callback) \ No newline at end of file From 021ec73de6a5731db6fa5b79734e66f802a55a49 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Mon, 3 Feb 2020 17:59:18 +0800 Subject: [PATCH 018/122] day3 add http server for geecache --- .../day2-single-node/geecache/geecache.go | 22 ++++- .../geecache/geecache_test.go | 32 +++++--- .../day3-http-server/geecache/byteview.go | 21 +++++ gee-cache/day3-http-server/geecache/cache.go | 35 ++++++++ .../day3-http-server/geecache/geecache.go | 81 +++++++++++++++++++ .../geecache/geecache_test.go | 48 +++++++++++ gee-cache/day3-http-server/geecache/go.mod | 3 + gee-cache/day3-http-server/geecache/http.go | 56 +++++++++++++ .../day3-http-server/geecache/lru/lru.go | 79 ++++++++++++++++++ .../day3-http-server/geecache/lru/lru_test.go | 55 +++++++++++++ gee-cache/day3-http-server/go.mod | 7 ++ gee-cache/day3-http-server/main.go | 30 +++++++ 12 files changed, 459 insertions(+), 10 deletions(-) create mode 100644 gee-cache/day3-http-server/geecache/byteview.go create mode 100644 gee-cache/day3-http-server/geecache/cache.go create mode 100644 gee-cache/day3-http-server/geecache/geecache.go create mode 100644 gee-cache/day3-http-server/geecache/geecache_test.go create mode 100644 gee-cache/day3-http-server/geecache/go.mod create mode 100644 gee-cache/day3-http-server/geecache/http.go create mode 100644 gee-cache/day3-http-server/geecache/lru/lru.go create mode 100644 gee-cache/day3-http-server/geecache/lru/lru_test.go create mode 100644 gee-cache/day3-http-server/go.mod create mode 100644 gee-cache/day3-http-server/main.go diff --git a/gee-cache/day2-single-node/geecache/geecache.go b/gee-cache/day2-single-node/geecache/geecache.go index 2b51565..4273256 100644 --- a/gee-cache/day2-single-node/geecache/geecache.go +++ b/gee-cache/day2-single-node/geecache/geecache.go @@ -1,5 +1,7 @@ package geecache +import "sync" + // A Group is a cache namespace and associated data loaded spread over type Group struct { name string @@ -20,16 +22,34 @@ func (f GetterFunc) Get(key string) ([]byte, error) { return f(key) } +var ( + mu sync.RWMutex + groups = make(map[string]*Group) +) + // NewGroup create a new instance of Group func NewGroup(name string, cacheBytes int64, getter Getter) *Group { if getter == nil { panic("nil Getter") } - return &Group{ + mu.Lock() + defer mu.Unlock() + g := &Group{ name: name, getter: getter, mainCache: cache{cacheBytes: cacheBytes}, } + groups[name] = g + return g +} + +// GetGroup returns the named group previously created with NewGroup, or +// nil if there's no such group. +func GetGroup(name string) *Group { + mu.RLock() + g := groups[name] + mu.RUnlock() + return g } // Get value for a key from cache diff --git a/gee-cache/day2-single-node/geecache/geecache_test.go b/gee-cache/day2-single-node/geecache/geecache_test.go index 64feb90..2bffb3f 100644 --- a/gee-cache/day2-single-node/geecache/geecache_test.go +++ b/gee-cache/day2-single-node/geecache/geecache_test.go @@ -7,19 +7,20 @@ import ( ) var db = map[string]string{ - "Tom": "123", - "Jack": "456", + "Tom": "630", + "Jack": "589", "Sam": "567", } func TestGet(t *testing.T) { - gee := NewGroup("demo", 2<<10, GetterFunc(func(key string) ([]byte, error) { - log.Printf("search key %s", key) - if v, ok := db[key]; ok { - return []byte(v), nil - } - return nil, fmt.Errorf("%s not exist", key) - })) + gee := NewGroup("scores", 2<<10, GetterFunc( + func(key string) ([]byte, error) { + log.Println("[group scores] search key", key) + if v, ok := db[key]; ok { + return []byte(v), nil + } + return nil, fmt.Errorf("%s not exist", key) + })) for k, v := range db { view, err := gee.Get(k) @@ -32,3 +33,16 @@ func TestGet(t *testing.T) { t.Fatalf("the value of unknow should be empty, but %s got", view) } } + +func TestGetGroup(t *testing.T) { + groupName := "scores" + NewGroup(groupName, 2<<10, GetterFunc( + func(key string) (bytes []byte, err error) { return })) + if group := GetGroup(groupName); group == nil || group.name != groupName { + t.Fatalf("group %s not exist", groupName) + } + + if group := GetGroup(groupName + "111"); group != nil { + t.Fatalf("expect nil, but %s got", group.name) + } +} diff --git a/gee-cache/day3-http-server/geecache/byteview.go b/gee-cache/day3-http-server/geecache/byteview.go new file mode 100644 index 0000000..a51394f --- /dev/null +++ b/gee-cache/day3-http-server/geecache/byteview.go @@ -0,0 +1,21 @@ +package geecache + +// A ByteView holds an immutable view of bytes. +type ByteView struct { + b []byte +} + +// Len returns the view's length +func (v ByteView) Len() int { + return len(v.b) +} + +// ByteSlice returns a copy of the data as a byte slice. +func (v ByteView) ByteSlice() []byte { + return cloneBytes(v.b) +} + +// String returns the data as a string, making a copy if necessary. +func (v ByteView) String() string { + return string(v.b) +} diff --git a/gee-cache/day3-http-server/geecache/cache.go b/gee-cache/day3-http-server/geecache/cache.go new file mode 100644 index 0000000..703d033 --- /dev/null +++ b/gee-cache/day3-http-server/geecache/cache.go @@ -0,0 +1,35 @@ +package geecache + +import ( + "geecache/lru" + "sync" +) + +type cache struct { + mu sync.RWMutex + lru *lru.Cache + cacheBytes int64 +} + +func (c *cache) add(key string, value ByteView) { + c.mu.RLock() + defer c.mu.RUnlock() + if c.lru == nil { + c.lru = lru.New(c.cacheBytes, nil) + } + c.lru.Add(key, value) +} + +func (c *cache) get(key string) (value ByteView, ok bool) { + c.mu.RLock() + defer c.mu.RUnlock() + if c.lru == nil { + return + } + + if v, ok := c.lru.Get(key); ok { + return v.(ByteView), ok + } + + return +} diff --git a/gee-cache/day3-http-server/geecache/geecache.go b/gee-cache/day3-http-server/geecache/geecache.go new file mode 100644 index 0000000..4273256 --- /dev/null +++ b/gee-cache/day3-http-server/geecache/geecache.go @@ -0,0 +1,81 @@ +package geecache + +import "sync" + +// A Group is a cache namespace and associated data loaded spread over +type Group struct { + name string + getter Getter + mainCache cache +} + +// A Getter loads data for a key. +type Getter interface { + Get(key string) ([]byte, error) +} + +// A GetterFunc implements Getter with a function. +type GetterFunc func(key string) ([]byte, error) + +// Get implements Getter interface function +func (f GetterFunc) Get(key string) ([]byte, error) { + return f(key) +} + +var ( + mu sync.RWMutex + groups = make(map[string]*Group) +) + +// NewGroup create a new instance of Group +func NewGroup(name string, cacheBytes int64, getter Getter) *Group { + if getter == nil { + panic("nil Getter") + } + mu.Lock() + defer mu.Unlock() + g := &Group{ + name: name, + getter: getter, + mainCache: cache{cacheBytes: cacheBytes}, + } + groups[name] = g + return g +} + +// GetGroup returns the named group previously created with NewGroup, or +// nil if there's no such group. +func GetGroup(name string) *Group { + mu.RLock() + g := groups[name] + mu.RUnlock() + return g +} + +// Get value for a key from cache +func (g *Group) Get(key string) (ByteView, error) { + if v, ok := g.mainCache.get(key); ok { + return v, nil + } + + return g.load(key) +} + +func cloneBytes(b []byte) []byte { + c := make([]byte, len(b)) + copy(c, b) + return c +} + +func (g *Group) load(key string) (value ByteView, err error) { + bytes, err := g.getter.Get(key) + if err == nil { + value = ByteView{cloneBytes(bytes)} + g.populateCache(key, value) + } + return +} + +func (g *Group) populateCache(key string, value ByteView) { + g.mainCache.add(key, value) +} diff --git a/gee-cache/day3-http-server/geecache/geecache_test.go b/gee-cache/day3-http-server/geecache/geecache_test.go new file mode 100644 index 0000000..2bffb3f --- /dev/null +++ b/gee-cache/day3-http-server/geecache/geecache_test.go @@ -0,0 +1,48 @@ +package geecache + +import ( + "fmt" + "log" + "testing" +) + +var db = map[string]string{ + "Tom": "630", + "Jack": "589", + "Sam": "567", +} + +func TestGet(t *testing.T) { + gee := NewGroup("scores", 2<<10, GetterFunc( + func(key string) ([]byte, error) { + log.Println("[group scores] search key", key) + if v, ok := db[key]; ok { + return []byte(v), nil + } + return nil, fmt.Errorf("%s not exist", key) + })) + + for k, v := range db { + view, err := gee.Get(k) + if err != nil || view.String() != v { + t.Fatal("failed to get value of Tom") + } + } + + if view, err := gee.Get("unknown"); err == nil { + t.Fatalf("the value of unknow should be empty, but %s got", view) + } +} + +func TestGetGroup(t *testing.T) { + groupName := "scores" + NewGroup(groupName, 2<<10, GetterFunc( + func(key string) (bytes []byte, err error) { return })) + if group := GetGroup(groupName); group == nil || group.name != groupName { + t.Fatalf("group %s not exist", groupName) + } + + if group := GetGroup(groupName + "111"); group != nil { + t.Fatalf("expect nil, but %s got", group.name) + } +} diff --git a/gee-cache/day3-http-server/geecache/go.mod b/gee-cache/day3-http-server/geecache/go.mod new file mode 100644 index 0000000..f9d454e --- /dev/null +++ b/gee-cache/day3-http-server/geecache/go.mod @@ -0,0 +1,3 @@ +module geecache + +go 1.13 diff --git a/gee-cache/day3-http-server/geecache/http.go b/gee-cache/day3-http-server/geecache/http.go new file mode 100644 index 0000000..4d33d5d --- /dev/null +++ b/gee-cache/day3-http-server/geecache/http.go @@ -0,0 +1,56 @@ +package geecache + +import ( + "log" + "net/http" + "strings" +) + +const defaultBasePath = "/_geecache/" + +// HTTPPool implements PeerPicker for a pool of HTTP peers. +type HTTPPool struct { + // this peer's base URL, e.g. "https://example.net:8000" + self string + basePath string +} + +// NewHTTPPool initializes an HTTP pool of peers, and registers itself as a PeerPicker. +func NewHTTPPool(self string) *HTTPPool { + return &HTTPPool{ + self: self, + basePath: defaultBasePath, + } +} + +// ServeHTTP handle all http requests +func (p *HTTPPool) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if !strings.HasPrefix(r.URL.Path, p.basePath) { + panic("HTTPPool serving unexpected path: " + r.URL.Path) + } + log.Println("[geecache server]", r.Method, r.URL.Path) + // /// required + parts := strings.SplitN(r.URL.Path[len(p.basePath):], "/", 2) + if len(parts) != 2 { + http.Error(w, "bad request", http.StatusBadRequest) + return + } + + groupName := parts[0] + key := parts[1] + + group := GetGroup(groupName) + if group == nil { + http.Error(w, "no such group: "+groupName, http.StatusNotFound) + return + } + + view, err := group.Get(key) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/octet-stream") + w.Write(view.ByteSlice()) +} diff --git a/gee-cache/day3-http-server/geecache/lru/lru.go b/gee-cache/day3-http-server/geecache/lru/lru.go new file mode 100644 index 0000000..81eee43 --- /dev/null +++ b/gee-cache/day3-http-server/geecache/lru/lru.go @@ -0,0 +1,79 @@ +package lru + +import "container/list" + +// Cache is a LRU cache. It is not safe for concurrent access. +type Cache struct { + maxBytes int64 + nbytes int64 + ll *list.List + cache map[string]*list.Element + // optional and executed when an entry is purged. + OnEvicted func(key string, value Value) +} + +type entry struct { + key string + value Value +} + +// Value use Len to count how many bytes it takes +type Value interface { + Len() int +} + +// New is the Constructor of Cache +func New(maxBytes int64, onEvicted func(string, Value)) *Cache { + return &Cache{ + maxBytes: maxBytes, + ll: list.New(), + cache: make(map[string]*list.Element), + OnEvicted: onEvicted, + } +} + +// Add adds a value to the cache. +func (c *Cache) Add(key string, value Value) { + if ele, ok := c.cache[key]; ok { + c.ll.MoveToFront(ele) + kv := ele.Value.(*entry) + kv.value = value + return + } + ele := c.ll.PushFront(&entry{key, value}) + c.cache[key] = ele + c.nbytes += int64(len(key)) + int64(value.Len()) + + for c.maxBytes != 0 && c.maxBytes < c.nbytes { + c.RemoveOldest() + } +} + +// Get look ups a key's value +func (c *Cache) Get(key string) (value Value, ok bool) { + if ele, ok := c.cache[key]; ok { + c.ll.MoveToFront(ele) + kv := ele.Value.(*entry) + return kv.value, true + } + return +} + +// RemoveOldest removes the oldest item +func (c *Cache) RemoveOldest() { + ele := c.ll.Back() + if ele != nil { + c.ll.Remove(ele) + kv := ele.Value.(*entry) + delete(c.cache, kv.key) + c.nbytes -= int64(len(kv.key)) + int64(kv.value.Len()) + if c.OnEvicted != nil { + c.OnEvicted(kv.key, kv.value) + } + } +} + +// Len the number of cache entries +func (c *Cache) Len() int { + return c.ll.Len() +} diff --git a/gee-cache/day3-http-server/geecache/lru/lru_test.go b/gee-cache/day3-http-server/geecache/lru/lru_test.go new file mode 100644 index 0000000..7308322 --- /dev/null +++ b/gee-cache/day3-http-server/geecache/lru/lru_test.go @@ -0,0 +1,55 @@ +package lru + +import ( + "reflect" + "testing" +) + +type String string + +func (d String) Len() int { + return len(d) +} + +func TestGet(t *testing.T) { + lru := New(int64(0), nil) + lru.Add("key1", String("1234")) + if v, ok := lru.Get("key1"); !ok || string(v.(String)) != "1234" { + t.Fatalf("cache hit key1=1234 failed") + } + if _, ok := lru.Get("key2"); ok { + t.Fatalf("cache miss key2 failed") + } +} + +func TestRemoveoldest(t *testing.T) { + k1, k2, k3 := "key1", "key2", "k3" + v1, v2, v3 := "value1", "value2", "v3" + cap := len(k1 + k2 + v1 + v2) + lru := New(int64(cap), nil) + lru.Add(k1, String(v1)) + lru.Add(k2, String(v2)) + lru.Add(k3, String(v3)) + + if _, ok := lru.Get("key1"); ok || lru.Len() != 2 { + t.Fatalf("Removeoldest key1 failed") + } +} + +func TestOnEvicted(t *testing.T) { + keys := make([]string, 0) + callback := func(key string, value Value) { + keys = append(keys, key) + } + lru := New(int64(10), callback) + lru.Add("key1", String("123456")) + lru.Add("k2", String("k2")) + lru.Add("k3", String("k3")) + lru.Add("k4", String("k4")) + + expect := []string{"key1", "k2"} + + if !reflect.DeepEqual(expect, keys) { + t.Fatalf("Call OnEvicted failed, expect keys equals to %s", expect) + } +} diff --git a/gee-cache/day3-http-server/go.mod b/gee-cache/day3-http-server/go.mod new file mode 100644 index 0000000..d0fd3ba --- /dev/null +++ b/gee-cache/day3-http-server/go.mod @@ -0,0 +1,7 @@ +module example + +go 1.13 + +require geecache v0.0.0 + +replace geecache => ./geecache diff --git a/gee-cache/day3-http-server/main.go b/gee-cache/day3-http-server/main.go new file mode 100644 index 0000000..570a90a --- /dev/null +++ b/gee-cache/day3-http-server/main.go @@ -0,0 +1,30 @@ +package main + +import ( + "fmt" + "geecache" + "log" + "net/http" +) + +var db = map[string]string{ + "Tom": "630", + "Jack": "589", + "Sam": "567", +} + +func main() { + geecache.NewGroup("scores", 2<<10, geecache.GetterFunc( + func(key string) ([]byte, error) { + log.Println("[group scores] search key", key) + if v, ok := db[key]; ok { + return []byte(v), nil + } + return nil, fmt.Errorf("%s not exist", key) + })) + + addr := "localhost:9999" + peers := geecache.NewHTTPPool(addr) + log.Println("geecache is running at", addr) + log.Fatal(http.ListenAndServe(addr, peers)) +} From 8ac52d7f3904e909c72a3a7bb1e5130f3cdb6285 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Mon, 3 Feb 2020 18:39:53 +0800 Subject: [PATCH 019/122] day4 add consistent hash algorithm --- .../day4-consistent-hash/geecache/byteview.go | 21 +++++ .../day4-consistent-hash/geecache/cache.go | 35 ++++++++ .../geecache/consistenthash/consistenthash.go | 58 +++++++++++++ .../consistenthash/consistenthash_test.go | 43 ++++++++++ .../day4-consistent-hash/geecache/geecache.go | 81 +++++++++++++++++++ .../geecache/geecache_test.go | 48 +++++++++++ .../day4-consistent-hash/geecache/go.mod | 3 + .../day4-consistent-hash/geecache/http.go | 56 +++++++++++++ .../day4-consistent-hash/geecache/lru/lru.go | 79 ++++++++++++++++++ .../geecache/lru/lru_test.go | 55 +++++++++++++ gee-cache/day4-consistent-hash/go.mod | 7 ++ gee-cache/day4-consistent-hash/main.go | 30 +++++++ 12 files changed, 516 insertions(+) create mode 100644 gee-cache/day4-consistent-hash/geecache/byteview.go create mode 100644 gee-cache/day4-consistent-hash/geecache/cache.go create mode 100644 gee-cache/day4-consistent-hash/geecache/consistenthash/consistenthash.go create mode 100644 gee-cache/day4-consistent-hash/geecache/consistenthash/consistenthash_test.go create mode 100644 gee-cache/day4-consistent-hash/geecache/geecache.go create mode 100644 gee-cache/day4-consistent-hash/geecache/geecache_test.go create mode 100644 gee-cache/day4-consistent-hash/geecache/go.mod create mode 100644 gee-cache/day4-consistent-hash/geecache/http.go create mode 100644 gee-cache/day4-consistent-hash/geecache/lru/lru.go create mode 100644 gee-cache/day4-consistent-hash/geecache/lru/lru_test.go create mode 100644 gee-cache/day4-consistent-hash/go.mod create mode 100644 gee-cache/day4-consistent-hash/main.go diff --git a/gee-cache/day4-consistent-hash/geecache/byteview.go b/gee-cache/day4-consistent-hash/geecache/byteview.go new file mode 100644 index 0000000..a51394f --- /dev/null +++ b/gee-cache/day4-consistent-hash/geecache/byteview.go @@ -0,0 +1,21 @@ +package geecache + +// A ByteView holds an immutable view of bytes. +type ByteView struct { + b []byte +} + +// Len returns the view's length +func (v ByteView) Len() int { + return len(v.b) +} + +// ByteSlice returns a copy of the data as a byte slice. +func (v ByteView) ByteSlice() []byte { + return cloneBytes(v.b) +} + +// String returns the data as a string, making a copy if necessary. +func (v ByteView) String() string { + return string(v.b) +} diff --git a/gee-cache/day4-consistent-hash/geecache/cache.go b/gee-cache/day4-consistent-hash/geecache/cache.go new file mode 100644 index 0000000..703d033 --- /dev/null +++ b/gee-cache/day4-consistent-hash/geecache/cache.go @@ -0,0 +1,35 @@ +package geecache + +import ( + "geecache/lru" + "sync" +) + +type cache struct { + mu sync.RWMutex + lru *lru.Cache + cacheBytes int64 +} + +func (c *cache) add(key string, value ByteView) { + c.mu.RLock() + defer c.mu.RUnlock() + if c.lru == nil { + c.lru = lru.New(c.cacheBytes, nil) + } + c.lru.Add(key, value) +} + +func (c *cache) get(key string) (value ByteView, ok bool) { + c.mu.RLock() + defer c.mu.RUnlock() + if c.lru == nil { + return + } + + if v, ok := c.lru.Get(key); ok { + return v.(ByteView), ok + } + + return +} diff --git a/gee-cache/day4-consistent-hash/geecache/consistenthash/consistenthash.go b/gee-cache/day4-consistent-hash/geecache/consistenthash/consistenthash.go new file mode 100644 index 0000000..c8c9082 --- /dev/null +++ b/gee-cache/day4-consistent-hash/geecache/consistenthash/consistenthash.go @@ -0,0 +1,58 @@ +package consistenthash + +import ( + "hash/crc32" + "sort" + "strconv" +) + +// Hash maps bytes to uint32 +type Hash func(data []byte) uint32 + +// Map constains all hashed keys +type Map struct { + hash Hash + replicas int + keys []int // Sorted + hashMap map[int]string +} + +// New creates a Map instance +func New(replicas int, fn Hash) *Map { + m := &Map{ + replicas: replicas, + hash: fn, + hashMap: make(map[int]string), + } + if m.hash == nil { + m.hash = crc32.ChecksumIEEE + } + return m +} + +// Add adds some keys to the hash. +func (m *Map) Add(keys ...string) { + for _, key := range keys { + for i := 0; i < m.replicas; i++ { + hash := int(m.hash([]byte(strconv.Itoa(i) + key))) + m.keys = append(m.keys, hash) + m.hashMap[hash] = key + } + } + sort.Ints(m.keys) +} + +// Get gets the closest item in the hash to the provided key. +func (m *Map) Get(key string) string { + if len(m.keys) == 0 { + return "" + } + + hash := int(m.hash([]byte(key))) + // Binary search for appropriate replica. + idx := sort.Search(len(m.keys), func(i int) bool { + return m.keys[i] >= hash + }) + + return m.hashMap[m.keys[idx%len(m.keys)]] +} diff --git a/gee-cache/day4-consistent-hash/geecache/consistenthash/consistenthash_test.go b/gee-cache/day4-consistent-hash/geecache/consistenthash/consistenthash_test.go new file mode 100644 index 0000000..34e1275 --- /dev/null +++ b/gee-cache/day4-consistent-hash/geecache/consistenthash/consistenthash_test.go @@ -0,0 +1,43 @@ +package consistenthash + +import ( + "strconv" + "testing" +) + +func TestHashing(t *testing.T) { + hash := New(3, func(key []byte) uint32 { + i, _ := strconv.Atoi(string(key)) + return uint32(i) + }) + + // Given the above hash function, this will give replicas with "hashes": + // 2, 4, 6, 12, 14, 16, 22, 24, 26 + hash.Add("6", "4", "2") + + testCases := map[string]string{ + "2": "2", + "11": "2", + "23": "4", + "27": "2", + } + + for k, v := range testCases { + if hash.Get(k) != v { + t.Errorf("Asking for %s, should have yielded %s", k, v) + } + } + + // Adds 8, 18, 28 + hash.Add("8") + + // 27 should now map to 8. + testCases["27"] = "8" + + for k, v := range testCases { + if hash.Get(k) != v { + t.Errorf("Asking for %s, should have yielded %s", k, v) + } + } + +} diff --git a/gee-cache/day4-consistent-hash/geecache/geecache.go b/gee-cache/day4-consistent-hash/geecache/geecache.go new file mode 100644 index 0000000..4273256 --- /dev/null +++ b/gee-cache/day4-consistent-hash/geecache/geecache.go @@ -0,0 +1,81 @@ +package geecache + +import "sync" + +// A Group is a cache namespace and associated data loaded spread over +type Group struct { + name string + getter Getter + mainCache cache +} + +// A Getter loads data for a key. +type Getter interface { + Get(key string) ([]byte, error) +} + +// A GetterFunc implements Getter with a function. +type GetterFunc func(key string) ([]byte, error) + +// Get implements Getter interface function +func (f GetterFunc) Get(key string) ([]byte, error) { + return f(key) +} + +var ( + mu sync.RWMutex + groups = make(map[string]*Group) +) + +// NewGroup create a new instance of Group +func NewGroup(name string, cacheBytes int64, getter Getter) *Group { + if getter == nil { + panic("nil Getter") + } + mu.Lock() + defer mu.Unlock() + g := &Group{ + name: name, + getter: getter, + mainCache: cache{cacheBytes: cacheBytes}, + } + groups[name] = g + return g +} + +// GetGroup returns the named group previously created with NewGroup, or +// nil if there's no such group. +func GetGroup(name string) *Group { + mu.RLock() + g := groups[name] + mu.RUnlock() + return g +} + +// Get value for a key from cache +func (g *Group) Get(key string) (ByteView, error) { + if v, ok := g.mainCache.get(key); ok { + return v, nil + } + + return g.load(key) +} + +func cloneBytes(b []byte) []byte { + c := make([]byte, len(b)) + copy(c, b) + return c +} + +func (g *Group) load(key string) (value ByteView, err error) { + bytes, err := g.getter.Get(key) + if err == nil { + value = ByteView{cloneBytes(bytes)} + g.populateCache(key, value) + } + return +} + +func (g *Group) populateCache(key string, value ByteView) { + g.mainCache.add(key, value) +} diff --git a/gee-cache/day4-consistent-hash/geecache/geecache_test.go b/gee-cache/day4-consistent-hash/geecache/geecache_test.go new file mode 100644 index 0000000..2bffb3f --- /dev/null +++ b/gee-cache/day4-consistent-hash/geecache/geecache_test.go @@ -0,0 +1,48 @@ +package geecache + +import ( + "fmt" + "log" + "testing" +) + +var db = map[string]string{ + "Tom": "630", + "Jack": "589", + "Sam": "567", +} + +func TestGet(t *testing.T) { + gee := NewGroup("scores", 2<<10, GetterFunc( + func(key string) ([]byte, error) { + log.Println("[group scores] search key", key) + if v, ok := db[key]; ok { + return []byte(v), nil + } + return nil, fmt.Errorf("%s not exist", key) + })) + + for k, v := range db { + view, err := gee.Get(k) + if err != nil || view.String() != v { + t.Fatal("failed to get value of Tom") + } + } + + if view, err := gee.Get("unknown"); err == nil { + t.Fatalf("the value of unknow should be empty, but %s got", view) + } +} + +func TestGetGroup(t *testing.T) { + groupName := "scores" + NewGroup(groupName, 2<<10, GetterFunc( + func(key string) (bytes []byte, err error) { return })) + if group := GetGroup(groupName); group == nil || group.name != groupName { + t.Fatalf("group %s not exist", groupName) + } + + if group := GetGroup(groupName + "111"); group != nil { + t.Fatalf("expect nil, but %s got", group.name) + } +} diff --git a/gee-cache/day4-consistent-hash/geecache/go.mod b/gee-cache/day4-consistent-hash/geecache/go.mod new file mode 100644 index 0000000..f9d454e --- /dev/null +++ b/gee-cache/day4-consistent-hash/geecache/go.mod @@ -0,0 +1,3 @@ +module geecache + +go 1.13 diff --git a/gee-cache/day4-consistent-hash/geecache/http.go b/gee-cache/day4-consistent-hash/geecache/http.go new file mode 100644 index 0000000..4d33d5d --- /dev/null +++ b/gee-cache/day4-consistent-hash/geecache/http.go @@ -0,0 +1,56 @@ +package geecache + +import ( + "log" + "net/http" + "strings" +) + +const defaultBasePath = "/_geecache/" + +// HTTPPool implements PeerPicker for a pool of HTTP peers. +type HTTPPool struct { + // this peer's base URL, e.g. "https://example.net:8000" + self string + basePath string +} + +// NewHTTPPool initializes an HTTP pool of peers, and registers itself as a PeerPicker. +func NewHTTPPool(self string) *HTTPPool { + return &HTTPPool{ + self: self, + basePath: defaultBasePath, + } +} + +// ServeHTTP handle all http requests +func (p *HTTPPool) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if !strings.HasPrefix(r.URL.Path, p.basePath) { + panic("HTTPPool serving unexpected path: " + r.URL.Path) + } + log.Println("[geecache server]", r.Method, r.URL.Path) + // /// required + parts := strings.SplitN(r.URL.Path[len(p.basePath):], "/", 2) + if len(parts) != 2 { + http.Error(w, "bad request", http.StatusBadRequest) + return + } + + groupName := parts[0] + key := parts[1] + + group := GetGroup(groupName) + if group == nil { + http.Error(w, "no such group: "+groupName, http.StatusNotFound) + return + } + + view, err := group.Get(key) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/octet-stream") + w.Write(view.ByteSlice()) +} diff --git a/gee-cache/day4-consistent-hash/geecache/lru/lru.go b/gee-cache/day4-consistent-hash/geecache/lru/lru.go new file mode 100644 index 0000000..81eee43 --- /dev/null +++ b/gee-cache/day4-consistent-hash/geecache/lru/lru.go @@ -0,0 +1,79 @@ +package lru + +import "container/list" + +// Cache is a LRU cache. It is not safe for concurrent access. +type Cache struct { + maxBytes int64 + nbytes int64 + ll *list.List + cache map[string]*list.Element + // optional and executed when an entry is purged. + OnEvicted func(key string, value Value) +} + +type entry struct { + key string + value Value +} + +// Value use Len to count how many bytes it takes +type Value interface { + Len() int +} + +// New is the Constructor of Cache +func New(maxBytes int64, onEvicted func(string, Value)) *Cache { + return &Cache{ + maxBytes: maxBytes, + ll: list.New(), + cache: make(map[string]*list.Element), + OnEvicted: onEvicted, + } +} + +// Add adds a value to the cache. +func (c *Cache) Add(key string, value Value) { + if ele, ok := c.cache[key]; ok { + c.ll.MoveToFront(ele) + kv := ele.Value.(*entry) + kv.value = value + return + } + ele := c.ll.PushFront(&entry{key, value}) + c.cache[key] = ele + c.nbytes += int64(len(key)) + int64(value.Len()) + + for c.maxBytes != 0 && c.maxBytes < c.nbytes { + c.RemoveOldest() + } +} + +// Get look ups a key's value +func (c *Cache) Get(key string) (value Value, ok bool) { + if ele, ok := c.cache[key]; ok { + c.ll.MoveToFront(ele) + kv := ele.Value.(*entry) + return kv.value, true + } + return +} + +// RemoveOldest removes the oldest item +func (c *Cache) RemoveOldest() { + ele := c.ll.Back() + if ele != nil { + c.ll.Remove(ele) + kv := ele.Value.(*entry) + delete(c.cache, kv.key) + c.nbytes -= int64(len(kv.key)) + int64(kv.value.Len()) + if c.OnEvicted != nil { + c.OnEvicted(kv.key, kv.value) + } + } +} + +// Len the number of cache entries +func (c *Cache) Len() int { + return c.ll.Len() +} diff --git a/gee-cache/day4-consistent-hash/geecache/lru/lru_test.go b/gee-cache/day4-consistent-hash/geecache/lru/lru_test.go new file mode 100644 index 0000000..7308322 --- /dev/null +++ b/gee-cache/day4-consistent-hash/geecache/lru/lru_test.go @@ -0,0 +1,55 @@ +package lru + +import ( + "reflect" + "testing" +) + +type String string + +func (d String) Len() int { + return len(d) +} + +func TestGet(t *testing.T) { + lru := New(int64(0), nil) + lru.Add("key1", String("1234")) + if v, ok := lru.Get("key1"); !ok || string(v.(String)) != "1234" { + t.Fatalf("cache hit key1=1234 failed") + } + if _, ok := lru.Get("key2"); ok { + t.Fatalf("cache miss key2 failed") + } +} + +func TestRemoveoldest(t *testing.T) { + k1, k2, k3 := "key1", "key2", "k3" + v1, v2, v3 := "value1", "value2", "v3" + cap := len(k1 + k2 + v1 + v2) + lru := New(int64(cap), nil) + lru.Add(k1, String(v1)) + lru.Add(k2, String(v2)) + lru.Add(k3, String(v3)) + + if _, ok := lru.Get("key1"); ok || lru.Len() != 2 { + t.Fatalf("Removeoldest key1 failed") + } +} + +func TestOnEvicted(t *testing.T) { + keys := make([]string, 0) + callback := func(key string, value Value) { + keys = append(keys, key) + } + lru := New(int64(10), callback) + lru.Add("key1", String("123456")) + lru.Add("k2", String("k2")) + lru.Add("k3", String("k3")) + lru.Add("k4", String("k4")) + + expect := []string{"key1", "k2"} + + if !reflect.DeepEqual(expect, keys) { + t.Fatalf("Call OnEvicted failed, expect keys equals to %s", expect) + } +} diff --git a/gee-cache/day4-consistent-hash/go.mod b/gee-cache/day4-consistent-hash/go.mod new file mode 100644 index 0000000..d0fd3ba --- /dev/null +++ b/gee-cache/day4-consistent-hash/go.mod @@ -0,0 +1,7 @@ +module example + +go 1.13 + +require geecache v0.0.0 + +replace geecache => ./geecache diff --git a/gee-cache/day4-consistent-hash/main.go b/gee-cache/day4-consistent-hash/main.go new file mode 100644 index 0000000..570a90a --- /dev/null +++ b/gee-cache/day4-consistent-hash/main.go @@ -0,0 +1,30 @@ +package main + +import ( + "fmt" + "geecache" + "log" + "net/http" +) + +var db = map[string]string{ + "Tom": "630", + "Jack": "589", + "Sam": "567", +} + +func main() { + geecache.NewGroup("scores", 2<<10, geecache.GetterFunc( + func(key string) ([]byte, error) { + log.Println("[group scores] search key", key) + if v, ok := db[key]; ok { + return []byte(v), nil + } + return nil, fmt.Errorf("%s not exist", key) + })) + + addr := "localhost:9999" + peers := geecache.NewHTTPPool(addr) + log.Println("geecache is running at", addr) + log.Fatal(http.ListenAndServe(addr, peers)) +} From 3c187693f2b22eaff89c9000d04d0ac6f8e222a7 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Tue, 4 Feb 2020 00:33:34 +0800 Subject: [PATCH 020/122] day5 multi node --- .../day2-single-node/geecache/geecache.go | 14 +- .../day3-http-server/geecache/geecache.go | 14 +- .../day4-consistent-hash/geecache/geecache.go | 14 +- .../day5-multi-nodes/geecache/byteview.go | 21 +++ gee-cache/day5-multi-nodes/geecache/cache.go | 35 +++++ .../geecache/consistenthash/consistenthash.go | 58 ++++++++ .../consistenthash/consistenthash_test.go | 43 ++++++ .../day5-multi-nodes/geecache/geecache.go | 109 ++++++++++++++ .../geecache/geecache_test.go | 48 +++++++ gee-cache/day5-multi-nodes/geecache/go.mod | 3 + gee-cache/day5-multi-nodes/geecache/http.go | 134 ++++++++++++++++++ .../day5-multi-nodes/geecache/lru/lru.go | 79 +++++++++++ .../day5-multi-nodes/geecache/lru/lru_test.go | 55 +++++++ gee-cache/day5-multi-nodes/geecache/peers.go | 40 ++++++ gee-cache/day5-multi-nodes/go.mod | 7 + gee-cache/day5-multi-nodes/main.go | 30 ++++ 16 files changed, 692 insertions(+), 12 deletions(-) create mode 100644 gee-cache/day5-multi-nodes/geecache/byteview.go create mode 100644 gee-cache/day5-multi-nodes/geecache/cache.go create mode 100644 gee-cache/day5-multi-nodes/geecache/consistenthash/consistenthash.go create mode 100644 gee-cache/day5-multi-nodes/geecache/consistenthash/consistenthash_test.go create mode 100644 gee-cache/day5-multi-nodes/geecache/geecache.go create mode 100644 gee-cache/day5-multi-nodes/geecache/geecache_test.go create mode 100644 gee-cache/day5-multi-nodes/geecache/go.mod create mode 100644 gee-cache/day5-multi-nodes/geecache/http.go create mode 100644 gee-cache/day5-multi-nodes/geecache/lru/lru.go create mode 100644 gee-cache/day5-multi-nodes/geecache/lru/lru_test.go create mode 100644 gee-cache/day5-multi-nodes/geecache/peers.go create mode 100644 gee-cache/day5-multi-nodes/go.mod create mode 100644 gee-cache/day5-multi-nodes/main.go diff --git a/gee-cache/day2-single-node/geecache/geecache.go b/gee-cache/day2-single-node/geecache/geecache.go index 4273256..c2c8333 100644 --- a/gee-cache/day2-single-node/geecache/geecache.go +++ b/gee-cache/day2-single-node/geecache/geecache.go @@ -68,12 +68,18 @@ func cloneBytes(b []byte) []byte { } func (g *Group) load(key string) (value ByteView, err error) { + return g.getLocally(key) +} + +func (g *Group) getLocally(key string) (ByteView, error) { bytes, err := g.getter.Get(key) - if err == nil { - value = ByteView{cloneBytes(bytes)} - g.populateCache(key, value) + if err != nil { + return ByteView{}, err + } - return + value := ByteView{b: cloneBytes(bytes)} + g.populateCache(key, value) + return value, nil } func (g *Group) populateCache(key string, value ByteView) { diff --git a/gee-cache/day3-http-server/geecache/geecache.go b/gee-cache/day3-http-server/geecache/geecache.go index 4273256..c2c8333 100644 --- a/gee-cache/day3-http-server/geecache/geecache.go +++ b/gee-cache/day3-http-server/geecache/geecache.go @@ -68,12 +68,18 @@ func cloneBytes(b []byte) []byte { } func (g *Group) load(key string) (value ByteView, err error) { + return g.getLocally(key) +} + +func (g *Group) getLocally(key string) (ByteView, error) { bytes, err := g.getter.Get(key) - if err == nil { - value = ByteView{cloneBytes(bytes)} - g.populateCache(key, value) + if err != nil { + return ByteView{}, err + } - return + value := ByteView{b: cloneBytes(bytes)} + g.populateCache(key, value) + return value, nil } func (g *Group) populateCache(key string, value ByteView) { diff --git a/gee-cache/day4-consistent-hash/geecache/geecache.go b/gee-cache/day4-consistent-hash/geecache/geecache.go index 4273256..c2c8333 100644 --- a/gee-cache/day4-consistent-hash/geecache/geecache.go +++ b/gee-cache/day4-consistent-hash/geecache/geecache.go @@ -68,12 +68,18 @@ func cloneBytes(b []byte) []byte { } func (g *Group) load(key string) (value ByteView, err error) { + return g.getLocally(key) +} + +func (g *Group) getLocally(key string) (ByteView, error) { bytes, err := g.getter.Get(key) - if err == nil { - value = ByteView{cloneBytes(bytes)} - g.populateCache(key, value) + if err != nil { + return ByteView{}, err + } - return + value := ByteView{b: cloneBytes(bytes)} + g.populateCache(key, value) + return value, nil } func (g *Group) populateCache(key string, value ByteView) { diff --git a/gee-cache/day5-multi-nodes/geecache/byteview.go b/gee-cache/day5-multi-nodes/geecache/byteview.go new file mode 100644 index 0000000..a51394f --- /dev/null +++ b/gee-cache/day5-multi-nodes/geecache/byteview.go @@ -0,0 +1,21 @@ +package geecache + +// A ByteView holds an immutable view of bytes. +type ByteView struct { + b []byte +} + +// Len returns the view's length +func (v ByteView) Len() int { + return len(v.b) +} + +// ByteSlice returns a copy of the data as a byte slice. +func (v ByteView) ByteSlice() []byte { + return cloneBytes(v.b) +} + +// String returns the data as a string, making a copy if necessary. +func (v ByteView) String() string { + return string(v.b) +} diff --git a/gee-cache/day5-multi-nodes/geecache/cache.go b/gee-cache/day5-multi-nodes/geecache/cache.go new file mode 100644 index 0000000..703d033 --- /dev/null +++ b/gee-cache/day5-multi-nodes/geecache/cache.go @@ -0,0 +1,35 @@ +package geecache + +import ( + "geecache/lru" + "sync" +) + +type cache struct { + mu sync.RWMutex + lru *lru.Cache + cacheBytes int64 +} + +func (c *cache) add(key string, value ByteView) { + c.mu.RLock() + defer c.mu.RUnlock() + if c.lru == nil { + c.lru = lru.New(c.cacheBytes, nil) + } + c.lru.Add(key, value) +} + +func (c *cache) get(key string) (value ByteView, ok bool) { + c.mu.RLock() + defer c.mu.RUnlock() + if c.lru == nil { + return + } + + if v, ok := c.lru.Get(key); ok { + return v.(ByteView), ok + } + + return +} diff --git a/gee-cache/day5-multi-nodes/geecache/consistenthash/consistenthash.go b/gee-cache/day5-multi-nodes/geecache/consistenthash/consistenthash.go new file mode 100644 index 0000000..c8c9082 --- /dev/null +++ b/gee-cache/day5-multi-nodes/geecache/consistenthash/consistenthash.go @@ -0,0 +1,58 @@ +package consistenthash + +import ( + "hash/crc32" + "sort" + "strconv" +) + +// Hash maps bytes to uint32 +type Hash func(data []byte) uint32 + +// Map constains all hashed keys +type Map struct { + hash Hash + replicas int + keys []int // Sorted + hashMap map[int]string +} + +// New creates a Map instance +func New(replicas int, fn Hash) *Map { + m := &Map{ + replicas: replicas, + hash: fn, + hashMap: make(map[int]string), + } + if m.hash == nil { + m.hash = crc32.ChecksumIEEE + } + return m +} + +// Add adds some keys to the hash. +func (m *Map) Add(keys ...string) { + for _, key := range keys { + for i := 0; i < m.replicas; i++ { + hash := int(m.hash([]byte(strconv.Itoa(i) + key))) + m.keys = append(m.keys, hash) + m.hashMap[hash] = key + } + } + sort.Ints(m.keys) +} + +// Get gets the closest item in the hash to the provided key. +func (m *Map) Get(key string) string { + if len(m.keys) == 0 { + return "" + } + + hash := int(m.hash([]byte(key))) + // Binary search for appropriate replica. + idx := sort.Search(len(m.keys), func(i int) bool { + return m.keys[i] >= hash + }) + + return m.hashMap[m.keys[idx%len(m.keys)]] +} diff --git a/gee-cache/day5-multi-nodes/geecache/consistenthash/consistenthash_test.go b/gee-cache/day5-multi-nodes/geecache/consistenthash/consistenthash_test.go new file mode 100644 index 0000000..34e1275 --- /dev/null +++ b/gee-cache/day5-multi-nodes/geecache/consistenthash/consistenthash_test.go @@ -0,0 +1,43 @@ +package consistenthash + +import ( + "strconv" + "testing" +) + +func TestHashing(t *testing.T) { + hash := New(3, func(key []byte) uint32 { + i, _ := strconv.Atoi(string(key)) + return uint32(i) + }) + + // Given the above hash function, this will give replicas with "hashes": + // 2, 4, 6, 12, 14, 16, 22, 24, 26 + hash.Add("6", "4", "2") + + testCases := map[string]string{ + "2": "2", + "11": "2", + "23": "4", + "27": "2", + } + + for k, v := range testCases { + if hash.Get(k) != v { + t.Errorf("Asking for %s, should have yielded %s", k, v) + } + } + + // Adds 8, 18, 28 + hash.Add("8") + + // 27 should now map to 8. + testCases["27"] = "8" + + for k, v := range testCases { + if hash.Get(k) != v { + t.Errorf("Asking for %s, should have yielded %s", k, v) + } + } + +} diff --git a/gee-cache/day5-multi-nodes/geecache/geecache.go b/gee-cache/day5-multi-nodes/geecache/geecache.go new file mode 100644 index 0000000..db6e67a --- /dev/null +++ b/gee-cache/day5-multi-nodes/geecache/geecache.go @@ -0,0 +1,109 @@ +package geecache + +import ( + "sync" +) + +// A Group is a cache namespace and associated data loaded spread over +type Group struct { + name string + getter Getter + mainCache cache + peers PeerPicker + peersOnce sync.Once +} + +// A Getter loads data for a key. +type Getter interface { + Get(key string) ([]byte, error) +} + +// A GetterFunc implements Getter with a function. +type GetterFunc func(key string) ([]byte, error) + +// Get implements Getter interface function +func (f GetterFunc) Get(key string) ([]byte, error) { + return f(key) +} + +var ( + mu sync.RWMutex + groups = make(map[string]*Group) +) + +// NewGroup create a new instance of Group +func NewGroup(name string, cacheBytes int64, getter Getter) *Group { + if getter == nil { + panic("nil Getter") + } + mu.Lock() + defer mu.Unlock() + g := &Group{ + name: name, + getter: getter, + mainCache: cache{cacheBytes: cacheBytes}, + } + groups[name] = g + return g +} + +// GetGroup returns the named group previously created with NewGroup, or +// nil if there's no such group. +func GetGroup(name string) *Group { + mu.RLock() + g := groups[name] + mu.RUnlock() + return g +} + +// Get value for a key from cache +func (g *Group) Get(key string) (ByteView, error) { + g.peersOnce.Do(func() { + g.peers = getPeers() + }) + if v, ok := g.mainCache.get(key); ok { + return v, nil + } + + return g.load(key) +} + +func cloneBytes(b []byte) []byte { + c := make([]byte, len(b)) + copy(c, b) + return c +} + +func (g *Group) load(key string) (value ByteView, err error) { + if peer, ok := g.peers.PickPeer(key); ok { + value, err = g.getFromPeer(peer, key) + if err == nil { + return value, nil + } + } + + return g.getLocally(key) +} + +func (g *Group) populateCache(key string, value ByteView) { + g.mainCache.add(key, value) +} + +func (g *Group) getLocally(key string) (ByteView, error) { + bytes, err := g.getter.Get(key) + if err != nil { + return ByteView{}, err + + } + value := ByteView{b: cloneBytes(bytes)} + g.populateCache(key, value) + return value, nil +} + +func (g *Group) getFromPeer(peer PeerGetter, key string) (ByteView, error) { + bytes, err := peer.Get(g.name, key) + if err != nil { + return ByteView{}, err + } + return ByteView{b: bytes}, nil +} diff --git a/gee-cache/day5-multi-nodes/geecache/geecache_test.go b/gee-cache/day5-multi-nodes/geecache/geecache_test.go new file mode 100644 index 0000000..2bffb3f --- /dev/null +++ b/gee-cache/day5-multi-nodes/geecache/geecache_test.go @@ -0,0 +1,48 @@ +package geecache + +import ( + "fmt" + "log" + "testing" +) + +var db = map[string]string{ + "Tom": "630", + "Jack": "589", + "Sam": "567", +} + +func TestGet(t *testing.T) { + gee := NewGroup("scores", 2<<10, GetterFunc( + func(key string) ([]byte, error) { + log.Println("[group scores] search key", key) + if v, ok := db[key]; ok { + return []byte(v), nil + } + return nil, fmt.Errorf("%s not exist", key) + })) + + for k, v := range db { + view, err := gee.Get(k) + if err != nil || view.String() != v { + t.Fatal("failed to get value of Tom") + } + } + + if view, err := gee.Get("unknown"); err == nil { + t.Fatalf("the value of unknow should be empty, but %s got", view) + } +} + +func TestGetGroup(t *testing.T) { + groupName := "scores" + NewGroup(groupName, 2<<10, GetterFunc( + func(key string) (bytes []byte, err error) { return })) + if group := GetGroup(groupName); group == nil || group.name != groupName { + t.Fatalf("group %s not exist", groupName) + } + + if group := GetGroup(groupName + "111"); group != nil { + t.Fatalf("expect nil, but %s got", group.name) + } +} diff --git a/gee-cache/day5-multi-nodes/geecache/go.mod b/gee-cache/day5-multi-nodes/geecache/go.mod new file mode 100644 index 0000000..f9d454e --- /dev/null +++ b/gee-cache/day5-multi-nodes/geecache/go.mod @@ -0,0 +1,3 @@ +module geecache + +go 1.13 diff --git a/gee-cache/day5-multi-nodes/geecache/http.go b/gee-cache/day5-multi-nodes/geecache/http.go new file mode 100644 index 0000000..c108edf --- /dev/null +++ b/gee-cache/day5-multi-nodes/geecache/http.go @@ -0,0 +1,134 @@ +package geecache + +import ( + "bytes" + "fmt" + "geecache/consistenthash" + "io" + "log" + "net/http" + "net/url" + "strings" + "sync" +) + +const ( + defaultBasePath = "/_geecache/" + defaultReplicas = 50 +) + +// HTTPPool implements PeerPicker for a pool of HTTP peers. +type HTTPPool struct { + // this peer's base URL, e.g. "https://example.net:8000" + self string + basePath string + mu sync.Mutex // guards peers and httpGetters + peers *consistenthash.Map + httpGetters map[string]*httpGetter // keyed by e.g. "http://10.0.0.2:8008" +} + +// NewHTTPPool initializes an HTTP pool of peers, and registers itself as a PeerPicker. +func NewHTTPPool(self string) *HTTPPool { + p := &HTTPPool{ + self: self, + basePath: defaultBasePath, + } + RegisterPeerPicker(func() PeerPicker { return p }) + return p +} + +// ServeHTTP handle all http requests +func (p *HTTPPool) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if !strings.HasPrefix(r.URL.Path, p.basePath) { + panic("HTTPPool serving unexpected path: " + r.URL.Path) + } + log.Println("[geecache server]", r.Method, r.URL.Path) + // /// required + parts := strings.SplitN(r.URL.Path[len(p.basePath):], "/", 2) + if len(parts) != 2 { + http.Error(w, "bad request", http.StatusBadRequest) + return + } + + groupName := parts[0] + key := parts[1] + + group := GetGroup(groupName) + if group == nil { + http.Error(w, "no such group: "+groupName, http.StatusNotFound) + return + } + + view, err := group.Get(key) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/octet-stream") + w.Write(view.ByteSlice()) +} + +// Set updates the pool's list of peers. +func (p *HTTPPool) Set(peers ...string) { + p.mu.Lock() + defer p.mu.Unlock() + p.peers = consistenthash.New(defaultReplicas, nil) + p.peers.Add(peers...) + p.httpGetters = make(map[string]*httpGetter, len(peers)) + for _, peer := range peers { + p.httpGetters[peer] = &httpGetter{baseURL: peer + p.basePath} + } +} + +// PickPeer picks a peer according to key +func (p *HTTPPool) PickPeer(key string) (PeerGetter, bool) { + p.mu.Lock() + defer p.mu.Unlock() + if peer := p.peers.Get(key); peer != "" && peer != p.self { + return p.httpGetters[peer], true + } + return nil, false +} + +var _ PeerPicker = (*HTTPPool)(nil) + +type httpGetter struct { + baseURL string +} + +var bufferPool = sync.Pool{ + New: func() interface{} { return new(bytes.Buffer) }, +} + +func (h *httpGetter) Get(group string, key string) ([]byte, error) { + u := fmt.Sprintf( + "%v%v/%v", + h.baseURL, + url.QueryEscape(group), + url.QueryEscape(key), + ) + res, err := http.Get(u) + if err != nil { + return nil, err + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("server returned: %v", res.Status) + } + + b := bufferPool.Get().(*bytes.Buffer) + b.Reset() + defer bufferPool.Put(b) + + _, err = io.Copy(b, res.Body) + + if err != nil { + return nil, fmt.Errorf("reading response body: %v", err) + } + + return b.Bytes(), nil +} + +var _ PeerGetter = (*httpGetter)(nil) diff --git a/gee-cache/day5-multi-nodes/geecache/lru/lru.go b/gee-cache/day5-multi-nodes/geecache/lru/lru.go new file mode 100644 index 0000000..81eee43 --- /dev/null +++ b/gee-cache/day5-multi-nodes/geecache/lru/lru.go @@ -0,0 +1,79 @@ +package lru + +import "container/list" + +// Cache is a LRU cache. It is not safe for concurrent access. +type Cache struct { + maxBytes int64 + nbytes int64 + ll *list.List + cache map[string]*list.Element + // optional and executed when an entry is purged. + OnEvicted func(key string, value Value) +} + +type entry struct { + key string + value Value +} + +// Value use Len to count how many bytes it takes +type Value interface { + Len() int +} + +// New is the Constructor of Cache +func New(maxBytes int64, onEvicted func(string, Value)) *Cache { + return &Cache{ + maxBytes: maxBytes, + ll: list.New(), + cache: make(map[string]*list.Element), + OnEvicted: onEvicted, + } +} + +// Add adds a value to the cache. +func (c *Cache) Add(key string, value Value) { + if ele, ok := c.cache[key]; ok { + c.ll.MoveToFront(ele) + kv := ele.Value.(*entry) + kv.value = value + return + } + ele := c.ll.PushFront(&entry{key, value}) + c.cache[key] = ele + c.nbytes += int64(len(key)) + int64(value.Len()) + + for c.maxBytes != 0 && c.maxBytes < c.nbytes { + c.RemoveOldest() + } +} + +// Get look ups a key's value +func (c *Cache) Get(key string) (value Value, ok bool) { + if ele, ok := c.cache[key]; ok { + c.ll.MoveToFront(ele) + kv := ele.Value.(*entry) + return kv.value, true + } + return +} + +// RemoveOldest removes the oldest item +func (c *Cache) RemoveOldest() { + ele := c.ll.Back() + if ele != nil { + c.ll.Remove(ele) + kv := ele.Value.(*entry) + delete(c.cache, kv.key) + c.nbytes -= int64(len(kv.key)) + int64(kv.value.Len()) + if c.OnEvicted != nil { + c.OnEvicted(kv.key, kv.value) + } + } +} + +// Len the number of cache entries +func (c *Cache) Len() int { + return c.ll.Len() +} diff --git a/gee-cache/day5-multi-nodes/geecache/lru/lru_test.go b/gee-cache/day5-multi-nodes/geecache/lru/lru_test.go new file mode 100644 index 0000000..7308322 --- /dev/null +++ b/gee-cache/day5-multi-nodes/geecache/lru/lru_test.go @@ -0,0 +1,55 @@ +package lru + +import ( + "reflect" + "testing" +) + +type String string + +func (d String) Len() int { + return len(d) +} + +func TestGet(t *testing.T) { + lru := New(int64(0), nil) + lru.Add("key1", String("1234")) + if v, ok := lru.Get("key1"); !ok || string(v.(String)) != "1234" { + t.Fatalf("cache hit key1=1234 failed") + } + if _, ok := lru.Get("key2"); ok { + t.Fatalf("cache miss key2 failed") + } +} + +func TestRemoveoldest(t *testing.T) { + k1, k2, k3 := "key1", "key2", "k3" + v1, v2, v3 := "value1", "value2", "v3" + cap := len(k1 + k2 + v1 + v2) + lru := New(int64(cap), nil) + lru.Add(k1, String(v1)) + lru.Add(k2, String(v2)) + lru.Add(k3, String(v3)) + + if _, ok := lru.Get("key1"); ok || lru.Len() != 2 { + t.Fatalf("Removeoldest key1 failed") + } +} + +func TestOnEvicted(t *testing.T) { + keys := make([]string, 0) + callback := func(key string, value Value) { + keys = append(keys, key) + } + lru := New(int64(10), callback) + lru.Add("key1", String("123456")) + lru.Add("k2", String("k2")) + lru.Add("k3", String("k3")) + lru.Add("k4", String("k4")) + + expect := []string{"key1", "k2"} + + if !reflect.DeepEqual(expect, keys) { + t.Fatalf("Call OnEvicted failed, expect keys equals to %s", expect) + } +} diff --git a/gee-cache/day5-multi-nodes/geecache/peers.go b/gee-cache/day5-multi-nodes/geecache/peers.go new file mode 100644 index 0000000..cfc39f7 --- /dev/null +++ b/gee-cache/day5-multi-nodes/geecache/peers.go @@ -0,0 +1,40 @@ +package geecache + +// PeerPicker is the interface that must be implemented to locate +// the peer that owns a specific key. +type PeerPicker interface { + PickPeer(key string) (peer PeerGetter, ok bool) +} + +// PeerGetter is the interface that must be implemented by a peer. +type PeerGetter interface { + Get(group string, key string) ([]byte, error) +} + +// NoPeers is an implementation of PeerPicker that never finds a peer. +type NoPeers struct{} + +// PickPeer return nothing +func (NoPeers) PickPeer(key string) (peer PeerGetter, ok bool) { return } + +var portPicker func() PeerPicker + +// RegisterPeerPicker registers the peer initialization function. +// It is called once, when the first group is created. +func RegisterPeerPicker(fn func() PeerPicker) { + if portPicker != nil { + panic("RegisterPeerPicker called more than once") + } + portPicker = fn +} + +func getPeers() PeerPicker { + if portPicker == nil { + return NoPeers{} + } + pk := portPicker() + if pk == nil { + pk = NoPeers{} + } + return pk +} diff --git a/gee-cache/day5-multi-nodes/go.mod b/gee-cache/day5-multi-nodes/go.mod new file mode 100644 index 0000000..d0fd3ba --- /dev/null +++ b/gee-cache/day5-multi-nodes/go.mod @@ -0,0 +1,7 @@ +module example + +go 1.13 + +require geecache v0.0.0 + +replace geecache => ./geecache diff --git a/gee-cache/day5-multi-nodes/main.go b/gee-cache/day5-multi-nodes/main.go new file mode 100644 index 0000000..570a90a --- /dev/null +++ b/gee-cache/day5-multi-nodes/main.go @@ -0,0 +1,30 @@ +package main + +import ( + "fmt" + "geecache" + "log" + "net/http" +) + +var db = map[string]string{ + "Tom": "630", + "Jack": "589", + "Sam": "567", +} + +func main() { + geecache.NewGroup("scores", 2<<10, geecache.GetterFunc( + func(key string) ([]byte, error) { + log.Println("[group scores] search key", key) + if v, ok := db[key]; ok { + return []byte(v), nil + } + return nil, fmt.Errorf("%s not exist", key) + })) + + addr := "localhost:9999" + peers := geecache.NewHTTPPool(addr) + log.Println("geecache is running at", addr) + log.Fatal(http.ListenAndServe(addr, peers)) +} From 8ac9c63d92c95fcebe047a180da9379cec3ea11a Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Wed, 5 Feb 2020 16:33:26 +0800 Subject: [PATCH 021/122] day5 multi nodes finish main func & add a auto start shell script --- .../day2-single-node/geecache/geecache.go | 11 ++- .../geecache/geecache_test.go | 2 +- .../day3-http-server/geecache/geecache.go | 11 ++- .../geecache/geecache_test.go | 2 +- gee-cache/day3-http-server/geecache/http.go | 8 ++- gee-cache/day3-http-server/main.go | 10 ++- .../day4-consistent-hash/geecache/geecache.go | 11 ++- .../geecache/geecache_test.go | 2 +- .../day4-consistent-hash/geecache/http.go | 8 ++- gee-cache/day4-consistent-hash/main.go | 10 ++- .../day5-multi-nodes/geecache/geecache.go | 29 +++++--- .../geecache/geecache_test.go | 2 +- gee-cache/day5-multi-nodes/geecache/http.go | 28 +++----- gee-cache/day5-multi-nodes/geecache/peers.go | 28 -------- gee-cache/day5-multi-nodes/main.go | 67 +++++++++++++++++-- gee-cache/day5-multi-nodes/run.sh | 9 +++ 16 files changed, 169 insertions(+), 69 deletions(-) create mode 100755 gee-cache/day5-multi-nodes/run.sh diff --git a/gee-cache/day2-single-node/geecache/geecache.go b/gee-cache/day2-single-node/geecache/geecache.go index c2c8333..58b98b8 100644 --- a/gee-cache/day2-single-node/geecache/geecache.go +++ b/gee-cache/day2-single-node/geecache/geecache.go @@ -1,6 +1,10 @@ package geecache -import "sync" +import ( + "fmt" + "log" + "sync" +) // A Group is a cache namespace and associated data loaded spread over type Group struct { @@ -54,7 +58,12 @@ func GetGroup(name string) *Group { // Get value for a key from cache func (g *Group) Get(key string) (ByteView, error) { + if key == "" { + return ByteView{}, fmt.Errorf("key is required") + } + if v, ok := g.mainCache.get(key); ok { + log.Println("[GeeCache] hit") return v, nil } diff --git a/gee-cache/day2-single-node/geecache/geecache_test.go b/gee-cache/day2-single-node/geecache/geecache_test.go index 2bffb3f..9cb4079 100644 --- a/gee-cache/day2-single-node/geecache/geecache_test.go +++ b/gee-cache/day2-single-node/geecache/geecache_test.go @@ -15,7 +15,7 @@ var db = map[string]string{ func TestGet(t *testing.T) { gee := NewGroup("scores", 2<<10, GetterFunc( func(key string) ([]byte, error) { - log.Println("[group scores] search key", key) + log.Println("[SlowDB] search key", key) if v, ok := db[key]; ok { return []byte(v), nil } diff --git a/gee-cache/day3-http-server/geecache/geecache.go b/gee-cache/day3-http-server/geecache/geecache.go index c2c8333..58b98b8 100644 --- a/gee-cache/day3-http-server/geecache/geecache.go +++ b/gee-cache/day3-http-server/geecache/geecache.go @@ -1,6 +1,10 @@ package geecache -import "sync" +import ( + "fmt" + "log" + "sync" +) // A Group is a cache namespace and associated data loaded spread over type Group struct { @@ -54,7 +58,12 @@ func GetGroup(name string) *Group { // Get value for a key from cache func (g *Group) Get(key string) (ByteView, error) { + if key == "" { + return ByteView{}, fmt.Errorf("key is required") + } + if v, ok := g.mainCache.get(key); ok { + log.Println("[GeeCache] hit") return v, nil } diff --git a/gee-cache/day3-http-server/geecache/geecache_test.go b/gee-cache/day3-http-server/geecache/geecache_test.go index 2bffb3f..9cb4079 100644 --- a/gee-cache/day3-http-server/geecache/geecache_test.go +++ b/gee-cache/day3-http-server/geecache/geecache_test.go @@ -15,7 +15,7 @@ var db = map[string]string{ func TestGet(t *testing.T) { gee := NewGroup("scores", 2<<10, GetterFunc( func(key string) ([]byte, error) { - log.Println("[group scores] search key", key) + log.Println("[SlowDB] search key", key) if v, ok := db[key]; ok { return []byte(v), nil } diff --git a/gee-cache/day3-http-server/geecache/http.go b/gee-cache/day3-http-server/geecache/http.go index 4d33d5d..ef5e2f1 100644 --- a/gee-cache/day3-http-server/geecache/http.go +++ b/gee-cache/day3-http-server/geecache/http.go @@ -1,6 +1,7 @@ package geecache import ( + "fmt" "log" "net/http" "strings" @@ -23,12 +24,17 @@ func NewHTTPPool(self string) *HTTPPool { } } +// Log info with server name +func (p *HTTPPool) Log(format string, v ...interface{}) { + log.Printf("[Server %s] %s", p.self, fmt.Sprintf(format, v...)) +} + // ServeHTTP handle all http requests func (p *HTTPPool) ServeHTTP(w http.ResponseWriter, r *http.Request) { if !strings.HasPrefix(r.URL.Path, p.basePath) { panic("HTTPPool serving unexpected path: " + r.URL.Path) } - log.Println("[geecache server]", r.Method, r.URL.Path) + p.Log("%s %s", r.Method, r.URL.Path) // /// required parts := strings.SplitN(r.URL.Path[len(p.basePath):], "/", 2) if len(parts) != 2 { diff --git a/gee-cache/day3-http-server/main.go b/gee-cache/day3-http-server/main.go index 570a90a..5442dd7 100644 --- a/gee-cache/day3-http-server/main.go +++ b/gee-cache/day3-http-server/main.go @@ -1,5 +1,13 @@ package main +/* +$ curl http://localhost:9999/_geecache/scores/Tom +630 + +$ curl http://localhost:9999/_geecache/scores/kkk +kkk not exist +*/ + import ( "fmt" "geecache" @@ -16,7 +24,7 @@ var db = map[string]string{ func main() { geecache.NewGroup("scores", 2<<10, geecache.GetterFunc( func(key string) ([]byte, error) { - log.Println("[group scores] search key", key) + log.Println("[SlowDB] search key", key) if v, ok := db[key]; ok { return []byte(v), nil } diff --git a/gee-cache/day4-consistent-hash/geecache/geecache.go b/gee-cache/day4-consistent-hash/geecache/geecache.go index c2c8333..58b98b8 100644 --- a/gee-cache/day4-consistent-hash/geecache/geecache.go +++ b/gee-cache/day4-consistent-hash/geecache/geecache.go @@ -1,6 +1,10 @@ package geecache -import "sync" +import ( + "fmt" + "log" + "sync" +) // A Group is a cache namespace and associated data loaded spread over type Group struct { @@ -54,7 +58,12 @@ func GetGroup(name string) *Group { // Get value for a key from cache func (g *Group) Get(key string) (ByteView, error) { + if key == "" { + return ByteView{}, fmt.Errorf("key is required") + } + if v, ok := g.mainCache.get(key); ok { + log.Println("[GeeCache] hit") return v, nil } diff --git a/gee-cache/day4-consistent-hash/geecache/geecache_test.go b/gee-cache/day4-consistent-hash/geecache/geecache_test.go index 2bffb3f..9cb4079 100644 --- a/gee-cache/day4-consistent-hash/geecache/geecache_test.go +++ b/gee-cache/day4-consistent-hash/geecache/geecache_test.go @@ -15,7 +15,7 @@ var db = map[string]string{ func TestGet(t *testing.T) { gee := NewGroup("scores", 2<<10, GetterFunc( func(key string) ([]byte, error) { - log.Println("[group scores] search key", key) + log.Println("[SlowDB] search key", key) if v, ok := db[key]; ok { return []byte(v), nil } diff --git a/gee-cache/day4-consistent-hash/geecache/http.go b/gee-cache/day4-consistent-hash/geecache/http.go index 4d33d5d..ef5e2f1 100644 --- a/gee-cache/day4-consistent-hash/geecache/http.go +++ b/gee-cache/day4-consistent-hash/geecache/http.go @@ -1,6 +1,7 @@ package geecache import ( + "fmt" "log" "net/http" "strings" @@ -23,12 +24,17 @@ func NewHTTPPool(self string) *HTTPPool { } } +// Log info with server name +func (p *HTTPPool) Log(format string, v ...interface{}) { + log.Printf("[Server %s] %s", p.self, fmt.Sprintf(format, v...)) +} + // ServeHTTP handle all http requests func (p *HTTPPool) ServeHTTP(w http.ResponseWriter, r *http.Request) { if !strings.HasPrefix(r.URL.Path, p.basePath) { panic("HTTPPool serving unexpected path: " + r.URL.Path) } - log.Println("[geecache server]", r.Method, r.URL.Path) + p.Log("%s %s", r.Method, r.URL.Path) // /// required parts := strings.SplitN(r.URL.Path[len(p.basePath):], "/", 2) if len(parts) != 2 { diff --git a/gee-cache/day4-consistent-hash/main.go b/gee-cache/day4-consistent-hash/main.go index 570a90a..5442dd7 100644 --- a/gee-cache/day4-consistent-hash/main.go +++ b/gee-cache/day4-consistent-hash/main.go @@ -1,5 +1,13 @@ package main +/* +$ curl http://localhost:9999/_geecache/scores/Tom +630 + +$ curl http://localhost:9999/_geecache/scores/kkk +kkk not exist +*/ + import ( "fmt" "geecache" @@ -16,7 +24,7 @@ var db = map[string]string{ func main() { geecache.NewGroup("scores", 2<<10, geecache.GetterFunc( func(key string) ([]byte, error) { - log.Println("[group scores] search key", key) + log.Println("[SlowDB] search key", key) if v, ok := db[key]; ok { return []byte(v), nil } diff --git a/gee-cache/day5-multi-nodes/geecache/geecache.go b/gee-cache/day5-multi-nodes/geecache/geecache.go index db6e67a..dbd5c3e 100644 --- a/gee-cache/day5-multi-nodes/geecache/geecache.go +++ b/gee-cache/day5-multi-nodes/geecache/geecache.go @@ -1,6 +1,8 @@ package geecache import ( + "fmt" + "log" "sync" ) @@ -10,7 +12,6 @@ type Group struct { getter Getter mainCache cache peers PeerPicker - peersOnce sync.Once } // A Getter loads data for a key. @@ -58,16 +59,26 @@ func GetGroup(name string) *Group { // Get value for a key from cache func (g *Group) Get(key string) (ByteView, error) { - g.peersOnce.Do(func() { - g.peers = getPeers() - }) + if key == "" { + return ByteView{}, fmt.Errorf("key is required") + } + if v, ok := g.mainCache.get(key); ok { + log.Println("[GeeCache] hit") return v, nil } return g.load(key) } +// RegisterPeers registers a PeerPicker for choosing remote peer +func (g *Group) RegisterPeers(peers PeerPicker) { + if g.peers != nil { + panic("RegisterPeerPicker called more than once") + } + g.peers = peers +} + func cloneBytes(b []byte) []byte { c := make([]byte, len(b)) copy(c, b) @@ -75,10 +86,12 @@ func cloneBytes(b []byte) []byte { } func (g *Group) load(key string) (value ByteView, err error) { - if peer, ok := g.peers.PickPeer(key); ok { - value, err = g.getFromPeer(peer, key) - if err == nil { - return value, nil + if g.peers != nil { + if peer, ok := g.peers.PickPeer(key); ok { + if value, err = g.getFromPeer(peer, key); err == nil { + return value, nil + } + log.Println("[GeeCache] Failed to get from peer", err) } } diff --git a/gee-cache/day5-multi-nodes/geecache/geecache_test.go b/gee-cache/day5-multi-nodes/geecache/geecache_test.go index 2bffb3f..9cb4079 100644 --- a/gee-cache/day5-multi-nodes/geecache/geecache_test.go +++ b/gee-cache/day5-multi-nodes/geecache/geecache_test.go @@ -15,7 +15,7 @@ var db = map[string]string{ func TestGet(t *testing.T) { gee := NewGroup("scores", 2<<10, GetterFunc( func(key string) ([]byte, error) { - log.Println("[group scores] search key", key) + log.Println("[SlowDB] search key", key) if v, ok := db[key]; ok { return []byte(v), nil } diff --git a/gee-cache/day5-multi-nodes/geecache/http.go b/gee-cache/day5-multi-nodes/geecache/http.go index c108edf..d7da2c3 100644 --- a/gee-cache/day5-multi-nodes/geecache/http.go +++ b/gee-cache/day5-multi-nodes/geecache/http.go @@ -1,10 +1,9 @@ package geecache import ( - "bytes" "fmt" "geecache/consistenthash" - "io" + "io/ioutil" "log" "net/http" "net/url" @@ -29,12 +28,15 @@ type HTTPPool struct { // NewHTTPPool initializes an HTTP pool of peers, and registers itself as a PeerPicker. func NewHTTPPool(self string) *HTTPPool { - p := &HTTPPool{ + return &HTTPPool{ self: self, basePath: defaultBasePath, } - RegisterPeerPicker(func() PeerPicker { return p }) - return p +} + +// Log info with server name +func (p *HTTPPool) Log(format string, v ...interface{}) { + log.Printf("[Server %s] %s", p.self, fmt.Sprintf(format, v...)) } // ServeHTTP handle all http requests @@ -42,7 +44,7 @@ func (p *HTTPPool) ServeHTTP(w http.ResponseWriter, r *http.Request) { if !strings.HasPrefix(r.URL.Path, p.basePath) { panic("HTTPPool serving unexpected path: " + r.URL.Path) } - log.Println("[geecache server]", r.Method, r.URL.Path) + p.Log("%s %s", r.Method, r.URL.Path) // /// required parts := strings.SplitN(r.URL.Path[len(p.basePath):], "/", 2) if len(parts) != 2 { @@ -86,6 +88,7 @@ func (p *HTTPPool) PickPeer(key string) (PeerGetter, bool) { p.mu.Lock() defer p.mu.Unlock() if peer := p.peers.Get(key); peer != "" && peer != p.self { + p.Log("Pick peer %s", peer) return p.httpGetters[peer], true } return nil, false @@ -97,10 +100,6 @@ type httpGetter struct { baseURL string } -var bufferPool = sync.Pool{ - New: func() interface{} { return new(bytes.Buffer) }, -} - func (h *httpGetter) Get(group string, key string) ([]byte, error) { u := fmt.Sprintf( "%v%v/%v", @@ -118,17 +117,12 @@ func (h *httpGetter) Get(group string, key string) ([]byte, error) { return nil, fmt.Errorf("server returned: %v", res.Status) } - b := bufferPool.Get().(*bytes.Buffer) - b.Reset() - defer bufferPool.Put(b) - - _, err = io.Copy(b, res.Body) - + bytes, err := ioutil.ReadAll(res.Body) if err != nil { return nil, fmt.Errorf("reading response body: %v", err) } - return b.Bytes(), nil + return bytes, nil } var _ PeerGetter = (*httpGetter)(nil) diff --git a/gee-cache/day5-multi-nodes/geecache/peers.go b/gee-cache/day5-multi-nodes/geecache/peers.go index cfc39f7..8d010e2 100644 --- a/gee-cache/day5-multi-nodes/geecache/peers.go +++ b/gee-cache/day5-multi-nodes/geecache/peers.go @@ -10,31 +10,3 @@ type PeerPicker interface { type PeerGetter interface { Get(group string, key string) ([]byte, error) } - -// NoPeers is an implementation of PeerPicker that never finds a peer. -type NoPeers struct{} - -// PickPeer return nothing -func (NoPeers) PickPeer(key string) (peer PeerGetter, ok bool) { return } - -var portPicker func() PeerPicker - -// RegisterPeerPicker registers the peer initialization function. -// It is called once, when the first group is created. -func RegisterPeerPicker(fn func() PeerPicker) { - if portPicker != nil { - panic("RegisterPeerPicker called more than once") - } - portPicker = fn -} - -func getPeers() PeerPicker { - if portPicker == nil { - return NoPeers{} - } - pk := portPicker() - if pk == nil { - pk = NoPeers{} - } - return pk -} diff --git a/gee-cache/day5-multi-nodes/main.go b/gee-cache/day5-multi-nodes/main.go index 570a90a..a99940c 100644 --- a/gee-cache/day5-multi-nodes/main.go +++ b/gee-cache/day5-multi-nodes/main.go @@ -1,6 +1,15 @@ package main +/* +$ curl "http://localhost:9999/api?key=Tom" +630 + +$ curl "http://localhost:9999/api?key=kkk" +kkk not exist +*/ + import ( + "flag" "fmt" "geecache" "log" @@ -13,18 +22,66 @@ var db = map[string]string{ "Sam": "567", } -func main() { - geecache.NewGroup("scores", 2<<10, geecache.GetterFunc( +func createGroup() *geecache.Group { + return geecache.NewGroup("scores", 2<<10, geecache.GetterFunc( func(key string) ([]byte, error) { - log.Println("[group scores] search key", key) + log.Println("[SlowDB] search key", key) if v, ok := db[key]; ok { return []byte(v), nil } return nil, fmt.Errorf("%s not exist", key) })) +} - addr := "localhost:9999" +func startCacheServer(addr string, addrs []string, gee *geecache.Group) { peers := geecache.NewHTTPPool(addr) + peers.Set(addrs...) + gee.RegisterPeers(peers) log.Println("geecache is running at", addr) - log.Fatal(http.ListenAndServe(addr, peers)) + log.Fatal(http.ListenAndServe(addr[7:], peers)) +} + +func startAPIServer(apiAddr string, gee *geecache.Group) { + http.Handle("/api", http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + key := r.URL.Query().Get("key") + view, err := gee.Get(key) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/octet-stream") + w.Write(view.ByteSlice()) + + })) + log.Println("fontend server is running at", apiAddr) + log.Fatal(http.ListenAndServe(apiAddr[7:], nil)) + +} + +func main() { + var port int + var api bool + flag.IntVar(&port, "port", 8001, "Geecache server port") + flag.BoolVar(&api, "api", false, "Start a api server?") + flag.Parse() + + apiAddr := "http://localhost:9999" + addrMap := map[int]string{ + 8001: "http://localhost:8001", + 8002: "http://localhost:8002", + 8003: "http://localhost:8003", + } + + addrs := make([]string, 3) + + for _, v := range addrMap { + addrs = append(addrs, v) + } + + gee := createGroup() + if api { + go startAPIServer(apiAddr, gee) + } + startCacheServer(addrMap[port], []string(addrs), gee) } diff --git a/gee-cache/day5-multi-nodes/run.sh b/gee-cache/day5-multi-nodes/run.sh new file mode 100755 index 0000000..d19b4c8 --- /dev/null +++ b/gee-cache/day5-multi-nodes/run.sh @@ -0,0 +1,9 @@ +#!/bin/bash +trap "rm server;kill 0" EXIT + +go build -o server +./server -port=8001 & +./server -port=8002 & +./server -port=8003 -api=1 & + +wait \ No newline at end of file From b3bf9326f5d5bb283c47b943ecf9b86ae49f6103 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Wed, 5 Feb 2020 21:17:51 +0800 Subject: [PATCH 022/122] add English version's README --- README.md | 83 +++++++++++++++++++++++++++++++++++------------ gee-web/README.md | 13 ++++++-- 2 files changed, 74 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 0aa3323..9d95aff 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,76 @@ -# 7天用Go从零实现系列 +# 7 days golang apps from scratch + +
+README 中文版本 +
+ +## 7天用Go从零实现系列 7天能写什么呢?类似 gin 的 web 框架?类似 groupcache 的分布式缓存?或者一个简单的 Python 解释器?希望这个仓库能给你答案。 推荐先阅读 **[Go 语言简明教程](https://geektutu.com/post/quick-golang.html)**,一篇文章了解Go的基本语法、并发编程,依赖管理等内容 -## 7天用Go从零实现Web框架Gee +### 7天用Go从零实现Web框架 - Gee + +[Gee](https://geektutu.com/post/gee.html) 是一个模仿 [gin](https://github.com/gin-gonic/gin) 实现的 Web 框架,[Go Gin简明教程](https://geektutu.com/post/quick-go-gin.html)可以快速入门。 + +- 第一天:[前置知识(http.Handler接口)](https://geektutu.com/post/gee-day1.html) | [Code](gee-web/day1-http-base) +- 第二天:[上下文设计(Context)](https://geektutu.com/post/gee-day2.html) | [Code](gee-web/day2-context) +- 第三天:[Tire树路由(Router)](https://geektutu.com/post/gee-day3.html) | [Code](gee-web/day3-router) +- 第四天:[分组控制(Group)](https://geektutu.com/post/gee-day4.html) | [Code](gee-web/day4-group) +- 第五天:[中间件(Middleware)](https://geektutu.com/post/gee-day5.html) | [Code](gee-web/day5-middleware) +- 第六天:[HTML模板(Template)](https://geektutu.com/post/gee-day6.html) | [Code](gee-web/day6-template) +- 第七天:[错误恢复(Panic Recover)](https://geektutu.com/post/gee-day7.html) | [Code](gee-web/day7-panic-recover) + +### 7天用Go从零实现分布式缓存 GeeCache + +GeeCache 是一个模仿 [groupcache](https://github.com/golang/groupcache) 实现的分布式缓存系统 + +- 第一天:LRU 缓存策略 | [Code](gee-cache/day1-lru) +- 第二天:单机并发缓存 | [Code](gee-cache/day2-single-node) +- 第三天:HTTP 服务端 | [Code](gee-cache/day3-http-server) +- 第四天:一致性哈希(Hash) | [Code](gee-cache/day4-consistent-hash) +- 第五天:分布式节点 | [Code](gee-cache/day5-multi-nodes) + +### WebAssembly 使用示例 + +具体的实践过程记录在 [Go WebAssembly 简明教程](https://geektutu.com/post/quick-go-wasm.html)。 + +- 示例一:Hello World | [Code](demo-wasm/hello-world) +- 示例二:注册函数 | [Code](demo-wasm/register-functions) +- 示例三:操作 DOM | [Code](demo-wasm/manipulate-dom) +- 示例四:回调函数 | [Code](demo-wasm/callback) + +
+
+ +What can I write in 7 days? A gin-like web framework? A distributed cache like groupcache? Or a simple Python interpreter? Hope this repo can give you the answer. -Gee 的设计与实现参考了Gin,[Go Gin简明教程](https://geektutu.com/post/quick-go-gin.html)可以快速入门。 +## Web Framework - Gee -#### [教程目录](https://geektutu.com/post/gee.html) +[Gee](https://geektutu.com/post/gee.html) is a [gin](https://github.com/gin-gonic/gin)-like framework -- [第一天:前置知识(http.Handler接口)](https://geektutu.com/post/gee-day1.html),[Code - Github](gee-web/day1-http-base) -- [第二天:上下文设计(Context)](https://geektutu.com/post/gee-day2.html),[Code - Github](gee-web/day2-context) -- [第三天:Tire树路由(Router)](https://geektutu.com/post/gee-day3.html),[Code - Github](gee-web/day3-router) -- [第四天:分组控制(Group)](https://geektutu.com/post/gee-day4.html),[Code - Github](gee-web/day4-group) -- [第五天:中间件(Middleware)](https://geektutu.com/post/gee-day5.html),[Code - Github](gee-web/day5-middleware) -- [第六天:HTML模板(Template)](https://geektutu.com/post/gee-day6.html),[Code - Github](gee-web/day6-template) -- [第七天:错误恢复(Panic Recover)](https://geektutu.com/post/gee-day7.html),[Code - Github](gee-web/day7-panic-recover) +- Day 1 - http.Handler Interface Basic [Code](gee-web/day1-http-base) +- Day 2 - Design a Flexiable Context [Code](gee-web/day2-context) +- Day 3 - Router with Tire-Tree Algorithm [Code](gee-web/day3-router) +- Day 4 - Group Control [Code](gee-web/day4-group) +- Day 5 - Middleware Mechanism [Code](gee-web/day5-middleware) +- Day 6 - Embeded Template Support [Code](gee-web/day6-template) +- Day 7 - Panic Recover & Make it Robust [Code](gee-web/day7-panic-recover) -## 7天用Go从零实现分布式缓存GeeCache +## Distributed Cache - Geecache -- 第一天:LRU 缓存策略,[Code - Github](gee-cache/day1-lru) -- 第二天:单节点缓存,[Code - Github](gee-cache/day2-single-node) +Geecache is a [groupcache](https://github.com/golang/groupcache)-like distributed cache -## WebAssembly Demo +- Day 1 - LRU (Least Recently Used) Caching Strategy [Code](gee-cache/day1-lru) +- Day 2 - Single Machine Concurrent Cache [Code](gee-cache/day2-single-node) +- Day 3 - Launch a HTTP Server [Code](gee-cache/day3-http-server) +- Day 4 - Consistent Hash Algorithm [Code](gee-cache/day4-consistent-hash) +- Day 5 - Communication between Distributed Nodes [Code](gee-cache/day5-multi-nodes) -#### [教程地址](https://geektutu.com/post/quick-go-wasm.html) +## Golang WebAssembly Demo -- [1. Hello World](demo-wasm/hello-world) -- [2. 注册函数](demo-wasm/register-functions) -- [3. 操作 DOM](demo-wasm/manipulate-dom) -- [4. 回调函数](demo-wasm/callback) \ No newline at end of file +- Demo 1 - Hello World [Code](demo-wasm/hello-world) +- Demo 2 - Register Functions [Code](demo-wasm/register-functions) +- Demo 3 - Manipulate DOM [Code](demo-wasm/manipulate-dom) +- Demo 4 - Callback [Code](demo-wasm/callback) \ No newline at end of file diff --git a/gee-web/README.md b/gee-web/README.md index 3361183..67ccdf2 100644 --- a/gee-web/README.md +++ b/gee-web/README.md @@ -1,8 +1,14 @@ -# 7天用Go从零实现Web框架Gee +# 7 Days Go Web Framework Gee from Scratch + +
+README 中文版本 +
+ +## 7天用Go从零实现Web框架Gee ![Gee](doc/gee/gee.jpg) -## 教程目录 +### Content - [第一天:前置知识(http.Handler接口)](https://geektutu.com/post/gee-day1.html) - [第二天:上下文设计(Context)](https://geektutu.com/post/gee-day2.html) @@ -12,6 +18,9 @@ - [第六天:HTML模板(Template)](https://geektutu.com/post/gee-day6.html) - [第七天:错误恢复(Panic Recover)](https://geektutu.com/post/gee-day7.html) +
+
+ ## Day 1 - Static Route ```go From 4b49356dd28779f8890d3da0365a327b93ab5c15 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Thu, 6 Feb 2020 22:51:06 +0800 Subject: [PATCH 023/122] day 6, support single flight --- gee-cache/day5-multi-nodes/run.sh | 6 + .../day6-single-flight/geecache/byteview.go | 21 +++ .../day6-single-flight/geecache/cache.go | 35 +++++ .../geecache/consistenthash/consistenthash.go | 58 ++++++++ .../consistenthash/consistenthash_test.go | 43 ++++++ .../day6-single-flight/geecache/geecache.go | 136 ++++++++++++++++++ .../geecache/geecache_test.go | 48 +++++++ gee-cache/day6-single-flight/geecache/go.mod | 3 + gee-cache/day6-single-flight/geecache/http.go | 128 +++++++++++++++++ .../day6-single-flight/geecache/lru/lru.go | 79 ++++++++++ .../geecache/lru/lru_test.go | 55 +++++++ .../day6-single-flight/geecache/peers.go | 12 ++ .../geecache/singleflight/singleflight.go | 46 ++++++ .../singleflight/singleflight_test.go | 16 +++ gee-cache/day6-single-flight/go.mod | 7 + gee-cache/day6-single-flight/main.go | 87 +++++++++++ gee-cache/day6-single-flight/run.sh | 15 ++ 17 files changed, 795 insertions(+) create mode 100644 gee-cache/day6-single-flight/geecache/byteview.go create mode 100644 gee-cache/day6-single-flight/geecache/cache.go create mode 100644 gee-cache/day6-single-flight/geecache/consistenthash/consistenthash.go create mode 100644 gee-cache/day6-single-flight/geecache/consistenthash/consistenthash_test.go create mode 100644 gee-cache/day6-single-flight/geecache/geecache.go create mode 100644 gee-cache/day6-single-flight/geecache/geecache_test.go create mode 100644 gee-cache/day6-single-flight/geecache/go.mod create mode 100644 gee-cache/day6-single-flight/geecache/http.go create mode 100644 gee-cache/day6-single-flight/geecache/lru/lru.go create mode 100644 gee-cache/day6-single-flight/geecache/lru/lru_test.go create mode 100644 gee-cache/day6-single-flight/geecache/peers.go create mode 100644 gee-cache/day6-single-flight/geecache/singleflight/singleflight.go create mode 100644 gee-cache/day6-single-flight/geecache/singleflight/singleflight_test.go create mode 100644 gee-cache/day6-single-flight/go.mod create mode 100644 gee-cache/day6-single-flight/main.go create mode 100755 gee-cache/day6-single-flight/run.sh diff --git a/gee-cache/day5-multi-nodes/run.sh b/gee-cache/day5-multi-nodes/run.sh index d19b4c8..066979d 100755 --- a/gee-cache/day5-multi-nodes/run.sh +++ b/gee-cache/day5-multi-nodes/run.sh @@ -6,4 +6,10 @@ go build -o server ./server -port=8002 & ./server -port=8003 -api=1 & +sleep 2 +echo ">>> start test" +curl "http://localhost:9999/api?key=Tom" & +curl "http://localhost:9999/api?key=Tom" & +curl "http://localhost:9999/api?key=Tom" & + wait \ No newline at end of file diff --git a/gee-cache/day6-single-flight/geecache/byteview.go b/gee-cache/day6-single-flight/geecache/byteview.go new file mode 100644 index 0000000..a51394f --- /dev/null +++ b/gee-cache/day6-single-flight/geecache/byteview.go @@ -0,0 +1,21 @@ +package geecache + +// A ByteView holds an immutable view of bytes. +type ByteView struct { + b []byte +} + +// Len returns the view's length +func (v ByteView) Len() int { + return len(v.b) +} + +// ByteSlice returns a copy of the data as a byte slice. +func (v ByteView) ByteSlice() []byte { + return cloneBytes(v.b) +} + +// String returns the data as a string, making a copy if necessary. +func (v ByteView) String() string { + return string(v.b) +} diff --git a/gee-cache/day6-single-flight/geecache/cache.go b/gee-cache/day6-single-flight/geecache/cache.go new file mode 100644 index 0000000..703d033 --- /dev/null +++ b/gee-cache/day6-single-flight/geecache/cache.go @@ -0,0 +1,35 @@ +package geecache + +import ( + "geecache/lru" + "sync" +) + +type cache struct { + mu sync.RWMutex + lru *lru.Cache + cacheBytes int64 +} + +func (c *cache) add(key string, value ByteView) { + c.mu.RLock() + defer c.mu.RUnlock() + if c.lru == nil { + c.lru = lru.New(c.cacheBytes, nil) + } + c.lru.Add(key, value) +} + +func (c *cache) get(key string) (value ByteView, ok bool) { + c.mu.RLock() + defer c.mu.RUnlock() + if c.lru == nil { + return + } + + if v, ok := c.lru.Get(key); ok { + return v.(ByteView), ok + } + + return +} diff --git a/gee-cache/day6-single-flight/geecache/consistenthash/consistenthash.go b/gee-cache/day6-single-flight/geecache/consistenthash/consistenthash.go new file mode 100644 index 0000000..c8c9082 --- /dev/null +++ b/gee-cache/day6-single-flight/geecache/consistenthash/consistenthash.go @@ -0,0 +1,58 @@ +package consistenthash + +import ( + "hash/crc32" + "sort" + "strconv" +) + +// Hash maps bytes to uint32 +type Hash func(data []byte) uint32 + +// Map constains all hashed keys +type Map struct { + hash Hash + replicas int + keys []int // Sorted + hashMap map[int]string +} + +// New creates a Map instance +func New(replicas int, fn Hash) *Map { + m := &Map{ + replicas: replicas, + hash: fn, + hashMap: make(map[int]string), + } + if m.hash == nil { + m.hash = crc32.ChecksumIEEE + } + return m +} + +// Add adds some keys to the hash. +func (m *Map) Add(keys ...string) { + for _, key := range keys { + for i := 0; i < m.replicas; i++ { + hash := int(m.hash([]byte(strconv.Itoa(i) + key))) + m.keys = append(m.keys, hash) + m.hashMap[hash] = key + } + } + sort.Ints(m.keys) +} + +// Get gets the closest item in the hash to the provided key. +func (m *Map) Get(key string) string { + if len(m.keys) == 0 { + return "" + } + + hash := int(m.hash([]byte(key))) + // Binary search for appropriate replica. + idx := sort.Search(len(m.keys), func(i int) bool { + return m.keys[i] >= hash + }) + + return m.hashMap[m.keys[idx%len(m.keys)]] +} diff --git a/gee-cache/day6-single-flight/geecache/consistenthash/consistenthash_test.go b/gee-cache/day6-single-flight/geecache/consistenthash/consistenthash_test.go new file mode 100644 index 0000000..34e1275 --- /dev/null +++ b/gee-cache/day6-single-flight/geecache/consistenthash/consistenthash_test.go @@ -0,0 +1,43 @@ +package consistenthash + +import ( + "strconv" + "testing" +) + +func TestHashing(t *testing.T) { + hash := New(3, func(key []byte) uint32 { + i, _ := strconv.Atoi(string(key)) + return uint32(i) + }) + + // Given the above hash function, this will give replicas with "hashes": + // 2, 4, 6, 12, 14, 16, 22, 24, 26 + hash.Add("6", "4", "2") + + testCases := map[string]string{ + "2": "2", + "11": "2", + "23": "4", + "27": "2", + } + + for k, v := range testCases { + if hash.Get(k) != v { + t.Errorf("Asking for %s, should have yielded %s", k, v) + } + } + + // Adds 8, 18, 28 + hash.Add("8") + + // 27 should now map to 8. + testCases["27"] = "8" + + for k, v := range testCases { + if hash.Get(k) != v { + t.Errorf("Asking for %s, should have yielded %s", k, v) + } + } + +} diff --git a/gee-cache/day6-single-flight/geecache/geecache.go b/gee-cache/day6-single-flight/geecache/geecache.go new file mode 100644 index 0000000..3965674 --- /dev/null +++ b/gee-cache/day6-single-flight/geecache/geecache.go @@ -0,0 +1,136 @@ +package geecache + +import ( + "fmt" + "geecache/singleflight" + "log" + "sync" +) + +// A Group is a cache namespace and associated data loaded spread over +type Group struct { + name string + getter Getter + mainCache cache + peers PeerPicker + // use singleflight.Group to make sure that + // each key is only fetched once + loader *singleflight.Group +} + +// A Getter loads data for a key. +type Getter interface { + Get(key string) ([]byte, error) +} + +// A GetterFunc implements Getter with a function. +type GetterFunc func(key string) ([]byte, error) + +// Get implements Getter interface function +func (f GetterFunc) Get(key string) ([]byte, error) { + return f(key) +} + +var ( + mu sync.RWMutex + groups = make(map[string]*Group) +) + +// NewGroup create a new instance of Group +func NewGroup(name string, cacheBytes int64, getter Getter) *Group { + if getter == nil { + panic("nil Getter") + } + mu.Lock() + defer mu.Unlock() + g := &Group{ + name: name, + getter: getter, + mainCache: cache{cacheBytes: cacheBytes}, + loader: &singleflight.Group{}, + } + groups[name] = g + return g +} + +// GetGroup returns the named group previously created with NewGroup, or +// nil if there's no such group. +func GetGroup(name string) *Group { + mu.RLock() + g := groups[name] + mu.RUnlock() + return g +} + +// Get value for a key from cache +func (g *Group) Get(key string) (ByteView, error) { + if key == "" { + return ByteView{}, fmt.Errorf("key is required") + } + + if v, ok := g.mainCache.get(key); ok { + log.Println("[GeeCache] hit") + return v, nil + } + + return g.load(key) +} + +// RegisterPeers registers a PeerPicker for choosing remote peer +func (g *Group) RegisterPeers(peers PeerPicker) { + if g.peers != nil { + panic("RegisterPeerPicker called more than once") + } + g.peers = peers +} + +func cloneBytes(b []byte) []byte { + c := make([]byte, len(b)) + copy(c, b) + return c +} + +func (g *Group) load(key string) (value ByteView, err error) { + // each key is only fetched once (either locally or remotely) + // regardless of the number of concurrent callers. + viewi, err := g.loader.Do(key, func() (interface{}, error) { + if g.peers != nil { + if peer, ok := g.peers.PickPeer(key); ok { + if value, err = g.getFromPeer(peer, key); err == nil { + return value, nil + } + log.Println("[GeeCache] Failed to get from peer", err) + } + } + + return g.getLocally(key) + }) + + if err == nil { + return viewi.(ByteView), nil + } + return +} + +func (g *Group) populateCache(key string, value ByteView) { + g.mainCache.add(key, value) +} + +func (g *Group) getLocally(key string) (ByteView, error) { + bytes, err := g.getter.Get(key) + if err != nil { + return ByteView{}, err + + } + value := ByteView{b: cloneBytes(bytes)} + g.populateCache(key, value) + return value, nil +} + +func (g *Group) getFromPeer(peer PeerGetter, key string) (ByteView, error) { + bytes, err := peer.Get(g.name, key) + if err != nil { + return ByteView{}, err + } + return ByteView{b: bytes}, nil +} diff --git a/gee-cache/day6-single-flight/geecache/geecache_test.go b/gee-cache/day6-single-flight/geecache/geecache_test.go new file mode 100644 index 0000000..9cb4079 --- /dev/null +++ b/gee-cache/day6-single-flight/geecache/geecache_test.go @@ -0,0 +1,48 @@ +package geecache + +import ( + "fmt" + "log" + "testing" +) + +var db = map[string]string{ + "Tom": "630", + "Jack": "589", + "Sam": "567", +} + +func TestGet(t *testing.T) { + gee := NewGroup("scores", 2<<10, GetterFunc( + func(key string) ([]byte, error) { + log.Println("[SlowDB] search key", key) + if v, ok := db[key]; ok { + return []byte(v), nil + } + return nil, fmt.Errorf("%s not exist", key) + })) + + for k, v := range db { + view, err := gee.Get(k) + if err != nil || view.String() != v { + t.Fatal("failed to get value of Tom") + } + } + + if view, err := gee.Get("unknown"); err == nil { + t.Fatalf("the value of unknow should be empty, but %s got", view) + } +} + +func TestGetGroup(t *testing.T) { + groupName := "scores" + NewGroup(groupName, 2<<10, GetterFunc( + func(key string) (bytes []byte, err error) { return })) + if group := GetGroup(groupName); group == nil || group.name != groupName { + t.Fatalf("group %s not exist", groupName) + } + + if group := GetGroup(groupName + "111"); group != nil { + t.Fatalf("expect nil, but %s got", group.name) + } +} diff --git a/gee-cache/day6-single-flight/geecache/go.mod b/gee-cache/day6-single-flight/geecache/go.mod new file mode 100644 index 0000000..f9d454e --- /dev/null +++ b/gee-cache/day6-single-flight/geecache/go.mod @@ -0,0 +1,3 @@ +module geecache + +go 1.13 diff --git a/gee-cache/day6-single-flight/geecache/http.go b/gee-cache/day6-single-flight/geecache/http.go new file mode 100644 index 0000000..d7da2c3 --- /dev/null +++ b/gee-cache/day6-single-flight/geecache/http.go @@ -0,0 +1,128 @@ +package geecache + +import ( + "fmt" + "geecache/consistenthash" + "io/ioutil" + "log" + "net/http" + "net/url" + "strings" + "sync" +) + +const ( + defaultBasePath = "/_geecache/" + defaultReplicas = 50 +) + +// HTTPPool implements PeerPicker for a pool of HTTP peers. +type HTTPPool struct { + // this peer's base URL, e.g. "https://example.net:8000" + self string + basePath string + mu sync.Mutex // guards peers and httpGetters + peers *consistenthash.Map + httpGetters map[string]*httpGetter // keyed by e.g. "http://10.0.0.2:8008" +} + +// NewHTTPPool initializes an HTTP pool of peers, and registers itself as a PeerPicker. +func NewHTTPPool(self string) *HTTPPool { + return &HTTPPool{ + self: self, + basePath: defaultBasePath, + } +} + +// Log info with server name +func (p *HTTPPool) Log(format string, v ...interface{}) { + log.Printf("[Server %s] %s", p.self, fmt.Sprintf(format, v...)) +} + +// ServeHTTP handle all http requests +func (p *HTTPPool) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if !strings.HasPrefix(r.URL.Path, p.basePath) { + panic("HTTPPool serving unexpected path: " + r.URL.Path) + } + p.Log("%s %s", r.Method, r.URL.Path) + // /// required + parts := strings.SplitN(r.URL.Path[len(p.basePath):], "/", 2) + if len(parts) != 2 { + http.Error(w, "bad request", http.StatusBadRequest) + return + } + + groupName := parts[0] + key := parts[1] + + group := GetGroup(groupName) + if group == nil { + http.Error(w, "no such group: "+groupName, http.StatusNotFound) + return + } + + view, err := group.Get(key) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/octet-stream") + w.Write(view.ByteSlice()) +} + +// Set updates the pool's list of peers. +func (p *HTTPPool) Set(peers ...string) { + p.mu.Lock() + defer p.mu.Unlock() + p.peers = consistenthash.New(defaultReplicas, nil) + p.peers.Add(peers...) + p.httpGetters = make(map[string]*httpGetter, len(peers)) + for _, peer := range peers { + p.httpGetters[peer] = &httpGetter{baseURL: peer + p.basePath} + } +} + +// PickPeer picks a peer according to key +func (p *HTTPPool) PickPeer(key string) (PeerGetter, bool) { + p.mu.Lock() + defer p.mu.Unlock() + if peer := p.peers.Get(key); peer != "" && peer != p.self { + p.Log("Pick peer %s", peer) + return p.httpGetters[peer], true + } + return nil, false +} + +var _ PeerPicker = (*HTTPPool)(nil) + +type httpGetter struct { + baseURL string +} + +func (h *httpGetter) Get(group string, key string) ([]byte, error) { + u := fmt.Sprintf( + "%v%v/%v", + h.baseURL, + url.QueryEscape(group), + url.QueryEscape(key), + ) + res, err := http.Get(u) + if err != nil { + return nil, err + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("server returned: %v", res.Status) + } + + bytes, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, fmt.Errorf("reading response body: %v", err) + } + + return bytes, nil +} + +var _ PeerGetter = (*httpGetter)(nil) diff --git a/gee-cache/day6-single-flight/geecache/lru/lru.go b/gee-cache/day6-single-flight/geecache/lru/lru.go new file mode 100644 index 0000000..81eee43 --- /dev/null +++ b/gee-cache/day6-single-flight/geecache/lru/lru.go @@ -0,0 +1,79 @@ +package lru + +import "container/list" + +// Cache is a LRU cache. It is not safe for concurrent access. +type Cache struct { + maxBytes int64 + nbytes int64 + ll *list.List + cache map[string]*list.Element + // optional and executed when an entry is purged. + OnEvicted func(key string, value Value) +} + +type entry struct { + key string + value Value +} + +// Value use Len to count how many bytes it takes +type Value interface { + Len() int +} + +// New is the Constructor of Cache +func New(maxBytes int64, onEvicted func(string, Value)) *Cache { + return &Cache{ + maxBytes: maxBytes, + ll: list.New(), + cache: make(map[string]*list.Element), + OnEvicted: onEvicted, + } +} + +// Add adds a value to the cache. +func (c *Cache) Add(key string, value Value) { + if ele, ok := c.cache[key]; ok { + c.ll.MoveToFront(ele) + kv := ele.Value.(*entry) + kv.value = value + return + } + ele := c.ll.PushFront(&entry{key, value}) + c.cache[key] = ele + c.nbytes += int64(len(key)) + int64(value.Len()) + + for c.maxBytes != 0 && c.maxBytes < c.nbytes { + c.RemoveOldest() + } +} + +// Get look ups a key's value +func (c *Cache) Get(key string) (value Value, ok bool) { + if ele, ok := c.cache[key]; ok { + c.ll.MoveToFront(ele) + kv := ele.Value.(*entry) + return kv.value, true + } + return +} + +// RemoveOldest removes the oldest item +func (c *Cache) RemoveOldest() { + ele := c.ll.Back() + if ele != nil { + c.ll.Remove(ele) + kv := ele.Value.(*entry) + delete(c.cache, kv.key) + c.nbytes -= int64(len(kv.key)) + int64(kv.value.Len()) + if c.OnEvicted != nil { + c.OnEvicted(kv.key, kv.value) + } + } +} + +// Len the number of cache entries +func (c *Cache) Len() int { + return c.ll.Len() +} diff --git a/gee-cache/day6-single-flight/geecache/lru/lru_test.go b/gee-cache/day6-single-flight/geecache/lru/lru_test.go new file mode 100644 index 0000000..7308322 --- /dev/null +++ b/gee-cache/day6-single-flight/geecache/lru/lru_test.go @@ -0,0 +1,55 @@ +package lru + +import ( + "reflect" + "testing" +) + +type String string + +func (d String) Len() int { + return len(d) +} + +func TestGet(t *testing.T) { + lru := New(int64(0), nil) + lru.Add("key1", String("1234")) + if v, ok := lru.Get("key1"); !ok || string(v.(String)) != "1234" { + t.Fatalf("cache hit key1=1234 failed") + } + if _, ok := lru.Get("key2"); ok { + t.Fatalf("cache miss key2 failed") + } +} + +func TestRemoveoldest(t *testing.T) { + k1, k2, k3 := "key1", "key2", "k3" + v1, v2, v3 := "value1", "value2", "v3" + cap := len(k1 + k2 + v1 + v2) + lru := New(int64(cap), nil) + lru.Add(k1, String(v1)) + lru.Add(k2, String(v2)) + lru.Add(k3, String(v3)) + + if _, ok := lru.Get("key1"); ok || lru.Len() != 2 { + t.Fatalf("Removeoldest key1 failed") + } +} + +func TestOnEvicted(t *testing.T) { + keys := make([]string, 0) + callback := func(key string, value Value) { + keys = append(keys, key) + } + lru := New(int64(10), callback) + lru.Add("key1", String("123456")) + lru.Add("k2", String("k2")) + lru.Add("k3", String("k3")) + lru.Add("k4", String("k4")) + + expect := []string{"key1", "k2"} + + if !reflect.DeepEqual(expect, keys) { + t.Fatalf("Call OnEvicted failed, expect keys equals to %s", expect) + } +} diff --git a/gee-cache/day6-single-flight/geecache/peers.go b/gee-cache/day6-single-flight/geecache/peers.go new file mode 100644 index 0000000..8d010e2 --- /dev/null +++ b/gee-cache/day6-single-flight/geecache/peers.go @@ -0,0 +1,12 @@ +package geecache + +// PeerPicker is the interface that must be implemented to locate +// the peer that owns a specific key. +type PeerPicker interface { + PickPeer(key string) (peer PeerGetter, ok bool) +} + +// PeerGetter is the interface that must be implemented by a peer. +type PeerGetter interface { + Get(group string, key string) ([]byte, error) +} diff --git a/gee-cache/day6-single-flight/geecache/singleflight/singleflight.go b/gee-cache/day6-single-flight/geecache/singleflight/singleflight.go new file mode 100644 index 0000000..85bd0dd --- /dev/null +++ b/gee-cache/day6-single-flight/geecache/singleflight/singleflight.go @@ -0,0 +1,46 @@ +package singleflight + +import "sync" + +// call is an in-flight or completed Do call +type call struct { + wg sync.WaitGroup + val interface{} + err error +} + +// Group represents a class of work and forms a namespace in which +// units of work can be executed with duplicate suppression. +type Group struct { + mu sync.Mutex // protects m + m map[string]*call // lazily initialized +} + +// Do executes and returns the results of the given function, making +// sure that only one execution is in-flight for a given key at a +// time. If a duplicate comes in, the duplicate caller waits for the +// original to complete and receives the same results. +func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) { + g.mu.Lock() + if g.m == nil { + g.m = make(map[string]*call) + } + if c, ok := g.m[key]; ok { + g.mu.Unlock() + c.wg.Wait() + return c.val, c.err + } + c := new(call) + c.wg.Add(1) + g.m[key] = c + g.mu.Unlock() + + c.val, c.err = fn() + c.wg.Done() + + g.mu.Lock() + delete(g.m, key) + g.mu.Unlock() + + return c.val, c.err +} diff --git a/gee-cache/day6-single-flight/geecache/singleflight/singleflight_test.go b/gee-cache/day6-single-flight/geecache/singleflight/singleflight_test.go new file mode 100644 index 0000000..450951a --- /dev/null +++ b/gee-cache/day6-single-flight/geecache/singleflight/singleflight_test.go @@ -0,0 +1,16 @@ +package singleflight + +import ( + "testing" +) + +func TestDo(t *testing.T) { + var g Group + v, err := g.Do("key", func() (interface{}, error) { + return "bar", nil + }) + + if v != "bar" || err != nil { + t.Errorf("Do v = %v, error = %v", v, err) + } +} diff --git a/gee-cache/day6-single-flight/go.mod b/gee-cache/day6-single-flight/go.mod new file mode 100644 index 0000000..d0fd3ba --- /dev/null +++ b/gee-cache/day6-single-flight/go.mod @@ -0,0 +1,7 @@ +module example + +go 1.13 + +require geecache v0.0.0 + +replace geecache => ./geecache diff --git a/gee-cache/day6-single-flight/main.go b/gee-cache/day6-single-flight/main.go new file mode 100644 index 0000000..a99940c --- /dev/null +++ b/gee-cache/day6-single-flight/main.go @@ -0,0 +1,87 @@ +package main + +/* +$ curl "http://localhost:9999/api?key=Tom" +630 + +$ curl "http://localhost:9999/api?key=kkk" +kkk not exist +*/ + +import ( + "flag" + "fmt" + "geecache" + "log" + "net/http" +) + +var db = map[string]string{ + "Tom": "630", + "Jack": "589", + "Sam": "567", +} + +func createGroup() *geecache.Group { + return geecache.NewGroup("scores", 2<<10, geecache.GetterFunc( + func(key string) ([]byte, error) { + log.Println("[SlowDB] search key", key) + if v, ok := db[key]; ok { + return []byte(v), nil + } + return nil, fmt.Errorf("%s not exist", key) + })) +} + +func startCacheServer(addr string, addrs []string, gee *geecache.Group) { + peers := geecache.NewHTTPPool(addr) + peers.Set(addrs...) + gee.RegisterPeers(peers) + log.Println("geecache is running at", addr) + log.Fatal(http.ListenAndServe(addr[7:], peers)) +} + +func startAPIServer(apiAddr string, gee *geecache.Group) { + http.Handle("/api", http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + key := r.URL.Query().Get("key") + view, err := gee.Get(key) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/octet-stream") + w.Write(view.ByteSlice()) + + })) + log.Println("fontend server is running at", apiAddr) + log.Fatal(http.ListenAndServe(apiAddr[7:], nil)) + +} + +func main() { + var port int + var api bool + flag.IntVar(&port, "port", 8001, "Geecache server port") + flag.BoolVar(&api, "api", false, "Start a api server?") + flag.Parse() + + apiAddr := "http://localhost:9999" + addrMap := map[int]string{ + 8001: "http://localhost:8001", + 8002: "http://localhost:8002", + 8003: "http://localhost:8003", + } + + addrs := make([]string, 3) + + for _, v := range addrMap { + addrs = append(addrs, v) + } + + gee := createGroup() + if api { + go startAPIServer(apiAddr, gee) + } + startCacheServer(addrMap[port], []string(addrs), gee) +} diff --git a/gee-cache/day6-single-flight/run.sh b/gee-cache/day6-single-flight/run.sh new file mode 100755 index 0000000..066979d --- /dev/null +++ b/gee-cache/day6-single-flight/run.sh @@ -0,0 +1,15 @@ +#!/bin/bash +trap "rm server;kill 0" EXIT + +go build -o server +./server -port=8001 & +./server -port=8002 & +./server -port=8003 -api=1 & + +sleep 2 +echo ">>> start test" +curl "http://localhost:9999/api?key=Tom" & +curl "http://localhost:9999/api?key=Tom" & +curl "http://localhost:9999/api?key=Tom" & + +wait \ No newline at end of file From 353ffe60d5eb825f6cf843cb045e3e3a1d5cfc2f Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Thu, 6 Feb 2020 23:32:31 +0800 Subject: [PATCH 024/122] day7 support proto --- gee-cache/day7-proto-buf/geecache/byteview.go | 21 +++ gee-cache/day7-proto-buf/geecache/cache.go | 35 +++++ .../geecache/consistenthash/consistenthash.go | 58 +++++++ .../consistenthash/consistenthash_test.go | 43 ++++++ gee-cache/day7-proto-buf/geecache/geecache.go | 142 ++++++++++++++++++ .../day7-proto-buf/geecache/geecache_test.go | 48 ++++++ .../geecache/geecachepb/geecachepb.pb.go | 128 ++++++++++++++++ .../geecache/geecachepb/geecachepb.proto | 16 ++ gee-cache/day7-proto-buf/geecache/go.mod | 5 + gee-cache/day7-proto-buf/geecache/go.sum | 2 + gee-cache/day7-proto-buf/geecache/http.go | 135 +++++++++++++++++ gee-cache/day7-proto-buf/geecache/lru/lru.go | 79 ++++++++++ .../day7-proto-buf/geecache/lru/lru_test.go | 55 +++++++ gee-cache/day7-proto-buf/geecache/peers.go | 14 ++ .../geecache/singleflight/singleflight.go | 46 ++++++ .../singleflight/singleflight_test.go | 16 ++ gee-cache/day7-proto-buf/go.mod | 7 + gee-cache/day7-proto-buf/go.sum | 2 + gee-cache/day7-proto-buf/main.go | 87 +++++++++++ gee-cache/day7-proto-buf/run.sh | 15 ++ 20 files changed, 954 insertions(+) create mode 100644 gee-cache/day7-proto-buf/geecache/byteview.go create mode 100644 gee-cache/day7-proto-buf/geecache/cache.go create mode 100644 gee-cache/day7-proto-buf/geecache/consistenthash/consistenthash.go create mode 100644 gee-cache/day7-proto-buf/geecache/consistenthash/consistenthash_test.go create mode 100644 gee-cache/day7-proto-buf/geecache/geecache.go create mode 100644 gee-cache/day7-proto-buf/geecache/geecache_test.go create mode 100644 gee-cache/day7-proto-buf/geecache/geecachepb/geecachepb.pb.go create mode 100644 gee-cache/day7-proto-buf/geecache/geecachepb/geecachepb.proto create mode 100644 gee-cache/day7-proto-buf/geecache/go.mod create mode 100644 gee-cache/day7-proto-buf/geecache/go.sum create mode 100644 gee-cache/day7-proto-buf/geecache/http.go create mode 100644 gee-cache/day7-proto-buf/geecache/lru/lru.go create mode 100644 gee-cache/day7-proto-buf/geecache/lru/lru_test.go create mode 100644 gee-cache/day7-proto-buf/geecache/peers.go create mode 100644 gee-cache/day7-proto-buf/geecache/singleflight/singleflight.go create mode 100644 gee-cache/day7-proto-buf/geecache/singleflight/singleflight_test.go create mode 100644 gee-cache/day7-proto-buf/go.mod create mode 100644 gee-cache/day7-proto-buf/go.sum create mode 100644 gee-cache/day7-proto-buf/main.go create mode 100755 gee-cache/day7-proto-buf/run.sh diff --git a/gee-cache/day7-proto-buf/geecache/byteview.go b/gee-cache/day7-proto-buf/geecache/byteview.go new file mode 100644 index 0000000..a51394f --- /dev/null +++ b/gee-cache/day7-proto-buf/geecache/byteview.go @@ -0,0 +1,21 @@ +package geecache + +// A ByteView holds an immutable view of bytes. +type ByteView struct { + b []byte +} + +// Len returns the view's length +func (v ByteView) Len() int { + return len(v.b) +} + +// ByteSlice returns a copy of the data as a byte slice. +func (v ByteView) ByteSlice() []byte { + return cloneBytes(v.b) +} + +// String returns the data as a string, making a copy if necessary. +func (v ByteView) String() string { + return string(v.b) +} diff --git a/gee-cache/day7-proto-buf/geecache/cache.go b/gee-cache/day7-proto-buf/geecache/cache.go new file mode 100644 index 0000000..703d033 --- /dev/null +++ b/gee-cache/day7-proto-buf/geecache/cache.go @@ -0,0 +1,35 @@ +package geecache + +import ( + "geecache/lru" + "sync" +) + +type cache struct { + mu sync.RWMutex + lru *lru.Cache + cacheBytes int64 +} + +func (c *cache) add(key string, value ByteView) { + c.mu.RLock() + defer c.mu.RUnlock() + if c.lru == nil { + c.lru = lru.New(c.cacheBytes, nil) + } + c.lru.Add(key, value) +} + +func (c *cache) get(key string) (value ByteView, ok bool) { + c.mu.RLock() + defer c.mu.RUnlock() + if c.lru == nil { + return + } + + if v, ok := c.lru.Get(key); ok { + return v.(ByteView), ok + } + + return +} diff --git a/gee-cache/day7-proto-buf/geecache/consistenthash/consistenthash.go b/gee-cache/day7-proto-buf/geecache/consistenthash/consistenthash.go new file mode 100644 index 0000000..c8c9082 --- /dev/null +++ b/gee-cache/day7-proto-buf/geecache/consistenthash/consistenthash.go @@ -0,0 +1,58 @@ +package consistenthash + +import ( + "hash/crc32" + "sort" + "strconv" +) + +// Hash maps bytes to uint32 +type Hash func(data []byte) uint32 + +// Map constains all hashed keys +type Map struct { + hash Hash + replicas int + keys []int // Sorted + hashMap map[int]string +} + +// New creates a Map instance +func New(replicas int, fn Hash) *Map { + m := &Map{ + replicas: replicas, + hash: fn, + hashMap: make(map[int]string), + } + if m.hash == nil { + m.hash = crc32.ChecksumIEEE + } + return m +} + +// Add adds some keys to the hash. +func (m *Map) Add(keys ...string) { + for _, key := range keys { + for i := 0; i < m.replicas; i++ { + hash := int(m.hash([]byte(strconv.Itoa(i) + key))) + m.keys = append(m.keys, hash) + m.hashMap[hash] = key + } + } + sort.Ints(m.keys) +} + +// Get gets the closest item in the hash to the provided key. +func (m *Map) Get(key string) string { + if len(m.keys) == 0 { + return "" + } + + hash := int(m.hash([]byte(key))) + // Binary search for appropriate replica. + idx := sort.Search(len(m.keys), func(i int) bool { + return m.keys[i] >= hash + }) + + return m.hashMap[m.keys[idx%len(m.keys)]] +} diff --git a/gee-cache/day7-proto-buf/geecache/consistenthash/consistenthash_test.go b/gee-cache/day7-proto-buf/geecache/consistenthash/consistenthash_test.go new file mode 100644 index 0000000..34e1275 --- /dev/null +++ b/gee-cache/day7-proto-buf/geecache/consistenthash/consistenthash_test.go @@ -0,0 +1,43 @@ +package consistenthash + +import ( + "strconv" + "testing" +) + +func TestHashing(t *testing.T) { + hash := New(3, func(key []byte) uint32 { + i, _ := strconv.Atoi(string(key)) + return uint32(i) + }) + + // Given the above hash function, this will give replicas with "hashes": + // 2, 4, 6, 12, 14, 16, 22, 24, 26 + hash.Add("6", "4", "2") + + testCases := map[string]string{ + "2": "2", + "11": "2", + "23": "4", + "27": "2", + } + + for k, v := range testCases { + if hash.Get(k) != v { + t.Errorf("Asking for %s, should have yielded %s", k, v) + } + } + + // Adds 8, 18, 28 + hash.Add("8") + + // 27 should now map to 8. + testCases["27"] = "8" + + for k, v := range testCases { + if hash.Get(k) != v { + t.Errorf("Asking for %s, should have yielded %s", k, v) + } + } + +} diff --git a/gee-cache/day7-proto-buf/geecache/geecache.go b/gee-cache/day7-proto-buf/geecache/geecache.go new file mode 100644 index 0000000..44161bc --- /dev/null +++ b/gee-cache/day7-proto-buf/geecache/geecache.go @@ -0,0 +1,142 @@ +package geecache + +import ( + "fmt" + pb "geecache/geecachepb" + "geecache/singleflight" + "log" + "sync" +) + +// A Group is a cache namespace and associated data loaded spread over +type Group struct { + name string + getter Getter + mainCache cache + peers PeerPicker + // use singleflight.Group to make sure that + // each key is only fetched once + loader *singleflight.Group +} + +// A Getter loads data for a key. +type Getter interface { + Get(key string) ([]byte, error) +} + +// A GetterFunc implements Getter with a function. +type GetterFunc func(key string) ([]byte, error) + +// Get implements Getter interface function +func (f GetterFunc) Get(key string) ([]byte, error) { + return f(key) +} + +var ( + mu sync.RWMutex + groups = make(map[string]*Group) +) + +// NewGroup create a new instance of Group +func NewGroup(name string, cacheBytes int64, getter Getter) *Group { + if getter == nil { + panic("nil Getter") + } + mu.Lock() + defer mu.Unlock() + g := &Group{ + name: name, + getter: getter, + mainCache: cache{cacheBytes: cacheBytes}, + loader: &singleflight.Group{}, + } + groups[name] = g + return g +} + +// GetGroup returns the named group previously created with NewGroup, or +// nil if there's no such group. +func GetGroup(name string) *Group { + mu.RLock() + g := groups[name] + mu.RUnlock() + return g +} + +// Get value for a key from cache +func (g *Group) Get(key string) (ByteView, error) { + if key == "" { + return ByteView{}, fmt.Errorf("key is required") + } + + if v, ok := g.mainCache.get(key); ok { + log.Println("[GeeCache] hit") + return v, nil + } + + return g.load(key) +} + +// RegisterPeers registers a PeerPicker for choosing remote peer +func (g *Group) RegisterPeers(peers PeerPicker) { + if g.peers != nil { + panic("RegisterPeerPicker called more than once") + } + g.peers = peers +} + +func cloneBytes(b []byte) []byte { + c := make([]byte, len(b)) + copy(c, b) + return c +} + +func (g *Group) load(key string) (value ByteView, err error) { + // each key is only fetched once (either locally or remotely) + // regardless of the number of concurrent callers. + viewi, err := g.loader.Do(key, func() (interface{}, error) { + if g.peers != nil { + if peer, ok := g.peers.PickPeer(key); ok { + if value, err = g.getFromPeer(peer, key); err == nil { + return value, nil + } + log.Println("[GeeCache] Failed to get from peer", err) + } + } + + return g.getLocally(key) + }) + + if err == nil { + return viewi.(ByteView), nil + } + return +} + +func (g *Group) populateCache(key string, value ByteView) { + g.mainCache.add(key, value) +} + +func (g *Group) getLocally(key string) (ByteView, error) { + bytes, err := g.getter.Get(key) + if err != nil { + return ByteView{}, err + + } + value := ByteView{b: cloneBytes(bytes)} + g.populateCache(key, value) + return value, nil +} + +func (g *Group) getFromPeer(peer PeerGetter, key string) (ByteView, error) { + req := &pb.Request{ + Group: g.name, + Key: key, + } + res := &pb.Response{} + err := peer.Get(req, res) + if err != nil { + return ByteView{}, err + } + return ByteView{b: res.Value}, nil +} diff --git a/gee-cache/day7-proto-buf/geecache/geecache_test.go b/gee-cache/day7-proto-buf/geecache/geecache_test.go new file mode 100644 index 0000000..9cb4079 --- /dev/null +++ b/gee-cache/day7-proto-buf/geecache/geecache_test.go @@ -0,0 +1,48 @@ +package geecache + +import ( + "fmt" + "log" + "testing" +) + +var db = map[string]string{ + "Tom": "630", + "Jack": "589", + "Sam": "567", +} + +func TestGet(t *testing.T) { + gee := NewGroup("scores", 2<<10, GetterFunc( + func(key string) ([]byte, error) { + log.Println("[SlowDB] search key", key) + if v, ok := db[key]; ok { + return []byte(v), nil + } + return nil, fmt.Errorf("%s not exist", key) + })) + + for k, v := range db { + view, err := gee.Get(k) + if err != nil || view.String() != v { + t.Fatal("failed to get value of Tom") + } + } + + if view, err := gee.Get("unknown"); err == nil { + t.Fatalf("the value of unknow should be empty, but %s got", view) + } +} + +func TestGetGroup(t *testing.T) { + groupName := "scores" + NewGroup(groupName, 2<<10, GetterFunc( + func(key string) (bytes []byte, err error) { return })) + if group := GetGroup(groupName); group == nil || group.name != groupName { + t.Fatalf("group %s not exist", groupName) + } + + if group := GetGroup(groupName + "111"); group != nil { + t.Fatalf("expect nil, but %s got", group.name) + } +} diff --git a/gee-cache/day7-proto-buf/geecache/geecachepb/geecachepb.pb.go b/gee-cache/day7-proto-buf/geecache/geecachepb/geecachepb.pb.go new file mode 100644 index 0000000..d89521d --- /dev/null +++ b/gee-cache/day7-proto-buf/geecache/geecachepb/geecachepb.pb.go @@ -0,0 +1,128 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: geecachepb.proto + +package geecachepb + +import ( + fmt "fmt" + proto "github.com/golang/protobuf/proto" + math "math" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package + +type Request struct { + Group string `protobuf:"bytes,1,opt,name=group,proto3" json:"group,omitempty"` + Key string `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Request) Reset() { *m = Request{} } +func (m *Request) String() string { return proto.CompactTextString(m) } +func (*Request) ProtoMessage() {} +func (*Request) Descriptor() ([]byte, []int) { + return fileDescriptor_889d0a4ad37a0d42, []int{0} +} + +func (m *Request) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Request.Unmarshal(m, b) +} +func (m *Request) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Request.Marshal(b, m, deterministic) +} +func (m *Request) XXX_Merge(src proto.Message) { + xxx_messageInfo_Request.Merge(m, src) +} +func (m *Request) XXX_Size() int { + return xxx_messageInfo_Request.Size(m) +} +func (m *Request) XXX_DiscardUnknown() { + xxx_messageInfo_Request.DiscardUnknown(m) +} + +var xxx_messageInfo_Request proto.InternalMessageInfo + +func (m *Request) GetGroup() string { + if m != nil { + return m.Group + } + return "" +} + +func (m *Request) GetKey() string { + if m != nil { + return m.Key + } + return "" +} + +type Response struct { + Value []byte `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Response) Reset() { *m = Response{} } +func (m *Response) String() string { return proto.CompactTextString(m) } +func (*Response) ProtoMessage() {} +func (*Response) Descriptor() ([]byte, []int) { + return fileDescriptor_889d0a4ad37a0d42, []int{1} +} + +func (m *Response) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Response.Unmarshal(m, b) +} +func (m *Response) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Response.Marshal(b, m, deterministic) +} +func (m *Response) XXX_Merge(src proto.Message) { + xxx_messageInfo_Response.Merge(m, src) +} +func (m *Response) XXX_Size() int { + return xxx_messageInfo_Response.Size(m) +} +func (m *Response) XXX_DiscardUnknown() { + xxx_messageInfo_Response.DiscardUnknown(m) +} + +var xxx_messageInfo_Response proto.InternalMessageInfo + +func (m *Response) GetValue() []byte { + if m != nil { + return m.Value + } + return nil +} + +func init() { + proto.RegisterType((*Request)(nil), "geecachepb.Request") + proto.RegisterType((*Response)(nil), "geecachepb.Response") +} + +func init() { proto.RegisterFile("geecachepb.proto", fileDescriptor_889d0a4ad37a0d42) } + +var fileDescriptor_889d0a4ad37a0d42 = []byte{ + // 148 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0x48, 0x4f, 0x4d, 0x4d, + 0x4e, 0x4c, 0xce, 0x48, 0x2d, 0x48, 0xd2, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x42, 0x88, + 0x28, 0x19, 0x72, 0xb1, 0x07, 0xa5, 0x16, 0x96, 0xa6, 0x16, 0x97, 0x08, 0x89, 0x70, 0xb1, 0xa6, + 0x17, 0xe5, 0x97, 0x16, 0x48, 0x30, 0x2a, 0x30, 0x6a, 0x70, 0x06, 0x41, 0x38, 0x42, 0x02, 0x5c, + 0xcc, 0xd9, 0xa9, 0x95, 0x12, 0x4c, 0x60, 0x31, 0x10, 0x53, 0x49, 0x81, 0x8b, 0x23, 0x28, 0xb5, + 0xb8, 0x20, 0x3f, 0xaf, 0x38, 0x15, 0xa4, 0xa7, 0x2c, 0x31, 0xa7, 0x34, 0x15, 0xac, 0x87, 0x27, + 0x08, 0xc2, 0x31, 0xb2, 0xe3, 0xe2, 0x72, 0x07, 0x69, 0x76, 0x06, 0x59, 0x22, 0x64, 0xc0, 0xc5, + 0xec, 0x9e, 0x5a, 0x22, 0x24, 0xac, 0x87, 0xe4, 0x10, 0xa8, 0x9d, 0x52, 0x22, 0xa8, 0x82, 0x10, + 0x53, 0x93, 0xd8, 0xc0, 0xee, 0x34, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 0x5c, 0xd5, 0xdd, 0x09, + 0xbb, 0x00, 0x00, 0x00, +} diff --git a/gee-cache/day7-proto-buf/geecache/geecachepb/geecachepb.proto b/gee-cache/day7-proto-buf/geecache/geecachepb/geecachepb.proto new file mode 100644 index 0000000..3f5b313 --- /dev/null +++ b/gee-cache/day7-proto-buf/geecache/geecachepb/geecachepb.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package geecachepb; + +message Request { + string group = 1; + string key = 2; +} + +message Response { + bytes value = 1; +} + +service GroupCache { + rpc Get(Request) returns (Response); +} diff --git a/gee-cache/day7-proto-buf/geecache/go.mod b/gee-cache/day7-proto-buf/geecache/go.mod new file mode 100644 index 0000000..2ad7119 --- /dev/null +++ b/gee-cache/day7-proto-buf/geecache/go.mod @@ -0,0 +1,5 @@ +module geecache + +go 1.13 + +require github.com/golang/protobuf v1.3.3 diff --git a/gee-cache/day7-proto-buf/geecache/go.sum b/gee-cache/day7-proto-buf/geecache/go.sum new file mode 100644 index 0000000..b1efb8b --- /dev/null +++ b/gee-cache/day7-proto-buf/geecache/go.sum @@ -0,0 +1,2 @@ +github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= diff --git a/gee-cache/day7-proto-buf/geecache/http.go b/gee-cache/day7-proto-buf/geecache/http.go new file mode 100644 index 0000000..a4d171c --- /dev/null +++ b/gee-cache/day7-proto-buf/geecache/http.go @@ -0,0 +1,135 @@ +package geecache + +import ( + "fmt" + "geecache/consistenthash" + pb "geecache/geecachepb" + "io/ioutil" + "log" + "net/http" + "net/url" + "strings" + "sync" + + "github.com/golang/protobuf/proto" +) + +const ( + defaultBasePath = "/_geecache/" + defaultReplicas = 50 +) + +// HTTPPool implements PeerPicker for a pool of HTTP peers. +type HTTPPool struct { + // this peer's base URL, e.g. "https://example.net:8000" + self string + basePath string + mu sync.Mutex // guards peers and httpGetters + peers *consistenthash.Map + httpGetters map[string]*httpGetter // keyed by e.g. "http://10.0.0.2:8008" +} + +// NewHTTPPool initializes an HTTP pool of peers, and registers itself as a PeerPicker. +func NewHTTPPool(self string) *HTTPPool { + return &HTTPPool{ + self: self, + basePath: defaultBasePath, + } +} + +// Log info with server name +func (p *HTTPPool) Log(format string, v ...interface{}) { + log.Printf("[Server %s] %s", p.self, fmt.Sprintf(format, v...)) +} + +// ServeHTTP handle all http requests +func (p *HTTPPool) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if !strings.HasPrefix(r.URL.Path, p.basePath) { + panic("HTTPPool serving unexpected path: " + r.URL.Path) + } + p.Log("%s %s", r.Method, r.URL.Path) + // /// required + parts := strings.SplitN(r.URL.Path[len(p.basePath):], "/", 2) + if len(parts) != 2 { + http.Error(w, "bad request", http.StatusBadRequest) + return + } + + groupName := parts[0] + key := parts[1] + + group := GetGroup(groupName) + if group == nil { + http.Error(w, "no such group: "+groupName, http.StatusNotFound) + return + } + + view, err := group.Get(key) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/octet-stream") + w.Write(view.ByteSlice()) +} + +// Set updates the pool's list of peers. +func (p *HTTPPool) Set(peers ...string) { + p.mu.Lock() + defer p.mu.Unlock() + p.peers = consistenthash.New(defaultReplicas, nil) + p.peers.Add(peers...) + p.httpGetters = make(map[string]*httpGetter, len(peers)) + for _, peer := range peers { + p.httpGetters[peer] = &httpGetter{baseURL: peer + p.basePath} + } +} + +// PickPeer picks a peer according to key +func (p *HTTPPool) PickPeer(key string) (PeerGetter, bool) { + p.mu.Lock() + defer p.mu.Unlock() + if peer := p.peers.Get(key); peer != "" && peer != p.self { + p.Log("Pick peer %s", peer) + return p.httpGetters[peer], true + } + return nil, false +} + +var _ PeerPicker = (*HTTPPool)(nil) + +type httpGetter struct { + baseURL string +} + +func (h *httpGetter) Get(in *pb.Request, out *pb.Response) error { + u := fmt.Sprintf( + "%v%v/%v", + h.baseURL, + url.QueryEscape(in.GetGroup()), + url.QueryEscape(in.GetKey()), + ) + res, err := http.Get(u) + if err != nil { + return err + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return fmt.Errorf("server returned: %v", res.Status) + } + + bytes, err := ioutil.ReadAll(res.Body) + if err != nil { + return fmt.Errorf("reading response body: %v", err) + } + + if err = proto.Unmarshal(bytes, out); err != nil { + return fmt.Errorf("decoding response body: %v", err) + } + + return nil +} + +var _ PeerGetter = (*httpGetter)(nil) diff --git a/gee-cache/day7-proto-buf/geecache/lru/lru.go b/gee-cache/day7-proto-buf/geecache/lru/lru.go new file mode 100644 index 0000000..81eee43 --- /dev/null +++ b/gee-cache/day7-proto-buf/geecache/lru/lru.go @@ -0,0 +1,79 @@ +package lru + +import "container/list" + +// Cache is a LRU cache. It is not safe for concurrent access. +type Cache struct { + maxBytes int64 + nbytes int64 + ll *list.List + cache map[string]*list.Element + // optional and executed when an entry is purged. + OnEvicted func(key string, value Value) +} + +type entry struct { + key string + value Value +} + +// Value use Len to count how many bytes it takes +type Value interface { + Len() int +} + +// New is the Constructor of Cache +func New(maxBytes int64, onEvicted func(string, Value)) *Cache { + return &Cache{ + maxBytes: maxBytes, + ll: list.New(), + cache: make(map[string]*list.Element), + OnEvicted: onEvicted, + } +} + +// Add adds a value to the cache. +func (c *Cache) Add(key string, value Value) { + if ele, ok := c.cache[key]; ok { + c.ll.MoveToFront(ele) + kv := ele.Value.(*entry) + kv.value = value + return + } + ele := c.ll.PushFront(&entry{key, value}) + c.cache[key] = ele + c.nbytes += int64(len(key)) + int64(value.Len()) + + for c.maxBytes != 0 && c.maxBytes < c.nbytes { + c.RemoveOldest() + } +} + +// Get look ups a key's value +func (c *Cache) Get(key string) (value Value, ok bool) { + if ele, ok := c.cache[key]; ok { + c.ll.MoveToFront(ele) + kv := ele.Value.(*entry) + return kv.value, true + } + return +} + +// RemoveOldest removes the oldest item +func (c *Cache) RemoveOldest() { + ele := c.ll.Back() + if ele != nil { + c.ll.Remove(ele) + kv := ele.Value.(*entry) + delete(c.cache, kv.key) + c.nbytes -= int64(len(kv.key)) + int64(kv.value.Len()) + if c.OnEvicted != nil { + c.OnEvicted(kv.key, kv.value) + } + } +} + +// Len the number of cache entries +func (c *Cache) Len() int { + return c.ll.Len() +} diff --git a/gee-cache/day7-proto-buf/geecache/lru/lru_test.go b/gee-cache/day7-proto-buf/geecache/lru/lru_test.go new file mode 100644 index 0000000..7308322 --- /dev/null +++ b/gee-cache/day7-proto-buf/geecache/lru/lru_test.go @@ -0,0 +1,55 @@ +package lru + +import ( + "reflect" + "testing" +) + +type String string + +func (d String) Len() int { + return len(d) +} + +func TestGet(t *testing.T) { + lru := New(int64(0), nil) + lru.Add("key1", String("1234")) + if v, ok := lru.Get("key1"); !ok || string(v.(String)) != "1234" { + t.Fatalf("cache hit key1=1234 failed") + } + if _, ok := lru.Get("key2"); ok { + t.Fatalf("cache miss key2 failed") + } +} + +func TestRemoveoldest(t *testing.T) { + k1, k2, k3 := "key1", "key2", "k3" + v1, v2, v3 := "value1", "value2", "v3" + cap := len(k1 + k2 + v1 + v2) + lru := New(int64(cap), nil) + lru.Add(k1, String(v1)) + lru.Add(k2, String(v2)) + lru.Add(k3, String(v3)) + + if _, ok := lru.Get("key1"); ok || lru.Len() != 2 { + t.Fatalf("Removeoldest key1 failed") + } +} + +func TestOnEvicted(t *testing.T) { + keys := make([]string, 0) + callback := func(key string, value Value) { + keys = append(keys, key) + } + lru := New(int64(10), callback) + lru.Add("key1", String("123456")) + lru.Add("k2", String("k2")) + lru.Add("k3", String("k3")) + lru.Add("k4", String("k4")) + + expect := []string{"key1", "k2"} + + if !reflect.DeepEqual(expect, keys) { + t.Fatalf("Call OnEvicted failed, expect keys equals to %s", expect) + } +} diff --git a/gee-cache/day7-proto-buf/geecache/peers.go b/gee-cache/day7-proto-buf/geecache/peers.go new file mode 100644 index 0000000..9324577 --- /dev/null +++ b/gee-cache/day7-proto-buf/geecache/peers.go @@ -0,0 +1,14 @@ +package geecache + +import pb "geecache/geecachepb" + +// PeerPicker is the interface that must be implemented to locate +// the peer that owns a specific key. +type PeerPicker interface { + PickPeer(key string) (peer PeerGetter, ok bool) +} + +// PeerGetter is the interface that must be implemented by a peer. +type PeerGetter interface { + Get(in *pb.Request, out *pb.Response) error +} diff --git a/gee-cache/day7-proto-buf/geecache/singleflight/singleflight.go b/gee-cache/day7-proto-buf/geecache/singleflight/singleflight.go new file mode 100644 index 0000000..85bd0dd --- /dev/null +++ b/gee-cache/day7-proto-buf/geecache/singleflight/singleflight.go @@ -0,0 +1,46 @@ +package singleflight + +import "sync" + +// call is an in-flight or completed Do call +type call struct { + wg sync.WaitGroup + val interface{} + err error +} + +// Group represents a class of work and forms a namespace in which +// units of work can be executed with duplicate suppression. +type Group struct { + mu sync.Mutex // protects m + m map[string]*call // lazily initialized +} + +// Do executes and returns the results of the given function, making +// sure that only one execution is in-flight for a given key at a +// time. If a duplicate comes in, the duplicate caller waits for the +// original to complete and receives the same results. +func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) { + g.mu.Lock() + if g.m == nil { + g.m = make(map[string]*call) + } + if c, ok := g.m[key]; ok { + g.mu.Unlock() + c.wg.Wait() + return c.val, c.err + } + c := new(call) + c.wg.Add(1) + g.m[key] = c + g.mu.Unlock() + + c.val, c.err = fn() + c.wg.Done() + + g.mu.Lock() + delete(g.m, key) + g.mu.Unlock() + + return c.val, c.err +} diff --git a/gee-cache/day7-proto-buf/geecache/singleflight/singleflight_test.go b/gee-cache/day7-proto-buf/geecache/singleflight/singleflight_test.go new file mode 100644 index 0000000..450951a --- /dev/null +++ b/gee-cache/day7-proto-buf/geecache/singleflight/singleflight_test.go @@ -0,0 +1,16 @@ +package singleflight + +import ( + "testing" +) + +func TestDo(t *testing.T) { + var g Group + v, err := g.Do("key", func() (interface{}, error) { + return "bar", nil + }) + + if v != "bar" || err != nil { + t.Errorf("Do v = %v, error = %v", v, err) + } +} diff --git a/gee-cache/day7-proto-buf/go.mod b/gee-cache/day7-proto-buf/go.mod new file mode 100644 index 0000000..d0fd3ba --- /dev/null +++ b/gee-cache/day7-proto-buf/go.mod @@ -0,0 +1,7 @@ +module example + +go 1.13 + +require geecache v0.0.0 + +replace geecache => ./geecache diff --git a/gee-cache/day7-proto-buf/go.sum b/gee-cache/day7-proto-buf/go.sum new file mode 100644 index 0000000..b1efb8b --- /dev/null +++ b/gee-cache/day7-proto-buf/go.sum @@ -0,0 +1,2 @@ +github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= diff --git a/gee-cache/day7-proto-buf/main.go b/gee-cache/day7-proto-buf/main.go new file mode 100644 index 0000000..a99940c --- /dev/null +++ b/gee-cache/day7-proto-buf/main.go @@ -0,0 +1,87 @@ +package main + +/* +$ curl "http://localhost:9999/api?key=Tom" +630 + +$ curl "http://localhost:9999/api?key=kkk" +kkk not exist +*/ + +import ( + "flag" + "fmt" + "geecache" + "log" + "net/http" +) + +var db = map[string]string{ + "Tom": "630", + "Jack": "589", + "Sam": "567", +} + +func createGroup() *geecache.Group { + return geecache.NewGroup("scores", 2<<10, geecache.GetterFunc( + func(key string) ([]byte, error) { + log.Println("[SlowDB] search key", key) + if v, ok := db[key]; ok { + return []byte(v), nil + } + return nil, fmt.Errorf("%s not exist", key) + })) +} + +func startCacheServer(addr string, addrs []string, gee *geecache.Group) { + peers := geecache.NewHTTPPool(addr) + peers.Set(addrs...) + gee.RegisterPeers(peers) + log.Println("geecache is running at", addr) + log.Fatal(http.ListenAndServe(addr[7:], peers)) +} + +func startAPIServer(apiAddr string, gee *geecache.Group) { + http.Handle("/api", http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + key := r.URL.Query().Get("key") + view, err := gee.Get(key) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/octet-stream") + w.Write(view.ByteSlice()) + + })) + log.Println("fontend server is running at", apiAddr) + log.Fatal(http.ListenAndServe(apiAddr[7:], nil)) + +} + +func main() { + var port int + var api bool + flag.IntVar(&port, "port", 8001, "Geecache server port") + flag.BoolVar(&api, "api", false, "Start a api server?") + flag.Parse() + + apiAddr := "http://localhost:9999" + addrMap := map[int]string{ + 8001: "http://localhost:8001", + 8002: "http://localhost:8002", + 8003: "http://localhost:8003", + } + + addrs := make([]string, 3) + + for _, v := range addrMap { + addrs = append(addrs, v) + } + + gee := createGroup() + if api { + go startAPIServer(apiAddr, gee) + } + startCacheServer(addrMap[port], []string(addrs), gee) +} diff --git a/gee-cache/day7-proto-buf/run.sh b/gee-cache/day7-proto-buf/run.sh new file mode 100755 index 0000000..066979d --- /dev/null +++ b/gee-cache/day7-proto-buf/run.sh @@ -0,0 +1,15 @@ +#!/bin/bash +trap "rm server;kill 0" EXIT + +go build -o server +./server -port=8001 & +./server -port=8002 & +./server -port=8003 -api=1 & + +sleep 2 +echo ">>> start test" +curl "http://localhost:9999/api?key=Tom" & +curl "http://localhost:9999/api?key=Tom" & +curl "http://localhost:9999/api?key=Tom" & + +wait \ No newline at end of file From e0f405bb08b7d341caf10e5d1ee0879ab7bf202f Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Thu, 6 Feb 2020 23:50:37 +0800 Subject: [PATCH 025/122] update README.md, add day6 & day7 code link --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 9d95aff..2ce094e 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,8 @@ GeeCache 是一个模仿 [groupcache](https://github.com/golang/groupcache) 实 - 第三天:HTTP 服务端 | [Code](gee-cache/day3-http-server) - 第四天:一致性哈希(Hash) | [Code](gee-cache/day4-consistent-hash) - 第五天:分布式节点 | [Code](gee-cache/day5-multi-nodes) +- 第六天:防止缓存击穿 | [Code](gee-cache/day6-single-flight) +- 第七天:使用 Protobuf 通信 | [Code](gee-cache/day7-proto-buf) ### WebAssembly 使用示例 @@ -67,6 +69,8 @@ Geecache is a [groupcache](https://github.com/golang/groupcache)-like distribute - Day 3 - Launch a HTTP Server [Code](gee-cache/day3-http-server) - Day 4 - Consistent Hash Algorithm [Code](gee-cache/day4-consistent-hash) - Day 5 - Communication between Distributed Nodes [Code](gee-cache/day5-multi-nodes) +- Day 6 - Cache Breakdown & Single Flight | [Code](gee-cache/day6-single-flight) +- Day 7 - Use Protobuf as RPC Data Exchange Type | [Code](gee-cache/day7-proto-buf) ## Golang WebAssembly Demo From 15f1e5c07c4d3247b29432f6d51027fe9f11655d Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Sat, 8 Feb 2020 02:13:55 +0800 Subject: [PATCH 026/122] add gee-cache doc --- README.md | 4 +- gee-cache/doc/geecache.md | 72 +++++++++++++++++++++++++ gee-cache/doc/geecache/geecache.jpg | Bin 0 -> 29407 bytes gee-cache/doc/geecache/geecache_sm.jpg | Bin 0 -> 6713 bytes gee-web/doc/gee-day1.md | 3 +- gee-web/doc/gee-day2.md | 3 +- gee-web/doc/gee-day3.md | 3 +- gee-web/doc/gee-day4.md | 3 +- gee-web/doc/gee-day5.md | 3 +- gee-web/doc/gee-day6.md | 3 +- gee-web/doc/gee-day7.md | 3 +- gee-web/doc/gee.md | 3 +- 12 files changed, 90 insertions(+), 10 deletions(-) create mode 100644 gee-cache/doc/geecache.md create mode 100644 gee-cache/doc/geecache/geecache.jpg create mode 100644 gee-cache/doc/geecache/geecache_sm.jpg diff --git a/README.md b/README.md index 2ce094e..cdc75ff 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ ### 7天用Go从零实现分布式缓存 GeeCache -GeeCache 是一个模仿 [groupcache](https://github.com/golang/groupcache) 实现的分布式缓存系统 +[GeeCache](https://geektutu.com/post/geecache.html) 是一个模仿 [groupcache](https://github.com/golang/groupcache) 实现的分布式缓存系统 - 第一天:LRU 缓存策略 | [Code](gee-cache/day1-lru) - 第二天:单机并发缓存 | [Code](gee-cache/day2-single-node) @@ -62,7 +62,7 @@ What can I write in 7 days? A gin-like web framework? A distributed cache like g ## Distributed Cache - Geecache -Geecache is a [groupcache](https://github.com/golang/groupcache)-like distributed cache +[GeeCache](https://geektutu.com/post/geecache.html) is a [groupcache](https://github.com/golang/groupcache)-like distributed cache - Day 1 - LRU (Least Recently Used) Caching Strategy [Code](gee-cache/day1-lru) - Day 2 - Single Machine Concurrent Cache [Code](gee-cache/day2-single-node) diff --git a/gee-cache/doc/geecache.md b/gee-cache/doc/geecache.md new file mode 100644 index 0000000..21b2f0d --- /dev/null +++ b/gee-cache/doc/geecache.md @@ -0,0 +1,72 @@ +--- +title: 7天用Go从零实现分布式缓存GeeCache +date: 2020-02-08 01:00:00 +description: 7天用 用 Go语言/golang 从零实现分布式缓存 GeeCache 教程(7 days implement golang distributed cache from scratch tutorial),动手写分布式缓存,参照 groupcache 的实现。功能包括单机/分布式缓存,LRU (Least Recently Used) 缓存策略,防止缓存击穿、一致性哈希(Consistent Hash),protobuf 通信等。 +tags: +- Go +nav: 从零实现 +categories: +- 分布式缓存 - GeeCache +keywords: +- Go语言 +- 从零实现分布式缓存 +- 动手写分布式缓存 +image: post/geecache/geecache_sm.jpg +github: https://github.com/geektutu/7days-golang +--- + +![分布式缓存geecache](geecache/geecache.jpg) + +## 1 谈谈分布式缓存 + +第一次请求时将一些耗时操作的结果暂存,以后遇到相同的请求,直接返回暂存的数据。我想这是大部分童鞋对于缓存的理解。在计算机系统中,缓存无处不在,比如我们访问一个网页,网页和引用的 JS/CSS 等静态文件,根据不同的策略,会缓存在浏览器本地或是 CDN 服务器,那在第二次访问的时候,就会觉得网页加载的速度快了不少;比如微博的点赞的数量,不可能每个人每次访问,都从数据库中查找所有点赞的记录再统计,数据库的操作是很耗时的,很难支持那么大的流量,所以一般点赞这类数据是缓存在 Redis 服务集群中的。 + +> 商业世界里,现金为王;架构世界里,缓存为王。 + +缓存中最简单的莫过于存储在内存中的键值对缓存了。说到键值对,很容易想到的是字典(dict)类型,Go 语言中称之为 map。那直接创建一个 map,每次有新数据就往 map 中插入不就好了,这不就是键值对缓存么?这样做有什么问题呢? + +1)内存不够了怎么办? + +那就随机删掉几条数据好了。随机删掉好呢?还是按照时间顺序好呢?或者是有没有其他更好的淘汰策略呢?不同数据的访问频率是不一样的,优先删除访问频率低的数据是不是更好呢?数据的访问频率可能随着时间变化,那优先删除最近最少访问的数据可能是一个更好的选择。我们需要实现一个合理的淘汰策略。 + +2)并发写入冲突了怎么办? + +对缓存的访问,一般不可能是串行的。map 是没有并发保护的,应对并发的场景,修改操作(包括新增,更新和删除)需要加锁。 + +3)单机性能不够怎么办? + +单台计算机的资源是有限的,计算、存储等都是有限的。随着业务量和访问量的增加,单台机器很容易遇到瓶颈。如果利用多台计算机的资源,并行处理提高性能就要缓存应用能够支持分布式,这称为水平扩展(scale horizontally)。与水平扩展相对应的是垂直扩展(scale vertically),即通过增加单个节点的计算、存储、带宽等,来提高系统的性能,硬件的成本和性能并非呈线性关系,大部分情况下,分布式系统是一个更优的选择。 + +4)... + +## 2 关于 GeeCache + +设计一个分布式缓存系统,需要考虑资源控制、淘汰策略、并发、分布式节点通信等各个方面的问题。而且,针对不同的应用场景,还需要在不同的特性之间权衡,例如,是否需要支持缓存更新?还是假定缓存在淘汰之前是不允许改变的。不同的权衡对应着不同的实现。 + +[groupcache](https://github.com/golang/groupcache) 是 Go 语言版的 memcached,目的是在某些特定场合替代 memcached。groupcache 的作者也是 memcached 的作者。无论是了解单机缓存还是分布式缓存,深入学习这个库的实现都是非常有意义的。 + +`GeeCache` 基本上模仿了 [groupcache](https://github.com/golang/groupcache) 的实现,为了将代码量限制在 500 行左右(groupcache 约 3000 行),裁剪了部分功能。但总体实现上,还是与 groupcache 非常接近的。支持特性有: + +- 单机缓存和基于 HTTP 的分布式缓存 +- 最近最少访问(Least Recently Used, LRU) 缓存策略 +- 使用 Go 锁机制防止缓存击穿 +- 使用一致性哈希选择节点,实现负载均衡 +- 使用 protobuf 优化节点间二进制通信 +- ... + +`GeeCache` 分7天实现,每天完成的部分都是可以独立运行和测试的,就像搭积木一样,每天实现的特性组合在一起就是最终的分布式缓存系统。每天的代码在 100 行左右。 + +## 3 目录 + +- 第一天:LRU 缓存策略 | [Code - Github](https://github.com/geektutu/7days-golang/blob/master/gee-cache/day1-lru) +- 第二天:单机并发缓存 | [Code - Github](https://github.com/geektutu/7days-golang/blob/master/gee-cache/day2-single-node) +- 第三天:HTTP 服务端 | [Code - Github](https://github.com/geektutu/7days-golang/blob/master/gee-cache/day3-http-server) +- 第四天:一致性哈希(Hash) | [Code - Github](https://github.com/geektutu/7days-golang/blob/master/gee-cache/day4-consistent-hash) +- 第五天:分布式节点 | [Code - Github](https://github.com/geektutu/7days-golang/blob/master/gee-cache/day5-multi-nodes) +- 第六天:防止缓存击穿 | [Code - Github](https://github.com/geektutu/7days-golang/blob/master/gee-cache/day6-single-flight) +- 第七天:使用 Protobuf 通信 | [Code - Github](https://github.com/geektutu/7days-golang/blob/master/gee-cache/day7-proto-buf) + +## 附 推荐阅读 + +- [Go 语言简明教程](https://geektutu.com/post/quick-golang.html) +- [Go Protobuf 简明教程](https://geektutu.com/post/quick-go-protobuf.html) \ No newline at end of file diff --git a/gee-cache/doc/geecache/geecache.jpg b/gee-cache/doc/geecache/geecache.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d19e7fead236e6aee190f73c7ed7e176dfa8d2c4 GIT binary patch literal 29407 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 literal 0 HcmV?d00001 diff --git a/gee-cache/doc/geecache/geecache_sm.jpg b/gee-cache/doc/geecache/geecache_sm.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a3832b0be4f66309561f5caabf371d4e332e7937 GIT binary patch literal 6713 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 QoO?gd&N;?!cKq%C0W4|;x&QzG literal 0 HcmV?d00001 diff --git a/gee-web/doc/gee-day1.md b/gee-web/doc/gee-day1.md index fd94496..ca00e8a 100644 --- a/gee-web/doc/gee-day1.md +++ b/gee-web/doc/gee-day1.md @@ -4,8 +4,9 @@ date: 2019-08-12 00:10:10 description: 7天用 Go语言 从零实现Web框架教程(7 days implement golang web framework from scratch tutorial),用 Go语言/golang 动手写Web框架,从零实现一个Web框架,以 Gin 为原型从零设计一个Web框架。本文介绍了Go标准库 net/http 和 http.Handler 接口的使用,拦截所有的 HTTP 请求,交给Gee框架处理。 tags: - Go +nav: 从零实现 categories: -- 从零实现 +- Web框架 - Gee keywords: - Go语言 - 从零实现Web框架 diff --git a/gee-web/doc/gee-day2.md b/gee-web/doc/gee-day2.md index e47467f..281ebcc 100644 --- a/gee-web/doc/gee-day2.md +++ b/gee-web/doc/gee-day2.md @@ -4,8 +4,9 @@ date: 2019-08-19 00:10:10 description: 7天用 Go语言 从零实现Web框架教程(7 days implement golang web framework from scratch tutorial),用 Go语言/golang 动手写Web框架,从零实现一个Web框架,以 Gin 为原型从零设计一个Web框架。本文介绍了请求上下文(Context)的设计理念,封装了返回JSON/String/Data/HTML等类型响应的方法。 tags: - Go +nav: 从零实现 categories: -- 从零实现 +- Web框架 - Gee keywords: - Go语言 - 从零实现Web框架 diff --git a/gee-web/doc/gee-day3.md b/gee-web/doc/gee-day3.md index 23a0e72..4b34f6d 100644 --- a/gee-web/doc/gee-day3.md +++ b/gee-web/doc/gee-day3.md @@ -4,8 +4,9 @@ date: 2019-08-28 00:10:10 description: 7天用 Go语言 从零实现Web框架教程(7 days implement golang web framework from scratch tutorial),用 Go语言/golang 动手写Web框架,从零实现一个Web框架,以 Gin 为原型从零设计一个Web框架。本文介绍了如何用 Trie 前缀树实现路由 Route。支持简单的参数解析和通配符的场景。 tags: - Go +nav: 从零实现 categories: -- 从零实现 +- Web框架 - Gee keywords: - Go语言 - 从零实现Web框架 diff --git a/gee-web/doc/gee-day4.md b/gee-web/doc/gee-day4.md index 7f71370..3a5eb21 100644 --- a/gee-web/doc/gee-day4.md +++ b/gee-web/doc/gee-day4.md @@ -4,8 +4,9 @@ date: 2019-09-01 15:10:10 description: 7天用 Go语言 从零实现Web框架教程(7 days implement golang web framework from scratch tutorial),用 Go语言/golang 动手写Web框架,从零实现一个Web框架,以 Gin 为原型从零设计一个Web框架。本文介绍了分组控制(Group Control)的意义,以及嵌套分组路由的实现。 tags: - Go +nav: 从零实现 categories: -- 从零实现 +- Web框架 - Gee keywords: - Go语言 - 从零实现Web框架 diff --git a/gee-web/doc/gee-day5.md b/gee-web/doc/gee-day5.md index 9f5d117..e4fb039 100644 --- a/gee-web/doc/gee-day5.md +++ b/gee-web/doc/gee-day5.md @@ -4,8 +4,9 @@ date: 2019-09-01 20:10:10 description: 7天用 Go语言 从零实现Web框架教程(7 days implement golang web framework from scratch tutorial),用 Go语言/golang 动手写Web框架,从零实现一个Web框架,以 Gin 为原型从零设计一个Web框架。本文介绍了如何为Web框架添加中间件的功能(middlewares)。 tags: - Go +nav: 从零实现 categories: -- 从零实现 +- Web框架 - Gee keywords: - Go语言 - 从零实现Web框架 diff --git a/gee-web/doc/gee-day6.md b/gee-web/doc/gee-day6.md index e429faf..b77f041 100644 --- a/gee-web/doc/gee-day6.md +++ b/gee-web/doc/gee-day6.md @@ -4,8 +4,9 @@ date: 2019-09-08 20:10:00 description: 7天用 Go语言 从零实现Web框架教程(7 days implement golang web framework from scratch tutorial),用 Go语言/golang 动手写Web框架,从零实现一个Web框架,以 Gin 为原型从零设计一个Web框架。本文介绍了如何为Web框架添加HTML模板(HTML Template)以及静态文件(Serve Static Files)的功能。 tags: - Go +nav: 从零实现 categories: -- 从零实现 +- Web框架 - Gee keywords: - Go语言 - 从零实现Web框架 diff --git a/gee-web/doc/gee-day7.md b/gee-web/doc/gee-day7.md index f72a314..b3f6873 100644 --- a/gee-web/doc/gee-day7.md +++ b/gee-web/doc/gee-day7.md @@ -4,8 +4,9 @@ date: 2020-01-09 01:00:00 description: 7天用 Go语言 从零实现Web框架教程(7 days implement golang web framework from scratch tutorial),用 Go语言/golang 动手写Web框架,从零实现一个Web框架,以 Gin 为原型从零设计一个Web框架。本文介绍了如何为Web框架增加错误处理机制。 tags: - Go +nav: 从零实现 categories: -- 从零实现 +- Web框架 - Gee keywords: - Go语言 - 从零实现Web框架 diff --git a/gee-web/doc/gee.md b/gee-web/doc/gee.md index c215534..c2581cc 100644 --- a/gee-web/doc/gee.md +++ b/gee-web/doc/gee.md @@ -4,8 +4,9 @@ date: 2019-08-11 02:10:10 description: 7天用 Go语言 从零实现Web框架教程(7 days implement golang web framework from scratch tutorial),用 Go语言/golang 动手写Web框架,从零实现一个Web框架,以 Gin 为原型从零设计一个Web框架。 tags: - Go +nav: 从零实现 categories: -- 从零实现 +- Web框架 - Gee keywords: - Gee教程 - 从零实现Web框架 From 1f47dbdd12c9f71ad4d2129e474a7a308d5e099a Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Sun, 9 Feb 2020 17:11:39 +0800 Subject: [PATCH 027/122] fix proto decode error --- gee-cache/day7-proto-buf/geecache/http.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/gee-cache/day7-proto-buf/geecache/http.go b/gee-cache/day7-proto-buf/geecache/http.go index a4d171c..4a1c129 100644 --- a/gee-cache/day7-proto-buf/geecache/http.go +++ b/gee-cache/day7-proto-buf/geecache/http.go @@ -70,8 +70,15 @@ func (p *HTTPPool) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } + // Write the value to the response body as a proto message. + body, err := proto.Marshal(&pb.Response{Value: view.ByteSlice()}) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/octet-stream") - w.Write(view.ByteSlice()) + w.Write(body) } // Set updates the pool's list of peers. From 3b2e7ea903f2f0bf86906543dc7b4aca2c12f03b Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Tue, 11 Feb 2020 22:10:37 +0800 Subject: [PATCH 028/122] add geecache-day1 doc --- README.md | 2 +- gee-cache/doc/geecache-day1.md | 251 +++++++++++++++++++++++ gee-cache/doc/geecache-day1/lru.jpg | Bin 0 -> 12957 bytes gee-cache/doc/geecache-day1/lru_logo.jpg | Bin 0 -> 5272 bytes gee-cache/doc/geecache.md | 5 +- 5 files changed, 255 insertions(+), 3 deletions(-) create mode 100644 gee-cache/doc/geecache-day1.md create mode 100755 gee-cache/doc/geecache-day1/lru.jpg create mode 100755 gee-cache/doc/geecache-day1/lru_logo.jpg diff --git a/README.md b/README.md index cdc75ff..28e085d 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ [GeeCache](https://geektutu.com/post/geecache.html) 是一个模仿 [groupcache](https://github.com/golang/groupcache) 实现的分布式缓存系统 -- 第一天:LRU 缓存策略 | [Code](gee-cache/day1-lru) +- [第一天:LRU 缓存淘汰策略](https://geektutu.com/post/geecache-day1.html) | [Code](gee-cache/day1-lru) - 第二天:单机并发缓存 | [Code](gee-cache/day2-single-node) - 第三天:HTTP 服务端 | [Code](gee-cache/day3-http-server) - 第四天:一致性哈希(Hash) | [Code](gee-cache/day4-consistent-hash) diff --git a/gee-cache/doc/geecache-day1.md b/gee-cache/doc/geecache-day1.md new file mode 100644 index 0000000..f5330d8 --- /dev/null +++ b/gee-cache/doc/geecache-day1.md @@ -0,0 +1,251 @@ +--- +title: 动手写分布式缓存 - GeeCache第一天 LRU 缓存淘汰策略 +date: 2020-02-11 22:00:00 +description: 7天用 Go语言/golang 从零实现分布式缓存 GeeCache 教程(7 days implement golang distributed cache from scratch tutorial),动手写分布式缓存,参照 groupcache 的实现。本文介绍常用的三种缓存淘汰(失效)算法:先进先出(FIFO),最少使用(LFU) 和 最近最少使用(LRU),并实现 LRU 算法和相应的测试代码。 +tags: +- Go +nav: 从零实现 +categories: +- 分布式缓存 - GeeCache +keywords: +- Go语言 +- 从零实现分布式缓存 +- 动手写分布式缓存 +- LRU +- 缓存失效 +image: post/geecache-day1/lru_logo.jpg +github: https://github.com/geektutu/7days-golang +--- + + +本文是[7天用Go从零实现分布式缓存GeeCache教程系列](https://geektutu.com/post/geecache.html)的第一篇。 + +- 介绍常用的三种缓存淘汰(失效)算法:FIFO,LFU 和 LRU +- 实现 LRU 缓存淘汰算法,**代码约80行** + +## 1 FIFO/LFU/LRU 算法简介 + +GeeCache 的缓存全部存储在内存中,内存是有限的,因此不可能无限制地添加数据。假定我们设置缓存能够使用的内存大小为 N,那么在某一个时间点,添加了某一条缓存记录之后,占用内存超过了 N,这个时候就需要从缓存中移除一条或多条数据了。那移除谁呢?我们肯定希望尽可能移除“没用”的数据,那如何判定数据“有用”还是“没用”呢? + +### 1.1 FIFO(First In First Out) + +先进先出,也就是淘汰缓存中最老(最早添加)的记录。FIFO 认为,最早添加的记录,其不再被使用的可能性比刚添加的可能性大。这种算法的实现也非常简单,创建一个队列,新增记录添加到队尾,每次内存不够时,淘汰队首。但是很多场景下,部分记录虽然是最早添加但也最常被访问,而不得不因为呆的时间太长而被淘汰。这类数据会被频繁地添加进缓存,又被淘汰出去,导致缓存命中率降低。 + +### 1.2 LFU(Least Frequently Used) + +最少使用,也就是淘汰缓存中访问频率最低的记录。LFU 认为,如果数据过去被访问多次,那么将来被访问的频率也更高。LFU 的实现需要维护一个按照访问次数排序的队列,每次访问,访问次数加1,队列重新排序,淘汰时选择访问次数最少的即可。LFU 算法的命中率是比较高的,但缺点也非常明显,维护每个记录的访问次数,对内存的消耗是很高的;另外,如果数据的访问模式发生变化,LFU 需要较长的时间去适应,也就是说 LFU 算法受历史数据的影响比较大。例如某个数据历史上访问次数奇高,但在某个时间点之后几乎不再被访问,但因为历史访问次数过高,而迟迟不能被淘汰。 + +### 1.3 LRU(Least Recently Used) + +最近最少使用,相对于仅考虑时间因素的 FIFO 和仅考虑访问频率的 LFU,LRU 算法可以认为是相对平衡的一种淘汰算法。LRU 认为,如果数据最近被访问过,那么将来被访问的概率也会更高。LRU 算法的实现非常简单,维护一个队列,如果某条记录被访问了,则移动到队尾,那么队首则是最近最少访问的数据,淘汰该条记录即可。 + +## 2 LRU 算法实现 + +### 2.1 核心数据结构 + +![implement lru algorithm with golang](geecache-day1/lru.jpg) + +这张图很好地表示了 LRU 算法最核心的 2 个数据结构 + +- 绿色的是字典(map),存储键和值的映射关系。这样根据某个键(key)查找对应的值(value)的复杂是`O(1)`,在字典中插入一条记录的复杂度也是`O(1)`。 +- 红色的是双向链表(double linked list)实现的队列。将所有的值放到双向链表中,这样,当访问到某个值时,将其移动到队尾的复杂度是`O(1)`,在队尾新增一条记录以及删除一条记录的复杂度均为`O(1)`。 + +接下来我们创建一个包含字典和双向链表的结构体类型 Cache,方便实现后续的增删查改操作。 + +[day1-lru/geecache/lru/lru.go - github](https://github.com/geektutu/7days-golang/tree/master/gee-cache/day1-lru/geecache/lru) + +```go +package lru + +import "container/list" + +// Cache is a LRU cache. It is not safe for concurrent access. +type Cache struct { + maxBytes int64 + nbytes int64 + ll *list.List + cache map[string]*list.Element + // optional and executed when an entry is purged. + OnEvicted func(key string, value Value) +} + +type entry struct { + key string + value Value +} + +// Value use Len to count how many bytes it takes +type Value interface { + Len() int +} +``` + +- 在这里我们直接使用 Go 语言标准库实现的双向链表`list.List`。 +- 字典的定义是 `map[string]*list.Element`,键是字符串,值是双向链表中对应节点的指针。 +- `maxBytes` 是允许使用的最大内存,`nbytes` 是当前已使用的内存,`OnEvicted` 是某条记录被移除时的回调函数,可以为 nil。 +- 键值对 `entry` 是双向链表节点的数据类型,在链表中仍保存每个值对应的 key 的好处在于,淘汰队首节点时,需要用 key 从字典中删除对应的映射。 +- 为了通用性,我们允许值是实现了 `Value` 接口的任意类型,该接口只包含了一个方法 `Len() int`,用于返回值所占用的内存大小。 + + +方便实例化 `Cache`,实现 `New()` 函数: + +```go +// New is the Constructor of Cache +func New(maxBytes int64, onEvicted func(string, Value)) *Cache { + return &Cache{ + maxBytes: maxBytes, + ll: list.New(), + cache: make(map[string]*list.Element), + OnEvicted: onEvicted, + } +} +``` + +### 2.2 查找功能 + +查找主要有 2 个步骤,第一步是从字典中找到对应的双向链表的节点,第二步,将该节点移动到队尾。 + +```go +// Get look ups a key's value +func (c *Cache) Get(key string) (value Value, ok bool) { + if ele, ok := c.cache[key]; ok { + c.ll.MoveToFront(ele) + kv := ele.Value.(*entry) + return kv.value, true + } + return +} +``` + +- 如果键对应的链表节点存在,则将对应节点移动到队尾,并返回查找到的值。 +- `c.ll.MoveToFront(ele)`,即将链表中的节点 `ele` 移动到队尾(双向链表作为队列,队首队尾是相对的,在这里约定 front 为队尾) + +### 2.3 删除 + +这里的删除,实际上是缓存淘汰。即移除最近最少访问的节点(队首) + +```go +// RemoveOldest removes the oldest item +func (c *Cache) RemoveOldest() { + ele := c.ll.Back() + if ele != nil { + c.ll.Remove(ele) + kv := ele.Value.(*entry) + delete(c.cache, kv.key) + c.nbytes -= int64(len(kv.key)) + int64(kv.value.Len()) + if c.OnEvicted != nil { + c.OnEvicted(kv.key, kv.value) + } + } +} +``` + +- `c.ll.Back()` 取到队首节点,从链表中删除。 +- `delete(c.cache, kv.key)`,从字典中`c.cache`删除该节点的映射关系。 +- 更新当前所用的内存 `c.nbytes`。 +- 如果回调函数 `OnEvicted` 不为 nil,则调用回调函数。 + +### 2.4 新增/修改 + +```go +// Add adds a value to the cache. +func (c *Cache) Add(key string, value Value) { + if ele, ok := c.cache[key]; ok { + c.ll.MoveToFront(ele) + kv := ele.Value.(*entry) + kv.value = value + return + } + ele := c.ll.PushFront(&entry{key, value}) + c.cache[key] = ele + c.nbytes += int64(len(key)) + int64(value.Len()) + + for c.maxBytes != 0 && c.maxBytes < c.nbytes { + c.RemoveOldest() + } +} +``` + +- 如果键存在,则更新对应节点的值,并将该节点移到队尾。 +- 不存在则是新增场景,首先队尾添加新节点 `&entry{key, value}`, 并字典中添加 `key` 和节点的映射关系。 +- 更新 `c.nbytes`,如果超过了设定的最大值 `c.maxBytes`,则移除最少访问的节点。 + +最后,为了方便测试,我们实现 `Len()` 用来获取添加了多少条数据。 + +```go +// Len the number of cache entries +func (c *Cache) Len() int { + return c.ll.Len() +} +``` + +## 3 测试 + +例如,我们可以尝试添加几条数据,测试 `Get` 方法 + +[day1-lru/geecache/lru/lru_test.go - github](https://github.com/geektutu/7days-golang/tree/master/gee-cache/day1-lru/geecache/lru) + +```go +type String string + +func (d String) Len() int { + return len(d) +} + +func TestGet(t *testing.T) { + lru := New(int64(0), nil) + lru.Add("key1", String("1234")) + if v, ok := lru.Get("key1"); !ok || string(v.(String)) != "1234" { + t.Fatalf("cache hit key1=1234 failed") + } + if _, ok := lru.Get("key2"); ok { + t.Fatalf("cache miss key2 failed") + } +} +``` + +测试,当使用内存超过了设定值时,是否会触发“无用”节点的移除: + +```go +func TestRemoveoldest(t *testing.T) { + k1, k2, k3 := "key1", "key2", "k3" + v1, v2, v3 := "value1", "value2", "v3" + cap := len(k1 + k2 + v1 + v2) + lru := New(int64(cap), nil) + lru.Add(k1, String(v1)) + lru.Add(k2, String(v2)) + lru.Add(k3, String(v3)) + + if _, ok := lru.Get("key1"); ok || lru.Len() != 2 { + t.Fatalf("Removeoldest key1 failed") + } +} +``` + +测试回调函数能否被调用: + +```go +func TestOnEvicted(t *testing.T) { + keys := make([]string, 0) + callback := func(key string, value Value) { + keys = append(keys, key) + } + lru := New(int64(10), callback) + lru.Add("key1", String("123456")) + lru.Add("k2", String("k2")) + lru.Add("k3", String("k3")) + lru.Add("k4", String("k4")) + + expect := []string{"key1", "k2"} + + if !reflect.DeepEqual(expect, keys) { + t.Fatalf("Call OnEvicted failed, expect keys equals to %s", expect) + } +} +``` + +## 附 推荐阅读 + +- [Go 语言简明教程](https://geektutu.com/post/quick-golang.html) +- [Go Test 单元测试简明教程](https://geektutu.com/post/quick-go-test.html) +- [list 官方文档 - golang.org](https://golang.org/pkg/container/list/) \ No newline at end of file diff --git a/gee-cache/doc/geecache-day1/lru.jpg b/gee-cache/doc/geecache-day1/lru.jpg new file mode 100755 index 0000000000000000000000000000000000000000..db90cd8386de9e36b59fa89b6328146242d08ff3 GIT binary patch literal 12957 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 literal 0 HcmV?d00001 diff --git a/gee-cache/doc/geecache.md b/gee-cache/doc/geecache.md index 21b2f0d..fc79090 100644 --- a/gee-cache/doc/geecache.md +++ b/gee-cache/doc/geecache.md @@ -1,7 +1,7 @@ --- title: 7天用Go从零实现分布式缓存GeeCache date: 2020-02-08 01:00:00 -description: 7天用 用 Go语言/golang 从零实现分布式缓存 GeeCache 教程(7 days implement golang distributed cache from scratch tutorial),动手写分布式缓存,参照 groupcache 的实现。功能包括单机/分布式缓存,LRU (Least Recently Used) 缓存策略,防止缓存击穿、一致性哈希(Consistent Hash),protobuf 通信等。 +description: 7天用 Go语言/golang 从零实现分布式缓存 GeeCache 教程(7 days implement golang distributed cache from scratch tutorial),动手写分布式缓存,参照 groupcache 的实现。功能包括单机/分布式缓存,LRU (Least Recently Used) 缓存策略,防止缓存击穿、一致性哈希(Consistent Hash),protobuf 通信等。 tags: - Go nav: 从零实现 @@ -58,7 +58,7 @@ github: https://github.com/geektutu/7days-golang ## 3 目录 -- 第一天:LRU 缓存策略 | [Code - Github](https://github.com/geektutu/7days-golang/blob/master/gee-cache/day1-lru) +- [第一天:LRU 缓存淘汰策略](https://geektutu.com/post/geecache-day1.html) | [Code - Github](https://github.com/geektutu/7days-golang/blob/master/gee-cache/day1-lru) - 第二天:单机并发缓存 | [Code - Github](https://github.com/geektutu/7days-golang/blob/master/gee-cache/day2-single-node) - 第三天:HTTP 服务端 | [Code - Github](https://github.com/geektutu/7days-golang/blob/master/gee-cache/day3-http-server) - 第四天:一致性哈希(Hash) | [Code - Github](https://github.com/geektutu/7days-golang/blob/master/gee-cache/day4-consistent-hash) @@ -69,4 +69,5 @@ github: https://github.com/geektutu/7days-golang ## 附 推荐阅读 - [Go 语言简明教程](https://geektutu.com/post/quick-golang.html) +- [Go Test 单元测试简明教程](https://geektutu.com/post/quick-go-test.html) - [Go Protobuf 简明教程](https://geektutu.com/post/quick-go-protobuf.html) \ No newline at end of file From 75a54051122fcbef6c5cbbcb2fb5d2ac4f454b65 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Wed, 12 Feb 2020 01:57:07 +0800 Subject: [PATCH 029/122] fix cache.go sync.Mutex, use Lock instead of RLock --- gee-cache/day2-single-node/geecache/cache.go | 10 +++++----- gee-cache/day3-http-server/geecache/cache.go | 10 +++++----- gee-cache/day4-consistent-hash/geecache/cache.go | 10 +++++----- gee-cache/day5-multi-nodes/geecache/cache.go | 10 +++++----- gee-cache/day6-single-flight/geecache/cache.go | 10 +++++----- gee-cache/day7-proto-buf/geecache/cache.go | 10 +++++----- 6 files changed, 30 insertions(+), 30 deletions(-) diff --git a/gee-cache/day2-single-node/geecache/cache.go b/gee-cache/day2-single-node/geecache/cache.go index 703d033..665c3f3 100644 --- a/gee-cache/day2-single-node/geecache/cache.go +++ b/gee-cache/day2-single-node/geecache/cache.go @@ -6,14 +6,14 @@ import ( ) type cache struct { - mu sync.RWMutex + mu sync.Mutex lru *lru.Cache cacheBytes int64 } func (c *cache) add(key string, value ByteView) { - c.mu.RLock() - defer c.mu.RUnlock() + c.mu.Lock() + defer c.mu.Unlock() if c.lru == nil { c.lru = lru.New(c.cacheBytes, nil) } @@ -21,8 +21,8 @@ func (c *cache) add(key string, value ByteView) { } func (c *cache) get(key string) (value ByteView, ok bool) { - c.mu.RLock() - defer c.mu.RUnlock() + c.mu.Lock() + defer c.mu.Unlock() if c.lru == nil { return } diff --git a/gee-cache/day3-http-server/geecache/cache.go b/gee-cache/day3-http-server/geecache/cache.go index 703d033..665c3f3 100644 --- a/gee-cache/day3-http-server/geecache/cache.go +++ b/gee-cache/day3-http-server/geecache/cache.go @@ -6,14 +6,14 @@ import ( ) type cache struct { - mu sync.RWMutex + mu sync.Mutex lru *lru.Cache cacheBytes int64 } func (c *cache) add(key string, value ByteView) { - c.mu.RLock() - defer c.mu.RUnlock() + c.mu.Lock() + defer c.mu.Unlock() if c.lru == nil { c.lru = lru.New(c.cacheBytes, nil) } @@ -21,8 +21,8 @@ func (c *cache) add(key string, value ByteView) { } func (c *cache) get(key string) (value ByteView, ok bool) { - c.mu.RLock() - defer c.mu.RUnlock() + c.mu.Lock() + defer c.mu.Unlock() if c.lru == nil { return } diff --git a/gee-cache/day4-consistent-hash/geecache/cache.go b/gee-cache/day4-consistent-hash/geecache/cache.go index 703d033..665c3f3 100644 --- a/gee-cache/day4-consistent-hash/geecache/cache.go +++ b/gee-cache/day4-consistent-hash/geecache/cache.go @@ -6,14 +6,14 @@ import ( ) type cache struct { - mu sync.RWMutex + mu sync.Mutex lru *lru.Cache cacheBytes int64 } func (c *cache) add(key string, value ByteView) { - c.mu.RLock() - defer c.mu.RUnlock() + c.mu.Lock() + defer c.mu.Unlock() if c.lru == nil { c.lru = lru.New(c.cacheBytes, nil) } @@ -21,8 +21,8 @@ func (c *cache) add(key string, value ByteView) { } func (c *cache) get(key string) (value ByteView, ok bool) { - c.mu.RLock() - defer c.mu.RUnlock() + c.mu.Lock() + defer c.mu.Unlock() if c.lru == nil { return } diff --git a/gee-cache/day5-multi-nodes/geecache/cache.go b/gee-cache/day5-multi-nodes/geecache/cache.go index 703d033..665c3f3 100644 --- a/gee-cache/day5-multi-nodes/geecache/cache.go +++ b/gee-cache/day5-multi-nodes/geecache/cache.go @@ -6,14 +6,14 @@ import ( ) type cache struct { - mu sync.RWMutex + mu sync.Mutex lru *lru.Cache cacheBytes int64 } func (c *cache) add(key string, value ByteView) { - c.mu.RLock() - defer c.mu.RUnlock() + c.mu.Lock() + defer c.mu.Unlock() if c.lru == nil { c.lru = lru.New(c.cacheBytes, nil) } @@ -21,8 +21,8 @@ func (c *cache) add(key string, value ByteView) { } func (c *cache) get(key string) (value ByteView, ok bool) { - c.mu.RLock() - defer c.mu.RUnlock() + c.mu.Lock() + defer c.mu.Unlock() if c.lru == nil { return } diff --git a/gee-cache/day6-single-flight/geecache/cache.go b/gee-cache/day6-single-flight/geecache/cache.go index 703d033..665c3f3 100644 --- a/gee-cache/day6-single-flight/geecache/cache.go +++ b/gee-cache/day6-single-flight/geecache/cache.go @@ -6,14 +6,14 @@ import ( ) type cache struct { - mu sync.RWMutex + mu sync.Mutex lru *lru.Cache cacheBytes int64 } func (c *cache) add(key string, value ByteView) { - c.mu.RLock() - defer c.mu.RUnlock() + c.mu.Lock() + defer c.mu.Unlock() if c.lru == nil { c.lru = lru.New(c.cacheBytes, nil) } @@ -21,8 +21,8 @@ func (c *cache) add(key string, value ByteView) { } func (c *cache) get(key string) (value ByteView, ok bool) { - c.mu.RLock() - defer c.mu.RUnlock() + c.mu.Lock() + defer c.mu.Unlock() if c.lru == nil { return } diff --git a/gee-cache/day7-proto-buf/geecache/cache.go b/gee-cache/day7-proto-buf/geecache/cache.go index 703d033..665c3f3 100644 --- a/gee-cache/day7-proto-buf/geecache/cache.go +++ b/gee-cache/day7-proto-buf/geecache/cache.go @@ -6,14 +6,14 @@ import ( ) type cache struct { - mu sync.RWMutex + mu sync.Mutex lru *lru.Cache cacheBytes int64 } func (c *cache) add(key string, value ByteView) { - c.mu.RLock() - defer c.mu.RUnlock() + c.mu.Lock() + defer c.mu.Unlock() if c.lru == nil { c.lru = lru.New(c.cacheBytes, nil) } @@ -21,8 +21,8 @@ func (c *cache) add(key string, value ByteView) { } func (c *cache) get(key string) (value ByteView, ok bool) { - c.mu.RLock() - defer c.mu.RUnlock() + c.mu.Lock() + defer c.mu.Unlock() if c.lru == nil { return } From 2031e28df3c731c0a3adda74d83b5d4ebbb561d6 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Wed, 12 Feb 2020 15:09:03 +0800 Subject: [PATCH 030/122] 1. move cloneBytes from geecache.go to byteview.go 2. add more tests --- README.md | 4 +- .../day2-single-node/geecache/byteview.go | 6 + .../day2-single-node/geecache/geecache.go | 6 - .../geecache/geecache_test.go | 23 +- .../day3-http-server/geecache/byteview.go | 6 + .../day3-http-server/geecache/geecache.go | 6 - .../geecache/geecache_test.go | 23 +- gee-cache/day3-http-server/geecache/http.go | 2 +- .../day4-consistent-hash/geecache/byteview.go | 6 + .../day4-consistent-hash/geecache/geecache.go | 6 - .../geecache/geecache_test.go | 23 +- .../day4-consistent-hash/geecache/http.go | 2 +- .../day5-multi-nodes/geecache/byteview.go | 6 + .../day5-multi-nodes/geecache/geecache.go | 6 - .../geecache/geecache_test.go | 23 +- gee-cache/day5-multi-nodes/geecache/http.go | 2 +- .../day6-single-flight/geecache/byteview.go | 6 + .../day6-single-flight/geecache/geecache.go | 6 - .../geecache/geecache_test.go | 23 +- gee-cache/day6-single-flight/geecache/http.go | 2 +- gee-cache/day7-proto-buf/geecache/byteview.go | 6 + gee-cache/day7-proto-buf/geecache/geecache.go | 6 - .../day7-proto-buf/geecache/geecache_test.go | 23 +- gee-cache/day7-proto-buf/geecache/http.go | 2 +- gee-cache/doc/geecache-day1.md | 10 +- gee-cache/doc/geecache-day1/lru.jpg | Bin gee-cache/doc/geecache-day1/lru_logo.jpg | Bin gee-cache/doc/geecache-day2.md | 430 ++++++++++++++++++ .../doc/geecache-day2/concurrent_cache.jpg | Bin 0 -> 13089 bytes .../geecache-day2/concurrent_cache_logo.jpg | Bin 0 -> 5339 bytes gee-cache/doc/geecache-day3.md | 254 +++++++++++ gee-cache/doc/geecache-day3/http.jpg | Bin 0 -> 14453 bytes gee-cache/doc/geecache-day3/http_logo.jpg | Bin 0 -> 6050 bytes gee-cache/doc/geecache.md | 4 +- 34 files changed, 860 insertions(+), 62 deletions(-) mode change 100755 => 100644 gee-cache/doc/geecache-day1/lru.jpg mode change 100755 => 100644 gee-cache/doc/geecache-day1/lru_logo.jpg create mode 100644 gee-cache/doc/geecache-day2.md create mode 100644 gee-cache/doc/geecache-day2/concurrent_cache.jpg create mode 100644 gee-cache/doc/geecache-day2/concurrent_cache_logo.jpg create mode 100644 gee-cache/doc/geecache-day3.md create mode 100755 gee-cache/doc/geecache-day3/http.jpg create mode 100755 gee-cache/doc/geecache-day3/http_logo.jpg diff --git a/README.md b/README.md index 28e085d..89fe062 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,8 @@ [GeeCache](https://geektutu.com/post/geecache.html) 是一个模仿 [groupcache](https://github.com/golang/groupcache) 实现的分布式缓存系统 - [第一天:LRU 缓存淘汰策略](https://geektutu.com/post/geecache-day1.html) | [Code](gee-cache/day1-lru) -- 第二天:单机并发缓存 | [Code](gee-cache/day2-single-node) -- 第三天:HTTP 服务端 | [Code](gee-cache/day3-http-server) +- [第二天:单机并发缓存](https://geektutu.com/post/geecache-day2.html) | [Code](gee-cache/day2-single-node) +- [第三天:HTTP 服务端](https://geektutu.com/post/geecache-day3.html) | [Code](gee-cache/day3-http-server) - 第四天:一致性哈希(Hash) | [Code](gee-cache/day4-consistent-hash) - 第五天:分布式节点 | [Code](gee-cache/day5-multi-nodes) - 第六天:防止缓存击穿 | [Code](gee-cache/day6-single-flight) diff --git a/gee-cache/day2-single-node/geecache/byteview.go b/gee-cache/day2-single-node/geecache/byteview.go index a51394f..3ee1022 100644 --- a/gee-cache/day2-single-node/geecache/byteview.go +++ b/gee-cache/day2-single-node/geecache/byteview.go @@ -19,3 +19,9 @@ func (v ByteView) ByteSlice() []byte { func (v ByteView) String() string { return string(v.b) } + +func cloneBytes(b []byte) []byte { + c := make([]byte, len(b)) + copy(c, b) + return c +} diff --git a/gee-cache/day2-single-node/geecache/geecache.go b/gee-cache/day2-single-node/geecache/geecache.go index 58b98b8..e289018 100644 --- a/gee-cache/day2-single-node/geecache/geecache.go +++ b/gee-cache/day2-single-node/geecache/geecache.go @@ -70,12 +70,6 @@ func (g *Group) Get(key string) (ByteView, error) { return g.load(key) } -func cloneBytes(b []byte) []byte { - c := make([]byte, len(b)) - copy(c, b) - return c -} - func (g *Group) load(key string) (value ByteView, err error) { return g.getLocally(key) } diff --git a/gee-cache/day2-single-node/geecache/geecache_test.go b/gee-cache/day2-single-node/geecache/geecache_test.go index 9cb4079..7ef9f4f 100644 --- a/gee-cache/day2-single-node/geecache/geecache_test.go +++ b/gee-cache/day2-single-node/geecache/geecache_test.go @@ -3,6 +3,7 @@ package geecache import ( "fmt" "log" + "reflect" "testing" ) @@ -12,21 +13,39 @@ var db = map[string]string{ "Sam": "567", } +func TestGetter(t *testing.T) { + var f Getter = GetterFunc(func(key string) ([]byte, error) { + return []byte(key), nil + }) + + expect := []byte("key") + if v, _ := f.Get("key"); !reflect.DeepEqual(v, expect) { + t.Fatal("callback failed") + } +} + func TestGet(t *testing.T) { + loadCounts := make(map[string]int, len(db)) gee := NewGroup("scores", 2<<10, GetterFunc( func(key string) ([]byte, error) { log.Println("[SlowDB] search key", key) if v, ok := db[key]; ok { + if _, ok := loadCounts[key]; !ok { + loadCounts[key] = 0 + } + loadCounts[key]++ return []byte(v), nil } return nil, fmt.Errorf("%s not exist", key) })) for k, v := range db { - view, err := gee.Get(k) - if err != nil || view.String() != v { + if view, err := gee.Get(k); err != nil || view.String() != v { t.Fatal("failed to get value of Tom") } + if _, err := gee.Get(k); err != nil || loadCounts[k] > 1 { + t.Fatalf("cache %s miss", k) + } } if view, err := gee.Get("unknown"); err == nil { diff --git a/gee-cache/day3-http-server/geecache/byteview.go b/gee-cache/day3-http-server/geecache/byteview.go index a51394f..3ee1022 100644 --- a/gee-cache/day3-http-server/geecache/byteview.go +++ b/gee-cache/day3-http-server/geecache/byteview.go @@ -19,3 +19,9 @@ func (v ByteView) ByteSlice() []byte { func (v ByteView) String() string { return string(v.b) } + +func cloneBytes(b []byte) []byte { + c := make([]byte, len(b)) + copy(c, b) + return c +} diff --git a/gee-cache/day3-http-server/geecache/geecache.go b/gee-cache/day3-http-server/geecache/geecache.go index 58b98b8..e289018 100644 --- a/gee-cache/day3-http-server/geecache/geecache.go +++ b/gee-cache/day3-http-server/geecache/geecache.go @@ -70,12 +70,6 @@ func (g *Group) Get(key string) (ByteView, error) { return g.load(key) } -func cloneBytes(b []byte) []byte { - c := make([]byte, len(b)) - copy(c, b) - return c -} - func (g *Group) load(key string) (value ByteView, err error) { return g.getLocally(key) } diff --git a/gee-cache/day3-http-server/geecache/geecache_test.go b/gee-cache/day3-http-server/geecache/geecache_test.go index 9cb4079..7ef9f4f 100644 --- a/gee-cache/day3-http-server/geecache/geecache_test.go +++ b/gee-cache/day3-http-server/geecache/geecache_test.go @@ -3,6 +3,7 @@ package geecache import ( "fmt" "log" + "reflect" "testing" ) @@ -12,21 +13,39 @@ var db = map[string]string{ "Sam": "567", } +func TestGetter(t *testing.T) { + var f Getter = GetterFunc(func(key string) ([]byte, error) { + return []byte(key), nil + }) + + expect := []byte("key") + if v, _ := f.Get("key"); !reflect.DeepEqual(v, expect) { + t.Fatal("callback failed") + } +} + func TestGet(t *testing.T) { + loadCounts := make(map[string]int, len(db)) gee := NewGroup("scores", 2<<10, GetterFunc( func(key string) ([]byte, error) { log.Println("[SlowDB] search key", key) if v, ok := db[key]; ok { + if _, ok := loadCounts[key]; !ok { + loadCounts[key] = 0 + } + loadCounts[key]++ return []byte(v), nil } return nil, fmt.Errorf("%s not exist", key) })) for k, v := range db { - view, err := gee.Get(k) - if err != nil || view.String() != v { + if view, err := gee.Get(k); err != nil || view.String() != v { t.Fatal("failed to get value of Tom") } + if _, err := gee.Get(k); err != nil || loadCounts[k] > 1 { + t.Fatalf("cache %s miss", k) + } } if view, err := gee.Get("unknown"); err == nil { diff --git a/gee-cache/day3-http-server/geecache/http.go b/gee-cache/day3-http-server/geecache/http.go index ef5e2f1..b9b994e 100644 --- a/gee-cache/day3-http-server/geecache/http.go +++ b/gee-cache/day3-http-server/geecache/http.go @@ -16,7 +16,7 @@ type HTTPPool struct { basePath string } -// NewHTTPPool initializes an HTTP pool of peers, and registers itself as a PeerPicker. +// NewHTTPPool initializes an HTTP pool of peers. func NewHTTPPool(self string) *HTTPPool { return &HTTPPool{ self: self, diff --git a/gee-cache/day4-consistent-hash/geecache/byteview.go b/gee-cache/day4-consistent-hash/geecache/byteview.go index a51394f..3ee1022 100644 --- a/gee-cache/day4-consistent-hash/geecache/byteview.go +++ b/gee-cache/day4-consistent-hash/geecache/byteview.go @@ -19,3 +19,9 @@ func (v ByteView) ByteSlice() []byte { func (v ByteView) String() string { return string(v.b) } + +func cloneBytes(b []byte) []byte { + c := make([]byte, len(b)) + copy(c, b) + return c +} diff --git a/gee-cache/day4-consistent-hash/geecache/geecache.go b/gee-cache/day4-consistent-hash/geecache/geecache.go index 58b98b8..e289018 100644 --- a/gee-cache/day4-consistent-hash/geecache/geecache.go +++ b/gee-cache/day4-consistent-hash/geecache/geecache.go @@ -70,12 +70,6 @@ func (g *Group) Get(key string) (ByteView, error) { return g.load(key) } -func cloneBytes(b []byte) []byte { - c := make([]byte, len(b)) - copy(c, b) - return c -} - func (g *Group) load(key string) (value ByteView, err error) { return g.getLocally(key) } diff --git a/gee-cache/day4-consistent-hash/geecache/geecache_test.go b/gee-cache/day4-consistent-hash/geecache/geecache_test.go index 9cb4079..7ef9f4f 100644 --- a/gee-cache/day4-consistent-hash/geecache/geecache_test.go +++ b/gee-cache/day4-consistent-hash/geecache/geecache_test.go @@ -3,6 +3,7 @@ package geecache import ( "fmt" "log" + "reflect" "testing" ) @@ -12,21 +13,39 @@ var db = map[string]string{ "Sam": "567", } +func TestGetter(t *testing.T) { + var f Getter = GetterFunc(func(key string) ([]byte, error) { + return []byte(key), nil + }) + + expect := []byte("key") + if v, _ := f.Get("key"); !reflect.DeepEqual(v, expect) { + t.Fatal("callback failed") + } +} + func TestGet(t *testing.T) { + loadCounts := make(map[string]int, len(db)) gee := NewGroup("scores", 2<<10, GetterFunc( func(key string) ([]byte, error) { log.Println("[SlowDB] search key", key) if v, ok := db[key]; ok { + if _, ok := loadCounts[key]; !ok { + loadCounts[key] = 0 + } + loadCounts[key]++ return []byte(v), nil } return nil, fmt.Errorf("%s not exist", key) })) for k, v := range db { - view, err := gee.Get(k) - if err != nil || view.String() != v { + if view, err := gee.Get(k); err != nil || view.String() != v { t.Fatal("failed to get value of Tom") } + if _, err := gee.Get(k); err != nil || loadCounts[k] > 1 { + t.Fatalf("cache %s miss", k) + } } if view, err := gee.Get("unknown"); err == nil { diff --git a/gee-cache/day4-consistent-hash/geecache/http.go b/gee-cache/day4-consistent-hash/geecache/http.go index ef5e2f1..b9b994e 100644 --- a/gee-cache/day4-consistent-hash/geecache/http.go +++ b/gee-cache/day4-consistent-hash/geecache/http.go @@ -16,7 +16,7 @@ type HTTPPool struct { basePath string } -// NewHTTPPool initializes an HTTP pool of peers, and registers itself as a PeerPicker. +// NewHTTPPool initializes an HTTP pool of peers. func NewHTTPPool(self string) *HTTPPool { return &HTTPPool{ self: self, diff --git a/gee-cache/day5-multi-nodes/geecache/byteview.go b/gee-cache/day5-multi-nodes/geecache/byteview.go index a51394f..3ee1022 100644 --- a/gee-cache/day5-multi-nodes/geecache/byteview.go +++ b/gee-cache/day5-multi-nodes/geecache/byteview.go @@ -19,3 +19,9 @@ func (v ByteView) ByteSlice() []byte { func (v ByteView) String() string { return string(v.b) } + +func cloneBytes(b []byte) []byte { + c := make([]byte, len(b)) + copy(c, b) + return c +} diff --git a/gee-cache/day5-multi-nodes/geecache/geecache.go b/gee-cache/day5-multi-nodes/geecache/geecache.go index dbd5c3e..5372a4a 100644 --- a/gee-cache/day5-multi-nodes/geecache/geecache.go +++ b/gee-cache/day5-multi-nodes/geecache/geecache.go @@ -79,12 +79,6 @@ func (g *Group) RegisterPeers(peers PeerPicker) { g.peers = peers } -func cloneBytes(b []byte) []byte { - c := make([]byte, len(b)) - copy(c, b) - return c -} - func (g *Group) load(key string) (value ByteView, err error) { if g.peers != nil { if peer, ok := g.peers.PickPeer(key); ok { diff --git a/gee-cache/day5-multi-nodes/geecache/geecache_test.go b/gee-cache/day5-multi-nodes/geecache/geecache_test.go index 9cb4079..7ef9f4f 100644 --- a/gee-cache/day5-multi-nodes/geecache/geecache_test.go +++ b/gee-cache/day5-multi-nodes/geecache/geecache_test.go @@ -3,6 +3,7 @@ package geecache import ( "fmt" "log" + "reflect" "testing" ) @@ -12,21 +13,39 @@ var db = map[string]string{ "Sam": "567", } +func TestGetter(t *testing.T) { + var f Getter = GetterFunc(func(key string) ([]byte, error) { + return []byte(key), nil + }) + + expect := []byte("key") + if v, _ := f.Get("key"); !reflect.DeepEqual(v, expect) { + t.Fatal("callback failed") + } +} + func TestGet(t *testing.T) { + loadCounts := make(map[string]int, len(db)) gee := NewGroup("scores", 2<<10, GetterFunc( func(key string) ([]byte, error) { log.Println("[SlowDB] search key", key) if v, ok := db[key]; ok { + if _, ok := loadCounts[key]; !ok { + loadCounts[key] = 0 + } + loadCounts[key]++ return []byte(v), nil } return nil, fmt.Errorf("%s not exist", key) })) for k, v := range db { - view, err := gee.Get(k) - if err != nil || view.String() != v { + if view, err := gee.Get(k); err != nil || view.String() != v { t.Fatal("failed to get value of Tom") } + if _, err := gee.Get(k); err != nil || loadCounts[k] > 1 { + t.Fatalf("cache %s miss", k) + } } if view, err := gee.Get("unknown"); err == nil { diff --git a/gee-cache/day5-multi-nodes/geecache/http.go b/gee-cache/day5-multi-nodes/geecache/http.go index d7da2c3..815591f 100644 --- a/gee-cache/day5-multi-nodes/geecache/http.go +++ b/gee-cache/day5-multi-nodes/geecache/http.go @@ -26,7 +26,7 @@ type HTTPPool struct { httpGetters map[string]*httpGetter // keyed by e.g. "http://10.0.0.2:8008" } -// NewHTTPPool initializes an HTTP pool of peers, and registers itself as a PeerPicker. +// NewHTTPPool initializes an HTTP pool of peers. func NewHTTPPool(self string) *HTTPPool { return &HTTPPool{ self: self, diff --git a/gee-cache/day6-single-flight/geecache/byteview.go b/gee-cache/day6-single-flight/geecache/byteview.go index a51394f..3ee1022 100644 --- a/gee-cache/day6-single-flight/geecache/byteview.go +++ b/gee-cache/day6-single-flight/geecache/byteview.go @@ -19,3 +19,9 @@ func (v ByteView) ByteSlice() []byte { func (v ByteView) String() string { return string(v.b) } + +func cloneBytes(b []byte) []byte { + c := make([]byte, len(b)) + copy(c, b) + return c +} diff --git a/gee-cache/day6-single-flight/geecache/geecache.go b/gee-cache/day6-single-flight/geecache/geecache.go index 3965674..69004ac 100644 --- a/gee-cache/day6-single-flight/geecache/geecache.go +++ b/gee-cache/day6-single-flight/geecache/geecache.go @@ -84,12 +84,6 @@ func (g *Group) RegisterPeers(peers PeerPicker) { g.peers = peers } -func cloneBytes(b []byte) []byte { - c := make([]byte, len(b)) - copy(c, b) - return c -} - func (g *Group) load(key string) (value ByteView, err error) { // each key is only fetched once (either locally or remotely) // regardless of the number of concurrent callers. diff --git a/gee-cache/day6-single-flight/geecache/geecache_test.go b/gee-cache/day6-single-flight/geecache/geecache_test.go index 9cb4079..7ef9f4f 100644 --- a/gee-cache/day6-single-flight/geecache/geecache_test.go +++ b/gee-cache/day6-single-flight/geecache/geecache_test.go @@ -3,6 +3,7 @@ package geecache import ( "fmt" "log" + "reflect" "testing" ) @@ -12,21 +13,39 @@ var db = map[string]string{ "Sam": "567", } +func TestGetter(t *testing.T) { + var f Getter = GetterFunc(func(key string) ([]byte, error) { + return []byte(key), nil + }) + + expect := []byte("key") + if v, _ := f.Get("key"); !reflect.DeepEqual(v, expect) { + t.Fatal("callback failed") + } +} + func TestGet(t *testing.T) { + loadCounts := make(map[string]int, len(db)) gee := NewGroup("scores", 2<<10, GetterFunc( func(key string) ([]byte, error) { log.Println("[SlowDB] search key", key) if v, ok := db[key]; ok { + if _, ok := loadCounts[key]; !ok { + loadCounts[key] = 0 + } + loadCounts[key]++ return []byte(v), nil } return nil, fmt.Errorf("%s not exist", key) })) for k, v := range db { - view, err := gee.Get(k) - if err != nil || view.String() != v { + if view, err := gee.Get(k); err != nil || view.String() != v { t.Fatal("failed to get value of Tom") } + if _, err := gee.Get(k); err != nil || loadCounts[k] > 1 { + t.Fatalf("cache %s miss", k) + } } if view, err := gee.Get("unknown"); err == nil { diff --git a/gee-cache/day6-single-flight/geecache/http.go b/gee-cache/day6-single-flight/geecache/http.go index d7da2c3..815591f 100644 --- a/gee-cache/day6-single-flight/geecache/http.go +++ b/gee-cache/day6-single-flight/geecache/http.go @@ -26,7 +26,7 @@ type HTTPPool struct { httpGetters map[string]*httpGetter // keyed by e.g. "http://10.0.0.2:8008" } -// NewHTTPPool initializes an HTTP pool of peers, and registers itself as a PeerPicker. +// NewHTTPPool initializes an HTTP pool of peers. func NewHTTPPool(self string) *HTTPPool { return &HTTPPool{ self: self, diff --git a/gee-cache/day7-proto-buf/geecache/byteview.go b/gee-cache/day7-proto-buf/geecache/byteview.go index a51394f..3ee1022 100644 --- a/gee-cache/day7-proto-buf/geecache/byteview.go +++ b/gee-cache/day7-proto-buf/geecache/byteview.go @@ -19,3 +19,9 @@ func (v ByteView) ByteSlice() []byte { func (v ByteView) String() string { return string(v.b) } + +func cloneBytes(b []byte) []byte { + c := make([]byte, len(b)) + copy(c, b) + return c +} diff --git a/gee-cache/day7-proto-buf/geecache/geecache.go b/gee-cache/day7-proto-buf/geecache/geecache.go index 44161bc..cbce7b9 100644 --- a/gee-cache/day7-proto-buf/geecache/geecache.go +++ b/gee-cache/day7-proto-buf/geecache/geecache.go @@ -85,12 +85,6 @@ func (g *Group) RegisterPeers(peers PeerPicker) { g.peers = peers } -func cloneBytes(b []byte) []byte { - c := make([]byte, len(b)) - copy(c, b) - return c -} - func (g *Group) load(key string) (value ByteView, err error) { // each key is only fetched once (either locally or remotely) // regardless of the number of concurrent callers. diff --git a/gee-cache/day7-proto-buf/geecache/geecache_test.go b/gee-cache/day7-proto-buf/geecache/geecache_test.go index 9cb4079..7ef9f4f 100644 --- a/gee-cache/day7-proto-buf/geecache/geecache_test.go +++ b/gee-cache/day7-proto-buf/geecache/geecache_test.go @@ -3,6 +3,7 @@ package geecache import ( "fmt" "log" + "reflect" "testing" ) @@ -12,21 +13,39 @@ var db = map[string]string{ "Sam": "567", } +func TestGetter(t *testing.T) { + var f Getter = GetterFunc(func(key string) ([]byte, error) { + return []byte(key), nil + }) + + expect := []byte("key") + if v, _ := f.Get("key"); !reflect.DeepEqual(v, expect) { + t.Fatal("callback failed") + } +} + func TestGet(t *testing.T) { + loadCounts := make(map[string]int, len(db)) gee := NewGroup("scores", 2<<10, GetterFunc( func(key string) ([]byte, error) { log.Println("[SlowDB] search key", key) if v, ok := db[key]; ok { + if _, ok := loadCounts[key]; !ok { + loadCounts[key] = 0 + } + loadCounts[key]++ return []byte(v), nil } return nil, fmt.Errorf("%s not exist", key) })) for k, v := range db { - view, err := gee.Get(k) - if err != nil || view.String() != v { + if view, err := gee.Get(k); err != nil || view.String() != v { t.Fatal("failed to get value of Tom") } + if _, err := gee.Get(k); err != nil || loadCounts[k] > 1 { + t.Fatalf("cache %s miss", k) + } } if view, err := gee.Get("unknown"); err == nil { diff --git a/gee-cache/day7-proto-buf/geecache/http.go b/gee-cache/day7-proto-buf/geecache/http.go index 4a1c129..be8a44e 100644 --- a/gee-cache/day7-proto-buf/geecache/http.go +++ b/gee-cache/day7-proto-buf/geecache/http.go @@ -29,7 +29,7 @@ type HTTPPool struct { httpGetters map[string]*httpGetter // keyed by e.g. "http://10.0.0.2:8008" } -// NewHTTPPool initializes an HTTP pool of peers, and registers itself as a PeerPicker. +// NewHTTPPool initializes an HTTP pool of peers. func NewHTTPPool(self string) *HTTPPool { return &HTTPPool{ self: self, diff --git a/gee-cache/doc/geecache-day1.md b/gee-cache/doc/geecache-day1.md index f5330d8..6848421 100644 --- a/gee-cache/doc/geecache-day1.md +++ b/gee-cache/doc/geecache-day1.md @@ -1,7 +1,7 @@ --- title: 动手写分布式缓存 - GeeCache第一天 LRU 缓存淘汰策略 date: 2020-02-11 22:00:00 -description: 7天用 Go语言/golang 从零实现分布式缓存 GeeCache 教程(7 days implement golang distributed cache from scratch tutorial),动手写分布式缓存,参照 groupcache 的实现。本文介绍常用的三种缓存淘汰(失效)算法:先进先出(FIFO),最少使用(LFU) 和 最近最少使用(LRU),并实现 LRU 算法和相应的测试代码。 +description: 7天用 Go语言/golang 从零实现分布式缓存 GeeCache 教程(7 days implement golang distributed cache from scratch tutorial),动手写分布式缓存,参照 groupcache 的实现。本文介绍了常用的三种缓存淘汰(失效)算法:先进先出(FIFO),最少使用(LFU) 和 最近最少使用(LRU),并实现 LRU 算法和相应的测试代码。 tags: - Go nav: 从零实现 @@ -9,8 +9,8 @@ categories: - 分布式缓存 - GeeCache keywords: - Go语言 -- 从零实现分布式缓存 -- 动手写分布式缓存 +- 从零实现 +- 分布式缓存 - LRU - 缓存失效 image: post/geecache-day1/lru_logo.jpg @@ -141,7 +141,7 @@ func (c *Cache) RemoveOldest() { ``` - `c.ll.Back()` 取到队首节点,从链表中删除。 -- `delete(c.cache, kv.key)`,从字典中`c.cache`删除该节点的映射关系。 +- `delete(c.cache, kv.key)`,从字典中 `c.cache` 删除该节点的映射关系。 - 更新当前所用的内存 `c.nbytes`。 - 如果回调函数 `OnEvicted` 不为 nil,则调用回调函数。 @@ -167,7 +167,7 @@ func (c *Cache) Add(key string, value Value) { ``` - 如果键存在,则更新对应节点的值,并将该节点移到队尾。 -- 不存在则是新增场景,首先队尾添加新节点 `&entry{key, value}`, 并字典中添加 `key` 和节点的映射关系。 +- 不存在则是新增场景,首先队尾添加新节点 `&entry{key, value}`, 并字典中添加 key 和节点的映射关系。 - 更新 `c.nbytes`,如果超过了设定的最大值 `c.maxBytes`,则移除最少访问的节点。 最后,为了方便测试,我们实现 `Len()` 用来获取添加了多少条数据。 diff --git a/gee-cache/doc/geecache-day1/lru.jpg b/gee-cache/doc/geecache-day1/lru.jpg old mode 100755 new mode 100644 diff --git a/gee-cache/doc/geecache-day1/lru_logo.jpg b/gee-cache/doc/geecache-day1/lru_logo.jpg old mode 100755 new mode 100644 diff --git a/gee-cache/doc/geecache-day2.md b/gee-cache/doc/geecache-day2.md new file mode 100644 index 0000000..c899111 --- /dev/null +++ b/gee-cache/doc/geecache-day2.md @@ -0,0 +1,430 @@ +--- +title: 动手写分布式缓存 - GeeCache第二天 单机并发缓存 +date: 2020-02-12 22:00:00 +description: 7天用 Go语言/golang 从零实现分布式缓存 GeeCache 教程(7 days implement golang distributed cache from scratch tutorial),动手写分布式缓存,参照 groupcache 的实现。本文介绍了 sync.Mutex 互斥锁的使用,并发控制 LRU 缓存。实现 GeeCache 核心数据结构 Group,缓存不存在时,调用回调函数(callback)获取源数据。 +tags: +- Go +nav: 从零实现 +categories: +- 分布式缓存 - GeeCache +keywords: +- Go语言 +- 从零实现 +- 分布式缓存 +- 互斥锁 +- sync.Mutex +image: post/geecache-day2/concurrent_cache_logo.jpg +github: https://github.com/geektutu/7days-golang +--- + +![geecache concurrent cache](geecache-day2/concurrent_cache.jpg) + +本文是[7天用Go从零实现分布式缓存GeeCache](https://geektutu.com/post/geecache.html)的第二篇。 + +- 介绍 sync.Mutex 互斥锁的使用,并实现 LRU 缓存的并发控制。 +- 实现 GeeCache 核心数据结构 Group,缓存不存在时,调用回调函数获取源数据,**代码约150行** + +## 1 sync.Mutex + +多个协程(goroutine)同时读写同一个变量,在并发度较高的情况下,会发生冲突。确保一次只有一个协程(goroutine)可以访问该变量以避免冲突,这称之为`互斥`,互斥锁可以解决这个问题。 + +> sync.Mutex 是一个互斥锁,可以由不同的协程加锁和解锁。 + +`sync.Mutex` 是 Go 语言标准库提供的一个互斥锁,当一个协程(goroutine)获得了这个锁的拥有权后,其它请求锁的协程(goroutine) 就会阻塞在 `Lock()` 方法的调用上,直到调用 `Unlock()` 锁被释放。 + +接下来举一个简单的例子,假设有10个并发的协程打印了同一个数字`100`,为了避免重复打印,实现了`printOnce(num int)` 函数,使用集合 set 记录已打印过的数字,如果数字已打印过,则不再打印。 + +```go +var set = make(map[int]bool, 0) + +func printOnce(num int) { + if _, exist := set[num]; !exist { + fmt.Println(num) + } + set[num] = true +} + +func main() { + for i := 0; i < 10; i++ { + go printOnce(100) + } + time.Sleep(time.Second) +} +``` + +我们运行 `go run .` 会发生什么情况呢? + +```bash +$ go run . +100 +100 +``` + +有时候打印 2 次,有时候打印 4 次,有时候还会触发 panic,因为对同一个数据结构`set`的访问冲突了。接下来用互斥锁的`Lock()`和`Unlock()` 方法将冲突的部分包裹起来: + +```go +var m sync.Mutex +var set = make(map[int]bool, 0) + +func printOnce(num int) { + m.Lock() + if _, exist := set[num]; !exist { + fmt.Println(num) + } + set[num] = true + m.Unlock() +} + +func main() { + for i := 0; i < 10; i++ { + go printOnce(100) + } + time.Sleep(time.Second) +} +``` + +```bash +$ go run . +100 +``` + +相同的数字只会被打印一次。当一个协程调用了 `Lock()` 方法时,其他协程被阻塞了,直到`Unlock()`调用将锁释放。因此被包裹部分的代码就能够避免冲突,实现互斥。 + +`Unlock()`释放锁还有另外一种写法: + +```go +func printOnce(num int) { + m.Lock() + defer m.Unlock() + if _, exist := set[num]; !exist { + fmt.Println(num) + } + set[num] = true +} +``` + +## 2 支持并发读写 + +上一篇文章 [GeeCache 第一天](https://geektutu.com/post/geecache-day1.html) 实现了 LRU 缓存淘汰策略。接下来我们使用 `sync.Mutex` 封装 LRU 的几个方法,使之支持并发的读写。在这之前,我们抽象了一个只读数据结构 `ByteView` 用来表示缓存值,是 GeeCache 主要的数据结构之一。 + +[day2-single-node/geecache/byteview.go - github](https://github.com/geektutu/7days-golang/tree/master/gee-cache/day2-single-node/geecache) + +```go +package geecache + +// A ByteView holds an immutable view of bytes. +type ByteView struct { + b []byte +} + +// Len returns the view's length +func (v ByteView) Len() int { + return len(v.b) +} + +// ByteSlice returns a copy of the data as a byte slice. +func (v ByteView) ByteSlice() []byte { + return cloneBytes(v.b) +} + +// String returns the data as a string, making a copy if necessary. +func (v ByteView) String() string { + return string(v.b) +} + +func cloneBytes(b []byte) []byte { + c := make([]byte, len(b)) + copy(c, b) + return c +} +``` + +- ByteView 只有一个数据成员,`b []byte`,b 将会存储真实的缓存值。选择 byte 类型是为了能够支持任意的数据类型的存储,例如字符串、图片等。 +- 实现 `Len() int` 方法,我们在 lru.Cache 的实现中,要求被缓存对象必须实现 Value 接口,即 `Len() int` 方法,返回其所占的内存大小。 +- `b` 是只读的,使用 `ByteSlice()` 方法返回一个拷贝,防止缓存值被外部程序修改。 + +接下来就可以为 lru.Cache 添加并发特性了。 + +[day2-single-node/geecache/cache.go - github](https://github.com/geektutu/7days-golang/tree/master/gee-cache/day2-single-node/geecache) + +```go +package geecache + +import ( + "geecache/lru" + "sync" +) + +type cache struct { + mu sync.Mutex + lru *lru.Cache + cacheBytes int64 +} + +func (c *cache) add(key string, value ByteView) { + c.mu.Lock() + defer c.mu.Unlock() + if c.lru == nil { + c.lru = lru.New(c.cacheBytes, nil) + } + c.lru.Add(key, value) +} + +func (c *cache) get(key string) (value ByteView, ok bool) { + c.mu.Lock() + defer c.mu.Unlock() + if c.lru == nil { + return + } + + if v, ok := c.lru.Get(key); ok { + return v.(ByteView), ok + } + + return +} +``` + +- `cache.go` 的实现非常简单,实例化 lru,封装 get 和 add 方法,并添加互斥锁 mu。 +- 在 `add` 方法中,判断了 `c.lru` 是否为 nil,如果不等于 nil 再创建实例。这种方法称之为延迟初始化(Lazy Initialization),一个对象的延迟初始化意味着该对象的创建将会延迟至第一次使用该对象时。主要用于提高性能,并减少程序内存要求。 + +## 3 主体结构 Group + +Group 是 GeeCache 最核心的数据结构,负责与用户的交互,并且控制缓存值存储和获取的流程。 + +```bash + 是 +接收 key --> 检查是否被缓存 -----> 返回缓存值 ⑴ + | 否 是 + |-----> 是否应当从远程节点获取 -----> 与远程节点交互 --> 返回缓存值 ⑵ + | 否 + |-----> 调用`回调函数`,获取值并添加到缓存 --> 返回缓存值 ⑶ +``` + +我们将在 `geecache.go` 中实现主体结构 Group,那么 GeeCache 的代码结构的雏形已经形成了。 + +```bash +geecache/ + |--lru/ + |--lru.go // lru 缓存淘汰策略 + |--byteview.go // 缓存值的抽象与封装 + |--cache.go // 并发控制 + |--geecache.go // 负责与外部交互,控制缓存存储和获取的主流程 +``` + +接下来我们将实现流程 ⑴ 和 ⑶,远程交互的部分后续再实现。 + + +### 3.1 回调 Getter + +我们思考一下,如果缓存不存在,应从数据源(文件,数据库等)获取数据并添加到缓存中。GeeCache 是否应该支持多种数据源的配置呢?不应该,一是数据源的种类太多,没办法一一实现;而是扩展性不好。如何从源头获取数据,应该是用户决定的事情,我们就把这件事交给用户好了。因此,我们设计了一个回调函数(callback),在缓存不存在时,调用这个函数,得到源数据。 + +[day2-single-node/geecache/geecache.go - github](https://github.com/geektutu/7days-golang/tree/master/gee-cache/day2-single-node/geecache) + +```go +// A Getter loads data for a key. +type Getter interface { + Get(key string) ([]byte, error) +} + +// A GetterFunc implements Getter with a function. +type GetterFunc func(key string) ([]byte, error) + +// Get implements Getter interface function +func (f GetterFunc) Get(key string) ([]byte, error) { + return f(key) +} +``` + +- 定义接口 Getter 和 回调函数 `Get(key string)([]byte, error)`,参数是 key,返回值是 []byte。 +- 定义函数类型 GetterFunc,并实现 Getter 接口的 `Get` 方法。 + +我们可以写一个测试用例来保证回调函数能够正常工作。 + +```go +func TestGetter(t *testing.T) { + var f Getter = GetterFunc(func(key string) ([]byte, error) { + return []byte(key), nil + }) + + expect := []byte("key") + if v, _ := f.Get("key"); !reflect.DeepEqual(v, expect) { + t.Errorf("callback failed") + } +} +``` + +- 在这个测试用例中,我们借助 GetterFunc 的类型转换,将一个匿名回调函数转换成了接口 `f Getter`。 +- 调用该接口的方法 `f.Get(key string)`,实际上就是在调用匿名回调函数。 + +> 定义一个函数类型 F,并且实现接口 A 的方法,然后在这个方法中调用自己。这是 Go 语言中将其他函数(参数返回值定义与 F 一致)转换为接口 A 的常用技巧。 + +### 3.2 Group 的定义 + +接下来是最核心数据结构 Group 的定义: + +[day2-single-node/geecache/geecache.go - github](https://github.com/geektutu/7days-golang/tree/master/gee-cache/day2-single-node/geecache) + +```go +// A Group is a cache namespace and associated data loaded spread over +type Group struct { + name string + getter Getter + mainCache cache +} + +var ( + mu sync.RWMutex + groups = make(map[string]*Group) +) + +// NewGroup create a new instance of Group +func NewGroup(name string, cacheBytes int64, getter Getter) *Group { + if getter == nil { + panic("nil Getter") + } + mu.Lock() + defer mu.Unlock() + g := &Group{ + name: name, + getter: getter, + mainCache: cache{cacheBytes: cacheBytes}, + } + groups[name] = g + return g +} + +// GetGroup returns the named group previously created with NewGroup, or +// nil if there's no such group. +func GetGroup(name string) *Group { + mu.RLock() + g := groups[name] + mu.RUnlock() + return g +} +``` + +- 一个 Group 可以认为是一个缓存的命名空间,每个 Group 拥有一个唯一的名称 `name`。比如可以创建三个 Group,缓存学生的成绩命名为 scores,缓存学生信息的命名为 info,缓存学生课程的命名为 courses。 +- 第二个属性是 `getter Getter`,即缓存未命中时获取源数据的回调(callback)。 +- 第三个属性是 `mainCache cache`,即一开始实现的并发缓存。 +- 构建函数 `NewGroup` 用来实例化 Group,并且将 group 存储在全局变量 `groups` 中。 +- `GetGroup` 用来特定名称的 Group,这里使用了只读锁 `RLock()`,因为不涉及任何冲突变量的写操作。 + +### 3.3 Group 的 Get 方法 + +接下来是 GeeCache 最为核心的方法 `Get`: + +```go +// Get value for a key from cache +func (g *Group) Get(key string) (ByteView, error) { + if key == "" { + return ByteView{}, fmt.Errorf("key is required") + } + + if v, ok := g.mainCache.get(key); ok { + log.Println("[GeeCache] hit") + return v, nil + } + + return g.load(key) +} + +func (g *Group) load(key string) (value ByteView, err error) { + return g.getLocally(key) +} + +func (g *Group) getLocally(key string) (ByteView, error) { + bytes, err := g.getter.Get(key) + if err != nil { + return ByteView{}, err + + } + value := ByteView{b: cloneBytes(bytes)} + g.populateCache(key, value) + return value, nil +} + +func (g *Group) populateCache(key string, value ByteView) { + g.mainCache.add(key, value) +} +``` + +- Get 方法实现了上述所说的流程 ⑴ 和 ⑶。 +- 流程 ⑴ :从 mainCache 中查找缓存,如果存在则返回缓存值。 +- 流程 ⑶ :缓存不存在,则调用 load 方法,load 调用 getLocally(分布式场景下会调用 getFromPeer 从其他节点获取),getLocally 调用用户回调函数 `g.getter.Get()` 获取源数据,并且将源数据添加到缓存 mainCache 中(通过 populateCache 方法) + +至此,这一章节的单机并发缓存就已经完成了。 + +## 4 测试 + +可以写测试用例,也可以写 main 函数来测试这一章节实现的功能。那我们通过测试用例来看一下,如何使用我们实现的单机并发缓存吧。 + +首先,用一个 map 模拟耗时的数据库。 + +```go +var db = map[string]string{ + "Tom": "630", + "Jack": "589", + "Sam": "567", +} +``` + +创建 group 实例,并测试 `Get` 方法 + +```go +func TestGet(t *testing.T) { + loadCounts := make(map[string]int, len(db)) + gee := NewGroup("scores", 2<<10, GetterFunc( + func(key string) ([]byte, error) { + log.Println("[SlowDB] search key", key) + if v, ok := db[key]; ok { + if _, ok := loadCounts[key]; !ok { + loadCounts[key] = 0 + } + loadCounts[key] += 1 + return []byte(v), nil + } + return nil, fmt.Errorf("%s not exist", key) + })) + + for k, v := range db { + if view, err := gee.Get(k); err != nil || view.String() != v { + t.Fatal("failed to get value of Tom") + } // load from callback function + if _, err := gee.Get(k); err != nil || loadCounts[k] > 1 { + t.Fatalf("cache %s miss", k) + } // cache hit + } + + if view, err := gee.Get("unknown"); err == nil { + t.Fatalf("the value of unknow should be empty, but %s got", view) + } +} +``` + +- 在这个测试用例中,我们主要测试了 2 种情况 +- 1)在缓存为空的情况下,能够通过回调函数获取到源数据。 +- 2)在缓存已经存在的情况下,是否直接从缓存中获取,为了实现这一点,使用 `loadCounts` 统计某个键调用回调函数的次数,如果次数大于1,则表示调用了多次回调函数,没有缓存。 + +测试结果如下: + +```bash +$ go test -run TestGet +2020/02/11 22:07:31 [SlowDB] search key Sam +2020/02/11 22:07:31 [GeeCache] hit +2020/02/11 22:07:31 [SlowDB] search key Tom +2020/02/11 22:07:31 [GeeCache] hit +2020/02/11 22:07:31 [SlowDB] search key Jack +2020/02/11 22:07:31 [GeeCache] hit +2020/02/11 22:07:31 [SlowDB] search key unknown +PASS +ok geecache 0.008s +``` + +可以很清晰地看到,缓存为空时,调用了回调函数,第二次访问时,则直接从缓存中读取。 + +## 附 推荐阅读 + +- [Go 语言简明教程 - 并发编程](https://geektutu.com/post/quick-golang.html#7-并发编程-goroutine) +- [Go Test 单元测试简明教程](https://geektutu.com/post/quick-go-test.html) +- [sync 官方文档 - golang.org](https://golang.org/pkg/sync/) \ No newline at end of file diff --git a/gee-cache/doc/geecache-day2/concurrent_cache.jpg b/gee-cache/doc/geecache-day2/concurrent_cache.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4c2f171087b1d4dfe76e69433b510feb0a6709a0 GIT binary patch literal 13089 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 literal 0 HcmV?d00001 diff --git a/gee-cache/doc/geecache-day2/concurrent_cache_logo.jpg b/gee-cache/doc/geecache-day2/concurrent_cache_logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1a6317c125d145482b277f4b4eab1793a5a7edb7 GIT binary patch literal 5339 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 literal 0 HcmV?d00001 diff --git a/gee-cache/doc/geecache-day3.md b/gee-cache/doc/geecache-day3.md new file mode 100644 index 0000000..b113e7c --- /dev/null +++ b/gee-cache/doc/geecache-day3.md @@ -0,0 +1,254 @@ +--- +title: 动手写分布式缓存 - GeeCache第三天 HTTP 服务端 +date: 2020-02-12 22:00:00 +description: 7天用 Go语言/golang 从零实现分布式缓存 GeeCache 教程(7 days implement golang distributed cache from scratch tutorial),动手写分布式缓存,参照 groupcache 的实现。本文介绍了如何使用标准库 http 搭建 HTTP Server,为 GeeCache 单机节点搭建 HTTP 服务,并进行相关的测试。 +tags: +- Go +nav: 从零实现 +categories: +- 分布式缓存 - GeeCache +keywords: +- Go语言 +- 从零实现 +- 分布式缓存 +- HTTP Server +image: post/geecache-day3/http_logo.jpg +github: https://github.com/geektutu/7days-golang +--- + +![geecache http server](geecache-day3/http.jpg) + +本文是[7天用Go从零实现分布式缓存GeeCache](https://geektutu.com/post/geecache.html)的第三篇。 + +- 介绍如何使用 Go 语言标准库 `http` 搭建 HTTP Server +- 并实现 main 函数启动 HTTP Server 测试 API,**代码约60行** + +## 1 http 标准库 + +Go 语言提供了 `http` 标准库,可以非常方便地搭建 HTTP 服务端和客户端。比如我们可以实现一个服务端,无论接收到什么请求,都返回字符串 "Hello World!" + +```go +package main + +import ( + "log" + "net/http" +) + +type server int + +func (h *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + log.Println(r.URL.Path) + w.Write([]byte("Hello World!")) +} + +func main() { + var s server + http.ListenAndServe("localhost:9999", &s) +} +``` + +- 创建任意类型 server,并实现 `ServeHTTP` 方法。 +- 调用 `http.ListenAndServe` 在 9999 端口启动 http 服务,处理请求的对象为 `s server`。 + +接下来我们执行 `go run .` 启动服务,借助 curl 来测试效果: + +```bash +$ curl http://localhost:9999 +Hello World! +$ curl http://localhost:9999/abc +Hello World! +``` + +Go 程序日志输出 + +```bash +2020/02/11 22:56:32 / +2020/02/11 22:56:34 /abc +``` + +> `http.ListenAndServe` 接收 2 个参数,第一个参数是服务启动的地址,第二个参数是 Handler,任何实现了 `ServeHTTP` 方法的对象都可以作为 HTTP 的 Handler。 + +在标准库中,http.Handler 接口的定义如下: + +```go +package http + +type Handler interface { + ServeHTTP(w ResponseWriter, r *Request) +} +``` + +## 2 GeeCache HTTP 服务端 + +分布式缓存需要实现节点间通信,建立基于 HTTP 的通信机制是比较常见和简单的做法。如果一个节点启动了 HTTP 服务,那么这个节点就可以被其他节点访问。今天我们就为单机节点搭建 HTTP Server。 + +不与其他部分耦合,我们将这部分代码放在新的 `http.go` 文件中,当前的代码结构如下: + +```bash +geecache/ + |--lru/ + |--lru.go // lru 缓存淘汰策略 + |--byteview.go // 缓存值的抽象与封装 + |--cache.go // 并发控制 + |--geecache.go // 负责与外部交互,控制缓存存储和获取的主流程 + |--http.go // 提供被其他节点访问的能力(基于http) +``` + +首先我们创建一个结构体 `HTTPPool`,作为承载节点间 HTTP 通信的核心数据结构(包括服务端和客户端,今天只实现服务端)。 + +[day3-http-server/geecache/http.go - github](https://github.com/geektutu/7days-golang/tree/master/gee-cache/day3-http-server/geecache) + +```go +package geecache + +import ( + "fmt" + "log" + "net/http" + "strings" +) + +const defaultBasePath = "/_geecache/" + +// HTTPPool implements PeerPicker for a pool of HTTP peers. +type HTTPPool struct { + // this peer's base URL, e.g. "https://example.net:8000" + self string + basePath string +} + +// NewHTTPPool initializes an HTTP pool of peers. +func NewHTTPPool(self string) *HTTPPool { + return &HTTPPool{ + self: self, + basePath: defaultBasePath, + } +} +``` + +- `HTTPPool` 只有 2 个参数,一个是 self,用来记录自己的地址,包括主机名/IP 和端口。 +- 另一个是 basePath,作为节点间通讯地址的前缀,默认是 `/_geecache/`,那么 http://example.com/_geecache/ 开头的请求,就用于节点间的访问。因为一个主机上还可能承载其他的服务,加一段 Path 是一个好习惯。比如,大部分网站的 API 接口,一般以 `/api` 作为前缀。 + +接下来,实现最为核心的 `ServeHTTP` 方法。 + +```go +// Log info with server name +func (p *HTTPPool) Log(format string, v ...interface{}) { + log.Printf("[Server %s] %s", p.self, fmt.Sprintf(format, v...)) +} + +// ServeHTTP handle all http requests +func (p *HTTPPool) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if !strings.HasPrefix(r.URL.Path, p.basePath) { + panic("HTTPPool serving unexpected path: " + r.URL.Path) + } + p.Log("%s %s", r.Method, r.URL.Path) + // /// required + parts := strings.SplitN(r.URL.Path[len(p.basePath):], "/", 2) + if len(parts) != 2 { + http.Error(w, "bad request", http.StatusBadRequest) + return + } + + groupName := parts[0] + key := parts[1] + + group := GetGroup(groupName) + if group == nil { + http.Error(w, "no such group: "+groupName, http.StatusNotFound) + return + } + + view, err := group.Get(key) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/octet-stream") + w.Write(view.ByteSlice()) +} +``` + +- ServeHTTP 的实现逻辑是比较简单的,首先判断访问路径的前缀是否是 `basePath`,不是返回错误。 +- 我们约定访问路径格式为 `///`,通过 groupname 得到 group 实例,再使用 `group.Get(key)` 获取缓存数据。 +- 最终使用 `w.Write()` 将缓存值作为 httpResponse 的 body 返回。 + +到这里,HTTP 服务端已经完整地实现了。接下来,我们将在单机上启动 HTTP 服务,使用 curl 进行测试。 + +## 3 测试 + +实现 main 函数,实例化 group,并启动 HTTP 服务。 + +[day3-http-server/main.go - github](https://github.com/geektutu/7days-golang/tree/master/gee-cache/day3-http-server) + +```go +package main + +import ( + "fmt" + "geecache" + "log" + "net/http" +) + +var db = map[string]string{ + "Tom": "630", + "Jack": "589", + "Sam": "567", +} + +func main() { + geecache.NewGroup("scores", 2<<10, geecache.GetterFunc( + func(key string) ([]byte, error) { + log.Println("[SlowDB] search key", key) + if v, ok := db[key]; ok { + return []byte(v), nil + } + return nil, fmt.Errorf("%s not exist", key) + })) + + addr := "localhost:9999" + peers := geecache.NewHTTPPool(addr) + log.Println("geecache is running at", addr) + log.Fatal(http.ListenAndServe(addr, peers)) +} +``` + +- 同样地,我们使用 map 模拟了数据源 db。 +- 创建一个名为 scores 的 Group,若缓存为空,回调函数会从 db 中获取数据并返回。 +- 使用 http.ListenAndServe 在 9999 端口启动了 HTTP 服务。 + +> 需要注意的点: +> main.go 和 geecache/ 在同级目录,但 go modules 不再支持 import <相对路径>,相对路径需要在 go.mod 中声明: +> require geecache v0.0.0 +> replace geecache => ./geecache + +接下来,运行 main 函数,使用 curl 做一些简单测试: + +```bash +$ curl http://localhost:9999/_geecache/scores/Tom +630 +$ curl http://localhost:9999/_geecache/scores/kkk +kkk not exist +``` + +GeeCache 的日志输出如下: + +```bash +2020/02/11 23:28:39 geecache is running at localhost:9999 +2020/02/11 23:29:08 [Server localhost:9999] GET /_geecache/scores/Tom +2020/02/11 23:29:08 [SlowDB] search key Tom +2020/02/11 23:29:16 [Server localhost:9999] GET /_geecache/scores/kkk +2020/02/11 23:29:16 [SlowDB] search key kkk +``` + +节点间的相互通信不仅需要 HTTP 服务端,还需要 HTTP 客户端,这就是我们下一步需要做的事情。 + +## 附 推荐阅读 + +- [Go 语言简明教程](https://geektutu.com/post/quick-golang.html) +- [Go Test 单元测试简明教程](https://geektutu.com/post/quick-go-test.html) +- [Go http.Handler 基础](https://geektutu.com/post/gee-day1.html) +- [http 官方文档 - golang.org](https://golang.org/pkg/http) \ No newline at end of file diff --git a/gee-cache/doc/geecache-day3/http.jpg b/gee-cache/doc/geecache-day3/http.jpg new file mode 100755 index 0000000000000000000000000000000000000000..20bde205b4a49ae27b28362abba05d7918835004 GIT binary patch literal 14453 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 literal 0 HcmV?d00001 diff --git a/gee-cache/doc/geecache-day3/http_logo.jpg b/gee-cache/doc/geecache-day3/http_logo.jpg new file mode 100755 index 0000000000000000000000000000000000000000..f681ee696a70a1edaff74de8cdc048239ef6f558 GIT binary patch literal 6050 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 Date: Thu, 13 Feb 2020 17:50:10 +0800 Subject: [PATCH 031/122] fix refer link format --- README.md | 6 +++--- gee-cache/doc/geecache.md | 6 +++--- gee-web/doc/gee.md | 15 ++++++++------- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 89fe062..bca1c37 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,9 @@ [GeeCache](https://geektutu.com/post/geecache.html) 是一个模仿 [groupcache](https://github.com/golang/groupcache) 实现的分布式缓存系统 -- [第一天:LRU 缓存淘汰策略](https://geektutu.com/post/geecache-day1.html) | [Code](gee-cache/day1-lru) -- [第二天:单机并发缓存](https://geektutu.com/post/geecache-day2.html) | [Code](gee-cache/day2-single-node) -- [第三天:HTTP 服务端](https://geektutu.com/post/geecache-day3.html) | [Code](gee-cache/day3-http-server) +- 第一天:[LRU 缓存淘汰策略](https://geektutu.com/post/geecache-day1.html) | [Code](gee-cache/day1-lru) +- 第二天:[单机并发缓存](https://geektutu.com/post/geecache-day2.html) | [Code](gee-cache/day2-single-node) +- 第三天:[HTTP 服务端](https://geektutu.com/post/geecache-day3.html) | [Code](gee-cache/day3-http-server) - 第四天:一致性哈希(Hash) | [Code](gee-cache/day4-consistent-hash) - 第五天:分布式节点 | [Code](gee-cache/day5-multi-nodes) - 第六天:防止缓存击穿 | [Code](gee-cache/day6-single-flight) diff --git a/gee-cache/doc/geecache.md b/gee-cache/doc/geecache.md index 1b38ab4..0429e78 100644 --- a/gee-cache/doc/geecache.md +++ b/gee-cache/doc/geecache.md @@ -58,9 +58,9 @@ github: https://github.com/geektutu/7days-golang ## 3 目录 -- [第一天:LRU 缓存淘汰策略](https://geektutu.com/post/geecache-day1.html) | [Code - Github](https://github.com/geektutu/7days-golang/blob/master/gee-cache/day1-lru) -- [第二天:单机并发缓存](https://geektutu.com/post/geecache-day2.html) | [Code - Github](https://github.com/geektutu/7days-golang/blob/master/gee-cache/day2-single-node) -- [第三天:HTTP 服务端](https://geektutu.com/post/geecache-day3.html) | [Code - Github](https://github.com/geektutu/7days-golang/blob/master/gee-cache/day3-http-server) +- 第一天:[LRU 缓存淘汰策略](https://geektutu.com/post/geecache-day1.html) | [Code - Github](https://github.com/geektutu/7days-golang/blob/master/gee-cache/day1-lru) +- 第二天:[单机并发缓存](https://geektutu.com/post/geecache-day2.html) | [Code - Github](https://github.com/geektutu/7days-golang/blob/master/gee-cache/day2-single-node) +- 第三天:[HTTP 服务端](https://geektutu.com/post/geecache-day3.html) | [Code - Github](https://github.com/geektutu/7days-golang/blob/master/gee-cache/day3-http-server) - 第四天:一致性哈希(Hash) | [Code - Github](https://github.com/geektutu/7days-golang/blob/master/gee-cache/day4-consistent-hash) - 第五天:分布式节点 | [Code - Github](https://github.com/geektutu/7days-golang/blob/master/gee-cache/day5-multi-nodes) - 第六天:防止缓存击穿 | [Code - Github](https://github.com/geektutu/7days-golang/blob/master/gee-cache/day6-single-flight) diff --git a/gee-web/doc/gee.md b/gee-web/doc/gee.md index c2581cc..87e3a34 100644 --- a/gee-web/doc/gee.md +++ b/gee-web/doc/gee.md @@ -63,15 +63,16 @@ func handler(w http.ResponseWriter, r *http.Request) { ## 目录 -- [第一天:前置知识(http.Handler接口)](https://geektutu.com/post/gee-day1.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/gee-web/day1-http-base) -- [第二天:上下文设计(Context)](https://geektutu.com/post/gee-day2.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/gee-web/day2-context) -- [第三天:Tire树路由(Router)](https://geektutu.com/post/gee-day3.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/gee-web/day3-router) -- [第四天:分组控制(Group)](https://geektutu.com/post/gee-day4.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/gee-web/day4-group) -- [第五天:中间件(Middleware)](https://geektutu.com/post/gee-day5.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/gee-web/day5-middleware) -- [第六天:HTML模板(Template)](https://geektutu.com/post/gee-day6.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/gee-web/day6-template) -- [第七天:错误恢复(Panic Recover)](https://geektutu.com/post/gee-day7.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/gee-web/day7-panic-recover) +- 第一天:[前置知识(http.Handler接口)](https://geektutu.com/post/gee-day1.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/gee-web/day1-http-base) +- 第二天:[上下文设计(Context)](https://geektutu.com/post/gee-day2.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/gee-web/day2-context) +- 第三天:[Tire树路由(Router)](https://geektutu.com/post/gee-day3.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/gee-web/day3-router) +- 第四天:[分组控制(Group)](https://geektutu.com/post/gee-day4.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/gee-web/day4-group) +- 第五天:[中间件(Middleware)](https://geektutu.com/post/gee-day5.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/gee-web/day5-middleware) +- 第六天:[HTML模板(Template)](https://geektutu.com/post/gee-day6.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/gee-web/day6-template) +- 第七天:[错误恢复(Panic Recover)](https://geektutu.com/post/gee-day7.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/gee-web/day7-panic-recover) ## 推荐阅读 - [Go 语言简明教程](https://geektutu.com/post/quick-golang.html) +- [Go Test 单元测试简明教程](https://geektutu.com/post/quick-golang.html) - [Go Gin 简明教程](https://geektutu.com/post/quick-go-gin.html) \ No newline at end of file From d7a003fd3dba3f37f20d1d1eecb692849fd54954 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Sat, 15 Feb 2020 14:02:38 +0800 Subject: [PATCH 032/122] fix lru dumplicate key, nbytes update issue --- gee-cache/day1-lru/geecache/lru/lru.go | 1 + gee-cache/day1-lru/geecache/lru/lru_test.go | 10 ++++++++++ gee-cache/day2-single-node/geecache/lru/lru.go | 1 + gee-cache/day2-single-node/geecache/lru/lru_test.go | 10 ++++++++++ gee-cache/day3-http-server/geecache/lru/lru.go | 1 + gee-cache/day3-http-server/geecache/lru/lru_test.go | 10 ++++++++++ gee-cache/day4-consistent-hash/geecache/lru/lru.go | 1 + .../day4-consistent-hash/geecache/lru/lru_test.go | 10 ++++++++++ gee-cache/day5-multi-nodes/geecache/lru/lru.go | 1 + gee-cache/day5-multi-nodes/geecache/lru/lru_test.go | 10 ++++++++++ gee-cache/day6-single-flight/geecache/lru/lru.go | 1 + gee-cache/day6-single-flight/geecache/lru/lru_test.go | 10 ++++++++++ gee-cache/day7-proto-buf/geecache/lru/lru.go | 1 + gee-cache/day7-proto-buf/geecache/lru/lru_test.go | 10 ++++++++++ gee-cache/doc/geecache-day1.md | 1 + 15 files changed, 78 insertions(+) diff --git a/gee-cache/day1-lru/geecache/lru/lru.go b/gee-cache/day1-lru/geecache/lru/lru.go index 81eee43..6fa617a 100644 --- a/gee-cache/day1-lru/geecache/lru/lru.go +++ b/gee-cache/day1-lru/geecache/lru/lru.go @@ -37,6 +37,7 @@ func (c *Cache) Add(key string, value Value) { if ele, ok := c.cache[key]; ok { c.ll.MoveToFront(ele) kv := ele.Value.(*entry) + c.nbytes += int64(value.Len()) - int64(kv.value.Len()) kv.value = value return } diff --git a/gee-cache/day1-lru/geecache/lru/lru_test.go b/gee-cache/day1-lru/geecache/lru/lru_test.go index 7308322..f2d3470 100644 --- a/gee-cache/day1-lru/geecache/lru/lru_test.go +++ b/gee-cache/day1-lru/geecache/lru/lru_test.go @@ -53,3 +53,13 @@ func TestOnEvicted(t *testing.T) { t.Fatalf("Call OnEvicted failed, expect keys equals to %s", expect) } } + +func TestAdd(t *testing.T) { + lru := New(int64(0), nil) + lru.Add("key", String("1")) + lru.Add("key", String("111")) + + if lru.nbytes != int64(len("key")+len("111")) { + t.Fatal("expected 6 but got", lru.nbytes) + } +} diff --git a/gee-cache/day2-single-node/geecache/lru/lru.go b/gee-cache/day2-single-node/geecache/lru/lru.go index 81eee43..6fa617a 100644 --- a/gee-cache/day2-single-node/geecache/lru/lru.go +++ b/gee-cache/day2-single-node/geecache/lru/lru.go @@ -37,6 +37,7 @@ func (c *Cache) Add(key string, value Value) { if ele, ok := c.cache[key]; ok { c.ll.MoveToFront(ele) kv := ele.Value.(*entry) + c.nbytes += int64(value.Len()) - int64(kv.value.Len()) kv.value = value return } diff --git a/gee-cache/day2-single-node/geecache/lru/lru_test.go b/gee-cache/day2-single-node/geecache/lru/lru_test.go index 7308322..f2d3470 100644 --- a/gee-cache/day2-single-node/geecache/lru/lru_test.go +++ b/gee-cache/day2-single-node/geecache/lru/lru_test.go @@ -53,3 +53,13 @@ func TestOnEvicted(t *testing.T) { t.Fatalf("Call OnEvicted failed, expect keys equals to %s", expect) } } + +func TestAdd(t *testing.T) { + lru := New(int64(0), nil) + lru.Add("key", String("1")) + lru.Add("key", String("111")) + + if lru.nbytes != int64(len("key")+len("111")) { + t.Fatal("expected 6 but got", lru.nbytes) + } +} diff --git a/gee-cache/day3-http-server/geecache/lru/lru.go b/gee-cache/day3-http-server/geecache/lru/lru.go index 81eee43..6fa617a 100644 --- a/gee-cache/day3-http-server/geecache/lru/lru.go +++ b/gee-cache/day3-http-server/geecache/lru/lru.go @@ -37,6 +37,7 @@ func (c *Cache) Add(key string, value Value) { if ele, ok := c.cache[key]; ok { c.ll.MoveToFront(ele) kv := ele.Value.(*entry) + c.nbytes += int64(value.Len()) - int64(kv.value.Len()) kv.value = value return } diff --git a/gee-cache/day3-http-server/geecache/lru/lru_test.go b/gee-cache/day3-http-server/geecache/lru/lru_test.go index 7308322..f2d3470 100644 --- a/gee-cache/day3-http-server/geecache/lru/lru_test.go +++ b/gee-cache/day3-http-server/geecache/lru/lru_test.go @@ -53,3 +53,13 @@ func TestOnEvicted(t *testing.T) { t.Fatalf("Call OnEvicted failed, expect keys equals to %s", expect) } } + +func TestAdd(t *testing.T) { + lru := New(int64(0), nil) + lru.Add("key", String("1")) + lru.Add("key", String("111")) + + if lru.nbytes != int64(len("key")+len("111")) { + t.Fatal("expected 6 but got", lru.nbytes) + } +} diff --git a/gee-cache/day4-consistent-hash/geecache/lru/lru.go b/gee-cache/day4-consistent-hash/geecache/lru/lru.go index 81eee43..6fa617a 100644 --- a/gee-cache/day4-consistent-hash/geecache/lru/lru.go +++ b/gee-cache/day4-consistent-hash/geecache/lru/lru.go @@ -37,6 +37,7 @@ func (c *Cache) Add(key string, value Value) { if ele, ok := c.cache[key]; ok { c.ll.MoveToFront(ele) kv := ele.Value.(*entry) + c.nbytes += int64(value.Len()) - int64(kv.value.Len()) kv.value = value return } diff --git a/gee-cache/day4-consistent-hash/geecache/lru/lru_test.go b/gee-cache/day4-consistent-hash/geecache/lru/lru_test.go index 7308322..f2d3470 100644 --- a/gee-cache/day4-consistent-hash/geecache/lru/lru_test.go +++ b/gee-cache/day4-consistent-hash/geecache/lru/lru_test.go @@ -53,3 +53,13 @@ func TestOnEvicted(t *testing.T) { t.Fatalf("Call OnEvicted failed, expect keys equals to %s", expect) } } + +func TestAdd(t *testing.T) { + lru := New(int64(0), nil) + lru.Add("key", String("1")) + lru.Add("key", String("111")) + + if lru.nbytes != int64(len("key")+len("111")) { + t.Fatal("expected 6 but got", lru.nbytes) + } +} diff --git a/gee-cache/day5-multi-nodes/geecache/lru/lru.go b/gee-cache/day5-multi-nodes/geecache/lru/lru.go index 81eee43..6fa617a 100644 --- a/gee-cache/day5-multi-nodes/geecache/lru/lru.go +++ b/gee-cache/day5-multi-nodes/geecache/lru/lru.go @@ -37,6 +37,7 @@ func (c *Cache) Add(key string, value Value) { if ele, ok := c.cache[key]; ok { c.ll.MoveToFront(ele) kv := ele.Value.(*entry) + c.nbytes += int64(value.Len()) - int64(kv.value.Len()) kv.value = value return } diff --git a/gee-cache/day5-multi-nodes/geecache/lru/lru_test.go b/gee-cache/day5-multi-nodes/geecache/lru/lru_test.go index 7308322..f2d3470 100644 --- a/gee-cache/day5-multi-nodes/geecache/lru/lru_test.go +++ b/gee-cache/day5-multi-nodes/geecache/lru/lru_test.go @@ -53,3 +53,13 @@ func TestOnEvicted(t *testing.T) { t.Fatalf("Call OnEvicted failed, expect keys equals to %s", expect) } } + +func TestAdd(t *testing.T) { + lru := New(int64(0), nil) + lru.Add("key", String("1")) + lru.Add("key", String("111")) + + if lru.nbytes != int64(len("key")+len("111")) { + t.Fatal("expected 6 but got", lru.nbytes) + } +} diff --git a/gee-cache/day6-single-flight/geecache/lru/lru.go b/gee-cache/day6-single-flight/geecache/lru/lru.go index 81eee43..6fa617a 100644 --- a/gee-cache/day6-single-flight/geecache/lru/lru.go +++ b/gee-cache/day6-single-flight/geecache/lru/lru.go @@ -37,6 +37,7 @@ func (c *Cache) Add(key string, value Value) { if ele, ok := c.cache[key]; ok { c.ll.MoveToFront(ele) kv := ele.Value.(*entry) + c.nbytes += int64(value.Len()) - int64(kv.value.Len()) kv.value = value return } diff --git a/gee-cache/day6-single-flight/geecache/lru/lru_test.go b/gee-cache/day6-single-flight/geecache/lru/lru_test.go index 7308322..f2d3470 100644 --- a/gee-cache/day6-single-flight/geecache/lru/lru_test.go +++ b/gee-cache/day6-single-flight/geecache/lru/lru_test.go @@ -53,3 +53,13 @@ func TestOnEvicted(t *testing.T) { t.Fatalf("Call OnEvicted failed, expect keys equals to %s", expect) } } + +func TestAdd(t *testing.T) { + lru := New(int64(0), nil) + lru.Add("key", String("1")) + lru.Add("key", String("111")) + + if lru.nbytes != int64(len("key")+len("111")) { + t.Fatal("expected 6 but got", lru.nbytes) + } +} diff --git a/gee-cache/day7-proto-buf/geecache/lru/lru.go b/gee-cache/day7-proto-buf/geecache/lru/lru.go index 81eee43..6fa617a 100644 --- a/gee-cache/day7-proto-buf/geecache/lru/lru.go +++ b/gee-cache/day7-proto-buf/geecache/lru/lru.go @@ -37,6 +37,7 @@ func (c *Cache) Add(key string, value Value) { if ele, ok := c.cache[key]; ok { c.ll.MoveToFront(ele) kv := ele.Value.(*entry) + c.nbytes += int64(value.Len()) - int64(kv.value.Len()) kv.value = value return } diff --git a/gee-cache/day7-proto-buf/geecache/lru/lru_test.go b/gee-cache/day7-proto-buf/geecache/lru/lru_test.go index 7308322..f2d3470 100644 --- a/gee-cache/day7-proto-buf/geecache/lru/lru_test.go +++ b/gee-cache/day7-proto-buf/geecache/lru/lru_test.go @@ -53,3 +53,13 @@ func TestOnEvicted(t *testing.T) { t.Fatalf("Call OnEvicted failed, expect keys equals to %s", expect) } } + +func TestAdd(t *testing.T) { + lru := New(int64(0), nil) + lru.Add("key", String("1")) + lru.Add("key", String("111")) + + if lru.nbytes != int64(len("key")+len("111")) { + t.Fatal("expected 6 but got", lru.nbytes) + } +} diff --git a/gee-cache/doc/geecache-day1.md b/gee-cache/doc/geecache-day1.md index 6848421..064d90a 100644 --- a/gee-cache/doc/geecache-day1.md +++ b/gee-cache/doc/geecache-day1.md @@ -153,6 +153,7 @@ func (c *Cache) Add(key string, value Value) { if ele, ok := c.cache[key]; ok { c.ll.MoveToFront(ele) kv := ele.Value.(*entry) + c.nbytes += int64(value.Len()) - int64(kv.value.Len()) kv.value = value return } From 0be9d979491294c1f126a4a64fc01fe8a2c685d1 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Sun, 16 Feb 2020 21:33:58 +0800 Subject: [PATCH 033/122] add day4/5 doc --- README.md | 8 +- gee-cache/day6-single-flight/run.sh | 4 + gee-cache/doc/geecache-day3.md | 2 +- gee-cache/doc/geecache-day4.md | 232 ++++++++++++ gee-cache/doc/geecache-day4/add_peer.jpg | Bin 0 -> 41749 bytes gee-cache/doc/geecache-day4/hash.jpg | Bin 0 -> 22150 bytes gee-cache/doc/geecache-day4/hash_logo.jpg | Bin 0 -> 10096 bytes gee-cache/doc/geecache-day4/hash_select.jpg | Bin 0 -> 8310 bytes gee-cache/doc/geecache-day5.md | 355 ++++++++++++++++++ gee-cache/doc/geecache-day5/dist_nodes.jpg | Bin 0 -> 21817 bytes .../doc/geecache-day5/dist_nodes_logo.jpg | Bin 0 -> 10792 bytes gee-cache/doc/geecache-day6.md | 203 ++++++++++ gee-cache/doc/geecache-day6/singleflight.jpg | Bin 0 -> 22694 bytes .../doc/geecache-day6/singleflight_logo.jpg | Bin 0 -> 9351 bytes gee-cache/doc/geecache-day7.md | 173 +++++++++ gee-cache/doc/geecache-day7/protobuf.jpg | Bin 0 -> 21374 bytes gee-cache/doc/geecache-day7/protobuf_logo.jpg | Bin 0 -> 9376 bytes gee-cache/doc/geecache.md | 8 +- 18 files changed, 976 insertions(+), 9 deletions(-) create mode 100644 gee-cache/doc/geecache-day4.md create mode 100755 gee-cache/doc/geecache-day4/add_peer.jpg create mode 100644 gee-cache/doc/geecache-day4/hash.jpg create mode 100644 gee-cache/doc/geecache-day4/hash_logo.jpg create mode 100755 gee-cache/doc/geecache-day4/hash_select.jpg create mode 100644 gee-cache/doc/geecache-day5.md create mode 100644 gee-cache/doc/geecache-day5/dist_nodes.jpg create mode 100644 gee-cache/doc/geecache-day5/dist_nodes_logo.jpg create mode 100644 gee-cache/doc/geecache-day6.md create mode 100644 gee-cache/doc/geecache-day6/singleflight.jpg create mode 100644 gee-cache/doc/geecache-day6/singleflight_logo.jpg create mode 100644 gee-cache/doc/geecache-day7.md create mode 100644 gee-cache/doc/geecache-day7/protobuf.jpg create mode 100644 gee-cache/doc/geecache-day7/protobuf_logo.jpg diff --git a/README.md b/README.md index bca1c37..469482f 100644 --- a/README.md +++ b/README.md @@ -29,10 +29,10 @@ - 第一天:[LRU 缓存淘汰策略](https://geektutu.com/post/geecache-day1.html) | [Code](gee-cache/day1-lru) - 第二天:[单机并发缓存](https://geektutu.com/post/geecache-day2.html) | [Code](gee-cache/day2-single-node) - 第三天:[HTTP 服务端](https://geektutu.com/post/geecache-day3.html) | [Code](gee-cache/day3-http-server) -- 第四天:一致性哈希(Hash) | [Code](gee-cache/day4-consistent-hash) -- 第五天:分布式节点 | [Code](gee-cache/day5-multi-nodes) -- 第六天:防止缓存击穿 | [Code](gee-cache/day6-single-flight) -- 第七天:使用 Protobuf 通信 | [Code](gee-cache/day7-proto-buf) +- 第四天:[一致性哈希(Hash)](https://geektutu.com/post/geecache-day4.html) | [Code](gee-cache/day4-consistent-hash) +- 第五天:[分布式节点](https://geektutu.com/post/geecache-day5.html) | [Code](gee-cache/day5-multi-nodes) +- 第六天:[防止缓存击穿](https://geektutu.com/post/geecache-day6.html) | [Code](gee-cache/day6-single-flight) +- 第七天:[使用 Protobuf 通信](https://geektutu.com/post/geecache-day7.html) | [Code](gee-cache/day7-proto-buf) ### WebAssembly 使用示例 diff --git a/gee-cache/day6-single-flight/run.sh b/gee-cache/day6-single-flight/run.sh index 066979d..28b421c 100755 --- a/gee-cache/day6-single-flight/run.sh +++ b/gee-cache/day6-single-flight/run.sh @@ -11,5 +11,9 @@ echo ">>> start test" curl "http://localhost:9999/api?key=Tom" & curl "http://localhost:9999/api?key=Tom" & curl "http://localhost:9999/api?key=Tom" & +curl "http://localhost:9999/api?key=Tom" & +curl "http://localhost:9999/api?key=Tom" & +curl "http://localhost:9999/api?key=Tom" & +curl "http://localhost:9999/api?key=Tom" & wait \ No newline at end of file diff --git a/gee-cache/doc/geecache-day3.md b/gee-cache/doc/geecache-day3.md index b113e7c..a129432 100644 --- a/gee-cache/doc/geecache-day3.md +++ b/gee-cache/doc/geecache-day3.md @@ -1,6 +1,6 @@ --- title: 动手写分布式缓存 - GeeCache第三天 HTTP 服务端 -date: 2020-02-12 22:00:00 +date: 2020-02-12 23:00:00 description: 7天用 Go语言/golang 从零实现分布式缓存 GeeCache 教程(7 days implement golang distributed cache from scratch tutorial),动手写分布式缓存,参照 groupcache 的实现。本文介绍了如何使用标准库 http 搭建 HTTP Server,为 GeeCache 单机节点搭建 HTTP 服务,并进行相关的测试。 tags: - Go diff --git a/gee-cache/doc/geecache-day4.md b/gee-cache/doc/geecache-day4.md new file mode 100644 index 0000000..30ca624 --- /dev/null +++ b/gee-cache/doc/geecache-day4.md @@ -0,0 +1,232 @@ +--- +title: 动手写分布式缓存 - GeeCache第四天 一致性哈希(hash) +date: 2020-02-16 20:00:00 +description: 7天用 Go语言/golang 从零实现分布式缓存 GeeCache 教程(7 days implement golang distributed cache from scratch tutorial),动手写分布式缓存,参照 groupcache 的实现。本文介绍了一致性哈希(consistent hashing)的原理、实现以及相关测试用例,一致性哈希为什么能避免缓存雪崩,虚拟节点为什么能解决数据倾斜的问题。 +tags: +- Go +nav: 从零实现 +categories: +- 分布式缓存 - GeeCache +keywords: +- Go语言 +- 从零实现 +- 一致性hash +- consistent hash +image: post/geecache-day4/hash_logo.jpg +github: https://github.com/geektutu/7days-golang +--- + +![一致性哈希 consistent hashing](geecache-day4/hash.jpg) + +本文是[7天用Go从零实现分布式缓存GeeCache](https://geektutu.com/post/geecache.html)的第四篇。 + +- 一致性哈希(consistent hashing)的原理以及为什么要使用一致性哈希。 +- 实现一致性哈希代码,添加相应的测试用例,**代码约60行** + +## 1 为什么使用一致性哈希 + +今天我们要实现的是一致性哈希算法,一致性哈希算法是 GeeCache 从单节点走向分布式节点的一个重要的环节。那你可能要问了, + +> 童鞋,一致性哈希算法是啥?为什么要使用一致性哈希算法?这和分布式有什么关系? + +### 1.1 我该访问谁? + +对于分布式缓存来说,当一个节点接收到请求,如果该节点并没有存储缓存值,那么它面临的难题是,从谁那获取数据?自己,还是节点1, 2, 3, 4... 。假设包括自己在内一共有 10 个节点,当一个节点接收到请求时,随机选择一个节点,由该节点从数据源获取数据。 + +假设第一次随机选取了节点 1 ,节点 1 从数据源获取到数据的同时缓存该数据;那第二次,只有 1/10 的可能性再次选择节点 1, 有 9/10 的概率选择了其他节点,如果选择了其他节点,就意味着需要再一次从数据源获取数据,一般来说,这个操作是很耗时的。这样做,一是缓存效率低,二是各个节点上存储着相同的数据,浪费了大量的存储空间。 + +那有什么办法,对于给定的 key,每一次都选择同一个节点呢?使用 hash 算法也能够做到这一点。那把 key 的每一个字符的 ASCII 码加起来,再除以 10 取余数可以吗?当然可以,这可以认为是自定义的 hash 算法。 + +![hash select peer](geecache-day4/hash_select.jpg) + +从上面的图可以看到,任意一个节点任意时刻请求查找键 `Tom` 对应的值,都会分配给节点 2,有效地解决了上述的问题。 + +### 1.2 节点数量变化了怎么办? + +简单求取 Hash 值解决了缓存性能的问题,但是没有考虑节点数量变化的场景。假设,移除了其中一台节点,只剩下 9 个,那么之前 `hash(key) % 10` 变成了 `hash(key) % 9`,也就意味着几乎缓存值对应的节点都发生了改变。即几乎所有的缓存值都失效了。节点在接收到对应的请求时,均需要重新去数据源获取数据,容易引起 `缓存雪崩`。 + +> 缓存雪崩:缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。常因为缓存服务器宕机,或缓存设置了相同的过期时间引起。 + +那如何解决这个问题呢?一致性哈希算法可以。 + +## 2 算法原理 + +### 2.1 步骤 + +一致性哈希算法将 key 映射到 2^32 的空间中,将这个数字首尾相连,形成一个环。 + +- 计算节点/机器(通常使用节点的名称、编号和 IP 地址)的哈希值,放置在环上。 +- 计算 key 的哈希值,放置在环上,顺时针寻找到的第一个节点,就是应选取的节点/机器。 + +![一致性哈希添加节点 consistent hashing add peer](geecache-day4/add_peer.jpg) + +环上有 peer2,peer4,peer6 三个节点,`key11`,`key2`,`key27` 均映射到 peer2,`key23` 映射到 peer4。此时,如果新增节点/机器 peer8,假设它新增位置如图所示,那么只有 `key27` 从 peer2 调整到 peer8,其余的映射均没有发生改变。 + +也就是说,一致性哈希算法,在新增/删除节点时,只需要重新定位该节点附近的一小部分数据,而不需要重新定位所有的节点,这就解决了上述的问题。 + +### 2.2 数据倾斜问题 + +如果服务器的节点过少,容易引起 key 的倾斜。例如上面例子中的 peer2,peer4,peer6 分布在环的上半部分,下半部分是空的。那么映射到环下半部分的 key 都会被分配给 peer2,key 过度向 peer2 倾斜,缓存节点间负载不均。 + +为了解决这个问题,引入了虚拟节点的概念,一个真实节点对应多个虚拟节点。 + +假设 1 个真实节点对应 3 个虚拟节点,那么 peer1 对应的虚拟节点是 peer1-1、 peer1-2、 peer1-3(通常以添加编号的方式实现),其余节点也以相同的方式操作。 + +- 第一步,计算虚拟节点的 Hash 值,放置在环上。 +- 第二步,计算 key 的 Hash 值,在环上顺时针寻找到应选取的虚拟节点,例如是 peer2-1,那么就对应真实节点 peer2。 + +虚拟节点扩充了节点的数量,解决了节点较少的情况下数据容易倾斜的问题。而且代价非常小,只需要增加一个字典(map)维护真实节点与虚拟节点的映射关系即可。 + +## 3 Go语言实现 + +我们在 geecache 目录下新建 package `consistenthash`,用来实现一致性哈希算法。 + +[day4-consistent-hash/geecache/consistenthash/consistenthash.go](https://github.com/geektutu/7days-golang/tree/master/gee-cache/day4-consistent-hash/geecache/consistenthash) + +```go +package consistenthash + +import ( + "hash/crc32" + "sort" + "strconv" +) + +// Hash maps bytes to uint32 +type Hash func(data []byte) uint32 + +// Map constains all hashed keys +type Map struct { + hash Hash + replicas int + keys []int // Sorted + hashMap map[int]string +} + +// New creates a Map instance +func New(replicas int, fn Hash) *Map { + m := &Map{ + replicas: replicas, + hash: fn, + hashMap: make(map[int]string), + } + if m.hash == nil { + m.hash = crc32.ChecksumIEEE + } + return m +} +``` + +- 定义了函数类型 `Hash`,采取依赖注入的方式,允许用于替换成自定义的 Hash 函数,也方便测试时替换,默认为 `crc32.ChecksumIEEE` 算法。 +- `Map` 是一致性哈希算法的主数据结构,包含 4 个成员变量:Hash 函数 `hash`;虚拟节点倍数 `replicas`;哈希环 `keys`;虚拟节点与真实节点的映射表 `hashMap`,键是虚拟节点的哈希值,值是真实节点的名称。 +- 构造函数 `New()` 允许自定义虚拟节点倍数和 Hash 函数。 + +接下来,实现添加真实节点/机器的 `Add()` 方法。 + +```go +// Add adds some keys to the hash. +func (m *Map) Add(keys ...string) { + for _, key := range keys { + for i := 0; i < m.replicas; i++ { + hash := int(m.hash([]byte(strconv.Itoa(i) + key))) + m.keys = append(m.keys, hash) + m.hashMap[hash] = key + } + } + sort.Ints(m.keys) +} +``` + +- `Add` 函数允许传入 0 或 多个真实节点的名称。 +- 对每一个真实节点 `key`,对应创建 `m.replicas` 个虚拟节点,虚拟节点的名称是:`strconv.Itoa(i) + key`,即通过添加编号的方式区分不同虚拟节点。 +- 使用 `m.hash()` 计算虚拟节点的哈希值,使用 `append(m.keys, hash)` 添加到环上。 +- 在 `hashMap` 中增加虚拟节点和真实节点的映射关系。 +- 最后一步,环上的哈希值排序。 + +最后一步,实现选择节点的 `Get()` 方法。 + +```go +// Get gets the closest item in the hash to the provided key. +func (m *Map) Get(key string) string { + if len(m.keys) == 0 { + return "" + } + + hash := int(m.hash([]byte(key))) + // Binary search for appropriate replica. + idx := sort.Search(len(m.keys), func(i int) bool { + return m.keys[i] >= hash + }) + + return m.hashMap[m.keys[idx%len(m.keys)]] +} +``` + +- 选择节点就非常简单了,第一步,计算 key 的哈希值。 +- 第二步,顺时针找到第一个匹配的虚拟节点的下标 `idx`,从 m.keys 中获取到对应的哈希值。如果 `idx == len(m.keys)`,说明应选择 `m.keys[0]`,因为 `m.keys` 是一个环状结构,所以用取余数的方式来处理这种情况。 +- 第三步,通过 `hashMap` 映射得到真实的节点。 + +至此,整个一致性哈希算法就实现完成了。 + +## 4 测试 + +最后呢,需要测试用例来验证我们的实现是否有问题。 + +[day4-consistent-hash/geecache/consistenthash/consistenthash_test.go](https://github.com/geektutu/7days-golang/tree/master/gee-cache/day4-consistent-hash/geecache/consistenthash) + +```go +package consistenthash + +import ( + "strconv" + "testing" +) + +func TestHashing(t *testing.T) { + hash := New(3, func(key []byte) uint32 { + i, _ := strconv.Atoi(string(key)) + return uint32(i) + }) + + // Given the above hash function, this will give replicas with "hashes": + // 2, 4, 6, 12, 14, 16, 22, 24, 26 + hash.Add("6", "4", "2") + + testCases := map[string]string{ + "2": "2", + "11": "2", + "23": "4", + "27": "2", + } + + for k, v := range testCases { + if hash.Get(k) != v { + t.Errorf("Asking for %s, should have yielded %s", k, v) + } + } + + // Adds 8, 18, 28 + hash.Add("8") + + // 27 should now map to 8. + testCases["27"] = "8" + + for k, v := range testCases { + if hash.Get(k) != v { + t.Errorf("Asking for %s, should have yielded %s", k, v) + } + } + +} +``` + +如果要进行测试,那么我们需要明确地知道每一个传入的 key 的哈希值,那使用默认的 `crc32.ChecksumIEEE` 算法显然达不到目的。所以在这里使用了自定义的 Hash 算法。自定义的 Hash 算法只处理数字,传入字符串表示的数字,返回对应的数字即可。 + +- 一开始,有 2/4/6 三个真实节点,对应的虚拟节点的哈希值是 02/12/22、04/14/24、06/16/26。 +- 那么用例 2/11/23/27 选择的虚拟节点分别是 02/12/24/02,也就是真实节点 2/2/4/2。 +- 添加一个真实节点 8,对应虚拟节点的哈希值是 08/18/28,此时,用例 27 对应的虚拟节点从 `02` 变更为 `28`,即真实节点 8。 + +## 附 推荐阅读 + +- [Go 语言简明教程](https://geektutu.com/post/quick-golang.html) +- [Go Test 单元测试简明教程](https://geektutu.com/post/quick-go-test.html) \ No newline at end of file diff --git a/gee-cache/doc/geecache-day4/add_peer.jpg b/gee-cache/doc/geecache-day4/add_peer.jpg new file mode 100755 index 0000000000000000000000000000000000000000..edf56f5856043c8368210b3fc62507aff8fded3c GIT binary patch literal 41749 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 literal 0 HcmV?d00001 diff --git a/gee-cache/doc/geecache-day4/hash_logo.jpg b/gee-cache/doc/geecache-day4/hash_logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..95778c9636dd2739189edb4c4cc89c1415f9f95b GIT binary patch literal 10096 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 literal 0 HcmV?d00001 diff --git a/gee-cache/doc/geecache-day5.md b/gee-cache/doc/geecache-day5.md new file mode 100644 index 0000000..28111ae --- /dev/null +++ b/gee-cache/doc/geecache-day5.md @@ -0,0 +1,355 @@ +--- +title: 动手写分布式缓存 - GeeCache第五天 分布式节点 +date: 2020-02-16 21:30:00 +description: 7天用 Go语言/golang 从零实现分布式缓存 GeeCache 教程(7 days implement golang distributed cache from scratch tutorial),动手写分布式缓存,参照 groupcache 的实现。本文介绍了为 GeeCache 添加了注册节点与选择节点的功能,并实现了 HTTP 客户端,与远程节点的服务端通信。 +tags: +- Go +nav: 从零实现 +categories: +- 分布式缓存 - GeeCache +keywords: +- Go语言 +- 从零实现 +- HTTP客户端 +- 分布式节点 +image: post/geecache-day5/dist_nodes_logo.jpg +github: https://github.com/geektutu/7days-golang +--- + +![分布式缓存节点](geecache-day5/dist_nodes.jpg) + +本文是[7天用Go从零实现分布式缓存GeeCache](https://geektutu.com/post/geecache.html)的第五篇。 + +- 注册节点(Register Peers),借助一致性哈希算法选择节点。 +- 实现 HTTP 客户端,与远程节点的服务端通信,**代码约90行** + +## 1 流程回顾 + +```bash + 是 +接收 key --> 检查是否被缓存 -----> 返回缓存值 ⑴ + | 否 是 + |-----> 是否应当从远程节点获取 -----> 与远程节点交互 --> 返回缓存值 ⑵ + | 否 + |-----> 调用`回调函数`,获取值并添加到缓存 --> 返回缓存值 ⑶ +``` + +我们在[GeeCache 第二天](https://geektutu.com/post/geecache-day2.html) 中描述了 geecache 的流程。在这之前已经实现了流程 ⑴ 和 ⑶,今天实现流程 ⑵,从远程节点获取缓存值。 + +我们进一步细化流程 ⑵: + +```bash +使用一致性哈希选择节点 是 是 + |-----> 是否是远程节点 -----> HTTP 客户端访问远程节点 --> 成功?-----> 服务端返回返回值 + | 否 ↓ 否 + |----------------------------> 回退到本地节点处理。 +``` + +## 2 抽象 PeerPicker + +[day5-multi-nodes/geecache/peers.go - github](https://github.com/geektutu/7days-golang/tree/master/gee-cache/day5-multi-nodes/geecache) + + +```go +package geecache + +// PeerPicker is the interface that must be implemented to locate +// the peer that owns a specific key. +type PeerPicker interface { + PickPeer(key string) (peer PeerGetter, ok bool) +} + +// PeerGetter is the interface that must be implemented by a peer. +type PeerGetter interface { + Get(group string, key string) ([]byte, error) +} +``` + +- 在这里,抽象出 2 个接口,PeerPicker 的 `PickPeer()` 方法用于根据传入的 key 选择相应节点 PeerGetter。 +- 接口 PeerGetter 的 `Get()` 方法用于从对应 group 查找缓存值。PeerGetter 就对应于上述流程中的 HTTP 客户端。 + +## 3 节点选择与 HTTP 客户端 + + +在 [GeeCache 第三天](https://geektutu.com/post/geecache-day3.html) 中我们为 `HTTPPool` 实现了服务端功能,通信不仅需要服务端还需要客户端,因此,我们接下来要为 `HTTPPool` 实现客户端的功能。 + +首先创建具体的 HTTP 客户端类 `httpGetter`,实现 PeerGetter 接口。 + +[day5-multi-nodes/geecache/http.go - github](https://github.com/geektutu/7days-golang/tree/master/gee-cache/day5-multi-nodes/geecache) + +```go +type httpGetter struct { + baseURL string +} + +func (h *httpGetter) Get(group string, key string) ([]byte, error) { + u := fmt.Sprintf( + "%v%v/%v", + h.baseURL, + url.QueryEscape(group), + url.QueryEscape(key), + ) + res, err := http.Get(u) + if err != nil { + return nil, err + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("server returned: %v", res.Status) + } + + bytes, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, fmt.Errorf("reading response body: %v", err) + } + + return bytes, nil +} + +var _ PeerGetter = (*httpGetter)(nil) +``` + +- baseURL 表示将要访问的远程节点的地址,例如 `http://example.com/_geecache/`。 +- 使用 `http.Get()` 方式获取返回值,并转换为 `[]bytes` 类型。 + +第二步,为 HTTPPool 添加节点选择的功能。 + +```go +const ( + defaultBasePath = "/_geecache/" + defaultReplicas = 50 +) +// HTTPPool implements PeerPicker for a pool of HTTP peers. +type HTTPPool struct { + // this peer's base URL, e.g. "https://example.net:8000" + self string + basePath string + mu sync.Mutex // guards peers and httpGetters + peers *consistenthash.Map + httpGetters map[string]*httpGetter // keyed by e.g. "http://10.0.0.2:8008" +} +``` + +- 新增成员变量 `peers`,类型是一致性哈希算法的 `Map`,用来根据具体的 key 选择节点。 +- 新增成员变量 `httpGetters`,映射远程节点与对应的 httpGetter。每一个远程节点对应一个 httpGetter,因为 httpGetter 与远程节点的地址 `baseURL` 有关。 + +第三步,实现 PeerPicker 接口。 + +```go +// Set updates the pool's list of peers. +func (p *HTTPPool) Set(peers ...string) { + p.mu.Lock() + defer p.mu.Unlock() + p.peers = consistenthash.New(defaultReplicas, nil) + p.peers.Add(peers...) + p.httpGetters = make(map[string]*httpGetter, len(peers)) + for _, peer := range peers { + p.httpGetters[peer] = &httpGetter{baseURL: peer + p.basePath} + } +} + +// PickPeer picks a peer according to key +func (p *HTTPPool) PickPeer(key string) (PeerGetter, bool) { + p.mu.Lock() + defer p.mu.Unlock() + if peer := p.peers.Get(key); peer != "" && peer != p.self { + p.Log("Pick peer %s", peer) + return p.httpGetters[peer], true + } + return nil, false +} + +var _ PeerPicker = (*HTTPPool)(nil) +``` + +- `Set()` 方法实例化了一致性哈希算法,并且添加了传入的节点。 +- 并为每一个节点创建了一个 HTTP 客户端 `httpGetter`。 +- `PickerPeer()` 包装了一致性哈希算法的 `Get()` 方法,根据具体的 key,选择节点,返回节点对应的 HTTP 客户端。 + +至此,HTTPPool 既具备了提供 HTTP 服务的能力,也具备了根据具体的 key,创建 HTTP 客户端从远程节点获取缓存值的能力。 + +## 4 实现主流程 + +最后,我们需要将上述新增的功能集成在主流程(geecache.go)中。 + +[day5-multi-nodes/geecache/geecache.go - github](https://github.com/geektutu/7days-golang/tree/master/gee-cache/day5-multi-nodes/geecache) + +```go +// A Group is a cache namespace and associated data loaded spread over +type Group struct { + name string + getter Getter + mainCache cache + peers PeerPicker +} + +// RegisterPeers registers a PeerPicker for choosing remote peer +func (g *Group) RegisterPeers(peers PeerPicker) { + if g.peers != nil { + panic("RegisterPeerPicker called more than once") + } + g.peers = peers +} + +func (g *Group) load(key string) (value ByteView, err error) { + if g.peers != nil { + if peer, ok := g.peers.PickPeer(key); ok { + if value, err = g.getFromPeer(peer, key); err == nil { + return value, nil + } + log.Println("[GeeCache] Failed to get from peer", err) + } + } + + return g.getLocally(key) +} + +func (g *Group) getFromPeer(peer PeerGetter, key string) (ByteView, error) { + bytes, err := peer.Get(g.name, key) + if err != nil { + return ByteView{}, err + } + return ByteView{b: bytes}, nil +} +``` + +- 新增 `RegisterPeers()` 方法,将 实现了 PeerPicker 接口的 HTTPPool 注入到 Group 中。 +- 新增 `getFromPeer()` 方法,使用实现了 PeerGetter 接口的 httpGetter 从访问远程节点,获取缓存值。 +- 修改 load 方法,使用 `PickPeer()` 方法选择节点,若非本机节点,则调用 `getFromPeer()` 从远程获取。若是本机节点或失败,则回退到 `getLocally()`。 + +## 5 main 函数测试。 + +[day5-multi-nodes/main.go - github](https://github.com/geektutu/7days-golang/tree/master/gee-cache/day5-multi-nodes) + +```go +var db = map[string]string{ + "Tom": "630", + "Jack": "589", + "Sam": "567", +} + +func createGroup() *geecache.Group { + return geecache.NewGroup("scores", 2<<10, geecache.GetterFunc( + func(key string) ([]byte, error) { + log.Println("[SlowDB] search key", key) + if v, ok := db[key]; ok { + return []byte(v), nil + } + return nil, fmt.Errorf("%s not exist", key) + })) +} + +func startCacheServer(addr string, addrs []string, gee *geecache.Group) { + peers := geecache.NewHTTPPool(addr) + peers.Set(addrs...) + gee.RegisterPeers(peers) + log.Println("geecache is running at", addr) + log.Fatal(http.ListenAndServe(addr[7:], peers)) +} + +func startAPIServer(apiAddr string, gee *geecache.Group) { + http.Handle("/api", http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + key := r.URL.Query().Get("key") + view, err := gee.Get(key) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/octet-stream") + w.Write(view.ByteSlice()) + + })) + log.Println("fontend server is running at", apiAddr) + log.Fatal(http.ListenAndServe(apiAddr[7:], nil)) + +} + +func main() { + var port int + var api bool + flag.IntVar(&port, "port", 8001, "Geecache server port") + flag.BoolVar(&api, "api", false, "Start a api server?") + flag.Parse() + + apiAddr := "http://localhost:9999" + addrMap := map[int]string{ + 8001: "http://localhost:8001", + 8002: "http://localhost:8002", + 8003: "http://localhost:8003", + } + + addrs := make([]string, 3) + + for _, v := range addrMap { + addrs = append(addrs, v) + } + + gee := createGroup() + if api { + go startAPIServer(apiAddr, gee) + } + startCacheServer(addrMap[port], []string(addrs), gee) +} +``` + +main 函数的代码比较多,但是逻辑是非常简单的。 + +- `startCacheServer()` 用来启动缓存服务器:创建 HTTPPool,添加节点信息,注册到 gee 中,启动 HTTP 服务(共3个端口,8001/8002/8003),用户不感知。 +- `startAPIServer()` 用来启动一个 API 服务(端口 9999),与用户进行交互,用户感知。 +- `main()` 函数需要命令行传入 `port` 和 `api` 2 个参数,用来在指定端口启动 HTTP 服务。 + +为了方便,我们将启动的命令封装为一个 `shell` 脚本: + +```bash +#!/bin/bash +trap "rm server;kill 0" EXIT + +go build -o server +./server -port=8001 & +./server -port=8002 & +./server -port=8003 -api=1 & + +sleep 2 +echo ">>> start test" +curl "http://localhost:9999/api?key=Tom" & +curl "http://localhost:9999/api?key=Tom" & +curl "http://localhost:9999/api?key=Tom" & + +wait +``` + +- `trap` 命令用于在 shell 脚本退出时,删掉临时文件,结束子进程。 + +```bash +$ ./run.sh +2020/02/16 21:17:43 geecache is running at http://localhost:8001 +2020/02/16 21:17:43 geecache is running at http://localhost:8002 +2020/02/16 21:17:43 geecache is running at http://localhost:8003 +2020/02/16 21:17:43 fontend server is running at http://localhost:9999 +>>> start test +2020/02/16 21:17:45 [Server http://localhost:8003] Pick peer http://localhost:8001 +2020/02/16 21:17:45 [Server http://localhost:8003] Pick peer http://localhost:8001 +2020/02/16 21:17:45 [Server http://localhost:8003] Pick peer http://localhost:8001 +... +630630630 +``` + +此时,我们可以打开一个新的 shell,进行测试: + +```bash +$ curl "http://localhost:9999/api?key=Tom" +630 +$ curl "http://localhost:9999/api?key=kkk" +kkk not exist +``` + +测试的时候,我们并发了 3 个请求 `?key=Tom`,从日志中可以看到,三次均选择了节点 `8001`,这是一致性哈希算法的功劳。但是有一个问题在于,同时向 `8001` 发起了 3 次请求。试想,假如有 10 万个在并发请求该数据呢?那就会向 `8001` 同时发起 10 万次请求,如果 `8001` 又同时向数据库发起 10 万次查询请求,很容易导致缓存被击穿。 + +三次请求的结果是一致的,对于相同的 key,能不能只向 `8001` 发起一次请求?这个问题下一次解决。 + +## 附 推荐阅读 + +- [Go 语言简明教程](https://geektutu.com/post/quick-golang.html) +- [Go Test 单元测试简明教程](https://geektutu.com/post/quick-go-test.html) \ No newline at end of file diff --git a/gee-cache/doc/geecache-day5/dist_nodes.jpg b/gee-cache/doc/geecache-day5/dist_nodes.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ff04b0dce174e46fa57009f5425a4daf7b06fcf8 GIT binary patch literal 21817 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

$!F7DquD&Lmwy*VZG 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 U=l|qI^h **缓存雪崩**:缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。缓存雪崩通常因为缓存服务器宕机、缓存的 key 设置了相同的过期时间等引起。 + +> **缓存击穿**:一个存在的key,在缓存过期的一刻,同时有大量的请求,这些请求都会击穿到 DB ,造成瞬时DB请求量大、压力骤增。 + +> **缓存穿透**:查询一个不存在的数据,因为不存在则不会写到缓存中,所以每次都会去请求 DB,如果瞬间流量过大,穿透到 DB,导致宕机。 + +## 2 singleflight 的实现 + +还记得 [GeeCache 第五天](https://geektutu.com/post/geecache-day5.html) 最后的测试结果吗? + +```bash +2020/02/16 21:17:45 [Server http://localhost:8003] Pick peer http://localhost:8001 +2020/02/16 21:17:45 [Server http://localhost:8003] Pick peer http://localhost:8001 +2020/02/16 21:17:45 [Server http://localhost:8003] Pick peer http://localhost:8001 +``` + +我们并发了 N 个请求 `?key=Tom`,8003 节点向 8001 同时发起了 N 次请求。假设对数据库的访问没有做任何限制的,很可能向数据库也发起 N 次请求,容易导致缓存击穿和穿透。即使对数据库做了防护,HTTP 请求是非常耗费资源的操作,针对相同的 key,8003 节点向 8001 发起三次请求也是没有必要的。那这种情况下,我们如何做到只向远端节点发起一次请求呢? + +geecache 实现了一个名为 singleflight 的 package 来解决这个问题。 + +[day6-single-flight/geecache/singleflight/singleflight.go - github](https://github.com/geektutu/7days-golang/tree/master/gee-cache/day6-single-flight/geecache/singleflight) + +首先创建 `call` 和 `Group` 类型。 + +```go +package singleflight + +import "sync" + +type call struct { + wg sync.WaitGroup + val interface{} + err error +} + +type Group struct { + mu sync.Mutex // protects m + m map[string]*call +} +``` + +- `call` 代表正在进行中,或已经结束的请求。使用 `sync.WaitGroup` 锁避免重入。 +- `Group` 是 singleflight 的主数据结构,管理不同 key 的请求(call)。 + +实现 `Do` 方法 + +```go +func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) { + g.mu.Lock() + if g.m == nil { + g.m = make(map[string]*call) + } + if c, ok := g.m[key]; ok { + g.mu.Unlock() + c.wg.Wait() + return c.val, c.err + } + c := new(call) + c.wg.Add(1) + g.m[key] = c + g.mu.Unlock() + + c.val, c.err = fn() + c.wg.Done() + + g.mu.Lock() + delete(g.m, key) + g.mu.Unlock() + + return c.val, c.err +} +``` + +- Do 方法,接收 2 个参数,第一个参数是 `key`,第二个参数是一个函数 `fn`。Do 的作用就是,针对相同的 key,无论 Do 被调用多少次,函数 `fn` 都只会被调用一次,等待 fn 调用结束了,返回返回值或错误。 + +`g.mu` 是保护 Group 的成员变量 `m` 不被并发读写而加上的锁。为了便于理解 `Do` 函数,我们将 `g.mu` 暂时去掉。并且把 `g.m` 延迟初始化的部分去掉,延迟初始化的目的很简单,提高内存使用效率。 + +剩下的逻辑就很清晰了: + +```go +func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) { + if c, ok := g.m[key]; ok { + c.wg.Wait() // 如果请求正在进行中,则等待 + return c.val, c.err // 请求结束,返回结果 + } + c := new(call) + c.wg.Add(1) // 发起请求前加锁 + g.m[key] = c // 添加到 g.m,表明 key 已经有对应的请求在处理 + + c.val, c.err = fn() // 调用 fn,发起请求 + c.wg.Done() // 请求结束 + + delete(g.m, key) // 更新 g.m + + return c.val, c.err // 返回结果 +} +``` + +并发协程之间不需要消息传递,非常适合 `sync.WaitGroup`。 + +- wg.Add(1) 锁加1。 +- wg.Wait() 阻塞,直到锁被释放。 +- wg.Done() 锁减1。 + +## 3 singleflight 的使用 + +[day6-single-flight/geecache/geecache.go - github](https://github.com/geektutu/7days-golang/tree/master/gee-cache/day6-single-flight/geecache) + +```go +type Group struct { + name string + getter Getter + mainCache cache + peers PeerPicker + // use singleflight.Group to make sure that + // each key is only fetched once + loader *singleflight.Group +} + +func NewGroup(name string, cacheBytes int64, getter Getter) *Group { + // ... + g := &Group{ + // ... + loader: &singleflight.Group{}, + } + return g +} + +func (g *Group) load(key string) (value ByteView, err error) { + // each key is only fetched once (either locally or remotely) + // regardless of the number of concurrent callers. + viewi, err := g.loader.Do(key, func() (interface{}, error) { + if g.peers != nil { + if peer, ok := g.peers.PickPeer(key); ok { + if value, err = g.getFromPeer(peer, key); err == nil { + return value, nil + } + log.Println("[GeeCache] Failed to get from peer", err) + } + } + + return g.getLocally(key) + }) + + if err == nil { + return viewi.(ByteView), nil + } + return +} +``` + +- 修改 `geecache.go` 中的 `Group`,添加成员变量 loader,并更新构建函数 `NewGroup`。 +- 修改 `load` 函数,将原来的 load 的逻辑,使用 `g.loader.Do` 包裹起来即可,这样确保了并发场景下针对相同的 key,`load` 过程只会调用一次。 + +## 4 测试 + +执行 `run.sh` 就可以看到效果了。 + +```bash +$ ./run.sh +2020/02/16 22:36:00 [Server http://localhost:8003] Pick peer http://localhost:8001 +2020/02/16 22:36:00 [Server http://localhost:8001] GET /_geecache/scores/Tom +2020/02/16 22:36:00 [SlowDB] search key Tom +630630630 +``` + +可以看到,向 API 发起了三次并发请求,但8003 只向 8001 发起了一次请求,就搞定了。 + +如果并发度不够高,可能仍会看到向 8001 请求三次的场景。这种情况下三次请求是串行执行的,并没有触发 `singleflight` 的锁机制工作,可以加大并发数量再测试。即,将 `run.sh` 中的 `curl` 命令复制 N 次。 + +## 附 推荐 + +- [Go 语言简明教程#并发编程](https://geektutu.com/post/quick-golang.html#7-%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B-goroutine) +- [Go Test 单元测试简明教程](https://geektutu.com/post/quick-go-test.html) \ No newline at end of file diff --git a/gee-cache/doc/geecache-day6/singleflight.jpg b/gee-cache/doc/geecache-day6/singleflight.jpg new file mode 100644 index 0000000000000000000000000000000000000000..09d0d88de986c79c2b3e8585d8418d6b8fc73380 GIT binary patch literal 22694 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 literal 0 HcmV?d00001 diff --git a/gee-cache/doc/geecache-day6/singleflight_logo.jpg b/gee-cache/doc/geecache-day6/singleflight_logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a53fb410020fe1f00e0219b0ca0b5a488c9b9bf2 GIT binary patch literal 9351 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 protobuf 即 Protocol Buffers,Google 开发的一种数据描述语言,是一种轻便高效的结构化数据存储格式,与语言、平台无关,可扩展可序列化。protobuf 以二进制方式存储,占用空间小。 + +protobuf 的安装和使用教程请移步 [Go Protobuf 简明教程](https://geektutu.com/post/quick-go-protobuf.html),这篇文章就不再赘述了。protobuf 广泛地应用于远程过程调用(RPC) 的二进制传输,使用 protobuf 的目的非常简单,为了获得更高的性能。传输前使用 protobuf 编码,接收方再进行解码,可以显著地降低二进制传输的大小。另外一方面,protobuf 可非常适合传输结构化数据,便于通信字段的扩展。 + +使用 protobuf 一般分为以下 2 步: + +- 按照 protobuf 的语法,在 `.proto` 文件中定义数据结构,并使用 `protoc` 生成 Go 代码(`.proto` 文件是跨平台的,还可以生成 C、Java 等其他源码文件)。 +- 在项目代码中引用生成的 Go 代码。 + +## 2 使用 protobuf 通信 + +新建 package `geecachepb`,定义 `geecachepb.proto` + +[day7-proto-buf/geecache/geecachepb/geecachepb.proto - github](https://github.com/geektutu/7days-golang/tree/master/gee-cache/day7-proto-buf/geecache/geecachepb) + +```go +syntax = "proto3"; + +package geecachepb; + +message Request { + string group = 1; + string key = 2; +} + +message Response { + bytes value = 1; +} + +service GroupCache { + rpc Get(Request) returns (Response); +} +``` + +- `Request` 包含 2 个字段, group 和 cache,这与我们之前定义的接口 `/_geecache//` 所需的参数吻合。 +- `Response` 包含 1 个字段,bytes,类型为 byte 数组,与之前吻合。 + +生成 `geecache.pb.go` + +```bash +$ protoc --go_out=. *.proto +$ ls +geecachepb.pb.go geecachepb.proto +``` + +可以看到 `geecachepb.pb.go` 中有如下数据类型: + +```go +type Request struct { + Group string `protobuf:"bytes,1,opt,name=group,proto3" json:"group,omitempty"` + Key string `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` + ... +} +type Response struct { + Value []byte `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` +} +``` + +接下来,修改 `peers.go` 中的 `PeerGetter` 接口,参数使用 `geecachepb.pb.go` 中的数据类型。 + +[day7-proto-buf/geecache/peers.go - github](https://github.com/geektutu/7days-golang/tree/master/gee-cache/day7-proto-buf/geecache) + +```go +import pb "geecache/geecachepb" + +type PeerGetter interface { + Get(in *pb.Request, out *pb.Response) error +} +``` + +最后,修改 `geecache.go` 和 `http.go` 中使用了 `PeerGetter` 接口的地方。 + +[day7-proto-buf/geecache/geecache.go - github](https://github.com/geektutu/7days-golang/tree/master/gee-cache/day7-proto-buf/geecache) + +```go +import ( + // ... + pb "geecache/geecachepb" +) + +func (g *Group) getFromPeer(peer PeerGetter, key string) (ByteView, error) { + req := &pb.Request{ + Group: g.name, + Key: key, + } + res := &pb.Response{} + err := peer.Get(req, res) + if err != nil { + return ByteView{}, err + } + return ByteView{b: res.Value}, nil +} +``` + +[day7-proto-buf/geecache/http.go - github](https://github.com/geektutu/7days-golang/tree/master/gee-cache/day7-proto-buf/geecache) + +```go +import ( + // ... + pb "geecache/geecachepb" + "github.com/golang/protobuf/proto" +) + +func (p *HTTPPool) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // ... + // Write the value to the response body as a proto message. + body, err := proto.Marshal(&pb.Response{Value: view.ByteSlice()}) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/octet-stream") + w.Write(body) +} + +func (h *httpGetter) Get(in *pb.Request, out *pb.Response) error { + u := fmt.Sprintf( + "%v%v/%v", + h.baseURL, + url.QueryEscape(in.GetGroup()), + url.QueryEscape(in.GetKey()), + ) + res, err := http.Get(u) + // ... + if err = proto.Unmarshal(bytes, out); err != nil { + return fmt.Errorf("decoding response body: %v", err) + } + + return nil +} +``` + +- `ServeHTTP()` 中使用 `proto.Marshal()` 编码 HTTP 响应。 +- `Get()` 中使用 `proto.Unmarshal()` 解码 HTTP 响应。 + +至此,我们已经将 HTTP 通信的中间载体替换成了 protobuf。运行 `run.sh` 即可以测试 GeeCache 能否正常工作。 + +## 总结 + +到这一篇为止,7 天用 Go 动手写/从零实现分布式缓存 GeeCache 这个系列就完成了。简单回顾下。第一天,为了解决资源限制的问题,实现了 LRU 缓存淘汰算法;第二天实现了单机并发,并给用户提供了自定义数据源的回调函数;第三天实现了 HTTP 服务端;第四天实现了一致性哈希算法,解决远程节点的挑选问题;第五天创建 HTTP 客户端,实现了多节点间的通信;第六天实现了 singleflight 解决缓存击穿的问题;第七天,使用 protobuf 库,优化了节点间通信的性能。如果看到这里,还没有动手写的话呢,赶紧动手写起来吧。一天差不多只需要实现 100 行代码呢。 + +## 附 推荐 + +- [Go 语言简明教程](https://geektutu.com/post/quick-golang.html) +- [Go Test 单元测试简明教程](https://geektutu.com/post/quick-go-test.html) +- [Go Protobuf 简明教程](https://geektutu.com/post/quick-go-protobuf.html) \ No newline at end of file diff --git a/gee-cache/doc/geecache-day7/protobuf.jpg b/gee-cache/doc/geecache-day7/protobuf.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ee96320b0ee08ee29d2f6dcf73db60b1fcc50a56 GIT binary patch literal 21374 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 literal 0 HcmV?d00001 diff --git a/gee-cache/doc/geecache-day7/protobuf_logo.jpg b/gee-cache/doc/geecache-day7/protobuf_logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cacd9628408559325a6c74adddb4d6146711ac37 GIT binary patch literal 9376 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 literal 0 HcmV?d00001 diff --git a/gee-cache/doc/geecache.md b/gee-cache/doc/geecache.md index 0429e78..3eb89ad 100644 --- a/gee-cache/doc/geecache.md +++ b/gee-cache/doc/geecache.md @@ -61,10 +61,10 @@ github: https://github.com/geektutu/7days-golang - 第一天:[LRU 缓存淘汰策略](https://geektutu.com/post/geecache-day1.html) | [Code - Github](https://github.com/geektutu/7days-golang/blob/master/gee-cache/day1-lru) - 第二天:[单机并发缓存](https://geektutu.com/post/geecache-day2.html) | [Code - Github](https://github.com/geektutu/7days-golang/blob/master/gee-cache/day2-single-node) - 第三天:[HTTP 服务端](https://geektutu.com/post/geecache-day3.html) | [Code - Github](https://github.com/geektutu/7days-golang/blob/master/gee-cache/day3-http-server) -- 第四天:一致性哈希(Hash) | [Code - Github](https://github.com/geektutu/7days-golang/blob/master/gee-cache/day4-consistent-hash) -- 第五天:分布式节点 | [Code - Github](https://github.com/geektutu/7days-golang/blob/master/gee-cache/day5-multi-nodes) -- 第六天:防止缓存击穿 | [Code - Github](https://github.com/geektutu/7days-golang/blob/master/gee-cache/day6-single-flight) -- 第七天:使用 Protobuf 通信 | [Code - Github](https://github.com/geektutu/7days-golang/blob/master/gee-cache/day7-proto-buf) +- 第四天:[一致性哈希(Hash)](https://geektutu.com/post/geecache-day4.html) | [Code - Github](https://github.com/geektutu/7days-golang/blob/master/gee-cache/day4-consistent-hash) +- 第五天:[分布式节点](https://geektutu.com/post/geecache-day5.html) | [Code - Github](https://github.com/geektutu/7days-golang/blob/master/gee-cache/day5-multi-nodes) +- 第六天:[防止缓存击穿](https://geektutu.com/post/geecache-day6.html) | [Code - Github](https://github.com/geektutu/7days-golang/blob/master/gee-cache/day6-single-flight) +- 第七天:[使用 Protobuf 通信](https://geektutu.com/post/geecache-day7.html) | [Code - Github](https://github.com/geektutu/7days-golang/blob/master/gee-cache/day7-proto-buf) ## 附 推荐阅读 From 30aad5035a981de4afe4c54679f8debd0446395b Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Thu, 20 Feb 2020 20:26:30 +0800 Subject: [PATCH 034/122] use go modules to replace relative path --- gee-web/day1-http-base/base1/go.mod | 3 +++ gee-web/day1-http-base/base2/go.mod | 3 +++ gee-web/day1-http-base/base3/gee/go.mod | 3 +++ gee-web/day1-http-base/base3/go.mod | 7 +++++++ gee-web/day1-http-base/base3/main.go | 2 +- gee-web/day2-context/gee/go.mod | 3 +++ gee-web/day2-context/go.mod | 7 +++++++ gee-web/day2-context/main.go | 2 +- gee-web/day3-router/gee/go.mod | 3 +++ gee-web/day3-router/go.mod | 7 +++++++ gee-web/day3-router/main.go | 2 +- gee-web/day4-group/gee/go.mod | 3 +++ gee-web/day4-group/go.mod | 7 +++++++ gee-web/day4-group/main.go | 2 +- gee-web/day5-middleware/gee/go.mod | 3 +++ gee-web/day5-middleware/go.mod | 7 +++++++ gee-web/day5-middleware/main.go | 2 +- gee-web/day6-template/gee/go.mod | 3 +++ gee-web/day6-template/go.mod | 7 +++++++ gee-web/day6-template/main.go | 2 +- gee-web/day7-panic-recover/gee/go.mod | 3 +++ gee-web/day7-panic-recover/go.mod | 7 +++++++ gee-web/day7-panic-recover/main.go | 2 +- gee-web/doc/gee-day1.md | 22 +++++++++++++++++++++- gee-web/doc/gee-day6.md | 20 ++++++++++++++++++++ gee-web/doc/gee-day7.md | 2 +- 26 files changed, 125 insertions(+), 9 deletions(-) create mode 100644 gee-web/day1-http-base/base1/go.mod create mode 100644 gee-web/day1-http-base/base2/go.mod create mode 100644 gee-web/day1-http-base/base3/gee/go.mod create mode 100644 gee-web/day1-http-base/base3/go.mod create mode 100644 gee-web/day2-context/gee/go.mod create mode 100644 gee-web/day2-context/go.mod create mode 100644 gee-web/day3-router/gee/go.mod create mode 100644 gee-web/day3-router/go.mod create mode 100644 gee-web/day4-group/gee/go.mod create mode 100644 gee-web/day4-group/go.mod create mode 100644 gee-web/day5-middleware/gee/go.mod create mode 100644 gee-web/day5-middleware/go.mod create mode 100644 gee-web/day6-template/gee/go.mod create mode 100644 gee-web/day6-template/go.mod create mode 100644 gee-web/day7-panic-recover/gee/go.mod create mode 100644 gee-web/day7-panic-recover/go.mod diff --git a/gee-web/day1-http-base/base1/go.mod b/gee-web/day1-http-base/base1/go.mod new file mode 100644 index 0000000..8d2394a --- /dev/null +++ b/gee-web/day1-http-base/base1/go.mod @@ -0,0 +1,3 @@ +module example + +go 1.13 diff --git a/gee-web/day1-http-base/base2/go.mod b/gee-web/day1-http-base/base2/go.mod new file mode 100644 index 0000000..8d2394a --- /dev/null +++ b/gee-web/day1-http-base/base2/go.mod @@ -0,0 +1,3 @@ +module example + +go 1.13 diff --git a/gee-web/day1-http-base/base3/gee/go.mod b/gee-web/day1-http-base/base3/gee/go.mod new file mode 100644 index 0000000..c944c8a --- /dev/null +++ b/gee-web/day1-http-base/base3/gee/go.mod @@ -0,0 +1,3 @@ +module gee + +go 1.13 diff --git a/gee-web/day1-http-base/base3/go.mod b/gee-web/day1-http-base/base3/go.mod new file mode 100644 index 0000000..b27ebc4 --- /dev/null +++ b/gee-web/day1-http-base/base3/go.mod @@ -0,0 +1,7 @@ +module example + +go 1.13 + +require gee v0.0.0 + +replace gee => ./gee diff --git a/gee-web/day1-http-base/base3/main.go b/gee-web/day1-http-base/base3/main.go index 5f55dc2..5cdac75 100644 --- a/gee-web/day1-http-base/base3/main.go +++ b/gee-web/day1-http-base/base3/main.go @@ -12,7 +12,7 @@ import ( "fmt" "net/http" - "./gee" + "gee" ) func main() { diff --git a/gee-web/day2-context/gee/go.mod b/gee-web/day2-context/gee/go.mod new file mode 100644 index 0000000..c944c8a --- /dev/null +++ b/gee-web/day2-context/gee/go.mod @@ -0,0 +1,3 @@ +module gee + +go 1.13 diff --git a/gee-web/day2-context/go.mod b/gee-web/day2-context/go.mod new file mode 100644 index 0000000..b27ebc4 --- /dev/null +++ b/gee-web/day2-context/go.mod @@ -0,0 +1,7 @@ +module example + +go 1.13 + +require gee v0.0.0 + +replace gee => ./gee diff --git a/gee-web/day2-context/main.go b/gee-web/day2-context/main.go index a493d3f..6f86bee 100644 --- a/gee-web/day2-context/main.go +++ b/gee-web/day2-context/main.go @@ -25,7 +25,7 @@ $ curl "http://localhost:9999/xxx" import ( "net/http" - "./gee" + "gee" ) func main() { diff --git a/gee-web/day3-router/gee/go.mod b/gee-web/day3-router/gee/go.mod new file mode 100644 index 0000000..c944c8a --- /dev/null +++ b/gee-web/day3-router/gee/go.mod @@ -0,0 +1,3 @@ +module gee + +go 1.13 diff --git a/gee-web/day3-router/go.mod b/gee-web/day3-router/go.mod new file mode 100644 index 0000000..b27ebc4 --- /dev/null +++ b/gee-web/day3-router/go.mod @@ -0,0 +1,7 @@ +module example + +go 1.13 + +require gee v0.0.0 + +replace gee => ./gee diff --git a/gee-web/day3-router/main.go b/gee-web/day3-router/main.go index 60a7d9a..25f623e 100644 --- a/gee-web/day3-router/main.go +++ b/gee-web/day3-router/main.go @@ -29,7 +29,7 @@ $ curl "http://localhost:9999/xxx" import ( "net/http" - "./gee" + "gee" ) func main() { diff --git a/gee-web/day4-group/gee/go.mod b/gee-web/day4-group/gee/go.mod new file mode 100644 index 0000000..c944c8a --- /dev/null +++ b/gee-web/day4-group/gee/go.mod @@ -0,0 +1,3 @@ +module gee + +go 1.13 diff --git a/gee-web/day4-group/go.mod b/gee-web/day4-group/go.mod new file mode 100644 index 0000000..b27ebc4 --- /dev/null +++ b/gee-web/day4-group/go.mod @@ -0,0 +1,7 @@ +module example + +go 1.13 + +require gee v0.0.0 + +replace gee => ./gee diff --git a/gee-web/day4-group/main.go b/gee-web/day4-group/main.go index 68c2d74..4336a9c 100644 --- a/gee-web/day4-group/main.go +++ b/gee-web/day4-group/main.go @@ -37,7 +37,7 @@ $ curl "http://localhost:9999/hello" import ( "net/http" - "./gee" + "gee" ) func main() { diff --git a/gee-web/day5-middleware/gee/go.mod b/gee-web/day5-middleware/gee/go.mod new file mode 100644 index 0000000..c944c8a --- /dev/null +++ b/gee-web/day5-middleware/gee/go.mod @@ -0,0 +1,3 @@ +module gee + +go 1.13 diff --git a/gee-web/day5-middleware/go.mod b/gee-web/day5-middleware/go.mod new file mode 100644 index 0000000..b27ebc4 --- /dev/null +++ b/gee-web/day5-middleware/go.mod @@ -0,0 +1,7 @@ +module example + +go 1.13 + +require gee v0.0.0 + +replace gee => ./gee diff --git a/gee-web/day5-middleware/main.go b/gee-web/day5-middleware/main.go index 469de0d..b66dbdc 100644 --- a/gee-web/day5-middleware/main.go +++ b/gee-web/day5-middleware/main.go @@ -24,7 +24,7 @@ import ( "net/http" "time" - "./gee" + "gee" ) func onlyForV2() gee.HandlerFunc { diff --git a/gee-web/day6-template/gee/go.mod b/gee-web/day6-template/gee/go.mod new file mode 100644 index 0000000..c944c8a --- /dev/null +++ b/gee-web/day6-template/gee/go.mod @@ -0,0 +1,3 @@ +module gee + +go 1.13 diff --git a/gee-web/day6-template/go.mod b/gee-web/day6-template/go.mod new file mode 100644 index 0000000..b27ebc4 --- /dev/null +++ b/gee-web/day6-template/go.mod @@ -0,0 +1,7 @@ +module example + +go 1.13 + +require gee v0.0.0 + +replace gee => ./gee diff --git a/gee-web/day6-template/main.go b/gee-web/day6-template/main.go index 2400183..79b89bc 100644 --- a/gee-web/day6-template/main.go +++ b/gee-web/day6-template/main.go @@ -39,7 +39,7 @@ import ( "net/http" "time" - "./gee" + "gee" ) type student struct { diff --git a/gee-web/day7-panic-recover/gee/go.mod b/gee-web/day7-panic-recover/gee/go.mod new file mode 100644 index 0000000..c944c8a --- /dev/null +++ b/gee-web/day7-panic-recover/gee/go.mod @@ -0,0 +1,3 @@ +module gee + +go 1.13 diff --git a/gee-web/day7-panic-recover/go.mod b/gee-web/day7-panic-recover/go.mod new file mode 100644 index 0000000..b27ebc4 --- /dev/null +++ b/gee-web/day7-panic-recover/go.mod @@ -0,0 +1,7 @@ +module example + +go 1.13 + +require gee v0.0.0 + +replace gee => ./gee diff --git a/gee-web/day7-panic-recover/main.go b/gee-web/day7-panic-recover/main.go index a342f66..c5b309b 100644 --- a/gee-web/day7-panic-recover/main.go +++ b/gee-web/day7-panic-recover/main.go @@ -35,7 +35,7 @@ Traceback: import ( "net/http" - "./gee" + "gee" ) func main() { diff --git a/gee-web/doc/gee-day1.md b/gee-web/doc/gee-day1.md index ca00e8a..de30bc9 100644 --- a/gee-web/doc/gee-day1.md +++ b/gee-web/doc/gee-day1.md @@ -131,9 +131,29 @@ func main() { ```bash gee/ |--gee.go + |--go.mod main.go +go.mod ``` +### go.mod + +**[day1-http-base/base3/go.mod](https://github.com/geektutu/7days-golang/tree/master/gee-web/day1-http-base/base3)** + +```bash +module example + +go 1.13 + +require gee v0.0.0 + +replace gee => ./gee +``` + +- 在 `go.mod` 中使用 `replace` 将 gee 指向 `./gee` + +> 从 go 1.11 版本开始,引用相对路径的 package 需要使用上述方式。 + ### main.go **[day1-http-base/base3/main.go](https://github.com/geektutu/7days-golang/tree/master/gee-web/day1-http-base/base3)** @@ -145,7 +165,7 @@ import ( "fmt" "net/http" - "./gee" + "gee" ) func main() { diff --git a/gee-web/doc/gee-day6.md b/gee-web/doc/gee-day6.md index b77f041..a12ff30 100644 --- a/gee-web/doc/gee-day6.md +++ b/gee-web/doc/gee-day6.md @@ -107,6 +107,12 @@ func (engine *Engine) LoadHTMLGlob(pattern string) { [day6-template/gee/context.go](https://github.com/geektutu/7days-golang/tree/master/gee-web/day6-template) ```go +type Context struct { + // ... + // engine pointer + engine *Engine +} + func (c *Context) HTML(code int, name string, data interface{}) { c.Writer.WriteHeader(code) c.Writer.Header().Set("Content-Type", "text/html") @@ -116,6 +122,20 @@ func (c *Context) HTML(code int, name string, data interface{}) { } ``` +我们在 `Context` 中添加了成员变量 `engine *Engine`,这样就能够通过 Context 访问 Engine 中的 HTML 模板。实例化 Context 时,还需要给 `c.engine` 赋值。 + +[day6-template/gee/gee.go](https://github.com/geektutu/7days-golang/tree/master/gee-web/day6-template) + +```go +func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { + // ... + c := newContext(w, req) + c.handlers = middlewares + c.engine = engine + engine.router.handle(c) +} +``` + ## 使用Demo 最终的目录结构 diff --git a/gee-web/doc/gee-day7.md b/gee-web/doc/gee-day7.md index b3f6873..08310ca 100644 --- a/gee-web/doc/gee-day7.md +++ b/gee-web/doc/gee-day7.md @@ -228,7 +228,7 @@ package main import ( "net/http" - "./gee" + "gee" ) func main() { From 78b6e80ae25cb71dbd0a23a9376c4405f5d93118 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Thu, 20 Feb 2020 21:00:29 +0800 Subject: [PATCH 035/122] add zhihu zhuanlan & weibo link --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 469482f..50682a1 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,9 @@ 7天能写什么呢?类似 gin 的 web 框架?类似 groupcache 的分布式缓存?或者一个简单的 Python 解释器?希望这个仓库能给你答案。 -推荐先阅读 **[Go 语言简明教程](https://geektutu.com/post/quick-golang.html)**,一篇文章了解Go的基本语法、并发编程,依赖管理等内容 +推荐先阅读 **[Go 语言简明教程](https://geektutu.com/post/quick-golang.html)**,一篇文章了解Go的基本语法、并发编程,依赖管理等内容。 + +期待关注我的「[知乎专栏](https://zhuanlan.zhihu.com/geekgo)」和「[微博](http://weibo.com/geektutu)」,查看最近的文章和动态。 ### 7天用Go从零实现Web框架 - Gee From c5c650bee4885987182c847e8d1857168fc389f9 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Sat, 22 Feb 2020 15:10:47 +0800 Subject: [PATCH 036/122] geeorm: a gorm-like orm framework --- .gitignore | 6 +- .../geeorm/dialect/dialect.go | 19 ++++++ .../geeorm/dialect/sqlite3.go | 43 ++++++++++++ .../geeorm/dialect/sqlite3_test.go | 25 +++++++ gee-orm/day1-database-sql/geeorm/geeorm.go | 67 +++++++++++++++++++ .../day1-database-sql/geeorm/geeorm_test.go | 20 ++++++ gee-orm/day1-database-sql/geeorm/go.mod | 5 ++ .../day1-database-sql/geeorm/schema/field.go | 13 ++++ .../day1-database-sql/geeorm/schema/schema.go | 45 +++++++++++++ .../geeorm/schema/schema_test.go | 20 ++++++ gee-orm/day1-database-sql/geeorm/session.go | 48 +++++++++++++ .../day1-database-sql/geeorm/session_test.go | 29 ++++++++ 12 files changed, 339 insertions(+), 1 deletion(-) create mode 100644 gee-orm/day1-database-sql/geeorm/dialect/dialect.go create mode 100644 gee-orm/day1-database-sql/geeorm/dialect/sqlite3.go create mode 100644 gee-orm/day1-database-sql/geeorm/dialect/sqlite3_test.go create mode 100644 gee-orm/day1-database-sql/geeorm/geeorm.go create mode 100644 gee-orm/day1-database-sql/geeorm/geeorm_test.go create mode 100644 gee-orm/day1-database-sql/geeorm/go.mod create mode 100644 gee-orm/day1-database-sql/geeorm/schema/field.go create mode 100644 gee-orm/day1-database-sql/geeorm/schema/schema.go create mode 100644 gee-orm/day1-database-sql/geeorm/schema/schema_test.go create mode 100644 gee-orm/day1-database-sql/geeorm/session.go create mode 100644 gee-orm/day1-database-sql/geeorm/session_test.go diff --git a/.gitignore b/.gitignore index ae93b1b..d9335e2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ .DS_Store -tmp \ No newline at end of file +.idea +.vscode +tmp +*.db +*.sum \ No newline at end of file diff --git a/gee-orm/day1-database-sql/geeorm/dialect/dialect.go b/gee-orm/day1-database-sql/geeorm/dialect/dialect.go new file mode 100644 index 0000000..ee07db3 --- /dev/null +++ b/gee-orm/day1-database-sql/geeorm/dialect/dialect.go @@ -0,0 +1,19 @@ +package dialect + +import "reflect" + +var dialectsMap = map[string]Dialect{} + +type Dialect interface { + DataTypeOf(typ reflect.Value) string + PrimaryKeyTag(key string) string +} + +func RegisterDialect(name string, dialect Dialect) { + dialectsMap[name] = dialect +} + +func GetDialect(name string) (dialect Dialect, ok bool) { + dialect, ok = dialectsMap[name] + return +} diff --git a/gee-orm/day1-database-sql/geeorm/dialect/sqlite3.go b/gee-orm/day1-database-sql/geeorm/dialect/sqlite3.go new file mode 100644 index 0000000..8eca05a --- /dev/null +++ b/gee-orm/day1-database-sql/geeorm/dialect/sqlite3.go @@ -0,0 +1,43 @@ +package dialect + +import ( + "fmt" + "reflect" + "time" +) + +type sqlite3 struct{} + +var _ Dialect = (*sqlite3)(nil) + +func init() { + RegisterDialect("sqlite3", &sqlite3{}) +} + +// Get Data Type for Sqlite Dialect +func (s *sqlite3) DataTypeOf(typ reflect.Value) string { + switch typ.Kind() { + case reflect.Bool: + return "bool" + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uintptr: + return "integer" + case reflect.Int64, reflect.Uint64: + return "bigint" + case reflect.Float32, reflect.Float64: + return "real" + case reflect.String: + return "text" + case reflect.Array, reflect.Slice: + return "blob" + case reflect.Struct: + if _, ok := typ.Interface().(time.Time); ok { + return "datetime" + } + } + panic(fmt.Sprintf("invalid sql type %s (%s)", typ.Type().Name(), typ.Kind())) +} + +func (s *sqlite3) PrimaryKeyTag(key string) string { + return "INTEGER PRIMARY KEY AUTOINCREMENT" +} diff --git a/gee-orm/day1-database-sql/geeorm/dialect/sqlite3_test.go b/gee-orm/day1-database-sql/geeorm/dialect/sqlite3_test.go new file mode 100644 index 0000000..3df5f07 --- /dev/null +++ b/gee-orm/day1-database-sql/geeorm/dialect/sqlite3_test.go @@ -0,0 +1,25 @@ +package dialect + +import ( + "reflect" + "testing" +) + +func TestDataTypeOf(t *testing.T) { + dial := &sqlite3{} + cases := []struct { + Value interface{} + Type string + }{ + {"Tom", "text"}, + {123, "integer"}, + {1.2, "real"}, + {[]int{1, 2, 3}, "blob"}, + } + + for _, c := range cases { + if typ := dial.DataTypeOf(reflect.ValueOf(c.Value)); typ != c.Type { + t.Fatalf("expect %s, but got %s", c.Type, typ) + } + } +} diff --git a/gee-orm/day1-database-sql/geeorm/geeorm.go b/gee-orm/day1-database-sql/geeorm/geeorm.go new file mode 100644 index 0000000..a1d1f7e --- /dev/null +++ b/gee-orm/day1-database-sql/geeorm/geeorm.go @@ -0,0 +1,67 @@ +package geeorm + +import ( + "database/sql" + "fmt" + "log" + "os" + + "geeorm/dialect" + "geeorm/schema" +) + +var ( + ErrorLog = log.New(os.Stdout, "[error] ", log.LstdFlags) + InfoLog = log.New(os.Stdout, "[info ] ", log.LstdFlags) +) + +type Engine struct { + db *sql.DB + dialect dialect.Dialect +} + +func NewEngine(driver, source string) (e *Engine, err error) { + db, err := sql.Open(driver, source) + if err != nil { + ErrorLog.Println(err) + return + } + // Send a ping to make sure the database connection is alive. + if err = db.Ping(); err != nil { + ErrorLog.Println(err) + return + } + // make sure the specific dialect exists + dial, ok := dialect.GetDialect(driver) + if !ok { + err = fmt.Errorf("dialect %s Not Found", driver) + ErrorLog.Println(err) + return + } + e = &Engine{db: db, dialect: dial} + InfoLog.Println("Connect database success") + return +} + +func (e *Engine) Close() (err error) { + if err = e.db.Close(); err == nil { + InfoLog.Println("Close database success") + } + return +} + +func (e *Engine) CreateTable(value interface{}) error { + _, err := e.NewSession(value).CreateTable().Exec() + return err +} + +func (e *Engine) NewSession(value interface{}) *Session { + var refTable *schema.Schema + if value != nil { + refTable = schema.Parse(value, e.dialect) + } + return &Session{ + refTable: refTable, + engine: e, + } +} diff --git a/gee-orm/day1-database-sql/geeorm/geeorm_test.go b/gee-orm/day1-database-sql/geeorm/geeorm_test.go new file mode 100644 index 0000000..c6da191 --- /dev/null +++ b/gee-orm/day1-database-sql/geeorm/geeorm_test.go @@ -0,0 +1,20 @@ +package geeorm + +import ( + _ "github.com/mattn/go-sqlite3" + "testing" +) + +func OpenDB(t *testing.T) *Engine { + t.Helper() + engine, err := NewEngine("sqlite3", "gee.db") + if err != nil { + t.Fatal("failed to connect", err) + } + return engine +} + +func TestNewEngine(t *testing.T) { + engine := OpenDB(t) + defer engine.Close() +} diff --git a/gee-orm/day1-database-sql/geeorm/go.mod b/gee-orm/day1-database-sql/geeorm/go.mod new file mode 100644 index 0000000..043b1c6 --- /dev/null +++ b/gee-orm/day1-database-sql/geeorm/go.mod @@ -0,0 +1,5 @@ +module geeorm + +go 1.13 + +require github.com/mattn/go-sqlite3 v2.0.3+incompatible diff --git a/gee-orm/day1-database-sql/geeorm/schema/field.go b/gee-orm/day1-database-sql/geeorm/schema/field.go new file mode 100644 index 0000000..46e82e4 --- /dev/null +++ b/gee-orm/day1-database-sql/geeorm/schema/field.go @@ -0,0 +1,13 @@ +package schema + +import "fmt" + +type Field struct { + Name string + Value interface{} + Tag string +} + +func (f *Field) String() string { + return fmt.Sprintf("%s %s", f.Name, f.Tag) +} diff --git a/gee-orm/day1-database-sql/geeorm/schema/schema.go b/gee-orm/day1-database-sql/geeorm/schema/schema.go new file mode 100644 index 0000000..ae8ea7b --- /dev/null +++ b/gee-orm/day1-database-sql/geeorm/schema/schema.go @@ -0,0 +1,45 @@ +package schema + +import ( + "fmt" + "go/ast" + "reflect" + "strings" + + "geeorm/dialect" +) + +type Schema struct { + Table string + PrimaryField *Field + Fields []*Field +} + +func Parse(dest interface{}, d dialect.Dialect) *Schema { + modelType := reflect.Indirect(reflect.ValueOf(dest)).Type() + + schema := &Schema{ + Table: modelType.Name(), + PrimaryField: &Field{Name: "ID", Value: 0}, + } + + for i := 0; i < modelType.NumField(); i++ { + p := modelType.Field(i) + if !p.Anonymous && ast.IsExported(p.Name) { + schema.Fields = append(schema.Fields, &Field{ + Name: p.Name, + Tag: d.DataTypeOf(reflect.Indirect(reflect.New(p.Type))), + }) + } + } + return schema +} + +func (s *Schema) String() string { + var fieldStr []string + for _, field := range s.Fields { + fieldStr = append(fieldStr, field.String()) + } + + return fmt.Sprintf("TABLE %s(%s)", s.Table, strings.Join(fieldStr, ", ")) +} diff --git a/gee-orm/day1-database-sql/geeorm/schema/schema_test.go b/gee-orm/day1-database-sql/geeorm/schema/schema_test.go new file mode 100644 index 0000000..5e1ad2d --- /dev/null +++ b/gee-orm/day1-database-sql/geeorm/schema/schema_test.go @@ -0,0 +1,20 @@ +package schema + +import ( + "geeorm/dialect" + "testing" +) + +type User struct { + Name string + Age int +} + +func TestParse(t *testing.T) { + dial, _ := dialect.GetDialect("sqlite3") + schema := Parse(&User{"Tom", 18}, dial) + + if schema.Table != "User" || len(schema.Fields) != 2 { + t.Fatal("failed to parse User struct") + } +} diff --git a/gee-orm/day1-database-sql/geeorm/session.go b/gee-orm/day1-database-sql/geeorm/session.go new file mode 100644 index 0000000..f1cd36a --- /dev/null +++ b/gee-orm/day1-database-sql/geeorm/session.go @@ -0,0 +1,48 @@ +package geeorm + +import ( + "database/sql" + "fmt" + "strings" + + "geeorm/schema" +) + +type Session struct { + engine *Engine + refTable *schema.Schema + + Value interface{} + SQL strings.Builder + SQLVars []interface{} +} + +func (s *Session) Exec() (result sql.Result, err error) { + if result, err = s.engine.db.Exec(s.SQL.String(), s.SQLVars...); err != nil { + ErrorLog.Println(err) + } + return +} + +func (s *Session) QueryRows() (rows *sql.Rows, err error) { + if rows, err = s.engine.db.Query(s.SQL.String(), s.SQLVars...); err != nil { + ErrorLog.Println(err) + } + return +} + +func (s *Session) Raw(sql string, values ...interface{}) *Session { + s.SQL.WriteString(sql) + s.SQLVars = values + return s +} + +func (s *Session) CreateTable() *Session { + var columns []string + for _, field := range s.refTable.Fields { + columns = append(columns, fmt.Sprintf("%s %s", field.Name, field.Tag)) + } + desc := strings.Join(columns, ",") + s.SQL.WriteString(fmt.Sprintf("CREATE TABLE %s (%s);", s.refTable.Table, desc)) + return s +} diff --git a/gee-orm/day1-database-sql/geeorm/session_test.go b/gee-orm/day1-database-sql/geeorm/session_test.go new file mode 100644 index 0000000..ea30d7c --- /dev/null +++ b/gee-orm/day1-database-sql/geeorm/session_test.go @@ -0,0 +1,29 @@ +package geeorm + +import "testing" + +func TestExec(t *testing.T) { + engine := OpenDB(t) + defer engine.Close() + engine.NewSession(nil).Raw("DROP TABLE USER;").Exec() + engine.NewSession(nil).Raw("CREATE TABLE USER(name text);").Exec() + result, _ := engine.NewSession(nil).Raw("INSERT INTO USER(`name`) values (?), (?)", "Tom", "Sam").Exec() + if count, err := result.RowsAffected(); err != nil || count != 2 { + t.Fatal("expect 2, but got", count) + } +} + +func TestQuery(t *testing.T) { + engine := OpenDB(t) + defer engine.Close() + engine.NewSession(nil).Raw("DROP TABLE USER;").Exec() + engine.NewSession(nil).Raw("CREATE TABLE USER(name text);").Exec() + rows, _ := engine.NewSession(nil).Raw("SELECT count(*) FROM USER").QueryRows() + defer rows.Close() + var count int + for rows.Next() { + if err := rows.Scan(&count); err != nil || count != 0 { + t.Fatal("failed to query db", err) + } + } +} From 8db04ddad961073b7995ca0dd14be1323caa2467 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Sun, 23 Feb 2020 00:10:20 +0800 Subject: [PATCH 037/122] add day2 schema design --- README.md | 14 ++++- gee-orm/day1-database-sql/geeorm/geeorm.go | 40 +++----------- .../day1-database-sql/geeorm/session_raw.go | 40 ++++++++++++++ .../geeorm/session_raw_test.go | 30 +++++++++++ .../day1-database-sql/geeorm/session_test.go | 29 ---------- .../geeorm/dialect/dialect.go | 1 + .../geeorm/dialect/sqlite3.go | 7 ++- .../geeorm/dialect/sqlite3_test.go | 0 gee-orm/day2-reflect-schema/geeorm/geeorm.go | 54 +++++++++++++++++++ .../day2-reflect-schema/geeorm/geeorm_test.go | 20 +++++++ gee-orm/day2-reflect-schema/geeorm/go.mod | 5 ++ .../geeorm/schema/field.go | 0 .../geeorm/schema/schema.go | 6 +-- .../geeorm/schema/schema_test.go | 2 +- .../geeorm/session_raw.go} | 18 +++---- .../geeorm/session_raw_test.go | 32 +++++++++++ .../geeorm/session_schema.go | 53 ++++++++++++++++++ .../geeorm/session_schema_test.go | 18 +++++++ 18 files changed, 291 insertions(+), 78 deletions(-) create mode 100644 gee-orm/day1-database-sql/geeorm/session_raw.go create mode 100644 gee-orm/day1-database-sql/geeorm/session_raw_test.go delete mode 100644 gee-orm/day1-database-sql/geeorm/session_test.go rename gee-orm/{day1-database-sql => day2-reflect-schema}/geeorm/dialect/dialect.go (86%) rename gee-orm/{day1-database-sql => day2-reflect-schema}/geeorm/dialect/sqlite3.go (80%) rename gee-orm/{day1-database-sql => day2-reflect-schema}/geeorm/dialect/sqlite3_test.go (100%) create mode 100644 gee-orm/day2-reflect-schema/geeorm/geeorm.go create mode 100644 gee-orm/day2-reflect-schema/geeorm/geeorm_test.go create mode 100644 gee-orm/day2-reflect-schema/geeorm/go.mod rename gee-orm/{day1-database-sql => day2-reflect-schema}/geeorm/schema/field.go (100%) rename gee-orm/{day1-database-sql => day2-reflect-schema}/geeorm/schema/schema.go (85%) rename gee-orm/{day1-database-sql => day2-reflect-schema}/geeorm/schema/schema_test.go (81%) rename gee-orm/{day1-database-sql/geeorm/session.go => day2-reflect-schema/geeorm/session_raw.go} (68%) create mode 100644 gee-orm/day2-reflect-schema/geeorm/session_raw_test.go create mode 100755 gee-orm/day2-reflect-schema/geeorm/session_schema.go create mode 100755 gee-orm/day2-reflect-schema/geeorm/session_schema_test.go diff --git a/README.md b/README.md index 50682a1..aee281f 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,12 @@ - 第六天:[防止缓存击穿](https://geektutu.com/post/geecache-day6.html) | [Code](gee-cache/day6-single-flight) - 第七天:[使用 Protobuf 通信](https://geektutu.com/post/geecache-day7.html) | [Code](gee-cache/day7-proto-buf) +### 7天用Go从零实现ORM框架 GeeORM + +[GeeORM] 是一个模仿 [gorm](https://github.com/jinzhu/gorm) 和 [xorm](https://github.com/go-xorm/xorm) 的 ORM 框架 + +gorm 准备推出完全重写的 v2 版本(目前还在开发中),相对 gorm-v1 来说,xorm 的设计更容易理解,所以 geeorm 接口设计上主要参考了 xorm,具体实现参考了 gorm。 + ### WebAssembly 使用示例 具体的实践过程记录在 [Go WebAssembly 简明教程](https://geektutu.com/post/quick-go-wasm.html)。 @@ -62,7 +68,7 @@ What can I write in 7 days? A gin-like web framework? A distributed cache like g - Day 6 - Embeded Template Support [Code](gee-web/day6-template) - Day 7 - Panic Recover & Make it Robust [Code](gee-web/day7-panic-recover) -## Distributed Cache - Geecache +## Distributed Cache - GeeCache [GeeCache](https://geektutu.com/post/geecache.html) is a [groupcache](https://github.com/golang/groupcache)-like distributed cache @@ -74,6 +80,12 @@ What can I write in 7 days? A gin-like web framework? A distributed cache like g - Day 6 - Cache Breakdown & Single Flight | [Code](gee-cache/day6-single-flight) - Day 7 - Use Protobuf as RPC Data Exchange Type | [Code](gee-cache/day7-proto-buf) +## Object Relational Mapping - GeeOrm + +[GeeOrm] is a [gorm](https://github.com/jinzhu/gorm)-like and [xorm](https://github.com/go-xorm/xorm)-like object relational mapping library + +Xorm's desgin is easier to understand than gorm-v1, so the main designs references xorm and some detailed implementions references gorm-v1. + ## Golang WebAssembly Demo - Demo 1 - Hello World [Code](demo-wasm/hello-world) diff --git a/gee-orm/day1-database-sql/geeorm/geeorm.go b/gee-orm/day1-database-sql/geeorm/geeorm.go index a1d1f7e..b978bf0 100644 --- a/gee-orm/day1-database-sql/geeorm/geeorm.go +++ b/gee-orm/day1-database-sql/geeorm/geeorm.go @@ -2,22 +2,17 @@ package geeorm import ( "database/sql" - "fmt" "log" "os" - - "geeorm/dialect" - "geeorm/schema" ) var ( - ErrorLog = log.New(os.Stdout, "[error] ", log.LstdFlags) - InfoLog = log.New(os.Stdout, "[info ] ", log.LstdFlags) + ErrorLog = log.New(os.Stdout, "[error] ", log.LstdFlags|log.Lshortfile) + InfoLog = log.New(os.Stdout, "[info ] ", log.LstdFlags|log.Lshortfile) ) type Engine struct { - db *sql.DB - dialect dialect.Dialect + db *sql.DB } func NewEngine(driver, source string) (e *Engine, err error) { @@ -31,37 +26,18 @@ func NewEngine(driver, source string) (e *Engine, err error) { ErrorLog.Println(err) return } - // make sure the specific dialect exists - dial, ok := dialect.GetDialect(driver) - if !ok { - err = fmt.Errorf("dialect %s Not Found", driver) - ErrorLog.Println(err) - return - } - e = &Engine{db: db, dialect: dial} + e = &Engine{db: db} InfoLog.Println("Connect database success") return } -func (e *Engine) Close() (err error) { - if err = e.db.Close(); err == nil { +func (engine *Engine) Close() (err error) { + if err = engine.db.Close(); err == nil { InfoLog.Println("Close database success") } return } -func (e *Engine) CreateTable(value interface{}) error { - _, err := e.NewSession(value).CreateTable().Exec() - return err -} - -func (e *Engine) NewSession(value interface{}) *Session { - var refTable *schema.Schema - if value != nil { - refTable = schema.Parse(value, e.dialect) - } - return &Session{ - refTable: refTable, - engine: e, - } +func (engine *Engine) NewSession() *Session { + return &Session{engine: engine} } diff --git a/gee-orm/day1-database-sql/geeorm/session_raw.go b/gee-orm/day1-database-sql/geeorm/session_raw.go new file mode 100644 index 0000000..c73f1c8 --- /dev/null +++ b/gee-orm/day1-database-sql/geeorm/session_raw.go @@ -0,0 +1,40 @@ +package geeorm + +import ( + "database/sql" + "strings" +) + +type Session struct { + engine *Engine + + SQL strings.Builder + SQLVars []interface{} +} + +func (s *Session) Exec() (result sql.Result, err error) { + InfoLog.Println(s.SQL, s.SQLVars) + if result, err = s.engine.db.Exec(s.SQL.String(), s.SQLVars...); err != nil { + ErrorLog.Println(err) + } + return +} + +func (s *Session) QueryRow() *sql.Row { + InfoLog.Println(s.SQL, s.SQLVars) + return s.engine.db.QueryRow(s.SQL.String(), s.SQLVars...) +} + +func (s *Session) QueryRows() (rows *sql.Rows, err error) { + InfoLog.Println(s.SQL, s.SQLVars) + if rows, err = s.engine.db.Query(s.SQL.String(), s.SQLVars...); err != nil { + ErrorLog.Println(err) + } + return +} + +func (s *Session) Raw(sql string, values ...interface{}) *Session { + s.SQL.WriteString(sql) + s.SQLVars = values + return s +} diff --git a/gee-orm/day1-database-sql/geeorm/session_raw_test.go b/gee-orm/day1-database-sql/geeorm/session_raw_test.go new file mode 100644 index 0000000..8b879ea --- /dev/null +++ b/gee-orm/day1-database-sql/geeorm/session_raw_test.go @@ -0,0 +1,30 @@ +package geeorm + +import ( + "testing" +) + +func TestSession_Exec(t *testing.T) { + engine := OpenDB(t) + defer engine.Close() + _, _ = engine.NewSession().Raw("DROP TABLE USER;").Exec() + _, _ = engine.NewSession().Raw("CREATE TABLE USER(name text);").Exec() + result, _ := engine.NewSession(). + Raw("INSERT INTO USER(`name`) values (?), (?)", "Tom", "Sam").Exec() + if count, err := result.RowsAffected(); err != nil || count != 2 { + t.Fatal("expect 2, but got", count) + } +} + +func TestSession_QueryRow(t *testing.T) { + engine := OpenDB(t) + defer engine.Close() + _, _ = engine.NewSession().Raw("DROP TABLE USER;").Exec() + _, _ = engine.NewSession().Raw("CREATE TABLE USER(name text);").Exec() + row := engine.NewSession().Raw("SELECT count(*) FROM USER").QueryRow() + + var count int + if err := row.Scan(&count); err != nil || count != 0 { + t.Fatal("failed to query db", err) + } +} diff --git a/gee-orm/day1-database-sql/geeorm/session_test.go b/gee-orm/day1-database-sql/geeorm/session_test.go deleted file mode 100644 index ea30d7c..0000000 --- a/gee-orm/day1-database-sql/geeorm/session_test.go +++ /dev/null @@ -1,29 +0,0 @@ -package geeorm - -import "testing" - -func TestExec(t *testing.T) { - engine := OpenDB(t) - defer engine.Close() - engine.NewSession(nil).Raw("DROP TABLE USER;").Exec() - engine.NewSession(nil).Raw("CREATE TABLE USER(name text);").Exec() - result, _ := engine.NewSession(nil).Raw("INSERT INTO USER(`name`) values (?), (?)", "Tom", "Sam").Exec() - if count, err := result.RowsAffected(); err != nil || count != 2 { - t.Fatal("expect 2, but got", count) - } -} - -func TestQuery(t *testing.T) { - engine := OpenDB(t) - defer engine.Close() - engine.NewSession(nil).Raw("DROP TABLE USER;").Exec() - engine.NewSession(nil).Raw("CREATE TABLE USER(name text);").Exec() - rows, _ := engine.NewSession(nil).Raw("SELECT count(*) FROM USER").QueryRows() - defer rows.Close() - var count int - for rows.Next() { - if err := rows.Scan(&count); err != nil || count != 0 { - t.Fatal("failed to query db", err) - } - } -} diff --git a/gee-orm/day1-database-sql/geeorm/dialect/dialect.go b/gee-orm/day2-reflect-schema/geeorm/dialect/dialect.go similarity index 86% rename from gee-orm/day1-database-sql/geeorm/dialect/dialect.go rename to gee-orm/day2-reflect-schema/geeorm/dialect/dialect.go index ee07db3..63c4e69 100644 --- a/gee-orm/day1-database-sql/geeorm/dialect/dialect.go +++ b/gee-orm/day2-reflect-schema/geeorm/dialect/dialect.go @@ -7,6 +7,7 @@ var dialectsMap = map[string]Dialect{} type Dialect interface { DataTypeOf(typ reflect.Value) string PrimaryKeyTag(key string) string + TableExistSQL(tableName string) (string, []interface{}) } func RegisterDialect(name string, dialect Dialect) { diff --git a/gee-orm/day1-database-sql/geeorm/dialect/sqlite3.go b/gee-orm/day2-reflect-schema/geeorm/dialect/sqlite3.go similarity index 80% rename from gee-orm/day1-database-sql/geeorm/dialect/sqlite3.go rename to gee-orm/day2-reflect-schema/geeorm/dialect/sqlite3.go index 8eca05a..3a3a444 100644 --- a/gee-orm/day1-database-sql/geeorm/dialect/sqlite3.go +++ b/gee-orm/day2-reflect-schema/geeorm/dialect/sqlite3.go @@ -14,7 +14,7 @@ func init() { RegisterDialect("sqlite3", &sqlite3{}) } -// Get Data Type for Sqlite Dialect +// Get Data Type for sqlite3 Dialect func (s *sqlite3) DataTypeOf(typ reflect.Value) string { switch typ.Kind() { case reflect.Bool: @@ -41,3 +41,8 @@ func (s *sqlite3) DataTypeOf(typ reflect.Value) string { func (s *sqlite3) PrimaryKeyTag(key string) string { return "INTEGER PRIMARY KEY AUTOINCREMENT" } + +func (s *sqlite3) TableExistSQL(tableName string) (string, []interface{}) { + args := []interface{}{tableName} + return "SELECT name FROM sqlite_master WHERE type='table' and name = ?", args +} diff --git a/gee-orm/day1-database-sql/geeorm/dialect/sqlite3_test.go b/gee-orm/day2-reflect-schema/geeorm/dialect/sqlite3_test.go similarity index 100% rename from gee-orm/day1-database-sql/geeorm/dialect/sqlite3_test.go rename to gee-orm/day2-reflect-schema/geeorm/dialect/sqlite3_test.go diff --git a/gee-orm/day2-reflect-schema/geeorm/geeorm.go b/gee-orm/day2-reflect-schema/geeorm/geeorm.go new file mode 100644 index 0000000..0121bfe --- /dev/null +++ b/gee-orm/day2-reflect-schema/geeorm/geeorm.go @@ -0,0 +1,54 @@ +package geeorm + +import ( + "database/sql" + "fmt" + "log" + "os" + + "geeorm/dialect" +) + +var ( + ErrorLog = log.New(os.Stdout, "[error] ", log.LstdFlags|log.Lshortfile) + InfoLog = log.New(os.Stdout, "[info ] ", log.LstdFlags|log.Lshortfile) +) + +type Engine struct { + db *sql.DB + dialect dialect.Dialect +} + +func NewEngine(driver, source string) (e *Engine, err error) { + db, err := sql.Open(driver, source) + if err != nil { + ErrorLog.Println(err) + return + } + // Send a ping to make sure the database connection is alive. + if err = db.Ping(); err != nil { + ErrorLog.Println(err) + return + } + // make sure the specific dialect exists + dial, ok := dialect.GetDialect(driver) + if !ok { + err = fmt.Errorf("dialect %s Not Found", driver) + ErrorLog.Println(err) + return + } + e = &Engine{db: db, dialect: dial} + InfoLog.Println("Connect database success") + return +} + +func (e *Engine) Close() (err error) { + if err = e.db.Close(); err == nil { + InfoLog.Println("Close database success") + } + return +} + +func (e *Engine) NewSession() *Session { + return &Session{engine: e} +} diff --git a/gee-orm/day2-reflect-schema/geeorm/geeorm_test.go b/gee-orm/day2-reflect-schema/geeorm/geeorm_test.go new file mode 100644 index 0000000..c6da191 --- /dev/null +++ b/gee-orm/day2-reflect-schema/geeorm/geeorm_test.go @@ -0,0 +1,20 @@ +package geeorm + +import ( + _ "github.com/mattn/go-sqlite3" + "testing" +) + +func OpenDB(t *testing.T) *Engine { + t.Helper() + engine, err := NewEngine("sqlite3", "gee.db") + if err != nil { + t.Fatal("failed to connect", err) + } + return engine +} + +func TestNewEngine(t *testing.T) { + engine := OpenDB(t) + defer engine.Close() +} diff --git a/gee-orm/day2-reflect-schema/geeorm/go.mod b/gee-orm/day2-reflect-schema/geeorm/go.mod new file mode 100644 index 0000000..043b1c6 --- /dev/null +++ b/gee-orm/day2-reflect-schema/geeorm/go.mod @@ -0,0 +1,5 @@ +module geeorm + +go 1.13 + +require github.com/mattn/go-sqlite3 v2.0.3+incompatible diff --git a/gee-orm/day1-database-sql/geeorm/schema/field.go b/gee-orm/day2-reflect-schema/geeorm/schema/field.go similarity index 100% rename from gee-orm/day1-database-sql/geeorm/schema/field.go rename to gee-orm/day2-reflect-schema/geeorm/schema/field.go diff --git a/gee-orm/day1-database-sql/geeorm/schema/schema.go b/gee-orm/day2-reflect-schema/geeorm/schema/schema.go similarity index 85% rename from gee-orm/day1-database-sql/geeorm/schema/schema.go rename to gee-orm/day2-reflect-schema/geeorm/schema/schema.go index ae8ea7b..03fcdce 100644 --- a/gee-orm/day1-database-sql/geeorm/schema/schema.go +++ b/gee-orm/day2-reflect-schema/geeorm/schema/schema.go @@ -10,7 +10,7 @@ import ( ) type Schema struct { - Table string + TableName string PrimaryField *Field Fields []*Field } @@ -19,7 +19,7 @@ func Parse(dest interface{}, d dialect.Dialect) *Schema { modelType := reflect.Indirect(reflect.ValueOf(dest)).Type() schema := &Schema{ - Table: modelType.Name(), + TableName: modelType.Name(), PrimaryField: &Field{Name: "ID", Value: 0}, } @@ -41,5 +41,5 @@ func (s *Schema) String() string { fieldStr = append(fieldStr, field.String()) } - return fmt.Sprintf("TABLE %s(%s)", s.Table, strings.Join(fieldStr, ", ")) + return fmt.Sprintf("TABLE %s(%s)", s.TableName, strings.Join(fieldStr, ", ")) } diff --git a/gee-orm/day1-database-sql/geeorm/schema/schema_test.go b/gee-orm/day2-reflect-schema/geeorm/schema/schema_test.go similarity index 81% rename from gee-orm/day1-database-sql/geeorm/schema/schema_test.go rename to gee-orm/day2-reflect-schema/geeorm/schema/schema_test.go index 5e1ad2d..4142b95 100644 --- a/gee-orm/day1-database-sql/geeorm/schema/schema_test.go +++ b/gee-orm/day2-reflect-schema/geeorm/schema/schema_test.go @@ -14,7 +14,7 @@ func TestParse(t *testing.T) { dial, _ := dialect.GetDialect("sqlite3") schema := Parse(&User{"Tom", 18}, dial) - if schema.Table != "User" || len(schema.Fields) != 2 { + if schema.TableName != "User" || len(schema.Fields) != 2 { t.Fatal("failed to parse User struct") } } diff --git a/gee-orm/day1-database-sql/geeorm/session.go b/gee-orm/day2-reflect-schema/geeorm/session_raw.go similarity index 68% rename from gee-orm/day1-database-sql/geeorm/session.go rename to gee-orm/day2-reflect-schema/geeorm/session_raw.go index f1cd36a..48b8309 100644 --- a/gee-orm/day1-database-sql/geeorm/session.go +++ b/gee-orm/day2-reflect-schema/geeorm/session_raw.go @@ -2,7 +2,6 @@ package geeorm import ( "database/sql" - "fmt" "strings" "geeorm/schema" @@ -18,13 +17,20 @@ type Session struct { } func (s *Session) Exec() (result sql.Result, err error) { + InfoLog.Println(s.SQL.String(), s.SQLVars) if result, err = s.engine.db.Exec(s.SQL.String(), s.SQLVars...); err != nil { ErrorLog.Println(err) } return } +func (s *Session) QueryRow() *sql.Row { + InfoLog.Println(s.SQL.String(), s.SQLVars) + return s.engine.db.QueryRow(s.SQL.String(), s.SQLVars...) +} + func (s *Session) QueryRows() (rows *sql.Rows, err error) { + InfoLog.Println(s.SQL.String(), s.SQLVars) if rows, err = s.engine.db.Query(s.SQL.String(), s.SQLVars...); err != nil { ErrorLog.Println(err) } @@ -36,13 +42,3 @@ func (s *Session) Raw(sql string, values ...interface{}) *Session { s.SQLVars = values return s } - -func (s *Session) CreateTable() *Session { - var columns []string - for _, field := range s.refTable.Fields { - columns = append(columns, fmt.Sprintf("%s %s", field.Name, field.Tag)) - } - desc := strings.Join(columns, ",") - s.SQL.WriteString(fmt.Sprintf("CREATE TABLE %s (%s);", s.refTable.Table, desc)) - return s -} diff --git a/gee-orm/day2-reflect-schema/geeorm/session_raw_test.go b/gee-orm/day2-reflect-schema/geeorm/session_raw_test.go new file mode 100644 index 0000000..c541a37 --- /dev/null +++ b/gee-orm/day2-reflect-schema/geeorm/session_raw_test.go @@ -0,0 +1,32 @@ +package geeorm + +import ( + "testing" +) + +func TestSession_Exec(t *testing.T) { + engine := OpenDB(t) + defer engine.Close() + _, _ = engine.NewSession().Raw("DROP TABLE USER;").Exec() + _, _ = engine.NewSession().Raw("CREATE TABLE USER(name text);").Exec() + result, _ := engine.NewSession(). + Raw("INSERT INTO USER(`name`) values (?), (?)", "Tom", "Sam").Exec() + if count, err := result.RowsAffected(); err != nil || count != 2 { + t.Fatal("expect 2, but got", count) + } +} + +func TestSession_QueryRows(t *testing.T) { + engine := OpenDB(t) + defer engine.Close() + _, _ = engine.NewSession().Raw("DROP TABLE USER;").Exec() + _, _ = engine.NewSession().Raw("CREATE TABLE USER(name text);").Exec() + rows, _ := engine.NewSession().Raw("SELECT count(*) FROM USER").QueryRows() + defer rows.Close() + var count int + for rows.Next() { + if err := rows.Scan(&count); err != nil || count != 0 { + t.Fatal("failed to query db", err) + } + } +} diff --git a/gee-orm/day2-reflect-schema/geeorm/session_schema.go b/gee-orm/day2-reflect-schema/geeorm/session_schema.go new file mode 100755 index 0000000..38a0bdc --- /dev/null +++ b/gee-orm/day2-reflect-schema/geeorm/session_schema.go @@ -0,0 +1,53 @@ +package geeorm + +import ( + "fmt" + "strings" + + "geeorm/schema" +) + +func (s *Session) RefTable(value interface{}) *schema.Schema { + if value == nil { + panic("value is nil") + } + if s.refTable == nil { + s.refTable = schema.Parse(value, s.engine.dialect) + } + return s.refTable +} + +func (s *Session) CreateTable(value interface{}) error { + table := s.RefTable(value) + var columns []string + for _, field := range table.Fields { + columns = append(columns, fmt.Sprintf("%s %s", field.Name, field.Tag)) + } + desc := strings.Join(columns, ",") + _, err := s.Raw(fmt.Sprintf("CREATE TABLE %s (%s);", table.TableName, desc)).Exec() + return err +} + +func (s *Session) DropTable(value interface{}) error { + table := s.RefTable(value) + _, err := s.Raw(fmt.Sprintf("DROP TABLE %s", table.TableName)).Exec() + return err +} + +func (s *Session) HasTable(value interface{}) bool { + tableName, ok := value.(string) + if !ok { + tableName = s.RefTable(value).TableName + } + + sql, values := s.engine.dialect.TableExistSQL(tableName) + row := s.Raw(sql, values...).QueryRow() + + + + var tmp string + if err := row.Scan(&tmp); err != nil { + ErrorLog.Println(err) + } + return tmp == tableName +} diff --git a/gee-orm/day2-reflect-schema/geeorm/session_schema_test.go b/gee-orm/day2-reflect-schema/geeorm/session_schema_test.go new file mode 100755 index 0000000..4c0ba43 --- /dev/null +++ b/gee-orm/day2-reflect-schema/geeorm/session_schema_test.go @@ -0,0 +1,18 @@ +package geeorm + +import "testing" + +type User struct { + Name string + Age int +} + +func TestSession_CreateTable(t *testing.T) { + engine := OpenDB(t) + defer engine.Close() + _ = engine.NewSession().DropTable(&User{}) + _ = engine.NewSession().CreateTable(&User{}) + if ! engine.NewSession().HasTable("User") { + t.Fatal("failed to create table User") + } +} From ba6512370d6141c39ee617321d30ab111893e5ba Mon Sep 17 00:00:00 2001 From: Dai Jie Date: Sun, 23 Feb 2020 21:56:26 +0800 Subject: [PATCH 038/122] Create LICENSE --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5763570 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Dai Jie + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From d2c408982d6d0894566c49d17247e856bf302709 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Sun, 23 Feb 2020 16:55:02 +0800 Subject: [PATCH 039/122] mv session to single directory - day2 reflect schema from struct - day3 query & save --- gee-orm/day1-database-sql/geeorm/geeorm.go | 25 +++---- .../day1-database-sql/geeorm/geeorm_test.go | 2 +- gee-orm/day1-database-sql/geeorm/log/log.go | 13 ++++ .../day1-database-sql/geeorm/session/raw.go | 53 ++++++++++++++ .../geeorm/session/raw_test.go | 50 ++++++++++++++ .../day1-database-sql/geeorm/session_raw.go | 40 ----------- .../geeorm/session_raw_test.go | 30 -------- .../geeorm/dialect/dialect.go | 4 +- .../geeorm/dialect/sqlite3.go | 5 +- gee-orm/day2-reflect-schema/geeorm/geeorm.go | 28 ++++---- .../day2-reflect-schema/geeorm/geeorm_test.go | 2 +- gee-orm/day2-reflect-schema/geeorm/log/log.go | 13 ++++ .../geeorm/schema/field.go | 13 ---- .../geeorm/schema/schema.go | 50 ++++++++++---- .../geeorm/schema/schema_test.go | 23 +++++-- .../day2-reflect-schema/geeorm/session/raw.go | 61 ++++++++++++++++ .../geeorm/session/raw_test.go | 54 +++++++++++++++ .../geeorm/session/schema.go | 59 ++++++++++++++++ .../geeorm/session/schema_test.go | 18 +++++ .../day2-reflect-schema/geeorm/session_raw.go | 44 ------------ .../geeorm/session_raw_test.go | 32 --------- .../geeorm/session_schema_test.go | 18 ----- .../day3-save-query/geeorm/dialect/dialect.go | 22 ++++++ .../day3-save-query/geeorm/dialect/sqlite3.go | 45 ++++++++++++ .../geeorm/dialect/sqlite3_test.go | 25 +++++++ gee-orm/day3-save-query/geeorm/geeorm.go | 54 +++++++++++++++ gee-orm/day3-save-query/geeorm/geeorm_test.go | 20 ++++++ gee-orm/day3-save-query/geeorm/go.mod | 5 ++ gee-orm/day3-save-query/geeorm/log/log.go | 13 ++++ .../day3-save-query/geeorm/schema/schema.go | 69 +++++++++++++++++++ .../geeorm/schema/schema_test.go | 36 ++++++++++ gee-orm/day3-save-query/geeorm/session/raw.go | 61 ++++++++++++++++ .../geeorm/session/raw_test.go | 54 +++++++++++++++ .../day3-save-query/geeorm/session/record.go | 43 ++++++++++++ .../geeorm/session/record_test.go | 16 +++++ .../geeorm/session/schema.go} | 22 +++--- .../geeorm/session/schema_test.go | 18 +++++ 37 files changed, 905 insertions(+), 235 deletions(-) create mode 100644 gee-orm/day1-database-sql/geeorm/log/log.go create mode 100644 gee-orm/day1-database-sql/geeorm/session/raw.go create mode 100644 gee-orm/day1-database-sql/geeorm/session/raw_test.go delete mode 100644 gee-orm/day1-database-sql/geeorm/session_raw.go delete mode 100644 gee-orm/day1-database-sql/geeorm/session_raw_test.go create mode 100644 gee-orm/day2-reflect-schema/geeorm/log/log.go delete mode 100644 gee-orm/day2-reflect-schema/geeorm/schema/field.go create mode 100644 gee-orm/day2-reflect-schema/geeorm/session/raw.go create mode 100644 gee-orm/day2-reflect-schema/geeorm/session/raw_test.go create mode 100644 gee-orm/day2-reflect-schema/geeorm/session/schema.go create mode 100644 gee-orm/day2-reflect-schema/geeorm/session/schema_test.go delete mode 100644 gee-orm/day2-reflect-schema/geeorm/session_raw.go delete mode 100644 gee-orm/day2-reflect-schema/geeorm/session_raw_test.go delete mode 100755 gee-orm/day2-reflect-schema/geeorm/session_schema_test.go create mode 100644 gee-orm/day3-save-query/geeorm/dialect/dialect.go create mode 100644 gee-orm/day3-save-query/geeorm/dialect/sqlite3.go create mode 100644 gee-orm/day3-save-query/geeorm/dialect/sqlite3_test.go create mode 100644 gee-orm/day3-save-query/geeorm/geeorm.go create mode 100644 gee-orm/day3-save-query/geeorm/geeorm_test.go create mode 100644 gee-orm/day3-save-query/geeorm/go.mod create mode 100644 gee-orm/day3-save-query/geeorm/log/log.go create mode 100644 gee-orm/day3-save-query/geeorm/schema/schema.go create mode 100644 gee-orm/day3-save-query/geeorm/schema/schema_test.go create mode 100644 gee-orm/day3-save-query/geeorm/session/raw.go create mode 100644 gee-orm/day3-save-query/geeorm/session/raw_test.go create mode 100644 gee-orm/day3-save-query/geeorm/session/record.go create mode 100644 gee-orm/day3-save-query/geeorm/session/record_test.go rename gee-orm/{day2-reflect-schema/geeorm/session_schema.go => day3-save-query/geeorm/session/schema.go} (63%) mode change 100755 => 100644 create mode 100644 gee-orm/day3-save-query/geeorm/session/schema_test.go diff --git a/gee-orm/day1-database-sql/geeorm/geeorm.go b/gee-orm/day1-database-sql/geeorm/geeorm.go index b978bf0..10de8b9 100644 --- a/gee-orm/day1-database-sql/geeorm/geeorm.go +++ b/gee-orm/day1-database-sql/geeorm/geeorm.go @@ -2,42 +2,43 @@ package geeorm import ( "database/sql" - "log" - "os" -) -var ( - ErrorLog = log.New(os.Stdout, "[error] ", log.LstdFlags|log.Lshortfile) - InfoLog = log.New(os.Stdout, "[info ] ", log.LstdFlags|log.Lshortfile) + "geeorm/log" + "geeorm/session" ) +// Engine is the main struct of geeorm, manages all db sessions and transactions. type Engine struct { db *sql.DB } +// NewEngine create a instance of Engine +// connect database and ping it to test whether it's alive func NewEngine(driver, source string) (e *Engine, err error) { db, err := sql.Open(driver, source) if err != nil { - ErrorLog.Println(err) + log.Error.Println(err) return } // Send a ping to make sure the database connection is alive. if err = db.Ping(); err != nil { - ErrorLog.Println(err) + log.Error.Println(err) return } e = &Engine{db: db} - InfoLog.Println("Connect database success") + log.Info.Println("Connect database success") return } +// Close database connection func (engine *Engine) Close() (err error) { if err = engine.db.Close(); err == nil { - InfoLog.Println("Close database success") + log.Info.Println("Close database success") } return } -func (engine *Engine) NewSession() *Session { - return &Session{engine: engine} +// NewSession creates a new session for next operations +func (engine *Engine) NewSession() *session.Session { + return session.New(engine.db) } diff --git a/gee-orm/day1-database-sql/geeorm/geeorm_test.go b/gee-orm/day1-database-sql/geeorm/geeorm_test.go index c6da191..35628be 100644 --- a/gee-orm/day1-database-sql/geeorm/geeorm_test.go +++ b/gee-orm/day1-database-sql/geeorm/geeorm_test.go @@ -16,5 +16,5 @@ func OpenDB(t *testing.T) *Engine { func TestNewEngine(t *testing.T) { engine := OpenDB(t) - defer engine.Close() + _ = engine.Close() } diff --git a/gee-orm/day1-database-sql/geeorm/log/log.go b/gee-orm/day1-database-sql/geeorm/log/log.go new file mode 100644 index 0000000..4e326f3 --- /dev/null +++ b/gee-orm/day1-database-sql/geeorm/log/log.go @@ -0,0 +1,13 @@ +package log + +import ( + "log" + "os" +) + +var ( + // Error is a logger for logging error messages + Error = log.New(os.Stdout, "[error] ", log.LstdFlags|log.Lshortfile) + // Info is a logger for logging normal messages + Info = log.New(os.Stdout, "[info ] ", log.LstdFlags|log.Lshortfile) +) diff --git a/gee-orm/day1-database-sql/geeorm/session/raw.go b/gee-orm/day1-database-sql/geeorm/session/raw.go new file mode 100644 index 0000000..2d5101a --- /dev/null +++ b/gee-orm/day1-database-sql/geeorm/session/raw.go @@ -0,0 +1,53 @@ +package session + +import ( + "database/sql" + "strings" + + "geeorm/log" +) + +// Session keep a pointer to sql.DB and provides all execution of all +// kind of database operations. +type Session struct { + db *sql.DB + + SQL strings.Builder + SQLVars []interface{} +} + +// New creates a instance of Session +func New(db *sql.DB) *Session { + return &Session{db: db} +} + +// Exec raw SQL with SQLVars +func (s *Session) Exec() (result sql.Result, err error) { + log.Info.Println(s.SQL.String(), s.SQLVars) + if result, err = s.db.Exec(s.SQL.String(), s.SQLVars...); err != nil { + log.Error.Println(err) + } + return +} + +// QueryRow gets a record from db +func (s *Session) QueryRow() *sql.Row { + log.Info.Println(s.SQL.String(), s.SQLVars) + return s.db.QueryRow(s.SQL.String(), s.SQLVars...) +} + +// QueryRows gets a list of records from db +func (s *Session) QueryRows() (rows *sql.Rows, err error) { + log.Info.Println(s.SQL.String(), s.SQLVars) + if rows, err = s.db.Query(s.SQL.String(), s.SQLVars...); err != nil { + log.Error.Println(err) + } + return +} + +// Raw appends SQL and SQLVars +func (s *Session) Raw(sql string, values ...interface{}) *Session { + s.SQL.WriteString(sql) + s.SQLVars = append(s.SQLVars, values...) + return s +} diff --git a/gee-orm/day1-database-sql/geeorm/session/raw_test.go b/gee-orm/day1-database-sql/geeorm/session/raw_test.go new file mode 100644 index 0000000..1502a7e --- /dev/null +++ b/gee-orm/day1-database-sql/geeorm/session/raw_test.go @@ -0,0 +1,50 @@ +package session + +import ( + "database/sql" + "os" + "testing" + + _ "github.com/mattn/go-sqlite3" +) + +var TestDB *sql.DB + +func setup() { + TestDB, _ = sql.Open("sqlite3", "gee.db") +} + +func teardown() { + _ = TestDB.Close() +} +func TestMain(m *testing.M) { + setup() + code := m.Run() + teardown() + os.Exit(code) +} + +func NewSession() *Session { + return &Session{db: TestDB} +} + +func TestSession_Exec(t *testing.T) { + _, _ = NewSession().Raw("DROP TABLE USER;").Exec() + _, _ = NewSession().Raw("CREATE TABLE USER(name text);").Exec() + result, _ := NewSession(). + Raw("INSERT INTO USER(`name`) values (?), (?)", "Tom", "Sam").Exec() + if count, err := result.RowsAffected(); err != nil || count != 2 { + t.Fatal("expect 2, but got", count) + } +} + +func TestSession_QueryRow(t *testing.T) { + _, _ = NewSession().Raw("DROP TABLE USER;").Exec() + _, _ = NewSession().Raw("CREATE TABLE USER(name text);").Exec() + row := NewSession().Raw("SELECT count(*) FROM USER").QueryRow() + + var count int + if err := row.Scan(&count); err != nil || count != 0 { + t.Fatal("failed to query db", err) + } +} diff --git a/gee-orm/day1-database-sql/geeorm/session_raw.go b/gee-orm/day1-database-sql/geeorm/session_raw.go deleted file mode 100644 index c73f1c8..0000000 --- a/gee-orm/day1-database-sql/geeorm/session_raw.go +++ /dev/null @@ -1,40 +0,0 @@ -package geeorm - -import ( - "database/sql" - "strings" -) - -type Session struct { - engine *Engine - - SQL strings.Builder - SQLVars []interface{} -} - -func (s *Session) Exec() (result sql.Result, err error) { - InfoLog.Println(s.SQL, s.SQLVars) - if result, err = s.engine.db.Exec(s.SQL.String(), s.SQLVars...); err != nil { - ErrorLog.Println(err) - } - return -} - -func (s *Session) QueryRow() *sql.Row { - InfoLog.Println(s.SQL, s.SQLVars) - return s.engine.db.QueryRow(s.SQL.String(), s.SQLVars...) -} - -func (s *Session) QueryRows() (rows *sql.Rows, err error) { - InfoLog.Println(s.SQL, s.SQLVars) - if rows, err = s.engine.db.Query(s.SQL.String(), s.SQLVars...); err != nil { - ErrorLog.Println(err) - } - return -} - -func (s *Session) Raw(sql string, values ...interface{}) *Session { - s.SQL.WriteString(sql) - s.SQLVars = values - return s -} diff --git a/gee-orm/day1-database-sql/geeorm/session_raw_test.go b/gee-orm/day1-database-sql/geeorm/session_raw_test.go deleted file mode 100644 index 8b879ea..0000000 --- a/gee-orm/day1-database-sql/geeorm/session_raw_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package geeorm - -import ( - "testing" -) - -func TestSession_Exec(t *testing.T) { - engine := OpenDB(t) - defer engine.Close() - _, _ = engine.NewSession().Raw("DROP TABLE USER;").Exec() - _, _ = engine.NewSession().Raw("CREATE TABLE USER(name text);").Exec() - result, _ := engine.NewSession(). - Raw("INSERT INTO USER(`name`) values (?), (?)", "Tom", "Sam").Exec() - if count, err := result.RowsAffected(); err != nil || count != 2 { - t.Fatal("expect 2, but got", count) - } -} - -func TestSession_QueryRow(t *testing.T) { - engine := OpenDB(t) - defer engine.Close() - _, _ = engine.NewSession().Raw("DROP TABLE USER;").Exec() - _, _ = engine.NewSession().Raw("CREATE TABLE USER(name text);").Exec() - row := engine.NewSession().Raw("SELECT count(*) FROM USER").QueryRow() - - var count int - if err := row.Scan(&count); err != nil || count != 0 { - t.Fatal("failed to query db", err) - } -} diff --git a/gee-orm/day2-reflect-schema/geeorm/dialect/dialect.go b/gee-orm/day2-reflect-schema/geeorm/dialect/dialect.go index 63c4e69..4696314 100644 --- a/gee-orm/day2-reflect-schema/geeorm/dialect/dialect.go +++ b/gee-orm/day2-reflect-schema/geeorm/dialect/dialect.go @@ -4,16 +4,18 @@ import "reflect" var dialectsMap = map[string]Dialect{} +// Dialect is an interface contains methods that a dialect has to implement type Dialect interface { DataTypeOf(typ reflect.Value) string - PrimaryKeyTag(key string) string TableExistSQL(tableName string) (string, []interface{}) } +// RegisterDialect register a dialect to the global variable func RegisterDialect(name string, dialect Dialect) { dialectsMap[name] = dialect } +// Get the dialect from global variable if it exists func GetDialect(name string) (dialect Dialect, ok bool) { dialect, ok = dialectsMap[name] return diff --git a/gee-orm/day2-reflect-schema/geeorm/dialect/sqlite3.go b/gee-orm/day2-reflect-schema/geeorm/dialect/sqlite3.go index 3a3a444..f3c3897 100644 --- a/gee-orm/day2-reflect-schema/geeorm/dialect/sqlite3.go +++ b/gee-orm/day2-reflect-schema/geeorm/dialect/sqlite3.go @@ -38,10 +38,7 @@ func (s *sqlite3) DataTypeOf(typ reflect.Value) string { panic(fmt.Sprintf("invalid sql type %s (%s)", typ.Type().Name(), typ.Kind())) } -func (s *sqlite3) PrimaryKeyTag(key string) string { - return "INTEGER PRIMARY KEY AUTOINCREMENT" -} - +// TableExistSQL returns SQL that judge whether the table exists in database func (s *sqlite3) TableExistSQL(tableName string) (string, []interface{}) { args := []interface{}{tableName} return "SELECT name FROM sqlite_master WHERE type='table' and name = ?", args diff --git a/gee-orm/day2-reflect-schema/geeorm/geeorm.go b/gee-orm/day2-reflect-schema/geeorm/geeorm.go index 0121bfe..ee3ec33 100644 --- a/gee-orm/day2-reflect-schema/geeorm/geeorm.go +++ b/gee-orm/day2-reflect-schema/geeorm/geeorm.go @@ -3,52 +3,52 @@ package geeorm import ( "database/sql" "fmt" - "log" - "os" "geeorm/dialect" + "geeorm/log" + "geeorm/session" ) -var ( - ErrorLog = log.New(os.Stdout, "[error] ", log.LstdFlags|log.Lshortfile) - InfoLog = log.New(os.Stdout, "[info ] ", log.LstdFlags|log.Lshortfile) -) - +// Engine is the main struct of geeorm, manages all db sessions and transactions. type Engine struct { db *sql.DB dialect dialect.Dialect } +// NewEngine create a instance of Engine +// connect database and ping it to test whether it's alive func NewEngine(driver, source string) (e *Engine, err error) { db, err := sql.Open(driver, source) if err != nil { - ErrorLog.Println(err) + log.Error.Println(err) return } // Send a ping to make sure the database connection is alive. if err = db.Ping(); err != nil { - ErrorLog.Println(err) + log.Error.Println(err) return } // make sure the specific dialect exists dial, ok := dialect.GetDialect(driver) if !ok { err = fmt.Errorf("dialect %s Not Found", driver) - ErrorLog.Println(err) + log.Error.Println(err) return } e = &Engine{db: db, dialect: dial} - InfoLog.Println("Connect database success") + log.Info.Println("Connect database success") return } +// Close database connection func (e *Engine) Close() (err error) { if err = e.db.Close(); err == nil { - InfoLog.Println("Close database success") + log.Info.Println("Close database success") } return } -func (e *Engine) NewSession() *Session { - return &Session{engine: e} +// NewSession creates a new session for next operations +func (e *Engine) NewSession() *session.Session { + return session.New(e.db, e.dialect) } diff --git a/gee-orm/day2-reflect-schema/geeorm/geeorm_test.go b/gee-orm/day2-reflect-schema/geeorm/geeorm_test.go index c6da191..35628be 100644 --- a/gee-orm/day2-reflect-schema/geeorm/geeorm_test.go +++ b/gee-orm/day2-reflect-schema/geeorm/geeorm_test.go @@ -16,5 +16,5 @@ func OpenDB(t *testing.T) *Engine { func TestNewEngine(t *testing.T) { engine := OpenDB(t) - defer engine.Close() + _ = engine.Close() } diff --git a/gee-orm/day2-reflect-schema/geeorm/log/log.go b/gee-orm/day2-reflect-schema/geeorm/log/log.go new file mode 100644 index 0000000..4e326f3 --- /dev/null +++ b/gee-orm/day2-reflect-schema/geeorm/log/log.go @@ -0,0 +1,13 @@ +package log + +import ( + "log" + "os" +) + +var ( + // Error is a logger for logging error messages + Error = log.New(os.Stdout, "[error] ", log.LstdFlags|log.Lshortfile) + // Info is a logger for logging normal messages + Info = log.New(os.Stdout, "[info ] ", log.LstdFlags|log.Lshortfile) +) diff --git a/gee-orm/day2-reflect-schema/geeorm/schema/field.go b/gee-orm/day2-reflect-schema/geeorm/schema/field.go deleted file mode 100644 index 46e82e4..0000000 --- a/gee-orm/day2-reflect-schema/geeorm/schema/field.go +++ /dev/null @@ -1,13 +0,0 @@ -package schema - -import "fmt" - -type Field struct { - Name string - Value interface{} - Tag string -} - -func (f *Field) String() string { - return fmt.Sprintf("%s %s", f.Name, f.Tag) -} diff --git a/gee-orm/day2-reflect-schema/geeorm/schema/schema.go b/gee-orm/day2-reflect-schema/geeorm/schema/schema.go index 03fcdce..299368f 100644 --- a/gee-orm/day2-reflect-schema/geeorm/schema/schema.go +++ b/gee-orm/day2-reflect-schema/geeorm/schema/schema.go @@ -2,44 +2,68 @@ package schema import ( "fmt" + "geeorm/dialect" "go/ast" "reflect" - "strings" - - "geeorm/dialect" ) +// Field represents a column of database +type Field struct { + Name string + Tag string +} + +// Schema represents a table of database type Schema struct { TableName string PrimaryField *Field Fields []*Field + FieldNames []string + BindVars []string } +// Values return the values of dest's member variables +func (schema *Schema) Values(dest interface{}) []interface{} { + destValue := reflect.Indirect(reflect.ValueOf(dest)) + var fieldValues []interface{} + for _, field := range schema.Fields { + fieldValues = append(fieldValues, destValue.FieldByName(field.Name).Interface()) + } + return fieldValues +} + +// Parse a struct to a Schema instance func Parse(dest interface{}, d dialect.Dialect) *Schema { modelType := reflect.Indirect(reflect.ValueOf(dest)).Type() - schema := &Schema{ TableName: modelType.Name(), - PrimaryField: &Field{Name: "ID", Value: 0}, + PrimaryField: &Field{Name: "ID", Tag: ""}, } for i := 0; i < modelType.NumField(); i++ { p := modelType.Field(i) if !p.Anonymous && ast.IsExported(p.Name) { - schema.Fields = append(schema.Fields, &Field{ + field := &Field{ Name: p.Name, Tag: d.DataTypeOf(reflect.Indirect(reflect.New(p.Type))), - }) + } + if v, ok := p.Tag.Lookup("geeorm"); ok && v == "primary_key" { + schema.PrimaryField = field + } + schema.Fields = append(schema.Fields, field) + schema.FieldNames = append(schema.FieldNames, p.Name) + schema.BindVars = append(schema.BindVars, "?") } } return schema } -func (s *Schema) String() string { - var fieldStr []string - for _, field := range s.Fields { - fieldStr = append(fieldStr, field.String()) - } +// String returns readable string +func (field *Field) String() string { + return fmt.Sprintf("(%s %s)", field.Name, field.Tag) +} - return fmt.Sprintf("TABLE %s(%s)", s.TableName, strings.Join(fieldStr, ", ")) +// String returns readable string +func (schema *Schema) String() string { + return fmt.Sprintf("TABLE %s(%v)", schema.TableName, schema.Fields) } diff --git a/gee-orm/day2-reflect-schema/geeorm/schema/schema_test.go b/gee-orm/day2-reflect-schema/geeorm/schema/schema_test.go index 4142b95..34725b7 100644 --- a/gee-orm/day2-reflect-schema/geeorm/schema/schema_test.go +++ b/gee-orm/day2-reflect-schema/geeorm/schema/schema_test.go @@ -6,15 +6,30 @@ import ( ) type User struct { - Name string + Name string `geeorm:"primary_key"` Age int } -func TestParse(t *testing.T) { - dial, _ := dialect.GetDialect("sqlite3") - schema := Parse(&User{"Tom", 18}, dial) +var TestDial, _ = dialect.GetDialect("sqlite3") +func TestParse(t *testing.T) { + schema := Parse(&User{}, TestDial) if schema.TableName != "User" || len(schema.Fields) != 2 { t.Fatal("failed to parse User struct") } + if schema.PrimaryField.Name != "Name" { + t.Fatal("failed to parse primary key") + } +} + +func TestSchema_Values(t *testing.T) { + schema := Parse(&User{}, TestDial) + values := schema.Values(&User{"Tom", 18}) + + name := values[0].(string) + age := values[1].(int) + + if name != "Tom" || age != 18 { + t.Fatal("failed to get values") + } } diff --git a/gee-orm/day2-reflect-schema/geeorm/session/raw.go b/gee-orm/day2-reflect-schema/geeorm/session/raw.go new file mode 100644 index 0000000..02ea7df --- /dev/null +++ b/gee-orm/day2-reflect-schema/geeorm/session/raw.go @@ -0,0 +1,61 @@ +package session + +import ( + "database/sql" + "strings" + + "geeorm/dialect" + "geeorm/log" + "geeorm/schema" +) + +// Session keep a pointer to sql.DB and provides all execution of all +// kind of database operations. +type Session struct { + db *sql.DB + dialect dialect.Dialect + refTable *schema.Schema + + Value interface{} + SQL strings.Builder + SQLVars []interface{} +} + +// New creates a instance of Session +func New(db *sql.DB, dialect dialect.Dialect) *Session { + return &Session{ + db: db, + dialect: dialect, + } +} + +// Exec raw SQL with SQLVars +func (s *Session) Exec() (result sql.Result, err error) { + log.Info.Println(s.SQL.String(), s.SQLVars) + if result, err = s.db.Exec(s.SQL.String(), s.SQLVars...); err != nil { + log.Error.Println(err) + } + return +} + +// QueryRow gets a record from db +func (s *Session) QueryRow() *sql.Row { + log.Info.Println(s.SQL.String(), s.SQLVars) + return s.db.QueryRow(s.SQL.String(), s.SQLVars...) +} + +// QueryRows gets a list of records from db +func (s *Session) QueryRows() (rows *sql.Rows, err error) { + log.Info.Println(s.SQL.String(), s.SQLVars) + if rows, err = s.db.Query(s.SQL.String(), s.SQLVars...); err != nil { + log.Error.Println(err) + } + return +} + +// Raw appends SQL and SQLVars +func (s *Session) Raw(sql string, values ...interface{}) *Session { + s.SQL.WriteString(sql) + s.SQLVars = append(s.SQLVars, values...) + return s +} diff --git a/gee-orm/day2-reflect-schema/geeorm/session/raw_test.go b/gee-orm/day2-reflect-schema/geeorm/session/raw_test.go new file mode 100644 index 0000000..bfcfb80 --- /dev/null +++ b/gee-orm/day2-reflect-schema/geeorm/session/raw_test.go @@ -0,0 +1,54 @@ +package session + +import ( + "database/sql" + "os" + "testing" + + "geeorm/dialect" + _ "github.com/mattn/go-sqlite3" +) + +var ( + TestDB *sql.DB + TestDial dialect.Dialect +) + +func setup() { + TestDB, _ = sql.Open("sqlite3", "gee.db") + TestDial, _ = dialect.GetDialect("sqlite3") +} + +func teardown() { + _ = TestDB.Close() +} + +func TestMain(m *testing.M) { + setup() + code := m.Run() + teardown() + os.Exit(code) +} + +func NewSession() *Session { + return &Session{db: TestDB, dialect: TestDial} +} + +func TestSession_Exec(t *testing.T) { + _, _ = NewSession().Raw("DROP TABLE USER;").Exec() + _, _ = NewSession().Raw("CREATE TABLE USER(name text);").Exec() + result, _ := NewSession().Raw("INSERT INTO USER(`name`) values (?), (?)", "Tom", "Sam").Exec() + if count, err := result.RowsAffected(); err != nil || count != 2 { + t.Fatal("expect 2, but got", count) + } +} + +func TestSession_QueryRows(t *testing.T) { + _, _ = NewSession().Raw("DROP TABLE USER;").Exec() + _, _ = NewSession().Raw("CREATE TABLE USER(name text);").Exec() + row := NewSession().Raw("SELECT count(*) FROM USER").QueryRow() + var count int + if err := row.Scan(&count); err != nil || count != 0 { + t.Fatal("failed to query db", err) + } +} diff --git a/gee-orm/day2-reflect-schema/geeorm/session/schema.go b/gee-orm/day2-reflect-schema/geeorm/session/schema.go new file mode 100644 index 0000000..0767d67 --- /dev/null +++ b/gee-orm/day2-reflect-schema/geeorm/session/schema.go @@ -0,0 +1,59 @@ +package session + +import ( + "fmt" + "strings" + + "geeorm/log" + "geeorm/schema" +) + +// RefTable returns a Schema instance that contains all parsed fields +func (s *Session) RefTable(value interface{}) *schema.Schema { + if value == nil { + panic("value is nil") + } + if s.refTable == nil { + s.refTable = schema.Parse(value, s.dialect) + } + return s.refTable +} + +// CreateTable create a table in database with a model +func (s *Session) CreateTable(value interface{}) error { + table := s.RefTable(value) + var columns []string + for _, field := range table.Fields { + tag := field.Tag + if field.Name == table.PrimaryField.Name { + tag = table.PrimaryField.Tag + } + columns = append(columns, fmt.Sprintf("%s %s", field.Name, tag)) + } + desc := strings.Join(columns, ",") + _, err := s.Raw(fmt.Sprintf("CREATE TABLE %s (%s);", table.TableName, desc)).Exec() + return err +} + +// DropTable drops a table with the name of model +func (s *Session) DropTable(value interface{}) error { + table := s.RefTable(value) + _, err := s.Raw(fmt.Sprintf("DROP TABLE %s", table.TableName)).Exec() + return err +} + +// HasTable returns true of the table exists +func (s *Session) HasTable(value interface{}) bool { + tableName, ok := value.(string) + if !ok { + tableName = s.RefTable(value).TableName + } + + sql, values := s.dialect.TableExistSQL(tableName) + row := s.Raw(sql, values...).QueryRow() + var tmp string + if err := row.Scan(&tmp); err != nil { + log.Error.Println(err) + } + return tmp == tableName +} diff --git a/gee-orm/day2-reflect-schema/geeorm/session/schema_test.go b/gee-orm/day2-reflect-schema/geeorm/session/schema_test.go new file mode 100644 index 0000000..8405886 --- /dev/null +++ b/gee-orm/day2-reflect-schema/geeorm/session/schema_test.go @@ -0,0 +1,18 @@ +package session + +import ( + "testing" +) + +type User struct { + Name string + Age int +} + +func TestSession_CreateTable(t *testing.T) { + _ = NewSession().DropTable(&User{}) + _ = NewSession().CreateTable(&User{}) + if !NewSession().HasTable("User") { + t.Fatal("failed to create table User") + } +} diff --git a/gee-orm/day2-reflect-schema/geeorm/session_raw.go b/gee-orm/day2-reflect-schema/geeorm/session_raw.go deleted file mode 100644 index 48b8309..0000000 --- a/gee-orm/day2-reflect-schema/geeorm/session_raw.go +++ /dev/null @@ -1,44 +0,0 @@ -package geeorm - -import ( - "database/sql" - "strings" - - "geeorm/schema" -) - -type Session struct { - engine *Engine - refTable *schema.Schema - - Value interface{} - SQL strings.Builder - SQLVars []interface{} -} - -func (s *Session) Exec() (result sql.Result, err error) { - InfoLog.Println(s.SQL.String(), s.SQLVars) - if result, err = s.engine.db.Exec(s.SQL.String(), s.SQLVars...); err != nil { - ErrorLog.Println(err) - } - return -} - -func (s *Session) QueryRow() *sql.Row { - InfoLog.Println(s.SQL.String(), s.SQLVars) - return s.engine.db.QueryRow(s.SQL.String(), s.SQLVars...) -} - -func (s *Session) QueryRows() (rows *sql.Rows, err error) { - InfoLog.Println(s.SQL.String(), s.SQLVars) - if rows, err = s.engine.db.Query(s.SQL.String(), s.SQLVars...); err != nil { - ErrorLog.Println(err) - } - return -} - -func (s *Session) Raw(sql string, values ...interface{}) *Session { - s.SQL.WriteString(sql) - s.SQLVars = values - return s -} diff --git a/gee-orm/day2-reflect-schema/geeorm/session_raw_test.go b/gee-orm/day2-reflect-schema/geeorm/session_raw_test.go deleted file mode 100644 index c541a37..0000000 --- a/gee-orm/day2-reflect-schema/geeorm/session_raw_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package geeorm - -import ( - "testing" -) - -func TestSession_Exec(t *testing.T) { - engine := OpenDB(t) - defer engine.Close() - _, _ = engine.NewSession().Raw("DROP TABLE USER;").Exec() - _, _ = engine.NewSession().Raw("CREATE TABLE USER(name text);").Exec() - result, _ := engine.NewSession(). - Raw("INSERT INTO USER(`name`) values (?), (?)", "Tom", "Sam").Exec() - if count, err := result.RowsAffected(); err != nil || count != 2 { - t.Fatal("expect 2, but got", count) - } -} - -func TestSession_QueryRows(t *testing.T) { - engine := OpenDB(t) - defer engine.Close() - _, _ = engine.NewSession().Raw("DROP TABLE USER;").Exec() - _, _ = engine.NewSession().Raw("CREATE TABLE USER(name text);").Exec() - rows, _ := engine.NewSession().Raw("SELECT count(*) FROM USER").QueryRows() - defer rows.Close() - var count int - for rows.Next() { - if err := rows.Scan(&count); err != nil || count != 0 { - t.Fatal("failed to query db", err) - } - } -} diff --git a/gee-orm/day2-reflect-schema/geeorm/session_schema_test.go b/gee-orm/day2-reflect-schema/geeorm/session_schema_test.go deleted file mode 100755 index 4c0ba43..0000000 --- a/gee-orm/day2-reflect-schema/geeorm/session_schema_test.go +++ /dev/null @@ -1,18 +0,0 @@ -package geeorm - -import "testing" - -type User struct { - Name string - Age int -} - -func TestSession_CreateTable(t *testing.T) { - engine := OpenDB(t) - defer engine.Close() - _ = engine.NewSession().DropTable(&User{}) - _ = engine.NewSession().CreateTable(&User{}) - if ! engine.NewSession().HasTable("User") { - t.Fatal("failed to create table User") - } -} diff --git a/gee-orm/day3-save-query/geeorm/dialect/dialect.go b/gee-orm/day3-save-query/geeorm/dialect/dialect.go new file mode 100644 index 0000000..4696314 --- /dev/null +++ b/gee-orm/day3-save-query/geeorm/dialect/dialect.go @@ -0,0 +1,22 @@ +package dialect + +import "reflect" + +var dialectsMap = map[string]Dialect{} + +// Dialect is an interface contains methods that a dialect has to implement +type Dialect interface { + DataTypeOf(typ reflect.Value) string + TableExistSQL(tableName string) (string, []interface{}) +} + +// RegisterDialect register a dialect to the global variable +func RegisterDialect(name string, dialect Dialect) { + dialectsMap[name] = dialect +} + +// Get the dialect from global variable if it exists +func GetDialect(name string) (dialect Dialect, ok bool) { + dialect, ok = dialectsMap[name] + return +} diff --git a/gee-orm/day3-save-query/geeorm/dialect/sqlite3.go b/gee-orm/day3-save-query/geeorm/dialect/sqlite3.go new file mode 100644 index 0000000..f3c3897 --- /dev/null +++ b/gee-orm/day3-save-query/geeorm/dialect/sqlite3.go @@ -0,0 +1,45 @@ +package dialect + +import ( + "fmt" + "reflect" + "time" +) + +type sqlite3 struct{} + +var _ Dialect = (*sqlite3)(nil) + +func init() { + RegisterDialect("sqlite3", &sqlite3{}) +} + +// Get Data Type for sqlite3 Dialect +func (s *sqlite3) DataTypeOf(typ reflect.Value) string { + switch typ.Kind() { + case reflect.Bool: + return "bool" + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uintptr: + return "integer" + case reflect.Int64, reflect.Uint64: + return "bigint" + case reflect.Float32, reflect.Float64: + return "real" + case reflect.String: + return "text" + case reflect.Array, reflect.Slice: + return "blob" + case reflect.Struct: + if _, ok := typ.Interface().(time.Time); ok { + return "datetime" + } + } + panic(fmt.Sprintf("invalid sql type %s (%s)", typ.Type().Name(), typ.Kind())) +} + +// TableExistSQL returns SQL that judge whether the table exists in database +func (s *sqlite3) TableExistSQL(tableName string) (string, []interface{}) { + args := []interface{}{tableName} + return "SELECT name FROM sqlite_master WHERE type='table' and name = ?", args +} diff --git a/gee-orm/day3-save-query/geeorm/dialect/sqlite3_test.go b/gee-orm/day3-save-query/geeorm/dialect/sqlite3_test.go new file mode 100644 index 0000000..3df5f07 --- /dev/null +++ b/gee-orm/day3-save-query/geeorm/dialect/sqlite3_test.go @@ -0,0 +1,25 @@ +package dialect + +import ( + "reflect" + "testing" +) + +func TestDataTypeOf(t *testing.T) { + dial := &sqlite3{} + cases := []struct { + Value interface{} + Type string + }{ + {"Tom", "text"}, + {123, "integer"}, + {1.2, "real"}, + {[]int{1, 2, 3}, "blob"}, + } + + for _, c := range cases { + if typ := dial.DataTypeOf(reflect.ValueOf(c.Value)); typ != c.Type { + t.Fatalf("expect %s, but got %s", c.Type, typ) + } + } +} diff --git a/gee-orm/day3-save-query/geeorm/geeorm.go b/gee-orm/day3-save-query/geeorm/geeorm.go new file mode 100644 index 0000000..ee3ec33 --- /dev/null +++ b/gee-orm/day3-save-query/geeorm/geeorm.go @@ -0,0 +1,54 @@ +package geeorm + +import ( + "database/sql" + "fmt" + + "geeorm/dialect" + "geeorm/log" + "geeorm/session" +) + +// Engine is the main struct of geeorm, manages all db sessions and transactions. +type Engine struct { + db *sql.DB + dialect dialect.Dialect +} + +// NewEngine create a instance of Engine +// connect database and ping it to test whether it's alive +func NewEngine(driver, source string) (e *Engine, err error) { + db, err := sql.Open(driver, source) + if err != nil { + log.Error.Println(err) + return + } + // Send a ping to make sure the database connection is alive. + if err = db.Ping(); err != nil { + log.Error.Println(err) + return + } + // make sure the specific dialect exists + dial, ok := dialect.GetDialect(driver) + if !ok { + err = fmt.Errorf("dialect %s Not Found", driver) + log.Error.Println(err) + return + } + e = &Engine{db: db, dialect: dial} + log.Info.Println("Connect database success") + return +} + +// Close database connection +func (e *Engine) Close() (err error) { + if err = e.db.Close(); err == nil { + log.Info.Println("Close database success") + } + return +} + +// NewSession creates a new session for next operations +func (e *Engine) NewSession() *session.Session { + return session.New(e.db, e.dialect) +} diff --git a/gee-orm/day3-save-query/geeorm/geeorm_test.go b/gee-orm/day3-save-query/geeorm/geeorm_test.go new file mode 100644 index 0000000..35628be --- /dev/null +++ b/gee-orm/day3-save-query/geeorm/geeorm_test.go @@ -0,0 +1,20 @@ +package geeorm + +import ( + _ "github.com/mattn/go-sqlite3" + "testing" +) + +func OpenDB(t *testing.T) *Engine { + t.Helper() + engine, err := NewEngine("sqlite3", "gee.db") + if err != nil { + t.Fatal("failed to connect", err) + } + return engine +} + +func TestNewEngine(t *testing.T) { + engine := OpenDB(t) + _ = engine.Close() +} diff --git a/gee-orm/day3-save-query/geeorm/go.mod b/gee-orm/day3-save-query/geeorm/go.mod new file mode 100644 index 0000000..043b1c6 --- /dev/null +++ b/gee-orm/day3-save-query/geeorm/go.mod @@ -0,0 +1,5 @@ +module geeorm + +go 1.13 + +require github.com/mattn/go-sqlite3 v2.0.3+incompatible diff --git a/gee-orm/day3-save-query/geeorm/log/log.go b/gee-orm/day3-save-query/geeorm/log/log.go new file mode 100644 index 0000000..4e326f3 --- /dev/null +++ b/gee-orm/day3-save-query/geeorm/log/log.go @@ -0,0 +1,13 @@ +package log + +import ( + "log" + "os" +) + +var ( + // Error is a logger for logging error messages + Error = log.New(os.Stdout, "[error] ", log.LstdFlags|log.Lshortfile) + // Info is a logger for logging normal messages + Info = log.New(os.Stdout, "[info ] ", log.LstdFlags|log.Lshortfile) +) diff --git a/gee-orm/day3-save-query/geeorm/schema/schema.go b/gee-orm/day3-save-query/geeorm/schema/schema.go new file mode 100644 index 0000000..299368f --- /dev/null +++ b/gee-orm/day3-save-query/geeorm/schema/schema.go @@ -0,0 +1,69 @@ +package schema + +import ( + "fmt" + "geeorm/dialect" + "go/ast" + "reflect" +) + +// Field represents a column of database +type Field struct { + Name string + Tag string +} + +// Schema represents a table of database +type Schema struct { + TableName string + PrimaryField *Field + Fields []*Field + FieldNames []string + BindVars []string +} + +// Values return the values of dest's member variables +func (schema *Schema) Values(dest interface{}) []interface{} { + destValue := reflect.Indirect(reflect.ValueOf(dest)) + var fieldValues []interface{} + for _, field := range schema.Fields { + fieldValues = append(fieldValues, destValue.FieldByName(field.Name).Interface()) + } + return fieldValues +} + +// Parse a struct to a Schema instance +func Parse(dest interface{}, d dialect.Dialect) *Schema { + modelType := reflect.Indirect(reflect.ValueOf(dest)).Type() + schema := &Schema{ + TableName: modelType.Name(), + PrimaryField: &Field{Name: "ID", Tag: ""}, + } + + for i := 0; i < modelType.NumField(); i++ { + p := modelType.Field(i) + if !p.Anonymous && ast.IsExported(p.Name) { + field := &Field{ + Name: p.Name, + Tag: d.DataTypeOf(reflect.Indirect(reflect.New(p.Type))), + } + if v, ok := p.Tag.Lookup("geeorm"); ok && v == "primary_key" { + schema.PrimaryField = field + } + schema.Fields = append(schema.Fields, field) + schema.FieldNames = append(schema.FieldNames, p.Name) + schema.BindVars = append(schema.BindVars, "?") + } + } + return schema +} + +// String returns readable string +func (field *Field) String() string { + return fmt.Sprintf("(%s %s)", field.Name, field.Tag) +} + +// String returns readable string +func (schema *Schema) String() string { + return fmt.Sprintf("TABLE %s(%v)", schema.TableName, schema.Fields) +} diff --git a/gee-orm/day3-save-query/geeorm/schema/schema_test.go b/gee-orm/day3-save-query/geeorm/schema/schema_test.go new file mode 100644 index 0000000..aba3e0b --- /dev/null +++ b/gee-orm/day3-save-query/geeorm/schema/schema_test.go @@ -0,0 +1,36 @@ +package schema + +import ( + "geeorm/dialect" + "testing" +) + +type User struct { + Name string `geeorm:"primary_key"` + Age int +} + +var TestDial, _ = dialect.GetDialect("sqlite3") + +func TestParse(t *testing.T) { + schema := Parse(&User{}, TestDial) + if schema.TableName != "User" || len(schema.Fields) != 2 { + t.Fatal("failed to parse User struct") + } + if schema.PrimaryField.Name != "Name" { + t.Fatal("failed to parse primary key") + } + t.Log(schema) +} + +func TestSchema_Values(t *testing.T) { + schema := Parse(&User{}, TestDial) + values := schema.Values(&User{"Tom", 18}) + + name := values[0].(string) + age := values[1].(int) + + if name != "Tom" || age != 18 { + t.Fatal("failed to get values") + } +} diff --git a/gee-orm/day3-save-query/geeorm/session/raw.go b/gee-orm/day3-save-query/geeorm/session/raw.go new file mode 100644 index 0000000..02ea7df --- /dev/null +++ b/gee-orm/day3-save-query/geeorm/session/raw.go @@ -0,0 +1,61 @@ +package session + +import ( + "database/sql" + "strings" + + "geeorm/dialect" + "geeorm/log" + "geeorm/schema" +) + +// Session keep a pointer to sql.DB and provides all execution of all +// kind of database operations. +type Session struct { + db *sql.DB + dialect dialect.Dialect + refTable *schema.Schema + + Value interface{} + SQL strings.Builder + SQLVars []interface{} +} + +// New creates a instance of Session +func New(db *sql.DB, dialect dialect.Dialect) *Session { + return &Session{ + db: db, + dialect: dialect, + } +} + +// Exec raw SQL with SQLVars +func (s *Session) Exec() (result sql.Result, err error) { + log.Info.Println(s.SQL.String(), s.SQLVars) + if result, err = s.db.Exec(s.SQL.String(), s.SQLVars...); err != nil { + log.Error.Println(err) + } + return +} + +// QueryRow gets a record from db +func (s *Session) QueryRow() *sql.Row { + log.Info.Println(s.SQL.String(), s.SQLVars) + return s.db.QueryRow(s.SQL.String(), s.SQLVars...) +} + +// QueryRows gets a list of records from db +func (s *Session) QueryRows() (rows *sql.Rows, err error) { + log.Info.Println(s.SQL.String(), s.SQLVars) + if rows, err = s.db.Query(s.SQL.String(), s.SQLVars...); err != nil { + log.Error.Println(err) + } + return +} + +// Raw appends SQL and SQLVars +func (s *Session) Raw(sql string, values ...interface{}) *Session { + s.SQL.WriteString(sql) + s.SQLVars = append(s.SQLVars, values...) + return s +} diff --git a/gee-orm/day3-save-query/geeorm/session/raw_test.go b/gee-orm/day3-save-query/geeorm/session/raw_test.go new file mode 100644 index 0000000..bfcfb80 --- /dev/null +++ b/gee-orm/day3-save-query/geeorm/session/raw_test.go @@ -0,0 +1,54 @@ +package session + +import ( + "database/sql" + "os" + "testing" + + "geeorm/dialect" + _ "github.com/mattn/go-sqlite3" +) + +var ( + TestDB *sql.DB + TestDial dialect.Dialect +) + +func setup() { + TestDB, _ = sql.Open("sqlite3", "gee.db") + TestDial, _ = dialect.GetDialect("sqlite3") +} + +func teardown() { + _ = TestDB.Close() +} + +func TestMain(m *testing.M) { + setup() + code := m.Run() + teardown() + os.Exit(code) +} + +func NewSession() *Session { + return &Session{db: TestDB, dialect: TestDial} +} + +func TestSession_Exec(t *testing.T) { + _, _ = NewSession().Raw("DROP TABLE USER;").Exec() + _, _ = NewSession().Raw("CREATE TABLE USER(name text);").Exec() + result, _ := NewSession().Raw("INSERT INTO USER(`name`) values (?), (?)", "Tom", "Sam").Exec() + if count, err := result.RowsAffected(); err != nil || count != 2 { + t.Fatal("expect 2, but got", count) + } +} + +func TestSession_QueryRows(t *testing.T) { + _, _ = NewSession().Raw("DROP TABLE USER;").Exec() + _, _ = NewSession().Raw("CREATE TABLE USER(name text);").Exec() + row := NewSession().Raw("SELECT count(*) FROM USER").QueryRow() + var count int + if err := row.Scan(&count); err != nil || count != 0 { + t.Fatal("failed to query db", err) + } +} diff --git a/gee-orm/day3-save-query/geeorm/session/record.go b/gee-orm/day3-save-query/geeorm/session/record.go new file mode 100644 index 0000000..9b63548 --- /dev/null +++ b/gee-orm/day3-save-query/geeorm/session/record.go @@ -0,0 +1,43 @@ +package session + +import ( + "fmt" + "strings" +) + +// Create one or more records in database +func (s *Session) Create(values ...interface{}) (int64, error) { + var flag bool + for i, value := range values { + table := s.RefTable(value) + filedSQL := strings.Join(table.FieldNames, ", ") + bindVarSQL := strings.Join(table.BindVars, ", ") + if !flag { + s.Raw(fmt.Sprintf("INSERT INTO %s (%v) VALUES ", table.TableName, filedSQL)) + flag = true + } + s.Raw(fmt.Sprintf("(%v)", bindVarSQL), table.Values(value)...) + if i == len(values)-1 { + s.Raw(";") + } else { + s.Raw(",") + } + } + + result, err := s.Exec() + if err != nil { + return 0, err + } + + return result.RowsAffected() +} + +func (s *Session) First(value interface{}) error { + table := s.RefTable(value) + fieldSQL := strings.Join(table.FieldNames, ", ") + sql := fmt.Sprintf("SELECT (%v) FROM %s LIMIT 1", fieldSQL, table.TableName) + + row := s.Raw(sql).QueryRow() + + return row.Scan(value) +} diff --git a/gee-orm/day3-save-query/geeorm/session/record_test.go b/gee-orm/day3-save-query/geeorm/session/record_test.go new file mode 100644 index 0000000..abf157a --- /dev/null +++ b/gee-orm/day3-save-query/geeorm/session/record_test.go @@ -0,0 +1,16 @@ +package session + +import "testing" + +var ( + user1 = &User{"Tom", 18} + user2 = &User{"Sam", 25} +) + +func TestSession_Create(t *testing.T) { + _ = NewSession().DropTable(&User{}) + _ = NewSession().CreateTable(&User{}) + if affected, err := NewSession().Create(user1, user2); err != nil || affected != 2 { + t.Fatal("failed to create record") + } +} diff --git a/gee-orm/day2-reflect-schema/geeorm/session_schema.go b/gee-orm/day3-save-query/geeorm/session/schema.go old mode 100755 new mode 100644 similarity index 63% rename from gee-orm/day2-reflect-schema/geeorm/session_schema.go rename to gee-orm/day3-save-query/geeorm/session/schema.go index 38a0bdc..d9135f9 --- a/gee-orm/day2-reflect-schema/geeorm/session_schema.go +++ b/gee-orm/day3-save-query/geeorm/session/schema.go @@ -1,53 +1,59 @@ -package geeorm +package session import ( "fmt" "strings" + "geeorm/log" "geeorm/schema" ) +// RefTable returns a Schema instance that contains all parsed fields func (s *Session) RefTable(value interface{}) *schema.Schema { if value == nil { panic("value is nil") } if s.refTable == nil { - s.refTable = schema.Parse(value, s.engine.dialect) + s.refTable = schema.Parse(value, s.dialect) } return s.refTable } +// CreateTable create a table in database with a model func (s *Session) CreateTable(value interface{}) error { table := s.RefTable(value) var columns []string for _, field := range table.Fields { - columns = append(columns, fmt.Sprintf("%s %s", field.Name, field.Tag)) + tag := field.Tag + if field.Name == table.PrimaryField.Name { + tag += " PRIMARY KEY" + } + columns = append(columns, fmt.Sprintf("%s %s", field.Name, tag)) } desc := strings.Join(columns, ",") _, err := s.Raw(fmt.Sprintf("CREATE TABLE %s (%s);", table.TableName, desc)).Exec() return err } +// DropTable drops a table with the name of model func (s *Session) DropTable(value interface{}) error { table := s.RefTable(value) _, err := s.Raw(fmt.Sprintf("DROP TABLE %s", table.TableName)).Exec() return err } +// HasTable returns true of the table exists func (s *Session) HasTable(value interface{}) bool { tableName, ok := value.(string) if !ok { tableName = s.RefTable(value).TableName } - sql, values := s.engine.dialect.TableExistSQL(tableName) + sql, values := s.dialect.TableExistSQL(tableName) row := s.Raw(sql, values...).QueryRow() - - - var tmp string if err := row.Scan(&tmp); err != nil { - ErrorLog.Println(err) + log.Error.Println(err) } return tmp == tableName } diff --git a/gee-orm/day3-save-query/geeorm/session/schema_test.go b/gee-orm/day3-save-query/geeorm/session/schema_test.go new file mode 100644 index 0000000..5c934fa --- /dev/null +++ b/gee-orm/day3-save-query/geeorm/session/schema_test.go @@ -0,0 +1,18 @@ +package session + +import ( + "testing" +) + +type User struct { + Name string `geeorm:"primary_key"` + Age int +} + +func TestSession_CreateTable(t *testing.T) { + _ = NewSession().DropTable(&User{}) + _ = NewSession().CreateTable(&User{}) + if !NewSession().HasTable("User") { + t.Fatal("failed to create table User") + } +} From 95814ae9ae52dc1b446f7506189eede16685dd78 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Mon, 24 Feb 2020 01:24:05 +0800 Subject: [PATCH 040/122] day3 add find multi records & find first record --- .../day3-save-query/geeorm/session/record.go | 44 +++++++++++++++++-- .../geeorm/session/record_test.go | 21 +++++++++ 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/gee-orm/day3-save-query/geeorm/session/record.go b/gee-orm/day3-save-query/geeorm/session/record.go index 9b63548..2b7c7da 100644 --- a/gee-orm/day3-save-query/geeorm/session/record.go +++ b/gee-orm/day3-save-query/geeorm/session/record.go @@ -2,6 +2,7 @@ package session import ( "fmt" + "reflect" "strings" ) @@ -32,12 +33,49 @@ func (s *Session) Create(values ...interface{}) (int64, error) { return result.RowsAffected() } +// First gets the 1st row func (s *Session) First(value interface{}) error { table := s.RefTable(value) + fieldSQL := strings.Join(table.FieldNames, ", ") - sql := fmt.Sprintf("SELECT (%v) FROM %s LIMIT 1", fieldSQL, table.TableName) + selectSQL := fmt.Sprintf("SELECT %v FROM %s LIMIT 1", fieldSQL, table.TableName) + row := s.Raw(selectSQL).QueryRow() + + dest := reflect.ValueOf(value).Elem() + var values []interface{} + for _, name := range table.FieldNames { + values = append(values, dest.FieldByName(name).Addr().Interface()) + } - row := s.Raw(sql).QueryRow() + return row.Scan(values...) +} - return row.Scan(value) +// Find gets all eligible records +func (s *Session) Find(values interface{}) error { + destSlice := reflect.Indirect(reflect.ValueOf(values)) + destType := destSlice.Type().Elem() + table := s.RefTable(reflect.New(destType).Elem().Interface()) + + fieldSQL := strings.Join(table.FieldNames, ", ") + selectSQL := fmt.Sprintf("SELECT %v FROM %s", fieldSQL, table.TableName) + rows, err := s.Raw(selectSQL).QueryRows() + if err != nil { + return err + } + + for rows.Next() { + dest := reflect.New(destType).Elem() + var values []interface{} + for _, name := range table.FieldNames { + values = append(values, dest.FieldByName(name).Addr().Interface()) + } + if err := rows.Scan(values...); err != nil { + return err + } + destSlice.Set(reflect.Append(destSlice, dest)) + } + if err := rows.Close(); err != nil { + return err + } + return nil } diff --git a/gee-orm/day3-save-query/geeorm/session/record_test.go b/gee-orm/day3-save-query/geeorm/session/record_test.go index abf157a..f57680e 100644 --- a/gee-orm/day3-save-query/geeorm/session/record_test.go +++ b/gee-orm/day3-save-query/geeorm/session/record_test.go @@ -14,3 +14,24 @@ func TestSession_Create(t *testing.T) { t.Fatal("failed to create record") } } + +func TestSession_First(t *testing.T) { + _ = NewSession().DropTable(&User{}) + _ = NewSession().CreateTable(&User{}) + _, _ = NewSession().Create(user1) + u := &User{} + err := NewSession().First(u) + if err != nil || u.Age != user1.Age || u.Name != user1.Name { + t.Fatal("failed to query first") + } +} + +func TestSession_Find(t *testing.T) { + _ = NewSession().DropTable(&User{}) + _ = NewSession().CreateTable(&User{}) + _, _ = NewSession().Create(user1, user2) + users := []User{} + if err := NewSession().Find(&users); err != nil || len(users) != 2 { + t.Fatal("failed to query all") + } +} From 66fdda820ed4f585623ceefda5dff37f6e6c0a52 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Mon, 24 Feb 2020 22:15:26 +0800 Subject: [PATCH 041/122] refactor a better logger that supports different levels --- gee-orm/day1-database-sql/geeorm/geeorm.go | 8 ++-- gee-orm/day1-database-sql/geeorm/log/log.go | 42 +++++++++++++++++-- .../day1-database-sql/geeorm/log/log_test.go | 17 ++++++++ .../day1-database-sql/geeorm/session/raw.go | 10 ++--- .../geeorm/session/raw_test.go | 2 + gee-orm/day2-reflect-schema/geeorm/geeorm.go | 13 +++--- gee-orm/day2-reflect-schema/geeorm/log/log.go | 42 +++++++++++++++++-- .../geeorm/log/log_test.go | 17 ++++++++ .../geeorm/schema/schema.go | 4 +- .../day2-reflect-schema/geeorm/session/raw.go | 10 ++--- .../geeorm/session/raw_test.go | 2 + .../geeorm/session/schema.go | 2 +- gee-orm/day3-save-query/geeorm/geeorm.go | 13 +++--- gee-orm/day3-save-query/geeorm/log/log.go | 42 +++++++++++++++++-- .../day3-save-query/geeorm/log/log_test.go | 17 ++++++++ .../day3-save-query/geeorm/schema/schema.go | 4 +- gee-orm/day3-save-query/geeorm/session/raw.go | 10 ++--- .../geeorm/session/raw_test.go | 2 + .../day3-save-query/geeorm/session/schema.go | 2 +- 19 files changed, 206 insertions(+), 53 deletions(-) create mode 100644 gee-orm/day1-database-sql/geeorm/log/log_test.go create mode 100644 gee-orm/day2-reflect-schema/geeorm/log/log_test.go create mode 100644 gee-orm/day3-save-query/geeorm/log/log_test.go diff --git a/gee-orm/day1-database-sql/geeorm/geeorm.go b/gee-orm/day1-database-sql/geeorm/geeorm.go index 10de8b9..ae2ef44 100644 --- a/gee-orm/day1-database-sql/geeorm/geeorm.go +++ b/gee-orm/day1-database-sql/geeorm/geeorm.go @@ -17,23 +17,23 @@ type Engine struct { func NewEngine(driver, source string) (e *Engine, err error) { db, err := sql.Open(driver, source) if err != nil { - log.Error.Println(err) + log.Error(err) return } // Send a ping to make sure the database connection is alive. if err = db.Ping(); err != nil { - log.Error.Println(err) + log.Error(err) return } e = &Engine{db: db} - log.Info.Println("Connect database success") + log.Info("Connect database success") return } // Close database connection func (engine *Engine) Close() (err error) { if err = engine.db.Close(); err == nil { - log.Info.Println("Close database success") + log.Info("Close database success") } return } diff --git a/gee-orm/day1-database-sql/geeorm/log/log.go b/gee-orm/day1-database-sql/geeorm/log/log.go index 4e326f3..684a718 100644 --- a/gee-orm/day1-database-sql/geeorm/log/log.go +++ b/gee-orm/day1-database-sql/geeorm/log/log.go @@ -1,13 +1,47 @@ package log import ( + "io/ioutil" "log" "os" + "sync" ) var ( - // Error is a logger for logging error messages - Error = log.New(os.Stdout, "[error] ", log.LstdFlags|log.Lshortfile) - // Info is a logger for logging normal messages - Info = log.New(os.Stdout, "[info ] ", log.LstdFlags|log.Lshortfile) + errorLog = log.New(os.Stdout, "\033[31m[error]\033[0m ", log.LstdFlags|log.Lshortfile) + infoLog = log.New(os.Stdout, "\033[34m[info ]\033[0m ", log.LstdFlags|log.Lshortfile) + loggers = []*log.Logger{errorLog, infoLog} + mu sync.Mutex ) + +// log methods +var ( + Error = errorLog.Println + Errorf = errorLog.Printf + Info = infoLog.Println + Infof = infoLog.Printf +) + +// log levels +const ( + InfoLevel = 0 + ErrorLevel = 1 + Disabled = 9999 +) + +// SetLevel controls log level +func SetLevel(level int) { + mu.Lock() + defer mu.Unlock() + + for _, logger := range loggers { + logger.SetOutput(os.Stdout) + } + + if ErrorLevel < level { + errorLog.SetOutput(ioutil.Discard) + } + if InfoLevel < level { + infoLog.SetOutput(ioutil.Discard) + } +} diff --git a/gee-orm/day1-database-sql/geeorm/log/log_test.go b/gee-orm/day1-database-sql/geeorm/log/log_test.go new file mode 100644 index 0000000..8cd403c --- /dev/null +++ b/gee-orm/day1-database-sql/geeorm/log/log_test.go @@ -0,0 +1,17 @@ +package log + +import ( + "os" + "testing" +) + +func TestSetLevel(t *testing.T) { + SetLevel(ErrorLevel) + if infoLog.Writer() == os.Stdout || errorLog.Writer() != os.Stdout { + t.Fatal("failed to set log level") + } + SetLevel(Disabled) + if infoLog.Writer() == os.Stdout || errorLog.Writer() == os.Stdout { + t.Fatal("failed to set log level") + } +} \ No newline at end of file diff --git a/gee-orm/day1-database-sql/geeorm/session/raw.go b/gee-orm/day1-database-sql/geeorm/session/raw.go index 2d5101a..6f9693c 100644 --- a/gee-orm/day1-database-sql/geeorm/session/raw.go +++ b/gee-orm/day1-database-sql/geeorm/session/raw.go @@ -23,24 +23,24 @@ func New(db *sql.DB) *Session { // Exec raw SQL with SQLVars func (s *Session) Exec() (result sql.Result, err error) { - log.Info.Println(s.SQL.String(), s.SQLVars) + log.Info(s.SQL.String(), s.SQLVars) if result, err = s.db.Exec(s.SQL.String(), s.SQLVars...); err != nil { - log.Error.Println(err) + log.Error(err) } return } // QueryRow gets a record from db func (s *Session) QueryRow() *sql.Row { - log.Info.Println(s.SQL.String(), s.SQLVars) + log.Info(s.SQL.String(), s.SQLVars) return s.db.QueryRow(s.SQL.String(), s.SQLVars...) } // QueryRows gets a list of records from db func (s *Session) QueryRows() (rows *sql.Rows, err error) { - log.Info.Println(s.SQL.String(), s.SQLVars) + log.Info(s.SQL.String(), s.SQLVars) if rows, err = s.db.Query(s.SQL.String(), s.SQLVars...); err != nil { - log.Error.Println(err) + log.Error(err) } return } diff --git a/gee-orm/day1-database-sql/geeorm/session/raw_test.go b/gee-orm/day1-database-sql/geeorm/session/raw_test.go index 1502a7e..9fe7460 100644 --- a/gee-orm/day1-database-sql/geeorm/session/raw_test.go +++ b/gee-orm/day1-database-sql/geeorm/session/raw_test.go @@ -2,6 +2,7 @@ package session import ( "database/sql" + "geeorm/log" "os" "testing" @@ -12,6 +13,7 @@ var TestDB *sql.DB func setup() { TestDB, _ = sql.Open("sqlite3", "gee.db") + log.SetLevel(log.ErrorLevel) } func teardown() { diff --git a/gee-orm/day2-reflect-schema/geeorm/geeorm.go b/gee-orm/day2-reflect-schema/geeorm/geeorm.go index ee3ec33..61ee9e0 100644 --- a/gee-orm/day2-reflect-schema/geeorm/geeorm.go +++ b/gee-orm/day2-reflect-schema/geeorm/geeorm.go @@ -2,8 +2,6 @@ package geeorm import ( "database/sql" - "fmt" - "geeorm/dialect" "geeorm/log" "geeorm/session" @@ -20,30 +18,29 @@ type Engine struct { func NewEngine(driver, source string) (e *Engine, err error) { db, err := sql.Open(driver, source) if err != nil { - log.Error.Println(err) + log.Error(err) return } // Send a ping to make sure the database connection is alive. if err = db.Ping(); err != nil { - log.Error.Println(err) + log.Error(err) return } // make sure the specific dialect exists dial, ok := dialect.GetDialect(driver) if !ok { - err = fmt.Errorf("dialect %s Not Found", driver) - log.Error.Println(err) + log.Errorf("dialect %s Not Found", driver) return } e = &Engine{db: db, dialect: dial} - log.Info.Println("Connect database success") + log.Info("Connect database success") return } // Close database connection func (e *Engine) Close() (err error) { if err = e.db.Close(); err == nil { - log.Info.Println("Close database success") + log.Info("Close database success") } return } diff --git a/gee-orm/day2-reflect-schema/geeorm/log/log.go b/gee-orm/day2-reflect-schema/geeorm/log/log.go index 4e326f3..684a718 100644 --- a/gee-orm/day2-reflect-schema/geeorm/log/log.go +++ b/gee-orm/day2-reflect-schema/geeorm/log/log.go @@ -1,13 +1,47 @@ package log import ( + "io/ioutil" "log" "os" + "sync" ) var ( - // Error is a logger for logging error messages - Error = log.New(os.Stdout, "[error] ", log.LstdFlags|log.Lshortfile) - // Info is a logger for logging normal messages - Info = log.New(os.Stdout, "[info ] ", log.LstdFlags|log.Lshortfile) + errorLog = log.New(os.Stdout, "\033[31m[error]\033[0m ", log.LstdFlags|log.Lshortfile) + infoLog = log.New(os.Stdout, "\033[34m[info ]\033[0m ", log.LstdFlags|log.Lshortfile) + loggers = []*log.Logger{errorLog, infoLog} + mu sync.Mutex ) + +// log methods +var ( + Error = errorLog.Println + Errorf = errorLog.Printf + Info = infoLog.Println + Infof = infoLog.Printf +) + +// log levels +const ( + InfoLevel = 0 + ErrorLevel = 1 + Disabled = 9999 +) + +// SetLevel controls log level +func SetLevel(level int) { + mu.Lock() + defer mu.Unlock() + + for _, logger := range loggers { + logger.SetOutput(os.Stdout) + } + + if ErrorLevel < level { + errorLog.SetOutput(ioutil.Discard) + } + if InfoLevel < level { + infoLog.SetOutput(ioutil.Discard) + } +} diff --git a/gee-orm/day2-reflect-schema/geeorm/log/log_test.go b/gee-orm/day2-reflect-schema/geeorm/log/log_test.go new file mode 100644 index 0000000..8cd403c --- /dev/null +++ b/gee-orm/day2-reflect-schema/geeorm/log/log_test.go @@ -0,0 +1,17 @@ +package log + +import ( + "os" + "testing" +) + +func TestSetLevel(t *testing.T) { + SetLevel(ErrorLevel) + if infoLog.Writer() == os.Stdout || errorLog.Writer() != os.Stdout { + t.Fatal("failed to set log level") + } + SetLevel(Disabled) + if infoLog.Writer() == os.Stdout || errorLog.Writer() == os.Stdout { + t.Fatal("failed to set log level") + } +} \ No newline at end of file diff --git a/gee-orm/day2-reflect-schema/geeorm/schema/schema.go b/gee-orm/day2-reflect-schema/geeorm/schema/schema.go index 299368f..f942aa0 100644 --- a/gee-orm/day2-reflect-schema/geeorm/schema/schema.go +++ b/gee-orm/day2-reflect-schema/geeorm/schema/schema.go @@ -65,5 +65,5 @@ func (field *Field) String() string { // String returns readable string func (schema *Schema) String() string { - return fmt.Sprintf("TABLE %s(%v)", schema.TableName, schema.Fields) -} + return fmt.Sprintf("TABLE %s %v", schema.TableName, schema.Fields) +} \ No newline at end of file diff --git a/gee-orm/day2-reflect-schema/geeorm/session/raw.go b/gee-orm/day2-reflect-schema/geeorm/session/raw.go index 02ea7df..61be125 100644 --- a/gee-orm/day2-reflect-schema/geeorm/session/raw.go +++ b/gee-orm/day2-reflect-schema/geeorm/session/raw.go @@ -31,24 +31,24 @@ func New(db *sql.DB, dialect dialect.Dialect) *Session { // Exec raw SQL with SQLVars func (s *Session) Exec() (result sql.Result, err error) { - log.Info.Println(s.SQL.String(), s.SQLVars) + log.Info(s.SQL.String(), s.SQLVars) if result, err = s.db.Exec(s.SQL.String(), s.SQLVars...); err != nil { - log.Error.Println(err) + log.Error(err) } return } // QueryRow gets a record from db func (s *Session) QueryRow() *sql.Row { - log.Info.Println(s.SQL.String(), s.SQLVars) + log.Info(s.SQL.String(), s.SQLVars) return s.db.QueryRow(s.SQL.String(), s.SQLVars...) } // QueryRows gets a list of records from db func (s *Session) QueryRows() (rows *sql.Rows, err error) { - log.Info.Println(s.SQL.String(), s.SQLVars) + log.Info(s.SQL.String(), s.SQLVars) if rows, err = s.db.Query(s.SQL.String(), s.SQLVars...); err != nil { - log.Error.Println(err) + log.Error(err) } return } diff --git a/gee-orm/day2-reflect-schema/geeorm/session/raw_test.go b/gee-orm/day2-reflect-schema/geeorm/session/raw_test.go index bfcfb80..fd804ee 100644 --- a/gee-orm/day2-reflect-schema/geeorm/session/raw_test.go +++ b/gee-orm/day2-reflect-schema/geeorm/session/raw_test.go @@ -2,6 +2,7 @@ package session import ( "database/sql" + "geeorm/log" "os" "testing" @@ -17,6 +18,7 @@ var ( func setup() { TestDB, _ = sql.Open("sqlite3", "gee.db") TestDial, _ = dialect.GetDialect("sqlite3") + log.SetLevel(log.ErrorLevel) } func teardown() { diff --git a/gee-orm/day2-reflect-schema/geeorm/session/schema.go b/gee-orm/day2-reflect-schema/geeorm/session/schema.go index 0767d67..1ef9a80 100644 --- a/gee-orm/day2-reflect-schema/geeorm/session/schema.go +++ b/gee-orm/day2-reflect-schema/geeorm/session/schema.go @@ -53,7 +53,7 @@ func (s *Session) HasTable(value interface{}) bool { row := s.Raw(sql, values...).QueryRow() var tmp string if err := row.Scan(&tmp); err != nil { - log.Error.Println(err) + log.Error(err) } return tmp == tableName } diff --git a/gee-orm/day3-save-query/geeorm/geeorm.go b/gee-orm/day3-save-query/geeorm/geeorm.go index ee3ec33..61ee9e0 100644 --- a/gee-orm/day3-save-query/geeorm/geeorm.go +++ b/gee-orm/day3-save-query/geeorm/geeorm.go @@ -2,8 +2,6 @@ package geeorm import ( "database/sql" - "fmt" - "geeorm/dialect" "geeorm/log" "geeorm/session" @@ -20,30 +18,29 @@ type Engine struct { func NewEngine(driver, source string) (e *Engine, err error) { db, err := sql.Open(driver, source) if err != nil { - log.Error.Println(err) + log.Error(err) return } // Send a ping to make sure the database connection is alive. if err = db.Ping(); err != nil { - log.Error.Println(err) + log.Error(err) return } // make sure the specific dialect exists dial, ok := dialect.GetDialect(driver) if !ok { - err = fmt.Errorf("dialect %s Not Found", driver) - log.Error.Println(err) + log.Errorf("dialect %s Not Found", driver) return } e = &Engine{db: db, dialect: dial} - log.Info.Println("Connect database success") + log.Info("Connect database success") return } // Close database connection func (e *Engine) Close() (err error) { if err = e.db.Close(); err == nil { - log.Info.Println("Close database success") + log.Info("Close database success") } return } diff --git a/gee-orm/day3-save-query/geeorm/log/log.go b/gee-orm/day3-save-query/geeorm/log/log.go index 4e326f3..684a718 100644 --- a/gee-orm/day3-save-query/geeorm/log/log.go +++ b/gee-orm/day3-save-query/geeorm/log/log.go @@ -1,13 +1,47 @@ package log import ( + "io/ioutil" "log" "os" + "sync" ) var ( - // Error is a logger for logging error messages - Error = log.New(os.Stdout, "[error] ", log.LstdFlags|log.Lshortfile) - // Info is a logger for logging normal messages - Info = log.New(os.Stdout, "[info ] ", log.LstdFlags|log.Lshortfile) + errorLog = log.New(os.Stdout, "\033[31m[error]\033[0m ", log.LstdFlags|log.Lshortfile) + infoLog = log.New(os.Stdout, "\033[34m[info ]\033[0m ", log.LstdFlags|log.Lshortfile) + loggers = []*log.Logger{errorLog, infoLog} + mu sync.Mutex ) + +// log methods +var ( + Error = errorLog.Println + Errorf = errorLog.Printf + Info = infoLog.Println + Infof = infoLog.Printf +) + +// log levels +const ( + InfoLevel = 0 + ErrorLevel = 1 + Disabled = 9999 +) + +// SetLevel controls log level +func SetLevel(level int) { + mu.Lock() + defer mu.Unlock() + + for _, logger := range loggers { + logger.SetOutput(os.Stdout) + } + + if ErrorLevel < level { + errorLog.SetOutput(ioutil.Discard) + } + if InfoLevel < level { + infoLog.SetOutput(ioutil.Discard) + } +} diff --git a/gee-orm/day3-save-query/geeorm/log/log_test.go b/gee-orm/day3-save-query/geeorm/log/log_test.go new file mode 100644 index 0000000..8cd403c --- /dev/null +++ b/gee-orm/day3-save-query/geeorm/log/log_test.go @@ -0,0 +1,17 @@ +package log + +import ( + "os" + "testing" +) + +func TestSetLevel(t *testing.T) { + SetLevel(ErrorLevel) + if infoLog.Writer() == os.Stdout || errorLog.Writer() != os.Stdout { + t.Fatal("failed to set log level") + } + SetLevel(Disabled) + if infoLog.Writer() == os.Stdout || errorLog.Writer() == os.Stdout { + t.Fatal("failed to set log level") + } +} \ No newline at end of file diff --git a/gee-orm/day3-save-query/geeorm/schema/schema.go b/gee-orm/day3-save-query/geeorm/schema/schema.go index 299368f..f942aa0 100644 --- a/gee-orm/day3-save-query/geeorm/schema/schema.go +++ b/gee-orm/day3-save-query/geeorm/schema/schema.go @@ -65,5 +65,5 @@ func (field *Field) String() string { // String returns readable string func (schema *Schema) String() string { - return fmt.Sprintf("TABLE %s(%v)", schema.TableName, schema.Fields) -} + return fmt.Sprintf("TABLE %s %v", schema.TableName, schema.Fields) +} \ No newline at end of file diff --git a/gee-orm/day3-save-query/geeorm/session/raw.go b/gee-orm/day3-save-query/geeorm/session/raw.go index 02ea7df..61be125 100644 --- a/gee-orm/day3-save-query/geeorm/session/raw.go +++ b/gee-orm/day3-save-query/geeorm/session/raw.go @@ -31,24 +31,24 @@ func New(db *sql.DB, dialect dialect.Dialect) *Session { // Exec raw SQL with SQLVars func (s *Session) Exec() (result sql.Result, err error) { - log.Info.Println(s.SQL.String(), s.SQLVars) + log.Info(s.SQL.String(), s.SQLVars) if result, err = s.db.Exec(s.SQL.String(), s.SQLVars...); err != nil { - log.Error.Println(err) + log.Error(err) } return } // QueryRow gets a record from db func (s *Session) QueryRow() *sql.Row { - log.Info.Println(s.SQL.String(), s.SQLVars) + log.Info(s.SQL.String(), s.SQLVars) return s.db.QueryRow(s.SQL.String(), s.SQLVars...) } // QueryRows gets a list of records from db func (s *Session) QueryRows() (rows *sql.Rows, err error) { - log.Info.Println(s.SQL.String(), s.SQLVars) + log.Info(s.SQL.String(), s.SQLVars) if rows, err = s.db.Query(s.SQL.String(), s.SQLVars...); err != nil { - log.Error.Println(err) + log.Error(err) } return } diff --git a/gee-orm/day3-save-query/geeorm/session/raw_test.go b/gee-orm/day3-save-query/geeorm/session/raw_test.go index bfcfb80..fd804ee 100644 --- a/gee-orm/day3-save-query/geeorm/session/raw_test.go +++ b/gee-orm/day3-save-query/geeorm/session/raw_test.go @@ -2,6 +2,7 @@ package session import ( "database/sql" + "geeorm/log" "os" "testing" @@ -17,6 +18,7 @@ var ( func setup() { TestDB, _ = sql.Open("sqlite3", "gee.db") TestDial, _ = dialect.GetDialect("sqlite3") + log.SetLevel(log.ErrorLevel) } func teardown() { diff --git a/gee-orm/day3-save-query/geeorm/session/schema.go b/gee-orm/day3-save-query/geeorm/session/schema.go index d9135f9..c8ea3de 100644 --- a/gee-orm/day3-save-query/geeorm/session/schema.go +++ b/gee-orm/day3-save-query/geeorm/session/schema.go @@ -53,7 +53,7 @@ func (s *Session) HasTable(value interface{}) bool { row := s.Raw(sql, values...).QueryRow() var tmp string if err := row.Scan(&tmp); err != nil { - log.Error.Println(err) + log.Error(err) } return tmp == tableName } From cf3ae163e6d826db95158777e8cfefced872b8c4 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Mon, 24 Feb 2020 22:22:19 +0800 Subject: [PATCH 042/122] refactor Session, remove Value & lower SQL & SQLVars --- .../day1-database-sql/geeorm/session/raw.go | 29 ++++---- .../day2-reflect-schema/geeorm/session/raw.go | 28 ++++---- .../geeorm/session/raw_test.go | 2 - .../day3-save-query/geeorm/clause/clause.go | 46 +++++++++++++ .../geeorm/clause/clause_test.go | 32 +++++++++ .../geeorm/clause/generator.go | 68 +++++++++++++++++++ .../day3-save-query/geeorm/schema/schema.go | 4 +- gee-orm/day3-save-query/geeorm/session/raw.go | 30 ++++---- .../geeorm/session/raw_test.go | 2 - .../day3-save-query/geeorm/session/record.go | 34 +++++----- 10 files changed, 204 insertions(+), 71 deletions(-) create mode 100755 gee-orm/day3-save-query/geeorm/clause/clause.go create mode 100755 gee-orm/day3-save-query/geeorm/clause/clause_test.go create mode 100755 gee-orm/day3-save-query/geeorm/clause/generator.go diff --git a/gee-orm/day1-database-sql/geeorm/session/raw.go b/gee-orm/day1-database-sql/geeorm/session/raw.go index 6f9693c..aecc158 100644 --- a/gee-orm/day1-database-sql/geeorm/session/raw.go +++ b/gee-orm/day1-database-sql/geeorm/session/raw.go @@ -2,18 +2,15 @@ package session import ( "database/sql" - "strings" - "geeorm/log" ) // Session keep a pointer to sql.DB and provides all execution of all // kind of database operations. type Session struct { - db *sql.DB - - SQL strings.Builder - SQLVars []interface{} + db *sql.DB + sql string + sqlVars []interface{} } // New creates a instance of Session @@ -21,10 +18,10 @@ func New(db *sql.DB) *Session { return &Session{db: db} } -// Exec raw SQL with SQLVars +// Exec raw sql with sqlVars func (s *Session) Exec() (result sql.Result, err error) { - log.Info(s.SQL.String(), s.SQLVars) - if result, err = s.db.Exec(s.SQL.String(), s.SQLVars...); err != nil { + log.Info(s.sql, s.sqlVars) + if result, err = s.db.Exec(s.sql, s.sqlVars...); err != nil { log.Error(err) } return @@ -32,22 +29,22 @@ func (s *Session) Exec() (result sql.Result, err error) { // QueryRow gets a record from db func (s *Session) QueryRow() *sql.Row { - log.Info(s.SQL.String(), s.SQLVars) - return s.db.QueryRow(s.SQL.String(), s.SQLVars...) + log.Info(s.sql, s.sqlVars) + return s.db.QueryRow(s.sql, s.sqlVars...) } // QueryRows gets a list of records from db func (s *Session) QueryRows() (rows *sql.Rows, err error) { - log.Info(s.SQL.String(), s.SQLVars) - if rows, err = s.db.Query(s.SQL.String(), s.SQLVars...); err != nil { + log.Info(s.sql, s.sqlVars) + if rows, err = s.db.Query(s.sql, s.sqlVars...); err != nil { log.Error(err) } return } -// Raw appends SQL and SQLVars +// Raw appends sql and sqlVars func (s *Session) Raw(sql string, values ...interface{}) *Session { - s.SQL.WriteString(sql) - s.SQLVars = append(s.SQLVars, values...) + s.sql += sql + s.sqlVars = append(s.sqlVars, values...) return s } diff --git a/gee-orm/day2-reflect-schema/geeorm/session/raw.go b/gee-orm/day2-reflect-schema/geeorm/session/raw.go index 61be125..6892e0c 100644 --- a/gee-orm/day2-reflect-schema/geeorm/session/raw.go +++ b/gee-orm/day2-reflect-schema/geeorm/session/raw.go @@ -2,8 +2,6 @@ package session import ( "database/sql" - "strings" - "geeorm/dialect" "geeorm/log" "geeorm/schema" @@ -15,10 +13,8 @@ type Session struct { db *sql.DB dialect dialect.Dialect refTable *schema.Schema - - Value interface{} - SQL strings.Builder - SQLVars []interface{} + sql string + sqlVars []interface{} } // New creates a instance of Session @@ -29,10 +25,10 @@ func New(db *sql.DB, dialect dialect.Dialect) *Session { } } -// Exec raw SQL with SQLVars +// Exec raw sql with sqlVars func (s *Session) Exec() (result sql.Result, err error) { - log.Info(s.SQL.String(), s.SQLVars) - if result, err = s.db.Exec(s.SQL.String(), s.SQLVars...); err != nil { + log.Info(s.sql, s.sqlVars) + if result, err = s.db.Exec(s.sql, s.sqlVars...); err != nil { log.Error(err) } return @@ -40,22 +36,22 @@ func (s *Session) Exec() (result sql.Result, err error) { // QueryRow gets a record from db func (s *Session) QueryRow() *sql.Row { - log.Info(s.SQL.String(), s.SQLVars) - return s.db.QueryRow(s.SQL.String(), s.SQLVars...) + log.Info(s.sql, s.sqlVars) + return s.db.QueryRow(s.sql, s.sqlVars...) } // QueryRows gets a list of records from db func (s *Session) QueryRows() (rows *sql.Rows, err error) { - log.Info(s.SQL.String(), s.SQLVars) - if rows, err = s.db.Query(s.SQL.String(), s.SQLVars...); err != nil { + log.Info(s.sql, s.sqlVars) + if rows, err = s.db.Query(s.sql, s.sqlVars...); err != nil { log.Error(err) } return } -// Raw appends SQL and SQLVars +// Raw appends sql and sqlVars func (s *Session) Raw(sql string, values ...interface{}) *Session { - s.SQL.WriteString(sql) - s.SQLVars = append(s.SQLVars, values...) + s.sql += sql + s.sqlVars = append(s.sqlVars, values...) return s } diff --git a/gee-orm/day2-reflect-schema/geeorm/session/raw_test.go b/gee-orm/day2-reflect-schema/geeorm/session/raw_test.go index fd804ee..bfcfb80 100644 --- a/gee-orm/day2-reflect-schema/geeorm/session/raw_test.go +++ b/gee-orm/day2-reflect-schema/geeorm/session/raw_test.go @@ -2,7 +2,6 @@ package session import ( "database/sql" - "geeorm/log" "os" "testing" @@ -18,7 +17,6 @@ var ( func setup() { TestDB, _ = sql.Open("sqlite3", "gee.db") TestDial, _ = dialect.GetDialect("sqlite3") - log.SetLevel(log.ErrorLevel) } func teardown() { diff --git a/gee-orm/day3-save-query/geeorm/clause/clause.go b/gee-orm/day3-save-query/geeorm/clause/clause.go new file mode 100755 index 0000000..8ee7b16 --- /dev/null +++ b/gee-orm/day3-save-query/geeorm/clause/clause.go @@ -0,0 +1,46 @@ +package clause + +import ( + "strings" +) + +// Clause contains SQL conditions +type Clause struct { + sql map[Type]string + sqlVars map[Type][]interface{} +} + +// Type is the type of Clause +type Type int + +// Support types for Clause +const ( + INSERT Type = 0 + VALUES Type = 1 + SELECT Type = 2 + LIMIT Type = 3 +) + +// Set adds a sub clause of specific type +func (c *Clause) Set(name Type, vars ...interface{}) { + if c.sql == nil { + c.sql = make(map[Type]string) + c.sqlVars = make(map[Type][]interface{}) + } + sql, vars := generators[name](vars...) + c.sql[name] = sql + c.sqlVars[name] = vars +} + +// Build generate the final SQL and SQLVars +func (c *Clause) Build(orders []Type) (string, []interface{}) { + var sqls []string + var vars []interface{} + for _, order := range orders { + if sql, ok := c.sql[order]; ok { + sqls = append(sqls, sql) + vars = append(vars, c.sqlVars[order]...) + } + } + return strings.Join(sqls, " "), vars +} diff --git a/gee-orm/day3-save-query/geeorm/clause/clause_test.go b/gee-orm/day3-save-query/geeorm/clause/clause_test.go new file mode 100755 index 0000000..99bb513 --- /dev/null +++ b/gee-orm/day3-save-query/geeorm/clause/clause_test.go @@ -0,0 +1,32 @@ +package clause + +import ( + "reflect" + "testing" +) + +func TestClause_Set(t *testing.T) { + var clause Clause + clause.Set(INSERT, "User", "Name,Age") + sql := clause.sql[INSERT] + vars := clause.sqlVars[INSERT] + t.Log(sql, vars) + if sql != "INSERT INTO User (Name,Age)" || len(vars) != 0 { + t.Fatal("failed to get clause") + } +} + +func TestClause_Build(t *testing.T) { + var clause Clause + clause.Set(LIMIT, 3) + clause.Set(SELECT, "User", "*") + orders := []Type{SELECT, LIMIT} + sql, vars := clause.Build(orders) + t.Log(sql, vars) + if sql != "SELECT * FROM User LIMIT ?" { + t.Fatal("failed to build SQL") + } + if !reflect.DeepEqual(vars, []interface{}{3}) { + t.Fatal("failed to build SQLVars") + } +} diff --git a/gee-orm/day3-save-query/geeorm/clause/generator.go b/gee-orm/day3-save-query/geeorm/clause/generator.go new file mode 100755 index 0000000..78b4989 --- /dev/null +++ b/gee-orm/day3-save-query/geeorm/clause/generator.go @@ -0,0 +1,68 @@ +package clause + +import ( + "fmt" + "log" + "strings" +) + +type generator func(values ...interface{}) (string, []interface{}) + +var generators map[Type]generator + +func init() { + generators = make(map[Type]generator) + generators[INSERT] = _insert + generators[VALUES] = _values + generators[SELECT] = _select + generators[LIMIT] = _limit +} + +func genBindVars(num int) string { + var vars []string + for i := 0; i < num; i++ { + vars = append(vars, "?") + } + return strings.Join(vars, ", ") +} + +func _insert(values ...interface{}) (string, []interface{}) { + // INSERT INTO $tableName ($fields) + tableName := values[0] + fields := values[1] + return fmt.Sprintf("INSERT INTO %s (%v)", tableName, fields), []interface{}{} +} + +func _values(values ...interface{}) (string, []interface{}) { + // VALUES ($v1), (&v2), ... + var bindStr string + var sql strings.Builder + var vars []interface{} + sql.WriteString("VALUES ") + for i, value := range values { + v := value.([]interface{}) + if bindStr == "" { + bindStr = genBindVars(len(v)) + } + sql.WriteString(fmt.Sprintf("(%v)", bindStr)) + if i+1 != len(values) { + sql.WriteString(", ") + } + vars = append(vars, v...) + } + return sql.String(), vars + +} + +func _select(values ...interface{}) (string, []interface{}) { + // SELECT $fields FROM $tableName + tableName := values[0] + fields := values[1] + return fmt.Sprintf("SELECT %v FROM %s", fields, tableName), []interface{}{} +} + +func _limit(values ...interface{}) (string, []interface{}) { + // LIMIT $num + log.Println(values...) + return "LIMIT ?", values +} diff --git a/gee-orm/day3-save-query/geeorm/schema/schema.go b/gee-orm/day3-save-query/geeorm/schema/schema.go index f942aa0..8519fd4 100644 --- a/gee-orm/day3-save-query/geeorm/schema/schema.go +++ b/gee-orm/day3-save-query/geeorm/schema/schema.go @@ -19,7 +19,6 @@ type Schema struct { PrimaryField *Field Fields []*Field FieldNames []string - BindVars []string } // Values return the values of dest's member variables @@ -52,7 +51,6 @@ func Parse(dest interface{}, d dialect.Dialect) *Schema { } schema.Fields = append(schema.Fields, field) schema.FieldNames = append(schema.FieldNames, p.Name) - schema.BindVars = append(schema.BindVars, "?") } } return schema @@ -66,4 +64,4 @@ func (field *Field) String() string { // String returns readable string func (schema *Schema) String() string { return fmt.Sprintf("TABLE %s %v", schema.TableName, schema.Fields) -} \ No newline at end of file +} diff --git a/gee-orm/day3-save-query/geeorm/session/raw.go b/gee-orm/day3-save-query/geeorm/session/raw.go index 61be125..6cb72c9 100644 --- a/gee-orm/day3-save-query/geeorm/session/raw.go +++ b/gee-orm/day3-save-query/geeorm/session/raw.go @@ -2,8 +2,7 @@ package session import ( "database/sql" - "strings" - + "geeorm/clause" "geeorm/dialect" "geeorm/log" "geeorm/schema" @@ -15,10 +14,9 @@ type Session struct { db *sql.DB dialect dialect.Dialect refTable *schema.Schema - - Value interface{} - SQL strings.Builder - SQLVars []interface{} + clause clause.Clause + sql string + sqlVars []interface{} } // New creates a instance of Session @@ -29,10 +27,10 @@ func New(db *sql.DB, dialect dialect.Dialect) *Session { } } -// Exec raw SQL with SQLVars +// Exec raw sql with sqlVars func (s *Session) Exec() (result sql.Result, err error) { - log.Info(s.SQL.String(), s.SQLVars) - if result, err = s.db.Exec(s.SQL.String(), s.SQLVars...); err != nil { + log.Info(s.sql, s.sqlVars) + if result, err = s.db.Exec(s.sql, s.sqlVars...); err != nil { log.Error(err) } return @@ -40,22 +38,22 @@ func (s *Session) Exec() (result sql.Result, err error) { // QueryRow gets a record from db func (s *Session) QueryRow() *sql.Row { - log.Info(s.SQL.String(), s.SQLVars) - return s.db.QueryRow(s.SQL.String(), s.SQLVars...) + log.Info(s.sql, s.sqlVars) + return s.db.QueryRow(s.sql, s.sqlVars...) } // QueryRows gets a list of records from db func (s *Session) QueryRows() (rows *sql.Rows, err error) { - log.Info(s.SQL.String(), s.SQLVars) - if rows, err = s.db.Query(s.SQL.String(), s.SQLVars...); err != nil { + log.Info(s.sql, s.sqlVars) + if rows, err = s.db.Query(s.sql, s.sqlVars...); err != nil { log.Error(err) } return } -// Raw appends SQL and SQLVars +// Raw appends sql and sqlVars func (s *Session) Raw(sql string, values ...interface{}) *Session { - s.SQL.WriteString(sql) - s.SQLVars = append(s.SQLVars, values...) + s.sql += sql + s.sqlVars = append(s.sqlVars, values...) return s } diff --git a/gee-orm/day3-save-query/geeorm/session/raw_test.go b/gee-orm/day3-save-query/geeorm/session/raw_test.go index fd804ee..bfcfb80 100644 --- a/gee-orm/day3-save-query/geeorm/session/raw_test.go +++ b/gee-orm/day3-save-query/geeorm/session/raw_test.go @@ -2,7 +2,6 @@ package session import ( "database/sql" - "geeorm/log" "os" "testing" @@ -18,7 +17,6 @@ var ( func setup() { TestDB, _ = sql.Open("sqlite3", "gee.db") TestDial, _ = dialect.GetDialect("sqlite3") - log.SetLevel(log.ErrorLevel) } func teardown() { diff --git a/gee-orm/day3-save-query/geeorm/session/record.go b/gee-orm/day3-save-query/geeorm/session/record.go index 2b7c7da..3d160bf 100644 --- a/gee-orm/day3-save-query/geeorm/session/record.go +++ b/gee-orm/day3-save-query/geeorm/session/record.go @@ -1,7 +1,7 @@ package session import ( - "fmt" + "geeorm/clause" "reflect" "strings" ) @@ -9,23 +9,20 @@ import ( // Create one or more records in database func (s *Session) Create(values ...interface{}) (int64, error) { var flag bool - for i, value := range values { + recordValues := make([]interface{}, 0) + for _, value := range values { table := s.RefTable(value) - filedSQL := strings.Join(table.FieldNames, ", ") - bindVarSQL := strings.Join(table.BindVars, ", ") if !flag { - s.Raw(fmt.Sprintf("INSERT INTO %s (%v) VALUES ", table.TableName, filedSQL)) flag = true + fieldSQL := strings.Join(table.FieldNames, ", ") + s.clause.Set(clause.INSERT, table.TableName, fieldSQL) } - s.Raw(fmt.Sprintf("(%v)", bindVarSQL), table.Values(value)...) - if i == len(values)-1 { - s.Raw(";") - } else { - s.Raw(",") - } + recordValues = append(recordValues, table.Values(value)) } - result, err := s.Exec() + s.clause.Set(clause.VALUES, recordValues...) + sql, vars := s.clause.Build([]clause.Type{clause.INSERT, clause.VALUES}) + result, err := s.Raw(sql, vars...).Exec() if err != nil { return 0, err } @@ -38,8 +35,12 @@ func (s *Session) First(value interface{}) error { table := s.RefTable(value) fieldSQL := strings.Join(table.FieldNames, ", ") - selectSQL := fmt.Sprintf("SELECT %v FROM %s LIMIT 1", fieldSQL, table.TableName) - row := s.Raw(selectSQL).QueryRow() + + s.clause.Set(clause.SELECT, table.TableName, fieldSQL) + s.clause.Set(clause.LIMIT, 1) + + sql, vars := s.clause.Build([]clause.Type{clause.SELECT, clause.LIMIT}) + row := s.Raw(sql, vars...).QueryRow() dest := reflect.ValueOf(value).Elem() var values []interface{} @@ -57,8 +58,9 @@ func (s *Session) Find(values interface{}) error { table := s.RefTable(reflect.New(destType).Elem().Interface()) fieldSQL := strings.Join(table.FieldNames, ", ") - selectSQL := fmt.Sprintf("SELECT %v FROM %s", fieldSQL, table.TableName) - rows, err := s.Raw(selectSQL).QueryRows() + s.clause.Set(clause.SELECT, table.TableName, fieldSQL) + sql, vars := s.clause.Build([]clause.Type{clause.SELECT}) + rows, err := s.Raw(sql, vars...).QueryRows() if err != nil { return err } From 01b2b4c4ce035637bd7233d7a93cdbd1593ee595 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Wed, 26 Feb 2020 00:08:18 +0800 Subject: [PATCH 043/122] fix Drop table --- .../geeorm/session/raw_test.go | 29 ++++++------------ .../geeorm/session/raw_test.go | 30 +++++++------------ .../geeorm/session/schema.go | 7 ++--- .../geeorm/clause/clause_test.go | 4 +-- .../geeorm/clause/generator.go | 6 ++-- .../geeorm/session/raw_test.go | 30 +++++++------------ .../day3-save-query/geeorm/session/record.go | 20 +++---------- .../day3-save-query/geeorm/session/schema.go | 7 ++--- 8 files changed, 43 insertions(+), 90 deletions(-) diff --git a/gee-orm/day1-database-sql/geeorm/session/raw_test.go b/gee-orm/day1-database-sql/geeorm/session/raw_test.go index 9fe7460..1cd7712 100644 --- a/gee-orm/day1-database-sql/geeorm/session/raw_test.go +++ b/gee-orm/day1-database-sql/geeorm/session/raw_test.go @@ -2,7 +2,6 @@ package session import ( "database/sql" - "geeorm/log" "os" "testing" @@ -11,18 +10,10 @@ import ( var TestDB *sql.DB -func setup() { - TestDB, _ = sql.Open("sqlite3", "gee.db") - log.SetLevel(log.ErrorLevel) -} - -func teardown() { - _ = TestDB.Close() -} func TestMain(m *testing.M) { - setup() + TestDB, _ = sql.Open("sqlite3", "gee.db") code := m.Run() - teardown() + _ = TestDB.Close() os.Exit(code) } @@ -31,20 +22,18 @@ func NewSession() *Session { } func TestSession_Exec(t *testing.T) { - _, _ = NewSession().Raw("DROP TABLE USER;").Exec() - _, _ = NewSession().Raw("CREATE TABLE USER(name text);").Exec() - result, _ := NewSession(). - Raw("INSERT INTO USER(`name`) values (?), (?)", "Tom", "Sam").Exec() + _, _ = NewSession().Raw("DROP TABLE IF EXISTS User;").Exec() + _, _ = NewSession().Raw("CREATE TABLE User(name text);").Exec() + result, _ := NewSession().Raw("INSERT INTO User(`name`) values (?), (?)", "Tom", "Sam").Exec() if count, err := result.RowsAffected(); err != nil || count != 2 { t.Fatal("expect 2, but got", count) } } -func TestSession_QueryRow(t *testing.T) { - _, _ = NewSession().Raw("DROP TABLE USER;").Exec() - _, _ = NewSession().Raw("CREATE TABLE USER(name text);").Exec() - row := NewSession().Raw("SELECT count(*) FROM USER").QueryRow() - +func TestSession_QueryRows(t *testing.T) { + _, _ = NewSession().Raw("DROP TABLE IF EXISTS User;").Exec() + _, _ = NewSession().Raw("CREATE TABLE User(name text);").Exec() + row := NewSession().Raw("SELECT count(*) FROM User").QueryRow() var count int if err := row.Scan(&count); err != nil || count != 0 { t.Fatal("failed to query db", err) diff --git a/gee-orm/day2-reflect-schema/geeorm/session/raw_test.go b/gee-orm/day2-reflect-schema/geeorm/session/raw_test.go index bfcfb80..ce1e75b 100644 --- a/gee-orm/day2-reflect-schema/geeorm/session/raw_test.go +++ b/gee-orm/day2-reflect-schema/geeorm/session/raw_test.go @@ -6,27 +6,19 @@ import ( "testing" "geeorm/dialect" + _ "github.com/mattn/go-sqlite3" ) var ( - TestDB *sql.DB - TestDial dialect.Dialect -) - -func setup() { - TestDB, _ = sql.Open("sqlite3", "gee.db") + TestDB *sql.DB TestDial, _ = dialect.GetDialect("sqlite3") -} - -func teardown() { - _ = TestDB.Close() -} +) func TestMain(m *testing.M) { - setup() + TestDB, _ = sql.Open("sqlite3", "gee.db") code := m.Run() - teardown() + _ = TestDB.Close() os.Exit(code) } @@ -35,18 +27,18 @@ func NewSession() *Session { } func TestSession_Exec(t *testing.T) { - _, _ = NewSession().Raw("DROP TABLE USER;").Exec() - _, _ = NewSession().Raw("CREATE TABLE USER(name text);").Exec() - result, _ := NewSession().Raw("INSERT INTO USER(`name`) values (?), (?)", "Tom", "Sam").Exec() + _, _ = NewSession().Raw("DROP TABLE IF EXISTS User;").Exec() + _, _ = NewSession().Raw("CREATE TABLE User(name text);").Exec() + result, _ := NewSession().Raw("INSERT INTO User(`name`) values (?), (?)", "Tom", "Sam").Exec() if count, err := result.RowsAffected(); err != nil || count != 2 { t.Fatal("expect 2, but got", count) } } func TestSession_QueryRows(t *testing.T) { - _, _ = NewSession().Raw("DROP TABLE USER;").Exec() - _, _ = NewSession().Raw("CREATE TABLE USER(name text);").Exec() - row := NewSession().Raw("SELECT count(*) FROM USER").QueryRow() + _, _ = NewSession().Raw("DROP TABLE IF EXISTS User;").Exec() + _, _ = NewSession().Raw("CREATE TABLE User(name text);").Exec() + row := NewSession().Raw("SELECT count(*) FROM User").QueryRow() var count int if err := row.Scan(&count); err != nil || count != 0 { t.Fatal("failed to query db", err) diff --git a/gee-orm/day2-reflect-schema/geeorm/session/schema.go b/gee-orm/day2-reflect-schema/geeorm/session/schema.go index 1ef9a80..35c5a68 100644 --- a/gee-orm/day2-reflect-schema/geeorm/session/schema.go +++ b/gee-orm/day2-reflect-schema/geeorm/session/schema.go @@ -4,7 +4,6 @@ import ( "fmt" "strings" - "geeorm/log" "geeorm/schema" ) @@ -38,7 +37,7 @@ func (s *Session) CreateTable(value interface{}) error { // DropTable drops a table with the name of model func (s *Session) DropTable(value interface{}) error { table := s.RefTable(value) - _, err := s.Raw(fmt.Sprintf("DROP TABLE %s", table.TableName)).Exec() + _, err := s.Raw(fmt.Sprintf("DROP TABLE IF EXISTS %s", table.TableName)).Exec() return err } @@ -52,8 +51,6 @@ func (s *Session) HasTable(value interface{}) bool { sql, values := s.dialect.TableExistSQL(tableName) row := s.Raw(sql, values...).QueryRow() var tmp string - if err := row.Scan(&tmp); err != nil { - log.Error(err) - } + _ = row.Scan(&tmp) return tmp == tableName } diff --git a/gee-orm/day3-save-query/geeorm/clause/clause_test.go b/gee-orm/day3-save-query/geeorm/clause/clause_test.go index 99bb513..0f3db6d 100755 --- a/gee-orm/day3-save-query/geeorm/clause/clause_test.go +++ b/gee-orm/day3-save-query/geeorm/clause/clause_test.go @@ -7,7 +7,7 @@ import ( func TestClause_Set(t *testing.T) { var clause Clause - clause.Set(INSERT, "User", "Name,Age") + clause.Set(INSERT, "User", []string{"Name", "Age"}) sql := clause.sql[INSERT] vars := clause.sqlVars[INSERT] t.Log(sql, vars) @@ -19,7 +19,7 @@ func TestClause_Set(t *testing.T) { func TestClause_Build(t *testing.T) { var clause Clause clause.Set(LIMIT, 3) - clause.Set(SELECT, "User", "*") + clause.Set(SELECT, "User", []string{"*"}) orders := []Type{SELECT, LIMIT} sql, vars := clause.Build(orders) t.Log(sql, vars) diff --git a/gee-orm/day3-save-query/geeorm/clause/generator.go b/gee-orm/day3-save-query/geeorm/clause/generator.go index 78b4989..78608d6 100755 --- a/gee-orm/day3-save-query/geeorm/clause/generator.go +++ b/gee-orm/day3-save-query/geeorm/clause/generator.go @@ -2,7 +2,6 @@ package clause import ( "fmt" - "log" "strings" ) @@ -29,7 +28,7 @@ func genBindVars(num int) string { func _insert(values ...interface{}) (string, []interface{}) { // INSERT INTO $tableName ($fields) tableName := values[0] - fields := values[1] + fields := strings.Join(values[1].([]string), ",") return fmt.Sprintf("INSERT INTO %s (%v)", tableName, fields), []interface{}{} } @@ -57,12 +56,11 @@ func _values(values ...interface{}) (string, []interface{}) { func _select(values ...interface{}) (string, []interface{}) { // SELECT $fields FROM $tableName tableName := values[0] - fields := values[1] + fields := strings.Join(values[1].([]string), ",") return fmt.Sprintf("SELECT %v FROM %s", fields, tableName), []interface{}{} } func _limit(values ...interface{}) (string, []interface{}) { // LIMIT $num - log.Println(values...) return "LIMIT ?", values } diff --git a/gee-orm/day3-save-query/geeorm/session/raw_test.go b/gee-orm/day3-save-query/geeorm/session/raw_test.go index bfcfb80..ce1e75b 100644 --- a/gee-orm/day3-save-query/geeorm/session/raw_test.go +++ b/gee-orm/day3-save-query/geeorm/session/raw_test.go @@ -6,27 +6,19 @@ import ( "testing" "geeorm/dialect" + _ "github.com/mattn/go-sqlite3" ) var ( - TestDB *sql.DB - TestDial dialect.Dialect -) - -func setup() { - TestDB, _ = sql.Open("sqlite3", "gee.db") + TestDB *sql.DB TestDial, _ = dialect.GetDialect("sqlite3") -} - -func teardown() { - _ = TestDB.Close() -} +) func TestMain(m *testing.M) { - setup() + TestDB, _ = sql.Open("sqlite3", "gee.db") code := m.Run() - teardown() + _ = TestDB.Close() os.Exit(code) } @@ -35,18 +27,18 @@ func NewSession() *Session { } func TestSession_Exec(t *testing.T) { - _, _ = NewSession().Raw("DROP TABLE USER;").Exec() - _, _ = NewSession().Raw("CREATE TABLE USER(name text);").Exec() - result, _ := NewSession().Raw("INSERT INTO USER(`name`) values (?), (?)", "Tom", "Sam").Exec() + _, _ = NewSession().Raw("DROP TABLE IF EXISTS User;").Exec() + _, _ = NewSession().Raw("CREATE TABLE User(name text);").Exec() + result, _ := NewSession().Raw("INSERT INTO User(`name`) values (?), (?)", "Tom", "Sam").Exec() if count, err := result.RowsAffected(); err != nil || count != 2 { t.Fatal("expect 2, but got", count) } } func TestSession_QueryRows(t *testing.T) { - _, _ = NewSession().Raw("DROP TABLE USER;").Exec() - _, _ = NewSession().Raw("CREATE TABLE USER(name text);").Exec() - row := NewSession().Raw("SELECT count(*) FROM USER").QueryRow() + _, _ = NewSession().Raw("DROP TABLE IF EXISTS User;").Exec() + _, _ = NewSession().Raw("CREATE TABLE User(name text);").Exec() + row := NewSession().Raw("SELECT count(*) FROM User").QueryRow() var count int if err := row.Scan(&count); err != nil || count != 0 { t.Fatal("failed to query db", err) diff --git a/gee-orm/day3-save-query/geeorm/session/record.go b/gee-orm/day3-save-query/geeorm/session/record.go index 3d160bf..cf35552 100644 --- a/gee-orm/day3-save-query/geeorm/session/record.go +++ b/gee-orm/day3-save-query/geeorm/session/record.go @@ -3,20 +3,14 @@ package session import ( "geeorm/clause" "reflect" - "strings" ) // Create one or more records in database func (s *Session) Create(values ...interface{}) (int64, error) { - var flag bool recordValues := make([]interface{}, 0) for _, value := range values { table := s.RefTable(value) - if !flag { - flag = true - fieldSQL := strings.Join(table.FieldNames, ", ") - s.clause.Set(clause.INSERT, table.TableName, fieldSQL) - } + s.clause.Set(clause.INSERT, table.TableName, table.FieldNames) recordValues = append(recordValues, table.Values(value)) } @@ -34,9 +28,7 @@ func (s *Session) Create(values ...interface{}) (int64, error) { func (s *Session) First(value interface{}) error { table := s.RefTable(value) - fieldSQL := strings.Join(table.FieldNames, ", ") - - s.clause.Set(clause.SELECT, table.TableName, fieldSQL) + s.clause.Set(clause.SELECT, table.TableName, table.FieldNames) s.clause.Set(clause.LIMIT, 1) sql, vars := s.clause.Build([]clause.Type{clause.SELECT, clause.LIMIT}) @@ -57,8 +49,7 @@ func (s *Session) Find(values interface{}) error { destType := destSlice.Type().Elem() table := s.RefTable(reflect.New(destType).Elem().Interface()) - fieldSQL := strings.Join(table.FieldNames, ", ") - s.clause.Set(clause.SELECT, table.TableName, fieldSQL) + s.clause.Set(clause.SELECT, table.TableName, table.FieldNames) sql, vars := s.clause.Build([]clause.Type{clause.SELECT}) rows, err := s.Raw(sql, vars...).QueryRows() if err != nil { @@ -76,8 +67,5 @@ func (s *Session) Find(values interface{}) error { } destSlice.Set(reflect.Append(destSlice, dest)) } - if err := rows.Close(); err != nil { - return err - } - return nil + return rows.Close() } diff --git a/gee-orm/day3-save-query/geeorm/session/schema.go b/gee-orm/day3-save-query/geeorm/session/schema.go index c8ea3de..11ff63b 100644 --- a/gee-orm/day3-save-query/geeorm/session/schema.go +++ b/gee-orm/day3-save-query/geeorm/session/schema.go @@ -4,7 +4,6 @@ import ( "fmt" "strings" - "geeorm/log" "geeorm/schema" ) @@ -38,7 +37,7 @@ func (s *Session) CreateTable(value interface{}) error { // DropTable drops a table with the name of model func (s *Session) DropTable(value interface{}) error { table := s.RefTable(value) - _, err := s.Raw(fmt.Sprintf("DROP TABLE %s", table.TableName)).Exec() + _, err := s.Raw(fmt.Sprintf("DROP TABLE IF EXISTS %s", table.TableName)).Exec() return err } @@ -52,8 +51,6 @@ func (s *Session) HasTable(value interface{}) bool { sql, values := s.dialect.TableExistSQL(tableName) row := s.Raw(sql, values...).QueryRow() var tmp string - if err := row.Scan(&tmp); err != nil { - log.Error(err) - } + _ = row.Scan(&tmp) return tmp == tableName } From 80881c5c31caaa4e6e32d50b0f727780608474c9 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Wed, 26 Feb 2020 00:47:28 +0800 Subject: [PATCH 044/122] add day4 chain operation --- .../day3-save-query/geeorm/clause/clause.go | 12 +-- .../geeorm/clause/clause_test.go | 9 ++- .../geeorm/clause/generator.go | 12 +++ .../day3-save-query/geeorm/session/record.go | 23 +----- .../geeorm/session/record_test.go | 31 ++++--- .../geeorm/clause/clause.go | 48 +++++++++++ .../geeorm/clause/clause_test.go | 33 ++++++++ .../geeorm/clause/generator.go | 78 ++++++++++++++++++ .../geeorm/dialect/dialect.go | 22 +++++ .../geeorm/dialect/sqlite3.go | 45 +++++++++++ .../geeorm/dialect/sqlite3_test.go | 25 ++++++ gee-orm/day4-chain-operation/geeorm/geeorm.go | 51 ++++++++++++ .../geeorm/geeorm_test.go | 20 +++++ gee-orm/day4-chain-operation/geeorm/go.mod | 5 ++ .../day4-chain-operation/geeorm/log/log.go | 47 +++++++++++ .../geeorm/log/log_test.go | 17 ++++ .../geeorm/schema/schema.go | 67 ++++++++++++++++ .../geeorm/schema/schema_test.go | 36 +++++++++ .../geeorm/session/raw.go | 59 ++++++++++++++ .../geeorm/session/raw_test.go | 46 +++++++++++ .../geeorm/session/record.go | 80 +++++++++++++++++++ .../geeorm/session/record_test.go | 76 ++++++++++++++++++ .../geeorm/session/schema.go | 56 +++++++++++++ .../geeorm/session/schema_test.go | 18 +++++ 24 files changed, 870 insertions(+), 46 deletions(-) create mode 100755 gee-orm/day4-chain-operation/geeorm/clause/clause.go create mode 100755 gee-orm/day4-chain-operation/geeorm/clause/clause_test.go create mode 100755 gee-orm/day4-chain-operation/geeorm/clause/generator.go create mode 100644 gee-orm/day4-chain-operation/geeorm/dialect/dialect.go create mode 100644 gee-orm/day4-chain-operation/geeorm/dialect/sqlite3.go create mode 100644 gee-orm/day4-chain-operation/geeorm/dialect/sqlite3_test.go create mode 100644 gee-orm/day4-chain-operation/geeorm/geeorm.go create mode 100644 gee-orm/day4-chain-operation/geeorm/geeorm_test.go create mode 100644 gee-orm/day4-chain-operation/geeorm/go.mod create mode 100644 gee-orm/day4-chain-operation/geeorm/log/log.go create mode 100644 gee-orm/day4-chain-operation/geeorm/log/log_test.go create mode 100644 gee-orm/day4-chain-operation/geeorm/schema/schema.go create mode 100644 gee-orm/day4-chain-operation/geeorm/schema/schema_test.go create mode 100644 gee-orm/day4-chain-operation/geeorm/session/raw.go create mode 100644 gee-orm/day4-chain-operation/geeorm/session/raw_test.go create mode 100644 gee-orm/day4-chain-operation/geeorm/session/record.go create mode 100644 gee-orm/day4-chain-operation/geeorm/session/record_test.go create mode 100644 gee-orm/day4-chain-operation/geeorm/session/schema.go create mode 100644 gee-orm/day4-chain-operation/geeorm/session/schema_test.go diff --git a/gee-orm/day3-save-query/geeorm/clause/clause.go b/gee-orm/day3-save-query/geeorm/clause/clause.go index 8ee7b16..77d88bc 100755 --- a/gee-orm/day3-save-query/geeorm/clause/clause.go +++ b/gee-orm/day3-save-query/geeorm/clause/clause.go @@ -15,10 +15,12 @@ type Type int // Support types for Clause const ( - INSERT Type = 0 - VALUES Type = 1 - SELECT Type = 2 - LIMIT Type = 3 + INSERT Type = 0 + VALUES Type = 1 + SELECT Type = 2 + LIMIT Type = 3 + WHERE Type = 4 + ORDERBY Type = 5 ) // Set adds a sub clause of specific type @@ -33,7 +35,7 @@ func (c *Clause) Set(name Type, vars ...interface{}) { } // Build generate the final SQL and SQLVars -func (c *Clause) Build(orders []Type) (string, []interface{}) { +func (c *Clause) Build(orders ...Type) (string, []interface{}) { var sqls []string var vars []interface{} for _, order := range orders { diff --git a/gee-orm/day3-save-query/geeorm/clause/clause_test.go b/gee-orm/day3-save-query/geeorm/clause/clause_test.go index 0f3db6d..3062bbd 100755 --- a/gee-orm/day3-save-query/geeorm/clause/clause_test.go +++ b/gee-orm/day3-save-query/geeorm/clause/clause_test.go @@ -20,13 +20,14 @@ func TestClause_Build(t *testing.T) { var clause Clause clause.Set(LIMIT, 3) clause.Set(SELECT, "User", []string{"*"}) - orders := []Type{SELECT, LIMIT} - sql, vars := clause.Build(orders) + clause.Set(WHERE, "Name = ?", 18) + clause.Set(ORDERBY, "Age ASC") + sql, vars := clause.Build(SELECT, WHERE, ORDERBY, LIMIT) t.Log(sql, vars) - if sql != "SELECT * FROM User LIMIT ?" { + if sql != "SELECT * FROM User WHERE Name = ? ORDER BY Age ASC LIMIT ?" { t.Fatal("failed to build SQL") } - if !reflect.DeepEqual(vars, []interface{}{3}) { + if !reflect.DeepEqual(vars, []interface{}{18, 3}) { t.Fatal("failed to build SQLVars") } } diff --git a/gee-orm/day3-save-query/geeorm/clause/generator.go b/gee-orm/day3-save-query/geeorm/clause/generator.go index 78608d6..0ac29ee 100755 --- a/gee-orm/day3-save-query/geeorm/clause/generator.go +++ b/gee-orm/day3-save-query/geeorm/clause/generator.go @@ -15,6 +15,8 @@ func init() { generators[VALUES] = _values generators[SELECT] = _select generators[LIMIT] = _limit + generators[WHERE] = _where + generators[ORDERBY] = _orderby } func genBindVars(num int) string { @@ -64,3 +66,13 @@ func _limit(values ...interface{}) (string, []interface{}) { // LIMIT $num return "LIMIT ?", values } + +func _where(values ...interface{}) (string, []interface{}) { + // WHERE $desc + desc, vars := values[0], values[1:] + return fmt.Sprintf("WHERE %s", desc), vars +} + +func _orderby(values ...interface{}) (string, []interface{}) { + return fmt.Sprintf("ORDER BY %s", values[0]), []interface{}{} +} diff --git a/gee-orm/day3-save-query/geeorm/session/record.go b/gee-orm/day3-save-query/geeorm/session/record.go index cf35552..1a0672a 100644 --- a/gee-orm/day3-save-query/geeorm/session/record.go +++ b/gee-orm/day3-save-query/geeorm/session/record.go @@ -15,7 +15,7 @@ func (s *Session) Create(values ...interface{}) (int64, error) { } s.clause.Set(clause.VALUES, recordValues...) - sql, vars := s.clause.Build([]clause.Type{clause.INSERT, clause.VALUES}) + sql, vars := s.clause.Build(clause.INSERT, clause.VALUES) result, err := s.Raw(sql, vars...).Exec() if err != nil { return 0, err @@ -24,25 +24,6 @@ func (s *Session) Create(values ...interface{}) (int64, error) { return result.RowsAffected() } -// First gets the 1st row -func (s *Session) First(value interface{}) error { - table := s.RefTable(value) - - s.clause.Set(clause.SELECT, table.TableName, table.FieldNames) - s.clause.Set(clause.LIMIT, 1) - - sql, vars := s.clause.Build([]clause.Type{clause.SELECT, clause.LIMIT}) - row := s.Raw(sql, vars...).QueryRow() - - dest := reflect.ValueOf(value).Elem() - var values []interface{} - for _, name := range table.FieldNames { - values = append(values, dest.FieldByName(name).Addr().Interface()) - } - - return row.Scan(values...) -} - // Find gets all eligible records func (s *Session) Find(values interface{}) error { destSlice := reflect.Indirect(reflect.ValueOf(values)) @@ -50,7 +31,7 @@ func (s *Session) Find(values interface{}) error { table := s.RefTable(reflect.New(destType).Elem().Interface()) s.clause.Set(clause.SELECT, table.TableName, table.FieldNames) - sql, vars := s.clause.Build([]clause.Type{clause.SELECT}) + sql, vars := s.clause.Build(clause.SELECT, clause.WHERE, clause.ORDERBY, clause.LIMIT) rows, err := s.Raw(sql, vars...).QueryRows() if err != nil { return err diff --git a/gee-orm/day3-save-query/geeorm/session/record_test.go b/gee-orm/day3-save-query/geeorm/session/record_test.go index f57680e..27b6d77 100644 --- a/gee-orm/day3-save-query/geeorm/session/record_test.go +++ b/gee-orm/day3-save-query/geeorm/session/record_test.go @@ -5,31 +5,30 @@ import "testing" var ( user1 = &User{"Tom", 18} user2 = &User{"Sam", 25} + user3 = &User{"Jack", 25} ) -func TestSession_Create(t *testing.T) { - _ = NewSession().DropTable(&User{}) - _ = NewSession().CreateTable(&User{}) - if affected, err := NewSession().Create(user1, user2); err != nil || affected != 2 { - t.Fatal("failed to create record") +func testRecordInit(t *testing.T) { + t.Helper() + err1 := NewSession().DropTable(&User{}) + err2 := NewSession().CreateTable(&User{}) + _, err3 := NewSession().Create(user1, user2) + if err1 != nil || err2 != nil || err3 != nil { + t.Fatal("failed init test records") } + } -func TestSession_First(t *testing.T) { - _ = NewSession().DropTable(&User{}) - _ = NewSession().CreateTable(&User{}) - _, _ = NewSession().Create(user1) - u := &User{} - err := NewSession().First(u) - if err != nil || u.Age != user1.Age || u.Name != user1.Name { - t.Fatal("failed to query first") +func TestSession_Create(t *testing.T) { + testRecordInit(t) + affected, err := NewSession().Create(user3) + if err != nil || affected != 1 { + t.Fatal("failed to create record") } } func TestSession_Find(t *testing.T) { - _ = NewSession().DropTable(&User{}) - _ = NewSession().CreateTable(&User{}) - _, _ = NewSession().Create(user1, user2) + testRecordInit(t) users := []User{} if err := NewSession().Find(&users); err != nil || len(users) != 2 { t.Fatal("failed to query all") diff --git a/gee-orm/day4-chain-operation/geeorm/clause/clause.go b/gee-orm/day4-chain-operation/geeorm/clause/clause.go new file mode 100755 index 0000000..77d88bc --- /dev/null +++ b/gee-orm/day4-chain-operation/geeorm/clause/clause.go @@ -0,0 +1,48 @@ +package clause + +import ( + "strings" +) + +// Clause contains SQL conditions +type Clause struct { + sql map[Type]string + sqlVars map[Type][]interface{} +} + +// Type is the type of Clause +type Type int + +// Support types for Clause +const ( + INSERT Type = 0 + VALUES Type = 1 + SELECT Type = 2 + LIMIT Type = 3 + WHERE Type = 4 + ORDERBY Type = 5 +) + +// Set adds a sub clause of specific type +func (c *Clause) Set(name Type, vars ...interface{}) { + if c.sql == nil { + c.sql = make(map[Type]string) + c.sqlVars = make(map[Type][]interface{}) + } + sql, vars := generators[name](vars...) + c.sql[name] = sql + c.sqlVars[name] = vars +} + +// Build generate the final SQL and SQLVars +func (c *Clause) Build(orders ...Type) (string, []interface{}) { + var sqls []string + var vars []interface{} + for _, order := range orders { + if sql, ok := c.sql[order]; ok { + sqls = append(sqls, sql) + vars = append(vars, c.sqlVars[order]...) + } + } + return strings.Join(sqls, " "), vars +} diff --git a/gee-orm/day4-chain-operation/geeorm/clause/clause_test.go b/gee-orm/day4-chain-operation/geeorm/clause/clause_test.go new file mode 100755 index 0000000..3062bbd --- /dev/null +++ b/gee-orm/day4-chain-operation/geeorm/clause/clause_test.go @@ -0,0 +1,33 @@ +package clause + +import ( + "reflect" + "testing" +) + +func TestClause_Set(t *testing.T) { + var clause Clause + clause.Set(INSERT, "User", []string{"Name", "Age"}) + sql := clause.sql[INSERT] + vars := clause.sqlVars[INSERT] + t.Log(sql, vars) + if sql != "INSERT INTO User (Name,Age)" || len(vars) != 0 { + t.Fatal("failed to get clause") + } +} + +func TestClause_Build(t *testing.T) { + var clause Clause + clause.Set(LIMIT, 3) + clause.Set(SELECT, "User", []string{"*"}) + clause.Set(WHERE, "Name = ?", 18) + clause.Set(ORDERBY, "Age ASC") + sql, vars := clause.Build(SELECT, WHERE, ORDERBY, LIMIT) + t.Log(sql, vars) + if sql != "SELECT * FROM User WHERE Name = ? ORDER BY Age ASC LIMIT ?" { + t.Fatal("failed to build SQL") + } + if !reflect.DeepEqual(vars, []interface{}{18, 3}) { + t.Fatal("failed to build SQLVars") + } +} diff --git a/gee-orm/day4-chain-operation/geeorm/clause/generator.go b/gee-orm/day4-chain-operation/geeorm/clause/generator.go new file mode 100755 index 0000000..0ac29ee --- /dev/null +++ b/gee-orm/day4-chain-operation/geeorm/clause/generator.go @@ -0,0 +1,78 @@ +package clause + +import ( + "fmt" + "strings" +) + +type generator func(values ...interface{}) (string, []interface{}) + +var generators map[Type]generator + +func init() { + generators = make(map[Type]generator) + generators[INSERT] = _insert + generators[VALUES] = _values + generators[SELECT] = _select + generators[LIMIT] = _limit + generators[WHERE] = _where + generators[ORDERBY] = _orderby +} + +func genBindVars(num int) string { + var vars []string + for i := 0; i < num; i++ { + vars = append(vars, "?") + } + return strings.Join(vars, ", ") +} + +func _insert(values ...interface{}) (string, []interface{}) { + // INSERT INTO $tableName ($fields) + tableName := values[0] + fields := strings.Join(values[1].([]string), ",") + return fmt.Sprintf("INSERT INTO %s (%v)", tableName, fields), []interface{}{} +} + +func _values(values ...interface{}) (string, []interface{}) { + // VALUES ($v1), (&v2), ... + var bindStr string + var sql strings.Builder + var vars []interface{} + sql.WriteString("VALUES ") + for i, value := range values { + v := value.([]interface{}) + if bindStr == "" { + bindStr = genBindVars(len(v)) + } + sql.WriteString(fmt.Sprintf("(%v)", bindStr)) + if i+1 != len(values) { + sql.WriteString(", ") + } + vars = append(vars, v...) + } + return sql.String(), vars + +} + +func _select(values ...interface{}) (string, []interface{}) { + // SELECT $fields FROM $tableName + tableName := values[0] + fields := strings.Join(values[1].([]string), ",") + return fmt.Sprintf("SELECT %v FROM %s", fields, tableName), []interface{}{} +} + +func _limit(values ...interface{}) (string, []interface{}) { + // LIMIT $num + return "LIMIT ?", values +} + +func _where(values ...interface{}) (string, []interface{}) { + // WHERE $desc + desc, vars := values[0], values[1:] + return fmt.Sprintf("WHERE %s", desc), vars +} + +func _orderby(values ...interface{}) (string, []interface{}) { + return fmt.Sprintf("ORDER BY %s", values[0]), []interface{}{} +} diff --git a/gee-orm/day4-chain-operation/geeorm/dialect/dialect.go b/gee-orm/day4-chain-operation/geeorm/dialect/dialect.go new file mode 100644 index 0000000..4696314 --- /dev/null +++ b/gee-orm/day4-chain-operation/geeorm/dialect/dialect.go @@ -0,0 +1,22 @@ +package dialect + +import "reflect" + +var dialectsMap = map[string]Dialect{} + +// Dialect is an interface contains methods that a dialect has to implement +type Dialect interface { + DataTypeOf(typ reflect.Value) string + TableExistSQL(tableName string) (string, []interface{}) +} + +// RegisterDialect register a dialect to the global variable +func RegisterDialect(name string, dialect Dialect) { + dialectsMap[name] = dialect +} + +// Get the dialect from global variable if it exists +func GetDialect(name string) (dialect Dialect, ok bool) { + dialect, ok = dialectsMap[name] + return +} diff --git a/gee-orm/day4-chain-operation/geeorm/dialect/sqlite3.go b/gee-orm/day4-chain-operation/geeorm/dialect/sqlite3.go new file mode 100644 index 0000000..f3c3897 --- /dev/null +++ b/gee-orm/day4-chain-operation/geeorm/dialect/sqlite3.go @@ -0,0 +1,45 @@ +package dialect + +import ( + "fmt" + "reflect" + "time" +) + +type sqlite3 struct{} + +var _ Dialect = (*sqlite3)(nil) + +func init() { + RegisterDialect("sqlite3", &sqlite3{}) +} + +// Get Data Type for sqlite3 Dialect +func (s *sqlite3) DataTypeOf(typ reflect.Value) string { + switch typ.Kind() { + case reflect.Bool: + return "bool" + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uintptr: + return "integer" + case reflect.Int64, reflect.Uint64: + return "bigint" + case reflect.Float32, reflect.Float64: + return "real" + case reflect.String: + return "text" + case reflect.Array, reflect.Slice: + return "blob" + case reflect.Struct: + if _, ok := typ.Interface().(time.Time); ok { + return "datetime" + } + } + panic(fmt.Sprintf("invalid sql type %s (%s)", typ.Type().Name(), typ.Kind())) +} + +// TableExistSQL returns SQL that judge whether the table exists in database +func (s *sqlite3) TableExistSQL(tableName string) (string, []interface{}) { + args := []interface{}{tableName} + return "SELECT name FROM sqlite_master WHERE type='table' and name = ?", args +} diff --git a/gee-orm/day4-chain-operation/geeorm/dialect/sqlite3_test.go b/gee-orm/day4-chain-operation/geeorm/dialect/sqlite3_test.go new file mode 100644 index 0000000..3df5f07 --- /dev/null +++ b/gee-orm/day4-chain-operation/geeorm/dialect/sqlite3_test.go @@ -0,0 +1,25 @@ +package dialect + +import ( + "reflect" + "testing" +) + +func TestDataTypeOf(t *testing.T) { + dial := &sqlite3{} + cases := []struct { + Value interface{} + Type string + }{ + {"Tom", "text"}, + {123, "integer"}, + {1.2, "real"}, + {[]int{1, 2, 3}, "blob"}, + } + + for _, c := range cases { + if typ := dial.DataTypeOf(reflect.ValueOf(c.Value)); typ != c.Type { + t.Fatalf("expect %s, but got %s", c.Type, typ) + } + } +} diff --git a/gee-orm/day4-chain-operation/geeorm/geeorm.go b/gee-orm/day4-chain-operation/geeorm/geeorm.go new file mode 100644 index 0000000..61ee9e0 --- /dev/null +++ b/gee-orm/day4-chain-operation/geeorm/geeorm.go @@ -0,0 +1,51 @@ +package geeorm + +import ( + "database/sql" + "geeorm/dialect" + "geeorm/log" + "geeorm/session" +) + +// Engine is the main struct of geeorm, manages all db sessions and transactions. +type Engine struct { + db *sql.DB + dialect dialect.Dialect +} + +// NewEngine create a instance of Engine +// connect database and ping it to test whether it's alive +func NewEngine(driver, source string) (e *Engine, err error) { + db, err := sql.Open(driver, source) + if err != nil { + log.Error(err) + return + } + // Send a ping to make sure the database connection is alive. + if err = db.Ping(); err != nil { + log.Error(err) + return + } + // make sure the specific dialect exists + dial, ok := dialect.GetDialect(driver) + if !ok { + log.Errorf("dialect %s Not Found", driver) + return + } + e = &Engine{db: db, dialect: dial} + log.Info("Connect database success") + return +} + +// Close database connection +func (e *Engine) Close() (err error) { + if err = e.db.Close(); err == nil { + log.Info("Close database success") + } + return +} + +// NewSession creates a new session for next operations +func (e *Engine) NewSession() *session.Session { + return session.New(e.db, e.dialect) +} diff --git a/gee-orm/day4-chain-operation/geeorm/geeorm_test.go b/gee-orm/day4-chain-operation/geeorm/geeorm_test.go new file mode 100644 index 0000000..35628be --- /dev/null +++ b/gee-orm/day4-chain-operation/geeorm/geeorm_test.go @@ -0,0 +1,20 @@ +package geeorm + +import ( + _ "github.com/mattn/go-sqlite3" + "testing" +) + +func OpenDB(t *testing.T) *Engine { + t.Helper() + engine, err := NewEngine("sqlite3", "gee.db") + if err != nil { + t.Fatal("failed to connect", err) + } + return engine +} + +func TestNewEngine(t *testing.T) { + engine := OpenDB(t) + _ = engine.Close() +} diff --git a/gee-orm/day4-chain-operation/geeorm/go.mod b/gee-orm/day4-chain-operation/geeorm/go.mod new file mode 100644 index 0000000..043b1c6 --- /dev/null +++ b/gee-orm/day4-chain-operation/geeorm/go.mod @@ -0,0 +1,5 @@ +module geeorm + +go 1.13 + +require github.com/mattn/go-sqlite3 v2.0.3+incompatible diff --git a/gee-orm/day4-chain-operation/geeorm/log/log.go b/gee-orm/day4-chain-operation/geeorm/log/log.go new file mode 100644 index 0000000..684a718 --- /dev/null +++ b/gee-orm/day4-chain-operation/geeorm/log/log.go @@ -0,0 +1,47 @@ +package log + +import ( + "io/ioutil" + "log" + "os" + "sync" +) + +var ( + errorLog = log.New(os.Stdout, "\033[31m[error]\033[0m ", log.LstdFlags|log.Lshortfile) + infoLog = log.New(os.Stdout, "\033[34m[info ]\033[0m ", log.LstdFlags|log.Lshortfile) + loggers = []*log.Logger{errorLog, infoLog} + mu sync.Mutex +) + +// log methods +var ( + Error = errorLog.Println + Errorf = errorLog.Printf + Info = infoLog.Println + Infof = infoLog.Printf +) + +// log levels +const ( + InfoLevel = 0 + ErrorLevel = 1 + Disabled = 9999 +) + +// SetLevel controls log level +func SetLevel(level int) { + mu.Lock() + defer mu.Unlock() + + for _, logger := range loggers { + logger.SetOutput(os.Stdout) + } + + if ErrorLevel < level { + errorLog.SetOutput(ioutil.Discard) + } + if InfoLevel < level { + infoLog.SetOutput(ioutil.Discard) + } +} diff --git a/gee-orm/day4-chain-operation/geeorm/log/log_test.go b/gee-orm/day4-chain-operation/geeorm/log/log_test.go new file mode 100644 index 0000000..8cd403c --- /dev/null +++ b/gee-orm/day4-chain-operation/geeorm/log/log_test.go @@ -0,0 +1,17 @@ +package log + +import ( + "os" + "testing" +) + +func TestSetLevel(t *testing.T) { + SetLevel(ErrorLevel) + if infoLog.Writer() == os.Stdout || errorLog.Writer() != os.Stdout { + t.Fatal("failed to set log level") + } + SetLevel(Disabled) + if infoLog.Writer() == os.Stdout || errorLog.Writer() == os.Stdout { + t.Fatal("failed to set log level") + } +} \ No newline at end of file diff --git a/gee-orm/day4-chain-operation/geeorm/schema/schema.go b/gee-orm/day4-chain-operation/geeorm/schema/schema.go new file mode 100644 index 0000000..8519fd4 --- /dev/null +++ b/gee-orm/day4-chain-operation/geeorm/schema/schema.go @@ -0,0 +1,67 @@ +package schema + +import ( + "fmt" + "geeorm/dialect" + "go/ast" + "reflect" +) + +// Field represents a column of database +type Field struct { + Name string + Tag string +} + +// Schema represents a table of database +type Schema struct { + TableName string + PrimaryField *Field + Fields []*Field + FieldNames []string +} + +// Values return the values of dest's member variables +func (schema *Schema) Values(dest interface{}) []interface{} { + destValue := reflect.Indirect(reflect.ValueOf(dest)) + var fieldValues []interface{} + for _, field := range schema.Fields { + fieldValues = append(fieldValues, destValue.FieldByName(field.Name).Interface()) + } + return fieldValues +} + +// Parse a struct to a Schema instance +func Parse(dest interface{}, d dialect.Dialect) *Schema { + modelType := reflect.Indirect(reflect.ValueOf(dest)).Type() + schema := &Schema{ + TableName: modelType.Name(), + PrimaryField: &Field{Name: "ID", Tag: ""}, + } + + for i := 0; i < modelType.NumField(); i++ { + p := modelType.Field(i) + if !p.Anonymous && ast.IsExported(p.Name) { + field := &Field{ + Name: p.Name, + Tag: d.DataTypeOf(reflect.Indirect(reflect.New(p.Type))), + } + if v, ok := p.Tag.Lookup("geeorm"); ok && v == "primary_key" { + schema.PrimaryField = field + } + schema.Fields = append(schema.Fields, field) + schema.FieldNames = append(schema.FieldNames, p.Name) + } + } + return schema +} + +// String returns readable string +func (field *Field) String() string { + return fmt.Sprintf("(%s %s)", field.Name, field.Tag) +} + +// String returns readable string +func (schema *Schema) String() string { + return fmt.Sprintf("TABLE %s %v", schema.TableName, schema.Fields) +} diff --git a/gee-orm/day4-chain-operation/geeorm/schema/schema_test.go b/gee-orm/day4-chain-operation/geeorm/schema/schema_test.go new file mode 100644 index 0000000..aba3e0b --- /dev/null +++ b/gee-orm/day4-chain-operation/geeorm/schema/schema_test.go @@ -0,0 +1,36 @@ +package schema + +import ( + "geeorm/dialect" + "testing" +) + +type User struct { + Name string `geeorm:"primary_key"` + Age int +} + +var TestDial, _ = dialect.GetDialect("sqlite3") + +func TestParse(t *testing.T) { + schema := Parse(&User{}, TestDial) + if schema.TableName != "User" || len(schema.Fields) != 2 { + t.Fatal("failed to parse User struct") + } + if schema.PrimaryField.Name != "Name" { + t.Fatal("failed to parse primary key") + } + t.Log(schema) +} + +func TestSchema_Values(t *testing.T) { + schema := Parse(&User{}, TestDial) + values := schema.Values(&User{"Tom", 18}) + + name := values[0].(string) + age := values[1].(int) + + if name != "Tom" || age != 18 { + t.Fatal("failed to get values") + } +} diff --git a/gee-orm/day4-chain-operation/geeorm/session/raw.go b/gee-orm/day4-chain-operation/geeorm/session/raw.go new file mode 100644 index 0000000..6cb72c9 --- /dev/null +++ b/gee-orm/day4-chain-operation/geeorm/session/raw.go @@ -0,0 +1,59 @@ +package session + +import ( + "database/sql" + "geeorm/clause" + "geeorm/dialect" + "geeorm/log" + "geeorm/schema" +) + +// Session keep a pointer to sql.DB and provides all execution of all +// kind of database operations. +type Session struct { + db *sql.DB + dialect dialect.Dialect + refTable *schema.Schema + clause clause.Clause + sql string + sqlVars []interface{} +} + +// New creates a instance of Session +func New(db *sql.DB, dialect dialect.Dialect) *Session { + return &Session{ + db: db, + dialect: dialect, + } +} + +// Exec raw sql with sqlVars +func (s *Session) Exec() (result sql.Result, err error) { + log.Info(s.sql, s.sqlVars) + if result, err = s.db.Exec(s.sql, s.sqlVars...); err != nil { + log.Error(err) + } + return +} + +// QueryRow gets a record from db +func (s *Session) QueryRow() *sql.Row { + log.Info(s.sql, s.sqlVars) + return s.db.QueryRow(s.sql, s.sqlVars...) +} + +// QueryRows gets a list of records from db +func (s *Session) QueryRows() (rows *sql.Rows, err error) { + log.Info(s.sql, s.sqlVars) + if rows, err = s.db.Query(s.sql, s.sqlVars...); err != nil { + log.Error(err) + } + return +} + +// Raw appends sql and sqlVars +func (s *Session) Raw(sql string, values ...interface{}) *Session { + s.sql += sql + s.sqlVars = append(s.sqlVars, values...) + return s +} diff --git a/gee-orm/day4-chain-operation/geeorm/session/raw_test.go b/gee-orm/day4-chain-operation/geeorm/session/raw_test.go new file mode 100644 index 0000000..ce1e75b --- /dev/null +++ b/gee-orm/day4-chain-operation/geeorm/session/raw_test.go @@ -0,0 +1,46 @@ +package session + +import ( + "database/sql" + "os" + "testing" + + "geeorm/dialect" + + _ "github.com/mattn/go-sqlite3" +) + +var ( + TestDB *sql.DB + TestDial, _ = dialect.GetDialect("sqlite3") +) + +func TestMain(m *testing.M) { + TestDB, _ = sql.Open("sqlite3", "gee.db") + code := m.Run() + _ = TestDB.Close() + os.Exit(code) +} + +func NewSession() *Session { + return &Session{db: TestDB, dialect: TestDial} +} + +func TestSession_Exec(t *testing.T) { + _, _ = NewSession().Raw("DROP TABLE IF EXISTS User;").Exec() + _, _ = NewSession().Raw("CREATE TABLE User(name text);").Exec() + result, _ := NewSession().Raw("INSERT INTO User(`name`) values (?), (?)", "Tom", "Sam").Exec() + if count, err := result.RowsAffected(); err != nil || count != 2 { + t.Fatal("expect 2, but got", count) + } +} + +func TestSession_QueryRows(t *testing.T) { + _, _ = NewSession().Raw("DROP TABLE IF EXISTS User;").Exec() + _, _ = NewSession().Raw("CREATE TABLE User(name text);").Exec() + row := NewSession().Raw("SELECT count(*) FROM User").QueryRow() + var count int + if err := row.Scan(&count); err != nil || count != 0 { + t.Fatal("failed to query db", err) + } +} diff --git a/gee-orm/day4-chain-operation/geeorm/session/record.go b/gee-orm/day4-chain-operation/geeorm/session/record.go new file mode 100644 index 0000000..c9902d0 --- /dev/null +++ b/gee-orm/day4-chain-operation/geeorm/session/record.go @@ -0,0 +1,80 @@ +package session + +import ( + "geeorm/clause" + "reflect" +) + +// Create one or more records in database +func (s *Session) Create(values ...interface{}) (int64, error) { + recordValues := make([]interface{}, 0) + for _, value := range values { + table := s.RefTable(value) + s.clause.Set(clause.INSERT, table.TableName, table.FieldNames) + recordValues = append(recordValues, table.Values(value)) + } + + s.clause.Set(clause.VALUES, recordValues...) + sql, vars := s.clause.Build(clause.INSERT, clause.VALUES) + result, err := s.Raw(sql, vars...).Exec() + if err != nil { + return 0, err + } + + return result.RowsAffected() +} + +// Find gets all eligible records +func (s *Session) Find(values interface{}) error { + destSlice := reflect.Indirect(reflect.ValueOf(values)) + destType := destSlice.Type().Elem() + table := s.RefTable(reflect.New(destType).Elem().Interface()) + + s.clause.Set(clause.SELECT, table.TableName, table.FieldNames) + sql, vars := s.clause.Build(clause.SELECT, clause.WHERE, clause.ORDERBY, clause.LIMIT) + rows, err := s.Raw(sql, vars...).QueryRows() + if err != nil { + return err + } + + for rows.Next() { + dest := reflect.New(destType).Elem() + var values []interface{} + for _, name := range table.FieldNames { + values = append(values, dest.FieldByName(name).Addr().Interface()) + } + if err := rows.Scan(values...); err != nil { + return err + } + destSlice.Set(reflect.Append(destSlice, dest)) + } + return rows.Close() +} + +// First gets the 1st row +func (s *Session) First(value interface{}) error { + dest := reflect.Indirect(reflect.ValueOf(value)) + destSlice := reflect.New(reflect.SliceOf(dest.Type())).Elem() + err := s.Limit(1).Find(destSlice.Addr().Interface()) + dest.Set(destSlice.Index(0)) + return err +} + +// Limit adds limit condition to clause +func (s *Session) Limit(num int) *Session { + s.clause.Set(clause.LIMIT, num) + return s +} + +// Where adds limit condition to clause +func (s *Session) Where(desc string, args ...interface{}) *Session { + var vars []interface{} + s.clause.Set(clause.WHERE, append(append(vars, desc), args...)...) + return s +} + +// OrderBy adds order by condition to clause +func (s *Session) OrderBy(desc string) *Session { + s.clause.Set(clause.ORDERBY, desc) + return s +} diff --git a/gee-orm/day4-chain-operation/geeorm/session/record_test.go b/gee-orm/day4-chain-operation/geeorm/session/record_test.go new file mode 100644 index 0000000..5879cfb --- /dev/null +++ b/gee-orm/day4-chain-operation/geeorm/session/record_test.go @@ -0,0 +1,76 @@ +package session + +import "testing" + +var ( + user1 = &User{"Tom", 18} + user2 = &User{"Sam", 25} + user3 = &User{"Jack", 25} +) + +func testRecordInit(t *testing.T) { + t.Helper() + err1 := NewSession().DropTable(&User{}) + err2 := NewSession().CreateTable(&User{}) + _, err3 := NewSession().Create(user1, user2) + if err1 != nil || err2 != nil || err3 != nil { + t.Fatal("failed init test records") + } + +} + +func TestSession_Create(t *testing.T) { + testRecordInit(t) + affected, err := NewSession().Create(user3) + if err != nil || affected != 1 { + t.Fatal("failed to create record") + } +} + +func TestSession_Find(t *testing.T) { + testRecordInit(t) + users := []User{} + if err := NewSession().Find(&users); err != nil || len(users) != 2 { + t.Fatal("failed to query all") + } +} + +func TestSession_First(t *testing.T) { + testRecordInit(t) + u := &User{} + err := NewSession().First(u) + if err != nil || u.Name != "Tom" || u.Age != 18 { + t.Fatal("failed to query first") + } +} + +func TestSession_Limit(t *testing.T) { + testRecordInit(t) + var users []User + err := NewSession().Limit(1).Find(&users) + if err != nil || len(users) != 1 { + t.Fatal("failed to query with limit condition") + } +} + +func TestSession_Where(t *testing.T) { + testRecordInit(t) + var users []User + _, err1 := NewSession().Create(user3) + err2 := NewSession().Where("Age = ?", 25).Find(&users) + + if err1 != nil || err2 != nil || len(users) != 2 { + t.Fatal("failed to query with where condition") + } +} + +func TestSession_OrderBy(t *testing.T) { + testRecordInit(t) + u := &User{} + err := NewSession().OrderBy("Age DESC").First(u) + + if err != nil || u.Age != 25 { + t.Fatal("failed to query with order by condition") + } + +} diff --git a/gee-orm/day4-chain-operation/geeorm/session/schema.go b/gee-orm/day4-chain-operation/geeorm/session/schema.go new file mode 100644 index 0000000..11ff63b --- /dev/null +++ b/gee-orm/day4-chain-operation/geeorm/session/schema.go @@ -0,0 +1,56 @@ +package session + +import ( + "fmt" + "strings" + + "geeorm/schema" +) + +// RefTable returns a Schema instance that contains all parsed fields +func (s *Session) RefTable(value interface{}) *schema.Schema { + if value == nil { + panic("value is nil") + } + if s.refTable == nil { + s.refTable = schema.Parse(value, s.dialect) + } + return s.refTable +} + +// CreateTable create a table in database with a model +func (s *Session) CreateTable(value interface{}) error { + table := s.RefTable(value) + var columns []string + for _, field := range table.Fields { + tag := field.Tag + if field.Name == table.PrimaryField.Name { + tag += " PRIMARY KEY" + } + columns = append(columns, fmt.Sprintf("%s %s", field.Name, tag)) + } + desc := strings.Join(columns, ",") + _, err := s.Raw(fmt.Sprintf("CREATE TABLE %s (%s);", table.TableName, desc)).Exec() + return err +} + +// DropTable drops a table with the name of model +func (s *Session) DropTable(value interface{}) error { + table := s.RefTable(value) + _, err := s.Raw(fmt.Sprintf("DROP TABLE IF EXISTS %s", table.TableName)).Exec() + return err +} + +// HasTable returns true of the table exists +func (s *Session) HasTable(value interface{}) bool { + tableName, ok := value.(string) + if !ok { + tableName = s.RefTable(value).TableName + } + + sql, values := s.dialect.TableExistSQL(tableName) + row := s.Raw(sql, values...).QueryRow() + var tmp string + _ = row.Scan(&tmp) + return tmp == tableName +} diff --git a/gee-orm/day4-chain-operation/geeorm/session/schema_test.go b/gee-orm/day4-chain-operation/geeorm/session/schema_test.go new file mode 100644 index 0000000..5c934fa --- /dev/null +++ b/gee-orm/day4-chain-operation/geeorm/session/schema_test.go @@ -0,0 +1,18 @@ +package session + +import ( + "testing" +) + +type User struct { + Name string `geeorm:"primary_key"` + Age int +} + +func TestSession_CreateTable(t *testing.T) { + _ = NewSession().DropTable(&User{}) + _ = NewSession().CreateTable(&User{}) + if !NewSession().HasTable("User") { + t.Fatal("failed to create table User") + } +} From 394ea2ec27d445f657de6e82b363f7f172d4ab53 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Wed, 26 Feb 2020 20:25:39 +0800 Subject: [PATCH 045/122] remove geeorm directory --- gee-orm/day1-database-sql/{geeorm => }/geeorm.go | 0 .../day1-database-sql/{geeorm => }/geeorm_test.go | 0 gee-orm/day1-database-sql/{geeorm => }/go.mod | 0 gee-orm/day1-database-sql/{geeorm => }/log/log.go | 6 +++--- .../day1-database-sql/{geeorm => }/log/log_test.go | 0 .../day1-database-sql/{geeorm => }/session/raw.go | 0 .../{geeorm => }/session/raw_test.go | 0 .../{geeorm => }/dialect/dialect.go | 0 .../{geeorm => }/dialect/sqlite3.go | 0 .../{geeorm => }/dialect/sqlite3_test.go | 0 gee-orm/day2-reflect-schema/{geeorm => }/geeorm.go | 0 .../day2-reflect-schema/{geeorm => }/geeorm_test.go | 0 gee-orm/day2-reflect-schema/{geeorm => }/go.mod | 0 gee-orm/day2-reflect-schema/{geeorm => }/log/log.go | 6 +++--- .../day2-reflect-schema/{geeorm => }/log/log_test.go | 0 .../{geeorm => }/schema/schema.go | 0 .../{geeorm => }/schema/schema_test.go | 0 .../day2-reflect-schema/{geeorm => }/session/raw.go | 0 .../{geeorm => }/session/raw_test.go | 0 .../{geeorm/session/schema.go => session/table.go} | 0 .../session/schema_test.go => session/table_test.go} | 0 .../geeorm => day3-save-query}/clause/clause.go | 12 ++++++------ .../{geeorm => }/clause/clause_test.go | 0 .../day3-save-query/{geeorm => }/clause/generator.go | 0 .../day3-save-query/{geeorm => }/dialect/dialect.go | 0 .../day3-save-query/{geeorm => }/dialect/sqlite3.go | 0 .../{geeorm => }/dialect/sqlite3_test.go | 0 gee-orm/day3-save-query/{geeorm => }/geeorm.go | 0 gee-orm/day3-save-query/{geeorm => }/geeorm_test.go | 0 gee-orm/day3-save-query/{geeorm => }/go.mod | 0 .../geeorm => day3-save-query}/log/log.go | 6 +++--- gee-orm/day3-save-query/{geeorm => }/log/log_test.go | 0 .../day3-save-query/{geeorm => }/schema/schema.go | 0 .../{geeorm => }/schema/schema_test.go | 0 gee-orm/day3-save-query/{geeorm => }/session/raw.go | 0 .../day3-save-query/{geeorm => }/session/raw_test.go | 0 .../day3-save-query/{geeorm => }/session/record.go | 0 .../{geeorm => }/session/record_test.go | 0 .../{geeorm/session/schema.go => session/table.go} | 0 .../session/schema_test.go => session/table_test.go} | 0 .../geeorm => day4-chain-operation}/clause/clause.go | 12 ++++++------ .../{geeorm => }/clause/clause_test.go | 0 .../{geeorm => }/clause/generator.go | 0 .../{geeorm => }/dialect/dialect.go | 0 .../{geeorm => }/dialect/sqlite3.go | 0 .../{geeorm => }/dialect/sqlite3_test.go | 0 gee-orm/day4-chain-operation/{geeorm => }/geeorm.go | 0 .../day4-chain-operation/{geeorm => }/geeorm_test.go | 0 gee-orm/day4-chain-operation/{geeorm => }/go.mod | 0 .../geeorm => day4-chain-operation}/log/log.go | 6 +++--- .../{geeorm => }/log/log_test.go | 0 .../{geeorm => }/schema/schema.go | 0 .../{geeorm => }/schema/schema_test.go | 0 .../day4-chain-operation/{geeorm => }/session/raw.go | 0 .../{geeorm => }/session/raw_test.go | 0 .../{geeorm => }/session/record.go | 0 .../{geeorm => }/session/record_test.go | 0 .../{geeorm/session/schema.go => session/table.go} | 0 .../session/schema_test.go => session/table_test.go} | 0 59 files changed, 24 insertions(+), 24 deletions(-) rename gee-orm/day1-database-sql/{geeorm => }/geeorm.go (100%) rename gee-orm/day1-database-sql/{geeorm => }/geeorm_test.go (100%) rename gee-orm/day1-database-sql/{geeorm => }/go.mod (100%) rename gee-orm/day1-database-sql/{geeorm => }/log/log.go (93%) rename gee-orm/day1-database-sql/{geeorm => }/log/log_test.go (100%) rename gee-orm/day1-database-sql/{geeorm => }/session/raw.go (100%) rename gee-orm/day1-database-sql/{geeorm => }/session/raw_test.go (100%) rename gee-orm/day2-reflect-schema/{geeorm => }/dialect/dialect.go (100%) rename gee-orm/day2-reflect-schema/{geeorm => }/dialect/sqlite3.go (100%) rename gee-orm/day2-reflect-schema/{geeorm => }/dialect/sqlite3_test.go (100%) rename gee-orm/day2-reflect-schema/{geeorm => }/geeorm.go (100%) rename gee-orm/day2-reflect-schema/{geeorm => }/geeorm_test.go (100%) rename gee-orm/day2-reflect-schema/{geeorm => }/go.mod (100%) rename gee-orm/day2-reflect-schema/{geeorm => }/log/log.go (93%) rename gee-orm/day2-reflect-schema/{geeorm => }/log/log_test.go (100%) rename gee-orm/day2-reflect-schema/{geeorm => }/schema/schema.go (100%) rename gee-orm/day2-reflect-schema/{geeorm => }/schema/schema_test.go (100%) rename gee-orm/day2-reflect-schema/{geeorm => }/session/raw.go (100%) rename gee-orm/day2-reflect-schema/{geeorm => }/session/raw_test.go (100%) rename gee-orm/day2-reflect-schema/{geeorm/session/schema.go => session/table.go} (100%) rename gee-orm/day2-reflect-schema/{geeorm/session/schema_test.go => session/table_test.go} (100%) rename gee-orm/{day4-chain-operation/geeorm => day3-save-query}/clause/clause.go (88%) rename gee-orm/day3-save-query/{geeorm => }/clause/clause_test.go (100%) rename gee-orm/day3-save-query/{geeorm => }/clause/generator.go (100%) rename gee-orm/day3-save-query/{geeorm => }/dialect/dialect.go (100%) rename gee-orm/day3-save-query/{geeorm => }/dialect/sqlite3.go (100%) rename gee-orm/day3-save-query/{geeorm => }/dialect/sqlite3_test.go (100%) rename gee-orm/day3-save-query/{geeorm => }/geeorm.go (100%) rename gee-orm/day3-save-query/{geeorm => }/geeorm_test.go (100%) rename gee-orm/day3-save-query/{geeorm => }/go.mod (100%) rename gee-orm/{day4-chain-operation/geeorm => day3-save-query}/log/log.go (93%) rename gee-orm/day3-save-query/{geeorm => }/log/log_test.go (100%) rename gee-orm/day3-save-query/{geeorm => }/schema/schema.go (100%) rename gee-orm/day3-save-query/{geeorm => }/schema/schema_test.go (100%) rename gee-orm/day3-save-query/{geeorm => }/session/raw.go (100%) rename gee-orm/day3-save-query/{geeorm => }/session/raw_test.go (100%) rename gee-orm/day3-save-query/{geeorm => }/session/record.go (100%) rename gee-orm/day3-save-query/{geeorm => }/session/record_test.go (100%) rename gee-orm/day3-save-query/{geeorm/session/schema.go => session/table.go} (100%) rename gee-orm/day3-save-query/{geeorm/session/schema_test.go => session/table_test.go} (100%) rename gee-orm/{day3-save-query/geeorm => day4-chain-operation}/clause/clause.go (88%) rename gee-orm/day4-chain-operation/{geeorm => }/clause/clause_test.go (100%) rename gee-orm/day4-chain-operation/{geeorm => }/clause/generator.go (100%) rename gee-orm/day4-chain-operation/{geeorm => }/dialect/dialect.go (100%) rename gee-orm/day4-chain-operation/{geeorm => }/dialect/sqlite3.go (100%) rename gee-orm/day4-chain-operation/{geeorm => }/dialect/sqlite3_test.go (100%) rename gee-orm/day4-chain-operation/{geeorm => }/geeorm.go (100%) rename gee-orm/day4-chain-operation/{geeorm => }/geeorm_test.go (100%) rename gee-orm/day4-chain-operation/{geeorm => }/go.mod (100%) rename gee-orm/{day3-save-query/geeorm => day4-chain-operation}/log/log.go (93%) rename gee-orm/day4-chain-operation/{geeorm => }/log/log_test.go (100%) rename gee-orm/day4-chain-operation/{geeorm => }/schema/schema.go (100%) rename gee-orm/day4-chain-operation/{geeorm => }/schema/schema_test.go (100%) rename gee-orm/day4-chain-operation/{geeorm => }/session/raw.go (100%) rename gee-orm/day4-chain-operation/{geeorm => }/session/raw_test.go (100%) rename gee-orm/day4-chain-operation/{geeorm => }/session/record.go (100%) rename gee-orm/day4-chain-operation/{geeorm => }/session/record_test.go (100%) rename gee-orm/day4-chain-operation/{geeorm/session/schema.go => session/table.go} (100%) rename gee-orm/day4-chain-operation/{geeorm/session/schema_test.go => session/table_test.go} (100%) diff --git a/gee-orm/day1-database-sql/geeorm/geeorm.go b/gee-orm/day1-database-sql/geeorm.go similarity index 100% rename from gee-orm/day1-database-sql/geeorm/geeorm.go rename to gee-orm/day1-database-sql/geeorm.go diff --git a/gee-orm/day1-database-sql/geeorm/geeorm_test.go b/gee-orm/day1-database-sql/geeorm_test.go similarity index 100% rename from gee-orm/day1-database-sql/geeorm/geeorm_test.go rename to gee-orm/day1-database-sql/geeorm_test.go diff --git a/gee-orm/day1-database-sql/geeorm/go.mod b/gee-orm/day1-database-sql/go.mod similarity index 100% rename from gee-orm/day1-database-sql/geeorm/go.mod rename to gee-orm/day1-database-sql/go.mod diff --git a/gee-orm/day1-database-sql/geeorm/log/log.go b/gee-orm/day1-database-sql/log/log.go similarity index 93% rename from gee-orm/day1-database-sql/geeorm/log/log.go rename to gee-orm/day1-database-sql/log/log.go index 684a718..eacc0c6 100644 --- a/gee-orm/day1-database-sql/geeorm/log/log.go +++ b/gee-orm/day1-database-sql/log/log.go @@ -24,9 +24,9 @@ var ( // log levels const ( - InfoLevel = 0 - ErrorLevel = 1 - Disabled = 9999 + InfoLevel = iota + ErrorLevel + Disabled ) // SetLevel controls log level diff --git a/gee-orm/day1-database-sql/geeorm/log/log_test.go b/gee-orm/day1-database-sql/log/log_test.go similarity index 100% rename from gee-orm/day1-database-sql/geeorm/log/log_test.go rename to gee-orm/day1-database-sql/log/log_test.go diff --git a/gee-orm/day1-database-sql/geeorm/session/raw.go b/gee-orm/day1-database-sql/session/raw.go similarity index 100% rename from gee-orm/day1-database-sql/geeorm/session/raw.go rename to gee-orm/day1-database-sql/session/raw.go diff --git a/gee-orm/day1-database-sql/geeorm/session/raw_test.go b/gee-orm/day1-database-sql/session/raw_test.go similarity index 100% rename from gee-orm/day1-database-sql/geeorm/session/raw_test.go rename to gee-orm/day1-database-sql/session/raw_test.go diff --git a/gee-orm/day2-reflect-schema/geeorm/dialect/dialect.go b/gee-orm/day2-reflect-schema/dialect/dialect.go similarity index 100% rename from gee-orm/day2-reflect-schema/geeorm/dialect/dialect.go rename to gee-orm/day2-reflect-schema/dialect/dialect.go diff --git a/gee-orm/day2-reflect-schema/geeorm/dialect/sqlite3.go b/gee-orm/day2-reflect-schema/dialect/sqlite3.go similarity index 100% rename from gee-orm/day2-reflect-schema/geeorm/dialect/sqlite3.go rename to gee-orm/day2-reflect-schema/dialect/sqlite3.go diff --git a/gee-orm/day2-reflect-schema/geeorm/dialect/sqlite3_test.go b/gee-orm/day2-reflect-schema/dialect/sqlite3_test.go similarity index 100% rename from gee-orm/day2-reflect-schema/geeorm/dialect/sqlite3_test.go rename to gee-orm/day2-reflect-schema/dialect/sqlite3_test.go diff --git a/gee-orm/day2-reflect-schema/geeorm/geeorm.go b/gee-orm/day2-reflect-schema/geeorm.go similarity index 100% rename from gee-orm/day2-reflect-schema/geeorm/geeorm.go rename to gee-orm/day2-reflect-schema/geeorm.go diff --git a/gee-orm/day2-reflect-schema/geeorm/geeorm_test.go b/gee-orm/day2-reflect-schema/geeorm_test.go similarity index 100% rename from gee-orm/day2-reflect-schema/geeorm/geeorm_test.go rename to gee-orm/day2-reflect-schema/geeorm_test.go diff --git a/gee-orm/day2-reflect-schema/geeorm/go.mod b/gee-orm/day2-reflect-schema/go.mod similarity index 100% rename from gee-orm/day2-reflect-schema/geeorm/go.mod rename to gee-orm/day2-reflect-schema/go.mod diff --git a/gee-orm/day2-reflect-schema/geeorm/log/log.go b/gee-orm/day2-reflect-schema/log/log.go similarity index 93% rename from gee-orm/day2-reflect-schema/geeorm/log/log.go rename to gee-orm/day2-reflect-schema/log/log.go index 684a718..eacc0c6 100644 --- a/gee-orm/day2-reflect-schema/geeorm/log/log.go +++ b/gee-orm/day2-reflect-schema/log/log.go @@ -24,9 +24,9 @@ var ( // log levels const ( - InfoLevel = 0 - ErrorLevel = 1 - Disabled = 9999 + InfoLevel = iota + ErrorLevel + Disabled ) // SetLevel controls log level diff --git a/gee-orm/day2-reflect-schema/geeorm/log/log_test.go b/gee-orm/day2-reflect-schema/log/log_test.go similarity index 100% rename from gee-orm/day2-reflect-schema/geeorm/log/log_test.go rename to gee-orm/day2-reflect-schema/log/log_test.go diff --git a/gee-orm/day2-reflect-schema/geeorm/schema/schema.go b/gee-orm/day2-reflect-schema/schema/schema.go similarity index 100% rename from gee-orm/day2-reflect-schema/geeorm/schema/schema.go rename to gee-orm/day2-reflect-schema/schema/schema.go diff --git a/gee-orm/day2-reflect-schema/geeorm/schema/schema_test.go b/gee-orm/day2-reflect-schema/schema/schema_test.go similarity index 100% rename from gee-orm/day2-reflect-schema/geeorm/schema/schema_test.go rename to gee-orm/day2-reflect-schema/schema/schema_test.go diff --git a/gee-orm/day2-reflect-schema/geeorm/session/raw.go b/gee-orm/day2-reflect-schema/session/raw.go similarity index 100% rename from gee-orm/day2-reflect-schema/geeorm/session/raw.go rename to gee-orm/day2-reflect-schema/session/raw.go diff --git a/gee-orm/day2-reflect-schema/geeorm/session/raw_test.go b/gee-orm/day2-reflect-schema/session/raw_test.go similarity index 100% rename from gee-orm/day2-reflect-schema/geeorm/session/raw_test.go rename to gee-orm/day2-reflect-schema/session/raw_test.go diff --git a/gee-orm/day2-reflect-schema/geeorm/session/schema.go b/gee-orm/day2-reflect-schema/session/table.go similarity index 100% rename from gee-orm/day2-reflect-schema/geeorm/session/schema.go rename to gee-orm/day2-reflect-schema/session/table.go diff --git a/gee-orm/day2-reflect-schema/geeorm/session/schema_test.go b/gee-orm/day2-reflect-schema/session/table_test.go similarity index 100% rename from gee-orm/day2-reflect-schema/geeorm/session/schema_test.go rename to gee-orm/day2-reflect-schema/session/table_test.go diff --git a/gee-orm/day4-chain-operation/geeorm/clause/clause.go b/gee-orm/day3-save-query/clause/clause.go similarity index 88% rename from gee-orm/day4-chain-operation/geeorm/clause/clause.go rename to gee-orm/day3-save-query/clause/clause.go index 77d88bc..daa930d 100755 --- a/gee-orm/day4-chain-operation/geeorm/clause/clause.go +++ b/gee-orm/day3-save-query/clause/clause.go @@ -15,12 +15,12 @@ type Type int // Support types for Clause const ( - INSERT Type = 0 - VALUES Type = 1 - SELECT Type = 2 - LIMIT Type = 3 - WHERE Type = 4 - ORDERBY Type = 5 + INSERT Type = iota + VALUES + SELECT + LIMIT + WHERE + ORDERBY ) // Set adds a sub clause of specific type diff --git a/gee-orm/day3-save-query/geeorm/clause/clause_test.go b/gee-orm/day3-save-query/clause/clause_test.go similarity index 100% rename from gee-orm/day3-save-query/geeorm/clause/clause_test.go rename to gee-orm/day3-save-query/clause/clause_test.go diff --git a/gee-orm/day3-save-query/geeorm/clause/generator.go b/gee-orm/day3-save-query/clause/generator.go similarity index 100% rename from gee-orm/day3-save-query/geeorm/clause/generator.go rename to gee-orm/day3-save-query/clause/generator.go diff --git a/gee-orm/day3-save-query/geeorm/dialect/dialect.go b/gee-orm/day3-save-query/dialect/dialect.go similarity index 100% rename from gee-orm/day3-save-query/geeorm/dialect/dialect.go rename to gee-orm/day3-save-query/dialect/dialect.go diff --git a/gee-orm/day3-save-query/geeorm/dialect/sqlite3.go b/gee-orm/day3-save-query/dialect/sqlite3.go similarity index 100% rename from gee-orm/day3-save-query/geeorm/dialect/sqlite3.go rename to gee-orm/day3-save-query/dialect/sqlite3.go diff --git a/gee-orm/day3-save-query/geeorm/dialect/sqlite3_test.go b/gee-orm/day3-save-query/dialect/sqlite3_test.go similarity index 100% rename from gee-orm/day3-save-query/geeorm/dialect/sqlite3_test.go rename to gee-orm/day3-save-query/dialect/sqlite3_test.go diff --git a/gee-orm/day3-save-query/geeorm/geeorm.go b/gee-orm/day3-save-query/geeorm.go similarity index 100% rename from gee-orm/day3-save-query/geeorm/geeorm.go rename to gee-orm/day3-save-query/geeorm.go diff --git a/gee-orm/day3-save-query/geeorm/geeorm_test.go b/gee-orm/day3-save-query/geeorm_test.go similarity index 100% rename from gee-orm/day3-save-query/geeorm/geeorm_test.go rename to gee-orm/day3-save-query/geeorm_test.go diff --git a/gee-orm/day3-save-query/geeorm/go.mod b/gee-orm/day3-save-query/go.mod similarity index 100% rename from gee-orm/day3-save-query/geeorm/go.mod rename to gee-orm/day3-save-query/go.mod diff --git a/gee-orm/day4-chain-operation/geeorm/log/log.go b/gee-orm/day3-save-query/log/log.go similarity index 93% rename from gee-orm/day4-chain-operation/geeorm/log/log.go rename to gee-orm/day3-save-query/log/log.go index 684a718..eacc0c6 100644 --- a/gee-orm/day4-chain-operation/geeorm/log/log.go +++ b/gee-orm/day3-save-query/log/log.go @@ -24,9 +24,9 @@ var ( // log levels const ( - InfoLevel = 0 - ErrorLevel = 1 - Disabled = 9999 + InfoLevel = iota + ErrorLevel + Disabled ) // SetLevel controls log level diff --git a/gee-orm/day3-save-query/geeorm/log/log_test.go b/gee-orm/day3-save-query/log/log_test.go similarity index 100% rename from gee-orm/day3-save-query/geeorm/log/log_test.go rename to gee-orm/day3-save-query/log/log_test.go diff --git a/gee-orm/day3-save-query/geeorm/schema/schema.go b/gee-orm/day3-save-query/schema/schema.go similarity index 100% rename from gee-orm/day3-save-query/geeorm/schema/schema.go rename to gee-orm/day3-save-query/schema/schema.go diff --git a/gee-orm/day3-save-query/geeorm/schema/schema_test.go b/gee-orm/day3-save-query/schema/schema_test.go similarity index 100% rename from gee-orm/day3-save-query/geeorm/schema/schema_test.go rename to gee-orm/day3-save-query/schema/schema_test.go diff --git a/gee-orm/day3-save-query/geeorm/session/raw.go b/gee-orm/day3-save-query/session/raw.go similarity index 100% rename from gee-orm/day3-save-query/geeorm/session/raw.go rename to gee-orm/day3-save-query/session/raw.go diff --git a/gee-orm/day3-save-query/geeorm/session/raw_test.go b/gee-orm/day3-save-query/session/raw_test.go similarity index 100% rename from gee-orm/day3-save-query/geeorm/session/raw_test.go rename to gee-orm/day3-save-query/session/raw_test.go diff --git a/gee-orm/day3-save-query/geeorm/session/record.go b/gee-orm/day3-save-query/session/record.go similarity index 100% rename from gee-orm/day3-save-query/geeorm/session/record.go rename to gee-orm/day3-save-query/session/record.go diff --git a/gee-orm/day3-save-query/geeorm/session/record_test.go b/gee-orm/day3-save-query/session/record_test.go similarity index 100% rename from gee-orm/day3-save-query/geeorm/session/record_test.go rename to gee-orm/day3-save-query/session/record_test.go diff --git a/gee-orm/day3-save-query/geeorm/session/schema.go b/gee-orm/day3-save-query/session/table.go similarity index 100% rename from gee-orm/day3-save-query/geeorm/session/schema.go rename to gee-orm/day3-save-query/session/table.go diff --git a/gee-orm/day3-save-query/geeorm/session/schema_test.go b/gee-orm/day3-save-query/session/table_test.go similarity index 100% rename from gee-orm/day3-save-query/geeorm/session/schema_test.go rename to gee-orm/day3-save-query/session/table_test.go diff --git a/gee-orm/day3-save-query/geeorm/clause/clause.go b/gee-orm/day4-chain-operation/clause/clause.go similarity index 88% rename from gee-orm/day3-save-query/geeorm/clause/clause.go rename to gee-orm/day4-chain-operation/clause/clause.go index 77d88bc..daa930d 100755 --- a/gee-orm/day3-save-query/geeorm/clause/clause.go +++ b/gee-orm/day4-chain-operation/clause/clause.go @@ -15,12 +15,12 @@ type Type int // Support types for Clause const ( - INSERT Type = 0 - VALUES Type = 1 - SELECT Type = 2 - LIMIT Type = 3 - WHERE Type = 4 - ORDERBY Type = 5 + INSERT Type = iota + VALUES + SELECT + LIMIT + WHERE + ORDERBY ) // Set adds a sub clause of specific type diff --git a/gee-orm/day4-chain-operation/geeorm/clause/clause_test.go b/gee-orm/day4-chain-operation/clause/clause_test.go similarity index 100% rename from gee-orm/day4-chain-operation/geeorm/clause/clause_test.go rename to gee-orm/day4-chain-operation/clause/clause_test.go diff --git a/gee-orm/day4-chain-operation/geeorm/clause/generator.go b/gee-orm/day4-chain-operation/clause/generator.go similarity index 100% rename from gee-orm/day4-chain-operation/geeorm/clause/generator.go rename to gee-orm/day4-chain-operation/clause/generator.go diff --git a/gee-orm/day4-chain-operation/geeorm/dialect/dialect.go b/gee-orm/day4-chain-operation/dialect/dialect.go similarity index 100% rename from gee-orm/day4-chain-operation/geeorm/dialect/dialect.go rename to gee-orm/day4-chain-operation/dialect/dialect.go diff --git a/gee-orm/day4-chain-operation/geeorm/dialect/sqlite3.go b/gee-orm/day4-chain-operation/dialect/sqlite3.go similarity index 100% rename from gee-orm/day4-chain-operation/geeorm/dialect/sqlite3.go rename to gee-orm/day4-chain-operation/dialect/sqlite3.go diff --git a/gee-orm/day4-chain-operation/geeorm/dialect/sqlite3_test.go b/gee-orm/day4-chain-operation/dialect/sqlite3_test.go similarity index 100% rename from gee-orm/day4-chain-operation/geeorm/dialect/sqlite3_test.go rename to gee-orm/day4-chain-operation/dialect/sqlite3_test.go diff --git a/gee-orm/day4-chain-operation/geeorm/geeorm.go b/gee-orm/day4-chain-operation/geeorm.go similarity index 100% rename from gee-orm/day4-chain-operation/geeorm/geeorm.go rename to gee-orm/day4-chain-operation/geeorm.go diff --git a/gee-orm/day4-chain-operation/geeorm/geeorm_test.go b/gee-orm/day4-chain-operation/geeorm_test.go similarity index 100% rename from gee-orm/day4-chain-operation/geeorm/geeorm_test.go rename to gee-orm/day4-chain-operation/geeorm_test.go diff --git a/gee-orm/day4-chain-operation/geeorm/go.mod b/gee-orm/day4-chain-operation/go.mod similarity index 100% rename from gee-orm/day4-chain-operation/geeorm/go.mod rename to gee-orm/day4-chain-operation/go.mod diff --git a/gee-orm/day3-save-query/geeorm/log/log.go b/gee-orm/day4-chain-operation/log/log.go similarity index 93% rename from gee-orm/day3-save-query/geeorm/log/log.go rename to gee-orm/day4-chain-operation/log/log.go index 684a718..eacc0c6 100644 --- a/gee-orm/day3-save-query/geeorm/log/log.go +++ b/gee-orm/day4-chain-operation/log/log.go @@ -24,9 +24,9 @@ var ( // log levels const ( - InfoLevel = 0 - ErrorLevel = 1 - Disabled = 9999 + InfoLevel = iota + ErrorLevel + Disabled ) // SetLevel controls log level diff --git a/gee-orm/day4-chain-operation/geeorm/log/log_test.go b/gee-orm/day4-chain-operation/log/log_test.go similarity index 100% rename from gee-orm/day4-chain-operation/geeorm/log/log_test.go rename to gee-orm/day4-chain-operation/log/log_test.go diff --git a/gee-orm/day4-chain-operation/geeorm/schema/schema.go b/gee-orm/day4-chain-operation/schema/schema.go similarity index 100% rename from gee-orm/day4-chain-operation/geeorm/schema/schema.go rename to gee-orm/day4-chain-operation/schema/schema.go diff --git a/gee-orm/day4-chain-operation/geeorm/schema/schema_test.go b/gee-orm/day4-chain-operation/schema/schema_test.go similarity index 100% rename from gee-orm/day4-chain-operation/geeorm/schema/schema_test.go rename to gee-orm/day4-chain-operation/schema/schema_test.go diff --git a/gee-orm/day4-chain-operation/geeorm/session/raw.go b/gee-orm/day4-chain-operation/session/raw.go similarity index 100% rename from gee-orm/day4-chain-operation/geeorm/session/raw.go rename to gee-orm/day4-chain-operation/session/raw.go diff --git a/gee-orm/day4-chain-operation/geeorm/session/raw_test.go b/gee-orm/day4-chain-operation/session/raw_test.go similarity index 100% rename from gee-orm/day4-chain-operation/geeorm/session/raw_test.go rename to gee-orm/day4-chain-operation/session/raw_test.go diff --git a/gee-orm/day4-chain-operation/geeorm/session/record.go b/gee-orm/day4-chain-operation/session/record.go similarity index 100% rename from gee-orm/day4-chain-operation/geeorm/session/record.go rename to gee-orm/day4-chain-operation/session/record.go diff --git a/gee-orm/day4-chain-operation/geeorm/session/record_test.go b/gee-orm/day4-chain-operation/session/record_test.go similarity index 100% rename from gee-orm/day4-chain-operation/geeorm/session/record_test.go rename to gee-orm/day4-chain-operation/session/record_test.go diff --git a/gee-orm/day4-chain-operation/geeorm/session/schema.go b/gee-orm/day4-chain-operation/session/table.go similarity index 100% rename from gee-orm/day4-chain-operation/geeorm/session/schema.go rename to gee-orm/day4-chain-operation/session/table.go diff --git a/gee-orm/day4-chain-operation/geeorm/session/schema_test.go b/gee-orm/day4-chain-operation/session/table_test.go similarity index 100% rename from gee-orm/day4-chain-operation/geeorm/session/schema_test.go rename to gee-orm/day4-chain-operation/session/table_test.go From 8ba70f262675cac2c34a15bd150ed17a7f3e94fe Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Wed, 26 Feb 2020 21:44:27 +0800 Subject: [PATCH 046/122] add day5 update delete & count --- gee-orm/day2-reflect-schema/session/table.go | 13 +- gee-orm/day3-save-query/clause/clause.go | 0 gee-orm/day3-save-query/clause/clause_test.go | 12 +- gee-orm/day3-save-query/clause/generator.go | 0 gee-orm/day3-save-query/session/table.go | 13 +- gee-orm/day4-chain-operation/clause/clause.go | 0 .../clause/clause_test.go | 12 +- .../day4-chain-operation/clause/generator.go | 0 gee-orm/day4-chain-operation/session/table.go | 13 +- gee-orm/day5-update-delete/clause/clause.go | 52 +++++++ .../day5-update-delete/clause/clause_test.go | 76 ++++++++++ .../day5-update-delete/clause/generator.go | 106 ++++++++++++++ gee-orm/day5-update-delete/dialect/dialect.go | 22 +++ gee-orm/day5-update-delete/dialect/sqlite3.go | 45 ++++++ .../dialect/sqlite3_test.go | 25 ++++ gee-orm/day5-update-delete/geeorm.go | 51 +++++++ gee-orm/day5-update-delete/geeorm_test.go | 20 +++ gee-orm/day5-update-delete/go.mod | 5 + gee-orm/day5-update-delete/log/log.go | 47 +++++++ gee-orm/day5-update-delete/log/log_test.go | 17 +++ gee-orm/day5-update-delete/schema/schema.go | 67 +++++++++ .../day5-update-delete/schema/schema_test.go | 36 +++++ gee-orm/day5-update-delete/session/raw.go | 59 ++++++++ .../day5-update-delete/session/raw_test.go | 46 +++++++ gee-orm/day5-update-delete/session/record.go | 130 ++++++++++++++++++ .../day5-update-delete/session/record_test.go | 96 +++++++++++++ gee-orm/day5-update-delete/session/table.go | 59 ++++++++ .../day5-update-delete/session/table_test.go | 18 +++ 28 files changed, 1019 insertions(+), 21 deletions(-) mode change 100755 => 100644 gee-orm/day3-save-query/clause/clause.go mode change 100755 => 100644 gee-orm/day3-save-query/clause/clause_test.go mode change 100755 => 100644 gee-orm/day3-save-query/clause/generator.go mode change 100755 => 100644 gee-orm/day4-chain-operation/clause/clause.go mode change 100755 => 100644 gee-orm/day4-chain-operation/clause/clause_test.go mode change 100755 => 100644 gee-orm/day4-chain-operation/clause/generator.go create mode 100644 gee-orm/day5-update-delete/clause/clause.go create mode 100644 gee-orm/day5-update-delete/clause/clause_test.go create mode 100644 gee-orm/day5-update-delete/clause/generator.go create mode 100644 gee-orm/day5-update-delete/dialect/dialect.go create mode 100644 gee-orm/day5-update-delete/dialect/sqlite3.go create mode 100644 gee-orm/day5-update-delete/dialect/sqlite3_test.go create mode 100644 gee-orm/day5-update-delete/geeorm.go create mode 100644 gee-orm/day5-update-delete/geeorm_test.go create mode 100644 gee-orm/day5-update-delete/go.mod create mode 100644 gee-orm/day5-update-delete/log/log.go create mode 100644 gee-orm/day5-update-delete/log/log_test.go create mode 100644 gee-orm/day5-update-delete/schema/schema.go create mode 100644 gee-orm/day5-update-delete/schema/schema_test.go create mode 100644 gee-orm/day5-update-delete/session/raw.go create mode 100644 gee-orm/day5-update-delete/session/raw_test.go create mode 100644 gee-orm/day5-update-delete/session/record.go create mode 100644 gee-orm/day5-update-delete/session/record_test.go create mode 100644 gee-orm/day5-update-delete/session/table.go create mode 100644 gee-orm/day5-update-delete/session/table_test.go diff --git a/gee-orm/day2-reflect-schema/session/table.go b/gee-orm/day2-reflect-schema/session/table.go index 35c5a68..525e249 100644 --- a/gee-orm/day2-reflect-schema/session/table.go +++ b/gee-orm/day2-reflect-schema/session/table.go @@ -41,13 +41,16 @@ func (s *Session) DropTable(value interface{}) error { return err } -// HasTable returns true of the table exists -func (s *Session) HasTable(value interface{}) bool { - tableName, ok := value.(string) - if !ok { - tableName = s.RefTable(value).TableName +func (s *Session) guessTableName(value interface{}) string { + if tableName, ok := value.(string); ok { + return tableName } + return s.RefTable(value).TableName +} +// HasTable returns true of the table exists +func (s *Session) HasTable(value interface{}) bool { + tableName := s.guessTableName(value) sql, values := s.dialect.TableExistSQL(tableName) row := s.Raw(sql, values...).QueryRow() var tmp string diff --git a/gee-orm/day3-save-query/clause/clause.go b/gee-orm/day3-save-query/clause/clause.go old mode 100755 new mode 100644 diff --git a/gee-orm/day3-save-query/clause/clause_test.go b/gee-orm/day3-save-query/clause/clause_test.go old mode 100755 new mode 100644 index 3062bbd..f5267ca --- a/gee-orm/day3-save-query/clause/clause_test.go +++ b/gee-orm/day3-save-query/clause/clause_test.go @@ -16,18 +16,24 @@ func TestClause_Set(t *testing.T) { } } -func TestClause_Build(t *testing.T) { +func testSelect(t *testing.T) { var clause Clause clause.Set(LIMIT, 3) clause.Set(SELECT, "User", []string{"*"}) - clause.Set(WHERE, "Name = ?", 18) + clause.Set(WHERE, "Name = ?", "Tom") clause.Set(ORDERBY, "Age ASC") sql, vars := clause.Build(SELECT, WHERE, ORDERBY, LIMIT) t.Log(sql, vars) if sql != "SELECT * FROM User WHERE Name = ? ORDER BY Age ASC LIMIT ?" { t.Fatal("failed to build SQL") } - if !reflect.DeepEqual(vars, []interface{}{18, 3}) { + if !reflect.DeepEqual(vars, []interface{}{"Tom", 3}) { t.Fatal("failed to build SQLVars") } } + +func TestClause_Build(t *testing.T) { + t.Run("select", func(t *testing.T) { + testSelect(t) + }) +} diff --git a/gee-orm/day3-save-query/clause/generator.go b/gee-orm/day3-save-query/clause/generator.go old mode 100755 new mode 100644 diff --git a/gee-orm/day3-save-query/session/table.go b/gee-orm/day3-save-query/session/table.go index 11ff63b..b644d3a 100644 --- a/gee-orm/day3-save-query/session/table.go +++ b/gee-orm/day3-save-query/session/table.go @@ -41,13 +41,16 @@ func (s *Session) DropTable(value interface{}) error { return err } -// HasTable returns true of the table exists -func (s *Session) HasTable(value interface{}) bool { - tableName, ok := value.(string) - if !ok { - tableName = s.RefTable(value).TableName +func (s *Session) guessTableName(value interface{}) string { + if tableName, ok := value.(string); ok { + return tableName } + return s.RefTable(value).TableName +} +// HasTable returns true of the table exists +func (s *Session) HasTable(value interface{}) bool { + tableName := s.guessTableName(value) sql, values := s.dialect.TableExistSQL(tableName) row := s.Raw(sql, values...).QueryRow() var tmp string diff --git a/gee-orm/day4-chain-operation/clause/clause.go b/gee-orm/day4-chain-operation/clause/clause.go old mode 100755 new mode 100644 diff --git a/gee-orm/day4-chain-operation/clause/clause_test.go b/gee-orm/day4-chain-operation/clause/clause_test.go old mode 100755 new mode 100644 index 3062bbd..f5267ca --- a/gee-orm/day4-chain-operation/clause/clause_test.go +++ b/gee-orm/day4-chain-operation/clause/clause_test.go @@ -16,18 +16,24 @@ func TestClause_Set(t *testing.T) { } } -func TestClause_Build(t *testing.T) { +func testSelect(t *testing.T) { var clause Clause clause.Set(LIMIT, 3) clause.Set(SELECT, "User", []string{"*"}) - clause.Set(WHERE, "Name = ?", 18) + clause.Set(WHERE, "Name = ?", "Tom") clause.Set(ORDERBY, "Age ASC") sql, vars := clause.Build(SELECT, WHERE, ORDERBY, LIMIT) t.Log(sql, vars) if sql != "SELECT * FROM User WHERE Name = ? ORDER BY Age ASC LIMIT ?" { t.Fatal("failed to build SQL") } - if !reflect.DeepEqual(vars, []interface{}{18, 3}) { + if !reflect.DeepEqual(vars, []interface{}{"Tom", 3}) { t.Fatal("failed to build SQLVars") } } + +func TestClause_Build(t *testing.T) { + t.Run("select", func(t *testing.T) { + testSelect(t) + }) +} diff --git a/gee-orm/day4-chain-operation/clause/generator.go b/gee-orm/day4-chain-operation/clause/generator.go old mode 100755 new mode 100644 diff --git a/gee-orm/day4-chain-operation/session/table.go b/gee-orm/day4-chain-operation/session/table.go index 11ff63b..b644d3a 100644 --- a/gee-orm/day4-chain-operation/session/table.go +++ b/gee-orm/day4-chain-operation/session/table.go @@ -41,13 +41,16 @@ func (s *Session) DropTable(value interface{}) error { return err } -// HasTable returns true of the table exists -func (s *Session) HasTable(value interface{}) bool { - tableName, ok := value.(string) - if !ok { - tableName = s.RefTable(value).TableName +func (s *Session) guessTableName(value interface{}) string { + if tableName, ok := value.(string); ok { + return tableName } + return s.RefTable(value).TableName +} +// HasTable returns true of the table exists +func (s *Session) HasTable(value interface{}) bool { + tableName := s.guessTableName(value) sql, values := s.dialect.TableExistSQL(tableName) row := s.Raw(sql, values...).QueryRow() var tmp string diff --git a/gee-orm/day5-update-delete/clause/clause.go b/gee-orm/day5-update-delete/clause/clause.go new file mode 100644 index 0000000..2195c86 --- /dev/null +++ b/gee-orm/day5-update-delete/clause/clause.go @@ -0,0 +1,52 @@ +package clause + +import ( + "strings" +) + +// Clause contains SQL conditions +type Clause struct { + sql map[Type]string + sqlVars map[Type][]interface{} +} + +// Type is the type of Clause +type Type int + +// Support types for Clause +const ( + INSERT Type = iota + VALUES + SELECT + LIMIT + WHERE + ORDERBY + UPDATE + SET + DELETE + COUNT +) + +// Set adds a sub clause of specific type +func (c *Clause) Set(name Type, vars ...interface{}) { + if c.sql == nil { + c.sql = make(map[Type]string) + c.sqlVars = make(map[Type][]interface{}) + } + sql, vars := generators[name](vars...) + c.sql[name] = sql + c.sqlVars[name] = vars +} + +// Build generate the final SQL and SQLVars +func (c *Clause) Build(orders ...Type) (string, []interface{}) { + var sqls []string + var vars []interface{} + for _, order := range orders { + if sql, ok := c.sql[order]; ok { + sqls = append(sqls, sql) + vars = append(vars, c.sqlVars[order]...) + } + } + return strings.Join(sqls, " "), vars +} diff --git a/gee-orm/day5-update-delete/clause/clause_test.go b/gee-orm/day5-update-delete/clause/clause_test.go new file mode 100644 index 0000000..8769c40 --- /dev/null +++ b/gee-orm/day5-update-delete/clause/clause_test.go @@ -0,0 +1,76 @@ +package clause + +import ( + "reflect" + "testing" +) + +func TestClause_Set(t *testing.T) { + var clause Clause + clause.Set(INSERT, "User", []string{"Name", "Age"}) + sql := clause.sql[INSERT] + vars := clause.sqlVars[INSERT] + t.Log(sql, vars) + if sql != "INSERT INTO User (Name,Age)" || len(vars) != 0 { + t.Fatal("failed to get clause") + } +} + +func testSelect(t *testing.T) { + var clause Clause + clause.Set(LIMIT, 3) + clause.Set(SELECT, "User", []string{"*"}) + clause.Set(WHERE, "Name = ?", "Tom") + clause.Set(ORDERBY, "Age ASC") + sql, vars := clause.Build(SELECT, WHERE, ORDERBY, LIMIT) + t.Log(sql, vars) + if sql != "SELECT * FROM User WHERE Name = ? ORDER BY Age ASC LIMIT ?" { + t.Fatal("failed to build SQL") + } + if !reflect.DeepEqual(vars, []interface{}{"Tom", 3}) { + t.Fatal("failed to build SQLVars") + } +} + +func testUpdate(t *testing.T) { + var clause Clause + clause.Set(UPDATE, "User") + clause.Set(WHERE, "Name = ?", "Tom") + clause.Set(SET, map[string]interface{}{"Age": 30, "Name": "Tommy"}) + + sql, vars := clause.Build(UPDATE, SET, WHERE) + t.Log(sql, vars) + if sql != "UPDATE User SET Age = ?, Name = ? WHERE Name = ?" { + t.Fatal("failed to build SQL") + } + if !reflect.DeepEqual(vars, []interface{}{30, "Tommy", "Tom"}) { + t.Fatal("failed to build SQLVars") + } +} + +func testDelete(t *testing.T) { + var clause Clause + clause.Set(DELETE, "User") + clause.Set(WHERE, "Name = ?", "Tom") + + sql, vars := clause.Build(DELETE, WHERE) + t.Log(sql, vars) + if sql != "DELETE FROM User WHERE Name = ?" { + t.Fatal("failed to build SQL") + } + if !reflect.DeepEqual(vars, []interface{}{"Tom"}) { + t.Fatal("failed to build SQLVars") + } +} + +func TestClause_Build(t *testing.T) { + t.Run("select", func(t *testing.T) { + testSelect(t) + }) + t.Run("update", func(t *testing.T) { + testUpdate(t) + }) + t.Run("delete", func(t *testing.T) { + testDelete(t) + }) +} diff --git a/gee-orm/day5-update-delete/clause/generator.go b/gee-orm/day5-update-delete/clause/generator.go new file mode 100644 index 0000000..1cf5aec --- /dev/null +++ b/gee-orm/day5-update-delete/clause/generator.go @@ -0,0 +1,106 @@ +package clause + +import ( + "fmt" + "strings" +) + +type generator func(values ...interface{}) (string, []interface{}) + +var generators map[Type]generator + +func init() { + generators = make(map[Type]generator) + generators[INSERT] = _insert + generators[VALUES] = _values + generators[SELECT] = _select + generators[LIMIT] = _limit + generators[WHERE] = _where + generators[ORDERBY] = _orderby + generators[UPDATE] = _update + generators[SET] = _set + generators[DELETE] = _delete + generators[COUNT] = _count +} + +func genBindVars(num int) string { + var vars []string + for i := 0; i < num; i++ { + vars = append(vars, "?") + } + return strings.Join(vars, ", ") +} + +func _insert(values ...interface{}) (string, []interface{}) { + // INSERT INTO $tableName ($fields) + tableName := values[0] + fields := strings.Join(values[1].([]string), ",") + return fmt.Sprintf("INSERT INTO %s (%v)", tableName, fields), []interface{}{} +} + +func _values(values ...interface{}) (string, []interface{}) { + // VALUES ($v1), (&v2), ... + var bindStr string + var sql strings.Builder + var vars []interface{} + sql.WriteString("VALUES ") + for i, value := range values { + v := value.([]interface{}) + if bindStr == "" { + bindStr = genBindVars(len(v)) + } + sql.WriteString(fmt.Sprintf("(%v)", bindStr)) + if i+1 != len(values) { + sql.WriteString(", ") + } + vars = append(vars, v...) + } + return sql.String(), vars + +} + +func _select(values ...interface{}) (string, []interface{}) { + // SELECT $fields FROM $tableName + tableName := values[0] + fields := strings.Join(values[1].([]string), ",") + return fmt.Sprintf("SELECT %v FROM %s", fields, tableName), []interface{}{} +} + +func _limit(values ...interface{}) (string, []interface{}) { + // LIMIT $num + return "LIMIT ?", values +} + +func _where(values ...interface{}) (string, []interface{}) { + // WHERE $desc + desc, vars := values[0], values[1:] + return fmt.Sprintf("WHERE %s", desc), vars +} + +func _orderby(values ...interface{}) (string, []interface{}) { + return fmt.Sprintf("ORDER BY %s", values[0]), []interface{}{} +} + +func _update(values ...interface{}) (string, []interface{}) { + return fmt.Sprintf("UPDATE %s", values[0]), []interface{}{} +} + +func _set(values ...interface{}) (string, []interface{}) { + m := values[0].(map[string]interface{}) + var keys []string + var vars []interface{} + for k, v := range m { + keys = append(keys, k+" = ?") + vars = append(vars, v) + } + + return fmt.Sprintf("SET %s", strings.Join(keys, ", ")), vars +} + +func _delete(values ...interface{}) (string, []interface{}) { + return fmt.Sprintf("DELETE FROM %s", values[0]), []interface{}{} +} + +func _count(values ...interface{}) (string, []interface{}) { + return _select(values[0], []string{"count(*)"}) +} diff --git a/gee-orm/day5-update-delete/dialect/dialect.go b/gee-orm/day5-update-delete/dialect/dialect.go new file mode 100644 index 0000000..4696314 --- /dev/null +++ b/gee-orm/day5-update-delete/dialect/dialect.go @@ -0,0 +1,22 @@ +package dialect + +import "reflect" + +var dialectsMap = map[string]Dialect{} + +// Dialect is an interface contains methods that a dialect has to implement +type Dialect interface { + DataTypeOf(typ reflect.Value) string + TableExistSQL(tableName string) (string, []interface{}) +} + +// RegisterDialect register a dialect to the global variable +func RegisterDialect(name string, dialect Dialect) { + dialectsMap[name] = dialect +} + +// Get the dialect from global variable if it exists +func GetDialect(name string) (dialect Dialect, ok bool) { + dialect, ok = dialectsMap[name] + return +} diff --git a/gee-orm/day5-update-delete/dialect/sqlite3.go b/gee-orm/day5-update-delete/dialect/sqlite3.go new file mode 100644 index 0000000..f3c3897 --- /dev/null +++ b/gee-orm/day5-update-delete/dialect/sqlite3.go @@ -0,0 +1,45 @@ +package dialect + +import ( + "fmt" + "reflect" + "time" +) + +type sqlite3 struct{} + +var _ Dialect = (*sqlite3)(nil) + +func init() { + RegisterDialect("sqlite3", &sqlite3{}) +} + +// Get Data Type for sqlite3 Dialect +func (s *sqlite3) DataTypeOf(typ reflect.Value) string { + switch typ.Kind() { + case reflect.Bool: + return "bool" + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uintptr: + return "integer" + case reflect.Int64, reflect.Uint64: + return "bigint" + case reflect.Float32, reflect.Float64: + return "real" + case reflect.String: + return "text" + case reflect.Array, reflect.Slice: + return "blob" + case reflect.Struct: + if _, ok := typ.Interface().(time.Time); ok { + return "datetime" + } + } + panic(fmt.Sprintf("invalid sql type %s (%s)", typ.Type().Name(), typ.Kind())) +} + +// TableExistSQL returns SQL that judge whether the table exists in database +func (s *sqlite3) TableExistSQL(tableName string) (string, []interface{}) { + args := []interface{}{tableName} + return "SELECT name FROM sqlite_master WHERE type='table' and name = ?", args +} diff --git a/gee-orm/day5-update-delete/dialect/sqlite3_test.go b/gee-orm/day5-update-delete/dialect/sqlite3_test.go new file mode 100644 index 0000000..3df5f07 --- /dev/null +++ b/gee-orm/day5-update-delete/dialect/sqlite3_test.go @@ -0,0 +1,25 @@ +package dialect + +import ( + "reflect" + "testing" +) + +func TestDataTypeOf(t *testing.T) { + dial := &sqlite3{} + cases := []struct { + Value interface{} + Type string + }{ + {"Tom", "text"}, + {123, "integer"}, + {1.2, "real"}, + {[]int{1, 2, 3}, "blob"}, + } + + for _, c := range cases { + if typ := dial.DataTypeOf(reflect.ValueOf(c.Value)); typ != c.Type { + t.Fatalf("expect %s, but got %s", c.Type, typ) + } + } +} diff --git a/gee-orm/day5-update-delete/geeorm.go b/gee-orm/day5-update-delete/geeorm.go new file mode 100644 index 0000000..61ee9e0 --- /dev/null +++ b/gee-orm/day5-update-delete/geeorm.go @@ -0,0 +1,51 @@ +package geeorm + +import ( + "database/sql" + "geeorm/dialect" + "geeorm/log" + "geeorm/session" +) + +// Engine is the main struct of geeorm, manages all db sessions and transactions. +type Engine struct { + db *sql.DB + dialect dialect.Dialect +} + +// NewEngine create a instance of Engine +// connect database and ping it to test whether it's alive +func NewEngine(driver, source string) (e *Engine, err error) { + db, err := sql.Open(driver, source) + if err != nil { + log.Error(err) + return + } + // Send a ping to make sure the database connection is alive. + if err = db.Ping(); err != nil { + log.Error(err) + return + } + // make sure the specific dialect exists + dial, ok := dialect.GetDialect(driver) + if !ok { + log.Errorf("dialect %s Not Found", driver) + return + } + e = &Engine{db: db, dialect: dial} + log.Info("Connect database success") + return +} + +// Close database connection +func (e *Engine) Close() (err error) { + if err = e.db.Close(); err == nil { + log.Info("Close database success") + } + return +} + +// NewSession creates a new session for next operations +func (e *Engine) NewSession() *session.Session { + return session.New(e.db, e.dialect) +} diff --git a/gee-orm/day5-update-delete/geeorm_test.go b/gee-orm/day5-update-delete/geeorm_test.go new file mode 100644 index 0000000..35628be --- /dev/null +++ b/gee-orm/day5-update-delete/geeorm_test.go @@ -0,0 +1,20 @@ +package geeorm + +import ( + _ "github.com/mattn/go-sqlite3" + "testing" +) + +func OpenDB(t *testing.T) *Engine { + t.Helper() + engine, err := NewEngine("sqlite3", "gee.db") + if err != nil { + t.Fatal("failed to connect", err) + } + return engine +} + +func TestNewEngine(t *testing.T) { + engine := OpenDB(t) + _ = engine.Close() +} diff --git a/gee-orm/day5-update-delete/go.mod b/gee-orm/day5-update-delete/go.mod new file mode 100644 index 0000000..043b1c6 --- /dev/null +++ b/gee-orm/day5-update-delete/go.mod @@ -0,0 +1,5 @@ +module geeorm + +go 1.13 + +require github.com/mattn/go-sqlite3 v2.0.3+incompatible diff --git a/gee-orm/day5-update-delete/log/log.go b/gee-orm/day5-update-delete/log/log.go new file mode 100644 index 0000000..eacc0c6 --- /dev/null +++ b/gee-orm/day5-update-delete/log/log.go @@ -0,0 +1,47 @@ +package log + +import ( + "io/ioutil" + "log" + "os" + "sync" +) + +var ( + errorLog = log.New(os.Stdout, "\033[31m[error]\033[0m ", log.LstdFlags|log.Lshortfile) + infoLog = log.New(os.Stdout, "\033[34m[info ]\033[0m ", log.LstdFlags|log.Lshortfile) + loggers = []*log.Logger{errorLog, infoLog} + mu sync.Mutex +) + +// log methods +var ( + Error = errorLog.Println + Errorf = errorLog.Printf + Info = infoLog.Println + Infof = infoLog.Printf +) + +// log levels +const ( + InfoLevel = iota + ErrorLevel + Disabled +) + +// SetLevel controls log level +func SetLevel(level int) { + mu.Lock() + defer mu.Unlock() + + for _, logger := range loggers { + logger.SetOutput(os.Stdout) + } + + if ErrorLevel < level { + errorLog.SetOutput(ioutil.Discard) + } + if InfoLevel < level { + infoLog.SetOutput(ioutil.Discard) + } +} diff --git a/gee-orm/day5-update-delete/log/log_test.go b/gee-orm/day5-update-delete/log/log_test.go new file mode 100644 index 0000000..8cd403c --- /dev/null +++ b/gee-orm/day5-update-delete/log/log_test.go @@ -0,0 +1,17 @@ +package log + +import ( + "os" + "testing" +) + +func TestSetLevel(t *testing.T) { + SetLevel(ErrorLevel) + if infoLog.Writer() == os.Stdout || errorLog.Writer() != os.Stdout { + t.Fatal("failed to set log level") + } + SetLevel(Disabled) + if infoLog.Writer() == os.Stdout || errorLog.Writer() == os.Stdout { + t.Fatal("failed to set log level") + } +} \ No newline at end of file diff --git a/gee-orm/day5-update-delete/schema/schema.go b/gee-orm/day5-update-delete/schema/schema.go new file mode 100644 index 0000000..8519fd4 --- /dev/null +++ b/gee-orm/day5-update-delete/schema/schema.go @@ -0,0 +1,67 @@ +package schema + +import ( + "fmt" + "geeorm/dialect" + "go/ast" + "reflect" +) + +// Field represents a column of database +type Field struct { + Name string + Tag string +} + +// Schema represents a table of database +type Schema struct { + TableName string + PrimaryField *Field + Fields []*Field + FieldNames []string +} + +// Values return the values of dest's member variables +func (schema *Schema) Values(dest interface{}) []interface{} { + destValue := reflect.Indirect(reflect.ValueOf(dest)) + var fieldValues []interface{} + for _, field := range schema.Fields { + fieldValues = append(fieldValues, destValue.FieldByName(field.Name).Interface()) + } + return fieldValues +} + +// Parse a struct to a Schema instance +func Parse(dest interface{}, d dialect.Dialect) *Schema { + modelType := reflect.Indirect(reflect.ValueOf(dest)).Type() + schema := &Schema{ + TableName: modelType.Name(), + PrimaryField: &Field{Name: "ID", Tag: ""}, + } + + for i := 0; i < modelType.NumField(); i++ { + p := modelType.Field(i) + if !p.Anonymous && ast.IsExported(p.Name) { + field := &Field{ + Name: p.Name, + Tag: d.DataTypeOf(reflect.Indirect(reflect.New(p.Type))), + } + if v, ok := p.Tag.Lookup("geeorm"); ok && v == "primary_key" { + schema.PrimaryField = field + } + schema.Fields = append(schema.Fields, field) + schema.FieldNames = append(schema.FieldNames, p.Name) + } + } + return schema +} + +// String returns readable string +func (field *Field) String() string { + return fmt.Sprintf("(%s %s)", field.Name, field.Tag) +} + +// String returns readable string +func (schema *Schema) String() string { + return fmt.Sprintf("TABLE %s %v", schema.TableName, schema.Fields) +} diff --git a/gee-orm/day5-update-delete/schema/schema_test.go b/gee-orm/day5-update-delete/schema/schema_test.go new file mode 100644 index 0000000..aba3e0b --- /dev/null +++ b/gee-orm/day5-update-delete/schema/schema_test.go @@ -0,0 +1,36 @@ +package schema + +import ( + "geeorm/dialect" + "testing" +) + +type User struct { + Name string `geeorm:"primary_key"` + Age int +} + +var TestDial, _ = dialect.GetDialect("sqlite3") + +func TestParse(t *testing.T) { + schema := Parse(&User{}, TestDial) + if schema.TableName != "User" || len(schema.Fields) != 2 { + t.Fatal("failed to parse User struct") + } + if schema.PrimaryField.Name != "Name" { + t.Fatal("failed to parse primary key") + } + t.Log(schema) +} + +func TestSchema_Values(t *testing.T) { + schema := Parse(&User{}, TestDial) + values := schema.Values(&User{"Tom", 18}) + + name := values[0].(string) + age := values[1].(int) + + if name != "Tom" || age != 18 { + t.Fatal("failed to get values") + } +} diff --git a/gee-orm/day5-update-delete/session/raw.go b/gee-orm/day5-update-delete/session/raw.go new file mode 100644 index 0000000..6cb72c9 --- /dev/null +++ b/gee-orm/day5-update-delete/session/raw.go @@ -0,0 +1,59 @@ +package session + +import ( + "database/sql" + "geeorm/clause" + "geeorm/dialect" + "geeorm/log" + "geeorm/schema" +) + +// Session keep a pointer to sql.DB and provides all execution of all +// kind of database operations. +type Session struct { + db *sql.DB + dialect dialect.Dialect + refTable *schema.Schema + clause clause.Clause + sql string + sqlVars []interface{} +} + +// New creates a instance of Session +func New(db *sql.DB, dialect dialect.Dialect) *Session { + return &Session{ + db: db, + dialect: dialect, + } +} + +// Exec raw sql with sqlVars +func (s *Session) Exec() (result sql.Result, err error) { + log.Info(s.sql, s.sqlVars) + if result, err = s.db.Exec(s.sql, s.sqlVars...); err != nil { + log.Error(err) + } + return +} + +// QueryRow gets a record from db +func (s *Session) QueryRow() *sql.Row { + log.Info(s.sql, s.sqlVars) + return s.db.QueryRow(s.sql, s.sqlVars...) +} + +// QueryRows gets a list of records from db +func (s *Session) QueryRows() (rows *sql.Rows, err error) { + log.Info(s.sql, s.sqlVars) + if rows, err = s.db.Query(s.sql, s.sqlVars...); err != nil { + log.Error(err) + } + return +} + +// Raw appends sql and sqlVars +func (s *Session) Raw(sql string, values ...interface{}) *Session { + s.sql += sql + s.sqlVars = append(s.sqlVars, values...) + return s +} diff --git a/gee-orm/day5-update-delete/session/raw_test.go b/gee-orm/day5-update-delete/session/raw_test.go new file mode 100644 index 0000000..ce1e75b --- /dev/null +++ b/gee-orm/day5-update-delete/session/raw_test.go @@ -0,0 +1,46 @@ +package session + +import ( + "database/sql" + "os" + "testing" + + "geeorm/dialect" + + _ "github.com/mattn/go-sqlite3" +) + +var ( + TestDB *sql.DB + TestDial, _ = dialect.GetDialect("sqlite3") +) + +func TestMain(m *testing.M) { + TestDB, _ = sql.Open("sqlite3", "gee.db") + code := m.Run() + _ = TestDB.Close() + os.Exit(code) +} + +func NewSession() *Session { + return &Session{db: TestDB, dialect: TestDial} +} + +func TestSession_Exec(t *testing.T) { + _, _ = NewSession().Raw("DROP TABLE IF EXISTS User;").Exec() + _, _ = NewSession().Raw("CREATE TABLE User(name text);").Exec() + result, _ := NewSession().Raw("INSERT INTO User(`name`) values (?), (?)", "Tom", "Sam").Exec() + if count, err := result.RowsAffected(); err != nil || count != 2 { + t.Fatal("expect 2, but got", count) + } +} + +func TestSession_QueryRows(t *testing.T) { + _, _ = NewSession().Raw("DROP TABLE IF EXISTS User;").Exec() + _, _ = NewSession().Raw("CREATE TABLE User(name text);").Exec() + row := NewSession().Raw("SELECT count(*) FROM User").QueryRow() + var count int + if err := row.Scan(&count); err != nil || count != 0 { + t.Fatal("failed to query db", err) + } +} diff --git a/gee-orm/day5-update-delete/session/record.go b/gee-orm/day5-update-delete/session/record.go new file mode 100644 index 0000000..bc6d1fe --- /dev/null +++ b/gee-orm/day5-update-delete/session/record.go @@ -0,0 +1,130 @@ +package session + +import ( + "geeorm/clause" + "reflect" +) + +// Create one or more records in database +func (s *Session) Create(values ...interface{}) (int64, error) { + recordValues := make([]interface{}, 0) + for _, value := range values { + table := s.RefTable(value) + s.clause.Set(clause.INSERT, table.TableName, table.FieldNames) + recordValues = append(recordValues, table.Values(value)) + } + + s.clause.Set(clause.VALUES, recordValues...) + sql, vars := s.clause.Build(clause.INSERT, clause.VALUES) + result, err := s.Raw(sql, vars...).Exec() + if err != nil { + return 0, err + } + + return result.RowsAffected() +} + +// Find gets all eligible records +func (s *Session) Find(values interface{}) error { + destSlice := reflect.Indirect(reflect.ValueOf(values)) + destType := destSlice.Type().Elem() + table := s.RefTable(reflect.New(destType).Elem().Interface()) + + s.clause.Set(clause.SELECT, table.TableName, table.FieldNames) + sql, vars := s.clause.Build(clause.SELECT, clause.WHERE, clause.ORDERBY, clause.LIMIT) + rows, err := s.Raw(sql, vars...).QueryRows() + if err != nil { + return err + } + + for rows.Next() { + dest := reflect.New(destType).Elem() + var values []interface{} + for _, name := range table.FieldNames { + values = append(values, dest.FieldByName(name).Addr().Interface()) + } + if err := rows.Scan(values...); err != nil { + return err + } + destSlice.Set(reflect.Append(destSlice, dest)) + } + return rows.Close() +} + +// First gets the 1st row +func (s *Session) First(value interface{}) error { + dest := reflect.Indirect(reflect.ValueOf(value)) + destSlice := reflect.New(reflect.SliceOf(dest.Type())).Elem() + err := s.Limit(1).Find(destSlice.Addr().Interface()) + dest.Set(destSlice.Index(0)) + return err +} + +// Limit adds limit condition to clause +func (s *Session) Limit(num int) *Session { + s.clause.Set(clause.LIMIT, num) + return s +} + +// Where adds limit condition to clause +func (s *Session) Where(desc string, args ...interface{}) *Session { + var vars []interface{} + s.clause.Set(clause.WHERE, append(append(vars, desc), args...)...) + return s +} + +// OrderBy adds order by condition to clause +func (s *Session) OrderBy(desc string) *Session { + s.clause.Set(clause.ORDERBY, desc) + return s +} + +// Set adds Assignment by condition to clause +// support map[string]interface{} +// also support "Name", "Tom", "Age", 18, etc +func (s *Session) Set(values ...interface{}) *Session { + m, ok := values[0].(map[string]interface{}) + if !ok { + m = make(map[string]interface{}) + for i := 0; i < len(values); i += 2 { + m[values[i].(string)] = values[i+1] + } + } + s.clause.Set(clause.SET, m) + return s +} + +// Update records with where clause +func (s *Session) Update(value interface{}) (int64, error) { + s.clause.Set(clause.UPDATE, s.guessTableName(value)) + sql, vars := s.clause.Build(clause.UPDATE, clause.SET, clause.WHERE) + result, err := s.Raw(sql, vars...).Exec() + if err != nil { + return 0, err + } + return result.RowsAffected() +} + +// Delete records with where clause +func (s *Session) Delete(value interface{}) (int64, error) { + s.clause.Set(clause.DELETE, s.guessTableName(value)) + sql, vars := s.clause.Build(clause.DELETE, clause.WHERE) + result, err := s.Raw(sql, vars...).Exec() + if err != nil { + return 0, err + } + return result.RowsAffected() +} + +// Count records with where clause +func (s *Session) Count(value interface{}) (int64, error) { + s.clause.Set(clause.COUNT, s.guessTableName(value)) + sql, vars := s.clause.Build(clause.COUNT, clause.WHERE) + row := s.Raw(sql, vars...).QueryRow() + var tmp int64 + + if err := row.Scan(&tmp); err != nil { + return 0, err + } + return tmp, nil +} diff --git a/gee-orm/day5-update-delete/session/record_test.go b/gee-orm/day5-update-delete/session/record_test.go new file mode 100644 index 0000000..3489287 --- /dev/null +++ b/gee-orm/day5-update-delete/session/record_test.go @@ -0,0 +1,96 @@ +package session + +import "testing" + +var ( + user1 = &User{"Tom", 18} + user2 = &User{"Sam", 25} + user3 = &User{"Jack", 25} +) + +func testRecordInit(t *testing.T) { + t.Helper() + err1 := NewSession().DropTable(&User{}) + err2 := NewSession().CreateTable(&User{}) + _, err3 := NewSession().Create(user1, user2) + if err1 != nil || err2 != nil || err3 != nil { + t.Fatal("failed init test records") + } + +} + +func TestSession_Create(t *testing.T) { + testRecordInit(t) + affected, err := NewSession().Create(user3) + if err != nil || affected != 1 { + t.Fatal("failed to create record") + } +} + +func TestSession_Find(t *testing.T) { + testRecordInit(t) + users := []User{} + if err := NewSession().Find(&users); err != nil || len(users) != 2 { + t.Fatal("failed to query all") + } +} + +func TestSession_First(t *testing.T) { + testRecordInit(t) + u := &User{} + err := NewSession().First(u) + if err != nil || u.Name != "Tom" || u.Age != 18 { + t.Fatal("failed to query first") + } +} + +func TestSession_Limit(t *testing.T) { + testRecordInit(t) + var users []User + err := NewSession().Limit(1).Find(&users) + if err != nil || len(users) != 1 { + t.Fatal("failed to query with limit condition") + } +} + +func TestSession_Where(t *testing.T) { + testRecordInit(t) + var users []User + _, err1 := NewSession().Create(user3) + err2 := NewSession().Where("Age = ?", 25).Find(&users) + + if err1 != nil || err2 != nil || len(users) != 2 { + t.Fatal("failed to query with where condition") + } +} + +func TestSession_OrderBy(t *testing.T) { + testRecordInit(t) + u := &User{} + err := NewSession().OrderBy("Age DESC").First(u) + + if err != nil || u.Age != 25 { + t.Fatal("failed to query with order by condition") + } +} + +func TestSession_Update(t *testing.T) { + testRecordInit(t) + affected, _ := NewSession().Where("Name = ?", "Tom").Set("Age", 30).Update(&User{}) + u := &User{} + _ = NewSession().OrderBy("Age DESC").First(u) + + if affected != 1 || u.Age != 30 { + t.Fatal("failed to update") + } +} + +func TestSession_DeleteAndCount(t *testing.T) { + testRecordInit(t) + affected, _ := NewSession().Where("Name = ?", "Tom").Delete("User") + count, _ := NewSession().Count("User") + + if affected != 1 || count != 1 { + t.Fatal("failed to delete or count") + } +} diff --git a/gee-orm/day5-update-delete/session/table.go b/gee-orm/day5-update-delete/session/table.go new file mode 100644 index 0000000..b644d3a --- /dev/null +++ b/gee-orm/day5-update-delete/session/table.go @@ -0,0 +1,59 @@ +package session + +import ( + "fmt" + "strings" + + "geeorm/schema" +) + +// RefTable returns a Schema instance that contains all parsed fields +func (s *Session) RefTable(value interface{}) *schema.Schema { + if value == nil { + panic("value is nil") + } + if s.refTable == nil { + s.refTable = schema.Parse(value, s.dialect) + } + return s.refTable +} + +// CreateTable create a table in database with a model +func (s *Session) CreateTable(value interface{}) error { + table := s.RefTable(value) + var columns []string + for _, field := range table.Fields { + tag := field.Tag + if field.Name == table.PrimaryField.Name { + tag += " PRIMARY KEY" + } + columns = append(columns, fmt.Sprintf("%s %s", field.Name, tag)) + } + desc := strings.Join(columns, ",") + _, err := s.Raw(fmt.Sprintf("CREATE TABLE %s (%s);", table.TableName, desc)).Exec() + return err +} + +// DropTable drops a table with the name of model +func (s *Session) DropTable(value interface{}) error { + table := s.RefTable(value) + _, err := s.Raw(fmt.Sprintf("DROP TABLE IF EXISTS %s", table.TableName)).Exec() + return err +} + +func (s *Session) guessTableName(value interface{}) string { + if tableName, ok := value.(string); ok { + return tableName + } + return s.RefTable(value).TableName +} + +// HasTable returns true of the table exists +func (s *Session) HasTable(value interface{}) bool { + tableName := s.guessTableName(value) + sql, values := s.dialect.TableExistSQL(tableName) + row := s.Raw(sql, values...).QueryRow() + var tmp string + _ = row.Scan(&tmp) + return tmp == tableName +} diff --git a/gee-orm/day5-update-delete/session/table_test.go b/gee-orm/day5-update-delete/session/table_test.go new file mode 100644 index 0000000..5c934fa --- /dev/null +++ b/gee-orm/day5-update-delete/session/table_test.go @@ -0,0 +1,18 @@ +package session + +import ( + "testing" +) + +type User struct { + Name string `geeorm:"primary_key"` + Age int +} + +func TestSession_CreateTable(t *testing.T) { + _ = NewSession().DropTable(&User{}) + _ = NewSession().CreateTable(&User{}) + if !NewSession().HasTable("User") { + t.Fatal("failed to create table User") + } +} From 53190e5d6a9f1de06d8e80663c6cf429d6959920 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Wed, 26 Feb 2020 22:16:13 +0800 Subject: [PATCH 047/122] add day6 transaction --- gee-orm/day6-transaction/clause/clause.go | 52 +++++++ .../day6-transaction/clause/clause_test.go | 76 ++++++++++ gee-orm/day6-transaction/clause/generator.go | 106 ++++++++++++++ gee-orm/day6-transaction/dialect/dialect.go | 22 +++ gee-orm/day6-transaction/dialect/sqlite3.go | 45 ++++++ .../day6-transaction/dialect/sqlite3_test.go | 25 ++++ gee-orm/day6-transaction/geeorm.go | 75 ++++++++++ gee-orm/day6-transaction/geeorm_test.go | 71 ++++++++++ gee-orm/day6-transaction/go.mod | 5 + gee-orm/day6-transaction/log/log.go | 47 +++++++ gee-orm/day6-transaction/log/log_test.go | 17 +++ gee-orm/day6-transaction/schema/schema.go | 67 +++++++++ .../day6-transaction/schema/schema_test.go | 36 +++++ gee-orm/day6-transaction/session/raw.go | 88 ++++++++++++ gee-orm/day6-transaction/session/raw_test.go | 46 +++++++ gee-orm/day6-transaction/session/record.go | 130 ++++++++++++++++++ .../day6-transaction/session/record_test.go | 96 +++++++++++++ gee-orm/day6-transaction/session/table.go | 59 ++++++++ .../day6-transaction/session/table_test.go | 18 +++ .../day6-transaction/session/transaction.go | 31 +++++ 20 files changed, 1112 insertions(+) create mode 100644 gee-orm/day6-transaction/clause/clause.go create mode 100644 gee-orm/day6-transaction/clause/clause_test.go create mode 100644 gee-orm/day6-transaction/clause/generator.go create mode 100644 gee-orm/day6-transaction/dialect/dialect.go create mode 100644 gee-orm/day6-transaction/dialect/sqlite3.go create mode 100644 gee-orm/day6-transaction/dialect/sqlite3_test.go create mode 100644 gee-orm/day6-transaction/geeorm.go create mode 100644 gee-orm/day6-transaction/geeorm_test.go create mode 100644 gee-orm/day6-transaction/go.mod create mode 100644 gee-orm/day6-transaction/log/log.go create mode 100644 gee-orm/day6-transaction/log/log_test.go create mode 100644 gee-orm/day6-transaction/schema/schema.go create mode 100644 gee-orm/day6-transaction/schema/schema_test.go create mode 100644 gee-orm/day6-transaction/session/raw.go create mode 100644 gee-orm/day6-transaction/session/raw_test.go create mode 100644 gee-orm/day6-transaction/session/record.go create mode 100644 gee-orm/day6-transaction/session/record_test.go create mode 100644 gee-orm/day6-transaction/session/table.go create mode 100644 gee-orm/day6-transaction/session/table_test.go create mode 100644 gee-orm/day6-transaction/session/transaction.go diff --git a/gee-orm/day6-transaction/clause/clause.go b/gee-orm/day6-transaction/clause/clause.go new file mode 100644 index 0000000..2195c86 --- /dev/null +++ b/gee-orm/day6-transaction/clause/clause.go @@ -0,0 +1,52 @@ +package clause + +import ( + "strings" +) + +// Clause contains SQL conditions +type Clause struct { + sql map[Type]string + sqlVars map[Type][]interface{} +} + +// Type is the type of Clause +type Type int + +// Support types for Clause +const ( + INSERT Type = iota + VALUES + SELECT + LIMIT + WHERE + ORDERBY + UPDATE + SET + DELETE + COUNT +) + +// Set adds a sub clause of specific type +func (c *Clause) Set(name Type, vars ...interface{}) { + if c.sql == nil { + c.sql = make(map[Type]string) + c.sqlVars = make(map[Type][]interface{}) + } + sql, vars := generators[name](vars...) + c.sql[name] = sql + c.sqlVars[name] = vars +} + +// Build generate the final SQL and SQLVars +func (c *Clause) Build(orders ...Type) (string, []interface{}) { + var sqls []string + var vars []interface{} + for _, order := range orders { + if sql, ok := c.sql[order]; ok { + sqls = append(sqls, sql) + vars = append(vars, c.sqlVars[order]...) + } + } + return strings.Join(sqls, " "), vars +} diff --git a/gee-orm/day6-transaction/clause/clause_test.go b/gee-orm/day6-transaction/clause/clause_test.go new file mode 100644 index 0000000..8769c40 --- /dev/null +++ b/gee-orm/day6-transaction/clause/clause_test.go @@ -0,0 +1,76 @@ +package clause + +import ( + "reflect" + "testing" +) + +func TestClause_Set(t *testing.T) { + var clause Clause + clause.Set(INSERT, "User", []string{"Name", "Age"}) + sql := clause.sql[INSERT] + vars := clause.sqlVars[INSERT] + t.Log(sql, vars) + if sql != "INSERT INTO User (Name,Age)" || len(vars) != 0 { + t.Fatal("failed to get clause") + } +} + +func testSelect(t *testing.T) { + var clause Clause + clause.Set(LIMIT, 3) + clause.Set(SELECT, "User", []string{"*"}) + clause.Set(WHERE, "Name = ?", "Tom") + clause.Set(ORDERBY, "Age ASC") + sql, vars := clause.Build(SELECT, WHERE, ORDERBY, LIMIT) + t.Log(sql, vars) + if sql != "SELECT * FROM User WHERE Name = ? ORDER BY Age ASC LIMIT ?" { + t.Fatal("failed to build SQL") + } + if !reflect.DeepEqual(vars, []interface{}{"Tom", 3}) { + t.Fatal("failed to build SQLVars") + } +} + +func testUpdate(t *testing.T) { + var clause Clause + clause.Set(UPDATE, "User") + clause.Set(WHERE, "Name = ?", "Tom") + clause.Set(SET, map[string]interface{}{"Age": 30, "Name": "Tommy"}) + + sql, vars := clause.Build(UPDATE, SET, WHERE) + t.Log(sql, vars) + if sql != "UPDATE User SET Age = ?, Name = ? WHERE Name = ?" { + t.Fatal("failed to build SQL") + } + if !reflect.DeepEqual(vars, []interface{}{30, "Tommy", "Tom"}) { + t.Fatal("failed to build SQLVars") + } +} + +func testDelete(t *testing.T) { + var clause Clause + clause.Set(DELETE, "User") + clause.Set(WHERE, "Name = ?", "Tom") + + sql, vars := clause.Build(DELETE, WHERE) + t.Log(sql, vars) + if sql != "DELETE FROM User WHERE Name = ?" { + t.Fatal("failed to build SQL") + } + if !reflect.DeepEqual(vars, []interface{}{"Tom"}) { + t.Fatal("failed to build SQLVars") + } +} + +func TestClause_Build(t *testing.T) { + t.Run("select", func(t *testing.T) { + testSelect(t) + }) + t.Run("update", func(t *testing.T) { + testUpdate(t) + }) + t.Run("delete", func(t *testing.T) { + testDelete(t) + }) +} diff --git a/gee-orm/day6-transaction/clause/generator.go b/gee-orm/day6-transaction/clause/generator.go new file mode 100644 index 0000000..1cf5aec --- /dev/null +++ b/gee-orm/day6-transaction/clause/generator.go @@ -0,0 +1,106 @@ +package clause + +import ( + "fmt" + "strings" +) + +type generator func(values ...interface{}) (string, []interface{}) + +var generators map[Type]generator + +func init() { + generators = make(map[Type]generator) + generators[INSERT] = _insert + generators[VALUES] = _values + generators[SELECT] = _select + generators[LIMIT] = _limit + generators[WHERE] = _where + generators[ORDERBY] = _orderby + generators[UPDATE] = _update + generators[SET] = _set + generators[DELETE] = _delete + generators[COUNT] = _count +} + +func genBindVars(num int) string { + var vars []string + for i := 0; i < num; i++ { + vars = append(vars, "?") + } + return strings.Join(vars, ", ") +} + +func _insert(values ...interface{}) (string, []interface{}) { + // INSERT INTO $tableName ($fields) + tableName := values[0] + fields := strings.Join(values[1].([]string), ",") + return fmt.Sprintf("INSERT INTO %s (%v)", tableName, fields), []interface{}{} +} + +func _values(values ...interface{}) (string, []interface{}) { + // VALUES ($v1), (&v2), ... + var bindStr string + var sql strings.Builder + var vars []interface{} + sql.WriteString("VALUES ") + for i, value := range values { + v := value.([]interface{}) + if bindStr == "" { + bindStr = genBindVars(len(v)) + } + sql.WriteString(fmt.Sprintf("(%v)", bindStr)) + if i+1 != len(values) { + sql.WriteString(", ") + } + vars = append(vars, v...) + } + return sql.String(), vars + +} + +func _select(values ...interface{}) (string, []interface{}) { + // SELECT $fields FROM $tableName + tableName := values[0] + fields := strings.Join(values[1].([]string), ",") + return fmt.Sprintf("SELECT %v FROM %s", fields, tableName), []interface{}{} +} + +func _limit(values ...interface{}) (string, []interface{}) { + // LIMIT $num + return "LIMIT ?", values +} + +func _where(values ...interface{}) (string, []interface{}) { + // WHERE $desc + desc, vars := values[0], values[1:] + return fmt.Sprintf("WHERE %s", desc), vars +} + +func _orderby(values ...interface{}) (string, []interface{}) { + return fmt.Sprintf("ORDER BY %s", values[0]), []interface{}{} +} + +func _update(values ...interface{}) (string, []interface{}) { + return fmt.Sprintf("UPDATE %s", values[0]), []interface{}{} +} + +func _set(values ...interface{}) (string, []interface{}) { + m := values[0].(map[string]interface{}) + var keys []string + var vars []interface{} + for k, v := range m { + keys = append(keys, k+" = ?") + vars = append(vars, v) + } + + return fmt.Sprintf("SET %s", strings.Join(keys, ", ")), vars +} + +func _delete(values ...interface{}) (string, []interface{}) { + return fmt.Sprintf("DELETE FROM %s", values[0]), []interface{}{} +} + +func _count(values ...interface{}) (string, []interface{}) { + return _select(values[0], []string{"count(*)"}) +} diff --git a/gee-orm/day6-transaction/dialect/dialect.go b/gee-orm/day6-transaction/dialect/dialect.go new file mode 100644 index 0000000..4696314 --- /dev/null +++ b/gee-orm/day6-transaction/dialect/dialect.go @@ -0,0 +1,22 @@ +package dialect + +import "reflect" + +var dialectsMap = map[string]Dialect{} + +// Dialect is an interface contains methods that a dialect has to implement +type Dialect interface { + DataTypeOf(typ reflect.Value) string + TableExistSQL(tableName string) (string, []interface{}) +} + +// RegisterDialect register a dialect to the global variable +func RegisterDialect(name string, dialect Dialect) { + dialectsMap[name] = dialect +} + +// Get the dialect from global variable if it exists +func GetDialect(name string) (dialect Dialect, ok bool) { + dialect, ok = dialectsMap[name] + return +} diff --git a/gee-orm/day6-transaction/dialect/sqlite3.go b/gee-orm/day6-transaction/dialect/sqlite3.go new file mode 100644 index 0000000..f3c3897 --- /dev/null +++ b/gee-orm/day6-transaction/dialect/sqlite3.go @@ -0,0 +1,45 @@ +package dialect + +import ( + "fmt" + "reflect" + "time" +) + +type sqlite3 struct{} + +var _ Dialect = (*sqlite3)(nil) + +func init() { + RegisterDialect("sqlite3", &sqlite3{}) +} + +// Get Data Type for sqlite3 Dialect +func (s *sqlite3) DataTypeOf(typ reflect.Value) string { + switch typ.Kind() { + case reflect.Bool: + return "bool" + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uintptr: + return "integer" + case reflect.Int64, reflect.Uint64: + return "bigint" + case reflect.Float32, reflect.Float64: + return "real" + case reflect.String: + return "text" + case reflect.Array, reflect.Slice: + return "blob" + case reflect.Struct: + if _, ok := typ.Interface().(time.Time); ok { + return "datetime" + } + } + panic(fmt.Sprintf("invalid sql type %s (%s)", typ.Type().Name(), typ.Kind())) +} + +// TableExistSQL returns SQL that judge whether the table exists in database +func (s *sqlite3) TableExistSQL(tableName string) (string, []interface{}) { + args := []interface{}{tableName} + return "SELECT name FROM sqlite_master WHERE type='table' and name = ?", args +} diff --git a/gee-orm/day6-transaction/dialect/sqlite3_test.go b/gee-orm/day6-transaction/dialect/sqlite3_test.go new file mode 100644 index 0000000..3df5f07 --- /dev/null +++ b/gee-orm/day6-transaction/dialect/sqlite3_test.go @@ -0,0 +1,25 @@ +package dialect + +import ( + "reflect" + "testing" +) + +func TestDataTypeOf(t *testing.T) { + dial := &sqlite3{} + cases := []struct { + Value interface{} + Type string + }{ + {"Tom", "text"}, + {123, "integer"}, + {1.2, "real"}, + {[]int{1, 2, 3}, "blob"}, + } + + for _, c := range cases { + if typ := dial.DataTypeOf(reflect.ValueOf(c.Value)); typ != c.Type { + t.Fatalf("expect %s, but got %s", c.Type, typ) + } + } +} diff --git a/gee-orm/day6-transaction/geeorm.go b/gee-orm/day6-transaction/geeorm.go new file mode 100644 index 0000000..b8cbb42 --- /dev/null +++ b/gee-orm/day6-transaction/geeorm.go @@ -0,0 +1,75 @@ +package geeorm + +import ( + "database/sql" + "geeorm/dialect" + "geeorm/log" + "geeorm/session" +) + +// Engine is the main struct of geeorm, manages all db sessions and transactions. +type Engine struct { + db *sql.DB + dialect dialect.Dialect +} + +// NewEngine create a instance of Engine +// connect database and ping it to test whether it's alive +func NewEngine(driver, source string) (e *Engine, err error) { + db, err := sql.Open(driver, source) + if err != nil { + log.Error(err) + return + } + // Send a ping to make sure the database connection is alive. + if err = db.Ping(); err != nil { + log.Error(err) + return + } + // make sure the specific dialect exists + dial, ok := dialect.GetDialect(driver) + if !ok { + log.Errorf("dialect %s Not Found", driver) + return + } + e = &Engine{db: db, dialect: dial} + log.Info("Connect database success") + return +} + +// Close database connection +func (e *Engine) Close() (err error) { + if err = e.db.Close(); err == nil { + log.Info("Close database success") + } + return +} + +// NewSession creates a new session for next operations +func (e *Engine) NewSession() *session.Session { + return session.New(e.db, e.dialect) +} + +// TxFunc will be called between tx.Begin() and tx.Commit() +// https://stackoverflow.com/questions/16184238/database-sql-tx-detecting-commit-or-rollback +type TxFunc func(*session.Session) (interface{}, error) + +// Transaction executes sql wrapped in a transaction, then automatically commit if no error occurs +func (e *Engine) Transaction(f TxFunc) (result interface{}, err error) { + s := e.NewSession() + if err := s.Begin(); err != nil { + return nil, err + } + defer func() { + if p := recover(); p != nil { + _ = s.Rollback() + panic(p) // re-throw panic after Rollback + } else if err != nil { + _ = s.Rollback() // err is non-nil; don't change it + } else { + err = s.Commit() // err is nil; if Commit returns error update err + } + }() + + return f(s) +} diff --git a/gee-orm/day6-transaction/geeorm_test.go b/gee-orm/day6-transaction/geeorm_test.go new file mode 100644 index 0000000..7b15616 --- /dev/null +++ b/gee-orm/day6-transaction/geeorm_test.go @@ -0,0 +1,71 @@ +package geeorm + +import ( + "errors" + "geeorm/session" + "testing" + + _ "github.com/mattn/go-sqlite3" +) + +func OpenDB(t *testing.T) *Engine { + t.Helper() + engine, err := NewEngine("sqlite3", "gee.db") + if err != nil { + t.Fatal("failed to connect", err) + } + return engine +} + +func CloseDB(engine *Engine) { + _ = engine.Close() +} + +func TestNewEngine(t *testing.T) { + engine := OpenDB(t) + _ = engine.Close() +} + +type User struct { + Name string `geeorm:"primary_key"` + Age int +} + +func transactionRollback(t *testing.T) { + engine := OpenDB(t) + defer CloseDB(engine) + _ = engine.NewSession().DropTable(&User{}) + _, err := engine.Transaction(func(s *session.Session) (result interface{}, err error) { + _ = s.CreateTable(&User{}) + _, err = s.Create(&User{"Tom", 18}) + return nil, errors.New("Error") + }) + if err == nil || engine.NewSession().HasTable("User") { + t.Fatal("failed to rollback") + } +} + +func transactionCommit(t *testing.T) { + engine := OpenDB(t) + defer CloseDB(engine) + _ = engine.NewSession().DropTable(&User{}) + _, err := engine.Transaction(func(s *session.Session) (result interface{}, err error) { + err = s.CreateTable(&User{}) + _, err = s.Create(&User{"Tom", 18}) + return + }) + u := &User{} + _ = engine.NewSession().First(u) + if err != nil || u.Name != "Tom" { + t.Fatal("failed to commit") + } +} + +func TestEngine_Transaction(t *testing.T) { + t.Run("rollback", func(t *testing.T) { + transactionRollback(t) + }) + t.Run("commit", func(t *testing.T) { + transactionCommit(t) + }) +} diff --git a/gee-orm/day6-transaction/go.mod b/gee-orm/day6-transaction/go.mod new file mode 100644 index 0000000..043b1c6 --- /dev/null +++ b/gee-orm/day6-transaction/go.mod @@ -0,0 +1,5 @@ +module geeorm + +go 1.13 + +require github.com/mattn/go-sqlite3 v2.0.3+incompatible diff --git a/gee-orm/day6-transaction/log/log.go b/gee-orm/day6-transaction/log/log.go new file mode 100644 index 0000000..eacc0c6 --- /dev/null +++ b/gee-orm/day6-transaction/log/log.go @@ -0,0 +1,47 @@ +package log + +import ( + "io/ioutil" + "log" + "os" + "sync" +) + +var ( + errorLog = log.New(os.Stdout, "\033[31m[error]\033[0m ", log.LstdFlags|log.Lshortfile) + infoLog = log.New(os.Stdout, "\033[34m[info ]\033[0m ", log.LstdFlags|log.Lshortfile) + loggers = []*log.Logger{errorLog, infoLog} + mu sync.Mutex +) + +// log methods +var ( + Error = errorLog.Println + Errorf = errorLog.Printf + Info = infoLog.Println + Infof = infoLog.Printf +) + +// log levels +const ( + InfoLevel = iota + ErrorLevel + Disabled +) + +// SetLevel controls log level +func SetLevel(level int) { + mu.Lock() + defer mu.Unlock() + + for _, logger := range loggers { + logger.SetOutput(os.Stdout) + } + + if ErrorLevel < level { + errorLog.SetOutput(ioutil.Discard) + } + if InfoLevel < level { + infoLog.SetOutput(ioutil.Discard) + } +} diff --git a/gee-orm/day6-transaction/log/log_test.go b/gee-orm/day6-transaction/log/log_test.go new file mode 100644 index 0000000..8cd403c --- /dev/null +++ b/gee-orm/day6-transaction/log/log_test.go @@ -0,0 +1,17 @@ +package log + +import ( + "os" + "testing" +) + +func TestSetLevel(t *testing.T) { + SetLevel(ErrorLevel) + if infoLog.Writer() == os.Stdout || errorLog.Writer() != os.Stdout { + t.Fatal("failed to set log level") + } + SetLevel(Disabled) + if infoLog.Writer() == os.Stdout || errorLog.Writer() == os.Stdout { + t.Fatal("failed to set log level") + } +} \ No newline at end of file diff --git a/gee-orm/day6-transaction/schema/schema.go b/gee-orm/day6-transaction/schema/schema.go new file mode 100644 index 0000000..8519fd4 --- /dev/null +++ b/gee-orm/day6-transaction/schema/schema.go @@ -0,0 +1,67 @@ +package schema + +import ( + "fmt" + "geeorm/dialect" + "go/ast" + "reflect" +) + +// Field represents a column of database +type Field struct { + Name string + Tag string +} + +// Schema represents a table of database +type Schema struct { + TableName string + PrimaryField *Field + Fields []*Field + FieldNames []string +} + +// Values return the values of dest's member variables +func (schema *Schema) Values(dest interface{}) []interface{} { + destValue := reflect.Indirect(reflect.ValueOf(dest)) + var fieldValues []interface{} + for _, field := range schema.Fields { + fieldValues = append(fieldValues, destValue.FieldByName(field.Name).Interface()) + } + return fieldValues +} + +// Parse a struct to a Schema instance +func Parse(dest interface{}, d dialect.Dialect) *Schema { + modelType := reflect.Indirect(reflect.ValueOf(dest)).Type() + schema := &Schema{ + TableName: modelType.Name(), + PrimaryField: &Field{Name: "ID", Tag: ""}, + } + + for i := 0; i < modelType.NumField(); i++ { + p := modelType.Field(i) + if !p.Anonymous && ast.IsExported(p.Name) { + field := &Field{ + Name: p.Name, + Tag: d.DataTypeOf(reflect.Indirect(reflect.New(p.Type))), + } + if v, ok := p.Tag.Lookup("geeorm"); ok && v == "primary_key" { + schema.PrimaryField = field + } + schema.Fields = append(schema.Fields, field) + schema.FieldNames = append(schema.FieldNames, p.Name) + } + } + return schema +} + +// String returns readable string +func (field *Field) String() string { + return fmt.Sprintf("(%s %s)", field.Name, field.Tag) +} + +// String returns readable string +func (schema *Schema) String() string { + return fmt.Sprintf("TABLE %s %v", schema.TableName, schema.Fields) +} diff --git a/gee-orm/day6-transaction/schema/schema_test.go b/gee-orm/day6-transaction/schema/schema_test.go new file mode 100644 index 0000000..aba3e0b --- /dev/null +++ b/gee-orm/day6-transaction/schema/schema_test.go @@ -0,0 +1,36 @@ +package schema + +import ( + "geeorm/dialect" + "testing" +) + +type User struct { + Name string `geeorm:"primary_key"` + Age int +} + +var TestDial, _ = dialect.GetDialect("sqlite3") + +func TestParse(t *testing.T) { + schema := Parse(&User{}, TestDial) + if schema.TableName != "User" || len(schema.Fields) != 2 { + t.Fatal("failed to parse User struct") + } + if schema.PrimaryField.Name != "Name" { + t.Fatal("failed to parse primary key") + } + t.Log(schema) +} + +func TestSchema_Values(t *testing.T) { + schema := Parse(&User{}, TestDial) + values := schema.Values(&User{"Tom", 18}) + + name := values[0].(string) + age := values[1].(int) + + if name != "Tom" || age != 18 { + t.Fatal("failed to get values") + } +} diff --git a/gee-orm/day6-transaction/session/raw.go b/gee-orm/day6-transaction/session/raw.go new file mode 100644 index 0000000..d99f631 --- /dev/null +++ b/gee-orm/day6-transaction/session/raw.go @@ -0,0 +1,88 @@ +package session + +import ( + "database/sql" + "geeorm/clause" + "geeorm/dialect" + "geeorm/log" + "geeorm/schema" +) + +// Session keep a pointer to sql.DB and provides all execution of all +// kind of database operations. +type Session struct { + db *sql.DB + dialect dialect.Dialect + tx *sql.Tx + refTable *schema.Schema + clause clause.Clause + sql string + sqlVars []interface{} +} + +// New creates a instance of Session +func New(db *sql.DB, dialect dialect.Dialect) *Session { + return &Session{ + db: db, + dialect: dialect, + } +} + +// Clear initialize the state of a session, except for isAutoCommit +func (s *Session) Clear() { + s.refTable, s.sqlVars = nil, nil + s.clause = clause.Clause{} + s.sql = "" +} + +// CommonDB is a minimal function set of db +type CommonDB interface { + Query(query string, args ...interface{}) (*sql.Rows, error) + QueryRow(query string, args ...interface{}) *sql.Row + Exec(query string, args ...interface{}) (sql.Result, error) +} + +var _ CommonDB = (*sql.DB)(nil) +var _ CommonDB = (*sql.Tx)(nil) + +// DB returns tx if a tx begins. otherwise return *sql.DB +func (s *Session) DB() CommonDB { + if s.tx != nil { + return s.tx + } + return s.db +} + +// Exec raw sql with sqlVars +func (s *Session) Exec() (result sql.Result, err error) { + defer s.Clear() + log.Info(s.sql, s.sqlVars) + if result, err = s.DB().Exec(s.sql, s.sqlVars...); err != nil { + log.Error(err) + } + return +} + +// QueryRow gets a record from db +func (s *Session) QueryRow() *sql.Row { + defer s.Clear() + log.Info(s.sql, s.sqlVars) + return s.DB().QueryRow(s.sql, s.sqlVars...) +} + +// QueryRows gets a list of records from db +func (s *Session) QueryRows() (rows *sql.Rows, err error) { + defer s.Clear() + log.Info(s.sql, s.sqlVars) + if rows, err = s.DB().Query(s.sql, s.sqlVars...); err != nil { + log.Error(err) + } + return +} + +// Raw appends sql and sqlVars +func (s *Session) Raw(sql string, values ...interface{}) *Session { + s.sql += sql + s.sqlVars = append(s.sqlVars, values...) + return s +} diff --git a/gee-orm/day6-transaction/session/raw_test.go b/gee-orm/day6-transaction/session/raw_test.go new file mode 100644 index 0000000..ce1e75b --- /dev/null +++ b/gee-orm/day6-transaction/session/raw_test.go @@ -0,0 +1,46 @@ +package session + +import ( + "database/sql" + "os" + "testing" + + "geeorm/dialect" + + _ "github.com/mattn/go-sqlite3" +) + +var ( + TestDB *sql.DB + TestDial, _ = dialect.GetDialect("sqlite3") +) + +func TestMain(m *testing.M) { + TestDB, _ = sql.Open("sqlite3", "gee.db") + code := m.Run() + _ = TestDB.Close() + os.Exit(code) +} + +func NewSession() *Session { + return &Session{db: TestDB, dialect: TestDial} +} + +func TestSession_Exec(t *testing.T) { + _, _ = NewSession().Raw("DROP TABLE IF EXISTS User;").Exec() + _, _ = NewSession().Raw("CREATE TABLE User(name text);").Exec() + result, _ := NewSession().Raw("INSERT INTO User(`name`) values (?), (?)", "Tom", "Sam").Exec() + if count, err := result.RowsAffected(); err != nil || count != 2 { + t.Fatal("expect 2, but got", count) + } +} + +func TestSession_QueryRows(t *testing.T) { + _, _ = NewSession().Raw("DROP TABLE IF EXISTS User;").Exec() + _, _ = NewSession().Raw("CREATE TABLE User(name text);").Exec() + row := NewSession().Raw("SELECT count(*) FROM User").QueryRow() + var count int + if err := row.Scan(&count); err != nil || count != 0 { + t.Fatal("failed to query db", err) + } +} diff --git a/gee-orm/day6-transaction/session/record.go b/gee-orm/day6-transaction/session/record.go new file mode 100644 index 0000000..bc6d1fe --- /dev/null +++ b/gee-orm/day6-transaction/session/record.go @@ -0,0 +1,130 @@ +package session + +import ( + "geeorm/clause" + "reflect" +) + +// Create one or more records in database +func (s *Session) Create(values ...interface{}) (int64, error) { + recordValues := make([]interface{}, 0) + for _, value := range values { + table := s.RefTable(value) + s.clause.Set(clause.INSERT, table.TableName, table.FieldNames) + recordValues = append(recordValues, table.Values(value)) + } + + s.clause.Set(clause.VALUES, recordValues...) + sql, vars := s.clause.Build(clause.INSERT, clause.VALUES) + result, err := s.Raw(sql, vars...).Exec() + if err != nil { + return 0, err + } + + return result.RowsAffected() +} + +// Find gets all eligible records +func (s *Session) Find(values interface{}) error { + destSlice := reflect.Indirect(reflect.ValueOf(values)) + destType := destSlice.Type().Elem() + table := s.RefTable(reflect.New(destType).Elem().Interface()) + + s.clause.Set(clause.SELECT, table.TableName, table.FieldNames) + sql, vars := s.clause.Build(clause.SELECT, clause.WHERE, clause.ORDERBY, clause.LIMIT) + rows, err := s.Raw(sql, vars...).QueryRows() + if err != nil { + return err + } + + for rows.Next() { + dest := reflect.New(destType).Elem() + var values []interface{} + for _, name := range table.FieldNames { + values = append(values, dest.FieldByName(name).Addr().Interface()) + } + if err := rows.Scan(values...); err != nil { + return err + } + destSlice.Set(reflect.Append(destSlice, dest)) + } + return rows.Close() +} + +// First gets the 1st row +func (s *Session) First(value interface{}) error { + dest := reflect.Indirect(reflect.ValueOf(value)) + destSlice := reflect.New(reflect.SliceOf(dest.Type())).Elem() + err := s.Limit(1).Find(destSlice.Addr().Interface()) + dest.Set(destSlice.Index(0)) + return err +} + +// Limit adds limit condition to clause +func (s *Session) Limit(num int) *Session { + s.clause.Set(clause.LIMIT, num) + return s +} + +// Where adds limit condition to clause +func (s *Session) Where(desc string, args ...interface{}) *Session { + var vars []interface{} + s.clause.Set(clause.WHERE, append(append(vars, desc), args...)...) + return s +} + +// OrderBy adds order by condition to clause +func (s *Session) OrderBy(desc string) *Session { + s.clause.Set(clause.ORDERBY, desc) + return s +} + +// Set adds Assignment by condition to clause +// support map[string]interface{} +// also support "Name", "Tom", "Age", 18, etc +func (s *Session) Set(values ...interface{}) *Session { + m, ok := values[0].(map[string]interface{}) + if !ok { + m = make(map[string]interface{}) + for i := 0; i < len(values); i += 2 { + m[values[i].(string)] = values[i+1] + } + } + s.clause.Set(clause.SET, m) + return s +} + +// Update records with where clause +func (s *Session) Update(value interface{}) (int64, error) { + s.clause.Set(clause.UPDATE, s.guessTableName(value)) + sql, vars := s.clause.Build(clause.UPDATE, clause.SET, clause.WHERE) + result, err := s.Raw(sql, vars...).Exec() + if err != nil { + return 0, err + } + return result.RowsAffected() +} + +// Delete records with where clause +func (s *Session) Delete(value interface{}) (int64, error) { + s.clause.Set(clause.DELETE, s.guessTableName(value)) + sql, vars := s.clause.Build(clause.DELETE, clause.WHERE) + result, err := s.Raw(sql, vars...).Exec() + if err != nil { + return 0, err + } + return result.RowsAffected() +} + +// Count records with where clause +func (s *Session) Count(value interface{}) (int64, error) { + s.clause.Set(clause.COUNT, s.guessTableName(value)) + sql, vars := s.clause.Build(clause.COUNT, clause.WHERE) + row := s.Raw(sql, vars...).QueryRow() + var tmp int64 + + if err := row.Scan(&tmp); err != nil { + return 0, err + } + return tmp, nil +} diff --git a/gee-orm/day6-transaction/session/record_test.go b/gee-orm/day6-transaction/session/record_test.go new file mode 100644 index 0000000..3489287 --- /dev/null +++ b/gee-orm/day6-transaction/session/record_test.go @@ -0,0 +1,96 @@ +package session + +import "testing" + +var ( + user1 = &User{"Tom", 18} + user2 = &User{"Sam", 25} + user3 = &User{"Jack", 25} +) + +func testRecordInit(t *testing.T) { + t.Helper() + err1 := NewSession().DropTable(&User{}) + err2 := NewSession().CreateTable(&User{}) + _, err3 := NewSession().Create(user1, user2) + if err1 != nil || err2 != nil || err3 != nil { + t.Fatal("failed init test records") + } + +} + +func TestSession_Create(t *testing.T) { + testRecordInit(t) + affected, err := NewSession().Create(user3) + if err != nil || affected != 1 { + t.Fatal("failed to create record") + } +} + +func TestSession_Find(t *testing.T) { + testRecordInit(t) + users := []User{} + if err := NewSession().Find(&users); err != nil || len(users) != 2 { + t.Fatal("failed to query all") + } +} + +func TestSession_First(t *testing.T) { + testRecordInit(t) + u := &User{} + err := NewSession().First(u) + if err != nil || u.Name != "Tom" || u.Age != 18 { + t.Fatal("failed to query first") + } +} + +func TestSession_Limit(t *testing.T) { + testRecordInit(t) + var users []User + err := NewSession().Limit(1).Find(&users) + if err != nil || len(users) != 1 { + t.Fatal("failed to query with limit condition") + } +} + +func TestSession_Where(t *testing.T) { + testRecordInit(t) + var users []User + _, err1 := NewSession().Create(user3) + err2 := NewSession().Where("Age = ?", 25).Find(&users) + + if err1 != nil || err2 != nil || len(users) != 2 { + t.Fatal("failed to query with where condition") + } +} + +func TestSession_OrderBy(t *testing.T) { + testRecordInit(t) + u := &User{} + err := NewSession().OrderBy("Age DESC").First(u) + + if err != nil || u.Age != 25 { + t.Fatal("failed to query with order by condition") + } +} + +func TestSession_Update(t *testing.T) { + testRecordInit(t) + affected, _ := NewSession().Where("Name = ?", "Tom").Set("Age", 30).Update(&User{}) + u := &User{} + _ = NewSession().OrderBy("Age DESC").First(u) + + if affected != 1 || u.Age != 30 { + t.Fatal("failed to update") + } +} + +func TestSession_DeleteAndCount(t *testing.T) { + testRecordInit(t) + affected, _ := NewSession().Where("Name = ?", "Tom").Delete("User") + count, _ := NewSession().Count("User") + + if affected != 1 || count != 1 { + t.Fatal("failed to delete or count") + } +} diff --git a/gee-orm/day6-transaction/session/table.go b/gee-orm/day6-transaction/session/table.go new file mode 100644 index 0000000..b644d3a --- /dev/null +++ b/gee-orm/day6-transaction/session/table.go @@ -0,0 +1,59 @@ +package session + +import ( + "fmt" + "strings" + + "geeorm/schema" +) + +// RefTable returns a Schema instance that contains all parsed fields +func (s *Session) RefTable(value interface{}) *schema.Schema { + if value == nil { + panic("value is nil") + } + if s.refTable == nil { + s.refTable = schema.Parse(value, s.dialect) + } + return s.refTable +} + +// CreateTable create a table in database with a model +func (s *Session) CreateTable(value interface{}) error { + table := s.RefTable(value) + var columns []string + for _, field := range table.Fields { + tag := field.Tag + if field.Name == table.PrimaryField.Name { + tag += " PRIMARY KEY" + } + columns = append(columns, fmt.Sprintf("%s %s", field.Name, tag)) + } + desc := strings.Join(columns, ",") + _, err := s.Raw(fmt.Sprintf("CREATE TABLE %s (%s);", table.TableName, desc)).Exec() + return err +} + +// DropTable drops a table with the name of model +func (s *Session) DropTable(value interface{}) error { + table := s.RefTable(value) + _, err := s.Raw(fmt.Sprintf("DROP TABLE IF EXISTS %s", table.TableName)).Exec() + return err +} + +func (s *Session) guessTableName(value interface{}) string { + if tableName, ok := value.(string); ok { + return tableName + } + return s.RefTable(value).TableName +} + +// HasTable returns true of the table exists +func (s *Session) HasTable(value interface{}) bool { + tableName := s.guessTableName(value) + sql, values := s.dialect.TableExistSQL(tableName) + row := s.Raw(sql, values...).QueryRow() + var tmp string + _ = row.Scan(&tmp) + return tmp == tableName +} diff --git a/gee-orm/day6-transaction/session/table_test.go b/gee-orm/day6-transaction/session/table_test.go new file mode 100644 index 0000000..5c934fa --- /dev/null +++ b/gee-orm/day6-transaction/session/table_test.go @@ -0,0 +1,18 @@ +package session + +import ( + "testing" +) + +type User struct { + Name string `geeorm:"primary_key"` + Age int +} + +func TestSession_CreateTable(t *testing.T) { + _ = NewSession().DropTable(&User{}) + _ = NewSession().CreateTable(&User{}) + if !NewSession().HasTable("User") { + t.Fatal("failed to create table User") + } +} diff --git a/gee-orm/day6-transaction/session/transaction.go b/gee-orm/day6-transaction/session/transaction.go new file mode 100644 index 0000000..3cdb451 --- /dev/null +++ b/gee-orm/day6-transaction/session/transaction.go @@ -0,0 +1,31 @@ +package session + +import "geeorm/log" + +// Begin a transaction +func (s *Session) Begin() (err error) { + log.Info("transaction begin") + if s.tx, err = s.db.Begin(); err != nil { + log.Error(err) + return + } + return +} + +// Commit a transaction +func (s *Session) Commit() (err error) { + log.Info("transaction commit") + if err = s.tx.Commit(); err != nil { + log.Error(err) + } + return +} + +// Rollback a transaction +func (s *Session) Rollback() (err error) { + log.Info("transaction rollback") + if err = s.tx.Rollback(); err != nil { + log.Error(err) + } + return +} From a5ca58d95fc58d08121ba4d5629beaf3bdb9d4c8 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Wed, 26 Feb 2020 22:51:36 +0800 Subject: [PATCH 048/122] clear session for every exec or query --- gee-orm/day1-database-sql/session/raw.go | 9 ++++ gee-orm/day1-database-sql/session/raw_test.go | 14 +++--- gee-orm/day2-reflect-schema/session/raw.go | 9 ++++ .../day2-reflect-schema/session/raw_test.go | 14 +++--- .../day2-reflect-schema/session/table_test.go | 7 +-- gee-orm/day3-save-query/session/raw.go | 10 ++++ gee-orm/day3-save-query/session/raw_test.go | 14 +++--- .../day3-save-query/session/record_test.go | 19 +++---- gee-orm/day3-save-query/session/table_test.go | 7 +-- gee-orm/day4-chain-operation/session/raw.go | 10 ++++ .../day4-chain-operation/session/raw_test.go | 14 +++--- .../session/record_test.go | 36 +++++++------- .../session/table_test.go | 7 +-- gee-orm/day5-update-delete/session/raw.go | 10 ++++ .../day5-update-delete/session/raw_test.go | 14 +++--- .../day5-update-delete/session/record_test.go | 49 ++++++++++--------- .../day5-update-delete/session/table_test.go | 7 +-- gee-orm/day6-transaction/geeorm_test.go | 10 ++-- gee-orm/day6-transaction/session/raw.go | 2 +- gee-orm/day6-transaction/session/raw_test.go | 14 +++--- .../day6-transaction/session/record_test.go | 49 ++++++++++--------- .../day6-transaction/session/table_test.go | 7 +-- gee-orm/run_test.sh | 10 ++++ 23 files changed, 212 insertions(+), 130 deletions(-) create mode 100755 gee-orm/run_test.sh diff --git a/gee-orm/day1-database-sql/session/raw.go b/gee-orm/day1-database-sql/session/raw.go index aecc158..a0e84cd 100644 --- a/gee-orm/day1-database-sql/session/raw.go +++ b/gee-orm/day1-database-sql/session/raw.go @@ -18,8 +18,15 @@ func New(db *sql.DB) *Session { return &Session{db: db} } +// Clear initialize the state of a session +func (s *Session) Clear() { + s.sqlVars = nil + s.sql = "" +} + // Exec raw sql with sqlVars func (s *Session) Exec() (result sql.Result, err error) { + defer s.Clear() log.Info(s.sql, s.sqlVars) if result, err = s.db.Exec(s.sql, s.sqlVars...); err != nil { log.Error(err) @@ -29,12 +36,14 @@ func (s *Session) Exec() (result sql.Result, err error) { // QueryRow gets a record from db func (s *Session) QueryRow() *sql.Row { + defer s.Clear() log.Info(s.sql, s.sqlVars) return s.db.QueryRow(s.sql, s.sqlVars...) } // QueryRows gets a list of records from db func (s *Session) QueryRows() (rows *sql.Rows, err error) { + defer s.Clear() log.Info(s.sql, s.sqlVars) if rows, err = s.db.Query(s.sql, s.sqlVars...); err != nil { log.Error(err) diff --git a/gee-orm/day1-database-sql/session/raw_test.go b/gee-orm/day1-database-sql/session/raw_test.go index 1cd7712..fbb1f1b 100644 --- a/gee-orm/day1-database-sql/session/raw_test.go +++ b/gee-orm/day1-database-sql/session/raw_test.go @@ -22,18 +22,20 @@ func NewSession() *Session { } func TestSession_Exec(t *testing.T) { - _, _ = NewSession().Raw("DROP TABLE IF EXISTS User;").Exec() - _, _ = NewSession().Raw("CREATE TABLE User(name text);").Exec() - result, _ := NewSession().Raw("INSERT INTO User(`name`) values (?), (?)", "Tom", "Sam").Exec() + s := NewSession() + _, _ = s.Raw("DROP TABLE IF EXISTS User;").Exec() + _, _ = s.Raw("CREATE TABLE User(name text);").Exec() + result, _ := s.Raw("INSERT INTO User(`name`) values (?), (?)", "Tom", "Sam").Exec() if count, err := result.RowsAffected(); err != nil || count != 2 { t.Fatal("expect 2, but got", count) } } func TestSession_QueryRows(t *testing.T) { - _, _ = NewSession().Raw("DROP TABLE IF EXISTS User;").Exec() - _, _ = NewSession().Raw("CREATE TABLE User(name text);").Exec() - row := NewSession().Raw("SELECT count(*) FROM User").QueryRow() + s := NewSession() + _, _ = s.Raw("DROP TABLE IF EXISTS User;").Exec() + _, _ = s.Raw("CREATE TABLE User(name text);").Exec() + row := s.Raw("SELECT count(*) FROM User").QueryRow() var count int if err := row.Scan(&count); err != nil || count != 0 { t.Fatal("failed to query db", err) diff --git a/gee-orm/day2-reflect-schema/session/raw.go b/gee-orm/day2-reflect-schema/session/raw.go index 6892e0c..abaaeb7 100644 --- a/gee-orm/day2-reflect-schema/session/raw.go +++ b/gee-orm/day2-reflect-schema/session/raw.go @@ -25,8 +25,15 @@ func New(db *sql.DB, dialect dialect.Dialect) *Session { } } +// Clear initialize the state of a session +func (s *Session) Clear() { + s.sqlVars = nil + s.sql = "" +} + // Exec raw sql with sqlVars func (s *Session) Exec() (result sql.Result, err error) { + defer s.Clear() log.Info(s.sql, s.sqlVars) if result, err = s.db.Exec(s.sql, s.sqlVars...); err != nil { log.Error(err) @@ -36,12 +43,14 @@ func (s *Session) Exec() (result sql.Result, err error) { // QueryRow gets a record from db func (s *Session) QueryRow() *sql.Row { + defer s.Clear() log.Info(s.sql, s.sqlVars) return s.db.QueryRow(s.sql, s.sqlVars...) } // QueryRows gets a list of records from db func (s *Session) QueryRows() (rows *sql.Rows, err error) { + defer s.Clear() log.Info(s.sql, s.sqlVars) if rows, err = s.db.Query(s.sql, s.sqlVars...); err != nil { log.Error(err) diff --git a/gee-orm/day2-reflect-schema/session/raw_test.go b/gee-orm/day2-reflect-schema/session/raw_test.go index ce1e75b..ed447a5 100644 --- a/gee-orm/day2-reflect-schema/session/raw_test.go +++ b/gee-orm/day2-reflect-schema/session/raw_test.go @@ -27,18 +27,20 @@ func NewSession() *Session { } func TestSession_Exec(t *testing.T) { - _, _ = NewSession().Raw("DROP TABLE IF EXISTS User;").Exec() - _, _ = NewSession().Raw("CREATE TABLE User(name text);").Exec() - result, _ := NewSession().Raw("INSERT INTO User(`name`) values (?), (?)", "Tom", "Sam").Exec() + s := NewSession() + _, _ = s.Raw("DROP TABLE IF EXISTS User;").Exec() + _, _ = s.Raw("CREATE TABLE User(name text);").Exec() + result, _ := s.Raw("INSERT INTO User(`name`) values (?), (?)", "Tom", "Sam").Exec() if count, err := result.RowsAffected(); err != nil || count != 2 { t.Fatal("expect 2, but got", count) } } func TestSession_QueryRows(t *testing.T) { - _, _ = NewSession().Raw("DROP TABLE IF EXISTS User;").Exec() - _, _ = NewSession().Raw("CREATE TABLE User(name text);").Exec() - row := NewSession().Raw("SELECT count(*) FROM User").QueryRow() + s := NewSession() + _, _ = s.Raw("DROP TABLE IF EXISTS User;").Exec() + _, _ = s.Raw("CREATE TABLE User(name text);").Exec() + row := s.Raw("SELECT count(*) FROM User").QueryRow() var count int if err := row.Scan(&count); err != nil || count != 0 { t.Fatal("failed to query db", err) diff --git a/gee-orm/day2-reflect-schema/session/table_test.go b/gee-orm/day2-reflect-schema/session/table_test.go index 8405886..7ed38ac 100644 --- a/gee-orm/day2-reflect-schema/session/table_test.go +++ b/gee-orm/day2-reflect-schema/session/table_test.go @@ -10,9 +10,10 @@ type User struct { } func TestSession_CreateTable(t *testing.T) { - _ = NewSession().DropTable(&User{}) - _ = NewSession().CreateTable(&User{}) - if !NewSession().HasTable("User") { + s := NewSession() + _ = s.DropTable(&User{}) + _ = s.CreateTable(&User{}) + if !s.HasTable("User") { t.Fatal("failed to create table User") } } diff --git a/gee-orm/day3-save-query/session/raw.go b/gee-orm/day3-save-query/session/raw.go index 6cb72c9..a47edcc 100644 --- a/gee-orm/day3-save-query/session/raw.go +++ b/gee-orm/day3-save-query/session/raw.go @@ -27,8 +27,16 @@ func New(db *sql.DB, dialect dialect.Dialect) *Session { } } +// Clear initialize the state of a session +func (s *Session) Clear() { + s.refTable, s.sqlVars = nil, nil + s.clause = clause.Clause{} + s.sql = "" +} + // Exec raw sql with sqlVars func (s *Session) Exec() (result sql.Result, err error) { + defer s.Clear() log.Info(s.sql, s.sqlVars) if result, err = s.db.Exec(s.sql, s.sqlVars...); err != nil { log.Error(err) @@ -38,12 +46,14 @@ func (s *Session) Exec() (result sql.Result, err error) { // QueryRow gets a record from db func (s *Session) QueryRow() *sql.Row { + defer s.Clear() log.Info(s.sql, s.sqlVars) return s.db.QueryRow(s.sql, s.sqlVars...) } // QueryRows gets a list of records from db func (s *Session) QueryRows() (rows *sql.Rows, err error) { + defer s.Clear() log.Info(s.sql, s.sqlVars) if rows, err = s.db.Query(s.sql, s.sqlVars...); err != nil { log.Error(err) diff --git a/gee-orm/day3-save-query/session/raw_test.go b/gee-orm/day3-save-query/session/raw_test.go index ce1e75b..ed447a5 100644 --- a/gee-orm/day3-save-query/session/raw_test.go +++ b/gee-orm/day3-save-query/session/raw_test.go @@ -27,18 +27,20 @@ func NewSession() *Session { } func TestSession_Exec(t *testing.T) { - _, _ = NewSession().Raw("DROP TABLE IF EXISTS User;").Exec() - _, _ = NewSession().Raw("CREATE TABLE User(name text);").Exec() - result, _ := NewSession().Raw("INSERT INTO User(`name`) values (?), (?)", "Tom", "Sam").Exec() + s := NewSession() + _, _ = s.Raw("DROP TABLE IF EXISTS User;").Exec() + _, _ = s.Raw("CREATE TABLE User(name text);").Exec() + result, _ := s.Raw("INSERT INTO User(`name`) values (?), (?)", "Tom", "Sam").Exec() if count, err := result.RowsAffected(); err != nil || count != 2 { t.Fatal("expect 2, but got", count) } } func TestSession_QueryRows(t *testing.T) { - _, _ = NewSession().Raw("DROP TABLE IF EXISTS User;").Exec() - _, _ = NewSession().Raw("CREATE TABLE User(name text);").Exec() - row := NewSession().Raw("SELECT count(*) FROM User").QueryRow() + s := NewSession() + _, _ = s.Raw("DROP TABLE IF EXISTS User;").Exec() + _, _ = s.Raw("CREATE TABLE User(name text);").Exec() + row := s.Raw("SELECT count(*) FROM User").QueryRow() var count int if err := row.Scan(&count); err != nil || count != 0 { t.Fatal("failed to query db", err) diff --git a/gee-orm/day3-save-query/session/record_test.go b/gee-orm/day3-save-query/session/record_test.go index 27b6d77..08881fa 100644 --- a/gee-orm/day3-save-query/session/record_test.go +++ b/gee-orm/day3-save-query/session/record_test.go @@ -8,29 +8,30 @@ var ( user3 = &User{"Jack", 25} ) -func testRecordInit(t *testing.T) { +func testRecordInit(t *testing.T) *Session { t.Helper() - err1 := NewSession().DropTable(&User{}) - err2 := NewSession().CreateTable(&User{}) - _, err3 := NewSession().Create(user1, user2) + s := NewSession() + err1 := s.DropTable(&User{}) + err2 := s.CreateTable(&User{}) + _, err3 := s.Create(user1, user2) if err1 != nil || err2 != nil || err3 != nil { t.Fatal("failed init test records") } - + return s } func TestSession_Create(t *testing.T) { - testRecordInit(t) - affected, err := NewSession().Create(user3) + s := testRecordInit(t) + affected, err := s.Create(user3) if err != nil || affected != 1 { t.Fatal("failed to create record") } } func TestSession_Find(t *testing.T) { - testRecordInit(t) + s := testRecordInit(t) users := []User{} - if err := NewSession().Find(&users); err != nil || len(users) != 2 { + if err := s.Find(&users); err != nil || len(users) != 2 { t.Fatal("failed to query all") } } diff --git a/gee-orm/day3-save-query/session/table_test.go b/gee-orm/day3-save-query/session/table_test.go index 5c934fa..91eb519 100644 --- a/gee-orm/day3-save-query/session/table_test.go +++ b/gee-orm/day3-save-query/session/table_test.go @@ -10,9 +10,10 @@ type User struct { } func TestSession_CreateTable(t *testing.T) { - _ = NewSession().DropTable(&User{}) - _ = NewSession().CreateTable(&User{}) - if !NewSession().HasTable("User") { + s := NewSession() + _ = s.DropTable(&User{}) + _ = s.CreateTable(&User{}) + if !s.HasTable("User") { t.Fatal("failed to create table User") } } diff --git a/gee-orm/day4-chain-operation/session/raw.go b/gee-orm/day4-chain-operation/session/raw.go index 6cb72c9..a47edcc 100644 --- a/gee-orm/day4-chain-operation/session/raw.go +++ b/gee-orm/day4-chain-operation/session/raw.go @@ -27,8 +27,16 @@ func New(db *sql.DB, dialect dialect.Dialect) *Session { } } +// Clear initialize the state of a session +func (s *Session) Clear() { + s.refTable, s.sqlVars = nil, nil + s.clause = clause.Clause{} + s.sql = "" +} + // Exec raw sql with sqlVars func (s *Session) Exec() (result sql.Result, err error) { + defer s.Clear() log.Info(s.sql, s.sqlVars) if result, err = s.db.Exec(s.sql, s.sqlVars...); err != nil { log.Error(err) @@ -38,12 +46,14 @@ func (s *Session) Exec() (result sql.Result, err error) { // QueryRow gets a record from db func (s *Session) QueryRow() *sql.Row { + defer s.Clear() log.Info(s.sql, s.sqlVars) return s.db.QueryRow(s.sql, s.sqlVars...) } // QueryRows gets a list of records from db func (s *Session) QueryRows() (rows *sql.Rows, err error) { + defer s.Clear() log.Info(s.sql, s.sqlVars) if rows, err = s.db.Query(s.sql, s.sqlVars...); err != nil { log.Error(err) diff --git a/gee-orm/day4-chain-operation/session/raw_test.go b/gee-orm/day4-chain-operation/session/raw_test.go index ce1e75b..ed447a5 100644 --- a/gee-orm/day4-chain-operation/session/raw_test.go +++ b/gee-orm/day4-chain-operation/session/raw_test.go @@ -27,18 +27,20 @@ func NewSession() *Session { } func TestSession_Exec(t *testing.T) { - _, _ = NewSession().Raw("DROP TABLE IF EXISTS User;").Exec() - _, _ = NewSession().Raw("CREATE TABLE User(name text);").Exec() - result, _ := NewSession().Raw("INSERT INTO User(`name`) values (?), (?)", "Tom", "Sam").Exec() + s := NewSession() + _, _ = s.Raw("DROP TABLE IF EXISTS User;").Exec() + _, _ = s.Raw("CREATE TABLE User(name text);").Exec() + result, _ := s.Raw("INSERT INTO User(`name`) values (?), (?)", "Tom", "Sam").Exec() if count, err := result.RowsAffected(); err != nil || count != 2 { t.Fatal("expect 2, but got", count) } } func TestSession_QueryRows(t *testing.T) { - _, _ = NewSession().Raw("DROP TABLE IF EXISTS User;").Exec() - _, _ = NewSession().Raw("CREATE TABLE User(name text);").Exec() - row := NewSession().Raw("SELECT count(*) FROM User").QueryRow() + s := NewSession() + _, _ = s.Raw("DROP TABLE IF EXISTS User;").Exec() + _, _ = s.Raw("CREATE TABLE User(name text);").Exec() + row := s.Raw("SELECT count(*) FROM User").QueryRow() var count int if err := row.Scan(&count); err != nil || count != 0 { t.Fatal("failed to query db", err) diff --git a/gee-orm/day4-chain-operation/session/record_test.go b/gee-orm/day4-chain-operation/session/record_test.go index 5879cfb..822201b 100644 --- a/gee-orm/day4-chain-operation/session/record_test.go +++ b/gee-orm/day4-chain-operation/session/record_test.go @@ -8,56 +8,58 @@ var ( user3 = &User{"Jack", 25} ) -func testRecordInit(t *testing.T) { +func testRecordInit(t *testing.T) *Session { t.Helper() - err1 := NewSession().DropTable(&User{}) - err2 := NewSession().CreateTable(&User{}) - _, err3 := NewSession().Create(user1, user2) + s := NewSession() + err1 := s.DropTable(&User{}) + err2 := s.CreateTable(&User{}) + _, err3 := s.Create(user1, user2) if err1 != nil || err2 != nil || err3 != nil { t.Fatal("failed init test records") } + return s } func TestSession_Create(t *testing.T) { - testRecordInit(t) - affected, err := NewSession().Create(user3) + s := testRecordInit(t) + affected, err := s.Create(user3) if err != nil || affected != 1 { t.Fatal("failed to create record") } } func TestSession_Find(t *testing.T) { - testRecordInit(t) + s := testRecordInit(t) users := []User{} - if err := NewSession().Find(&users); err != nil || len(users) != 2 { + if err := s.Find(&users); err != nil || len(users) != 2 { t.Fatal("failed to query all") } } func TestSession_First(t *testing.T) { - testRecordInit(t) + s := testRecordInit(t) u := &User{} - err := NewSession().First(u) + err := s.First(u) if err != nil || u.Name != "Tom" || u.Age != 18 { t.Fatal("failed to query first") } } func TestSession_Limit(t *testing.T) { - testRecordInit(t) + s := testRecordInit(t) var users []User - err := NewSession().Limit(1).Find(&users) + err := s.Limit(1).Find(&users) if err != nil || len(users) != 1 { t.Fatal("failed to query with limit condition") } } func TestSession_Where(t *testing.T) { - testRecordInit(t) + s := testRecordInit(t) var users []User - _, err1 := NewSession().Create(user3) - err2 := NewSession().Where("Age = ?", 25).Find(&users) + _, err1 := s.Create(user3) + err2 := s.Where("Age = ?", 25).Find(&users) if err1 != nil || err2 != nil || len(users) != 2 { t.Fatal("failed to query with where condition") @@ -65,9 +67,9 @@ func TestSession_Where(t *testing.T) { } func TestSession_OrderBy(t *testing.T) { - testRecordInit(t) + s := testRecordInit(t) u := &User{} - err := NewSession().OrderBy("Age DESC").First(u) + err := s.OrderBy("Age DESC").First(u) if err != nil || u.Age != 25 { t.Fatal("failed to query with order by condition") diff --git a/gee-orm/day4-chain-operation/session/table_test.go b/gee-orm/day4-chain-operation/session/table_test.go index 5c934fa..91eb519 100644 --- a/gee-orm/day4-chain-operation/session/table_test.go +++ b/gee-orm/day4-chain-operation/session/table_test.go @@ -10,9 +10,10 @@ type User struct { } func TestSession_CreateTable(t *testing.T) { - _ = NewSession().DropTable(&User{}) - _ = NewSession().CreateTable(&User{}) - if !NewSession().HasTable("User") { + s := NewSession() + _ = s.DropTable(&User{}) + _ = s.CreateTable(&User{}) + if !s.HasTable("User") { t.Fatal("failed to create table User") } } diff --git a/gee-orm/day5-update-delete/session/raw.go b/gee-orm/day5-update-delete/session/raw.go index 6cb72c9..a47edcc 100644 --- a/gee-orm/day5-update-delete/session/raw.go +++ b/gee-orm/day5-update-delete/session/raw.go @@ -27,8 +27,16 @@ func New(db *sql.DB, dialect dialect.Dialect) *Session { } } +// Clear initialize the state of a session +func (s *Session) Clear() { + s.refTable, s.sqlVars = nil, nil + s.clause = clause.Clause{} + s.sql = "" +} + // Exec raw sql with sqlVars func (s *Session) Exec() (result sql.Result, err error) { + defer s.Clear() log.Info(s.sql, s.sqlVars) if result, err = s.db.Exec(s.sql, s.sqlVars...); err != nil { log.Error(err) @@ -38,12 +46,14 @@ func (s *Session) Exec() (result sql.Result, err error) { // QueryRow gets a record from db func (s *Session) QueryRow() *sql.Row { + defer s.Clear() log.Info(s.sql, s.sqlVars) return s.db.QueryRow(s.sql, s.sqlVars...) } // QueryRows gets a list of records from db func (s *Session) QueryRows() (rows *sql.Rows, err error) { + defer s.Clear() log.Info(s.sql, s.sqlVars) if rows, err = s.db.Query(s.sql, s.sqlVars...); err != nil { log.Error(err) diff --git a/gee-orm/day5-update-delete/session/raw_test.go b/gee-orm/day5-update-delete/session/raw_test.go index ce1e75b..ed447a5 100644 --- a/gee-orm/day5-update-delete/session/raw_test.go +++ b/gee-orm/day5-update-delete/session/raw_test.go @@ -27,18 +27,20 @@ func NewSession() *Session { } func TestSession_Exec(t *testing.T) { - _, _ = NewSession().Raw("DROP TABLE IF EXISTS User;").Exec() - _, _ = NewSession().Raw("CREATE TABLE User(name text);").Exec() - result, _ := NewSession().Raw("INSERT INTO User(`name`) values (?), (?)", "Tom", "Sam").Exec() + s := NewSession() + _, _ = s.Raw("DROP TABLE IF EXISTS User;").Exec() + _, _ = s.Raw("CREATE TABLE User(name text);").Exec() + result, _ := s.Raw("INSERT INTO User(`name`) values (?), (?)", "Tom", "Sam").Exec() if count, err := result.RowsAffected(); err != nil || count != 2 { t.Fatal("expect 2, but got", count) } } func TestSession_QueryRows(t *testing.T) { - _, _ = NewSession().Raw("DROP TABLE IF EXISTS User;").Exec() - _, _ = NewSession().Raw("CREATE TABLE User(name text);").Exec() - row := NewSession().Raw("SELECT count(*) FROM User").QueryRow() + s := NewSession() + _, _ = s.Raw("DROP TABLE IF EXISTS User;").Exec() + _, _ = s.Raw("CREATE TABLE User(name text);").Exec() + row := s.Raw("SELECT count(*) FROM User").QueryRow() var count int if err := row.Scan(&count); err != nil || count != 0 { t.Fatal("failed to query db", err) diff --git a/gee-orm/day5-update-delete/session/record_test.go b/gee-orm/day5-update-delete/session/record_test.go index 3489287..faf8535 100644 --- a/gee-orm/day5-update-delete/session/record_test.go +++ b/gee-orm/day5-update-delete/session/record_test.go @@ -8,56 +8,57 @@ var ( user3 = &User{"Jack", 25} ) -func testRecordInit(t *testing.T) { +func testRecordInit(t *testing.T) *Session { t.Helper() - err1 := NewSession().DropTable(&User{}) - err2 := NewSession().CreateTable(&User{}) - _, err3 := NewSession().Create(user1, user2) + s := NewSession() + err1 := s.DropTable(&User{}) + err2 := s.CreateTable(&User{}) + _, err3 := s.Create(user1, user2) if err1 != nil || err2 != nil || err3 != nil { t.Fatal("failed init test records") } - + return s } func TestSession_Create(t *testing.T) { - testRecordInit(t) - affected, err := NewSession().Create(user3) + s := testRecordInit(t) + affected, err := s.Create(user3) if err != nil || affected != 1 { t.Fatal("failed to create record") } } func TestSession_Find(t *testing.T) { - testRecordInit(t) + s := testRecordInit(t) users := []User{} - if err := NewSession().Find(&users); err != nil || len(users) != 2 { + if err := s.Find(&users); err != nil || len(users) != 2 { t.Fatal("failed to query all") } } func TestSession_First(t *testing.T) { - testRecordInit(t) + s := testRecordInit(t) u := &User{} - err := NewSession().First(u) + err := s.First(u) if err != nil || u.Name != "Tom" || u.Age != 18 { t.Fatal("failed to query first") } } func TestSession_Limit(t *testing.T) { - testRecordInit(t) + s := testRecordInit(t) var users []User - err := NewSession().Limit(1).Find(&users) + err := s.Limit(1).Find(&users) if err != nil || len(users) != 1 { t.Fatal("failed to query with limit condition") } } func TestSession_Where(t *testing.T) { - testRecordInit(t) + s := testRecordInit(t) var users []User - _, err1 := NewSession().Create(user3) - err2 := NewSession().Where("Age = ?", 25).Find(&users) + _, err1 := s.Create(user3) + err2 := s.Where("Age = ?", 25).Find(&users) if err1 != nil || err2 != nil || len(users) != 2 { t.Fatal("failed to query with where condition") @@ -65,9 +66,9 @@ func TestSession_Where(t *testing.T) { } func TestSession_OrderBy(t *testing.T) { - testRecordInit(t) + s := testRecordInit(t) u := &User{} - err := NewSession().OrderBy("Age DESC").First(u) + err := s.OrderBy("Age DESC").First(u) if err != nil || u.Age != 25 { t.Fatal("failed to query with order by condition") @@ -75,10 +76,10 @@ func TestSession_OrderBy(t *testing.T) { } func TestSession_Update(t *testing.T) { - testRecordInit(t) - affected, _ := NewSession().Where("Name = ?", "Tom").Set("Age", 30).Update(&User{}) + s := testRecordInit(t) + affected, _ := s.Where("Name = ?", "Tom").Set("Age", 30).Update(&User{}) u := &User{} - _ = NewSession().OrderBy("Age DESC").First(u) + _ = s.OrderBy("Age DESC").First(u) if affected != 1 || u.Age != 30 { t.Fatal("failed to update") @@ -86,9 +87,9 @@ func TestSession_Update(t *testing.T) { } func TestSession_DeleteAndCount(t *testing.T) { - testRecordInit(t) - affected, _ := NewSession().Where("Name = ?", "Tom").Delete("User") - count, _ := NewSession().Count("User") + s := testRecordInit(t) + affected, _ := s.Where("Name = ?", "Tom").Delete("User") + count, _ := s.Count("User") if affected != 1 || count != 1 { t.Fatal("failed to delete or count") diff --git a/gee-orm/day5-update-delete/session/table_test.go b/gee-orm/day5-update-delete/session/table_test.go index 5c934fa..91eb519 100644 --- a/gee-orm/day5-update-delete/session/table_test.go +++ b/gee-orm/day5-update-delete/session/table_test.go @@ -10,9 +10,10 @@ type User struct { } func TestSession_CreateTable(t *testing.T) { - _ = NewSession().DropTable(&User{}) - _ = NewSession().CreateTable(&User{}) - if !NewSession().HasTable("User") { + s := NewSession() + _ = s.DropTable(&User{}) + _ = s.CreateTable(&User{}) + if !s.HasTable("User") { t.Fatal("failed to create table User") } } diff --git a/gee-orm/day6-transaction/geeorm_test.go b/gee-orm/day6-transaction/geeorm_test.go index 7b15616..17a1d2b 100644 --- a/gee-orm/day6-transaction/geeorm_test.go +++ b/gee-orm/day6-transaction/geeorm_test.go @@ -34,13 +34,14 @@ type User struct { func transactionRollback(t *testing.T) { engine := OpenDB(t) defer CloseDB(engine) - _ = engine.NewSession().DropTable(&User{}) + s := engine.NewSession() + _ = s.DropTable(&User{}) _, err := engine.Transaction(func(s *session.Session) (result interface{}, err error) { _ = s.CreateTable(&User{}) _, err = s.Create(&User{"Tom", 18}) return nil, errors.New("Error") }) - if err == nil || engine.NewSession().HasTable("User") { + if err == nil || s.HasTable("User") { t.Fatal("failed to rollback") } } @@ -48,14 +49,15 @@ func transactionRollback(t *testing.T) { func transactionCommit(t *testing.T) { engine := OpenDB(t) defer CloseDB(engine) - _ = engine.NewSession().DropTable(&User{}) + s := engine.NewSession() + _ = s.DropTable(&User{}) _, err := engine.Transaction(func(s *session.Session) (result interface{}, err error) { err = s.CreateTable(&User{}) _, err = s.Create(&User{"Tom", 18}) return }) u := &User{} - _ = engine.NewSession().First(u) + _ = s.First(u) if err != nil || u.Name != "Tom" { t.Fatal("failed to commit") } diff --git a/gee-orm/day6-transaction/session/raw.go b/gee-orm/day6-transaction/session/raw.go index d99f631..c30f1c3 100644 --- a/gee-orm/day6-transaction/session/raw.go +++ b/gee-orm/day6-transaction/session/raw.go @@ -28,7 +28,7 @@ func New(db *sql.DB, dialect dialect.Dialect) *Session { } } -// Clear initialize the state of a session, except for isAutoCommit +// Clear initialize the state of a session func (s *Session) Clear() { s.refTable, s.sqlVars = nil, nil s.clause = clause.Clause{} diff --git a/gee-orm/day6-transaction/session/raw_test.go b/gee-orm/day6-transaction/session/raw_test.go index ce1e75b..ed447a5 100644 --- a/gee-orm/day6-transaction/session/raw_test.go +++ b/gee-orm/day6-transaction/session/raw_test.go @@ -27,18 +27,20 @@ func NewSession() *Session { } func TestSession_Exec(t *testing.T) { - _, _ = NewSession().Raw("DROP TABLE IF EXISTS User;").Exec() - _, _ = NewSession().Raw("CREATE TABLE User(name text);").Exec() - result, _ := NewSession().Raw("INSERT INTO User(`name`) values (?), (?)", "Tom", "Sam").Exec() + s := NewSession() + _, _ = s.Raw("DROP TABLE IF EXISTS User;").Exec() + _, _ = s.Raw("CREATE TABLE User(name text);").Exec() + result, _ := s.Raw("INSERT INTO User(`name`) values (?), (?)", "Tom", "Sam").Exec() if count, err := result.RowsAffected(); err != nil || count != 2 { t.Fatal("expect 2, but got", count) } } func TestSession_QueryRows(t *testing.T) { - _, _ = NewSession().Raw("DROP TABLE IF EXISTS User;").Exec() - _, _ = NewSession().Raw("CREATE TABLE User(name text);").Exec() - row := NewSession().Raw("SELECT count(*) FROM User").QueryRow() + s := NewSession() + _, _ = s.Raw("DROP TABLE IF EXISTS User;").Exec() + _, _ = s.Raw("CREATE TABLE User(name text);").Exec() + row := s.Raw("SELECT count(*) FROM User").QueryRow() var count int if err := row.Scan(&count); err != nil || count != 0 { t.Fatal("failed to query db", err) diff --git a/gee-orm/day6-transaction/session/record_test.go b/gee-orm/day6-transaction/session/record_test.go index 3489287..faf8535 100644 --- a/gee-orm/day6-transaction/session/record_test.go +++ b/gee-orm/day6-transaction/session/record_test.go @@ -8,56 +8,57 @@ var ( user3 = &User{"Jack", 25} ) -func testRecordInit(t *testing.T) { +func testRecordInit(t *testing.T) *Session { t.Helper() - err1 := NewSession().DropTable(&User{}) - err2 := NewSession().CreateTable(&User{}) - _, err3 := NewSession().Create(user1, user2) + s := NewSession() + err1 := s.DropTable(&User{}) + err2 := s.CreateTable(&User{}) + _, err3 := s.Create(user1, user2) if err1 != nil || err2 != nil || err3 != nil { t.Fatal("failed init test records") } - + return s } func TestSession_Create(t *testing.T) { - testRecordInit(t) - affected, err := NewSession().Create(user3) + s := testRecordInit(t) + affected, err := s.Create(user3) if err != nil || affected != 1 { t.Fatal("failed to create record") } } func TestSession_Find(t *testing.T) { - testRecordInit(t) + s := testRecordInit(t) users := []User{} - if err := NewSession().Find(&users); err != nil || len(users) != 2 { + if err := s.Find(&users); err != nil || len(users) != 2 { t.Fatal("failed to query all") } } func TestSession_First(t *testing.T) { - testRecordInit(t) + s := testRecordInit(t) u := &User{} - err := NewSession().First(u) + err := s.First(u) if err != nil || u.Name != "Tom" || u.Age != 18 { t.Fatal("failed to query first") } } func TestSession_Limit(t *testing.T) { - testRecordInit(t) + s := testRecordInit(t) var users []User - err := NewSession().Limit(1).Find(&users) + err := s.Limit(1).Find(&users) if err != nil || len(users) != 1 { t.Fatal("failed to query with limit condition") } } func TestSession_Where(t *testing.T) { - testRecordInit(t) + s := testRecordInit(t) var users []User - _, err1 := NewSession().Create(user3) - err2 := NewSession().Where("Age = ?", 25).Find(&users) + _, err1 := s.Create(user3) + err2 := s.Where("Age = ?", 25).Find(&users) if err1 != nil || err2 != nil || len(users) != 2 { t.Fatal("failed to query with where condition") @@ -65,9 +66,9 @@ func TestSession_Where(t *testing.T) { } func TestSession_OrderBy(t *testing.T) { - testRecordInit(t) + s := testRecordInit(t) u := &User{} - err := NewSession().OrderBy("Age DESC").First(u) + err := s.OrderBy("Age DESC").First(u) if err != nil || u.Age != 25 { t.Fatal("failed to query with order by condition") @@ -75,10 +76,10 @@ func TestSession_OrderBy(t *testing.T) { } func TestSession_Update(t *testing.T) { - testRecordInit(t) - affected, _ := NewSession().Where("Name = ?", "Tom").Set("Age", 30).Update(&User{}) + s := testRecordInit(t) + affected, _ := s.Where("Name = ?", "Tom").Set("Age", 30).Update(&User{}) u := &User{} - _ = NewSession().OrderBy("Age DESC").First(u) + _ = s.OrderBy("Age DESC").First(u) if affected != 1 || u.Age != 30 { t.Fatal("failed to update") @@ -86,9 +87,9 @@ func TestSession_Update(t *testing.T) { } func TestSession_DeleteAndCount(t *testing.T) { - testRecordInit(t) - affected, _ := NewSession().Where("Name = ?", "Tom").Delete("User") - count, _ := NewSession().Count("User") + s := testRecordInit(t) + affected, _ := s.Where("Name = ?", "Tom").Delete("User") + count, _ := s.Count("User") if affected != 1 || count != 1 { t.Fatal("failed to delete or count") diff --git a/gee-orm/day6-transaction/session/table_test.go b/gee-orm/day6-transaction/session/table_test.go index 5c934fa..91eb519 100644 --- a/gee-orm/day6-transaction/session/table_test.go +++ b/gee-orm/day6-transaction/session/table_test.go @@ -10,9 +10,10 @@ type User struct { } func TestSession_CreateTable(t *testing.T) { - _ = NewSession().DropTable(&User{}) - _ = NewSession().CreateTable(&User{}) - if !NewSession().HasTable("User") { + s := NewSession() + _ = s.DropTable(&User{}) + _ = s.CreateTable(&User{}) + if !s.HasTable("User") { t.Fatal("failed to create table User") } } diff --git a/gee-orm/run_test.sh b/gee-orm/run_test.sh new file mode 100755 index 0000000..d5c5e7f --- /dev/null +++ b/gee-orm/run_test.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -eou pipefail + +cur=$PWD +for item in $(ls -d $cur/day*/) +do + echo $item + cd $item + go test geeorm/... 2>&1 | grep -v warning +done \ No newline at end of file From 09b6c3bf34af13d458e93ad5efd3d5a34d71b63c Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Fri, 28 Feb 2020 20:20:51 +0800 Subject: [PATCH 049/122] combine chain opertion & delete & update --- gee-orm/day1-database-sql/geeorm.go | 8 +-- gee-orm/day1-database-sql/geeorm_test.go | 2 +- gee-orm/day1-database-sql/session/raw.go | 25 ++++--- gee-orm/day1-database-sql/session/raw_test.go | 4 +- gee-orm/day2-reflect-schema/geeorm.go | 12 ++-- gee-orm/day2-reflect-schema/geeorm_test.go | 2 +- gee-orm/day2-reflect-schema/schema/schema.go | 42 ++++++------ .../day2-reflect-schema/schema/schema_test.go | 10 +-- gee-orm/day2-reflect-schema/session/raw.go | 25 ++++--- .../day2-reflect-schema/session/raw_test.go | 4 +- gee-orm/day2-reflect-schema/session/table.go | 49 +++++++------- .../day2-reflect-schema/session/table_test.go | 22 +++++-- gee-orm/day3-save-query/clause/generator.go | 4 +- gee-orm/day3-save-query/geeorm.go | 12 ++-- gee-orm/day3-save-query/geeorm_test.go | 2 +- gee-orm/day3-save-query/schema/schema.go | 40 ++++++------ gee-orm/day3-save-query/schema/schema_test.go | 11 ++-- gee-orm/day3-save-query/session/raw.go | 29 +++++---- gee-orm/day3-save-query/session/raw_test.go | 4 +- gee-orm/day3-save-query/session/record.go | 12 ++-- .../day3-save-query/session/record_test.go | 14 ++-- gee-orm/day3-save-query/session/table.go | 49 +++++++------- gee-orm/day3-save-query/session/table_test.go | 21 ++++-- gee-orm/day4-chain-operation/clause/clause.go | 3 + .../clause/clause_test.go | 35 ++++++++++ .../day4-chain-operation/clause/generator.go | 27 +++++++- gee-orm/day4-chain-operation/geeorm.go | 12 ++-- gee-orm/day4-chain-operation/geeorm_test.go | 2 +- gee-orm/day4-chain-operation/schema/schema.go | 40 ++++++------ .../schema/schema_test.go | 11 ++-- gee-orm/day4-chain-operation/session/raw.go | 29 +++++---- .../day4-chain-operation/session/raw_test.go | 4 +- .../day4-chain-operation/session/record.go | 65 ++++++++++++++++--- .../session/record_test.go | 37 ++++++++--- gee-orm/day4-chain-operation/session/table.go | 49 +++++++------- .../session/table_test.go | 21 ++++-- .../clause/clause.go | 1 - .../clause/clause_test.go | 6 +- .../clause/generator.go | 15 ++--- .../dialect/dialect.go | 0 .../dialect/sqlite3.go | 0 .../dialect/sqlite3_test.go | 0 .../geeorm.go | 12 ++-- .../geeorm_test.go | 2 +- .../{day5-update-delete => day5-hooks}/go.mod | 0 .../log/log.go | 0 .../log/log_test.go | 0 .../schema/schema.go | 40 ++++++------ .../schema/schema_test.go | 11 ++-- .../session/raw.go | 29 +++++---- .../session/raw_test.go | 4 +- .../session/record.go | 65 +++++++++++-------- .../session/record_test.go | 22 +++---- gee-orm/day5-hooks/session/table.go | 54 +++++++++++++++ gee-orm/day5-hooks/session/table_test.go | 28 ++++++++ gee-orm/day5-update-delete/session/table.go | 59 ----------------- .../day5-update-delete/session/table_test.go | 19 ------ gee-orm/day6-transaction/clause/clause.go | 1 - .../day6-transaction/clause/clause_test.go | 6 +- gee-orm/day6-transaction/clause/generator.go | 15 ++--- gee-orm/day6-transaction/geeorm.go | 16 ++--- gee-orm/day6-transaction/geeorm_test.go | 26 ++++---- gee-orm/day6-transaction/schema/schema.go | 40 ++++++------ .../day6-transaction/schema/schema_test.go | 11 ++-- gee-orm/day6-transaction/session/raw.go | 24 +++---- gee-orm/day6-transaction/session/raw_test.go | 4 +- gee-orm/day6-transaction/session/record.go | 53 ++++++++------- .../day6-transaction/session/record_test.go | 22 +++---- gee-orm/day6-transaction/session/table.go | 49 +++++++------- .../day6-transaction/session/table_test.go | 21 ++++-- gee-orm/run_test.sh | 6 +- 71 files changed, 779 insertions(+), 620 deletions(-) rename gee-orm/{day5-update-delete => day5-hooks}/clause/clause.go (99%) rename gee-orm/{day5-update-delete => day5-hooks}/clause/clause_test.go (92%) rename gee-orm/{day5-update-delete => day5-hooks}/clause/generator.go (87%) rename gee-orm/{day5-update-delete => day5-hooks}/dialect/dialect.go (100%) rename gee-orm/{day5-update-delete => day5-hooks}/dialect/sqlite3.go (100%) rename gee-orm/{day5-update-delete => day5-hooks}/dialect/sqlite3_test.go (100%) rename gee-orm/{day5-update-delete => day5-hooks}/geeorm.go (78%) rename gee-orm/{day5-update-delete => day5-hooks}/geeorm_test.go (93%) rename gee-orm/{day5-update-delete => day5-hooks}/go.mod (100%) rename gee-orm/{day5-update-delete => day5-hooks}/log/log.go (100%) rename gee-orm/{day5-update-delete => day5-hooks}/log/log_test.go (100%) rename gee-orm/{day5-update-delete => day5-hooks}/schema/schema.go (59%) rename gee-orm/{day5-update-delete => day5-hooks}/schema/schema_test.go (65%) rename gee-orm/{day5-update-delete => day5-hooks}/session/raw.go (69%) rename gee-orm/{day5-update-delete => day5-hooks}/session/raw_test.go (89%) rename gee-orm/{day5-update-delete => day5-hooks}/session/record.go (64%) rename gee-orm/{day5-update-delete => day5-hooks}/session/record_test.go (81%) create mode 100644 gee-orm/day5-hooks/session/table.go create mode 100644 gee-orm/day5-hooks/session/table_test.go delete mode 100644 gee-orm/day5-update-delete/session/table.go delete mode 100644 gee-orm/day5-update-delete/session/table_test.go diff --git a/gee-orm/day1-database-sql/geeorm.go b/gee-orm/day1-database-sql/geeorm.go index ae2ef44..3611b94 100644 --- a/gee-orm/day1-database-sql/geeorm.go +++ b/gee-orm/day1-database-sql/geeorm.go @@ -31,11 +31,11 @@ func NewEngine(driver, source string) (e *Engine, err error) { } // Close database connection -func (engine *Engine) Close() (err error) { - if err = engine.db.Close(); err == nil { - log.Info("Close database success") +func (engine *Engine) Close() { + if err := engine.db.Close(); err != nil { + log.Error("Failed to close database") } - return + log.Info("Close database success") } // NewSession creates a new session for next operations diff --git a/gee-orm/day1-database-sql/geeorm_test.go b/gee-orm/day1-database-sql/geeorm_test.go index 35628be..c6da191 100644 --- a/gee-orm/day1-database-sql/geeorm_test.go +++ b/gee-orm/day1-database-sql/geeorm_test.go @@ -16,5 +16,5 @@ func OpenDB(t *testing.T) *Engine { func TestNewEngine(t *testing.T) { engine := OpenDB(t) - _ = engine.Close() + defer engine.Close() } diff --git a/gee-orm/day1-database-sql/session/raw.go b/gee-orm/day1-database-sql/session/raw.go index a0e84cd..f9f4f87 100644 --- a/gee-orm/day1-database-sql/session/raw.go +++ b/gee-orm/day1-database-sql/session/raw.go @@ -3,13 +3,14 @@ package session import ( "database/sql" "geeorm/log" + "strings" ) // Session keep a pointer to sql.DB and provides all execution of all // kind of database operations. type Session struct { db *sql.DB - sql string + sql strings.Builder sqlVars []interface{} } @@ -20,15 +21,20 @@ func New(db *sql.DB) *Session { // Clear initialize the state of a session func (s *Session) Clear() { + s.sql.Reset() s.sqlVars = nil - s.sql = "" +} + +// DB returns *sql.DB +func (s *Session) DB() *sql.DB { + return s.db } // Exec raw sql with sqlVars func (s *Session) Exec() (result sql.Result, err error) { defer s.Clear() - log.Info(s.sql, s.sqlVars) - if result, err = s.db.Exec(s.sql, s.sqlVars...); err != nil { + log.Info(s.sql.String(), s.sqlVars) + if result, err = s.DB().Exec(s.sql.String(), s.sqlVars...); err != nil { log.Error(err) } return @@ -37,15 +43,15 @@ func (s *Session) Exec() (result sql.Result, err error) { // QueryRow gets a record from db func (s *Session) QueryRow() *sql.Row { defer s.Clear() - log.Info(s.sql, s.sqlVars) - return s.db.QueryRow(s.sql, s.sqlVars...) + log.Info(s.sql.String(), s.sqlVars) + return s.DB().QueryRow(s.sql.String(), s.sqlVars...) } // QueryRows gets a list of records from db func (s *Session) QueryRows() (rows *sql.Rows, err error) { defer s.Clear() - log.Info(s.sql, s.sqlVars) - if rows, err = s.db.Query(s.sql, s.sqlVars...); err != nil { + log.Info(s.sql.String(), s.sqlVars) + if rows, err = s.DB().Query(s.sql.String(), s.sqlVars...); err != nil { log.Error(err) } return @@ -53,7 +59,8 @@ func (s *Session) QueryRows() (rows *sql.Rows, err error) { // Raw appends sql and sqlVars func (s *Session) Raw(sql string, values ...interface{}) *Session { - s.sql += sql + s.sql.WriteString(sql) + s.sql.WriteString(" ") s.sqlVars = append(s.sqlVars, values...) return s } diff --git a/gee-orm/day1-database-sql/session/raw_test.go b/gee-orm/day1-database-sql/session/raw_test.go index fbb1f1b..8a18162 100644 --- a/gee-orm/day1-database-sql/session/raw_test.go +++ b/gee-orm/day1-database-sql/session/raw_test.go @@ -25,7 +25,7 @@ func TestSession_Exec(t *testing.T) { s := NewSession() _, _ = s.Raw("DROP TABLE IF EXISTS User;").Exec() _, _ = s.Raw("CREATE TABLE User(name text);").Exec() - result, _ := s.Raw("INSERT INTO User(`name`) values (?), (?)", "Tom", "Sam").Exec() + result, _ := s.Raw("INSERT INTO User(`Name`) values (?), (?)", "Tom", "Sam").Exec() if count, err := result.RowsAffected(); err != nil || count != 2 { t.Fatal("expect 2, but got", count) } @@ -34,7 +34,7 @@ func TestSession_Exec(t *testing.T) { func TestSession_QueryRows(t *testing.T) { s := NewSession() _, _ = s.Raw("DROP TABLE IF EXISTS User;").Exec() - _, _ = s.Raw("CREATE TABLE User(name text);").Exec() + _, _ = s.Raw("CREATE TABLE User(Name text);").Exec() row := s.Raw("SELECT count(*) FROM User").QueryRow() var count int if err := row.Scan(&count); err != nil || count != 0 { diff --git a/gee-orm/day2-reflect-schema/geeorm.go b/gee-orm/day2-reflect-schema/geeorm.go index 61ee9e0..b1881ce 100644 --- a/gee-orm/day2-reflect-schema/geeorm.go +++ b/gee-orm/day2-reflect-schema/geeorm.go @@ -38,14 +38,14 @@ func NewEngine(driver, source string) (e *Engine, err error) { } // Close database connection -func (e *Engine) Close() (err error) { - if err = e.db.Close(); err == nil { - log.Info("Close database success") +func (engine *Engine) Close() { + if err := engine.db.Close(); err != nil { + log.Error("Failed to close database") } - return + log.Info("Close database success") } // NewSession creates a new session for next operations -func (e *Engine) NewSession() *session.Session { - return session.New(e.db, e.dialect) +func (engine *Engine) NewSession() *session.Session { + return session.New(engine.db, engine.dialect) } diff --git a/gee-orm/day2-reflect-schema/geeorm_test.go b/gee-orm/day2-reflect-schema/geeorm_test.go index 35628be..c6da191 100644 --- a/gee-orm/day2-reflect-schema/geeorm_test.go +++ b/gee-orm/day2-reflect-schema/geeorm_test.go @@ -16,5 +16,5 @@ func OpenDB(t *testing.T) *Engine { func TestNewEngine(t *testing.T) { engine := OpenDB(t) - _ = engine.Close() + defer engine.Close() } diff --git a/gee-orm/day2-reflect-schema/schema/schema.go b/gee-orm/day2-reflect-schema/schema/schema.go index f942aa0..ff76283 100644 --- a/gee-orm/day2-reflect-schema/schema/schema.go +++ b/gee-orm/day2-reflect-schema/schema/schema.go @@ -1,7 +1,6 @@ package schema import ( - "fmt" "geeorm/dialect" "go/ast" "reflect" @@ -10,20 +9,26 @@ import ( // Field represents a column of database type Field struct { Name string + Type string Tag string } // Schema represents a table of database type Schema struct { - TableName string - PrimaryField *Field - Fields []*Field - FieldNames []string - BindVars []string + Model interface{} + Name string + Fields []*Field + FieldNames []string + fieldMap map[string]*Field +} + +// GetField returns field by name +func (schema *Schema) GetField(name string) *Field { + return schema.fieldMap[name] } // Values return the values of dest's member variables -func (schema *Schema) Values(dest interface{}) []interface{} { +func (schema *Schema) RecordValues(dest interface{}) []interface{} { destValue := reflect.Indirect(reflect.ValueOf(dest)) var fieldValues []interface{} for _, field := range schema.Fields { @@ -36,8 +41,9 @@ func (schema *Schema) Values(dest interface{}) []interface{} { func Parse(dest interface{}, d dialect.Dialect) *Schema { modelType := reflect.Indirect(reflect.ValueOf(dest)).Type() schema := &Schema{ - TableName: modelType.Name(), - PrimaryField: &Field{Name: "ID", Tag: ""}, + Model: dest, + Name: modelType.Name(), + fieldMap: make(map[string]*Field), } for i := 0; i < modelType.NumField(); i++ { @@ -45,25 +51,15 @@ func Parse(dest interface{}, d dialect.Dialect) *Schema { if !p.Anonymous && ast.IsExported(p.Name) { field := &Field{ Name: p.Name, - Tag: d.DataTypeOf(reflect.Indirect(reflect.New(p.Type))), + Type: d.DataTypeOf(reflect.Indirect(reflect.New(p.Type))), } - if v, ok := p.Tag.Lookup("geeorm"); ok && v == "primary_key" { - schema.PrimaryField = field + if v, ok := p.Tag.Lookup("geeorm"); ok { + field.Tag = v } schema.Fields = append(schema.Fields, field) schema.FieldNames = append(schema.FieldNames, p.Name) - schema.BindVars = append(schema.BindVars, "?") + schema.fieldMap[p.Name] = field } } return schema } - -// String returns readable string -func (field *Field) String() string { - return fmt.Sprintf("(%s %s)", field.Name, field.Tag) -} - -// String returns readable string -func (schema *Schema) String() string { - return fmt.Sprintf("TABLE %s %v", schema.TableName, schema.Fields) -} \ No newline at end of file diff --git a/gee-orm/day2-reflect-schema/schema/schema_test.go b/gee-orm/day2-reflect-schema/schema/schema_test.go index 34725b7..47ae9fc 100644 --- a/gee-orm/day2-reflect-schema/schema/schema_test.go +++ b/gee-orm/day2-reflect-schema/schema/schema_test.go @@ -6,7 +6,7 @@ import ( ) type User struct { - Name string `geeorm:"primary_key"` + Name string `geeorm:"PRIMARY KEY"` Age int } @@ -14,17 +14,17 @@ var TestDial, _ = dialect.GetDialect("sqlite3") func TestParse(t *testing.T) { schema := Parse(&User{}, TestDial) - if schema.TableName != "User" || len(schema.Fields) != 2 { + if schema.Name != "User" || len(schema.Fields) != 2 { t.Fatal("failed to parse User struct") } - if schema.PrimaryField.Name != "Name" { + if schema.GetField("Name").Tag != "PRIMARY KEY" { t.Fatal("failed to parse primary key") } } -func TestSchema_Values(t *testing.T) { +func TestSchema_RecordValues(t *testing.T) { schema := Parse(&User{}, TestDial) - values := schema.Values(&User{"Tom", 18}) + values := schema.RecordValues(&User{"Tom", 18}) name := values[0].(string) age := values[1].(int) diff --git a/gee-orm/day2-reflect-schema/session/raw.go b/gee-orm/day2-reflect-schema/session/raw.go index abaaeb7..862c501 100644 --- a/gee-orm/day2-reflect-schema/session/raw.go +++ b/gee-orm/day2-reflect-schema/session/raw.go @@ -5,6 +5,7 @@ import ( "geeorm/dialect" "geeorm/log" "geeorm/schema" + "strings" ) // Session keep a pointer to sql.DB and provides all execution of all @@ -13,7 +14,7 @@ type Session struct { db *sql.DB dialect dialect.Dialect refTable *schema.Schema - sql string + sql strings.Builder sqlVars []interface{} } @@ -27,15 +28,20 @@ func New(db *sql.DB, dialect dialect.Dialect) *Session { // Clear initialize the state of a session func (s *Session) Clear() { + s.sql.Reset() s.sqlVars = nil - s.sql = "" +} + +// DB returns *sql.DB +func (s *Session) DB() *sql.DB { + return s.db } // Exec raw sql with sqlVars func (s *Session) Exec() (result sql.Result, err error) { defer s.Clear() - log.Info(s.sql, s.sqlVars) - if result, err = s.db.Exec(s.sql, s.sqlVars...); err != nil { + log.Info(s.sql.String(), s.sqlVars) + if result, err = s.DB().Exec(s.sql.String(), s.sqlVars...); err != nil { log.Error(err) } return @@ -44,15 +50,15 @@ func (s *Session) Exec() (result sql.Result, err error) { // QueryRow gets a record from db func (s *Session) QueryRow() *sql.Row { defer s.Clear() - log.Info(s.sql, s.sqlVars) - return s.db.QueryRow(s.sql, s.sqlVars...) + log.Info(s.sql.String(), s.sqlVars) + return s.DB().QueryRow(s.sql.String(), s.sqlVars...) } // QueryRows gets a list of records from db func (s *Session) QueryRows() (rows *sql.Rows, err error) { defer s.Clear() - log.Info(s.sql, s.sqlVars) - if rows, err = s.db.Query(s.sql, s.sqlVars...); err != nil { + log.Info(s.sql.String(), s.sqlVars) + if rows, err = s.DB().Query(s.sql.String(), s.sqlVars...); err != nil { log.Error(err) } return @@ -60,7 +66,8 @@ func (s *Session) QueryRows() (rows *sql.Rows, err error) { // Raw appends sql and sqlVars func (s *Session) Raw(sql string, values ...interface{}) *Session { - s.sql += sql + s.sql.WriteString(sql) + s.sql.WriteString(" ") s.sqlVars = append(s.sqlVars, values...) return s } diff --git a/gee-orm/day2-reflect-schema/session/raw_test.go b/gee-orm/day2-reflect-schema/session/raw_test.go index ed447a5..a96770b 100644 --- a/gee-orm/day2-reflect-schema/session/raw_test.go +++ b/gee-orm/day2-reflect-schema/session/raw_test.go @@ -30,7 +30,7 @@ func TestSession_Exec(t *testing.T) { s := NewSession() _, _ = s.Raw("DROP TABLE IF EXISTS User;").Exec() _, _ = s.Raw("CREATE TABLE User(name text);").Exec() - result, _ := s.Raw("INSERT INTO User(`name`) values (?), (?)", "Tom", "Sam").Exec() + result, _ := s.Raw("INSERT INTO User(`Name`) values (?), (?)", "Tom", "Sam").Exec() if count, err := result.RowsAffected(); err != nil || count != 2 { t.Fatal("expect 2, but got", count) } @@ -39,7 +39,7 @@ func TestSession_Exec(t *testing.T) { func TestSession_QueryRows(t *testing.T) { s := NewSession() _, _ = s.Raw("DROP TABLE IF EXISTS User;").Exec() - _, _ = s.Raw("CREATE TABLE User(name text);").Exec() + _, _ = s.Raw("CREATE TABLE User(Name text);").Exec() row := s.Raw("SELECT count(*) FROM User").QueryRow() var count int if err := row.Scan(&count); err != nil || count != 0 { diff --git a/gee-orm/day2-reflect-schema/session/table.go b/gee-orm/day2-reflect-schema/session/table.go index 525e249..58e7b0f 100644 --- a/gee-orm/day2-reflect-schema/session/table.go +++ b/gee-orm/day2-reflect-schema/session/table.go @@ -2,58 +2,53 @@ package session import ( "fmt" + "geeorm/log" + "reflect" "strings" "geeorm/schema" ) -// RefTable returns a Schema instance that contains all parsed fields -func (s *Session) RefTable(value interface{}) *schema.Schema { - if value == nil { - panic("value is nil") +// Model assigns refTable +func (s *Session) Model(value interface{}) *Session { + // nil or different model, update refTable + if s.refTable == nil || reflect.TypeOf(value) != reflect.TypeOf(s.refTable.Model) { + s.refTable = schema.Parse(value, s.dialect) } + return s +} + +// RefTable returns a Schema instance that contains all parsed fields +func (s *Session) RefTable() *schema.Schema { if s.refTable == nil { - s.refTable = schema.Parse(value, s.dialect) + log.Error("Model is not set") } return s.refTable } // CreateTable create a table in database with a model -func (s *Session) CreateTable(value interface{}) error { - table := s.RefTable(value) +func (s *Session) CreateTable() error { + table := s.RefTable() var columns []string for _, field := range table.Fields { - tag := field.Tag - if field.Name == table.PrimaryField.Name { - tag = table.PrimaryField.Tag - } - columns = append(columns, fmt.Sprintf("%s %s", field.Name, tag)) + columns = append(columns, fmt.Sprintf("%s %s %s", field.Name, field.Type, field.Tag)) } desc := strings.Join(columns, ",") - _, err := s.Raw(fmt.Sprintf("CREATE TABLE %s (%s);", table.TableName, desc)).Exec() + _, err := s.Raw(fmt.Sprintf("CREATE TABLE %s (%s);", table.Name, desc)).Exec() return err } // DropTable drops a table with the name of model -func (s *Session) DropTable(value interface{}) error { - table := s.RefTable(value) - _, err := s.Raw(fmt.Sprintf("DROP TABLE IF EXISTS %s", table.TableName)).Exec() +func (s *Session) DropTable() error { + _, err := s.Raw(fmt.Sprintf("DROP TABLE IF EXISTS %s", s.RefTable().Name)).Exec() return err } -func (s *Session) guessTableName(value interface{}) string { - if tableName, ok := value.(string); ok { - return tableName - } - return s.RefTable(value).TableName -} - // HasTable returns true of the table exists -func (s *Session) HasTable(value interface{}) bool { - tableName := s.guessTableName(value) - sql, values := s.dialect.TableExistSQL(tableName) +func (s *Session) HasTable() bool { + sql, values := s.dialect.TableExistSQL(s.RefTable().Name) row := s.Raw(sql, values...).QueryRow() var tmp string _ = row.Scan(&tmp) - return tmp == tableName + return tmp == s.RefTable().Name } diff --git a/gee-orm/day2-reflect-schema/session/table_test.go b/gee-orm/day2-reflect-schema/session/table_test.go index 7ed38ac..c070c7b 100644 --- a/gee-orm/day2-reflect-schema/session/table_test.go +++ b/gee-orm/day2-reflect-schema/session/table_test.go @@ -5,15 +5,23 @@ import ( ) type User struct { - Name string + Name string `geeorm:"PRIMARY KEY"` Age int } - func TestSession_CreateTable(t *testing.T) { - s := NewSession() - _ = s.DropTable(&User{}) - _ = s.CreateTable(&User{}) - if !s.HasTable("User") { - t.Fatal("failed to create table User") + s := NewSession().Model(&User{}) + _ = s.DropTable() + _ = s.CreateTable() + if !s.HasTable() { + t.Fatal("Failed to create table User") + } +} + +func TestSession_Model(t *testing.T) { + s := NewSession().Model(&User{}) + table := s.RefTable() + s.Model(&Session{}) + if table.Name != "User" || s.RefTable().Name != "Session" { + t.Fatal("Failed to change model") } } diff --git a/gee-orm/day3-save-query/clause/generator.go b/gee-orm/day3-save-query/clause/generator.go index 0ac29ee..b257469 100644 --- a/gee-orm/day3-save-query/clause/generator.go +++ b/gee-orm/day3-save-query/clause/generator.go @@ -16,7 +16,7 @@ func init() { generators[SELECT] = _select generators[LIMIT] = _limit generators[WHERE] = _where - generators[ORDERBY] = _orderby + generators[ORDERBY] = _orderBy } func genBindVars(num int) string { @@ -73,6 +73,6 @@ func _where(values ...interface{}) (string, []interface{}) { return fmt.Sprintf("WHERE %s", desc), vars } -func _orderby(values ...interface{}) (string, []interface{}) { +func _orderBy(values ...interface{}) (string, []interface{}) { return fmt.Sprintf("ORDER BY %s", values[0]), []interface{}{} } diff --git a/gee-orm/day3-save-query/geeorm.go b/gee-orm/day3-save-query/geeorm.go index 61ee9e0..b1881ce 100644 --- a/gee-orm/day3-save-query/geeorm.go +++ b/gee-orm/day3-save-query/geeorm.go @@ -38,14 +38,14 @@ func NewEngine(driver, source string) (e *Engine, err error) { } // Close database connection -func (e *Engine) Close() (err error) { - if err = e.db.Close(); err == nil { - log.Info("Close database success") +func (engine *Engine) Close() { + if err := engine.db.Close(); err != nil { + log.Error("Failed to close database") } - return + log.Info("Close database success") } // NewSession creates a new session for next operations -func (e *Engine) NewSession() *session.Session { - return session.New(e.db, e.dialect) +func (engine *Engine) NewSession() *session.Session { + return session.New(engine.db, engine.dialect) } diff --git a/gee-orm/day3-save-query/geeorm_test.go b/gee-orm/day3-save-query/geeorm_test.go index 35628be..c6da191 100644 --- a/gee-orm/day3-save-query/geeorm_test.go +++ b/gee-orm/day3-save-query/geeorm_test.go @@ -16,5 +16,5 @@ func OpenDB(t *testing.T) *Engine { func TestNewEngine(t *testing.T) { engine := OpenDB(t) - _ = engine.Close() + defer engine.Close() } diff --git a/gee-orm/day3-save-query/schema/schema.go b/gee-orm/day3-save-query/schema/schema.go index 8519fd4..ff76283 100644 --- a/gee-orm/day3-save-query/schema/schema.go +++ b/gee-orm/day3-save-query/schema/schema.go @@ -1,7 +1,6 @@ package schema import ( - "fmt" "geeorm/dialect" "go/ast" "reflect" @@ -10,19 +9,26 @@ import ( // Field represents a column of database type Field struct { Name string + Type string Tag string } // Schema represents a table of database type Schema struct { - TableName string - PrimaryField *Field - Fields []*Field - FieldNames []string + Model interface{} + Name string + Fields []*Field + FieldNames []string + fieldMap map[string]*Field +} + +// GetField returns field by name +func (schema *Schema) GetField(name string) *Field { + return schema.fieldMap[name] } // Values return the values of dest's member variables -func (schema *Schema) Values(dest interface{}) []interface{} { +func (schema *Schema) RecordValues(dest interface{}) []interface{} { destValue := reflect.Indirect(reflect.ValueOf(dest)) var fieldValues []interface{} for _, field := range schema.Fields { @@ -35,8 +41,9 @@ func (schema *Schema) Values(dest interface{}) []interface{} { func Parse(dest interface{}, d dialect.Dialect) *Schema { modelType := reflect.Indirect(reflect.ValueOf(dest)).Type() schema := &Schema{ - TableName: modelType.Name(), - PrimaryField: &Field{Name: "ID", Tag: ""}, + Model: dest, + Name: modelType.Name(), + fieldMap: make(map[string]*Field), } for i := 0; i < modelType.NumField(); i++ { @@ -44,24 +51,15 @@ func Parse(dest interface{}, d dialect.Dialect) *Schema { if !p.Anonymous && ast.IsExported(p.Name) { field := &Field{ Name: p.Name, - Tag: d.DataTypeOf(reflect.Indirect(reflect.New(p.Type))), + Type: d.DataTypeOf(reflect.Indirect(reflect.New(p.Type))), } - if v, ok := p.Tag.Lookup("geeorm"); ok && v == "primary_key" { - schema.PrimaryField = field + if v, ok := p.Tag.Lookup("geeorm"); ok { + field.Tag = v } schema.Fields = append(schema.Fields, field) schema.FieldNames = append(schema.FieldNames, p.Name) + schema.fieldMap[p.Name] = field } } return schema } - -// String returns readable string -func (field *Field) String() string { - return fmt.Sprintf("(%s %s)", field.Name, field.Tag) -} - -// String returns readable string -func (schema *Schema) String() string { - return fmt.Sprintf("TABLE %s %v", schema.TableName, schema.Fields) -} diff --git a/gee-orm/day3-save-query/schema/schema_test.go b/gee-orm/day3-save-query/schema/schema_test.go index aba3e0b..47ae9fc 100644 --- a/gee-orm/day3-save-query/schema/schema_test.go +++ b/gee-orm/day3-save-query/schema/schema_test.go @@ -6,7 +6,7 @@ import ( ) type User struct { - Name string `geeorm:"primary_key"` + Name string `geeorm:"PRIMARY KEY"` Age int } @@ -14,18 +14,17 @@ var TestDial, _ = dialect.GetDialect("sqlite3") func TestParse(t *testing.T) { schema := Parse(&User{}, TestDial) - if schema.TableName != "User" || len(schema.Fields) != 2 { + if schema.Name != "User" || len(schema.Fields) != 2 { t.Fatal("failed to parse User struct") } - if schema.PrimaryField.Name != "Name" { + if schema.GetField("Name").Tag != "PRIMARY KEY" { t.Fatal("failed to parse primary key") } - t.Log(schema) } -func TestSchema_Values(t *testing.T) { +func TestSchema_RecordValues(t *testing.T) { schema := Parse(&User{}, TestDial) - values := schema.Values(&User{"Tom", 18}) + values := schema.RecordValues(&User{"Tom", 18}) name := values[0].(string) age := values[1].(int) diff --git a/gee-orm/day3-save-query/session/raw.go b/gee-orm/day3-save-query/session/raw.go index a47edcc..161fcb4 100644 --- a/gee-orm/day3-save-query/session/raw.go +++ b/gee-orm/day3-save-query/session/raw.go @@ -6,6 +6,7 @@ import ( "geeorm/dialect" "geeorm/log" "geeorm/schema" + "strings" ) // Session keep a pointer to sql.DB and provides all execution of all @@ -15,7 +16,7 @@ type Session struct { dialect dialect.Dialect refTable *schema.Schema clause clause.Clause - sql string + sql strings.Builder sqlVars []interface{} } @@ -29,16 +30,21 @@ func New(db *sql.DB, dialect dialect.Dialect) *Session { // Clear initialize the state of a session func (s *Session) Clear() { - s.refTable, s.sqlVars = nil, nil + s.sql.Reset() + s.sqlVars = nil s.clause = clause.Clause{} - s.sql = "" +} + +// DB returns *sql.DB +func (s *Session) DB() *sql.DB { + return s.db } // Exec raw sql with sqlVars func (s *Session) Exec() (result sql.Result, err error) { defer s.Clear() - log.Info(s.sql, s.sqlVars) - if result, err = s.db.Exec(s.sql, s.sqlVars...); err != nil { + log.Info(s.sql.String(), s.sqlVars) + if result, err = s.DB().Exec(s.sql.String(), s.sqlVars...); err != nil { log.Error(err) } return @@ -47,15 +53,15 @@ func (s *Session) Exec() (result sql.Result, err error) { // QueryRow gets a record from db func (s *Session) QueryRow() *sql.Row { defer s.Clear() - log.Info(s.sql, s.sqlVars) - return s.db.QueryRow(s.sql, s.sqlVars...) + log.Info(s.sql.String(), s.sqlVars) + return s.DB().QueryRow(s.sql.String(), s.sqlVars...) } // QueryRows gets a list of records from db func (s *Session) QueryRows() (rows *sql.Rows, err error) { defer s.Clear() - log.Info(s.sql, s.sqlVars) - if rows, err = s.db.Query(s.sql, s.sqlVars...); err != nil { + log.Info(s.sql.String(), s.sqlVars) + if rows, err = s.DB().Query(s.sql.String(), s.sqlVars...); err != nil { log.Error(err) } return @@ -63,7 +69,8 @@ func (s *Session) QueryRows() (rows *sql.Rows, err error) { // Raw appends sql and sqlVars func (s *Session) Raw(sql string, values ...interface{}) *Session { - s.sql += sql + s.sql.WriteString(sql) + s.sql.WriteString(" ") s.sqlVars = append(s.sqlVars, values...) return s -} +} \ No newline at end of file diff --git a/gee-orm/day3-save-query/session/raw_test.go b/gee-orm/day3-save-query/session/raw_test.go index ed447a5..a96770b 100644 --- a/gee-orm/day3-save-query/session/raw_test.go +++ b/gee-orm/day3-save-query/session/raw_test.go @@ -30,7 +30,7 @@ func TestSession_Exec(t *testing.T) { s := NewSession() _, _ = s.Raw("DROP TABLE IF EXISTS User;").Exec() _, _ = s.Raw("CREATE TABLE User(name text);").Exec() - result, _ := s.Raw("INSERT INTO User(`name`) values (?), (?)", "Tom", "Sam").Exec() + result, _ := s.Raw("INSERT INTO User(`Name`) values (?), (?)", "Tom", "Sam").Exec() if count, err := result.RowsAffected(); err != nil || count != 2 { t.Fatal("expect 2, but got", count) } @@ -39,7 +39,7 @@ func TestSession_Exec(t *testing.T) { func TestSession_QueryRows(t *testing.T) { s := NewSession() _, _ = s.Raw("DROP TABLE IF EXISTS User;").Exec() - _, _ = s.Raw("CREATE TABLE User(name text);").Exec() + _, _ = s.Raw("CREATE TABLE User(Name text);").Exec() row := s.Raw("SELECT count(*) FROM User").QueryRow() var count int if err := row.Scan(&count); err != nil || count != 0 { diff --git a/gee-orm/day3-save-query/session/record.go b/gee-orm/day3-save-query/session/record.go index 1a0672a..69fd50d 100644 --- a/gee-orm/day3-save-query/session/record.go +++ b/gee-orm/day3-save-query/session/record.go @@ -6,12 +6,12 @@ import ( ) // Create one or more records in database -func (s *Session) Create(values ...interface{}) (int64, error) { +func (s *Session) Insert(values ...interface{}) (int64, error) { recordValues := make([]interface{}, 0) for _, value := range values { - table := s.RefTable(value) - s.clause.Set(clause.INSERT, table.TableName, table.FieldNames) - recordValues = append(recordValues, table.Values(value)) + table := s.Model(value).RefTable() + s.clause.Set(clause.INSERT, table.Name, table.FieldNames) + recordValues = append(recordValues, table.RecordValues(value)) } s.clause.Set(clause.VALUES, recordValues...) @@ -28,9 +28,9 @@ func (s *Session) Create(values ...interface{}) (int64, error) { func (s *Session) Find(values interface{}) error { destSlice := reflect.Indirect(reflect.ValueOf(values)) destType := destSlice.Type().Elem() - table := s.RefTable(reflect.New(destType).Elem().Interface()) + table := s.Model(reflect.New(destType).Elem().Interface()).RefTable() - s.clause.Set(clause.SELECT, table.TableName, table.FieldNames) + s.clause.Set(clause.SELECT, table.Name, table.FieldNames) sql, vars := s.clause.Build(clause.SELECT, clause.WHERE, clause.ORDERBY, clause.LIMIT) rows, err := s.Raw(sql, vars...).QueryRows() if err != nil { diff --git a/gee-orm/day3-save-query/session/record_test.go b/gee-orm/day3-save-query/session/record_test.go index 08881fa..67bfb2a 100644 --- a/gee-orm/day3-save-query/session/record_test.go +++ b/gee-orm/day3-save-query/session/record_test.go @@ -10,19 +10,19 @@ var ( func testRecordInit(t *testing.T) *Session { t.Helper() - s := NewSession() - err1 := s.DropTable(&User{}) - err2 := s.CreateTable(&User{}) - _, err3 := s.Create(user1, user2) + s := NewSession().Model(&User{}) + err1 := s.DropTable() + err2 := s.CreateTable() + _, err3 := s.Insert(user1, user2) if err1 != nil || err2 != nil || err3 != nil { t.Fatal("failed init test records") } return s } -func TestSession_Create(t *testing.T) { +func TestSession_Insert(t *testing.T) { s := testRecordInit(t) - affected, err := s.Create(user3) + affected, err := s.Insert(user3) if err != nil || affected != 1 { t.Fatal("failed to create record") } @@ -30,7 +30,7 @@ func TestSession_Create(t *testing.T) { func TestSession_Find(t *testing.T) { s := testRecordInit(t) - users := []User{} + var users []User if err := s.Find(&users); err != nil || len(users) != 2 { t.Fatal("failed to query all") } diff --git a/gee-orm/day3-save-query/session/table.go b/gee-orm/day3-save-query/session/table.go index b644d3a..58e7b0f 100644 --- a/gee-orm/day3-save-query/session/table.go +++ b/gee-orm/day3-save-query/session/table.go @@ -2,58 +2,53 @@ package session import ( "fmt" + "geeorm/log" + "reflect" "strings" "geeorm/schema" ) -// RefTable returns a Schema instance that contains all parsed fields -func (s *Session) RefTable(value interface{}) *schema.Schema { - if value == nil { - panic("value is nil") +// Model assigns refTable +func (s *Session) Model(value interface{}) *Session { + // nil or different model, update refTable + if s.refTable == nil || reflect.TypeOf(value) != reflect.TypeOf(s.refTable.Model) { + s.refTable = schema.Parse(value, s.dialect) } + return s +} + +// RefTable returns a Schema instance that contains all parsed fields +func (s *Session) RefTable() *schema.Schema { if s.refTable == nil { - s.refTable = schema.Parse(value, s.dialect) + log.Error("Model is not set") } return s.refTable } // CreateTable create a table in database with a model -func (s *Session) CreateTable(value interface{}) error { - table := s.RefTable(value) +func (s *Session) CreateTable() error { + table := s.RefTable() var columns []string for _, field := range table.Fields { - tag := field.Tag - if field.Name == table.PrimaryField.Name { - tag += " PRIMARY KEY" - } - columns = append(columns, fmt.Sprintf("%s %s", field.Name, tag)) + columns = append(columns, fmt.Sprintf("%s %s %s", field.Name, field.Type, field.Tag)) } desc := strings.Join(columns, ",") - _, err := s.Raw(fmt.Sprintf("CREATE TABLE %s (%s);", table.TableName, desc)).Exec() + _, err := s.Raw(fmt.Sprintf("CREATE TABLE %s (%s);", table.Name, desc)).Exec() return err } // DropTable drops a table with the name of model -func (s *Session) DropTable(value interface{}) error { - table := s.RefTable(value) - _, err := s.Raw(fmt.Sprintf("DROP TABLE IF EXISTS %s", table.TableName)).Exec() +func (s *Session) DropTable() error { + _, err := s.Raw(fmt.Sprintf("DROP TABLE IF EXISTS %s", s.RefTable().Name)).Exec() return err } -func (s *Session) guessTableName(value interface{}) string { - if tableName, ok := value.(string); ok { - return tableName - } - return s.RefTable(value).TableName -} - // HasTable returns true of the table exists -func (s *Session) HasTable(value interface{}) bool { - tableName := s.guessTableName(value) - sql, values := s.dialect.TableExistSQL(tableName) +func (s *Session) HasTable() bool { + sql, values := s.dialect.TableExistSQL(s.RefTable().Name) row := s.Raw(sql, values...).QueryRow() var tmp string _ = row.Scan(&tmp) - return tmp == tableName + return tmp == s.RefTable().Name } diff --git a/gee-orm/day3-save-query/session/table_test.go b/gee-orm/day3-save-query/session/table_test.go index 91eb519..3bb7554 100644 --- a/gee-orm/day3-save-query/session/table_test.go +++ b/gee-orm/day3-save-query/session/table_test.go @@ -5,15 +5,24 @@ import ( ) type User struct { - Name string `geeorm:"primary_key"` + Name string `geeorm:"PRIMARY KEY"` Age int } func TestSession_CreateTable(t *testing.T) { - s := NewSession() - _ = s.DropTable(&User{}) - _ = s.CreateTable(&User{}) - if !s.HasTable("User") { - t.Fatal("failed to create table User") + s := NewSession().Model(&User{}) + _ = s.DropTable() + _ = s.CreateTable() + if !s.HasTable() { + t.Fatal("Failed to create table User") + } +} + +func TestSession_Model(t *testing.T) { + s := NewSession().Model(&User{}) + table := s.RefTable() + s.Model(&Session{}) + if table.Name != "User" || s.RefTable().Name != "Session" { + t.Fatal("Failed to change model") } } diff --git a/gee-orm/day4-chain-operation/clause/clause.go b/gee-orm/day4-chain-operation/clause/clause.go index daa930d..02fcf93 100644 --- a/gee-orm/day4-chain-operation/clause/clause.go +++ b/gee-orm/day4-chain-operation/clause/clause.go @@ -21,6 +21,9 @@ const ( LIMIT WHERE ORDERBY + UPDATE + DELETE + COUNT ) // Set adds a sub clause of specific type diff --git a/gee-orm/day4-chain-operation/clause/clause_test.go b/gee-orm/day4-chain-operation/clause/clause_test.go index f5267ca..b54c6e0 100644 --- a/gee-orm/day4-chain-operation/clause/clause_test.go +++ b/gee-orm/day4-chain-operation/clause/clause_test.go @@ -32,8 +32,43 @@ func testSelect(t *testing.T) { } } +func testUpdate(t *testing.T) { + var clause Clause + clause.Set(UPDATE, "User", map[string]interface{}{"Age": 30, "Name": "Tommy"}) + clause.Set(WHERE, "Name = ?", "Tom") + sql, vars := clause.Build(UPDATE, WHERE) + t.Log(sql, vars) + if sql != "UPDATE User SET Age = ?, Name = ? WHERE Name = ?" { + t.Fatal("failed to build SQL") + } + if !reflect.DeepEqual(vars, []interface{}{30, "Tommy", "Tom"}) { + t.Fatal("failed to build SQLVars") + } +} + +func testDelete(t *testing.T) { + var clause Clause + clause.Set(DELETE, "User") + clause.Set(WHERE, "Name = ?", "Tom") + + sql, vars := clause.Build(DELETE, WHERE) + t.Log(sql, vars) + if sql != "DELETE FROM User WHERE Name = ?" { + t.Fatal("failed to build SQL") + } + if !reflect.DeepEqual(vars, []interface{}{"Tom"}) { + t.Fatal("failed to build SQLVars") + } +} + func TestClause_Build(t *testing.T) { t.Run("select", func(t *testing.T) { testSelect(t) }) + t.Run("update", func(t *testing.T) { + testUpdate(t) + }) + t.Run("delete", func(t *testing.T) { + testDelete(t) + }) } diff --git a/gee-orm/day4-chain-operation/clause/generator.go b/gee-orm/day4-chain-operation/clause/generator.go index 0ac29ee..127fc43 100644 --- a/gee-orm/day4-chain-operation/clause/generator.go +++ b/gee-orm/day4-chain-operation/clause/generator.go @@ -16,7 +16,10 @@ func init() { generators[SELECT] = _select generators[LIMIT] = _limit generators[WHERE] = _where - generators[ORDERBY] = _orderby + generators[ORDERBY] = _orderBy + generators[UPDATE] = _update + generators[DELETE] = _delete + generators[COUNT] = _count } func genBindVars(num int) string { @@ -73,6 +76,26 @@ func _where(values ...interface{}) (string, []interface{}) { return fmt.Sprintf("WHERE %s", desc), vars } -func _orderby(values ...interface{}) (string, []interface{}) { +func _orderBy(values ...interface{}) (string, []interface{}) { return fmt.Sprintf("ORDER BY %s", values[0]), []interface{}{} } + +func _update(values ...interface{}) (string, []interface{}) { + tableName := values[0] + m := values[1].(map[string]interface{}) + var keys []string + var vars []interface{} + for k, v := range m { + keys = append(keys, k+" = ?") + vars = append(vars, v) + } + return fmt.Sprintf("UPDATE %s SET %s", tableName, strings.Join(keys, ", ")), vars +} + +func _delete(values ...interface{}) (string, []interface{}) { + return fmt.Sprintf("DELETE FROM %s", values[0]), []interface{}{} +} + +func _count(values ...interface{}) (string, []interface{}) { + return _select(values[0], []string{"count(*)"}) +} diff --git a/gee-orm/day4-chain-operation/geeorm.go b/gee-orm/day4-chain-operation/geeorm.go index 61ee9e0..b1881ce 100644 --- a/gee-orm/day4-chain-operation/geeorm.go +++ b/gee-orm/day4-chain-operation/geeorm.go @@ -38,14 +38,14 @@ func NewEngine(driver, source string) (e *Engine, err error) { } // Close database connection -func (e *Engine) Close() (err error) { - if err = e.db.Close(); err == nil { - log.Info("Close database success") +func (engine *Engine) Close() { + if err := engine.db.Close(); err != nil { + log.Error("Failed to close database") } - return + log.Info("Close database success") } // NewSession creates a new session for next operations -func (e *Engine) NewSession() *session.Session { - return session.New(e.db, e.dialect) +func (engine *Engine) NewSession() *session.Session { + return session.New(engine.db, engine.dialect) } diff --git a/gee-orm/day4-chain-operation/geeorm_test.go b/gee-orm/day4-chain-operation/geeorm_test.go index 35628be..c6da191 100644 --- a/gee-orm/day4-chain-operation/geeorm_test.go +++ b/gee-orm/day4-chain-operation/geeorm_test.go @@ -16,5 +16,5 @@ func OpenDB(t *testing.T) *Engine { func TestNewEngine(t *testing.T) { engine := OpenDB(t) - _ = engine.Close() + defer engine.Close() } diff --git a/gee-orm/day4-chain-operation/schema/schema.go b/gee-orm/day4-chain-operation/schema/schema.go index 8519fd4..ff76283 100644 --- a/gee-orm/day4-chain-operation/schema/schema.go +++ b/gee-orm/day4-chain-operation/schema/schema.go @@ -1,7 +1,6 @@ package schema import ( - "fmt" "geeorm/dialect" "go/ast" "reflect" @@ -10,19 +9,26 @@ import ( // Field represents a column of database type Field struct { Name string + Type string Tag string } // Schema represents a table of database type Schema struct { - TableName string - PrimaryField *Field - Fields []*Field - FieldNames []string + Model interface{} + Name string + Fields []*Field + FieldNames []string + fieldMap map[string]*Field +} + +// GetField returns field by name +func (schema *Schema) GetField(name string) *Field { + return schema.fieldMap[name] } // Values return the values of dest's member variables -func (schema *Schema) Values(dest interface{}) []interface{} { +func (schema *Schema) RecordValues(dest interface{}) []interface{} { destValue := reflect.Indirect(reflect.ValueOf(dest)) var fieldValues []interface{} for _, field := range schema.Fields { @@ -35,8 +41,9 @@ func (schema *Schema) Values(dest interface{}) []interface{} { func Parse(dest interface{}, d dialect.Dialect) *Schema { modelType := reflect.Indirect(reflect.ValueOf(dest)).Type() schema := &Schema{ - TableName: modelType.Name(), - PrimaryField: &Field{Name: "ID", Tag: ""}, + Model: dest, + Name: modelType.Name(), + fieldMap: make(map[string]*Field), } for i := 0; i < modelType.NumField(); i++ { @@ -44,24 +51,15 @@ func Parse(dest interface{}, d dialect.Dialect) *Schema { if !p.Anonymous && ast.IsExported(p.Name) { field := &Field{ Name: p.Name, - Tag: d.DataTypeOf(reflect.Indirect(reflect.New(p.Type))), + Type: d.DataTypeOf(reflect.Indirect(reflect.New(p.Type))), } - if v, ok := p.Tag.Lookup("geeorm"); ok && v == "primary_key" { - schema.PrimaryField = field + if v, ok := p.Tag.Lookup("geeorm"); ok { + field.Tag = v } schema.Fields = append(schema.Fields, field) schema.FieldNames = append(schema.FieldNames, p.Name) + schema.fieldMap[p.Name] = field } } return schema } - -// String returns readable string -func (field *Field) String() string { - return fmt.Sprintf("(%s %s)", field.Name, field.Tag) -} - -// String returns readable string -func (schema *Schema) String() string { - return fmt.Sprintf("TABLE %s %v", schema.TableName, schema.Fields) -} diff --git a/gee-orm/day4-chain-operation/schema/schema_test.go b/gee-orm/day4-chain-operation/schema/schema_test.go index aba3e0b..47ae9fc 100644 --- a/gee-orm/day4-chain-operation/schema/schema_test.go +++ b/gee-orm/day4-chain-operation/schema/schema_test.go @@ -6,7 +6,7 @@ import ( ) type User struct { - Name string `geeorm:"primary_key"` + Name string `geeorm:"PRIMARY KEY"` Age int } @@ -14,18 +14,17 @@ var TestDial, _ = dialect.GetDialect("sqlite3") func TestParse(t *testing.T) { schema := Parse(&User{}, TestDial) - if schema.TableName != "User" || len(schema.Fields) != 2 { + if schema.Name != "User" || len(schema.Fields) != 2 { t.Fatal("failed to parse User struct") } - if schema.PrimaryField.Name != "Name" { + if schema.GetField("Name").Tag != "PRIMARY KEY" { t.Fatal("failed to parse primary key") } - t.Log(schema) } -func TestSchema_Values(t *testing.T) { +func TestSchema_RecordValues(t *testing.T) { schema := Parse(&User{}, TestDial) - values := schema.Values(&User{"Tom", 18}) + values := schema.RecordValues(&User{"Tom", 18}) name := values[0].(string) age := values[1].(int) diff --git a/gee-orm/day4-chain-operation/session/raw.go b/gee-orm/day4-chain-operation/session/raw.go index a47edcc..161fcb4 100644 --- a/gee-orm/day4-chain-operation/session/raw.go +++ b/gee-orm/day4-chain-operation/session/raw.go @@ -6,6 +6,7 @@ import ( "geeorm/dialect" "geeorm/log" "geeorm/schema" + "strings" ) // Session keep a pointer to sql.DB and provides all execution of all @@ -15,7 +16,7 @@ type Session struct { dialect dialect.Dialect refTable *schema.Schema clause clause.Clause - sql string + sql strings.Builder sqlVars []interface{} } @@ -29,16 +30,21 @@ func New(db *sql.DB, dialect dialect.Dialect) *Session { // Clear initialize the state of a session func (s *Session) Clear() { - s.refTable, s.sqlVars = nil, nil + s.sql.Reset() + s.sqlVars = nil s.clause = clause.Clause{} - s.sql = "" +} + +// DB returns *sql.DB +func (s *Session) DB() *sql.DB { + return s.db } // Exec raw sql with sqlVars func (s *Session) Exec() (result sql.Result, err error) { defer s.Clear() - log.Info(s.sql, s.sqlVars) - if result, err = s.db.Exec(s.sql, s.sqlVars...); err != nil { + log.Info(s.sql.String(), s.sqlVars) + if result, err = s.DB().Exec(s.sql.String(), s.sqlVars...); err != nil { log.Error(err) } return @@ -47,15 +53,15 @@ func (s *Session) Exec() (result sql.Result, err error) { // QueryRow gets a record from db func (s *Session) QueryRow() *sql.Row { defer s.Clear() - log.Info(s.sql, s.sqlVars) - return s.db.QueryRow(s.sql, s.sqlVars...) + log.Info(s.sql.String(), s.sqlVars) + return s.DB().QueryRow(s.sql.String(), s.sqlVars...) } // QueryRows gets a list of records from db func (s *Session) QueryRows() (rows *sql.Rows, err error) { defer s.Clear() - log.Info(s.sql, s.sqlVars) - if rows, err = s.db.Query(s.sql, s.sqlVars...); err != nil { + log.Info(s.sql.String(), s.sqlVars) + if rows, err = s.DB().Query(s.sql.String(), s.sqlVars...); err != nil { log.Error(err) } return @@ -63,7 +69,8 @@ func (s *Session) QueryRows() (rows *sql.Rows, err error) { // Raw appends sql and sqlVars func (s *Session) Raw(sql string, values ...interface{}) *Session { - s.sql += sql + s.sql.WriteString(sql) + s.sql.WriteString(" ") s.sqlVars = append(s.sqlVars, values...) return s -} +} \ No newline at end of file diff --git a/gee-orm/day4-chain-operation/session/raw_test.go b/gee-orm/day4-chain-operation/session/raw_test.go index ed447a5..a96770b 100644 --- a/gee-orm/day4-chain-operation/session/raw_test.go +++ b/gee-orm/day4-chain-operation/session/raw_test.go @@ -30,7 +30,7 @@ func TestSession_Exec(t *testing.T) { s := NewSession() _, _ = s.Raw("DROP TABLE IF EXISTS User;").Exec() _, _ = s.Raw("CREATE TABLE User(name text);").Exec() - result, _ := s.Raw("INSERT INTO User(`name`) values (?), (?)", "Tom", "Sam").Exec() + result, _ := s.Raw("INSERT INTO User(`Name`) values (?), (?)", "Tom", "Sam").Exec() if count, err := result.RowsAffected(); err != nil || count != 2 { t.Fatal("expect 2, but got", count) } @@ -39,7 +39,7 @@ func TestSession_Exec(t *testing.T) { func TestSession_QueryRows(t *testing.T) { s := NewSession() _, _ = s.Raw("DROP TABLE IF EXISTS User;").Exec() - _, _ = s.Raw("CREATE TABLE User(name text);").Exec() + _, _ = s.Raw("CREATE TABLE User(Name text);").Exec() row := s.Raw("SELECT count(*) FROM User").QueryRow() var count int if err := row.Scan(&count); err != nil || count != 0 { diff --git a/gee-orm/day4-chain-operation/session/record.go b/gee-orm/day4-chain-operation/session/record.go index c9902d0..b2850cf 100644 --- a/gee-orm/day4-chain-operation/session/record.go +++ b/gee-orm/day4-chain-operation/session/record.go @@ -1,17 +1,18 @@ package session import ( + "errors" "geeorm/clause" "reflect" ) // Create one or more records in database -func (s *Session) Create(values ...interface{}) (int64, error) { +func (s *Session) Insert(values ...interface{}) (int64, error) { recordValues := make([]interface{}, 0) for _, value := range values { - table := s.RefTable(value) - s.clause.Set(clause.INSERT, table.TableName, table.FieldNames) - recordValues = append(recordValues, table.Values(value)) + table := s.Model(value).RefTable() + s.clause.Set(clause.INSERT, table.Name, table.FieldNames) + recordValues = append(recordValues, table.RecordValues(value)) } s.clause.Set(clause.VALUES, recordValues...) @@ -28,9 +29,9 @@ func (s *Session) Create(values ...interface{}) (int64, error) { func (s *Session) Find(values interface{}) error { destSlice := reflect.Indirect(reflect.ValueOf(values)) destType := destSlice.Type().Elem() - table := s.RefTable(reflect.New(destType).Elem().Interface()) + table := s.Model(reflect.New(destType).Elem().Interface()).RefTable() - s.clause.Set(clause.SELECT, table.TableName, table.FieldNames) + s.clause.Set(clause.SELECT, table.Name, table.FieldNames) sql, vars := s.clause.Build(clause.SELECT, clause.WHERE, clause.ORDERBY, clause.LIMIT) rows, err := s.Raw(sql, vars...).QueryRows() if err != nil { @@ -55,9 +56,14 @@ func (s *Session) Find(values interface{}) error { func (s *Session) First(value interface{}) error { dest := reflect.Indirect(reflect.ValueOf(value)) destSlice := reflect.New(reflect.SliceOf(dest.Type())).Elem() - err := s.Limit(1).Find(destSlice.Addr().Interface()) + if err := s.Limit(1).Find(destSlice.Addr().Interface()); err != nil { + return err + } + if destSlice.Len() == 0 { + return errors.New("NOT FOUND") + } dest.Set(destSlice.Index(0)) - return err + return nil } // Limit adds limit condition to clause @@ -78,3 +84,46 @@ func (s *Session) OrderBy(desc string) *Session { s.clause.Set(clause.ORDERBY, desc) return s } + +// Update records with where clause +// support map[string]interface{} +// also support kv list: "Name", "Tom", "Age", 18, .... +func (s *Session) Update(kv ...interface{}) (int64, error) { + m, ok := kv[0].(map[string]interface{}) + if !ok { + m = make(map[string]interface{}) + for i := 0; i < len(kv); i += 2 { + m[kv[i].(string)] = kv[i+1] + } + } + s.clause.Set(clause.UPDATE, s.RefTable().Name, m) + sql, vars := s.clause.Build(clause.UPDATE, clause.WHERE) + result, err := s.Raw(sql, vars...).Exec() + if err != nil { + return 0, err + } + return result.RowsAffected() +} + +// Delete records with where clause +func (s *Session) Delete() (int64, error) { + s.clause.Set(clause.DELETE, s.RefTable().Name) + sql, vars := s.clause.Build(clause.DELETE, clause.WHERE) + result, err := s.Raw(sql, vars...).Exec() + if err != nil { + return 0, err + } + return result.RowsAffected() +} + +// Count records with where clause +func (s *Session) Count() (int64, error) { + s.clause.Set(clause.COUNT, s.RefTable().Name) + sql, vars := s.clause.Build(clause.COUNT, clause.WHERE) + row := s.Raw(sql, vars...).QueryRow() + var tmp int64 + if err := row.Scan(&tmp); err != nil { + return 0, err + } + return tmp, nil +} diff --git a/gee-orm/day4-chain-operation/session/record_test.go b/gee-orm/day4-chain-operation/session/record_test.go index 822201b..5d482a0 100644 --- a/gee-orm/day4-chain-operation/session/record_test.go +++ b/gee-orm/day4-chain-operation/session/record_test.go @@ -10,20 +10,19 @@ var ( func testRecordInit(t *testing.T) *Session { t.Helper() - s := NewSession() - err1 := s.DropTable(&User{}) - err2 := s.CreateTable(&User{}) - _, err3 := s.Create(user1, user2) + s := NewSession().Model(&User{}) + err1 := s.DropTable() + err2 := s.CreateTable() + _, err3 := s.Insert(user1, user2) if err1 != nil || err2 != nil || err3 != nil { t.Fatal("failed init test records") } return s - } -func TestSession_Create(t *testing.T) { +func TestSession_Insert(t *testing.T) { s := testRecordInit(t) - affected, err := s.Create(user3) + affected, err := s.Insert(user3) if err != nil || affected != 1 { t.Fatal("failed to create record") } @@ -31,7 +30,7 @@ func TestSession_Create(t *testing.T) { func TestSession_Find(t *testing.T) { s := testRecordInit(t) - users := []User{} + var users []User if err := s.Find(&users); err != nil || len(users) != 2 { t.Fatal("failed to query all") } @@ -58,7 +57,7 @@ func TestSession_Limit(t *testing.T) { func TestSession_Where(t *testing.T) { s := testRecordInit(t) var users []User - _, err1 := s.Create(user3) + _, err1 := s.Insert(user3) err2 := s.Where("Age = ?", 25).Find(&users) if err1 != nil || err2 != nil || len(users) != 2 { @@ -74,5 +73,25 @@ func TestSession_OrderBy(t *testing.T) { if err != nil || u.Age != 25 { t.Fatal("failed to query with order by condition") } +} + +func TestSession_Update(t *testing.T) { + s := testRecordInit(t) + affected, _ := s.Where("Name = ?", "Tom").Update("Age", 30) + u := &User{} + _ = s.OrderBy("Age DESC").First(u) + + if affected != 1 || u.Age != 30 { + t.Fatal("failed to update") + } +} +func TestSession_DeleteAndCount(t *testing.T) { + s := testRecordInit(t) + affected, _ := s.Where("Name = ?", "Tom").Delete() + count, _ := s.Count() + + if affected != 1 || count != 1 { + t.Fatal("failed to delete or count") + } } diff --git a/gee-orm/day4-chain-operation/session/table.go b/gee-orm/day4-chain-operation/session/table.go index b644d3a..58e7b0f 100644 --- a/gee-orm/day4-chain-operation/session/table.go +++ b/gee-orm/day4-chain-operation/session/table.go @@ -2,58 +2,53 @@ package session import ( "fmt" + "geeorm/log" + "reflect" "strings" "geeorm/schema" ) -// RefTable returns a Schema instance that contains all parsed fields -func (s *Session) RefTable(value interface{}) *schema.Schema { - if value == nil { - panic("value is nil") +// Model assigns refTable +func (s *Session) Model(value interface{}) *Session { + // nil or different model, update refTable + if s.refTable == nil || reflect.TypeOf(value) != reflect.TypeOf(s.refTable.Model) { + s.refTable = schema.Parse(value, s.dialect) } + return s +} + +// RefTable returns a Schema instance that contains all parsed fields +func (s *Session) RefTable() *schema.Schema { if s.refTable == nil { - s.refTable = schema.Parse(value, s.dialect) + log.Error("Model is not set") } return s.refTable } // CreateTable create a table in database with a model -func (s *Session) CreateTable(value interface{}) error { - table := s.RefTable(value) +func (s *Session) CreateTable() error { + table := s.RefTable() var columns []string for _, field := range table.Fields { - tag := field.Tag - if field.Name == table.PrimaryField.Name { - tag += " PRIMARY KEY" - } - columns = append(columns, fmt.Sprintf("%s %s", field.Name, tag)) + columns = append(columns, fmt.Sprintf("%s %s %s", field.Name, field.Type, field.Tag)) } desc := strings.Join(columns, ",") - _, err := s.Raw(fmt.Sprintf("CREATE TABLE %s (%s);", table.TableName, desc)).Exec() + _, err := s.Raw(fmt.Sprintf("CREATE TABLE %s (%s);", table.Name, desc)).Exec() return err } // DropTable drops a table with the name of model -func (s *Session) DropTable(value interface{}) error { - table := s.RefTable(value) - _, err := s.Raw(fmt.Sprintf("DROP TABLE IF EXISTS %s", table.TableName)).Exec() +func (s *Session) DropTable() error { + _, err := s.Raw(fmt.Sprintf("DROP TABLE IF EXISTS %s", s.RefTable().Name)).Exec() return err } -func (s *Session) guessTableName(value interface{}) string { - if tableName, ok := value.(string); ok { - return tableName - } - return s.RefTable(value).TableName -} - // HasTable returns true of the table exists -func (s *Session) HasTable(value interface{}) bool { - tableName := s.guessTableName(value) - sql, values := s.dialect.TableExistSQL(tableName) +func (s *Session) HasTable() bool { + sql, values := s.dialect.TableExistSQL(s.RefTable().Name) row := s.Raw(sql, values...).QueryRow() var tmp string _ = row.Scan(&tmp) - return tmp == tableName + return tmp == s.RefTable().Name } diff --git a/gee-orm/day4-chain-operation/session/table_test.go b/gee-orm/day4-chain-operation/session/table_test.go index 91eb519..3bb7554 100644 --- a/gee-orm/day4-chain-operation/session/table_test.go +++ b/gee-orm/day4-chain-operation/session/table_test.go @@ -5,15 +5,24 @@ import ( ) type User struct { - Name string `geeorm:"primary_key"` + Name string `geeorm:"PRIMARY KEY"` Age int } func TestSession_CreateTable(t *testing.T) { - s := NewSession() - _ = s.DropTable(&User{}) - _ = s.CreateTable(&User{}) - if !s.HasTable("User") { - t.Fatal("failed to create table User") + s := NewSession().Model(&User{}) + _ = s.DropTable() + _ = s.CreateTable() + if !s.HasTable() { + t.Fatal("Failed to create table User") + } +} + +func TestSession_Model(t *testing.T) { + s := NewSession().Model(&User{}) + table := s.RefTable() + s.Model(&Session{}) + if table.Name != "User" || s.RefTable().Name != "Session" { + t.Fatal("Failed to change model") } } diff --git a/gee-orm/day5-update-delete/clause/clause.go b/gee-orm/day5-hooks/clause/clause.go similarity index 99% rename from gee-orm/day5-update-delete/clause/clause.go rename to gee-orm/day5-hooks/clause/clause.go index 2195c86..02fcf93 100644 --- a/gee-orm/day5-update-delete/clause/clause.go +++ b/gee-orm/day5-hooks/clause/clause.go @@ -22,7 +22,6 @@ const ( WHERE ORDERBY UPDATE - SET DELETE COUNT ) diff --git a/gee-orm/day5-update-delete/clause/clause_test.go b/gee-orm/day5-hooks/clause/clause_test.go similarity index 92% rename from gee-orm/day5-update-delete/clause/clause_test.go rename to gee-orm/day5-hooks/clause/clause_test.go index 8769c40..b54c6e0 100644 --- a/gee-orm/day5-update-delete/clause/clause_test.go +++ b/gee-orm/day5-hooks/clause/clause_test.go @@ -34,11 +34,9 @@ func testSelect(t *testing.T) { func testUpdate(t *testing.T) { var clause Clause - clause.Set(UPDATE, "User") + clause.Set(UPDATE, "User", map[string]interface{}{"Age": 30, "Name": "Tommy"}) clause.Set(WHERE, "Name = ?", "Tom") - clause.Set(SET, map[string]interface{}{"Age": 30, "Name": "Tommy"}) - - sql, vars := clause.Build(UPDATE, SET, WHERE) + sql, vars := clause.Build(UPDATE, WHERE) t.Log(sql, vars) if sql != "UPDATE User SET Age = ?, Name = ? WHERE Name = ?" { t.Fatal("failed to build SQL") diff --git a/gee-orm/day5-update-delete/clause/generator.go b/gee-orm/day5-hooks/clause/generator.go similarity index 87% rename from gee-orm/day5-update-delete/clause/generator.go rename to gee-orm/day5-hooks/clause/generator.go index 1cf5aec..127fc43 100644 --- a/gee-orm/day5-update-delete/clause/generator.go +++ b/gee-orm/day5-hooks/clause/generator.go @@ -16,9 +16,8 @@ func init() { generators[SELECT] = _select generators[LIMIT] = _limit generators[WHERE] = _where - generators[ORDERBY] = _orderby + generators[ORDERBY] = _orderBy generators[UPDATE] = _update - generators[SET] = _set generators[DELETE] = _delete generators[COUNT] = _count } @@ -77,24 +76,20 @@ func _where(values ...interface{}) (string, []interface{}) { return fmt.Sprintf("WHERE %s", desc), vars } -func _orderby(values ...interface{}) (string, []interface{}) { +func _orderBy(values ...interface{}) (string, []interface{}) { return fmt.Sprintf("ORDER BY %s", values[0]), []interface{}{} } func _update(values ...interface{}) (string, []interface{}) { - return fmt.Sprintf("UPDATE %s", values[0]), []interface{}{} -} - -func _set(values ...interface{}) (string, []interface{}) { - m := values[0].(map[string]interface{}) + tableName := values[0] + m := values[1].(map[string]interface{}) var keys []string var vars []interface{} for k, v := range m { keys = append(keys, k+" = ?") vars = append(vars, v) } - - return fmt.Sprintf("SET %s", strings.Join(keys, ", ")), vars + return fmt.Sprintf("UPDATE %s SET %s", tableName, strings.Join(keys, ", ")), vars } func _delete(values ...interface{}) (string, []interface{}) { diff --git a/gee-orm/day5-update-delete/dialect/dialect.go b/gee-orm/day5-hooks/dialect/dialect.go similarity index 100% rename from gee-orm/day5-update-delete/dialect/dialect.go rename to gee-orm/day5-hooks/dialect/dialect.go diff --git a/gee-orm/day5-update-delete/dialect/sqlite3.go b/gee-orm/day5-hooks/dialect/sqlite3.go similarity index 100% rename from gee-orm/day5-update-delete/dialect/sqlite3.go rename to gee-orm/day5-hooks/dialect/sqlite3.go diff --git a/gee-orm/day5-update-delete/dialect/sqlite3_test.go b/gee-orm/day5-hooks/dialect/sqlite3_test.go similarity index 100% rename from gee-orm/day5-update-delete/dialect/sqlite3_test.go rename to gee-orm/day5-hooks/dialect/sqlite3_test.go diff --git a/gee-orm/day5-update-delete/geeorm.go b/gee-orm/day5-hooks/geeorm.go similarity index 78% rename from gee-orm/day5-update-delete/geeorm.go rename to gee-orm/day5-hooks/geeorm.go index 61ee9e0..b1881ce 100644 --- a/gee-orm/day5-update-delete/geeorm.go +++ b/gee-orm/day5-hooks/geeorm.go @@ -38,14 +38,14 @@ func NewEngine(driver, source string) (e *Engine, err error) { } // Close database connection -func (e *Engine) Close() (err error) { - if err = e.db.Close(); err == nil { - log.Info("Close database success") +func (engine *Engine) Close() { + if err := engine.db.Close(); err != nil { + log.Error("Failed to close database") } - return + log.Info("Close database success") } // NewSession creates a new session for next operations -func (e *Engine) NewSession() *session.Session { - return session.New(e.db, e.dialect) +func (engine *Engine) NewSession() *session.Session { + return session.New(engine.db, engine.dialect) } diff --git a/gee-orm/day5-update-delete/geeorm_test.go b/gee-orm/day5-hooks/geeorm_test.go similarity index 93% rename from gee-orm/day5-update-delete/geeorm_test.go rename to gee-orm/day5-hooks/geeorm_test.go index 35628be..c6da191 100644 --- a/gee-orm/day5-update-delete/geeorm_test.go +++ b/gee-orm/day5-hooks/geeorm_test.go @@ -16,5 +16,5 @@ func OpenDB(t *testing.T) *Engine { func TestNewEngine(t *testing.T) { engine := OpenDB(t) - _ = engine.Close() + defer engine.Close() } diff --git a/gee-orm/day5-update-delete/go.mod b/gee-orm/day5-hooks/go.mod similarity index 100% rename from gee-orm/day5-update-delete/go.mod rename to gee-orm/day5-hooks/go.mod diff --git a/gee-orm/day5-update-delete/log/log.go b/gee-orm/day5-hooks/log/log.go similarity index 100% rename from gee-orm/day5-update-delete/log/log.go rename to gee-orm/day5-hooks/log/log.go diff --git a/gee-orm/day5-update-delete/log/log_test.go b/gee-orm/day5-hooks/log/log_test.go similarity index 100% rename from gee-orm/day5-update-delete/log/log_test.go rename to gee-orm/day5-hooks/log/log_test.go diff --git a/gee-orm/day5-update-delete/schema/schema.go b/gee-orm/day5-hooks/schema/schema.go similarity index 59% rename from gee-orm/day5-update-delete/schema/schema.go rename to gee-orm/day5-hooks/schema/schema.go index 8519fd4..ff76283 100644 --- a/gee-orm/day5-update-delete/schema/schema.go +++ b/gee-orm/day5-hooks/schema/schema.go @@ -1,7 +1,6 @@ package schema import ( - "fmt" "geeorm/dialect" "go/ast" "reflect" @@ -10,19 +9,26 @@ import ( // Field represents a column of database type Field struct { Name string + Type string Tag string } // Schema represents a table of database type Schema struct { - TableName string - PrimaryField *Field - Fields []*Field - FieldNames []string + Model interface{} + Name string + Fields []*Field + FieldNames []string + fieldMap map[string]*Field +} + +// GetField returns field by name +func (schema *Schema) GetField(name string) *Field { + return schema.fieldMap[name] } // Values return the values of dest's member variables -func (schema *Schema) Values(dest interface{}) []interface{} { +func (schema *Schema) RecordValues(dest interface{}) []interface{} { destValue := reflect.Indirect(reflect.ValueOf(dest)) var fieldValues []interface{} for _, field := range schema.Fields { @@ -35,8 +41,9 @@ func (schema *Schema) Values(dest interface{}) []interface{} { func Parse(dest interface{}, d dialect.Dialect) *Schema { modelType := reflect.Indirect(reflect.ValueOf(dest)).Type() schema := &Schema{ - TableName: modelType.Name(), - PrimaryField: &Field{Name: "ID", Tag: ""}, + Model: dest, + Name: modelType.Name(), + fieldMap: make(map[string]*Field), } for i := 0; i < modelType.NumField(); i++ { @@ -44,24 +51,15 @@ func Parse(dest interface{}, d dialect.Dialect) *Schema { if !p.Anonymous && ast.IsExported(p.Name) { field := &Field{ Name: p.Name, - Tag: d.DataTypeOf(reflect.Indirect(reflect.New(p.Type))), + Type: d.DataTypeOf(reflect.Indirect(reflect.New(p.Type))), } - if v, ok := p.Tag.Lookup("geeorm"); ok && v == "primary_key" { - schema.PrimaryField = field + if v, ok := p.Tag.Lookup("geeorm"); ok { + field.Tag = v } schema.Fields = append(schema.Fields, field) schema.FieldNames = append(schema.FieldNames, p.Name) + schema.fieldMap[p.Name] = field } } return schema } - -// String returns readable string -func (field *Field) String() string { - return fmt.Sprintf("(%s %s)", field.Name, field.Tag) -} - -// String returns readable string -func (schema *Schema) String() string { - return fmt.Sprintf("TABLE %s %v", schema.TableName, schema.Fields) -} diff --git a/gee-orm/day5-update-delete/schema/schema_test.go b/gee-orm/day5-hooks/schema/schema_test.go similarity index 65% rename from gee-orm/day5-update-delete/schema/schema_test.go rename to gee-orm/day5-hooks/schema/schema_test.go index aba3e0b..47ae9fc 100644 --- a/gee-orm/day5-update-delete/schema/schema_test.go +++ b/gee-orm/day5-hooks/schema/schema_test.go @@ -6,7 +6,7 @@ import ( ) type User struct { - Name string `geeorm:"primary_key"` + Name string `geeorm:"PRIMARY KEY"` Age int } @@ -14,18 +14,17 @@ var TestDial, _ = dialect.GetDialect("sqlite3") func TestParse(t *testing.T) { schema := Parse(&User{}, TestDial) - if schema.TableName != "User" || len(schema.Fields) != 2 { + if schema.Name != "User" || len(schema.Fields) != 2 { t.Fatal("failed to parse User struct") } - if schema.PrimaryField.Name != "Name" { + if schema.GetField("Name").Tag != "PRIMARY KEY" { t.Fatal("failed to parse primary key") } - t.Log(schema) } -func TestSchema_Values(t *testing.T) { +func TestSchema_RecordValues(t *testing.T) { schema := Parse(&User{}, TestDial) - values := schema.Values(&User{"Tom", 18}) + values := schema.RecordValues(&User{"Tom", 18}) name := values[0].(string) age := values[1].(int) diff --git a/gee-orm/day5-update-delete/session/raw.go b/gee-orm/day5-hooks/session/raw.go similarity index 69% rename from gee-orm/day5-update-delete/session/raw.go rename to gee-orm/day5-hooks/session/raw.go index a47edcc..161fcb4 100644 --- a/gee-orm/day5-update-delete/session/raw.go +++ b/gee-orm/day5-hooks/session/raw.go @@ -6,6 +6,7 @@ import ( "geeorm/dialect" "geeorm/log" "geeorm/schema" + "strings" ) // Session keep a pointer to sql.DB and provides all execution of all @@ -15,7 +16,7 @@ type Session struct { dialect dialect.Dialect refTable *schema.Schema clause clause.Clause - sql string + sql strings.Builder sqlVars []interface{} } @@ -29,16 +30,21 @@ func New(db *sql.DB, dialect dialect.Dialect) *Session { // Clear initialize the state of a session func (s *Session) Clear() { - s.refTable, s.sqlVars = nil, nil + s.sql.Reset() + s.sqlVars = nil s.clause = clause.Clause{} - s.sql = "" +} + +// DB returns *sql.DB +func (s *Session) DB() *sql.DB { + return s.db } // Exec raw sql with sqlVars func (s *Session) Exec() (result sql.Result, err error) { defer s.Clear() - log.Info(s.sql, s.sqlVars) - if result, err = s.db.Exec(s.sql, s.sqlVars...); err != nil { + log.Info(s.sql.String(), s.sqlVars) + if result, err = s.DB().Exec(s.sql.String(), s.sqlVars...); err != nil { log.Error(err) } return @@ -47,15 +53,15 @@ func (s *Session) Exec() (result sql.Result, err error) { // QueryRow gets a record from db func (s *Session) QueryRow() *sql.Row { defer s.Clear() - log.Info(s.sql, s.sqlVars) - return s.db.QueryRow(s.sql, s.sqlVars...) + log.Info(s.sql.String(), s.sqlVars) + return s.DB().QueryRow(s.sql.String(), s.sqlVars...) } // QueryRows gets a list of records from db func (s *Session) QueryRows() (rows *sql.Rows, err error) { defer s.Clear() - log.Info(s.sql, s.sqlVars) - if rows, err = s.db.Query(s.sql, s.sqlVars...); err != nil { + log.Info(s.sql.String(), s.sqlVars) + if rows, err = s.DB().Query(s.sql.String(), s.sqlVars...); err != nil { log.Error(err) } return @@ -63,7 +69,8 @@ func (s *Session) QueryRows() (rows *sql.Rows, err error) { // Raw appends sql and sqlVars func (s *Session) Raw(sql string, values ...interface{}) *Session { - s.sql += sql + s.sql.WriteString(sql) + s.sql.WriteString(" ") s.sqlVars = append(s.sqlVars, values...) return s -} +} \ No newline at end of file diff --git a/gee-orm/day5-update-delete/session/raw_test.go b/gee-orm/day5-hooks/session/raw_test.go similarity index 89% rename from gee-orm/day5-update-delete/session/raw_test.go rename to gee-orm/day5-hooks/session/raw_test.go index ed447a5..a96770b 100644 --- a/gee-orm/day5-update-delete/session/raw_test.go +++ b/gee-orm/day5-hooks/session/raw_test.go @@ -30,7 +30,7 @@ func TestSession_Exec(t *testing.T) { s := NewSession() _, _ = s.Raw("DROP TABLE IF EXISTS User;").Exec() _, _ = s.Raw("CREATE TABLE User(name text);").Exec() - result, _ := s.Raw("INSERT INTO User(`name`) values (?), (?)", "Tom", "Sam").Exec() + result, _ := s.Raw("INSERT INTO User(`Name`) values (?), (?)", "Tom", "Sam").Exec() if count, err := result.RowsAffected(); err != nil || count != 2 { t.Fatal("expect 2, but got", count) } @@ -39,7 +39,7 @@ func TestSession_Exec(t *testing.T) { func TestSession_QueryRows(t *testing.T) { s := NewSession() _, _ = s.Raw("DROP TABLE IF EXISTS User;").Exec() - _, _ = s.Raw("CREATE TABLE User(name text);").Exec() + _, _ = s.Raw("CREATE TABLE User(Name text);").Exec() row := s.Raw("SELECT count(*) FROM User").QueryRow() var count int if err := row.Scan(&count); err != nil || count != 0 { diff --git a/gee-orm/day5-update-delete/session/record.go b/gee-orm/day5-hooks/session/record.go similarity index 64% rename from gee-orm/day5-update-delete/session/record.go rename to gee-orm/day5-hooks/session/record.go index bc6d1fe..86b9ddd 100644 --- a/gee-orm/day5-update-delete/session/record.go +++ b/gee-orm/day5-hooks/session/record.go @@ -1,17 +1,30 @@ package session import ( + "errors" "geeorm/clause" "reflect" ) +// CallMethod calls the registered hooks +func (s *Session) CallMethod(method string) error { + if fm := reflect.ValueOf(s.refTable.Model).MethodByName(method); fm.IsValid() { + if v := fm.Call([]reflect.Value{}); len(v) > 0 { + if err, ok := v[0].Interface().(error); ok { + return err + } + } + } + return nil +} + // Create one or more records in database -func (s *Session) Create(values ...interface{}) (int64, error) { +func (s *Session) Insert(values ...interface{}) (int64, error) { recordValues := make([]interface{}, 0) for _, value := range values { - table := s.RefTable(value) - s.clause.Set(clause.INSERT, table.TableName, table.FieldNames) - recordValues = append(recordValues, table.Values(value)) + table := s.Model(value).RefTable() + s.clause.Set(clause.INSERT, table.Name, table.FieldNames) + recordValues = append(recordValues, table.RecordValues(value)) } s.clause.Set(clause.VALUES, recordValues...) @@ -28,9 +41,9 @@ func (s *Session) Create(values ...interface{}) (int64, error) { func (s *Session) Find(values interface{}) error { destSlice := reflect.Indirect(reflect.ValueOf(values)) destType := destSlice.Type().Elem() - table := s.RefTable(reflect.New(destType).Elem().Interface()) + table := s.Model(reflect.New(destType).Elem().Interface()).RefTable() - s.clause.Set(clause.SELECT, table.TableName, table.FieldNames) + s.clause.Set(clause.SELECT, table.Name, table.FieldNames) sql, vars := s.clause.Build(clause.SELECT, clause.WHERE, clause.ORDERBY, clause.LIMIT) rows, err := s.Raw(sql, vars...).QueryRows() if err != nil { @@ -55,9 +68,14 @@ func (s *Session) Find(values interface{}) error { func (s *Session) First(value interface{}) error { dest := reflect.Indirect(reflect.ValueOf(value)) destSlice := reflect.New(reflect.SliceOf(dest.Type())).Elem() - err := s.Limit(1).Find(destSlice.Addr().Interface()) + if err := s.Limit(1).Find(destSlice.Addr().Interface()); err != nil { + return err + } + if destSlice.Len() == 0 { + return errors.New("NOT FOUND") + } dest.Set(destSlice.Index(0)) - return err + return nil } // Limit adds limit condition to clause @@ -79,25 +97,19 @@ func (s *Session) OrderBy(desc string) *Session { return s } -// Set adds Assignment by condition to clause +// Update records with where clause // support map[string]interface{} -// also support "Name", "Tom", "Age", 18, etc -func (s *Session) Set(values ...interface{}) *Session { - m, ok := values[0].(map[string]interface{}) +// also support kv list: "Name", "Tom", "Age", 18, .... +func (s *Session) Update(kv ...interface{}) (int64, error) { + m, ok := kv[0].(map[string]interface{}) if !ok { m = make(map[string]interface{}) - for i := 0; i < len(values); i += 2 { - m[values[i].(string)] = values[i+1] + for i := 0; i < len(kv); i += 2 { + m[kv[i].(string)] = kv[i+1] } } - s.clause.Set(clause.SET, m) - return s -} - -// Update records with where clause -func (s *Session) Update(value interface{}) (int64, error) { - s.clause.Set(clause.UPDATE, s.guessTableName(value)) - sql, vars := s.clause.Build(clause.UPDATE, clause.SET, clause.WHERE) + s.clause.Set(clause.UPDATE, s.RefTable().Name, m) + sql, vars := s.clause.Build(clause.UPDATE, clause.WHERE) result, err := s.Raw(sql, vars...).Exec() if err != nil { return 0, err @@ -106,8 +118,8 @@ func (s *Session) Update(value interface{}) (int64, error) { } // Delete records with where clause -func (s *Session) Delete(value interface{}) (int64, error) { - s.clause.Set(clause.DELETE, s.guessTableName(value)) +func (s *Session) Delete() (int64, error) { + s.clause.Set(clause.DELETE, s.RefTable().Name) sql, vars := s.clause.Build(clause.DELETE, clause.WHERE) result, err := s.Raw(sql, vars...).Exec() if err != nil { @@ -117,12 +129,11 @@ func (s *Session) Delete(value interface{}) (int64, error) { } // Count records with where clause -func (s *Session) Count(value interface{}) (int64, error) { - s.clause.Set(clause.COUNT, s.guessTableName(value)) +func (s *Session) Count() (int64, error) { + s.clause.Set(clause.COUNT, s.RefTable().Name) sql, vars := s.clause.Build(clause.COUNT, clause.WHERE) row := s.Raw(sql, vars...).QueryRow() var tmp int64 - if err := row.Scan(&tmp); err != nil { return 0, err } diff --git a/gee-orm/day5-update-delete/session/record_test.go b/gee-orm/day5-hooks/session/record_test.go similarity index 81% rename from gee-orm/day5-update-delete/session/record_test.go rename to gee-orm/day5-hooks/session/record_test.go index faf8535..5d482a0 100644 --- a/gee-orm/day5-update-delete/session/record_test.go +++ b/gee-orm/day5-hooks/session/record_test.go @@ -10,19 +10,19 @@ var ( func testRecordInit(t *testing.T) *Session { t.Helper() - s := NewSession() - err1 := s.DropTable(&User{}) - err2 := s.CreateTable(&User{}) - _, err3 := s.Create(user1, user2) + s := NewSession().Model(&User{}) + err1 := s.DropTable() + err2 := s.CreateTable() + _, err3 := s.Insert(user1, user2) if err1 != nil || err2 != nil || err3 != nil { t.Fatal("failed init test records") } return s } -func TestSession_Create(t *testing.T) { +func TestSession_Insert(t *testing.T) { s := testRecordInit(t) - affected, err := s.Create(user3) + affected, err := s.Insert(user3) if err != nil || affected != 1 { t.Fatal("failed to create record") } @@ -30,7 +30,7 @@ func TestSession_Create(t *testing.T) { func TestSession_Find(t *testing.T) { s := testRecordInit(t) - users := []User{} + var users []User if err := s.Find(&users); err != nil || len(users) != 2 { t.Fatal("failed to query all") } @@ -57,7 +57,7 @@ func TestSession_Limit(t *testing.T) { func TestSession_Where(t *testing.T) { s := testRecordInit(t) var users []User - _, err1 := s.Create(user3) + _, err1 := s.Insert(user3) err2 := s.Where("Age = ?", 25).Find(&users) if err1 != nil || err2 != nil || len(users) != 2 { @@ -77,7 +77,7 @@ func TestSession_OrderBy(t *testing.T) { func TestSession_Update(t *testing.T) { s := testRecordInit(t) - affected, _ := s.Where("Name = ?", "Tom").Set("Age", 30).Update(&User{}) + affected, _ := s.Where("Name = ?", "Tom").Update("Age", 30) u := &User{} _ = s.OrderBy("Age DESC").First(u) @@ -88,8 +88,8 @@ func TestSession_Update(t *testing.T) { func TestSession_DeleteAndCount(t *testing.T) { s := testRecordInit(t) - affected, _ := s.Where("Name = ?", "Tom").Delete("User") - count, _ := s.Count("User") + affected, _ := s.Where("Name = ?", "Tom").Delete() + count, _ := s.Count() if affected != 1 || count != 1 { t.Fatal("failed to delete or count") diff --git a/gee-orm/day5-hooks/session/table.go b/gee-orm/day5-hooks/session/table.go new file mode 100644 index 0000000..58e7b0f --- /dev/null +++ b/gee-orm/day5-hooks/session/table.go @@ -0,0 +1,54 @@ +package session + +import ( + "fmt" + "geeorm/log" + "reflect" + "strings" + + "geeorm/schema" +) + +// Model assigns refTable +func (s *Session) Model(value interface{}) *Session { + // nil or different model, update refTable + if s.refTable == nil || reflect.TypeOf(value) != reflect.TypeOf(s.refTable.Model) { + s.refTable = schema.Parse(value, s.dialect) + } + return s +} + +// RefTable returns a Schema instance that contains all parsed fields +func (s *Session) RefTable() *schema.Schema { + if s.refTable == nil { + log.Error("Model is not set") + } + return s.refTable +} + +// CreateTable create a table in database with a model +func (s *Session) CreateTable() error { + table := s.RefTable() + var columns []string + for _, field := range table.Fields { + columns = append(columns, fmt.Sprintf("%s %s %s", field.Name, field.Type, field.Tag)) + } + desc := strings.Join(columns, ",") + _, err := s.Raw(fmt.Sprintf("CREATE TABLE %s (%s);", table.Name, desc)).Exec() + return err +} + +// DropTable drops a table with the name of model +func (s *Session) DropTable() error { + _, err := s.Raw(fmt.Sprintf("DROP TABLE IF EXISTS %s", s.RefTable().Name)).Exec() + return err +} + +// HasTable returns true of the table exists +func (s *Session) HasTable() bool { + sql, values := s.dialect.TableExistSQL(s.RefTable().Name) + row := s.Raw(sql, values...).QueryRow() + var tmp string + _ = row.Scan(&tmp) + return tmp == s.RefTable().Name +} diff --git a/gee-orm/day5-hooks/session/table_test.go b/gee-orm/day5-hooks/session/table_test.go new file mode 100644 index 0000000..3bb7554 --- /dev/null +++ b/gee-orm/day5-hooks/session/table_test.go @@ -0,0 +1,28 @@ +package session + +import ( + "testing" +) + +type User struct { + Name string `geeorm:"PRIMARY KEY"` + Age int +} + +func TestSession_CreateTable(t *testing.T) { + s := NewSession().Model(&User{}) + _ = s.DropTable() + _ = s.CreateTable() + if !s.HasTable() { + t.Fatal("Failed to create table User") + } +} + +func TestSession_Model(t *testing.T) { + s := NewSession().Model(&User{}) + table := s.RefTable() + s.Model(&Session{}) + if table.Name != "User" || s.RefTable().Name != "Session" { + t.Fatal("Failed to change model") + } +} diff --git a/gee-orm/day5-update-delete/session/table.go b/gee-orm/day5-update-delete/session/table.go deleted file mode 100644 index b644d3a..0000000 --- a/gee-orm/day5-update-delete/session/table.go +++ /dev/null @@ -1,59 +0,0 @@ -package session - -import ( - "fmt" - "strings" - - "geeorm/schema" -) - -// RefTable returns a Schema instance that contains all parsed fields -func (s *Session) RefTable(value interface{}) *schema.Schema { - if value == nil { - panic("value is nil") - } - if s.refTable == nil { - s.refTable = schema.Parse(value, s.dialect) - } - return s.refTable -} - -// CreateTable create a table in database with a model -func (s *Session) CreateTable(value interface{}) error { - table := s.RefTable(value) - var columns []string - for _, field := range table.Fields { - tag := field.Tag - if field.Name == table.PrimaryField.Name { - tag += " PRIMARY KEY" - } - columns = append(columns, fmt.Sprintf("%s %s", field.Name, tag)) - } - desc := strings.Join(columns, ",") - _, err := s.Raw(fmt.Sprintf("CREATE TABLE %s (%s);", table.TableName, desc)).Exec() - return err -} - -// DropTable drops a table with the name of model -func (s *Session) DropTable(value interface{}) error { - table := s.RefTable(value) - _, err := s.Raw(fmt.Sprintf("DROP TABLE IF EXISTS %s", table.TableName)).Exec() - return err -} - -func (s *Session) guessTableName(value interface{}) string { - if tableName, ok := value.(string); ok { - return tableName - } - return s.RefTable(value).TableName -} - -// HasTable returns true of the table exists -func (s *Session) HasTable(value interface{}) bool { - tableName := s.guessTableName(value) - sql, values := s.dialect.TableExistSQL(tableName) - row := s.Raw(sql, values...).QueryRow() - var tmp string - _ = row.Scan(&tmp) - return tmp == tableName -} diff --git a/gee-orm/day5-update-delete/session/table_test.go b/gee-orm/day5-update-delete/session/table_test.go deleted file mode 100644 index 91eb519..0000000 --- a/gee-orm/day5-update-delete/session/table_test.go +++ /dev/null @@ -1,19 +0,0 @@ -package session - -import ( - "testing" -) - -type User struct { - Name string `geeorm:"primary_key"` - Age int -} - -func TestSession_CreateTable(t *testing.T) { - s := NewSession() - _ = s.DropTable(&User{}) - _ = s.CreateTable(&User{}) - if !s.HasTable("User") { - t.Fatal("failed to create table User") - } -} diff --git a/gee-orm/day6-transaction/clause/clause.go b/gee-orm/day6-transaction/clause/clause.go index 2195c86..02fcf93 100644 --- a/gee-orm/day6-transaction/clause/clause.go +++ b/gee-orm/day6-transaction/clause/clause.go @@ -22,7 +22,6 @@ const ( WHERE ORDERBY UPDATE - SET DELETE COUNT ) diff --git a/gee-orm/day6-transaction/clause/clause_test.go b/gee-orm/day6-transaction/clause/clause_test.go index 8769c40..b54c6e0 100644 --- a/gee-orm/day6-transaction/clause/clause_test.go +++ b/gee-orm/day6-transaction/clause/clause_test.go @@ -34,11 +34,9 @@ func testSelect(t *testing.T) { func testUpdate(t *testing.T) { var clause Clause - clause.Set(UPDATE, "User") + clause.Set(UPDATE, "User", map[string]interface{}{"Age": 30, "Name": "Tommy"}) clause.Set(WHERE, "Name = ?", "Tom") - clause.Set(SET, map[string]interface{}{"Age": 30, "Name": "Tommy"}) - - sql, vars := clause.Build(UPDATE, SET, WHERE) + sql, vars := clause.Build(UPDATE, WHERE) t.Log(sql, vars) if sql != "UPDATE User SET Age = ?, Name = ? WHERE Name = ?" { t.Fatal("failed to build SQL") diff --git a/gee-orm/day6-transaction/clause/generator.go b/gee-orm/day6-transaction/clause/generator.go index 1cf5aec..127fc43 100644 --- a/gee-orm/day6-transaction/clause/generator.go +++ b/gee-orm/day6-transaction/clause/generator.go @@ -16,9 +16,8 @@ func init() { generators[SELECT] = _select generators[LIMIT] = _limit generators[WHERE] = _where - generators[ORDERBY] = _orderby + generators[ORDERBY] = _orderBy generators[UPDATE] = _update - generators[SET] = _set generators[DELETE] = _delete generators[COUNT] = _count } @@ -77,24 +76,20 @@ func _where(values ...interface{}) (string, []interface{}) { return fmt.Sprintf("WHERE %s", desc), vars } -func _orderby(values ...interface{}) (string, []interface{}) { +func _orderBy(values ...interface{}) (string, []interface{}) { return fmt.Sprintf("ORDER BY %s", values[0]), []interface{}{} } func _update(values ...interface{}) (string, []interface{}) { - return fmt.Sprintf("UPDATE %s", values[0]), []interface{}{} -} - -func _set(values ...interface{}) (string, []interface{}) { - m := values[0].(map[string]interface{}) + tableName := values[0] + m := values[1].(map[string]interface{}) var keys []string var vars []interface{} for k, v := range m { keys = append(keys, k+" = ?") vars = append(vars, v) } - - return fmt.Sprintf("SET %s", strings.Join(keys, ", ")), vars + return fmt.Sprintf("UPDATE %s SET %s", tableName, strings.Join(keys, ", ")), vars } func _delete(values ...interface{}) (string, []interface{}) { diff --git a/gee-orm/day6-transaction/geeorm.go b/gee-orm/day6-transaction/geeorm.go index b8cbb42..a08fd46 100644 --- a/gee-orm/day6-transaction/geeorm.go +++ b/gee-orm/day6-transaction/geeorm.go @@ -38,16 +38,16 @@ func NewEngine(driver, source string) (e *Engine, err error) { } // Close database connection -func (e *Engine) Close() (err error) { - if err = e.db.Close(); err == nil { - log.Info("Close database success") +func (engine *Engine) Close() { + if err := engine.db.Close(); err != nil { + log.Error("Failed to close database") } - return + log.Info("Close database success") } // NewSession creates a new session for next operations -func (e *Engine) NewSession() *session.Session { - return session.New(e.db, e.dialect) +func (engine *Engine) NewSession() *session.Session { + return session.New(engine.db, engine.dialect) } // TxFunc will be called between tx.Begin() and tx.Commit() @@ -55,8 +55,8 @@ func (e *Engine) NewSession() *session.Session { type TxFunc func(*session.Session) (interface{}, error) // Transaction executes sql wrapped in a transaction, then automatically commit if no error occurs -func (e *Engine) Transaction(f TxFunc) (result interface{}, err error) { - s := e.NewSession() +func (engine *Engine) Transaction(f TxFunc) (result interface{}, err error) { + s := engine.NewSession() if err := s.Begin(); err != nil { return nil, err } diff --git a/gee-orm/day6-transaction/geeorm_test.go b/gee-orm/day6-transaction/geeorm_test.go index 17a1d2b..c3cf12a 100644 --- a/gee-orm/day6-transaction/geeorm_test.go +++ b/gee-orm/day6-transaction/geeorm_test.go @@ -17,43 +17,39 @@ func OpenDB(t *testing.T) *Engine { return engine } -func CloseDB(engine *Engine) { - _ = engine.Close() -} - func TestNewEngine(t *testing.T) { engine := OpenDB(t) - _ = engine.Close() + defer engine.Close() } type User struct { - Name string `geeorm:"primary_key"` + Name string `geeorm:"PRIMARY KEY"` Age int } func transactionRollback(t *testing.T) { engine := OpenDB(t) - defer CloseDB(engine) + defer engine.Close() s := engine.NewSession() - _ = s.DropTable(&User{}) + _ = s.Model(&User{}).DropTable() _, err := engine.Transaction(func(s *session.Session) (result interface{}, err error) { - _ = s.CreateTable(&User{}) - _, err = s.Create(&User{"Tom", 18}) + _ = s.Model(&User{}).CreateTable() + _, err = s.Insert(&User{"Tom", 18}) return nil, errors.New("Error") }) - if err == nil || s.HasTable("User") { + if err == nil || s.HasTable() { t.Fatal("failed to rollback") } } func transactionCommit(t *testing.T) { engine := OpenDB(t) - defer CloseDB(engine) + defer engine.Close() s := engine.NewSession() - _ = s.DropTable(&User{}) + _ = s.Model(&User{}).DropTable() _, err := engine.Transaction(func(s *session.Session) (result interface{}, err error) { - err = s.CreateTable(&User{}) - _, err = s.Create(&User{"Tom", 18}) + _ = s.Model(&User{}).CreateTable() + _, err = s.Insert(&User{"Tom", 18}) return }) u := &User{} diff --git a/gee-orm/day6-transaction/schema/schema.go b/gee-orm/day6-transaction/schema/schema.go index 8519fd4..ff76283 100644 --- a/gee-orm/day6-transaction/schema/schema.go +++ b/gee-orm/day6-transaction/schema/schema.go @@ -1,7 +1,6 @@ package schema import ( - "fmt" "geeorm/dialect" "go/ast" "reflect" @@ -10,19 +9,26 @@ import ( // Field represents a column of database type Field struct { Name string + Type string Tag string } // Schema represents a table of database type Schema struct { - TableName string - PrimaryField *Field - Fields []*Field - FieldNames []string + Model interface{} + Name string + Fields []*Field + FieldNames []string + fieldMap map[string]*Field +} + +// GetField returns field by name +func (schema *Schema) GetField(name string) *Field { + return schema.fieldMap[name] } // Values return the values of dest's member variables -func (schema *Schema) Values(dest interface{}) []interface{} { +func (schema *Schema) RecordValues(dest interface{}) []interface{} { destValue := reflect.Indirect(reflect.ValueOf(dest)) var fieldValues []interface{} for _, field := range schema.Fields { @@ -35,8 +41,9 @@ func (schema *Schema) Values(dest interface{}) []interface{} { func Parse(dest interface{}, d dialect.Dialect) *Schema { modelType := reflect.Indirect(reflect.ValueOf(dest)).Type() schema := &Schema{ - TableName: modelType.Name(), - PrimaryField: &Field{Name: "ID", Tag: ""}, + Model: dest, + Name: modelType.Name(), + fieldMap: make(map[string]*Field), } for i := 0; i < modelType.NumField(); i++ { @@ -44,24 +51,15 @@ func Parse(dest interface{}, d dialect.Dialect) *Schema { if !p.Anonymous && ast.IsExported(p.Name) { field := &Field{ Name: p.Name, - Tag: d.DataTypeOf(reflect.Indirect(reflect.New(p.Type))), + Type: d.DataTypeOf(reflect.Indirect(reflect.New(p.Type))), } - if v, ok := p.Tag.Lookup("geeorm"); ok && v == "primary_key" { - schema.PrimaryField = field + if v, ok := p.Tag.Lookup("geeorm"); ok { + field.Tag = v } schema.Fields = append(schema.Fields, field) schema.FieldNames = append(schema.FieldNames, p.Name) + schema.fieldMap[p.Name] = field } } return schema } - -// String returns readable string -func (field *Field) String() string { - return fmt.Sprintf("(%s %s)", field.Name, field.Tag) -} - -// String returns readable string -func (schema *Schema) String() string { - return fmt.Sprintf("TABLE %s %v", schema.TableName, schema.Fields) -} diff --git a/gee-orm/day6-transaction/schema/schema_test.go b/gee-orm/day6-transaction/schema/schema_test.go index aba3e0b..47ae9fc 100644 --- a/gee-orm/day6-transaction/schema/schema_test.go +++ b/gee-orm/day6-transaction/schema/schema_test.go @@ -6,7 +6,7 @@ import ( ) type User struct { - Name string `geeorm:"primary_key"` + Name string `geeorm:"PRIMARY KEY"` Age int } @@ -14,18 +14,17 @@ var TestDial, _ = dialect.GetDialect("sqlite3") func TestParse(t *testing.T) { schema := Parse(&User{}, TestDial) - if schema.TableName != "User" || len(schema.Fields) != 2 { + if schema.Name != "User" || len(schema.Fields) != 2 { t.Fatal("failed to parse User struct") } - if schema.PrimaryField.Name != "Name" { + if schema.GetField("Name").Tag != "PRIMARY KEY" { t.Fatal("failed to parse primary key") } - t.Log(schema) } -func TestSchema_Values(t *testing.T) { +func TestSchema_RecordValues(t *testing.T) { schema := Parse(&User{}, TestDial) - values := schema.Values(&User{"Tom", 18}) + values := schema.RecordValues(&User{"Tom", 18}) name := values[0].(string) age := values[1].(int) diff --git a/gee-orm/day6-transaction/session/raw.go b/gee-orm/day6-transaction/session/raw.go index c30f1c3..5bdd039 100644 --- a/gee-orm/day6-transaction/session/raw.go +++ b/gee-orm/day6-transaction/session/raw.go @@ -6,6 +6,7 @@ import ( "geeorm/dialect" "geeorm/log" "geeorm/schema" + "strings" ) // Session keep a pointer to sql.DB and provides all execution of all @@ -16,7 +17,7 @@ type Session struct { tx *sql.Tx refTable *schema.Schema clause clause.Clause - sql string + sql strings.Builder sqlVars []interface{} } @@ -30,9 +31,9 @@ func New(db *sql.DB, dialect dialect.Dialect) *Session { // Clear initialize the state of a session func (s *Session) Clear() { - s.refTable, s.sqlVars = nil, nil + s.sql.Reset() + s.sqlVars = nil s.clause = clause.Clause{} - s.sql = "" } // CommonDB is a minimal function set of db @@ -56,8 +57,8 @@ func (s *Session) DB() CommonDB { // Exec raw sql with sqlVars func (s *Session) Exec() (result sql.Result, err error) { defer s.Clear() - log.Info(s.sql, s.sqlVars) - if result, err = s.DB().Exec(s.sql, s.sqlVars...); err != nil { + log.Info(s.sql.String(), s.sqlVars) + if result, err = s.DB().Exec(s.sql.String(), s.sqlVars...); err != nil { log.Error(err) } return @@ -66,15 +67,15 @@ func (s *Session) Exec() (result sql.Result, err error) { // QueryRow gets a record from db func (s *Session) QueryRow() *sql.Row { defer s.Clear() - log.Info(s.sql, s.sqlVars) - return s.DB().QueryRow(s.sql, s.sqlVars...) + log.Info(s.sql.String(), s.sqlVars) + return s.DB().QueryRow(s.sql.String(), s.sqlVars...) } // QueryRows gets a list of records from db func (s *Session) QueryRows() (rows *sql.Rows, err error) { defer s.Clear() - log.Info(s.sql, s.sqlVars) - if rows, err = s.DB().Query(s.sql, s.sqlVars...); err != nil { + log.Info(s.sql.String(), s.sqlVars) + if rows, err = s.DB().Query(s.sql.String(), s.sqlVars...); err != nil { log.Error(err) } return @@ -82,7 +83,8 @@ func (s *Session) QueryRows() (rows *sql.Rows, err error) { // Raw appends sql and sqlVars func (s *Session) Raw(sql string, values ...interface{}) *Session { - s.sql += sql + s.sql.WriteString(sql) + s.sql.WriteString(" ") s.sqlVars = append(s.sqlVars, values...) return s -} +} \ No newline at end of file diff --git a/gee-orm/day6-transaction/session/raw_test.go b/gee-orm/day6-transaction/session/raw_test.go index ed447a5..a96770b 100644 --- a/gee-orm/day6-transaction/session/raw_test.go +++ b/gee-orm/day6-transaction/session/raw_test.go @@ -30,7 +30,7 @@ func TestSession_Exec(t *testing.T) { s := NewSession() _, _ = s.Raw("DROP TABLE IF EXISTS User;").Exec() _, _ = s.Raw("CREATE TABLE User(name text);").Exec() - result, _ := s.Raw("INSERT INTO User(`name`) values (?), (?)", "Tom", "Sam").Exec() + result, _ := s.Raw("INSERT INTO User(`Name`) values (?), (?)", "Tom", "Sam").Exec() if count, err := result.RowsAffected(); err != nil || count != 2 { t.Fatal("expect 2, but got", count) } @@ -39,7 +39,7 @@ func TestSession_Exec(t *testing.T) { func TestSession_QueryRows(t *testing.T) { s := NewSession() _, _ = s.Raw("DROP TABLE IF EXISTS User;").Exec() - _, _ = s.Raw("CREATE TABLE User(name text);").Exec() + _, _ = s.Raw("CREATE TABLE User(Name text);").Exec() row := s.Raw("SELECT count(*) FROM User").QueryRow() var count int if err := row.Scan(&count); err != nil || count != 0 { diff --git a/gee-orm/day6-transaction/session/record.go b/gee-orm/day6-transaction/session/record.go index bc6d1fe..b2850cf 100644 --- a/gee-orm/day6-transaction/session/record.go +++ b/gee-orm/day6-transaction/session/record.go @@ -1,17 +1,18 @@ package session import ( + "errors" "geeorm/clause" "reflect" ) // Create one or more records in database -func (s *Session) Create(values ...interface{}) (int64, error) { +func (s *Session) Insert(values ...interface{}) (int64, error) { recordValues := make([]interface{}, 0) for _, value := range values { - table := s.RefTable(value) - s.clause.Set(clause.INSERT, table.TableName, table.FieldNames) - recordValues = append(recordValues, table.Values(value)) + table := s.Model(value).RefTable() + s.clause.Set(clause.INSERT, table.Name, table.FieldNames) + recordValues = append(recordValues, table.RecordValues(value)) } s.clause.Set(clause.VALUES, recordValues...) @@ -28,9 +29,9 @@ func (s *Session) Create(values ...interface{}) (int64, error) { func (s *Session) Find(values interface{}) error { destSlice := reflect.Indirect(reflect.ValueOf(values)) destType := destSlice.Type().Elem() - table := s.RefTable(reflect.New(destType).Elem().Interface()) + table := s.Model(reflect.New(destType).Elem().Interface()).RefTable() - s.clause.Set(clause.SELECT, table.TableName, table.FieldNames) + s.clause.Set(clause.SELECT, table.Name, table.FieldNames) sql, vars := s.clause.Build(clause.SELECT, clause.WHERE, clause.ORDERBY, clause.LIMIT) rows, err := s.Raw(sql, vars...).QueryRows() if err != nil { @@ -55,9 +56,14 @@ func (s *Session) Find(values interface{}) error { func (s *Session) First(value interface{}) error { dest := reflect.Indirect(reflect.ValueOf(value)) destSlice := reflect.New(reflect.SliceOf(dest.Type())).Elem() - err := s.Limit(1).Find(destSlice.Addr().Interface()) + if err := s.Limit(1).Find(destSlice.Addr().Interface()); err != nil { + return err + } + if destSlice.Len() == 0 { + return errors.New("NOT FOUND") + } dest.Set(destSlice.Index(0)) - return err + return nil } // Limit adds limit condition to clause @@ -79,25 +85,19 @@ func (s *Session) OrderBy(desc string) *Session { return s } -// Set adds Assignment by condition to clause +// Update records with where clause // support map[string]interface{} -// also support "Name", "Tom", "Age", 18, etc -func (s *Session) Set(values ...interface{}) *Session { - m, ok := values[0].(map[string]interface{}) +// also support kv list: "Name", "Tom", "Age", 18, .... +func (s *Session) Update(kv ...interface{}) (int64, error) { + m, ok := kv[0].(map[string]interface{}) if !ok { m = make(map[string]interface{}) - for i := 0; i < len(values); i += 2 { - m[values[i].(string)] = values[i+1] + for i := 0; i < len(kv); i += 2 { + m[kv[i].(string)] = kv[i+1] } } - s.clause.Set(clause.SET, m) - return s -} - -// Update records with where clause -func (s *Session) Update(value interface{}) (int64, error) { - s.clause.Set(clause.UPDATE, s.guessTableName(value)) - sql, vars := s.clause.Build(clause.UPDATE, clause.SET, clause.WHERE) + s.clause.Set(clause.UPDATE, s.RefTable().Name, m) + sql, vars := s.clause.Build(clause.UPDATE, clause.WHERE) result, err := s.Raw(sql, vars...).Exec() if err != nil { return 0, err @@ -106,8 +106,8 @@ func (s *Session) Update(value interface{}) (int64, error) { } // Delete records with where clause -func (s *Session) Delete(value interface{}) (int64, error) { - s.clause.Set(clause.DELETE, s.guessTableName(value)) +func (s *Session) Delete() (int64, error) { + s.clause.Set(clause.DELETE, s.RefTable().Name) sql, vars := s.clause.Build(clause.DELETE, clause.WHERE) result, err := s.Raw(sql, vars...).Exec() if err != nil { @@ -117,12 +117,11 @@ func (s *Session) Delete(value interface{}) (int64, error) { } // Count records with where clause -func (s *Session) Count(value interface{}) (int64, error) { - s.clause.Set(clause.COUNT, s.guessTableName(value)) +func (s *Session) Count() (int64, error) { + s.clause.Set(clause.COUNT, s.RefTable().Name) sql, vars := s.clause.Build(clause.COUNT, clause.WHERE) row := s.Raw(sql, vars...).QueryRow() var tmp int64 - if err := row.Scan(&tmp); err != nil { return 0, err } diff --git a/gee-orm/day6-transaction/session/record_test.go b/gee-orm/day6-transaction/session/record_test.go index faf8535..5d482a0 100644 --- a/gee-orm/day6-transaction/session/record_test.go +++ b/gee-orm/day6-transaction/session/record_test.go @@ -10,19 +10,19 @@ var ( func testRecordInit(t *testing.T) *Session { t.Helper() - s := NewSession() - err1 := s.DropTable(&User{}) - err2 := s.CreateTable(&User{}) - _, err3 := s.Create(user1, user2) + s := NewSession().Model(&User{}) + err1 := s.DropTable() + err2 := s.CreateTable() + _, err3 := s.Insert(user1, user2) if err1 != nil || err2 != nil || err3 != nil { t.Fatal("failed init test records") } return s } -func TestSession_Create(t *testing.T) { +func TestSession_Insert(t *testing.T) { s := testRecordInit(t) - affected, err := s.Create(user3) + affected, err := s.Insert(user3) if err != nil || affected != 1 { t.Fatal("failed to create record") } @@ -30,7 +30,7 @@ func TestSession_Create(t *testing.T) { func TestSession_Find(t *testing.T) { s := testRecordInit(t) - users := []User{} + var users []User if err := s.Find(&users); err != nil || len(users) != 2 { t.Fatal("failed to query all") } @@ -57,7 +57,7 @@ func TestSession_Limit(t *testing.T) { func TestSession_Where(t *testing.T) { s := testRecordInit(t) var users []User - _, err1 := s.Create(user3) + _, err1 := s.Insert(user3) err2 := s.Where("Age = ?", 25).Find(&users) if err1 != nil || err2 != nil || len(users) != 2 { @@ -77,7 +77,7 @@ func TestSession_OrderBy(t *testing.T) { func TestSession_Update(t *testing.T) { s := testRecordInit(t) - affected, _ := s.Where("Name = ?", "Tom").Set("Age", 30).Update(&User{}) + affected, _ := s.Where("Name = ?", "Tom").Update("Age", 30) u := &User{} _ = s.OrderBy("Age DESC").First(u) @@ -88,8 +88,8 @@ func TestSession_Update(t *testing.T) { func TestSession_DeleteAndCount(t *testing.T) { s := testRecordInit(t) - affected, _ := s.Where("Name = ?", "Tom").Delete("User") - count, _ := s.Count("User") + affected, _ := s.Where("Name = ?", "Tom").Delete() + count, _ := s.Count() if affected != 1 || count != 1 { t.Fatal("failed to delete or count") diff --git a/gee-orm/day6-transaction/session/table.go b/gee-orm/day6-transaction/session/table.go index b644d3a..58e7b0f 100644 --- a/gee-orm/day6-transaction/session/table.go +++ b/gee-orm/day6-transaction/session/table.go @@ -2,58 +2,53 @@ package session import ( "fmt" + "geeorm/log" + "reflect" "strings" "geeorm/schema" ) -// RefTable returns a Schema instance that contains all parsed fields -func (s *Session) RefTable(value interface{}) *schema.Schema { - if value == nil { - panic("value is nil") +// Model assigns refTable +func (s *Session) Model(value interface{}) *Session { + // nil or different model, update refTable + if s.refTable == nil || reflect.TypeOf(value) != reflect.TypeOf(s.refTable.Model) { + s.refTable = schema.Parse(value, s.dialect) } + return s +} + +// RefTable returns a Schema instance that contains all parsed fields +func (s *Session) RefTable() *schema.Schema { if s.refTable == nil { - s.refTable = schema.Parse(value, s.dialect) + log.Error("Model is not set") } return s.refTable } // CreateTable create a table in database with a model -func (s *Session) CreateTable(value interface{}) error { - table := s.RefTable(value) +func (s *Session) CreateTable() error { + table := s.RefTable() var columns []string for _, field := range table.Fields { - tag := field.Tag - if field.Name == table.PrimaryField.Name { - tag += " PRIMARY KEY" - } - columns = append(columns, fmt.Sprintf("%s %s", field.Name, tag)) + columns = append(columns, fmt.Sprintf("%s %s %s", field.Name, field.Type, field.Tag)) } desc := strings.Join(columns, ",") - _, err := s.Raw(fmt.Sprintf("CREATE TABLE %s (%s);", table.TableName, desc)).Exec() + _, err := s.Raw(fmt.Sprintf("CREATE TABLE %s (%s);", table.Name, desc)).Exec() return err } // DropTable drops a table with the name of model -func (s *Session) DropTable(value interface{}) error { - table := s.RefTable(value) - _, err := s.Raw(fmt.Sprintf("DROP TABLE IF EXISTS %s", table.TableName)).Exec() +func (s *Session) DropTable() error { + _, err := s.Raw(fmt.Sprintf("DROP TABLE IF EXISTS %s", s.RefTable().Name)).Exec() return err } -func (s *Session) guessTableName(value interface{}) string { - if tableName, ok := value.(string); ok { - return tableName - } - return s.RefTable(value).TableName -} - // HasTable returns true of the table exists -func (s *Session) HasTable(value interface{}) bool { - tableName := s.guessTableName(value) - sql, values := s.dialect.TableExistSQL(tableName) +func (s *Session) HasTable() bool { + sql, values := s.dialect.TableExistSQL(s.RefTable().Name) row := s.Raw(sql, values...).QueryRow() var tmp string _ = row.Scan(&tmp) - return tmp == tableName + return tmp == s.RefTable().Name } diff --git a/gee-orm/day6-transaction/session/table_test.go b/gee-orm/day6-transaction/session/table_test.go index 91eb519..3bb7554 100644 --- a/gee-orm/day6-transaction/session/table_test.go +++ b/gee-orm/day6-transaction/session/table_test.go @@ -5,15 +5,24 @@ import ( ) type User struct { - Name string `geeorm:"primary_key"` + Name string `geeorm:"PRIMARY KEY"` Age int } func TestSession_CreateTable(t *testing.T) { - s := NewSession() - _ = s.DropTable(&User{}) - _ = s.CreateTable(&User{}) - if !s.HasTable("User") { - t.Fatal("failed to create table User") + s := NewSession().Model(&User{}) + _ = s.DropTable() + _ = s.CreateTable() + if !s.HasTable() { + t.Fatal("Failed to create table User") + } +} + +func TestSession_Model(t *testing.T) { + s := NewSession().Model(&User{}) + table := s.RefTable() + s.Model(&Session{}) + if table.Name != "User" || s.RefTable().Name != "Session" { + t.Fatal("Failed to change model") } } diff --git a/gee-orm/run_test.sh b/gee-orm/run_test.sh index d5c5e7f..23475a2 100755 --- a/gee-orm/run_test.sh +++ b/gee-orm/run_test.sh @@ -2,9 +2,9 @@ set -eou pipefail cur=$PWD -for item in $(ls -d $cur/day*/) +for item in "$cur"/day*/ do - echo $item - cd $item + echo "$item" + cd "$item" go test geeorm/... 2>&1 | grep -v warning done \ No newline at end of file From dd3cedd241244d0786be150d3596bb820ed1ed32 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Fri, 28 Feb 2020 23:37:41 +0800 Subject: [PATCH 050/122] fix Insert Comment && add hooks feature --- gee-orm/day1-database-sql/session/raw_test.go | 2 +- .../day2-reflect-schema/session/raw_test.go | 2 +- gee-orm/day3-save-query/session/raw_test.go | 2 +- gee-orm/day3-save-query/session/record.go | 2 +- .../day4-chain-operation/session/raw_test.go | 2 +- .../day4-chain-operation/session/record.go | 2 +- gee-orm/day5-hooks/session/hooks.go | 32 ++++++++++++++++ gee-orm/day5-hooks/session/hooks_test.go | 37 +++++++++++++++++++ gee-orm/day5-hooks/session/raw_test.go | 2 +- gee-orm/day5-hooks/session/record.go | 23 +++++------- gee-orm/day6-transaction/session/hooks.go | 32 ++++++++++++++++ .../day6-transaction/session/hooks_test.go | 37 +++++++++++++++++++ gee-orm/day6-transaction/session/raw_test.go | 2 +- gee-orm/day6-transaction/session/record.go | 11 +++++- 14 files changed, 164 insertions(+), 24 deletions(-) create mode 100644 gee-orm/day5-hooks/session/hooks.go create mode 100644 gee-orm/day5-hooks/session/hooks_test.go create mode 100644 gee-orm/day6-transaction/session/hooks.go create mode 100644 gee-orm/day6-transaction/session/hooks_test.go diff --git a/gee-orm/day1-database-sql/session/raw_test.go b/gee-orm/day1-database-sql/session/raw_test.go index 8a18162..670420e 100644 --- a/gee-orm/day1-database-sql/session/raw_test.go +++ b/gee-orm/day1-database-sql/session/raw_test.go @@ -11,7 +11,7 @@ import ( var TestDB *sql.DB func TestMain(m *testing.M) { - TestDB, _ = sql.Open("sqlite3", "gee.db") + TestDB, _ = sql.Open("sqlite3", "../gee.db") code := m.Run() _ = TestDB.Close() os.Exit(code) diff --git a/gee-orm/day2-reflect-schema/session/raw_test.go b/gee-orm/day2-reflect-schema/session/raw_test.go index a96770b..c6c3f08 100644 --- a/gee-orm/day2-reflect-schema/session/raw_test.go +++ b/gee-orm/day2-reflect-schema/session/raw_test.go @@ -16,7 +16,7 @@ var ( ) func TestMain(m *testing.M) { - TestDB, _ = sql.Open("sqlite3", "gee.db") + TestDB, _ = sql.Open("sqlite3", "../gee.db") code := m.Run() _ = TestDB.Close() os.Exit(code) diff --git a/gee-orm/day3-save-query/session/raw_test.go b/gee-orm/day3-save-query/session/raw_test.go index a96770b..c6c3f08 100644 --- a/gee-orm/day3-save-query/session/raw_test.go +++ b/gee-orm/day3-save-query/session/raw_test.go @@ -16,7 +16,7 @@ var ( ) func TestMain(m *testing.M) { - TestDB, _ = sql.Open("sqlite3", "gee.db") + TestDB, _ = sql.Open("sqlite3", "../gee.db") code := m.Run() _ = TestDB.Close() os.Exit(code) diff --git a/gee-orm/day3-save-query/session/record.go b/gee-orm/day3-save-query/session/record.go index 69fd50d..5f033e0 100644 --- a/gee-orm/day3-save-query/session/record.go +++ b/gee-orm/day3-save-query/session/record.go @@ -5,7 +5,7 @@ import ( "reflect" ) -// Create one or more records in database +// Insert one or more records in database func (s *Session) Insert(values ...interface{}) (int64, error) { recordValues := make([]interface{}, 0) for _, value := range values { diff --git a/gee-orm/day4-chain-operation/session/raw_test.go b/gee-orm/day4-chain-operation/session/raw_test.go index a96770b..c6c3f08 100644 --- a/gee-orm/day4-chain-operation/session/raw_test.go +++ b/gee-orm/day4-chain-operation/session/raw_test.go @@ -16,7 +16,7 @@ var ( ) func TestMain(m *testing.M) { - TestDB, _ = sql.Open("sqlite3", "gee.db") + TestDB, _ = sql.Open("sqlite3", "../gee.db") code := m.Run() _ = TestDB.Close() os.Exit(code) diff --git a/gee-orm/day4-chain-operation/session/record.go b/gee-orm/day4-chain-operation/session/record.go index b2850cf..cef890b 100644 --- a/gee-orm/day4-chain-operation/session/record.go +++ b/gee-orm/day4-chain-operation/session/record.go @@ -6,7 +6,7 @@ import ( "reflect" ) -// Create one or more records in database +// Insert one or more records in database func (s *Session) Insert(values ...interface{}) (int64, error) { recordValues := make([]interface{}, 0) for _, value := range values { diff --git a/gee-orm/day5-hooks/session/hooks.go b/gee-orm/day5-hooks/session/hooks.go new file mode 100644 index 0000000..12462a4 --- /dev/null +++ b/gee-orm/day5-hooks/session/hooks.go @@ -0,0 +1,32 @@ +package session + +import "reflect" + +// Hooks constants +const ( + BeforeQuery = "BeforeQuery" + AfterQuery = "AfterQuery" + BeforeUpdate = "BeforeUpdate" + AfterUpdate = "AfterUpate" + BeforeDelete = "BeforeDelete" + AfterDelete = "AfterDelete" + BeforeInsert = "BeforeInsert" + AfterInsert = "AfterInsert" +) + +// CallMethod calls the registered hooks +func (s *Session) CallMethod(method string, value interface{}) error { + fm := reflect.ValueOf(s.RefTable().Model).MethodByName(method) + if value != nil { + fm = reflect.ValueOf(value).MethodByName(method) + } + param := []reflect.Value{reflect.ValueOf(s)} + if fm.IsValid() { + if v := fm.Call(param); len(v) > 0 { + if err, ok := v[0].Interface().(error); ok { + return err + } + } + } + return nil +} diff --git a/gee-orm/day5-hooks/session/hooks_test.go b/gee-orm/day5-hooks/session/hooks_test.go new file mode 100644 index 0000000..f896d01 --- /dev/null +++ b/gee-orm/day5-hooks/session/hooks_test.go @@ -0,0 +1,37 @@ +package session + +import ( + "geeorm/log" + "testing" +) + +type Account struct { + ID int `geeorm:"PRIMARY KEY"` + Password string +} + +func (account *Account) BeforeInsert(s *Session) error { + log.Info("before inert", account) + account.ID += 1000 + return nil +} + +func (account *Account) AfterQuery(s *Session) error { + log.Info("after query", account) + account.Password = "******" + return nil +} + +func TestSession_CallMethod(t *testing.T) { + s := NewSession().Model(&Account{}) + _ = s.DropTable() + _ = s.CreateTable() + _, _ = s.Insert(&Account{1, "123456"}, &Account{2, "qwerty"}) + + u := &Account{} + + err := s.First(u) + if err != nil || u.ID != 1001 || u.Password != "******" { + t.Fatal("Failed to call hooks after query, got", u) + } +} diff --git a/gee-orm/day5-hooks/session/raw_test.go b/gee-orm/day5-hooks/session/raw_test.go index a96770b..c6c3f08 100644 --- a/gee-orm/day5-hooks/session/raw_test.go +++ b/gee-orm/day5-hooks/session/raw_test.go @@ -16,7 +16,7 @@ var ( ) func TestMain(m *testing.M) { - TestDB, _ = sql.Open("sqlite3", "gee.db") + TestDB, _ = sql.Open("sqlite3", "../gee.db") code := m.Run() _ = TestDB.Close() os.Exit(code) diff --git a/gee-orm/day5-hooks/session/record.go b/gee-orm/day5-hooks/session/record.go index 86b9ddd..fdfca4d 100644 --- a/gee-orm/day5-hooks/session/record.go +++ b/gee-orm/day5-hooks/session/record.go @@ -6,22 +6,11 @@ import ( "reflect" ) -// CallMethod calls the registered hooks -func (s *Session) CallMethod(method string) error { - if fm := reflect.ValueOf(s.refTable.Model).MethodByName(method); fm.IsValid() { - if v := fm.Call([]reflect.Value{}); len(v) > 0 { - if err, ok := v[0].Interface().(error); ok { - return err - } - } - } - return nil -} - -// Create one or more records in database +// Insert one or more records in database func (s *Session) Insert(values ...interface{}) (int64, error) { recordValues := make([]interface{}, 0) for _, value := range values { + s.CallMethod(BeforeInsert, value) table := s.Model(value).RefTable() s.clause.Set(clause.INSERT, table.Name, table.FieldNames) recordValues = append(recordValues, table.RecordValues(value)) @@ -33,12 +22,13 @@ func (s *Session) Insert(values ...interface{}) (int64, error) { if err != nil { return 0, err } - + s.CallMethod(AfterInsert, nil) return result.RowsAffected() } // Find gets all eligible records func (s *Session) Find(values interface{}) error { + s.CallMethod(BeforeQuery, nil) destSlice := reflect.Indirect(reflect.ValueOf(values)) destType := destSlice.Type().Elem() table := s.Model(reflect.New(destType).Elem().Interface()).RefTable() @@ -59,6 +49,7 @@ func (s *Session) Find(values interface{}) error { if err := rows.Scan(values...); err != nil { return err } + s.CallMethod(AfterQuery, dest.Addr().Interface()) destSlice.Set(reflect.Append(destSlice, dest)) } return rows.Close() @@ -101,6 +92,7 @@ func (s *Session) OrderBy(desc string) *Session { // support map[string]interface{} // also support kv list: "Name", "Tom", "Age", 18, .... func (s *Session) Update(kv ...interface{}) (int64, error) { + s.CallMethod(BeforeUpdate, nil) m, ok := kv[0].(map[string]interface{}) if !ok { m = make(map[string]interface{}) @@ -114,17 +106,20 @@ func (s *Session) Update(kv ...interface{}) (int64, error) { if err != nil { return 0, err } + s.CallMethod(AfterUpdate, nil) return result.RowsAffected() } // Delete records with where clause func (s *Session) Delete() (int64, error) { + s.CallMethod(BeforeDelete, nil) s.clause.Set(clause.DELETE, s.RefTable().Name) sql, vars := s.clause.Build(clause.DELETE, clause.WHERE) result, err := s.Raw(sql, vars...).Exec() if err != nil { return 0, err } + s.CallMethod(AfterDelete, nil) return result.RowsAffected() } diff --git a/gee-orm/day6-transaction/session/hooks.go b/gee-orm/day6-transaction/session/hooks.go new file mode 100644 index 0000000..12462a4 --- /dev/null +++ b/gee-orm/day6-transaction/session/hooks.go @@ -0,0 +1,32 @@ +package session + +import "reflect" + +// Hooks constants +const ( + BeforeQuery = "BeforeQuery" + AfterQuery = "AfterQuery" + BeforeUpdate = "BeforeUpdate" + AfterUpdate = "AfterUpate" + BeforeDelete = "BeforeDelete" + AfterDelete = "AfterDelete" + BeforeInsert = "BeforeInsert" + AfterInsert = "AfterInsert" +) + +// CallMethod calls the registered hooks +func (s *Session) CallMethod(method string, value interface{}) error { + fm := reflect.ValueOf(s.RefTable().Model).MethodByName(method) + if value != nil { + fm = reflect.ValueOf(value).MethodByName(method) + } + param := []reflect.Value{reflect.ValueOf(s)} + if fm.IsValid() { + if v := fm.Call(param); len(v) > 0 { + if err, ok := v[0].Interface().(error); ok { + return err + } + } + } + return nil +} diff --git a/gee-orm/day6-transaction/session/hooks_test.go b/gee-orm/day6-transaction/session/hooks_test.go new file mode 100644 index 0000000..f896d01 --- /dev/null +++ b/gee-orm/day6-transaction/session/hooks_test.go @@ -0,0 +1,37 @@ +package session + +import ( + "geeorm/log" + "testing" +) + +type Account struct { + ID int `geeorm:"PRIMARY KEY"` + Password string +} + +func (account *Account) BeforeInsert(s *Session) error { + log.Info("before inert", account) + account.ID += 1000 + return nil +} + +func (account *Account) AfterQuery(s *Session) error { + log.Info("after query", account) + account.Password = "******" + return nil +} + +func TestSession_CallMethod(t *testing.T) { + s := NewSession().Model(&Account{}) + _ = s.DropTable() + _ = s.CreateTable() + _, _ = s.Insert(&Account{1, "123456"}, &Account{2, "qwerty"}) + + u := &Account{} + + err := s.First(u) + if err != nil || u.ID != 1001 || u.Password != "******" { + t.Fatal("Failed to call hooks after query, got", u) + } +} diff --git a/gee-orm/day6-transaction/session/raw_test.go b/gee-orm/day6-transaction/session/raw_test.go index a96770b..c6c3f08 100644 --- a/gee-orm/day6-transaction/session/raw_test.go +++ b/gee-orm/day6-transaction/session/raw_test.go @@ -16,7 +16,7 @@ var ( ) func TestMain(m *testing.M) { - TestDB, _ = sql.Open("sqlite3", "gee.db") + TestDB, _ = sql.Open("sqlite3", "../gee.db") code := m.Run() _ = TestDB.Close() os.Exit(code) diff --git a/gee-orm/day6-transaction/session/record.go b/gee-orm/day6-transaction/session/record.go index b2850cf..fdfca4d 100644 --- a/gee-orm/day6-transaction/session/record.go +++ b/gee-orm/day6-transaction/session/record.go @@ -6,10 +6,11 @@ import ( "reflect" ) -// Create one or more records in database +// Insert one or more records in database func (s *Session) Insert(values ...interface{}) (int64, error) { recordValues := make([]interface{}, 0) for _, value := range values { + s.CallMethod(BeforeInsert, value) table := s.Model(value).RefTable() s.clause.Set(clause.INSERT, table.Name, table.FieldNames) recordValues = append(recordValues, table.RecordValues(value)) @@ -21,12 +22,13 @@ func (s *Session) Insert(values ...interface{}) (int64, error) { if err != nil { return 0, err } - + s.CallMethod(AfterInsert, nil) return result.RowsAffected() } // Find gets all eligible records func (s *Session) Find(values interface{}) error { + s.CallMethod(BeforeQuery, nil) destSlice := reflect.Indirect(reflect.ValueOf(values)) destType := destSlice.Type().Elem() table := s.Model(reflect.New(destType).Elem().Interface()).RefTable() @@ -47,6 +49,7 @@ func (s *Session) Find(values interface{}) error { if err := rows.Scan(values...); err != nil { return err } + s.CallMethod(AfterQuery, dest.Addr().Interface()) destSlice.Set(reflect.Append(destSlice, dest)) } return rows.Close() @@ -89,6 +92,7 @@ func (s *Session) OrderBy(desc string) *Session { // support map[string]interface{} // also support kv list: "Name", "Tom", "Age", 18, .... func (s *Session) Update(kv ...interface{}) (int64, error) { + s.CallMethod(BeforeUpdate, nil) m, ok := kv[0].(map[string]interface{}) if !ok { m = make(map[string]interface{}) @@ -102,17 +106,20 @@ func (s *Session) Update(kv ...interface{}) (int64, error) { if err != nil { return 0, err } + s.CallMethod(AfterUpdate, nil) return result.RowsAffected() } // Delete records with where clause func (s *Session) Delete() (int64, error) { + s.CallMethod(BeforeDelete, nil) s.clause.Set(clause.DELETE, s.RefTable().Name) sql, vars := s.clause.Build(clause.DELETE, clause.WHERE) result, err := s.Raw(sql, vars...).Exec() if err != nil { return 0, err } + s.CallMethod(AfterDelete, nil) return result.RowsAffected() } From dd2f1d1d4658787329168241e949779e8886273f Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Sat, 29 Feb 2020 00:29:53 +0800 Subject: [PATCH 051/122] add day7 migrate --- README.md | 22 ++- gee-orm/day1-database-sql/session/raw_test.go | 2 +- .../day2-reflect-schema/session/raw_test.go | 2 +- gee-orm/day3-save-query/session/raw_test.go | 2 +- .../clause/clause_test.go | 6 +- .../day4-chain-operation/session/raw_test.go | 2 +- gee-orm/day5-hooks/clause/clause_test.go | 6 +- gee-orm/day5-hooks/session/raw_test.go | 2 +- .../day6-transaction/clause/clause_test.go | 6 +- gee-orm/day6-transaction/session/raw_test.go | 2 +- gee-orm/day7-migrate/clause/clause.go | 51 +++++++ gee-orm/day7-migrate/clause/clause_test.go | 74 ++++++++++ gee-orm/day7-migrate/clause/generator.go | 101 +++++++++++++ gee-orm/day7-migrate/dialect/dialect.go | 22 +++ gee-orm/day7-migrate/dialect/sqlite3.go | 45 ++++++ gee-orm/day7-migrate/dialect/sqlite3_test.go | 25 ++++ gee-orm/day7-migrate/geeorm.go | 127 ++++++++++++++++ gee-orm/day7-migrate/geeorm_test.go | 86 +++++++++++ gee-orm/day7-migrate/go.mod | 5 + gee-orm/day7-migrate/log/log.go | 47 ++++++ gee-orm/day7-migrate/log/log_test.go | 17 +++ gee-orm/day7-migrate/schema/schema.go | 65 +++++++++ gee-orm/day7-migrate/schema/schema_test.go | 35 +++++ gee-orm/day7-migrate/session/hooks.go | 32 +++++ gee-orm/day7-migrate/session/hooks_test.go | 37 +++++ gee-orm/day7-migrate/session/raw.go | 90 ++++++++++++ gee-orm/day7-migrate/session/raw_test.go | 48 +++++++ gee-orm/day7-migrate/session/record.go | 136 ++++++++++++++++++ gee-orm/day7-migrate/session/record_test.go | 97 +++++++++++++ gee-orm/day7-migrate/session/table.go | 54 +++++++ gee-orm/day7-migrate/session/table_test.go | 28 ++++ gee-orm/day7-migrate/session/transaction.go | 31 ++++ 32 files changed, 1287 insertions(+), 18 deletions(-) create mode 100644 gee-orm/day7-migrate/clause/clause.go create mode 100644 gee-orm/day7-migrate/clause/clause_test.go create mode 100644 gee-orm/day7-migrate/clause/generator.go create mode 100644 gee-orm/day7-migrate/dialect/dialect.go create mode 100644 gee-orm/day7-migrate/dialect/sqlite3.go create mode 100644 gee-orm/day7-migrate/dialect/sqlite3_test.go create mode 100644 gee-orm/day7-migrate/geeorm.go create mode 100644 gee-orm/day7-migrate/geeorm_test.go create mode 100644 gee-orm/day7-migrate/go.mod create mode 100644 gee-orm/day7-migrate/log/log.go create mode 100644 gee-orm/day7-migrate/log/log_test.go create mode 100644 gee-orm/day7-migrate/schema/schema.go create mode 100644 gee-orm/day7-migrate/schema/schema_test.go create mode 100644 gee-orm/day7-migrate/session/hooks.go create mode 100644 gee-orm/day7-migrate/session/hooks_test.go create mode 100644 gee-orm/day7-migrate/session/raw.go create mode 100644 gee-orm/day7-migrate/session/raw_test.go create mode 100644 gee-orm/day7-migrate/session/record.go create mode 100644 gee-orm/day7-migrate/session/record_test.go create mode 100644 gee-orm/day7-migrate/session/table.go create mode 100644 gee-orm/day7-migrate/session/table_test.go create mode 100644 gee-orm/day7-migrate/session/transaction.go diff --git a/README.md b/README.md index aee281f..047f2a0 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,15 @@ [GeeORM] 是一个模仿 [gorm](https://github.com/jinzhu/gorm) 和 [xorm](https://github.com/go-xorm/xorm) 的 ORM 框架 -gorm 准备推出完全重写的 v2 版本(目前还在开发中),相对 gorm-v1 来说,xorm 的设计更容易理解,所以 geeorm 接口设计上主要参考了 xorm,具体实现参考了 gorm。 +gorm 准备推出完全重写的 v2 版本(目前还在开发中),相对 gorm-v1 来说,xorm 的设计更容易理解,所以 geeorm 接口设计上主要参考了 xorm,一些细节实现上参考了 gorm。 + +- 第一天:database/sql 基础 | [Code](gee-cache/day1-database-sql) +- 第二天:对象表结构映射 | [Code](gee-cache/day2-reflect-schema) +- 第三天:插入/查询记录 | [Code](gee-cache/day3-save-query) +- 第四天:链式操作与更新删除 | [Code](gee-cache/day4-chain-operation) +- 第五天:实现钩子(Hooks) | [Code](gee-cache/day5-hooks) +- 第六天:支持事务(Transaction) | [Code](gee-cache/day6-transaction) +- 第七天:数据库迁移(Migrate) | [Code](gee-cache/day7-migrate) ### WebAssembly 使用示例 @@ -80,12 +88,20 @@ What can I write in 7 days? A gin-like web framework? A distributed cache like g - Day 6 - Cache Breakdown & Single Flight | [Code](gee-cache/day6-single-flight) - Day 7 - Use Protobuf as RPC Data Exchange Type | [Code](gee-cache/day7-proto-buf) -## Object Relational Mapping - GeeOrm +## Object Relational Mapping - GeeORM -[GeeOrm] is a [gorm](https://github.com/jinzhu/gorm)-like and [xorm](https://github.com/go-xorm/xorm)-like object relational mapping library +[GeeORM] is a [gorm](https://github.com/jinzhu/gorm)-like and [xorm](https://github.com/go-xorm/xorm)-like object relational mapping library Xorm's desgin is easier to understand than gorm-v1, so the main designs references xorm and some detailed implementions references gorm-v1. +- Day 1 - database/sql Basic | [Code](gee-cache/day1-database-sql) +- Day 2 - Object Schame Mapping | [Code](gee-cache/day2-reflect-schema) +- Day 3 - Insert and Query | [Code](gee-cache/day3-save-query) +- Day 4 - Chain, Delete and Update | [Code](gee-cache/day4-chain-operation) +- Day 5 - Support Hooks | [Code](gee-cache/day5-hooks) +- Day 6 - Support Transaction | [Code](gee-cache/day6-transaction) +- Day 7 - Migrate Database | [Code](gee-cache/day7-migrate) + ## Golang WebAssembly Demo - Demo 1 - Hello World [Code](demo-wasm/hello-world) diff --git a/gee-orm/day1-database-sql/session/raw_test.go b/gee-orm/day1-database-sql/session/raw_test.go index 670420e..5721844 100644 --- a/gee-orm/day1-database-sql/session/raw_test.go +++ b/gee-orm/day1-database-sql/session/raw_test.go @@ -24,7 +24,7 @@ func NewSession() *Session { func TestSession_Exec(t *testing.T) { s := NewSession() _, _ = s.Raw("DROP TABLE IF EXISTS User;").Exec() - _, _ = s.Raw("CREATE TABLE User(name text);").Exec() + _, _ = s.Raw("CREATE TABLE User(Name text);").Exec() result, _ := s.Raw("INSERT INTO User(`Name`) values (?), (?)", "Tom", "Sam").Exec() if count, err := result.RowsAffected(); err != nil || count != 2 { t.Fatal("expect 2, but got", count) diff --git a/gee-orm/day2-reflect-schema/session/raw_test.go b/gee-orm/day2-reflect-schema/session/raw_test.go index c6c3f08..d521173 100644 --- a/gee-orm/day2-reflect-schema/session/raw_test.go +++ b/gee-orm/day2-reflect-schema/session/raw_test.go @@ -29,7 +29,7 @@ func NewSession() *Session { func TestSession_Exec(t *testing.T) { s := NewSession() _, _ = s.Raw("DROP TABLE IF EXISTS User;").Exec() - _, _ = s.Raw("CREATE TABLE User(name text);").Exec() + _, _ = s.Raw("CREATE TABLE User(Name text);").Exec() result, _ := s.Raw("INSERT INTO User(`Name`) values (?), (?)", "Tom", "Sam").Exec() if count, err := result.RowsAffected(); err != nil || count != 2 { t.Fatal("expect 2, but got", count) diff --git a/gee-orm/day3-save-query/session/raw_test.go b/gee-orm/day3-save-query/session/raw_test.go index c6c3f08..d521173 100644 --- a/gee-orm/day3-save-query/session/raw_test.go +++ b/gee-orm/day3-save-query/session/raw_test.go @@ -29,7 +29,7 @@ func NewSession() *Session { func TestSession_Exec(t *testing.T) { s := NewSession() _, _ = s.Raw("DROP TABLE IF EXISTS User;").Exec() - _, _ = s.Raw("CREATE TABLE User(name text);").Exec() + _, _ = s.Raw("CREATE TABLE User(Name text);").Exec() result, _ := s.Raw("INSERT INTO User(`Name`) values (?), (?)", "Tom", "Sam").Exec() if count, err := result.RowsAffected(); err != nil || count != 2 { t.Fatal("expect 2, but got", count) diff --git a/gee-orm/day4-chain-operation/clause/clause_test.go b/gee-orm/day4-chain-operation/clause/clause_test.go index b54c6e0..62e0ccb 100644 --- a/gee-orm/day4-chain-operation/clause/clause_test.go +++ b/gee-orm/day4-chain-operation/clause/clause_test.go @@ -34,14 +34,14 @@ func testSelect(t *testing.T) { func testUpdate(t *testing.T) { var clause Clause - clause.Set(UPDATE, "User", map[string]interface{}{"Age": 30, "Name": "Tommy"}) + clause.Set(UPDATE, "User", map[string]interface{}{"Age": 30}) clause.Set(WHERE, "Name = ?", "Tom") sql, vars := clause.Build(UPDATE, WHERE) t.Log(sql, vars) - if sql != "UPDATE User SET Age = ?, Name = ? WHERE Name = ?" { + if sql != "UPDATE User SET Age = ? WHERE Name = ?" { t.Fatal("failed to build SQL") } - if !reflect.DeepEqual(vars, []interface{}{30, "Tommy", "Tom"}) { + if !reflect.DeepEqual(vars, []interface{}{30, "Tom"}) { t.Fatal("failed to build SQLVars") } } diff --git a/gee-orm/day4-chain-operation/session/raw_test.go b/gee-orm/day4-chain-operation/session/raw_test.go index c6c3f08..d521173 100644 --- a/gee-orm/day4-chain-operation/session/raw_test.go +++ b/gee-orm/day4-chain-operation/session/raw_test.go @@ -29,7 +29,7 @@ func NewSession() *Session { func TestSession_Exec(t *testing.T) { s := NewSession() _, _ = s.Raw("DROP TABLE IF EXISTS User;").Exec() - _, _ = s.Raw("CREATE TABLE User(name text);").Exec() + _, _ = s.Raw("CREATE TABLE User(Name text);").Exec() result, _ := s.Raw("INSERT INTO User(`Name`) values (?), (?)", "Tom", "Sam").Exec() if count, err := result.RowsAffected(); err != nil || count != 2 { t.Fatal("expect 2, but got", count) diff --git a/gee-orm/day5-hooks/clause/clause_test.go b/gee-orm/day5-hooks/clause/clause_test.go index b54c6e0..62e0ccb 100644 --- a/gee-orm/day5-hooks/clause/clause_test.go +++ b/gee-orm/day5-hooks/clause/clause_test.go @@ -34,14 +34,14 @@ func testSelect(t *testing.T) { func testUpdate(t *testing.T) { var clause Clause - clause.Set(UPDATE, "User", map[string]interface{}{"Age": 30, "Name": "Tommy"}) + clause.Set(UPDATE, "User", map[string]interface{}{"Age": 30}) clause.Set(WHERE, "Name = ?", "Tom") sql, vars := clause.Build(UPDATE, WHERE) t.Log(sql, vars) - if sql != "UPDATE User SET Age = ?, Name = ? WHERE Name = ?" { + if sql != "UPDATE User SET Age = ? WHERE Name = ?" { t.Fatal("failed to build SQL") } - if !reflect.DeepEqual(vars, []interface{}{30, "Tommy", "Tom"}) { + if !reflect.DeepEqual(vars, []interface{}{30, "Tom"}) { t.Fatal("failed to build SQLVars") } } diff --git a/gee-orm/day5-hooks/session/raw_test.go b/gee-orm/day5-hooks/session/raw_test.go index c6c3f08..d521173 100644 --- a/gee-orm/day5-hooks/session/raw_test.go +++ b/gee-orm/day5-hooks/session/raw_test.go @@ -29,7 +29,7 @@ func NewSession() *Session { func TestSession_Exec(t *testing.T) { s := NewSession() _, _ = s.Raw("DROP TABLE IF EXISTS User;").Exec() - _, _ = s.Raw("CREATE TABLE User(name text);").Exec() + _, _ = s.Raw("CREATE TABLE User(Name text);").Exec() result, _ := s.Raw("INSERT INTO User(`Name`) values (?), (?)", "Tom", "Sam").Exec() if count, err := result.RowsAffected(); err != nil || count != 2 { t.Fatal("expect 2, but got", count) diff --git a/gee-orm/day6-transaction/clause/clause_test.go b/gee-orm/day6-transaction/clause/clause_test.go index b54c6e0..62e0ccb 100644 --- a/gee-orm/day6-transaction/clause/clause_test.go +++ b/gee-orm/day6-transaction/clause/clause_test.go @@ -34,14 +34,14 @@ func testSelect(t *testing.T) { func testUpdate(t *testing.T) { var clause Clause - clause.Set(UPDATE, "User", map[string]interface{}{"Age": 30, "Name": "Tommy"}) + clause.Set(UPDATE, "User", map[string]interface{}{"Age": 30}) clause.Set(WHERE, "Name = ?", "Tom") sql, vars := clause.Build(UPDATE, WHERE) t.Log(sql, vars) - if sql != "UPDATE User SET Age = ?, Name = ? WHERE Name = ?" { + if sql != "UPDATE User SET Age = ? WHERE Name = ?" { t.Fatal("failed to build SQL") } - if !reflect.DeepEqual(vars, []interface{}{30, "Tommy", "Tom"}) { + if !reflect.DeepEqual(vars, []interface{}{30, "Tom"}) { t.Fatal("failed to build SQLVars") } } diff --git a/gee-orm/day6-transaction/session/raw_test.go b/gee-orm/day6-transaction/session/raw_test.go index c6c3f08..d521173 100644 --- a/gee-orm/day6-transaction/session/raw_test.go +++ b/gee-orm/day6-transaction/session/raw_test.go @@ -29,7 +29,7 @@ func NewSession() *Session { func TestSession_Exec(t *testing.T) { s := NewSession() _, _ = s.Raw("DROP TABLE IF EXISTS User;").Exec() - _, _ = s.Raw("CREATE TABLE User(name text);").Exec() + _, _ = s.Raw("CREATE TABLE User(Name text);").Exec() result, _ := s.Raw("INSERT INTO User(`Name`) values (?), (?)", "Tom", "Sam").Exec() if count, err := result.RowsAffected(); err != nil || count != 2 { t.Fatal("expect 2, but got", count) diff --git a/gee-orm/day7-migrate/clause/clause.go b/gee-orm/day7-migrate/clause/clause.go new file mode 100644 index 0000000..02fcf93 --- /dev/null +++ b/gee-orm/day7-migrate/clause/clause.go @@ -0,0 +1,51 @@ +package clause + +import ( + "strings" +) + +// Clause contains SQL conditions +type Clause struct { + sql map[Type]string + sqlVars map[Type][]interface{} +} + +// Type is the type of Clause +type Type int + +// Support types for Clause +const ( + INSERT Type = iota + VALUES + SELECT + LIMIT + WHERE + ORDERBY + UPDATE + DELETE + COUNT +) + +// Set adds a sub clause of specific type +func (c *Clause) Set(name Type, vars ...interface{}) { + if c.sql == nil { + c.sql = make(map[Type]string) + c.sqlVars = make(map[Type][]interface{}) + } + sql, vars := generators[name](vars...) + c.sql[name] = sql + c.sqlVars[name] = vars +} + +// Build generate the final SQL and SQLVars +func (c *Clause) Build(orders ...Type) (string, []interface{}) { + var sqls []string + var vars []interface{} + for _, order := range orders { + if sql, ok := c.sql[order]; ok { + sqls = append(sqls, sql) + vars = append(vars, c.sqlVars[order]...) + } + } + return strings.Join(sqls, " "), vars +} diff --git a/gee-orm/day7-migrate/clause/clause_test.go b/gee-orm/day7-migrate/clause/clause_test.go new file mode 100644 index 0000000..62e0ccb --- /dev/null +++ b/gee-orm/day7-migrate/clause/clause_test.go @@ -0,0 +1,74 @@ +package clause + +import ( + "reflect" + "testing" +) + +func TestClause_Set(t *testing.T) { + var clause Clause + clause.Set(INSERT, "User", []string{"Name", "Age"}) + sql := clause.sql[INSERT] + vars := clause.sqlVars[INSERT] + t.Log(sql, vars) + if sql != "INSERT INTO User (Name,Age)" || len(vars) != 0 { + t.Fatal("failed to get clause") + } +} + +func testSelect(t *testing.T) { + var clause Clause + clause.Set(LIMIT, 3) + clause.Set(SELECT, "User", []string{"*"}) + clause.Set(WHERE, "Name = ?", "Tom") + clause.Set(ORDERBY, "Age ASC") + sql, vars := clause.Build(SELECT, WHERE, ORDERBY, LIMIT) + t.Log(sql, vars) + if sql != "SELECT * FROM User WHERE Name = ? ORDER BY Age ASC LIMIT ?" { + t.Fatal("failed to build SQL") + } + if !reflect.DeepEqual(vars, []interface{}{"Tom", 3}) { + t.Fatal("failed to build SQLVars") + } +} + +func testUpdate(t *testing.T) { + var clause Clause + clause.Set(UPDATE, "User", map[string]interface{}{"Age": 30}) + clause.Set(WHERE, "Name = ?", "Tom") + sql, vars := clause.Build(UPDATE, WHERE) + t.Log(sql, vars) + if sql != "UPDATE User SET Age = ? WHERE Name = ?" { + t.Fatal("failed to build SQL") + } + if !reflect.DeepEqual(vars, []interface{}{30, "Tom"}) { + t.Fatal("failed to build SQLVars") + } +} + +func testDelete(t *testing.T) { + var clause Clause + clause.Set(DELETE, "User") + clause.Set(WHERE, "Name = ?", "Tom") + + sql, vars := clause.Build(DELETE, WHERE) + t.Log(sql, vars) + if sql != "DELETE FROM User WHERE Name = ?" { + t.Fatal("failed to build SQL") + } + if !reflect.DeepEqual(vars, []interface{}{"Tom"}) { + t.Fatal("failed to build SQLVars") + } +} + +func TestClause_Build(t *testing.T) { + t.Run("select", func(t *testing.T) { + testSelect(t) + }) + t.Run("update", func(t *testing.T) { + testUpdate(t) + }) + t.Run("delete", func(t *testing.T) { + testDelete(t) + }) +} diff --git a/gee-orm/day7-migrate/clause/generator.go b/gee-orm/day7-migrate/clause/generator.go new file mode 100644 index 0000000..127fc43 --- /dev/null +++ b/gee-orm/day7-migrate/clause/generator.go @@ -0,0 +1,101 @@ +package clause + +import ( + "fmt" + "strings" +) + +type generator func(values ...interface{}) (string, []interface{}) + +var generators map[Type]generator + +func init() { + generators = make(map[Type]generator) + generators[INSERT] = _insert + generators[VALUES] = _values + generators[SELECT] = _select + generators[LIMIT] = _limit + generators[WHERE] = _where + generators[ORDERBY] = _orderBy + generators[UPDATE] = _update + generators[DELETE] = _delete + generators[COUNT] = _count +} + +func genBindVars(num int) string { + var vars []string + for i := 0; i < num; i++ { + vars = append(vars, "?") + } + return strings.Join(vars, ", ") +} + +func _insert(values ...interface{}) (string, []interface{}) { + // INSERT INTO $tableName ($fields) + tableName := values[0] + fields := strings.Join(values[1].([]string), ",") + return fmt.Sprintf("INSERT INTO %s (%v)", tableName, fields), []interface{}{} +} + +func _values(values ...interface{}) (string, []interface{}) { + // VALUES ($v1), (&v2), ... + var bindStr string + var sql strings.Builder + var vars []interface{} + sql.WriteString("VALUES ") + for i, value := range values { + v := value.([]interface{}) + if bindStr == "" { + bindStr = genBindVars(len(v)) + } + sql.WriteString(fmt.Sprintf("(%v)", bindStr)) + if i+1 != len(values) { + sql.WriteString(", ") + } + vars = append(vars, v...) + } + return sql.String(), vars + +} + +func _select(values ...interface{}) (string, []interface{}) { + // SELECT $fields FROM $tableName + tableName := values[0] + fields := strings.Join(values[1].([]string), ",") + return fmt.Sprintf("SELECT %v FROM %s", fields, tableName), []interface{}{} +} + +func _limit(values ...interface{}) (string, []interface{}) { + // LIMIT $num + return "LIMIT ?", values +} + +func _where(values ...interface{}) (string, []interface{}) { + // WHERE $desc + desc, vars := values[0], values[1:] + return fmt.Sprintf("WHERE %s", desc), vars +} + +func _orderBy(values ...interface{}) (string, []interface{}) { + return fmt.Sprintf("ORDER BY %s", values[0]), []interface{}{} +} + +func _update(values ...interface{}) (string, []interface{}) { + tableName := values[0] + m := values[1].(map[string]interface{}) + var keys []string + var vars []interface{} + for k, v := range m { + keys = append(keys, k+" = ?") + vars = append(vars, v) + } + return fmt.Sprintf("UPDATE %s SET %s", tableName, strings.Join(keys, ", ")), vars +} + +func _delete(values ...interface{}) (string, []interface{}) { + return fmt.Sprintf("DELETE FROM %s", values[0]), []interface{}{} +} + +func _count(values ...interface{}) (string, []interface{}) { + return _select(values[0], []string{"count(*)"}) +} diff --git a/gee-orm/day7-migrate/dialect/dialect.go b/gee-orm/day7-migrate/dialect/dialect.go new file mode 100644 index 0000000..4696314 --- /dev/null +++ b/gee-orm/day7-migrate/dialect/dialect.go @@ -0,0 +1,22 @@ +package dialect + +import "reflect" + +var dialectsMap = map[string]Dialect{} + +// Dialect is an interface contains methods that a dialect has to implement +type Dialect interface { + DataTypeOf(typ reflect.Value) string + TableExistSQL(tableName string) (string, []interface{}) +} + +// RegisterDialect register a dialect to the global variable +func RegisterDialect(name string, dialect Dialect) { + dialectsMap[name] = dialect +} + +// Get the dialect from global variable if it exists +func GetDialect(name string) (dialect Dialect, ok bool) { + dialect, ok = dialectsMap[name] + return +} diff --git a/gee-orm/day7-migrate/dialect/sqlite3.go b/gee-orm/day7-migrate/dialect/sqlite3.go new file mode 100644 index 0000000..f3c3897 --- /dev/null +++ b/gee-orm/day7-migrate/dialect/sqlite3.go @@ -0,0 +1,45 @@ +package dialect + +import ( + "fmt" + "reflect" + "time" +) + +type sqlite3 struct{} + +var _ Dialect = (*sqlite3)(nil) + +func init() { + RegisterDialect("sqlite3", &sqlite3{}) +} + +// Get Data Type for sqlite3 Dialect +func (s *sqlite3) DataTypeOf(typ reflect.Value) string { + switch typ.Kind() { + case reflect.Bool: + return "bool" + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uintptr: + return "integer" + case reflect.Int64, reflect.Uint64: + return "bigint" + case reflect.Float32, reflect.Float64: + return "real" + case reflect.String: + return "text" + case reflect.Array, reflect.Slice: + return "blob" + case reflect.Struct: + if _, ok := typ.Interface().(time.Time); ok { + return "datetime" + } + } + panic(fmt.Sprintf("invalid sql type %s (%s)", typ.Type().Name(), typ.Kind())) +} + +// TableExistSQL returns SQL that judge whether the table exists in database +func (s *sqlite3) TableExistSQL(tableName string) (string, []interface{}) { + args := []interface{}{tableName} + return "SELECT name FROM sqlite_master WHERE type='table' and name = ?", args +} diff --git a/gee-orm/day7-migrate/dialect/sqlite3_test.go b/gee-orm/day7-migrate/dialect/sqlite3_test.go new file mode 100644 index 0000000..3df5f07 --- /dev/null +++ b/gee-orm/day7-migrate/dialect/sqlite3_test.go @@ -0,0 +1,25 @@ +package dialect + +import ( + "reflect" + "testing" +) + +func TestDataTypeOf(t *testing.T) { + dial := &sqlite3{} + cases := []struct { + Value interface{} + Type string + }{ + {"Tom", "text"}, + {123, "integer"}, + {1.2, "real"}, + {[]int{1, 2, 3}, "blob"}, + } + + for _, c := range cases { + if typ := dial.DataTypeOf(reflect.ValueOf(c.Value)); typ != c.Type { + t.Fatalf("expect %s, but got %s", c.Type, typ) + } + } +} diff --git a/gee-orm/day7-migrate/geeorm.go b/gee-orm/day7-migrate/geeorm.go new file mode 100644 index 0000000..017d116 --- /dev/null +++ b/gee-orm/day7-migrate/geeorm.go @@ -0,0 +1,127 @@ +package geeorm + +import ( + "database/sql" + "fmt" + "geeorm/dialect" + "geeorm/log" + "geeorm/session" + "strings" +) + +// Engine is the main struct of geeorm, manages all db sessions and transactions. +type Engine struct { + db *sql.DB + dialect dialect.Dialect +} + +// NewEngine create a instance of Engine +// connect database and ping it to test whether it's alive +func NewEngine(driver, source string) (e *Engine, err error) { + db, err := sql.Open(driver, source) + if err != nil { + log.Error(err) + return + } + // Send a ping to make sure the database connection is alive. + if err = db.Ping(); err != nil { + log.Error(err) + return + } + // make sure the specific dialect exists + dial, ok := dialect.GetDialect(driver) + if !ok { + log.Errorf("dialect %s Not Found", driver) + return + } + e = &Engine{db: db, dialect: dial} + log.Info("Connect database success") + return +} + +// Close database connection +func (engine *Engine) Close() { + if err := engine.db.Close(); err != nil { + log.Error("Failed to close database") + } + log.Info("Close database success") +} + +// NewSession creates a new session for next operations +func (engine *Engine) NewSession() *session.Session { + return session.New(engine.db, engine.dialect) +} + +// TxFunc will be called between tx.Begin() and tx.Commit() +// https://stackoverflow.com/questions/16184238/database-sql-tx-detecting-commit-or-rollback +type TxFunc func(*session.Session) (interface{}, error) + +// Transaction executes sql wrapped in a transaction, then automatically commit if no error occurs +func (engine *Engine) Transaction(f TxFunc) (result interface{}, err error) { + s := engine.NewSession() + if err := s.Begin(); err != nil { + return nil, err + } + defer func() { + if p := recover(); p != nil { + _ = s.Rollback() + panic(p) // re-throw panic after Rollback + } else if err != nil { + _ = s.Rollback() // err is non-nil; don't change it + } else { + err = s.Commit() // err is nil; if Commit returns error update err + } + }() + + return f(s) +} + +// difference returns a - b +func difference(a []string, b []string) (diff []string) { + mapB := make(map[string]bool) + for _, v := range b { + mapB[v] = true + } + for _, v := range a { + if _, ok := mapB[v]; !ok { + diff = append(diff, v) + } + } + return +} + +// Migrate table +func (engine *Engine) Migrate(value interface{}) error { + _, err := engine.Transaction(func(s *session.Session) (result interface{}, err error) { + if !s.Model(value).HasTable() { + log.Infof("table %s doesn't exist", s.RefTable().Name) + return nil, s.CreateTable() + } + table := s.RefTable() + rows, _ := s.Raw(fmt.Sprintf("SELECT * FROM %s LIMIT 1", table.Name)).QueryRows() + columns, _ := rows.Columns() + addCols := difference(table.FieldNames, columns) + delCols := difference(columns, table.FieldNames) + log.Infof("added cols %v, deleted cols %v", addCols, delCols) + + for _, col := range addCols { + f := table.GetField(col) + sqlStr := fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s %s;", table.Name, f.Name, f.Tag) + if _, err = s.Raw(sqlStr).Exec(); err != nil { + return + } + } + + if len(delCols) == 0 { + return + } + tmp := "tmp_" + table.Name + fieldStr := strings.Join(table.FieldNames, ", ") + s.Raw(fmt.Sprintf("CREATE TABLE %s AS SELECT %s from %s;", tmp, fieldStr, table.Name)) + s.Raw(fmt.Sprintf("DROP TABLE %s;", table.Name)) + s.Raw(fmt.Sprintf("ALTER TABLE %s RENAME TO %s;", tmp, table.Name)) + _, err = s.Exec() + return + }) + return err +} diff --git a/gee-orm/day7-migrate/geeorm_test.go b/gee-orm/day7-migrate/geeorm_test.go new file mode 100644 index 0000000..b9656c5 --- /dev/null +++ b/gee-orm/day7-migrate/geeorm_test.go @@ -0,0 +1,86 @@ +package geeorm + +import ( + "errors" + "geeorm/session" + "reflect" + "testing" + + _ "github.com/mattn/go-sqlite3" +) + +func OpenDB(t *testing.T) *Engine { + t.Helper() + engine, err := NewEngine("sqlite3", "gee.db") + if err != nil { + t.Fatal("failed to connect", err) + } + return engine +} + +func TestNewEngine(t *testing.T) { + engine := OpenDB(t) + defer engine.Close() +} + +type User struct { + Name string `geeorm:"PRIMARY KEY"` + Age int +} + +func transactionRollback(t *testing.T) { + engine := OpenDB(t) + defer engine.Close() + s := engine.NewSession() + _ = s.Model(&User{}).DropTable() + _, err := engine.Transaction(func(s *session.Session) (result interface{}, err error) { + _ = s.Model(&User{}).CreateTable() + _, err = s.Insert(&User{"Tom", 18}) + return nil, errors.New("Error") + }) + if err == nil || s.HasTable() { + t.Fatal("failed to rollback") + } +} + +func transactionCommit(t *testing.T) { + engine := OpenDB(t) + defer engine.Close() + s := engine.NewSession() + _ = s.Model(&User{}).DropTable() + _, err := engine.Transaction(func(s *session.Session) (result interface{}, err error) { + _ = s.Model(&User{}).CreateTable() + _, err = s.Insert(&User{"Tom", 18}) + return + }) + u := &User{} + _ = s.First(u) + if err != nil || u.Name != "Tom" { + t.Fatal("failed to commit") + } +} + +func TestEngine_Transaction(t *testing.T) { + t.Run("rollback", func(t *testing.T) { + transactionRollback(t) + }) + t.Run("commit", func(t *testing.T) { + transactionCommit(t) + }) +} + +func TestEngine_Migrate(t *testing.T) { + engine := OpenDB(t) + defer engine.Close() + s := engine.NewSession() + _, _ = s.Raw("DROP TABLE IF EXISTS User;").Exec() + _, _ = s.Raw("CREATE TABLE User(Name text, XXX integer);").Exec() + _, _ = s.Raw("INSERT INTO User(`Name`) values (?), (?)", "Tom", "Sam").Exec() + engine.Migrate(&User{}) + + rows, _ := s.Raw("SELECT * FROM User").QueryRows() + columns, _ := rows.Columns() + if !reflect.DeepEqual(columns, []string{"Name", "Age"}) { + t.Fatal("Failed to migrate table User, got columns", columns) + } +} diff --git a/gee-orm/day7-migrate/go.mod b/gee-orm/day7-migrate/go.mod new file mode 100644 index 0000000..043b1c6 --- /dev/null +++ b/gee-orm/day7-migrate/go.mod @@ -0,0 +1,5 @@ +module geeorm + +go 1.13 + +require github.com/mattn/go-sqlite3 v2.0.3+incompatible diff --git a/gee-orm/day7-migrate/log/log.go b/gee-orm/day7-migrate/log/log.go new file mode 100644 index 0000000..eacc0c6 --- /dev/null +++ b/gee-orm/day7-migrate/log/log.go @@ -0,0 +1,47 @@ +package log + +import ( + "io/ioutil" + "log" + "os" + "sync" +) + +var ( + errorLog = log.New(os.Stdout, "\033[31m[error]\033[0m ", log.LstdFlags|log.Lshortfile) + infoLog = log.New(os.Stdout, "\033[34m[info ]\033[0m ", log.LstdFlags|log.Lshortfile) + loggers = []*log.Logger{errorLog, infoLog} + mu sync.Mutex +) + +// log methods +var ( + Error = errorLog.Println + Errorf = errorLog.Printf + Info = infoLog.Println + Infof = infoLog.Printf +) + +// log levels +const ( + InfoLevel = iota + ErrorLevel + Disabled +) + +// SetLevel controls log level +func SetLevel(level int) { + mu.Lock() + defer mu.Unlock() + + for _, logger := range loggers { + logger.SetOutput(os.Stdout) + } + + if ErrorLevel < level { + errorLog.SetOutput(ioutil.Discard) + } + if InfoLevel < level { + infoLog.SetOutput(ioutil.Discard) + } +} diff --git a/gee-orm/day7-migrate/log/log_test.go b/gee-orm/day7-migrate/log/log_test.go new file mode 100644 index 0000000..8cd403c --- /dev/null +++ b/gee-orm/day7-migrate/log/log_test.go @@ -0,0 +1,17 @@ +package log + +import ( + "os" + "testing" +) + +func TestSetLevel(t *testing.T) { + SetLevel(ErrorLevel) + if infoLog.Writer() == os.Stdout || errorLog.Writer() != os.Stdout { + t.Fatal("failed to set log level") + } + SetLevel(Disabled) + if infoLog.Writer() == os.Stdout || errorLog.Writer() == os.Stdout { + t.Fatal("failed to set log level") + } +} \ No newline at end of file diff --git a/gee-orm/day7-migrate/schema/schema.go b/gee-orm/day7-migrate/schema/schema.go new file mode 100644 index 0000000..ff76283 --- /dev/null +++ b/gee-orm/day7-migrate/schema/schema.go @@ -0,0 +1,65 @@ +package schema + +import ( + "geeorm/dialect" + "go/ast" + "reflect" +) + +// Field represents a column of database +type Field struct { + Name string + Type string + Tag string +} + +// Schema represents a table of database +type Schema struct { + Model interface{} + Name string + Fields []*Field + FieldNames []string + fieldMap map[string]*Field +} + +// GetField returns field by name +func (schema *Schema) GetField(name string) *Field { + return schema.fieldMap[name] +} + +// Values return the values of dest's member variables +func (schema *Schema) RecordValues(dest interface{}) []interface{} { + destValue := reflect.Indirect(reflect.ValueOf(dest)) + var fieldValues []interface{} + for _, field := range schema.Fields { + fieldValues = append(fieldValues, destValue.FieldByName(field.Name).Interface()) + } + return fieldValues +} + +// Parse a struct to a Schema instance +func Parse(dest interface{}, d dialect.Dialect) *Schema { + modelType := reflect.Indirect(reflect.ValueOf(dest)).Type() + schema := &Schema{ + Model: dest, + Name: modelType.Name(), + fieldMap: make(map[string]*Field), + } + + for i := 0; i < modelType.NumField(); i++ { + p := modelType.Field(i) + if !p.Anonymous && ast.IsExported(p.Name) { + field := &Field{ + Name: p.Name, + Type: d.DataTypeOf(reflect.Indirect(reflect.New(p.Type))), + } + if v, ok := p.Tag.Lookup("geeorm"); ok { + field.Tag = v + } + schema.Fields = append(schema.Fields, field) + schema.FieldNames = append(schema.FieldNames, p.Name) + schema.fieldMap[p.Name] = field + } + } + return schema +} diff --git a/gee-orm/day7-migrate/schema/schema_test.go b/gee-orm/day7-migrate/schema/schema_test.go new file mode 100644 index 0000000..47ae9fc --- /dev/null +++ b/gee-orm/day7-migrate/schema/schema_test.go @@ -0,0 +1,35 @@ +package schema + +import ( + "geeorm/dialect" + "testing" +) + +type User struct { + Name string `geeorm:"PRIMARY KEY"` + Age int +} + +var TestDial, _ = dialect.GetDialect("sqlite3") + +func TestParse(t *testing.T) { + schema := Parse(&User{}, TestDial) + if schema.Name != "User" || len(schema.Fields) != 2 { + t.Fatal("failed to parse User struct") + } + if schema.GetField("Name").Tag != "PRIMARY KEY" { + t.Fatal("failed to parse primary key") + } +} + +func TestSchema_RecordValues(t *testing.T) { + schema := Parse(&User{}, TestDial) + values := schema.RecordValues(&User{"Tom", 18}) + + name := values[0].(string) + age := values[1].(int) + + if name != "Tom" || age != 18 { + t.Fatal("failed to get values") + } +} diff --git a/gee-orm/day7-migrate/session/hooks.go b/gee-orm/day7-migrate/session/hooks.go new file mode 100644 index 0000000..12462a4 --- /dev/null +++ b/gee-orm/day7-migrate/session/hooks.go @@ -0,0 +1,32 @@ +package session + +import "reflect" + +// Hooks constants +const ( + BeforeQuery = "BeforeQuery" + AfterQuery = "AfterQuery" + BeforeUpdate = "BeforeUpdate" + AfterUpdate = "AfterUpate" + BeforeDelete = "BeforeDelete" + AfterDelete = "AfterDelete" + BeforeInsert = "BeforeInsert" + AfterInsert = "AfterInsert" +) + +// CallMethod calls the registered hooks +func (s *Session) CallMethod(method string, value interface{}) error { + fm := reflect.ValueOf(s.RefTable().Model).MethodByName(method) + if value != nil { + fm = reflect.ValueOf(value).MethodByName(method) + } + param := []reflect.Value{reflect.ValueOf(s)} + if fm.IsValid() { + if v := fm.Call(param); len(v) > 0 { + if err, ok := v[0].Interface().(error); ok { + return err + } + } + } + return nil +} diff --git a/gee-orm/day7-migrate/session/hooks_test.go b/gee-orm/day7-migrate/session/hooks_test.go new file mode 100644 index 0000000..f896d01 --- /dev/null +++ b/gee-orm/day7-migrate/session/hooks_test.go @@ -0,0 +1,37 @@ +package session + +import ( + "geeorm/log" + "testing" +) + +type Account struct { + ID int `geeorm:"PRIMARY KEY"` + Password string +} + +func (account *Account) BeforeInsert(s *Session) error { + log.Info("before inert", account) + account.ID += 1000 + return nil +} + +func (account *Account) AfterQuery(s *Session) error { + log.Info("after query", account) + account.Password = "******" + return nil +} + +func TestSession_CallMethod(t *testing.T) { + s := NewSession().Model(&Account{}) + _ = s.DropTable() + _ = s.CreateTable() + _, _ = s.Insert(&Account{1, "123456"}, &Account{2, "qwerty"}) + + u := &Account{} + + err := s.First(u) + if err != nil || u.ID != 1001 || u.Password != "******" { + t.Fatal("Failed to call hooks after query, got", u) + } +} diff --git a/gee-orm/day7-migrate/session/raw.go b/gee-orm/day7-migrate/session/raw.go new file mode 100644 index 0000000..5bdd039 --- /dev/null +++ b/gee-orm/day7-migrate/session/raw.go @@ -0,0 +1,90 @@ +package session + +import ( + "database/sql" + "geeorm/clause" + "geeorm/dialect" + "geeorm/log" + "geeorm/schema" + "strings" +) + +// Session keep a pointer to sql.DB and provides all execution of all +// kind of database operations. +type Session struct { + db *sql.DB + dialect dialect.Dialect + tx *sql.Tx + refTable *schema.Schema + clause clause.Clause + sql strings.Builder + sqlVars []interface{} +} + +// New creates a instance of Session +func New(db *sql.DB, dialect dialect.Dialect) *Session { + return &Session{ + db: db, + dialect: dialect, + } +} + +// Clear initialize the state of a session +func (s *Session) Clear() { + s.sql.Reset() + s.sqlVars = nil + s.clause = clause.Clause{} +} + +// CommonDB is a minimal function set of db +type CommonDB interface { + Query(query string, args ...interface{}) (*sql.Rows, error) + QueryRow(query string, args ...interface{}) *sql.Row + Exec(query string, args ...interface{}) (sql.Result, error) +} + +var _ CommonDB = (*sql.DB)(nil) +var _ CommonDB = (*sql.Tx)(nil) + +// DB returns tx if a tx begins. otherwise return *sql.DB +func (s *Session) DB() CommonDB { + if s.tx != nil { + return s.tx + } + return s.db +} + +// Exec raw sql with sqlVars +func (s *Session) Exec() (result sql.Result, err error) { + defer s.Clear() + log.Info(s.sql.String(), s.sqlVars) + if result, err = s.DB().Exec(s.sql.String(), s.sqlVars...); err != nil { + log.Error(err) + } + return +} + +// QueryRow gets a record from db +func (s *Session) QueryRow() *sql.Row { + defer s.Clear() + log.Info(s.sql.String(), s.sqlVars) + return s.DB().QueryRow(s.sql.String(), s.sqlVars...) +} + +// QueryRows gets a list of records from db +func (s *Session) QueryRows() (rows *sql.Rows, err error) { + defer s.Clear() + log.Info(s.sql.String(), s.sqlVars) + if rows, err = s.DB().Query(s.sql.String(), s.sqlVars...); err != nil { + log.Error(err) + } + return +} + +// Raw appends sql and sqlVars +func (s *Session) Raw(sql string, values ...interface{}) *Session { + s.sql.WriteString(sql) + s.sql.WriteString(" ") + s.sqlVars = append(s.sqlVars, values...) + return s +} \ No newline at end of file diff --git a/gee-orm/day7-migrate/session/raw_test.go b/gee-orm/day7-migrate/session/raw_test.go new file mode 100644 index 0000000..d521173 --- /dev/null +++ b/gee-orm/day7-migrate/session/raw_test.go @@ -0,0 +1,48 @@ +package session + +import ( + "database/sql" + "os" + "testing" + + "geeorm/dialect" + + _ "github.com/mattn/go-sqlite3" +) + +var ( + TestDB *sql.DB + TestDial, _ = dialect.GetDialect("sqlite3") +) + +func TestMain(m *testing.M) { + TestDB, _ = sql.Open("sqlite3", "../gee.db") + code := m.Run() + _ = TestDB.Close() + os.Exit(code) +} + +func NewSession() *Session { + return &Session{db: TestDB, dialect: TestDial} +} + +func TestSession_Exec(t *testing.T) { + s := NewSession() + _, _ = s.Raw("DROP TABLE IF EXISTS User;").Exec() + _, _ = s.Raw("CREATE TABLE User(Name text);").Exec() + result, _ := s.Raw("INSERT INTO User(`Name`) values (?), (?)", "Tom", "Sam").Exec() + if count, err := result.RowsAffected(); err != nil || count != 2 { + t.Fatal("expect 2, but got", count) + } +} + +func TestSession_QueryRows(t *testing.T) { + s := NewSession() + _, _ = s.Raw("DROP TABLE IF EXISTS User;").Exec() + _, _ = s.Raw("CREATE TABLE User(Name text);").Exec() + row := s.Raw("SELECT count(*) FROM User").QueryRow() + var count int + if err := row.Scan(&count); err != nil || count != 0 { + t.Fatal("failed to query db", err) + } +} diff --git a/gee-orm/day7-migrate/session/record.go b/gee-orm/day7-migrate/session/record.go new file mode 100644 index 0000000..fdfca4d --- /dev/null +++ b/gee-orm/day7-migrate/session/record.go @@ -0,0 +1,136 @@ +package session + +import ( + "errors" + "geeorm/clause" + "reflect" +) + +// Insert one or more records in database +func (s *Session) Insert(values ...interface{}) (int64, error) { + recordValues := make([]interface{}, 0) + for _, value := range values { + s.CallMethod(BeforeInsert, value) + table := s.Model(value).RefTable() + s.clause.Set(clause.INSERT, table.Name, table.FieldNames) + recordValues = append(recordValues, table.RecordValues(value)) + } + + s.clause.Set(clause.VALUES, recordValues...) + sql, vars := s.clause.Build(clause.INSERT, clause.VALUES) + result, err := s.Raw(sql, vars...).Exec() + if err != nil { + return 0, err + } + s.CallMethod(AfterInsert, nil) + return result.RowsAffected() +} + +// Find gets all eligible records +func (s *Session) Find(values interface{}) error { + s.CallMethod(BeforeQuery, nil) + destSlice := reflect.Indirect(reflect.ValueOf(values)) + destType := destSlice.Type().Elem() + table := s.Model(reflect.New(destType).Elem().Interface()).RefTable() + + s.clause.Set(clause.SELECT, table.Name, table.FieldNames) + sql, vars := s.clause.Build(clause.SELECT, clause.WHERE, clause.ORDERBY, clause.LIMIT) + rows, err := s.Raw(sql, vars...).QueryRows() + if err != nil { + return err + } + + for rows.Next() { + dest := reflect.New(destType).Elem() + var values []interface{} + for _, name := range table.FieldNames { + values = append(values, dest.FieldByName(name).Addr().Interface()) + } + if err := rows.Scan(values...); err != nil { + return err + } + s.CallMethod(AfterQuery, dest.Addr().Interface()) + destSlice.Set(reflect.Append(destSlice, dest)) + } + return rows.Close() +} + +// First gets the 1st row +func (s *Session) First(value interface{}) error { + dest := reflect.Indirect(reflect.ValueOf(value)) + destSlice := reflect.New(reflect.SliceOf(dest.Type())).Elem() + if err := s.Limit(1).Find(destSlice.Addr().Interface()); err != nil { + return err + } + if destSlice.Len() == 0 { + return errors.New("NOT FOUND") + } + dest.Set(destSlice.Index(0)) + return nil +} + +// Limit adds limit condition to clause +func (s *Session) Limit(num int) *Session { + s.clause.Set(clause.LIMIT, num) + return s +} + +// Where adds limit condition to clause +func (s *Session) Where(desc string, args ...interface{}) *Session { + var vars []interface{} + s.clause.Set(clause.WHERE, append(append(vars, desc), args...)...) + return s +} + +// OrderBy adds order by condition to clause +func (s *Session) OrderBy(desc string) *Session { + s.clause.Set(clause.ORDERBY, desc) + return s +} + +// Update records with where clause +// support map[string]interface{} +// also support kv list: "Name", "Tom", "Age", 18, .... +func (s *Session) Update(kv ...interface{}) (int64, error) { + s.CallMethod(BeforeUpdate, nil) + m, ok := kv[0].(map[string]interface{}) + if !ok { + m = make(map[string]interface{}) + for i := 0; i < len(kv); i += 2 { + m[kv[i].(string)] = kv[i+1] + } + } + s.clause.Set(clause.UPDATE, s.RefTable().Name, m) + sql, vars := s.clause.Build(clause.UPDATE, clause.WHERE) + result, err := s.Raw(sql, vars...).Exec() + if err != nil { + return 0, err + } + s.CallMethod(AfterUpdate, nil) + return result.RowsAffected() +} + +// Delete records with where clause +func (s *Session) Delete() (int64, error) { + s.CallMethod(BeforeDelete, nil) + s.clause.Set(clause.DELETE, s.RefTable().Name) + sql, vars := s.clause.Build(clause.DELETE, clause.WHERE) + result, err := s.Raw(sql, vars...).Exec() + if err != nil { + return 0, err + } + s.CallMethod(AfterDelete, nil) + return result.RowsAffected() +} + +// Count records with where clause +func (s *Session) Count() (int64, error) { + s.clause.Set(clause.COUNT, s.RefTable().Name) + sql, vars := s.clause.Build(clause.COUNT, clause.WHERE) + row := s.Raw(sql, vars...).QueryRow() + var tmp int64 + if err := row.Scan(&tmp); err != nil { + return 0, err + } + return tmp, nil +} diff --git a/gee-orm/day7-migrate/session/record_test.go b/gee-orm/day7-migrate/session/record_test.go new file mode 100644 index 0000000..5d482a0 --- /dev/null +++ b/gee-orm/day7-migrate/session/record_test.go @@ -0,0 +1,97 @@ +package session + +import "testing" + +var ( + user1 = &User{"Tom", 18} + user2 = &User{"Sam", 25} + user3 = &User{"Jack", 25} +) + +func testRecordInit(t *testing.T) *Session { + t.Helper() + s := NewSession().Model(&User{}) + err1 := s.DropTable() + err2 := s.CreateTable() + _, err3 := s.Insert(user1, user2) + if err1 != nil || err2 != nil || err3 != nil { + t.Fatal("failed init test records") + } + return s +} + +func TestSession_Insert(t *testing.T) { + s := testRecordInit(t) + affected, err := s.Insert(user3) + if err != nil || affected != 1 { + t.Fatal("failed to create record") + } +} + +func TestSession_Find(t *testing.T) { + s := testRecordInit(t) + var users []User + if err := s.Find(&users); err != nil || len(users) != 2 { + t.Fatal("failed to query all") + } +} + +func TestSession_First(t *testing.T) { + s := testRecordInit(t) + u := &User{} + err := s.First(u) + if err != nil || u.Name != "Tom" || u.Age != 18 { + t.Fatal("failed to query first") + } +} + +func TestSession_Limit(t *testing.T) { + s := testRecordInit(t) + var users []User + err := s.Limit(1).Find(&users) + if err != nil || len(users) != 1 { + t.Fatal("failed to query with limit condition") + } +} + +func TestSession_Where(t *testing.T) { + s := testRecordInit(t) + var users []User + _, err1 := s.Insert(user3) + err2 := s.Where("Age = ?", 25).Find(&users) + + if err1 != nil || err2 != nil || len(users) != 2 { + t.Fatal("failed to query with where condition") + } +} + +func TestSession_OrderBy(t *testing.T) { + s := testRecordInit(t) + u := &User{} + err := s.OrderBy("Age DESC").First(u) + + if err != nil || u.Age != 25 { + t.Fatal("failed to query with order by condition") + } +} + +func TestSession_Update(t *testing.T) { + s := testRecordInit(t) + affected, _ := s.Where("Name = ?", "Tom").Update("Age", 30) + u := &User{} + _ = s.OrderBy("Age DESC").First(u) + + if affected != 1 || u.Age != 30 { + t.Fatal("failed to update") + } +} + +func TestSession_DeleteAndCount(t *testing.T) { + s := testRecordInit(t) + affected, _ := s.Where("Name = ?", "Tom").Delete() + count, _ := s.Count() + + if affected != 1 || count != 1 { + t.Fatal("failed to delete or count") + } +} diff --git a/gee-orm/day7-migrate/session/table.go b/gee-orm/day7-migrate/session/table.go new file mode 100644 index 0000000..58e7b0f --- /dev/null +++ b/gee-orm/day7-migrate/session/table.go @@ -0,0 +1,54 @@ +package session + +import ( + "fmt" + "geeorm/log" + "reflect" + "strings" + + "geeorm/schema" +) + +// Model assigns refTable +func (s *Session) Model(value interface{}) *Session { + // nil or different model, update refTable + if s.refTable == nil || reflect.TypeOf(value) != reflect.TypeOf(s.refTable.Model) { + s.refTable = schema.Parse(value, s.dialect) + } + return s +} + +// RefTable returns a Schema instance that contains all parsed fields +func (s *Session) RefTable() *schema.Schema { + if s.refTable == nil { + log.Error("Model is not set") + } + return s.refTable +} + +// CreateTable create a table in database with a model +func (s *Session) CreateTable() error { + table := s.RefTable() + var columns []string + for _, field := range table.Fields { + columns = append(columns, fmt.Sprintf("%s %s %s", field.Name, field.Type, field.Tag)) + } + desc := strings.Join(columns, ",") + _, err := s.Raw(fmt.Sprintf("CREATE TABLE %s (%s);", table.Name, desc)).Exec() + return err +} + +// DropTable drops a table with the name of model +func (s *Session) DropTable() error { + _, err := s.Raw(fmt.Sprintf("DROP TABLE IF EXISTS %s", s.RefTable().Name)).Exec() + return err +} + +// HasTable returns true of the table exists +func (s *Session) HasTable() bool { + sql, values := s.dialect.TableExistSQL(s.RefTable().Name) + row := s.Raw(sql, values...).QueryRow() + var tmp string + _ = row.Scan(&tmp) + return tmp == s.RefTable().Name +} diff --git a/gee-orm/day7-migrate/session/table_test.go b/gee-orm/day7-migrate/session/table_test.go new file mode 100644 index 0000000..3bb7554 --- /dev/null +++ b/gee-orm/day7-migrate/session/table_test.go @@ -0,0 +1,28 @@ +package session + +import ( + "testing" +) + +type User struct { + Name string `geeorm:"PRIMARY KEY"` + Age int +} + +func TestSession_CreateTable(t *testing.T) { + s := NewSession().Model(&User{}) + _ = s.DropTable() + _ = s.CreateTable() + if !s.HasTable() { + t.Fatal("Failed to create table User") + } +} + +func TestSession_Model(t *testing.T) { + s := NewSession().Model(&User{}) + table := s.RefTable() + s.Model(&Session{}) + if table.Name != "User" || s.RefTable().Name != "Session" { + t.Fatal("Failed to change model") + } +} diff --git a/gee-orm/day7-migrate/session/transaction.go b/gee-orm/day7-migrate/session/transaction.go new file mode 100644 index 0000000..3cdb451 --- /dev/null +++ b/gee-orm/day7-migrate/session/transaction.go @@ -0,0 +1,31 @@ +package session + +import "geeorm/log" + +// Begin a transaction +func (s *Session) Begin() (err error) { + log.Info("transaction begin") + if s.tx, err = s.db.Begin(); err != nil { + log.Error(err) + return + } + return +} + +// Commit a transaction +func (s *Session) Commit() (err error) { + log.Info("transaction commit") + if err = s.tx.Commit(); err != nil { + log.Error(err) + } + return +} + +// Rollback a transaction +func (s *Session) Rollback() (err error) { + log.Info("transaction rollback") + if err = s.tx.Rollback(); err != nil { + log.Error(err) + } + return +} From b7fdf41baa9be2b24ad6381b49301a2e7a3afa16 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Sun, 1 Mar 2020 00:41:03 +0800 Subject: [PATCH 052/122] add geeorm post url --- README.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 047f2a0..5a9a75a 100644 --- a/README.md +++ b/README.md @@ -38,17 +38,17 @@ ### 7天用Go从零实现ORM框架 GeeORM -[GeeORM] 是一个模仿 [gorm](https://github.com/jinzhu/gorm) 和 [xorm](https://github.com/go-xorm/xorm) 的 ORM 框架 +[GeeORM](https://geektutu.com/post/geeorm.html) 是一个模仿 [gorm](https://github.com/jinzhu/gorm) 和 [xorm](https://github.com/go-xorm/xorm) 的 ORM 框架 gorm 准备推出完全重写的 v2 版本(目前还在开发中),相对 gorm-v1 来说,xorm 的设计更容易理解,所以 geeorm 接口设计上主要参考了 xorm,一些细节实现上参考了 gorm。 -- 第一天:database/sql 基础 | [Code](gee-cache/day1-database-sql) -- 第二天:对象表结构映射 | [Code](gee-cache/day2-reflect-schema) -- 第三天:插入/查询记录 | [Code](gee-cache/day3-save-query) -- 第四天:链式操作与更新删除 | [Code](gee-cache/day4-chain-operation) -- 第五天:实现钩子(Hooks) | [Code](gee-cache/day5-hooks) -- 第六天:支持事务(Transaction) | [Code](gee-cache/day6-transaction) -- 第七天:数据库迁移(Migrate) | [Code](gee-cache/day7-migrate) +- 第一天:database/sql 基础 | [Code](gee-orm/day1-database-sql) +- 第二天:对象表结构映射 | [Code](gee-orm/day2-reflect-schema) +- 第三天:插入/查询记录 | [Code](gee-orm/day3-save-query) +- 第四天:链式操作与更新删除 | [Code](gee-orm/day4-chain-operation) +- 第五天:实现钩子(Hooks) | [Code](gee-orm/day5-hooks) +- 第六天:支持事务(Transaction) | [Code](gee-orm/day6-transaction) +- 第七天:数据库迁移(Migrate) | [Code](gee-orm/day7-migrate) ### WebAssembly 使用示例 @@ -90,17 +90,17 @@ What can I write in 7 days? A gin-like web framework? A distributed cache like g ## Object Relational Mapping - GeeORM -[GeeORM] is a [gorm](https://github.com/jinzhu/gorm)-like and [xorm](https://github.com/go-xorm/xorm)-like object relational mapping library +[GeeORM](https://geektutu.com/post/geeorm.html) is a [gorm](https://github.com/jinzhu/gorm)-like and [xorm](https://github.com/go-xorm/xorm)-like object relational mapping library Xorm's desgin is easier to understand than gorm-v1, so the main designs references xorm and some detailed implementions references gorm-v1. -- Day 1 - database/sql Basic | [Code](gee-cache/day1-database-sql) -- Day 2 - Object Schame Mapping | [Code](gee-cache/day2-reflect-schema) -- Day 3 - Insert and Query | [Code](gee-cache/day3-save-query) -- Day 4 - Chain, Delete and Update | [Code](gee-cache/day4-chain-operation) -- Day 5 - Support Hooks | [Code](gee-cache/day5-hooks) -- Day 6 - Support Transaction | [Code](gee-cache/day6-transaction) -- Day 7 - Migrate Database | [Code](gee-cache/day7-migrate) +- Day 1 - database/sql Basic | [Code](gee-orm/day1-database-sql) +- Day 2 - Object Schame Mapping | [Code](gee-orm/day2-reflect-schema) +- Day 3 - Insert and Query | [Code](gee-orm/day3-save-query) +- Day 4 - Chain, Delete and Update | [Code](gee-orm/day4-chain-operation) +- Day 5 - Support Hooks | [Code](gee-orm/day5-hooks) +- Day 6 - Support Transaction | [Code](gee-orm/day6-transaction) +- Day 7 - Migrate Database | [Code](gee-orm/day7-migrate) ## Golang WebAssembly Demo From 7645de81c718bed1e4f956951f19f3fa4b00b5c0 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Sun, 1 Mar 2020 02:41:53 +0800 Subject: [PATCH 053/122] add geeorm day0 intro --- gee-orm/doc/geeorm.md | 137 +++++++++++++++++++++++++++++++ gee-orm/doc/geeorm/geeorm.jpg | Bin 0 -> 27950 bytes gee-orm/doc/geeorm/geeorm_sm.jpg | Bin 0 -> 7426 bytes 3 files changed, 137 insertions(+) create mode 100644 gee-orm/doc/geeorm.md create mode 100644 gee-orm/doc/geeorm/geeorm.jpg create mode 100644 gee-orm/doc/geeorm/geeorm_sm.jpg diff --git a/gee-orm/doc/geeorm.md b/gee-orm/doc/geeorm.md new file mode 100644 index 0000000..78f4087 --- /dev/null +++ b/gee-orm/doc/geeorm.md @@ -0,0 +1,137 @@ +--- +title: 7天用Go从零实现ORM框架GeeORM +date: 2020-03-01 01:00:00 +description: 7天用 Go语言/golang 从零实现 ORM 框架 GeeORM 教程(7 days implement golang object relational mapping framework from scratch tutorial),动手写 ORM 框架,参照 gorm, xorm 的实现。功能包括对象和表结构的相互映射,表的创建删除(table),记录的增删查改,事务支持(transaction),数据库迁移(migrate),钩子(hooks)等。 +tags: +- Go +nav: 从零实现 +categories: +- ORM框架 - GeeORM +keywords: +- Go语言 +- 从零实现ORM框架 +- 动手写ORM框架 +- database/sql +- sqlite3 +image: post/geeorm/geeorm_sm.jpg +github: https://github.com/geektutu/7days-golang +--- + +![golang ORM framework](geeorm/geeorm.jpg) + +## 1 谈谈 ORM 框架 + +> 对象关系映射(Object Relational Mapping,简称ORM)是通过使用描述对象和数据库之间映射的元数据,将面向对象语言程序中的对象自动持久化到关系数据库中。 + +那对象和数据库是如何映射的呢? + +| 数据库 | 面向对象的编程语言 | +|:---:|:---:| +| 表(table) | 类(class/struct) | +| 记录(record, row) | 对象 (object) | +| 字段(field, column) | 对象属性(attribute) | + +举一个具体的例子,来理解 ORM。 + +```sql +CREATE TABLE `User` (`Name` text, `Age` integer); +INSERT INTO `User` (`Name`, `Age`) VALUES ("Tom", 18); +SELECT * FROM `User`; +``` + +第一条 SQL 语句,在数据库中创建了表 `User`,并且定义了 2 个字段 `Name` 和 `Age`;第二条 SQL 语句往表中添加了一条记录;最后一条语句返回表中的所有记录。 + +假如我们使用了 ORM 框架,可以这么写: + +```go +type User struct { + Name string + Age int +} + +orm.CreateTable(&User{}) +orm.Save(&User{"Tom", 18}) +var users []User +orm.Find(&users) +``` + +ORM 框架相当于对象和数据库中间的一个桥梁,借助 ORM 可以避免写繁琐的 SQL 语言,仅仅通过操作具体的对象,就能够完成对关系型数据库的操作。 + +那如何实现一个 ORM 框架呢? + +- `CreateTable` 方法需要从参数 `&User{}` 得到对应的结构体的名称 User 作为表名,成员变量 Name, Age 作为列名,同时还需要知道成员变量对应的类型。 +- `Save` 方法则需要知道每个成员变量的值。 +- `Find` 方法仅从传入的空切片 `&[]User`,得到对应的结构体名也就是表名 User,并从数据库中取到所有的记录,将其转换成 User 对象,添加到切片中。 + +如果这些方法只接受 User 类型的参数,那是很容易实现的。但是 ORM 框架是通用的,也就是说可以将任意合法的对象转换成数据库中的表和记录。例如: + +```go +type Account struct { + Username string + Password string +} + +orm.CreateTable(&Account{}) +``` + +这就面临了一个很重要的问题:如何根据任意类型的指针,得到其对应的结构体的信息。这涉及到了 Go 语言的反射机制(reflect),通过反射,可以获取到对象对应的结构体名称,成员变量、方法等信息,例如: + +```go +typ := reflect.Indirect(reflect.ValueOf(&Account{})).Type() +fmt.Println(typ.Name()) // Account + +for i := 0; i < typ.NumField(); i++ { + field := typ.Field(i) + fmt.Println(field.Name) // Username Password +} +``` + +- `reflect.ValueOf()` 获取指针对应的反射值。 +- `reflect.Indirect()` 获取指针指向的对象的反射值。 +- `(reflect.Type).Name()` 返回类名(字符串)。 +- `(reflect.Type).Field(i)` 获取第 i 个成员变量。 + +除了对象和表结构/记录的映射以外,设计 ORM 框架还需要关注什么问题呢? + +1)MySQL,PostgreSQL,SQLite 等数据库的 SQL 语句是有区别的,ORM 框架如何在开发者不感知的情况下适配多种数据库? + +2)如何对象的字段发生改变,数据库表结构能够自动更新,即是否支持数据库自动迁移(migrate)? + +3)数据库支持的功能很多,例如事务(transaction),ORM 框架能实现哪些? + +4)... + +## 2 关于 GeeORM + +数据库的特性非常多,简单的增删查改使用 ORM 替代 SQL 语句是没有问题的,但是也有很多特性难以用 ORM 替代,比如复杂的多表关联查询,ORM 也可能支持,但是基于性能的考虑,开发者自己写 SQL 语句很可能更高效。 + +因此,设计实现一个 ORM 框架,就需要给功能特性排优先级了。 + +Go 语言中使用比较广泛 ORM 框架是 [gorm](https://github.com/jinzhu/gorm) 和 [xorm](https://github.com/go-xorm/xorm)。除了基础的功能,比如表的操作,记录的增删查改,gorm 还实现了关联关系(一对一、一对多等),回调插件等;xorm 实现了读写分离(支持配置多个数据库),数据同步,导入导出等。 + +gorm 正在彻底重构 v1 版本,短期内看不到发布 v2 的可能。相比于 gorm-v1,xorm 在设计上更清晰。GeeORM 的设计主要参考了 xorm,一些细节上的实现参考了 gorm。GeeORM 的目的主要是了解 ORM 框架设计的原理,具体实现上鲁棒性做得不够,一些复杂的特性,例如 gorm 的关联关系,xorm 的读写分离没有实现。目前支持的特性有: + +- 表的创建、删除、迁移。 +- 记录的增删查改,查询条件的链式操作。 +- 单一主键的设置(primary key)。 +- 钩子(在创建/更新/删除/查找之前或之后) +- 事务(transaction)。 +- ... + +`GeeORM` 分7天实现,每天完成的部分都是可以独立运行和测试的,就像搭积木一样,一个个独立的特性组合在一起就是最终的 ORM 框架。每天的代码在 100 行左右,同时配有较为完备的单元测试用例。 + +## 3 目录 + +- 第一天:database/sql 基础 | [Code](https://github.com/geektutu/7days-golang/blob/master/gee-orm/day1-database-sql) +- 第二天:对象表结构映射 | [Code](https://github.com/geektutu/7days-golang/blob/master/gee-orm/day2-reflect-schema) +- 第三天:插入/查询记录 | [Code](https://github.com/geektutu/7days-golang/blob/master/gee-orm/day3-save-query) +- 第四天:链式操作与更新删除 | [Code](https://github.com/geektutu/7days-golang/blob/master/gee-orm/day4-chain-operation) +- 第五天:实现钩子(Hooks) | [Code](https://github.com/geektutu/7days-golang/blob/master/gee-orm/day5-hooks) +- 第六天:支持事务(Transaction) | [Code](https://github.com/geektutu/7days-golang/blob/master/gee-orm/day6-transaction) +- 第七天:数据库迁移(Migrate) | [Code](https://github.com/geektutu/7days-golang/blob/master/gee-orm/day7-migrate) + + +## 附 推荐阅读 + +- [Go 语言简明教程](https://geektutu.com/post/quick-golang.html) +- [Go Test 单元测试简明教程](https://geektutu.com/post/quick-go-test.html) \ No newline at end of file diff --git a/gee-orm/doc/geeorm/geeorm.jpg b/gee-orm/doc/geeorm/geeorm.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6b980ac12c30b1eb84bd75193a777b24c75a1008 GIT binary patch literal 27950 zcmce-b980Rw134}c4-ASfspu$KP?kWkW>G(h2 z0|t*4G^^Bc&are<<|n1b5cfaCVLk3~^S3r3K?q*?D`PJ6_noBwL(Y~oF)N^h@FK>z z;Jq&J{3s#thxph3&;Uh)Pq`}f&6}jLOKf^bK0VM-n{rsy|L;1G5&5ZynR&mCo(g|- z9=H;qo@XB${dWZr9W|V2)dQzV^T8?Hq^7YZV^8;k-yLp(|6Qqkd$}niNJ+_MO^82v zkAyXUPE}9ne>9fn=>ua%yZ&cL%)p|t4!rx#^MF0J^(i<2lGjLOKe09b0RY$?1fyU{ ztdiAv=Ds^$<91lB2EGH%Mqu{XPmJJ;3P)ez;PeUkYqM{aM*_^t!yW)1k$?(Pwn?Hr z#UHRZcg?`R+Nvx0WA>bArl^iTKG;sC3s}(ZVJkY+J>oOUp+IpXj%17z zv7MvfcN3N?fbeczbPd`u%^3iJn*eS)k9*-+!8Cm#g^7;0fAZw3@LB=3&0T6EEAz(cmLGh&HhN9O_#;J%SuE)uk zoqS_-M=}Dwwn@AsM^7qzoC~^SG^tQc2V20>&@^Vo2EMwR~kEBzE3H7j((uXZMJ4;ax6Uln0?H|^0jT) zEr;m6^6kFv6R2?puO2F!$Co4hvlRC6F}<~xdx^aZ4Q8MC|t1(HRqCZ50qv1 zMOz!q|JL`oAar=2!E&@!M3Vjeo5lOD!d5efi*43Vz>KnX#C*l?$ENYdw>4ndRGAkc z$uH(nTc&r*wp11q33_hc*X#i5*E(kaO@GMX?Ofks%G9K7U*Z5u966%r?$KI+=(Fxs zTHwaw0!K#pm!gmOA1a&Qr`Snv4^BO8fkk5LWmHkpp86xbg9lz!USGX;;X7~6c7amh zW!OaHvU?puXV~C6;e{`KTyRBAKeW9qcz*&6m#5Nv-`1v69tZQuvtOi&CQ&%$rtv#D zv}WyrWeROGe%9NL3`{wbc@Y!e$?&{;qtUHw~(a(Y`1006b&-UR(KfsUPTL~{` ztDB{sYOC9<84fAb=eFq)P#9N_Lzf38M|wSFSMN0zwy?_!%nsI*`!{aTV4cSR`VYx= zXJcA;-924*2RCC0Y~8Pi2CkNG7*JC$E+tp#vHp3H2mXG`XY&XX1{&cg*cFp;6wv!N;3677?z#kAi zdR>2`KUl(_3SG8bAYyY{C6e^an&W-)1LUTdrmIM(^GJ@!l#TXhNR`=~2}~%RGXwAixU_NrAp1H#0K#=M`!n^z!fx4@ zIoEzMTjeGQ5Wm6eeCOa)$PCGnuX-~HFA#l-bvN-EIBZ%;n19}pEEjqrt`F^|f2pb^ zq1mX+7DLR+oS%s^6e`~y9@}|aOR=;um|!P5C))Z$p~58q07y1TX_T$K)BBRv)-tOj zk;%ej*Q*`=og{1pWoqkF<0e5e9wWr6l`l;p!z8PjHi^5rFG3m#x=GmXT?=v@aNEr{)cEA#|ApOJwSeeK^_32>VvQ}5GKn# z|60Ostjc7pic&)6_Y1kX-qX&G%3E#`Tjon@7l-1@jT}*5ba#Q$#Wgf)sZ8OIm6tPf z;Ssi7)-^G{wtT+4n5C0AVtZtZdILE$zZ z#%Mco?q+?D5$c}Q9farF0}JKla+t*R|0m{wixoOowzNHi3=#ssfSSN3t|I_=-;D=y zD4&7d5^=phXXd;e@LvEHBM4T(cS;EWg9ovY0=DQsK(i#g$O-W1a;lWm{|^@!*dp94 zTF&Qi2ez4U{QgF|{{XI%CYNo5)8z~s^nL0|hyQ;hKn!1=Cab2niT}2QT=0Jg0Ohu{ zpMdO&t^dh|iPeZV|7+y`4!qlI*pNwW%lFScD8Hgy;?eiQ86#bN7{ZNIjx^9YH`!m6}kp z_tTuKJpMdy`eN0bJOA0UAKIOk`-;b=(BWbPf9&w>`$rBy4`+9icYfu=-p(DtieJF8 zajtTm3@oA`CDy zd}d999eP+jd2Nd5&&VF~<)J_Z^NKPw@Ed=vsl^|_*MoOS1F)~>;5f>={;3S;^f;J| z&A)X7aI@{`QvbsSfUM%`|1Zqk|6k<)atHfA`x?;IK>oL{f&F!%`u|J*|IG%WaxHOn zDF2s4nYn#h{CeB>FQw&@T8n;J#P}~&CqO?R6s?&`1FEdRM~ITCoo$3goZ$`tT`A?5 z$FZXsg7CHbo%Rm!^b`6eH}r1nBfxh#eY3*HyhPrq02B+t8 zqvCb%xk-1teB`#*ekpeV)T(4R+P^8q35?q6r@4AeEEBcLdl4H@J6}B({RBk4Y?Av% zv7{R*G$N}gj2pXF+=FR!cp0ZsBwIcCH9 zMv-zXqq^*wA9DuVUK<2Ox|3gzcr!a(^#O3T05qex2<=0ji2<2qBR{&zu@$OX?Lq(m z!vf^nBcn3_$~`fZ`;rF{0H))ApxF-guPwk@{UIs+|H0Wb0fu4!V4&xOkof<~DXIZo zKP7|#fG;AHG0p&uau8VkzY=(7@CN|p_|!jI5NTapA+7(Q0JwEfEW>|Mbjzb)R{wu- zF*)k3e=+cqv#x)!|Cc)f&@VU<0U+R@AfTYo;1J;dtjEEDF$e+xi3Ej=h=PhnNW{p* zCWtO1uaCjZ!ma>KOu|ZL5GVS-7U>|+fKR~bB~`}9lbkP_KD3P*vYz67Awd{o@ z&(wZ)334vYn&{ecVoC+a^{yBAaJ1~r2tVg=!=O~9*z;0e9RmSOH=tQW>Dv&MA1W+mYGORxIpyG+che>NI zNK~VIWIJ2r?I3frH7M4QFDNVwyWBF6~yjiBDZ-mCHobW{av3Lr77@T+Wk*}0xGoB(t49=*|)y+Bf>0D zgCAk^$KP8kej9SKemUH5OVzPrb z#v@nv=+uqIf4+oGr`R>{H<;=39sBKoTFMn1W}yhJ;~;MQQ~Q~b_TzHe=~qxmKdj$A z$Z}I?3S26u@QXSo+HiRKAfnJ9P-s6RE~?DO9NjBj28$LfTwk|HBn*_qCvgKg96 zRu!0pH=C{!J^^;Wb5yHuUpgJ7FHHCwTH}05r*%>CPK?`4;?qR)P~zqF12I0Dx;BsZ zn&;s|=u^)aI_3bxdeOd;e^lX96~Bt$Rk}r2C|45IB0zR>U)45n3d_ZkMn!N8y{@KK z#5$j9sH>z}V_L3rXRtSzmYEY_#J?LPe$7fOZmA%CDik?so?(ryU;B_smAeOK|nm3IvHh-S7 z>U!PU2RU=9UMNdJ9swsYl-e}-PJ{mwV801iL?^<6gL}VCv7Kx@Rd&&W%wu!Tc#av} z)+3O2W|`*;wxp(}Iib#ENGn_E>v0_x(b=MV*9+l%~~ z>Fb^7&WA_qVz~&krDZaIkP~R@pkvM5=&yls;?8l>Je>)1J(0xACb|SZD?RpMPeUZg zKeMJa&u@))5|87!-fp)%>@PF4E;qXvqAU6|b@MVkmX^h*G#r*7maHQ%s=c!n zg}Thy1^MM2CdGlUh}W?6XrvC1$_aP5O2)Ix#{^K0Vlx}^Hu6m8@zGws#8uGE`}-5x zd9v16JI}d>SC`^Je8jMKVNAG-#55{Hb1GUWy4n}MYHDr6XiW7{*NN8lfz$ZHqU8y62=Cv{v|q{IwX1?hBe(KnM&jeM-?R9@ZK}|KNkFRX4(H; zmDw_>vTVT;%un$V`3Mr1(9?vq!1jJ-GORQrB1F~t&wL9;T4DAaHeJ3i(S#Ciaad_< ztSU8CR>Qnz1zJ7?R?V5Fv0Yut`HwZHv;nS57SG?|AL3?u=XrA-X0~FtMhP9swNhEEFBVM*@9+$dY;rqiJ@G~(eBk|F*zE=`f+C0VDdI&X85ba7Hz!P|2OvAy4ym_-n;nL3P^w@H3}X`><25DXZd zgZHt-hb#_(F70G{aBC20w^h!qsPH6yTjZF`728JZZ&Rq{Ouo7H8Ku3Iy*Ms2x1r4n zhrq*}+j&+v0D9ix^H662x^=@wRWmv1l(lyqG{ke~(rYltpMAG-dWhhePk^MhT=Kv6 z+kuHMs&&p|3RP6WT%T1WFey_d&lWNfo0GJUxq2*WXK>*zD{6H6(XLq@BTk4uSaw5P zU()be2Klf$YgPZ#;bEaY%ZSv}B7wiUjLy0z9G7WBNVESt_fG@K^Fisnc`UJ7Wwu^y z2FjQ0uGa{)!OOP~^i!;r`y4GF-A;G3d%;Gcx9_<(QA8=Q8xnC6yOeoj7smJQT+Woh zS##{|NOlq2ueTSdAQ7T*F^;K`KE(37Z?%D3Mi$WT;eIDYp}D9XZ+GA7reP+9X{gj6 zK%@QSb3OqHjm}~;D!75NGZ2BMuQ(P|{9TrX)dKl?iyXb)FQgvNCiHcpGU6!XbteJe zKofQ6m6cUb-RK%BjU%@^-jP~992t(q()Zmmz(uQ>^I@C67#oX_!e-J?V3W@cby^ECsI0=x9ZYSL5l|(9L~^= z%y1~W^n8##t=HDNerU)$BC}{-;yhC1G_c%Cm%P|7JNNr|x0&#kkU8U;(S%)i?6OnN z>jw&d0(jvlQg|6;)ws0995V`S8YcLYjm;=K?xriT7_%yW4Qxzccaf7zkb2khiyfhj zR8e;?|Bd*c0Ite+m^x08NBVK;230vLN}Eju{T^g4l4N!X{C~S=K$$sJ$QZq2sqmZeuSc~$AG5RPPG(-@?ED$$$}nY>wyvwz zHu>nsqigM(2(EX3?u(bfpgiW`D!=C~lQpck{cM;6pFfjOU$6C+tyF5?UR;MOUlFqt zQup?zXK39<=J^tQ%^P2UZ6-phBmoHnIUineYVrLOVHK4%}?((iSt=bZE*xIUaO(XS=(e0>~8SV=Dp8t-V0z%6tBrQ30Rle0; zXD%l_#!ph0A|gRb3xwG-#42hOU$3nJ5q)5QUxk_n+ZLMc27a*C6h`v5&gY{#dm}Qo zZ!#enRzw|eY27@$1#w?e+8V~Zd?p6^X*!d+pLKfvwD-nr^vMd)HpUPqRfvUs$8Fh{ zm|!);%|3@_4N)DY!C$Pwoqwj+5W-TTjc-=OE=}F%bI3)$U~Yu&Pa+_uW@u-g`kfna zJz@jXa^YT8!Oy0wNBC2hDF=_(DbHPmRL~m;buM_CU@vZ(AgPt@89P8BfcJ$}~x3CJBwU5{^Zssjth-gw)^ zopZFWfXEyv9bgOZm2}LPoIBfv3@3?QACSQ2D~-`-Dr!Fpn-i8O5YP2muzk*V+CcM+ z`_MsU>Y1~4FOHE#eQ9(puOYRJ$WV)``xbWAWM@AZ@ z+q2%@lWQp4R><;eNjE->30r4H=mUwEMT&LJji`%H%cdga~ah-Ivdnhhj`a$e<)>6G?&;tE~Acz z8Fhz!`^%Wea#VUvSnBcDtoW?B7TnAyNvGA!l*B6_3k%4MYr$zvpHMF?!|c0>w>M9q z)SSCk7LjFkh_7vDBt6UIrNvo?A@`sPqCgZ)EO7HR}8|7INUoXcO!AX;4PDoqp-t|3tk&2Bz5AANB>w)5|^-150=Iod}=L}Z;2wU6!cmdhm=Kv#JhGkyX(lX{3m+#CfX26sE8BGKVD7N~uL zC$Z+pY~cFI$NJ8dMeLTS8;VO0F;N84C~A!l2&{^8UsQiNjvY~XFY121dR0O~;&q>x z`EJUg0jFWlAe5lIcV4ZrG^}@1(TQ zs(b`z&sZ0c6wY*qa|iUvVLk#!p3W`@^kx?KAHHm5GfZ=gJGUt71m@<>X+=z1Lq2WN zHh=l0<(nrPcHkZF$2yU}oxDp23yIsRbF=LAcvRn+d^hf%Cu)b5>4t4^rV;zUJa;ja*I2kwIGJ`rVQWkjNH zciEy>Vq7IX4DB9zmO@5;nx$xd4)?Z!@?XL1D-oMz5goT)-B*wCOhC$BV@BG%2w|Wz zK_$d0_>=vba6>rCcUT^2MMN~_rA7{YTIhsN*>HY6QmE%^X7=hfg>V@G8(_9lN!)1L zu)wsjmLP_razg~x!X?Lzd%Hc!4;$l7c5!n(5-p-)I$71uCD->8>nS-%09oFMottA6 zSNh4dmCZ%L8Z!S(uR`KolC+CN5ih&Z}+_jqB7i(tY0Z> zfMeigbzWr#`6rKja%S9U&(mX#iV~M}MloT81pfG>jwA8mE}R?5YOzTgr_tkk(!nqs z>u#;)?Jg(hh#l4Lh|^$jfFvPl!U|l$A~-kdK1=Wf#;!|g68t>2rTO6Rmn7Csn*n*I z1s#(bq7Gc+)%|iO$-Xx~oA6GpN_lk|6GbM+^#!^D>+W3V6W+!d!^%q1wndj@9Z{vl zCl`$UN~^4V#WviGhT*hv@y5o)7)kS=+Sf@ghQDh5bSO_#c4#2V2e$~4>SbwYglaO! zNqAk~h!>M^q#q@rYvNUFsXZe+{0SN?A5aDf>k_dPc*aYU(riU`+WwP;YmLg>_O8Hh zDO%QZg}l%xM%Ce45{FkA$xGTrJ3(kpq0@NKQ%JanPM2qs%-+FqrscI#;Z-B%tkBHV zyJmn5@mwiI>md3}TDzGx*pfqjUPJvz<|dC@X(b!zE*_lt-O)NnC0SXbL%Z~9bI^_M zv7^WXwngKM#jSMBdU<~}t*aGBRWq{M=(bI33R^hi*D{?XTPDTHOpy=I{Xr(Y3K}Qf zV*cF;Yk`1DmMgXic`J0ZWJ~Es7$eIu`AHgA8xI)M_NVwLqIvB7{q3e}Sd9`R`U&;# znse4Jzj;gUT;lI0<39l?HdRu1rduLM+sFcXHNl&flipF%$DTwug=dED&y2gdc*Uhf z{FFf zkSP^M9cEND3tp@nclNRCn~{aAQCU16tea3RBKNKP=^5#$akLxA```9ZP7TcFh#QhH*zQ^hHqe{;z3=DoF(ba;X_ zg9^v%eRC)8rVYU+oD=K2wViNGU&p}1!@+7rB&A~peW>LF8kyHa_W3FARN^OqiJvRV z)3w#H92H0O8GD);MMQ@-q-VL&k=V6S>V%yml3oXY<)n64{hM==S;j}U-*okOhSm-~ zLh0qI1L|4G6k=ltJ6FW~589E@_yP7U~3YH0hdU0b_$|6Dfe4pij>i5!b zd_F8%x2*q-K&><%XKzZCddjao$d}LmSt@ z)!<(mxi=|%GgmO57w4qsmad)!;obR~{^Tc@E&^e~10hZc0@*XdB=tcs=PNT+mY2Xq z(*I0ve$?Mag&Q9`V)E7K&G=4_);kqDeTT$XY63i*?dfR$xe}vpCPX@DqBXfS9tpL# z))iI43vE|3=y(q4Ta@o|Wv}v)IEF$)LQe_r*(%JfE3@v;CgN4-gns95aEjief_gct zi(?&Mlhejw_Ygf6l$7A5&{Im&VUBXMEW7WVuot8hbjj?^IkTXwap*+yXD}#VK4#|K zS2-C2-3>&rT?et^(d6YxXEtF~2b}o91mNl~r;Xc2)?l_%)T^sx_jQ_Gk5B>4HugnC8i?hmoNfo$0iU zfqB;ow#6zjxz?PPa#sTKvuXS>$q7+SL#BT0RmkAawD~FaDP*}mxir)8Pk@z!-KvZ8 zVSgnwtmnBC4JEa|S@e%l0K{mTGQZ2ygj>J^ky>AEl1Fu_7Yi7M=3=@cPc~6EtbIb1 z@b0Q*qxsYEL+L1aLs_~`AcUl>^BbA|L2}8hbdIvl^k_y=EA+>Q^xLiT;y%$v4SJA$ zRwH_p@Vj7@wbkC(t4;E-?o_BkH>R1E8djAyyZOMz@m_Fcx?yL+Rs0r?MzM>I5pkkN zs}1PQ0g zN^-O_38u+xKXWClWq(qR+y8keVN0B<-HqYQvv%0~9ZvH$tV;=N*#3&&hg#vTlJ-z5 zNI0P!uudSW6k1o6mOO#LsjeS_{?PAIYn``*2=;B(ww%;?KR`~YqL(Q)^}wddJc=DU zbe%AS#8e%O+IWFI)n@bP_MBC=5w0>K?V;yKxS^xneP%|*!hp-^602q=SZT0x-MPT( zg2!(kLvP@cAW+;Si-c{oUsNp}WapbS%HIx;@%)P&uvgkj6>sh`4Ch_QN|!VQa)=$@z()r zLKfA?g-h08g6vbgh-Ya(!f7VT3)lBT1J1!}0ShGkPvN)HQ~SorT193nX92aZ#Vh-~ z(V}?@I6K;~gc{pEWcmT>Vx;bS7nX(U7VIo5)Pv}?Bl!}LfI;brVe-_tfhq@(ar=AY zfg_Rnuy^Ff4(e?Ze7<6@T{=VKmpw|tQeqPmaIg)=!gI%sh;7q~!ty5KCnwm^Zw0yr z5q#3fv*TpWX{?VmIUTje=GS~WU-5UgeSW^j%1{TzK8{yKYCjASvhX`Rd2nkxVBV~1 zu;d40zL(qh_ihK=CvD_=;o#0Ta-u6YEQUAUN`z|Vx(G%rmPd)XmX$+V&6m2!b~sNa zSjm_wXx9dpt*P^xD5^gU&3QSQ*Edy$lfmVksZELy+UT}nrjr`t5ceDvq~&;_>B;do z_Uh3UKu8;is(t4oQ;MgOBg40;o)%w9LGP_4SdpLi&5ti;@0Ryay&&P+)x(J_cAid+9};_ zqQm3!v3HsFAnr&ZYt*=Qe=J>I>@_#aPmT=>5lZ(a74ff;stn|`M_$ULwN@5$L0X#t zb7W1hF$u>+#UVV4@1^rS2rI1sbMSOr8EG$DFdP!O$9&lN!x5Al2`=M&C9V!HzF8}t zU;~#Fg2!5U6PhlEy3n#`^%CV0EGM49;j0ZV!p`gmeUXA8-w~&8a+;lfXe; zmyn1B8=ctB#c(5zTbH*U!Qh-Z0{ljoa*Q8QoG5&(w^ls)q|nUyp3txXA$A}d-A#Nm z{ZukSZ7g5TOr0C`Y^=r`kaCn3cBETV6A3GUYCbm%YPD9WC+vigp(?g|A%rw>;n+_w z9LBowK^T2z$u|2Hc4GeobNJT0y5-|BWG?Z?9n!TRno@8Cx$wrqF5yHSG{nNu;2{><4B7oF}wUI)}9=fl$zFGYk`8>bvT%JMcZ=fkhV^P)-4 zYvl({X}x4(a)+yCX&`$7WiYn(+3Qv-py3sMU+p2?bP*3#8rSA);Yaj=_L-NSj3HUE zZ+oE&DvHHH%md7l+r{tk>Z-}jE2VR_ovP)k(XFbc!BsmG5upyNMa+tGUH*P9m2Gyu zy7`&f+A^`@8zsqCD?70J6&z#|(gD$-r3IFUDgvyLXsaYkPfz>@vV!-6Qs)^+5shs8 zo-UZ_pNYH8%a0QH^ev^w$}fHNcY5ojM@U4oiE1`ENhOHuF%gYJG=fmP3}M1kUy(=aP-I$AcUOuP-|HD0sm^L2ebmSw$Z9(b z0zKv9I?X&MLc-*@!Rg$_wu$Pt^l|Rq)8jg`*!k5?#$3euorv4TC`&+;x^-TzZ2F$N z@QQF3o`r|-{kUyHq&<^AhfGtAQM3!ZmYFi+@l-IjWqs*f)vv>r{EcBQ$CbpnJvUu> zgI%)4GjVR^RIn57)LBJdX*Ng)>N3?Z!h9XK&iPa-NnB;o8O}_HXfNOsuwi@1ZsSMvQ5!U*l?xoeZwwjeVL(UXdHrHi6}rlDmB9%1e1aEWj|gU%zzh2c=nN+l}~TYlFU=I+B%pSpqkHT;z(VZW9(!@}bX7 znt64Kdhk+ia0^dh@O=0djj;@_NkoPbn0H=VB%L?;;EplzO)pM793X{%mj_GSa?!pv$Zq%l*H2@Ook0!L3p^v zbuI4c$~NK}di_N+T2O>@Q#$d+22`-2t~T-dH4d{YoeO?2aub(vc-zyOozi0+20lL0 zea6ZM?zjZ`$TC@&wveEMd%y(aifCaD%$V&h(lNAEj(uE4HM9y(7 zv12nVObd+UC3#K^raYiH@+W{diq@%@ILXAp2Z7qrm7L$u9y7u3+xb15U~M>*b47dDOHDrAtY(Sj1Y$z zMVsxnae7IAZgf{i6#spfYsj2IA-(`J&4opVd2WqGJyQ;RB_dsLFH#R1&rsF*vvVss z1_EX_2H_kP8)6JJvHGq;GX(k#;pz2~|GF?f&hn{s2fTnvvu+9Ol)Z6` zY@Em1i>g_%8{}+>JoIt-`*;)=64B^(QeHT)@GdhhrmwvpmhYPH@aR_qP*_<_S9#7M zn2hcH=dQ1_5$eBA=pUVi)jU`e&N3taXlpd-QQjGm=OBs7tX+CwzFT0P47NSv%Ghx> z^u`cEzh%5@bcxI_QlGBaMNui z8nyyDRog^5f6f(LmB1!2HKaG2v!)QWP6TWT=bwkL&t#hU}^2Kw>hBZM41jFA;Nm7@Y|MQLq%AWy6qH#lQ|C7IWP z9Jxd2fWa-MX|?`$IE7P$;8afWMCEYyDY>uw@;3 z5EnG!MzkFT0?-zb+W4;6wxj0g!k$@4Kan4L5=R-f^ZnZQ(J$6gnFojCQL6OT>>%UY z%QYx?Q!g&H)l+&N>RseHvFm8HSG9Rx5vPOH1Nd=Ned4*sLO-OiqjS^9Pp&!Lrni3D zsX~R4OuO{haDGq-5Wex&LMeng2BojoQjHomMpZ;*hV4?s{Z0yiEpmYRhTu>ru*IV& z7jNhssMLGO>SMb&n9%IJjeTK(on7wyou38S`*}ZK(U@?nI@nnSCw-`Y5M0oKyJjyh zydpZzNjC$zFo8fky1rlP~A40M)fQqz~oMAf9zqr04<4TO{v+ z3;`qxfr7UX!Rl@lp6nhD_q0WS ztTZjb*GRdUZ@cnBn?`-AL)xmi7lYeclNI^)g{wuPF&)VIe2S_lC=Ct?$VkLkC>xf8 z-fEE6qNCl&4C}Oozt#1_G)NTm1zIj|J^@$jmBXO}AT6e#BlE@2x4F@4a`t&uAl+g_ z;ud0BG`*F@?cRH;%q2Z`v5dR~j6Yp9#d!`olyuJHscB;UpvA3gyaDPDe;N<*P( zJP1Q`*;tWMrR7UlejB?X?RuXVmzPn=?X^kNOR6Ti6A`1eiS=b&=zVd7?hV1A#K)wkXY@iodkj6*y)U+}k z$Pf=ar?ZyWpOw?aJt}94VzaEKt$EPd*p#iUd#StamusEFCH&bTHk6`vXX7`gh?2l@ zAPT;Jj(6}8izeXLix%_xpZ#_|_ z`1a=|9$m>=n_RGHF;}LHWXDnQ{I9elivXwrJfvth%Vd5{qeBV@~z0Iq)}1&kopkLrwGx~Fs{5Jm-jYQ6!kU+#p@GCmdc zS|tH$sxWTtoHhm5S#@P1W4El4W^De$PkRdL!4ZsN!{3}*Jx|agFyHzVaxSlVJu#sd zFebEI!qzNouy>3xjrs=PgS=;u4KoZJGC-n_*3}`E#t1U^tRe{}nrZM;(d3A2l*BG! zQu((3J_v)FBi7rVU^)b?gW_<__oKZJ@=Y)4Dwvoy#^Cc6YLr@2_O()l@c(dAZ^T-} zTnanps`8_6EEKN}GPN_AZ;tbdIpi%Fnc>n+wAdYL6S+Rb(VYINY7?v&$?<|pEYiSr z^b#rl1RS7xrXWo}6rU#fq;iw1PdQNB99T#W0K|V@LUTQ)f#+M3#oc@Q-A`^XdFXe#N_C$K$-Y38)Y9OVM203`e9%45NN5gBl1YZo)bnHw$@mJ0XWmzFn z-EO0kM}h+x)fna+0bStF74@NGcCcu$`o_9-WN0tR=!tLCO6&_+%-<6IA_qXYEXI~n z4-+`C8iuhmN4T+6lyy?0{c*>!u5UL+(eas=qrDOOL_%bYlU=99m^mT_$WctcWQc|K zZt|vD65%rDh)>zV@LTTielwxvD1%c8ehN~|zUKRtMipP>8ZDC)Aiy)y%=C5|QGNU_ z|8_D50iqjwU$F;)-w`6OdFFB`+p%I&N3=WTC%T6KCN}An4Wez4JQ5UEq-S2Js5(aL z;cPeh^QG*p=(+6v+K=<5W0c^)%5Na=?KHKjL2Npz*|M-?qP# zyO&YwlqRYeDtuFnwW_>!r9YN_iyIiPojw|l81^VNaPMnAFgDLvSESpG{i2;oa;{H} z@p?s28L+cz<$BT`<=MU0rR9w4-F< zponKXA0SP+>^?NX*z?P*mp`UYRYE_FQ4DdUClW5Qk=`D-O$bThA(!#ZI4sPe1`cU9 zz|-C+iA|TXS5#>z=oUCb-DtP6cklx%lBPUZXh92!U6#+ysIxgq!0A5+g#mlzJCvtZd>=P?&4wnM;TYZoW!!VnS5Ey=}dxOz$+h2V?#-!fTde+^P&n>9m4+v)QVhc<;MkIs7xt76j_%% zA-6-mm396Tm=1@S80$USkwrrakf5(AN`F3wQy_ZK%!iSDV}t677(tbzM;8@n%&%|~ zU_luSi@Z}WRv)B3wlc-(H@t2YUqjUvP8PYR3n)M`Xv_ko6{gG`!Y46AkJqI#GIk-} zD*K9Pg}{SXmR6rL!A}|J15N_M6JvC|zostz?u8(9EA;(@)f30{-IId`(Z{9p zRbe^~n^guUX4f~kEgp#)o`U}R6s@?hzIv_SuL5g}H|QXe_}YxeKhMpTb}-e*Nh$|V z+AJHLG_>bv z`6QM_n`Tf`Dycfc=~2BZLg6j_?a2dRklaxXwZd@gfRSx%$TE@_gZFl=Wd%uu6cU%J z?BXqMX|0Nc0ZZ}=e=&xNxhW%Kp##y-wOI^4#p%Z06mmONEb}&Z*1#A^yw@FkgXL0z z%&a%hs^g&sGNhB8)u1zsw;xV)w=2dlqYQ4~x4wJr3E$7li?kn!$Nbqn88zGMYgAvE zxIv`9h}4j{z+oc#II}68Ww?~Bl@#^2gz_j+=fO$zdCUQe_ktJ8 zG*`FS;gBZ^sKMKbPIwR9_k}EwavGxw;+m>n%MJHRi&TGk8ZU^KZu? zvXi?u8{N}K9Q!(-fJ~oKxf?`&E%=lC$mI%qG54yO6k#WL#Xd(y{+e5TM3FG!t)Q-j z-tO|2I>?jmnga$gS*_5z!{AvIu)#|>n(ZGc;9Z7UP(5Mald3|}O|`y)V8t*DTiK1q zMzESDg2mQhyusIFsS25j-o_6^6}1thNe>1&;G?bJtsY}~mfqtKsNm*3%^BFWKV@UD zPyIgu(I_t0J4>d2=~0u&jcPW(;WY!>ovbNC5OiJQj)$OSYg+!7jYP-13)F0X(rOn2 znHCf?HtXqM4Q}dT=OT1>5ZKHWeEd)VI}rbm1V=IDtKF<*bV2ppe1BBWl?_6`qhNQ z84!k2tEeN5VcrRdw2`p2m~XdnsLQv;5Re7UVP*DK{;hB9=ogWgI&%eU2`{pe(w8zn zGk%XjzbeAZ?5Nof##q?z)PIo@I@zBefhTiEHm<&=`d<3D!fm)gWZ-39jC|N^cFIPQ zGOocMJ?@UXnwd@`81IXlFQqZf^E=N)?Mf|5J(LA*<0#EMwZs|lvesZw9xmCk4SWSJqPg`8k41Y$Id+-pGP72bZ#0C z3fvmiyK(S69*OAmkNFS-8M37kleoM*MShPF3d+6N#r~Hg$1GAziy{)Nx?a8;ONbJ} zuCZemu8ozeY~f?TTy@xN6lEH2=Vqk}sG(F201E;0JS(4*&!?Y4?xdxqq0lL5DJdvd z6R$_F$lQ0<;A?RfJsy`wqg(h!Gejg?eA)Lh;Cq ztPds@reikCnDs>cd@Q7x*nKUCIBaxn>T!~q?*Moid1PV*51ur}js9SHL#LGwC~&TQ z9OzLxQ6_~Bo^&gfZWK3k4fVM63g`YuAIXOJo;$u74KQUO6=A3#i-JCa%Ajv7Km){@ z#o@>OMgH|7NCxtZ8}jncnHo#`w#{3Y6q5Vv{#+O85_JGpi5?kcgNxsHHya&8EJ>M zjXX)Ub}rDv<1jKU&|)Bv@bBQ%2ri1m9rq&sPZMu#ExN&qkJhqh`^H_i7S@!uA{ASR z>po0$?VpD}d@0J0wT3d8Rr z5jGZB#wafkHyU7K#6*&z?qPqV3w0YOKi)(Q4DmE>4z~?O`t6mCk`C7M88<`pepH&a z$I17U*ljE{b4TAIkEC(H4_%b~*UCY){5m%Qb6ZXU+)a&bOh3I_QxES})WiGLv|(qFf~u*x zB<}gen(Y{H%C{CK+RV5$RxNjyA=nn&|dKsLGJ+<>4V#aqliYyJa6rIzD( zxS~CxoLLaD0NmL{&48?77aUQAV~4uciDq|jssc8o>GV7_k}Sy0nFtojcWY6}8aN~g z22)|j!^k51uh4m9n=a(bY`q+h4{{YErRuyuStdU<& z6{!Qp;_Ab36spe@d6YKm-msW++!c*rptpLu)F%$?Y_1MuCz0sG;mXA+MI;-LDN$yM zOERYHr%-k2h6ai^$QiClBYCLfLmZ(-OX>>WTl9XeS{4TZ?Cfx?dqdSC6PcXfH-W9M z{{YEbU;hA-v1u}OETjV01f4wNS-vsVp06#ho7YOi4r8>8%7e0MGE}mxrpk2#P4$Xm ziX<+XR12<_tYop7F-)oQj1*jEt)|R@l}K4${iAL*BRe)GOO|5CO+IvAt-?qE?*ihD z)iH|0#n62l(jT1=M_#dp{3MSeDE-k<#sC*2sL%>fu!2%M04#v^oZ6Huai~=u9BUr} zNQTU;)*`l!8{ZX-hDUIqD!3<8Sh%8RoLD;mY!hNe(+Q!uJX56dwKo+)w5gof}<(c;+3-A_UzlBlN=4zM!Yj_C&ZeG8xYD2 zk`Z%rz;XhjNf3(_U=3o@7!pA6vAak(+SEZd7bA(O(lp>(iR3%!aV|X}9b$6GcO-)$ zJ{RfUFz7wzt_Y*pD{w|X!oIvRk8O`kZpC1d`Ba-+ipSuhi>iw(amPxZ zB3n8}(X$@OJAkHG3bnmY8BC&4|q;Lps{x!s-PAK0!1h z#A#4E^c^4IkX{74G zX&XXG7vWK(Byl>iXA7$u07#Icum<1|HP*3_MJ0n}V{H!VnFK!>mAb-gEJm8vGAW&K z&c#&T!!4`q@zUQUaBwB7NryZ6pwvq8Nvul3;a7pTGfFMW!lG23 zHO%1!gQ(x2ro3{gc>e%phVVQ^exkn0f0~gv!2r5vjAG!l4&bv+x^2u}Ap0b1miDAb-V=Im?j3;eYssa*YG4iHu&tdV2P24LA$ zya3-)yI}q+Ap@)gWhthDn=H=^0b4O!5O>^G({8RM%*wMlHoa{*6;bjW@~}6x>TxGn zsgU)CP221EQv%t;&uwjO%i?Ni#H6&du3!q2s0X|X-POv({x_#pMaG-4HUqJ3^@@r| zGDq201nAZFi9UDhM#I`Gd|3YgqYH?kcO$t)=$lWAQS0;kmb93UJdNTpOY)eF6S^_c zikvj82~`VWrK=H$_N_}RODi?QTEPIPby*aIWVQ2-8rCBb{{XhJbApbos8f8V{Tz?w zK}hN((AfQYjxFPY&{Dm2BSX5Qi`cXPA00~pyUgg6l1*mnBx1QqxsCUW9!8rqus>Gq#EV@? z@3#Jn5ebbfc;wcOS6c}qJJG1yED!A>)NHFFs~+ai^xBKAdlzFSRTdVg=2ncK_J(tF z!?F(H-%;no%7C-R+8u@ti5<~F#JLt=ZFQU-2dWY6J!S;uT#!AYuwrhqO!P<@$$zzxi+avZ4;A$rT zF=UDS=YL76Xku9rddq`+!$NF1ib!X91S2!47tOd1LiLQ0ODmU=V^NSX;4i5ai^LC7 z)bAph3xdD`FT+or;w|m28+LT{FVUYX9660oe5Wl7G{xm(_?yWji6;=p_!QgwnifP* zsyl~Eg+x-!$8Z9H3nK)KdJ!)2Xk>}9g(K=nlt7hxGbf_M`W_uT5s$E5u^mZR%!RB@ zgznO|u=iTA85}&T)>!>Y+W!FiTBYj@SfyP2?zg_Z0RI59<1>1*X8esCt1e)7iycKY z{ba3n%Jvtvv=m<Xq@PigG=+iO!nTBw z1Zyy4EJ5U`HyXrFtVm-%M$Smxdc>^wR&Lrdo~_rOQ@GY9Tr-_WNSMY?;WZ@B2sT*} zj77V`wPI&!BquB~5IV3A8x7{Lra;jL%&rvbZED$$=-zU(p%s9{9?20#&ZHqb$Tb;A z^G9SPUn*;B*3X;YTEI*D#i)9**0N>H8_nZ!(ITX4ikmaBuo`iq?VAkwrz5buG#qOj zvOY2)hecH?MUB|gGOV*AlBB7zJjG)s4cDxJGJ-oA0e$={zJ`;;t`DmzqkF`v-ORcX z&}<`#kF1LUL-b{(W84S#HTsiKi4Z_o3z4nrERh42WVs*{xv3E+gm3^E6M^p|qy8j6 znuX*~l#lTxJ&>WDZ^JH)L*&ecW*m_`(O<8N`K#><{GzbZ#}RR2N9o&uUg1C)UO8T4 z&5sWOQQm*>z?V9U& zF{{XVwOO%Jfz~$#9*CsP8;cRdcaHJGn3Zi3OI;;TMTIzmdutP$d#8x#Zy06!($HGm-UFZdzMUh$2V7)gM$+j;{*cU?)%X_urKvNC5?h2;yDK;%Hho zk1E^hk>-)%NZA8*c!V54+{5z~BD`)ctT$4Oz*sQ=kShTj$s=ZVVo4)DH z0CgV{e`wO>Naul6asg=DtBud;r zHRNjtip_5}!k}b}I4cTvncMcMK*Y=jU_PRa%U`KA_EY@Sp`&#w!0`r&&jaQ|yR3}x zJ|ddwAQtXCLGKm+01{f}X^Jd>vJt0ZP2!f#J6SL2C4Rmt`$GQ!B_gp_%%qEuM=JCs zudsjgsTCR54nsBUGPTWZRksn9$gQfY;EXF2lB|&h77Pu+<9c;P15(E*9yg#d5{){| z(XC4B3msjq8ONf7fT-0YPl|w}b+c=FU2|r5MuZXdl98J(opO%Qx27u<6oC3HFYM>@ zyy)rvFz4vZ7Ca)%GElQ5P>)aiD;O*U7$JZPb{-VH|TMA_tNEEt!rTCG;kp>vQ$b>gh`%lL7`NIHR{{UIv>Z5%IUi;O)FrbrjZ2i?c zJd#qSZAo~PwY4qaTz{zry>sp8|xG$op-So1MrR&AXf|7kn2-|FTVY+Rbq=q4`x-~2cqMZLb1xj zw4mw#0BiE5Mv(4p!~wWgNn~BiChPc$c14WYiwh!t72;@;r+GWym}*kR$`ApwD=6W5 z>)JNZhI9%?c@-{s(S5(j4{23?5}l2kxN{FNd)uOpc(!!A2)saArF;EUgZ=lv5vs*bGR`%%tr$Fjpjtj#?cvIU6b*;DvC_uot^xYPd8aEW8ai(W zznwZwBDJAlEC9ChK6iLV{HaZ!8Gh{dC+MseD3P7X;HocJF*jd|DTBKakt40UkHBID zm?V<4@HcTu38)tfws78h3yARv`q)YG2OEzdP`0-eG#n^+`2$idR-xM0_|>Ruaejfl zx)SuZHnn;dp}O|y9B6MMxmt#stBnsgbqy5I@1tsKp85f*6}?yvLa^!wg&PCFX-7do zoh?dDN2M=j^t76Cp{4K8IGS5EjcaX14a8Eduh2MJhT+gRiWaUkFQoz$P}J|}A6oUq zP~I&=N*qC=h#7FWWf_QLx!6==6}0h3IY~fa4r;{7vv!0% zq*$D!QZ-_t!0k<*Esu&~6L9?{MB`*&L{Z`7Ag^BW63X9sX9*TIBZS5$G#i?&+(!|> z{RC^upcw75HN;qW`(hL6NmG1tY)&sEtZpJFdaq8(!cR1QWRga#!`4M2swmMwupZC6 zg5Uy3@TpwQjcH@RBC)m>BsN-+TWD;lP8Snn=Y;bl9n3y~i;E8k*EELL?gNRa9|f~> z0XEW<5bV5iQna|I{WdO5Ws~n#+}c|)XL9qwhLCkk&AotYg(j!PVR^W=XX1KWMRBI#xaL*n(~_z>x}6NRgnjqJR_u z$Pc}O;@XZChHOU7mhZGSa+{SmzgrDr1E}-kI!PHk8F~To!9SO-wv<7@zf~8?NPSCR z2IjFM*tz!)$!lxkakG*cucMc{dMI*42w=Vc&cxY~IA6`20$>!J9<)DHRlO00I0y`7l_~ zOEVq74N0V8F|rtN8P>&cvGErGfc+3IA@OO{!H44R_3)Dsv%1h^7*K$P4_^$nnRDms zTXSjSW8)jjR<P`;A&ITlekMPDf&Tyt#$diDJp3Sj1?y?yM$Ot% zd~*qj5(bX%SdojxU623706Gx>0s#U90|o>H1p@^D000010ssRM1QH=J1wl~|6CffoVK7i} zfipC6krWj`LQ*4Qp;KdUfU*DD00;pB0RcY%{{YY+3525V*FMoM{)E%1i%Ge#{i9QL z*C=7_p^6oalKRV-~UZ4hQW%rvU*RQ-D}mdxG^MU5uxQYw`UY)3w^ z&zR}+HB!dkyhYAzJtO{L01E-xT8MB+9%)JYx`O`zLjkzB?Zng;$Ugm{v!-rie}jj? z*RQIsolG1ar#BL>3$B9eW)Ol6j{g7zvaaUPRisUWukSC}nMULZoj=GxLD-JW$~R#F z2Vy&Or%^OJe?ek)m2Q;n&5WQeNqiNM_rw8sg0&^N7ab?@DbUK8rf*2FmnWZX7PF3r zLl{_*#FkH%uRZDN+T%t0yhxF6YICM!0{r)rD88@gE)zx)k9JslfAo7hZ~C+ zbQaal!cwD)#uCM}>SJ)4!F;uDWh_o33s)se+WLbjy1FYa#A!I0BNJdw!Y2%>$gC@F zFDlZ-;L5gpgUlI$#T-Q}$J7ms;$)0XfjbGp>oV0e+n;56f|Vz8$VYmNOOkav%n`mC zm+AQ65JDZzKI|ED$?ci2TTWiX-z!qIhT8vGa#2Ha?xgP!&Ih9+e z*T=!v&h5|%#hUe1c>w~GweRgas=wf2Mp1I4OcoI0A6*o0DmDWSaufvfkA=fxXy>8= znUS;PwzssWg2Q3yXRYQu??I$;r}{+Vl~7R0a9Ekb5rsBmu^>(*`N8&>lsJ4%I)gxL zWj7TOSaTDLRQxN4cXxd5}q%AIj4D506XMB9Y&D$2p0%b+<`Qqa$2I#8Y9Z zv4*`N{nwQ-8T_g&Im)2t{{ZBthZej3(dmUkPz#B2NhiZWv?xydLI}|KxI203FR+|l zEm>QU69-*r)ectT#7bO7m0F5|ovv&r6$7|kInERZ@Z^ojm2ecEqp_|5CuxJDPYkR( z;K0RHJWHs(<~D$=s2ZSKYbjtU)2NOsi-UQ^4L6{qFg87AFlENQ{{YAtOx;xbc5!9{ z4guyw;OlBmdYQ#PH}(+WF^faI6-gOyU`)Z#Ohsa~9N&9Hrj*nwivc*7=NIiU^gG-r zAZnuXh1bnRk&dYl99};Bft+8}OX)CQm8J9_92*d2N@-BtO^-QMN~&Jm$AnaHEt^S2 z^HWo~*vgfW2B%(-@8Q}p&WhajJ}e!6;<~pjjKxq)T@jS)xH0fJl{Uys!C)(jsZqvI zx`8;euXc-lZ+lJ?RZ2ZFb~h6nhpM&M*pN-Z5{3e`L@Z8QZFz@tLlmTtY}PY~(rVOI z(_3_uu=T1qP#AS2Z83O8!{xhz4C0t(Bvnz$#$Y&t3Sym8Vl)OEW*7^af@3iU7gQ}Z zHW~?qpeRsO=WS;b{NLD0R_+t@iHxH7TtIZt6LB!ug|A6mZ_aTq&JVOu4ImE1W0%6# zs)Lv!aGE)3wNf=>IM1q|(993zX?+LuE(=pW+W3PJu_NGN8&*>+ItbtfHt;IuHk5G( z43@peLqD8<+k=Xi@`VdAV=>QiNtipU=eg216)J3LgJ|4L)Ka8n9@4>|PG&ASs8dz8 zU^El?w$`Kvyv5cPpEb!cy-O(;2Jmpjt9HIw{G5N)GL9T!oLFntDYyn>aDuIT<&MHq z08>%W35`Kq1eiP@@b@{`-*GXRL01u)0wbdns?Me-JVbo zDhq8Ll-ly?(l}5w5z{k3000iki8~hqraOs$vN|e&AyZ>2rbBVD z+s3HQY$ZC>kwRZ+Y9LZ>Hiv%NhWSso%$Cfh4?Uq4J}cVupM?5T^!&&h8CY4*LNDjh)|8-rVsZ(iDGYiK}}8f z>=c;m8A|RVqc+gfa3Y~=+7LmpiiNZgsZv4SWf@fSFw+Beg;WnzM7@rYsqbPZb~N)w z79@Dv@K|&|3=5Gedacj_*taAL%8jkL2n9P0ObWR-HkPN>W3k$b_gF%vO2*lz;UV_|#32qbil4n>r~%H4}oKnU6mqdkt$F$UsTTg*4-DtcBH zHURA@<+E$8paUyfe{uaXkQt*fEOm+IMhBEZP&NL zhe$!N&_kO&fEJZA3+OE=fCgfI&;UbN>SfmIZ?t1}C&qsX{v9EDiaq0-u|1UZ1oZ4g zExypy0jxIKG7+d6KrTRxFrrwSwyva)X%YV%p~3 zo^jaSZOlaSJ&bB?azLMkru&$Lq(TOKf<3EJw;o(r0C!}gb3xj^eHUxHOZK1W`ayE+gH|Gwf z^D)c`);ngj&0(Yrd7U8A@G-vdF;Z+|kPXBpB@J64pTMF&htBRGWQz5NLxU1jUZ#MyavYn<^0!#f^KGSDvp!9Jp?rkwh%i3 zXsiLhw7DA?#FKO5Z^Cu^tto>^8T#f}J(g`HW^K=LiG(h3(EYG;v}f zprD^Poz*^UPtpR5=})RwQ)9JAxzfY(D2pE4Iv;rL%eF+y-(%p(GDo@Br5s{%Q%Q=A zjkf&N!P~)9W;Rey;*1jka~lsTAdX#u9E*h~%Doi7;(v-NM&R}c-0Y2sS?>3f=B`RL1fimL&HIwBWIt_acjlAPp+Ch% z8%-vs^Fs{~7RyuNfEQ0OSYaSbIQ)1VHv(}|8|aI>;&1NAa$v* zcuaIT;|dzu5v5BTMhuRAR1_UFz}hDjxo&ABuE5c4`czgaS#SFYjybuq6`ByC9^fU z)apZ%*SJ-*UX>g~pKEF;HyRpKbqBi3m5A@-53&c|uxn_^>S&v8Ycll}aCwTJZ$`g! zwIIYx?FO|8rHR76y=ySGZdScC(#qLWYGH_oX3cR>Oe9~p-ndc-umQF$a%&SSOFWKF z=d*!K!d8gvMeB{k7ZR*LP}aCN!Fig*kL%0(&2U&HIGpw&KsCpmOtG!?16tt!004aJ z1nY{y%L{HUDp=Y%9S-VkTn`QlyvJo02Cy^wd{!m70s#C9;$Imz?(xpl!1_BcN`m=K z`qQ9sV@(5trlN#09#*K~%c+s6W>3I#vkfiproCZKoRXzJIX=GMW~h-Z!1(1n8_h8PdhS^=+3o- z%p@Yl*6Cb4hvCRS@gk%dUB^S&z|@>kJrvg-Z|lal)C%C=1V1#m{0mR$j36Au5xN=I7CDfe7?5j?VT$4v)M)y7)7t~X0R2U6 z=4*f(iHSVxiF{<=yW|p5paL!i)f)yaZtg`U?@b@1LOJrN zouQE2erQh-9`LF02vDl)dclL3n`vssCZVbcRihUo;-thPkOsesk#od;;;_;Lat}Ir z5mw~XGKm4OBGtihilL!$I*Zp5;Zhk4u#CaAAX4BlY}OGq>j8ya1P<~Mn2VanVooCx z@5^Ca8{mA+Vh{U?NI$IC51lc%LmSwz7B!5+J{oq6jX55oxIe(3I%X+5Fx)Yy#CY5^ z4%~=7Vz}lbG>Zs2D6Rpt)x>@>Z{6}SHJo%k;sM-v7=fT_xuM)9!u0V8TIYK8Eq&_s zK%)1(MWD-V1xu*QLkJGJtwRbFgL~07U3yV~NW|@4snv<7kfPeK(Ott5Yz-bT@Y!q{ z)&9QU^$MRC>yg=;PL%3Zh&99U#vC?4pz3O7%`3I7b~VGC;fa@-8j8o`#wdVL45vfR zuo%{h3R1xDOm6M>HG##PGy!xs*ILHir)MYL*0^3M;ltTMy<*?ePVH-4{EoDZnL+Vo z7ehb|JFQAB*iFT)N;Re$)tZiKwbp<8t5nUI@7AG`$!f|o z5>GmkMk{6PicG0%((3m$ETokKl}O=`b*LXL>O*%Y!N zbt9;ux5(W0ZffSPdoF6{vi|@p6XzD@H}A6r7c}w61I(FMN1JfC(m?vz*7uh41w|#YiKqD`rqffYK`Oze;9iH#>44 zRU-8p3LdQtW5 IKliWy*-pB?&;S4c literal 0 HcmV?d00001 diff --git a/gee-orm/doc/geeorm/geeorm_sm.jpg b/gee-orm/doc/geeorm/geeorm_sm.jpg new file mode 100644 index 0000000000000000000000000000000000000000..960a985f9ffb3bdec2f06a9728c8b305e3746890 GIT binary patch literal 7426 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 VSs~Gavv8Pz`0uK|lP`bg{s*+JHLU;u literal 0 HcmV?d00001 From a875e92d6853a3be917bc48dbe32a57db2349d12 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Mon, 2 Mar 2020 19:52:45 +0800 Subject: [PATCH 054/122] fix main.go addr define code --- gee-cache/day5-multi-nodes/main.go | 3 +-- gee-cache/day6-single-flight/main.go | 3 +-- gee-cache/day7-proto-buf/main.go | 3 +-- gee-cache/doc/geecache-day5.md | 3 +-- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/gee-cache/day5-multi-nodes/main.go b/gee-cache/day5-multi-nodes/main.go index a99940c..b8677f2 100644 --- a/gee-cache/day5-multi-nodes/main.go +++ b/gee-cache/day5-multi-nodes/main.go @@ -73,8 +73,7 @@ func main() { 8003: "http://localhost:8003", } - addrs := make([]string, 3) - + var addrs []string for _, v := range addrMap { addrs = append(addrs, v) } diff --git a/gee-cache/day6-single-flight/main.go b/gee-cache/day6-single-flight/main.go index a99940c..b8677f2 100644 --- a/gee-cache/day6-single-flight/main.go +++ b/gee-cache/day6-single-flight/main.go @@ -73,8 +73,7 @@ func main() { 8003: "http://localhost:8003", } - addrs := make([]string, 3) - + var addrs []string for _, v := range addrMap { addrs = append(addrs, v) } diff --git a/gee-cache/day7-proto-buf/main.go b/gee-cache/day7-proto-buf/main.go index a99940c..b8677f2 100644 --- a/gee-cache/day7-proto-buf/main.go +++ b/gee-cache/day7-proto-buf/main.go @@ -73,8 +73,7 @@ func main() { 8003: "http://localhost:8003", } - addrs := make([]string, 3) - + var addrs []string for _, v := range addrMap { addrs = append(addrs, v) } diff --git a/gee-cache/doc/geecache-day5.md b/gee-cache/doc/geecache-day5.md index 28111ae..4e28083 100644 --- a/gee-cache/doc/geecache-day5.md +++ b/gee-cache/doc/geecache-day5.md @@ -280,8 +280,7 @@ func main() { 8003: "http://localhost:8003", } - addrs := make([]string, 3) - + var addrs []string for _, v := range addrMap { addrs = append(addrs, v) } From 9e1174e1cd35b467a0974a2df72eed11c71c3ee8 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Tue, 3 Mar 2020 19:49:14 +0800 Subject: [PATCH 055/122] change the order of Header().Set() & WriteHeader() --- gee-web/day2-context/gee/context.go | 6 +++--- gee-web/day3-router/gee/context.go | 6 +++--- gee-web/day4-group/gee/context.go | 6 +++--- gee-web/day5-middleware/gee/context.go | 6 +++--- gee-web/day6-template/gee/context.go | 8 ++++---- gee-web/day7-panic-recover/gee/context.go | 8 ++++---- gee-web/doc/gee-day2.md | 6 +++--- gee-web/doc/gee-day6.md | 4 ++-- 8 files changed, 25 insertions(+), 25 deletions(-) diff --git a/gee-web/day2-context/gee/context.go b/gee-web/day2-context/gee/context.go index 795bf16..72a7fe7 100644 --- a/gee-web/day2-context/gee/context.go +++ b/gee-web/day2-context/gee/context.go @@ -46,14 +46,14 @@ func (c *Context) SetHeader(key string, value string) { } func (c *Context) String(code int, format string, values ...interface{}) { - c.Status(code) c.SetHeader("Content-Type", "text/plain") + c.Status(code) c.Writer.Write([]byte(fmt.Sprintf(format, values...))) } func (c *Context) JSON(code int, obj interface{}) { - c.Status(code) c.SetHeader("Content-Type", "application/json") + c.Status(code) encoder := json.NewEncoder(c.Writer) if err := encoder.Encode(obj); err != nil { http.Error(c.Writer, err.Error(), 500) @@ -66,7 +66,7 @@ func (c *Context) Data(code int, data []byte) { } func (c *Context) HTML(code int, html string) { - c.Status(code) c.SetHeader("Content-Type", "text/html") + c.Status(code) c.Writer.Write([]byte(html)) } diff --git a/gee-web/day3-router/gee/context.go b/gee-web/day3-router/gee/context.go index 2733bdb..cf79939 100644 --- a/gee-web/day3-router/gee/context.go +++ b/gee-web/day3-router/gee/context.go @@ -52,14 +52,14 @@ func (c *Context) SetHeader(key string, value string) { } func (c *Context) String(code int, format string, values ...interface{}) { - c.Status(code) c.SetHeader("Content-Type", "text/plain") + c.Status(code) c.Writer.Write([]byte(fmt.Sprintf(format, values...))) } func (c *Context) JSON(code int, obj interface{}) { - c.Status(code) c.SetHeader("Content-Type", "application/json") + c.Status(code) encoder := json.NewEncoder(c.Writer) if err := encoder.Encode(obj); err != nil { http.Error(c.Writer, err.Error(), 500) @@ -72,7 +72,7 @@ func (c *Context) Data(code int, data []byte) { } func (c *Context) HTML(code int, html string) { - c.Status(code) c.SetHeader("Content-Type", "text/html") + c.Status(code) c.Writer.Write([]byte(html)) } diff --git a/gee-web/day4-group/gee/context.go b/gee-web/day4-group/gee/context.go index 2733bdb..cf79939 100644 --- a/gee-web/day4-group/gee/context.go +++ b/gee-web/day4-group/gee/context.go @@ -52,14 +52,14 @@ func (c *Context) SetHeader(key string, value string) { } func (c *Context) String(code int, format string, values ...interface{}) { - c.Status(code) c.SetHeader("Content-Type", "text/plain") + c.Status(code) c.Writer.Write([]byte(fmt.Sprintf(format, values...))) } func (c *Context) JSON(code int, obj interface{}) { - c.Status(code) c.SetHeader("Content-Type", "application/json") + c.Status(code) encoder := json.NewEncoder(c.Writer) if err := encoder.Encode(obj); err != nil { http.Error(c.Writer, err.Error(), 500) @@ -72,7 +72,7 @@ func (c *Context) Data(code int, data []byte) { } func (c *Context) HTML(code int, html string) { - c.Status(code) c.SetHeader("Content-Type", "text/html") + c.Status(code) c.Writer.Write([]byte(html)) } diff --git a/gee-web/day5-middleware/gee/context.go b/gee-web/day5-middleware/gee/context.go index 63eca76..1885e0c 100644 --- a/gee-web/day5-middleware/gee/context.go +++ b/gee-web/day5-middleware/gee/context.go @@ -69,14 +69,14 @@ func (c *Context) SetHeader(key string, value string) { } func (c *Context) String(code int, format string, values ...interface{}) { - c.Status(code) c.SetHeader("Content-Type", "text/plain") + c.Status(code) c.Writer.Write([]byte(fmt.Sprintf(format, values...))) } func (c *Context) JSON(code int, obj interface{}) { - c.Status(code) c.SetHeader("Content-Type", "application/json") + c.Status(code) encoder := json.NewEncoder(c.Writer) if err := encoder.Encode(obj); err != nil { http.Error(c.Writer, err.Error(), 500) @@ -89,7 +89,7 @@ func (c *Context) Data(code int, data []byte) { } func (c *Context) HTML(code int, html string) { - c.Status(code) c.SetHeader("Content-Type", "text/html") + c.Status(code) c.Writer.Write([]byte(html)) } diff --git a/gee-web/day6-template/gee/context.go b/gee-web/day6-template/gee/context.go index 4e16ca2..9c47b0c 100644 --- a/gee-web/day6-template/gee/context.go +++ b/gee-web/day6-template/gee/context.go @@ -71,14 +71,14 @@ func (c *Context) SetHeader(key string, value string) { } func (c *Context) String(code int, format string, values ...interface{}) { - c.Status(code) c.SetHeader("Content-Type", "text/plain") + c.Status(code) c.Writer.Write([]byte(fmt.Sprintf(format, values...))) } func (c *Context) JSON(code int, obj interface{}) { - c.Status(code) c.SetHeader("Content-Type", "application/json") + c.Status(code) encoder := json.NewEncoder(c.Writer) if err := encoder.Encode(obj); err != nil { http.Error(c.Writer, err.Error(), 500) @@ -93,8 +93,8 @@ func (c *Context) Data(code int, data []byte) { // HTML template render // refer https://golang.org/pkg/html/template/ func (c *Context) HTML(code int, name string, data interface{}) { - c.Writer.WriteHeader(code) - c.Writer.Header().Set("Content-Type", "text/html") + c.SetHeader("Content-Type", "text/html") + c.Status(code) if err := c.engine.htmlTemplates.ExecuteTemplate(c.Writer, name, data); err != nil { c.Fail(500, err.Error()) } diff --git a/gee-web/day7-panic-recover/gee/context.go b/gee-web/day7-panic-recover/gee/context.go index 4e16ca2..9c47b0c 100644 --- a/gee-web/day7-panic-recover/gee/context.go +++ b/gee-web/day7-panic-recover/gee/context.go @@ -71,14 +71,14 @@ func (c *Context) SetHeader(key string, value string) { } func (c *Context) String(code int, format string, values ...interface{}) { - c.Status(code) c.SetHeader("Content-Type", "text/plain") + c.Status(code) c.Writer.Write([]byte(fmt.Sprintf(format, values...))) } func (c *Context) JSON(code int, obj interface{}) { - c.Status(code) c.SetHeader("Content-Type", "application/json") + c.Status(code) encoder := json.NewEncoder(c.Writer) if err := encoder.Encode(obj); err != nil { http.Error(c.Writer, err.Error(), 500) @@ -93,8 +93,8 @@ func (c *Context) Data(code int, data []byte) { // HTML template render // refer https://golang.org/pkg/html/template/ func (c *Context) HTML(code int, name string, data interface{}) { - c.Writer.WriteHeader(code) - c.Writer.Header().Set("Content-Type", "text/html") + c.SetHeader("Content-Type", "text/html") + c.Status(code) if err := c.engine.htmlTemplates.ExecuteTemplate(c.Writer, name, data); err != nil { c.Fail(500, err.Error()) } diff --git a/gee-web/doc/gee-day2.md b/gee-web/doc/gee-day2.md index 281ebcc..f63fde1 100644 --- a/gee-web/doc/gee-day2.md +++ b/gee-web/doc/gee-day2.md @@ -134,14 +134,14 @@ func (c *Context) SetHeader(key string, value string) { } func (c *Context) String(code int, format string, values ...interface{}) { - c.Status(code) c.SetHeader("Content-Type", "text/plain") + c.Status(code) c.Writer.Write([]byte(fmt.Sprintf(format, values...))) } func (c *Context) JSON(code int, obj interface{}) { - c.Status(code) c.SetHeader("Content-Type", "application/json") + c.Status(code) encoder := json.NewEncoder(c.Writer) if err := encoder.Encode(obj); err != nil { http.Error(c.Writer, err.Error(), 500) @@ -154,8 +154,8 @@ func (c *Context) Data(code int, data []byte) { } func (c *Context) HTML(code int, html string) { - c.Status(code) c.SetHeader("Content-Type", "text/html") + c.Status(code) c.Writer.Write([]byte(html)) } ``` diff --git a/gee-web/doc/gee-day6.md b/gee-web/doc/gee-day6.md index a12ff30..be0b0fe 100644 --- a/gee-web/doc/gee-day6.md +++ b/gee-web/doc/gee-day6.md @@ -114,8 +114,8 @@ type Context struct { } func (c *Context) HTML(code int, name string, data interface{}) { - c.Writer.WriteHeader(code) - c.Writer.Header().Set("Content-Type", "text/html") + c.SetHeader("Content-Type", "text/html") + c.Status(code) if err := c.engine.htmlTemplates.ExecuteTemplate(c.Writer, name, data); err != nil { c.Fail(500, err.Error()) } From e64de2e960e2e1fdb0d3359c9605a5886b5cb1fa Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Tue, 3 Mar 2020 21:49:29 +0800 Subject: [PATCH 056/122] init geeorm titles & description --- gee-orm/doc/geeorm-day1.md | 23 +++++++++++++++++++++++ gee-orm/doc/geeorm-day2.md | 26 ++++++++++++++++++++++++++ gee-orm/doc/geeorm-day3.md | 25 +++++++++++++++++++++++++ gee-orm/doc/geeorm-day4.md | 25 +++++++++++++++++++++++++ gee-orm/doc/geeorm-day5.md | 25 +++++++++++++++++++++++++ gee-orm/doc/geeorm-day6.md | 24 ++++++++++++++++++++++++ gee-orm/doc/geeorm-day7.md | 24 ++++++++++++++++++++++++ 7 files changed, 172 insertions(+) create mode 100644 gee-orm/doc/geeorm-day1.md create mode 100644 gee-orm/doc/geeorm-day2.md create mode 100644 gee-orm/doc/geeorm-day3.md create mode 100644 gee-orm/doc/geeorm-day4.md create mode 100644 gee-orm/doc/geeorm-day5.md create mode 100644 gee-orm/doc/geeorm-day6.md create mode 100644 gee-orm/doc/geeorm-day7.md diff --git a/gee-orm/doc/geeorm-day1.md b/gee-orm/doc/geeorm-day1.md new file mode 100644 index 0000000..e90353b --- /dev/null +++ b/gee-orm/doc/geeorm-day1.md @@ -0,0 +1,23 @@ +--- +title: 动手写ORM框架 - GeeORM第一天 database/sql 基础 +date: 2020-03-03 23:00:00 +description: 7天用 Go语言/golang 从零实现 ORM 框架 GeeORM 教程(7 days implement golang object relational mapping framework from scratch tutorial),动手写 ORM 框架,参照 gorm, xorm 的实现。介绍了 SQLite 的基础操作(连接数据库,创建表、增删记录等),使用 Go 标准库 database/sql 操作 SQLite 数据库,包括执行(Exec),查询(Query, QueryRow)。 +tags: +- Go +nav: 从零实现 +categories: +- ORM框架 - GeeORM +keywords: +- Go语言 +- 从零实现ORM框架 +- database/sql +- sqlite +image: post/geeorm/geeorm_sm.jpg +github: https://github.com/geektutu/7days-golang +published: false +--- + +本文是[7天用Go从零实现ORM框架GeeORM](https://geektutu.com/post/geeorm.html)的第一篇。 + +- SQLite 的基础操作(连接数据库,创建表、增删记录等)。 +- 使用 Go 语言标准库 database/sql 连接并操作 SQLite 数据库,并简单封装。 \ No newline at end of file diff --git a/gee-orm/doc/geeorm-day2.md b/gee-orm/doc/geeorm-day2.md new file mode 100644 index 0000000..3e33396 --- /dev/null +++ b/gee-orm/doc/geeorm-day2.md @@ -0,0 +1,26 @@ +--- +title: 动手写ORM框架 - GeeORM第二天 对象表结构映射 +date: 2020-03-03 23:00:00 +description: 7天用 Go语言/golang 从零实现 ORM 框架 GeeORM 教程(7 days implement golang object relational mapping framework from scratch tutorial),动手写 ORM 框架,参照 gorm, xorm 的实现。使用反射(reflect)获取任意 struct 对象的名称和字段,映射为数据中的表;使用 dialect 隔离不同数据库之间的差异,便于扩展;数据表的创建(create)、删除(drop)。 +tags: +- Go +nav: 从零实现 +categories: +- ORM框架 - GeeORM +keywords: +- Go语言 +- 从零实现ORM框架 +- database/sql +- sqlite +- reflect +- table mapping +image: post/geeorm/geeorm_sm.jpg +github: https://github.com/geektutu/7days-golang +published: false +--- + +本文是[7天用Go从零实现ORM框架GeeORM](https://geektutu.com/post/geeorm.html)的第二篇。 + +- 使用反射(reflect)获取任意 struct 对象的名称和字段,映射为数据中的表。 +- 使用 dialect 隔离不同数据库之间的差异,便于扩展。 +- 数据表的创建(create)、删除(drop)。 \ No newline at end of file diff --git a/gee-orm/doc/geeorm-day3.md b/gee-orm/doc/geeorm-day3.md new file mode 100644 index 0000000..ed0b47d --- /dev/null +++ b/gee-orm/doc/geeorm-day3.md @@ -0,0 +1,25 @@ +--- +title: 动手写ORM框架 - GeeORM第三天 插入和查询记录 +date: 2020-03-03 23:00:00 +description: 7天用 Go语言/golang 从零实现 ORM 框架 GeeORM 教程(7 days implement golang object relational mapping framework from scratch tutorial),动手写 ORM 框架,参照 gorm, xorm 的实现。实现新增(insert)记录的功能;使用反射(reflect)将数据库的记录转换为对应的结构体,实现查询(select)功能。 +tags: +- Go +nav: 从零实现 +categories: +- ORM框架 - GeeORM +keywords: +- Go语言 +- 从零实现ORM框架 +- database/sql +- sqlite +- insert into +- select from +image: post/geeorm/geeorm_sm.jpg +github: https://github.com/geektutu/7days-golang +published: false +--- + +本文是[7天用Go从零实现ORM框架GeeORM](https://geektutu.com/post/geeorm.html)的第三篇。 + +- 实现新增(insert)记录的功能。 +- 使用反射(reflect)将数据库的记录转换为对应的结构体,实现查询(select)功能。 \ No newline at end of file diff --git a/gee-orm/doc/geeorm-day4.md b/gee-orm/doc/geeorm-day4.md new file mode 100644 index 0000000..98cc55c --- /dev/null +++ b/gee-orm/doc/geeorm-day4.md @@ -0,0 +1,25 @@ +--- +title: 动手写ORM框架 - GeeORM第四天 链式操作与更新删除 +date: 2020-03-03 23:00:00 +description: 7天用 Go语言/golang 从零实现 ORM 框架 GeeORM 教程(7 days implement golang object relational mapping framework from scratch tutorial),动手写 ORM 框架,参照 gorm, xorm 的实现。通过链式(chain)操作,支持查询条件(where, order by, limit 等)的叠加;实现记录的更新(update)和删除(delete)功能。 +tags: +- Go +nav: 从零实现 +categories: +- ORM框架 - GeeORM +keywords: +- Go语言 +- 从零实现ORM框架 +- database/sql +- sqlite +- chain operation +- delete from +image: post/geeorm/geeorm_sm.jpg +github: https://github.com/geektutu/7days-golang +published: false +--- + +本文是[7天用Go从零实现ORM框架GeeORM](https://geektutu.com/post/geeorm.html)的第四篇。 + +- 通过链式(chain)操作,支持查询条件(where, order by, limit 等)的叠加。 +- 实现记录的更新(update)和删除(delete)功能。 \ No newline at end of file diff --git a/gee-orm/doc/geeorm-day5.md b/gee-orm/doc/geeorm-day5.md new file mode 100644 index 0000000..cbb86cd --- /dev/null +++ b/gee-orm/doc/geeorm-day5.md @@ -0,0 +1,25 @@ +--- +title: 动手写ORM框架 - GeeORM第五天 实现钩子(Hooks) +date: 2020-03-03 23:00:00 +description: 7天用 Go语言/golang 从零实现 ORM 框架 GeeORM 教程(7 days implement golang object relational mapping framework from scratch tutorial),动手写 ORM 框架,参照 gorm, xorm 的实现。通过反射(reflect)获取结构体绑定的钩子(hooks),并调用;支持增删查改(CRUD)前后调用钩子。 +tags: +- Go +nav: 从零实现 +categories: +- ORM框架 - GeeORM +keywords: +- Go语言 +- 从零实现ORM框架 +- database/sql +- sqlite +- hooks +- BeforeUpdate +image: post/geeorm/geeorm_sm.jpg +github: https://github.com/geektutu/7days-golang +published: false +--- + +本文是[7天用Go从零实现ORM框架GeeORM](https://geektutu.com/post/geeorm.html)的第五篇。 + +- 通过反射(reflect)获取结构体绑定的钩子(hooks),并调用。 +- 支持增删查改(CRUD)前后调用钩子。 \ No newline at end of file diff --git a/gee-orm/doc/geeorm-day6.md b/gee-orm/doc/geeorm-day6.md new file mode 100644 index 0000000..e6f6f03 --- /dev/null +++ b/gee-orm/doc/geeorm-day6.md @@ -0,0 +1,24 @@ +--- +title: 动手写ORM框架 - GeeORM第六天 支持事务(Transaction) +date: 2020-03-03 23:00:00 +description: 7天用 Go语言/golang 从零实现 ORM 框架 GeeORM 教程(7 days implement golang object relational mapping framework from scratch tutorial),动手写 ORM 框架,参照 gorm, xorm 的实现。介绍 SQLite 数据库中的事务(transaction);封装事务,用户自定义回调函数实现原子操作。 +tags: +- Go +nav: 从零实现 +categories: +- ORM框架 - GeeORM +keywords: +- Go语言 +- 从零实现ORM框架 +- database/sql +- sqlite +- transaction +image: post/geeorm/geeorm_sm.jpg +github: https://github.com/geektutu/7days-golang +published: false +--- + +本文是[7天用Go从零实现ORM框架GeeORM](https://geektutu.com/post/geeorm.html)的第六篇。 + +- 介绍 SQLite 数据库中的事务(transaction)。 +- 封装事务,用户自定义回调函数实现原子操作。 \ No newline at end of file diff --git a/gee-orm/doc/geeorm-day7.md b/gee-orm/doc/geeorm-day7.md new file mode 100644 index 0000000..6e2bf40 --- /dev/null +++ b/gee-orm/doc/geeorm-day7.md @@ -0,0 +1,24 @@ +--- +title: 动手写ORM框架 - GeeORM第七天 数据库迁移(Migrate) +date: 2020-03-03 23:00:00 +description: 7天用 Go语言/golang 从零实现 ORM 框架 GeeORM 教程(7 days implement golang object relational mapping framework from scratch tutorial),动手写 ORM 框架,参照 gorm, xorm 的实现。结构体(struct)变更时,数据库表的字段(field)自动迁移(migrate);仅支持字段新增与删除,不支持字段类型变更。 +tags: +- Go +nav: 从零实现 +categories: +- ORM框架 - GeeORM +keywords: +- Go语言 +- 从零实现ORM框架 +- database/sql +- sqlite +- migrate +image: post/geeorm/geeorm_sm.jpg +github: https://github.com/geektutu/7days-golang +published: false +--- + +本文是[7天用Go从零实现ORM框架GeeORM](https://geektutu.com/post/geeorm.html)的第七篇。 + +- 结构体(struct)变更时,数据库表的字段(field)自动迁移(migrate)。 +- 仅支持字段新增与删除,不支持字段类型变更。 \ No newline at end of file From 32082371e8f0ac326604743e3b979079b281350c Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Sat, 7 Mar 2020 22:21:54 +0800 Subject: [PATCH 057/122] add geeorm day1 & cmd_test --- gee-orm/day1-database-sql/cmd_test/main.go | 20 + gee-orm/day1-database-sql/session/raw_test.go | 2 +- .../day2-reflect-schema/session/raw_test.go | 2 +- gee-orm/day3-save-query/session/raw_test.go | 3 +- .../day4-chain-operation/session/raw_test.go | 2 +- gee-orm/day5-hooks/session/raw_test.go | 2 +- gee-orm/day6-transaction/session/raw_test.go | 2 +- gee-orm/day7-migrate/session/raw_test.go | 2 +- gee-orm/doc/geeorm-day1.md | 397 +++++++++++++++++- gee-orm/doc/geeorm-day1/geeorm_log.png | Bin 0 -> 6745 bytes 10 files changed, 421 insertions(+), 11 deletions(-) create mode 100755 gee-orm/day1-database-sql/cmd_test/main.go create mode 100755 gee-orm/doc/geeorm-day1/geeorm_log.png diff --git a/gee-orm/day1-database-sql/cmd_test/main.go b/gee-orm/day1-database-sql/cmd_test/main.go new file mode 100755 index 0000000..1048cf4 --- /dev/null +++ b/gee-orm/day1-database-sql/cmd_test/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "fmt" + "geeorm" + _ "github.com/mattn/go-sqlite3" +) + +func main() { + engine, _ := geeorm.NewEngine("sqlite3", "gee.db") + defer engine.Close() + s := engine.NewSession() + _, _ = s.Raw("DROP TABLE IF EXISTS User;").Exec() + _, _ = s.Raw("CREATE TABLE User(Name text);").Exec() + _, _ = s.Raw("CREATE TABLE User(Name text);").Exec() + result, _ := s.Raw("INSERT INTO User(`Name`) values (?), (?)", "Tom", "Sam").Exec() + count, _ := result.RowsAffected() + fmt.Printf("Exec success, %d affected\n", count) + +} diff --git a/gee-orm/day1-database-sql/session/raw_test.go b/gee-orm/day1-database-sql/session/raw_test.go index 5721844..36e9678 100644 --- a/gee-orm/day1-database-sql/session/raw_test.go +++ b/gee-orm/day1-database-sql/session/raw_test.go @@ -18,7 +18,7 @@ func TestMain(m *testing.M) { } func NewSession() *Session { - return &Session{db: TestDB} + return New(TestDB) } func TestSession_Exec(t *testing.T) { diff --git a/gee-orm/day2-reflect-schema/session/raw_test.go b/gee-orm/day2-reflect-schema/session/raw_test.go index d521173..404bb6e 100644 --- a/gee-orm/day2-reflect-schema/session/raw_test.go +++ b/gee-orm/day2-reflect-schema/session/raw_test.go @@ -23,7 +23,7 @@ func TestMain(m *testing.M) { } func NewSession() *Session { - return &Session{db: TestDB, dialect: TestDial} + return New(TestDB, TestDial) } func TestSession_Exec(t *testing.T) { diff --git a/gee-orm/day3-save-query/session/raw_test.go b/gee-orm/day3-save-query/session/raw_test.go index d521173..d2212fe 100644 --- a/gee-orm/day3-save-query/session/raw_test.go +++ b/gee-orm/day3-save-query/session/raw_test.go @@ -23,9 +23,8 @@ func TestMain(m *testing.M) { } func NewSession() *Session { - return &Session{db: TestDB, dialect: TestDial} + return New(TestDB, TestDial) } - func TestSession_Exec(t *testing.T) { s := NewSession() _, _ = s.Raw("DROP TABLE IF EXISTS User;").Exec() diff --git a/gee-orm/day4-chain-operation/session/raw_test.go b/gee-orm/day4-chain-operation/session/raw_test.go index d521173..404bb6e 100644 --- a/gee-orm/day4-chain-operation/session/raw_test.go +++ b/gee-orm/day4-chain-operation/session/raw_test.go @@ -23,7 +23,7 @@ func TestMain(m *testing.M) { } func NewSession() *Session { - return &Session{db: TestDB, dialect: TestDial} + return New(TestDB, TestDial) } func TestSession_Exec(t *testing.T) { diff --git a/gee-orm/day5-hooks/session/raw_test.go b/gee-orm/day5-hooks/session/raw_test.go index d521173..404bb6e 100644 --- a/gee-orm/day5-hooks/session/raw_test.go +++ b/gee-orm/day5-hooks/session/raw_test.go @@ -23,7 +23,7 @@ func TestMain(m *testing.M) { } func NewSession() *Session { - return &Session{db: TestDB, dialect: TestDial} + return New(TestDB, TestDial) } func TestSession_Exec(t *testing.T) { diff --git a/gee-orm/day6-transaction/session/raw_test.go b/gee-orm/day6-transaction/session/raw_test.go index d521173..404bb6e 100644 --- a/gee-orm/day6-transaction/session/raw_test.go +++ b/gee-orm/day6-transaction/session/raw_test.go @@ -23,7 +23,7 @@ func TestMain(m *testing.M) { } func NewSession() *Session { - return &Session{db: TestDB, dialect: TestDial} + return New(TestDB, TestDial) } func TestSession_Exec(t *testing.T) { diff --git a/gee-orm/day7-migrate/session/raw_test.go b/gee-orm/day7-migrate/session/raw_test.go index d521173..404bb6e 100644 --- a/gee-orm/day7-migrate/session/raw_test.go +++ b/gee-orm/day7-migrate/session/raw_test.go @@ -23,7 +23,7 @@ func TestMain(m *testing.M) { } func NewSession() *Session { - return &Session{db: TestDB, dialect: TestDial} + return New(TestDB, TestDial) } func TestSession_Exec(t *testing.T) { diff --git a/gee-orm/doc/geeorm-day1.md b/gee-orm/doc/geeorm-day1.md index e90353b..10a8863 100644 --- a/gee-orm/doc/geeorm-day1.md +++ b/gee-orm/doc/geeorm-day1.md @@ -14,10 +14,401 @@ keywords: - sqlite image: post/geeorm/geeorm_sm.jpg github: https://github.com/geektutu/7days-golang -published: false --- -本文是[7天用Go从零实现ORM框架GeeORM](https://geektutu.com/post/geeorm.html)的第一篇。 +本文是[7天用Go从零实现ORM框架GeeORM](https://geektutu.com/post/geeorm.html)的第一篇。介绍了 - SQLite 的基础操作(连接数据库,创建表、增删记录等)。 -- 使用 Go 语言标准库 database/sql 连接并操作 SQLite 数据库,并简单封装。 \ No newline at end of file +- 使用 Go 语言标准库 database/sql 连接并操作 SQLite 数据库,并简单封装。**代码约150行** + +## 1 初识 SQLite + +> SQLite is a C-language library that implements a small, fast, self-contained, high-reliability, full-featured, SQL database engine. +> -- [SQLite 官网](https://sqlite.org/index.html) + +SQLite 是一款轻量级的,遵守 ACID 事务原则的关系型数据库。SQLite 可以直接嵌入到代码中,不需要像 MySQL、PostgreSQL 需要启动独立的服务才能使用。SQLite 将数据存储在单一的磁盘文件中,使用起来非常方便。也非常适合初学者用来学习关系型数据的使用。GeeORM 的所有的开发和测试均基于 SQLite。 + +在 Ubuntu 上,安装 SQLite 只需要一行命令,无需配置即可使用。 + +```bash +apt-get install sqlite3 +``` + +接下来,连接数据库(gee.db),如若 gee.db 不存在,则会新建。如果连接成功,就进入到了 SQLite 的命令行模式,执行 `.help` 可以看到所有的帮助命令。 + +```bash +> sqlite3 gee.db +SQLite version 3.22.0 2018-01-22 18:45:57 +Enter ".help" for usage hints. +sqlite> +``` + +使用 SQL 语句新建一张表 `User`,包含两个字段,字符串 Name 和 整型 Age。 + +```bash +sqlite> CREATE TABLE User(Name text, Age integer); +``` + +插入两条数据 + +```bash +sqlite> INSERT INTO User(Name, Age) VALUES ("Tom", 18), ("Jack", 25); +``` + +执行简单的查询操作,在执行之前使用 `.head on` 打开显示列名的开关,这样查询结果看上去更直观。 + +```bash +sqlite> .head on + +# 查找 `Age > 20` 的记录; +sqlite> SELECT * FROM User WHERE Age > 20; +Name|Age +Jack|25 + +# 统计记录个数。 +sqlite> SELECT COUNT(*) FROM User; +COUNT(*) +2 +``` + +使用 `.table` 查看当前数据库中所有的表(table),执行 `.schema ` 查看建表的 SQL 语句。 + +```bash +sqlite> .table +User + +sqlite> .schema User +CREATE TABLE User(Name text, Age integer); +``` + +SQLite 的使用暂时介绍这么多,了解了以上使用方法已经足够我们完成今天的任务了。如果想了解更多用法,可参考 [SQLite 常用命令](https://geektutu.com/post/cheat-sheet-sqlite.html)。 + + +## 2 database/sql 标准库 + +Go 语言提供了标准库 `database/sql` 用于和数据库的交互,接下来我们写一个 Demo,看一看这个库的用法。 + +```go +package main + +import ( + "database/sql" + "log" + + _ "github.com/mattn/go-sqlite3" +) + +func main() { + db, _ := sql.Open("sqlite3", "gee.db") + defer func() { _ = db.Close() }() + _, _ = db.Exec("DROP TABLE IF EXISTS User;") + _, _ = db.Exec("CREATE TABLE User(Name text);") + result, err := db.Exec("INSERT INTO User(`Name`) values (?), (?)", "Tom", "Sam") + if err == nil { + affected, _ := result.RowsAffected() + log.Println(affected) + } + row := db.QueryRow("SELECT Name FROM User LIMIT 1") + var name string + if err := row.Scan(&name); err == nil { + log.Println(name) + } +} +``` + +> go-sqlite3 依赖于 gcc,如果这份代码在 Windows 上运行的话,需要安装 [mingw](http://mingw.org/) 或其他包含有 gcc 编译器的工具包。 + +执行 `go run .`,输出如下。 + +```bash +> go run . +2020/03/07 20:28:37 2 +2020/03/07 20:28:37 Tom +``` + +- 使用 `sql.Open()` 连接数据库,第一个参数是驱动名称,import 语句 `_ "github.com/mattn/go-sqlite3"` 包导入时会注册 sqlite3 的驱动,第二个参数是数据库的名称,对于 SQLite 来说,也就是文件名,不存在会新建。返回一个 `sql.DB` 实例的指针。 +- `Exec()` 用于执行 SQL 语句,如果是查询语句,不会返回相关的记录。所以查询语句通常使用 `Query()` 和 `QueryRow()`,前者可以返回多条记录,后者只返回一条记录。 +- `Exec()`、`Query()`、`QueryRow()` 接受1或多个入参,第一个入参是 SQL 语句,后面的入参是 SQL 语句中的占位符 `?` 对应的值,占位符一般用来防 SQL 注入。 +- `QueryRow()` 的返回值类型是 `*sql.Row`,`row.Scan()` 接受1或多个指针作为参数,可以获取对应列(column)的值,在这个示例中,只有 `Name` 一列,因此传入字符串指针 `&name` 即可获取到查询的结果。 + + +掌握了基础的 SQL 语句和 Go 标准库 `database/sql` 的使用,可以开始实现 ORM 框架的雏形了。 + +## 3 实现一个简单的 log 库 + +开发一个框架/库并不容易,详细的日志能够帮助我们快速地定位问题。因此,在写核心代码之前,我们先用几十行代码实现一个简单的 log 库。 + +> 为什么不直接使用原生的 log 库呢?log 标准库没有日志分级,不打印文件和行号,这就意味着我们很难快速知道是哪个地方发生了错误。 + +这个简易的 log 库具备以下特性: + +- 支持日志分级(Info、Error、Disabled 三级)。 +- 不同层级日志显示时使用不同的颜色区分。 +- 显示打印日志代码对应的文件名和行号。 + +```bash +go mod init geeorm +``` + +首先创建一个名为 geeorm 的 module,并新建文件 log/log.go,用于放置和日志相关的代码。GeeORM 现在长这个样子: + +```bash +day1-database-sql/ + |--log/ + |--log.go + |--go.mod +``` + +第一步,创建 2 个日志实例分别用于打印 Info 和 Error 日志。 + +[day1-database-sql/log/log.go](https://github.com/geektutu/7days-golang/tree/master/gee-orm/day1-database-sql/log) + +```go +package log + +import ( + "io/ioutil" + "log" + "os" + "sync" +) + +var ( + errorLog = log.New(os.Stdout, "\033[31m[error]\033[0m ", log.LstdFlags|log.Lshortfile) + infoLog = log.New(os.Stdout, "\033[34m[info ]\033[0m ", log.LstdFlags|log.Lshortfile) + loggers = []*log.Logger{errorLog, infoLog} + mu sync.Mutex +) + +// log methods +var ( + Error = errorLog.Println + Errorf = errorLog.Printf + Info = infoLog.Println + Infof = infoLog.Printf +) +``` + +- `[info ]` 颜色为蓝色,`[error]` 为红色。 +- 使用 `log.Lshortfile` 支持显示文件名和代码行号。 +- 暴露 `Error`,`Errorf`,`Info`,`Infof` 4个方法。 + +第二步呢,支持设置日志的层级(InfoLevel, ErrorLevel, Disabled)。 + +```go +// log levels +const ( + InfoLevel = iota + ErrorLevel + Disabled +) + +// SetLevel controls log level +func SetLevel(level int) { + mu.Lock() + defer mu.Unlock() + + for _, logger := range loggers { + logger.SetOutput(os.Stdout) + } + + if ErrorLevel < level { + errorLog.SetOutput(ioutil.Discard) + } + if InfoLevel < level { + infoLog.SetOutput(ioutil.Discard) + } +} +``` + +- 这一部分的实现非常简单,三个层级声明为三个常量,通过控制 `Output`,来控制日志是否打印。 +- 如果设置为 ErrorLevel,infoLog 的输出会被定向到 `ioutil.Discard`,即不打印该日志。 + +至此呢,一个简单的支持分级的 log 库就实现完成了。 + +## 4 核心结构 Session + +我们在根目录下新建一个文件夹 session,用于实现与数据库的交互。今天我们只实现直接调用 SQL 语句进行原生交互的部分,这部分代码实现在 `session/raw.go` 中。 + +[day1-database-sql/session/raw.go](https://github.com/geektutu/7days-golang/tree/master/gee-orm/day1-database-sql/session) + +```go +package session + +import ( + "database/sql" + "geeorm/log" + "strings" +) + +type Session struct { + db *sql.DB + sql strings.Builder + sqlVars []interface{} +} + +func New(db *sql.DB) *Session { + return &Session{db: db} +} + +func (s *Session) Clear() { + s.sql.Reset() + s.sqlVars = nil +} + +func (s *Session) DB() *sql.DB { + return s.db +} + +func (s *Session) Raw(sql string, values ...interface{}) *Session { + s.sql.WriteString(sql) + s.sql.WriteString(" ") + s.sqlVars = append(s.sqlVars, values...) + return s +} +``` + +- Session 结构体目前只包含三个成员变量,第一个是 `db *sql.DB`,即使用 `sql.Open()` 方法连接数据库成功之后返回的指针。 +- 第二个和第三个成员变量用来拼接 SQL 语句和 SQL 语句中占位符的对应值。用户调用 `Raw()` 方法即可改变这两个变量的值。 + +接下来呢,封装 `Exec()`、`Query()` 和 `QueryRow()` 三个原生方法。 + +```go +// Exec raw sql with sqlVars +func (s *Session) Exec() (result sql.Result, err error) { + defer s.Clear() + log.Info(s.sql.String(), s.sqlVars) + if result, err = s.DB().Exec(s.sql.String(), s.sqlVars...); err != nil { + log.Error(err) + } + return +} + +// QueryRow gets a record from db +func (s *Session) QueryRow() *sql.Row { + defer s.Clear() + log.Info(s.sql.String(), s.sqlVars) + return s.DB().QueryRow(s.sql.String(), s.sqlVars...) +} + +// QueryRows gets a list of records from db +func (s *Session) QueryRows() (rows *sql.Rows, err error) { + defer s.Clear() + log.Info(s.sql.String(), s.sqlVars) + if rows, err = s.DB().Query(s.sql.String(), s.sqlVars...); err != nil { + log.Error(err) + } + return +} +``` + +- 封装有 2 个目的,一是统一打印日志(包括 执行的SQL 语句和错误日志)。 +- 二是执行完成后,清空 `(s *Session).sql` 和 `(s *Session).sqlVars` 两个变量。这样 Session 可以复用,开启一次会话,可以执行多次 SQL。 + +## 4 核心结构 Engine + +Session 负责与数据库的交互,那交互前的准备工作(比如连接/测试数据库),交互后的收尾工作(关闭连接)等就交给 Engine 来负责了。Engine 是 GeeORM 与用户交互的入口。代码位于根目录的 `geeorm.go`。 + +[day1-database-sql/geeorm.go](https://github.com/geektutu/7days-golang/tree/master/gee-orm/day1-database-sql) + +```go +package geeorm + +import ( + "database/sql" + + "geeorm/log" + "geeorm/session" +) + +type Engine struct { + db *sql.DB +} + +func NewEngine(driver, source string) (e *Engine, err error) { + db, err := sql.Open(driver, source) + if err != nil { + log.Error(err) + return + } + // Send a ping to make sure the database connection is alive. + if err = db.Ping(); err != nil { + log.Error(err) + return + } + e = &Engine{db: db} + log.Info("Connect database success") + return +} + +func (engine *Engine) Close() { + if err := engine.db.Close(); err != nil { + log.Error("Failed to close database") + } + log.Info("Close database success") +} + +func (engine *Engine) NewSession() *session.Session { + return session.New(engine.db) +} +``` + +Engine 的逻辑非常简单,最重要的方法是 `NewEngine`,`NewEngine` 主要做了两件事。 + +- 连接数据库,返回 `*sql.DB`。 +- 调用 `db.Ping()`,检查数据库是否能够正常连接。 + +另外呢,提供了 Engine 提供了 `NewSession()` 方法,这样可以通过 `Engine` 实例创建会话,进而与数据库进行交互了。到这一步,整个 GeeORM 的框架雏形已经出来了。 + +```bash +day1-database-sql/ + |--log/ # 日志 + |--log.go + |--session/ # 数据库交互 + |--raw.go + |--geeorm.go # 用户交互 + |--go.mod +``` + +## 5 测试 + +GeeORM 的单元测试是比较完备的,可以参考 `log_test.go`、`raw_test.go` 和 `geeorm_test.go` 等几个测试文件,在这里呢,就不一一讲解了。接下来呢,我们将 geeorm 视为第三方库来使用。 + +在根目录下新建 cmd_test 目录放置测试代码,新建文件 main.go。 + +[day1-database-sql/cmd_test/main.go](https://github.com/geektutu/7days-golang/tree/master/gee-orm/day1-database-sql/cmd_test) + +```go +package main + +import ( + "geeorm" + "geeorm/log" + + _ "github.com/mattn/go-sqlite3" +) + +func main() { + engine, _ := geeorm.NewEngine("sqlite3", "gee.db") + defer engine.Close() + s := engine.NewSession() + _, _ = s.Raw("DROP TABLE IF EXISTS User;").Exec() + _, _ = s.Raw("CREATE TABLE User(Name text);").Exec() + _, _ = s.Raw("CREATE TABLE User(Name text);").Exec() + result, _ := s.Raw("INSERT INTO User(`Name`) values (?), (?)", "Tom", "Sam").Exec() + count, _ := result.RowsAffected() + fmt.Printf("Exec success, %d affected\n", count) +} +``` + +执行 `go run main.go`,将会看到如下的输出: + +![geeorm log](geeorm-day1/geeorm_log.png) + +日志中出现了一行报错信息,*table User already exists*,因为我们在 main 函数中执行了两次创建表 `User` 的语句。可以看到,每一行日志均标明了报错的文件和行号,而且不同层级日志的颜色是不同的。 + +## 附 推荐阅读 + +- [Go 语言简明教程](https://geektutu.com/post/quick-golang.html) +- [Go Test 单元测试简明教程](https://geektutu.com/post/quick-go-test.html) +- [SQLite 常用命令](https://geektutu.com/post/cheat-sheet-sqlite.html) \ No newline at end of file diff --git a/gee-orm/doc/geeorm-day1/geeorm_log.png b/gee-orm/doc/geeorm-day1/geeorm_log.png new file mode 100755 index 0000000000000000000000000000000000000000..dc58aaf388bc55ece329d1faa0cf53349ba53fc1 GIT binary patch literal 6745 zcmb7pcT`hNw?0jbAV>rOrAjAC@s%DR^eECq5JjpYAjNpCM#+raB0O z#bVJ2Xa-LF1QFzj`t$lT0=*f8K6!vL(qvE=31X;>kwdFDLH8SwG0$$qkX`QI zzMD-(Rt??N2f5t;1Asx%`Jo&g=>|4*KBdySP^pMu=@Oi{kI!v1216giyHs57hBUIO z;RXn#4!Z4AH3R}ZbEz7lyA^Tbhz#hYu>pZbd*6_CsUo8=Xgz0%h%^cH`wfncDDPld zyF-%{xeC7iJSwN!JK-y_0>1!qrE!3W^ArN!`G!%g_ zOz($igAhJLp^w%m! zY0t>`|sF8?5re+FNax;*=) zL0rZF-D;MU{W+4~Ddf_Ol$D#NnRnGtxHMybZc+>^Iq=#m6ZZ~3psudF?W}`LaP*JV zX0Y;m-Q{c++3CkZ1KSQW^Zg;U+L<)r4?UuuKzEq8=Z0t|h#Mj9ln{RIYQ(46?s#^R z?(&1Ax(*ht`xyyfmX1@1iL7rlmk)>lf5h&Nr6nkHq1`0@;h>Cvx3vRLB?WW;M^SSD9n_ zqEibs_cR<_zF*Q|AH(GEjC+)-xzm@Mtv4v$b{qBDM8!G9>Fh2LcM(xYm=})L95@`> z@5Cmm1E(np-^aVyNq7J+!)6Afp5Xl9%lWoz2b^1}xtp&7nJ9M~4=;Z3FKR>~f^;RjJhc{vtXxCHwjLBSC9-pPBJD2 z#ML;D_y$;)tsb{#Wqdkv4AsKUjzon81spmud8+uWzx?A;r`exiC8Wa*8cS;~hJ zWtz<%!H~yks4^{q#Gd^9wX!Fxj-OhjV)E=UkVTj4f82sZg4Q#5;9lJV7Avh7IqQr* zY+BR``gSkupqE6NFqG>rJB8(VHP`~vAgY_Ep zj;{<&z+XBXkj;e$eH`qW$a?&H_!wga(_*P)wicQLf1NJQ8 zhlQa6!T(1Op6KiR2qR{Tg$z?Amln^keWgqammq`=kJGQe4#Ky-G0Q;ny; z%X<8FsFI8jAhfk^kn{36}yN0)(Emuy;k;x7V~ByU!u7^UQh|s#I3!~wN>yoOfJ}K z_n(>7*WT-$w^ORhbc5>3q!gZz^$`bXi9EG$gM-w7{q>)6u9RT}YV~RFx;$pP$)(

agwxsH02r-hscgAzex z8I9{)2pUb4nR|C1t*>>#p!A=WXws4&nKs zD}QDQYKpk_$tpkH8&0Vh2p<0x+*J3zF#6E>{nGFEV8@P(y@Z=S%sZ|(lLy7i{GjNTvK>YSK0R+F~( zJw}uRN2_cz$}Ud6otGBiD?oIr;JQ3L(6Ah2U8O+K#&B<_@^ej>3N=YG3EIO|3sEuD zj8}VApTCCvapJ|ocR(W7-HTuxP%Dx0{6R|9&sF{$0rf>mj!+iEf5n^*WiFf)#h80Q zU1N+CScCCJldgswQazded|3q6DLYdm1sd2RMFIcojwc({0=8U#5rbIm2^RhZHi z{en0?2Gzd&gnnnuRD4_Fb8(*A=!MpVBGm~}SKerxeo04J#L)0L6*A#okLHx>Odtjt z808BpW4v_XdtJWuo7w#U;b_&yqvaA@?uajl6LZ}mQ5DiSnH5m1<@*NQq)!3BrlrS+ z2!;ru@P`d^rAOaLk|AknXX|ecYtvek2$>I763|!b^(nq=Fwx4khx)&!7u-@>Sr=Sj z(xK?Vw|M)Qzsgf?@)0uQpL^7^5Xz1T8ztU?=x0+aPnm??@Y#B?62zM$YFYIz_>?(1 z=`V~Rx@%r6wwm(}*>l_nT|uYS-Sa&t*FED_1@ojlQ6MA?t0}!v*|=<*;|*7h;Oy@) z8q{hR0C?6)K35qX&7rV)0oJw6C0WfVH|@7|5X;TsEk)D#_uw7Yj)Z~69Zw}s>^dCr z$575ythvovzqDv#|FZ~fuVl!dPCRHNZn&NAI>)L(Cz+?#(Dz$hT5DS3MM;d?wa?H= z?-DFK{G|eXhGmWe4Q{_M6mYcd_;ZZ5CXHkgNV+3Wv(pu)Cdge%Y;V{o0;}5E6k`uE zxcGhdrhnS_7mLW_ff(2ua_pde>JPJl{d}b`YTLO(78Q5&lPN0Rp6cUf)K&079&;r* z)PCJaVxeKeZojvMLQ10#TH|Wf$Wa&)t@@!EV8ipR!tg}pMKjjWf(hH-Md^V9zE_LD zBEiUKv{)ab&qYNGGbToedA8xvnGCg#j^)z=K6X<0!JkVl<5GCKV!-ibLv9Q#iEPB8 za!{~=1>&_*;tidm9z4o9iof?icOLpiw1_K@faEuy|MqisE>aN9MT{r24j44%Yz96| zb-o-O5qAGF>bPoDGvn)&k_|)7DOSj72pI&~vzqgNE{4wc1G6bktGjNUr#W=@t zVCeLDrKOS!it~v@87_R>Iw#BS^QI5pg0$}`z62|GtqOwyN4(6*+h>ITu~6hZp`a+E zv}(br2+vx*Wm+0Kt-J6+?wEgrA!4iJEJ?+axH{YYzkG=(cK38~p2q|$w#>oNz zYh_|l@d|LXjYr$4p?BqNuFFBx%!o{8&BH>tUo&Q#O+Y6*c-O@_4yPO{oxTW*N)qW7 zyYCL0TLYX%#2p_k8?gI$Fk~&DEj~?-yZT`3XwHZJH@Hp=8w`{5yl4=H*n` z6C1iT6bL}^BfIIc@LV$W^moaMsLnxF_GboxpYK9~46a5B=EVGmTtu_xIGGT-y+NS( ziCXmJ6s`JyYO1{O?^b_8^8cgMef;0q4-g3z3Hh}t9UBMPA9<9bl?>GM0uVNGng{R? z>A~~u7DPzrOhiBuVvpDJbkM8G$KJC?RWI;JmUaQn0pe%nWqGBFprss7yM#k_)w2g6yW{NM`}4PKryl831_3bltc(_AvI0--I5>7qvW-{i z@iod{_fn z2m%*LAjhhS-<>_3^s4!wyIgC*aQxwiaHCWE-TGRaBRzNJ&BXfbEuu7cdK7rXix1Ol zE=VQz7wx8-e=^(O5_s&|b@$9k{1;Bxr%U)?(rw}j7_T`*%f+&}b3wJYEX*~3A6V

r~&`I39VmUBG`=qJB8H8E@aFe4(9J|CAEL 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 QGGe%CXl78Y=M?$>01DJCb^rhX literal 0 HcmV?d00001 From 8b48181e8f0b65fa2ae20e71c88291488f6d8919 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Sun, 8 Mar 2020 00:35:59 +0800 Subject: [PATCH 058/122] add day2 --- gee-orm/doc/geeorm-day1.md | 6 +- gee-orm/doc/geeorm-day2.md | 371 ++++++++++++++++++++++++++++++++++++- 2 files changed, 369 insertions(+), 8 deletions(-) diff --git a/gee-orm/doc/geeorm-day1.md b/gee-orm/doc/geeorm-day1.md index 10a8863..f25df3b 100644 --- a/gee-orm/doc/geeorm-day1.md +++ b/gee-orm/doc/geeorm-day1.md @@ -1,6 +1,6 @@ --- title: 动手写ORM框架 - GeeORM第一天 database/sql 基础 -date: 2020-03-03 23:00:00 +date: 2020-03-07 23:00:00 description: 7天用 Go语言/golang 从零实现 ORM 框架 GeeORM 教程(7 days implement golang object relational mapping framework from scratch tutorial),动手写 ORM 框架,参照 gorm, xorm 的实现。介绍了 SQLite 的基础操作(连接数据库,创建表、增删记录等),使用 Go 标准库 database/sql 操作 SQLite 数据库,包括执行(Exec),查询(Query, QueryRow)。 tags: - Go @@ -305,7 +305,7 @@ func (s *Session) QueryRows() (rows *sql.Rows, err error) { - 封装有 2 个目的,一是统一打印日志(包括 执行的SQL 语句和错误日志)。 - 二是执行完成后,清空 `(s *Session).sql` 和 `(s *Session).sqlVars` 两个变量。这样 Session 可以复用,开启一次会话,可以执行多次 SQL。 -## 4 核心结构 Engine +## 5 核心结构 Engine Session 负责与数据库的交互,那交互前的准备工作(比如连接/测试数据库),交互后的收尾工作(关闭连接)等就交给 Engine 来负责了。Engine 是 GeeORM 与用户交互的入口。代码位于根目录的 `geeorm.go`。 @@ -370,7 +370,7 @@ day1-database-sql/ |--go.mod ``` -## 5 测试 +## 6 测试 GeeORM 的单元测试是比较完备的,可以参考 `log_test.go`、`raw_test.go` 和 `geeorm_test.go` 等几个测试文件,在这里呢,就不一一讲解了。接下来呢,我们将 geeorm 视为第三方库来使用。 diff --git a/gee-orm/doc/geeorm-day2.md b/gee-orm/doc/geeorm-day2.md index 3e33396..8546762 100644 --- a/gee-orm/doc/geeorm-day2.md +++ b/gee-orm/doc/geeorm-day2.md @@ -1,7 +1,7 @@ --- title: 动手写ORM框架 - GeeORM第二天 对象表结构映射 -date: 2020-03-03 23:00:00 -description: 7天用 Go语言/golang 从零实现 ORM 框架 GeeORM 教程(7 days implement golang object relational mapping framework from scratch tutorial),动手写 ORM 框架,参照 gorm, xorm 的实现。使用反射(reflect)获取任意 struct 对象的名称和字段,映射为数据中的表;使用 dialect 隔离不同数据库之间的差异,便于扩展;数据表的创建(create)、删除(drop)。 +date: 2020-03-08 00:20:00 +description: 7天用 Go语言/golang 从零实现 ORM 框架 GeeORM 教程(7 days implement golang object relational mapping framework from scratch tutorial),动手写 ORM 框架,参照 gorm, xorm 的实现。使用反射(reflect)获取任意 struct 对象的名称和字段,映射为数据中的表;使用 dialect 隔离不同数据库之间的差异,便于扩展;数据库表的创建(create)、删除(drop)。 tags: - Go nav: 从零实现 @@ -16,11 +16,372 @@ keywords: - table mapping image: post/geeorm/geeorm_sm.jpg github: https://github.com/geektutu/7days-golang -published: false --- 本文是[7天用Go从零实现ORM框架GeeORM](https://geektutu.com/post/geeorm.html)的第二篇。 -- 使用反射(reflect)获取任意 struct 对象的名称和字段,映射为数据中的表。 - 使用 dialect 隔离不同数据库之间的差异,便于扩展。 -- 数据表的创建(create)、删除(drop)。 \ No newline at end of file +- 使用反射(reflect)获取任意 struct 对象的名称和字段,映射为数据中的表。 +- 数据库表的创建(create)、删除(drop)。 + +## 1 Dialect + +SQL 语句中的类型和 Go 语言中的类型是不同的,例如Go 语言中的 `int`、`int8`、`int16` 等类型均对应 SQLite 中的 `integer` 类型。因此实现 ORM 映射的第一步,需要思考如何将 Go 语言的类型映射为数据库中的类型。 + +同时,不同数据库支持的数据类型也是有差异的,即使功能相同,在 SQL 语句的表达上也可能有差异。ORM 框架往往需要兼容多种数据库,因此我们需要将差异的这一部分提取出来,每一种数据库分别实现,实现最大程度的复用和解耦。这部分代码称之为 `dialect`。 + +在根目录下新建文件夹 dialect,并在 dialect 文件夹下新建文件 `dialect.go`,抽象出各个数据库差异的部分。 + +[day2-reflect-schema/dialect/dialect.go](https://github.com/geektutu/7days-golang/tree/master/gee-orm/day2-reflect-schema/dialect) + +```go +package dialect + +import "reflect" + +var dialectsMap = map[string]Dialect{} + +type Dialect interface { + DataTypeOf(typ reflect.Value) string + TableExistSQL(tableName string) (string, []interface{}) +} + +func RegisterDialect(name string, dialect Dialect) { + dialectsMap[name] = dialect +} + +func GetDialect(name string) (dialect Dialect, ok bool) { + dialect, ok = dialectsMap[name] + return +} +``` + +`Dialect` 接口包含 2 个方法: + +- `DataTypeOf` 用于将 Go 语言的类型转换为该数据库的数据类型。 +- `TableExistSQL` 返回某个表是否存在的 SQL 语句,参数是表名(table)。 + +当然,不同数据库之间的差异远远不止这两个地方,随着 ORM 框架功能的增多,dialect 的实现也会逐渐丰富起来,同时框架的其他部分不会受到影响。 + +同时,声明了 `RegisterDialect` 和 `GetDialect` 两个方法用于注册和获取 dialect 实例。如果新增加对某个数据库的支持,那么调用 `RegisterDialect` 即可注册到全局。 + +接下来,在`dialect` 目录下新建文件 `sqlite3.go` 增加对 SQLite 的支持。 + +[day2-reflect-schema/dialect/sqlite3.go](https://github.com/geektutu/7days-golang/tree/master/gee-orm/day2-reflect-schema/dialect) + +```go +package dialect + +import ( + "fmt" + "reflect" + "time" +) + +type sqlite3 struct{} + +var _ Dialect = (*sqlite3)(nil) + +func init() { + RegisterDialect("sqlite3", &sqlite3{}) +} + +func (s *sqlite3) DataTypeOf(typ reflect.Value) string { + switch typ.Kind() { + case reflect.Bool: + return "bool" + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uintptr: + return "integer" + case reflect.Int64, reflect.Uint64: + return "bigint" + case reflect.Float32, reflect.Float64: + return "real" + case reflect.String: + return "text" + case reflect.Array, reflect.Slice: + return "blob" + case reflect.Struct: + if _, ok := typ.Interface().(time.Time); ok { + return "datetime" + } + } + panic(fmt.Sprintf("invalid sql type %s (%s)", typ.Type().Name(), typ.Kind())) +} + +func (s *sqlite3) TableExistSQL(tableName string) (string, []interface{}) { + args := []interface{}{tableName} + return "SELECT name FROM sqlite_master WHERE type='table' and name = ?", args +} +``` + +- `sqlite3.go` 的实现虽然比较繁琐,但是整体逻辑还是非常清晰的。`DataTypeOf` 将 Go 语言的类型映射为 SQLite 的数据类型。`TableExistSQL` 返回了在 SQLite 中判断表 `tableName` 是否存在的 SQL 语句。 +- 实现了 `init()` 函数,包在第一次加载时,会将 sqlite3 的 dialect 自动注册到全局。 + +## 2 Schema + +Dialect 实现了一些特定的 SQL 语句的转换,接下来我们将要实现 ORM 框架中最为核心的转换——对象(object)和表(table)的转换。给定一个任意的对象,转换为关系型数据库中的表结构。 + +在数据库中创建一张表需要哪些要素呢? + +- 表名(table name) —— 结构体名(struct name) +- 字段名和字段类型 —— 成员变量和类型。 +- 额外的约束条件(例如非空、主键等) —— 成员变量的Tag(Go 语言通过 Tag 实现,Java、Python 等语言通过注解实现) + +举一个实际的例子: + +```go +type User struct { + Name string `geeorm:"PRIMARY KEY"` + Age int +} +``` + +期望对应的 schema 语句: + +```sql +CREATE TABLE `User` (`Name` text PRIMARY KEY, `Age` integer); +``` + +我们将这部分代码的实现放置在一个子包 `schema/schema.go` 中。 + +[day2-reflect-schema/schema/schema.go](https://github.com/geektutu/7days-golang/tree/master/gee-orm/day2-reflect-schema/schema) + +```go +package schema + +import ( + "geeorm/dialect" + "go/ast" + "reflect" +) + +// Field represents a column of database +type Field struct { + Name string + Type string + Tag string +} + +// Schema represents a table of database +type Schema struct { + Model interface{} + Name string + Fields []*Field + FieldNames []string + fieldMap map[string]*Field +} + +func (schema *Schema) GetField(name string) *Field { + return schema.fieldMap[name] +} +``` + +- Field 包含 3 个成员变量,字段名 Name、类型 Type、和约束条件 Tag +- Schema 主要包含被映射的对象 Model、表名 Name 和字段 Fields。 +- FieldNames 包含所有的字段名(列名),fieldMap 记录字段名和 Field 的映射关系,方便之后直接使用,无需遍历 Fields。 + +接下来实现 Parse 函数,将任意的对象解析为 Schema 实例。 + +```go +func Parse(dest interface{}, d dialect.Dialect) *Schema { + modelType := reflect.Indirect(reflect.ValueOf(dest)).Type() + schema := &Schema{ + Model: dest, + Name: modelType.Name(), + fieldMap: make(map[string]*Field), + } + + for i := 0; i < modelType.NumField(); i++ { + p := modelType.Field(i) + if !p.Anonymous && ast.IsExported(p.Name) { + field := &Field{ + Name: p.Name, + Type: d.DataTypeOf(reflect.Indirect(reflect.New(p.Type))), + } + if v, ok := p.Tag.Lookup("geeorm"); ok { + field.Tag = v + } + schema.Fields = append(schema.Fields, field) + schema.FieldNames = append(schema.FieldNames, p.Name) + schema.fieldMap[p.Name] = field + } + } + return schema +} +``` + +- `TypeOf()` 和 `ValueOf()` 是 reflect 包最为基本也是最重要的 2 个方法,分别用来返回入参的类型和值。因为设计的入参是一个对象的指针,因此需要 `reflect.Indirect()` 获取指针指向的实例。 +- `modelType.Name()` 获取到结构体的名称作为表名。 +- `NumField()` 获取实例的字段的个数,然后通过下标获取到特定字段 `p := modelType.Field(i)`。 +- `p.Name` 即字段名,`p.Type` 即字段类型,通过 `(Dialect).DataTypeOf()` 转换为数据库的字段类型,`p.Tag` 即额外的约束条件。 + +写一个测试用例来验证 Parse 函数。 + +```go +// schema_test.go +type User struct { + Name string `geeorm:"PRIMARY KEY"` + Age int +} + +var TestDial, _ = dialect.GetDialect("sqlite3") + +func TestParse(t *testing.T) { + schema := Parse(&User{}, TestDial) + if schema.Name != "User" || len(schema.Fields) != 2 { + t.Fatal("failed to parse User struct") + } + if schema.GetField("Name").Tag != "PRIMARY KEY" { + t.Fatal("failed to parse primary key") + } +} +``` + +## Session + +Session 的核心功能是与数据库进行交互。因此,我们将数据库表的增/删操作实现在子包 session 中。在此之前,Session 的结构需要做一些调整。 + +```go +type Session struct { + db *sql.DB + dialect dialect.Dialect + refTable *schema.Schema + sql strings.Builder + sqlVars []interface{} +} + +func New(db *sql.DB, dialect dialect.Dialect) *Session { + return &Session{ + db: db, + dialect: dialect, + } +} +``` + +- `Session` 成员变量新增 dialect 和 refTable +- 构造函数 `New` 的参数改为 2 个,db 和 dialect。 + +在文件夹 `session` 下新建 `table.go` 用于放置操作数据库表相关的代码。 + +[day2-reflect-schema/session/table.go](https://github.com/geektutu/7days-golang/tree/master/gee-orm/day2-reflect-schema/session) + +```go +func (s *Session) Model(value interface{}) *Session { + // nil or different model, update refTable + if s.refTable == nil || reflect.TypeOf(value) != reflect.TypeOf(s.refTable.Model) { + s.refTable = schema.Parse(value, s.dialect) + } + return s +} + +func (s *Session) RefTable() *schema.Schema { + if s.refTable == nil { + log.Error("Model is not set") + } + return s.refTable +} +``` + +- `Model()` 方法用于给 refTable 赋值。解析操作是比较耗时的,因此将解析的结果保存在成员变量 refTable 中,即使 `Model()` 被调用多次,如果传入的结构体名称不发生变化,则不会更新 refTable 的值。 +- `RefTable()` 方法返回 refTable 的值,如果 refTable 未被赋值,则打印错误日志。 + +接下来实现数据库表的创建、删除和判断是否存在的功能。三个方法的实现逻辑是相似的,利用 `RefTable()` 返回的数据库表和字段的信息,拼接出 SQL 语句,调用原生 SQL 接口执行。 + +```go +func (s *Session) CreateTable() error { + table := s.RefTable() + var columns []string + for _, field := range table.Fields { + columns = append(columns, fmt.Sprintf("%s %s %s", field.Name, field.Type, field.Tag)) + } + desc := strings.Join(columns, ",") + _, err := s.Raw(fmt.Sprintf("CREATE TABLE %s (%s);", table.Name, desc)).Exec() + return err +} + +func (s *Session) DropTable() error { + _, err := s.Raw(fmt.Sprintf("DROP TABLE IF EXISTS %s", s.RefTable().Name)).Exec() + return err +} + +func (s *Session) HasTable() bool { + sql, values := s.dialect.TableExistSQL(s.RefTable().Name) + row := s.Raw(sql, values...).QueryRow() + var tmp string + _ = row.Scan(&tmp) + return tmp == s.RefTable().Name +} +``` + +在 `table_test.go` 中实现对应的测试用例: + +```go +type User struct { + Name string `geeorm:"PRIMARY KEY"` + Age int +} + +func TestSession_CreateTable(t *testing.T) { + s := NewSession().Model(&User{}) + _ = s.DropTable() + _ = s.CreateTable() + if !s.HasTable() { + t.Fatal("Failed to create table User") + } +} +``` + +## Engine + +因为 Session 构造函数增加了对 dialect 的依赖,Engine 需要作一些细微的调整。 + +[day2-reflect-schema/geeorm.go](https://github.com/geektutu/7days-golang/tree/master/gee-orm/day2-reflect-schema) + +```go +type Engine struct { + db *sql.DB + dialect dialect.Dialect +} + +func NewEngine(driver, source string) (e *Engine, err error) { + db, err := sql.Open(driver, source) + if err != nil { + log.Error(err) + return + } + // Send a ping to make sure the database connection is alive. + if err = db.Ping(); err != nil { + log.Error(err) + return + } + // make sure the specific dialect exists + dial, ok := dialect.GetDialect(driver) + if !ok { + log.Errorf("dialect %s Not Found", driver) + return + } + e = &Engine{db: db, dialect: dial} + log.Info("Connect database success") + return +} + +func (engine *Engine) NewSession() *session.Session { + return session.New(engine.db, engine.dialect) +} +``` + +- `NewEngine` 创建 Engine 实例时,获取 driver 对应的 dialect。 +- `NewSession` 创建 Session 实例时,传递 dialect 给构造函数 New。 + +至此,第二天的内容已经完成了,总结一下今天的成果: + +- 1)为适配不同的数据库,映射数据类型和特定的 SQL 语句,创建 Dialect 层屏蔽数据库差异。 +- 2)设计 Schema,利用反射(reflect)完成结构体和数据库表结构的映射,包括表名、字段名、字段类型、字段 tag 等。 +- 3)构造创建(create)、删除(drop)、存在性(table exists) 的 SQL 语句完成数据库表的基本操作。 + +## 附 推荐阅读 + +- [Go 语言简明教程](https://geektutu.com/post/quick-golang.html) +- [Go Test 单元测试简明教程](https://geektutu.com/post/quick-go-test.html) +- [SQLite 常用命令](https://geektutu.com/post/cheat-sheet-sqlite.html) \ No newline at end of file From 9453b8b64c571fa9e5c5f61dfe97489def105f42 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Sun, 8 Mar 2020 01:49:56 +0800 Subject: [PATCH 059/122] add day3 --- gee-orm/day3-save-query/clause/generator.go | 2 +- .../day4-chain-operation/clause/generator.go | 2 +- gee-orm/day5-hooks/clause/generator.go | 2 +- gee-orm/day6-transaction/clause/generator.go | 2 +- gee-orm/day7-migrate/clause/generator.go | 2 +- gee-orm/doc/geeorm-day1.md | 2 +- gee-orm/doc/geeorm-day2.md | 6 +- gee-orm/doc/geeorm-day3.md | 362 +++++++++++++++++- gee-orm/doc/geeorm.md | 3 +- 9 files changed, 370 insertions(+), 13 deletions(-) diff --git a/gee-orm/day3-save-query/clause/generator.go b/gee-orm/day3-save-query/clause/generator.go index b257469..9ad8e24 100644 --- a/gee-orm/day3-save-query/clause/generator.go +++ b/gee-orm/day3-save-query/clause/generator.go @@ -35,7 +35,7 @@ func _insert(values ...interface{}) (string, []interface{}) { } func _values(values ...interface{}) (string, []interface{}) { - // VALUES ($v1), (&v2), ... + // VALUES ($v1), ($v2), ... var bindStr string var sql strings.Builder var vars []interface{} diff --git a/gee-orm/day4-chain-operation/clause/generator.go b/gee-orm/day4-chain-operation/clause/generator.go index 127fc43..23635ba 100644 --- a/gee-orm/day4-chain-operation/clause/generator.go +++ b/gee-orm/day4-chain-operation/clause/generator.go @@ -38,7 +38,7 @@ func _insert(values ...interface{}) (string, []interface{}) { } func _values(values ...interface{}) (string, []interface{}) { - // VALUES ($v1), (&v2), ... + // VALUES ($v1), ($v2), ... var bindStr string var sql strings.Builder var vars []interface{} diff --git a/gee-orm/day5-hooks/clause/generator.go b/gee-orm/day5-hooks/clause/generator.go index 127fc43..23635ba 100644 --- a/gee-orm/day5-hooks/clause/generator.go +++ b/gee-orm/day5-hooks/clause/generator.go @@ -38,7 +38,7 @@ func _insert(values ...interface{}) (string, []interface{}) { } func _values(values ...interface{}) (string, []interface{}) { - // VALUES ($v1), (&v2), ... + // VALUES ($v1), ($v2), ... var bindStr string var sql strings.Builder var vars []interface{} diff --git a/gee-orm/day6-transaction/clause/generator.go b/gee-orm/day6-transaction/clause/generator.go index 127fc43..23635ba 100644 --- a/gee-orm/day6-transaction/clause/generator.go +++ b/gee-orm/day6-transaction/clause/generator.go @@ -38,7 +38,7 @@ func _insert(values ...interface{}) (string, []interface{}) { } func _values(values ...interface{}) (string, []interface{}) { - // VALUES ($v1), (&v2), ... + // VALUES ($v1), ($v2), ... var bindStr string var sql strings.Builder var vars []interface{} diff --git a/gee-orm/day7-migrate/clause/generator.go b/gee-orm/day7-migrate/clause/generator.go index 127fc43..23635ba 100644 --- a/gee-orm/day7-migrate/clause/generator.go +++ b/gee-orm/day7-migrate/clause/generator.go @@ -38,7 +38,7 @@ func _insert(values ...interface{}) (string, []interface{}) { } func _values(values ...interface{}) (string, []interface{}) { - // VALUES ($v1), (&v2), ... + // VALUES ($v1), ($v2), ... var bindStr string var sql strings.Builder var vars []interface{} diff --git a/gee-orm/doc/geeorm-day1.md b/gee-orm/doc/geeorm-day1.md index f25df3b..3ecb72c 100644 --- a/gee-orm/doc/geeorm-day1.md +++ b/gee-orm/doc/geeorm-day1.md @@ -411,4 +411,4 @@ func main() { - [Go 语言简明教程](https://geektutu.com/post/quick-golang.html) - [Go Test 单元测试简明教程](https://geektutu.com/post/quick-go-test.html) -- [SQLite 常用命令](https://geektutu.com/post/cheat-sheet-sqlite.html) \ No newline at end of file +- [SQLite 常用命令速查表](https://geektutu.com/post/cheat-sheet-sqlite.html) \ No newline at end of file diff --git a/gee-orm/doc/geeorm-day2.md b/gee-orm/doc/geeorm-day2.md index 8546762..60f02f5 100644 --- a/gee-orm/doc/geeorm-day2.md +++ b/gee-orm/doc/geeorm-day2.md @@ -238,7 +238,7 @@ func TestParse(t *testing.T) { } ``` -## Session +## 3 Session Session 的核心功能是与数据库进行交互。因此,我们将数据库表的增/删操作实现在子包 session 中。在此之前,Session 的结构需要做一些调整。 @@ -332,7 +332,7 @@ func TestSession_CreateTable(t *testing.T) { } ``` -## Engine +## 4 Engine 因为 Session 构造函数增加了对 dialect 的依赖,Engine 需要作一些细微的调整。 @@ -384,4 +384,4 @@ func (engine *Engine) NewSession() *session.Session { - [Go 语言简明教程](https://geektutu.com/post/quick-golang.html) - [Go Test 单元测试简明教程](https://geektutu.com/post/quick-go-test.html) -- [SQLite 常用命令](https://geektutu.com/post/cheat-sheet-sqlite.html) \ No newline at end of file +- [SQLite 常用命令速查表](https://geektutu.com/post/cheat-sheet-sqlite.html) \ No newline at end of file diff --git a/gee-orm/doc/geeorm-day3.md b/gee-orm/doc/geeorm-day3.md index ed0b47d..26790de 100644 --- a/gee-orm/doc/geeorm-day3.md +++ b/gee-orm/doc/geeorm-day3.md @@ -1,7 +1,7 @@ --- title: 动手写ORM框架 - GeeORM第三天 插入和查询记录 -date: 2020-03-03 23:00:00 -description: 7天用 Go语言/golang 从零实现 ORM 框架 GeeORM 教程(7 days implement golang object relational mapping framework from scratch tutorial),动手写 ORM 框架,参照 gorm, xorm 的实现。实现新增(insert)记录的功能;使用反射(reflect)将数据库的记录转换为对应的结构体,实现查询(select)功能。 +date: 2020-03-08 01:00:00 +description: 7天用 Go语言/golang 从零实现 ORM 框架 GeeORM 教程(7 days implement golang object relational mapping framework from scratch tutorial),动手写 ORM 框架,参照 gorm, xorm 的实现。实现新增(insert)记录的功能;使用反射(reflect)将数据库的记录转换为对应的结构体实例,实现查询(select)功能。 tags: - Go nav: 从零实现 @@ -22,4 +22,360 @@ published: false 本文是[7天用Go从零实现ORM框架GeeORM](https://geektutu.com/post/geeorm.html)的第三篇。 - 实现新增(insert)记录的功能。 -- 使用反射(reflect)将数据库的记录转换为对应的结构体,实现查询(select)功能。 \ No newline at end of file +- 使用反射(reflect)将数据库的记录转换为对应的结构体实例,实现查询(select)功能。 + +## 1 Clause 构造 SQL 语句 + +从第三天开始,GeeORM 需要涉及一些较为复杂的操作,例如查询操作。查询语句一般由很多个子句(clause) 构成。SELECT 语句的构成通常是这样的: + +```sql +SELECT col1, col2, ... + FROM table_name + WHERE [ conditions ] + GROUP BY col1 + HAVING [ conditions ] +``` + +也就是说,如果想一次构造出完整的 SQL 语句是比较困难的,因此我们将构造 SQL 语句这一部分独立出来,放在子package clause 中实现。 + +首先在 `clause/generator.go` 中实现各个子句的生成规则。 + +[day3-save-query/clause/generator.go](https://github.com/geektutu/7days-golang/tree/master/gee-orm/day3-save-query/clause) + + +```go +package clause + +import ( + "fmt" + "strings" +) + +type generator func(values ...interface{}) (string, []interface{}) + +var generators map[Type]generator + +func init() { + generators = make(map[Type]generator) + generators[INSERT] = _insert + generators[VALUES] = _values + generators[SELECT] = _select + generators[LIMIT] = _limit + generators[WHERE] = _where + generators[ORDERBY] = _orderBy +} + +func genBindVars(num int) string { + var vars []string + for i := 0; i < num; i++ { + vars = append(vars, "?") + } + return strings.Join(vars, ", ") +} + +func _insert(values ...interface{}) (string, []interface{}) { + // INSERT INTO $tableName ($fields) + tableName := values[0] + fields := strings.Join(values[1].([]string), ",") + return fmt.Sprintf("INSERT INTO %s (%v)", tableName, fields), []interface{}{} +} + +func _values(values ...interface{}) (string, []interface{}) { + // VALUES ($v1), ($v2), ... + var bindStr string + var sql strings.Builder + var vars []interface{} + sql.WriteString("VALUES ") + for i, value := range values { + v := value.([]interface{}) + if bindStr == "" { + bindStr = genBindVars(len(v)) + } + sql.WriteString(fmt.Sprintf("(%v)", bindStr)) + if i+1 != len(values) { + sql.WriteString(", ") + } + vars = append(vars, v...) + } + return sql.String(), vars + +} + +func _select(values ...interface{}) (string, []interface{}) { + // SELECT $fields FROM $tableName + tableName := values[0] + fields := strings.Join(values[1].([]string), ",") + return fmt.Sprintf("SELECT %v FROM %s", fields, tableName), []interface{}{} +} + +func _limit(values ...interface{}) (string, []interface{}) { + // LIMIT $num + return "LIMIT ?", values +} + +func _where(values ...interface{}) (string, []interface{}) { + // WHERE $desc + desc, vars := values[0], values[1:] + return fmt.Sprintf("WHERE %s", desc), vars +} + +func _orderBy(values ...interface{}) (string, []interface{}) { + return fmt.Sprintf("ORDER BY %s", values[0]), []interface{}{} +} +``` + +然后在 `clause/clause.go` 中实现结构体 `Clause` 拼接各个独立的子句。 + +[day3-save-query/clause/clause.go](https://github.com/geektutu/7days-golang/tree/master/gee-orm/day3-save-query/clause) + +```go +package clause + +import "strings" + +type Clause struct { + sql map[Type]string + sqlVars map[Type][]interface{} +} + +type Type int +const ( + INSERT Type = iota + VALUES + SELECT + LIMIT + WHERE + ORDERBY +) + +func (c *Clause) Set(name Type, vars ...interface{}) { + if c.sql == nil { + c.sql = make(map[Type]string) + c.sqlVars = make(map[Type][]interface{}) + } + sql, vars := generators[name](vars...) + c.sql[name] = sql + c.sqlVars[name] = vars +} + +func (c *Clause) Build(orders ...Type) (string, []interface{}) { + var sqls []string + var vars []interface{} + for _, order := range orders { + if sql, ok := c.sql[order]; ok { + sqls = append(sqls, sql) + vars = append(vars, c.sqlVars[order]...) + } + } + return strings.Join(sqls, " "), vars +} +``` + +- `Set` 方法根据 `Type` 调用对应的 generator,生成该子句对应的 SQL 语句。 +- `Build` 方法根据传入的 `Type` 的顺序,构造出最终的 SQL 语句。 + +在 `clause_test.go` 实现对应的测试用例: + +```go +func testSelect(t *testing.T) { + var clause Clause + clause.Set(LIMIT, 3) + clause.Set(SELECT, "User", []string{"*"}) + clause.Set(WHERE, "Name = ?", "Tom") + clause.Set(ORDERBY, "Age ASC") + sql, vars := clause.Build(SELECT, WHERE, ORDERBY, LIMIT) + t.Log(sql, vars) + if sql != "SELECT * FROM User WHERE Name = ? ORDER BY Age ASC LIMIT ?" { + t.Fatal("failed to build SQL") + } + if !reflect.DeepEqual(vars, []interface{}{"Tom", 3}) { + t.Fatal("failed to build SQLVars") + } +} + +func TestClause_Build(t *testing.T) { + t.Run("select", func(t *testing.T) { + testSelect(t) + }) +} +``` + +## 2 实现 Insert 功能 + +clause 已经支持生成简单的插入(INSERT) 和 查询(SELECT) 的 SQL 语句,那么紧接着我们就可以在 session 中实现对应的功能了。 + +INSERT 对应的 SQL 语句一般是这样的: + +```sql +INSERT INTO table_name(col1, col2, col3, ...) VALUES + (A1, A2, A3, ...), + (B1, B2, B3, ...), + ... +``` + +在 ORM 框架中期望 Insert 的调用方式如下: + +```go +s := geeorm.NewEngine().NewSession() +u1 := &User{Name: "Tom", Age: 18} +u2 := &User{Name: "Sam", Age: 25} +s.Insert(u1, u2, ...) +``` + +也就是说,我们还需要一个步骤,根据数据库中列的顺序,从对象中找到对应的值,按顺序平铺。即 `u1`、`u2` 转换为 `("Tom", 18), ("Same", 25)` 这样的格式。 + +因此在实现 Insert 功能之前,还需要给 `Schema` 新增一个函数 `RecordValues` 完成上述的转换。 + +[day3-save-query/schema/schema.go](https://github.com/geektutu/7days-golang/tree/master/gee-orm/day3-save-query/schema) + +```go +func (schema *Schema) RecordValues(dest interface{}) []interface{} { + destValue := reflect.Indirect(reflect.ValueOf(dest)) + var fieldValues []interface{} + for _, field := range schema.Fields { + fieldValues = append(fieldValues, destValue.FieldByName(field.Name).Interface()) + } + return fieldValues +} +``` + +在 session 文件夹下新建 record.go,用于实现记录增删查改相关的代码。 + +[day3-save-query/session/record.go](https://github.com/geektutu/7days-golang/tree/master/gee-orm/day3-save-query/session) + +```go +package session + +import ( + "geeorm/clause" + "reflect" +) + +func (s *Session) Insert(values ...interface{}) (int64, error) { + recordValues := make([]interface{}, 0) + for _, value := range values { + table := s.Model(value).RefTable() + s.clause.Set(clause.INSERT, table.Name, table.FieldNames) + recordValues = append(recordValues, table.RecordValues(value)) + } + + s.clause.Set(clause.VALUES, recordValues...) + sql, vars := s.clause.Build(clause.INSERT, clause.VALUES) + result, err := s.Raw(sql, vars...).Exec() + if err != nil { + return 0, err + } + + return result.RowsAffected() +} +``` + +后续所有构造 SQL 语句的方式都将与 `Insert` 中构造 SQL 语句的方式一致。分两步: + +- 1)多次调用 `clause.Set()` 构造好每一个子句。 +- 2)调用一次 `clause.Build()` 按照传入的顺序构造出最终的 SQL 语句。 + +构造完成后,调用 `Raw().Exec()` 方法执行。 + +## 3 实现 Find 功能 + +期望的调用方式是这样的:传入一个切片指针,查询的结果保存在切片中。 + +```go +s := geeorm.NewEngine().NewSession() +var users []User +s.Find(&users); +``` + +Find 功能的难点和 Insert 恰好反了过来。Insert 需要将已经存在的对象的每一个字段的值平铺开来,而 Find 则是需要根据平铺开的字段的值构造出对象。同样,也需要用到反射(reflect)。 + +```go +func (s *Session) Find(values interface{}) error { + destSlice := reflect.Indirect(reflect.ValueOf(values)) + destType := destSlice.Type().Elem() + table := s.Model(reflect.New(destType).Elem().Interface()).RefTable() + + s.clause.Set(clause.SELECT, table.Name, table.FieldNames) + sql, vars := s.clause.Build(clause.SELECT, clause.WHERE, clause.ORDERBY, clause.LIMIT) + rows, err := s.Raw(sql, vars...).QueryRows() + if err != nil { + return err + } + + for rows.Next() { + dest := reflect.New(destType).Elem() + var values []interface{} + for _, name := range table.FieldNames { + values = append(values, dest.FieldByName(name).Addr().Interface()) + } + if err := rows.Scan(values...); err != nil { + return err + } + destSlice.Set(reflect.Append(destSlice, dest)) + } + return rows.Close() +} +``` + +Find 的代码实现比较复杂,主要分为以下几步: + +- 1) `destSlice.Type().Elem()` 获取切片的单个元素的类型 `destType`,使用 `reflect.New()` 方法创建一个 `destType` 的实例,作为 `Model()` 的入参,映射出表结构 `RefTable()`。 +- 2)根据表结构,使用 clause 构造出 SELECT 语句,查询到所有符合条件的记录 `rows`。 +- 3)遍历每一行记录,利用反射创建 `destType` 的实例 `dest`,将 `dest` 的所有字段平铺开,构造切片 `values`。 +- 4)调用 `rows.Scan()` 将该行记录每一列的值依次赋值给 values 中的每一个字段。 +- 5)将 `dest` 添加到切片 `destSlice` 中。循环直到所有的记录都添加到切片 `destSlice` 中。 + +## 4 测试 + +在 session 文件夹下新建 `record_test.go`,创建测试用例。 + +> `User` 和 `NewSession()` 的定义位于 raw_test.go 中。 + +[day3-save-query/session/record_test.go](https://github.com/geektutu/7days-golang/tree/master/gee-orm/day3-save-query/session) + +```go +package session + +import "testing" + +var ( + user1 = &User{"Tom", 18} + user2 = &User{"Sam", 25} + user3 = &User{"Jack", 25} +) + +func testRecordInit(t *testing.T) *Session { + t.Helper() + s := NewSession().Model(&User{}) + err1 := s.DropTable() + err2 := s.CreateTable() + _, err3 := s.Insert(user1, user2) + if err1 != nil || err2 != nil || err3 != nil { + t.Fatal("failed init test records") + } + return s +} + +func TestSession_Insert(t *testing.T) { + s := testRecordInit(t) + affected, err := s.Insert(user3) + if err != nil || affected != 1 { + t.Fatal("failed to create record") + } +} + +func TestSession_Find(t *testing.T) { + s := testRecordInit(t) + var users []User + if err := s.Find(&users); err != nil || len(users) != 2 { + t.Fatal("failed to query all") + } +} +``` + +## 附 推荐阅读 + +- [Go 语言简明教程](https://geektutu.com/post/quick-golang.html) +- [Go Test 单元测试简明教程](https://geektutu.com/post/quick-go-test.html) +- [SQLite 常用命令速查表](https://geektutu.com/post/cheat-sheet-sqlite.html) +- [Laws Of Reflection - golang.org](https://blog.golang.org/laws-of-reflection) \ No newline at end of file diff --git a/gee-orm/doc/geeorm.md b/gee-orm/doc/geeorm.md index 78f4087..c25af6c 100644 --- a/gee-orm/doc/geeorm.md +++ b/gee-orm/doc/geeorm.md @@ -134,4 +134,5 @@ gorm 正在彻底重构 v1 版本,短期内看不到发布 v2 的可能。相 ## 附 推荐阅读 - [Go 语言简明教程](https://geektutu.com/post/quick-golang.html) -- [Go Test 单元测试简明教程](https://geektutu.com/post/quick-go-test.html) \ No newline at end of file +- [Go Test 单元测试简明教程](https://geektutu.com/post/quick-go-test.html) +- [SQLite 常用命令速查表](https://geektutu.com/post/cheat-sheet-sqlite.html) \ No newline at end of file From 2b03ff8dbb8dbc4a976c8602406a4ad8f8eb91fd Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Sun, 8 Mar 2020 22:07:06 +0800 Subject: [PATCH 060/122] add day4-day7 --- README.md | 14 +- gee-orm/day5-hooks/session/hooks.go | 13 +- gee-orm/day6-transaction/session/hooks.go | 13 +- gee-orm/day7-migrate/geeorm_test.go | 2 +- gee-orm/day7-migrate/session/hooks.go | 13 +- gee-orm/doc/geeorm-day2.md | 2 +- gee-orm/doc/geeorm-day3.md | 28 ++- gee-orm/doc/geeorm-day4.md | 266 +++++++++++++++++++++- gee-orm/doc/geeorm-day5.md | 136 ++++++++++- gee-orm/doc/geeorm-day6.md | 257 ++++++++++++++++++++- gee-orm/doc/geeorm-day7.md | 150 +++++++++++- gee-orm/doc/geeorm.md | 14 +- 12 files changed, 858 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 5a9a75a..a85bc69 100644 --- a/README.md +++ b/README.md @@ -42,13 +42,13 @@ gorm 准备推出完全重写的 v2 版本(目前还在开发中),相对 gorm-v1 来说,xorm 的设计更容易理解,所以 geeorm 接口设计上主要参考了 xorm,一些细节实现上参考了 gorm。 -- 第一天:database/sql 基础 | [Code](gee-orm/day1-database-sql) -- 第二天:对象表结构映射 | [Code](gee-orm/day2-reflect-schema) -- 第三天:插入/查询记录 | [Code](gee-orm/day3-save-query) -- 第四天:链式操作与更新删除 | [Code](gee-orm/day4-chain-operation) -- 第五天:实现钩子(Hooks) | [Code](gee-orm/day5-hooks) -- 第六天:支持事务(Transaction) | [Code](gee-orm/day6-transaction) -- 第七天:数据库迁移(Migrate) | [Code](gee-orm/day7-migrate) +- 第一天:[database/sql 基础](https://geektutu.com/post/geeorm-day1.html) | [Code](gee-orm/day1-database-sql) +- 第二天:[对象表结构映射](https://geektutu.com/post/geeorm-day2.html) | [Code](gee-orm/day2-reflect-schema) +- 第三天:[记录新增和查询](https://geektutu.com/post/geeorm-day3.html) | [Code](gee-orm/day3-save-query) +- 第四天:[链式操作与更新删除](https://geektutu.com/post/geeorm-day4.html) | [Code](gee-orm/day4-chain-operation) +- 第五天:[实现钩子(Hooks)](https://geektutu.com/post/geeorm-day5.html) | [Code](gee-orm/day5-hooks) +- 第六天:[支持事务(Transaction)](https://geektutu.com/post/geeorm-day6.html) | [Code](gee-orm/day6-transaction) +- 第七天:[数据库迁移(Migrate)](https://geektutu.com/post/geeorm-day7.html) | [Code](gee-orm/day7-migrate) ### WebAssembly 使用示例 diff --git a/gee-orm/day5-hooks/session/hooks.go b/gee-orm/day5-hooks/session/hooks.go index 12462a4..d73c3c2 100644 --- a/gee-orm/day5-hooks/session/hooks.go +++ b/gee-orm/day5-hooks/session/hooks.go @@ -1,13 +1,16 @@ package session -import "reflect" +import ( + "geeorm/log" + "reflect" +) // Hooks constants const ( BeforeQuery = "BeforeQuery" AfterQuery = "AfterQuery" BeforeUpdate = "BeforeUpdate" - AfterUpdate = "AfterUpate" + AfterUpdate = "AfterUpdate" BeforeDelete = "BeforeDelete" AfterDelete = "AfterDelete" BeforeInsert = "BeforeInsert" @@ -15,7 +18,7 @@ const ( ) // CallMethod calls the registered hooks -func (s *Session) CallMethod(method string, value interface{}) error { +func (s *Session) CallMethod(method string, value interface{}) { fm := reflect.ValueOf(s.RefTable().Model).MethodByName(method) if value != nil { fm = reflect.ValueOf(value).MethodByName(method) @@ -24,9 +27,9 @@ func (s *Session) CallMethod(method string, value interface{}) error { if fm.IsValid() { if v := fm.Call(param); len(v) > 0 { if err, ok := v[0].Interface().(error); ok { - return err + log.Error(err) } } } - return nil + return } diff --git a/gee-orm/day6-transaction/session/hooks.go b/gee-orm/day6-transaction/session/hooks.go index 12462a4..d73c3c2 100644 --- a/gee-orm/day6-transaction/session/hooks.go +++ b/gee-orm/day6-transaction/session/hooks.go @@ -1,13 +1,16 @@ package session -import "reflect" +import ( + "geeorm/log" + "reflect" +) // Hooks constants const ( BeforeQuery = "BeforeQuery" AfterQuery = "AfterQuery" BeforeUpdate = "BeforeUpdate" - AfterUpdate = "AfterUpate" + AfterUpdate = "AfterUpdate" BeforeDelete = "BeforeDelete" AfterDelete = "AfterDelete" BeforeInsert = "BeforeInsert" @@ -15,7 +18,7 @@ const ( ) // CallMethod calls the registered hooks -func (s *Session) CallMethod(method string, value interface{}) error { +func (s *Session) CallMethod(method string, value interface{}) { fm := reflect.ValueOf(s.RefTable().Model).MethodByName(method) if value != nil { fm = reflect.ValueOf(value).MethodByName(method) @@ -24,9 +27,9 @@ func (s *Session) CallMethod(method string, value interface{}) error { if fm.IsValid() { if v := fm.Call(param); len(v) > 0 { if err, ok := v[0].Interface().(error); ok { - return err + log.Error(err) } } } - return nil + return } diff --git a/gee-orm/day7-migrate/geeorm_test.go b/gee-orm/day7-migrate/geeorm_test.go index b9656c5..4ccacf7 100644 --- a/gee-orm/day7-migrate/geeorm_test.go +++ b/gee-orm/day7-migrate/geeorm_test.go @@ -74,7 +74,7 @@ func TestEngine_Migrate(t *testing.T) { defer engine.Close() s := engine.NewSession() _, _ = s.Raw("DROP TABLE IF EXISTS User;").Exec() - _, _ = s.Raw("CREATE TABLE User(Name text, XXX integer);").Exec() + _, _ = s.Raw("CREATE TABLE User(Name text PRIMARY KEY, XXX integer);").Exec() _, _ = s.Raw("INSERT INTO User(`Name`) values (?), (?)", "Tom", "Sam").Exec() engine.Migrate(&User{}) diff --git a/gee-orm/day7-migrate/session/hooks.go b/gee-orm/day7-migrate/session/hooks.go index 12462a4..d73c3c2 100644 --- a/gee-orm/day7-migrate/session/hooks.go +++ b/gee-orm/day7-migrate/session/hooks.go @@ -1,13 +1,16 @@ package session -import "reflect" +import ( + "geeorm/log" + "reflect" +) // Hooks constants const ( BeforeQuery = "BeforeQuery" AfterQuery = "AfterQuery" BeforeUpdate = "BeforeUpdate" - AfterUpdate = "AfterUpate" + AfterUpdate = "AfterUpdate" BeforeDelete = "BeforeDelete" AfterDelete = "AfterDelete" BeforeInsert = "BeforeInsert" @@ -15,7 +18,7 @@ const ( ) // CallMethod calls the registered hooks -func (s *Session) CallMethod(method string, value interface{}) error { +func (s *Session) CallMethod(method string, value interface{}) { fm := reflect.ValueOf(s.RefTable().Model).MethodByName(method) if value != nil { fm = reflect.ValueOf(value).MethodByName(method) @@ -24,9 +27,9 @@ func (s *Session) CallMethod(method string, value interface{}) error { if fm.IsValid() { if v := fm.Call(param); len(v) > 0 { if err, ok := v[0].Interface().(error); ok { - return err + log.Error(err) } } } - return nil + return } diff --git a/gee-orm/doc/geeorm-day2.md b/gee-orm/doc/geeorm-day2.md index 60f02f5..9287a51 100644 --- a/gee-orm/doc/geeorm-day2.md +++ b/gee-orm/doc/geeorm-day2.md @@ -22,7 +22,7 @@ github: https://github.com/geektutu/7days-golang - 使用 dialect 隔离不同数据库之间的差异,便于扩展。 - 使用反射(reflect)获取任意 struct 对象的名称和字段,映射为数据中的表。 -- 数据库表的创建(create)、删除(drop)。 +- 数据库表的创建(create)、删除(drop)。**代码约150行** ## 1 Dialect diff --git a/gee-orm/doc/geeorm-day3.md b/gee-orm/doc/geeorm-day3.md index 26790de..ff089e1 100644 --- a/gee-orm/doc/geeorm-day3.md +++ b/gee-orm/doc/geeorm-day3.md @@ -1,5 +1,5 @@ --- -title: 动手写ORM框架 - GeeORM第三天 插入和查询记录 +title: 动手写ORM框架 - GeeORM第三天 记录新增和查询 date: 2020-03-08 01:00:00 description: 7天用 Go语言/golang 从零实现 ORM 框架 GeeORM 教程(7 days implement golang object relational mapping framework from scratch tutorial),动手写 ORM 框架,参照 gorm, xorm 的实现。实现新增(insert)记录的功能;使用反射(reflect)将数据库的记录转换为对应的结构体实例,实现查询(select)功能。 tags: @@ -22,7 +22,7 @@ published: false 本文是[7天用Go从零实现ORM框架GeeORM](https://geektutu.com/post/geeorm.html)的第三篇。 - 实现新增(insert)记录的功能。 -- 使用反射(reflect)将数据库的记录转换为对应的结构体实例,实现查询(select)功能。 +- 使用反射(reflect)将数据库的记录转换为对应的结构体实例,实现查询(select)功能。**代码约150行** ## 1 Clause 构造 SQL 语句 @@ -202,6 +202,26 @@ func TestClause_Build(t *testing.T) { ## 2 实现 Insert 功能 +首先为 Session 添加成员变量 clause + +```go +// session/raw.go +type Session struct { + db *sql.DB + dialect dialect.Dialect + refTable *schema.Schema + clause clause.Clause + sql strings.Builder + sqlVars []interface{} +} + +func (s *Session) Clear() { + s.sql.Reset() + s.sqlVars = nil + s.clause = clause.Clause{} +} +``` + clause 已经支持生成简单的插入(INSERT) 和 查询(SELECT) 的 SQL 语句,那么紧接着我们就可以在 session 中实现对应的功能了。 INSERT 对应的 SQL 语句一般是这样的: @@ -216,7 +236,7 @@ INSERT INTO table_name(col1, col2, col3, ...) VALUES 在 ORM 框架中期望 Insert 的调用方式如下: ```go -s := geeorm.NewEngine().NewSession() +s := geeorm.NewEngine("sqlite3", "gee.db").NewSession() u1 := &User{Name: "Tom", Age: 18} u2 := &User{Name: "Sam", Age: 25} s.Insert(u1, u2, ...) @@ -282,7 +302,7 @@ func (s *Session) Insert(values ...interface{}) (int64, error) { 期望的调用方式是这样的:传入一个切片指针,查询的结果保存在切片中。 ```go -s := geeorm.NewEngine().NewSession() +s := geeorm.NewEngine("sqlite3", "gee.db").NewSession() var users []User s.Find(&users); ``` diff --git a/gee-orm/doc/geeorm-day4.md b/gee-orm/doc/geeorm-day4.md index 98cc55c..6e0d307 100644 --- a/gee-orm/doc/geeorm-day4.md +++ b/gee-orm/doc/geeorm-day4.md @@ -1,7 +1,7 @@ --- title: 动手写ORM框架 - GeeORM第四天 链式操作与更新删除 -date: 2020-03-03 23:00:00 -description: 7天用 Go语言/golang 从零实现 ORM 框架 GeeORM 教程(7 days implement golang object relational mapping framework from scratch tutorial),动手写 ORM 框架,参照 gorm, xorm 的实现。通过链式(chain)操作,支持查询条件(where, order by, limit 等)的叠加;实现记录的更新(update)和删除(delete)功能。 +date: 2020-03-08 16:00:00 +description: 7天用 Go语言/golang 从零实现 ORM 框架 GeeORM 教程(7 days implement golang object relational mapping framework from scratch tutorial),动手写 ORM 框架,参照 gorm, xorm 的实现。通过链式(chain)操作,支持查询条件(where, order by, limit 等)的叠加;实现记录的更新(update)、删除(delete)和统计(count)功能。 tags: - Go nav: 从零实现 @@ -16,10 +16,268 @@ keywords: - delete from image: post/geeorm/geeorm_sm.jpg github: https://github.com/geektutu/7days-golang -published: false --- 本文是[7天用Go从零实现ORM框架GeeORM](https://geektutu.com/post/geeorm.html)的第四篇。 - 通过链式(chain)操作,支持查询条件(where, order by, limit 等)的叠加。 -- 实现记录的更新(update)和删除(delete)功能。 \ No newline at end of file +- 实现记录的更新(update)、删除(delete)和统计(count)功能。**代码约100行** + +## 1 支持 Update、Delete 和 Count + +### 1.1 子句生成器 + +clause 负责构造 SQL 语句,如果需要增加对更新(update)、删除(delete)和统计(count)功能的支持,第一步自然是在 clause 中实现 update、delete 和 count 子句的生成器。 + +第一步:在原来的基础上,新增 UPDATE、DELETE、COUNT 三个 `Type` 类型的枚举值。 + +[day4-chain-operation/clause/clause.go](https://github.com/geektutu/7days-golang/tree/master/gee-orm/day4-chain-operation/clause) + +```go +// Support types for Clause +const ( + INSERT Type = iota + VALUES + SELECT + LIMIT + WHERE + ORDERBY + UPDATE + DELETE + COUNT +) +``` + +第二步:实现对应字句的 generator,并注册到全局变量 `generators` 中 + +[day4-chain-operation/clause/generator.go](https://github.com/geektutu/7days-golang/tree/master/gee-orm/day4-chain-operation/clause) + +```go +func init() { + generators = make(map[Type]generator) + generators[INSERT] = _insert + generators[VALUES] = _values + generators[SELECT] = _select + generators[LIMIT] = _limit + generators[WHERE] = _where + generators[ORDERBY] = _orderBy + generators[UPDATE] = _update + generators[DELETE] = _delete + generators[COUNT] = _count +} + +func _update(values ...interface{}) (string, []interface{}) { + tableName := values[0] + m := values[1].(map[string]interface{}) + var keys []string + var vars []interface{} + for k, v := range m { + keys = append(keys, k+" = ?") + vars = append(vars, v) + } + return fmt.Sprintf("UPDATE %s SET %s", tableName, strings.Join(keys, ", ")), vars +} + +func _delete(values ...interface{}) (string, []interface{}) { + return fmt.Sprintf("DELETE FROM %s", values[0]), []interface{}{} +} + +func _count(values ...interface{}) (string, []interface{}) { + return _select(values[0], []string{"count(*)"}) +} +``` + +- `_update` 设计入参是2个,第一个参数是表名(table),第二个参数是 map 类型,表示待更新的键值对。 +- `_delete` 只有一个入参,即表名。 +- `_count` 只有一个入参,即表名,并复用了 `_select` 生成器。 + + +### 1.2 Update 方法 + +子句的 generator 已经准备好了,接下来和 Insert、Find 等方法一样,在 `session/record.go` 中按照一定顺序拼接 SQL 语句并调用就可以了。 + +[day4-chain-operation/session/record.go](https://github.com/geektutu/7days-golang/tree/master/gee-orm/day4-chain-operation/session) + +```go +// support map[string]interface{} +// also support kv list: "Name", "Tom", "Age", 18, .... +func (s *Session) Update(kv ...interface{}) (int64, error) { + m, ok := kv[0].(map[string]interface{}) + if !ok { + m = make(map[string]interface{}) + for i := 0; i < len(kv); i += 2 { + m[kv[i].(string)] = kv[i+1] + } + } + s.clause.Set(clause.UPDATE, s.RefTable().Name, m) + sql, vars := s.clause.Build(clause.UPDATE, clause.WHERE) + result, err := s.Raw(sql, vars...).Exec() + if err != nil { + return 0, err + } + return result.RowsAffected() +} +``` + +Update 方法比较特别的一点在于,Update 接受 2 种入参,平铺开来的键值对和 map 类型的键值对。因为 generator 接受的参数是 map 类型的键值对,因此 `Update` 方法会动态地判断传入参数的类型,如果是不是 map 类型,则会自动转换。 + + +### 1.3 Delete 方法 + +```go +// Delete records with where clause +func (s *Session) Delete() (int64, error) { + s.clause.Set(clause.DELETE, s.RefTable().Name) + sql, vars := s.clause.Build(clause.DELETE, clause.WHERE) + result, err := s.Raw(sql, vars...).Exec() + if err != nil { + return 0, err + } + return result.RowsAffected() +} +``` + +### 1.4 Count 方法 + +```go +// Count records with where clause +func (s *Session) Count() (int64, error) { + s.clause.Set(clause.COUNT, s.RefTable().Name) + sql, vars := s.clause.Build(clause.COUNT, clause.WHERE) + row := s.Raw(sql, vars...).QueryRow() + var tmp int64 + if err := row.Scan(&tmp); err != nil { + return 0, err + } + return tmp, nil +} +``` + +## 2 链式调用(chain) + +链式调用是一种简化代码的编程方式,能够使代码更简洁、易读。链式调用的原理也非常简单,某个对象调用某个方法后,将该对象的引用/指针返回,即可以继续调用该对象的其他方法。通常来说,当某个对象需要一次调用多个方法来设置其属性时,就非常适合改造为链式调用了。 + +SQL 语句的构造过程就非常符合这个条件。SQL 语句由多个子句构成,典型的例如 SELECT 语句,往往需要设置查询条件(WHERE)、限制返回行数(LIMIT)等。理想的调用方式应该是这样的: + +```go +s := geeorm.NewEngine("sqlite3", "gee.db").NewSession() +var users []User +s.Where("Age > 18").Limit(3).Find(&users) +``` + +从上面的示例中,可以看出,`WHERE`、`LIMIT`、`ORDER BY` 等查询条件语句非常适合链式调用。这几个子句的 generator 在之前就已经实现了,那我们接下来在 `session/record.go` 中添加对应的方法即可。 + +[day4-chain-operation/session/record.go](https://github.com/geektutu/7days-golang/tree/master/gee-orm/day4-chain-operation/session) + +```go +// Limit adds limit condition to clause +func (s *Session) Limit(num int) *Session { + s.clause.Set(clause.LIMIT, num) + return s +} + +// Where adds limit condition to clause +func (s *Session) Where(desc string, args ...interface{}) *Session { + var vars []interface{} + s.clause.Set(clause.WHERE, append(append(vars, desc), args...)...) + return s +} + +// OrderBy adds order by condition to clause +func (s *Session) OrderBy(desc string) *Session { + s.clause.Set(clause.ORDERBY, desc) + return s +} +``` + +## 3 First 只返回一条记录 + +很多时候,我们期望 SQL 语句只返回一条记录,比如根据某个童鞋的学号查询他的信息,返回结果有且只有一条。结合链式调用,我们可以非常容易地实现 First 方法。 + +```go +func (s *Session) First(value interface{}) error { + dest := reflect.Indirect(reflect.ValueOf(value)) + destSlice := reflect.New(reflect.SliceOf(dest.Type())).Elem() + if err := s.Limit(1).Find(destSlice.Addr().Interface()); err != nil { + return err + } + if destSlice.Len() == 0 { + return errors.New("NOT FOUND") + } + dest.Set(destSlice.Index(0)) + return nil +} +``` + +First 方法可以这么使用: + +```go +u := &User{} +_ = s.OrderBy("Age DESC").First(u) +``` + +> 实现原理:根据传入的类型,利用反射构造切片,调用 `Limit(1)` 限制返回的行数,调用 `Find` 方法获取到查询结果。 + +## 4 测试 + +接下来呢,我们在 `record_test.go` 中添加几个测试用例,检测功能是否正常。 + +```go +package session + +import "testing" + +var ( + user1 = &User{"Tom", 18} + user2 = &User{"Sam", 25} + user3 = &User{"Jack", 25} +) + +func testRecordInit(t *testing.T) *Session { + t.Helper() + s := NewSession().Model(&User{}) + err1 := s.DropTable() + err2 := s.CreateTable() + _, err3 := s.Insert(user1, user2) + if err1 != nil || err2 != nil || err3 != nil { + t.Fatal("failed init test records") + } + return s +} + +func TestSession_Limit(t *testing.T) { + s := testRecordInit(t) + var users []User + err := s.Limit(1).Find(&users) + if err != nil || len(users) != 1 { + t.Fatal("failed to query with limit condition") + } +} + +func TestSession_Update(t *testing.T) { + s := testRecordInit(t) + affected, _ := s.Where("Name = ?", "Tom").Update("Age", 30) + u := &User{} + _ = s.OrderBy("Age DESC").First(u) + + if affected != 1 || u.Age != 30 { + t.Fatal("failed to update") + } +} + +func TestSession_DeleteAndCount(t *testing.T) { + s := testRecordInit(t) + affected, _ := s.Where("Name = ?", "Tom").Delete() + count, _ := s.Count() + + if affected != 1 || count != 1 { + t.Fatal("failed to delete or count") + } +} +``` + +## 附 推荐阅读 + +- [Go 语言简明教程](https://geektutu.com/post/quick-golang.html) +- [Go Test 单元测试简明教程](https://geektutu.com/post/quick-go-test.html) +- [SQLite 常用命令速查表](https://geektutu.com/post/cheat-sheet-sqlite.html) \ No newline at end of file diff --git a/gee-orm/doc/geeorm-day5.md b/gee-orm/doc/geeorm-day5.md index cbb86cd..e0bf9e2 100644 --- a/gee-orm/doc/geeorm-day5.md +++ b/gee-orm/doc/geeorm-day5.md @@ -1,6 +1,6 @@ --- title: 动手写ORM框架 - GeeORM第五天 实现钩子(Hooks) -date: 2020-03-03 23:00:00 +date: 2020-03-08 18:00:00 description: 7天用 Go语言/golang 从零实现 ORM 框架 GeeORM 教程(7 days implement golang object relational mapping framework from scratch tutorial),动手写 ORM 框架,参照 gorm, xorm 的实现。通过反射(reflect)获取结构体绑定的钩子(hooks),并调用;支持增删查改(CRUD)前后调用钩子。 tags: - Go @@ -16,10 +16,140 @@ keywords: - BeforeUpdate image: post/geeorm/geeorm_sm.jpg github: https://github.com/geektutu/7days-golang -published: false --- 本文是[7天用Go从零实现ORM框架GeeORM](https://geektutu.com/post/geeorm.html)的第五篇。 - 通过反射(reflect)获取结构体绑定的钩子(hooks),并调用。 -- 支持增删查改(CRUD)前后调用钩子。 \ No newline at end of file +- 支持增删查改(CRUD)前后调用钩子。**代码约50行** + +## 1 Hook 机制 + +Hook,翻译为钩子,其主要思想是提前在可能增加功能的地方埋好(预设)一个钩子,当我们需要重新修改或者增加这个地方的逻辑的时候,把扩展的类或者方法挂载到这个点即可。钩子的应用非常广泛,例如 Github 支持的 travis 持续集成服务,当有 `git push` 事件发生时,会触发 travis 拉取新的代码进行构建。IDE 中钩子也非常常见,比如,当按下 `Ctrl + s` 后,自动格式化代码。再比如前端常用的 `hot reload` 机制,前端代码发生变更时,自动编译打包,通知浏览器自动刷新页面,实现所写即所得。 + +钩子机制设计的好坏,取决于扩展点选择的是否合适。例如对于持续集成来说,代码如果不发生变更,反复构建是没有意义的,因此钩子应设计在代码可能发生变更的地方,比如 MR、PR 合并前后。 + +那对于 ORM 框架来说,合适的扩展点在哪里呢?很显然,记录的增删查改前后都是非常合适的。 + +比如,我们设计一个 `Account` 类,`Account` 包含有一个隐私字段 `Password`,那么每次查询后都需要做脱敏处理,才能继续使用。如果提供了 `AfterQuery` 的钩子,查询后,自动地将 `Password` 字段的值脱敏,是不是能省去很多冗余的代码呢? + +## 2 实现钩子 + +GeeORM 的钩子与结构体绑定,即每个结构体需要实现各自的钩子。hook 相关的代码实现在 `session/hooks.go` 中。 + +[day5-hooks/session/hooks.go](https://github.com/geektutu/7days-golang/tree/master/gee-orm/day5-hooks/session) + +```go +package session + +import ( + "geeorm/log" + "reflect" +) + +// Hooks constants +const ( + BeforeQuery = "BeforeQuery" + AfterQuery = "AfterQuery" + BeforeUpdate = "BeforeUpdate" + AfterUpdate = "AfterUpdate" + BeforeDelete = "BeforeDelete" + AfterDelete = "AfterDelete" + BeforeInsert = "BeforeInsert" + AfterInsert = "AfterInsert" +) + +// CallMethod calls the registered hooks +func (s *Session) CallMethod(method string, value interface{}) { + fm := reflect.ValueOf(s.RefTable().Model).MethodByName(method) + if value != nil { + fm = reflect.ValueOf(value).MethodByName(method) + } + param := []reflect.Value{reflect.ValueOf(s)} + if fm.IsValid() { + if v := fm.Call(param); len(v) > 0 { + if err, ok := v[0].Interface().(error); ok { + log.Error(err) + } + } + } + return +} +``` + +- 钩子机制同样是通过反射来实现的,`s.RefTable().Model` 或 `value` 即当前会话正在操作的对象,使用 `MethodByName` 方法反射得到该对象的方法。 +- 将 `s *Session` 作为入参调用。每一个钩子的入参类型均是 `*Session`。 + +接下来,将 `CallMethod()` 方法在 Find、Insert、Update、Delete 方法内部调用即可。例如,`Find` 方法修改为: + +```go +// Find gets all eligible records +func (s *Session) Find(values interface{}) error { + s.CallMethod(BeforeQuery, nil) + // ... + for rows.Next() { + dest := reflect.New(destType).Elem() + // ... + s.CallMethod(AfterQuery, dest.Addr().Interface()) + // ... + } + return rows.Close() +} +``` + +- `AfterQuery` 钩子可以操作每一行记录。 + +## 3 测试 + +新建 `session/hooks.go` 文件添加对应的测试用例。 + +```go +package session + +import ( + "geeorm/log" + "testing" +) + +type Account struct { + ID int `geeorm:"PRIMARY KEY"` + Password string +} + +func (account *Account) BeforeInsert(s *Session) error { + log.Info("before inert", account) + account.ID += 1000 + return nil +} + +func (account *Account) AfterQuery(s *Session) error { + log.Info("after query", account) + account.Password = "******" + return nil +} + +func TestSession_CallMethod(t *testing.T) { + s := NewSession().Model(&Account{}) + _ = s.DropTable() + _ = s.CreateTable() + _, _ = s.Insert(&Account{1, "123456"}, &Account{2, "qwerty"}) + + u := &Account{} + + err := s.First(u) + if err != nil || u.ID != 1001 || u.Password != "******" { + t.Fatal("Failed to call hooks after query, got", u) + } +} +``` + +在这个测试用例中,测试了 `BeforeInsert` 和 `AfterQuery` 2 个钩子。 + +- `BeforeInsert` 将 account.ID 的值增加 1000 +- `AfterQuery` 将密码脱敏,显示为 6 个 `*`。 + +## 附 推荐阅读 + +- [Go 语言简明教程](https://geektutu.com/post/quick-golang.html) +- [Go Test 单元测试简明教程](https://geektutu.com/post/quick-go-test.html) +- [SQLite 常用命令速查表](https://geektutu.com/post/cheat-sheet-sqlite.html) \ No newline at end of file diff --git a/gee-orm/doc/geeorm-day6.md b/gee-orm/doc/geeorm-day6.md index e6f6f03..37955d3 100644 --- a/gee-orm/doc/geeorm-day6.md +++ b/gee-orm/doc/geeorm-day6.md @@ -1,7 +1,7 @@ --- title: 动手写ORM框架 - GeeORM第六天 支持事务(Transaction) -date: 2020-03-03 23:00:00 -description: 7天用 Go语言/golang 从零实现 ORM 框架 GeeORM 教程(7 days implement golang object relational mapping framework from scratch tutorial),动手写 ORM 框架,参照 gorm, xorm 的实现。介绍 SQLite 数据库中的事务(transaction);封装事务,用户自定义回调函数实现原子操作。 +date: 2020-03-08 21:00:00 +description: 7天用 Go语言/golang 从零实现 ORM 框架 GeeORM 教程(7 days implement golang object relational mapping framework from scratch tutorial),动手写 ORM 框架,参照 gorm, xorm 的实现。介绍数据库中的事务(transaction);封装事务,用户自定义回调函数实现原子操作。 tags: - Go nav: 从零实现 @@ -15,10 +15,257 @@ keywords: - transaction image: post/geeorm/geeorm_sm.jpg github: https://github.com/geektutu/7days-golang -published: false --- 本文是[7天用Go从零实现ORM框架GeeORM](https://geektutu.com/post/geeorm.html)的第六篇。 -- 介绍 SQLite 数据库中的事务(transaction)。 -- 封装事务,用户自定义回调函数实现原子操作。 \ No newline at end of file +- 介绍数据库中的事务(transaction)。 +- 封装事务,用户自定义回调函数实现原子操作。**代码约100行** + +## 1 事务的 ACID 属性 + +> 数据库事务(transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务由事务开始与事务结束之间执行的全部数据库操作组成。 + +举一个简单的例子,转账。A 转账给 B 一万元,那么数据库至少需要执行 2 个操作: + +- 1)A 的账户减掉一万元。 +- 2)B 的账户增加一万元。 + +这两个操作要么全部执行,代表转账成功。任意一个操作失败了,之前的操作都必须回退,代表转账失败。一个操作完成,另一个操作失败,这种结果是不能够接受的。这种场景就非常适合利用数据库事务的特性来解决。 + +如果一个数据库支持事务,那么必须具备 ACID 四个属性。 + +- 1)原子性(Atomicity):事务中的全部操作在数据库中是不可分割的,要么全部完成,要么全部不执行。 +- 2)一致性(Consistency): 几个并行执行的事务,其执行结果必须与按某一顺序 串行执行的结果相一致。 +- 3)隔离性(Isolation):事务的执行不受其他事务的干扰,事务执行的中间结果对其他事务必须是透明的。 +- 4)持久性(Durability):对于任意已提交事务,系统必须保证该事务对数据库的改变不被丢失,即使数据库出现故障。 + +## 2 SQLite 和 Go 标准库中的事务 + +SQLite 中创建一个事务的原生 SQL 长什么样子呢? + +```sql +sqlite> BEGIN; +sqlite> DELETE FROM User WHERE Age > 25; +sqlite> INSERT INTO User VALUES ("Tom", 25), ("Jack", 18); +sqlite> COMMIT; +``` + +`BEGIN` 开启事务,`COMMIT` 提交事务,`ROLLBACK` 回滚事务。任何一个事务,均以 `BEGIN` 开始,`COMMIT` 或 `ROLLBACK` 结束。 + +Go 语言标准库 database/sql 提供了支持事务的接口。用一个简单的例子,看一看 Go 语言标准是如何支持事务的。 + +```go +package main + +import ( + "database/sql" + _ "github.com/mattn/go-sqlite3" + "log" +) + +func main() { + db, _ := sql.Open("sqlite3", "gee.db") + defer func() { _ = db.Close() }() + _, _ = db.Exec("CREATE TABLE IF NOT EXISTS User(`Name` text);") + + tx, _ := db.Begin() + _, err1 := tx.Exec("INSERT INTO User(`Name`) VALUES (?)", "Tom") + _, err2 := tx.Exec("INSERT INTO User(`Name`) VALUES (?)", "Jack") + if err1 != nil || err2 != nil { + _ = tx.Rollback() + log.Println("Rollback", err1, err2) + } else { + _ = tx.Commit() + log.Println("Commit") + } +} +``` + +Go 语言中实现事务和 SQL 原生语句其实是非常接近的。调用 `db.Begin()` 得到 `*sql.Tx` 对象,使用 `tx.Exec()` 执行一系列操作,如果发生错误,通过 `tx.Rollback()` 回滚,如果没有发生错误,则通过 `tx.Commit()` 提交。 + +## 3 GeeORM 支持事务 + +GeeORM 之前的操作均是执行完即自动提交的,每个操作是相互独立的。之前直接使用 `sql.DB` 对象执行 SQL 语句,如果要支持事务,需要更改为 `sql.Tx` 执行。在 Session 结构体中新增成员变量 `tx *sql.Tx`,当 `tx` 不为空时,则使用 `tx` 执行 SQL 语句,否则使用 `db` 执行 SQL 语句。这样既兼容了原有的执行方式,又提供了对事务的支持。 + +[day6-transaction/session/raw.go](https://github.com/geektutu/7days-golang/tree/master/gee-orm/day6-transaction/session) + +```go +type Session struct { + db *sql.DB + dialect dialect.Dialect + tx *sql.Tx + refTable *schema.Schema + clause clause.Clause + sql strings.Builder + sqlVars []interface{} +} + +// CommonDB is a minimal function set of db +type CommonDB interface { + Query(query string, args ...interface{}) (*sql.Rows, error) + QueryRow(query string, args ...interface{}) *sql.Row + Exec(query string, args ...interface{}) (sql.Result, error) +} + +var _ CommonDB = (*sql.DB)(nil) +var _ CommonDB = (*sql.Tx)(nil) + +// DB returns tx if a tx begins. otherwise return *sql.DB +func (s *Session) DB() CommonDB { + if s.tx != nil { + return s.tx + } + return s.db +} +``` + +新建文件 `session/transaction.go` 封装事务的 Begin、Commit 和 Rollback 三个接口。 + +[day6-transaction/session/transaction.go](https://github.com/geektutu/7days-golang/tree/master/gee-orm/day6-transaction/session) + +```go +package session + +import "geeorm/log" + +func (s *Session) Begin() (err error) { + log.Info("transaction begin") + if s.tx, err = s.db.Begin(); err != nil { + log.Error(err) + return + } + return +} + +func (s *Session) Commit() (err error) { + log.Info("transaction commit") + if err = s.tx.Commit(); err != nil { + log.Error(err) + } + return +} + +func (s *Session) Rollback() (err error) { + log.Info("transaction rollback") + if err = s.tx.Rollback(); err != nil { + log.Error(err) + } + return +} +``` + +- 调用 `s.db.Begin()` 得到 `*sql.Tx` 对象,赋值给 s.tx。 +- 封装的另一个目的是统一打印日志,方便定位问题。 + + +最后一步,在 `geeorm.go` 中为用户提供傻瓜式/一键式使用的接口。 + +[day6-transaction/geeorm.go](https://github.com/geektutu/7days-golang/tree/master/gee-orm/day6-transaction) + +```go +type TxFunc func(*session.Session) (interface{}, error) + +func (engine *Engine) Transaction(f TxFunc) (result interface{}, err error) { + s := engine.NewSession() + if err := s.Begin(); err != nil { + return nil, err + } + defer func() { + if p := recover(); p != nil { + _ = s.Rollback() + panic(p) // re-throw panic after Rollback + } else if err != nil { + _ = s.Rollback() // err is non-nil; don't change it + } else { + err = s.Commit() // err is nil; if Commit returns error update err + } + }() + + return f(s) +} +``` + +> Transaction 的实现参考了 [stackoverflow](https://stackoverflow.com/questions/16184238/database-sql-tx-detecting-commit-or-rollback) + +用户只需要将所有的操作放到一个回调函数中,作为入参传递给 `engine.Transaction()`,发生任何错误,自动回滚,如果没有错误发生,则提交。 + +## 4 测试 + +在 `geeorm_test.go` 中添加测试用例看看 Transaction 如何工作的吧。 + +```go +func OpenDB(t *testing.T) *Engine { + t.Helper() + engine, err := NewEngine("sqlite3", "gee.db") + if err != nil { + t.Fatal("failed to connect", err) + } + return engine +} + +type User struct { + Name string `geeorm:"PRIMARY KEY"` + Age int +} + +func TestEngine_Transaction(t *testing.T) { + t.Run("rollback", func(t *testing.T) { + transactionRollback(t) + }) + t.Run("commit", func(t *testing.T) { + transactionCommit(t) + }) +} +``` + +首先是 rollback 的用例: + +```go +func transactionRollback(t *testing.T) { + engine := OpenDB(t) + defer engine.Close() + s := engine.NewSession() + _ = s.Model(&User{}).DropTable() + _, err := engine.Transaction(func(s *session.Session) (result interface{}, err error) { + _ = s.Model(&User{}).CreateTable() + _, err = s.Insert(&User{"Tom", 18}) + return nil, errors.New("Error") + }) + if err == nil || s.HasTable() { + t.Fatal("failed to rollback") + } +} +``` + +- 在这个用例中,如何执行成功,则会创建一张表 `User`,并插入一条记录。 +- 故意返回了一个自定义 error,最终事务回滚,表创建失败。 + +接下来是 commit 的用例: + +```go +func transactionCommit(t *testing.T) { + engine := OpenDB(t) + defer engine.Close() + s := engine.NewSession() + _ = s.Model(&User{}).DropTable() + _, err := engine.Transaction(func(s *session.Session) (result interface{}, err error) { + _ = s.Model(&User{}).CreateTable() + _, err = s.Insert(&User{"Tom", 18}) + return + }) + u := &User{} + _ = s.First(u) + if err != nil || u.Name != "Tom" { + t.Fatal("failed to commit") + } +} +``` + +- 创建表和插入记录均成功执行,最终通过 `s.First()` 方法查询到插入的记录。 + +## 附 推荐阅读 + +- [Go 语言简明教程](https://geektutu.com/post/quick-golang.html) +- [Go Test 单元测试简明教程](https://geektutu.com/post/quick-go-test.html) +- [SQLite 常用命令速查表](https://geektutu.com/post/cheat-sheet-sqlite.html) \ No newline at end of file diff --git a/gee-orm/doc/geeorm-day7.md b/gee-orm/doc/geeorm-day7.md index 6e2bf40..44079fd 100644 --- a/gee-orm/doc/geeorm-day7.md +++ b/gee-orm/doc/geeorm-day7.md @@ -1,6 +1,6 @@ --- title: 动手写ORM框架 - GeeORM第七天 数据库迁移(Migrate) -date: 2020-03-03 23:00:00 +date: 2020-03-08 23:00:00 description: 7天用 Go语言/golang 从零实现 ORM 框架 GeeORM 教程(7 days implement golang object relational mapping framework from scratch tutorial),动手写 ORM 框架,参照 gorm, xorm 的实现。结构体(struct)变更时,数据库表的字段(field)自动迁移(migrate);仅支持字段新增与删除,不支持字段类型变更。 tags: - Go @@ -15,10 +15,154 @@ keywords: - migrate image: post/geeorm/geeorm_sm.jpg github: https://github.com/geektutu/7days-golang -published: false --- 本文是[7天用Go从零实现ORM框架GeeORM](https://geektutu.com/post/geeorm.html)的第七篇。 - 结构体(struct)变更时,数据库表的字段(field)自动迁移(migrate)。 -- 仅支持字段新增与删除,不支持字段类型变更。 \ No newline at end of file +- 仅支持字段新增与删除,不支持字段类型变更。**代码约70行** + +## 1 使用 SQL 语句 Migrate + +数据库 Migrate 一直是数据库运维人员最为头痛的问题,如果仅仅是一张表增删字段还比较容易,那如果涉及到外键等复杂的关联关系,数据库的迁移就会变得非常困难。 + +GeeORM 的 Migrate 操作仅针对最为简单的场景,即支持字段的新增与删除,不支持字段类型变更。 + +在实现 Migrate 之前,我们先看看如何使用原生的 SQL 语句增删字段。 + +### 1.1 新增字段 + +```sql +ALTER TABLE table_name ADD COLUMN col_name, col_type; +``` + +大部分数据支持使用 `ALTER` 关键字新增字段,或者重命名字段。 + +### 1.2 删除字段 + +> 参考 [sqlite delete or add column - stackoverflow](https://stackoverflow.com/questions/8442147/how-to-delete-or-add-column-in-sqlite) + +对于 SQLite 来说,删除字段并不像新增字段那么容易,一个比较可行的方法需要执行下列几个步骤: + +```sql +CREATE TABLE new_table AS SELECT col1, col2, ... from old_table +DROP TABLE old_table +ALTER TABLE new_table RENAME TO old_table; +``` + +- 第一步:从 `old_table` 中挑选需要保留的字段到 `new_table` 中。 +- 第二步:删除 `old_table`。 +- 第三步:重命名 `new_table` 为 `old_table`。 + +## 2 GeeORM 实现 Migrate + +按照原生的 SQL 命令,利用之前实现的事务,在 `geeorm.go` 中实现 Migrate 方法。 + +```go +// difference returns a - b +func difference(a []string, b []string) (diff []string) { + mapB := make(map[string]bool) + for _, v := range b { + mapB[v] = true + } + for _, v := range a { + if _, ok := mapB[v]; !ok { + diff = append(diff, v) + } + } + return +} + +// Migrate table +func (engine *Engine) Migrate(value interface{}) error { + _, err := engine.Transaction(func(s *session.Session) (result interface{}, err error) { + if !s.Model(value).HasTable() { + log.Infof("table %s doesn't exist", s.RefTable().Name) + return nil, s.CreateTable() + } + table := s.RefTable() + rows, _ := s.Raw(fmt.Sprintf("SELECT * FROM %s LIMIT 1", table.Name)).QueryRows() + columns, _ := rows.Columns() + addCols := difference(table.FieldNames, columns) + delCols := difference(columns, table.FieldNames) + log.Infof("added cols %v, deleted cols %v", addCols, delCols) + + for _, col := range addCols { + f := table.GetField(col) + sqlStr := fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s %s;", table.Name, f.Name, f.Tag) + if _, err = s.Raw(sqlStr).Exec(); err != nil { + return + } + } + + if len(delCols) == 0 { + return + } + tmp := "tmp_" + table.Name + fieldStr := strings.Join(table.FieldNames, ", ") + s.Raw(fmt.Sprintf("CREATE TABLE %s AS SELECT %s from %s;", tmp, fieldStr, table.Name)) + s.Raw(fmt.Sprintf("DROP TABLE %s;", table.Name)) + s.Raw(fmt.Sprintf("ALTER TABLE %s RENAME TO %s;", tmp, table.Name)) + _, err = s.Exec() + return + }) + return err +} +``` + +- `difference` 用来计算前后两个字段切片的差集。新表 - 旧表 = 新增字段,旧表 - 新表 = 删除字段。 +- 使用 `ALTER` 语句新增字段。 +- 使用创建新表并重命名的方式删除字段。 + +## 3 测试 + +在 `geeorm_test.go` 中添加 Migrate 的测试用例: + +```go +type User struct { + Name string `geeorm:"PRIMARY KEY"` + Age int +} + +func TestEngine_Migrate(t *testing.T) { + engine := OpenDB(t) + defer engine.Close() + s := engine.NewSession() + _, _ = s.Raw("DROP TABLE IF EXISTS User;").Exec() + _, _ = s.Raw("CREATE TABLE User(Name text PRIMARY KEY, XXX integer);").Exec() + _, _ = s.Raw("INSERT INTO User(`Name`) values (?), (?)", "Tom", "Sam").Exec() + engine.Migrate(&User{}) + + rows, _ := s.Raw("SELECT * FROM User").QueryRows() + columns, _ := rows.Columns() + if !reflect.DeepEqual(columns, []string{"Name", "Age"}) { + t.Fatal("Failed to migrate table User, got columns", columns) + } +} +``` + +- 首先假设原有的 `User` 包含两个字段 `Name` 和 `XXX`,在一次业务变更之后,`User` 结构体的字段变更为 `Name` 和 `Age`。 +- 即需要删除原有字段 `XXX`,并新增字段 `Age`。 +- 调用 `Migrate(&User{})` 之后,新表的结构为 `Name`,`Age` + +## 4 总结 + +GeeORM 的整体实现比较粗糙,比如数据库的迁移仅仅考虑了最简单的场景。实现的特性也比较少,比如结构体嵌套的场景,外键的场景,复合主键的场景都没有覆盖。ORM 框架的代码规模一般都比较大,如果想尽可能地逼近数据库,就需要大量的代码来实现相关的特性;二是数据库之间的差异也是比较大的,实现的功能越多,数据库之间的差异就会越突出,有时候为了达到较好的性能,就不得不为每个数据做特殊处理;还有些 ORM 框架同时支持关系型数据库和非关系型数据库,这就要求框架本身有更高层次的抽象,不能局限在 SQL 这一层。 + +GeeORM 仅 800 左右的代码是不可能做到这一点的。不过,GeeORM 的目的并不是实现一个可以在生产使用的 ORM 框架,而是希望尽可能多地介绍 ORM 框架大致的实现原理,例如 + +- 在框架中如何屏蔽不同数据库之间的差异; +- 数据库中表结构和编程语言中的对象是如何映射的; +- 如何优雅地模拟查询条件,链式调用是个不错的选择; +- 为什么 ORM 框架通常会提供 hooks 扩展的能力; +- 事务的原理和 ORM 框架如何集成对事务的支持; +- 一些难点问题,例如数据库迁移。 +- ... + +基于这几点,我觉得 GeeORM 的目的达到了。 + +## 附 推荐阅读 + +- [Go Test 单元测试简明教程](https://geektutu.com/post/quick-go-test.html) +- [SQLite 常用命令速查表](https://geektutu.com/post/cheat-sheet-sqlite.html) +- [sqlite delete or add column - stackoverflow](https://stackoverflow.com/questions/8442147/how-to-delete-or-add-column-in-sqlite) diff --git a/gee-orm/doc/geeorm.md b/gee-orm/doc/geeorm.md index c25af6c..f9c14da 100644 --- a/gee-orm/doc/geeorm.md +++ b/gee-orm/doc/geeorm.md @@ -122,13 +122,13 @@ gorm 正在彻底重构 v1 版本,短期内看不到发布 v2 的可能。相 ## 3 目录 -- 第一天:database/sql 基础 | [Code](https://github.com/geektutu/7days-golang/blob/master/gee-orm/day1-database-sql) -- 第二天:对象表结构映射 | [Code](https://github.com/geektutu/7days-golang/blob/master/gee-orm/day2-reflect-schema) -- 第三天:插入/查询记录 | [Code](https://github.com/geektutu/7days-golang/blob/master/gee-orm/day3-save-query) -- 第四天:链式操作与更新删除 | [Code](https://github.com/geektutu/7days-golang/blob/master/gee-orm/day4-chain-operation) -- 第五天:实现钩子(Hooks) | [Code](https://github.com/geektutu/7days-golang/blob/master/gee-orm/day5-hooks) -- 第六天:支持事务(Transaction) | [Code](https://github.com/geektutu/7days-golang/blob/master/gee-orm/day6-transaction) -- 第七天:数据库迁移(Migrate) | [Code](https://github.com/geektutu/7days-golang/blob/master/gee-orm/day7-migrate) +- 第一天:[database/sql 基础](https://geektutu.com/post/geeorm-day1.html) | [Code](https://github.com/geektutu/7days-golang/blob/master/gee-orm/day1-database-sql) +- 第二天:[对象表结构映射](https://geektutu.com/post/geeorm-day2.html) | [Code](https://github.com/geektutu/7days-golang/blob/master/gee-orm/day2-reflect-schema) +- 第三天:[记录新增和查询](https://geektutu.com/post/geeorm-day3.html) | [Code](https://github.com/geektutu/7days-golang/blob/master/gee-orm/day3-save-query) +- 第四天:[链式操作与更新删除](https://geektutu.com/post/geeorm-day4.html) | [Code](https://github.com/geektutu/7days-golang/blob/master/gee-orm/day4-chain-operation) +- 第五天:[实现钩子(Hooks)](https://geektutu.com/post/geeorm-day5.html) | [Code](https://github.com/geektutu/7days-golang/blob/master/gee-orm/day5-hooks) +- 第六天:[支持事务(Transaction)](https://geektutu.com/post/geeorm-day6.html) | [Code](https://github.com/geektutu/7days-golang/blob/master/gee-orm/day6-transaction) +- 第七天:[数据库迁移(Migrate)](https://geektutu.com/post/geeorm-day7.html) | [Code](https://github.com/geektutu/7days-golang/blob/master/gee-orm/day7-migrate) ## 附 推荐阅读 From b9f5de709c2b5ce49405bfc5e45d2e9ce82f2c4b Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Mon, 9 Mar 2020 01:41:19 +0800 Subject: [PATCH 061/122] publish day3 --- gee-orm/doc/geeorm-day3.md | 1 - 1 file changed, 1 deletion(-) diff --git a/gee-orm/doc/geeorm-day3.md b/gee-orm/doc/geeorm-day3.md index ff089e1..85f5f6f 100644 --- a/gee-orm/doc/geeorm-day3.md +++ b/gee-orm/doc/geeorm-day3.md @@ -16,7 +16,6 @@ keywords: - select from image: post/geeorm/geeorm_sm.jpg github: https://github.com/geektutu/7days-golang -published: false --- 本文是[7天用Go从零实现ORM框架GeeORM](https://geektutu.com/post/geeorm.html)的第三篇。 From eaf66a3933f7d125a1e0f8ad4718293f3f2484f2 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Sat, 14 Mar 2020 11:12:50 +0800 Subject: [PATCH 062/122] fix typo --- gee-cache/day1-lru/geecache/lru/lru.go | 9 ++++----- gee-cache/day2-single-node/geecache/lru/lru.go | 9 ++++----- gee-cache/day3-http-server/geecache/lru/lru.go | 9 ++++----- gee-cache/day4-consistent-hash/geecache/lru/lru.go | 9 ++++----- gee-cache/day5-multi-nodes/geecache/lru/lru.go | 9 ++++----- gee-cache/day6-single-flight/geecache/lru/lru.go | 9 ++++----- gee-cache/day7-proto-buf/geecache/lru/lru.go | 9 ++++----- gee-cache/doc/geecache-day1.md | 9 ++++----- gee-orm/day7-migrate/geeorm.go | 2 +- gee-orm/doc/geeorm-day7.md | 2 +- gee-web/doc/gee-day3.md | 2 +- 11 files changed, 35 insertions(+), 43 deletions(-) diff --git a/gee-cache/day1-lru/geecache/lru/lru.go b/gee-cache/day1-lru/geecache/lru/lru.go index 6fa617a..dc1a317 100644 --- a/gee-cache/day1-lru/geecache/lru/lru.go +++ b/gee-cache/day1-lru/geecache/lru/lru.go @@ -39,12 +39,11 @@ func (c *Cache) Add(key string, value Value) { kv := ele.Value.(*entry) c.nbytes += int64(value.Len()) - int64(kv.value.Len()) kv.value = value - return + } else { + ele := c.ll.PushFront(&entry{key, value}) + c.cache[key] = ele + c.nbytes += int64(len(key)) + int64(value.Len()) } - ele := c.ll.PushFront(&entry{key, value}) - c.cache[key] = ele - c.nbytes += int64(len(key)) + int64(value.Len()) - for c.maxBytes != 0 && c.maxBytes < c.nbytes { c.RemoveOldest() } diff --git a/gee-cache/day2-single-node/geecache/lru/lru.go b/gee-cache/day2-single-node/geecache/lru/lru.go index 6fa617a..dc1a317 100644 --- a/gee-cache/day2-single-node/geecache/lru/lru.go +++ b/gee-cache/day2-single-node/geecache/lru/lru.go @@ -39,12 +39,11 @@ func (c *Cache) Add(key string, value Value) { kv := ele.Value.(*entry) c.nbytes += int64(value.Len()) - int64(kv.value.Len()) kv.value = value - return + } else { + ele := c.ll.PushFront(&entry{key, value}) + c.cache[key] = ele + c.nbytes += int64(len(key)) + int64(value.Len()) } - ele := c.ll.PushFront(&entry{key, value}) - c.cache[key] = ele - c.nbytes += int64(len(key)) + int64(value.Len()) - for c.maxBytes != 0 && c.maxBytes < c.nbytes { c.RemoveOldest() } diff --git a/gee-cache/day3-http-server/geecache/lru/lru.go b/gee-cache/day3-http-server/geecache/lru/lru.go index 6fa617a..dc1a317 100644 --- a/gee-cache/day3-http-server/geecache/lru/lru.go +++ b/gee-cache/day3-http-server/geecache/lru/lru.go @@ -39,12 +39,11 @@ func (c *Cache) Add(key string, value Value) { kv := ele.Value.(*entry) c.nbytes += int64(value.Len()) - int64(kv.value.Len()) kv.value = value - return + } else { + ele := c.ll.PushFront(&entry{key, value}) + c.cache[key] = ele + c.nbytes += int64(len(key)) + int64(value.Len()) } - ele := c.ll.PushFront(&entry{key, value}) - c.cache[key] = ele - c.nbytes += int64(len(key)) + int64(value.Len()) - for c.maxBytes != 0 && c.maxBytes < c.nbytes { c.RemoveOldest() } diff --git a/gee-cache/day4-consistent-hash/geecache/lru/lru.go b/gee-cache/day4-consistent-hash/geecache/lru/lru.go index 6fa617a..dc1a317 100644 --- a/gee-cache/day4-consistent-hash/geecache/lru/lru.go +++ b/gee-cache/day4-consistent-hash/geecache/lru/lru.go @@ -39,12 +39,11 @@ func (c *Cache) Add(key string, value Value) { kv := ele.Value.(*entry) c.nbytes += int64(value.Len()) - int64(kv.value.Len()) kv.value = value - return + } else { + ele := c.ll.PushFront(&entry{key, value}) + c.cache[key] = ele + c.nbytes += int64(len(key)) + int64(value.Len()) } - ele := c.ll.PushFront(&entry{key, value}) - c.cache[key] = ele - c.nbytes += int64(len(key)) + int64(value.Len()) - for c.maxBytes != 0 && c.maxBytes < c.nbytes { c.RemoveOldest() } diff --git a/gee-cache/day5-multi-nodes/geecache/lru/lru.go b/gee-cache/day5-multi-nodes/geecache/lru/lru.go index 6fa617a..dc1a317 100644 --- a/gee-cache/day5-multi-nodes/geecache/lru/lru.go +++ b/gee-cache/day5-multi-nodes/geecache/lru/lru.go @@ -39,12 +39,11 @@ func (c *Cache) Add(key string, value Value) { kv := ele.Value.(*entry) c.nbytes += int64(value.Len()) - int64(kv.value.Len()) kv.value = value - return + } else { + ele := c.ll.PushFront(&entry{key, value}) + c.cache[key] = ele + c.nbytes += int64(len(key)) + int64(value.Len()) } - ele := c.ll.PushFront(&entry{key, value}) - c.cache[key] = ele - c.nbytes += int64(len(key)) + int64(value.Len()) - for c.maxBytes != 0 && c.maxBytes < c.nbytes { c.RemoveOldest() } diff --git a/gee-cache/day6-single-flight/geecache/lru/lru.go b/gee-cache/day6-single-flight/geecache/lru/lru.go index 6fa617a..dc1a317 100644 --- a/gee-cache/day6-single-flight/geecache/lru/lru.go +++ b/gee-cache/day6-single-flight/geecache/lru/lru.go @@ -39,12 +39,11 @@ func (c *Cache) Add(key string, value Value) { kv := ele.Value.(*entry) c.nbytes += int64(value.Len()) - int64(kv.value.Len()) kv.value = value - return + } else { + ele := c.ll.PushFront(&entry{key, value}) + c.cache[key] = ele + c.nbytes += int64(len(key)) + int64(value.Len()) } - ele := c.ll.PushFront(&entry{key, value}) - c.cache[key] = ele - c.nbytes += int64(len(key)) + int64(value.Len()) - for c.maxBytes != 0 && c.maxBytes < c.nbytes { c.RemoveOldest() } diff --git a/gee-cache/day7-proto-buf/geecache/lru/lru.go b/gee-cache/day7-proto-buf/geecache/lru/lru.go index 6fa617a..dc1a317 100644 --- a/gee-cache/day7-proto-buf/geecache/lru/lru.go +++ b/gee-cache/day7-proto-buf/geecache/lru/lru.go @@ -39,12 +39,11 @@ func (c *Cache) Add(key string, value Value) { kv := ele.Value.(*entry) c.nbytes += int64(value.Len()) - int64(kv.value.Len()) kv.value = value - return + } else { + ele := c.ll.PushFront(&entry{key, value}) + c.cache[key] = ele + c.nbytes += int64(len(key)) + int64(value.Len()) } - ele := c.ll.PushFront(&entry{key, value}) - c.cache[key] = ele - c.nbytes += int64(len(key)) + int64(value.Len()) - for c.maxBytes != 0 && c.maxBytes < c.nbytes { c.RemoveOldest() } diff --git a/gee-cache/doc/geecache-day1.md b/gee-cache/doc/geecache-day1.md index 064d90a..2f096d9 100644 --- a/gee-cache/doc/geecache-day1.md +++ b/gee-cache/doc/geecache-day1.md @@ -155,12 +155,11 @@ func (c *Cache) Add(key string, value Value) { kv := ele.Value.(*entry) c.nbytes += int64(value.Len()) - int64(kv.value.Len()) kv.value = value - return + } else { + ele := c.ll.PushFront(&entry{key, value}) + c.cache[key] = ele + c.nbytes += int64(len(key)) + int64(value.Len()) } - ele := c.ll.PushFront(&entry{key, value}) - c.cache[key] = ele - c.nbytes += int64(len(key)) + int64(value.Len()) - for c.maxBytes != 0 && c.maxBytes < c.nbytes { c.RemoveOldest() } diff --git a/gee-orm/day7-migrate/geeorm.go b/gee-orm/day7-migrate/geeorm.go index 017d116..f9bbcd8 100644 --- a/gee-orm/day7-migrate/geeorm.go +++ b/gee-orm/day7-migrate/geeorm.go @@ -106,7 +106,7 @@ func (engine *Engine) Migrate(value interface{}) error { for _, col := range addCols { f := table.GetField(col) - sqlStr := fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s %s;", table.Name, f.Name, f.Tag) + sqlStr := fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s %s;", table.Name, f.Name, f.Type) if _, err = s.Raw(sqlStr).Exec(); err != nil { return } diff --git a/gee-orm/doc/geeorm-day7.md b/gee-orm/doc/geeorm-day7.md index 44079fd..7e6dc1c 100644 --- a/gee-orm/doc/geeorm-day7.md +++ b/gee-orm/doc/geeorm-day7.md @@ -89,7 +89,7 @@ func (engine *Engine) Migrate(value interface{}) error { for _, col := range addCols { f := table.GetField(col) - sqlStr := fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s %s;", table.Name, f.Name, f.Tag) + sqlStr := fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s %s;", table.Name, f.Name, f.Type) if _, err = s.Raw(sqlStr).Exec(); err != nil { return } diff --git a/gee-web/doc/gee-day3.md b/gee-web/doc/gee-day3.md index 4b34f6d..0d66e54 100644 --- a/gee-web/doc/gee-day3.md +++ b/gee-web/doc/gee-day3.md @@ -18,7 +18,7 @@ github: https://github.com/geektutu/7days-golang 本文是 [7天用Go从零实现Web框架Gee教程系列](https://geektutu.com/post/gee.html)的第三篇。 -- 使用 Tire 树实现动态路由(dynamic route)解析。 +- 使用 Trie 树实现动态路由(dynamic route)解析。 - 支持两种模式`:name`和`*filepath`,**代码约150行**。 ## Trie 树简介 From d8451f920dee2b55a186d1af3187395f53e83697 Mon Sep 17 00:00:00 2001 From: chaunceyjiang Date: Mon, 23 Mar 2020 11:54:32 +0800 Subject: [PATCH 063/122] Add custom table name --- gee-orm/day2-reflect-schema/schema/schema.go | 13 ++++++++++++- .../day2-reflect-schema/schema/schema_test.go | 16 ++++++++++++++++ gee-orm/day3-save-query/schema/schema.go | 14 ++++++++++++-- gee-orm/day3-save-query/schema/schema_test.go | 16 ++++++++++++++++ gee-orm/day4-chain-operation/schema/schema.go | 14 ++++++++++++-- .../day4-chain-operation/schema/schema_test.go | 16 ++++++++++++++++ gee-orm/day5-hooks/schema/schema.go | 13 ++++++++++++- gee-orm/day5-hooks/schema/schema_test.go | 16 ++++++++++++++++ gee-orm/day6-transaction/schema/schema.go | 14 ++++++++++++-- gee-orm/day6-transaction/schema/schema_test.go | 16 ++++++++++++++++ gee-orm/day7-migrate/schema/schema.go | 14 ++++++++++++-- gee-orm/day7-migrate/schema/schema_test.go | 16 ++++++++++++++++ 12 files changed, 168 insertions(+), 10 deletions(-) diff --git a/gee-orm/day2-reflect-schema/schema/schema.go b/gee-orm/day2-reflect-schema/schema/schema.go index ff76283..93d36da 100644 --- a/gee-orm/day2-reflect-schema/schema/schema.go +++ b/gee-orm/day2-reflect-schema/schema/schema.go @@ -37,12 +37,23 @@ func (schema *Schema) RecordValues(dest interface{}) []interface{} { return fieldValues } +type ITableName interface { + TableName() string +} + // Parse a struct to a Schema instance func Parse(dest interface{}, d dialect.Dialect) *Schema { modelType := reflect.Indirect(reflect.ValueOf(dest)).Type() + var tableName string + t, ok := dest.(ITableName) + if !ok { + tableName = modelType.Name() + } else { + tableName = t.TableName() + } schema := &Schema{ Model: dest, - Name: modelType.Name(), + Name: tableName, fieldMap: make(map[string]*Field), } diff --git a/gee-orm/day2-reflect-schema/schema/schema_test.go b/gee-orm/day2-reflect-schema/schema/schema_test.go index 47ae9fc..8f625cb 100644 --- a/gee-orm/day2-reflect-schema/schema/schema_test.go +++ b/gee-orm/day2-reflect-schema/schema/schema_test.go @@ -33,3 +33,19 @@ func TestSchema_RecordValues(t *testing.T) { t.Fatal("failed to get values") } } + +type UserTest struct { + Name string `geeorm:"PRIMARY KEY"` + Age int +} + +func (u *UserTest) TableName() string { + return "ns_user_test" +} + +func TestSchema_TableName(t *testing.T) { + schema := Parse(&UserTest{}, TestDial) + if schema.Name != "ns_user_test" || len(schema.Fields) != 2 { + t.Fatal("failed to parse User struct") + } +} diff --git a/gee-orm/day3-save-query/schema/schema.go b/gee-orm/day3-save-query/schema/schema.go index ff76283..2c9b927 100644 --- a/gee-orm/day3-save-query/schema/schema.go +++ b/gee-orm/day3-save-query/schema/schema.go @@ -37,15 +37,25 @@ func (schema *Schema) RecordValues(dest interface{}) []interface{} { return fieldValues } +type ITableName interface { + TableName() string +} + // Parse a struct to a Schema instance func Parse(dest interface{}, d dialect.Dialect) *Schema { modelType := reflect.Indirect(reflect.ValueOf(dest)).Type() + var tableName string + t, ok := dest.(ITableName) + if !ok { + tableName = modelType.Name() + } else { + tableName = t.TableName() + } schema := &Schema{ Model: dest, - Name: modelType.Name(), + Name: tableName, fieldMap: make(map[string]*Field), } - for i := 0; i < modelType.NumField(); i++ { p := modelType.Field(i) if !p.Anonymous && ast.IsExported(p.Name) { diff --git a/gee-orm/day3-save-query/schema/schema_test.go b/gee-orm/day3-save-query/schema/schema_test.go index 47ae9fc..8f625cb 100644 --- a/gee-orm/day3-save-query/schema/schema_test.go +++ b/gee-orm/day3-save-query/schema/schema_test.go @@ -33,3 +33,19 @@ func TestSchema_RecordValues(t *testing.T) { t.Fatal("failed to get values") } } + +type UserTest struct { + Name string `geeorm:"PRIMARY KEY"` + Age int +} + +func (u *UserTest) TableName() string { + return "ns_user_test" +} + +func TestSchema_TableName(t *testing.T) { + schema := Parse(&UserTest{}, TestDial) + if schema.Name != "ns_user_test" || len(schema.Fields) != 2 { + t.Fatal("failed to parse User struct") + } +} diff --git a/gee-orm/day4-chain-operation/schema/schema.go b/gee-orm/day4-chain-operation/schema/schema.go index ff76283..2c9b927 100644 --- a/gee-orm/day4-chain-operation/schema/schema.go +++ b/gee-orm/day4-chain-operation/schema/schema.go @@ -37,15 +37,25 @@ func (schema *Schema) RecordValues(dest interface{}) []interface{} { return fieldValues } +type ITableName interface { + TableName() string +} + // Parse a struct to a Schema instance func Parse(dest interface{}, d dialect.Dialect) *Schema { modelType := reflect.Indirect(reflect.ValueOf(dest)).Type() + var tableName string + t, ok := dest.(ITableName) + if !ok { + tableName = modelType.Name() + } else { + tableName = t.TableName() + } schema := &Schema{ Model: dest, - Name: modelType.Name(), + Name: tableName, fieldMap: make(map[string]*Field), } - for i := 0; i < modelType.NumField(); i++ { p := modelType.Field(i) if !p.Anonymous && ast.IsExported(p.Name) { diff --git a/gee-orm/day4-chain-operation/schema/schema_test.go b/gee-orm/day4-chain-operation/schema/schema_test.go index 47ae9fc..8f625cb 100644 --- a/gee-orm/day4-chain-operation/schema/schema_test.go +++ b/gee-orm/day4-chain-operation/schema/schema_test.go @@ -33,3 +33,19 @@ func TestSchema_RecordValues(t *testing.T) { t.Fatal("failed to get values") } } + +type UserTest struct { + Name string `geeorm:"PRIMARY KEY"` + Age int +} + +func (u *UserTest) TableName() string { + return "ns_user_test" +} + +func TestSchema_TableName(t *testing.T) { + schema := Parse(&UserTest{}, TestDial) + if schema.Name != "ns_user_test" || len(schema.Fields) != 2 { + t.Fatal("failed to parse User struct") + } +} diff --git a/gee-orm/day5-hooks/schema/schema.go b/gee-orm/day5-hooks/schema/schema.go index ff76283..93d36da 100644 --- a/gee-orm/day5-hooks/schema/schema.go +++ b/gee-orm/day5-hooks/schema/schema.go @@ -37,12 +37,23 @@ func (schema *Schema) RecordValues(dest interface{}) []interface{} { return fieldValues } +type ITableName interface { + TableName() string +} + // Parse a struct to a Schema instance func Parse(dest interface{}, d dialect.Dialect) *Schema { modelType := reflect.Indirect(reflect.ValueOf(dest)).Type() + var tableName string + t, ok := dest.(ITableName) + if !ok { + tableName = modelType.Name() + } else { + tableName = t.TableName() + } schema := &Schema{ Model: dest, - Name: modelType.Name(), + Name: tableName, fieldMap: make(map[string]*Field), } diff --git a/gee-orm/day5-hooks/schema/schema_test.go b/gee-orm/day5-hooks/schema/schema_test.go index 47ae9fc..8f625cb 100644 --- a/gee-orm/day5-hooks/schema/schema_test.go +++ b/gee-orm/day5-hooks/schema/schema_test.go @@ -33,3 +33,19 @@ func TestSchema_RecordValues(t *testing.T) { t.Fatal("failed to get values") } } + +type UserTest struct { + Name string `geeorm:"PRIMARY KEY"` + Age int +} + +func (u *UserTest) TableName() string { + return "ns_user_test" +} + +func TestSchema_TableName(t *testing.T) { + schema := Parse(&UserTest{}, TestDial) + if schema.Name != "ns_user_test" || len(schema.Fields) != 2 { + t.Fatal("failed to parse User struct") + } +} diff --git a/gee-orm/day6-transaction/schema/schema.go b/gee-orm/day6-transaction/schema/schema.go index ff76283..2c9b927 100644 --- a/gee-orm/day6-transaction/schema/schema.go +++ b/gee-orm/day6-transaction/schema/schema.go @@ -37,15 +37,25 @@ func (schema *Schema) RecordValues(dest interface{}) []interface{} { return fieldValues } +type ITableName interface { + TableName() string +} + // Parse a struct to a Schema instance func Parse(dest interface{}, d dialect.Dialect) *Schema { modelType := reflect.Indirect(reflect.ValueOf(dest)).Type() + var tableName string + t, ok := dest.(ITableName) + if !ok { + tableName = modelType.Name() + } else { + tableName = t.TableName() + } schema := &Schema{ Model: dest, - Name: modelType.Name(), + Name: tableName, fieldMap: make(map[string]*Field), } - for i := 0; i < modelType.NumField(); i++ { p := modelType.Field(i) if !p.Anonymous && ast.IsExported(p.Name) { diff --git a/gee-orm/day6-transaction/schema/schema_test.go b/gee-orm/day6-transaction/schema/schema_test.go index 47ae9fc..8f625cb 100644 --- a/gee-orm/day6-transaction/schema/schema_test.go +++ b/gee-orm/day6-transaction/schema/schema_test.go @@ -33,3 +33,19 @@ func TestSchema_RecordValues(t *testing.T) { t.Fatal("failed to get values") } } + +type UserTest struct { + Name string `geeorm:"PRIMARY KEY"` + Age int +} + +func (u *UserTest) TableName() string { + return "ns_user_test" +} + +func TestSchema_TableName(t *testing.T) { + schema := Parse(&UserTest{}, TestDial) + if schema.Name != "ns_user_test" || len(schema.Fields) != 2 { + t.Fatal("failed to parse User struct") + } +} diff --git a/gee-orm/day7-migrate/schema/schema.go b/gee-orm/day7-migrate/schema/schema.go index ff76283..2c9b927 100644 --- a/gee-orm/day7-migrate/schema/schema.go +++ b/gee-orm/day7-migrate/schema/schema.go @@ -37,15 +37,25 @@ func (schema *Schema) RecordValues(dest interface{}) []interface{} { return fieldValues } +type ITableName interface { + TableName() string +} + // Parse a struct to a Schema instance func Parse(dest interface{}, d dialect.Dialect) *Schema { modelType := reflect.Indirect(reflect.ValueOf(dest)).Type() + var tableName string + t, ok := dest.(ITableName) + if !ok { + tableName = modelType.Name() + } else { + tableName = t.TableName() + } schema := &Schema{ Model: dest, - Name: modelType.Name(), + Name: tableName, fieldMap: make(map[string]*Field), } - for i := 0; i < modelType.NumField(); i++ { p := modelType.Field(i) if !p.Anonymous && ast.IsExported(p.Name) { diff --git a/gee-orm/day7-migrate/schema/schema_test.go b/gee-orm/day7-migrate/schema/schema_test.go index 47ae9fc..8f625cb 100644 --- a/gee-orm/day7-migrate/schema/schema_test.go +++ b/gee-orm/day7-migrate/schema/schema_test.go @@ -33,3 +33,19 @@ func TestSchema_RecordValues(t *testing.T) { t.Fatal("failed to get values") } } + +type UserTest struct { + Name string `geeorm:"PRIMARY KEY"` + Age int +} + +func (u *UserTest) TableName() string { + return "ns_user_test" +} + +func TestSchema_TableName(t *testing.T) { + schema := Parse(&UserTest{}, TestDial) + if schema.Name != "ns_user_test" || len(schema.Fields) != 2 { + t.Fatal("failed to parse User struct") + } +} From 09764a019e3322ce1eb3b9e4f6c5a0df6e2acb87 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Sun, 29 Mar 2020 15:09:21 +0800 Subject: [PATCH 064/122] fix tire to trie --- README.md | 4 ++-- gee-web/README.md | 2 +- gee-web/doc/gee.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a85bc69..37d366d 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ - 第一天:[前置知识(http.Handler接口)](https://geektutu.com/post/gee-day1.html) | [Code](gee-web/day1-http-base) - 第二天:[上下文设计(Context)](https://geektutu.com/post/gee-day2.html) | [Code](gee-web/day2-context) -- 第三天:[Tire树路由(Router)](https://geektutu.com/post/gee-day3.html) | [Code](gee-web/day3-router) +- 第三天:[Trie树路由(Router)](https://geektutu.com/post/gee-day3.html) | [Code](gee-web/day3-router) - 第四天:[分组控制(Group)](https://geektutu.com/post/gee-day4.html) | [Code](gee-web/day4-group) - 第五天:[中间件(Middleware)](https://geektutu.com/post/gee-day5.html) | [Code](gee-web/day5-middleware) - 第六天:[HTML模板(Template)](https://geektutu.com/post/gee-day6.html) | [Code](gee-web/day6-template) @@ -70,7 +70,7 @@ What can I write in 7 days? A gin-like web framework? A distributed cache like g - Day 1 - http.Handler Interface Basic [Code](gee-web/day1-http-base) - Day 2 - Design a Flexiable Context [Code](gee-web/day2-context) -- Day 3 - Router with Tire-Tree Algorithm [Code](gee-web/day3-router) +- Day 3 - Router with Trie-Tree Algorithm [Code](gee-web/day3-router) - Day 4 - Group Control [Code](gee-web/day4-group) - Day 5 - Middleware Mechanism [Code](gee-web/day5-middleware) - Day 6 - Embeded Template Support [Code](gee-web/day6-template) diff --git a/gee-web/README.md b/gee-web/README.md index 67ccdf2..6e8f19b 100644 --- a/gee-web/README.md +++ b/gee-web/README.md @@ -12,7 +12,7 @@ - [第一天:前置知识(http.Handler接口)](https://geektutu.com/post/gee-day1.html) - [第二天:上下文设计(Context)](https://geektutu.com/post/gee-day2.html) -- [第三天:Tire树路由(Router)](https://geektutu.com/post/gee-day3.html) +- [第三天:Trie树路由(Router)](https://geektutu.com/post/gee-day3.html) - [第四天:分组控制(Group)](https://geektutu.com/post/gee-day4.html) - [第五天:中间件(Middleware)](https://geektutu.com/post/gee-day5.html) - [第六天:HTML模板(Template)](https://geektutu.com/post/gee-day6.html) diff --git a/gee-web/doc/gee.md b/gee-web/doc/gee.md index 87e3a34..0596aaf 100644 --- a/gee-web/doc/gee.md +++ b/gee-web/doc/gee.md @@ -65,7 +65,7 @@ func handler(w http.ResponseWriter, r *http.Request) { - 第一天:[前置知识(http.Handler接口)](https://geektutu.com/post/gee-day1.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/gee-web/day1-http-base) - 第二天:[上下文设计(Context)](https://geektutu.com/post/gee-day2.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/gee-web/day2-context) -- 第三天:[Tire树路由(Router)](https://geektutu.com/post/gee-day3.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/gee-web/day3-router) +- 第三天:[Trie树路由(Router)](https://geektutu.com/post/gee-day3.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/gee-web/day3-router) - 第四天:[分组控制(Group)](https://geektutu.com/post/gee-day4.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/gee-web/day4-group) - 第五天:[中间件(Middleware)](https://geektutu.com/post/gee-day5.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/gee-web/day5-middleware) - 第六天:[HTML模板(Template)](https://geektutu.com/post/gee-day6.html),[Code - Github](https://github.com/geektutu/7days-golang/tree/master/gee-web/day6-template) From bd6ec6c992c538ec2ed806f6a3b44272f96376e4 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Sun, 29 Mar 2020 22:24:42 +0800 Subject: [PATCH 065/122] init geebolt --- gee-bolt/day1-pages/go.mod | 3 ++ gee-bolt/day1-pages/meta.go | 33 ++++++++++++++ gee-bolt/day1-pages/page.go | 88 +++++++++++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+) create mode 100644 gee-bolt/day1-pages/go.mod create mode 100644 gee-bolt/day1-pages/meta.go create mode 100644 gee-bolt/day1-pages/page.go diff --git a/gee-bolt/day1-pages/go.mod b/gee-bolt/day1-pages/go.mod new file mode 100644 index 0000000..17b5990 --- /dev/null +++ b/gee-bolt/day1-pages/go.mod @@ -0,0 +1,3 @@ +module geebolt + +go 1.13 diff --git a/gee-bolt/day1-pages/meta.go b/gee-bolt/day1-pages/meta.go new file mode 100644 index 0000000..4e9cdb1 --- /dev/null +++ b/gee-bolt/day1-pages/meta.go @@ -0,0 +1,33 @@ +package geebolt + +import ( + "errors" + "hash/fnv" + "unsafe" +) + +// Represent a marker value to indicate that a file is a gee-bolt DB +const magic uint32 = 0xED0CDAED + +type meta struct { + magic uint32 + pageSize uint32 + pgid uint64 + checksum uint64 +} + +func (m *meta) sum64() uint64 { + var h = fnv.New64a() + _, _ = h.Write((*[unsafe.Offsetof(meta{}.checksum)]byte)(unsafe.Pointer(m))[:]) + return h.Sum64() +} + +func (m *meta) validate() error { + if m.magic != magic { + return errors.New("invalid magic number") + } + if m.checksum != m.sum64() { + return errors.New("invalid checksum") + } + return nil +} diff --git a/gee-bolt/day1-pages/page.go b/gee-bolt/day1-pages/page.go new file mode 100644 index 0000000..18fc492 --- /dev/null +++ b/gee-bolt/day1-pages/page.go @@ -0,0 +1,88 @@ +package geebolt + +import ( + "fmt" + "reflect" + "unsafe" +) + +const pageHeaderSize = unsafe.Sizeof(page{}) +const branchPageElementSize = unsafe.Sizeof(branchPageElement{}) +const leafPageElementSize = unsafe.Sizeof(leafPageElement{}) +const maxKeysPerPage = 1024 + +const ( + branchPageFlag uint16 = iota + leafPageFlag + metaPageFlag + freelistPageFlag +) + +type page struct { + id uint64 + flags uint16 + count uint16 + overflow uint32 +} + +type leafPageElement struct { + pos uint32 + ksize uint32 + vsize uint32 +} + +type branchPageElement struct { + pos uint32 + ksize uint32 + pgid uint64 +} + +func (p *page) typ() string { + switch p.flags { + case branchPageFlag: + return "branch" + case leafPageFlag: + return "leaf" + case metaPageFlag: + return "meta" + case freelistPageFlag: + return "freelist" + } + return fmt.Sprintf("unknown<%02x>", p.flags) +} + +func (p *page) meta() *meta { + return (*meta)(unsafe.Pointer(uintptr(unsafe.Pointer(p)) + pageHeaderSize)) +} + +func (p *page) dataPtr() unsafe.Pointer { + return unsafe.Pointer(&reflect.SliceHeader{ + Data: uintptr(unsafe.Pointer(p)) + pageHeaderSize, + Len: int(p.count), + Cap: int(p.count), + }) +} + +func (p *page) leafPageElement(index uint16) *leafPageElement { + off := pageHeaderSize + uintptr(index)*leafPageElementSize + return (*leafPageElement)(unsafe.Pointer(uintptr(unsafe.Pointer(p)) + off)) +} + +func (p *page) leafPageElements() []leafPageElement { + if p.count == 0 { + return nil + } + return *(*[]leafPageElement)(p.dataPtr()) +} + +func (p *page) branchPageElement(index uint16) *branchPageElement { + off := pageHeaderSize + uintptr(index)*branchPageElementSize + return (*branchPageElement)(unsafe.Pointer(uintptr(unsafe.Pointer(p)) + off)) +} + +func (p *page) branchPageElements() []branchPageElement { + if p.count == 0 { + return nil + } + return *(*[]branchPageElement)(p.dataPtr()) +} From 7123e232f439741b6857354a98afb71f4d8808c5 Mon Sep 17 00:00:00 2001 From: singlemancombat Date: Tue, 31 Mar 2020 16:28:22 -0700 Subject: [PATCH 066/122] Remove redundant type conversion. --- gee-cache/day5-multi-nodes/main.go | 2 +- gee-cache/day6-single-flight/main.go | 2 +- gee-cache/day7-proto-buf/main.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gee-cache/day5-multi-nodes/main.go b/gee-cache/day5-multi-nodes/main.go index b8677f2..56abc7e 100644 --- a/gee-cache/day5-multi-nodes/main.go +++ b/gee-cache/day5-multi-nodes/main.go @@ -82,5 +82,5 @@ func main() { if api { go startAPIServer(apiAddr, gee) } - startCacheServer(addrMap[port], []string(addrs), gee) + startCacheServer(addrMap[port], addrs, gee) } diff --git a/gee-cache/day6-single-flight/main.go b/gee-cache/day6-single-flight/main.go index b8677f2..56abc7e 100644 --- a/gee-cache/day6-single-flight/main.go +++ b/gee-cache/day6-single-flight/main.go @@ -82,5 +82,5 @@ func main() { if api { go startAPIServer(apiAddr, gee) } - startCacheServer(addrMap[port], []string(addrs), gee) + startCacheServer(addrMap[port], addrs, gee) } diff --git a/gee-cache/day7-proto-buf/main.go b/gee-cache/day7-proto-buf/main.go index b8677f2..56abc7e 100644 --- a/gee-cache/day7-proto-buf/main.go +++ b/gee-cache/day7-proto-buf/main.go @@ -82,5 +82,5 @@ func main() { if api { go startAPIServer(apiAddr, gee) } - startCacheServer(addrMap[port], []string(addrs), gee) + startCacheServer(addrMap[port], addrs, gee) } From fb9b6cfadef3e9807bd8d9ed137484a28e4938bb Mon Sep 17 00:00:00 2001 From: singlemancombat Date: Wed, 1 Apr 2020 00:48:51 -0700 Subject: [PATCH 067/122] Return without info logging if failed to close database. --- gee-orm/day7-migrate/geeorm.go | 1 + 1 file changed, 1 insertion(+) diff --git a/gee-orm/day7-migrate/geeorm.go b/gee-orm/day7-migrate/geeorm.go index f9bbcd8..fe6d477 100644 --- a/gee-orm/day7-migrate/geeorm.go +++ b/gee-orm/day7-migrate/geeorm.go @@ -43,6 +43,7 @@ func NewEngine(driver, source string) (e *Engine, err error) { func (engine *Engine) Close() { if err := engine.db.Close(); err != nil { log.Error("Failed to close database") + return } log.Info("Close database success") } From d5509e03bb15a3dc8725acdc5aa821d80c008317 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Mon, 20 Apr 2020 23:22:11 +0800 Subject: [PATCH 068/122] fix geecache typo --- gee-cache/doc/geecache-day2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gee-cache/doc/geecache-day2.md b/gee-cache/doc/geecache-day2.md index c899111..7f75af8 100644 --- a/gee-cache/doc/geecache-day2.md +++ b/gee-cache/doc/geecache-day2.md @@ -217,7 +217,7 @@ geecache/ ### 3.1 回调 Getter -我们思考一下,如果缓存不存在,应从数据源(文件,数据库等)获取数据并添加到缓存中。GeeCache 是否应该支持多种数据源的配置呢?不应该,一是数据源的种类太多,没办法一一实现;而是扩展性不好。如何从源头获取数据,应该是用户决定的事情,我们就把这件事交给用户好了。因此,我们设计了一个回调函数(callback),在缓存不存在时,调用这个函数,得到源数据。 +我们思考一下,如果缓存不存在,应从数据源(文件,数据库等)获取数据并添加到缓存中。GeeCache 是否应该支持多种数据源的配置呢?不应该,一是数据源的种类太多,没办法一一实现;二是扩展性不好。如何从源头获取数据,应该是用户决定的事情,我们就把这件事交给用户好了。因此,我们设计了一个回调函数(callback),在缓存不存在时,调用这个函数,得到源数据。 [day2-single-node/geecache/geecache.go - github](https://github.com/geektutu/7days-golang/tree/master/gee-cache/day2-single-node/geecache) From 2d18404992d9414f3658cafb467a6a5a728ee0c7 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Sun, 3 May 2020 18:52:50 +0800 Subject: [PATCH 069/122] geerpc init project, add feature: decode & encode args, reply --- gee-bolt/day2-mmap/db.go | 18 ++++ gee-bolt/day2-mmap/go.mod | 3 + gee-bolt/day3-tree/go.mod | 3 + gee-bolt/day3-tree/meta.go | 33 +++++++ gee-bolt/day3-tree/node.go | 53 ++++++++++ gee-bolt/day3-tree/page.go | 88 +++++++++++++++++ gee-rpc/day1-encode/go.mod | 3 + gee-rpc/day1-encode/protocol/codec.go | 26 +++++ gee-rpc/day1-encode/protocol/message.go | 113 ++++++++++++++++++++++ gee-rpc/day1-encode/protocol/path.go | 3 + gee-rpc/day1-encode/server/server.go | 76 +++++++++++++++ gee-rpc/day1-encode/server/server_test.go | 76 +++++++++++++++ gee-rpc/day1-encode/server/service.go | 99 +++++++++++++++++++ 13 files changed, 594 insertions(+) create mode 100755 gee-bolt/day2-mmap/db.go create mode 100755 gee-bolt/day2-mmap/go.mod create mode 100755 gee-bolt/day3-tree/go.mod create mode 100644 gee-bolt/day3-tree/meta.go create mode 100755 gee-bolt/day3-tree/node.go create mode 100644 gee-bolt/day3-tree/page.go create mode 100644 gee-rpc/day1-encode/go.mod create mode 100755 gee-rpc/day1-encode/protocol/codec.go create mode 100755 gee-rpc/day1-encode/protocol/message.go create mode 100755 gee-rpc/day1-encode/protocol/path.go create mode 100755 gee-rpc/day1-encode/server/server.go create mode 100755 gee-rpc/day1-encode/server/server_test.go create mode 100755 gee-rpc/day1-encode/server/service.go diff --git a/gee-bolt/day2-mmap/db.go b/gee-bolt/day2-mmap/db.go new file mode 100755 index 0000000..9c644d1 --- /dev/null +++ b/gee-bolt/day2-mmap/db.go @@ -0,0 +1,18 @@ +package geebolt + +import "os" + +type DB struct { + data []byte + file *os.File +} + +const maxMapSize = 1 << 31 + +func (db *DB) mmap(sz int) error { + b, err := syscall.Mmap() +} + +func Open(path string) { + +} diff --git a/gee-bolt/day2-mmap/go.mod b/gee-bolt/day2-mmap/go.mod new file mode 100755 index 0000000..17b5990 --- /dev/null +++ b/gee-bolt/day2-mmap/go.mod @@ -0,0 +1,3 @@ +module geebolt + +go 1.13 diff --git a/gee-bolt/day3-tree/go.mod b/gee-bolt/day3-tree/go.mod new file mode 100755 index 0000000..17b5990 --- /dev/null +++ b/gee-bolt/day3-tree/go.mod @@ -0,0 +1,3 @@ +module geebolt + +go 1.13 diff --git a/gee-bolt/day3-tree/meta.go b/gee-bolt/day3-tree/meta.go new file mode 100644 index 0000000..4e9cdb1 --- /dev/null +++ b/gee-bolt/day3-tree/meta.go @@ -0,0 +1,33 @@ +package geebolt + +import ( + "errors" + "hash/fnv" + "unsafe" +) + +// Represent a marker value to indicate that a file is a gee-bolt DB +const magic uint32 = 0xED0CDAED + +type meta struct { + magic uint32 + pageSize uint32 + pgid uint64 + checksum uint64 +} + +func (m *meta) sum64() uint64 { + var h = fnv.New64a() + _, _ = h.Write((*[unsafe.Offsetof(meta{}.checksum)]byte)(unsafe.Pointer(m))[:]) + return h.Sum64() +} + +func (m *meta) validate() error { + if m.magic != magic { + return errors.New("invalid magic number") + } + if m.checksum != m.sum64() { + return errors.New("invalid checksum") + } + return nil +} diff --git a/gee-bolt/day3-tree/node.go b/gee-bolt/day3-tree/node.go new file mode 100755 index 0000000..a40c51a --- /dev/null +++ b/gee-bolt/day3-tree/node.go @@ -0,0 +1,53 @@ +package geebolt + +import ( + "bytes" + "sort" +) + +type kv struct { + key []byte + value []byte +} + +type node struct { + isLeaf bool + key []byte + parent *node + children []*node + kvs []kv +} + +func (n *node) root() *node { + if n.parent == nil { + return n + } + return n.parent.root() +} + +func (n *node) index(key []byte) (index int, exact bool) { + index = sort.Search(len(n.kvs), func(i int) bool { + return bytes.Compare(n.kvs[i].key, key) != -1 + }) + exact = len(n.kvs) > 0 && index < len(n.kvs) && bytes.Equal(n.kvs[index].key, key) + return +} + +func (n *node) put(oldKey, newKey, value []byte) { + index, exact := n.index(oldKey) + if !exact { + n.kvs = append(n.kvs, kv{}) + copy(n.kvs[index+1:], n.kvs[index:]) + } + kv := &n.kvs[index] + kv.key = newKey + kv.value = value +} + +func (n *node) del(key []byte) { + index, exact := n.index(key) + if exact { + n.kvs = append(n.kvs[:index], n.kvs[index+1:]...) + } +} + diff --git a/gee-bolt/day3-tree/page.go b/gee-bolt/day3-tree/page.go new file mode 100644 index 0000000..18fc492 --- /dev/null +++ b/gee-bolt/day3-tree/page.go @@ -0,0 +1,88 @@ +package geebolt + +import ( + "fmt" + "reflect" + "unsafe" +) + +const pageHeaderSize = unsafe.Sizeof(page{}) +const branchPageElementSize = unsafe.Sizeof(branchPageElement{}) +const leafPageElementSize = unsafe.Sizeof(leafPageElement{}) +const maxKeysPerPage = 1024 + +const ( + branchPageFlag uint16 = iota + leafPageFlag + metaPageFlag + freelistPageFlag +) + +type page struct { + id uint64 + flags uint16 + count uint16 + overflow uint32 +} + +type leafPageElement struct { + pos uint32 + ksize uint32 + vsize uint32 +} + +type branchPageElement struct { + pos uint32 + ksize uint32 + pgid uint64 +} + +func (p *page) typ() string { + switch p.flags { + case branchPageFlag: + return "branch" + case leafPageFlag: + return "leaf" + case metaPageFlag: + return "meta" + case freelistPageFlag: + return "freelist" + } + return fmt.Sprintf("unknown<%02x>", p.flags) +} + +func (p *page) meta() *meta { + return (*meta)(unsafe.Pointer(uintptr(unsafe.Pointer(p)) + pageHeaderSize)) +} + +func (p *page) dataPtr() unsafe.Pointer { + return unsafe.Pointer(&reflect.SliceHeader{ + Data: uintptr(unsafe.Pointer(p)) + pageHeaderSize, + Len: int(p.count), + Cap: int(p.count), + }) +} + +func (p *page) leafPageElement(index uint16) *leafPageElement { + off := pageHeaderSize + uintptr(index)*leafPageElementSize + return (*leafPageElement)(unsafe.Pointer(uintptr(unsafe.Pointer(p)) + off)) +} + +func (p *page) leafPageElements() []leafPageElement { + if p.count == 0 { + return nil + } + return *(*[]leafPageElement)(p.dataPtr()) +} + +func (p *page) branchPageElement(index uint16) *branchPageElement { + off := pageHeaderSize + uintptr(index)*branchPageElementSize + return (*branchPageElement)(unsafe.Pointer(uintptr(unsafe.Pointer(p)) + off)) +} + +func (p *page) branchPageElements() []branchPageElement { + if p.count == 0 { + return nil + } + return *(*[]branchPageElement)(p.dataPtr()) +} diff --git a/gee-rpc/day1-encode/go.mod b/gee-rpc/day1-encode/go.mod new file mode 100644 index 0000000..0ec8aeb --- /dev/null +++ b/gee-rpc/day1-encode/go.mod @@ -0,0 +1,3 @@ +module geerpc + +go 1.13 diff --git a/gee-rpc/day1-encode/protocol/codec.go b/gee-rpc/day1-encode/protocol/codec.go new file mode 100755 index 0000000..62b3b50 --- /dev/null +++ b/gee-rpc/day1-encode/protocol/codec.go @@ -0,0 +1,26 @@ +package protocol + +import ( + "bytes" + "encoding/json" +) + +type Codec interface { + Encode(i interface{}) ([]byte, error) + Decode(data []byte, i interface{}) error +} + +// JSONCodec uses json marshaler and unmarshaler. +type JSONCodec struct{} + +// Encode encodes an object into slice of bytes. +func (c JSONCodec) Encode(i interface{}) ([]byte, error) { + return json.Marshal(i) +} + +// Decode decodes an object from slice of bytes. +func (c JSONCodec) Decode(data []byte, i interface{}) error { + d := json.NewDecoder(bytes.NewBuffer(data)) + d.UseNumber() + return d.Decode(i) +} diff --git a/gee-rpc/day1-encode/protocol/message.go b/gee-rpc/day1-encode/protocol/message.go new file mode 100755 index 0000000..08dd3ca --- /dev/null +++ b/gee-rpc/day1-encode/protocol/message.go @@ -0,0 +1,113 @@ +package protocol + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + "strings" +) + +const MagicNumber int32 = 0xECABCD + +type SerializeType int8 + +const ( + JSON SerializeType = iota +) + +var Codecs = map[SerializeType]Codec{ + JSON: &JSONCodec{}, +} + +type Status int8 + +const ( + OK Status = iota + ExecError + NotFoundError +) + +type Header struct { + Magic int32 + Status Status + SerializeType SerializeType + ServiceMethodSize int32 + PayloadSize int32 +} + +type Message struct { + *Header + ServiceMethod string + Payload []byte +} + +func NewMessage() *Message { + return &Message{ + Header: &Header{Magic: MagicNumber}, + } +} + +func (m *Message) HandleError(status Status, err error) *Message { + m.Status = status + _ = m.SetPayload(err) + return m +} + +func (m *Message) SetServiceMethod(name string) { + m.ServiceMethod = name +} + +func (m *Message) GetServiceMethod() (service, method string, err error) { + parts := strings.Split(m.ServiceMethod, ".") + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return "", "", fmt.Errorf(" format error") + } + return parts[0], parts[1], nil +} + +func (m *Message) GetPayload(i interface{}) error { + return Codecs[m.SerializeType].Decode(m.Payload, i) +} + +func (m *Message) SetPayload(i interface{}) (err error) { + m.Payload, err = Codecs[m.SerializeType].Encode(i) + return +} + +func (m *Message) Clone() *Message { + m2 := NewMessage() + *m2.Header = *m.Header + m2.ServiceMethod = m.ServiceMethod + return m2 +} +func Read(r io.Reader) (*Message, error) { + m := NewMessage() + if err := binary.Read(r, binary.BigEndian, m.Header); err != nil { + return nil, err + } + if m.Magic != MagicNumber { + return nil, fmt.Errorf("invalid message: wrong magic number") + } + + buf := make([]byte, m.ServiceMethodSize+m.PayloadSize) + if err := binary.Read(r, binary.BigEndian, buf); err != nil { + return nil, err + } + m.ServiceMethod = string(buf[:m.ServiceMethodSize]) + m.Payload = buf[m.ServiceMethodSize:] + return m, nil +} +func (m *Message) Write(w io.Writer) error { + m.PayloadSize = int32(len(m.Payload)) + m.ServiceMethodSize = int32(len(m.ServiceMethod)) + buf := bytes.NewBufferString(m.ServiceMethod) + buf.Write(m.Payload) + if err := binary.Write(w, binary.BigEndian, m.Header); err != nil { + return err + } + if err := binary.Write(w, binary.BigEndian, buf.Bytes()); err != nil { + return err + } + return nil +} diff --git a/gee-rpc/day1-encode/protocol/path.go b/gee-rpc/day1-encode/protocol/path.go new file mode 100755 index 0000000..cbd854c --- /dev/null +++ b/gee-rpc/day1-encode/protocol/path.go @@ -0,0 +1,3 @@ +package protocol + +const DefaultRPCPath = "/_geerpc" diff --git a/gee-rpc/day1-encode/server/server.go b/gee-rpc/day1-encode/server/server.go new file mode 100755 index 0000000..021b3ae --- /dev/null +++ b/gee-rpc/day1-encode/server/server.go @@ -0,0 +1,76 @@ +package server + +import ( + "fmt" + "log" + "net" + "net/http" + + "geerpc/protocol" +) + +type Server struct { + ln net.Listener + service map[string]*service +} + +func NewServer() *Server { + return &Server{ + service: make(map[string]*service), + } +} + +func (s *Server) Address() net.Addr { + return s.ln.Addr() +} + +func (s *Server) Serve(network, address string) (err error) { + if network == "http" { + if s.ln, err = net.Listen("tcp", address); err != nil { + return err + } + http.Handle(protocol.DefaultRPCPath, s) + return http.Serve(s.ln, nil) + } + panic(network + " not support") +} + +func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) { + m, err := protocol.Read(req.Body) + if err != nil { + log.Println("failed to read message from body") + _, _ = w.Write([]byte("fail")) + return + } + + log.Println(req.Method, m.ServiceMethod) + + respMsg := s.call(m) + _ = respMsg.Write(w) +} + +func (s *Server) Register(receiver interface{}) { + service := newService(receiver) + s.service[service.name] = service +} + +func (s *Server) call(req *protocol.Message) (resp *protocol.Message) { + serviceName, methodName, err := req.GetServiceMethod() + resp = req.Clone() + if err != nil { + return resp.HandleError(protocol.NotFoundError, err) + } + + service := s.service[serviceName] + if service == nil || service.method[methodName] == nil { + return resp.HandleError(protocol.NotFoundError, fmt.Errorf("%s not found", req.ServiceMethod)) + } + + return service.call(methodName, req) +} + +func _assert(condition bool, msg string, v ...interface{}) { + if !condition { + panic(fmt.Sprintf(msg, v...)) + } +} diff --git a/gee-rpc/day1-encode/server/server_test.go b/gee-rpc/day1-encode/server/server_test.go new file mode 100755 index 0000000..6aed5a5 --- /dev/null +++ b/gee-rpc/day1-encode/server/server_test.go @@ -0,0 +1,76 @@ +package server + +import ( + "bytes" + "fmt" + "geerpc/protocol" + "net" + "net/http" + "testing" + "time" +) + +type Calc struct{} + +type Req struct { + Num1 int + Num2 int +} + +func (c *Calc) Add(req Req, reply *int) error { + *reply = req.Num1 + req.Num2 + return nil +} + +func TestServer_Register(t *testing.T) { + s := NewServer() + s.Register(&Calc{}) + + service := s.service["Calc"] + if service == nil || service.method["Add"] == nil { + t.Fatal("failed to register") + } +} + +func TestServer_Call(t *testing.T) { + s := NewServer() + s.Register(&Calc{}) + req := &Req{Num1: 10, Num2: 20} + + reqMsg := protocol.NewMessage() + reqMsg.SetServiceMethod("Calc.Add") + _ = reqMsg.SetPayload(req) + + respMsg := s.call(reqMsg) + var ans int + _ = respMsg.GetPayload(&ans) + if ans != req.Num1+req.Num2 { + t.Fatal("failed to call Calc.Add") + } +} + +func TestServer_Serve(t *testing.T) { + s := NewServer() + s.Register(&Calc{}) + go func() { _ = s.Serve("http", ":0") }() + + time.Sleep(time.Second) + port := s.Address().(*net.TCPAddr).Port + addr := fmt.Sprintf("http://localhost:%d%s", port, protocol.DefaultRPCPath) + + reqMsg := protocol.NewMessage() + reqMsg.SetServiceMethod("Calc.Add") + _ = reqMsg.SetPayload(&Req{1, 2}) + + var buf bytes.Buffer + _ = reqMsg.Write(&buf) + resp, _ := http.Post(addr, "application/octet-stream", &buf) + + respMsg, _ := protocol.Read(resp.Body) + + var ans int + _ = respMsg.GetPayload(&ans) + if respMsg.Status != protocol.OK || ans != 3 { + t.Fatal("failed to call Calc.Add") + } +} diff --git a/gee-rpc/day1-encode/server/service.go b/gee-rpc/day1-encode/server/service.go new file mode 100755 index 0000000..2cc506b --- /dev/null +++ b/gee-rpc/day1-encode/server/service.go @@ -0,0 +1,99 @@ +package server + +import ( + "geerpc/protocol" + "go/ast" + "log" + "reflect" +) + +type methodType struct { + method reflect.Method + ArgType reflect.Type + ReplyType reflect.Type +} + +func (m *methodType) NewArg() interface{} { + return newTypeInter(m.ArgType) +} + +func (m *methodType) NewReply() interface{} { + return newTypeInter(m.ReplyType) +} + +func newTypeInter(t reflect.Type) interface{} { + var v reflect.Value + if t.Kind() == reflect.Ptr { // reply must be ptr + v = reflect.New(t.Elem()) + } else { + v = reflect.New(t) + } + return v.Interface() +} + +type service struct { + name string + rcvr reflect.Value + method map[string]*methodType +} + +func newService(receiver interface{}) *service { + service := new(service) + service.method = make(map[string]*methodType) + service.name = reflect.Indirect(reflect.ValueOf(receiver)).Type().Name() + service.rcvr = reflect.ValueOf(receiver) + + _assert(ast.IsExported(service.name), "%service is not exported", service.name) + rcvrType := reflect.TypeOf(receiver) + for i := 0; i < rcvrType.NumMethod(); i++ { + method := rcvrType.Method(i) + mType := method.Type + if mType.NumIn() != 3 || mType.NumOut() != 1 { + continue + } + if mType.Out(0) != reflect.TypeOf((*error)(nil)).Elem() { + continue + } + + argType, replyType := mType.In(1), mType.In(2) + if !isExportedOrBuiltinType(argType) || !isExportedOrBuiltinType(replyType) { + continue + } + + service.method[method.Name] = &methodType{ + method: method, + ArgType: argType, + ReplyType: replyType, + } + log.Printf("Register %s.%s\n", service.name, method.Name) + } + + return service +} + +func (s *service) call(methodName string, reqMsg *protocol.Message) (resp *protocol.Message) { + mType := s.method[methodName] + resp = reqMsg.Clone() + + arg, reply := mType.NewArg(), mType.NewReply() + if err := reqMsg.GetPayload(arg); err != nil { + return resp.HandleError(protocol.ExecError, err) + } + + f := mType.method.Func + returnValues := f.Call([]reflect.Value{s.rcvr, reflect.ValueOf(arg).Elem(), reflect.ValueOf(reply)}) + + if errInter := returnValues[0].Interface(); errInter != nil { + return resp.HandleError(protocol.ExecError, errInter.(error)) + } + + if err := resp.SetPayload(reply); err != nil { + return resp.HandleError(protocol.ExecError, err) + } + + return resp +} + +func isExportedOrBuiltinType(t reflect.Type) bool { + return ast.IsExported(t.Name()) || t.PkgPath() == "" +} From 9aae5955bc400655843e51bae0e6c992bc769903 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Sat, 16 May 2020 10:43:00 +0800 Subject: [PATCH 070/122] change top pos --- gee-cache/doc/geecache.md | 1 + 1 file changed, 1 insertion(+) diff --git a/gee-cache/doc/geecache.md b/gee-cache/doc/geecache.md index 3eb89ad..ab0219b 100644 --- a/gee-cache/doc/geecache.md +++ b/gee-cache/doc/geecache.md @@ -11,6 +11,7 @@ keywords: - Go语言 - 从零实现分布式缓存 - 动手写分布式缓存 +top: 2 image: post/geecache/geecache_sm.jpg github: https://github.com/geektutu/7days-golang --- From 21f733ba6d69dbc722821db5eb42a769f40a41cc Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Sat, 16 May 2020 10:45:11 +0800 Subject: [PATCH 071/122] change top pos --- gee-cache/doc/geecache.md | 1 - 1 file changed, 1 deletion(-) diff --git a/gee-cache/doc/geecache.md b/gee-cache/doc/geecache.md index ab0219b..3eb89ad 100644 --- a/gee-cache/doc/geecache.md +++ b/gee-cache/doc/geecache.md @@ -11,7 +11,6 @@ keywords: - Go语言 - 从零实现分布式缓存 - 动手写分布式缓存 -top: 2 image: post/geecache/geecache_sm.jpg github: https://github.com/geektutu/7days-golang --- From b4651d23b0f213e550c2af4c3cd07cbd76de0657 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Wed, 30 Sep 2020 01:04:47 +0800 Subject: [PATCH 072/122] add codec & server code for gee-rpc --- gee-rpc/day1-codec/codec/codec.go | 39 ++++++ gee-rpc/day1-codec/codec/gob.go | 57 ++++++++ gee-rpc/{day1-encode => day1-codec}/go.mod | 0 gee-rpc/day1-codec/main/main.go | 52 ++++++++ gee-rpc/day1-codec/server.go | 144 +++++++++++++++++++++ gee-rpc/day1-encode/protocol/codec.go | 26 ---- gee-rpc/day1-encode/protocol/message.go | 113 ---------------- gee-rpc/day1-encode/protocol/path.go | 3 - gee-rpc/day1-encode/server/server.go | 76 ----------- gee-rpc/day1-encode/server/server_test.go | 76 ----------- gee-rpc/day1-encode/server/service.go | 99 -------------- gee-rpc/day3-service/service.go | 96 ++++++++++++++ 12 files changed, 388 insertions(+), 393 deletions(-) create mode 100644 gee-rpc/day1-codec/codec/codec.go create mode 100644 gee-rpc/day1-codec/codec/gob.go rename gee-rpc/{day1-encode => day1-codec}/go.mod (100%) create mode 100644 gee-rpc/day1-codec/main/main.go create mode 100644 gee-rpc/day1-codec/server.go delete mode 100755 gee-rpc/day1-encode/protocol/codec.go delete mode 100755 gee-rpc/day1-encode/protocol/message.go delete mode 100755 gee-rpc/day1-encode/protocol/path.go delete mode 100755 gee-rpc/day1-encode/server/server.go delete mode 100755 gee-rpc/day1-encode/server/server_test.go delete mode 100755 gee-rpc/day1-encode/server/service.go create mode 100644 gee-rpc/day3-service/service.go diff --git a/gee-rpc/day1-codec/codec/codec.go b/gee-rpc/day1-codec/codec/codec.go new file mode 100644 index 0000000..54d2e56 --- /dev/null +++ b/gee-rpc/day1-codec/codec/codec.go @@ -0,0 +1,39 @@ +package codec + +import ( + "io" + "sync" +) + +type Header struct { + ServiceMethod string // format "Service.Method" + Seq uint64 // sequence number chosen by client + Error string +} + +var HeaderPool = sync.Pool{ + New: func() interface{} { return &Header{} }, +} + +type Codec interface { + io.Closer + ReadHeader(*Header) error + ReadBody(interface{}) error + Write(*Header, interface{}) error +} + +type NewCodecFunc func(io.ReadWriteCloser) Codec + +type Type string + +const ( + GobType Type = "application/gob" + JsonType Type = "application/json" +) + +var NewCodecFuncMap map[Type]NewCodecFunc + +func init() { + NewCodecFuncMap = make(map[Type]NewCodecFunc) + NewCodecFuncMap[GobType] = NewGobCodec +} diff --git a/gee-rpc/day1-codec/codec/gob.go b/gee-rpc/day1-codec/codec/gob.go new file mode 100644 index 0000000..808d97b --- /dev/null +++ b/gee-rpc/day1-codec/codec/gob.go @@ -0,0 +1,57 @@ +package codec + +import ( + "bufio" + "encoding/gob" + "io" + "log" +) + +type GobCodec struct { + conn io.ReadWriteCloser + buf *bufio.Writer + dec *gob.Decoder + enc *gob.Encoder +} + +var _ Codec = (*GobCodec)(nil) + +func NewGobCodec(conn io.ReadWriteCloser) Codec { + buf := bufio.NewWriter(conn) + return &GobCodec{ + conn: conn, + buf: buf, + dec: gob.NewDecoder(conn), + enc: gob.NewEncoder(buf), + } +} + +func (c *GobCodec) ReadHeader(h *Header) error { + return c.dec.Decode(h) +} + +func (c *GobCodec) ReadBody(body interface{}) error { + return c.dec.Decode(body) +} + +func (c *GobCodec) Write(h *Header, body interface{}) (err error) { + defer func() { + _ = c.buf.Flush() + if err != nil { + _ = c.Close() + } + }() + if err := c.enc.Encode(h); err != nil { + log.Println("rpc: gob error encoding header:", err) + return err + } + if err := c.enc.Encode(body); err != nil { + log.Println("rpc: gob error encoding body:", err) + return err + } + return nil +} + +func (c *GobCodec) Close() error { + return c.conn.Close() +} diff --git a/gee-rpc/day1-encode/go.mod b/gee-rpc/day1-codec/go.mod similarity index 100% rename from gee-rpc/day1-encode/go.mod rename to gee-rpc/day1-codec/go.mod diff --git a/gee-rpc/day1-codec/main/main.go b/gee-rpc/day1-codec/main/main.go new file mode 100644 index 0000000..96f2273 --- /dev/null +++ b/gee-rpc/day1-codec/main/main.go @@ -0,0 +1,52 @@ +package main + +import ( + "encoding/json" + "fmt" + "geerpc" + "geerpc/codec" + "log" + "net" + "net/http" + "time" +) + +func startServer() { + l, err := net.Listen("tcp", ":9999") + if err != nil { + log.Panic("network error:", err) + } + geerpc.Accept(l) + +} + +func main() { + log.SetPrefix("") + go startServer() + time.Sleep(time.Second) + + // In fact, following code is like a simple GeeRPC Client + conn, _ := net.Dial("tcp", ":9999") + defer func() { _ = conn.Close() }() + + // negotiate options + _ = json.NewEncoder(conn).Encode(&geerpc.Options{ + MagicNumber: geerpc.MagicNumber, + CodecType: codec.GobType, + }) + + cc := codec.NewGobCodec(conn) + // send request & receive response + for i := 0; i < 3; i++ { + h := &codec.Header{ + ServiceMethod: "Foo.Sum", + Seq: uint64(i), + } + _ = cc.Write(h, fmt.Sprintf("geerpc req %d", h.Seq)) + _ = cc.ReadHeader(h) + var reply string + _ = cc.ReadBody(&reply) + log.Println("reply:", reply) + } + log.Fatal(http.ListenAndServe(":9999", nil)) +} diff --git a/gee-rpc/day1-codec/server.go b/gee-rpc/day1-codec/server.go new file mode 100644 index 0000000..3e32f1f --- /dev/null +++ b/gee-rpc/day1-codec/server.go @@ -0,0 +1,144 @@ +package geerpc + +import ( + "encoding/json" + "fmt" + "geerpc/codec" + "io" + "log" + "net" + "sync" +) + +type Server struct{} + +const MagicNumber = 0x3bef5c + +type Options struct { + MagicNumber int // MagicNumber marks this's a geerpc request + CodecType codec.Type // client may choose different Codec to encode body +} + +func newServer() *Server { + return &Server{} +} + +var DefaultServer = newServer() + +func (server *Server) ServeConn(conn io.ReadWriteCloser) { + defer func() { _ = conn.Close() }() + var opt Options + if err := json.NewDecoder(conn).Decode(&opt); err != nil { + log.Println("rpc server: options error: ", err) + return + } + if opt.MagicNumber != MagicNumber { + log.Printf("rpc server: invalid magic number %x", opt.MagicNumber) + return + } + f := codec.NewCodecFuncMap[opt.CodecType] + if f == nil { + log.Printf("rpc server: invalid codec type %s", opt.CodecType) + return + } + server.ServeCodec(f(conn)) +} + +// request stores all information of a call +type request struct { + h *codec.Header // header of request argv + argv string // TODO suppose argv is a string +} + +var requestPool = sync.Pool{ + New: func() interface{} { return &request{} }, +} + +func (server *Server) readRequestHeader(cc codec.Codec) (req *request, keepReading bool, err error) { + req, _ = requestPool.Get().(*request) + h, _ := codec.HeaderPool.Get().(*codec.Header) + if err = cc.ReadHeader(h); err != nil { + log.Println("rpc server: read header error:", err) + return + } + // We read the header successfully. If we see an error now, + // we can still recover and move on to the next request. + keepReading = true + req.h = h + return +} + +func (server *Server) readRequest(cc codec.Codec) (req *request, keepReading bool, err error) { + req, keepReading, err = server.readRequestHeader(cc) + if err != nil { + // discard argv + _ = cc.ReadBody(nil) + return + } + + // We read the header successfully. If we see an error now, + // we can still recover and move on to the next request. + keepReading = true + + // TODO: suppose argv is a string, now we can't judge the type of request argv + var str string + if err = cc.ReadBody(&str); err != nil { + log.Println("rpc server: read argv err:", err) + } + req.argv = str + return +} + +func (server *Server) sendResponse(cc codec.Codec, h *codec.Header, body interface{}, sending *sync.Mutex) { + sending.Lock() + defer sending.Unlock() + if err := cc.Write(h, body); err != nil { + log.Println("rpc server: write response error:", err) + } +} + +func (server *Server) Handle(cc codec.Codec, req *request, sending *sync.Mutex, wg *sync.WaitGroup) { + // TODO, should call registered rpc methods + // day 1 just print argv and send a hello message + defer wg.Done() + log.Println(req.h, req.argv) + server.sendResponse(cc, req.h, fmt.Sprintf("geerpc resp %d", req.h.Seq), sending) +} + +// invalidRequest is a placeholder for response argv when error occurs +var invalidRequest = struct{}{} + +func (server *Server) ServeCodec(cc codec.Codec) { + sending := new(sync.Mutex) // ensure header and argv is not separated by other response + wg := new(sync.WaitGroup) // wait until all request are handled + for { + req, keepReading, err := server.readRequest(cc) + if err != nil { + if !keepReading { + break // it's not possible to recover, so close the connection + } + if req != nil { + req.h.Error = err.Error() + server.sendResponse(cc, req.h, invalidRequest, sending) + } + continue + } + wg.Add(1) + go server.Handle(cc, req, sending, wg) + } + wg.Wait() + _ = cc.Close() +} + +func (server *Server) Accept(lis net.Listener) { + for { + conn, err := lis.Accept() + if err != nil { + log.Println("rpc server: accept error:", err) + return + } + go server.ServeConn(conn) + } +} + +func Accept(lis net.Listener) { DefaultServer.Accept(lis) } diff --git a/gee-rpc/day1-encode/protocol/codec.go b/gee-rpc/day1-encode/protocol/codec.go deleted file mode 100755 index 62b3b50..0000000 --- a/gee-rpc/day1-encode/protocol/codec.go +++ /dev/null @@ -1,26 +0,0 @@ -package protocol - -import ( - "bytes" - "encoding/json" -) - -type Codec interface { - Encode(i interface{}) ([]byte, error) - Decode(data []byte, i interface{}) error -} - -// JSONCodec uses json marshaler and unmarshaler. -type JSONCodec struct{} - -// Encode encodes an object into slice of bytes. -func (c JSONCodec) Encode(i interface{}) ([]byte, error) { - return json.Marshal(i) -} - -// Decode decodes an object from slice of bytes. -func (c JSONCodec) Decode(data []byte, i interface{}) error { - d := json.NewDecoder(bytes.NewBuffer(data)) - d.UseNumber() - return d.Decode(i) -} diff --git a/gee-rpc/day1-encode/protocol/message.go b/gee-rpc/day1-encode/protocol/message.go deleted file mode 100755 index 08dd3ca..0000000 --- a/gee-rpc/day1-encode/protocol/message.go +++ /dev/null @@ -1,113 +0,0 @@ -package protocol - -import ( - "bytes" - "encoding/binary" - "fmt" - "io" - "strings" -) - -const MagicNumber int32 = 0xECABCD - -type SerializeType int8 - -const ( - JSON SerializeType = iota -) - -var Codecs = map[SerializeType]Codec{ - JSON: &JSONCodec{}, -} - -type Status int8 - -const ( - OK Status = iota - ExecError - NotFoundError -) - -type Header struct { - Magic int32 - Status Status - SerializeType SerializeType - ServiceMethodSize int32 - PayloadSize int32 -} - -type Message struct { - *Header - ServiceMethod string - Payload []byte -} - -func NewMessage() *Message { - return &Message{ - Header: &Header{Magic: MagicNumber}, - } -} - -func (m *Message) HandleError(status Status, err error) *Message { - m.Status = status - _ = m.SetPayload(err) - return m -} - -func (m *Message) SetServiceMethod(name string) { - m.ServiceMethod = name -} - -func (m *Message) GetServiceMethod() (service, method string, err error) { - parts := strings.Split(m.ServiceMethod, ".") - if len(parts) != 2 || parts[0] == "" || parts[1] == "" { - return "", "", fmt.Errorf(" format error") - } - return parts[0], parts[1], nil -} - -func (m *Message) GetPayload(i interface{}) error { - return Codecs[m.SerializeType].Decode(m.Payload, i) -} - -func (m *Message) SetPayload(i interface{}) (err error) { - m.Payload, err = Codecs[m.SerializeType].Encode(i) - return -} - -func (m *Message) Clone() *Message { - m2 := NewMessage() - *m2.Header = *m.Header - m2.ServiceMethod = m.ServiceMethod - return m2 -} -func Read(r io.Reader) (*Message, error) { - m := NewMessage() - if err := binary.Read(r, binary.BigEndian, m.Header); err != nil { - return nil, err - } - if m.Magic != MagicNumber { - return nil, fmt.Errorf("invalid message: wrong magic number") - } - - buf := make([]byte, m.ServiceMethodSize+m.PayloadSize) - if err := binary.Read(r, binary.BigEndian, buf); err != nil { - return nil, err - } - m.ServiceMethod = string(buf[:m.ServiceMethodSize]) - m.Payload = buf[m.ServiceMethodSize:] - return m, nil -} -func (m *Message) Write(w io.Writer) error { - m.PayloadSize = int32(len(m.Payload)) - m.ServiceMethodSize = int32(len(m.ServiceMethod)) - buf := bytes.NewBufferString(m.ServiceMethod) - buf.Write(m.Payload) - if err := binary.Write(w, binary.BigEndian, m.Header); err != nil { - return err - } - if err := binary.Write(w, binary.BigEndian, buf.Bytes()); err != nil { - return err - } - return nil -} diff --git a/gee-rpc/day1-encode/protocol/path.go b/gee-rpc/day1-encode/protocol/path.go deleted file mode 100755 index cbd854c..0000000 --- a/gee-rpc/day1-encode/protocol/path.go +++ /dev/null @@ -1,3 +0,0 @@ -package protocol - -const DefaultRPCPath = "/_geerpc" diff --git a/gee-rpc/day1-encode/server/server.go b/gee-rpc/day1-encode/server/server.go deleted file mode 100755 index 021b3ae..0000000 --- a/gee-rpc/day1-encode/server/server.go +++ /dev/null @@ -1,76 +0,0 @@ -package server - -import ( - "fmt" - "log" - "net" - "net/http" - - "geerpc/protocol" -) - -type Server struct { - ln net.Listener - service map[string]*service -} - -func NewServer() *Server { - return &Server{ - service: make(map[string]*service), - } -} - -func (s *Server) Address() net.Addr { - return s.ln.Addr() -} - -func (s *Server) Serve(network, address string) (err error) { - if network == "http" { - if s.ln, err = net.Listen("tcp", address); err != nil { - return err - } - http.Handle(protocol.DefaultRPCPath, s) - return http.Serve(s.ln, nil) - } - panic(network + " not support") -} - -func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) { - m, err := protocol.Read(req.Body) - if err != nil { - log.Println("failed to read message from body") - _, _ = w.Write([]byte("fail")) - return - } - - log.Println(req.Method, m.ServiceMethod) - - respMsg := s.call(m) - _ = respMsg.Write(w) -} - -func (s *Server) Register(receiver interface{}) { - service := newService(receiver) - s.service[service.name] = service -} - -func (s *Server) call(req *protocol.Message) (resp *protocol.Message) { - serviceName, methodName, err := req.GetServiceMethod() - resp = req.Clone() - if err != nil { - return resp.HandleError(protocol.NotFoundError, err) - } - - service := s.service[serviceName] - if service == nil || service.method[methodName] == nil { - return resp.HandleError(protocol.NotFoundError, fmt.Errorf("%s not found", req.ServiceMethod)) - } - - return service.call(methodName, req) -} - -func _assert(condition bool, msg string, v ...interface{}) { - if !condition { - panic(fmt.Sprintf(msg, v...)) - } -} diff --git a/gee-rpc/day1-encode/server/server_test.go b/gee-rpc/day1-encode/server/server_test.go deleted file mode 100755 index 6aed5a5..0000000 --- a/gee-rpc/day1-encode/server/server_test.go +++ /dev/null @@ -1,76 +0,0 @@ -package server - -import ( - "bytes" - "fmt" - "geerpc/protocol" - "net" - "net/http" - "testing" - "time" -) - -type Calc struct{} - -type Req struct { - Num1 int - Num2 int -} - -func (c *Calc) Add(req Req, reply *int) error { - *reply = req.Num1 + req.Num2 - return nil -} - -func TestServer_Register(t *testing.T) { - s := NewServer() - s.Register(&Calc{}) - - service := s.service["Calc"] - if service == nil || service.method["Add"] == nil { - t.Fatal("failed to register") - } -} - -func TestServer_Call(t *testing.T) { - s := NewServer() - s.Register(&Calc{}) - req := &Req{Num1: 10, Num2: 20} - - reqMsg := protocol.NewMessage() - reqMsg.SetServiceMethod("Calc.Add") - _ = reqMsg.SetPayload(req) - - respMsg := s.call(reqMsg) - var ans int - _ = respMsg.GetPayload(&ans) - if ans != req.Num1+req.Num2 { - t.Fatal("failed to call Calc.Add") - } -} - -func TestServer_Serve(t *testing.T) { - s := NewServer() - s.Register(&Calc{}) - go func() { _ = s.Serve("http", ":0") }() - - time.Sleep(time.Second) - port := s.Address().(*net.TCPAddr).Port - addr := fmt.Sprintf("http://localhost:%d%s", port, protocol.DefaultRPCPath) - - reqMsg := protocol.NewMessage() - reqMsg.SetServiceMethod("Calc.Add") - _ = reqMsg.SetPayload(&Req{1, 2}) - - var buf bytes.Buffer - _ = reqMsg.Write(&buf) - resp, _ := http.Post(addr, "application/octet-stream", &buf) - - respMsg, _ := protocol.Read(resp.Body) - - var ans int - _ = respMsg.GetPayload(&ans) - if respMsg.Status != protocol.OK || ans != 3 { - t.Fatal("failed to call Calc.Add") - } -} diff --git a/gee-rpc/day1-encode/server/service.go b/gee-rpc/day1-encode/server/service.go deleted file mode 100755 index 2cc506b..0000000 --- a/gee-rpc/day1-encode/server/service.go +++ /dev/null @@ -1,99 +0,0 @@ -package server - -import ( - "geerpc/protocol" - "go/ast" - "log" - "reflect" -) - -type methodType struct { - method reflect.Method - ArgType reflect.Type - ReplyType reflect.Type -} - -func (m *methodType) NewArg() interface{} { - return newTypeInter(m.ArgType) -} - -func (m *methodType) NewReply() interface{} { - return newTypeInter(m.ReplyType) -} - -func newTypeInter(t reflect.Type) interface{} { - var v reflect.Value - if t.Kind() == reflect.Ptr { // reply must be ptr - v = reflect.New(t.Elem()) - } else { - v = reflect.New(t) - } - return v.Interface() -} - -type service struct { - name string - rcvr reflect.Value - method map[string]*methodType -} - -func newService(receiver interface{}) *service { - service := new(service) - service.method = make(map[string]*methodType) - service.name = reflect.Indirect(reflect.ValueOf(receiver)).Type().Name() - service.rcvr = reflect.ValueOf(receiver) - - _assert(ast.IsExported(service.name), "%service is not exported", service.name) - rcvrType := reflect.TypeOf(receiver) - for i := 0; i < rcvrType.NumMethod(); i++ { - method := rcvrType.Method(i) - mType := method.Type - if mType.NumIn() != 3 || mType.NumOut() != 1 { - continue - } - if mType.Out(0) != reflect.TypeOf((*error)(nil)).Elem() { - continue - } - - argType, replyType := mType.In(1), mType.In(2) - if !isExportedOrBuiltinType(argType) || !isExportedOrBuiltinType(replyType) { - continue - } - - service.method[method.Name] = &methodType{ - method: method, - ArgType: argType, - ReplyType: replyType, - } - log.Printf("Register %s.%s\n", service.name, method.Name) - } - - return service -} - -func (s *service) call(methodName string, reqMsg *protocol.Message) (resp *protocol.Message) { - mType := s.method[methodName] - resp = reqMsg.Clone() - - arg, reply := mType.NewArg(), mType.NewReply() - if err := reqMsg.GetPayload(arg); err != nil { - return resp.HandleError(protocol.ExecError, err) - } - - f := mType.method.Func - returnValues := f.Call([]reflect.Value{s.rcvr, reflect.ValueOf(arg).Elem(), reflect.ValueOf(reply)}) - - if errInter := returnValues[0].Interface(); errInter != nil { - return resp.HandleError(protocol.ExecError, errInter.(error)) - } - - if err := resp.SetPayload(reply); err != nil { - return resp.HandleError(protocol.ExecError, err) - } - - return resp -} - -func isExportedOrBuiltinType(t reflect.Type) bool { - return ast.IsExported(t.Name()) || t.PkgPath() == "" -} diff --git a/gee-rpc/day3-service/service.go b/gee-rpc/day3-service/service.go new file mode 100644 index 0000000..de7182b --- /dev/null +++ b/gee-rpc/day3-service/service.go @@ -0,0 +1,96 @@ +package geerpc + +import ( + "go/ast" + "log" + "reflect" +) + +type methodType struct { + method reflect.Method + argType reflect.Type + replyType reflect.Type +} + +func (m *methodType) NewArg() reflect.Value { + var argv reflect.Value + // arg may be a pointer type, or a value type + if m.argType.Kind() == reflect.Ptr { + argv = reflect.New(m.argType.Elem()) + } else { + argv = reflect.New(m.argType) + } + return argv +} + +func (m *methodType) NewReply() interface{} { + // reply must be a pointer type + replyv := reflect.New(m.replyType.Elem()) + switch m.replyType.Elem().Kind() { + case reflect.Map: + replyv.Elem().Set(reflect.MakeMap(m.replyType.Elem())) + case reflect.Slice: + replyv.Elem().Set(reflect.MakeSlice(m.replyType.Elem(), 0, 0)) + } + return replyv +} + +type service struct { + name string + typ reflect.Type + rcvr reflect.Value + method map[string]*methodType +} + +func newService(rcvr interface{}) *service { + s := new(service) + s.rcvr = reflect.ValueOf(rcvr) + s.name = reflect.Indirect(s.rcvr).Type().Name() + s.typ = reflect.TypeOf(rcvr) + if !ast.IsExported(s.name) { + log.Fatalf("rpc server: %s is not a valid service name", s.name) + } + return s +} + +func (s *service) registerMethods() { + s.method = make(map[string]*methodType) + for i := 0; i < s.typ.NumMethod(); i++ { + method := s.typ.Method(i) + mType := method.Type + if mType.NumIn() != 3 || mType.NumOut() != 1 { + continue + } + if mType.Out(0) != reflect.TypeOf((*error)(nil)).Elem() { + continue + } + + argType, replyType := mType.In(1), mType.In(2) + if !isExportedOrBuiltinType(argType) || !isExportedOrBuiltinType(replyType) { + continue + } + s.method[method.Name] = &methodType{ + method: method, + argType: argType, + replyType: replyType, + } + log.Printf("rpc server: register %s.%s\n", s.name, method.Name) + } +} + +func (s *service) call(mType methodType, argv, replyv reflect.Value) error { + f := mType.method.Func + // if argv is not a ptr, need to indirect before calling. + if mType.argType.Kind() != reflect.Ptr { + argv = argv.Elem() + } + returnValues := f.Call([]reflect.Value{s.rcvr, argv, replyv}) + if errInter := returnValues[0].Interface(); errInter != nil { + return errInter.(error) + } + return nil +} + +func isExportedOrBuiltinType(t reflect.Type) bool { + return ast.IsExported(t.Name()) || t.PkgPath() == "" +} From dfdb0ed8bb5a259e6430bcfce73d15e91718f423 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Thu, 1 Oct 2020 15:59:49 +0800 Subject: [PATCH 073/122] geerpc day2, implement a rpc client --- gee-rpc/day1-codec/main/main.go | 24 ++-- gee-rpc/day1-codec/server.go | 106 ++++++++------ gee-rpc/day2-client/client.go | 219 +++++++++++++++++++++++++++++ gee-rpc/day2-client/codec/codec.go | 39 +++++ gee-rpc/day2-client/codec/gob.go | 57 ++++++++ gee-rpc/day2-client/go.mod | 3 + gee-rpc/day2-client/main/main.go | 36 +++++ gee-rpc/day2-client/server.go | 170 ++++++++++++++++++++++ 8 files changed, 601 insertions(+), 53 deletions(-) create mode 100644 gee-rpc/day2-client/client.go create mode 100644 gee-rpc/day2-client/codec/codec.go create mode 100644 gee-rpc/day2-client/codec/gob.go create mode 100644 gee-rpc/day2-client/go.mod create mode 100644 gee-rpc/day2-client/main/main.go create mode 100644 gee-rpc/day2-client/server.go diff --git a/gee-rpc/day1-codec/main/main.go b/gee-rpc/day1-codec/main/main.go index 96f2273..95dd9e2 100644 --- a/gee-rpc/day1-codec/main/main.go +++ b/gee-rpc/day1-codec/main/main.go @@ -7,29 +7,28 @@ import ( "geerpc/codec" "log" "net" - "net/http" - "time" ) -func startServer() { - l, err := net.Listen("tcp", ":9999") +func startServer(addr chan string) { + // pick a free port + l, err := net.Listen("tcp", ":0") if err != nil { - log.Panic("network error:", err) + log.Fatal("network error:", err) } + log.Println("start rpc server on", l.Addr()) + addr <- l.Addr().String() geerpc.Accept(l) - } func main() { - log.SetPrefix("") - go startServer() - time.Sleep(time.Second) + addr := make(chan string) + go startServer(addr) - // In fact, following code is like a simple GeeRPC Client - conn, _ := net.Dial("tcp", ":9999") + // in fact, following code is like a simple geerpc client + conn, _ := net.Dial("tcp", <-addr) defer func() { _ = conn.Close() }() - // negotiate options + // send options _ = json.NewEncoder(conn).Encode(&geerpc.Options{ MagicNumber: geerpc.MagicNumber, CodecType: codec.GobType, @@ -48,5 +47,4 @@ func main() { _ = cc.ReadBody(&reply) log.Println("reply:", reply) } - log.Fatal(http.ListenAndServe(":9999", nil)) } diff --git a/gee-rpc/day1-codec/server.go b/gee-rpc/day1-codec/server.go index 3e32f1f..7646f9d 100644 --- a/gee-rpc/day1-codec/server.go +++ b/gee-rpc/day1-codec/server.go @@ -1,3 +1,7 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + package geerpc import ( @@ -7,11 +11,10 @@ import ( "io" "log" "net" + "reflect" "sync" ) -type Server struct{} - const MagicNumber = 0x3bef5c type Options struct { @@ -19,12 +22,24 @@ type Options struct { CodecType codec.Type // client may choose different Codec to encode body } -func newServer() *Server { +var defaultOptions = &Options{ + MagicNumber: MagicNumber, + CodecType: codec.GobType, +} + +// Server represents an RPC Server. +type Server struct{} + +// NewServer returns a new Server. +func NewServer() *Server { return &Server{} } -var DefaultServer = newServer() +// DefaultServer is the default instance of *Server. +var DefaultServer = NewServer() +// ServeConn runs the server on a single connection. +// ServeConn blocks, serving the connection until the client hangs up. func (server *Server) ServeConn(conn io.ReadWriteCloser) { defer func() { _ = conn.Close() }() var opt Options @@ -41,13 +56,38 @@ func (server *Server) ServeConn(conn io.ReadWriteCloser) { log.Printf("rpc server: invalid codec type %s", opt.CodecType) return } - server.ServeCodec(f(conn)) + server.serveCodec(f(conn)) +} + +// invalidRequest is a placeholder for response argv when error occurs +var invalidRequest = struct{}{} + +func (server *Server) serveCodec(cc codec.Codec) { + sending := new(sync.Mutex) // ensure header and argv is not separated by other response + wg := new(sync.WaitGroup) // wait until all request are handled + for { + req, keepReading, err := server.readRequest(cc) + if err != nil { + if !keepReading { + break // it's not possible to recover, so close the connection + } + if req != nil { + req.h.Error = err.Error() + server.sendResponse(cc, req.h, invalidRequest, sending) + } + continue + } + wg.Add(1) + go server.handleRequest(cc, req, sending, wg) + } + wg.Wait() + _ = cc.Close() } // request stores all information of a call type request struct { - h *codec.Header // header of request argv - argv string // TODO suppose argv is a string + h *codec.Header // header of request + argv, replyv reflect.Value // argv and replyv of request } var requestPool = sync.Pool{ @@ -58,6 +98,10 @@ func (server *Server) readRequestHeader(cc codec.Codec) (req *request, keepReadi req, _ = requestPool.Get().(*request) h, _ := codec.HeaderPool.Get().(*codec.Header) if err = cc.ReadHeader(h); err != nil { + // client closed the connection + if err == io.EOF || err != io.ErrUnexpectedEOF { + return + } log.Println("rpc server: read header error:", err) return } @@ -80,12 +124,12 @@ func (server *Server) readRequest(cc codec.Codec) (req *request, keepReading boo // we can still recover and move on to the next request. keepReading = true - // TODO: suppose argv is a string, now we can't judge the type of request argv - var str string - if err = cc.ReadBody(&str); err != nil { + // TODO: now we can't judge the type of request argv + // day 1, just suppose it's string + req.argv = reflect.New(reflect.TypeOf("")) + if err = cc.ReadBody(req.argv.Interface()); err != nil { log.Println("rpc server: read argv err:", err) } - req.argv = str return } @@ -97,39 +141,19 @@ func (server *Server) sendResponse(cc codec.Codec, h *codec.Header, body interfa } } -func (server *Server) Handle(cc codec.Codec, req *request, sending *sync.Mutex, wg *sync.WaitGroup) { - // TODO, should call registered rpc methods - // day 1 just print argv and send a hello message +func (server *Server) handleRequest(cc codec.Codec, req *request, sending *sync.Mutex, wg *sync.WaitGroup) { + // TODO, should call registered rpc methods to get the right replyv + // day 1, just print argv and send a hello message defer wg.Done() - log.Println(req.h, req.argv) - server.sendResponse(cc, req.h, fmt.Sprintf("geerpc resp %d", req.h.Seq), sending) -} - -// invalidRequest is a placeholder for response argv when error occurs -var invalidRequest = struct{}{} + defer codec.HeaderPool.Put(req.h) // recycle Header object + log.Println(req.h, req.argv.Elem()) + req.replyv = reflect.ValueOf(fmt.Sprintf("geerpc resp %d", req.h.Seq)) + server.sendResponse(cc, req.h, req.replyv.Interface(), sending) -func (server *Server) ServeCodec(cc codec.Codec) { - sending := new(sync.Mutex) // ensure header and argv is not separated by other response - wg := new(sync.WaitGroup) // wait until all request are handled - for { - req, keepReading, err := server.readRequest(cc) - if err != nil { - if !keepReading { - break // it's not possible to recover, so close the connection - } - if req != nil { - req.h.Error = err.Error() - server.sendResponse(cc, req.h, invalidRequest, sending) - } - continue - } - wg.Add(1) - go server.Handle(cc, req, sending, wg) - } - wg.Wait() - _ = cc.Close() } +// Accept accepts connections on the listener and serves requests +// for each incoming connection. func (server *Server) Accept(lis net.Listener) { for { conn, err := lis.Accept() @@ -141,4 +165,6 @@ func (server *Server) Accept(lis net.Listener) { } } +// Accept accepts connections on the listener and serves requests +// for each incoming connection. func Accept(lis net.Listener) { DefaultServer.Accept(lis) } diff --git a/gee-rpc/day2-client/client.go b/gee-rpc/day2-client/client.go new file mode 100644 index 0000000..79ef524 --- /dev/null +++ b/gee-rpc/day2-client/client.go @@ -0,0 +1,219 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package geerpc + +import ( + "encoding/json" + "errors" + "fmt" + "geerpc/codec" + "io" + "log" + "net" + "sync" +) + +// Call represents an active RPC. +type Call struct { + ServiceMethod string // format "." + Args interface{} // arguments to the function + Reply interface{} // reply from the function + Error error // if error occurs, it will be set + Done chan *Call // Strobes when call is complete. +} + +func (call *Call) done() { + call.Done <- call +} + +// Client represents an RPC Client. +// There may be multiple outstanding Calls associated +// with a single Client, and a Client may be used by +// multiple goroutines simultaneously. +type Client struct { + cc codec.Codec + sending sync.Mutex // protect sending a complete request + mu sync.Mutex // protect following + seq uint64 + pending map[uint64]*Call + closed bool // user has called Close +} + +var _ io.Closer = (*Client)(nil) + +var ErrShutdown = errors.New("connection is shut down") + +// Close the connection +func (client *Client) Close() error { + client.mu.Lock() + defer client.mu.Unlock() + if client.closed { + return ErrShutdown + } + client.closed = true + return client.cc.Close() +} + +func (client *Client) registerCall(call *Call) (uint64, error) { + client.mu.Lock() + defer client.mu.Unlock() + if client.closed { + return 0, ErrShutdown + } + seq := client.seq + client.pending[seq] = call + client.seq++ + return seq, nil +} + +func (client *Client) removeCall(seq uint64) *Call { + client.mu.Lock() + defer client.mu.Unlock() + call := client.pending[seq] + delete(client.pending, seq) + return call +} + +func (client *Client) terminateCalls(err error) { + client.sending.Lock() + defer client.sending.Unlock() + client.mu.Lock() + defer client.mu.Unlock() + for _, call := range client.pending { + call.Error = err + call.done() + } +} + +func (client *Client) send(call *Call) { + // make sure that the client will send a complete request + client.sending.Lock() + defer client.sending.Unlock() + + // register this call. + seq, err := client.registerCall(call) + if err != nil { + call.Error = err + call.done() + return + } + + // prepare request header + h, _ := codec.HeaderPool.Get().(*codec.Header) + h.ServiceMethod = call.ServiceMethod + h.Seq = seq + h.Error = "" + defer codec.HeaderPool.Put(h) + + // encode and send the request + if err := client.cc.Write(h, call.Args); err != nil { + call := client.removeCall(seq) + // call may be nil, it usually means that Write partially failed, + // client has received the response and handled + if call != nil { + call.Error = err + call.done() + } + } +} + +func (client *Client) receive() { + h, _ := codec.HeaderPool.Get().(*codec.Header) + defer codec.HeaderPool.Put(h) + var err error + for err == nil { + if err = client.cc.ReadHeader(h); err != nil { + break + } + call := client.removeCall(h.Seq) + switch { + case call == nil: + // it usually means that Write partially failed + // and call was already removed. + err = client.cc.ReadBody(nil) + case h.Error != "": + call.Error = fmt.Errorf(h.Error) + err = client.cc.ReadBody(nil) + call.done() + default: + err = client.cc.ReadBody(call.Reply) + call.done() + } + } + // error occurs, so terminateCalls pending calls + client.terminateCalls(err) +} + +// Go invokes the function asynchronously. +// It returns the Call structure representing the invocation. +func (client *Client) Go(serviceMethod string, args, reply interface{}, done chan *Call) *Call { + if done == nil { + done = make(chan *Call, 10) + } else if cap(done) == 0 { + log.Panic("rpc client: done channel is unbuffered") + } + call := &Call{ + ServiceMethod: serviceMethod, + Args: args, + Reply: reply, + Done: done, + } + client.send(call) + return call +} + +// Call invokes the named function, waits for it to complete, +// and returns its error status. +func (client *Client) Call(serviceMethod string, args, reply interface{}) error { + call := <-client.Go(serviceMethod, args, reply, make(chan *Call, 1)).Done + return call.Error +} + +func NewClient(conn io.ReadWriteCloser, opt *Options) (*Client, error) { + var err error + defer func() { + if err != nil { + _ = conn.Close() + } + }() + if opt.MagicNumber == 0 { + opt.MagicNumber = MagicNumber + } + f := codec.NewCodecFuncMap[opt.CodecType] + if f == nil { + err = fmt.Errorf("invalid codec type %s", opt.CodecType) + log.Println("rpc client: codec error:", err) + return nil, err + } + // send options with server + if err = json.NewEncoder(conn).Encode(opt); err != nil { + log.Println("rpc client: options error: ", err) + return nil, err + } + return newClientCodec(f(conn)), nil +} + +func newClientCodec(cc codec.Codec) *Client { + client := &Client{ + cc: cc, + pending: make(map[uint64]*Call), + } + go client.receive() + return client +} + +// DialWithOptions connects to an RPC server at the specified network address +func DialWithOptions(network, address string, opt *Options) (*Client, error) { + conn, err := net.Dial(network, address) + if err != nil { + return nil, err + } + return NewClient(conn, opt) +} + +// Dial connects to an RPC server at the specified network address +func Dial(network, address string) (*Client, error) { + return DialWithOptions(network, address, defaultOptions) +} diff --git a/gee-rpc/day2-client/codec/codec.go b/gee-rpc/day2-client/codec/codec.go new file mode 100644 index 0000000..54d2e56 --- /dev/null +++ b/gee-rpc/day2-client/codec/codec.go @@ -0,0 +1,39 @@ +package codec + +import ( + "io" + "sync" +) + +type Header struct { + ServiceMethod string // format "Service.Method" + Seq uint64 // sequence number chosen by client + Error string +} + +var HeaderPool = sync.Pool{ + New: func() interface{} { return &Header{} }, +} + +type Codec interface { + io.Closer + ReadHeader(*Header) error + ReadBody(interface{}) error + Write(*Header, interface{}) error +} + +type NewCodecFunc func(io.ReadWriteCloser) Codec + +type Type string + +const ( + GobType Type = "application/gob" + JsonType Type = "application/json" +) + +var NewCodecFuncMap map[Type]NewCodecFunc + +func init() { + NewCodecFuncMap = make(map[Type]NewCodecFunc) + NewCodecFuncMap[GobType] = NewGobCodec +} diff --git a/gee-rpc/day2-client/codec/gob.go b/gee-rpc/day2-client/codec/gob.go new file mode 100644 index 0000000..808d97b --- /dev/null +++ b/gee-rpc/day2-client/codec/gob.go @@ -0,0 +1,57 @@ +package codec + +import ( + "bufio" + "encoding/gob" + "io" + "log" +) + +type GobCodec struct { + conn io.ReadWriteCloser + buf *bufio.Writer + dec *gob.Decoder + enc *gob.Encoder +} + +var _ Codec = (*GobCodec)(nil) + +func NewGobCodec(conn io.ReadWriteCloser) Codec { + buf := bufio.NewWriter(conn) + return &GobCodec{ + conn: conn, + buf: buf, + dec: gob.NewDecoder(conn), + enc: gob.NewEncoder(buf), + } +} + +func (c *GobCodec) ReadHeader(h *Header) error { + return c.dec.Decode(h) +} + +func (c *GobCodec) ReadBody(body interface{}) error { + return c.dec.Decode(body) +} + +func (c *GobCodec) Write(h *Header, body interface{}) (err error) { + defer func() { + _ = c.buf.Flush() + if err != nil { + _ = c.Close() + } + }() + if err := c.enc.Encode(h); err != nil { + log.Println("rpc: gob error encoding header:", err) + return err + } + if err := c.enc.Encode(body); err != nil { + log.Println("rpc: gob error encoding body:", err) + return err + } + return nil +} + +func (c *GobCodec) Close() error { + return c.conn.Close() +} diff --git a/gee-rpc/day2-client/go.mod b/gee-rpc/day2-client/go.mod new file mode 100644 index 0000000..0ec8aeb --- /dev/null +++ b/gee-rpc/day2-client/go.mod @@ -0,0 +1,3 @@ +module geerpc + +go 1.13 diff --git a/gee-rpc/day2-client/main/main.go b/gee-rpc/day2-client/main/main.go new file mode 100644 index 0000000..b370e82 --- /dev/null +++ b/gee-rpc/day2-client/main/main.go @@ -0,0 +1,36 @@ +package main + +import ( + "fmt" + "geerpc" + "log" + "net" +) + +func startServer(addr chan string) { + // pick a free port + l, err := net.Listen("tcp", ":0") + if err != nil { + log.Fatal("network error:", err) + } + log.Println("start rpc server on", l.Addr()) + addr <- l.Addr().String() + geerpc.Accept(l) +} + +func main() { + addr := make(chan string) + go startServer(addr) + client, _ := geerpc.Dial("tcp", <-addr) + defer func() { _ = client.Close() }() + + // send request & receive response + for i := 0; i < 3; i++ { + args := fmt.Sprintf("geerpc req %d", i) + var reply string + if err := client.Call("Foo.Sum", args, &reply); err != nil { + log.Fatal("call Foo.Sum error", err) + } + log.Println("reply:", reply) + } +} diff --git a/gee-rpc/day2-client/server.go b/gee-rpc/day2-client/server.go new file mode 100644 index 0000000..7646f9d --- /dev/null +++ b/gee-rpc/day2-client/server.go @@ -0,0 +1,170 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package geerpc + +import ( + "encoding/json" + "fmt" + "geerpc/codec" + "io" + "log" + "net" + "reflect" + "sync" +) + +const MagicNumber = 0x3bef5c + +type Options struct { + MagicNumber int // MagicNumber marks this's a geerpc request + CodecType codec.Type // client may choose different Codec to encode body +} + +var defaultOptions = &Options{ + MagicNumber: MagicNumber, + CodecType: codec.GobType, +} + +// Server represents an RPC Server. +type Server struct{} + +// NewServer returns a new Server. +func NewServer() *Server { + return &Server{} +} + +// DefaultServer is the default instance of *Server. +var DefaultServer = NewServer() + +// ServeConn runs the server on a single connection. +// ServeConn blocks, serving the connection until the client hangs up. +func (server *Server) ServeConn(conn io.ReadWriteCloser) { + defer func() { _ = conn.Close() }() + var opt Options + if err := json.NewDecoder(conn).Decode(&opt); err != nil { + log.Println("rpc server: options error: ", err) + return + } + if opt.MagicNumber != MagicNumber { + log.Printf("rpc server: invalid magic number %x", opt.MagicNumber) + return + } + f := codec.NewCodecFuncMap[opt.CodecType] + if f == nil { + log.Printf("rpc server: invalid codec type %s", opt.CodecType) + return + } + server.serveCodec(f(conn)) +} + +// invalidRequest is a placeholder for response argv when error occurs +var invalidRequest = struct{}{} + +func (server *Server) serveCodec(cc codec.Codec) { + sending := new(sync.Mutex) // ensure header and argv is not separated by other response + wg := new(sync.WaitGroup) // wait until all request are handled + for { + req, keepReading, err := server.readRequest(cc) + if err != nil { + if !keepReading { + break // it's not possible to recover, so close the connection + } + if req != nil { + req.h.Error = err.Error() + server.sendResponse(cc, req.h, invalidRequest, sending) + } + continue + } + wg.Add(1) + go server.handleRequest(cc, req, sending, wg) + } + wg.Wait() + _ = cc.Close() +} + +// request stores all information of a call +type request struct { + h *codec.Header // header of request + argv, replyv reflect.Value // argv and replyv of request +} + +var requestPool = sync.Pool{ + New: func() interface{} { return &request{} }, +} + +func (server *Server) readRequestHeader(cc codec.Codec) (req *request, keepReading bool, err error) { + req, _ = requestPool.Get().(*request) + h, _ := codec.HeaderPool.Get().(*codec.Header) + if err = cc.ReadHeader(h); err != nil { + // client closed the connection + if err == io.EOF || err != io.ErrUnexpectedEOF { + return + } + log.Println("rpc server: read header error:", err) + return + } + // We read the header successfully. If we see an error now, + // we can still recover and move on to the next request. + keepReading = true + req.h = h + return +} + +func (server *Server) readRequest(cc codec.Codec) (req *request, keepReading bool, err error) { + req, keepReading, err = server.readRequestHeader(cc) + if err != nil { + // discard argv + _ = cc.ReadBody(nil) + return + } + + // We read the header successfully. If we see an error now, + // we can still recover and move on to the next request. + keepReading = true + + // TODO: now we can't judge the type of request argv + // day 1, just suppose it's string + req.argv = reflect.New(reflect.TypeOf("")) + if err = cc.ReadBody(req.argv.Interface()); err != nil { + log.Println("rpc server: read argv err:", err) + } + return +} + +func (server *Server) sendResponse(cc codec.Codec, h *codec.Header, body interface{}, sending *sync.Mutex) { + sending.Lock() + defer sending.Unlock() + if err := cc.Write(h, body); err != nil { + log.Println("rpc server: write response error:", err) + } +} + +func (server *Server) handleRequest(cc codec.Codec, req *request, sending *sync.Mutex, wg *sync.WaitGroup) { + // TODO, should call registered rpc methods to get the right replyv + // day 1, just print argv and send a hello message + defer wg.Done() + defer codec.HeaderPool.Put(req.h) // recycle Header object + log.Println(req.h, req.argv.Elem()) + req.replyv = reflect.ValueOf(fmt.Sprintf("geerpc resp %d", req.h.Seq)) + server.sendResponse(cc, req.h, req.replyv.Interface(), sending) + +} + +// Accept accepts connections on the listener and serves requests +// for each incoming connection. +func (server *Server) Accept(lis net.Listener) { + for { + conn, err := lis.Accept() + if err != nil { + log.Println("rpc server: accept error:", err) + return + } + go server.ServeConn(conn) + } +} + +// Accept accepts connections on the listener and serves requests +// for each incoming connection. +func Accept(lis net.Listener) { DefaultServer.Accept(lis) } From 0cc8314dff4ecca8a50d7126f2238136e99a1ce2 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Thu, 1 Oct 2020 16:18:30 +0800 Subject: [PATCH 074/122] fix HeaderPool recycle opportunity, add geerpc day3 service --- gee-rpc/day1-codec/codec/gob.go | 4 +- gee-rpc/day1-codec/main/main.go | 2 +- gee-rpc/day1-codec/server.go | 57 +++---- gee-rpc/day2-client/client.go | 8 +- gee-rpc/day2-client/codec/gob.go | 4 +- gee-rpc/day2-client/main/main.go | 21 ++- gee-rpc/day2-client/server.go | 57 +++---- gee-rpc/day3-service/client.go | 221 +++++++++++++++++++++++++++ gee-rpc/day3-service/codec/codec.go | 39 +++++ gee-rpc/day3-service/codec/gob.go | 57 +++++++ gee-rpc/day3-service/go.mod | 3 + gee-rpc/day3-service/main/main.go | 55 +++++++ gee-rpc/day3-service/server.go | 203 ++++++++++++++++++++++++ gee-rpc/day3-service/service.go | 19 ++- gee-rpc/day3-service/service_test.go | 48 ++++++ 15 files changed, 697 insertions(+), 101 deletions(-) create mode 100644 gee-rpc/day3-service/client.go create mode 100644 gee-rpc/day3-service/codec/codec.go create mode 100644 gee-rpc/day3-service/codec/gob.go create mode 100644 gee-rpc/day3-service/go.mod create mode 100644 gee-rpc/day3-service/main/main.go create mode 100644 gee-rpc/day3-service/server.go create mode 100644 gee-rpc/day3-service/service_test.go diff --git a/gee-rpc/day1-codec/codec/gob.go b/gee-rpc/day1-codec/codec/gob.go index 808d97b..e4b2a67 100644 --- a/gee-rpc/day1-codec/codec/gob.go +++ b/gee-rpc/day1-codec/codec/gob.go @@ -42,11 +42,11 @@ func (c *GobCodec) Write(h *Header, body interface{}) (err error) { } }() if err := c.enc.Encode(h); err != nil { - log.Println("rpc: gob error encoding header:", err) + log.Println("rpc codec: gob error encoding header:", err) return err } if err := c.enc.Encode(body); err != nil { - log.Println("rpc: gob error encoding body:", err) + log.Println("rpc codec: gob error encoding body:", err) return err } return nil diff --git a/gee-rpc/day1-codec/main/main.go b/gee-rpc/day1-codec/main/main.go index 95dd9e2..a715209 100644 --- a/gee-rpc/day1-codec/main/main.go +++ b/gee-rpc/day1-codec/main/main.go @@ -36,7 +36,7 @@ func main() { cc := codec.NewGobCodec(conn) // send request & receive response - for i := 0; i < 3; i++ { + for i := 0; i < 5; i++ { h := &codec.Header{ ServiceMethod: "Foo.Sum", Seq: uint64(i), diff --git a/gee-rpc/day1-codec/server.go b/gee-rpc/day1-codec/server.go index 7646f9d..5a8a9bf 100644 --- a/gee-rpc/day1-codec/server.go +++ b/gee-rpc/day1-codec/server.go @@ -63,18 +63,16 @@ func (server *Server) ServeConn(conn io.ReadWriteCloser) { var invalidRequest = struct{}{} func (server *Server) serveCodec(cc codec.Codec) { - sending := new(sync.Mutex) // ensure header and argv is not separated by other response + sending := new(sync.Mutex) // make sure to send a complete response wg := new(sync.WaitGroup) // wait until all request are handled for { - req, keepReading, err := server.readRequest(cc) + req, err := server.readRequest(cc) if err != nil { - if !keepReading { + if req == nil { break // it's not possible to recover, so close the connection } - if req != nil { - req.h.Error = err.Error() - server.sendResponse(cc, req.h, invalidRequest, sending) - } + req.h.Error = err.Error() + server.sendResponse(cc, req.h, invalidRequest, sending) continue } wg.Add(1) @@ -90,47 +88,31 @@ type request struct { argv, replyv reflect.Value // argv and replyv of request } -var requestPool = sync.Pool{ - New: func() interface{} { return &request{} }, -} - -func (server *Server) readRequestHeader(cc codec.Codec) (req *request, keepReading bool, err error) { - req, _ = requestPool.Get().(*request) +func (server *Server) readRequestHeader(cc codec.Codec) (*codec.Header, error) { h, _ := codec.HeaderPool.Get().(*codec.Header) - if err = cc.ReadHeader(h); err != nil { - // client closed the connection - if err == io.EOF || err != io.ErrUnexpectedEOF { - return + if err := cc.ReadHeader(h); err != nil { + codec.HeaderPool.Put(h) + if err != io.EOF && err != io.ErrUnexpectedEOF { + log.Println("rpc server: read header error:", err) } - log.Println("rpc server: read header error:", err) - return + return nil, err } - // We read the header successfully. If we see an error now, - // we can still recover and move on to the next request. - keepReading = true - req.h = h - return + return h, nil } -func (server *Server) readRequest(cc codec.Codec) (req *request, keepReading bool, err error) { - req, keepReading, err = server.readRequestHeader(cc) +func (server *Server) readRequest(cc codec.Codec) (*request, error) { + h, err := server.readRequestHeader(cc) if err != nil { - // discard argv - _ = cc.ReadBody(nil) - return + return nil, err } - - // We read the header successfully. If we see an error now, - // we can still recover and move on to the next request. - keepReading = true - - // TODO: now we can't judge the type of request argv + req := &request{h: h} + // TODO: now we don't know the type of request argv // day 1, just suppose it's string req.argv = reflect.New(reflect.TypeOf("")) if err = cc.ReadBody(req.argv.Interface()); err != nil { log.Println("rpc server: read argv err:", err) } - return + return req, nil } func (server *Server) sendResponse(cc codec.Codec, h *codec.Header, body interface{}, sending *sync.Mutex) { @@ -139,17 +121,16 @@ func (server *Server) sendResponse(cc codec.Codec, h *codec.Header, body interfa if err := cc.Write(h, body); err != nil { log.Println("rpc server: write response error:", err) } + codec.HeaderPool.Put(h) // recycle Header object } func (server *Server) handleRequest(cc codec.Codec, req *request, sending *sync.Mutex, wg *sync.WaitGroup) { // TODO, should call registered rpc methods to get the right replyv // day 1, just print argv and send a hello message defer wg.Done() - defer codec.HeaderPool.Put(req.h) // recycle Header object log.Println(req.h, req.argv.Elem()) req.replyv = reflect.ValueOf(fmt.Sprintf("geerpc resp %d", req.h.Seq)) server.sendResponse(cc, req.h, req.replyv.Interface(), sending) - } // Accept accepts connections on the listener and serves requests diff --git a/gee-rpc/day2-client/client.go b/gee-rpc/day2-client/client.go index 79ef524..2cbbb0c 100644 --- a/gee-rpc/day2-client/client.go +++ b/gee-rpc/day2-client/client.go @@ -120,11 +120,10 @@ func (client *Client) send(call *Call) { } func (client *Client) receive() { - h, _ := codec.HeaderPool.Get().(*codec.Header) - defer codec.HeaderPool.Put(h) + var h codec.Header var err error for err == nil { - if err = client.cc.ReadHeader(h); err != nil { + if err = client.cc.ReadHeader(&h); err != nil { break } call := client.removeCall(h.Seq) @@ -139,6 +138,9 @@ func (client *Client) receive() { call.done() default: err = client.cc.ReadBody(call.Reply) + if err != nil { + call.Error = errors.New("reading body " + err.Error()) + } call.done() } } diff --git a/gee-rpc/day2-client/codec/gob.go b/gee-rpc/day2-client/codec/gob.go index 808d97b..e4b2a67 100644 --- a/gee-rpc/day2-client/codec/gob.go +++ b/gee-rpc/day2-client/codec/gob.go @@ -42,11 +42,11 @@ func (c *GobCodec) Write(h *Header, body interface{}) (err error) { } }() if err := c.enc.Encode(h); err != nil { - log.Println("rpc: gob error encoding header:", err) + log.Println("rpc codec: gob error encoding header:", err) return err } if err := c.enc.Encode(body); err != nil { - log.Println("rpc: gob error encoding body:", err) + log.Println("rpc codec: gob error encoding body:", err) return err } return nil diff --git a/gee-rpc/day2-client/main/main.go b/gee-rpc/day2-client/main/main.go index b370e82..e291353 100644 --- a/gee-rpc/day2-client/main/main.go +++ b/gee-rpc/day2-client/main/main.go @@ -5,6 +5,7 @@ import ( "geerpc" "log" "net" + "sync" ) func startServer(addr chan string) { @@ -25,12 +26,18 @@ func main() { defer func() { _ = client.Close() }() // send request & receive response - for i := 0; i < 3; i++ { - args := fmt.Sprintf("geerpc req %d", i) - var reply string - if err := client.Call("Foo.Sum", args, &reply); err != nil { - log.Fatal("call Foo.Sum error", err) - } - log.Println("reply:", reply) + var wg sync.WaitGroup + for i := 0; i < 5; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + args := fmt.Sprintf("geerpc req %d", i) + var reply string + if err := client.Call("Foo.Sum", args, &reply); err != nil { + log.Fatal("call Foo.Sum error:", err) + } + log.Println("reply:", reply) + }(i) } + wg.Wait() } diff --git a/gee-rpc/day2-client/server.go b/gee-rpc/day2-client/server.go index 7646f9d..5a8a9bf 100644 --- a/gee-rpc/day2-client/server.go +++ b/gee-rpc/day2-client/server.go @@ -63,18 +63,16 @@ func (server *Server) ServeConn(conn io.ReadWriteCloser) { var invalidRequest = struct{}{} func (server *Server) serveCodec(cc codec.Codec) { - sending := new(sync.Mutex) // ensure header and argv is not separated by other response + sending := new(sync.Mutex) // make sure to send a complete response wg := new(sync.WaitGroup) // wait until all request are handled for { - req, keepReading, err := server.readRequest(cc) + req, err := server.readRequest(cc) if err != nil { - if !keepReading { + if req == nil { break // it's not possible to recover, so close the connection } - if req != nil { - req.h.Error = err.Error() - server.sendResponse(cc, req.h, invalidRequest, sending) - } + req.h.Error = err.Error() + server.sendResponse(cc, req.h, invalidRequest, sending) continue } wg.Add(1) @@ -90,47 +88,31 @@ type request struct { argv, replyv reflect.Value // argv and replyv of request } -var requestPool = sync.Pool{ - New: func() interface{} { return &request{} }, -} - -func (server *Server) readRequestHeader(cc codec.Codec) (req *request, keepReading bool, err error) { - req, _ = requestPool.Get().(*request) +func (server *Server) readRequestHeader(cc codec.Codec) (*codec.Header, error) { h, _ := codec.HeaderPool.Get().(*codec.Header) - if err = cc.ReadHeader(h); err != nil { - // client closed the connection - if err == io.EOF || err != io.ErrUnexpectedEOF { - return + if err := cc.ReadHeader(h); err != nil { + codec.HeaderPool.Put(h) + if err != io.EOF && err != io.ErrUnexpectedEOF { + log.Println("rpc server: read header error:", err) } - log.Println("rpc server: read header error:", err) - return + return nil, err } - // We read the header successfully. If we see an error now, - // we can still recover and move on to the next request. - keepReading = true - req.h = h - return + return h, nil } -func (server *Server) readRequest(cc codec.Codec) (req *request, keepReading bool, err error) { - req, keepReading, err = server.readRequestHeader(cc) +func (server *Server) readRequest(cc codec.Codec) (*request, error) { + h, err := server.readRequestHeader(cc) if err != nil { - // discard argv - _ = cc.ReadBody(nil) - return + return nil, err } - - // We read the header successfully. If we see an error now, - // we can still recover and move on to the next request. - keepReading = true - - // TODO: now we can't judge the type of request argv + req := &request{h: h} + // TODO: now we don't know the type of request argv // day 1, just suppose it's string req.argv = reflect.New(reflect.TypeOf("")) if err = cc.ReadBody(req.argv.Interface()); err != nil { log.Println("rpc server: read argv err:", err) } - return + return req, nil } func (server *Server) sendResponse(cc codec.Codec, h *codec.Header, body interface{}, sending *sync.Mutex) { @@ -139,17 +121,16 @@ func (server *Server) sendResponse(cc codec.Codec, h *codec.Header, body interfa if err := cc.Write(h, body); err != nil { log.Println("rpc server: write response error:", err) } + codec.HeaderPool.Put(h) // recycle Header object } func (server *Server) handleRequest(cc codec.Codec, req *request, sending *sync.Mutex, wg *sync.WaitGroup) { // TODO, should call registered rpc methods to get the right replyv // day 1, just print argv and send a hello message defer wg.Done() - defer codec.HeaderPool.Put(req.h) // recycle Header object log.Println(req.h, req.argv.Elem()) req.replyv = reflect.ValueOf(fmt.Sprintf("geerpc resp %d", req.h.Seq)) server.sendResponse(cc, req.h, req.replyv.Interface(), sending) - } // Accept accepts connections on the listener and serves requests diff --git a/gee-rpc/day3-service/client.go b/gee-rpc/day3-service/client.go new file mode 100644 index 0000000..2cbbb0c --- /dev/null +++ b/gee-rpc/day3-service/client.go @@ -0,0 +1,221 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package geerpc + +import ( + "encoding/json" + "errors" + "fmt" + "geerpc/codec" + "io" + "log" + "net" + "sync" +) + +// Call represents an active RPC. +type Call struct { + ServiceMethod string // format "." + Args interface{} // arguments to the function + Reply interface{} // reply from the function + Error error // if error occurs, it will be set + Done chan *Call // Strobes when call is complete. +} + +func (call *Call) done() { + call.Done <- call +} + +// Client represents an RPC Client. +// There may be multiple outstanding Calls associated +// with a single Client, and a Client may be used by +// multiple goroutines simultaneously. +type Client struct { + cc codec.Codec + sending sync.Mutex // protect sending a complete request + mu sync.Mutex // protect following + seq uint64 + pending map[uint64]*Call + closed bool // user has called Close +} + +var _ io.Closer = (*Client)(nil) + +var ErrShutdown = errors.New("connection is shut down") + +// Close the connection +func (client *Client) Close() error { + client.mu.Lock() + defer client.mu.Unlock() + if client.closed { + return ErrShutdown + } + client.closed = true + return client.cc.Close() +} + +func (client *Client) registerCall(call *Call) (uint64, error) { + client.mu.Lock() + defer client.mu.Unlock() + if client.closed { + return 0, ErrShutdown + } + seq := client.seq + client.pending[seq] = call + client.seq++ + return seq, nil +} + +func (client *Client) removeCall(seq uint64) *Call { + client.mu.Lock() + defer client.mu.Unlock() + call := client.pending[seq] + delete(client.pending, seq) + return call +} + +func (client *Client) terminateCalls(err error) { + client.sending.Lock() + defer client.sending.Unlock() + client.mu.Lock() + defer client.mu.Unlock() + for _, call := range client.pending { + call.Error = err + call.done() + } +} + +func (client *Client) send(call *Call) { + // make sure that the client will send a complete request + client.sending.Lock() + defer client.sending.Unlock() + + // register this call. + seq, err := client.registerCall(call) + if err != nil { + call.Error = err + call.done() + return + } + + // prepare request header + h, _ := codec.HeaderPool.Get().(*codec.Header) + h.ServiceMethod = call.ServiceMethod + h.Seq = seq + h.Error = "" + defer codec.HeaderPool.Put(h) + + // encode and send the request + if err := client.cc.Write(h, call.Args); err != nil { + call := client.removeCall(seq) + // call may be nil, it usually means that Write partially failed, + // client has received the response and handled + if call != nil { + call.Error = err + call.done() + } + } +} + +func (client *Client) receive() { + var h codec.Header + var err error + for err == nil { + if err = client.cc.ReadHeader(&h); err != nil { + break + } + call := client.removeCall(h.Seq) + switch { + case call == nil: + // it usually means that Write partially failed + // and call was already removed. + err = client.cc.ReadBody(nil) + case h.Error != "": + call.Error = fmt.Errorf(h.Error) + err = client.cc.ReadBody(nil) + call.done() + default: + err = client.cc.ReadBody(call.Reply) + if err != nil { + call.Error = errors.New("reading body " + err.Error()) + } + call.done() + } + } + // error occurs, so terminateCalls pending calls + client.terminateCalls(err) +} + +// Go invokes the function asynchronously. +// It returns the Call structure representing the invocation. +func (client *Client) Go(serviceMethod string, args, reply interface{}, done chan *Call) *Call { + if done == nil { + done = make(chan *Call, 10) + } else if cap(done) == 0 { + log.Panic("rpc client: done channel is unbuffered") + } + call := &Call{ + ServiceMethod: serviceMethod, + Args: args, + Reply: reply, + Done: done, + } + client.send(call) + return call +} + +// Call invokes the named function, waits for it to complete, +// and returns its error status. +func (client *Client) Call(serviceMethod string, args, reply interface{}) error { + call := <-client.Go(serviceMethod, args, reply, make(chan *Call, 1)).Done + return call.Error +} + +func NewClient(conn io.ReadWriteCloser, opt *Options) (*Client, error) { + var err error + defer func() { + if err != nil { + _ = conn.Close() + } + }() + if opt.MagicNumber == 0 { + opt.MagicNumber = MagicNumber + } + f := codec.NewCodecFuncMap[opt.CodecType] + if f == nil { + err = fmt.Errorf("invalid codec type %s", opt.CodecType) + log.Println("rpc client: codec error:", err) + return nil, err + } + // send options with server + if err = json.NewEncoder(conn).Encode(opt); err != nil { + log.Println("rpc client: options error: ", err) + return nil, err + } + return newClientCodec(f(conn)), nil +} + +func newClientCodec(cc codec.Codec) *Client { + client := &Client{ + cc: cc, + pending: make(map[uint64]*Call), + } + go client.receive() + return client +} + +// DialWithOptions connects to an RPC server at the specified network address +func DialWithOptions(network, address string, opt *Options) (*Client, error) { + conn, err := net.Dial(network, address) + if err != nil { + return nil, err + } + return NewClient(conn, opt) +} + +// Dial connects to an RPC server at the specified network address +func Dial(network, address string) (*Client, error) { + return DialWithOptions(network, address, defaultOptions) +} diff --git a/gee-rpc/day3-service/codec/codec.go b/gee-rpc/day3-service/codec/codec.go new file mode 100644 index 0000000..54d2e56 --- /dev/null +++ b/gee-rpc/day3-service/codec/codec.go @@ -0,0 +1,39 @@ +package codec + +import ( + "io" + "sync" +) + +type Header struct { + ServiceMethod string // format "Service.Method" + Seq uint64 // sequence number chosen by client + Error string +} + +var HeaderPool = sync.Pool{ + New: func() interface{} { return &Header{} }, +} + +type Codec interface { + io.Closer + ReadHeader(*Header) error + ReadBody(interface{}) error + Write(*Header, interface{}) error +} + +type NewCodecFunc func(io.ReadWriteCloser) Codec + +type Type string + +const ( + GobType Type = "application/gob" + JsonType Type = "application/json" +) + +var NewCodecFuncMap map[Type]NewCodecFunc + +func init() { + NewCodecFuncMap = make(map[Type]NewCodecFunc) + NewCodecFuncMap[GobType] = NewGobCodec +} diff --git a/gee-rpc/day3-service/codec/gob.go b/gee-rpc/day3-service/codec/gob.go new file mode 100644 index 0000000..808d97b --- /dev/null +++ b/gee-rpc/day3-service/codec/gob.go @@ -0,0 +1,57 @@ +package codec + +import ( + "bufio" + "encoding/gob" + "io" + "log" +) + +type GobCodec struct { + conn io.ReadWriteCloser + buf *bufio.Writer + dec *gob.Decoder + enc *gob.Encoder +} + +var _ Codec = (*GobCodec)(nil) + +func NewGobCodec(conn io.ReadWriteCloser) Codec { + buf := bufio.NewWriter(conn) + return &GobCodec{ + conn: conn, + buf: buf, + dec: gob.NewDecoder(conn), + enc: gob.NewEncoder(buf), + } +} + +func (c *GobCodec) ReadHeader(h *Header) error { + return c.dec.Decode(h) +} + +func (c *GobCodec) ReadBody(body interface{}) error { + return c.dec.Decode(body) +} + +func (c *GobCodec) Write(h *Header, body interface{}) (err error) { + defer func() { + _ = c.buf.Flush() + if err != nil { + _ = c.Close() + } + }() + if err := c.enc.Encode(h); err != nil { + log.Println("rpc: gob error encoding header:", err) + return err + } + if err := c.enc.Encode(body); err != nil { + log.Println("rpc: gob error encoding body:", err) + return err + } + return nil +} + +func (c *GobCodec) Close() error { + return c.conn.Close() +} diff --git a/gee-rpc/day3-service/go.mod b/gee-rpc/day3-service/go.mod new file mode 100644 index 0000000..0ec8aeb --- /dev/null +++ b/gee-rpc/day3-service/go.mod @@ -0,0 +1,3 @@ +module geerpc + +go 1.13 diff --git a/gee-rpc/day3-service/main/main.go b/gee-rpc/day3-service/main/main.go new file mode 100644 index 0000000..c526e11 --- /dev/null +++ b/gee-rpc/day3-service/main/main.go @@ -0,0 +1,55 @@ +package main + +import ( + "geerpc" + "log" + "net" + "sync" +) + +type Foo int + +type Args struct{ Num1, Num2 int } + +func (f Foo) Sum(args Args, reply *int) error { + *reply = args.Num1 + args.Num2 + return nil +} + +func startServer(addr chan string) { + var foo Foo + if err := geerpc.Register(&foo); err != nil { + log.Fatal("register error:", err) + } + // pick a free port + l, err := net.Listen("tcp", ":0") + if err != nil { + log.Fatal("network error:", err) + } + log.Println("start rpc server on", l.Addr()) + addr <- l.Addr().String() + geerpc.Accept(l) +} + +func main() { + addr := make(chan string) + go startServer(addr) + client, _ := geerpc.Dial("tcp", <-addr) + defer func() { _ = client.Close() }() + + // send request & receive response + var wg sync.WaitGroup + for i := 0; i < 5; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + args := &Args{Num1: i, Num2: i * i} + var reply int + if err := client.Call("Foo.Sum", args, &reply); err != nil { + log.Fatal("call Foo.Sum error:", err) + } + log.Printf("%d + %d = %d", args.Num1, args.Num2, reply) + }(i) + } + wg.Wait() +} diff --git a/gee-rpc/day3-service/server.go b/gee-rpc/day3-service/server.go new file mode 100644 index 0000000..f29f33d --- /dev/null +++ b/gee-rpc/day3-service/server.go @@ -0,0 +1,203 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package geerpc + +import ( + "encoding/json" + "errors" + "geerpc/codec" + "io" + "log" + "net" + "reflect" + "strings" + "sync" +) + +const MagicNumber = 0x3bef5c + +type Options struct { + MagicNumber int // MagicNumber marks this's a geerpc request + CodecType codec.Type // client may choose different Codec to encode body +} + +var defaultOptions = &Options{ + MagicNumber: MagicNumber, + CodecType: codec.GobType, +} + +// Server represents an RPC Server. +type Server struct { + serviceMap sync.Map +} + +// NewServer returns a new Server. +func NewServer() *Server { + return &Server{} +} + +// DefaultServer is the default instance of *Server. +var DefaultServer = NewServer() + +// ServeConn runs the server on a single connection. +// ServeConn blocks, serving the connection until the client hangs up. +func (server *Server) ServeConn(conn io.ReadWriteCloser) { + defer func() { _ = conn.Close() }() + var opt Options + if err := json.NewDecoder(conn).Decode(&opt); err != nil { + log.Println("rpc server: options error: ", err) + return + } + if opt.MagicNumber != MagicNumber { + log.Printf("rpc server: invalid magic number %x", opt.MagicNumber) + return + } + f := codec.NewCodecFuncMap[opt.CodecType] + if f == nil { + log.Printf("rpc server: invalid codec type %s", opt.CodecType) + return + } + server.serveCodec(f(conn)) +} + +// invalidRequest is a placeholder for response argv when error occurs +var invalidRequest = struct{}{} + +func (server *Server) serveCodec(cc codec.Codec) { + sending := new(sync.Mutex) // make sure to send a complete response + wg := new(sync.WaitGroup) // wait until all request are handled + for { + req, err := server.readRequest(cc) + if err != nil { + if req == nil { + break // it's not possible to recover, so close the connection + } + req.h.Error = err.Error() + server.sendResponse(cc, req.h, invalidRequest, sending) + continue + } + wg.Add(1) + go server.handleRequest(cc, req, sending, wg) + } + wg.Wait() + _ = cc.Close() +} + +// request stores all information of a call +type request struct { + h *codec.Header // header of request + argv, replyv reflect.Value // argv and replyv of request + mtype *methodType + svc *service +} + +func (server *Server) readRequestHeader(cc codec.Codec) (*codec.Header, error) { + h, _ := codec.HeaderPool.Get().(*codec.Header) + if err := cc.ReadHeader(h); err != nil { + codec.HeaderPool.Put(h) + if err != io.EOF && err != io.ErrUnexpectedEOF { + log.Println("rpc server: read header error:", err) + } + return nil, err + } + return h, nil +} + +func (server *Server) findService(serviceMethod string) (svc *service, mtype *methodType, err error) { + dot := strings.LastIndex(serviceMethod, ".") + if dot < 0 { + err = errors.New("rpc server: service/method request ill-formed: " + serviceMethod) + return + } + serviceName, methodName := serviceMethod[:dot], serviceMethod[dot+1:] + svci, ok := server.serviceMap.Load(serviceName) + if !ok { + err = errors.New("rpc server: can't find service " + serviceName) + return + } + svc = svci.(*service) + mtype = svc.method[methodName] + if mtype == nil { + err = errors.New("rpc server: can't find method " + methodName) + } + return +} + +func (server *Server) readRequest(cc codec.Codec) (*request, error) { + h, err := server.readRequestHeader(cc) + if err != nil { + return nil, err + } + req := &request{h: h} + req.svc, req.mtype, err = server.findService(h.ServiceMethod) + if err != nil { + return req, err + } + req.argv = req.mtype.newArgv() + req.replyv = req.mtype.newReplyv() + + // make sure that argvi is a pointer, ReadBody need a pointer as parameter + argvi := req.argv.Interface() + if req.argv.Type().Kind() != reflect.Ptr { + argvi = req.argv.Addr().Interface() + } + if err = cc.ReadBody(argvi); err != nil { + log.Println("rpc server: read body err:", err) + return req, err + } + return req, nil +} + +func (server *Server) sendResponse(cc codec.Codec, h *codec.Header, body interface{}, sending *sync.Mutex) { + sending.Lock() + defer sending.Unlock() + if err := cc.Write(h, body); err != nil { + log.Println("rpc server: write response error:", err) + } + codec.HeaderPool.Put(h) // recycle Header object +} + +func (server *Server) handleRequest(cc codec.Codec, req *request, sending *sync.Mutex, wg *sync.WaitGroup) { + defer wg.Done() + err := req.svc.call(req.mtype, req.argv, req.replyv) + if err != nil { + req.h.Error = err.Error() + } + server.sendResponse(cc, req.h, req.replyv.Interface(), sending) +} + +// Accept accepts connections on the listener and serves requests +// for each incoming connection. +func (server *Server) Accept(lis net.Listener) { + for { + conn, err := lis.Accept() + if err != nil { + log.Println("rpc server: accept error:", err) + return + } + go server.ServeConn(conn) + } +} + +// Accept accepts connections on the listener and serves requests +// for each incoming connection. +func Accept(lis net.Listener) { DefaultServer.Accept(lis) } + +// Register publishes in the server the set of methods of the +// receiver value that satisfy the following conditions: +// - exported method of exported type +// - two arguments, both of exported type +// - the second argument is a pointer +// - one return value, of type error +func (server *Server) Register(rcvr interface{}) error { + s := newService(rcvr) + if _, dup := server.serviceMap.LoadOrStore(s.name, s); dup { + return errors.New("rpc: service already defined: " + s.name) + } + return nil +} + +// Register publishes the receiver's methods in the DefaultServer. +func Register(rcvr interface{}) error { return DefaultServer.Register(rcvr) } diff --git a/gee-rpc/day3-service/service.go b/gee-rpc/day3-service/service.go index de7182b..fcdfe8d 100644 --- a/gee-rpc/day3-service/service.go +++ b/gee-rpc/day3-service/service.go @@ -4,26 +4,28 @@ import ( "go/ast" "log" "reflect" + "sync/atomic" ) type methodType struct { method reflect.Method argType reflect.Type replyType reflect.Type + numCalls uint64 } -func (m *methodType) NewArg() reflect.Value { +func (m *methodType) newArgv() reflect.Value { var argv reflect.Value // arg may be a pointer type, or a value type if m.argType.Kind() == reflect.Ptr { argv = reflect.New(m.argType.Elem()) } else { - argv = reflect.New(m.argType) + argv = reflect.New(m.argType).Elem() } return argv } -func (m *methodType) NewReply() interface{} { +func (m *methodType) newReplyv() reflect.Value { // reply must be a pointer type replyv := reflect.New(m.replyType.Elem()) switch m.replyType.Elem().Kind() { @@ -50,6 +52,7 @@ func newService(rcvr interface{}) *service { if !ast.IsExported(s.name) { log.Fatalf("rpc server: %s is not a valid service name", s.name) } + s.registerMethods() return s } @@ -64,7 +67,6 @@ func (s *service) registerMethods() { if mType.Out(0) != reflect.TypeOf((*error)(nil)).Elem() { continue } - argType, replyType := mType.In(1), mType.In(2) if !isExportedOrBuiltinType(argType) || !isExportedOrBuiltinType(replyType) { continue @@ -78,12 +80,9 @@ func (s *service) registerMethods() { } } -func (s *service) call(mType methodType, argv, replyv reflect.Value) error { - f := mType.method.Func - // if argv is not a ptr, need to indirect before calling. - if mType.argType.Kind() != reflect.Ptr { - argv = argv.Elem() - } +func (s *service) call(m *methodType, argv, replyv reflect.Value) error { + atomic.AddUint64(&m.numCalls, 1) + f := m.method.Func returnValues := f.Call([]reflect.Value{s.rcvr, argv, replyv}) if errInter := returnValues[0].Interface(); errInter != nil { return errInter.(error) diff --git a/gee-rpc/day3-service/service_test.go b/gee-rpc/day3-service/service_test.go new file mode 100644 index 0000000..4b35124 --- /dev/null +++ b/gee-rpc/day3-service/service_test.go @@ -0,0 +1,48 @@ +package geerpc + +import ( + "fmt" + "reflect" + "testing" +) + +type Foo int + +type Args struct{ Num1, Num2 int } + +func (f Foo) Sum(args Args, reply *int) error { + *reply = args.Num1 + args.Num2 + return nil +} + +// it's not a exported method +func (f Foo) sum(args Args, reply *int) error { + *reply = args.Num1 + args.Num2 + return nil +} + +func _assert(condition bool, msg string, v ...interface{}) { + if !condition { + panic(fmt.Sprintf("assertion failed: "+msg, v...)) + } +} + +func TestNewService(t *testing.T) { + var foo Foo + s := newService(&foo) + _assert(len(s.method) == 1, "wrong service method, expect 1, but got %d", len(s.method)) + mType := s.method["Sum"] + _assert(mType != nil, "wrong method, Sum shouldn't nil") +} + +func TestMethodType_Call(t *testing.T) { + var foo Foo + s := newService(&foo) + mType := s.method["Sum"] + + argv := mType.newArgv() + replyv := mType.newReplyv() + argv.Set(reflect.ValueOf(Args{Num1: 1, Num2: 3})) + err := s.call(mType, argv, replyv) + _assert(err == nil && *replyv.Interface().(*int) == 4 && mType.numCalls == 1, "failed to call Foo.Sum") +} From 7bd912d051d6e94c4e4a723800c4ad1625e82996 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Thu, 1 Oct 2020 19:24:57 +0800 Subject: [PATCH 075/122] remove headerPool to simplify the code --- gee-rpc/day1-codec/codec/codec.go | 5 ----- gee-rpc/day1-codec/server.go | 8 +++----- gee-rpc/day2-client/client.go | 15 +++++++-------- gee-rpc/day2-client/codec/codec.go | 5 ----- gee-rpc/day2-client/server.go | 8 +++----- gee-rpc/day3-service/client.go | 15 +++++++-------- gee-rpc/day3-service/codec/codec.go | 5 ----- gee-rpc/day3-service/server.go | 8 +++----- 8 files changed, 23 insertions(+), 46 deletions(-) diff --git a/gee-rpc/day1-codec/codec/codec.go b/gee-rpc/day1-codec/codec/codec.go index 54d2e56..ba28fba 100644 --- a/gee-rpc/day1-codec/codec/codec.go +++ b/gee-rpc/day1-codec/codec/codec.go @@ -2,7 +2,6 @@ package codec import ( "io" - "sync" ) type Header struct { @@ -11,10 +10,6 @@ type Header struct { Error string } -var HeaderPool = sync.Pool{ - New: func() interface{} { return &Header{} }, -} - type Codec interface { io.Closer ReadHeader(*Header) error diff --git a/gee-rpc/day1-codec/server.go b/gee-rpc/day1-codec/server.go index 5a8a9bf..06ef763 100644 --- a/gee-rpc/day1-codec/server.go +++ b/gee-rpc/day1-codec/server.go @@ -89,15 +89,14 @@ type request struct { } func (server *Server) readRequestHeader(cc codec.Codec) (*codec.Header, error) { - h, _ := codec.HeaderPool.Get().(*codec.Header) - if err := cc.ReadHeader(h); err != nil { - codec.HeaderPool.Put(h) + var h codec.Header + if err := cc.ReadHeader(&h); err != nil { if err != io.EOF && err != io.ErrUnexpectedEOF { log.Println("rpc server: read header error:", err) } return nil, err } - return h, nil + return &h, nil } func (server *Server) readRequest(cc codec.Codec) (*request, error) { @@ -121,7 +120,6 @@ func (server *Server) sendResponse(cc codec.Codec, h *codec.Header, body interfa if err := cc.Write(h, body); err != nil { log.Println("rpc server: write response error:", err) } - codec.HeaderPool.Put(h) // recycle Header object } func (server *Server) handleRequest(cc codec.Codec, req *request, sending *sync.Mutex, wg *sync.WaitGroup) { diff --git a/gee-rpc/day2-client/client.go b/gee-rpc/day2-client/client.go index 2cbbb0c..3fd885e 100644 --- a/gee-rpc/day2-client/client.go +++ b/gee-rpc/day2-client/client.go @@ -34,7 +34,8 @@ func (call *Call) done() { // multiple goroutines simultaneously. type Client struct { cc codec.Codec - sending sync.Mutex // protect sending a complete request + sending sync.Mutex // protect following + header codec.Header mu sync.Mutex // protect following seq uint64 pending map[uint64]*Call @@ -101,14 +102,12 @@ func (client *Client) send(call *Call) { } // prepare request header - h, _ := codec.HeaderPool.Get().(*codec.Header) - h.ServiceMethod = call.ServiceMethod - h.Seq = seq - h.Error = "" - defer codec.HeaderPool.Put(h) + client.header.ServiceMethod = call.ServiceMethod + client.header.Seq = seq + client.header.Error = "" // encode and send the request - if err := client.cc.Write(h, call.Args); err != nil { + if err := client.cc.Write(&client.header, call.Args); err != nil { call := client.removeCall(seq) // call may be nil, it usually means that Write partially failed, // client has received the response and handled @@ -120,9 +119,9 @@ func (client *Client) send(call *Call) { } func (client *Client) receive() { - var h codec.Header var err error for err == nil { + var h codec.Header if err = client.cc.ReadHeader(&h); err != nil { break } diff --git a/gee-rpc/day2-client/codec/codec.go b/gee-rpc/day2-client/codec/codec.go index 54d2e56..ba28fba 100644 --- a/gee-rpc/day2-client/codec/codec.go +++ b/gee-rpc/day2-client/codec/codec.go @@ -2,7 +2,6 @@ package codec import ( "io" - "sync" ) type Header struct { @@ -11,10 +10,6 @@ type Header struct { Error string } -var HeaderPool = sync.Pool{ - New: func() interface{} { return &Header{} }, -} - type Codec interface { io.Closer ReadHeader(*Header) error diff --git a/gee-rpc/day2-client/server.go b/gee-rpc/day2-client/server.go index 5a8a9bf..06ef763 100644 --- a/gee-rpc/day2-client/server.go +++ b/gee-rpc/day2-client/server.go @@ -89,15 +89,14 @@ type request struct { } func (server *Server) readRequestHeader(cc codec.Codec) (*codec.Header, error) { - h, _ := codec.HeaderPool.Get().(*codec.Header) - if err := cc.ReadHeader(h); err != nil { - codec.HeaderPool.Put(h) + var h codec.Header + if err := cc.ReadHeader(&h); err != nil { if err != io.EOF && err != io.ErrUnexpectedEOF { log.Println("rpc server: read header error:", err) } return nil, err } - return h, nil + return &h, nil } func (server *Server) readRequest(cc codec.Codec) (*request, error) { @@ -121,7 +120,6 @@ func (server *Server) sendResponse(cc codec.Codec, h *codec.Header, body interfa if err := cc.Write(h, body); err != nil { log.Println("rpc server: write response error:", err) } - codec.HeaderPool.Put(h) // recycle Header object } func (server *Server) handleRequest(cc codec.Codec, req *request, sending *sync.Mutex, wg *sync.WaitGroup) { diff --git a/gee-rpc/day3-service/client.go b/gee-rpc/day3-service/client.go index 2cbbb0c..3fd885e 100644 --- a/gee-rpc/day3-service/client.go +++ b/gee-rpc/day3-service/client.go @@ -34,7 +34,8 @@ func (call *Call) done() { // multiple goroutines simultaneously. type Client struct { cc codec.Codec - sending sync.Mutex // protect sending a complete request + sending sync.Mutex // protect following + header codec.Header mu sync.Mutex // protect following seq uint64 pending map[uint64]*Call @@ -101,14 +102,12 @@ func (client *Client) send(call *Call) { } // prepare request header - h, _ := codec.HeaderPool.Get().(*codec.Header) - h.ServiceMethod = call.ServiceMethod - h.Seq = seq - h.Error = "" - defer codec.HeaderPool.Put(h) + client.header.ServiceMethod = call.ServiceMethod + client.header.Seq = seq + client.header.Error = "" // encode and send the request - if err := client.cc.Write(h, call.Args); err != nil { + if err := client.cc.Write(&client.header, call.Args); err != nil { call := client.removeCall(seq) // call may be nil, it usually means that Write partially failed, // client has received the response and handled @@ -120,9 +119,9 @@ func (client *Client) send(call *Call) { } func (client *Client) receive() { - var h codec.Header var err error for err == nil { + var h codec.Header if err = client.cc.ReadHeader(&h); err != nil { break } diff --git a/gee-rpc/day3-service/codec/codec.go b/gee-rpc/day3-service/codec/codec.go index 54d2e56..ba28fba 100644 --- a/gee-rpc/day3-service/codec/codec.go +++ b/gee-rpc/day3-service/codec/codec.go @@ -2,7 +2,6 @@ package codec import ( "io" - "sync" ) type Header struct { @@ -11,10 +10,6 @@ type Header struct { Error string } -var HeaderPool = sync.Pool{ - New: func() interface{} { return &Header{} }, -} - type Codec interface { io.Closer ReadHeader(*Header) error diff --git a/gee-rpc/day3-service/server.go b/gee-rpc/day3-service/server.go index f29f33d..6558dad 100644 --- a/gee-rpc/day3-service/server.go +++ b/gee-rpc/day3-service/server.go @@ -94,15 +94,14 @@ type request struct { } func (server *Server) readRequestHeader(cc codec.Codec) (*codec.Header, error) { - h, _ := codec.HeaderPool.Get().(*codec.Header) - if err := cc.ReadHeader(h); err != nil { - codec.HeaderPool.Put(h) + var h codec.Header + if err := cc.ReadHeader(&h); err != nil { if err != io.EOF && err != io.ErrUnexpectedEOF { log.Println("rpc server: read header error:", err) } return nil, err } - return h, nil + return &h, nil } func (server *Server) findService(serviceMethod string) (svc *service, mtype *methodType, err error) { @@ -156,7 +155,6 @@ func (server *Server) sendResponse(cc codec.Codec, h *codec.Header, body interfa if err := cc.Write(h, body); err != nil { log.Println("rpc server: write response error:", err) } - codec.HeaderPool.Put(h) // recycle Header object } func (server *Server) handleRequest(cc codec.Codec, req *request, sending *sync.Mutex, wg *sync.WaitGroup) { From 4f981d40adab02909882ff23d94940783c58ada5 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Thu, 1 Oct 2020 22:12:48 +0800 Subject: [PATCH 076/122] add geerpc day4 http and debug --- gee-rpc/day2-client/client.go | 13 +- gee-rpc/day3-service/client.go | 13 +- gee-rpc/day3-service/service.go | 32 +-- gee-rpc/day4-http-debug/client.go | 254 ++++++++++++++++++++++++ gee-rpc/day4-http-debug/codec/codec.go | 34 ++++ gee-rpc/day4-http-debug/codec/gob.go | 57 ++++++ gee-rpc/day4-http-debug/debug.go | 60 ++++++ gee-rpc/day4-http-debug/go.mod | 3 + gee-rpc/day4-http-debug/main/main.go | 53 +++++ gee-rpc/day4-http-debug/server.go | 238 ++++++++++++++++++++++ gee-rpc/day4-http-debug/service.go | 95 +++++++++ gee-rpc/day4-http-debug/service_test.go | 48 +++++ 12 files changed, 870 insertions(+), 30 deletions(-) create mode 100644 gee-rpc/day4-http-debug/client.go create mode 100644 gee-rpc/day4-http-debug/codec/codec.go create mode 100644 gee-rpc/day4-http-debug/codec/gob.go create mode 100644 gee-rpc/day4-http-debug/debug.go create mode 100644 gee-rpc/day4-http-debug/go.mod create mode 100644 gee-rpc/day4-http-debug/main/main.go create mode 100644 gee-rpc/day4-http-debug/server.go create mode 100644 gee-rpc/day4-http-debug/service.go create mode 100644 gee-rpc/day4-http-debug/service_test.go diff --git a/gee-rpc/day2-client/client.go b/gee-rpc/day2-client/client.go index 3fd885e..83119f0 100644 --- a/gee-rpc/day2-client/client.go +++ b/gee-rpc/day2-client/client.go @@ -205,16 +205,15 @@ func newClientCodec(cc codec.Codec) *Client { return client } -// DialWithOptions connects to an RPC server at the specified network address -func DialWithOptions(network, address string, opt *Options) (*Client, error) { +// Dial connects to an RPC server at the specified network address +func Dial(network, address string, opts ...*Options) (*Client, error) { + opt := defaultOptions + if len(opts) > 0 && opts[0] != nil { + opt = opts[0] + } conn, err := net.Dial(network, address) if err != nil { return nil, err } return NewClient(conn, opt) } - -// Dial connects to an RPC server at the specified network address -func Dial(network, address string) (*Client, error) { - return DialWithOptions(network, address, defaultOptions) -} diff --git a/gee-rpc/day3-service/client.go b/gee-rpc/day3-service/client.go index 3fd885e..83119f0 100644 --- a/gee-rpc/day3-service/client.go +++ b/gee-rpc/day3-service/client.go @@ -205,16 +205,15 @@ func newClientCodec(cc codec.Codec) *Client { return client } -// DialWithOptions connects to an RPC server at the specified network address -func DialWithOptions(network, address string, opt *Options) (*Client, error) { +// Dial connects to an RPC server at the specified network address +func Dial(network, address string, opts ...*Options) (*Client, error) { + opt := defaultOptions + if len(opts) > 0 && opts[0] != nil { + opt = opts[0] + } conn, err := net.Dial(network, address) if err != nil { return nil, err } return NewClient(conn, opt) } - -// Dial connects to an RPC server at the specified network address -func Dial(network, address string) (*Client, error) { - return DialWithOptions(network, address, defaultOptions) -} diff --git a/gee-rpc/day3-service/service.go b/gee-rpc/day3-service/service.go index fcdfe8d..e0711cd 100644 --- a/gee-rpc/day3-service/service.go +++ b/gee-rpc/day3-service/service.go @@ -8,31 +8,31 @@ import ( ) type methodType struct { - method reflect.Method - argType reflect.Type - replyType reflect.Type - numCalls uint64 + Method reflect.Method + ArgType reflect.Type + ReplyType reflect.Type + NumCalls uint64 } func (m *methodType) newArgv() reflect.Value { var argv reflect.Value // arg may be a pointer type, or a value type - if m.argType.Kind() == reflect.Ptr { - argv = reflect.New(m.argType.Elem()) + if m.ArgType.Kind() == reflect.Ptr { + argv = reflect.New(m.ArgType.Elem()) } else { - argv = reflect.New(m.argType).Elem() + argv = reflect.New(m.ArgType).Elem() } return argv } func (m *methodType) newReplyv() reflect.Value { // reply must be a pointer type - replyv := reflect.New(m.replyType.Elem()) - switch m.replyType.Elem().Kind() { + replyv := reflect.New(m.ReplyType.Elem()) + switch m.ReplyType.Elem().Kind() { case reflect.Map: - replyv.Elem().Set(reflect.MakeMap(m.replyType.Elem())) + replyv.Elem().Set(reflect.MakeMap(m.ReplyType.Elem())) case reflect.Slice: - replyv.Elem().Set(reflect.MakeSlice(m.replyType.Elem(), 0, 0)) + replyv.Elem().Set(reflect.MakeSlice(m.ReplyType.Elem(), 0, 0)) } return replyv } @@ -72,17 +72,17 @@ func (s *service) registerMethods() { continue } s.method[method.Name] = &methodType{ - method: method, - argType: argType, - replyType: replyType, + Method: method, + ArgType: argType, + ReplyType: replyType, } log.Printf("rpc server: register %s.%s\n", s.name, method.Name) } } func (s *service) call(m *methodType, argv, replyv reflect.Value) error { - atomic.AddUint64(&m.numCalls, 1) - f := m.method.Func + atomic.AddUint64(&m.NumCalls, 1) + f := m.Method.Func returnValues := f.Call([]reflect.Value{s.rcvr, argv, replyv}) if errInter := returnValues[0].Interface(); errInter != nil { return errInter.(error) diff --git a/gee-rpc/day4-http-debug/client.go b/gee-rpc/day4-http-debug/client.go new file mode 100644 index 0000000..3b53c16 --- /dev/null +++ b/gee-rpc/day4-http-debug/client.go @@ -0,0 +1,254 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package geerpc + +import ( + "bufio" + "encoding/json" + "errors" + "fmt" + "geerpc/codec" + "io" + "log" + "net" + "net/http" + "sync" +) + +// Call represents an active RPC. +type Call struct { + ServiceMethod string // format "." + Args interface{} // arguments to the function + Reply interface{} // reply from the function + Error error // if error occurs, it will be set + Done chan *Call // Strobes when call is complete. +} + +func (call *Call) done() { + call.Done <- call +} + +// Client represents an RPC Client. +// There may be multiple outstanding Calls associated +// with a single Client, and a Client may be used by +// multiple goroutines simultaneously. +type Client struct { + cc codec.Codec + sending sync.Mutex // protect following + header codec.Header + mu sync.Mutex // protect following + seq uint64 + pending map[uint64]*Call + closed bool // user has called Close +} + +var _ io.Closer = (*Client)(nil) + +var ErrShutdown = errors.New("connection is shut down") + +// Close the connection +func (client *Client) Close() error { + client.mu.Lock() + defer client.mu.Unlock() + if client.closed { + return ErrShutdown + } + client.closed = true + return client.cc.Close() +} + +func (client *Client) registerCall(call *Call) (uint64, error) { + client.mu.Lock() + defer client.mu.Unlock() + if client.closed { + return 0, ErrShutdown + } + seq := client.seq + client.pending[seq] = call + client.seq++ + return seq, nil +} + +func (client *Client) removeCall(seq uint64) *Call { + client.mu.Lock() + defer client.mu.Unlock() + call := client.pending[seq] + delete(client.pending, seq) + return call +} + +func (client *Client) terminateCalls(err error) { + client.sending.Lock() + defer client.sending.Unlock() + client.mu.Lock() + defer client.mu.Unlock() + for _, call := range client.pending { + call.Error = err + call.done() + } +} + +func (client *Client) send(call *Call) { + // make sure that the client will send a complete request + client.sending.Lock() + defer client.sending.Unlock() + + // register this call. + seq, err := client.registerCall(call) + if err != nil { + call.Error = err + call.done() + return + } + + // prepare request header + client.header.ServiceMethod = call.ServiceMethod + client.header.Seq = seq + client.header.Error = "" + + // encode and send the request + if err := client.cc.Write(&client.header, call.Args); err != nil { + call := client.removeCall(seq) + // call may be nil, it usually means that Write partially failed, + // client has received the response and handled + if call != nil { + call.Error = err + call.done() + } + } +} + +func (client *Client) receive() { + var err error + for err == nil { + var h codec.Header + if err = client.cc.ReadHeader(&h); err != nil { + break + } + call := client.removeCall(h.Seq) + switch { + case call == nil: + // it usually means that Write partially failed + // and call was already removed. + err = client.cc.ReadBody(nil) + case h.Error != "": + call.Error = fmt.Errorf(h.Error) + err = client.cc.ReadBody(nil) + call.done() + default: + err = client.cc.ReadBody(call.Reply) + if err != nil { + call.Error = errors.New("reading body " + err.Error()) + } + call.done() + } + } + // error occurs, so terminateCalls pending calls + client.terminateCalls(err) +} + +// Go invokes the function asynchronously. +// It returns the Call structure representing the invocation. +func (client *Client) Go(serviceMethod string, args, reply interface{}, done chan *Call) *Call { + if done == nil { + done = make(chan *Call, 10) + } else if cap(done) == 0 { + log.Panic("rpc client: done channel is unbuffered") + } + call := &Call{ + ServiceMethod: serviceMethod, + Args: args, + Reply: reply, + Done: done, + } + client.send(call) + return call +} + +// Call invokes the named function, waits for it to complete, +// and returns its error status. +func (client *Client) Call(serviceMethod string, args, reply interface{}) error { + call := <-client.Go(serviceMethod, args, reply, make(chan *Call, 1)).Done + return call.Error +} + +func NewClient(conn io.ReadWriteCloser, opt *Options) (*Client, error) { + var err error + defer func() { + if err != nil { + _ = conn.Close() + } + }() + if opt.MagicNumber == 0 { + opt.MagicNumber = MagicNumber + } + f := codec.NewCodecFuncMap[opt.CodecType] + if f == nil { + err = fmt.Errorf("invalid codec type %s", opt.CodecType) + log.Println("rpc client: codec error:", err) + return nil, err + } + // send options with server + if err = json.NewEncoder(conn).Encode(opt); err != nil { + log.Println("rpc client: options error: ", err) + return nil, err + } + return newClientCodec(f(conn)), nil +} + +func newClientCodec(cc codec.Codec) *Client { + client := &Client{ + cc: cc, + pending: make(map[uint64]*Call), + } + go client.receive() + return client +} + +// Dial connects to an RPC server at the specified network address +func Dial(network, address string, opts ...*Options) (*Client, error) { + opt := defaultOptions + if len(opts) > 0 && opts[0] != nil { + opt = opts[0] + } + conn, err := net.Dial(network, address) + if err != nil { + return nil, err + } + return NewClient(conn, opt) +} + +// DialHTTP connects to an HTTP RPC server at the specified network address +// listening on the default HTTP RPC path. +func DialHTTP(network, address string, opts ...*Options) (*Client, error) { + return DialHTTPPath(network, address, defaultRPCPath, opts...) +} + +// DialHTTPPath connects to an HTTP RPC server +// at the specified network address and path. +func DialHTTPPath(network, address, path string, opts ...*Options) (*Client, error) { + opt := defaultOptions + if len(opts) > 0 && opts[0] != nil { + opt = opts[0] + } + var err error + conn, err := net.Dial(network, address) + if err != nil { + return nil, err + } + _, _ = io.WriteString(conn, fmt.Sprintf("CONNECT %s HTTP/1.0\n\n", path)) + + // Require successful HTTP response + // before switching to RPC protocol. + resp, err := http.ReadResponse(bufio.NewReader(conn), &http.Request{Method: "CONNECT"}) + if err == nil && resp.Status == connected { + return NewClient(conn, opt) + } + if err == nil { + err = errors.New("unexpected HTTP response: " + resp.Status) + } + _ = conn.Close() + return nil, err +} diff --git a/gee-rpc/day4-http-debug/codec/codec.go b/gee-rpc/day4-http-debug/codec/codec.go new file mode 100644 index 0000000..ba28fba --- /dev/null +++ b/gee-rpc/day4-http-debug/codec/codec.go @@ -0,0 +1,34 @@ +package codec + +import ( + "io" +) + +type Header struct { + ServiceMethod string // format "Service.Method" + Seq uint64 // sequence number chosen by client + Error string +} + +type Codec interface { + io.Closer + ReadHeader(*Header) error + ReadBody(interface{}) error + Write(*Header, interface{}) error +} + +type NewCodecFunc func(io.ReadWriteCloser) Codec + +type Type string + +const ( + GobType Type = "application/gob" + JsonType Type = "application/json" +) + +var NewCodecFuncMap map[Type]NewCodecFunc + +func init() { + NewCodecFuncMap = make(map[Type]NewCodecFunc) + NewCodecFuncMap[GobType] = NewGobCodec +} diff --git a/gee-rpc/day4-http-debug/codec/gob.go b/gee-rpc/day4-http-debug/codec/gob.go new file mode 100644 index 0000000..808d97b --- /dev/null +++ b/gee-rpc/day4-http-debug/codec/gob.go @@ -0,0 +1,57 @@ +package codec + +import ( + "bufio" + "encoding/gob" + "io" + "log" +) + +type GobCodec struct { + conn io.ReadWriteCloser + buf *bufio.Writer + dec *gob.Decoder + enc *gob.Encoder +} + +var _ Codec = (*GobCodec)(nil) + +func NewGobCodec(conn io.ReadWriteCloser) Codec { + buf := bufio.NewWriter(conn) + return &GobCodec{ + conn: conn, + buf: buf, + dec: gob.NewDecoder(conn), + enc: gob.NewEncoder(buf), + } +} + +func (c *GobCodec) ReadHeader(h *Header) error { + return c.dec.Decode(h) +} + +func (c *GobCodec) ReadBody(body interface{}) error { + return c.dec.Decode(body) +} + +func (c *GobCodec) Write(h *Header, body interface{}) (err error) { + defer func() { + _ = c.buf.Flush() + if err != nil { + _ = c.Close() + } + }() + if err := c.enc.Encode(h); err != nil { + log.Println("rpc: gob error encoding header:", err) + return err + } + if err := c.enc.Encode(body); err != nil { + log.Println("rpc: gob error encoding body:", err) + return err + } + return nil +} + +func (c *GobCodec) Close() error { + return c.conn.Close() +} diff --git a/gee-rpc/day4-http-debug/debug.go b/gee-rpc/day4-http-debug/debug.go new file mode 100644 index 0000000..d76de55 --- /dev/null +++ b/gee-rpc/day4-http-debug/debug.go @@ -0,0 +1,60 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package geerpc + +import ( + "fmt" + "html/template" + "net/http" +) + +const debugText = ` + + GeeRPC Services + {{range .}} +


+ Service {{.Name}} +
+
+ + {{range $name, $mtype := .Method}} + + + + + {{end}} +
MethodCalls
{{$name}}({{$mtype.ArgType}}, {{$mtype.ReplyType}}) error{{$mtype.NumCalls}}
+ {{end}} + + ` + +var debug = template.Must(template.New("RPC debug").Parse(debugText)) + +type debugHTTP struct { + *Server +} + +type debugService struct { + Name string + Method map[string]*methodType +} + +// Runs at /debug/rpc +func (server debugHTTP) ServeHTTP(w http.ResponseWriter, req *http.Request) { + // Build a sorted version of the data. + var services []debugService + server.serviceMap.Range(func(namei, svci interface{}) bool { + svc := svci.(*service) + services = append(services, debugService{ + Name: namei.(string), + Method: svc.method, + }) + return true + }) + err := debug.Execute(w, services) + if err != nil { + _, _ = fmt.Fprintln(w, "rpc: error executing template:", err.Error()) + } +} diff --git a/gee-rpc/day4-http-debug/go.mod b/gee-rpc/day4-http-debug/go.mod new file mode 100644 index 0000000..0ec8aeb --- /dev/null +++ b/gee-rpc/day4-http-debug/go.mod @@ -0,0 +1,3 @@ +module geerpc + +go 1.13 diff --git a/gee-rpc/day4-http-debug/main/main.go b/gee-rpc/day4-http-debug/main/main.go new file mode 100644 index 0000000..13bb592 --- /dev/null +++ b/gee-rpc/day4-http-debug/main/main.go @@ -0,0 +1,53 @@ +package main + +import ( + "geerpc" + "log" + "net/http" + "sync" + "time" +) + +type Foo int + +type Args struct{ Num1, Num2 int } + +func (f Foo) Sum(args Args, reply *int) error { + *reply = args.Num1 + args.Num2 + return nil +} + +func startServer(addr string) { + var foo Foo + _ = geerpc.Register(&foo) + geerpc.HandleHTTP() + log.Fatal(http.ListenAndServe(addr, nil)) +} + +func call() { + // start server may cost some time + time.Sleep(time.Second) + client, _ := geerpc.DialHTTP("tcp", ":9999") + defer func() { _ = client.Close() }() + + // send request & receive response + var wg sync.WaitGroup + for i := 0; i < 5; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + args := &Args{Num1: i, Num2: i * i} + var reply int + if err := client.Call("Foo.Sum", args, &reply); err != nil { + log.Fatal("call Foo.Sum error:", err) + } + log.Printf("%d + %d = %d", args.Num1, args.Num2, reply) + }(i) + } + wg.Wait() +} + +func main() { + go call() + startServer(":9999") +} diff --git a/gee-rpc/day4-http-debug/server.go b/gee-rpc/day4-http-debug/server.go new file mode 100644 index 0000000..ffb85ee --- /dev/null +++ b/gee-rpc/day4-http-debug/server.go @@ -0,0 +1,238 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package geerpc + +import ( + "encoding/json" + "errors" + "geerpc/codec" + "io" + "log" + "net" + "net/http" + "reflect" + "strings" + "sync" +) + +const MagicNumber = 0x3bef5c + +type Options struct { + MagicNumber int // MagicNumber marks this's a geerpc request + CodecType codec.Type // client may choose different Codec to encode body +} + +var defaultOptions = &Options{ + MagicNumber: MagicNumber, + CodecType: codec.GobType, +} + +// Server represents an RPC Server. +type Server struct { + serviceMap sync.Map +} + +// NewServer returns a new Server. +func NewServer() *Server { + return &Server{} +} + +// DefaultServer is the default instance of *Server. +var DefaultServer = NewServer() + +// ServeConn runs the server on a single connection. +// ServeConn blocks, serving the connection until the client hangs up. +func (server *Server) ServeConn(conn io.ReadWriteCloser) { + defer func() { _ = conn.Close() }() + var opt Options + if err := json.NewDecoder(conn).Decode(&opt); err != nil { + log.Println("rpc server: options error: ", err) + return + } + if opt.MagicNumber != MagicNumber { + log.Printf("rpc server: invalid magic number %x", opt.MagicNumber) + return + } + f := codec.NewCodecFuncMap[opt.CodecType] + if f == nil { + log.Printf("rpc server: invalid codec type %s", opt.CodecType) + return + } + server.serveCodec(f(conn)) +} + +// invalidRequest is a placeholder for response argv when error occurs +var invalidRequest = struct{}{} + +func (server *Server) serveCodec(cc codec.Codec) { + sending := new(sync.Mutex) // make sure to send a complete response + wg := new(sync.WaitGroup) // wait until all request are handled + for { + req, err := server.readRequest(cc) + if err != nil { + if req == nil { + break // it's not possible to recover, so close the connection + } + req.h.Error = err.Error() + server.sendResponse(cc, req.h, invalidRequest, sending) + continue + } + wg.Add(1) + go server.handleRequest(cc, req, sending, wg) + } + wg.Wait() + _ = cc.Close() +} + +// request stores all information of a call +type request struct { + h *codec.Header // header of request + argv, replyv reflect.Value // argv and replyv of request + mtype *methodType + svc *service +} + +func (server *Server) readRequestHeader(cc codec.Codec) (*codec.Header, error) { + var h codec.Header + if err := cc.ReadHeader(&h); err != nil { + if err != io.EOF && err != io.ErrUnexpectedEOF { + log.Println("rpc server: read header error:", err) + } + return nil, err + } + return &h, nil +} + +func (server *Server) findService(serviceMethod string) (svc *service, mtype *methodType, err error) { + dot := strings.LastIndex(serviceMethod, ".") + if dot < 0 { + err = errors.New("rpc server: service/Method request ill-formed: " + serviceMethod) + return + } + serviceName, methodName := serviceMethod[:dot], serviceMethod[dot+1:] + svci, ok := server.serviceMap.Load(serviceName) + if !ok { + err = errors.New("rpc server: can't find service " + serviceName) + return + } + svc = svci.(*service) + mtype = svc.method[methodName] + if mtype == nil { + err = errors.New("rpc server: can't find Method " + methodName) + } + return +} + +func (server *Server) readRequest(cc codec.Codec) (*request, error) { + h, err := server.readRequestHeader(cc) + if err != nil { + return nil, err + } + req := &request{h: h} + req.svc, req.mtype, err = server.findService(h.ServiceMethod) + if err != nil { + return req, err + } + req.argv = req.mtype.newArgv() + req.replyv = req.mtype.newReplyv() + + // make sure that argvi is a pointer, ReadBody need a pointer as parameter + argvi := req.argv.Interface() + if req.argv.Type().Kind() != reflect.Ptr { + argvi = req.argv.Addr().Interface() + } + if err = cc.ReadBody(argvi); err != nil { + log.Println("rpc server: read body err:", err) + return req, err + } + return req, nil +} + +func (server *Server) sendResponse(cc codec.Codec, h *codec.Header, body interface{}, sending *sync.Mutex) { + sending.Lock() + defer sending.Unlock() + if err := cc.Write(h, body); err != nil { + log.Println("rpc server: write response error:", err) + } +} + +func (server *Server) handleRequest(cc codec.Codec, req *request, sending *sync.Mutex, wg *sync.WaitGroup) { + defer wg.Done() + err := req.svc.call(req.mtype, req.argv, req.replyv) + if err != nil { + req.h.Error = err.Error() + } + server.sendResponse(cc, req.h, req.replyv.Interface(), sending) +} + +// Accept accepts connections on the listener and serves requests +// for each incoming connection. +func (server *Server) Accept(lis net.Listener) { + for { + conn, err := lis.Accept() + if err != nil { + log.Println("rpc server: accept error:", err) + return + } + go server.ServeConn(conn) + } +} + +// Accept accepts connections on the listener and serves requests +// for each incoming connection. +func Accept(lis net.Listener) { DefaultServer.Accept(lis) } + +// Register publishes in the server the set of methods of the +// receiver value that satisfy the following conditions: +// - exported Method of exported type +// - two arguments, both of exported type +// - the second argument is a pointer +// - one return value, of type error +func (server *Server) Register(rcvr interface{}) error { + s := newService(rcvr) + if _, dup := server.serviceMap.LoadOrStore(s.name, s); dup { + return errors.New("rpc: service already defined: " + s.name) + } + return nil +} + +// Register publishes the receiver's methods in the DefaultServer. +func Register(rcvr interface{}) error { return DefaultServer.Register(rcvr) } + +const ( + connected = "200 Connected to Gee RPC" + defaultRPCPath = "/_geeprc_" + defaultDebugPath = "/debug/geerpc" +) + +// ServeHTTP implements an http.Handler that answers RPC requests. +func (server *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) { + if req.Method != "CONNECT" { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.WriteHeader(http.StatusMethodNotAllowed) + _, _ = io.WriteString(w, "405 must CONNECT\n") + return + } + conn, _, err := w.(http.Hijacker).Hijack() + if err != nil { + log.Print("rpc hijacking ", req.RemoteAddr, ": ", err.Error()) + return + } + _, _ = io.WriteString(conn, "HTTP/1.0 "+connected+"\n\n") + server.ServeConn(conn) +} + +// HandleHTTP registers an HTTP handler for RPC messages on rpcPath, +// and a debugging handler on debugPath. +// It is still necessary to invoke http.Serve(), typically in a go statement. +func (server *Server) HandleHTTP(rpcPath, debugPath string) { + http.Handle(rpcPath, server) + http.Handle(debugPath, debugHTTP{server}) + log.Println("rpc server debug path:", debugPath) +} + +func HandleHTTP() { + DefaultServer.HandleHTTP(defaultRPCPath, defaultDebugPath) +} diff --git a/gee-rpc/day4-http-debug/service.go b/gee-rpc/day4-http-debug/service.go new file mode 100644 index 0000000..e0711cd --- /dev/null +++ b/gee-rpc/day4-http-debug/service.go @@ -0,0 +1,95 @@ +package geerpc + +import ( + "go/ast" + "log" + "reflect" + "sync/atomic" +) + +type methodType struct { + Method reflect.Method + ArgType reflect.Type + ReplyType reflect.Type + NumCalls uint64 +} + +func (m *methodType) newArgv() reflect.Value { + var argv reflect.Value + // arg may be a pointer type, or a value type + if m.ArgType.Kind() == reflect.Ptr { + argv = reflect.New(m.ArgType.Elem()) + } else { + argv = reflect.New(m.ArgType).Elem() + } + return argv +} + +func (m *methodType) newReplyv() reflect.Value { + // reply must be a pointer type + replyv := reflect.New(m.ReplyType.Elem()) + switch m.ReplyType.Elem().Kind() { + case reflect.Map: + replyv.Elem().Set(reflect.MakeMap(m.ReplyType.Elem())) + case reflect.Slice: + replyv.Elem().Set(reflect.MakeSlice(m.ReplyType.Elem(), 0, 0)) + } + return replyv +} + +type service struct { + name string + typ reflect.Type + rcvr reflect.Value + method map[string]*methodType +} + +func newService(rcvr interface{}) *service { + s := new(service) + s.rcvr = reflect.ValueOf(rcvr) + s.name = reflect.Indirect(s.rcvr).Type().Name() + s.typ = reflect.TypeOf(rcvr) + if !ast.IsExported(s.name) { + log.Fatalf("rpc server: %s is not a valid service name", s.name) + } + s.registerMethods() + return s +} + +func (s *service) registerMethods() { + s.method = make(map[string]*methodType) + for i := 0; i < s.typ.NumMethod(); i++ { + method := s.typ.Method(i) + mType := method.Type + if mType.NumIn() != 3 || mType.NumOut() != 1 { + continue + } + if mType.Out(0) != reflect.TypeOf((*error)(nil)).Elem() { + continue + } + argType, replyType := mType.In(1), mType.In(2) + if !isExportedOrBuiltinType(argType) || !isExportedOrBuiltinType(replyType) { + continue + } + s.method[method.Name] = &methodType{ + Method: method, + ArgType: argType, + ReplyType: replyType, + } + log.Printf("rpc server: register %s.%s\n", s.name, method.Name) + } +} + +func (s *service) call(m *methodType, argv, replyv reflect.Value) error { + atomic.AddUint64(&m.NumCalls, 1) + f := m.Method.Func + returnValues := f.Call([]reflect.Value{s.rcvr, argv, replyv}) + if errInter := returnValues[0].Interface(); errInter != nil { + return errInter.(error) + } + return nil +} + +func isExportedOrBuiltinType(t reflect.Type) bool { + return ast.IsExported(t.Name()) || t.PkgPath() == "" +} diff --git a/gee-rpc/day4-http-debug/service_test.go b/gee-rpc/day4-http-debug/service_test.go new file mode 100644 index 0000000..7ba2786 --- /dev/null +++ b/gee-rpc/day4-http-debug/service_test.go @@ -0,0 +1,48 @@ +package geerpc + +import ( + "fmt" + "reflect" + "testing" +) + +type Foo int + +type Args struct{ Num1, Num2 int } + +func (f Foo) Sum(args Args, reply *int) error { + *reply = args.Num1 + args.Num2 + return nil +} + +// it's not a exported Method +func (f Foo) sum(args Args, reply *int) error { + *reply = args.Num1 + args.Num2 + return nil +} + +func _assert(condition bool, msg string, v ...interface{}) { + if !condition { + panic(fmt.Sprintf("assertion failed: "+msg, v...)) + } +} + +func TestNewService(t *testing.T) { + var foo Foo + s := newService(&foo) + _assert(len(s.method) == 1, "wrong service Method, expect 1, but got %d", len(s.method)) + mType := s.method["Sum"] + _assert(mType != nil, "wrong Method, Sum shouldn't nil") +} + +func TestMethodType_Call(t *testing.T) { + var foo Foo + s := newService(&foo) + mType := s.method["Sum"] + + argv := mType.newArgv() + replyv := mType.newReplyv() + argv.Set(reflect.ValueOf(Args{Num1: 1, Num2: 3})) + err := s.call(mType, argv, replyv) + _assert(err == nil && *replyv.Interface().(*int) == 4 && mType.NumCalls == 1, "failed to call Foo.Sum") +} From 714311df7dca8bb6412872779abb4dc8aa277d8c Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Fri, 2 Oct 2020 16:56:01 +0800 Subject: [PATCH 077/122] remove duplicate error definition --- gee-rpc/day4-http-debug/client.go | 1 - 1 file changed, 1 deletion(-) diff --git a/gee-rpc/day4-http-debug/client.go b/gee-rpc/day4-http-debug/client.go index 3b53c16..8ab5ef0 100644 --- a/gee-rpc/day4-http-debug/client.go +++ b/gee-rpc/day4-http-debug/client.go @@ -233,7 +233,6 @@ func DialHTTPPath(network, address, path string, opts ...*Options) (*Client, err if len(opts) > 0 && opts[0] != nil { opt = opts[0] } - var err error conn, err := net.Dial(network, address) if err != nil { return nil, err From a411eba0b231930f93333fffa4259652e8917ef6 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Fri, 2 Oct 2020 17:25:53 +0800 Subject: [PATCH 078/122] fix service numCalls read issue --- gee-rpc/day3-service/service.go | 14 +++++++++----- gee-rpc/day3-service/service_test.go | 8 ++++---- gee-rpc/day4-http-debug/service.go | 14 +++++++++----- gee-rpc/day4-http-debug/service_test.go | 2 +- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/gee-rpc/day3-service/service.go b/gee-rpc/day3-service/service.go index e0711cd..306683c 100644 --- a/gee-rpc/day3-service/service.go +++ b/gee-rpc/day3-service/service.go @@ -8,10 +8,14 @@ import ( ) type methodType struct { - Method reflect.Method + method reflect.Method ArgType reflect.Type ReplyType reflect.Type - NumCalls uint64 + numCalls uint64 +} + +func (m *methodType) NumCalls() uint64 { + return atomic.LoadUint64(&m.numCalls) } func (m *methodType) newArgv() reflect.Value { @@ -72,7 +76,7 @@ func (s *service) registerMethods() { continue } s.method[method.Name] = &methodType{ - Method: method, + method: method, ArgType: argType, ReplyType: replyType, } @@ -81,8 +85,8 @@ func (s *service) registerMethods() { } func (s *service) call(m *methodType, argv, replyv reflect.Value) error { - atomic.AddUint64(&m.NumCalls, 1) - f := m.Method.Func + atomic.AddUint64(&m.numCalls, 1) + f := m.method.Func returnValues := f.Call([]reflect.Value{s.rcvr, argv, replyv}) if errInter := returnValues[0].Interface(); errInter != nil { return errInter.(error) diff --git a/gee-rpc/day3-service/service_test.go b/gee-rpc/day3-service/service_test.go index 4b35124..c8266df 100644 --- a/gee-rpc/day3-service/service_test.go +++ b/gee-rpc/day3-service/service_test.go @@ -15,7 +15,7 @@ func (f Foo) Sum(args Args, reply *int) error { return nil } -// it's not a exported method +// it's not a exported Method func (f Foo) sum(args Args, reply *int) error { *reply = args.Num1 + args.Num2 return nil @@ -30,9 +30,9 @@ func _assert(condition bool, msg string, v ...interface{}) { func TestNewService(t *testing.T) { var foo Foo s := newService(&foo) - _assert(len(s.method) == 1, "wrong service method, expect 1, but got %d", len(s.method)) + _assert(len(s.method) == 1, "wrong service Method, expect 1, but got %d", len(s.method)) mType := s.method["Sum"] - _assert(mType != nil, "wrong method, Sum shouldn't nil") + _assert(mType != nil, "wrong Method, Sum shouldn't nil") } func TestMethodType_Call(t *testing.T) { @@ -44,5 +44,5 @@ func TestMethodType_Call(t *testing.T) { replyv := mType.newReplyv() argv.Set(reflect.ValueOf(Args{Num1: 1, Num2: 3})) err := s.call(mType, argv, replyv) - _assert(err == nil && *replyv.Interface().(*int) == 4 && mType.numCalls == 1, "failed to call Foo.Sum") + _assert(err == nil && *replyv.Interface().(*int) == 4 && mType.NumCalls() == 1, "failed to call Foo.Sum") } diff --git a/gee-rpc/day4-http-debug/service.go b/gee-rpc/day4-http-debug/service.go index e0711cd..306683c 100644 --- a/gee-rpc/day4-http-debug/service.go +++ b/gee-rpc/day4-http-debug/service.go @@ -8,10 +8,14 @@ import ( ) type methodType struct { - Method reflect.Method + method reflect.Method ArgType reflect.Type ReplyType reflect.Type - NumCalls uint64 + numCalls uint64 +} + +func (m *methodType) NumCalls() uint64 { + return atomic.LoadUint64(&m.numCalls) } func (m *methodType) newArgv() reflect.Value { @@ -72,7 +76,7 @@ func (s *service) registerMethods() { continue } s.method[method.Name] = &methodType{ - Method: method, + method: method, ArgType: argType, ReplyType: replyType, } @@ -81,8 +85,8 @@ func (s *service) registerMethods() { } func (s *service) call(m *methodType, argv, replyv reflect.Value) error { - atomic.AddUint64(&m.NumCalls, 1) - f := m.Method.Func + atomic.AddUint64(&m.numCalls, 1) + f := m.method.Func returnValues := f.Call([]reflect.Value{s.rcvr, argv, replyv}) if errInter := returnValues[0].Interface(); errInter != nil { return errInter.(error) diff --git a/gee-rpc/day4-http-debug/service_test.go b/gee-rpc/day4-http-debug/service_test.go index 7ba2786..c8266df 100644 --- a/gee-rpc/day4-http-debug/service_test.go +++ b/gee-rpc/day4-http-debug/service_test.go @@ -44,5 +44,5 @@ func TestMethodType_Call(t *testing.T) { replyv := mType.newReplyv() argv.Set(reflect.ValueOf(Args{Num1: 1, Num2: 3})) err := s.call(mType, argv, replyv) - _assert(err == nil && *replyv.Interface().(*int) == 4 && mType.NumCalls == 1, "failed to call Foo.Sum") + _assert(err == nil && *replyv.Interface().(*int) == 4 && mType.NumCalls() == 1, "failed to call Foo.Sum") } From d9162cc910a51159d7a23a64628834d6cd13c35c Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Fri, 2 Oct 2020 23:44:13 +0800 Subject: [PATCH 079/122] gee-rpc: add Client struct filed opt to record options user will pass --- gee-rpc/day2-client/client.go | 42 ++++++++++++++++---------- gee-rpc/day3-service/client.go | 42 ++++++++++++++++---------- gee-rpc/day4-http-debug/client.go | 50 +++++++++++++++++-------------- 3 files changed, 80 insertions(+), 54 deletions(-) diff --git a/gee-rpc/day2-client/client.go b/gee-rpc/day2-client/client.go index 83119f0..b73c461 100644 --- a/gee-rpc/day2-client/client.go +++ b/gee-rpc/day2-client/client.go @@ -34,6 +34,7 @@ func (call *Call) done() { // multiple goroutines simultaneously. type Client struct { cc codec.Codec + opt *Options sending sync.Mutex // protect following header codec.Header mu sync.Mutex // protect following @@ -172,15 +173,26 @@ func (client *Client) Call(serviceMethod string, args, reply interface{}) error return call.Error } -func NewClient(conn io.ReadWriteCloser, opt *Options) (*Client, error) { - var err error - defer func() { - if err != nil { - _ = conn.Close() - } - }() - if opt.MagicNumber == 0 { - opt.MagicNumber = MagicNumber +func parseOptions(opts ...*Options) (*Options, error) { + // if opts is nil or pass nil as parameter + if len(opts) == 0 || opts[0] == nil { + return defaultOptions, nil + } + if len(opts) != 1 { + return nil, errors.New("number of options is more than 1") + } + opt := opts[0] + opt.MagicNumber = defaultOptions.MagicNumber + if opt.CodecType == "" { + opt.CodecType = defaultOptions.CodecType + } + return opt, nil +} + +func NewClient(conn io.ReadWriteCloser, opts ...*Options) (*Client, error) { + opt, err := parseOptions(opts...) + if err != nil { + return nil, err } f := codec.NewCodecFuncMap[opt.CodecType] if f == nil { @@ -191,14 +203,16 @@ func NewClient(conn io.ReadWriteCloser, opt *Options) (*Client, error) { // send options with server if err = json.NewEncoder(conn).Encode(opt); err != nil { log.Println("rpc client: options error: ", err) + _ = conn.Close() return nil, err } - return newClientCodec(f(conn)), nil + return newClientCodec(f(conn), opt), nil } -func newClientCodec(cc codec.Codec) *Client { +func newClientCodec(cc codec.Codec, opt *Options) *Client { client := &Client{ cc: cc, + opt: opt, pending: make(map[uint64]*Call), } go client.receive() @@ -207,13 +221,9 @@ func newClientCodec(cc codec.Codec) *Client { // Dial connects to an RPC server at the specified network address func Dial(network, address string, opts ...*Options) (*Client, error) { - opt := defaultOptions - if len(opts) > 0 && opts[0] != nil { - opt = opts[0] - } conn, err := net.Dial(network, address) if err != nil { return nil, err } - return NewClient(conn, opt) + return NewClient(conn, opts...) } diff --git a/gee-rpc/day3-service/client.go b/gee-rpc/day3-service/client.go index 83119f0..b73c461 100644 --- a/gee-rpc/day3-service/client.go +++ b/gee-rpc/day3-service/client.go @@ -34,6 +34,7 @@ func (call *Call) done() { // multiple goroutines simultaneously. type Client struct { cc codec.Codec + opt *Options sending sync.Mutex // protect following header codec.Header mu sync.Mutex // protect following @@ -172,15 +173,26 @@ func (client *Client) Call(serviceMethod string, args, reply interface{}) error return call.Error } -func NewClient(conn io.ReadWriteCloser, opt *Options) (*Client, error) { - var err error - defer func() { - if err != nil { - _ = conn.Close() - } - }() - if opt.MagicNumber == 0 { - opt.MagicNumber = MagicNumber +func parseOptions(opts ...*Options) (*Options, error) { + // if opts is nil or pass nil as parameter + if len(opts) == 0 || opts[0] == nil { + return defaultOptions, nil + } + if len(opts) != 1 { + return nil, errors.New("number of options is more than 1") + } + opt := opts[0] + opt.MagicNumber = defaultOptions.MagicNumber + if opt.CodecType == "" { + opt.CodecType = defaultOptions.CodecType + } + return opt, nil +} + +func NewClient(conn io.ReadWriteCloser, opts ...*Options) (*Client, error) { + opt, err := parseOptions(opts...) + if err != nil { + return nil, err } f := codec.NewCodecFuncMap[opt.CodecType] if f == nil { @@ -191,14 +203,16 @@ func NewClient(conn io.ReadWriteCloser, opt *Options) (*Client, error) { // send options with server if err = json.NewEncoder(conn).Encode(opt); err != nil { log.Println("rpc client: options error: ", err) + _ = conn.Close() return nil, err } - return newClientCodec(f(conn)), nil + return newClientCodec(f(conn), opt), nil } -func newClientCodec(cc codec.Codec) *Client { +func newClientCodec(cc codec.Codec, opt *Options) *Client { client := &Client{ cc: cc, + opt: opt, pending: make(map[uint64]*Call), } go client.receive() @@ -207,13 +221,9 @@ func newClientCodec(cc codec.Codec) *Client { // Dial connects to an RPC server at the specified network address func Dial(network, address string, opts ...*Options) (*Client, error) { - opt := defaultOptions - if len(opts) > 0 && opts[0] != nil { - opt = opts[0] - } conn, err := net.Dial(network, address) if err != nil { return nil, err } - return NewClient(conn, opt) + return NewClient(conn, opts...) } diff --git a/gee-rpc/day4-http-debug/client.go b/gee-rpc/day4-http-debug/client.go index 8ab5ef0..7ae8938 100644 --- a/gee-rpc/day4-http-debug/client.go +++ b/gee-rpc/day4-http-debug/client.go @@ -19,7 +19,7 @@ import ( // Call represents an active RPC. type Call struct { - ServiceMethod string // format "." + ServiceMethod string // format "." Args interface{} // arguments to the function Reply interface{} // reply from the function Error error // if error occurs, it will be set @@ -36,6 +36,7 @@ func (call *Call) done() { // multiple goroutines simultaneously. type Client struct { cc codec.Codec + opt *Options sending sync.Mutex // protect following header codec.Header mu sync.Mutex // protect following @@ -174,15 +175,26 @@ func (client *Client) Call(serviceMethod string, args, reply interface{}) error return call.Error } -func NewClient(conn io.ReadWriteCloser, opt *Options) (*Client, error) { - var err error - defer func() { - if err != nil { - _ = conn.Close() - } - }() - if opt.MagicNumber == 0 { - opt.MagicNumber = MagicNumber +func parseOptions(opts ...*Options) (*Options, error) { + // if opts is nil or pass nil as parameter + if len(opts) == 0 || opts[0] == nil { + return defaultOptions, nil + } + if len(opts) != 1 { + return nil, errors.New("number of options is more than 1") + } + opt := opts[0] + opt.MagicNumber = defaultOptions.MagicNumber + if opt.CodecType == "" { + opt.CodecType = defaultOptions.CodecType + } + return opt, nil +} + +func NewClient(conn io.ReadWriteCloser, opts ...*Options) (*Client, error) { + opt, err := parseOptions(opts...) + if err != nil { + return nil, err } f := codec.NewCodecFuncMap[opt.CodecType] if f == nil { @@ -193,14 +205,16 @@ func NewClient(conn io.ReadWriteCloser, opt *Options) (*Client, error) { // send options with server if err = json.NewEncoder(conn).Encode(opt); err != nil { log.Println("rpc client: options error: ", err) + _ = conn.Close() return nil, err } - return newClientCodec(f(conn)), nil + return newClientCodec(f(conn), opt), nil } -func newClientCodec(cc codec.Codec) *Client { +func newClientCodec(cc codec.Codec, opt *Options) *Client { client := &Client{ cc: cc, + opt: opt, pending: make(map[uint64]*Call), } go client.receive() @@ -209,15 +223,11 @@ func newClientCodec(cc codec.Codec) *Client { // Dial connects to an RPC server at the specified network address func Dial(network, address string, opts ...*Options) (*Client, error) { - opt := defaultOptions - if len(opts) > 0 && opts[0] != nil { - opt = opts[0] - } conn, err := net.Dial(network, address) if err != nil { return nil, err } - return NewClient(conn, opt) + return NewClient(conn, opts...) } // DialHTTP connects to an HTTP RPC server at the specified network address @@ -229,10 +239,6 @@ func DialHTTP(network, address string, opts ...*Options) (*Client, error) { // DialHTTPPath connects to an HTTP RPC server // at the specified network address and path. func DialHTTPPath(network, address, path string, opts ...*Options) (*Client, error) { - opt := defaultOptions - if len(opts) > 0 && opts[0] != nil { - opt = opts[0] - } conn, err := net.Dial(network, address) if err != nil { return nil, err @@ -243,7 +249,7 @@ func DialHTTPPath(network, address, path string, opts ...*Options) (*Client, err // before switching to RPC protocol. resp, err := http.ReadResponse(bufio.NewReader(conn), &http.Request{Method: "CONNECT"}) if err == nil && resp.Status == connected { - return NewClient(conn, opt) + return NewClient(conn, opts...) } if err == nil { err = errors.New("unexpected HTTP response: " + resp.Status) From 7e7ee27beae493108f0eeabfb29479f96cd56dec Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Sat, 3 Oct 2020 10:54:28 +0800 Subject: [PATCH 080/122] gee-rpc: implement day5-timeout --- gee-rpc/day1-codec/main/main.go | 6 +- gee-rpc/day1-codec/server.go | 6 +- gee-rpc/day2-client/client.go | 39 ++- gee-rpc/day2-client/server.go | 6 +- gee-rpc/day3-service/client.go | 39 ++- gee-rpc/day3-service/server.go | 8 +- .../client.go | 92 +++--- gee-rpc/day4-timeout/client_test.go | 63 ++++ .../codec/codec.go | 0 .../codec/gob.go | 0 .../{day4-http-debug => day4-timeout}/go.mod | 0 gee-rpc/day4-timeout/main/main.go | 56 ++++ gee-rpc/day4-timeout/server.go | 228 +++++++++++++ .../service.go | 0 .../service_test.go | 0 gee-rpc/day5-http-debug/client.go | 310 ++++++++++++++++++ gee-rpc/day5-http-debug/client_test.go | 63 ++++ gee-rpc/day5-http-debug/codec/codec.go | 34 ++ gee-rpc/day5-http-debug/codec/gob.go | 57 ++++ .../debug.go | 0 gee-rpc/day5-http-debug/go.mod | 3 + .../main/main.go | 3 +- .../server.go | 63 +++- gee-rpc/day5-http-debug/service.go | 99 ++++++ gee-rpc/day5-http-debug/service_test.go | 48 +++ 25 files changed, 1117 insertions(+), 106 deletions(-) rename gee-rpc/{day4-http-debug => day4-timeout}/client.go (74%) create mode 100644 gee-rpc/day4-timeout/client_test.go rename gee-rpc/{day4-http-debug => day4-timeout}/codec/codec.go (100%) rename gee-rpc/{day4-http-debug => day4-timeout}/codec/gob.go (100%) rename gee-rpc/{day4-http-debug => day4-timeout}/go.mod (100%) create mode 100644 gee-rpc/day4-timeout/main/main.go create mode 100644 gee-rpc/day4-timeout/server.go rename gee-rpc/{day4-http-debug => day4-timeout}/service.go (100%) rename gee-rpc/{day4-http-debug => day4-timeout}/service_test.go (100%) create mode 100644 gee-rpc/day5-http-debug/client.go create mode 100644 gee-rpc/day5-http-debug/client_test.go create mode 100644 gee-rpc/day5-http-debug/codec/codec.go create mode 100644 gee-rpc/day5-http-debug/codec/gob.go rename gee-rpc/{day4-http-debug => day5-http-debug}/debug.go (100%) create mode 100644 gee-rpc/day5-http-debug/go.mod rename gee-rpc/{day4-http-debug => day5-http-debug}/main/main.go (90%) rename gee-rpc/{day4-http-debug => day5-http-debug}/server.go (80%) create mode 100644 gee-rpc/day5-http-debug/service.go create mode 100644 gee-rpc/day5-http-debug/service_test.go diff --git a/gee-rpc/day1-codec/main/main.go b/gee-rpc/day1-codec/main/main.go index a715209..8f29531 100644 --- a/gee-rpc/day1-codec/main/main.go +++ b/gee-rpc/day1-codec/main/main.go @@ -29,11 +29,7 @@ func main() { defer func() { _ = conn.Close() }() // send options - _ = json.NewEncoder(conn).Encode(&geerpc.Options{ - MagicNumber: geerpc.MagicNumber, - CodecType: codec.GobType, - }) - + _ = json.NewEncoder(conn).Encode(geerpc.DefaultOption) cc := codec.NewGobCodec(conn) // send request & receive response for i := 0; i < 5; i++ { diff --git a/gee-rpc/day1-codec/server.go b/gee-rpc/day1-codec/server.go index 06ef763..fb93e4f 100644 --- a/gee-rpc/day1-codec/server.go +++ b/gee-rpc/day1-codec/server.go @@ -17,12 +17,12 @@ import ( const MagicNumber = 0x3bef5c -type Options struct { +type Option struct { MagicNumber int // MagicNumber marks this's a geerpc request CodecType codec.Type // client may choose different Codec to encode body } -var defaultOptions = &Options{ +var DefaultOption = &Option{ MagicNumber: MagicNumber, CodecType: codec.GobType, } @@ -42,7 +42,7 @@ var DefaultServer = NewServer() // ServeConn blocks, serving the connection until the client hangs up. func (server *Server) ServeConn(conn io.ReadWriteCloser) { defer func() { _ = conn.Close() }() - var opt Options + var opt Option if err := json.NewDecoder(conn).Decode(&opt); err != nil { log.Println("rpc server: options error: ", err) return diff --git a/gee-rpc/day2-client/client.go b/gee-rpc/day2-client/client.go index b73c461..f9b09ee 100644 --- a/gee-rpc/day2-client/client.go +++ b/gee-rpc/day2-client/client.go @@ -17,6 +17,7 @@ import ( // Call represents an active RPC. type Call struct { + Seq uint64 ServiceMethod string // format "." Args interface{} // arguments to the function Reply interface{} // reply from the function @@ -34,7 +35,7 @@ func (call *Call) done() { // multiple goroutines simultaneously. type Client struct { cc codec.Codec - opt *Options + opt *Option sending sync.Mutex // protect following header codec.Header mu sync.Mutex // protect following @@ -96,6 +97,7 @@ func (client *Client) send(call *Call) { // register this call. seq, err := client.registerCall(call) + call.Seq = seq if err != nil { call.Error = err call.done() @@ -173,35 +175,31 @@ func (client *Client) Call(serviceMethod string, args, reply interface{}) error return call.Error } -func parseOptions(opts ...*Options) (*Options, error) { +func parseOptions(opts ...*Option) (*Option, error) { // if opts is nil or pass nil as parameter if len(opts) == 0 || opts[0] == nil { - return defaultOptions, nil + return DefaultOption, nil } if len(opts) != 1 { return nil, errors.New("number of options is more than 1") } opt := opts[0] - opt.MagicNumber = defaultOptions.MagicNumber + opt.MagicNumber = DefaultOption.MagicNumber if opt.CodecType == "" { - opt.CodecType = defaultOptions.CodecType + opt.CodecType = DefaultOption.CodecType } return opt, nil } -func NewClient(conn io.ReadWriteCloser, opts ...*Options) (*Client, error) { - opt, err := parseOptions(opts...) - if err != nil { - return nil, err - } +func NewClient(conn net.Conn, opt *Option) (*Client, error) { f := codec.NewCodecFuncMap[opt.CodecType] if f == nil { - err = fmt.Errorf("invalid codec type %s", opt.CodecType) + err := fmt.Errorf("invalid codec type %s", opt.CodecType) log.Println("rpc client: codec error:", err) return nil, err } // send options with server - if err = json.NewEncoder(conn).Encode(opt); err != nil { + if err := json.NewEncoder(conn).Encode(opt); err != nil { log.Println("rpc client: options error: ", err) _ = conn.Close() return nil, err @@ -209,8 +207,9 @@ func NewClient(conn io.ReadWriteCloser, opts ...*Options) (*Client, error) { return newClientCodec(f(conn), opt), nil } -func newClientCodec(cc codec.Codec, opt *Options) *Client { +func newClientCodec(cc codec.Codec, opt *Option) *Client { client := &Client{ + seq: 1, // seq starts with 1, 0 means invalid call cc: cc, opt: opt, pending: make(map[uint64]*Call), @@ -219,11 +218,19 @@ func newClientCodec(cc codec.Codec, opt *Options) *Client { return client } -// Dial connects to an RPC server at the specified network address -func Dial(network, address string, opts ...*Options) (*Client, error) { +func dial(network, address string, opt *Option) (*Client, error) { conn, err := net.Dial(network, address) if err != nil { return nil, err } - return NewClient(conn, opts...) + return NewClient(conn, opt) +} + +// Dial connects to an RPC server at the specified network address +func Dial(network, address string, opts ...*Option) (*Client, error) { + opt, err := parseOptions(opts...) + if err != nil { + return nil, err + } + return dial(network, address, opt) } diff --git a/gee-rpc/day2-client/server.go b/gee-rpc/day2-client/server.go index 06ef763..fb93e4f 100644 --- a/gee-rpc/day2-client/server.go +++ b/gee-rpc/day2-client/server.go @@ -17,12 +17,12 @@ import ( const MagicNumber = 0x3bef5c -type Options struct { +type Option struct { MagicNumber int // MagicNumber marks this's a geerpc request CodecType codec.Type // client may choose different Codec to encode body } -var defaultOptions = &Options{ +var DefaultOption = &Option{ MagicNumber: MagicNumber, CodecType: codec.GobType, } @@ -42,7 +42,7 @@ var DefaultServer = NewServer() // ServeConn blocks, serving the connection until the client hangs up. func (server *Server) ServeConn(conn io.ReadWriteCloser) { defer func() { _ = conn.Close() }() - var opt Options + var opt Option if err := json.NewDecoder(conn).Decode(&opt); err != nil { log.Println("rpc server: options error: ", err) return diff --git a/gee-rpc/day3-service/client.go b/gee-rpc/day3-service/client.go index b73c461..f9b09ee 100644 --- a/gee-rpc/day3-service/client.go +++ b/gee-rpc/day3-service/client.go @@ -17,6 +17,7 @@ import ( // Call represents an active RPC. type Call struct { + Seq uint64 ServiceMethod string // format "." Args interface{} // arguments to the function Reply interface{} // reply from the function @@ -34,7 +35,7 @@ func (call *Call) done() { // multiple goroutines simultaneously. type Client struct { cc codec.Codec - opt *Options + opt *Option sending sync.Mutex // protect following header codec.Header mu sync.Mutex // protect following @@ -96,6 +97,7 @@ func (client *Client) send(call *Call) { // register this call. seq, err := client.registerCall(call) + call.Seq = seq if err != nil { call.Error = err call.done() @@ -173,35 +175,31 @@ func (client *Client) Call(serviceMethod string, args, reply interface{}) error return call.Error } -func parseOptions(opts ...*Options) (*Options, error) { +func parseOptions(opts ...*Option) (*Option, error) { // if opts is nil or pass nil as parameter if len(opts) == 0 || opts[0] == nil { - return defaultOptions, nil + return DefaultOption, nil } if len(opts) != 1 { return nil, errors.New("number of options is more than 1") } opt := opts[0] - opt.MagicNumber = defaultOptions.MagicNumber + opt.MagicNumber = DefaultOption.MagicNumber if opt.CodecType == "" { - opt.CodecType = defaultOptions.CodecType + opt.CodecType = DefaultOption.CodecType } return opt, nil } -func NewClient(conn io.ReadWriteCloser, opts ...*Options) (*Client, error) { - opt, err := parseOptions(opts...) - if err != nil { - return nil, err - } +func NewClient(conn net.Conn, opt *Option) (*Client, error) { f := codec.NewCodecFuncMap[opt.CodecType] if f == nil { - err = fmt.Errorf("invalid codec type %s", opt.CodecType) + err := fmt.Errorf("invalid codec type %s", opt.CodecType) log.Println("rpc client: codec error:", err) return nil, err } // send options with server - if err = json.NewEncoder(conn).Encode(opt); err != nil { + if err := json.NewEncoder(conn).Encode(opt); err != nil { log.Println("rpc client: options error: ", err) _ = conn.Close() return nil, err @@ -209,8 +207,9 @@ func NewClient(conn io.ReadWriteCloser, opts ...*Options) (*Client, error) { return newClientCodec(f(conn), opt), nil } -func newClientCodec(cc codec.Codec, opt *Options) *Client { +func newClientCodec(cc codec.Codec, opt *Option) *Client { client := &Client{ + seq: 1, // seq starts with 1, 0 means invalid call cc: cc, opt: opt, pending: make(map[uint64]*Call), @@ -219,11 +218,19 @@ func newClientCodec(cc codec.Codec, opt *Options) *Client { return client } -// Dial connects to an RPC server at the specified network address -func Dial(network, address string, opts ...*Options) (*Client, error) { +func dial(network, address string, opt *Option) (*Client, error) { conn, err := net.Dial(network, address) if err != nil { return nil, err } - return NewClient(conn, opts...) + return NewClient(conn, opt) +} + +// Dial connects to an RPC server at the specified network address +func Dial(network, address string, opts ...*Option) (*Client, error) { + opt, err := parseOptions(opts...) + if err != nil { + return nil, err + } + return dial(network, address, opt) } diff --git a/gee-rpc/day3-service/server.go b/gee-rpc/day3-service/server.go index 6558dad..4634394 100644 --- a/gee-rpc/day3-service/server.go +++ b/gee-rpc/day3-service/server.go @@ -18,12 +18,12 @@ import ( const MagicNumber = 0x3bef5c -type Options struct { +type Option struct { MagicNumber int // MagicNumber marks this's a geerpc request CodecType codec.Type // client may choose different Codec to encode body } -var defaultOptions = &Options{ +var DefaultOption = &Option{ MagicNumber: MagicNumber, CodecType: codec.GobType, } @@ -45,7 +45,7 @@ var DefaultServer = NewServer() // ServeConn blocks, serving the connection until the client hangs up. func (server *Server) ServeConn(conn io.ReadWriteCloser) { defer func() { _ = conn.Close() }() - var opt Options + var opt Option if err := json.NewDecoder(conn).Decode(&opt); err != nil { log.Println("rpc server: options error: ", err) return @@ -162,6 +162,8 @@ func (server *Server) handleRequest(cc codec.Codec, req *request, sending *sync. err := req.svc.call(req.mtype, req.argv, req.replyv) if err != nil { req.h.Error = err.Error() + server.sendResponse(cc, req.h, invalidRequest, sending) + return } server.sendResponse(cc, req.h, req.replyv.Interface(), sending) } diff --git a/gee-rpc/day4-http-debug/client.go b/gee-rpc/day4-timeout/client.go similarity index 74% rename from gee-rpc/day4-http-debug/client.go rename to gee-rpc/day4-timeout/client.go index 7ae8938..c9900fa 100644 --- a/gee-rpc/day4-http-debug/client.go +++ b/gee-rpc/day4-timeout/client.go @@ -5,7 +5,7 @@ package geerpc import ( - "bufio" + "context" "encoding/json" "errors" "fmt" @@ -13,12 +13,13 @@ import ( "io" "log" "net" - "net/http" "sync" + "time" ) // Call represents an active RPC. type Call struct { + Seq uint64 ServiceMethod string // format "." Args interface{} // arguments to the function Reply interface{} // reply from the function @@ -36,7 +37,7 @@ func (call *Call) done() { // multiple goroutines simultaneously. type Client struct { cc codec.Codec - opt *Options + opt *Option sending sync.Mutex // protect following header codec.Header mu sync.Mutex // protect following @@ -98,6 +99,7 @@ func (client *Client) send(call *Call) { // register this call. seq, err := client.registerCall(call) + call.Seq = seq if err != nil { call.Error = err call.done() @@ -170,40 +172,42 @@ func (client *Client) Go(serviceMethod string, args, reply interface{}, done cha // Call invokes the named function, waits for it to complete, // and returns its error status. -func (client *Client) Call(serviceMethod string, args, reply interface{}) error { - call := <-client.Go(serviceMethod, args, reply, make(chan *Call, 1)).Done - return call.Error +func (client *Client) Call(ctx context.Context, serviceMethod string, args, reply interface{}) error { + call := client.Go(serviceMethod, args, reply, make(chan *Call, 1)) + select { + case <-ctx.Done(): + client.removeCall(call.Seq) + return errors.New("rpc client: call failed: " + ctx.Err().Error()) + case call := <-call.Done: + return call.Error + } } -func parseOptions(opts ...*Options) (*Options, error) { +func parseOptions(opts ...*Option) (*Option, error) { // if opts is nil or pass nil as parameter if len(opts) == 0 || opts[0] == nil { - return defaultOptions, nil + return DefaultOption, nil } if len(opts) != 1 { return nil, errors.New("number of options is more than 1") } opt := opts[0] - opt.MagicNumber = defaultOptions.MagicNumber + opt.MagicNumber = DefaultOption.MagicNumber if opt.CodecType == "" { - opt.CodecType = defaultOptions.CodecType + opt.CodecType = DefaultOption.CodecType } return opt, nil } -func NewClient(conn io.ReadWriteCloser, opts ...*Options) (*Client, error) { - opt, err := parseOptions(opts...) - if err != nil { - return nil, err - } +func NewClient(conn net.Conn, opt *Option) (*Client, error) { f := codec.NewCodecFuncMap[opt.CodecType] if f == nil { - err = fmt.Errorf("invalid codec type %s", opt.CodecType) + err := fmt.Errorf("invalid codec type %s", opt.CodecType) log.Println("rpc client: codec error:", err) return nil, err } // send options with server - if err = json.NewEncoder(conn).Encode(opt); err != nil { + if err := json.NewEncoder(conn).Encode(opt); err != nil { log.Println("rpc client: options error: ", err) _ = conn.Close() return nil, err @@ -211,8 +215,9 @@ func NewClient(conn io.ReadWriteCloser, opts ...*Options) (*Client, error) { return newClientCodec(f(conn), opt), nil } -func newClientCodec(cc codec.Codec, opt *Options) *Client { +func newClientCodec(cc codec.Codec, opt *Option) *Client { client := &Client{ + seq: 1, // seq starts with 1, 0 means invalid call cc: cc, opt: opt, pending: make(map[uint64]*Call), @@ -221,39 +226,44 @@ func newClientCodec(cc codec.Codec, opt *Options) *Client { return client } -// Dial connects to an RPC server at the specified network address -func Dial(network, address string, opts ...*Options) (*Client, error) { +func dial(network, address string, opt *Option) (*Client, error) { conn, err := net.Dial(network, address) if err != nil { return nil, err } - return NewClient(conn, opts...) + return NewClient(conn, opt) } -// DialHTTP connects to an HTTP RPC server at the specified network address -// listening on the default HTTP RPC path. -func DialHTTP(network, address string, opts ...*Options) (*Client, error) { - return DialHTTPPath(network, address, defaultRPCPath, opts...) +type clientResult struct { + client *Client + err error } -// DialHTTPPath connects to an HTTP RPC server -// at the specified network address and path. -func DialHTTPPath(network, address, path string, opts ...*Options) (*Client, error) { - conn, err := net.Dial(network, address) - if err != nil { - return nil, err +func dialTimeout(f func() (client *Client, err error), timeout time.Duration) (*Client, error) { + if timeout == 0 { + return f() + } + ch := make(chan clientResult) + go func() { + client, err := f() + ch <- clientResult{client: client, err: err} + }() + select { + case <-time.After(timeout): + return nil, fmt.Errorf("rpc client: dial timeout: expect within %s", timeout) + case result := <-ch: + return result.client, result.err } - _, _ = io.WriteString(conn, fmt.Sprintf("CONNECT %s HTTP/1.0\n\n", path)) +} - // Require successful HTTP response - // before switching to RPC protocol. - resp, err := http.ReadResponse(bufio.NewReader(conn), &http.Request{Method: "CONNECT"}) - if err == nil && resp.Status == connected { - return NewClient(conn, opts...) +// Dial connects to an RPC server at the specified network address +func Dial(network, address string, opts ...*Option) (*Client, error) { + opt, err := parseOptions(opts...) + if err != nil { + return nil, err } - if err == nil { - err = errors.New("unexpected HTTP response: " + resp.Status) + f := func() (client *Client, err error) { + return dial(network, address, opt) } - _ = conn.Close() - return nil, err + return dialTimeout(f, opt.ConnectTimeout) } diff --git a/gee-rpc/day4-timeout/client_test.go b/gee-rpc/day4-timeout/client_test.go new file mode 100644 index 0000000..5669f9c --- /dev/null +++ b/gee-rpc/day4-timeout/client_test.go @@ -0,0 +1,63 @@ +package geerpc + +import ( + "context" + "net" + "strings" + "testing" + "time" +) + +type Bar int + +func (b Bar) Timeout(argv int, reply *int) error { + time.Sleep(time.Second * 2) + return nil +} + +func startServer(addr chan string) { + var b Bar + _ = Register(&b) + // pick a free port + l, _ := net.Listen("tcp", ":0") + addr <- l.Addr().String() + Accept(l) +} + +func TestClient_dialTimeout(t *testing.T) { + t.Parallel() + f := func() (client *Client, err error) { + time.Sleep(time.Second * 2) + return nil, nil + } + t.Run("timeout", func(t *testing.T) { + _, err := dialTimeout(f, time.Second) + _assert(err != nil && strings.Contains(err.Error(), "dial timeout"), "expect a timeout error") + }) + t.Run("0", func(t *testing.T) { + _, err := dialTimeout(f, 0) + _assert(err == nil, "0 means no limit") + }) +} + +func TestClient_Call(t *testing.T) { + t.Parallel() + addrCh := make(chan string) + go startServer(addrCh) + addr := <-addrCh + t.Run("client timeout", func(t *testing.T) { + client, _ := Dial("tcp", addr) + ctx, _ := context.WithTimeout(context.Background(), time.Second) + var reply int + err := client.Call(ctx, "Bar.Timeout", 1, &reply) + _assert(err != nil && strings.Contains(err.Error(), ctx.Err().Error()), "expect a timeout error") + }) + t.Run("server handle timeout", func(t *testing.T) { + client, _ := Dial("tcp", addr, &Option{ + HandleTimeout: time.Second, + }) + var reply int + err := client.Call(context.Background(), "Bar.Timeout", 1, &reply) + _assert(err != nil && strings.Contains(err.Error(), "handle timeout"), "expect a timeout error") + }) +} diff --git a/gee-rpc/day4-http-debug/codec/codec.go b/gee-rpc/day4-timeout/codec/codec.go similarity index 100% rename from gee-rpc/day4-http-debug/codec/codec.go rename to gee-rpc/day4-timeout/codec/codec.go diff --git a/gee-rpc/day4-http-debug/codec/gob.go b/gee-rpc/day4-timeout/codec/gob.go similarity index 100% rename from gee-rpc/day4-http-debug/codec/gob.go rename to gee-rpc/day4-timeout/codec/gob.go diff --git a/gee-rpc/day4-http-debug/go.mod b/gee-rpc/day4-timeout/go.mod similarity index 100% rename from gee-rpc/day4-http-debug/go.mod rename to gee-rpc/day4-timeout/go.mod diff --git a/gee-rpc/day4-timeout/main/main.go b/gee-rpc/day4-timeout/main/main.go new file mode 100644 index 0000000..e5e6050 --- /dev/null +++ b/gee-rpc/day4-timeout/main/main.go @@ -0,0 +1,56 @@ +package main + +import ( + "context" + "geerpc" + "log" + "net" + "sync" +) + +type Foo int + +type Args struct{ Num1, Num2 int } + +func (f Foo) Sum(args Args, reply *int) error { + *reply = args.Num1 + args.Num2 + return nil +} + +func startServer(addr chan string) { + var foo Foo + if err := geerpc.Register(&foo); err != nil { + log.Fatal("register error:", err) + } + // pick a free port + l, err := net.Listen("tcp", ":0") + if err != nil { + log.Fatal("network error:", err) + } + log.Println("start rpc server on", l.Addr()) + addr <- l.Addr().String() + geerpc.Accept(l) +} + +func main() { + addr := make(chan string) + go startServer(addr) + client, _ := geerpc.Dial("tcp", <-addr) + defer func() { _ = client.Close() }() + + // send request & receive response + var wg sync.WaitGroup + for i := 0; i < 5; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + args := &Args{Num1: i, Num2: i * i} + var reply int + if err := client.Call(context.Background(), "Foo.Sum", args, &reply); err != nil { + log.Fatal("call Foo.Sum error:", err) + } + log.Printf("%d + %d = %d", args.Num1, args.Num2, reply) + }(i) + } + wg.Wait() +} diff --git a/gee-rpc/day4-timeout/server.go b/gee-rpc/day4-timeout/server.go new file mode 100644 index 0000000..a049914 --- /dev/null +++ b/gee-rpc/day4-timeout/server.go @@ -0,0 +1,228 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package geerpc + +import ( + "encoding/json" + "errors" + "fmt" + "geerpc/codec" + "io" + "log" + "net" + "reflect" + "strings" + "sync" + "time" +) + +const MagicNumber = 0x3bef5c + +type Option struct { + MagicNumber int // MagicNumber marks this's a geerpc request + CodecType codec.Type // client may choose different Codec to encode body + ConnectTimeout time.Duration // 0 means no limit + HandleTimeout time.Duration +} + +var DefaultOption = &Option{ + MagicNumber: MagicNumber, + CodecType: codec.GobType, + ConnectTimeout: time.Second * 10, +} + +// Server represents an RPC Server. +type Server struct { + serviceMap sync.Map +} + +// NewServer returns a new Server. +func NewServer() *Server { + return &Server{} +} + +// DefaultServer is the default instance of *Server. +var DefaultServer = NewServer() + +// ServeConn runs the server on a single connection. +// ServeConn blocks, serving the connection until the client hangs up. +func (server *Server) ServeConn(conn io.ReadWriteCloser) { + defer func() { _ = conn.Close() }() + var opt Option + if err := json.NewDecoder(conn).Decode(&opt); err != nil { + log.Println("rpc server: options error: ", err) + return + } + if opt.MagicNumber != MagicNumber { + log.Printf("rpc server: invalid magic number %x", opt.MagicNumber) + return + } + f := codec.NewCodecFuncMap[opt.CodecType] + if f == nil { + log.Printf("rpc server: invalid codec type %s", opt.CodecType) + return + } + server.serveCodec(f(conn), &opt) +} + +// invalidRequest is a placeholder for response argv when error occurs +var invalidRequest = struct{}{} + +func (server *Server) serveCodec(cc codec.Codec, opt *Option) { + sending := new(sync.Mutex) // make sure to send a complete response + wg := new(sync.WaitGroup) // wait until all request are handled + for { + req, err := server.readRequest(cc) + if err != nil { + if req == nil { + break // it's not possible to recover, so close the connection + } + req.h.Error = err.Error() + server.sendResponse(cc, req.h, invalidRequest, sending) + continue + } + wg.Add(1) + go server.handleRequest(cc, req, sending, wg, opt.HandleTimeout) + } + wg.Wait() + _ = cc.Close() +} + +// request stores all information of a call +type request struct { + h *codec.Header // header of request + argv, replyv reflect.Value // argv and replyv of request + mtype *methodType + svc *service +} + +func (server *Server) readRequestHeader(cc codec.Codec) (*codec.Header, error) { + var h codec.Header + if err := cc.ReadHeader(&h); err != nil { + if err != io.EOF && err != io.ErrUnexpectedEOF { + log.Println("rpc server: read header error:", err) + } + return nil, err + } + return &h, nil +} + +func (server *Server) findService(serviceMethod string) (svc *service, mtype *methodType, err error) { + dot := strings.LastIndex(serviceMethod, ".") + if dot < 0 { + err = errors.New("rpc server: service/method request ill-formed: " + serviceMethod) + return + } + serviceName, methodName := serviceMethod[:dot], serviceMethod[dot+1:] + svci, ok := server.serviceMap.Load(serviceName) + if !ok { + err = errors.New("rpc server: can't find service " + serviceName) + return + } + svc = svci.(*service) + mtype = svc.method[methodName] + if mtype == nil { + err = errors.New("rpc server: can't find method " + methodName) + } + return +} + +func (server *Server) readRequest(cc codec.Codec) (*request, error) { + h, err := server.readRequestHeader(cc) + if err != nil { + return nil, err + } + req := &request{h: h} + req.svc, req.mtype, err = server.findService(h.ServiceMethod) + if err != nil { + return req, err + } + req.argv = req.mtype.newArgv() + req.replyv = req.mtype.newReplyv() + + // make sure that argvi is a pointer, ReadBody need a pointer as parameter + argvi := req.argv.Interface() + if req.argv.Type().Kind() != reflect.Ptr { + argvi = req.argv.Addr().Interface() + } + if err = cc.ReadBody(argvi); err != nil { + log.Println("rpc server: read body err:", err) + return req, err + } + return req, nil +} + +func (server *Server) sendResponse(cc codec.Codec, h *codec.Header, body interface{}, sending *sync.Mutex) { + sending.Lock() + defer sending.Unlock() + if err := cc.Write(h, body); err != nil { + log.Println("rpc server: write response error:", err) + } +} + +func (server *Server) handleRequest(cc codec.Codec, req *request, sending *sync.Mutex, wg *sync.WaitGroup, timeout time.Duration) { + defer wg.Done() + called := make(chan struct{}) + sent := make(chan struct{}) + go func() { + err := req.svc.call(req.mtype, req.argv, req.replyv) + called <- struct{}{} + if err != nil { + req.h.Error = err.Error() + server.sendResponse(cc, req.h, invalidRequest, sending) + sent <- struct{}{} + return + } + server.sendResponse(cc, req.h, req.replyv.Interface(), sending) + sent <- struct{}{} + }() + + if timeout == 0 { + <-called + <-sent + return + } + select { + case <-time.After(timeout): + req.h.Error = fmt.Sprintf("rpc server: request handle timeout: expect within %s", timeout) + server.sendResponse(cc, req.h, invalidRequest, sending) + case <-called: + <-sent + } +} + +// Accept accepts connections on the listener and serves requests +// for each incoming connection. +func (server *Server) Accept(lis net.Listener) { + for { + conn, err := lis.Accept() + if err != nil { + log.Println("rpc server: accept error:", err) + return + } + go server.ServeConn(conn) + } +} + +// Accept accepts connections on the listener and serves requests +// for each incoming connection. +func Accept(lis net.Listener) { DefaultServer.Accept(lis) } + +// Register publishes in the server the set of methods of the +// receiver value that satisfy the following conditions: +// - exported method of exported type +// - two arguments, both of exported type +// - the second argument is a pointer +// - one return value, of type error +func (server *Server) Register(rcvr interface{}) error { + s := newService(rcvr) + if _, dup := server.serviceMap.LoadOrStore(s.name, s); dup { + return errors.New("rpc: service already defined: " + s.name) + } + return nil +} + +// Register publishes the receiver's methods in the DefaultServer. +func Register(rcvr interface{}) error { return DefaultServer.Register(rcvr) } diff --git a/gee-rpc/day4-http-debug/service.go b/gee-rpc/day4-timeout/service.go similarity index 100% rename from gee-rpc/day4-http-debug/service.go rename to gee-rpc/day4-timeout/service.go diff --git a/gee-rpc/day4-http-debug/service_test.go b/gee-rpc/day4-timeout/service_test.go similarity index 100% rename from gee-rpc/day4-http-debug/service_test.go rename to gee-rpc/day4-timeout/service_test.go diff --git a/gee-rpc/day5-http-debug/client.go b/gee-rpc/day5-http-debug/client.go new file mode 100644 index 0000000..5f336d7 --- /dev/null +++ b/gee-rpc/day5-http-debug/client.go @@ -0,0 +1,310 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package geerpc + +import ( + "bufio" + "context" + "encoding/json" + "errors" + "fmt" + "geerpc/codec" + "io" + "log" + "net" + "net/http" + "sync" + "time" +) + +// Call represents an active RPC. +type Call struct { + Seq uint64 + ServiceMethod string // format "." + Args interface{} // arguments to the function + Reply interface{} // reply from the function + Error error // if error occurs, it will be set + Done chan *Call // Strobes when call is complete. +} + +func (call *Call) done() { + call.Done <- call +} + +// Client represents an RPC Client. +// There may be multiple outstanding Calls associated +// with a single Client, and a Client may be used by +// multiple goroutines simultaneously. +type Client struct { + cc codec.Codec + opt *Option + sending sync.Mutex // protect following + header codec.Header + mu sync.Mutex // protect following + seq uint64 + pending map[uint64]*Call + closed bool // user has called Close +} + +var _ io.Closer = (*Client)(nil) + +var ErrShutdown = errors.New("connection is shut down") + +// Close the connection +func (client *Client) Close() error { + client.mu.Lock() + defer client.mu.Unlock() + if client.closed { + return ErrShutdown + } + client.closed = true + return client.cc.Close() +} + +func (client *Client) registerCall(call *Call) (uint64, error) { + client.mu.Lock() + defer client.mu.Unlock() + if client.closed { + return 0, ErrShutdown + } + seq := client.seq + client.pending[seq] = call + client.seq++ + return seq, nil +} + +func (client *Client) removeCall(seq uint64) *Call { + client.mu.Lock() + defer client.mu.Unlock() + call := client.pending[seq] + delete(client.pending, seq) + return call +} + +func (client *Client) terminateCalls(err error) { + client.sending.Lock() + defer client.sending.Unlock() + client.mu.Lock() + defer client.mu.Unlock() + for _, call := range client.pending { + call.Error = err + call.done() + } +} + +func (client *Client) send(call *Call) { + // make sure that the client will send a complete request + client.sending.Lock() + defer client.sending.Unlock() + + // register this call. + seq, err := client.registerCall(call) + call.Seq = seq + if err != nil { + call.Error = err + call.done() + return + } + + // prepare request header + client.header.ServiceMethod = call.ServiceMethod + client.header.Seq = seq + client.header.Error = "" + + // encode and send the request + if err := client.cc.Write(&client.header, call.Args); err != nil { + call := client.removeCall(seq) + // call may be nil, it usually means that Write partially failed, + // client has received the response and handled + if call != nil { + call.Error = err + call.done() + } + } +} + +func (client *Client) receive() { + var err error + for err == nil { + var h codec.Header + if err = client.cc.ReadHeader(&h); err != nil { + break + } + call := client.removeCall(h.Seq) + switch { + case call == nil: + // it usually means that Write partially failed + // and call was already removed. + err = client.cc.ReadBody(nil) + case h.Error != "": + call.Error = fmt.Errorf(h.Error) + err = client.cc.ReadBody(nil) + call.done() + default: + err = client.cc.ReadBody(call.Reply) + if err != nil { + call.Error = errors.New("reading body " + err.Error()) + } + call.done() + } + } + // error occurs, so terminateCalls pending calls + client.terminateCalls(err) +} + +// Go invokes the function asynchronously. +// It returns the Call structure representing the invocation. +func (client *Client) Go(serviceMethod string, args, reply interface{}, done chan *Call) *Call { + if done == nil { + done = make(chan *Call, 10) + } else if cap(done) == 0 { + log.Panic("rpc client: done channel is unbuffered") + } + call := &Call{ + ServiceMethod: serviceMethod, + Args: args, + Reply: reply, + Done: done, + } + client.send(call) + return call +} + +// Call invokes the named function, waits for it to complete, +// and returns its error status. +func (client *Client) Call(ctx context.Context, serviceMethod string, args, reply interface{}) error { + call := client.Go(serviceMethod, args, reply, make(chan *Call, 1)) + select { + case <-ctx.Done(): + client.removeCall(call.Seq) + return errors.New("rpc client: call failed: " + ctx.Err().Error()) + case call := <-call.Done: + return call.Error + } +} + +func parseOptions(opts ...*Option) (*Option, error) { + // if opts is nil or pass nil as parameter + if len(opts) == 0 || opts[0] == nil { + return DefaultOption, nil + } + if len(opts) != 1 { + return nil, errors.New("number of options is more than 1") + } + opt := opts[0] + opt.MagicNumber = DefaultOption.MagicNumber + if opt.CodecType == "" { + opt.CodecType = DefaultOption.CodecType + } + return opt, nil +} + +func NewClient(conn net.Conn, opt *Option) (*Client, error) { + f := codec.NewCodecFuncMap[opt.CodecType] + if f == nil { + err := fmt.Errorf("invalid codec type %s", opt.CodecType) + log.Println("rpc client: codec error:", err) + return nil, err + } + // send options with server + if err := json.NewEncoder(conn).Encode(opt); err != nil { + log.Println("rpc client: options error: ", err) + _ = conn.Close() + return nil, err + } + return newClientCodec(f(conn), opt), nil +} + +func newClientCodec(cc codec.Codec, opt *Option) *Client { + client := &Client{ + seq: 1, // seq starts with 1, 0 means invalid call + cc: cc, + opt: opt, + pending: make(map[uint64]*Call), + } + go client.receive() + return client +} + +func dial(network, address string, opt *Option) (*Client, error) { + conn, err := net.Dial(network, address) + if err != nil { + return nil, err + } + return NewClient(conn, opt) +} + +type clientResult struct { + client *Client + err error +} + +func dialTimeout(f func() (client *Client, err error), timeout time.Duration) (*Client, error) { + if timeout == 0 { + return f() + } + ch := make(chan clientResult) + go func() { + client, err := f() + ch <- clientResult{client: client, err: err} + }() + select { + case <-time.After(timeout): + return nil, fmt.Errorf("rpc client: dial timeout: expect within %s", timeout) + case result := <-ch: + return result.client, result.err + } +} + +// Dial connects to an RPC server at the specified network address +func Dial(network, address string, opts ...*Option) (*Client, error) { + opt, err := parseOptions(opts...) + if err != nil { + return nil, err + } + f := func() (client *Client, err error) { + return dial(network, address, opt) + } + return dialTimeout(f, opt.ConnectTimeout) +} + +func dialHTTPPath(network, address, path string, opt *Option) (*Client, error) { + conn, err := net.Dial(network, address) + if err != nil { + return nil, err + } + _, _ = io.WriteString(conn, fmt.Sprintf("CONNECT %s HTTP/1.0\n\n", path)) + + // Require successful HTTP response + // before switching to RPC protocol. + resp, err := http.ReadResponse(bufio.NewReader(conn), &http.Request{Method: "CONNECT"}) + if err == nil && resp.Status == connected { + return NewClient(conn, opt) + } + if err == nil { + err = errors.New("unexpected HTTP response: " + resp.Status) + } + _ = conn.Close() + return nil, err +} + +// DialHTTPPath connects to an HTTP RPC server +// at the specified network address and path. +func DialHTTPPath(network, address, path string, opts ...*Option) (*Client, error) { + opt, err := parseOptions(opts...) + if err != nil { + return nil, err + } + f := func() (*Client, error) { + return dialHTTPPath(network, address, path, opt) + } + return dialTimeout(f, opt.ConnectTimeout) +} + +// DialHTTP connects to an HTTP RPC server at the specified network address +// listening on the default HTTP RPC path. +func DialHTTP(network, address string, opts ...*Option) (*Client, error) { + return DialHTTPPath(network, address, defaultRPCPath, opts...) +} diff --git a/gee-rpc/day5-http-debug/client_test.go b/gee-rpc/day5-http-debug/client_test.go new file mode 100644 index 0000000..5669f9c --- /dev/null +++ b/gee-rpc/day5-http-debug/client_test.go @@ -0,0 +1,63 @@ +package geerpc + +import ( + "context" + "net" + "strings" + "testing" + "time" +) + +type Bar int + +func (b Bar) Timeout(argv int, reply *int) error { + time.Sleep(time.Second * 2) + return nil +} + +func startServer(addr chan string) { + var b Bar + _ = Register(&b) + // pick a free port + l, _ := net.Listen("tcp", ":0") + addr <- l.Addr().String() + Accept(l) +} + +func TestClient_dialTimeout(t *testing.T) { + t.Parallel() + f := func() (client *Client, err error) { + time.Sleep(time.Second * 2) + return nil, nil + } + t.Run("timeout", func(t *testing.T) { + _, err := dialTimeout(f, time.Second) + _assert(err != nil && strings.Contains(err.Error(), "dial timeout"), "expect a timeout error") + }) + t.Run("0", func(t *testing.T) { + _, err := dialTimeout(f, 0) + _assert(err == nil, "0 means no limit") + }) +} + +func TestClient_Call(t *testing.T) { + t.Parallel() + addrCh := make(chan string) + go startServer(addrCh) + addr := <-addrCh + t.Run("client timeout", func(t *testing.T) { + client, _ := Dial("tcp", addr) + ctx, _ := context.WithTimeout(context.Background(), time.Second) + var reply int + err := client.Call(ctx, "Bar.Timeout", 1, &reply) + _assert(err != nil && strings.Contains(err.Error(), ctx.Err().Error()), "expect a timeout error") + }) + t.Run("server handle timeout", func(t *testing.T) { + client, _ := Dial("tcp", addr, &Option{ + HandleTimeout: time.Second, + }) + var reply int + err := client.Call(context.Background(), "Bar.Timeout", 1, &reply) + _assert(err != nil && strings.Contains(err.Error(), "handle timeout"), "expect a timeout error") + }) +} diff --git a/gee-rpc/day5-http-debug/codec/codec.go b/gee-rpc/day5-http-debug/codec/codec.go new file mode 100644 index 0000000..ba28fba --- /dev/null +++ b/gee-rpc/day5-http-debug/codec/codec.go @@ -0,0 +1,34 @@ +package codec + +import ( + "io" +) + +type Header struct { + ServiceMethod string // format "Service.Method" + Seq uint64 // sequence number chosen by client + Error string +} + +type Codec interface { + io.Closer + ReadHeader(*Header) error + ReadBody(interface{}) error + Write(*Header, interface{}) error +} + +type NewCodecFunc func(io.ReadWriteCloser) Codec + +type Type string + +const ( + GobType Type = "application/gob" + JsonType Type = "application/json" +) + +var NewCodecFuncMap map[Type]NewCodecFunc + +func init() { + NewCodecFuncMap = make(map[Type]NewCodecFunc) + NewCodecFuncMap[GobType] = NewGobCodec +} diff --git a/gee-rpc/day5-http-debug/codec/gob.go b/gee-rpc/day5-http-debug/codec/gob.go new file mode 100644 index 0000000..808d97b --- /dev/null +++ b/gee-rpc/day5-http-debug/codec/gob.go @@ -0,0 +1,57 @@ +package codec + +import ( + "bufio" + "encoding/gob" + "io" + "log" +) + +type GobCodec struct { + conn io.ReadWriteCloser + buf *bufio.Writer + dec *gob.Decoder + enc *gob.Encoder +} + +var _ Codec = (*GobCodec)(nil) + +func NewGobCodec(conn io.ReadWriteCloser) Codec { + buf := bufio.NewWriter(conn) + return &GobCodec{ + conn: conn, + buf: buf, + dec: gob.NewDecoder(conn), + enc: gob.NewEncoder(buf), + } +} + +func (c *GobCodec) ReadHeader(h *Header) error { + return c.dec.Decode(h) +} + +func (c *GobCodec) ReadBody(body interface{}) error { + return c.dec.Decode(body) +} + +func (c *GobCodec) Write(h *Header, body interface{}) (err error) { + defer func() { + _ = c.buf.Flush() + if err != nil { + _ = c.Close() + } + }() + if err := c.enc.Encode(h); err != nil { + log.Println("rpc: gob error encoding header:", err) + return err + } + if err := c.enc.Encode(body); err != nil { + log.Println("rpc: gob error encoding body:", err) + return err + } + return nil +} + +func (c *GobCodec) Close() error { + return c.conn.Close() +} diff --git a/gee-rpc/day4-http-debug/debug.go b/gee-rpc/day5-http-debug/debug.go similarity index 100% rename from gee-rpc/day4-http-debug/debug.go rename to gee-rpc/day5-http-debug/debug.go diff --git a/gee-rpc/day5-http-debug/go.mod b/gee-rpc/day5-http-debug/go.mod new file mode 100644 index 0000000..0ec8aeb --- /dev/null +++ b/gee-rpc/day5-http-debug/go.mod @@ -0,0 +1,3 @@ +module geerpc + +go 1.13 diff --git a/gee-rpc/day4-http-debug/main/main.go b/gee-rpc/day5-http-debug/main/main.go similarity index 90% rename from gee-rpc/day4-http-debug/main/main.go rename to gee-rpc/day5-http-debug/main/main.go index 13bb592..f25d909 100644 --- a/gee-rpc/day4-http-debug/main/main.go +++ b/gee-rpc/day5-http-debug/main/main.go @@ -1,6 +1,7 @@ package main import ( + "context" "geerpc" "log" "net/http" @@ -38,7 +39,7 @@ func call() { defer wg.Done() args := &Args{Num1: i, Num2: i * i} var reply int - if err := client.Call("Foo.Sum", args, &reply); err != nil { + if err := client.Call(context.Background(), "Foo.Sum", args, &reply); err != nil { log.Fatal("call Foo.Sum error:", err) } log.Printf("%d + %d = %d", args.Num1, args.Num2, reply) diff --git a/gee-rpc/day4-http-debug/server.go b/gee-rpc/day5-http-debug/server.go similarity index 80% rename from gee-rpc/day4-http-debug/server.go rename to gee-rpc/day5-http-debug/server.go index ffb85ee..5be1c66 100644 --- a/gee-rpc/day4-http-debug/server.go +++ b/gee-rpc/day5-http-debug/server.go @@ -7,6 +7,7 @@ package geerpc import ( "encoding/json" "errors" + "fmt" "geerpc/codec" "io" "log" @@ -15,18 +16,22 @@ import ( "reflect" "strings" "sync" + "time" ) const MagicNumber = 0x3bef5c -type Options struct { - MagicNumber int // MagicNumber marks this's a geerpc request - CodecType codec.Type // client may choose different Codec to encode body +type Option struct { + MagicNumber int // MagicNumber marks this's a geerpc request + CodecType codec.Type // client may choose different Codec to encode body + ConnectTimeout time.Duration // 0 means no limit + HandleTimeout time.Duration } -var defaultOptions = &Options{ - MagicNumber: MagicNumber, - CodecType: codec.GobType, +var DefaultOption = &Option{ + MagicNumber: MagicNumber, + CodecType: codec.GobType, + ConnectTimeout: time.Second * 10, } // Server represents an RPC Server. @@ -46,7 +51,7 @@ var DefaultServer = NewServer() // ServeConn blocks, serving the connection until the client hangs up. func (server *Server) ServeConn(conn io.ReadWriteCloser) { defer func() { _ = conn.Close() }() - var opt Options + var opt Option if err := json.NewDecoder(conn).Decode(&opt); err != nil { log.Println("rpc server: options error: ", err) return @@ -60,13 +65,13 @@ func (server *Server) ServeConn(conn io.ReadWriteCloser) { log.Printf("rpc server: invalid codec type %s", opt.CodecType) return } - server.serveCodec(f(conn)) + server.serveCodec(f(conn), &opt) } // invalidRequest is a placeholder for response argv when error occurs var invalidRequest = struct{}{} -func (server *Server) serveCodec(cc codec.Codec) { +func (server *Server) serveCodec(cc codec.Codec, opt *Option) { sending := new(sync.Mutex) // make sure to send a complete response wg := new(sync.WaitGroup) // wait until all request are handled for { @@ -80,7 +85,7 @@ func (server *Server) serveCodec(cc codec.Codec) { continue } wg.Add(1) - go server.handleRequest(cc, req, sending, wg) + go server.handleRequest(cc, req, sending, wg, opt.HandleTimeout) } wg.Wait() _ = cc.Close() @@ -108,7 +113,7 @@ func (server *Server) readRequestHeader(cc codec.Codec) (*codec.Header, error) { func (server *Server) findService(serviceMethod string) (svc *service, mtype *methodType, err error) { dot := strings.LastIndex(serviceMethod, ".") if dot < 0 { - err = errors.New("rpc server: service/Method request ill-formed: " + serviceMethod) + err = errors.New("rpc server: service/method request ill-formed: " + serviceMethod) return } serviceName, methodName := serviceMethod[:dot], serviceMethod[dot+1:] @@ -120,7 +125,7 @@ func (server *Server) findService(serviceMethod string) (svc *service, mtype *me svc = svci.(*service) mtype = svc.method[methodName] if mtype == nil { - err = errors.New("rpc server: can't find Method " + methodName) + err = errors.New("rpc server: can't find method " + methodName) } return } @@ -158,13 +163,35 @@ func (server *Server) sendResponse(cc codec.Codec, h *codec.Header, body interfa } } -func (server *Server) handleRequest(cc codec.Codec, req *request, sending *sync.Mutex, wg *sync.WaitGroup) { +func (server *Server) handleRequest(cc codec.Codec, req *request, sending *sync.Mutex, wg *sync.WaitGroup, timeout time.Duration) { defer wg.Done() - err := req.svc.call(req.mtype, req.argv, req.replyv) - if err != nil { - req.h.Error = err.Error() + called := make(chan struct{}) + sent := make(chan struct{}) + go func() { + err := req.svc.call(req.mtype, req.argv, req.replyv) + called <- struct{}{} + if err != nil { + req.h.Error = err.Error() + server.sendResponse(cc, req.h, invalidRequest, sending) + sent <- struct{}{} + return + } + server.sendResponse(cc, req.h, req.replyv.Interface(), sending) + sent <- struct{}{} + }() + + if timeout == 0 { + <-called + <-sent + return + } + select { + case <-time.After(timeout): + req.h.Error = fmt.Sprintf("rpc server: request handle timeout: expect within %s", timeout) + server.sendResponse(cc, req.h, invalidRequest, sending) + case <-called: + <-sent } - server.sendResponse(cc, req.h, req.replyv.Interface(), sending) } // Accept accepts connections on the listener and serves requests @@ -186,7 +213,7 @@ func Accept(lis net.Listener) { DefaultServer.Accept(lis) } // Register publishes in the server the set of methods of the // receiver value that satisfy the following conditions: -// - exported Method of exported type +// - exported method of exported type // - two arguments, both of exported type // - the second argument is a pointer // - one return value, of type error diff --git a/gee-rpc/day5-http-debug/service.go b/gee-rpc/day5-http-debug/service.go new file mode 100644 index 0000000..306683c --- /dev/null +++ b/gee-rpc/day5-http-debug/service.go @@ -0,0 +1,99 @@ +package geerpc + +import ( + "go/ast" + "log" + "reflect" + "sync/atomic" +) + +type methodType struct { + method reflect.Method + ArgType reflect.Type + ReplyType reflect.Type + numCalls uint64 +} + +func (m *methodType) NumCalls() uint64 { + return atomic.LoadUint64(&m.numCalls) +} + +func (m *methodType) newArgv() reflect.Value { + var argv reflect.Value + // arg may be a pointer type, or a value type + if m.ArgType.Kind() == reflect.Ptr { + argv = reflect.New(m.ArgType.Elem()) + } else { + argv = reflect.New(m.ArgType).Elem() + } + return argv +} + +func (m *methodType) newReplyv() reflect.Value { + // reply must be a pointer type + replyv := reflect.New(m.ReplyType.Elem()) + switch m.ReplyType.Elem().Kind() { + case reflect.Map: + replyv.Elem().Set(reflect.MakeMap(m.ReplyType.Elem())) + case reflect.Slice: + replyv.Elem().Set(reflect.MakeSlice(m.ReplyType.Elem(), 0, 0)) + } + return replyv +} + +type service struct { + name string + typ reflect.Type + rcvr reflect.Value + method map[string]*methodType +} + +func newService(rcvr interface{}) *service { + s := new(service) + s.rcvr = reflect.ValueOf(rcvr) + s.name = reflect.Indirect(s.rcvr).Type().Name() + s.typ = reflect.TypeOf(rcvr) + if !ast.IsExported(s.name) { + log.Fatalf("rpc server: %s is not a valid service name", s.name) + } + s.registerMethods() + return s +} + +func (s *service) registerMethods() { + s.method = make(map[string]*methodType) + for i := 0; i < s.typ.NumMethod(); i++ { + method := s.typ.Method(i) + mType := method.Type + if mType.NumIn() != 3 || mType.NumOut() != 1 { + continue + } + if mType.Out(0) != reflect.TypeOf((*error)(nil)).Elem() { + continue + } + argType, replyType := mType.In(1), mType.In(2) + if !isExportedOrBuiltinType(argType) || !isExportedOrBuiltinType(replyType) { + continue + } + s.method[method.Name] = &methodType{ + method: method, + ArgType: argType, + ReplyType: replyType, + } + log.Printf("rpc server: register %s.%s\n", s.name, method.Name) + } +} + +func (s *service) call(m *methodType, argv, replyv reflect.Value) error { + atomic.AddUint64(&m.numCalls, 1) + f := m.method.Func + returnValues := f.Call([]reflect.Value{s.rcvr, argv, replyv}) + if errInter := returnValues[0].Interface(); errInter != nil { + return errInter.(error) + } + return nil +} + +func isExportedOrBuiltinType(t reflect.Type) bool { + return ast.IsExported(t.Name()) || t.PkgPath() == "" +} diff --git a/gee-rpc/day5-http-debug/service_test.go b/gee-rpc/day5-http-debug/service_test.go new file mode 100644 index 0000000..c8266df --- /dev/null +++ b/gee-rpc/day5-http-debug/service_test.go @@ -0,0 +1,48 @@ +package geerpc + +import ( + "fmt" + "reflect" + "testing" +) + +type Foo int + +type Args struct{ Num1, Num2 int } + +func (f Foo) Sum(args Args, reply *int) error { + *reply = args.Num1 + args.Num2 + return nil +} + +// it's not a exported Method +func (f Foo) sum(args Args, reply *int) error { + *reply = args.Num1 + args.Num2 + return nil +} + +func _assert(condition bool, msg string, v ...interface{}) { + if !condition { + panic(fmt.Sprintf("assertion failed: "+msg, v...)) + } +} + +func TestNewService(t *testing.T) { + var foo Foo + s := newService(&foo) + _assert(len(s.method) == 1, "wrong service Method, expect 1, but got %d", len(s.method)) + mType := s.method["Sum"] + _assert(mType != nil, "wrong Method, Sum shouldn't nil") +} + +func TestMethodType_Call(t *testing.T) { + var foo Foo + s := newService(&foo) + mType := s.method["Sum"] + + argv := mType.newArgv() + replyv := mType.newReplyv() + argv.Set(reflect.ValueOf(Args{Num1: 1, Num2: 3})) + err := s.call(mType, argv, replyv) + _assert(err == nil && *replyv.Interface().(*int) == 4 && mType.NumCalls() == 1, "failed to call Foo.Sum") +} From bc9eadfff653a36f35866a7083a5ce7355155edf Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Sun, 4 Oct 2020 10:08:04 +0800 Subject: [PATCH 081/122] gee-rpc day6 registry --- gee-rpc/day2-client/client.go | 31 +- gee-rpc/day3-service/client.go | 31 +- gee-rpc/day4-timeout/client.go | 73 +++-- gee-rpc/day4-timeout/client_test.go | 6 +- gee-rpc/day5-http-debug/client.go | 110 ++++--- gee-rpc/day5-http-debug/client_test.go | 27 +- gee-rpc/day5-http-debug/debug.go | 2 +- gee-rpc/day5-http-debug/main/main.go | 19 +- gee-rpc/day5-http-debug/server.go | 11 +- gee-rpc/day6-discovery/client.go | 324 ++++++++++++++++++++ gee-rpc/day6-discovery/client_test.go | 84 +++++ gee-rpc/day6-discovery/codec/codec.go | 34 ++ gee-rpc/day6-discovery/codec/gob.go | 57 ++++ gee-rpc/day6-discovery/debug.go | 60 ++++ gee-rpc/day6-discovery/go.mod | 3 + gee-rpc/day6-discovery/main/main.go | 55 ++++ gee-rpc/day6-discovery/server.go | 266 ++++++++++++++++ gee-rpc/day6-discovery/service.go | 99 ++++++ gee-rpc/day6-discovery/service_test.go | 48 +++ gee-rpc/day6-discovery/xclient/discovery.go | 57 ++++ gee-rpc/day6-discovery/xclient/xclient.go | 48 +++ 21 files changed, 1322 insertions(+), 123 deletions(-) create mode 100644 gee-rpc/day6-discovery/client.go create mode 100644 gee-rpc/day6-discovery/client_test.go create mode 100644 gee-rpc/day6-discovery/codec/codec.go create mode 100644 gee-rpc/day6-discovery/codec/gob.go create mode 100644 gee-rpc/day6-discovery/debug.go create mode 100644 gee-rpc/day6-discovery/go.mod create mode 100644 gee-rpc/day6-discovery/main/main.go create mode 100644 gee-rpc/day6-discovery/server.go create mode 100644 gee-rpc/day6-discovery/service.go create mode 100644 gee-rpc/day6-discovery/service_test.go create mode 100644 gee-rpc/day6-discovery/xclient/discovery.go create mode 100644 gee-rpc/day6-discovery/xclient/xclient.go diff --git a/gee-rpc/day2-client/client.go b/gee-rpc/day2-client/client.go index f9b09ee..3df6ec9 100644 --- a/gee-rpc/day2-client/client.go +++ b/gee-rpc/day2-client/client.go @@ -34,14 +34,15 @@ func (call *Call) done() { // with a single Client, and a Client may be used by // multiple goroutines simultaneously. type Client struct { - cc codec.Codec - opt *Option - sending sync.Mutex // protect following - header codec.Header - mu sync.Mutex // protect following - seq uint64 - pending map[uint64]*Call - closed bool // user has called Close + cc codec.Codec + opt *Option + sending sync.Mutex // protect following + header codec.Header + mu sync.Mutex // protect following + seq uint64 + pending map[uint64]*Call + closing bool // user has called Close + shutdown bool // server has told us to stop } var _ io.Closer = (*Client)(nil) @@ -52,17 +53,24 @@ var ErrShutdown = errors.New("connection is shut down") func (client *Client) Close() error { client.mu.Lock() defer client.mu.Unlock() - if client.closed { + if client.closing { return ErrShutdown } - client.closed = true + client.closing = true return client.cc.Close() } +// IsAvailable return true if the client does work +func (client *Client) IsAvailable() bool { + client.mu.Lock() + defer client.mu.Unlock() + return !client.shutdown && !client.closing +} + func (client *Client) registerCall(call *Call) (uint64, error) { client.mu.Lock() defer client.mu.Unlock() - if client.closed { + if client.closing || client.shutdown { return 0, ErrShutdown } seq := client.seq @@ -84,6 +92,7 @@ func (client *Client) terminateCalls(err error) { defer client.sending.Unlock() client.mu.Lock() defer client.mu.Unlock() + client.shutdown = true for _, call := range client.pending { call.Error = err call.done() diff --git a/gee-rpc/day3-service/client.go b/gee-rpc/day3-service/client.go index f9b09ee..3df6ec9 100644 --- a/gee-rpc/day3-service/client.go +++ b/gee-rpc/day3-service/client.go @@ -34,14 +34,15 @@ func (call *Call) done() { // with a single Client, and a Client may be used by // multiple goroutines simultaneously. type Client struct { - cc codec.Codec - opt *Option - sending sync.Mutex // protect following - header codec.Header - mu sync.Mutex // protect following - seq uint64 - pending map[uint64]*Call - closed bool // user has called Close + cc codec.Codec + opt *Option + sending sync.Mutex // protect following + header codec.Header + mu sync.Mutex // protect following + seq uint64 + pending map[uint64]*Call + closing bool // user has called Close + shutdown bool // server has told us to stop } var _ io.Closer = (*Client)(nil) @@ -52,17 +53,24 @@ var ErrShutdown = errors.New("connection is shut down") func (client *Client) Close() error { client.mu.Lock() defer client.mu.Unlock() - if client.closed { + if client.closing { return ErrShutdown } - client.closed = true + client.closing = true return client.cc.Close() } +// IsAvailable return true if the client does work +func (client *Client) IsAvailable() bool { + client.mu.Lock() + defer client.mu.Unlock() + return !client.shutdown && !client.closing +} + func (client *Client) registerCall(call *Call) (uint64, error) { client.mu.Lock() defer client.mu.Unlock() - if client.closed { + if client.closing || client.shutdown { return 0, ErrShutdown } seq := client.seq @@ -84,6 +92,7 @@ func (client *Client) terminateCalls(err error) { defer client.sending.Unlock() client.mu.Lock() defer client.mu.Unlock() + client.shutdown = true for _, call := range client.pending { call.Error = err call.done() diff --git a/gee-rpc/day4-timeout/client.go b/gee-rpc/day4-timeout/client.go index c9900fa..b301647 100644 --- a/gee-rpc/day4-timeout/client.go +++ b/gee-rpc/day4-timeout/client.go @@ -36,14 +36,15 @@ func (call *Call) done() { // with a single Client, and a Client may be used by // multiple goroutines simultaneously. type Client struct { - cc codec.Codec - opt *Option - sending sync.Mutex // protect following - header codec.Header - mu sync.Mutex // protect following - seq uint64 - pending map[uint64]*Call - closed bool // user has called Close + cc codec.Codec + opt *Option + sending sync.Mutex // protect following + header codec.Header + mu sync.Mutex // protect following + seq uint64 + pending map[uint64]*Call + closing bool // user has called Close + shutdown bool // server has told us to stop } var _ io.Closer = (*Client)(nil) @@ -54,17 +55,24 @@ var ErrShutdown = errors.New("connection is shut down") func (client *Client) Close() error { client.mu.Lock() defer client.mu.Unlock() - if client.closed { + if client.closing { return ErrShutdown } - client.closed = true + client.closing = true return client.cc.Close() } +// IsAvailable return true if the client does work +func (client *Client) IsAvailable() bool { + client.mu.Lock() + defer client.mu.Unlock() + return !client.shutdown && !client.closing +} + func (client *Client) registerCall(call *Call) (uint64, error) { client.mu.Lock() defer client.mu.Unlock() - if client.closed { + if client.closing || client.shutdown { return 0, ErrShutdown } seq := client.seq @@ -86,6 +94,7 @@ func (client *Client) terminateCalls(err error) { defer client.sending.Unlock() client.mu.Lock() defer client.mu.Unlock() + client.shutdown = true for _, call := range client.pending { call.Error = err call.done() @@ -226,44 +235,44 @@ func newClientCodec(cc codec.Codec, opt *Option) *Client { return client } -func dial(network, address string, opt *Option) (*Client, error) { - conn, err := net.Dial(network, address) - if err != nil { - return nil, err - } - return NewClient(conn, opt) -} - type clientResult struct { client *Client err error } -func dialTimeout(f func() (client *Client, err error), timeout time.Duration) (*Client, error) { - if timeout == 0 { - return f() +type dialFunc func(network, address string, opt *Option) (client *Client, err error) + +func dialTimeout(f dialFunc, network, address string, opts ...*Option) (*Client, error) { + opt, err := parseOptions(opts...) + if err != nil { + return nil, err } ch := make(chan clientResult) go func() { - client, err := f() + client, err := f(network, address, opt) ch <- clientResult{client: client, err: err} }() + if opt.ConnectTimeout == 0 { + result := <-ch + return result.client, result.err + } select { - case <-time.After(timeout): - return nil, fmt.Errorf("rpc client: dial timeout: expect within %s", timeout) + case <-time.After(opt.ConnectTimeout): + return nil, fmt.Errorf("rpc client: dial timeout: expect within %s", opt.ConnectTimeout) case result := <-ch: return result.client, result.err } } -// Dial connects to an RPC server at the specified network address -func Dial(network, address string, opts ...*Option) (*Client, error) { - opt, err := parseOptions(opts...) +func dial(network, address string, opt *Option) (*Client, error) { + conn, err := net.Dial(network, address) if err != nil { return nil, err } - f := func() (client *Client, err error) { - return dial(network, address, opt) - } - return dialTimeout(f, opt.ConnectTimeout) + return NewClient(conn, opt) +} + +// Dial connects to an RPC server at the specified network address +func Dial(network, address string, opts ...*Option) (*Client, error) { + return dialTimeout(dial, network, address, opts...) } diff --git a/gee-rpc/day4-timeout/client_test.go b/gee-rpc/day4-timeout/client_test.go index 5669f9c..ab2fa64 100644 --- a/gee-rpc/day4-timeout/client_test.go +++ b/gee-rpc/day4-timeout/client_test.go @@ -26,16 +26,16 @@ func startServer(addr chan string) { func TestClient_dialTimeout(t *testing.T) { t.Parallel() - f := func() (client *Client, err error) { + f := func(network, address string, opt *Option) (client *Client, err error) { time.Sleep(time.Second * 2) return nil, nil } t.Run("timeout", func(t *testing.T) { - _, err := dialTimeout(f, time.Second) + _, err := dialTimeout(f, "", "", &Option{ConnectTimeout: time.Second}) _assert(err != nil && strings.Contains(err.Error(), "dial timeout"), "expect a timeout error") }) t.Run("0", func(t *testing.T) { - _, err := dialTimeout(f, 0) + _, err := dialTimeout(f, "", "", &Option{ConnectTimeout: 0}) _assert(err == nil, "0 means no limit") }) } diff --git a/gee-rpc/day5-http-debug/client.go b/gee-rpc/day5-http-debug/client.go index 5f336d7..e9b4540 100644 --- a/gee-rpc/day5-http-debug/client.go +++ b/gee-rpc/day5-http-debug/client.go @@ -15,6 +15,7 @@ import ( "log" "net" "net/http" + "strings" "sync" "time" ) @@ -38,14 +39,15 @@ func (call *Call) done() { // with a single Client, and a Client may be used by // multiple goroutines simultaneously. type Client struct { - cc codec.Codec - opt *Option - sending sync.Mutex // protect following - header codec.Header - mu sync.Mutex // protect following - seq uint64 - pending map[uint64]*Call - closed bool // user has called Close + cc codec.Codec + opt *Option + sending sync.Mutex // protect following + header codec.Header + mu sync.Mutex // protect following + seq uint64 + pending map[uint64]*Call + closing bool // user has called Close + shutdown bool // server has told us to stop } var _ io.Closer = (*Client)(nil) @@ -56,17 +58,24 @@ var ErrShutdown = errors.New("connection is shut down") func (client *Client) Close() error { client.mu.Lock() defer client.mu.Unlock() - if client.closed { + if client.closing { return ErrShutdown } - client.closed = true + client.closing = true return client.cc.Close() } +// IsAvailable return true if the client does work +func (client *Client) IsAvailable() bool { + client.mu.Lock() + defer client.mu.Unlock() + return !client.shutdown && !client.closing +} + func (client *Client) registerCall(call *Call) (uint64, error) { client.mu.Lock() defer client.mu.Unlock() - if client.closed { + if client.closing || client.shutdown { return 0, ErrShutdown } seq := client.seq @@ -88,6 +97,7 @@ func (client *Client) terminateCalls(err error) { defer client.sending.Unlock() client.mu.Lock() defer client.mu.Unlock() + client.shutdown = true for _, call := range client.pending { call.Error = err call.done() @@ -228,54 +238,54 @@ func newClientCodec(cc codec.Codec, opt *Option) *Client { return client } -func dial(network, address string, opt *Option) (*Client, error) { - conn, err := net.Dial(network, address) - if err != nil { - return nil, err - } - return NewClient(conn, opt) -} - type clientResult struct { client *Client err error } -func dialTimeout(f func() (client *Client, err error), timeout time.Duration) (*Client, error) { - if timeout == 0 { - return f() +type dialFunc func(network, address string, opt *Option) (client *Client, err error) + +func dialTimeout(f dialFunc, network, address string, opts ...*Option) (*Client, error) { + opt, err := parseOptions(opts...) + if err != nil { + return nil, err } ch := make(chan clientResult) go func() { - client, err := f() + client, err := f(network, address, opt) ch <- clientResult{client: client, err: err} }() + if opt.ConnectTimeout == 0 { + result := <-ch + return result.client, result.err + } select { - case <-time.After(timeout): - return nil, fmt.Errorf("rpc client: dial timeout: expect within %s", timeout) + case <-time.After(opt.ConnectTimeout): + return nil, fmt.Errorf("rpc client: dial timeout: expect within %s", opt.ConnectTimeout) case result := <-ch: return result.client, result.err } } -// Dial connects to an RPC server at the specified network address -func Dial(network, address string, opts ...*Option) (*Client, error) { - opt, err := parseOptions(opts...) +func dial(network, address string, opt *Option) (*Client, error) { + conn, err := net.Dial(network, address) if err != nil { return nil, err } - f := func() (client *Client, err error) { - return dial(network, address, opt) - } - return dialTimeout(f, opt.ConnectTimeout) + return NewClient(conn, opt) } -func dialHTTPPath(network, address, path string, opt *Option) (*Client, error) { +// Dial connects to an RPC server at the specified network address +func Dial(network, address string, opts ...*Option) (*Client, error) { + return dialTimeout(dial, network, address, opts...) +} + +func dialHTTP(network, address string, opt *Option) (*Client, error) { conn, err := net.Dial(network, address) if err != nil { return nil, err } - _, _ = io.WriteString(conn, fmt.Sprintf("CONNECT %s HTTP/1.0\n\n", path)) + _, _ = io.WriteString(conn, fmt.Sprintf("CONNECT %s HTTP/1.0\n\n", defaultRPCPath)) // Require successful HTTP response // before switching to RPC protocol. @@ -290,21 +300,25 @@ func dialHTTPPath(network, address, path string, opt *Option) (*Client, error) { return nil, err } -// DialHTTPPath connects to an HTTP RPC server -// at the specified network address and path. -func DialHTTPPath(network, address, path string, opts ...*Option) (*Client, error) { - opt, err := parseOptions(opts...) - if err != nil { - return nil, err - } - f := func() (*Client, error) { - return dialHTTPPath(network, address, path, opt) - } - return dialTimeout(f, opt.ConnectTimeout) -} - // DialHTTP connects to an HTTP RPC server at the specified network address // listening on the default HTTP RPC path. func DialHTTP(network, address string, opts ...*Option) (*Client, error) { - return DialHTTPPath(network, address, defaultRPCPath, opts...) + return dialTimeout(dialHTTP, network, address, opts...) +} + +// XDial use a general format to represent a rpc server +// eg, http@10.0.0.1:7001, tcp@10.0.0.1:9999, unix@/tmp/geerpc.sock +func XDial(rpcAddr string, opts ...*Option) (*Client, error) { + parts := strings.Split(rpcAddr, "@") + if len(parts) != 2 { + return nil, fmt.Errorf("rpc client err: wrong format '%s', expect protocol@addr", rpcAddr) + } + protocol, addr := parts[0], parts[1] + switch protocol { + case "http": + return DialHTTP("tcp", addr, opts...) + default: + // tcp, unix or other transport protocol + return Dial(protocol, addr, opts...) + } } diff --git a/gee-rpc/day5-http-debug/client_test.go b/gee-rpc/day5-http-debug/client_test.go index 5669f9c..10b9817 100644 --- a/gee-rpc/day5-http-debug/client_test.go +++ b/gee-rpc/day5-http-debug/client_test.go @@ -3,6 +3,8 @@ package geerpc import ( "context" "net" + "os" + "runtime" "strings" "testing" "time" @@ -26,16 +28,16 @@ func startServer(addr chan string) { func TestClient_dialTimeout(t *testing.T) { t.Parallel() - f := func() (client *Client, err error) { + f := func(network, address string, opt *Option) (client *Client, err error) { time.Sleep(time.Second * 2) return nil, nil } t.Run("timeout", func(t *testing.T) { - _, err := dialTimeout(f, time.Second) + _, err := dialTimeout(f, "", "", &Option{ConnectTimeout: time.Second}) _assert(err != nil && strings.Contains(err.Error(), "dial timeout"), "expect a timeout error") }) t.Run("0", func(t *testing.T) { - _, err := dialTimeout(f, 0) + _, err := dialTimeout(f, "", "", &Option{ConnectTimeout: 0}) _assert(err == nil, "0 means no limit") }) } @@ -61,3 +63,22 @@ func TestClient_Call(t *testing.T) { _assert(err != nil && strings.Contains(err.Error(), "handle timeout"), "expect a timeout error") }) } + +func TestXDial(t *testing.T) { + if runtime.GOOS == "linux" { + ch := make(chan struct{}) + addr := "/tmp/geerpc.sock" + go func() { + _ = os.Remove(addr) + l, err := net.Listen("unix", addr) + if err != nil { + t.Fatal("failed to listen unix socket") + } + ch <- struct{}{} + Accept(l) + }() + <-ch + _, err := XDial("unix@" + addr) + _assert(err == nil, "failed to connect unix socket") + } +} diff --git a/gee-rpc/day5-http-debug/debug.go b/gee-rpc/day5-http-debug/debug.go index d76de55..ece1ffd 100644 --- a/gee-rpc/day5-http-debug/debug.go +++ b/gee-rpc/day5-http-debug/debug.go @@ -41,7 +41,7 @@ type debugService struct { Method map[string]*methodType } -// Runs at /debug/rpc +// Runs at /debug/geerpc func (server debugHTTP) ServeHTTP(w http.ResponseWriter, req *http.Request) { // Build a sorted version of the data. var services []debugService diff --git a/gee-rpc/day5-http-debug/main/main.go b/gee-rpc/day5-http-debug/main/main.go index f25d909..a71af74 100644 --- a/gee-rpc/day5-http-debug/main/main.go +++ b/gee-rpc/day5-http-debug/main/main.go @@ -4,9 +4,9 @@ import ( "context" "geerpc" "log" + "net" "net/http" "sync" - "time" ) type Foo int @@ -18,17 +18,17 @@ func (f Foo) Sum(args Args, reply *int) error { return nil } -func startServer(addr string) { +func startServer(addrCh chan string) { var foo Foo + l, _ := net.Listen("tcp", ":9999") _ = geerpc.Register(&foo) geerpc.HandleHTTP() - log.Fatal(http.ListenAndServe(addr, nil)) + addrCh <- l.Addr().String() + _ = http.Serve(l, nil) } -func call() { - // start server may cost some time - time.Sleep(time.Second) - client, _ := geerpc.DialHTTP("tcp", ":9999") +func call(addrCh chan string) { + client, _ := geerpc.DialHTTP("tcp", <-addrCh) defer func() { _ = client.Close() }() // send request & receive response @@ -49,6 +49,7 @@ func call() { } func main() { - go call() - startServer(":9999") + ch := make(chan string) + go call(ch) + startServer(ch) } diff --git a/gee-rpc/day5-http-debug/server.go b/gee-rpc/day5-http-debug/server.go index 5be1c66..38fad20 100644 --- a/gee-rpc/day5-http-debug/server.go +++ b/gee-rpc/day5-http-debug/server.go @@ -254,12 +254,13 @@ func (server *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) { // HandleHTTP registers an HTTP handler for RPC messages on rpcPath, // and a debugging handler on debugPath. // It is still necessary to invoke http.Serve(), typically in a go statement. -func (server *Server) HandleHTTP(rpcPath, debugPath string) { - http.Handle(rpcPath, server) - http.Handle(debugPath, debugHTTP{server}) - log.Println("rpc server debug path:", debugPath) +func (server *Server) HandleHTTP() { + http.Handle(defaultRPCPath, server) + http.Handle(defaultDebugPath, debugHTTP{server}) + log.Println("rpc server debug path:", defaultDebugPath) } +// HandleHTTP is a convenient approach for default server to register HTTP handlers func HandleHTTP() { - DefaultServer.HandleHTTP(defaultRPCPath, defaultDebugPath) + DefaultServer.HandleHTTP() } diff --git a/gee-rpc/day6-discovery/client.go b/gee-rpc/day6-discovery/client.go new file mode 100644 index 0000000..d958696 --- /dev/null +++ b/gee-rpc/day6-discovery/client.go @@ -0,0 +1,324 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package geerpc + +import ( + "bufio" + "context" + "encoding/json" + "errors" + "fmt" + "geerpc/codec" + "io" + "log" + "net" + "net/http" + "strings" + "sync" + "time" +) + +// Call represents an active RPC. +type Call struct { + Seq uint64 + ServiceMethod string // format "." + Args interface{} // arguments to the function + Reply interface{} // reply from the function + Error error // if error occurs, it will be set + Done chan *Call // Strobes when call is complete. +} + +func (call *Call) done() { + call.Done <- call +} + +// Client represents an RPC Client. +// There may be multiple outstanding Calls associated +// with a single Client, and a Client may be used by +// multiple goroutines simultaneously. +type Client struct { + cc codec.Codec + opt *Option + sending sync.Mutex // protect following + header codec.Header + mu sync.Mutex // protect following + seq uint64 + pending map[uint64]*Call + closing bool // user has called Close + shutdown bool // server has told us to stop +} + +var _ io.Closer = (*Client)(nil) + +var ErrShutdown = errors.New("connection is shut down") + +// Close the connection +func (client *Client) Close() error { + client.mu.Lock() + defer client.mu.Unlock() + if client.closing { + return ErrShutdown + } + client.closing = true + return client.cc.Close() +} + +// IsAvailable return true if the client does work +func (client *Client) IsAvailable() bool { + client.mu.Lock() + defer client.mu.Unlock() + return !client.shutdown && !client.closing +} + +func (client *Client) registerCall(call *Call) (uint64, error) { + client.mu.Lock() + defer client.mu.Unlock() + if client.closing || client.shutdown { + return 0, ErrShutdown + } + seq := client.seq + client.pending[seq] = call + client.seq++ + return seq, nil +} + +func (client *Client) removeCall(seq uint64) *Call { + client.mu.Lock() + defer client.mu.Unlock() + call := client.pending[seq] + delete(client.pending, seq) + return call +} + +func (client *Client) terminateCalls(err error) { + client.sending.Lock() + defer client.sending.Unlock() + client.mu.Lock() + defer client.mu.Unlock() + client.shutdown = true + for _, call := range client.pending { + call.Error = err + call.done() + } +} + +func (client *Client) send(call *Call) { + // make sure that the client will send a complete request + client.sending.Lock() + defer client.sending.Unlock() + + // register this call. + seq, err := client.registerCall(call) + call.Seq = seq + if err != nil { + call.Error = err + call.done() + return + } + + // prepare request header + client.header.ServiceMethod = call.ServiceMethod + client.header.Seq = seq + client.header.Error = "" + + // encode and send the request + if err := client.cc.Write(&client.header, call.Args); err != nil { + call := client.removeCall(seq) + // call may be nil, it usually means that Write partially failed, + // client has received the response and handled + if call != nil { + call.Error = err + call.done() + } + } +} + +func (client *Client) receive() { + var err error + for err == nil { + var h codec.Header + if err = client.cc.ReadHeader(&h); err != nil { + break + } + call := client.removeCall(h.Seq) + switch { + case call == nil: + // it usually means that Write partially failed + // and call was already removed. + err = client.cc.ReadBody(nil) + case h.Error != "": + call.Error = fmt.Errorf(h.Error) + err = client.cc.ReadBody(nil) + call.done() + default: + err = client.cc.ReadBody(call.Reply) + if err != nil { + call.Error = errors.New("reading body " + err.Error()) + } + call.done() + } + } + // error occurs, so terminateCalls pending calls + client.terminateCalls(err) +} + +// Go invokes the function asynchronously. +// It returns the Call structure representing the invocation. +func (client *Client) Go(serviceMethod string, args, reply interface{}, done chan *Call) *Call { + if done == nil { + done = make(chan *Call, 10) + } else if cap(done) == 0 { + log.Panic("rpc client: done channel is unbuffered") + } + call := &Call{ + ServiceMethod: serviceMethod, + Args: args, + Reply: reply, + Done: done, + } + client.send(call) + return call +} + +// Call invokes the named function, waits for it to complete, +// and returns its error status. +func (client *Client) Call(ctx context.Context, serviceMethod string, args, reply interface{}) error { + call := client.Go(serviceMethod, args, reply, make(chan *Call, 1)) + select { + case <-ctx.Done(): + client.removeCall(call.Seq) + return errors.New("rpc client: call failed: " + ctx.Err().Error()) + case call := <-call.Done: + return call.Error + } +} + +func parseOptions(opts ...*Option) (*Option, error) { + // if opts is nil or pass nil as parameter + if len(opts) == 0 || opts[0] == nil { + return DefaultOption, nil + } + if len(opts) != 1 { + return nil, errors.New("number of options is more than 1") + } + opt := opts[0] + opt.MagicNumber = DefaultOption.MagicNumber + if opt.CodecType == "" { + opt.CodecType = DefaultOption.CodecType + } + return opt, nil +} + +func NewClient(conn net.Conn, opt *Option) (*Client, error) { + f := codec.NewCodecFuncMap[opt.CodecType] + if f == nil { + err := fmt.Errorf("invalid codec type %s", opt.CodecType) + log.Println("rpc client: codec error:", err) + return nil, err + } + // send options with server + if err := json.NewEncoder(conn).Encode(opt); err != nil { + log.Println("rpc client: options error: ", err) + _ = conn.Close() + return nil, err + } + return newClientCodec(f(conn), opt), nil +} + +func newClientCodec(cc codec.Codec, opt *Option) *Client { + client := &Client{ + seq: 1, // seq starts with 1, 0 means invalid call + cc: cc, + opt: opt, + pending: make(map[uint64]*Call), + } + go client.receive() + return client +} + +type clientResult struct { + client *Client + err error +} + +type dialFunc func(network, address string, opt *Option) (client *Client, err error) + +func dialTimeout(f dialFunc, network, address string, opts ...*Option) (*Client, error) { + opt, err := parseOptions(opts...) + if err != nil { + return nil, err + } + ch := make(chan clientResult) + go func() { + client, err := f(network, address, opt) + ch <- clientResult{client: client, err: err} + }() + if opt.ConnectTimeout == 0 { + result := <-ch + return result.client, result.err + } + select { + case <-time.After(opt.ConnectTimeout): + return nil, fmt.Errorf("rpc client: dial timeout: expect within %s", opt.ConnectTimeout) + case result := <-ch: + return result.client, result.err + } +} + +func dial(network, address string, opt *Option) (*Client, error) { + conn, err := net.Dial(network, address) + if err != nil { + return nil, err + } + return NewClient(conn, opt) +} + +// Dial connects to an RPC server at the specified network address +func Dial(network, address string, opts ...*Option) (*Client, error) { + return dialTimeout(dial, network, address, opts...) +} + +func dialHTTP(network, address string, opt *Option) (*Client, error) { + conn, err := net.Dial(network, address) + if err != nil { + return nil, err + } + _, _ = io.WriteString(conn, fmt.Sprintf("CONNECT %s HTTP/1.0\n\n", defaultRPCPath)) + + // Require successful HTTP response + // before switching to RPC protocol. + resp, err := http.ReadResponse(bufio.NewReader(conn), &http.Request{Method: "CONNECT"}) + if err == nil && resp.Status == connected { + return NewClient(conn, opt) + } + if err == nil { + err = errors.New("unexpected HTTP response: " + resp.Status) + } + _ = conn.Close() + return nil, err +} + +// DialHTTP connects to an HTTP RPC server at the specified network address +// listening on the default HTTP RPC path. +func DialHTTP(network, address string, opts ...*Option) (*Client, error) { + return dialTimeout(dialHTTP, network, address, opts...) +} + +// XDial use a general format to represent a rpc server +// eg, http@10.0.0.1:7001, tcp@10.0.0.1:9999, unix@/tmp/geerpc.sock +func XDial(rpcAddr string, opts ...*Option) (*Client, error) { + parts := strings.Split(rpcAddr, "@") + if len(parts) != 2 { + return nil, fmt.Errorf("rpc client err: wrong format '%s', expect protocol@addr", rpcAddr) + } + protocol, addr := parts[0], parts[1] + switch protocol { + case "http": + return DialHTTP("tcp", addr) + default: + // tcp, unix or other transport protocol + return Dial(protocol, addr) + } +} diff --git a/gee-rpc/day6-discovery/client_test.go b/gee-rpc/day6-discovery/client_test.go new file mode 100644 index 0000000..10b9817 --- /dev/null +++ b/gee-rpc/day6-discovery/client_test.go @@ -0,0 +1,84 @@ +package geerpc + +import ( + "context" + "net" + "os" + "runtime" + "strings" + "testing" + "time" +) + +type Bar int + +func (b Bar) Timeout(argv int, reply *int) error { + time.Sleep(time.Second * 2) + return nil +} + +func startServer(addr chan string) { + var b Bar + _ = Register(&b) + // pick a free port + l, _ := net.Listen("tcp", ":0") + addr <- l.Addr().String() + Accept(l) +} + +func TestClient_dialTimeout(t *testing.T) { + t.Parallel() + f := func(network, address string, opt *Option) (client *Client, err error) { + time.Sleep(time.Second * 2) + return nil, nil + } + t.Run("timeout", func(t *testing.T) { + _, err := dialTimeout(f, "", "", &Option{ConnectTimeout: time.Second}) + _assert(err != nil && strings.Contains(err.Error(), "dial timeout"), "expect a timeout error") + }) + t.Run("0", func(t *testing.T) { + _, err := dialTimeout(f, "", "", &Option{ConnectTimeout: 0}) + _assert(err == nil, "0 means no limit") + }) +} + +func TestClient_Call(t *testing.T) { + t.Parallel() + addrCh := make(chan string) + go startServer(addrCh) + addr := <-addrCh + t.Run("client timeout", func(t *testing.T) { + client, _ := Dial("tcp", addr) + ctx, _ := context.WithTimeout(context.Background(), time.Second) + var reply int + err := client.Call(ctx, "Bar.Timeout", 1, &reply) + _assert(err != nil && strings.Contains(err.Error(), ctx.Err().Error()), "expect a timeout error") + }) + t.Run("server handle timeout", func(t *testing.T) { + client, _ := Dial("tcp", addr, &Option{ + HandleTimeout: time.Second, + }) + var reply int + err := client.Call(context.Background(), "Bar.Timeout", 1, &reply) + _assert(err != nil && strings.Contains(err.Error(), "handle timeout"), "expect a timeout error") + }) +} + +func TestXDial(t *testing.T) { + if runtime.GOOS == "linux" { + ch := make(chan struct{}) + addr := "/tmp/geerpc.sock" + go func() { + _ = os.Remove(addr) + l, err := net.Listen("unix", addr) + if err != nil { + t.Fatal("failed to listen unix socket") + } + ch <- struct{}{} + Accept(l) + }() + <-ch + _, err := XDial("unix@" + addr) + _assert(err == nil, "failed to connect unix socket") + } +} diff --git a/gee-rpc/day6-discovery/codec/codec.go b/gee-rpc/day6-discovery/codec/codec.go new file mode 100644 index 0000000..ba28fba --- /dev/null +++ b/gee-rpc/day6-discovery/codec/codec.go @@ -0,0 +1,34 @@ +package codec + +import ( + "io" +) + +type Header struct { + ServiceMethod string // format "Service.Method" + Seq uint64 // sequence number chosen by client + Error string +} + +type Codec interface { + io.Closer + ReadHeader(*Header) error + ReadBody(interface{}) error + Write(*Header, interface{}) error +} + +type NewCodecFunc func(io.ReadWriteCloser) Codec + +type Type string + +const ( + GobType Type = "application/gob" + JsonType Type = "application/json" +) + +var NewCodecFuncMap map[Type]NewCodecFunc + +func init() { + NewCodecFuncMap = make(map[Type]NewCodecFunc) + NewCodecFuncMap[GobType] = NewGobCodec +} diff --git a/gee-rpc/day6-discovery/codec/gob.go b/gee-rpc/day6-discovery/codec/gob.go new file mode 100644 index 0000000..808d97b --- /dev/null +++ b/gee-rpc/day6-discovery/codec/gob.go @@ -0,0 +1,57 @@ +package codec + +import ( + "bufio" + "encoding/gob" + "io" + "log" +) + +type GobCodec struct { + conn io.ReadWriteCloser + buf *bufio.Writer + dec *gob.Decoder + enc *gob.Encoder +} + +var _ Codec = (*GobCodec)(nil) + +func NewGobCodec(conn io.ReadWriteCloser) Codec { + buf := bufio.NewWriter(conn) + return &GobCodec{ + conn: conn, + buf: buf, + dec: gob.NewDecoder(conn), + enc: gob.NewEncoder(buf), + } +} + +func (c *GobCodec) ReadHeader(h *Header) error { + return c.dec.Decode(h) +} + +func (c *GobCodec) ReadBody(body interface{}) error { + return c.dec.Decode(body) +} + +func (c *GobCodec) Write(h *Header, body interface{}) (err error) { + defer func() { + _ = c.buf.Flush() + if err != nil { + _ = c.Close() + } + }() + if err := c.enc.Encode(h); err != nil { + log.Println("rpc: gob error encoding header:", err) + return err + } + if err := c.enc.Encode(body); err != nil { + log.Println("rpc: gob error encoding body:", err) + return err + } + return nil +} + +func (c *GobCodec) Close() error { + return c.conn.Close() +} diff --git a/gee-rpc/day6-discovery/debug.go b/gee-rpc/day6-discovery/debug.go new file mode 100644 index 0000000..ece1ffd --- /dev/null +++ b/gee-rpc/day6-discovery/debug.go @@ -0,0 +1,60 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package geerpc + +import ( + "fmt" + "html/template" + "net/http" +) + +const debugText = ` + + GeeRPC Services + {{range .}} +


+ Service {{.Name}} +
+ + + {{range $name, $mtype := .Method}} + + + + + {{end}} +
MethodCalls
{{$name}}({{$mtype.ArgType}}, {{$mtype.ReplyType}}) error{{$mtype.NumCalls}}
+ {{end}} + + ` + +var debug = template.Must(template.New("RPC debug").Parse(debugText)) + +type debugHTTP struct { + *Server +} + +type debugService struct { + Name string + Method map[string]*methodType +} + +// Runs at /debug/geerpc +func (server debugHTTP) ServeHTTP(w http.ResponseWriter, req *http.Request) { + // Build a sorted version of the data. + var services []debugService + server.serviceMap.Range(func(namei, svci interface{}) bool { + svc := svci.(*service) + services = append(services, debugService{ + Name: namei.(string), + Method: svc.method, + }) + return true + }) + err := debug.Execute(w, services) + if err != nil { + _, _ = fmt.Fprintln(w, "rpc: error executing template:", err.Error()) + } +} diff --git a/gee-rpc/day6-discovery/go.mod b/gee-rpc/day6-discovery/go.mod new file mode 100644 index 0000000..0ec8aeb --- /dev/null +++ b/gee-rpc/day6-discovery/go.mod @@ -0,0 +1,3 @@ +module geerpc + +go 1.13 diff --git a/gee-rpc/day6-discovery/main/main.go b/gee-rpc/day6-discovery/main/main.go new file mode 100644 index 0000000..a71af74 --- /dev/null +++ b/gee-rpc/day6-discovery/main/main.go @@ -0,0 +1,55 @@ +package main + +import ( + "context" + "geerpc" + "log" + "net" + "net/http" + "sync" +) + +type Foo int + +type Args struct{ Num1, Num2 int } + +func (f Foo) Sum(args Args, reply *int) error { + *reply = args.Num1 + args.Num2 + return nil +} + +func startServer(addrCh chan string) { + var foo Foo + l, _ := net.Listen("tcp", ":9999") + _ = geerpc.Register(&foo) + geerpc.HandleHTTP() + addrCh <- l.Addr().String() + _ = http.Serve(l, nil) +} + +func call(addrCh chan string) { + client, _ := geerpc.DialHTTP("tcp", <-addrCh) + defer func() { _ = client.Close() }() + + // send request & receive response + var wg sync.WaitGroup + for i := 0; i < 5; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + args := &Args{Num1: i, Num2: i * i} + var reply int + if err := client.Call(context.Background(), "Foo.Sum", args, &reply); err != nil { + log.Fatal("call Foo.Sum error:", err) + } + log.Printf("%d + %d = %d", args.Num1, args.Num2, reply) + }(i) + } + wg.Wait() +} + +func main() { + ch := make(chan string) + go call(ch) + startServer(ch) +} diff --git a/gee-rpc/day6-discovery/server.go b/gee-rpc/day6-discovery/server.go new file mode 100644 index 0000000..38fad20 --- /dev/null +++ b/gee-rpc/day6-discovery/server.go @@ -0,0 +1,266 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package geerpc + +import ( + "encoding/json" + "errors" + "fmt" + "geerpc/codec" + "io" + "log" + "net" + "net/http" + "reflect" + "strings" + "sync" + "time" +) + +const MagicNumber = 0x3bef5c + +type Option struct { + MagicNumber int // MagicNumber marks this's a geerpc request + CodecType codec.Type // client may choose different Codec to encode body + ConnectTimeout time.Duration // 0 means no limit + HandleTimeout time.Duration +} + +var DefaultOption = &Option{ + MagicNumber: MagicNumber, + CodecType: codec.GobType, + ConnectTimeout: time.Second * 10, +} + +// Server represents an RPC Server. +type Server struct { + serviceMap sync.Map +} + +// NewServer returns a new Server. +func NewServer() *Server { + return &Server{} +} + +// DefaultServer is the default instance of *Server. +var DefaultServer = NewServer() + +// ServeConn runs the server on a single connection. +// ServeConn blocks, serving the connection until the client hangs up. +func (server *Server) ServeConn(conn io.ReadWriteCloser) { + defer func() { _ = conn.Close() }() + var opt Option + if err := json.NewDecoder(conn).Decode(&opt); err != nil { + log.Println("rpc server: options error: ", err) + return + } + if opt.MagicNumber != MagicNumber { + log.Printf("rpc server: invalid magic number %x", opt.MagicNumber) + return + } + f := codec.NewCodecFuncMap[opt.CodecType] + if f == nil { + log.Printf("rpc server: invalid codec type %s", opt.CodecType) + return + } + server.serveCodec(f(conn), &opt) +} + +// invalidRequest is a placeholder for response argv when error occurs +var invalidRequest = struct{}{} + +func (server *Server) serveCodec(cc codec.Codec, opt *Option) { + sending := new(sync.Mutex) // make sure to send a complete response + wg := new(sync.WaitGroup) // wait until all request are handled + for { + req, err := server.readRequest(cc) + if err != nil { + if req == nil { + break // it's not possible to recover, so close the connection + } + req.h.Error = err.Error() + server.sendResponse(cc, req.h, invalidRequest, sending) + continue + } + wg.Add(1) + go server.handleRequest(cc, req, sending, wg, opt.HandleTimeout) + } + wg.Wait() + _ = cc.Close() +} + +// request stores all information of a call +type request struct { + h *codec.Header // header of request + argv, replyv reflect.Value // argv and replyv of request + mtype *methodType + svc *service +} + +func (server *Server) readRequestHeader(cc codec.Codec) (*codec.Header, error) { + var h codec.Header + if err := cc.ReadHeader(&h); err != nil { + if err != io.EOF && err != io.ErrUnexpectedEOF { + log.Println("rpc server: read header error:", err) + } + return nil, err + } + return &h, nil +} + +func (server *Server) findService(serviceMethod string) (svc *service, mtype *methodType, err error) { + dot := strings.LastIndex(serviceMethod, ".") + if dot < 0 { + err = errors.New("rpc server: service/method request ill-formed: " + serviceMethod) + return + } + serviceName, methodName := serviceMethod[:dot], serviceMethod[dot+1:] + svci, ok := server.serviceMap.Load(serviceName) + if !ok { + err = errors.New("rpc server: can't find service " + serviceName) + return + } + svc = svci.(*service) + mtype = svc.method[methodName] + if mtype == nil { + err = errors.New("rpc server: can't find method " + methodName) + } + return +} + +func (server *Server) readRequest(cc codec.Codec) (*request, error) { + h, err := server.readRequestHeader(cc) + if err != nil { + return nil, err + } + req := &request{h: h} + req.svc, req.mtype, err = server.findService(h.ServiceMethod) + if err != nil { + return req, err + } + req.argv = req.mtype.newArgv() + req.replyv = req.mtype.newReplyv() + + // make sure that argvi is a pointer, ReadBody need a pointer as parameter + argvi := req.argv.Interface() + if req.argv.Type().Kind() != reflect.Ptr { + argvi = req.argv.Addr().Interface() + } + if err = cc.ReadBody(argvi); err != nil { + log.Println("rpc server: read body err:", err) + return req, err + } + return req, nil +} + +func (server *Server) sendResponse(cc codec.Codec, h *codec.Header, body interface{}, sending *sync.Mutex) { + sending.Lock() + defer sending.Unlock() + if err := cc.Write(h, body); err != nil { + log.Println("rpc server: write response error:", err) + } +} + +func (server *Server) handleRequest(cc codec.Codec, req *request, sending *sync.Mutex, wg *sync.WaitGroup, timeout time.Duration) { + defer wg.Done() + called := make(chan struct{}) + sent := make(chan struct{}) + go func() { + err := req.svc.call(req.mtype, req.argv, req.replyv) + called <- struct{}{} + if err != nil { + req.h.Error = err.Error() + server.sendResponse(cc, req.h, invalidRequest, sending) + sent <- struct{}{} + return + } + server.sendResponse(cc, req.h, req.replyv.Interface(), sending) + sent <- struct{}{} + }() + + if timeout == 0 { + <-called + <-sent + return + } + select { + case <-time.After(timeout): + req.h.Error = fmt.Sprintf("rpc server: request handle timeout: expect within %s", timeout) + server.sendResponse(cc, req.h, invalidRequest, sending) + case <-called: + <-sent + } +} + +// Accept accepts connections on the listener and serves requests +// for each incoming connection. +func (server *Server) Accept(lis net.Listener) { + for { + conn, err := lis.Accept() + if err != nil { + log.Println("rpc server: accept error:", err) + return + } + go server.ServeConn(conn) + } +} + +// Accept accepts connections on the listener and serves requests +// for each incoming connection. +func Accept(lis net.Listener) { DefaultServer.Accept(lis) } + +// Register publishes in the server the set of methods of the +// receiver value that satisfy the following conditions: +// - exported method of exported type +// - two arguments, both of exported type +// - the second argument is a pointer +// - one return value, of type error +func (server *Server) Register(rcvr interface{}) error { + s := newService(rcvr) + if _, dup := server.serviceMap.LoadOrStore(s.name, s); dup { + return errors.New("rpc: service already defined: " + s.name) + } + return nil +} + +// Register publishes the receiver's methods in the DefaultServer. +func Register(rcvr interface{}) error { return DefaultServer.Register(rcvr) } + +const ( + connected = "200 Connected to Gee RPC" + defaultRPCPath = "/_geeprc_" + defaultDebugPath = "/debug/geerpc" +) + +// ServeHTTP implements an http.Handler that answers RPC requests. +func (server *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) { + if req.Method != "CONNECT" { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.WriteHeader(http.StatusMethodNotAllowed) + _, _ = io.WriteString(w, "405 must CONNECT\n") + return + } + conn, _, err := w.(http.Hijacker).Hijack() + if err != nil { + log.Print("rpc hijacking ", req.RemoteAddr, ": ", err.Error()) + return + } + _, _ = io.WriteString(conn, "HTTP/1.0 "+connected+"\n\n") + server.ServeConn(conn) +} + +// HandleHTTP registers an HTTP handler for RPC messages on rpcPath, +// and a debugging handler on debugPath. +// It is still necessary to invoke http.Serve(), typically in a go statement. +func (server *Server) HandleHTTP() { + http.Handle(defaultRPCPath, server) + http.Handle(defaultDebugPath, debugHTTP{server}) + log.Println("rpc server debug path:", defaultDebugPath) +} + +// HandleHTTP is a convenient approach for default server to register HTTP handlers +func HandleHTTP() { + DefaultServer.HandleHTTP() +} diff --git a/gee-rpc/day6-discovery/service.go b/gee-rpc/day6-discovery/service.go new file mode 100644 index 0000000..306683c --- /dev/null +++ b/gee-rpc/day6-discovery/service.go @@ -0,0 +1,99 @@ +package geerpc + +import ( + "go/ast" + "log" + "reflect" + "sync/atomic" +) + +type methodType struct { + method reflect.Method + ArgType reflect.Type + ReplyType reflect.Type + numCalls uint64 +} + +func (m *methodType) NumCalls() uint64 { + return atomic.LoadUint64(&m.numCalls) +} + +func (m *methodType) newArgv() reflect.Value { + var argv reflect.Value + // arg may be a pointer type, or a value type + if m.ArgType.Kind() == reflect.Ptr { + argv = reflect.New(m.ArgType.Elem()) + } else { + argv = reflect.New(m.ArgType).Elem() + } + return argv +} + +func (m *methodType) newReplyv() reflect.Value { + // reply must be a pointer type + replyv := reflect.New(m.ReplyType.Elem()) + switch m.ReplyType.Elem().Kind() { + case reflect.Map: + replyv.Elem().Set(reflect.MakeMap(m.ReplyType.Elem())) + case reflect.Slice: + replyv.Elem().Set(reflect.MakeSlice(m.ReplyType.Elem(), 0, 0)) + } + return replyv +} + +type service struct { + name string + typ reflect.Type + rcvr reflect.Value + method map[string]*methodType +} + +func newService(rcvr interface{}) *service { + s := new(service) + s.rcvr = reflect.ValueOf(rcvr) + s.name = reflect.Indirect(s.rcvr).Type().Name() + s.typ = reflect.TypeOf(rcvr) + if !ast.IsExported(s.name) { + log.Fatalf("rpc server: %s is not a valid service name", s.name) + } + s.registerMethods() + return s +} + +func (s *service) registerMethods() { + s.method = make(map[string]*methodType) + for i := 0; i < s.typ.NumMethod(); i++ { + method := s.typ.Method(i) + mType := method.Type + if mType.NumIn() != 3 || mType.NumOut() != 1 { + continue + } + if mType.Out(0) != reflect.TypeOf((*error)(nil)).Elem() { + continue + } + argType, replyType := mType.In(1), mType.In(2) + if !isExportedOrBuiltinType(argType) || !isExportedOrBuiltinType(replyType) { + continue + } + s.method[method.Name] = &methodType{ + method: method, + ArgType: argType, + ReplyType: replyType, + } + log.Printf("rpc server: register %s.%s\n", s.name, method.Name) + } +} + +func (s *service) call(m *methodType, argv, replyv reflect.Value) error { + atomic.AddUint64(&m.numCalls, 1) + f := m.method.Func + returnValues := f.Call([]reflect.Value{s.rcvr, argv, replyv}) + if errInter := returnValues[0].Interface(); errInter != nil { + return errInter.(error) + } + return nil +} + +func isExportedOrBuiltinType(t reflect.Type) bool { + return ast.IsExported(t.Name()) || t.PkgPath() == "" +} diff --git a/gee-rpc/day6-discovery/service_test.go b/gee-rpc/day6-discovery/service_test.go new file mode 100644 index 0000000..c8266df --- /dev/null +++ b/gee-rpc/day6-discovery/service_test.go @@ -0,0 +1,48 @@ +package geerpc + +import ( + "fmt" + "reflect" + "testing" +) + +type Foo int + +type Args struct{ Num1, Num2 int } + +func (f Foo) Sum(args Args, reply *int) error { + *reply = args.Num1 + args.Num2 + return nil +} + +// it's not a exported Method +func (f Foo) sum(args Args, reply *int) error { + *reply = args.Num1 + args.Num2 + return nil +} + +func _assert(condition bool, msg string, v ...interface{}) { + if !condition { + panic(fmt.Sprintf("assertion failed: "+msg, v...)) + } +} + +func TestNewService(t *testing.T) { + var foo Foo + s := newService(&foo) + _assert(len(s.method) == 1, "wrong service Method, expect 1, but got %d", len(s.method)) + mType := s.method["Sum"] + _assert(mType != nil, "wrong Method, Sum shouldn't nil") +} + +func TestMethodType_Call(t *testing.T) { + var foo Foo + s := newService(&foo) + mType := s.method["Sum"] + + argv := mType.newArgv() + replyv := mType.newReplyv() + argv.Set(reflect.ValueOf(Args{Num1: 1, Num2: 3})) + err := s.call(mType, argv, replyv) + _assert(err == nil && *replyv.Interface().(*int) == 4 && mType.NumCalls() == 1, "failed to call Foo.Sum") +} diff --git a/gee-rpc/day6-discovery/xclient/discovery.go b/gee-rpc/day6-discovery/xclient/discovery.go new file mode 100644 index 0000000..98b58d6 --- /dev/null +++ b/gee-rpc/day6-discovery/xclient/discovery.go @@ -0,0 +1,57 @@ +package xclient + +import ( + "math/rand" + "sync" + "time" +) + +type SelectMode int + +const ( + RandomSelect SelectMode = iota // select randomly + RobbinSelect // select using Robbin algorithm +) + +type Discovery interface { + Get(mode SelectMode) string +} + +var _ Discovery = (*MultiServersDiscovery)(nil) + +// MultiServersDiscovery is a discovery for multi servers without a registry center +// user provides the server addresses explicitly instead +type MultiServersDiscovery struct { + r *rand.Rand // generate random number + mu sync.RWMutex // protect following + servers []string +} + +// Update the servers of discovery dynamically if needed +func (d *MultiServersDiscovery) Update(servers []string) { + d.mu.Lock() + defer d.mu.Unlock() + d.servers = servers +} + +func (d *MultiServersDiscovery) Get(mode SelectMode) string { + d.mu.RLock() + defer d.mu.RUnlock() + if len(d.servers) == 0 { + return "" + } + switch mode { + case RandomSelect: + return d.servers[d.r.Intn(len(d.servers))] + default: + return "" + } +} + +// NewMultiServerDiscovery creates a MultiServersDiscovery instance +func NewMultiServerDiscovery(servers []string) *MultiServersDiscovery { + return &MultiServersDiscovery{ + servers: servers, + r: rand.New(rand.NewSource(time.Now().UnixNano())), + } +} diff --git a/gee-rpc/day6-discovery/xclient/xclient.go b/gee-rpc/day6-discovery/xclient/xclient.go new file mode 100644 index 0000000..ff6992f --- /dev/null +++ b/gee-rpc/day6-discovery/xclient/xclient.go @@ -0,0 +1,48 @@ +package xclient + +import ( + "context" + . "geerpc" + "io" + "sync" +) + +type XClient struct { + d Discovery + mode SelectMode + opt *Option + clients sync.Map +} + +var _ io.Closer = (*XClient)(nil) + +func NewXClient(d Discovery, mode SelectMode, opt *Option) *XClient { + return &XClient{d: d, mode: mode, opt: opt} +} + +func (xc *XClient) Close() error { + xc.clients.Range(func(k, v interface{}) bool { + // I have no idea how to deal with error, just ignore it. + _ = v.(*Client).Close() + return true + }) + xc.clients = sync.Map{} + return nil +} + +// Call invokes the named function, waits for it to complete, +// and returns its error status. +// xc will choose a proper server. +func (xc *XClient) Call(ctx context.Context, serviceMethod string, args, reply interface{}) error { + rpcAddr := xc.d.Get(xc.mode) + client, ok := xc.clients.Load(rpcAddr) + if !ok { + var err error + client, err = XDial(rpcAddr, xc.opt) + if err != nil { + return err + } + xc.clients.Store(rpcAddr, client) + } + return client.(*Client).Call(ctx, serviceMethod, args, reply) +} From 7cd0399e24af59a09cab4bca804dee5087a55679 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Mon, 5 Oct 2020 02:16:39 +0800 Subject: [PATCH 082/122] gee-rpc day6 add discovery --- gee-rpc/day1-codec/codec/codec.go | 2 +- gee-rpc/day2-client/codec/codec.go | 2 +- gee-rpc/day3-service/codec/codec.go | 2 +- gee-rpc/day4-timeout/codec/codec.go | 2 +- gee-rpc/day5-http-debug/codec/codec.go | 2 +- gee-rpc/day6-discovery/client.go | 4 +- gee-rpc/day6-discovery/codec/codec.go | 2 +- gee-rpc/day6-discovery/main/main.go | 76 ++++++++++++++---- gee-rpc/day6-discovery/xclient/discovery.go | 12 ++- gee-rpc/day6-discovery/xclient/xclient.go | 87 +++++++++++++++++---- 10 files changed, 149 insertions(+), 42 deletions(-) diff --git a/gee-rpc/day1-codec/codec/codec.go b/gee-rpc/day1-codec/codec/codec.go index ba28fba..20b6ba7 100644 --- a/gee-rpc/day1-codec/codec/codec.go +++ b/gee-rpc/day1-codec/codec/codec.go @@ -23,7 +23,7 @@ type Type string const ( GobType Type = "application/gob" - JsonType Type = "application/json" + JsonType Type = "application/json" // not implemented ) var NewCodecFuncMap map[Type]NewCodecFunc diff --git a/gee-rpc/day2-client/codec/codec.go b/gee-rpc/day2-client/codec/codec.go index ba28fba..20b6ba7 100644 --- a/gee-rpc/day2-client/codec/codec.go +++ b/gee-rpc/day2-client/codec/codec.go @@ -23,7 +23,7 @@ type Type string const ( GobType Type = "application/gob" - JsonType Type = "application/json" + JsonType Type = "application/json" // not implemented ) var NewCodecFuncMap map[Type]NewCodecFunc diff --git a/gee-rpc/day3-service/codec/codec.go b/gee-rpc/day3-service/codec/codec.go index ba28fba..20b6ba7 100644 --- a/gee-rpc/day3-service/codec/codec.go +++ b/gee-rpc/day3-service/codec/codec.go @@ -23,7 +23,7 @@ type Type string const ( GobType Type = "application/gob" - JsonType Type = "application/json" + JsonType Type = "application/json" // not implemented ) var NewCodecFuncMap map[Type]NewCodecFunc diff --git a/gee-rpc/day4-timeout/codec/codec.go b/gee-rpc/day4-timeout/codec/codec.go index ba28fba..20b6ba7 100644 --- a/gee-rpc/day4-timeout/codec/codec.go +++ b/gee-rpc/day4-timeout/codec/codec.go @@ -23,7 +23,7 @@ type Type string const ( GobType Type = "application/gob" - JsonType Type = "application/json" + JsonType Type = "application/json" // not implemented ) var NewCodecFuncMap map[Type]NewCodecFunc diff --git a/gee-rpc/day5-http-debug/codec/codec.go b/gee-rpc/day5-http-debug/codec/codec.go index ba28fba..20b6ba7 100644 --- a/gee-rpc/day5-http-debug/codec/codec.go +++ b/gee-rpc/day5-http-debug/codec/codec.go @@ -23,7 +23,7 @@ type Type string const ( GobType Type = "application/gob" - JsonType Type = "application/json" + JsonType Type = "application/json" // not implemented ) var NewCodecFuncMap map[Type]NewCodecFunc diff --git a/gee-rpc/day6-discovery/client.go b/gee-rpc/day6-discovery/client.go index d958696..e9b4540 100644 --- a/gee-rpc/day6-discovery/client.go +++ b/gee-rpc/day6-discovery/client.go @@ -316,9 +316,9 @@ func XDial(rpcAddr string, opts ...*Option) (*Client, error) { protocol, addr := parts[0], parts[1] switch protocol { case "http": - return DialHTTP("tcp", addr) + return DialHTTP("tcp", addr, opts...) default: // tcp, unix or other transport protocol - return Dial(protocol, addr) + return Dial(protocol, addr, opts...) } } diff --git a/gee-rpc/day6-discovery/codec/codec.go b/gee-rpc/day6-discovery/codec/codec.go index ba28fba..20b6ba7 100644 --- a/gee-rpc/day6-discovery/codec/codec.go +++ b/gee-rpc/day6-discovery/codec/codec.go @@ -23,7 +23,7 @@ type Type string const ( GobType Type = "application/gob" - JsonType Type = "application/json" + JsonType Type = "application/json" // not implemented ) var NewCodecFuncMap map[Type]NewCodecFunc diff --git a/gee-rpc/day6-discovery/main/main.go b/gee-rpc/day6-discovery/main/main.go index a71af74..308da5c 100644 --- a/gee-rpc/day6-discovery/main/main.go +++ b/gee-rpc/day6-discovery/main/main.go @@ -3,10 +3,11 @@ package main import ( "context" "geerpc" + "geerpc/xclient" "log" "net" - "net/http" "sync" + "time" ) type Foo int @@ -18,38 +19,79 @@ func (f Foo) Sum(args Args, reply *int) error { return nil } +func (f Foo) Sleep(args Args, reply *int) error { + time.Sleep(time.Second * time.Duration(args.Num1)) + *reply = args.Num1 + args.Num2 + return nil +} + func startServer(addrCh chan string) { var foo Foo - l, _ := net.Listen("tcp", ":9999") - _ = geerpc.Register(&foo) - geerpc.HandleHTTP() + l, _ := net.Listen("tcp", ":0") + server := geerpc.NewServer() + _ = server.Register(&foo) addrCh <- l.Addr().String() - _ = http.Serve(l, nil) + server.Accept(l) } -func call(addrCh chan string) { - client, _ := geerpc.DialHTTP("tcp", <-addrCh) - defer func() { _ = client.Close() }() +func foo(xc *xclient.XClient, ctx context.Context, typ, serviceMethod string, args *Args) { + var reply int + var err error + switch typ { + case "call": + err = xc.Call(ctx, serviceMethod, args, &reply) + case "broadcast": + err = xc.Broadcast(ctx, serviceMethod, args, &reply) + } + if err != nil { + log.Printf("%s %s error: %v", typ, serviceMethod, err) + } else { + log.Printf("%s Foo.Sum success: %d + %d = %d", typ, args.Num1, args.Num2, reply) + } +} +func call(addr1, addr2 string) { + d := xclient.NewMultiServerDiscovery([]string{"tcp@" + addr1, "tcp@" + addr2}) + xc := xclient.NewXClient(d, xclient.RandomSelect, nil) + defer func() { _ = xc.Close() }() // send request & receive response var wg sync.WaitGroup for i := 0; i < 5; i++ { wg.Add(1) go func(i int) { defer wg.Done() - args := &Args{Num1: i, Num2: i * i} - var reply int - if err := client.Call(context.Background(), "Foo.Sum", args, &reply); err != nil { - log.Fatal("call Foo.Sum error:", err) - } - log.Printf("%d + %d = %d", args.Num1, args.Num2, reply) + foo(xc, context.Background(), "call", "Foo.Sum", &Args{Num1: i, Num2: i * i}) + }(i) + } + wg.Wait() +} + +func broadcast(addr1, addr2 string) { + d := xclient.NewMultiServerDiscovery([]string{"tcp@" + addr1, "tcp@" + addr2}) + xc := xclient.NewXClient(d, xclient.RandomSelect, nil) + var wg sync.WaitGroup + for i := 0; i < 5; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + foo(xc, context.Background(), "broadcast", "Foo.Sum", &Args{Num1: i, Num2: i * i}) + // expect 2 - 5 timeout + ctx, _ := context.WithTimeout(context.Background(), time.Second*2) + foo(xc, ctx, "broadcast", "Foo.Sleep", &Args{Num1: i, Num2: i * i}) }(i) } wg.Wait() } func main() { - ch := make(chan string) - go call(ch) - startServer(ch) + ch1 := make(chan string) + ch2 := make(chan string) + // start two servers + go startServer(ch1) + go startServer(ch2) + + addr1 := <-ch1 + addr2 := <-ch2 + call(addr1, addr2) + broadcast(addr1, addr2) } diff --git a/gee-rpc/day6-discovery/xclient/discovery.go b/gee-rpc/day6-discovery/xclient/discovery.go index 98b58d6..7e52e3f 100644 --- a/gee-rpc/day6-discovery/xclient/discovery.go +++ b/gee-rpc/day6-discovery/xclient/discovery.go @@ -10,11 +10,12 @@ type SelectMode int const ( RandomSelect SelectMode = iota // select randomly - RobbinSelect // select using Robbin algorithm + RobbinSelect // select using Robbin algorithm, not implemented ) type Discovery interface { Get(mode SelectMode) string + All() []string } var _ Discovery = (*MultiServersDiscovery)(nil) @@ -48,6 +49,15 @@ func (d *MultiServersDiscovery) Get(mode SelectMode) string { } } +func (d *MultiServersDiscovery) All() []string { + d.mu.RLock() + defer d.mu.RUnlock() + // return a copy of d.servers + servers := make([]string, len(d.servers), len(d.servers)) + copy(servers, d.servers) + return servers +} + // NewMultiServerDiscovery creates a MultiServersDiscovery instance func NewMultiServerDiscovery(servers []string) *MultiServersDiscovery { return &MultiServersDiscovery{ diff --git a/gee-rpc/day6-discovery/xclient/xclient.go b/gee-rpc/day6-discovery/xclient/xclient.go index ff6992f..f3df99c 100644 --- a/gee-rpc/day6-discovery/xclient/xclient.go +++ b/gee-rpc/day6-discovery/xclient/xclient.go @@ -4,6 +4,7 @@ import ( "context" . "geerpc" "io" + "reflect" "sync" ) @@ -11,38 +12,92 @@ type XClient struct { d Discovery mode SelectMode opt *Option - clients sync.Map + mu sync.Mutex // protect following + clients map[string]*Client } var _ io.Closer = (*XClient)(nil) func NewXClient(d Discovery, mode SelectMode, opt *Option) *XClient { - return &XClient{d: d, mode: mode, opt: opt} + return &XClient{d: d, mode: mode, opt: opt, clients: make(map[string]*Client)} } func (xc *XClient) Close() error { - xc.clients.Range(func(k, v interface{}) bool { + xc.mu.Lock() + defer xc.mu.Unlock() + for key, client := range xc.clients { // I have no idea how to deal with error, just ignore it. - _ = v.(*Client).Close() - return true - }) - xc.clients = sync.Map{} + _ = client.Close() + delete(xc.clients, key) + } return nil } +func (xc *XClient) dial(rpcAddr string) (*Client, error) { + xc.mu.Lock() + defer xc.mu.Unlock() + client, ok := xc.clients[rpcAddr] + if ok && !client.IsAvailable() { + _ = client.Close() + delete(xc.clients, rpcAddr) + client = nil + } + if client == nil { + var err error + client, err = XDial(rpcAddr, xc.opt) + if err != nil { + return nil, err + } + xc.clients[rpcAddr] = client + } + return client, nil +} + +func (xc *XClient) call(rpcAddr string, ctx context.Context, serviceMethod string, args, reply interface{}) error { + client, err := xc.dial(rpcAddr) + if err != nil { + return err + } + return client.Call(ctx, serviceMethod, args, reply) +} + // Call invokes the named function, waits for it to complete, // and returns its error status. // xc will choose a proper server. func (xc *XClient) Call(ctx context.Context, serviceMethod string, args, reply interface{}) error { rpcAddr := xc.d.Get(xc.mode) - client, ok := xc.clients.Load(rpcAddr) - if !ok { - var err error - client, err = XDial(rpcAddr, xc.opt) - if err != nil { - return err - } - xc.clients.Store(rpcAddr, client) + return xc.call(rpcAddr, ctx, serviceMethod, args, reply) +} + +// Broadcast invokes the named function for every server registered in discovery +func (xc *XClient) Broadcast(ctx context.Context, serviceMethod string, args, reply interface{}) error { + servers := xc.d.All() + var wg sync.WaitGroup + var mu sync.Mutex + var e error + replyDone := reply == nil // if reply is nil, don't need to set value + ctx, cancel := context.WithCancel(ctx) + for _, rpcAddr := range servers { + wg.Add(1) + go func() { + defer wg.Done() + var clonedReply interface{} + if reply != nil { + clonedReply = reflect.New(reflect.ValueOf(reply).Elem().Type()).Interface() + } + err := xc.call(rpcAddr, ctx, serviceMethod, args, clonedReply) + mu.Lock() + if err != nil && e == nil { + e = err + cancel() // if any call failed, cancel unfinished calls + } + if err == nil && !replyDone { + reflect.ValueOf(reply).Elem().Set(reflect.ValueOf(clonedReply).Elem()) + replyDone = true + } + mu.Unlock() + }() } - return client.(*Client).Call(ctx, serviceMethod, args, reply) + wg.Wait() + return e } From 366f09aa6b0bf1095a9d7c7aa380276ed403c7cf Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Mon, 5 Oct 2020 11:48:48 +0800 Subject: [PATCH 083/122] gee-rpc day6 add robin select for discovery --- gee-rpc/day6-discovery/xclient/discovery.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/gee-rpc/day6-discovery/xclient/discovery.go b/gee-rpc/day6-discovery/xclient/discovery.go index 7e52e3f..d00db3d 100644 --- a/gee-rpc/day6-discovery/xclient/discovery.go +++ b/gee-rpc/day6-discovery/xclient/discovery.go @@ -10,7 +10,7 @@ type SelectMode int const ( RandomSelect SelectMode = iota // select randomly - RobbinSelect // select using Robbin algorithm, not implemented + RobinSelect // select using Robbin algorithm ) type Discovery interface { @@ -26,6 +26,7 @@ type MultiServersDiscovery struct { r *rand.Rand // generate random number mu sync.RWMutex // protect following servers []string + index int // record the selected position for robin algorithm } // Update the servers of discovery dynamically if needed @@ -36,14 +37,18 @@ func (d *MultiServersDiscovery) Update(servers []string) { } func (d *MultiServersDiscovery) Get(mode SelectMode) string { - d.mu.RLock() - defer d.mu.RUnlock() + d.mu.Lock() + defer d.mu.Unlock() if len(d.servers) == 0 { return "" } switch mode { case RandomSelect: return d.servers[d.r.Intn(len(d.servers))] + case RobinSelect: + s := d.servers[d.index] + d.index = (d.index + 1) % len(d.servers) + return s default: return "" } From 5a84e23349ee7f95c12be005cf4f2588f06bf550 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Mon, 5 Oct 2020 12:01:04 +0800 Subject: [PATCH 084/122] gee-rpc/day6: rename Robin to RoundRobin --- gee-rpc/day6-discovery/xclient/discovery.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gee-rpc/day6-discovery/xclient/discovery.go b/gee-rpc/day6-discovery/xclient/discovery.go index d00db3d..b473b5d 100644 --- a/gee-rpc/day6-discovery/xclient/discovery.go +++ b/gee-rpc/day6-discovery/xclient/discovery.go @@ -9,8 +9,8 @@ import ( type SelectMode int const ( - RandomSelect SelectMode = iota // select randomly - RobinSelect // select using Robbin algorithm + RandomSelect SelectMode = iota // select randomly + RoundRobinSelect // select using Robbin algorithm ) type Discovery interface { @@ -45,7 +45,7 @@ func (d *MultiServersDiscovery) Get(mode SelectMode) string { switch mode { case RandomSelect: return d.servers[d.r.Intn(len(d.servers))] - case RobinSelect: + case RoundRobinSelect: s := d.servers[d.index] d.index = (d.index + 1) % len(d.servers) return s From 78789790bdc73851103995f29f18c4e966cbf60d Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Mon, 5 Oct 2020 18:07:16 +0800 Subject: [PATCH 085/122] gee-rpc/day7 add registry --- README.md | 28 ++ gee-rpc/day1-codec/main/main.go | 2 + gee-rpc/day2-client/main/main.go | 2 + gee-rpc/day3-service/main/main.go | 2 + gee-rpc/day4-timeout/main/main.go | 2 + gee-rpc/day5-http-debug/main/main.go | 2 + .../client.go | 0 .../client_test.go | 0 .../codec/codec.go | 0 .../codec/gob.go | 0 .../debug.go | 0 .../go.mod | 0 .../main/main.go | 2 + .../server.go | 0 .../service.go | 0 .../service_test.go | 0 .../xclient/discovery.go | 31 +- .../xclient/xclient.go | 12 +- gee-rpc/day7-registry/client.go | 324 ++++++++++++++++++ gee-rpc/day7-registry/client_test.go | 84 +++++ gee-rpc/day7-registry/codec/codec.go | 34 ++ gee-rpc/day7-registry/codec/gob.go | 57 +++ gee-rpc/day7-registry/debug.go | 60 ++++ gee-rpc/day7-registry/go.mod | 3 + gee-rpc/day7-registry/main/main.go | 112 ++++++ gee-rpc/day7-registry/registry/registry.go | 123 +++++++ gee-rpc/day7-registry/server.go | 266 ++++++++++++++ gee-rpc/day7-registry/service.go | 99 ++++++ gee-rpc/day7-registry/service_test.go | 48 +++ gee-rpc/day7-registry/xclient/discovery.go | 83 +++++ .../day7-registry/xclient/discovery_gee.go | 74 ++++ gee-rpc/day7-registry/xclient/xclient.go | 109 ++++++ 32 files changed, 1546 insertions(+), 13 deletions(-) rename gee-rpc/{day6-discovery => day6-load-balance}/client.go (100%) rename gee-rpc/{day6-discovery => day6-load-balance}/client_test.go (100%) rename gee-rpc/{day6-discovery => day6-load-balance}/codec/codec.go (100%) rename gee-rpc/{day6-discovery => day6-load-balance}/codec/gob.go (100%) rename gee-rpc/{day6-discovery => day6-load-balance}/debug.go (100%) rename gee-rpc/{day6-discovery => day6-load-balance}/go.mod (100%) rename gee-rpc/{day6-discovery => day6-load-balance}/main/main.go (98%) rename gee-rpc/{day6-discovery => day6-load-balance}/server.go (100%) rename gee-rpc/{day6-discovery => day6-load-balance}/service.go (100%) rename gee-rpc/{day6-discovery => day6-load-balance}/service_test.go (100%) rename gee-rpc/{day6-discovery => day6-load-balance}/xclient/discovery.go (63%) rename gee-rpc/{day6-discovery => day6-load-balance}/xclient/xclient.go (93%) create mode 100644 gee-rpc/day7-registry/client.go create mode 100644 gee-rpc/day7-registry/client_test.go create mode 100644 gee-rpc/day7-registry/codec/codec.go create mode 100644 gee-rpc/day7-registry/codec/gob.go create mode 100644 gee-rpc/day7-registry/debug.go create mode 100644 gee-rpc/day7-registry/go.mod create mode 100644 gee-rpc/day7-registry/main/main.go create mode 100644 gee-rpc/day7-registry/registry/registry.go create mode 100644 gee-rpc/day7-registry/server.go create mode 100644 gee-rpc/day7-registry/service.go create mode 100644 gee-rpc/day7-registry/service_test.go create mode 100644 gee-rpc/day7-registry/xclient/discovery.go create mode 100644 gee-rpc/day7-registry/xclient/discovery_gee.go create mode 100644 gee-rpc/day7-registry/xclient/xclient.go diff --git a/README.md b/README.md index 37d366d..c2d9d02 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ 推荐先阅读 **[Go 语言简明教程](https://geektutu.com/post/quick-golang.html)**,一篇文章了解Go的基本语法、并发编程,依赖管理等内容。 +另外推荐 **[Go 语言笔试面试题](https://geektutu.com/post/qa-golang.html)**,加深对 Go 语言的理解。 + 期待关注我的「[知乎专栏](https://zhuanlan.zhihu.com/geekgo)」和「[微博](http://weibo.com/geektutu)」,查看最近的文章和动态。 ### 7天用Go从零实现Web框架 - Gee @@ -50,6 +52,20 @@ gorm 准备推出完全重写的 v2 版本(目前还在开发中),相对 gorm- - 第六天:[支持事务(Transaction)](https://geektutu.com/post/geeorm-day6.html) | [Code](gee-orm/day6-transaction) - 第七天:[数据库迁移(Migrate)](https://geektutu.com/post/geeorm-day7.html) | [Code](gee-orm/day7-migrate) + +### 7天用Go从零实现RPC框架 GeeRPC + +[GeeRPC](https://geektutu.com/post/geerpc.html) 是一个基于 [net/rpc](https://github.com/golang/go/tree/master/src/net/rpc) 开发的 RPC 框架 +GeeRPC 是基于 Go 语言标准库 `net/rpc` 实现的,添加了协议交换、服务注册与发现、负载均衡等功能,代码约 1k。 + +- 第一天 - [服务端与消息编码](https://geektutu.com/post/geerpc-day1.html) | [Code](gee-rpc/day1-codec) +- 第二天 - [支持并发与异步的客户端](https://geektutu.com/post/geerpc-day2.html) | [Code](gee-rpc/day2-client) +- 第三天 - [服务注册(service register)](https://geektutu.com/post/geerpc-day3.html) | [Code](gee-rpc/day3-service ) +- 第四天 - [超时处理(timeout)](https://geektutu.com/post/geerpc-day4.html) | [Code](gee-rpc/day4-timeout ) +- 第五天 - [支持HTTP协议](https://geektutu.com/post/geerpc-day5.html) | [Code](gee-rpc/day5-http-debug) +- 第六天 - [负载均衡(load balance)](https://geektutu.com/post/geerpc-day6.html) | [Code](gee-rpc/day6-load-balance) +- 第七天 - [服务发现与注册中心(registry)](https://geektutu.com/post/geerpc-day7.html) | [Code](gee-rpc/day7-registry) + ### WebAssembly 使用示例 具体的实践过程记录在 [Go WebAssembly 简明教程](https://geektutu.com/post/quick-go-wasm.html)。 @@ -102,6 +118,18 @@ Xorm's desgin is easier to understand than gorm-v1, so the main designs referenc - Day 6 - Support Transaction | [Code](gee-orm/day6-transaction) - Day 7 - Migrate Database | [Code](gee-orm/day7-migrate) +[GeeRPC](https://geektutu.com/post/geerpc.html) is a [net/rpc](https://github.com/golang/go/tree/master/src/net/rpc)-like RPC framework + +Based on golang standard library `net/rpc`, GeeRPC implements more features. eg, protocol exchange, service registration and discovery, load balance, etc. + +- Day 1 - Server Message Codec | [Code](gee-rpc/day1-codec) +- Day 2 - Concurrent Client | [Code](gee-rpc/day2-client) +- Day 3 - Service Register | [Code](gee-rpc/day3-service ) +- Day 4 - Timeout Processing | [Code](gee-rpc/day4-timeout ) +- Day 5 - Support HTTP Protocol | [Code](gee-rpc/day5-http-debug) +- Day 6 - Load Balance | [Code](gee-rpc/day6-load-balance) +- Day 7 - Discovery and Registry | [Code](gee-rpc/day7-registry) + ## Golang WebAssembly Demo - Demo 1 - Hello World [Code](demo-wasm/hello-world) diff --git a/gee-rpc/day1-codec/main/main.go b/gee-rpc/day1-codec/main/main.go index 8f29531..948b763 100644 --- a/gee-rpc/day1-codec/main/main.go +++ b/gee-rpc/day1-codec/main/main.go @@ -7,6 +7,7 @@ import ( "geerpc/codec" "log" "net" + "time" ) func startServer(addr chan string) { @@ -28,6 +29,7 @@ func main() { conn, _ := net.Dial("tcp", <-addr) defer func() { _ = conn.Close() }() + time.Sleep(time.Second) // send options _ = json.NewEncoder(conn).Encode(geerpc.DefaultOption) cc := codec.NewGobCodec(conn) diff --git a/gee-rpc/day2-client/main/main.go b/gee-rpc/day2-client/main/main.go index e291353..8502fe9 100644 --- a/gee-rpc/day2-client/main/main.go +++ b/gee-rpc/day2-client/main/main.go @@ -6,6 +6,7 @@ import ( "log" "net" "sync" + "time" ) func startServer(addr chan string) { @@ -25,6 +26,7 @@ func main() { client, _ := geerpc.Dial("tcp", <-addr) defer func() { _ = client.Close() }() + time.Sleep(time.Second) // send request & receive response var wg sync.WaitGroup for i := 0; i < 5; i++ { diff --git a/gee-rpc/day3-service/main/main.go b/gee-rpc/day3-service/main/main.go index c526e11..0f0b668 100644 --- a/gee-rpc/day3-service/main/main.go +++ b/gee-rpc/day3-service/main/main.go @@ -5,6 +5,7 @@ import ( "log" "net" "sync" + "time" ) type Foo int @@ -37,6 +38,7 @@ func main() { client, _ := geerpc.Dial("tcp", <-addr) defer func() { _ = client.Close() }() + time.Sleep(time.Second) // send request & receive response var wg sync.WaitGroup for i := 0; i < 5; i++ { diff --git a/gee-rpc/day4-timeout/main/main.go b/gee-rpc/day4-timeout/main/main.go index e5e6050..9693eb5 100644 --- a/gee-rpc/day4-timeout/main/main.go +++ b/gee-rpc/day4-timeout/main/main.go @@ -6,6 +6,7 @@ import ( "log" "net" "sync" + "time" ) type Foo int @@ -38,6 +39,7 @@ func main() { client, _ := geerpc.Dial("tcp", <-addr) defer func() { _ = client.Close() }() + time.Sleep(time.Second) // send request & receive response var wg sync.WaitGroup for i := 0; i < 5; i++ { diff --git a/gee-rpc/day5-http-debug/main/main.go b/gee-rpc/day5-http-debug/main/main.go index a71af74..6499b53 100644 --- a/gee-rpc/day5-http-debug/main/main.go +++ b/gee-rpc/day5-http-debug/main/main.go @@ -7,6 +7,7 @@ import ( "net" "net/http" "sync" + "time" ) type Foo int @@ -31,6 +32,7 @@ func call(addrCh chan string) { client, _ := geerpc.DialHTTP("tcp", <-addrCh) defer func() { _ = client.Close() }() + time.Sleep(time.Second) // send request & receive response var wg sync.WaitGroup for i := 0; i < 5; i++ { diff --git a/gee-rpc/day6-discovery/client.go b/gee-rpc/day6-load-balance/client.go similarity index 100% rename from gee-rpc/day6-discovery/client.go rename to gee-rpc/day6-load-balance/client.go diff --git a/gee-rpc/day6-discovery/client_test.go b/gee-rpc/day6-load-balance/client_test.go similarity index 100% rename from gee-rpc/day6-discovery/client_test.go rename to gee-rpc/day6-load-balance/client_test.go diff --git a/gee-rpc/day6-discovery/codec/codec.go b/gee-rpc/day6-load-balance/codec/codec.go similarity index 100% rename from gee-rpc/day6-discovery/codec/codec.go rename to gee-rpc/day6-load-balance/codec/codec.go diff --git a/gee-rpc/day6-discovery/codec/gob.go b/gee-rpc/day6-load-balance/codec/gob.go similarity index 100% rename from gee-rpc/day6-discovery/codec/gob.go rename to gee-rpc/day6-load-balance/codec/gob.go diff --git a/gee-rpc/day6-discovery/debug.go b/gee-rpc/day6-load-balance/debug.go similarity index 100% rename from gee-rpc/day6-discovery/debug.go rename to gee-rpc/day6-load-balance/debug.go diff --git a/gee-rpc/day6-discovery/go.mod b/gee-rpc/day6-load-balance/go.mod similarity index 100% rename from gee-rpc/day6-discovery/go.mod rename to gee-rpc/day6-load-balance/go.mod diff --git a/gee-rpc/day6-discovery/main/main.go b/gee-rpc/day6-load-balance/main/main.go similarity index 98% rename from gee-rpc/day6-discovery/main/main.go rename to gee-rpc/day6-load-balance/main/main.go index 308da5c..d00f864 100644 --- a/gee-rpc/day6-discovery/main/main.go +++ b/gee-rpc/day6-load-balance/main/main.go @@ -92,6 +92,8 @@ func main() { addr1 := <-ch1 addr2 := <-ch2 + + time.Sleep(time.Second) call(addr1, addr2) broadcast(addr1, addr2) } diff --git a/gee-rpc/day6-discovery/server.go b/gee-rpc/day6-load-balance/server.go similarity index 100% rename from gee-rpc/day6-discovery/server.go rename to gee-rpc/day6-load-balance/server.go diff --git a/gee-rpc/day6-discovery/service.go b/gee-rpc/day6-load-balance/service.go similarity index 100% rename from gee-rpc/day6-discovery/service.go rename to gee-rpc/day6-load-balance/service.go diff --git a/gee-rpc/day6-discovery/service_test.go b/gee-rpc/day6-load-balance/service_test.go similarity index 100% rename from gee-rpc/day6-discovery/service_test.go rename to gee-rpc/day6-load-balance/service_test.go diff --git a/gee-rpc/day6-discovery/xclient/discovery.go b/gee-rpc/day6-load-balance/xclient/discovery.go similarity index 63% rename from gee-rpc/day6-discovery/xclient/discovery.go rename to gee-rpc/day6-load-balance/xclient/discovery.go index b473b5d..f823a7b 100644 --- a/gee-rpc/day6-discovery/xclient/discovery.go +++ b/gee-rpc/day6-load-balance/xclient/discovery.go @@ -1,6 +1,7 @@ package xclient import ( + "errors" "math/rand" "sync" "time" @@ -14,8 +15,10 @@ const ( ) type Discovery interface { - Get(mode SelectMode) string - All() []string + Refresh() error // refresh from remote registry + Update(servers []string) error + Get(mode SelectMode) (string, error) + GetAll() ([]string, error) } var _ Discovery = (*MultiServersDiscovery)(nil) @@ -29,38 +32,46 @@ type MultiServersDiscovery struct { index int // record the selected position for robin algorithm } +// Refresh doesn't make sense for MultiServersDiscovery, so ignore it +func (d *MultiServersDiscovery) Refresh() error { + return nil +} + // Update the servers of discovery dynamically if needed -func (d *MultiServersDiscovery) Update(servers []string) { +func (d *MultiServersDiscovery) Update(servers []string) error { d.mu.Lock() defer d.mu.Unlock() d.servers = servers + return nil } -func (d *MultiServersDiscovery) Get(mode SelectMode) string { +// Get a server according to mode +func (d *MultiServersDiscovery) Get(mode SelectMode) (string, error) { d.mu.Lock() defer d.mu.Unlock() if len(d.servers) == 0 { - return "" + return "", errors.New("rpc discovery: no available servers") } switch mode { case RandomSelect: - return d.servers[d.r.Intn(len(d.servers))] + return d.servers[d.r.Intn(len(d.servers))], nil case RoundRobinSelect: s := d.servers[d.index] d.index = (d.index + 1) % len(d.servers) - return s + return s, nil default: - return "" + return "", errors.New("rpc discovery: not supported select mode") } } -func (d *MultiServersDiscovery) All() []string { +// returns all servers in discovery +func (d *MultiServersDiscovery) GetAll() ([]string, error) { d.mu.RLock() defer d.mu.RUnlock() // return a copy of d.servers servers := make([]string, len(d.servers), len(d.servers)) copy(servers, d.servers) - return servers + return servers, nil } // NewMultiServerDiscovery creates a MultiServersDiscovery instance diff --git a/gee-rpc/day6-discovery/xclient/xclient.go b/gee-rpc/day6-load-balance/xclient/xclient.go similarity index 93% rename from gee-rpc/day6-discovery/xclient/xclient.go rename to gee-rpc/day6-load-balance/xclient/xclient.go index f3df99c..838b343 100644 --- a/gee-rpc/day6-discovery/xclient/xclient.go +++ b/gee-rpc/day6-load-balance/xclient/xclient.go @@ -65,15 +65,21 @@ func (xc *XClient) call(rpcAddr string, ctx context.Context, serviceMethod strin // and returns its error status. // xc will choose a proper server. func (xc *XClient) Call(ctx context.Context, serviceMethod string, args, reply interface{}) error { - rpcAddr := xc.d.Get(xc.mode) + rpcAddr, err := xc.d.Get(xc.mode) + if err != nil { + return err + } return xc.call(rpcAddr, ctx, serviceMethod, args, reply) } // Broadcast invokes the named function for every server registered in discovery func (xc *XClient) Broadcast(ctx context.Context, serviceMethod string, args, reply interface{}) error { - servers := xc.d.All() + servers, err := xc.d.GetAll() + if err != nil { + return err + } var wg sync.WaitGroup - var mu sync.Mutex + var mu sync.Mutex // protect e and replyDone var e error replyDone := reply == nil // if reply is nil, don't need to set value ctx, cancel := context.WithCancel(ctx) diff --git a/gee-rpc/day7-registry/client.go b/gee-rpc/day7-registry/client.go new file mode 100644 index 0000000..e9b4540 --- /dev/null +++ b/gee-rpc/day7-registry/client.go @@ -0,0 +1,324 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package geerpc + +import ( + "bufio" + "context" + "encoding/json" + "errors" + "fmt" + "geerpc/codec" + "io" + "log" + "net" + "net/http" + "strings" + "sync" + "time" +) + +// Call represents an active RPC. +type Call struct { + Seq uint64 + ServiceMethod string // format "." + Args interface{} // arguments to the function + Reply interface{} // reply from the function + Error error // if error occurs, it will be set + Done chan *Call // Strobes when call is complete. +} + +func (call *Call) done() { + call.Done <- call +} + +// Client represents an RPC Client. +// There may be multiple outstanding Calls associated +// with a single Client, and a Client may be used by +// multiple goroutines simultaneously. +type Client struct { + cc codec.Codec + opt *Option + sending sync.Mutex // protect following + header codec.Header + mu sync.Mutex // protect following + seq uint64 + pending map[uint64]*Call + closing bool // user has called Close + shutdown bool // server has told us to stop +} + +var _ io.Closer = (*Client)(nil) + +var ErrShutdown = errors.New("connection is shut down") + +// Close the connection +func (client *Client) Close() error { + client.mu.Lock() + defer client.mu.Unlock() + if client.closing { + return ErrShutdown + } + client.closing = true + return client.cc.Close() +} + +// IsAvailable return true if the client does work +func (client *Client) IsAvailable() bool { + client.mu.Lock() + defer client.mu.Unlock() + return !client.shutdown && !client.closing +} + +func (client *Client) registerCall(call *Call) (uint64, error) { + client.mu.Lock() + defer client.mu.Unlock() + if client.closing || client.shutdown { + return 0, ErrShutdown + } + seq := client.seq + client.pending[seq] = call + client.seq++ + return seq, nil +} + +func (client *Client) removeCall(seq uint64) *Call { + client.mu.Lock() + defer client.mu.Unlock() + call := client.pending[seq] + delete(client.pending, seq) + return call +} + +func (client *Client) terminateCalls(err error) { + client.sending.Lock() + defer client.sending.Unlock() + client.mu.Lock() + defer client.mu.Unlock() + client.shutdown = true + for _, call := range client.pending { + call.Error = err + call.done() + } +} + +func (client *Client) send(call *Call) { + // make sure that the client will send a complete request + client.sending.Lock() + defer client.sending.Unlock() + + // register this call. + seq, err := client.registerCall(call) + call.Seq = seq + if err != nil { + call.Error = err + call.done() + return + } + + // prepare request header + client.header.ServiceMethod = call.ServiceMethod + client.header.Seq = seq + client.header.Error = "" + + // encode and send the request + if err := client.cc.Write(&client.header, call.Args); err != nil { + call := client.removeCall(seq) + // call may be nil, it usually means that Write partially failed, + // client has received the response and handled + if call != nil { + call.Error = err + call.done() + } + } +} + +func (client *Client) receive() { + var err error + for err == nil { + var h codec.Header + if err = client.cc.ReadHeader(&h); err != nil { + break + } + call := client.removeCall(h.Seq) + switch { + case call == nil: + // it usually means that Write partially failed + // and call was already removed. + err = client.cc.ReadBody(nil) + case h.Error != "": + call.Error = fmt.Errorf(h.Error) + err = client.cc.ReadBody(nil) + call.done() + default: + err = client.cc.ReadBody(call.Reply) + if err != nil { + call.Error = errors.New("reading body " + err.Error()) + } + call.done() + } + } + // error occurs, so terminateCalls pending calls + client.terminateCalls(err) +} + +// Go invokes the function asynchronously. +// It returns the Call structure representing the invocation. +func (client *Client) Go(serviceMethod string, args, reply interface{}, done chan *Call) *Call { + if done == nil { + done = make(chan *Call, 10) + } else if cap(done) == 0 { + log.Panic("rpc client: done channel is unbuffered") + } + call := &Call{ + ServiceMethod: serviceMethod, + Args: args, + Reply: reply, + Done: done, + } + client.send(call) + return call +} + +// Call invokes the named function, waits for it to complete, +// and returns its error status. +func (client *Client) Call(ctx context.Context, serviceMethod string, args, reply interface{}) error { + call := client.Go(serviceMethod, args, reply, make(chan *Call, 1)) + select { + case <-ctx.Done(): + client.removeCall(call.Seq) + return errors.New("rpc client: call failed: " + ctx.Err().Error()) + case call := <-call.Done: + return call.Error + } +} + +func parseOptions(opts ...*Option) (*Option, error) { + // if opts is nil or pass nil as parameter + if len(opts) == 0 || opts[0] == nil { + return DefaultOption, nil + } + if len(opts) != 1 { + return nil, errors.New("number of options is more than 1") + } + opt := opts[0] + opt.MagicNumber = DefaultOption.MagicNumber + if opt.CodecType == "" { + opt.CodecType = DefaultOption.CodecType + } + return opt, nil +} + +func NewClient(conn net.Conn, opt *Option) (*Client, error) { + f := codec.NewCodecFuncMap[opt.CodecType] + if f == nil { + err := fmt.Errorf("invalid codec type %s", opt.CodecType) + log.Println("rpc client: codec error:", err) + return nil, err + } + // send options with server + if err := json.NewEncoder(conn).Encode(opt); err != nil { + log.Println("rpc client: options error: ", err) + _ = conn.Close() + return nil, err + } + return newClientCodec(f(conn), opt), nil +} + +func newClientCodec(cc codec.Codec, opt *Option) *Client { + client := &Client{ + seq: 1, // seq starts with 1, 0 means invalid call + cc: cc, + opt: opt, + pending: make(map[uint64]*Call), + } + go client.receive() + return client +} + +type clientResult struct { + client *Client + err error +} + +type dialFunc func(network, address string, opt *Option) (client *Client, err error) + +func dialTimeout(f dialFunc, network, address string, opts ...*Option) (*Client, error) { + opt, err := parseOptions(opts...) + if err != nil { + return nil, err + } + ch := make(chan clientResult) + go func() { + client, err := f(network, address, opt) + ch <- clientResult{client: client, err: err} + }() + if opt.ConnectTimeout == 0 { + result := <-ch + return result.client, result.err + } + select { + case <-time.After(opt.ConnectTimeout): + return nil, fmt.Errorf("rpc client: dial timeout: expect within %s", opt.ConnectTimeout) + case result := <-ch: + return result.client, result.err + } +} + +func dial(network, address string, opt *Option) (*Client, error) { + conn, err := net.Dial(network, address) + if err != nil { + return nil, err + } + return NewClient(conn, opt) +} + +// Dial connects to an RPC server at the specified network address +func Dial(network, address string, opts ...*Option) (*Client, error) { + return dialTimeout(dial, network, address, opts...) +} + +func dialHTTP(network, address string, opt *Option) (*Client, error) { + conn, err := net.Dial(network, address) + if err != nil { + return nil, err + } + _, _ = io.WriteString(conn, fmt.Sprintf("CONNECT %s HTTP/1.0\n\n", defaultRPCPath)) + + // Require successful HTTP response + // before switching to RPC protocol. + resp, err := http.ReadResponse(bufio.NewReader(conn), &http.Request{Method: "CONNECT"}) + if err == nil && resp.Status == connected { + return NewClient(conn, opt) + } + if err == nil { + err = errors.New("unexpected HTTP response: " + resp.Status) + } + _ = conn.Close() + return nil, err +} + +// DialHTTP connects to an HTTP RPC server at the specified network address +// listening on the default HTTP RPC path. +func DialHTTP(network, address string, opts ...*Option) (*Client, error) { + return dialTimeout(dialHTTP, network, address, opts...) +} + +// XDial use a general format to represent a rpc server +// eg, http@10.0.0.1:7001, tcp@10.0.0.1:9999, unix@/tmp/geerpc.sock +func XDial(rpcAddr string, opts ...*Option) (*Client, error) { + parts := strings.Split(rpcAddr, "@") + if len(parts) != 2 { + return nil, fmt.Errorf("rpc client err: wrong format '%s', expect protocol@addr", rpcAddr) + } + protocol, addr := parts[0], parts[1] + switch protocol { + case "http": + return DialHTTP("tcp", addr, opts...) + default: + // tcp, unix or other transport protocol + return Dial(protocol, addr, opts...) + } +} diff --git a/gee-rpc/day7-registry/client_test.go b/gee-rpc/day7-registry/client_test.go new file mode 100644 index 0000000..10b9817 --- /dev/null +++ b/gee-rpc/day7-registry/client_test.go @@ -0,0 +1,84 @@ +package geerpc + +import ( + "context" + "net" + "os" + "runtime" + "strings" + "testing" + "time" +) + +type Bar int + +func (b Bar) Timeout(argv int, reply *int) error { + time.Sleep(time.Second * 2) + return nil +} + +func startServer(addr chan string) { + var b Bar + _ = Register(&b) + // pick a free port + l, _ := net.Listen("tcp", ":0") + addr <- l.Addr().String() + Accept(l) +} + +func TestClient_dialTimeout(t *testing.T) { + t.Parallel() + f := func(network, address string, opt *Option) (client *Client, err error) { + time.Sleep(time.Second * 2) + return nil, nil + } + t.Run("timeout", func(t *testing.T) { + _, err := dialTimeout(f, "", "", &Option{ConnectTimeout: time.Second}) + _assert(err != nil && strings.Contains(err.Error(), "dial timeout"), "expect a timeout error") + }) + t.Run("0", func(t *testing.T) { + _, err := dialTimeout(f, "", "", &Option{ConnectTimeout: 0}) + _assert(err == nil, "0 means no limit") + }) +} + +func TestClient_Call(t *testing.T) { + t.Parallel() + addrCh := make(chan string) + go startServer(addrCh) + addr := <-addrCh + t.Run("client timeout", func(t *testing.T) { + client, _ := Dial("tcp", addr) + ctx, _ := context.WithTimeout(context.Background(), time.Second) + var reply int + err := client.Call(ctx, "Bar.Timeout", 1, &reply) + _assert(err != nil && strings.Contains(err.Error(), ctx.Err().Error()), "expect a timeout error") + }) + t.Run("server handle timeout", func(t *testing.T) { + client, _ := Dial("tcp", addr, &Option{ + HandleTimeout: time.Second, + }) + var reply int + err := client.Call(context.Background(), "Bar.Timeout", 1, &reply) + _assert(err != nil && strings.Contains(err.Error(), "handle timeout"), "expect a timeout error") + }) +} + +func TestXDial(t *testing.T) { + if runtime.GOOS == "linux" { + ch := make(chan struct{}) + addr := "/tmp/geerpc.sock" + go func() { + _ = os.Remove(addr) + l, err := net.Listen("unix", addr) + if err != nil { + t.Fatal("failed to listen unix socket") + } + ch <- struct{}{} + Accept(l) + }() + <-ch + _, err := XDial("unix@" + addr) + _assert(err == nil, "failed to connect unix socket") + } +} diff --git a/gee-rpc/day7-registry/codec/codec.go b/gee-rpc/day7-registry/codec/codec.go new file mode 100644 index 0000000..20b6ba7 --- /dev/null +++ b/gee-rpc/day7-registry/codec/codec.go @@ -0,0 +1,34 @@ +package codec + +import ( + "io" +) + +type Header struct { + ServiceMethod string // format "Service.Method" + Seq uint64 // sequence number chosen by client + Error string +} + +type Codec interface { + io.Closer + ReadHeader(*Header) error + ReadBody(interface{}) error + Write(*Header, interface{}) error +} + +type NewCodecFunc func(io.ReadWriteCloser) Codec + +type Type string + +const ( + GobType Type = "application/gob" + JsonType Type = "application/json" // not implemented +) + +var NewCodecFuncMap map[Type]NewCodecFunc + +func init() { + NewCodecFuncMap = make(map[Type]NewCodecFunc) + NewCodecFuncMap[GobType] = NewGobCodec +} diff --git a/gee-rpc/day7-registry/codec/gob.go b/gee-rpc/day7-registry/codec/gob.go new file mode 100644 index 0000000..808d97b --- /dev/null +++ b/gee-rpc/day7-registry/codec/gob.go @@ -0,0 +1,57 @@ +package codec + +import ( + "bufio" + "encoding/gob" + "io" + "log" +) + +type GobCodec struct { + conn io.ReadWriteCloser + buf *bufio.Writer + dec *gob.Decoder + enc *gob.Encoder +} + +var _ Codec = (*GobCodec)(nil) + +func NewGobCodec(conn io.ReadWriteCloser) Codec { + buf := bufio.NewWriter(conn) + return &GobCodec{ + conn: conn, + buf: buf, + dec: gob.NewDecoder(conn), + enc: gob.NewEncoder(buf), + } +} + +func (c *GobCodec) ReadHeader(h *Header) error { + return c.dec.Decode(h) +} + +func (c *GobCodec) ReadBody(body interface{}) error { + return c.dec.Decode(body) +} + +func (c *GobCodec) Write(h *Header, body interface{}) (err error) { + defer func() { + _ = c.buf.Flush() + if err != nil { + _ = c.Close() + } + }() + if err := c.enc.Encode(h); err != nil { + log.Println("rpc: gob error encoding header:", err) + return err + } + if err := c.enc.Encode(body); err != nil { + log.Println("rpc: gob error encoding body:", err) + return err + } + return nil +} + +func (c *GobCodec) Close() error { + return c.conn.Close() +} diff --git a/gee-rpc/day7-registry/debug.go b/gee-rpc/day7-registry/debug.go new file mode 100644 index 0000000..ece1ffd --- /dev/null +++ b/gee-rpc/day7-registry/debug.go @@ -0,0 +1,60 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package geerpc + +import ( + "fmt" + "html/template" + "net/http" +) + +const debugText = ` + + GeeRPC Services + {{range .}} +
+ Service {{.Name}} +
+ + + {{range $name, $mtype := .Method}} + + + + + {{end}} +
MethodCalls
{{$name}}({{$mtype.ArgType}}, {{$mtype.ReplyType}}) error{{$mtype.NumCalls}}
+ {{end}} + + ` + +var debug = template.Must(template.New("RPC debug").Parse(debugText)) + +type debugHTTP struct { + *Server +} + +type debugService struct { + Name string + Method map[string]*methodType +} + +// Runs at /debug/geerpc +func (server debugHTTP) ServeHTTP(w http.ResponseWriter, req *http.Request) { + // Build a sorted version of the data. + var services []debugService + server.serviceMap.Range(func(namei, svci interface{}) bool { + svc := svci.(*service) + services = append(services, debugService{ + Name: namei.(string), + Method: svc.method, + }) + return true + }) + err := debug.Execute(w, services) + if err != nil { + _, _ = fmt.Fprintln(w, "rpc: error executing template:", err.Error()) + } +} diff --git a/gee-rpc/day7-registry/go.mod b/gee-rpc/day7-registry/go.mod new file mode 100644 index 0000000..0ec8aeb --- /dev/null +++ b/gee-rpc/day7-registry/go.mod @@ -0,0 +1,3 @@ +module geerpc + +go 1.13 diff --git a/gee-rpc/day7-registry/main/main.go b/gee-rpc/day7-registry/main/main.go new file mode 100644 index 0000000..0776312 --- /dev/null +++ b/gee-rpc/day7-registry/main/main.go @@ -0,0 +1,112 @@ +package main + +import ( + "context" + "geerpc" + registy "geerpc/registry" + "geerpc/xclient" + "log" + "net" + "net/http" + "sync" + "time" +) + +type Foo int + +type Args struct{ Num1, Num2 int } + +func (f Foo) Sum(args Args, reply *int) error { + *reply = args.Num1 + args.Num2 + return nil +} + +func (f Foo) Sleep(args Args, reply *int) error { + time.Sleep(time.Second * time.Duration(args.Num1)) + *reply = args.Num1 + args.Num2 + return nil +} + +func startRegistry(wg *sync.WaitGroup) { + l, _ := net.Listen("tcp", ":9999") + registy.HandleHTTP() + wg.Done() + _ = http.Serve(l, nil) +} + +func startServer(registry string, wg *sync.WaitGroup) { + var foo Foo + l, _ := net.Listen("tcp", ":0") + server := geerpc.NewServer() + _ = server.Register(&foo) + registy.Heartbeat(registry, "tcp@"+l.Addr().String(), 0) + wg.Done() + server.Accept(l) +} + +func foo(xc *xclient.XClient, ctx context.Context, typ, serviceMethod string, args *Args) { + var reply int + var err error + switch typ { + case "call": + err = xc.Call(ctx, serviceMethod, args, &reply) + case "broadcast": + err = xc.Broadcast(ctx, serviceMethod, args, &reply) + } + if err != nil { + log.Printf("%s %s error: %v", typ, serviceMethod, err) + } else { + log.Printf("%s Foo.Sum success: %d + %d = %d", typ, args.Num1, args.Num2, reply) + } +} + +func call(registry string) { + d := xclient.NewGeeRegistryDiscovery(registry, 0) + xc := xclient.NewXClient(d, xclient.RandomSelect, nil) + defer func() { _ = xc.Close() }() + // send request & receive response + var wg sync.WaitGroup + for i := 0; i < 5; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + foo(xc, context.Background(), "call", "Foo.Sum", &Args{Num1: i, Num2: i * i}) + }(i) + } + wg.Wait() +} + +func broadcast(registry string) { + d := xclient.NewGeeRegistryDiscovery(registry, 0) + xc := xclient.NewXClient(d, xclient.RandomSelect, nil) + var wg sync.WaitGroup + for i := 0; i < 5; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + foo(xc, context.Background(), "broadcast", "Foo.Sum", &Args{Num1: i, Num2: i * i}) + // expect 2 - 5 timeout + ctx, _ := context.WithTimeout(context.Background(), time.Second*2) + foo(xc, ctx, "broadcast", "Foo.Sleep", &Args{Num1: i, Num2: i * i}) + }(i) + } + wg.Wait() +} + +func main() { + registryAddr := "http://localhost:9999/_geerpc_/registry" + var wg sync.WaitGroup + wg.Add(1) + go startRegistry(&wg) + wg.Wait() + + time.Sleep(time.Second) + wg.Add(2) + go startServer(registryAddr, &wg) + go startServer(registryAddr, &wg) + wg.Wait() + + time.Sleep(time.Second) + call(registryAddr) + broadcast(registryAddr) +} diff --git a/gee-rpc/day7-registry/registry/registry.go b/gee-rpc/day7-registry/registry/registry.go new file mode 100644 index 0000000..0d50b4d --- /dev/null +++ b/gee-rpc/day7-registry/registry/registry.go @@ -0,0 +1,123 @@ +package registy + +import ( + "log" + "net/http" + "strings" + "sync" + "time" +) + +// Registry is a simple register center, provide following functions. +// add a server and receive heartbeat to keep it alive. +// returns all alive servers and delete dead servers sync simultaneously. +type Registry struct { + timeout time.Duration + mu sync.Mutex // protect following + servers map[string]*ServerItem +} + +type ServerItem struct { + Addr string + start time.Time +} + +// New create a registry instance with timeout setting +func New(timeout time.Duration) *Registry { + return &Registry{ + servers: make(map[string]*ServerItem), + timeout: timeout, + } +} + +var DefaultRegister = New(defaultTimeout) + +func (r *Registry) putServer(addr string) { + r.mu.Lock() + defer r.mu.Unlock() + s := r.servers[addr] + if s == nil { + r.servers[addr] = &ServerItem{Addr: addr, start: time.Now()} + } else { + s.start = time.Now() // if exists, update start time to keep alive + } +} + +func (r *Registry) aliveServers() []string { + r.mu.Lock() + defer r.mu.Unlock() + var alive []string + for addr, s := range r.servers { + if r.timeout == 0 || s.start.Add(r.timeout).After(time.Now()) { + alive = append(alive, addr) + } else { + delete(r.servers, addr) + } + } + return alive +} + +const ( + defaultPath = "/_geerpc_/registry" + defaultTimeout = time.Minute * 5 +) + +// Runs at /_geerpc_/registry +func (r *Registry) ServeHTTP(w http.ResponseWriter, req *http.Request) { + switch req.Method { + case "GET": + // keep it simple, server is in req.Header + w.Header().Set("X-Geerpc-Servers", strings.Join(r.aliveServers(), ",")) + case "POST": + // keep it simple, server is in req.Header + addr := req.Header.Get("X-Geerpc-Server") + if addr == "" { + w.WriteHeader(http.StatusInternalServerError) + return + } + r.putServer(addr) + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } +} + +// HandleHTTP registers an HTTP handler for Registry messages on registryPath +func (r *Registry) HandleHTTP(registryPath string) { + http.Handle(registryPath, r) + log.Println("rpc registry path:", registryPath) +} + +func HandleHTTP() { + DefaultRegister.HandleHTTP(defaultPath) +} + +// Heartbeat send a heartbeat message every once in a while +// it's a helper function for a server to register or send heartbeat +func Heartbeat(registry, addr string, duration time.Duration) { + if duration == 0 { + // make sure there is enough time to send heart beat + // before it's removed from registry + duration = defaultTimeout - time.Duration(1)*time.Minute + } + var err error + err = sendHeartbeat(registry, addr) + go func() { + t := time.NewTicker(duration) + for err == nil { + <-t.C + err = sendHeartbeat(registry, addr) + } + }() +} + +func sendHeartbeat(registry, addr string) error { + log.Println(addr, "send heart beat to registry") + httpClient := &http.Client{} + req, _ := http.NewRequest("POST", registry, nil) + req.Header.Set("X-Geerpc-Server", addr) + if _, err := httpClient.Do(req); err != nil { + log.Println("rpc server: heart beat err:", err) + return err + } + return nil +} diff --git a/gee-rpc/day7-registry/server.go b/gee-rpc/day7-registry/server.go new file mode 100644 index 0000000..38fad20 --- /dev/null +++ b/gee-rpc/day7-registry/server.go @@ -0,0 +1,266 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package geerpc + +import ( + "encoding/json" + "errors" + "fmt" + "geerpc/codec" + "io" + "log" + "net" + "net/http" + "reflect" + "strings" + "sync" + "time" +) + +const MagicNumber = 0x3bef5c + +type Option struct { + MagicNumber int // MagicNumber marks this's a geerpc request + CodecType codec.Type // client may choose different Codec to encode body + ConnectTimeout time.Duration // 0 means no limit + HandleTimeout time.Duration +} + +var DefaultOption = &Option{ + MagicNumber: MagicNumber, + CodecType: codec.GobType, + ConnectTimeout: time.Second * 10, +} + +// Server represents an RPC Server. +type Server struct { + serviceMap sync.Map +} + +// NewServer returns a new Server. +func NewServer() *Server { + return &Server{} +} + +// DefaultServer is the default instance of *Server. +var DefaultServer = NewServer() + +// ServeConn runs the server on a single connection. +// ServeConn blocks, serving the connection until the client hangs up. +func (server *Server) ServeConn(conn io.ReadWriteCloser) { + defer func() { _ = conn.Close() }() + var opt Option + if err := json.NewDecoder(conn).Decode(&opt); err != nil { + log.Println("rpc server: options error: ", err) + return + } + if opt.MagicNumber != MagicNumber { + log.Printf("rpc server: invalid magic number %x", opt.MagicNumber) + return + } + f := codec.NewCodecFuncMap[opt.CodecType] + if f == nil { + log.Printf("rpc server: invalid codec type %s", opt.CodecType) + return + } + server.serveCodec(f(conn), &opt) +} + +// invalidRequest is a placeholder for response argv when error occurs +var invalidRequest = struct{}{} + +func (server *Server) serveCodec(cc codec.Codec, opt *Option) { + sending := new(sync.Mutex) // make sure to send a complete response + wg := new(sync.WaitGroup) // wait until all request are handled + for { + req, err := server.readRequest(cc) + if err != nil { + if req == nil { + break // it's not possible to recover, so close the connection + } + req.h.Error = err.Error() + server.sendResponse(cc, req.h, invalidRequest, sending) + continue + } + wg.Add(1) + go server.handleRequest(cc, req, sending, wg, opt.HandleTimeout) + } + wg.Wait() + _ = cc.Close() +} + +// request stores all information of a call +type request struct { + h *codec.Header // header of request + argv, replyv reflect.Value // argv and replyv of request + mtype *methodType + svc *service +} + +func (server *Server) readRequestHeader(cc codec.Codec) (*codec.Header, error) { + var h codec.Header + if err := cc.ReadHeader(&h); err != nil { + if err != io.EOF && err != io.ErrUnexpectedEOF { + log.Println("rpc server: read header error:", err) + } + return nil, err + } + return &h, nil +} + +func (server *Server) findService(serviceMethod string) (svc *service, mtype *methodType, err error) { + dot := strings.LastIndex(serviceMethod, ".") + if dot < 0 { + err = errors.New("rpc server: service/method request ill-formed: " + serviceMethod) + return + } + serviceName, methodName := serviceMethod[:dot], serviceMethod[dot+1:] + svci, ok := server.serviceMap.Load(serviceName) + if !ok { + err = errors.New("rpc server: can't find service " + serviceName) + return + } + svc = svci.(*service) + mtype = svc.method[methodName] + if mtype == nil { + err = errors.New("rpc server: can't find method " + methodName) + } + return +} + +func (server *Server) readRequest(cc codec.Codec) (*request, error) { + h, err := server.readRequestHeader(cc) + if err != nil { + return nil, err + } + req := &request{h: h} + req.svc, req.mtype, err = server.findService(h.ServiceMethod) + if err != nil { + return req, err + } + req.argv = req.mtype.newArgv() + req.replyv = req.mtype.newReplyv() + + // make sure that argvi is a pointer, ReadBody need a pointer as parameter + argvi := req.argv.Interface() + if req.argv.Type().Kind() != reflect.Ptr { + argvi = req.argv.Addr().Interface() + } + if err = cc.ReadBody(argvi); err != nil { + log.Println("rpc server: read body err:", err) + return req, err + } + return req, nil +} + +func (server *Server) sendResponse(cc codec.Codec, h *codec.Header, body interface{}, sending *sync.Mutex) { + sending.Lock() + defer sending.Unlock() + if err := cc.Write(h, body); err != nil { + log.Println("rpc server: write response error:", err) + } +} + +func (server *Server) handleRequest(cc codec.Codec, req *request, sending *sync.Mutex, wg *sync.WaitGroup, timeout time.Duration) { + defer wg.Done() + called := make(chan struct{}) + sent := make(chan struct{}) + go func() { + err := req.svc.call(req.mtype, req.argv, req.replyv) + called <- struct{}{} + if err != nil { + req.h.Error = err.Error() + server.sendResponse(cc, req.h, invalidRequest, sending) + sent <- struct{}{} + return + } + server.sendResponse(cc, req.h, req.replyv.Interface(), sending) + sent <- struct{}{} + }() + + if timeout == 0 { + <-called + <-sent + return + } + select { + case <-time.After(timeout): + req.h.Error = fmt.Sprintf("rpc server: request handle timeout: expect within %s", timeout) + server.sendResponse(cc, req.h, invalidRequest, sending) + case <-called: + <-sent + } +} + +// Accept accepts connections on the listener and serves requests +// for each incoming connection. +func (server *Server) Accept(lis net.Listener) { + for { + conn, err := lis.Accept() + if err != nil { + log.Println("rpc server: accept error:", err) + return + } + go server.ServeConn(conn) + } +} + +// Accept accepts connections on the listener and serves requests +// for each incoming connection. +func Accept(lis net.Listener) { DefaultServer.Accept(lis) } + +// Register publishes in the server the set of methods of the +// receiver value that satisfy the following conditions: +// - exported method of exported type +// - two arguments, both of exported type +// - the second argument is a pointer +// - one return value, of type error +func (server *Server) Register(rcvr interface{}) error { + s := newService(rcvr) + if _, dup := server.serviceMap.LoadOrStore(s.name, s); dup { + return errors.New("rpc: service already defined: " + s.name) + } + return nil +} + +// Register publishes the receiver's methods in the DefaultServer. +func Register(rcvr interface{}) error { return DefaultServer.Register(rcvr) } + +const ( + connected = "200 Connected to Gee RPC" + defaultRPCPath = "/_geeprc_" + defaultDebugPath = "/debug/geerpc" +) + +// ServeHTTP implements an http.Handler that answers RPC requests. +func (server *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) { + if req.Method != "CONNECT" { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.WriteHeader(http.StatusMethodNotAllowed) + _, _ = io.WriteString(w, "405 must CONNECT\n") + return + } + conn, _, err := w.(http.Hijacker).Hijack() + if err != nil { + log.Print("rpc hijacking ", req.RemoteAddr, ": ", err.Error()) + return + } + _, _ = io.WriteString(conn, "HTTP/1.0 "+connected+"\n\n") + server.ServeConn(conn) +} + +// HandleHTTP registers an HTTP handler for RPC messages on rpcPath, +// and a debugging handler on debugPath. +// It is still necessary to invoke http.Serve(), typically in a go statement. +func (server *Server) HandleHTTP() { + http.Handle(defaultRPCPath, server) + http.Handle(defaultDebugPath, debugHTTP{server}) + log.Println("rpc server debug path:", defaultDebugPath) +} + +// HandleHTTP is a convenient approach for default server to register HTTP handlers +func HandleHTTP() { + DefaultServer.HandleHTTP() +} diff --git a/gee-rpc/day7-registry/service.go b/gee-rpc/day7-registry/service.go new file mode 100644 index 0000000..306683c --- /dev/null +++ b/gee-rpc/day7-registry/service.go @@ -0,0 +1,99 @@ +package geerpc + +import ( + "go/ast" + "log" + "reflect" + "sync/atomic" +) + +type methodType struct { + method reflect.Method + ArgType reflect.Type + ReplyType reflect.Type + numCalls uint64 +} + +func (m *methodType) NumCalls() uint64 { + return atomic.LoadUint64(&m.numCalls) +} + +func (m *methodType) newArgv() reflect.Value { + var argv reflect.Value + // arg may be a pointer type, or a value type + if m.ArgType.Kind() == reflect.Ptr { + argv = reflect.New(m.ArgType.Elem()) + } else { + argv = reflect.New(m.ArgType).Elem() + } + return argv +} + +func (m *methodType) newReplyv() reflect.Value { + // reply must be a pointer type + replyv := reflect.New(m.ReplyType.Elem()) + switch m.ReplyType.Elem().Kind() { + case reflect.Map: + replyv.Elem().Set(reflect.MakeMap(m.ReplyType.Elem())) + case reflect.Slice: + replyv.Elem().Set(reflect.MakeSlice(m.ReplyType.Elem(), 0, 0)) + } + return replyv +} + +type service struct { + name string + typ reflect.Type + rcvr reflect.Value + method map[string]*methodType +} + +func newService(rcvr interface{}) *service { + s := new(service) + s.rcvr = reflect.ValueOf(rcvr) + s.name = reflect.Indirect(s.rcvr).Type().Name() + s.typ = reflect.TypeOf(rcvr) + if !ast.IsExported(s.name) { + log.Fatalf("rpc server: %s is not a valid service name", s.name) + } + s.registerMethods() + return s +} + +func (s *service) registerMethods() { + s.method = make(map[string]*methodType) + for i := 0; i < s.typ.NumMethod(); i++ { + method := s.typ.Method(i) + mType := method.Type + if mType.NumIn() != 3 || mType.NumOut() != 1 { + continue + } + if mType.Out(0) != reflect.TypeOf((*error)(nil)).Elem() { + continue + } + argType, replyType := mType.In(1), mType.In(2) + if !isExportedOrBuiltinType(argType) || !isExportedOrBuiltinType(replyType) { + continue + } + s.method[method.Name] = &methodType{ + method: method, + ArgType: argType, + ReplyType: replyType, + } + log.Printf("rpc server: register %s.%s\n", s.name, method.Name) + } +} + +func (s *service) call(m *methodType, argv, replyv reflect.Value) error { + atomic.AddUint64(&m.numCalls, 1) + f := m.method.Func + returnValues := f.Call([]reflect.Value{s.rcvr, argv, replyv}) + if errInter := returnValues[0].Interface(); errInter != nil { + return errInter.(error) + } + return nil +} + +func isExportedOrBuiltinType(t reflect.Type) bool { + return ast.IsExported(t.Name()) || t.PkgPath() == "" +} diff --git a/gee-rpc/day7-registry/service_test.go b/gee-rpc/day7-registry/service_test.go new file mode 100644 index 0000000..c8266df --- /dev/null +++ b/gee-rpc/day7-registry/service_test.go @@ -0,0 +1,48 @@ +package geerpc + +import ( + "fmt" + "reflect" + "testing" +) + +type Foo int + +type Args struct{ Num1, Num2 int } + +func (f Foo) Sum(args Args, reply *int) error { + *reply = args.Num1 + args.Num2 + return nil +} + +// it's not a exported Method +func (f Foo) sum(args Args, reply *int) error { + *reply = args.Num1 + args.Num2 + return nil +} + +func _assert(condition bool, msg string, v ...interface{}) { + if !condition { + panic(fmt.Sprintf("assertion failed: "+msg, v...)) + } +} + +func TestNewService(t *testing.T) { + var foo Foo + s := newService(&foo) + _assert(len(s.method) == 1, "wrong service Method, expect 1, but got %d", len(s.method)) + mType := s.method["Sum"] + _assert(mType != nil, "wrong Method, Sum shouldn't nil") +} + +func TestMethodType_Call(t *testing.T) { + var foo Foo + s := newService(&foo) + mType := s.method["Sum"] + + argv := mType.newArgv() + replyv := mType.newReplyv() + argv.Set(reflect.ValueOf(Args{Num1: 1, Num2: 3})) + err := s.call(mType, argv, replyv) + _assert(err == nil && *replyv.Interface().(*int) == 4 && mType.NumCalls() == 1, "failed to call Foo.Sum") +} diff --git a/gee-rpc/day7-registry/xclient/discovery.go b/gee-rpc/day7-registry/xclient/discovery.go new file mode 100644 index 0000000..f823a7b --- /dev/null +++ b/gee-rpc/day7-registry/xclient/discovery.go @@ -0,0 +1,83 @@ +package xclient + +import ( + "errors" + "math/rand" + "sync" + "time" +) + +type SelectMode int + +const ( + RandomSelect SelectMode = iota // select randomly + RoundRobinSelect // select using Robbin algorithm +) + +type Discovery interface { + Refresh() error // refresh from remote registry + Update(servers []string) error + Get(mode SelectMode) (string, error) + GetAll() ([]string, error) +} + +var _ Discovery = (*MultiServersDiscovery)(nil) + +// MultiServersDiscovery is a discovery for multi servers without a registry center +// user provides the server addresses explicitly instead +type MultiServersDiscovery struct { + r *rand.Rand // generate random number + mu sync.RWMutex // protect following + servers []string + index int // record the selected position for robin algorithm +} + +// Refresh doesn't make sense for MultiServersDiscovery, so ignore it +func (d *MultiServersDiscovery) Refresh() error { + return nil +} + +// Update the servers of discovery dynamically if needed +func (d *MultiServersDiscovery) Update(servers []string) error { + d.mu.Lock() + defer d.mu.Unlock() + d.servers = servers + return nil +} + +// Get a server according to mode +func (d *MultiServersDiscovery) Get(mode SelectMode) (string, error) { + d.mu.Lock() + defer d.mu.Unlock() + if len(d.servers) == 0 { + return "", errors.New("rpc discovery: no available servers") + } + switch mode { + case RandomSelect: + return d.servers[d.r.Intn(len(d.servers))], nil + case RoundRobinSelect: + s := d.servers[d.index] + d.index = (d.index + 1) % len(d.servers) + return s, nil + default: + return "", errors.New("rpc discovery: not supported select mode") + } +} + +// returns all servers in discovery +func (d *MultiServersDiscovery) GetAll() ([]string, error) { + d.mu.RLock() + defer d.mu.RUnlock() + // return a copy of d.servers + servers := make([]string, len(d.servers), len(d.servers)) + copy(servers, d.servers) + return servers, nil +} + +// NewMultiServerDiscovery creates a MultiServersDiscovery instance +func NewMultiServerDiscovery(servers []string) *MultiServersDiscovery { + return &MultiServersDiscovery{ + servers: servers, + r: rand.New(rand.NewSource(time.Now().UnixNano())), + } +} diff --git a/gee-rpc/day7-registry/xclient/discovery_gee.go b/gee-rpc/day7-registry/xclient/discovery_gee.go new file mode 100644 index 0000000..865c30e --- /dev/null +++ b/gee-rpc/day7-registry/xclient/discovery_gee.go @@ -0,0 +1,74 @@ +package xclient + +import ( + "log" + "net/http" + "strings" + "time" +) + +type GeeRegistryDiscovery struct { + *MultiServersDiscovery + registry string + timeout time.Duration + lastUpdate time.Time +} + +const defaultUpdateTimeout = time.Second * 10 + +func (d *GeeRegistryDiscovery) Update(servers []string) error { + d.mu.Lock() + defer d.mu.Unlock() + d.servers = servers + d.lastUpdate = time.Now() + return nil +} + +func (d *GeeRegistryDiscovery) Refresh() error { + d.mu.Lock() + defer d.mu.Unlock() + if d.lastUpdate.Add(d.timeout).After(time.Now()) { + return nil + } + log.Println("rpc registry: refresh servers from registry", d.registry) + resp, err := http.Get(d.registry) + if err != nil { + log.Println("rpc registry refresh err:", err) + return err + } + servers := strings.Split(resp.Header.Get("X-Geerpc-Servers"), ",") + d.servers = make([]string, 0, len(servers)) + for _, server := range servers { + if strings.TrimSpace(server) != "" { + d.servers = append(d.servers, strings.TrimSpace(server)) + } + } + d.lastUpdate = time.Now() + return nil +} + +func (d *GeeRegistryDiscovery) Get(mode SelectMode) (string, error) { + if err := d.Refresh(); err != nil { + return "", err + } + return d.MultiServersDiscovery.Get(mode) +} + +func (d *GeeRegistryDiscovery) GetAll() ([]string, error) { + if err := d.Refresh(); err != nil { + return nil, err + } + return d.MultiServersDiscovery.GetAll() +} + +func NewGeeRegistryDiscovery(registerAddr string, timeout time.Duration) *GeeRegistryDiscovery { + if timeout == 0 { + timeout = defaultUpdateTimeout + } + d := &GeeRegistryDiscovery{ + MultiServersDiscovery: NewMultiServerDiscovery(make([]string, 0)), + registry: registerAddr, + timeout: timeout, + } + return d +} diff --git a/gee-rpc/day7-registry/xclient/xclient.go b/gee-rpc/day7-registry/xclient/xclient.go new file mode 100644 index 0000000..838b343 --- /dev/null +++ b/gee-rpc/day7-registry/xclient/xclient.go @@ -0,0 +1,109 @@ +package xclient + +import ( + "context" + . "geerpc" + "io" + "reflect" + "sync" +) + +type XClient struct { + d Discovery + mode SelectMode + opt *Option + mu sync.Mutex // protect following + clients map[string]*Client +} + +var _ io.Closer = (*XClient)(nil) + +func NewXClient(d Discovery, mode SelectMode, opt *Option) *XClient { + return &XClient{d: d, mode: mode, opt: opt, clients: make(map[string]*Client)} +} + +func (xc *XClient) Close() error { + xc.mu.Lock() + defer xc.mu.Unlock() + for key, client := range xc.clients { + // I have no idea how to deal with error, just ignore it. + _ = client.Close() + delete(xc.clients, key) + } + return nil +} + +func (xc *XClient) dial(rpcAddr string) (*Client, error) { + xc.mu.Lock() + defer xc.mu.Unlock() + client, ok := xc.clients[rpcAddr] + if ok && !client.IsAvailable() { + _ = client.Close() + delete(xc.clients, rpcAddr) + client = nil + } + if client == nil { + var err error + client, err = XDial(rpcAddr, xc.opt) + if err != nil { + return nil, err + } + xc.clients[rpcAddr] = client + } + return client, nil +} + +func (xc *XClient) call(rpcAddr string, ctx context.Context, serviceMethod string, args, reply interface{}) error { + client, err := xc.dial(rpcAddr) + if err != nil { + return err + } + return client.Call(ctx, serviceMethod, args, reply) +} + +// Call invokes the named function, waits for it to complete, +// and returns its error status. +// xc will choose a proper server. +func (xc *XClient) Call(ctx context.Context, serviceMethod string, args, reply interface{}) error { + rpcAddr, err := xc.d.Get(xc.mode) + if err != nil { + return err + } + return xc.call(rpcAddr, ctx, serviceMethod, args, reply) +} + +// Broadcast invokes the named function for every server registered in discovery +func (xc *XClient) Broadcast(ctx context.Context, serviceMethod string, args, reply interface{}) error { + servers, err := xc.d.GetAll() + if err != nil { + return err + } + var wg sync.WaitGroup + var mu sync.Mutex // protect e and replyDone + var e error + replyDone := reply == nil // if reply is nil, don't need to set value + ctx, cancel := context.WithCancel(ctx) + for _, rpcAddr := range servers { + wg.Add(1) + go func() { + defer wg.Done() + var clonedReply interface{} + if reply != nil { + clonedReply = reflect.New(reflect.ValueOf(reply).Elem().Type()).Interface() + } + err := xc.call(rpcAddr, ctx, serviceMethod, args, clonedReply) + mu.Lock() + if err != nil && e == nil { + e = err + cancel() // if any call failed, cancel unfinished calls + } + if err == nil && !replyDone { + reflect.ValueOf(reply).Elem().Set(reflect.ValueOf(clonedReply).Elem()) + replyDone = true + } + mu.Unlock() + }() + } + wg.Wait() + return e +} From c5797482a91ec9c457cbf3b48b4538f38b684f3e Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Mon, 5 Oct 2020 21:15:32 +0800 Subject: [PATCH 086/122] README.md: add RPC Framework title for geerpc --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index c2d9d02..ff6b021 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,8 @@ Xorm's desgin is easier to understand than gorm-v1, so the main designs referenc - Day 6 - Support Transaction | [Code](gee-orm/day6-transaction) - Day 7 - Migrate Database | [Code](gee-orm/day7-migrate) +## RPC Framework - GeeRPC + [GeeRPC](https://geektutu.com/post/geerpc.html) is a [net/rpc](https://github.com/golang/go/tree/master/src/net/rpc)-like RPC framework Based on golang standard library `net/rpc`, GeeRPC implements more features. eg, protocol exchange, service registration and discovery, load balance, etc. From b5c91b0ceefc5da0b6813f753e229d446a5ab860 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Tue, 6 Oct 2020 19:27:02 +0800 Subject: [PATCH 087/122] gee-web/day6 fix formatAsDate in go 1.15 issue #19 --- gee-web/README.md | 4 ++-- gee-web/day6-template/main.go | 4 ++-- gee-web/day6-template/templates/custom_func.tmpl | 2 +- gee-web/doc/gee-day6.md | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/gee-web/README.md b/gee-web/README.md index 6e8f19b..d56ed8d 100644 --- a/gee-web/README.md +++ b/gee-web/README.md @@ -168,7 +168,7 @@ type student struct { Age int8 } -func formatAsDate(t time.Time) string { +func FormatAsDate(t time.Time) string { year, month, day := t.Date() return fmt.Sprintf("%d-%02d-%02d", year, month, day) } @@ -177,7 +177,7 @@ func main() { r := gee.New() r.Use(gee.Logger()) r.SetFuncMap(template.FuncMap{ - "formatAsDate": formatAsDate, + "FormatAsDate": FormatAsDate, }) r.LoadHTMLGlob("templates/*") r.Static("/assets", "./static") diff --git a/gee-web/day6-template/main.go b/gee-web/day6-template/main.go index 79b89bc..dea17b2 100644 --- a/gee-web/day6-template/main.go +++ b/gee-web/day6-template/main.go @@ -47,7 +47,7 @@ type student struct { Age int8 } -func formatAsDate(t time.Time) string { +func FormatAsDate(t time.Time) string { year, month, day := t.Date() return fmt.Sprintf("%d-%02d-%02d", year, month, day) } @@ -56,7 +56,7 @@ func main() { r := gee.New() r.Use(gee.Logger()) r.SetFuncMap(template.FuncMap{ - "formatAsDate": formatAsDate, + "FormatAsDate": FormatAsDate, }) r.LoadHTMLGlob("templates/*") r.Static("/assets", "./static") diff --git a/gee-web/day6-template/templates/custom_func.tmpl b/gee-web/day6-template/templates/custom_func.tmpl index 2d50a8e..c267ebd 100644 --- a/gee-web/day6-template/templates/custom_func.tmpl +++ b/gee-web/day6-template/templates/custom_func.tmpl @@ -2,7 +2,7 @@

hello, {{.title}}

-

Date: {{.now | formatAsDate}}

+

Date: {{.now | FormatAsDate}}

diff --git a/gee-web/doc/gee-day6.md b/gee-web/doc/gee-day6.md index be0b0fe..a95d578 100644 --- a/gee-web/doc/gee-day6.md +++ b/gee-web/doc/gee-day6.md @@ -169,7 +169,7 @@ type student struct { Age int8 } -func formatAsDate(t time.Time) string { +func FormatAsDate(t time.Time) string { year, month, day := t.Date() return fmt.Sprintf("%d-%02d-%02d", year, month, day) } @@ -178,7 +178,7 @@ func main() { r := gee.New() r.Use(gee.Logger()) r.SetFuncMap(template.FuncMap{ - "formatAsDate": formatAsDate, + "FormatAsDate": FormatAsDate, }) r.LoadHTMLGlob("templates/*") r.Static("/assets", "./static") From d52a027256336052a79c7a8d72e8ae36e0e42921 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Tue, 6 Oct 2020 19:38:27 +0800 Subject: [PATCH 088/122] gee-rpc/day7 use d.index to mode n to ensure safety --- gee-rpc/day7-registry/registry/registry.go | 2 ++ gee-rpc/day7-registry/xclient/discovery.go | 9 +++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/gee-rpc/day7-registry/registry/registry.go b/gee-rpc/day7-registry/registry/registry.go index 0d50b4d..2c22244 100644 --- a/gee-rpc/day7-registry/registry/registry.go +++ b/gee-rpc/day7-registry/registry/registry.go @@ -3,6 +3,7 @@ package registy import ( "log" "net/http" + "sort" "strings" "sync" "time" @@ -54,6 +55,7 @@ func (r *Registry) aliveServers() []string { delete(r.servers, addr) } } + sort.Strings(alive) return alive } diff --git a/gee-rpc/day7-registry/xclient/discovery.go b/gee-rpc/day7-registry/xclient/discovery.go index f823a7b..783ec4a 100644 --- a/gee-rpc/day7-registry/xclient/discovery.go +++ b/gee-rpc/day7-registry/xclient/discovery.go @@ -49,15 +49,16 @@ func (d *MultiServersDiscovery) Update(servers []string) error { func (d *MultiServersDiscovery) Get(mode SelectMode) (string, error) { d.mu.Lock() defer d.mu.Unlock() - if len(d.servers) == 0 { + n := len(d.servers) + if n == 0 { return "", errors.New("rpc discovery: no available servers") } switch mode { case RandomSelect: - return d.servers[d.r.Intn(len(d.servers))], nil + return d.servers[d.r.Intn(n)], nil case RoundRobinSelect: - s := d.servers[d.index] - d.index = (d.index + 1) % len(d.servers) + s := d.servers[d.index%n] // servers could be updated, so mode n to ensure safety + d.index = (d.index + 1) % n return s, nil default: return "", errors.New("rpc discovery: not supported select mode") From 9a5edca9afd80c998347ebfe86aafe2a960f143f Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Tue, 6 Oct 2020 19:44:59 +0800 Subject: [PATCH 089/122] geerpc/day5 day6 day7: update the function description of XDial --- gee-rpc/day5-http-debug/client.go | 4 +++- gee-rpc/day6-load-balance/client.go | 4 +++- gee-rpc/day7-registry/client.go | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/gee-rpc/day5-http-debug/client.go b/gee-rpc/day5-http-debug/client.go index e9b4540..795a18b 100644 --- a/gee-rpc/day5-http-debug/client.go +++ b/gee-rpc/day5-http-debug/client.go @@ -306,7 +306,9 @@ func DialHTTP(network, address string, opts ...*Option) (*Client, error) { return dialTimeout(dialHTTP, network, address, opts...) } -// XDial use a general format to represent a rpc server +// XDial calls different functions to connect to a RPC server +// according the first parameter rpcAddr. +// rpcAddr is a general format (protocol@addr) to represent a rpc server // eg, http@10.0.0.1:7001, tcp@10.0.0.1:9999, unix@/tmp/geerpc.sock func XDial(rpcAddr string, opts ...*Option) (*Client, error) { parts := strings.Split(rpcAddr, "@") diff --git a/gee-rpc/day6-load-balance/client.go b/gee-rpc/day6-load-balance/client.go index e9b4540..795a18b 100644 --- a/gee-rpc/day6-load-balance/client.go +++ b/gee-rpc/day6-load-balance/client.go @@ -306,7 +306,9 @@ func DialHTTP(network, address string, opts ...*Option) (*Client, error) { return dialTimeout(dialHTTP, network, address, opts...) } -// XDial use a general format to represent a rpc server +// XDial calls different functions to connect to a RPC server +// according the first parameter rpcAddr. +// rpcAddr is a general format (protocol@addr) to represent a rpc server // eg, http@10.0.0.1:7001, tcp@10.0.0.1:9999, unix@/tmp/geerpc.sock func XDial(rpcAddr string, opts ...*Option) (*Client, error) { parts := strings.Split(rpcAddr, "@") diff --git a/gee-rpc/day7-registry/client.go b/gee-rpc/day7-registry/client.go index e9b4540..795a18b 100644 --- a/gee-rpc/day7-registry/client.go +++ b/gee-rpc/day7-registry/client.go @@ -306,7 +306,9 @@ func DialHTTP(network, address string, opts ...*Option) (*Client, error) { return dialTimeout(dialHTTP, network, address, opts...) } -// XDial use a general format to represent a rpc server +// XDial calls different functions to connect to a RPC server +// according the first parameter rpcAddr. +// rpcAddr is a general format (protocol@addr) to represent a rpc server // eg, http@10.0.0.1:7001, tcp@10.0.0.1:9999, unix@/tmp/geerpc.sock func XDial(rpcAddr string, opts ...*Option) (*Client, error) { parts := strings.Split(rpcAddr, "@") From 03c85f7f987b62bfd85dfca78b4d2344083dca2f Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Tue, 6 Oct 2020 23:54:08 +0800 Subject: [PATCH 090/122] gee-rpc add doc --- gee-rpc/doc/geerpc.md | 66 ++++++++++++++++++++++++++++++++++ gee-rpc/doc/geerpc/geerpc.jpg | Bin 0 -> 32791 bytes 2 files changed, 66 insertions(+) create mode 100644 gee-rpc/doc/geerpc.md create mode 100644 gee-rpc/doc/geerpc/geerpc.jpg diff --git a/gee-rpc/doc/geerpc.md b/gee-rpc/doc/geerpc.md new file mode 100644 index 0000000..9c49e6d --- /dev/null +++ b/gee-rpc/doc/geerpc.md @@ -0,0 +1,66 @@ +--- +title: 7天用Go从零实现RPC框架GeeRPC +date: 2020-10-06 16:00:00 +description: 7天用 Go语言/golang 从零实现 RPC 框架 GeeRPC 教程(7 days implement golang remote procedure call framework from scratch tutorial),动手写 RPC 框架,参照 golang标准库 net/rpc 的实现,实现了服务端(server)、支持异步和并发的客户端(client)、消息编码与解码(message encoding and decoding)、服务注册(service register)、支持 TCP/Unix/HTTP 等多种传输协议。并在此基础上新增了协议交换(protocol exchange)、注册中心(registry)、服务发现(service discovery)、负载均衡(load balance)、超时处理(timeout processing)等特性。 +tags: +- Go +nav: 从零实现 +categories: +- RPC框架 - GeeRPC +keywords: +- Go语言 +- 从零实现RPC框架 +- 动手写RPC框架 +- 服务注册与发现 +- 负载均衡 +image: post/geerpc/geerpc.jpg +github: https://github.com/geektutu/7days-golang +--- + +![golang RPC framework](geerpc/geerpc.jpg) + +## 1 谈谈 RPC 框架 + +RPC(Remote Procedure Call,远程过程调用)是一种计算机通信协议,允许调用不同进程空间的程序。RPC 的客户端和服务器可以在一台机器上,也可以在不同的机器上。程序员使用时,就像调用本地程序一样,无需关注内部的实现细节。 + +不同的应用程序之间的通信方式有很多,比如浏览器和服务器之间广泛使用的基于 HTTP 协议的 Restful API。与 RPC 相比,Restful API 有相对统一的标准,因而更通用,兼容性更好,支持不同的语言。HTTP 协议是基于文本的,一般具备更好的可读性。但是缺点也很明显: + +- Restful 接口需要额外的定义,无论是客户端还是服务端,都需要额外的代码来处理,而 RPC 调用则更接近于直接调用。 +- 基于 HTTP 协议的 Restful 报文冗余,承载了过多的无效信息,而 RPC 通常使用自定义的协议格式,减少冗余报文。 +- RPC 可以采用更高效的序列化协议,将文本转为二进制传输,获得更高的性能。 +- 因为 RPC 的灵活性,所以更容易扩展和集成诸如注册中心、负载均衡等功能。 + +## 2 RPC 框架需要解决什么问题 + +RPC 框架需要解决什么问题?或者我们换一个问题,为什么需要 RPC 框架? + +我们可以想象下两台机器上,两个应用程序之间需要通信,那么首先,需要确定采用的传输协议是什么?如果这个两个应用程序位于不同的机器,那么一般会选择 TCP 协议或者 HTTP 协议;那如果两个应用程序位于相同的机器,也可以选择 Unix Socket 协议。传输协议确定之后,还需要确定报文的编码格式,比如采用最常用的 JSON 或者 XML,那如果报文比较大,还可能会选择 protobuf 等其他的编码方式,甚至编码之后,再进行压缩。接收端获取报文则需要相反的过程,先解压再解码。 + +解决了传输协议和报文编码的问题,接下来还需要解决一系列的可用性问题,例如,连接超时了怎么办?是否支持异步请求和并发? + +如果服务端的实例很多,客户端并不关心这些实例的地址和部署位置,只关心自己能否获取到期待的结果,那就引出了注册中心(registry)和负载均衡(load balance)的问题。简单地说,即客户端和服务端互相不感知对方的存在,服务端启动时将自己注册到注册中心,客户端调用时,从注册中心获取到所有可用的实例,选择一个来调用。这样服务端和客户端只需要感知注册中心的存在就够了。注册中心通常还需要实现服务动态添加、删除,使用心跳确保服务处于可用状态等功能。 + +再进一步,假设服务端是不同的团队提供的,如果没有统一的 RPC 框架,各个团队的服务提供方就需要各自实现一套消息编解码、连接池、收发线程、超时处理等“业务之外”的重复技术劳动,造成整体的低效。因此,“业务之外”的这部分公共的能力,即是 RPC 框架所需要具备的能力。 + +## 3 关于 GeeRPC + +Go 语言广泛地应用于云计算和微服务,成熟的 RPC 框架和微服务框架汗牛充栋。`grpc`、`rpcx`、`go-micro` 等都是非常成熟的框架。一般而言,RPC 是微服务框架的一个子集,微服务框架可以自己实现 RPC 部分,当然,也可以选择不同的 RPC 框架作为通信基座。 + +考虑性能和功能,上述成熟的框架代码量都比较庞大,而且通常和第三方库,例如 `protobuf`、`etcd`、`zookeeper` 等有比较深的耦合,难以直观地窥视框架的本质。GeeRPC 的目的是以最少的代码,实现 RPC 框架中最为重要的部分,帮助大家理解 RPC 框架在设计时需要考虑什么。代码简洁是第一位的,功能是第二位的。 + +因此,GeeRPC 选择从零实现 Go 语言官方的标准库 `net/rpc`,并在此基础上,新增了协议交换(protocol exchange)、注册中心(registry)、服务发现(service discovery)、负载均衡(load balance)、超时处理(timeout processing)等特性。分七天完成,最终代码约 1000 行。 + +## 4 目录 + +- 第一天 - [服务端与消息编码](https://geektutu.com/post/geerpc-day1.html) | [Code](gee-rpc/day1-codec) +- 第二天 - [支持并发与异步的客户端](https://geektutu.com/post/geerpc-day2.html) | [Code](gee-rpc/day2-client) +- 第三天 - [服务注册(service register)](https://geektutu.com/post/geerpc-day3.html) | [Code](gee-rpc/day3-service ) +- 第四天 - [超时处理(timeout)](https://geektutu.com/post/geerpc-day4.html) | [Code](gee-rpc/day4-timeout ) +- 第五天 - [支持HTTP协议](https://geektutu.com/post/geerpc-day5.html) | [Code](gee-rpc/day5-http-debug) +- 第六天 - [负载均衡(load balance)](https://geektutu.com/post/geerpc-day6.html) | [Code](gee-rpc/day6-load-balance) +- 第七天 - [服务发现与注册中心(registry)](https://geektutu.com/post/geerpc-day7.html) | [Code](gee-rpc/day7-registry) + +## 附 推荐阅读 + +- [Go 语言简明教程](https://geektutu.com/post/quick-golang.html) +- [Go 语言笔试面试题](https://geektutu.com/post/qa-golang.html) \ No newline at end of file diff --git a/gee-rpc/doc/geerpc/geerpc.jpg b/gee-rpc/doc/geerpc/geerpc.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5a86ebe270233a2b2aa9ef68335d66f14001c70c GIT binary patch literal 32791 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
yI2GS?B;s3AR>qx!#qndY2* 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 f2kq2pQR*OpQ6{`o8t&8O!=@cjP+8;sPr literal 0 HcmV?d00001 From 298ce560a496da66c93c1d3f80e9650ee89720b8 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Wed, 7 Oct 2020 00:49:17 +0800 Subject: [PATCH 091/122] gee-rpc: gob.go use err = to replace err := --- gee-rpc/day1-codec/codec/gob.go | 14 +++++++------- gee-rpc/day2-client/codec/gob.go | 14 +++++++------- gee-rpc/day3-service/codec/gob.go | 10 +++++----- gee-rpc/day4-timeout/codec/gob.go | 10 +++++----- gee-rpc/day5-http-debug/codec/gob.go | 10 +++++----- gee-rpc/day6-load-balance/codec/gob.go | 10 +++++----- gee-rpc/day7-registry/codec/gob.go | 10 +++++----- 7 files changed, 39 insertions(+), 39 deletions(-) diff --git a/gee-rpc/day1-codec/codec/gob.go b/gee-rpc/day1-codec/codec/gob.go index e4b2a67..d9ef2e6 100644 --- a/gee-rpc/day1-codec/codec/gob.go +++ b/gee-rpc/day1-codec/codec/gob.go @@ -41,15 +41,15 @@ func (c *GobCodec) Write(h *Header, body interface{}) (err error) { _ = c.Close() } }() - if err := c.enc.Encode(h); err != nil { - log.Println("rpc codec: gob error encoding header:", err) - return err + if err = c.enc.Encode(h); err != nil { + log.Println("rpc: gob error encoding header:", err) + return } - if err := c.enc.Encode(body); err != nil { - log.Println("rpc codec: gob error encoding body:", err) - return err + if err = c.enc.Encode(body); err != nil { + log.Println("rpc: gob error encoding body:", err) + return } - return nil + return } func (c *GobCodec) Close() error { diff --git a/gee-rpc/day2-client/codec/gob.go b/gee-rpc/day2-client/codec/gob.go index e4b2a67..d9ef2e6 100644 --- a/gee-rpc/day2-client/codec/gob.go +++ b/gee-rpc/day2-client/codec/gob.go @@ -41,15 +41,15 @@ func (c *GobCodec) Write(h *Header, body interface{}) (err error) { _ = c.Close() } }() - if err := c.enc.Encode(h); err != nil { - log.Println("rpc codec: gob error encoding header:", err) - return err + if err = c.enc.Encode(h); err != nil { + log.Println("rpc: gob error encoding header:", err) + return } - if err := c.enc.Encode(body); err != nil { - log.Println("rpc codec: gob error encoding body:", err) - return err + if err = c.enc.Encode(body); err != nil { + log.Println("rpc: gob error encoding body:", err) + return } - return nil + return } func (c *GobCodec) Close() error { diff --git a/gee-rpc/day3-service/codec/gob.go b/gee-rpc/day3-service/codec/gob.go index 808d97b..d9ef2e6 100644 --- a/gee-rpc/day3-service/codec/gob.go +++ b/gee-rpc/day3-service/codec/gob.go @@ -41,15 +41,15 @@ func (c *GobCodec) Write(h *Header, body interface{}) (err error) { _ = c.Close() } }() - if err := c.enc.Encode(h); err != nil { + if err = c.enc.Encode(h); err != nil { log.Println("rpc: gob error encoding header:", err) - return err + return } - if err := c.enc.Encode(body); err != nil { + if err = c.enc.Encode(body); err != nil { log.Println("rpc: gob error encoding body:", err) - return err + return } - return nil + return } func (c *GobCodec) Close() error { diff --git a/gee-rpc/day4-timeout/codec/gob.go b/gee-rpc/day4-timeout/codec/gob.go index 808d97b..d9ef2e6 100644 --- a/gee-rpc/day4-timeout/codec/gob.go +++ b/gee-rpc/day4-timeout/codec/gob.go @@ -41,15 +41,15 @@ func (c *GobCodec) Write(h *Header, body interface{}) (err error) { _ = c.Close() } }() - if err := c.enc.Encode(h); err != nil { + if err = c.enc.Encode(h); err != nil { log.Println("rpc: gob error encoding header:", err) - return err + return } - if err := c.enc.Encode(body); err != nil { + if err = c.enc.Encode(body); err != nil { log.Println("rpc: gob error encoding body:", err) - return err + return } - return nil + return } func (c *GobCodec) Close() error { diff --git a/gee-rpc/day5-http-debug/codec/gob.go b/gee-rpc/day5-http-debug/codec/gob.go index 808d97b..d9ef2e6 100644 --- a/gee-rpc/day5-http-debug/codec/gob.go +++ b/gee-rpc/day5-http-debug/codec/gob.go @@ -41,15 +41,15 @@ func (c *GobCodec) Write(h *Header, body interface{}) (err error) { _ = c.Close() } }() - if err := c.enc.Encode(h); err != nil { + if err = c.enc.Encode(h); err != nil { log.Println("rpc: gob error encoding header:", err) - return err + return } - if err := c.enc.Encode(body); err != nil { + if err = c.enc.Encode(body); err != nil { log.Println("rpc: gob error encoding body:", err) - return err + return } - return nil + return } func (c *GobCodec) Close() error { diff --git a/gee-rpc/day6-load-balance/codec/gob.go b/gee-rpc/day6-load-balance/codec/gob.go index 808d97b..d9ef2e6 100644 --- a/gee-rpc/day6-load-balance/codec/gob.go +++ b/gee-rpc/day6-load-balance/codec/gob.go @@ -41,15 +41,15 @@ func (c *GobCodec) Write(h *Header, body interface{}) (err error) { _ = c.Close() } }() - if err := c.enc.Encode(h); err != nil { + if err = c.enc.Encode(h); err != nil { log.Println("rpc: gob error encoding header:", err) - return err + return } - if err := c.enc.Encode(body); err != nil { + if err = c.enc.Encode(body); err != nil { log.Println("rpc: gob error encoding body:", err) - return err + return } - return nil + return } func (c *GobCodec) Close() error { diff --git a/gee-rpc/day7-registry/codec/gob.go b/gee-rpc/day7-registry/codec/gob.go index 808d97b..d9ef2e6 100644 --- a/gee-rpc/day7-registry/codec/gob.go +++ b/gee-rpc/day7-registry/codec/gob.go @@ -41,15 +41,15 @@ func (c *GobCodec) Write(h *Header, body interface{}) (err error) { _ = c.Close() } }() - if err := c.enc.Encode(h); err != nil { + if err = c.enc.Encode(h); err != nil { log.Println("rpc: gob error encoding header:", err) - return err + return } - if err := c.enc.Encode(body); err != nil { + if err = c.enc.Encode(body); err != nil { log.Println("rpc: gob error encoding body:", err) - return err + return } - return nil + return } func (c *GobCodec) Close() error { From 6366346ed99323cbeced9b79b942aa9a4ad18c42 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Wed, 7 Oct 2020 17:35:12 +0800 Subject: [PATCH 092/122] gee-rpc client move call.Seq = seq to registerCall --- gee-rpc/day1-codec/main/main.go | 1 + gee-rpc/day2-client/client.go | 7 +- gee-rpc/day2-client/main/main.go | 1 + gee-rpc/day3-service/client.go | 7 +- gee-rpc/day3-service/main/main.go | 1 + gee-rpc/day4-timeout/client.go | 7 +- gee-rpc/day4-timeout/main/main.go | 1 + gee-rpc/day5-http-debug/client.go | 7 +- gee-rpc/day5-http-debug/main/main.go | 1 + gee-rpc/day6-load-balance/client.go | 7 +- gee-rpc/day6-load-balance/main/main.go | 1 + gee-rpc/day7-registry/client.go | 7 +- gee-rpc/day7-registry/main/main.go | 1 + gee-rpc/doc/geerpc-day1.md | 440 +++++++++++++++++++++++++ gee-rpc/doc/geerpc-day2.md | 397 ++++++++++++++++++++++ gee-rpc/doc/geerpc.md | 2 +- 16 files changed, 863 insertions(+), 25 deletions(-) create mode 100644 gee-rpc/doc/geerpc-day1.md create mode 100644 gee-rpc/doc/geerpc-day2.md diff --git a/gee-rpc/day1-codec/main/main.go b/gee-rpc/day1-codec/main/main.go index 948b763..2bc6a8a 100644 --- a/gee-rpc/day1-codec/main/main.go +++ b/gee-rpc/day1-codec/main/main.go @@ -22,6 +22,7 @@ func startServer(addr chan string) { } func main() { + log.SetFlags(0) addr := make(chan string) go startServer(addr) diff --git a/gee-rpc/day2-client/client.go b/gee-rpc/day2-client/client.go index 3df6ec9..1c8a3cb 100644 --- a/gee-rpc/day2-client/client.go +++ b/gee-rpc/day2-client/client.go @@ -73,10 +73,10 @@ func (client *Client) registerCall(call *Call) (uint64, error) { if client.closing || client.shutdown { return 0, ErrShutdown } - seq := client.seq - client.pending[seq] = call + call.Seq = client.seq + client.pending[call.Seq] = call client.seq++ - return seq, nil + return call.Seq, nil } func (client *Client) removeCall(seq uint64) *Call { @@ -106,7 +106,6 @@ func (client *Client) send(call *Call) { // register this call. seq, err := client.registerCall(call) - call.Seq = seq if err != nil { call.Error = err call.done() diff --git a/gee-rpc/day2-client/main/main.go b/gee-rpc/day2-client/main/main.go index 8502fe9..099eb50 100644 --- a/gee-rpc/day2-client/main/main.go +++ b/gee-rpc/day2-client/main/main.go @@ -21,6 +21,7 @@ func startServer(addr chan string) { } func main() { + log.SetFlags(0) addr := make(chan string) go startServer(addr) client, _ := geerpc.Dial("tcp", <-addr) diff --git a/gee-rpc/day3-service/client.go b/gee-rpc/day3-service/client.go index 3df6ec9..1c8a3cb 100644 --- a/gee-rpc/day3-service/client.go +++ b/gee-rpc/day3-service/client.go @@ -73,10 +73,10 @@ func (client *Client) registerCall(call *Call) (uint64, error) { if client.closing || client.shutdown { return 0, ErrShutdown } - seq := client.seq - client.pending[seq] = call + call.Seq = client.seq + client.pending[call.Seq] = call client.seq++ - return seq, nil + return call.Seq, nil } func (client *Client) removeCall(seq uint64) *Call { @@ -106,7 +106,6 @@ func (client *Client) send(call *Call) { // register this call. seq, err := client.registerCall(call) - call.Seq = seq if err != nil { call.Error = err call.done() diff --git a/gee-rpc/day3-service/main/main.go b/gee-rpc/day3-service/main/main.go index 0f0b668..89add53 100644 --- a/gee-rpc/day3-service/main/main.go +++ b/gee-rpc/day3-service/main/main.go @@ -33,6 +33,7 @@ func startServer(addr chan string) { } func main() { + log.SetFlags(0) addr := make(chan string) go startServer(addr) client, _ := geerpc.Dial("tcp", <-addr) diff --git a/gee-rpc/day4-timeout/client.go b/gee-rpc/day4-timeout/client.go index b301647..45fc9e6 100644 --- a/gee-rpc/day4-timeout/client.go +++ b/gee-rpc/day4-timeout/client.go @@ -75,10 +75,10 @@ func (client *Client) registerCall(call *Call) (uint64, error) { if client.closing || client.shutdown { return 0, ErrShutdown } - seq := client.seq - client.pending[seq] = call + call.Seq = client.seq + client.pending[call.Seq] = call client.seq++ - return seq, nil + return call.Seq, nil } func (client *Client) removeCall(seq uint64) *Call { @@ -108,7 +108,6 @@ func (client *Client) send(call *Call) { // register this call. seq, err := client.registerCall(call) - call.Seq = seq if err != nil { call.Error = err call.done() diff --git a/gee-rpc/day4-timeout/main/main.go b/gee-rpc/day4-timeout/main/main.go index 9693eb5..efcf75c 100644 --- a/gee-rpc/day4-timeout/main/main.go +++ b/gee-rpc/day4-timeout/main/main.go @@ -34,6 +34,7 @@ func startServer(addr chan string) { } func main() { + log.SetFlags(0) addr := make(chan string) go startServer(addr) client, _ := geerpc.Dial("tcp", <-addr) diff --git a/gee-rpc/day5-http-debug/client.go b/gee-rpc/day5-http-debug/client.go index 795a18b..bdc2c0f 100644 --- a/gee-rpc/day5-http-debug/client.go +++ b/gee-rpc/day5-http-debug/client.go @@ -78,10 +78,10 @@ func (client *Client) registerCall(call *Call) (uint64, error) { if client.closing || client.shutdown { return 0, ErrShutdown } - seq := client.seq - client.pending[seq] = call + call.Seq = client.seq + client.pending[call.Seq] = call client.seq++ - return seq, nil + return call.Seq, nil } func (client *Client) removeCall(seq uint64) *Call { @@ -111,7 +111,6 @@ func (client *Client) send(call *Call) { // register this call. seq, err := client.registerCall(call) - call.Seq = seq if err != nil { call.Error = err call.done() diff --git a/gee-rpc/day5-http-debug/main/main.go b/gee-rpc/day5-http-debug/main/main.go index 6499b53..cfd1b88 100644 --- a/gee-rpc/day5-http-debug/main/main.go +++ b/gee-rpc/day5-http-debug/main/main.go @@ -51,6 +51,7 @@ func call(addrCh chan string) { } func main() { + log.SetFlags(0) ch := make(chan string) go call(ch) startServer(ch) diff --git a/gee-rpc/day6-load-balance/client.go b/gee-rpc/day6-load-balance/client.go index 795a18b..bdc2c0f 100644 --- a/gee-rpc/day6-load-balance/client.go +++ b/gee-rpc/day6-load-balance/client.go @@ -78,10 +78,10 @@ func (client *Client) registerCall(call *Call) (uint64, error) { if client.closing || client.shutdown { return 0, ErrShutdown } - seq := client.seq - client.pending[seq] = call + call.Seq = client.seq + client.pending[call.Seq] = call client.seq++ - return seq, nil + return call.Seq, nil } func (client *Client) removeCall(seq uint64) *Call { @@ -111,7 +111,6 @@ func (client *Client) send(call *Call) { // register this call. seq, err := client.registerCall(call) - call.Seq = seq if err != nil { call.Error = err call.done() diff --git a/gee-rpc/day6-load-balance/main/main.go b/gee-rpc/day6-load-balance/main/main.go index d00f864..550e848 100644 --- a/gee-rpc/day6-load-balance/main/main.go +++ b/gee-rpc/day6-load-balance/main/main.go @@ -84,6 +84,7 @@ func broadcast(addr1, addr2 string) { } func main() { + log.SetFlags(0) ch1 := make(chan string) ch2 := make(chan string) // start two servers diff --git a/gee-rpc/day7-registry/client.go b/gee-rpc/day7-registry/client.go index 795a18b..bdc2c0f 100644 --- a/gee-rpc/day7-registry/client.go +++ b/gee-rpc/day7-registry/client.go @@ -78,10 +78,10 @@ func (client *Client) registerCall(call *Call) (uint64, error) { if client.closing || client.shutdown { return 0, ErrShutdown } - seq := client.seq - client.pending[seq] = call + call.Seq = client.seq + client.pending[call.Seq] = call client.seq++ - return seq, nil + return call.Seq, nil } func (client *Client) removeCall(seq uint64) *Call { @@ -111,7 +111,6 @@ func (client *Client) send(call *Call) { // register this call. seq, err := client.registerCall(call) - call.Seq = seq if err != nil { call.Error = err call.done() diff --git a/gee-rpc/day7-registry/main/main.go b/gee-rpc/day7-registry/main/main.go index 0776312..50b57dd 100644 --- a/gee-rpc/day7-registry/main/main.go +++ b/gee-rpc/day7-registry/main/main.go @@ -94,6 +94,7 @@ func broadcast(registry string) { } func main() { + log.SetFlags(0) registryAddr := "http://localhost:9999/_geerpc_/registry" var wg sync.WaitGroup wg.Add(1) diff --git a/gee-rpc/doc/geerpc-day1.md b/gee-rpc/doc/geerpc-day1.md new file mode 100644 index 0000000..abfeb3c --- /dev/null +++ b/gee-rpc/doc/geerpc-day1.md @@ -0,0 +1,440 @@ +--- +title: 动手写RPC框架 - GeeRPC第一天 服务端与消息编码 +date: 2020-10-06 17:00:00 +description: 7天用 Go语言/golang 从零实现 RPC 框架 GeeRPC 教程(7 days implement golang remote procedure call framework from scratch tutorial),动手写 RPC 框架,参照 golang 标准库 net/rpc 的实现,实现了服务端(server)、支持异步和并发的客户端(client)、消息编码与解码(message encoding and decoding)、服务注册(service register)、支持 TCP/Unix/HTTP 等多种传输协议。第一天实现了一个简单的服务端和消息的编码与解码。 +tags: +- Go +nav: 从零实现 +categories: +- RPC框架 - GeeRPC +keywords: +- Go语言 +- 从零实现RPC框架 +- Codec +- 序列化 +- 反序列化 +image: post/geerpc/geerpc.jpg +github: https://github.com/geektutu/7days-golang +--- + +![golang RPC framework](geerpc/geerpc.jpg) + +本文是[7天用Go从零实现RPC框架GeeRPC]的第一篇。 + +- 使用 `encoding/gob` 实现消息的编解码(序列化与反序列化) +- 实现一个简易的服务端,仅接受消息,不处理,代码约 200 行 + + +## 消息的序列化与反序列化 + +一个典型的 RPC 调用如下: + +```go +err = client.Call("Arith.Multiply", args, &reply) +``` + +客户端发送的请求包括服务名 `Arith`,方法名 `Multiply`,参数 `args` 三个,服务端的响应包括错误 `error`,返回值 `reply` 2 个。我们将请求和响应中的参数和返回值抽象为 body,剩余的信息放在 header 中,那么就可以抽象出数据结构 Header: + +[day1-codec/codec/codec.go](https://github.com/geektutu/7days-golang/tree/master/gee-rpc/day1-codec) + +```go +package codec + +import "io" + +type Header struct { + ServiceMethod string // format "Service.Method" + Seq uint64 // sequence number chosen by client + Error string +} +``` + +- ServiceMethod 是服务名和方法名,通常与 Go 语言中的结构体和方法相映射。 +- Seq 是请求的序号,也可以认为是某个请求的 ID,用来区分不同的请求。 +- Error 是错误信息,客户端置为空,服务端如果如果发生错误,将错误信息置于 Error 中。 + + +我们将和消息编解码相关的代码都防到 codec 目录中。 + +进一步,抽象出对消息体进行编解码的接口 Codec,抽象出接口是为了实现不同的 Codec 实例: + +```go +type Codec interface { + io.Closer + ReadHeader(*Header) error + ReadBody(interface{}) error + Write(*Header, interface{}) error +} +``` + +紧接着,抽象出 Codec 的构造函数,客户端和服务端可以通过 Codec 的 `Type` 得到构造函数,从而创建 Codec 实例。这部分代码和工厂模式类似,与工厂模式不同的是,返回的是构造函数,而非实例。 + +```go +type NewCodecFunc func(io.ReadWriteCloser) Codec + +type Type string + +const ( + GobType Type = "application/gob" + JsonType Type = "application/json" // not implemented +) + +var NewCodecFuncMap map[Type]NewCodecFunc + +func init() { + NewCodecFuncMap = make(map[Type]NewCodecFunc) + NewCodecFuncMap[GobType] = NewGobCodec +} +``` + +我们定义了 2 种 Codec,`Gob` 和 `Json`,但是实际代码中只实现了 `Gob` 一种,事实上,2 者的实现非常接近,甚至只需要把 `gob` 换成 `json` 即可。 + +首先定义 `GobCodec` 结构体,这个结构体由四部分构成,`conn` 是由构建函数传入,通常是通过 TCP 或者 Unix 建立 socket 时得到的链接实例,dec 和 enc 对应 gob 的 Decoder 和 Encoder,buf 是为了防止阻塞而创建的带缓冲的 `Writer`,一般这么做能提升性能。 + +[day1-codec/codec/gob.go](https://github.com/geektutu/7days-golang/tree/master/gee-rpc/day1-codec) + +```go +package codec + +import ( + "bufio" + "encoding/gob" + "io" + "log" +) + +type GobCodec struct { + conn io.ReadWriteCloser + buf *bufio.Writer + dec *gob.Decoder + enc *gob.Encoder +} + +var _ Codec = (*GobCodec)(nil) + +func NewGobCodec(conn io.ReadWriteCloser) Codec { + buf := bufio.NewWriter(conn) + return &GobCodec{ + conn: conn, + buf: buf, + dec: gob.NewDecoder(conn), + enc: gob.NewEncoder(buf), + } +} +``` + +接着实现 `ReadHeader`、`ReadBody`、`Write` 和 `Close` 方法。 + +```go +func (c *GobCodec) ReadHeader(h *Header) error { + return c.dec.Decode(h) +} + +func (c *GobCodec) ReadBody(body interface{}) error { + return c.dec.Decode(body) +} + +func (c *GobCodec) Write(h *Header, body interface{}) (err error) { + defer func() { + _ = c.buf.Flush() + if err != nil { + _ = c.Close() + } + }() + if err := c.enc.Encode(h); err != nil { + log.Println("rpc codec: gob error encoding header:", err) + return err + } + if err := c.enc.Encode(body); err != nil { + log.Println("rpc codec: gob error encoding body:", err) + return err + } + return nil +} + +func (c *GobCodec) Close() error { + return c.conn.Close() +} +``` + +## 通信过程 + +客户端与服务端的通信需要协商一些内容,例如 HTTP 报文,分为 header 和 body 2 部分,body 的格式和长度通过 header 中的 `Content-Type` 和 `Content-Length` 指定,服务端通过解析 header 就能够知道如何从 body 中读取需要的信息。对于 RPC 协议来说,这部分协商是需要自主设计的。为了提升性能,一般在报文的最开始会规划固定的字节,来协商相关的信息。比如第1个字节用来表示序列化方式,第2个字节表示压缩方式,第3-6字节表示 header 的长度,7-10 字节表示 body 的长度。 + +对于 GeeRPC 来说,目前需要协商的唯一一项内容是消息的编解码方式。我们将这部分信息,放到结构体 `Option` 中承载。目前,已经进入到服务端的实现阶段了。 + +[day1-codec/server.go](https://github.com/geektutu/7days-golang/tree/master/gee-rpc/day1-codec) + +```go +package geerpc + +const MagicNumber = 0x3bef5c + +type Option struct { + MagicNumber int // MagicNumber marks this's a geerpc request + CodecType codec.Type // client may choose different Codec to encode body +} + +var DefaultOption = &Option{ + MagicNumber: MagicNumber, + CodecType: codec.GobType, +} +``` + +一般来说,涉及协议协商的这部分信息,需要设计固定的字节来传输的。但是为了实现上更简单,GeeRPC 客户端固定采用 JSON 编码 Option,后续的 header 和 body 的编码方式由 Option 中的 CodeType 指定,服务端首先使用 JSON 解码 Option,然后通过 Option 得 CodeType 解码剩余的内容。即报文将以这样的形式发送: + +```bash +| Option{MagicNumber: xxx, CodecType: xxx} | Header{ServiceMethod ...} | Body interface{} | +| <------ 固定 JSON 编码 ------> | <------- 编码方式由 CodeType 决定 ------->| +``` + +在一次连接中,Option 固定在报文的最开始,Header 和 Body 可以有多个,即报文可能是这样的。 + +```go +| Option | Header1 | Body1 | Header2 | Body2 | ... +``` + +## 服务端的实现 + +通信过程已经定义清楚了,那么服务端的实现就比较直接了。 + +[day1-codec/server.go](https://github.com/geektutu/7days-golang/tree/master/gee-rpc/day1-codec) + +```go +// Server represents an RPC Server. +type Server struct{} + +// NewServer returns a new Server. +func NewServer() *Server { + return &Server{} +} + +// DefaultServer is the default instance of *Server. +var DefaultServer = NewServer() + +// Accept accepts connections on the listener and serves requests +// for each incoming connection. +func (server *Server) Accept(lis net.Listener) { + for { + conn, err := lis.Accept() + if err != nil { + log.Println("rpc server: accept error:", err) + return + } + go server.ServeConn(conn) + } +} + +// Accept accepts connections on the listener and serves requests +// for each incoming connection. +func Accept(lis net.Listener) { DefaultServer.Accept(lis) } +``` + +- 首先定义了结构体 `Server`,没有任何的成员字段。 +- 实现了 `Accept` 方式,`net.Listener` 作为参数,for 循环等待 socket 连接建立,并开启子协程处理,处理过程交给了 `ServerConn` 方法。 +- DefaultServer 是一个默认的 `Server` 实例,主要为了用户使用方便。 + +如果想启动服务,过程是非常简单的,传入 listener 即可,tcp 协议和 unix 协议都支持。 + +```go +lis, _ := net.Listen("tcp", ":9999") +geerpc.Accept(lis) +``` + +`ServeConn` 的实现就和之前讨论的通信过程紧密相关了,首先使用 `json.NewDecoder` 反序列化得到 Option 实例,检查 MagicNumber 和 CodeType 的值是否正确。然后根据 CodeType 得到对应的消息编解码器,接下来的处理交给 `serverCodec`。 + +```go +// ServeConn runs the server on a single connection. +// ServeConn blocks, serving the connection until the client hangs up. +func (server *Server) ServeConn(conn io.ReadWriteCloser) { + defer func() { _ = conn.Close() }() + var opt Option + if err := json.NewDecoder(conn).Decode(&opt); err != nil { + log.Println("rpc server: options error: ", err) + return + } + if opt.MagicNumber != MagicNumber { + log.Printf("rpc server: invalid magic number %x", opt.MagicNumber) + return + } + f := codec.NewCodecFuncMap[opt.CodecType] + if f == nil { + log.Printf("rpc server: invalid codec type %s", opt.CodecType) + return + } + server.serveCodec(f(conn)) +} + +// invalidRequest is a placeholder for response argv when error occurs +var invalidRequest = struct{}{} + +func (server *Server) serveCodec(cc codec.Codec) { + sending := new(sync.Mutex) // make sure to send a complete response + wg := new(sync.WaitGroup) // wait until all request are handled + for { + req, err := server.readRequest(cc) + if err != nil { + if req == nil { + break // it's not possible to recover, so close the connection + } + req.h.Error = err.Error() + server.sendResponse(cc, req.h, invalidRequest, sending) + continue + } + wg.Add(1) + go server.handleRequest(cc, req, sending, wg) + } + wg.Wait() + _ = cc.Close() +} +``` + +`serveCodec` 的过程非常简单。主要包含三个阶段 + +- 读取请求 readRequest +- 处理请求 handleRequest +- 回复请求 sendResponse + +之前提到过,在一次连接中,允许接收多个请求,即多个 request header 和 request body,因此这里使用了 for 无限制地等待请求的到来,直到发生错误(例如连接被关闭,接收到的报文有问题等),这里需要注意的点有三个: + +- handleRequest 使用了协程并发执行请求。 +- 处理请求是并发的,但是回复请求的报文必须是逐个发送的,并发容易导致多个回复报文交织在一起,客户端无法解析。在这里使用锁(sending)保证。 +- 尽力而为,只有在 header 解析失败时,才终止循环。 + +```go +// request stores all information of a call +type request struct { + h *codec.Header // header of request + argv, replyv reflect.Value // argv and replyv of request +} + +func (server *Server) readRequestHeader(cc codec.Codec) (*codec.Header, error) { + var h codec.Header + if err := cc.ReadHeader(&h); err != nil { + if err != io.EOF && err != io.ErrUnexpectedEOF { + log.Println("rpc server: read header error:", err) + } + return nil, err + } + return &h, nil +} + +func (server *Server) readRequest(cc codec.Codec) (*request, error) { + h, err := server.readRequestHeader(cc) + if err != nil { + return nil, err + } + req := &request{h: h} + // TODO: now we don't know the type of request argv + // day 1, just suppose it's string + req.argv = reflect.New(reflect.TypeOf("")) + if err = cc.ReadBody(req.argv.Interface()); err != nil { + log.Println("rpc server: read argv err:", err) + } + return req, nil +} + +func (server *Server) sendResponse(cc codec.Codec, h *codec.Header, body interface{}, sending *sync.Mutex) { + sending.Lock() + defer sending.Unlock() + if err := cc.Write(h, body); err != nil { + log.Println("rpc server: write response error:", err) + } +} + +func (server *Server) handleRequest(cc codec.Codec, req *request, sending *sync.Mutex, wg *sync.WaitGroup) { + // TODO, should call registered rpc methods to get the right replyv + // day 1, just print argv and send a hello message + defer wg.Done() + log.Println(req.h, req.argv.Elem()) + req.replyv = reflect.ValueOf(fmt.Sprintf("geerpc resp %d", req.h.Seq)) + server.sendResponse(cc, req.h, req.replyv.Interface(), sending) +} +``` + +目前还不能判断 body 的类型,因此在 readRequest 和 handleRequest 中,day1 将 body 作为字符串处理。接收到请求,打印 header,并回复 `geerpc resp ${req.h.Seq}`。这一部分后续再实现。 + + +## main 函数(一个简易的客户端) + +day1 的内容就到此为止了,在这里我们已经实现了一个消息的编解码器 `GobCodec`,并且客户端与服务端实现了简单的协议交换(protocol exchange),即允许客户端使用不同的编码方式。同时实现了服务端的雏形,建立连接,读取、处理并回复客户端的请求。 + +接下来,我们就在 main 函数中看看如何使用刚实现的 GeeRPC 吧。 + +[day1-codec/main/main.go](https://github.com/geektutu/7days-golang/tree/master/gee-rpc/day1-codec) + +```go +package main + +import ( + "encoding/json" + "fmt" + "geerpc" + "geerpc/codec" + "log" + "net" + "time" +) + +func startServer(addr chan string) { + // pick a free port + l, err := net.Listen("tcp", ":0") + if err != nil { + log.Fatal("network error:", err) + } + log.Println("start rpc server on", l.Addr()) + addr <- l.Addr().String() + geerpc.Accept(l) +} + +func main() { + addr := make(chan string) + go startServer(addr) + + // in fact, following code is like a simple geerpc client + conn, _ := net.Dial("tcp", <-addr) + defer func() { _ = conn.Close() }() + + time.Sleep(time.Second) + // send options + _ = json.NewEncoder(conn).Encode(geerpc.DefaultOption) + cc := codec.NewGobCodec(conn) + // send request & receive response + for i := 0; i < 5; i++ { + h := &codec.Header{ + ServiceMethod: "Foo.Sum", + Seq: uint64(i), + } + _ = cc.Write(h, fmt.Sprintf("geerpc req %d", h.Seq)) + _ = cc.ReadHeader(h) + var reply string + _ = cc.ReadBody(&reply) + log.Println("reply:", reply) + } +} +``` + +- 在 `startServer` 中使用了信道 `addr`,确保服务端端口监听成功,客户端再发起请求。 +- 客户端首先发送 `Option` 进行协议交换,接下来发送消息头 `h := &codec.Header{}`,和消息体 `geerpc req ${h.Seq}`。 +- 最后解析服务端的响应 `reply`,并打印出来。 + +执行结果如下: + +```bash +start rpc server on [::]:63662 +&{Foo.Sum 0 } geerpc req 0 +reply: geerpc resp 0 +&{Foo.Sum 1 } geerpc req 1 +reply: geerpc resp 1 +&{Foo.Sum 2 } geerpc req 2 +reply: geerpc resp 2 +&{Foo.Sum 3 } geerpc req 3 +reply: geerpc resp 3 +&{Foo.Sum 4 } geerpc req 4 +reply: geerpc resp 4 +``` + +## 附 推荐阅读 + +- [Go 语言简明教程](https://geektutu.com/post/quick-golang.html) +- [Go 语言笔试面试题](https://geektutu.com/post/qa-golang.html) \ No newline at end of file diff --git a/gee-rpc/doc/geerpc-day2.md b/gee-rpc/doc/geerpc-day2.md new file mode 100644 index 0000000..2d356e3 --- /dev/null +++ b/gee-rpc/doc/geerpc-day2.md @@ -0,0 +1,397 @@ +--- +title: 动手写RPC框架 - GeeRPC第二天 支持并发与异步的客户端 +date: 2020-10-07 18:00:00 +description: 7天用 Go语言/golang 从零实现 RPC 框架 GeeRPC 教程(7 days implement golang remote procedure call framework from scratch tutorial),动手写 RPC 框架,参照 golang 标准库 net/rpc 的实现,实现了服务端(server)、支持异步和并发的客户端(client)、消息编码与解码(message encoding and decoding)、服务注册(service register)、支持 TCP/Unix/HTTP 等多种传输协议。第二天实现了一个支持异步(asynchronous)和并发(concurrent)的客户端。 +tags: +- Go +nav: 从零实现 +categories: +- RPC框架 - GeeRPC +keywords: +- Go语言 +- 从零实现RPC框架 +- 客户端 +- 异步 +- 并发 +image: post/geerpc/geerpc.jpg +github: https://github.com/geektutu/7days-golang +--- + +![golang RPC framework](geerpc/geerpc.jpg) + +本文是[7天用Go从零实现RPC框架GeeRPC]的第二篇。 + +- 实现一个支持异步和并发的高性能客户端,代码约 250 行 + + +## Call 的设计 + +对 `net/rpc` 而言,一个函数需要能够被远程调用,需要满足如下五个条件: + +- the method's type is exported. +- the method is exported. +- the method has two arguments, both exported (or builtin) types. +- the method's second argument is a pointer. +- the method has return type error. + +更直观一些: + +```go +func (t *T) MethodName(argType T1, replyType *T2) error +``` + +根据上述要求,首先我们封装了结构体 Call 来承载一次 RPC 调用所需要的信息。 + +[day2-client/client.go](https://github.com/geektutu/7days-golang/tree/master/gee-rpc/day2-client) + +```go +// Call represents an active RPC. +type Call struct { + Seq uint64 + ServiceMethod string // format "." + Args interface{} // arguments to the function + Reply interface{} // reply from the function + Error error // if error occurs, it will be set + Done chan *Call // Strobes when call is complete. +} + +func (call *Call) done() { + call.Done <- call +} +``` + +为了支持异步调用,Call 结构体中添加了一个字段 Done,Done 的类型是 `chan *Call`,当调用结束时,会调用 `call.done()` 通知调用方。 + + +## 实现 Client + +接下来,我们将实现 GeeRPC 客户端最核心的部分 Client。 + +```go +// Client represents an RPC Client. +// There may be multiple outstanding Calls associated +// with a single Client, and a Client may be used by +// multiple goroutines simultaneously. +type Client struct { + cc codec.Codec + opt *Option + sending sync.Mutex // protect following + header codec.Header + mu sync.Mutex // protect following + seq uint64 + pending map[uint64]*Call + closing bool // user has called Close + shutdown bool // server has told us to stop +} + +var _ io.Closer = (*Client)(nil) + +var ErrShutdown = errors.New("connection is shut down") + +// Close the connection +func (client *Client) Close() error { + client.mu.Lock() + defer client.mu.Unlock() + if client.closing { + return ErrShutdown + } + client.closing = true + return client.cc.Close() +} + +// IsAvailable return true if the client does work +func (client *Client) IsAvailable() bool { + client.mu.Lock() + defer client.mu.Unlock() + return !client.shutdown && !client.closing +} +``` + +Client 的字段比较复杂: + +- cc 是消息的编解码器,和服务端类似,用来序列化将要发送出去的请求,以及反序列化接收到的响应。 +- sending 是一个互斥锁,和服务端类似,为了保证请求的有序发送,即防止出现多个请求报文混淆。 +- header 是每个请求的消息头,header 只有在请求发送时才需要,而请求发送是互斥的,因此每个客户端只需要一个,声明在 Client 结构体中可以复用。 +- seq 用于给发送的请求编号,每个请求拥有唯一编号。 +- pending 存储未处理完的请求,键是编号,值是 Call 实例。 +- closing 和 shutdown 任意一个值置为 true,则表示 Client 处于不可用的状态,但有些许的差别,closing 是用户主动关闭的,即调用 `Close` 方法,而 shutdown 置为 true 一般是有错误发生。 + +紧接着,实现和 Call 相关的三个方法。 + +```go +func (client *Client) registerCall(call *Call) (uint64, error) { + client.mu.Lock() + defer client.mu.Unlock() + if client.closing || client.shutdown { + return 0, ErrShutdown + } + call.Seq = client.seq + client.pending[call.Seq] = call + client.seq++ + return call.Seq, nil +} + +func (client *Client) removeCall(seq uint64) *Call { + client.mu.Lock() + defer client.mu.Unlock() + call := client.pending[seq] + delete(client.pending, seq) + return call +} + +func (client *Client) terminateCalls(err error) { + client.sending.Lock() + defer client.sending.Unlock() + client.mu.Lock() + defer client.mu.Unlock() + client.shutdown = true + for _, call := range client.pending { + call.Error = err + call.done() + } +} +``` + +- registerCall:将参数 call 添加到 client.pending 中,并更新 client.seq。 +- removeCall:根据 seq,从 client.pending 中移除对应的 call,并返回。 +- terminateCalls:服务端或客户端发生错误时调用,将 shutdown 设置为 true,且将错误信息通知所有 pending 状态的 call。 + +对一个客户端端来说,接收响应、发送请求是最重要的 2 个功能。那么首先实现接收功能,接收到的响应有三种情况: + +- call 不存在,可能是请求没有发送完整,或者因为其他原因被取消,但是服务端仍旧处理了。 +- call 存在,但服务端处理出错,即 h.Error 不为空。 +- call 存在,服务端处理正常,那么需要从 body 中读取 Reply 的值。 + +```go +func (client *Client) receive() { + var err error + for err == nil { + var h codec.Header + if err = client.cc.ReadHeader(&h); err != nil { + break + } + call := client.removeCall(h.Seq) + switch { + case call == nil: + // it usually means that Write partially failed + // and call was already removed. + err = client.cc.ReadBody(nil) + case h.Error != "": + call.Error = fmt.Errorf(h.Error) + err = client.cc.ReadBody(nil) + call.done() + default: + err = client.cc.ReadBody(call.Reply) + if err != nil { + call.Error = errors.New("reading body " + err.Error()) + } + call.done() + } + } + // error occurs, so terminateCalls pending calls + client.terminateCalls(err) +} +``` + +创建 Client 实例时,首先需要完成一开始的协议交换,即发送 `Option` 信息给服务端。协商好消息的编解码方式之后,再创建一个子协程调用 `receive()` 接收响应。 + +```go +func NewClient(conn net.Conn, opt *Option) (*Client, error) { + f := codec.NewCodecFuncMap[opt.CodecType] + if f == nil { + err := fmt.Errorf("invalid codec type %s", opt.CodecType) + log.Println("rpc client: codec error:", err) + return nil, err + } + // send options with server + if err := json.NewEncoder(conn).Encode(opt); err != nil { + log.Println("rpc client: options error: ", err) + _ = conn.Close() + return nil, err + } + return newClientCodec(f(conn), opt), nil +} + +func newClientCodec(cc codec.Codec, opt *Option) *Client { + client := &Client{ + seq: 1, // seq starts with 1, 0 means invalid call + cc: cc, + opt: opt, + pending: make(map[uint64]*Call), + } + go client.receive() + return client +} +``` + +还需要实现 `Dial` 函数,便于用户传入服务端地址,创建 Client 实例。为了简化用户调用,通过 `...*Option` 将 Option 实现为可选参数。 + +```go +func parseOptions(opts ...*Option) (*Option, error) { + // if opts is nil or pass nil as parameter + if len(opts) == 0 || opts[0] == nil { + return DefaultOption, nil + } + if len(opts) != 1 { + return nil, errors.New("number of options is more than 1") + } + opt := opts[0] + opt.MagicNumber = DefaultOption.MagicNumber + if opt.CodecType == "" { + opt.CodecType = DefaultOption.CodecType + } + return opt, nil +} + +func dial(network, address string, opt *Option) (*Client, error) { + conn, err := net.Dial(network, address) + if err != nil { + return nil, err + } + return NewClient(conn, opt) +} + +// Dial connects to an RPC server at the specified network address +func Dial(network, address string, opts ...*Option) (*Client, error) { + opt, err := parseOptions(opts...) + if err != nil { + return nil, err + } + return dial(network, address, opt) +} +``` + +此时,GeeRPC 客户端已经具备了完整的创建连接和接收响应的能力了,最后还需要实现发送请求的能力。 + +```go +func (client *Client) send(call *Call) { + // make sure that the client will send a complete request + client.sending.Lock() + defer client.sending.Unlock() + + // register this call. + seq, err := client.registerCall(call) + if err != nil { + call.Error = err + call.done() + return + } + + // prepare request header + client.header.ServiceMethod = call.ServiceMethod + client.header.Seq = seq + client.header.Error = "" + + // encode and send the request + if err := client.cc.Write(&client.header, call.Args); err != nil { + call := client.removeCall(seq) + // call may be nil, it usually means that Write partially failed, + // client has received the response and handled + if call != nil { + call.Error = err + call.done() + } + } +} + +// Go invokes the function asynchronously. +// It returns the Call structure representing the invocation. +func (client *Client) Go(serviceMethod string, args, reply interface{}, done chan *Call) *Call { + if done == nil { + done = make(chan *Call, 10) + } else if cap(done) == 0 { + log.Panic("rpc client: done channel is unbuffered") + } + call := &Call{ + ServiceMethod: serviceMethod, + Args: args, + Reply: reply, + Done: done, + } + client.send(call) + return call +} + +// Call invokes the named function, waits for it to complete, +// and returns its error status. +func (client *Client) Call(serviceMethod string, args, reply interface{}) error { + call := <-client.Go(serviceMethod, args, reply, make(chan *Call, 1)).Done + return call.Error +} +``` + +- `Go` 和 `Call` 是客户端暴露给用户的两个 RPC 服务调用接口,`Go` 是一个异步接口,返回 call 实例。 +- `Call` 是对 `Go` 的封装,阻塞 call.Done,等待响应返回,是一个同步接口。 + +至此,一个支持异步和并发的 GeeRPC 客户端已经完成。 + +## Demo + +第一天 GeeRPC 只实现了服务端,因此我们在 main 函数中手动模拟了整个通信过程,今天我们就将 main 函数中通信部分替换为今天的客户端吧。 + +[day2-client/main/main.go](https://github.com/geektutu/7days-golang/tree/master/gee-rpc/day2-client) + +startServer 没有发生变化。 + +```go +func startServer(addr chan string) { + // pick a free port + l, err := net.Listen("tcp", ":0") + if err != nil { + log.Fatal("network error:", err) + } + log.Println("start rpc server on", l.Addr()) + addr <- l.Addr().String() + geerpc.Accept(l) +} +``` + +在 main 函数中使用了 `client.Call` 并发了 5 个 RPC 同步调用,参数和返回值的类型均为 string。 + +```go +func main() { + log.SetFlags(0) + addr := make(chan string) + go startServer(addr) + client, _ := geerpc.Dial("tcp", <-addr) + defer func() { _ = client.Close() }() + + time.Sleep(time.Second) + // send request & receive response + var wg sync.WaitGroup + for i := 0; i < 5; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + args := fmt.Sprintf("geerpc req %d", i) + var reply string + if err := client.Call("Foo.Sum", args, &reply); err != nil { + log.Fatal("call Foo.Sum error:", err) + } + log.Println("reply:", reply) + }(i) + } + wg.Wait() +} +``` + +运行结果如下: + +```bash +start rpc server on [::]:50658 +&{Foo.Sum 5 } geerpc req 3 +&{Foo.Sum 1 } geerpc req 0 +&{Foo.Sum 3 } geerpc req 1 +&{Foo.Sum 2 } geerpc req 4 +&{Foo.Sum 4 } geerpc req 2 +reply: geerpc resp 1 +reply: geerpc resp 5 +reply: geerpc resp 3 +reply: geerpc resp 2 +reply: geerpc resp 4 +``` + +## 附 推荐阅读 + +- [Go 语言简明教程](https://geektutu.com/post/quick-golang.html) +- [Go 语言笔试面试题](https://geektutu.com/post/qa-golang.html) \ No newline at end of file diff --git a/gee-rpc/doc/geerpc.md b/gee-rpc/doc/geerpc.md index 9c49e6d..7a6a324 100644 --- a/gee-rpc/doc/geerpc.md +++ b/gee-rpc/doc/geerpc.md @@ -1,7 +1,7 @@ --- title: 7天用Go从零实现RPC框架GeeRPC date: 2020-10-06 16:00:00 -description: 7天用 Go语言/golang 从零实现 RPC 框架 GeeRPC 教程(7 days implement golang remote procedure call framework from scratch tutorial),动手写 RPC 框架,参照 golang标准库 net/rpc 的实现,实现了服务端(server)、支持异步和并发的客户端(client)、消息编码与解码(message encoding and decoding)、服务注册(service register)、支持 TCP/Unix/HTTP 等多种传输协议。并在此基础上新增了协议交换(protocol exchange)、注册中心(registry)、服务发现(service discovery)、负载均衡(load balance)、超时处理(timeout processing)等特性。 +description: 7天用 Go语言/golang 从零实现 RPC 框架 GeeRPC 教程(7 days implement golang remote procedure call framework from scratch tutorial),动手写 RPC 框架,参照 golang 标准库 net/rpc 的实现,实现了服务端(server)、支持异步和并发的客户端(client)、消息编码与解码(message encoding and decoding)、服务注册(service register)、支持 TCP/Unix/HTTP 等多种传输协议。并在此基础上新增了协议交换(protocol exchange)、注册中心(registry)、服务发现(service discovery)、负载均衡(load balance)、超时处理(timeout processing)等特性。 tags: - Go nav: 从零实现 From 1498ec2a82ce9a71f6bdacb7f10bbaf523940767 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Wed, 7 Oct 2020 23:08:58 +0800 Subject: [PATCH 093/122] gee-rpc: add day3 & day4 doc --- gee-rpc/day4-timeout/client_test.go | 1 + gee-rpc/day5-http-debug/client_test.go | 1 + gee-rpc/day6-load-balance/client_test.go | 1 + gee-rpc/day7-registry/client_test.go | 1 + gee-rpc/doc/geerpc-day3.md | 495 +++++++++++++++++++++++ 5 files changed, 499 insertions(+) create mode 100644 gee-rpc/doc/geerpc-day3.md diff --git a/gee-rpc/day4-timeout/client_test.go b/gee-rpc/day4-timeout/client_test.go index ab2fa64..c5c1d1f 100644 --- a/gee-rpc/day4-timeout/client_test.go +++ b/gee-rpc/day4-timeout/client_test.go @@ -45,6 +45,7 @@ func TestClient_Call(t *testing.T) { addrCh := make(chan string) go startServer(addrCh) addr := <-addrCh + time.Sleep(time.Second) t.Run("client timeout", func(t *testing.T) { client, _ := Dial("tcp", addr) ctx, _ := context.WithTimeout(context.Background(), time.Second) diff --git a/gee-rpc/day5-http-debug/client_test.go b/gee-rpc/day5-http-debug/client_test.go index 10b9817..bd46675 100644 --- a/gee-rpc/day5-http-debug/client_test.go +++ b/gee-rpc/day5-http-debug/client_test.go @@ -47,6 +47,7 @@ func TestClient_Call(t *testing.T) { addrCh := make(chan string) go startServer(addrCh) addr := <-addrCh + time.Sleep(time.Second) t.Run("client timeout", func(t *testing.T) { client, _ := Dial("tcp", addr) ctx, _ := context.WithTimeout(context.Background(), time.Second) diff --git a/gee-rpc/day6-load-balance/client_test.go b/gee-rpc/day6-load-balance/client_test.go index 10b9817..bd46675 100644 --- a/gee-rpc/day6-load-balance/client_test.go +++ b/gee-rpc/day6-load-balance/client_test.go @@ -47,6 +47,7 @@ func TestClient_Call(t *testing.T) { addrCh := make(chan string) go startServer(addrCh) addr := <-addrCh + time.Sleep(time.Second) t.Run("client timeout", func(t *testing.T) { client, _ := Dial("tcp", addr) ctx, _ := context.WithTimeout(context.Background(), time.Second) diff --git a/gee-rpc/day7-registry/client_test.go b/gee-rpc/day7-registry/client_test.go index 10b9817..bd46675 100644 --- a/gee-rpc/day7-registry/client_test.go +++ b/gee-rpc/day7-registry/client_test.go @@ -47,6 +47,7 @@ func TestClient_Call(t *testing.T) { addrCh := make(chan string) go startServer(addrCh) addr := <-addrCh + time.Sleep(time.Second) t.Run("client timeout", func(t *testing.T) { client, _ := Dial("tcp", addr) ctx, _ := context.WithTimeout(context.Background(), time.Second) diff --git a/gee-rpc/doc/geerpc-day3.md b/gee-rpc/doc/geerpc-day3.md new file mode 100644 index 0000000..7a0c4af --- /dev/null +++ b/gee-rpc/doc/geerpc-day3.md @@ -0,0 +1,495 @@ +--- +title: 动手写RPC框架 - GeeRPC第三天 服务注册(service register) +date: 2020-10-07 19:00:00 +description: 7天用 Go语言/golang 从零实现 RPC 框架 GeeRPC 教程(7 days implement golang remote procedure call framework from scratch tutorial),动手写 RPC 框架,参照 golang 标准库 net/rpc 的实现,实现了服务端(server)、支持异步和并发的客户端(client)、消息编码与解码(message encoding and decoding)、服务注册(service register)、支持 TCP/Unix/HTTP 等多种传输协议。第三天实现了服务注册,即将 Go 语言结构体通过反射映射为服务。 +tags: +- Go +nav: 从零实现 +categories: +- RPC框架 - GeeRPC +keywords: +- Go语言 +- 从零实现RPC框架 +- 反射 +- 服务 +image: post/geerpc/geerpc.jpg +github: https://github.com/geektutu/7days-golang +--- + +![golang RPC framework](geerpc/geerpc.jpg) + +本文是[7天用Go从零实现RPC框架GeeRPC]的第三篇。 + +- 通过反射实现服务注册功能 +- 在服务端实现服务调用,代码约 150 行 + +## 结构体映射为服务 + +RPC 框架的一个基础能力是:像调用本地程序一样调用远程服务。那如何将程序映射为服务呢?那么对 Go 来说,这个问题就变成了如何将结构体的方法映射为服务。 + +对 `net/rpc` 而言,一个函数需要能够被远程调用,需要满足如下五个条件: + +- the method's type is exported. -- 方法所属类型是导出的。 +- the method is exported. -- 方式是导出的。 +- the method has two arguments, both exported (or builtin) types. -- 两个入参,均为导出或内置类型。 +- the method's second argument is a pointer. -- 第二个入参必须是一个指针。 +- the method has return type error. -- 返回值为 error 类型。 + +更直观一些: + +```go +func (t *T) MethodName(argType T1, replyType *T2) error +``` + +假设客户端发过来一个请求,包含 ServiceMethod 和 Argv。 + +```json +{ + "ServiceMethod": "T.MethodName" + "Argv":"0101110101..." // 序列化之后的字节流 +} +``` + +通过 "T.MethodName" 可以确定调用的是类型 T 的 MethodName,如果硬编码实现这个功能,很可能是这样: + +```go +switch req.ServiceMethod { + case "T.MethodName": + t := new(t) + reply := new(T2) + var argv T1 + gob.NewDecoder(conn).Decode(&argv) + err := t.MethodName(argv, reply) + server.sendMessage(reply, err) + case "Foo.Sum": + f := new(Foo) + ... +} +``` + +也就是说,如果使用硬编码的方式来实现结构体与服务的映射,那么每暴露一个方法,就需要编写等量的代码。那有没有什么方式,能够将这个映射过程自动化呢?可以借助反射。 + +通过反射,我们能够非常容易地获取某个结构体的所有方法,并且能够通过方法,获取到该方法所有的参数类型与返回值。例如: + +```go +func main() { + var wg sync.WaitGroup + typ := reflect.TypeOf(&wg) + for i := 0; i < typ.NumMethod(); i++ { + method := typ.Method(i) + argv := make([]string, 0, method.Type.NumIn()) + returns := make([]string, 0, method.Type.NumIn()) + // j 从 1 开始,第 0 个入参是 wg 自己。 + for j := 1; j < method.Type.NumIn(); j++ { + argv = append(argv, method.Type.In(j).Name()) + } + for j := 0; j < method.Type.NumOut(); j++ { + returns = append(returns, method.Type.Out(j).Name()) + } + log.Printf("func (w *%s) %s(%s) %s", + typ.Elem().Name(), + method.Name, + strings.Join(argv, ","), + strings.Join(returns, ",")) + } +} +``` + +运行的结果是: + +```go +func (w *WaitGroup) Add(int) +func (w *WaitGroup) Done() +func (w *WaitGroup) Wait() +``` + +## 通过反射实现 service + +前面两天我们完成了客户端和服务端,客户端相对来说功能是比较完整的,但是服务端的功能并不完整,仅仅将请求的 header 打印了出来,并没有真正地处理。那今天的主要目的是补全这部分功能。首先通过反射实现结构体与服务的映射关系,代码独立放置在 `service.go` 中。 + +[day3-service/service.go](https://github.com/geektutu/7days-golang/tree/master/gee-rpc/day3-service) + +第一步,定义结构体 methodType: + +```go +type methodType struct { + method reflect.Method + ArgType reflect.Type + ReplyType reflect.Type + numCalls uint64 +} + +func (m *methodType) NumCalls() uint64 { + return atomic.LoadUint64(&m.numCalls) +} + +func (m *methodType) newArgv() reflect.Value { + var argv reflect.Value + // arg may be a pointer type, or a value type + if m.ArgType.Kind() == reflect.Ptr { + argv = reflect.New(m.ArgType.Elem()) + } else { + argv = reflect.New(m.ArgType).Elem() + } + return argv +} + +func (m *methodType) newReplyv() reflect.Value { + // reply must be a pointer type + replyv := reflect.New(m.ReplyType.Elem()) + switch m.ReplyType.Elem().Kind() { + case reflect.Map: + replyv.Elem().Set(reflect.MakeMap(m.ReplyType.Elem())) + case reflect.Slice: + replyv.Elem().Set(reflect.MakeSlice(m.ReplyType.Elem(), 0, 0)) + } + return replyv +} +``` + +每一个 methodType 实例包含了一个方法的完整信息。包括 + +- method:方法本身 +- ArgType:第一个参数的类型 +- ReplyType:第二个参数的类型 +- numCalls:后续统计方法调用次数时会用到 + +另外,我们还实现了 2 个方法 `newArgv` 和 `newReplyv`,用于创建对应类型的实例。`newArgv` 方法有一个小细节,指针类型和值类型创建实例的方式有细微区别。 + +第二步,定义结构体 service: + +```go +type service struct { + name string + typ reflect.Type + rcvr reflect.Value + method map[string]*methodType +} +``` + +service 的定义也是非常简洁的,name 即映射的结构体的名称,比如 `T`,比如 `WaitGroup`;typ 是结构体的类型;rcvr 即结构体的实例本身,保留 rcvr 是因为在调用时需要 rcvr 作为第 0 个参数;method 是 map 类型,存储映射的结构体的所有符合条件的方法。 + +接下来,完成构造函数 `newService`,入参是任意需要映射为服务的结构体实例。 + +```go +func newService(rcvr interface{}) *service { + s := new(service) + s.rcvr = reflect.ValueOf(rcvr) + s.name = reflect.Indirect(s.rcvr).Type().Name() + s.typ = reflect.TypeOf(rcvr) + if !ast.IsExported(s.name) { + log.Fatalf("rpc server: %s is not a valid service name", s.name) + } + s.registerMethods() + return s +} + +func (s *service) registerMethods() { + s.method = make(map[string]*methodType) + for i := 0; i < s.typ.NumMethod(); i++ { + method := s.typ.Method(i) + mType := method.Type + if mType.NumIn() != 3 || mType.NumOut() != 1 { + continue + } + if mType.Out(0) != reflect.TypeOf((*error)(nil)).Elem() { + continue + } + argType, replyType := mType.In(1), mType.In(2) + if !isExportedOrBuiltinType(argType) || !isExportedOrBuiltinType(replyType) { + continue + } + s.method[method.Name] = &methodType{ + method: method, + ArgType: argType, + ReplyType: replyType, + } + log.Printf("rpc server: register %s.%s\n", s.name, method.Name) + } +} + +func isExportedOrBuiltinType(t reflect.Type) bool { + return ast.IsExported(t.Name()) || t.PkgPath() == "" +} +``` + +`registerMethods` 过滤出了符合条件的方法: + +- 两个导出或内置类型的入参(反射时为 3 个,第 0 个是自身,类似于 python 的 self,java 中的 this) +- 返回值有且只有 1 个,类型为 error + +最后,我们还需要实现 `call` 方法,即能够通过反射值调用方法。 + +```go +func (s *service) call(m *methodType, argv, replyv reflect.Value) error { + atomic.AddUint64(&m.numCalls, 1) + f := m.method.Func + returnValues := f.Call([]reflect.Value{s.rcvr, argv, replyv}) + if errInter := returnValues[0].Interface(); errInter != nil { + return errInter.(error) + } + return nil +} +``` + +## service 的测试用例 + +为了保证 service 实现的正确性,我们为 service.go 写了几个测试用例。 + +[day3-service/service_test.go](https://github.com/geektutu/7days-golang/tree/master/gee-rpc/day3-service) + +定义结构体 Foo,实现 2 个方法,导出方法 Sum 和 非导出方法 sum。 + +```go +type Foo int + +type Args struct{ Num1, Num2 int } + +func (f Foo) Sum(args Args, reply *int) error { + *reply = args.Num1 + args.Num2 + return nil +} + +// it's not a exported Method +func (f Foo) sum(args Args, reply *int) error { + *reply = args.Num1 + args.Num2 + return nil +} + +func _assert(condition bool, msg string, v ...interface{}) { + if !condition { + panic(fmt.Sprintf("assertion failed: "+msg, v...)) + } +} +``` + +测试 newService 和 call 方法。 + +```go +func TestNewService(t *testing.T) { + var foo Foo + s := newService(&foo) + _assert(len(s.method) == 1, "wrong service Method, expect 1, but got %d", len(s.method)) + mType := s.method["Sum"] + _assert(mType != nil, "wrong Method, Sum shouldn't nil") +} + +func TestMethodType_Call(t *testing.T) { + var foo Foo + s := newService(&foo) + mType := s.method["Sum"] + + argv := mType.newArgv() + replyv := mType.newReplyv() + argv.Set(reflect.ValueOf(Args{Num1: 1, Num2: 3})) + err := s.call(mType, argv, replyv) + _assert(err == nil && *replyv.Interface().(*int) == 4 && mType.NumCalls() == 1, "failed to call Foo.Sum") +} +``` + +## 集成到服务端 + +通过反射结构体已经映射为服务,但请求的处理过程还没有完成。从接收到请求到回复还差以下几个步骤:第一步,根据入参类型,将请求的 body 反序列化;第二步,调用 `service.call`,完成方法调用;第三步,将 reply 序列化为字节流,构造响应报文,返回。 + +回到代码本身,补全之前在 `server.go` 中遗留的 2 个 TODO 任务 `readRequest` 和 `handleRequest` 即可。 + +在这之前,我们还需要为 Server 实现一个方法 `Register`。 + +[day3-service/server.go](https://github.com/geektutu/7days-golang/tree/master/gee-rpc/day3-service) + +```go +// Server represents an RPC Server. +type Server struct { + serviceMap sync.Map +} + +// Register publishes in the server the set of methods of the +func (server *Server) Register(rcvr interface{}) error { + s := newService(rcvr) + if _, dup := server.serviceMap.LoadOrStore(s.name, s); dup { + return errors.New("rpc: service already defined: " + s.name) + } + return nil +} + +// Register publishes the receiver's methods in the DefaultServer. +func Register(rcvr interface{}) error { return DefaultServer.Register(rcvr) } +``` + +配套实现 `findService` 方法,即通过 `ServiceMethod` 从 serviceMap 中找到对应的 service + +```go +func (server *Server) findService(serviceMethod string) (svc *service, mtype *methodType, err error) { + dot := strings.LastIndex(serviceMethod, ".") + if dot < 0 { + err = errors.New("rpc server: service/method request ill-formed: " + serviceMethod) + return + } + serviceName, methodName := serviceMethod[:dot], serviceMethod[dot+1:] + svci, ok := server.serviceMap.Load(serviceName) + if !ok { + err = errors.New("rpc server: can't find service " + serviceName) + return + } + svc = svci.(*service) + mtype = svc.method[methodName] + if mtype == nil { + err = errors.New("rpc server: can't find method " + methodName) + } + return +} +``` + +`findService` 的实现看似比较繁琐,但是逻辑还是非常清晰的。因为 ServiceMethod 的构成是 "Service.Method",因此先将其分割成 2 部分,第一部分是 Service 的名称,第二部分即方法名。现在 serviceMap 中找到对应的 service 实例,再从 service 实例的 method 中,找到对应的 methodType。 + +准备工具已经就绪,我们首先补全 readRequest 方法: + +```go +// request stores all information of a call +type request struct { + h *codec.Header // header of request + argv, replyv reflect.Value // argv and replyv of request + mtype *methodType + svc *service +} + +func (server *Server) readRequest(cc codec.Codec) (*request, error) { + h, err := server.readRequestHeader(cc) + if err != nil { + return nil, err + } + req := &request{h: h} + req.svc, req.mtype, err = server.findService(h.ServiceMethod) + if err != nil { + return req, err + } + req.argv = req.mtype.newArgv() + req.replyv = req.mtype.newReplyv() + + // make sure that argvi is a pointer, ReadBody need a pointer as parameter + argvi := req.argv.Interface() + if req.argv.Type().Kind() != reflect.Ptr { + argvi = req.argv.Addr().Interface() + } + if err = cc.ReadBody(argvi); err != nil { + log.Println("rpc server: read body err:", err) + return req, err + } + return req, nil +} +``` + +readRequest 方法中最重要的部分,即通过 `newArgv()` 和 `newReplyv()` 两个方法创建出两个入参实例,然后通过 `cc.ReadBody()` 将请求报文反序列化为第一个入参 argv,在这里同样需要注意 argv 可能是值类型,也可能是指针类型,所以处理方式有点差异。 + +接下来补全 handleRequest 方法: + +```go +func (server *Server) handleRequest(cc codec.Codec, req *request, sending *sync.Mutex, wg *sync.WaitGroup) { + defer wg.Done() + err := req.svc.call(req.mtype, req.argv, req.replyv) + if err != nil { + req.h.Error = err.Error() + server.sendResponse(cc, req.h, invalidRequest, sending) + return + } + server.sendResponse(cc, req.h, req.replyv.Interface(), sending) +} +``` + +相对于 readRequest,handleRequest 的实现非常简单,通过 `req.svc.call` 完成方法调用,将 replyv 传递给 sendResponse 完成序列化即可。 + +到这里,今天的所有内容已经实现完成,成功在服务端实现了服务注册与调用。 + +## Demo + +最后,还是需要写一个可执行程序(main)验证今天的成果。 + +[day3-service/main/main.go](https://github.com/geektutu/7days-golang/tree/master/gee-rpc/day3-service) + +第一步,定义结构体 Foo 和方法 Sum + +```go +package main + +import ( + "geerpc" + "log" + "net" + "sync" + "time" +) + +type Foo int + +type Args struct{ Num1, Num2 int } + +func (f Foo) Sum(args Args, reply *int) error { + *reply = args.Num1 + args.Num2 + return nil +} +``` + +第二步,注册 Foo 到 Server 中,并启动 RPC 服务 + +```go +func startServer(addr chan string) { + var foo Foo + if err := geerpc.Register(&foo); err != nil { + log.Fatal("register error:", err) + } + // pick a free port + l, err := net.Listen("tcp", ":0") + if err != nil { + log.Fatal("network error:", err) + } + log.Println("start rpc server on", l.Addr()) + addr <- l.Addr().String() + geerpc.Accept(l) +} +``` + +第三步,构造参数,发送 RPC 请求,并打印结果。 + +```go +func main() { + log.SetFlags(0) + addr := make(chan string) + go startServer(addr) + client, _ := geerpc.Dial("tcp", <-addr) + defer func() { _ = client.Close() }() + + time.Sleep(time.Second) + // send request & receive response + var wg sync.WaitGroup + for i := 0; i < 5; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + args := &Args{Num1: i, Num2: i * i} + var reply int + if err := client.Call("Foo.Sum", args, &reply); err != nil { + log.Fatal("call Foo.Sum error:", err) + } + log.Printf("%d + %d = %d", args.Num1, args.Num2, reply) + }(i) + } + wg.Wait() +} +``` + +运行结果如下: + +```bash +rpc server: register Foo.Sum +start rpc server on [::]:57509 +1 + 1 = 2 +2 + 4 = 6 +3 + 9 = 12 +0 + 0 = 0 +4 + 16 = 20 +``` + +## 附 推荐阅读 + +- [Go 语言简明教程](https://geektutu.com/post/quick-golang.html) +- [Go 语言笔试面试题](https://geektutu.com/post/qa-golang.html) \ No newline at end of file From 28f1449d61d14db1936b8def0ce7d7a076b66a48 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Thu, 8 Oct 2020 01:05:16 +0800 Subject: [PATCH 094/122] gee-rpc: if client is nil, close the connection --- gee-rpc/day2-client/client.go | 20 ++++++------ gee-rpc/day3-service/client.go | 21 +++++++------ gee-rpc/day4-timeout/client.go | 39 ++++++++++++------------ gee-rpc/day4-timeout/client_test.go | 11 ++++--- gee-rpc/day5-http-debug/client.go | 38 +++++++++++------------ gee-rpc/day5-http-debug/client_test.go | 11 ++++--- gee-rpc/day6-load-balance/client.go | 38 +++++++++++------------ gee-rpc/day6-load-balance/client_test.go | 11 ++++--- gee-rpc/day7-registry/client.go | 38 +++++++++++------------ gee-rpc/day7-registry/client_test.go | 11 ++++--- gee-rpc/doc/geerpc-day2.md | 20 ++++++------ 11 files changed, 135 insertions(+), 123 deletions(-) diff --git a/gee-rpc/day2-client/client.go b/gee-rpc/day2-client/client.go index 1c8a3cb..4af2ebf 100644 --- a/gee-rpc/day2-client/client.go +++ b/gee-rpc/day2-client/client.go @@ -226,19 +226,21 @@ func newClientCodec(cc codec.Codec, opt *Option) *Client { return client } -func dial(network, address string, opt *Option) (*Client, error) { - conn, err := net.Dial(network, address) +// Dial connects to an RPC server at the specified network address +func Dial(network, address string, opts ...*Option) (client *Client, err error) { + opt, err := parseOptions(opts...) if err != nil { return nil, err } - return NewClient(conn, opt) -} - -// Dial connects to an RPC server at the specified network address -func Dial(network, address string, opts ...*Option) (*Client, error) { - opt, err := parseOptions(opts...) + conn, err := net.Dial(network, address) if err != nil { return nil, err } - return dial(network, address, opt) + // close the connection if client is nil + defer func() { + if client == nil { + _ = conn.Close() + } + }() + return NewClient(conn, opt) } diff --git a/gee-rpc/day3-service/client.go b/gee-rpc/day3-service/client.go index 1c8a3cb..77c9436 100644 --- a/gee-rpc/day3-service/client.go +++ b/gee-rpc/day3-service/client.go @@ -209,7 +209,6 @@ func NewClient(conn net.Conn, opt *Option) (*Client, error) { // send options with server if err := json.NewEncoder(conn).Encode(opt); err != nil { log.Println("rpc client: options error: ", err) - _ = conn.Close() return nil, err } return newClientCodec(f(conn), opt), nil @@ -226,19 +225,21 @@ func newClientCodec(cc codec.Codec, opt *Option) *Client { return client } -func dial(network, address string, opt *Option) (*Client, error) { - conn, err := net.Dial(network, address) +// Dial connects to an RPC server at the specified network address +func Dial(network, address string, opts ...*Option) (client *Client, err error) { + opt, err := parseOptions(opts...) if err != nil { return nil, err } - return NewClient(conn, opt) -} - -// Dial connects to an RPC server at the specified network address -func Dial(network, address string, opts ...*Option) (*Client, error) { - opt, err := parseOptions(opts...) + conn, err := net.Dial(network, address) if err != nil { return nil, err } - return dial(network, address, opt) + // close the connection if client is nil + defer func() { + if client == nil { + _ = conn.Close() + } + }() + return NewClient(conn, opt) } diff --git a/gee-rpc/day4-timeout/client.go b/gee-rpc/day4-timeout/client.go index 45fc9e6..29af7aa 100644 --- a/gee-rpc/day4-timeout/client.go +++ b/gee-rpc/day4-timeout/client.go @@ -207,18 +207,17 @@ func parseOptions(opts ...*Option) (*Option, error) { return opt, nil } -func NewClient(conn net.Conn, opt *Option) (*Client, error) { +func NewClient(conn net.Conn, opt *Option) (client *Client, err error) { f := codec.NewCodecFuncMap[opt.CodecType] if f == nil { - err := fmt.Errorf("invalid codec type %s", opt.CodecType) + err = fmt.Errorf("invalid codec type %s", opt.CodecType) log.Println("rpc client: codec error:", err) - return nil, err + return } // send options with server - if err := json.NewEncoder(conn).Encode(opt); err != nil { + if err = json.NewEncoder(conn).Encode(opt); err != nil { log.Println("rpc client: options error: ", err) - _ = conn.Close() - return nil, err + return } return newClientCodec(f(conn), opt), nil } @@ -239,16 +238,26 @@ type clientResult struct { err error } -type dialFunc func(network, address string, opt *Option) (client *Client, err error) +type newClientFunc func(conn net.Conn, opt *Option) (client *Client, err error) -func dialTimeout(f dialFunc, network, address string, opts ...*Option) (*Client, error) { +func dialTimeout(f newClientFunc, network, address string, opts ...*Option) (client *Client, err error) { opt, err := parseOptions(opts...) if err != nil { return nil, err } + conn, err := net.Dial(network, address) + if err != nil { + return nil, err + } + // close the connection if client is nil + defer func() { + if client == nil { + _ = conn.Close() + } + }() ch := make(chan clientResult) go func() { - client, err := f(network, address, opt) + client, err := f(conn, opt) ch <- clientResult{client: client, err: err} }() if opt.ConnectTimeout == 0 { @@ -257,21 +266,13 @@ func dialTimeout(f dialFunc, network, address string, opts ...*Option) (*Client, } select { case <-time.After(opt.ConnectTimeout): - return nil, fmt.Errorf("rpc client: dial timeout: expect within %s", opt.ConnectTimeout) + return nil, fmt.Errorf("rpc client: connect timeout: expect within %s", opt.ConnectTimeout) case result := <-ch: return result.client, result.err } } -func dial(network, address string, opt *Option) (*Client, error) { - conn, err := net.Dial(network, address) - if err != nil { - return nil, err - } - return NewClient(conn, opt) -} - // Dial connects to an RPC server at the specified network address func Dial(network, address string, opts ...*Option) (*Client, error) { - return dialTimeout(dial, network, address, opts...) + return dialTimeout(NewClient, network, address, opts...) } diff --git a/gee-rpc/day4-timeout/client_test.go b/gee-rpc/day4-timeout/client_test.go index c5c1d1f..4488455 100644 --- a/gee-rpc/day4-timeout/client_test.go +++ b/gee-rpc/day4-timeout/client_test.go @@ -26,16 +26,19 @@ func startServer(addr chan string) { func TestClient_dialTimeout(t *testing.T) { t.Parallel() - f := func(network, address string, opt *Option) (client *Client, err error) { + l, _ := net.Listen("tcp", ":0") + + f := func(conn net.Conn, opt *Option) (client *Client, err error) { + _ = conn.Close() time.Sleep(time.Second * 2) return nil, nil } t.Run("timeout", func(t *testing.T) { - _, err := dialTimeout(f, "", "", &Option{ConnectTimeout: time.Second}) - _assert(err != nil && strings.Contains(err.Error(), "dial timeout"), "expect a timeout error") + _, err := dialTimeout(f, "tcp", l.Addr().String(), &Option{ConnectTimeout: time.Second}) + _assert(err != nil && strings.Contains(err.Error(), "connect timeout"), "expect a timeout error") }) t.Run("0", func(t *testing.T) { - _, err := dialTimeout(f, "", "", &Option{ConnectTimeout: 0}) + _, err := dialTimeout(f, "tcp", l.Addr().String(), &Option{ConnectTimeout: 0}) _assert(err == nil, "0 means no limit") }) } diff --git a/gee-rpc/day5-http-debug/client.go b/gee-rpc/day5-http-debug/client.go index bdc2c0f..b8b57f7 100644 --- a/gee-rpc/day5-http-debug/client.go +++ b/gee-rpc/day5-http-debug/client.go @@ -242,16 +242,26 @@ type clientResult struct { err error } -type dialFunc func(network, address string, opt *Option) (client *Client, err error) +type newClientFunc func(conn net.Conn, opt *Option) (client *Client, err error) -func dialTimeout(f dialFunc, network, address string, opts ...*Option) (*Client, error) { +func dialTimeout(f newClientFunc, network, address string, opts ...*Option) (client *Client, err error) { opt, err := parseOptions(opts...) if err != nil { return nil, err } + conn, err := net.Dial(network, address) + if err != nil { + return nil, err + } + // close the connection if client is nil + defer func() { + if client == nil { + _ = conn.Close() + } + }() ch := make(chan clientResult) go func() { - client, err := f(network, address, opt) + client, err := f(conn, opt) ch <- clientResult{client: client, err: err} }() if opt.ConnectTimeout == 0 { @@ -260,30 +270,19 @@ func dialTimeout(f dialFunc, network, address string, opts ...*Option) (*Client, } select { case <-time.After(opt.ConnectTimeout): - return nil, fmt.Errorf("rpc client: dial timeout: expect within %s", opt.ConnectTimeout) + return nil, fmt.Errorf("rpc client: connect timeout: expect within %s", opt.ConnectTimeout) case result := <-ch: return result.client, result.err } } -func dial(network, address string, opt *Option) (*Client, error) { - conn, err := net.Dial(network, address) - if err != nil { - return nil, err - } - return NewClient(conn, opt) -} - // Dial connects to an RPC server at the specified network address func Dial(network, address string, opts ...*Option) (*Client, error) { - return dialTimeout(dial, network, address, opts...) + return dialTimeout(NewClient, network, address, opts...) } -func dialHTTP(network, address string, opt *Option) (*Client, error) { - conn, err := net.Dial(network, address) - if err != nil { - return nil, err - } +// NewHTTPClient new a Client instance via HTTP as transport protocol +func NewHTTPClient(conn net.Conn, opt *Option) (*Client, error) { _, _ = io.WriteString(conn, fmt.Sprintf("CONNECT %s HTTP/1.0\n\n", defaultRPCPath)) // Require successful HTTP response @@ -295,14 +294,13 @@ func dialHTTP(network, address string, opt *Option) (*Client, error) { if err == nil { err = errors.New("unexpected HTTP response: " + resp.Status) } - _ = conn.Close() return nil, err } // DialHTTP connects to an HTTP RPC server at the specified network address // listening on the default HTTP RPC path. func DialHTTP(network, address string, opts ...*Option) (*Client, error) { - return dialTimeout(dialHTTP, network, address, opts...) + return dialTimeout(NewHTTPClient, network, address, opts...) } // XDial calls different functions to connect to a RPC server diff --git a/gee-rpc/day5-http-debug/client_test.go b/gee-rpc/day5-http-debug/client_test.go index bd46675..3b13cb0 100644 --- a/gee-rpc/day5-http-debug/client_test.go +++ b/gee-rpc/day5-http-debug/client_test.go @@ -28,16 +28,19 @@ func startServer(addr chan string) { func TestClient_dialTimeout(t *testing.T) { t.Parallel() - f := func(network, address string, opt *Option) (client *Client, err error) { + l, _ := net.Listen("tcp", ":0") + + f := func(conn net.Conn, opt *Option) (client *Client, err error) { + _ = conn.Close() time.Sleep(time.Second * 2) return nil, nil } t.Run("timeout", func(t *testing.T) { - _, err := dialTimeout(f, "", "", &Option{ConnectTimeout: time.Second}) - _assert(err != nil && strings.Contains(err.Error(), "dial timeout"), "expect a timeout error") + _, err := dialTimeout(f, "tcp", l.Addr().String(), &Option{ConnectTimeout: time.Second}) + _assert(err != nil && strings.Contains(err.Error(), "connect timeout"), "expect a timeout error") }) t.Run("0", func(t *testing.T) { - _, err := dialTimeout(f, "", "", &Option{ConnectTimeout: 0}) + _, err := dialTimeout(f, "tcp", l.Addr().String(), &Option{ConnectTimeout: 0}) _assert(err == nil, "0 means no limit") }) } diff --git a/gee-rpc/day6-load-balance/client.go b/gee-rpc/day6-load-balance/client.go index bdc2c0f..b8b57f7 100644 --- a/gee-rpc/day6-load-balance/client.go +++ b/gee-rpc/day6-load-balance/client.go @@ -242,16 +242,26 @@ type clientResult struct { err error } -type dialFunc func(network, address string, opt *Option) (client *Client, err error) +type newClientFunc func(conn net.Conn, opt *Option) (client *Client, err error) -func dialTimeout(f dialFunc, network, address string, opts ...*Option) (*Client, error) { +func dialTimeout(f newClientFunc, network, address string, opts ...*Option) (client *Client, err error) { opt, err := parseOptions(opts...) if err != nil { return nil, err } + conn, err := net.Dial(network, address) + if err != nil { + return nil, err + } + // close the connection if client is nil + defer func() { + if client == nil { + _ = conn.Close() + } + }() ch := make(chan clientResult) go func() { - client, err := f(network, address, opt) + client, err := f(conn, opt) ch <- clientResult{client: client, err: err} }() if opt.ConnectTimeout == 0 { @@ -260,30 +270,19 @@ func dialTimeout(f dialFunc, network, address string, opts ...*Option) (*Client, } select { case <-time.After(opt.ConnectTimeout): - return nil, fmt.Errorf("rpc client: dial timeout: expect within %s", opt.ConnectTimeout) + return nil, fmt.Errorf("rpc client: connect timeout: expect within %s", opt.ConnectTimeout) case result := <-ch: return result.client, result.err } } -func dial(network, address string, opt *Option) (*Client, error) { - conn, err := net.Dial(network, address) - if err != nil { - return nil, err - } - return NewClient(conn, opt) -} - // Dial connects to an RPC server at the specified network address func Dial(network, address string, opts ...*Option) (*Client, error) { - return dialTimeout(dial, network, address, opts...) + return dialTimeout(NewClient, network, address, opts...) } -func dialHTTP(network, address string, opt *Option) (*Client, error) { - conn, err := net.Dial(network, address) - if err != nil { - return nil, err - } +// NewHTTPClient new a Client instance via HTTP as transport protocol +func NewHTTPClient(conn net.Conn, opt *Option) (*Client, error) { _, _ = io.WriteString(conn, fmt.Sprintf("CONNECT %s HTTP/1.0\n\n", defaultRPCPath)) // Require successful HTTP response @@ -295,14 +294,13 @@ func dialHTTP(network, address string, opt *Option) (*Client, error) { if err == nil { err = errors.New("unexpected HTTP response: " + resp.Status) } - _ = conn.Close() return nil, err } // DialHTTP connects to an HTTP RPC server at the specified network address // listening on the default HTTP RPC path. func DialHTTP(network, address string, opts ...*Option) (*Client, error) { - return dialTimeout(dialHTTP, network, address, opts...) + return dialTimeout(NewHTTPClient, network, address, opts...) } // XDial calls different functions to connect to a RPC server diff --git a/gee-rpc/day6-load-balance/client_test.go b/gee-rpc/day6-load-balance/client_test.go index bd46675..3b13cb0 100644 --- a/gee-rpc/day6-load-balance/client_test.go +++ b/gee-rpc/day6-load-balance/client_test.go @@ -28,16 +28,19 @@ func startServer(addr chan string) { func TestClient_dialTimeout(t *testing.T) { t.Parallel() - f := func(network, address string, opt *Option) (client *Client, err error) { + l, _ := net.Listen("tcp", ":0") + + f := func(conn net.Conn, opt *Option) (client *Client, err error) { + _ = conn.Close() time.Sleep(time.Second * 2) return nil, nil } t.Run("timeout", func(t *testing.T) { - _, err := dialTimeout(f, "", "", &Option{ConnectTimeout: time.Second}) - _assert(err != nil && strings.Contains(err.Error(), "dial timeout"), "expect a timeout error") + _, err := dialTimeout(f, "tcp", l.Addr().String(), &Option{ConnectTimeout: time.Second}) + _assert(err != nil && strings.Contains(err.Error(), "connect timeout"), "expect a timeout error") }) t.Run("0", func(t *testing.T) { - _, err := dialTimeout(f, "", "", &Option{ConnectTimeout: 0}) + _, err := dialTimeout(f, "tcp", l.Addr().String(), &Option{ConnectTimeout: 0}) _assert(err == nil, "0 means no limit") }) } diff --git a/gee-rpc/day7-registry/client.go b/gee-rpc/day7-registry/client.go index bdc2c0f..b8b57f7 100644 --- a/gee-rpc/day7-registry/client.go +++ b/gee-rpc/day7-registry/client.go @@ -242,16 +242,26 @@ type clientResult struct { err error } -type dialFunc func(network, address string, opt *Option) (client *Client, err error) +type newClientFunc func(conn net.Conn, opt *Option) (client *Client, err error) -func dialTimeout(f dialFunc, network, address string, opts ...*Option) (*Client, error) { +func dialTimeout(f newClientFunc, network, address string, opts ...*Option) (client *Client, err error) { opt, err := parseOptions(opts...) if err != nil { return nil, err } + conn, err := net.Dial(network, address) + if err != nil { + return nil, err + } + // close the connection if client is nil + defer func() { + if client == nil { + _ = conn.Close() + } + }() ch := make(chan clientResult) go func() { - client, err := f(network, address, opt) + client, err := f(conn, opt) ch <- clientResult{client: client, err: err} }() if opt.ConnectTimeout == 0 { @@ -260,30 +270,19 @@ func dialTimeout(f dialFunc, network, address string, opts ...*Option) (*Client, } select { case <-time.After(opt.ConnectTimeout): - return nil, fmt.Errorf("rpc client: dial timeout: expect within %s", opt.ConnectTimeout) + return nil, fmt.Errorf("rpc client: connect timeout: expect within %s", opt.ConnectTimeout) case result := <-ch: return result.client, result.err } } -func dial(network, address string, opt *Option) (*Client, error) { - conn, err := net.Dial(network, address) - if err != nil { - return nil, err - } - return NewClient(conn, opt) -} - // Dial connects to an RPC server at the specified network address func Dial(network, address string, opts ...*Option) (*Client, error) { - return dialTimeout(dial, network, address, opts...) + return dialTimeout(NewClient, network, address, opts...) } -func dialHTTP(network, address string, opt *Option) (*Client, error) { - conn, err := net.Dial(network, address) - if err != nil { - return nil, err - } +// NewHTTPClient new a Client instance via HTTP as transport protocol +func NewHTTPClient(conn net.Conn, opt *Option) (*Client, error) { _, _ = io.WriteString(conn, fmt.Sprintf("CONNECT %s HTTP/1.0\n\n", defaultRPCPath)) // Require successful HTTP response @@ -295,14 +294,13 @@ func dialHTTP(network, address string, opt *Option) (*Client, error) { if err == nil { err = errors.New("unexpected HTTP response: " + resp.Status) } - _ = conn.Close() return nil, err } // DialHTTP connects to an HTTP RPC server at the specified network address // listening on the default HTTP RPC path. func DialHTTP(network, address string, opts ...*Option) (*Client, error) { - return dialTimeout(dialHTTP, network, address, opts...) + return dialTimeout(NewHTTPClient, network, address, opts...) } // XDial calls different functions to connect to a RPC server diff --git a/gee-rpc/day7-registry/client_test.go b/gee-rpc/day7-registry/client_test.go index bd46675..3b13cb0 100644 --- a/gee-rpc/day7-registry/client_test.go +++ b/gee-rpc/day7-registry/client_test.go @@ -28,16 +28,19 @@ func startServer(addr chan string) { func TestClient_dialTimeout(t *testing.T) { t.Parallel() - f := func(network, address string, opt *Option) (client *Client, err error) { + l, _ := net.Listen("tcp", ":0") + + f := func(conn net.Conn, opt *Option) (client *Client, err error) { + _ = conn.Close() time.Sleep(time.Second * 2) return nil, nil } t.Run("timeout", func(t *testing.T) { - _, err := dialTimeout(f, "", "", &Option{ConnectTimeout: time.Second}) - _assert(err != nil && strings.Contains(err.Error(), "dial timeout"), "expect a timeout error") + _, err := dialTimeout(f, "tcp", l.Addr().String(), &Option{ConnectTimeout: time.Second}) + _assert(err != nil && strings.Contains(err.Error(), "connect timeout"), "expect a timeout error") }) t.Run("0", func(t *testing.T) { - _, err := dialTimeout(f, "", "", &Option{ConnectTimeout: 0}) + _, err := dialTimeout(f, "tcp", l.Addr().String(), &Option{ConnectTimeout: 0}) _assert(err == nil, "0 means no limit") }) } diff --git a/gee-rpc/doc/geerpc-day2.md b/gee-rpc/doc/geerpc-day2.md index 2d356e3..bf4db48 100644 --- a/gee-rpc/doc/geerpc-day2.md +++ b/gee-rpc/doc/geerpc-day2.md @@ -243,21 +243,23 @@ func parseOptions(opts ...*Option) (*Option, error) { return opt, nil } -func dial(network, address string, opt *Option) (*Client, error) { - conn, err := net.Dial(network, address) +// Dial connects to an RPC server at the specified network address +func Dial(network, address string, opts ...*Option) (client *Client, err error) { + opt, err := parseOptions(opts...) if err != nil { return nil, err } - return NewClient(conn, opt) -} - -// Dial connects to an RPC server at the specified network address -func Dial(network, address string, opts ...*Option) (*Client, error) { - opt, err := parseOptions(opts...) + conn, err := net.Dial(network, address) if err != nil { return nil, err } - return dial(network, address, opt) + // close the connection if client is nil + defer func() { + if client == nil { + _ = conn.Close() + } + }() + return NewClient(conn, opt) } ``` From 9f58ccc1c9cb44a3fd7c5fe2e1216c3ce529e72f Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Thu, 8 Oct 2020 01:37:53 +0800 Subject: [PATCH 095/122] gee-rpc/day4: add doc --- gee-rpc/day2-client/client.go | 2 +- gee-rpc/day3-service/client.go | 2 +- gee-rpc/day4-timeout/client.go | 4 +- gee-rpc/day5-http-debug/client.go | 4 +- gee-rpc/day6-load-balance/client.go | 4 +- gee-rpc/day7-registry/client.go | 4 +- gee-rpc/doc/geerpc-day4.md | 268 ++++++++++++++++++++++++++++ 7 files changed, 278 insertions(+), 10 deletions(-) create mode 100644 gee-rpc/doc/geerpc-day4.md diff --git a/gee-rpc/day2-client/client.go b/gee-rpc/day2-client/client.go index 4af2ebf..41cff0f 100644 --- a/gee-rpc/day2-client/client.go +++ b/gee-rpc/day2-client/client.go @@ -238,7 +238,7 @@ func Dial(network, address string, opts ...*Option) (client *Client, err error) } // close the connection if client is nil defer func() { - if client == nil { + if err != nil { _ = conn.Close() } }() diff --git a/gee-rpc/day3-service/client.go b/gee-rpc/day3-service/client.go index 77c9436..5beef93 100644 --- a/gee-rpc/day3-service/client.go +++ b/gee-rpc/day3-service/client.go @@ -237,7 +237,7 @@ func Dial(network, address string, opts ...*Option) (client *Client, err error) } // close the connection if client is nil defer func() { - if client == nil { + if err != nil { _ = conn.Close() } }() diff --git a/gee-rpc/day4-timeout/client.go b/gee-rpc/day4-timeout/client.go index 29af7aa..bc3602a 100644 --- a/gee-rpc/day4-timeout/client.go +++ b/gee-rpc/day4-timeout/client.go @@ -245,13 +245,13 @@ func dialTimeout(f newClientFunc, network, address string, opts ...*Option) (cli if err != nil { return nil, err } - conn, err := net.Dial(network, address) + conn, err := net.DialTimeout(network, address, opt.ConnectTimeout) if err != nil { return nil, err } // close the connection if client is nil defer func() { - if client == nil { + if err != nil { _ = conn.Close() } }() diff --git a/gee-rpc/day5-http-debug/client.go b/gee-rpc/day5-http-debug/client.go index b8b57f7..1a62b1e 100644 --- a/gee-rpc/day5-http-debug/client.go +++ b/gee-rpc/day5-http-debug/client.go @@ -249,13 +249,13 @@ func dialTimeout(f newClientFunc, network, address string, opts ...*Option) (cli if err != nil { return nil, err } - conn, err := net.Dial(network, address) + conn, err := net.DialTimeout(network, address, opt.ConnectTimeout) if err != nil { return nil, err } // close the connection if client is nil defer func() { - if client == nil { + if err != nil { _ = conn.Close() } }() diff --git a/gee-rpc/day6-load-balance/client.go b/gee-rpc/day6-load-balance/client.go index b8b57f7..1a62b1e 100644 --- a/gee-rpc/day6-load-balance/client.go +++ b/gee-rpc/day6-load-balance/client.go @@ -249,13 +249,13 @@ func dialTimeout(f newClientFunc, network, address string, opts ...*Option) (cli if err != nil { return nil, err } - conn, err := net.Dial(network, address) + conn, err := net.DialTimeout(network, address, opt.ConnectTimeout) if err != nil { return nil, err } // close the connection if client is nil defer func() { - if client == nil { + if err != nil { _ = conn.Close() } }() diff --git a/gee-rpc/day7-registry/client.go b/gee-rpc/day7-registry/client.go index b8b57f7..1a62b1e 100644 --- a/gee-rpc/day7-registry/client.go +++ b/gee-rpc/day7-registry/client.go @@ -249,13 +249,13 @@ func dialTimeout(f newClientFunc, network, address string, opts ...*Option) (cli if err != nil { return nil, err } - conn, err := net.Dial(network, address) + conn, err := net.DialTimeout(network, address, opt.ConnectTimeout) if err != nil { return nil, err } // close the connection if client is nil defer func() { - if client == nil { + if err != nil { _ = conn.Close() } }() diff --git a/gee-rpc/doc/geerpc-day4.md b/gee-rpc/doc/geerpc-day4.md new file mode 100644 index 0000000..be573e4 --- /dev/null +++ b/gee-rpc/doc/geerpc-day4.md @@ -0,0 +1,268 @@ +--- +title: 动手写RPC框架 - GeeRPC第四天 超时处理(timeout) +date: 2020-10-07 23:00:00 +description: 7天用 Go语言/golang 从零实现 RPC 框架 GeeRPC 教程(7 days implement golang remote procedure call framework from scratch tutorial),动手写 RPC 框架,参照 golang 标准库 net/rpc 的实现,实现了服务端(server)、支持异步和并发的客户端(client)、消息编码与解码(message encoding and decoding)、服务注册(service register)、支持 TCP/Unix/HTTP 等多种传输协议。第四天为RPC框架提供了处理超时的能力(timeout processing)。 +tags: +- Go +nav: 从零实现 +categories: +- RPC框架 - GeeRPC +keywords: +- Go语言 +- 从零实现RPC框架 +- 连接超时 +image: post/geerpc/geerpc.jpg +github: https://github.com/geektutu/7days-golang +--- + +![golang RPC framework](geerpc/geerpc.jpg) + +本文是[7天用Go从零实现RPC框架GeeRPC]的第四篇。 + +- 增加连接超时的处理机制 +- 增加服务端处理超时的处理机制,代码约 100 行 + +## 为什么需要超时处理机制 + +超时处理是 RPC 框架一个比较基本的能力,如果缺少超时处理机制,无论是服务端还是客户端都容易因为网络或其他错误导致挂死,资源耗尽,这些问题的出现大大地降低了服务的可用性。因此,我们需要在 RPC 框架中加入超时处理的能力。 + +纵观整个远程调用的过程,需要客户端处理超时的地方有: + +- 与服务端建立连接,导致的超时 +- 发送请求到服务端,写报文导致的超时 +- 等待服务端处理时,等待处理导致的超时(比如服务端已挂死,迟迟不响应) +- 从服务端接收响应时,读报文导致的超时 + +需要服务端处理超时的地方有: + +- 读取客户端请求报文时,读报文导致的超时 +- 发送响应报文时,写报文导致的超时 +- 调用映射服务的方法时,处理报文导致的超时 + + +GeeRPC 在 3 个地方添加了超时处理机制。分别是: + +1)客户端创建连接时 +2)客户端 `Client.Call()` 整个过程导致的超时(包含发送报文,等待处理,接收报文所有阶段) +3)服务端处理报文,即 `Server.handleRequest` 超时。 + +## 创建连接超时 + +为了实现上的简单,将超时设定放在了 Option 中。ConnectTimeout 默认值为 10s,HandleTimeout 默认值为 0,即不设限。 + +```go +type Option struct { + MagicNumber int // MagicNumber marks this's a geerpc request + CodecType codec.Type // client may choose different Codec to encode body + ConnectTimeout time.Duration // 0 means no limit + HandleTimeout time.Duration +} + +var DefaultOption = &Option{ + MagicNumber: MagicNumber, + CodecType: codec.GobType, + ConnectTimeout: time.Second * 10, +} +``` + +客户端连接超时,只需要为 Dial 添加一层超时处理的外壳即可。 + +[day4-timeout/client.go](https://github.com/geektutu/7days-golang/tree/master/gee-rpc/day4-timeout) + +```go +type clientResult struct { + client *Client + err error +} + +type newClientFunc func(conn net.Conn, opt *Option) (client *Client, err error) + +func dialTimeout(f newClientFunc, network, address string, opts ...*Option) (client *Client, err error) { + opt, err := parseOptions(opts...) + if err != nil { + return nil, err + } + conn, err := net.DialTimeout(network, address, opt.ConnectTimeout) + if err != nil { + return nil, err + } + // close the connection if client is nil + defer func() { + if err != nil { + _ = conn.Close() + } + }() + ch := make(chan clientResult) + go func() { + client, err := f(conn, opt) + ch <- clientResult{client: client, err: err} + }() + if opt.ConnectTimeout == 0 { + result := <-ch + return result.client, result.err + } + select { + case <-time.After(opt.ConnectTimeout): + return nil, fmt.Errorf("rpc client: connect timeout: expect within %s", opt.ConnectTimeout) + case result := <-ch: + return result.client, result.err + } +} + +// Dial connects to an RPC server at the specified network address +func Dial(network, address string, opts ...*Option) (*Client, error) { + return dialTimeout(NewClient, network, address, opts...) +} +``` + +在这里实现了一个超时处理的外壳 `dialTimeout`,这个壳将 NewClient 作为入参,在 2 个地方添加了超时处理的机制。 + +1) 将 `net.Dial` 替换为 `net.DialTimeout`,如果连接创建超时,将返回错误。 +2)使用子协程执行 NewClient,执行完成后则通过信道 ch 发送结果,如果 `time.After()` 信道先接收到消息,则说明 NewClient 执行超时,返回错误。 + +## Client.Call 超时 + +`Client.Call` 的超时处理机制,使用 context 包实现,控制权交给用户,控制更为灵活。 + +```go +// Call invokes the named function, waits for it to complete, +// and returns its error status. +func (client *Client) Call(ctx context.Context, serviceMethod string, args, reply interface{}) error { + call := client.Go(serviceMethod, args, reply, make(chan *Call, 1)) + select { + case <-ctx.Done(): + client.removeCall(call.Seq) + return errors.New("rpc client: call failed: " + ctx.Err().Error()) + case call := <-call.Done: + return call.Error + } +} +``` + +用户可以使用 `context.WithTimeout` 创建具备超时检测能力的 context 对象来控制。例如: + +```go +ctx, _ := context.WithTimeout(context.Background(), time.Second) +var reply int +err := client.Call(ctx, "Foo.Sum", &Args{1, 2}, &reply) +... +``` + +## 服务端处理超时 + +这一部分的实现与客户端很接近,使用 `time.After()` 结合 `select+chan` 完成。 + +[day4-timeout/server.go](https://github.com/geektutu/7days-golang/tree/master/gee-rpc/day4-timeout) + +```go +func (server *Server) handleRequest(cc codec.Codec, req *request, sending *sync.Mutex, wg *sync.WaitGroup, timeout time.Duration) { + defer wg.Done() + called := make(chan struct{}) + sent := make(chan struct{}) + go func() { + err := req.svc.call(req.mtype, req.argv, req.replyv) + called <- struct{}{} + if err != nil { + req.h.Error = err.Error() + server.sendResponse(cc, req.h, invalidRequest, sending) + sent <- struct{}{} + return + } + server.sendResponse(cc, req.h, req.replyv.Interface(), sending) + sent <- struct{}{} + }() + + if timeout == 0 { + <-called + <-sent + return + } + select { + case <-time.After(timeout): + req.h.Error = fmt.Sprintf("rpc server: request handle timeout: expect within %s", timeout) + server.sendResponse(cc, req.h, invalidRequest, sending) + case <-called: + <-sent + } +} +``` + +这里需要确保 `sendResponse` 仅调用一次,因此将整个过程拆分为 `called` 和 `sent` 两个阶段,在这段代码中只会发生如下两种情况: + +1) called 信道接收到消息,代表处理没有超时,继续执行 sendResponse。 +2) `time.After()` 先于 called 接收到消息,说明处理已经超时,called 和 sent 都将被阻塞。在 `case <-time.After(timeout)` 处调用 `sendResponse`。 + +## 测试用例 + +第一个测试用例,用于测试连接超时。NewClient 函数耗时 2s,ConnectionTimeout 分别设置为 1s 和 0 两种场景。 + +[day4-timeout/client_test.go](https://github.com/geektutu/7days-golang/tree/master/gee-rpc/day4-timeout) + +```go +func TestClient_dialTimeout(t *testing.T) { + t.Parallel() + l, _ := net.Listen("tcp", ":0") + + f := func(conn net.Conn, opt *Option) (client *Client, err error) { + _ = conn.Close() + time.Sleep(time.Second * 2) + return nil, nil + } + t.Run("timeout", func(t *testing.T) { + _, err := dialTimeout(f, "tcp", l.Addr().String(), &Option{ConnectTimeout: time.Second}) + _assert(err != nil && strings.Contains(err.Error(), "connect timeout"), "expect a timeout error") + }) + t.Run("0", func(t *testing.T) { + _, err := dialTimeout(f, "tcp", l.Addr().String(), &Option{ConnectTimeout: 0}) + _assert(err == nil, "0 means no limit") + }) +} +``` + +第二个测试用例,用于测试处理超时。`Bar.Timeout` 耗时 2s,场景一:客户端设置超时时间为 1s,服务端无限制;场景二,服务端设置超时时间为1s,客户端无限制。 + +```go +type Bar int + +func (b Bar) Timeout(argv int, reply *int) error { + time.Sleep(time.Second * 2) + return nil +} + +func startServer(addr chan string) { + var b Bar + _ = Register(&b) + // pick a free port + l, _ := net.Listen("tcp", ":0") + addr <- l.Addr().String() + Accept(l) +} + +func TestClient_Call(t *testing.T) { + t.Parallel() + addrCh := make(chan string) + go startServer(addrCh) + addr := <-addrCh + time.Sleep(time.Second) + t.Run("client timeout", func(t *testing.T) { + client, _ := Dial("tcp", addr) + ctx, _ := context.WithTimeout(context.Background(), time.Second) + var reply int + err := client.Call(ctx, "Bar.Timeout", 1, &reply) + _assert(err != nil && strings.Contains(err.Error(), ctx.Err().Error()), "expect a timeout error") + }) + t.Run("server handle timeout", func(t *testing.T) { + client, _ := Dial("tcp", addr, &Option{ + HandleTimeout: time.Second, + }) + var reply int + err := client.Call(context.Background(), "Bar.Timeout", 1, &reply) + _assert(err != nil && strings.Contains(err.Error(), "handle timeout"), "expect a timeout error") + }) +} +``` + +## 附 推荐阅读 + +- [Go 语言简明教程](https://geektutu.com/post/quick-golang.html) +- [Go 语言笔试面试题](https://geektutu.com/post/qa-golang.html) \ No newline at end of file From e871cb3b8bc9037c33f37e867a81dc272bb978cd Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Thu, 8 Oct 2020 12:39:49 +0800 Subject: [PATCH 096/122] gee-rpc: doc add day5 day6 day7 --- gee-rpc/day6-load-balance/main/main.go | 3 +- .../day6-load-balance/xclient/discovery.go | 14 +- gee-rpc/day7-registry/main/main.go | 11 +- gee-rpc/day7-registry/registry/registry.go | 36 +- gee-rpc/day7-registry/xclient/discovery.go | 5 +- gee-rpc/doc/geerpc-day1.md | 4 +- gee-rpc/doc/geerpc-day2.md | 2 +- gee-rpc/doc/geerpc-day3.md | 2 +- gee-rpc/doc/geerpc-day4.md | 2 +- gee-rpc/doc/geerpc-day5.md | 374 +++++++++++++++ gee-rpc/doc/geerpc-day5/geerpc_debug.png | Bin 0 -> 5823 bytes gee-rpc/doc/geerpc-day6.md | 438 ++++++++++++++++++ gee-rpc/doc/geerpc-day7.md | 395 ++++++++++++++++ gee-rpc/doc/geerpc-day7/registry.jpg | Bin 0 -> 19887 bytes 14 files changed, 1251 insertions(+), 35 deletions(-) create mode 100644 gee-rpc/doc/geerpc-day5.md create mode 100644 gee-rpc/doc/geerpc-day5/geerpc_debug.png create mode 100644 gee-rpc/doc/geerpc-day6.md create mode 100644 gee-rpc/doc/geerpc-day7.md create mode 100644 gee-rpc/doc/geerpc-day7/registry.jpg diff --git a/gee-rpc/day6-load-balance/main/main.go b/gee-rpc/day6-load-balance/main/main.go index 550e848..2a476b7 100644 --- a/gee-rpc/day6-load-balance/main/main.go +++ b/gee-rpc/day6-load-balance/main/main.go @@ -46,7 +46,7 @@ func foo(xc *xclient.XClient, ctx context.Context, typ, serviceMethod string, ar if err != nil { log.Printf("%s %s error: %v", typ, serviceMethod, err) } else { - log.Printf("%s Foo.Sum success: %d + %d = %d", typ, args.Num1, args.Num2, reply) + log.Printf("%s %s success: %d + %d = %d", typ, serviceMethod, args.Num1, args.Num2, reply) } } @@ -69,6 +69,7 @@ func call(addr1, addr2 string) { func broadcast(addr1, addr2 string) { d := xclient.NewMultiServerDiscovery([]string{"tcp@" + addr1, "tcp@" + addr2}) xc := xclient.NewXClient(d, xclient.RandomSelect, nil) + defer func() { _ = xc.Close() }() var wg sync.WaitGroup for i := 0; i < 5; i++ { wg.Add(1) diff --git a/gee-rpc/day6-load-balance/xclient/discovery.go b/gee-rpc/day6-load-balance/xclient/discovery.go index f823a7b..70d1cbb 100644 --- a/gee-rpc/day6-load-balance/xclient/discovery.go +++ b/gee-rpc/day6-load-balance/xclient/discovery.go @@ -2,6 +2,7 @@ package xclient import ( "errors" + "math" "math/rand" "sync" "time" @@ -49,15 +50,16 @@ func (d *MultiServersDiscovery) Update(servers []string) error { func (d *MultiServersDiscovery) Get(mode SelectMode) (string, error) { d.mu.Lock() defer d.mu.Unlock() - if len(d.servers) == 0 { + n := len(d.servers) + if n == 0 { return "", errors.New("rpc discovery: no available servers") } switch mode { case RandomSelect: - return d.servers[d.r.Intn(len(d.servers))], nil + return d.servers[d.r.Intn(n)], nil case RoundRobinSelect: - s := d.servers[d.index] - d.index = (d.index + 1) % len(d.servers) + s := d.servers[d.index%n] // servers could be updated, so mode n to ensure safety + d.index = (d.index + 1) % n return s, nil default: return "", errors.New("rpc discovery: not supported select mode") @@ -76,8 +78,10 @@ func (d *MultiServersDiscovery) GetAll() ([]string, error) { // NewMultiServerDiscovery creates a MultiServersDiscovery instance func NewMultiServerDiscovery(servers []string) *MultiServersDiscovery { - return &MultiServersDiscovery{ + d := &MultiServersDiscovery{ servers: servers, r: rand.New(rand.NewSource(time.Now().UnixNano())), } + d.index = d.r.Intn(math.MaxInt32 - 1) + return d } diff --git a/gee-rpc/day7-registry/main/main.go b/gee-rpc/day7-registry/main/main.go index 50b57dd..1797707 100644 --- a/gee-rpc/day7-registry/main/main.go +++ b/gee-rpc/day7-registry/main/main.go @@ -3,7 +3,7 @@ package main import ( "context" "geerpc" - registy "geerpc/registry" + "geerpc/registry" "geerpc/xclient" "log" "net" @@ -29,17 +29,17 @@ func (f Foo) Sleep(args Args, reply *int) error { func startRegistry(wg *sync.WaitGroup) { l, _ := net.Listen("tcp", ":9999") - registy.HandleHTTP() + registry.HandleHTTP() wg.Done() _ = http.Serve(l, nil) } -func startServer(registry string, wg *sync.WaitGroup) { +func startServer(registryAddr string, wg *sync.WaitGroup) { var foo Foo l, _ := net.Listen("tcp", ":0") server := geerpc.NewServer() _ = server.Register(&foo) - registy.Heartbeat(registry, "tcp@"+l.Addr().String(), 0) + registry.Heartbeat(registryAddr, "tcp@"+l.Addr().String(), 0) wg.Done() server.Accept(l) } @@ -56,7 +56,7 @@ func foo(xc *xclient.XClient, ctx context.Context, typ, serviceMethod string, ar if err != nil { log.Printf("%s %s error: %v", typ, serviceMethod, err) } else { - log.Printf("%s Foo.Sum success: %d + %d = %d", typ, args.Num1, args.Num2, reply) + log.Printf("%s %s success: %d + %d = %d", typ, serviceMethod, args.Num1, args.Num2, reply) } } @@ -79,6 +79,7 @@ func call(registry string) { func broadcast(registry string) { d := xclient.NewGeeRegistryDiscovery(registry, 0) xc := xclient.NewXClient(d, xclient.RandomSelect, nil) + defer func() { _ = xc.Close() }() var wg sync.WaitGroup for i := 0; i < 5; i++ { wg.Add(1) diff --git a/gee-rpc/day7-registry/registry/registry.go b/gee-rpc/day7-registry/registry/registry.go index 2c22244..29d23a8 100644 --- a/gee-rpc/day7-registry/registry/registry.go +++ b/gee-rpc/day7-registry/registry/registry.go @@ -1,4 +1,4 @@ -package registy +package registry import ( "log" @@ -9,10 +9,10 @@ import ( "time" ) -// Registry is a simple register center, provide following functions. +// GeeRegistry is a simple register center, provide following functions. // add a server and receive heartbeat to keep it alive. // returns all alive servers and delete dead servers sync simultaneously. -type Registry struct { +type GeeRegistry struct { timeout time.Duration mu sync.Mutex // protect following servers map[string]*ServerItem @@ -23,17 +23,22 @@ type ServerItem struct { start time.Time } +const ( + defaultPath = "/_geerpc_/registry" + defaultTimeout = time.Minute * 5 +) + // New create a registry instance with timeout setting -func New(timeout time.Duration) *Registry { - return &Registry{ +func New(timeout time.Duration) *GeeRegistry { + return &GeeRegistry{ servers: make(map[string]*ServerItem), timeout: timeout, } } -var DefaultRegister = New(defaultTimeout) +var DefaultGeeRegister = New(defaultTimeout) -func (r *Registry) putServer(addr string) { +func (r *GeeRegistry) putServer(addr string) { r.mu.Lock() defer r.mu.Unlock() s := r.servers[addr] @@ -44,7 +49,7 @@ func (r *Registry) putServer(addr string) { } } -func (r *Registry) aliveServers() []string { +func (r *GeeRegistry) aliveServers() []string { r.mu.Lock() defer r.mu.Unlock() var alive []string @@ -59,13 +64,8 @@ func (r *Registry) aliveServers() []string { return alive } -const ( - defaultPath = "/_geerpc_/registry" - defaultTimeout = time.Minute * 5 -) - // Runs at /_geerpc_/registry -func (r *Registry) ServeHTTP(w http.ResponseWriter, req *http.Request) { +func (r *GeeRegistry) ServeHTTP(w http.ResponseWriter, req *http.Request) { switch req.Method { case "GET": // keep it simple, server is in req.Header @@ -83,14 +83,14 @@ func (r *Registry) ServeHTTP(w http.ResponseWriter, req *http.Request) { } } -// HandleHTTP registers an HTTP handler for Registry messages on registryPath -func (r *Registry) HandleHTTP(registryPath string) { +// HandleHTTP registers an HTTP handler for GeeRegistry messages on registryPath +func (r *GeeRegistry) HandleHTTP(registryPath string) { http.Handle(registryPath, r) log.Println("rpc registry path:", registryPath) } func HandleHTTP() { - DefaultRegister.HandleHTTP(defaultPath) + DefaultGeeRegister.HandleHTTP(defaultPath) } // Heartbeat send a heartbeat message every once in a while @@ -113,7 +113,7 @@ func Heartbeat(registry, addr string, duration time.Duration) { } func sendHeartbeat(registry, addr string) error { - log.Println(addr, "send heart beat to registry") + log.Println(addr, "send heart beat to registry", registry) httpClient := &http.Client{} req, _ := http.NewRequest("POST", registry, nil) req.Header.Set("X-Geerpc-Server", addr) diff --git a/gee-rpc/day7-registry/xclient/discovery.go b/gee-rpc/day7-registry/xclient/discovery.go index 783ec4a..70d1cbb 100644 --- a/gee-rpc/day7-registry/xclient/discovery.go +++ b/gee-rpc/day7-registry/xclient/discovery.go @@ -2,6 +2,7 @@ package xclient import ( "errors" + "math" "math/rand" "sync" "time" @@ -77,8 +78,10 @@ func (d *MultiServersDiscovery) GetAll() ([]string, error) { // NewMultiServerDiscovery creates a MultiServersDiscovery instance func NewMultiServerDiscovery(servers []string) *MultiServersDiscovery { - return &MultiServersDiscovery{ + d := &MultiServersDiscovery{ servers: servers, r: rand.New(rand.NewSource(time.Now().UnixNano())), } + d.index = d.r.Intn(math.MaxInt32 - 1) + return d } diff --git a/gee-rpc/doc/geerpc-day1.md b/gee-rpc/doc/geerpc-day1.md index abfeb3c..51db1fb 100644 --- a/gee-rpc/doc/geerpc-day1.md +++ b/gee-rpc/doc/geerpc-day1.md @@ -19,7 +19,7 @@ github: https://github.com/geektutu/7days-golang ![golang RPC framework](geerpc/geerpc.jpg) -本文是[7天用Go从零实现RPC框架GeeRPC]的第一篇。 +本文是[7天用Go从零实现RPC框架GeeRPC](https://geektutu.com/post/geerpc.html)的第一篇。 - 使用 `encoding/gob` 实现消息的编解码(序列化与反序列化) - 实现一个简易的服务端,仅接受消息,不处理,代码约 200 行 @@ -54,7 +54,7 @@ type Header struct { - Error 是错误信息,客户端置为空,服务端如果如果发生错误,将错误信息置于 Error 中。 -我们将和消息编解码相关的代码都防到 codec 目录中。 +我们将和消息编解码相关的代码都防到 codec 子目录中,在此之前,还需要在根目录下使用 `go mod init geerpc` 初始化项目,方便后续子 package 之间的引用。 进一步,抽象出对消息体进行编解码的接口 Codec,抽象出接口是为了实现不同的 Codec 实例: diff --git a/gee-rpc/doc/geerpc-day2.md b/gee-rpc/doc/geerpc-day2.md index bf4db48..ff2ce20 100644 --- a/gee-rpc/doc/geerpc-day2.md +++ b/gee-rpc/doc/geerpc-day2.md @@ -19,7 +19,7 @@ github: https://github.com/geektutu/7days-golang ![golang RPC framework](geerpc/geerpc.jpg) -本文是[7天用Go从零实现RPC框架GeeRPC]的第二篇。 +本文是[7天用Go从零实现RPC框架GeeRPC](https://geektutu.com/post/geerpc.html)的第二篇。 - 实现一个支持异步和并发的高性能客户端,代码约 250 行 diff --git a/gee-rpc/doc/geerpc-day3.md b/gee-rpc/doc/geerpc-day3.md index 7a0c4af..d1a6982 100644 --- a/gee-rpc/doc/geerpc-day3.md +++ b/gee-rpc/doc/geerpc-day3.md @@ -18,7 +18,7 @@ github: https://github.com/geektutu/7days-golang ![golang RPC framework](geerpc/geerpc.jpg) -本文是[7天用Go从零实现RPC框架GeeRPC]的第三篇。 +本文是[7天用Go从零实现RPC框架GeeRPC](https://geektutu.com/post/geerpc.html)的第三篇。 - 通过反射实现服务注册功能 - 在服务端实现服务调用,代码约 150 行 diff --git a/gee-rpc/doc/geerpc-day4.md b/gee-rpc/doc/geerpc-day4.md index be573e4..b249d2e 100644 --- a/gee-rpc/doc/geerpc-day4.md +++ b/gee-rpc/doc/geerpc-day4.md @@ -17,7 +17,7 @@ github: https://github.com/geektutu/7days-golang ![golang RPC framework](geerpc/geerpc.jpg) -本文是[7天用Go从零实现RPC框架GeeRPC]的第四篇。 +本文是[7天用Go从零实现RPC框架GeeRPC](https://geektutu.com/post/geerpc.html)的第四篇。 - 增加连接超时的处理机制 - 增加服务端处理超时的处理机制,代码约 100 行 diff --git a/gee-rpc/doc/geerpc-day5.md b/gee-rpc/doc/geerpc-day5.md new file mode 100644 index 0000000..5b1797d --- /dev/null +++ b/gee-rpc/doc/geerpc-day5.md @@ -0,0 +1,374 @@ +--- +title: 动手写RPC框架 - GeeRPC第五天 支持HTTP协议 +date: 2020-10-08 11:00:00 +description: 7天用 Go语言/golang 从零实现 RPC 框架 GeeRPC 教程(7 days implement golang remote procedure call framework from scratch tutorial),动手写 RPC 框架,参照 golang 标准库 net/rpc 的实现,实现了服务端(server)、支持异步和并发的客户端(client)、消息编码与解码(message encoding and decoding)、服务注册(service register)、支持 TCP/Unix/HTTP 等多种传输协议。第五天支持了 HTTP 协议,并且提供了一个简单的 DEBUG 页面。 +tags: +- Go +nav: 从零实现 +categories: +- RPC框架 - GeeRPC +keywords: +- Go语言 +- 从零实现RPC框架 +- HTTP +- debug +image: post/geerpc/geerpc.jpg +github: https://github.com/geektutu/7days-golang +--- + +![golang RPC framework](geerpc/geerpc.jpg) + +本文是[7天用Go从零实现RPC框架GeeRPC](https://geektutu.com/post/geerpc.html)的第五篇。 + +- 支持 HTTP 协议 +- 基于 HTTP 实现一个简单的 Debug 页面,代码约 150 行。 + +## 支持 HTTP 协议需要做什么? + +Web 开发中,我们经常使用 HTTP 协议中的 HEAD、GET、POST 等方式发送请求,等待响应。但 RPC 的消息格式与标准的 HTTP 协议并不兼容,在这种情况下,就需要一个协议的转换过程。HTTP 协议的 CONNECT 方法恰好提供了这个能力,CONNECT 一般用于代理服务。 + +假设浏览器与服务器之间的 HTTPS 通信都是加密的,浏览器通过代理服务器发起 HTTPS 请求时,由于请求的站点地址和端口号都是加密保存在 HTTPS 请求报文头中的,代理服务器如何知道往哪里发送请求呢?为了解决这个问题,浏览器通过 HTTP 明文形式向代理服务器发送一个 CONNECT 请求告诉代理服务器目标地址和端口,代理服务器接收到这个请求后,会在对应端口与目标站点建立一个 TCP 连接,连接建立成功后返回 HTTP 200 状态码告诉浏览器与该站点的加密通道已经完成。接下来代理服务器仅需透传浏览器和服务器之间的加密数据包即可,代理服务器无需解析 HTTPS 报文。 + +举一个简单例子: + +1) 浏览器向代理服务器发送 CONNECT 请求。 + +```bash +CONNECT geektutu.com:443 HTTP/1.0 +``` + +2) 代理服务器返回 HTTP 200 状态码表示连接已经建立。 + +```bash +HTTP/1.0 200 Connection Established +``` + +3) 之后浏览器和服务器开始 HTTPS 握手并交换加密数据,代理服务器只负责传输彼此的数据包,并不能读取具体数据内容(代理服务器也可以选择安装可信根证书解密 HTTPS 报文)。 + +事实上,这个过程其实是通过代理服务器将 HTTP 协议转换为 HTTPS 协议的过程。对 RPC 服务端来,需要做的是将 HTTP 协议转换为 RPC 协议,对客户端来说,需要新增通过 HTTP CONNECT 请求创建连接的逻辑。 + + +## 服务端支持 HTTP 协议 + +那通信过程应该是这样的: + +1) 客户端向 RPC 服务器发送 CONNECT 请求 + +```bash +CONNECT 10.0.0.1:9999/_geerpc_ HTTP/1.0 +``` + +2) RPC 服务器返回 HTTP 200 状态码表示连接建立。 + +```bash +HTTP/1.0 200 Connected to Gee RPC +``` + +3) 客户端使用创建好的连接发送 RPC 报文,先发送 Option,再发送 N 个请求报文,服务端处理 RPC 请求并响应。 + +在 `server.go` 中新增如下的方法: + +[day5-http-debug/server.go](https://github.com/geektutu/7days-golang/tree/master/gee-rpc/day5-http-debug) + +```go +const ( + connected = "200 Connected to Gee RPC" + defaultRPCPath = "/_geeprc_" + defaultDebugPath = "/debug/geerpc" +) + +// ServeHTTP implements an http.Handler that answers RPC requests. +func (server *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) { + if req.Method != "CONNECT" { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.WriteHeader(http.StatusMethodNotAllowed) + _, _ = io.WriteString(w, "405 must CONNECT\n") + return + } + conn, _, err := w.(http.Hijacker).Hijack() + if err != nil { + log.Print("rpc hijacking ", req.RemoteAddr, ": ", err.Error()) + return + } + _, _ = io.WriteString(conn, "HTTP/1.0 "+connected+"\n\n") + server.ServeConn(conn) +} + +// HandleHTTP registers an HTTP handler for RPC messages on rpcPath. +// It is still necessary to invoke http.Serve(), typically in a go statement. +func (server *Server) HandleHTTP() { + http.Handle(defaultRPCPath, server) +} + +// HandleHTTP is a convenient approach for default server to register HTTP handlers +func HandleHTTP() { + DefaultServer.HandleHTTP() +} +``` + +`defaultDebugPath` 是为后续 DEBUG 页面预留的地址。 + +在 Go 语言中处理 HTTP 请求是非常简单的一件事,Go 标准库中 `http.Handle` 的实现如下: + +```go +package http +// Handle registers the handler for the given pattern +// in the DefaultServeMux. +// The documentation for ServeMux explains how patterns are matched. +func Handle(pattern string, handler Handler) { DefaultServeMux.Handle(pattern, handler) } +``` + +第一个参数是支持通配的字符串 pattern,在这里,我们固定传入 `/_geeprc_`,第二个参数是 Handler 类型,Handler 是一个接口类型,定义如下: + +```go +type Handler interface { + ServeHTTP(w ResponseWriter, r *Request) +} +``` + +也就是说,只需要实现接口 Handler 即可作为一个 HTTP Handler 处理 HTTP 请求。接口 Handler 只定义了一个方法 `ServeHTTP`,实现该方法即可。 + +> 关于 http.Handler 的更多信息,推荐阅读 [Go语言动手写Web框架 - Gee第一天 http.Handler](https://geektutu.com/post/gee-day1.html) + +## 客户端支持 HTTP 协议 + +服务端已经能够接受 CONNECT 请求,并返回了 200 状态码 `HTTP/1.0 200 Connected to Gee RPC`,客户端要做的,发起 CONNECT 请求,检查返回状态码即可成功建立连接。 + +[day5-http-debug/client.go](https://github.com/geektutu/7days-golang/tree/master/gee-rpc/day5-http-debug) + +```go +// NewHTTPClient new a Client instance via HTTP as transport protocol +func NewHTTPClient(conn net.Conn, opt *Option) (*Client, error) { + _, _ = io.WriteString(conn, fmt.Sprintf("CONNECT %s HTTP/1.0\n\n", defaultRPCPath)) + + // Require successful HTTP response + // before switching to RPC protocol. + resp, err := http.ReadResponse(bufio.NewReader(conn), &http.Request{Method: "CONNECT"}) + if err == nil && resp.Status == connected { + return NewClient(conn, opt) + } + if err == nil { + err = errors.New("unexpected HTTP response: " + resp.Status) + } + return nil, err +} + +// DialHTTP connects to an HTTP RPC server at the specified network address +// listening on the default HTTP RPC path. +func DialHTTP(network, address string, opts ...*Option) (*Client, error) { + return dialTimeout(NewHTTPClient, network, address, opts...) +} +``` + +通过 HTTP CONNECT 请求建立连接之后,后续的通信过程就交给 NewClient 了。 + +为了简化调用,提供了一个统一入口 `XDial` + +```go +// XDial calls different functions to connect to a RPC server +// according the first parameter rpcAddr. +// rpcAddr is a general format (protocol@addr) to represent a rpc server +// eg, http@10.0.0.1:7001, tcp@10.0.0.1:9999, unix@/tmp/geerpc.sock +func XDial(rpcAddr string, opts ...*Option) (*Client, error) { + parts := strings.Split(rpcAddr, "@") + if len(parts) != 2 { + return nil, fmt.Errorf("rpc client err: wrong format '%s', expect protocol@addr", rpcAddr) + } + protocol, addr := parts[0], parts[1] + switch protocol { + case "http": + return DialHTTP("tcp", addr, opts...) + default: + // tcp, unix or other transport protocol + return Dial(protocol, addr, opts...) + } +} +``` + +添加一个测试用例试一试,这个测试用例使用了 unix 协议创建 socket 连接,适用于本机内部的通信,使用上与 TCP 协议并无区别。 + +[day5-http-debug/client_test.go](https://github.com/geektutu/7days-golang/tree/master/gee-rpc/day5-http-debug) + +```go +func TestXDial(t *testing.T) { + if runtime.GOOS == "linux" { + ch := make(chan struct{}) + addr := "/tmp/geerpc.sock" + go func() { + _ = os.Remove(addr) + l, err := net.Listen("unix", addr) + if err != nil { + t.Fatal("failed to listen unix socket") + } + ch <- struct{}{} + Accept(l) + }() + <-ch + _, err := XDial("unix@" + addr) + _assert(err == nil, "failed to connect unix socket") + } +} +``` + + +## 实现简单的 DEBUG 页面 + +支持 HTTP 协议的好处在于,RPC 服务仅仅使用了监听端口的 `/_geerpc` 路径,在其他路径上我们可以提供诸如日志、统计等更为丰富的功能。接下来我们在 `/debug/geerpc` 上展示服务的调用统计视图。 + +[day5-http-debug/debug.go](https://github.com/geektutu/7days-golang/tree/master/gee-rpc/day5-http-debug) + +```go +package geerpc + +import ( + "fmt" + "html/template" + "net/http" +) + +const debugText = ` + + GeeRPC Services + {{range .}} +
+ Service {{.Name}} +
+ + + {{range $name, $mtype := .Method}} + + + + + {{end}} +
MethodCalls
{{$name}}({{$mtype.ArgType}}, {{$mtype.ReplyType}}) error{{$mtype.NumCalls}}
+ {{end}} + + ` + +var debug = template.Must(template.New("RPC debug").Parse(debugText)) + +type debugHTTP struct { + *Server +} + +type debugService struct { + Name string + Method map[string]*methodType +} + +// Runs at /debug/geerpc +func (server debugHTTP) ServeHTTP(w http.ResponseWriter, req *http.Request) { + // Build a sorted version of the data. + var services []debugService + server.serviceMap.Range(func(namei, svci interface{}) bool { + svc := svci.(*service) + services = append(services, debugService{ + Name: namei.(string), + Method: svc.method, + }) + return true + }) + err := debug.Execute(w, services) + if err != nil { + _, _ = fmt.Fprintln(w, "rpc: error executing template:", err.Error()) + } +} +``` + +在这里,我们将返回一个 HTML 报文,这个报文将展示注册所有的 service 的每一个方法的调用情况。 + +将 debugHTTP 实例绑定到地址 `/debug/geerpc`。 + +```go +func (server *Server) HandleHTTP() { + http.Handle(defaultRPCPath, server) + http.Handle(defaultDebugPath, debugHTTP{server}) + log.Println("rpc server debug path:", defaultDebugPath) +} +``` + +## Demo + +OK,我们已经迫不及待地想看看最终的效果了。 + +[day5-http-debug/main/main.go](https://github.com/geektutu/7days-golang/tree/master/gee-rpc/day5-http-debug) + +和之前的例子相比较,将 startServer 中的 `geerpc.Accept()` 替换为了 `geerpc.HandleHTTP()`,端口固定为 9999。 + +```go +type Foo int + +type Args struct{ Num1, Num2 int } + +func (f Foo) Sum(args Args, reply *int) error { + *reply = args.Num1 + args.Num2 + return nil +} + +func startServer(addrCh chan string) { + var foo Foo + l, _ := net.Listen("tcp", ":9999") + _ = geerpc.Register(&foo) + geerpc.HandleHTTP() + addrCh <- l.Addr().String() + _ = http.Serve(l, nil) +} +``` + +客户端将 `Dial` 替换为 `DialHTTP`,其余地方没有发生改变。 + +```go +func call(addrCh chan string) { + client, _ := geerpc.DialHTTP("tcp", <-addrCh) + defer func() { _ = client.Close() }() + + time.Sleep(time.Second) + // send request & receive response + var wg sync.WaitGroup + for i := 0; i < 5; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + args := &Args{Num1: i, Num2: i * i} + var reply int + if err := client.Call(context.Background(), "Foo.Sum", args, &reply); err != nil { + log.Fatal("call Foo.Sum error:", err) + } + log.Printf("%d + %d = %d", args.Num1, args.Num2, reply) + }(i) + } + wg.Wait() +} + +func main() { + log.SetFlags(0) + ch := make(chan string) + go call(ch) + startServer(ch) +} +``` + +main 函数中,我们在最后调用 `startServer`,服务启动后将一直等待。 + +运行结果如下: + +```bash +main$ go run . +rpc server: register Foo.Sum +rpc server debug path: /debug/geerpc +3 + 9 = 12 +2 + 4 = 6 +4 + 16 = 20 +0 + 0 = 0 +1 + 1 = 2 +``` + +服务已经启动,此时我们如果在浏览器中访问 `localhost:9999/debug/geerpc`,将会看到: + +![geerpc services debug](geerpc-day5/geerpc_debug.png) + +## 附 推荐阅读 + +- [Go 语言简明教程](https://geektutu.com/post/quick-golang.html) +- [Go 语言笔试面试题](https://geektutu.com/post/qa-golang.html) \ No newline at end of file diff --git a/gee-rpc/doc/geerpc-day5/geerpc_debug.png b/gee-rpc/doc/geerpc-day5/geerpc_debug.png new file mode 100644 index 0000000000000000000000000000000000000000..b60e390d981df024ca3ae4a69d67cf7e891a4cc0 GIT binary patch literal 5823 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 Z?EbrkF_8`@sC?Uig~|CdRY*77zX6|I*hByT literal 0 HcmV?d00001 diff --git a/gee-rpc/doc/geerpc-day6.md b/gee-rpc/doc/geerpc-day6.md new file mode 100644 index 0000000..87f5877 --- /dev/null +++ b/gee-rpc/doc/geerpc-day6.md @@ -0,0 +1,438 @@ +--- +title: 动手写RPC框架 - GeeRPC第六天 负载均衡(load balance) +date: 2020-10-08 14:00:00 +description: 7天用 Go语言/golang 从零实现 RPC 框架 GeeRPC 教程(7 days implement golang remote procedure call framework from scratch tutorial),动手写 RPC 框架,参照 golang 标准库 net/rpc 的实现,实现了服务端(server)、支持异步和并发的客户端(client)、消息编码与解码(message encoding and decoding)、服务注册(service register)、支持 TCP/Unix/HTTP 等多种传输协议。第六天实现了2种简单的负载均衡(load balance)算法,随机选择和 Round Robin 轮询调度算法。 +tags: +- Go +nav: 从零实现 +categories: +- RPC框架 - GeeRPC +keywords: +- Go语言 +- 从零实现RPC框架 +- 负载均衡 +- 轮询调度 +image: post/geerpc/geerpc.jpg +github: https://github.com/geektutu/7days-golang +--- + +![golang RPC framework](geerpc/geerpc.jpg) + +本文是[7天用Go从零实现RPC框架GeeRPC](https://geektutu.com/post/geerpc.html)的第六篇。 + +- 通过随机选择和 Round Robin 轮询调度算法实现服务端负载均衡,代码约 250 行 + +## 负载均衡策略 + +假设有多个服务实例,每个实例提供相同的功能,为了提高整个系统的吞吐量,每个实例部署在不同的机器上。客户端可以选择任意一个实例进行调用,获取想要的结果。那如何选择呢?取决了负载均衡的策略。对于 RPC 框架来说,我们可以很容易地想到这么几种策略: + +- 随机选择策略 - 从服务列表中随机选择一个。 +- 轮询算法(Round Robin) - 依次调度不同的服务器,每次调度执行 i = (i + 1) mode n。 +- 加权轮询(Weight Round Robin) - 在轮询算法的基础上,为每个服务实例设置一个权重,高性能的机器赋予更高的权重,也可以根据服务实例的当前的负载情况做动态的调整,例如考虑最近5分钟部署服务器的 CPU、内存消耗情况。 +- 哈希/一致性哈希策略 - 依据请求的某些特征,计算一个 hash 值,根据 hash 值将请求发送到对应的机器。一致性 hash 还可以解决服务实例动态添加情况下,调度抖动的问题。一致性哈希的一个典型应用场景是分布式缓存服务。感兴趣可以阅读[动手写分布式缓存 - GeeCache第四天 一致性哈希(hash)](https://geektutu.com/post/geecache-day4.html) +- ... + +## 服务发现 + +负载均衡的前提是有多个服务实例,那我们首先实现一个最基础的服务发现模块 Discovery。为了与通信部分解耦,这部分的代码统一放置在 xclient 子目录下。 + +定义 2 个类型: + +- SelectMode 代表不同的负载均衡策略,简单起见,GeeRPC 仅实现 Random 和 RoundRobin 两种策略。 +- Discovery 是一个接口类型,包含了服务发现所需要的最基本的接口。 + - Refresh() 从注册中心更新服务列表 + - Update(servers []string) 手动更新服务列表 + - Get(mode SelectMode) 根据负载均衡策略,选择一个服务实例 + - GetAll() 返回所有的服务实例 + +[day6-load-balance/xclient/discovery.go](https://github.com/geektutu/7days-golang/tree/master/gee-rpc/day6-load-balance) + +```go +package xclient + +import ( + "errors" + "math" + "math/rand" + "sync" + "time" +) + +type SelectMode int + +const ( + RandomSelect SelectMode = iota // select randomly + RoundRobinSelect // select using Robbin algorithm +) + +type Discovery interface { + Refresh() error // refresh from remote registry + Update(servers []string) error + Get(mode SelectMode) (string, error) + GetAll() ([]string, error) +} +``` + +紧接着,我们实现一个不需要注册中心,服务列表由手工维护的服务发现的结构体:MultiServersDiscovery + +```go +// MultiServersDiscovery is a discovery for multi servers without a registry center +// user provides the server addresses explicitly instead +type MultiServersDiscovery struct { + r *rand.Rand // generate random number + mu sync.RWMutex // protect following + servers []string + index int // record the selected position for robin algorithm +} + +// NewMultiServerDiscovery creates a MultiServersDiscovery instance +func NewMultiServerDiscovery(servers []string) *MultiServersDiscovery { + d := &MultiServersDiscovery{ + servers: servers, + r: rand.New(rand.NewSource(time.Now().UnixNano())), + } + d.index = d.r.Intn(math.MaxInt32 - 1) + return d +} +``` + +- r 是一个产生随机数的实例,初始化时使用时间戳设定随机数种子,避免每次产生相同的随机数序列。 +- index 记录 Round Robin 算法已经轮询到的位置,为了避免每次从 0 开始,初始化时随机设定一个值。 + +然后,实现 Discovery 接口 + +```go +var _ Discovery = (*MultiServersDiscovery)(nil) + +// Refresh doesn't make sense for MultiServersDiscovery, so ignore it +func (d *MultiServersDiscovery) Refresh() error { + return nil +} + +// Update the servers of discovery dynamically if needed +func (d *MultiServersDiscovery) Update(servers []string) error { + d.mu.Lock() + defer d.mu.Unlock() + d.servers = servers + return nil +} + +// Get a server according to mode +func (d *MultiServersDiscovery) Get(mode SelectMode) (string, error) { + d.mu.Lock() + defer d.mu.Unlock() + n := len(d.servers) + if n == 0 { + return "", errors.New("rpc discovery: no available servers") + } + switch mode { + case RandomSelect: + return d.servers[d.r.Intn(n)], nil + case RoundRobinSelect: + s := d.servers[d.index%n] // servers could be updated, so mode n to ensure safety + d.index = (d.index + 1) % n + return s, nil + default: + return "", errors.New("rpc discovery: not supported select mode") + } +} + +// returns all servers in discovery +func (d *MultiServersDiscovery) GetAll() ([]string, error) { + d.mu.RLock() + defer d.mu.RUnlock() + // return a copy of d.servers + servers := make([]string, len(d.servers), len(d.servers)) + copy(servers, d.servers) + return servers, nil +} +``` + +## 支持负载均衡的客户端 + +接下来,我们向用户暴露一个支持负载均衡的客户端 XClient。 + +[day6-load-balance/xclient/xclient.go](https://github.com/geektutu/7days-golang/tree/master/gee-rpc/day6-load-balance) + +```go +package xclient + +import ( + "context" + . "geerpc" + "io" + "reflect" + "sync" +) + +type XClient struct { + d Discovery + mode SelectMode + opt *Option + mu sync.Mutex // protect following + clients map[string]*Client +} + +var _ io.Closer = (*XClient)(nil) + +func NewXClient(d Discovery, mode SelectMode, opt *Option) *XClient { + return &XClient{d: d, mode: mode, opt: opt, clients: make(map[string]*Client)} +} + +func (xc *XClient) Close() error { + xc.mu.Lock() + defer xc.mu.Unlock() + for key, client := range xc.clients { + // I have no idea how to deal with error, just ignore it. + _ = client.Close() + delete(xc.clients, key) + } + return nil +} +``` + +XClient 的构造函数需要传入三个参数,服务发现实例 Discovery、负载均衡模式 SelectMode 以及协议选项 Option。为了尽量地复用已经创建好的 Socket 连接,使用 clients 保存创建成功的 Client 实例,并提供 Close 方法在结束后,关闭已经建立的连接。 + +接下来,实现客户端最基本的功能 `Call`。 + +```go +func (xc *XClient) dial(rpcAddr string) (*Client, error) { + xc.mu.Lock() + defer xc.mu.Unlock() + client, ok := xc.clients[rpcAddr] + if ok && !client.IsAvailable() { + _ = client.Close() + delete(xc.clients, rpcAddr) + client = nil + } + if client == nil { + var err error + client, err = XDial(rpcAddr, xc.opt) + if err != nil { + return nil, err + } + xc.clients[rpcAddr] = client + } + return client, nil +} + +func (xc *XClient) call(rpcAddr string, ctx context.Context, serviceMethod string, args, reply interface{}) error { + client, err := xc.dial(rpcAddr) + if err != nil { + return err + } + return client.Call(ctx, serviceMethod, args, reply) +} + +// Call invokes the named function, waits for it to complete, +// and returns its error status. +// xc will choose a proper server. +func (xc *XClient) Call(ctx context.Context, serviceMethod string, args, reply interface{}) error { + rpcAddr, err := xc.d.Get(xc.mode) + if err != nil { + return err + } + return xc.call(rpcAddr, ctx, serviceMethod, args, reply) +} +``` + +我们将复用 Client 的能力封装在方法 `dial` 中,dial 的处理逻辑如下: + +1) 检查 `xc.clients` 是否有缓存的 Client,如果有,检查是否是可用状态,如果是则返回缓存的 Client,如果不可用,则从缓存中删除。 +2) 如果步骤 1) 没有返回缓存的 Client,则说明需要创建新的 Client,缓存并返回。 + +另外,我们为 XClient 添加一个常用功能:`Broadcast`。 + +```go +// Broadcast invokes the named function for every server registered in discovery +func (xc *XClient) Broadcast(ctx context.Context, serviceMethod string, args, reply interface{}) error { + servers, err := xc.d.GetAll() + if err != nil { + return err + } + var wg sync.WaitGroup + var mu sync.Mutex // protect e and replyDone + var e error + replyDone := reply == nil // if reply is nil, don't need to set value + ctx, cancel := context.WithCancel(ctx) + for _, rpcAddr := range servers { + wg.Add(1) + go func() { + defer wg.Done() + var clonedReply interface{} + if reply != nil { + clonedReply = reflect.New(reflect.ValueOf(reply).Elem().Type()).Interface() + } + err := xc.call(rpcAddr, ctx, serviceMethod, args, clonedReply) + mu.Lock() + if err != nil && e == nil { + e = err + cancel() // if any call failed, cancel unfinished calls + } + if err == nil && !replyDone { + reflect.ValueOf(reply).Elem().Set(reflect.ValueOf(clonedReply).Elem()) + replyDone = true + } + mu.Unlock() + }() + } + wg.Wait() + return e +} +``` + +Broadcast 将请求广播到所有的服务实例,如果任意一个实例发生错误,则返回其中一个错误;如果调用成功,则返回其中一个的结果。有以下几点需要注意: + +1) 为了提升性能,请求是并发的。 +2) 并发情况下需要使用互斥锁保证 error 和 reply 能被正确赋值。 +3) 借助 context.WithCancel 确保有错误发生时,快速失败。 + +## Demo + +又到了 Demo 环节,我们还是借助一个简单的 Demo 验证今天的成果吧。 + +首先,启动 RPC 服务的代码还是类似的,Sum 是正常的方法,Sleep 用于验证 XClient 的超时机制能否正常运作。 + +[day6-load-balance/main/main.go](https://github.com/geektutu/7days-golang/tree/master/gee-rpc/day6-load-balance) + +```go +package main + +import ( + "context" + "geerpc" + "geerpc/xclient" + "log" + "net" + "sync" + "time" +) + +type Foo int + +type Args struct{ Num1, Num2 int } + +func (f Foo) Sum(args Args, reply *int) error { + *reply = args.Num1 + args.Num2 + return nil +} + +func (f Foo) Sleep(args Args, reply *int) error { + time.Sleep(time.Second * time.Duration(args.Num1)) + *reply = args.Num1 + args.Num2 + return nil +} + +func startServer(addrCh chan string) { + var foo Foo + l, _ := net.Listen("tcp", ":0") + server := geerpc.NewServer() + _ = server.Register(&foo) + addrCh <- l.Addr().String() + server.Accept(l) +} +``` + +封装一个方法 `foo`,便于在 `Call` 或 `Broadcast` 之后统一打印成功或失败的日志。 + +```go +func foo(xc *xclient.XClient, ctx context.Context, typ, serviceMethod string, args *Args) { + var reply int + var err error + switch typ { + case "call": + err = xc.Call(ctx, serviceMethod, args, &reply) + case "broadcast": + err = xc.Broadcast(ctx, serviceMethod, args, &reply) + } + if err != nil { + log.Printf("%s %s error: %v", typ, serviceMethod, err) + } else { + log.Printf("%s %s success: %d + %d = %d", typ, serviceMethod, args.Num1, args.Num2, reply) + } +} +``` + +call 调用单个服务实例,broadcast 调用所有服务实例 + +```go +func call(addr1, addr2 string) { + d := xclient.NewMultiServerDiscovery([]string{"tcp@" + addr1, "tcp@" + addr2}) + xc := xclient.NewXClient(d, xclient.RandomSelect, nil) + defer func() { _ = xc.Close() }() + // send request & receive response + var wg sync.WaitGroup + for i := 0; i < 5; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + foo(xc, context.Background(), "call", "Foo.Sum", &Args{Num1: i, Num2: i * i}) + }(i) + } + wg.Wait() +} + +func broadcast(addr1, addr2 string) { + d := xclient.NewMultiServerDiscovery([]string{"tcp@" + addr1, "tcp@" + addr2}) + xc := xclient.NewXClient(d, xclient.RandomSelect, nil) + defer func() { _ = xc.Close() }() + var wg sync.WaitGroup + for i := 0; i < 5; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + foo(xc, context.Background(), "broadcast", "Foo.Sum", &Args{Num1: i, Num2: i * i}) + // expect 2 - 5 timeout + ctx, _ := context.WithTimeout(context.Background(), time.Second*2) + foo(xc, ctx, "broadcast", "Foo.Sleep", &Args{Num1: i, Num2: i * i}) + }(i) + } + wg.Wait() +} + + +func main() { + log.SetFlags(0) + ch1 := make(chan string) + ch2 := make(chan string) + // start two servers + go startServer(ch1) + go startServer(ch2) + + addr1 := <-ch1 + addr2 := <-ch2 + + time.Sleep(time.Second) + call(addr1, addr2) + broadcast(addr1, addr2) +} +``` + +运行结果如下: + +```go +rpc server: register Foo.Sleep +rpc server: register Foo.Sum +rpc server: register Foo.Sleep +rpc server: register Foo.Sum +call Foo.Sum success: 4 + 16 = 20 +call Foo.Sum success: 0 + 0 = 0 +call Foo.Sum success: 3 + 9 = 12 +call Foo.Sum success: 2 + 4 = 6 +call Foo.Sum success: 1 + 1 = 2 +broadcast Foo.Sum success: 3 + 9 = 12 +broadcast Foo.Sum success: 1 + 1 = 2 +broadcast Foo.Sum success: 0 + 0 = 0 +broadcast Foo.Sum success: 4 + 16 = 20 +broadcast Foo.Sum success: 2 + 4 = 6 +broadcast Foo.Sleep success: 0 + 0 = 0 +broadcast Foo.Sleep success: 1 + 1 = 2 +broadcast Foo.Sleep error: rpc client: call failed: context deadline exceeded +broadcast Foo.Sleep error: rpc client: call failed: context deadline exceeded +broadcast Foo.Sleep error: rpc client: call failed: context deadline exceeded +``` + +## 附 推荐阅读 + +- [Go 语言简明教程](https://geektutu.com/post/quick-golang.html) +- [Go 语言笔试面试题](https://geektutu.com/post/qa-golang.html) \ No newline at end of file diff --git a/gee-rpc/doc/geerpc-day7.md b/gee-rpc/doc/geerpc-day7.md new file mode 100644 index 0000000..32105ef --- /dev/null +++ b/gee-rpc/doc/geerpc-day7.md @@ -0,0 +1,395 @@ +--- +title: 动手写RPC框架 - GeeRPC第六天 服务发现与注册中心(registry) +date: 2020-10-08 16:00:00 +description: 7天用 Go语言/golang 从零实现 RPC 框架 GeeRPC 教程(7 days implement golang remote procedure call framework from scratch tutorial),动手写 RPC 框架,参照 golang 标准库 net/rpc 的实现,实现了服务端(server)、支持异步和并发的客户端(client)、消息编码与解码(message encoding and decoding)、服务注册(service register)、支持 TCP/Unix/HTTP 等多种传输协议。第七天实现了一个简单的注册中心(registry),具备超时移除、接收心跳(heartbeat)等能力,并且实现了一个简单的服务发现(server discovery)模块。 +tags: +- Go +nav: 从零实现 +categories: +- RPC框架 - GeeRPC +keywords: +- Go语言 +- 从零实现RPC框架 +- 注册中心 +- 服务发现 +image: post/geerpc/geerpc.jpg +github: https://github.com/geektutu/7days-golang +--- + +![golang RPC framework](geerpc/geerpc.jpg) + +本文是[7天用Go从零实现RPC框架GeeRPC](https://geektutu.com/post/geerpc.html)的第七篇。 + +- 实现一个简单的注册中心,支持服务注册、接收心跳等功能 +- 客户端实现基于注册中心的服务发现机制,代码约 250 行 + +## 注册中心的位置 + +![geerpc registry](geerpc-day7/registry.jpg) + +注册中心的位置如上图所示。注册中心的好处在于,客户端和服务端都只需要感知注册中心的存在,而无需感知对方的存在。更具体一些: + +1) 服务端启动后,向注册中心发送注册消息,注册中心得知该服务已经启动,处于可用状态。一般来说,服务端还需要定期向注册中心发送心跳,证明自己还活着。 +2) 客户端向注册中心询问,当前哪天服务是可用的,注册中心将可用的服务列表返回客户端。 +3) 客户端根据注册中心得到的服务列表,选择其中一个发起调用。 + +如果没有注册中心,就像 GeeRPC 第六天实现的一样,客户端需要硬编码服务端的地址,而且没有机制保证服务端是否处于可用状态。当然注册中心的功能还有很多,比如配置的动态同步、通知机制等。比较常用的注册中心有 [etcd](https://github.com/etcd-io/etcd)、[zookeeper](https://github.com/apache/zookeeper)、[consul](https://github.com/hashicorp/consul),一般比较出名的微服务或者 RPC 框架,这些主流的注册中心都是支持的。 + + +## Gee Registry + +主流的注册中心 etcd、zookeeper 等功能强大,与这类注册中心的对接代码量是比较大的,需要实现的接口很多。GeeRPC 选择自己实现一个简单的支持心跳保活的注册中心。 + +GeeRegistry 的代码独立放置在子目录 registry 中。 + +首先定义 GeeRegistry 结构体,默认超时时间设置为 5 min,也就是说,任何注册的服务超过 5 min,即视为不可用状态。 + +[day7-registry/registry/registry.go](https://github.com/geektutu/7days-golang/tree/master/gee-rpc/day7-registry) + +```go +// GeeRegistry is a simple register center, provide following functions. +// add a server and receive heartbeat to keep it alive. +// returns all alive servers and delete dead servers sync simultaneously. +type GeeRegistry struct { + timeout time.Duration + mu sync.Mutex // protect following + servers map[string]*ServerItem +} + +type ServerItem struct { + Addr string + start time.Time +} + +const ( + defaultPath = "/_geerpc_/registry" + defaultTimeout = time.Minute * 5 +) + +// New create a registry instance with timeout setting +func New(timeout time.Duration) *GeeRegistry { + return &GeeRegistry{ + servers: make(map[string]*ServerItem), + timeout: timeout, + } +} + +var DefaultGeeRegister = New(defaultTimeout) +``` + +为 GeeRegistry 实现添加服务实例和返回服务列表的方法。 + +- putServer:添加服务实例,如果服务已经存在,则更新 start。 +- aliveServers:返回可用的服务列表,如果存在超时的服务,则删除。 + +```go +func (r *GeeRegistry) putServer(addr string) { + r.mu.Lock() + defer r.mu.Unlock() + s := r.servers[addr] + if s == nil { + r.servers[addr] = &ServerItem{Addr: addr, start: time.Now()} + } else { + s.start = time.Now() // if exists, update start time to keep alive + } +} + +func (r *GeeRegistry) aliveServers() []string { + r.mu.Lock() + defer r.mu.Unlock() + var alive []string + for addr, s := range r.servers { + if r.timeout == 0 || s.start.Add(r.timeout).After(time.Now()) { + alive = append(alive, addr) + } else { + delete(r.servers, addr) + } + } + sort.Strings(alive) + return alive +} +``` + +为了实现上的简单,GeeRegistry 采用 HTTP 协议提供服务,且所有的有用信息都承载在 HTTP Header 中。 + +- Get:返回所有可用的服务列表,通过自定义字段 X-Geerpc-Servers 承载。 +- Post:添加服务实例或发送心跳,通过自定义字段 X-Geerpc-Server 承载。 + +```go +// Runs at /_geerpc_/registry +func (r *GeeRegistry) ServeHTTP(w http.ResponseWriter, req *http.Request) { + switch req.Method { + case "GET": + // keep it simple, server is in req.Header + w.Header().Set("X-Geerpc-Servers", strings.Join(r.aliveServers(), ",")) + case "POST": + // keep it simple, server is in req.Header + addr := req.Header.Get("X-Geerpc-Server") + if addr == "" { + w.WriteHeader(http.StatusInternalServerError) + return + } + r.putServer(addr) + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } +} + +// HandleHTTP registers an HTTP handler for GeeRegistry messages on registryPath +func (r *GeeRegistry) HandleHTTP(registryPath string) { + http.Handle(registryPath, r) + log.Println("rpc registry path:", registryPath) +} + +func HandleHTTP() { + DefaultGeeRegister.HandleHTTP(defaultPath) +} +``` + +另外,提供 Heartbeat 方法,便于服务启动时定时向注册中心发送心跳,默认周期比注册中心设置的过期时间少 1 min。 + +```go +// Heartbeat send a heartbeat message every once in a while +// it's a helper function for a server to register or send heartbeat +func Heartbeat(registry, addr string, duration time.Duration) { + if duration == 0 { + // make sure there is enough time to send heart beat + // before it's removed from registry + duration = defaultTimeout - time.Duration(1)*time.Minute + } + var err error + err = sendHeartbeat(registry, addr) + go func() { + t := time.NewTicker(duration) + for err == nil { + <-t.C + err = sendHeartbeat(registry, addr) + } + }() +} + +func sendHeartbeat(registry, addr string) error { + log.Println(addr, "send heart beat to registry", registry) + httpClient := &http.Client{} + req, _ := http.NewRequest("POST", registry, nil) + req.Header.Set("X-Geerpc-Server", addr) + if _, err := httpClient.Do(req); err != nil { + log.Println("rpc server: heart beat err:", err) + return err + } + return nil +} +``` + +## GeeRegistryDiscovery + +在 xclient 中对应实现 Discovery。 + +[day7-registry/xclient/discovery_gee.go](https://github.com/geektutu/7days-golang/tree/master/gee-rpc/day7-registry) + +```go +package xclient + +type GeeRegistryDiscovery struct { + *MultiServersDiscovery + registry string + timeout time.Duration + lastUpdate time.Time +} + +const defaultUpdateTimeout = time.Second * 10 + +func NewGeeRegistryDiscovery(registerAddr string, timeout time.Duration) *GeeRegistryDiscovery { + if timeout == 0 { + timeout = defaultUpdateTimeout + } + d := &GeeRegistryDiscovery{ + MultiServersDiscovery: NewMultiServerDiscovery(make([]string, 0)), + registry: registerAddr, + timeout: timeout, + } + return d +} +``` + +- GeeRegistryDiscovery 嵌套了 MultiServersDiscovery,很多能力可以复用。 +- registry 即注册中心的地址 +- timeout 服务列表的过期时间 +- lastUpdate 是代表最后从注册中心更新服务列表的时间,默认 10s 过期,即 10s 之后,需要从注册中心更新新的列表。 + +实现 Update 和 Refresh 方法,超时重新获取的逻辑在 Refresh 中实现: + +```go +func (d *GeeRegistryDiscovery) Update(servers []string) error { + d.mu.Lock() + defer d.mu.Unlock() + d.servers = servers + d.lastUpdate = time.Now() + return nil +} + +func (d *GeeRegistryDiscovery) Refresh() error { + d.mu.Lock() + defer d.mu.Unlock() + if d.lastUpdate.Add(d.timeout).After(time.Now()) { + return nil + } + log.Println("rpc registry: refresh servers from registry", d.registry) + resp, err := http.Get(d.registry) + if err != nil { + log.Println("rpc registry refresh err:", err) + return err + } + servers := strings.Split(resp.Header.Get("X-Geerpc-Servers"), ",") + d.servers = make([]string, 0, len(servers)) + for _, server := range servers { + if strings.TrimSpace(server) != "" { + d.servers = append(d.servers, strings.TrimSpace(server)) + } + } + d.lastUpdate = time.Now() + return nil +} +``` + +`Get` 和 `GetAll` 与 MultiServersDiscovery 相似,唯一的不同在于,GeeRegistryDiscovery 需要先调用 Refresh 确保服务列表没有过期。 + +```go +func (d *GeeRegistryDiscovery) Get(mode SelectMode) (string, error) { + if err := d.Refresh(); err != nil { + return "", err + } + return d.MultiServersDiscovery.Get(mode) +} + +func (d *GeeRegistryDiscovery) GetAll() ([]string, error) { + if err := d.Refresh(); err != nil { + return nil, err + } + return d.MultiServersDiscovery.GetAll() +} +``` + +## Demo + +最后,依旧通过简单的 Demo 验证今天的成果。 + +添加函数 startRegistry,稍微修改 startServer,添加调用注册中心的 `Heartbeat` 方法的逻辑,定期向注册中心发送心跳保活。 + +[day7-registry/main/main.go](https://github.com/geektutu/7days-golang/tree/master/gee-rpc/day7-registry) + +```go +func startRegistry(wg *sync.WaitGroup) { + l, _ := net.Listen("tcp", ":9999") + registry.HandleHTTP() + wg.Done() + _ = http.Serve(l, nil) +} + +func startServer(registryAddr string, wg *sync.WaitGroup) { + var foo Foo + l, _ := net.Listen("tcp", ":0") + server := geerpc.NewServer() + _ = server.Register(&foo) + registry.Heartbeat(registryAddr, "tcp@"+l.Addr().String(), 0) + wg.Done() + server.Accept(l) +} +``` + +接下来,将 call 和 broadcast 的 MultiServersDiscovery 替换为 GeeRegistryDiscovery,不再需要硬编码服务列表。 + +```go +func call(registry string) { + d := xclient.NewGeeRegistryDiscovery(registry, 0) + xc := xclient.NewXClient(d, xclient.RandomSelect, nil) + defer func() { _ = xc.Close() }() + // send request & receive response + var wg sync.WaitGroup + for i := 0; i < 5; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + foo(xc, context.Background(), "call", "Foo.Sum", &Args{Num1: i, Num2: i * i}) + }(i) + } + wg.Wait() +} + +func broadcast(registry string) { + d := xclient.NewGeeRegistryDiscovery(registry, 0) + xc := xclient.NewXClient(d, xclient.RandomSelect, nil) + defer func() { _ = xc.Close() }() + var wg sync.WaitGroup + for i := 0; i < 5; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + foo(xc, context.Background(), "broadcast", "Foo.Sum", &Args{Num1: i, Num2: i * i}) + // expect 2 - 5 timeout + ctx, _ := context.WithTimeout(context.Background(), time.Second*2) + foo(xc, ctx, "broadcast", "Foo.Sleep", &Args{Num1: i, Num2: i * i}) + }(i) + } + wg.Wait() +} +``` + +最后在 main 函数中,将所有的逻辑串联起来,确保注册中心启动后,再启动 RPC 服务端,最后客户端远程调用。 + +```go +func main() { + log.SetFlags(0) + registryAddr := "http://localhost:9999/_geerpc_/registry" + var wg sync.WaitGroup + wg.Add(1) + go startRegistry(&wg) + wg.Wait() + + time.Sleep(time.Second) + wg.Add(2) + go startServer(registryAddr, &wg) + go startServer(registryAddr, &wg) + wg.Wait() + + time.Sleep(time.Second) + call(registryAddr) + broadcast(registryAddr) +} +``` + +运行结果如下: + +```go +rpc registry path: /_geerpc_/registry +rpc server: register Foo.Sleep +rpc server: register Foo.Sum +tcp@[::]:56276 send heart beat to registry http://localhost:9999/_geerpc_/registry +rpc server: register Foo.Sleep +rpc server: register Foo.Sum +tcp@[::]:56277 send heart beat to registry http://localhost:9999/_geerpc_/registry +rpc registry: refresh servers from registry http://localhost:9999/_geerpc_/registry +call Foo.Sum success: 3 + 9 = 12 +call Foo.Sum success: 4 + 16 = 20 +call Foo.Sum success: 1 + 1 = 2 +call Foo.Sum success: 0 + 0 = 0 +call Foo.Sum success: 2 + 4 = 6 +rpc registry: refresh servers from registry http://localhost:9999/_geerpc_/registry +broadcast Foo.Sum success: 4 + 16 = 20 +broadcast Foo.Sum success: 1 + 1 = 2 +broadcast Foo.Sum success: 3 + 9 = 12 +broadcast Foo.Sum success: 0 + 0 = 0 +broadcast Foo.Sum success: 2 + 4 = 6 +broadcast Foo.Sleep success: 0 + 0 = 0 +broadcast Foo.Sleep success: 1 + 1 = 2 +broadcast Foo.Sleep error: rpc client: call failed: context deadline exceeded +broadcast Foo.Sleep error: rpc client: call failed: context deadline exceeded +broadcast Foo.Sleep error: rpc client: call failed: context deadline exceeded +``` + +到这里,七天用 Go 从零实现 RPC 框架的教程也结束了。我们用七天时间参照 golang 标准库 net/rpc,实现了服务端以及支持并发的客户端,并且支持选择不同的序列化与反序列化方式;为了防止服务挂死,在其中一些关键部分添加了超时处理机制;支持 TCP、Unix、HTTP 等多种传输协议;支持多种负载均衡模式,最后还实现了一个简易的服务注册和发现中心。 + +## 附 推荐阅读 + +- [Go 语言简明教程](https://geektutu.com/post/quick-golang.html) +- [Go 语言笔试面试题](https://geektutu.com/post/qa-golang.html) \ No newline at end of file diff --git a/gee-rpc/doc/geerpc-day7/registry.jpg b/gee-rpc/doc/geerpc-day7/registry.jpg new file mode 100644 index 0000000000000000000000000000000000000000..726113206ba4748acadab290e033a96fcad8b230 GIT binary patch literal 19887 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 literal 0 HcmV?d00001 From e8f6f7ee0c1147c67277e6b585834d2ef59f53ce Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Thu, 8 Oct 2020 17:38:47 +0800 Subject: [PATCH 097/122] gee-rpc/doc fix day7 title --- gee-rpc/doc/geerpc-day7.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gee-rpc/doc/geerpc-day7.md b/gee-rpc/doc/geerpc-day7.md index 32105ef..ad4f502 100644 --- a/gee-rpc/doc/geerpc-day7.md +++ b/gee-rpc/doc/geerpc-day7.md @@ -1,5 +1,5 @@ --- -title: 动手写RPC框架 - GeeRPC第六天 服务发现与注册中心(registry) +title: 动手写RPC框架 - GeeRPC第七天 服务发现与注册中心(registry) date: 2020-10-08 16:00:00 description: 7天用 Go语言/golang 从零实现 RPC 框架 GeeRPC 教程(7 days implement golang remote procedure call framework from scratch tutorial),动手写 RPC 框架,参照 golang 标准库 net/rpc 的实现,实现了服务端(server)、支持异步和并发的客户端(client)、消息编码与解码(message encoding and decoding)、服务注册(service register)、支持 TCP/Unix/HTTP 等多种传输协议。第七天实现了一个简单的注册中心(registry),具备超时移除、接收心跳(heartbeat)等能力,并且实现了一个简单的服务发现(server discovery)模块。 tags: From fa606012192b1d40ff1ef1fecc725fcc150fbeed Mon Sep 17 00:00:00 2001 From: ImgBotApp Date: Fri, 16 Oct 2020 05:15:39 +0000 Subject: [PATCH 098/122] [ImgBot] Optimize images *Total -- 135.49kb -> 117.71kb (13.13%) /gee-rpc/doc/geerpc-day5/geerpc_debug.png -- 5.69kb -> 3.86kb (32.08%) /gee-cache/doc/geecache-day4/hash.jpg -- 21.63kb -> 18.50kb (14.46%) /gee-cache/doc/geecache-day5/dist_nodes.jpg -- 21.31kb -> 18.29kb (14.14%) /gee-cache/doc/geecache-day7/protobuf.jpg -- 20.87kb -> 17.97kb (13.89%) /gee-cache/doc/geecache-day6/singleflight.jpg -- 22.16kb -> 19.16kb (13.55%) /gee-cache/doc/geecache-day6/singleflight_logo.jpg -- 9.13kb -> 8.17kb (10.52%) /gee-cache/doc/geecache-day4/hash_logo.jpg -- 9.86kb -> 8.87kb (10.01%) /gee-cache/doc/geecache-day7/protobuf_logo.jpg -- 9.16kb -> 8.25kb (9.85%) /gee-cache/doc/geecache-day5/dist_nodes_logo.jpg -- 10.54kb -> 9.52kb (9.71%) /gee-cache/doc/geecache-day1/lru_logo.jpg -- 5.15kb -> 5.10kb (0.89%) Signed-off-by: ImgBotApp --- gee-cache/doc/geecache-day1/lru_logo.jpg | Bin 5272 -> 5225 bytes gee-cache/doc/geecache-day4/hash.jpg | Bin 22150 -> 18947 bytes gee-cache/doc/geecache-day4/hash_logo.jpg | Bin 10096 -> 9085 bytes gee-cache/doc/geecache-day5/dist_nodes.jpg | Bin 21817 -> 18731 bytes .../doc/geecache-day5/dist_nodes_logo.jpg | Bin 10792 -> 9744 bytes gee-cache/doc/geecache-day6/singleflight.jpg | Bin 22694 -> 19619 bytes .../doc/geecache-day6/singleflight_logo.jpg | Bin 9351 -> 8367 bytes gee-cache/doc/geecache-day7/protobuf.jpg | Bin 21374 -> 18405 bytes gee-cache/doc/geecache-day7/protobuf_logo.jpg | Bin 9376 -> 8452 bytes gee-rpc/doc/geerpc-day5/geerpc_debug.png | Bin 5823 -> 3955 bytes 10 files changed, 0 insertions(+), 0 deletions(-) diff --git a/gee-cache/doc/geecache-day1/lru_logo.jpg b/gee-cache/doc/geecache-day1/lru_logo.jpg index 7ab49f44e6a6e2b1483a5ef88ed3bce9b98de4bd..306b014ea4889c97efe15f4baa41134889fdd64f 100644 GIT binary patch delta 5087 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%0DNT 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
P%hIwB+mqX~@k6*TU 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 diff --git a/gee-cache/doc/geecache-day4/hash_logo.jpg b/gee-cache/doc/geecache-day4/hash_logo.jpg index 95778c9636dd2739189edb4c4cc89c1415f9f95b..c9cab999fc3595ef77d05d9772989e8d3c8a8d8e 100644 GIT binary patch literal 9085 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 YsamS0=(qXY2LBo24m_IQRR8}b0J~l7%K!iX literal 21817 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

$!F7DquD&Lmwy*VZG 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�n>WQjF7AHv zEiZakTCw35$BUWgs%E}^AQ!PtD90bm+a-;cioG8{ABjGwST;t zBd+`JUm^F;Q@m7d<(nnEZquKB7HO|qnZf@g{NUpATEd|MZXCSRKCk}i&(`_s{$io* zw7Q=^6x|en!%NQ&x7$vfa+dRFs#2!^Gykda8mj|&j(pDvt2@{mF8!%?nw(?X^9v9D zrTHK0jD7r4#AdqDhU>@Pt^8@b`DLU*_2F;Vmj8Hc>jO$y!K70)n(kSjxF_zn(dfKd z@I+Mm>)yzOT`6UgevA3H$2SAl&TCqbV=UA;b?5io`y298rZahurB`DgV{CGn<`x91h)m_$Q;YA@@s_wZRwmIs@T{H{ZDHo{wiNDBQVnmhhD| zUq4)aXa3+S|I)`6tY({$JX$}|cpH;?O5RUDC%0Wcg)$SE*KPQ4lQr1Jt-eFS^6dJi h)xeCE|Ht`1!?Go3mMZPLyy=Yn`h-INoA&>20st%f{xbjo literal 22694 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 diff --git a/gee-cache/doc/geecache-day6/singleflight_logo.jpg b/gee-cache/doc/geecache-day6/singleflight_logo.jpg index a53fb410020fe1f00e0219b0ca0b5a488c9b9bf2..4da1facc0f344b6cf3e2dc509319e0245734522b 100644 GIT binary patch literal 8367 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 literal 9351 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 literal 21374 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 diff --git a/gee-cache/doc/geecache-day7/protobuf_logo.jpg b/gee-cache/doc/geecache-day7/protobuf_logo.jpg index cacd9628408559325a6c74adddb4d6146711ac37..e97f835dc5dadd39c70b7c9fcfe774080f0a10e5 100644 GIT binary patch literal 8452 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 literal 9376 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 diff --git a/gee-rpc/doc/geerpc-day5/geerpc_debug.png b/gee-rpc/doc/geerpc-day5/geerpc_debug.png index b60e390d981df024ca3ae4a69d67cf7e891a4cc0..55a396d27e073cbc9f66a64ea41f49c4a3547f37 100644 GIT binary patch literal 3955 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 literal 5823 zcmbW4cUTkKw!p_olwO3R2vV#>K?72PfC&U?3K9e<0ttxpCen;j1A>ASMLHM*3aCi$ zB~dv>k)~2Y2aO~uB|@a65MGY=yRY5%zIVR&$LyIkvu4d+d$01FJE-$!V#0F5004+t zn4h@-03a~`x=L^de=Hk~5#pb=gxRt21+u(DX5(GF+!aSYo3Te*bQB}$#}Nu*p8-$ z%ug~m&Cj(pPiIgsk)G)--F&eP0DPqQ9>g7x;ZMi}!XHvlamCkpTr6{pj?v1%8~7^1 zc?~R*S;o$Zbt4Zw9>+0luVqArYju@@uS~_L1=VWtZ97)szy5U%3h7Wxy`M zN{K=A4}!+s5=hGUIVoZFo_gHZtK{zQz?OxE30+T6f?4C?tHx{AYn?k3m-K@dyJgMn$M7CuR~v)1Nxb2i5BKq& zd3Wn+1V~EBiRvbC@P0{i;~R5i2AhzqLYQUnJ}XJ=7Ug~KFJrNLNQ2tcjw?l$(sFCW z#JQf@-23=-DN#ju6T+~O%~^Bd(t1cwjAkWB%uX6~niZeJRqNubd-OS1TNgAFxld?M znh_f0MM6wiDQgbX`jLx@Mf_}gzUt0o!_YxQ#UE)B^~*{y#`JCS%ykm0yK*_B5Z|%B z7{i4Tr#d(`dPHb*C1sj3DyuTrw7M!$8Dbj!)2p2bD@@Z4!iPJ}KdJa*#GarmA+S8P zOt6sDwNuaZf^yUjTd}+moK~g5Zv)H4H!GrVZ;#)1^J%m|LhDv@yw=U=V?TeS(Wurj z=)1Y@C(oC)){n_h=fmF^8`Z&|uN+w@2%3BV3vNj35D|t0XI{AnL`^HZ8dhlae)PH_ zN^nyfMGg55e?irh8A|RuAp0&RNkw3nxL*uoeao<1sL>y*EVw2m`Y>v(mG!yyEL!B9 zE#S*uzA6M=J2#?4L;-YsZkl2N!;mw>fmwJW@xY1B@K~MG_!ZjfJdgXlwflpkfihw;N z)AveVue3yU?o?jv#{1lHuW<@2R%XD%k~ufgXXZX>9Snn-Fpu<9CE~6)RX?!SGi^4@ zoqx*-?4}EKH7>Kusv{R!R9?%|d@F6gUJt6O)npKl+|YJ>vEfpk)yT?Z&CeSLgp}Pm z_vaonohL3U>?uB>R;tiko9^m-j&6miePDD1V~PmYJ@Q%iUC56v$7r~N2Y-_S{Uacb zq5NgpD~ha7!|eUD(onfk)gxYc)Kfi=IUw6JMT!c3Dd(MSsfvL&kh8OsLF?_qU#X^* zVz5HmafO-wt@kfGOd**`#pxLoZTFkM)~mIGHygI50?#>WH|*D|JF-m?1_St z!Pm}@pQSL?^~cF!g~qbC2TDUK@6NRtN)D{MGlL?kAbt}C3c|?JeA`_IA`WYDP91*b z=I|Qsnq+YG?w#_fBeIT4MVE$`xGz+|7e03*;geNrGCtC2a+TE?)4a1YdCVgQjw37e zs7!;sjy0!EsSH1#q8xvSCsoX<{!?a~m!{6Nj{)JRDKl~I&(8!wp$kre=7puI#Zb{6 zP~7t{*J01F`4jS;5BhV1@m0{kx>BZnLbj5N0k+ zCWY*nB;XKokeLQmgH?UF`XXuEexHD;v61Iqd!M5@+MeiQa&V-59bWH+MwNuVeQ5PQ z(uS@>Q(8%cYbshP(M{PJu;;L6w{Ni@i3ub}MN4LRFgsy9M^bXlIl@>*zTQ-xYX`jI z7AQJ8Gx}&PT3;N@Dvf6L$5IH&inQ19n{AgbAC4s0&k^=l*DhCK$zsm~dIyb-PFdOU z-88B3^}hzGP-0v@Z6YOH5l;C6m8;U0UC@-geH5c5`kKl+I@qt#IA&cO)JwH?V4HNE zm9{Ei8kQa}+=w8!eOkNnWF}Iu`q1t~P$1GN%kpeNH!4d`9+SA^`O~q!aqYU8OT}IR z&M9&Cbu6EMKho1!(1|fMz>js5_dXwdz?sQzl?WL&j*sR>Nvb#Vea56B;~hQJM+qPHtmbf1GB5YfjhT}hZ=gJi#8g_%q=s#|Jii5r@b545B-lend9fB@Bo(pdYwLK+ zK^@HA>OMWc*Gucik*^*sF3;=GDXULrEX?J7Cs1F=QZTU>O|2bL+HEEG?xy8>vAp!6 zV|F@MYAPzi^eNo??nDB)CyO?jw%!9Ni0ptD9~q}zepq>@c`nE1ye+&j?_@PXrCY;i zJ=|@xZDtB9F9`0w3Ulp9dvjxAj)-2^ZW8E_Y8>xqtFC)SS@25|6f>KOIn13PZ6%9~ zP?K?87u?4@%Fe@RV@z!9-dfJv)N!{=ghu9l_s@9vG*1b-k~WG+X(oPiupEqbQdi)8 znvgHG7!U$))&#k}SsQ9PEKg4`35irFa2u&;TS=o>d1x_emu(X6*A+O7Sw_E^C`+60 ziR;$NetFN*HmKA;ojC4+;z1L>!`~^i=&XOp>$=hQohp}4DJXNaSGwJBKk#`n?&oH@ zIzGIJ84c}6m;^dG-vZs+nb32_d4nFyqAcBszj!i8Dc&$K{4PJ_WHP)ukeMl4 z`BX;Kdd4q?f_eAANlS8{h``X3+#M;@sKn!aT3Swll@?h_RoACChs;MyDz=Y|5DfVN z9qx2Ubl)*afx4L$g#^izLxS&Mo1UX^d_kfYqA}w0SUuKS%pI5q>Gbv_AVue-(9*d1P>@P7pkF!%u|(B(F|UX0!@P z7u&h_sgvbknebEDr-{|XV>Nz-*xcQOxJ%Z2<`_%e1MQWN`inWV`CQ{fJ75HnV9D2e(JTlay+a?s z!|>5O6Xr&@G!n!In0vbkd~kvw`80(}1OdP!K|bCYx^f1#*pL503`oisi~suUUzm&k zzRYhUnlh7Z;i>#rN-CsP`9f=GIriu5HoglcHY#=<@ZW^?@7n+ElmENf--@c@;Pt}P zc}jsl1v-ugfR{wEWZca?ePfMH8j;~6E|W~^+R|0REN`*iN2&BcCIFl_*Oo~%#l}!1 zP=z$Eu+G`$>SR#^0I0M+EVI%6t5AN6Gi=JI#z%6(I(@AMaJ-CgRNObkcJIE`rB-nS zX+HNTe?Xrrn(D`&`qTaK8e7>J1Bb`XX?*S`51~**FWuk_JKrB+e3RWjvv5s>C)EUf|$&ly3s{k%Vrqw&8$;7 zW?Q};aJ#hw8K0%q`}p%nL4y)}1fm+by4ucF3;3EchmG0zAe|wJLbDH(@PT)JGWh|9 zn0RH52W@2rR1o*Dy1cAg1Scx7@szsq@c5*n-15w|3jO_@xFLBou)1?n) z#zvz#0rEXmjc`2x81bI9h!5Tf$&dWZ3UkVn6#{#eI+#KvdA=si#Hc7obYO?OfsAfd zYX*6DL)@*tqy5aRfVWZtgV z9@yCP?e&X4PJCmwgnRbkQCAJxLbFF$u80^Aa$B77ei^mQWKg~Pbh}Ug7vJ59xR}$4 zIBLCbe|so~Uq@cbNeSF4x6F2{!V(^-%ZiuZ!d2x| zD!sHg$Y0nb+`{^YnpARBKH(8*K5V7z6ReJ_pi6M8g2-5&@>;gVKlVz(*&|$CSgkwb z^pLMWQ*U4{ze?NNf2NZH7P|I?okK912+Db>Uw4do6;v1+buVM-K-TvoNw}}~t~E?v+gx5V z>$%IgECe3(%vfQTL$7h?TAcA|L;_R-d~AFNKPI03V@q3X$YMhQFE?*t3S;whsy7bc zwR5X%EXBYtVm3`Lf^It{;;^RaI@uO?{c$ z-0wKM@vkQ1L-b^jr-p*!qD!Bjf33EUGkb-$`KKXd673*m|Wy1WwEB>FtRns3aR z8(S!LCWR;MDEB<={*yzw81c4%1h04z{G+%EkI8Z9G+r91{hn?%sXn;c#0hJjDML45 z&dFKL4O>KtZ!$K+##pI^vDXyeFW*p0sd4KbxrZMNa%OK*7L`twEf2~&u&~SXfz2V( zWyKAhp)KK$=~95WH#-Zrb|4RyJlcVhlsE@nv1f|)l0TQ9qov8CYL_-HL+ATrt?5VQ zN*jqE;+TP&hbG(>N>5VuzphPbZVisvyf#^Lr8V#>Wcx>8YR4&jUFENi5v0*IUKjk3 z`=7c*qh|P4`_m%Q#-!84foE@rM)5iFyP*g7ahld5RDz3OfbOF^{LU$F9kVP-yM+sA zySiV=S6Q!`_3P2eV9(Q7Tk~&PAEQ_XH1-b;{uq?aTDHj!w`-n}VC3j~BFx@J+?xAN zG@1C)@4i=f#mnB(yJ+yCzK-!kfD&~f&%ViXO=GoZv0MUwl*1&+FUNWl)>`3!xs@n5 zy&b|yy%^z+-y>WnQcQBA@1cdEBWW=czRwO!OseQ$iCcIS+u!$Eh?vM>AW*E+eD zqS_t-uj;0#>jj8Uvl{j^2)Nl` zAoJDgJd})uK%kFlCc2oMT=auY2G&-#ZIr&}=atiICezw=QKXU?)p>alun9F|uBWoH zba%&V9g6(hAo+74;*qDcs%!rc zIK(fiQfbfvg43I>_pD32*SVNHCb_Do2l_rcm3AE>II(B3Fj&$e>s=xFPui}vsm&$% zv#Tdy{<_1avl$^#Ty8^MRS-MExoz6a>Exj&*P@QUUV(S@Q|!KA-4q6ybCqd)+Td zu`9Q_2`O>_91{+TfR6{pv8L$0p^;z72eGM&q&tI(Dboto0foS}>i;nIf&NXUZ`QOJgvw0Q$}Ce~T*qhrfh>DX9NXw*OyC^ouf9 Z?EbrkF_8`@sC?Uig~|CdRY*77zX6|I*hByT From 3024e8b3af5959b6aa720e94b0d59566e049f62e Mon Sep 17 00:00:00 2001 From: Dai Jie Date: Mon, 19 Oct 2020 00:36:21 +0800 Subject: [PATCH 099/122] README.md: change the title apps to programs --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ff6b021..48e5228 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# 7 days golang apps from scratch +# 7 days golang programs from scratch

README 中文版本 @@ -78,7 +78,7 @@ GeeRPC 是基于 Go 语言标准库 `net/rpc` 实现的,添加了协议交换
-What can I write in 7 days? A gin-like web framework? A distributed cache like groupcache? Or a simple Python interpreter? Hope this repo can give you the answer. +What can be accomplished in 7 days? A gin-like web framework? A distributed cache like groupcache? Or a simple Python interpreter? Hope this repo can give you the answer. ## Web Framework - Gee @@ -137,4 +137,4 @@ Based on golang standard library `net/rpc`, GeeRPC implements more features. eg, - Demo 1 - Hello World [Code](demo-wasm/hello-world) - Demo 2 - Register Functions [Code](demo-wasm/register-functions) - Demo 3 - Manipulate DOM [Code](demo-wasm/manipulate-dom) -- Demo 4 - Callback [Code](demo-wasm/callback) \ No newline at end of file +- Demo 4 - Callback [Code](demo-wasm/callback) From 9ddb657d0b944f7ffe00b81cc68f8b0f9c0b82bf Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Mon, 19 Oct 2020 01:26:01 +0800 Subject: [PATCH 100/122] README.md: add badges --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 48e5228..aa0c66a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # 7 days golang programs from scratch +[![CodeSize](https://img.shields.io/github/languages/code-size/geektutu/7days-golang)](https://github.com/geektutu/7days-golang) +[![LICENSE](https://img.shields.io/badge/license-MIT-green)](https://mit-license.org/) +[![HitCount](https://hits.dwyl.com/geektutu/7days-golang.svg)](http://github.com/geektutu/7days-golang) +
README 中文版本
From c0e1f0915e8a0feadd7dc1bc3034fd376841695d Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Sun, 25 Oct 2020 18:41:12 +0800 Subject: [PATCH 101/122] add 7days-golang questions --- questions/7days-golang-q1.md | 201 ++++++++++++++++++ questions/7days-golang-q1/7days-golang-qa.jpg | Bin 0 -> 72108 bytes 2 files changed, 201 insertions(+) create mode 100644 questions/7days-golang-q1.md create mode 100644 questions/7days-golang-q1/7days-golang-qa.jpg diff --git a/questions/7days-golang-q1.md b/questions/7days-golang-q1.md new file mode 100644 index 0000000..f1e241b --- /dev/null +++ b/questions/7days-golang-q1.md @@ -0,0 +1,201 @@ +--- +title: Go 接口型函数的使用场景 +date: 2099-10-25 12:30:00 +description: Go 语言/golang 中函数式接口或接口型函数的实现与价值,什么是接口型函数,为什么不直接将函数作为参数,而是封装为一个接口。Go 语言标准库 net/http 中是如何使用接口型函数的。 +tags: +- Go +nav: 从零实现 +categories: +- 7days-golang Q & A +keywords: +- 函数式接口 +- 接口型函数 +- net/http +image: post/7days-golang-q1/7days-golang-qa.jpg +github: https://github.com/geektutu/7days-golang +--- + +![7days-golang 有价值的问题](7days-golang-q1/7days-golang-qa.jpg) + +## 问题 + +在 [动手写分布式缓存 - GeeCache第二天 单机并发缓存](https://geektutu.com/post/geecache-day2.html) 这篇文章中,有一个接口型函数的实现: + +```go +// A Getter loads data for a key. +type Getter interface { + Get(key string) ([]byte, error) +} + +// A GetterFunc implements Getter with a function. +type GetterFunc func(key string) ([]byte, error) + +// Get implements Getter interface function +func (f GetterFunc) Get(key string) ([]byte, error) { + return f(key) +} +``` + +这里呢,定义了一个接口 `Getter`,只包含一个方法 `Get(key string) ([]byte, error)`,紧接着定义了一个函数类型 `GetterFunc`,GetterFunc 参数和返回值与 Getter 中 Get 方法是一致的。而且 GetterFunc 还定义了 Get 方式,并在 Get 方法中调用自己,这样就实现了接口 Getter。所以 GetterFunc 是一个实现了接口的函数类型,简称为接口型函数。 + +这个接口型函数的实现就引起了好几个童鞋的关注。接口型函数只能应用于接口内部只定义了一个方法的情况,例如接口 Getter 内部有且只有一个方法 Get。既然只有一个方法,为什么还要多此一举,封装为一个接口呢?定义参数的时候,直接用 GetterFunc 这个函数类型不就好了,让用户直接传入一个函数作为参数,不更简单吗? + +所以呢,接口型函数的价值什么? + + +## 价值 + +我们想象这么一个使用场景,`GetFromSource` 的作用是从某数据源获取结果,接口类型 Getter 是其中一个参数,代表某数据源: + +```go +func GetFromSource(getter Getter, key string) []byte { + buf, err := getter.Get(key) + if err == nil { + return buf + } + return nil +} +``` + +我们可以有多种方式调用该函数: + +- 方式一:GetterFunc 类型的函数作为参数 + +```go +GetFromSource(GetterFunc(func(key string) ([]byte, error) { + return []byte(key), nil +}), "hello") +``` + +支持匿名函数,也支持普通的函数: + +```go +func test(key string) ([]byte, error) { + return []byte(key), nil +} + +func main() { + GetFromSource(GetterFunc(test), "hello") +} +``` + +将 test 强制类型转换为 GetterFunc,GetterFunc 实现了接口 Getter,是一个合法参数。这种方式适用于逻辑较为简单的场景。 + + +- 方式二:实现了 Getter 接口的结构体作为参数 + +```go +type DB struct{ url string} + +func (db *DB) Query(sql string, args ...string) string { + // ... + return "hello" +} + +func (db *DB) Get(key string) ([]byte, error) { + // ... + v := db.Query("SELECT NAME FROM TABLE WHEN NAME= ?", key) + return []byte(v), nil +} + +func main() { + GetFromSource(new(DB), "hello") +} +``` + +DB 实现了接口 Getter,也是一个合法参数。这种方式适用于逻辑较为复杂的场景,如果对数据库的操作需要很多信息,地址、用户名、密码,还有很多中间状态需要保持,比如超时、重连、加锁等等。这种情况下,更适合封装为一个结构体作为参数。 + +这样,既能够将普通的函数类型(需类型转换)作为参数,也可以将结构体作为参数,使用更为灵活,可读性也更好,这就是接口型函数的价值。 + +## 使用场景 + +这个特性在 groupcache 等大量的 Go 语言开源项目中被广泛使用,标准库中用得也不少,`net/http` 的 Handler 和 HandlerFunc 就是一个典型。 + +我们先看一下 Handler 的定义: + +```go +type Handler interface { + ServeHTTP(ResponseWriter, *Request) +} +type HandlerFunc func(ResponseWriter, *Request) + +func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { + f(w, r) +} +``` + +> 摘自 Go 语言源代码 [net/http/server.go](https://github.com/golang/go/blob/master/src/net/http/server.go) + +我们可以 `http.Handle` 来映射请求路径和处理函数,Handle 的定义如下: + +```go +func Handle(pattern string, handler Handler) +``` + +第二个参数是即接口类型 Handler,我们可以这么用。 + +```go +func home(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("hello, index page")) +} + +func main() { + http.Handle("/home", http.HandlerFunc(home)) + _ = http.ListenAndServe("localhost:8000", nil) +} +``` + +通常,我们还会使用另外一个函数 `http.HandleFunc`,HandleFunc 的定义如下: + +```go +func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) +``` + +第二个参数是一个普通的函数类型,那可以直接将 home 传递给 HandleFunc: + +```go +func main() { + http.HandleFunc("/home", home) + _ = http.ListenAndServe("localhost:8000", nil) +} +``` + +那如果我们看过 HandleFunc 的内部实现的话,就会知道两种写法是完全等价的,内部将第二种写法转换为了第一种写法。 + +```go +func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { + if handler == nil { + panic("http: nil handler") + } + mux.Handle(pattern, HandlerFunc(handler)) +} +``` + +如果你仔细观察,会发现 `http.ListenAndServe` 的第二个参数也是接口类型 `Handler`,我们使用了标准库 `net/http` 内置的路由,因此呢,传入的值是 nil。那如果这个地方我们传入的是一个实现了 `Handler` 接口的结构体呢?就可以完全托管所有的 HTTP 请求,后续怎么路由,怎么处理,请求前后增加什么功能,都可以自定义了。慢慢地,就变成了一个功能丰富的 Web 框架了。如果你感兴趣呢,可以阅读 [7天用Go从零实现Web框架Gee教程](https://geektutu.com/post/gee.html)。 + +## 其他语言类似特性 + +如果有 Java 编程经验的同学可能比较有感触。Java 1.5 中是不支持直接传入函数的,参数要么是接口,要么是对象。举一个最简单的例子,列表自定义排序时,需要实现一个匿名的 Comparator 类,重写 compare 方法。 + +```java +Collections.sort(list, new Comparator(){ + @Override + public int compare(Integer o1, Integer o2) { + return o2 - o1; + } +}); +``` + +Java 1.8 中引入了大量的函数式编程的特性,其中 lambda 表达式和函数式接口就是一个很好的简化 Java 写法的特性。Java 1.8 中,上述的例子可以简化为: + +```java +Collections.sort(list, (Integer o1, Integer o2) -> o2 - o1 ); +``` + +即从需要构造一个匿名对象简化为只需要一个 lambda 函数表达式,可以认为是面向对象与函数式编程的一种结合。同样地,这种写法只支持只定义了一个方法的接口类型。正是这种结合,可以达到实现相同代码,代码量更少的目的。 + +## 附 参考 + +- [7days-golang 有价值的问题讨论汇总贴](https://github.com/geektutu/7days-golang/issues/24) +- [GeeCache第二天 单机并发缓存 - Github 评论区](https://github.com/geektutu/geektutu-blog/issues/64) \ No newline at end of file diff --git a/questions/7days-golang-q1/7days-golang-qa.jpg b/questions/7days-golang-q1/7days-golang-qa.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c783be82584089b064c09395727426aa1e7aa9fb GIT binary patch literal 72108 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 literal 0 HcmV?d00001 From c79596f9acf25e3eae1090a03b19c6fdff2abff7 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Sun, 25 Oct 2020 18:53:35 +0800 Subject: [PATCH 102/122] modified geecache-day2 & add refer to 7days-golang Q & A --- gee-cache/doc/geecache-day2.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gee-cache/doc/geecache-day2.md b/gee-cache/doc/geecache-day2.md index 7f75af8..ead3b38 100644 --- a/gee-cache/doc/geecache-day2.md +++ b/gee-cache/doc/geecache-day2.md @@ -238,6 +238,9 @@ func (f GetterFunc) Get(key string) ([]byte, error) { - 定义接口 Getter 和 回调函数 `Get(key string)([]byte, error)`,参数是 key,返回值是 []byte。 - 定义函数类型 GetterFunc,并实现 Getter 接口的 `Get` 方法。 +- 函数类型实现某一个接口,称之为接口型函数,方便使用者在调用时既能够传入函数作为参数,也能够传入实现了该接口的结构体作为参数。 + +> 了解接口型函数的使用场景,可以参考 [Go 接口型函数的使用场景 - 7days-golang Q & A](https://geektutu.com/post/7days-golang-q1.html) 我们可以写一个测试用例来保证回调函数能够正常工作。 From 2f2be0cec01a082e41444cf231f49309d07ff37c Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Tue, 3 Nov 2020 23:53:26 +0800 Subject: [PATCH 103/122] support book for blog --- gee-cache/doc/geecache-day1.md | 2 ++ gee-cache/doc/geecache-day2.md | 2 ++ gee-cache/doc/geecache-day3.md | 2 ++ gee-cache/doc/geecache-day4.md | 2 ++ gee-cache/doc/geecache-day5.md | 2 ++ gee-cache/doc/geecache-day6.md | 2 ++ gee-cache/doc/geecache-day7.md | 2 ++ gee-cache/doc/geecache.md | 2 ++ gee-orm/doc/geeorm-day1.md | 2 ++ gee-orm/doc/geeorm-day2.md | 2 ++ gee-orm/doc/geeorm-day3.md | 2 ++ gee-orm/doc/geeorm-day4.md | 2 ++ gee-orm/doc/geeorm-day5.md | 2 ++ gee-orm/doc/geeorm-day6.md | 2 ++ gee-orm/doc/geeorm-day7.md | 2 ++ gee-orm/doc/geeorm.md | 2 ++ gee-rpc/doc/geerpc-day1.md | 2 ++ gee-rpc/doc/geerpc-day2.md | 2 ++ gee-rpc/doc/geerpc-day3.md | 2 ++ gee-rpc/doc/geerpc-day4.md | 2 ++ gee-rpc/doc/geerpc-day5.md | 2 ++ gee-rpc/doc/geerpc-day6.md | 2 ++ gee-rpc/doc/geerpc-day7.md | 2 ++ gee-rpc/doc/geerpc.md | 2 ++ gee-web/doc/gee-day1.md | 2 ++ gee-web/doc/gee-day2.md | 2 ++ gee-web/doc/gee-day3.md | 2 ++ gee-web/doc/gee-day4.md | 2 ++ gee-web/doc/gee-day5.md | 2 ++ gee-web/doc/gee-day6.md | 2 ++ gee-web/doc/gee-day7.md | 2 ++ gee-web/doc/gee.md | 2 ++ questions/7days-golang-q1.md | 4 +++- 33 files changed, 67 insertions(+), 1 deletion(-) diff --git a/gee-cache/doc/geecache-day1.md b/gee-cache/doc/geecache-day1.md index 2f096d9..0bdcf75 100644 --- a/gee-cache/doc/geecache-day1.md +++ b/gee-cache/doc/geecache-day1.md @@ -15,6 +15,8 @@ keywords: - 缓存失效 image: post/geecache-day1/lru_logo.jpg github: https://github.com/geektutu/7days-golang +book: 七天用Go从零实现系列 +book_title: Day1 LRU 缓存淘汰策略 --- diff --git a/gee-cache/doc/geecache-day2.md b/gee-cache/doc/geecache-day2.md index ead3b38..0aa3dde 100644 --- a/gee-cache/doc/geecache-day2.md +++ b/gee-cache/doc/geecache-day2.md @@ -15,6 +15,8 @@ keywords: - sync.Mutex image: post/geecache-day2/concurrent_cache_logo.jpg github: https://github.com/geektutu/7days-golang +book: 七天用Go从零实现系列 +book_title: Day2 单机并发缓存 --- ![geecache concurrent cache](geecache-day2/concurrent_cache.jpg) diff --git a/gee-cache/doc/geecache-day3.md b/gee-cache/doc/geecache-day3.md index a129432..a251dcc 100644 --- a/gee-cache/doc/geecache-day3.md +++ b/gee-cache/doc/geecache-day3.md @@ -14,6 +14,8 @@ keywords: - HTTP Server image: post/geecache-day3/http_logo.jpg github: https://github.com/geektutu/7days-golang +book: 七天用Go从零实现系列 +book_title: Day3 HTTP 服务端 --- ![geecache http server](geecache-day3/http.jpg) diff --git a/gee-cache/doc/geecache-day4.md b/gee-cache/doc/geecache-day4.md index 30ca624..02db009 100644 --- a/gee-cache/doc/geecache-day4.md +++ b/gee-cache/doc/geecache-day4.md @@ -14,6 +14,8 @@ keywords: - consistent hash image: post/geecache-day4/hash_logo.jpg github: https://github.com/geektutu/7days-golang +book: 七天用Go从零实现系列 +book_title: Day4 一致性哈希 --- ![一致性哈希 consistent hashing](geecache-day4/hash.jpg) diff --git a/gee-cache/doc/geecache-day5.md b/gee-cache/doc/geecache-day5.md index 4e28083..9f995ba 100644 --- a/gee-cache/doc/geecache-day5.md +++ b/gee-cache/doc/geecache-day5.md @@ -14,6 +14,8 @@ keywords: - 分布式节点 image: post/geecache-day5/dist_nodes_logo.jpg github: https://github.com/geektutu/7days-golang +book: 七天用Go从零实现系列 +book_title: Day5 分布式节点 --- ![分布式缓存节点](geecache-day5/dist_nodes.jpg) diff --git a/gee-cache/doc/geecache-day6.md b/gee-cache/doc/geecache-day6.md index a2236f6..73ad7dd 100644 --- a/gee-cache/doc/geecache-day6.md +++ b/gee-cache/doc/geecache-day6.md @@ -14,6 +14,8 @@ keywords: - 分布式节点 image: post/geecache-day6/singleflight_logo.jpg github: https://github.com/geektutu/7days-golang +book: 七天用Go从零实现系列 +book_title: Day6 防止缓存击穿 --- ![geecache single flight](geecache-day6/singleflight.jpg) diff --git a/gee-cache/doc/geecache-day7.md b/gee-cache/doc/geecache-day7.md index c1bb34c..2f26210 100644 --- a/gee-cache/doc/geecache-day7.md +++ b/gee-cache/doc/geecache-day7.md @@ -14,6 +14,8 @@ keywords: - 分布式节点 image: post/geecache-day7/protobuf_logo.jpg github: https://github.com/geektutu/7days-golang +book: 七天用Go从零实现系列 +book_title: Day7 使用 Protobuf 通信 --- ![geecache protobuf](geecache-day7/protobuf.jpg) diff --git a/gee-cache/doc/geecache.md b/gee-cache/doc/geecache.md index 3eb89ad..dccc53d 100644 --- a/gee-cache/doc/geecache.md +++ b/gee-cache/doc/geecache.md @@ -13,6 +13,8 @@ keywords: - 动手写分布式缓存 image: post/geecache/geecache_sm.jpg github: https://github.com/geektutu/7days-golang +book: 七天用Go从零实现系列 +book_title: Day0 序言 --- ![分布式缓存geecache](geecache/geecache.jpg) diff --git a/gee-orm/doc/geeorm-day1.md b/gee-orm/doc/geeorm-day1.md index 3ecb72c..9fe3adf 100644 --- a/gee-orm/doc/geeorm-day1.md +++ b/gee-orm/doc/geeorm-day1.md @@ -14,6 +14,8 @@ keywords: - sqlite image: post/geeorm/geeorm_sm.jpg github: https://github.com/geektutu/7days-golang +book: 七天用Go从零实现系列 +book_title: Day1 database/sql 基础 --- 本文是[7天用Go从零实现ORM框架GeeORM](https://geektutu.com/post/geeorm.html)的第一篇。介绍了 diff --git a/gee-orm/doc/geeorm-day2.md b/gee-orm/doc/geeorm-day2.md index 9287a51..0f6c9d4 100644 --- a/gee-orm/doc/geeorm-day2.md +++ b/gee-orm/doc/geeorm-day2.md @@ -16,6 +16,8 @@ keywords: - table mapping image: post/geeorm/geeorm_sm.jpg github: https://github.com/geektutu/7days-golang +book: 七天用Go从零实现系列 +book_title: Day2 对象表结构映射 --- 本文是[7天用Go从零实现ORM框架GeeORM](https://geektutu.com/post/geeorm.html)的第二篇。 diff --git a/gee-orm/doc/geeorm-day3.md b/gee-orm/doc/geeorm-day3.md index 85f5f6f..d541ec3 100644 --- a/gee-orm/doc/geeorm-day3.md +++ b/gee-orm/doc/geeorm-day3.md @@ -16,6 +16,8 @@ keywords: - select from image: post/geeorm/geeorm_sm.jpg github: https://github.com/geektutu/7days-golang +book: 七天用Go从零实现系列 +book_title: Day3 记录新增和查询 --- 本文是[7天用Go从零实现ORM框架GeeORM](https://geektutu.com/post/geeorm.html)的第三篇。 diff --git a/gee-orm/doc/geeorm-day4.md b/gee-orm/doc/geeorm-day4.md index 6e0d307..8cfcc75 100644 --- a/gee-orm/doc/geeorm-day4.md +++ b/gee-orm/doc/geeorm-day4.md @@ -16,6 +16,8 @@ keywords: - delete from image: post/geeorm/geeorm_sm.jpg github: https://github.com/geektutu/7days-golang +book: 七天用Go从零实现系列 +book_title: Day4 链式操作与更新删除 --- 本文是[7天用Go从零实现ORM框架GeeORM](https://geektutu.com/post/geeorm.html)的第四篇。 diff --git a/gee-orm/doc/geeorm-day5.md b/gee-orm/doc/geeorm-day5.md index e0bf9e2..3d6ffda 100644 --- a/gee-orm/doc/geeorm-day5.md +++ b/gee-orm/doc/geeorm-day5.md @@ -16,6 +16,8 @@ keywords: - BeforeUpdate image: post/geeorm/geeorm_sm.jpg github: https://github.com/geektutu/7days-golang +book: 七天用Go从零实现系列 +book_title: Day5 实现钩子 --- 本文是[7天用Go从零实现ORM框架GeeORM](https://geektutu.com/post/geeorm.html)的第五篇。 diff --git a/gee-orm/doc/geeorm-day6.md b/gee-orm/doc/geeorm-day6.md index 37955d3..abe43a9 100644 --- a/gee-orm/doc/geeorm-day6.md +++ b/gee-orm/doc/geeorm-day6.md @@ -15,6 +15,8 @@ keywords: - transaction image: post/geeorm/geeorm_sm.jpg github: https://github.com/geektutu/7days-golang +book: 七天用Go从零实现系列 +book_title: Day6 支持事务 --- 本文是[7天用Go从零实现ORM框架GeeORM](https://geektutu.com/post/geeorm.html)的第六篇。 diff --git a/gee-orm/doc/geeorm-day7.md b/gee-orm/doc/geeorm-day7.md index 7e6dc1c..efd96db 100644 --- a/gee-orm/doc/geeorm-day7.md +++ b/gee-orm/doc/geeorm-day7.md @@ -15,6 +15,8 @@ keywords: - migrate image: post/geeorm/geeorm_sm.jpg github: https://github.com/geektutu/7days-golang +book: 七天用Go从零实现系列 +book_title: Day7 数据库迁移 --- 本文是[7天用Go从零实现ORM框架GeeORM](https://geektutu.com/post/geeorm.html)的第七篇。 diff --git a/gee-orm/doc/geeorm.md b/gee-orm/doc/geeorm.md index f9c14da..c188002 100644 --- a/gee-orm/doc/geeorm.md +++ b/gee-orm/doc/geeorm.md @@ -15,6 +15,8 @@ keywords: - sqlite3 image: post/geeorm/geeorm_sm.jpg github: https://github.com/geektutu/7days-golang +book: 七天用Go从零实现系列 +book_title: Day0 序言 --- ![golang ORM framework](geeorm/geeorm.jpg) diff --git a/gee-rpc/doc/geerpc-day1.md b/gee-rpc/doc/geerpc-day1.md index 51db1fb..dea8b43 100644 --- a/gee-rpc/doc/geerpc-day1.md +++ b/gee-rpc/doc/geerpc-day1.md @@ -15,6 +15,8 @@ keywords: - 反序列化 image: post/geerpc/geerpc.jpg github: https://github.com/geektutu/7days-golang +book: 七天用Go从零实现系列 +book_title: Day1 服务端与消息编码 --- ![golang RPC framework](geerpc/geerpc.jpg) diff --git a/gee-rpc/doc/geerpc-day2.md b/gee-rpc/doc/geerpc-day2.md index ff2ce20..da70f3a 100644 --- a/gee-rpc/doc/geerpc-day2.md +++ b/gee-rpc/doc/geerpc-day2.md @@ -15,6 +15,8 @@ keywords: - 并发 image: post/geerpc/geerpc.jpg github: https://github.com/geektutu/7days-golang +book: 七天用Go从零实现系列 +book_title: Day2 高性能客户端 --- ![golang RPC framework](geerpc/geerpc.jpg) diff --git a/gee-rpc/doc/geerpc-day3.md b/gee-rpc/doc/geerpc-day3.md index d1a6982..912ea14 100644 --- a/gee-rpc/doc/geerpc-day3.md +++ b/gee-rpc/doc/geerpc-day3.md @@ -14,6 +14,8 @@ keywords: - 服务 image: post/geerpc/geerpc.jpg github: https://github.com/geektutu/7days-golang +book: 七天用Go从零实现系列 +book_title: Day3 服务注册 --- ![golang RPC framework](geerpc/geerpc.jpg) diff --git a/gee-rpc/doc/geerpc-day4.md b/gee-rpc/doc/geerpc-day4.md index b249d2e..4d89c05 100644 --- a/gee-rpc/doc/geerpc-day4.md +++ b/gee-rpc/doc/geerpc-day4.md @@ -13,6 +13,8 @@ keywords: - 连接超时 image: post/geerpc/geerpc.jpg github: https://github.com/geektutu/7days-golang +book: 七天用Go从零实现系列 +book_title: Day4 超时处理 --- ![golang RPC framework](geerpc/geerpc.jpg) diff --git a/gee-rpc/doc/geerpc-day5.md b/gee-rpc/doc/geerpc-day5.md index 5b1797d..c6813da 100644 --- a/gee-rpc/doc/geerpc-day5.md +++ b/gee-rpc/doc/geerpc-day5.md @@ -14,6 +14,8 @@ keywords: - debug image: post/geerpc/geerpc.jpg github: https://github.com/geektutu/7days-golang +book: 七天用Go从零实现系列 +book_title: Day5 支持HTTP协议 --- ![golang RPC framework](geerpc/geerpc.jpg) diff --git a/gee-rpc/doc/geerpc-day6.md b/gee-rpc/doc/geerpc-day6.md index 87f5877..a0b54cc 100644 --- a/gee-rpc/doc/geerpc-day6.md +++ b/gee-rpc/doc/geerpc-day6.md @@ -14,6 +14,8 @@ keywords: - 轮询调度 image: post/geerpc/geerpc.jpg github: https://github.com/geektutu/7days-golang +book: 七天用Go从零实现系列 +book_title: Day6 负载均衡 --- ![golang RPC framework](geerpc/geerpc.jpg) diff --git a/gee-rpc/doc/geerpc-day7.md b/gee-rpc/doc/geerpc-day7.md index ad4f502..c7bb6c2 100644 --- a/gee-rpc/doc/geerpc-day7.md +++ b/gee-rpc/doc/geerpc-day7.md @@ -14,6 +14,8 @@ keywords: - 服务发现 image: post/geerpc/geerpc.jpg github: https://github.com/geektutu/7days-golang +book: 七天用Go从零实现系列 +book_title: Day7 服务发现与注册中心 --- ![golang RPC framework](geerpc/geerpc.jpg) diff --git a/gee-rpc/doc/geerpc.md b/gee-rpc/doc/geerpc.md index 7a6a324..5f5f1d8 100644 --- a/gee-rpc/doc/geerpc.md +++ b/gee-rpc/doc/geerpc.md @@ -15,6 +15,8 @@ keywords: - 负载均衡 image: post/geerpc/geerpc.jpg github: https://github.com/geektutu/7days-golang +book: 七天用Go从零实现系列 +book_title: Day0 序言 --- ![golang RPC framework](geerpc/geerpc.jpg) diff --git a/gee-web/doc/gee-day1.md b/gee-web/doc/gee-day1.md index de30bc9..9a09b37 100644 --- a/gee-web/doc/gee-day1.md +++ b/gee-web/doc/gee-day1.md @@ -14,6 +14,8 @@ keywords: - net/http image: post/gee/gee.jpg github: https://github.com/geektutu/7days-golang +book: 七天用Go从零实现系列 +book_title: Day1 HTTP 基础 --- 本文是 [7天用Go从零实现Web框架Gee教程系列](https://geektutu.com/post/gee.html)的第一篇。 diff --git a/gee-web/doc/gee-day2.md b/gee-web/doc/gee-day2.md index f63fde1..9bd011f 100644 --- a/gee-web/doc/gee-day2.md +++ b/gee-web/doc/gee-day2.md @@ -14,6 +14,8 @@ keywords: - Context image: post/gee/gee.jpg github: https://github.com/geektutu/7days-golang +book: 七天用Go从零实现系列 +book_title: Day2 上下文 --- 本文是 [7天用Go从零实现Web框架Gee教程系列](https://geektutu.com/post/gee.html)的第二篇。 diff --git a/gee-web/doc/gee-day3.md b/gee-web/doc/gee-day3.md index 0d66e54..3586a2f 100644 --- a/gee-web/doc/gee-day3.md +++ b/gee-web/doc/gee-day3.md @@ -14,6 +14,8 @@ keywords: - Route image: post/gee-day3/trie_router.jpg github: https://github.com/geektutu/7days-golang +book: 七天用Go从零实现系列 +book_title: Day3 前缀树路由 --- 本文是 [7天用Go从零实现Web框架Gee教程系列](https://geektutu.com/post/gee.html)的第三篇。 diff --git a/gee-web/doc/gee-day4.md b/gee-web/doc/gee-day4.md index 3a5eb21..d48202f 100644 --- a/gee-web/doc/gee-day4.md +++ b/gee-web/doc/gee-day4.md @@ -14,6 +14,8 @@ keywords: - Group Control image: post/gee-day4/group.jpg github: https://github.com/geektutu/7days-golang +book: 七天用Go从零实现系列 +book_title: Day4 分组控制 --- 本文是 [7天用Go从零实现Web框架Gee教程系列](https://geektutu.com/post/gee.html)的第四篇。 diff --git a/gee-web/doc/gee-day5.md b/gee-web/doc/gee-day5.md index e4fb039..7a540a1 100644 --- a/gee-web/doc/gee-day5.md +++ b/gee-web/doc/gee-day5.md @@ -14,6 +14,8 @@ keywords: - Middlewares image: post/gee-day5/middleware.jpg github: https://github.com/geektutu/7days-golang +book: 七天用Go从零实现系列 +book_title: Day5 中间件 --- 本文是 [7天用Go从零实现Web框架Gee教程系列](https://geektutu.com/post/gee.html)的第五篇。 diff --git a/gee-web/doc/gee-day6.md b/gee-web/doc/gee-day6.md index a95d578..1a6283a 100644 --- a/gee-web/doc/gee-day6.md +++ b/gee-web/doc/gee-day6.md @@ -14,6 +14,8 @@ keywords: - Template image: post/gee-day6/html.png github: https://github.com/geektutu/7days-golang +book: 七天用Go从零实现系列 +book_title: Day6 模板 Template --- 本文是 [7天用Go从零实现Web框架Gee教程系列](https://geektutu.com/post/gee.html)的第六篇。 diff --git a/gee-web/doc/gee-day7.md b/gee-web/doc/gee-day7.md index 08310ca..f64de84 100644 --- a/gee-web/doc/gee-day7.md +++ b/gee-web/doc/gee-day7.md @@ -15,6 +15,8 @@ keywords: - Recover image: post/gee-day7/go-panic.png github: https://github.com/geektutu/7days-golang +book: 七天用Go从零实现系列 +book_title: Day7 错误恢复 --- 本文是[7天用Go从零实现Web框架Gee教程系列](https://geektutu.com/post/gee.html)的第七篇。 diff --git a/gee-web/doc/gee.md b/gee-web/doc/gee.md index 0596aaf..9dbfc54 100644 --- a/gee-web/doc/gee.md +++ b/gee-web/doc/gee.md @@ -14,6 +14,8 @@ keywords: - from scratch image: post/gee/gee.jpg github: https://github.com/geektutu/7days-golang +book: 七天用Go从零实现系列 +book_title: Day0 序言 --- ![gee](gee/gee.jpg) diff --git a/questions/7days-golang-q1.md b/questions/7days-golang-q1.md index f1e241b..ccabb4e 100644 --- a/questions/7days-golang-q1.md +++ b/questions/7days-golang-q1.md @@ -1,6 +1,6 @@ --- title: Go 接口型函数的使用场景 -date: 2099-10-25 12:30:00 +date: 2020-10-25 12:30:00 description: Go 语言/golang 中函数式接口或接口型函数的实现与价值,什么是接口型函数,为什么不直接将函数作为参数,而是封装为一个接口。Go 语言标准库 net/http 中是如何使用接口型函数的。 tags: - Go @@ -13,6 +13,8 @@ keywords: - net/http image: post/7days-golang-q1/7days-golang-qa.jpg github: https://github.com/geektutu/7days-golang +book: 七天用Go从零实现系列 +book_title: 接口型函数 --- ![7days-golang 有价值的问题](7days-golang-q1/7days-golang-qa.jpg) From d6325500c452f60fa4c4e5714085a5d70e2fcfdd Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Thu, 5 Nov 2020 01:01:54 +0800 Subject: [PATCH 104/122] geerpc/doc: fix github url --- gee-rpc/doc/geerpc.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/gee-rpc/doc/geerpc.md b/gee-rpc/doc/geerpc.md index 5f5f1d8..a010128 100644 --- a/gee-rpc/doc/geerpc.md +++ b/gee-rpc/doc/geerpc.md @@ -54,13 +54,13 @@ Go 语言广泛地应用于云计算和微服务,成熟的 RPC 框架和微服 ## 4 目录 -- 第一天 - [服务端与消息编码](https://geektutu.com/post/geerpc-day1.html) | [Code](gee-rpc/day1-codec) -- 第二天 - [支持并发与异步的客户端](https://geektutu.com/post/geerpc-day2.html) | [Code](gee-rpc/day2-client) -- 第三天 - [服务注册(service register)](https://geektutu.com/post/geerpc-day3.html) | [Code](gee-rpc/day3-service ) -- 第四天 - [超时处理(timeout)](https://geektutu.com/post/geerpc-day4.html) | [Code](gee-rpc/day4-timeout ) -- 第五天 - [支持HTTP协议](https://geektutu.com/post/geerpc-day5.html) | [Code](gee-rpc/day5-http-debug) -- 第六天 - [负载均衡(load balance)](https://geektutu.com/post/geerpc-day6.html) | [Code](gee-rpc/day6-load-balance) -- 第七天 - [服务发现与注册中心(registry)](https://geektutu.com/post/geerpc-day7.html) | [Code](gee-rpc/day7-registry) +- 第一天 - [服务端与消息编码](https://geektutu.com/post/geerpc-day1.html) | [Code](ghttps://github.com/geektutu/7days-golang/tree/master/ee-rpc/day1-codec) +- 第二天 - [支持并发与异步的客户端](https://geektutu.com/post/geerpc-day2.html) | [Code](ghttps://github.com/geektutu/7days-golang/tree/master/ee-rpc/day2-client) +- 第三天 - [服务注册(service register)](https://geektutu.com/post/geerpc-day3.html) | [Code](https://github.com/geektutu/7days-golang/tree/master/gee-rpc/day3-service ) +- 第四天 - [超时处理(timeout)](https://geektutu.com/post/geerpc-day4.html) | [Code](ghttps://github.com/geektutu/7days-golang/tree/master/ee-rpc/day4-timeout ) +- 第五天 - [支持HTTP协议](https://geektutu.com/post/geerpc-day5.html) | [Code](ghttps://github.com/geektutu/7days-golang/tree/master/ee-rpc/day5-http-debug) +- 第六天 - [负载均衡(load balance)](https://geektutu.com/post/geerpc-day6.html) | [Code](https://github.com/geektutu/7days-golang/tree/master/gee-rpc/day6-load-balance) +- 第七天 - [服务发现与注册中心(registry)](https://geektutu.com/post/geerpc-day7.html) | [Code](https://github.com/geektutu/7days-golang/tree/master/gee-rpc/day7-registry) ## 附 推荐阅读 From 8e70671907ed9affd9bc7e93ef130fef6dd9acac Mon Sep 17 00:00:00 2001 From: Dai Jie Date: Sun, 22 Nov 2020 22:31:44 +0800 Subject: [PATCH 105/122] method.Type.NumIn() => method.Type.NumOut() --- gee-rpc/doc/geerpc-day3.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gee-rpc/doc/geerpc-day3.md b/gee-rpc/doc/geerpc-day3.md index 912ea14..c1b720c 100644 --- a/gee-rpc/doc/geerpc-day3.md +++ b/gee-rpc/doc/geerpc-day3.md @@ -80,8 +80,8 @@ func main() { for i := 0; i < typ.NumMethod(); i++ { method := typ.Method(i) argv := make([]string, 0, method.Type.NumIn()) - returns := make([]string, 0, method.Type.NumIn()) - // j 从 1 开始,第 0 个入参是 wg 自己。 + returns := make([]string, 0, method.Type.NumOut()) + // j 从 1 开始,第 0 个入参是 wg 自己。 for j := 1; j < method.Type.NumIn(); j++ { argv = append(argv, method.Type.In(j).Name()) } @@ -494,4 +494,4 @@ start rpc server on [::]:57509 ## 附 推荐阅读 - [Go 语言简明教程](https://geektutu.com/post/quick-golang.html) -- [Go 语言笔试面试题](https://geektutu.com/post/qa-golang.html) \ No newline at end of file +- [Go 语言笔试面试题](https://geektutu.com/post/qa-golang.html) From 658cc8a8943e7663f38ec828c1c4e331e223fae7 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Sun, 22 Nov 2020 23:18:37 +0800 Subject: [PATCH 106/122] remove hitcount, add high performance go --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index aa0c66a..168b858 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ [![CodeSize](https://img.shields.io/github/languages/code-size/geektutu/7days-golang)](https://github.com/geektutu/7days-golang) [![LICENSE](https://img.shields.io/badge/license-MIT-green)](https://mit-license.org/) -[![HitCount](https://hits.dwyl.com/geektutu/7days-golang.svg)](http://github.com/geektutu/7days-golang)
README 中文版本 @@ -14,7 +13,9 @@ 推荐先阅读 **[Go 语言简明教程](https://geektutu.com/post/quick-golang.html)**,一篇文章了解Go的基本语法、并发编程,依赖管理等内容。 -另外推荐 **[Go 语言笔试面试题](https://geektutu.com/post/qa-golang.html)**,加深对 Go 语言的理解。 +推荐 **[Go 语言笔试面试题](https://geektutu.com/post/qa-golang.html)**,加深对 Go 语言的理解。 + +推荐 **[Go 语言高性能编程](https://geektutu.com/post/high-performace-go.html)**,写出高性能的 Go 代码。 期待关注我的「[知乎专栏](https://zhuanlan.zhihu.com/geekgo)」和「[微博](http://weibo.com/geektutu)」,查看最近的文章和动态。 From 92a52ed38910a664d02c9cf2ea26dde10a82a26a Mon Sep 17 00:00:00 2001 From: Dai Jie Date: Mon, 23 Nov 2020 09:22:20 +0800 Subject: [PATCH 107/122] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 168b858..7c9781e 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ 推荐 **[Go 语言笔试面试题](https://geektutu.com/post/qa-golang.html)**,加深对 Go 语言的理解。 -推荐 **[Go 语言高性能编程](https://geektutu.com/post/high-performace-go.html)**,写出高性能的 Go 代码。 +推荐 **[Go 语言高性能编程](https://geektutu.com/post/high-performance-go.html)**,写出高性能的 Go 代码。 期待关注我的「[知乎专栏](https://zhuanlan.zhihu.com/geekgo)」和「[微博](http://weibo.com/geektutu)」,查看最近的文章和动态。 From 898e35e34c89e2b2774b463551fdd2a954e747ca Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Tue, 1 Dec 2020 23:30:50 +0800 Subject: [PATCH 108/122] fix goroutine loop variable reference issue --- gee-rpc/day6-load-balance/xclient/xclient.go | 4 ++-- gee-rpc/day7-registry/xclient/xclient.go | 4 ++-- gee-rpc/doc/geerpc-day6.md | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/gee-rpc/day6-load-balance/xclient/xclient.go b/gee-rpc/day6-load-balance/xclient/xclient.go index 838b343..3194d27 100644 --- a/gee-rpc/day6-load-balance/xclient/xclient.go +++ b/gee-rpc/day6-load-balance/xclient/xclient.go @@ -85,7 +85,7 @@ func (xc *XClient) Broadcast(ctx context.Context, serviceMethod string, args, re ctx, cancel := context.WithCancel(ctx) for _, rpcAddr := range servers { wg.Add(1) - go func() { + go func(rpcAddr string) { defer wg.Done() var clonedReply interface{} if reply != nil { @@ -102,7 +102,7 @@ func (xc *XClient) Broadcast(ctx context.Context, serviceMethod string, args, re replyDone = true } mu.Unlock() - }() + }(rpcAddr) } wg.Wait() return e diff --git a/gee-rpc/day7-registry/xclient/xclient.go b/gee-rpc/day7-registry/xclient/xclient.go index 838b343..3194d27 100644 --- a/gee-rpc/day7-registry/xclient/xclient.go +++ b/gee-rpc/day7-registry/xclient/xclient.go @@ -85,7 +85,7 @@ func (xc *XClient) Broadcast(ctx context.Context, serviceMethod string, args, re ctx, cancel := context.WithCancel(ctx) for _, rpcAddr := range servers { wg.Add(1) - go func() { + go func(rpcAddr string) { defer wg.Done() var clonedReply interface{} if reply != nil { @@ -102,7 +102,7 @@ func (xc *XClient) Broadcast(ctx context.Context, serviceMethod string, args, re replyDone = true } mu.Unlock() - }() + }(rpcAddr) } wg.Wait() return e diff --git a/gee-rpc/doc/geerpc-day6.md b/gee-rpc/doc/geerpc-day6.md index a0b54cc..7b0ab97 100644 --- a/gee-rpc/doc/geerpc-day6.md +++ b/gee-rpc/doc/geerpc-day6.md @@ -259,7 +259,7 @@ func (xc *XClient) Broadcast(ctx context.Context, serviceMethod string, args, re ctx, cancel := context.WithCancel(ctx) for _, rpcAddr := range servers { wg.Add(1) - go func() { + go func(rpcAddr string) { defer wg.Done() var clonedReply interface{} if reply != nil { @@ -276,7 +276,7 @@ func (xc *XClient) Broadcast(ctx context.Context, serviceMethod string, args, re replyDone = true } mu.Unlock() - }() + }(rpcAddr) } wg.Wait() return e From 8ff3c45ccfe8ae4ddaa97b1df23fb4934af0d0a8 Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Sun, 6 Dec 2020 17:49:40 +0800 Subject: [PATCH 109/122] add project url --- README.md | 2 +- gee-orm/doc/geeorm-day2.md | 1 + gee-orm/doc/geeorm.md | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7c9781e..296bff7 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ 推荐 **[Go 语言笔试面试题](https://geektutu.com/post/qa-golang.html)**,加深对 Go 语言的理解。 -推荐 **[Go 语言高性能编程](https://geektutu.com/post/high-performance-go.html)**,写出高性能的 Go 代码。 +推荐 **[Go 语言高性能编程](https://geektutu.com/post/high-performance-go.html)**([项目地址](https://github.com/geektutu/high-performance-go)),写出高性能的 Go 代码。 期待关注我的「[知乎专栏](https://zhuanlan.zhihu.com/geekgo)」和「[微博](http://weibo.com/geektutu)」,查看最近的文章和动态。 diff --git a/gee-orm/doc/geeorm-day2.md b/gee-orm/doc/geeorm-day2.md index 0f6c9d4..f4a1794 100644 --- a/gee-orm/doc/geeorm-day2.md +++ b/gee-orm/doc/geeorm-day2.md @@ -386,4 +386,5 @@ func (engine *Engine) NewSession() *session.Session { - [Go 语言简明教程](https://geektutu.com/post/quick-golang.html) - [Go Test 单元测试简明教程](https://geektutu.com/post/quick-go-test.html) +- [Go Reflect 提高反射性能](https://geektutu.com/post/hpg-reflect.html) - [SQLite 常用命令速查表](https://geektutu.com/post/cheat-sheet-sqlite.html) \ No newline at end of file diff --git a/gee-orm/doc/geeorm.md b/gee-orm/doc/geeorm.md index c188002..6db5c98 100644 --- a/gee-orm/doc/geeorm.md +++ b/gee-orm/doc/geeorm.md @@ -137,4 +137,5 @@ gorm 正在彻底重构 v1 版本,短期内看不到发布 v2 的可能。相 - [Go 语言简明教程](https://geektutu.com/post/quick-golang.html) - [Go Test 单元测试简明教程](https://geektutu.com/post/quick-go-test.html) +- [Go Reflect 提高反射性能](https://geektutu.com/post/hpg-reflect.html) - [SQLite 常用命令速查表](https://geektutu.com/post/cheat-sheet-sqlite.html) \ No newline at end of file From 3d0e400941171abcc83a8e8ad568392d6927202a Mon Sep 17 00:00:00 2001 From: gzdaijie Date: Fri, 1 Jan 2021 21:49:05 +0800 Subject: [PATCH 110/122] rename geektutu-blog to blog --- questions/7days-golang-q1.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/questions/7days-golang-q1.md b/questions/7days-golang-q1.md index ccabb4e..9020b8c 100644 --- a/questions/7days-golang-q1.md +++ b/questions/7days-golang-q1.md @@ -200,4 +200,4 @@ Collections.sort(list, (Integer o1, Integer o2) -> o2 - o1 ); ## 附 参考 - [7days-golang 有价值的问题讨论汇总贴](https://github.com/geektutu/7days-golang/issues/24) -- [GeeCache第二天 单机并发缓存 - Github 评论区](https://github.com/geektutu/geektutu-blog/issues/64) \ No newline at end of file +- [GeeCache第二天 单机并发缓存 - Github 评论区](https://github.com/geektutu/blog/issues/64) \ No newline at end of file From 45e999298e91680833d8349c2e78b049f5110c79 Mon Sep 17 00:00:00 2001 From: Dai Jie Date: Wed, 6 Jan 2021 10:13:14 +0800 Subject: [PATCH 111/122] Update geecache-day2.md --- gee-cache/doc/geecache-day2.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gee-cache/doc/geecache-day2.md b/gee-cache/doc/geecache-day2.md index 0aa3dde..583408b 100644 --- a/gee-cache/doc/geecache-day2.md +++ b/gee-cache/doc/geecache-day2.md @@ -188,7 +188,7 @@ func (c *cache) get(key string) (value ByteView, ok bool) { ``` - `cache.go` 的实现非常简单,实例化 lru,封装 get 和 add 方法,并添加互斥锁 mu。 -- 在 `add` 方法中,判断了 `c.lru` 是否为 nil,如果不等于 nil 再创建实例。这种方法称之为延迟初始化(Lazy Initialization),一个对象的延迟初始化意味着该对象的创建将会延迟至第一次使用该对象时。主要用于提高性能,并减少程序内存要求。 +- 在 `add` 方法中,判断了 `c.lru` 是否为 nil,如果等于 nil 再创建实例。这种方法称之为延迟初始化(Lazy Initialization),一个对象的延迟初始化意味着该对象的创建将会延迟至第一次使用该对象时。主要用于提高性能,并减少程序内存要求。 ## 3 主体结构 Group @@ -432,4 +432,4 @@ ok geecache 0.008s - [Go 语言简明教程 - 并发编程](https://geektutu.com/post/quick-golang.html#7-并发编程-goroutine) - [Go Test 单元测试简明教程](https://geektutu.com/post/quick-go-test.html) -- [sync 官方文档 - golang.org](https://golang.org/pkg/sync/) \ No newline at end of file +- [sync 官方文档 - golang.org](https://golang.org/pkg/sync/) From 371cbe02192565c9d10280d773105e510c740ef2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Images=E3=80=82?= Date: Tue, 25 May 2021 09:58:19 +0800 Subject: [PATCH 112/122] typo: geerpc-day1.md --- gee-rpc/doc/geerpc-day1.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gee-rpc/doc/geerpc-day1.md b/gee-rpc/doc/geerpc-day1.md index dea8b43..4e0c22a 100644 --- a/gee-rpc/doc/geerpc-day1.md +++ b/gee-rpc/doc/geerpc-day1.md @@ -56,7 +56,7 @@ type Header struct { - Error 是错误信息,客户端置为空,服务端如果如果发生错误,将错误信息置于 Error 中。 -我们将和消息编解码相关的代码都防到 codec 子目录中,在此之前,还需要在根目录下使用 `go mod init geerpc` 初始化项目,方便后续子 package 之间的引用。 +我们将和消息编解码相关的代码都放到 codec 子目录中,在此之前,还需要在根目录下使用 `go mod init geerpc` 初始化项目,方便后续子 package 之间的引用。 进一步,抽象出对消息体进行编解码的接口 Codec,抽象出接口是为了实现不同的 Codec 实例: @@ -439,4 +439,4 @@ reply: geerpc resp 4 ## 附 推荐阅读 - [Go 语言简明教程](https://geektutu.com/post/quick-golang.html) -- [Go 语言笔试面试题](https://geektutu.com/post/qa-golang.html) \ No newline at end of file +- [Go 语言笔试面试题](https://geektutu.com/post/qa-golang.html) From 9ef5cbb96b5eee3bd126cf54147396d0cc4b70c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Images=E3=80=82?= Date: Tue, 25 May 2021 11:28:03 +0800 Subject: [PATCH 113/122] Update geerpc-day1.md --- gee-rpc/doc/geerpc-day1.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gee-rpc/doc/geerpc-day1.md b/gee-rpc/doc/geerpc-day1.md index dea8b43..cff6f52 100644 --- a/gee-rpc/doc/geerpc-day1.md +++ b/gee-rpc/doc/geerpc-day1.md @@ -183,7 +183,7 @@ var DefaultOption = &Option{ } ``` -一般来说,涉及协议协商的这部分信息,需要设计固定的字节来传输的。但是为了实现上更简单,GeeRPC 客户端固定采用 JSON 编码 Option,后续的 header 和 body 的编码方式由 Option 中的 CodeType 指定,服务端首先使用 JSON 解码 Option,然后通过 Option 得 CodeType 解码剩余的内容。即报文将以这样的形式发送: +一般来说,涉及协议协商的这部分信息,需要设计固定的字节来传输的。但是为了实现上更简单,GeeRPC 客户端固定采用 JSON 编码 Option,后续的 header 和 body 的编码方式由 Option 中的 CodeType 指定,服务端首先使用 JSON 解码 Option,然后通过 Option 的 CodeType 解码剩余的内容。即报文将以这样的形式发送: ```bash | Option{MagicNumber: xxx, CodecType: xxx} | Header{ServiceMethod ...} | Body interface{} | @@ -439,4 +439,4 @@ reply: geerpc resp 4 ## 附 推荐阅读 - [Go 语言简明教程](https://geektutu.com/post/quick-golang.html) -- [Go 语言笔试面试题](https://geektutu.com/post/qa-golang.html) \ No newline at end of file +- [Go 语言笔试面试题](https://geektutu.com/post/qa-golang.html) From 40355892d009fffe7b2af3ca86e3f3bb563c3011 Mon Sep 17 00:00:00 2001 From: alcuin Date: Wed, 7 Jun 2023 19:54:23 +0800 Subject: [PATCH 114/122] =?UTF-8?q?Day4=20=E4=B8=80=E8=87=B4=E6=80=A7?= =?UTF-8?q?=E5=93=88=E5=B8=8C=E5=AD=A6=E4=B9=A0=E7=AC=94=E8=AE=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../geecache/consistenthash/consistenthash.go | 24 ++++++++++++------- .../consistenthash/consistenthash_test.go | 6 +++-- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/gee-cache/day4-consistent-hash/geecache/consistenthash/consistenthash.go b/gee-cache/day4-consistent-hash/geecache/consistenthash/consistenthash.go index c8c9082..3227290 100644 --- a/gee-cache/day4-consistent-hash/geecache/consistenthash/consistenthash.go +++ b/gee-cache/day4-consistent-hash/geecache/consistenthash/consistenthash.go @@ -11,13 +11,13 @@ type Hash func(data []byte) uint32 // Map constains all hashed keys type Map struct { - hash Hash - replicas int - keys []int // Sorted - hashMap map[int]string + hash Hash // 定义了函数类型 Hash, + replicas int // 虚拟节点倍数 + keys []int // Sorted 哈希环 + hashMap map[int]string // 虚拟节点与真实节点的映射表 hashMap,键是虚拟节点的哈希值,值是真实节点的名称。 } -// New creates a Map instance +// New creates a Map instance 构造函数 New() 允许自定义虚拟节点倍数和 Hash 函数。 func New(replicas int, fn Hash) *Map { m := &Map{ replicas: replicas, @@ -25,7 +25,7 @@ func New(replicas int, fn Hash) *Map { hashMap: make(map[int]string), } if m.hash == nil { - m.hash = crc32.ChecksumIEEE + m.hash = crc32.ChecksumIEEE // 采取依赖注入的方式,允许用于替换成自定义的 Hash 函数,也方便测试时替换,默认为 crc32.ChecksumIEEE 算法。 } return m } @@ -34,11 +34,15 @@ func New(replicas int, fn Hash) *Map { func (m *Map) Add(keys ...string) { for _, key := range keys { for i := 0; i < m.replicas; i++ { + // 对每一个真实节点 key,对应创建 m.replicas 个虚拟节点,虚拟节点的名称是:strconv.Itoa(i) + key,即通过添加编号的方式区分不同虚拟节点。 hash := int(m.hash([]byte(strconv.Itoa(i) + key))) + // 使用 m.hash() 计算虚拟节点的哈希值,使用 append(m.keys, hash) 添加到环上。 m.keys = append(m.keys, hash) + // 在 hashMap 中增加虚拟节点和真实节点的映射关系 m.hashMap[hash] = key } } + // 最后一步,环上的哈希值排序。 sort.Ints(m.keys) } @@ -47,12 +51,16 @@ func (m *Map) Get(key string) string { if len(m.keys) == 0 { return "" } - + // 第一步,计算 key 的哈希值。 hash := int(m.hash([]byte(key))) // Binary search for appropriate replica. + // 第二步,顺时针找到第一个匹配的虚拟节点的下标 idx,从 m.keys 中获取到对应的哈希值。如果 idx == len(m.keys), + // 说明应选择 m.keys[0], idx := sort.Search(len(m.keys), func(i int) bool { + // 寻找到第一个大于这个hash值的keys[i]的坐标idx return m.keys[i] >= hash }) - + // 第三步,通过 hashMap 映射得到真实的节点。 + // 因为 m.keys 是一个环状结构,所以用取余数的方式来处理这种情况。 return m.hashMap[m.keys[idx%len(m.keys)]] } diff --git a/gee-cache/day4-consistent-hash/geecache/consistenthash/consistenthash_test.go b/gee-cache/day4-consistent-hash/geecache/consistenthash/consistenthash_test.go index 34e1275..5faab00 100644 --- a/gee-cache/day4-consistent-hash/geecache/consistenthash/consistenthash_test.go +++ b/gee-cache/day4-consistent-hash/geecache/consistenthash/consistenthash_test.go @@ -6,6 +6,7 @@ import ( ) func TestHashing(t *testing.T) { + // 自定义的 Hash 算法只处理数字,传入字符串表示的数字,返回对应的数字即可。 hash := New(3, func(key []byte) uint32 { i, _ := strconv.Atoi(string(key)) return uint32(i) @@ -13,13 +14,14 @@ func TestHashing(t *testing.T) { // Given the above hash function, this will give replicas with "hashes": // 2, 4, 6, 12, 14, 16, 22, 24, 26 - hash.Add("6", "4", "2") - + hash.Add("6", "4", "2") // 一开始,有 2/4/6 三个真实节点,对应的虚拟节点的哈希值是 02/12/22、04/14/24、06/16/26。 + //那么用例 2/11/23/27 选择的虚拟节点分别是 02/12/24/02,也就是真实节点 2/2/4/2。 testCases := map[string]string{ "2": "2", "11": "2", "23": "4", "27": "2", + "3": "4", } for k, v := range testCases { From 757bcdcbb119403320bbb6de28c8382ea3ae1c58 Mon Sep 17 00:00:00 2001 From: Alcuin <41372488+Alcuin1@users.noreply.github.com> Date: Thu, 8 Jun 2023 01:44:07 +0800 Subject: [PATCH 115/122] Update lru.go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Day1 LRU缓存淘汰策略 --- gee-cache/day1-lru/geecache/lru/lru.go | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/gee-cache/day1-lru/geecache/lru/lru.go b/gee-cache/day1-lru/geecache/lru/lru.go index dc1a317..c4f88b9 100644 --- a/gee-cache/day1-lru/geecache/lru/lru.go +++ b/gee-cache/day1-lru/geecache/lru/lru.go @@ -4,20 +4,22 @@ import "container/list" // Cache is a LRU cache. It is not safe for concurrent access. type Cache struct { - maxBytes int64 - nbytes int64 - ll *list.List - cache map[string]*list.Element + maxBytes int64 // 是允许使用的最大内存 + nbytes int64 // nbytes 是当前已使用的内存 + ll *list.List // 在这里我们直接使用 Go 语言标准库实现的双向链表list.List + cache map[string]*list.Element // 键是字符串,值是双向链表中对应节点的指针 // optional and executed when an entry is purged. - OnEvicted func(key string, value Value) + OnEvicted func(key string, value Value) // 是某条记录被移除时的回调函数,可以为 nil } +// 键值对 entry 是双向链表节点的数据类型,在链表中仍保存每个值对应的 key 的好处在于,淘汰队首节点时,需要用 key 从字典中删除对应的映射 type entry struct { key string value Value } // Value use Len to count how many bytes it takes +// 为了通用性,我们允许值是实现了 Value 接口的任意类型,该接口只包含了一个方法 Len() int,用于返回值所占用的内存大小。 type Value interface { Len() int } @@ -34,22 +36,28 @@ func New(maxBytes int64, onEvicted func(string, Value)) *Cache { // Add adds a value to the cache. func (c *Cache) Add(key string, value Value) { + // 如果键对应的链表节点存在,则将对应节点移动到队尾,并返回查找到的值 + // 如果键存在,则更新对应节点的值,并将该节点移到队尾 if ele, ok := c.cache[key]; ok { + // c.ll.MoveToFront(ele),即将链表中的节点 ele 移动到队尾(双向链表作为队列,队首队尾是相对的,在这里约定 front 为队尾) c.ll.MoveToFront(ele) kv := ele.Value.(*entry) c.nbytes += int64(value.Len()) - int64(kv.value.Len()) kv.value = value } else { + // 不存在则是新增场景,首先队尾添加新节点 &entry{key, value}, 并字典中添加 key 和节点的映射关系。 ele := c.ll.PushFront(&entry{key, value}) c.cache[key] = ele c.nbytes += int64(len(key)) + int64(value.Len()) } + // 更新 c.nbytes,如果超过了设定的最大值 c.maxBytes,则移除最少访问的节点 for c.maxBytes != 0 && c.maxBytes < c.nbytes { c.RemoveOldest() } } // Get look ups a key's value +// 查找主要有 2 个步骤,第一步是从字典中找到对应的双向链表的节点,第二步,将该节点移动到队尾 func (c *Cache) Get(key string) (value Value, ok bool) { if ele, ok := c.cache[key]; ok { c.ll.MoveToFront(ele) @@ -60,13 +68,18 @@ func (c *Cache) Get(key string) (value Value, ok bool) { } // RemoveOldest removes the oldest item +// 这里的删除,实际上是缓存淘汰。即移除最近最少访问的节点(队首) func (c *Cache) RemoveOldest() { + // c.ll.Back() 取到队首节点,从链表中删除 ele := c.ll.Back() if ele != nil { c.ll.Remove(ele) kv := ele.Value.(*entry) + // delete(c.cache, kv.key),从字典中 c.cache 删除该节点的映射关系 delete(c.cache, kv.key) + // 更新当前所用的内存 c.nbytes c.nbytes -= int64(len(kv.key)) + int64(kv.value.Len()) + // 如果回调函数 OnEvicted 不为 nil,则调用回调函数 if c.OnEvicted != nil { c.OnEvicted(kv.key, kv.value) } @@ -74,6 +87,7 @@ func (c *Cache) RemoveOldest() { } // Len the number of cache entries +// 最后,为了方便测试,我们实现 Len() 用来获取添加了多少条数据 func (c *Cache) Len() int { return c.ll.Len() } From 2bcb48d91cfe718d2a67238833072a09aa5b2426 Mon Sep 17 00:00:00 2001 From: Alcuin <41372488+Alcuin1@users.noreply.github.com> Date: Thu, 8 Jun 2023 15:33:03 +0800 Subject: [PATCH 116/122] =?UTF-8?q?Day2=20=E5=8D=95=E6=9C=BA=E5=B9=B6?= =?UTF-8?q?=E5=8F=91=E7=BC=93=E5=AD=98geecache.go?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../day2-single-node/geecache/geecache.go | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/gee-cache/day2-single-node/geecache/geecache.go b/gee-cache/day2-single-node/geecache/geecache.go index e289018..a3ca247 100644 --- a/gee-cache/day2-single-node/geecache/geecache.go +++ b/gee-cache/day2-single-node/geecache/geecache.go @@ -7,21 +7,33 @@ import ( ) // A Group is a cache namespace and associated data loaded spread over +// 一个 Group 可以认为是一个缓存的命名空间 type Group struct { - name string - getter Getter + // 每个 Group 拥有一个唯一的名称 name。比如可以创建三个 Group, + // 缓存学生的成绩命名为 scores,缓存学生信息的命名为 info,缓存学生课程的命名为 courses。 + name string + // 第二个属性是 getter Getter,即缓存未命中时获取源数据的回调(callback)。 + getter Getter + // 第三个属性是 mainCache cache,即一开始实现的并发缓存 mainCache cache } // A Getter loads data for a key. +// 定义接口 Getter 和 回调函数 Get(key string)([]byte, error),参数是 key,返回值是 []byte。 type Getter interface { Get(key string) ([]byte, error) } // A GetterFunc implements Getter with a function. +// 定义函数类型 GetterFunc,并实现 Getter 接口的 Get 方法。 type GetterFunc func(key string) ([]byte, error) // Get implements Getter interface function +// 函数类型实现某一个接口,称之为接口型函数,方便使用者在调用时既能够传入函数作为参数,也能够传入实现了该接口的结构体作为参数。 +// 补充: +// 这里呢,定义了一个接口 Getter,只包含一个方法 Get(key string) ([]byte, error),紧接着定义了一个函数类型 GetterFunc, +// GetterFunc 参数和返回值与 Getter 中 Get 方法是一致的。而且 GetterFunc 还定义了 Get 方式,并在 Get 方法中调用自己, +// 这样就实现了接口 Getter。所以 GetterFunc 是一个实现了接口的函数类型,简称为接口型函数。 func (f GetterFunc) Get(key string) ([]byte, error) { return f(key) } @@ -32,6 +44,7 @@ var ( ) // NewGroup create a new instance of Group +// 构建函数 NewGroup 用来实例化 Group,并且将 group 存储在全局变量 groups 中。 func NewGroup(name string, cacheBytes int64, getter Getter) *Group { if getter == nil { panic("nil Getter") @@ -52,28 +65,32 @@ func NewGroup(name string, cacheBytes int64, getter Getter) *Group { func GetGroup(name string) *Group { mu.RLock() g := groups[name] + // GetGroup 用来特定名称的 Group,这里使用了只读锁 RLock(),因为不涉及任何冲突变量的写操作 mu.RUnlock() return g } // Get value for a key from cache +// Get 方法实现了上述所说的流程 ⑴ 和 ⑶。 func (g *Group) Get(key string) (ByteView, error) { if key == "" { return ByteView{}, fmt.Errorf("key is required") } - + // 流程 ⑴ :从 mainCache 中查找缓存,如果存在则返回缓存值 if v, ok := g.mainCache.get(key); ok { log.Println("[GeeCache] hit") return v, nil } - + // 流程 ⑶ :缓存不存在,则调用 load 方法 return g.load(key) } +// load 调用 getLocally(分布式场景下会调用 getFromPeer 从其他节点获取), func (g *Group) load(key string) (value ByteView, err error) { return g.getLocally(key) } +// getLocally 调用用户回调函数 g.getter.Get() 获取源数据,并且将源数据添加到缓存 mainCache 中(通过 populateCache 方法) func (g *Group) getLocally(key string) (ByteView, error) { bytes, err := g.getter.Get(key) if err != nil { From 4815d603f9530b9de440fbaa2f4d82b58b00a5b8 Mon Sep 17 00:00:00 2001 From: Alcuin <41372488+Alcuin1@users.noreply.github.com> Date: Thu, 8 Jun 2023 15:37:52 +0800 Subject: [PATCH 117/122] =?UTF-8?q?Day1=20LRU=20=E7=BC=93=E5=AD=98?= =?UTF-8?q?=E6=B7=98=E6=B1=B0=E7=AD=96=E7=95=A5=20cache.go?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gee-cache/day2-single-node/geecache/cache.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gee-cache/day2-single-node/geecache/cache.go b/gee-cache/day2-single-node/geecache/cache.go index 665c3f3..6f56ab8 100644 --- a/gee-cache/day2-single-node/geecache/cache.go +++ b/gee-cache/day2-single-node/geecache/cache.go @@ -5,12 +5,15 @@ import ( "sync" ) +// cache.go 的实现非常简单,实例化 lru,封装 get 和 add 方法,并添加互斥锁 mu。 type cache struct { mu sync.Mutex lru *lru.Cache cacheBytes int64 } +// 在 add 方法中,判断了 c.lru 是否为 nil,如果等于 nil 再创建实例。这种方法称之为延迟初始化(Lazy Initialization), +// 一个对象的延迟初始化意味着该对象的创建将会延迟至第一次使用该对象时。主要用于提高性能,并减少程序内存要求。 func (c *cache) add(key string, value ByteView) { c.mu.Lock() defer c.mu.Unlock() From a5f414e966fc900964fc1a16d82689d733427701 Mon Sep 17 00:00:00 2001 From: Alcuin <41372488+Alcuin1@users.noreply.github.com> Date: Thu, 8 Jun 2023 15:40:30 +0800 Subject: [PATCH 118/122] =?UTF-8?q?Day2=20=E5=8D=95=E6=9C=BA=E5=B9=B6?= =?UTF-8?q?=E5=8F=91=E7=BC=93=E5=AD=98=20byteview.go?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gee-cache/day2-single-node/geecache/byteview.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gee-cache/day2-single-node/geecache/byteview.go b/gee-cache/day2-single-node/geecache/byteview.go index 3ee1022..62a793a 100644 --- a/gee-cache/day2-single-node/geecache/byteview.go +++ b/gee-cache/day2-single-node/geecache/byteview.go @@ -1,16 +1,19 @@ package geecache // A ByteView holds an immutable view of bytes. +// ByteView 只有一个数据成员,b []byte,b 将会存储真实的缓存值。选择 byte 类型是为了能够支持任意的数据类型的存储,例如字符串、图片等。 type ByteView struct { b []byte } // Len returns the view's length +// 实现 Len() int 方法,我们在 lru.Cache 的实现中,要求被缓存对象必须实现 Value 接口,即 Len() int 方法,返回其所占的内存大小。 func (v ByteView) Len() int { return len(v.b) } // ByteSlice returns a copy of the data as a byte slice. +// b 是只读的,使用 ByteSlice() 方法返回一个拷贝,防止缓存值被外部程序修改。 func (v ByteView) ByteSlice() []byte { return cloneBytes(v.b) } From fd3278efa07e32d1ce13540c3b9989ef0f61c778 Mon Sep 17 00:00:00 2001 From: Alcuin <41372488+Alcuin1@users.noreply.github.com> Date: Thu, 8 Jun 2023 15:42:13 +0800 Subject: [PATCH 119/122] =?UTF-8?q?Day2=20=E5=8D=95=E6=9C=BA=E5=B9=B6?= =?UTF-8?q?=E5=8F=91=E7=BC=93=E5=AD=98=20geecache=5Ftest.go?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gee-cache/day2-single-node/geecache/geecache_test.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/gee-cache/day2-single-node/geecache/geecache_test.go b/gee-cache/day2-single-node/geecache/geecache_test.go index 7ef9f4f..d507b9a 100644 --- a/gee-cache/day2-single-node/geecache/geecache_test.go +++ b/gee-cache/day2-single-node/geecache/geecache_test.go @@ -13,12 +13,13 @@ var db = map[string]string{ "Sam": "567", } +// 在这个测试用例中,我们借助 GetterFunc 的类型转换,将一个匿名回调函数转换成了接口 f Getter。 func TestGetter(t *testing.T) { var f Getter = GetterFunc(func(key string) ([]byte, error) { return []byte(key), nil }) - expect := []byte("key") + // 调用该接口的方法 f.Get(key string),实际上就是在调用匿名回调函数。 if v, _ := f.Get("key"); !reflect.DeepEqual(v, expect) { t.Fatal("callback failed") } @@ -29,10 +30,13 @@ func TestGet(t *testing.T) { gee := NewGroup("scores", 2<<10, GetterFunc( func(key string) ([]byte, error) { log.Println("[SlowDB] search key", key) + // 如果存在db中 if v, ok := db[key]; ok { + // 如果该key没有调用回调函数,就初始化一下 if _, ok := loadCounts[key]; !ok { loadCounts[key] = 0 } + // count++ loadCounts[key]++ return []byte(v), nil } @@ -43,6 +47,7 @@ func TestGet(t *testing.T) { if view, err := gee.Get(k); err != nil || view.String() != v { t.Fatal("failed to get value of Tom") } + // 在缓存已经存在的情况下,是否直接从缓存中获取,为了实现这一点,使用 loadCounts 统计某个键调用回调函数的次数,如果次数大于1,则表示调用了多次回调函数,没有缓存。 if _, err := gee.Get(k); err != nil || loadCounts[k] > 1 { t.Fatalf("cache %s miss", k) } From a668fe12668adad47df82d6c45704be04d425695 Mon Sep 17 00:00:00 2001 From: Alcuin <41372488+Alcuin1@users.noreply.github.com> Date: Thu, 8 Jun 2023 16:06:07 +0800 Subject: [PATCH 120/122] =?UTF-8?q?Day3=20HTTP=20=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E7=AB=AF=20http.go?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gee-cache/day3-http-server/geecache/http.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/gee-cache/day3-http-server/geecache/http.go b/gee-cache/day3-http-server/geecache/http.go index b9b994e..468ef5e 100644 --- a/gee-cache/day3-http-server/geecache/http.go +++ b/gee-cache/day3-http-server/geecache/http.go @@ -31,32 +31,33 @@ func (p *HTTPPool) Log(format string, v ...interface{}) { // ServeHTTP handle all http requests func (p *HTTPPool) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // ServeHTTP 的实现逻辑是比较简单的,首先判断访问路径的前缀是否是 basePath,不是返回错误。 if !strings.HasPrefix(r.URL.Path, p.basePath) { panic("HTTPPool serving unexpected path: " + r.URL.Path) } p.Log("%s %s", r.Method, r.URL.Path) // /// required + // 我们约定访问路径格式为 /// parts := strings.SplitN(r.URL.Path[len(p.basePath):], "/", 2) if len(parts) != 2 { http.Error(w, "bad request", http.StatusBadRequest) return } - groupName := parts[0] key := parts[1] - + // 通过 groupname 得到 group 实例 group := GetGroup(groupName) if group == nil { http.Error(w, "no such group: "+groupName, http.StatusNotFound) return } - + // 再使用 group.Get(key) 获取缓存数据 view, err := group.Get(key) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } - w.Header().Set("Content-Type", "application/octet-stream") + // 最终使用 w.Write() 将缓存值作为 httpResponse 的 body 返回 w.Write(view.ByteSlice()) } From 444d80fb156fe831db6f8a27818990f1ac41d0c9 Mon Sep 17 00:00:00 2001 From: Alcuin <41372488+Alcuin1@users.noreply.github.com> Date: Thu, 8 Jun 2023 16:06:40 +0800 Subject: [PATCH 121/122] =?UTF-8?q?Day3=20HTTP=20=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E7=AB=AF=20main.go?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gee-cache/day3-http-server/main.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gee-cache/day3-http-server/main.go b/gee-cache/day3-http-server/main.go index 5442dd7..dff9003 100644 --- a/gee-cache/day3-http-server/main.go +++ b/gee-cache/day3-http-server/main.go @@ -22,6 +22,7 @@ var db = map[string]string{ } func main() { + // 创建一个名为 scores 的 Group,若缓存为空,回调函数会从 db 中获取数据并返回。 geecache.NewGroup("scores", 2<<10, geecache.GetterFunc( func(key string) ([]byte, error) { log.Println("[SlowDB] search key", key) @@ -30,7 +31,7 @@ func main() { } return nil, fmt.Errorf("%s not exist", key) })) - + // 使用 http.ListenAndServe 在 9999 端口启动了 HTTP 服务。 addr := "localhost:9999" peers := geecache.NewHTTPPool(addr) log.Println("geecache is running at", addr) From 3c0a79366de7a4a293097ab985eb1a05f17330f4 Mon Sep 17 00:00:00 2001 From: Alcuin <41372488+Alcuin1@users.noreply.github.com> Date: Thu, 8 Jun 2023 16:56:55 +0800 Subject: [PATCH 122/122] =?UTF-8?q?Day5=20=E5=88=86=E5=B8=83=E5=BC=8F?= =?UTF-8?q?=E8=8A=82=E7=82=B9=20peers.go?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gee-cache/day5-multi-nodes/geecache/peers.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gee-cache/day5-multi-nodes/geecache/peers.go b/gee-cache/day5-multi-nodes/geecache/peers.go index 8d010e2..6246267 100644 --- a/gee-cache/day5-multi-nodes/geecache/peers.go +++ b/gee-cache/day5-multi-nodes/geecache/peers.go @@ -2,11 +2,13 @@ package geecache // PeerPicker is the interface that must be implemented to locate // the peer that owns a specific key. +// PeerPicker 的 PickPeer() 方法用于根据传入的 key 选择相应节点 PeerGetter。 type PeerPicker interface { PickPeer(key string) (peer PeerGetter, ok bool) } // PeerGetter is the interface that must be implemented by a peer. +// 接口 PeerGetter 的 Get() 方法用于从对应 group 查找缓存值。PeerGetter 就对应于上述流程中的 HTTP 客户端 type PeerGetter interface { Get(group string, key string) ([]byte, error) }