Browser Automation in Go: Selenium, chromedp, Playwright, ZenRows

Selenium, chromedp, Playwright, ZenRows - in Go.

Page content

Choosing the right browser automation stack and webscraping in Go affects speed, maintenance, and where your code runs.

This overview compares chromedp, Playwright for Go, Selenium (Go client), and ZenRows from Go-with code examples for each-so you can pick the best fit for scraping, E2E tests, or scheduled automation.

laptop on the wooden table with vscode

TL;DR - Quick comparison

Tool Browser scope Runtime Best for
chromedp Chrome/Chromium Pure Go, no server Scraping, lightweight automation
Playwright Go Chromium, Firefox, WebKit Playwright binaries E2E tests, cross-browser, scraping
Selenium (Go) Any WebDriver Driver or grid Legacy suites, broad ecosystem
ZenRows Cloud (API/Browser) HTTP from Go Scraping with proxies/anti-bot

When to use which

chromedp - Idiomatic Go library that drives Chrome/Chromium via the Chrome DevTools Protocol (CDP). No WebDriver or Selenium server; no external runtime. Ideal for scraping and lightweight automation when Chrome-only is acceptable. The main difference from Playwright for Go is that chromedp is pure Go and Chrome-only, while Playwright supports multiple browsers and requires installing browser binaries.

Playwright for Go - Community-maintained Go bindings for Microsoft Playwright. One API for Chromium, Firefox, and WebKit; auto-waiting for elements; modern selectors and features. Use it when you need cross-browser E2E tests or a test-focused API and are fine with an extra install step for browsers.

Selenium (Go) - The classic WebDriver approach: a Go client talks to a browser driver (ChromeDriver, GeckoDriver, etc.). Selenium does support Go; you run a driver process or connect to a grid. Use it for legacy suites or when you need the broadest ecosystem; for new Go projects, chromedp or Playwright for Go often simplify setup.

ZenRows - Not a driver library but a Scraper API (and optional Scraping Browser) you call from Go over HTTP. ZenRows handles headless browsers, JS rendering, residential proxies, anti-bot bypass, and CAPTCHA. Use it when your goal is scraping and you hit blocks or rate limits; for local E2E tests, chromedp or Playwright are usually enough.

For a quick reference of Go tooling and structure, see Go Project Structure: Practices & Patterns; keeping automation in a dedicated package fits well with internal/ or pkg/.

chromedp: pure Go, Chrome-only

chromedp requires no third-party binaries: it implements the CDP in Go and launches (or connects to) Chrome/Chromium. Install:

go get -u github.com/chromedp/chromedp

Example: navigate, read title, and extract text by selector. All actions run inside chromedp.Run; use chromedp.ByQuery for CSS selectors.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/chromedp/chromedp"
)

func main() {
	ctx, cancel := chromedp.NewContext(context.Background())
	defer cancel()

	var title string
	var bodyText string
	err := chromedp.Run(ctx,
		chromedp.Navigate("https://example.com"),
		chromedp.Title(&title),
		chromedp.Text("h1", &bodyText, chromedp.ByQuery),
	)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("Title:", title)
	fmt.Println("Heading:", bodyText)
}

Example: click and read updated HTML. Use chromedp.Click and chromedp.OuterHTML (or chromedp.InnerHTML) with chromedp.ByQuery. Replace targetURL with your page (e.g. a test server or any URL):

	targetURL := "https://example.com"
	var outerBefore, outerAfter string
	err := chromedp.Run(ctx,
		chromedp.Navigate(targetURL),
		chromedp.OuterHTML("#content", &outerBefore, chromedp.ByQuery),
		chromedp.Click("#content", chromedp.ByQuery),
		chromedp.OuterHTML("#content", &outerAfter, chromedp.ByQuery),
	)

By default, Chrome runs headless. To show a window or change flags, use a custom allocator (see the chromedp ExecAllocator example). For Docker or CI, the chromedp/headless-shell image provides a smaller headless Chrome build that chromedp can use out of the box-so you can run chromedp in headless environments without installing Chrome on the host.

More examples (screenshots, PDFs, forms, cookies) are in the chromedp/examples repository.

Playwright for Go: cross-browser, auto-wait

Playwright for Go gives you the same multi-browser and auto-wait features as Playwright in other languages. Install the library and then the browser binaries:

go get -u github.com/playwright-community/playwright-go
go run github.com/playwright-community/playwright-go/cmd/playwright@latest install --with-deps

Example: launch Chromium, open a page, take a screenshot. You can use pw.Firefox or pw.WebKit for other engines.

package main

import (
	"log"

	"github.com/playwright-community/playwright-go"
)

