Nová funkcionalita v Go 1.20: detekce skutečně volaných řádků v programovém kódu

23. 2. 2023
Doba čtení: 24 minut

Sdílet

 Autor: Depositphotos
V Go verze 1.20 se objevila poměrně dlouho očekávaná funkcionalita. Jedná se o relativně snadno použitelnou technologii umožňující detekci skutečně volaných řádků a bloků v programovém kódu.

Obsah

1. Nová funkcionalita v Go 1.20: detekce skutečně volaných řádků v programovém kódu

2. Nástroj cover a jednotkové testy

3. Jednoduchá webová služba a její jednotkové testy

4. Zjištění pokrytí kódu jednotkovými testy

5. Výpis informací o pokrytí kódu jednotkovými testy

6. Vygenerování upraveného zdrojového kódu aplikace s explicitními testy pokrytí

7. Ukázka transformace kódu nástrojem cover

8. Od jednotkových testů k detekci řádků, které jsou spuštěny při reálném běhu aplikace

9. Složitější aplikace s přepínači zadávanými na příkazovém řádku

10. Projektový soubor testované aplikace

11. Překlad aplikace s přidáním „sledovacího“ kódu

12. Spuštění aplikace s uvedením několika přepínačů na příkazovém řádku (různé režimy činnosti)

13. Spojení a konverze automaticky vytvořených souborů s informacemi o činnosti aplikace

14. Výpis informací bězích aplikace na úrovni funkcí a metod

15. Vizualizace volaných a nevolaných řádků na webové stránce

16. Závěrečné zhodnocení

17. Repositář s demonstračními příklady

18. Odkazy na Internetu

1. Nová funkcionalita v Go 1.20: detekce skutečně volaných řádků v programovém kódu

V Go verze 1.20 se objevila poměrně dlouho očekávaná funkcionalita. Jedná se o relativně snadno využitelnou technologii umožňující detekci skutečně volaných řádků v programovém kódu. Díky této nové funkcionalitě je tedy možné například detekovat sémanticky „mrtvý“ kód, kód, jenž je použit jen ve specifických případech atd. Při bližším pohledu zjistíme, že se vlastně ve skutečnosti nejedná o zcela novou technologii, protože již v prvních verzích programovacího jazyka Go měli programátoři možnost zjistit pokrytí kódu testy, což ovšem byla technologie do značné míry svázaná s nástrojem go test a taktéž se standardním balíčkem testing. Použití mimo takto definované mantinely bylo velmi složité a mnohdy i nemožné.

Není tedy divu, že praktické využití této technologie bylo omezeno na jednotkové testy (unit tests) popř. pro testy komponent (ovšem za předpokladu, že testy komponent jsou založeny na výše zmíněném balíčku testing). S příchodem Go verze 1.20 je nově umožněno například spustit backend naprogramovaný v jazyce Go, provést několik operací na frontendu (UI) a následně zjistit, které části backendu byly v tomto konkrétním případě využity. Totéž je pochopitelně možné provést i v případě mikroslužeb (bez frontendu) atd. atd. – nyní se již žádné překážky nekladou, jak ostatně uvidíme v dalším textu.

Poznámka: detekce, které části kódu byly volány, se provádí na úrovni funkcí, metod a programových bloků. To tedy znamená, že může dojít k situaci, kdy je nějaká funkce volána, ovšem některých z programových bloků uvnitř této funkce volán není (typicky se jedná o blok reagující na chybu).

2. Nástroj cover a jednotkové testy

Jak jsme si již řekli v úvodní kapitole, bylo zjištění, které části programového kódu jsou volány, v předchozích verzích jazyka Go omezeno na jednotkové testy, resp. přesněji řečeno na standardní balíček testing a na něj navazujícího nástroje cover. Pro úplnost si tedy v dalších kapitolách ukážeme dnes již zcela standardní způsob použití balíčku testing, a to včetně zobrazení (vizualizace) těch řádků v programovém kódu, které jsou jednotkovými testy přímo či nepřímo volány (tedy „zasaženy“). Jak uvidíme dále, je výsledkem této analýzy buď výstup ve formě seznamu funkcí a metod s procentuálním výpisem pokrytí testy, popř. HTML stránka (stránky) s podrobnějšími informacemi o tom, které programové řádky byly testy „zasaženy“ a které nikoli (samozřejmě se ignorují řádky bez spustitelného kódu).

3. Jednoduchá webová služba a její jednotkové testy

Nejprve si ukažme kód HTTP serveru, který budeme chtít testovat. Tento server po svém spuštění poskytuje statické soubory umístěné v aktuálním adresáři a na endpointech /data a /other odpovídá posláním odpovědi s nastaveným typem „application/json“. V obou případech je kód odpovědi 200 OK (a ve skutečnosti druhý handler nevrací validní JSON). Zdrojový kód tohoto HTTP serveru naleznete na adrese https://github.com/tisnik/go-root/blob/master/article_A5/httpSer­ver/httpServer1.go

package main
 
import (
        "fmt"
        "log"
        "net/http"
)
 
func dataHandler(writer http.ResponseWriter, request *http.Request) {
        writer.Header().Set("Content-Type", "application/json")
        writer.WriteHeader(http.StatusOK)
        fmt.Fprintf(writer, `"x": [1, 2, 3, 4, 5]`)
}
 
func otherHandler(writer http.ResponseWriter, request *http.Request) {
        writer.Header().Set("Content-Type", "application/json")
        writer.WriteHeader(http.StatusOK)
        fmt.Fprintf(writer, `foobar`)
}
 
func startHttpServer(address string) {
        log.Printf("Starting server on address %s", address)
        http.Handle("/", http.FileServer(http.Dir(".")))
        http.HandleFunc("/data", dataHandler)
        http.HandleFunc("/other", otherHandler)
        http.ListenAndServe(address, nil)
}
 
func main() {
        startHttpServer(":8080")
}

Typickou úlohou je otestování funkcionality jednotlivých handlerů. Realizaci si ukážeme na jednoduchém testu pro handler obsluhující endpoint /data. Nejdříve vytvoříme objekt realizující dotaz provedený HTTP metodou GET:

request, err := http.NewRequest("GET", "/data", nil)
if err != nil {
        t.Fatal(err)
}

Dále vytvoříme objekt, který bude zaznamenávat provedené operace:

recorder := httptest.NewRecorder()

Třetím a posledním objektem je adaptér umožňující použít libovolnou funkci s příslušnou signaturou jako handler HTTP serveru:

handler := http.HandlerFunc(dataHandler)

Nyní spustíme „záznam“ činnosti HTTP serveru pro již dříve vytvořený dotaz (HTTP GET na endpointu /data):

handler.ServeHTTP(recorder, request)

Celý průběh se zaznamená, což znamená, že později můžeme činnost handleru prozkoumat čtením atributů struktury recorder.

Otestování HTTP kódu odpovědi (očekáváme 200 OK):

if status := recorder.Code; status != http.StatusOK {
        t.Errorf("improper status code: got %v instead of %v",
                status, http.StatusOK)
}

Otestování, zda odpověď obsahuje hlavičku „Content-Type“ s očekávaným obsahem „application/json“:

if ctype := recorder.Header().Get("Content-Type"); ctype != "application/json" {
        t.Errorf("content type header does not match: got %s want %s",
                ctype, "application/json")
}

A pochopitelně můžeme přistupovat i k datům poslaným v těle odpovědi:

body := recorder.Body.String()
if body != `"x": [1, 2, 3, 4, 5]` {
        t.Errorf("wrong response body: %s", body)
}

Úplný zdrojový kód jednotkového testu je umístěn na adrese: https://github.com/tisnik/go-root/blob/master/article_A5/httpSer­ver/httpServer1_test.go

package main
 
import (
        "net/http"
        "net/http/httptest"
        "testing"
)
 
func TestDataHandler(t *testing.T) {
        request, err := http.NewRequest("GET", "/data", nil)
        if err != nil {
                t.Fatal(err)
        }
 
        recorder := httptest.NewRecorder()
        handler := http.HandlerFunc(dataHandler)
 
        handler.ServeHTTP(recorder, request)
 
        if status := recorder.Code; status != http.StatusOK {
                t.Errorf("improper status code: got %v instead of %v",
                        status, http.StatusOK)
        }
 
        body := recorder.Body.String()
        if body != `"x": [1, 2, 3, 4, 5]` {
                t.Errorf("wrong response body: %s", body)
        }
 
        if ctype := recorder.Header().Get("Content-Type"); ctype != "application/json" {
                t.Errorf("content type header does not match: got %s want %s",
                        ctype, "application/json")
        }
}

4. Zjištění pokrytí kódu jednotkovými testy

Jednotkové testy pro naši implementaci služby spustíme příkazem go test:

$ go help test
 
usage: go test [build/test flags] [packages] [build/test flags & test binary flags]
 
'Go test' automates testing the packages named by the import paths.
It prints a summary of the test results in the format:
 
        ok   archive/tar   0.011s
        FAIL archive/zip   0.022s
        ok   compress/gzip 0.033s
        ...
 
followed by detailed output for each failed package.
 
...
...
...

Ovšem navíc budeme specifikovat, že je nutné zjistit pokrytí kódu testy a současně uložit naměřená data do souboru nazvaného „coverage.out“. K oběma zmíněným účelům slouží přepínač -coverprofile:

$ go test -coverprofile coverage.out

Výsledky budou vypadat následovně:

PASS
coverage: 25.0% of statements
ok      _/home/ptisnovs/src/go-root/article_A5/httpServer       0.003s

Soubor „coverage.out“ obsahuje informace o spuštěných programových řádcích v průběhu testů; současně se ovšem jedná o soubor, který nebudeme přímo dekódovat, ale použijeme na to k tomu určené nástroje zmíněné v navazující kapitole:

mode: set
/home/ptisnovs/src/go-root/article_A5/httpServer/httpServer1.go:18.69,22.2 3 1
/home/ptisnovs/src/go-root/article_A5/httpServer/httpServer1.go:24.70,28.2 3 0
/home/ptisnovs/src/go-root/article_A5/httpServer/httpServer1.go:30.38,36.2 5 0
/home/ptisnovs/src/go-root/article_A5/httpServer/httpServer1.go:38.13,40.2 1 0

5. Výpis informací o pokrytí kódu jednotkovými testy

V této kapitole využijeme standardní nástroj cover, jenž se volá příkazem go tool cover a který akceptuje různé parametry vypsané v nápovědě:

$ go tool cover
 
Usage of 'go tool cover':
Given a coverage profile produced by 'go test':
        go test -coverprofile=c.out
 
Open a web browser displaying annotated source code:
        go tool cover -html=c.out
 
Write out an HTML file instead of launching a web browser:
        go tool cover -html=c.out -o coverage.html
 
Display coverage percentages to stdout for each function:
        go tool cover -func=c.out
 
Finally, to generate modified source code with coverage annotations
(what go test -cover does):
        go tool cover -mode=set -var=CoverageVariableName program.go
 
Flags:
  -V    print version and exit
  -func string
        output coverage profile information for each function
  -html string
        generate HTML representation of coverage profile
  -mode string
        coverage mode: set, count, atomic
  -o string
        file for output; default: stdout
  -var string
        name of coverage variable to generate (default "GoCover")
 
  Only one of -html, -func, or -mode may be set.

Z vytvořeného souboru „coverage.out“ vytvoříme čitelný výpis s informacemi o tom, jaké funkce HTTP serveru byly skutečně otestovány:

$ go tool cover -func=coverage.out

Výsledek by mohl vypadat následovně (cesty se samozřejmě budou ve vašem případě odlišovat, ovšem čísla řádků, jména funkcí a konkrétní naměřené hodnoty budou shodné):

/home/ptisnovs/src/go-root/article_A5/httpServer/httpServer1.go:18:     dataHandler     100.0%
/home/ptisnovs/src/go-root/article_A5/httpServer/httpServer1.go:24:     otherHandler    0.0%
/home/ptisnovs/src/go-root/article_A5/httpServer/httpServer1.go:30:     startHttpServer 0.0%
/home/ptisnovs/src/go-root/article_A5/httpServer/httpServer1.go:38:     main            0.0%
total:                                                                  (statements)    25.0%

Vidíme, že handler realizovaný funkcí dataHandler je skutečně plně pokryt testy, na rozdíl od ostatního programového kódu.

Navíc si můžeme nechat zobrazit HTML stránku (stránky), na nichž bude zvýrazněn kód pokrytý testy, ostatní programový kód a řádky, které programový kód netvoří:

$ go tool cover -html=coverage.out

S tímto výsledkem:

Obrázek 1: Vizualizace pokrytí programového kódu jednotkovými testy.

6. Vygenerování upraveného zdrojového kódu aplikace s explicitními testy pokrytí

Nástroj cover navíc dokáže transformovat zvolený zdrojový kód takovým způsobem, že do něj přidá nový kód testující vstup do zvolené funkce, metody nebo programového bloku. Tento nový kód používá strukturu se jménem specifikovaným na příkazovém řádku, přičemž nový kód může pouze nastavovat příznak přístupu do funkce nebo programového bloku (tedy „vstoupilo se/nevstoupilo se“) nebo může příslušný čítač zvýšit o jedničku (tedy můžeme testovat, kolikrát byla funkce nebo programový blok volán) nebo je zvýšení hodnoty čítače realizováno atomickou operací (použití v gorutinách atd.). Pro generování transformovaného kódu jsou k dispozici tyto přepínače předávané nástroji cover:

Přepínač Stručný popis
-mode=set ve vygenerované části kódu se nastavuje příznak vstupu do funkce/bloku
-mode=count ve vygenerované části kódu se zvyšuje hodnota čítače vstupu do funkce/bloku
-mode=atomic podobné předchozímu, ovšem čítače jsou realizovány atomicky měnitelnými hodnotami
-var=xyz jméno datové struktury s čítači

7. Ukázka transformace kódu nástrojem cover

Podívejme se pro zajímavost, jak vlastně vypadají zdrojové kódy transformované nástrojem cover. Nejdříve si necháme vytvořit kód se „sledovací“ strukturou, jejíž prvky se nastavují na 0 nebo na 1, podle toho, zda je nějaký blok kódu volán či nikoli. Tento kód se vytvoří příkazem:

$ go tool cover -mode=set -var=coverageVariable httpServer1.go

Výsledkem bude následující kód, jehož upravené (přidané) části jsou zvýrazněny:

package main
 
import (
        "fmt"
        "log"
        "net/http"
)
 
func dataHandler(writer http.ResponseWriter, request *http.Request) {coverageVariable.Count[0] = 1;
        writer.Header().Set("Content-Type", "application/json")
        writer.WriteHeader(http.StatusOK)
        fmt.Fprintf(writer, `"x": [1, 2, 3, 4, 5]`)
}
 
func otherHandler(writer http.ResponseWriter, request *http.Request) {coverageVariable.Count[1] = 1;
        writer.Header().Set("Content-Type", "application/json")
        writer.WriteHeader(http.StatusOK)
        fmt.Fprintf(writer, `foobar`)
}
 
func startHttpServer(address string) {coverageVariable.Count[2] = 1;
        log.Printf("Starting server on address %s", address)
        http.Handle("/", http.FileServer(http.Dir(".")))
        http.HandleFunc("/data", dataHandler)
        http.HandleFunc("/other", otherHandler)
        http.ListenAndServe(address, nil)
}
 
func main() {coverageVariable.Count[3] = 1;
        startHttpServer(":8080")
}
 
var coverageVariable = struct {
        Count     [4]uint32
        Pos       [3 * 4]uint32
        NumStmt   [4]uint16
} {
        Pos: [3 * 4]uint32{
                18, 22, 0x20045, // [0]
                24, 28, 0x20046, // [1]
                30, 36, 0x20026, // [2]
                38, 40, 0x2000d, // [3]
        },
        NumStmt: [4]uint16{
                3, // 0
                3, // 1
                5, // 2
                1, // 3
        },
}
Poznámka: povšimněte si, že nová struktura je dobře připravena pro vygenerování výsledného reportu.

Druhý zdrojový kód byl získán přidáním čítačů do původního kódu. Pro jeho vygenerování použijeme tento příkaz:

$ go tool cover -mode=count -var=coverageVariable httpServer1.go

Výsledek je podobný předchozím výsledku, až na odlišné chování – při vstupu do testovaných funkcí se hodnota čítačů zvyšuje o jedničku a nikoli přímo nastavuje na jedničku, takže lze snadno získat přehled o počtu volání:

package main
 
import (
        "fmt"
        "log"
        "net/http"
)
 
func dataHandler(writer http.ResponseWriter, request *http.Request) {coverageVariable.Count[0]++;
        writer.Header().Set("Content-Type", "application/json")
        writer.WriteHeader(http.StatusOK)
        fmt.Fprintf(writer, `"x": [1, 2, 3, 4, 5]`)
}
 
func otherHandler(writer http.ResponseWriter, request *http.Request) {coverageVariable.Count[1]++;
        writer.Header().Set("Content-Type", "application/json")
        writer.WriteHeader(http.StatusOK)
        fmt.Fprintf(writer, `foobar`)
}
 
func startHttpServer(address string) {coverageVariable.Count[2]++;
        log.Printf("Starting server on address %s", address)
        http.Handle("/", http.FileServer(http.Dir(".")))
        http.HandleFunc("/data", dataHandler)
        http.HandleFunc("/other", otherHandler)
        http.ListenAndServe(address, nil)
}
 
func main() {coverageVariable.Count[3]++;
        startHttpServer(":8080")
}
 
var coverageVariable = struct {
        Count     [4]uint32
        Pos       [3 * 4]uint32
        NumStmt   [4]uint16
} {
        Pos: [3 * 4]uint32{
                18, 22, 0x20045, // [0]
                24, 28, 0x20046, // [1]
                30, 36, 0x20026, // [2]
                38, 40, 0x2000d, // [3]
        },
        NumStmt: [4]uint16{
                3, // 0
                3, // 1
                5, // 2
                1, // 3
        },
}

A konečně poslední příklad ukazuje, jak se namísto běžných čítačů (tj. prvků typu uint32) používají atomické čítače. Kód tedy bude připraven pro běh ve více gorutinách:

$ go tool cover -mode=atomic -var=coverageVariable httpServer1.go

Podívejme se na výsledek:

package main; import _cover_atomic_ "sync/atomic"
 
import (
        "fmt"
        "log"
        "net/http"
)
 
func dataHandler(writer http.ResponseWriter, request *http.Request) {_cover_atomic_.AddUint32(&coverageVariable.Count[0], 1);
        writer.Header().Set("Content-Type", "application/json")
        writer.WriteHeader(http.StatusOK)
        fmt.Fprintf(writer, `"x": [1, 2, 3, 4, 5]`)
}
 
func otherHandler(writer http.ResponseWriter, request *http.Request) {_cover_atomic_.AddUint32(&coverageVariable.Count[1], 1);
        writer.Header().Set("Content-Type", "application/json")
        writer.WriteHeader(http.StatusOK)
        fmt.Fprintf(writer, `foobar`)
}
 
func startHttpServer(address string) {_cover_atomic_.AddUint32(&coverageVariable.Count[2], 1);
        log.Printf("Starting server on address %s", address)
        http.Handle("/", http.FileServer(http.Dir(".")))
        http.HandleFunc("/data", dataHandler)
        http.HandleFunc("/other", otherHandler)
        http.ListenAndServe(address, nil)
}
 
func main() {_cover_atomic_.AddUint32(&camp;overageVariable.Count[3], 1);
        startHttpServer(":8080")
}
 
var coverageVariable = struct {
        Count     [4]uint32
        Pos       [3 * 4]uint32
        NumStmt   [4]uint16
} {
        Pos: [3 * 4]uint32{
                18, 22, 0x20045, // [0]
                24, 28, 0x20046, // [1]
                30, 36, 0x20026, // [2]
                38, 40, 0x2000d, // [3]
        },
        NumStmt: [4]uint16{
                3, // 0
                3, // 1
                5, // 2
                1, // 3
        },
}
var _ = _cover_atomic_.LoadUint32

8. Od jednotkových testů k detekci řádků, které jsou spuštěny při reálném běhu aplikace

V předchozím textu jsme si ukázali, jakým způsobem je možné zjistit pokrytí zdrojových kódů (code coverage) jednotkovými testy (unit tests) a taktéž to, že samotné zdrojové kódy je možné transformovat takovým způsobem, že se na důležitá místa v kódu (začátky funkcí a metod, vstupy do programových bloků) explicitně vloží nové příkazy určené pro zjištění, které části programu jsou vlastně použity (ovšem samotný export dat atd. je již ponechán na vývojáři). V jazyce Go verze 1.19 i ve všech starších variantách jazyka Go však nebyla k dispozici žádná jednoduše použitelná technologie, která by oba přístupy sjednotila tak, aby bylo možné aplikaci spustit (klidně i vícekrát) a posléze získat informaci o tom, do kterých funkcí/metod/bloků se skutečně vstoupilo. Tato technologie byla přidána až do Go 1.20 a způsob jejího využití si ukážeme na nepatrně složitějším demonstračním příkladu (který částečně vychází ze zdrojových kódů reálné aplikace).

9. Složitější aplikace s přepínači zadávanými na příkazovém řádku

Aplikace, kterou přeložíme, spustíme a budeme analyzovat v navazujících kapitolách, podporuje přepínače (flags) nastavované na příkazovém řádku. Tyto přepínače slouží pro spuštění některé části aplikace, popř. pro spuštění HTTP služby (samotná implementace této služby je vynechána – zbytečně by celý programový kód ještě více prodlužovala). Taktéž je možné, aby se aplikace pouze pokusila připojit k Apache Kafce atd. – jedná se tedy o aplikaci, která se po spuštění může ubírat několika větvemi. Ty se budeme snažit detekovat nástrojem cover:

package main
 
import (
        "flag"
        "fmt"
        "net/http"
        "os"
 
        "github.com/Shopify/sarama"
 
        "github.com/prometheus/client_golang/prometheus/promhttp"
 
        "github.com/rs/zerolog/log"
)
 
// Messages to be displayed on terminal or written into logs
const (
        versionMessage                 = "Spejbl version 1.0"
        authorsMessage                 = "Pavel Tisnovsky, Red Hat Inc."
        connectionToBrokerMessage      = "Connection to broker"
        operationFailedMessage         = "Operation failed"
        notConnectedToBrokerMessage    = "Not connected to broker"
        brokerConnectionSuccessMessage = "Broker connection OK"
)
 
// Exit codes
const (
        // ExitStatusOK means that the tool finished with success
        ExitStatusOK = iota
        // ExitStatusConsumerError is returned in case of any consumer-related error
        ExitStatusConsumerError
        // ExitStatusKafkaError is returned in case of any Kafka-related error
        ExitStatusKafkaError
        // ExitStatusHTTPServerError is returned in case the HTTP server can not be started
        ExitStatusHTTPServerError
)
 
// CliFlags represents structure holding all command line arguments and flags.
type CliFlags struct {
        CheckConnectionToKafka bool
        ShowVersion            bool
        ShowAuthors            bool
}
 
type ConfigStruct struct {
        // Address represents Kafka address
        Address string `mapstructure:"address" toml:"address"`
        // SecurityProtocol represents the security protocol used by the broker
        SecurityProtocol string `mapstructure:"security_protocol" toml:"security_protocol"`
        //      CertPath is the path to a file containing the certificate to be used with the broker
        CertPath string `mapstructure:"cert_path" toml:"cert_path"`
        // SaslMechanism is the SASL mechanism used for authentication
        SaslMechanism string `mapstructure:"sasl_mechanism" toml:"sasl_mechanism"`
        // SaslUsername is the username used in case of PLAIN mechanism
        SaslUsername string `mapstructure:"sasl_username" toml:"sasl_username"`
        // SaslPassword is the password used in case of PLAIN mechanism
        SaslPassword string `mapstructure:"sasl_password" toml:"sasl_password"`
        // Topic is name of Kafka topic
        Topic string `mapstructure:"topic" toml:"topic"`
        // Group is name of Kafka group
        Group string `mapstructure:"group" toml:"group"`
        // Enabled is set to true if Kafka consumer is to be enabled
        Enabled bool `mapstructure:"enabled" toml:"enabled"`
 
        MetricsAddress string
}
 
// showVersion function displays version information to standard output.
func showVersion() {
        fmt.Println(versionMessage)
}
 
// showAuthors function displays information about authors to standard output.
func showAuthors() {
        fmt.Println(authorsMessage)
}
 
// tryToConnectToKafka function just tries to establish connection to Kafka
// broker
func tryToConnectToKafka(configuration *ConfigStruct) (int, error) {
        log.Info().Msg("Checking connection to Kafka")
 
        // display basic info about broker that will be used
        log.Info().
                Str("broker address", configuration.Address).
                Msg("Broker address")
 
        // create new broker instance (w/o any checks)
        broker := sarama.NewBroker(configuration.Address)
 
        // check broker connection
        err := broker.Open(nil)
        if err != nil {
                log.Error().Err(err).Msg(connectionToBrokerMessage)
                return ExitStatusKafkaError, err
        }
 
        // check if connection remain
        connected, err := broker.Connected()
        if err != nil {
                log.Error().Err(err).Msg(connectionToBrokerMessage)
                return ExitStatusKafkaError, err
        }
        if !connected {
                log.Error().Err(err).Msg(notConnectedToBrokerMessage)
                return ExitStatusConsumerError, err
        }
 
        // connection was established
        log.Info().Msg(brokerConnectionSuccessMessage)
 
        // everything seems to be ok
        return ExitStatusOK, nil
}
 
// startService function tries to start the notification writer service,
// connect to storage and initialize connection to message broker.
func startService(configuration *ConfigStruct) (int, error) {
        // prepare HTTP server with metrics exposed
        err := startHTTPServer(configuration.MetricsAddress)
        if err != nil {
                log.Error().Err(err)
                return ExitStatusHTTPServerError, err
        }
 
        return ExitStatusOK, nil
}
 
// startHTTP server starts HTTP or HTTPS server with exposed metrics.
func startHTTPServer(address string) error {
        // setup handlers
        http.Handle("/metrics", promhttp.Handler())
 
        // start the server
        log.Info().Str("HTTP server address", address).Msg("Starting HTTP server")
        err := http.ListenAndServe(address, nil) // #nosec G114
        if err != nil {
                log.Error().Err(err).Msg("Listen and serve")
                return err
        }
        return nil
}
 
func doSelectedOperation(configuration *ConfigStruct, cliFlags CliFlags) (int, error) {
        switch {
        case cliFlags.ShowVersion:
                showVersion()
                return ExitStatusOK, nil
        case cliFlags.ShowAuthors:
                showAuthors()
                return ExitStatusOK, nil
        case cliFlags.CheckConnectionToKafka:
                return tryToConnectToKafka(configuration)
        default:
                exitCode, err := startService(configuration)
                return exitCode, err
        }
        // this can not happen: return ExitStatusOK, nil
}
 
// main function is entry point to the Notification writer service.
func main() {
        var cliFlags CliFlags
 
        // define and then parse all command line options
        flag.BoolVar(&cliFlags.CheckConnectionToKafka, "check-kafka", false, "check connection to Kafka")
        flag.BoolVar(&cliFlags.ShowVersion, "version", false, "show version")
        flag.BoolVar(&cliFlags.ShowAuthors, "authors", false, "show authors")
        flag.Parse()
 
        configuration := ConfigStruct{}
 
        // perform selected operation
        exitStatus, err := doSelectedOperation(&configuration, cliFlags)
        if err != nil {
                log.Err(err).Msg("Do selected operation")
                os.Exit(exitStatus)
                return
        }
 
        log.Debug().Msg("Finished")
}

10. Projektový soubor testované aplikace

Pro úplnost se podívejme na to, jak vypadá projektový soubor testované aplikace, tedy soubor se jménem go.mod:

module app
 
go 1.20
 
require (
        github.com/Shopify/sarama v1.35.0
        github.com/prometheus/client_golang v1.12.1
        github.com/rs/zerolog v1.21.0
)
 
require (
        github.com/beorn7/perks v1.0.1 // indirect
        github.com/cespare/xxhash/v2 v2.1.2 // indirect
        github.com/davecgh/go-spew v1.1.1 // indirect
        github.com/eapache/go-resiliency v1.3.0 // indirect
        github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 // indirect
        github.com/eapache/queue v1.1.0 // indirect
        github.com/golang/protobuf v1.5.2 // indirect
        github.com/golang/snappy v0.0.4 // indirect
        github.com/hashicorp/errwrap v1.0.0 // indirect
        github.com/hashicorp/go-multierror v1.1.1 // indirect
        github.com/hashicorp/go-uuid v1.0.2 // indirect
        github.com/jcmturner/aescts/v2 v2.0.0 // indirect
        github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
        github.com/jcmturner/gofork v1.0.0 // indirect
        github.com/jcmturner/gokrb5/v8 v8.4.2 // indirect
        github.com/jcmturner/rpc/v2 v2.0.3 // indirect
        github.com/klauspost/compress v1.15.8 // indirect
        github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
        github.com/pierrec/lz4/v4 v4.1.15 // indirect
        github.com/prometheus/client_model v0.2.0 // indirect
        github.com/prometheus/common v0.32.1 // indirect
        github.com/prometheus/procfs v0.7.3 // indirect
        github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
        golang.org/x/crypto v0.0.0-20220214200702-86341886e292 // indirect
        golang.org/x/net v0.0.0-20220708220712-1185a9018129 // indirect
        golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
        google.golang.org/protobuf v1.26.0 // indirect
)

11. Překlad aplikace s přidáním „sledovacího“ kódu

Výše uvedenou aplikaci můžeme přeložit běžným způsobem; konkrétně tímto příkazem:

$ go build

Při použití Go 1.20 na architektuře x86–64 bude mít výsledný binární spustitelný soubor velikost 14 963 996 bajtů.

My ovšem budeme potřebovat, aby výsledný spustitelný program obsahoval i logiku pro zjištění těch částí kódu, které jsou skutečně volány. Proto budeme muset provést překlad tímto příkazem:

$ go build -cover

Výsledkem bude binární spustitelný soubor o velikosti 15 061 624 bajtů. Větší velikost je pochopitelná, protože nyní jsme vlastně do výsledné aplikace přidali další funkcionalitu (a to ne zcela triviální).

12. Spuštění aplikace s uvedením několika přepínačů na příkazovém řádku (různé režimy činnosti)

Nyní tedy máme k dispozici binární spustitelný kód aplikace, která dokáže po každém svém spuštění vygenerovat soubor s informacemi o tom, které bloky programového kódu byly skutečně volány. Ovšem export těchto informací je nejprve nutné nakonfigurovat, protože bez korektní konfigurace se aplikace sice bez problémů spustí, ale export dat neprovede (viz podtržené chybové hlášení):

$ ./app --help
 
warning: GOCOVERDIR not set, no coverage data emitted
Usage of ./app:
  -authors
        show authors
  -check-kafka
        check connection to Kafka
  -version
        show version

Ve skutečnosti je konfigurace velmi snadná. Nejdříve si vytvoříme nový podadresář, do kterého se budou ukládat soubory s informacemi o pokrytí:

$ mkdir coverage

Jméno tohoto adresáře uložíme do proměnné prostředí nazvané GOCOVERDIR:

$ export GOCOVERDIR=coverage/

Aplikaci spustíme; nejprve si necháme zobrazit nápovědu:

$ ./app --help
 
Usage of ./app:
  -authors
        show authors
  -check-kafka
        check connection to Kafka
  -version
        show version
Poznámka: už nyní dojde k vytvoření prvního souboru v nakonfigurovaném podadresáři.

Dále si necháme vypsat informaci o verzi aplikace:

$ ./app -version
 
Spejbl version 1.0
{"level":"debug","time":"2023-02-22T14:53:26+01:00","message":"Finished"}

A konečně se pokusíme o připojení k message brokeru, což se v tomto případě nepodaří (podle očekávání), protože broker v daném okamžiku na počítači neběžel:

$ ./app -check-kafka
 
{"level":"info","time":"2023-02-22T14:53:44+01:00","message":"Checking connection to Kafka"}
{"level":"info","broker address":"","time":"2023-02-22T14:53:44+01:00","message":"Broker address"}
{"level":"error","error":"dial tcp: missing address","time":"2023-02-22T14:53:44+01:00","message":"Connection to broker"}
{"level":"error","error":"dial tcp: missing address","time":"2023-02-22T14:53:44+01:00","message":"Do selected operation"}

13. Spojení a konverze automaticky vytvořených souborů s informacemi o činnosti aplikace

