JSON RESTful Web-Service mit Golang implementieren

JSON RESTful Web-Service mit Golang implementieren

Mit seiner Dissertation hat Roy Thomas Fielding im Jahr 2000 das Programmierparadigma Representational State Transfer (kurz: Rest) für verteilte Systeme, vor allem für Web-Services, beschrieben.

In der Folge entstanden sehr viele RESTful Web APIs, die leider nicht alle Vorteile bzw. Ideen hinter dem beschriebenen Architekturstil umsetzten und somit nicht das volle Potential ausnutzten.

Leonard Richardson entwickelte daraufhin ein Reifegradmodell mit dem sich Rest-Services auf ihre “Rest-kompatibilität” prüfen lassen. Bekannt wurde das Modell unter dem Namen “Richardson Maturity Model / RMM”.

Dieses Modell teilt Services in 4 verschiedene Stufen ein:

  • Level 0 - The Swamp of POX
  • Level 1 - Ressources
  • Level 2 - HTTP Verbs
  • Level 3 - Hypermedia Controls

Daran soll sich auch die in diesem Artikel entstehende Implementierung orientieren. Es wird ein Go basierter Rest-Service implementiert, der zumindest Level 2 des RMM umsetzt. Auf die Verwendung von Hypermedia wird verzichtet. In diesem Beispiel soll keine “externe” Bibliothek oder Framework eingesetzt werden.

Rest-Service Beschreibung

Über einen Customer-Service können Kunden angelegt oder gelistet werden. Jeder Kunde stellt eine Ressource dar und erhält dementsprechend eine eindeutige URI über die er abgerufen werden kann.

Beispiel einer eindeutigen Kunden URI:

http://localhost:8080/customer/{id}

Ein Kunde wird folgende Properties besitzen:

  • id
  • firstname
  • lastname

Im Beispiel wird auch auf die Umsetzung einer Content Negotiation verzichtet. Alle Daten sollen fest mittels JSON repräsentiert werden. Unabhängig welche HTTP-Header mitgeschickt werden.

Beispielkunde:

{
    "id": 1,
    "firstname": "Kristian",
    "lastname": "Köhler"
}

Eine Kommunikation mit dem Service soll so aussehen:

Kunde mit Id 1 anfragen:

GET http://localhost:8080/customer/1

Es wurde kein Kunde mit Id 1 gefunden (das Repository ist am Anfang noch leer):

HTTP/1.1 404 Not Found
Content-Length: 9
Content-Type: text/plain; charset=utf-8
Connection: close

Not found

Neuer Kunde anlegen:

PUT http://localhost:8080/customer/1

{
    "id": 1,
    "firstname": "Kristian",
    "lastname": "Köhler"
}

Kunde wurde erfolgreich angelegt:

HTTP/1.1 204 No Content
Date: <val>
Connection: close

Nochmalige Anfrage

GET http://localhost:8080/customer/1

Daten werden erfolgreich ermittelt:

HTTP/1.1 200 OK
Content-Type: application/json
Date: <val>
Content-Length: 53
Connection: close

{
  "id": 1,
  "firstname": "Kristian",
  "lastname": "Köhler"
}

Implementierung

Die Implementierung des Service wird in einen Controller und ein Repository aufgeteilt. Der Controller soll die Http-Anfragen abarbeiten, das Repository ist für die Datenablage verantworlich. Der eigentliche Server wird über das go http Package implementiert.

JSON basierter Rest Service

Rest Service Implementierung

Das komplette Beispiel kann auch in GitHub gefunden werden.

Go JSON Ressource

Im ersten Schritt wird ein struct für die Kunden-Repräsentation benötigt. Der Kunde enthält die drei oben genannten Attribute Id, Firstname und Lastname. Diese werden über Field Tags mit Metainformationen für das JSON Marshalling versehen, so dass die JSON-Felder nur Kleinbuchstaben enthalten. Zusätzlich wird über das omitempty definiert, dass das Feld, wenn es leer ist, nicht übertragen werden soll. In der Doku zum JSON Marshaller findet man Dokumentation zu weiteren möglichen JSON Tags, die das Verhalten bei einer JSON Serialisierung steuern.

package model

type Customer struct {
    Id        int    `json:"id,omitempty"`
	Firstname string `json:"firstname,omitempty"`
	Lastname  string `json:"lastname,omitempty"`
}

Repository zur Ablage

Die Ablage der Kundendaten übernimmt im Beispiel ein CustomerRepository, das zwei Methoden beinhaltet. Mit der Methode Get können Daten abgefragt werden, mit Add können neue Daten aufgenommen werden. Beide Methoden werden im dafür vorgesehenen Interface CustomerRepository definiert.

type CustomerRepository interface {
	Get(id int) *model.Customer
	Add(customer *model.Customer)
}

Für die Beispiel-Implementierung wird das ArrayCustomerRepository umgesetzt, das intern ein Array für die Ablage der Kunden nutzt und das Interface CustomerRepository implementiert.

type ArrayCustomerRepository struct {
	customers []model.Customer
}

Go Rest Service Controller

Wie im obigen Diagramm gezeigt wird ein Controller implementiert, der das Handler Interface aus dem net/http Package umsetzt. Somit kann die Controller Implementierung direkt in der http.Handle() Methode aus dem go http Package verwendet werden. Dazu aber später mehr.

Der Controller muss, damit das Handler Interface korrekt umgesetzt wird, die ServeHTTP Methode implementieren. In dieser Methode erfolgt die Auswertung der HTTP Verben (siehe hierzu auch RMM Level 2) und der Aufruf der entsprechenden Business-Logik. Anschließend wird noch der Kunde ins JSON Format gewandelt und ausgegeben.

type CustomerController struct {
	repo repo.CustomerRepository
}

