API-Entwicklung in Go mit Goa

Veröffentlicht: 2022-03-11

API-Entwicklung ist heutzutage ein heißes Thema. Es gibt eine Vielzahl von Möglichkeiten, wie Sie eine API entwickeln und bereitstellen können, und große Unternehmen haben umfangreiche Lösungen entwickelt, die Ihnen helfen, eine Anwendung schnell zu booten.

Den meisten dieser Optionen fehlt jedoch eine Schlüsselfunktion: das Management des Entwicklungslebenszyklus. Entwickler verbringen also einige Zyklen damit, nützliche und robuste APIs zu erstellen, haben aber am Ende Probleme mit der erwarteten organischen Entwicklung ihres Codes und den Auswirkungen, die eine kleine Änderung der API auf den Quellcode hat.

2016 schuf Raphael Simon Goa, ein Framework für die API-Entwicklung in Golang mit einem Lebenszyklus, bei dem das API-Design an erster Stelle steht. In Goa wird Ihre API-Definition nicht nur als Code beschrieben, sondern ist auch die Quelle, aus der Servercode, Clientcode und Dokumentation abgeleitet werden. Das bedeutet, dass Ihr Code in Ihrer API-Definition mit einer Golang Domain Specific Language (DSL) beschrieben, dann mit goa cli generiert und separat von Ihrem Anwendungsquellcode implementiert wird.

Das ist der Grund, warum Goa glänzt. Es handelt sich um eine Lösung mit einem klar definierten Lebenszyklusvertrag für die Entwicklung, der sich beim Generieren von Code auf Best Practices stützt (z. B. das Aufteilen verschiedener Domänen und Bedenken in Schichten, damit Transportaspekte die geschäftlichen Aspekte der Anwendung nicht beeinträchtigen) und einem sauberen Architekturmuster folgt, sofern dies möglich ist Module werden für die Transport-, Endpunkt- und Geschäftslogikschichten in Ihrer Anwendung generiert.

Einige Goa-Funktionen, wie von der offiziellen Website definiert, umfassen:

  • Zusammensetzbarkeit . Das Paket, die Codegenerierungsalgorithmen und der generierte Code sind alle modular.
  • Transport-agnostisch . Die Entkopplung der Transportschicht von der eigentlichen Dienstimplementierung bedeutet, dass derselbe Dienst Endpunkte verfügbar machen kann, auf die über mehrere Transporte wie HTTP und/oder gRPC zugegriffen werden kann.
  • Trennung von Anliegen . Die tatsächliche Dienstimplementierung ist vom Transportcode isoliert.
  • Verwendung von Go-Standardbibliothekstypen . Dies erleichtert die Schnittstelle mit externem Code.

In diesem Artikel erstelle ich eine Anwendung und führe Sie durch die Phasen des API-Entwicklungslebenszyklus. Die Anwendung verwaltet Details über Kunden, wie Name, Adresse, Telefonnummer, soziale Medien usw. Am Ende werden wir versuchen, sie zu erweitern und neue Funktionen hinzuzufügen, um ihren Entwicklungslebenszyklus zu nutzen.

Also lasst uns anfangen!

Vorbereitung Ihres Entwicklungsbereichs

Unser erster Schritt besteht darin, das Repository zu initiieren und die Unterstützung von Go-Modulen zu aktivieren:

 mkdir -p clients/design cd clients go mod init clients

Am Ende sollte Ihre Repo-Struktur wie folgt aussehen:

 $ tree . ├── design └── go.mod

Entwerfen Ihrer API

Die Quelle der Wahrheit für Ihre API ist Ihre Designdefinition. In der Dokumentation heißt es: „Mit Goa können Sie unabhängig von Implementierungsproblemen über Ihre APIs nachdenken und dieses Design dann mit allen Beteiligten überprüfen, bevor Sie die Implementierung schreiben.“ Das bedeutet, dass hier zuerst jedes Element der API definiert wird, bevor der eigentliche Anwendungscode generiert wird. Aber genug geredet!

Öffnen Sie die Datei clients/design/design.go und fügen Sie den folgenden Inhalt hinzu:

 /* This is the design file. It contains the API specification, methods, inputs, and outputs using Goa DSL code. The objective is to use this as a single source of truth for the entire API source code. */ package design import ( . "goa.design/goa/v3/dsl" ) // Main API declaration var _ = API("clients", func() { Title("An api for clients") Description("This api manages clients with CRUD operations") Server("clients", func() { Host("localhost", func() { URI("http://localhost:8080/api/v1") }) }) }) // Client Service declaration with two methods and Swagger API specification file var _ = Service("client", func() { Description("The Client service allows access to client members") Method("add", func() { Payload(func() { Field(1, "ClientID", String, "Client ID") Field(2, "ClientName", String, "Client ID") Required("ClientID", "ClientName") }) Result(Empty) Error("not_found", NotFound, "Client not found") HTTP(func() { POST("/api/v1/client/{ClientID}") Response(StatusCreated) }) }) Method("get", func() { Payload(func() { Field(1, "ClientID", String, "Client ID") Required("ClientID") }) Result(ClientManagement) Error("not_found", NotFound, "Client not found") HTTP(func() { GET("/api/v1/client/{ClientID}") Response(StatusOK) }) }) Method("show", func() { Result(CollectionOf(ClientManagement)) HTTP(func() { GET("/api/v1/client") Response(StatusOK) }) }) Files("/openapi.json", "./gen/http/openapi.json") }) // ClientManagement is a custom ResultType used to configure views for our custom type var ClientManagement = ResultType("application/vnd.client", func() { Description("A ClientManagement type describes a Client of company.") Reference(Client) TypeName("ClientManagement") Attributes(func() { Attribute("ClientID", String, "ID is the unique id of the Client.", func() { Example("ABCDEF12356890") }) Field(2, "ClientName") }) View("default", func() { Attribute("ClientID") Attribute("ClientName") }) Required("ClientID") }) // Client is the custom type for clients in our database var Client = Type("Client", func() { Description("Client describes a customer of company.") Attribute("ClientID", String, "ID is the unique id of the Client Member.", func() { Example("ABCDEF12356890") }) Attribute("ClientName", String, "Name of the Client", func() { Example("John Doe Limited") }) Required("ClientID", "ClientName") }) // NotFound is a custom type where we add the queried field in the response var NotFound = Type("NotFound", func() { Description("NotFound is the type returned when " + "the requested data that does not exist.") Attribute("message", String, "Message of error", func() { Example("Client ABCDEF12356890 not found") }) Field(2, "id", String, "ID of missing data") Required("message", "id") })

Das erste, was Sie bemerken können, ist, dass die obige DSL eine Reihe von Go-Funktionen ist, die zusammengesetzt werden können, um eine Remote-Service-API zu beschreiben. Die Funktionen werden unter Verwendung anonymer Funktionsargumente zusammengesetzt. In den DSL-Funktionen haben wir eine Teilmenge von Funktionen, die nicht in anderen Funktionen erscheinen sollen, die wir Top-Level-DSLs nennen. Unten haben Sie einen Teilsatz von DSL-Funktionen und deren Struktur:

DSL-Funktionen

Wir haben also in unserem anfänglichen Design eine API-Top-Level-DSL, die die API unseres Clients beschreibt, einen Service-Top-Level-DSLs, der den Haupt-API-Service, die clients und die Bereitstellung der API-Swagger-Datei beschreibt, und zwei Top-Level-DSLs zur Beschreibung der Objektansichtstyp, der in der Transportnutzlast verwendet wird.

Die API -Funktion ist eine optionale Top-Level-DSL, die die globalen Eigenschaften der API auflistet, wie z. B. einen Namen, eine Beschreibung und auch einen oder mehrere Server, die möglicherweise verschiedene Sätze von Diensten verfügbar machen. In unserem Fall reicht ein Server aus, aber Sie könnten auch verschiedene Dienste in verschiedenen Ebenen bereitstellen: Entwicklung, Test und Produktion zum Beispiel.

Die Service definiert eine Gruppe von Methoden, die potenziell einer Ressource im Transport zugeordnet werden. Ein Dienst kann auch allgemeine Fehlerantworten definieren. Die Dienstmethoden werden mit Method beschrieben. Diese Funktion definiert die Typen Payload (Eingabe) und Ergebnis (Ausgabe). Wenn Sie den Payload- oder Ergebnistyp weglassen, wird der integrierte Typ Empty verwendet, der einem leeren Text in HTTP zugeordnet wird.

Schließlich definieren die Type oder ResultType -Funktionen benutzerdefinierte Typen, wobei der Hauptunterschied darin besteht, dass ein Ergebnistyp auch eine Reihe von „Ansichten“ definiert.

In unserem Beispiel haben wir die API beschrieben und erklärt, wie sie funktionieren soll, und außerdem haben wir Folgendes erstellt:

  • Ein Dienst namens clients
  • Drei Methoden: add (zum Erstellen eines Clients), get (zum Abrufen eines Clients) und show (zum Auflisten aller Clients)
  • Unsere eigenen benutzerdefinierten Typen, die sich bei der Integration in eine Datenbank als nützlich erweisen, und ein benutzerdefinierter Fehlertyp

Nachdem unsere Anwendung nun beschrieben wurde, können wir den Boilerplate-Code generieren. Der folgende Befehl verwendet den Importpfad des Designpakets als Argument. Es akzeptiert auch den Pfad zum Ausgabeverzeichnis als optionales Flag:

 goa gen clients/design

Der Befehl gibt die Namen der erzeugten Dateien aus. Dort enthält das gen -Verzeichnis das Anwendungsnamen-Unterverzeichnis, das den transportunabhängigen Dienstcode beherbergt. Das http -Unterverzeichnis beschreibt den HTTP-Transport (wir haben Server- und Client-Code mit der Logik zum Codieren und Decodieren von Anforderungen und Antworten und den CLI-Code zum Erstellen von HTTP-Anforderungen über die Befehlszeile). Es enthält auch die Open API 2.0-Spezifikationsdateien im JSON- und YAML-Format.

Sie können den Inhalt einer Swagger-Datei kopieren und in einen beliebigen Online-Swagger-Editor (wie den auf swagger.io) einfügen, um Ihre API-Spezifikationsdokumentation zu visualisieren. Sie unterstützen sowohl YAML- als auch JSON-Formate.

Wir sind jetzt bereit für unseren nächsten Schritt im Entwicklungslebenszyklus.

Implementieren Ihrer API

Nachdem Ihr Boilerplate-Code erstellt wurde, ist es an der Zeit, ihm etwas Geschäftslogik hinzuzufügen. An dieser Stelle sollte Ihr Code so aussehen:

API-Entwicklung in Go

Wobei jede obige Datei von Goa gepflegt und aktualisiert wird, wenn wir die CLI ausführen. Wenn sich also die Architektur weiterentwickelt, folgt Ihr Design der Entwicklung, ebenso wie Ihr Quellcode. Um die Anwendung zu implementieren, führen wir den folgenden Befehl aus (er generiert eine grundlegende Implementierung des Dienstes zusammen mit erstellbaren Serverdateien, die Goroutinen starten, um einen HTTP-Server und Clientdateien zu starten, die Anfragen an diesen Server stellen können):

 goa example clients/design

Dadurch wird ein cmd-Ordner mit erstellbaren Server- und Clientquellen generiert. Es wird Ihre Anwendung geben, und das sind die Dateien, die Sie selbst pflegen sollten, nachdem Goa sie zum ersten Mal generiert hat.

Die Goa-Dokumentation macht Folgendes deutlich: „Dieser Befehl generiert einen Ausgangspunkt für den Dienst, um die Bootstrap-Entwicklung zu unterstützen – insbesondere ist er NICHT dazu gedacht, erneut ausgeführt zu werden, wenn sich das Design ändert.“

Jetzt sieht Ihr Code so aus:

API-Entwicklung in Go: cmd-Ordner

Dabei ist client.go eine Beispieldatei mit einer Dummy-Implementierung der Methoden get und show . Lassen Sie uns etwas Geschäftslogik hinzufügen!

Der Einfachheit halber verwenden wir SQLite anstelle einer In-Memory-Datenbank und Gorm als unser ORM. Erstellen Sie die Datei sqlite.go und fügen Sie den folgenden Inhalt hinzu - das fügt Datenbanklogik hinzu, um Datensätze in der Datenbank zu erstellen und eine und/oder mehrere Zeilen aus der Datenbank aufzulisten:

 package clients import ( "clients/gen/client" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/sqlite" ) var db *gorm.DB var err error type Client *client.ClientManagement // InitDB is the function that starts a database file and table structures // if not created then returns db object for next functions func InitDB() *gorm.DB { // Opening file db, err := gorm.Open("sqlite3", "./data.db") // Display SQL queries db.LogMode(true) // Error if err != nil { panic(err) } // Creating the table if it doesn't exist var TableStruct = client.ClientManagement{} if !db.HasTable(TableStruct) { db.CreateTable(TableStruct) db.Set("gorm:table_options", "ENGINE=InnoDB").CreateTable(TableStruct) } return db } // GetClient retrieves one client by its ID func GetClient(clientID string) (client.ClientManagement, error) { db := InitDB() defer db.Close() var clients client.ClientManagement db.Where("client_id = ?", clientID).First(&clients) return clients, err } // CreateClient created a client row in DB func CreateClient(client Client) error { db := InitDB() defer db.Close() err := db.Create(&client).Error return err } // ListClients retrieves the clients stored in Database func ListClients() (client.ClientManagementCollection, error) { db := InitDB() defer db.Close() var clients client.ClientManagementCollection err := db.Find(&clients).Error return clients, err }

Dann bearbeiten wir client.go, um alle Methoden im Client-Service zu aktualisieren, die Datenbankaufrufe zu implementieren und die API-Antworten zu erstellen:

 // Add implements add. func (s *clientsrvc) Add(ctx context.Context, p *client.AddPayload) (err error) { s.logger.Print("client.add started") newClient := client.ClientManagement{ ClientID: p.ClientID, ClientName: p.ClientName, } err = CreateClient(&newClient) if err != nil { s.logger.Print("An error occurred...") s.logger.Print(err) return } s.logger.Print("client.add completed") return } // Get implements get. func (s *clientsrvc) Get(ctx context.Context, p *client.GetPayload) (res *client.ClientManagement, err error) { s.logger.Print("client.get started") result, err := GetClient(p.ClientID) if err != nil { s.logger.Print("An error occurred...") s.logger.Print(err) return } s.logger.Print("client.get completed") return &result, err } // Show implements show. func (s *clientsrvc) Show(ctx context.Context) (res client.ClientManagementCollection, err error) { s.logger.Print("client.show started") res, err = ListClients() if err != nil { s.logger.Print("An error occurred...") s.logger.Print(err) return } s.logger.Print("client.show completed") return }

Der erste Schnitt unserer Anwendung kann kompiliert werden. Führen Sie den folgenden Befehl aus, um Server- und Client-Binärdateien zu erstellen:

 go build ./cmd/clients go build ./cmd/clients-cli

Um den Server auszuführen, führen Sie einfach ./clients . Lass es erstmal laufen. Sie sollten sehen, dass es erfolgreich ausgeführt wird, wie im Folgenden:

 $ ./clients [clients] 00:00:01 HTTP "Add" mounted on POST /api/v1/client/{ClientID} [clients] 00:00:01 HTTP "Get" mounted on GET /api/v1/client/{ClientID} [clients] 00:00:01 HTTP "Show" mounted on GET /api/v1/client [clients] 00:00:01 HTTP "./gen/http/openapi.json" mounted on GET /openapi.json [clients] 00:00:01 HTTP server listening on "localhost:8080"

Wir sind bereit, einige Tests in unserer Anwendung durchzuführen. Lassen Sie uns alle Methoden mit dem CLI ausprobieren:

 $ ./clients-cli client add --body '{"ClientName": "Cool Company"}' \ --client-id "1" $ ./clients-cli client get --client-id "1" { "ClientID": "1", "ClientName": "Cool Company" } $ ./clients-cli client show [ { "ClientID": "1", "ClientName": "Cool Company" } ]

Wenn Sie einen Fehler erhalten, überprüfen Sie die Serverprotokolle, um sicherzustellen, dass die SQLite-ORM-Logik gut ist und keine Datenbankfehler auftreten, z. B. Datenbank nicht initialisiert oder Abfragen, die keine Zeilen zurückgeben.

Erweitern Sie Ihre API

Das Framework unterstützt die Entwicklung von Plugins, um Ihre API zu erweitern und weitere Funktionen einfach hinzuzufügen. Goa hat ein Repository für Plugins, die von der Community erstellt wurden.

Wie ich bereits erläutert habe, können wir uns im Rahmen des Entwicklungslebenszyklus auf das Toolset verlassen, um unsere Anwendung zu erweitern, indem wir zur Designdefinition zurückkehren, sie aktualisieren und unseren generierten Code aktualisieren. Lassen Sie uns zeigen, wie Plugins helfen können, indem wir CORS und Authentifizierung zur API hinzufügen.

Aktualisieren Sie die Datei „ clients/design/design.go auf den folgenden Inhalt:

 /* This is the design file. It contains the API specification, methods, inputs, and outputs using Goa DSL code. The objective is to use this as a single source of truth for the entire API source code. */ package design import ( . "goa.design/goa/v3/dsl" cors "goa.design/plugins/v3/cors/dsl" ) // Main API declaration var _ = API("clients", func() { Title("An api for clients") Description("This api manages clients with CRUD operations") cors.Origin("/.*localhost.*/", func() { cors.Headers("X-Authorization", "X-Time", "X-Api-Version", "Content-Type", "Origin", "Authorization") cors.Methods("GET", "POST", "OPTIONS") cors.Expose("Content-Type", "Origin") cors.MaxAge(100) cors.Credentials() }) Server("clients", func() { Host("localhost", func() { URI("http://localhost:8080/api/v1") }) }) }) // Client Service declaration with two methods and Swagger API specification file var _ = Service("client", func() { Description("The Client service allows access to client members") Error("unauthorized", String, "Credentials are invalid") HTTP(func() { Response("unauthorized", StatusUnauthorized) }) Method("add", func() { Payload(func() { TokenField(1, "token", String, func() { Description("JWT used for authentication") }) Field(2, "ClientID", String, "Client ID") Field(3, "ClientName", String, "Client ID") Field(4, "ContactName", String, "Contact Name") Field(5, "ContactEmail", String, "Contact Email") Field(6, "ContactMobile", Int, "Contact Mobile Number") Required("token", "ClientID", "ClientName", "ContactName", "ContactEmail", "ContactMobile") }) Security(JWTAuth, func() { Scope("api:write") }) Result(Empty) Error("invalid-scopes", String, "Token scopes are invalid") Error("not_found", NotFound, "Client not found") HTTP(func() { POST("/api/v1/client/{ClientID}") Header("token:X-Authorization") Response("invalid-scopes", StatusForbidden) Response(StatusCreated) }) }) Method("get", func() { Payload(func() { TokenField(1, "token", String, func() { Description("JWT used for authentication") }) Field(2, "ClientID", String, "Client ID") Required("token", "ClientID") }) Security(JWTAuth, func() { Scope("api:read") }) Result(ClientManagement) Error("invalid-scopes", String, "Token scopes are invalid") Error("not_found", NotFound, "Client not found") HTTP(func() { GET("/api/v1/client/{ClientID}") Header("token:X-Authorization") Response("invalid-scopes", StatusForbidden) Response(StatusOK) }) }) Method("show", func() { Payload(func() { TokenField(1, "token", String, func() { Description("JWT used for authentication") }) Required("token") }) Security(JWTAuth, func() { Scope("api:read") }) Result(CollectionOf(ClientManagement)) Error("invalid-scopes", String, "Token scopes are invalid") HTTP(func() { GET("/api/v1/client") Header("token:X-Authorization") Response("invalid-scopes", StatusForbidden) Response(StatusOK) }) }) Files("/openapi.json", "./gen/http/openapi.json") }) // ClientManagement is a custom ResultType used to // configure views for our custom type var ClientManagement = ResultType("application/vnd.client", func() { Description("A ClientManagement type describes a Client of company.") Reference(Client) TypeName("ClientManagement") Attributes(func() { Attribute("ClientID", String, "ID is the unique id of the Client.", func() { Example("ABCDEF12356890") }) Field(2, "ClientName") Attribute("ContactName", String, "Name of the Contact.", func() { Example("John Doe") }) Field(4, "ContactEmail") Field(5, "ContactMobile") }) View("default", func() { Attribute("ClientID") Attribute("ClientName") Attribute("ContactName") Attribute("ContactEmail") Attribute("ContactMobile") }) Required("ClientID") }) // Client is the custom type for clients in our database var Client = Type("Client", func() { Description("Client describes a customer of company.") Attribute("ClientID", String, "ID is the unique id of the Client Member.", func() { Example("ABCDEF12356890") }) Attribute("ClientName", String, "Name of the Client", func() { Example("John Doe Limited") }) Attribute("ContactName", String, "Name of the Client Contact.", func() { Example("John Doe") }) Attribute("ContactEmail", String, "Email of the Client Contact", func() { Example("[email protected]") }) Attribute("ContactMobile", Int, "Mobile number of the Client Contact", func() { Example(12365474235) }) Required("ClientID", "ClientName", "ContactName", "ContactEmail", "ContactMobile") }) // NotFound is a custom type where we add the queried field in the response var NotFound = Type("NotFound", func() { Description("NotFound is the type returned " + "when the requested data that does not exist.") Attribute("message", String, "Message of error", func() { Example("Client ABCDEF12356890 not found") }) Field(2, "id", String, "ID of missing data") Required("message", "id") }) // Creds is a custom type for replying Tokens var Creds = Type("Creds", func() { Field(1, "jwt", String, "JWT token", func() { Example("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9" + "lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHD" + "cEfxjoYZgeFONFh7HgQ") }) Required("jwt") }) // JWTAuth is the JWTSecurity DSL function for adding JWT support in the API var JWTAuth = JWTSecurity("jwt", func() { Description(`Secures endpoint by requiring a valid JWT token retrieved via the signin endpoint. Supports scopes "api:read" and "api:write".`) Scope("api:read", "Read-only access") Scope("api:write", "Read and write access") }) // BasicAuth is the BasicAuth DSL function for // adding basic auth support in the API var BasicAuth = BasicAuthSecurity("basic", func() { Description("Basic authentication used to " + "authenticate security principal during signin") Scope("api:read", "Read-only access") }) // Signin Service is the service used to authenticate users and assign JWT tokens for their sessions var _ = Service("signin", func() { Description("The Signin service authenticates users and validate tokens") Error("unauthorized", String, "Credentials are invalid") HTTP(func() { Response("unauthorized", StatusUnauthorized) }) Method("authenticate", func() { Description("Creates a valid JWT") Security(BasicAuth) Payload(func() { Description("Credentials used to authenticate to retrieve JWT token") UsernameField(1, "username", String, "Username used to perform signin", func() { Example("user") }) PasswordField(2, "password", String, "Password used to perform signin", func() { Example("password") }) Required("username", "password") }) Result(Creds) HTTP(func() { POST("/signin/authenticate") Response(StatusOK) }) }) })

Sie können zwei Hauptunterschiede im neuen Design feststellen. Wir haben im client -Dienst einen Sicherheitsbereich definiert, damit wir überprüfen können, ob ein Benutzer berechtigt ist, den Dienst aufzurufen, und wir haben einen zweiten Dienst namens signin definiert, den wir verwenden, um Benutzer zu authentifizieren und JSON-Web-Tokens (JWT) zu generieren, die die client -Service verwendet, um Anrufe zu autorisieren. Außerdem haben wir unserem benutzerdefinierten Client-Typ weitere Felder hinzugefügt. Dies ist ein häufiger Fall bei der Entwicklung einer API – die Notwendigkeit, Daten umzugestalten oder umzustrukturieren.

Beim Design mögen diese Änderungen einfach klingen, aber wenn man darüber nachdenkt, sind viele minimale Funktionen erforderlich, um das zu erreichen, was beim Design beschrieben wird. Nehmen Sie zum Beispiel die Architekturschemata für die Authentifizierung und Autorisierung mit unseren API-Methoden:

API-Entwicklung in Go: Architekturschemata für Authentifizierung und Autorisierung

Das sind alles neue Funktionen, die unser Code noch nicht hat. Auch hier fügt Goa Ihren Entwicklungsbemühungen mehr Wert hinzu. Lassen Sie uns diese Funktionen auf der Transportseite implementieren, indem Sie den Quellcode mit dem folgenden Befehl erneut generieren:

 goa gen clients/design

Wenn Sie Git verwenden, werden Sie an dieser Stelle das Vorhandensein neuer Dateien bemerken, während andere als aktualisiert angezeigt werden. Das liegt daran, dass Goa den Boilerplate-Code ohne unser Eingreifen nahtlos entsprechend aktualisiert hat.

Jetzt müssen wir den dienstseitigen Code implementieren. In einer realen Anwendung würden Sie die Anwendung manuell aktualisieren, nachdem Sie Ihre Quelle aktualisiert haben, um alle Designänderungen widerzuspiegeln. Dies ist die Vorgehensweise, die Goa empfiehlt, aber der Kürze halber werde ich die Beispielanwendung löschen und neu generieren, um schneller dorthin zu gelangen. Führen Sie die folgenden Befehle aus, um die Beispielanwendung zu löschen und neu zu generieren:

 rm -rf cmd client.go goa example clients/design

Damit sollte Ihr Code wie folgt aussehen:

API-Entwicklung in Go: regenerieren

In unserer Beispielanwendung sehen wir eine neue Datei: signin.go , die die Anmeldedienstlogik enthält. Wir können jedoch sehen, dass client.go auch mit einer JWTAuth-Funktion zur Validierung von Token aktualisiert wurde. Dies stimmt mit dem überein, was wir im Design geschrieben haben, sodass jeder Aufruf einer beliebigen Methode im Client zur Tokenvalidierung abgefangen und nur weitergeleitet wird, wenn er durch ein gültiges Token und einen korrekten Bereich autorisiert ist.

Daher werden wir die Methoden in unserem Anmeldedienst innerhalb signin.go , um die Logik zum Generieren der Token hinzuzufügen, die die API für authentifizierte Benutzer erstellt. Kopieren Sie den folgenden Kontext und fügen Sie ihn in signin.go :

 package clients import ( signin "clients/gen/signin" "context" "log" "time" jwt "github.com/dgrijalva/jwt-go" "goa.design/goa/v3/security" ) // signin service example implementation. // The example methods log the requests and return zero values. type signinsrvc struct { logger *log.Logger } // NewSignin returns the signin service implementation. func NewSignin(logger *log.Logger) signin.Service { return &signinsrvc{logger} } // BasicAuth implements the authorization logic for service "signin" for the // "basic" security scheme. func (s *signinsrvc) BasicAuth(ctx context.Context, user, pass string, scheme *security.BasicScheme) (context.Context, error) { if user != "gopher" && pass != "academy" { return ctx, signin. Unauthorized("invalid username and password combination") } return ctx, nil } // Creates a valid JWT func (s *signinsrvc) Authenticate(ctx context.Context, p *signin.AuthenticatePayload) (res *signin.Creds, err error) { // create JWT token token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ "nbf": time.Date(2015, 10, 10, 12, 0, 0, 0, time.UTC).Unix(), "iat": time.Now().Unix(), "exp": time.Now().Add(time.Duration(9) * time.Minute).Unix(), "scopes": []string{"api:read", "api:write"}, }) s.logger.Printf("user '%s' logged in", p.Username) // note that if "SignedString" returns an error then it is returned as // an internal error to the client t, err := token.SignedString(Key) if err != nil { return nil, err } res = &signin.Creds{ JWT: t, } return }

Da wir unserem benutzerdefinierten Typ weitere Felder hinzugefügt haben, müssen wir schließlich die Add-Methode im Client-Service in client.go , um solche Änderungen widerzuspiegeln. Kopieren Sie Folgendes und fügen Sie es ein, um client.go zu aktualisieren:

 package clients import ( client "clients/gen/client" "context" "log" jwt "github.com/dgrijalva/jwt-go" "goa.design/goa/v3/security" ) var ( // Key is the key used in JWT authentication Key = []byte("secret") ) // client service example implementation. // The example methods log the requests and return zero values. type clientsrvc struct { logger *log.Logger } // NewClient returns the client service implementation. func NewClient(logger *log.Logger) client.Service { return &clientsrvc{logger} } // JWTAuth implements the authorization logic for service "client" for the // "jwt" security scheme. func (s *clientsrvc) JWTAuth(ctx context.Context, token string, scheme *security.JWTScheme) (context.Context, error) { claims := make(jwt.MapClaims) // authorize request // 1. parse JWT token, token key is hardcoded to "secret" in this example _, err := jwt.ParseWithClaims(token, claims, func(_ *jwt.Token) (interface{}, error) { return Key, nil }) if err != nil { s.logger.Print("Unable to obtain claim from token, it's invalid") return ctx, client.Unauthorized("invalid token") } s.logger.Print("claims retrieved, validating against scope") s.logger.Print(claims) // 2. validate provided "scopes" claim if claims["scopes"] == nil { s.logger.Print("Unable to get scope since the scope is empty") return ctx, client.InvalidScopes("invalid scopes in token") } scopes, ok := claims["scopes"].([]interface{}) if !ok { s.logger.Print("An error occurred when retrieving the scopes") s.logger.Print(ok) return ctx, client.InvalidScopes("invalid scopes in token") } scopesInToken := make([]string, len(scopes)) for _, scp := range scopes { scopesInToken = append(scopesInToken, scp.(string)) } if err := scheme.Validate(scopesInToken); err != nil { s.logger.Print("Unable to parse token, check error below") return ctx, client.InvalidScopes(err.Error()) } return ctx, nil } // Add implements add. func (s *clientsrvc) Add(ctx context.Context, p *client.AddPayload) (err error) { s.logger.Print("client.add started") newClient := client.ClientManagement{ ClientID: p.ClientID, ClientName: p.ClientName, ContactName: p.ContactName, ContactEmail: p.ContactEmail, ContactMobile: p.ContactMobile, } err = CreateClient(&newClient) if err != nil { s.logger.Print("An error occurred...") s.logger.Print(err) return } s.logger.Print("client.add completed") return } // Get implements get. func (s *clientsrvc) Get(ctx context.Context, p *client.GetPayload) (res *client.ClientManagement, err error) { s.logger.Print("client.get started") result, err := GetClient(p.ClientID) if err != nil { s.logger.Print("An error occurred...") s.logger.Print(err) return } s.logger.Print("client.get completed") return &result, err } // Show implements show. func (s *clientsrvc) Show(ctx context.Context, p *client.ShowPayload) (res client.ClientManagementCollection, err error) { s.logger.Print("client.show started") res, err = ListClients() if err != nil { s.logger.Print("An error occurred...") s.logger.Print(err) return } s.logger.Print("client.show completed") return }

Und das ist es! Lassen Sie uns die Anwendung neu kompilieren und erneut testen. Führen Sie die folgenden Befehle aus, um die alten Binärdateien zu entfernen und neue zu kompilieren:

 rm -f clients clients-cli go build ./cmd/clients go build ./cmd/clients-cli

Führen ./clients erneut aus und lassen Sie es laufen. Sie sollten sehen, dass es erfolgreich läuft, aber dieses Mal mit den implementierten neuen Methoden:

 $ ./clients [clients] 00:00:01 HTTP "Add" mounted on POST /api/v1/client/{ClientID} [clients] 00:00:01 HTTP "Get" mounted on GET /api/v1/client/{ClientID} [clients] 00:00:01 HTTP "Show" mounted on GET /api/v1/client [clients] 00:00:01 HTTP "CORS" mounted on OPTIONS /api/v1/client/{ClientID} [clients] 00:00:01 HTTP "CORS" mounted on OPTIONS /api/v1/client [clients] 00:00:01 HTTP "CORS" mounted on OPTIONS /openapi.json [clients] 00:00:01 HTTP "./gen/http/openapi.json" mounted on GET /openapi.json [clients] 00:00:01 HTTP "Authenticate" mounted on POST /signin/authenticate [clients] 00:00:01 HTTP "CORS" mounted on OPTIONS /signin/authenticate [clients] 00:00:01 HTTP server listening on "localhost:8080"

Lassen Sie uns zum Testen alle API-Methoden mit der CLI ausführen – beachten Sie, dass wir die fest codierten Anmeldeinformationen verwenden:

 $ ./clients-cli signin authenticate \ --username "gopher" --password "academy" { "JWT": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.\ eyJleHAiOjE1NzcyMTQxMjEsImlhdCI6MTU3NzIxMzU4 \ MSwibmJmIjoxNDQ0NDc4NDAwLCJzY29wZXMiOlsiY \ XBpOnJlYWQiLCJhcGk6d3JpdGUiXX0.\ tva_E3xbzur_W56pjzIll_pdFmnwmF083TKemSHQkSw" } $ ./clients-cli client add --body \ '{"ClientName": "Cool Company", \ "ContactName": "Jane Masters", \ "ContactEmail": "[email protected]", \ "ContactMobile": 13426547654 }' \ --client-id "1" --token "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.\ eyJleHAiOjE1NzcyMTQxMjEsImlhdCI6MTU3NzIxMzU4MSwibmJmI\ joxNDQ0NDc4NDAwLCJzY29wZXMiOlsiYXBpOnJlYWQiLCJhcGk6d3JpdGUiXX0.\ tva_E3xbzur_W56pjzIll_pdFmnwmF083TKemSHQkSw" $ ./clients-cli client get --client-id "1" \ --token "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.\ eyJleHAiOjE1NzcyMTQxMjEsImlhdCI6MTU3NzIxMzU4MSwibmJmI\ joxNDQ0NDc4NDAwLCJzY29wZXMiOlsiYXBpOnJlYWQiLCJhcGk6d3JpdGUiXX0.\ tva_E3xbzur_W56pjzIll_pdFmnwmF083TKemSHQkSw" { "ClientID": "1", "ClientName": "Cool Company", "ContactName": "Jane Masters", "ContactEmail": "[email protected]", "ContactMobile": 13426547654 } $ ./clients-cli client show \ --token "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.\ eyJleHAiOjE1NzcyMTQxMjEsImlhdCI6MTU3NzIxMzU4MSwibmJmI\ joxNDQ0NDc4NDAwLCJzY29wZXMiOlsiYXBpOnJlYWQiLCJhcGk6d3JpdGUiXX0.\ tva_E3xbzur_W56pjzIll_pdFmnwmF083TKemSHQkSw" [ { "ClientID": "1", "ClientName": "Cool Company", "ContactName": "Jane Masters", "ContactEmail": "[email protected]", "ContactMobile": 13426547654 } ]

Und los geht's! Wir haben eine minimalistische Anwendung mit richtiger Authentifizierung, Bereichsautorisierung und Raum für evolutionäres Wachstum. Danach könnten Sie Ihre eigene Authentifizierungsstrategie mit Cloud-Diensten oder einem anderen Identitätsanbieter Ihrer Wahl entwickeln. Sie können auch Plugins für Ihre bevorzugte Datenbank oder Ihr bevorzugtes Messaging-System erstellen oder sogar problemlos in andere APIs integrieren.

Sehen Sie sich das GitHub-Projekt von Goa an, um weitere Plugins, Beispiele (die spezifische Fähigkeiten des Frameworks zeigen) und andere nützliche Ressourcen zu finden.

Das war `s für heute. Ich hoffe, es hat Ihnen Spaß gemacht, mit Goa zu spielen und diesen Artikel zu lesen. Wenn Sie Feedback zu den Inhalten haben, wenden Sie sich bitte an GitHub, Twitter oder LinkedIn.

Außerdem hängen wir im Kanal #goa auf Gophers Slack ab, also kommt vorbei und sagt Hallo!

Weitere Informationen zu Golang finden Sie unter Go Programming Language: An Introductory Golang Tutorial und Well-structured Logic: A Golang OOP Tutorial.