Go标准库http server优雅启动深入理解

 更新时间:2024年01月15日 10:52:33   作者:凉凉的知识库  
这篇文章主要介绍了Go标准库http server优雅启动深入理解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

如何用最少的代码创建一个HTTP server?

package main

import (
 "net"
 "net/http"
)

func main() {
 // 方式1
 err := http.ListenAndServe(":8080", nil)
 if err != nil {
   panic(err)
 }
}

点开http.ListenAndServe可以看到函数只是创建了Server类型并调用server.ListenAndServe()

所以下面的和上面的代码没有区别

package main

import (
 "net"
 "net/http"
)

func main() {
 // 方式2
 server := &http.Server{Addr: ":8080"}
 err := server.ListenAndServe()
 if err != nil {
  panic(err)
 }
}

ListenAndServe()如其名会干两件事

  • 监听一个端口,即Listen的过程

  • 处理进入端口的连接,即Serve的过程

所以下面的代码和上面的代码也没区别

package main

import (
 "net"
 "net/http"
)

func main() {
 // 方式3
 ln, err := net.Listen("tcp", ":8080")
 if err != nil {
  panic(err)
 }

 server := &http.Server{}
 err = server.Serve(ln)
 if err != nil {
  panic(err)
 }
}

一张图展示三种使用方式

路由?no!Handler!

按上面的代码启动HTTP Server没有太大意义,因为我们还没有设定路由,所以无法正常响应请求

$ curl  127.0.0.1:8080 
404 page not found

暂停思考一下,服务器返回404是因为没有设定路由么?no,no,no,你需要转变一下思维。服务器返回404不是因为我们没有设置路由,而是因为没有设置请求的处理程序,这个处理程序在Go中叫作:Handler

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

🌲 怎么定义请求的处理程序?

由上可知,仅需要实现ServeHTTP(ResponseWriter, *Request)接口即可

注意,示例代码没有判断任何路由(PATH)

type handlerImp struct {
}
func (imp handlerImp) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 if r.Method == "GET" {
  w.Write([]byte("Receive GET request"))
  return
 }
 if r.Method == "POST" {
  w.Write([]byte("Receive POST request"))
  return
 }
 return
}

🌲 怎么设置请求的处理程序?

三种方式本质上都是把自定义的Handler赋值到ServerHandler属性中

func main() {
 // 方式1
 // err := http.ListenAndServe(":8080", handlerImp{})
 // if err != nil {
 //  panic(err)
 // }
 // 方式2
 // server := &http.Server{Addr: ":8080", Handler: handlerImp{}}
 // err := server.ListenAndServe()
 // if err != nil {
 //  panic(err)
 // }
 // 方式3
 ln, err := net.Listen("tcp", ":8080")
 if err != nil {
  panic(err)
 }
 server := &http.Server{Handler:handlerImp{}}
 err = server.Serve(ln)
 if err != nil {
  panic(err)
 }
}

🌲 设置请求的处理程序之后的效果

handlerImp只针对Method做了不同的响应,没有对PATH做任何的判断,所以无论请求什么样的路径都能拿到一个预期的响应。

$ curl  -X POST 127.0.0.1:8080/foo
Receive POST request%  

$ curl  127.0.0.1:8080/foo/bar 
Receive GET request%  

此时再体会一下这句话:我们设置的不是路由,而是设置请求的处理程序

再聊Handler

type handlerImp struct {
}
func (imp handlerImp) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 if r.Method == "GET" {
  w.Write([]byte("Receive GET request"))
  return
 }
 if r.Method == "POST" {
  w.Write([]byte("Receive POST request"))
  return
 }
 return
}

如上所述,无论任何PATH,任何Method等,所有的请求都会被handlerImp.ServeHTTP处理。

我们可以判断PATH、Method等,根据不同的请求特征执行不同的逻辑,并且全部在这一个函数中全部完成

很明显,这违反了高内聚,低耦合的编程范式

