2014-11-24 7 views
6

Ich möchte den idiomatischen Weg, um dies zu lösen wissen (die derzeit einen Deadlock-Fehler wirft), verzweigt die Rekursion eine unbekannte Anzahl von Zeiten, so kann ich nicht einfach den Kanal schließen.Rekursive Goroutines, was ist der beste Weg zu sagen, um das Lesen aus dem Kanal zu stoppen?

http://play.golang.org/p/avLf_sQJj_

Ich habe es funktioniert, hergestellt durch einen Zeiger auf eine Nummer übergeben, und Erhöhen sie, und ich habe sah in mit Sync waitgroups. Ich hatte nicht das Gefühl (und ich könnte falsch liegen), dass ich eine elegante Lösung gefunden hatte. Die Go-Beispiele, die ich gesehen habe, sind in der Regel einfach, clever und prägnant.

Dies ist die letzte Übung von einer Tour of Go, https://tour.golang.org/#73

Wissen Sie ‚wie ein Go-Programmierer‘ würde dies verwalten? Jede Hilfe wäre willkommen. Ich versuche von Anfang an gut zu lernen.

Antwort

2

Anstatt sync.WaitGroup einzubeziehen, können Sie das Ergebnis, das über eine geparste URL gesendet wird, erweitern und die Anzahl neuer URLs einschließen. In Ihrer Hauptschleife würden Sie dann die Ergebnisse lesen, solange es etwas zu sammeln gibt.

In Ihrem Fall würde die Anzahl der gefundenen URLs Anzahl der ausgeführten Routinen sein, aber das muss nicht unbedingt sein. Ich würde persönlich mehr oder weniger feste Anzahl von Holroutinen erzeugen, also öffnest du nicht zu viele HTTP-Anfragen (oder zumindest hast du die Kontrolle darüber). Ihre Hauptschleife würde sich dann nicht ändern, da es nicht wichtig ist, wie das Holen ausgeführt wird. Die wichtige Tatsache hier ist, dass Sie entweder ein Ergebnis oder einen Fehler für jede URL senden müssen - ich habe den Code hier geändert, so dass es keine neuen Routinen erzeugt, wenn die Tiefe bereits 1 ist.

Ein Nebeneffekt von Diese Lösung besteht darin, dass Sie den Fortschritt in Ihrer Hauptschleife einfach ausdrucken können. Hier

ist das Beispiel, das auf Spielplatz:

http://play.golang.org/p/BRlUc6bojf

package main 

import (
    "fmt" 
) 

type Fetcher interface { 
    // Fetch returns the body of URL and 
    // a slice of URLs found on that page. 
    Fetch(url string) (body string, urls []string, err error) 
} 

type Res struct { 
    url string 
    body string 
    found int // Number of new urls found 
} 

// Crawl uses fetcher to recursively crawl 
// pages starting with url, to a maximum of depth. 
func Crawl(url string, depth int, fetcher Fetcher, ch chan Res, errs chan error, visited map[string]bool) { 
    body, urls, err := fetcher.Fetch(url) 
    visited[url] = true 
    if err != nil { 
     errs <- err 
     return 
    } 

    newUrls := 0  
    if depth > 1 { 
     for _, u := range urls { 
      if !visited[u] { 
       newUrls++ 
       go Crawl(u, depth-1, fetcher, ch, errs, visited) 
      } 
     } 
    } 

    // Send the result along with number of urls to be fetched 
    ch <- Res{url, body, newUrls} 

    return 
} 

func main() { 
    ch := make(chan Res) 
    errs := make(chan error) 
    visited := map[string]bool{} 
    go Crawl("http://golang.org/", 4, fetcher, ch, errs, visited) 
    tocollect := 1 
    for n := 0; n < tocollect; n++ { 
     select { 
     case s := <-ch: 
      fmt.Printf("found: %s %q\n", s.url, s.body) 
      tocollect += s.found 
     case e := <-errs: 
      fmt.Println(e) 
     } 
    } 

} 

