Tutorial6 min read

Web Scraping With Go: Colly and goquery

Short answer

Web scraping in Go uses two libraries for most jobs: goquery, which parses HTML and selects elements with jQuery-style CSS selectors, and Colly, a scraping and crawling framework that adds callbacks, link following, rate limits, parallelism and proxy support on top of the same selectors. For pages that need JavaScript, chromedp or rod drive a headless Chrome from Go. A Colly scraper that walks every page of a catalog is about 30 lines and compiles to one binary. ProxyField's residential proxies plug into Colly with c.SetProxy(), with 412M+ IPs in 195+ countries from $1.95/GB; start with the Easy Trial: 1 GB for $2.95.

residential IPs
412M+
from, traffic never expires
$1.95/GB
Easy Trial
1 GB for $2.95
On this page 10 sections
  1. Is Go good for web scraping?
  2. Go web scraping libraries
  3. Set up a Go scraping project
  4. Scraping with goquery and net/http
  5. A Colly scraper and crawler, step by step
  6. Colly vs goquery
  7. Pages that need JavaScript: chromedp
  8. Crawling a site's URLs with gospider
  9. Using proxies in Go scrapers
  10. Web Scraping With Go FAQ

Is Go good for web scraping?

Yes, especially for large crawls. Go's goroutines make thousands of concurrent requests light on memory and simple to write, the standard library's net/http is production-grade, and a scraper compiles to a single static binary that is easy to run on a server or in a container. Python has more scraping tutorials and libraries and is quicker for one-off scripts; Go pays off when a scraper runs every day at volume. Choosing a language in general is covered in choosing a language for web scraping.

Go web scraping libraries

LibraryWhat it doesRuns JavaScriptLicenseUse it for
net/httpThe standard HTTP clientNoGo's BSD-style licenseFetching pages and JSON APIs
goquery (github.com/PuerkitoBio/goquery)Parses HTML; CSS selectors, jQuery-like APINoBSD-3-ClauseExtracting fields from a page you fetched
Colly (github.com/gocolly/colly/v2)Scraping and crawling framework: callbacks, link following, limits, async, proxies, cachingNoApache-2.0Scrapers and crawlers: the default for most projects
chromedp (github.com/chromedp/chromedp)Drives Chrome over the DevTools ProtocolYesMITJavaScript-rendered pages
rod (github.com/go-rod/rod)Drives Chrome with a higher-level, auto-waiting APIYesMITThe same, with less code
gospider (github.com/jaeles-project/gospider)A command-line crawler that lists a site's URLsNoMITQuick URL discovery, without writing code

Set up a Go scraping project

Shell
mkdir books-scraper && cd books-scraper
go mod init example.com/books-scraper
go get github.com/gocolly/colly/v2 github.com/PuerkitoBio/goquery

Scraping with goquery and net/http

goquery parses a page you have fetched yourself, which keeps full control over the HTTP request:

package main

import (
	"fmt"
	"log"
	"net/http"
	"time"

	"github.com/PuerkitoBio/goquery"
)

func main() {
	client := &http.Client{Timeout: 30 * time.Second}
	req, _ := http.NewRequest("GET", "https://books.toscrape.com/", nil)
	req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; books-demo/1.0)")

	res, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		log.Fatalf("status %d", res.StatusCode)
	}

	doc, err := goquery.NewDocumentFromReader(res.Body)
	if err != nil {
		log.Fatal(err)
	}
	doc.Find("article.product_pod").Each(func(i int, s *goquery.Selection) {
		title, _ := s.Find("h3 a").Attr("title")
		price := s.Find(".price_color").Text()
		fmt.Printf("%s: %s\n", title, price)
	})
}

Find() takes a CSS selector, Each() loops over the matches, Attr() returns an attribute and whether it exists, and Text() the combined text. goquery does not support XPath; if you need XPath in Go, the antchfx/htmlquery package provides it.

A Colly scraper and crawler, step by step

Colly wraps fetching and parsing in a collector: you register callbacks for the elements you care about, and call Visit(). This program collects every book's title, price and stock status from all 50 pages of a practice catalog, two pages at a time with a random delay, and writes a CSV file.

package main

import (
	"encoding/csv"
	"log"
	"os"
	"strings"
	"sync"
	"time"

	"github.com/gocolly/colly/v2"
)