停下来思考下,如何编写一个高内聚,低耦合的handlerImp.ServeHTTP,使之针对不同HTTP请求执行不同的逻辑呢

type handlerImp struct {
}
func (imp handlerImp) handleMethodGet(w http.ResponseWriter, r *http.Request) {
 w.Write([]byte("Receive GET request"))
 return
}
func (imp handlerImp) handleMethodPost(w http.ResponseWriter, r *http.Request) {
 w.Write([]byte("Receive POST request"))
 return
}
func (imp handlerImp) handlePathFoo(w http.ResponseWriter, r *http.Request) {
 w.Write([]byte("Receive path foo"))
 return
}
func (imp handlerImp) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 if r.URL.Path == "/foo" {
  imp.handlePathFoo(w, r)
  return
 }
 if r.Method == "GET" {
  imp.handleMethodGet(w, r)
  return
 }
 if r.Method == "POST" {
  imp.handleMethodPost(w, r)
  return
 }
 return
}

如果你的答案和上面的代码类似,那么我对于这段代码的点评是:不太高明☹️

🌲 如何编写一个高内聚,低耦合的ServeHTTP,针对不同HTTP请求执行不同的逻辑?

不知道你有没有听过设计模式中,组合模式。没有了解可以去了解下,或者看下图

经过组合模式重新设计的handlerImp,已经不再包含具体的逻辑了,它先搜索有没有针对PATH处理的逻辑,再搜索有没有针对Method处理的逻辑,它专注于逻辑分派,它是组合模式中的容器

容器(Container):容器接收到请求后会将工作分配给自己的子项目, 处理中间结果, 然后将最终结果返回给客户端。

type handlerImp struct {
 pathHandlers   map[string]http.Handler
 methodHandlers map[string]http.Handler
}
func NewHandlerImp() handlerImp {
 return handlerImp{
  pathHandlers:   make(map[string]http.Handler),
  methodHandlers: make(map[string]http.Handler),
 }
}
func (imp handlerImp) AddPathHandler(path string, h http.Handler) {
 imp.pathHandlers[path] = h
}
func (imp handlerImp) AddMethodHandler(method string, h http.Handler) {
 imp.methodHandlers[method] = h
}
func (imp handlerImp) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 if h, ok := imp.pathHandlers[r.URL.Path]; ok {
  h.ServeHTTP(w, r)
  return
 }
 if h, ok := imp.methodHandlers[r.Method]; ok {
  h.ServeHTTP(w, r)
  return
 }
 return
}

重新设计的handlerImp不执行逻辑,实际的逻辑被分离到每一个叶子结点中,而每一个叶子结点也都实现了ServeHTTP函数,即Handler接口

type PathFoo struct {
}
func (m PathFoo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 w.Write([]byte("Receive path foo"))
 return
}
type MethodGet struct {
}
func (m MethodGet) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 w.Write([]byte("Receive GET request"))
 return
}
type MethodPost struct {
}
func (m MethodPost) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 w.Write([]byte("Receive POST request"))
 return
}

再次强调,通过对组合模式的运用,我们把逻辑分派的功能聚合到handlerImp,把具体的逻辑聚合到PathFooMethodGetMethodPost

func main() {
 // 方式3
 ln, err := net.Listen("tcp", ":8080")
 if err != nil {
  panic(err)
 }

 h := NewHandlerImp()
 h.AddMethodHandler("GET", MethodGet{})
 h.AddMethodHandler("POST", MethodPost{})
 h.AddPathHandler("/foo", PathFoo{})

 server := &http.Server{Handler: h}
 err = server.Serve(ln)
 if err != nil {
  panic(err)
 }
}

一些Handlers

上面实现的handlerImp利用组合设计模式,已经能针对Path和Method设定和处理不同的逻辑,但整体功能略显简单。有哪些可以供我们使用且功能强大的Handlers呢?

http.ServeMux

Go标准库中就提供了一个Handler实现叫作http.ServeMux

