Читачі та письменники¶
io.Reader та io.Writer — два найважливіших інтерфейси в Go.
Інтерфейси представили їх
як приклади маленьких інтерфейсів; тут показано, як ними користуватись
насправді.
b, err := io.ReadAll(strings.NewReader("abc"))
fmt.Println(err) // output: <nil>
fmt.Printf("%q\n", string(b)) // output: "abc"
По одному методу¶
type Reader interface { Read(p []byte) (n int, err error) }
type Writer interface { Write(p []byte) (n int, err error) }
Це весь контракт, і саме тому файл, мережеве з'єднання, тіло HTTP-
запиту, gzip-потік, strings.Reader і bytes.Buffer — усі
взаємозамінні. Функція, що приймає io.Reader, працює з кожним із них,
включно з тими, яких не існувало, коли її написали.
Беріть інтерфейс, а не *os.File. Це нічого не коштує й робить функцію
тестованою через strings.NewReader.
io.Copy — робочий кінь¶
Копіювання переміщує дані фіксованими шматками, тож пам'ять залишається рівною незалежно від розміру джерела:
var dst bytes.Buffer
n, err := io.Copy(&dst, strings.NewReader("stream me"))
fmt.Println(n, err, dst.String()) // output: 9 <nil> stream me
Копіювання файлу — це два відкриття й один Copy — функції os.Copy
немає:
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return err
}
if _, err := io.Copy(out, in); err != nil {
out.Close()
return err
}
return out.Close() // закриття, яке має значення: повідомте його помилку
io.Discard — це письменник, який усе викидає, корисний для
спорожнення тіла запиту, яке вам байдуже:
io.EOF — це значення, а не збій¶
Read повертає кількість отриманих байтів і помилку. У кінці вводу ця
помилка — io.EOF, що є звичайним результатом:
r := strings.NewReader("hello")
buf := make([]byte, 2)
for {
n, err := r.Read(buf)
if n > 0 {
fmt.Printf("%q ", string(buf[:n]))
}
if err == io.EOF {
break
}
}
// output: "he" "ll" "o"
Два правила, на яких люди спотикаються. Обробляйте байти до перевірки
помилки — Read може повернути дані й io.EOF одночасно. І завжди
зрізайте до buf[:n]; решта буфера містить те, що там було раніше.
Ви рідко пишете цей цикл самі. io.ReadAll, io.Copy і
bufio.Scanner роблять це за вас.
io.ReadFull — виняток, що трактує короткий зчитаний фрагмент як
помилку:
_, err := io.ReadFull(strings.NewReader("abc"), make([]byte, 10))
fmt.Println(errors.Is(err, io.ErrUnexpectedEOF), err)
// output: true unexpected EOF
bufio.Scanner для рядків¶
Читання рядок за рядком — достатньо поширена задача, щоб мати спеціальний тип:
sc := bufio.NewScanner(r)
for sc.Scan() {
fmt.Println(sc.Text())
}
if err := sc.Err(); err != nil {
return err
}
Scan повертає false і в кінці вводу, і при помилці, тож
перевірка sc.Err() після циклу не опційна — пропустіть її, і збій
читання буде виглядати точнісінько як чистий кінець файлу.
Text() повертає рядок без символу нового рядка. Bytes() уникає
виділення пам'яті, але дійсний лише до наступного Scan.
Змінити те, що вважається токеном, можна через Split:
sc := bufio.NewScanner(strings.NewReader("a bb ccc"))
sc.Split(bufio.ScanWords)
for sc.Scan() {
fmt.Printf("%q ", sc.Text())
}
// output: "a" "bb" "ccc"
bufio.ScanLines — типовий варіант; ScanWords, ScanRunes і
ScanBytes — інші.
Ліміт у 64 КБ¶
Scanner відмовляється від будь-якого токена, довшого за 64 КБ, і
повідомляє про це як про помилку, а не обрізає:
long := strings.Repeat("x", 100000)
sc := bufio.NewScanner(strings.NewReader(long))
fmt.Println(sc.Scan(), sc.Err())
// output: false bufio.Scanner: token too long
Це класичний баг обробника логів: він працює, поки не прийде один
величезний рядок, а тоді зупиняється, і без перевірки sc.Err()
зупиняється мовчки. Піднімайте межу, коли рядки можуть бути
довгими:
sc := bufio.NewScanner(strings.NewReader(long))
sc.Buffer(make([]byte, 0, 64*1024), 1024*1024)
fmt.Println(sc.Scan(), sc.Err(), len(sc.Text()))
// output: true <nil> 100000
Для дійсно необмежених рядків використовуйте
bufio.Reader.ReadString('\n'), який росте за потреби.
bufio.Writer треба скидати¶
Обгортання письменника пакує багато дрібних записів у кілька великих. Буферизовані дані не записуються, поки ви не скажете:
var out bytes.Buffer
w := bufio.NewWriter(&out)
w.WriteString("buffered")
fmt.Printf("%q ", out.String()) // output: ""
w.Flush()
fmt.Printf("%q\n", out.String()) // output: "buffered"
defer w.Flush() відкидає помилку. Якщо дані важливі, скидайте явно й
перевіряйте.
Комбінування читачів і письменників¶
lb, _ := io.ReadAll(io.LimitReader(strings.NewReader("abcdefgh"), 3))
fmt.Printf("%q\n", string(lb)) // output: "abc"
io.LimitReader обмежує, скільки читається — стандартний захист від
тіла запиту, яке заявляє, що воно нескінченне. io.TeeReader пропускає
дані крізь себе, копіюючи їх ще кудись, а io.MultiReader з'єднує
читачів один за одним.
Написання власного¶
Реалізуйте один метод — і все вище працює з вашим типом:
type upperReader struct{ r io.Reader }
func (u upperReader) Read(p []byte) (int, error) {
n, err := u.r.Read(p)
for i := range p[:n] {
if p[i] >= 'a' && p[i] <= 'z' {
p[i] -= 32
}
}
return n, err
}
b, _ := io.ReadAll(upperReader{strings.NewReader("shout")})
fmt.Printf("%q\n", string(b)) // output: "SHOUT"
Зверніть увагу: перетворюється лише p[:n], а помилка передається без
змін.
Звідки береться кожне джерело¶
| У вас є | Обгорніть через |
|---|---|
string |
strings.NewReader(s) |
[]byte |
bytes.NewReader(b) |
| байти, що ростуть | bytes.Buffer — і читач, і письменник |
| рядок, який будується | strings.Builder — лише письменник |
| файл | os.Open / os.Create |
| стандартні потоки | os.Stdin, os.Stdout, os.Stderr |
| нічого | io.Discard |
strings.NewReader — причина, чому кожен приклад у цій статті можна
запустити без файлу, і саме так варто тестувати будь-що у формі читача.
З досвіду Python:
io.Reader— це кінець файлового об'єкта для читання, але звужений до одного методу, тож будь-що може ним бути.io.Copy— цеshutil.copyfileobj. Дві відмінності, які кусаються:EOF— це повернуте значення, а не порожній рядок, аbufio.Scannerмовчки обмежує рядки 64 КБ там, де ітерація в Python такого ліміту не має.
Швидка довідка¶
| Завдання | Виклик |
|---|---|
| прочитати все | io.ReadAll(r) |
| перенести з одного в інше потоково | io.Copy(w, r) |
| скопіювати файл | os.Open + os.Create + io.Copy |
| рядок за рядком | bufio.NewScanner(r), потім перевірте sc.Err() |
| довгі рядки | sc.Buffer(...) або bufio.Reader.ReadString('\n') |
| пакувати дрібні записи | bufio.NewWriter(w) + Flush() |
| обмежити ввід | io.LimitReader(r, n) |
| розгалузити | io.MultiWriter(a, b) |
| спостерігати проходячи повз | io.TeeReader(r, w) |
| викинути | io.Discard |
| читач із рядка | strings.NewReader(s) |