goweb编程

第一部分 Go 与 Web 应用

1. Go 与 Web 应用

一门新的编程语言,需要构建与互联网(internet)和万维网(World Wide Web)交互的库和框架,go 语言从问世以来,在编写 web 应用和 x 即服务系统(*-as-a-service system)方面受到追捧

1.1 使用 go 语言构建 web 应用

大规模可扩展的 web 应用通常具备以下特质:

  • 可扩展
  • 模块化
  • 可维护
  • 高性能

1.1.1 go 与可扩展 web 应用

可扩展的应用意味着它是线性的,可以添加更多硬件来获得更强的请求处理能力

有两种方式进行性能扩展

  • 垂直扩展: 提升单台设备的 CPU 数量或性能
  • 水平扩展: 增加计算机数量

垂直扩展方面,go 语言只需要使用一个操作系统线程,就可以通过调度来高效运行数十万个 goruntine
水平扩展方面,go 语言可以编译成不包含任何动态依赖关系的静态二进制文件,我们可以把这些文件分发到没有 go 环境的系统里,用一种简单一致的方式部署 go web 应用

1.1.2 go 与模块化 web 应用

大规模 web 应用应该由可替换组件构成,可以更容易修改,并且可以复用

go 是静态类型语言,但是可以通过接口机制对行为进行描述,从而实现动态类型匹配.go 语言支持函数式编程

只需要创建多个小的 go 语言服务,然后组成一个 web 应用,就可以实现可替换、可扩展的微服务

1.1.3 go 与可维护的 web 应用

可维护的项目应该以合适的方式组织代码

go 有自带包管理系统

godoc 可以生成文档

gotest 可以进行测试,还有 web 应用测试工具

1.1.4 go 与高性能 web 应用

go 语言的性能强大,程序被编译为本地码,goroutine 提供了并发支持

1.2 web 应用的工作原理

险隘的角度看,web 应用是会对客户端发送的 http 请求做出响应,并通过 http 响应将 html 回传给客户端的计算机程序,这样一来,web 服务器和 web 应用看起来没有区别

但是像 httpd 和 apache 这样的 web 服务器会监听特定目录,并在接收到请求时返回该目录的文件,而 web 应用不会简单地返回文件,而是执行应用程序中预先设定好的操作

所以应该把 web 服务器看作是特殊的 web 应用,它只会返回被请求的文件

应用定义为: 与用户进行互动并帮助用户执行指定活动的软件程序.

web 应用应该满足: 1.向发送请求的客户端返回 html,客户端则展示渲染后的 html;2.向客户端传输数据时必须使用 http 协议

1.3 http 简介

http 是万维网的应用层通信协议.http 从 0.9 以来经过 3 次修改(1.0,1.1,2),应用最广泛的是 1.1

http 允许客户端和服务器连接,向服务器发送以空行(CRLF)结尾的 ASCII 字符串请求,服务器则返回不带任何元数据的 html

定义: http 是一种无状态、由文本构成的请求-响应协议,这种协议使用的是客户端-服务器计算模型

http 是无状态的,对后续请求和之前发生过的请求一无所知.而像 FTP、Telnet 这种面向连接的协议则会在客户端和服务器之间创建持续存在的通信通道.

http 以纯文本传输数据,利于排查错误,而不需要借助协议分析工具

1.4 web 应用的诞生

CGI 通用网关接口,是早期尝试动态生成 html 内容的技术之一.但是没有成为正式的互联网标准,CGI 这个名字倒是被后来的规范沿用下来

CGI 是一个接口,允许 web 服务器和 web 服务器上运行的进程之外的进程对接.这个对接的程序叫 CGI 程序,可以使用任意语言编写,CGI 程序运行后向标准输出返回结果,服务器则将这些结果传送到客户端

和 CGI 同期出现的还有服务端包含(SSI)技术.它允许开发者在 html 包含一些指令.在向客户端返回 html 之前,服务器会先执行文件中的指令,并将指令出现的位置替换成指令执行结果.最常见的用法是在 html 文件中包含被频繁使用的文件,或者是整个网站都会出现的首部和尾部代码段嵌入 html 文件中