⚠️ 当前(go1.21.*)版本仅支持匹配Path,但目前已经在讨论支持Method匹配和占位符了:net/http: add methods and path variables to ServeMux patterns #60227[1]

使用的方式如下

http.ServeMux提供两个函数用于注册不同Path的处理函数

  • ServeMux.Handle 接收的是Handler接口实现

  • ServeMux.HandleFunc 接收的是匿名函数

type PathBar struct {
}

func (m PathBar) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 w.Write([]byte("Receive path bar"))
 return
}

func main() {
 // 方式3
 ln, err := net.Listen("tcp", ":8080")
 if err != nil {
  panic(err)
 }

 mx := http.NewServeMux()
 mx.Handle("/bar/", PathBar{})
 mx.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) {
  w.Write([]byte("Receive path foo"))
 })

 server := &http.Server{Handler: mx}
 err = server.Serve(ln)
 if err != nil {
  panic(err)
 }
}

代码mx.Handle("/bar/", PathBar{})/bar//结尾,所以它可以匹配/bar/*所有的Path

关于http.ServeMux的细节不是本篇重点,后续会单独介绍

🌲 默认的Handler

因为是标准库内置的实现,当没有设置http.Server.Handler属性时,http.Server就会使用一个全局的变量DefaultServeMux *ServeMux来作为http.Server.Handler的值

var DefaultServeMux = &defaultServeMux

var defaultServeMux ServeMux

http包同时提供了两个函数可以在DefaultServeMux注册不同Path的处理函数

func main() {
 http.Handle("/bar/", PathBar{})
 http.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) {
  w.Write([]byte("Receive path foo"))
 })

 // 方式1
 err := http.ListenAndServe(":8080", nil)
 if err != nil {
  panic(err)
 }
}

http.Handle 接收的是Handler接口实现,对应的是

func Handle(pattern string, handler Handler) { DefaultServeMux.Handle(pattern, handler) }

http.HandleFunc 接收的是匿名函数,对应的是

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
 DefaultServeMux.HandleFunc(pattern, handler)
}

gorilla/mux

gorilla/mux是一个相当流行的第三方库,用法这里简单写下

除了经典的HandleHandleFunc函数,gorilla/mux还提供了MethodsSchemesHost等非常复杂的功能

但无论多复杂,其一定包含了ServeHTTP函数,即实现了Handler接口

func main() {
 r := mux.NewRouter()
    r.Handle("/foo/{bar}", PathBar{})
 r.Handle("/bar/", PathBar{})
 r.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) {
  w.Write([]byte("Receive path foo"))
 })
 r.Methods("GET").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  w.Write([]byte("Receive GET request"))
 })

 // 方式1
 err := http.ListenAndServe(":8080", r)
 if err != nil {
  panic(err)
 }
}

其他

还有很多其他优秀的mux实现,具体可以参考各自的官方文档。

https://github.com/go-chi/chi  star 15.9k

https://github.com/julienschmidt/httprouter  star 15.6k

关于Go标准库、第三方库中这些结构的关系通过下图展示

再聊组合模式

无论是官方的http.ServeMux,还是一些第三方库,实现上大多使用了组合设计模式

