2024年Go最全Go分布式爬虫学习笔记(十四)_golang分布式爬虫(1),Golang开发者必看

img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

如果远程服务 F 在 200ms 后没有返回,所有协程都需要感知到并快速关闭。

而使用 Context 标准库就是当前处理这种协程级联退出的标准做法。

Context 的使用方法。

Context 标准库中重要的结构 context.Context 其实是一个接口,它提供了 Deadline、Done、Err、Value 这 4 种方法:

type Context interface {
  Deadline() (deadline time.Time, ok bool)
  Done() <-chan struct{}
  Err() error
  Value(key interface{}) interface{}
}

  • Deadline 方法用于返回 Context 的过期时间。Deadline 第一个返回值表示 Context 的过期时间,第二个返回值表示是否设置了过期时间,如果多次调用 Deadline 方法会返回相同的值。
  • Done 是使用最频繁的方法,它会返回一个通道。一般的做法是调用者在 select 中监听该通道的信号,如果该通道关闭则表示服务超时或异常,需要执行后续退出逻辑。多次调用 Done 方法会返回相同的通道。
  • 通道关闭后,Err 方法会返回退出的原因。
  • Value 方法返回指定 key 对应的 value,这是 Context 携带的值。key 必须是可比较的,一般的用法 key 是一个全局变量,通过context.WithValue 将 key 存储到 Context 中,并通过Context.Value 方法取出。

Context 接口中的这四个方法可以被多次调用,其返回的结果相同。同时,Context 的接口是并发安全的,可以被多个协程同时使用。

context.Value

  • 一般在远程过程调用中使用
    例如存储分布式链路跟踪的 traceId 或者鉴权相关的信息,并且该值的作用域在请求结束时终结。
    同时 key 必须是访问安全的,因为可能有多个协程同时访问它。
  • 如下所示,withAuth 函数是一个中间件,它可以让我们在完成实际的 HTTP 请求处理前进行 hook。
    在这个例子中,我们获取了 HTTP 请求 Header 头中的鉴权字段 Authorization,并将其存入了请求的上下文 Context 中。
    而实际的处理函数 Handle 会从 Context 中获取并验证用户的授权信息,以此判断用户是否已经登录。
const TokenContextKey = "MyAppToken"

// 中间件
func WithAuth(a Authorizer, next http.Handler) http.Handler {
  return http.HandleFunc(func(w http.ResponseWriter, r \*http.Request) {
    auth := r.Header.Get("Authorization")
    if auth == "" {
      next.ServeHTTP(w, r) // 没有授权
      return
    }
    token, err := a.Authorize(auth)
    if err != nil {
      http.Error(w, err.Error(), http.StatusUnauthorized)
      return
    }
    ctx := context.WithValue(r.Context(), TokenContextKey, token)
    next.ServeHTTP(w, r.WithContext(ctx))
  })
}

// HTTP请求实际处理函数
func Handle(w http.ResponseWriter, r \*http.Request) {
  // 获取授权
  if token := r.Context().Value(TokenContextKey); token != nil {
    // 用户登录
  } else {
    // 用户未登录
  }
}

  • Context 是一个接口,这意味着需要有对应的具体实现。用户可以自己实现 Context 接口,并严格遵守 Context 接口规定的语义。当然,我们使用得最多的还是 Go 标准库中的实现。

context

  • 当我们调用 context.Background 函数或 context.TODO 函数时,会返回最简单的 Context 实现。
  • context.Background 返回的 Context 一般是作为根对象存在,不具有任何功能,不可以退出,也不能携带值。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context

  • WithCancel 函数会返回一个子 Context 和 cancel 方法。子 Context 会在两种情况下触发退出:一种情况是调用者主动调用了返回的 cancel 方法;另一种情况是当参数中的父 Context 退出时,子 Context 将级联退出。
  • WithTimeout 函数指定超时时间。当超时发生后,子 Context 将退出。因此,子 Context 的退出有三种时机,一种是父 Context 退出;一种是超时退出;最后一种是主动调用 cancel 函数退出。
  • WithDeadline 和 WithTimeout 函数的处理方法相似,不过它们的参数指定的是最后到期的时间。
  • WithValue 函数会返回带 key-value 的子 Context。

Context 最佳实践

在网络连接到请求处理的多个阶段,都可能有相对应的超时时间。

以 HTTP 请求为例,http.Client 有一个参数 Timeout 用于指定当前请求的总超时时间,它包括从连接、发送请求、到处理服务器响应的时间的总和。

c := &http.Client{
    Timeout: 15 \* time.Second,
}
resp, err := c.Get("<https://baidu.com/>")

标准库 client.Do 方法内部会将超时时间换算为截止时间并传递到下一层。setRequestCancel 函数内部则会调用 context.WithDeadline ,派生出一个子 Context 并赋值给 req 中的 Context。