SSI 演化为在 html 里嵌入更复杂的代码,并使用更强大的解释器.这一模式衍生出 PHP、ASP、JSP 和 ColdFusion 等一系列引擎,同时也是 Mustache、ERB、Velocity 等 web 模板引擎的基础.

1.5 http 请求

是一种请求-响应协议,以一个请求开始,http 请求和其他所有 http 报文(message)一样,由一系列文本行组成,会按照以下顺序排列

  1. 请求行(request-line)
  2. 零个或任意多个请求首部(header)
  3. 一个空行
  4. 可选的报文主体(body)
GET /Protocols/rfc2616/rfc2616.html HTTP/1.1
Host: www.w3.org
User-Agent: Mozilla/5.0
(empty line)

上面代码的第一行就是请求行

第一个单词为请求方法(request method),然后跟着的是统一文件描述符(URI),以及所用的 http 版本

接下来的两个文本行是请求的首部,而最后一行是空行,即使报文的主体内容为空,这个空行也必须存在(我: 可能因为浏览器编译需要这个?).是否包含主体内容,则根据请求方法而定

1.5.1 请求方法

请求方法是请求行中的第一个单词,指明了客户端对资源执行的操作

http0.9 只有 get 方法,http1.0 添加了 post 和 head 方法,http1.1 添加了 put,delete,options,trace,connect 方法,并允许开发者自行添加更多方法.

各个 http 方法的作用如下:

  • GET 命令服务器返回指定资源
  • HEAD 与 get 方法的作用类似,不同在于不要求服务器返回报文的主体.通常用于不获取报文主体的情况下,取到响应的首部
  • POST 命令服务器将报文主体中的数据传递给 URI 指定的资源,具体会对这些数据做什么则是服务器自己决定
  • PUT 命令服务器将报文主体中的数据设置为 URI 指定的资源,如果 URI 指定位置已经有数据存在,就用报文主体中的数据去代替已有的数据,如果资源尚未存在,则在 URI 指定的位置上创建一个新资源
  • DELETE 命令服务器删除 URI 指定的资源
  • TRACE 命令服务器返回请求本身.客户端可以知道介于它和服务器之间的其他服务器是如何处理请求的
  • OPTIONS 命令服务器返回它支持的 http 方法列表
  • CONNECT 命令服务器与客户端建立一个物理连接,通常用于设置 SSL 隧道以开启 HTTPS 功能
  • PATCH 命令服务器使用报文主体中的数据对 URI 指定的资源进行修改

1.5.2 安全的请求方法

如果一个 http 方法只要求服务器提供信息而不会对服务器的状态做任何修改,则是安全的(safe)

  • 安全的: GET,HEAD,OPTIONS,TRACE
  • 不安全的: POST,PUT,DELETE

1.5.3 幂等的请求方法

如果一个 http 方法在使用相同的数据进行第二次调用的时候,不会对服务器的状态造成任何改变,则是幂等的

安全的方法天生是幂等的(只要求返回数据,不对服务器修改)

PUT 和 DELETE 不安全,但是幂等,因为同样的数据第二次调用 PUT 或 DELETE 不会导致服务器状态改变

POST 既不安全也不幂等,多次用同样数据 POST 是否会改变服务器状态是由服务器自身决定的

1.5.4 浏览器对请求方法的支持

GET 是最基本的 http 方法,所有浏览器都支持

POST 从 HTML2.0 开始通过添加 HTML 表单实现,可以通过 form 标签的 method 属性来设置 get 或 post 请求方法

HTML 不支持除了 GET 和 POST 以外的方法

用户可以通过 XHR(XMLHttpRequest)来获得对 put 和 delete 方法的支持.XHR 是浏览器对象,是一系列浏览器 API,允许程序员向服务器发送 HTTP 请求.XHR 不止支持 xml,还支持包括 json 以及纯文本在内的任何格式的请求和响应

1.5.5 请求首部

http 请求的首部记录了请求本身以及客户端有关的信息.请求首部由任意多个用冒号分隔的纯文本键值对组成,最后以回车(CR)和换行(LF)结尾

