分享
  1. 首页
  2. 文章

golang 实现轻量web框架

hutusheng · · 6954 次点击 · · 开始浏览
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

经常看到很多同学在打算使用go做开发的时候会问用什么http框架比较好。其实go的 http package 非常强大,对于一般的 http rest api 开发,完全可以不用框架就可以实现想要的功能。

我们开始尝试用不到100行代码定制出基本的功能框架。

首先思考下基本功能需求:

  1. 输出访问日子,需要知道:
    • Method
    • status code
    • url
    • 响应消耗时间
    • response content-length
  2. 错误捕获,当http请求出现异常时捕获错误,返回异常信息

以上是几个基本需求,未来可能还会有很多,所以应该尝试设计成中间件的形式来应对未来需求的变化,让功能根据需求增减。

我们可以把 http 框架中间件的设计比做洋葱,一层一层的,到最中间就进入业务层,再一层一层的出来。

把流程画出来是这个样子的:

Http:
| LogRequst
| ErrCatch
| Cookie
| Handler
| cookie
| ErrCatch
V LogRequst

调用过程类似于每个中间件逐层包裹,这样的过程很符合函数栈层层调用的过程。

注意:因为这个小框架最终是要被 http.Servehttp.ListenAndServe 调用的,所以需要实现 http.Handler 接口,接收到的参数为 http.ResponseWriter*http.Request

好啦!目标确定了下面我们开始想办法实现它

首先需要定义一个 struct 结构,其中需要保存中间件,和最终要执行的 http.Handler

// MiddlewareFunc filter type
type MiddlewareFunc func(ResponseWriteReader, *http.Request, func())
// MiddlewareServe server struct
type MiddlewareServe struct {
 middlewares []MiddlewareFunc
 Handler http.Handler
}

这里有个问题,因为默认接收到的参数 http.ResponseWriter 接口是一个只能写入不能读取的接口,但我们又需要能读取 status codecontent-length 。这个时候接口设计的神奇之处就体现出来啦,重新定义一个接口且包涵 http.ResponseWriter ,加入读取 status codecontent-length 的功能

// ResponseWriteReader for middleware
type ResponseWriteReader interface {
 StatusCode() int
 ContentLength() int
 http.ResponseWriter
}

定义一个 warp struct 实现 ResponseWriteReader 接口

// WrapResponseWriter implement ResponseWriteReader interface
type WrapResponseWriter struct {
 status int
 length int
 http.ResponseWriter
}
// NewWrapResponseWriter create wrapResponseWriter
func NewWrapResponseWriter(w http.ResponseWriter) *WrapResponseWriter {
 wr := new(WrapResponseWriter)
 wr.ResponseWriter = w
 wr.status = 200
 return wr
}
// WriteHeader write status code
func (p *WrapResponseWriter) WriteHeader(status int) {
 p.status = status
 p.ResponseWriter.WriteHeader(status)
}
func (p *WrapResponseWriter) Write(b []byte) (int, error) {
 n, err := p.ResponseWriter.Write(b)
 p.length += n
 return n, err
}
// StatusCode return status code
func (p *WrapResponseWriter) StatusCode() int {
 return p.status
}
// ContentLength return content length
func (p *WrapResponseWriter) ContentLength() int {
 return p.length
}

接下来,MiddlewareServe 本身需要符合 http.Handler, 所以我们需要定义 ServeHTTP

// ServeHTTP for http.Handler interface
func (p *MiddlewareServe) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 i := 0
 // warp http.ResponseWriter 可以让中间件读取到 status code
 wr := NewWrapResponseWriter(w)
 var next func() // next 函数指针
 next = func() {
 if i < len(p.middlewares) {
 i++
 p.middlewares[i-1](wr, r, next)
 } else if p.Handler != nil {
 p.Handler.ServeHTTP(wr, r)
 }
 }
 next()
}

再加入一个插入中间件的方法

// Use push MiddlewareFunc
func (p *MiddlewareServe) Use(funcs ...MiddlewareFunc) { // 可以一次插入一个或多个
 for _, f := range funcs {
 p.middlewares = append(p.middlewares, f)
 }
}

到这里,一个支持中间件的小框架就定义好了,加上注释一共也不到80行代码

下面开始实现几个中间件测试一下。

// LogRequest print a request status
func LogRequest(w ResponseWriteReader, r *http.Request, next func()) {
 t := time.Now()
 next()
 log.Printf("%v %v %v use time %v content-length %v",
 r.Method,
 w.StatusCode(),
 r.URL.String(),
 time.Now().Sub(t).String(),
 w.ContentLength())
}

这个函数会打印出 http request Method, status code, url, 处理请求消耗时间, response content-length

测试一下

