How to Use Web Browsers in Go
Automate Chrome, Firefox and WebKit from Go with reliable waits, diagnostics, screenshots and PDFs using Chromedp, Rod, Playwright or Selenium.
Direct answer
Go does not contain a browser. A Go program starts a real browser, usually Chrome or Chromium, and controls it through an automation library. The main choices are Chromedp and Rod for direct Chrome DevTools Protocol control, Playwright for Go for Chromium, Firefox and WebKit, and Selenium WebDriver for a language-neutral WebDriver API.
Choose Chromedp for Chrome-only CDP automation, Rod for higher-level waiting and browser helpers, Playwright for cross-browser testing, and Selenium when your organization already uses WebDriver or a Selenium grid.
Choose a Go browser library
| Library | Browsers | Control layer | Good fit | Operational requirement |
|---|---|---|---|---|
| Chromedp | Chrome/Chromium | Direct CDP | Scraping, screenshots, PDFs, forms and end-to-end checks | Chrome or Chromium must be installed |
| Rod | Browsers supporting CDP | Direct CDP | Auto-waiting, downloads, frames, shadow DOM and request interception | Some platforms need a manual browser install |
| Playwright for Go | Chromium, Firefox, WebKit | Playwright driver | Cross-browser tests and web-first assertions | Install matching driver and browser binaries |
| Selenium | Browser-specific backends | WebDriver | Existing Selenium grids and common browser APIs | Install a compatible driver for each browser |
See the Chromedp package documentation, Rod documentation, Playwright Go README and Selenium WebDriver documentation for library-specific APIs.
Install Go and a browser
- Create a module:
mkdir go-browser-demo cd go-browser-demo go mod init example.com/go-browser-demo - Install a package:
go get github.com/chromedp/chromedp # or go get github.com/go-rod/rod # or go get github.com/playwright-community/playwright-go - Provision the runtime. Chromedp needs Chrome or Chromium. Rod can discover or download a browser, depending on the platform. Playwright needs its matching driver and browser bundle. Selenium needs the browser and its browser-specific WebDriver.
Pin browser, driver and library versions in CI. Playwright browser binaries are tied to a Playwright release, and Selenium requires a compatible driver.
Minimal Chromedp program
This program navigates, waits for a heading, extracts text and writes a screenshot. The timeout prevents a broken page from hanging the process.
package main
import (
"context"
"fmt"
"log"
"os"
"time"
"github.com/chromedp/chromedp"
)
func main() {
browserCtx, cancel := chromedp.NewContext(context.Background())
defer cancel()
ctx, cancel := context.WithTimeout(browserCtx, 45*time.Second)
defer cancel()
var title string
var shot []byte
err := chromedp.Run(ctx,
chromedp.Navigate("https://example.com"),
chromedp.WaitVisible("h1", chromedp.ByQuery),
chromedp.Text("h1", &title, chromedp.ByQuery),
chromedp.FullScreenshot(&shot, 90),
)
if err != nil { log.Fatal(err) }
if err := os.WriteFile("example.png", shot, 0644); err != nil { log.Fatal(err) }
fmt.Println(title)
}
Run it with go run .. Chromedp is headless by default. Launch a headed browser while debugging selectors, then use headless mode in CI.
Common browser tasks
Forms and rendered content
err := chromedp.Run(ctx,
chromedp.Navigate("https://example.com/login"),
chromedp.WaitVisible("form", chromedp.ByQuery),
chromedp.SetValue("input[name=email]", "dev@example.com", chromedp.ByQuery),
chromedp.SetValue("input[name=password]", os.Getenv("PASSWORD"), chromedp.ByQuery),
chromedp.Click("button[type=submit]", chromedp.ByQuery),
chromedp.WaitVisible("main.dashboard", chromedp.ByQuery),
)
Wait for a state that proves completion instead of sleeping for an arbitrary duration.
Element screenshots and PDFs
var card []byte
err := chromedp.Run(ctx,
chromedp.Navigate("https://example.com"),
chromedp.WaitVisible(".pricing-card", chromedp.ByQuery),
chromedp.Screenshot(".pricing-card", &card, chromedp.NodeVisible, chromedp.ByQuery),
)
if err != nil { log.Fatal(err) }
var pdf []byte
err = chromedp.Run(ctx, chromedp.PrintToPDF().WithPrintBackground(true).Do(ctx, &pdf))
if err != nil { log.Fatal(err) }
if err := os.WriteFile("page.pdf", pdf, 0644); err != nil { log.Fatal(err) }
Frames, shadow DOM and downloads
A frame is a separate document, so select its frame context before querying inside it. Shadow roots also require shadow-aware helpers. Rod documents helpers for frames, shadow DOM, downloads and request interception. Use stable attributes such as data-testid where possible.
Playwright for Go
package main
import (
"log"
"github.com/playwright-community/playwright-go"
)
func main() {
if err := playwright.Install(); err != nil { log.Fatal(err) }
pw, err := playwright.Run(); if err != nil { log.Fatal(err) }
defer pw.Stop()
browser, err := pw.Chromium.Launch(playwright.BrowserTypeLaunchOptions{Headless: playwright.Bool(true)})
if err != nil { log.Fatal(err) }
defer browser.Close()
page, err := browser.NewPage(); if err != nil { log.Fatal(err) }
if _, err = page.Goto("https://example.com"); err != nil { log.Fatal(err) }
if err = page.Locator("h1").WaitFor(); err != nil { log.Fatal(err) }
title, err := page.Locator("h1").InnerText(); if err != nil { log.Fatal(err) }
log.Println(title)
_, err = page.Screenshot(playwright.PageScreenshotOptions{Path: playwright.String("example.png"), FullPage: playwright.Bool(true)})
if err != nil { log.Fatal(err) }
}
Playwright actions wait for actionable elements and assertions retry. Install the browser bundle documented for your pinned release.
Rod
package main
import (
"log"
"github.com/go-rod/rod"
"github.com/go-rod/rod/lib/launcher"
)
func main() {
controlURL := launcher.New().Headless(true).MustLaunch()
browser := rod.New().ControlURL(controlURL).MustConnect()
defer browser.MustClose()
page := browser.MustPage("https://example.com")
page.MustElement("h1").MustWaitVisible()
log.Println(page.MustElement("h1").MustText())
page.MustScreenshot("example.png")
}
Rod’s Must* methods are convenient for scripts. Use error-returning methods in services that must recover or report failures precisely.
Selenium WebDriver
package main
import (
"log"
"github.com/tebeka/selenium"
)
func main() {
wd, err := selenium.NewRemote(selenium.Capabilities{"browserName": "chrome"}, "http://localhost:4444/wd/hub")
if err != nil { log.Fatal(err) }
defer wd.Quit()
if err := wd.Get("https://example.com"); err != nil { log.Fatal(err) }
heading, err := wd.FindElement(selenium.ByCSSSelector, "h1"); if err != nil { log.Fatal(err) }
text, err := heading.Text(); if err != nil { log.Fatal(err) }
log.Println(text)
}
Reliability checklist
- Use a parent browser context and a child timeout context.
- Wait for visibility, URL changes, responses or application state; avoid arbitrary sleeps.
- Capture a screenshot, HTML, URL and browser logs when a job fails.
- Use headed mode locally and headless mode in CI.
- Pin browser, driver and library versions.
- Cap concurrency and reuse a browser only when cookie and storage isolation are safe.
- Sandbox browser processes, restrict outbound access and never expose debugging endpoints publicly.
- Set navigation and action timeouts and clean up every page and process.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| Executable not found | Browser is missing or path is wrong | Install Chrome/Chromium and configure the executable path in the CI image. |
| Playwright launch failure | Driver and browser bundle do not match | Run the install command for the pinned Playwright release. |
| Selenium session failure | Driver mismatch or unreachable grid | Check the driver log, endpoint URL and browser version. |
| Context deadline exceeded | Navigation or wait exceeded its timeout | Identify the slow step, wait on real state and adjust the timeout based on measurements. |
| Element not found | Wrong frame, shadow root, selector or timing | Inspect headed mode, select the correct frame or shadow root and use stable attributes. |
| Blank screenshot | Lazy content has not loaded | Wait for the content selector and application or network idle; scroll when loading depends on intersection. |
| Works locally but not in CI | Missing fonts, sandbox settings, browser or version differences | Use a pinned image, install fonts, collect logs and reproduce with identical headless options. |
| Context canceled | Browser connection or parent context ended | Keep the parent context alive, check crashes and retry the complete browser job when appropriate. |
Performance, reliability and cost
Launching a browser costs more than an HTTP request. Reuse a browser when isolation permits, cap parallel pages, block unnecessary resources and cache stable results. Full-page screenshots and PDFs use more memory than viewport captures. Measure queue, navigation, rendering and output time separately.
Bound retries and make them idempotent. Do not blindly repeat form submissions. Keep credentials out of page JavaScript and logs. Treat remote pages, scripts and downloads as untrusted input and isolate browser workers.
Or skip the browser setup
For a clean screenshot or PDF without managing a browser, ScreenshotNeo provides a GET API. See the ScreenshotNeo API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Before capture, ScreenshotNeo accepts cookie or consent banners and removes more than 60 known consent platforms, newsletter popups and chat widgets; each step can be turned off. Bot checks, blank pages, timeouts, failed loads and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server lets Claude, Cursor and other MCP clients use take_screenshot, get_page_info and capture_pdf. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
FAQ
Does Go automate Chrome by itself?
No. Go calls a library, and the library controls a separate browser process.
Do I need ChromeDriver with Chromedp?
No. Chromedp communicates directly with Chrome DevTools Protocol. Selenium requires a browser-specific WebDriver.
Which library supports Firefox and WebKit?
Playwright for Go provides one API for Chromium, Firefox and WebKit.
When should I use an API instead of browser automation?
Use an API for repeatable screenshots or PDFs when you do not need arbitrary clicks, application state or custom browser logic. Use a Go library when you need to interact with the page.


