设为首页 收藏本站
查看: 2020|回复: 0

[经验分享] golang 实现轻量web框架

[复制链接]

尚未签到

发表于 2018-9-20 06:16:18 | 显示全部楼层 |阅读模式
  经常看到很多同学在打算使用go做开发的时候会问用什么http框架比较好。其实go的 http package 非常强大,对于一般的 http rest api 开发,完全可以不用框架就可以实现想要的功能。
  我们开始尝试用不到100行代码定制出基本的功能框架。
  首先思考下基本功能需求:


  • 输出访问日子,需要知道:

    • Method
    • status code
    • url
    • 响应消耗时间
    • response content-length

  • 错误捕获,当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(&quot;%v %v %v use time %v content-length %v&quot;,
  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 (
  &quot;fmt&quot;
  &quot;net/http&quot;
  
)
  

  
func helloHandle(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, &quot; hello ! this's a http request \n method %v \n request url is %v &quot;, 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(&quot;:3000&quot;, 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
  

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


  • http://127.0.0.1:3000
  • http://127.0.0.1:3000/hello/go
  再加一个错误捕获中间件:
  

// 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 (
  &quot;fmt&quot;
  &quot;net/http&quot;
  
)
  

  
func helloHandle(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, &quot; hello ! this's a http request \n method %v \n request url is %v \n&quot;, r.Method, r.URL.String())
  
}
  

  
func panicHandle(w http.ResponseWriter, r *http.Request) {
  panic(&quot;help me !&quot;)
  
}
  

  
func main() {
  // create middleware server
  s := new(MiddlewareServe)
  route := http.NewServeMux()
  

  route.Handle(&quot;/hello&quot;, http.HandlerFunc(helloHandle))
  route.Handle(&quot;/panic&quot;, http.HandlerFunc(panicHandle))
  

  s.Handler = route
  s.Use(LogRequest, ErrCatch)
  // start server
  fmt.Println(http.ListenAndServe(&quot;:3000&quot;, s))
  
}
  

  运行
  

$ curl -i 127.0.0.1:3000/panic  
> HTTP/1.1 500 Internal Server Error
  
> Date: Sat, 23 Apr 2016 18:51:12 GMT
  
> Content-Length: 0
  
> Content-Type: text/plain;
  

  
# 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: Sat, 23 Apr 2016 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
  自己写的第一篇博客希望以后可以坚持写下去



运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-596693-1-1.html 上篇帖子: golang调用动态库 下篇帖子: golang几种post方式
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表