|
| 1 | +// Copyright 2025 The Go Authors. All rights reserved. |
| 2 | +// Use of this source code is governed by a BSD-style |
| 3 | +// license that can be found in the LICENSE file. |
| 4 | + |
| 5 | +package iter_test |
| 6 | + |
| 7 | +import ( |
| 8 | + "bufio" |
| 9 | + "errors" |
| 10 | + "fmt" |
| 11 | + "iter" |
| 12 | + "os" |
| 13 | + "path/filepath" |
| 14 | +) |
| 15 | + |
| 16 | +func Example() { |
| 17 | + |
| 18 | + // errorHandler prints error message and exits 1 on error |
| 19 | + var err error |
| 20 | + defer errorHandler(&err) |
| 21 | + |
| 22 | + // create test file |
| 23 | + var filename = filepath.Join(os.TempDir(), "test.txt") |
| 24 | + if err = os.WriteFile(filename, []byte("one\ntwo\n"), 0o600); err != nil { |
| 25 | + return |
| 26 | + } |
| 27 | + |
| 28 | + // iterate over lines from test.txt |
| 29 | + // - the LineReader iterator is allocated on the stack |
| 30 | + // - stack allocation is faster than heap allocation |
| 31 | + // - LineReader is on stack even if NewLineReader is in another module |
| 32 | + // - LineReader pointer receiver is more performant |
| 33 | + for line := range NewLineReader(&LineReader{}, filename, &err).Lines { |
| 34 | + fmt.Println("iterator line:", line) |
| 35 | + } |
| 36 | + // return here, err may be non-nil |
| 37 | + |
| 38 | + // Output: |
| 39 | + // iterator line: one |
| 40 | + // iterator line: two |
| 41 | +} |
| 42 | + |
| 43 | +// LineReader provides an iterator reading a file line-by-line |
| 44 | +type LineReader struct { |
| 45 | + // the file lines are being read from |
| 46 | + filename string |
| 47 | + // a pointer to store occurring errors |
| 48 | + errp *error |
| 49 | + // the open file |
| 50 | + osFile *os.File |
| 51 | +} |
| 52 | + |
| 53 | +// NewLineReader returns an iterator over the lines of a file |
| 54 | +// - [LineReader.Lines] is iterator function |
| 55 | +// - new-function provides LineReader encapsulation |
| 56 | +func NewLineReader(fieldp *LineReader, filename string, errp *error) (lineReader *LineReader) { |
| 57 | + if fieldp != nil { |
| 58 | + lineReader = fieldp |
| 59 | + osFile = nil |
| 60 | + } else { |
| 61 | + lineReader = &LineReader{} |
| 62 | + } |
| 63 | + lineReader.filename = filename |
| 64 | + lineReader.errp = errp |
| 65 | + |
| 66 | + return |
| 67 | +} |
| 68 | + |
| 69 | +// Lines is the iterator providing text-lines from the file filename |
| 70 | +// - defer cleanup ensures cleanup is executed on panic |
| 71 | +// in Lines method or for block |
| 72 | +// - cleanup updates *LineReader.errp |
| 73 | +func (r *LineReader) Lines(yield func(line string) (keepGoing bool)) { |
| 74 | + var err error |
| 75 | + defer r.cleanup(&err) |
| 76 | + |
| 77 | + if r.osFile, err = os.Open(r.filename); err != nil { |
| 78 | + return // i/o error |
| 79 | + } |
| 80 | + var scanner = bufio.NewScanner(r.osFile) |
| 81 | + for scanner.Scan() { |
| 82 | + if !yield(scanner.Text()) { |
| 83 | + return // iteration canceled by break or such |
| 84 | + } |
| 85 | + } |
| 86 | + // reached end of file |
| 87 | +} |
| 88 | + |
| 89 | +// LineReader.Lines is iter.Seq string |
| 90 | +var _ iter.Seq[string] = (&LineReader{}).Lines |
| 91 | + |
| 92 | +// cleanup is invoked on iteration end or any panic |
| 93 | +// - errp: possible error from Lines |
| 94 | +func (r *LineReader) cleanup(errp *error) { |
| 95 | + var err error |
| 96 | + if r.osFile != nil { |
| 97 | + err = r.osFile.Close() |
| 98 | + } |
| 99 | + if err != nil || *errp != nil { |
| 100 | + // aggregate errors in order of occurrence |
| 101 | + *r.errp = errors.Join(*r.errp, *errp, err) |
| 102 | + } |
| 103 | +} |
| 104 | + |
| 105 | +// errorHandler prints error message and exits 1 on error |
| 106 | +// - deferrable |
| 107 | +func errorHandler(errp *error) { |
| 108 | + var err = *errp |
| 109 | + if err == nil { |
| 110 | + return |
| 111 | + } |
| 112 | + fmt.Fprintln(os.Stderr, err) |
| 113 | + os.Exit(1) |
| 114 | +} |
0 commit comments