Skip to content

Iterator Guidelines

Jonathan Amsterdam edited this page Jul 18, 2016 · 22 revisions

#Introduction

Google Cloud Platform API clients for Go should be as consistent as possible. Since Go has no standard iterator pattern, this document establishes guidelines for iterators.

Most iterators will result from the standard Google APIs List method with the pagination pattern: each call to a List method for a resource returns a sequence (“page”) of resource items (e.g. Books) along with a “next-page token” that can be passed to the List method to retrieve the next page.

Each List method will result in an iterator (which we call a List iterator) with two methods, one for individual items and one for pages. Iterators may also arise from other sources, like streaming RPCs or Cloud PubSub message subscriptions. These may or may not support page-by-page iteration.

Examples

Here is what iterators written according to these guidelines will look like to users. Here is the List iterator for the Book resource in the library example:

it := client.Books(ctx, shelfName)
for {
	book, err := it.Next()
	if err == library.Done {
		break
	}
	if err != nil {
		return err
	}
	process(book)
}

Here's the same code using a switch:

it := client.Books(ctx, shelfName)
loop: for {
	book, err := it.Next()
	switch err {
	case nil: process(book)
	case library.Done: break loop
	default: return err
}

Here is what iteration by pages looks like:

it := client.Books(ctx, shelfName)
for {
	books, err := it.NextPage()
	if err != nil && err != library.Done {
		return err
	}
	for _, b := range books {
		process(b)
	}
	if err == library.Done {
		break
	}
}

Here we retrieve the first page of 25 (or fewer) books and display the page and the next-page token:

it := client.Books(ctx, shelfName)
it.SetPageSize(25)
books, err := it.NextPage()
if err == nil {
	display(books, it.NextPageToken())
}

When the next-page token is handed back to us later (possibly in another process), we can get the next page:

it := client.Books(ctx, shelfName)
it.SetPageSize(25)
it.SetPageToken(token)
books, err := it.NextPage()
if err == nil || err == library.Done {
	display(books, it.NextPageToken())
}

The Iterator Type

An iterator should be represented by a type whose name ends in Iterator. If the iterator is a List iterator, the type's name should be ResourceIterator, E.g. BookIterator. The type should have at least one method, called Next. List iterators will also have a NextPage method. Next and NextPage are described below.

Example:

type BookIterator struct { ... }

The Creating Method

Typically, the client will have a single method that returns an iterator of a particular type. We will call this the creating method.

The name of the creating method for a List iterator should be the plural of the resource, e.g. Books (not ListBooks, which is a bit verbose for Go). For other kinds of iterators, the name of the creating method should be a plural noun, but a different name can be used if it makes more sense.

The first argument to the creating method should be a context.Context. The iterator will use that context throughout the iteration. In the unlikely event that neither the creating method nor the iterator makes any RPCs, the context can be omitted.

The creating method may accept other arguments after the context, as needed.

In most cases, the creating method will simply create an instance of the iterator type and return it, leaving the initial work, including any RPCs, to the first call to Next:

func (c *Client) Books(ctx context.Context, shelf string) *BookIterator { ... }

A creating method may return an error along with the iterator.

The Next Method

An iterator over values of type T will have a method called Next that returns (T, error). For example,

func (it *BookIterator) Next() (*Book, error) { ... }

Next will typically have no arguments, but it may in some cases. (For example, the Datastore iterator's Next method takes an argument into which it copies the entity.) None of the arguments should be a context, because Next should use the context passed when the iterator was created.

Following standard Go convention, if Next’s second return value is non-nil, then the first must be the zero value for T.

A special error value returned by Next signals the successful end of the iteration. This sentinel value will be named Done (unless that results in a conflict) and will be declared as a variable in the same package as the iterator. After Next returns Done, all subsequent calls to it will return Done.

If feasible, the user should be able to continue calling Next even if it returns an error that is not Done. If that is not feasible, it should be so documented.

The documentation comment for Next should be as follows:

Next returns the next result. Its second return value is Done if there are no more results. Once Next returns Done, all subsequent calls will return Done.

Internally, Next retrieves results in bulk. You can call SetPageSize as a performance hint to affect how many results are retrieved in a single RPC.

SetPageToken should not be called when using Next.

Next and NextPage should not be used with the same iterator.

The NextPage Method

Iterators that support pagination, such as List iterators, should also have a NextPage method that returns ([]Resource, error). For example,

func (it *BookIterator) NextPage() ([]*Book, error) { ... }

If NextPage's second return value is neither nil nor Done (in other words, an actual error), then its first return value must be nil. If the second return value is Done, the first may be nil or a zero-length slice, or it may contain a final page of items. Hence loops with NextPage should check first for a true error, then process the list of items, and finally check for Done. See the third example above.

After NextPage returns Done, all subsequent calls to NextPage should return (nil, Done).

See the next section for the recommended documentation comment for NextPage.

Page Size

Iterators that support pagination should have a SetPageSize method which sets the page size for all subsequent RPCs from the current iterator. It should take an int32 argument and have no return value:

func (it *BookIterator) SetPageSize(int32)

Callers who use only the iterator's Next method should treat SetPageSize as a hint, much as a buffer size argument in an I/O method.

The documentation comment for SetPageSize should be as follows:

SetPageSize sets the page size for all subsequent calls to NextPage.

Page size semantics may differ across clients. In those intended to reflect the underlying API closely (e.g. machine-generated clients), the page size provided by SetPageSize is a maximum; fewer items may be returned in subsequent NextPage calls. Indeed, it is valid for NextPage to return (r, nil) where len(r) == 0. In other words, there can be empty pages in the middle of the iteration. These clients should not impose a default page size; they should let the underlying service choose.

Other clients aspire to be a higher-level wrapper on top of the API, built to favor user convenience over faithfulness to the underlying RPCs. The NextPage methods in such clients should return exactly the page size (if that many items are available), to facilitate the common use case of displaying pages of items in a web or command-line interface. They should specify a default page size in a constant named DefaultPageSize, to be used if SetPageSize is not called or is called with a value less than 1.

For iterators that reflect the underlying API, the documentation comment for NextPage should be as follows:

NextPage returns the next page of results. It will return at most the number of results specified by the last call to SetPageSize. If SetPageSize was never called or was called with a value less than 1, the page size is determined by the underlying service.

NextPage may return a second return value of Done along with the last page of results. After NextPage returns Done, all subsequent calls to NextPage will return (nil, Done).

Next and NextPage should not be used with the same iterator.

For iterators that return the exact page size, the first paragraph should instead read:

NextPage returns the next page of results. It will return exactly the number of results specified by the last call to SetPageSize, unless there are not enough results available. If SetPageSize was never called or was called with a value less than 1, it uses DefaultPageSize.

Page Tokens

Google API calls that support pagination typically return a next-page token, which can be passed to a subsequent RPC to obtain the next page of results. Iterators built on such RPCs should provide two methods to support this, NextPageToken and SetPageToken. Note that for a simple loop implementing page-by-page iteration as in the example above, explicit use of the page token is unnecessary; the iterator will perform the necessary manipulations (the equivalent of SetPageToken(NextPageToken())) itself. The page token methods are only necessary when an iteration needs to be resumed later, possibly in another process.

The NextPageToken method should take no arguments and return the next-page token, typically a string:

func (it *BookIterator) NextPageToken() string

The SetPageToken method should accept a string and have no return value:

func (it *BookIterator) SetPageToken(string) { ... }

It should set the page token for the next RPC.

The documentation comment for NextPageToken should be as follows:

NextPageToken returns a page token that can be used with SetPageToken to resume iteration from the next page. It returns the empty string if there are no more pages.

The documentation comment for SetPageToken should be as follows:

SetPageToken sets the page token for the next call to NextPage, to resume the iteration from a previous point.

The Close and Stop Methods

An iterator may require clean-up work to happen after it completes, or if the user abandons it before reaching the end. The iterator type should have a method of no arguments that performs this clean-up.

If the clean-up work can result in an error that should be returned, the method should be named Close. If no error is possible (or if the error is to be exposed in another way), the method should be named Stop:

func (it *CleanupWithErrorIterator) Close() error { ... }

func (it *CleanupNoErrorIterator) Stop() { ... }

If an iterator does not require clean-up, it should define neither Close nor Stop.

If Close or Stop does not support being called multiple times, that should be documented.

Other Guidelines

If an iterator supports both Next and NextPage, it is not required to be “smart” about interleaved calls to those methods. That is, it need not support any particular semantics for a call to Next followed by a call to NextPage, or vice versa. For the life a single iterator, we expect users to call only Next, or only NextPage.

None of the iterator methods are required to be thread-safe, and Close (or Stop), Next and NextPage are not required to be concurrently callable.

Iterators may have fields and methods other than those described here.

Clone this wiki locally