Семафор ограничивает число одновременных операций. В Go его проще всего реализовать через буферизированный канал — размер буфера равен лимиту конкурентности.
sem := make(chan struct{}, 5) // максимум 5 одновременных операций
for _, url := range urls {
sem <- struct{}{} // захватить слот (заблокируется если 5 уже заняты)
go func(u string) {
defer func() { <-sem }() // освободить слот
fetch(u)
}(url)
}
Запись в канал — acquire, чтение — release. Когда буфер полон, следующая запись блокируется, и новая горутина не запустится, пока кто-то не освободит слот. Это проще и идиоматичнее, чем использовать sync-примитивы. Для продакшена есть golang.org/x/sync/semaphore с поддержкой weighted acquire и контекста с таймаутом.