package main
import (
 "fmt"
 "net/http"
)
func helloHandle(w http.ResponseWriter, r *http.Request) {
 fmt.Fprintf(w, " hello ! this's a http request \n method %v \n request url is %v ", r.Method, r.URL.String())
}
func main() {
 // create middleware server
 s := new(MiddlewareServe)
 s.Handler = http.HandlerFunc(helloHandle)
 s.Use(LogRequest)
 // start server
 fmt.Println(http.ListenAndServe(":3000", s))
}

运行

$ go run *.go
$ curl 127.0.0.1:3000
> hello ! this's a http request
> method GET
> request url is
# 输出日志
> 2016年04月24日 02:28:12 GET 200 / use time 61.717μs content-length 64
$ curl 127.0.0.1:3000/hello/go
> hello ! this's a http request
> method GET
> request url is /hello/go
# 输出日志
> 2016年04月24日 02:31:36 GET 200 /hello/go use time 28.207μs content-length 72

或者用浏览器请求地址查看效果

再加一个错误捕获中间件:

// ErrCatch catch and recover
func ErrCatch(w ResponseWriteReader, r *http.Request, next func()) {
 defer func() {
 if err := recover(); err != nil {
 fmt.Println(err)
 debug.PrintStack()
 w.WriteHeader(http.StatusInternalServerError) // 500
 }
 }()
 next()
}

测试

package main
import (
 "fmt"
 "net/http"
)
func helloHandle(w http.ResponseWriter, r *http.Request) {
 fmt.Fprintf(w, " hello ! this's a http request \n method %v \n request url is %v \n", r.Method, r.URL.String())
}
func panicHandle(w http.ResponseWriter, r *http.Request) {
 panic("help me !")
}
func main() {
 // create middleware server
 s := new(MiddlewareServe)
 route := http.NewServeMux()
 route.Handle("/hello", http.HandlerFunc(helloHandle))
 route.Handle("/panic", http.HandlerFunc(panicHandle))
 s.Handler = route
 s.Use(LogRequest, ErrCatch)
 // start server
 fmt.Println(http.ListenAndServe(":3000", s))
}

运行

$ curl -i 127.0.0.1:3000/panic
> HTTP/1.1 500 Internal Server Error
> Date: 2016年4月23日 18:51:12 GMT
> Content-Length: 0
> Content-Type: text/plain; charset=utf-8
# log
> help me !
> ... # debug.Stack
> 2016年04月24日 02:51:12 GET 500 /panic use time 142.885μs content-length 0
$ curl -i 127.0.0.1:3000/hello/go
> HTTP/1.1 404 Not Found
> Content-Type: text/plain; charset=utf-8
> X-Content-Type-Options: nosniff
> Date: 2016年4月23日 18:55:30 GMT
> Content-Length: 19
>
> 404 page not found
# log
2016年04月24日 02:55:30 GET 404 /hello/go use time 41.14μs content-length 19

到这里,一个灵活的核心就实现出来了。我尽量只使用标准包,没有引入第三方包,希望这样可能帮助刚学习go的同学更加了解 net.http package,当然在真实使用中可以根据需要引入其他的符合 http.Handler 接口的 router 替代 ServeMux

有些同学可能已经看出来了,既然 MiddlewareServe 实现了 http.Handler 那就可以挂到 router 中。没错,这样就相当于可以为某个路径下单独定制 MiddlewareServe 了,比如某些接口需要权限校验,我会在下一篇中来尝试写权限校验。

涉及模版的调用我就不写了,因为这个确实是脚本语言更佳适合

全部代码的github地址: https://github.com/ifanfan/golearn/tree/master/websrv

自己写的第一篇博客希望以后可以坚持写下去


有疑问加站长微信联系(非本文作者)

本文来自:博客园

感谢作者:hutusheng

查看原文:golang 实现轻量web框架

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

关注微信
6954 次点击
暂无回复
添加一条新回复 (您需要 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传

用户登录

没有账号?注册
(追記) (追記ここまで)

今日阅读排行

    加载中
(追記) (追記ここまで)

一周阅读排行

    加载中

关注我

  • 扫码关注领全套学习资料 关注微信公众号
  • 加入 QQ 群:
    • 192706294(已满)
    • 731990104(已满)
    • 798786647(已满)
    • 729884609(已满)
    • 977810755(已满)
    • 815126783(已满)
    • 812540095(已满)
    • 1006366459(已满)
    • 692541889

  • 关注微信公众号
  • 加入微信群:liuxiaoyan-s,备注入群
  • 也欢迎加入知识星球 Go粉丝们(免费)

给该专栏投稿 写篇新文章

每篇文章有总共有 5 次投稿机会

收入到我管理的专栏 新建专栏