A Pit Stop with Gin-Gonic 🥃 in Golang
Backend Developer - Cybersecurity Enthusiast
Sviluppo principalmente in Golang. Nel tempo libero mi interesso di Crypto e Cybersecurity.
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.
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.
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")
}()
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.
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.
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
}
🔍 Esaminiamo Gin, Echo e Fiber per capire le loro differenze e i punti di forza.
Gin è noto per la sua velocità e semplicità. Offre un routing performante, middleware facile da usare e ottima gestione degli errori.
Echo è un framework altamente personalizzabile con funzionalità come il binding automatico e il rendering di template. Tuttavia, può essere più verboso di Gin.
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.
🏆 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.
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
}
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
...
}
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"`
}
...
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)
}
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
}
... 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
}
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
}
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)
...
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))
}
}
}
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",
})
}
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))
}
}
}
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)
}
}
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
},
...
]
I test in Go sono scritti utilizzando il pacchetto `testify`.
La convenzione prevede che i file di test abbiano il suffisso `_test.go`.
È 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 └── ...
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")
}
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)
})
}
Se avete ulteriori domande o feedback, non esitate a contattarmi.
![]() |
@xm1k3 | ![]() |
![]() |
@mihai-gabriel-canea | ![]() |
![]() |
@xm1k3_ | ![]() |