Прерывание http запроса

173 views
Skip to first unread message

forwo...@gmail.com

unread,
Nov 29, 2017, 12:16:20 PM11/29/17
to Golang Russian
Подскажите пожалуйста как можно прерывать выполнение горутин выполняющих загрузку данных с web страниц?

package main

import (
   
"bytes"
   "errors"
   "fmt"
   "github.com/levigross/grequests"
   "golang.org/x/net/html/charset"
   "io/ioutil"
)

var workerComplete = make(chan []string)


func fetch(url string) (string, error) {
   response
, err := grequests.Get(url, nil)
   
if err != nil {
     
return "", errors.New("Ошибка при выполнении запроса: " + err.Error())
   
}

   
defer response.ClearInternalBuffer()

   raw_page
, err := charset.NewReader(bytes.NewReader(response.Bytes()), response.Header.Get("Content-Type"))
   
if err != nil {
     
return "", errors.New("Ошибка при определении кодировки: " + err.Error())
   
}

   unicodePage
, err := ioutil.ReadAll(raw_page)
   
if err != nil {
     
return "", errors.New("Ошибка при чтении декодированного response : " + err.Error())
   
}

   
return string(unicodePage), nil
}

func downloadWebPage(url_ string, pageDownloaded chan []string) {

   html
, err := fetch(url_)
   
if err != nil {
      err
= errors.New("Ошибка при получении данных со страницы " + url_ + " : " + err.Error())
      pageDownloaded
<- []string{"", err.Error()}
   
}
   pageDownloaded
<- []string{html, ""}
}

func main() {
   urls
:= []string{"http://golang-book.ru/chapter-10-concurrency.html",
     
"http://golang-book.ru/chapter-10-concurrency.html", "https://gobyexample.com/json"}

   urlsCount
:= 0

   for _, url_ := range urls {
         urlsCount
+= 1
         go downloadWebPage(url_, workerComplete)
   
}


   
for i := 0; i < urlsCount; {
      result
:= <-workerComplete
      html
, err := result[0], result[1]

     
if err != "" {
         fmt
.Println(err)
     
}

      fmt
.Println(html[:10])
      i
+= 1
   }

}


Alex Lurye

unread,
Nov 29, 2017, 3:30:30 PM11/29/17
to gola...@googlegroups.com

В стандартной библиотеке Go есть возможность отправлять запросы с контекстом: https://golang.org/pkg/net/http/#Request.WithContext
Если контекст отменится, запрос тут же прервется и вернёт ошибку.

Использовать примерно так:

ctx, cancel := context.WithCancel(context.Background())

req := &Request{...}
req = req.WithContext(ctx)
resp, err := http.DefaultTransport.RoundTrip(req)

Когда надо прервать, вызываете cancel(), и все запросы, которые этот контекст используют, автоматически прервутся и вернут context.Canceled.

Вы используете какую-то стороннюю библиотеку, API которой, похоже, не позволяет отменять запросы.

--
Вы получили это сообщение, поскольку подписаны на группу "Golang Russian".
Чтобы отменить подписку на эту группу и больше не получать от нее сообщения, отправьте письмо на электронный адрес golang-ru+...@googlegroups.com.
Чтобы настроить другие параметры, перейдите по ссылке https://groups.google.com/d/optout.

forwo...@gmail.com

unread,
Dec 1, 2017, 2:11:50 PM12/1/17
to Golang Russian
Объясни пожалуйста почему вот такой код не работает? И что значит вот такая вот запись case <-cx.Done():, когда не указывается канал куда будет происходить запись.

package main

import (
   
"context"
   "net/http"
   "time"
   "fmt"
)

func main() {
   cx
, cancel := context.WithCancel(context.Background())
   req
, _ := http.NewRequest("GET", "http://google.com", nil)
   req
= req.WithContext(cx)
   ch
:= make(chan []string)

   
go func() {
      _
, err := http.DefaultClient.Do(req)
     
select {
     
case <-cx.Done():
      default:
         ch
<- []string{"body", err.Error()}
     
}
   
}()

   
// Simulating user cancel request
   go func() {
      time
.Sleep(100 * time.Millisecond)
      fmt
.Println(cancel)
     
//cancel()
   }()
   
select {
   
case err := <-ch:
     
if err != nil {
         
// HTTP error
         fmt.Println("error without calcel")
     
}
      fmt
.Println("body")
   
case <-cx.Done():
      fmt
.Println("cancel")
   
}

}


Alex Lurye