Vzhledem k tomu, že jsme testovanou aplikaci spustiti třikrát, měli bychom v podadresáři coverage najít minimálně tři soubory obsahující zjištěné informace o běhu aplikace. Tyto soubory začínají slovem covcounters. Navíc by se zde měl nacházet čtvrtý soubor s metainformacemi, jehož jméno začíná slovem covmeta. Na mém testovacím počítači se jedná o tuto čtveřici souborů (ve vašem případě se budou konkrétní jména odlišovat, to však vůbec nevadí):

$ ls -l coverage
 
total 16
-rw-rw-r-- 1 ptisnovs ptisnovs 142 Feb 20 17:52 covcounters.810ac8e7733136bd7e57b4cac39e2180.2759513.1676911936041875369
-rw-rw-r-- 1 ptisnovs ptisnovs 154 Feb 20 17:52 covcounters.810ac8e7733136bd7e57b4cac39e2180.2759662.1676911945240574602
-rw-rw-r-- 1 ptisnovs ptisnovs 164 Feb 20 17:52 covcounters.810ac8e7733136bd7e57b4cac39e2180.2759853.1676911951857842965
-rw-rw-r-- 1 ptisnovs ptisnovs 426 Feb 20 17:52 covmeta.810ac8e7733136bd7e57b4cac39e2180

Nyní musíme provést dvojí konverzi – spojení souborů covcounters do souboru jediného a konverze výsledku do textového souboru se (strojově zpracovatelnými) informacemi o tom, které části kódu byly volány a které nikoli.

Spojení souborů do souboru jediného zajišťuje příkaz:

$ go tool covdata merge -i=coverage/ -o=.

Dojde k vytvoření souboru s názvem covcounters.810ac8e7733136bd7e57b4c­ac39e2180.0.1677074073986857547 (ovšem konkrétní jméno se může lišit).

Export dat do textového formátu provedeme příkazem:

$ go tool covdata textfmt -i=. -o=out.txt

Výsledkem činnosti těchto nástrojů by měl být soubor nazvaný out.txt, který obsahuje (strojově čitelné) informace o tom, které řádky původního programového kódu byly skutečně spuštěny. Obsah tohoto souboru si můžeme snadno vypsat – měl by vypadat takto:

$ cat out.txt
 
mode: set
app/app.go:69.20,71.2 1 1
app/app.go:74.20,76.2 1 0
app/app.go:80.68,93.16 5 1
app/app.go:93.16,96.3 2 0
app/app.go:99.2,100.16 2 1
app/app.go:100.16,103.3 2 1
app/app.go:104.2,104.16 1 0
app/app.go:104.16,107.3 2 0
app/app.go:110.2,113.26 2 0
app/app.go:118.61,121.16 2 0
app/app.go:121.16,124.3 2 0
app/app.go:126.2,126.26 1 0
app/app.go:130.44,137.16 4 0
app/app.go:137.16,140.3 2 0
app/app.go:141.2,141.12 1 0
app/app.go:144.87,145.9 1 1
app/app.go:146.28,148.27 2 1
app/app.go:149.28,151.27 2 0
app/app.go:152.39,153.44 1 1
app/app.go:154.10,156.23 2 0
app/app.go:162.13,175.16 8 1
app/app.go:175.16,179.3 3 1
app/app.go:181.2,181.29 1 1

14. Výpis informací bězích aplikace na úrovni funkcí a metod

Nyní již můžeme soubor out.txt, který byl vygenerován příkazy popsanými v předchozí kapitole, použít pro zobrazení informace o tom, které funkce a metody byly volány a kolik řádků z každé funkce/metody bylo spuštěno. Pro snazší porovnání je počet řádků uveden relativně vůči celkovému počtu řádků a hodnota je ze zlomku převedena na procenta.

Výsledek vypadá následovně:

$ go tool cover -func=out.txt
 
app/app.go:69:  showVersion             100.0%
app/app.go:74:  showAuthors             0.0%
app/app.go:80:  tryToConnectToKafka     56.2%
app/app.go:118: startService            0.0%
app/app.go:130: startHTTPServer         0.0%
app/app.go:144: doSelectedOperation     50.0%
app/app.go:162: main                    100.0%
total:          (statements)            52.0%

Ze zobrazených výsledků je patrné, že pouze u dvou funkcí byly spuštěny všechny řádky, tři funkce nebyly zavolány vůbec a u dvou funkcí bylo spuštěna jen přibližně polovina příkazů.

15. Vizualizace volaných a nevolaných řádků na webové stránce

V případě, že nám výše uvedené výsledky nedostačují (například když je nutné přesně znát řádky, které byly spuštěny), je možné si nechat nástrojem cover spustit webový server, který v prohlížeči zobrazí podrobnější informace o analyzovaném zdrojovém kódu, a to právě až na úrovni jednotlivých programových řádků:

$ go tool cover -html=out.txt

Výsledky budou v našem konkrétním případě vypadat takto:

Obrázek 2: Funkce showVersion byla zavolána, zatímco funkce showAuthors nikoli.

Obrázek 3: Ve funkci tryToConnectToKafka se nepodařilo připojení k brokeru, takže byl z funkce proveden výskok ještě před jejím koncem.

Obrázek 4: Tyto funkce nebyly volány vůbec.

Obrázek 5: Rozeskok ve funkci doSelectedOperation a plné pokrytí hlavní funkce main.

bitcoin_skoleni

16. Závěrečné zhodnocení

