概述

一个典型的 Go Web 程序结构如下,摘自《Go Web 编程》:

  • 客户端发送请求;
  • 服务器中的多路复用器收到请求;
  • 多路复用器根据请求的 URL 找到注册的处理器,将请求交由处理器处理;
  • 处理器执行程序逻辑,必要时与数据库进行交互,得到处理结果;
  • 处理器调用模板引擎将指定的模板和上一步得到的结果渲染成客户端可识别的数据格式(通常是 HTML);
  • 最后将数据通过响应返回给客户端;
  • 客户端拿到数据,执行对应的操作,如渲染出来呈现给用户。

本文介绍如何创建多路复用器,如何注册处理器,最后再简单介绍一下 URL 匹配。我们以上一篇文章中的"Hello World"程序作为基础。

package main

import (
"fmt"
"log"
"net/http"
) func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World")
} func main() {
http.HandleFunc("/", hello)
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}

多路复用器

默认多路复用器

net/http 包为了方便我们使用,内置了一个默认的多路复用器DefaultServeMux。定义如下:

// src/net/http/server.go

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

这里给大家介绍一下 Go 标准库代码的组织方式,便于大家对照。

  • Windows上,Go 语言的默认安装目录为C:\Go,即GOROOT
  • GOROOT下有一个 src 目录,标库库的代码都在这个目录中;
  • 每个包有一个单独的目录,例如 fmt 包在src/fmt目录中;
  • 子包在其父包的子目录中,例如 net/http 包在src/net/http目录中。

net/http 包中很多方法都在内部调用DefaultServeMux的对应方法,如HandleFunc。我们知道,HandleFunc是为指定的 URL 注册一个处理器(准确来说,hello是处理器函数,见下文)。其内部实现如下:

// src/net/http/server.go
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}

实际上,http.HandleFunc方法是将处理器注册到DefaultServeMux中的。

另外,我们使用 ":8080" 和 nil 作为参数调用http.ListenAndServe时,会创建一个默认的服务器:

// src/net/http/server.go
func ListenAndServe(addr string, handler Handler) {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}

这个服务器默认使用DefaultServeMux来处理器请求:

type serverHandler struct {
srv *Server
} func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
handler := sh.srv.Handler
if handler == nil {
handler = DefaultServeMux
}
handler.ServeHTTP(rw, req)
}

服务器收到的每个请求会调用对应多路复用器(即ServeMux)的ServeHTTP方法。在ServeMuxServeHTTP方法中,根据 URL 查找我们注册的处理器,然后将请求交由它处理。

虽然默认的多路复用器使用起来很方便,但是在生产环境中不建议使用。由于DefaultServeMux是一个全局变量,所有代码,包括第三方代码都可以修改它。

有些第三方代码会在DefaultServeMux注册一些处理器,这可能与我们注册的处理器冲突。

比较推荐的做法是自己创建多路复用器。

创建多路复用器

创建多路复用器也比较简单,直接调用http.NewServeMux方法即可。然后,在新创建的多路复用器上注册处理器:

package main

import (
"fmt"
"log"
"net/http"
) func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World")
} func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", hello) server := &http.Server{
Addr: ":8080",
Handler: mux,
} if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}

上面代码的功能与 "Hello World" 程序相同。这里我们还自己创建了服务器对象。通过指定服务器的参数,我们可以创建定制化的服务器。

server := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 1 * time.Second,
WriteTimeout: 1 * time.Second,
}

在上面代码,我们创建了一个读超时和写超时均为 1s 的服务器。

处理器和处理器函数

上文中提到,服务器收到请求后,会根据其 URL 将请求交给相应的处理器处理。处理器是实现了Handler接口的结构,Handler接口定义在 net/http 包中:

// src/net/http/server.go
type Handler interface {
func ServeHTTP(w Response.Writer, r *Request)
}

我们可以定义一个实现该接口的结构,注册这个结构类型的对象到多路复用器中:

package main

import (
"fmt"
"log"
"net/http"
) type GreetingHandler struct {
Language string
} func (h GreetingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "%s", h.Language)
} func main() {
mux := http.NewServeMux()
mux.Handle("/chinese", GreetingHandler{Language: "你好"})
mux.Handle("/english", GreetingHandler{Language: "Hello"}) server := &http.Server {
Addr: ":8080",
Handler: mux,
} if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}

与前面的代码有所不同,上段代码中,定义了一个实现Handler接口的结构GreetingHandler。然后,创建该结构的两个对象,分别将它注册到多路复用器的/hello/world路径上。注意,这里注册使用的是Handle方法,注意与HandleFunc方法对比。

启动服务器之后,在浏览器的地址栏中输入localhost:8080/chinese,浏览器中将显示你好,输入localhost:8080/english将显示Hello

虽然,自定义处理器这种方式比较灵活,强大,但是需要定义一个新的结构,实现ServeHTTP方法,还是比较繁琐的。为了方便使用,net/http 包提供了以函数的方式注册处理器,即使用HandleFunc注册。函数必须满足签名:func (w http.ResponseWriter, r *http.Request)