// fakeFetcher is Fetcher that returns canned results. 
type fakeFetcher map[string]*fakeResult 

type fakeResult struct { 
    body string 
    urls []string 
} 

func (f fakeFetcher) Fetch(url string) (string, []string, error) { 
    if res, ok := f[url]; ok { 
     return res.body, res.urls, nil 
    } 
    return "", nil, fmt.Errorf("not found: %s", url) 
} 

// fetcher is a populated fakeFetcher. 
var fetcher = fakeFetcher{ 
    "http://golang.org/": &fakeResult{ 
     "The Go Programming Language", 
     []string{ 
      "http://golang.org/pkg/", 
      "http://golang.org/cmd/", 
     }, 
    }, 
    "http://golang.org/pkg/": &fakeResult{ 
     "Packages", 
     []string{ 
      "http://golang.org/", 
      "http://golang.org/cmd/", 
      "http://golang.org/pkg/fmt/", 
      "http://golang.org/pkg/os/", 
     }, 
    }, 
    "http://golang.org/pkg/fmt/": &fakeResult{ 
     "Package fmt", 
     []string{ 
      "http://golang.org/", 
      "http://golang.org/pkg/", 
     }, 
    }, 
    "http://golang.org/pkg/os/": &fakeResult{ 
     "Package os", 
     []string{ 
      "http://golang.org/", 
      "http://golang.org/pkg/", 
     }, 
    }, 
} 

Und ja, @jimt Rat folgen und sicheren Zugriff auf die Karte Thread machen.

+0

Ich habe dies als die Antwort markiert, weil, während ich denke, die Lösung @jimt zur Verfügung gestellt ist elegant, und lehrt eine Menge nützlicher Dinge, das ist getan, ohne auf Werkzeuge über den Rahmen der vorherigen Tutorials verlassen. Ich habe jedoch nach der idiomatischen "richtigen Antwort" gefragt, und da ich nicht erfahren genug bin, weiß ich nicht, welches das ist, was ich dafür markieren würde. (versucht, beide Antworten zu akzeptieren) – SamMorrowDrums

2

Hier ist meine Interpretation der Übung. Es gibt viele davon, aber das ist meins. Ich verwende sync.WaitGroup und eine benutzerdefinierte, Mutex-geschützte Karte, um besuchte URLs zu speichern. Meistens weil Go's Standard map nicht Thread-sicher ist. Ich kombiniere auch die Daten- und Fehlerkanäle in einer einzigen Struktur, die eine Methode zum Lesen dieser Kanäle hat. Meistens für die Trennung von Sorgen und (wohl) für etwas sauberere Dinge.

Beispiel on playground:

package main 

import (
    "fmt" 
    "sync" 
) 

type Fetcher interface { 
    // Fetch returns the body of URL and 
    // a slice of URLs found on that page. 
    Fetch(url string) (body string, urls []string, err error) 
} 

// Crawl uses fetcher to recursively crawl 
// pages starting with url, to a maximum of depth. 
func Crawl(wg *sync.WaitGroup, url string, depth int, fetcher Fetcher, cache *UrlCache, results *Results) { 
    defer wg.Done() 

    if depth <= 0 || !cache.AtomicSet(url) { 
     return 
    } 

    body, urls, err := fetcher.Fetch(url) 
    if err != nil { 
     results.Error <- err 
     return 
    } 

    results.Data <- [2]string{url, body} 

    for _, url := range urls { 
     wg.Add(1) 
     go Crawl(wg, url, depth-1, fetcher, cache, results) 
    } 
} 

func main() { 
    var wg sync.WaitGroup 
    cache := NewUrlCache() 

    results := NewResults() 
    defer results.Close() 

    wg.Add(1) 
    go Crawl(&wg, "http://golang.org/", 4, fetcher, cache, results) 
    go results.Read() 
    wg.Wait() 
} 

// Results defines channels which yield results for a single crawled URL. 
type Results struct { 
    Data chan [2]string // url + body. 
    Error chan error  // Possible fetcher error. 
} 