func main() {
	f, err := os.Create("books.csv")
	if err != nil {
		log.Fatal(err)
	}
	defer f.Close()
	w := csv.NewWriter(f)
	defer w.Flush()
	w.Write([]string{"title", "price", "in_stock"})
	var mu sync.Mutex // async callbacks run in several goroutines

	c := colly.NewCollector(
		colly.AllowedDomains("books.toscrape.com"),
		colly.Async(true),
	)
	c.SetRequestTimeout(30 * time.Second)
	c.Limit(&colly.LimitRule{DomainGlob: "*", Parallelism: 2, RandomDelay: time.Second})

	// one row per book on a list page
	c.OnHTML("article.product_pod", func(e *colly.HTMLElement) {
		inStock := strings.Contains(e.ChildText(".availability"), "In stock")
		mu.Lock()
		w.Write([]string{e.ChildAttr("h3 a", "title"), e.ChildText(".price_color"), yesNo(inStock)})
		mu.Unlock()
	})
	// follow the pagination
	c.OnHTML("li.next a[href]", func(e *colly.HTMLElement) {
		e.Request.Visit(e.Attr("href"))     // relative links are resolved for you
	})
	c.OnError(func(r *colly.Response, err error) {
		log.Printf("error %d on %s: %v", r.StatusCode, r.Request.URL, err)
	})

	c.Visit("https://books.toscrape.com/catalogue/page-1.html")
	c.Wait() // wait for the async requests to finish
}

func yesNo(b bool) string {
	if b {
		return "yes"
	}
	return "no"
}

How it works:

  1. colly.NewCollector() creates the collector; AllowedDomains keeps it on one site and Async(true) lets requests run in parallel.
  2. Limit() sets the politeness rule: at most 2 requests at a time to the site, with up to 1 s of random delay.
  3. OnHTML(selector, fn) runs fn for every element that matches, with helpers such as ChildText and ChildAttr.
  4. e.Request.Visit() queues a link; Colly skips URLs it has already visited.
  5. OnError receives failed requests; Wait() blocks until every queued request is done.

In async mode the callbacks run in several goroutines, so the sync.Mutex lets only one of them write to the CSV file at a time; sending rows to a channel and writing them from one goroutine works too.

Turning the scraper into a crawler

To crawl a whole site, follow every link instead of only the "next" link, and cap the depth:

c := colly.NewCollector(colly.AllowedDomains("books.toscrape.com"), colly.MaxDepth(3))
c.OnHTML("a[href]", func(e *colly.HTMLElement) {
	e.Request.Visit(e.Attr("href"))
})
c.OnResponse(func(r *colly.Response) {
	log.Println("visited", r.Request.URL)
})
c.Visit("https://books.toscrape.com/")

Add colly.CacheDir("./cache") to keep downloaded pages on disk while you develop, so re-running the parser does not hit the site again. The general method is in how to crawl a website.

Colly vs goquery

They are not alternatives so much as layers: Colly uses goquery internally, and e.DOM in a callback is a goquery selection. Use goquery alone when you fetch pages yourself (from an API client, a file, a headless browser) and only need parsing. Use Colly when you want the crawl handled for you: queues, deduplication, domain limits, parallelism, retries you write in OnError, cookies and proxy switching.

Pages that need JavaScript: chromedp

Colly and goquery see only the HTML the server sends. When the data is rendered by scripts, first look for its JSON request in the browser's DevTools (Network tab) and call it with net/http. If there is none, chromedp runs a headless Chrome:

ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
var quotes []string
err := chromedp.Run(ctx,
	chromedp.Navigate("https://quotes.toscrape.com/js/"),
	chromedp.WaitVisible(".quote", chromedp.ByQuery),
	chromedp.Evaluate(`[...document.querySelectorAll('.quote .text')].map(e => e.textContent)`, &quotes),
)

The full chromedp and rod examples, and when a browser is worth its cost, are in headless browser scraping.

Crawling a site's URLs with gospider

gospider is a command-line crawler written in Go that lists the URLs it finds on a site: links, JavaScript files, sitemap and robots.txt entries. It is handy for mapping a site before you write a scraper (gospider on GitHub).

Shell
go install github.com/jaeles-project/gospider@latest
gospider -s "https://books.toscrape.com/" -d 2 -c 5 -k 1 -o output

-s is the start URL, -d the depth, -c the concurrent requests, -k the delay in seconds and -o the output folder; -p sets a proxy. Its last commit is from March 2024, so check that it still fits your needs, and use Colly when you need the page contents rather than the URL list.

Using proxies in Go scrapers

When a site limits requests per IP, route them through a rotating residential proxy. Colly takes a proxy URL with the credentials inside:

if err := c.SetProxy("http://<login>-country-us-city-new_york:<password>@gate.proxyfield.io:7000"); err != nil {
	log.Fatal(err)
}

With net/http and goquery, set the proxy on the transport:

proxyURL, _ := url.Parse("http://<login>-country-us-city-new_york:<password>@gate.proxyfield.io:7000")
client := &http.Client{
	Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)},
	Timeout:   30 * time.Second,
}

The ProxyField gateway rotates the IP for you, a new one on every request, so you do not need Colly's round-robin proxy switcher or a proxy list. <login>-country-us-city-new_york asks for an IP in New York, United States. Region, session and session time go in the same username: -region-<region>, -session-<id> and -ttl-<seconds> (30 to 129,600 seconds, 20 minutes by default), as in <login>-country-us-region-new_york-city-new_york-session-he9f2qga-ttl-600; every part after the login is optional. Keep the request rate low, read each site's terms and collect only public data; see web scraping best practices and is web scraping legal. ProxyField residential proxies cost $2.95/GB for 1 GB down to $1.95/GB from 1,000 GB, with no subscription; the traffic never expires, and all purchases are final (residential and mobile proxy pricing).

Web Scraping With Go FAQ

Is Golang good for web scraping?

ProxyField's residential proxies (412M+ residential IPs, from $1.95/GB) plug into Colly's SetProxy(), and Golang is good for web scraping, especially for large daily crawls: goroutines make concurrent requests light and a scraper ships as one binary. For quick one-off scripts, Python is usually faster to write.

How do you scrape in Golang?

Fetch the page with net/http and parse it with goquery's CSS selectors, or let Colly do both: create a collector, register OnHTML callbacks for the elements you need, and call Visit(). Use chromedp or rod for pages that need JavaScript.

What is the best library for Golang scraping?

For most projects, Colly, because it adds crawling, rate limits, parallelism and proxy support on top of goquery's selectors. goquery alone is enough when you fetch pages yourself; chromedp or rod handle JavaScript-rendered pages.

Is Colly good for large-scale scraping?

Yes, within limits: async mode, per-domain parallelism and delays, a disk cache and proxy switching cover most large crawls. For crawls spread over several machines, add an external queue (Colly's queue package takes a pluggable storage backend) and your own monitoring.

Does Colly support JavaScript rendering?

No. Colly downloads the HTML the server sends and does not run scripts. Call the page's JSON endpoint instead, or render the page with chromedp or rod and parse the result with goquery.

What is goquery?

A Go library (BSD-3-Clause) that parses HTML and lets you select elements with CSS selectors in a jQuery-like style: doc.Find(".price").Text(). It does only parsing, so you pair it with net/http or another fetcher.

Goquery vs Colly: which should I use?

Use goquery when you only need to parse pages you fetch yourself, and Colly when you want the crawling handled: visiting links, deduplication, limits, parallelism and proxies. Colly uses goquery inside, so the selectors are the same.

How do I use gospider for web crawling?

Install it with go install github.com/jaeles-project/gospider@latest and run gospider -s <url> -d 2 -c 5 -o output, where -d is the depth and -c the concurrent requests; it writes the URLs it finds. Add -k for a delay and -p for a proxy.

What is chromedp?

A Go library (MIT) that drives Chrome or Chromium over the Chrome DevTools Protocol, without external dependencies such as Selenium. You run a list of actions (navigate, wait, click, evaluate) in a context and read the results.

How do I use a proxy with Colly?

Call c.SetProxy("http://user:pass@host:port") on the collector, or c.SetProxyFunc() with Colly's round-robin switcher to cycle a list of proxies. With a rotating gateway, one proxy URL is enough.

Show 1 more questionShow fewer questions

How do I build a web crawler in Golang?

Create a Colly collector with AllowedDomains and MaxDepth, add an OnHTML("a[href]") callback that calls e.Request.Visit() on each link, set a Limit rule for politeness, and start with Visit(). Colly skips URLs it has already seen.

Easy Trial: 1 GB for $2.95.

Try for the price of 1 GB, no subscription, traffic never expires.