我们称这个函数为处理器函数。我们的 "Hello World" 程序中使用的就是这种方式。HandleFunc方法内部,会将传入的处理器函数转换为HandlerFunc类型。

// src/net/http/server.go
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if handler == nil {
panic("http: nil handler")
}
mux.Handle(pattern, HandlerFunc(handler))
}

HandlerFunc是底层类型为func (w ResponseWriter, r *Request)的新类型,它可以自定义其方法。由于HandlerFunc类型实现了Handler接口,所以它也是一个处理器类型,最终使用Handle注册。

// src/net/http/server.go
type HandlerFunc func(w *ResponseWriter, r *Request) func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}

注意,这几个接口和方法名很容易混淆,这里再强调一下:

  • Handler:处理器接口,定义在 net/http 包中。实现该接口的类型,其对象可以注册到多路复用器中;
  • Handle:注册处理器的方法;
  • HandleFunc:注册处理器函数的方法;
  • HandlerFunc:底层类型为func (w ResponseWriter, r *Request)的新类型,实现了Handler接口。它连接了处理器函数处理器

URL 匹配

一般的 Web 服务器有非常多的 URL 绑定,不同的 URL 对应不同的处理器。但是服务器是怎么决定使用哪个处理器的呢?例如,我们现在绑定了 3 个 URL,//hello/hello/world

显然,如果请求的 URL 为/,则调用/对应的处理器。如果请求的 URL 为/hello,则调用/hello对应的处理器。如果请求的 URL 为/hello/world,则调用/hello/world对应的处理器。

但是,如果请求的是/hello/others,那么使用哪一个处理器呢? 匹配遵循以下规则:

  • 首先,精确匹配。即查找是否有/hello/others对应的处理器。如果有,则查找结束。如果没有,执行下一步;
  • 将路径中最后一个部分去掉,再次查找。即查找/hello/对应的处理器。如果有,则查找结束。如果没有,继续执行这一步。即查找/对应的处理器。

这里有一个注意点,如果注册的 URL 不是以/结尾的,那么它只能精确匹配请求的 URL。反之,即使请求的 URL 只有前缀与被绑定的 URL 相同,ServeMux也认为它们是匹配的。

这也是为什么上面步骤进行到/hello/时,不能匹配/hello的原因。因为/hello不以/结尾,必须要精确匹配。

如果,我们绑定的 URL 为/hello/,那么当服务器找不到与/hello/others完全匹配的处理器时,就会退而求其次,开始寻找能够与/hello/匹配的处理器。

看下面的代码:

package main

import (
"fmt"
"log"
"net/http"
) func indexHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "This is the index page")
} func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "This is the hello page")
} func worldHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "This is the world page")
} func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", indexHandler)
mux.HandleFunc("/hello", helloHandler)
mux.HandleFunc("/hello/world", worldHandler) server := &http.Server{
Addr: ":8080",
Handler: mux,
} if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
  • 浏览器请求localhost:8080/将返回"This is the index page",因为/精确匹配;

  • 浏览器请求localhost:8080/hello将返回"This is the hello page",因为/hello精确匹配;

  • 浏览器请求localhost:8080/hello/将返回"This is the index page"注意这里不是hello,因为绑定的/hello需要精确匹配,而请求的/hello/不能与之精确匹配。故而向上查找到/

  • 浏览器请求localhost:8080/hello/world将返回"This is the world page",因为/hello/world精确匹配;

  • 浏览器请求localhost:8080/hello/world/将返回"This is the index page"查找步骤为/hello/world/(不能与/hello/world精确匹配)-> /hello/(不能与/hello/精确匹配)-> /

  • 浏览器请求localhost:8080/hello/other将返回"This is the index page"查找步骤为/hello/others -> /hello/(不能与/hello精确匹配)-> /

如果注册时,将/hello改为/hello/,那么请求localhost:8080/hello/localhost:8080/hello/world/都将返回"This is the hello page"。自己试试吧!

思考:

使用/hello/注册处理器时,localhost:8080/hello/返回什么?

总结

本文介绍了 Go Web 程序的基本结构。Go Web 的基本形式如下:

package main

import (
"fmt"
"log"
"net/http"
) func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World")
} type greetingHandler struct {
Name string
} func (h greetingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %s", h.Name)
} func main() {
mux := http.NewServeMux()
// 注册处理器函数
mux.HandleFunc("/hello", helloHandler) // 注册处理器
mux.Handle("/greeting/golang", greetingHandler{Name: "Golang"}) server := &http.Server {
Addr: ":8080",
Handler: mux,
}
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}

后续文章中大部分程序只是在此基础上增加处理器或处理器函数并注册到相应的 URL 中而已。处理器和处理器函数可以只使用一种或两者都使用。注意,为了方便,命名中我都加上了Handler

参考资料

  1. Go Web 编程

我的博客

欢迎关注我的微信公众号【GoUpUp】,共同学习,一起进步~

本文由博客一文多发平台 OpenWrite 发布!

