Die erste Frage für Entwickler, die gerade erst anfangen, Go zu verwenden, sieht oft so aus: "Welches Framework sollte zur Lösung von Problem X verwendet werden?". Während dies eine ganz normale Frage ist, wenn Webanwendungen und Server in vielen anderen Sprachen geschrieben werden, sind im Fall von Go bei der Beantwortung dieser Frage viele Feinheiten zu berücksichtigen. Es gibt starke Argumente für und gegen die Verwendung von Frameworks in Go-Projekten. Während ich an Artikeln aus dieser Reihe arbeite, sehe ich mein Ziel als eine objektive, vielseitige Untersuchung dieses Themas.
Eine Aufgabe
Zunächst möchte ich sagen, dass ich hier von der Annahme ausgehe, dass der Leser mit dem Konzept des "REST-Servers" vertraut ist. Wenn Sie eine Auffrischung benötigen, schauen Sie sich dieses gute Material an (aber es gibt viele andere ähnliche Artikel). Von nun an gehe ich davon aus, dass Sie verstehen werden, was ich meine, wenn ich die Begriffe "Pfad", "HTTP-Header", "Antwortcode" und dergleichen verwende.
In unserem Fall ist der Server ein einfaches Backend-System für eine Anwendung, die Aufgabenverwaltungsfunktionen implementiert (wie Google Keep, Todoist und dergleichen). Der Server stellt Clients die folgende REST-API zur Verfügung:
POST /task/ : ID GET /task/<taskid> : ID GET /task/ : DELETE /task/<taskid> : ID GET /tag/<tagname> : GET /due/<yy>/<mm>/<dd> : ,
Beachten Sie, dass diese API speziell für unser Beispiel erstellt wurde. In den nächsten Abschnitten dieser Reihe werden wir über einen strukturierteren und standardisierteren Ansatz für das API-Design sprechen.
Unser Server unterstützt GET-, POST- und DELETE-Anforderungen, von denen einige mehrere Pfade verwenden können. Was in der API-Beschreibung in spitzen Klammern (
<...>
) angezeigt wird, bezeichnet Parameter, die der Client dem Server als Teil einer Anforderung zur Verfügung stellt. Beispielsweise wird die Anforderung
GET /task/42
angewiesen, eine Aufgabe vom Server mit zu empfangen
ID
42
.
ID
Sind eindeutige Aufgabenkennungen.
Die Daten werden im JSON-Format codiert. Bei der Ausführung einer Anfrage
POST /task/
Der Client sendet eine JSON-Darstellung der zu erstellenden Aufgabe an den Server. Ebenso enthalten die Antworten auf diese Anfragen, deren Beschreibung besagt, dass sie etwas "zurückgeben", JSON-Daten. Insbesondere werden sie in den Hauptteil der HTTP-Antworten eingefügt.
Der Code
Als nächstes werden wir uns Schritt für Schritt mit dem Schreiben des Servercodes in Go befassen. Die Vollversion finden Sie hier . Es ist ein in sich geschlossenes Go-Modul, das keine Abhängigkeiten verwendet. Nach dem Klonen oder Kopieren des Projektverzeichnisses auf den Computer kann der Server sofort ohne zusätzliche Installation Folgendes ausführen:
$ SERVERPORT=4112 go run .
Bitte beachten Sie, dass
SERVERPORT
Sie jeden Port verwenden können, der den lokalen Server überwacht, während Sie auf Verbindungen warten. Nachdem der Server über ein separates Terminalfenster gestartet wurde, können Sie beispielsweise mit einem Dienstprogramm damit arbeiten
curl
. Sie können auch mit anderen ähnlichen Programmen damit interagieren. Beispiele für Befehle zum Senden von Anforderungen an den Server finden Sie in diesem Skript . Das Verzeichnis, das dieses Skript enthält, enthält Tools für automatisierte Servertests.
Modell
Beginnen wir mit der Erörterung des Modells (oder der "Datenschicht") für unseren Server. Sie finden es im Paket
taskstore
(
internal/taskstore
im Projektverzeichnis). Dies ist eine einfache Abstraktion, die eine Datenbank darstellt, in der Aufgaben gespeichert sind. Hier ist seine API:
func New() *TaskStore
// CreateTask .
func (ts *TaskStore) CreateTask(text string, tags []string, due time.Time) int
// GetTask ID. ID -
// .
func (ts *TaskStore) GetTask(id int) (Task, error)
// DeleteTask ID. ID -
// .
func (ts *TaskStore) DeleteTask(id int) error
// DeleteAllTasks .
func (ts *TaskStore) DeleteAllTasks() error
// GetAllTasks .
func (ts *TaskStore) GetAllTasks() []Task
// GetTasksByTag , ,
// .
func (ts *TaskStore) GetTasksByTag(tag string) []Task
// GetTasksByDueDate , , ,
// .
func (ts *TaskStore) GetTasksByDueDate(year int, month time.Month, day int) []Task
Hier ist eine Typdeklaration
Task
:
type Task struct {
Id int `json:"id"`
Text string `json:"text"`
Tags []string `json:"tags"`
Due time.Time `json:"due"`
}
Das Paket
taskstore
implementiert diese API mithilfe eines einfachen Wörterbuchs
map[int]Task
und speichert die Daten im Speicher. Eine datenbankgesteuerte Implementierung dieser API ist jedoch nicht schwer vorstellbar. In einer realen Anwendung
TaskStore
handelt es sich höchstwahrscheinlich um eine Schnittstelle, die von verschiedenen Backends implementiert werden kann. Für unser einfaches Beispiel reicht diese API jedoch aus. Wenn Sie üben möchten, implementieren Sie
TaskStore
etwas wie MongoDB.
Server für die Arbeit vorbereiten
Die Funktion
main
unseres Servers ist ganz einfach:
func main() {
mux := http.NewServeMux()
server := NewTaskServer()
mux.HandleFunc("/task/", server.taskHandler)
mux.HandleFunc("/tag/", server.tagHandler)
mux.HandleFunc("/due/", server.dueHandler)
log.Fatal(http.ListenAndServe("localhost:"+os.Getenv("SERVERPORT"), mux))
}
Nehmen
NewTaskServer
wir uns etwas Zeit für das Team und sprechen dann über den Router und die Pfadhandler.
NewTaskServer
Ist ein Konstruktor für unseren Server vom Typ
taskServer
. Der Server enthält
TaskStore
Informationen, die hinsichtlich des gleichzeitigen Datenzugriffs sicher sind .
type taskServer struct {
store *taskstore.TaskStore
}
func NewTaskServer() *taskServer {
store := taskstore.New()
return &taskServer{store: store}
}
Routing- und Pfadhandler
Kommen wir nun zum Routing zurück. Dies verwendet den im Paket enthaltenen Standard-HTTP-Multiplexer
net/http
:
mux.HandleFunc("/task/", server.taskHandler)
mux.HandleFunc("/tag/", server.tagHandler)
mux.HandleFunc("/due/", server.dueHandler)
Der Standard-Multiplexer hat eher bescheidene Fähigkeiten. Dies ist sowohl seine Stärke als auch seine Schwäche. Seine Stärke ist, dass es sehr einfach ist, damit umzugehen, da es nichts Schwieriges in seiner Arbeit gibt. Und die Schwäche des Standard-Multiplexers besteht darin, dass seine Lösung manchmal das Lösen des Problems des Abgleichs von Anforderungen mit den im System verfügbaren Pfaden ziemlich mühsam macht. Was nach der Logik der Dinge schön wäre, an einem Ort zu sein, muss man an verschiedenen Orten platzieren. Wir werden in Kürze mehr darüber sprechen.
Da der Standardmultiplexer nur die exakte Zuordnung von Anforderungen zu Pfadpräfixen unterstützt, sind wir praktisch gezwungen, uns nur auf die Stammpfade auf der obersten Ebene zu verlassen und die Aufgabe, den genauen Pfad zu finden, an die Pfadhandler zu delegieren.
Lassen Sie uns den Pfadhandler untersuchen
taskHandler
:
func (ts *taskServer) taskHandler(w http.ResponseWriter, req *http.Request) {
if req.URL.Path == "/task/" {
// "/task/", ID.
if req.Method == http.MethodPost {
ts.createTaskHandler(w, req)
} else if req.Method == http.MethodGet {
ts.getAllTasksHandler(w, req)
} else if req.Method == http.MethodDelete {
ts.deleteAllTasksHandler(w, req)
} else {
http.Error(w, fmt.Sprintf("expect method GET, DELETE or POST at /task/, got %v", req.Method), http.StatusMethodNotAllowed)
return
}
Wir beginnen mit der Überprüfung der genauen Übereinstimmung des Pfads mit
/task/
(was bedeutet, dass es nicht am Ende ist
<taskid>
). Hier müssen wir verstehen, welche HTTP-Methode verwendet wird, und die entsprechende Servermethode aufrufen. Die meisten Pfadhandler sind ziemlich einfache API-Wrapper
TaskStore
. Schauen wir uns einen dieser Handler an:
func (ts *taskServer) getAllTasksHandler(w http.ResponseWriter, req *http.Request) {
log.Printf("handling get all tasks at %s\n", req.URL.Path)
allTasks := ts.store.GetAllTasks()
js, err := json.Marshal(allTasks)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(js)
}
Es löst zwei Hauptaufgaben:
- Empfängt Daten von model (
TaskStore
). - Generiert eine HTTP-Antwort für den Client.
Beide Aufgaben sind recht einfach und unkompliziert. Wenn Sie jedoch den Code anderer Pfadhandler untersuchen, können Sie feststellen, dass sich die zweite Aufgabe in der Regel wiederholt. Sie besteht darin, JSON-Daten zu sammeln, den richtigen HTTP-Antwortheader vorzubereiten und in andere ähnliche Aktionen ausführen. ... Wir werden dieses Problem später noch einmal ansprechen.
Gehen wir jetzt zurück zu
taskHandler
. Bisher haben wir nur gesehen, wie Anforderungen mit einer genauen Pfadübereinstimmung behandelt werden
/task/
. Was ist mit dem Weg
/task/<taskid>
? Hier kommt der zweite Teil der Funktion ins Spiel:
} else {
// ID, "/task/<id>".
path := strings.Trim(req.URL.Path, "/")
pathParts := strings.Split(path, "/")
if len(pathParts) < 2 {
http.Error(w, "expect /task/<id> in task handler", http.StatusBadRequest)
return
}
id, err := strconv.Atoi(pathParts[1])
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if req.Method == http.MethodDelete {
ts.deleteTaskHandler(w, req, int(id))
} else if req.Method == http.MethodGet {
ts.getTaskHandler(w, req, int(id))
} else {
http.Error(w, fmt.Sprintf("expect method GET or DELETE at /task/<id>, got %v", req.Method), http.StatusMethodNotAllowed)
return
}
}
Wenn die Abfrage nicht genau mit dem Pfad übereinstimmt
/task/
, erwarten wir, dass das numerische
ID
Problem dem Schrägstrich folgt . Der obige Code analysiert diesen
ID
und ruft den entsprechenden Handler auf (basierend auf der HTTP-Anforderungsmethode).
Der Rest des Codes ähnelt mehr oder weniger dem, den wir bereits behandelt haben. Er sollte leicht zu verstehen sein.
Serververbesserung
Nachdem wir nun eine grundlegende funktionierende Version des Servers haben, ist es an der Zeit, über mögliche Probleme nachzudenken und diese zu verbessern.
Eines der von uns verwendeten Programmierkonstrukte, das offensichtlich verbessert werden muss und über das wir bereits gesprochen haben, ist der sich wiederholende Code zum Aufbereiten von JSON-Daten beim Generieren von HTTP-Antworten. Ich habe eine separate Version des Servers erstellt, stdlib-factorjson , mit der dieses Problem behoben wird. Ich habe diese Serverimplementierung in einen separaten Ordner unterteilt, um den Vergleich mit dem ursprünglichen Servercode zu vereinfachen und die Änderungen zu analysieren. Die Hauptinnovation dieses Codes wird durch die folgende Funktion dargestellt:
// renderJSON 'v' JSON , , w.
func renderJSON(w http.ResponseWriter, v interface{}) {
js, err := json.Marshal(v)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(js)
}
Mit dieser Funktion können wir den Code aller Pfadhandler neu schreiben und verkürzen. So sieht der Code jetzt beispielsweise aus
getAllTasksHandler
:
func (ts *taskServer) getAllTasksHandler(w http.ResponseWriter, req *http.Request) {
log.Printf("handling get all tasks at %s\n", req.URL.Path)
allTasks := ts.store.GetAllTasks()
renderJSON(w, allTasks)
}
Eine grundlegendere Verbesserung wäre, den Code für die Zuordnung von Anforderungen zu Pfaden sauberer zu gestalten und diesen Code nach Möglichkeit an einem Ort zu sammeln. Während der derzeitige Ansatz zum Abgleichen von Anforderungen und Pfaden das Debuggen erleichtert, ist der dahinter stehende Code auf den ersten Blick schwer zu verstehen, da er über mehrere Funktionen verteilt ist. Angenommen, wir versuchen herauszufinden, wie eine Anforderung
DELETE
an a gerichtet ist
/task/<taskid>
. Gehen Sie dazu folgendermaßen vor:
- - —
main
,/task/
taskHandler
. - ,
taskHandler
,else
, ,/task/
.<taskid>
. - —
if
, , , , ,DELETE
deleteTaskHandler
.
Sie können diesen gesamten Code an einem Ort ablegen. Es wird viel einfacher und bequemer sein, damit zu arbeiten. Genau darauf zielen HTTP-Router von Drittanbietern ab. Wir werden im zweiten Teil dieser Artikelserie darüber sprechen.
❒ Dies ist der erste Teil einer Reihe zur Entwicklung von Go-Servern. Sie können die Artikelliste am Anfang des Originals dieses Materials anzeigen .