func (controller *CustomerController) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
    ...
    if request.Method == "GET" {
        ...
        customer := controller.repo.Get(customerId)
        ...
        json, err := json.Marshal(customer)
        ...
		header := writer.Header()
		header.Add("content-type", "application/json")
        writer.Write(json)
        ...
    }
}

Im Beispiel wird auf “Go Bordmittel” gesetzt. Die Implementierung des Handler Interfaces geht mit Bibliotheken oder Frameworks wie z. B. http://www.gorillatoolkit.org/ einfacher von der Hand!

Der komplette Controller aus dem Beispiel:

type CustomerController struct {
	repo repo.CustomerRepository
}

func NewController(repo repo.CustomerRepository) CustomerController {
	return CustomerController{repo: repo}
}

func (controller *CustomerController) ServeHTTP(writer http.ResponseWriter, request *http.Request) {

	path := request.URL.Path
	idx := strings.LastIndex(path, "/") + 1
	customerId, err := strconv.Atoi(path[idx:])

    if err != nil {
        writer.WriteHeader(400)
        writer.Write([]byte("Not found"))
        return
    }

	if request.Method == "GET" {

		customer := controller.repo.Get(customerId)

		if customer == nil {
			writer.WriteHeader(404)
			writer.Write([]byte("Not found"))
			return
		}

		json, _ := json.Marshal(customer)
		header := writer.Header()
		header.Add("content-type", "application/json")
		writer.Write(json)

	}

	if request.Method == "PUT" {

		customer := &model.Customer{}
		decoder := json.NewDecoder(request.Body)
		decoder.Decode(customer)

		controller.repo.Add(customer)

		writer.WriteHeader(204)
		writer.Write([]byte("Not found"))

	}

}

Go Server starten

Um den Controller über HTTP zur Verfügung zu stellen kann dieser direkt als Parameter bei Aufruf der http.Handle() Methode verwendet werden. Durch den Aufruf der Methode wird der Handler im sogenannten DefaultServerMux angemeldet und erhält zukünftig die entsprechenden Aufrufe. Beim ersten Parameter der Methode handelt es sich um das Pattern unter dem der Handler angemeldet wird.

ServeMux is an HTTP request multiplexer. It matches the URL of each incoming request against a list of registered patterns and calls the handler for the pattern that most closely matches the URL.

Mit einem http.ListenAndServe(port, nil) wird dann der Server gestartet.

port := ":8080"
...
log.Printf("Listen on port %s", port)
http.Handle("/customer/", &customerController)
log.Fatal(http.ListenAndServe(port, nil))

Der komplette Code:

package main

import (
	"log"
	"net/http"

	"github.com/kkoehler/golang/jsonrest/repo"

	"github.com/kkoehler/golang/jsonrest/controller"
)

func main() {

	port := ":8080"

	log.Printf("Listen on port %s", port)

	repository := repo.ArrayCustomerRepository{}
	repository.Init()

	customerController := controller.NewController(&repository)

	http.Handle("/customer/", &customerController)

	log.Fatal(http.ListenAndServe(port, nil))

}

Das komplette Beispiel kann auch in GitHub gefunden werden.

Middleware zur Absicherung

Nach der Umsetzung der eigentlichen Business-Logik soll der Service noch abgesichert werden. Dieser logische Aspekt bzw. Cross-Cutting Concern wird bei Go im http Package über eine sogenannte Middleware umgesetzt. Hierbei handelt es sich, vereinfacht ausgedrückt, um einen Wrapper-Handler, der einfach bei der Anmeldung des Handlers “zwischengeschoben” wird. Man erreicht so etwas wie eine Aspektorientierte Programmierung.

Innerhalb einer Middleware lassen sich die verschiedensten Funktionalitäten umsetzen. Denkbar und sinnvoll sind z. B.:

  • Logging
  • Filterung
  • Anpassung von Responses oder Requests
  • oder eben auch Security

Die Implementierung einer Middleware kann wie folgt aussehen:

package middleware

import (
	"log"
	"net/http"
)

func SecurityMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ...
        next.ServeHTTP(w, r)
        ...
	})
}

Beim Start des Servers kann nun, anstelle des Ziel Handlers, die SecurityMiddleware verwendet werden. Diese sollte natürlich den eigentlichen Ziel-Handler als Referenz übergeben bekommen.

customerController := controller.NewController(&repository)
http.Handle("/customer/", middleware.SecurityMiddleware(&customerController))

Fazit

Wie man sieht lässt sich mit Go auch mit “Bordmitteln” recht einfach ein performanter Rest-basierter WebService implementieren. Der eingebaute HTTP Server gilt auch schon als “production-ready” und unterstützt direkt HTTP/2. Punkte wie SSL oder Connection Konfigurationen solle man sich allerdings sicher noch vor einer Produktivnahme genauer anschauen.

Das komplette Beispiel kann auch in GitHub gefunden werden.

22.02.2019

 

Kennen Sie schon das Buch zum Thema?

Der praktische Soforteinstieg für Developer und Softwarearchitekten, die direkt mit Go produktiv werden wollen.

  • Von den Sprachgrundlagen bis zur Qualitätssicherung
  • Architekturstil verstehen und direkt anwenden
  • Idiomatic Go, gRPC, Go Cloud Development Kit
  • Cloud-native Anwendungen erstellen
Microservices mit Go Buch

zur Buchseite beim Rheinwerk Verlag Rheinwerk Computing, ISBN 978-3-8362-7559-0 (als PDF, EPUB, MOBI und Papier)

Kontakt

Source Fellows GmbH

Source Fellows GmbH Logo

Lerchenstraße 31

72762 Reutlingen

Telefon: (0049) 07121 6969 802

E-Mail: info@source-fellows.com