Confusion regarding channel directions and blocking in Go

Issue

In a function definition, if a channel is an argument without a direction, does it have to send or receive something?

func makeRequest(url string, ch chan<- string, results chan<- string) {
    start := time.Now()

    resp, err := http.Get(url)
    defer resp.Body.Close()
    if err != nil {
        fmt.Printf("%v", err)
    }

    resp, err = http.Post(url, "text/plain", bytes.NewBuffer([]byte("Hey")))
    defer resp.Body.Close()

    secs := time.Since(start).Seconds()

    if err != nil {
        fmt.Printf("%v", err)
    }
    // Cannot move past this.
    ch <- fmt.Sprintf("%f", secs) 
    results <- <- ch
}

func MakeRequestHelper(url string, ch chan string, results chan string, iterations int) {
    for i := 0; i < iterations; i++ {
        makeRequest(url, ch, results)
    }
    for i := 0; i < iterations; i++ {
        fmt.Println(<-ch)
    }
}

func main() {
    args := os.Args[1:]
    threadString := args[0]
    iterationString := args[1]
    url := args[2]
    threads, err := strconv.Atoi(threadString)
    if err != nil {
        fmt.Printf("%v", err)
    }
    iterations, err := strconv.Atoi(iterationString)
    if err != nil {
        fmt.Printf("%v", err)
    }

    channels := make([]chan string, 100)
    for i := range channels {
        channels[i] = make(chan string)
    }

    // results aggregate all the things received by channels in all goroutines
    results := make(chan string, iterations*threads)

    for i := 0; i < threads; i++ {
        go MakeRequestHelper(url, channels[i], results, iterations)

    }

    resultSlice := make([]string, threads*iterations)
    for i := 0; i < threads*iterations; i++ {
        resultSlice[i] = <-results
    }
}

In the above code,

ch <- or <-results

seems to be blocking every goroutine that executes makeRequest.

I am new to concurrency model of Go. I understand that sending to and receiving from a channel blocks but find it difficult what is blocking what in this code.

Solution

I’m not really sure that you are doing… It seems really convoluted. I suggest you read up on how to use channels.

https://tour.golang.org/concurrency/2

That being said you have so much going on in your code that it was much easier to just gut it to something a bit simpler. (It can be simplified further). I left comments to understand the code.

package main

import (
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "sync"
    "time"
)

// using structs is a nice way to organize your code
type Worker struct {
    wg        sync.WaitGroup
    semaphore chan struct{}
    result    chan Result
    client    http.Client
}

// group returns so that you don't have to send to many channels
type Result struct {
    duration float64
    results  string
}

// closing your channels will stop the for loop in main
func (w *Worker) Close() {
    close(w.semaphore)
    close(w.result)
}

func (w *Worker) MakeRequest(url string) {
    // a semaphore is a simple way to rate limit the amount of goroutines running at any single point of time
    // google them, Go uses them often
    w.semaphore <- struct{}{}
    defer func() {
        w.wg.Done()
        <-w.semaphore
    }()

    start := time.Now()

    resp, err := w.client.Get(url)
    if err != nil {
        log.Println("error", err)
        return
    }
    defer resp.Body.Close()

    // don't have any examples where I need to also POST anything but the point should be made
    // resp, err = http.Post(url, "text/plain", bytes.NewBuffer([]byte("Hey")))
    // if err != nil {
    // log.Println("error", err)
    // return
    // }
    // defer resp.Body.Close()

    secs := time.Since(start).Seconds()

    b, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        log.Println("error", err)
        return
    }
    w.result <- Result{duration: secs, results: string(b)}
}

func main() {
    urls := []string{"https://facebook.com/", "https://twitter.com/", "https://google.com/", "https://youtube.com/", "https://linkedin.com/", "https://wordpress.org/",
        "https://instagram.com/", "https://pinterest.com/", "https://wikipedia.org/", "https://wordpress.com/", "https://blogspot.com/", "https://apple.com/",
    }

    workerNumber := 5
    worker := Worker{
        semaphore: make(chan struct{}, workerNumber),
        result:    make(chan Result),
        client:    http.Client{Timeout: 5 * time.Second},
    }

    // use sync groups to allow your code to wait for
    // all your goroutines to finish
    for _, url := range urls {
        worker.wg.Add(1)
        go worker.MakeRequest(url)
    }

    // by declaring wait and close as a seperate goroutine
    // I can get to the for loop below and iterate on the results
    // in a non blocking fashion
    go func() {
        worker.wg.Wait()
        worker.Close()
    }()

    // do something with the results channel
    for res := range worker.result {
        fmt.Printf("Request took %2.f seconds.\nResults: %s\n\n", res.duration, res.results)
    }
}

Answered By – reticentroot

Answer Checked By – Mildred Charles (GoLangFix Admin)

Leave a Reply

Your email address will not be published.