func main() {
	pw, err := playwright.Run()
	if err != nil {
		log.Fatalf("could not launch playwright: %v", err)
	}
	defer pw.Stop()

	browser, err := pw.Chromium.Launch(playwright.BrowserTypeLaunchOptions{Headless: playwright.Bool(true)})
	if err != nil {
		log.Fatalf("could not launch Chromium: %v", err)
	}
	defer browser.Close()

	page, err := browser.NewPage()
	if err != nil {
		log.Fatalf("could not create page: %v", err)
	}

	_, err = page.Goto("https://example.com")
	if err != nil {
		log.Fatalf("could not goto: %v", err)
	}

	_, err = page.Screenshot(playwright.PageScreenshotOptions{Path: playwright.String("example.png")})
	if err != nil {
		log.Fatalf("could not screenshot: %v", err)
	}
}

Example: fill a form and get text. Playwright auto-waits for elements to be actionable, which reduces flakiness compared to raw CDP or Selenium without explicit waits.

	page.Goto("https://example.com/login")
	page.Locator("#username").Fill("user")
	page.Locator("#password").Fill("secret")
	page.Locator("button[type=submit]").Click()
	content, _ := page.Locator("h1").TextContent()
	fmt.Println(content)

You can use Playwright for Go for web scraping as well as testing: navigate, click, extract HTML or text, and optionally drive ZenRows’ Scraping Browser via its CDP/Playwright-compatible endpoint when you need anti-bot or proxies.

Selenium (Go client)

Selenium’s WebDriver API is available in Go via community clients (e.g. tebeka/selenium). You run a browser driver (ChromeDriver, GeckoDriver) or connect to a grid; the Go code sends WebDriver commands. So yes, Selenium does support Go-you just need to manage the driver process or use a cloud grid.

Example: connect to ChromeDriver, navigate, get title. The driver must be running (e.g. chromedriver --port=4444 or Selenium Manager in Selenium 4).

package main

import (
	"fmt"
	"log"

	"github.com/tebeka/selenium"
)

func main() {
	caps := selenium.Capabilities{"browserName": "chrome"}
	wd, err := selenium.NewRemote(caps, "http://localhost:4444/wd/hub")
	if err != nil {
		log.Fatal(err)
	}
	defer wd.Quit()

	err = wd.Get("https://example.com")
	if err != nil {
		log.Fatal(err)
	}
	title, err := wd.Title()
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("Title:", title)
}

For new Go projects, chromedp (no driver) or Playwright for Go (multi-browser, auto-wait) are often easier; use Selenium when you already have WebDriver suites or need a specific grid.

ZenRows: Scraper API from Go

ZenRows exposes a Scraper API (HTTP) and an optional Scraping Browser (CDP/Playwright-compatible). From Go you typically use the Scraper API: send a GET (or POST) with the target URL and options; ZenRows returns the rendered HTML or other formats. No local browser to manage. Use ZenRows when scraping is the goal and you need proxies, anti-bot bypass, or CAPTCHA handling; for simple local automation, chromedp or Playwright are enough.

Install the official Go SDK:

go get github.com/zenrows/zenrows-go-sdk/service/api

Example: simple GET with the ZenRows Scraper API. Set your API key via the client or ZENROWS_API_KEY env var.

package main

import (
	"context"
	"fmt"
	"log"

	scraperapi "github.com/zenrows/zenrows-go-sdk/service/api"
)

func main() {
	client := scraperapi.NewClient(
		scraperapi.WithAPIKey("YOUR_API_KEY"),
	)

	response, err := client.Get(context.Background(), "https://example.com", nil)
	if err != nil {
		log.Fatal(err)
	}
	if err := response.Error(); err != nil {
		log.Fatal(err)
	}
	fmt.Println("Body length:", len(response.Body()))
	fmt.Println("Status:", response.Status())
}

Example: JS rendering and premium proxies. Customize behavior with RequestParameters:

	params := &scraperapi.RequestParameters{
		JSRender:          true,
		UsePremiumProxies:  true,
		ProxyCountry:       "US",
	}
	response, err := client.Get(context.Background(), "https://example.com", params)
	if err != nil {
		log.Fatal(err)
	}
	if err := response.Error(); err != nil {
		log.Fatal(err)
	}
	html := response.String()

The SDK supports concurrency limits (WithMaxConcurrentRequests), retries (WithMaxRetryCount, WithRetryWaitTime), and other options; see the ZenRows Scraper API Go SDK docs.

Summary

  • chromedp: Pure Go, CDP, Chrome-only; no driver. Use for fast, low-overhead automation and scraping. Run in Docker with chromedp/headless-shell if needed.
  • Playwright for Go: Multi-browser, auto-wait, test-friendly. Use for E2E tests or when you want one API for Chromium, Firefox, and WebKit; also fine for scraping.
  • Selenium (Go): WebDriver from Go; driver or grid required. Use when maintaining existing Selenium suites or when you need a specific grid.
  • ZenRows: Scraper API (and Scraping Browser) from Go. Use when scraping is the focus and you need resilience to blocks, rate limits, and anti-bot.

For more Go practices-linters, project layout, and dependency injection-see Go Linters: Essential Tools for Code Quality, Dependency Injection in Go: Patterns & Best Practices, and the Go Cheatsheet. If you combine browser automation with LLM pipelines in Go, the Go SDKs for Ollama and Reranking with Ollama and Qwen3 Embedding in Go are useful references.