Go dla początkujących - Część 13: RESTful API
Mateusz Kędziora

Hej! Jeżeli dopiero zaczynasz swoją przygodę z Go i chcesz nauczyć się tworzyć API, to trafiłeś idealnie. W tym artykule pokażę Ci krok po kroku, jak zbudować proste, ale funkcjonalne RESTful API w Go, korzystając z popularnego frameworka Gin. Omówimy podstawowe koncepcje, zdefiniujemy endpointy, obsłużymy różne metody HTTP, nauczymy się walidować dane i zwracać odpowiedzi JSON. Gotowy? Zaczynamy!
Co to jest RESTful API i dlaczego Go?
Zanim przejdziemy do kodu, warto zrozumieć, czym właściwie jest RESTful API. W skrócie, to interfejs, który pozwala różnym aplikacjom komunikować się ze sobą za pomocą standardowych protokołów HTTP. REST (Representational State Transfer) to styl architektury, który narzuca pewne zasady, takie jak bezstanowość (statelessness), jednolity interfejs (uniform interface) i warstwowość (layered system).
Dlaczego Go jest dobrym wyborem do budowy API? Przede wszystkim ze względu na:
- Wydajność: Go jest kompilowanym językiem, co oznacza, że jest bardzo szybki i efektywny.
- Współbieżność: Go oferuje wbudowane wsparcie dla współbieżności (goroutines i channels), co ułatwia obsługę wielu żądań jednocześnie.
- Prostota: Go ma czystą i prostą składnię, co ułatwia naukę i pisanie kodu.
- Biblioteki: Go ma bogaty ekosystem bibliotek, w tym doskonałe frameworki do budowy API.
Wybór frameworka: Gin
Do budowy naszego API wybierzemy framework Gin. W poprzednich częściach skupialiśmy się głównie na wbudowanym serwerze net/http
lecz Gin oferuje znacznie większe możliwości przy porównywalnej lub nawet lepszej wydajności. Gin to lekki i szybki framework HTTP dla Go. Jest inspirowany frameworkiem Martini, ale oferuje znacznie lepszą wydajność. Gin ułatwia obsługę routingu, middleware, walidacji i renderowania odpowiedzi JSON.
Konfiguracja środowiska
Przed rozpoczęciem pracy tworzymi nowy katalog dla tego projektu i inicjujemy go jako moduł Go:
mkdir my-api
cd my-api
go mod init my-api
Teraz musisz zainstalować Gin:
go get -u github.com/gin-gonic/gin
Tworzymy pierwszy endpoint
Stwórz plik main.go
i wklej do niego poniższy kod:
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default() // Tworzymy instancję routera Gin
router.GET("/ping", func(c *gin.Context) { // Definiujemy endpoint GET /ping
c.JSON(http.StatusOK, gin.H{ // Zwracamy odpowiedź JSON
"message": "pong",
})
})
router.Run(":8080") // Uruchamiamy serwer na porcie 8080
}
Wyjaśnienie kodu:
package main
: Definiuje główny pakiet, który jest punktem wejścia do programu.import
: Importuje niezbędne pakiety, takie jaknet/http
do obsługi HTTP igithub.com/gin-gonic/gin
do korzystania z frameworka Gin.gin.Default()
: Tworzy domyślną instancję routera Gin z domyślnym middleware (np. logger, recovery).router.GET("/ping", ...)
: Definiuje endpoint GET na ścieżce/ping
. Drugi argument to funkcja obsługująca żądanie.c *gin.Context
:c
to kontekst żądania Gin. Zawiera informacje o żądaniu (np. nagłówki, parametry) i pozwala na manipulowanie odpowiedzią.c.JSON(http.StatusOK, gin.H{...})
: Zwraca odpowiedź JSON z kodem statusu HTTP 200 (OK) i danymi JSON.gin.H
to alias dlamap[string]interface{}
– wygodny sposób na tworzenie mapy klucz-wartość.router.Run(":8080")
: Uruchamia serwer HTTP na porcie 8080.
Uruchom program:
go run main.go
Otwórz przeglądarkę lub użyj narzędzia curl
i wyślij żądanie GET na adres http://localhost:8080/ping
. Powinieneś zobaczyć odpowiedź JSON:
{
"message": "pong"
}
Gratulacje! Właśnie stworzyłeś swój pierwszy endpoint w Go z użyciem Gin!
Obsługa innych metod HTTP: POST, PUT, DELETE
Teraz rozszerzymy nasze API o obsługę innych metod HTTP: POST, PUT i DELETE. Stworzymy endpoint do zarządzania listą książek.
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
// Book reprezentuje dane książki
type Book struct {
ID string `json:"id"`
Title string `json:"title"`
Author string `json:"author"`
}
var books = []Book{ // Inicjalizujemy listę książek
{ID: "1", Title: "Władca Pierścieni", Author: "J.R.R. Tolkien"},
{ID: "2", Title: "Hobbit", Author: "J.R.R. Tolkien"},
}
func main() {
router := gin.Default()
// GET /books - zwraca listę wszystkich książek
router.GET("/books", getBooks)
// GET /books/:id - zwraca książkę o danym ID
router.GET("/books/:id", getBookByID)
// POST /books - dodaje nową książkę
router.POST("/books", createBook)
// PUT /books/:id - aktualizuje książkę o danym ID
router.PUT("/books/:id", updateBook)
// DELETE /books/:id - usuwa książkę o danym ID
router.DELETE("/books/:id", deleteBook)
router.Run(":8080")
}
// getBooks zwraca listę wszystkich książek
func getBooks(c *gin.Context) {
c.JSON(http.StatusOK, books)
}
// getBookByID zwraca książkę o danym ID
func getBookByID(c *gin.Context) {
id := c.Param("id") // Pobieramy parametr "id" z URL
for _, book := range books {
if book.ID == id {
c.JSON(http.StatusOK, book)
return
}
}
c.JSON(http.StatusNotFound, gin.H{"message": "Książka nieznaleziona"})
}
// createBook dodaje nową książkę
func createBook(c *gin.Context) {
var newBook Book
// Spróbuj zbindować dane JSON z żądania do struktury Book
if err := c.BindJSON(&newBook); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
books = append(books, newBook)
c.JSON(http.StatusCreated, newBook)
}
// updateBook aktualizuje książkę o danym ID
func updateBook(c *gin.Context) {
id := c.Param("id")
var updatedBook Book
if err := c.BindJSON(&updatedBook); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
for i, book := range books {
if book.ID == id {
books[i] = updatedBook
c.JSON(http.StatusOK, updatedBook)
return
}
}
c.JSON(http.StatusNotFound, gin.H{"message": "Książka nieznaleziona"})
}
// deleteBook usuwa książkę o danym ID
func deleteBook(c *gin.Context) {
id := c.Param("id")
for i, book := range books {
if book.ID == id {
books = append(books[:i], books[i+1:]...) // Usuwamy książkę ze slice
c.JSON(http.StatusOK, gin.H{"message": "Książka usunięta"})
return
}
}
c.JSON(http.StatusNotFound, gin.H{"message": "Książka nieznaleziona"})
}
Wyjaśnienie kodu:
type Book struct { ... }
: Definiuje strukturęBook
, która reprezentuje dane książki.var books = []Book{ ... }
: Inicjalizuje slice (dynamiczną tablicę) książek.router.GET("/books", getBooks)
: Definiuje endpoint GET/books
, który obsługuje funkcjagetBooks
.router.GET("/books/:id", getBookByID)
: Definiuje endpoint GET/books/:id
, gdzie:id
jest parametrem URL. Obsługuje go funkcjagetBookByID
.c.Param("id")
: Pobiera wartość parametruid
z URL.router.POST("/books", createBook)
: Definiuje endpoint POST/books
, który obsługuje funkcjacreateBook
.c.BindJSON(&newBook)
: Próbuje zbindować dane JSON z ciała żądania HTTP do strukturynewBook
. Jeśli operacja się nie powiedzie (np. JSON jest nieprawidłowy), zwraca błąd.router.PUT("/books/:id", updateBook)
: Definiuje endpoint PUT/books/:id
, który obsługuje funkcjaupdateBook
.router.DELETE("/books/:id", deleteBook)
: Definiuje endpoint DELETE/books/:id
, który obsługuje funkcjadeleteBook
.books = append(books[:i], books[i+1:]...)
: Usuwa element ze slice.
Uruchom program i przetestuj endpointy za pomocą narzędzia curl
lub Postman.
Przykłady użycia curl
:
GET /books:
curl http://localhost:8080/books
GET /books/1:
curl http://localhost:8080/books/1
POST /books:
curl -X POST -H "Content-Type: application/json" -d '{"id": "3", "title": "Nowa Książka", "author": "Nowy Autor"}' http://localhost:8080/books
PUT /books/1:
curl -X PUT -H "Content-Type: application/json" -d '{"id": "1", "title": "Zaktualizowany Tytuł", "author": "Zaktualizowany Autor"}' http://localhost:8080/books/1
DELETE /books/1:
curl -X DELETE http://localhost:8080/books/1
Walidacja danych wejściowych
Walidacja danych wejściowych jest kluczowa dla bezpieczeństwa i stabilności API. Chroni przed nieprawidłowymi danymi, które mogą prowadzić do błędów lub nawet ataków.
W Gin możemy użyć biblioteki github.com/go-playground/validator/v10
do walidacji danych. Najpierw zainstaluj bibliotekę:
go get github.com/go-playground/validator/v10
Zmodyfikuj strukturę Book
i dodaj tagi walidacji:
type Book struct {
ID string `json:"id" binding:"required"` // ID musi być obecne
Title string `json:"title" binding:"required"` // Tytuł musi być obecny
Author string `json:"author" binding:"required,min=3"` // Autor musi być obecny i mieć co najmniej 3 znaki
}
Zmodyfikuj funkcję createBook
i dodaj walidację:
func createBook(c *gin.Context) {
var newBook Book
if err := c.BindJSON(&newBook); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Walidacja danych
validate := validator.New()
err := validate.Struct(newBook)
if err != nil {
validationErrors := []string{}
for _, err := range err.(validator.ValidationErrors) {
validationErrors = append(validationErrors, err.Field()+" "+err.Tag())
}
c.JSON(http.StatusBadRequest, gin.H{"errors": validationErrors})
return
}
books = append(books, newBook)
c.JSON(http.StatusCreated, newBook)
}
Wyjaśnienie kodu:
binding:"required"
: Tag binding informuje Gin, że pole musi być obecne w żądaniu.binding:"required,min=3"
: Tag binding informuje Gin, że pole musi być obecne i mieć co najmniej 3 znaki.validator.New()
: Tworzy nową instancję validatora.validate.Struct(newBook)
: Waliduje strukturęnewBook
na podstawie tagów walidacji.- Sprawdzamy czy err jest typu validator.ValidationErrors, iterujemy po wszystkich błędach walidacji i tworzymy listę błędów do zwrócenia.
Teraz, jeśli spróbujesz wysłać żądanie POST z brakującym polem lub nieprawidłowymi danymi, otrzymasz odpowiedź z błędami walidacji.
Najlepsze praktyki projektowania API
Podczas projektowania API warto przestrzegać kilku najlepszych praktyk:
- Używaj rzeczowników w URL: Endpointy powinny reprezentować zasoby, np.
/books
, a nie czynności, np./getBooks
. - Używaj odpowiednich metod HTTP: GET do pobierania danych, POST do tworzenia nowych zasobów, PUT do aktualizacji istniejących zasobów, DELETE do usuwania zasobów.
- Zwracaj odpowiednie kody statusu HTTP: 200 OK, 201 Created, 400 Bad Request, 404 Not Found, 500 Internal Server Error itp.
- Używaj JSON do przesyłania danych: JSON jest powszechnie używany i łatwy do parsowania.
- Dokumentuj API: Używaj narzędzi takich jak Swagger/OpenAPI do dokumentowania API.
- Waliduj dane wejściowe: Chroń API przed nieprawidłowymi danymi.
- Obsługuj błędy: Zwracaj zrozumiałe komunikaty o błędach.
- Paginate wyniki: Jeśli API zwraca duże zbiory danych, używaj paginacji, aby poprawić wydajność.
- Używaj wersji API: Wprowadzaj zmiany w API w sposób kompatybilny wstecz, a jeśli to niemożliwe, używaj wersji API (np.
/v1/books
).
Praca domowa
- Dodaj obsługę paginacji do endpointu
/books
. Umożliw użytkownikowi określenie numeru strony i rozmiaru strony w zapytaniu (np.GET /books?page=2&size=10
). - Dodaj obsługę filtrowania do endpointu
/books
. Umożliw użytkownikowi filtrowanie książek po tytule lub autorze (np.GET /books?title=Władca
). - Zaimplementuj middleware do logowania każdego żądania HTTP. Zapisuj informacje takie jak metoda HTTP, URL, kod statusu i czas trwania żądania.
- Zaimplementuj obsługę błędów, która zwraca bardziej szczegółowe informacje o błędach w formacie JSON. Dodaj pole
code
imessage
do odpowiedzi JSON. - Zintegruj API z bazą danych (np. PostgreSQL lub MySQL). Zamiast przechowywać dane w pamięci, zapisuj je i pobieraj z bazy danych.
Podsumowanie
W tym artykule pokazaliśmy, jak stworzyć proste, ale funkcjonalne RESTful API w Go z użyciem frameworka Gin. Nauczyliśmy się definiować endpointy, obsługiwać różne metody HTTP, walidować dane i zwracać odpowiedzi JSON. Omówiliśmy również najlepsze praktyki projektowania API.
Mam nadzieję, że ten artykuł był dla Ciebie pomocny. Zachęcam Cię do dalszego eksperymentowania z Go i Gin oraz do budowy własnych API. Pamiętaj, że nauka programowania to ciągły proces. Im więcej ćwiczysz, tym lepszy będziesz.
Nie zapomnij zajrzeć do innych moich postów na temat Go! Znajdziesz tam więcej praktycznych przykładów i porad.
Przydatne zasoby
- Oficjalna dokumentacja Go: https://go.dev/doc/
- A Tour of Go: https://go.dev/tour/welcome/1
- Gin documentation: https://github.com/gin-gonic/gin
- Go by Example: https://gobyexample.com/
- Effective Go: https://go.dev/doc/effective_go
- Go Packages: https://pkg.go.dev/
Powodzenia w dalszej nauce Go!
Polecane artykuły
Docker vs Kubernetes: Który dla Ciebie w 2025?
Docker i Kubernetes objaśnione! Która technologia lepsza dla początkujących w 2025? Porównanie, przykłady i przyszłość.
Mateusz Kędziora
DevOps: Automatyzacja zadań sysadmina dla programistów
Zautomatyzuj pracę sysadmina w środowisku DevOps! Praktyczne przykłady, skrypty, Ansible, Terraform, Prometheus i Grafana.
Mateusz Kędziora
Automatyzacja Linux/macOS z Bash: Praktyczny Przewodnik
Zacznij automatyzować system Linux/macOS z Bash! Dowiedz się, czym jest Bash, jak pisać skrypty i używać podstawowych komend.
Mateusz Kędziora