pixel
Seite wählen

NETWAYS Blog

Je suis #nginx! | Я/Мы #nginx!

Der Autor mit einem russischen Plakat: “Je suis Nginx”

Stellt Dir vor, Du hast um 2004 herum als Programmierer für ein Unternehmen gearbeitet. Nach und nach hast Du Dir eine Nebentätigkeit aufgebaut, die Du schließlich hauptberuflich als Unternehmer betreibst. Und jetzt kommt aus heiterem Himmel Dein ehemaliger Arbeitgeber und fordert nichts geringeres, als ihm Dein schönes neues Unternehmen zu überlassen.

Ich weiß, das klingt unvorstellbar – zumindest für ein Land wie Deutschland. Aber genau das ist dem Nginx-Gründer Igor Sysoev so passiert.

Ich selbst weiß noch genau wie mein Ausbilder mir den Übernahmevertrag erläutert hat. Beim Punkt, dass ich dem Betrieb automatisch die Rechte am von mir geschriebenen Code übergebe, wurde ich hellhörig. Ich fragte nach, ob dass denn nur für den auf der Arbeit geschriebenen Code gelte und wo das im Vertrag stehe. Darauf antwortete der Ausbilder, dass es nirgendwo explizit stehen müsste, da es sich aus dem Kontext ergebe.

Zahlen, Daten, Fakten

Igor Sysoev war seit Ende 2000 Chef-Admin der russischen Suchmaschine Rambler. Nebenbei entwickelte er 2004 die Webserver-Software Nginx. 2011 gründete er Nginx Inc. als kommerziellen Verwerter der Software.

Rambler ist gegen diese Tätigkeit nie vorgegangen… bis vor Kurzem, als die Strafverfolger vor Nginx’ Tür standen. Diese nahmen Sysoev vorübergehend fest und werfen ihm als ehemaligen Rambler-Mitarbeiter Urheberrechtsverletzung gegenüber dem Arbeitgeber vor. Und als ob das nicht schon genug wäre: Die Beschuldigungen fußen wohl auf einem Gesetz aus 2006 – sprich, zwei Jahre nach der “Tat”.

Und jetzt?

Gerüchten zufolge hat der russische Geheimdienst FSB es auf den Löwenanteil des russischen Internets abgesehen – dieser läuft auf Nginx. Wenn das stimmt, haben die ihre Rechnung ohne Nginx Inc. gemacht. Diese speichert nämlich Ihren Code im Ausland – und hat jetzt zusätzliche Sicherheits-Maßnahmen ergriffen. Außerdem installieren nicht alle Nutzer Nginx from-Source – viele bedienen sich der Paket-Quellen des Betriebssystems. Deren Maintainer werden jetzt noch ein bisschen genauer hinschauen. Die Docker-Images fußen zwar auf einem GitHub-Repository der Organisation NGINX, Inc., aber alle Mitglieder geben (Stand 16.12.19) einen ausländischen Wohnort an.

Ich selbst habe privat nach Kenntnis der Meldung zwei neue Nginx-Instanzen in Betrieb genommen und kann angesichts meiner Erkenntnisse aus dem letzten Absatz allen Sysadmins Entwarnung geben – und ein Lob zugunsten von Nginx Inc. aussprechen.

Fazit

Dieser Fall zeigt, wie wichtig Open Source und freie Software heutzutage sind. Gerade in Zeiten politischer Unsicherheit ist allen Beteiligten Transparenz wichtiger denn je. Gerade deshalb liegt uns Open Source so am Herzen. Das heißt aber nicht, dass wir ITler uns erdreisten sollten, jetzt die Füße hochzulegen. Im Gegenteil:

Der Autor auf Twitter: “Je suis #Nginx!”

Quellen

Alexander Klimov
Alexander Klimov
Senior Developer

Alexander hat 2017 seine Ausbildung zum Developer bei NETWAYS erfolgreich abgeschlossen. Als leidenschaftlicher Programmierer und begeisterter Anhänger der Idee freier Software, hat er sich dabei innerhalb kürzester Zeit in die Herzen seiner Kollegen im Development geschlichen. Wäre nicht ausgerechnet Gandhi sein Vorbild, würde er von dort aus daran arbeiten, seinen geheimen Plan, erst die Abteilung und dann die Weltherrschaft an sich zu reißen, zu realisieren - tut er aber nicht. Stattdessen beschreitet er mit der...

