Cyfroteka.pl

klikaj i czytaj online

Cyfro
Czytomierz
00462 008170 11207255 na godz. na dobę w sumie
Programowanie w języku Go. Koncepcje i przykłady. Wydanie II - ebook/pdf
Programowanie w języku Go. Koncepcje i przykłady. Wydanie II - ebook/pdf
Autor: Liczba stron: 384
Wydawca: Helion Język publikacji: polski
ISBN: 978-83-283-3458-8 Data wydania:
Lektor:
Kategoria: ebooki >> komputery i informatyka >> programowanie >> inne - programowanie
Porównaj ceny (książka, ebook (-20%), audiobook).

Go jest nowoczesnym językiem programowania rozwijanym przez firmę Google. Ostatnie zmiany sprawiły, że stał się on komfortowym narzędziem do tworzenia wydajnych aplikacji. Programiści Go mogą korzystać z wciąż rosnącego zbioru pakietów dostępnych jako open source, dzięki czemu tworzą i wdrażają oprogramowanie o znakomitej jakości. Taki kod od pierwszego dnia działa dobrze, a przy tym imponuje skalowalnością. Wbudowane mechanizmy Go, takie jak współbieżność, zapewniają możliwość uzyskiwania fantastycznych wyników nawet na najprostszym sprzęcie.

Niniejsza książka stanowi znakomite wprowadzenie do programowania w Go. Przyda się zarówno początkującym, jak i zaawansowanym programistom. Podstawą prezentowanych tu projektów są skalowalność, wydajność działania oraz wysoka dostępność. Poza opisem języka przedstawiono tu sporo istotnych koncepcji architektury oprogramowania. Wykorzystano aspekty filozofii wykorzystywanej przez „zwinnych” programistów. Opisano zasady tworzenia aplikacji korzystających z Google App Engine, pisania i korzystania z mikrousług czy też tworzenia obrazów Dockera. Wiedza zdobyta dzięki tej książce ułatwi każdemu stosowanie rozwiązań najwyższej klasy.

Najważniejsze zagadnienia:

Język Go. Idź i programuj!


Mat Ryer — programuje od szóstego roku życia. Swoje pierwsze eksperymenty przeprowadzał wraz z ojcem w języku BASIC, a następnie w językach AmigaBASIC i AMOS. Jego kariera zawodowa od początku była związana z programowaniem. Używał wielu różnych języków, aż w końcu zwrócił uwagę na rozwijany przez Google język Go. Ryer od lat używa języka Go do tworzenia przeróżnych produktów, usług i projektów typu open source. Jest gorącym orędownikiem tego rozwiązania. Pisze o Go artykuły, a podczas różnych wykładów i konferencji zachęca programistów, by go wypróbowali.

Znajdź podobne książki Ostatnio czytane w tej kategorii

Darmowy fragment publikacji:

