本教程参考:七天用Go从零实现Web框架Gee教程
本文主要看点;
使用效果:
func main() {
r := geo.New()
r.GET("/", func(c *geo.Context) {
c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")
})
r.GET("/hello", func(c *geo.Context) {
c.String(http.StatusOK, "hello %s, you're at %s\n", c.Query("name"), c.Path)
})
r.POST("/login", func(c *geo.Context) {
c.JSON(http.StatusOK, geo.H{
"username": c.PostForm("username"),
"password": c.PostForm("password"),
})
})
r.Run(":9999")
}
必要性:
用返回 JSON 数据作比较,感受下封装前后的差距。
封装前:
obj = map[string]interface{}{
"name": "dhy",
"password": "1234",
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
encoder := json.NewEncoder(w)
if err := encoder.Encode(obj); err != nil {
http.Error(w, err.Error(), 500)
}
VS 封装后:
c.JSON(http.StatusOK, geo.H{
"username": c.PostForm("username"),
"password": c.PostForm("password"),
})
具体实现:
package geo
import (
"encoding/json"
"fmt"
"net/http"
)
type H map[string]interface{}
//Context 内部维护当前请求的一系列信息
type Context struct {
//req和res
Writer http.ResponseWriter
Req *http.Request
//关于请求的相关信息
Path string
Method string
//关于响应相关信息
StatusCode int
}
func newContext(w http.ResponseWriter, req *http.Request) *Context {
return &Context{
Writer: w,
Req: req,
Path: req.URL.Path,
Method: req.Method,
}
}
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)
}
//Status 设置响应状态码
func (c *Context) Status(code int) {
c.StatusCode = code
//如果不手动调用WriteHeader设置响应状态码,那么默认会调用WriteHeader(http.StatusOK)
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.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.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)
}
}
func (c *Context) Data(code int, data []byte) {
c.Status(code)
c.Writer.Write(data)
}
func (c *Context) HTML(code int, html string) {
c.SetHeader("Content-Type", "text/html")
c.Status(code)
c.Writer.Write([]byte(html))
}
我们将和路由相关的方法和结构提取了出来,放到了一个新的文件中router.go,方便我们下一次对 router 的功能进行增强,例如提供动态路由的支持。 router 的 handle 方法作了一个细微的调整,即 handler 的参数,变成了 Context。
路由Router的核心方法就是:
package geo
import (
"log"
"net/http"
)
type router struct {
//路由映射表
handlers map[string]HandlerFunc
}
func newRouter() *router {
return &router{handlers: make(map[string]HandlerFunc)}
}
//addRoute 增加路由,目前就是增加一个路由映射
func (r *router) addRoute(method string, pattern string, handler HandlerFunc) {
log.Printf("Route %4s - %s", method, pattern)
key := method + "-" + pattern
r.handlers[key] = handler
}
//handle 从映射表中获取处理器,进行处理
func (r *router) handle(c *Context) {
key := c.Method + "-" + c.Path
if handler, ok := r.handlers[key]; ok {
//处理器函数入口参数变成了Context
handler(c)
} else {
c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
}
}
package geo
import (
"net/http"
)
type HandlerFunc func(*Context)
type Engine struct {
//路由表
router *router
}
func New() *Engine {
return &Engine{router: newRouter()}
}
//路由的添加
func (engine *Engine) addRoute(method string, pattern string, handler HandlerFunc) {
engine.router.addRoute(method, pattern, handler)
}
func (engine *Engine) GET(pattern string, handler HandlerFunc) {
engine.addRoute("GET", pattern, handler)
}
func (engine *Engine) POST(pattern string, handler HandlerFunc) {
engine.addRoute("POST", pattern, handler)
}
// 服务器启动
func (engine *Engine) Run(addr string) (err error) {
return http.ListenAndServe(addr, engine)
}
// 处理请求---请求统一派发的入口
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
//为当前请求构建上下文环境,然后去路由表中查询出对应的处理器进行处理
c := newContext(w, req)
engine.router.handle(c)
}
将router相关的代码独立后,geo.go简单了不少。最重要的还是通过实现了 ServeHTTP 接口,接管了所有的 HTTP 请求。相比第一天的代码,这个方法也有细微的调整,在调用 router.handle 之前,构造了一个 Context 对象。这个对象目前还非常简单,仅仅是包装了原来的两个参数,之后我们会慢慢地给Context插上翅膀。
如何使用,main.go一开始就已经亮相了。运行go run main.go,借助 curl ,一起看一看今天的成果吧。
$ 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>
$ curl "http://localhost:9999/hello?name=dhy"
hello dhy, you're at /hello
$ curl "http://localhost:9999/login" -X POST -d 'username=dhy&password=1234'
{"password":"1234","username":"dhy"}
$ curl "http://localhost:9999/xxx"
404 NOT FOUND: /xxx
func (c *Context) JSON(code int, obj interface{}) {
c.SetHeader("Content-Type", "application/json")
c.Status(code)
// c.StatusCode = code
// c.Writer.WriteHeader(code)
encoder := json.NewEncoder(c.Writer)
if err := encoder.Encode(obj); err != nil {
http.Error(c.Writer, err.Error(), 500)
// w.Header().Set("Content-Type", "text/plain; charset=utf-8")
// w.Header().Set("X-Content-Type-Options", "nosniff")
// w.WriteHeader(code)
// fmt.Fprintln(w, error)
}
}
在 WriteHeader() 后调用 Header().Set 是不会生效的
如果err!=nil的话http.Error(c.Writer, err.Error(), 500)这里是不起作用的,因为前面已经执行了WriteHeader(code),那么返回码将不会再更改http.Error(c.Writer, err.Error(), 500)里面的w.WriteHeader(code)、w.Header().Set()不起作用,而且encoder.Encode(obj)相当于调用了Write(),http.Error(c.Writer, err.Error(), 500)里面的WriteHeader、Header().Set()操作都是无效的。我看了gin的代码,如果encoder.Encode(obj)这里报错的话是直接panic,感觉这里如果err!=nil的话确实不好处理
附上gin的代码
render/json.go 56行
// Render (JSON) writes data with custom ContentType.
func (r JSON) Render(w http.ResponseWriter) (err error) {
if err = WriteJSON(w, r.Data); err != nil {
panic(err)
}
return
}
// WriteContentType (JSON) writes JSON ContentType.
func (r JSON) WriteContentType(w http.ResponseWriter) {
writeContentType(w, jsonContentType)
}
// WriteJSON marshals the given interface object and writes it with custom ContentType.
func WriteJSON(w http.ResponseWriter, obj interface{}) error {
writeContentType(w, jsonContentType)
encoder := json.NewEncoder(w)
err := encoder.Encode(&obj)
return err
}