Wie der Titel schon sagt, habe ich mit Go x MySQL x Docker einen API-Server erstellt. am Anfang,
・ Erstellen Sie mit Go eine einfache REST-API ・ Richten Sie mit Docker eine Entwicklungsumgebung ein
Ich habe es zu dem Zweck gemacht, aber als ich mich fragte, was ich mit der Verzeichnisstruktur anfangen und verschiedene Dinge untersuchen sollte, fand ich viele Artikel, die API-Server mit Go × Clean-Architektur herstellen, und entschied mich daher, Clean Architecture auszuprobieren. Ich versuchte es.
Also dieser Artikel ist
・ Ich möchte eine API in Go schreiben ・ Ich möchte mit Docker eine Entwicklungsumgebung erstellen ・ Zu diesem Zeitpunkt möchte ich eine saubere Architektur annehmen
Empfohlen für diejenigen, die ein Gefühl von Temperatur haben.
Im Gegenteil, es ist möglicherweise nicht sehr hilfreich für diejenigen, die eine solide, saubere Architektur studieren möchten. ..
Darüber hinaus sollte beachtet werden ** Echo ** im Framework (https://echo.labstack.com/) ** Gorm ** (https://github.com/go-gorm/gorm), um auf die Datenbank zuzugreifen mit.
Klicken Sie hier für das Repository ↓
Ich habe mit Docker eine Entwicklungsumgebung erstellt.
Dockerfile Dockerfile Official Reference ist eine gute Referenz zum Schreiben einer einfachen Dockerfile.
Schauen wir sie uns jetzt kurz an.
Zuerst ist Go.
docker/api/Dockerfile
FROM golang:1.14
#benutze das go Modul
ENV GO111MODULE=on
#Geben Sie das Verzeichnis an, in dem die Anwendung ausgeführt werden soll
WORKDIR /go/src/github.com/Le0tk0k/go-rest-api
#Gehen Sie zum obigen Verzeichnis.mod und los.Summe kopieren
COPY go.mod go.sum ./
#Wenn sich in der obigen Datei nichts ändert, kann der Cache verwendet werden
RUN go mod download
COPY . .
RUN go build .
RUN go get github.com/pilu/fresh
EXPOSE 8080
#Starten Sie den Server mit dem Befehl fresh
CMD ["fresh"]
Hot Reloading unter github.com/pilu/fresh aktiviert. Davon abgesehen ist nichts Besonderes daran.
Referenz ・ Verwenden des Go-Mod-Downloads zur Beschleunigung der Golang Docker-Builds ・ Go v1.11 + Docker + frisch, um eine Hot-Reload-Entwicklungsumgebung zu erstellen und ein angenehmes Go-Sprachleben zu führen
Als nächstes kommt MySQL.
docker/mysql/Dockerfile
FROM mysql
EXPOSE 3306
#Kopieren Sie die MySQL-Konfigurationsdatei in das Image
COPY ./docker/mysql/my.cnf /etc/mysql/conf.d/my.cnf
CMD ["mysqld"]
Die MySQL-Konfigurationsdatei sieht folgendermaßen aus: Da sich die Authentifizierungsmethode zum Zeitpunkt der Verbindung in MySQL 8.0 oder höher geändert hat, scheint die Beschreibung in der zweiten Zeile erforderlich zu sein. Außerdem wurde der Zeichencode geändert, um verstümmelte Zeichen zu vermeiden.
docker/mysql/my.cnf
[mysqld]
default_authentication_plugin=mysql_native_password
character-set-server=utf8mb4
[client]
default-character-set=utf8mb4
Wenn Sie die Initialisierungs-SQL und das Skript in das Verzeichnis /docker-entrypoint-initdb.d/ stellen, können die Daten beim ersten Start des Images automatisch initialisiert werden.
Erstellen wir also eine Tabelle.
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;
Referenz
docker-compose.yml Sie können auch auf Compose File / Official Reference verweisen.
docker-compose.yml
version: '3'
services:
db:
build:
#Geben Sie den Pfad des Verzeichnisses an, in dem sich die Docker-Datei befindet
context: .
dockerfile: ./docker/mysql/Dockerfile
ports:
#Geben Sie den öffentlichen Port an
- "3306:3306"
volumes:
#Daten beim Start initialisieren
- ./docker/mysql/db:/docker-entrypoint-initdb.d
#MySQL-Persistenz
- ./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 startet zuerst
- db
Da MySQL persistent gemacht wird, bleiben die vorherigen Daten auch dann erhalten, wenn der Container neu gestartet wird.
Wenn Sie abhängige_on für API angeben, wird zuerst db und dann api gestartet. Depend_on gibt jedoch nur die Reihenfolge des Starts an und wartet nicht, bis dieser abgeschlossen ist. Daher ist eine weitere Maßnahme erforderlich. Die Gegenmaßnahmen werden im Implementierungsabschnitt vorgestellt.
Referenz
Clean Architecture ist eine Architektur, die in The Clean Architecture vorgeschlagen wird.
Bin ich überhaupt eine Architektur? Was das? Es war in einem Zustand, aber nachdem ich den folgenden Artikel gelesen hatte, glaube ich, dass ich ungefähr 1 mm Überstand verstanden habe. Es gibt viele andere Artikel, also schaut sie euch bitte an!
Referenz (Es gibt auch andere Artikel als saubere Architektur)
Die Verzeichnisstruktur ist wie folgt.
├── 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
Domain → Entities-Ebene Infrastruktur → Frameworks & Drivers-Ebene Schnittstellen → Schnittstellenschicht Anwendungsfall → Anwendungsfall-Ebene
Es entspricht.
Die innerste Schicht, die von allem unabhängig ist. Wir werden das Benutzermodell definieren.
Für json:" - "
wird beim Schreiben- keine Ausgabe ausgegeben.
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
Als nächstes werden wir den Datenbankbereich implementieren. Da es sich bei der Datenbankverbindung um eine externe Verbindung handelt, definieren Sie sie in der äußersten Infrastrukturschicht.
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)
}
//Überprüfen Sie, ob Sie eine Verbindung herstellen können
err = conn.DB().Ping()
if err != nil {
panic(err)
}
//Details zum Ausgabeprotokoll
conn.LogMode(true)
//Richten Sie die DB-Engine ein
conn.Set("gorm:table_options", "ENGINE=InnoDB")
sqlHandler := new(SqlHandler)
sqlHandler.Conn = conn
return sqlHandler
}
//Stellen Sie sicher, dass der API-Container gestartet wird, nachdem Sie den Start von MySQL bestätigt haben
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)
}
In opan () wird der API-Container so eingestellt, dass er startet, nachdem auf den Start des DB-Containers gewartet wurde. Dieses Mal habe ich diese Funktion in Go-Code implementiert. Ist es jedoch besser, sie in einem Shell-Skript zu implementieren? Ich kenne die beste Praxis nicht ... Referenz
Darüber hinaus ist die Infrastrukturschicht nur eine Datenbankverbindung, und die eigentliche Verarbeitung wird in der Schnittstellen- / Datenbankschicht implementiert.
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
}
Ich binde einen SqlHandler in das UserRepository ein. Ich importiere die Domänenschicht, die die innerste Schicht ist, damit die Abhängigkeiten erhalten bleiben.
Da SqlHandler jedoch in der äußersten Infrastrukturschicht definiert ist, werden die Abhängigkeiten von innen nach außen gerichtet, und die Regeln werden möglicherweise nicht befolgt. Der hier aufgerufene SqlHandler ist jedoch keine in der Infrastrukturschicht definierte Struktur, sondern eine in der Schnittstellen- / Datenbankschicht definierte Schnittstelle, die dieselbe Schicht ist. Dies wird als ** DIP (Dependency Reversal Principle) ** bezeichnet.
Wenn Sie in interfaces / database / user_repository.go den in der Infrastructure-Schicht definierten Prozess aufrufen, ändert sich die Abhängigkeit von innen nach außen, und Sie können die Abhängigkeit nicht schützen. Um dies zu vermeiden, definieren wir die Interaktion mit DB in derselben Ebene mit der Schnittstelle und rufen sie auf. Obwohl die Verarbeitung tatsächlich in der Infrastrukturschicht ausgeführt wird, ist die Abhängigkeit dadurch geschützt, da die Schnittstelle / database / user_repository.go die SqlHandler-Schnittstelle aufruft.
Und in Go, wenn ** Typ T alle in Schnittstelle I definierten Methoden hat, bedeutet dies, dass Schnittstelle I implementiert ist **, also die SqlHandler-Schnittstelle von interfaces / database / sql_hander in der Infrastructure.SqlHandler-Struktur. Definiert die in definierte Methode.
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
}
Die Usecase-Schicht ist dafür verantwortlich, Informationen von der Schnittstellen- / Datenbankschicht zu empfangen und an die Schnittstellen- / Controller-Schicht weiterzuleiten.
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 ruft UserRepository auf, aber wenn Sie es von der Interfase-Ebene aus aufrufen, hängt es von innen → außen ab und Sie können die Abhängigkeit nicht schützen. Erstellen Sie also user_repository.go in derselben Ebene und implementieren Sie die Schnittstelle * Schützen Sie Abhängigkeiten mit * 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)
}
Dann werden wir den Controller und den Router implementieren.
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
}
Da echo echo.Context verwendet, definieren Sie die Schnittstelle der diesmal verwendeten Methode.
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(),
}
}
Da das Routing Echo verwendet (mithilfe eines externen Pakets), implementieren Sie es in der Infrastrukturschicht. Der Typ echo.Context kann als Argument im uesrController übergeben werden. Funktion (c), da echo.Context die Kontextschnittstelle von interfaces / controller / context.go erfüllt. (Das heißt, der Typ echo.Context verfügt über alle Methoden der Kontextschnittstelle.)
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())
//Ausgabeanforderungsbasierte Protokolle wie Zugriffsprotokolle ausgeben
e.Use(middleware.Logger())
//Wenn Sie versehentlich irgendwo in Ihrer Anwendung in Panik geraten, wird der Server wiederhergestellt, sodass er eine Fehlerantwort zurückgeben kann, ohne sie zu löschen.
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"))
}
Rufen Sie es schließlich von server.go auf.
server.go
package main
import "github.com/Le0tk0k/go-rest-api/infrastructure"
func main() {
infrastructure.Init()
}
Lassen Sie uns nun die fertige API treffen. Diesmal möchte ich ** Postman ** verwenden. (https://www.postman.com/downloads/)
Setzen Sie zunächst den Inhaltstyp der Header auf application / json.
GetUsers Versuchen Sie, alle Benutzer zu bekommen. Senden Sie eine GET-Anfrage an / users.
Getuser Versuchen Sie, einen Benutzer zu finden. Senden Sie eine GET-Anfrage an / users /: id.
CreateUser Zuerst erstellen wir einen Benutzer. Senden Sie eine POST-Anfrage an / users.
UpdateUser Versuchen Sie, einen beliebigen Benutzer zu aktualisieren. Senden Sie eine PUT-Anfrage an / users /: id.
DeleteUser Versuchen Sie, einen beliebigen Benutzer zu löschen. Senden Sie eine DELETE-Anfrage an / users /: id.
Wenn ich es mit GET überprüfe, wird es fest gelöscht.
Es funktioniert gut!
Ich hätte einfach eine Entwicklungsumgebung mit Docker einrichten und eine REST-API erstellen sollen, aber es war schwierig, saubere Architektur zu studieren, aber ich denke, es war eine großartige Gelegenheit, die Architektur kennenzulernen. Im Gegenteil, mit einer kleinen API wie dieser scheint es kompliziert zu sein, aber ich hatte den Eindruck, dass es gut wäre, wenn es ein großer Maßstab wäre. (~~ Eigentlich verstehe ich nicht viel ~~) Ich würde gerne wieder Design studieren!
Recommended Posts