Any nie jest Any, ale Any jest Any

Any
W języku Go typ any jest aliasem interface{}, magicznego interfejsu, który może przechowywać wartości dowolnego typu. Innymi słowy, do zmiennej typu any można przypisać wartość dowolnego typu.
1var a any
2a = 42 // int
3a = "hello" // string
4a = true // bool
5a = 3.14 // float64
Jak widać, typ any pozwala przechowywać wartości różnych typów, co umożliwia pisanie elastycznego kodu. Jednak podczas korzystania z typu any należy zwrócić uwagę na kilka kwestii.
Type Assertion
Aby uzyskać rzeczywistą wartość ze zmiennej typu any, należy skorzystać z asercji typu (type assertion). Asercja typu informuje kompilator, że zmienna typu any jest określonego typu.
1var a any = "hello"
2str, ok := a.(string)
3if ok {
4 fmt.Println("String value:", str)
5} else {
6 fmt.Println("Not a string")
7}
W powyższym przykładzie a.(string) potwierdza, że a jest typu string. Jeśli a nie jest typu string, wartość ok wynosi false, co pozwala uniknąć wpadnięcia programu w panikę.
Type Switch
Instrukcja przełącznika typu (type switch) pozwala wykonywać różne działania w zależności od rzeczywistego typu zmiennej typu any. Jest to przydatne, gdy należy obsłużyć wiele typów.
1var a any = 42
2switch v := a.(type) {
3case int:
4 fmt.Println("Integer:", v)
5case string:
6 fmt.Println("String:", v)
7case bool:
8 fmt.Println("Boolean:", v)
9default:
10 fmt.Println("Unknown type")
11}
W powyższym przykładzie a.(type) sprawdza rzeczywisty typ a i wykonuje przypadek odpowiadający temu typowi.
Reflection
Korzystając z pakietu reflect języka Go, można dynamicznie sprawdzać i modyfikować typ oraz wartość zmiennej typu any w czasie działania programu. Jest to przydatne przy pracy ze złożonymi strukturami danych.
1import (
2 "fmt"
3 "reflect"
4)
5
6var a any = 3.14
7v := reflect.ValueOf(a)
8fmt.Println("Type:", v.Type())
9fmt.Println("Value:", v.Float())
W powyższym przykładzie pakiet reflect służy do sprawdzenia typu i wartości a w czasie działania programu.
EFace
W języku Go interfejsy dzielą się na dwie główne formy: EFace (Empty Interface) oraz IFace (Interface with Methods). Spośród nich EFace jest pojęciem tożsamym z typem any, o którym mówiliśmy dotychczas.
Struktura
EFace jest specjalną strukturą istniejącą wyłącznie wewnątrz środowiska uruchomieniowego (runtime) Go, dlatego tutaj posłużymy się imitacją (mimic).
1type runtimeTypeMimic struct {
2 Size_ uintptr
3 PtrBytes uintptr // number of (prefix) bytes in the type that can contain pointers
4 Hash uint32 // hash of type; avoids computation in hash tables
5 TFlag uint8 // extra type information flags
6 Align_ uint8 // alignment of variable with this type
7 FieldAlign_ uint8 // alignment of struct field with this type
8 Kind_ uint8 // enumeration for C
9 Equal func(unsafe.Pointer, unsafe.Pointer) bool
10 GCData *byte
11 Str int32 // string form
12 PtrToThis int32 // type for pointer to this type, may be zero
13}
14
15type eFaceMimic struct {
16 Type *runtimeTypeMimic
17 Data unsafe.Pointer
18}
EFace posiada dwa pola:
Type: wskaźnik zawierający metadane o rzeczywistym typie wartości.Data: wskaźnik zawierający rzeczywistą wartość.
Ponadto EFace wewnętrznie zarządza informacjami o typie za pomocą struktury runtimeType. Struktura ta zawiera różne metadane, takie jak rozmiar typu, wyrównanie oraz wartość skrótu (hash).
Na co warto zwrócić szczególną uwagę w tej strukturze, to pola Kind_, Hash oraz Equal. Kind_ określa rodzaj typu, natomiast Hash i Equal są wykorzystywane do obliczania wartości skrótu oraz porównywania typów w tablicach haszujących.
Opakowanie
Teraz owińmy EFace w wrapper, aby ułatwić sobie korzystanie z czarnej magii.
1type TypeInfo struct {
2 Hash uint32
3 TFlag uint8
4 Kind uint8
5 Equal func(unsafe.Pointer, unsafe.Pointer) bool
6 This unsafe.Pointer
7}
8
9func GetTypeInfo(v any) TypeInfo {
10 eface := *(*eFaceMimic)(unsafe.Pointer(&v))
11 return TypeInfo{
12 Hash: eface.Type.Hash,
13 TFlag: eface.Type.TFlag,
14 Kind: eface.Type.Kind_,
15 Equal: eface.Type.Equal,
16 This: eface.Data,
17 }
18}
Powyższa funkcja przyjmuje zmienną typu any, wyodrębnia wewnętrzną strukturę EFace, pobiera z niej informacje o typie i zwraca je jako strukturę TypeInfo. Dzięki temu można łatwo uzyskać dostęp do wartości skrótu typu, jego rodzaju, funkcji porównującej itd.
Zastosowanie
Użycie jest bardzo proste. Wystarczy przekazać zmienną typu any do funkcji GetTypeInfo.
1func main() {
2 i := 42
3 eface := GetTypeInfo(i)
4 fmt.Printf("%+v\n", eface)
5
6 f := 3.14
7 eface2 := GetTypeInfo(f)
8 fmt.Printf("%+v\n", eface2)
9
10 // Compare the two values using the Equal function from the type info
11 log.Println(eface.Equal(eface.This, eface.This))
12 log.Println(eface2.Equal(eface2.This, eface2.This))
13 log.Println(eface2.Equal(eface.This, eface2.This))
14 log.Println(eface.Equal(eface2.This, eface.This))
15}
W powyższym przykładzie wartości całkowitą i zmiennoprzecinkową przekazujemy do funkcji GetTypeInfo, aby wypisać informacje o ich typach, a następnie za pomocą funkcji Equal porównujemy wartości. Dzięki temu można szybciej uzyskać informacje o typie oraz porównywać typy.
Ta metoda może być również stosowana w przypadku struktur, wskaźników, interfejsów itp., a także pozwala rozwiązać częsty problem typed nil. Pole Hash jest przypisane, jeśli typ istnieje, natomiast This ma wartość 0 (nil), gdy wskaźnik jest pusty.
Podsumowanie
EFace odgrywa istotną rolę w obsłudze typu any w języku Go obecnie. Pozwala to elastycznie i szybko przetwarzać wartości różnych typów oraz szczegółowo uzyskiwać i wykorzystywać informacje o typach. Należy jednak pamiętać, że jest to funkcjonalność ukryta w środowisku uruchomieniowym, a więc może ulec zmianie wraz ze zmianami specyfikacji języka.