unread,
Dec 1, 2017, 2:36:55 PM12/1/17
to gola...@googlegroups.com
Блин, почта - не лучший инструмент для code review, но попробую... В следующий раз - залейте куда-нибудь, где можно комментить построчно.
   req, _ := http.NewRequest("GET", "http://google.com", nil)
Никогда не игнорируйте ошибки. Это, в лучшем случае, может привести к крашам по nil pointer dereference, а в худшем - к трудноуловимым багам.
   _, err := http.DefaultClient.Do(req)
1. Вы делаете запрос без использования контекста - прервать его невозможно. Вам надо перед этой строчкой вставить
req = req.WithContext(cx)
2. Вы игнорируете собственно тело ответа. Для демки, наверное, ок, но в будущем - не стоит.

      select {
     
case <-cx.Done():
      default:
         ch
<- []string{"body", err.Error()}
     
}
Вместо всей этой конструкции вам нужно что-то типа
if err != nil {
  ch <- []string{"", err.Error()}
  return
}

А в случае успеха - вычитать реальный ответ и вернуть его:
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
  // Соединение прервалось на середине и данные не докачались.
  ch <- []string{"", err.Error()}
  return
}
ch <- []string{string(data), ""}
   select {
   
case err := <-ch:
     
if err != nil {
         
// HTTP error
         fmt.Println("error without calcel")
     
}
      fmt
.Println("body")
   
case <-cx.Done():
      fmt
.Println("cancel")
   
}
Здесь не понятно, что именно вы хотите сделать. Текущая реализация вообще имеет недетерминированное поведение. Когда контекст отменяется, либо сработает case <-ct.Done(), либо горутина успеет отмениться и насрать в канал, и тогда первый case может сработать. Это гонка. Я бы просто дождался, когда горутина вернёт хоть что-нибудь - или ошибку, или не ошибку. Я бы так сделал:
res := <-ch
if res[1] != "" {
  select {
  case <-cx.Done():
    // Отмена пользователем.
    fmt.Println("cancel")
  default:
    // Запрос не удался.
    fmt.Println(res[1])
  }
} else {
  // Нормальный ответ.
  fmt.Println(res[0])
}

Ну и ещё метакомментарий - обработку ошибок лучше делать по-другому. Не надо их в строки передавать - а возвращать надо, как есть.

> И что значит вот такая вот запись case <-cx.Done():, когда не указывается канал куда будет происходить запись.

Это чтение из канала и игнорирование результата. Объект Context никогда туда реально ничего не пишет, но когда контекст прерывается, канал закрывается, и выполняется эта ветка селекта.
...

forwo...@gmail.com

unread,
Dec 1, 2017, 4:04:42 PM12/1/17
to Golang Russian
Спасибо за объяснения. Код такой кривой потому, что я его взял с интернета и сам толком не понимал, что там происходит. Обработку ошибок не делал, потому что пытался понять концепцию.

Я вот немного не понимаю как мне возвращать именно ошибку, а не строку с описанием ошибки, строгая типизация же.

Исправил основные косяки о которых ты написал. Посмотри пожалуйста, все я исправил или нет?

P.S. огромное спасибо, что помогаешь, в начале изучения это очень сильно необходимо

package main

import (
   
"context"
   "net/http"
   "time"
   "fmt"
   "io/ioutil"
)

var ch = make(chan []string)


func main() {
   cx
, cancel := context.WithCancel(context.Background())
   req
, _ := http.NewRequest("GET", "http://google.com", nil)
   req
= req.WithContext(cx)


   
go func() {
      resp
, err := http.DefaultClient.Do(req)


     
if err != nil {
         ch
<- []string{"", err.Error()}
         
return
      }


      data
, err := ioutil.ReadAll(resp.Body)

     
if err != nil {
         
// Соединение прервалось на середине и данные не докачались.
         ch <- []string{"", err.Error()}
         
return
      }

      resp
.Body.Close()
      ch
<- []string{string(data), ""}

   
}()

   
// Simulating user cancel request
   go func() {
      time
.Sleep(100 * time.Millisecond)
      fmt
.Println(cancel)
     
//cancel()
   }()


   res
:= <-ch
   
if res[1] != "" {

     
select {
     
case <-cx.Done():
         
// Отмена пользователем.
         fmt.Println("Запрос отменен пользователем")

     
default:
         
// Запрос не удался.
         fmt.Println("Ошибка при выполнении запроса: ", res[1])
     
}
   
} else {
     
// Нормальный ответ.
      fmt.Println("Нормальный ответ: ", res[0][:50])
   
}

}


