Задача «Получить значение у N url из списка» с собеседования на Go
- четверг, 8 февраля 2024 г. в 00:00:20
На данный момент я нахожусь в активном поиске нового проекта, поэтому активно хожу на собеседования.
Решил поделиться своими мыслями о решении задачи, которую (как мне кажется) часто дают на собеседованиях.
Написать функцию, которая принимает несколько url адресов, а отдает сумму байт body ответов списка адресов и ошибку, если что-то пошло не так (если произошла ошибка, нужно вернуть ошибку как можно скорее, значение - не важно).
Интересно обсудить варианты решения?
Итак, у нас отдельная программа. Есть 2 набора данных - с успешным и не успешным кейсом. Причем в наборах данных неуспешного кейса специально включены разные зоны. Набор данных придумывал я сам, поэтому если считаете, что их охват не полный, напишите в комментариях.
Банальный вариант
В банальном варианте (чтобы работало) мы берем и просто обходим весь набор данных. Зато вариант - рабочий!
// Банальный синхронный вариант
package main
import (
"fmt"
"io"
"net/http"
"time"
)
const byteInMegabyte = 1024 * 1024
func main() {
urlsList1 := []string{
"https://youtube.com",
"https://ya.ru",
"https://reddit.com",
"https://google.com",
"https://mail.ru",
"https://amazon.com",
"https://instagram.com",
"https://wikipedia.org",
"https://linkedin.com",
"https://netflix.com",
}
urlsList2 := append(urlsList1, "https://111.321", "https://999.000")
{
t1 := time.Now()
byteSum, err := requesSumm(urlsList1)
fmt.Printf("Сумма страниц в Мб=%.2f, ошибка - %v \n", (float64(byteSum) / byteInMegabyte), err)
fmt.Printf("Время выполнение запросов %.2f сек. \n", time.Now().Sub(t1).Seconds())
}
fmt.Println("++++++++")
{
t1 := time.Now()
byteSum, err := requesSumm(urlsList2)
fmt.Printf("Сумма страниц в Мб=%.2f, ошибка - %v \n", (float64(byteSum) / byteInMegabyte), err)
fmt.Printf("Время выполнение запросов %.2f сек. \n", time.Now().Sub(t1).Seconds())
}
}
func requesSumm(urlsSlv []string) (int64, error) {
var sum int64
client := &http.Client{
Timeout: 10 * time.Second,
}
for _, v := range urlsSlv {
resp, err := client.Get(v)
if err != nil {
return 0, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return 0, err
}
sum += int64(len(body))
}
return sum, nil
}
Время выполнение, как думаю понятно из определения, равно сумме всех запросов.
ilia@goDevLaptop sobesi % go run httpget/v1.go
Сумма страниц в Мб=2.12, ошибка - <nil>
Время выполнение запросов 16.01 сек.
++++++++
Сумма страниц в Мб=0.00, ошибка - Get "https://111.321": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
Время выполнение запросов 18.88 сек.
ilia@goDevLaptop sobesi %
Затем очевидный вариант для языка Golang - это подключение асинхронного вызова, основанного на отдельных горутинах. Давайте посмотрим, как изменится время выполнения?
// Банальный ассинхронный вариант
package main
import (
"fmt"
"io"
"net/http"
"sync"
"time"
)
const byteInMegabytev2 = 1024 * 1024
type respSt struct {
lenBody int64
err error
}
func main() {
urlsList1 := []string{
"https://youtube.com",
"https://ya.ru",
"https://reddit.com",
"https://google.com",
"https://mail.ru",
"https://amazon.com",
"https://instagram.com",
"https://wikipedia.org",
"https://linkedin.com",
"https://netflix.com",
}
urlsList2 := append(urlsList1, "https://111.321", "https://999.000")
{
t1 := time.Now()
byteSum, err := requesSummAsync(urlsList1)
fmt.Printf("Сумма страниц в Мб=%.2f, ошибка - %v \n", (float64(byteSum) / byteInMegabytev2), err)
fmt.Printf("Время выполнение запросов %.2f сек. \n", time.Now().Sub(t1).Seconds())
}
fmt.Println("++++++++")
{
t1 := time.Now()
byteSum, err := requesSummAsync(urlsList2)
fmt.Printf("Сумма страниц в Мб=%.2f, ошибка - %v \n", (float64(byteSum) / byteInMegabytev2), err)
fmt.Printf("Время выполнение запросов %.2f сек. \n", time.Now().Sub(t1).Seconds())
}
}
func requesSummAsync(urls []string) (int64, error) {
var wg sync.WaitGroup
ansCh := make(chan respSt, len(urls))
client := &http.Client{
Timeout: 10 * time.Second,
}
for _, url := range urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
resp, err := client.Get(u)
if err != nil {
ansCh <- respSt{
lenBody: 0,
err: err,
}
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
ansCh <- respSt{
lenBody: 0,
err: err,
}
return
}
ansCh <- respSt{
lenBody: int64(len(body)),
err: nil,
}
}(url)
}
go func() {
wg.Wait()
close(ansCh)
}()
var sum int64
var err error
for bodyLen := range ansCh {
sum += bodyLen.lenBody
if bodyLen.err != nil {
if err == nil {
err = fmt.Errorf("Ошибка %v у сайта %v", bodyLen.err)
continue
}
err = fmt.Errorf("Ошибка %v у сайта %v;%v", bodyLen.err, err)
}
}
if err != nil {
return 0, err
}
return sum, err
}
Фактически время выполнение будет равно выполнению самого медленного запроса + время на сложение.
ilia@goDevLaptop sobesi % go run httpget/v2.go
Сумма страниц в Мб=2.50, ошибка - <nil>
Время выполнение запросов 2.81 сек.
++++++++
Сумма страниц в Мб=0.00, ошибка - Ошибка Get "https://111.321": context deadline exceeded (Client.Timeout exceeded while awaiting headers) у сайта Ошибка Get "https://999.000": dial tcp: lookup 999.000: no such host у сайта %!v(MISSING);%!v(MISSING)
Время выполнение запросов 10.00 сек.
ilia@goDevLaptop sobesi %
Тайм аут запроса 10 секунд, но можно ли улучшить скорость в нашей задаче?
Давайте дополним реализацию выше еще и общим контекстом, который будет общим для всех созданных горутин.
// Ассинхронный вариант с контекстом
package main
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"sync"
"time"
)
type respStC struct {
lenBody int64
err error
}
const byteInMegabytev3 = 1024 * 1024
func main() {
urlsList1 := []string{
"https://youtube.com",
"https://ya.ru",
"https://reddit.com",
"https://google.com",
"https://mail.ru",
"https://amazon.com",
"https://instagram.com",
"https://wikipedia.org",
"https://linkedin.com",
"https://netflix.com",
}
urlsList2 := append(urlsList1, "https://111.321", "https://999.000")
{
t1 := time.Now()
byteSum, err := requestSumAsyncWithCtx(urlsList1)
fmt.Printf("Сумма страниц в Мб=%.2f, ошибка - %v \n", (float64(byteSum) / byteInMegabytev3), err)
fmt.Printf("Время выполнение запросов %.2f сек. \n", time.Now().Sub(t1).Seconds())
}
fmt.Println("++++++++")
{
t1 := time.Now()
byteSum, err := requestSumAsyncWithCtx(urlsList2)
fmt.Printf("Сумма страниц в Мб=%.2f, ошибка - %v \n", (float64(byteSum) / byteInMegabytev3), err)
fmt.Printf("Время выполнение запросов %.2f сек. \n", time.Now().Sub(t1).Seconds())
}
}
func requestSumAsyncWithCtx(urls []string) (int64, error) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var wg sync.WaitGroup
ansCh := make(chan respStC, len(urls))
client := &http.Client{
Timeout: 10 * time.Second,
}
for _, url := range urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
req, err := http.NewRequestWithContext(ctx, "GET", u, nil)
if err != nil {
ansCh <- respStC{lenBody: 0, err: err}
return
}
resp, err := client.Do(req)
if err != nil {
ansCh <- respStC{lenBody: 0, err: err}
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
ansCh <- respStC{lenBody: 0, err: err}
return
}
ansCh <- respStC{lenBody: int64(len(body)), err: nil}
}(url)
}
go func() {
wg.Wait()
close(ansCh)
}()
var sum int64
var err error
for bodyLen := range ansCh {
sum += bodyLen.lenBody
if bodyLen.err != nil && !errors.Is(bodyLen.err, context.Canceled) {
if err != nil {
err = fmt.Errorf("Ошибка %v у сайта %v;%v", bodyLen.err, bodyLen.lenBody, err)
} else {
err = fmt.Errorf("Ошибка %v у сайта %v", bodyLen.err, bodyLen.lenBody)
}
cancel()
}
}
return sum, err
}
Теперь посмотрим на время исполнения.
ilia@goDevLaptop sobesi % go run httpget/v3.go
Сумма страниц в Мб=2.50, ошибка - <nil>
Время выполнение запросов 2.89 сек.
++++++++
Сумма страниц в Мб=0.00, ошибка - Ошибка Get "https://999.000": dial tcp: lookup 999.000: no such host у сайта 0
Время выполнение запросов 0.00 сек.
ilia@goDevLaptop sobesi %
И вот уже получается, что мы можем не ждать каждый запрос, а вернем ошибку сразу.
Но реальная жизнь, это всегда борьба с ограничениями, и, если нам дадут слишком большой список (больше доступных сетевых соединений) в той среде, где выполняется программа, то мы уже получим не определенное поведение как нашей программы, так и среды вокруг. Поэтому мы ограничим возможность программы создавать больше заданного количества сетевых соединений одновременно.
Для этого конечно мы воспользуемся буфферизированным каналом ;-)
// Ассинхронный вариант с контекстом и пулом соединений в poolHTTPReq
package main
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"sync"
"time"
)
type respStCWP struct {
lenBody int64
err error
}
const poolHTTPReq = 2
const byteInMegabytev4 = 1024 * 1024
func main() {
urlsList1 := []string{
"https://youtube.com",
"https://ya.ru",
"https://reddit.com",
"https://google.com",
"https://mail.ru",
"https://amazon.com",
"https://instagram.com",
"https://wikipedia.org",
"https://linkedin.com",
"https://netflix.com",
}
urlsList2 := append(urlsList1, "https://111.321", "https://999.000")
{
t1 := time.Now()
byteSum, err := requestSumAsyncWithCtxAndPool(urlsList1)
fmt.Printf("Сумма страниц в Мб=%.2f, ошибка - %v \n", (float64(byteSum) / byteInMegabytev4), err)
fmt.Printf("Время выполнение запросов %.2f сек. \n", time.Now().Sub(t1).Seconds())
}
fmt.Println("++++++++")
{
t1 := time.Now()
byteSum, err := requestSumAsyncWithCtxAndPool(urlsList2)
fmt.Printf("Сумма страниц в Мб=%.2f, ошибка - %v \n", (float64(byteSum) / byteInMegabytev4), err)
fmt.Printf("Время выполнение запросов %.2f сек. \n", time.Now().Sub(t1).Seconds())
}
}
func requestSumAsyncWithCtxAndPool(urls []string) (int64, error) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var wg sync.WaitGroup
ansCh := make(chan respStCWP, len(urls))
semaphore := make(chan struct{}, poolHTTPReq)
for _, url := range urls {
semaphore <- struct{}{}
wg.Add(1)
go func(u string) {
defer func() {
<-semaphore
wg.Done()
}()
req, err := http.NewRequestWithContext(ctx, "GET", u, nil)
if err != nil {
ansCh <- respStCWP{lenBody: 0, err: err}
return
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
ansCh <- respStCWP{lenBody: 0, err: err}
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
ansCh <- respStCWP{lenBody: 0, err: err}
return
}
ansCh <- respStCWP{lenBody: int64(len(body)), err: nil}
}(url)
}
go func() {
wg.Wait()
close(ansCh)
close(semaphore)
}()
var sum int64
var err error
for bodyLen := range ansCh {
sum += bodyLen.lenBody
if bodyLen.err != nil && !errors.Is(bodyLen.err, context.Canceled) {
if err != nil {
err = fmt.Errorf("Ошибка %v у сайта %v;%v", bodyLen.err, bodyLen.lenBody, err)
} else {
err = fmt.Errorf("Ошибка %v у сайта %v", bodyLen.err, bodyLen.lenBody)
}
cancel()
}
}
return sum, err
}
И получим значения, конечно, хуже предыдущего варианта, но уже более приближенные к жизни.
ilia@goDevLaptop sobesi % go run httpget/v4.go
Сумма страниц в Мб=2.50, ошибка - <nil>
Время выполнение запросов 9.05 сек.
++++++++
Сумма страниц в Мб=2.12, ошибка - Ошибка Get "https://999.000": dial tcp: lookup 999.000: no such host у сайта 0
Время выполнение запросов 4.29 сек.
ilia@goDevLaptop sobesi %
Весь код естественно выложен на GitHub
Если вам интересны подобные статьи или у вас есть вопросы, замечания, пожелания - обязательно напишите комментарий.
А так же подписывайтесь на мой телеграмм канал, где я публикую свои мысли на все интересное, что попадает мне на глаза из мира it.