func (c \*Client) do(req \*Request) (retres \*Response, reterr error) {
  ...
  deadline      = c.deadline()
  c.send(req, deadline);
}

func setRequestCancel(req \*Request, rt RoundTripper, deadline time.Time) {
    req.ctx, cancelCtx = context.WithDeadline(oldCtx, deadline)
    ...
}

在获取连接时,如果从闲置连接中找不到连接,则需要陷入 select 中去等待。如果连接时间超时,req.Context().Done() 通道会收到信号立即退出。在实际发送数据的 transport.roundTrip 函数中.

func (t \*Transport) getConn(treq \*transportRequest, cm connectMethod) (pc \*persistConn, err error){
...
select {
  case <-w.ready:
    return w.pc, w.err
  case <-req.Cancel:
    return nil, errRequestCanceledConn
  case <-req.Context().Done():
    return nil, req.Context().Err()
    return nil, err
  }
}

获取 TCP 连接需要调用 sysDialer.dialSerial 方法,dialSerial 的功能是从 addrList 地址列表中取出一个地址进行连接,如果与任一地址连接成功则立即返回。代码如下所示,不出所料,该方法的第一个参数为上游传递的 Context。

// net/dial.go
func (sd \*sysDialer) dialSerial(ctx context.Context, ras addrList) (Conn, error) {
  for i, ra := range ras {
    // 协程是否需要退出
    select {
    case <-ctx.Done():
      return nil, &OpError{Op: "dial", Net: sd.network, Source: sd.LocalAddr, Addr: ra, Err: mapErr(ctx.Err())}
    default:
    }

    dialCtx := ctx

     // 是否设置了超时时间
    if deadline, hasDeadline := ctx.Deadline(); hasDeadline {
      // 计算连接的超时时间
      partialDeadline, err := partialDeadline(time.Now(), deadline, len(ras)-i)
      if err != nil {
        // 已经超时了.
        if firstErr == nil {
          firstErr = &OpError{Op: "dial", Net: sd.network, Source: sd.LocalAddr, Addr: ra, Err: err}
        }
        break
      }
      // 派生出新的context,传递给下游
      if partialDeadline.Before(deadline) {
        var cancel context.CancelFunc
        dialCtx, cancel = context.WithDeadline(ctx, partialDeadline)
        defer cancel()
      }
    }

    c, err := sd.dialSingle(dialCtx, ra)
    ...
}

我们来看看 dialSerial 函数几个比较有代表性的 Context 用法。

  • 首先,第 3 行代码遍历地址列表时,判断 Context 通道是否已经退出,如果没有退出,会进入到 select 的 default 分支。如果通道已经退出了,则直接返回,因为继续执行已经没有必要了。
  • 接下来,第 14 行代码通过 ctx.Deadline() 判断是否传递进来的 Context 有超时时间。
    如果有超时时间,我们需要协调好后面每一个连接的超时时间。
    例如,我们总的超时时间是 600ms,一共有 3 个连接,那么每个连接分到的超时时间就是 200ms,这是为了防止前面的连接过度占用了时间。partialDeadline 会帮助我们计算好每一个连接的新的到期时间,如果该到期时间小于总到期时间,我们会派生出一个子 Context 传递给 dialSingle 函数,用于控制该连接的超时。
  • dialSingle 函数中调用了 ctx.Value,用来获取一个特殊的接口 nettrace.Trace。nettrace.Trace 用于对网络包中一些特殊的地方进行 hook。dialSingle 函数作为网络连接的起点,如果上下文中注入了 trace.ConnectStart 函数,则会在 dialSingle 函数之前调用 trace.ConnectStart 函数,如果上下文中注入了 trace.ConnectDone 函数,则会在执行 dialSingle 函数之后调用 trace.ConnectDone 函数。
func (sd \*sysDialer) dialSingle(ctx context.Context, ra Addr) (c Conn, err error) {
  trace, \_ := ctx.Value(nettrace.TraceKey{}).(\*nettrace.Trace)
  if trace != nil {
    raStr := ra.String()
    if trace.ConnectStart != nil {
      trace.ConnectStart(sd.network, raStr)
    }
    if trace.ConnectDone != nil {
      defer func() { trace.ConnectDone(sd.network, raStr, err) }()
    }
  }
  la := sd.LocalAddr
  switch ra := ra.(type) {
  case \*TCPAddr:
    la, \_ := la.(\*TCPAddr)
    // tcp连接
    c, err = sd.dialTCP(ctx, la, ra)
    ...  
}

由于标准库为我们提供了 Timeout 参数,我们在项目中实践超时控制就容易多了。只要在 BrowserFetch 结构体中增加 Timeout 超时参数,然后设置超时参数到 http.Client 中就大功告成了。

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

15702682112)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值