HTTP1.1 RFC 对主要的 http 请求字段进行了标准化,过去非标准的 http 请求以 x-作为前缀.

大多数 http 请求首部是可选的,宿主(Host)首部字段是 http1.1 唯一强制要求的.根据请求方法不同,如果请求报文中包含可选的主体,那么请求首部还需要带有内容长度(Content-Length)字段或者传输编码(Transfer-Encoding)字段


1.6 HTTP 响应

http 响应报文是对 http 请求报文的回复.也是由一系列文本行组成:

  1. 一个状态行(包含状态码 status code 和相应的原因短语 reason phrase)
  2. 零个或任意个响应首部
  3. 一个空行
  4. 一个可选的报文主体

1.6.1 响应状态码

http 响应中的状态码表明了响应的类型,共用 5 种,以不同的数字作为前缀

1.6.2 响应首部

也是由冒号分隔的纯文本键值对组成,以回车(CR)和换行(LF)结尾

1.7 URI

Tim Berners-Lee 在创建万维网的同时,也引入了使用位置字符串表示互联网资源的概念.在 1994 年发表的 RFC1630 中,对统一资源标识符 URI 进行定义.

URI 包含统一资源名称 URN(使用字符串表示资源名字的方法)和统一资源定位符 URL(使用字符串表示资源所在位置的方法)

