golang 實現輕量web框架


經常看到很多同學在打算使用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: Sat, 23 Apr 2016 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: 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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM