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:

  1. Przede wszystkim szukam rozwiązania, które pozwoli mi na budowanie binarek, zamiast robienia i instalacji dziwnych paczek na systemie
  2. Będzie szybkie w działaniu - tak tzw. "performance" jest bardzo ważny
  3. Otworzy mi szerszy zakres możliwości technologicznych oraz biznesowych:
    • Micro-services
    • Serverless
    • Big Data
    • Machine Learning
  4. 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

Extra

Kategorie: devops, language, learning

Tagi: go, dev, lang, language, golang