golang Hook

juniway · · 7910 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

简介

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

在了解使用 Go 语言编写 Hook 之前,最好先掌握 Context 的用法, go 1.7 中 context 已经进入标准库 context,直接 import "context" 就可以使用。
在标准库 context.go 中, Context 定义为一个 interface{} 接口类型。

// A Context carries a deadline, a cancelation signal, and other values across
// API boundaries.
//
// Context's methods may be called by multiple goroutines simultaneously.
type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}

标准库用了一个 emptyCtx 来作为默认的 implementer

type emptyCtx int
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
    return
}

func (*emptyCtx) Done() <-chan struct{} {
    return nil
}
...

由于默认的 emptyCtx 是未导出的,因此,标准库又提供了两个公共的函数 Background 和 TODO 来访问这个 implementer。

var (
    background = new(emptyCtx)
    todo       = new(emptyCtx)
)

func Background() Context {
    return background
}

func TODO() Context {
    return todo
}

当然,还提供了一个 CancelFunc 的回调函数原型,WithCancel() 函数可用来给 Context 注册一个 cancel 函数。
还提供了另外几个其他的 With 函数,分别是 WithDeadline(), WithTimeout() 和 WithValue()。
WithDeadline 和 WithTimeout 都是给 parent Context 加上一个 timeout 并返回一个 cancel func。

WithValue() 复制原 context,并保存一对 key、value 到副本中。

func WithValue(parent Context, key, val interface{}) Context {
    if key == nil {
        panic("nil key")
    }
    if !reflect.TypeOf(key).Comparable() {
        panic("key is not comparable")
    }
    return &valueCtx{parent, key, val}
}

由于 Context 本身是 interface{} 接口类型,没法保存 key/value,其实质是通过一个包的私有类型 valueCtx 来保存的,如下:

// A valueCtx carries a key-value pair. It implements Value for that key and
// delegates all other calls to the embedded Context.
type valueCtx struct {
    Context
    key, val interface{}
}

可见,通过传入一个 parent context,以及 key/value,最终会保存在 valueCtx 中并返回。

再来看 http.Request,这是一个结构体,其有一个成员就是 ctx Context 接口类型(私有成员)。通过包方法 http.NewRequest() 来创建。默认 Context 成员未被初始化,也就是 nil。
request 提供了一个公共方法 Context() 来访问私有的 ctx 成员。

func (r *Request) Context() context.Context {
    if r.ctx != nil {
        return r.ctx
    }
    return context.Background()
}

可见,默认创建好的 request, 其 ctx 是 Background() 。

Request 结构体提供了 WithContext(ctx context.Context) 方法用来给自己的 ctx 成员赋值。

// WithContext returns a shallow copy of r with its context changed
// to ctx. The provided ctx must be non-nil.
func (r *Request) WithContext(ctx context.Context) *Request {
    if ctx == nil {
        panic("nil context")
    }
    r2 := new(Request)
    *r2 = *r
    r2.ctx = ctx
    return r2
}

由于 Request 是客户端的行为,为了追踪 Request 请求过程中发生的各种事件及行为,标准库 trace.go 中提供了一个叫 ClientTrace 的结构体,它包含了一系列的钩子函数 hooks,如下:

// ClientTrace is a set of hooks to run at various stages of an outgoing HTTP request. 
type ClientTrace struct {
    // GetConn is called before a connection is created or retrieved from an idle pool. 
    GetConn func(hostPort string)

    // GotConn is called after a successful connection is obtained.
    GotConn func(GotConnInfo)

    // PutIdleConn is called when the connection is returned to the idle pool. 
    PutIdleConn func(err error)

    GotFirstResponseByte func()
    Got100Continue func()

    // DNSStart is called when a DNS lookup begins.
    DNSStart func(DNSStartInfo)

    // DNSDone is called when a DNS lookup ends.
    DNSDone func(DNSDoneInfo)

    // ConnectStart is called when a new connection's Dial begins.
    ConnectStart func(network, addr string)

    // ConnectDone is called when a new connection's Dial completes.
    ConnectDone func(network, addr string, err error)

    // TLSHandshakeStart is called when the TLS handshake is started. 
    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() 保存到 Request 中就可以了。

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

接下来我们看看整个流程:

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() 里面执行,我们看看是怎么执行的。

注意到这里的 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() 内部:

```go
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() 中调用钩子函数,如下:



有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:juniway

查看原文:golang Hook

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

7910 次点击  
加入收藏 微博
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传