간단한 Web Framework 구현하기 (7) - Renderer (XML/JSON)

|

Renderer XML/JSON

context.go

package main

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

type Context struct {
	Params map[string]interface{}

	ResponseWriter http.ResponseWriter
	Request        *http.Request
}

type HandlerFunc func(*Context)

func (c *Context) RenderError(code int, err error) {
	if err != nil {
		errorCode := http.StatusInternalServerError

		if code > 0 {
			errorCode = code
		}

		http.Error(c.ResponseWriter, http.StatusText(errorCode), errorCode)
	}
}

func (c *Context) RenderJson(v interface{}) {
	c.ResponseWriter.WriteHeader(http.StatusOK)
	c.ResponseWriter.Header().Set("Content-Type", "application/json; charset=utf-8")

	if err := json.NewEncoder(c.ResponseWriter).Encode(v); err != nil {
		c.RenderError(http.StatusInternalServerError, err)
	}
}

func (c *Context) RenderXml(v interface{}) {
	c.ResponseWriter.WriteHeader(http.StatusOK)
	c.ResponseWriter.Header().Set("Content-Type", "application/json; charset=utf-8")

	if err := xml.NewEncoder(c.ResponseWriter).Encode(v); err != nil {
		c.RenderError(http.StatusInternalServerError, err)
	}
}


main.go

package main

import (
	"fmt"
)

type User struct {
	Id   string
	Name string
}

func main() {
	fmt.Println("Simple Web Framework")

	s := NewServer()

	s.HandleFunc("GET", "/", func(c *Context) {
		fmt.Fprintln(c.ResponseWriter, "This is an index page.")
	})

	s.HandleFunc("GET", "/about", func(c *Context) {
		fmt.Fprintln(c.ResponseWriter, "This is an about page.")
	})

	s.HandleFunc("GET", "/users/:id", func(c *Context) {
		u := User{Id: c.Params["id"].(string)}

		c.RenderXml(u)
	})

	s.HandleFunc("GET", "/users/:id/name/:name", func(c *Context) {
		u := User{Id: c.Params["id"].(string),
			Name: c.Params["name"].(string),
		}

		c.RenderJson(u)
	})

	s.HandleFunc("POST", "/users", func(c *Context) {
		fmt.Fprintln(c.ResponseWriter, "Create user")
	})

	s.Run(":8080")
}

간단한 Web Framework 구현하기 (6) - Abstraction

|

추상화

라우터를 직접 생성하는 것보다 생성자 함수로 감싸는 것이 좀 더 안전한 사용법이며, 미들웨어도 대부분의 핸들러에 적용이 되어야 하기 때문에 미들웨어 체인 형태로 사용하는 것이 더 간편할 수 있습니다.

Server라는 패키지를 만들어서 그 안에서 라우터 생성 및 미들웨어 체인을 사용하도록 한 코드입니다.

server.go

package main

import "net/http"

type Server struct {
	*router
	middlewares  []Middleware
	startHandler HandlerFunc
}

func NewServer() *Server {
	r := &router{make(map[string]map[string]HandlerFunc)}
	s := &Server{router: r}
	s.middlewares = []Middleware{
		logHandler,
		recoverHandler,
		staticHandler,
		parseFormHandler,
		parseJsonBodyHandler,
	}

	return s
}

func (s *Server) Run(addr string) {
	s.startHandler = s.router.handler()

	for i := len(s.middlewares) - 1; i >= 0; i-- {
		s.startHandler = s.middlewares[i](s.startHandler)
	}

	if err := http.ListenAndServe(addr, s); err != nil {
		panic(err)
	}
}

func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	c := &Context{
		Params:         make(map[string]interface{}),
		ResponseWriter: w,
		Request:        req,
	}

	for k, v := range req.URL.Query() {
		c.Params[k] = v[0]
	}

	s.startHandler(c)
}


router.go

package main

import (
	"net/http"
	"strings"
)

type router struct {
	handlers map[string]map[string]HandlerFunc
}