URI 的一般格式为: <方案名称>:<分层部分>[ ? <查询参数> ] [ # <片段> ]

方案名称记录了 URI 正在使用的方案,定义了 URI 其余部分的结构.

分层部分包含了资源的识别信息,这些信息以分层的方式组织.如果分层部分以双斜杠//开头,则说明包含了可选的用户信息,这些信息将以@符号结尾,后跟分层路径.不带用户信息的分层部分就是一个单纯的路径,每个路径由一连串的分段组成,各个分段之间以单斜线/分隔

URI 的各个部分中,只有方案名称和分层部分是必须的.查询参数是可选的,这些参数包含无法使用分层方式表示的其他信息.多个查询参数会被组织成一连串的键值对,各个键值对之间用&符合分隔

另一个可选部分: 片段,用#分隔,对 URI 定义的资源中的次级资源进行标识.如果 URI 包含查询参数,片段会放到查询参数后面,因为 URI 的片段是由客户端负责处理的

http://sausheong:password@www.example.com/docs/file?name=sausheong&location=singapore#summary

上面这个 URI 示例: 使用 HTTP 方案,方案后到@之间是用户名和密码,www.example.com/docs/file是分层的其他部分,还跟了查询参数name和location,以及片段summary

URL 是一个单独的字符串,不能包含空格,一些符号也有特殊的含义,如果需要带有这些符号的字符串,需要使用 URL 编码(也叫百分号编码),来对特殊符号进行转换

RFC 3986 定义了 URL 中的保留字符以及非保留字符,URL 编码会把保留字符转换成该字符在 ASCII 编码中对应的字节值,然后把字节值表示为两位长的十六进制数字,并在这个十六进制数字的前面加上%.

URL 编码举例: 空格的 ASCII 字节值是 32,转换为十六进制的 20,URL 编码为%20

1.8 HTTP/2 简介

是 http 协议的新版本,对性能十分关注.有 SPDY/2 协议(谷歌公司为了传输 web 内容而开发的一种开放的网络协议)改进而来

http/2 是二进制协议,但是无法使用 telnet 等应用程序直接发送 http/2 报文来进行调试

http1.x 在一个网络连接里每次只能发送单个请求,http/2 则是完全多路复用的(fully multiplexed).多个请求和响应可以在同一时间内使用同一个连接.http/2 还对首部进行压缩以减少要传送的数据量,并允许服务器将响应推送(push)到客户端

http/2 没有对 http 协议本身的语法进行修改.在 go1.6 版本中,用户使用 https 时会自动使用 http/2,而 1.6 之前的版本则在 golang.org/x/net/http2 包里实现了 http/2 协议

1.9 web 应用的各个组成部分

web 应用就是一个执行以下任务的程序:

  1. 通过 http 协议,以 http 请求报文的形式获取客户端输入
  2. 对 http 请求报文进行处理,并执行必要的操作
  3. 生成 html,并以 http 响应格式将其返回给客户端

为了完成这些任务,web 应用分成了处理器(handler)和模板引擎(template engine)

1.9.1 处理器

web 应用的处理器,要接收和处理客户端发来的请求,还需要调用模板引擎,然后由模板引擎生成 html 并将数据填充至将要回传给客户端的响应报文中

以 mvc 模式来说,处理器既是控制器(controller)又是模型(model).在理想的 mvc 模式实现中,控制器应该只包含路由(routing)代码以及 http 报文的解包和打包逻辑;而模型应该包含应用的逻辑以及数据

“模型-视图-控制器”模式,包含三个部分,模型用于表示底层的数据,视图则以可视化的方式向用户展示模型,控制器则会根据用户的输入对模型进行修改.模型发生变化时,视图会自动更新,展现模型的最新状态.

MVC 模式起源于桌面开发,但是在 web 应用也流行起来.在很多 MVC 框架中,模型一般通过结构体(struct)或对象(object)映射到数据库,视图则被渲染为 html,控制器负责对请求进行路由,并管理对模型的访问.

MVC 不是 web 开发的唯一方式,web 应用本质上时一个通过 http 协议与用户互动的程序,只要能实现这种互动,不是 mvc 模式也可以

为了防止模型过于臃肿,以及代码复用的需要,开发者有时会使用服务对象(service object)或者函数(function)对模型进行操作.服务对象不是 mvc 的一部分,但是通过把相同的逻辑放置到服务对象里,然后将服务对象用到不同模型上,可以有效避免在多个模型里复制相同代码的窘境

将控制器和模型进行合并,然后由处理器直接执行所有操作并向客户端返回响应的做法是可行且合理的

1.9.2 模板引擎

模板引擎通过模板和数据来生成最终的 html,可以分为静态和动态两种

  • 静态模板是一些夹杂者占位符的 html,模板引擎将占位符替换为相应的数据来生成 html,和 SSI 技术的概念相似.因为静态模板不含(或含少量)逻辑代码,所以也叫无逻辑模板
  • 动态模板除了包含 html 和占位符之外,还包含一些编程语言结构(条件语句、变量等)

1.10 Hello Go

first_webapp/server.go

package main

import (
    "fmt"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello World, %s!", r.URL.Path[1:])
}

func main() {
    http.HandleFunc("/", handler)
    http.ListenAndServe(":8080", nil)
}
# go install first_webapp
go run server.go

其他语言通常需要将 web 应用部署到应用服务器上,由应用服务器为 web 应用提供运行环境,但是 go 的 web 应用的运行环境由 net/http 包直接提供,这个包和应用的源代码会一起编译成一个可以快速部署的独立 web 应用

处理器通过用来表示指定事件被触发之后,复制对事件进行处理的回调函数,不过在 go 语言里,这个函数不是一个处理器,而是一个处理器函数

处理器函数参数 1 是 responseWriter 接口,可以将响应返回给客户端,参数 2 是 request 接口,在该处理器函数里,通过 Fprintf 向 responseWriter 里写入格式化字符串(包含从 request 里提取的路径信息)

2. ChitChat 论坛

论坛应用: 允许用户登录,允许发布新帖和回复

2.1 ChitChat 简介

2.2 应用设计

2.3 数据模型

使用 postgreSql 数据库

  • user 用户信息
  • session 论坛用户当前的登录会话
  • thread 论坛里的帖子,每个帖子记录了多个用户之间的对话
  • post 在帖子里添加的回复

2.4 请求的接收与处理

  1. 客户端发送请求到服务器一个 URL 上
  2. 服务器的多路复用器将请求重定向到对应处理器
  3. 处理器处理请求并执行操作
  4. 处理器调用模板引擎,生成 html 返回给客户端

2.4.1 多路复用器

package main

import "net/http"

func main() {
    // 默认的多路复用器
    mux := http.NewServeMux()
    files := http.FileServer(http.Dir("/public"))
    mux.Handle("/static/", http.StripPrefix("/static/", files))

    mux.HandleFunc("/", index)

    server := &http.Server{
        Addr:    "0.0.0.0:8080",
        Handler: mux,
    }
    server.ListenAndServe()
}

这里的 mux 是 net/http 提供的默认的多路复用器,它的 HandleFunc 方法接收 URL 和处理器名称,会将对应 URL 的请求转发给对应处理器处理,这里没有严格区分处理器函数和处理器的区别,后面会说

2.4.2 服务静态文件

files := http.FileServer(http.Dir("/public"))
mux.Handle("/static/", http.StripPrefix("/static/", files))

用 FileServer 指定了静态资源去哪个目录找,然后传递给多路复用器,当接收到/static/的请求时,多路复用器会去掉/static/然后去 public 找相应的文件

比如 http://localhost/static/html/index.html,会返回 public/html/index.html 这个文件

2.4.3 创建处理器函数

func index(w http.ResponseWriter, r *http.Request) {
    files := []string{"templates/layout.html", "templates/navbar.html", "templates/index.html"}
    templates := template.Must(template.ParseFiles(files...))
    if threads, err := data.Threads(); err != nil {
        templates.ExecuteTemplate(w, "layout", threads)
    }
}

还有其他的

跳过,源代码不全,而且没有给出源代码的 github 地址

第二部分 web 应用的基本组成部分

3. 接收请求

3.1 go 的 net/http 标准库

web 应用框架都认为自己提供的约定和模式是最佳实践,但是如果开发者没有正确理解,则可能发展为货物崇拜编程(cargo cult programming),比如 http 是无状态,所以才用 cookie 来在客户端实现数据持久化,用会话的方式在服务器上实现数据持久化,不了解这点的以为框架提供的接口就是唯一要做的事,而却不理解底层发生了什么

3.2 使用 go 构建服务器

3.2.1 go web 服务器

go 和其他编程语言不太一样,go 提供了一系列创建 web 服务器的标准库.创建服务器只需要调用 ListenAndServer 就行,如果传入的网络地址参数是空串,则会默认监听 80 端口,如果处理器参数为 nil,则使用默认的 DefaultServeMux 多路复用器

package main

import (
    "net/http"
)

func main() {
    // 最简单的web服务器
    http.ListenAndServe("", nil)
}

Server 可以设置更详细的配置

package main

import (
    "net/http"
)

func main() {
    server := http.Server{
        Addr:    "127.0.0.1:8080",
        Handler: nil,
    }
    server.ListenAndServe()
}

Server 可选的配置

type Server struct {
    Addr string
    Handler Handler
    TLSConfig *tls.Config
    ReadTimeout time.Duration
    ReadHeaderTimeout time.Duration
    WriteTimeout time.Duration
    IdleTimeout time.Duration
    MaxHeaderBytes int
    TLSNextProto map[string]func(*Server, *tls.Conn, Handler)
    ConnState func(net.Conn, ConnState)
    ErrorLog *log.Logger
}

3.2.2 通过 https 提供服务

当客户端和服务器共享私密信息时,一般会使用 https 进行加密,如果用了支付功能,甚至是强制的(根据支付卡行业数据安全标准),https 实际上是将 http 通信放到 ssl 上进行

package main

import (
    "net/http"
)

func main() {
    server := http.Server{
        Addr:    "127.0.0.1:8080",
        Handler: nil,
    }
    server.ListenAndServeTLS("cert.pem", "key.pem")
}

cert.pem 是 SSL 证书,key.pem 是服务器的私钥,生产环境中需要从 VeriSign、Thawte、Comodo SSL 这样的 CA 取得,如果是为了测试,使用自己生成的就行,可以使用 go 标准库生成

SSL,TLS 和 HTTPS

ssl 是一种通过公钥基础设施为通信双方提供数据加密和身份验证的协议,之后由 IETF 接收 SSL 后,改名为 LTS,HTTPS 就是 SSL 之上的 HTTP,实际上就是在 SSL/TLS 连接的上层进行 HTTP 通信

https 需要 SSL/TLS 证书进行加密和验证,SSL 证书存储在服务器上,是一种用 X.509 进行格式化的数据,为了保证可靠性,一般由证书分发机构 CA 签发,客户端向服务器发送请求,服务器会把证书和响应返回给客户端,客户端确认证书真实性后,生成一个随机密钥,使用证书里的公钥对随机密钥加密,产生对称密钥,在双方通信时,负责对通信进行加密

生成 SSL 证书和密钥并不复杂,SSL 证书实际上就是一个将扩展密钥用法设置成服务器身份验证操作的 X.509 证书

package main

import (
    "crypto/rand"
    "crypto/rsa"
    "crypto/x509"
    "crypto/x509/pkix"
    "encoding/pem"
    "math/big"
    "net"
    "os"
    "time"
)

func main() {
    max := new(big.Int).Lsh(big.NewInt(1), 128)
    serialNumber, _ := rand.Int(rand.Reader, max)
    subject := pkix.Name{
        Organization:       []string{"Manning Publications Co."},
        OrganizationalUnit: []string{"Books"},
        CommonName:         "Go Web Programming",
    }

    template := x509.Certificate{
        SerialNumber: serialNumber,
        Subject:      subject,
        NotBefore:    time.Now(),
        NotAfter:     time.Now().Add(365 * 24 * time.Hour),
        KeyUsage:     x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
        ExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
        IPAddresses:  []net.IP{net.ParseIP("127.0.0.1")},
    }

    pk, _ := rsa.GenerateKey(rand.Reader, 2048)

    derBytes, _ := x509.CreateCertificate(rand.Reader, &template, &template, &pk.PublicKey, pk)
    certOut, _ := os.Create("cert.pem")
    pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
    certOut.Close()

    keyOut, _ := os.Create("key.pem")
    pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(pk)})
    keyOut.Close()
}