Rozšíření možností poskytovaných nástrojem cover je velmi užitečné. Například je možné zjistit, jak kvalitní a podrobné jsou integrační testy, testy komponent, nebo BDD testy (které navíc mohou být psány v jiném jazyce, neboť Go v tomto případě nemusí být ideálním řešením). Popř. je možné v testovacím nebo předprodukčním prostředí detekovat zcela mrtvé části kódu.

17. Repositář s demonstračními příklady

Zdrojové kódy všech dnes použitých demonstračních příkladů byly uloženy do Git repositáře, který je dostupný na adrese https://github.com/tisnik/go-root. V případě, že nebudete chtít klonovat celý repositář, můžete namísto toho použít odkazy na jednotlivé demonstrační příklady, které naleznete v následující tabulce:

# Příklad/soubor Stručný popis Cesta
1 httpServer1.go implementace jednoduché webové služby s několika endpointy https://github.com/tisnik/go-root/blob/master/article_A5/httpSer­ver/httpServer1.go
2 httpServer1_test.go jednotkové testy pro webovou službu https://github.com/tisnik/go-root/blob/master/article_A5/httpSer­ver/httpServer1_test.go
3 httpServer1_cover_set.go automaticky transformovaný kód obsahující „sledovače“ vstupu do funkcí https://github.com/tisnik/go-root/blob/master/article_A5/httpSer­ver/httpServer1_cover_set­.go
4 httpServer1_cover_count.go automaticky transformovaný kód obsahující čítače vstupu do funkcí https://github.com/tisnik/go-root/blob/master/article_A5/httpSer­ver/httpServer1_cover_cou­nt.go
5 httpServer1_cover_atomic.go automaticky transformovaný kód obsahující atomické čítače vstupu do funkcí https://github.com/tisnik/go-root/blob/master/article_A5/httpSer­ver/httpServer1_cover_ato­mic.go
       
6 app.go zdrojový kód aplikace, u níž se detekují použité části kódu https://github.com/tisnik/go-root/blob/master/article_A5/app/app.go
7 go.mod projektový soubor aplikace https://github.com/tisnik/go-root/blob/master/article_A5/app/go.mod
8 go.sum seznam všech tranzitivních závislostí aplikace https://github.com/tisnik/go-root/blob/master/article_A5/app/go.sum

18. Odkazy na Internetu

  1. Go 1.20 Release Notes
    https://go.dev/doc/go1.20
  2. Go 1.20 Release Notes: Cover
    https://go.dev/doc/go1.20#cover
  3. Working with coverage data files
    https://go.dev/testing/co­verage/#working
  4. Proposal: extend code coverage testing to include applications
    https://github.com/golang/go/is­sues/51430
  5. Package trace
    https://golang.org/pkg/runtime/trace/
  6. An Introduction to Benchmarking Your Go Programs
    https://tutorialedge.net/go­lang/benchmarking-your-go-programs/
  7. How the Go runtime implements maps efficiently (without generics)
    https://dave.cheney.net/2018/05/29/how-the-go-runtime-implements-maps-efficiently-without-generics
  8. Go18DS (Go 1.18+ Data Structures)
    https://github.com/daichi-m/go18ds
  9. TreeMap v2
    https://github.com/igrmk/treemap
  10. Go Data Structures: Binary Search Tree
    https://flaviocopes.com/golang-data-structure-binary-search-tree/
  11. Generics in Go
    https://bitfieldconsultin­g.com/golang/generics
  12. Tutorial: Getting started with generics
    https://go.dev/doc/tutorial/generics
  13. Gobs of data
    https://blog.golang.org/gobs-of-data
  14. Performance at Scale: MinIO Pushes Past 1.4 terabits per second with 256 NVMe Drives
    https://blog.min.io/performance-at-scale-minio-pushes-past-1–3-terabits-per-second-with-256-nvme-drives/
  15. Benchmarking MinIO vs. AWS S3 for Apache Spark
    https://blog.min.io/benchmarking-apache-spark-vs-aws-s3/
  16. Know Go: Generics (Kniha)
    https://bitfieldconsultin­g.com/books/generics
  17. Go 1.18 Generics based slice package
    https://golangexample.com/go-1–18-generics-based-slice-package/
  18. Highly extensible Go source code linter providing checks currently missing from other linters
    https://github.com/go-critic/go-critic
  19. Fast linters runner for Go
    https://github.com/golangci/golangci-lint
  20. Checkers from the “performance” group
    https://go-critic.com/overview#checkers-from-the-performance-group
  21. rangeValCopy
    https://go-critic.com/overview#rangeValCopy-ref
  22. C vs Rust vs Go: performance analysis
    https://medium.com/@marek.michalik/c-vs-rust-vs-go-performance-analysis-945ab749056c
  23. Golang Performance Comparison | Why is GO Fast?
    https://www.golinuxcloud.com/golang-performance/
  24. Go mutex vs channels benchmark
    https://github.com/danil/go_mu­tex_vs_channels_benchmark
  25. Techniques to Maximize Your Go Application’s Performance
    https://golangdocs.com/techniques-to-maximize-your-go-applications-performance
  26. Go language performance optimization
    https://www.programmerall­.com/article/8929467838/
  27. Ultimate Golang Performance Optimization Guide
    https://www.bacancytechno­logy.com/blog/golang-performance
  28. Optimizing a Golang service to reduce over 40% CPU
    https://medium.com/coralogix-engineering/optimizing-a-golang-service-to-reduce-over-40-cpu-366b67c67ef9
  29. Tutorial for optimizing golang program
    https://github.com/caibirdme/hand-to-hand-optimize-go/blob/master/README.md
  30. How to optimise your Go code
    https://codeburst.io/how-to-optimise-your-go-code-c6b27d4f1452

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.