Golang API Framework

Während ich Golang kennenlernte, beschloss ich, den Rahmen für die Anwendung festzulegen, mit dem ich in Zukunft bequem arbeiten kann. Das Ergebnis war meiner Meinung nach ein gutes Werkstück, das ich teilen und gleichzeitig die Momente diskutieren wollte, die während der Erstellung des Rahmens entstanden sind.


Bild


Im Prinzip deutet das Design der Go-Sprache darauf hin, dass keine umfangreichen Anwendungen erforderlich sind (ich meine das Fehlen von Generika und einen nicht sehr leistungsfähigen Fehlerbehandlungsmechanismus). Wir wissen jedoch immer noch, dass die Größe der Anwendungen normalerweise nicht abnimmt, sondern häufiger im Gegenteil. Daher ist es besser, sofort ein Framework zu erstellen, in dem neue Funktionen verknüpft werden können, ohne die Codeunterstützung zu beeinträchtigen.


Ich habe versucht, weniger Code in den Artikel einzufügen, stattdessen habe ich Links zu bestimmten Codezeilen auf Github hinzugefügt, in der Hoffnung, dass es bequemer wäre, das gesamte Bild zu sehen.


Zuerst skizzierte ich einen Plan für das, was in der Anwendung enthalten sein sollte. Da ich in dem Artikel über jedes Element einzeln sprechen werde, werde ich zuerst das Hauptelement aus dieser Liste als Inhalt angeben.


  • Wählen Sie den Paketmanager
  • Wählen Sie ein Framework zum Erstellen einer API
  • Wählen Sie das Werkzeug für die Abhängigkeitsinjektion (DI).
  • Webanforderungsrouten
  • JSON / XML-Antworten gemäß Anforderungsheadern
  • ORM
  • Migrationen
  • Erstellen Sie Basisklassen für Modellebenen Service-> Repository-> Entität
  • Grundlegendes CRUD-Repository
  • Grundlegender CRUD-Service
  • Grundlegender CRUD-Controller
  • Validierung anfordern
  • Konfigurationen und Umgebungsvariablen
  • Konsolenbefehle
  • Protokollierung
  • Logger-Integration mit Sentry oder einem anderen Warnsystem
  • Alarm für Fehler einstellen
  • Unit-Tests mit Neudefinition von Diensten durch DI
  • Prozent- und Testabdeckungscode-Karte
  • Prahlerei
  • Docker komponieren

Paketmanager


Nachdem ich die Beschreibungen für verschiedene Implementierungen gelesen hatte , entschied ich mich für den Gouverneur und war im Moment mit der Wahl zufrieden. Der Grund ist einfach: Sie können Abhängigkeiten mit der Anwendung im Verzeichnis installieren und Informationen zu Paketen und deren Versionen speichern.


Informationen zu Paketen und ihren Versionen werden in einer vendor.json- Datei gespeichert . Auch bei diesem Ansatz gibt es ein Minus. Wenn Sie ein Paket mit seinen Abhängigkeiten hinzufügen, werden neben Informationen zum Paket auch Informationen zu seinen Abhängigkeiten in die Datei aufgenommen. Die Datei wächst schnell und es ist nicht mehr möglich, eindeutig zu bestimmen, welche Abhängigkeiten die Hauptabhängigkeiten und welche Ableitungen sind.


In PHP Composer oder in npm werden die Hauptabhängigkeiten in einer Datei beschrieben, und alle Haupt- und abgeleiteten Abhängigkeiten und ihre Versionen werden automatisch in der Sperrdatei aufgezeichnet. Dieser Ansatz ist meiner Meinung nach bequemer. Aber für den Moment hat mir die Implementierung des Gouverneurs gereicht.


Framework


Aus dem Framework brauche ich nicht viel, einen praktischen Router, die Validierung von Anfragen. All dies wurde im beliebten Gin gefunden . Er blieb stehen.


Abhängigkeitsinjektion


Mit DI musste ich ein wenig leiden. Zuerst wählte Dig. Und anfangs war alles super. Beschriebene Dienste, Dig baut bequemerweise weitere Abhängigkeiten auf. Dann stellte sich jedoch heraus, dass Dienste beispielsweise beim Testen nicht neu definiert werden können. Daher kam ich am Ende zu dem Schluss, dass ich einen einfachen Servicebehälter sarulabs / di genommen habe .


Ich musste es nur gabeln, weil es Ihnen sofort erlaubt, Dienste hinzuzufügen, und verbietet, sie neu zu definieren. Und beim Schreiben von Autotests ist es meiner Meinung nach bequemer, den Container wie in der Anwendung zu initialisieren und dann einige der Dienste neu zu definieren und stattdessen Stubs anzugeben. In Fork fügte er eine Methode hinzu, um die Beschreibung des Dienstes zu überschreiben.


Aber am Ende musste ich sowohl im Fall von Dig als auch im Fall des Service-Containers die Tests in ein separates Paket packen. Andernfalls stellt sich heraus, dass die Tests separat in Paketen ausgeführt werden ( go test model/service ), aber aufgrund der auftretenden zyklischen Abhängigkeiten nicht sofort für die gesamte Anwendung gestartet werden ( go test ./... ).


JSON / XML-Antworten gemäß Anforderungsheadern


In Gin habe ich dies nicht gefunden, daher habe ich dem Basis-Controller eine Methode hinzugefügt, die abhängig vom Anforderungsheader eine Antwort generiert.


 func (c BaseController) response(context *gin.Context, obj interface{}, code int) { switch context.GetHeader("Accept") { case "application/xml": context.XML(code, obj) default: context.JSON(code, obj) } } 

ORM


Mit ORM fühlte sich die lange Qual der Wahl nicht an. Es gab viel zur Auswahl. Aber gemäß der Beschreibung der Funktionen mochte ich GORM, das zum Zeitpunkt der Auswahl eines der beliebtesten ist. Das am häufigsten verwendete DBMS wird unterstützt. Zumindest PostgreSQL und MySQL sind definitiv da. Es enthält auch Methoden zum Verwalten des Basisschemas, die Sie beim Erstellen von Migrationen verwenden können.


Migrationen


Für Migrationen habe ich mich für das Gorm-Goose- Paket entschieden. Ich habe ein separates Paket global abgelegt und die Migration dorthin gestartet. Eine solche Implementierung war zunächst peinlich, da die Verbindung zur Datenbank in einer separaten Datei db / dbconf.yml beschrieben werden musste . Dann stellte sich jedoch heraus, dass die darin enthaltene Verbindungszeichenfolge so beschrieben werden kann, dass der Wert der Umgebungsvariablen entnommen wird.


 development: driver: postgres open: $DB_URL 

Und das ist ganz praktisch. Zumindest mit Docker-Compose musste ich die Verbindungszeichenfolge nicht duplizieren.


Gorm-Goose unterstützt auch Migrations-Rollbacks, was ich sehr nützlich finde.


Grundlegendes CRUD-Repository


Ich bevorzuge alles, was sich auf Ressourcen bezieht, in einer separaten Repository-Schicht zu platzieren. Meiner Meinung nach ist bei diesem Ansatz der Geschäftslogikcode sauberer. In diesem Fall weiß der Geschäftslogikcode nur, dass er mit den Daten arbeiten muss, die er aus dem Repository entnimmt. Und was im Repository passiert, ist die Geschäftslogik nicht wichtig. Das Repository kann mit einer relationalen Datenbank, einem KV-Speicher, einer Festplatte oder möglicherweise mit der API eines anderen Dienstes arbeiten. Der Geschäftslogikcode ist in all diesen Fällen derselbe.


Das CRUD-Repository implementiert die folgende Schnittstelle


 type CrudRepositoryInterface interface { BaseRepositoryInterface GetModel() (entity.InterfaceEntity) Find(id uint) (entity.InterfaceEntity, error) List(parameters ListParametersInterface) (entity.InterfaceEntity, error) Create(item entity.InterfaceEntity) entity.InterfaceEntity Update(item entity.InterfaceEntity) entity.InterfaceEntity Delete(id uint) error } 

Das heißt, CRUD implementiert die GetModel() Create() , Find() , List() , Update() , Delete() und die Methode GetModel() .


Über GetModel () . Es gibt ein grundlegendes CrudRepository Repository, das grundlegende CRUD-Operationen implementiert. In den Repositorys, in die es eingebettet ist, reicht es aus, anzugeben, mit welchem ​​Modell sie arbeiten sollen. Dazu muss die GetModel() -Methode ein GORM-Modell zurückgeben. Dann mussten wir das Ergebnis von GetModel() Verwendung von Reflektion in CRUD-Methoden verwenden.


Zum Beispiel


 func (c CrudRepository) Find(id uint) (entity.InterfaceEntity, error) { item := reflect.New(reflect.TypeOf(c.GetModel()).Elem()).Interface() err := c.db.First(item, id).Error return item, err } 

Das heißt, in diesem Fall war es notwendig, die statische Typisierung zugunsten der dynamischen Typisierung aufzugeben. In solchen Momenten ist der Mangel an Generika in der Sprache besonders zu spüren.


Damit die Repositorys, die mit bestimmten Modellen arbeiten, ihre eigenen Regeln zum Filtern von Listen in der List() -Methode implementieren, habe ich zuerst die späte Bindung implementiert, sodass die für die Erstellung der Auswahlabfrage verantwortliche Methode aus der List() -Methode aufgerufen wird. Und diese Methode könnte in einem bestimmten Repository implementiert werden. Es ist schwierig, die Denkmuster, die bei der Arbeit mit anderen Sprachen entstanden sind, irgendwie aufzugeben. Aber als er dies mit einem frischen Blick betrachtete und die „Eleganz“ des gewählten Pfades schätzte, überarbeitete er ihn zu einem Ansatz, der näher an Go liegt. Dazu wird einfach in CrudRepository über die Schnittstelle ein Abfrage-Builder deklariert, der bereits List() .


 listQueryBuilder ListQueryBuilderInterface 

Es wird ziemlich lustig. Die Beschränkung der Sprache auf spätes Binden, was zunächst als Fehler erscheint, fördert eine klarere Trennung des Codes.


Grundlegender CRUD-Service


Hier gibt es nichts Interessantes, da das Framework keine Geschäftslogik enthält. Aufrufe von CRUD-Methoden an das Repository werden einfach weitergeleitet .


In der Serviceschicht muss Geschäftslogik implementiert werden.


Grundlegender CRUD-Controller


Der Controller implementiert CRUD-Methoden . Sie verarbeiten die Parameter aus der Anforderung, die Steuerung wird an die entsprechende Dienstmethode übertragen, und basierend auf der Antwort des Dienstes wird eine Antwort an den Client gebildet.


Mit dem Controller hatte ich die gleiche Geschichte wie mit dem Repository in Bezug auf Filterlisten. Infolgedessen habe ich die Implementierung mit hausgemachter Spätbindung überarbeitet und einen Hydrator hinzugefügt, der basierend auf den Anforderungsparametern eine Struktur mit Parametern zum Filtern der Liste bildet.


In dem mit dem CRUD-Controller gelieferten Hydrator werden nur die Paginierungsparameter verarbeitet. In den spezifischen Reglern, in die der CRUD-Regler integriert ist, kann der Hydrator neu definiert werden .


Validierung anfordern


Die Validierung wird von Gin durchgeführt. Wenn Sie beispielsweise einen Datensatz hinzufügen ( Create() -Methode), reicht es aus, die Elemente der Entitätsstruktur zu dekorieren


 Name string `binding:"required"` 

Die ShouldBindJSON() -Methode des Frameworks überprüft die Anforderungsparameter auf Übereinstimmung mit den im Dekorator beschriebenen Anforderungen.


Konfigurationen und Umgebungsvariablen


Die Implementierung von Viper hat mir sehr gut gefallen, besonders in Verbindung mit Cobra.


Lesen der Konfiguration, die ich in main.go beschrieben habe. Grundlegende Parameter, die keine Geheimnisse enthalten, werden in der Datei base.env beschrieben . Sie können sie in der .env-Datei überschreiben, die zu .gitignore hinzugefügt wird. In .env können Sie geheime Werte für die Umgebung beschreiben.


Umgebungsvariablen haben eine höhere Priorität.


Konsolenbefehle


Für die Beschreibung der Konsolenbefehle habe ich Cobra gewählt . Dann ist es gut, Cobra zusammen mit Viper zu verwenden. Wir können den Befehl beschreiben


 serverCmd.PersistentFlags().StringVar(&serverPort, "port", defaultServerPort, "Server port") 

Binden Sie die Umgebungsvariable an den Wert des Befehlsparameters


 viper.BindPFlag("SERVER_PORT", serverCmd.PersistentFlags().Lookup("port")) 

Tatsächlich ist die gesamte Anwendung dieses Frameworks eine Konsole. Der Webserver wird mit einem der Serverkonsolenbefehle gestartet.


 gin -i run server 

Protokollierung


Ich habe das logrus- Paket für die Protokollierung ausgewählt , da es alles enthält, was ich normalerweise benötige: Festlegen der Protokollierungsstufen, Protokollieren, Hinzufügen von Hooks, z. B. zum Senden von Protokollen an das Warnsystem.


Logger-Integration mit dem Warnsystem


Ich habe mich für Sentry entschieden, weil sich dank der einfachen Integration in logrus: logrus_sentry alles als recht einfach herausstellte . Ich habe die Parameter mit der URL zu Sentry SENTRY_DSN und dem Timeout für das Senden an Sentry SENTRY_TIMEOUT . Es stellte sich heraus, dass das Zeitlimit standardmäßig klein ist, wenn nicht falsch, 300 ms, und viele Nachrichten nicht zugestellt wurden.


Alarm für Fehler einstellen


Ich habe die Panikverarbeitung separat für den Webserver und für Konsolenbefehle durchgeführt .


Unit-Tests mit Neudefinition von Diensten durch DI


Wie oben erwähnt, musste für Unit-Tests ein separates Paket zugewiesen werden. Da die ausgewählte Bibliothek zum Erstellen eines Dienstcontainers keine Neudefinition von Diensten zuließ, wurde in fork eine Methode zum Neudefinieren der Beschreibung von Diensten hinzugefügt. Aus diesem Grund können Sie im Komponententest dieselbe Beschreibung der Dienste wie in der Anwendung verwenden


 dic.InitBuilder() 

Und definieren Sie auf diese Weise nur einige Servicebeschreibungen in Stubs neu


 dic.Builder.Set(di.Def{ Name: dic.UserRepository, Build: func(ctn di.Container) (interface{}, error) { return NewUserRepositoryMock(), nil }, }) 

Als Nächstes können Sie einen Container erstellen und die erforderlichen Dienste im Test verwenden:


 dic.Container = dic.Builder.Build() userService := dic.Container.Get(dic.UserService).(service.UserServiceInterface) 

Daher werden wir userService testen, der anstelle des realen Repositorys den bereitgestellten Stub verwendet.


Prozent- und Testabdeckungscode-Karte
Ich war mit dem Standard-Go-Test-Dienstprogramm völlig zufrieden.


Sie können Tests einzeln ausführen


 go test test/unit/user_service_test.go -v 

Sie können alle Tests gleichzeitig ausführen


 go test ./... -v 

Sie können eine Abdeckungskarte erstellen und den Prozentsatz der Abdeckung berechnen


 go test ./... -v -coverpkg=./... -coverprofile=coverage.out 

Und sehen Sie sich eine Karte der Codeabdeckung mit Tests in einem Browser an


 go tool cover -html=coverage.out 

Prahlerei


Für Gin gibt es ein Gin-Swagger- Projekt, mit dem sowohl Spezifikationen für Swagger als auch darauf basierende Dokumentationen erstellt werden können. Wie sich herausstellte, müssen jedoch Kommentare zu bestimmten Funktionen der Steuerung angegeben werden, um Spezifikationen für bestimmte Vorgänge zu generieren. Dies stellte sich für mich als nicht sehr praktisch heraus, da ich den CRUD-Operationscode nicht in jedem Controller duplizieren wollte. Stattdessen bettete ich in bestimmten Controllern einfach einen CRUD-Controller wie oben beschrieben ein. Ich wollte auch dafür keine Stub-Funktionen erstellen.


Daher bin ich zu dem Schluss gekommen, dass die Spezifikation mit goswagger generiert wird , da in diesem Fall die Operationen beschrieben werden können, ohne an bestimmte Funktionen gebunden zu sein .


 swagger generate spec -o doc/swagger.yml 

Übrigens, mit goswagger können Sie sogar vom Gegenteil ausgehen und den Webserver-Code basierend auf der Swagger-Spezifikation generieren. Bei diesem Ansatz gab es jedoch Schwierigkeiten bei der Verwendung von ORM, und ich gab es schließlich auf.


Die Dokumentation wird mit Gin-Swagger erstellt. Hierzu wird eine vorgenerierte Spezifikationsdatei angezeigt .


Docker komponieren


Im Framework habe ich eine Beschreibung von zwei Containern hinzugefügt - für den Code und für die Basis . Zu Beginn des Containers mit dem Code warten wir, bis der Container mit der Basis vollständig gestartet ist. Und bei jedem Start rollen wir bei Bedarf Migrationen. Die Parameter für die Verbindung zur Datenbank für Migrationen sind, wie oben erwähnt, in dbconf.yml beschrieben , wo die Umgebungsvariable zum Übertragen der Einstellungen für die Verbindung zur Datenbank verwendet werden konnte.


Vielen Dank für Ihre Aufmerksamkeit. Dabei musste ich mich an die Merkmale der Sprache anpassen. Es würde mich interessieren, die Meinung von Kollegen zu erfahren, die mehr Zeit mit Go verbracht haben. Sicherlich könnten einige Momente eleganter gestaltet werden, daher freue ich mich über nützliche Kritik. Link zum Frame: https://github.com/zubroide/go-api-boilerplate

Source: https://habr.com/ru/post/de455302/


All Articles