首先,用一个 Certificate 结构对证书进行配置

    template := x509.Certificate{
        SerialNumber: serialNumber,
        Subject:      subject,
        NotBefore:    time.Now(),
        NotAfter:     time.Now().Add(365 * 24 * time.Hour),
        KeyUsage:     x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
        ExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
        IPAddresses:  []net.IP{net.ParseIP("127.0.0.1")},
    }

结构中的证书序列号(SerialNumber)用于记录 CA 分发的唯一号码(这里用随机整数替代).然后,创建了一个专有名称(distinguished name),设置为证书的标题(subject).证书的有效期设置为 1 年.keyUsage 和 ExtKeyUsage 指明该证书用于服务器身份验证.最后设置该证书只能在 IP 127.0.0.1 之上运行

生成了一个 RSA 私钥

pk, _ := rsa.GenerateKey(rand.Reader, 2048)

使用公钥,私钥,等参数生成 ssl 证书,创建一个经过 DER 编码格式化的字符串切片

derBytes, _ := x509.CreateCertificate(rand.Reader, &template, &template, &pk.PublicKey, pk)

将证书编码到 cert.pem

    certOut, _ := os.Create("cert.pem")
    pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
    certOut.Close()

pem 编码的方式将之前生成的密钥编码到 key.pem

    keyOut, _ := os.Create("key.pem")
    pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(pk)})
    keyOut.Close()