forwo...@gmail.com

unread,
Dec 1, 2017, 4:09:50 PM12/1/17
to Golang Russian
И ещё не совсем понял зачем в этом куске кода select. Ведь мы до входа в условие уже дождались возврата ответа от горутины, а select на сколько я понял служит для того, чтобы ожидать данных из каналов.

res := <-ch
if res[1] != "" {
   
select {
   
case <-cx.Done():
     
// Отмена пользователем.
      fmt.Println("Запрос отменен пользователем")

   
default:
     
// Запрос не удался.
      fmt.Println("Ошибка при выполнении запроса: ", res[1])
   
}
} else {
   
// Нормальный ответ.

Alex Lurye

unread,
Dec 1, 2017, 4:38:44 PM12/1/17
to gola...@googlegroups.com
Косяки в коде есть - типа игнорирования ошибок, возврата текста ошибки вместо самой ошибки, defer body.Close() лучше сделать сразу, как только успешный ответ получили - иначе его можно забыть (что вы и сделали в случае ошибки), ну и т.д.

select нужен, чтобы отличить, по какой причине ошибка произошла - из-за отмены запроса или из-за какой-то другой ошибки. Если бы вы ошибки нормально возвращали, то можно было бы просто проверить if err == context.Canceled {...} else {...}

forwo...@gmail.com

unread,
Dec 1, 2017, 4:43:01 PM12/1/17
to Golang Russian
Как можно вернуть результат запроса и ошибку? Учитывая строгую типизацию я не знаю как мне это сделать.

Alex Lurye

unread,
Dec 1, 2017, 4:49:04 PM12/1/17
to gola...@googlegroups.com
Либо в виде структуры с двумя полями - результат и ошибка (и у вас тогда будет канал структур), либо в виде двух каналов - один для результатов, другой - для ошибок. Сильно зависит от задачи.

On Fri, Dec 1, 2017 at 1:43 PM <forwo...@gmail.com> wrote:
Как можно вернуть результат запроса и ошибку? Учитывая строгую типизацию я не знаю как мне это сделать.

--

forwo...@gmail.com

unread,
Dec 1, 2017, 4:53:23 PM12/1/17
to Golang Russian
Так лучше?
package main

import (
   
"context"
   "net/http"
   "time"
   "fmt"
   "io/ioutil"
)

type DownloadResult struct {
   page
string
   err error
}

var ch = make(chan DownloadResult)


func main() {
   cx
, cancel := context.WithCancel(context.Background())
   req
, _ := http.NewRequest("GET", "http://google.com", nil)
   req
= req.WithContext(cx)


   
go func() {
      resp
, err := http.DefaultClient.Do(req)

     
if err != nil {
         ch
<- DownloadResult{
            page
: "",
            err
: err,

         
}
         
return
      }

      data
, err := ioutil.ReadAll(resp.Body)

      resp
.Body.Close()

     
if err != nil {
         
// Соединение прервалось на середине и данные не докачались.
         ch <- DownloadResult{
            page
: "",
            err
: err,
         
}
         
return
      }

      ch
<- DownloadResult{
         page
: string(data),
         err
: nil,

     
}
   
}()

   
// Simulating user cancel request
   go func() {
      time
.Sleep(100 * time.Millisecond)
      fmt
.Println(cancel)
     
//cancel()
   }()


   res
:= <-ch
   
if res.err != nil {
     
if res.err == context.Canceled {
         fmt
.Println("Отмена загрузки")
     
} else {
         fmt
.Println("Ошибка при загрузке: ", res.err)
     
}
   
} else {
      fmt
.Println("Загрузка завершена\n", res.page[:50])
   
}
}


Alex Lurye

unread,
Dec 2, 2017, 1:35:16 AM12/2/17
to gola...@googlegroups.com

Ну да, типа того. Работает?


forwo...@gmail.com

unread,
Dec 2, 2017, 2:23:25 AM12/2/17
to Golang Russian
Частично. Не выполняется условие почему-то

Alex Lurye

unread,
Dec 2, 2017, 8:43:04 PM12/2/17
to gola...@googlegroups.com

Может http заворачивает ошибку в свою ошибку. Нехорошо, конечно, с его стороны. Тогда сделайте проверку - если ошибка пришла, тогда проверяйте - если контекст отменен, то одно сообщение, если не отменен - другое. Через select-default


--
Reply all
Reply to author
Forward
0 new messages