Задача «Получить значение у N url из списка» с собеседования на Go

от автора

На данный момент я нахожусь в активном поиске нового проекта, поэтому активно хожу на собеседования.

Решил поделиться своими мыслями о решении задачи, которую (как мне кажется) часто дают на собеседованиях.

Задача

Написать функцию, которая принимает несколько 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.


ссылка на оригинал статьи https://habr.com/ru/articles/791874/