C++Go. Halb C++ – halb Go.

Scherz beiseite, die Go-Entwickler bieten noch keine Möglichkeit, C++-Bibliotheken ohne weiteres anzusprechen. Aber es geht ja auch mit weiteres. Das weitere besteht darin, dass C++-Funktionen mittels C-Bibliotheken gewrapped werden können. Und in meinem letzten Blogpost zu diesem Thema habe ich bereits erklärt, wie C-Bibliotheken in Go angesprochen werden können. Sprich, es braucht nur eine hauchdünne C-Schicht zwischen C++ und Go.

Multilingual++ in der Praxis

Wie auch letztes mal habe ich schon mal was vorbereitet – eine Schnittstelle für die Boost.Regex-Bibliothek. Diese findet sich in diesem GitHub-Repo und besteht aus folgenden Komponenten:

  • Ein Struct, das boost::basic_regex<char> wrapped
  • Eine C-Bibliothek, die den C++-Teil wrapped
  • Die Go-Bibliothek, die die C-Bibliothek verwendet

Wrapper-Struct

Dieses Struct ist Notwendig, da die Größe von boost::basic_regex<char> zwar C++ bekannt ist, aber nicht Go. Das Wrapper-Struct hingegen hat eine feste Größe (ein Zeiger).

libcxx/regex.hpp

#pragma once

#include <boost/regex.hpp>
// boost::basic_regex
// boost::match_results
// boost::regex_search

#include <utility>
// std::forward
// std::move

template<class Char>
struct Regex
{
	template<class... Args>
	inline
	Regex(Args&&... args) : Rgx(new boost::basic_regex<Char>(std::forward<Args>(args)...))
	{
	}

	Regex(const Regex& origin) : Rgx(new boost::basic_regex<Char>(*origin.Rgx))
	{
	}

	Regex& operator=(const Regex& origin)
	{
		Regex copy (origin);
		return *this = std::move(copy);
	}

	inline
	Regex(Regex&& origin) noexcept : Rgx(origin.Rgx)
	{
		origin.Rgx = nullptr;
	}

	inline
	Regex& operator=(Regex&& origin) noexcept
	{
		this->~Regex();
		new(this) Regex(std::move(origin));
		return *this;
	}

	inline
	~Regex()
	{
		delete this->Rgx;
	}

	template<class Iterator>
	bool MatchesSomewhere(Iterator first, Iterator last) const
	{
		boost::match_results<Iterator> m;
		return boost::regex_search(first, last, m, *(const boost::basic_regex<Char>*)this->Rgx);
	}

	boost::basic_regex<Char>* Rgx;
};

Eine C-Bibliothek, die den C++-Teil wrapped

Die folgenden Funktionen sind zwar waschechte C++-Funktionen, aber dank dem extern "C" werden sind sie in der Bibliothek als C-Funktionen sichtbar und können von Go angesprochen werden.

libcxx/regex.cpp

#include "regex.hpp"
// Regex

#include <boost/regex.hpp>
using boost::bad_expression;

#include <stdint.h>
// uint64_t

#include <utility>
using std::move;

extern "C" unsigned char CompileRegex(uint64_t pattern_start, uint64_t pattern_end, uint64_t out)
{
	try {
		*(Regex<char>*)out = Regex<char>((const char*)pattern_start, (const char*)pattern_end);
	} catch (const boost::bad_expression&) {
		return 2;
	} catch (...) {
		return 1;
	}

	return 0;
}

extern "C" void FreeRegex(uint64_t rgx)
{
	try {
		Regex<char> r (move(*(Regex<char>*)rgx));
	} catch (...) {
	}
}

extern "C" signed char MatchesSomewhere(uint64_t rgx, uint64_t subject_start, uint64_t subject_end)
{
	try {
		return ((const Regex<char>*)rgx)->MatchesSomewhere((const char*)subject_start, (const char*)subject_end);
	} catch (...) {
		return -1;
	}
}

libcxx/regex.h

#pragma once

#include <stdint.h>
// uint64_t

unsigned char CompileRegex(uint64_t pattern_start, uint64_t pattern_end, uint64_t out);

