Інтерфейси в Go дозволяють описати доступ до даних або методів без створення самих реалізацій в цих інтерфейсах.
Таким чином ми створюємо “загальну шину”, яку далі можемо використовувати для “підключення” зовнішніх “систем”.
Тобто інтерфейс – це абстракція, яка описує доступ до іншого типу, але конкретна реалізація цієї поведінки вже буде залежати від того, що саме ми підключимо до інтерфейсу.
Взагалі хотів просто написати пост при використання інтерфейсів, але натомість вийшов пост про те, як інтерфейси реалізовані взагалі, і про “магію” того, як через них відбувається виклик даних.
Насправді спочатку було б добре написати про pointers та методи в Go, бо в цьому матеріалі саме на них побудоване все пояснення роботи інтерфейсів, але це вже іншим разом.
Див. доповнення у Golang: інтерфейси, типи та методи на прикладі io.Copy().
Зміст
Empty Interfaces
Найпростіший приклад інтерфейсу – коли в ньому не задається ані метод, ані тип даних, який цей метод повертає.
Використовуючи такий інтерфейс ми можемо створити функцію, яка буде приймати будь-який тип даних – бо інакше при оголошенні функції нам треба вказати тип даних, який вона приймає в параметрі:
package main
import "fmt"
// define an empty interface
// it can hold a value of any type
type Any interface{}
func printValue(v Any) {
// print the value
fmt.Println("Value:", v)
}
func main() {
// pass int
printValue(42)
// pass string
printValue("hello")
// pass float
printValue(3.14)
// pass slice
printValue([]int{1, 2, 3})
}
Без використання інтерфейсу – нам довелось би створювати окремі функції для кожного типу, який ми хочемо передати, або, як альтернатива – використовувати generics.
Інший варіант створення порожнього інтерфейсу – це використання типу any, який по факту є аліасом на interface{}:
...
type MyAny any
func printValue(v Any) {
// print the value
fmt.Println("Value:", v)
}
...
Interfaces та Methods
Якщо “порожній” інтерфейс any каже “я приймаю будь-яке значення”, то “класичний” інтерфейс каже: “мене цікавить лише певна поведінка”.
Ця поведінка описується через набір сигнатур методів (method signatures), які тип має реалізувати, щоб відповідати інтерфейсу
Коли ми створюємо змінну інтерфейсного типу або передаємо значення у функцію, що приймає інтерфейс, Go перевіряє, чи тип реалізує всі методи цього інтерфейсу, і створює зв’язок між ними.
І потім ми, використовуючи цей інтерфейс, можемо викликати пов’язані з ним методи.
Тобто інтерфейс – це посередник, який дозволяє викликати метод незалежно від конкретного типу.
Наприклад:
package main
import "fmt"
// define an interface 'MyInterface' with a single method 'MyMethod' returning a string
type MyInterface interface {
MyMethod() string
}
// define 'MyStruct' struct with a 'MyField' field
type MyStruct struct {
MyField string
}
// define 'MyMethod' method for the 'MyStruct' struct
// this makes 'MyStruct' implicitly implement 'MyInterface'
// 'MyMethod' method uses 'MyStruct' as the receiver, so this method is tied to the 'MyStruct' type
func (receiver MyStruct) MyMethod() string {
return "Executing " + receiver.MyField
}
// define a function 'sayHello()' which accepts any type that implements 'MyInterface'
// and prints the value returned by its 'MyMethod'
func sayHello(g MyInterface) {
fmt.Println(g.MyMethod())
}
func main() {
// create an instance of MyStruct
myObj := MyStruct{MyField: "Hello, Interface!"}
// pass the MyStruct instance to the function.
// this works because MyStruct implements MyInterface.
sayHello(myObj)
}
Тут ми:
- оголошуємо власний інтерфейсний тип з іменем
MyInterface - цей інтерфейс описує одну сигнатуру методу –
MyMethod(), і цей метод має повертати дані з типом string - створюємо власний тип даних
MyStructз типомstruct, в якому є одне полеMyFieldз типомstring - до цієї структури “прив’язуємо” функцію
MyMethod()– через вказання ресивера (receiver MyStruct), завдяки чомуMyStructреалізує інтерфейсMyInterface - описуємо нашу “основному робочу” функцію
sayHello(), яка аргументом приймає інтерфейс і викликає методMyMethod(), який є в цьому інтерфейсі - створюємо інстанс нашого типу даних
MyStruct, якому в полеMyFieldзаписуємо значення “Hello, Interface!“ - і викликаємо нашу робочу функцію, передаючи аргументом цю структуру
Постарався відобразити зв’язки між всіма об’єктами, бо вони дуже не явні, вийшло щось таке:
- створюємо об’єкт
myObjз типомMyStruct - викликаємо
sayHello(), передаючи аргументомmyObj, який всередині функціїsayHello()стає змінноюg, яка пов’язується з нашим інтерфейсомMyInterface, який надає доступ до методуMyMethod() - в функції
sayHello()через викликg.MyMethod()ми звертаємось до інтерфейсуMyInterface, кажучи “мені потрібен твій метод MyMethod()“ - інтерфейс
MyInterface“бачить”, що всередині нього зараз схований об’єктmyObj(типуMyStruct), тому він перенаправляє цей виклик саме до методу цієї конкретної структури
Окей – тепер картина стає більш зрозумілою.
Окрім одного моменту – як саме інтерфейс “бачить”, що “в ньому” є об’єкт myObj з методом MyMethod()?
The interface’s “magic”: type iface struct
Для того, аби розібратись з цим – трохи зануримось в магію вказівників (pointers), а саме – створимо власну структуру, яка буде копіювати те, як в type MyInterface interface структуровані дані.
А потім через вказівники – подивимось на адреси і зміст даних.
“Трохи” перепишемо наш код:
package main
import (
"fmt"
"unsafe"
)
// define MyInterface interface
// (same as before)
type MyInterface interface {
MyMethod() string
}
// define MyStruct struct
// (same as before)
type MyStruct struct {
MyField string
}
// define MyMethod method with a POINTER receiver
// - before was func (p MyStruct) MyMethod() ... - by value
// - now is func (p *MyStruct) MyMethod() ... - by pointer
// This means the method operates on the original data.
func (p *MyStruct) MyMethod() string {
return "Executing " + p.MyField
}
// This is the helper struct to inspect an interface
// It represents the internal memory layout of an interface variable
// the 'tab' has a table with information about the interface's type and methods
//
// type iface struct {
// // pointer to the 'itab' struct, see below
// tab *itab
// // here will be a pointer to the 'myObj' struct
// data unsafe.Pointer
// }
//
// type itab struct {
// // pointer to the 'type MyInterface interface'
// inter *interfacetype
// // pointer to the 'type MyStruct struct'
// typ *rtype
// // in our case we have 1 method, thus '[N]uintptr' == [1]uintptr
// // and in the 'fun[0]' will be the address of the method 'MyMethod'
// fun [N]uintptr // will have '[1]uintptr', and
// }
type ifaceStruct struct {
// Pointer to type/method info table
tab unsafe.Pointer
// Pointer to the actual data
// in our case, here will be a pointer to the 'myObj' struct
data unsafe.Pointer
}
// HERE IS THE "MAGIC"
// We modify sayHello to inspect the `g` it receives.
//
// 'g' is a new, local variable of the 'MyInterface' type.
// When the function is called, `myObj` is assigned to `g`.
//
// Because 'g' is an interface, it internally consists of two pointers:
// 1. tab: A pointer to the "interface table" (itab) that links
// the interface type (MyInterface) to the concrete type (*MyStruct)
// and stores pointers to the methods that satisfy the interface
// 2. data: A pointer to the actual data. In our case, this will be
// the pointer we passed in (`myObj`).
func sayHello(g MyInterface) {
fmt.Println("Inside sayHello()")
// Get the address of `g` and cast it to our helper struct 'ifaceStruct'
// This line does three things in one go:
// 1. &g - takes the memory address of our interface variable `g`
// 2. unsafe.Pointer(&g) - casts that address to a raw, untyped pointer
// 3. (*ifaceStruct)(...) - re-interprets that raw pointer as a pointer to our helper struct
// As a result, `g_internal` is now a `*ifaceStruct` that points to
// the exact same memory location as `g`, letting us access its .tab and .data fields.
g_internal := (*ifaceStruct)(unsafe.Pointer(&g))
fmt.Printf("Internal 'Type' pointer (tab): %p\n", g_internal.tab)
fmt.Printf("Internal 'Data' pointer (data): %p\n", g_internal.data)
fmt.Println("Result:", g.MyMethod())
}
func main() {
// Create the object and get a pointer to it
// 'myObj' now holds a pointer to a MyStruct instance in memory
myObj := &MyStruct{MyField: "Hello, Interface!"}
// Print location of the 'myObj' struct
fmt.Println("Inside main()")
fmt.Printf("Address of the original 'myObj' in main(): %p\n", myObj)
// Pass the pointer to the function
// i.e. we pass an address of the 'myObj' struct location
sayHello(myObj)
}
Запускаємо:
$ go run interface-details.go Inside main() Address of the original 'myObj' in main(): 0xc000014070 Inside sayHello() Internal 'Type' pointer (tab): 0x4e5a28 Internal 'Data' pointer (data): 0xc000014070 Result: Executing Hello, Interface!
В коментах розписав все детально, але по суті коротко ми:
- у виклику
sayHello(myObj)до функціїsayHello()передаємо адресу “0xc000014070” – посилання на структуруMyStructз полемMyField, в яке записане значення “Hello, Interface!“ - функція
sayHello()приймає аргумент типу інтерфейс, і зміннаgмістить два вказівники –tab(на структуруitab, яка зберігає інформацію про тип і методи), таdata(на значення типуMyStruct)
Можна спробувати візуалізувати так:
MyInterface (variable 'g')
+----------------------------------------+
| tab => itab(MyInterface, *MyStruct)
| data => &MyStruct{"Hello, Interface!"}
+----------------------------------------+
А сама цікава магія відбувається під час компіляції програми і створення структури itab:
- Go перевіряє методи в коді, знаходить структуру
MyStructз методомMyMethod() - перевіряє інтерфейси, і знаходить
MyInterface, який вимагає методMyMethod() string - перевіряє, що
MyStruct.MyMethod()таMyInterface.MyMethod()збігаються - створює таблицю інтерфейсу (
itab– interface table), яка пов’язуєMyStructзMyInterfaceі зберігає адреси методів, що реалізують інтерфейс
І далі під час виконання програми під час виклику sayHello(myObj) Go створює нову змінну g типу iface, у якій ці два вказівники (tab та data) поєднуються:
- вказівник на
itab(яку компілятор створив для париMyStruct+MyInterface) буде поміщено вg.tab - вказівник на
myObj(тобто адреса типу “0xc000014070“) буде поміщено вg.data
В результаті в g.tab у нас буде структура itab – в полі fun[0] якої буде адреса функції MyMethod(), а в g.data – буде вказівник на екземпляр MyStruct з полем MyField.
І тоді при виклику:
...
fmt.Println("\nResult:", g.MyMethod())
...
Ми запускаємо:
... return "Executing " + *MyStruct.MyField ...
Наостанок – можна ще вивести і саму itab, аналогічно тому, як зробили для самого інтерфейсу, через створення власної структури type itabStruct struct:
package main
import (
"fmt"
"unsafe"
)
// define MyInterface interface
type MyInterface interface {
MyMethod() string
}
// define MyStruct struct
type MyStruct struct {
MyField string
}
// define MyMethod method with a POINTER receiver
func (p *MyStruct) MyMethod() string {
return "Executing " + p.MyField
}
// This helper represents the interface value itself (the 2-word struct)
type ifaceStruct struct {
// Pointer to the 'itab' (interface table)
tab unsafe.Pointer
// Pointer to the actual data (our *MyStruct)
data unsafe.Pointer
}
// NEW
// This helper represents the internal 'runtime.itab' struct
type itabStruct struct {
// inter: Pointer to the interface type's definition (MyInterface)
inter unsafe.Pointer
// typ: Pointer to the concrete type's definition (*MyStruct)
typ unsafe.Pointer
// hash: Hash of the concrete type, used for lookups
hash uint32
// _ [4]byte: Padding (on 64-bit systems)
_ [4]byte
// fun: The method dispatch table - an array of function pointers
// Each entry corresponds to a method defined in the interface
// Here we have one entry: the address of MyStruct.MyMethod()
fun [1]uintptr
}
// HERE IS THE "MAGIC"
func sayHello(g MyInterface) {
fmt.Println("--- Inside sayHello() ---")
// 1. Get the address of `g` and cast it to our helper struct
g_internal := (*ifaceStruct)(unsafe.Pointer(&g))
// Print the two main pointers
fmt.Printf("g.tab (pointer to itab): %p\n", g_internal.tab)
fmt.Printf("g.data (pointer to myObj): %p\n", g_internal.data)
// NEW - 2. DE-REFERENCE THE 'tab' POINTER
// Cast the 'tab' pointer to our itabStruct pointer
itab_ptr := (*itabStruct)(g_internal.tab)
// NEW - 3. PRINT THE CONTENTS OF THE 'itab'
fmt.Println("\n--- Inspecting the 'itab' (at address g.tab) ---")
fmt.Printf("itab.inter (ptr to MyInterface info): %p\n", itab_ptr.inter)
fmt.Printf("itab.typ (ptr to *MyStruct info): %p\n", itab_ptr.typ)
fmt.Printf("itab.hash (hash of *MyStruct type): %x\n", itab_ptr.hash)
// This is the final link!
// This is the actual memory address of the function to be called, i.e. the 'g.MyMethod()' in this case
fmt.Printf("itab.fun[0] (ADDRESS OF THE METHOD): 0x%x\n", itab_ptr.fun[0])
// 4. Call the method as usual
fmt.Println("\nResult:", g.MyMethod())
}
func main() {
// Create the object and get a pointer to it
myObj := &MyStruct{MyField: "Hello, Interface!"}
fmt.Println("--- Inside main() ---")
fmt.Printf("Address of original 'myObj' in main(): %p\n", myObj)
// Pass the pointer to the function - Go will create an 'iface' value
// linking the interface 'MyInterface' with the concrete type *MyStruct.
sayHello(myObj)
}
Результат:
$ go run interface-details-3.go --- Inside main() --- Address of original 'myObj' in main(): 0xc00019a020 --- Inside sayHello() --- g.tab (pointer to itab): 0x4e6c08 g.data (pointer to myObj): 0xc00019a020 --- Inspecting the 'itab' (at address g.tab) --- itab.inter (ptr to MyInterface info): 0x4a9d80 itab.typ (ptr to *MyStruct info): 0x4a86e0 itab.hash (hash of *MyStruct type): 1ac3179f itab.fun[0] (ADDRESS OF THE METHOD): 0x499c40 Result: Executing Hello, Interface!
Тобто, коли ми викликаємо g.MyMethod(), Go бере адресу функції з itab.fun[0] і викликає її, передаючи їй як аргумент вказівник з g.data – от і вся “магія” динамічного виклику методів через інтерфейс.
Ну і тепер можна використовувати інтерфейси, вже маючи уявлення про те, як саме вони працюють.
Корисні посилання
