Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,11 +355,32 @@ for {
}
```

#### Iterators (**experimental**) ####
#### Iterators ####

Go v1.23 introduces the new `iter` package.

With the `enrichman/gh-iter` package, it is possible to create iterators for `go-github`. The iterator will handle pagination for you, looping through all the available results.
The new `github/gen-iterators.go` file auto-generates "*Iter" methods in `github/github-iterators.go`
for all methods that support page number iteration (using the `NextPage` field in each response).
To handle rate limiting issues, make sure to use a rate-limiting transport.
(See [Rate Limiting](/#rate-limiting) above for more details.)
To use these methods, simply create an iterator and then range over it, for example:

```go
client := github.NewClient(nil)
var allRepos []*github.Repository

// create an iterator and start looping through all the results
iter := client.Repositories.ListIter(ctx, "github", nil)
for repo, err := range iter {
if err != nil {
log.Fatal(err)
}
allRepos = append(allRepos, repo)
}
```

Alternatively, if you wish to use an external package, there is `enrichman/gh-iter`.
Its iterator will handle pagination for you, looping through all the available results.

```go
client := github.NewClient(nil)
Expand Down
146 changes: 114 additions & 32 deletions github/gen-iterators.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
//go:build ignore

// gen-iterators generates iterator methods for List methods.
//
// It is meant to be used by go-github contributors in conjunction with the
// go generate tool before sending a PR to GitHub.
// Please see the CONTRIBUTING.md file for more information.
package main

