使用go搭建簡單的http服務(wù)及源碼解析

創(chuàng)建一個簡單的http服務(wù)

使用go語言搭建一個http服務(wù)事件很簡單的事情馋吗,如果代碼邏輯很簡單的話甚至不用框架就行。話不多說,直接看代碼吧限匣。

package main

import (
    "fmt"
    "net/http"
)

func echo(rw http.ResponseWriter, r *http.Request) {
    var rspText string
    params := r.URL.Query() // 獲取查詢參數(shù)
    values, ok := params["name"]
    if ok && len(values) > 0 {
        rspText = fmt.Sprintf("hello, %s!", values[0])
    } else {
        rspText = "hello, boy!"
    }
    rw.Header().Add("Content-Type", "text/plain") // 添加header讥珍,值得注意的是历极,header必須在Write之前調(diào)用,否則不會生效
    rw.WriteHeader(http.StatusOK)                 // 這里可以省略
    rw.Write([]byte(rspText))
}

func main() {
    http.HandleFunc("/echo", echo) // 注冊路由以及回調(diào)函數(shù)
    // 監(jiān)聽ip和端口衷佃,第二個參數(shù)為nil趟卸,則使用默認(rèn)的DefaultServeMux來處理請求
    if err := http.ListenAndServe("127.0.0.1:10001", nil); err != nil {
        panic(err)
    }
}

運(yùn)行以及返回結(jié)果

~ http :10001/echo name==coolboy -v
GET /echo?name=coolboy HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Host: localhost:10001
User-Agent: HTTPie/2.0.0


HTTP/1.1 200 OK
Content-Length: 15
Content-Type: text/plain
Date: Wed, 19 Jan 2022 08:00:36 GMT

hello, coolboy!

可看出go語言創(chuàng)建http服務(wù)最簡單只需要兩步

  1. 使用http.HandleFunc綁定路由以及路由對應(yīng)的回調(diào)函數(shù)
  2. 使用http.ListenAndServe指定監(jiān)聽端口,并啟動服務(wù)

下面我們第一步的去分析這兩個函數(shù)氏义。

了解http.HandleFunc

首先我們先看下源碼锄列,go語言的源碼注釋還是可以的,借助源碼我們可以方便正確的了解該函數(shù)的作用

// file: net/http/server.go  
var defaultServeMux ServeMux

// DefaultServeMux is the default ServeMux used by Serve.
var DefaultServeMux = &defaultServeMux

// HandleFunc registers the handler function for the given pattern
// in the DefaultServeMux.
// The documentation for ServeMux explains how patterns are matched.
// 這里可以看出惯悠,實(shí)際上是調(diào)用了DefaultServeMux.HandleFunc
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    DefaultServeMux.HandleFunc(pattern, handler)
}

首先看參數(shù)類型邻邮,第一個是路由地址,沒啥好說的克婶。不過注釋提示了饶囚,如果想進(jìn)一步了解路由的匹配規(guī)則帕翻,可以查看ServeMux的文檔。第二個參數(shù)也限定了回調(diào)函數(shù)的類型萝风。下面我們具體查看下回調(diào)函數(shù)的這兩個參數(shù)類型嘀掸。
首先是ResponseWriter,它其實(shí)是一個接口類型规惰〔撬看起來也是簡單且清晰的,就是三件事三個接口

  1. 寫入http response status狀態(tài)碼
  2. 寫入http response header
  3. 寫入http response body(這里使用byte類型歇万,即支持任意數(shù)據(jù)類型)
type Header map[string][]string

// 注意揩晴,源碼注釋比較詳細(xì),但是字?jǐn)?shù)太多就不放這里了贪磺,推薦去看看的
type ResponseWriter interface {
    // 返回一個header字典硫兰,調(diào)用WriteHeader函數(shù)時,將會返回該map數(shù)據(jù)寒锚。但是對header的
    // 修改必須在調(diào)用WriteHeader前劫映,否則不會生效
    Header() Header
    // 寫數(shù)據(jù)到http響應(yīng)里的body中
    // 調(diào)用該函數(shù)之前,如果沒調(diào)用WriteHeader則會先調(diào)用一次
    Write([]byte) (int, error)
    // 先http相應(yīng)寫入statusCode以及header內(nèi)容
    WriteHeader(statusCode int)
}

下面再來看看Request刹前,它是一個結(jié)構(gòu)體泳赋,而不像ResponseWriter是一個接口類型。

type URL struct {
    Scheme      string
    Opaque      string    // encoded opaque data
    User        *Userinfo // username and password information
    Host        string    // host or host:port
    Path        string    // path (relative paths may omit leading slash)
    RawPath     string    // encoded path hint (see EscapedPath method)
    ForceQuery  bool      // append a query ('?') even if RawQuery is empty
    RawQuery    string    // encoded query values, without '?'
    Fragment    string    // fragment for references, without '#'
    RawFragment string    // encoded fragment hint (see EscapedFragment method)
}

type Request struct {
    // http請求方法:如GET, POST, PUT
    Method string

    // 如上面URL所示喇喉,記錄了請求url以及url后面的查詢參數(shù)信息
    URL *url.URL

    Proto      string // "HTTP/1.0"
    ProtoMajor int    // 1
    ProtoMinor int    // 0

    // If a server received a request with header lines,
    //
    //  Host: example.com
    //  accept-encoding: gzip, deflate
    //  Accept-Language: en-us
    //  fOO: Bar
    //  foo: two
    //
    // then
    //
    //  Header = map[string][]string{
    //      "Accept-Encoding": {"gzip, deflate"},
    //      "Accept-Language": {"en-us"},
    //      "Foo": {"Bar", "two"},
    //  }
    Header Header

    // Body is the request's body.
    //
    // For client requests, a nil body means the request has no
    // body, such as a GET request. The HTTP Client's Transport
    // is responsible for calling the Close method.
    // 這里由提示祖今,如果由使用Body,則要記得調(diào)用Close方法關(guān)閉Body
    Body io.ReadCloser

    GetBody func() (io.ReadCloser, error)

    // ContentLength records the length of the associated content.
    // The value -1 indicates that the length is unknown.
    // Values >= 0 indicate that the given number of bytes may
    // be read from Body.
    //
    // For client requests, a value of 0 with a non-nil Body is
    // also treated as unknown.
    ContentLength int64

    // TransferEncoding lists the transfer encodings from outermost to
    // innermost. An empty list denotes the "identity" encoding.
    // TransferEncoding can usually be ignored; chunked encoding is
    // automatically added and removed as necessary when sending and
    // receiving requests.
    TransferEncoding []string

    Close bool
    Host string

    // Form contains the parsed form data, including both the URL
    // field's query parameters and the PATCH, POST, or PUT form data.
    // This field is only available after ParseForm is called.
    // The HTTP client ignores Form and uses Body instead.
    // 注意的是拣技,這里的值包括url和body里的兩部分
    Form url.Values

    // PostForm contains the parsed form data from PATCH, POST
    // or PUT body parameters.
    // This field is only available after ParseForm is called.
    // The HTTP client ignores PostForm and uses Body instead.
    PostForm url.Values

    MultipartForm *multipart.Form
    Trailer Header
    RemoteAddr string
    RequestURI string
    TLS *tls.ConnectionState
    Cancel <-chan struct{}
    
    // Response is the redirect response which caused this request
    // to be created. This field is only populated during client
    // redirects.
    Response *Response
    ctx context.Context
}

可以看出千诬,Request結(jié)構(gòu)包含了http請求的信息并做了簡單的處理,同時也內(nèi)置了一些基礎(chǔ)的函數(shù)膏斤,大家可以通過IDE的智能提示或者源碼看看大渤。

了解ListenAndServe

ListenAndServe函數(shù)主要是指定監(jiān)聽的ip和端口,并指定一個Handler處理監(jiān)聽的請求掸绞。

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}

Handler也是一個接口類型泵三,但是Handler里的ServeHTTP(ResponseWriter, *Request)的參數(shù)類型是不是很熟悉,跟我們上面路由回調(diào)函數(shù)一模一樣衔掸,這兩者之間是不是有啥不可告人的事呢烫幕?
Server涉及到http服務(wù)器的處理細(xì)節(jié),我們先不細(xì)究敞映,主要還是看看Handler较曼。

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
    handler := sh.srv.Handler
    if handler == nil {
        handler = DefaultServeMux
    }
    if req.RequestURI == "*" && req.Method == "OPTIONS" {
        handler = globalOptionsHandler{}
    }
    handler.ServeHTTP(rw, req)
}

