add day3 & day4

This commit is contained in:
gzdaijie
2019-08-13 02:18:33 +08:00
parent d488fedaf9
commit 947c55dc95
23 changed files with 1045 additions and 204 deletions
+116 -7
View File
@@ -2,12 +2,121 @@
![Gee](doc/gee/gee.jpg)
## [目录](https://geektutu.com/post/gee.html)
## [教程目录](https://geektutu.com/post/gee.html)
- [第一天:前置知识(http.Handler接口)](https://geektutu.com/post/gee-day1.html)[Code - Github](day1-http-base)
- 第二天:Tire树实现路由(Router)[Code - Github](day2-router)
- 第三天:设计Context
- 第四天:支持模板(HTML Template)
- 第五天:支持中间件(Middleware)
- 第六天:简单鉴权(BASIC AUTH)
- 第七天:异常错误处理(Panic)
- 第二天:Context上下文设计[Code - Github](day2-context)
- 第三天:Tire树路由(Router)[Code - Github](day3-router)
- 第四天:分组控制(Group)[Code - Github](day4-group)
- 第五天:中间件(Middleware)
- 第六天:HTML模板(Template)
- 第七天:异常错误处理(Panic)
## 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, "<h1>Hello Gee</h1>")
})
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, "<h1>Hello Gee</h1>")
})
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.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 4 - Nesting Group Control
```go
func main() {
r := gee.New()
v1 := r.Group("/v1")
{
v1.GET("/", func(c *gee.Context) {
c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")
})
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")
}
```
+13 -2
View File
@@ -18,9 +18,19 @@ func New() *Engine {
return &Engine{router: make(map[string]HandlerFunc)}
}
func (engine *Engine) addRoute(method string, pattern string, handler HandlerFunc) {
key := method + "-" + pattern
engine.router[key] = handler
}
// GET defines the method to add GET request
func (engine *Engine) GET(pattern string, handler HandlerFunc) {
engine.router[pattern] = handler
engine.addRoute("GET", pattern, handler)
}
// POST defines the method to add POST request
func (engine *Engine) POST(pattern string, handler HandlerFunc) {
engine.addRoute("POST", pattern, handler)
}
// Run defines the method to start a http server
@@ -29,7 +39,8 @@ func (engine *Engine) Run(addr string) (err error) {
}
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if handler, ok := engine.router[req.URL.Path]; ok {
key := req.Method + "-" + req.URL.Path
if handler, ok := engine.router[key]; ok {
handler(w, req)
} else {
fmt.Fprintf(w, "404 NOT FOUND: %s\n", req.URL)
+65
View File
@@ -0,0 +1,65 @@
package gee
import (
"encoding/json"
"fmt"
"net/http"
)
type Context struct {
Path string
Method string
Writer http.ResponseWriter
Req *http.Request
}
func newContext(w http.ResponseWriter, req *http.Request) *Context {
return &Context{
Path: req.URL.Path,
Method: req.Method,
Req: req,
Writer: w,
}
}
func (c *Context) PostForm(key string) string {
return c.Req.FormValue(key)
}
func (c *Context) Query(key string) string {
return c.Req.URL.Query().Get(key)
}
func (c *Context) Status(code int) {
c.Writer.WriteHeader(code)
}
func (c *Context) SetHeader(key string, value string) {
c.Writer.Header().Set(key, value)
}
func (c *Context) String(code int, format string, values ...interface{}) {
c.Status(code)
c.SetHeader("Content-Type", "text/plain")
c.Writer.Write([]byte(fmt.Sprintf(format, values...)))
}
func (c *Context) HTML(code int, html string) {
c.Writer.WriteHeader(code)
c.SetHeader("Content-Type", "text/html")
c.Writer.Write([]byte(html))
}
func (c *Context) JSON(code int, obj interface{}) {
c.Writer.WriteHeader(code)
c.SetHeader("Content-Type", "application/json")
encoder := json.NewEncoder(c.Writer)
if err := encoder.Encode(obj); err != nil {
http.Error(c.Writer, err.Error(), 500)
}
}
func (c *Context) Data(code int, data []byte) {
c.Writer.WriteHeader(code)
c.Writer.Write(data)
}
+41
View File
@@ -0,0 +1,41 @@
package gee
import (
"net/http"
)
// HandlerFunc defines the request handler used by gee
type HandlerFunc func(*Context)
// Engine implement the interface of ServeHTTP
type Engine struct {
router *router
}
// New is the constructor of gee.Engine
func New() *Engine {
return &Engine{router: newRouter()}
}
func (engine *Engine) addRoute(method string, pattern string, handler HandlerFunc) {
engine.router.addRoute(method, pattern, handler)
}
// GET defines the method to add GET request
func (engine *Engine) GET(pattern string, handler HandlerFunc) {
engine.addRoute("GET", pattern, handler)
}
// POST defines the method to add POST request
func (engine *Engine) POST(pattern string, handler HandlerFunc) {
engine.addRoute("POST", pattern, handler)
}
// Run defines the method to start a http server
func (engine *Engine) Run(addr string) (err error) {
return http.ListenAndServe(addr, engine)
}
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
engine.router.handle(w, req)
}
+28
View File
@@ -0,0 +1,28 @@
package gee
import (
"net/http"
)
type router struct {
handlers map[string]HandlerFunc
}
func newRouter() *router {
return &router{handlers: make(map[string]HandlerFunc)}
}
func (r *router) addRoute(method string, pattern string, handler HandlerFunc) {
key := method + "-" + pattern
r.handlers[key] = handler
}
func (r *router) handle(w http.ResponseWriter, req *http.Request) {
key := req.Method + "-" + req.URL.Path
c := newContext(w, req)
if handler, ok := r.handlers[key]; ok {
handler(c)
} else {
c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
}
}
+47
View File
@@ -0,0 +1,47 @@
package main
// (1)
// $ curl -i http://localhost:9999/
// HTTP/1.1 200 OK
// Date: Mon, 12 Aug 2019 16:52:52 GMT
// Content-Length: 18
// Content-Type: text/html; charset=utf-8
// <h1>Hello Gee</h1>
// (2)
// $ curl "http://localhost:9999/hello?name=geektutu"
// hello geektutu, you're at /hello
// (3)
// $ curl "http://localhost:9999/login" -X POST -d 'username=geektutu&password=1234'
// {"password":"1234","username":"geektutu"}
// (4)
// $ curl "http://localhost:9999/xxx"
// 404 NOT FOUND: /xxx
import (
"net/http"
"./gee"
)
func main() {
r := gee.New()
r.GET("/", func(c *gee.Context) {
c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")
})
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")
}
-52
View File
@@ -1,52 +0,0 @@
package gee
import (
"fmt"
"net/http"
)
type Params map[string]string
func (ps *Params) Get(key string) string {
if value, ok := (*ps)[key]; ok {
return value
}
return ""
}
func (ps *Params) set(key string, value string) {
(*ps)[key] = value
}
// HandlerFunc defines the request handler used by gee
type HandlerFunc func(http.ResponseWriter, *http.Request, *Params)
// Engine is defined to handle all requests
type Engine struct {
router *router
}
// New is constructor of Engine
func New() *Engine {
return &Engine{
router: &router{root: &node{}},
}
}
// GET defines the method to add GET request
func (engine *Engine) GET(pattern string, handler HandlerFunc) {
engine.router.addRoute(pattern, handler)
}
// Run defines the method to start a http server
func (engine *Engine) Run(addr string) (err error) {
return http.ListenAndServe(addr, engine)
}
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if n, params := engine.router.getRoute(req.URL.Path); n != nil {
n.handler(w, req, params)
} else {
fmt.Fprintf(w, "404 NOT FOUND: %s\n", req.URL)
}
}
-49
View File
@@ -1,49 +0,0 @@
package gee
import (
"strings"
)
type router struct {
root *node
}
func (r *router) addRoute(path string, handler HandlerFunc) {
parts := filterNonEmpty(strings.Split(path, "/"))
r.root.insert(path, parts, handler, 0)
}
func (r *router) getRoute(path string) (*node, *Params) {
searchParts := filterNonEmpty(strings.Split(path, "/"))
n := r.root.search(searchParts, 0)
if n.path != "" {
parts := filterNonEmpty(strings.Split(n.path, "/"))
params := &Params{}
for index, part := range parts {
if part[0] == ':' {
params.set(part[1:], searchParts[index])
}
}
return n, params
}
return nil, nil
}
func (r *router) getRoutes() []*node {
list := make([]*node, 0)
r.root.travel(&list)
return list
}
func filterNonEmpty(vs []string) []string {
parts := make([]string, 0)
for _, item := range vs {
if item != "" {
parts = append(parts, item)
}
}
return parts
}
-46
View File
@@ -1,46 +0,0 @@
package gee
import (
"fmt"
"testing"
)
func newTestRouter() *router {
r := &router{root: &node{}}
r.addRoute("/", nil)
r.addRoute("/hello/:name", nil)
r.addRoute("/hello/b/c", nil)
r.addRoute("/hi/:name", nil)
return r
}
func TestGetRoute(t *testing.T) {
r := newTestRouter()
n, ps := r.getRoute("/a/geektutu")
fmt.Printf("matched path: %s, params['name']: %s\n", n.path, ps.Get("name"))
if n == nil {
t.Fatal("nil shouldn't be returned")
}
if n.path != "/a/:name" {
t.Fatal("should match /a/:name")
}
if ps.Get("name") != "geektutu" {
t.Fatal("name should be equal to 'geektutu'")
}
}
func TestGetRoutes(t *testing.T) {
r := newTestRouter()
for i, n := range r.getRoutes() {
fmt.Println(i+1, n)
}
if len(r.getRoutes()) != 4 {
t.Fatal("the number of routes shoule be 4")
}
}
-31
View File
@@ -1,31 +0,0 @@
package main
// $ curl http://localhost:9999/
// URL.Path = "/"
// $ curl http://localhost:9999/hello
// Header["Accept"] = ["*/*"]
// Header["User-Agent"] = ["curl/7.54.0"]
// curl http://localhost:9999/world
// 404 NOT FOUND: /world
import (
"fmt"
"net/http"
"./gee"
)
func main() {
r := gee.New()
r.GET("/", func(w http.ResponseWriter, req *http.Request, params *gee.Params) {
fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
})
r.GET("/hello/:name", helloHandler)
r.Run(":9999")
}
func helloHandler(w http.ResponseWriter, req *http.Request, params *gee.Params) {
fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
fmt.Fprintf(w, "Parse params in path, name: %s\n", params.Get("name"))
}
+72
View File
@@ -0,0 +1,72 @@
package gee
import (
"encoding/json"
"fmt"
"net/http"
)
type Context struct {
Path string
Method string
Writer http.ResponseWriter
Req *http.Request
Params map[string]string
}
func newContext(w http.ResponseWriter, req *http.Request, params map[string]string) *Context {
return &Context{
Path: req.URL.Path,
Method: req.Method,
Params: make(map[string]string),
Req: req,
Writer: w,
}
}
func (c *Context) Param(key string) string {
value, _ := c.Params[key]
return value
}
func (c *Context) PostForm(key string) string {
return c.Req.FormValue(key)
}
func (c *Context) Query(key string) string {
return c.Req.URL.Query().Get(key)
}
func (c *Context) Status(code int) {
c.Writer.WriteHeader(code)
}
func (c *Context) SetHeader(key string, value string) {
c.Writer.Header().Set(key, value)
}
func (c *Context) String(code int, format string, values ...interface{}) {
c.Status(code)
c.SetHeader("Content-Type", "text/plain")
c.Writer.Write([]byte(fmt.Sprintf(format, values...)))
}
func (c *Context) HTML(code int, html string) {
c.Writer.WriteHeader(code)
c.SetHeader("Content-Type", "text/html")
c.Writer.Write([]byte(html))
}
func (c *Context) JSON(code int, obj interface{}) {
c.Writer.WriteHeader(code)
c.SetHeader("Content-Type", "application/json")
encoder := json.NewEncoder(c.Writer)
if err := encoder.Encode(obj); err != nil {
http.Error(c.Writer, err.Error(), 500)
}
}
func (c *Context) Data(code int, data []byte) {
c.Writer.WriteHeader(code)
c.Writer.Write(data)
}
+41
View File
@@ -0,0 +1,41 @@
package gee
import (
"net/http"
)
// HandlerFunc defines the request handler used by gee
type HandlerFunc func(*Context)
// Engine implement the interface of ServeHTTP
type Engine struct {
router *router
}
// New is the constructor of gee.Engine
func New() *Engine {
return &Engine{router: newRouter()}
}
func (engine *Engine) addRoute(method string, pattern string, handler HandlerFunc) {
engine.router.addRoute(method, pattern, handler)
}
// GET defines the method to add GET request
func (engine *Engine) GET(pattern string, handler HandlerFunc) {
engine.addRoute("GET", pattern, handler)
}
// POST defines the method to add POST request
func (engine *Engine) POST(pattern string, handler HandlerFunc) {
engine.addRoute("POST", pattern, handler)
}
// Run defines the method to start a http server
func (engine *Engine) Run(addr string) (err error) {
return http.ListenAndServe(addr, engine)
}
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
engine.router.handle(w, req)
}
+84
View File
@@ -0,0 +1,84 @@
package gee
import (
"net/http"
"strings"
)
type router struct {
roots map[string]*node
handlers map[string]HandlerFunc
}
func newRouter() *router {
return &router{
roots: make(map[string]*node),
handlers: make(map[string]HandlerFunc),
}
}
func (r *router) addRoute(method string, pattern string, handler HandlerFunc) {
parts := filterNonEmpty(strings.Split(pattern, "/"))
key := method + "-" + pattern
_, ok := r.roots[method]
if !ok {
r.roots[method] = &node{}
}
r.roots[method].insert(pattern, parts, 0)
r.handlers[key] = handler
}
func (r *router) handle(w http.ResponseWriter, req *http.Request) {
n, params := r.getRoute(req.Method, req.URL.Path)
c := newContext(w, req, params)
if n != nil {
key := c.Method + "-" + n.pattern
r.handlers[key](c)
} else {
c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
}
}
func (r *router) getRoute(method string, pattern string) (*node, map[string]string) {
searchParts := filterNonEmpty(strings.Split(pattern, "/"))
params := make(map[string]string)
root, ok := r.roots[method]
if !ok {
return nil, nil
}
n := root.search(searchParts, 0)
if n != nil {
parts := filterNonEmpty(strings.Split(n.pattern, "/"))
for index, part := range parts {
if part[0] == ':' {
params[part[1:]] = searchParts[index]
}
}
return n, params
}
return nil, nil
}
func (r *router) getRoutes(method string) []*node {
root, ok := r.roots[method]
if !ok {
return nil
}
nodes := make([]*node, 0)
root.travel(&nodes)
return nodes
}
func filterNonEmpty(vs []string) []string {
parts := make([]string, 0)
for _, item := range vs {
if item != "" {
parts = append(parts, item)
}
}
return parts
}
+47
View File
@@ -0,0 +1,47 @@
package gee
import (
"fmt"
"testing"
)
func newTestRouter() *router {
r := newRouter()
r.addRoute("GET", "/", nil)
r.addRoute("GET", "/hello/:name", nil)
r.addRoute("GET", "/hello/b/c", nil)
r.addRoute("GET", "/hi/:name", nil)
return r
}
func TestGetRoute(t *testing.T) {
r := newTestRouter()
n, ps := r.getRoute("GET", "/hello/geektutu")
if n == nil {
t.Fatal("nil shouldn't be returned")
}
if n.pattern != "/hello/:name" {
t.Fatal("should match /hello/:name")
}
if ps["name"] != "geektutu" {
t.Fatal("name should be equal to 'geektutu'")
}
fmt.Printf("matched path: %s, params['name']: %s\n", n.pattern, ps["name"])
}
func TestGetRoutes(t *testing.T) {
r := newTestRouter()
nodes := r.getRoutes("GET")
for i, n := range nodes {
fmt.Println(i+1, n)
}
if len(nodes) != 4 {
t.Fatal("the number of routes shoule be 4")
}
}
@@ -5,21 +5,19 @@ import (
)
type node struct {
path string
pattern string
part string
children []*node
isWild bool
handler HandlerFunc
}
func (n *node) String() string {
return fmt.Sprintf("node{path=%s, part=%s, isWild=%t}", n.path, n.part, n.isWild)
return fmt.Sprintf("node{pattern=%s, part=%s, isWild=%t}", n.pattern, n.part, n.isWild)
}
func (n *node) insert(path string, parts []string, handler HandlerFunc, height int) {
func (n *node) insert(pattern string, parts []string, height int) {
if len(parts) == height {
n.path = path
n.handler = handler
n.pattern = pattern
return
}
@@ -29,12 +27,12 @@ func (n *node) insert(path string, parts []string, handler HandlerFunc, height i
child = &node{part: part, isWild: part[0] == ':'}
n.children = append(n.children, child)
}
child.insert(path, parts, handler, height+1)
child.insert(pattern, parts, height+1)
}
func (n *node) search(parts []string, height int) *node {
if len(parts) == height {
if n.path == "" {
if n.pattern == "" {
return nil
}
return n
@@ -54,7 +52,7 @@ func (n *node) search(parts []string, height int) *node {
}
func (n *node) travel(list *([]*node)) {
if n.path != "" {
if n.pattern != "" {
*list = append(*list, n)
}
for _, child := range n.children {
+57
View File
@@ -0,0 +1,57 @@
package main
// (1)
// $ curl -i http://localhost:9999/
// HTTP/1.1 200 OK
// Date: Mon, 12 Aug 2019 16:52:52 GMT
// Content-Length: 18
// Content-Type: text/html; charset=utf-8
// <h1>Hello Gee</h1>
// (2)
// $ curl "http://localhost:9999/hello?name=geektutu"
// hello geektutu, you're at /hello
// (3)
// $ curl "http://localhost:9999/hello/geektutu"
// hello , you're at /hello/geektutu
// (4)
// $ curl "http://localhost:9999/login" -X POST -d 'username=geektutu&password=1234'
// {"password":"1234","username":"geektutu"}
// (5)
// $ curl "http://localhost:9999/xxx"
// 404 NOT FOUND: /xxx
import (
"net/http"
"./gee"
)
func main() {
r := gee.New()
r.GET("/", func(c *gee.Context) {
c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")
})
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.POST("/login", func(c *gee.Context) {
c.JSON(http.StatusOK, &map[string]string{
"username": c.PostForm("username"),
"password": c.PostForm("password"),
})
})
r.Run(":9999")
}
+72
View File
@@ -0,0 +1,72 @@
package gee
import (
"encoding/json"
"fmt"
"net/http"
)
type Context struct {
Path string
Method string
Writer http.ResponseWriter
Req *http.Request
Params map[string]string
}
func newContext(w http.ResponseWriter, req *http.Request, params map[string]string) *Context {
return &Context{
Path: req.URL.Path,
Method: req.Method,
Params: make(map[string]string),
Req: req,
Writer: w,
}
}
func (c *Context) Param(key string) string {
value, _ := c.Params[key]
return value
}
func (c *Context) PostForm(key string) string {
return c.Req.FormValue(key)
}
func (c *Context) Query(key string) string {
return c.Req.URL.Query().Get(key)
}
func (c *Context) Status(code int) {
c.Writer.WriteHeader(code)
}
func (c *Context) SetHeader(key string, value string) {
c.Writer.Header().Set(key, value)
}
func (c *Context) String(code int, format string, values ...interface{}) {
c.Status(code)
c.SetHeader("Content-Type", "text/plain")
c.Writer.Write([]byte(fmt.Sprintf(format, values...)))
}
func (c *Context) HTML(code int, html string) {
c.Writer.WriteHeader(code)
c.SetHeader("Content-Type", "text/html")
c.Writer.Write([]byte(html))
}
func (c *Context) JSON(code int, obj interface{}) {
c.Writer.WriteHeader(code)
c.SetHeader("Content-Type", "application/json")
encoder := json.NewEncoder(c.Writer)
if err := encoder.Encode(obj); err != nil {
http.Error(c.Writer, err.Error(), 500)
}
}
func (c *Context) Data(code int, data []byte) {
c.Writer.WriteHeader(code)
c.Writer.Write(data)
}
+63
View File
@@ -0,0 +1,63 @@
package gee
import (
"net/http"
)
// HandlerFunc defines the request handler used by gee
type HandlerFunc func(*Context)
// Engine implement the interface of ServeHTTP
type (
RouterGroup struct {
prefix string
parent *RouterGroup // support nesting
engine *Engine // all groups share a Engine instance
}
Engine struct {
*RouterGroup
router *router
}
)
// New is the constructor of gee.Engine
func New() *Engine {
engine := &Engine{router: newRouter()}
engine.RouterGroup = &RouterGroup{engine: engine}
return engine
}
// Group is defined to create a new RouterGroup
// remember all groups share the same Engine instance
func (group *RouterGroup) Group(prefix string) *RouterGroup {
return &RouterGroup{
prefix: group.prefix + prefix,
parent: group,
engine: group.engine,
}
}
func (group *RouterGroup) addRoute(method string, comp string, handler HandlerFunc) {
pattern := group.prefix + comp
group.engine.router.addRoute(method, pattern, handler)
}
// GET defines the method to add GET request
func (group *RouterGroup) GET(pattern string, handler HandlerFunc) {
group.addRoute("GET", pattern, handler)
}
// POST defines the method to add POST request
func (group *RouterGroup) POST(pattern string, handler HandlerFunc) {
group.addRoute("POST", pattern, handler)
}
// Run defines the method to start a http server
func (engine *Engine) Run(addr string) (err error) {
return http.ListenAndServe(addr, engine)
}
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
engine.router.handle(w, req)
}
+84
View File
@@ -0,0 +1,84 @@
package gee
import (
"net/http"
"strings"
)
type router struct {
roots map[string]*node
handlers map[string]HandlerFunc
}
func newRouter() *router {
return &router{
roots: make(map[string]*node),
handlers: make(map[string]HandlerFunc),
}
}
func (r *router) addRoute(method string, pattern string, handler HandlerFunc) {
parts := filterNonEmpty(strings.Split(pattern, "/"))
key := method + "-" + pattern
_, ok := r.roots[method]
if !ok {
r.roots[method] = &node{}
}
r.roots[method].insert(pattern, parts, 0)
r.handlers[key] = handler
}
func (r *router) handle(w http.ResponseWriter, req *http.Request) {
n, params := r.getRoute(req.Method, req.URL.Path)
c := newContext(w, req, params)
if n != nil {
key := c.Method + "-" + n.pattern
r.handlers[key](c)
} else {
c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
}
}
func (r *router) getRoute(method string, pattern string) (*node, map[string]string) {
searchParts := filterNonEmpty(strings.Split(pattern, "/"))
params := make(map[string]string)
root, ok := r.roots[method]
if !ok {
return nil, nil
}
n := root.search(searchParts, 0)
if n != nil {
parts := filterNonEmpty(strings.Split(n.pattern, "/"))
for index, part := range parts {
if part[0] == ':' {
params[part[1:]] = searchParts[index]
}
}
return n, params
}
return nil, nil
}
func (r *router) getRoutes(method string) []*node {
root, ok := r.roots[method]
if !ok {
return nil
}
nodes := make([]*node, 0)
root.travel(&nodes)
return nodes
}
func filterNonEmpty(vs []string) []string {
parts := make([]string, 0)
for _, item := range vs {
if item != "" {
parts = append(parts, item)
}
}
return parts
}
+47
View File
@@ -0,0 +1,47 @@
package gee
import (
"fmt"
"testing"
)
func newTestRouter() *router {
r := newRouter()
r.addRoute("GET", "/", nil)
r.addRoute("GET", "/hello/:name", nil)
r.addRoute("GET", "/hello/b/c", nil)
r.addRoute("GET", "/hi/:name", nil)
return r
}
func TestGetRoute(t *testing.T) {
r := newTestRouter()
n, ps := r.getRoute("GET", "/hello/geektutu")
if n == nil {
t.Fatal("nil shouldn't be returned")
}
if n.pattern != "/hello/:name" {
t.Fatal("should match /hello/:name")
}
if ps["name"] != "geektutu" {
t.Fatal("name should be equal to 'geektutu'")
}
fmt.Printf("matched path: %s, params['name']: %s\n", n.pattern, ps["name"])
}
func TestGetRoutes(t *testing.T) {
r := newTestRouter()
nodes := r.getRoutes("GET")
for i, n := range nodes {
fmt.Println(i+1, n)
}
if len(nodes) != 4 {
t.Fatal("the number of routes shoule be 4")
}
}
+80
View File
@@ -0,0 +1,80 @@
package gee
import (
"fmt"
)
type node struct {
pattern string
part string
children []*node
isWild bool
}
func (n *node) String() string {
return fmt.Sprintf("node{pattern=%s, part=%s, isWild=%t}", n.pattern, n.part, n.isWild)
}
func (n *node) insert(pattern string, parts []string, height int) {
if len(parts) == height {
n.pattern = pattern
return
}
part := parts[height]
child := n.matchChild(part)
if child == nil {
child = &node{part: part, isWild: part[0] == ':'}
n.children = append(n.children, child)
}
child.insert(pattern, parts, height+1)
}
func (n *node) search(parts []string, height int) *node {
if len(parts) == height {
if n.pattern == "" {
return nil
}
return n
}
part := parts[height]
children := n.matchChildren(part)
for _, child := range children {
result := child.search(parts, height+1)
if result != nil {
return result
}
}
return nil
}
func (n *node) travel(list *([]*node)) {
if n.pattern != "" {
*list = append(*list, n)
}
for _, child := range n.children {
child.travel(list)
}
}
func (n *node) matchChild(part string) *node {
for _, child := range n.children {
if child.part == part || child.isWild {
return child
}
}
return nil
}
func (n *node) matchChildren(part string) []*node {
nodes := make([]*node, 0)
for _, child := range n.children {
if child.part == part || child.isWild {
nodes = append(nodes, child)
}
}
return nodes
}
+62
View File
@@ -0,0 +1,62 @@
package main
// (1) v1
// $ curl -i http://localhost:9999/v1/
// HTTP/1.1 200 OK
// Date: Mon, 12 Aug 2019 18:11:07 GMT
// Content-Length: 18
// Content-Type: text/html; charset=utf-8
// <h1>Hello Gee</h1>
// (2)
// $ curl "http://localhost:9999/v1/hello?name=geektutu"
// hello geektutu, you're at /v1/hello
// (3)
// $ curl "http://localhost:9999/v2/hello/geektutu"
// hello , you're at /v2/hello/geektutu
// (4)
// $ curl "http://localhost:9999/v2/login" -X POST -d 'username=geektutu&password=1234'
// {"password":"1234","username":"geektutu"}
// (5)
// $ curl "http://localhost:9999/hello"
// 404 NOT FOUND: /hello
import (
"net/http"
"./gee"
)
func main() {
r := gee.New()
v1 := r.Group("/v1")
{
v1.GET("/", func(c *gee.Context) {
c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")
})
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")
}
+19 -8
View File
@@ -18,7 +18,7 @@ github: https://github.com/geektutu/7days-gee-golang-web
本文是 [7天用Go从零实现Web框架Gee教程系列](https://geektutu.com/post/gee.html)的第一篇。
- 简单介绍`net/http`库以及`http.Handler`接口。
- 搭建`Gee`框架的雏形,**代码约40行**。
- 搭建`Gee`框架的雏形,**代码约50行**。
## 标准库启动Web服务
@@ -115,11 +115,11 @@ func main() {
}
```
我们定义了一个空的结构体`Engine`,实现了方法`ServeHTTP`。这个方法有2个参数,第二个参数是 _Request_ ,该对象包含了该HTTP请求的所有的信息,比如请求地址、Header和Body等信息;第一个参数是 _ResponseWriter_ ,利用 _ResponseWriter_ 可以构造针对该请求的响应。
- 我们定义了一个空的结构体`Engine`,实现了方法`ServeHTTP`。这个方法有2个参数,第二个参数是 _Request_ ,该对象包含了该HTTP请求的所有的信息,比如请求地址、Header和Body等信息;第一个参数是 _ResponseWriter_ ,利用 _ResponseWriter_ 可以构造针对该请求的响应。
_main_ 函数中,我们给 _ListenAndServe_ 方法的第二个参数传入了刚才创建的`engine`实例。至此,我们走出了实现Web框架的第一步,即,将所有的HTTP请求转向了我们自己的处理逻辑。还记得吗,在实现`Engine`之前,我们调用 _http.HandleFunc_ 实现了路由和Handler的映射,也就是只能针对具体的路由写处理逻辑。比如`/hello`。但是在实现`Engine`之后,我们拦截了所有的HTTP请求,拥有了统一的控制入口。在这里我们可以自由定义路由映射的规则,也可以统一添加一些处理逻辑,例如日志、异常处理等。
- _main_ 函数中,我们给 _ListenAndServe_ 方法的第二个参数传入了刚才创建的`engine`实例。至此,我们走出了实现Web框架的第一步,即,将所有的HTTP请求转向了我们自己的处理逻辑。还记得吗,在实现`Engine`之前,我们调用 _http.HandleFunc_ 实现了路由和Handler的映射,也就是只能针对具体的路由写处理逻辑。比如`/hello`。但是在实现`Engine`之后,我们拦截了所有的HTTP请求,拥有了统一的控制入口。在这里我们可以自由定义路由映射的规则,也可以统一添加一些处理逻辑,例如日志、异常处理等。
代码的运行结果与之前的是一致的。
- 代码的运行结果与之前的是一致的。
## Gee框架的雏形
@@ -190,9 +190,19 @@ func New() *Engine {
return &Engine{router: make(map[string]HandlerFunc)}
}
func (engine *Engine) addRoute(method string, pattern string, handler HandlerFunc) {
key := method + "-" + pattern
engine.router[key] = handler
}
// GET defines the method to add GET request
func (engine *Engine) GET(pattern string, handler HandlerFunc) {
engine.router[pattern] = handler
engine.addRoute("GET", pattern, handler)
}
// POST defines the method to add POST request
func (engine *Engine) POST(pattern string, handler HandlerFunc) {
engine.addRoute("POST", pattern, handler)
}
// Run defines the method to start a http server
@@ -201,7 +211,8 @@ func (engine *Engine) Run(addr string) (err error) {
}
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if handler, ok := engine.router[req.URL.Path]; ok {
key := req.Method + "-" + req.URL.Path
if handler, ok := engine.router[key]; ok {
handler(w, req)
} else {
fmt.Fprintf(w, "404 NOT FOUND: %s\n", req.URL)
@@ -211,9 +222,9 @@ func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
那么`gee.go`就是重头戏了。我们重点介绍一下这部分的实现。
- 首先定义了类型`HandlerFunc`,这是提供给框架用户的,用来定义路由映射的处理方法。我们在`Engine`中,添加了一张路由映射表`router`key 静态路由地址,例如`/``/hello`value 是用户映射的处理方法。
- 首先定义了类型`HandlerFunc`,这是提供给框架用户的,用来定义路由映射的处理方法。我们在`Engine`中,添加了一张路由映射表`router`key 由请求方法和静态路由地址构成,例如`GET-/``GET-/hello``POST-/hello`,这样针对相同的路由,如果请求方法不同,可以映射不同的处理方法(Handler)value 是用户映射的处理方法。
- 当用户调用`(*Engine).GET()`方法时,会将路由和处理方法注册到映射表 _router_ 中,`(*Engine).RUN()`方法,_ListenAndServe_ 的包装。
- 当用户调用`(*Engine).GET()`方法时,会将路由和处理方法注册到映射表 _router_ 中,`(*Engine).Run()`方法,是 _ListenAndServe_ 的包装。
- `Engine`实现的 _ServeHTTP_ 方法的作用就是,解析请求的路径,查找路由映射表,如果查到,就执行注册的处理方法。如果查不到,就返回 _404 NOT FOUND_