使用Go語(yǔ)言打造輕量級(jí)Web框架

前言

Web框架是Web開(kāi)發(fā)中不可或缺的組件卖子。它們的主要目標(biāo)是抽象出HTTP請(qǐng)求和響應(yīng)的細(xì)節(jié),使開(kāi)發(fā)人員可以更專注于業(yè)務(wù)邏輯的實(shí)現(xiàn)刑峡。在本篇文章中洋闽,我們將使用Go語(yǔ)言實(shí)現(xiàn)一個(gè)簡(jiǎn)單的Web框架,類似于Gin框架突梦。

功能

我們的Web框架需要實(shí)現(xiàn)以下功能:

  • 路由:處理HTTP請(qǐng)求的路由诫舅,并支持路徑參數(shù)和通配符。
  • 上下文:封裝HTTP請(qǐng)求和響應(yīng)宫患,并提供訪問(wèn)請(qǐng)求參數(shù)的方法刊懈。
  • 中間件:在請(qǐng)求處理之前或之后運(yùn)行的函數(shù)。
  • HTTP請(qǐng)求和響應(yīng):支持GET、POST等HTTP方法虚汛。

實(shí)現(xiàn)

首先匾浪,我們需要定義一個(gè)HandlerFunc類型,表示處理HTTP請(qǐng)求的函數(shù)卷哩。這個(gè)函數(shù)需要接受一個(gè)Context類型的參數(shù)蛋辈,用于訪問(wèn)請(qǐng)求和響應(yīng)。

type HandlerFunc func(Context)

接下來(lái)将谊,我們需要定義一個(gè)Context類型冷溶,封裝HTTP請(qǐng)求和響應(yīng),并提供訪問(wèn)請(qǐng)求參數(shù)的方法尊浓。我們可以使用Go語(yǔ)言標(biāo)準(zhǔn)庫(kù)中的http.ResponseWriterhttp.Request類型分別表示響應(yīng)和請(qǐng)求挂洛。

type Context struct {
Response http.ResponseWriter
Request  *http.Request
Params   map[string]string
}

Params字段用于存儲(chǔ)路徑參數(shù)。例如眠砾,如果路由路徑為/users/:id虏劲,則可以使用c.Params["id"]訪問(wèn)路徑參數(shù)id的值。

現(xiàn)在褒颈,我們可以開(kāi)始實(shí)現(xiàn)路由柒巫。我們需要定義一個(gè)Route類型,表示一個(gè)路由谷丸,包含HTTP方法堡掏、路徑和處理函數(shù)。我們還需要一個(gè)Router類型刨疼,表示整個(gè)應(yīng)用程序的路由器泉唁。它應(yīng)該包含所有的路由,包含需要的中間件揩慕,并能夠處理HTTP請(qǐng)求亭畜。

type Route struct {
    method  string
    path    string
    handler HandlerFunc
}

type Router struct {
    routes []*Route
    middlewares []MiddlewareFunc

}

我們可以使用Handle方法將路由添加到路由器中。

func (r *Router) Handle(method, path string, handler HandlerFunc) {
    r.routes = append(r.routes, &Route{method, path, handler})
}

當(dāng)HTTP請(qǐng)求到達(dá)時(shí)迎卤,需要遍歷所有的路由拴鸵,并找到匹配的路由。如果找到了一個(gè)匹配的路由蜗搔,我們就調(diào)用它的處理函數(shù)劲藐,并且如果有中間件,需要遍歷所有中間件執(zhí)行中間件處理邏輯樟凄。否則聘芜,我們返回HTTP 404錯(cuò)誤。

func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    var match *Route
    params := make(map[string]string)

    for _, route := range r.routes {
        if req.Method == route.method {
            if ok, p := matchPath(route.path, req.URL.Path); ok {
                match = route
                params = p
                break
            }
        }
    }

    if match != nil {
        handler := match.handler

        for i := len(r.middlewares) - 1; i >= 0; i-- {
            handler = r.middlewares[i](handler)
        }
        handler(Context{w, req, params})
    } else {
        http.NotFound(w, req)
    }
}

在上面的代碼中缝龄,我們使用了matchPath函數(shù)來(lái)比較HTTP請(qǐng)求的路徑和路由的路徑汰现,以確定是否匹配挂谍。這個(gè)函數(shù)還會(huì)返回路徑參數(shù)的值,以便我們可以在Context中訪問(wèn)它們服鹅。

現(xiàn)在,我們可以實(shí)現(xiàn)中間件百新。中間件是在請(qǐng)求處理前或處理后運(yùn)行的函數(shù)企软,它們可以修改請(qǐng)求或響應(yīng),或執(zhí)行其他任務(wù)饭望。我們可以定義一個(gè)MiddlewareFunc類型仗哨,表示中間件函數(shù)。

type MiddlewareFunc func(handler HandlerFunc) HandlerFunc

接下來(lái)铅辞,我們可以在Router中添加一個(gè)Use方法厌漂,用于注冊(cè)中間件。這個(gè)方法會(huì)往路由中添加一個(gè)中間件斟珊,后面處理函數(shù)時(shí)候會(huì)要遍歷使用苇倡。

func (r *Router) Use(middleware MiddlewareFunc) {
    r.middlewares = append(r.middlewares, middleware)
}

最后,我們需要添加HTTP方法的支持囤踩。我們可以為每個(gè)HTTP方法定義一個(gè)快捷方法旨椒,它們分別調(diào)用Handle方法并傳遞正確的HTTP方法和路徑。

例如堵漱,對(duì)于GET方法综慎,我們可以定義一個(gè)GET方法,如下所示:

func (r *Router) GET(path string, handler HandlerFunc) {
    r.Handle("GET", path, handler)
}

現(xiàn)在勤庐,我們已經(jīng)完成了一個(gè)簡(jiǎn)單的Web框架的實(shí)現(xiàn)示惊。下面是完整的代碼:

完整的代碼

package main

import (
    "fmt"
    "net/http"
    "strings"
    "time"
)

type HandlerFunc func(Context)

type Context struct {
    Response http.ResponseWriter
    Request  *http.Request
    Params   map[string]string
}

type Route struct {
    method  string
    path    string
    handler HandlerFunc
}

type Router struct {
    routes      []*Route
    middlewares []MiddlewareFunc
}

type MiddlewareFunc func(handler HandlerFunc) HandlerFunc

func NewRouter() *Router {
    return &Router{}
}

func (r *Router) Handle(method, path string, handler HandlerFunc) {
    r.routes = append(r.routes, &Route{method, path, handler})
}

func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    var match *Route
    params := make(map[string]string)

    for _, route := range r.routes {
        if req.Method == route.method {
            if ok, p := matchPath(route.path, req.URL.Path); ok {
                match = route
                params = p
                break
            }
        }
    }

    if match != nil {
        handler := match.handler

        for i := len(r.middlewares) - 1; i >= 0; i-- {
            handler = r.middlewares[i](handler)
        }
        handler(Context{w, req, params})
    } else {
        http.NotFound(w, req)
    }
}

func (r *Router) Use(middleware MiddlewareFunc) {
    r.middlewares = append(r.middlewares, middleware)
}

func (r *Router) GET(path string, handler HandlerFunc) {
    r.Handle("GET", path, handler)
}

func (r *Router) POST(path string, handler HandlerFunc) {
    r.Handle("POST", path, handler)
}

func (r *Router) PUT(path string, handler HandlerFunc) {
    r.Handle("PUT", path, handler)
}

func (r *Router) DELETE(path string, handler HandlerFunc) {
    r.Handle("DELETE", path, handler)
}

func matchPath(path, pattern string) (bool, map[string]string) {
    parts1 := strings.Split(path, "/")
    parts2 := strings.Split(pattern, "/")

    if len(parts1) != len(parts2) {
        return false, nil
    }

    params := make(map[string]string)

    for i, part := range parts1 {
        if part != parts2[i] {
            if strings.HasPrefix(part, ":") {
                params[part[1:]] = parts2[i]
            } else if strings.HasPrefix(part, "*") {
                params[part[1:]] = strings.Join(parts2[i:], "/")
                break
            } else {
                return false, nil
            }
        }
    }

    return true, params
}


使用案例

