Accelerating API Development:

A Pit Stop with Gin-Gonic 🥃 in Golang

Panoramica del Talk

  • Introduzione
  • Panoramica di GoLang
  • Introduzione a Gin
  • Confronto tra Gin/Echo/Fiber
  • Architettura del Progetto gof1
  • Creazione di API RESTful con Gin
  • Tests
  • Q&A

Chi sono

Foto Profilo

Backend Developer - Cybersecurity Enthusiast

Sviluppo principalmente in Golang. Nel tempo libero mi interesso di Crypto e Cybersecurity.

GitHub | LinkedIn | X

Storia e Sviluppo di GoLang

GoLang, spesso chiamato Go, è stato sviluppato da Google nel 2007. È stato progettato per migliorare la produttività nella programmazione grazie alla sua semplicità e alla sua capacità di gestire sistemi di grandi dimensioni.

Vantaggi di GoLang

Go è noto per la sua efficienza e performance, simile al C, ma con una sintassi più pulita. Supporta la concorrenza, fondamentale nell'era del cloud computing.

Caratteristiche Principali di GoLang

Le goroutines sono una delle caratteristiche chiave di Go, permettendo la concorrenza leggera e efficiente. L'uso delle interfacce e una robusta gestione degli errori rendono Go un linguaggio potente e flessibile.


					// Esempio di Goroutine
					go func() {
						fmt.Println("Esecuzione concorrente")
					}()
					

Introduzione a Gin Gonic

Gin è un framework web HTTP in Go che offre prestazioni ottimali grazie al suo design minimalista. È uno dei framework più popolari e veloci per Go.

Caratteristiche di Gin

Gin fornisce un routing potente, gestione degli errori, middleware e la capacità di creare API RESTful con facilità. La sua struttura consente di scrivere applicazioni meno verbali e più efficienti.

Esempio di Endpoint con Gin

Ecco un semplice esempio di un endpoint API scritto con Gin:


					package main
			
					import "github.com/gin-gonic/gin"
			
					func main() {
						r := gin.Default()
						r.GET("/ping", func(c *gin.Context) {
							c.JSON(200, gin.H{
								"message": "pong",
							})
						})
						r.Run() // Ascolta sulla porta 8080 per default
					}
					

Confronto tra Framework Web in Go

🔍 Esaminiamo Gin, Echo e Fiber per capire le loro differenze e i punti di forza.

Logo Gin

Gin Gonic ✅

Gin è noto per la sua velocità e semplicità. Offre un routing performante, middleware facile da usare e ottima gestione degli errori.

Logo Echo

Echo

Echo è un framework altamente personalizzabile con funzionalità come il binding automatico e il rendering di template. Tuttavia, può essere più verboso di Gin.

Logo Fiber

Fiber

Fiber si ispira a Express.js e punta sulla facilità d'uso. Nonostante sia user-friendly, in alcuni casi non raggiunge le prestazioni di Gin.

Perché Gin?

🏆 Gin equilibra velocità, facilità d'uso e funzionalità, rendendolo ideale per una vasta gamma di applicazioni web in Go.

Ecco alcuni dati di benchmark che mostrano le prestazioni di Gin rispetto a Echo e Fiber:

Framework Richieste al secondo Latenza media
Gin 12345 req/s 0.2 ms
Echo 11789 req/s 0.25 ms
Fiber 12001 req/s 0.22 ms

⚡ Questi risultati dimostrano la superiore efficienza di Gin in termini di gestione delle richieste e bassa latenza.

Architettura del Progetto gof1

Diagramma del Database

Controller

I Controller gestiscono la logica di interazione con l'utente, ricevendo richieste e inviando risposte.


		type Controller struct {
			Service F1Service
			DB      *gorm.DB
		}
		
		type Options struct {
			Database string
		}

		func NewController(opts Options) Controller {
			db, err := config.ConnectSqlite3(opts.Database)
			if err != nil {
				log.Fatal(err)
			}
			repositories := repositories.F1Repository{
				DB: db,
			}
			service := services.F1Service{
				Repository: repositories,
			}
			c := Controller{
				Service: service,
				DB:      db,
			}
			return c
		}

					

Interfacce

Le interfacce in Go definiscono le firme per i nostri Service e Repository.


	type F1Service interface {
		AddDriver(driver models.Driver) error
		GetDriver(id int) (models.Driver, error)
		GetDrivers(page, limit int) ([]models.Driver, error)
		GetDriversByYear(year int) ([]models.Driver, error)
		GetDriverStandingsByYear(year int) ([]models.DriverStanding, error)
		UpdateDriver(driver models.Driver) error
		DeleteDriver(id int) error
		ImportDriversFromCsv(record []string) error
		...
	}
					

Modelli

I modelli definiscono le strutture dei dati che andremo ad utilizzare


	type Driver struct {
		gorm.Model
		DriverID    int       `gorm:"column:id" gorm:"primary_key" csv:"driverId"`
		DriverRef   string    `gorm:"column:driverRef" csv:"driverRef"`
		Number      string    `gorm:"column:number" csv:"number"`
		Code        string    `gorm:"column:code" csv:"code"`
		Forename    string    `gorm:"column:forename" csv:"forename"`
		Surname     string    `gorm:"column:surname" csv:"surname"`
		DOB         time.Time `gorm:"column:dob" csv:"dob"`
		Nationality string    `gorm:"column:nationality" csv:"nationality"`
		URL         string    `gorm:"column:url" csv:"url"`
	}
	...
					

Service

I Service contengono la logica e interagiscono con i Repository per l'accesso ai dati.


					func (s *F1Service) GetDriver(id int) (models.Driver, error) {
						return s.Repository.GetDriver(id)
					}
					

Repository

I Repository sono responsabili dell'interazione diretta con il database, eseguendo query e aggiornamenti.


					func (r *F1Repository) GetDriver(id int) (models.Driver, error) {
						var driver models.Driver
						r.DB.First(&driver, id)
						return driver, nil
					}
					

Service

... ma aumentiamo di poco la complessità...


	func (f F1Service) GetDriverStandingsByYear(year int) ([]models.DriverStanding, error) {
		if year < 1950 || year > time.Now().Year() {
			return nil, fmt.Errorf("year is out of valid range")
		}
	
		standings, err := f.Repository.GetDriverStandingsByYear(year)
		if err != nil {
			return nil, fmt.Errorf("error retrieving driver standings: %w", err)
		}
	
		if len(standings) == 0 {
			return nil, fmt.Errorf("no driver standings found for year %d", year)
		}
	
		return standings, nil
	}
					

Repository


	func (r F1Repository) GetDriverStandingsByYear(year int) ([]models.DriverStanding, error) {
		var standings []models.DriverStanding
	
		err := r.DB.
			Table("results").
			Select("drivers.id, drivers.forename, drivers.surname, SUM(results.points) as points").
			Joins("JOIN drivers on drivers.id = results.driverId").
			Joins("JOIN races on races.id = results.raceId").
			Where("races.year = ?", year).
			Group("drivers.id, drivers.forename, drivers.surname").
			Order("SUM(results.points) DESC").
			Scan(&standings).Error
	
		return standings, err
	}
					

Creazione di API RESTful con Gin

Illustreremo come Gin semplifica lo sviluppo di API RESTful, focalizzandoci su routing, parametri, middleware e operazioni CRUD.


		...
		databaseFlag, _ := rootCmd.PersistentFlags().GetString("database")

		opts := pkg.Options{
			Database: databaseFlag,
		}

		newController := pkg.NewController(opts)

		router := gin.New()
		setupRouter(router, newController)

		router.Run(":" + port)
		...
					

Configurazione delle Route

Le route GET non richiedono autenticazione, rendendo le informazioni disponibili pubblicamente.


func setupRouter(router *gin.Engine, controller pkg.Controller) {
	v1 := router.Group("/v1")
	{
		v1.GET("/driver/:id", api.GetDriver(controller))
		v1.GET("/drivers/", api.GetDrivers(controller))
		v1.GET("/drivers/year/:year", api.GetDriversByYear(controller))
		v1.GET("/drivers/standings/:year", api.GetDriverStandingsByYear(controller))
	}

	v1Auth := router.Group("/v1")
	{
		v1Auth.Use(BasicAuth())
		{
			v1Auth.POST("/drivers", api.AddDriver(controller))
		}
	}
}										
					

Configurazione della Basic Auth con Gin

Utilizzo del middleware di Gin per applicare la Basic Auth a specifiche route.


					// BasicAuth middleware
					// Username: admin, Password: password
					func BasicAuth() gin.HandlerFunc {
						return gin.BasicAuth(gin.Accounts{
							"admin": "password",
						})
					}
					

Configurazione delle Route

Le route POST richiedono invece autenticazione, mettiamo come middleware BasicAuth()


	func setupRouter(router *gin.Engine, controller pkg.Controller) {
		v1 := router.Group("/v1")
		{
			v1.GET("/driver/:id", api.GetDriver(controller))
			v1.GET("/drivers/", api.GetDrivers(controller))
			v1.GET("/drivers/year/:year", api.GetDriversByYear(controller))
		}

		v1Auth := router.Group("/v1")
		{
			v1Auth.Use(BasicAuth())
			{
				v1Auth.POST("/drivers", api.AddDriver(controller))
			}
		}
	}						
					

Esempio

Prendiamo in esempio GetDriverStandingsByYear() visto prima e proviamo ora ad assemlare i pezzi


	func GetDriverStandingsByYear(controller pkg.Controller) gin.HandlerFunc {
		return func(c *gin.Context) {
			driverIDStr := c.Param("year")
			driverID, err := strconv.Atoi(driverIDStr)
			if err != nil {
				c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid year param"})
				return
			}
			driverStanding, err := controller.Service.GetDriverStandingsByYear(driverID)
			if err != nil {
				c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
				return
			}
			c.JSON(http.StatusOK, driverStanding)
		}
	}				
					

Curl

Proviamo ora a fare la richiesta con cUrl


	curl localhost:8080/v1/drivers/standings/2023 | jq

	[
	{
		"forename": "Max",
		"surname": "Verstappen",
		"points": 292
	},
	{
		"forename": "Sergio",
		"surname": "Pérez",
		"points": 174
	},
	{
		"forename": "Lewis",
		"surname": "Hamilton",
		"points": 144
	},
	...
]
					

Tests

Introduzione ai Test in Go

I test in Go sono scritti utilizzando il pacchetto `testify`.

La convenzione prevede che i file di test abbiano il suffisso `_test.go`.

Struttura del Progetto per i Test

È comune avere una struttura del progetto separata per i test. Ad esempio:

				project/
				├── main/
				│   ├── main.go
				│   └── ...
				├── pkg/
				│   ├── main.go
				│   ├── controller.go
				│   ├── service.go
				│   └── ...
				└── test/
					├── main_test.go
					├── controller_test.go
					├── service_test.go
					└── ...
				

Esecuzione dei Test

Eseguire i test è semplice utilizzando il comando `go test` dalla radice del progetto:


					$ go get github.com/stretchr/testify
					

E modifica i test utilizzando assert:


					import "github.com/stretchr/testify/assert"

					func TestExample(t *testing.T) {
						assert.Equal(t, 123, 123, "they should be equal")
					}
					

Esempio di Test per il Controller

Creiamo un file di test `controller_test.go` per il package `pkg`.


	func TestGetDriver(t *testing.T) {
		// Setup
		controller := NewController(Options{
			Database: "test.db",
		})
	

		t.Run("GetDriver - Valid ID", func(t *testing.T) {
			req, _ := http.NewRequest("GET", "/v1/driver/1", nil)
			resp := httptest.NewRecorder()

			router := gin.New()
			setupRouter(router, controller)
			router.ServeHTTP(resp, req)

			assert.Equal(t, http.StatusOK, resp.Code)
		})
	}
					

Q&A

Grazie

Se avete ulteriori domande o feedback, non esitate a contattarmi.

Seguimi sui Social

GitHub Logo @xm1k3 GitHub QR Code
LinkedIn Logo @mihai-gabriel-canea LinkedIn QR Code
Twitter Logo @xm1k3_ Twitter QR Code