web-dev-qa-db-fra.com

Quel est le moyen le plus idiomatique de créer un itérateur dans Go?

Une option consiste à utiliser des canaux. Les canaux sont en quelque sorte des itérateurs et vous pouvez les parcourir en utilisant le mot-clé range. Mais lorsque vous découvrez que vous ne pouvez pas sortir de cette boucle sans fuir du goroutine, l'utilisation devient limitée.

Quelle est la manière idiomatique de créer un motif d'itérateur au go?

Modifier :

Le problème fondamental des canaux est qu’ils sont un modèle Push. Iterator est un modèle tiré. Vous n'êtes pas obligé de dire à l'itérateur de s'arrêter. Je cherche un moyen de parcourir les collections de manière expressive et sympathique. J'aimerais également enchaîner les itérateurs (map, filter, fold alternatives).

38
Kugel

Les canaux sont utiles, mais les fermetures sont souvent plus appropriées.

package main

import "fmt"

func main() {
    gen := newEven()
    fmt.Println(gen())
    fmt.Println(gen())
    fmt.Println(gen())
    gen = nil // release for garbage collection
}

func newEven() func() int {
    n := 0
    // closure captures variable n
    return func() int {
        n += 2
        return n
    }
}

Aire de jeux: http://play.golang.org/p/W7pG_HUOzw

Vous n'aimez pas les fermetures non plus? Utilisez un type nommé avec une méthode:

package main

import "fmt"

func main() {
    gen := even(0)
    fmt.Println(gen.next())
    fmt.Println(gen.next())
    fmt.Println(gen.next())
}

type even int

func (e *even) next() int {
    *e += 2
    return int(*e)
}

Aire de jeux: http://play.golang.org/p/o0lerLcAh3

Il existe des compromis entre les trois techniques, vous ne pouvez donc pas en désigner une comme idiomatique. Utilisez ce qui répond le mieux à vos besoins.

Le chaînage est facile car les fonctions sont des objets de première classe. Voici une extension de l'exemple de fermeture. J'ai ajouté un type intGen pour le générateur d'entiers qui indique clairement où les fonctions du générateur sont utilisées comme arguments et valeurs de retour. mapInt est défini d'une manière générale pour mapper toute fonction entière à un générateur d'entiers. D'autres fonctions telles que filtrer et replier pourraient être définies de la même manière.

package main

import "fmt"

func main() {
    gen := mapInt(newEven(), square)
    fmt.Println(gen())
    fmt.Println(gen())
    fmt.Println(gen())
    gen = nil // release for garbage collection
}

type intGen func() int

func newEven() intGen {
    n := 0
    return func() int {
        n += 2
        return n
    }
}

func mapInt(g intGen, f func(int) int) intGen {
    return func() int {
        return f(g())
    }
}

func square(i int) int {
    return i * i
}

Aire de jeux: http://play.golang.org/p/L1OFm6JuX0

45
Sonia

TL; DR: Oubliez les fermetures et les canaux, trop lentement. Si les éléments individuels de votre collection sont accessibles par index, optez pour l'itération C classique sur un type de type tableau. Sinon, implémentez un itérateur avec état.

J'avais besoin d'itérer sur un type de collection pour lequel l'implémentation de stockage exacte n'est pas encore gravée dans le marbre. Ceci, ajouté aux zillions d’autres raisons pour extraire les détails de l’implémentation du client, m’a amené à faire des tests avec diverses méthodes d’itération. Code complet ici , y compris certaines implémentations qui utilisent les erreurs comme valeurs . Voici les résultats de référence:

  • itération C classique sur une structure semblable à un tableau. Le type fournit les méthodes ValueAt () et Len ():

    l := Len(collection)
    for i := 0; i < l; i++ { value := collection.ValueAt(i) }
    // benchmark result: 2492641 ns/op
    
  • Itérateur de style de fermeture. La méthode Iterator de la collection renvoie une fonction next () (une fermeture sur la collection et le curseur) et un booléen hasNext. next () renvoie la valeur suivante et un booléen hasNext. Notez que cela est beaucoup plus rapide que d'utiliser des fermetures séparées next () et hasNext () renvoyant des valeurs uniques:

    for next, hasNext := collection.Iterator(); hasNext; {
        value, hasNext = next()
    }
    // benchmark result: 7966233 ns/op !!!
    
  • Itérateur stateful. Une structure simple avec deux champs de données, la collection et un curseur, et deux méthodes: Next () et HasNext (). Cette fois, la méthode Iterator () de la collection renvoie un pointeur sur une structure d'itérateur correctement initialisée:

    for iter := collection.Iterator(); iter.HasNext(); {
        value := iter.Next()
    }
    // benchmark result: 4010607 ns/op
    

Autant que j'aime les fermetures, la performance est un non-Go. En ce qui concerne les modèles de conception, bien sûr, les Gophers préfèrent le terme "méthode idiomatique". De plus, grep est l’arborescence des sources pour les itérateurs: avec si peu de fichiers qui mentionnent leur nom, les itérateurs ne sont certainement pas une chose à faire.

Consultez également cette page: http://ewencp.org/blog/golang-iterators/

Quoi qu'il en soit, les interfaces ne vous aident d'aucune façon ici, sauf si vous souhaitez définir une interface Iterable, mais il s'agit d'un sujet complètement différent.

14
wldsvc

TL; DR: les itérateurs ne sont pas idiomatiques dans Go. Laissez-les dans d'autres langues.