Go Web 编程之 程序结构的更多相关文章

  1. 微信小程序 - 结构目录 | 配置介绍

    结构目录 小程序框架提供了自己的视图层描述语言 WXML 和 WXSS,以及 JavaScript,并在视图层与逻辑层间提供了数据传输和事件系统,让开发者能够专注于数据与逻辑. 一.小程序文件结构和传 ...

  2. 微信小程序结构目录、配置介绍、视图层(数据绑定,运算,列表渲染,条件渲染)

    目录 一.小程序结构目录 1.1 小程序文件结构和传统web对比 1.2 基本的项目目录 二.配置介绍 2.1 配置介绍 2.2 全局配置app.json 2.3 page.json 三.视图层 3. ...

  3. 了解ASP.NET5 Web应用程序结构

    本文参考ASP.NET5 官方文档 Understanding ASP.NET 5 Web Apps,加入了一些个人理解,理解不对的地方希望大家能指出,互相学习. ASP.NET 5 针对WEB编程引 ...

  4. [ Python ] Flask 基于 Web开发 大型程序的结构实例解析

    作为一个编程入门新手,Flask是我接触到的第一个Web框架.想要深入学习,就从<FlaskWeb开发:基于Python的Web应用开发实战>这本书入手,本书由于是翻译过来的中文版,理解起 ...

  5. python3编程基础之一:程序结构

    程序从程序入口进入,到程序执行结束,大体是按照顺序结构执行语句.函数或代码块,掌握程序的结构,有利于把握程序的主体框架. 1.顺序结构--最常见的结构 顺序结构的程序设计是最简单的,只要按照解决问题的 ...

  6. python web编程-概念预热篇

    互联网正在引发一场革命??不喜欢看概念的跳过,注意这里仅仅是一些从python核心编程一书的摘抄 这正是最激动人心的一部分了,web编程 Web 客户端和服务器端交互使用的“语言”,Web 交互的标准 ...

  7. 全部编程皆为Web编程

    原文作者:Jeff Atwood   原文链接:http://blog.codinghorror.com/all-programming-is-web-programming Michael Brau ...

  8. Python 四大主流 Web 编程框架

    Python 四大主流 Web 编程框架 目前Python的网络编程框架已经多达几十个,逐个学习它们显然不现实.但这些框架在系统架构和运行环境中有很多共通之处,本文带领读者学习基于Python网络框架 ...

  9. 清晰架构(Clean Architecture)的Go微服务: 程序结构

    我使用Go和gRPC创建了一个微服务,并试图找出最佳的程序结构,它可以用作我未来程序的模板. 我有Java背景,并发现自己在Java和Go之间挣扎,它们之间的编程理念完全不同.我写了一系列关于在项目工 ...

随机推荐

  1. H3C 路由协议与可路由协议

  2. ip2long之后有什么好处?

    ip2long需要bigint来存储,而且在32位和64位系统中存储方式还有区别: 而保存成字符串,只需要char20即可. 那么,ip2long好处在哪? 做投票项目的时候,将ip地址处理后用int ...

  3. element-ui css 文件加载 失败(https://unpkg.com/element-ui/lib/theme-chalk/index.css,加载失败)

    在main.js文件引入   import Vue from 'vue'; import Element from 'element-ui'; import 'element-ui/lib/theme ...

  4. springboot2动态数据源的绑定

    由于springboot2更新了绑定参数的api,部分springboot1用于绑定的工具类如RelaxedPropertyResolver已经无法在新版本中使用.本文实现参考了https://blo ...

  5. HTTP协议---三次握手

    HTTP协议 关于协议 ​ 对于应用层开发人员,接触最多的网络协议通常都是传输层的TCP,为什么这么说,因为再往上的应用层协议,如:HTTP.HTTPS.POP3.SMTP.RPC.FTP.TELNE ...

  6. ios9.3.3 h5的js代码全部失效

    做微信公众号页面时,ios9.3.3 h5的js代码全部失效描述: 机型iphone6 plus,ios9.3.3js代码全部失效,刚开始还以为是ios和jq兼容问题, 后来发现是es6语法不能读,导 ...

  7. Vue的Router路由传参

    一.文件结构 二.vue.js 打开此链接 https://cdn.bootcss.com/vue/2.6.10/vue.js 复制粘贴页面的所有内容 三.vue-router.js 打开此链接  h ...

  8. Educational Codeforces Round 54 (Rated for Div. 2) D Edge Deletion (SPFA + bfs)

    题目大意:给定你一个包含n个点m条边的无向图,现在最多在图中保留k条边,问怎么删除多的边,使得图中良好的节点数最多,求出保留在图中的边的数量和编号. 良好的节点定义为:删除某条边后该点到点1的最短距离 ...

  9. Vue2.0 Vue.set的使用

    原文链接: https://blog.csdn.net/qq_30455841/article/details/78666571

  10. vue面试的一些总结

    vue中组件的data为什么是一个函数? 组件是可复用的vue实例,一个组件被创建好之后,就可能被用在各个地方,而组件不管被复用了多少次,组件中的data数据都应该是相互隔离,互不影响的,基于这一理念 ...