func (r *router) handler() HandlerFunc {
	return func(c *Context) {
		for pattern, handler := range r.handlers[c.Request.Method] {
			if ok, params := match(pattern, c.Request.URL.Path); ok {

				for k, v := range params {
					c.Params[k] = v
				}

				handler(c)
				return
			}
		}

		http.NotFound(c.ResponseWriter, c.Request)
	}
}

func (r *router) HandleFunc(method, pattern string, h HandlerFunc) {
	m, ok := r.handlers[method]

	if !ok {
		m = make(map[string]HandlerFunc)
		r.handlers[method] = m
	}
	m[pattern] = h
}

func match(pattern, path string) (bool, map[string]string) {
	if pattern == path {
		return true, nil
	}

	patterns := strings.Split(pattern, "/")
	paths := strings.Split(path, "/")

	if len(patterns) != len(paths) {
		return false, nil
	}

	params := make(map[string]string)

	for i := 0; i < len(patterns); i++ {
		switch {
		case patterns[i] == paths[i]:
		case len(patterns[i]) > 0 && patterns[i][0] == ':':
			params[patterns[i][1:]] = paths[i]
		default:
			return false, nil
		}
	}

	return true, params
}


main.go

package main

import (
	"fmt"
)

func main() {
	fmt.Println("Simple Web Framework")

	s := NewServer()

	s.HandleFunc("GET", "/", func(c *Context) {
		fmt.Fprintln(c.ResponseWriter, "This is an index page.")
	})

	s.HandleFunc("GET", "/about", func(c *Context) {
		fmt.Fprintln(c.ResponseWriter, "This is an about page.")
	})

	s.HandleFunc("GET", "/users/:id", func(c *Context) {
		userId := c.Params["id"]

		fmt.Fprintln(c.ResponseWriter, "Retrieve user: ", userId)

		if userId == "-1" {
			panic("id is not valid..")
		}

	})

	s.HandleFunc("POST", "/users", func(c *Context) {
		fmt.Fprintln(c.ResponseWriter, "Create user")
	})

	s.Run(":8080")
}

간단한 Web Framework 구현하기 (4) - Middleware

|

Middleware

로그를 남기거나 에러 처리(500 Internal Server Error 등)를 하는 미들웨어는 다음과 같이 구현할 수 있습니다.

middleware.go

package main

import (
	"time"
	"log"
	"net/http"
)

type Middleware func(next HandlerFunc) HandlerFunc

func logHandler(next HandlerFunc) HandlerFunc {
	return func(c *Context) {
		t := time.Now()

		next(c)

		log.Printf("[%s] %q %v\n",
			c.Request.Method,
			c.Request.URL.String(),
			time.Now().Sub(t))
	}
}

func recoverHandler(next HandlerFunc) HandlerFunc {
	return func(c *Context) {
		defer func() {
			if err := recover(); err != nil {
				log.Printf("panic: %+v", err)
				http.Error(c.ReponseWriter,
					http.StatusText(http.StatusInternalServerError),
					http.StatusInternalServerError)
			}
		}()
		next(c)
	}
}


이를 호출하는 부분을 다음과 같이 수정하면, 미들웨어를 적용할 수 있습니다.

main.go

...

r.HandleFunc("GET", "/users/:id", logHandler(recoverHandler(func(c *Context) {
		userId := c.Params["id"]

		fmt.Fprintln(c.ReponseWriter, "Retrieve user: ", userId)

		if userId == "-1" {
			panic("id is not valid..")
		}

	})))

...


json Parsing 미들웨어

그 외에도 Post 메시지 내용이나 json 파싱을 하는 미들웨어 코드는 다음과 같습니다.

func parseFormHandler(next HandlerFunc) HandlerFunc {
	return func(c *Context) {
		fmt.Println("HandlerFunc is called.")

		c.Request.ParseForm()
		fmt.Println(c.Request.PostForm)

		for k, v := range c.Request.PostForm {
			if len(v) > 0 {
				c.Params[k] = v[0]
			}
		}

		next(c)
	}
}

