Go dla początkujących - Część 8: Debugowanie aplikacji

2/6/2025 Kurs Go

Mateusz Kędziora

image

Go, język stworzony przez Google, zyskuje coraz większą popularność dzięki swojej prostocie, wydajności i wsparciu dla współbieżności. Pisanie w Go jest przyjemne, ale jak w każdym innym języku programowania, prędzej czy później napotkasz błędy w swoim kodzie. I wtedy zaczyna się debugowanie.

Ten artykuł jest skierowany do początkujących programistów Go, którzy chcą nauczyć się debugować swoje aplikacje. Przejdziemy przez podstawy debugowania, nauczymy się używać debuggera Delve (dlv), a także omówimy inne przydatne techniki, takie jak logowanie. Przygotuj się na praktyczną wiedzę, którą od razu wykorzystasz w swoich projektach.

Dlaczego Debugowanie Jest Ważne?

Wyobraź sobie, że piszesz program, który ma obliczyć średnią ocen studentów. Wszystko wydaje się logiczne, ale program zwraca nieprawidłowy wynik. Co robisz? Możesz zgadywać, przeglądać kod linia po linii, ale to zajmuje dużo czasu i frustruje.

Debugowanie to proces identyfikacji i eliminowania błędów w kodzie. Efektywne debugowanie pozwala:

  • Szybko znaleźć źródło problemu: Zamiast spędzać godziny na zgadywaniu, debugger wskaże Ci konkretną linię kodu, która powoduje błąd.
  • Zrozumieć, jak działa Twój kod: Obserwując wartości zmiennych i śledząc wykonanie programu, lepiej zrozumiesz, co się dzieje “pod maską”.
  • Poprawić jakość kodu: Debugowanie pomaga nie tylko usunąć błędy, ale również zidentyfikować potencjalne problemy i zoptymalizować kod.

Wprowadzenie do Delve (dlv)

Delve (dlv) to popularny debugger dla Go. Pozwala na:

  • Ustawianie breakpointów: Zatrzymywanie wykonania programu w określonych miejscach.
  • Inspekcję zmiennych: Sprawdzanie wartości zmiennych w trakcie działania programu.
  • Śledzenie wykonania programu: Przechodzenie przez kod linia po linii, aby zobaczyć, co się dzieje.
  • Wywoływanie funkcji: Uruchamianie wybranych funkcji w trakcie debugowania.

Instalacja Delve:

Aby zainstalować Delve, użyj następującego polecenia:

go install github.com/go-delve/delve/cmd/dlv@latest

Upewnij się, że masz poprawnie skonfigurowane środowisko Go. Po instalacji, dlv powinien być dostępny w Twojej ścieżce systemowej.

Pierwszy Program do Debugowania

Zacznijmy od prostego programu, który zawiera błąd. Użyjemy go, aby pokazać podstawowe funkcje debuggera Delve.

package main

import "fmt"

func main() {
	numbers := []int{1, 2, 3, 4, 5}
	sum := calculateSum(numbers)
	fmt.Println("Suma liczb:", sum)
}

func calculateSum(nums []int) int {
	sum := 0
	for i := 1; i <= len(nums); i++ { // Błąd: indeks zaczyna się od 1
		sum += nums[i]
	}
	return sum
}

Opis kodu:

  • main funkcja tworzy slice liczb numbers i wywołuje funkcję calculateSum, aby obliczyć ich sumę.
  • calculateSum funkcja iteruje po slice liczb i dodaje każdą liczbę do zmiennej sum. Krytyczny błąd: Pętla for zaczyna indeksowanie od 1, a nie od 0, co powoduje odwołanie się do elementu poza zakresem slice.

Zapisz ten kod jako main.go.

Uruchamianie Debuggera Delve

Aby uruchomić debugger Delve, otwórz terminal w katalogu, w którym znajduje się plik main.go, i wpisz następujące polecenie:

dlv debug

Debugger powinien się uruchomić i zatrzymać w funkcji main. Zobaczysz coś takiego:

Type 'help' for list of commands.
(dlv)

Ustawianie Breakpointów

Breakpoint to punkt w kodzie, w którym debugger zatrzyma wykonanie programu. Umożliwia to sprawdzenie stanu programu w konkretnym miejscu.

Aby ustawić breakpoint w funkcji calculateSum, użyj polecenia break (lub jego skrótu b):

(dlv) break main.calculateSum
Breakpoint 1 set at 0x11bf73a for main.calculateSum() ./main.go:10

Teraz uruchom program dalej, używając polecenia continue (lub jego skrótu c):

(dlv) continue
> main.calculateSum() ./main.go:10 (hits breakpoint) (PC: 0x11bf73a)

Program zatrzymał się wewnątrz funkcji calculateSum, w miejscu, gdzie ustawiliśmy breakpoint.

Inspekcja Zmiennych

W debugerze możesz sprawdzić wartości zmiennych, aby zobaczyć, co się dzieje w programie. Aby sprawdzić wartość zmiennej i, użyj polecenia print (lub jego skrótu p):

(dlv) print i
1

Podobnie, możesz sprawdzić wartość zmiennej sum:

(dlv) print sum
0

Możesz także sprawdzić zawartość slice nums:

(dlv) print nums
[]int {1, 2, 3, 4, 5}

Śledzenie Wykonania Programu (Stepping)

Aby przejść do następnej linii kodu, użyj polecenia next (lub jego skrótu n):

(dlv) next
> main.calculateSum() ./main.go:11 (PC: 0x11bf79a)

Teraz jesteśmy w linii sum += nums[i]. Możemy sprawdzić, co się stanie, gdy ta linia zostanie wykonana. Ponownie użyj polecenia print, aby sprawdzić wartości i i sum.

(dlv) print i
1
(dlv) print sum
0
(dlv) next
panic: runtime error: index out of range [1] with length 5

goroutine 1 [running]:
main.calculateSum(0xc00001a1e0?)
        ./main.go:11 +0xba
main.main()
        ./main.go:6 +0x44

Ojej! Program wygenerował błąd index out of range. Teraz już wiemy, że problem leży w tym, że pętla for zaczyna indeksowanie od 1, zamiast od 0. To dlatego próbowaliśmy odwołać się do elementu nums[5], który nie istnieje (slice ma indeksy od 0 do 4).

Naprawa Błędu

Aby naprawić błąd, zmień pętlę for w funkcji calculateSum na:

func calculateSum(nums []int) int {
	sum := 0
	for i := 0; i < len(nums); i++ { // Poprawka: indeks zaczyna się od 0
		sum += nums[i]
	}
	return sum
}

Zapisz zmiany i uruchom program ponownie (bez debuggera). Teraz program powinien działać poprawnie i wyświetlić prawidłową sumę liczb:

Suma liczb: 15

Bardziej Zaawansowane Funkcje Delve

Delve oferuje wiele innych funkcji, które mogą być przydatne podczas debugowania:

  • step (lub s): Wchodzi do wywoływanej funkcji. Jeśli znajdujesz się na linii, która wywołuje inną funkcję, step pozwoli Ci wejść do tej funkcji i śledzić jej wykonanie.
  • step-out (lub so): Wychodzi z aktualnie wykonywanej funkcji. Kontynuuje wykonywanie programu, aż do powrotu z aktualnej funkcji.
  • locals: Wyświetla listę lokalnych zmiennych w aktualnie wykonywanej funkcji.
  • args: Wyświetla argumenty przekazane do aktualnie wykonywanej funkcji.
  • breakpoints: Wyświetla listę wszystkich ustawionych breakpointów.
  • clear: Usuwa breakpoint. Możesz usunąć breakpoint po numerze (clear 1) lub po lokalizacji (clear main.calculateSum).

Debugowanie Kodu Współbieżnego

Go słynie ze wsparcia dla współbieżności. Debugowanie kodu współbieżnego może być trudniejsze, ale Delve oferuje narzędzia, które to ułatwiają:

  • goroutines: Wyświetla listę wszystkich aktywnych goroutines.
  • goroutine <id>: Przełącza do określonej goroutine. Możesz użyć tego polecenia, aby śledzić wykonanie konkretnej goroutine.
  • Breakpointy warunkowe: Możesz ustawić breakpoint, który zostanie aktywowany tylko wtedy, gdy spełniony jest określony warunek. Na przykład, możesz ustawić breakpoint w goroutine tylko wtedy, gdy wartość zmiennej spełnia określone kryterium.