上面是Server處理http請求的一個關(guān)鍵代碼,可以看到如果handler為空振愿,則會使用DefaultServeMux來代替捷犹,那么我們下面就來仔細(xì)探究DefaultServeMux弛饭。請看代碼:

var DefaultServeMux = &defaultServeMux

var defaultServeMux ServeMux

type ServeMux struct {
    mu    sync.RWMutex
    m     map[string]muxEntry  // 這里就是路由映射表
    es    []muxEntry // slice of entries sorted from longest to shortest.
    hosts bool       // whether any patterns contain hostnames
}

// 這里就是路由以及對應(yīng)的路由回調(diào)函數(shù)
type muxEntry struct {
    h       Handler
    pattern string
}

// ServeMux的一些關(guān)鍵函數(shù)
// HandleFunc registers the handler function for the given pattern.
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    if handler == nil {
        panic("http: nil handler")
    }
    mux.Handle(pattern, HandlerFunc(handler))
}

func (mux *ServeMux) Handle(pattern string, handler Handler) {
    mux.mu.Lock()
    defer mux.mu.Unlock()

    // 判斷路由不能為空,以及不能重復(fù)設(shè)置
    if pattern == "" {
        panic("http: invalid pattern")
    }
    if handler == nil {
        panic("http: nil handler")
    }
    if _, exist := mux.m[pattern]; exist {
        panic("http: multiple registrations for " + pattern)
    }

    if mux.m == nil {
        mux.m = make(map[string]muxEntry)
    }
    // 將路由及回調(diào)函數(shù)保存到路由映射表中
    e := muxEntry{h: handler, pattern: pattern}
    mux.m[pattern] = e
    if pattern[len(pattern)-1] == '/' {
        mux.es = appendSorted(mux.es, e)
    }

    if pattern[0] != '/' {
        mux.hosts = true
    }
}


// 這個函數(shù)的作用萍歉,就是請求url(路由)匹配一個handler(路由回調(diào)函數(shù))
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {

    // 不支持 http.method == CONNECT
    if r.Method == "CONNECT" {
        if u, ok := mux.redirectToPathSlash(r.URL.Host, r.URL.Path, r.URL); ok {
            return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
        }
        return mux.handler(r.Host, r.URL.Path)
    }

    // 獲取host和path
    // 比如url="https://www.baidu.com:8080/hello?a=b
    // 則 host = www.baidu.com
    // path = /hello
    host := stripHostPort(r.Host)
    path := cleanPath(r.URL.Path)

    // 打個比方侣颂,如過請求 https://baidu.com/aaa 沒有匹配成功
    // 則重定向到 https://baidu.com/aaa/
    if u, ok := mux.redirectToPathSlash(host, path, r.URL); ok {
        return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
    }

    // 這里的path跟URL里的path不一致,則重定向到URL里的path去
    if path != r.URL.Path {
        _, pattern = mux.handler(host, path)
        url := *r.URL
        url.Path = path
        return RedirectHandler(url.String(), StatusMovedPermanently), pattern
    }

    return mux.handler(host, r.URL.Path)
}

// 其實(shí)這個才是真正的路由匹配函數(shù)枪孩,注意它跟Handler首字母大小寫不同
func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
    mux.mu.RLock()
    defer mux.mu.RUnlock()

    // Host-specific pattern takes precedence over generic ones
    if mux.hosts {
        h, pattern = mux.match(host + path)
    }
    if h == nil {
        h, pattern = mux.match(path)
    }
    if h == nil {
        h, pattern = NotFoundHandler(), ""
    }
    return
}

// 這里大致可以看出憔晒。先使用完全匹配,然后在按照順序使用前綴匹配
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
    // Check for exact match first.
    v, ok := mux.m[path]
    if ok {
        return v.h, v.pattern
    }

    // Check for longest valid match.  mux.es contains all patterns
    // that end in / sorted from longest to shortest.
    for _, e := range mux.es {
        if strings.HasPrefix(path, e.pattern) {
            return e.h, e.pattern
        }
    }
    return nil, ""
}

