Почав писати log collector з S3 до VictoriaLogs з використанням AWS GO SDK, і в коді достатньо багато використовуються різні Input/Ouput операції, бо треба отримати лог, розпарсити, записати дані.
В попередньому пості по інтерфейсам – Golang: interfaces – “магія” виклику методів через інтерфейси – вже трохи торкався теми того, що таке інтерфейси і як саме вони працюють, але там не було прикладів того, як саме вони використовуються.
Тож цього разу подивимось на дуже класний приклад використання інтерфейсів при роботі з функцією io.Copy() і ще раз трохи зазирнемо під капот внутрішньої реалізації інтерфейсів в Go.
Зміст
Basic I/O example – os.Open(), os.Create() та io.Copy()
Напишемо простий код, який буде з одного файлу копіювати в інший:
package main
import (
"fmt"
"io"
"os"
)
func main() {
// open the source file for reading
// returns pointer to os.File:
// 'func os.Open(name string) (*os.File, error)':
//
// os.File represents an open file descriptor:
// type File struct {
// // contains filtered or unexported fields
// }
sourceFile, err := os.Open("source.txt")
if err != nil {
panic(err)
}
// always close files when done
defer sourceFile.Close()
// create destination file for writing
// 'func os.Create(name string) (*os.File, error)'
destFile, err := os.Create("dest.txt")
if err != nil {
panic(err)
}
defer destFile.Close()
// copy data from source to destination
// io.Copy() pulls bytes from any Reader and pushes them into any Writer
// 'func io.Copy(dst io.Writer, src io.Reader) (written int64, err error)'
bytesWritten, err := io.Copy(destFile, sourceFile)
if err != nil {
panic(err)
}
fmt.Println("Copied bytes:", bytesWritten)
}
Тут ми:
- з
os.Open("source.txt")відкриваємо файл на читання - з
os.Create("dest.txt")створюємо файл, в який будемо копіювати дані - і з
io.Copy()копіюємо дані з “source.txt” до “dest.txt“
Створюємо тестовий файл source.txt:
$ echo source > source.txt
Запускаємо нашу програму:
$ go run main.go Copied bytes: 7
Що для нас важливо зараз – що виклики os.Open() і os.Create() повертають об’єкти типу *os.File struct – посилання на структуру.
А *os.File struct має набір методів, які далі використовуються для запуску процесу копіювання файлу з io.Copy().
Методи інтерфейсів на прикладі io.Copy()
Отже, до функції io.Copy() передаються destFile та sourceFile, які є об’єктами з типом *os.File:
... bytesWritten, err := io.Copy(destFile, sourceFile) ...
Функція io.Copy() приймає два аргументи – dst Writer та src Reader, які далі передає до функції copyBuffer():
// Copy copies from src to dst until either EOF is reached
func Copy(dst Writer, src Reader) (written int64, err error) {
return copyBuffer(dst, src, nil)
}
copyBuffer() приймає такі самі аргументи та повертає такі самі дані:
func copyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error) {
...
}
І для обох функцій – і Copy(), і copyBuffer() – аргументи є interface type:
А type Writer interface описує вимоги до типу, що може бути використаний через цей інтерфейс: такий тип повинен мати метод Write(), приймати аргумент з типом slice of bytes, і повертати значення int та err:
// Writer is the interface that wraps the basic Write method.
type Writer interface {
Write(p []byte) (n int, err error)
}
Тобто в нашому коді:
...
bytesWritten, err := io.Copy(destFile, sourceFile)
...
Об’єкт destFile повинен мати метод Write() – який в нього є, бо destFile – це *os.File struct, в якої є набір методів, в тому числі як раз Read() та Write():
$ go doc os.File
package os // import "os"
type File struct {
// Has unexported fields.
}
File represents an open file descriptor.
...
func (f *File) Read(b []byte) (n int, err error)
...
func (f *File) Write(b []byte) (n int, err error)
Де метод Write() приймає слайс байтів []byte:
func (f *File) Write(b []byte) (n int, err error) {
...
}
А отже, маючи об’єкт з типом *os.File – ми через відповідні інтерфейси можемо викликати *os.File.Write():
func Copy(dst Writer, ...)каже – “dst повинен мати метод Write([]byte) (int, error)“- тип
*os.Fileмає методWrite()– а значить він задовольняє Writer interface
Тобто: інтерфейси в Go описують не типи даних, а вимоги до методів, які мають бути реалізовані, щоб ці методи можна було викликати через інтерфейс.
І коли ми пишемо і запускаємо io.Copy(destFile, ...) – під капотом Go під час компіляції програми:
- перевіряє, який тип приймає
io.Copy()– це interface type - аби задовільнити конкретно цей interface type – об’єкт (тип), який передається аргументом до
io.Copy(), повинен мати методWrite() - Go перевіряє, чи є у переданого типу такий метод – чи є для об’єкту
*os.FileметодWrite()
Далі – “магія”, описана в попередньому пості: ще раз глянемо на те, як працюють інтерфейси, як через них викликаються методи, і що саме знаходиться в аргументах io.Copy() та copyBuffer() при роботі програми.
Структури iface та itab
Коли ми передаємо обʼєкт (pointer на *os.File) у параметр із типом інтерфейсу (dst Writer) – то Go формує дві внутрішні структури, які передаються до функцій як interface value.
Перша, type iface structiface, має два поля – tab і data:
type iface struct {
// Pointer to the 'itab' (interface table)
tab unsafe.Pointer
// Pointer to the actual data (our *os.File struct)
data unsafe.Pointer
}
Де:
tab unsafe.Pointer: pointer на другий типtype itab, який описаний вtype ITab struct- раніше було
type itab struct, зараз перенесли в Go ABI, про ABI в наступному пості
- раніше було
data unsafe.Pointer: pointer на наш об’єкт з типомos.Filestruct, який має методWrite()
Друга структура, ITab struct, має свої три поля:
type ITab struct {
// pointer to the 'type Writer interface'
Inter *InterfaceType
// pointer to the 'type File struct'
Type *Type
// in our case if we have 1 method, thus '[N]uintptr' == [1]uintptr
// and in the 'fun[0]' will be the address of the method 'Write()' of the 'os.File' struct
Fun [1]uintptr // will have '[1]uintptr', and
}
Тут:
Inter *InterfaceType: pointer на описtype Writer interface- “який інтерфейс треба задовольнити“
Type: pointer на опис конкретного типу значення (у нашому випадку тип*os.File)- “який тип ми передаємо“
'Fun[0]': буде посиланням на методWrite()структуриos.File- “ось адреси методів, які цей тип використовує для реалізації цього інтерфейсу“
І коли ми в коді передаємо значення типу *os.File в параметр інтерфейсного типу (dst Writer) – то Go створює ці структури, і передає структура iface з полями tab і data до виклику io.Copy(), а потім далі – до copyBuffer():
io.Copy(iface): - iface.tab => вказівник на структуру itab - iface.data => вказівник на *os.File
В itab struct маємо таблицю методів, пов’язаних з цим інтерфейсом (або – які імплементують цей інтерфейс), а в полі fun структури itab знаходиться масив з pointers, де кожен елемент містить адресу функції, яка реалізує відповідний метод інтерфейсу для конкретного типу.
І у випадку з інтерфейсом Writer – це буде масив fun[0] зі значенням, наприклад, 0xc000014070, де за адресою 0xc000014070 буде розташований метод Write() типу *os.File.
І коли в copyBuffer(dst Writer) виконується виклик Write(), який описаний як:
func copyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error) {
...
nw, ew := dst.Write(buf[0:nr])
...
То фактично це перетворюється на виклик itab.fun[0]:
copyBuffer(iface struct) => itab struct => fun[0] field => 0xc000014070 => os.File.Write()
Повертаючись до твердження “при виклику io.Copy(*os.File) – викликається copyBuffer(), якому першим аргументом передається структура iface” – давайте подивимось на аргументи, з якими ми працюємо.
Перевірка типів інтерфейсних значень в аргументах
Аби побачити все своїми очима – повторимо “хак” з попереднього поста – створимо власну структуру, яка аналогічна до iface, бо напряму до iface ми звернутись не можемо – але можемо прочитати її памʼять через unsafe.Pointer.
І на додачу створимо власну функцію myCopy(), яка буде мати в параметрах наші власні інтерфейси – аналогічно тому, як це зроблено для io.Copy().
Тобто – ми повністю повторюємо поведінку оригінального io.Copy(), але замість справжніх io.Reader та io.Writer використовуємо свої інтерфейси і власну структуру myIfaceStruct, аби подивитись, як Go зберігає інтерфейс у памʼяті:
- створюємо два об’єкти
sourceFileтаdestFile, які є pointers на*os.File - описуємо власну функцію
myCopy(), яка в параметрах описує отримання інтерфейсних типів - наші інтерфейси
myReaderInterfaceтаmyWriterInterfaceвимагають методівRead()таWrite(), які є уsourceFileтаdestFile
Код виходить такий:
package main
import (
"fmt"
"io"
"os"
"unsafe"
)
type myIfaceStruct struct {
tab unsafe.Pointer
data unsafe.Pointer
}
// Writer is the interface that wraps the basic Write method.
type myWriterInterface interface {
// define Write method to satisfy the myWriterInterface interface
Write(p []byte) (n int, err error)
}
// Reader is the interface that wraps the basic Read method.
type myReaderInterface interface {
// define Read method to satisfy the myReaderInterface interface
Read(p []byte) (n int, err error)
}
// accept any type which has Read and Write methods
func myCopy(src myReaderInterface, dst myWriterInterface) (int64, error) {
// '&src' gives us the address of the interface variable 'src'
// 'unsafe.Pointer(&src)' allows us to reinterpret that memory as a different type
// the interface value occupies 16 bytes:
// - first 8 bytes: pointer to the method/type table ('tab')
// - next 8 bytes: pointer to the actual value ('data')
// '(*myIfaceStruct)(...)' tells Go to treat those bytes as a 'myIfaceStruct'
// '*(*myIfaceStruct)(...)' finally copies those bytes into the 'rawIface' variable
rawIface := *(*myIfaceStruct)(unsafe.Pointer(&src))
fmt.Println()
// Print diagnostic messages
//
// we intentionally use '%p' modifier with a non-pointer value argument
// this causes a formatting error, and 'fmt' prints a diagnostic message
// that includes the full content of 'rawIface' (its type and both fields)
fmt.Printf("'rawIface' data: %p\n", rawIface)
// same idea for %s: &src is a *myReaderInterface, not a string
// so fmt prints a diagnostic message showing the type and value
fmt.Printf("'src' data: %s\n", &src)
fmt.Println()
// Print addresses from the 'iface' struct
//
// 'tab' field is a pointer to the interface's method table (the 'itab' struct)
// this value is copied from the real interface value stored in 'src'
fmt.Printf("Copy of the 'iface.tab': address stored inside 'rawIface.tab': %p\n", rawIface.tab)
// 'data' field is a pointer to the underlying object (the *os.File struct)
// also copied directly from the actual interface storage
fmt.Printf("Copy of the 'iface.data': address stored inside 'rawIface.data': %p\n", rawIface.data)
// print the address of the real underlying object (*os.File)
// this should match the value stored in rawIface.data
fmt.Printf("The 'src' (*os.File) actual object address: %p\n", src)
fmt.Println()
// Test sizes
//
// 'src' will have 16 bytes
// because 'iface' has two fields: 'tab' and 'data'
// they are pointers, each of 8 bytes
fmt.Println("sizeof the 'src' (size of 'iface' with two pointers):", unsafe.Sizeof(src))
// but pointer to the '*os.File' object size will be 8 bytes
testSource, _ := os.Open("source.txt")
fmt.Println("sizeof the 'testSource' (size of '*os.File' with one pointer):", unsafe.Sizeof(testSource))
fmt.Println()
// demonstrate "dynamic types"
//
// - Printf '%T' modifier will print the type of the variable
// - Printf '%p' modifier will print the address pointed to by '&'
fmt.Printf("'src' type: %T\n", src)
// address of the the 'src'
fmt.Printf("'src' address: %p\n", &src)
fmt.Println()
return io.Copy(dst, src)
}
func main() {
// sourceFile is *os.File
sourceFile, _ := os.Open("source.txt")
defer sourceFile.Close()
// destFile is *os.File
destFile, _ := os.Create("dest.txt")
defer destFile.Close()
myCopy(sourceFile, destFile)
}
Запускаємо:
$ go run test-int.go
'rawIface' data: %!p(main.myIfaceStruct={0x4eee38 0xc000062030})
'src' data: %!s(*main.myReaderInterface=0xc000014070)
Copy of the 'iface.tab': address stored inside 'rawIface.tab': 0x4eee38
Copy of the 'iface.data': address stored inside 'rawIface.data': 0xc000062030
The 'src' (*os.File) actual object address: 0xc000062030
sizeof the 'src' (size of 'iface' with two pointers): 16
sizeof the 'testSource' (size of '*os.File' with one pointer): 8
'src' type: *os.File
'src' address: 0xc000014070
І розбираємо результат.
Перші два – зовсім “грязний хак”, випадково на нього натрапив: якщо до модифікатора в fmt.Printf() передати не той тип даних, який він очікує – він виводить повідомлення з деталями по помилці, де можемо побачити, що саме повністю передавалось (хоча як виявилось, під капотом просто викликається (reflect.TypeOf(p.arg).String())).
Перший блок:
rawIfaceє типомmain.myIfaceStruct, яка містить два вказівники на адреси 0x4eee38 та 0xc000062030 – див. далі про змістrawIfacesrcє поінтером на*main.myReaderInterface– структуру, яка знаходиться за адресою 0xc000014070
Далі – виводимо адреси, які зберігаються в полях iface (і які ми отримали через нашу власну структуру):
'rawIface.tab': 0x4eee38– тут адреса розміщенняitabstruct'rawIface.data': 0xc000062030– тут адреса переданого черезsrcоб’єктуos.File- і ту саму адресу ми бачимо в наступному рядку –
srcє pointer на*os.File, зPrintf(%p)отримуємо адресу, на якуsrcвказує
Найбільш явний доказ того, що насправді myCopy() у (src myReaderInterface) працює з інтерфейсом, а не *os.File – це розмір:
- з
unsafe.Sizeof(src)отримуємо розмір самого інтерфейсного значення (iface), яке складається з двох pointers –tabіdata, по 8 байт кожен - а
testSource := os.Open("source.txt")має розмір 8 байт, бо це один поінтер
Інтерфейси Go та “dynamic type”
А далі ми бачимо те, що називають “динамічними типами”: в результатах unsafe.Sizeof(src)) ми побачили, що там 2 поінтери, тобто це 100% тип interface value з двома pointers.
Але в fmt.Printf("'src' type: %T\n", src) ми отримуємо тип *os.File – бо це pointer на структуру os.File:
$ go run test-int.go ... 'src' data: %!s(*main.myReaderInterface=0xc000014070) ... 'src' type: *os.File 'src' address: 0xc000014070
То чому не якийсь myReaderInterface{}?
Дивимось документацію по Variables:
The static type (or just type) of a variable is the type given in its declaration, the type provided in the new call or composite literal, or the type of an element of a structured variable. Variables of interface type also have a distinct dynamic type, which is the (non-interface) type of the value assigned to the variable at run time (unless the value is the predeclared identifier nil, which has no type). The dynamic type may vary during execution but values stored in interface variables are always assignable to the static type of the variable.
Отже, змінна має static type, коли:
- змінна оголошується (
var i int) - тип заданий під час присвоювання даних при виклику функцій (
x := new(int)) - при використанні composite literal (
y := []string{"a", "b"})
Проте змінні інтерфейсного типу завжди мають фіксований статичний тип (сам інтерфейс) – але реальний об’єкт всередині неї має окремий dynamic type – це конкретний тип значення, присвоєного під час виконання.
І в нашому прикладі вище – iface.data як раз і є тою змінною, яка визначає dynamic type, і тому ми в результаті fmt.Printf("'src' type: %T\n", src) бачимо саме *os.File.
Додаємо до нашого коду ще трохи дебагу:
...
// show the static type of the interface itself
// - (*myReaderInterface)(nil) creates a nil pointer to the interface type
// - reflect.TypeOf(...) gives the type of that pointer
// - Elem() gives the type the pointer points to (the interface type)
// this demonstrates that the static type is 'myReaderInterface'
fmt.Println("static type of the 'myReaderInterface':", reflect.TypeOf((*myReaderInterface)(nil)).Elem())
// show the dynamic type stored inside the interface variable 'src'
// - 'src' is an interface value (16-byte iface: tab + data)
// - reflect.TypeOf(src) reads the real type stored in iface.data
// this prints the actual type, '*os.File' in our case
//
// and this is exactly the same information that 'fmt.Printf("%T", src)' prints:
// both reflect.TypeOf(src) and %T reveal the dynamic type stored in the interface
fmt.Printf("'src' dynamic type: %v\n", reflect.TypeOf(src))
// show the type of the variable 'src' itself, not the value stored inside it
// this is exactly what the myCopy() function "sees" when receiving its argument
// - '&src' is a pointer to the interface variable
// - reflect.TypeOf(&src) therefore reports: "*myReaderInterface"
// this confirms that 'src' is an interface-typed variable, not a concrete value
fmt.Println("'src' variable type: ", reflect.TypeOf(&src))
...
Результат:
$ go run test-int.go ... static type of the 'myReaderInterface': main.myReaderInterface 'src' dynamic type: *os.File 'src' variable type: *main.myReaderInterface
Тут ми:
- в першій перевірці просто створюємо вказівник на інтерфейсний тип (але без створення самого об’єкту): результат є
main.myReaderInterface - другий результат – “прочитай значення інтерфейсної змінної src, і скажи, який там тип” – саме тут ми бачимо, що в
iface.dataзберігається pointer на об’єкт типу –*os.File - третя перевірка – “сходи за адресою, де зберігається змінна src, і скажи який за цією адресою тип даних” – отримуємо pointer на
*main.myReaderInterface
Використання інтерфейсів на прикладі io.Copy()
То що це все значить для нас?
А значить, що використовуючи інтерфейси, ми можемо передати будь-які значення (типи), які реалізують інтерфейс.
Якщо повернутись до нашого першого коду, то в io.Copy() першим параметром ми можемо передати будь-який тип, який має метод Write([]byte) (int, error), а в другий – аналогічно, тільки Read(), бо під капотом Copy() викликає copyBuffer(), а той просто створює буфер розміром в 32 кілобайти, чей який “переливає” з одного “каналу” в інший:
func copyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error) {
...
if buf == nil {
size := 32 * 1024
...
buf = make([]byte, size)
}
for {
nr, er := src.Read(buf)
if nr > 0 {
nw, ew := dst.Write(buf[0:nr])
...
А значить – ми можемо у Writer передати os.Stdout, тобто просто вивести на консоль:
...
func main() {
// open the source file for reading
// returs pointer to os.File:
// 'func os.Open(name string) (*os.File, error)':
//
// os.File represents an open file descriptor:
// type File struct {
// // contains filtered or unexported fields
// }
sourceFile, err := os.Open("source.txt")
if err != nil {
panic(err)
}
// always close files when done
defer sourceFile.Close()
// printto console instead
// actualy, os.Stdout is also *os.File
// thus it also has Write() method
bytesWritten, err := io.Copy(os.Stdout, sourceFile)
if err != nil {
panic(err)
}
fmt.Println("Copied bytes:", bytesWritten)
}
Результат:
$ go run main.go source Copied bytes: 7
Або можемо створити власний буфер в пам’яті, і писати в нього, бо bytes.Buffer теж має метод Write():
$ go doc bytes.Buffer | grep "Write("
func (b *Buffer) Write(p []byte) (n int, err error)
А тому ми можемо передати його до io.Copy():
...
buf := &bytes.Buffer{}
io.Copy(buf, sourceFile)
fmt.Println("Buffer content:", buf.String())
...
Результат:
$ go run main.go Buffer content: source
Або більш цікавий приклад – з викликом одного інтерфейсу через інший.
Такий код:
...
// create a pointer to bytes.Buffer (it implements io.Writer)
buf := &bytes.Buffer{}
// var resp *http.Response
resp, _ := http.Get("https://example.com")
defer resp.Body.Close()
fmt.Printf("resp.Body %T\n", resp.Body)
io.Copy(buf, resp.Body)
// print the buffer content
fmt.Println("Buffer content:", buf.String())
...
Результат:
$ go run main.go resp.Body type: *http.http2gzipReader Buffer content: <!doctype html><html lang="en"><head><title>Example Domain</title> ...
Тут:
http.Get()повернув*http.Response, тобто структуруResponse- у структурі
Responseє полеBodyз типомio.ReadCloserinterface - інтерфейс
io.ReadCloserвизначає два методи –Read()іClose() - сервер надіслав відповідь у gzip, тому HTTP-клієнт Go автоматично обгорнув
Bodyу декомпресор – тип*http.http2gzipReader - тип
*http.http2gzipReaderреалізує методRead(), тому він задовольняє інтерфейсio.Reader - також у нього є метод
Close(), тому він задовольняє іio.ReadCloser - оскільки
resp.Bodyмає динамічний тип*http.http2gzipReader, то викликio.Copy(buf, resp.Body)фактично викликає(*http.http2gzipReader).Read() - тому
io.Copy()може прийнятиresp.Bodyякio.Readerі прочитати через gzip-декодер
Власне, мабуть, на цьому і все.
Далі треба вчитись писати код з інтерфейсами самому.
