Go ist wohl die beste verbreitete Sprache für die Fetching-Hälfte des Web Scraping: Goroutines lassen tausend gleichzeitige Requests wie nichts erscheinen, und das Ergebnis kompiliert zu einer einzigen Binary, die du auf jeden Server werfen kannst. Die Parsing- und Anti-Bot-Hälfte ist der Punkt, an dem Python immer noch den tieferen Werkzeugkasten hat. Dieser Guide baut einen Go-Scraper auf die ehrliche Art — Standardbibliothek plus goquery zuerst, dann Colly für Crawling und Rate limits, dann ein Worker-Pool, der deine IP nicht sperren lässt — und sagt dir genau, wo DIY-Go-Scraping aufhört zu funktionieren und eine API übernimmt.
Warum Go für Web Scraping — und warum nicht
Das Argument für Go ist echt, also formulieren wir es fair.
Rohe Geschwindigkeit und Nebenläufigkeit. Scraping ist I/O-gebundene Arbeit, und Goroutines sind das billigste Nebenläufigkeitsprimitiv jeder verbreiteten Sprache — je ein paar Kilobyte Stack. Ein Go-Scraper, der 200 gleichzeitige Verbindungen auslastet, ist ein Nachmittagsprojekt; das Äquivalent in Python bedeutet asyncio, einen asynchronen HTTP-Client und ein mentales Modell, das viele Teams nie ganz verinnerlichen.
Single-Binary-Deploys. go build liefert dir eine einzige statische ausführbare Datei. Kein virtualenv, keine Abhängigkeitsauflösung auf dem Server, kein „works on my machine". Für Scraper, die auf Cronjobs, in Containern oder auf günstigen VPS leben, ist das ein ehrlich unterschätzter Vorteil.
Typisierte Ausgabe von Haus aus. Du parst in Structs, sodass Schema-Drift als Compile-Fehler oder als offensichtlich leeres Feld auftaucht — nicht als stiller KeyError drei Tage in einen Lauf hinein.
Nun die ehrlichen Nachteile. Gos Scraping-Ökosystem ist kleiner: Python hat BeautifulSoup, Scrapy, Playwright mit Stealth-Patches, curl-impersonate-Bindings und ein Jahrzehnt an Stack-Overflow-Antworten für jeden Sonderfall. Go hat goquery, Colly und chromedp — gute Werkzeuge, aber weniger davon, und weit weniger Anti-Bot- und Stealth-Bibliotheken. Außerdem schreibst du mehr Boilerplate: Fehlerbehandlung bei jedem Request, explizite Structs für jede Form. Wenn dein Flaschenhals das Parsen von unsauberem HTML oder das Umgehen ausgefeilter Bot-Erkennung ist statt der Durchsatz, dann ist web scraping with Python der nachsichtigere Startpunkt — dieser Guide deckt dieselbe Reise mit requests, BeautifulSoup und Scrapy ab.
Wenn dir Durchsatz, Deployment und langfristige Zuverlässigkeit wichtig sind, lies weiter.
Level 1: net/http + goquery
Das net/http der Standardbibliothek holt Seiten; goquery gibt dir jQuery-artige CSS-Selektoren über dem geparsten DOM. Installiere es mit:
go get github.com/PuerkitoBio/goquery
Hier ist ein vollständiger Scraper für eine Katalogseite im Stil von books.toscrape.com — holen, den Statuscode prüfen, Elemente auswählen, in Structs extrahieren, JSON ausgeben:
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
"github.com/PuerkitoBio/goquery"
)
type Book struct {
Title string `json:"title"`
Price string `json:"price"`
InStock bool `json:"in_stock"`
}
func main() {
res, err := http.Get("https://books.toscrape.com/")
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
log.Fatalf("status code error: %d %s", res.StatusCode, res.Status)
}
doc, err := goquery.NewDocumentFromReader(res.Body)
if err != nil {
log.Fatal(err)
}
var books []Book
doc.Find("article.product_pod").Each(func(i int, s *goquery.Selection) {
books = append(books, Book{
Title: s.Find("h3 a").AttrOr("title", ""),
Price: strings.TrimSpace(s.Find(".price_color").Text()),
InStock: s.Find(".instock.availability").Length() > 0,
})
})
out, err := json.MarshalIndent(books, "", " ")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(out))
}
Das Muster zum Verinnerlichen: NewDocumentFromReader parst den Response-Body einmal, Find nimmt jeden CSS-Selektor, Each iteriert über die Treffer, und AttrOr / Text ziehen Werte mit vernünftigen Fallbacks. Prüfe immer res.StatusCode vor dem Parsen — goquery parst eine Fehlerseite bereitwillig in eine leere Selection, und du jagst einem Phantom-Bug hinterher. Zwei weitere Hinweise aus der Standardbibliothek für die Produktion: Nutze http.Client mit einem expliziten Timeout statt des Nullwert-Defaults (der nie eine Zeitüberschreitung auslöst), und denk daran, dass goquery UTF-8 erwartet — transkodiere veraltete Encodings zuerst.
Diese Ebene ist perfekt für einmalige Jobs gegen statisches HTML. Sobald du Links über Seiten hinweg verfolgen musst, willst du ein Framework.
Level 2: Colly — das Go-Scraping-Framework
Colly verpackt die Fetch-Parse-Follow-Schleife in einen Collector mit Event-Callbacks. Es ist das, was Go Scrapy am nächsten kommt, und es erledigt web crawling — Links zu durchlaufen, nicht nur eine Seite zu parsen — mit fast keinem Zeremoniell. Beachte den v2-Modulpfad:
go get github.com/gocolly/colly/v2
Derselbe Buchladen, jetzt jede Katalogseite über den „next"-Link crawlend, mit eingebauten höflichen Delays:
package main
import (
"fmt"
"log"
"time"
"github.com/gocolly/colly/v2"
)
func main() {
c := colly.NewCollector(
colly.AllowedDomains("books.toscrape.com"),
)
// Politeness: delay between requests to this domain.
if err := c.Limit(&colly.LimitRule{
DomainGlob: "books.toscrape.com",
Delay: 500 * time.Millisecond,
RandomDelay: 500 * time.Millisecond,
}); err != nil {
log.Fatal(err)
}
c.OnHTML("article.product_pod", func(e *colly.HTMLElement) {
fmt.Printf("%s — %s\n",
e.ChildAttr("h3 a", "title"),
e.ChildText(".price_color"))
})
// Pagination: follow the next-page link until it stops existing.
c.OnHTML("li.next a", func(e *colly.HTMLElement) {
e.Request.Visit(e.Attr("href"))
})
c.OnError(func(r *colly.Response, err error) {
log.Println("request failed:", r.Request.URL, err)
})
if err := c.Visit("https://books.toscrape.com/"); err != nil {
log.Fatal(err)
}
}
Drei Ideen tragen das ganze Framework. OnHTML-Callbacks feuern für jedes Element, das auf einen Selektor passt, sodass Extraktion und Link-Verfolgung beide nur Callbacks sind. Pagination ist e.Request.Visit(e.Attr("href")) — Colly löst relative URLs auf, dedupliziert besuchte Seiten und respektiert AllowedDomains, sodass du nicht versehentlich das ganze Internet crawlst. LimitRule gibt dir Delay pro Domain plus RandomDelay-Jitter, ohne selbst Timing-Code zu schreiben.
Level 3: Nebenläufigkeit richtig gemacht
Das ist der Abschnitt, für den Go-Leser gekommen sind, und die Stelle, an der die meisten Go-Scraper scheitern. Der naive Schritt — eine Goroutine pro URL starten — funktioniert in einer Demo und schmilzt in der Produktion dahin: Du öffnest Tausende gleichzeitiger Verbindungen, die Zielseite drosselt oder sperrt dich, und deinem eigenen Prozess gehen die File-Deskriptoren aus. Nebenläufigkeit braucht zwei Grenzen: wie viele Worker gleichzeitig laufen und wie viele Requests pro Sekunde den Prozess verlassen. (Für die serverseitige Sicht auf das Warum siehe what is rate limiting.)
Die idiomatische Form ist ein begrenzter Worker-Pool, gespeist von einem Channel, mit einem gemeinsamen time.Ticker als globalem Rate Limiter:
func scrapeAll(urls []string, workers, perSecond int) {
jobs := make(chan string)
ticker := time.NewTicker(time.Second / time.Duration(perSecond))
defer ticker.Stop()
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for u := range jobs {
<-ticker.C // one tick per request, shared by all workers
if err := scrapeOne(u); err != nil {
log.Println(u, err)
}
}
}()
}
for _, u := range urls {
jobs <- u
}
close(jobs)
wg.Wait()
}
Zehn Worker bei fünf Requests pro Sekunde sind ein sinnvoller Startpunkt für eine Seite, die du nicht kontrollierst. Der Ticker wird geteilt, sodass zusätzliche Worker die Überlapp-Toleranz erhöhen (langsame Responses blockieren die Queue nicht), ohne die Request-Rate zu erhöhen — die beiden Stellschrauben bleiben unabhängig, was genau das ist, was du beim Feintuning gegen einen Rate Limiter willst.
Wenn du bereits in Colly bist, bekommst du dasselbe Verhalten deklarativ — erstelle den Collector mit colly.Async(), begrenze Parallelism in der LimitRule und blockiere auf Wait:
c := colly.NewCollector(colly.Async())
c.Limit(&colly.LimitRule{
DomainGlob: "*",
Parallelism: 4,
Delay: 200 * time.Millisecond,
})
// ... register OnHTML callbacks, then:
c.Visit(startURL)
c.Wait() // block until all async requests finish
So oder so gilt dieselbe Regel: Begrenze die Worker, dosiere die Requests und behandle 429- und 403-Antworten als Signal zum Zurückfahren, nicht zum härteren Wiederholen.
JS-lastige Seiten: chromedp gibt es, aber wäge die Kosten ab
Alles oben Genannte setzt voraus, dass die Daten im HTML stecken, das der Server sendet. Zunehmend ist das nicht der Fall — sie werden clientseitig von JavaScript gerendert, und net/http sieht eine leere Hülle. Gos Antwort ist chromedp, das eine echte Chrome-Instanz über das DevTools-Protokoll steuert (siehe what is a headless browser):
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
var html string
err := chromedp.Run(ctx,
chromedp.Navigate("https://example.com/js-rendered-listing"),
chromedp.WaitVisible(".product-grid"),
chromedp.OuterHTML("html", &html),
)
// feed html into goquery.NewDocumentFromReader as before
Es funktioniert, und die API ist angenehm. Aber sei ehrlich darüber, worauf du dich eingelassen hast: ein volles Chrome pro Kontext (Hunderte MB RAM), Chrome-Binaries in deinem zuvor aufgeräumten Single-Binary-Deployment und rund 10–50x die Latenz eines schlichten HTTP-Requests. Dein Durchsatz fällt von Hunderten Seiten pro Sekunde auf eine Handvoll. Nutze chromedp, wenn ein bestimmtes, wertvolles Ziel wirklich Rendering erfordert — nicht als Standardweg. Oft ist der bessere Schritt, die DevTools zu öffnen und den JSON-Endpoint zu finden, den die Seite selbst aufruft; das mit net/http zu scrapen ist schneller, als Rendering es je sein wird.
Die Produktionsmauer — und die API-Brücke
Skaliere irgendetwas davon gegen echte kommerzielle Ziele — Amazon, Google, soziale Plattformen — und du stößt gegen eine Mauer, die nichts mit deiner Codequalität zu tun hat: Anti-Bot-Systeme. Rechenzentrums-IPs werden markiert, CAPTCHAs tauchen auf, und Header allein retten dich nicht.
Go hat hier ein spezifisches Problem, das man kennen sollte. Anti-Bot-Anbieter nehmen einen Fingerprint des TLS-Handshakes — der Cipher Suites und Extensions, die ein Client anbietet, gehasht zu einer JA3/JA4-Signatur — und Gos crypto/tls-Stack erzeugt einen Handshake, der überhaupt nicht wie der von Chrome aussieht. Geschützte Seiten können einen Go-Client identifizieren, bevor er ein einziges HTTP-Byte sendet, egal wie sorgfältig du den User-Agent fälschst. Das ist eine Spielart des browser fingerprinting, und obwohl es Community-Forks gibt, die Browser-TLS imitieren, sind sie genau die Art von nischiger, schnell verfallender Abhängigkeit, von der das Go-Ökosystem weniger hat als das von Python. Für das vollständige Bild dessen, womit du es zu tun hast, siehe scraping sites that block bots.
An diesem Punkt ist der pragmatische Schritt, Go für das zu behalten, worin es großartig ist — Orchestrierung, Nebenläufigkeit, Pipelines — und den feindseligen Fetch an eine Scraping API zu delegieren. Crawlora stellt strukturierte Endpoints über schlichtes HTTPS bereit, sodass der Aufruf ganz normales net/http ist, und die Antwort ist normalisiertes JSON statt HTML, das du parsen und hüten musst:
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"net/url"
"os"
)
type searchResponse struct {
Code int `json:"code"`
Data []struct {
ASIN string `json:"asin"`
Link string `json:"link"`
ListPrice float64 `json:"list_price"`
} `json:"data"`
}
func main() {
q := url.Values{}
q.Set("k", "mechanical keyboard")
q.Set("page", "1")
req, err := http.NewRequest(http.MethodGet,
"https://api.crawlora.net/api/v1/amazon/search?"+q.Encode(), nil)
if err != nil {
log.Fatal(err)
}
req.Header.Set("x-api-key", os.Getenv("CRAWLORA_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
var sr searchResponse
if err := json.NewDecoder(res.Body).Decode(&sr); err != nil {
log.Fatal(err)
}
for _, item := range sr.Data {
fmt.Printf("%s $%.2f %s\n", item.ASIN, item.ListPrice, item.Link)
}
}
Keine Proxys zum Rotieren, kein TLS-Impersonation-Fork zum Nachhalten, kein Parser, den du reparieren musst, wenn Amazon ein Redesign ausliefert — typisierte Structs über dokumentiertem JSON, genau der Workflow, für den Go gebaut ist. Proxys, Rendering und Retries passieren hinter dem Endpoint. Es gibt außerdem ein official Go SDK (neben TypeScript und Python), falls du generierte Clients gegenüber rohem net/http bevorzugst, und dasselbe Worker-Pool-Muster aus Level 3 gilt unverändert — richte es auf API-Aufrufe statt auf rohe Seiten. Die Abrechnung erfolgt pay-on-success: Dir werden nur erfolgreiche 2xx-Antworten berechnet, ein fehlgeschlagener Fetch kostet also nichts (pricing). Ob du ein Ziel selbst scrapst oder eine API dafür aufrufst, ist eine Ermessensfrage — web scraping vs API geht sie durch.
- Statisches HTML, eine Seite, moderates Volumen: net/http + goquery ist alles, was du brauchst.
- Crawling mit Pagination: Colly v2 mit AllowedDomains und einer LimitRule (Delay + RandomDelay).
- Hohes Volumen: begrenzter Worker-Pool + gemeinsamer time.Ticker oder colly.Async mit Parallelism — niemals unbegrenzte Goroutines.
- Setze einen echten User-Agent und einen http.Client-Timeout; fahre bei 429/403 zurück.
- JS-gerenderte Seiten: prüfe auf den zugrunde liegenden JSON-Endpoint, bevor du zu chromedp greifst.
- Anti-Bot-Mauern und TLS-Fingerprint-Blocks: leite solche Ziele über eine Scraping API und behalte deinen Go-Code für die Orchestrierung.
Fazit
Go belohnt Scraper, die aussehen wie die Systeme, für die Go entworfen wurde: nebenläufig, begrenzt, typisiert, als eine Binary deployt. Beginne mit net/http und goquery, steige auf Colly um, wenn du crawlen musst, und stelle einen Worker-Pool mit einem Ticker vor alles. Wenn die Abwehr eines Ziels mehr Engineering-Zeit kostet, als die Daten wert sind, überbrücke aus derselben Codebasis zu einer API — du kannst die Endpoints im playground ausprobieren, ohne eine Zeile Go zu schreiben, und dann im docs den vollständigen Endpoint-Katalog durchstöbern.
Behalte Go für die Pipeline, überspring das Anti-Bot-Wettrüsten
Strukturierte Endpoints, normalisiertes JSON, verwaltete Proxys und Retries — aufgerufen mit schlichtem net/http oder dem offiziellen Go SDK. Kostenlose Stufe: 2,000 Credits/Monat, ohne Karte.
Weiterführende Lektüre: Web scraping with Python (dieselbe Reise im größeren Ökosystem) · Scraping sites that block bots · Web scraping vs API · What is browser fingerprinting
Häufig gestellte Fragen
Ist Go gut für Web Scraping?
Ja — Go glänzt bei der Fetching-Hälfte des Scrapings: Goroutines machen hochgradig nebenläufige Requests billig, und go build erzeugt eine einzige statische Binary, die sich überall deployen lässt. Sein Ökosystem ist allerdings kleiner als das von Python, mit weniger Parsern und weit weniger Anti-Bot- oder Stealth-Bibliotheken, sodass Go am besten passt, wenn Durchsatz und Deployment wichtiger sind als das Umgehen ausgefeilter Bot-Erkennung.
Colly vs goquery — was ist der Unterschied?
goquery ist eine Parsing-Bibliothek: Sie gibt dir jQuery-artige CSS-Selektoren (Find, Each, Attr) über HTML, das du bereits mit net/http geholt hast. Colly ist ein vollständiges Scraping-Framework auf Basis von goquerys Selektor-Engine: Es verwaltet die Fetch-Schleife, OnHTML-Callbacks, Link-Verfolgung und Pagination, Deduplizierung sowie Delays und Parallelität pro Domain über LimitRule. Nutze goquery allein für Ein-Seiten-Jobs; nutze Colly, wenn du crawlen musst.
Ist Go schneller als Python beim Scraping?
Für den netzgebundenen Teil im Maßstab spürbar ja: Goroutines lassen einen Go-Scraper Hunderte gleichzeitiger Verbindungen mit trivialem Speicheraufwand halten, ohne ein async-Framework lernen zu müssen. Für eine einzelne Seite dominiert das Netzwerk und die Sprache spielt kaum eine Rolle. Python verkleinert den Abstand mit asyncio, aber Gos Nebenläufigkeit ist einfacher korrekt zu schreiben, und die kompilierte Binary braucht weniger Speicher pro Worker.
Wie scrape ich JavaScript-gerenderte Seiten in Go?
Nutze chromedp, das headless Chrome über das DevTools-Protokoll steuert: Navigate, WaitVisible, dann das gerenderte HTML extrahieren und mit goquery parsen. Sei dir der Kosten bewusst — ein volles Chrome pro Kontext, hoher Speicherverbrauch und die 10–50-fache Latenz eines schlichten HTTP-Requests. Bevor du danach greifst, prüfe in den DevTools den JSON-Endpoint, den die Seite aufruft; ihn direkt mit net/http zu holen ist meist schneller.
Warum blockieren Websites Go-Scraper selbst mit einem Browser-User-Agent?
Anti-Bot-Systeme nehmen einen Fingerprint des TLS-Handshakes (JA3/JA4-Signaturen), und Gos crypto/tls-Stack erzeugt einen Handshake, der überhaupt nicht wie der von Chrome aussieht — sodass geschützte Seiten einen Go-Client identifizieren können, bevor ein einziger HTTP-Header gesendet wird. Das Fälschen des User-Agents ändert den TLS-Fingerprint nicht. Community-Forks zur TLS-Impersonation gibt es, aber sie verfallen schnell; für stark geschützte Ziele ist es zuverlässiger, den Fetch über eine Scraping API zu leiten.
Wie drossle ich einen nebenläufigen Go-Scraper?
Begrenze zwei Dinge unabhängig voneinander: die Worker-Zahl (ein fester Pool von Goroutines, die aus einem Jobs-Channel lesen) und die Request-Rate (ein gemeinsamer time.Ticker, aus dem jeder Worker liest, bevor er einen Request abfeuert). In Colly ist das deklarative Äquivalent colly.Async() plus eine LimitRule mit Parallelism und Delay, dann c.Wait(). Behandle 429- und 403-Antworten als Signal zum Zurückfahren, nicht zum härteren Wiederholen.
Hat Crawlora ein Go SDK?
Ja — Crawlora veröffentlicht ein offizielles Go-SDK-Modul neben seinen TypeScript- und Python-SDKs, und jeder Endpoint funktioniert auch mit schlichtem net/http: Sende ein GET an den dokumentierten Pfad auf https://api.crawlora.net/api/v1 mit deinem Key im x-api-key-Header und dekodiere das normalisierte JSON in Structs. Die kostenlose Stufe umfasst 2,000 Credits/Monat, ohne Karte, und die Abrechnung erfolgt pay-on-success — dir werden nur erfolgreiche 2xx-Antworten berechnet.