如果证书是由 CA 签发的,则证书文件中将包含服务器签名和 CA 签名,服务器签名在前,CA 签名在后

SSL 证书:

X.509 时国际电信联盟电信标准化部门(ITU-T)为公钥基础设施制定的一个标准,包含了公钥证书的标准格式.一个 x.509 证书是一个经过编码的 ASN.1 格式的电子文档,ASN.1 既是一个标准,也是一种表示法,描述了表示电信以及计算机网络数据的规则和结构.X.509 证书可以使用多种格式编码,其中一种是 BER,BER 指定了一种自解释并且自定义的格式用于对 ASN.1 数据结构进行编码.DER 是 BER 的子集,只提供了一种编码 SN.1 值的方法.ssl 证书可以以多种不同格式保存,其中就有 PEM,PEM 格式会对 DER 格式的 X.509 证书进行 Base64 编码,并且这种格式的文件都以—–BEGIN CERTIFICATE —–开头,以—–END CERTIFICATE —–结尾.

3.3 处理器和处理器函数

3.3.1 处理请求

在 GO 语言中,一个处理器就是一个拥有 ServeHTTP 方法的接口,这个方法接收 ResponseWriter 和*Request 两个参数.

DefaultServeMux 多路复用器既是 ServeMux 实例,同时也是 Handler 结构的实例,是一个特殊的处理器,也是多路复用器,它的处理器比较特殊,是用来转发 URL 请求到不同处理器的.