func parseJsonBodyHandler(next HandlerFunc) HandlerFunc {
	return func(c *Context) {
		fmt.Println("parseJsonBodyHandler is called.")

		var m map[string]interface{}

		if json.NewDecoder(c.Request.Body).Decode(&m); len(m) > 0 {
			for k, v := range m {
				c.Params[k] = v
			}
		}

		next(c)
	}
}


정적 파일 처리를 위한 미들웨어

func staticHandler(next HandlerFunc) HandlerFunc {
	var (
		dir       = http.Dir(".")
		indexFile = "index.html"
	)
	
	return func(c *Context) {
		if c.Request.Method != "GET" && c.Request.Method != "HEAD" {
			next(c)
			return
		}

		file := c.Request.URL.Path
		f, err := dir.Open(file)
		if err != nil {
			next(c)
			return
		}
		defer f.Close()

		fi, err := f.Stat()
		if err != nil {
			next(c)
			return
		}

		if fi.IsDir() {
			if !strings.HasSuffix(c.Request.URL.Path, "/") {
				http.Redirect(c.ResponseWriter, c.Request, c.Request.URL.Path+"/", http.StatusFound)
				return
			}

			file = path.Join(file, indexFile)

			f, err = dir.Open(file)
			if err != nil {
				next(c)
				return
			}
			defer f.Close()

			fi, err = f.Stat()
			if err != nil || fi.IsDir() {
				next(c)
				return
			}
		}

		http.ServeContent(c.ResponseWriter, c.Request, file, fi.ModTime(), f)
	}
}

간단한 Web Framework 구현하기 (4) - Context

|

Context

Context는 사용자로부터 URL 패턴을 받았을 때, 해당 매개변수 및 Request, Repose 등을 저장할 수 있는 구조체입니다.

여기서는 다음과 같이 정의했습니다.

context.go

package main

import "net/http"

type Context struct {
	Params map[string]interface{}

	ResponseWriter http.ResponseWriter
	Request        *http.Request
}

type HandlerFunc func(*Context)


router.go

위에서 만든 Context 구조체를 이용해서 기존의 http.ResponseWriterhttp.Request를 저장할 수 있습니다. 또한 이 구조체를 매개변수로 하는 HandlerFunc라는 인터페이스를 정의했기 때문에 기존의 http.HandlerFunc() 함수 호출 부분을 수정해줍니다.

package main

import (
	"net/http"
	"strings"
)

type router struct {
	handlers map[string]map[string]HandlerFunc
}

func (r *router) HandleFunc(method, pattern string, h HandlerFunc) {
	m, ok := r.handlers[method]

	if !ok {
		m = make(map[string]HandlerFunc)
		r.handlers[method] = m
	}
	m[pattern] = h
}

func (r *router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	for pattern, handler := range r.handlers[req.Method] {
		if ok, params := match(pattern, req.URL.Path); ok {
			// Create Context

			c := Context{
				Params:         make(map[string]interface{}),
				ResponseWriter: w,
				Request:        req,
			}

			for k, v := range params {
				c.Params[k] = v
			}

			handler(&c)
			return
		}
	}

	http.NotFound(w, req)
}

func match(pattern, path string) (bool, map[string]string) {
	if pattern == path {
		return true, nil
	}

	patterns := strings.Split(pattern, "/")
	paths := strings.Split(path, "/")

	if len(patterns) != len(paths) {
		return false, nil
	}

	params := make(map[string]string)

	for i := 0; i < len(patterns); i++ {
		switch {
		case patterns[i] == paths[i]:
		case len(patterns[i]) > 0 && patterns[i][0] == ':':
			params[patterns[i][1:]] = paths[i]
		default:
			return false, nil
		}
	}

	return true, params
}


main.go

package main

import (
	"fmt"
	"net/http"
)

