Go aka golang wurde hauptsächlich entwickelt, um Concurrency optimal zu unterstüzten. Daher bietet sich an, eine Web API mit Go zu entwickeln. Dieser Beitrag zeigt, was dafür notwendig ist und dass dafür nicht sonderlich viel Aufwand eingesetzt werden muss.

Folgendes möchte der Beitrag zeigen:

  1. Web API mit versionierter API-Entstelle, JSON wird als Datenformat verwendet; Die Web API kann eine Liste von Kontakten zurückgeben, sowie neue Kontakte erstellen
  2. Interaktion mit einer MySQL-Datenbank

Notwendiges Vorwissen

  • Grundlagen Go / Umgebung aufsetzen und ausführen können, Verwendung von Paketen und Modulen
  • Grundlagen Web APIs
  • JSON
  • MySQL Schema und Tabelle anlegen können

Tabelle erzeugen

Für dieses Beispiel wird eine Tabelle benötigt, dies ist mit folgendem Script anzulegen:

CREATE TABLE `contact` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `firstName` varchar(50) DEFAULT NULL,
  `lastName` varchar(50) DEFAULT NULL,
  `email` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `id_UNIQUE` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

Im Beispiel wird von einem existierenden Schema contacts ausgegangen.

Verwendete Packages

Mit dem Paket net/http, welches mit Go ausgeliefert wird, können Routen grundsätzlich implementiert werden. Wer es etwas komfortabler haben möchte, verwendet github.com/gorilla/mux. Dadurch kann beispielsweise einfacher mit HTTP-Verbs gearbeitet werden.

Für den Zugriff auf MySQL wird github.com/go-sql-driver/mysqleingesetzt. Dies setzt auf das bereits enthaltene Paket database/sql auf.

Zugriff auf MySQL

Mittels db.go wird die Initialisierung der Datenbank zur Verfügung gestellt:

package models

import (
    "database/sql"
    "log"
    _ "github.com/go-sql-driver/mysql"
)

var db *sql.DB

func InitializeDatabase(connectionString string) {
    log.Println("Initializing database ...")
    var err error
    db, err = sql.Open("mysql", connectionString)
    if err != nil {
        log.Panic(err)
    }

    if err = db.Ping(); err != nil {
        log.Panic(err)
    }
}

In contacts.go wird sowohl der Typ Contact zur Verfügung gestellt, als auch die Funktionen für den Umgang mit der Datenbank:

package models

import "log"

type Contact struct {
    Id int `json:"id"`
    FirstName string `json:"firstName"`
    LastName string `json:"lastName"`
    Email string `json:"email"`
}

func AllContacts() ([]*Contact, error) {
    rows, err := db.Query("SELECT id, firstName, lastName, email FROM contact")
    if err != nil {
        return nil, err
    }

    log.Println("Successfully loaded contacts from database")
    contacts := make([]*Contact, 0)
    for rows.Next() {
        contact := new(Contact)
        err := rows.Scan(&contact.Id, &contact.FirstName, &contact.LastName, &contact.Email)
        if err != nil {
            return nil, err
        }
        contacts = append(contacts, contact)
    }
    if err = rows.Err(); err != nil {
        return nil, err
    }

    defer rows.Close()

    return contacts, nil
}

func InsertContact(contact *Contact) (error){
    result, err := db.Exec("INSERT INTO contact (firstName, lastName, email) VALUES (?,?,?)", contact.FirstName, contact.LastName, contact.Email)
    if err != nil {
        return err
    }

    id, _ := result.LastInsertId()

    contact.Id = int(id)

    return nil
}

Verarbeitung der API-Calls

Da nun der Datenbank-Zugriff geregelt ist, wenden wir uns der Kommunikation über HTTP zu. Es muss entsprechende Funktionen für die Rückgabe der bereits vorhandenen Kontakte, sowie zur Anlage geben.

package controller

import (
    "net/http"
    "sampleWebApi/models"
    "encoding/json"
    "log"
)

func AllContacts(w http.ResponseWriter, r *http.Request) {
    log.Println("Endpoint AllContacts")
    contacts, err := models.AllContacts()
    if err != nil {
        log.Fatal(err)
        http.Error(w, http.StatusText(500), 500)
        return
    }
    json.NewEncoder(w).Encode(contacts)
    log.Println("Sent all contacts")
}

func InsertContact(w http.ResponseWriter, r *http.Request) {
    log.Println("Endpoint InsertContact")
    var c models.Contact
    if r.Body == nil {
        http.Error(w, "Please send any body", http.StatusBadRequest)
        return
    }
    err := json.NewDecoder(r.Body).Decode(&c)
    if err != nil {
        log.Fatal(err)
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    err = models.InsertContact(&c)
    if err != nil {
        log.Fatal(err)
        http.Error(w, err.Error(), http.StatusInternalServerError)
    }
    w.WriteHeader(http.StatusOK)
    log.Println("Inserted new contact")
}

Routing und Verbinden der Teile

In der main.go werden nun die Einzelteile miteinander verbunden. So erfolgt im ersten Schritt die Initialisierung der Datenbank, danach wird das Routing (und somit die möglichen API-Endpunkte) konfiguriert:

package main

import (
    "net/http"
    "log"
    "github.com/gorilla/mux" // specify used verbs
    "sampleWebApi/models"
    "sampleWebApi/controller"
)

func defaultPage(w http.ResponseWriter, r *http.Request) {
    log.Println(w, "Endpoint hit")
}

func handleRequests() {
    log.Println("Handling requests ...")
    router := mux.NewRouter().StrictSlash(true)

    router.HandleFunc("/", defaultPage)
    router.HandleFunc("/api/1/contacts", controller.AllContacts).Methods("GET")
    router.HandleFunc("/api/1/contacts", controller.InsertContact).Methods("POST")
    log.Fatal(http.ListenAndServe(":8081", router))
}

func main() {
    models.InitializeDatabase("user:pass@tcp(127.0.0.1:3306)/contacts")
    handleRequests()
}

Download

Dieses Beispiel steht auf GitHub zur Verfügung und kann frei verwendet werden. Entwickelt und getestet wurde es mit Go 1.10.1. Docker-Support inklusive.

Happy coding!

Über den Autor

Norbert Eder

Ich bin ein leidenschaftlicher Softwareentwickler und Fotograf. Mein Wissen und meine Gedanken teile ich nicht nur hier im Blog, sondern auch in Fachartikeln und Büchern.