void FreeRegex(uint64_t rgx);

signed char MatchesSomewhere(uint64_t rgx, uint64_t subject_start, uint64_t subject_end);

Go-Bibliothek

Diese spricht letztendlich die C-Funktionen an. Dabei übergibt sie die Zeiger als Ganzzahlen, um gewisse Sicherheitsmaßnahmen von CGo zu umgehen. Das Regex-Struct entspricht dem Regex-Struct aus dem C++-Teil.

regex.go

package boostregex2go

import (
	"io"
	"reflect"
	"runtime"
	"unsafe"
)

/*
#include "libcxx/regex.h"
// CompileRegex
// FreeRegex
// MatchesSomewhere
*/
import "C"

type OOM struct {
}

var _ error = OOM{}

func (OOM) Error() string {
	return "out of memory"
}

type BadPattern struct {
}

var _ error = BadPattern{}

func (BadPattern) Error() string {
	return "bad pattern"
}

type Regex struct {
	rgx unsafe.Pointer
}

var _ io.Closer = (*Regex)(nil)

func (r *Regex) Close() error {
	C.FreeRegex(rgxPtr64(r))
	return nil
}

func (r *Regex) MatchesSomewhere(subject []byte) (bool, error) {
	defer runtime.KeepAlive(subject)
	start, end := bytesToCharRange(subject)

	switch C.MatchesSomewhere(rgxPtr64(r), start, end) {
	case 0:
		return false, nil
	case 1:
		return true, nil
	default:
		return false, OOM{}
	}
}

func NewRegex(pattern []byte) (*Regex, error) {
	rgx := &Regex{}

	defer runtime.KeepAlive(pattern)
	start, end := bytesToCharRange(pattern)

	switch C.CompileRegex(start, end, rgxPtr64(rgx)) {
	case 0:
		return rgx, nil
	case 2:
		return nil, BadPattern{}
	default:
		return nil, OOM{}
	}
}

func bytesToCharRange(b []byte) (C.uint64_t, C.uint64_t) {
	sh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
	return C.uint64_t(sh.Data), C.uint64_t(sh.Data + uintptr(sh.Len))
}

func rgxPtr64(p *Regex) C.uint64_t {
	return C.uint64_t(uintptr(unsafe.Pointer(p)))
}

Fazit++

Wenn etwas abgedrehtes mal nicht zu gehen scheint, dann gebe ich doch nicht auf, sondern ich mache es einfach noch abgedrehter. Impossible is nothing.

Wenn Du auch lernen willst, wie man unmögliches möglich macht, komm auf unsere Seite der Macht.

Alexander Klimov
Alexander Klimov
Senior Developer

Alexander hat 2017 seine Ausbildung zum Developer bei NETWAYS erfolgreich abgeschlossen. Als leidenschaftlicher Programmierer und begeisterter Anhänger der Idee freier Software, hat er sich dabei innerhalb kürzester Zeit in die Herzen seiner Kollegen im Development geschlichen. Wäre nicht ausgerechnet Gandhi sein Vorbild, würde er von dort aus daran arbeiten, seinen geheimen Plan, erst die Abteilung und dann die Weltherrschaft an sich zu reißen, zu realisieren - tut er aber nicht. Stattdessen beschreitet er mit der...

Ansible + Icinga 2 = #monitoringlove

Nein, ich bin nicht auf den Hype als solchen mit aufgesprungen. Es ist nur so… Ansible ist extrem nützlich. Und genau das werde ich anhand eines konkreten Beispiels aufzeigen: Ein Icinga 2 Cluster, vollständig aufgesetzt mit Ansible.

Vorkenntnisse

Wer noch nicht weiß, was zum Geier ein Icinga 2 Cluster und/oder Ansible ist, sollte sich vor dem Weiterlesen entsprechend einarbeiten, am besten im Rahmen einer entsprechenden Schulung in unserem Hause:

Alternativ ginge auch das wesentlich mühseligere Selbststudium:

Umgebung

So ein bisschen sensationell soll der Cluster schon sein, aber mit zunehmenden VMs wird es auch zunehmend eng auf meinem Arbeitsgerät. Wie praktisch, dass wir bei NWS einen OpenStack haben, dem ich mal schnell mit Terraform 14 VMs reindrücken kann (2 Master, 2 Satelliten und 10 Agents):

resource "openstack_compute_instance_v2" "aklimov-icinga2" {
	count = 14
	name = "aklimov-icinga2-${count.index}"
	image_name = "Centos 7"
	flavor_name = "s1.small"
	network { name = "${var.tenant_network}" }
	security_groups = [ "default", "Icinga" ]
	key_pair = "${var.openstack_keypair}"
}

Mit einem kleinen Skript in der Sprache meiner Wahl erstelle ich mir anhand der Terraform-Daten mehr oder weniger automatisch ein Ansible-Inventory (inventory.txt):

aklimov-icinga2-5 ansible_host=10.77.27.54 ansible_user=centos
aklimov-icinga2-4 ansible_host=10.77.27.45 ansible_user=centos
aklimov-icinga2-7 ansible_host=10.77.27.51 ansible_user=centos
aklimov-icinga2-9 ansible_host=10.77.27.40 ansible_user=centos
aklimov-icinga2-12 ansible_host=10.77.27.28 ansible_user=centos
aklimov-icinga2-13 ansible_host=10.77.27.18 ansible_user=centos
aklimov-icinga2-10 ansible_host=10.77.27.12 ansible_user=centos
aklimov-icinga2-1 ansible_host=10.77.27.34 ansible_user=centos
aklimov-icinga2-6 ansible_host=10.77.27.20 ansible_user=centos
aklimov-icinga2-3 ansible_host=10.77.27.49 ansible_user=centos
aklimov-icinga2-11 ansible_host=10.77.27.19 ansible_user=centos
aklimov-icinga2-0 ansible_host=10.77.27.21 ansible_user=centos
aklimov-icinga2-2 ansible_host=10.77.27.46 ansible_user=centos
aklimov-icinga2-8 ansible_host=10.77.27.50 ansible_user=centos

Auf die Plätze, fertig, Ansible!

Zunächst verifiziere ich die Funktionalität der VMs und sammle nebenbei deren öffentliche SSH-Schlüssel ein:

ansible all -i inventory.txt -m ping \
--ssh-common-args='-o StrictHostKeyChecking=no'

Ist das getan, geht es auch schon los mit dem Ansible-Playbook (playbook.yml):

---
- name: Icinga 2
  hosts: all
  become: yes
  become_method: sudo
  tasks:
  - name: EPEL repo
    yum:
      name: epel-release
  - name: Icinga repo key
    rpm_key:
      key: https://packages.icinga.com/icinga.key
  - name: Icinga repo
    yum:
      name: https://packages.icinga.com/epel/icinga-rpm-release-7-latest.noarch.rpm
  - name: Icinga 2 package
    yum:
      name: icinga2
  - name: Monitoring plugins
    yum:
      name: nagios-plugins-all
  - name: Icinga 2 service
    service:
      name: icinga2
      state: started
      enabled: yes
# (...)

Ein paar Repositories, ein paar Pakete – und Icinga ist auch schon einsatzbereit. Fehlt nur noch der eigentliche Cluster:

---
- name: Icinga 2
# (...)
- name: Icinga PKI master
  hosts: aklimov-icinga2-0
  become: yes
  become_method: sudo
  tasks:
  - name: Icinga 2 cluster setup
    shell: >
      icinga2 node setup
      --zone master
      --listen 0.0.0.0,5665
      --cn {{ inventory_hostname }}
      --master
      --disable-confd;
      rm -f /var/cache/icinga2/icinga2.vars
    args:
      creates: /var/lib/icinga2/certs/ca.crt
    notify: Restart Icinga 2
  - name: /var/cache/icinga2/icinga2.vars
    shell: icinga2 daemon -C
    args:
      creates: /var/cache/icinga2/icinga2.vars
  - name: Icinga 2 PKI ticket
    with_inventory_hostnames:
    - 'all:!{{ inventory_hostname }}'
    shell: >
      icinga2 pki ticket --cn {{ item }}
      >/var/cache/icinga2/{{ item }}.ticket
    args:
      creates: '/var/cache/icinga2/{{ item }}.ticket'
  - name: Fetch Icinga 2 PKI ticket
    with_inventory_hostnames:
    - 'all:!{{ inventory_hostname }}'
    fetch:
      dest: .tempfiles
      src: '/var/cache/icinga2/{{ item }}.ticket'
  - name: Fetch Icinga 2 master cert
    fetch:
      dest: .tempfiles
      src: '/var/lib/icinga2/certs/{{ inventory_hostname }}.crt'
  handlers:
  - name: Restart Icinga 2
    service:
      name: icinga2
      state: restarted
# (...)

Zuerst wird der Knoten eingerichtet, der allen anderen Tickets und später SSL/TLS Zertifikate ausstellt. Dann werden auch schon die Tickets ausgestellt, nachdem /var/cache/icinga2/icinga2.vars dafür neu erstellt wurde. Diese Tickets und das Zertifikat des Knotens werden danach in das Verzeichnis .tempfiles heruntergeladen. Die anderen Knoten brauchen diese Dateien, um sich automatisch an den Cluster anzuschließen:

---
- name: Icinga 2
# (...)
- name: Icinga PKI master
# (...)
- name: Icinga cluster nodes
  hosts: 'all:!aklimov-icinga2-0'
  become: yes
  become_method: sudo
  tasks:
  - name: Icinga master cert
    copy:
      dest: /var/cache/icinga2/trusted.crt
      owner: icinga
      group: icinga
      mode: '0644'
      src: .tempfiles/aklimov-icinga2-0/var/lib/icinga2/certs/aklimov-icinga2-0.crt
  - name: Icinga PKI ticket
    copy:
      dest: /var/cache/icinga2/my.ticket
      owner: icinga
      group: icinga
      mode: '0600'
      src: '.tempfiles/aklimov-icinga2-0/var/cache/icinga2/{{ inventory_hostname }}.ticket'
  - name: Icinga 2 cluster setup
    shell: >
      icinga2 node setup
      --zone {{ inventory_hostname }}
      --endpoint aklimov-icinga2-0,{{ hostvars['aklimov-icinga2-0'].ansible_all_ipv4_addresses[0] }},5665
      --parent_host {{ hostvars['aklimov-icinga2-0'].ansible_all_ipv4_addresses[0] }},5665
      --parent_zone master
      --listen 0.0.0.0,5665
      --ticket `cat /var/cache/icinga2/my.ticket`
      --trustedcert /var/cache/icinga2/trusted.crt
      --cn {{ inventory_hostname }}
      --accept-config
      --accept-commands
      --disable-confd
    args:
      creates: /var/lib/icinga2/certs/ca.crt
    notify: Restart Icinga 2
  handlers:
  - name: Restart Icinga 2
    service:
      name: icinga2
      state: restarted

Und ab dafür:

ansible-playbook -i inventory.txt playbook.yml

Voilà!

(...)

PLAY RECAP *******************************************************************************************************
aklimov-icinga2-0          : ok=14   changed=12   unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
aklimov-icinga2-1          : ok=12   changed=10   unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
aklimov-icinga2-10         : ok=12   changed=10   unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
aklimov-icinga2-11         : ok=12   changed=10   unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
aklimov-icinga2-12         : ok=12   changed=10   unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
aklimov-icinga2-13         : ok=12   changed=10   unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
aklimov-icinga2-2          : ok=12   changed=10   unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
aklimov-icinga2-3          : ok=12   changed=10   unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
aklimov-icinga2-4          : ok=12   changed=10   unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
aklimov-icinga2-5          : ok=12   changed=10   unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
aklimov-icinga2-6          : ok=12   changed=10   unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
aklimov-icinga2-7          : ok=12   changed=10   unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
aklimov-icinga2-8          : ok=12   changed=10   unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
aklimov-icinga2-9          : ok=12   changed=10   unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Jetzt fehlt nur noch die Icinga-Konfiguration. Aber diese überlasse ich keinem geringeren als dem Icinga Director.

Fazit

Was man nicht alles von A bis Z automatisieren kann!

Viel Spaß beim Nachmachen in der NWS Cloud!

Alexander Klimov
Alexander Klimov
Senior Developer