En profondeur, commence par l'entrée de Wikipedia "Modèle d'itérateur", "Dans la programmation orientée objet, le motif d'itérateur est un motif de conception ..." Deux signaux d'alarme: Premièrement, les concepts de programmation orientés objet ne se traduisent souvent pas bien en Go. et deuxièmement, de nombreux programmeurs de Go ne pensent pas beaucoup aux modèles de conception. Ce premier paragraphe inclut également "Le modèle d'itérateur sépare les algorithmes des conteneurs", mais seulement après avoir déclaré "un itérateur [accède] aux éléments du conteneur. Eh bien c'est quoi? Si un algorithme accède aux éléments du conteneur, il peut difficilement prétendre être découplé. La réponse dans de nombreux langages implique une sorte de générique qui permet au langage de se généraliser sur des structures de données similaires. Les réponses en Go sont les interfaces. On entend par comportement les capacités exprimées par des méthodes sur les données.

Pour un type d'itérateur minimal, la capacité nécessaire est une méthode Next. Une interface Go peut représenter un objet itérateur en spécifiant simplement cette signature de méthode unique. Si vous voulez qu'un type de conteneur soit itérable, il doit satisfaire l'interface itérateur en implémentant toutes les méthodes de l'interface. (Nous n'en avons qu'une ici, et en fait, il est courant que les interfaces n'aient qu'une seule méthode.)

Un exemple de travail minimal:

package main

import "fmt"

// IntIterator is an iterator object.
// yes, it's just an interface.
type intIterator interface {
    Next() (value int, ok bool)
}

// IterableSlice is a container data structure
// that supports iteration.
// That is, it satisfies intIterator.
type iterableSlice struct {
    x int
    s []int
}

// iterableSlice.Next implements intIterator.Next,
// satisfying the interface.
func (s *iterableSlice) Next() (value int, ok bool) {
    s.x++
    if s.x >= len(s.s) {
        return 0, false
    }
    return s.s[s.x], true
}

// newSlice is a constructor that constructs an iterable
// container object from the native Go slice type.
func newSlice(s []int) *iterableSlice {
    return &iterableSlice{-1, s}
}

func main() {
    // Ds is just intIterator type.
    // It has no access to any data structure.
    var ds intIterator

    // Construct.  Assign the concrete result from newSlice
    // to the interface ds.  ds has a non-nil value now,
    // but still has no access to the structure of the
    // concrete type.
    ds = newSlice([]int{3, 1, 4})

    // iterate
    for {
        // Use behavior only.  Next returns values
        // but without insight as to how the values
        // might have been represented or might have
        // been computed.
        v, ok := ds.Next()
        if !ok {
            break
        }
        fmt.Println(v)
    }
}

Aire de jeux: http://play.golang.org/p/AFZzA7PRDR

C’est l’idée de base des interfaces, mais c’est une exagération absurde pour une itération sur une tranche. Dans de nombreux cas, lorsque vous recherchez un itérateur dans d'autres langues, vous écrivez du code Go à l'aide de primitives de langage intégrées qui itèrent directement sur des types de base. Votre code reste clair et concis. Lorsque cela devient compliqué, réfléchissez aux fonctionnalités dont vous avez réellement besoin. Avez-vous besoin d’émettre des résultats d’endroits aléatoires dans une fonction quelconque? Les canaux fournissent une capacité de rendement qui le permet. Avez-vous besoin de listes infinies ou d'évaluation paresseuse? Les fermetures fonctionnent très bien. Avez-vous différents types de données et en avez-vous besoin pour prendre en charge de manière transparente les mêmes opérations? Les interfaces livrent. Avec les canaux, les fonctions et les interfaces, tous des objets de première classe, ces techniques sont facilement composables. Alors, quelle est alors la manière la plus idiomatique? Il est nécessaire d’expérimenter différentes techniques, de s’y habituer et d’utiliser ce qui répond à vos besoins de la manière la plus simple possible. Les itérateurs, dans le sens orienté objet de toute façon, ne sont presque jamais les plus simples.

13
Sonia

Vous pouvez sortir sans laisser de fuites en donnant à vos goroutines un deuxième canal pour les messages de contrôle. Dans le cas le plus simple, il ne s'agit que d'un chan bool. Lorsque vous voulez que le goroutine s’arrête, vous envoyez sur ce canal. À l'intérieur de la goroutine, vous placez l'envoi de canal de l'itérateur et l'écoute sur le canal de contrôle dans une sélection.

Voici un exemple.

Vous pouvez aller plus loin en autorisant différents messages de contrôle, tels que "ignorer".

Votre question est assez abstraite, pour en dire plus, un exemple concret serait utile.

4
Thomas Kappler

Voici une façon dont j'ai pensé le faire avec des canaux et des goroutines:

package main

import (
    "fmt"
)

func main() {
    c := nameIterator(3)
    for batch := range c {
        fmt.Println(batch)
    }
}

func nameIterator(batchSize int) <-chan []string {
    names := []string{"Cherry", "Cami", "Tildy", "Cory", "Ronnie", "Aleksandr", "Billie", "Reine", "Gilbertina", "Dotti"}

    c := make(chan []string)

    go func() {
        for i := 0; i < len(names); i++ {
            startIdx := i * batchSize
            endIdx := startIdx + batchSize

            if startIdx > len(names) {
                continue
            }
            if endIdx > len(names) {
                c <- names[startIdx:]
            } else {
                c <- names[startIdx:endIdx]
            }
        }

        close(c)
    }()

    return c
}

https://play.golang.org/p/M6NPT-hYPNd

J'ai eu l'idée du discours de Rob Pike Go Concurrency Patterns .

1
user9772923

En regardant le conteneur/liste, il semble qu’il n’ya aucun moyen de le faire. C-like devrait être utilisé si vous parcourez un objet.

Quelque chose comme ça.

type Foo struct {
...
}

func (f *Foo) Next() int {
...
}

foo := Foo(10)

for f := foo.Next(); f >= 0; f = foo.Next() {
...
}
1