Skip to content

iter: merge package example of iterator with complex datasource #71787

Closed as not planned
@haraldrudell

Description

Proposal Details

An example is added to the iter package comment featuring:

  • A stack-allocated iterator based on struct
  • Demonstrates an iterator for reading lines from a file
  • handling of state, error and panic
  • separating consuming the iterator from data retrieval
  • the code design used for every other problem solved by Go

Text added to package comment:

Iterators with complex datasource

Iterators encapsulating complex datasources offer value by separating
the iterator consumer from data-retrieval concerns in terms of databases,
networking and file systems. Intra-thread iteration over a function means
additional freedom in designing the iterator for concurrency, synchronization
and threading.

There are four needs on such iterators, referring to the below example:

  1. Receive and maintain internal state: filename, errp, osFile
  2. Provide iteration values and determine end of iteration: [LineReader.Lines]
  3. Release resources upon end of iteration or panic: [LineReader.cleanup]
  4. Propagate error conditions outside the for statement: errp

The below construct ensures faster stack allocation, as opposed to on the heap,
and features potentially reusable iterator-state encapsulated in struct.

The code example (formatted for package comment):

func Example() {

	// errorHandler prints error message and exits 1 on error
	var err error
	defer errorHandler(&err)

	// create test file
	var filename = filepath.Join(os.TempDir(), "test.txt")
	if err = os.WriteFile(filename, []byte("one\ntwo\n"), 0o600); err != nil {
		return
	}

	// iterate over lines from test.txt
	//	- first argument shows iterator allocated on the stack
	//	- second argument shows providing data to iterator
	//	- third argument shows receiving error from iterator
	//	- —
	//   - stack allocation is faster than heap allocation
	//   - LineReader is on stack even if NewLineReader is in another module
	//   - LineReader pointer receiver is more performant
	for line := range NewLineReader(&LineReader{}, filename, &err).Lines {
		fmt.Println("iterator line:", line)
	}
	// return here, err may be non-nil

	// Output:
	// iterator line: one
	// iterator line: two
}

// LineReader provides an iterator reading a file line-by-line
type LineReader struct {
	// the file lines are being read from
	filename string
	// a pointer to store occurring errors
	errp *error
	// the open file
	osFile *os.File
}

// NewLineReader returns an iterator over the lines of a file
//   - [LineReader.Lines] is iterator function
//   - new-function provides LineReader encapsulation
func NewLineReader(fieldp *LineReader, filename string, errp *error) (lineReader *LineReader) {
	if fieldp != nil {
		lineReader = fieldp
		lineReader.osFile = nil
	} else {
		lineReader = &LineReader{}
	}
	lineReader.filename = filename
	lineReader.errp = errp

	return
}

// Lines is the iterator providing text-lines from the file filename
//   - defer cleanup ensures cleanup is executed on panic
//     in Lines method or for block
//   - cleanup updates *LineReader.errp
func (r *LineReader) Lines(yield func(line string) (keepGoing bool)) {
	var err error
	defer r.cleanup(&err)

	if r.osFile, err = os.Open(r.filename); err != nil {
		return // i/o error
	}
	var scanner = bufio.NewScanner(r.osFile)
	for scanner.Scan() {
		if !yield(scanner.Text()) {
			return // iteration canceled by break or such
		}
	}
	err = scanner.Err()
	// reached end of file or error
}

// LineReader.Lines is iter.Seq string
var _ iter.Seq[string] = (&LineReader{}).Lines

// cleanup is invoked on iteration end or any panic
//   - errp: possible error from Lines
func (r *LineReader) cleanup(errp *error) {
	var err error
	if r.osFile != nil {
		err = r.osFile.Close()
	}
	if err != nil || *errp != nil {
		// aggregate errors in order of occurrence
		*r.errp = errors.Join(*r.errp, *errp, err)
	}
}

// errorHandler prints error message and exits 1 on error
//   - deferrable
func errorHandler(errp *error) {
	var err = *errp
	if err == nil {
		return
	}
	fmt.Fprintln(os.Stderr, err)
	os.Exit(1)
}

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Assignees

No one assigned

    Labels

    DocumentationIssues describing a change to documentation.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions