Go Lang - czas na zmiany post
Czy istnieje świat poza PHPem? Jakie są alternatywy i co to jest w ogóle ten GoLang?
Ostatnie 8 lat komercyjnego kodowania spędziłem w ekosystemie PHPa. To, jaką ewolucję zrobił ten język i społeczność budzi we mnie szacunek. Społeczność OpenSource, wielu programistów, którzy uczynili PHP językiem dla ludzi, językiem dla biznesu. I nie chodzi tutaj o samą składnię, brak silnego typowania etc. A sam sposób pracy, dojrzałość, narzędzia, artykuły, wiedzę, konferencje oraz podejście do wytwarzania kodu.
Warto tutaj nadmienić takie projekty jak:
- PSR - PHP Standards Recommendations - https://www.php-fig.org/psr/
- Symfony - https://github.com/symfony
- Doctrine - https://github.com/doctrine
- Monolog - https://github.com/Seldaek/monolog
oraz wiele, wiele innych ...
Jednakże żyjemy w dobie chmury, projektów serverless. Pomimo, że w PHPie można stworzyć niemalże każdy projekt. Ekosystem PHPa posiada większość alternatywnych rozwiązań do tego co oferuje Java, C# czy NodeJS, to skalowanie aplikacji w PHPie odbywa się na innym poziomie niż w przypadku ww. języków.
Dlaczego szukam alternatywy
Język to nie wszystko, ale:
- Przede wszystkim szukam rozwiązania, które pozwoli mi na budowanie binarek, zamiast robienia i instalacji dziwnych paczek na systemie
- Będzie szybkie w działaniu - tak tzw. "performance" jest bardzo ważny
- Otworzy mi szerszy zakres możliwości technologicznych oraz biznesowych:
- Micro-services
- Serverless
- Big Data
- Machine Learning
- Wprowadzi powiew świeżości do moich codziennych obowiązków
Czas na zmiany
Podczas podróży świątecznej do rodziny, siedząc na fotelu pasażera, zamiast robić kolejny serwis w PHPie lub TypeScript'cie, postanowiłem coś zmienić. I choć co nie co znam Javę, to szukałem lżejszej alternatywy. Wybór padł na Go.
W związku z tym, że kiedyś już próbowałem swoich sił z Go przejście przewodnika, którego oferują autorzy języka pod adresem https://tour.golang.org/ zajęło mi niewiele czasu.
Po odświeżeniu podstawowej wiedzy z języka postanowiłem:
- zaktualizować wersję Go
- zainstalować dodatki do Go dla IDE (IntelliJ)
- oraz napisać pierwszy program w Go.
Nie będzie to kolejny "Hello World".
Był to zlepek różnych możliwości języka:
- struktury
- interfejsy
- metody
- funkcje
- wskaźniki
- kawałki (sic!) - slices
- mapy
Po przypomnieniu składni i podstawowych możliwości czas na coś ambitniejszego.
Pierwszy sensowny program
W jednym z projektów potrzebuję serwisu, który potrafi przetworzyć adres pisany słownie na pozycję geograficzną. Postanowiłem, że napiszę ten serwis właśnie w Go.
Pierwsza wersja programu z komentarzami poniżej:
// nazwa pakietu
package main
// import zależności
import (
"encoding/json"
"io"
"io/ioutil"
"log"
"net/http"
)
// stała z adresem URL do api
const NOMINATIM_API_URL string = "https://nominatim.openstreetmap.org/search";
// struktura odpowiedzi z api
// warto zwrócić uwagę w ty miejscu, że LookupResult nie jest pojedynczym wpisem,
// a w nomenklaturze ogólnej jest tablicą (kolekcją)
type LookupResult []struct {
PlaceID string `json:"place_id"`
Licence string `json:"licence"`
OsmType string `json:"osm_type"`
OsmID string `json:"osm_id"`
Boundingbox []string `json:"boundingbox"`
Lat string `json:"lat"`
Lon string `json:"lon"`
DisplayName string `json:"display_name"`
Class string `json:"class"`
Type string `json:"type"`
Importance float64 `json:"importance"`
Icon string `json:"icon"`
}
// funkcja odpytując API
// jak widać przyjume ona `query` jako `string` i zwraca strukturę `LookupResult`
func requestNominatim(query string) (result LookupResult) {
client := &http.Client{}
req, _ := http.NewRequest("GET", NOMINATIM_API_URL, nil);
queryParams := req.URL.Query();
queryParams.Add("q", query);
queryParams.Add("format", "json");
req.URL.RawQuery = queryParams.Encode();
res, err := client.Do(req);
if err != nil {
log.Fatal(err)
}
defer res.Body.Close();
body, _ := ioutil.ReadAll(res.Body)
if err := json.Unmarshal(body, &result); err != nil {
log.Fatal(err)
}
return result
}
// handler do rozwiązywania naszego zapytania
func addressLookup(w http.ResponseWriter, r *http.Request) {
// odpytaj API nominatim
lookup := requestNominatim(r.URL.Query().Get("query"))
// serializuj strukturę do JSONa
res, _ := json.Marshal(lookup)
// wypluj JSONa na ekran ;-)
io.WriteString(w, string(res))
}
func main() {
// odpalenie wbudowanego serwera HTTP
mux := http.NewServeMux()
mux.HandleFunc("/", addressLookup)
http.ListenAndServe(":8000", mux)
}
Choć nie obyło się bez problemów, to Go mnie bardzo pozytywnie zaskoczyło. Od razu polubiłem wbudowane wsparcie dla JSON (serializacja i deserializacja). W przypadku PHP skorzystalibyśmy z takich paczek jak:
- JMS Serialize
- Symfony Serializer
lub po prostu sami napisali mapper dla popularnego json_decode($data, true)
.
Jak można zauważyć to funkcja main()
używa dostarczonego przez twórców języka serwera HTTP.
Tak jak w przypadku PHPa dostajemy wbudowany serwer HTTP php -S localhost:8000 address_lookup.php
.
Jego odpalenie oraz przypięcie handlera dla danej ścieżki do zaledwie 3 linie kodu.
Przerobienie aplikacji na gRPC
O gRPC dowiedziałem się od Pawła kilka lat temu. Sam temat ucichł na jakiś czas. Potrzebowałem trochę czasu na "uleżenie się tematu". Jednak dzięki temu, przy kolejnej próbie sporo dowiedziałem się o gRPC. Tym doświadczeniem podzieliłem się z rzeszowską społecznością podczas rg-dev, a slajdy wrzuciłem na tutaj.
W związku z pracą w ekosystemie PHPa, brakiem obsługi części serwerowej gRPC w PHPie oraz specyfiką pracy (głównie support legacy apps) byłem zmuszony do odłożenia tematu gRPC na półkę. Jednak jak mawiają:
Co się odwlecze, to nie uciecze.
Postanowiłem, że nowy serwis do geokodowania adresów będzie używał tych narzędzi. Zacząłem od definicji serwisu i wiadomości w formacie Proto Buffers.
syntax = "proto3";
package it.kruczek.address_lookup;
message LookupRequest {
string query = 1;
}
message LookupResponse {
string display_name = 1;
float latitude = 2;
float longitude = 3;
message Address {
string city = 1;
string county = 2;
string state = 3;
string country = 4;
string country_code = 5;
string postcode = 6;
}
Address address = 4;
}
service AddressLookupService {
rpc lookup (LookupRequest) returns (LookupResponse) {
}
}
Sama definicja serwisu jest krótka, zawiera tylko jedną metodę, która przyjmuje wiadomość zawierającą query
tj. nasze zapytanie, a na wyjściu dostaje odpowiedź zawierającą dane na temat adresu.
Przejdźmy już do samej implementacji. Ale zanim spojrzymy na kod, ważna informacja:
- kod jest tylko poglądowy,
- nie uda się go skompilować i odpalić,
- bez prywatnych zależności nic z nim nie zrobimy
package main
import (
"context"
"errors"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
"log"
"net"
pb "kruczek.it/address_lookup/proto"
"strconv"
)
// definicja serwisu
type LookupService struct {}
// implementacja metody serwisu zgodnie z definicją interfejsu wygenerowaną przez gRPC
func (s *LookupService) Lookup(ctx context.Context, req *pb.LookupRequest) (*pb.LookupResponse, error) {
// odpytanie API Nominatim
results := resolveOSM(req.Query)
// sprawdzenie czy API zwróciło wyniki
if len(results) == 0 {
return nil, errors.New("Lookup failed.")
}
// przemapowanie odpowiedzi
response, nil := createResponseFromQueryResults(results)
s.repo.store(req, response)
return response, nil
}
// przemapowanie wyników zwracanych przez Nominatim na nasz format wiadomości zgodny z gRPC
func createResponseFromQueryResults(results []QueryResult) (*pb.LookupResponse, error) {
firstResult := results[0]
lat, _ := strconv.ParseFloat(firstResult.Latitude, 32);
lon, _ := strconv.ParseFloat(firstResult.Longitude, 32);
address := &pb.LookupResponse_Address{
City: firstResult.Address.City,
County: firstResult.Address.County,
State: firstResult.Address.State,
Country: firstResult.Address.Country,
CountryCode: firstResult.Address.CountryCode,
Postcode: firstResult.Address.PostCode,
}
response := &pb.LookupResponse{
DisplayName: firstResult.DisplayName,
Latitude: float32(lat),
Longitude: float32(lon),
Address: address,
}
return response, nil
}
func main() {
lis, err := net.Listen("tcp", "50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterAddressLookupServiceServer(s, &LookupService{})
reflection.Register(s)
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
Sama obsługa API Nominatim jest lekko przerobioną wersją programu pierwszego.
Podsumowanie
Powyższa implementacja niestety ma jeden minus. Bez lokalnego zbiornika danych (storage'a), który będzie cache'ował nasze zapytania wąskim gardłem szybko okaże się oczywiście API od Nominatim. Dlatego implementacja musi zostać poprawiona o:
- dodanie repozytorium dla odpowiedzi oraz implementacji "InMemory"
- dodanie nośnika danych (patrz storage)
- dodanie do serwisu nowej metody
suggestions
, która na podstawie zapytania da nam X potencjalnych odpowiedzi dla usługi podpowiadania
Podczas całego procesu miałem kilka problemów, w których pomogła mi społeczność:
- struktura katalogów oraz
GOPATH
i lokalny ekosystem Go - kwestie te możemy zgłębić na stronie https://golang.org/doc/code.html , ale warto także dołączyć do społeczności Go na Slacku mono repo i Go - tutaj sprawa się komplikuje, dlatego ja mój kod źródłowy trzymam w katalogu projektu, a następnie symlinkuję go odpowiedniego katalogu w
GOPATH
.PS Uważajcie, bo fish automatycznie zmienia katalog symlinka na docelowy, a wtedy znajdziecie się poza GOPATHem.
Linki
Go
- https://golang.org/
- https://tour.golang.org/
- https://www.jetbrains.com/go/
- https://github.com/a8m/go-lang-cheat-sheet
- https://golang.github.io/dep/
- https://golang.org/doc/code.html
- https://invite.slack.golangbridge.org