Как вернуть первый http ответ на ответ - PullRequest
0 голосов
/ 17 октября 2019

Я использую несколько подпрограмм для выполнения задачи, и когда одна из них будет выполнена, вернитесь и закройте канал, что вызовет панику: отправка по закрытому каналу .

См. код:

func fetch(urls []string) *http.Response {
    ch := make(chan *http.Response)
    defer close(ch)
    for _, url := range urls {
        go func() {
            resp, err := http.Get(url)
            if err == nil {
                ch <- resp
            }
        }()
    }
    return <-ch
}

Если не закрыть канал, проблем нет, но я не думаю, что это хорошо, так есть ли какое-нибудь элегантное решение?

Спасибо за всеответы, вот мой окончательный код:

func fetch(urls []string) *http.Response {
    var wg sync.WaitGroup
    ch := make(chan *http.Response)
    wg.Add(len(urls))
    for _, url := range urls {
        go func(url string) {
            defer wg.Done()
            resp, err := http.Get(url)
            if err == nil {
                ch <- resp
            }
        }(url)
    }
    go func() {
        wg.Wait()
        close(ch)
    }()
    return <-ch
}

Ответы [ 5 ]

1 голос
/ 17 октября 2019

@ Клемент обнаружил ошибку в ответе Флимзи. Он слаб к write on closed channel ошибкам, но он не смог выдать простую правильную версию. Версия Flimzy также слаба для ранней отмены контекста.

вот исправление

package main

import (
    "fmt"
)

func fetch(urls []string) *http.Response {
    var wg sync.WaitGroup
    ch := make(chan *http.Response, len(urls))
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    for _, url := range urls {
        wg.Add(1)
        go func(ctx context.Context, url string) {
            defer wg.Done()
            req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
            resp, err := http.Do(req)
            if err == nil {
                ch <- resp
            }
        }(ctx, url)
    }
    go func() {
        wg.Wait()
        close(ch)
    }()

    return <-ch
}
1 голос
/ 17 октября 2019

Если ваша цель - прочитать только один результат, а затем отменить другие запросы, попробуйте что-то вроде этого:

func fetch(urls []string) *http.Response {
    ch := make(chan *http.Response)
    defer close(ch)
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    for _, url := range urls {
        go func(ctx context.Context, url string) {
            req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
            resp, err := http.Do(req)
            if err == nil {
                select {
                case ch <- resp:
                case <- ctx.Done():
                }
            }
        }(ctx, url)
    }
    return <-ch
}

При этом используется контекст с возможностью отмены, поэтому после возврата первого результата оставшийся httpзапросы сигнализируются об отмене.


ПРИМЕЧАНИЕ: Ваш код содержит ошибку, которую я исправил выше:

func _, url := range urls {
    go func() {
        http.Do(url) // `url` is changed here on each iteration through the for loop, meaning you will not be calling the url you expect
    }()
}

Исправьте это, передав url для функции goroutine вместо использования замыкания:

func _, url := range urls {
    go func(url string) {
        http.Do(url) // `url` is now safe
    }(url)
}

Связанный пост: Почему golang не выполняет итерацию правильно в цикле for с диапазоном?

0 голосов
/ 17 октября 2019

Вы можете добавить две процедуры:

  1. Получить все запросы, отправив первый, который будет возвращен, и отбросив следующие. Когда WaitGroup завершает работу, он закрывает ваш первый канал.
  2. Один для ожидания WaitGroup и отправки сигнала о закрытии первого канала.
func fetch(urls []string) *http.Response {
    var wg sync.WaitGroup
    ch := make(chan *http.Response)
    for _, url := range urls {
        wg.Add(1)
        go func(url string) {
            resp, err := http.Get(url)          
            if err == nil {
                ch <- resp:
            }
            wg.Done()
        }(url)
    }
    done := make(chan interface{})
    go func(){
        wg.Wait()
        done <- interface{}{}
        close(done)
    }

    out := make(chan *http.Response)
    defer close(out)
    go func(){
        first = true
        for {
            select {
            case r <- ch:
                if first {
                    first = false
                    out <- r
                }
            case <-done:
                close(ch)
                return
            }
        }
    }()

    return <-out
}

Это должно быть безопасно. .. возможно.

0 голосов
/ 17 октября 2019

Вы закрываете канал слишком рано, поэтому вы можете увидеть эту ошибку,
лучше закрывать канал только тогда, когда вы больше ничего не будете записывать в канал, и для этих целей вы можете использовать sync.WaitGroup, например так:

package main

import (
    "fmt"
    "net/http"
    "sync"
)

func main() {
    ch := fetch([]string{"http://github.com/cn007b", "http://github.com/thepkg"})
    fmt.Println("\n", <-ch)
    fmt.Println("\n", <-ch)
}

func fetch(urls []string) chan *http.Response {
    ch := make(chan *http.Response, len(urls))
    wg := sync.WaitGroup{}
    wg.Add(len(urls))
    for _, url := range urls {
        go func() {
            defer wg.Done()
            resp, err := http.Get(url)
            if err == nil {
                ch <- resp
            }
        }()
    }
    go func() {
        wg.Wait()
        close(ch)
    }()
    return ch
}

Кроме того, с целью предоставления среза с ответами в результате вы можете сделать что-то вроде этого:

func fetch2(urls []string) (result []*http.Response) {
    ch := make(chan *http.Response, len(urls))
    wg := sync.WaitGroup{}
    wg.Add(len(urls))
    for _, url := range urls {
        go func() {
            defer wg.Done()
            resp, err := http.Get(url)
            if err == nil {
                ch <- resp
            }
        }()
    }
    wg.Wait()
    close(ch)
    for v := range ch {
        result = append(result, v)
    }
    return result
}
0 голосов
/ 17 октября 2019

Ваш код возвращается после получения первого ответа. Затем канал закрывается, и другие подпрограммы go отправляются по закрытому каналу.

Вместо того, чтобы возвращать первый ответ, может оказаться более целесообразным вернуть массив ответов, упорядоченных по той же длине, что иurls.

Поскольку запрос http может привести к ошибке, было бы целесообразно также вернуть массив ошибок.

package main

import (
    "fmt"
    "net/http"
)

func main() {
    fmt.Println(fetch([]string{
        "https://google.com",
        "https://stackoverflow.com",
        "https://passkit.com",
    }))
}

type response struct {
    key      int
    response *http.Response
    err      error
}

func fetch(urls []string) ([]*http.Response, []error) {
    ch := make(chan response)
    defer close(ch)
    for k, url := range urls {
        go func(k int, url string) {
            r, err := http.Get(url)
            resp := response {
                key:      k,
                response: r,
                err:      err,
            }
            ch <- resp
        }(k, url)
    }

    resp := make([]*http.Response, len(urls))
    respErrors := make([]error, len(urls))

    for range urls {
        r := <-ch
        resp[r.key] = r.response
        respErrors[r.key] = r.err
    }
    return resp[:], respErrors[:]
}

детская площадка

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...