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)