Jak ustawić limit czasu dla żądań http.Get () w języku Golang?

106

Robię narzędzie do pobierania adresów URL w Go i mam listę adresów URL do pobrania. Wysyłam http.Get()zapytania na każdy adres URL i otrzymuję odpowiedź.

resp,fetch_err := http.Get(url)

Jak mogę ustawić niestandardowy limit czasu dla każdego żądania Get? (Domyślny czas jest bardzo długi, co powoduje, że mój moduł pobierania jest naprawdę wolny). Chcę, aby mój moduł pobierania miał limit czasu wynoszący około 40-45 sekund, po którym powinien zwrócić komunikat „przekroczono limit czasu żądania” i przejść do następnego adresu URL.

Jak mogę to osiągnąć?

pymd
źródło
1
Po prostu informuję, że ten sposób jest dla mnie wygodniejszy (limit czasu wybierania nie działa dobrze, jeśli są problemy z siecią, przynajmniej dla mnie): blog.golang.org/context
Audrius
@Audrius Masz jakiś pomysł, dlaczego limit czasu wybierania nie działa, gdy występują problemy z siecią? Myślę, że widzę to samo. Myślałem, że do tego służy DialTimeout?!?!
Jordan
@Jordan Trudno powiedzieć, ponieważ nie zagłębiałem się tak głęboko w kod biblioteki. Poniżej zamieściłem moje rozwiązanie. Używam go w produkcji już od jakiegoś czasu i na razie "po prostu działa" (tm).
Audrius

Odpowiedzi:

255

Najwyraźniej w Go 1.3 http Klient ma pole Timeout

client := http.Client{
    Timeout: 5 * time.Second,
}
client.Get(url)

To mi wystarczyło.

sparrovv
źródło
10
Cóż, to mi wystarczy. Cieszę się, że trochę przewinęłam :)
James Adam
5
Czy można ustawić inny limit czasu na żądanie?
Arnaud Rinquin
11
Co się stanie, gdy upłynie limit czasu? Czy Getzwraca błąd? Jestem trochę zdezorientowany, ponieważ Godoc for Clientmówi: Licznik czasu pozostaje uruchomiony po powrocie Get, Head, Post lub Do i przerwie czytanie Response.Body. Więc to znaczy, że albo Get czy odczyt Response.Bodymógł być przerwany błędem?
Avi Flax
1
Pytanie, jaka jest różnica między http.Client.Timeoutvs. http.Transport.ResponseHeaderTimeout?
Roy Lee
2
@Roylee Jedna z głównych różnic według dokumentacji: http.Client.Timeoutobejmuje czas potrzebny na przeczytanie treści odpowiedzi, http.Transport.ResponseHeaderTimeoutnie zawiera jej.
będę miał
53

Musisz skonfigurować własnego klienta z własnym transportem, który wykorzystuje niestandardową funkcję wybierania, która otacza DialTimeout .

Coś takiego (całkowicie niesprawdzone ) to :

var timeout = time.Duration(2 * time.Second)

func dialTimeout(network, addr string) (net.Conn, error) {
    return net.DialTimeout(network, addr, timeout)
}

func main() {
    transport := http.Transport{
        Dial: dialTimeout,
    }

    client := http.Client{
        Transport: &transport,
    }

    resp, err := client.Get("http://some.url")
}
Volker
źródło
Wielkie dzięki! To jest dokładnie to, czego szukałem.
pymd
jaka jest przewaga używania net.DialTimeout nad Transport.ResponseHeaderTimeout opisanym w odpowiedzi zzzz?
Daniele B
4
@Daniel B: Zadajesz złe pytanie. Nie chodzi o zalety, ale o to, co robi każdy kod. DialTimeouts wskakuje, jeśli nie można zadzwonić do serwera, podczas gdy inne limity czasu pojawiają się, jeśli niektóre operacje na ustanowionym połączeniu trwają zbyt długo. Jeśli Twoje serwery docelowe szybko nawiązują połączenie, ale następnie zaczną powoli blokować, limit czasu wybierania nie pomoże.
Volker
1
@Volker, dziękuję za odpowiedź. Właściwie zdałem sobie z tego sprawę: wygląda na to, że Transport.ResponseHeaderTimeout ustawia limit czasu odczytu, czyli limit czasu po nawiązaniu połączenia, podczas gdy twój jest limitem czasu wybierania. Rozwiązanie firmy dmichael zajmuje się zarówno limitem czasu wybierania, jak i limitem czasu odczytu.
Daniele B,
1
@Jonno: W Go nie ma obsady. To są konwersje typów.
Volker
31

Aby dodać do odpowiedzi Volkera, jeśli chcesz również ustawić limit czasu odczytu / zapisu oprócz limitu czasu połączenia, możesz zrobić coś takiego

