Содержание урока · 10 разделов
- chromedp вкладки: NewContext от родителя
- Что общее, а что своё: куки, сторож, процесс
- Навигация по существующим: Targets и WithTargetID
- Утечка вкладок: cancel, память и уборка
- Задачи вокруг нескольких страниц
- Параллельный обход: вкладка на goroutine
- Правила гигиены вкладок
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Человек открывает третий десяток вкладок, не задумываясь, и парсер хочет того же: одна страница для навигации, вторая для карточки товара, третья — внезапный попап, который сайт решил показать. Несколько страниц Go-программа держит тем же способом, что и человек, — вкладками, но под каждой вкладкой chromedp стоит конкретная единица: контекст. Именно контекст определяет, куда приедет следующий Run: у каждого контекста своя цель — target, то есть отдельная страница со своей навигацией и DOM, — и поэтому «работать с двумя вкладками» в терминах библиотеки звучит как «держать два контекста». Каркас при этом уже знаком: NewContext, Run, действия, отмена. Новыми оказываются только две вещи — откуда порождать контекст и когда его закрывать, и обе решаются связкой с родителем. Порядок изучения такой: сначала увидеть, где кончается браузер и начинается вкладка, потом научиться перечислять страницы и подхватывать новые, и в конце — параллелить и закрывать без утечек. К концу урока схема «браузер — вкладка — контекст» будет читаться в коде без усилий, а закрытие вкладки станет планироваться так же заранее, как открытие.
chromedp вкладки: NewContext от родителя#
chromedp.NewContext(parentCtx) создаёт дочерний контекст, и его судьба зависит от родителя: если в родителе уже есть браузер, дочерний контекст получает тот же браузер, но собственную вкладку — первый Run на нём не заводит процесс, а создаёт новую страницу в существующем. Каждый Execute и каждый Run с этим контекстом едут ровно в эту вкладку: навигация, клики и чтения не перелезают на соседние страницы. Второй Run по baseCtx поедет в свою вкладку, не в дочернюю: контексты не переключаются между страницами сами. Родительский контекст при этом продолжает работать со своей вкладкой, как ни в чём не бывало. Внутренности видны через chromedp.FromContext: у обоих контекстов поле Browser одно и то же, а поля Target разные — это точный ответ на вопрос, что общего и что своё. Пример manyTabs из исходников библиотеки печатает ровно это: Same browser: true, Same tab: false. Прогрев вкладки дешёвый — Run(tabCtx) без действий создаёт страницу и готовит её к работе; полезно вызвать его сразу, чтобы вкладка существовала до начала цикла, а ошибки создания ловились в одном месте. Заодно прогрев снимает вопрос «а когда именно появится вкладка»: после него — гарантированно уже.
Главная вкладка и дочерняя: две goroutines
GObaseCtx, cancel := chromedp.NewContext(context.Background())
defer cancel()
// прогрев: создаёт браузер и первую вкладку
if err := chromedp.Run(baseCtx); err != nil {
log.Fatal(err)
}
// вторая вкладка того же браузера
tabCtx, tabCancel := chromedp.NewContext(baseCtx)
defer tabCancel()
if err := chromedp.Run(tabCtx); err != nil {
log.Fatal(err)
}
var wg sync.WaitGroup
wg.Add(2)
go func() { // вкладка один: свой поток действий
defer wg.Done()
err := chromedp.Run(baseCtx,
chromedp.Navigate(url1),
chromedp.WaitVisible(".list"))
log.Println("tab1:", err)
}()
go func() { // вкладка два: независимая страница
defer wg.Done()
err := chromedp.Run(tabCtx,
chromedp.Navigate(url2),
chromedp.WaitVisible(".list"))
log.Println("tab2:", err)
}()
wg.Wait()Оба контекста сидят на одном браузере, но у каждого свой Target — своя страница со своим DOM. Действия одного контекста последовательны: два Run по baseCtx из разных goroutines перетасовали бы команды одной вкладки. Разные вкладки — разные потоки: wg дожидается обеих, defer cancel гасит родителя, а вместе с ним и дочернюю вкладку, если та осталась открытой.
Типичная утечка: в цикле по адресам на каждый URL создаётся свежий chromedp.NewContext, работа идёт, цикл едет дальше — и никто не вызывает cancel. Через сотню страниц Chrome ест гигабайты: каждая забытая вкладка держит DOM, кучи JavaScript и историю, а рантайм Go видит только маленький хэндл. Диагностика простая: chromedp.Targets показывает список открытых целей, и призраки видны сразу. Лечение ещё проще: tabCancel после каждого шага — вкладка закрыта, память возвращена, браузер продолжает работать, а сессии профиля при этом остаются на месте.
Что общее, а что своё: куки, сторож, процесс#
Вкладки одного профиля — соседи: куки и localStorage у них общие, вход, выполненный в одной вкладке, мгновенно виден в другой, и сессия из тринадцатого урока про заголовки и куки работает на все страницы сразу. На уровне браузера делится и кэш: картинка, загруженная одной вкладкой, второй достаётся из памяти, а единый интерфейс браузера обслуживает все страницы сразу — настройки, сертификаты, разрешённые сайты. Своё у каждой вкладки — DOM, JavaScript-куча, история навигации и viewport, поэтому селектор, найденный в одной странице, в другой бессмыслен, а взгляд viewport'а одной вкладки не переезжает на соседку. Полная изоляция делается иначе: опция chromedp.WithNewBrowserContext() при NewContext заводит отдельный BrowserContext — собственную куки-банку, где сторож и хранилище не пересекаются с родительскими вкладками. Это профиль-близнец: тот же сайт выглядит незнакомцем, вход нужен заново, зато и следы не смешиваются. Новый BrowserContext уничтожается при отмене контекста, так что такая изоляция не течёт.
Targets и попап: подхватить то, что открыл сайт
GO// инвентаризация: что сейчас открыто в браузере
infos, err := chromedp.Targets(ctx)
if err != nil {
log.Fatal(err)
}
for _, info := range infos {
log.Printf("%s: %s", info.Type, info.URL)
}
// попап, который сайт откроет по клику
ch := chromedp.WaitNewTarget(ctx, func(info *target.Info) bool {
return info.Type == "page" && info.URL != ""
})
if err := chromedp.Run(ctx, chromedp.Click("#open-popup")); err != nil {
log.Fatal(err)
}
popupCtx, popupCancel := chromedp.NewContext(ctx,
chromedp.WithTargetID(<-ch))
defer popupCancel()
var loc string
if err := chromedp.Run(popupCtx, chromedp.Location(&loc)); err != nil {
log.Fatal(err)
}Targets возвращает срез target.Info по всем целям браузера: тип и адрес видны сразу, призраки и попапы находятся именно здесь. WaitNewTarget подписывается на события целей и отдаёт в канал ID появившейся страницы: сайт открыл окно — WithTargetID строит контекст прямо на него, и все дальнейшие действия едут в попап, а не в основную вкладку.
Навигация по существующим: Targets и WithTargetID#
Вкладки появляются не только от ваших NewContext: сайт открывает попап, OAuth-окно, всплывашку с капчей — и работать надо уже с тем, что открыл кто-то другой. Первая помощь — chromedp.Targets: действие возвращает срез целей с типом и адресом, по нему видно, сколько страниц открыто и какая из кого. Вторая — chromedp.WaitNewTarget: подписка, которая отдаёт в канал ID только что созданной цели по вашему предикату, — так попап ловится в момент рождения, а не поиском по списку после. Полученный ID скармливается WithTargetID: контекст строится поверх существующей вкладки, и обычный Navigate гоняет её по кругу — это и есть навигация между существующими страницами. И помните про типы: цели бывают не только страницами — iframe и worker тоже фигурируют в списке, поэтому фильтр по Type == page — привычка, которую стоит завести сразу. Тот же приём годится для восстановления: программа перезапустилась, браузер ещё жив, контекст потерян — Targets находит открытые страницы, и работа продолжается с любого из них без нового запуска.
Утечка вкладок: cancel, память и уборка#
Отмена — вторая половина жизни вкладки. chromedp.NewContext возвращает пару контекст и cancel не для красоты: отмена контекста вкладки гасит именно её — библиотека отсоединяет сессию от браузера и отдаёт команде закрытия цели, а родительский браузер при этом не трогает: он живёт, пока жив отец. Забытые cancel — самый тихий путь к росту памяти: каждая вкладка держит свой DOM и JavaScript-кучу, и мусорщик Go здесь бессилен, потому что вкладки живут в чужом процессе. Отсюда дисциплина: defer cancel для вкладки, чья жизнь равна жизни программы, и явный tabCancel сразу после шага для вкладок-однодневок из цикла. Особый случай — WithNewBrowserContext: отдельный BrowserContext удаляется вместе с контекстом, и несделанный cancel оставляет после себя целую куки-банку. Если память всё же поползла — chromedp.Targets покажет, сколько целей открыто на самом деле, и список призраков станет точным счётом вашей забывчивости.
Задачи вокруг нескольких страниц#
| Задача | Механизм | Нюанс |
|---|---|---|
| новая вкладка | NewContext от контекста с браузером | первый Run создаёт вкладку, не браузер |
| список открытых страниц | chromedp.Targets(ctx) | срез target.Info: тип и адрес каждой цели |
| подхватить попап | WaitNewTarget + WithTargetID | канал отдаёт ID новой цели по предикату |
| изолированные куки | NewContext с WithNewBrowserContext | свой профиль: куки и сторож не общие |
| закрыть вкладку | cancel контекста вкладки | браузер живёт, пока жив родитель |
| параллельный обход | вкладка на goroutine | один контекст — один поток действий |
Параллельный обход: вкладка на goroutine#
Схема из первого примера масштабируется: сколько вкладок — столько независимых потоков работы, и ожидание медленной страницы больше не тормозит быструю. Правило безопасности простое: один контекст обслуживается одной goroutine — действия контекста последовательны по своей природе, и два Run по одному контексту из разных goroutines перемешают команды в пределах одной вкладки. Параллелить нужно контексты, а не действия. На этом держится и пул из девятнадцатого урока про параллельный пул: N браузеров, у каждого M вкладок, каждая единица работы — контекст в goroutine. Но вкладки — не бесплатный параллелизм: все они живут в одном процессе Chrome и делят его процессор и память, поэтому десять goroutines на одной вкладке — гонка, а пятьдесят вкладок в одном браузере — движение к OOM. Ожидания, таймауты и ретраи из предыдущего урока действуют на каждую вкладку независимо, и дедлайн одной страницы не задевает соседнюю. Разумный потолок — единицы вкладок на браузер, а дальше добавляйте браузеры: если задача не влезает в один Chrome, не мучайте вкладки сверх меры — следующий уровень масштаба — отдельные браузеры из аллокатора.
Тематическая песочница исполняет учебную программу на чистом Go без браузера: вкладок, попапов и реального Chrome в ней нет, и честно не воспроизвести ни WaitNewTarget, ни утечку вкладок. Всё из этого урока проверяется локально: установленный Chrome из второго урока, любой сайт с кнопкой window.open — и попап ловится по-настоящему. Годится и чистый пример без попапа: две вкладки с навигацией уже показывают разделение контекстов. Для повторения примеров удобен httptest-сервер: страница с onclick='window.open' даёт безопасный полигон для тренировки подхвата цели.
Правила гигиены вкладок#
- контекст — вкладка: NewContext от родителя с браузером, не от context.Background() случайно
- первый Run новой вкладки — прогрев: Run(tabCtx) без действий сразу после создания
- cancel после шага: вкладка закрыта, память возвращена, браузер продолжает работать
- Targets как инвентаризация: вкладки-призраки видны в списке целей
- изоляция — WithNewBrowserContext: куки и сторож не пересекаются с основным профилем
- параллель — вкладка на goroutine: один контекст обслуживает один поток
Проверка: что общее у вкладок
Пример manyTabs из исходников: ctx2 := chromedp.NewContext(ctx1), оба Run выполнены. Что печатает сравнение c1.Browser == c2.Browser и c1.Target == c2.Target?
Проверка: cancel дочернего контекста
Родительский контекст держит браузер, дочерний — вторую вкладку. Что произойдёт при отмене дочернего контекста?
Что сделает первый Run второй вкладки
Прочитайте код и выберите, что он выведет:
baseCtx, cancel := chromedp.NewContext(context.Background())
defer cancel()
chromedp.Run(baseCtx) // браузер заведён
tabCtx, _ := chromedp.NewContext(baseCtx)
err := chromedp.Run(tabCtx)Соберите подхват попапа
Восстановите каркас: подписка на новые цели, контекст на ID попапа и его освобождение.
Расставьте каркас двух вкладок
Соберите по порядку: браузер с прогревом, вторая вкладка, две goroutines и ожидание.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: вкладки и окна
Что дальше: вкладки — каркас масштаба, но страницы редко живут в вакууме — они ходят в сеть. Семнадцатый урок смотрит на эту сторону: перехват сетевых запросов — события сети, тела XHR-ответов и блокировка трекеров. Восемнадцатый продолжает тему экономии — блокировка картинок и шрифтов — и собирает лёгкий профиль страницы. Практика — локально: песочница сайта работает без браузера, поэтому вкладки и попапы проверяются только на настоящем Chrome; в песочнице нет ни окна, ни цели, которую можно подхватить. Домашняя проверка на честность: откройте циклом десять вкладок без cancel, вызовите chromedp.Targets и посчитайте призраков, затем добавьте tabCancel и убедитесь, что список опустел, а память браузера вернулась к стартовой. Цель всех проверок одна: почувствовать, что вкладка — ресурс, который вы создаёте и возвращаете, а не фон, живущий сам по себе.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Вкладка — это контекст: NewContext от родителя с браузером создаёт новую страницу, и все действия контекста едут в неё.
- У вкладок общий Browser и свои Target: браузер один, страницы разные — так отвечает пример manyTabs из исходников.
- Куки и localStorage общие для вкладок профиля; изоляция — WithNewBrowserContext, отдельная банка, уничтожаемая при отмене.
- chromedp.Targets перечисляет открытые цели: инвентаризация, поиск призраков, восстановление после перезапуска.
- Попап подхватывается связкой WaitNewTarget и WithTargetID: контекст строится поверх цели, открытой самим сайтом.
- cancel контекста вкладки закрывает её; без cancel вкладки копятся, и память растёт с темпом обхода.
- Первый Run вкладки — прогрев: Run(tabCtx) без действий создаёт страницу заранее.
- Один контекст — одна goroutine: действия контекста последовательны, параллелят вкладки, а не действия одной страницы.
- Пул из девятнадцатого урока — та же схема в большом масштабе: браузеры на аллокаторах и вкладки на goroutines с рабочим потолком по памяти.
Домашнее задание
Соберите программу из двух вкладок: браузер с прогревом, дочерний NewContext, две goroutines, каждая возит свою вкладку по трём адресам и пишет время шага. Добавьте страницу с кнопкой window.open: кликните её и подхватите попап через WaitNewTarget и WithTargetID, выведите адрес попапа. Затем испортите программу — уберите tabCancel из цикла на десять адресов — и сравните chromedp.Targets до и после: посчитайте вкладки-призраки. Финал: заведите изолированную вкладку через WithNewBrowserContext и убедитесь, что вход, выполненный в основной вкладке, в ней не виден. Лог времени шагов подскажет, даёт ли параллельность реальный выигрыш на ваших страницах.
Личные заметкиПусто
Вопросы и ответы по уроку
Что сломается при двух Run по одной вкладке из разных goroutines?
Действия контекста последовательны: команды двух goroutines перемешаются в пределах одной вкладки — Navigate одной вместо Navigate другой, клик не туда, чужой DOM под чужими ожиданиями. Ошибок в духе «контекст занят» библиотека не вернёт, поэтому гонку не поймать по логам — она проявляется испорченными шагами. Правило: один контекст — один поток. Параллельную работу делает новая вкладка: NewContext от того же родителя, и у goroutine появляется собственная страница.
Чем вкладка отличается от окна?
Для chromedp — ничем существенным: и вкладка, и отдельное окно являются целью типа page со своим ID, и контекст через WithTargetID прицепляется к любой. Разница в интерфейсе браузера, а не в протоколе: целевые события, поля target.Info и действия одинаковы. Практически это значит, что вся механика урока — Targets, WaitNewTarget, cancel — работает и с настоящими окнами, и с попапами, которые сайт открыл как отдельные окна.
Как выбрать: несколько вкладок одного браузера или несколько браузеров?
Вкладки дешевле: они уже в памяти процесса, старт мгновенный, но делят процессор и память одного Chrome и общие куки профиля. Отдельный браузер изолирует всё — ресурсы, профиль, крэши — но стоит на запуск и место в памяти. Разумная лестница: вкладки для лёгкого параллелизма в пределах профиля, WithNewBrowserContext для изоляции сессий, отдельные браузеры из аллокатора для тяжёлых и независимых задач — об этом девятнадцатый урок.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Testcontainers в Go: интеграционные тесты с Docker
Курс testcontainers-go: Postgres, Redis, Kafka и Mongo в тестах через Docker, wait-стратегии, сети, параллельность, CI и итоговый стенд. 25 уроков.
- Поднимать реальные зависимости в Docker из кода теста
- Использовать модули Postgres, Redis, MongoDB, Kafka и RabbitMQ
- Ждать готовность сервиса wait-стратегиями вместо sleep
- Собирать стенды из нескольких контейнеров и своего образа