Comme le titre l'indique, j'ai créé un serveur API avec Go x MySQL x Docker. au début,
・ Créez une API REST simple avec Go ・ Mettre en place un environnement de développement avec Docker
Je le faisais dans le but de, mais quand je me demandais quoi faire avec la structure de répertoire et que je recherchais diverses choses, j'ai trouvé de nombreux articles qui fabriquent des serveurs API avec une architecture Go × clean, alors j'ai décidé d'essayer une architecture propre. Je l'ai essayé.
Donc cet article est
・ Je souhaite écrire une API dans Go ・ Je souhaite créer un environnement de développement avec Docker ・ A cette époque, j'aimerais adopter une architecture épurée
Recommandé pour ceux qui ont une sensation de température.
Au contraire, cela peut ne pas être très utile pour ceux qui souhaitent étudier une architecture solide et propre. ..
De plus, il convient de noter ** Echo ** dans le framework (https://echo.labstack.com/) ** Gorm ** (https://github.com/go-gorm/gorm) pour accéder à la base de données en utilisant.
Cliquez ici pour le référentiel ↓
J'ai construit un environnement de développement avec Docker.
Dockerfile Référence officielle Dockerfile est une bonne référence pour savoir comment écrire un Dockerfile basique.
Maintenant, regardons-les rapidement dans l'ordre.
Le premier est Go.
docker/api/Dockerfile
FROM golang:1.14
#utiliser le module go
ENV GO111MODULE=on
#Spécifiez le répertoire pour exécuter l'application
WORKDIR /go/src/github.com/Le0tk0k/go-rest-api
#Allez dans le répertoire ci-dessus.mod et c'est parti.Copier la somme
COPY go.mod go.sum ./
#S'il n'y a pas de changement dans le fichier ci-dessus, le cache peut être utilisé
RUN go mod download
COPY . .
RUN go build .
RUN go get github.com/pilu/fresh
EXPOSE 8080
#Démarrez le serveur avec la nouvelle commande
CMD ["fresh"]
Activation du rechargement à chaud sur github.com/pilu/fresh. À part cela, il n'y a rien de spécial à ce sujet.
référence ・ Utilisation du téléchargement de go mod pour accélérer les versions de Golang Docker ・ Go v1.11 + Docker + fresh pour créer un environnement de développement de rechargement à chaud et avoir une vie de langage Go agréable
Vient ensuite MySQL.
docker/mysql/Dockerfile
FROM mysql
EXPOSE 3306
#Copiez le fichier de configuration MySQL dans l'image
COPY ./docker/mysql/my.cnf /etc/mysql/conf.d/my.cnf
CMD ["mysqld"]
Le fichier de configuration MySQL ressemble à ceci: Puisque la méthode d'authentification au moment de la connexion a changé dans MySQL 8.0 ou version ultérieure, il semble que la description sur la deuxième ligne soit nécessaire. En outre, le code de caractère a été modifié pour éviter les caractères déformés.
docker/mysql/my.cnf
[mysqld]
default_authentication_plugin=mysql_native_password
character-set-server=utf8mb4
[client]
default-character-set=utf8mb4
Si vous placez le SQL et le script d'initialisation dans le répertoire /docker-entrypoint-initdb.d/, les données peuvent être initialisées automatiquement lorsque l'image est démarrée pour la première fois.
Alors, créons une table.
docker/mysql/db/init.sql
CREATE DATABASE IF NOT EXISTS go_rest_api;
USE go_rest_api;
CREATE TABLE IF NOT EXISTS users (
id INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
name VARCHAR(256) NOT NULL,
age INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
updated_at TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
référence
docker-compose.yml Vous pouvez également consulter Compose File / Official Reference.
docker-compose.yml
version: '3'
services:
db:
build:
#Spécifiez le chemin du répertoire où se trouve le Dockerfile
context: .
dockerfile: ./docker/mysql/Dockerfile
ports:
#Spécifiez le port public
- "3306:3306"
volumes:
#Initialiser les données au démarrage
- ./docker/mysql/db:/docker-entrypoint-initdb.d
#persistance mysql
- ./docker/mysql/db/mysql_data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: go_rest_api
MYSQL_USER: user
MYSQL_PASSWORD: password
api:
build:
context: .
dockerfile: ./docker/api/Dockerfile
volumes:
- ./:/go/src/github.com/Le0tk0k/go-rest-api
ports:
- "8080:8080"
depends_on:
#db commence en premier
- db
Puisque mysql est rendu persistant, les données précédentes resteront même si le conteneur est redémarré.
De plus, en spécifiant depend_on pour api, db démarrera en premier, puis api démarrera. Cependant, depend_on spécifie uniquement l'ordre de démarrage et n'attend pas qu'il soit terminé, une autre mesure est donc requise. Les contre-mesures sont présentées dans la section mise en œuvre.
référence
Clean Architecture est une architecture proposée dans The Clean Architecture.
Suis-je une architecture en premier lieu? Quoi ça? C'était dans un état, mais après avoir lu l'article suivant, je pense avoir compris environ 1 mm de surnageant. Il existe de nombreux autres articles, alors n'hésitez pas à les consulter!
Référence (il existe également des articles autres que l'architecture propre)
La structure des répertoires est la suivante.
├── docker
│ ├── api
│ │ └── Dockerfile
│ └── mysql
│ ├── Dockerfile
│ ├── db
│ │ ├── init.sql
│ │ └── mysql_data
│ └── my.cnf
├── docker-compose.yml
├── domain
│ └── user.go
├── infrastructure
│ ├── router.go
│ └── sqlhandler.go
├── interfaces
│ ├── controllers
│ │ ├── context.go
│ │ ├── error.go
│ │ └── user_controller.go
│ └── database
│ ├── sql_handler.go
│ └── user_repository.go
|── usecase
│ ├── user_interactor.go
│ └── user_repository.go
├── server.go
├── go.mod
├── go.sum
domaine → Couche d'entités infrastructure → Couche Frameworks & Drivers interfaces → Couche d'interface usecase → Couche de cas d'utilisation
Cela correspond à.
La couche la plus interne, indépendante de quoi que ce soit. Nous définirons le modèle utilisateur.
Pour json:" - "
, si vous écrivez-, il ne sera pas affiché.
domain/user.go
package domain
import "time"
type User struct {
ID int `gorm:"primary_key" json:"id"`
Name string `json:"name"`
Age int `json:"age"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
}
type Users []User
Ensuite, nous allons implémenter la zone de base de données. La connexion à la base de données étant une connexion externe, définissez-la dans la couche Infrastructure la plus externe.
infrastructure/sqlhandler.go
package infrastructure
import (
"fmt"
"time"
"github.com/Le0tk0k/go-rest-api/interfaces/database"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
)
type SqlHandler struct {
Conn *gorm.DB
}
func NewMySqlDb() database.SqlHandler {
connectionString := fmt.Sprintf(
"%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=true&loc=Local",
"user",
"password",
"db",
"3306",
"go_rest_api",
)
conn, err := open(connectionString, 30)
if err != nil {
panic(err)
}
//Vérifiez si vous pouvez vous connecter
err = conn.DB().Ping()
if err != nil {
panic(err)
}
//Détails du journal de sortie
conn.LogMode(true)
//Configurer le moteur DB
conn.Set("gorm:table_options", "ENGINE=InnoDB")
sqlHandler := new(SqlHandler)
sqlHandler.Conn = conn
return sqlHandler
}
//Assurez-vous que le conteneur d'API démarre après avoir confirmé le démarrage de MySQL
func open(path string, count uint) (*gorm.DB, error) {
db, err := gorm.Open("mysql", path)
if err != nil {
if count == 0 {
return nil, fmt.Errorf("Retry count over")
}
time.Sleep(time.Second)
count--
return open(path, count)
}
return db, nil
}
func (handler *SqlHandler) Find(out interface{}, where ...interface{}) *gorm.DB {
return handler.Conn.Find(out, where...)
}
func (handler *SqlHandler) Create(value interface{}) *gorm.DB {
return handler.Conn.Create(value)
}
func (handler *SqlHandler) Save(value interface{}) *gorm.DB {
return handler.Conn.Save(value)
}
func (handler *SqlHandler) Delete(value interface{}) *gorm.DB {
return handler.Conn.Delete(value)
}
Dans opan (), le conteneur api est configuré pour démarrer après avoir attendu le démarrage du conteneur db. Cette fois, j'ai implémenté cette fonctionnalité dans le code Go, mais est-il préférable de l'implémenter dans un script shell? Je ne connais pas la meilleure pratique ... référence
De plus, la couche infrastructure n'est qu'une connexion db et le traitement proprement dit est implémenté dans la couche interfaces / base de données.
interfaces/database/user_repository.go
package database
import (
"github.com/Le0tk0k/go-rest-api/domain"
)
type UserRepository struct {
SqlHandler
}
func (userRepository *UserRepository) FindByID(id int) (user domain.User, err error) {
if err = userRepository.Find(&user, id).Error; err != nil {
return
}
return
}
func (userRepository *UserRepository) Store(u domain.User) (user domain.User, err error) {
if err = userRepository.Create(&u).Error; err != nil {
return
}
user = u
return
}
func (userRepository *UserRepository) Update(u domain.User) (user domain.User, err error) {
if err = userRepository.Save(&u).Error; err != nil {
return
}
user = u
return
}
func (userRepository *UserRepository) DeleteByID(user domain.User) (err error) {
if err = userRepository.Delete(&user).Error; err != nil {
return
}
return
}
func (userRepository *UserRepository) FindAll() (users domain.Users, err error) {
if err = userRepository.Find(&users).Error; err != nil {
return
}
return
}
J'intègre un SqlHandler dans le UserRepository. J'importe la couche de domaine, qui est la couche la plus interne, donc les dépendances sont préservées.
Cependant, puisque SqlHandler est défini dans la couche Infrastructure la plus externe, les dépendances sont dirigées de l'intérieur vers l'extérieur et les règles peuvent ne pas être suivies. Cependant, le SqlHandler appelé ici n'est pas une structure définie dans la couche Infrastructure, mais une interface définie dans la couche interface / base de données, qui est la même couche. Ceci est appelé ** DIP (Dependency Reversal Principle) **.
Cela signifie que dans interfaces / database / user_repository.go, si vous appelez le processus défini dans la couche Infrastructure, la dépendance changera de l'intérieur vers l'extérieur et vous ne pourrez pas protéger la dépendance. Afin d'éviter cela, définissons l'interaction avec DB dans la même couche avec interface et appelons-la. Avec cela, bien que le traitement soit réellement effectué dans la couche Infrastructure, la dépendance est protégée car les interfaces / database / user_repository.go appelle l'interface SqlHandler.
Et dans Go, quand ** type T a toutes les méthodes définies dans l'interface I, cela signifie que l'interface I est implémentée **, donc l'interface SqlHandler des interfaces / databases / sql_hander dans la structure Infrastructure.SqlHandler. Définit la méthode définie dans.
interfaces/database/sql_handler.go
package database
import "github.com/jinzhu/gorm"
type SqlHandler interface {
Find(interface{}, ...interface{}) *gorm.DB
Create(interface{}) *gorm.DB
Save(interface{}) *gorm.DB
Delete(interface{}) *gorm.DB
}
La couche Usecase est chargée de recevoir les informations de la couche interfaces / base de données et de les transmettre à la couche interfaces / contrôleur.
usecase/user_interactor.go
package usecase
import "github.com/Le0tk0k/go-rest-api/domain"
type UserInteractor struct {
UserRepository UserRepository
}
func (interactor *UserInteractor) UserById(id int) (user domain.User, err error) {
user, err = interactor.UserRepository.FindByID(id)
return
}
func (interactor *UserInteractor) Users() (users domain.Users, err error) {
users, err = interactor.UserRepository.FindAll()
return
}
func (interactor *UserInteractor) Add(u domain.User) (user domain.User, err error) {
user, err = interactor.UserRepository.Store(u)
return
}
func (interactor *UserInteractor) Update(u domain.User) (user domain.User, err error) {
user, err = interactor.UserRepository.Update(u)
return
}
func (interactor *UserInteractor) DeleteById(user domain.User) (err error) {
err = interactor.UserRepository.DeleteByID(user)
return
}
UserInteractor appelle UserRepository, mais si vous l'appelez depuis la couche Interfase, cela dépendra de l'intérieur → de l'extérieur et vous ne pourrez pas protéger la dépendance, alors créez user_repository.go dans la même couche et implémentez l'interface * Protégez les dépendances en utilisant * DIP (Dependency Reversal Principle) **.
usecase/user_repository.go
package usecase
import "github.com/Le0tk0k/go-rest-api/domain"
type UserRepository interface {
FindByID(id int) (domain.User, error)
Store(domain.User) (domain.User, error)
Update(domain.User) (domain.User, error)
DeleteByID(domain.User) error
FindAll() (domain.Users, error)
}
Ensuite, nous mettrons en œuvre le contrôleur et le routeur.
interfaces/controllers/user_controller.go
package controllers
import (
"strconv"
"github.com/Le0tk0k/go-rest-api/domain"
"github.com/Le0tk0k/go-rest-api/interfaces/database"
"github.com/Le0tk0k/go-rest-api/usecase"
)
type UserController struct {
Interactor usecase.UserInteractor
}
func NewUserController(sqlHandler database.SqlHandler) *UserController {
return &UserController{
Interactor: usecase.UserInteractor{
UserRepository: &database.UserRepository{
SqlHandler: sqlHandler,
},
},
}
}
func (controller *UserController) CreateUser(c Context) (err error) {
u := domain.User{}
c.Bind(&u)
user, err := controller.Interactor.Add(u)
if err != nil {
c.JSON(500, NewError(err))
return
}
c.JSON(201, user)
return
}
func (controller *UserController) GetUsers(c Context) (err error) {
users, err := controller.Interactor.Users()
if err != nil {
c.JSON(500, NewError(err))
return
}
c.JSON(200, users)
return
}
func (controller *UserController) GetUser(c Context) (err error) {
id, _ := strconv.Atoi(c.Param("id"))
user, err := controller.Interactor.UserById(id)
if err != nil {
c.JSON(500, NewError(err))
return
}
c.JSON(200, user)
return
}
func (controller *UserController) UpdateUser(c Context) (err error) {
id, _ := strconv.Atoi(c.Param("id"))
u := domain.User{ID: id}
c.Bind(&u)
user, err := controller.Interactor.Update(u)
if err != nil {
c.JSON(500, NewError(err))
return
}
c.JSON(201, user)
return
}
func (controller *UserController) DeleteUser(c Context) (err error) {
id, _ := strconv.Atoi(c.Param("id"))
user := domain.User{ID: id}
err = controller.Interactor.DeleteById(user)
if err != nil {
c.JSON(500, NewError(err))
return
}
c.JSON(200, user)
return
}
Puisque echo utilise echo.Context, définissez l'interface de la méthode utilisée cette fois.
interfaces/controllers/context.go
package controllers
type Context interface {
Param(string) string
Bind(interface{}) error
JSON(int, interface{}) error
}
interfaces/controllers/error.go
package controllers
type Error struct {
Message string
}
func NewError(err error) *Error {
return &Error{
Message: err.Error(),
}
}
Étant donné que le routage utilise l'écho (à l'aide d'un package externe), implémentez-le dans la couche Infrastructure. Le type echo.Context peut être passé en argument dans uesrController. Fonction (c) car echo.Context satisfait l'interface de contexte d'interfaces / controllers / context.go. (Autrement dit, le type echo.Context a toutes les méthodes de l'interface de contexte.)
infrastructure/router.go
package infrastructure
import (
"github.com/Le0tk0k/go-rest-api/interfaces/controllers"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
)
func Init() {
e := echo.New()
userController := controllers.NewUserController(NewMySqlDb())
//Sortie de journaux basés sur les demandes, tels que les journaux d'accès
e.Use(middleware.Logger())
//Si vous paniquez accidentellement quelque part dans votre application, le serveur se rétablira afin de pouvoir renvoyer une réponse d'erreur sans le laisser tomber.
e.Use(middleware.Recover())
e.GET("/users", func(c echo.Context) error { return userController.GetUsers(c) })
e.GET("/users/:id", func(c echo.Context) error { return userController.GetUser(c) })
e.POST("/users", func(c echo.Context) error { return userController.CreateUser(c) })
e.PUT("/users/:id", func(c echo.Context) error { return userController.UpdateUser(c) })
e.DELETE("/users/:id", func(c echo.Context) error { return userController.DeleteUser(c) })
e.Logger.Fatal(e.Start(":8080"))
}
Enfin, appelez-le depuis server.go.
server.go
package main
import "github.com/Le0tk0k/go-rest-api/infrastructure"
func main() {
infrastructure.Init()
}
Passons maintenant à l'API terminée. Cette fois, j'aimerais utiliser ** Postman **. (https://www.postman.com/downloads/)
Tout d'abord, définissez Content-Type of Headers sur application / json.
GetUsers Essayez d'obtenir tous les utilisateurs. Envoyez une requête GET à / users.
Getuser Essayez d'obtenir n'importe quel utilisateur. Envoyez une requête GET à / users /: id.
CreateUser Commençons par créer un utilisateur. Envoyez une requête POST à / users.
UpdateUser Essayez de mettre à jour n'importe quel utilisateur. Envoyez une requête PUT à / users /: id.
DeleteUser Essayez de supprimer n'importe quel utilisateur. Envoyez une demande DELETE à / users /: id.
Lorsque je le vérifie avec GET, il est supprimé fermement.
Ça marche bien!
J'aurais dû juste mettre en place un environnement de développement avec Docker et créer une API REST, mais c'était difficile d'étudier l'architecture propre, mais je pense que c'était une excellente opportunité de connaître l'architecture. Au contraire, avec une petite API comme celle-ci, cela semble compliqué, mais j'avais l'impression que ce serait bien si c'était à grande échelle. (~~ En fait, je ne comprends pas grand-chose ~~) J'aimerais à nouveau étudier le design!
Recommended Posts