简单的GO应用:以一个爬虫脚本为例

一个GO爬虫编写的过程

边学GO语言边写。项目地址:PixivDownloader

协程

  • 定义:协程(Coroutine)是一种比线程更轻量级的并发单元。协程独立执行,但可以在某些点暂停并交出控制权。
  • 协程和线程的区别:与传统线程不同,协程在执行时不会强制切换上下文,调度更加灵活和高效。协程更像一个 “子程序”,可以并发运行,也可以通过通道与其他协程同步。

在 Go 中,协程被称为 Goroutine。启动一个 Goroutine 只需要简单地在函数调用前加 go 关键字,例如:go func() { … }。这使得并发编程变得极为方便。 “不要用线程,用goroutine”是Go的设计理念。

脚本需求分析

  • 设置Headers
  • 使用代理服务器
  • 下载作品
  • 多线程下载作品
  • 获取作者的作品列表
  • 多线程获取作者的作品列表
  • 重试下载
  • 多线程重试下载

按逻辑顺序分为三步:

  1. 作者作品列表的获取
  2. 作品的获取(失败即放入重试列表)
  3. 重试列表的重新获取

脚本结构设计

  • 由于Go存在着Goroutine,这使得多线程非常简单。可以直接使用go关键字开一个Goroutine。
  • 多个线程间通过Go Channel来共享和传递数据,从而并发协作。可以直接用make()函数初始一个Channel。
  • Channel(通道)是一种用于在不同的 Goroutine 之间传递数据的管道。Channel 是 Go 并发编程的核心,用于实现协程之间的通信和数据共享。

开三个channel,AuthorChannel储存作者,WorkChannel储存作品,RetryChannel储存下载失败的作品;

  1. 向AuthorChannel送入所有Author
  2. 开多个Goroutine进行作品链接的获取 AuthorChannel->WorkChannel
  3. 开多个Goroutine进行作品的下载 WorkChannel->RetryChannel
  4. 开多个Goroutine进行失败作品的重试

Go的特色

  • Go 中采用显式错误处理,每个步骤都需要检查并处理错误,错误信息详细,便于定位问题。在编写新函数时,返回error。如果error!=nil,则程序出了问题
  • Go的JSON解析有一些独特的特点和机制,使其在处理JSON时表现出简洁和高效的特性。Go支持在结构体定义中使用标签(struct tags)来定制JSON字段的解析方式。例如,可以通过设置标签来控制JSON字段名、忽略某个字段、设置字段的自定义名称等。

问题与讨论

问题1:为什么使用三个for循环,而不是直接用三个匿名函数开三个不同的Goroutine,再在这些Goroutine中开新的Goroutine进行功能模块的运行?

  • 使用三个 for 循环的设计,目的是逐步、有序地处理每个功能模块的数据流,确保每个阶段的数据处理完全结束后再进入下一阶段。相比直接启动三个匿名 goroutine 并在每个匿名函数中运行子任务,分阶段的 for 循环设计更适合这个场景。
  • 如果直接使用三个匿名函数开三个不同的Goroutine,不做顺序处理,这三个Goroutine就是同时进行的。从结构设计来看,这会造成生产者协程和消费者协程同时进行,导致程序意外终止。顺序执行可以确保生产者和消费者的顺序是正常有序的。
  • 在Goroutine中嵌套Goroutine,会导致代码的层次变得复杂难读,不利于维护。过多嵌套的 goroutine 也可能带来意外的同步、资源竞争等问题。

问题2:为什么使用匿名函数开Goroutine来处理下载?

  • 闭包捕获问题:在 Go 中,for 循环中的循环变量是共享的。当我们在 for 循环中启动多个 goroutine 时,所有的 goroutine 会共享同一个 author 变量的引用。如果不将 author 作为参数传入匿名函数,匿名函数中使用的 author 实际上是循环变量的地址引用,而不是每次循环的具体值。这样会导致每个 goroutine 可能最终都引用到 for 循环结束后的同一个 author 值。
  • 并且,这样可以分开功能函数和多线程的功能,也是一定的“高内聚低耦合”,保证了代码的可读性和安全性。

问题3:是否有更加简单的下载/爬取方式?

  • Colly是一个Go的web自动化包,可以实现多线程访问网站等功能。
  • Colly本身的多线程会和Goroutine重复,使代码变得复杂;
  • 单线程的Colly消耗的资源相较HTTP多,会拖慢运行速度。