import (
Expand All @@ -24,7 +28,7 @@ import (
)

const (
fileSuffix = "iterators.go"
fileSuffix = "-iterators.go"
)

var (
Expand Down Expand Up @@ -56,9 +60,10 @@ func main() {

for pkgName, pkg := range pkgs {
t := &templateData{
Package: pkgName,
Methods: []*method{},
Structs: make(map[string]*structDef),
filename: pkgName + fileSuffix,
Package: pkgName,
Methods: []*method{},
Structs: make(map[string]*structDef),
}

for _, f := range pkg.Files {
Expand All @@ -83,9 +88,10 @@ func sourceFilter(fi os.FileInfo) bool {
}

type templateData struct {
Package string
Methods []*method
Structs map[string]*structDef
filename string
Package string
Methods []*method
Structs map[string]*structDef
}

type structDef struct {
Expand All @@ -102,14 +108,17 @@ type method struct {
IterMethod string
Args string
CallArgs string
TestCallArgs string
ZeroArgs string
ReturnType string
OptsType string
OptsName string
OptsIsPtr bool
UseListOptions bool
UsePage bool
TestJSON string
TestJSON1 string
TestJSON2 string
TestJSON3 string
}

// customTestJSON maps method names to the JSON response they expect in tests.
Expand Down Expand Up @@ -196,7 +205,7 @@ func getZeroValue(typeStr string) string {
case "bool":
return "false"
case "context.Context":
return "context.Background()"
return "t.Context()"
default:
return "nil"
}
Expand Down Expand Up @@ -245,6 +254,7 @@ func (t *templateData) processMethods(f *ast.File) error {

args := []string{}
callArgs := []string{}
testCallArgs := []string{}
zeroArgs := []string{}
var optsType string
var optsName string
Expand All @@ -253,10 +263,11 @@ func (t *templateData) processMethods(f *ast.File) error {

for _, field := range fd.Type.Params.List {
typeStr := typeToString(field.Type)
zeroArg := getZeroValue(typeStr)
for _, name := range field.Names {
args = append(args, fmt.Sprintf("%s %s", name.Name, typeStr))
callArgs = append(callArgs, name.Name)
zeroArgs = append(zeroArgs, getZeroValue(typeStr))
zeroArgs = append(zeroArgs, zeroArg)

if strings.HasSuffix(typeStr, "Options") {
optsType = strings.TrimPrefix(typeStr, "*")
Expand All @@ -265,6 +276,14 @@ func (t *templateData) processMethods(f *ast.File) error {
optsIsPtr = strings.HasPrefix(typeStr, "*")
}
}
// second pass: generate testCallArgs after optsName is identified
for _, name := range field.Names {
if name.Name == optsName {
testCallArgs = append(testCallArgs, name.Name)
} else {
testCallArgs = append(testCallArgs, zeroArg)
}
}
}

if !hasOpts {
Expand Down Expand Up @@ -292,6 +311,9 @@ func (t *templateData) processMethods(f *ast.File) error {
if val, ok := customTestJSON[fd.Name.Name]; ok {
testJSON = val
}
testJSON1 := strings.ReplaceAll(testJSON, "[]", "[{},{},{}]") // Call 1 - return 3 items
testJSON2 := strings.ReplaceAll(testJSON, "[]", "[{},{},{},{}]") // Call 1 part 2 - return 4 items
testJSON3 := strings.ReplaceAll(testJSON, "[]", "[{},{}]") // Call 2 - return 2 items

m := &method{
RecvType: recType,
Expand All @@ -301,14 +323,17 @@ func (t *templateData) processMethods(f *ast.File) error {
IterMethod: fd.Name.Name + "Iter",
Args: strings.Join(args, ", "),
CallArgs: strings.Join(callArgs, ", "),
TestCallArgs: strings.Join(testCallArgs, ", "),
ZeroArgs: strings.Join(zeroArgs, ", "),
ReturnType: eltType,
OptsType: optsType,
OptsName: optsName,
OptsIsPtr: optsIsPtr,
UseListOptions: useListOptions,
UsePage: usePage,
TestJSON: testJSON,
TestJSON1: testJSON1,
TestJSON2: testJSON2,
TestJSON3: testJSON3,
}
t.Methods = append(t.Methods, m)
}
Expand Down Expand Up @@ -354,24 +379,26 @@ func (t *templateData) dump() error {
return fmt.Errorf("format.Source: %v\n%s", err, buf.String())
}
logf("Writing %v...", filename)
return os.WriteFile(filename, clean, 0644)
return os.WriteFile(filename, clean, 0o644)
}

if err := processTemplate(sourceTmpl, "iterators.go"); err != nil {
if err := processTemplate(sourceTmpl, t.filename); err != nil {
return err
}
return processTemplate(testTmpl, "iterators_gen_test.go")
return processTemplate(testTmpl, strings.ReplaceAll(t.filename, ".go", "_test.go"))
}

const source = `// Copyright {{.Year}} The go-github AUTHORS. All rights reserved.
const doNotEditHeader = `// Code generated by gen-iterators; DO NOT EDIT.
// Instead, please run "go generate ./..." as described here:
// https://github.com/google/go-github/blob/master/CONTRIBUTING.md#submitting-a-patch

// Copyright 2026 The go-github AUTHORS. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
`

// Code generated by gen-iterators; DO NOT EDIT.
// Instead, please run "go generate ./..." as described here:
// https://github.com/google/go-github/blob/master/CONTRIBUTING.md#submitting-a-patch

const source = doNotEditHeader + `
package {{.Package}}

import (
Expand All @@ -383,15 +410,15 @@ import (
// {{.IterMethod}} returns an iterator that paginates through all results of {{.MethodName}}.
func ({{.RecvVar}} *{{.RecvType}}) {{.IterMethod}}({{.Args}}) iter.Seq2[{{.ReturnType}}, error] {
return func(yield func({{.ReturnType}}, error) bool) {
{{if .OptsIsPtr}}
{{if .OptsIsPtr -}}
// Create a copy of opts to avoid mutating the caller's struct
if {{.OptsName}} == nil {
{{.OptsName}} = &{{.OptsType}}{}
} else {
{{.OptsName}} = Ptr(*{{.OptsName}})
}
{{end}}

{{end}}
for {
items, resp, err := {{.RecvVar}}.{{.MethodName}}({{.CallArgs}})
if err != nil {
Expand All @@ -412,24 +439,17 @@ func ({{.RecvVar}} *{{.RecvType}}) {{.IterMethod}}({{.Args}}) iter.Seq2[{{.Retur
{{.OptsName}}.ListOptions.Page = resp.NextPage
{{else}}
{{.OptsName}}.Page = resp.NextPage
{{end}}
{{end -}}
}
}
}
{{end}}
`

const test = `// Code generated by gen-iterators; DO NOT EDIT.

// Copyright 2026 The go-github AUTHORS. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

const test = doNotEditHeader + `
package {{.Package}}

import (
"context"
"fmt"
"net/http"
"testing"
Expand All @@ -439,17 +459,79 @@ import (
func Test{{.RecvType}}_{{.IterMethod}}(t *testing.T) {
t.Parallel()
client, mux, _ := setup(t)
var callNum int
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, ` + "`" + `{{.TestJSON}}` + "`" + `)
callNum++
switch callNum {
case 1:
w.Header().Set("Link", ` + "`" + `<https://api.github.com/?page=1>; rel="next"` + "`" + `)
fmt.Fprint(w, ` + "`" + `{{.TestJSON1}}` + "`" + `) // Call 1 below: return 3 items, NextPage=1, no errors
case 2:
fmt.Fprint(w, ` + "`" + `{{.TestJSON2}}` + "`" + `) // still Call 1 below: return 4 more items, no next page, no errors
case 3:
fmt.Fprint(w, ` + "`" + `{{.TestJSON3}}` + "`" + `) // Call 2 below: return 2 items, no next page, no errors
case 4:
w.WriteHeader(http.StatusNotFound) // Call 3 below: endpoint returns an error
case 5:
fmt.Fprint(w, ` + "`" + `{{.TestJSON3}}` + "`" + `) // Call 4 below: return 2 items, no next page, no errors
}
})

// Call iterator with zero values
// Call 1: iterator using zero values
iter := client.{{.ClientField}}.{{.IterMethod}}({{.ZeroArgs}})
var gotItems int
for _, err := range iter {
gotItems++
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
}
if want := 7; gotItems != want {
t.Errorf("client.{{.ClientField}}.{{.IterMethod}} call 1 got %v items; want %v", gotItems, want)
}

// Call 2: iterator using non-nil opts
{{.OptsName}} := &{{.OptsType}}{}
iter = client.{{.ClientField}}.{{.IterMethod}}({{.TestCallArgs}})
gotItems = 0 // reset
for _, err := range iter {
gotItems++
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
}
if want := 2; gotItems != want {
t.Errorf("client.{{.ClientField}}.{{.IterMethod}} call 2 got %v items; want %v", gotItems, want)
}

// Call 3: iterator returns an error
iter = client.{{.ClientField}}.{{.IterMethod}}({{.ZeroArgs}})
gotItems = 0 // reset
for _, err := range iter {
gotItems++
if err == nil {
t.Error("expected error; got nil")
}
}
if gotItems != 1 {
t.Errorf("client.{{.ClientField}}.{{.IterMethod}} call 3 got %v items; want 1 (an error)", gotItems)
}

// Call 4: iterator returns false
iter = client.{{.ClientField}}.{{.IterMethod}}({{.ZeroArgs}})
gotItems = 0 // reset
iter(func(item {{.ReturnType}}, err error) bool {
gotItems++
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
// Force the iterator to hit:
// if !yield(item, nil) { return }
return false
})
if gotItems != 1 {
t.Errorf("client.{{.ClientField}}.{{.IterMethod}} call 4 got %v items; want 1 (an error)", gotItems)
}
}
{{end}}
`
Loading
Loading