使用go搭建简单的http服务及源码解析

创建一个简单的http服务

使用go语言搭建一个http服务事件很简单的事情,如果代码逻辑很简单的话甚至不用框架就行。话不多说,直接看代码吧。

package main

import (
    "fmt"
    "net/http"
)

func echo(rw http.ResponseWriter, r *http.Request) {
    var rspText string
    params := r.URL.Query() // 获取查询参数
    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之前调用,否则不会生效
    rw.WriteHeader(http.StatusOK)                 // 这里可以省略
    rw.Write([]byte(rspText))
}

func main() {
    http.HandleFunc("/echo", echo) // 注册路由以及回调函数
    // 监听ip和端口,第二个参数为nil,则使用默认的DefaultServeMux来处理请求
    if err := http.ListenAndServe("127.0.0.1:10001", nil); err != nil {
        panic(err)
    }
}

运行以及返回结果

~ 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语言创建http服务最简单只需要两步

  1. 使用http.HandleFunc绑定路由以及路由对应的回调函数
  2. 使用http.ListenAndServe指定监听端口,并启动服务

下面我们第一步的去分析这两个函数。

了解http.HandleFunc

首先我们先看下源码,go语言的源码注释还是可以的,借助源码我们可以方便正确的了解该函数的作用

// 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.
// 这里可以看出,实际上是调用了DefaultServeMux.HandleFunc
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    DefaultServeMux.HandleFunc(pattern, handler)
}

首先看参数类型,第一个是路由地址,没啥好说的。不过注释提示了,如果想进一步了解路由的匹配规则,可以查看ServeMux的文档。第二个参数也限定了回调函数的类型。下面我们具体查看下回调函数的这两个参数类型。
首先是ResponseWriter,它其实是一个接口类型。看起来也是简单且清晰的,就是三件事三个接口

  1. 写入http response status状态码
  2. 写入http response header
  3. 写入http response body(这里使用byte类型,即支持任意数据类型)
type Header map[string][]string

// 注意,源码注释比较详细,但是字数太多就不放这里了,推荐去看看的
type ResponseWriter interface {
    // 返回一个header字典,调用WriteHeader函数时,将会返回该map数据。但是对header的
    // 修改必须在调用WriteHeader前,否则不会生效
    Header() Header
    // 写数据到http响应里的body中
    // 调用该函数之前,如果没调用WriteHeader则会先调用一次
    Write([]byte) (int, error)
    // 先http相应写入statusCode以及header内容
    WriteHeader(statusCode int)
}

下面再来看看Request,它是一个结构体,而不像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后面的查询参数信息
    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,则要记得调用Close方法关闭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结构包含了http请求的信息并做了简单的处理,同时也内置了一些基础的函数,大家可以通过IDE的智能提示或者源码看看。

了解ListenAndServe

ListenAndServe函数主要是指定监听的ip和端口,并指定一个Handler处理监听的请求。

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)的参数类型是不是很熟悉,跟我们上面路由回调函数一模一样,这两者之间是不是有啥不可告人的事呢?
Server涉及到http服务器的处理细节,我们先不细究,主要还是看看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请求的一个关键代码,可以看到如果handler为空,则会使用DefaultServeMux来代替,那么我们下面就来仔细探究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
}

// 这里就是路由以及对应的路由回调函数
type muxEntry struct {
    h       Handler
    pattern string
}

// ServeMux的一些关键函数
// 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()

    // 判断路由不能为空,以及不能重复设置
    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)
    }
    // 将路由及回调函数保存到路由映射表中
    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
    }
}


// 这个函数的作用,就是请求url(路由)匹配一个handler(路由回调函数)
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)
}

// 其实这个才是真正的路由匹配函数,注意它跟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部分所说,当一个http请求进来时,默认使用DefaultServeMux
// 从而进入到这个函数来。
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
    }
    // 根据路由规则获取对应路由回调函数
    h, _ := mux.Handler(r)
    // 执行路由回调函数
    h.ServeHTTP(w, r)
}

另一种写法

上面里的main函数可以换成下面写法,两者是等价的

func main() {
    server := http.ServeMux{}
    server.HandleFunc("/echo", echo) // 注册路由以及回调函数
    // 监听ip和端口,第二个参数为nil,则使用默认的DefaultServeMux来处理请求
    if err := http.ListenAndServe("127.0.0.1:10001", &server); err != nil {
        panic(err)
    }
}

进一步思考

通过上面的源码,我可以可以发现go语言更多的是定义http处理相关接口,比如上面的ServeMux,它负责将路由映射到处理函数中。实际上我们完成可以自己写一个,只要实现相关的接口就ok。
这里是不是可以看出go语言的一些哲学呢?比如提供接口,让用户去决定具体实现?(原谅我知识面不够,不知道怎么说...)
最后我们想一想,利用这些接口,我们是不是可以开发一个简单的go框架呢,比如说最简单的MVC模型框架?我倒是挺想试试的!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容