package httpclient

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

func TimeoutDialer(cTimeout time.Duration, rwTimeout time.Duration) func(net, addr string) (c net.Conn, err error) {
    return func(netw, addr string) (net.Conn, error) {
        conn, err := net.DialTimeout(netw, addr, cTimeout)
        if err != nil {
            return nil, err
        }
        conn.SetDeadline(time.Now().Add(rwTimeout))
        return conn, nil
    }
}

func NewTimeoutClient(connectTimeout time.Duration, readWriteTimeout time.Duration) *http.Client {

    return &http.Client{
        Transport: &http.Transport{
            Dial: TimeoutDialer(connectTimeout, readWriteTimeout),
        },
    }
}

Ten kod jest testowany i działa w produkcji. Pełna treść testów dostępna jest tutaj https://gist.github.com/dmichael/5710968

Należy pamiętać, że dla każdego żądania konieczne będzie utworzenie nowego klienta ze względu na conn.SetDeadlineodwołanie do punktu w przyszłościtime.Now()

dmichael
źródło
Nie powinieneś sprawdzić wartości zwracanej przez conn.SetDeadline?
Eric Urban
3
Ten limit czasu nie działa z połączeniami utrzymującymi aktywność, co jest domyślne i wyobrażam sobie, z czego większość ludzi powinna korzystać. Oto, co wymyśliłem, aby sobie z tym poradzić: gist.github.com/seantalts/11266762
xitrium
Dzięki @xitrium i Eric za dodatkowy wkład.
dmichael
Wydaje mi się, że nie jest tak, jak powiedziałeś, że będziemy musieli tworzyć nowego klienta dla każdego żądania. Ponieważ Dial jest funkcją, która wydaje mi się, że jest wywoływana za każdym razem, gdy wysyłasz każde żądanie w tym samym kliencie.
A-letubby
Jesteś pewien, że za każdym razem potrzebujesz nowego klienta? Za każdym razem, gdy dzwoni, zamiast używać net.Dial, użyje funkcji, którą tworzy TimeoutDialer. To jest nowe połączenie, z ostatecznym terminem ocenianym za każdym razem, od nowego wywołania Now ().
Blake Caldwell
16

Jeśli chcesz to zrobić na żądanie, obsługa błędów została zignorowana ze względu na zwięzłość:

ctx, cncl := context.WithTimeout(context.Background(), time.Second*3)
defer cncl()

req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "https://google.com", nil)

resp, _ := http.DefaultClient.Do(req)
Chad Grant
źródło
1
Dodatkowe informacje: na dokument termin narzucony przez Kontekst obejmuje również przeczytanie treści, podobnie jak w przypadku http.Client.Timeout.
kubańczyk
1
Powinna być akceptowana dla Go 1.7+. For Go 1.13+ można nieco skrócić za pomocą NewRequestWithContext
kubanczyk
9

Szybki i brudny sposób:

http.DefaultTransport.(*http.Transport).ResponseHeaderTimeout = time.Second * 45

To jest mutacja stanu globalnego bez jakiejkolwiek koordynacji. Jednak może to być w porządku dla twojego modułu pobierania adresów URL. W przeciwnym razie utwórz prywatną instancję http.RoundTripper:

var myTransport http.RoundTripper = &http.Transport{
        Proxy:                 http.ProxyFromEnvironment,
        ResponseHeaderTimeout: time.Second * 45,
}

var myClient = &http.Client{Transport: myTransport}

resp, err := myClient.Get(url)
...

Nic powyżej nie zostało przetestowane.

zzzz
źródło
Proszę, niech ktoś mnie poprawi, ale wygląda na to, że ResponseHeaderTimeout dotyczy limitu czasu odczytu, czyli limitu czasu po ustanowieniu połączenia. Najbardziej kompleksowym rozwiązaniem wydaje się być @dmichael, ponieważ pozwala ustawić zarówno limit czasu wybierania, jak i limit czasu odczytu.
Daniele B
http.DefaultTransport.(*http.Transport).ResponseHeaderTimeout = time.Second * 45pomóż mi dużo w teście pisania dla przekroczenia limitu czasu żądania. Dziękuję Ci bardzo.
lee
0

Możesz użyć https://github.com/franela/goreq, który obsługuje limity czasu w modny i prosty sposób.

marcosnils
źródło
Ta biblioteka wygląda niesamowicie!
ChrisR
-1
timeout := time.Duration(5 * time.Second)
transport := &http.Transport{Proxy: http.ProxyURL(proxyUrl), ResponseHeaderTimeout:timeout}

Może to pomóc, ale zauważ, że ResponseHeaderTimeoutzaczyna się dopiero po ustanowieniu połączenia.

T.Max
źródło