Пустой интерфейс может использоваться для хранения любых данных, и он может быть полезным параметром, поскольку может работать с любым типом. Чтобы понять, как работает пустой интерфейс и как он может содержать любой тип, мы должны сначала понять концепцию, лежащую в его основе.
Пустой интерфейс
Вот хорошее определение пустого интерфейса:
Интерфейс - это два компонента: это набор методов и тип.
Тип interface{} - это интерфейс, не имеющий методов. Поскольку в Go нет ключевого слова implements (реализует), все типы реализуют по крайней мере ноль методов, и удовлетворение интерфейса выполняется автоматически, все типы удовлетворяют пустой интерфейс. Следовательно, метод с пустым интерфейсом в качестве аргумента может принимать любой тип. Go перейдет к преобразованию в тип интерфейса, который будет выполнять эту функцию.
Расс Кокс, один из ведущих разработчиков Go, написал статью о внутреннем представлении интерфейсов и объяснил, что интерфейс состоит из двух слов:
- указатель на информацию о сохраненном типе
- указатель на связанные данные
Эта было в 2009 году, когда среда выполнения была написана на C.
Среда выполнения теперь написана на Go, но представление осталось прежним. Мы можем убедиться в этом, распечатав пустой интерфейс:
package main
func main() {
var myVar int32 = 1
printInterface(myVar)
}
//go:noinline
func printInterface(val interface{}) {
println(val)
}
Вывод:
(0x459dc0,0xc00003476c)
Оба адреса представляют собой два указателя на тип информации и значение.
Базовая структура
Базовое представление пустого интерфейса задокументировано в пакете reflect:
type emptyInterface struct {
typ *rtype // слово 1 с описанием типа
word unsafe.Pointer // слово 2 со значением
}
Как объяснялось ранее, мы ясно видим, что пустой интерфейс имеет слово описания типа, за которым следует слово, содержащее данные.
Структура rtype содержит основу описания типа:
type rtype struct {
size uintptr
ptrdata uintptr // количество байтов в типе, которое может содержать указатели
hash uint32 // хэш типа; избегает вычислений в хэш-таблицах
tflag tflag // флаги дополнительной информации типа
align uint8 // выравнивание переменной с этим типом
fieldAlign uint8 // выравнивание поля структуры с этим типом
kind uint8 // перечисление для C
// функция для сравнения объектов этого типа
// (ptr to object A, ptr to object B) -> ==?
equal func(unsafe.Pointer, unsafe.Pointer) bool
gcdata *byte // данные сборки мусора
str nameOff // строковая форма
ptrToThis typeOff // тип для указателя на этот тип, может быть нулевым
}
Среди этих полей некоторые довольно простые и хорошо известные:
- size - это размер в байтах
- kind содержит тип: int8, int16, bool и т. д.
- align - это выравнивание переменной с этим типом
В зависимости от типа, встроенного в пустой интерфейс, мы можем сопоставить экспортируемые поля или перечислить методы:
// structType представляет struct тип.
type structType struct {
rtype
pkgPath name
fields []structField
}
В типе структуры есть еще два поля, включая список полей структуры. Это ясно показывает, что преобразование встроенного типа (rtype) в пустой интерфейс приведет к плоскому преобразованию, при котором описание поля и его значение будут сохранены в памяти.
Давайте теперь посмотрим, какие виды конверсии действительно возможны из пустого интерфейса.
Конверсии
Давайте попробуем простую программу, которая использует пустой интерфейс с неправильным преобразованием:
package main
func main() {
var myVar int32 = 1
printInterface(myVar)
}
//go:noinline
func printInterface(val interface{}) {
n := val.(int64)
println(n)
}
Хотя преобразование из int32 в int64 допустимо, программа запаникует:
panic: interface conversion: interface {} is int32, not int64
goroutine 1 [running]:
main.printInterface(0xe49f20, 0xc00002ff74)
/home/myuser/main.go:10 +0x85
main.main()
/home/myuser/main.go:5 +0x46
Давайте сгенерируем asm-код, чтобы увидеть, какая проверка выполняется Go:
...
0x0024 00036 (/home/myuser/main.go:10) LEAQ type.int64(SB), AX // Шаг 1
0x002b 00043 (/home/myuser/main.go:10) MOVQ "".val+48(SP), CX
0x0030 00048 (/home/myuser/main.go:10) CMPQ AX, CX
0x0033 00051 (/home/myuser/main.go:10) JNE 105 // Шаг 2
0x0035 00053 (/home/myuser/main.go:10) MOVQ "".val+56(SP), AX
0x003a 00058 (/home/myuser/main.go:10) MOVQ (AX), AX
0x003d 00061 (/home/myuser/main.go:10) MOVQ AX, "".n+24(SP)
0x0042 00066 (/home/myuser/main.go:11) PCDATA $1, $1
0x0042 00066 (/home/myuser/main.go:11) CALL runtime.printlock(SB)
0x0047 00071 (/home/myuser/main.go:11) MOVQ "".n+24(SP), AX
0x004c 00076 (/home/myuser/main.go:11) MOVQ AX, (SP)
0x0050 00080 (/home/myuser/main.go:11) CALL runtime.printint(SB)
0x0055 00085 (/home/myuser/main.go:11) CALL runtime.printnl(SB)
0x005a 00090 (/home/myuser/main.go:11) CALL runtime.printunlock(SB)
0x005f 00095 (/home/myuser/main.go:12) MOVQ 32(SP), BP
0x0064 00100 (/home/myuser/main.go:12) ADDQ $40, SP
0x0068 00104 (/home/myuser/main.go:12) RET
0x0069 00105 (/home/myuser/main.go:10) MOVQ CX, (SP)
0x006d 00109 (/home/myuser/main.go:10) MOVQ AX, 8(SP)
0x0072 00114 (/home/myuser/main.go:10) LEAQ type.interface {}(SB), AX
0x0079 00121 (/home/myuser/main.go:10) MOVQ AX, 16(SP)
0x007e 00126 (/home/myuser/main.go:10) NOP
0x0080 00128 (/home/myuser/main.go:10) CALL runtime.panicdottypeE(SB) // Шаг 3
0x0085 00133 (/home/myuser/main.go:10) XCHGL AX, AX // Шаг 4
0x0086 00134 (/home/myuser/main.go:10) NOP
0x0086 00134 (/home/myuser/main.go:9) PCDATA $1, $-1
0x0086 00134 (/home/myuser/main.go:9) PCDATA $0, $-2
0x0086 00134 (/home/myuser/main.go:9) CALL runtime.morestack_noctxt(SB)
0x008b 00139 (/home/myuser/main.go:9) PCDATA $0, $-1
0x008b 00139 (/home/myuser/main.go:9) JMP 0
...
Вот несколько шагов:
- Шаг 1: сравнить (инструкция CMPQ) тип int64 (загруженный инструкцией LEAQ, Load Effective Address) с внутренним типом пустого интерфейса (инструкция MOVQ, которая считывает память со смещением 48 байтов из сегмента памяти пустого интерфейса)
- Шаг 2: инструкция JNE "Перейти, если не равно" перейдет к сгенерированным инструкциям, которые будут обрабатывать ошибку на шаге 3.
- Шаг 3: код запаникует и выдаст сообщение об ошибке, которое мы видели ранее
- Шаг 4: это конец инструкций по ошибке. На эту конкретную инструкцию ссылается сообщение об ошибке, которое показывает инструкцию: main.go: 10 + 0x85
Любое преобразование из внутреннего типа пустого интерфейса должно выполняться после преобразования исходного типа. Это преобразование в пустой интерфейс, а затем обратно в исходный тип, требует затрат для вашей программы. Давайте проведем несколько тестов, чтобы получить общее представление об этом.
Производительность
Вот два теста (Golang 1.16.2). Один с копией структуры, а другой с использованием пустого интерфейса:
package maintest
import (
"testing"
)
var instance StructWithFields
type StructWithFields struct {
field1 int
field2 string
field3 float32
field4 float64
field5 int32
field6 bool
field7 uint64
field8 *string
field9 uint16
}
//go:noinline
func emptyInterface(i interface {}) {
str := i.(StructWithFields)
instance = str
}
//go:noinline
func withType(str StructWithFields) {
instance = str
}
func BenchmarkWithType(b *testing.B) {
str := StructWithFields{field1: 1, field2: "string", field8: new(string)}
for i := 0; i < b.N; i++ {
withType(str)
}
}
func BenchmarkWithEmptyInterface(b *testing.B) {
str := StructWithFields{field1: 1, field2: "string", field8: new(string)}
for i := 0; i < b.N; i++ {
emptyInterface(str)
}
}
Вот результаты:
BenchmarkWithType-4 74843296 16.75 ns/op
BenchmarkWithEmptyInterface-4 48790801 33.27 ns/op
Для двойного преобразования типа в пустой интерфейс, а затем обратно в тип требуется на 16 наносекунд больше, чем копирование структуры.
Хорошим решением было бы использовать указатель и преобразовать обратно в тот же указатель на структуру.
package maintest
import (
"testing"
)
var instance *StructWithFields
type StructWithFields struct {
field1 int
field2 string
field3 float32
field4 float64
field5 int32
field6 bool
field7 uint64
field8 *string
field9 uint16
}
//go:noinline
func emptyInterface(i interface {}) {
str := i.(*StructWithFields)
instance = str
}
//go:noinline
func withType(str *StructWithFields) {
instance = str
}
func BenchmarkWithType(b *testing.B) {
str := StructWithFields{field1: 1, field2: "string", field8: new(string)}
for i := 0; i < b.N; i++ {
withType(&str)
}
}
func BenchmarkWithEmptyInterface(b *testing.B) {
str := StructWithFields{field1: 1, field2: "string", field8: new(string)}
for i := 0; i < b.N; i++ {
emptyInterface(&str)
}
}
Результаты теперь совсем другие:
BenchmarkWithType-4 187950846 6.374 ns/op
BenchmarkWithEmptyInterface-4 150776062 8.141 ns/op
Что касается базового типа, такого как int или string, производительность немного отличается:
package maintest
import (
"testing"
)
var intVar int
var stringVar string
//go:noinline
func emptyInterfaceInt(i interface {}) {
str := i.(int)
intVar = str
}
//go:noinline
func withTypeInt(str int) {
intVar = str
}
//go:noinline
func emptyInterfaceString(i interface {}) {
str := i.(string)
stringVar = str
}
//go:noinline
func withTypeString(str string) {
stringVar = str
}
func BenchmarkWithTypeInt(b *testing.B) {
str := 123
for i := 0; i < b.N; i++ {
withTypeInt(str)
}
}
func BenchmarkWithEmptyInterfaceInt(b *testing.B) {
str := 123
for i := 0; i < b.N; i++ {
emptyInterfaceInt(str)
}
}
func BenchmarkWithTypeString(b *testing.B) {
str := "123"
for i := 0; i < b.N; i++ {
withTypeString(str)
}
}
func BenchmarkWithEmptyInterfaceString(b *testing.B) {
str := "123"
for i := 0; i < b.N; i++ {
emptyInterfaceString(str)
}
}
BenchmarkWithTypeInt-4 228122656 5.319 ns/op
BenchmarkWithEmptyInterfaceInt-4 144611329 8.028 ns/op
BenchmarkWithTypeString-4 158939217 7.508 ns/op
BenchmarkWithEmptyInterfaceString-4 100000000 10.10 ns/op
Пустой интерфейс в большинстве случаев может быть полезен в ваших приложениях, при правильном использовании и с осторожностью, учитывая его влияние на производительность.
Читайте также:
- Пакет net/http, краткий обзор
- Разработка RESTful API с помощью Go и Gin
- Функции пакета net/http, примеры
Комментариев нет:
Отправить комментарий