go web 框架: https://github.com/mingrammer/go-web-framework-stars
课程资料:https://github.com/gohade/hade hade
说明文档:http://hade.funaio.cn/
框架倾向性:一类是追求运行性能如Gin ,一类是追求开发效率如beego。
产品共性:开发、迭代速度快 思考好问题:什么应该是写业务的人负责的?什么应该是做架构的人负责的?
实战第一关:搭建出 Web 框架最核心的设计部分
实战第二关:如果要搭建出一个“一切皆服务”的框架应该如何设计,用gin实战
实战第三关:增加不同的周边功能 实战第四关:框架应用开发一个统计管理后台,使用 vue-element-admin 来做前端封面,开发具体的统计展示和计算业务。
在任何领域做到第一名的产品基本上都有一个共性:开发、迭代速度快。这就和古龙小说中评价天下侠士的武功一样,唯快不破。所以作为开发必备的框架,在提效上尤为重要。这就要求好的框架要能区分清楚业务团队和架构团队的边界,什么应该是写业务的人负责的?什么应该是做架构的人负责的?写架构的同学,做好框架的底层封装。而写业务的同学可以从底层实现中释放出来,专注于业务逻辑,遇到任何底层问题,在框架中都有简单易用的封装可用,框架中的每一个类、每一个服务接口都在告诉你,要完成这个功能,只需要这样使用,无需更多的操作。
Go 语言中的 Goroutine 设计,提供了“一个请求一个协程”的请求模型,对比 PHP 的“一个请求一个进程”的模型,能有效提升后端的资源占用和调度负载;另外,Go 的 Runtime 机制让运行程序不再依赖各种的环境和库,将 Web 服务的部署和搭建变得简单高效;而 Go 提供的交叉编译、数据结构、channel 等语言级别特性,都让“处理 Web 请求”这个事情变得非常简单。
Go 语言中的 Goroutine 设计,提供了“一个请求一个协程”的请求模型,对比 PHP 的“一个请求一个进程”的模型,能有效提升后端的资源占用和调度负载;另外,Go 的 Runtime 机制让运行程序不再依赖各种的环境和库,将 Web 服务的部署和搭建变得简单高效;而 Go 提供的交叉编译、数据结构、channel 等语言级别特性,都让“处理 Web 请求”这个事情变得非常简单。
这里我教给你一个快速掌握代码库的技巧:库函数 > 结构定义 > 结构函数。
简单来说,就是当你在阅读一个代码库的时候,不应该从上到下阅读整个代码文档,而应该先阅读整个代码库提供的对外库函数(function),再读这个库提供的结构(struct/class),最后再阅读每个结构函数(method)。
为什么要这么学呢?因为这种阅读思路和代码库作者的思路是一致的。首先搞清楚这个库要提供什么功能(提供什么样的对外函数),然后为了提供这些功能,我要把整个库分为几个核心模块(结构),最后每个核心模块,我应该提供什么样的能力(具体的结构函数)来满足我的需求。
你直接通过 go doc net/http | grep "^func"
命令行能查询出 net/http 库所有的对外库函数:
func CanonicalHeaderKey(s string) string
func DetectContentType(data []byte) string
func Error(w ResponseWriter, error string, code int)
func Get(url string) (resp *Response, err error)
func Handle(pattern string, handler Handler)
func HandleFunc(pattern string, handler func(ResponseWriter, *Request))
func Head(url string) (resp *Response, err error)
func ListenAndServe(addr string, handler Handler) error
func ListenAndServeTLS(addr, certFile, keyFile string, handler Handler) error
func MaxBytesReader(w ResponseWriter, r io.ReadCloser, n int64) io.ReadCloser
func NewRequest(method, url string, body io.Reader) (*Request, error)
func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error)
func NotFound(w ResponseWriter, r *Request)
func ParseHTTPVersion(vers string) (major, minor int, ok bool)
func ParseTime(text string) (t time.Time, err error)
func Post(url, contentType string, body io.Reader) (resp *Response, err error)
func PostForm(url string, data url.Values) (resp *Response, err error)
func ProxyFromEnvironment(req *Request) (*url.URL, error)
func ProxyURL(fixedURL *url.URL) func(*Request) (*url.URL, error)
func ReadRequest(b *bufio.Reader) (*Request, error)
func ReadResponse(r *bufio.Reader, req *Request) (*Response, error)
func Redirect(w ResponseWriter, r *Request, url string, code int)
func Serve(l net.Listener, handler Handler) error
func ServeContent(w ResponseWriter, req *Request, name string, modtime time.Time, ...)
func ServeFile(w ResponseWriter, r *Request, name string)
func ServeTLS(l net.Listener, handler Handler, certFile, keyFile string) error
func SetCookie(w ResponseWriter, cookie *Cookie)
func StatusText(code int) string
在这个库提供的方法中,我们去掉一些 New 和 Set 开头的函数,因为你从命名上可以看出,这些函数是对某个对象或者属性的设置。剩下的函数大致可以分成三类:
为服务端提供创建 HTTP 服务的函数,名字中一般包含 Serve 字样,比如 Serve、ServeFile、ListenAndServe 等。
为客户端提供调用 HTTP 服务的类库,以 HTTP 的 method 同名,比如 Get、Post、Head 等。
提供中转代理的一些函数,比如 ProxyURL、ProxyFromEnvironment 等。
我们现在研究的是,如何创建一个 HTTP 服务,所以关注包含 Serve 字样的函数就可以了。
// 通过监听的URL地址和控制器函数来创建HTTP服务
func ListenAndServe(addr string, handler Handler) error{}
// 通过监听的URL地址和控制器函数来创建HTTPS服务
func ListenAndServeTLS(addr, certFile, keyFile string, handler Handler) error{}
// 通过net.Listener结构和控制器函数来创建HTTP服务
func Serve(l net.Listener, handler Handler) error{}
// 通过net.Listener结构和控制器函数来创建HTTPS服务
func ServeTLS(l net.Listener, handler Handler, certFile, keyFile string) error{}
然后,我们过一遍这个库提供的所有 struct,看看核心模块有哪些 然后,我们过一遍这个库提供的所有 struct,看看核心模块有哪些
你可以看到整个库最核心的几个结构:
type Client struct{ ... }
type Cookie struct{ ... }
type ProtocolError struct{ ... }
type PushOptions struct{ ... }
type Request struct{ ... }
type Response struct{ ... }
type ServeMux struct{ ... }
type Server struct{ ... }
type Transport struct{ ... }
看结构的名字或者 go doc 查看结构说明文档,能逐渐了解它们的功能:
Client 负责构建 HTTP 客户端;
Server 负责构建 HTTP 服务端;
ServerMux 负责 HTTP 服务端路由;
Transport、Request、Response、Cookie 负责客户端和服务端传输对应的不同模块。
现在通过库方法(function)和结构体(struct),我们对整个库的结构和功能有大致印象了。整个库承担了两部分功能,一部分是构建 HTTP 客户端,一部分是构建 HTTP 服务端。构建的 HTTP 服务端除了提供真实服务之外,也能提供代理中转服务,它们分别由 Client 和 Server 两个数据结构负责。除了这两个最重要的数据结构之外,HTTP 协议的每个部分,比如请求、返回、传输设置等都有具体的数据结构负责。
HTTP 库提供 FileServer 来封装对文件读取的 HTTP 服务。实现代码也非常简单:
fs := http.FileServer(http.Dir("/home/bob/static"))
http.Handle("/static/", http.StripPrefix("/static", fs))
请问它的主流程逻辑是什么?你认为其中最关键的节点是什么?
作者回复: 你好,你的逻辑是正确的,不过可能过多关注分支细节。在使用思维导图的时候,如果对于比较复杂的逻辑,我们需要分析哪些是关键节点,哪些是非关键节点。
比如FileServer, 其关键点有两个:
1 fileHandler 我们能和ListenAndServe 连接起来,它提供了ServeHTTP的方法, 这个是请求处理的入口函数
2 FileServer 最本质的函数是封装了io.CopyN,基本逻辑是:
如果是读取文件夹,则遍历文件夹内所有文件,将文件名直接输出返回值。
如果是读取文件,则设置文件的阅读指针(如果需要多次读取文件,创建goroutine,且为每个goroutine创建阅读指针),使用io.CopyN读取文件内容输出返回值。
在这个逻辑链条中,每个本地处理逻辑,或者下游服务请求节点,都有可能存在超时问题。而对于 HTTP 服务而言,超时往往是造成服务不可用、甚至系统瘫痪的罪魁祸首。系统瘫痪也就是我们俗称的雪崩,某个服务的不可用引发了其他服务的不可用。比如上图中,如果服务 d 超时,导致请求处理缓慢甚至不可用,加剧了 Goroutine 堆积,同时也造成了服务 a/b/c 的请求堆积,Goroutine 堆积,瞬时请求数加大,导致 a/b/c 的服务都不可用,整个系统瘫痪,怎么办?最有效的方法就是从源头上控制一个请求的“最大处理时长”,所以,对于一个 Web 框架而言,“超时控制”能力是必备的。今天我们就用 Context 为框架增加这个能力。
context 标准库设计思路
如何控制超时,官方是有提供 context 标准库作为解决方案的,但是由于标准库的功能并不够完善,一会我们会基于标准库,来根据需求自定义框架的 Context。所以理解其背后的设计思路就可以了。为了防止雪崩,context 标准库的解决思路是:在整个树形逻辑链条中,用上下文控制器 Context,实现每个节点的信息传递和共享。具体操作是:用 Context 定时器为整个链条设置超时时间,时间一到,结束事件被触发,链条中正在处理的服务逻辑会监听到,从而结束整个逻辑链条,让后续操作不再进行。明白操作思路之后,我们深入 context 标准库看看要对应具备哪些功能。按照上一讲介绍的了解标准库的方法,我们先通过 go doc context | grep “^func” 看提供了哪些库函数(function):
// 创建退出 Context
func WithCancel(parent Context) (ctx Context, cancel CancelFunc){}
// 创建有超时时间的 Context
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc){}
// 创建有截止时间的 Context
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc){}
我们先通过 go doc context | grep “^type” ,搞清楚 Context 的结构定义和函数句柄,再来解答这个问题。
type Context interface {
// 当 Context 被取消或者到了 deadline,返回一个被关闭的 channel
Done() <-chan struct{}
...
}
//函数句柄
type CancelFunc func()
这个库虽然不大,但是设计感强,比较抽象,并不是很好理解。所以这里,我把 Context 的其他字段省略了。现在,我们只理解核心的 Done() 方法和 CancelFunc 这两个函数就可以了。
在树形逻辑链条上,一个节点其实有两个角色:一是下游树的管理者;二是上游树的被管理者,那么就对应需要有两个能力:一个是能让整个下游树结束的能力,也就是函数句柄 CancelFunc;另外一个是在上游树结束的时候被通知的能力,也就是 Done() 方法。同时因为通知是需要不断监听的,所以 Done() 方法需要通过 channel 作为返回值让使用方进行监听。
package main
import (
"context"
"fmt"
"time"
)
const shortDuration = 1 * time.Millisecond
func main() {
// 创建截止时间
d := time.Now().Add(shortDuration)
// 创建有截止时间的 Context
ctx, cancel := context.WithDeadline(context.Background(), d)
defer cancel()
// 使用 select 监听 1s 和有截止时间的 Context 哪个先结束
select {
case <-time.After(1 * time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err())
}
}
其实每个连接的 Context 都是基于 baseContext 复制来的。对应到代码中就是,在为某个连接开启 Goroutine 的时候,为当前连接创建了一个 connContext,这个 connContext 是基于 server 中的 Context 而来,而 server 中 Context 的基础就是 baseContext。
BaseContext 是整个 Context 生成的源头,如果我们不希望使用默认的 context.Backgroud(),可以替换这个源头。而在每个连接生成自己要使用的 Context 时,会调用 ConnContext ,它的第二个参数是 net.Conn,能让我们对某些特定连接进行设置,比如要针对性设置某个调用 IP。
type Server struct {
...
// BaseContext 用来为整个链条创建初始化 Context
// 如果没有设置的话,默认使用 context.Background()
BaseContext func(net.Listener) context.Context{}
// ConnContext 用来为每个连接封装 Context
// 参数中的 context.Context 是从 BaseContext 继承来的
ConnContext func(ctx context.Context, c net.Conn) context.Context{}
...
}