Przykład breakpointu warunkowego:

(dlv) break main.myGoroutine if myVar > 10

Ten breakpoint zatrzyma program w funkcji myGoroutine tylko wtedy, gdy wartość zmiennej myVar jest większa niż 10.

Logowanie: Alternatywna Metoda Debugowania

Oprócz debuggera, logowanie jest bardzo przydatną techniką debugowania. Polega na wstawianiu do kodu instrukcji, które wypisują informacje o stanie programu.

Przykład:

package main

import (
	"fmt"
	"log"
)

func main() {
	numbers := []int{1, 2, 3, 4, 5}
	log.Println("Początek programu") // Logowanie
	sum := calculateSum(numbers)
	fmt.Println("Suma liczb:", sum)
	log.Println("Koniec programu") // Logowanie
}

func calculateSum(nums []int) int {
	sum := 0
	log.Println("Początek calculateSum") // Logowanie
	for i := 0; i < len(nums); i++ {
		log.Printf("i: %d, nums[i]: %d, sum: %d\n", i, nums[i], sum) // Logowanie
		sum += nums[i]
	}
	log.Println("Koniec calculateSum") // Logowanie
	return sum
}

Opis kodu:

  • Używamy pakietu log do wypisywania komunikatów do konsoli.
  • log.Println wypisuje komunikat z nową linią na końcu.
  • log.Printf wypisuje sformatowany komunikat (podobnie do fmt.Printf).

Logowanie jest szczególnie przydatne w sytuacjach, gdy nie możesz użyć debuggera (np. w środowisku produkcyjnym) lub gdy debugujesz kod współbieżny. Można logować do pliku, zamiast do konsoli. To pozwala analizować działanie programu po jego uruchomieniu.

Kiedy Używać Debuggera, a Kiedy Logowania?

  • Debugger: Idealny do interaktywnego debugowania, gdy chcesz dokładnie śledzić wykonanie programu i sprawdzić wartości zmiennych w czasie rzeczywistym.
  • Logowanie: Przydatne do debugowania w środowiskach, gdzie debugger nie jest dostępny, do monitorowania działania programu w środowisku produkcyjnym i do analizy problemów, które występują sporadycznie.

Często najlepszym rozwiązaniem jest połączenie obu technik. Używaj debuggera do szybkiego znajdowania i naprawiania prostych błędów, a logowania do monitorowania działania programu i diagnozowania trudniejszych problemów.

Praca Domowa

  1. Stwórz program, który oblicza średnią arytmetyczną liczb w slice. Celowo wprowadź do niego błąd (np. dzielenie przez zero, indeksowanie poza zakresem).
  2. Użyj debuggera Delve, aby znaleźć i naprawić błąd. Ustaw breakpointy, sprawdzaj wartości zmiennych i śledź wykonanie programu.
  3. Dodaj logowanie do programu. Wypisz informacje o wartościach zmiennych i kluczowych momentach w działaniu programu.
  4. Porównaj, jak szybko udało Ci się znaleźć błąd, używając debuggera i logowania.
  5. Zmień program, aby uruchamiał obliczanie średniej w osobnej goroutine. Spróbuj zdebugować program, używając poleceń goroutines i goroutine <id>.

Podsumowanie

W tym artykule omówiliśmy podstawy debugowania aplikacji Go. Nauczyliśmy się używać debuggera Delve, ustawiać breakpointy, inspekcjonować zmienne i śledzić wykonanie programu. Pokazaliśmy również, jak używać logowania jako alternatywnej metody debugowania.

Pamiętaj, że debugowanie to umiejętność, którą rozwija się z praktyką. Im więcej debugujesz, tym szybciej będziesz znajdować i naprawiać błędy w swoim kodzie. Zachęcam Cię do eksperymentowania z debuggerem Delve i logowaniem w swoich projektach.

Przydatne Linki

Powodzenia w debugowaniu! Nie zapomnij sprawdzić pozostałych wpisów na blogu, aby poszerzyć swoją wiedzę na temat Go. Następny artykuł może dotyczyć testowania jednostkowego w Go, co jest kolejnym ważnym elementem tworzenia niezawodnych aplikacji.

Polecane artykuły