func main() {

    router := NewRouter()

    router.Use(func(handler HandlerFunc) HandlerFunc {
        return func(ctx Context) {
            start := time.Now()
            handler(ctx)
            fmt.Printf("%s cost %s\n", ctx.Request.RequestURI, time.Now().Sub(start))
        }
    })

    router.GET("/", func(c Context) {
        fmt.Fprintf(c.Response, "歡迎使用我的web框架!")
    })

    router.GET("/users/:id", func(c Context) {
        fmt.Fprintf(c.Response, "User ID: %s", c.Params["id"])
    })

    router.GET("/users/:id/friends", func(c Context) {
        fmt.Fprintf(c.Response, "User ID: %s, list all friends.", c.Params["id"])
    })

    router.GET("/*path", func(c Context) {
        fmt.Fprintf(c.Response, "User path: %s", c.Params["path"])
    })

    http.ListenAndServe(":8080", router)
}

在上面的代碼中,我們添加了一個(gè)中間件函數(shù)記錄請(qǐng)求耗時(shí)愉镰,它用于記錄每個(gè)HTTP請(qǐng)求的執(zhí)行米罚。我們還添加了幾個(gè)路由,以演示路徑參數(shù)和通配符的用法丈探。

運(yùn)行結(jié)果

$ curl localhost:8080/users
User path: users

$ curl localhost:8080/users/1001
User ID: 1001

$ curl localhost:8080/users/1001/friends
User ID: 1001, list all friends.

$ curl localhost:8080/users/1001/friends/xxx
404 page not found

$ curl localhost:8080/xxxx 
User path: xxxx

---------------------------------------------------
/users cost 51.917μs
/users/1001 cost 2.75μs
/users/1001/friends cost 5.833μs
/xxxx cost 6.875μs

總結(jié)

在本文中阔拳,我們使用Go語(yǔ)言實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的Web框架。我們實(shí)現(xiàn)了路由类嗤、上下文糊肠、中間件、HTTP請(qǐng)求和響應(yīng)等功能遗锣。還演示了如何使用路徑參數(shù)和通配符來(lái)匹配不同的路徑货裹。這個(gè)Web框架雖然比不上流行的框架,但它可以作為學(xué)習(xí)Web框架實(shí)現(xiàn)的好起點(diǎn)精偿。


歡迎關(guān)注弧圆,學(xué)習(xí)不迷路

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末赋兵,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子搔预,更是在濱河造成了極大的恐慌霹期,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,978評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件拯田,死亡現(xiàn)場(chǎng)離奇詭異历造,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)船庇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門吭产,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人鸭轮,你說(shuō)我怎么就攤上這事臣淤。” “怎么了窃爷?”我有些...
    開(kāi)封第一講書人閱讀 156,623評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵邑蒋,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我按厘,道長(zhǎng)寺董,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 56,324評(píng)論 1 282
  • 正文 為了忘掉前任刻剥,我火速辦了婚禮遮咖,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘造虏。我一直安慰自己御吞,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布漓藕。 她就那樣靜靜地躺著陶珠,像睡著了一般。 火紅的嫁衣襯著肌膚如雪享钞。 梳的紋絲不亂的頭發(fā)上揍诽,一...
    開(kāi)封第一講書人閱讀 49,741評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死箍铭,一個(gè)胖子當(dāng)著我的面吹牛舱殿,可吹牛的內(nèi)容都是我干的巩螃。 我是一名探鬼主播,決...
    沈念sama閱讀 38,892評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了碟联?” 一聲冷哼從身側(cè)響起妓美,我...
    開(kāi)封第一講書人閱讀 37,655評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎鲤孵,沒(méi)想到半個(gè)月后壶栋,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,104評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡普监,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年贵试,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鹰椒。...
    茶點(diǎn)故事閱讀 38,569評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡锡移,死狀恐怖呕童,靈堂內(nèi)的尸體忽然破棺而出漆际,到底是詐尸還是另有隱情,我是刑警寧澤夺饲,帶...
    沈念sama閱讀 34,254評(píng)論 4 328
  • 正文 年R本政府宣布奸汇,位于F島的核電站,受9級(jí)特大地震影響往声,放射性物質(zhì)發(fā)生泄漏擂找。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評(píng)論 3 312
  • 文/蒙蒙 一浩销、第九天 我趴在偏房一處隱蔽的房頂上張望贯涎。 院中可真熱鬧,春花似錦慢洋、人聲如沸塘雳。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,725評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)败明。三九已至,卻和暖如春太防,著一層夾襖步出監(jiān)牢的瞬間妻顶,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,950評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工蜒车, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留讳嘱,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,260評(píng)論 2 360
  • 正文 我出身青樓酿愧,卻偏偏與公主長(zhǎng)得像呢燥,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子寓娩,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容