Для подписчиковВ этой статье мы рассмотрим основные приемы работы с каналами в Go и познакомимся с приемами и практиками написания многопоточного кода. В качестве практичного примера напишем свой быстрый фаззер, который будет проверять, есть ли на хосте файлы из словаря.
В этой статье мы рассмотрим основные приемы работы с каналами в Go и познакомимся с приемами и практиками написания многопоточного кода. В качестве практичного примера напишем свой быстрый фаззер, который будет проверять, есть ли на хосте файлы из словаря.
Принятая в Go реализация конкурентности через обмен сообщениями между независимыми друг от друга потоками называется CSP (Communicating Sequential Processes). В ее основе — каналы, передающие сообщения от одного потока (горутины) к другому.
Если ты знаком с концепцией системных каналов (pipes), то уже примерно понимаешь, что к чему. Однако, рантайм Go не использует средства операционной системы, у него имеется собственная реализация горутин и каналов.
В саму методику фаззинга мы здесь погружаться не будем: все‑таки основная цель — освоить Go, а потому концентрируемся сейчас на инструментах, предоставляемых языком программирования. Так что реализация будет максимально простая: добавляем к имени хоста слово из словаря, затем выполняем GET запрос по получившемуся адресу и сохраняем код ответа.
Словарь можно взять, например, здесь, но я советую не брать очень большой список, а написать свой буквально из десятка позиций — этого более чем достаточно для начала.
Часто модель CSP описывают одним утверждением: «не реализовывай коммуникацию посредством совместного использования памяти; вместо этого совместно используй память посредством коммуникации».
Модель CSP разработана и описана Чарльзом Энтони Хоаром — возможно, ты уже слышал его имя в связи с алгоритмом быстрой сортировки (qsort).
Создадим новый модуль:
mkdir dirfuzzer && cd $_
go mod init dirfuzzer
touch main.go
Разделяем код на модули
В файл main.go вставим следующий код:
package main
import (
"bufio"
"fmt"
"io"
"net/http"
"os"
"strings"
"sync"
"time"
)
type Result struct {
Name string
Code int
}
// produce генерирует задания для обработки,
// комбинируя host со значениями, считанными из filename,
// и помещает их в канал outCh.
func produce(filename string, host string, outCh chan<- string) {
file, err := os.Open(filename)
if err != nil {
fmt.Fprintf(os.Stderr, "opening %s: %v\n", filename, err)
return
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
s := strings.TrimSpace(scanner.Text())
if s == "" {
continue
}
outCh <- "https://" + host + "/" + s
}
if err := scanner.Err(); err != nil {
fmt.Fprintf(os.Stderr, "reading %s: %v\n", filename, err)
}
}
// worker получает значения из канала inCh, пока он остается открытым, выполняет обработку и помещает результаты в outCh.
func worker(c *http.Client, inCh <-chan string, outCh chan<- Result) {
for job := range inCh {
resp, err := c.Get(job)
if err != nil {
continue
}
resp.Body.Close()
io.Copy(io.Discard, resp.Body)
result := Result{
Name: job,
Code: resp.StatusCode,
}
outCh <- result
}
}
// collect получает значения из канала resultCh, пока он остается открытым, и записывает их в файл filename.
func collect(filename string, resultCh <-chan Result) {
dstFile, err := os.Create(filename)
if err != nil {
fmt.Fprintf(os.Stderr, "creating %s: %v\n", filename, err)
return
}
defer dstFile.Close()
writer := bufio.NewWriter(dstFile)
for r := range resultCh {
s := fmt.Sprintf("%s - %d %s\n", r.Name, r.Code, http.StatusText(r.Code))
_, err = writer.WriteString(s)
if err != nil {
fmt.Fprintf(os.Stderr, "writing to %s: %v\n", filename, err)
}
}
if err = writer.Flush(); err != nil {
fmt.Fprintf(os.Stderr, "writing to %s: %v\n", filename, err)
}
}
func main() {
// TODO
}
Как видишь, мы логически распределили фрагменты кода между отдельными изолированными функциями в соответствии с ролями, которые на этот код возложены.
Функция produce() генерирует задания для последующей обработки. Она вычитывает строки из списка и формирует URL, по которым будут осуществляться попытки соединения. Сама обработка лежит вне зоны ответственности этой функции и никак не влияет на ее код. Другие часто используемые имена для такого кода: generate, source.
Функция worker() обрабатывает задания. В данном случае, выполняется GET запрос к серверу и сохранение кода ответа. Как ты видишь, функция «не знает» ничего ни о происхождении заданий, ни о дальнейшей судьбе результатов — это к ней не относится. Другие часто встречающиеся имена для такой роли: process, handle.
Функция collect() вычитывает результаты и сохраняет их в файл. По аналогии с предыдущими она ничего не знает и не должна знать о том, откуда и каким образом эти результаты взялись. У нее есть только одна четко очерченная зона ответственности — собрать и сохранить. Другие часто встречающиеся имена: consume, sink, save.
Подобное разделение воплощает принцип построения программных систем loose couping, high cohesion (один из вариантов перевода: «слабая связанность, сильное сцепление»). Согласно этому принципу, система строится из отдельных компонентов, которые минимально зависят друг от друга (это и есть loose coupling). При этом каждый такой компонент выполняет четко очерченную роль, и код, ответственный за ту или иную функцию, сконцентрирован внутри «своего» компонента, а не размазан по другим (а это — high cohesion).
Такой подход облегчает понимание работы системы, позволяет применять модульное тестирование, улучшает повторное использование кода и дает возможность в будущем наращивать возможности.
Если мы захотим, к примеру, сохранять результаты в другом формате или вообще писать их не в файл, а, скажем, отправлять по сети на другой сервер — нам нужно только заменить компонент collect, не трогая остальные и даже не читая их код. Это общий принцип, он применим не только к отдельным функциям или классам, но и к любым компонентам программной системы в широком понимании этого слова.
Разделение большого монолитного куска кода, решающего ту или иную задачу, на отдельные компоненты, называется модуляризацией (modularization). Не знаю, как тебе, а мне это трудно даже произнести, поэтому я предпочитаю родственное понятие — декомпозиция. Хотя, конечно, оно больше про подход к решению задачи, нежели про структурирование реализации этого решения.
Запускать эти функции будем как отдельные горутины. При этом мы откажемся от «классического» подхода к обмену информацией через совместно используемые объекты, защищенные мьютексами. Вместо этого воспользуемся каналами — специальным инструментом коммуникации между горутинами. Каналы потокобезопасны по определению и не требуют использования мьютексов или иных примитивов синхронизации.
| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | Как сделать свой статический анализатор для Go? | 0 | 8.96 | 12-01-2026 |
| 2 | Покрываем REST-сервис на Rust тестами | 0 | 5 | 05-05-2026 |
| 3 | Библиотека на Go для опроса оборудования по SNMP версии 3 (а та | 0 | 5 | 28-04-2026 |
| 4 | Пишем простой REST-сервис на Rust | 0 | 5 | 16-04-2026 |
| 5 | Обыск в офисе компании: как проходит на практике | 0 | 5 | 27-06-2026 |
| 6 | fz – сборка ассемблера и C без make/cmake, одна команда, парал | 7 | 6 | 18-05-2026 |
| 7 | Когда может пригодиться экзотика в ООП: миксины/трейты/аспекты | 0 | 5 | 08-07-2026 |
| 8 | Безопасно ли запускать GitHub Advanced Security? | 0 | 5 | 03-06-2026 |
| 9 | Заумство терминологий | 0 | 5 | 29-03-2026 |
| 10 | ПУТЬ ГЕРОЯ | КНИГА 1 ЧАСТЬ 1 из 3 | ... | 0 | 5 | 27-06-2026 |