Construire une API simple en utilisant PostgreSQL, Gin et Gorm étape par étape
Il ne faut pas cacher le fait que je suis un ardent défenseur de C# depuis de nombreuses années. Cela fait plus d’une décennie que je me tourne vers le langage de programmation, et ces dernières années, le langage lui-même et ses cadres périphériques ont parcouru un long chemin.
L’un des aspects les moins attrayants de C # est qu’il est extrêmement verbeux. Même pour écrire un projet relativement petit, la quantité de projets, de fichiers et de lignes de code explose rapidement.
Pour garder les choses gérables, en particulier dans les grands projets, de nombreux modèles et meilleures pratiques ont émergé au fil des ans. Pour la plupart des projets Web modernes en C #, l’injection de dépendances est la norme, les bases de données sont accessibles via un ORM et le routage est géré par un cadre élaboré.
Tout cela touche à la complexité. Pour gérer cette complexité, des architectures comme la Architecture d’oignon sont introduites, compliquant encore plus les choses. Vous vous retrouvez avec un projet où, même pour effectuer le changement le plus fondamental, vous aurez besoin d’une solide compréhension de tous les concepts mentionnés ci-dessus.
J’écris du code depuis plus de quinze ans. Et au cours de ces années, il y a un principe que j’en suis venu à considérer comme l’un des concepts fondamentaux de l’écriture de bon code : dans tout code que vous écrivez, tailler impitoyablement la complexité.
Il y a une langue qui, je pense, incarne vraiment l’uniformité et la simplicité, et cette langue est Va.
La plupart du nouveau code que j’ai écrit au cours des derniers mois a été en Go, et ça a été une bouffée d’air frais. Je partagerai certaines des choses que j’ai apprises en cours de route dans cet article, car nous allons construire une API très rudimentaire.
Tout d’abord. Vous devrez installer le Aller aux outils de développement, et vous aurez besoin d’un éditeur de code. Au moment d’écrire ces lignes, j’utilise Go 1.19 et j’utilise Visual Studio Code avec l’extension Go comme éditeur.
Vous pouvez faire la plupart des choses avec Go directement depuis votre ligne de commande, donc un IDE est complètement facultatif. Vous pouvez même utiliser le Bloc-notes si vous le souhaitez.
Une fois que vous êtes prêt, vérifiez que vous avez tout configuré correctement en créant un nouveau dossier et en exécutant :
$ go mod init aevitas.dev/go-api
go: creating new go.mod: module aevitas.dev/go-api
Vous devriez voir un go.mod
fichier dans votre nouveau dossier contenant les éléments suivants :
module aevitas.dev/go-apigo 1.19
Si vous le souhaitez, vous pouvez modifier le nom du package, c’est-à-dire aevitas.dev/go-api
– mais gardez à l’esprit que vous devrez appliquer ce changement pour tous les import
déclarations plus tard, aussi!
Le point d’entrée d’une application Go est package main
— ce paquet doit contenir une fonction appelée main()
qui sera appelée à chaque démarrage de votre application.
Parce que nous ne voulons pas écrire tout le code de notre API nous-mêmes, mais plutôt utiliser des packages pour des choses comme le routage des requêtes et l’obtention de variables d’environnement, nous devrons nous familiariser avec le gestionnaire de packages de Go.
Contrairement aux gestionnaires de packages comme NuGet ou NPM, le gestionnaire de packages Go peut récupérer un package à partir de n’importe quelle URL que vous lancez. go get
tant que l’URL de destination contient un package valide.
Pour commencer, nous devrons récupérer la chaîne de connexion à la base de données à partir de nos variables d’environnement. Vous pouvez soit définir les variables d’environnement directement, soit utiliser un .env
fichier pour les stocker. Nous utiliserons ce dernier.
Dans votre dossier de projet, exécutez go get github.com/joho/godotenv
— ceci installera le godotenv
package dans votre projet.
Une fois que c’est en place, créez un fichier appelé main.go
dans le même répertoire que go.mod
et ajoutez le code suivant :
package mainimport (
"log"
"os"
"github.com/joho/godotenv"
)
func main() {
err := godotenv.Load()
if err != nil {
log.Fatal(err)
}
dsn := os.Getenv("DB_DSN")
}
Vous recevrez probablement un avertissement disant dsn
est inutilisé. C’est correct. Nous reviendrons sur ce fichier plus tard et corrigerons cela. Si cela vous dérange vraiment, ajoutez simplement //
au début pour commenter cette ligne.
Nous aurons affaire à — surprise — livres comme sujet de notre API. Avant de pouvoir le faire, nous devrons définir ce qui comprend un livre dans notre domaine.
C’est juste une façon élégante de dire que nous devrons créer un modèle pour notre livre. Dans Go, tout code susceptible d’être utilisé en externe doit aller dans le pkg
dossier (et inversement, tout code qui jamais doit être utilisé à l’extérieur dans le internal
dossier).
Parce que nous construisons une API, nous voulons que les utilisateurs externes utilisent notre code modèle. Par conséquent, à la racine de votre projet, créez un pkg
dossier contenant un fichier : pkg/book.go
Dans le book.go
fichier, ajoutez le code suivant :
package pkgtype Book struct {
Id uint64 `json:"id"`
ISBN string `json:"isbn"`
Title string `json:"title"`
Author string `json:"author"`
Price float64 `json:"price"`
}
Notez comment le nom du package a changé de main
à pkg
— ceci indique au compilateur que le code réside dans un package différent. Chaque paquet est traité comme une unité distincte, donc pour que nous puissions utiliser ce code, nous devons le définir comme pkg.Book
Étant donné que notre modèle finira par être sérialisé en JSON, nous fournissons le nom de la propriété sérialisée après le type du champ : json:"id"
– le sérialiseur le reconnaîtra et nommera la propriété en conséquence.
C’est assez sur le modèle de livre pour l’instant. Ajoutons quelques fonctionnalités.
Permettez-moi de préfacer cette section en disant qu’il existe de nombreuses façons d’aborder cela, et ce n’est que l’une d’entre elles. Le problème fondamental que j’ai rencontré lors de l’accès à une base de données dans Go, c’est que l’accès à la base de données doit passer par le programme.
Dans ce guide, j’ai fait le Server
type responsable de cela. Je ne suis pas sûr que ce soit la meilleure approche, mais parmi celles que j’ai vues, c’est celle qui me semble la plus logique.
Quoi qu’il en soit, commençons. Dans votre dossier de projet, créez le ./api
dossier et ajoutez un server.go
pour que le chemin ./api/server.go
est valable.
Notre serveur se compose de deux composants : un pointeur de base de données et un routeur. Pour les garder organisés, nous les définissons dans un struct
— une structure de données pouvant contenir ces deux éléments. Le principe de base du code ressemble à ceci :
package apitype Server struct {
DB *gorm.DB
Gin *gin.Engine
}
Bien sûr, Gorm et Gin sont définis dans des packages externes. Il va falloir les récupérer :
$ go get gorm.io/gorm
$ go get gorm.io/driver/postgres
$ go get github.com/gin-gonic/gin
Notez que dans ce tutoriel, j’utiliserai un serveur PostgreSQL local. Si vous n’en avez pas, c’est parfaitement bien d’utiliser SQLite à la place – juste go get gorm.io/driver/sqlite
au lieu.
Ensuite, nous allons écrire quelques fonctions qui nous aideront à configurer notre instance de serveur — initialiser la connexion à la base de données, configurer le routeur, etc. Nous ajouterons également un Ready()
fonction pour indiquer si le serveur est prêt ou non :
func (s *Server) InitDb(dsn string) *Server {
db, err := gorm.Open(postgres.Open(dsn))if err != nil {
log.Fatal(err)
}
s.DB = db
return s
}
func (s *Server) InitGin() *Server {
g := gin.Default()
s.Gin = g
return s
}
func (s *Server) Ready() bool {
return s.DB != nil && s.Gin != nil
}
Après avoir inséré ce code, vous verrez probablement beaucoup de lignes ondulées rouges, car le compilateur ne peut pas résoudre certaines des références. Cours go mod tidy
dans votre ligne de commande pour atténuer cela – cela garantira que tous les packages se résolvent correctement.
Enfin, si vous utilisez SQLite, remplacez le postgres.Open
appeler avec un sqlite.Open
call, et utilisez simplement un nom de fichier au lieu de passer dsn
– la ligne résultante ressemblerait à quelque chose comme :
db, err := gorm.Open(postgres.Open("books.db"))
Il n’y a pas grand intérêt à avoir un serveur que nous ne pouvons pas démarrer, donc en bas du fichier, ajoutez :
func (s *Server) Start(ep string) error {
if !s.Ready() {
return errors.New("server isn't ready - make sure to init db and gin")
}if err := http.ListenAndServe(ep, s.Gin.Handler()); err != nil {
log.Fatal(err)
}
return nil
}
Nous définissons toutes ces fonctions comme des méthodes de Server
taper. Cela signifie que nous ne pouvons pas les appeler directement, mais devons plutôt créer une instance de serveur et appeler les méthodes par la suite. Nous réglerons cela dans le main.go
dossier.
Revenir à main.go
, nous avons maintenant un type de serveur que nous pouvons instancier et exécuter pour servir notre API. Autour du dsn
ligne de définition, ajoutez ce qui suit :
srv := &api.Server{}dsn := os.Getenv("DB_DSN")
srv.InitDb(dsn)
srv.InitGin()
srv.RegisterRoutes()
srv.Start(":8050")
La première ligne ici crée une nouvelle instance de Server
type, sans spécifier aucun de ses champs. La suite InitDb
et InitGin
les appels sont ceux que nous venons d’implémenter, et vont configurer notre base de données et notre routeur.
Notre finale main.go
le fichier devrait ressembler à ceci :
package mainimport (
"log"
"os"
"aevitas.dev/go-books/api"
"github.com/joho/godotenv"
)
func main() {
err := godotenv.Load()
if err != nil {
log.Fatal(err)
}
srv := &api.Server{}
dsn := os.Getenv("DB_DSN")
srv.InitDb(dsn)
srv.InitGin()
srv.RegisterRoutes()
srv.Start(":8050") // Or grab this from the env, too!
}
L’observateur parmi vous aura remarqué deux choses :
- Nous n’avons pas encore d’API à servir
- nous n’avons pas implémenté
RegisterRoutes
dansserver.go
Et les deux sont corrects. Ajoutons quelques gestionnaires afin que nous puissions commencer à servir du contenu.
Comme avec The Server, il existe de très nombreuses façons de procéder. Pour les besoins de cet article, j’ai décidé une fois de plus de garder les choses simples et d’implémenter la logique directement dans les gestionnaires.
Créer un fichier nommé add_book.go
dans le api
dossier et ajoutez le code suivant :
package apiimport (
"log"
"net/http"
"aevitas.dev/go-books/pkg"
"github.com/gin-gonic/gin"
"github.com/oklog/ulid"
)
func (s *Server) HandleAddBook(ctx *gin.Context) {
var book pkg.Book
err := ctx.BindJSON(&book)
if err != nil {
ctx.AbortWithError(http.StatusBadRequest, err)
return
}
book.Id = ulid.Now()
r := s.DB.Create(&book)
if r.Error != nil {
log.Fatal(r.Error)
}
s.DB.Save(&book)
ctx.JSON(http.StatusOK, &book)
}
Assurez-vous de saisir le ulid
empaqueter et exécuter go mod tidy
ensuite.
Le ctx
Le paramètre contient le contexte Gin, ou plutôt le contexte HTTP. Ce contexte contient la demande que nous avons reçue de l’utilisateur, ainsi que la réponse éventuelle que nous lui servirons.
La première chose à faire est de définir un book
variable du modèle que nous avons défini précédemment — pkg.Book
Ensuite, nous tenterons de désérialiser le contenu de la requête sur cette variable. Si tout se passe bien, nous aurons un livre valide avec un titre, un ISBN, un auteur, etc.
Sinon, nous court-circuiterons la demande en appelant ctx.AbortWithError
et spécifiez une mauvaise demande, ainsi que l’erreur que nous avons reçue du sérialiseur.
Notez que même si cela raccourcit le pipeline de réponse, le code continuera à s’exécuter – d’où le return
Parce que ce gestionnaire est déclaré comme une méthode de Server
nous aurons accès au s.DB
pointeur pour accéder à la base de données. Nous allons créer et enregistrer le livre, et si tout se passe bien, renvoyons le livre en tant qu’objet JSON à l’appelant.
Maintenant que nous pouvons créer des livres, nous devrions également pouvoir les récupérer. Ajouter un get_book.go
gestionnaire contenant le code suivant (très similaire) :
package apiimport (
"net/http"
"aevitas.dev/go-books/pkg"
"github.com/gin-gonic/gin"
)
func (s *Server) HandleGetByISBN(ctx *gin.Context) {
var book pkg.Book
isbn := ctx.Param("isbn")
ret := s.DB.First(&book, "isbn = ?", isbn)
if ret.RowsAffected == 0 {
ctx.AbortWithStatus(http.StatusNotFound)
return
}
if ret.Error != nil {
ctx.AbortWithError(http.StatusBadRequest, ret.Error)
return
}
ctx.JSON(http.StatusOK, book)
}
Comme vous pouvez le voir, la principale différence entre les deux est que ce dernier récupère l’ISBN à partir des paramètres de la requête (car la requête sera un GET plutôt qu’un POST, et n’a donc personne) et effectue une simple requête DB.
Maintenant, tout ce qui manque (sauf pour les tests !) est le RegisterRoutes
une fonction.
Go est livré avec un serveur HTTP très solide inclus dans le http
forfait. Nous l’utiliserons pour servir notre API tout en utilisant Gin pour gérer le routage. Gin est beaucoup, beaucoup plus puissant que ce que j’ai montré dans cet article.
Dans le api
package, créez un fichier nommé routes.go
et ajoutez le code suivant :
package apifunc (s *Server) RegisterRoutes() {
s.Gin.GET("/books/:isbn", s.HandleGetByISBN)
s.Gin.POST("/books", s.HandleAddBook)
}
Comme pour les gestionnaires individuels, nous avons déclaré ces fonctions comme des méthodes de Server
— même s’ils se trouvent dans des fichiers différents. Cela n’est possible que pour les fichiers du même package, et c’est ainsi que j’en suis venu à préférer regrouper des éléments de fonctionnalité.
Supposons que vous étendiez également cette API avec des informations détaillées sur l’auteur, vous pouvez déplacer les gestionnaires que nous avons écrits ci-dessus dans un books
paquet, avec son propre RegisterBookRoutes
fonction ou similaire exposée, et faites de même pour authors
– de cette façon, vous pouvez emballer soigneusement votre API dans tranches verticales de fonctionnalité.
C’est tout! Maintenant, ajoutez simplement un .env
fichier et définir le DB_DSN
avec quelque chose du genre :
host=localhost user=foo password=bar dbname=books
Et vous serez prêt. Exécutez votre application en exécutant go run .
et vous devriez pouvoir utiliser l’API sur votre hôte local sur le port 8050.
Nous avons construit une API très simple dans Go qui accède à une base de données pour stocker et récupérer des livres, et avons appliqué une structure de base à notre projet pour l’étendre à l’avenir.
Autant que je sache, il n’y a pas de structure singulière qui soit une « meilleure pratique » ou une norme, comme avec d’autres langages. Cela dépend beaucoup de vos besoins et préférences individuels, et finalement de ce que vous pouvez trouver qui fonctionne. C’est peut-être pour cela que je considère cette langue comme une bouffée d’air frais.
Le code final accompagnant cet article peut être trouvé ici.
Merci d’avoir lu, et j’espère que ce guide vous a été utile !