В этой статье я хочу показать, как просто в Go можно делать достаточно сложные вещи, и какую мощь в себе несут интерфейсы. Речь пойдет о симуляции медленного соединения — но, в отличие от популярных решений в виде правил для iptables, мы реализуем это на стороне кода — так, чтобы можно было легко использовать, к примеру, в тестах.
Ничего сложного тут не будет, и ради большей наглядности я записал ascii-анимации (с помощью сервиса asciinema), но, надеюсь, будет познавательно.
Интерфейсы
Интерфейсы — это специальный тип в системе типов Go, позволяющий описывать поведение объекта. Любой статический тип, для которого определены методы (поведение) неявно реализует интерфейс, который описывает эти методы. Самый известный пример — интерфейс из стандартной библиотеки io.Reader:
// Reader is the interface that wraps the basic Read method.
// ...
type Reader interface {
Read(p []byte) (n int, err error)
}
Любая структура, для которой вы определите метод Read([]byte) (int, error) — может использоваться как io.Reader.
Простая идея, не кажущаяся поначалу слишком ценной и мощной, принимает совсем другой вид, когда интерфейсы используются другими библиотеками. Для демонстрации этого стандартная библиотека и io.Reader — идеальные кандидаты.
Вывод в консоль
Итак, начнем с простейшего применения Reader-а — выведем строчку в stdout. Конечно, для этой задачи лучше использовать функции из пакета fmt, но мы же хотим продемонстрировать работу Reader-а. Поэтому создадим переменную типа strings.Reader (которая реализует io.Reader) и, с помощью функции io.Copy() — которая, как раз тоже работает с io.Reader, скопируем это в os.Stdout (которая, в свою очередь, имплементирует io.Writer).
package main
import (
"io"
"os"
"strings"
)
func main() {
r := strings.NewReader("Not very long line...")
io.Copy(os.Stdout, r)
}
А теперь, используя композицию (composition), создадим свой тип SlowReader, который будет читать из оригинального Reader-а по одному символу с задержкой, скажем, в 100 миллисекунд — таким образом, обеспечивая скорость 10 байт в секунду.
// SlowReader reads 10 chars per second
type SlowReader struct {
r io.Reader
}
func (sr SlowReader) Read(p []byte) (int, error) {
time.Sleep(100 * time.Millisecond)
return sr.r.Read(p[:1])
}
Что такое p[:1], надеюсь, объяснять не нужно — просто новый slice, состоящий из 1 первого символа от оргинального slice-а.
Всё что нам остается — это использовать наш strings.Reader в качестве оригинального io.Reader-а, и передать в io.Copy() медленный SlowReader! Посмотрите, как просто и круто одновременно.
(ascii-каст открывается в новом окне, js-скрипты на хабре запрещено встраивать)
Вы уже должны начать подозревать, что этот простой SlowReader можно использовать не только для вывода на экран. Также можно добавить параметр вроде delay. А еще лучше — вынести SlowReader в отдельный package, чтобы было легко использовать в дальнейших примерах. Немного причешем код.
Причёсываем код
Создадим директорию test/habr/slow и перенесем код туда:
package slow
import (
"io"
"time"
)
type SlowReader struct {
delay time.Duration
r io.Reader
}
func (sr SlowReader) Read(p []byte) (int, error) {
time.Sleep(sr.delay)
return sr.r.Read(p[:1])
}
func NewReader(r io.Reader, bps int) io.Reader {
delay := time.Second / time.Duration(bps)
return SlowReader{
r: r,
delay: delay,
}
}
Или, кому интересно смотреть ascii-касты, вот так — выносим в отдельный package:
И добавляем параметр delay типа time.Duration:
(Правильнее было бы, после выноса кода в отдельный пакет, назвать тип Reader — чтобы было slow.Reader, а не slow.SlowReader, но скринкаст уже записан так).
Чтение из файла
А теперь, практически без усилий, проверим наш SlowReader для медленного чтения из файлов. Получив переменную типа *os.File, которая хранит в себе дескриптор открытого файла, но при этом реализует интерфейс io.Reader — мы можем работать с файлом точно также, как и ранее со strings.Reader.
package main
import (
"io"
"os"
"test/habr/slow"
)
func main() {
file, err := os.Open("test.txt")
if err != nil {
panic(err)
}
defer file.Close() // close file on exit
r := slow.NewReader(file, 5) // 5 bps
io.Copy(os.Stdout, r)
}
Декодируем JSON
Но с чтением из файла — это слишком просто. Давайте рассмотрим пример чуть интереснее — JSON-декодер из стандартной библиотеки. Хотя для удобства пакет encoding/json предоставляет функцию json.Unmarshal(), он также позволяет работать с io.Reader с помощью json.Decoder — с ним можно десериализовать потоковые данные в json-формате.
Мы возьмем простую json-encoded строку и будем её «медленно читать» с помощью нашего SlowReader-а — а json.Decoder выдаст готовый объект только после того, как дойдут все байты. Чтобы это было очевидно, мы добавим в функцию slow.SlowReader.Read() вывод на экран каждого прочитанного символа:
package main
import (
"encoding/json"
"fmt"
"strings"
"test/habr/slow"
)
func main() {
sr := strings.NewReader(`{"value": "some text", "id": 42}`) // encoded json
r := slow.NewReader(sr, 5)
dec := json.NewDecoder(r)
type Sample struct {
Value string `json:"value"`
ID int64 `json:"id"`
}
var sample Sample
err := dec.Decode(&sample)
if err != nil {
panic(err)
}
fmt.Println("Decoded JSON value:", sample)
}
Если на вас ещё не свалилось осознание возможностей, которые даёт нам такая простая концепция интерфейсов, то идём дальше — собственно, приходим к теме поста — используем наш SlowReader для того, чтобы медленно скачивать страницу из интернета.
«Медленный» HTTP-клиент
Вас уже не должно удивлять, что io.Reader используется в стандартной библиотеке повсевместно — для всего, что умеет что-либо откуда-либо читать. Чтение из сети не исключение — io.Reader используется на нескольких уровнях, и спрятан под капотом такого, вроде бы, простого однострочного вызова http.Get(url string).
Для начала напишем стандартный код для HTTP GET запроса и выведем ответ в консоль:
package main
import (
"io"
"net/http"
"os"
)
func main() {
resp, err := http.Get("http://golang.org")
if err != nil {
panic(err)
}
defer resp.Body.Close()
io.Copy(os.Stdout, resp.Body)
}
Для тех, кто ещё не успел познакомиться с net/http-библиотекой — несколько объяснений. http.Get() — это обертка для метода Get() реализованного для типа http.Client — но в этой обёртке используется «подходящая для большинства случаев» уже иницилизированная переменная под названием DefaultClient. Собственно, Client дальше выполняет всю пыльную работу, в том числе и читает из сети с помощью объекта типа Transport, который в свою очередь использует более низкоуровневый объект типа net.Conn. Поначалу это может показаться запутанным, но, на самом деле, это достаточно легко изучается простым чтением исходников библиотеки — вот что-что, а стандартная библиотека в Go, в отличие от большинства других языков — это образцовый код, на котором можно (и нужно) учиться Go и брать с него пример.
Чуть ранее я упомянул про «io.Reader используется на нескольких уровнях» и это действительно так — к примеру resp.Body — это тоже io.Reader, но нам он не интересен, потому что нам интересно симулировать не тормознутый браузер, а медленное соединение — значит нужно найти io.Reader, который читает из сети. И это, забегая вперед, переменная типа net.Conn — а значит именно её нам и нужно переопределить для нашего кастомного http-клиента. Мы это можем сделать с помощью встраивания (embedding):
type SlowConn struct {
net.Conn // embedding
r slow.SlowReader // in ascii-cast I use io.Reader here, but this one a bit better
}
// SlowConn is also io.Reader!
func (sc SlowConn) Read(p []byte) (int, error) {
return sc.r.Read(p)
}
Самое сложное тут заключается в том, чтобы всё-таки немного глубже разобраться в пакетах net и net/http из стандартной библиотеки, и правильно создать наш http.Client, использующий медленный io.Reader. Но, в результате ничего сложного — надеюсь, на скринкасте видна логика, по мере того, как я поглядываю в код стандартной библиотеки.
В итоге получается следующий клиент (для реального кода это лучше вынести в отдельную функцию и чуть причесать, но для proof-of-concept примера сойдет):
client := http.Client{
Transport: &http.Transport{
Dial: func(network, address string) (net.Conn, error) {
conn, err := net.Dial(network, address)
if err != nil {
return nil, err
}
return SlowConn{conn, slow.NewReader(conn, 100)}, nil
},
},
}
Ну а теперь склеиваем это всё вместе и смотрим результат:
В конце видно, что HTTP-заголовки выводятся в консоль нормально, а текст, собственно, страницы выводится с удвоением каждого символа — это нормально, поскольку мы выводим resp.Body с помощью io.Copy() и при этом наша, чуть модифицированная, реализация SlowReader.Read() выводит каждый символ тоже.
Заключение
Как говорилось в начале статьи, интерфейсы — чрезвычайно мощный инструментарий, да и сама идея разделения типов для свойств и для поведения — очень правильная. Но по-настоящему эта мощь проявляется, когда интерфейсы действительно используются по назначению в разных библиотеках. Это позволяет соединять очень разный функционал, и использовать чужой код для вещей, о которых оригинальный автор мог даже не подозревать. И речь не только о стандартных интерфейсах — внутри больших проектов интерфейсы дают огромную гибкость и модульность.
Ссылки
Поскольку идея этого поста была нагло утянута из твиттера Francesc Campoy, то только одна ссылка :)
twitter.com/francesc/status/563310996845244416
Автор: divan0