func NewResults() *Results { 
    return &Results{ 
     Data: make(chan [2]string, 1), 
     Error: make(chan error, 1), 
    } 
} 

func (r *Results) Close() error { 
    close(r.Data) 
    close(r.Error) 
    return nil 
} 

// Read reads crawled results or errors, for as long as the channels are open. 
func (r *Results) Read() { 
    for { 
     select { 
     case data := <-r.Data: 
      fmt.Println(">", data) 

     case err := <-r.Error: 
      fmt.Println("e", err) 
     } 
    } 
} 

// UrlCache defines a cache of URL's we've already visited. 
type UrlCache struct { 
    sync.Mutex 
    data map[string]struct{} // Empty struct occupies 0 bytes, whereas bool takes 1 bytes. 
} 

func NewUrlCache() *UrlCache { return &UrlCache{data: make(map[string]struct{})} } 

// AtomicSet sets the given url in the cache and returns false if it already existed. 
// 
// All within the same locked context. Modifying a map without synchronisation is not safe 
// when done from multiple goroutines. Doing a Exists() check and Set() separately will 
// create a race condition, so we must combine both in a single operation. 
func (c *UrlCache) AtomicSet(url string) bool { 
    c.Lock() 
    _, ok := c.data[url] 
    c.data[url] = struct{}{} 
    c.Unlock() 
    return !ok 
} 

// fakeFetcher is Fetcher that returns canned results. 
type fakeFetcher map[string]*fakeResult 

type fakeResult struct { 
    body string 
    urls []string 
} 

func (f fakeFetcher) Fetch(url string) (string, []string, error) { 
    if res, ok := f[url]; ok { 
     return res.body, res.urls, nil 
    } 
    return "", nil, fmt.Errorf("not found: %s", url) 
} 

// fetcher is a populated fakeFetcher. 
var fetcher = fakeFetcher{ 
    "http://golang.org/": &fakeResult{ 
     "The Go Programming Language", 
     []string{ 
      "http://golang.org/pkg/", 
      "http://golang.org/cmd/", 
     }, 
    }, 
    "http://golang.org/pkg/": &fakeResult{ 
     "Packages", 
     []string{ 
      "http://golang.org/", 
      "http://golang.org/cmd/", 
      "http://golang.org/pkg/fmt/", 
      "http://golang.org/pkg/os/", 
     }, 
    }, 
    "http://golang.org/pkg/fmt/": &fakeResult{ 
     "Package fmt", 
     []string{ 
      "http://golang.org/", 
      "http://golang.org/pkg/", 
     }, 
    }, 
    "http://golang.org/pkg/os/": &fakeResult{ 
     "Package os", 
     []string{ 
      "http://golang.org/", 
      "http://golang.org/pkg/", 
     }, 
    }, 
} 

Dies wurde ausgiebig nicht getestet, so vielleicht gibt Optimierungen und Fehlerbehebungen sind, die angewendet werden können, aber es sollte Ihnen einige Ideen zumindest geben.

+0

Vielen Dank @jimt, das ist ein interessantes Beispiel. Die grundlegende Antwort in der obigen Frage ist also sync.WaitGroup. Ich hätte gedacht, dass es eine Lösung gegeben hätte, die nur die Dinge mit einbezieht, die in den vorherigen Folien gelehrt wurden, aber wenn niemand Weisheit hat, denke ich, dass WaitGroups der richtige Weg ist. Noch ein paar nette Tipps, die ich eigentlich vergessen hatte, zu verschieben, was wirklich nützlich ist. Ich helfe davon, dies als die Antwort zu markieren, nur um anderen die Chance zu geben, aber es ist sicherlich eine großartige Antwort! – SamMorrowDrums

+0

Würden Sie Ihre Antwort eher "idiomatisch" in einer Produktions-Go-App sagen, dann Tomasz 'Antwort? Ich mag es nicht, mehrere Antworten zu markieren, sondern zu kreditieren, wo es fällig ist. – SamMorrowDrums