Blog
Go lernen - einfache, zuverlässige und effiziente Software erstellen

In meinem Blog AWS Lambda mit Golang haben wir einen AWS Lambda erstellt, der Go-Code ausführt. Wir haben den Lambda erstellt, verpackt und ausgeführt. Diesmal werfen wir einen Blick auf die Programmiersprache Go und einige der Funktionen, die die Go-Standardbibliothek bietet. Auf diese Weise erhalten wir ein gutes Verständnis dafür, was Go bietet und warum die Sprache so beliebt ist.
In diesem Blog gehe ich davon aus, dass der Leser Erfahrung mit der Programmierung in anderen Programmiersprachen wie Java oder Python hat, daher werde ich nur kleine Beispiele zeigen, ohne sie zu sehr zu erklären. Lassen Sie uns einen Blick darauf werfen!
Eine beliebte Sprache
Viele bekannte Open-Source-Projekte sind in Go programmiert, wie Docker, Kubernetes, Etcd, Influx DB, Traefik. Go wird von Amazon Web Services (AWS) und Google Cloud Platform (GCP) unterstützt, um Webanwendungen und im Falle von AWS auch Lambda-Funktionen zu erstellen.
Geschichte
Go ist eine von Google entwickelte Programmiersprache. Version 1 wurde im März 2012 veröffentlicht. Zum Zeitpunkt der Erstellung dieses Artikels ist die neueste Version von Go v1.11, die im August 2018 veröffentlicht wurde. Go ist eine statisch typisierte, kompilierte Sprache. Der Compiler erzeugt eine statische Binärdatei, die ohne eine virtuelle Maschine ausgeführt werden kann. Wie andere verwaltete Sprachen wie Java und c-sharp hat Go die Vorteile der Speichersicherheit und der Garbage Collection. Go wurde von Robert Griesemer, Rob Pike und Ken Thompson entwickelt.
Installieren von Go
Die Installation von Go ist auf Mac oder Linux ganz einfach. Auf dem Mac geben Sie einfach 'brew install go' ein. Unter Linux geben Sie, je nach Distribution, 'sudo yum install golang.x86_64' ein.
Go lernen
Es gibt eine Vielzahl von Ressourcen, um Go zu lernen. Die Tour of Go bietet eine Online-Tour durch die Sprache. Die Dokumentation von Go ist eine der besten, die ich je gesehen habe, und sie ist sehr zugänglich. Es gibt viele Bücher zum Erlernen von Go wie Go By Example und An introduction to programming in Go. Es gibt viele kostenlose Youtube-Videos wie GopherCon.
Die Go CLI
Die Programmiersprache Go hat einen einzigen CLI-Befehl namens 'go'. Die folgenden Befehle sind wichtig:
- go version: gibt die Version von go aus und beendet sich
- go run: ein Programm ausführen
- go build: Kompiliert das Programm zu einer statischen Binärdatei
- go fmt: formatiert die Go-Quelldateien
- go env: gibt die Go-Umgebungsvariablen und Exits aus
- go get: Quelldateien herunterladen und installieren
- go test: Tests ausführen
$ go version
go version go1.11.2 darwin/amd64
# get help for a command
$ go help get
$ go env GOPATH
/Users/dennis/go
Der Go-Arbeitsbereich
Go verwendet Arbeitsbereiche, um Codebasen zusammenzufassen. Ein Arbeitsbereich ist ein Verzeichnis, auf das der 'GOPATH' zeigt. Standardmäßig verweist der 'GODIR' auf '~/go'. Wenn Sie 'go env GOPATH' eingeben, sehen Sie, was der aktuelle Arbeitsbereich ist. Sie können den Standard-Arbeitsbereich ändern, indem Sie 'GOPATH' mit Hilfe von Umgebungsvariablen auf ein anderes Verzeichnis setzen.
Der Go-Arbeitsbereich ist eine Ansammlung von Codebases. Das bedeutet, dass der Arbeitsbereich ein einzelnes Projekt oder mehrere Projekte enthalten kann. Das Ergebnis eines Builds auf dem Arbeitsbereich ist ein binäres Artefakt. Das binäre Artefakt ist in den meisten Fällen die Aggregation der Codebasen aus dem Arbeitsbereich. Dies bedeutet einen einzigen Abhängigkeitsgraphen, der im Arbeitsbereich verfügbar ist. Erstellen Sie mehrere Arbeitsbereiche, wenn Sie Codebasen voneinander trennen müssen, d.h. einen anderen Abhängigkeitsgraphen.
Für diesen Blog verwenden wir den Standardarbeitsbereich, der auf '~/go' verweist. Ein Arbeitsbereich hat die folgenden Verzeichnisse:
.
├── bin
└── src
└── github.com
└── dnvriend
Sie legen Ihre Quelldateien im Verzeichnis 'src/github.com/dnvriend/blog-examples' ab. Das Verzeichnis 'blog-examples' enthält ein '.git'-Verzeichnis und wird auf github hochgeladen. Die binären Artefakte werden im Verzeichnis 'bin' gespeichert.
Hallo Welt
Wir beginnen mit einem Hello World-Beispiel. Erstellen Sie die Datei 'src/github.com/dnvriend/blog-examples/main.go' und fügen Sie den folgenden Code ein. Um das Beispiel auszuführen, geben Sie 'go run src/github.com/dnvriend/blog-examples/main.go' ein. Um eine statische Binärdatei zu erstellen, geben Sie 'go install src/github.com/dnvriend/blog-examples/main.go' ein.
package main
import (
"fmt"
"log"
)
func main() {
log.Println("Hello World!") // write to stdlog
fmt.Println("Hello World!") // write to stdout
println("Hello World!") // write to stderr
}
Primitive Typen
Go unterstützt die folgenden primitiven Typen:
- boolean
- int, int8, int16, int32, int64
- float32, float64
- Byte
- komplex64, komplex128
- String
Sehen wir uns an, wie Sie diese Typen verwenden können:
package main
import (
"fmt"
)
func main() {
const a bool = true
const b int = 42
const c float32 = 3.1415
const d string = "Dennis"
const e complex64 = 12 + 5i
fmt.Println(a, b, c, d, e)
}
Es gibt eine kürzere Notation, die nur in Funktionen funktioniert:
package main
import (
"fmt"
)
func main() {
a := true
b := 42
c := 3.1415
d := "Dennis"
e := 12 + 5i
fmt.Println(a, b, c, d, e)
}
Funktionen
Funktionen verwenden das Schlüsselwort 'func' und funktionieren so, wie Sie es erwarten würden.
package main
import (
"fmt"
)
// a function
func world() string {
return "World"
}
// higher order function
func hello(f func() string) string {
// apply the function 'f'
return "Hello " + f() + "!"
}
func main() {
// pass the function reference to hello
fmt.Println(hello(world))
}
Importe
Wir haben bereits Importe gesehen. Lassen Sie uns das Paket 'math' importieren.
package main
import (
"math"
"fmt"
)
func main() {
pi := math.Pi
circum := pi*6
surface := math.Pow(6, 2) * pi / 4
fmt.Printf("circum %f surface %f", circum, surface)
}
Wir können auch unser eigenes Paket erstellen und es importieren. Legen wir in unserem Projektverzeichnis ein Verzeichnis namens 'formulas' an und erstellen Sie die Datei 'circle.go':
package formulas
import "math"
func Circum(d float64) float64 {
return math.Pi*d
}
func Surface(d float64) float64 {
return math.Pow(d, 2) * math.Pi / 4
}
Lassen Sie uns das Paket importieren:
package main
import (
"fmt"
"github.com/dnvriend/blog-examples/formulas"
)
func main() {
d := 6.25342
fmt.Printf("circum %f surface %f", formulas.Circum(d), formulas.Surface(d))
}
Versuchen Sie, die Groß- und Kleinschreibung von Circum und Surface in 'circle.go' zu ändern, was passiert?
Exportierte Bezeichner
Ein Identifikator kann exportiert werden, um den Zugriff auf ihn von einem anderen Paket aus zu ermöglichen. Ein Bezeichner wird exportiert, wenn beide:
- das erste Zeichen des Namens des Identifikators ist ein Großbuchstabe,
- der Bezeichner ist im Paketblock deklariert oder es handelt sich um einen Feldnamen oder einen Methodennamen.
Kontrollfluss
package main
import (
"fmt"
)
func main() {
x := 2
if x == 1 {
fmt.Println("One")
} else if x == 2 {
fmt.Println("Two")
} else {
fmt.Println("Something else")
}
switch x {
case 1:
fmt.Println("One")
case 2:
fmt.Println("Two")
default:
fmt.Println("Something else...")
}
}
Machen Sie
Die Funktion 'make' erstellt nur Slices, Maps und Channels und gibt eine initialisierte Version des genannten Objekts zurück. Slices, Maps und Channels müssen vor der Verwendung initialisiert werden.
package main
import (
"fmt"
)
func main() {
xs := make([]int, 3)
xs[0] = 1
xs[1] = 2
kv := make(map[string]string)
kv["foo"] = "bar"
kv["baz"] = "quz"
fmt.Printf("xs is %d, and ys is %s", xs, kv)
}
Sammlungen
Go unterstützt die Sammlungen Array, Slice und Map.
package main
import (
"fmt"
)
func main() {
// array
xs := [2]int{1, 2}
// slice
ys := []int{1, 2, 3}
// map
kv := map[string]string {"foo":"bar", "baz":"quz", "abc":"def"}
fmt.Printf("xs=%d, ys=%d, zs=%s", xs, ys, kv)
}
Schleifen
Go kann Schleifen ausführen, über Sammlungen und Bereiche iterieren:
package main
import (
"fmt"
)
func main() {
for i := 0; i < 5; i++ {
fmt.Println("Counter:", i)
}
xs := []int{1, 2, 3, 4}
for i, e := range xs {
fmt.Printf("i=%d and e=%dn", i, e)
}
kv := map[string]string {"foo":"bar", "baz":"quz"}
for k, v := range(kv) {
fmt.Printf("k=%s, v=%sn", k, v)
}
}
Fehlerbehandlung: Aufschieben, Panik und Wiederherstellung
Go verfügt nicht über Ausnahmeregelungen wie try, catch, finally, sondern löst den Umgang mit Fehlern auf eine interessante Weise]. Go verwendet die Funktionen panic und recover, um Fehler zu behandeln, und das Schlüsselwort defer, um eine Funktion auf den Stack zu legen, die immer nach der Ausführung des Stacks aufgerufen wird.
package main
import (
"fmt"
)
func main() {
defer func() {
if err := recover(); err != nil {
// handle the error
fmt.Println("Handling error: ", err)
// 'throw' ie. 'panic' with a new message
panic("My Error Message")
} else {
fmt.Println("Nothing happened")
}
}()
xs := []int{}
fmt.Println("xs:", xs[0])
}
Zeiger
Go arbeitet mit Zeigern, d.h. mit Primitiven, die auf einen Speicherplatz zeigen. Zeiger haben in Go einen Nutzen, denn immer wenn Sie eine Methode mit einer Objektreferenz aufrufen, wird das Objekt kopiert. Da Go schnelle und effiziente Anwendungen ermöglicht, können Sie für Werte, die einen großen Speicherbedarf haben, auch einen Zeiger übergeben, der das Objekt nicht kopiert.
Im folgenden Beispiel erhält die Funktion 'addOne' einen Zeiger. Um den zugrunde liegenden Wert zu ändern, muss der Zeiger mit dem Operator '*' dereferenziert werden.
package main
import (
"fmt"
)
func addOne(x *int) {
*x = *x + 2
}
func main() {
x := 1
addOne(&x)
fmt.Println("x is", x)
}
Goroutinen
Go unterstützt Threads, die als 'Goroutinen' bezeichnet werden. Threads sind sehr einfach zu erstellen. Setzen Sie einfach das Schlüsselwort 'go' vor den Namen einer Funktion, wenn Sie diese aufrufen. Das Beispiel verwendet das sync-Paket und nutzt eine WaitGroup, um auf die Beendigung einer Sammlung von goroutines zu warten. Eine bessere Möglichkeit, mit Threads zu arbeiten, sind Kanäle.
package main
import (
"fmt"
"time"
"sync"
)
var wg sync.WaitGroup
func say(msg string) {
for i := 0; i < 3; i++ {
fmt.Printf("%s World!n", msg)
time.Sleep(time.Millisecond * 100)
}
wg.Done()
}
func main() {
wg.Add(1)
go say("Hello")
wg.Add(1)
go say("Goodbye")
wg.Wait()
}
Kanäle
Kanäle sind die Leitungen, die Goroutinen verbinden. Sie können Werte von einer Goroutine in Kanäle senden und diese Werte in einer anderen Goroutine empfangen.
package main
import (
"fmt"
"time"
)
func pinger(ping chan string, pong chan string) {
for {
fmt.Println(<-ping)
time.Sleep(time.Millisecond * 300)
pong <- "pong"
}
}
func ponger(ping chan string, pong chan string) {
for {
fmt.Println(<-pong)
time.Sleep(time.Millisecond * 300)
ping <- "ping"
}
}
func main() {
done := make(chan bool)
ping := make(chan string, 10)
pong := make(chan string, 10)
go pinger(ping, pong)
go ponger(ping, pong)
ping <- "ping"
<-done
fmt.Println("Done")
}
Logger
Go bietet Unterstützung für die Protokollierung von Nachrichten. Das Paket log definiert den Logger, der Ausgabezeilen erzeugt.
package main
import (
"log"
)
func main() {
log.Println("Normal")
log.Fatal("Fatal")
// log and then panic
log.Panic("Panic")
}
Zufallszahlengenerator
Go bietet einen Pseudo-Zufallszahlengenerator im Paket math/rand.
package main
import (
"fmt"
"math/rand"
)
func main() {
for i := 0; i < 5; i++ {
x := rand.Intn(100)
y := rand.Float64() * 5
fmt.Println(x, y)
}
}
UUID
Go verfügt nicht über eine eingebaute UUID-Funktion. Glücklicherweise hat Google UUID als
$ go get github.com/google/uuid
Um zufällige UUIDs zu erzeugen, geben Sie ein:
package main
import (
"fmt"
"github.com/google/uuid"
)
func main() {
for i := 0; i< 5; i++ {
id, _ := uuid.NewRandom()
fmt.Println(id.String())
}
}
Dateien schreiben
Go bietet Unterstützung für das Schreiben von Dateien. Das Paket
Schreibt eine Zeichenkette in eine Datei:
package main
import (
"fmt"
"os"
)
func check(e error) {
if e != nil {
panic(e)
}
}
func main() {
f, err := os.Create("/tmp/test.txt")
check(err)
defer f.Close()
n3, err := f.WriteString("Hello World!n")
check(err)
fmt.Printf("wrote %d bytesn", n3)
f.Sync()
}
Schreiben Sie Bytes in eine Datei:
package main
import (
"fmt"
"io/ioutil"
"os"
"bufio"
)
func check(e error) {
if e != nil {
panic(e)
}
}
func main() {
// write bytes to a file
data := []byte("Hello There!n")
err := ioutil.WriteFile("/tmp/test1.dat", data, 0644)
check(err)
// write bytes to a file
data2 := []byte{115, 111, 109, 101, 10}
file2, err := os.Create("/tmp/test2.dat")
defer file2.Close()
check(err)
n, err := file2.Write(data2)
fmt.Printf("wrote %d bytesn", n)
check(err)
file2.Sync()
// write bytes to a file more efficiently
file3, err := os.Create("/tmp/test3.dat")
defer file3.Close()
writer := bufio.NewWriter(file3)
n2, err := writer.WriteString("bufferedn")
fmt.Printf("wrote %d bytesn", n2)
writer.Flush()
}
Dateien lesen
Go bietet Unterstützung für das Lesen von Dateien. In dem folgenden Beispiel lesen wir die Dateien, die wir im Beispiel 'Dateien schreiben' erstellt haben.
package main
import (
"fmt"
"io/ioutil"
"os"
"bufio"
)
func check(e error) {
if e != nil {
panic(e)
}
}
func main() {
// read the whole file
data, err := ioutil.ReadFile("/tmp/test1.dat")
check(err)
fmt.Print(string(data))
// read 5 bytes
f, err := os.Open("/tmp/test1.dat")
defer f.Close()
check(err)
b1 := make([]byte, 5)
n1, err := f.Read(b1)
check(err)
fmt.Printf("%d bytes: %sn", n1, string(b1))
// start reading from the 6th byte
o2, err := f.Seek(6, 0)
check(err)
b2 := make([]byte, 6)
n2, err := f.Read(b2)
check(err)
fmt.Printf("%d bytes @ %d: %sn", n2, o2, string(b2))
// set the file pointer to the beginning
_, err = f.Seek(0, 0)
check(err)
r4 := bufio.NewReader(f)
// read 5 bytes without advancing the reader
b4, err := r4.Peek(5)
check(err)
fmt.Printf("5 bytes: %sn", string(b4))
}
CSV lesen und schreiben
Go bietet Unterstützung für das Lesen und Schreiben von CSV-Dateien.
CSV schreiben:
package main
import (
"encoding/csv"
"os"
"log"
"bufio"
)
func main() {
records := [][]string{
{"first_name", "last_name", "username"},
{"Rob", "Pike", "rob"},
{"Ken", "Thompson", "ken"},
{"Robert", "Griesemer", "gri"},
}
// write a record to console
f, _ := os.Create("/tmp/names.csv")
defer f.Close()
cw := csv.NewWriter(bufio.NewWriter(f))
for _, record := range records {
if err := cw.Write(record); err != nil {
log.Fatal("error writing record to csv:", err)
}
}
cw.Flush()
}
CSV lesen:
package main
import (
"encoding/csv"
"os"
"bufio"
"fmt"
)
func main() {
// write a record to console
f, _ := os.Open("/tmp/names.csv")
defer f.Close()
cr := csv.NewReader(bufio.NewReader(f))
names, _ := cr.ReadAll()
for _, row := range names {
for i, field := range row {
fmt.Println(i, field)
}
}
}
Komprimierung
Go bietet Unterstützung für die Komprimierung und Dekomprimierung von Daten. Go unterstützt
GZIP schreiben:
package main
import (
"compress/gzip"
"os"
)
func check(e error) {
if e != nil {
panic(e)
}
}
func main() {
f, e1 := os.Create("/tmp/test.gz")
defer f.Close()
check(e1)
gz := gzip.NewWriter(f)
defer func() {
gz.Flush()
gz.Close()
}()
_, e2 := gz.Write([]byte("Hello World!n"))
check(e2)
}
GZIP lesen:
package main
import (
"bytes"
"compress/gzip"
"fmt"
"io"
"os"
)
func check(e error) {
if e != nil {
panic(e)
}
}
func main() {
// var buf bytes.Buffer
f, e1 := os.Open("/tmp/test.gz")
defer f.Close()
check(e1)
gz, e2 := gzip.NewReader(f)
check(e2)
var buf bytes.Buffer
_, e3 := io.Copy(&buf, gz)
check(e3)
fmt.Print(string(buf.Bytes()))
}
Base64 Kodierung/Dekodierung
Go bietet Unterstützung für die base64-Kodierung.
package main
import (
"encoding/base64"
"fmt"
)
func main() {
msg := "Hello World!"
encoded := base64.StdEncoding.EncodeToString([]byte(msg))
fmt.Println(encoded)
enc := "SGVsbG8gV29ybGQh"
decoded, _ := base64.StdEncoding.DecodeString(enc)
fmt.Println(string(decoded))
}
Hashing
Go bietet Unterstützung für Hashing-Algorithmen. Go unterstützt crypto/md5, crypto/sha1, crypto/sha256, crypto/sha512.
md5 digest:
package main
import (
"crypto/md5"
"fmt"
)
func main() {
h := md5.New()
h.Write([]byte("Hello World!n"))
fmt.Printf("%x", h.Sum(nil))
}
sha-256 digest:
package main
import (
"crypto/sha256"
"fmt"
)
func main() {
h := sha256.New()
h.Write([]byte("Hello World!n"))
fmt.Printf("%x", h.Sum(nil))
}
Krypto
Go unterstützt verschiedene Verschlüsselungsalgorithmen wie crypto/aes, crypto/des, crypto/x509.
AES verschlüsseln:
package main
import (
"crypto/aes"
"fmt"
"io"
"crypto/rand"
"crypto/cipher"
"encoding/hex"
)
func main() {
key, _ := hex.DecodeString("6368616e676520746869732070617373776f726420746f206120736563726574")
plaintext := []byte("Hello World!")
block, _ := aes.NewCipher(key)
nonce := make([]byte, 12)
io.ReadFull(rand.Reader, nonce)
aesgcm, _ := cipher.NewGCM(block)
ciphertext := aesgcm.Seal(nil, nonce, plaintext, nil)
fmt.Printf("Nonce: %xn", nonce)
fmt.Printf("Cipher: %xn", ciphertext)
}
AES entschlüsseln:
package main
import (
"crypto/aes"
"fmt"
"crypto/cipher"
"encoding/hex"
)
func main() {
key, _ := hex.DecodeString("6368616e676520746869732070617373776f726420746f206120736563726574")
ciphertext, _ := hex.DecodeString("c8eaf641d7a25d32d489a4666d7bfb3fad2c8b482ffebc891076876a")
nonce, _ := hex.DecodeString("de3964e81c1a32bfc9b37b98")
block, _ := aes.NewCipher(key)
aesgcm, _ := cipher.NewGCM(block)
plaintext, _ := aesgcm.Open(nil, nonce, ciphertext, nil)
fmt.Printf("%sn", plaintext)
}
Datum und Uhrzeit
Go unterstützt Datum und Uhrzeit mit dem Paket time. Auch Text kann in Zeitobjekte umgewandelt werden.
package main
import (
"time"
"fmt"
)
func main() {
// now returns a Time
now := time.Now()
fmt.Println(now.Format(time.RFC3339))
fmt.Printf("%d-%02d-%02dT%02d:%02d:%02d-00:00n",
now.Year(), now.Month(), now.Day(),
now.Hour(), now.Minute(), now.Second())
parsed, _ := time.Parse(time.RFC3339, "2018-11-24T19:02:17+01:00")
fmt.Println(parsed)
}
JSON-Kodierung
Go bietet mit dem Paket encoding/json native Unterstützung für JSON-Kodierung.
package main
import (
"encoding/json"
"fmt"
)
type Person struct {
Name string json:"name"
Age int json:"age"
}
func main() {
person := Person { "Dennis", 42 }
bytes, _ := json.Marshal(person)
fmt.Println(string(bytes))
p := Person{}
json.Unmarshal(bytes, &p)
fmt.Println(p)
}
HTTP-Server
Go bietet sowohl einen HTTP-Client als auch einen Server im Paket net/http.
package main
import (
"io"
"net/http"
)
func rootHandler(w http.ResponseWriter, req *http.Request) {
io.WriteString(w, "Hello, world!n")
}
func main() {
http.HandleFunc("/", rootHandler)
http.ListenAndServe(":8080", nil)
}
HTTP-Client
Das Paket net/http bietet einen vollwertigen http-Client.
package main
import (
"net/http"
"fmt"
"io/ioutil"
)
func main() {
res, _ := http.Get("https://www.google.nl/robots.txt")
defer res.Body.Close()
robots, _ := ioutil.ReadAll(res.Body)
fmt.Printf("%s", robots)
}
Testen Sie
Go bietet ein Testpaket und einen Test-Runner. Tests werden durch die Eingabe von 'go test' ausgeführt, wodurch die Testfunktionen ausgeführt werden.
package calc
import (
"github.com/dnvriend/blog-examples/calc"
"testing"
)
func TestAddOne(t *testing.T) {
x := calc.AddOne(1)
if x != 2 {
t.Fatal("Does not add one")
}
}
Obwohl Go das Testen von Code unterstützt, benötigen Sie dennoch eine Bibliothek, um das Testen entwicklerfreundlicher zu gestalten. Die Bibliothek Testify bietet allgemeine Assertions, Mocks und Assertion-Fehler, wenn Tests fehlschlagen. Testify kann durch die Eingabe von 'go get github.com/stretchr/testify' installiert werden.
package calc
import (
"github.com/dnvriend/blog-examples/calc"
"github.com/stretchr/testify/assert"
"testing"
)
func TestAddOne(t *testing.T) {
assert.Equal(t, calc.AddOne(1), 2, "Does not add one")
}
Fazit
Go ist eine allgemeine Programmiersprache mit vollem Funktionsumfang zur Erstellung hocheffizienter, moderner Anwendungen. Die Standardbibliothek von Go bietet die meisten Funktionen, die moderne vernetzte Anwendungen benötigen. Go bietet HTTP-Client- und -Server-Implementierungen, Kryptographie, Komprimierung, Hashing-Algorithmen und JSON-Kodierungsfunktionen. Go unterstützt Goroutinen und Channels, um nebenläufige Anwendungen zu erstellen. Nächstes Mal werden wir uns ansehen, wie Sie CLI-Anwendungen mit Go erstellen.
Verfasst von
Dennis Vriend
Contact