package main

import (
    "fmt"
    "net/http"
)

type MyHandler struct{}

func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello World!")
}
func main() {
    handler := MyHandler{}
    server := http.Server{
        Addr:    "127.0.0.1:8080",
        Handler: &handler,
    }
    server.ListenAndServe()
}

此时,无论访问 127.0.0.1:8080/xxx 都会显示 hello world

3.3.2 使用多个处理器

通过 http.Handler 将处理器绑定到 DefaultServeMux

package main

import (
    "fmt"
    "net/http"
)

type HelloHandler struct{}

func (h *HelloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello")
}

type WorldHandler struct{}

func (h *WorldHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "World")
}

func main() {
    hhandler := HelloHandler{}
    whandler := WorldHandler{}
    server := http.Server{
        Addr: "127.0.0.1:8080",
    }
    http.Handle("/hello", &hhandler)
    http.Handle("/world", &whandler)
    server.ListenAndServe()
}

3.3.3 处理器函数

是和处理器有相同行为的函数,与 ServeHttp 方法有相同的签名

package main

import (
    "fmt"
    "net/http"
)

func hello(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello")
}

func world(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "World")
}

func main() {
    server := http.Server{
        Addr: "127.0.0.1:8080",
    }
    http.HandleFunc("/hello", hello)
    http.HandleFunc("/world", world)
    server.ListenAndServe()
}

HandlerFunc 会将处理器函数 f 转换为带 f 方法的 Handler 结构,其实是简化了创建结构体然后再实现 ServeHTTP 方法的步骤

HandlerFunc 源码

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    DefaultServeMux.HandleFunc(pattern, handler)
}
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    if handler == nil {
        panic("http: nil handler")
    }
    mux.Handle(pattern, HandlerFunc(handler))
}
type muxEntry struct {
    h       Handler
    pattern string
}
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
}
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")
    }
    // 如果已经注册过该url
    if _, exist := mux.m[pattern]; exist {
        panic("http: multiple registrations for " + pattern)
    }

    // 如果还没有存放url的map
    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
    }
}

3.3.4 串联多个处理器和处理器函数

像日志记录、安全检查和错误处理这样的操作通常被称为横切关注点,为了防止代码重复和代码依赖问题,我们可以用串联技术分隔代码中的横切关注点

package main

import (
    "fmt"
    "net/http"
    "reflect"
    "runtime"
)

func hello(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello")
}

func log(h http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        name := runtime.FuncForPC(reflect.ValueOf(h).Pointer()).Name()
        fmt.Println("Handler function called - ", name)
        h(w, r)
    }
}

func main() {
    server := http.Server{
        Addr: "127.0.0.1:8080",
    }
    http.HandleFunc("/hello", log(hello))
    server.ListenAndServe()
}

还可以串联起更多,这种做法也称为管道处理


  转载请注明: malred-blog goweb编程

 上一篇
TCP/IP网络编程 TCP/IP网络编程
第一部分 开始网络编程1. 理解网络编程和套接字1.1 理解网络编程和套接字 c 语言的 print 和 scanf 进行控制台输入输出,控制台 IO 和文件 IO 很类似,而网络编程和文件 IO 也有很多相似之处 网络编程和套接字概要
下一篇 
前端跨界开发指南 前端跨界开发指南
基础篇1. Mock.js: 如何与后端潇洒分手 现在的开发基本上都是前后端分离,除了在性能要求较高的场景中才可能选择 SSR 服务端渲染,前端在前后端分离的情况下,专注渲染逻辑和样式和性能等,开发依赖数据驱动,如果后端还没写好数据,则需要
2023-06-17
  目录