golang Hook

简介

这篇文章主要是通过官方提供的 HTTP 追踪来学习使用 Hook 的编程思想。

简单来说 Hook 的编程思想跟事件驱动是类似的,通过预先保存一些要执行函数或方法,在满足某些条件的时候自动执行。
在了解使用 Go 语言编写 Hook 之前,最好先掌握 Context 的用法,关于 Context 的用法,请详见[另外一篇文章][]。

官方提供的 net/http/httptrace主要是用于追踪客户端的 Request 请求过程中发生的各种事件及行为,在标准库 net/http/httptrace/trace.go 中定义了一个叫 ClientTrace 的结构体,它包含了一系列的钩子函数 hooks 作为成员变量,如下:

// ClientTrace is a set of hooks to run at various stages of an outgoing HTTP request. 
type ClientTrace struct {
    GetConn func(hostPort string)
    GotConn func(GotConnInfo)
    PutIdleConn func(err error)

    GotFirstResponseByte func()
    Got100Continue func()

    DNSStart func(DNSStartInfo)
    DNSDone func(DNSDoneInfo)

    ConnectStart func(network, addr string)
    ConnectDone func(network, addr string, err error)

    TLSHandshakeStart func()
    TLSHandshakeDone func(tls.ConnectionState, error)
    WroteHeaders func()
    Wait100Continue func()
    WroteRequest func(WroteRequestInfo)
}

trace.go 还提供了一个 WithClientTrace() 包函数,用来把 ClientTrace 结构体中的钩子都保存(注册)到 Context 中去(因为 Context 提供 key/value 存储嘛),
key 就是一个叫 clientEventContextKey 的空结构体,value 是 nettrace 包中的 Trace 结构体,这个结构体作用跟 ClientTrace 一样,都是包含了一堆 hook 函数作为成员,
在这里它的目的只是封装下 ClientTrace 中的 hook 函数。最终, WithClientTrace() 会返回一个 context,它保存了上述的 hook 函数。

type clientEventContextKey struct{}
func WithClientTrace(ctx context.Context, trace *ClientTrace) context.Context {
    if trace == nil {
        panic("nil trace")
    }
    old := ContextClientTrace(ctx)
    trace.compose(old)

    ctx = context.WithValue(ctx, clientEventContextKey{}, trace)
    if trace.hasNetHooks() {
        nt := &nettrace.Trace{
            ConnectStart: trace.ConnectStart,
            ConnectDone:  trace.ConnectDone,
        }
        if trace.DNSStart != nil {
            nt.DNSStart = func(name string) {
                trace.DNSStart(DNSStartInfo{Host: name})
            }
        }
        if trace.DNSDone != nil {
            ...
        }
        ctx = context.WithValue(ctx, nettrace.TraceKey{}, nt)
    }
    return ctx
}

通过 ContextClientTrace() 的函数,可以把 ClientTrace 从 Context 中取出来。

// ContextClientTrace returns the ClientTrace associated with the
// provided context. If none, it returns nil.
func ContextClientTrace(ctx context.Context) *ClientTrace {
    trace, _ := ctx.Value(clientEventContextKey{}).(*ClientTrace)
    return trace
}

现在,我们知道,有了 WithClientTrace(),我们就可以把钩子函数保存在 Context 中了,现在,我们要把这些钩子函数挂到 Request 中去,该怎么弄?
很简单,通过 Request.WithContext() 把刚才赋值好的 Context 保存到 Request 中就可以了。

现在 Request 有了这些钩子函数,那么什么时候会被调用呢? 当然会 http.Client.Do(req) 的时候啦。

接下来我们通过一段实际的代码看看整个流程:

package main

import (
    "fmt"
    "log"
    "net/http"
    "net/http/httptrace"
)

// transport is an http.RoundTripper that keeps track of the in-flight
// request and implements hooks to report HTTP tracing events.
type transport struct {
    current *http.Request
}

// RoundTrip wraps http.DefaultTransport.RoundTrip to keep track
// of the current request.
func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) {
    t.current = req
    return http.DefaultTransport.RoundTrip(req)
}

// GotConn prints whether the connection has been used previously
// for the current request.
func (t *transport) GotConn(info httptrace.GotConnInfo) {
    fmt.Printf("Connection reused for %v? %v\n", t.current.URL, info.Reused)
}

func main() {
    t := &transport{}

    req, _ := http.NewRequest("GET", "https://google.com", nil)
    trace := &httptrace.ClientTrace{
        GotConn: t.GotConn,
    }
    req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))

    client := &http.Client{Transport: t}
    if _, err := client.Do(req); err != nil {
        log.Fatal(err)
    }
}

所有的钩子的调用,最终都会在 client.Do(req) 里面执行,我们看看是怎么执行的。

注意到这里的 transport 结构体,它其实是 RoundTripper 接口类型(在 client.go 中声明)的一个 implementer,这个 RoundTripper 实际只有一个方法:

// RoundTripper is an interface representing the ability to execute a
// single HTTP transaction, obtaining the Response for a given Request.
type RoundTripper interface {
    // RoundTrip executes a single HTTP transaction, returning
    // a Response for the provided Request.
    RoundTrip(*Request) (*Response, error)
}

在 client.Do() 中,会调用 client.send(),如下:

 resp, didTimeout, err = c.send(req, deadline)

c.send() 内部:

// didTimeout is non-nil only if err != nil.
func (c *Client) send(req *Request, deadline time.Time) (resp *Response, didTimeout func() bool, err error) {
    ...
    resp, didTimeout, err = send(req, c.transport(), deadline)
    ...
    return resp, nil, nil
}

send() 内部:

func send(ireq *Request, rt RoundTripper, deadline time.Time) (resp *Response, didTimeout func() bool, err error) {
    ...
    resp, err = rt.RoundTrip(req)
    ...
}

可见,最终调用了 rt.RoundTrip() 函数。也就是上述 main.go 中 transport 实现的 RoundTrip() 函数。

在 rt.RoundTrip() 里面,把 req 赋给了 DefaultTransport.RoundTrip(req),
这个 DefaultTransport 是包提供的一个 RoundTripper 的默认实现,

var DefaultTransport RoundTripper = &Transport{
    Proxy: ProxyFromEnvironment,
    DialContext: (&net.Dialer{
        Timeout:   30 * time.Second,
        KeepAlive: 30 * time.Second,
        DualStack: true,
    }).DialContext,
    MaxIdleConns:          100,
    IdleConnTimeout:       90 * time.Second,
    TLSHandshakeTimeout:   10 * time.Second,
    ExpectContinueTimeout: 1 * time.Second,
}

然后,在它的 RoundTrip() 函数里面最终会调用上述的钩子函数。

// RoundTrip implements the RoundTripper interface.
//
// For higher-level HTTP client support (such as handling of cookies
// and redirects), see Get, Post, and the Client type.
func (t *Transport) RoundTrip(req *Request) (*Response, error) {
    t.nextProtoOnce.Do(t.onceSetNextProtoDefaults)
    ctx := req.Context()
    trace := httptrace.ContextClientTrace(ctx)
    
    for {
        treq := &transportRequest{Request: req, trace: trace}
        cm, err := t.connectMethodForRequest(treq)
        ...
        pconn, err := t.getConn(treq, cm)
    }
}

解析:

通过调用 httptrace.ContextClientTrace(ctx) 把 context 中的钩子函数都取出来,再在 t.getConn() 中调用钩子函数,如下:

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

推荐阅读更多精彩内容