REST API építése a nulláról Go és a net/http csomag segítségével

Üdvözöllek a Go programozás és a webfejlesztés izgalmas világában! Ha valaha is elgondolkodtál azon, hogyan működnek a modern alkalmazások a háttérben, hogyan kommunikálnak egymással a különböző szolgáltatások, akkor valószínűleg már találkoztál a REST API fogalmával. Ez a cikk egy átfogó, lépésről lépésre bemutató útmutató arról, hogyan építhetsz egy egyszerű, de funkcionális REST API-t a nulláról a Go programozási nyelv és annak beépített net/http csomagja segítségével.

A Go, vagy ahogy sokan hívják, Golang, az elmúlt években rendkívül népszerűvé vált a nagy teljesítményű, skálázható hálózati alkalmazások, beleértve az API-kat is, fejlesztéséhez. Egyszerűsége, kiváló konkurencia kezelése és beépített eszközök gazdag tárháza teszi ideális választássá. Készülj fel, mert egy izgalmas utazás vár ránk, ahol megtanulhatod az alapokat, amelyekre később komplex rendszereket építhetsz!

Mi az a REST API és miért van rá szükségünk?

A REST (Representational State Transfer) egy építészeti stílus a elosztott rendszerek számára, ami az interneten keresztüli kommunikációra vonatkozik. Egy REST API lehetővé teszi, hogy különböző szoftverkomponensek – például egy mobilalkalmazás és egy szerver, vagy két szerverszolgáltatás – szabványos módon kommunikáljanak egymással HTTP protokollon keresztül. Gondolj rá úgy, mint egy pincérre (az API), aki felveszi a rendelésed (kérésed) és elhozza az ételt (válaszodat) a konyhából (adatbázisból vagy szolgáltatásból).

Az API-k kritikus szerepet játszanak a modern szoftverarchitektúrákban, lehetővé téve a moduláris felépítést, az adatok megosztását és a szolgáltatások integrációját. Segítségükkel alkalmazásaink könnyebben skálázhatók, karbantarthatók és fejleszthetők.

Miért Go a REST API-hoz?

  • Teljesítmény: A Go rendkívül gyors, közel natív teljesítményt nyújt, ami kulcsfontosságú a nagy terhelésű API-k esetében.
  • Konkurencia: A goroutine-ok és channel-ek egyszerűvé teszik a konkurens kód írását, lehetővé téve, hogy az API párhuzamosan több kérést is kezeljen.
  • Egyszerűség és olvashatóság: A Go szintaxisa tiszta és minimalista, ami megkönnyíti a kód írását és megértését.
  • Beépített eszközök: A net/http csomag a Go standard könyvtárának része, így nincs szükség külső függőségekre egy alapvető API elkészítéséhez. Ez leegyszerűsíti a fejlesztést és a telepítést.
  • Statisztikai fordítás: A Go alkalmazások egyetlen bináris fájlba fordulnak, ami rendkívül egyszerűvé teszi a telepítést és a futtatást.

Előfeltételek és a Projekt Beállítása

Mielőtt belevágnánk a kódolásba, győződj meg róla, hogy a Go telepítve van a gépeden. Ha mégsem, látogass el a go.dev/dl/ oldalra és kövesd az ott található utasításokat. A Go telepítése után hozzunk létre egy új projektet:


mkdir go-rest-api
cd go-rest-api
go mod init go-rest-api
    

Ez létrehoz egy go.mod fájlt, ami kezeli a projekt függőségeit (bár ebben az esetben nem lesz sok, hiszen a net/http beépített).

A net/http Csomag Alapjai

A Go net/http csomagja mindent biztosít, amire szükségünk van egy HTTP szerver és kliens implementálásához. Két kulcsfontosságú eleme van, amire fókuszálunk:

  • http.Handler: Ez egy interfész, amely egyetlen metódust, a ServeHTTP(w http.ResponseWriter, r *http.Request)-et definiálja. Minden olyan típus, amely ezt a metódust implementálja, egy HTTP kérést tud kezelni.
  • http.ServeMux: Egy HTTP multiplexer, ami a beérkező kérések URL-jét vizsgálja, és a megfelelő http.Handler-hez irányítja azokat. Ez a routerünk.
  • http.Request: Ez az objektum tartalmazza a beérkező HTTP kérés minden információját (metódus, URL, fejlécek, törzs stb.).
  • http.ResponseWriter: Ezen keresztül küldjük vissza a választ a kliensnek (státuszkód, fejlécek, törzs).

Egy Egyszerű Szerver Felépítése

Kezdjük egy alapvető HTTP szerverrel, ami egy egyszerű „Hello, World!” üzenetet küld vissza. Hozz létre egy main.go fájlt a projekt gyökérkönyvtárában:


// main.go
package main

import (
	"fmt"
	"log"
	"net/http"
)

func main() {
	// A handler függvény, ami minden kérésre Hello, World!-öt válaszol
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Hello, World! Ez az én Go API-m!n")
	})

	fmt.Println("A szerver elindult a :8080 porton...")
	// Elindítjuk a szervert a 8080-as porton
	// A log.Fatal blokkolja a végrehajtást és kilép, ha hiba történik
	log.Fatal(http.ListenAndServe(":8080", nil))
}
    

Futtasd a kódot a terminálban:


go run main.go
    

Nyiss meg egy böngészőt, és navigálj a http://localhost:8080 címre. Látnod kell a „Hello, World!” üzenetet. Gratulálok, az első Go HTTP szervered fut!

Adatmodell és In-Memory Adattárolás

Egy REST API általában adatokkal dolgozik. Készítsünk egy egyszerű adatmodellt, például könyveket fogunk kezelni. Az egyszerűség kedvéért az adatokat egy memóriában tárolt map-ben fogjuk tartani, ami a szerver újraindításakor elveszik. Egy valós alkalmazásban adatbázist használnál (pl. PostgreSQL, MySQL, MongoDB).


// main.go (a main függvény előtt)
package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "strconv"
    "strings" // Az URL útvonalak elemzéséhez
    "sync"    // A konkurens hozzáférések védelméhez
)

// Book reprezentál egy könyvet
type Book struct {
    ID     int    `json:"id"`
    Title  string `json:"title"`
    Author string `json:"author"`
    ISBN   string `json:"isbn"`
}

// globális változók az adatok tárolására és a konkurens hozzáférés védelmére
var (
    books = make(map[int]Book)
    mu    sync.Mutex // Mutex a map védelméhez
    nextID = 1        // Következő rendelkezésre álló ID
)

func init() {
    // Kezdeti adatok hozzáadása
    books[nextID] = Book{ID: nextID, Title: "A Gyűrűk Ura", Author: "J.R.R. Tolkien", ISBN: "978-9633041927"}
    nextID++
    books[nextID] = Book{ID: nextID, Title: "1984", Author: "George Orwell", ISBN: "978-9630784914"}
    nextID++
}
    

A `json:"id"` tag-ek jelzik, hogy hogyan kell a mezőket JSON-né szerializálni vagy deszerializálni. A sync.Mutex a konkurens írás/olvasás problémák elkerülésére szolgál, mivel több kérés is hozzáférhet egyszerre a books map-hez.

API Végpontok (Endpointok) Létrehozása

A REST API a HTTP metódusokat (GET, POST, PUT, DELETE) használja az erőforrásokon végzett műveletek jelzésére. Most elkészítjük a CRUD (Create, Read, Update, Delete) műveletekhez tartozó handler függvényeket.

1. Könyvek Lekérése (GET /books) és Egy Könyv Lekérése (GET /books/{id})

A getBooksHandler felelős az összes könyv listázásáért vagy egy adott könyv lekéréséért ID alapján. Itt fogjuk demonstrálni, hogyan kell manuálisan kinyerni az ID-t az URL-ből, mivel a net/http.ServeMux nem támogatja az útvonalparamétereket „dobozból”.


// getBooksHandler kezeli a GET /books és GET /books/{id} kéréseket
func getBooksHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json") // Válasz típusának beállítása

    // Ellenőrizzük, hogy van-e ID az URL-ben (pl. /books/1)
    pathParts := strings.Split(r.URL.Path, "/")
    if len(pathParts) == 3 && pathParts[2] != "" { // Pl. /books/123
        idStr := pathParts[2]
        id, err := strconv.Atoi(idStr)
        if err != nil {
            http.Error(w, "Érvénytelen könyv ID formátum", http.StatusBadRequest)
            return
        }

        mu.Lock() // Adatok olvasása előtt zároljuk a mutexet
        book, ok := books[id]
        mu.Unlock() // Olvasás után feloldjuk

        if !ok {
            http.Error(w, "A könyv nem található", http.StatusNotFound)
            return
        }
        json.NewEncoder(w).Encode(book)
        return
    }

    // Ha nincs ID, akkor az összes könyvet adjuk vissza
    mu.Lock()
    allBooks := make([]Book, 0, len(books))
    for _, book := range books {
        allBooks = append(allBooks, book)
    }
    mu.Unlock()

    json.NewEncoder(w).Encode(allBooks)
}
    

2. Könyv Létrehozása (POST /books)

A createBookHandler új könyvet hoz létre a kérés törzsében (body) található JSON adatok alapján.


// createBookHandler kezeli a POST /books kéréseket
func createBookHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")

    var newBook Book
    // A kérés törzsének (body) dekódolása JSON-ból Book struktúrába
    err := json.NewDecoder(r.Body).Decode(&newBook)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    mu.Lock() // Írás előtt zároljuk
    newBook.ID = nextID
    books[nextID] = newBook
    nextID++
    mu.Unlock() // Írás után feloldjuk

    w.WriteHeader(http.StatusCreated) // 201 Created státuszkód
    json.NewEncoder(w).Encode(newBook)
}
    

3. Könyv Frissítése (PUT /books/{id})

A updateBookHandler egy létező könyvet frissít az ID és a kérés törzsében lévő adatok alapján.


// updateBookHandler kezeli a PUT /books/{id} kéréseket
func updateBookHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")

    pathParts := strings.Split(r.URL.Path, "/")
    if len(pathParts) != 3 || pathParts[2] == "" {
        http.Error(w, "Érvénytelen könyv ID", http.StatusBadRequest)
        return
    }
    idStr := pathParts[2]
    id, err := strconv.Atoi(idStr)
    if err != nil {
        http.Error(w, "Érvénytelen könyv ID formátum", http.StatusBadRequest)
        return
    }

    var updatedBook Book
    err = json.NewDecoder(r.Body).Decode(&updatedBook)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    updatedBook.ID = id // Biztosítjuk, hogy a frissített könyv ID-je megegyezzen az URL-ben lévővel

    mu.Lock()
    _, ok := books[id]
    if !ok {
        mu.Unlock()
        http.Error(w, "A könyv nem található", http.StatusNotFound)
        return
    }
    books[id] = updatedBook
    mu.Unlock()

    json.NewEncoder(w).Encode(updatedBook)
}
    

4. Könyv Törlése (DELETE /books/{id})

A deleteBookHandler egy könyvet töröl az ID alapján.


// deleteBookHandler kezeli a DELETE /books/{id} kéréseket
func deleteBookHandler(w http.ResponseWriter, r *http.Request) {
    pathParts := strings.Split(r.URL.Path, "/")
    if len(pathParts) != 3 || pathParts[2] == "" {
        http.Error(w, "Érvénytelen könyv ID", http.StatusBadRequest)
        return
    }
    idStr := pathParts[2]
    id, err := strconv.Atoi(idStr)
    if err != nil {
        http.Error(w, "Érvénytelen könyv ID formátum", http.StatusBadRequest)
        return
    }

    mu.Lock()
    _, ok := books[id]
    if !ok {
        mu.Unlock()
        http.Error(w, "A könyv nem található", http.StatusNotFound)
        return
    }
    delete(books, id)
    mu.Unlock()

    w.WriteHeader(http.StatusNoContent) // 204 No Content státuszkód (sikeres törlés, nincs válasz törzs)
}
    

Router és Fő Server Beállítás

Most, hogy megvannak a handler függvényeink, össze kell kötnünk őket a megfelelő URL útvonalakkal és HTTP metódusokkal. A net/http.ServeMux használatával:


// main.go (a main függvényben)

func main() {
    // A router létrehozása
    mux := http.NewServeMux()

    // Regisztráljuk a handler függvényeket
    // A GET és POST kéréseket a /books útvonalra a booksHandler fogja kezelni
    mux.HandleFunc("/books", func(w http.ResponseWriter, r *http.Request) {
        switch r.Method {
        case http.MethodGet:
            getBooksHandler(w, r)
        case http.MethodPost:
            createBookHandler(w, r)
        default:
            http.Error(w, "Nem engedélyezett metódus", http.StatusMethodNotAllowed)
        }
    })

    // A GET, PUT és DELETE kéréseket a /books/{id} útvonalra
    // Mivel a ServeMux nem tudja kezelni a paramétereket,
    // a /books/ útvonalat regisztráljuk, és a handleren belül vizsgáljuk az ID-t.
    mux.HandleFunc("/books/", func(w http.ResponseWriter, r *http.Request) {
        switch r.Method {
        case http.MethodGet:
            getBooksHandler(w, r)
        case http.MethodPut:
            updateBookHandler(w, r)
        case http.MethodDelete:
            deleteBookHandler(w, r)
        default:
            http.Error(w, "Nem engedélyezett metódus", http.StatusMethodNotAllowed)
        }
    })

    fmt.Println("A Go REST API elindult a :8080 porton...")
    log.Fatal(http.ListenAndServe(":8080", mux)) // A mux-ot adjuk át a ListenAndServe-nek
}
    

A Teljes main.go Fájl

Összegyűjtve az összes kódrészletet, a main.go fájlodnak valahogy így kell kinéznie:


package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"strconv"
	"strings"
	"sync"
)

// Book reprezentál egy könyvet
type Book struct {
	ID     int    `json:"id"`
	Title  string `json:"title"`
	Author string `json:"author"`
	ISBN   string `json:"isbn"`
}

// globális változók az adatok tárolására és a konkurens hozzáférés védelmére
var (
	books  = make(map[int]Book)
	mu     sync.Mutex // Mutex a map védelméhez
	nextID = 1        // Következő rendelkezésre álló ID
)

func init() {
	// Kezdeti adatok hozzáadása
	books[nextID] = Book{ID: nextID, Title: "A Gyűrűk Ura", Author: "J.R.R. Tolkien", ISBN: "978-9633041927"}
	nextID++
	books[nextID] = Book{ID: nextID, Title: "1984", Author: "George Orwell", ISBN: "978-9630784914"}
	nextID++
}

// getBooksHandler kezeli a GET /books és GET /books/{id} kéréseket
func getBooksHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")

	pathParts := strings.Split(r.URL.Path, "/")
	if len(pathParts) == 3 && pathParts[2] != "" {
		idStr := pathParts[2]
		id, err := strconv.Atoi(idStr)
		if err != nil {
			http.Error(w, "Érvénytelen könyv ID formátum", http.StatusBadRequest)
			return
		}

		mu.Lock()
		book, ok := books[id]
		mu.Unlock()

		if !ok {
			http.Error(w, "A könyv nem található", http.StatusNotFound)
			return
		}
		json.NewEncoder(w).Encode(book)
		return
	}

	mu.Lock()
	allBooks := make([]Book, 0, len(books))
	for _, book := range books {
		allBooks = append(allBooks, book)
	}
	mu.Unlock()

	json.NewEncoder(w).Encode(allBooks)
}

// createBookHandler kezeli a POST /books kéréseket
func createBookHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")

	var newBook Book
	err := json.NewDecoder(r.Body).Decode(&newBook)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	mu.Lock()
	newBook.ID = nextID
	books[nextID] = newBook
	nextID++
	mu.Unlock()

	w.WriteHeader(http.StatusCreated)
	json.NewEncoder(w).Encode(newBook)
}

// updateBookHandler kezeli a PUT /books/{id} kéréseket
func updateBookHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")

	pathParts := strings.Split(r.URL.Path, "/")
	if len(pathParts) != 3 || pathParts[2] == "" {
		http.Error(w, "Érvénytelen könyv ID", http.StatusBadRequest)
		return
	}
	idStr := pathParts[2]
	id, err := strconv.Atoi(idStr)
	if err != nil {
		http.Error(w, "Érvénytelen könyv ID formátum", http.StatusBadRequest)
		return
	}

	var updatedBook Book
	err = json.NewDecoder(r.Body).Decode(&updatedBook)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	updatedBook.ID = id

	mu.Lock()
	_, ok := books[id]
	if !ok {
		mu.Unlock()
		http.Error(w, "A könyv nem található", http.StatusNotFound)
		return
	}
	books[id] = updatedBook
	mu.Unlock()

	json.NewEncoder(w).Encode(updatedBook)
}

// deleteBookHandler kezeli a DELETE /books/{id} kéréseket
func deleteBookHandler(w http.ResponseWriter, r *http.Request) {
	pathParts := strings.Split(r.URL.Path, "/")
	if len(pathParts) != 3 || pathParts[2] == "" {
		http.Error(w, "Érvénytelen könyv ID", http.StatusBadRequest)
		return
	}
	idStr := pathParts[2]
	id, err := strconv.Atoi(idStr)
	if err != nil {
		http.Error(w, "Érvénytelen könyv ID formátum", http.StatusBadRequest)
		return
	}

	mu.Lock()
	_, ok := books[id]
	if !ok {
		mu.Unlock()
		http.Error(w, "A könyv nem található", http.StatusNotFound)
		return
	}
	delete(books, id)
	mu.Unlock()

	w.WriteHeader(http.StatusNoContent)
}

func main() {
	mux := http.NewServeMux()

	mux.HandleFunc("/books", func(w http.ResponseWriter, r *http.Request) {
		switch r.Method {
		case http.MethodGet:
			getBooksHandler(w, r)
		case http.MethodPost:
			createBookHandler(w, r)
		default:
			http.Error(w, "Nem engedélyezett metódus", http.StatusMethodNotAllowed)
		}
	})

	mux.HandleFunc("/books/", func(w http.ResponseWriter, r *http.Request) {
		switch r.Method {
		case http.MethodGet:
			getBooksHandler(w, r)
		case http.MethodPut:
			updateBookHandler(w, r)
		case http.MethodDelete:
			deleteBookHandler(w, r)
		default:
			http.Error(w, "Nem engedélyezett metódus", http.StatusMethodNotAllowed)
		}
	})

	fmt.Println("A Go REST API elindult a :8080 porton...")
	log.Fatal(http.ListenAndServe(":8080", mux))
}
    

API Tesztelése

Indítsd el a szervert:


go run main.go
    

Most használhatsz curl-t (vagy Postman, Insomnia programokat) az API teszteléséhez:

  • Összes könyv lekérése:
    
    curl http://localhost:8080/books
                
  • Egy konkrét könyv lekérése (pl. ID=1):
    
    curl http://localhost:8080/books/1
                
  • Új könyv létrehozása:
    
    curl -X POST -H "Content-Type: application/json" -d '{"title":"Dűne", "author":"Frank Herbert", "isbn":"978-9630784914"}' http://localhost:8080/books
                
  • Könyv frissítése (pl. az újonnan létrehozott könyv ID-je legyen 3):
    
    curl -X PUT -H "Content-Type: application/json" -d '{"id":3, "title":"Dűne: A próféta", "author":"Frank Herbert", "isbn":"978-9630784914"}' http://localhost:8080/books/3
                
  • Könyv törlése (pl. ID=3):
    
    curl -X DELETE http://localhost:8080/books/3
                

Gyakorlati Tippek és Következő Lépések

Ez az API egy nagyszerű kiindulópont, de egy éles (production) környezetben számos további szempontot figyelembe kell venni:

  • Adatbázis integráció: Helyettesítsd az in-memory tárolást egy igazi adatbázissal (pl. PostgreSQL, MongoDB). Használhatsz ORM-et (pl. GORM) vagy adatbázis-illesztőprogramokat (pl. database/sql).
  • Hiba kezelés: A hibaüzenetek legyenek részletesebbek, és a kliens számára is értelmezhetőek (pl. hibakódok, validációs üzenetek).
  • Validáció: Validáld a beérkező adatokat (pl. ne lehessen üres címmel könyvet létrehozni).
  • Robusztusabb routing: Bár a net/http.ServeMux elegendő az alapokhoz, komplexebb útválasztáshoz (pl. middleware, path paraméterek közvetlen kezelése) érdemes lehet külső router könyvtárakat (pl. Gorilla Mux, Chi) használni. Ezek sokkal tisztább kódot eredményeznek.
  • Middleware: Használj middleware-eket a keresztmetszeti aggodalmak kezelésére, mint például autentikáció, autorizáció, logolás, CORS fejlécek.
  • Autentikáció és Autorizáció: Egy valós API-nak szüksége van mechanizmusokra a felhasználók azonosítására és jogosultságaik ellenőrzésére (pl. JWT tokenek, OAuth2).
  • Logolás: Részletes logolás segít a hibakeresésben és a rendszer monitorozásában.
  • Tesztelés: Írj egység- és integrációs teszteket az API végpontokhoz.
  • Konfiguráció: Ne hardkódold az érzékeny adatokat (pl. adatbázis jelszavak). Használj környezeti változókat vagy konfigurációs fájlokat.
  • Dokumentáció: Dokumentáld az API-dat (pl. OpenAPI/Swagger segítségével), hogy más fejlesztők könnyen használhassák.

Összefoglalás

Gratulálok! Megépítetted az első REST API-dat Go-ban, kizárólag a net/http csomag segítségével. Ez a projekt megmutatta, hogy a Go beépített eszközei milyen hatékonyak és egyszerűek lehetnek egy alapvető, de működőképes API létrehozásához. Megismerkedtél a HTTP metódusokkal, a JSON adatok kezelésével, a routing alapjaival és a konkurens hozzáférés védelmével.

A Go egyszerűsége, teljesítménye és a net/http csomag rugalmassága ideális választássá teszi API-k és mikro-szolgáltatások építéséhez. Ne állj meg itt! Kísérletezz, mélyedj el a Go további funkcióiban, és építs egyre összetettebb, robusztusabb rendszereket. A tudás, amit ma szereztél, szilárd alapot nyújt a további Go-s webfejlesztési utadhoz. Boldog kódolást!

Leave a Reply

Az e-mail címet nem tesszük közzé. A kötelező mezőket * karakterrel jelöltük