Fully-featured Golang mit Gin Web Framework

15.09.2021 Ricky Elfner
Tech Cloud golang microservices distributed-systems framework handson

Grundlegendes

In dem heutigen TechUp beschäftigen wir uns ein weiteres Mal mit einem Thema rund um Go. Wir hatten bereits mehrere TechUps über Golang geschrieben und verweisen hier gerne nochmals auf die bisherigen Beiträge dazu.

Wir bei b-nova haben bereits viele Erfahrung mit Golang sammeln können und wollen heute Ihnen nochmal einen neuen Aspekt von Go vorstellen. Laut der diesjährigen JetBrain's The State of Developer Ecosystem 2021 ist das weitverbreiteste Framework in Golang mit 40% Gin (vielleicht erwähnenswert hier ist sicherlich der Umstand, dass 43% von den Teilnehmenr sagten dass sie kein spezifisches Framework im Einsatz haben). Aus diesem Grund schauen wir uns Gin im heutigen TechUp Gin etwas genauer an. Dieses bietet eine API ähnlich wie Martini, jedoch laut eigenen Angaben über etwa 40-mal so schnell. Diese Performance zeigen sie auch innerhalb ihres Git-Repository mit verschiedenen Benchmark Tests und Vergleichen.

Dabei werden von Haus aus Funktionen für das Routing mitgebracht, inklusive Patterns und gruppierte URLs. Auch im Bereich des Renderns bietet Gin direkt Möglichkeiten, die Response als HTML, XML, JSON und auch weitere Formate zurückzugeben. Des Weiteren gibt es Support von Middleware. Hierzu gehört beispielsweise ein Authentifizierungsverfahren. Durch diese Standard-Funktionen, die Ihnen bereitgestellt werden, sparen sich einen Haufen Boilerplate Code und können sich somit schnell und leicht eine Web-Applikation oder einen Microservice bauen.

Key-Features:

  • schnelle Performance

  • Middleware Support

  • Crash-free →

  • Fehlermanagement

  • JSON Validierung

  • URL Management

  • Rendering

  • Erweiterbar

Der Praxis-Teil

Kommen wir nun wie bei uns üblich zu einem praktischen Teil.
Um das Gin-Framework nutzen zu können, müssen Sie dieses zunächst einmal installieren.

go get -u github.com/gin-gonic/gin

Das Layout

Da wir bei dieser Applikation ein UI nutzen möchten, um das Wechseln der verschiedenen Seiten besser darzustellen, zu können und auch diese Funktionalität zu zeigen, müssen Sie zuerst die notwendigen HTML-Files erstellen. Dafür haben wir uns entschieden, Seiten in vier Teile aufzuteilen. Dadurch ist es Ihnen möglich, beispielsweise den Header mehrfach zu nutzen oder auch auf gewissen Seiten auszutauschen.

Zuerst erstellen Sie den Header der HTML-Seite. Auf der letzten Zeile können Sie erkennen, das der Header zusätzlich ein Menü enthält.

<!doctype html>
<html>

<head>
  <title>{{ .title }}</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta charset="UTF-8">

  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anonymous">
</head>

<body class="container">
{{ template "menu.html" . }}

Das Menü wird in unserer Applikation wie folgt aufgebaut:

<nav class="navbar navbar-default">
  <div class="container">
    <div class="navbar-header">
      <a class="navbar-brand" href="/">Home</a>
    </div>
  </div>
</nav>

Der dritte Teil unserer HTML-Seite wird jener Teil, der abhängig von der URL ausgetauscht wird. Denn es soll einmal eine Startseite geben und eine zweite Seite, welche genauere Information über den Blog anzeigt. Zu diesen beiden HTML Templates werden wir jedoch erst einmal später kommen.

Nun fehlt nur noch der Footer, der das Konstrukt vervollständigt und die notwendigen und noch offenen Tags schliesst.

  </body>
</html>

Der Router

Als nächstes werden wir den Gin-Router erstellen, genauso wie die verschiedenen Routes. Dafür erstellen Sie zu einmal eine Variable router von der Gin Engine Instance, welche den Muxer, die Middleware und die Konfigurationseinstellungen enthält. Diese kann entweder mit Default() oder New() erstellt werden. Da Sie bereits die verschiedenen HTML-Templates erstellt haben, können Sie die Gin-Funktion LoadHTMLGlob verwenden. Diese lädt anhand des Glob-Patters, welches als Parameter übergeben wird, die gewünschten Files und verknüpft diese mit einem HTML-Renderer. Dadurch werden die Templates direkt beim Start geladen und müssen nicht noch einmal zu einem späteren Zeitpunkt nachgeladen werden. Im Anschluss wird die Methode InitRoutes() aufgerufen, welche Sie als Nächstes erstellen werden. Um die Applikation nun bereitstellen zu können, muss die Methode Run noch ausgeführt werden.

var router *gin.Engine

func main() {
	router = gin.Default()
	router.LoadHTMLGlob("templates/*")
	rest.InitRoutes(router)
	router.Run()
}

Für den Anfang soll es die beiden Routes für die Startseite und für die Detailseite geben. Dabei bestimmt der erste Parameter den Relativen-Pfad und der zweite eine Handler-Funktion.

func InitRoutes(router *gin.Engine){
	router.GET("/", initPage)
	router.GET("/blog/content/:blog_id", getBlog)
}

Zuerst einmal wollen wir die Startseite erstellen. Dafür gibt es die Funktion initPage(). Innerhalb dieser Funktionen sind zwei weitere Funktionen notwendig. Die erste Funktion, die wir anschliessend erstellen müssen, ist eine Funktion um alle Blogs zu bekommen die gespeichert sind. Und die zweite Funktion ist dafür zuständig, dass das HTML gerendert wird. Dafür wird einmal der Gin-Context übergeben, eine Map (gin.H{}) mit dem entsprechenden Inhalt und zum Schluss noch der Name des Templates.

func initPage(c *gin.Context) {
   blogs := staticPage.GetAllBlogs()

   RenderHTML(c, gin.H{
      "title": "Home Page",
      "payload": blogs,
   }, "index.html")
}

Das Attribut gin.H{} bestimmt in unserem Beispiel, welche Daten dem HTML-Template übergeben werden sollen. Dies ist einmal der Titel und einmal der Payload, also die Blog-Artikel selbst. Innerhalb des header.html-Files konnten Sie bereits sehen, wie die Daten aus dieser Map verwendet werden:

<title>{{ .title }}</title>

Das Rendering - Teil 1

Die RenderHTML-Funktion wird wie folgt aufgebaut und nutzt die HTML-Funktion, die über das Gin-Framework zur Verfügung gestellt wird:

func RenderHTML(c *gin.Context, data gin.H, templateName string){
	c.HTML(http.StatusOK, templateName, data)
}

Die Daten

Um nun auch Daten zu haben, die Sie anzeigen können, müssen Sie nun ein File mit dem Namen staticpage.go anlegen. Innerhalb davon legen Sie sich zuerst einen strukturierten Datentyp mit dem Namen StaticPage an:

type StaticPage struct {
	Title        string `json:"title"`
	Permalink    string `json:"permalink"`
	Author       string `json:"author"`
	Categories   string `json:"categories"`
}

Zu Testzwecken legen Sie nun einfachheitshalber ein Array mit dem Typ StaticPage an, welches unsere Testdaten widerspiegelt.

var staticPageList = []StaticPage {
	StaticPage{Title: "title1",Permalink: "link1",Author: "author1",Categories: "categories1"},
	StaticPage{Title: "title2",Permalink: "link2",Author: "author2",Categories: "categories2"},
}

Anschliessend müssen Sie die Funktion GetAllBlogs()implementieren, welche Sie innerhalb der initPage() Funktion aufrufen. Diese gibt das zuvor erstellte StaticPage-Array mit den Testdaten zurück.

func GetAllBlogs() []StaticPage {
   return staticPageList
}

Die Startseite

Für die Startseite muss nun das entsprechende HTML-Markup noch erstellt werden. Dafür legen Sie die Datei index.html an. Dabei wird zuerst der Header der Seite integriert. Nach der Überschrift wird ein Abschnitt geöffnet, in dem Sie zugriff, auf alle Daten aus dem Payload haben, welchen Sie übergeben haben. Und zum Schluss wird noch der Footer mit eingebunden, damit auch alle Tags korrekt geschlossen sind.

<!--index.html-->
{{ template "header.html" .}}

<h1>Meine Blogs!</h1>

{{range .payload }}
<a href="/blog/content/{{.Title}}">
    <h2>{{.Title}}</h2>
</a>
{{end}}

{{ template "footer.html" .}}

Danach können Sie Ihre Applikation das erste mal builden und Starten lassen:

go build -o app
./app

Sobald Sie in ihrem Browser die Adresse http://localhost:8080/ aufrufen, sollten Sie folgendes sehen:

Die Detailseite

Da es nun auch möglich sein soll, eine Seite zu haben, die genauere Informationen über den Blog anzeigt, muss die Applikation noch ein wenig erweitert werden. Dafür müssen Sie nun die Funktion getBlog, welche Sie bereits in der Funktion InitRoutes definiert haben, implementieren. Dafür lesen wir zuerst einmal den Parameter blog_id aus dem Gin-Context aus und speichern diesen als Variable ab. Nun sollte überprüft werden, ob diese Variable leer ist oder nicht. Falls ja, soll ein 404-Error ausgegeben werden.

func getBlog(c *gin.Context) {
   blogID := c.Param("blog_id")
   if blogID != "" {
      if blog, err := staticPage.GetBlogByID(string(blogID)); err == nil {
         RenderHTML(c, gin.H{
            "title":   blog.Title,
            "payload": blog,
         }, "blog.html")

      } else {
         c.AbortWithError(http.StatusNotFound, err)
      }
   } else {
      c.AbortWithStatus(http.StatusNotFound)
   }
}

Da es im Moment noch kein HTML-Template für diese Seite gibt, müssen Sie dieses nun erstellen. Für test zwecke reicht hier ein sehr einfach Layout. Zuerst einmal wird der Header mit eingebunden. Im Anschluss werden die verfügbaren Attribute des Blogs ausgelesen und innerhalb von HTML-Tags verwendet. Und auch hier muss zum Schluss der Footer wieder mit integriert werden.

{{ template "header.html" .}}

<h1>{{.payload.Title}}</h1>

<p>{{.payload.Permalink}}</p>
<p>{{.payload.Author}}</p>
<p>{{.payload.Categories}}</p>


{{ template "footer.html" .}}

Wenn Sie nun alles gespeichert haben, können Sie die Applikation wieder builden und anschliessend starten lassen.

go build -o app
./app

Sobald Sie nun auf der Startseite beispielsweise auf den ersten Blog klicken, werden Sie zu der entsprechenden Detailseite weitergeleitet.

Das Rendering - Teil 2

Um die Applikation noch universell Einsetzbarer zu machen, kann man die RenderHTML in eine generische Variante umwandeln. Dadurch können Sie anhand des Request-Headers entscheiden, in welchem Format die Response zurückgegeben werden soll. Anstatt nur die HTML-Funktion von Gin zu nutzen, wird je nach Header die JSON, XML oder HTML Funktion verwendet, um die Response zu erstellen.

func RenderByType(c *gin.Context, data gin.H, templateName string) {
   switch c.Request.Header.Get("Accept") {
   case "application/json":
     c.JSON(http.StatusOK, data["payload"])
   case "application/xml":
      c.XML(http.StatusOK, data["payload"])
   default:
      c.HTML(http.StatusOK, templateName, data)
   }
}

Nun können Sie auch jeden RenderHTML-Methode durch diese RenderByType-Methode ersetzen.

Hierfür sehen Sie nun zwei Beispiele mit der jeweiligen Response. Bei dem ersten Beispiel handelt es sich um die XML-Variante und beim zweiten um die JSON-Variante.

curl -X GET -H "Accept: application/xml" http://localhost:8080/blog/content/title1
> <StaticPage><Title>title1</Title><Permalink>link1</Permalink><Author>author1</Author><Categories>categories1</Categories></StaticPage>
curl -X GET -H "Accept: application/json" http://localhost:8080/blog/content/title1
{"title":"title1","permalink":"link1","author":"author1","categories":"categories1"}

Testing

Vor Kurzem haben wir bereits einen Blogartikel zum Thema Testing geschrieben. Auch bei einer solchen Applikation macht es Sinn, einige Tests zu schreiben. Wie Sie solch einen Test schreiben könnten, zeigen wir Ihnen natürlich auch. Dabei geht es zunächst einmal um die eigentlichen Funktionen und wie Sie Gin innerhalb eines Testes verwenden könnten.

Für unsere Router-Funktionalität haben wir das Package restverwendet. Innerhalb davon finden Sie dessen Implementierung rest.go. Nun müssen zusätzlich ein weiteres go-File mit dem Namen rest_test.go erstellen.

├── rest
│   ├── rest.go
│   └── rest.go_test.go

Innerhalb dieses Test-Files können Sie sich zunächst eine Funktion schreiben, die Ihnen als Rückgabewert einen Router liefert, bei dem bereits Ihre Templates geladen wurden. Sollten Sie nämlich zu einem späteren Zeitpunkt mehrere verschiedene Seiten testen möchten, werden Sie darüber froh sein.

func getRouterWithTemplates() *gin.Engine {
   router := gin.Default()
   router.LoadHTMLGlob("../../cmd/hw/templates/*")
   return router
}

Als erstes Beispiel bietet es sich zunächst einmal an, die Startseite Ihrer Applikation zu testen. Dafür initialisieren Sie Ihren Router über die zuvor erstellte Funktion. Anschliessend definieren Sie die Router Ihrer Startseite, wie Sie dies bereits in router.go gemacht haben. Um nun testen zu können müssen Sie sich ein Get-Request auf die Startseite mit dem Pfad / erstellen. Ebenso müssen Sie noch das HTML bestimmen welches überprüft werden soll, ob es innerhalb der Response vorhanden ist.

func TestInitPage(t *testing.T){
	router := getRouterWithTemplates()
	router.GET("/", initPage)
	request, _ := http.NewRequest("GET", "/", nil)

	htmlToTest := "<title>Home Page</title>"
	testHTTPResponseByHTML(t, router, request, htmlToTest)
}

Als nächstens müssen Sie die testHTTPResponseByHTML-Funktion erstellen. Dabei müssen Sie einen neuen ResponseRecorder initialisieren, um später den Inhalt, der von der Seite zurückkommt, überprüfen zu können. Diesen müssen Sie zusammen mit dem Request dem Router übergeben, damit der Request ausgeführt wird. Zum Schluss gibt es dann die testHTML-Funktion mit welcher überprüft werden soll, ob der Body dieses HTML enthält.

func testHTTPResponseByHTML(t *testing.T, router *gin.Engine, request *http.Request, html string) {
	w := httptest.NewRecorder()
	router.ServeHTTP(w, request)
	if !testHTML(w, html) {
		t.Fail()
	}
}

Innerhalb dieser Funktion wird zunächst überprüft, ob der Code innerhalb des Response-Bodys einem http.StatusOk, also einer HTML Code 200, entspricht. Anschliessend wir der Body ausgelesen und der Variable body übergeben. Anhand davon wird im nächsten Schritt überprüft, ob das gewünschte HTML-Layout darin vorhanden ist. Anhand dieser beiden Boolean-Werte wird wiederum überprüft, ob der Aufruf erfolgreich war.

 func testHTML(w *httptest.ResponseRecorder, html string) bool {
	statusOK := w.Code == http.StatusOK
	body, err := ioutil.ReadAll(w.Body)
	pageOK := err == nil && strings.Index(string(body), html) > 0
	return statusOK && pageOK
}

Wollen Sie nun eine weitere Seite testen und beispielsweise nach einem p-Tag mit einem bestimmten Inhalt ist auch dies im Handumdrehen erledigt.

func TestDetailPage(t *testing.T){
   router := getRouterWithTemplates()
   router.GET("/blog/content/:blog_id", getBlog)

   request, _ := http.NewRequest("GET", "/blog/content/title1", nil)
   htmlToTest := "<p>link1</p>"
   testHTTPResponseByHTML(t, router, request, htmlToTest)
}

Nun kennen Sie das meist genutzte Web-Framework Gin, wenn es um die Entwicklung mit go geht! Stay tuned!

Ricky Elfner – Denker, Überlebenskünstler, Gadget-Sammler. Dabei ist er immer auf der Suche nach neuen Innovationen, sowie Tech News, um immer über aktuelle Themen schreiben zu können.