Diversity w polskim IT
Jacob Bennett
Jacob Bennett

Pisz w języku Go jak senior!

Co chciałbym wiedzieć, gdy zaczynałem pisać w Go (Golangu)
8.01.20244 min
Pisz w języku Go jak senior!

Swoją zawodową przygodę z Go zaczynałem w 2018 roku. Oto, co chciałbym sobie z tamtego czasu.

Go to przekazywanie wartości

Go to wyłącznie przekazywanie wartości. Oznacza to, że każda funkcja otrzymuje kopię wartości tego, co przekazałeś. Bez wyjątków.

Tak jakby.

Możesz również przekazać wskaźniki (technicznie przekazujesz wartość wskaźnika - adres). A dzięki silnemu typowaniu Go otrzymasz sprawdzanie typu na wskaźniku bazowym.

W tym przykładzie przekazujemy strukturę rect przez wartość. Struktura przechodzi do funkcji i wszystkie operacje modyfikują wartość, która została przekazana, ale oryginalny obiekt pozostaje niezmieniony, ponieważ nie został przekazany przez referencję.

Ta funkcja nie robi tego, co chcemy, aby zrobiła. Obiekt nie został zaktualizowany.

Ale możemy użyć wskaźnika do rect jako argumentu do funkcji DoubleHeight i skutecznie przekazać rect przez referencję.

Używaj (ale nie nadużywaj) wskaźników

Istnieją 2 operatory związane ze wskaźnikami: * i &.

Operator *

Operator * nazywany jest operatorem „wskaźnika”. Typ *Rectangle jest wskaźnikiem do instancji Rectangle. Zadeklarowanie argumentu typu *Rectangle oznacza, że funkcja przyjmuje „wskaźnik do instancji Rectangle”.

Wartością zerową wskaźnika jest nil. Super przydatne w wielu przypadkach! Ale często doprowadzi to do wywołania panic, gdy spróbujesz wywołać funkcje na wskaźniku nil. Okropne do debugowania (więcej na ten temat później).

Operator &

Operator & (zwany operatorem „adresu”) generuje wskaźnik do argumentu operacji.

Aby uzyskać wartość bazową wskaźnika, użyj operatora *. Nazywa się to „dereferencją wskaźnika”

Jeśli kiedykolwiek zastanawiasz się „czy powinienem użyć tutaj wskaźnika?”, odpowiedź to prawdopodobnie „tak”. Jeśli masz wątpliwości, użyj wskaźnika.

Błędy deferencji wskaźnika nil

Zmora mojej egzystencji.

Czasami, gdy używasz wskaźników, otrzymasz panic, który całkowicie zamyka twój program:

Było to spowodowane tym, że próbowałeś wyłuskać wskaźnik, którym był nil.

Naprawienie tego jest dość proste. Możesz sprawdzić wartości wskaźników, zanim wywołasz na nich metody.

Takie błędy często są bardzo trudne do rozwiązania i musiałem na nie polować wiele godzin. Jeśli zdecydujesz się użyć wskaźników, zawsze, ale to zawsze sprawdzaj, czy nie mają wartości nil!

Deklaruj interfejsy tam, gdzie ich używasz

No dobra. Wskaźniki są dość fundamentalne w Go. I nie są one wyjątkowe dla Go. Ale interfejsy naprawdę oszukują inżynierów JS lub Pythona, gdy zaczynają z Go.

Interfejsy w Go są jak kontrakt, który określa zestaw metod, które typ musi zaimplementować, aby spełnić warunki kontraktu. Na przykład, jeśli masz interfejs o nazwie Reader, który definiuje metodę Read(), każdy typ, który implementuje metodę Read(), jest uważany za implementujący interfejs Reader i może być używany wszędzie tam, gdzie oczekuje się Reader. 

Jedną z fajnych rzeczy w interfejsach w Go jest to, że typ może implementować wiele interfejsów. Przykładowo, jeśli masz typ o nazwie File, który implementuje interfejsy Reader i Writer, możesz użyć File wszędzie tam, gdzie potrzebujesz Reader lub Writer, a nawet wszędzie tam, gdzie potrzebujesz czegoś, co implementuje oba interfejsy. Dzięki temu łatwo jest napisać kod, który jest elastyczny i wielokrotnego użytku.

Inną fajną rzeczą w interfejsach w Go jest to, że możesz ich użyć do zdefiniowania generycznych algorytmów lub funkcji, które działają z wieloma typami. Na przykład możesz napisać funkcję sortowania, która przyjmuje sort.Interface jako dane wejściowe i może sortować dowolny typ, który implementuje wymagane metody.

Dzięki temu łatwo jest napisać kod, który jest bardzo elastyczny i możliwy do dostosowania. Możesz utworzyć niestandardowy typ (np. UserList lub PostFeed) i zdefiniować te metody i użyć ich ze standardową biblioteką.

Tajna broń

Nie jestem pewien, dlaczego Go nie robi tego OOTB. Ale jest wiele rzeczy, których nie robi OOTB (np. obsługa błędów). Mogę tylko przypuszczać, że jest to celowy zabieg.

Problem jest taki:

Jak można zagwarantować, że struktura jest zgodna ze wszystkimi metodami interfejsu?

Ta ostatnia linia zapewnia, że Implementation spełnia wszystkie metody SomeInterface i nie skompiluje się, jeśli SomeInterface doda metody, których Implementation nie spełnia.

Preferuj testy oparte na tabeli ale nie przesadzaj z ich ilością

Kiedy testujesz funkcję, tak naprawdę zmieniasz tylko to, jakie są dane wejścia i oczekiwane dane wyjściowe. Go pozwala zrobić to w naprawdę prosty sposób, używając testów opartych na tabeli.

Zacznijmy od kodu, który chcemy przetestować. Trochę tu bałaganu, ale uruchamia krytyczną funkcję w naszym systemie, więc nie chcemy tego dotykać, nie wiedząc, że robi dokładnie to, czego oczekujemy.

Prawdopodobnie chcemy przetestować szeroki zakres danych wejściowych, aby upewnić się, że otrzymujemy odpowiednie dane wyjściowe. Zamiast pisać dla tego indywidualne testy, użyj testów opartych na tabeli, aby osiągnąć ten sam wynik z czystszym, łatwiejszym do utrzymania wynikiem.

Te przypadki testowe naprawdę miło się czyta!

Kiedy należy unikać testów opartych na tabeli

Sprawdź to w ten sposób: Jeśli robisz gałęzie logiczne w swoim rzeczywistym wywołaniu testu, to albo nie powinieneś używać testów opartych na tabeli, albo twoja funkcja jest zbyt skomplikowana i powinna zostać rozbita.

Ten przykład nie jest jeszcze bardzo zły do naśladowania (nawet jeśli nie jest to świetny kod), ale nadal jest to code smell, który może wskazywać na źle zaprojektowaną funkcję.le


Oryginał tekstu w języku angielskim przeczytasz tutaj.

<p>Loading...</p>