组合模式的魔力还不止于此。思考一下这个场景:目前已经存在路由servemux/*,并且使用了ServeMux

mx := http.NewServeMux()
mx.Handle("/servemux/bar/", PathBar{})
mx.HandleFunc("/servemux/foo", func(w http.ResponseWriter, r *http.Request) {
 w.Write([]byte("Receive servemux path foo"))
})

但此时还有另外一组路由/gorilla/*,使用了开源库gorilla/mux

r := mux.NewRouter()
r.Handle("/gorilla/bar/", PathBar{})
r.HandleFunc("/gorilla/foo", func(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Receive gorilla path foo"))
})

如何启动这样的服务器呢?

func main() {
 mx := http.NewServeMux()
 mx.Handle("/servemux/bar/", PathBar{})
 mx.HandleFunc("/servemux/foo", func(w http.ResponseWriter, r *http.Request) {
  w.Write([]byte("Receive servemux path foo"))
 })
 r := mux.NewRouter()
 r.Handle("/gorilla/bar/", PathBar{})
 r.HandleFunc("/gorilla/foo", func(w http.ResponseWriter, r *http.Request) {
  w.Write([]byte("Receive gorilla path foo"))
 })
 h := http.NewServeMux()
 h.Handle("/servemux/", mx)
 h.Handle("/gorilla/", r)
 // 方式1
 err := http.ListenAndServe(":8080", h)
 if err != nil {
  panic(err)
 }
}

利用组合设计模式,h := http.NewServeMux()作为新的容器,将不同的路由分配给另外两个容器

  • mx := http.NewServeMux()

  • r := mux.NewRouter()

总结

本文主要介绍了Go http server的启动方式,重点介绍了http server的请求处理器

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

别看它仅包含一个方法,但在组合模式的加成下,可以实现千变万化的形态。

除了Go标准库中提供了http.ServeMux还有一系列开源库gorilla/muxgo-chi/chijulienschmidt/httprouterHandler进行了实现。

每一个库具有的能力、使用方式、性能不同,但万变不离其宗,都绕不开组合模式和Handler接口

以上就是Go标准库http server优雅启动深入理解的详细内容,更多关于Go标准库http server启动的资料请关注脚本之家其它相关文章!

相关文章

  • Gin框架中的路由与请求处理的实现

    Gin框架中的路由与请求处理的实现

    本文主要介绍了Gin框架中的路由与请求处理的实现,包括路径参数、查询参数和路由分组的使用,具有一定的参考价值,感兴趣的可以了解一下
    2025-06-06
  • 使用Go语言启动Redis的实例详解

    使用Go语言启动Redis的实例详解

    这篇文章主要为大家介绍了Go语言中一个可以用来启动 redis-server 的开源库 github.com/stvp/tempredis,感兴趣的小伙伴可以跟随小编一起学习一下
    2025-01-01
  • golang使用http client发起get和post请求示例

    golang使用http client发起get和post请求示例

    这篇文章主要介绍了golang使用http client发起get和post请求示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-02-02
  • Golang中channel使用的一些小技巧

    Golang中channel使用的一些小技巧

    这篇文章主要介绍了Golang中channel使用的一些小技巧,本文讲解了关闭2次、读取的时候channel提前关闭了、向已经关闭的channel写数据等技巧及这实例代码,需要的朋友可以参考下
    2015-07-07
  • 详解go-micro微服务consul配置及注册中心

    详解go-micro微服务consul配置及注册中心

    这篇文章主要为大家介绍了go-micro微服务consul配置及注册中心示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-01-01
  • beego获取ajax数据的实例

    beego获取ajax数据的实例

    下面小编就为大家分享一篇beego获取ajax数据的实例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2017-12-12
  • go语言编程二维码生成及识别

    go语言编程二维码生成及识别

    这篇文章主要为大家介绍了go语言编程二维码的生成及识别示例演示,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-04-04
  • Go语言中panic和recover的实现

    Go语言中panic和recover的实现

    本文主要介绍了Go语言中panic和recover的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2025-10-10
  • Go语言中的逃逸分析究竟是什么?

    Go语言中的逃逸分析究竟是什么?

    这篇文章主要介绍了Go语言中的逃逸,套哟究竟是什么呢?通俗来讲,当一个对象的指针被多个方法或线程引用时,我们称这个指针发生了“逃逸”。下面文章将详细介绍Go语言中的逃逸,需要的朋友可以参考一下
    2021-09-09
  • go mod文件内容版本号简单用法详解

    go mod文件内容版本号简单用法详解

    这篇文章主要为大家介绍了go mod文件内容版本号简单用法详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-10-10

最新评论