Tytuł oryginału: Go Programming Blueprints, Second Edition Tłumaczenie: Piotr Rajca ISBN: 978-83-283-3457-1 Copyright © Packt Publishing 2016. First published in the English language under the title Go Programming Blueprints - Second Edition - (9781786468949) Polish edition copyright © 2017 by Helion SA All rights reserved. All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from the Publisher. Wszelkie prawa zastrzeżone. Nieautoryzowane rozpowszechnianie całości lub fragmentu niniejszej publikacji w jakiejkolwiek postaci jest zabronione. Wykonywanie kopii metodą kserograficzną, fotograficzną, a także kopiowanie książki na nośniku filmowym, magnetycznym lub innym powoduje naruszenie praw autorskich niniejszej publikacji. Wszystkie znaki występujące w tekście są zastrzeżonymi znakami firmowymi bądź towarowymi ich właścicieli. Autor oraz Wydawnictwo HELION dołożyli wszelkich starań, by zawarte w tej książce informacje były kompletne i rzetelne. Nie biorą jednak żadnej odpowiedzialności ani za ich wykorzystanie, ani za związane z tym ewentualne naruszenie praw patentowych lub autorskich. Autor oraz Wydawnictwo HELION nie ponoszą również żadnej odpowiedzialności za ewentualne szkody wynikłe z wykorzystania informacji zawartych w książce. Wydawnictwo HELION ul. Kościuszki 1c, 44-100 GLIWICE tel. 32 231 22 19, 32 230 98 63 e-mail: helion@helion.pl WWW: http://helion.pl (księgarnia internetowa, katalog książek) Pliki z przykładami omawianymi w książce można znaleźć pod adresem: ftp://ftp.helion.pl/przyklady/progo2.zip Drogi Czytelniku! Jeżeli chcesz ocenić tę książkę, zajrzyj pod adres http://helion.pl/user/opinie/progo2 Możesz tam wpisać swoje uwagi, spostrzeżenia, recenzję. Printed in Poland. • Kup książkę • Poleć książkę • Oceń książkę • Księgarnia internetowa • Lubię to! » Nasza społeczność Spis treści O autorze O recenzentach Podziękowania Wstęp Rozdział 1. Komunikator korzystający z gniazd internetowych Prosty serwer WWW Modelowanie pokoju rozmów oraz klientów na serwerze Pisanie kodu HTML i JavaScript klienta pogawędek Śledzenie kodu w celu określenia, jak działa Podsumowanie Rozdział 2. Dodawanie kont użytkowników Wszędzie tylko funkcje obsługi Tworzenie atrakcyjnej strony logowania z użyciem serwisów społecznościowych Punkty końcowe używające dynamicznych ścieżek Pierwsze kroki z OAuth2 Poinformowanie dostawców autoryzacji o naszej aplikacji Implementacja zewnętrznego logowania Podsumowanie Rozdział 3. Trzy sposoby implementacji zdjęć profilowych Pobieranie awatarów z serwerów OAuth2 Implementacja usługi Gravatar Przesyłanie zdjęcia profilowego na serwer Połączenie wszystkich trzech implementacji Podsumowanie 7 9 11 13 19 20 26 34 38 50 53 54 57 59 61 63 64 75 77 78 85 93 109 110 Poleć książkęKup książkę Spis treści Rozdział 4. Narzędzia do znajdywania nazw domen uruchamiane z poziomu wiersza poleceń Stosowanie potoków w narzędziach uruchamianych z poziomu wiersza poleceń Pięć prostych programów Połączenie wszystkich pięciu programów Podsumowanie Rozdział 5. Tworzenie systemów rozproszonych i praca z elastycznymi danymi Projekt systemu Instalacja środowiska Odczytywanie głosów z Twittera Zliczanie głosów Uruchamianie rozwiązania Podsumowanie Rozdział 6. Udostępnianie danych i możliwości funkcjonalnych przez API internetowej usługi danych typu RESTful Projektowanie API typu RESTful Współdzielenie danych pomiędzy funkcjami obsługi Opakowywanie funkcji obsługi Wstrzykiwanie zależności Odpowiedzi Wyjaśnienie obiektu żądania Udostępnianie API składającego się z jednej funkcji Obsługa punktów końcowych Internetowy klient korzystający z API Uruchamianie rozwiązania Podsumowanie Rozdział 7. Internetowa usługa losowych rekomendacji Ogólne informacje o projekcie Reprezentacja danych w kodzie Generacja losowych rekomendacji Podsumowanie Rozdział 8. Kopia zapasowa systemu plików Projekt rozwiązania Struktura projektu Pakiet backup Program narzędziowy uruchamiany z wiersza poleceń Program demona backupd Testowanie rozwiązania Podsumowanie 4 113 114 115 134 139 141 142 144 148 164 171 172 175 176 177 179 181 182 184 186 188 196 202 204 207 208 211 215 230 231 232 232 233 242 248 254 255 Poleć książkęKup książkę Spis treści Rozdział 9. Tworzenie aplikacji pytań i odpowiedzi dla platformy Google App Engine Google App Engine API dla języka Go Magazyn danych Google Cloud Datastore Encje i dostęp do danych Użytkownicy Google App Engine Transakcje w Google Cloud Datastore Przeszukiwanie Google Cloud Datastore Głosy Rejestracja głosu Udostępnianie operacji na danych przy użyciu protokołu HTTP Uruchamianie aplikacji składających się z kilku modułów Wdrażanie aplikacji składającej się z kilku modułów Podsumowanie Rozdział 10. Tworzenie mikrousług w języku Go przy użyciu frameworka Go kit Prezentacja gRPC Bufory protokołu Implementacja usługi Modelowanie wywołań metod przy użyciu żądań i odpowiedzi Serwer HTTP we frameworku Go kit Serwer gRPC we frameworku Go kit Tworzenie polecenia serwera Implementacja klienta gRPC Ograniczanie częstości przy wykorzystaniu oprogramowania warstwy pośredniej usługi Podsumowanie Rozdział 11. Wdrażanie aplikacji Go przy użyciu Dockera Stosowanie Dockera na lokalnym komputerze Wdrażanie obrazów Dockera Wdrażanie w chmurze Digital Ocean Podsumowanie Dodatek A. Dobre praktyki przygotowywania stabilnego środowiska języka Go Instalowanie języka Go Konfiguracja języka Go Narzędzia języka Go Czyszczenie, budowanie i wykonywanie testów podczas zapisywania plików źródłowych Zintegrowane środowiska programistyczne Podsumowanie Skorowidz 257 258 266 268 272 275 280 282 286 289 302 304 305 307 309 310 314 318 323 324 328 334 339 344 345 346 351 353 359 361 362 362 364 367 368 374 375 5 Poleć książkęKup książkę Programowanie w języku Go. Koncepcje i przykłady Poleć książkęKup książkę 9 Tworzenie aplikacji pytań i odpowiedzi dla platformy Google App Engine Platforma Google App Engine zapewnia programistom możliwość bezwysiłkowego wdrażania własnych aplikacji (w terminologii platformy stosowane jest określenie NoOps — No Operations — co oznacza, że programiści i inżynierowie nie muszą nic robić, by uruchomić i wdrożyć utworzony kod), a od ponad roku Go jest oficjalnie jednym z języków obsługiwanych przez tę platformę. Architektura firmy Google obsługuje wiele największych aplikacji na świecie, takich jak Google Search, Google Maps czy też Gmail, zatem bez wahania można jej powierzyć obsługę własnego kodu. Google App Engine umożliwia napisanie aplikacji w języku Go, dodanie do niej kilku specjal- nych plików konfiguracyjnych, a następnie wdrożenie jej na serwerach Google, gdzie zostanie uruchomiona i udostępniona w środowisku cechującym się bardzo wysoką dostępnością, skalo- walnością i elastycznością. Instancje aplikacji będą automatycznie uruchamiane w celu zaspokajania rosnących potrzeb, a następnie, gdy obciążenie zmaleje, łagodnie zamykane, by oszczędzać dar- mowe limity lub zmieścić się w z góry założonych budżetach. Google App Engine, oprócz uruchamiania instancji aplikacji, udostępnia setki użytecznych usług, takich jak szybkie i skalowalne magazyny danych, wyszukiwanie, memcache — usługę pamięci podręcznej czy też kolejki zadań. Dzięki niezauważalnym mechanizmom równoważenia Poleć książkęKup książkę Programowanie w języku Go. Koncepcje i przykłady obciążenia programiści nie muszą tworzyć lub utrzymywać dodatkowego oprogramowania czy też specjalnych konfiguracji sprzętowych, by zapewnić, że nie dojdzie do przeciążenia serwerów, a żądania będą obsługiwane błyskawicznie. W tym rozdziale napiszemy serwerowy interfejs API usługi służącej do obsługi listy pytań i od- powiedzi, przypominającej nieco serwis Stack Overflow bądź Quora, a następnie wdrożymy tę usługę na Google App Engine. W trakcie prac nad tym rozwiązaniem poznamy techniki, wzorce oraz praktyki, które będzie można stosować podczas tworzenia wszelkich aplikacji tego typu, jak również dokładniej przyjrzymy się niektórym z najbardziej przydatnych usług, z jakich może korzystać nasza aplikacja. Konkretnie rzecz biorąc, w tym rozdziale zostaną przedstawione następujące zagadnienia.  Sposoby korzystania z Googe App Engine SDK dla języka Go do pisania i testowania aplikacji lokalnie, przed ich wdrożeniem w chmurze.  Stosowanie pliku app.yaml do konfigurowania aplikacji.  Wykorzystanie modułów (Modules) platformy Google App Engine do niezależnego zarządzania poszczególnymi komponentami tworzącymi aplikację.  Możliwości wykorzystania Google Cloud Datastore do trwałego przechowywania i przeszukiwania danych z zachowaniem możliwości skalowania rozwiązania.  Sensowny sposób modelowania danych oraz korzystania z kluczy w bazie Google Cloud Datastore.  Wykorzystanie Google App Engine User API do uwierzytelniania użytkowników z użyciem ich kont Google.  Wzorzec projektowy pozwalający na osadzanie w encjach nieznormalizowanych danych.  Sposoby zapewniania integralności danych oraz tworzenia liczników z wykorzystaniem transakcji.  Wpływ zachowywania dobrej linii kodu na lepsze możliwości jego utrzymania.  Sposoby tworzenia tras HTTP bez wprowadzania zależności do zewnętrznych pakietów. Google App Engine API dla języka Go Aby uruchamiać i wdrażać aplikacje Google App Engine, konieczne jest pobranie i skonfigurowa- nie SDK dla języka Go. W tym celu należy przejść na stronę https://cloud.google.com/appengine/ downloads, kliknąć duży, niebieski przycisk GO i pobrać na komputer najnowszą wersję Google App Engine SDK for Go. Pobrany plik ZIP zawiera katalog go_appengine, który należy umie- ścić w wybranym miejscu poza katalogiem wskazanym w zmiennej środowiskowej GOPATH, na przykład w katalogu c:\Użytkownicy\nazwa\work\go_appengine. 258 Poleć książkęKup książkę Rozdział 9. • Tworzenie aplikacji pytań i odpowiedzi dla platformy Google Możliwe, że w przyszłości nazwy plików SDK ulegną zmianie; w takim razie należy przejrzeć stronę główną projektu (https://github.com/matryer/goblueprints) i poszukać na niej dodatkowych informacji. Następnie trzeba będzie dodać ścieżkę dostępu do katalogu go_appengine do zmiennej śro- dowiskowej PATH, tak samo jak wcześniej podczas konfigurowania języka Go zrobiliśmy z ka- talogiem, w którym został on umieszczony. Aby przetestować instalację, należy wykonać polecenie: goapp version Powinno ono wyświetlić następujący komunikat: go version go1.6.3 (appengine-1.9.48) windows/amd64 Wersja Go wyświetlona przez to polecenie będzie zapewne inna i opóźniona o kilka miesięcy względem kompilatora Go. Wynika to z faktu, że zespół Cloud Platform w firmie Google też potrzebuje trochę czasu na zapewnienie obsługi nowej wersji języka. Polecenie goapp jest w rzeczywistości skrótową formą zapisu polecenia go uzupełnionego o kilka innych podpoleceń; dzięki czemu można go używać na przykład w takich poleceniach jak goapp test oraz goapp vet. Tworzenie aplikacji Aby wdrożyć aplikację na serwerach firmy Google, trzeba skonfigurować ją przy użyciu Google Cloud Platform Console. W tym celu należy wyświetlić w przeglądarce stronę https://console. cloud.google.com/ i zalogować się na swoje konto Google. Następnie trzeba poszukać w menu opcji Utwórz projekt; jej położenie na stronie może się zmieniać, gdyż postać strony konsoli od czasu do czasu jest modyfikowana. Jeśli czytelnicy dysponują już jakimś projektem, należy kliknąć jego nazwę, aby otworzyć menu, w nim będzie dostępna opcja tworzenia nowego projektu. W razie problemów ze znalezieniem tej opcji wystarczy wpisać w wyszukiwarce internetowej hasło Creating App Engine project. Po wyświetleniu okna dialogowego Nowy projekt zostaniemy poproszeni o podanie nazwy tworzonej aplikacji. Nazwa może być dowolna (na przykład Odpowiedzi), należy jednak zwró- cić uwagę na automatycznie wygenerowany identyfikator projektu — trzeba go będzie podać podczas konfigurowania aplikacji. Można także kliknąć odnośnik Edytuj i samodzielnie określić postać tego identyfikatora; należy jednak pamiętać, że musi on być unikalny w skali całego świa- ta, więc podczas jego wymyślania przyda się sporo kreatywności. W tej książce użyjemy identyfi- katora goapp-odpowiedzi, choć oczywiście czytelnicy już nie będą mogli go użyć. 259 Poleć książkęKup książkę Programowanie w języku Go. Koncepcje i przykłady Utworzenie projektu może zająć minutę lub dwie; nie trzeba czekać na zakończenie tej operacji — można kontynuować lekturę i zajrzeć na stronę konsoli Google nieco później. Aplikacje App Engine są pakietami Go Teraz, kiedy mamy już skonfigurowany Google App Engine SDK dla języka Go, a aplikacja została utworzona, możemy zająć się jej implementacją. Na platformie Google App Engine aplikacja jest zwyczajnym pakietem języka Go dysponują- cym funkcją init, która rejestruje funkcje obsługi przy wykorzystaniu funkcji http.Handle lub http.HandleFunc. Warto zwrócić uwagę, że funkcja init nie musi się znajdować w pakiecie main. Utwórzmy zatem (gdzieś wewnątrz katalogu podanego w zmiennej środowiskowej GOPATH) nowy katalog o nazwie answerapp/api, a w nim plik main.go o następującej zawartości: package api import ( io net/http ) func init() { http.HandleFunc( / , handleHello) } func handleHello(w http.ResponseWriter, r *http.Request) { io.WriteString(w, Witamy w Google App Engine ) } Przeważająca większość tego kodu powinna już wyglądać znajomo, warto jednak zwrócić uwagę, że brakuje w nim wywołania funkcji ListenAndServe, a funkcje obsługi są określane w funkcji init, a nie main. Jak widać, wszystkie żądania będą obsługiwane przez naszą prostą funkcję handleHello, której działanie ogranicza się jedynie do wyświetlenia łańcucha znaków z powi- taniem. Plik app.yaml Aby przekształcić nasz prosty pakiet Go w aplikację Google App Engine, musimy do niego dodać specjalny plik konfiguracyjny app.yaml. Należy go umieścić w katalogu głównym aplikacji lub modułu, a zatem w naszym przypadku zapiszemy go w katalogu answerapp/api. Poniżej przedstawiona została zawartość tego pliku: application: TWÓJ_IDENTYFIKATOR_APLIKACJI version: 1 runtime: go api_version: go1 handlers: - url: /.* script: _go_app 260 Poleć książkęKup książkę Rozdział 9. • Tworzenie aplikacji pytań i odpowiedzi dla platformy Google Ten plik to czytelny zarówno dla ludzi, jak i dla komputerów konfiguracyjny plik YAML (Yet Another Markup Language1 — dodatkowe informacje na temat tego formatu można znaleźć na stronie http://yaml.org). Każda z właściwości użytych w powyższym kodzie została opisana w tabeli 9.1. Tabela 9.1. Właściwości konfiguracyjne aplikacji Google App Engine Właściwość Opis application version runtime Identyfikator aplikacji (skopiowany z okna dialogowego do tworzenia aplikacji). Numer wersji aplikacji; można wdrażać wiele wersji aplikacji, a nawet rozdzielać ruch pomiędzy różne wersje w celu na przykład testowania nowych możliwości. Nasza aplikacja będzie na razie mieć numer wersji 1. Nazwa środowiska wykonawczego, w którym będzie działać aplikacja. Ponieważ to książka o programowaniu w Go i piszemy aplikację właśnie w tym języku, zatem właściwość ta będzie mieć wartość go. api_version Wersja API o nazwie go1 to wersja środowiska wykonawczego języka Go obsługiwana przez Google; można przypuszczać, że w przyszłości zostanie udostępniona wersja go2. Zestaw skonfigurowanych powiązań URL. W naszym przypadku wszystkie żądania będą kierowane do specjalnego skryptu _go_app, jednak nic nie stoi na przeszkodzie, by określać tu także statyczne pliki i katalogi. handlers Uruchamianie prostej aplikacji na lokalnym komputerze Zanim wdrożymy naszą aplikację na serwerach firmy Google, warto ją przetestować lokalnie. Do tego celu można wykorzystać pobrane i skonfigurowane wcześniej Google App Engine SDK. Przejdźmy zatem do katalogu answerapp/api i w oknie terminala wykonajmy następujące po- lecenie: goapp serve W efekcie w oknie konsoli powinny się pojawić komunikaty przedstawione na rysunku 9.1. Rysunek 9.1. Uruchamianie lokalnego serwera aplikacji Google App Engine 1 Jeszcze jeden język znacznikowy — przyp. tłum. 261 Poleć książkęKup książkę Programowanie w języku Go. Koncepcje i przykłady Komunikaty przedstawione na rysunku 9.1 informują, że serwer API jest dostępny na porcie :57482, serwer administracyjny jest dostępny na porcie :8000, a nasza aplikacja (moduł default) pod adresem localhost:8080. Wyświetlmy zatem naszą aplikację w przeglądarce (patrz rysu- nek 9.2). Rysunek 9.2. Efekty odwołania do aplikacji uruchomionej na lokalnym komputerze Ponieważ w przeglądarce został wyświetlony komunikat Witamy w Google App Engine, wiemy, że udało się uruchomić aplikację na lokalnym komputerze. Przejdźmy teraz na serwer admini- stracyjny — w tym celu wystarczy zmienić numer portu z :8080 na :8000 (patrz rysunek 9.3). Rysunek 9.3. Strona serwera administracyjnego 262 Poleć książkęKup książkę Rozdział 9. • Tworzenie aplikacji pytań i odpowiedzi dla platformy Google Na rysunku 9.3 przedstawiono portal, którego można używać do sprawdzania różnych aspek- tów aplikacji; na przykład można wyświetlić liczbę uruchomionych instancji aplikacji, prze- glądać jej magazyn danych, zarządzać zadaniami w kolejce i tak dalej. Wdrażanie prostej aplikacji na Google App Engine Aby w pełni zrozumieć i docenić ogrom możliwości, jakie zapewnia platforma Google App Engine, oraz łatwość, z jaką można na niej uruchamiać aplikacje, spróbujemy teraz wdrożyć w chmurze nasz prosty program. W tym celu należy wrócić do okna terminala, zatrzymać serwer, naciskając kombinację klawiszy Ctrl+C, a następnie wykonać polecenie: goapp deploy W efekcie aplikacja zostanie spakowana i skopiowana na serwery firmy Google. Po zakończeniu operacji w oknie konsoli zostanie wyświetlony komunikat podobny do przedstawionego poniżej: Completed update of app: goapp-odpowiedzi, version: 1 I to tyle. O tym, że faktycznie tylko tyle wystarczy, by udostępnić aplikację na serwerach Google, możemy się przekonać, odwołując się do punktu końcowego, bezpłatnie udostępnianego dla każdej apli- kacji Google App Engine; ma on następującą postać: https://ID_APLIKACJI.appspot.com. Po przejściu na tę stronę w przeglądarce zostanie wyświetlony ten sam komunikat, co wcześniej (może przy tym zostać użyta inna czcionka, gdyż w odróżnieniu od lokalnego serwera używa- nego do celów programistycznych serwery Google robią pewne założenia dotyczące typów zawartości). Aplikacja jest udostępniana przy użyciu protokołu HTTP/2 i ma możliwości obsługi naprawdę bardzo du- żego obciążenia, a wszystkim, co musieliśmy zrobić, by ją utworzyć i udostępnić, było napisanie prostego pliku konfiguracyjnego i kilku wierszy kodu. Moduły w Google App Engine Moduły to pakiety języka Go, którym można przypisywać numery wersji, aktualizować je oraz zarządzać nimi niezależnie od pozostałych modułów wchodzących w skład rozwiązania. Apli- kacja może się składać z jednego bądź też z wielu modułów, z których każdy będzie unikalny, lecz należący do tej samej aplikacji i dysponujący dostępem do tych samych danych i usług. Każda aplikacja musi mieć swój moduł domyślny nawet wtedy, kiedy będzie bardzo prosta. Aplikacja, którą napiszemy w tym rozdziale, będzie się składać z trzech modułów przedsta- wionych w tabeli 9.2. 263 Poleć książkęKup książkę Programowanie w języku Go. Koncepcje i przykłady Tabela 9.2. Moduły aplikacji tworzonej w tym rozdziale Opis modułu Obowiązkowy moduł domyślny Pakiet API typu RESTful używający formatu JSON Statyczna witryna WWW udostępniająca pliki HTML, CSS i JavaScript, korzystająca z API przy użyciu technologii AJAX Nazwa modułu default api web Każdy z tych modułów będzie odrębnym pakietem języka Go, a ich zawartość zostanie umiesz- czona w odrębnych katalogach. Podzielmy zatem nasz projekt na moduły, dodając obok katalogu api nowy katalog o nazwie default. Domyślny moduł naszej aplikacji będzie służył praktycznie wyłącznie do celów konfiguracyj- nych, gdyż chcemy, by wszystkie kluczowe możliwości funkcjonalne aplikacji były obsługiwane przez dwa pozostałe moduły. Jeśli jednak ten moduł będzie pusty, Google App Engine poskarży się, że nie ma w nim nic do zbudowania. Wewnątrz katalogu default dodajmy zatem plik main.go o poniższej, wyjątkowo krótkiej za- wartości: package defaultmodule func init() {} Jak widać, ten plik nie robi nic — jedynie zapewnia możliwość istnienia modułu default. Byłoby dobrze, gdyby nazwy naszych pakietów mogły odpowiadać nazwom katalogów, jednak w języku Go default jest słowem kluczowym, więc w tym przypadku mamy dobry powód, by złamać tę regułę. Ostatni moduł naszej aplikacji będzie nosił nazwę web, a więc w tym samym miejscu, gdzie znajdują się katalogi api oraz default, utwórzmy jeszcze jeden — web. W tym rozdziale zaj- miemy się jedynie zaimplementowaniem interfejsu API dla naszej aplikacji, natomiast jeśli cho- dzi o pakiet web, uprościmy sobie życie i po prostu skopiujemy jego zawartość. Archiwum ZIP zawierające wszystkie pliki wchodzące w skład tego modułu jest dostępne w przykładach dołączonych do książki, w katalogu rozdzial_09 i nosi nazwę web.zip. Należy je rozpakować, a całą zawartość umieścić w katalogu web. Obecnie nasza aplikacja ma następującą strukturę: /answerapp/api /answerapp/default /answerapp/web 264 Poleć książkęKup książkę Rozdział 9. • Tworzenie aplikacji pytań i odpowiedzi dla platformy Google Określanie modułów Aby określić, którym modułem stanie się nasz pakiet api, musimy dodać odpowiednią wła- ściwość do pliku app.yaml umieszczonego w katalogu api. Konkretnie rzecz biorąc, należy do niego dodać właściwość module: application: TWÓJ_IDENTYFIKATOR_APLIKACJI version: 1 runtime: go module: api api_version: go1 handlers: - url: /.* script: _go_app Ponieważ drugi z modułów — default — także trzeba będzie wdrożyć, również do niego mu- simy dodać plik konfiguracyjny app.yaml. A zatem skopiujmy plik api/app.yaml i zapiszmy go pod tą samą nazwą w katalogu default, a następnie zmieńmy, tak by jego zawartość wyglądała jak na poniższym przykładzie: application: TWÓJ_IDENTYFIKATOR_APLIKACJI version: 1 runtime: go module: default api_version: go1 handlers: - url: /.* script: _go_app Kierowanie żądań do modułów przy wykorzystaniu pliku dispatch.yaml Aby w odpowiedni sposób kierować żądania do modułów, trzeba będzie utworzyć jeszcze jeden plik konfiguracyjny o nazwie dispatch.yaml, który będzie kojarzył wzorce URL z modułami. Chcemy, aby wszystkie żądania kierowane pod adresy zawierające ścieżkę /api/ były przekie- rowywane do modułu api, natomiast wszystkie pozostałe mają trafiać do modułu web. Zgodnie z informacjami podanymi już wcześniej nie zamierzamy używać modułu default do obsługi jakichkolwiek żądań; jednak dalej w tym rozdziale znajdziemy dla niego inne zastosowanie. W katalogu głównym answersapp (w którym znajdują się już katalogi poszczególnych modułów) musimy utworzyć nowy plik o nazwie dispatch.yaml i następującej zawartości: application: TWÓJ_IDENTYFIKATOR_APLIKACJI dispatch: - url: */api/* module: api - url: */* module: web 265 Poleć książkęKup książkę Programowanie w języku Go. Koncepcje i przykłady Ta sama właściwość application informuje Google App Engine SDK dla języka Go, o którą aplikację nam chodzi; natomiast sekcja dispatch kojarzy adresy URL z modułami. Magazyn danych Google Cloud Datastore Jedną z usług dostępnych dla programistów korzystających z App Engine jest Google Cloud Datastore — dokumentowa baza danych NoSQL, stworzona pod kątem zapewniania możli- wości automatycznego skalowania i wysokiej wydajności. Baza gwarantuje ogromne możliwości skalowania, jednak pomyślne jej wykorzystanie w projekcie wymaga dokładnego zrozumienia ograniczeń oraz najlepszych sposobów użycia. Denormalizacja danych Programiści mający doświadczenie w korzystaniu z relacyjnych systemów baz danych (RDBMS) zazwyczaj starają się ograniczać nadmiarowość danych (czyli dążyć do tego, by każda informacja występowała w bazie danych tylko jeden raz), przeprowadzając tak zwaną normalizację tych danych — rozdzielają je pomiędzy różnymi tabelami i dodają odwołania (klucze obce), a dopiero potem łączą przy użyciu zapytań i uzyskują zamierzoną postać informacji. Jednak w przypadku baz danych pozbawionych schematu oraz baz NoSQL zazwyczaj postępuje się w odwrotny sposób — dane są denormalizowane, co oznacza, że każdy dokument zawiera wszystkie niezbędne dane, dzięki czemu czasy są bardzo krótkie, gdyż z bazy trzeba odczytać tylko jeden element. W ramach przykładu zastanówmy się, w jaki sposób można zamodelować tweety w relacyjnej bazie danych, takiej jak MySQL lub Postgres. Postać struktury takiej bazy przedstawiono na rysunku 9.4. Rysunek 9.4. Postać relacyjnej bazy danych do przechowywania tweetów Sam tweet zawiera wyłącznie swój unikalny identyfikator, klucz obcy do tabeli użytkowników (Users) reprezentujący autora tweetu, jak również, ewentualnie, dowolną liczbę adresów URL wspomnianych w treści tweetu (TweetBody). Jedną z zalet tego projektu jest to, że użytkownik bez przeszkód może zmienić swoją nazwę (Name) lub adres URL zdjęcia profilowego (AvatarURL), a zmiany te zostaną uwzględnione we wszystkich tweetach zarówno tych starych, jak i przyszłych. Jest to coś, czego — niestety — nie da się równie łatwo osiągnąć w świecie danych zdenormalizowanych. 266 Poleć książkęKup książkę Rozdział 9. • Tworzenie aplikacji pytań i odpowiedzi dla platformy Google Jednak z drugiej strony, aby wyświetlić tweet użytkownikowi, trzeba wczytać sam tweet, wy- szukać (przy użyciu odpowiedniego złączenia) nazwę autora oraz adres URL jego zdjęcia profi- lowego, jak również wczytać dane z tabeli adresów URL, by pokazać podgląd odnośników. W przypadku wielkiej liczby danych operacje te mogą się okazać problematyczne, gdyż wszyst- kie trzy tabele mogą być od siebie fizycznie odseparowane, a to z kolei oznacza, że uzyskanie pełnej postaci danych może wymagać wykonania wielu operacji. A teraz przyjrzyjmy się zdenormalizowanej postaci danych, która została przedstawiona na rysunku 9.5. Rysunek 9.5. Zdenormalizowana postać danych tweetów Także w tym przypadku mamy te same trzy kolekcje danych, z tą różnicą, że teraz tweety za- wierają wszystkie informacje niezbędne do ich wyświetlenia i to bez konieczności wyszuki- wania i pobierania danych z innych kolekcji. Doświadczeni projektanci relacyjnych baz danych czytający te słowa zapewne już wiedzą, co to oznacza, i bez wątpienia nie jest im z tym lekko. A zatem to rozwiązanie oznacza, że:  te same dane są powielane — zawartość pola AvatarURL z kolekcji Users jest powtarzana w polu UserAvatarURL w kolekcji Tweets (czysta strata miejsca, nieprawdaż?),  jeśli któreś z pól użytkownika, na przykład AvatarURL, zmieni swoją wartość, wszystkie tweety będą zawierać nieaktualne dane. W ostatecznym rozrachunku decyzje projektowe związane z bazą danych sprowadzają się do fizyki. Uznajemy, że nasze tweety będą odczytywane znacznie częściej niż zapisywane, więc z góry akceptujemy te problemy i zwiększone wymagania dotyczące przestrzeni zajmowanej przez dane. W takim przypadku powielanie danych samo w sobie nie jest niczym strasznym, o ile tylko pamiętamy, który zestaw danych jest nadrzędny, a które są powielane w celu zwięk- szenia efektywności działania aplikacji. Modyfikacja danych jest interesującym zagadnieniem, lecz warto się zastanowić nad powo- dami, które sprawiają, że jesteśmy w stanie zaakceptować wady takiego rozwiązania. Przede wszystkim zwiększona szybkość odczytu tweetów zapewne z nawiązką rekompensuje fakt, że ewentualne zmiany danych nadrzędnych nie będą uwzględniane w już istniejących dokumentach. Już sam ten powód wystarczyłby, żeby pogodzić się utrudnieniami i dodatkowym nakładem pracy, jaki należy włożyć w ich rozwiązanie. 267 Poleć książkęKup książkę Programowanie w języku Go. Koncepcje i przykłady Poza tym możemy dojść do wniosku, że warto będzie przechowywać kopię danych z określo- nego momentu czasu. Wyobraźmy sobie na przykład, że ktoś napisał tweet z pytaniem, czy znajomym podoba się jego zdjęcie profilowe. Gdyby zdjęcie zostało zmienione, kontekst tego pytania zostałby bezpowrotnie stracony. I jeszcze jeden, nieco bardziej poważny przykład: zastanówmy się, co by się stało, gdybyśmy odwoływali się do wiersza w tabeli adresów okre- ślającego miejsce dostarczenia przesyłki i gdyby ten adres został zmieniony. Nagle można by odnieść wrażenie, że zamówienie zostało przesłane na inny adres. I w końcu ostatnia sprawa: magazynowanie danych jest coraz tańsze, dlatego też powoli znika potrzeba normalizacji danych w celu oszczędzania miejsca. Twitter posunął się aż do tego, że dokument tweetu jest kopiowany dla każdej z osób, które śledzą wypowiedzi autora tego tweetu. Setka osób śledzących nasze tweety oznacza, że tweet zostanie skopiowany 100 razy, a może nawet więcej ze względu na nadmiarowość. Dla entuzjastów relacyjnych baz danych takie roz- wiązanie może się wydawać czystym szaleństwem, jednak Twitter godzi się na mądry kompromis, a kieruje poprawą wrażeń użytkowników — bez oporów zgodzą się na wielokrotne zapisywanie tweeta, wiedząc, że dzięki temu użytkownik nie będzie musiał długo czekać na ich wyświetlenie, kiedy odświeży swój strumień tweetów. Gdyby ktoś chciał sprawdzić skalę oraz konsekwencje tych decyzji projektowych, warto zajrzeć do do- kumentacji API serwisu Twitter i zobaczyć, co zawiera dokument tweetu. Zapisywanych w nim jest na- prawdę sporo danych. A potem proszę sprawdzić, ile osób śledzi tweety Lady Gagi. W niektórych kręgach mówi się nawet o „problemie Lady Gagi”, a jest on rozwiązywany przez wiele technologii i technik, których przedstawienie wykracza poza ramy tematyczne tego rozdziału. Skoro już znamy dobre praktyki projektowania baz NoSQL, możemy zająć się implementacją typów, funkcji oraz metod niezbędnych do obsługi danych w ramach API naszej aplikacji. Encje i dostęp do danych Aby zapisać dane w Google Cloud Database, będziemy potrzebować struktur do reprezentowania encji. Te struktury encji będą następnie serializowane i deserializowane — odpowiednio — podczas zapisu i odczytu danych przy wykorzystaniu API datastore. Można także dodać do nich metody pomocnicze, które będą obsługiwać interakcje z magazynem danych, co jest do- skonałym sposobem, by zlokalizować te możliwości funkcjonalne w jednym miejscu z encjami. Przykładowo odpowiedzi będą reprezentowane przez strukturę Answer, do której dodamy metodę Create, która z kolei będzie wywoływać odpowiednie funkcje pakietu datastore. W ten spo- sób unikniemy zaśmiecania kodu funkcji obsługi żądań HTTP rozbudowanym kodem obsłu- gującym dostęp do danych, co pozwoli zachować ich przejrzystość i prostotę. Jednym z kluczowych elementów naszego rozwiązania jest pojęcie pytania. Pytanie zadaje jeden użytkownik, natomiast odpowiedzieć na nie może wiele osób. Pytanie będzie mieć swój unikalny identyfikator, dzięki czemu będzie można się do niego odwołać (za pośrednictwem adresu URL), dodamy do niego także znacznik czasu określający, kiedy zostało zadane. 268 Poleć książkęKup książkę Rozdział 9. • Tworzenie aplikacji pytań i odpowiedzi dla platformy Google Utwórzmy zatem w katalogu answerapp nowy plik o nazwie questions.go, zawierający począt- kowo poniższą definicję struktury: type Question struct { Key *datastore.Key `json: id datastore: - ` CTime time.Time `json: created ` Question string `json: question ` User UserCard `json: user ` AnswersCount int `json: answers_count ` } Struktura opisuje pytanie, które będzie można zadawać za pośrednictwem naszej aplikacji. Większość tego kodu powinna być zrozumiała, gdyż podobne rozwiązania stosowaliśmy już w poprzednich rozdziałach książki. Struktura UserCard reprezentuje zdenormalizowaną encję User, którą zajmiemy się nieco później. Pakiet datastore można zaimportować do projektu, używając polecenia: import google.golang.org/appengine/datastore . Zanim zajmiemy się kolejnymi zagadnieniami związanymi z danymi w naszej aplikacji, warto poświęcić nieco czasu na dokładniejsze przedstawienie typu datastore.Key. Klucze w Google Cloud Datastore Każda encja zapisywana w Cloud Datastore ma swój klucz, który w unikalny sposób ją iden- tyfikuje. Klucze te mogą być łańcuchami znaków lub liczbami, zależnie od tego, co w kon- kretnym przypadku jest bardziej sensowne. Wartości kluczy możemy podawać samodzielnie bądź też pozwolić, by były one określane automatycznie przez magazyn danych; także w tym przypadku decyzja o tym, które z tych dwóch rozwiązań zostanie zastosowane, jest zazwyczaj podejmowana na podstawie konkretnego przypadku użycia, a obie możliwości zostaną bardziej szczegółowo opisane dalej w tym rozdziale. Do tworzenia kluczy używane są dwie funkcje — datastore.NewKey oraz datastore.New IncompleteKey. Kluczy po utworzeniu można używać do zapisu oraz odczytu danych z maga- zynu; do czego z kolei służą funkcje datastore.Get oraz datastore.Put. W Google Cloud Datastore dane oraz zawartości encji są niezależne od siebie; odróżnia to ten magazyn danych od bazy MongoDB oraz innych technologii bazujących na języku SQL, w których indeks jest po prostu kolejnym polem w dokumencie lub rekordzie. Właśnie z tego powodu usuwamy pole Key z naszej struktury Question, umieszczając za nim flagę datastore: - . Podobnie jak w przypadku flag dotyczących formatu JSON, także w tym przypadku zastosowany zapis oznacza, że Cloud Datastore ma całkowicie zignorować pole Key podczas odczytu i zapisu danych. 269 Poleć książkęKup książkę Programowanie w języku Go. Koncepcje i przykłady Opcjonalnie klucze mogą mieć swoje elementy nadrzędne, co stanowi miły sposób grupowa- nia powiązanych ze sobą informacji, a Datastore daje pewne gwarancje dotyczące takich grup encji — więcej informacji na ich temat można znaleźć w dostępnej w internecie dokumentacji Google Cloud Datastore. Zapis danych w Google Cloud Datastore Przed zapisaniem danych w Cloud Datastore chcemy sprawdzić, czy pytanie jest prawidłowe. W tym celu poniżej definicji struktury Question dodamy następującą metodę: func (q Question) OK() error { if len(q.Question) 10 { return errors.New( Pytanie jest zbyt krótkie ) } return nil } Funkcja OK zwraca błąd, jeśli pytanie nie jest prawidłowe, w przeciwnym przypadku zwraca nil. Teraz sprawdzamy jedynie długość pytania, która musi być większa od 10 znaków. W celu zapisywania danych w magazynie dodamy do struktury Question metodę Create. Oto fragment kodu, który należy umieścić na końcu pliku questions.go: func (q *Question) Create(ctx context.Context) error { log.Debugf(ctx, Zapisywanie pytania: s , q.Question) if q.Key == nil { q.Key = datastore.NewIncompleteKey(ctx, Question , nil) } user, err := UserFromAEUser(ctx) if err != nil { return err } q.User = user.Card() q.CTime = time.Now() q.Key, err = datastore.Put(ctx, q.Key, q) if err != nil { return err } return nil } Metoda Create pobiera wskaźnik do obiektu Question, co jest ważne, gdyż chcemy mieć możli- wość wprowadzania zmian w jej polach. Jeśli odbiorca metody zostałby określony jako (q Question) bez *, metoda dysponowałaby kopią py- tania, a nie wskaźnikiem do niego; a zatem wszelkie zmiany wprowadzone wewnątrz metody byłyby wprowadzane w kopii, a nie w początkowej strukturze Question. 270 Poleć książkęKup książkę Rozdział 9. • Tworzenie aplikacji pytań i odpowiedzi dla platformy Google Pierwszą czynnością wykonywaną wewnątrz metody Create jest użycie pakietu log (patrz strona https://godoc.org/google.golang.org/appengine/log) do zapisania komunikatu testowego, informującego o zapisywaniu pytania. W przypadku wykonywania tego kodu na komputerze używanym do programowania komunikat ten zostanie wyświetlony w oknie terminala; z kolei w razie uruchomienia aplikacji w środowisku produkcyjnym będzie on wyświetlany w specjalnej usłudze dziennika, udostępnianej przez Google Cloud Platform. Jeśli pole klucza przekazanej struktury ma wartość nil (czyli gdy mamy do czynienia z no- wym pytaniem), zapisujemy w polu niekompletny klucz, co informuje Cloud Datastore, że ma ten klucz wygenerować. Trzema argumentami przekazywanymi do funkcji NewIncompleteKey są: context.Context (obiekt, który trzeba przekazywać do wszystkich funkcji i metod operujących na magazynie danych), łańcuch znaków określający rodzaj encji oraz klucz nadrzędny (w naszym przypadku jest to nil). Kiedy już będziemy mieć pewność, że klucz jest zapisany, wywołujemy metodę (dodamy ją nieco później), która pobierze lub utworzy strukturę User na podstawie użytkownika platfor- my App Engine. Następnie określamy tego użytkownika w pytaniu i ustawiamy wartość pola CTime (czas utworzenia pytania), zapisując w nim wartość time.Now — bieżący znacznik czasu, który będzie reprezentować moment utworzenia pytania. Po tych przygotowaniach możemy w końcu wywołać funkcję datastore.Put, aby zapisać py- tanie w magazynie danych. Także w tym wywołaniu pierwszym argumentem jest obiekt con- text.Context. Kolejnymi dwoma argumentami są — odpowiednio — klucz pytania oraz sama encja pytania. Ponieważ w Google Cloud Datastore klucze są niezależne od encji, aby zatem powiązać je ze sobą w naszym kodzie, będziemy musieli wykonać nieco dodatkowej pracy. Funkcja datastore.Put zwraca dwa argumenty, czyli kompletny klucz oraz błąd. Argument klucza jest bardzo przy- datny, gdyż zapisując pytanie, przekazaliśmy w nim niekompletny klucz i poprosiliśmy magazyn danych o utworzenie kompletnego klucza — co też magazyn zrobił. W przypadku poprawnego wykonania operacji zapisu funkcja datastore.Put zwraca nowy obiekt datastore.Key; repre- zentuje on kompletny klucz i możemy go zapisać w polu Key naszego obiektu Question. Jeśli wszystkie operacje zostaną wykonane prawidłowo, funkcja Create zwraca nil. W kolejnym kroku dodamy następną funkcję pomocniczą, przeznaczoną do aktualizacji już istniejącego pytania: func (q *Question) Update(ctx context.Context) error { if q.Key == nil { q.Key = datastore.NewIncompleteKey(ctx, Question , nil) } var err error q.Key, err = datastore.Put(ctx, q.Key, q) if err != nil { return err } 271 Poleć książkęKup książkę Programowanie w języku Go. Koncepcje i przykłady return nil } Jak widać, funkcja ta jest podobna do poprzedniej, z tym że nie zmienia wartości ustawionych wcześniej pól CTime oraz User. Odczyt danych z Google Cloud Datastore Odczyt danych z magazynu sprowadza się właściwie do wywołania funkcji datastore.Get, ponieważ jednak zależy nam na zachowaniu powiązania kluczy z encjami w naszym kodzie (metody datastore nie działają w taki sposób), skorzystamy z popularnego rozwiązania pole- gającego na napisaniu odpowiedniej funkcji pomocniczej. Jej kod został przedstawiony poniżej, a należy go umieścić na końcu pliku questions.go: func GetQuestion(ctx context.Context, key *datastore.Key) (*Question, error) { var q Question err := datastore.Get(ctx, key, q) if err != nil { return nil, err } q.Key = key return q, nil } Funkcja GetQuestion pobiera argumenty typu context.Context oraz datastore.Key, przy czym ten drugi reprezentuje klucz pytania, które należy pobrać. Funkcja najpierw tworzy obiekt Qu- estion, a następnie wywołuje funkcję datastore.Get i przed zwróceniem pobranej encji zapisuje w niej klucz. Oczywiście wszelkie błędy są obsługiwane w standardowy sposób. To bardzo wygodny wzorzec, dzięki któremu użytkownicy naszego kodu mogą mieć pewność, że nigdy nie będą musieli samodzielnie korzystać z funkcji datastore.Put oraz datastore.Get, a zamiast nich mogą się posługiwać funkcjami pomocniczymi, które zapewnią prawidłową ob- sługę kluczy (jak również wszelkich innych operacji, które ewentualnie trzeba będzie wykonywać przed zapisem lub wczytaniem danych). Użytkownicy Google App Engine Kolejną usługą, której użyjemy, jest API Google App Engine User, który odpowiada za uwie- rzytelnianie kont Google (oraz kont Google Apps). Utwórzmy zatem nowy plik o nazwie users.go i następującej zawartości: type User struct { Key *datastore.Key `json: id datastore: - ` UserID string `json: - ` 272 Poleć książkęKup książkę Rozdział 9. • Tworzenie aplikacji pytań i odpowiedzi dla platformy Google DisplayName string `json: display_name ` AvatarURL string `json: avatar_url ` Score int `json: score ` } Podobnie jak struktura Question, także i ten typ zawiera pole Key oraz kilka innych pól two- rzących encję User. Struktura ta reprezentuje obiekt należący do naszej aplikacji i opisujący użytkownika. Taki obiekt będziemy tworzyć dla każdego uwierzytelnionego użytkownika naszego systemu, jednak nie jest to ten sam obiekt, który będzie zwracać User API. Zaimportowanie pakietu https://godoc.org/google.golang.org/appengine/user pozwoli nam wywo- łać funkcję user.Current(context.Context), która zwraca bądź to nil (jeśli nie został uwie- rzytelniony żaden użytkownik), bądź też obiekt user.User. Obiekt ten należy do User API i nie nadaje się do zastosowania w magazynie danych naszej aplikacji; dlatego też konieczne będzie napisanie funkcji pomocniczej, która przekształci ten obiekt na nasz obiekt User. Trzeba uważać, aby program goimports nie zaimportował automatycznie pakietu os/user; czasami najlepiej samodzielnie zarządzać importowanymi pakietami. Oto kod, który należy dodać do pliku users.go: func UserFromAEUser(ctx context.Context) (*User, error) { aeuser := user.Current(ctx) if aeuser == nil { return nil, errors.New( brak zalogowanego użytkownika ) } var appUser User appUser.Key = datastore.NewKey(ctx, User , aeuser.ID, 0, nil) err := datastore.Get(ctx, appUser.Key, appUser) if err != nil err != datastore.ErrNoSuchEntity { return nil, err } if err == nil { return appUser, nil } appUser.UserID = aeuser.ID appUser.DisplayName = aeuser.String() appUser.AvatarURL = gravatarURL(aeuser.Email) log.Infof(ctx, zapisuję nowego użytkownika: s , aeuser.String()) appUser.Key, err = datastore.Put(ctx, appUser.Key, appUser) if err != nil { return nil, err } return appUser, nil } 273 Poleć książkęKup książkę Programowanie w języku Go. Koncepcje i przykłady Aktualnie uwierzytelnionego użytkownika można pobrać, wywołując funkcję user.Current. Jeśli wywołanie zwróci wartość nil, powyższa funkcja zakończy się zwróceniem błędu. Będzie to oznaczać, że użytkownik nie jest zalogowany i operacja nie może zostać wykonana. Nasze API będzie sprawdzać i wymagać, by użytkownik był zalogowany, dlatego do momentu gdy użyt- kownik dotrze do tego punktu końcowego API, powinien już być uwierzytelniony. Teraz funkcja tworzy zmienną appUser (naszego typu User) oraz określa wartość pola datastore. Key. W tym przypadku nie tworzymy niekompletnego klucza, zamiast tego wywołujemy funkcję datastore.NewKey, przekazując do niej łańcuch znaków identyfikatora, odpowiadający identyfikatorowi User API. Przewidywalność tego klucza oznacza, że nie tylko każdemu uwie- rzytelnionemu użytkownikowi będzie odpowiadać w naszej aplikacji dokładnie jedna encja User, lecz także że będziemy mogli wczytać tę encję bez stosowania zapytania. Gdyby identyfikator użytkownika App Engine był przechowywany w polu naszej struktury, musielibyśmy użyć zapytania do pobrania interesującego nas rekordu. Wykonanie zapytania jest znacznie bardziej kosztowną operacją niż bezpośrednie pobranie danych przy użyciu metody Get, a zatem przedstawione tu rozwiązanie zawsze będzie preferowane, oczywiście, o ile tylko będzie można je zastosować. Następnie wywołujemy metodę datastore.Get, aby wczytać encję User. Jeśli użytkownik za- logował się po raz pierwszy, takiej encji nie będzie, a wywołanie zwróci specjalny błąd o wartości datastore.ErrNoSuchEntity. W takim przypadku ustawiamy wartości odpowiednich pól i za- pisujemy encję, wywołując w tym celu metodę datastore.Put. W przeciwnym razie, jeśli nie wystąpiły żadne błędy, zwracamy wczytaną encję User. Warto zwrócić uwagę, że w powyższej funkcji sprawdzamy możliwości wcześniejszego zakończenia jej działania. Robimy to, by ułatwić analizę jej kodu oraz przebiegu jego wykonywania, a także uniknięcia konieczności stosowania w nim wielu wciętych bloków. Nazywam tę cechę kodu „linią kodu” (ang. line of sight of code) i napisałem o tym we wpisie na swoim blogu, na stronie https://medium.com/@matryer. Także w tym przypadku do wyświetlania obrazków profilowych użytkowników skorzystamy z serwisu Gravatar, a zatem dodamy poniższą funkcję pomocniczą u dołu pliku users.go: func gravatarURL(email string) string { m := md5.New() io.WriteString(m, strings.ToLower(email)) return fmt.Sprintf( //www.gravatar.com/avatar/ x , m.Sum(nil)) } Osadzanie zdenormalizowanych danych Jak już pisałem, nasz typ Question nie przechowuje użytkowników jako obiektów typu User — zamiast tego stosuje typ UserCard. Czasami, podczas osadzania jednych encji w innych może się pojawić konieczność nadania im nieco innego wyglądu, niż miała encja nadrzędna. W naszym przypadku, ponieważ nie przechowujemy klucza w encji User (bo dla pola Key zo- stała użyta etykieta datastore: - ), do zapisania klucza będziemy potrzebować nowego typu. 274 Poleć książkęKup książkę Rozdział 9. • Tworzenie aplikacji pytań i odpowiedzi dla platformy Google Poniższą strukturę UserCard oraz powiązaną z nią metodę pomocniczą typu User należy dodać na końcu pliku users.go: type UserCard struct { Key *datastore.Key `json: id ` DisplayName string `json: display_name ` AvatarURL string `json: avatar_url ` } func (u User) Card() UserCard { return UserCard{ Key: u.Key, DisplayName: u.DisplayName, AvatarURL: u.AvatarURL, } } Warto zwrócić uwagę, że w strukturze UserCard nie umieściliśmy znaczników datastore, za- tem pole Key faktycznie zostanie trwale zachowane w magazynie danych. Przedstawiona po- wyżej funkcja pomocnicza Card() tworzy i zwraca obiekt UserCard, kopiując wartości każdego z pól danej typu User. Choć takie rozwiązanie może się wydawać marnowaniem zasobów, jednak zapewnia świetną kontrolę zwłaszcza wtedy, gdy osadzane dane mają wyglądać zupełnie inaczej niż ich oryginał. Transakcje w Google Cloud Datastore Transakcje pozwalają określić grupę zmian wprowadzanych w magazynie danych i zatwier- dzać je jako jedną operację. Jeśli którejkolwiek z poszczególnych zmian nie uda się wprowa- dzić prawidłowo, cała transakcja nie zostanie wykonana. Transakcje są niezwykle użyteczne w przypadkach, gdy trzeba przechowywać liczniki, bądź gdy w danych występują encje, których stan zależy od siebie nawzajem. W magazynie Google Cloud Datastore podczas wykonywania transakcji wszystkie odczytywane encje są blokowane (inny kod nie może wprowadzać w nich żadnych zmian) aż do jej zakończenia, co stanowi dodatkowe zabezpieczenie i eliminuje moż- liwość występowania wyścigów. Gdybyśmy tworzyli aplikację dla banku (to może wydawać się zwariowane, jednak firma Monzo z Lon- dynu naprawdę pisze taką aplikację, używając języka Go), konta użytkowników mogłyby być reprezen- towane przy użyciu encji o nazwie Account. Aby przelać pieniądze z jednego konta na drugie, trzeba by się upewnić, że określona kwota została odjęta od konta A i przelana na konto B w ramach jednej transakcji. Gdyby którakolwiek z tych operacji zakończyła się niepowodzeniem, ktoś byłby bardzo nieszczęśliwy (choć prawdę mówiąc, gdyby to operacja odjęcia przelewanej kwoty od konta A zakończyła się niepowodze- niem, właściciel tego konta mógłby być całkiem zadowolony, gdyż nic by nie stracił, a właściciel konta B i tak otrzymałby swoje pieniądze). 275 Poleć książkęKup książkę Programowanie w języku Go. Koncepcje i przykłady Aby przekonać się, gdzie transakcje zostaną wykorzystane w naszej aplikacji, musimy zacząć od dodania do niej struktury reprezentującej odpowiedzi na pytania. A zatem utwórzmy nowy plik o nazwie answers.go, który początkowo będzie zawierać definicję struktury oraz metodę weryfikującą poprawność jej danych: type Answer struct { Key *datastore.Key `json: id datastore: - ` Answer string `json: answer ` CTime time.Time `json: created ` User UserCard `json: user ` Score int `json: score ` } func (a Answer) OK() error { if len(a.Answer) 10 { return errors.New( Odpowiedź jest zbyt krótka ) } return nil } Struktura Answer jest bardzo podobna do struktury Question — zawiera pole typu datastore.Key (którego wartość nie jest trwale przechowywana), pole CTime zawierające znacznik czasu, jak również pole UserCard (reprezentujące osobę, która odpowiada na pytanie). Oprócz tego struktu- ra zawiera także pole liczbowe o nazwie Score, którego wartość będzie się zmieniać w zależności od pozytywnych lub negatywnych opinii innych użytkowników na temat danej odpowiedzi. Stosowanie transakcji do przechowywania liczników Struktura Question zawiera pole o nazwie AnswerCount, które według naszych zamierzeń ma być używane do przechowywania liczby całkowitej reprezentującej liczbę zarejestrowanych od- powiedzi na dane pytanie. Na początek przyjrzyjmy się, co się może stać, jeśli podczas modyfikowania wartości pola AnswerCount nie użyjemy transakcji. W tym celu w tabeli 9.3 zostały przedstawione poszcze- gólne operacje wykonywane podczas równoczesnego dodawania odpowiedzi numer 4 i 5 na to samo pytanie. Tabela 9.3. Przebieg jednoczesnego dodawania dwóch odpowiedzi na to samo pytanie Odpowiedź 5. Wczytanie pytania AnswerCount = 3 AnswerCount++ AnswerCount = 4 Question.AnswerCount 3 3 3 3 4 Zapisanie odpowiedzi i pytania Zapisanie odpowiedzi i pytania Krok Odpowiedź 4. Wczytanie pytania AnswerCount = 3 AnswerCount++ AnswerCount = 4 1 2 3 4 5 276 Poleć książkęKup książkę Rozdział 9. • Tworzenie aplikacji pytań i odpowiedzi dla platformy Google Na podstawie tej tabeli widać, że jeśli obie odpowiedzi zostaną zapisane jednocześnie bez blokowania obiektu Question, końcowa wartość pola AnswerCount może wynieść 4 zamiast 5. Zablokowanie danych przy użyciu transakcji sprawi, że cała operacja będzie wyglądać w sposób przedstawiony w tabeli 9.4. Tabela 9.4. Jednoczesny zapis dwóch odpowiedzi w przypadku stosowania transakcji i blokowania Krok Odpowiedź 4. Odpowiedź 5. Question.AnswerCount 1 2 3 4 5 6 7 8 9 Zablokowanie pytania AnswerCount = 3 AnswerCount++ Zapisanie odpowiedzi i pytania Zwolnienie blokady Zakończenie operacji Zablokowanie pytania Oczekiwanie na odblokowanie Oczekiwanie na odblokowanie Oczekiwanie na odblokowanie Oczekiwanie na odblokowanie Zablokowanie pytania AnswerCount = 4 AnswerCount++ Zapisanie odpowiedzi i pytania 3 3 3 4 4 4 4 4 5 W tym przypadku ta z odpowiedzi, której uda się uzyskać blokadę, będzie mogła wykonać zamierzoną operację, natomiast druga będzie musiała czekać na jej zakończenie. To najpraw- dopodobniej wydłuży nieco czas potrzebny na zapisanie obu odpowiedzi (gdyż druga z operacji musi czekać na zakończenie pierwszej), jednak jest to cena, którą warto zapłacić za zachowanie poprawności danych. Warto zwracać uwagę na to, by liczba operacji wykonywanych w ramach transakcji była możliwie jak najmniejsza, gdyż na czas realizacji transakcji operacje wykonywane przez inne osoby są zablokowane. Z wyjątkiem tych przypadków, gdy są używane transakcje, Google Cloud Datastore działa bardzo szybko, gdyż normalny sposób działania tego magazynu danych nie daje tych samych gwarancji, jakie zapewniają transakcje. Kod transakcji powstaje przy użyciu funkcji datastore.RunInTransaction. Poniższy fragment kodu należy dodać do pliku answers.go: func (a *Answer) Create(ctx context.Context, questionKey *datastore.Key) error { a.Key = datastore.NewIncompleteKey(ctx, Answer , questionKey) user, err := UserFromAEUser(ctx) if err != nil { return err } a.User = user.Card() a.CTime = time.Now() err = datastore.RunInTransaction(ctx, func(ctx context.Context) error { q, err := GetQuestion(ctx, questionKey) 277 Poleć książkęKup książkę Programowanie w języku Go. Koncepcje i przykłady if err != nil { return err } err = a.Put(ctx) if err != nil { return err } q.AnswersCount++ err = q.Update(ctx) if err != nil { return err } return nil }, datastore.TransactionOptions{XG: true}) if err != nil { return err } return nil } W powyższej funkcji Create najpierw tworzymy nowy niekompletny klucz (przy użyciu typu Answer), określając przy tym, że jego elementem nadrzędnym jest klucz pytania. Oznacza to, że pytanie stanie się przodkiem wszystkich tych odpowiedzi. Klucze przodków mają specjalne znaczenie w magazynie Google Cloud Datastore i zalecane jest prze- czytanie dokumentacji magazynu w celu zdobycia dodatkowych informacji na temat wszelkich niuansów związanych z ich tworzeniem i stosowaniem. Następnie, używając naszej funkcji UserFromAEUser, pobieramy użytkownika, który odpowiada na pytanie, zapisujemy obiekt UserCard w obiekcie Answer i, podobnie jak wcześniej, w polu CTime zapisujemy bieżącą godzinę. Następnie rozpoczynamy transakcję, wywołując w tym celu funkcję datastore.RunInTransaction. Funkcja ta wymaga przekazania kontekstu oraz funkcji zwrotnej zawierającej cały kod, który ma zostać wykonany w ramach transakcji. Trzecim argumentem wywołania funkcji datastore.Run InTransaction jest struktura datastore.TransactionOptions, której potrzebujemy, by ustawić wartość jej pola XG na true. Pole to informuje magazyn danych o tym, że transakcja będzie obej- mować grupę różnych encji (a konkretnie encji typów Answer oraz Question). Podczas pisania własnych funkcji oraz projektowania własnych API zalecane jest umieszczanie wszelkich argumentów będących funkcjami na końcu listy — w przeciwnym razie definicje funkcji umieszczane bezpośrednio w kodzie, takie jak w przedstawionej powyżej funkcji Create, zaciemniają kod i utrud- niają zauważenie, że wywołanie ma jeszcze jakieś inne argumenty. Patrząc na powyższy przykład, na- prawdę trudno zdać sobie sprawę z faktu, że za funkcją przekazywaną w wywołaniu funkcji RunIn Transaction jest do niej przekazywany jeszcze jeden argument — TransactionOptions; przypusz- czam, że ktoś w firmie Google żałuje tej decyzji. 278 Poleć książkęKup książkę Rozdział 9. • Tworzenie aplikacji pytań i odpowiedzi dla platformy Google Działanie transakcji opiera się na udostępnieniu nam nowego kontekstu, co oznacza, że kod umieszczany wewnątrz funkcji transakcji wygląda dokładnie tak samo jak kod, który nie byłby wykonywany w transakcji. To bardzo miły aspekt projektu API magazynu Google Cloud Datasto- re (który dodatkowo sprawia, że możemy wybaczyć fakt, iż funkcja transakcji nie jest ostatnim argumentem przekazywanym w wywołaniu funkcji RunInTransaction). Wewnątrz funkcji transakcji najpierw wczytujemy pytanie, używając funkcji pomocniczej GetQuestion. To właśnie wczytanie danych w funkcji transakcji jest operacją, która powoduje zablokowanie dostępu do tych danych. Następnie wywołujemy metodę Put, aby zapisać od- powiedź, aktualizujemy wartość pola AnswerCount i w końcu aktualizujemy pytanie. Jeśli wszyst- ko pójdzie dobrze (czyli wykonanie żadnej z tych czynności nie zakończy się zwróceniem błędu), odpowiedź zostanie zapisana w magazynie, a wartość pola AnswerCount — powiększona o jeden. Jeśli jednak którakolwiek z operacji wykonywanych w transakcji zwróci błąd, wszystkie pozo- stałe zostaną anulowane, a funkcja transakcji także zwróci błąd. W takim przypadku funkcja Answer.Create też zwróci błąd, a użytkownik będzie musiał spróbować zapisać odpowiedź jeszcze raz. Naszym kolejnym zadaniem jest napisanie funkcji pomocniczej GetAnswer, która będzie bardzo podobna do funkcji GetQuestion: func GetAnswer(ctx context.Context, answerKey *datastore.Key) (*Answer, error) { var answer Answer err := datastore.Get(ctx, answerKey, answer) if err != nil { return nil, err } answer.Key = answerKey return answer, nil } Kolejną funkcją, którą dodamy do pliku answers.go, będzie funkcja Put, której kod został przedstawiony poniżej: func (a *Answer) Put(ctx context.Context) error { var err error a.Key, err = datastore.Put(ctx, a.Key, a) if err != nil { return err } return nil } Te dwie funkcje są bardzo podobne do przedstawionych wcześniej funkcji GetQuestion oraz Question.Put, jednak na razie warto oprzeć się pokusie wyodrębniania ich w formie nowych abstrakcji i usuwania z kodu wszelkich możliwych powtórzeń. 279 Poleć książkęKup książkę Programowanie w języku Go. Koncepcje i przykłady Unikanie zbyt wczesnego tworzenia abstrakcji Kopiowanie kodu i wklejanie go w innych miejscach jest, ogólnie rzecz biorąc, uważane przez programistów za błąd w sztuce, gdyż zazwyczaj istnieje możliwość wyodrębnienia bardziej ogól- nej idei i uniknięcia wprowadzania do kodu powtórzeń, co odpowiada ogólnie przyjętej zasadzie DRY (ang. Don’t repeat yourself) — nie powtarzaj się. Jednak na razie warto oprzeć się pokusie tworzenia takich abstrakcji, gdyż bardzo łatwo moż- na zrobić to źle, co może być sporym problemem, gdy nasz kod zacznie już zależeć od tych abstrakcji. Znacznie lepiej najpierw powtórzyć kod w kilku miejscach, a dopiero potem do niego wrócić i przekonać się, czy można utworzyć jakieś sensowne abstrakcje. Przeszukiwanie Google Cloud Datastore Dotychczas operacje wykonywane na magazynie Google Cloud Datastore ograniczały się do zapisu lub odczytu pojedynczych obiektów. Jednak w przypadku wyświetlania listy odpowie- dzi na pytanie trzeba będzie w ramach jednej operacji pobrać wszystkie odpowiedzi. Do tego celu można skorzystać z funkcji datastore.Query. Interfejs do wykonywania zapytań jest tak zwanym płynnym API — każda jego metoda zwraca ten sam lub nieco zmieniony obiekt, dzięki czemu wywołania kolejnych metod można łączyć w sekwencję lub łańcuch. Tego rozwiązania można używać do tworzenia zapytań określają- cych sposób sortowania, limity ilości zwracanych danych, przodków, filtry i tak dalej. W na- szej aplikacji skorzystamy z niego do napisania funkcji, która wczyta wszystkie odpowiedzi na konkretne pytanie, przy czym na początku będą wyświetlane najbardziej popularne (mające największą wartość pola Score). Poniżej został przedstawiony kod kolejnej funkcji, którą należy dodać do pliku answers.go: func GetAnswers(ctx context.Context, questionKey *datastore.Key) ([]*Answer, error) { var answers []*Answer log.Debugf(ctx, GetAnswers for s , questionKey) answerKeys, err := datastore.NewQuery( Answer ). Ancestor(questionKey). Order( -Score ). Order( CTime ). GetAll(ctx, answers) log.Debugf(ctx, = s , answerKeys) for i, answer := range answers { answer.Key = answerKeys[i] } if err != nil { return nil, err } return answers, nil } 280 Poleć książkęKup książkę Rozdział 9. • Tworzenie aplikacji pytań i odpowiedzi dla platformy Google Najpierw tworzymy pusty wycinek wskaźników typu Answer, a następnie wywołujemy funkcję datastore.NewQuery, by rozpocząć tworzenie zapytania. Metoda Ancestor oznacza, że poszu- kiwane będą wyłącznie odpowiedzi należące do podanego pytania, natomiast wywołania me- tody Order określają, że odpowiedzi najpierw mają być sortowane według malejącej wartości pola Score, a następnie od najnowszych do najstarszych. Operacja wyszukania jest wykonywana po wywołaniu metody GetAll, która wymaga przekazania wskaźnika do wycinka (w jakim będą zapisywane wyniki) i zwraca nowy wycinek zawierający klucze. Kolejność zwróconych kluczy będzie odpowiadać kolejności encji zapisanych w wycinku przekazanym do metody. Właśnie w ten sposób można się zorientować, który klucz odpowiada któremu elementowi danych. Ponieważ w naszym API klucze i pola encji są przechowywane razem, dlatego przeglądamy wszystkie odpowiedzi i przypisujemy answer.Key do odpowiedniego argumentu datastore.Key zwróconego przez metodę GetAll. Dbając o prostotę pierwszej wersji naszego API, nie zaimplementujemy w nim podziału wyników na strony, choć w optymalnym rozwiązaniu należałoby to zrobić. W przeciwnym razie, kiedy liczba pytań i odpowiedzi wzrośnie, dostarczenie wszystkich odpowiedzi w jednym żądaniu mogłoby przytłoczyć użyt- kownika i znacząco zwiększyć obciążenie serwerów. Gdyby w naszej aplikacji dodawanie odpowiedzi wymagało wcześniejszego uwierzytelnienia (w celu ochrony przed dodawaniem spamu lub nieodpowiednich treści), można by pomyśleć o zastosowaniu dodatkowego filtra, który zwracałby tylko te encje, w których pole Authorized ma wartość true. Oto fragment kodu przedstawiający użycie takiego filtra: datastore.NewQuery( Answer ). Filter( Authorized = , true) Więcej informacji o zapytaniach oraz filtrowaniu danych można znaleźć w internetowej dokumentacji Google Cloud Datastore API. Kolejne miejsce, w którym konieczne jest wyszukanie danych w magazynie, to lista najpopu- larniejszych pytań prezentowana na stronie głównej aplikacji. W pierwszej wersji tej listy będą na niej prezentowane po prostu te pytania, które mają najwięcej odpowiedzi — gdyż uznajemy je za najbardziej interesujące, oczywiście sposób doboru tych pytań można by później dowolnie zmieniać bez modyfikowania publicznego interfejsu naszego API, na przykład wybierać pytania na podstawie wartości pola Score lub nawet liczby wyświetleń. To zapytanie będzie operować na encjach typu Question i używać metody Order, by najpierw posortować pytania na podstawie liczby odpowiedzi (malejąco), a następnie na podstawie czasu dodania (także w odwrotnej kolejności chronologicznej). Dodatkowo w pytaniu zastosujemy metodę Limit, by mieć pewność, że zostanie pobranych jedynie 25 pytań. W kolejnych wer- sjach naszego API, po wprowadzeniu podziału na strony, ta liczba mogłaby być nawet okre- ślana dynamicznie. 281 Poleć książkęKup książkę Pr
Pobierz darmowy fragment (pdf)

Gdzie kupić całą publikację:

Programowanie w języku Go. Koncepcje i przykłady. Wydanie II
Autor:

Opinie na temat publikacji:


Inne popularne pozycje z tej kategorii:


Czytaj również:


Prowadzisz stronę lub blog? Wstaw link do fragmentu tej książki i współpracuj z Cyfroteką: