В предыдущих уроках в определениях функций и структур использовались интерфейсы. Но бывают ситуации, когда не всегда можно указать конкретные типы переменных. Например, если получаем JSON-документ от пользователя, то структура этого документа может быть любой.
Разрешить функции принимать произвольный параметр можно с помощью пустого интерфейса — интерфейса без методов.
Действительно, так как интерфейс является набором требований к типу, то пустой интерфейс ничего от типа не требует. Он может быть чем угодно. Однако и использовать переменную типа пустого интерфейса невозможно.
func PassAnyType(v interface{}) {
// ...
}
Note
В Go 1.18 у типа
interface{}
появился более короткий и понятный псевдонимany
(от англ. any — любой).
Но с переменной v
нельзя ничего сделать. Нужно привести её к определённому типу оператором v.(Тип)
.
Оператор приведения типа приводит переменную интерфейсного типа к конкретному типу или другому интерфейсу.
func PassAnyType(v interface{}) {
i := v.(int) // если v не число, то будет паника, то есть программа не сможет работать и прекратит выполнение
i, ok := v.(int) // альтернативный формат: если v не число, то будет false
// паники не будет
if ok {
// ...
}
}
Второй вариант использования более предпочтителен.
Оператором switch
можно лаконично запрограммировать логику относительно каждой проверки типа. Для примера напишем свою реализацию функции fmt.Printf
:
func Printf(v interface{}) {
switch v2 := v.(type) {
case int:
fmt.Print("Это число " + strconv.FormatInt(v2, 10))
case string:
fmt.Print("Это строка " + v2)
case Stringer:
fmt.Print("Это тип, реализующий Stringer, " + v2.String())
default:
fmt.Print("Неизвестный тип")
}
}
Пример: нужно реализовать обобщение операции умножения для чисел и строк. Если первый аргумент функции — строка, то повторить её b
раз, а если число, то вернуть a*b
.
func Mul(a interface{}, b int) interface{} {
switch va := a.(type) {
case int:
return va * b
case string:
return strings.Repeat(va, b)
case fmt.Stringer:
return strings.Repeat(va.String(), b)
default :
return nil
}
}
Внутреннее устройство интерфейсов
Чтобы понять, каким образом интерфейсы приводятся к другим типам, следует разобраться, как устроен интерфейс изнутри.
Соединим примеры из предыдущего и текущего уроков:
type User struct {
Email string
Password string
LastAccess time.Time
}
func (u User) String() string {
return "user with email " + u.Email
}
func Printf(v Stringer) {
fmt.Print("Это тип, реализующий Stringer, " + v.String())
}
func main() {
u := User{Email: "example@yandex.ru"}
Printf(u)
}
Вот какие данные будут храниться в переменной v Stringer
:
То, что изображено на схеме, можно посмотреть подробнее в src/runtime/runtime2.go.
На схеме видно, что интерфейс состоит из двух указателей: на метаданные типа и на сами данные. При приведении типа используются эти метаданные, чтобы вычислить, какой конкретный тип представляет этот интерфейс и как правильно разыменовать указатель на данные.
Почему так устроено? Всё дело в том, что переменной интерфейсного типа могут быть присвоены данные разного размера. Например, интерфейсу Stringer
может удовлетворять и большая сложная структура, и пользовательский int
. Поэтому сохраняем данные по указателю. А вот в itable
храним метаинформацию о типе, который там содержится.
Когда присваиваем переменную конкретного типа, происходит следующее:
// Компилятор создаёт в программе метаданные со списком методов интерфейса
type Stringer interface {
String() string
}
// Компилятор создаёт в памяти структуру с описанием типа User, его полей
type User struct {
}
var user Stringer // Объявляется переменная интерфейса — её значение пока что равно nil, в памяти выделяется два машинных слова под указатели tab и data
// Переменной присваивается конкретное значение. Проверяется, удовлетворяет ли тип переменной интерфейсу.
// При этом в tab записывается указатель на структуру itable, связывающий информацию о типе User и Stringer,
// а в data — указатель на User{}
user := User{}
// Интересно, что связывание типа и интерфейса происходит не на этапе компиляции, иначе размер программы был бы слишком большим, а на этапе выполнения. Эта операция кешируется, так что выполняется всего один раз и не влияет на производительность.
// Проверяем, что в tab у нас лежит действительно тип User — и если да, то ok true, а в переменную u копируется значение data.
// Если типы не совпадают, то ok — false
u, ok := user.(User)
Note
Интерфейсная переменная по природе относится к ссылочному типу. Если передать её в функцию, то она скопируется, но, так как указатели будут указывать на ту же исходную переменную, изменение переменной через вызов методов может изменить данные.
В Go есть особенность, связанная с nil
. Посмотрите ещё раз на схему: интерфейс может быть nil
, а может быть с nil
-указателем на данные. Покажем пример, где эта особенность приводит к ошибке:
// Собственный тип для ошибок. Аналогичен стандартному error
type Error interface {
Error() string
}
// MyError — структура, реализующая нашу ошибку
type MyError struct {
// ...
}
// Error — метод для удовлетворения интерфейсу Error
func (e *MyError) Error() string {
return "..."
}
// переменная типа ошибки — указатель на пустую структуру
var ErrFriday13 = &MyError{}
func CheckTodayIsOkay() Error {
var err *MyError // указатель на переменную типа Error
// получаем текущее время
t := time.Now()
// если день недели пятница и число месяца 13, то вернём ошибку
if t.Weekday() == time.Friday && t.Day() == 13 {
err = ErrFriday13
}
// вернём ошибку ... указатель же nil
return err
}
func main() {
err := CheckTodayIsOkay()
// проверяем err на nil — и внезапно всегда не nil
if err != nil {
fmt.Println("error is not nil")
return
}
fmt.Println("error is nil")
}
Код выведет строчку "error is not nil"
, потому что Go обернёт nil
-указатель *MyError
в не-nil
-интерфейс Error
.
То есть мы присвоили переменной типа interface{}
переменную определённого типа. Это значит, что метаданные непустые. Исправить проблему можно так:
func CheckTodayIsOkay() Error {
var err Error
t := time.Now()
if t.Weekday() == time.Friday && t.Day() == 13 {
err = ErrFriday13
}
return err
}
В этом случае код выведет строчку "error is nil"
, потому что сам интерфейс Error
будет nil
.
Сравнение интерфейсов
Сравнение интерфейсных типов имеет одну особенность, которую важно знать, так как она может привести к неочевидным ошибкам. Рассмотрим следующий пример:
type Stringer interface {
String string
}
// создадим свой собственный тип на основе map
type MyMap map[string]string
func (m MyMap) String () string {
return fmt.Sprintf("%v", m)
}
// И ещё один тип на основе int
type MyInt int
func (m MyInt) String () string {
return fmt.Sprintf("%v", m)
}
func main () {
var mm MyMap
var mi MyInt
mm = MyMap{}
mi = MyInt(5)
fmt.Println(mm == mi) // false
fmt.Println(mm == mm) // Паника!
}
На первый взгляд, в программе творится нечто странное: с одной стороны, мы сравнили без ошибок мапу и целое, с другой — получили ошибку при сравнении переменной с самой собой.
Всё дело в том, что интерфейсы сравниваются по цепочке. Сначала сравнивается тип: если типы переменных внутри разные, то вернётся false; если одинаковые, то сравниваются уже сами данные. А map
сравнивать между собой нельзя, и получаем панику.
Ключевые мысли
- Интерфейсные переменные по своей природе динамичны. Их можно преобразовать к переменным другого типа через операцию приведения типа
a.(type)
. - Интерфейс изнутри представляет собой два указателя — на обёрнутую переменную и на информацию о её типе. Преобразование типа изменяет информацию о типе либо возвращает обёрнутую переменную.
- Операция приведения типа — это не то же самое, что преобразование типа.
- Интерфейс, равный
nil
, и интерфейс, оборачивающийnil
, — разные вещи. - Сравнение интерфейсов необходимо делать аккуратно.
- Наилучшим вариантом использования интерфейса является всё же использование не пустого, а конкретного интерфейса и обращение к его методам. Однако иногда без них не обойтись.
📂 Go | Последнее изменение: 28.08.2024 21:02