Alexander hat 2017 seine Ausbildung zum Developer bei NETWAYS erfolgreich abgeschlossen. Als leidenschaftlicher Programmierer und begeisterter Anhänger der Idee freier Software, hat er sich dabei innerhalb kürzester Zeit in die Herzen seiner Kollegen im Development geschlichen. Wäre nicht ausgerechnet Gandhi sein Vorbild, würde er von dort aus daran arbeiten, seinen geheimen Plan, erst die Abteilung und dann die Weltherrschaft an sich zu reißen, zu realisieren - tut er aber nicht. Stattdessen beschreitet er mit der...

Generics waren gestern. Lang lebe Golangs Reflection!

Vor einiger Zeit habe ich die Programmiersprache Golang und all ihren Nutzen für die Entwickler vorgestellt. Zugegeben, eine in der Konkurrenz sehr verbreitete Funktionalität besitzt Go nicht: Generics. Und diese Funktionalität is noch dazu sehr gefragt. Allein das Gesamtbild der Reaktionen (Smileys) auf den Vorschlag, Generics in Go v2 zu integrieren, sagt mehr als 1000 Worte. Aber es geht auch anders…

Problem

Aktuell arbeite ich an einem (streng geheimen) Programm, das u.a. mit SQL-Datenbanken kommunizieren soll. Die grundsätzliche Infrastruktur dafür bringt Go von Haus aus mit. Jedoch kann es etwas mühselig sein, bei jeder Abfrage dieselbe Routine (samt Fehlerbehandlung) durchzukauen:

package blog

import (
	"database/sql"
)

type Employee struct {
	GivenName, FamilyName string
}

func GetEmployees(db *sql.DB) ([]Employee, error) {
	rows, errQuery := db.Query("SELECT given_name, family_name FROM employee")
	if errQuery != nil {
		return nil, errQuery
	}

	defer rows.Close()

	employees := []Employee{}

	for {
		if rows.Next() {
			row := Employee{}

			if errScan := rows.Scan(&row.GivenName, &row.FamilyName); errScan != nil {
				return nil, errScan
			}

			employees = append(employees, row)
		} else if errNext := rows.Err(); errNext == nil {
			break
		} else {
			return nil, errNext
		}
	}

	return employees, nil
}

Außerdem hat sich in einem vergangenen Projekt herausgestellt, dass (zumindest in Transaktionen) erst nach rows.Close() die nächste Datenbank-Operation beginnen kann. Dies verpflichtete fast schon dazu, den Code ab defer db.Close() bei jeder Abfrage so oder so ähnlich zu schreiben. Letztendlich löste das Team das Problem mit folgender Funktion:

func FetchAll(db *sql.DB, query string, args ...interface{}) ([][]interface{}, error)

Diese erledigte die oben gezeigte Routinearbeit und verringerte damit den Aufwand pro Abfrage deutlich:

func GetEmployees(db *sql.DB) ([]Employee, error) {
	rows, errFetchAll := FetchAll(db, "SELECT given_name, family_name FROM employee")
	if errFetchAll != nil {
		return nil, errFetchAll
	}

	employees := []Employee{}

	for _, row := range rows {
		employees = append(employees, Employee{row[0].(string), row[1].(string)})
	}

	return employees, nil
}

Jedoch war nun jede Spalte jeder Zeile des Ergebnisses ein interface{}, das erstmal in den richtigen Datentyp umgewandelt werden musste. Dafür wiederum musste die neue Funktion zusätzlich den Spaltentyp beim Datenbanktreiber erfragen, um immer die (hinter dem interface{} versteckten) Datentypen zurückzugeben, die die konkrete Abfrage erwartet. Andernfalls hätten wir uns auf die Standard-Datentypen der Datenbanktreiber verlassen müssen.

Lösung

Nun übernehme ich also den Code Schritt für Schritt in das neue Projekt und frage mich: Geht das nicht auch einfacher? Ja, mit sog. Reflection:

package blog

import (
	"database/sql"
	"reflect"
)

func FetchAll(db *sql.DB, rowType interface{}, query string, args ...interface{}) (interface{}, error) {
	rows, errQuery := db.Query(query, args...)

	if errQuery != nil {
		return nil, errQuery
	}

	defer rows.Close()

	blankRow := reflect.ValueOf(rowType)
	res := reflect.MakeSlice(reflect.SliceOf(blankRow.Type()), 0, 0)
	idx := -1
	scanDest := make([]interface{}, blankRow.NumField())

	for {
		if rows.Next() {
			res = reflect.Append(res, blankRow)
			idx++

			row := res.Index(idx)

			for i := range scanDest {
				scanDest[i] = row.Field(i).Addr().Interface()
			}

			if errScan := rows.Scan(scanDest...); errScan != nil {
				return nil, errScan
			}
		} else if errNext := rows.Err(); errNext == nil {
			break
		} else {
			return nil, errNext
		}
	}

	return res.Interface(), nil
}

Diese Funktion erwartet einen zusätzlichen Parameter, rowType. Dessen eigentlicher Typ hinter interface{} (Employee) bestimmt den Typ einer Zeile des Abfrage-Ergebnisses. Das komplette Ergebnis ist logischerweise eine Slice aus Zeilen ([]Employee). Mit Hilfe von Funktionen aus dem reflect-Paket arbeitet FetchAll() zur Laufzeit mit dem konkreten Datentyp Employee, fast so als wäre er mittels Generics zur Kompilierzeit bekannt:

  • reflect.ValueOf(rowType) analysiert rowType und kapselt ihn als Wert vom Typ Employee
  • reflect.ValueOf(rowType).Type() steht für Employee
  • reflect.SliceOf(Employee) steht für []Employee
  • reflect.MakeSlice([]Employee, 0, 0) steht für make([]Employee, 0, 0)
  • reflect.ValueOf(rowType).NumField() zählt die Felder des Structs Employee

Ja, richtig, rowType muss ein Struct sein, sonst stürzt das Programm spätestens bei reflect.ValueOf(rowType).NumField() ab. Jedes Feld des Structs steht nämlich für eine Spalte des Abfrage-Ergebnisses. Genau das wird in der darauf folgenden Schleife wie folgt bewerkstelligt:

  • res = reflect.Append(res, reflect.ValueOf(rowType)) steht für res = append(res, Employee{})
  • res.Index(idx) steht für res[idx]
  • res[idx].Field(0) steht für res[idx].GivenName
  • res[idx].GivenName.Addr() steht für &res[idx].GivenName

Und .Interface() holt letztendlich den Zeiger auf das Struct-Feld aus der Reflection-Versenkung, damit rows.Scan() die entsprechende Spalte des Abfrage-Ergebnisses darin speichert. Am Ende verbirgt sich hinter res tatsächlich ein []Employee, das mit res.Interface() in ein interface{} gekapselt, um es zurückzugeben. Damit bestimmt GetEmployees() den Zeilen-Typ im voraus und schrumpft auf ein vernünftiges Minimum:

func GetEmployees(db *sql.DB) ([]Employee, error) {
	rows, errFetchAll := FetchAll(db, Employee{}, "SELECT given_name, family_name FROM employee")
	if errFetchAll != nil {
		return nil, errFetchAll
	}

	return rows.([]Employee), nil
}

Fazit

Nachdem ich zuletzt schon eine C-Bibliothek in Go wiederverwendet habe, spare ich schon zum zweiten mal in Folge Code und damit Zeit. Sprich, wir arbeiten jetzt noch ein bisschen effizienter (als sowieso schon) an euren Projekten. Bestelle noch heute!

Alexander Klimov
Alexander Klimov
Senior Developer

Alexander hat 2017 seine Ausbildung zum Developer bei NETWAYS erfolgreich abgeschlossen. Als leidenschaftlicher Programmierer und begeisterter Anhänger der Idee freier Software, hat er sich dabei innerhalb kürzester Zeit in die Herzen seiner Kollegen im Development geschlichen. Wäre nicht ausgerechnet Gandhi sein Vorbild, würde er von dort aus daran arbeiten, seinen geheimen Plan, erst die Abteilung und dann die Weltherrschaft an sich zu reißen, zu realisieren - tut er aber nicht. Stattdessen beschreitet er mit der...

CGo. Halb C – halb Go.

In meinem letzten Blog-Post, The way to Go, habe ich bereits einige Vorteile der Programmiersprache Go aufgezeigt. Und als ob es nicht schon genug wäre, dass man beim Umstieg auf Go nicht gleich das Kind mit dem Bade ausschütten muss: Ein einzelnes Programm muss nicht mal komplett in Go geschrieben sein…

Multilingual unterwegs

Ich arbeite für einen deutschen IT-Dienstleister, aber beim recherchieren von Lösungen für Kundenprobleme muss ich die russischen Quellen nicht erst ins Deutsche übersetzen, sondern kann direkt mit ihnen arbeiten. Genau so kann bspw. ein Go-Programm direkt von C-Bibliotheken Gebrauch machen – und umgekehrt. Vorausgesetzt, die Schnittstellen beschränken sich auf den kleinsten gemeinsamen Nenner.

Was wäre das auch wirtschaftlich für ein Fass ohne Boden wenn sämtliche benötigte Bibliotheken erst in Go neu geschrieben werden müssten. Umso mehr wundert es mich, dass so manche Datenbank-Treiber doch komplett neu geschrieben wurden…

Good programmers know what to write.
Great ones know what to rewrite (and reuse).

Eric Steven Raymond, The Cathedral and the Bazaar

Multilingual in der Praxis

Ich bin zwar kein Fernsehkoch, aber habe trotzdem schon mal was vorbereitet – eine extrem abgespeckte Version von go-linux-sensors:

package go_linux_sensors

/*
#include <stdio.h>
#include <string.h>
#include <sensors/sensors.h>
#include <sensors/error.h>

typedef const char cchar;

#cgo LDFLAGS: -lsensors
*/
import "C"

import (
	"reflect"
	"unsafe"
)

type Error struct {
	errnr C.int
}

func (e Error) Error() string {
	return cStr2str(C.sensors_strerror(e.errnr))
}

func Init() error {
	if err := C.sensors_init((*C.FILE)(nil)); err != C.int(0) {
		return Error{err}
	}

	return nil
}

func Cleanup() {
	C.sensors_cleanup()
}

func GetLibsensorsVersion() string {
	return cStr2str(C.libsensors_version)
}

func cStr2str(cStr *C.cchar) string {
	slen := int(C.strlen(cStr))

	bridge := reflect.SliceHeader{
		Data: uintptr(unsafe.Pointer(cStr)),
		Len:  slen,
		Cap:  slen,
	}

	return string(*(*[]byte)(unsafe.Pointer(&bridge)))
}

Diese schlägt eine Brücke zu libsensors, um die Hardware-Sensoren auszulesen.

Direkt nach dem Package folgt auch schon der Import des ominösen Pakets “C”. Dieses enthält sämtliche C-Standard-Datentypen und alle Datentypen und Funktionen, die über Header-Dateien im Kommentar direkt darüber eingebunden wurden. Ja, richtig, in diesem “magischen” Kommentar lässt sich nahezu beliebiger C-Code unterbringen, um auf jenen im Go-Code zuzugreifen. Des weiteren lassen sich die tatsächlichen Bibliotheken mit “#cgo LDFLAGS:” automatisch linken. Bei höheren Mengen an Code/Includes bevorzuge ich persönlich eine extra Header-Datei statt einem langen Kommentar.

Etwas weiter unten ruft Init() auch schon sensors_init() auf – als wäre es eine gewöhnliche Go-Funktion im Paket C. Und auch die Datentypen lassen sich verwenden als seien es die eigenen – wovon Error fleißig Gebrauch macht.

Lediglich cStr2str() macht einen etwas unorthodoxen Eindruck… aber hey – es funktioniert. 😉

Jeder gesparte Euro zählt…

… und jede wiederverwendete Zeile Code. Wir bei NETWAYS wissen das zu schätzen und arbeiten dementsprechend effizient – an euren Projekten!

Alexander Klimov
Alexander Klimov
Senior Developer

Alexander hat 2017 seine Ausbildung zum Developer bei NETWAYS erfolgreich abgeschlossen. Als leidenschaftlicher Programmierer und begeisterter Anhänger der Idee freier Software, hat er sich dabei innerhalb kürzester Zeit in die Herzen seiner Kollegen im Development geschlichen. Wäre nicht ausgerechnet Gandhi sein Vorbild, würde er von dort aus daran arbeiten, seinen geheimen Plan, erst die Abteilung und dann die Weltherrschaft an sich zu reißen, zu realisieren - tut er aber nicht. Stattdessen beschreitet er mit der...

Trainings

Web Services

Events