// 這里就是上面Server部分所說蔑舞,當(dāng)一個http請求進(jìn)來時拒担,默認(rèn)使用DefaultServeMux
// 從而進(jìn)入到這個函數(shù)來。
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
    if r.RequestURI == "*" {
        if r.ProtoAtLeast(1, 1) {
            w.Header().Set("Connection", "close")
        }
        w.WriteHeader(StatusBadRequest)
        return
    }
    // 根據(jù)路由規(guī)則獲取對應(yīng)路由回調(diào)函數(shù)
    h, _ := mux.Handler(r)
    // 執(zhí)行路由回調(diào)函數(shù)
    h.ServeHTTP(w, r)
}

另一種寫法

上面里的main函數(shù)可以換成下面寫法攻询,兩者是等價的

func main() {
    server := http.ServeMux{}
    server.HandleFunc("/echo", echo) // 注冊路由以及回調(diào)函數(shù)
    // 監(jiān)聽ip和端口从撼,第二個參數(shù)為nil,則使用默認(rèn)的DefaultServeMux來處理請求
    if err := http.ListenAndServe("127.0.0.1:10001", &server); err != nil {
        panic(err)
    }
}

進(jìn)一步思考

通過上面的源碼钧栖,我可以可以發(fā)現(xiàn)go語言更多的是定義http處理相關(guān)接口低零,比如上面的ServeMux,它負(fù)責(zé)將路由映射到處理函數(shù)中桐经。實(shí)際上我們完成可以自己寫一個,只要實(shí)現(xiàn)相關(guān)的接口就ok浙滤。
這里是不是可以看出go語言的一些哲學(xué)呢阴挣?比如提供接口,讓用戶去決定具體實(shí)現(xiàn)纺腊?(原諒我知識面不夠畔咧,不知道怎么說...)
最后我們想一想,利用這些接口揖膜,我們是不是可以開發(fā)一個簡單的go框架呢誓沸,比如說最簡單的MVC模型框架?我倒是挺想試試的壹粟!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末拜隧,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子趁仙,更是在濱河造成了極大的恐慌洪添,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,509評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件雀费,死亡現(xiàn)場離奇詭異干奢,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)盏袄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評論 3 394
  • 文/潘曉璐 我一進(jìn)店門忿峻,熙熙樓的掌柜王于貴愁眉苦臉地迎上來薄啥,“玉大人,你說我怎么就攤上這事逛尚÷⒕澹” “怎么了?”我有些...
    開封第一講書人閱讀 163,875評論 0 354
  • 文/不壞的土叔 我叫張陵黑低,是天一觀的道長赘艳。 經(jīng)常有香客問我,道長克握,這世上最難降的妖魔是什么蕾管? 我笑而不...
    開封第一講書人閱讀 58,441評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮菩暗,結(jié)果婚禮上掰曾,老公的妹妹穿的比我還像新娘。我一直安慰自己停团,他們只是感情好旷坦,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,488評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著佑稠,像睡著了一般秒梅。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上舌胶,一...
    開封第一講書人閱讀 51,365評論 1 302
  • 那天捆蜀,我揣著相機(jī)與錄音,去河邊找鬼幔嫂。 笑死辆它,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的履恩。 我是一名探鬼主播锰茉,決...
    沈念sama閱讀 40,190評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼切心!你這毒婦竟也來了飒筑?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,062評論 0 276
  • 序言:老撾萬榮一對情侶失蹤绽昏,失蹤者是張志新(化名)和其女友劉穎扬霜,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體而涉,經(jīng)...
    沈念sama閱讀 45,500評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡著瓶,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,706評論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了啼县。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片材原。...
    茶點(diǎn)故事閱讀 39,834評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡沸久,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出余蟹,到底是詐尸還是另有隱情卷胯,我是刑警寧澤,帶...
    沈念sama閱讀 35,559評論 5 345
  • 正文 年R本政府宣布威酒,位于F島的核電站窑睁,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏葵孤。R本人自食惡果不足惜担钮,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,167評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望尤仍。 院中可真熱鬧箫津,春花似錦、人聲如沸宰啦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽赡模。三九已至田炭,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間漓柑,已是汗流浹背教硫。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留欺缘,地道東北人栋豫。 一個月前我還...
    沈念sama閱讀 47,958評論 2 370
  • 正文 我出身青樓挤安,卻偏偏與公主長得像谚殊,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子蛤铜,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,779評論 2 354

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