"Obiektowość" w Go
Go umożliwia programowanie obiektowe. Co prawda nie ma klas, obiektów ani dziedziczenia jako takich, ale przy odpowiednim zastosowaniu konstrukcji które posiada, można osiągać równoważne rezultaty.
Z arsenału obiektowego programowania w Go dostępne są:
- structy, w których można gromadzić powiązane ze sobą dane
- metody typów
- interfejsy
- embedowanie (jako forma dziedziczenia, trochę dziwna...)
Czego nie ma:
- przełączników prywatności
- hierarchiczności (takiej wprost)
- konstruktorów
Ponieważ gdzie indziej rozpisuję się o structach i implementacji metod, tutaj przedstawię interfejsy i embedowanie.
W większości języków omawianie obiektowości zacząłbym od klas/obiektów, może
napisałbym o dziedziczeniu, ale w Go jest inaczej. Podstawą obiektowości Go
są typy (w szczególności: nazwane struct
y) i interfejsy.
Interfejsy
interface
to typ reprezentujący zestaw metod. To jedyny rodzaj typu
którego wartości nie da się utworzyć ani za pomocą literału ani przez new
.
Dowolna wartość, której typ implementuje wszystkie metody interfejsu, może być
przechowywana w zmiennej typu interfejsowego. Jego użyteczność polega na tym,
że wiele różnych typów może "spełniać" interface, innymi słowy: zapewnia
polimorfizm, którego nie daje embedowanie. Najprostsze wykorzystanie interfejsów:
type stringer interface {
string() string
}
func printMe(s stringer) {
fmt.Print(s.string())
}
W powyższym przykładzie zdefiniowaliśmy interface
stringer
, zawierający
metodę o string()
, nie przyjmującą żadnych argumentów za to zwracającą
wartość typu string
. Następnie zadeklarowaliśmy interfejs stringer
jako typ
argumentu funkcji printMe
, dzięki temu mogliśmy użyć metody string()
zmiennej s. Dla przykładu stwórzmy jakiś typ implementujący ten interface:
type stringerType int
func (s stringerType) string() string {
if s > 10 {
return "zmienna większa od 10"
}
return "zmienna nie większa niż 10"
}
Wykorzystanie stringerType
:
printMe(stringerType(10))
printMe(stringerType(100))
Warto pamiętać, że na zmiennej "interfejsowej", nie możemy wywołać innych metod, niż te zdefiniowane w interfejsie, poza asercją typów, nawet jeśli wartość posiada inne metody.
Szczególnym przypadkiem jest pusty interfejs:
interface{}
Każda wartość go spełnia, dzięki temu możemy pisać jeszcze bardziej "dynamiczny" kod, choć osobiście nie znajduję zbyt wiele zastosowań dla tego typu.
Asercja typów
To operacja pozwala wartość zmiennej typu interfejsowego przekształcić w wartość innego typu. Jak wiadomo jeśli zmienna ma typ interfejsowy, to można do niej przypisać wartość dowolnego typu, który spełnia ten interface. Dzięki asercji jesteśmy w stanie sprawdzić czy wartość kryjąca się pod zmienną implementuje jakiś inny interface lub spróbować przypisać wartość tej zmiennej do typu bazowego tej wartości. Piszę "sprawdzić" i "spróbować", bo informacja czy asercja do typu się udała czy nie jest zwracana z tego wyrażenia. Przykład:
type Reader interface {
Read() []byte
}
type ReadWriter interface {
Read() []byte
Write([]byte)
}
func smthStupid(r Reader) {
buf := r.Read()
rw, ok := r.(ReadWriter)//to tu! tak wykonujemy asercję
if ok {
rw.Write(buf)
}
}
Stworzyliśmy dwa interfejsy Reader
i ReadWriter
, pierwszy umożliwia wczytanie
wycinka danych, drugi dodatkowo umożliwia zapisanie wycinka danych. Następnie
W funkcji smthStupid
jako argument przyjmujemy zmienną której wartość
implementuje interfejs Reader (czyli typ tej wartości ma metodę Read() []byte
).
Następnie wczytujemy bufor danych i jeśli wartość ma także metodę Write([]byte)
,
czyli spełnia interfejs ReadWriter
to ponownie zapisujemy w nim ten bufor.
Zatem asercję wykonujemy: umieszczając nazwę zmiennej, po niej kropkę i w nawiasach typ którego się spodziewamy, zwrotnie dostajemy wartość typu ujętego w nawiasy oraz wartość logiczną, mówiącą czy asercja się powiodła czy nie.
Co zostanie przypisane do zmiennej rw
jeśli ok
jest false. a zatem asercja
nie powiodła się? Otóż wartość zerową typu (dla przypomnienia: dla wskaźników,
typów referencyjnych i interfejsów to nil
)
Nie można wykonywać asercji na zmiennych o konkretnych typach - nieinterfejsowych - bo taka operacja nie ma większego sensu. Nie możemy próbować wyłuskiwać typu "konkretnego" ze zmiennej innego typu "konkretnego", bo dane pod nimi skrywane są przechowywane w odmienny sposób, albo można użyć konwersji typów. Z drugiej strony próba otrzymania wartości typu interfejsowego z wartości konkretnej jest bezcelowa, bo nie ma takiej możliwości by wartość zmiennej nie implementowała interfejsu jeśli zadbaliśmy o to by jej typ miał określone metody.
A w skrócie: nie mają sensu próby wyciągnięcia pomarańczy ze skrzynki jabłek, ale ma sens próba wyciągnięcia pomarańczy z kosza owoców :-)
type Writer interface {
Write([]byte)
}
Embedowanie czyli osadzanie struktur
Deklarując strukturę zazwyczaj podajemy identyfikator pola i jego typ. Możemy jednak pominąć identyfikator pola i utworzy się tak zwane anonimowe pole. Nie jest ono tak całkowicie anonimowe, możemy się do niego odwołać po nazwie typu. Dodatkowo jeśli anonimowe pole jest typem struktury to do pola typu osadzonego możemy odwoływać się bezpośrednio na wartości typu w którym jest osadzone
I tak w linii //1 mamy przykład na to, że pole struktury typu point
jest
dostępne bezpośrednio z wartości typu namedPoint
, W linii //2 odwołujemy się
do pól x i y podając nazwę typu osadzonego w którym zostały wcześniej
zadeklarowane.
Okazuje się to być niejednoznaczne w kilku przypadkach:
Gdy osadzamy typ i jego typ wskaźnikowy. Mimo iż co do zasady to różne typy to taka operacja jest nielegalna.
Tak samo nielegalne jest osadzanie typów o takich samych nazwach z różnych pakietów
Za to dozwolone jest użycie konfliktującej nazwy pola w strukturze osadzającej:
type uintXPoint {
point
x uint
}
Takie postępowanie prowadzi do nieporozumień. Może się wydawać, że nowe pole x
nadpisuje pole x
struktury point
, ale to nie prawda.
Nie tylko pola struktur osadzanych są dostępne w strukturze osadzającej, także metody. To czasem miłe bo to oznacza, że typ embedujący spełnia z automatu wszystkie interfejsy, które spełniał typ embedowany.
Konstruktory
We wstępie napisałem, że w Go nie ma konstruktorów, jest jednak konwencja pisania funkcji konstruujących nowe wartości danego typu. Sprowadza się ona do nazywania takich funkcji rozpoczynając od słówka new,
type point struct {
x, y int
}
func newPoint(x,y int) *point {
return &point{x,y}
}
oraz stosowania literałów. Literały są czytelniejsze a przy okazji mniej kodu trzeba napisać by stworzyć nową wartość.