func main() {
	fmt.Println("Simple Web Framework")

	r := &router{make(map[string]map[string]HandlerFunc)}

	r.HandleFunc("GET", "/", func(c *Context) {
		fmt.Fprintln(c.ResponseWriter, "This is an index page.")
	})

	r.HandleFunc("GET", "/about", func(c *Context) {
		fmt.Fprintln(c.ResponseWriter, "This is an about page.")
	})

	r.HandleFunc("GET", "/users/:id", func(c *Context) {
		fmt.Fprintln(c.ResponseWriter, "Retrieve user: ", c.Params["id"])
	})

	r.HandleFunc("POST", "/users", func(c *Context) {
		fmt.Fprintln(c.ResponseWriter, "Create user")
	})

	http.ListenAndServe(":8080", r)
}

간단한 Web Framework 구현하기 (3) - Router

|

Router 추가

사용자로부터 요청받은 Request Method(GET 또는 POST 등) 및 URL에 따라 각각을 처리할 수 있는 모듈인 Router를 구현해봅니다.

main.go

package main

import (
	"fmt"
	"net/http"
)

func main() {
	fmt.Println("Simple Web Framework")

	r := &router{make(map[string]map[string]http.HandlerFunc)}

	r.HandleFunc("GET", "/", func(w http.ResponseWriter, req *http.Request) {
		fmt.Fprintln(w, "This is an index page.")
	})

	r.HandleFunc("GET", "/about", func(w http.ResponseWriter, req *http.Request) {
		fmt.Fprintln(w, "This is an about page.")
	})

	http.ListenAndServe(":8080", r)
}


router.go

package main

import (
	"net/http"
)

type router struct {
	handlers map[string]map[string]http.HandlerFunc
}

func (r *router) HandleFunc(method, pattern string, handler http.HandlerFunc) {
	m, ok := r.handlers[method]

	if !ok {
		m = make(map[string]http.HandlerFunc)
		r.handlers[method] = m
	}
	m[pattern] = handler
}

func (r *router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	if m, ok := r.handlers[req.Method]; ok {
		if handler, ok := m[req.URL.Path]; ok {
			handler(w, req)
			return
		}
	}

	http.NotFound(w, req)
}


router에 정규식 적용

main.go

package main

import (
	"fmt"
	"net/http"
)

func main() {
	fmt.Println("Simple Web Framework")

	r := &router{make(map[string]map[string]http.HandlerFunc)}

	r.HandleFunc("GET", "/", func(w http.ResponseWriter, req *http.Request) {
		fmt.Fprintln(w, "This is an index page.")
	})

	r.HandleFunc("GET", "/about", func(w http.ResponseWriter, req *http.Request) {
		fmt.Fprintln(w, "This is an about page.")
	})

	r.HandleFunc("GET", "/users/:id", func(w http.ResponseWriter, req *http.Request) {
		fmt.Fprintln(w, "Retrieve user")
	})

	r.HandleFunc("POST", "/users", func(w http.ResponseWriter, req *http.Request) {
		fmt.Fprintln(w, "Create user")
	})

	http.ListenAndServe(":8080", r)
}


router.go

package main

import (
	"net/http"
	"strings"
)

type router struct {
	handlers map[string]map[string]http.HandlerFunc
}

func (r *router) HandleFunc(method, pattern string, h http.HandlerFunc) {
	m, ok := r.handlers[method]

	if !ok {
		m = make(map[string]http.HandlerFunc)
		r.handlers[method] = m
	}
	m[pattern] = h
}

func (r *router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	for pattern, handler := range r.handlers[req.Method] {
		if ok, _ := match(pattern, req.URL.Path); ok {
			handler(w, req)
			return
		}
	}

	http.NotFound(w, req)
}

func match(pattern, path string) (bool, map[string]string) {
	if pattern == path {
		return true, nil
	}

	patterns := strings.Split(pattern, "/")
	paths := strings.Split(path, "/")

	if len(patterns) != len(paths) {
		return false, nil
	}

	params := make(map[string]string)

	for i := 0; i < len(patterns); i++ {
		switch {
		case patterns[i] == paths[i]:
		case len(patterns[i]) > 0 && patterns[i][0] == ':':
			params[patterns[i][1:]] = paths[i]
		default:
			return false, nil
		}
	}

	return true, params
}