web 框架之与http.Func兼容
一下假设我们正在写的web框架叫nice
为了兼容使一些函数能够兼容http.handleFunc,比如
Nice.Get("/hello",func(){
return "hello"
})
在访问127.0.0.1:3000的时候就能看到hello。
实现的方法就是,我们要为每个请求维护一个上下文,然后利用对被兼容函数的反射,自适应相应的handleFunc。
然后再实现http的接口Nice.ServerHTTP(w,r)即可。
好吧,我觉得这句话也没怎么说清楚。先看代码。
首先,怎样才能自适应呢?怎样才能达到变参的效果呢?如果换作别的场景,做起来就很困难了,因为函数的参数变化多端,类型,数量,都是不确定的,虽然反射没办法获得函数的参数名,只能获得参数类型。但是web的场景却比较特殊,因为在web里面,每个handle请求所要用到的参数是有限,并且类型一般是唯一的,比如resPonseWriter和*request,这两个参数。http服务器为每个请求单独开了一个gorutine,我们可以为每个请求再创建一个上下文,用来维护可能用到的参数。
那就创建一个对象用来维护上下文
type context struct {
S *server
//这是一个接口
RW http.ResponseWriter
R *http.Request
handle Handler
}
这个Handler是一个interface{},之所以定义成interface{}是为了能够存下各种handle函数。
s 就是我们的nice对象,存在上下文里面表示一个全局对象。这章的内容与这个对象无关。
对于上下文中的handle,在实际调用中需要用到反射。
func (ct *context) process() {
if ct.handle ==nil{
panic("handle is nil")
}
fmt.Println(ct.R.URL.Path)
t := reflect.TypeOf(ct.handle)
var in = make([]reflect.Value,0,t.NumIn())
for i := 0; i < t.NumIn(); i++ {
argType := t.In(i)
if val, ok := ct.get(argType); ok {
in = append(in, val)
}
}
ct.S.logger.Println(len(in))
rets := reflect.ValueOf(ct.handle).Call(in)
if len(rets) > 0 {
var content []byte
sval := rets[0]
if sval.Kind() == reflect.String {
content = []byte(sval.String())
} else if sval.Kind() == reflect.Slice && sval.Type().Elem().Kind() == reflect.Uint8 {
content = sval.Interface().([]byte)
}
//ct.SetHeader("Content-Length", strconv.Itoa(len(content)), true)
ct.RW.Header().Set("Content-Length", strconv.Itoa(len(content)))
_, err := ct.RW.Write(content)
if err != nil {
ct.S.logger.Println("Error during write: ", err)
}
}
}
这个process的工作就是通过反射把函数的参数类型提取出来,再根据这个类型从上下文里面取需要的参数。因为上下文中的参数的类型都是不同的,所以非常好取。最后把运行结果写入rw里面就可以了。
提取参数的代码
func (ct *context) get(t reflect.Type) (reflect.Value, bool) {
val := reflect.ValueOf(ct)
//这个只要是针对*http.Request的
//如果指针类型的
if val.Kind()==reflect.Ptr{
//就把指向的value取到
val = val.Elem()
}
for i := 0; i < val.NumField(); i++ {
v := val.Field(i)
if v.Type() == t {
return v, true
}
}
panic("Type doesn't exists")
}
这就是整个自适应http.handleFunc的主要代码了。
全部代码如下
package nice
import (
"fmt"
"net/http"
"os"
"reflect"
"strconv"
"regexp"
"log"
)
type Handler interface{}
func doNothing() {
fmt.Println("hello")
}
type context struct {
S *server
//这是一个接口
RW http.ResponseWriter
R *http.Request
handle Handler
}
type route struct {
method string
regex *regexp.Regexp
handle Handler
}
type Config struct {
StaticDir string
Addr string
Port int
CookieSecret string
RecoverPanic bool
Profiler bool
}
type server struct {
Config
//represents global Middlewares
mids []Handler
//represents route dispatch handler
routes []route
//global logger
logger *log.Logger
}
func (ct *context) get(t reflect.Type) (reflect.Value, bool) {
val := reflect.ValueOf(ct)
//如果指针类型的
if val.Kind()==reflect.Ptr{
//就把指向的value取到
val = val.Elem()
}
for i := 0; i < val.NumField(); i++ {
v := val.Field(i)
if v.Type() == t {
return v, true
}
}
panic("Type doesn't exists")
}
func (ct *context) process() {
if ct.handle ==nil{
panic("handle is nil")
}
fmt.Println(ct.R.URL.Path)
t := reflect.TypeOf(ct.handle)
var in = make([]reflect.Value,0,t.NumIn())
for i := 0; i < t.NumIn(); i++ {
argType := t.In(i)
if val, ok := ct.get(argType); ok {
in = append(in, val)
}
}
ct.S.logger.Println(len(in))
//in的参数要是结构体里面可以到处的域,说白了就是要大写
rets := reflect.ValueOf(ct.handle).Call(in)
if len(rets) > 0 {
var content []byte
sval := rets[0]
if sval.Kind() == reflect.String {
content = []byte(sval.String())
} else if sval.Kind() == reflect.Slice && sval.Type().Elem().Kind() == reflect.Uint8 {
content = sval.Interface().([]byte)
}
//ct.SetHeader("Content-Length", strconv.Itoa(len(content)), true)
ct.RW.Header().Set("Content-Length", strconv.Itoa(len(content)))
_, err := ct.RW.Write(content)
if err != nil {
ct.S.logger.Println("Error during write: ", err)
}
}
}
//match url
func (nice *server) matchRoute(req *http.Request) Handler {
for _, route := range nice.routes {
//HEAD can be used as GET
if route.method != req.Method && !(req.Method == "HEAD" && route.method == "GET") {
continue
}
if !route.regex.MatchString(req.URL.Path) {
continue
}
if route.handle != nil {
return route.handle
}
}
return nil
}
//method to create a new context with res and req
func (nice *server) touchContext(res http.ResponseWriter, req *http.Request) *context {
handle := nice.matchRoute(req)
ctx := context{nice, res, req, handle}
return &ctx
}
func (nice *server) ServeHTTP(res http.ResponseWriter, req *http.Request) {
nice.touchContext(res, req).process()
}
func (nice *server) addRoute(pattern string, method string, handler Handler) {
cr, err := regexp.Compile(pattern)
if err != nil {
panic(err)
}
nice.routes = append(nice.routes, route{method, cr, handler})
}
func (nice *server) GET(pattern string,handler Handler) {
nice.addRoute(pattern, "GET", handler)
}
func (nice *server) Run() {
port := os.Getenv("PORT")
if port == "" {
port = "3000"
}
host := os.Getenv("HOST")
nice.logger.Println("listening on " + host + ":" + port)
nice.logger.Fatalln(http.ListenAndServe(host+":"+port, nice))
}
func New() *server {
return &server{logger: log.New(os.Stdout, "", log.Ldate|log.Ltime)}
}
参考了 martini 和 web 两个开源go web框架的代码。
但是martini另外实现了一个injector对象把上下文存在了map里面,这样的好处是可以动态添加上下文为实现中间件埋下伏笔。这里为了实现简单就直接存成了固定的结构体。
2014-4-1补充:
看了一下revel的代码,其实没看懂,是问别人的。revel的兼容方法,把参数的Name和Value存起来供函数的反射调用。问题是我一直没看出来,revel是怎么做到的。原来是通过go 下面的那些包,生成了语法树,去构建的。直接用到了编译的语法了。然后再去生成相应的代码,那段代码在app/tmp/main.go里面。真是应该好好学编译呀。
打个小广告:求一份实习